修改后的程序在运行时,无论信号发生在任何时刻都不会导致程序的死锁,信号函数在请求锁时一旦失败立即返回,而并不是死等在锁的请求上。当主程序有机会继续运行时,将锁进行释放,从而避免了死锁。执行结果如下:
xxx@xxx-desktop:~$ ./test & … xxx@xxx-desktop:~$ kill -35 18748 enter signal handler. If there is no exit message, a dead lock happened. exit signal handler. Lock failed main thread, crit_value = 1. xxx@xxx-desktop:~$ kill -35 18748 … signal handled, crit_value = 0. … |
前后两次发送信号发生在不同的阶段,第一个信号发生于主函数持有锁的过程中,信号函数由于未能获得锁而退出;第二个信号发生在主函数释放锁之后,信号函数成功获得锁。虽然这种方法避免了死锁,但却是以丢弃信号作为代价,因此这并不是一个好的解决办法,是否有更好的办法避免这个问题呢,下面引入第二节,来探讨这个问题。
方案 2,使用双线程处理信号与锁
参考 Linux 系统、NPTL 对实时信号的实现,当系统派发一个信号给一个进程时,会选择该进程的某个线程进行处理,前提是这个线程未屏蔽该信号。而被选中的线程将先中断自己的执行并跳转至信号函数执行,当信号函数执行完毕后,信号就被处理完毕,并进行释放,最后被中断的线程返回到中断处继续运行。但是,在被选中线程处理信号的过程中,其他线程并不会停止运行,而是和信号处理线程处于平行关系的执行顺序,与线程间的执行关系完全相同,即信号函数的执行空间是在线程内的。如图 2
图 2. 双线程时,信号的处理
这就给我们以一个提示,如果我们用 MainThread线程作为主线程运行,并使其屏蔽该信号,Thread1线程作为信号处理线程执行,专门用于信号函数的处理,并对 MainThread和 SignalHandler之间临界区的访问进行加锁,这样既避免死锁的问题,又避免了临界区访问重入问题,同时也避免了信号丢失。其中,需要使用到线程信号处理函数:
int pthread_sigmask(int how, const sigset_t *newmask, sigset_t *oldmask); |
该函数的作用与 sigprocmask 函数颇为类似,但它处理的范围仅限于调用线程。how指定了处理 signalmask的方法,可以为 SIG_SETMASK,SIG_BLOCK或者 SIG_UNBLOCK,顾名思义,SIG_SETMASK使用 newmask参数替换原有 signal mask;SIG_BLOCK将 newmask里面的 signal mask标志位置为 block状态;SIG_UNBLOCK与前者功能相反。oldmask里面用来存放替换前的 signal mask状态,如果程序不需要恢复原来的 signal mask状态,可将这个参数置为 NULL。
按照思路修改程序,其中关键程序片段如下:
清单 3. 使用 pthread_sigmask 屏蔽主线程接收信号
int main() { … if ( pthread_create( &Thread1_pt, NULL, &thread1_func, NULL ) != 0) perror( "Creating Child thread failed\n" ); // Blocking RT_TEST_SIG in Main Thread, always using thread1. sigset_t sigmask; sigemptyset( &sigmask ); sigaddset( &sigmask, RT_TEST_SIG ); // Should not use sigprocmask, it will block signals over process instead thread. pthread_sigmask( SIG_BLOCK, &sigmask, NULL ); … } |