进程的信号

目录

1、信号被接收到后就立即执行吗? 

2、如何理解发送信号和信号被进程保存呢?

3、信号如何产生呢?

3.1、方法一

3.2、方法二

3.3、方法三

3.4、方法四

4、信号产生后,对信号的处理方式有三种

4.1、函数sighandler_t signal(int signum,sighandler_t handler)

4.1.1、核心转储

4.2、函数int sigaction (int signo, const struct sigaction *act, struct sigaction *oact)

4.2.1、与函数同名的sigaction结构体(内容包括对信号未处理完,又收到该信号的处理方式的说明)

5、信号的其他相关概念

6、阻塞信号

6.1、阻塞信号在内核中的表示

6.2、sigset_t类型

6.2.1、int sigemptyset(sigset_t* set)

6.2.2、int sigfillset(sigset_t* set)

6.2.3、int sigaddset(sigset_t* set, int signo)

6.2.4、int sigdelset(sigset_t* set, int signo)

6.2.5、int sigismember(const sigset_t* set, int signo)

6.2.6、int sigprocmask(int how, const sigset_t* set, sigset_t* oset)

6.2.7、int sigpending(sigset_t *set)

7、有没有一种办法,让一个进程无法被杀死呢?

8、我们说信号产生后,信号可能无法被立即处理,会在合适的时机处理,那什么时候是合适的时机呢?

8.1、处理信号的流程图

8.2、用户态和内核态

8.3、OS如何识别CPU是处于哪种状态呢?

8.4、CPU那如何进入内核态呢?

8.5、为什么执行完系统调用后,CPU一定要从内核态返回用户态呢?

8.6、CPU执行不写系统接口的进程的代码时会进入内核态吗?

9、volatile关键字

10、SIGCHLD信号

10.1、如果我们不想让父进程等待子进程,还想让子进程退出变成僵尸进程后能够被自动释放,该怎么做呢?


1、信号被接收到后就立即执行吗? 

信号的产生是随机的,进程可能正在忙自己的事情,所以信号的后序处理不一定是实时处理的。那么不是立即处理信号,该进程就一定得保存该信号,方便后序处理。

2、如何理解发送信号和信号被进程保存呢?

如果发出的信号很多,那一定需要管理它们,如何管理呢?同样的借助PCB,即task_struct完成管理。task_struct中有一个pending信号位图,比如用一个unsigned int表示pending位图,它有32个位,刚好可以表示32个信号,第一个位表示信号1,第二个位表示信号2并依次类推。如果位图上某个位的数字为0,表示OS没有发该信号,如果为1则表示OS发送了该信号。所以发送信号的本质就是OS向目标进程写信号,OS直接修改目标进程的task_struct中的指定位图结构,将信号对应的比特位从0修改成1,又因为task_struct是内核数据结构,所以只有操作系统可以访问它,所以不管以什么封装后的手段发送信号,最终本质都是由操作系统亲自进行的。

3、信号如何产生呢?

首先下结论:下面说的所有信号产生的方式,本质都是被OS识别,解释,并发送的,即所有信号的产生,都由OS来执行,因为OS是进程的管理者。

3.1、方法一

利用键盘上的组合键产生信号,但本质还是系统调用。

3.2、方法二

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

问题:如何更深入的理解除0产生信号呢?

答案:进行计算的硬件是CPU而CPU中是有很多寄存器的,其中一种寄存器叫做状态寄存器,该寄存器不保存任何用于计算的数值,只负责保存计算后的状态,比如本次计算是否是对的,有没有进位,有没有溢出。状态寄存器中有许多状态标记位,可以把该寄存器当成一个位图,每次计算完后,OS都会检测该寄存器,如果溢出标记位为1,那OS立刻可以识别到计算产生了溢出问题。那么OS如何得知是哪个进程产生了溢出问题并向该进程发送信号呢?此时CPU中的所有寄存器保存的都是产生溢出问题的进程的上下文数据,而其中有一个寄存器专用于保存当前正在运行的进程的PCB,而PCB中又保存了进程的PID,所以只需要提取该进程的PID,向它发送信号即可,之后进程会在合适的时机处理信号。所以除0错误根本不是软件问题,而是产生了硬件的异常,之后被OS识别到并向进程发送信号,最后进程退出。

问题:那么一旦出现硬件异常,进程一定会退出吗?

答案:不一定,一般默认是进程退出,但我们可以用signal函数改变异常的处理方式。所以如果程序中没有异常处理,发生一个异常一定会导致程序终止运行,但假如程序中有异常处理,发生一个异常并被捕获,则异常处理后,程序会继续运行。

代码如下

c3772742987d45328114c1e8a92dfca9.png

运行结果如下

94e60252105b469aaf184f338a2dd156.png

问题:为什么会死循环呢?或者说为什么进程不断在受到OS发出的异常信号,并且调用handler函数呢?

答案:首先因为用signal改变了接收异常信号后的处理方式,所以进程不会在产生异常后直接退出,而是调用了handler函数。那么为什么循环打印 【获得了一个信号:8】 呢?因为CPU的状态寄存器中的上下文信息没有改变,并且进程又一直不退出,OS将该进程换出运行队列时会保护该寄存器的上下文信息,重新回到运行队列时又将之前的上下文信息恢复到状态寄存器中,所以OS会一直检测到除0产生的异常,所以会不断向进程发送8号信号。有没有什么好的办法结束循环呢?没有,只能手动终止进程。

问题:如何更加深入的理解野指针越界呢?

答案:无论是野指针,还是越界,都要通过虚拟地址找到物理地址,而只有页表和MMU(即内存管理单元 Memory Management Unit)协同工作才可以将虚拟地址转换成物理地址。无论是野指针,还是越界,它们都访问了非法虚拟地址,那么虚拟地址通过MMU转换时一定会报错,而MMU是个硬件,几乎所有硬件里都有寄存器,那么MMU报错的含义就是将MMU内部的状态寄存器上对应的状态标记位上改成1,表示目前存在越界或者野指针的问题。那么OS如何确定是哪个进程产生了野指针越界问题呢?和除0问题中的方案一样,MMU中有一个寄存器专用于保存当前正在运行的进程的PCB,而PCB中又保存了进程的PID,所以只需要提取该进程的PID,向它发送信号即可。

3.3、方法三

由软件条件产生信号,OS识别到某种软件的条件触发或者不满足,就会构建信号,发送给指定的进程。举个例子,比如管道就是一个软件,建立管道后,关闭读端的fd,此时如果写端还在写,OS就会终止写端进程,如何终止呢?OS会向写端发送13号信号,即SIGPIPE。如何验证这一点呢?流程如下图。

4dc99792b61545a1bf56b66fc99cb81f.png

还有其他由软件条件产生的信号吗?请往下看。

通过alarm函数

函数原型为unsigned int alarm(unsigned int seconds)。调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发SIGALRM信号,该信号的默认处理动作是终止当前进程。注意该函数只能产生一次信号,即闹钟触发后就自动移除了,如果想再次产生信号,就得再次调用alarm函数,如下图就以一种巧妙的形式循环调用catchSig函数,每次调用catchSig函数都会重设alarm函数,alarm函数产生信号,signal函数收到信号又继续调用catchSig函数。注意,如果下图没有注释掉cout<<语句,即count++一次后立马要cout输出count目前的值,那么1s后cout<<count的值就是2w多,即count只被++了2w多次。为什么cpu会跑得这么慢呢?实际上是因为这里涉及到IO,不是cpu跑得慢,而是IO慢,因为cout输出语句不执行完,count++语句不会执行,而IO的过程占据了1s的绝大多数时间,所以真正count++的时间没有多少。

3f3b4a1fc1414cb2895b9eefe447530f.png

3.4、方法四

利用系统函数向进程发信号。那么有哪几种系统函数可以发信号呢?请往下看。

通过kill函数

2fe8877070ec4a7288778435eb3835cf.png

如上图的kill函数,kill指令本质就是调用了kill函数,下面是kill指令的模拟实现。

911cfd0a64fb4d30b3db3aa3007303e9.png

通过raise函数

26c6572f0e8c4c49b855c13b3e9dc55c.png

raise函数用于给该函数所在的进程发送信号,即给自己发送信号。比如在进程执行raise时,就给该进程发送信号。

通过abort函数

c3799c59de824573a54d85ee7148d9e6.png

abort函数用于给该函数所在的进程发送abort信号,即如下图的6号信号,主要用于退出进程,而且如果打开了核心转储功能,还会在退出进程时自动创建core文件。 可以认为abort()函数就底层调用了raise(6)。

2e217ca37b3243e5bf2de33fb8272937.png

4、信号产生后,对信号的处理方式有三种

1.按默认设置的方式处理。

2.忽略信号。

3.自定义捕捉,即将接收到的对应信号的处理方式自定义,具体情景看signal函数。注意信号处理后也要修改pending位图,将处理的信号对应的比特位从1修改成0。

4.1、函数sighandler_t signal(int signum,sighandler_t handler)

11858b612a034524ad8a00dcf50b2647.png

1.函数用于将第signum号信号的处理方式改成函数指针handler指向的函数的执行逻辑。

2.第一个参数表示信号,可以直接传入对应信号的宏,比如SIGINT,也可以直接传入信号代表的整形,比如SIGINT的整形就是2。

3.接下来说说第二个参数handler,它描述了与信号关联的动作。参数可以为三种值,第一种为SIG_IGN,表示ignore忽略掉该信号,第二种为SIG_DFL ,表示default恢复对信号的系统默认处理,不写此处理函数默认也是执行系统默认操作。注意不要把SIG_IGN与SIG_DFL和信号混为一谈,所有信号的宏如SIGINT是没有下划线【_】的,那它们是什么呢?SIG_IGN与SIG_DFL本质就是函数,又因为函数名就是函数的地址,所以SIG_IGN与SIG_DFL同时也是函数的地址。第三种表示用户自定义,比如先typedef void(*sighandler_t)(int);重定义了一个函数指针类型后,此时sighandler_t就变成了一个函数指针的类型,可以指向任何参数类型为int,返回值为void的函数,将一个类型匹配的函数A传入参数handler,就可以将对应信号的处理方式改成执行函数A,比如ctrl c这个键盘组合本质就是发出2号信号,让OS终止一个进程,调用signal(2,func)后,再次ctrl c就不再具有终止进程的功能了,此时会执行函数func,就如同下图情景。注意自定义的信号处理函数的形参必须是int类型,接收到信号后,执行信号处理函数时,OS会自动把信号的int值传入信号处理函数。

代码如下

8b042efddfc848a8bf501395de32f33d.png

代码如下

a02275b2ef7e46d6a79f08fffe1d33ca.png

如上图红框处,此时ctrl c不会终止进程,而是执行自定义的cathSig函数。

4.返回值不重要,但也简单说一下,成功时返回先前的信号处理函数指针,如果有错误则返回SIG_ERR(-1),并设置错误码errno。   

4.1.1、核心转储

976fc0ad2b584ddda77c0c296ee93790.png

问题:2号或者3号信号都用于终止一个进程,但如上图,2号信号的Action是Term表示terminate(终止),而3号表示Core,这是什么意思呢?

答案:不管Action是Term还是Core,它们都用于终止进程,而Core表示该信号具有核心转储的功能,即先运行一个进程,如果该进程运行时出现异常,OS就会将内存中该进程的核心数据转存到磁盘上,目的主要是方便调试,如何方便调试请继续看下文。

一般而言,云服务器这种生产环境的核心转储功能是被关闭了的,为什么呢?因为core文件不小,频繁生成会占用大量磁盘空间,所以关闭了该功能。使用ulimit -a指令可以查看是否被关闭,如下图红框处设置的core file size为0,就表示该功能被关闭了。

1814ce99b66c43efbe0e4a847aeebb6c.png

4683eba7810e46ba8d82a803154b5335.png

那么该如何打开核心转储的功能呢?如上图红框处,给定的选项是 -c,那么就可以像下图这样设置core file size,设置后该功能就可用了,如下图红框处从0变成了10240,表示允许文件最大为1024K。

fc2abc39f11c43d8ab753c8b33f53fd5.pnga54dc36b912c4fd6a58f02c3198d054d.png

核心转储功能可用后,假如shell界面A中正在运行一个进程,进程逻辑为不断打印数据,此时在另一个shell界面B中再次使用Action为Core的信号中断一个进程时,shell界面A中打印数据的末尾除了会有该信号的Comment,还会加上一句core dumped,并且会自动生成一个core文件,如下图。

976fc0ad2b584ddda77c0c296ee93790.png

a714ce0f94cb45348e3ca7b94c7aa86a.png

19b9737331cf469884ce4b539b54be53.png

5923300407f945d7ad7e0f9e97e5c62e.png

core文件的名称生成规则:前缀是core,后缀是对应进程的PID。由于core文件里的数据是从内存中转存到core文件的,所以数据全是一些二进制数,由vim编辑器打开就是一堆乱码,使用二进制工具打开文件才可以看到一堆二进制码,如输入指令od core.8728。

核心转储为什么方便调试呢?如下图,因为核心转储功能打开后,进程运行时出现异常并且退出后会生成core文件,此时将对应的core文件加载进gdb中就可以直接定位到出现异常的位置,如下图中将core文件加载进gdb后,立马定位到了该进程异常退出是因为29行发生了除0错误。

fe77eeb456474afd96068d6851037b52.png

0d4ebb57424749feb766f48beedc6b45.png

4.2、函数int sigaction (int signo, const struct sigaction *act, struct sigaction *oact)

和signal函数的功能极为相似,sigaction函数可以读取和修改与指定信号相关联的处理动作。调用成功则返回0,出错则返回 - 1。signo是指定信号的编号。若act指针非空,则根据act修改该信号的处理动作。若oact指针非空,则通过oact传出该信号原来的处理动作。act和oact指向sigaction结构体,这里有点特殊,函数名和结构体名相同,一般不建议这么写,但设计者就是这么做了。

1.第一组实验

代码如下

运行结果如下

为什么结果是0呢?SIG_DFL是个宏,表示信号的默认处理程序,同时也表示函数的地址,因为函数名就是函数的地址,DFL是default默认的意思。这里在调用sigaction函数前没有对2号信号的捕捉方式(或者说信号处理函数)做自定义处理,所以2号信号对应的信号处理函数就是SIG_DFL,oact.sa_handler是oact结构体中的指针成员,这个指针就指向SIG_DFL函数,将函数地址强转成int后值就为0。这里也能看出可以通过结构体参数oact传出该信号原来的信号处理动作(信号处理函数)。

2.第二组实验

代码如下

运行结果如下

为什么结果是1呢?SIG_IGN是个宏,值为1,表示忽略信号对应的处理函数,同时也表示函数地址,因为函数名就是函数的地址。这里在调用sigaction函数前用了signal函数将2号信号的处理方式换成了SIG_IGN,又因为结构体参数oact是用来获取信号原来的信号处理动作,即信号处理函数的,所以调用sigacation函数后,第二次修改了2号信号的处理函数,此时将获取到的函数的地址强转后得到的值为1。

4.2.1、与函数同名的sigaction结构体(内容包括对信号未处理完,又收到该信号的处理方式的说明)

本章只讨论上图中打勾的结构体成员。

1、函数指针void (*sa_handler)(int),指向函数的参数类型为int,返回值为void。此参数和signal()的参数handler相同,代表新的信号处理函数。

2、一种信号正在被处理,但没有被处理完,如果这种信号再次产生该怎么办呢?当某个信号的处理函数被调用时,内核OS会自动将该信号加入进程的信号屏蔽字(即下文中会讲的block位图),并且当信号处理函数结束返回时OS会自动恢复原来的信号屏蔽字(即将该信号从block位图中去掉,也就是将该信号对应的位修改成0),这样就保证了在处理某个信号时,如果这种信号再次产生,那么该信号会被阻塞到先来的信号处理结束为止。说详细点就是:

  • (首先要注意,只要信号开始处理,即使没有被处理完,pending位图上表示该信号的位也要设置成0,不然在执行该信号的过程中如果再次收到该信号,那该信号就会丢失)当4号信号开始被处理时,OS就会自动将信号屏蔽字即block位图上代表4号信号的位被设置为1,那么如果此时4号信号没有被处理完,并且4号信号再次产生时,OS就会因为block位图而不会执行4号信号的信号处理函数,而只是会把pending位图上4号信号代表的位设为1,等到最开始的4号信号被处理完后,OS再自动将block位图上代表4号信号的位设置成0,这时因为pending位图上代表4号信号的位为1,并且block位图上代表4号信号的位为0,所以OS检测到这一点后就会再执行4号信号的处理函数,然后4号信号开始处理时就又将pending位图上代表4号信号的位设置成0,并且自动将block位图上代表4号信号的位设置成1。

如果在调用信号处理函数时,除了当前信号被OS自动屏蔽之外,你还希望自动屏蔽另外一些信号,则可以用sa_mask字段说明这些需要额外屏蔽的信号,当信号处理函数结束返回时又会自动恢复原来的信号屏蔽字(说一下,sa_mask是个sigset_t类型的变量,所以它也是个位图,并且注意sa_mask并不等价于信号屏蔽字即block位图,只是说会把sa_mask的值赋给block位图,以此来告诉OS需要额外暂时搁置哪些信号)

3、sa_flags字段包含一些选项,本章不详细解释这个字段,而是都把sa_flags设为0,有兴趣的同学可以再自行了解一下。

4、sa_sigaction是实时信号的处理函数,本章不详细解释这个字段,有兴趣的同学可以再自行了解一下。

具体操作看下图的实验,下图红框处也能看出sa_mask的使用方法,因为它也是sigset_t类型的变量,所以也得用系统提供的接口实现位操作。一般来说不会出现【信号未被处理完又收到该信号】的情况,因为信号被处理的很快,一瞬间信号处理函数就被调用完毕并返回了。

代码如下

运行结果如下

打开两个shell界面,上图是下图的进程在运行时输入的命令。

如上图,利用kill发送信号后打印pending位图,会发现信号对应的位上从0变1,只是目前block位图上信号对应的位也是1,信号被暂时屏蔽了,当发送的第一个信号,即2号信号处理完后,信号屏蔽字即block位图就会自动被OS还原,所以后序的信号最终都会被执行,由于其他信号的处理方式都是默认的,也就是退出进程,随机执行某个信号后,进程就退出了。

那如果真正意义上的同时收到多个同一信号该怎么办呢?

没有什么特别好的办法,因为pending位图上只能记录该信号是否产生,而无法记录产生了多少次。如果这个【同时】是严格意义上的意思,那同时向某个进程发送多个同一信号,只能造成信号丢失了。

5、信号的其他相关概念

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

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

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

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

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

6、阻塞信号

task_struct中除了有pending位图,还有block位图和handler数组。下图的handler是一个函数指针数组,数组中的元素下标为几,则代表几号信号的处理函数。signal函数本质就是将handler数组中下标为signum的元素换成指定的函数的地址。block也是一个位图,和pending结构上没有区别,只不过是位上的0和1表示的含义不同,block位图上的位为1表示频闭该信号,举个例子,假如OS发送2号信号将pending上的第二个位设置为1,但此时block位图上的第二个位也为1,此时就阻塞该信号(注意不是忽略而是阻塞),不执行2号信号对应的处理函数。简单来说:发送信号就是修改pending位图的结构,然后会在合适的时机处理信号,如何处理呢?首先看pending位图上对应的位是不是1,如果是1,就去查block位图上对应的位,如果是1,则阻塞该信号,如果是0,就执行信号对应的处理函数。但注意,即使信号被阻塞了,但只要发送了某信号,pending集中对应的位上的数字一定是1,只不过没有执行handler表中的处理函数罢了。比如我们将2号信号block,并且不断的获取并打印当前进程的pending信号集,如果我们突然发送一个2号信号,我们一定可以肉眼看到pending信号集中,有一个比特位从0变成1。

6.1、阻塞信号在内核中的表示

f3fee1e69b644d5386aa0ba121e19d14.png

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

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

3.SIGQUIT信号未产生过, 一旦产生SIGQUIT信号将被阻塞, 它的处理动作是用户自定义函数sighandler。如果在进程解除对某信号的阻塞之前这种信号产生过多次, 将如何处理 ? POSIX.1允许系统递送该信号一次或多次。Linux是这样实现的:常规信号在递达之前产生多次只计一次,而实时信号在递达之前产生多次可以依次放在一个队列里。这里不讨论实时信号。

6.2、sigset_t类型

1.从上面阻塞信号在内核的表示图中可以发现,每个信号只有一个bit的未决标志,它非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。因此未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号的【有效】或【无效】状态,在阻塞信号集,即block位图中【有效】和【无效】的含义是该信号是否被阻塞,而在未决信号集,也就是pending位图中【有效】和【无效】的含义是该信号是否处于未决状态。阻塞信号集也叫当前进程的信号屏蔽字(Signal Mask),这里的【屏蔽】应该理解为阻塞而不是忽略。这个类型的变量一般是给PCB中的block或pending位图作参考,之后OS会将某个位图的结构修改得和sigset_t变量一样。

2.sigset_t是一种由OS提供的数据类型,它也是被当作一个位图使用的。用户是可以直接使用该类型的变量的,就和其他内置类型或者自定义类型一样。注意不允许用户对sigset_t类型的变量进行位操作,即用户不被允许用该变量进行按位与等等操作,OS会提供一套操作位图的方法,一定需要对应的系统接口来完成对应的功能,其中系统接口需要的参数,可能就包含了sigset _t定义的变量或者对象。有哪些接口呢?请往下看。

6.2.1、int sigemptyset(sigset_t* set)

在头文件<signal.h>中。函数sigemptyset初始化set所指向的信号集,使其中所有信号的对应bit设置成0,表示该信号集不包含任何有效信号。成功返回0,出错返回-1。

6.2.2、int sigfillset(sigset_t* set)

在头文件<signal.h>中。用于初始化set所指向的信号集,使其中所有信号的对应bit置为1,表示该信号集的有效信号包括系统支持的所有信号。成功返回0,出错返回-1。

6.2.3、int sigaddset(sigset_t* set, int signo)

在头文件<signal.h>中,用于将signo对应的位设置成1。注意在使用sigset_ t类型的变量之前一定要调用sigemptyset函数或sigfillset函数将sigset_t类型的变量初始化,使信号集处于确定的状态。初始化sigset_t变量之后就可以再调用sigaddset和sigdelset在该信号集中添加或删除某种有效信号。成功返回0,出错返回-1。

6.2.4、int sigdelset(sigset_t* set, int signo)

在头文件<signal.h>中,用于将signo对应的位设置成0。注意在使用sigset_ t类型的变量之前一定要调用sigemptyset或sigfillset做初始化,使信号集处于确定的状态。初始化sigset_t变量之后就可以再调用sigaddset和sigdelset在该信号集中添加或删除某种有效信号。成功返回0,出错返回-1。

6.2.5、int sigismember(const sigset_t* set, int signo)

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

6.2.6、int sigprocmask(int how, const sigset_t* set, sigset_t* oset)

1.在头文件<signal.h>中,用于检查和更改block位图,即阻塞信号集。如下图,参数how有三种值,第一种是SIG_BLOCK,表示将函数的第二个参数set设置好的位新增block位图中,注意这里只是新增,而不是重置,比如set中第二个位为1,其他位都是0,那么传SIG_BLOCK给how就表示将block位图上第二个位设置成1,其他位保持原样。第二种是SIG_UNBLOCK,与第一种相反,表示将set设置好的位在block位图中删除。第三章是SIG_SETMASK,表示直接将block位图结构设置成函数的第二个参数set。成功返回0,失败返回-1,并设置错误码errno。

bed930c962dc42d682e3589631c88f7a.png

2.无论how的值是第几种,调用函数后,原来的block位图结构一定已经改变了,那想要恢复成原来的block位图结构该怎么办呢?可以依靠函数的第三个参数,第三个参数oset是一个输出型参数,用于记录旧的block位图结构,设置一个sigset_t类型的变量,然后将变量地址传进函数,函数结束后不仅可以更新block位图,还可以拿到旧的block位图。

6.2.7、int sigpending(sigset_t *set)

在头文件<signal.h>中,用于获取当前进程的pending信号集,即pending位图。参数set是一个输出型参数,OS会将pending位图传给参数set。成功返回0,失败返回-1并设置错误码errno。

7、有没有一种办法,让一个进程无法被杀死呢?

问题:如果我们对所有的信号都进行了自定义捕捉,我们是不是就写了一个不会被异常或者用户杀掉的进程?

代码如下

1efc8981d93141b0b8b5924a4461da2d.png

运行结果如下e75153b4b53b4bdcbc2db28bd789b679.png

答案:不是。9号信号SIGKILL即使被捕捉了,设置的处理方案也不会生效,9号信号是管理员信号。

问题:如果我们对所有的信号都进行block阻塞,我们是不是就写了一个不会被异常或者用户杀掉的进程?

答案:也不是,同样的,9号信号SIGKILL无法被阻塞或者说屏蔽。

8、我们说信号产生后,信号可能无法被立即处理,会在合适的时机处理,那什么时候是合适的时机呢?

信号的相关数据字段都在进程的PCB内部,而PCB是属于内核,那么想要处理信号,首先还得检测信号,但不管是处理信号还是检测信号,由于信号的相关字段都在PCB中,而只有OS有权限访问PCB,所以一个进程可以通过系统调用来处理信号,但因为CPU只有处于内核态时才可以执行系统调用的代码,所以CPU一定是处于内核态时才会处理信号,更准确地说,在CPU处于内核态的时间段中,从内核态返回用户态之前,才会进行信号的检测与处理。举个例子,如下:

  • 问题:进程切换时,cpu会从用户态变成内核态(不理解看下图),既然信号会在cpu从内核态变回用户态时被处理,那么假如要处理进程A的信号,现在是进程A马上要切换成进程B,那么是在进程A切换成进程B的前一刻处理信号,还是从进程B再次切回进程A的前一刻(因为进程B即将切回进程A时,也要即将从内核态转换成用户态,此时也满足处理信号的条件)处理信号呢?
  • 答案:虽然进程A即将切换到进程B,但现在还没有完成切换,而此时因为进程切换对应系统调用即将结束,也就是即将要从内核态返回到用户态,满足了处理信号的条件,而此时因为进程A没有完全切换成进程B,所以处理进程A的信号,也就是说,上面问题的答案是在进程A切换成进程B的前一刻处理信号。

这里额外补充一点,页表除了用户级页表还存在内核级页表,因为进程具有独立性,不同进程的代码和数据在物理内存上得分开,所以每一个进程的用户级页表的映射关系都不一样,即每个进程的用户级页表不一样。但因为OS的代码和数据在物理内存中只有一份,所以每个进程的内核级页表可以完全一样,所以内核级页表在OS中可以只存在一份。

8.1、处理信号的流程图

8.2、用户态和内核态

用户态和内核态是操作系统中CPU的两种运行状态。

内核态:处于内核态的 CPU 可以访问任意的数据,包括外围设备,比如网卡、硬盘等,处于内核态的 CPU 可以从一个程序切换到另外一个程序,并且占用 CPU 不会发生抢占情况,一般处于特权级 0 的状态我们称之为内核态。

用户态:处于用户态的 CPU 只能受限的访问内存,并且不允许访问外围设备,用户态下的 CPU 不允许独占,也就是说 CPU 能够被其他程序获取。 当程序运行在3级特权级上时,可以称之为运行在用户态。

8.3、OS如何识别CPU是处于哪种状态呢?

CPU中有两套寄存器,有一套寄存器CPU不可见,其中就有一个寄存器CR3表示CPU执行权限,比如值为1,就表示内核态,值为3就表示用户态,所以OS读取该寄存器就可以知道CPU的状态了。

8.4、CPU那如何进入内核态呢?

OS发现进程在执行系统调用的代码时,OS就修改CR3的值,让CPU进入内核态。注意前面说的系统调用都是合法的系统调用,是不需要身份认证的谁都可以调用的接口,而执行不合法的系统调用时,首先会进行身份认证,如果认证通过,才会让CPU进入内核态。注意系统调用结束后,OS一定会将CPU从内核态切换成用户态。

8.5、为什么执行完系统调用后,CPU一定要从内核态返回用户态呢?

CPU处于内核态是可以访问属于用户的虚拟地址空间中的数据和代码的,但处于用户态不可以访问属于OS的虚拟地址空间中的数据和代码,那既然CPU处于内核态时权限更高,可以执行属于用户的代码,为什么执行用户的代码时要从内核态返回用户态呢?如果CPU处于内核态执行用户代码,意味着用户写出什么,CPU就会执行什么,那如果某个用户带着恶意比如想删除或者盗取磁盘文件该怎么办呢?所以CPU返回用户态的意义就在于此,因为用户态的程序就不能随意操作内核地址空间,具有安全保护作用。

8.6、CPU执行不写系统接口的进程的代码时会进入内核态吗?

比如一个进程只有一个while死循环,并且什么也不做,用户没有写任何系统调用,是不是CPU就无法进入内核态了呢?答案:不是,因为即使用户没有写系统调用,当该进程的时间片结束后,需要发生进程切换时也会在死循环的进程中调用进程切换的系统接口,所以也会进入内核态。所以可以认为CPU在执行进程代码时,不管进程是什么样的,CPU都会进入内核态,也正是因为无论如何都会进入内核态,所以就可以在CPU从内核态返回用户态时顺便检测并处理一下信号。

9、volatile关键字

先说结论,volatile关键字就是让编译器不要优化用该关键字修饰过的变量。更详细的内容请往下看。

有些编译器会自动给我们的代码进行优化,并且还存在优化级别这个概念,如下图红框处的几个参数。一般来说,在用gcc或者g++编译时如果不加上任何优化参数,则优化参数默认为 -O0,即没有优化,还有优化参数 -O1、-O2、-O3,随着数字变大,代码的优化级别也越高,不过优化级别高有时不是一件好事,甚至会出现很多问题,比如明明代码没有问题,但程序出现莫名其妙的错误,并且很难通过调试找到问题,从哪里体现呢?请看下图中的实验。

1.第一组实验

代码如下

运行结果如下 

按CTRL+C后,即发送2号信号后,执行了changeFlag函数,改变了flag的值,从而退出了死循环,最后进程正常退出。

2.第二组实验

代码如下

和第一组实验的代码完全相同,只不过在用g++编译时加上了优化参数-O3,如下图

运行结果如下

按CTRL+C后,即发送2号信号后,执行了changeFlag函数,改变了flag的值,但死循环结束不了,导致进程无法退出,为什么呢?请继续往下看。

问题:可以发现上图中,明明都改变了flag的值,但只有第一组实验可以结束死循环并正常退出进程,第二组无法结束死循环从而无法退出进程。为什么呢?

答案:如下图,编译器在编译时,发现flag变量在main函数中压根没有改变,只做了while循环的判断条件,那编译器就会想:只有CPU具备执行代码的功能,而CPU每次做循环的条件判断时都要去内存中找flag变量,外设之间(CPU和内存之间)IO相对于不进行IO是需要更多时间的,会影响效率,既然flag的值在main函数中不发生改变,让flag的值加载进CPU的dex寄存器中,每次CPU做while循环的条件判断时,直接让CPU访问CPU内部的寄存器就可以了,所以编译器就在编译时自动增加一些代码,代码的含义就是:以后只要是CPU需要访问flag变量,就直接让CPU读取寄存器中的值,而不往内存读取。所以在受到ctrl+c,即2号信号时,即使调用了changeFlag函数修改了flag的值,让在物理内存中的flag从0变成了1,但CPU压根不读取内存的flag,只读取寄存器中的flag,即0,所以最后导致结束不了死循环,从而无法退出进程。

10、SIGCHLD信号

之前说过用waitwaitpid函数清理僵尸进程,父进程可以阻塞等待子进程结束,也可以非阻塞地查询是否有子进程结束等待清理(也就是给waitpid函数传WNOHANG,然后waitpid函数和循环组合使用)。采用第一种方式,父进程阻塞了就不能处理自己的工作了,采用第二种方式父进程在处理自己的工作的同时还要记得时不时地轮询一 下,程序实现复杂。

事实上子进程在终止时会给父进程发SIGCHLD信号(还有其他发送SIGCHLD信号的方法,请往下看),SIGCHLD信号的默认处理函数是忽略函数,但这个忽略函数有点特别,特别的点在哪儿呢?请继续往下看。父进程可以自定义SIGCHLD信号的处理函数,这样父进程只需专心处理自己的工作,不必关心子进程了,子进程终止时会给父进程发信号,父进程在信号处理函数中调用wait清理子进程即可。如何验证子进程在终止时会给父进程发SIGCHLD信号呢?请看下图中的实验。

代码如下

 运行结果如下

 成功调用了handler函数,说明了子进程退出的确会给父进程发送SIGCHLD信号。

问题:如果同时需要等待很多子进程该怎么办呢?
答案:如下图,利用waitpid函数和循环,waitpid参数为-1表示等待任意子进程,等待成功打印子进程的ID。

10.1、如果我们不想让父进程等待子进程,还想让子进程退出变成僵尸进程后能够被自动释放,该怎么做呢?

之前我们说SIGCHLD信号的默认处理函数是忽略函数,这个忽略函数有点特别,原因就是默认处理函数的忽略函数和用户设置的忽略函数不一样,什么意思呢?如下图红框处,在signal函数中手动设置SIG_IGN忽略函数,就可以完成【不用让父进程等待子进程,还可以让子进程退出变成僵尸进程后能够被自动释放】这一功能,但如果不在signal函数中设置SIG_IGN忽略函数,虽然默认还是执行SIG_IGN函数,但就是会造成子进程退出后变成僵尸进程,无法被释放的问题。

这里补充一点,如下图红框处,不止只有子进程退出时会给父进程发送SIGCHLD信号,当子进程暂停的时候也会给父进程发送SIGCHLD信号。如何验证呢?请看下图实验。

代码如下

运行结果如下

如上图,先把进程运行起来,然后用19号信号暂停子进程,此时子进程就会给父进程发送SIGCHLD信号,然后父进程就会执行handler函数的代码。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值