内核如何实现信号的捕捉
信号捕捉的方法出了我们之前的signal之外,还有其它方法
sigaction

sigaction:检查或更改一个信号的动作即捕捉信号
第一个参数,要捕捉的信号对应的编号,第二个参数:结构体(这个结构体类型名和函数名一样,这里不是回调这个函数),该类型是输入型参数,第三个参数 输出型参数,对于这个信号的老的处理方法,类似于sigprocmask。成功返回0,失败返回-1,并设置错误码
sigaction结构体

第一个是信号捕捉对应的回调函数,第2,4,5个参数暂时不考虑
基本用法
makefile里面最后这个-fpermissive是防止打印默认处理方法时,在强制转换那里出错,加上之后就不会报错,但是会变成警告



我们把二号信号的处理方法设置为忽略

此时默认处理就是1,1是忽略
处理信号的时候,执行自定义动作,如果在处理信号期间又来了同样的信号,OS会如何处理?
本质:为什么要有block。
当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生,那么 它会被阻塞到当前处理结束为止。 如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用sa_mask字段说明这些需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字。 sa_flags字段包含一些选项,本章的代码都把sa_flags设为0,sa_sigaction是实时信号的处理函数
验证:
我们在处理信号的时候sleep10秒

这10s期间,我们不断发送二号信号,我们此时看到我们发送的二号信号没有被处理
我们观察一下,上述过程发生期间,pending位图


我们把3-7号信号也加入


我们可以看出在我们处理2号信号期间,其它信号也是被block的,直到信号处理完毕,才会处理下一个信号。
可重入函数
信号捕捉并没有创建新的进程或者线程

当我们头插node1调用insert函数时,收到了二号信号,二号信号的捕捉方法里面也是一个头插,此时将node2进行头插,之后返回node1头插,这里俩次头插让head改变了俩次,最后造成了n2节点丢失,内存泄漏的问题。这种现象叫函数重入
这种因为时序产生的问题存在,而且不好被排查。
可重入函数:一个函数在特定的时间段内被多个执行流重复进入,这种情况就叫重入函数。而该函数重入后没有问题,称之为可重入函数。而上面的insert方法,出现了问题这种就叫不可重入函数。这俩种函数都是函数的一种特征,大多数函数都是不可重入的。
如果一个函数符合以下条件之一则是不可重入的:
调用了malloc或free,因为malloc也是用全局链表来管理堆的。
调用了标准I/O库函数。标准I/O库的很多实现都以不可重入的方式使用全局数据结构。
volatile

捕捉二号信号后,让flag=1,我们看while循环此时能不能退出
我们发现此时while循环退出。

编译器有时候会自动的给我们进行代码优化。
gcc里面O3是优化级别最高的,我们添加该选项

优化后,我们执行程序,发送二号信号

此时flag改变后,while循环居然不退出。
这是因为之前编译器未优化。我们定义的flag是全局变量,在内存中给flag开辟空间后,我们执行代码让flag=1,代码是在CPU中执行的,flag的值读到CPU寄存器中进行修改,将修改后的值返回。

当优化之后,编译器发现main函数里没有任何一个语句是修改flag的,编译器在第一次启动时候,直接把flag的值0放到edx中,之后while循环做检测,CPU就自行检测flag,不再进行访问内存,当修改flag时,flag有0变1,但此时只是将内存中的flag修改了,CPU中的flag的值还是0
也就是说因为有优化的存在,数据被优化到了寄存器当中,进而导致程序不会访问内存,只访问寄存器的值,也就是说让CPU无法看到内存了,导致内存被遮盖。

为解决这种问题,即每次让CPU去访问内存,保证内存的可见性,引入了volatile关键字,加入之后由于有O3所以代码既被优化,有保证了内存的可见性。


上面把数据直接放入CPU寄存器的优化发生在编译的时候。
SIGCHLD信号
用wait和waitpid函数清理僵尸进程,父进程可以阻塞等待子进程结束,也可以非阻 塞地查询是否有子进程结束等待清理(也就是轮询的方式)。
采用第一种方式,父进程阻塞了就不 能处理自己的工作了;采用第二种方式,父进程在处理自己的工作的同时还要记得时不时地轮询一 下,程序实现复杂。
其实,子进程在终止时会通过操作系统给父进程发SIGCHLD(17号信号)信号(注:只有Linux下才有这种情况),该信号的默认处理动作是忽略,父进程可以自 定义SIGCHLD信号的处理函数,这样父进程只需专心处理自己的工作,不必关心子进程了,子进程 终止时会通知父进程,父进程在信号处理函数中调用wait清理子进程即可。


当运行程序后,子进程退出时,我们发现我们说到了SIGCHLD信号,证明了子进程退出会给父进程发送信号。
若有多个子进程,多个子进程同时退出,我们设置一个循环用wait()去进行回收,但如果10个子进程里面前5个子进程要退出。我们用while循环检测到前五个退出了并回收,那对于第六个子进程我们需不需要检测并回收?
答案是需要检测因为waitpid和wait除了回收之外还可以检测子进程是否退出,当我们同一时间收到5个SIGCHLD信号,可SIGCHLD这个信号对应的比特位只有一个,所以实际我们只会收到一个信号,因为pending位图中只有一个比特位表示是否收到某一个信号。
当我们检测到第六个子进程没退出,这时信号处理只能被阻塞,此时main执行流的事情就没办法执行了,为了解决此问题,我们可采用while循环用vector非阻塞的去遍历所有进程,然后回收掉该回收的进程,即退出了就回收,没退出就下一个。
若不想用vector保存,我们可将waitpid第一个参数设置为-1,-1的效果等价于wait,相等于等待任意退出的进程。
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
void handler(int sig)
{
pid_t id;
while( (id = waitpid(-1, NULL, WNOHANG)) > 0){
printf("wait child success: %d\n", id);
}
printf("child is quit! %d\n", getpid());
}
int main()
{
signal(SIGCHLD, handler);
pid_t cid;
if((cid = fork()) == 0)
{//child
printf("child : %d\n", getpid());
sleep(3);
exit(1);
}
while(1)
{
printf("father proc is doing some thing!\n");
sleep(1);
}
return 0;
}
若我们不关心进程的退出码,退出信号等。我们又该如何处理。
答:如果我们不想等待子进程,并且我们还想让子进程退出之后自动释放僵尸子进程。我们可用SIG_IGN

while :; do ps ajx | head -1 &&ps axj | grep mysignal|grep -v grep ; sleep 1; echo "-----------------------------------"; done
我们看到5s后子进程退出状态变为Z

我们手动对子进程进行忽略

5S后我们未看到子进程

SIG_CHLD的处理动作默认就是忽略,可我们又显示的设置了一个忽略,我们也可以发现设置忽略和没设置忽略差别很大,因为这俩个忽略的概念不一样,OS级别的忽略是默认的动作,即什么也不做,该是僵尸就是僵尸,而我们自己的SIG_IGN是用户告诉了OS我们要真的忽略,即在子进程退出时,直接把子进程释放掉。
我们验证进程暂停
我们让子进程sleep(100),暂时不退出


我们暂停了19号信号,发送了暂停信号,我们看到父进程受到了子进程的SIGCHLD信号。
因此在进程等待的时候,我们一般把等待方式设置为非阻塞,因为有可能子进程会暂停,我们必须得进行检测,检测到退出,我们就进行回收。