【Linux】进程信号详解(三)

文章讨论了可重入函数的概念,解释了为何某些函数在多线程或信号处理上下文中可能导致问题。volatile关键字在此被用来确保变量在多线程环境中的内存可见性,防止编译器优化导致的数据异步问题。此外,文章还介绍了SIGCHLD信号在进程管理中的作用,如何通过捕获该信号来避免父进程阻塞等待或轮询子进程,以及通过设置SIG_IGN自动清理子进程的方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、可重入函数

假设有一个不带头的单链表,要进行头插操作,在我们数据结构阶段都已经学习过,我们可以有以下的步骤:
在这里插入图片描述

要将node1头插到单链表中,调用insert函数,第一步将p的next指向head,再将head的值赋为p,但是在学习了信号之后,我们就要考虑一个问题,如果在第一个结束之后,将p连在了后边,此时收到一个信号,而此信号被自定义捕捉,而捕捉函数内部又是一个头插函数,此时的insert函数被重复进入了,将node2头插,但是在头插成功之后,返回主函数,又将node1的地址给了head,此时node2就会造成内存泄露,说明这个函数是不可重入的。

像上例这样,insert函数被不同的控制流程调用,有可能在第一次调用还没返回时就再次进入该函数,这称重入,insert函数访问一个全局链表,有可能因为重入而造成错乱,像这样的函数称为 不可重入函数,反之,如果一个函数只访问自己的局部变量或参数,则称为可重入(Reentrant) 函数。想一下,为什么两个不同的控制流程调用同一个函数,访问它的同一个局部变量或参数就不会造成错乱.

如果一个函数符合以下条件之一则是不可重入的:

调用了malloc或free,因为malloc也是用全局链表来管理堆的。
调用了标准I/O库函数。标准I/O库的很多实现都以不可重入的方式使用全局数据结构

二、volatile

在c语言中我们就接触过这个关键字,但是当时我们并不清楚这个关键字的含义,今天站在信号的角度上重新理解一下:
我们先来看这段程序:

#include <stdio.h>
#include <signal.h>
int flag = 0;
void handler(int sig)
{
 printf("chage flag 0 to 1\n");
 flag = 1;
}
int main()
{
 signal(2, handler);
 while(!flag);
 printf("process quit normal\n");
 return 0;
}

标准情况下,键入 CTRL-C ,2号信号被捕捉,执行自定义动作,修改 flag=1 , while 条件不满足,退出循环,进程退出。
在这里插入图片描述
而当我们改变一下优化的强度,优化情况下,键入 CTRL-C ,2号信号被捕捉,执行自定义动作,修改 flag=1 ,但是 while 条件依旧满足,进程继续运行!但是很明显flag肯定已经被修改了,但是为何循环依旧执行?很明显, while 循环检查的flag,并不是内存中最新的flag,这就存在了数据二异性的问题。 while 检测的flag其实已经因为优化,被放在了CPU寄存器当中。

sig:sig.c
	gcc -o $@ $^ -O3
.PHONY:clean
clean:
	rm -rf sig 

在这里插入图片描述

如何解决呢?很明显需要 volatile.

#include <stdio.h>
#include <signal.h>
volatile int flag = 0;
void handler(int sig)
{
 printf("chage flag 0 to 1\n");
 flag = 1;
}
int main()
{
 signal(2, handler);
 while(!flag);
 printf("process quit normal\n");
 return 0;
}

在这里插入图片描述
当在全局变量前加上volatile之后,我们会发现程序照应可以正常运行了,这是因为volatile关键字可以保持内存的可见性,告知编译器,被该关键字修饰的变量,不允许被优化,对该变量的任何操作,都必须在真实的内存中进行操作,就是每次处理这个变量之前,都去内存中找,而不是使用寄存器中保存的该变量的值。

三、SIGCHLD信号

在进程的等待章节,我们知道了可以使用wait和waitpid系统接口来等待进程,父进程可以阻塞等待子进程结束,也可以非阻 塞地查询是否有子进程结束等待清理(也就是轮询的方式)。采用第一种方式,父进程阻塞了就不 能处理自己的工作了;采用第二种方式,父进程在处理自己的工作的同时还要记得时不时地轮询一 下,程序实现复杂。

其实,我子进程在终止时也会给父进程发送一个信号,如果父进程收到信号后将子进程清理,就不用一直阻塞等待或者轮询等待子进程了,子进程会给父进程发送SIGCHLD信号,是17号信号,他的默认处理动作是忽略,所以我们平时才没有注意到这个信号,如果我们自定义捕捉这个信号,并且在捕捉函数中去处理这个信号,那么是不是父进程就不用一直等待了呢?

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include<unistd.h>
#include<sys/wait.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;
}

在这里插入图片描述
事实上,由于UNIX 的历史原因,要想不产生僵尸进程还有另外一种办法:父进程调 用sigaction将SIGCHLD的处理动作置为SIG_IGN,这样fork出来的子进程在终止时会自动清理掉,不会产生僵尸进程,也不会通知父进程。系统默认的忽略动作和用户用sigaction函数自定义的忽略 通常是没有区别的,但这是一个特例。此处手动设置处理动作为SIG_IGN与默认处理动作为忽略是不一样的。

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include<unistd.h>
#include<sys/wait.h>
int main()
{
  	signal(SIGCHLD,SIG_IGN);
 	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,让系统在子进程运行完成之后,直接清理掉。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

清扰077

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值