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

文章目录

  • 1、了解信号
  • 2、了解信号处理
  • 3、信号产生
    • 1、键盘按键产生
    • 2、系统接口产生
    • 3、软件条件产生
    • 4、硬件异常
  • 4、Core和Term的区别
  • 5、信号保存
    • 1、在系统中的表现形式
    • 2、信号集操作函数
      • 1、sigprocmask
      • 2、sigpending
  • 6、重谈地址空间
  • 7、信号处理与捕捉
    • sigaction


1、了解信号

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

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

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

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

在这里插入图片描述

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

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

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

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

2、了解信号处理

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

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

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

在这里插入图片描述

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

在这里插入图片描述

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

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

3、信号产生

1、键盘按键产生

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

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

2、系统接口产生

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

模拟实现: https://gitee.com/kongqizyd/linux-beginner/blob/master/mysignal/mykill.cc(48行以下)

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.

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可以读取或更改进程的信号屏蔽字(阻塞信号集)(bloc-、k)。

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

how是怎么改,*set有三个参数

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

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

哪个进程调用这个接口,就设置谁的信号集。

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

在这里插入图片描述

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);
        int n = sigpending(&pending);
        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号进程就是系统在调用自己;系统能够无时无刻处理我们的操作,也是因为有一些硬件在帮忙,比如时钟硬件,主板上有一个纽扣电池,它一直在给时钟硬件充电,当电脑关机时,电脑还有硬件在运行,这样当你下一次打开电脑的时候,会发现时间是对的,而不是上一次关机时的时间。这个时钟硬件每隔很短的时间向系统发送中断,系统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同样作用的代码

当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生,那么它会被阻塞到当前处理结束为止。 如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用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);
        int n = sigpending(&pending);
        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;
}

结束。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/18163.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

MySQL索引、事务与存储引擎

数据库索引 是一个排序的列表&#xff0c;存储着索引值和这个值对应的物理地址&#xff0c;在这个列表中存储着索引的值和包含这个值的数据所在行的物理地址&#xff08;类似于C语言的链表通过指针指向数据记录的内存地址&#xff09;无需对整个表进行扫描&#xff0c;而是先通…

IS210AEBIH3BEC隔离器用于变压器等高压设备

IS210AEBIH3BEC隔离器用于变压器等高压设备 隔离器可以根据在电力系统中的位置进行分类 母线侧隔离器——隔离器直接连接到主母线线路侧隔离器 - 隔离器将放置在任何馈线的线路侧Transfer bus side isolator – isolator will be directly connected with the transfer bus S…

【LeetCode】1143. 最长公共子序列

1.问题 给定两个字符串 text1 和 text2&#xff0c;返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 &#xff0c;返回 0 。 一个字符串的 子序列 是指这样一个新的字符串&#xff1a;它是由原字符串在不改变字符的相对顺序的情况下删除某些字符&#xff0…

怎么让chatGTP写论文-chatGTP写论文工具

chatGTP如何写论文 ChatGPT是一个使用深度学习技术训练的自然语言处理模型&#xff0c;可以用于生成自然语言文本&#xff0c;例如对话、摘要、文章等。作为一个人工智能技术&#xff0c;ChatGPT可以帮助你处理一些文字内容&#xff0c;但并不能代替人类的创造性思考和判断。以…

手机录屏怎么操作?有哪些好用的方法

在现代科技的时代&#xff0c;手机录屏已经成为了常见的操作。这项技术允许我们在手机上录制视频并分享给他人。但是&#xff0c;很多人可能并不知道如何进行手机录屏。下面我们将介绍手机录屏的操作方法和一些值得推荐的工具。 手机录屏操作方法 对于iOS用户&#xff0c;可以…

Ribbon负载均衡

目录 1.Ribbon负载均衡 1.1.负载均衡原理 1.2.源码跟踪 1&#xff09;LoadBalancerIntercepor 2&#xff09;LoadBalancerClient 3&#xff09;负载均衡策略IRule 4&#xff09;总结 1.3.负载均衡策略 1.3.1.负载均衡策略 1.3.2.自定义负载均衡策略 1.4.饥饿加载 1.R…

InnoDB 与MyISAM 的区别

MyISAM和InnoDB都是Mysql里面的两个存储引擎。 在Mysql里面&#xff0c;存储引擎是可以自己扩展的&#xff0c;它的本质其实是定义数据存储的方式以及数据读取的实现逻辑。 不同存储引擎本身的特性&#xff0c;使得我们可以针对性的选择合适的引擎来实现不同的业务场景。从而获…

Java企业级信息系统开发01—采用spring配置文件管理bean

文章目录 一、Web开发技术二、spring框架&#xff08;一&#xff09;spring官网&#xff08;二&#xff09;spring框架优点&#xff08;三&#xff09;Spring框架核心概念1、IoC&#xff08;Inversion of Control&#xff09;和容器2、AOP&#xff08;Aspect-Oriented Programm…

间谍软件开发商利用漏洞利用链攻击移动生态系统

导语&#xff1a;间谍软件开发商结合使用了零日漏洞和已知漏洞。谷歌TAG的研究人员督促厂商和用户应加快给移动设备打补丁的步伐。 间谍软件开发商利用漏洞利用链攻击移动生态系统去年&#xff0c;几家商业间谍软件开发商开发并利用了针对 iOS 和安卓用户的零日漏洞。然而&…

【Python】什么是爬虫,爬虫实例

有s表示加密的访问方式 一、初识爬虫 什么是爬虫 网络爬虫&#xff0c;是一种按照一定规则&#xff0c;自动抓取互联网信息的程序或者脚本。由于互联网数据的多样性和资源的有限性&#xff0c;根据用户需求定向抓取相关网页并分析已成为如今主流的爬取策略爬虫可以做什么 你可以…

stream的collectors

起因的话&#xff0c;新进公司&#xff0c;看见了一段有意思的代码。 public final class MyCollectors {private MyCollectors() {}static final Set<Collector.Characteristics> CH_ID Collections.unmodifiableSet(EnumSet.of(Collector.Characteristics.IDENTITY_F…

从点赞到数字货币:揭秘Diem币与Facebook的联系

大家都知道Facebook是一个全球知名的社交媒体平台&#xff0c;但你是否听说过与Facebook有关的数字货币Diem币呢&#xff1f;或许你会想&#xff0c;从点赞到数字货币&#xff0c;这是怎么回事&#xff1f;别着急&#xff0c;让我们一起揭秘Diem币与Facebook的联系。 首先&…

rk平台调试音频(从驱动到apk)

需要实现的功能&#xff1a; 输入&#xff1a;hdmiin、uvc、mic可以实时切换 输出&#xff1a;耳机和HDMI OUT同时输出声音 这里注意&#xff1a;mic是存在hedset情况&#xff0c;4节耳机&#xff0c;即可输出又可输出同时进行 开发情况&#xff1a; 一、先熟悉大致的Andro…

ArcMap最短路径分析和网络数据集的构建

打断相交点 1.单击【编辑器】工具条上的编辑工具。 2.选择要在交叉点处进行分割的线要素。 3.单击【高级编辑】工具条上的打断相交线工具。 4.默认或可输入拓扑容差。 5.单击确定。 结果:所选线在相交处分割为多个新要素。“打断”操作还会移除叠置的线段-例如&#xff0…

怎么控制别人的电脑屏幕?

为什么需要控制别人的屏幕&#xff1f; 我们不可避免地会遇到一些情况&#xff0c;比如我们需要为我们的朋友、同事或家人提供有关 IT 相关问题的帮助&#xff0c;如果他们不知道它该怎么处理这些问题该怎么办呢&#xff1f; 这时&#xff0c;我们可能需要用我们的电脑…

测试20K要什么水平?25岁测试工程师成功斩下offer(附面试题)

年少不懂面试经&#xff0c;读懂已是测试人。 大家好&#xff0c;我是一名历经沧桑&#xff0c;看透互联网行业百态的测试从业者&#xff0c;经过数年的勤学苦练&#xff0c;精钻深研究&#xff0c;终于从初出茅庐的职场新手成长为现在的测试老鸟&#xff0c;早已看透了面试官…

三维数据学习笔记:ply数据内容介绍

目录 前言1. 三维数据的组成1.1 点云数据1.2 网格数据 2. ply数据内容2.1 属性2.1.1 文本描述属性2.1.2 数据描述属性2.1.2.1 顶点(vertex)2.1.2.2 面(face)2.1.2.3 相机(camera) 2.2 数据2.2.1 顶点(vertex)2.2.2 面(face)2.2.3 相机(camera) 3. 示例3.1 示例13.2 示例2 前言 …

Java基础(十七)File类与IO流

1. java.io.File类的使用 1.1 概述 File类及本章下的各种流&#xff0c;都定义在java.io包下。一个File对象代表硬盘或网络中可能存在的一个文件或者文件目录&#xff08;俗称文件夹&#xff09;&#xff0c;与平台无关。&#xff08;体会万事万物皆对象&#xff09;File 能新…

JDK17新特性之--JDK9到JDK17 String 新增的新方法

JDK9之后对String底层存储数据结构进行了重大的修改1&#xff0c;同步也增加了许多新的方法&#xff0c;主要有Text Blocks、chars()、codePoints()、describeConstable()、formatted()、indent()、isBlank()、isEmpty()、lines()、repeat()、strip()、stripLeading()、stripIn…

DolphinScheduler 3.1.4详细教程

文章目录 第一章 DolphinScheduler介绍1.1 关于DolphinScheduler1.2 特性1.3 名词解释1.3.1 名词解释1.3.2 模块介绍 第二章 DolphinScheduler系统架构2.1 系统架构图2.2 架构说明该服务包含&#xff1a; 2.3 启动流程活动图2.4 架构设计思想2.4.1 去中心化vs中心化2.4.1.1 中心…