Linux学习记录——이십이 进程信号(1)


1、了解信号

信号没有产生前,进程在没有收到这个信号前,进程也知道怎么处理。

进程能够认识并处理一个信号,说明进程是能识别这个信号的。

进程是怎么做到这些的?是靠程序员来设计识别信号的能力。

信号可能随时产生,所以在信号产生前,如果进程在做优先级更高的事情,就不会立马处理这个信号,会在之后合适的时间再去处理。信号产生和信号处理之间有个时间窗口,在这段时间内,信号会被保存起来,所以进程需要有记录信号的能力。

在这里插入图片描述

这是linux中所有的信号,1 - 31是普通信号,34 - 64是实时信号,数字就是信号,后面的是信号名称,其实就是宏。但是会发现没有0,32,33号信号。

信号的产生对于进程来说是异步的,意思是进程不太关心信号的产生,信号产生就产生,进程继续做自己的东西,识别信号也不是什么重要的事情。

进程记录信号是先描述,再组织。分时操作系统像Linux,Windows,追求公平调度;实时操作系统则要求高响应,新的任务会优先给到反馈。普通信号的保存只保存有无产生,而实时信号则是立马处理,可以发出很多,并且都能保存下来。

0表示无,1表示有,用位图结构来保存信号,1 - 31,34 - 64,正好一个4字节二进制数,32位。在进程的pcb内部有一个位图结构去保存信号,发送信号时,其实就是写入信号,会修改进程的信号位图中的对应的比特位。比特位的位置就是信号的编号,比特位的内容表示是否收到信号。改变位图结构的只能是系统,无论信号如何产生,最后都由系统来发送信号,也就是写入信号,改变位图结构。

2、了解信号处理

信号的处理有默认处理,忽略信号,用户自定义等方式。

一个进程执行时,此时无法执行其他命令,因为这时候这个进程是前台进程,系统只会去运行它,一般bash调起的进程会是前台进程,如果./程序名 &,这个进程就变成后台程序了,执行其它命令也可以。

前台进程执行时,我们可以Ctrl + C停掉这个进程,本质也是发送信号,但如何保证一按这个键杀掉的就是这个进程呢?

在这里插入图片描述

有一个函数signal,返回值是函数指针类型,参数为int类型;signal的参数里,第一个参数是信号编号,第二个则是要处理的方式。快捷键就调用了这个函数。

在这里插入图片描述

像这样,程序运行时,不断按Ctrl + c就会打印handler里的内容,系统会自动把2分配给signo。想要终止进程,可以用Ctrl + \来发送3号信号,当然也可以把3号信号变成自定义动作,Ctrl + C是2号信号。

无论怎样,kill -9不会被替代,它是管理员信号,可以用它来直接杀掉进程。

3、信号产生

1、键盘按键产生

CPU有很多针脚来接到主板上,键盘有一定的硬件连接到CPU,比如中断控制器。键盘有一个键被按下时就立马通过中断控制器等向CPU发送中断号,CPU内部有相应的寄存器,如果有传过来中断号,寄存器里就存入这个号。这是硬件中断。

系统维护了一张中断向量表,里面有很多指针,中断号就是对应的下标,执行对应的方法,从键盘中读取对应的数据,比如a b,比如shift,比如Ctlr + C等等,这些操作是系统做的,它会把这些读到的数据转换成信号,然后找前台进程,写入2号进程。实际上读取中断号等等靠的是驱动。

2、系统接口产生

kill命令,两个参数,一个是进程pid,一个几号信号。

模拟实现kill

raise命令,只有一个参数sig,谁调用这个接口,就给谁发信号。

在这里插入图片描述

先捕捉信号,然后再用raise发送信号,就会看到它每隔1s自动发一次信号,signal除了传信号编号,也可以传信号名。

abort函数,void abort(void),自己给自己发6信号,假如6信号被改变处理方式了,执行一遍后abort必须退出,它会用别的方法去结束进程。

在这里插入图片描述

3、软件条件产生

指的是因为在软件层面上不符合某些要求,导致这个进程做的工作无意义,那么会收到信号。

alarm函数,调用alarm函数可以设定一个闹钟,也就是告诉内核在second秒之后给当前进程发送SIGALRM信号,该信号的默认处理动作是终止当前进程,函数返回值是0或者是以前设定的闹钟时间还余下的秒数。

在这里插入图片描述

会在一秒之后结束进程,但多运行几次后就会发现每次最后的count数都不一样。这里是在计算1s内计算机能将一个整数累计到多少,但这很不正确,因为有网络,IO等影响因素,减少IO影响可以这样

在这里插入图片描述

count为全局变量。对比两个结果就会发现IO的效率很低下。闹钟是一次性的。如果想持续闹钟,可以在myhandler打印语句后写一个alarm(1)。

对于另一种返回值:

在这里插入图片描述

开另一个窗口,在等待过程中手动发一遍信号,就会看到返回上一个闹钟剩余的值。想取消闹钟就alarm(0)。

系统对于闹钟也是先描述再组织,因为有很多进程都会开闹钟。系统里有一个闹钟结构体,里面有各种属性,比如timestamp,就是当前时间+未来设置的时间

4、硬件异常

硬件异常被硬件以某种方式检测到并通知内核,然后内核向当前进程发送适当的信号。例如当前进程执行了除以0的指令,CPU的运算单元会产生异常,内核将这个异常解释为SIGFPE信号发送给进程。再比如当前进程访问了非法内存地址,MMU会产生异常,内核将这个异常解释为SIGSEGV信号发送给进程。

以一个除0操作来举例子。代码中创建变量n,然后让它 /= 0。在CPU的寄存器中,会存入n,0,然后进行操作 /=。在进行计算时,有一个状态寄存器去存储本次计算是否有溢出等问题,当然也有寄存器来存储结果。如果有溢出,状态寄存器的计数标志位就由0变为1,随后系统会检查到这个异常位置,CPU也知道,系统会发送信号,浮点数异常,8号信号,进程接受信号后就会执行默认信号Core,也就是终止进程。

在运行进程时,CPU里还会有寄存器去存储pcb地址,当出现异常是,系统就会在这里面找对应的进程,具体的之后再写。

除0的本质就是触发硬件异常。

为了验证这个信号,我们可以使用signal接口来自定义处理动作,但如果这样运行,程序会一直运行,停不下来。这是因为进程出现异常后,收到8号信号后,就做我们自定义的动作,它并没有退出,标志位还是1。所以自定义处理动作里也得有退出动作exit(1)。

看这个代码

int* p = nullptr;
*p = 100;

典型的野指针问题。在内部,p指向的地址是在地址空间的,它会通过页表映射到物理内存中。页表进行查表的动作是通过MMU硬件完成,内存管理单元,先把映射关系放到MMU里,再去映射到物理内存中。

现在*p = 100。那么地址是否有映射关系,我们对映射关系是否有写的权限?实际上是没有的,只能读,所以直接错了。当执行这行代码时,会先找到这块空间,也就是进行虚拟到物理地址的转换,如果没有映射关系,那么MMU硬件报错;如果有映射关系,但是没有写权限,那么就无法把新值放进内存中,所以MMU报错。系统会收到这个报错,然后向进程写入信号。

像野指针,越界等这样的段错误是11号信号,SEGV就是代表段错误。所有信号可以通过kill -l查看。处理动作是Core。

MMU是集成在CPU里的。

4、Core和Term的区别

刚才提到了Core,除此之外,信号的默认处理动作该有Term。

一个进程的status中,低8位表示退出状态,另有7位表示收到的终止信号,而中间那一位就是core dump标志,0或者1。

Linux系统级别提供了一种能力,一个进程在异常的时候,系统可以将核心代码进行转储,将内存中的相关数据全部dump(转储)到磁盘中,一般会在当前进程的运行目录下,形成core.pid这样的二进制文件。

不过云服务默认关闭了这个功能,ulimit -a可以查看当前系统特定资源的上限,不过不一定准。

在这里插入图片描述
会发现core file size为0。我们可以这样设置ulimit -c 数字。

在这里插入图片描述

测试一下

在这里插入图片描述

运行后,用默认处理动作为Core的信号来终止进程,会发现目录出现了一个新文件,core.pid文件,并且在结束进程时报出的错误后面有(core dumped)。

这里区分出了Term和Core,Term只是终止进程,而Core会进程核心转储。

核心转储有什么用?方便异常后进行调试,不过默认程序生成时是release,在生成时,g++语句最后加个-g就行。进入gdb后,core file core.pid命令就可以直接看到都有什么错。

云服务器核心转储为什么要默认关闭?云服务器是生产环境,还有开发环境,测试环境。测试会在开发环境测试,通过后放到生产环境形成成品服务于人。转储文件是比较大的,在服务器上一个程序挂掉,会有专门的检测程序去重启它,如果这个程序一直挂掉,就要一直重启,就会有大量的core dumped文件产生,对于磁盘的占用就纯属浪费空间,所以在云服务器上核心转储要关掉。

回到一开始的core dumped标志位,为0就是没开,为1就是打开了,信号处理动作如果是Term那就是0,如果是Core,那么设置为1和0对于整体的status各有什么影响?我们可以用父子进程来验证,子进程造一个野指针问题,父进程获取子进程信息。如果是设置为1,那么标志位上确实是1,如果没有,就是0。也就是说core dumped标志,设置为1,就是Core,设置为0就是Term。

5、信号保存

实际执行信号的处理动作称为信号递达(Delivery)
信号从产生到递达之间的状态,称为信号未决(Pending)。
进程可以选择阻塞 (Block )某个信号。
被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作。
注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。

1、在系统中的表现形式

在进程的pcb中,系统会维护三张表。

pending表

位图结构。比特位的位置,表示哪一个信号,比特位的内容代表是否收到该信号。系统发信号就是在这个表中修改位图结构。pending就是一个32位数字,pending |= (1 << (signo - 1))。

block表

位图结构。比特位的位置,表示哪一个信号;比特位的内容代表对应的信号是否被阻塞,1表示被屏蔽,0表示可处理,不屏蔽。

handler表

函数指针数组,指针类型是void(*sighandler_t)(int),该数组下标表示信号编号,下标对应的内容表示信号的递达动作。

进程靠这三张表识别信号。

在这里插入图片描述

报错,终止,忽略动作。

在这里插入图片描述

每个信号都有两个标志位分别表示阻塞(block)和未决(pending),还有一个函数指针表示处理动作。信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。在上图的例子中,SIGHUP信号未阻塞也未产生过,当它递达时执行默认处理动作。
SIGINT信号产生过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略,但在没有解除阻塞之前不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞。
SIGQUIT信号未产生过,一旦产生SIGQUIT信号将被阻塞,它的处理动作是用户自定义函数sighandler。
如果在进程解除对某信号的阻塞之前这种信号产生过多次,将如何处理?POSIX.1允许系统递送该信号一次或多次。Linux是这样实现的:常规信号在递达之前产生多次只计一次,而实时信号在递达之前产生多次可以依次放在一个队列里。

sigset_t

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

2、信号集操作函数

#include <signal.h>
int sigemptyset(sigset_t *set); 清空
int sigfillset(sigset_t *set); 全部设为1
int sigaddset (sigset_t *set, int signo); 信号添加
int sigdelset(sigset_t *set, int signo); 删除信号
int sigismember(const sigset_t *set, int signo); 判断信号是否存在

1、sigprocmask

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

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

how是怎么改,有三个参数

SIG_BLOCK 添加信号,相当于与mask = mask | set。
SIG_UNBLOCK 删除某个信号,相当于mask = mask & ~set。
SIG_SETMASK 设置当前信号屏蔽字为set所指向的值,相当于 mask = set。
信号屏蔽字是一个管理block的sigset_t对象,管理pending的是pending信号集。

第三个参数*oset是输出型参数,会返回改之前的信号集。

哪个进程调用这个接口,就设置谁的信号集。这个接口用于临时屏蔽某个信号,防止这个信号影响进程,比如下面的代码就是屏蔽了2号信号,最后的恢复也可以把&set换成NULL。

写一个代码来理解这个函数

#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
using namespace std;

void showBlock(&oset)
{
    int signo = 1;
    for(; signo <= 31; signo++)
    {
        if(sigismember(oset, signo)) cout << "1";
        else cout << "0";
    }
    cout << endl;
}

int main()
{
    //只是在用户层面上进行设置
    sigset_t set, oset;
    sigemptyset(&set);
    sigemptyset(&oset);
    sigaddset(&set, 2);
    //设置进入进程,谁调用,设置谁
    sigprocmask(SIG_SETMASK, &set, &oset);//2号信号应当没反应,看到的老的信号屏蔽字是全0
    int cnt = 0;
    while(true)
    {
        showBlock(&oset);
        sleep(1);
        cnt++;
        if(cnt == 10)
        {
            cout << "recover block" << endl;
            sigprocmask(SIG_SETMASK, &oset, &set);//恢复2号信号
            showBlock(&set);
        }
    }
    return 0;
}

2、sigpending

查看pending表。

根据上面的接口,我们可以写个demo样例

#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <cassert>
using namespace std;

static void handler(int signo)
{
    cout << "对特定信号: " << signo << "执行捕捉动作" << endl;

}

static void PrintPending(const sigset_t &pending)
{
    for(int signo = 1; signo <= 31; signo++)
    {
        if(sigismember(&pending, signo)) cout << "1";
        else cout << "0";
    }
    cout << "\n";
}

int main()
{
    //1、2信号,进程的默认处理动作是终止进程
    //2、signal可以进行对指定的信号设定自定义处理动作
    //3、signal(2, handler)调用完这个函数的时候,handler方法被调用了吗?没有,只是更改了2信号的处理方式,并没有调用handler,需要在handler内部调用一下handler函数才会执行handler函数
    /*signal(2, handler);
    while(true)
    {
        std::cout << "我是一个进程,我正在运行, pid: " << getpid() << endl;
        sleep(1);
    }*/
    //1、屏蔽2号信号
    sigset_t set, oset;
    //1、1 初始化
    sigemptyset(&set);
    sigemptyset(&oset);
    //1、2 将2号信号添加到set中
    sigaddset(&set, SIGINT);
    //1、3 将新的信号屏蔽字设置进程
    sigprocmask(SIG_BLOCK, &set, &oset);
    //2、 while获取进程的pending信号集合,并按照01打印
    while(true)
    {
        //2、1 设置2号信号的自定义捕捉
        signal(2, handler);
        sigset_t pending;
        sigemptyset(&pending);
        //pending表是挂起的信号集,所以它会打印已被挂起的信号
        int n = sigpending(&pending);//如果返回-1就是出异常了
        assert(n == 0);
        (void)n;
        //2、2 打印pending信号集
        PrintPenging(pending);
        //2、3 休眠一下
        sleep(1);
        //2、4 10s之后,恢复对所有信号的block动作
        if(cnt++ == 10)
        {
            cout << "解除对2号信号的屏蔽" << endl;

        }
    }
    return 0;
}

6、重谈地址空间

之前写到过,系统会在适当的时候去处理信号。

信号可以立即被处理,如果一个信号之前被block,当它解除block的时候,对应的信号会被立即递达。这是一种特殊情况,大多数的时候,信号不是立即处理的,信号的产生是异步的,当前进程可能正在做更重要的事情。

那么什么是适当的时候呢?当进程从内核态切换回用户态的时候,进程会在系统的指导下,进行信号的检测与处理。

什么是用户态和内核态?执行用户写的代码的时候,进程所处的状态是用户态;执行系统的代码的时候,进程所处的状态是内核态。什么是系统代码?比如进程时间片到了,系统就会切换进程;系统调用接口等等。我们写的代码运行的时候,系统的代码也在运行,系统代码怎么运行的?

在地址空间中,内核空间有1G,用户空间有3G,用户空间就有栈,堆,共享区等等,内核空间在高地址处,用户空间在低地址处。之前所写的页表,链接关系,物理内存等等,都是在用户角度写的,页表也是用户级页表,那操作系统在哪里?开机的时候,系统得把自己的东西加载到物理内存中,从哪开始加载的?其实系统也在地址空间,而系统也有自己的内核级页表,从内核空间的那1G中找到系统的代码和数据,然后通过内核级页表,加载到物理内存中。对于所有的进程,0-3GB是不同的,是用户空间,但3-4GB,也就是那个内核空间,是一样的,无论进程如何切换,都不影响这个1G空间,所有进程都可以通过统一的窗口看到同一个系统。所以系统调用的本质,就是在自己的地址空间中进行函数跳转并返回即可。

但这里就有一个问题,用户可以访问系统调用接口,那系统的代码和数据呢?这些东西不能让用户访问到,所以就定义了用户态和内核态,只要访问用户空间,就是用户态,如果访问到内核空间,系统会检查身份,如果不是内核态,那么一些非法的操作就不被执行。在CPU中,有一个CR3寄存器,如果对应的比特位是3,就代表正在运行的进程级别是用户态,是0就表示是内核态。谁来更改这个比特位?肯定不能让用户修改,但是还得给用户提供调用接口,怎样控制用户根本接触不到系统的数据?操作系统提供的所有的系统调用接口中,内部在正式执行调用逻辑的时候,会去修改执行级别。

清楚这些后,看这个问题,进程是如何被调度的?先看下面所写。

在一些老的计算机中,调起系统后,它会有一个1号进程,这是在没有任何一个进程时系统自己打开的进程。

当我们在计算机上操作的时候,即使什么都不做,系统也会在管理一些数据,管理整个计算机。如果我们打开某个软件,系统就会打开,加载,无论我们做什么,系统都有反应,且及时。操作系统是怎么做到的?实际上,系统的本质是一个软件,是一个死循环的软件,刚才写的1号进程就是系统在调用自己;系统能够无时无刻处理我们的操作,也是因为有一些硬件在帮忙,比如时钟硬件,主板上有一个纽扣电池,它一直在给时钟硬件充电,当电脑关机时,电脑还有硬件在运行,这样当你下一次打开电脑的时候,会发现时间是对的,而不是上一次关机时的时间。这个时钟硬件每隔很短的时间向系统发送中断,系统就会执行对应的中断处理方法。

回到上面的问题,进程被调度的本质就是时间片到了,系统将进程对应的上下文等进行保存并切换,选择合适的进程,做这些事情的是schedule函数。系统的中断处理方法是检测当前进程的时间片,进程pcb中有调度时间,系统拿到这一次的减去上一次的,如果超过规定的时间,就让进程调用schedule函数,然后把其他进程的地址空间换过来,运行下一个进程就好。

所以系统调用的本质也就清楚了。当进入内核空间执行系统接口时,此时的用户就不是用户了,而是系统,系统借助这个执行流去执行自己的一些代码,返回来时改一下执行级别,变回了用户级别。

总结

什么时候从用户态到内核态?要检查时间片,开始调度进程;调用系统调用接口。

什么时候从内核态到用户态?调用接口结束后,此时信号也会被处理。

7、信号处理与捕捉

内核态转回到用户态时不是直接回去,而是先去检查进程pcb中的信号相关部分,然后执行处理动作。这之中自定义处理动作比较特殊,当检测到自定义时,就会转到相应的函数去运行代码,之后回到进程pcb中,然后再回到用户态,再去执行下一个代码。

这里还有一些细节。当跳转到handler函数,应该以什么状态去执行?用户态,因为系统不相信用户,它不知道handler函数里做了什么,所以要转换成用户态。当运行完函数代码后,不能直接跳到下面要执行的代码,因为在用户态转为内核态时中断的位置只有系统,所以还要通过系统调用,再次嵌入内核,转为内核态,这个调用是系统自己做的,这个接口就是sys_sigreturn()。当系统发现信号处理完,不需要调用系统接口了,就转为用户态再去运行下面的代码、

进程不会一直在内核态,即使一直死循环,系统也会把你给拉出来。

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

sigaction

对于pending和block表都有对应的函数,对于handler表除了之前的signal函数,还有sigaction函数。

#include <signal.h>
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);

在这里插入图片描述

    struct sigaction act, oldact;
    memset(&act, 0, sizeof(act));
    memset(&oldact, 0, sizeof(oldact));
    act.sa_handler = handler;
    act.sa_flags = 0;
    sigaction(2, &act, &oldact);
    while(true)
    {
        sleep(1);
    }
//与之前的signal同样作用的代码,但是这个接口比signal更好

当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生,那么它会被阻塞到当前处理结束为止。 如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用sa_mask字段说明这些需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字。

为了看一下mask字段的作用,这样改代码

sigemptyset(&act.sa_mask);
sigaction(2, &act, &oldact);

在action之前写上一行代码,然后handler内部

static void handler(int signo)
{
    cout << "对特定信号: " << signo << "执行捕捉动作" << endl;
    int cnt = 10;
    while(cnt)
    {
        cnt--;
        sigset_t pending;
        sigemptyset(&pending);
        sigpending(&pending);
        PrintPending(pending);
        sleep(1);
    }
}

通过这个就能看出来,在调用handler之前,pending表中对应的信号就由1变为0了。

现在要求屏蔽2号信号时顺便把其他信号给屏蔽了。

    sigemptyset(&act.sa_mask);
    sigaddset(&act.sa_mask, 3);
    sigaddset(&act.sa_mask, 4);
    sigaction(2, &act, &oldact);

cnt改长一些,在while(true)可以打印pid。

static void handler(int signo)
{
    cout << "对特定信号: " << signo << "执行捕捉动作" << endl;
    int cnt = 30;
    while(cnt)
    {
        cnt--;
        sigset_t pending;
        sigemptyset(&pending);
        sigpending(&pending);
        PrintPending(pending);
        sleep(1);
    }
}

static void PrintPending(const sigset_t &pending)
{
    for(int signo = 1; signo <= 31; signo++)
    {
        if(sigismember(&pending, signo)) cout << "1";
        else cout << "0";
    }
    cout << "\n";
}

int main()
{
    struct sigaction act, oldact;
    memset(&act, 0, sizeof(act));
    memset(&oldact, 0, sizeof(oldact));
    act.sa_handler = handler;
    act.sa_flags = 0;
    sigemptyset(&act.sa_mask);
    sigaddset(&act.sa_mask, 3);
    sigaddset(&act.sa_mask, 4);
    sigaction(2, &act, &oldact);
    while(true)
    {
        cout << getpid() << endl;
        sleep(1);
    }
    //1、2信号,进程的默认处理动作是终止进程
    //2、signal可以进行对指定的信号设定自定义处理动作
    //3、signal(2, handler)调用完这个函数的时候,handler方法被调用了吗?没有,只是更改了2信号的处理方式,并没有调用handler,需要在handler内部调用一下handler函数才会执行handler函数
    /*signal(2, handler);
    while(true)
    {
        std::cout << "我是一个进程,我正在运行, pid: " << getpid() << endl;
        sleep(1);
    }*/
    //1、屏蔽2号信号
    /*sigset_t set, oset;
    //1、1 初始化
    sigemptyset(&set);
    sigemptyset(&oset);
    //1、2 将2号信号添加到set中
    sigaddset(&set, SIGINT);
    //1、3 将新的信号屏蔽字设置进程
    sigprocmask(SIG_BLOCK, &set, &oset);
    //2、 while获取进程的pending信号集合,并按照01打印
    while(true)
    {
        //2、1 设置2号信号的自定义捕捉
        signal(2, handler);
        sigset_t pending;
        sigemptyset(&pending);
        //pending表是挂起的信号集,所以它会打印已被挂起的信号
        int n = sigpending(&pending);//如果返回-1就是出异常了
        assert(n == 0);
        (void)n;
        //2、2 打印pending信号集
        PrintPenging(pending);
        //2、3 休眠一下
        sleep(1);
        //2、4 10s之后,恢复对所有信号的block动作
        if(cnt++ == 10)
        {
            cout << "解除对2号信号的屏蔽" << endl;  
        }
    }*/
    return 0;
}

结束。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值