一. 信号
我们在shell下运行起来一个程序,可以在这个进程正在运行的时候键盘输入一个Ctrl+C,就会看到这个进程被终止掉了,其实当我们键入Ctrl+C的时候是向进程发送了一个SIGINT信号,这时候产生了硬件中断则系统会从执行代码的用户态切入到内核态去处理这个信号,而一般这个信号的默认处理动作是终止进程,因此正在运行的进程就会被终止了。像SIGINT就是系统中定义的一个信号,除此之外还有其他信号,可以通过 kill -l 命令来查看:
如上图中有从1到64的各种信号,但是32和33号信号并没有,因此系统中一共有62个信号,其中1至31号是系统中的普通信号,而34到64是实时信号,这里暂不讨论。
-------------------------------------------------------------------------------------------
二. 信号的产生
那么,像上面的那些信号都是如何产生的呢?
- 首先,是由终端输入产生的,就像刚才举的栗子,从键盘输入一条命令其实就是在向一个进程发送特定的信号,但是像Ctrl+C这条命令只能发给前台进程,对后台进程是不起作用的,一个shell可以同时运行一个前台进程和多个后台进程;
- 其次,可以调用系统函数向进程发送信号,比如kill命令其实是终止一个进程,但kill命令是调用kill函数来实现的,kill函数可以给一个指定的进程发送特定的信号,其命令使用如下:
在上面的命令中,signal是为指定要发的信号,可以是信号的名字也可以是上面图片中每个信号前面的代码号;而pid就表示要发送信号的进程的pid号;
下面就可以写一个死循环的程序并且放在后台运行,当Ctrl+c对后台进程不起作用时就可以使用kill命令来给指定的进程发送2号信号也就是SIGINT信号也就是Ctrl+c,信号的处理方式为默认的也就是终止一个进程:
当输入一行命令"kill -2 6156"时按一下回车并没有结果,是因为当按下回车时要先一步回到bash等待用户输入下一条命令,这里并不希望命令的结果和用户输入的下条命令产生冲突显示错乱,因此要按两次回车才能看到进程被中断的结果;
除了kill函数还有一个函数raise函数,该函数可以给当前进程发送信号也就是自己给自己发送信号,这两个函数都是成功返回0,失败返回-1:
类似的abort函数是向当前进程发送6号信号也就是SIGABRT信号,该信号是使进程异常终止,和exit函数一样,该函数一定会成功因此并没有返回值;
- 还有一种是由软件条件产生的信号,比如以前谈论匿名管道的时候,当管道的读端关闭写端仍继续往里写入的时候进程就会接收到系统发来的一个SIGPIPE信号,该信号的默认处理动作是终止进程;
然而这里谈论一个函数alarm函数,该函数用于设定特定的秒数,而当超时的时候系统就会发送一个SIGALRM信号给该进程,而这个信号的默认处理动作仍然是终止进程:
函数参数seconds是指要设定的秒数,函数的返回值为0或者返回以前设定的闹钟余下的秒数,比如当闹钟设定时间还未到但又重新设定了一个闹钟,那么原来闹钟就会返回余下的时间,而当seconds值为0时,表示清除设定的闹钟,闹钟的返回值仍然是余下的秒数;
栗子时间:
#include <stdio.h> #include <unistd.h> int main() { int i = 0; alarm(1); while(1) { printf("i: %d\n",i++); } return 0; }
上面的程序是设定了一个闹钟,闹钟的时间为1秒,而在这一秒内不断地进行i++,当一秒钟之后,闹钟到时,系统就会给进程发送一个SIGALRM的信号,这个信号的处理动作会终止这个进程;
程序运行结果:
结果显示,也就是在一秒内i进行自加的次数为13628次;
-------------------------------------------------------------------------------------------
三. 信号的处理方式
一般来说,当一个信号产生之后,系统对其的处理方式有如下三种:
- 可以忽略这个信号,并不执行任何动作;
- 执行信号的默认处理动作,而大多数普通信号的默认处理动作是终止一个进程;
- 可以执行用户自定义的处理动作,这称为信号的捕捉(catch);
信号被捕捉,也就是当产生了这个信号的时候,要执行用户自定义的函数处理动作,而捕捉一个信号的处理函数有signal和函数sigaction:
signal函数参数中,
signum是要捕捉信号的代表号码;
handler是一个函数指针,可以为用户自定义的一个函数,表示信号要处理的动作函数;
sigaction函数同样是捕捉一个信号执行用户自定义的函数,
signum同样是为要捕捉的信号号码;
act则是一个数据结构,数据结构的定义如下:
数据结构中,sa_handler和signal函数中是一样的,是一个函数指针可以指向用户自定义的函数,当sa_handler赋值为SIG_IGN的时候表示信号的默认处理动作为忽略这个信号,赋值为SIG_DFL的时候表示执行默认处理动作,而sa_mask表示需要额外屏蔽的一些信号,当信号处理函数返回时自动恢复为原来状态;sa_flag表示一些标志来修改信号的行为,这里设置为0即可;sa_sigaction表示实时信号的处理函数,这里不讨论;而sa_restorer是过时的且不应该被使用的,POSIX并不提供该元素;
oldact是指信号原来的数据结构信息,当其不为空的时候就将修改之前的信息保存其中以便日后恢复;
除了上面两种信号的捕捉函数,还有一个函数pause,该函数使进程挂起直到有信号递达,如果此号的处理动作是执行系统默认处理动作,则进程终止该函数没有返回值;如果信号的处理动作是忽略,则进程会一直处于挂起状态,pause同样没有机会返回;如果信号的处理动作是捕捉这个信号执行用户自定义的函数,则pause会出错返回-1并设置错误码,因此和程序替换函数exec有些类似,只有出错返回值;
下面举个栗子来使用信号的捕捉函数,可以自我实现一个sleep函数:
#include <stdio.h> #include <signal.h> void handler(int sig)//用户自定义函数 { printf("i get a sig %d\n", sig); } void mysleep(unsigned int time) { struct sigaction new, old;//设定两个结构体 new.sa_handler = handler; sigemptyset(&new.sa_mask); new.sa_flags = 0; sigaction(SIGALRM, &new, &old);//注册信号处理函数,捕捉SIGALRM信号 alarm(time);//设定闹钟为自定义时间 pause();//将进程挂起直到闹钟结束向进程发送SIGALRM信号 alarm(0);//撤销闹钟 sigaction(SIGALRM, &old, NULL);//恢复对信号的默认处理动作 } int main() { //主函数内要实现的是每隔两秒打印一句“hello world...” while(1) { mysleep(2);//实现mysleep printf("hello world...\n"); } return 0; }
运行程序,结果如下:
对于上面的过程,其实是有一个重要的点要解释清楚的,那就是系统是如何完成从信号产生到信号被处理这一系列动作的;这里需要知道的是,信号并不是一产生就立即被处理的,而是有一个合适的契机来决定是否处理这个信号,具体解释如下图:
-------------------------------------------------------------------------------------------
四. 信号的状态
信号从产生到进程接收到是有一个中间过程的,当进程接收到一个信号也就是信号的递达(delivery);而信号从产生到递达这之间的状态叫信号的未决(pending);但同时,信号还有一种状态叫信号的阻塞(block),也就是信号产生处于未决状态但没有被递达而是被阻塞住了;这里需要强调的是,信号的阻塞和信号的忽略是不一样的,信号的忽略是在信号递达之后的一种处理方式,而信号的阻塞是信号还没有被递达。
信号的状态在每个进程的PCB中都有一张可以说是相对应的表来表示相应的状态,信号产生时,内核就会在进程PCB块中的未决表中设置相应的信号位,同样的,当一个信号信号被阻塞,也有一个block表来记录信号是否被阻塞,另外还有一张handler表来记录信号的处理方式;
因为我们这里讨论1~31个系统的普通信号,而每一个信号是否产生(未决)是否被阻塞都可以用两个状态0和1来记录表示,其实也就类似于32位平台下一个int类型的32个比特位的状态,这里的状态在未决表中0和1表示信号是否产生,而block表则表示信号是否被阻塞;但是,信号的产生并不影响信号的阻塞,当一个信号产生时在未被递达之前是可以被阻塞的;同样,信号是否阻塞也并不影响信号是否产生,一个信号被阻塞了,当它产生时只是不会被递达而已;两者并没有关系;
sigset_t是可以用来存储信号的未决和阻塞状态的,称为信号集。它可以被看做是由32个比特位组成的,而每一个比特位表示信号是否有效和无效,至于内部到底是如何存储的,作为使用者暂时是不用关心的;阻塞信号集也叫作当前信号的信号屏蔽字;
而关于信号集的操作函数如下:
sigemptyset函数表示清空一个信号集,该信号集中不包含任何有效信号;
sigfillset函数表示将一个信号集全部置位,该信号集包含了所有系统中的有效信号;
sigaddset函数表示将一个有效信号添加到某个信号集中;
sigdelset函数表示将一个有效信号从某个信号集中删除;
sigismember函数的返回值是一个bool类型,用于判断一个信号集中是否包含某个有效信号;
函数参数中set是指向sigset_t类型的指针,signum则是信号的号码;
这里需要强调的是,在使用sigaddset函数和sigdelset函数对一个信号集进行添加或删除某个有效信号之前,需要调用sigemptyset函数或者sigfillset函数将信号集进行初始化;
上面的函数成功返回0,失败返回-1;
函数sigprocmask函数可用于修改和读取进程的信号屏蔽字:
函数参数中,
set表示要修改的信号集;
oldset表示修改之前信号集的状态,以便恢复;
how表示如何修改,为SIG_BLOCK时,表示将set信号集中的有效信号阻塞;为SIG_UNBLOCK时,表示将信号集中的而有效信号解除阻塞,而为SIG_SETMASK时,表示值就为set;
设置了信号屏蔽字之后,同样可以读取当前进程信号的未决状态,也就是信号产生与否:
函数从内核中读取出未决信号集状态并保存到set中;
下面举个栗子使用上面的信号集操作函数:
#include <stdio.h> #include <signal.h> void printpending(sigset_t *p) { int i = 0; for(i = 1; i < 32; ++i) { if(sigismember(p, i)) //判断1~31号信号是否产生 printf("1"); else printf("0"); } printf("\n"); } int main() { sigset_t b, p;//设置两个信号集block和pending sigemptyset(&b);//将block信号集初始化 sigaddset(&b, SIGINT);//将SIGINT信号也就是Ctrl+c产生的信号添加到block信号集中 sigprocmask(SIG_BLOCK, &b, NULL);//设置进程的信号屏蔽字,阻塞SIGINT信号 while(1) { sigpending(&p);//每隔1秒获取当前进程的信号未决状态 printpending(&p); sleep(1); } return 0; }
上面的程序阻塞掉了2号信号也就是SIGINT信号,因此,当键入Ctrl+c的时候,产生了SIGINT信号,但是因为信号被阻塞了因此就一直处于未决状态并不会递达,每隔一秒获取当前进程信号未决状态查看对应信号是否产生,运行程序:
可以看到,当键入Ctrl+c的时候产生了2号信号,而相应的pending信号集中的第二位就会被置为1,说明2号信号也就是SIGINT信号产生。
因为2号信号被阻塞并不会递达被处理,因此Ctrl+c并不能终止程序,可以用Ctrl+\来终止程序。
-------------------------------------------------------------------------------------------
五. 总结
- 信号的产生:终端输入、调用系统函数向进程发送指定的信号、软件条件产生比如错误和异常;
- 信号的处理方式:忽略、执行默认处理动作(一般是终止进程)、执行用户自定义动作(捕捉);
- 信号的状态:未决、阻塞、递达;未决和阻塞之间相互不影响,而只有信号处于未决状态且没有被阻塞时才会被递达;
- 对于信号的捕捉,其实是在用户和内核之间来回切换的,用户--(因为错误、异常或中断进入内核)-->内核--(在处理完异常之后检查是否有信号要处理,若被捕捉进入用户)-->用户--(执行完信号处理函数重新返回内核)-->内核--(重新返回异常产生处继续执行用户代码)-->用户。
《完》