嵌入式复位流程与优化(2)

2019-04-14 19:34发布

背景

在前一篇《嵌入式复位流程研究与优化》中,主要针对之前复位流程中无脑关闭FD进行了优化。这几日,出现了一次复位流程未能走完的问题,一直阻塞在umount中不能出来。经过优化后,即使卡在umount中,也不会像以前那样控制台断开了(因为没有关闭控制台对应的套接字),保留了现场。经过对内核态堆栈的分析,我们认为应当是阻塞在了工作队列(work queue)上,在umount中,需要刷新cache到一个工作队列上,并等待该工作队列中的工作全部处理完,但是很遗憾,我们没有等到。板卡复位后,通过对日志的分析,发现有一个任务一直在占用CPU。目前,该问题我们可以在实验室稳定复现,可以证明时该问题导致了复位流程阻塞,未能执行完,导致复位不成功。
由于umount阻塞的问题不止一次出现,且原因并不相同,考虑到该问题严重影响设备稳定性,我们不能仅解决引起umount阻塞的问题,我们必须寻求办法来进行规避,使阻塞在umount中的问题,在任何情况下都不能出现。

分析与解决

解决该问题最直接且暴力的方法,就是另起一个线程,等待一定时间后直接无脑复位。但我一直认为这种修改比较别扭,未采用。主要原因如下:
(1) 需要pthread_create创建一个线程,还需要设置一大堆参数,优先级设置的不合适不定出现什么问题,麻烦;
(2) pthread_create需要指定一个入口函数,目前我们没有这样线程的入口函数。而且在新平台架构下,复位函数被绑定到了板卡配置的一个reset_func函数指针上。目前复位函数本身,包含了文件系统同步,而这个线程入口函数,则不能含有文件系统的同步。新写一个函数是肯定的了,而这个函数由需要直接操作寄存器,怎么写都觉得别扭。
直到今日翻《APUE》时,偶然看到了其中对于慢速I/O读超时的处理方法。书中的例子主要针对的是read函数,使用alarm函数来实现。alarm函数向系统注册一个alarm定时器,该定时器超时后,会向进程发送SIGALRM信号。在SIGALRM信号中,我们只需要使其跳出阻塞的系统调用即可。信号处理函数,会打断系统调用的执行,在处理完信号后,系统调用是否会重试,依赖于具体的操作系统。在我们的这种场景下,我们需要系统调用被打断后,不能重试,否则又会陷入到阻塞态。这里,我们就要使用setjmp以及longjmp,书中之前就多次提到这两个函数配合信号使用,达到打断系统调用的目的。先写了一个demo做试验,代码如下: #include #include #include #include static jmp_buf env_alrm; void sig_alrm(int signo) { printf("receive sig alarm. "); longjmp(env_alrm, 1); } int main() { if(signal(SIGALRM, sig_alrm) == SIG_ERR) { printf("fail to regist sig_alrm. "); } if(setjmp(env_alrm) != 0) { printf("saved by alarm. "); return 0; } alarm(10); while(1) { sleep(1); } } 代码十分简单,在main函数中,通过setjmp设置jump点,在收到alarm信号后,即可还原到该点,然后从main函数中退出。代码在调用alarm注册后,直接陷入无限循环。我们期望,alarm信号可以把这段代码从无限循环中拯救出来。运行结果如下: $ ./test4.o receive sig alarm. saved by alarm. 代码最终正常退出。这段代码对应到我们的项目中,main函数就可以理解成reboot_hook函数,在该函数内仅执行文件系统同步的功能,并不执行复位操作,我们可以通过alarm注册,保证该函数在一定时间内一定会返回,不会导致再次卡死在复位函数中。