在程序清单 1 中,信号函数与主函数都会访问 crit_value这一临界资源,于是在访问这一共享资源之前,用锁进行访问的互斥(实际上,printf函数也是不可重入的,也意味着主程序与信号函数对它的访问同样需要注意临界区的问题,如果出于安全性的考虑,应该使用 write 替换 printf,在本文中它不是讨论的主要目的,请读者不必对 printf较真)。编译并运行该程序(编译时需要连接 rt 库,以及给 gcc 增加参数 -lrt),如果不做任何动作,则程序在 20 秒之后退出,并输出:
xxx@xxx-desktop:~$ ./test main thread started, use "kill -35 18283" to trigger dead lock. \ Otherwise, program will exit within 10secs. main thread, crit_value = 1. main thread, job done. |
若按照程序的指示在前 10 秒内用‘ kill ’向本程序发送信号 35(注,用户可能得到的信号值不一定是 35)。程序将无法自行退出,输出为:
xxx@xxx-desktop:~$ ./test & … xxx@xxx-desktop:~$ kill -35 18466 enter signal handler. If there is no exit message, a dead lock happened. |
此时程序进入死锁状态,需要用 kill或者 Ctrl+C强制使程序退出。该死锁发生的机制可以进行如下解释:如图 1 所示,当主程序请求并持有锁(sem_lock),开始做一些工作时。如果此时有信号的发生,主程序会被中断执行并跳转至信号函数 signal_test_func执行。进入信号函数之后,由于信号函数也需要访问共享资源,进而请求锁,由于锁仍然被主程序持有,信号函数就会一直等待锁的释放。然而,因为主程序的运行已经被信号函数抢占,在信号函数完成之前无法运行,也就无法继续执行解锁动作,于是信号函数在请求锁时变成了死等待。
图 1. 在信号函数与主函数间加锁
方案 1,使用测试加锁
如何解决这种问题呢?第一种思路可以将信号函数的加锁动作替换为测试加锁动作,例如使用:
int sem_trywait(sem_t *sem); int sem_timedwait(sem_t *restrict sem, const struct timespec *restrict abs_timeout); |
该函数是 sem_wait的非阻塞版本,如果加锁失败或超时则返回 -1。使用 sem_trywait修改函数 signal_test_func如下:
清单 2. 使用 sem_trywait 代替 sem_wait
void signal_test_func( int signo, siginfo_t * siginfo, void * ptr ) { … if ( sem_trywait( &semlock ) != 0 ) { msg = "exit signal handler. Lock failed\n"; write( 1, msg, strlen(msg)); return; } // do something here. crit_value = 0; printf( "signal handled, crit_value = %d. \n", crit_value ); sem_post( &semlock ); … } |