前言
Linux 下的信号分为可靠信号和不可靠信号,或称为实时信号和非实时信号,对应于 Linux 的信号值为 1-31 和 34-64。对于他们的分类以及应用的时的区分并不在本文的讨论范围之内,读者可参考文献 1,对其应用做初步的了解。本文仅针对在应用实时信号处理函数时,如何解决其重入问题进行一些探索。
Linux 下的实时信号更类似于软件层次的“中断”,它可能发生在任何时刻,而这与程序的运行必然存在一定的冲突,即重入性问题。在一次应用 Linux RT 信号编写程序的过程中,碰见了这个问题,尽管这是个老话题,几乎所有的文章都强调,不要将不可重入的代码段置于信号处理函数中,但是由于特定场合的需要,在一个信号函数中无法避免地处理某个临界区。在一般情况下,在处理多线程临界区时,采用加、解锁的办法达到对临界区串行的访问目的,是最简单、实用的解决方法。那么有没有办法,通过使用的锁来解决信号函数的重入的问题呢?答案是可以的,但是在信号函数中要格外注意锁的正确用法。本文将针对这种较为特殊的状况,介绍相应的处理方案,由于 LinuxThread 和 NPTL 在信号处理上的不同,本文仅讨论 NPTL 的情况。
另外本文在提及的所有代码可在下载中获取。
信号函数与主程序间潜在的重入性问题
在 Linux 中,对于实时信号,推荐使用如下函数装载信号函数:
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact); |
其中,sigaction 的定义如下:
struct sigaction { void (*sa_handler)(int); void (*sa_sigaction)(int, siginfo_t *, void *); sigset_t sa_mask; int sa_flags; void (*sa_restorer)(void); }; |
对于实时信号而言,在实时信号抵达时,如果该信号不在 sa_mask指定的信号集之内,即信号未被屏蔽,信号函数 sa_sigaction才会被执行。反之,如果实时信号存在于 sa_mask指定的信号集中,该信号会被加入到该进程的信号队列中,直到进程解除对该信号的屏蔽。由于实时信号的默认动作是打出该信号值,并退出当前进程,用户还需根据自己的实际情况屏蔽不必要的信号,可以使用下面方法在 sa_mask里面添加信号:
struct sigaction act; sigemptyset( & act.sa_mask); // empty signal set, sigaddset( & act.sa_mask, SIGRTMIN+1 ); // add SIGRTMIN+1 to set. sigaddset( & act.sa_mask, SIGRTMIN+2 ); |
…
在默认情况下,在 sa_flags中不会设置 SA_NODEFER选项,当进程正在处理信号时,若再有新信号(相同信号)抵达,该信号会加入到进程的信号队列中,直到进程的信号处理过程结束;反之,程序会立即处理新信号。如果用户确定自己的信号函数可重入,也可以在 sa_flags里面设置 SA_NODEFER选项,此选项可以使得信号函数在收到信号时被立即执行。但是当到达的信号达到一定的频度之后,程序将因为无法及时处理信号函数而导致信号函数套嵌,积累到一定程度就会出现问题。
在默认的情况下,当内核派发信号到这个程序时,信号函数会按照串行化方式被执行。由于信号的发生是非预期的,它可能在主程序运行到任何时刻发生,而信号函数的执行总是以中断当前主程序的运行为代价的,也可以认为,执行信号的进程与主程序进程本身就是同一个进程,但是它们并非是按照一定顺序顺次执行的。设想,如果主程序正在处理一个临界区,而到来的信号函数也要处理同一个临界区,就会面临类似于多线程程序的重入性问题。在普通的多线程应用中,各个线程之间的关系是并行的。我们一般可以用锁来解决这种问题,但是,如果简单的将这种方案引入信号函数处理过程中,就会出现一个比较讨厌的问题——死锁:
清单 1. 信号函数与主程序间的死锁
void signal_test_func( int signo, siginfo_t * siginfo, void * ptr ) { … sem_wait( &semlock ); // acquire lock // do something here. crit_value = 0; printf( "signal handled, crit_value = %d. \n", crit_value ); sem_post( &semlock ); // release lock … } int main() { // create semaphone lock init value = 1 … // link signal handler … sem_wait( &semlock ); // acquire lock // use sleep to simulate main thread is doing some work. crit_value = 1; sleep(10); printf( "main thread, crit_value = %d. \n", crit_value ); sem_post( &semlock ); // release lock sleep(10); printf( "main thread, job done. \n", crit_value ); } |