信号
信号是一种软件中断,提供了一种处理异步事件 的方法。
例:中断用户键入中断键,则会通过信号机制停止一个程序
对应在现实生活中的一个例子就是:你在打游戏的时候,你妈喊你去楼下买瓶酱油。
买瓶酱油的“买”就是你妈发给你的信号。
信号概念
跟你妈让你做的事情一样,诸如买东西、写作业。每个信号都有自己的名字,且均以SIG
开头。
在头文件<singal.h>中,这些信号都被定义成正整数,也就是他们的信号编号。但是,有一点要注意:不存在编号为0的信号。
kil函数对编号0有特殊的应用
信号的产生
有以下的条件可以产生信号:
- 用户按下某些终端键
- 硬件异常产生的信号,
如除数为0、无效的内存引用等
- 进程调用kill(2)函数产生信号给所有者相同的进程。当然,超级用户也可以
- 用户可用kill(1)命令发给其他进程
- 当检测到某种软件条件已经发生,并应将其通知有关进程时也产生信号
信号处理
进程在接收到信号时,会告诉内核,在此信号出现时,执行以下动作。
- 忽略此信号。
注意!有两种信号
SIGKILL
、SIGSTOP
不能被忽略。因为他们是向超级用户提供了使进程终止或停止的可靠方法。
- 捕捉信号
需要通知内核在某种信号发生时调用一个用户函数。但是不能捕捉
SIGKILL
和SIGSTOP
信号
- 执行系统默认动作
针对大多数的信号的系统默认动作时终止进程
信号处理函数
UNIX提供了signal()
和sigaction()
函数来改变对于信号的处理方法。其中signal()是一个基于sigaction()
系统调用的glibc库函数。
其函数原型如下:
#include<signal.h>
void (*signal(int sig,void(*hander)(int))) (int);
signal函数的第一个参数sig,要传入希望修改的信号编号。
第二个参数,是一个无返回值、接收一个int形参的函数指针。指向一个用户处理函数。第二个参数有三种选择:
- 我们自己定义的信号处理函数
- 传入SIG_DFL表示将之前改变的信号处理方式还原
- 第三种是传入SIG_IGN,表示忽略该信号
signal函数的返回值同样是一个无返回值、接收一个int形参的函数指针。当signal()函数成功执行时,返回值是执行signal函数之前的sig信号的处理函数指针,如果失败,则返回SIG_ERR
。
所以按照以上描述,signal函数可以重写为一下形式。
typedef void (* sighandler_t)(int);
sighandler_t signal(int sig,sighandler_t handler);
以上typedef已被包含在apue.h
文件中。
信号处理实例
int main()
{
if(signal(SIGUSR1, sig_usr) == SIG_ERR)
err_sys("can't catch SIGUSR1");
if(signal(SIGUSR2, sig_usr) == SIG_ERR)
err_sys("can't catch SIGUSR2");
while(1)
{
pause();
}
}
static void sig_usr(int signo)
{
if(signo == SIGUSR1)
printf("received SIGUSR1\n");
else if(signo == SIGUSR2)
printf("received SIGUSR2\n");
}
注意,signal() 会堵塞当前正在处理的信号,不过不会阻塞其它信号,如正在处理 SIG_INT
,再来一个 SIG_INT
则会堵塞,但如果是 SIG_QUIT
则会被其中断,在处理完 SIG_QUIT
信号之后,SIG_INT
才会接着刚才处理。
信号阻塞
信号在内核中会分为如下几类:
- 信号递达,实际执行信号处理信号的动作
- 信号未决,信号从产生到抵达之间的状态,信号产生但是未处理。
- 忽略,抵达之后忽略,不做处理
- 阻塞,收到信号不立即处理,被阻塞的信号将保持未决状态,知道进程解除对此信号的阻塞
每个信号都由两个标志位分别表示阻塞和未决,以及一个函数指针表示信号的处理动作。
在上图的例子中,其状态信息解释如下:
SIGHUP
未阻塞也未产生过,当它递达时执行默认处理动作。
SIGINT
信号产生过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略,但在没有解除阻塞之前不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞。
SIGQUIT
信号未产生过,一旦产生 SIGQUIT 信号将被阻塞,它的处理动作是用户自定义函数 sighandler。
信号产生但是不立即处理,前提条件是要把它保存在 pending 表中,表明信号已经产生。
信号集
信号集用来描述信号的集合,每个信号占用一位,总共 64 位,Linux 所支持的所有信号可以全部或部分的出现在信号集中,主要与信号阻塞相关函数配合使用。
每个进程都有一个用来描述哪些信号递送到进程时将被阻塞的信号集,该信号集中的所有信号在递送到进程后都将被阻塞。
如下是常见的信号集的操作函数:
#include <signal.h>
int sigemptyset(sigset_t *set); /* 所有信号的对应位清0 */
int sigfillset(sigset_t *set); /* 设置所有的信号,包括系统支持的所有信号 */
int sigaddset(sigset_t *set, int signo); /* 在该信号集中添加有效信号 */
int sigdelset(sigset_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);
信号集实例
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
void print_sigset(sigset_t *set)
{
int i;
for(i = 1; i < NSIG; ++i){
if(sigismember(set, i))
putchar('1');
else
putchar('0');
}
putchar('\n');
}
int main(void)
{
sigset_t foobar;
sigemptyset(&foobar);
sigaddset(&foobar, SIGINT);
sigaddset(&foobar, SIGQUIT);
sigaddset(&foobar, SIGUSR1);
sigaddset(&foobar, SIGRTMIN);
print_sigset(&foobar);
return 0;
}
内核处理信号
如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为信号捕捉。由于信号处理函数的代码是在用户空间的,处理过程比较复杂。
也就是说,处理信号最好的时机是程序从内核态切换到用户态时。
参考文献
[1] UNIX环境高级编程(第二版)
[2]JIN-YANG. linux信号机制. GitHub
对了,我舍友让我在这篇博客夸他帅。
就是这个逼,臭不要脸。