Linux进程信号

文章详细阐述了Linux系统中进程信号的分类、产生方式,包括终端按键、系统调用、软件条件和硬件异常。同时,介绍了信号的处理机制,如忽略、默认处理和捕获,并通过实例展示了如何自定义信号处理函数。此外,讨论了信号的保存状态(递达、未决、阻塞)以及内核数据结构。最后,文章提及了特定信号如SIGCHLD的处理和volatile关键字的作用。
摘要由CSDN通过智能技术生成


进程信号

信号产生

在Linux系统中信号分为2类:普通信号和实时信号。可以使用kill -l命令查看系统中所有的信号。

其中1-31号信号为普通信号,34-64号信号为实时信号,没有32和33号信号。

实时信号(了解)

常用的信号是普通信号,实时信号主要应用于实时操作系统,Linux操作系统是支持实时的,但一般采用分时。

分时操作系统:CPU基于时间片轮转进行进程调度的OS

实时操作系统:不基于时间片轮转,用于特殊场合,例如军工。

信号的特点

  • 信号的产生相对于进程来说是异步的,进程调度与信号产生没有必然联系
  • 进程接收到信号可能不是立即处理,对于信号的处理可能具有延后性

通过终端按键产生信号

通过键盘上的组合键可以产生信号,键盘是通过硬件中断的方式进行工作的,键盘上的组合键可以被OS识别并解释。若OS识别到用户按下了ctrl + c,会将其解释为信号,写给正在运行的目标前台进程。

通过终端按键只能够向正在运行的前台进程发送信号,无法给后台进程发送信号。

系统调用产生信号

kill系统调用函数可以向指定进程发送指定的信号,函数调用成功返回0,失败返回-1.

#include<sys/types.h>
#include<signal.h>
int kill(pid_t pid,int signum);

demo:使用kill函数模拟实现kill命令

int main(int argc,char* argv[]){
    if(argc!=3){
        cout<<"命令行参数有误"<<endl;
        abort();
    }
    kill(atoi(argv[2]),atoi(argv[1]));
    return 0;
}

raise系统调用函数可以让进程向自己发送指定信号,相当于kill(getpid(),int signum).raise函数调用成功返回0,失败返回-1.

#include<signal.h>
int raise(int signum)
int main(){
    raise(SIGQUIT);//进程向自身发送3号信号SIGQUIT,导致进程core dump
    return 0;
}

abort系统调用函数可以让进程向自身发送SIGABRT信号,进程对于SIGABRT信号的默认处理方式是Core

#include<stdlib.h>
void abort();

abort函数没有返回值,调用总是成功。

由软件条件产生信号

进程在运行过程中不符合某种软件条件时OS会向进程发送信号,例如管道的读端关闭,写端依旧在向管道中写入内容,当写端将管道写满之后,OS会向写端进程发送SIGPIPE信号(13),进程对于SIGPIPE信号的默认处理方式是Term(终止进程)

int main(){
    int pipefd[2]={0};
    pipe(pipefd);
    if(!fork()){
        close(pipefd[0]);
        while(1){
            write(pipefd[1],"123456",6);
        }
        exit(0);
    }
    close(pipefd[0]);
    close(pipefd[1]);
    int status;
    wait(&status);
    cout<<"子进程的退出信号为:"<<(status&0x7f)<<endl;//13号,SIGPIPE
    cout<<"子进程的core dump标记位为:"<<((status>>7)&1)<<endl;
    return 0;
}

设置系统闹钟产生信号也属于软件条件产生信号,可以使用系统调用alarm设置一个闹钟,让内核在若干秒之后向进程发送SIGALRM(14)信号(进程对于SIGALRM信号的默认处理方式是Term)

#include<unistd.h>
unsigned int alarm(unsigned int seconds);

demo:使用alarm函数测试当前机器的算力

int g_val=0;
int main(){
    auto f=[](int signum){
        cout<<"收到"<<signum<<"号信号"<<endl;
        cout<<g_val<<endl;
        exit(0);
    };
    sighandler_t catchf=f;
    signal(SIGALRM,catchf);
    alarm(1);
    while(1){
        g_val++;
    }
    return 0;
}

alarm设定闹钟的特点:alarm设定一个闹钟,在若干秒后,OS会向进程发送SIGALRM(14)号信号,闹钟生效,闹钟一旦触发,会立刻自动解除。

demo:利用alarm的特点实现定时器功能

u_int64_t g_val=0;
int cnt=10;
int main(){
    auto f=[](int signum){
        cout<<"收到"<<signum<<"号信号"<<endl;
        cout<<g_val<<endl;
        if(cnt--==0){
            exit(0);
        }
        alarm(1);
    };
    sighandler_t catchf=f;
    signal(SIGALRM,catchf);
    alarm(1);
    while(1){
        g_val++;
    }
    return 0;
}

软件条件产生信号的原理

由软件条件产生信号在系统层面上的理解:OS识别到某种软件条件被触发(例如alarm设置的闹钟到期)或不满足某种软件条件(例如管道失去了读端)之后,OS会构建信号,写入到指定进程。

OS是进程的管理者,OS有能力识别进程是否触发某些软件条件或不满足某些软件条件,进而给进程发送信号。

SIGPIPE(13)信号的产生原理:

  • 进程之间通过管道通信,先描述,在组织,在内核中一定存在管理管道的内核数据结构,这个数据结构中记录了管道对应哪些读端进程和写端进程,记录了读端进程与写端进程的pid。
  • 当管道的读端进程关闭时,OS检测到该数据结构管理的管道失去了读端进程,便会向写端进程发送SIGPIPE(13)信号。这个检测工作是由内核完成的,用户不必关心其具体过程。

alarm设置定时闹钟产生信号的原理:

  • 通过alarm设置的闹钟本质上也是一种内核数据结构,在同一时刻,OS中可能会有很多进程通过alarm设置了闹钟,OS需要使用特定的数据结构将这些闹钟组织起来(例如链表),并定期检测内核中的过期闹钟,向创建该闹钟的进程发送SIGALRM(14)信号。
  • OS检测系统的过期闹钟是由内核完成的,用户也不必关系其具体细节。

硬件异常产生信号

demo:除0错误和越界野指针问题

int a=10;
a/=0;//除0错误,SIGFPE(8),默认处理方式core dump
int* p=reinterpret_cast<int*>(0x00112233);
*p=10;//野指针,段错误,SIGSEGV(11),默认处理方式core dump
int arr[5]={1,2,3,4,5};
arr[10]=10;//数组越界写,段错误,SIGSEGV(11)

若捕捉因为硬件异常产生的信号,可能会导致死循环。

int main(){
    auto f=[](int signum){
        cout<<"捕捉到"<<signum<<"号信号"<<endl;
        sleep(1);
    };
    sighandler_t catchf=f;
    signal(8,catchf);
    int a=10;
    a/=0;//除0错误,SIGFPE(8),默认处理方式core dump
    return 0;
}

对于除0错误导致硬件异常产生信号的理解:

  1. a/=0指令的计算过程是由CPU执行的,CPU中有一套完整的寄存器,其中有一个寄存器称为状态寄存器,状态寄存器中存在状态标记位溢出标记位,用来记录此次CPU的计算是否发生了除0和溢出。若发生了,那么状态标记位和溢出标记位会被设置,这个操作是由CPU硬件完成的,与OS无关。
  2. 在CPU每一次计算完毕之后,OS会对CPU状态寄存器中的状态标记位和溢出标记位做检测,若检测到状态标记位或溢出标记位被设置,那么OS认为CPU在计算进程的某些数据时发生异常,此时OS会找到当前正在被CPU调度的进程,向其发送SIGFPE(8)信号。如果用户没有自定义捕捉,那么进程默认会core dump。
  3. 在CPU寄存器中的数据属于进程的上下文数据,CPU状态寄存器中的状态标记位和溢出标记位数据也不例外。在分时操作系统中,每一次进程被CPU调度,都要恢复自己的上下文数据,由于CPU状态寄存器中的异常数据一直没有被解决,每一次CPU在调度该进程时,OS对CPU做检测,得到的状态标记位和溢出标记位永远都是1,OS便会向进程继续发送SIGFPE(8)信号。

越界野指针问题导致硬件异常产生信号的理解:

  1. 野指针的解引用和数组的越界访问,都是通过进程地址空间的虚拟地址映射到物理地址来完成的。
  2. 将进程地址空间的虚拟地址转化为物理地址需要借助页表(内核数据结构)和MMU(内存管理单元)完成,MMU属于硬件,在MMU中也有寄存器,当用户代码出现野指针或越界访问时,MMU在进行虚拟地址到物理地址的转化会报错,导致MMU寄存器中的某些数据出现异常,当OS检测到MMU中的数据出现异常时(检测工作由内核完成,用户无需关心具体细节),会向进程发送SIGSEGV(11)信号。

硬件异常产生信号的理解

硬件异常产生信号的本质是硬件中的数据出现异常。因为发生某些错误导致硬件中的一些数据出现异常被OS识别到,然后OS向进程发送特定的信号。

若进程不终止,那么由硬件异常产生的信号问题就是长期性的,只要硬件中的异常数据没有被解决,信号问题就一直存在。一般而言,出现了硬件异常,OS向进程写入相应的信号后,进程会退出,若用户自定义捕捉了信号,进程没有退出,那么硬件异常导致的信号问题一直存在。

信号产生的本质

不论是通过终端按键产生信号、软件条件产生信号、系统调用产生信号,还是硬件异常产生信号,本质上都是通过特定的方式驱使内核向进程写入信号

内核向进程发送信号,实际上是更改了进程PCB(task_struct)中的未决信号集(pending),将对应的比特位由0置为1,当信号递达之后,pending中的对应比特位在由1置为0,因此,向进程发送信号也可称为向进程写入信号。

信号保存

递达、未决、阻塞

递达(Delivery):信号递达指的是进程收到信号,并且处理(忽略、捕捉、默认处理)了信号

未决(pending):信号未决指的是进程收到了信号,但是暂时没有时间没有处理,信号在没有递达之前处于未决状态

阻塞(block):被阻塞的信号会一直处于未决状态,不会被递达,除非阻塞被解除

信号被阻塞与信号被忽略:

  • 信号被阻塞指的是信号没有递达,一直处于未决状态
  • 信号被忽略表示信号已经递达了,并且进程对信号进行了处理,处理方式是忽略

保存信号的内核数据结构

在进程的task_struct内部含有与信号保存相关的3个内核数据结构,分别是阻塞信号集(又称信号屏蔽字,block)、未决信号集(pending),handler。其中信号屏蔽字用来记录那些信号被设置为阻塞,未决信号集用来表示进程当前有哪些信号处于未决状态,未决信号集(pending)和阻塞信号集(block)都是内核中的位图结构,采用比特位来保存信号信息。

task_struct中的handler用来记录进程对于信号的处理方式。

获取pending与设置block

OS提供了系统调用可以让用户设置进程的信号屏蔽字、获取进程的未决信号集。

sigpending
#include<signal.h>
int sigpending(sigset_t* set);

用户可以通过sigpending获取进程的未决信号集,sigpending函数的参数set属于输出型参数,内核会将进程pending内容写入到用户传入的set,供用户读取。

sigpending函数调用成功返回0,失败返回-1,sigpending函数的参数类型sigset_t是OS提供的一种数据类型,称为信号集,用户可以通过该类型,使用特定接口得到进程的block和pending信息。sigset_t使用位图结构,通过位操作可以设置进程的阻塞信号集,得到进程的未决信号集。

OS提供了修改sigset_t类型对象的接口,用户不应该直接对信号集进行操作(而非不能),应当使用系统调用接口进行操作,操作信号集常用的接口:

#include<unistd.h>
int sigemptyset(sigset_t* set);//清空信号集
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);//检测信号集中是否存在特定信号

demo:

int main(){
    auto&& ShowSigset=[](const sigset_t& s){
        //打印信号集
        for(int i=31;i>=1;i--){
            if(sigismember(&s,i)){
                cout<<'1';
            }else{
                cout<<'0';
            }
        }
        cout<<endl;
    };
    sigset_t s;
    ShowSigset(s);//打印没有初始化的信号集
    sigemptyset(&s);
    ShowSigset(s);//打印被清空的信号集
    sigfillset(&s);
    ShowSigset(s);//打印添加了所有信号的信号集
    for(int i=10;i<=20;i++){
        sigdelset(&s,i);//去除信号集中的10~20号信号
    }
    ShowSigset(s);
    for(int i=10;i<=15;i++){
        sigaddset(&s,i);//向信号集中添加10~15号信号
    }
    ShowSigset(s);
    return 0;
}
/*
0000000000000000000000000000000
0000000000000000000000000000000
1111111111111111111111111111111
1111111111100000000000111111111
1111111111100000111111111111111
*/
sigprocmask
#include<signal.h>
int sigprocmask(int how,const sigset_t* set,sigset* oldset);

sigprocmask可以修改进程的信号屏蔽字,同时可以获取进程原来的信号屏蔽字。

参数:

  • how,how参数选择调用sigprocmask函数的哪种功能,对应的选项有3个。
    1. SIG_BLOCK,向进程的阻塞信号集中添加set信号集所包含的信号
    2. SIG_UNBLOCK,将set信号集中包含的信号从进程的阻塞信号信号集移除
    3. SIG_SETMASK,设置进程阻塞信号集的内容为set信号集
  • set,set参数用于修改阻塞信号集
  • oldset,oldset参数可以得到进程原来的阻塞信号集

demo:将所有信号加入阻塞信号集,是否所有信号都不会被递达?

int main(){
    sigset_t s;
    sigfillset(&s);
    sigprocmask(SIG_BLOCK,&s,nullptr);
    for(int i=1;i<=31;i++){
        raise(i);
        printf("%d号信号成功被阻塞\n",i);
    }
    sleep(10);
    exit(0);
}
/*
1号信号成功被阻塞
2号信号成功被阻塞
3号信号成功被阻塞
4号信号成功被阻塞
5号信号成功被阻塞
6号信号成功被阻塞
7号信号成功被阻塞
8号信号成功被阻塞
Killed
*/

OS为了防止恶意进程采用阻塞信号的方式一直占用系统资源,设置了某些信号不能被屏蔽,也不能自定义捕捉,例如SIGKILL(9)和SIGSTOP(19)。其中SIGKILL(9)属于管理员信号,是终止恶意进程的最后一道屏障,因为OS将其设计为不能被屏蔽,不能被捕捉;SIGSTOP(19)是暂停进程的信号,也无法屏蔽,无法捕捉。

对于block,OS提供了获取block的接口(通过sigprocmask的oldset参数可获取)和修改block的接口,对于pending,OS只提供了获取pending的接口,没有提供设置pending的接口,产生信号应当是通过某种手段驱使内核向进程写信号,而非直接修改进程的pending位图。

信号处理

信号的常见处理方式

忽略

进程在收到信号以后什么都不做,内核直接将pending对应的位由1置0.

执行默认处理方式

OS的设计者在设计进程的时候,给每一个进程设定了对于信号默认的处理方式,这个属性是进程天然就有的,可以通过man 7 signal进行查看,例如对于SIGHUP(1)信号,默认处理方式是Term(终止进程),对于SIGABRT(6)信号,默认处理方式是Core,Core也是终止进程,与Term的区别在于以Core的方式终止进程,可能会产生core文件,发生核心转储

一般而言,在服务器上,默认核心转储功能处于关闭状态;在虚拟机中,核心转储功能处于开启状态,可以使用ulimit -a查看相关信息,若核心转储功能处于关闭状态,可以使用命令ulimit -c 4096开启核心转储功能,4096是指定生成core文件的大小为4096Bytes。

demo:创建一个子进程,让其以Core的方式终止,通过wait/waitpid拿到子进程的core dump标志。

int main(){
    if(fork()==0){
        abort();//子进程向自己发送SIGABRT(6)信号,默认处理方式是Core
    }
    int status;
    wait(&status);
    cout<<"子进程的退出信号为:"<<(status&0x7f)<<endl;
    cout<<"子进程的core dump标记是否被设置:"<<(((status>>7)&1)?"是":"否")<<endl;
    cout<<"子进程的退出码为:"<<((status>>8)&0xff)<<endl;
    return 0;
}

当子进程以Core的方式终止,若系统的核心转储功能处于开启状态,那么OS会将子进程在内存中的核心数据转存到磁盘,生成一个core文件,文件后缀为子进程的pid,core文件可以gdb调试,帮助定位子进程产生信号的位置,可以在gdb中使用命令core-file core.???进行调试。

在一般的生成环境中系统的core dump选项默认处于关闭状态,主要是为了避免进程因为Core退出产生的core文件占用磁盘空间。

进程对于信号的默认处理方式除了Term和Core之外,还有Lgn(对于SIGCHLD(17)信号,父进程的默认处理方式是忽略)、Stop(对于SIGSTOP(19)信号,进程的默认处理方式是暂停当前进程)、Cont(对于SIGCONT(18)信号,进程的默认处理方式是继续执行)。

捕捉信号

使用系统调用signal可以实现对信号的捕捉,用户可以通过signal注册一个函数,用来处理特定的信号,这样进程在收到该信号时,会由内核态切换到用户态,调用注册的函数。

#include<signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum,sighandler_t handler);

signal函数调用成功会返回之前的信号处理函数,若之前对于信号采取的方式是默认处理,那么signal函数返回SIG_DFL(#define SIG_DFL ((sighandler_t)0));若signal函数调用失败,返回SIG_ERR(#define SIG_ERR ((sighandler_t)-1))。

demo:是否所有的信号都能被捕捉?

int main(){
    auto f=[](int signum){
        cout<<"进程收到了"<<signum<<"号信号"<<endl;
    };
    sighandler_t catchf=f;
    for(int i=1;i<=31;i++){
        signal(i,catchf);//将所有信号都捕捉
    }
    for(int i=1;i<=31;i++){
        raise(i);//向自己发送信号
    }
    return 0;
}

SIGKILL(9)和SIGSTOP(19)信号不能被捕捉,SIGKILL(9)和SIGSTOP(19)既不能被捕捉,也不能被设置为阻塞。

信号的处理时机

进程在由内核态返回到用户态时会对信号进行处理。原因:进程对信号进行处理的本质是将进程的未决信号集对应的比特位由1置为0,然后调用handler表中相应的处理方法,进程的未决信号集和handler表都是属于内核数据结构,必须在进程处于内核态时才能进行修改和访问,OS的设计者没有选择在进程刚进入内核时对信号进行处理,而是选择了在进程由内核态返回用户态的时候进行处理。

内核态与用户态

用户态是一个受管控的状态,权限相对有限,内核态时OS执行自己代码的一个状态,内核态具备非常高的优先级和权限。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-G0a55KLi-1676432396509)(D:\Typora图片\image-20230113204217714.png)]

进程进入内核态的方式:

  • 进行系统调用
  • 进程因为缺陷、陷阱、异常而进入内核态
  • 进程被调度或者切换,进程被调度或者切换是通过OS执行进程切换的代码实现的(例如OS执行switch process函数进行进程切换),本质上还是在进行系统调用。在进行进程切换的时候,需要执行OS的进程切换代码将进程在寄存器中的上下文数据保存在PCB中;在进程重新被调度时,需要执行OS与进程调度相关的代码,将进程PCB中的上下文数据恢复到寄存器中。

从根本上来说,进程想要由用户态进入内核态,都是通过系统调用实现的,OS为了系统安全,需要确保用户想要进入内核只能是通过系统调用,不能通过其它手段。系统调用函数中内嵌了int 80汇编指令(80号中断),int 80汇编指令可以让用户陷入内核,将代码的执行权限交给OS。

进程地址空间的内核空间

在进程地址空间中,[0GB,3GB]是用户空间,[3GB,4GB]是内核空间,由用户态进入内核态,就是从进程地址空间的用户空间切换到内核空间,执行OS的代码,在由用户空间切换到内核空间时,需要更改CPU寄存器中的数据。

一般CPU的寄存器有2套,一套是可见寄存器,给用户用的,一套是不可见寄存器,CPU自己用的,在不可见寄存器中,存在一个CR3寄存器,CR3寄存器中的数据用来表示当前CPU的执行权限是内核态还是用户态,当进程由用户空间切换到内核空间想要执行OS的代码时,OS会修改CPU的执行权限为内核态,这样才能执行OS的代码。若用户通过非法手段想要进入内核空间,OS是不会修改CPU的执行权限的,CPU的执行权限依然是用户态,无法执行OS的代码;只有用户是通过系统调用的方式进入内核空间,OS才会修改CPU的执行权限。

进程从用户态进入内核态:从进程地址空间的用户空间切换到内核空间,OS修改CPU的执行权限为内核态,进程调用OS的代码

进程从内核态切回用户态:进程执行OS的代码完毕,从内核空间返回用户空间,OS修改CPU的执行权限为用户态,进程在用户空间继续执行用户的后序代码

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sQospi5M-1676432396510)(D:\Typora图片\image-20230113212624328.png)]

每一个进程都有用户地址空间和内核地址空间,进程的用户地址空间映射到物理内存的不同区域,内核地址空间映射到物理内存的同一区域,因为OS只有一份。

判断进程处于用户态还是内核态的依据是CPU的执行权限是内核态还是用户态,进程处于内核态还是用户态是与硬件相关的。

OS存在于每一个进程的进程地址空间上下文中,每一个进程通过自己[3GB,4GB]的内核空间+内核级页表(内核级页表只有一份,所有进程共享)就可以访问OS的代码和数据。实际上进程进行系统调用和使用动态库的代码没有本质区别,只不过CPU的执行权限不一样,使用的页表不一样。

捕捉信号

信号捕捉,并没有创建新的进程或线程。

demo:在信号捕捉函数中打印线程id和进程的pid

int main()
{
    cout << "进程的pid为" << getpid() << endl;
    cout << "线程id为" << pthread_self() << endl;
    auto &&f = [](int signum) -> void
    {
        cout << "收到" << signum << "号信号" << endl;
        cout << "进程的pid为" << getpid() << endl;
        cout << "线程id为" << pthread_self() << endl;
    };
    struct sigaction act;
    act.sa_handler = f;
    sigaction(SIGINT, &act, nullptr);
    sleep(5);
    return 0;
}
/*
进程的pid为3046
线程id为140573100148544
^C收到2号信号
进程的pid为3046
线程id为140573100148544
*/
捕捉信号的流程

时钟中断的概念:时钟中断属于硬件层面上的概念,CPU每隔一段时间会向OS发送时钟中断,告诉OS进程的时间片到了,OS要执行进程调度的代码,调度下一个进程。

内核态与用户态的切换是通过软硬件结合的方式完成的,在硬件上CPU有寄存器,在软件上,进程有进程地址空间,软硬件结合完成内核态到用户态的切换。

信号的处理流程:

  • 用户进程因为系统调用或者CPU调度的原因进入内核
  • OS的调度代码执行完毕或系统调用代码执行完毕,从内核态返回到用户态之前,进行信号检测
  • 若检测到未决信号集中有信号,会调用handler表中的信号处理方法
    1. 若handler表中的信号处理方法为忽略,则将未决信号集对应的位置由1置为0,然后直接返回到用户态,执行用户代码
    2. 若handler表中的信号处理方法为默认,则将未决信号集对应的位置由1置为0,执行OS的默认处理方法(一般是终止进程)
    3. 若handler表中的处理方式是用户自定义捕捉,那么进程会先从内核态返回到用户态,执行用户的捕捉函数(注意OS可以在内核态调用用户的捕捉函数,但是OS不会在内核态调用用户的捕捉函数,因为内核态的优先级和权限过高,若用户的捕捉函数中存在非法操作,会影响整个OS的安全性),在用户态将捕捉函数执行完毕以后,进程会自动调用sigreturn系统函数再次进入内核,修改进程的pending信号集,将对应比特位由1置为0。然后调用sys_sigreturn函数返回到用户,继续执行用户代码

信号检测的流程可以用下图表示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QLrjGGJa-1676432396511)(D:\Typora图片\image-20230113223139529.png)]

  1. 进程因为系统调用或者CPU执行进程的调度代码,进程进入内核态
  2. 进程从内核态返回用户态时对信号做检测,需要执行用户自定义的回调函数,并且将pending位图对应的比特位置为0
  3. 进程从内核返回用户,执行信号的回调函数
  4. 执行回调函数完毕,再次进入内核
  5. 准备从内核返回用户
  6. 从内核返回用户完成,继续执行用户代码。第5步和第6步之间没有信号检测的过程,是直接返回到用户的过程

整个过程一共有2次进入内核,2次从内核回到用户。

sigaction函数

进行信号捕捉可以使用signal函数

#include<signal.h>
typedef void(*sighandler_t)(int);
sighandler_t signal(int signum,sighandler_t handler);

若使用signal函数对SIGINT(2)信号进行了捕捉,当进程收到2号信号时,会调用自定义的捕捉函数,在调用捕捉函数期间,进程会自动阻塞2号信号。

demo:进程正在处理SIGINT(2)信号时,再次收到SIGINT(2)信号

int main()
{
    auto catchfunc = [](int signum) -> void
    {
        cout << "进程收到" << signum << "号信号" << endl;
        sigset_t sigset;
        sigemptyset(&sigset);
        int cnt = 5;
        while (cnt--)
        {
            sigpending(&sigset);
            for (int i = 31; i >= 1; i--)
            {
                cout << (sigismember(&sigset, i) ? '1' : '0');
            }
            cout << endl;
            raise(SIGINT); // 进程再次向自己发送2号信号,此时2号信号被屏蔽
            sleep(1);
        }
    };
    signal(SIGINT, catchfunc);
    sleep(5);
    return 0;
}

进程在由内核态返回到用户态,会对信号做检测,从内核态切换到用户态执行回调函数之前,会先将未决信号集的对应比特位置为0

进程若正在某一个信号时,该信号会被自动设置为阻塞。

#include<signal.h>
int sigaction(int signum,const struct sigaction* act/*输入型参数*/,struct sigaction* oldact/*输出型参数*/);
struct sigaction{
	void(*sa_handler)(int);//信号捕捉函数
    sigset_t sa_mask;
    //...............
};

sigset_t sa_mask:在调用信号处理函数时,进程会自动屏蔽当前信号,如果还希望进程自动屏蔽一些别的信号,那么需要使用sa_mask字段额外说明。

demo:使用sigaction函数,让进程在处理SIGINT(2)信号的时候,除了自动屏蔽SIGINT(2)信号之外,还屏蔽SIGQUIT(3)信号。

int main()
{
    auto f = [](int signum) -> void
    {
        cout << "收到" << signum << "号信号" << endl;
        sigset_t sigset;
        sigemptyset(&sigset);
        while (true)
        {
            sigpending(&sigset);
            for (int i = 31; i >= 1; i--)
            {
                cout << sigismember(&sigset, i) ? '1' : '0';
            }
            cout << endl;
            raise(SIGINT);
            sleep(1);
        }
    };
    struct sigaction act, oact;
    act.sa_flags = 0;
    act.sa_handler = f; // 设置回调函数
    sigemptyset(&act.sa_mask);
    sigaddset(&act.sa_mask, 3); // 进程在处理某一个信号时,额外屏蔽3号信号
    sigaction(SIGINT, &act, &oact);
    // 打印进程原来的sa_mask
    for (int i = 31; i >= 1; i--)
    {
        cout << sigismember(&oact.sa_mask, i) ? '1' : '0';
    }
    cout << endl<<"------------------------------"<<endl;
    sleep(5);
    return 0;
}

可重入函数的概念

可重入与不可重入是函数的一种特性,可重入函数。指的是多个执行流同时调用这个函数,不会出现问题,这样的函数叫做可重入函数,一般的函数都是不可重入的函数。大多数函数都是不可重入函数

volatile关键字

远程拷贝数据的命令scp

scp code.tgz slowstep@192.144.141:/home/

volatile关键是保持内存可见性的一个关键字。

int main()
{
    const int x = 10;
    *const_cast<int *>(&x) = 20;
    cout << x << endl;//10,g++编译器认为x被const修饰,不会被改变,因此读取x的值直接在寄存器读取
    return 0;
}

用volatile修饰变量表示每一次在读取变量的时候都在内存中读取。

int main()
{
    volatile const int x = 10;
    *const_cast<int *>(&x) = 20;
    cout << x << endl;//20
    return 0;
}

在信号捕捉的场景下也有类似情况

int flag = 1;
void catchfun(int signum)
{
    cout << "save " << signum << " signal" << endl;
    cout << "has changed g_val" << (flag = 0) << endl;
}
int main()
{
    struct sigaction act;
    act.sa_handler = catchfun;
    sigaction(SIGINT, &act, nullptr);
    while (flag);
    cout << "flag=" << flag << endl;
    return 0;
}

当采用g++编译器,使用-O3进行代码优化时,while循环每一次判断都会从寄存器读取flag的值,因为编译器在编译代码的时候认为main函数中没有修改falg的操作,所以即使在内存中修改了falg的值为0,while循环依旧从寄存器读取,不会退出。

加了volatile关键字之后,while循环每一次判断都会从内存中读取flag的值。

volatile int flag = 1;
void catchfun(int signum)
{
    cout << "save " << signum << " signal" << endl;
    cout << "has changed g_val" << (flag = 0) << endl;
}
int main()
{
    struct sigaction act;
    act.sa_handler = catchfun;
    sigaction(SIGINT, &act, nullptr);
    while (flag);
    cout << "flag=" << flag << endl;
    return 0;
}

volatile关键字在编译器进行编译的时候就已经起效果了,但是在运行的时候才真正发挥作用。

SIGCHLD信号

当子进程被暂停或终止,子进程会主动向父进程发送SIGCHLD(17)信号,父进程的默认处理方式是忽略。

demo:创建10个子进程,利用SIGCHLD信号的机制进行非阻塞回收子进程。

int main()
{
    auto &&f = [](int signum) -> void
    {
        cout << "收到" << signum << "信号" << endl;
        // 由于不知道是哪一个子进程发过来的SIGCHLD(17)信号,可能是多个子进程同时发送,因此需要循环检测回收
        pid_t id = 0;
        while ((id = waitpid(-1, nullptr, WNOHANG)) > 0)
        {
            cout << "成功回收pid为" << id << "的子进程" << endl;
        }
    };
    struct sigaction act;
    act.sa_handler = f;
    sigaction(SIGCHLD, &act, nullptr);
    for (int i = 0; i < 10; i++)
    {
        if (fork() == 0)
        {
            exit(0);
        }
    }
    sleep(3);
    return 0;
}

如果父进程即不想等待子进程,并且还想让子进程退出之后,自动释放僵尸进程,可以手动设置对SIGCHLD(17)忽略。

#define SIG_DFL ((__sighandler_t)0)
#define SIG_IGN ((__sighandler_t)1)
signal(SIGCHLD,SIG_IGN);
struct sigaction act;
act.sa_handler=SIG_IGN;
sigaction(SIGCHLD,&act,nullptr);

SIGCHLD(17)的默认处理方式是忽略,这个忽略指的是OS级别的忽略,表示OS不管用户创建的子进程,子进程的回收工作应该由父进程完成

使用signal(SIGCHLD,SIG_IGN)表示用户告诉OS,用户忽略对于创建的子进程的回收工作,要求OS自动回收用户创建的子进程。

int main()
{
    struct sigaction act;
    act.sa_handler = SIG_IGN;
    sigaction(SIGCHLD, &act, nullptr);
    for (int i = 0; i < 10; i++)
    {
        if (fork() == 0)
        {
            cout << "子进程pid为" << getpid() << endl;
            exit(0);
        }
    }
    sleep(20);
    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值