目录
信号的概念
在Linux中,信号是一种软件中断,用于通知进程发生了某个事件。这些事件可能是用户请求(如按下Ctrl+C),也可能是内核通知(如进程访问了无效的内存地址)。信号提供了一种机制,使得进程能够响应这些事件,并采取适当的行动。
信号的递达与未决
一、信号的递达
信号的递达是指进程实际执行信号的处理动作。当信号被发送给一个进程后,操作系统会进行相应的处理,确保信号能够准确地传递给目标进程。一旦信号被递达给进程,进程就会根据信号的类型和设定的处理方式执行相应的动作。
在信号的递达过程中,进程可以自定义信号的处理函数,用于执行特定的逻辑。当信号递达时,进程会调用相应的处理函数,并执行其中的代码。这样,进程可以根据不同的信号进行不同的处理,以满足不同的需求。
二、信号的未决
信号的未决状态是指信号从产生到递达之间的状态。当一个信号被发送给一个进程后,该信号首先会处于未决状态。这意味着信号已经产生,但还没有被进程实际处理。
在信号的未决状态中,操作系统会维护一个信号未决表,用于记录当前进程尚未处理的信号。这个表记录了哪些信号已经产生但尚未递达给进程。进程可以通过检查信号未决表来了解当前有哪些信号等待处理。
进程的信号未决状态可以被阻塞或解除阻塞。当进程选择阻塞某个信号时,该信号将保持在未决状态,不会被递达给进程。只有当进程解除对该信号的阻塞时,信号才会从未决状态转变为递达状态,进而执行相应的处理动作。
通过信号的递达和未决机制,操作系统能够灵活地管理进程对信号的处理。进程可以根据需要自定义信号的处理方式,并在合适的时机执行相应的处理动作。这种机制为进程间的通信和异常处理提供了强大的支持。
生活中的信号
-
闹钟信号:假设你设定了一个闹钟,在早上7点响铃。这个闹钟响铃就是一个信号,通知你该起床了。在Linux中,这可以类比为一个进程设定了一个定时器(如使用
setitimer
系统调用),当定时器到期时,内核会向该进程发送一个信号(如SIGALRM
),通知它定时器已经到期。 -
电话铃声:当你正在做其他事情时,电话铃声响起,这是一个信号,告诉你有人正在呼叫你。你可以选择接听电话、挂断电话或忽略它。在Linux中,当一个进程接收到一个信号时,它可以选择忽略该信号、执行默认操作或自定义一个信号处理函数来处理该信号。
-
交通信号灯:交通信号灯是道路上的重要信号,告诉驾驶员何时可以前进、何时需要停止或等待。如果驾驶员违反信号灯指示,可能会引发交通事故。类似地,在Linux中,如果进程不正确地处理信号(如忽略重要的信号),可能会导致不可预期的行为或程序崩溃。
-
敲门声:当你正在房间里工作时,突然听到敲门声,这是一个信号,告诉你有人想要和你交流。你可以继续工作不理会敲门声,也可以停下来去开门。在Linux中,进程可以选择忽略某些信号(如
SIGCHLD
,当子进程状态改变时发送),也可以对它们进行特殊处理。
Linux中的信号
Linux中的信号是一种进程间通信(Inter-Process Communication, IPC)机制,用于向其他进程发送通知或指示,通常是为了通知特定事件的发生,如程序终止、用户按下特定按键等。信号提供了一种异步的、非阻塞的通信方式,因此在某些情况下非常有用。
信号全称为软中断信号,也有人称作软中断。信号机制是进程之间相互传递消息的一种方法。进程之间可以互相通过系统调用kill发送软中断信号,通知进程发生了某个事件。内核也可以因为内部事件而给进程发送信号,通知进程发生了某个事件。例如,当中断用户键入中断键(CTRL+C)时,会通过信号机制停止一个程序。
Linux系统定义了64种信号,这些信号可以分为两类:可靠信号与不可靠信号。前32种信号为不可靠信号,也称为非实时信号,它们不支持排队,信号可能会丢失。比如,发送多次相同的信号,进程只能收到一次。而后32种信号为可靠信号,也称为实时信号,它们支持排队,信号不会丢失,发多少次就可以收到多少次。
每种信号都有其特定的用途和处理方式。例如,SIGTERM信号用于请求终止进程,它可以被捕获或忽略;SIGQUIT信号请求进程退出并产生core文件,同样可以被捕获或忽略;而SIGSTOP信号会暂停进程的执行,并且不能被捕获或忽略。
在Linux中,进程可以选择忽略某些信号,也可以自定义信号处理函数来处理特定信号。当进程接收到一个信号时,它会根据信号的类型和当前的状态来决定如何响应。如果进程没有为某个信号指定处理函数,那么它会执行该信号的默认操作。
总的来说,Linux中的信号机制为进程间的通信和同步提供了强大的支持,使得进程能够灵活地响应各种事件和条件。通过合理地使用和处理信号,我们可以编写出更加健壮和高效的程序。
kill命令
kill通常,
kill 命令用于终止进程,但实际上它可以发送任何有效的信号给进程。默认情况下,
kill命令发送
SIGTERM信号,该信号会请求进程优雅地终止。如果进程没有响应
SIGTERM信号,可以使用
kill -9命令发送
SIGKILL` 信号强制终止进程。
kill 命令的使用
kill
命令的基本语法如下:
kill [选项] <进程ID> [进程ID ...]
或者:
kill -s <信号> <进程ID>
其中,<进程ID>
是你想要发送信号的进程的进程ID,<信号>
是你想要发送的信号名或信号编号。
常见的信号
-
SIGTERM
(默认信号,编号 15):请求进程终止。进程在接收到此信号后,可以执行清理操作并退出。 -
SIGKILL
(编号 9):强制终止进程。进程在接收到此信号后不能捕获或忽略它,会立即停止执行。 -
SIGINT
(编号 2):通常由 Ctrl+C 产生,请求进程中断。 -
SIGHUP
(编号 1):当控制终端挂起时,通知进程。
命令行代码示例
-
发送 SIGTERM 信号终止进程
如果你知道进程的ID,比如 1234
,你可以使用以下命令来发送 SIGTERM
信号:
kill 1234
如果进程没有响应,你可以使用 kill -l
来查看所有可用的信号列表,并决定发送哪种信号。
-
发送 SIGKILL 信号强制终止进程
如果 SIGTERM
没有效果,你可以发送 SIGKILL
信号来强制终止进程:
kill -9 1234
这条命令会强制终止进程ID为 1234
的进程。
-
发送特定信号
如果你想发送一个特定的信号,比如 SIGHUP
,你可以使用 -s
选项:
kill -s SIGHUP 1234
或者你也可以使用信号的编号:
kill -1 1234
这条命令会向进程ID为 1234
的进程发送 SIGHUP
信号。
-
发送信号给多个进程
你可以一次向多个进程发送信号:
kill 1234 5678
这条命令会向进程ID为 1234
和 5678
的进程发送 SIGTERM
信号。 5. 查看信号列表 你可以查看信号列表
kill -l
注意事项
-
在使用
kill -9
之前,请确保你已经尝试了SIGTERM
信号,因为SIGKILL
会立即停止进程,不给进程任何清理的机会。 -
不是所有的进程都可以被
kill
命令终止。有些系统进程或守护进程可能设计为忽略某些信号。 -
错误的
kill
命令可能会导致系统不稳定或数据丢失,所以请确保你知道正在终止什么进程,以及这个操作可能带来的后果。
kill
命令是一个非常强大的工具,但也需要谨慎使用。在发送信号之前,最好先了解进程的用途和重要性,以避免不必要的系统干扰或数据损失。
信号的处理方式
在Linux中,常见的信号处理方法主要有以下几种:
-
忽略信号:当进程接收到某个信号时,可以选择忽略它,即不执行任何操作。这通常用于处理那些对进程来说不重要的信号。
-
执行默认处理动作:对于每个信号,系统都定义了一个默认的处理动作。当进程接收到信号且没有自定义处理函数时,就会执行这个默认动作。例如,对于
SIGTERM
信号,默认的处理动作是终止进程。 -
自定义信号处理函数:进程可以注册一个信号处理函数,当接收到特定信号时,内核会调用这个函数来处理信号。这种方式允许进程根据信号的类型和当前的状态来执行特定的操作,如清理资源、记录日志等。通过自定义信号处理函数,进程可以更加灵活地响应各种事件和条件。
需要注意的是,并非所有的信号都可以被忽略或自定义处理。例如,SIGKILL
和SIGSTOP
等信号是不能被忽略或捕获的,它们会强制终止进程或暂停进程的执行。
产生信号
在Linux中,信号的产生主要有以下几种方式:
-
通过键盘产生:
-
用户可以通过特定的按键组合来产生信号。例如,按下Ctrl+C会产生SIGINT信号,通常用于终止前台进程。这种信号产生方式实际上是操作系统捕获到键盘上的按键组合,并将其解析成相应的信号,然后发送给目标进程。
-
-
通过系统调用产生:
- Linux提供了多个系统调用来产生信号,其中最常见的是
kill
和raise
函数。-
kill
函数允许一个进程向另一个进程发送信号。通过指定目标进程的进程ID和要发送的信号,可以实现对目标进程的控制,如终止进程、暂停进程等。 -
raise
函数则允许一个进程向自身发送信号,常用于实现进程内部的信号处理逻辑。
-
- Linux提供了多个系统调用来产生信号,其中最常见的是
-
由软件条件产生:
-
当某些软件条件满足时,操作系统会自动向进程发送信号。例如,当进程访问了非法内存地址时,会产生SIGSEGV信号;当进程执行了除零操作时,会产生SIGFPE信号。此外,定时器到期、I/O操作完成等事件也会触发相应的信号。
-
-
由硬件异常产生:
-
当硬件发生异常时,如CPU异常、总线错误等,操作系统会向相关进程发送信号。这些信号通常用于通知进程硬件层面的错误,以便进程能够采取相应的处理措施。
-
信号的捕捉
信号捕捉示意图
内核如何实现信号捕捉
在Linux内核中,信号捕捉的实现涉及多个组件和步骤。信号捕捉允许进程在接收到信号时执行自定义的处理逻辑,而不是采用默认的信号处理行为。以下是信号捕捉在内核中的实现细节的详细介绍:
-
信号注册与信号处理器设置:
-
当进程通过
signal()
或sigaction()
系统调用设置信号处理器时,内核会记录该进程的信号处理器信息。 -
内核维护了一个数据结构(如
task_struct
中的字段),该结构包含指向每个信号对应的处理器函数的指针。
-
-
信号产生:
-
信号可以由多种原因产生,如用户按下Ctrl+C(产生SIGINT信号),或者内核检测到某种异常条件(如段错误产生SIGSEGV信号)。
-
当信号产生时,内核会更新目标进程的信号状态,将其标记为未决状态。
-
-
信号传递:
-
内核定期检查进程的信号状态,并尝试将未决信号传递给进程。
-
在信号传递之前,内核会检查进程是否阻塞了该信号。如果信号被阻塞,则保持未决状态;否则,进入信号处理流程。
-
-
信号处理:
-
内核调用进程为该信号设置的处理器函数(也称为信号处理程序)。
-
信号处理程序可以在用户空间执行,这意味着当处理程序被调用时,内核会切换到用户模式并执行处理程序中的代码。
-
信号处理程序可以执行任何合法的用户空间代码,包括改变程序的控制流、修改变量值或执行其他任务。
-
-
信号恢复:
-
信号处理程序执行完毕后,控制流返回到内核,内核负责恢复进程的上下文并继续执行。
-
内核还负责清理信号处理过程中的任何临时状态,确保系统的稳定性。
-
-
信号与阻塞:
-
进程可以使用
sigprocmask()
等系统调用来修改其信号屏蔽字(即阻塞的信号集)。 -
当进程阻塞某个信号时,该信号即使产生也不会被立即处理,而是保持未决状态,直到进程解除对该信号的阻塞。
-
-
实时信号与排队:
-
Linux支持实时信号,这些信号与普通信号的主要区别在于它们可以排队等待处理。
-
对于实时信号,内核会为每个信号维护一个队列,允许多个相同的实时信号被同时接收并排队等待处理。
-
-
安全性与可靠性:
-
内核在信号处理过程中要确保系统的安全性和可靠性。
-
例如,内核会检查信号处理程序的有效性,防止恶意代码的执行。
-
内核还会处理信号处理过程中的异常情况,如处理程序中的错误或异常退出。
-
信号的捕捉与处理
一、信号的捕捉
在Linux中,信号的捕捉是指进程在接收到信号时,不是按照信号的默认行为来处理,而是执行自定义的信号处理函数。这样,进程就可以根据信号的种类和当前的状态来决定如何响应信号。
二、使用sigaction设置信号处理函数
sigaction
函数提供了更丰富的功能,并且更加安全,因此在实际应用中更为常用。下面将介绍如何使用sigaction
函数来设置信号处理函数。
三、捕捉空指针异常(SIGSEGV)的示例
下面以捕捉空指针异常(即SIGSEGV信号)为例,展示如何使用sigaction
函数来设置信号处理函数。
3.1 定义信号处理函数
首先,需要定义一个信号处理函数,该函数将在接收到信号时被调用。在这个例子中,我们定义了一个名为handle_segv
的函数,用于处理SIGSEGV信号。
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <string.h>
#include <execinfo.h>
void handle_segv(int sig, siginfo_t *info, void *context) {
// 处理SIGSEGV信号的逻辑,如打印调用栈信息
}
3.2 设置信号处理函数
接下来,在程序的主函数中,使用sigaction
函数来设置SIGSEGV信号的处理函数。
int main() {
struct sigaction sa;
memset(&sa, 0, sizeof(sa));
sa.sa_sigaction = handle_segv;
sa.sa_flags = SA_SIGINFO;
if (sigaction(SIGSEGV, &sa, NULL) == -1) {
// 处理设置信号处理函数失败的情况
}
// 程序的正常逻辑,可能会触发SIGSEGV信号
return 0;
}
在上述代码中,sigaction
函数用于设置SIGSEGV信号的处理函数为handle_segv
。当程序因为解引用空指针而触发SIGSEGV信号时,会调用handle_segv
函数来处理该信号。
3.3 触发空指针异常并处理
在程序的正常逻辑中,可以故意触发一个空指针异常来测试信号捕捉的处理逻辑。例如,通过解引用一个空指针来触发SIGSEGV。
// 在程序的正常逻辑中触发空指针异常
int *ptr = NULL;
*ptr = 1; // 这将导致SIGSEGV信号
当程序执行到这一行时,会触发SIGSEGV信号,并调用之前设置的handle_segv
函数来处理该信号。在handle_segv
函数中,可以执行自定义的处理逻辑,如打印调用栈信息、记录错误日志等。
请注意,在实际的生产环境中,对于空指针异常,通常更好的做法是修复程序中的缺陷,避免解引用空指针的情况发生,而不是尝试捕捉并恢复。
小结
-
所有的信号都由操作系统来产生,因为操作系统是进程的管理者
-
信号不是立即处理的,而是在适合的情况进行处理
阻塞信号
信号在内核中的表示图
信号在Linux内核中的表示涉及到信号的三种状态:抵达态、未决态和阻塞态(也称为屏蔽态)。
-
抵达态:这是执行信号的处理动作的状态。也就是说,当信号的处理动作被执行时,信号就处于抵达态。抵达态下的信号处理方式有三种:忽略、默认或执行自定义函数。
-
未决态:这是从信号的产生到信号抵达之间的状态。当信号被发送,但还未被进程处理时,信号就处于未决态。这种状态下的信号在内核中会被记录,等待处理。
-
阻塞态(屏蔽态):这是用户可以手动设置的状态。当信号处于阻塞态时,即使信号已经产生,也不会被立即处理,而是保持未决状态。只有当用户解除了对该信号的阻塞(屏蔽)后,信号才会转变为抵达状态,执行相应的处理动作。
在内核中,这三种状态通常使用结构体来存储,每种状态对应一个结构体。这些结构体内部包含足够多的比特位,每一个比特位对应一个信号值,以此来记录和表示每个信号的状态。
另外,Linux中的每个进程都拥有两个位向量:pending位向量和blocked位向量。pending位向量包含了那些内核发送给进程但还没有被进程处理掉的信号,而blocked位向量则包含了那些被进程屏蔽掉的信号。这两个位向量共同决定了进程将如何处理信号。
在信号处理机制中,当内核发送一个信号给进程时,它会修改进程的pending位向量。同样地,当进程屏蔽掉一个信号时,它会修改blocked位向量。只有当进程解除了对某个信号的屏蔽后,如果pending位向量中对应该信号的值被设置为1(表示该信号已产生且未处理),那么该信号才会被递达给进程进行处理。
信号集操作函数
在Linux中,信号集操作函数是用于操作信号集的一系列函数。信号集是一个包含多个信号的数据结构,用于表示一组信号。这些函数提供了添加、删除、测试信号集中信号的功能,以及用于阻塞和解除阻塞信号的功能。
下面是对这些函数的详细介绍和简单的示范代码:
-
sigemptyset
sigemptyset
函数用于初始化一个信号集,将其所有位都设置为0,即清空信号集。
#include <signal.h>
int sigemptyset(sigset_t *set);
示例:
sigset_t emptyset;
sigemptyset(&emptyset); // 将emptyset初始化为空信号集
-
sigfillset
sigfillset
函数用于将信号集中的所有位都设置为1,即包含所有可能的信号。
#include <signal.h>
int sigfillset(sigset_t *set);
示例:
sigset_t fullset;
sigfillset(&fullset); // 将fullset初始化为包含所有信号的信号集
-
sigaddset
sigaddset
函数用于将一个指定的信号添加到信号集中。
#include <signal.h>
int sigaddset(sigset_t *set, int signum);
示例:
sigset_t myset;
sigemptyset(&myset);
sigaddset(&myset, SIGINT); // 将SIGINT信号添加到myset信号集中
-
sigdelset
sigdelset
函数用于从信号集中删除一个指定的信号。
#include <signal.h>
int sigdelset(sigset_t *set, int signum);
示例:
sigset_t myset;
sigfillset(&myset);
sigdelset(&myset, SIGINT); // 从myset信号集中删除SIGINT信号
-
sigismember
sigismember
函数用于检查一个指定的信号是否存在于信号集中。
#include <signal.h>
int sigismember(const sigset_t *set, int signum);
示例:
sigset_t myset;
sigaddset(&myset, SIGINT);
if (sigismember(&myset, SIGINT)) {
// SIGINT存在于myset信号集中
}
-
sigprocmask
sigprocmask
函数用于检查和/或更改当前进程的信号屏蔽字(即阻塞的信号集)。
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
-
how
参数决定了如何更改信号屏蔽字:SIG_BLOCK
(添加信号到当前阻塞集)、SIG_UNBLOCK
(从当前阻塞集中移除信号)或SIG_SETMASK
(设置当前阻塞集为指定的信号集)。 -
set
参数指向包含新信号集的指针。 -
oldset
参数指向存储当前信号屏蔽字的指针,或者为NULL。
示例:
sigset_t blockset, oldset;
sigemptyset(&blockset);
sigaddset(&blockset, SIGINT);
sigprocmask(SIG_BLOCK, &blockset, &oldset); // 阻塞SIGINT信号
-
sigpending
sigpending
函数用于获取当前进程被阻塞的、但尚未处理的信号集。
#include <signal.h>
int sigpending(sigset_t *set);
示例:
sigset_t pendingset;
sigpending(&pendingset); // 获取当前进程的pending信号集
注意:在使用信号集操作函数时,需要包含头文件<signal.h>
。同时,不同的操作可能需要不同的权限,因此在某些情况下可能需要以root权限运行程序。
可重入函数
可重入函数示意图
可重入函数概念
可重入函数(Reentrant Function)是指在执行过程中,可以被多个线程同时调用的函数,且每个线程调用该函数时,都会得到正确的结果,不会因为其他线程的影响而导致数据混乱或错误。在多线程编程中,使用可重入函数是非常重要的,以确保线程安全。
可重入函数的使用
为了确保函数是可重入的,通常需要注意以下几点:
-
避免使用全局变量:全局变量在多线程环境中可能导致数据竞争和不一致的结果。
-
避免使用静态局部变量:静态局部变量在函数调用之间保持其值,这可能导致线程间的数据混淆。
-
避免调用不可重入的函数:如果函数调用了其他不可重入的函数,那么该函数本身也将是不可重入的。
-
保护共享资源:如果函数需要访问共享资源(如文件、数据库等),应使用适当的同步机制(如互斥锁)来确保同时只有一个线程可以访问。
下面是一个简单的可重入函数的示范代码:
#include <stdio.h>
#include <pthread.h>
// 这是一个可重入函数,因为它不依赖任何全局或静态变量
int add(int a, int b) {
return a + b;
}
// 线程函数,它将调用可重入函数add
void* thread_func(void* arg) {
int a = *((int*)arg);
int b = 5;
int result = add(a, b);
printf("Thread %ld: %d + %d = %d\n", (long)pthread_self(), a, b, result);
return NULL;
}
int main() {
pthread_t threads[5];
int values[] = {1, 2, 3, 4, 5};
// 创建5个线程,每个线程都调用add函数
for (int i = 0; i < 5; ++i) {
pthread_create(&threads[i], NULL, thread_func, (void*)&values[i]);
}
// 等待所有线程完成
for (int i = 0; i < 5; ++i) {
pthread_join(threads[i], NULL);
}
return 0;
}
在这个示例中,add
函数是一个可重入函数,因为它只依赖于其参数,并不使用任何全局或静态变量。thread_func
是线程函数,它调用了add
函数。我们创建了5个线程,每个线程都调用add
函数,但由于add
是可重入的,所以每个线程都会得到正确的结果,而不会相互干扰。
volatile关键字
volatile
关键字在C和C++编程中,尤其是在嵌入式系统和多线程编程中,是一个非常重要的概念。在Linux编程中,它同样扮演着重要角色。volatile
告诉编译器不要对该变量进行任何优化,每次访问这个变量时都应该直接从其内存地址中读取,而不是使用寄存器中的值或进行其他可能的优化。
这是因为有些变量的值可能会在编译器无法检测到的情况下改变,例如:
-
硬件寄存器:在嵌入式编程中,我们可能会直接操作硬件寄存器,这些寄存器的值可能会在任何时候由硬件改变。
-
多线程共享变量:在多线程环境中,一个线程可能会修改一个变量的值,而另一个线程需要立即看到这个改变。
-
信号或中断服务例程:在操作系统中,中断服务例程可能会改变某些变量的值,而这些改变是编译器无法预测的。
在这些情况下,使用volatile
可以确保每次访问变量时都会从内存中读取其最新值,而不是使用编译器可能缓存的“旧”值。
示例代码:
假设我们正在编写一个驱动程序,该驱动程序需要读取一个硬件寄存器的值。
#include <stdio.h>
// 假设这是一个硬件寄存器的地址
volatile int *hardware_register = (volatile int*)0x12345678;
int main() {
int value;
// 读取硬件寄存器的值
value = *hardware_register;
printf("The value of the hardware register is: %d\n", value);
return 0;
}
在这个例子中,hardware_register
是一个指向硬件寄存器地址的指针,并且它被声明为volatile
。这意味着每次我们读取*hardware_register
时,都会直接从硬件寄存器中读取其值,而不是使用任何可能的编译器优化或缓存的值。
需要注意的是,volatile
并不提供任何形式的原子性保证。在多线程环境中,如果多个线程可能同时修改一个volatile
变量,那么仍然需要使用适当的同步机制(如互斥锁或原子操作)来确保数据的一致性。同样,volatile
也不能防止编译器重新排序读写操作。如果需要这些更高级的特性,通常需要使用特定的编译器扩展或语言特性(如C11中的_Atomic
类型或C++中的std::atomic
)。
SIGHLD信号
SIGHLD信号介绍
SIGCHLD信号的概念
SIGCHLD信号是一个在Unix和类Unix系统中用于通知父进程其子进程状态改变的信号。
SIGCHLD信号的作用
当子进程状态改变时,父进程会收到SIGCHLD信号,这使得父进程能够适当地清理子进程的资源,例如回收僵尸进程。
SIGCHLD信号使用示例
示例代码实现
下面是一个简单的示例代码,展示了如何使用SIGCHLD信号来回收子进程的资源。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
void handle_sigchld(int sig) {
// 当收到SIGCHLD信号时,这个函数会被调用
pid_t pid;
int status;
while ((pid = waitpid(-1, &status, WNOHANG)) > 0) {
// 使用WNOHANG标志来尝试非阻塞地等待子进程
// 如果成功回收了一个子进程,waitpid会返回该子进程的PID
// 如果没有子进程需要回收,waitpid会返回0
// 如果发生错误,waitpid会返回-1
printf("Child %d exited with status %d\n", pid, WEXITSTATUS(status));
}
}
int main() {
pid_t pid;
struct sigaction sa;
// 设置SIGCHLD信号的处理函数
sa.sa_handler = handle_sigchld;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
if (sigaction(SIGCHLD, &sa, NULL) == -1) {
perror("sigaction");
exit(EXIT_FAILURE);
}
// 创建子进程
pid = fork();
if (pid == -1) {
perror("fork");
exit(EXIT_FAILURE);
} else if (pid == 0) {
// 子进程代码
printf("Child process (PID %d) running...\n", getpid());
sleep(2); // 子进程休眠2秒
printf("Child process exiting...\n");
exit(42); // 子进程以状态码42退出
} else {
// 父进程代码
printf("Parent process (PID %d) waiting for child...\n", getpid());
// 父进程可以继续执行其他任务,当子进程退出时,SIGCHLD信号会被发送并处理
}
// 父进程等待一段时间以确保子进程有机会运行和退出
sleep(5);
return 0;
}
示例代码解释
-
信号处理器设置
定义了一个名为handle_sigchld
的函数,作为SIGCHLD信号的处理器。该函数会在收到SIGCHLD信号时被调用,并尝试非阻塞地回收子进程。
-
子进程创建与退出
在main
函数中,使用fork
创建了一个子进程。子进程执行一些操作后,以特定的状态码退出。
-
父进程等待与处理
父进程等待一段时间,以便子进程有机会运行和退出。当子进程退出时,父进程会收到SIGCHLD信号,并调用之前设置的信号处理器函数handle_sigchld
来回收子进程资源。
示例代码总结
通过示例代码展示了如何使用SIGCHLD信号来异步地回收子进程资源,避免了僵尸进程的问题。在实际应用中,可以根据具体需求调整代码以适应不同场景。