目录
1.信号分为普通信号与实时信号
每个进程的PCB内都能找到3张表 block pending hander.
不同的环境实现这三张表的方式可能略有区别,可以理解为block 和 pending表示类似于位图的结构 hander表是指向信号对应处理方法的数组(指针数组).

1.但pending表某一个位为1,代表收到了对应位置的信号
2.若block表为1代表即使收到了信号,也要阻塞它(拦住不处理)
3.若信号没有被阻塞,当检测到信号(发现pending表对应位置为1)后,将pending对应位置,置回0,并执行hander表里对应的方法,
4.若不做处理,hander表为信号的默认处理办法,多数为终止程序
对于信号的理解就基于这三张表展开,从信号的产生,检测,到处理来逐步剖析
信号产生
信号的产生,就是pengding表内信号对应位置由0置为1
要明确,这三张表都是再内核中存储的,对它们操作,只能由操作系统来做,所以但提到:
"信号如何产生?"或者"给某个进程发信号"的时候,本质上是讲:"谁让OS修改了那个进程PCB内pending表对应信号位置的值"
常见的信号产生方式
1.键盘产生
ctrl+c 可以对正在前台运行的进程发送 2号信号 ctrl+/ 3号信号 ctrl+/z 20号信号

本质上是键盘中断,cpu感知到执行对应内核代码,讲前台进程pcb中pending表对应位置由0置为1
2.kill 指令
通过终端下指令[ kill - 信号编号 pid ]来让os将pid进程中pending表信号编号对应位置置为1(后文简称为给对应进程发送信号)
1~31号为普通信号,32~62为实时信号,普通信号和实时信号各有32个
测试代码:
#include<iostream>
#include<unistd.h>
#include<signal.h>
using namespace std;
int main(){
while(1){
cout<<"Runing Pid: "<<getpid()<<endl;
sleep(1);
}
}

当进程检测到信号,执行了kill信号的默认行为:终止进程.
3.硬件出错
当硬件执行出错,也会给进程发送信号,包括但不限于如下情况:
1.除0错误
如当cpu在执行运算时,发现除数为0,给当前进程发送信号8号信号
int mian(){
int a=1/0;
return 0;
}

2.野指针
当MMU在执行虚拟地址的物理寻址的时候发生错误,产生11号信号
int main(){
*((int*)0x0)=1;
return 0;
}

4.系统调用产生信号
通过一些系统调用接口产生信号,包括但不限于:
(1)alarm
参数秒后给调用它的进程产生14号信号
#include<iostream>
#include<unistd.h>
using namespace std;
int main(){
alarm(3);
while(1){
cout<<"Runing Pid: "<<getpid()<<endl;
sleep(1);
}
}
(2)abort
调用abort后给调用它的进程产生6号信号,并强制退出.
#include<iostream>
#include<unistd.h>
#include<stdlib.h>
using namespace std;
int main(){
while(1){
cout<<"Runing Pid: "<<getpid()<<endl;
abort();
sleep(1);
}
}
(3)kill
类似于kill指令的用法
#include<iostream>
#include<unistd.h>
#include<sys/types.h>
#include<signal.h>
using namespace std;
int main(){
while(1){
cout<<"Runing Pid: "<<getpid()<<endl;
kill(getpid(),9);//进程自杀
sleep(1);
}
}

信号未决
当信号产生了,但并没有被处理的状态叫做信号未决
可以认为只要pending表对应位置为1,这个信号就是处于未决状态.
那么一个信号能处于未决,就只有两种情况:
1.发现了没处理 -> 被阻塞 ->block表为1
2.没来得及发现 -> 没来得及检测pending表
从而抛出两个问题:
1.如何阻塞信号-> 如何设置block表?
2.何时检测 -> 进程何时检测pending表每个位置的值?
1.如何设置信号阻塞
sigset_t 信号集
sigset_t类型,是os对pending表和block表的封装,相当于位图,可以用操作系统提供的接口来设置对应信号位置的值,然后通过这个sigset_t,可以获取,pending表和修改或覆盖block表
sigset_t myset;
sigemptyset(&myset);//所有信号对应位置全部设置为0
sigfillset(&myset);//所有信号对应位置全部设置为1
sigaddset(&myset,1);//set设置信号 跟位图操作一样 重复多少次也是1
sigdelset(&myset,2);//置为0
操作系统为我们提供了系统接口来设置block表
第一个参数是模式,有三种:
SIG_BLOCK:确保参数二传入的位图中的信号添加至block表(确保为1)
SIG_UNBLOCK:确保传入位图中为1的信号 在block中为0
SIG_SETMASK: 将传入的位图覆盖到block表
第二个参数为要传入的位图,第三个参数可以选择获取原来的位图,不获取可以传入nullptr 代码案例,阻塞2号信号
#include<iostream>
#include<unistd.h>
#include<sys/types.h>
#include<signal.h>
using namespace std;
int main(){
//添加2号信号到block表
sigset_t block_set;
sigaddset(&block_set,2);
sigprocmask(SIG_BLOCK,&block_set,nullptr);
while(1){
cout<<"Runing Pid: "<<getpid()<<endl;
sleep(1);
}
}
把2好信号添加在block表中后,ctrl+c 产生2号信号无法打断进程
需要注意的是,9号和19号信号,这两个信号无法这是阻塞
2.信号的检测机制
进程在从内核态返回用户态时检测信号!
内核态 用户态?
当执行系统调用 或 时间片到了要进行进程切换时,需要执行操作系统的内核级别的代码,这时进程会被切换成内核态,以内核的身份执行内核的代码,执行完毕后再返回用户态.比如,调用open,进程就会被切换成内核态,去执行打开文件的相关代码,完毕后再拿着fd准备切换回用户态继续执行open以后的代码,信号就在这时被检测! 如果pending表为1,block表不为1,就执行hander表对应位置的方法,执行完毕后再切换回用户态,执行后续用户代码.
信号递达(处理)
1.默认方法
使用man手册 man 7 signal 可以查看每个信号的默认处理方法
Term代表终止进程
Core代表终止进程并生成Core_dump文件(如果环境允许)
Ign 代表忽略,donothing
2.自定义方法
我们也可为信号设置自定义行为
方法1: signal
自定义一个 void(int sig)的函数,作为signum的处理方法
#include<iostream>
#include<unistd.h>
#include<sys/types.h>
#include<signal.h>
using namespace std;
void handler(int sig){
cout<<"接收到 "<<sig<<"号信号"<<endl;
cout<<"do nothing"<<endl;
}
int main(){
signal(2,handler);
while(1){
cout<<"Runing Pid: "<<getpid()<<endl;
sleep(1);
}
}
为2号信号设置了自定义捕获方法,3号信号为默认方法
方法2: sigaction
再处理某个信号时,会自动阻塞正在处理的这种信号,处理完后自动接触对这个信号的阻塞.
但如果想再处理某个信号的时候添加对其他信号的阻塞,可以使用sigaction这个系统调用
这里需要传入 sigaction结构体 相当于时对handler表里数据类型的封装
除了 handler mask 其他我们不考虑都可以设为0,就是默认.
mask 为sigset_t类型,代表在执行handler时要阻塞的信号集,handler 依旧为 void(*)(int)类型
#include<iostream>
#include<unistd.h>
#include<sys/types.h>
#include<signal.h>
using namespace std;
void handler(int sig){
cout<<"接收到 "<<sig<<"号信号"<<endl;
while(1){
cout<<"handling"<<endl;
sleep(1);
}
}
int main(){
struct sigaction action;
//设为默认
action.sa_flags=0;
action.sa_restorer=nullptr;
action.sa_sigaction=nullptr;
//自定义行为
action.sa_handler=handler;
//在处理2号信号时 阻塞其他所有信号
sigset_t block_set;
sigfillset(&block_set);
action.sa_mask=block_set;
//与2号信号绑定
sigaction(2,&action,nullptr);//不需要获取曾经的action
while(1){
cout<<"Runing Pid: "<<getpid()<<endl;
sleep(1);
}
}
除了9号与19号不可被阻塞,其他信号都在执行2号信号handler时被阻塞
主动设置忽略
用方法一 方法二 将对应的handler传入 宏SIG_IGN表示忽略这个信号 SIG_DFL代表设置回默认行为,有些信号的默认行为就是忽略,这是不冲突的.
#include<iostream>
#include<unistd.h>
#include<sys/types.h>
#include<signal.h>
using namespace std;
int main(){
//主动设置忽略
signal(2,SIG_IGN);
while(1){
cout<<"Runing Pid: "<<getpid()<<endl;
sleep(2);
}
}
代码案例 :利用信号自动化回收子进程
当子进程退出时,父进程会产生SIGCHLD信号,我们对SIGCHLD设置自定义行为,就可以实现全自动回收子进程.
#include<iostream>
#include<signal.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
using namespace std;
void SIGCHLD_hander(int sig){
while(1){//回收当前所有的僵尸子进程
//WNOHANG 不让父进程阻塞,收到信号就回收当前所有需要回收的
//为什么要用循环和waitpid的返回值控制,不收到一个信号回收一个僵尸??
//因为如果几个子进程同时退出,抵达一个信号,阻塞一个信号,其他信号就丢失了
//故,收到一次信号需要回收当前所有的僵尸子进程,就算丢失了某些进程的信号也能正确回收它们
//回收之后返回做自己的事情,等待信号
pid_t wait_ret=waitpid(-1,nullptr,WNOHANG);
if(wait_ret>0){
cout<<"成功回收子进程 pid: "<<wait_ret<<endl;
continue;
}
if(wait_ret==0){//还有子进程在运行,还是回去做自己的工作,等待信号
cout<<"已经没有需要回收的子进程,我回去做自己的事情了!"<<endl;
break;
}
if(wait_ret<0){//返回0代表没有子进程了
cout<<"所有子进程已经回收完毕!"<<endl;
exit(0);
}
}
}
int main(){
// signal(SIGCHLD,SIG_IGN);//虽然默认方法就算忽略,但若手动绑定,os会做特殊处理,使得子进程退出不给父进程发信号,并且退出后直接被回收!
// signal(SIGCHLD,SIGCHLD_hander);
//sigaction 可以设置处理信号时自动阻塞其他信号
//使得回收僵尸进程完毕后 再处理其他信号
struct sigaction waitpid_act;
waitpid_act.sa_flags=0;
waitpid_act.sa_restorer=nullptr;
sigset_t waitpid_hander_mask;
sigfillset(&waitpid_hander_mask);
waitpid_act.sa_mask=waitpid_hander_mask;
waitpid_act.sa_handler=SIGCHLD_hander;
sigaction(SIGCHLD,&waitpid_act,nullptr);
pid_t fork_ret=1;
int i=0;
for(i=0;i<10;i++){
fork_ret=fork();
if(fork_ret==0){
//child
//(1) 5个进程直接一起退出,看能否全部被回收
//(2) 另外5个等会儿一起退出,看退出前父进程能否继续做自己的事情
//(3) 观察收到信号后能否再返回来回收剩余的5个进程
//(4) 观察能否正确识别,是否还有未退出的子进程
if(i>5){
sleep(10);
}
sleep(1);
exit(0);
}
else
cout<<"创建子进程成功 pid: "<<fork_ret<<endl;
}
while(1){
cout<<"我是父进程,我在做自己的事情"<<endl;
sleep(1);
}
return 0;
}

可见10个子进程全部回收完毕
总结 从产生到递达的全过程
一. 信号通过键盘,硬件异常,系统调用,系统指令等方式产生-> os将对应信号的pending信号集位置由0置为1
二.当进程从用户态陷入内核(要执行系统调用等原因,切换为内核态),执行完内核代码准备返回时,检查pending信号集和block信号集,确认要处理信号
三.执行信号对应处理方法
(1)若为默认行为,继续用内核身份执行
(2)若为自定义行为,切换回用户态,执行用户handle代码
而在 3(2) 这里,切换回用户态执行用户handle方法,在执行过程中,又有可能因为(时间片到了,执行系统调用等原因)陷入内核,返回时再次回到步骤一 处,如此往复,直到某次检测到pending表全为0.
当handler为默认行为
当handler为自定义行为