最近在写一个linux程序,其中一个逻辑是要等待信号(signal),我使用了条件变量(condition variable)来完成这个操作,但是实际效果却是整个程序卡住了,经过查阅资料后得知在信号处理函数中是不能进行锁操作的。

1 问题现象

最初的情况是,我在信号处理函数中打印了日志,在收到信号时有机率会导致整个程序卡死。使用 pstack 观察程序的运行状态,发现程序卡在写日志的加锁操作中。由于程序是多线程的,所以为了保证在输出日志时线程间能够同步,在写日志时进行了加锁操作。然后我将写日志的语句注释掉后,程序就能够正常运行了。这样我开始怀疑是日志模块出了问题,但是经过反复排查后,并没有发现明显的问题,于是我选择暂时搁置这个问题。随着开发的推进,当我在信号处理函数中进行条件变量操作的时候,程序再一次卡死了,我开始怀疑是线程同步的相关操作中出现了问题。

2 信号处理函数中不进行锁操作

信号处理函数将会在同一个线程中执行,如果在信号处理函数中进行加锁,则可能会引起互锁。
如果必须要完成对锁的操作呢,妥协办法是在信号处理函数中创建一个线程,在该线程中进行锁相关的操作。

实际上,在信号处理函数中的操作要求是“可重入的”(reentrancy),有很多系统调用也是受限制的。以下是 可以 在信号处理函数中调用的系统函数。

access       alarm         cfgetispeed   cfgetospeed    cfsetispeed   cfsetospeed
chdir        chmod         chown         close          creat         dup
dup2         execle        execve        _exit          fcntl         fork
fstat        getgroups     getpgrp       getpid         getppid       getuid
kill         link          lseek         mkdir          mkfifo        open
pathconf     pause         pipe          read           rename        rmdir
setgid       setpgid       setsid        setuid         sigaction     sigaddset
sigdelset    sigemptyset   sigfillset    sigismember    sigpending    sigprocmask
sigsuspend   sleep         stat          sysconf        tcdrain       tcflow
tcflush      tcgetattr     tcgetpgrp     tcsendbreak    tcsetattr     tcsetgrp
time         times         umask         uname          unlink        utime
wait         waitpid       write

3 题外话:使用 sigwaitsigtimedwait 等待信号

在需要等待某个信号(Linux signal),可以采用 sigwaitsigtimedwait 来实现。这两个函数会阻塞当前线程的执行以等待即将到来的信号。

思考: sigwait 如何应对有多个信号需要等待,每个信号的所对应的处理流程都不一样,而且信号接收的顺序可能有变化。

解决方法:可以用 sigaction 接口指定处理每个信号的回调函数,这些回调函数可以使用同一个函数作为入口,在函数中对信号进行区分和处理。但是 sigaction 并不是阻塞函数,并不能达到等待信号的效果,所以我们需要使用其他的同步机制(如:条件变量)达到阻塞的目的。

updated 2020/10/09:
其他问题:可以使用 sigtimedwait 等待即将到来的信号,但有可能在开始等待之前已经错过等待的信号。特别是在一些异步流程中,例如在进行一些操作后需要等待某个信号,在等待时需要思考信号到来时机的问题。可以单独启动一个线程等待指定的信号,这样就不会错过信号了,但在信号到来时会随机选择一个线程执行信号处理,这就需要为每个线程设置合理的 signal mask 以确保信号能够交给该线程处理。