Linux进程信号(下:补充)
Linux进程信号(下)https://blog.csdn.net/Small_entreprene/article/details/146323008?fromshare=blogdetail&sharetype=blogdetail&sharerId=146323008&sharerefer=PC&sharesource=Small_entreprene&sharefrom=from_link我们本篇来补充对上文的叙述:理解用户态和内核态。
用户态和内核态
系统调用过程
系统调用过程:
用户程序发起请求:用户程序在用户态下运行,当需要操作系统服务时,它会执行系统调用指令,如 syscall
或 int 0x80
。
切换到内核态:执行系统调用指令会导致CPU从用户态切换到内核态。图中显示了通过 syscall
指令进行的切换。在切换过程中,CPU的特权级别(CPL)从3(用户态)变为0(内核态)。(下面呢,会重点讲!)
保存用户态上下文:在切换到内核态之前,用户态的上下文(如寄存器状态)会被保存,以便在系统调用完成后能够恢复。
执行系统调用服务:内核根据系统调用号找到相应的服务例程,并在内核态下执行。内核可以直接访问所有内存区域,包括用户态和内核态的内存。
返回用户态:系统调用完成后,内核会将控制权返回给用户程序,并将CPU从内核态切换回用户态。用户态的上下文会被恢复,程序继续执行。
结合图中的信息,我们可以得出以下结论:
-
系统调用的本质是地址空间的跳转:从用户地址空间跳转到内核地址空间,然后再跳转回来。
-
系统调用过程中涉及特权级别的切换:从用户态(CPL=3)切换到内核态(CPL=0),然后再切换回用户态。
-
内核态可以访问所有内存区域:内核态下的程序可以访问用户态和内核态的内存,而用户态下的程序只能访问自己的内存区域。
这种机制确保了操作系统的稳定性和安全性,防止用户程序直接访问或修改操作系统的核心数据结构。
到这,我们要知道:系统调用的过程,也是在进程地址空间上进行的!!!所有的系统调用,本质都是地址空间之间的跳转。
内核页表:引入用户态和内核态
整个进程地址空间:
【0GB,3GB】为用户空间
【3GB,4GB】为内核空间
用户在访问用户空间的时候,是不需要任何系统调用的:在用户自己的代码和数据当中,访问全局数据,堆区数据,栈数据,动态库数据,命令行参数和环境变量...只需要拿到虚拟地址就可以直接访问·【0GB,3GB】的所有数据;
曾经说过,当前用户的所有代码和数据,包括动静态库,栈区,堆区...最终都会通过页表(用户页表)映射到对应的物理空间上。
但也知道,操作系统也是软件,一定也在内存的!操作系统是属于内核的,也是需要虚拟地址到物理地址的映射的,所以我们要引入一个新概念:内核页表
从映射关系上,内核页表和用户页表在表结构上是没有区别的,核心工作也是没有区别的,但是在用户当前的进程当中,系统里加载进程可能加载了1个或多个,也就意味着,每一个进程都要有自己的【0GB,3GB】所对应的代码和数据,也就是一个进程,一个用户页表,换句话说:用户页表,会存在多份;
而内核页表:
内核页表的唯一性及其作用
内核页表是操作系统中一个核心的数据结构,它负责将内核虚拟地址空间映射到物理内存。由于内核是操作系统的核心,它需要在任何时候都能被访问,并且这种访问是不受当前运行进程影响的。因此,内核页表在整个系统中只有一份,这意味着所有进程在执行内核代码或访问内核数据时,都使用同一份页表来进行地址映射。这份页表确保了内核空间(【3GB, 4GB】)在所有进程中都是一致的,从而保证了内核代码和数据的全局性和一致性。
与内核页表不同,用户页表是为每个用户进程单独创建的,它们负责将每个进程的虚拟地址空间(【0GB, 3GB】)映射到物理内存。这种设计允许每个进程拥有自己的地址空间,从而实现了进程间的隔离。每个用户页表都是独立的,这意味着一个进程的内存布局和访问模式不会影响其他进程。
内核页表,系统中只有一份!所有进程共享!!! 意味着,无论进程如何调度,我们总能找到操作系统!!!
如果用户随便拿着一个虚拟地址【3GB,4GB】,用户不就可以随便访问内核中的代码和数据了吗?但是,我们之前不是说过:OS为了保护自己,是不相信任何人的,必须采用系统调用的方式进行访问!就连我们查看进程PID都需要采用getpid的系统调用,那这不就矛盾了吗?
所以为了能够既保证操作系统本身的安全性,又能保证让用户能够访问到操作系统,此时我们就引入了一种概念:用户态和内核态!
用户态:以用户身份,只能访问自己的【0GB,3GB】
内核态:以内核的身份,允许通过系统调用的方式,访问OS【3GB,4GB】
可是,在系统中,用户或者OS自己,怎么知道当前处于内核态,还是用户态?
其实要详细解释的话,是非常复杂的,怎么区分此时是用户态还是内核态,操作系统是做了很多工作的,我们简化来理解:在我们CPU内部,有些特定寄存器的位置是会记录下来,当前是用户态还是内核态的,这个寄存器通常是使用cs寄存器(代码段寄存器),一般是低两位比特位为1(也就是3),代表用户态,低两位比特位为0,代表内核态。在Linux内核里边,角色只有两种,分别对应CPU的数字是0和3,也就是说当前是处于用户态还是内核态是由硬件决定的,是CPU自己为我们提供了当前的系统所处的工作模式(用户·内核),cs寄存器是0,则是内核态,是3,则是用户态,所以CPU内就有相应的模式变化,当前正在访问自己的代码和数据时,对应cs段寄存器指向的段起始地址是指向用户的代码段,cs对应的后两位就被设置成11了,代表的就是用户态,在11的状态下,如果想访问【3GB,4GB】,拿着虚拟地址给CPU,CPU就要寻址,发现地址范围在【3GB,4GB】,而且发现cs寄存器的状态是11,CPU就会直接终止这个进程,终止的错误就是按照权限或越界直接拦截,走中断流程,终止进程了,所以CPU内部有对用标志位来记录当前是用户态还是内核态。
所以CPU内要提供一个指令集:int 0x80/syscall,这个指令集是做什么呢?凭什么说陷入内核了?
在计算机系统中,CPU通过代码段寄存器(CS)的低两位来区分当前是用户态(低两位为3)还是内核态(低两位为0)。当用户程序运行时,CS寄存器的低两位被设置为3,表示用户态,此时程序只能访问用户空间的内存区域(通常是0GB到3GB)。如果用户程序尝试访问内核空间(3GB到4GB),CPU会检测到权限越界并触发异常,导致进程终止。而当需要执行系统调用时,CPU会执行特定的指令(如int 0x80
或syscall:采用系统调用号+系统调用表,也是为什么叫做陷阱:陷入内核
)(而不允许直接用地址去访问),这些指令的作用是改变CS寄存器的值,将其低两位设置为0,从而触发从用户态到内核态的切换,允许程序访问内核空间并执行内核代码。这个切换过程是硬件层面的,由CPU自动完成,确保了系统的安全性和稳定性。
像这种0,3这样的权限位有专门的名词:称为当前特权级(Current Privilege Level,简称CPL):当前权限级别!
CPL
CPL记录在代码段寄存器(CS)的低两位中,用于标识当前执行代码的特权级别。在x86架构中,CPL为0时代表内核态,而CPL为3时代表用户态。当操作系统需要区分当前是用户态还是内核态时,它会检查CPL的值。如果CPL为0,则表示当前执行环境具有最高权限,可以访问所有资源,包括内核空间;如果CPL为3,则表示当前执行环境权限受限,只能访问用户空间资源。这种设计确保了操作系统的安全性,防止用户程序直接操作或访问内核数据,从而保护系统的核心功能和资源。
我们现在回过头想,当我们从键盘上按下ctrl➕c的时候,会以硬件中断的形式告诉操作系统,有键盘被按下了,识别到是终止前台任务,然后执行中断处理方法,我们信号产生,保存,处理就很明了了。
sigaction函数与信号屏蔽机制
除了signal用来自定义捕捉,还有一个系统调用可以实现自定义捕捉:
sigaction
是Linux系统中用于处理信号的函数。它通过操作sigaction
结构体,可以读取和修改指定信号的处理动作。通过设置结构体中的字段,可以指定信号的处理函数、是否忽略信号、是否执行默认动作,以及在信号处理函数执行期间需要额外屏蔽的信号等。它比signal
函数更强大、更灵活,能更好地控制信号的处理行为。
在Linux系统编程中,信号是一种重要的进程间通信机制,而sigaction
函数则是用于读取和修改与指定信号相关联的处理动作的核心工具。它通过操作sigaction
结构体,实现了对信号处理方式的精细控制,其中信号屏蔽机制是其关键特性之一。
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <cstdlib>
void handler(int signum)
{
std::cout << "hello signal: " << signum << std::endl;
while (true)
{
// 不断获取pending表!
sigset_t pending;
sigpending(&pending);
for (int i = 31; i >= 1; i--)
{
if (sigismember(&pending, i))
std::cout << "1";
else
std::cout << "0";
}
std::cout << std::endl;
sleep(1);
}
exit(0);
}
int main()
{
struct sigaction act, oact;
act.sa_handler = handler;
sigemptyset(&act.sa_mask);
// sigaddset(&act.sa_mask, 3);
// sigaddset(&act.sa_mask, 4);
act.sa_flags = 0;
sigaction(SIGINT, &act, &oact); // 对2号信号进行了捕捉, 2,3,4都屏蔽
while (true)
{
std::cout << "hello world: " << getpid() << std::endl;
sleep(1);
}
return 0;
}
sigaction
结构体中的sa_mask
字段是实现信号屏蔽的关键。当一个信号的处理函数被调用时,内核会自动将当前信号加入进程的信号屏蔽字(屏蔽字就是设置block表的对应位置置1),这意味着在信号处理函数执行期间,该信号会被阻塞,直到信号处理函数返回后,内核才会自动恢复原来的信号屏蔽字。这种机制确保了在处理某个信号时,如果该信号再次产生,它会被阻塞,直到当前处理完成。这避免了信号处理函数被重复调用,从而防止可能的混乱和错误:
然而,sa_mask
字段的作用不仅如此。除了自动屏蔽当前信号外,我们还可以通过sa_mask
字段指定其他需要在信号处理函数执行期间被屏蔽的信号。这为我们提供了更大的灵活性,使得我们可以根据实际需求,控制在信号处理过程中哪些信号应该被阻塞。当信号处理函数返回时,内核会自动恢复原来的信号屏蔽字,从而保证了信号屏蔽的临时性和可恢复性:(对sa_mask设置3和4号信号)
这种信号屏蔽机制与pending
表和block
表密切相关。pending
表用于记录当前进程已经产生但尚未处理的信号,而block
表则记录了当前进程被屏蔽的信号。当一个信号产生时,如果该信号不在block
表中,它会被立即处理;如果在block
表中,则会被加入到pending
表中,等待被解屏蔽后再处理。通过sigaction
函数对sa_mask
字段的操作,我们实际上是在动态地修改block
表的内容,从而影响信号的处理顺序和时机。
例如,假设我们有一个自定义的信号处理函数handler
,用于处理SIGINT
信号(通常由Ctrl+C产生)。我们希望在处理SIGINT
信号时,同时屏蔽SIGQUIT
信号,以避免在处理SIGINT
信号的过程中被SIGQUIT
信号打断。我们可以通过设置sa_mask
字段来实现这一点。在信号处理函数handler
执行期间,SIGINT
信号和SIGQUIT
信号都会被加入到block
表中,即使SIGQUIT
信号在此期间产生,它也会被加入到pending
表中,等待handler
函数执行完毕后,内核会恢复原来的信号屏蔽字,此时SIGQUIT
信号才会从pending
表中取出并进行处理。
通过sigaction
函数对信号屏蔽机制的灵活运用,我们可以更好地控制信号的处理流程,避免信号处理过程中的冲突和混乱,从而提高程序的可靠性和稳定性。
对于信号的话题,我们到这就有了深刻的认识与理解,接下来,我们来谈谈几个子概念。
可重入函数
在多线程编程中,函数的可重入性是一个重要的考虑因素。可重入函数是指可以在任何时刻被中断,并在之后安全地重新进入的函数。不可重入函数则可能导致数据竞争和不一致的问题。
示例分析
以下是一个典型的不可重入函数的示例:
在这个例子中,main
函数调用 insert
函数向一个链表 head
中插入节点 node1
。插入操作分为两步,刚做完第一步的时候,因为硬件中断使进程切换到内核,再次回用户态之前检查到有信号待处理,于是切换到 sighandler
函数。sighandler
也调用 insert
函数向同一个链表 head
中插入节点 node2
。插入操作的两步都做完之后从 sighandler
返回内核态,再次回到用户态就从 main
函数调用的 insert
函数中继续往下执行,先前做第一步之后被打断,现在继续做完第二步。结果是,main
函数和 sighandler
先后向链表中插入两个节点,而最后只有一个节点真正插入链表中了。这就造成了node2节点的丢失,造成内存泄漏!
可重入性问题
像上例这样,insert
函数被不同的控制流程调用(main执行流和handler执行流),有可能在第一次调用还没返回时就再次进入该函数,这称为重入。insert
函数访问一个全局链表,有可能因为重入而造成错乱,像这样的函数称为不可重入函数(会带来问题)。反之,如果一个函数只访问自己的局部变量或参数,则称为可重入(Reentrant)函数。(不会带来问题)
大部分的函数都是不可重入的,这是由于许多函数会访问或修改共享资源,如全局变量、文件描述符或内存分配器,因此它们通常是不可重入的,这在多线程或中断处理程序中可能导致数据竞争和不一致性。
我们如何来快速判断一个函数是否为可重入函数,是否为不可重入函数?
如果一个函数符合以下条件之一则是不可重入的:
-
调用了
malloc
或free
,因为malloc
也是用全局链表来管理堆的。 -
调用了标准 I/O 库函数。标准 I/O 库的很多实现都以不可重入的方式使用全局数据结构。
为了确保程序的可靠性和稳定性,我们需要尽量避免使用不可重入函数,或者采取措施来保证函数的可重入性。
那么像函数中只有自己的临时变量,这种大概率是可重入函数的。
上面的示例就是因为链表是全局的,是共享的,是可以访问的公共资源!
多线程再谈~~~
volatile 关键字
在C语言中,volatile
关键字用于声明一个变量的值可能会被外部事件(如硬件中断或信号处理函数)改变,因此编译器不能对其进行优化。volatile
告诉编译器,该变量可能会随时改变,因此每次使用该变量时都必须从内存中重新读取,而不是使用寄存器中的缓存值。
示例代码
以下是一个使用 volatile
关键字的示例代码:
#include <stdio.h>
#include <signal.h>
volatile int flag = 0;
void handler(int sig)
{
std::cout << "修改全局变量, " << flag << " -> 1" << std::endl;
flag = 1;
}
int main()
{
signal(2, handler);
while (!flag)
;
printf("process quit normal\n");
return 0;
}
在这个示例中,flag
变量被声明为 volatile
。当信号处理函数 handler
被调用时,它会改变 flag
的值。在 main
函数中,while(!flag)
循环会一直等待,直到 flag
的值变为 1。由于 flag
是 volatile
的,编译器不会对其进行优化,从而确保每次循环都会检查 flag
的最新值。
优化问题
在没有使用 volatile
的情况下,编译器可能会对 while(!flag)
循环进行优化,导致程序无法正确响应信号。这是因为编译器可能会将 flag
的值缓存在寄存器中,而不是每次都从内存中读取。这样,即使 handler
函数改变了 flag
的值,main
函数也无法感知到。(main里面,并不会对flag进行修改)(寄存器覆盖了进程看到变量的真实情况,内存不可见了)
使用 volatile
的原因
使用 volatile
的主要原因是确保程序在多线程或中断驱动的环境中能够正确地响应外部事件。通过将变量声明为 volatile
,可以防止编译器对其进行优化,从而确保每次访问该变量时都能获取到最新的值。
示例分析
如果不使用volatile,而且使用优化过的编译器编译:使用O1优化:
g++ test.cpp -O1
在示例代码中,flag
变量被声明为 volatile
,以确保在信号处理函数 handler
修改 flag
的值后,main
函数能够正确地感知到这一变化。如果没有使用 volatile
,编译器可能会将 flag
的值缓存在寄存器中,导致 main
函数无法正确地响应信号:
通过使用 volatile
关键字,可以确保程序在多线程或中断驱动的环境中能够正确地响应外部事件,从而提高程序的可靠性和稳定性。
SIGCHLD信号
我们目前也学习到7,8个信号了,这其实也是够用了,但是SIGCHLD信号和我们之前的只是有关联,我们再来多认识一个:
在进程管理中,正确处理子进程的结束是至关重要的。SIGCHLD
信号(17号)为父进程提供了一种机制来响应子进程的结束。通过使用 wait
和 waitpid
函数,父进程可以阻塞等待子进程结束,也可以非阻塞地查询子进程的状态,但是代码相对写起来就比较复杂。(解决僵尸进程)此外,父进程还可以定义自己的信号处理函数来专门处理子进程的结束,这就是SIGCHLD信号的作用。
其实,子进程在终止时会给父进程发SIGCHLD信号,该信号的默认处理动作是忽略,父进程可以自定义SIGCHLD信号的处理函数,这样父进程只需专心处理自己的工作,不必关心子进程了,子进程终止时会通知父进程,父进程在信号处理函数中调用wait清理子进程即可。
轮询处理方式
父进程可以在处理自己的工作的同时,不时地轮询检查子进程是否结束。这种方式虽然可以实现功能,但效率较低,因为它需要不断地检查子进程的状态。
使用信号处理函数
父进程可以定义一个信号处理函数来专门处理 SIGCHLD
信号。这样,当子进程结束时,父进程会在信号处理函数中被通知,并调用 wait
清理子进程。这种方式可以让父进程专注于自己的工作,而不必关心子进程的结束。
我们可以验证一下子进程退出的时候确实会向父进程发送信号:
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <cstdlib>
#include <sys/wait.h>
void Say(int num)
{
std::cout << "fathet get a signal: " << num << std::endl;
}
int main()
{
// 父进程
signal(SIGCHLD, Say); // 父进程
pid_t id = fork(); // 如果我们有10个子进程呢??6退出了,4个没退
if (id == 0)
{
sleep(3);
std::cout << "I am child, exit" << std::endl;
exit(3);
}
waitpid(id, nullptr, 0);
std::cout << "I am fater, exit" << std::endl;
sleep(1);
return 0;
}
示例代码
以下是一个示例程序,展示了如何使用 SIGCHLD
信号和信号处理函数来处理子进程的结束:
将进程的等待工作放在信号捕捉函数当中:-1表示任意一个子进程,waitpid是默认阻塞的,使用WNOHANG来实现非阻塞轮询。
#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(3);
}
while(1) {
printf("father proc is doing some thing!\n");
sleep(1);
}
return 0;
}
这段代码演示了如何使用 SIGCHLD
信号和非阻塞轮询来回收多个子进程。在父进程中,通过 signal(SIGCHLD, handler);
注册了一个信号处理函数 handler
来处理 SIGCHLD
信号,该信号在每个子进程结束时由操作系统发送给父进程。
在 handler
函数中,使用 waitpid(-1, NULL, WNOHANG)
进行非阻塞轮询。waitpid
函数的 WNOHANG
选项使得调用不会阻塞,即使没有子进程结束也会立即返回。如果 waitpid
成功回收了一个子进程(即返回值大于0),则打印一条消息并继续检查是否有其他子进程需要回收。如果没有子进程结束,则 waitpid
返回0,信号处理函数结束执行。
如果采用阻塞轮询(即不使用 WNOHANG
选项),waitpid
会阻塞直到一个子进程结束,这会导致父进程在等待期间无法执行其他任务。这种阻塞方式在只有一个子进程时问题不大,但如果父进程需要同时管理多个子进程,或者需要同时执行其他任务,阻塞轮询就会效率低下,并且可能造成资源浪费。
通过使用非阻塞轮询,父进程可以在处理自己的工作的同时,有效地回收结束的子进程,而不会因等待子进程结束而被阻塞。这种方式提高了程序的响应性和效率,允许父进程在子进程运行期间继续执行其他重要任务。
那不是一个子进程退出了,不就会发送信号给父进程,那这个while有什么用?
确实,当一个子进程退出时,操作系统会向其父进程发送 SIGCHLD
信号。然而,while
循环在 handler
函数中的作用并不是等待子进程退出,而是确保在信号处理函数被触发时,能够一次性回收所有已经结束的子进程。
在多子进程环境中,可能会发生以下情况:
-
多个子进程同时结束:如果有多个子进程几乎同时结束,操作系统可能会一次性发送一个
SIGCHLD
信号给父进程,而不是为每个结束的子进程发送一个信号。 -
信号处理函数只被调用一次:即使有多个子进程结束,
SIGCHLD
信号的默认行为是只触发一次信号处理函数。这意味着,如果不在信号处理函数中检查并回收所有已结束的子进程,可能会遗留一些未被回收的子进程(僵尸进程)。 -
非阻塞回收:
waitpid(-1, NULL, WNOHANG)
是一个非阻塞调用,它尝试回收一个已结束的子进程,但如果没有任何子进程结束,则立即返回0。通过在while
循环中使用这个调用,可以连续检查并回收所有已结束的子进程,直到没有更多的子进程可以回收为止。
因此,while
循环的作用是确保在信号处理函数被触发时,能够一次性清理所有已结束的子进程,避免遗漏。这样可以有效地管理子进程的生命周期,防止僵尸进程的产生,同时确保父进程可以继续执行其任务而不受干扰。
总结来说,while
循环在 handler
函数中的作用是确保在 SIGCHLD
信号触发时,能够一次性回收所有已经结束的子进程,从而避免僵尸进程的产生,并提高程序的健壮性和效率。
上面的做法是比较优雅的了,但是,我们还有更好的办法:(缺点:不回收子进程的退出信息)
事实上,由于UNIX系统的历史原因,存在一种方法可以避免产生僵尸进程:父进程可以通过调用sigaction/signal
函数将SIGCHLD
信号的处理动作设置为SIG_IGN
。这样,当使用fork
创建的子进程终止时,系统会自动进行清理,不会产生僵尸进程,同时也不会通知父进程。需要注意的是,这种方法在Linux系统中是可行的,但在其他UNIX系统上可能不适用。为了验证这种方法的有效性,可以通过编写程序来进行测试,确保在子进程终止后不会产生僵尸进程。
int main()
{
// 父进程
signal(SIGCHLD, SIG_IGN); // 使用SIG_IGN
pid_t id = fork();
if (id == 0)
{
sleep(3);
std::cout << "I am child, exit" << std::endl;
exit(3);
}
waitpid(id, nullptr, 0);
std::cout << "I am fater, exit" << std::endl;
sleep(1);
return 0;
}
通常情况下,系统默认的忽略动作和用户通过sigaction/signal
函数自定义的忽略动作是没有区别的,但在这个特定的情况下,它们的行为有所不同。这是为什么:(系统的会带来僵尸问题,显示传却不会)?
通常情况下,系统默认的忽略动作(SIG_IGN
)和用户通过 sigaction
或 signal
函数自定义的忽略动作(SIG_IGN
)在大多数信号上是没有区别的。然而,对于 SIGCHLD
信号,确实存在一些特殊的行为。
对于 SIGCHLD
信号:
-
默认动作(
SIG_DFL
):当子进程停止或结束时,父进程会收到SIGCHLD
信号。默认情况下,如果父进程没有明确指定如何处理SIGCHLD
信号(即没有设置为SIG_IGN
或指定一个处理函数),父进程会采取默认动作SIG_DFL
。在大多数系统中,SIG_DFL
对于SIGCHLD
信号的默认行为是忽略该信号,即不进行任何处理。 -
自定义忽略动作(通过
sigaction
或signal
设置为SIG_IGN
):如果父进程显式地将SIGCHLD
信号的处理方式设置为SIG_IGN
,那么父进程将完全忽略该信号。这意味着当子进程结束时,父进程不会收到任何通知,也不会采取任何行动。
在大多数系统中,无论是默认的忽略(SIG_DFL
)还是用户自定义的忽略(SIG_IGN
),SIGCHLD
信号都会被忽略,父进程不会收到通知。然而,Linux 系统在这里有一个特殊的行为:当父进程显式地将 SIGCHLD
信号的处理方式设置为 SIG_IGN
时,系统会自动回收子进程的资源,从而避免产生僵尸进程。
这种行为的特殊之处在于,它允许父进程在不关心子进程具体何时结束的情况下,避免产生僵尸进程。这是通过显式设置 SIGCHLD
信号的处理方式为 SIG_IGN
来实现的,而不是依赖于默认的 SIG_DFL
行为。
总结来说,虽然在大多数情况下,系统默认的忽略动作和用户自定义的忽略动作没有区别,但对于 SIGCHLD
信号,Linux 系统提供了一个特殊的机制,允许父进程通过显式设置 SIG_IGN
来自动回收子进程资源,避免僵尸进程的产生。
注意事项
-
系统默认的忽略动作和用户用
sigaction
函数自定义的忽略通常是没有区别的,但这是一个特例。此方法对于 Linux 可用,但不保证在其它 UNIX 系统上都可用。 -
编写程序验证这样不会产生僵尸进程。
通过使用 SIGCHLD
信号和信号处理函数,可以更高效地管理子进程的结束,避免僵尸进程的产生。