Linux基础内容(22)—— 信号_哈里沃克的博客-CSDN博客https://blog.csdn.net/m0_63488627/article/details/130835485
目录
1.可重入函数
1.情况假设
2.volatile
3.SIGCHLD信号
1.SIGCHLD介绍
2.信号的确认
3.wait的处理
1.可重入函数
1.情况假设
1.我们实现了一个main函数,是关于链表头插的逻辑。也就是说当我们调用该函数,指定的链表通过这个函数,new出一个新的节点,头插到链表中,头节点重新更新为当前节点
2.我们先描述一下这个头插的过程,就是先将节点new出来,随后将节点的next指向链表,最后将头节点更新
3.现在我们将注意放在信号处理逻辑上,首先我们先得到一个信号处理的函数,该函数的处理逻辑与new新节点头插链表完全一致。即当指定一个定义的信号后,信号会被捕获执行new新节点进行头插的逻辑
4.随后当执行头插函数过程中,当这个函数执行到将节点的next指向链表后,就接收到指定的信号进行处理逻辑,那么此时我们会发现一个事实,我们的链表分叉了。
1.像上例这样,insert函数被不同的控制流程调用,有可能在第一次调用还没返回时就再次进入该函数,这称为重入,insert函数访问一个全局链表,有可能因为重入而造成错乱,像这样的函数称为不可重入函数
2.如果一个函数只访问自己的局部变量或参数,则称为可重入函数
3.调用了malloc或free,因为malloc也是用全局链表来管理堆的。调用了标准I/O库函数。标准I/O库的很多实现都以不可重入的方式使用全局数据结构。使用这些都是不可重入函数
2.volatile
以下是一段关于信号来进行退出进程的代码
#include<stdio.h> #include<signal.h> int quit=0; void handler(int signo) { printf("%d 号信号,正在被捕捉\n",signo); printf("quit: %d",quit); quit=1; printf("-> %d",quit); } int main() { signal(2,handler); while(!quit); printf("正常退出\n"); return 0; }
1.正常编译该代码后执行,会发现进程一直循环着,一旦我们对进程传入2号信号后,该循环会被正常退出.这样的结果是可预见的
2.不过,我们需要介绍编译器优化。默认的编译器优化级别其实不高,但是我们可以手动调搞优化级别,在makefile中编译添加 -Ox(x为优化级别)。那下面我们对该段代码重新编译-O3级别,随后执行代码我们会发现:最开始代码仍然在循环中,说明此时quit=,随后信号2输入进程中,进程打印quit由0变1,但是此时我们发现进程并没有退出。随后继续不断输入2信号,quit由1变1,但是进程始终没有退出,代码并没有按照我们本来的意愿进行执行正常退出。
原因:
1.一个数据的流程是先由cpu在内存中得到对应的值,随后进行一系列操作cpu得到最终的结果将值返回给内存
2..对于默认优化的编译,其数值其实本来是在内存中,然后当while循环判断时,quit在内存中会将值传入cpu,cpu判断后,进行循环;一旦信号递达后,quit在内存中就被修改为1了,那么此时while判断时,cpu会从内存中得到quit的值,此时cpu的判断循环结束,整个进程在之后也就结束了
3.不过被优化过的编译,会认为quit这个值对于编译器而言就是只需要进行判断的,不需要返回给内存,那么自然在qiut被定义后,这个0值就一直在cpu的寄存器中存着,但是每一次的信号递达都修改的是内存中的quit值,但是内存的值不代表cpu中寄存器的值,循环结束的条件依然是quit在寄存器中的值,寄存器中的值至始至终都没有变过,循环一直进行。所以出现了我们即使信号递达后修改了当前的值也没有结束进程的奇怪思路
那么此时volatile就能被应用了
// volatile:保持内存可见性 volatile int quit=0; void handler(int signo) { printf("%d 号信号,正在被捕捉\n",signo); printf("quit: %d",quit); quit=1; printf("-> %d",quit); } int main() { signal(2,handler); while(!quit); printf("正常退出\n"); return 0; }
volatile:保持内存可见性,即每次数据都从内存中读取,而不是在cpu的寄存器中读。
执行该代码,优化后也依然是我们想要的第一种结果。
3.SIGCHLD信号
1.SIGCHLD介绍
父进程wait等待子进程并不是白白的硬等。子进程在要退出前会变成僵尸进程,在变成僵尸进程时,会像父进程发送SIGCHLD信号,使得父进程知道子进程死亡了。SIGCHLD的默认操作是系统层面的忽略(Ign)。
2.信号的确认
写一段代码验证
void handler(int signo) { printf("%d 号信号,正在被捕捉\n",signo); while(1) { pid_t ret = waitpid(-1, NULL, WNOHANG); if(ret == 0) break; } } int main() { signal(SIGCHLD,handler); printf("父进程 pid:%d ppid:%d\n",getpid(),getppid()); pid_t id = fork(); if(id==0) { printf("子进程 pid:%d ppid:%d\n",getpid(),getppid()); exit(1); } while(1) sleep(1); return 0; }
结果确实是说明的那样。
3.wait的处理
1.如果我们创造了一批的子进程并且全部的子进程是同一时间结束,那么父进程的waitpid就必须循环式的等待,因为只等待一次就只能回收一个,所以是while中循环的waitpid(-1, NULL, 0)回收任意的子进程。-1是指回收任意的子进程宏定义
2.如果我们想要回收一批创建好的子进程,不过这次我们的子进程并不是所有回收,只是回收一部分。那么此时的我们不能waitpid(-1, NULL, 0)这样等待,因为这样的等待父进程不会进行其他操作,并且回收完了父进程也不会知道,依然回收,最后导致父进程难被回收。那么我们想要挂起等待,waitpid(-1, NULL, WNOHANG),确保父进程不被挂起
3.当然,我们也可以直接选择用signal信号进行回收子进程,其做法很简单,就是将signal信号接收SIGCHLD后对信号手动忽略(SIG_IGN),即signal(SIGCHLD, SIG_IGN)。此时子进程被回收的工作就不用我们手动wait,系统自动的回收了。不过我们不会得到子进程的退出状态码,这一点需要注意
4.手动的SIG_IGN和信号自己的Ign的忽略不是同一个忽略。系统的Ign在内核会有其他的操作,我们用户调用后就能得到对应的状态码;但是手动的SIG_IGN不同,它是一个宏定义,它内部是将一个地址宏定义了,就是访问地址为1的空间