最近在做一个嵌入式项目,涉及按键值的读取部分,进程的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),悲催啊,只能在自己这里找方法了。