【Linux】信号

  1. 信号入门

1.1.看看信号

1.2.感性的认识信号

生活角度的信号

技术应用角度的信号

共识:信号是进程发的 例如: kill -9 pid

例如:用户输入命令,在Shell下启动一个前台进程。

用户按下Ctrl-C ,这个键盘输入产生一个硬件中断,被OS获取,解释成2号信号,发送给目标前台进程前台进程因为收到信号,进而引起进程退出。
#include <stdio.h>
int main()
{
     while(1){
     printf("I am a process, I am waiting signal!\n");
     sleep(1);
     }
}

注意:

1. Ctrl-C 产生的信号只能发给前台进程。一个命令后面加个&可以放到后台运行,这样Shell不必等待进程结束就可以接受新的命令,启动新的进程。

2. Shell可以同时运行一个前台进程和任意多个后台进程,只有前台进程才能接到像 Ctrl-C 这种控制键产生的信号。这就是为什么我们在bash终端等待指令的时候 ctrl+c 和 ctrl + \ 可以退出。

3. 前台进程在运行过程中用户随时可能按下 Ctrl-C 而产生一个信号,也就是说该进程的用户空间代码执行到任何地方都有可能收到 SIGINT 信号而终止,所以信号相对于进程的控制流程来说是异步(Asynchronous)的。

  1. 信号概念

信号是进程之间事件异步通知的一种方式,属于软中断。

查看信号和信号对应的含义

查看全部信号
查看每个信号的默认捕捉方法

每个信号都有一个编号和一个宏定义名称,这些宏定义可以在signal.h中找到,例如其中有定 义 #define SIGINT 2

编号34以上的是实时信号,本章只讨论编号34以下的信号,不讨论实时信号。这些信号各自在什么条件下产生,默认的处理动作是什么,在signal(7)中都有详细说明: man 7 signal

信号处理常见方式概览

(sigaction函数稍后详细介绍),可选的处理动作有以下三种:

1. 忽略此信号。

2. 执行该信号的默认处理动作。

3. 提供一个信号处理函数,要求内核在处理该信号时切换到用户态执行这个处理函数,这种方式称为捕捉(Catch)一个信号

  1. 调用接口

signal函数:修改信号处理方法

我们上一个例子中,OS可以捕捉键盘的ctrl+c 解释成2号:2)SIGINT信号,用来终止进程。我们可以通过signal接口修改2)SIGINT信号的默认行为。

举例:

#include <stdio.h>
#include<iostream>
#include<unistd.h>
#include <sys/types.h>
#include <unistd.h>
#include<signal.h>
void handler(int signo)
{
    std::cout<<"我捕捉到2号信号,并且自定义了处理方法"<<std::endl;
}
int main()
{
    //这里是signal函数的调用,不是handler函数的调用
    //这里只是设置了对2号进程的的捕捉方法,并不代表这此方法被调用了
    //这个handler函数在收到对应的信号才能执行。
    signal(2 , handler);
     while(1){
        std::cout<<"我是一个进程,我正在执行!!我的pid:"<<getpid()<<std::endl;
        sleep(1);
     }
}

我们看到了此时我们ctrl+c 不能终止此进程了。kill -2 发送信号也不终止进程了,但是我们发送kill -3 信号可以退出。kill -9 可以退出,我们没有修改3号 和 9号的捕捉方法。

有人就会说了我们吧所有的信号都自定义捕捉了,是不是进程就无法通过信号推出了呢?

void handler(int signo)
{
    std::cout<<"捕捉到一个信号:"<<signo<<std::endl;
    //不退出;
}

int main()
{
    //自定义所有信号的捕捉方法;
    for(int i =1; i<=31; i++)
    {
        signal(i , handler);
    }

    while(1)
    {
        std::cout<<"进程运行中!!pid:"<<getpid()<<std::endl;
        sleep(3);
    }
    return 0;
}

通过发送信号,我们可以得到只有9(退出), 19(暂停运行) , 18(继续运行)号信号不能被自定义捕捉,其他信号都可以自定义捕捉。9号信号被称为管理员信号。来杀掉所有的异常进程。

kill函数:给任意进程发送信号

kill 不仅仅是一个指令还是一个系统调用,kill指令底层就是使用了kill系统接口

#include <stdio.h>
#include<iostream>
#include<unistd.h>
#include <sys/types.h>
#include <unistd.h>
#include<signal.h>

void Uasge(const char* str)
{
    std::cout<<"\nuasge:"<<str<< " pid signal\n"<<std::endl;
}
int main(int argc, char* argv[]){
    if(argc != 3){
        std::cout<<"Usage error"<< std::endl;
        Uasge(argv[0]);
        exit(1);
    }
    pid_t id = atoi(argv[1]);
    int sig = atoi(argv[2]);
    int ret = kill(id,sig);
    //成功返回0 ,不成功返回-1
    if(ret == -1){
        perror("kill");
        exit(1);
    }
    return 0;
}

raise函数:给自己进程发送信号


int main()
{
    int cnt =0;
    while(true)
    {

        printf("cnt: %d\r", cnt);
        fflush(stdout);
        cnt++;

        if(cnt == 10)
        {
            int ret = raise(3);// * == kill(getpid(),3);
            if(ret == -1)
            {
                perror("raise");
                exit(1);
            }
        }

        sleep(1);
    }

    return 0;
}

abort函数:使当前进程接收到信号而异常终止。

首先解除对SIGABRT信号的阻塞,然后为调用进程发出该信号。这会导致进程异常终止,除非SIGABRT信号被捕获,信号处理程序不会返回
这个指定的信号是6) SIGABRT,导致进程退出。
#include <stdlib.h>
void abort(void);// * == kill(getpid(), SIGABRT);
就像exit函数一样,abort函数总是会成功的,所以没有返回值。

int main()
{
    int cnt =0;
    while(true)
    {

        printf("cnt: %d\r", cnt);
        fflush(stdout);
        cnt++;

        if(cnt == 10)
        {
            abort();
        }
        sleep(1);
    }

    return 0;
}

  1. 信号产生

4.1.通过终端按键产生信号

(ctrl+c):SIGINT的默认处理动作是终止进程,(ctrl+\):SIGQUIT的默认处理动作是终止进程并且Core Dump,现在我们来验证一下。

Core Dump

首先解释什么是Core Dump。当一个进程要异常终止时,可以选择把进程的用户空间内存数据全部 保存到磁盘上,文件名通常是core,这叫做Core Dump。进程异常终止通常是因为有Bug,比如非法内存访问导致段错误,事后可以用调试器检查core文件以查清错误原因,这叫做Post-mortem Debug(事后调试)。一个进程允许产生多大的core文件取决于进程的Resource Limit(这个信息保存 在PCB中)。默认是不允许产生core文件的,因为core文件中可能包含用户密码等敏感信息,不安全。在开发调试阶段可以用ulimit命令改变这个限制,允许产生core文件。 首先用ulimit命令改变Shell进程的Resource Limit,允许core文件最大1024K: $ ulimit -c1024

4.2.调用系统函数向进程发信号

我们可以通过一个进程调用系统接口去给另外一个信号发送信号,所以OS只有发送信号的能力,但是具体的发送给谁发送什么信号是进程实现的。

包括我们前面自己实现的mykill和系统指令kill都是进程,都是通过一个进程向另一个进程发送信号。

10413是test进程的id。之所以要再次回车才显示 Segmentation fault ,是因为在4568进程终止掉 之前已经回到了Shell提示符等待用户输入下一条命令,Shell不希望Segmentation fault信息和用户的输入交错在一起,所以等用户输入命令之后才显示。

指定发送某种信号的kill命令可以有多种写法,上面的命令还可以写成 kill -SIGSEGV 4568 或 kill -11 4568 , 11是信号SIGSEGV的编号。以往遇 到的段错误都是由非法内存访问产生的,而这个程序本身没错,给它发SIGSEGV也能产生段错误。

kill命令是调用kill函数实现的。kill函数可以给一个指定的进程发送指定的信号。raise函数可以给当前进程发送指定的信号(自己给自己发信号)。

#include <signal.h>
int kill(pid_t pid, int signo);
int raise(int signo);
//这两个函数都是成功返回0,错误返回-1。

4.3.由软件条件产生信号

SIGPIPE是一种由软件条件产生的信号,在“管道”中,如果读取端口关闭,会给写入端口进程发信号13) SIGPIPE,终止写入 端进程。管道就是软件条件产生信号。

本节主要介绍alarm函数 和14)SIGALRM信号。

#include <unistd.h>
unsigned int alarm(unsigned int seconds);
调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发SIGALRM信号, 
该信号的默认处理动作是终止当前进程。

这个函数的返回值是0或者是以前设定的闹钟时间还余下的秒数。

打个比方,某人要小睡一觉,设定闹钟为30分钟之后响,20分钟后被人吵醒了,还想多睡一会儿,于是重新设定闹钟为15分钟之后响,“以前设定的闹钟时间还余下的时间”就是10分钟。

如果seconds值为0,表示取消以前设定的闹钟,函数的返回值仍然是以前设定的闹钟时间还余下的秒数。

//这个程序的作用是1秒钟之内不停地数数,1秒钟到了就被SIGALRM信号终止。
int cnt = 0;
void handler(int signo)
{
    std::cout<<"捕捉到一条信号:"<<signo<<std::endl;
    std::cout<<cnt<<std::endl;
    alarm(1);//捕捉之后我们再给进程设置一个闹钟。//这个逻辑就很像sleep了。
    //exit(1);
}

//此进程就是每间隔1秒打印cnt的值。
int main()
{
    alarm(1);//设置一个时间为1秒的闹钟。
    //一秒后给自己进程发送 14) SIGALRM 
    //一次性闹钟。
    signal(14, handler);
 
    while(true)
    {
        cnt++;
        //std::cout<<cnt<<std::endl;//如果我们在这里每++一次打印一次,cnt就加不到那么大
        //说明IO真的很慢很慢。
    }
    return 0;
}

OS的闹钟就是使用软件实现的。

任意一个进程都可以通过系统调用设置自己的闹钟,OS内部内部会存在很多的闹钟,所以所有的闹钟在OS内部会被管理起来(先描述再组织)。

4.4.硬件异常产生信号

硬件异常被硬件以某种方式被硬件检测到并通知内核,然后内核向当前进程发送适当的信号

例如当前进程执行了除以0的指令,CPU的运算单元会产生异常,内核将这个异常解释 为SIGFPE信号发送给进程。
再比如当前进程访问了非法内存地址,,MMU会产生异常,内核将这个异常解释为SIGSEGV信号发送给进程。

举例:除0

void catchsig(int signo)
{
    std::cout<<"我捕捉到一个信号:"<<signo<<std::endl;
}
int main()
{
    signal(SIGFPE, catchsig);
    while(true)
    {
        std::cout<<"我是一个进程,我正在运行——————————"<<std::endl;
        sleep(1);
        int i =10;
        i = i/0;
    }
    return 0;
}

实验现象可以看到OS会一直不停的给进程发送终止信号,这是为什么呢?

当进程在CPU执行的时候,遇到 / 0 的错误,CPU的状态寄存器的溢出标识位会由0 置1,CPU就出现了异常,OS内核将这个异常解释 为SIGFPE信号发送给进程。但是此时进程的捕捉方法被我们自己修改为自定义方法了,进程并未退出,还会被调度器调度,而且电脑里面的CPU只有一份,CPU中各个寄存器的数据有很多属于各个进程的上下文,进程切换的时候,就有无数的次的将状态寄存器的数据保存和恢复的过程,每一次的恢复,就让OS识别到CPU内部的状态寄存器的溢出标记位为1,就会给进程发送信号,每一次时间片轮转就会给进程发送一次信号。下面的对空指针解引用也是相同的原理。MMU发送异常,OS内核将这个异常解释为对应的11)SIGSEGV信号发送给进程。

举例:空指针解引用

void handler(int sig)
{
    printf("catch a sig : %d\n", sig);
}
int main()
{
    signal(SIGSEGV, handler);
    sleep(1);
    int *p = NULL;
    *p = 100;
    while (1)
        ;
    return 0;
}

由此可以确认,我们在C/C++当中除零,内存越界等异常,在系统层面上,是被当成信号处理的。硬件异常,OS会向进程发送对应的信号。根据不同的信号虽然处理结果一般都是退出,但是代表不同的原因,例如收到段错误,可能是进程出现野指针,内存越界等错误,收到浮点数溢出错误,可能是进程中/0 了。

4.5.对信号处理的理解

截至当前,我们看到进程受到的大部分信号的默认处理方法都是终止进程
所以信号的不同不是由处理方法决定的。
信号的不同,代表着不同的处理事件,但是对于事件发送之后的处理动作可以一样!

4.6.term和core(核心转储问题)

term是正常结束,而core在结束之前要做一些其他的事情。

int main()
{
    //核心转储
    int arr[10];
    arr[10000] = 100;//数组越界
    //C语言对于数组的越界是通过抽查的方式检查的。不一定能检查出来。
    //数组越界不一定会导致程序崩溃,
    return 0;
}

但是我们实验的时候并没有发送什么不同。好像只是打印的终止原因。

这是因为:在云服务器上 ,默认如果进程是core退出的,我们暂时看不到明显的现象,如果想看到现象,就需要打开一个选项core file size 选项。(打开核心转储)

核心转储的目的是:为了支持调试。

这种调试称为事后调试。只有以core退出的进程才可以发生核心转储。term退出的不行。

term是正常退出。

4.7.总结思考

上面所说的所有信号产生,最终都要有OS来进行执行,为什么?

OS是进程的管理者,只有OS才有权限去发送信号(修改PCB对应的信号位图)。

信号的处理是否是立即处理的?

在合适的时候,内核态到用户态期间。

信号如果不是被立即处理,那么信号是否需要暂时被进程记录下来?记录在哪里最合适呢?

需要,保存在进程PCB中。

一个进程在没有收到信号的时候,能否能知道,自己应该对合法信号作何处理呢?

知道,PCB中有有对应的指针指向:处理方法的函数指针数组。

如何理解OS向进程发送信号?

OS发送信号的本质就是OS修改PCB的信号位图。

  1. 信号的保存

OS会提供对应的接口,让我们对这些数据结构进行访问。

注意:信号没有产生也可以被阻塞,pending和block位图是分开的。

注意:因为是位图结构,所以信号不会叠加,相同信号只能保存一个。

  1. 阻塞信号

5.1.基本概念

实际执行信号的处理动作称为信号递达(Delivery)。

信号从产生到递达之间的状态,称为信号未决(Pending)

进程可以选择阻塞 (Block )某个信号。

被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作.

注意:阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作

5.2. 在内核中的表示

每个信号都有两个标志位分别表示阻塞(block)和未决(pending),还有一个函数指针表示处理动作。信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。在上图的例中, SIGHUP 信号未阻塞也未产生过,当它递达时执行默认处理动作。

SIGINT信号产生过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略,但在没有解除阻塞之前不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞。

SIGQUIT信号未产生过,一旦产生SIGQUIT信号将被阻塞,它的处理动作是用户自定义函sighandler。

如果在进程解除对某信号的阻塞之前这种信号产生过多次,将如何处理?POSIX.1允许系统递送该信号一次或多次。Linux是这样实现的:常规信号在递达之前产生多次只计一次,而实时信号在递达之前产生多次可以依次放在一个队列里。本章不讨论实时信号。

5.3.sigset_t

概念

从上图来看,每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。下面将详细介绍信号集的各种操作。 阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask),这里的“屏蔽”应该理解为阻塞而不是忽略。

各种操作

sigset_t类型对于每种信号用一个bit表示“有效”或“无效”状态,至于这个类型内部如何存储这些bit则依赖于系统实现,从使用者的角度是不必关心的,使用者只能调用以下函数来操作sigset_ t变量,而不应该对它的内部数据做任何解释,比如用printf直接打印sigset_t变量是没有意义的。

#include <signal.h>
int sigemptyset(sigset_t *set);//清空(置0)
int sigfillset(sigset_t *set);//置1
int sigaddset (sigset_t *set, int signo);//添加signo信号
int sigdelset(sigset_t *set, int signo);//删除signo信号
int sigismember(const sigset_t *set, int signo);//判断是否包含signo信号
//这四个函数都是成功返回0,出错返回-1。

函数sigemptyset初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含任何有效信号。

函数sigfillset初始化set所指向的信号集,使其中所有信号的对应bit置位,表示该信号集的有效信号包括系统支持的所有信号。

注意,在使用sigset_ t类型的变量之前,一定要调 用sigemptyset或sigfifillset做初始化,使信号集处于确定的状态。初始化sigset_t变量之后就可以在调用sigaddset和sigdelset在该信号集中添加或删除某种有效信号

sigismember是一个布尔函数,用于判断一个信号集的有效信号中是否包含某种 信号,若包含则返回1,不包含则返回0,出错返回-1。

5.4.sigprocmask:读取或更改进程的信号屏蔽字(阻塞信号集)。

调用函数sigprocmask可以读取或更改进程的信号屏蔽字(阻塞信号集)。

#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset); 
//返回值:若成功则为0,若出错则为-1

如果oset是非空指针,则读取进程的当前信号屏蔽字通过oset参数传出。如果set是非空指针,则 更改进程的信号屏蔽字,参数how指示如何更改。如果oset和set都是非空指针,则先将原来的信号 屏蔽字备份到oset里,然后根据set和how参数更改信号屏蔽字。假设当前的信号屏蔽字为mask,下表说明了how参数的可选值。

如果调用sigprocmask解除了对当前若干个未决信号的阻塞,则在sigprocmask返回前,至少将其中一个信号递达。从内核态到用户态,会检查未决信号集(pending).并且捕捉信号。

5.5.sigpending:读取pending信号位图

#include <signal.h>
int sigpending(sigset_t *oset);
//读取当前进程的未决信号集,通过set参数传出。调用成功则返回0,出错则返回-1。 

举例:

void printsig(const sigset_t p)
{
    for( int i = 1; i<= 31; i++)
    {
        if(sigismember(&p,i)){
            std::cout<<'1';
        }
        else{
            std::cout<<'0';
        }
    }
    std::cout<<std::endl;
}


int main()
{
    sigset_t s1,s2,p;
    sigemptyset(&s1);
    sigemptyset(&s2);
    sigaddset(&s1, 2);
    sigprocmask(SIG_BLOCK, &s1,&s2);
    while(1)
    {
        sigemptyset(&p);
        sigpending(&p);
        printsig(p);
        sleep(1);
    }
    return 0;  
}

程序运行时,每秒钟把各信号的未决状态打印一遍,由于我们阻塞了SIGINT信号,按Ctrl-C将会 使SIGINT信号处于未决状态,按Ctrl-\仍然可以终止程序,因为SIGQUIT信号没有阻塞。

  1. 捕捉信号

7.1.基本概念

在 Linux操作系统中,信号是一种进程间通信机制,可以用来传递异步事件或通知进程特定事件的发生。当某个信号在进程中产生时,可以通过两种方式进行处理:

1.信号被捕捉:进程可以注册一个信号处理函数,当某个信号被触发时,操作系统会调用该信号处理函数来处理信号。在信号处理函数中,进程可以根据具体情况进行相应的处理。比如:打印日志、修改变量等操作。

2.信号递达:进程可以选择不捕捉某些信号,而是让操作系统默认处理这些信号。当这些信号被触发时,操作系统会执行默认的信号处理程序,比如终止进程、打印调试信息等操作。

需要注意的是,无论是信号被捕捉还是信号递达,都是异步事件,即进程不知道何时会接收到信号。因此,在编写处理信号的代码时,需要注意处理信号的时机和具体内容,以确保进程能够正确地处理信号并保持稳定运行

7.2.内核如何实现信号的捕捉

如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。由于信号处理函数的代码是在用户空间的,处理过程比较复杂,举例如下: 用户程序注册了SIGQUIT信号的处理函数sighandler。 当前正在执行main函数,这时发生中断或异常切换到内核态。 在中断处理完毕后要返回用户态的main函数之前检查到有信号SIGQUIT递达。 内核决定返回用户态后不是恢复main函数的上下文继续执行,而是执行sighandler函 数,sighandler和main函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是 两个独立的控制流程。 sighandler函数返回后自动执行特殊的系统调用sigreturn再次进入内核态。 如果没有新的信号要递达,这次再返回用户态就是恢复main函数的上下文继续执行了。

7.3.sigaction函数:自定义捕捉方法

sigaction函数可以读取和修改与指定信号相关联的处理动作。调用成功则返回0,出错则返回- 1。signo是指定信号的编号。若act指针非空,则根据act修改该信号的处理动作。若oact指针非 空,则通过oact传出该信号原来的处理动作。act和oact指向sigaction结构体。
将sa_handler赋值为常数SIG_IGN传给sigaction表示忽略信号,赋值为常数SIG_DFL表示执行系统默认动作,赋值为一个函数指针表示用自定义函数捕捉信号,或者说向内核注册了一个信号处理函数,该函数返回值为void,可以带一个int参数,通过参数可以得知当前信号的编号,这样就可以用同一个函数处理多种信号。显然,这也是一个回调函数,不是被main函数调用,而是被系统所调用。

当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生,那么它会被阻塞到当前处理结束为止。 如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用sa_mask字段说明这些需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字。 sa_flflags字段包含一些选项,本章的代码都把sa_flflags设为0,sa_sigaction是实时信号的处理函数,本章不详细解释这两个字段,有兴趣的同学可以在了解一下。

void handler(int signo)
{
    int cnt =0;
    std::cout<<"我捕捉到一个信号:"<<signo<<std::endl;
    while(1)
    {
        printf("cnt : %2d\r",cnt++);
        fflush(stdout);
        if(cnt == 20 )
        {
            break;
        }
        sleep(1);
    }
}

int main()
{
    struct sigaction act, oact;
    act.sa_handler = handler;
    act.sa_flags =0 ;
    sigemptyset(&act.sa_mask);
    //sa_mask,当我么正在处理某一个信号期间,此信号会自动被屏蔽。
    //当我们处理某种信号的时候也顺便想屏蔽其他信号,就可以添加到sa_mask中。
    //处理完成之后就会解除屏蔽。
    sigaddset(&act.sa_mask, 3);
    //处理2号信号的时候顺便屏蔽3号信号。
    sigaction(2, &act, &oact);
    while(1)
    {
        sleep(1);
    }
    return 0;
}
  1. 可重入函数

像上例这样,insert函数被不同的控制流程调用,有可能在第一次调用还没返回时就再次进入该函数,这称为重入,insert函数访问一个全局链表,有可能因为重入而造成错乱,像这样的函数称为 不可重入函数,反之,如果一个函数只访问自己的局部变量或参数,则称为可重入(Reentrant) 函数。

如果一个函数符合以下条件之一则是不可重入的:

调用了malloc或free,因为malloc也是用全局链表来管理堆的。
调用了标准I/O库函数。标准I/O库的很多实现都以不可重入的方式使用全局数据结构。

  1. volatile:保持内存可见性

直接上案例:

int flag = 0;
//volatile int flag = 0;
void handler(int signo)
{
    std::cout<<signo<<"号信号被捕捉!!"<<std::endl;
    std::cout<<"flag: 0 -> 1"<<std::endl;
    flag = 1;
}
int main()
{
    signal(2,handler);
    while(!flag);
    std::cout<<"进程正常退出!!"<<std::endl;
    return 0;
}
标准情况下,键入 CTRL-C ,2号信号被捕捉,执行自定义动作,修改 flag=1 , while 条件不满足,退出循环,进程退出。
int flag = 0;
//volatile int flag = 0;
void handler(int signo)
{
    std::cout<<signo<<"号信号被捕捉!!"<<std::endl;
    std::cout<<"flag: 0 -> 1"<<std::endl;
    flag = 1;
}
int main()
{
    signal(2,handler);
    while(!flag);
    std::cout<<"进程正常退出!!"<<std::endl;
    return 0;
}
优化情况下,键入 CTRL-C ,2号信号被捕捉,执行自定义动作,修改 flag=1 ,但是 while 条件依旧满足,进程继续运行!但是很明显flflag肯定已经被修改了,但是为何循环依旧执行?很明显, while 循环检查的flflag,并不是内存中最新的flflag,这就存在了数据二异性的问题。 while 检测的flflag其实已经因为优化,被放在了CPU寄存器当中。如何解决呢?很明显需要 volatile
//int flag = 0;
volatile int flag = 0;
void handler(int signo)
{
    std::cout<<signo<<"号信号被捕捉!!"<<std::endl;
    std::cout<<"flag: 0 -> 1"<<std::endl;
    flag = 1;
}
int main()
{
    signal(2,handler);
    while(!flag);
    std::cout<<"进程正常退出!!"<<std::endl;
    return 0;
}
volatile 作用:保持内存的可见性,告知编译器,被该关键字修饰的变量,不允许被优化,对该变量的任何操作,都必须在真实的内存中进行操作。

  1. SIGCHLD信号

进程一章讲过用wait和waitpid函数清理僵尸进程,父进程可以阻塞等待子进程结束,也可以非阻 塞地查询是否有子进程结束等待清理(也就是轮询的方式)。采用第一种方式,父进程阻塞了就不 能处理自己的工作了;采用第二种方式,父进程在处理自己的工作的同时还要记得时不时地轮询一 下,程序实现复杂。

其实,子进程在终止时会给父进程发SIGCHLD信号,该信号的默认处理动作是忽略,父进程可以自 定义SIGCHLD信号的处理函数,这样父进程只需专心处理自己的工作,不必关心子进程了,子进程 终止时会通知父进程,父进程在信号处理函数中调用wait清理子进程即可。

请编写一个程序完成以下功能:父进程fork出子进程,子进程调用exit(2)终止,父进程自定 义SIGCHLD信号的处理函数,在其中调用wait获得子进程的退出状态并打印。

void handler(int signo)
{
    std::cout << "pid: "<<getpid()<<"; "<<signo << "号信号被捕捉!!" << std::endl;
    //假设这个父进程有很多的子进程,有多个子进程同时退出,会给父进程发送多次14号信号
    //但是信号只能保存一个,handler函数只能执行一次。
    //所有这里需要循环式的等待,直至当前所有的子进程都被成功等待,才结束。
    //waitpid,使用WNOHANG参数后,为非阻塞等待,
    //等待成功返回被回收子进程的pid。
    //等待失败(所有子进程都没成功回收)返回0;
    //错误返回-1;
    pid_t id;
    while (id = waitpid(-1, nullptr,WNOHANG) > 0)
    {
        std::cout<<"等待成功!!"<<std::endl;
    }
    //此时所有退出的子进程都被成功等待。
}

int main()
{

    signal(SIGCHLD, handler);
    pid_t id = fork();
    if (id < 0)
    {
        perror("fork");
        exit(1);
    }
    else if (id == 0)
    {
        // 子进程代码
        std::cout << "我是子进程,pid:" << getpid() << ",我正在运行!!" << std::endl;
        sleep(5);
        exit(1); // 子进程五秒后退出。
    }

    // 父进程代码
    while (1)
    {
        std::cout << "我是父进程,pid:" << getpid() << ",我正在做其他工作!!" << std::endl;
        sleep(1);
    }
    //父进程一直在做自己的事情,当子进程退出的时候,给父进程发送17)SIGCHLD信号,
    //父进程发送软中断,去等待子进程。这样不需要阻塞等待和轮询等待了。

    return 0;
}

事实上,由于UNIX 的历史原因,要想不产生僵尸进程还有另外一种办法:父进程调 用sigaction将SIGCHLD的处理动作置为SIG_IGN,这样fork出来的子进程在终止时会自动清理掉,不会产生僵尸进程,也不会通知父进程。系统默认的忽略动作和用户用sigaction函数自定义的忽略 通常是没有区别的,但这是一个特例。此方法对于Linux可用,但不保证在其它UNIX系统上都可 用。请编写程序验证这样做不会产生僵尸进程。

int main()
{
    signal(SIGCHLD, SIG_IGN);
    pid_t id = fork();
    if (id < 0)
    {
        perror("fork");
        exit(1);
    }
    else if (id == 0)
    {
        // 子进程代码
        std::cout << "我是子进程,pid:" << getpid() << ",我正在运行!!" << std::endl;
        sleep(5);
        exit(1); // 子进程五秒后退出。
    }

    // 父进程代码
    while (1)
    {
        std::cout << "我是父进程,pid:" << getpid() << ",我正在做其他工作!!" << std::endl;
        sleep(1);
    }
    return 0;
}

直接就自动退出了,不会出现僵尸进程。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小峰同学&&&

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值