目录
信号的概念
生活中的信号
Linux中的信号
kill命令
kill 命令的使用
常见的信号
命令行代码示例
注意事项
信号的处理方式
产生信号
信号的捕捉
信号捕捉示意图
内核如何实现信号捕捉
信号的捕捉与处理
小结
阻塞信号
信号在内核中的表示图
信号集操作函数
可重入函数
可重入函数示意图
可重入函数概念
可重入函数的使用
volatile关键字
SIGHLD信号
SIGHLD信号介绍
SIGCHLD信号的概念
SIGCHLD信号的作用
SIGCHLD信号使用示例
示例代码实现
示例代码解释
示例代码总结
信号的概念
在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信号来异步地回收子进程资源,避免了僵尸进程的问题。在实际应用中,可以根据具体需求调整代码以适应不同场景。