APUE 3 - 信号 (signal)<II>: 可靠信号
一个事件可以使一个信号发送给一个进程,这个事件可以是硬件异常,可以是软件条件触发,可以是终端产生信号,也可以是一个kill函数调用。当信号产生后,内核通常会在进程表中设置某种形式的标志(flag)。我们可以认为当进程中的信号处理函数被触发的时候认为信号下达到了(delivered)这个进程。从信号产生到信号下达到进程这段期间,信号被认为是挂起状态(pending)。进程拥有阻塞信号下达的选项。如果一个阻塞信号要发送给一个进程,而且信号的处理方式是默认处理或者被进程捕获,那么这个信号将一直处于挂起状态(pending)直到这个进程将信号设置成非阻塞状态或将信号的处理方式改为忽略。操作系统在信号下达(delivered)的时候决定如何处理这个阻塞信号,而不是在信号产生的时候。这样做允许进程在信号下达前更改信号的处理方式。
如果一个阻塞信号在进程解除它的阻塞前产生多次,那么unix内核也仅仅会向进程下发一次这个信号。POSIX并没有规定信号下发到进程的顺序,然而与进程当前状态相关的信号会较先到达进程。
每个进程都有一个信号掩码(signal mask),它定义了一个当前被阻塞发送到该进程的信号的集合。我们可以认为这个掩码对于每个可能下发到该进程的信号有一个与之对应的bit位,对于一个给定的信号来说,如果与之对应的bit位是打开状态,那么意味着这个信号当前应处于阻塞状态。进程可以通过sigprocmask来检查并更改他当前的信号掩码。因为信号的数量是有可能超过一个整数正bit位位数的,所有POSIX规范定义了一个叫做sigset_t的数据类型,它包含了所有信号集。信号掩码就是存储在这其中一个信号集中的。
发送信号
kill & raise
可以使用kill函数向一个进程或进程组发送信号。raise函数允许进程给他自己发送信号。
1 #include <signal.h> 2 3 /* return 0 if OK, -1 on error */ 4 int kill(pid_t pid, int signo); 5 6 /* return 0 if OK, -1 on error */ 7 int raise(int signo);
函数调用 raise(signo); 等同于 kill(getpid(), signo); 。
对于kill函数,pid有以下四种不同的选择:
- pid > 0: 信号被发送给进程号为pid的进程
- pid == 0: 信号被发送给所有进程组Id等于发送者进程组的进程(即发送给发送进程所属进程组下的所有进程),前提是发送进程有权限将信号发送给该进程并且该进程不是系统进程。
- pid < 0: 信号被发送给进程组Id为pid绝对值的进程组下的所有进程,前提是发送进程有权限发送信号到该进程并且该进程不是系统进程。
- pid == -1: 信号被发送给发送进程有权限发送的所有进程,但不包含系统进程。
正如之前我们提到的,进程需要足够的权限才能向另一个进程发送信号,超级用户可以向任何进程发送信号。对于其他用户,可以发送信号的基本准则是:发送进程的真实用户Id(real user Id)或有效用户Id(effective user Id)必须等于接收进程的真实用户Id或有效用户Id。如果实现支持 _POSIX_SAVED_IDS的话,系统会检查接收进程的saved set-user-ID而不是有效用户Id。关于发送信号权限的一个特殊情景是:如果待发送信号是SIGCONT,那么发送进程可以将信号发送给他所属会话下的任何进程。
POSIX规范定义信号值为0的信号为空信号(null signal)。它可以用于通过kill函数来检测某一进程是否存在。kill函数在收到值为0的信号后会进程正常的错误检查,但是不会发送此信号。因此我们可以通过 kill(pid, 0) 来判断进程id为pid的进程是否存在。然而, UNIX系统在一定时间后会循环使用进程 IDs,所以通过pid检查出来的进程未必真的是你认为的那个进程(即函数调用时,通过pid查询到的进程未必会是你认为的那个进程)。另外kill函数不是原子的,当kill函数返回时,有可能被发送信号的进程已经结束。
alarm & pause
alarm函数允许我们设置一个定时器,这个定时器在未来某个时间点触发,这个定时器触发后会产生一个SIGALRM信号,此信号的默认处理方式是结束进程。
1 #include <unistd.h> 2 3 /* 如果之前没有设置alarm返回0,否则返回之前 4 设置的alarm所剩余的秒数 */ 5 unsigned int alarm(unsigned int seconds);
一旦到了alarm所设置的时间点,内核就会发送alarm信号,而由于处理器调度延时进程此时可能还无法获取到此信号处理的控制权。每个进程中只有一个alarm时钟。当我们调用alarm时,如果当前进程之前注册的闹钟还未到期,那么此函数返回之前闹钟距到期剩余的秒数,并且之前注册的闹钟会被这个新的闹钟值取代。另外,如果之前注册的闹钟还未到期并且新注册的闹钟值为0的话,那么之前注册的闹钟会被取消。
pause函数的调用会阻塞调用进程直到调用进程捕获到一个信号。
1 #include <unistd.h> 2 3 /*Returns: -1 with errno set to EINTR*/ 4 int pause(void);
信号集
像我们之前提到的,不同信号的数量可能会超过一个整数的bit位所能表示的信号数量。POSIX规范定义了sigset_t类型用来表示信号集,并使用下面的5个函数来管理信号集:
1 #include <unistd.h> 2 3 /*All four return 0 if OK, -1 on error*/ 4 5 /*清空set信号集中的所有信号*/ 6 int sigemptyset(sigset_t* set); 7 /*使set包含所有信号*/ 8 int sigfillset(sigset_t* set); 9 /*将信号signo加入到信号集set中*/ 10 int sigaddset(sigset_t* set, int signo); 11 /*从信号集set中删除signo*/ 12 int sigdelset(sigset_t* set, int signo); 13 14 /*Returns 1 if true, 0 if false, -1 on error*/ 15 int sigismember(const sigset_t* set, int signo);
sigprocmask
一个进程的信号掩码是指被阻止下发到此进程的所有信号的集合。进程可以检查并更改他的信号掩码。
#include <signal.h> /*
如果oset不为空,那么此进程信号掩码之前的值会被复制到oset中。
如果set为空,那么此进程的信号掩码不会被更改,而信号掩码的当前值
也不会复制到oset中。
*/ int sigprocmask(int how, const sigset_t* restrict set, sigset_t* restrict oset);
how参数的值指明如何修改信号掩码:
- SIG_BLOCK: set包含另外的我们想阻塞的信号
- SIG_UNBLOCK:set包含我们想要解除阻塞的信号
- SIG_SETMASK:使用set替代进程当前信号掩码
sigprocmask 不支持多线程环境。
sigpending
#include <unistd.h> /*通过set返回发送给当前进程但被阻塞的信号*/ int sigpending(sigset_t* set);
sigaction
我们可以通过sigaction方法检查并修改特定信号的处理方式(action)。他是早期sinal函数的取代版本。
#include <unistd.h> /*若oact不为空,函数通过oact返回当前signo的action * 若act不为空,则修改signo的当前action*/ int sigaction(int signo, const struct sigaction* restrict act, struct sigaction* restrict oact); struct sigaction{ void (*sa_handler)(int); /*addr of signal handler,
or SIG_IGN or SIG_DFL */ sigset_t sa_mask; /*additional signals to block*/ int sa_flag; /*signal options*/ /*alternate handler*/ void (*sa_sigaction)(int,siginfo_t *, void *); };
当使用sigaction改变signo的action时,如果sa_handler指向了一个信号处理函数(SIG_IGN和SIG_DFL除外),sigaction函数会将sa_mask指向的信号集合在这个信号处理函数(sa_handler)被调用前加入到当前进程掩码中,当信号处理函数返回时,进程的信号掩码会恢复为他原来的值。这样,使我们能够在信号处理函数被调用是阻塞一部分信号的到达。一旦我们为一个信号安装了action, 那么对于这个信号这个action将一直处于安装状态,除非我们使用sigaction方法明确的更改可它。
sa_flags:
sigsuspend
等待信号到达的一个整洁而可靠地方式是先阻塞这个信号然后使用sigsuspend。
#include <signal.h> /* 将当前进程信号掩码设置为sigmask, 函数返回后将进程掩码恢复为调用前 的值, 该函数总是返回-1,并设置 errno 为 -1 */ int sigsuspend(const sigset_t* sigmask);
sigsuspend 可用于等待指定信号的到达,他的常用用法如下:
1 sigset_t mask, oldmask; 2 3 … 4 5 /* Set up the mask of signals to temporarily block. */ 6 sigemptyset (&mask); 7 sigaddset (&mask, SIGUSR1); 8 9 … 10 11 /* Wait for a signal to arrive. */ 12 sigprocmask (SIG_BLOCK, &mask, &oldmask); 13 while (!usr_interrupt) 14 sigsuspend (&oldmask); 15 sigprocmask (SIG_UNBLOCK, &mask, NULL);
通过user_interrupt 判断是否等待的SIGUSR1信号已到达,sigsuspend再返回时将进程信号掩码设置为他被调用前的值,因此我们最后需要将添加mask移除掉。
Signal Names and Numbers
数组sys_siglist可以帮助我们匹配信号与信号名:
1 extern char* sys_siglist[]; 数组索引为信号值, 数组元素值为信号名。
信号与信号名的转换:
1 #include <signal.h> 2 3 /* 如果msg不为空,则向stderr 输出msg紧跟一个冒号加一个空着在加信号描述;如果msg为空则只向stderr输出信号描述*/ 4 void psignal(int signo, const char* msg); 5 6 void psiginfo(const siginfo_t info, const char* msg); 7 8 /*获取信号描述*/ 9 char* strsignal(int signo); 10 11 void sig2str(int signo, char* str); 12 void str2sig(const char* str, int* signop);
总结
信号通常用于一些相对复杂的程序, 理解如何及为何处理信号对于UNIX高级编程是必要的。