信号是提供异步事件处理机制的软件中断。
这些异步事件可能来自硬件设备,如用户同时按下了Ctrl和C键;也可能来自系统内核,如试图访问尚未映射的虚拟内存;又或者来自用户进程,如尝试计算 int / 0表达式。
进程之间可以相互发送信号,这使信号成为一种进程间通信(Inter Process Communication, IPC)的基本手段。
信号的异步特性不仅表现为它的产生是异步的,对它的处理同样也是异步的:
程序的设计者不可能也不需要精确地预见什么时候触发什么信号,也同样无法预见该信号究竟在什么时候会被处理。一切都在内核的操控下,异步地运行。
信号是在软件层面对中断机制的一种模拟。
信号是很短的信息,本质就是一个整数,用以代表不同事件。
signum.h头文件中用一组名字前缀为SIG的宏来标识信号,即为信号的名字。
kill -l 命令可查看所有62个信号
生成转储文件,俗称吐核。
其中前31个信号为不可靠的非实时信号,后31个为可靠的实时信号。
编号 | 名称 | 说明 | 默认操作 |
---|---|---|---|
1 | SIGHUP | 进程的控制终端关闭(用户登出) | 终止 |
2 | SIGINT | 用户产生中断符(Crtl + C) | 终止 |
3 | SIGQUIT | 用户产生退出符(Ctrl + \) | 终止 + 转储 |
4 | SIGILL | 进程试图执行非法指令 | 终止 + 转储 |
5 | SIGTRAP | 进入断点 | 终止 + 转储 |
6 | SIGABRT | abort()函数产生 | 终止 + 转储 |
7 | SIGBUS | 硬件或对齐错误 | 终止 + 转储 |
8 | SIGFPE | 算术异常(除以0 ...) | 终止 + 转储 |
9 | SIGKILL | 不能被捕获或忽略的进程终止信号 | 终止 |
10 | SIGUSR1 | 进程自定义的信号 | 终止 |
11 | SIGSEGV | 无效内存访问 | 终止 + 转储 |
12 | SIGUSR2 | 进程自定义的信号 | 终止 |
13 | SIGPIPE | 向读端已关闭的管道写入 | 终止 |
14 | SIGALRM | alarm()函数产生/真实定时器到期 | 终止 |
15 | SIGTERM | 可以被捕获或忽略的进程终止信号 | 终止 |
16 | SIGSTKFLT | 协处理器栈错误 | 终止 |
17 | SIGCHLD | 子进程终止 | 忽略 |
18 | SIGCONT | 进程由停止状态恢复运行 | 忽略 |
19 | SIGSTOP | 不能被捕获或忽略的进程终止信号 | 停止 |
20 | SIGTSTP | 用户产生停止符(Ctrl + Z) | 停止 |
21 | SIGTTIN | 后台进程读控制终端 | 停止 |
22 | SIGTTOU | 后台进程写控制终端 | 停止 |
23 | SIGURG | 紧急I/O未处理 | 忽略 |
24 | SIGXCPU | 进程资源超限 | 终止 + 转储 |
25 | SIGXFSZ | 文件资源超限 | 终止 + 转储 |
26 | SIGVTALRM | 虚拟定时器到期 | 终止 |
27 | SIGPROF | 实用定时器到期 | 终止 |
28 | SIGWINCH | 控制终端窗口大小改变 | 忽略 |
29 | SIGIO | 异步I/O事件 | 终止 |
30 | SIGPWR | 断电 | 终止 |
31 | SIGSYS | 进程试图执行无效系统调用 | 终止 + 转储 |
9和19都是不能被捕获或忽略的进程终止信号。
02是Ctrl+C,终止;
20是Ctrl+Z,停止。
忽略:什么也不做,SIGKILL(9)和SIGSTOP(19)不能被忽略
默认:在没有人为设置的情况,系统缺省的处理行为
捕获:接收到信号的进程会暂停执行,转而执行一段事先编写好的处理代码,执行完毕后再
从暂停执行的地方继续运行。
信号捕获一旦开启,就由内核形成捕获机制:循环捕获,啥时候来啥时候捕获。
#include
typedef void(*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
功能:设置调用进程针对特定信号的处理方式
signum:信号编号
handler:信号的处理方式,可以取以下值
SIG_IGN 忽略
SIG_DFL 默认
信号处理函数指针 捕获 //信号处理函数,由内核负责调用
返回值:成功(一般不关心)返回原信号处理方式,如果之前未处理过则返回NULL
失败返回SIG_ERR !!!
//signal.c 信号处理:忽略、捕获、恢复默认
#include
#include
#include
//信号处理函数,由内核负责调用!!!
void sigfun(int signum){
printf("%d进程:捕获到%d号信号\n",getpid(),signum);
}
int main(void){
//对2号信号进行忽略处理(此开启了一种机制,这机制是忽略)
if(signal(/*2*/SIGINT,SIG_IGN) == SIG_ERR){ //注意是宏SIG_ERR
perror("signal");
return -1;
}//忽略以下代码,保留for(;;),编译执行,按Ctrl+C发现被忽略
//对2号信号进行捕获处理
if(signal(SIGINT,sigfun) == SIG_ERR){
perror("signal");
return -1;
}//忽略以下代码,保留for(;;),编译执行,按Ctrl+C发现被捕获
//对2号信号进行默认处理
if(signal(SIGINT,SIG_DFL) == SIG_ERR){
perror("signal");
return -1;
}//编译执行,按Ctrl+C,发现恢复为SIGINT
for(;;);
return 0;
}
主控制流程、信号处理流程、内核处理流程:
当有信号到来时,内核会保存当前进程的栈帧,然后再执行信号处理函数。
当信号处理函数结束后,内核会恢复之前保存的进程的栈帧,使之继续运行。
前述博文中的wait()也好,waitpid()也好,阻塞也好,非阻塞也好,都不如太平间信号收尸来的完美(把收尸的事放到17信号处理函数中)。
如前所述,无论一个进程是正常终止还是异常终止,都会通过系统内核向其父进程发送SIGCHLD(17)信号,对17号信号默认处理方式是:忽略。
父进程完全可以在针对SIGCHLD(17)信号的信号处理函数中,异步地回收子进程的僵尸,简洁高效(子进程死了发送17,父就及时收尸,收尸后不影响父进程干自己的事):
void sigchld (int signum){
pid_t pid = wait(NULL);
if(pid == -1){
perror("wait");
exit(EXIT_FAILURE);
}
printf("%d子进程终止\n,pid);
}
上述方式是收到1个17号信号,父进程收1个尸。
这样处理存在一个潜在的风险,就是在sigchld()信号处理函数执行过程中,又有多个子进程终止,由于SIGCHLD(17)信号不可靠,可能会丢失(信号集,信号屏蔽uc_11),形成漏网僵尸,因此有必要在一个信号处理函数调用过程中循环waitpid(),回收尽可能多的僵尸:
5个子进程同时死,发出5个17号信号。第1个17号信号过来,调用信号处理函数,此时丢失3个信号。此时若还是按照1个17号信号,收1个尸的话,会产生3个僵尸进程,ps aux命令查看。
解决方案:在每次调用信号处理函数的时候,不再只收1个子进程尸体,而是循环多次收尸,一次把5个尸体收干净:
for(;;){
pid_t pid = waitpid(-1,NULL,WNOHANG);
if(-1 == pid){
if(ECHILD == errno){
printf("%d进程:没有子进程可回收了\n",getpid());
break;
}else{
perror("waitpid");
return;
}
}else if(0 == pid){
printf("%d进程:子进程在运行,没法回收\n",getpid());
break;
}else{
printf("%d进程:回收了%d进程的僵尸\n",getpid(),pid);
}
}
//sigchld.c 17号信号/太平间信号
#include
#include
#include
#include
#include
//信号处理函数,负责收尸
void sigchild(int signum){
printf("%d进程:捕获到%d号信号\n",getpid(),signum);
sleep(2);//假装信号处理函数比较耗时
//waitpid()循环收尸,非阻塞方式,最完美
for(;;){
pid_t pid = waitpid(-1,NULL,WNOHANG);
if(pid == -1){
if(errno == ECHILD){
printf("%d进程:没有子进程了\n",getpid());
break;
}else{
perror("waitpid");
return;
}
}else if(pid == 0){
printf("%d进程:子进程在运行,收不了\n",getpid());
break; //老六老也不死,退出循环,啥时候死,啥时候内核再调用本函数
}else{
printf("%d进程:回收了%d进程的僵尸\n",getpid(),pid);
}
}
//wait()循环收尸。wait()是阻塞函数,天然缺陷(老六一直不死,父进程不能就干等着吧)
/*for(;;){
pid_t pid = wait(NULL);//NULL,不存子进程终止状态
if(pid == -1){
if(errno == ECHILD){
printf("%d进程:没有子进程了\n",getpid());
break;
}else{
perror("wait");
return;
}
}
printf("%d进程:回收了%d号进程\n",getpid(),pid);
}*/
}
int main(void){
//对17号信号进行捕获处理,把收尸的事放在信号处理函数这里处理,完美
if(signal(SIGCHLD,sigchild)==SIG_ERR){
perror("signal");
return -1;
}
//创建多个子进程
for(int i = 0;i < 5;i++){
pid_t pid = fork();
if(pid == -1){
perror("fork");
return -1;
}
if(pid == 0){
printf("%d进程:我是子进程\n",getpid());
//sleep(i + 1); //一个一个死,17号信号一个一个来,都收到
sleep(1); //同时来5个不可靠的17号信号,只收到2个,其余丢弃(信号屏蔽uc_11)
//所以顶部代码中,每次17->调用信号处理函数,就循环多次收尸。
return 0;//!!!!!!! 子进程代码勿忘return 0;
}
}
//创建老六
pid_t oldsix = fork();
if(oldsix == -1){
perror("oldsix");
return -1;
}
if(oldsix == 0){
printf("%d进程:我是老六,就不结束\n",getpid());
sleep(15);
return 0;
}
//父进程该干啥就干啥
for(;;);
return 0;
}
//编译执行
编译执行,结果如下,好好理解下~
综上,上述最完美代码解决了以下痛点:
为了避免子进程同时死,同时发送多个17,信号丢失,处理方式是每次收到17号信号,调用信号处理函数时都循环多次收尸;
为了避免子进程老也不死,不建议父进程wait()阻塞干等着,而是用waitpid(),不耽误父进程干别的事。
信号发送,共3种方式:kill命令、kill()、raise()。
kill [-信号] PID //在终端手动敲kill命令
若不指明具体信号,缺省发送SIGTERM(15)信号(可被捕获或忽略的进程终止信号)。
若要指明具体信号,可用信号编号,也可用信号名称,且前缀SIG可省略不写。如:
kill -9 1234 //终止,终止1个进程
kill -SIGKILL 1234 5678 //终止,终止多个进程
kill -KILL -1 //终止,终止所有进程
root用户可以发给任何进程,普通用户只能发给自己的进程。
#include
int kill(pid_t pid, int signum);
功能:向指定的进程发送信号 //注意,进程死后,进程仍存在,是僵尸进程,需收尸
pid:可以取以下值
-1:向系统中的所有进程发送信号
>0:向特定进程(由PID标识)发送信号
signum:信号编号,取0可用于检查pid进程是否存在,
如不存在,kill()返回-1,且errno为ESRCH
返回值:成功(至少发出去一个信号)返回0,失败返回-1
//kill.c kill()函数演示
#include
#include
#include
#include
#include
int main(void){
//父进程创建子进程
pid_t pid = fork();
if(pid == -1){
perror("fork");
return -1;
}
//子进程执行
if(pid == 0){
printf("%d进程:我是子进程\n",getpid());
for(;;);
return 0;
}
//父进程给子进程发送信号
getchar();
if(kill(pid,SIGINT) == -1){
perror("kill");
return -1;
}
getchar();
if(kill(pid,0) == -1){
printf("子进程不存在\n");
}else{
printf("子进程存在\n"); //子进程通过kill死了,子进程仍存在,是尸体,需收尸
}
if(wait(NULL) == -1){
perror("wait");
return -1;
}
getchar();
if(kill(pid,0) == -1){
printf("子进程不存在\n");
}else{
printf("子进程存在\n");
}
return 0;
}
#include
int raise (int signum);
功能:向调用进程自己发送信号
signum:信号编号
返回值:成功返回0,失败返回非0
本函数等效于:
kill (getpid(), signum);
fork()函数创建的子进程会继承父进程的信号处理方式:
父进程中被忽略的信号,在子进程中依然被忽略。
父进程中被捕获的信号,在子进程中依然被捕获。
exec家族函数创建的新进程对信号的处理方式和原进程稍有不同:
原进程中被忽略的信号,在新进程中依然被忽略。
原进程中被捕获的信号,在新进程中被默认处理。
代码验证:编写代码,父子进程都无限循环,父进程忽略2号,捕获3号,编译执行。
另起终端窗口,kill -2 25543命令,ps aux查看。
验证结束,kill -9 25543杀死。