记一次代码小改进——读取按键与select、alarm等

      最近在做一个嵌入式项目,涉及按键值的读取部分,进程的CPU占用率比较高,达到50%以上,要改进一下。先用伪代码交代一下原来代码流程吧。

//在while(1)大循环中
//非阻塞方式
打开设备文件,读按键值 if(vKey != -1) //有按键值, 返回 { return vKey; } else { usleep(10000); //10ms //心跳 //时间显示(有秒) }

      从代码中很明显可以知道,CPU占用率高是因为休眠的时间太少,把时间调到100ms,CPU降到30%左右。但是随之有个问题,按键反应很迟钝。休眠时间和按键反应这两者之间有矛盾。

 

尝试改进一,select和timeout

思路就是,让进程阻塞在select函数,有数据就返回,能及时反应;如果没有数据,时间超时也会返回,就去做其他事情(心跳、时间显示等)。

tv.tv_sec = 0;
tv.tv_usec = 500000;  //500ms

int ret = select(fd_button+1, &rfds, NULL, NULL, &tv);

if(ret>0)
{
      n = read(fd_button, (char*)&bt, sizeof(BUTTON));
      //处理数据、返回键值
}
else
{
      //设置了timeout时间,不休眠了
      //心跳
      //时间显示
}

结果不如我所料,分两种情况:(1)非阻塞打开设备文件,不按按键,没有等超时,select直接返回ret == 1,读按键数据n == 0;(2)阻塞打开设备文件,不按按键,没有等超时,select直接返回ret == 1,读按键数据,进程阻塞。

很显然这都不是我想要,而为什么select没有等到超时再返回呢?

上网查了select函数的实现原理才知道,原来select需要驱动程序的支持,要实现fops内的poll函数。具体的实现原理可以参考:

Select函数实现原理分析 http://linux.chinaunix.net/techdoc/net/2009/05/03/1109887.shtml

 

尝试改进二,定时器alarm和信号

思路参考APUE第十章信号,涉及alarm函数,sigsetjmp和siglongjmp函数。具体思路就是,以阻塞方式打开设备文件,在读数据前设置定时器;当有数据读取时,读取数据,清除定时器;当没有数据读取,进程阻塞,如果定时器超时,从read阻塞中返回,进入信号处理函数,longjmp返回read之前,去做其他的事情。

//信息处理函数
 static void dealSigAlarm(int signo)
{
    longjmp(env_alrm,1);
}

//部分关键代码

struct sigaction alrmact;
 bzero(&alrmact,sizeof(alrmact));
 alrmact.sa_handler = dealSigAlarm;
 alrmact.sa_flags = SA_NOMASK;//使用SA_RESTART将会阻塞在read函数
 alrmact.sa_restorer = NULL;
 sigaction(SIGALRM,&alrmact,NULL);

if(setjmp(env_alrm) != 0)
{
    printf("setjmp return\n");
    return -1;
}

alarm(1);   //1秒,

if((n = read(fd_button, (char*)&bt, sizeof(BUTTON))) < 0)
 {
 …… 
}
 …… 
alarm(0);

 

结果还是不如我所愿,进程直接阻塞在read函数,也没有进入定时器的信号处理函数(实验时有打log)。

又上网查了很久,终于知道了原因:进程阻塞后,有两种睡眠状态(1)TASK_INTERRUPTIBLE(可中断的睡眠状态)(2)TASK_UNINTERRUPTIBLE(不可中断的睡眠状态)。所以这个按键驱动的read阻塞后极有可能是进入不可中断的睡眠状态,所以定时器的信号无法及时产生中断使进程恢复。从书上的一段话可以印证:在进程对某些硬件进行操作时(比如进程调用read系统调用对某个设备文件进行读操作,而read系统调用最终执行到对应设备驱动的代 码,并与对应的物理设备进行交互),可能需要使用TASK_UNINTERRUPTIBLE状态对进程进行保护,以避免进程与设备交互的过程被打断,造成 设备陷入不可控的状态。

参与网文:http://andylin02.iteye.com/blog/858708

              http://blog.csdn.net/li4850729/article/details/7554074

 

尝试改进三,修改终端控制特性

终端通常理解为平常登录的sell终端,进行输入输出的,不过我想串口或者按键也可以称为终端,作为输入。直接上一段网上的代码

struct termios termios;

tcgetattr(filedesc, &termios);
termios.c_lflag &= ~ICANON;   /* Set non-canonical mode */
termios.c_cc[VTIME] = 100;      /* Set timeout of 10.0 seconds */
termios.c_cc[VMIN] = 0;
tcsetattr(filedesc, TCSANOW, &termios);

这段代码要打开文件之后,read之前,进行设置。思路就是设置文件描述符的控制特性来跳出阻塞,VTIME设置超时,VMIN设置最少读入字节数,在此设置为0。那么当没有数据输入,定时器超时,read返回0,终止阻塞。

这方法也不错的,可惜我的尝试还是失败了,原因估计还是因为按键驱动进入了不可中断的睡眠状态。哎,超打击的…………

参考网文:

http://biancheng.dnbcw.info/c/430189.html

http://stackoverflow.com/questions/2917881/how-to-implement-a-timeout-in-read-function-call

 

尝试改进四,分而治之

通过超时来解决这个问题已经是不可能了,只好另找方法。上网查查CPU占用率高的解决方法,看了其他一些领域的处理方法,居然给我找到了灵感,CPU占用率果然降下来了,占有5%左右。我叫这个方法为:分而治之。

灵感来源:

记一次代码优化(大数据量处理及存储)http://inter12.iteye.com/blog/593572

修改代码后大概如此:

       //检查100次,每次休眠5ms(大概500ms,大部分在休眠)
        int i = 100;
        for(; i > 0; i--)
        {
            vKey = read_key();

            if(vKey != -1)    //有按键值, 跳出循环,准备去响应按键
            {
                return vKey;
            }
            else
            {
                usleep(5000);
            }
        }

        //到此for结束
        //心跳,显示时间等

这样做的思路就是,把休眠的时间粒度分小,提高响应的灵敏度;把检查次数增大,使进程尽可能地休眠,降低CPU占用率。

 

总结

经过测试,上面前三种方法对于标准输入STDIN_FILENO都是可行的。但是对于按键设备文件却不行。

虽然这个按键程序很小很小,不过五十行代码,但是却影响很大。在想办法改进的过程中,学到了不少知识,最终采取了一个不是方法的非常规方法,居然解决了,真是出乎预料。在工作的最后一天解决了一这个问题,感觉好爽,这周末是个好周末。

当然有人会问,为什么不改按键驱动以支持select函数。这就涉及到部门之间的协调问题,我们部门只做嵌入式软件,驱动是另一个部门做,发了邮件给相关负责人反应,没见有回复(可能是年末了,忙着项目XXX),悲催啊,只能在自己这里找方法了。

 

 

posted @ 2013-01-18 17:13  qinwanlin  阅读(1174)  评论(0编辑  收藏  举报