试用一下CSDN博客的在线markdown编辑器,感觉还不错:)
信号概念
信号是一种软件中断,用于提供异步事件处理机制。以下情形会产生信号:
- 终端键盘输入,比如Ctrl+c(SIGINT)。
- 硬件异常,比如除零&浮点数溢出(SIGFPE),非法内存引用(SIGSEGV)等。硬件探测到异常后通知内核,内核向应用进程发送信号。
- kill函数或者kill命令给指定进程/进程组发送各种信号(不局限于杀死进程);abort函数发送SIGABRT信号导致进程非正常退出等。
- 软件产生信号,比如网络连接中的带外数据(SIGURG),向管道写入数据但无读取方(SIGPIPE),闹钟定时器alarm超时(SIGALARM),还有前一章中作业控制中的一些场景也会产生信号,子进程退出向父进程发送信号(SIGCHLD,可以在处理SIGCHLD信号时使用wait),Ctrl+z停止前台进程组(SIGTSTP),后台进程组需要进行输入/输出(SIGTTIN/SIGTTOU)等。
对信号的处理方式有忽略(不能忽略SIGKILL、SIGSTOP和硬件异常)、默认动作(大部分都是终止进程,部分还会产生core)和设置自定义处理函数三种。
signal函数
void (*signal(int signo, void (*func)(int))(int);
或者:
typedef void Sigfunc(int);
Sigfunc *signal(int, Sigfunc *);
#define SIG_ERR (void (*)())-1
#define SIG_DFL (void (*)())0 默认动作
#define SIG_IGN (void (*)())1 无视信号
signal函数用于对信号signo设置信号函数func,返回之前的信号处理函数。signal函数在ISO C中定义,不涉及到多进程,定义够简明+含糊,所以基本上没什么用,一般用sigaction(后面介绍)
不可靠信号处理
早期UNIX系统中信号机制是不可靠的,主要存在以下几个问题:
- 每次信号处理时,信号的处理方式会被重置为默认动作。常见的处理方式是在信号处理函数中重新设置,但如果信号发生在重新设置生效之前,信号会被丢失掉。
- 不能某些时候选择阻塞信号,只能选择SIG_IGN。
自动重启被中断的系统调用
一些“低速”系统调用在信号发生时,可能会被中断,比如被阻塞的对管道或网络设备的读写操作、pause函数和某些ioctl函数。为了减少以下检查代码,操作系统对ioctl、read、write和wait等系统调用都会进行自动重启。
again:
if ((n = read(fd, buf, BUFFSIZE)) < 0) {
if (errno == EINTR)
goto again; /* just an interrupted system call */
}
下表总结了使用signal和sigaction函数时,各系统是否重置默认动作、能否阻塞信号和自动重启动被中断的系统调用等行为。
可重入函数
因为信号处理函数是异步的,信号处理时中断程序正在运行的方法,所以要求在信号处理函数中被调用的方法具有可重入性(reentrant)或异步信号安全性(async-signalsafe),很多函数都不具备可重入性,比如:
- 使用了static数据结构(静态数据结构可能在主程序中被覆写)。
- 调用了malloc或者free(malloc中维护了已分配区域链表,被中断时可能正处于交换链表的过程中)。
- 标准I/O库(使用了全局数据结构)
可靠信号
几个术语:
- generated:事件发生,生成信号。
- delivered:信号已经传递到进程,开始被处理
- pending:未决,信号介于genaerated和delivered之间
- blocking:进程可以选择阻塞信号,被阻塞的信号处于pending状态,直到进程unbloc信号或者设置SIG_IGN(是的,只要在delivered之前都还重新设置信号处理函数)。
- queued:被阻塞后信号多次生成,一些系统支持信号队列,后续多次deliver信号。(使用sigqueue函数依次发送信号给指定进程)
alarm&pause&sleep函数
unsigned int alarm(unsigned int seconds);
int pause(void);
unsigned int sleep(unsigned int seconds)
alarm用于设置闹钟,超时后,内核产生SIGALARM信号,默认行为是终止进程。每个处理器只有一个闹钟时钟,调用alarm时,如果上次闹钟还没有超时,返回它剩余时间。pause函数挂起当前进程,直到信号生成并被处理后才返回。早期UNIX版本有用alarm和pause实现sleep,但alarm和pause调用存在竞争条件,如果alarm超时之后pause才被调用,进程可能被永久挂起,更好的方案是使用sigprocmask和sigsuspend。
信号集
因为各个系统信号数量都不同,比如Linux的信号数量已经超过40中,超出整型位数,所以POSIX定义了sigset_t用来代表信号集合,并提供了一系列函数用来操纵信号集。
#include <signal.h>
int sigemptyset(sigset_t *set);
int sigfillset(siget_t *set);
int sigaddset(sigset_t *set, int signo);
int sigdelset(setset_t *set, int signo);
int sigismember(const sigset_t *set, int signo);
int sigprocmask(int how, const sigset_t *restrict set, sigset_t *restrict oset);
int sigpending(sigset_t *set);
int sigsuspend(const sigset_t *sigmask);
其中:
- sigprocmask被进程用于屏蔽(阻塞)/解除屏蔽信号集。
- sigpending返回当前被阻塞的信号集。
- sigsupend等于sigprocmask和sigpending的原子操作,用于挂起进程并等待特定信号。sigsuspend和kill(SIGUSR1和SIGUSR2信号)函数组合是有那个可以用来实现TELL_WAIT,TELL_PARENT,TELL_CHILD,WAIT_PARENT和WAIT_CHILD父子进程同步
sigaction函数
int sigaction(int signo, const struct sigaction *restrict act, struct sigaction *restrict oact);
struct sigactgion {
void (*sa_handler)(int);
sigset_t sa_mask;
int sa_flags;
void (*sa_sigaction)(int siginfo_t *, void *);
}
sigaction函数非常强大,能够定义信号处理函数(sa_handler),同时设置屏蔽信号集(sa_mask),是否重启被中断的系统调用(sa_flags),更多信号和进程的信息(siginfo_t)。系统一般使用sigaction实现signal函数。
include "apue.h"
/* Reliable version of signal(), using POSIX sigaction(). */
Sigfunc *
signal(int signo, Sigfunc *func)
{
struct sigaction act, oact;
act.sa_handler = func;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
if (signo == SIGALRM) {
#ifdef SA_INTERRUPT
act.sa_flags |= SA_INTERRUPT;
#endif
} else {
act.sa_flags |= SA_RESTART;
}
if (sigaction(signo, &act, &oact) < 0)
return(SIG_ERR);
return(oact.sa_handler);
}