Linux | 进程信号 产生未决到递达

发布于:2022-11-27 ⋅ 阅读:(347) ⋅ 点赞:(0)

目录

信号产生

常见的信号产生方式

1.键盘产生 

2.kill 指令

3.硬件出错

 4.系统调用产生信号

信号未决

1.如何设置信号阻塞

sigset_t  信号集

2.信号的检测机制

内核态 用户态?

信号递达(处理)

1.默认方法

2.自定义方法

方法1: signal

方法2: sigaction

主动设置忽略

​编辑代码案例 :利用信号自动化回收子进程

总结 从产生到递达的全过程

当handler为默认行为

当handler为自定义行为


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指令发送信号

当进程检测到信号,执行了kill信号的默认行为:终止进程.

3.硬件出错

当硬件执行出错,也会给进程发送信号,包括但不限于如下情况:

1.除0错误

如当cpu在执行运算时,发现除数为0,给当前进程发送信号8号信号

int mian(){
    int a=1/0;
    return 0;
}
除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为自定义行为

        

本文含有隐藏内容,请 开通VIP 后查看