信号就是通知某个进程发了某个事件,也称为软件中断。信号提供了一种处理异步事件的方法。信号通常是异步发生的,进程预先不知道信号准确发生的时刻。后端程序(daemon)往往需要提供7*24不间断的服务,因此,编程 daemon 程序时对信号的正确处理尤为重要。
下面和大家分享编写 daemon 程序时信号处理的注意事项,内容都来自 Internet,只是进行了整理和总结。关于信号的基础只是请参考 APUE。
常见的信号
SIGHUP 1 和终端的连接断开,发送该信号给控制进程。通常用此信号来通知 daemon 重新读取配置文件(因为 daemon 不会有控制终端,通常不会收到该信号)。
SIGINT 2 用户中断(Ctrl + C)。
SIGABRT 6 调用 abort 函数产生(通常是自杀)。
SIGKILL 9 可以杀死任意进程,不能被捕获或忽略(俗称酒杀)。
SIGSEGV 11 无效的内存引用(segmentation fault)。
SIGPIPE 13 对于 socket fd,当一个进程向某个已经收到 RST 的 fd 执行写操作时,内核会向该进程发送该信号。
SIGTERM 15 kill 命令发送的默认终止信号。
SIGCHLD 17 进程终止时向其父进程发送的信号。
SIGPROF 27 使用 gprof 工具测试时会收到该信号。
别用 signal,用 sigaction
可以通过 signal 或者 sigaction 函数来设置信号处理函数,但 signal 函数太过古老,因此推荐使用 sigaction。理由如下:
1. sigaction 可以提供更多接收到信号的信息。
2. 调用完信号处理函数后重新设置处理函数不会对 sigaction 有影响,因为 sigaction 默认是不会去重置处理函数的,同时在执行处理函数会屏蔽掉该信号,也不会有竞争。
3. signal 函数在某些系统中会默认重启被中断的系统调用,而 sigaction 默认不会这样做。
4. signal 函数在多线程环境中的行为是未定义的,必须使用 sigaction 函数。
数据传输和信号
使用了系统调用的函数都有可能被信号中断,立刻返回的函数(不需要等待I/O操作的完成或 sleep)不会被中断,而需要等待的函数(等待网络传输,管道的读或者 sleep)将会被中断,比如 select, read, connect。
在 daemon 程序中,恰当地处理被中断的系统调用是非常重要的。如果 read, write 等传输数据的函数被中断,必须处理这种情况并恢复数据的传输。有两种被中断的场景:
1. 当没有数据传输时就被中断,函数返回-1,这时可以通过判断 errno 的值来识别这种错误,如果 errno == EINTR,则表示函数在没有任何数据传输的情况下就被中断,这时可以通过同样的参数来再次调用该数据。
2. 另一种情况是数据传输已经在进行,但在没有完成之前被中断;这种情况下函数不会返回错误,而是返回一个小于期望大小的值,同时 errno 也不会有错误设置,想识别这种情况只能捕获导致中断的信号。在中断之后恢复数据传输时一定记得部分数据已经被传输,必须从正确的偏移再次发起传输。
不要通过 sigaction 函数设置 SA_RESTART 来处理被中断的系统调用。
多线程和信号
多线程程序的信号处理和单线程程序有很大的区别。根据 POSIX 规范,一个多线程的程序只有一个进程和一个 pid,哪个线程会被中断并处理到达的信号呢?有两种情况:
- 发送给进程的信号比如用 kill 向一个 pid 发送信号,每个线程都有单独的信号掩码,可以通过 pthread_sigmask 来设置。信号不会分发给已经屏蔽了该信号的线程,而是在没有屏蔽该信号的线程中任选一个来接收,但没有指定哪个线程一定可以接收到。如果所有的线程都屏蔽了该信号,该信号将在预处理队列中排队。
- 发送给线程的信号 pthread_kill 可以用来向指定的线程发送信号,分发和排队都是线程级别的。如果没有指定信号处理函数,而默认是结束进程,发送给线程的信号也会导致整个进程退出。
不要在信号处理函数中使用锁。
同步化信号处理 signalfd
signalfd 是一个在 linux kernel 2.6.22 提供的系统调用,功能是使用一个 fd 来接收信号。这样就可以同步地处理信号,也不需要设置处理函数。可以 man signalfd 查看示例程序。首先必须使用 sigprocmask 来屏蔽要使用 signalfd 来处理的信号,然后调用 signalfd 创建一个 fd 用来读取到达的信号。当被屏蔽的信号到达时,程序将不会被中断,也不会有处理函数被调用。信号会在 fd 中排队。signalfd 创建的 fd 可以和其他 fd 一样:可以放在 select, poll, epoll 中;可以设置为非阻塞;可以为不同的信号创建不同的 fd;在 fork 之后该 fd 也不会关闭掉,子进程同样可以读懂发送给父进程的信号。signalfd 非常适合在主循环中执行 epoll 处理大量连接的单进程网络服务程序中使用,信号的处理可以和其他 fd 一样加到 epoll 中。由于程序不会被中断,可以选择合适的时机才去处理信号。