进程间异步通信的核心机制

信号(Signal)是 Linux 系统中用于进程间异步通信的一种基础机制,主要用于通知进程发生了某种特定事件(如用户交互、硬件错误、超时等)。当进程接收到信号时,会暂时中断当前的执行流程,转而处理信号(执行预设的处理函数、遵循系统默认行为或直接忽略)。与信号相关的系统调用及宏定义均在 <signal.h> 头文件中声明,是 Linux 进程管理与事件响应的核心组件。

信号的核心特点

信号的设计围绕“异步通知”展开,具备以下四个关键特性,使其能灵活应对各类系统事件:

异步性

信号的产生和处理时机是不确定的,进程无法提前预测信号何时到达。例如,用户在进程运行过程中突然按下 Ctrl+C(触发 SIGINT 信号),进程会立即中断当前的循环或计算,优先处理该信号。这种异步性让进程无需持续轮询事件,大幅提升了资源利用率。

事件驱动

每个信号都对应一种特定的系统事件,信号本身就是“事件发生”的通知载体。例如:

  • SIGSEGV 对应“进程访问无效内存地址”(如空指针解引用);
  • SIGCHLD 对应“子进程终止或状态改变”;
  • SIGALRM 对应“定时器超时”(由 alarm() 函数设置)。
    通过不同的信号,系统能清晰区分事件类型,让进程针对性处理。

简单性

信号仅传递“事件发生”的通知,不携带复杂数据——本质上只是一个整数标识(信号编号)。这种轻量级设计让信号的传递和处理效率极高,适合快速响应紧急事件(如硬件错误)。

优先级

部分信号具备强制优先级,无法被进程忽略或自定义处理,以确保系统能对关键事件进行管控。例如:

  • SIGKILL(信号9):强制终止进程,无论进程是否注册了处理函数,都会被终止;
  • SIGSTOP(信号19):强制暂停进程,同样不可忽略或捕获。
    这类信号是系统管理进程的“最终手段”,避免进程失控。

常见信号及其含义

Linux 系统定义了数十种信号(可通过 kill -l 命令在终端查看所有信号),不同信号对应不同的事件和默认行为。以下是最常用的信号及核心宏定义,涵盖日常开发与系统管理中高频使用的场景:

常用信号详情表

信号编号 信号名 含义及触发场景 默认行为
1 SIGHUP 终端挂起(如关闭终端窗口)、进程脱离控制终端(如 daemon 进程后台运行后);也常用于重新加载进程配置(如 Nginx 重载配置) 终止进程
2 SIGINT 用户按下 Ctrl+C(键盘中断),用于手动终止前台运行的进程 终止进程
3 SIGQUIT 用户按下 Ctrl+\(键盘退出),比 SIGINT 更彻底 终止进程并生成核心转储文件(core dump)
8 SIGFPE 浮点异常(如除零错误、浮点运算溢出) 终止进程并生成核心转储文件
9 SIGKILL 强制终止进程(“必杀信号”),系统管理员常用此信号终止无响应进程 终止进程(不可捕获、不可忽略)
11 SIGSEGV 段错误(进程访问无效内存地址,如空指针解引用、数组越界访问未授权内存) 终止进程并生成核心转储文件
13 SIGPIPE 管道破裂:当管道的读端已关闭,写端仍尝试向管道写入数据时触发 终止进程
14 SIGALRM 定时器超时:由 alarm(seconds) 函数设置,seconds 秒后向进程发送该信号 终止进程
15 SIGTERM 优雅终止请求(kill 命令默认发送的信号),进程可捕获该信号执行清理操作(如保存数据) 终止进程
17 SIGCHLD 子进程终止、暂停或恢复时,内核向父进程发送该信号;默认情况下父进程会忽略该信号 忽略(无操作)
18 SIGCONT 恢复被暂停的进程(与 SIGSTOP/SIGTSTP 配合使用) 恢复进程运行
19 SIGSTOP 强制暂停进程,无法被忽略或捕获,用于临时冻结进程执行 暂停进程(不可捕获、不可忽略)
20 SIGTSTP 用户按下 Ctrl+Z(键盘暂停),将前台进程切换到后台并暂停 暂停进程
30 SIGUSR1 用户自定义信号1,无默认事件,由开发者根据需求触发和处理(如进程间自定义通信) 终止进程
31 SIGUSR2 用户自定义信号2,功能与 SIGUSR1 类似,供开发者灵活使用 终止进程

注:核心转储文件(core dump)是进程崩溃时生成的内存快照文件,包含进程崩溃前的内存数据、寄存器状态等信息,可通过 gdb ./程序名 core 调试崩溃原因,默认情况下部分系统可能关闭核心转储(可通过 ulimit -c unlimited 开启)。

信号宏定义(<signal.h> 中声明)

系统通过宏定义统一标识信号,避免直接使用魔法数字,提高代码可读性和可移植性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#define    SIGHUP       1   // 终端挂起
#define SIGINT 2 // 键盘中断(Ctrl+C)
#define SIGQUIT 3 // 键盘退出(Ctrl+\)
#define SIGILL 4 // 非法指令
#define SIGTRAP 5 // 调试陷阱(用于断点调试)
#define SIGABRT 6 // 异常终止(abort() 函数触发)
#define SIGIOT 6 // 与 SIGABRT 同义,兼容旧系统
#define SIGBUS 7 // 总线错误(内存访问对齐错误)
#define SIGFPE 8 // 浮点异常
#define SIGKILL 9 // 强制终止(不可捕获)
#define SIGUSR1 10 // 用户自定义信号1
#define SIGSEGV 11 // 段错误
#define SIGUSR2 12 // 用户自定义信号2
#define SIGPIPE 13 // 管道破裂
#define SIGALRM 14 // 定时器超时
#define SIGTERM 15 // 优雅终止(kill 默认信号)
#define SIGSTKFLT 16 // 栈错误(仅部分架构支持)
#define SIGCHLD 17 // 子进程状态改变
#define SIGCONT 18 // 恢复进程
#define SIGSTOP 19 // 强制暂停(不可捕获)
#define SIGTSTP 20 // 键盘暂停(Ctrl+Z)
#define SIGTTIN 21 // 后台进程读终端
#define SIGTTOU 22 // 后台进程写终端
#define SIGURG 23 // 紧急数据到达(如网络套接字)
#define SIGXCPU 24 // 超出CPU时间限制
#define SIGXFSZ 25 // 超出文件大小限制
#define SIGVTALRM 26 // 虚拟时钟超时
#define SIGPROF 27 // profiling 时钟超时
#define SIGWINCH 28 // 终端窗口大小改变
#define SIGIO 29 // I/O 就绪通知
#define SIGPWR 30 // 电源故障(仅部分系统支持)

信号的生命周期

信号从“产生”到“递达”需经历三个阶段:产生(Generate)、未决(Pending)、递达(Deliver)。每个阶段对应内核与进程的不同处理逻辑,确保信号能有序响应。

信号产生(Generate)

信号由内核、其他进程或进程自身触发,常见的产生途径包括:

  • 内核触发
    • 硬件事件:如内存访问错误(触发 SIGSEGV)、除零错误(触发 SIGFPE)、CPU 时间超限(触发 SIGXCPU);
    • 软件事件:如定时器超时(alarm() 触发 SIGALRM)、管道破裂(SIGPIPE)、终端窗口大小改变(SIGWINCH)。
  • 其他进程触发:通过 kill(pid, sig) 系统调用向指定进程发送信号。例如,终端执行 kill 1234 9 表示向 PID=1234 的进程发送 SIGKILL 信号。
  • 进程自身触发:通过 raise(sig) 函数向当前进程发送信号(等价于 kill(getpid(), sig)),或调用 abort() 函数触发 SIGABRT 信号(强制进程异常终止)。

信号未决(Pending)

信号产生后,若进程暂时无法处理(如进程正在处理更高优先级的信号,或该信号被“信号掩码”阻塞),会进入“未决状态”。未决信号会被存储在进程的 PCB(进程控制块) 中的“未决信号集”(一个位图结构,每一位代表一个信号的未决状态)。

信号掩码(Signal Mask)

进程可以通过 sigprocmask() 函数设置“信号掩码”(一组需要阻塞的信号)。被掩码阻塞的信号产生后,不会立即递达,而是保持未决状态,直到掩码解除阻塞(进程调用 sigprocmask() 移除该信号的阻塞)。

例如:若进程设置掩码阻塞 SIGINT,按下 Ctrl+CSIGINT 会进入未决状态;当进程解除 SIGINT 的阻塞后,该信号才会递达并被处理。

信号递达(Deliver)

进程从“未决信号集”中取出信号并处理的过程称为“递达”。进程对信号的处理方式有三种,优先级从高到低依次为:

强制默认行为(不可修改)

仅针对 SIGKILL(9)和 SIGSTOP(19),这两个信号无法被忽略、捕获或修改处理方式,确保系统能强制控制进程(如终止无响应进程、暂停恶意进程)。

自定义处理(捕获信号)

进程通过 signal()sigaction() 函数为信号注册“自定义处理函数”,信号递达时会执行该函数。例如,为 SIGTERM 注册处理函数,让进程在收到终止信号时先保存数据再退出。

忽略信号

进程明确指定对信号不做任何处理(通过 signal(sig, SIG_IGN) 实现)。除 SIGKILLSIGSTOP 外,其他信号均可被忽略。例如,忽略 SIGCHLD 信号(默认行为),但需注意:若父进程忽略 SIGCHLD,子进程终止后会直接被内核回收,不会成为僵尸进程。

信号处理的基本操作

Linux 提供了一系列系统调用用于信号的发送、注册和等待,以下是核心操作的详细说明及示例代码。

发送信号:kill()raise()

kill() 函数:向指定进程发送信号

kill() 是最常用的信号发送函数,可向任意进程(需权限)发送指定信号,原型如下:

1
2
3
4
#include <sys/types.h>
#include <signal.h>

int kill(pid_t pid, int sig);
参数说明:
  • pid:目标进程/进程组的标识,支持四种取值:
    • pid > 0:向 PID 为 pid 的单个进程发送信号;
    • pid = 0:向当前进程所在的“进程组”内所有进程发送信号;
    • pid = -1:向系统中所有有权限发送的进程发送信号(谨慎使用,可能影响系统稳定性);
    • pid < -1:向进程组 ID 为 -pid 的所有进程发送信号(如 pid = -1234 表示向进程组 1234 的所有进程发送信号)。
  • sig:要发送的信号(如 SIGTERMSIGKILL,取值为信号编号或宏定义)。
返回值:
  • 成功:返回 0;
  • 失败:返回 -1,并设置 errno(如 EPERM 表示无权限发送,ESRCH 表示目标进程不存在)。
示例代码:父进程向子进程发送 SIGTERM 信号
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include <signal.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>

int main() {
pid_t pid = fork(); // 创建子进程
if (pid == -1) {
perror("fork failed"); // 打印错误原因(如资源不足)
exit(1); // 异常退出
}

if (pid == 0) {
// 子进程逻辑:暂停等待信号
printf("子进程(PID: %d)启动,等待信号...\n", getpid());
while (1) {
pause(); // 暂停进程,直到收到一个非阻塞信号
// 若收到 SIGTERM(默认行为是终止),不会执行到这里
printf("子进程收到信号但未终止(此句不会执行)\n");
}
} else {
// 父进程逻辑:等待1秒后发送终止信号
printf("父进程(PID: %d)启动,子进程PID: %d\n", getpid(), pid);
sleep(1); // 等待1秒,确保子进程已进入等待状态

// 向子进程发送 SIGTERM 信号(优雅终止请求)
if (kill(pid, SIGTERM) == -1) {
perror("kill failed");
exit(1);
}
printf("父进程已发送 SIGTERM 信号,子进程将终止\n");

// 等待子进程退出,避免僵尸进程
wait(NULL);
printf("子进程已被回收\n");
}

return 0;
}
raise() 函数:向当前进程发送信号

raise() 是简化版的 kill(),仅能向当前进程发送信号,原型如下:

1
2
3
#include <signal.h>

int raise(int sig);
功能:

等价于 kill(getpid(), sig),无需指定进程 ID,适合进程向自身发送信号(如异常时自我终止)。

示例代码:进程向自身发送 SIGINT 信号
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

void handle_sigint(int sig) {
printf("\n进程(PID: %d)收到 SIGINT 信号,即将退出\n", getpid());
exit(0);
}

int main() {
// 注册 SIGINT 处理函数
signal(SIGINT, handle_sigint);
printf("进程启动,3秒后向自身发送 SIGINT 信号...\n");
sleep(3);

// 向当前进程发送 SIGINT 信号
if (raise(SIGINT) == -1) {
perror("raise failed");
return 1;
}

// 信号处理后进程已退出,此句不会执行
printf("信号发送后继续执行(此句不会执行)\n");
return 0;
}

注册信号处理函数:signal()sigaction()

signal() 函数:简单注册处理函数

signal() 是最基础的信号处理注册函数,用于为指定信号绑定自定义处理函数,原型如下:

1
2
3
4
5
6
#include <signal.h>

// 定义信号处理函数类型:参数为信号编号,无返回值
typedef void (*sighandler_t)(int);

sighandler_t signal(int sig, sighandler_t handler);
参数说明:
  • sig:要注册处理函数的信号(如 SIGINTSIGTERMSIGKILL/SIGSTOP 除外);
  • handler:信号的处理方式,支持三种取值:
    • 自定义函数:符合 sighandler_t 类型的函数(如 void handle_sig(int sig)),信号递达时执行;
    • SIG_IGN:宏,表示“忽略该信号”;
    • SIG_DFL:宏,表示“恢复该信号的系统默认行为”。
返回值:
  • 成功:返回之前注册的处理函数(若之前未注册,返回 SIG_DFL);
  • 失败:返回 SIG_ERR(通常是 (sighandler_t)-1),并设置 errno
示例代码:捕获 SIGINT 信号,自定义处理逻辑
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <unistd.h>

// 自定义 SIGINT 处理函数
void handle_sigint(int sig) {
// 注意:处理函数应尽量使用“异步安全函数”(如 write、_exit),避免 printf(非异步安全)
write(STDOUT_FILENO, "\n收到 SIGINT 信号(Ctrl+C),不退出!\n", 36);
}

int main() {
// 注册 SIGINT 信号的处理函数
sighandler_t old_handler = signal(SIGINT, handle_sigint);
if (old_handler == SIG_ERR) {
perror("signal 注册失败");
return 1;
}

// 无限循环,观察信号处理
int count = 0;
while (1) {
printf("运行中...(第 %d 秒,按 Ctrl+C 测试)\n", count++);
sleep(1);
}

return 0;
}
注意:signal() 的局限性

signal() 是早期接口,存在兼容性问题(不同系统对信号重注册的处理不同),且无法处理“可靠信号”(34+)的排队问题。在复杂场景下,建议使用更强大的 sigaction() 函数。

sigaction() 函数:可靠的信号注册

sigaction()signal() 的增强版,支持更精细的信号控制(如设置信号掩码、获取信号信息),且兼容可靠信号,原型如下:

1
2
3
#include <signal.h>

int sigaction(int sig, const struct sigaction *act, struct sigaction *oldact);
核心结构体 struct sigaction
1
2
3
4
5
6
7
struct sigaction {
void (*sa_handler)(int); // 处理函数(与 signal() 兼容)
void (*sa_sigaction)(int, siginfo_t *, void *); // 高级处理函数(可获取信号详情)
sigset_t sa_mask; // 处理该信号时,临时阻塞的信号集
int sa_flags; // 行为标志(如 SA_SIGINFO 表示使用 sa_sigaction)
void (*sa_restorer)(void); // 已废弃,无需关注
};
示例代码:用 sigaction() 捕获 SIGCHLD 回收子进程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#include <signal.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>

// 自定义 SIGCHLD 处理函数:回收所有终止的子进程
void handle_sigchld(int sig, siginfo_t *info, void *ucontext) {
// 非阻塞回收所有子进程,避免遗漏(WNOHANG 表示无终止子进程时立即返回)
while (waitpid(-1, NULL, WNOHANG) > 0) {
printf("子进程(PID: %d)已终止,已回收\n", info->si_pid);
}
}

int main() {
struct sigaction act;
// 初始化信号处理结构体
act.sa_sigaction = handle_sigchld; // 使用高级处理函数
sigemptyset(&act.sa_mask); // 处理信号时不阻塞其他信号
act.sa_flags = SA_SIGINFO; // 启用 sa_sigaction,传递信号详情

// 注册 SIGCHLD 信号处理函数
if (sigaction(SIGCHLD, &act, NULL) == -1) {
perror("sigaction 注册失败");
return 1;
}

// 创建3个子进程
for (int i = 0; i < 3; i++) {
pid_t pid = fork();
if (pid == 0) {
printf("子进程(PID: %d)启动,5秒后退出\n", getpid());
sleep(5); // 子进程运行5秒后退出
exit(0);
} else if (pid == -1) {
perror("fork failed");
return 1;
}
}

// 父进程无限循环,等待子进程终止信号
while (1) {
sleep(1);
}

return 0;
}

等待信号:pause() 函数

pause() 函数使当前进程暂停运行,直到收到一个“非阻塞且未被忽略”的信号。若信号被捕获且处理函数返回,进程会继续执行;若信号的默认行为是终止或暂停,进程会遵循默认行为。

原型:
1
2
3
#include <unistd.h>

int pause(void);
返回值:

pause() 始终返回 -1,并设置 errnoEINTR(表示被信号中断)——因为只有收到信号后进程才会恢复,不会正常返回。

示例代码:pause() 配合 signal() 等待信号
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>

int flag = 0; // 信号触发标志

void handle_sigusr1(int sig) {
printf("收到 SIGUSR1 信号,flag 置1\n");
flag = 1;
}

int main() {
// 注册 SIGUSR1 处理函数
signal(SIGUSR1, handle_sigusr1);
printf("进程(PID: %d)启动,等待 SIGUSR1 信号...\n", getpid());
printf("请在另一个终端执行:kill -USR1 %d\n", getpid());

// 循环等待信号触发
while (!flag) {
pause(); // 暂停进程,直到收到信号
printf("pause() 被信号中断,flag = %d\n", flag);
}

printf("信号处理完成,进程退出\n");
return 0;
}
操作步骤:
  1. 编译运行程序,终端显示进程 PID(如 5678);
  2. 打开另一个终端,执行 kill -USR1 5678(向进程发送 SIGUSR1 信号);
  3. 原终端会打印信号处理信息,进程继续执行并退出。

信号的注意事项

在使用信号时,需关注以下细节,避免出现信号丢失、进程崩溃或资源泄漏等问题。

不可靠信号与可靠信号

Linux 信号分为两类,核心差异在于是否支持“排队”:

不可靠信号(编号 1-31)
  • 也称为“非实时信号”,早期 Unix 信号的延续;
  • 不支持排队:若同一信号多次发送,进程可能只处理一次(未决信号集中仅用一位标识,无法记录次数);
  • 可能被重置:部分系统中,信号被捕获后,处理函数会自动重置为默认行为(需在处理函数中重新注册)。

例如:向进程快速发送 3 次 SIGINT,进程可能只执行一次处理函数。

可靠信号(编号 34+)
  • 也称为“实时信号”,Linux 扩展的信号;
  • 支持排队:未决信号集中用多个位或队列记录信号次数,多次发送的信号会依次递达;
  • 不会被重置:处理函数注册后始终有效,无需重新注册。

可靠信号的命名格式为 SIGRTMIN + n(如 SIGRTMIN 为 34,SIGRTMIN+1 为 35),使用时需通过 sigaction() 注册处理函数。

信号处理函数的安全性

信号处理函数会中断进程的正常执行流程,因此必须使用“异步安全函数”(Async-Safe Functions),避免调用非异步安全函数导致数据竞争或死锁。

常见的异步安全函数:
  • 进程控制:_exit()fork()
  • I/O 操作:write()read()close()
  • 信号操作:signal()sigaction()sigprocmask()
  • 其他:getpid()getppid()
非异步安全函数(避免在处理函数中使用):
  • 标准 I/O:printf()fprintf()fopen()(内部有缓冲区,可能导致数据错乱);
  • 内存分配:malloc()free()(内部有锁,可能导致死锁);
  • 字符串处理:strcat()strcpy()(部分实现非线程安全)。
错误示例(非异步安全):
1
2
3
void handle_sigint(int sig) {
printf("收到 SIGINT 信号\n"); // 错误:printf 非异步安全
}
正确示例(异步安全):
1
2
3
4
void handle_sigint(int sig) {
const char msg[] = "收到 SIGINT 信号\n";
write(STDOUT_FILENO, msg, sizeof(msg)-1); // 正确:write 是异步安全函数
}

禁止捕获的信号

SIGKILL(9)和 SIGSTOP(19)是 Linux 中唯一两个“不可捕获、不可忽略、不可修改处理方式”的信号,原因如下:

  • SIGKILL:确保系统管理员能强制终止任何失控进程(如死循环、无响应进程);
  • SIGSTOP:确保能临时冻结进程(如调试时暂停进程),避免进程规避调试。

任何尝试为这两个信号注册处理函数的操作都会失败,例如:

1
2
3
4
// 尝试捕获 SIGKILL,会失败
if (signal(SIGKILL, handle_sigkill) == SIG_ERR) {
perror("signal SIGKILL 失败"); // 输出:signal SIGKILL 失败: Invalid argument
}

信号与进程状态的交互

当进程处于以下状态时,收到信号的处理逻辑会有所不同:

阻塞状态(如 sleep()wait()read() 等待 I/O)

进程会立即被唤醒,优先处理信号。若信号处理函数返回,进程会继续执行原阻塞操作(如 sleep(5) 被信号中断后,会返回剩余睡眠时间,或重新进入阻塞)。

暂停状态(SIGSTOP/SIGTSTP 触发)

只有 SIGCONT 信号能恢复进程运行,其他信号会被暂存(进入未决状态),直到进程恢复后再递达。

僵尸状态

僵尸进程已终止,仅保留 PCB 信息,无法接收或处理任何信号。只有父进程调用 wait() 回收后,僵尸进程才会消失。

信号的实际应用场景

信号在 Linux 系统和应用开发中应用广泛,以下是几个典型场景:

进程间自定义通信

通过 SIGUSR1SIGUSR2 两个用户自定义信号,实现进程间简单的状态同步。例如:

  • 进程 A 完成数据准备后,向进程 B 发送 SIGUSR1 信号;
  • 进程 B 收到信号后,开始读取数据并处理。

子进程回收

父进程注册 SIGCHLD 信号处理函数,在子进程终止时自动调用 waitpid() 回收,避免僵尸进程。这是服务器程序中常用的模式(如 Nginx、Apache 等多进程服务器)。

定时器与超时控制

通过 alarm(seconds) 函数设置定时器,seconds 秒后向进程发送 SIGALRM 信号。例如:

  • 网络编程中,设置 5 秒超时,若 5 秒内未收到数据,触发 SIGALRM 信号,关闭连接。

程序优雅退出

捕获 SIGTERM 信号(kill 命令默认信号),在处理函数中执行资源清理操作(如关闭文件、释放内存、保存配置),让程序优雅退出,避免数据丢失。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void handle_sigterm(int sig) {
printf("收到终止信号,开始清理资源...\n");
// 关闭已打开的文件
close(log_fd);
// 释放动态内存
free(buffer);
printf("资源清理完成,程序退出\n");
exit(0);
}

int main() {
signal(SIGTERM, handle_sigterm);
// 程序主逻辑...
return 0;
}

修改信号的响应方式

在 Linux 系统中,进程收到信号后的响应方式并非固定不变,可通过 signal() 函数在 “内核预设行为”“主动忽略”“自定义处理” 三种模式间调整。需注意,部分核心信号(如 SIGKILL、SIGSTOP)的响应方式无法修改,以确保系统对进程的强制管控能力。以下详细说明信号响应方式的分类、规则及实操示例。

信号的 3 种响应方式

进程对信号的响应逻辑由 signal() 函数的 handler 参数决定,共支持三种核心类型,每种类型对应不同的使用场景与限制。

默认行为(SIG_DFL)

默认行为是内核为每个信号预设的处理逻辑,无需用户额外配置,进程启动后默认遵循。常见的默认行为包括:

  • 终止进程:多数信号的默认行为,如 SIGINT(用户按下 Ctrl+C)、SIGTERM(kill 命令默认信号)、SIGHUP(终端挂起),触发后进程直接退出;

  • 终止进程并生成核心转储文件:针对程序异常类信号,如 SIGQUIT(用户按下 Ctrl+\)、SIGSEGV(段错误,如空指针解引用)、SIGFPE(浮点异常,如除零错误)。核心转储文件(core dump)包含进程崩溃前的内存快照,可通过 gdb ./程序名 core 调试崩溃原因;

  • 暂停进程:用于临时冻结进程执行,如 SIGSTOP(强制暂停,不可修改响应方式)、SIGTSTP(用户按下 Ctrl+Z,前台进程切换至后台暂停);

  • 恢复进程:仅针对 SIGCONT 信号,用于恢复被 SIGSTOP/SIGTSTP 暂停的进程,无其他默认行为;

  • 忽略信号:部分信号默认被内核忽略,如 SIGCHLD(子进程终止或状态改变时,父进程默认忽略)、SIGURG(网络套接字紧急数据到达时默认忽略)。

默认行为的触发无需用户干预,仅当需要修改响应逻辑时,才需通过 signal() 函数调整。

忽略信号(SIG_IGN)

忽略信号是指进程明确指定对某类信号不做任何处理,信号递达后直接被内核丢弃,进程继续执行当前逻辑。

  • 触发方式:通过 signal(sig, SIG_IGN) 注册,其中 sig 为目标信号,SIG_IGN 是系统定义的宏,表示 “忽略该信号”;

  • 适用场景

  1. 避免程序被无关信号中断,如后台服务进程忽略 SIGINT(禁止用户用 Ctrl+C 终止);

  2. 防止特定信号导致程序异常退出,如忽略 SIGPIPE(当管道读端已关闭、写端仍写入时,默认会终止程序,忽略后可自定义处理写失败逻辑);

  • 限制:SIGKILL(信号 9)和 SIGSTOP(信号 19)无法被忽略,这两个信号是系统强制管控进程的 “最终手段”,确保能终止或暂停失控进程。

自定义处理(捕获信号)

自定义处理(又称 “捕获信号”)是指进程通过注册自定义函数,让信号递达时执行该函数(替代内核默认行为),灵活实现业务逻辑(如资源清理、状态同步)。

  • 触发方式:通过 signal(sig, handler) 注册,其中 handler 为用户定义的函数指针,函数原型需符合 void (*sighandler_t)(int)(参数为信号编号,无返回值);

  • 适用场景

  1. 实现程序 “优雅退出”:捕获 SIGTERM 信号,在进程终止前执行资源清理(如关闭文件、释放动态内存、保存配置数据);

  2. 自动回收子进程:捕获 SIGCHLD 信号,当子进程终止时调用 waitpid() 回收资源,避免僵尸进程;

  3. 自定义事件响应:通过 SIGUSR1/SIGUSR2 等用户自定义信号,实现进程间简单的状态同步(如进程 A 完成数据准备后,向进程 B 发送 SIGUSR1 触发数据读取);

  • 实现限制
  1. SIGKILL 和 SIGSTOP 无法被捕获,确保系统能强制终止或暂停任何进程;

  2. 推荐使用 sigaction() 替代 signal() 实现复杂场景(signal() 是早期接口,存在兼容性问题,且不支持可靠信号的排队处理)。

关键规则与注意事项

使用 signal() 函数修改信号响应方式时,需遵循以下规则,避免出现进程崩溃、信号丢失或资源泄漏等问题。

不可修改响应方式的信号

Linux 系统中,仅 SIGKILL(信号 9)SIGSTOP(信号 19) 无法通过 signal() 或其他函数修改响应方式,具体表现为:

  • 无法忽略:调用 signal(SIGKILL, SIG_IGN) 会返回错误(SIG_ERR),信号仍会强制终止进程;

  • 无法捕获:注册自定义处理函数(如 signal(SIGSTOP, handler))会失败,信号仍会强制暂停进程;

  • 设计目的:这两个信号是系统管理员管控进程的 “最后手段”,避免进程通过自定义逻辑规避终止或暂停(如恶意进程拒绝退出)。

信号处理函数的安全性

自定义信号处理函数会中断进程的正常执行流程,因此必须使用 “异步安全函数”(Async-Safe Functions),避免调用非异步安全函数导致数据竞争或死锁。

常见异步安全函数
  • 进程控制:_exit()、fork();

  • I/O 操作:write()、read()、close()、fcntl();

  • 信号操作:signal()、sigaction()、sigprocmask();

  • 基础工具:getpid()、getppid()、alarm()。

非异步安全函数(禁止使用)
  • 标准 I/O 函数:printf()、fprintf()、fopen()、fclose()(内部维护缓冲区,信号中断可能导致缓冲区数据错乱);

  • 内存分配函数:malloc()、free()、realloc()(内部使用全局锁,信号中断可能导致锁死);

  • 字符串处理函数:strcat()、strcpy()、sprintf()(部分实现依赖全局状态,非线程 / 信号安全)。

安全示例
1
2
3
4
5
6
7
8
9
10
// 正确:使用异步安全函数 write() 输出信息
void safe_handler(int sig) {
const char msg[] = "收到信号,执行安全处理\n";
write(STDOUT_FILENO, msg, sizeof(msg) - 1); // STDOUT_FILENO 是标准输出的文件描述符(1)
}

// 错误:使用非异步安全函数 printf()
void unsafe_handler(int sig) {
printf("收到信号,执行不安全处理\n"); // 可能导致缓冲区错乱或死锁
}

信号的可重入性问题

若同一信号在处理过程中再次触发(如信号处理函数执行时,再次收到相同信号),会导致处理函数 “嵌套执行”,引发数据不一致(不可重入)。

问题示例
1
2
3
4
5
6
7
8
int global_count = 0;

void handler(int sig) {
global_count++; // 非原子操作:读取→修改→写入
printf("count: %d\n", global_count); // 非异步安全函数,且可能嵌套执行
}

// 若短时间内多次发送 SIGINT,global_count 可能出现计算错误

通过 sigprocmask() 函数解决,在处理信号时 “阻塞同类信号”,避免嵌套执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <signal.h>

void handler(int sig) {
sigset_t mask, old_mask;
// 1. 创建信号集,包含当前要处理的信号
sigemptyset(&mask);
sigaddset(&mask, sig);
// 2. 阻塞该信号,保存原有信号掩码
sigprocmask(SIG_BLOCK, &mask, &old_mask);

// 3. 执行核心处理逻辑(可安全操作全局变量)
global_count++;
const char msg[32];
snprintf(msg, sizeof(msg), "count: %d\n", global_count); // 仅在处理函数内使用栈内存
write(STDOUT_FILENO, msg, strlen(msg));

// 4. 恢复原有信号掩码,允许再次接收该信号
sigprocmask(SIG_SETMASK, &old_mask, NULL);
}

默认行为的恢复

若已通过 signal() 自定义信号的响应方式,可通过 signal(sig, SIG_DFL) 恢复该信号的内核默认行为。

适用场景
  • 临时修改信号响应:如程序启动时忽略 SIGINT,完成初始化后恢复默认终止行为;

  • 动态调整逻辑:如子进程继承父进程的信号处理方式后,按需恢复默认行为。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <signal.h>
#include <stdio.h>
#include <unistd.h>

void temp_handler(int sig) {
printf("\n临时处理:当前忽略 Ctrl+C,3秒后恢复默认行为\n");
sleep(3);
// 恢复 SIGINT 的默认行为(再次按 Ctrl+C 会终止进程)
signal(SIGINT, SIG_DFL);
printf("已恢复默认行为:下次 Ctrl+C 将终止进程\n");
}

int main() {
// 注册临时处理函数
signal(SIGINT, temp_handler);
printf("程序运行中,按 Ctrl+C 测试(前3秒忽略终止)\n");
while (1) sleep(1); // 保持进程运行
return 0;
}

调整信号响应方式

以下通过具体代码示例,展示如何使用 signal() 函数实现 “忽略信号”“自定义处理”“恢复默认行为” 三种场景。

忽略信号(以 SIGPIPE 为例)

SIGPIPE 信号通常在 “管道读端已关闭、写端仍写入” 时触发,默认行为是终止程序。忽略该信号可避免程序意外退出,便于自定义处理写失败逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <signal.h>
#include <stdio.h>
#include <unistd.h>

int main() {
// 忽略 SIGPIPE 信号:避免向关闭的管道写入时程序终止
if (signal(SIGPIPE, SIG_IGN) == SIG_ERR) {
perror("signal(SIGPIPE, SIG_IGN) 失败"); // 打印错误原因(如信号无效)
return 1;
}

printf("已忽略 SIGPIPE 信号,程序可安全处理管道写失败\n");
while (1) pause(); // 持续运行,等待其他信号或事件(避免进程退出)
return 0;
}
  • signal(SIGPIPE, SIG_IGN):将 SIGPIPE 信号的响应方式设为 “忽略”;

  • pause():使进程暂停运行,直到收到一个非忽略的信号(此处用于保持进程存活,观察信号处理效果)。

自定义处理(以 SIGINT 为例)

SIGINT 信号由 Ctrl+C 触发,默认行为是终止进程。通过自定义处理函数,可实现 “按 Ctrl+C 不退出,仅输出提示” 的逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>

// 自定义 SIGINT 处理函数:参数 sig 为收到的信号编号(此处固定为 SIGINT=2)
void handle_sigint(int sig) {
// 使用 write() 而非 printf(),确保异步安全
const char msg[] = "\n收到 SIGINT 信号(Ctrl+C),程序不退出,继续运行...\n";
write(STDOUT_FILENO, msg, strlen(msg));
}

int main() {
// 注册自定义处理函数:将 SIGINT 绑定到 handle_sigint
sighandler_t old_handler = signal(SIGINT, handle_sigint);
if (old_handler == SIG_ERR) {
perror("signal(SIGINT, handle_sigint) 失败");
return 1;
}

// 循环输出运行状态,观察信号处理效果
int count = 0;
while (1) {
printf("程序运行中(第 %d 秒),按 Ctrl+C 测试\n", count++);
sleep(1); // 每秒输出一次,降低打印频率
}

return 0;
}
  • sighandler_t:信号处理函数的类型定义(typedef void (*sighandler_t)(int)),用于接收 signal() 的返回值;

  • old_handler:存储之前的信号处理方式(如默认行为 SIG_DFL),可用于后续恢复。

恢复默认行为(以 SIGINT 为例)

先自定义 SIGINT 的处理方式,一段时间后恢复默认行为,实现 “临时忽略 Ctrl+C,之后允许终止” 的逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>

void handle_sigint(int sig) {
const char msg1[] = "\n收到 SIGINT 信号,当前忽略终止,3秒后恢复默认行为...\n";
write(STDOUT_FILENO, msg1, strlen(msg1));

sleep(3); // 等待3秒,模拟临时处理逻辑

// 恢复 SIGINT 的默认行为
signal(SIGINT, SIG_DFL);
const char msg2[] = "已恢复默认行为:下次按 Ctrl+C 将终止程序\n";
write(STDOUT_FILENO, msg2, strlen(msg2));
}

int main() {
// 注册自定义处理函数
if (signal(SIGINT, handle_sigint) == SIG_ERR) {
perror("signal(SIGINT, handle_sigint) 失败");
return 1;
}

printf("程序启动,前3秒按 Ctrl+C 不终止,之后恢复默认行为\n");
while (1) sleep(1); // 保持进程运行
return 0;
}
  1. 程序启动后,按 Ctrl+C 会输出提示,不终止;

  2. 3 秒后,再次按 Ctrl+C,进程会遵循默认行为终止。

完整示例:main.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#include<stdio.h>    // 标准输入输出库,提供printf、perror等函数
#include<unistd.h> // Unix系统调用库,提供sleep、pause、write等函数
#include<stdlib.h> // 标准库,提供exit函数(本示例暂未使用)
#include<string.h> // 字符串处理库,提供strlen等函数
#include<signal.h> // 信号处理库,提供signal函数、信号宏(SIGINT、SIG_IGN等)

// 全局标志:标记是否恢复默认行为
int restore_default = 0;

// 自定义信号处理函数:处理 SIGINT 信号
void fun_sig(int sig) {
// 打印收到的信号编号(SIGINT 对应编号2)
const char msg_prefix[] = "收到信号:";
char msg_sig[16];
snprintf(msg_sig, sizeof(msg_sig), "%d\n", sig);
write(STDOUT_FILENO, msg_prefix, strlen(msg_prefix));
write(STDOUT_FILENO, msg_sig, strlen(msg_sig));

// 若未恢复默认行为,提示3秒后切换逻辑
if (!restore_default) {
const char msg_temp[] = "当前自定义处理,3秒后切换为「忽略信号」\n";
write(STDOUT_FILENO, msg_temp, strlen(msg_temp));
sleep(3);

// 切换为“忽略 SIGINT 信号”
signal(SIGINT, SIG_IGN);
const char msg_ign[] = "已切换为忽略 SIGINT,按 Ctrl+C 无响应,3秒后恢复默认\n";
write(STDOUT_FILENO, msg_ign, strlen(msg_ign));
sleep(3);

// 恢复 SIGINT 的默认行为
signal(SIGINT, SIG_DFL);
const char msg_dfl[] = "已恢复默认行为,下次按 Ctrl+C 将终止程序\n";
write(STDOUT_FILENO, msg_dfl, strlen(msg_dfl));
restore_default = 1;
}
}

int main() {
// 初始注册 SIGINT 的自定义处理函数
if (signal(SIGINT, fun_sig) == SIG_ERR) {
perror("signal(SIGINT, fun_sig) 失败");
return 1;
}

// 输出操作提示
const char prompt[] = "程序运行中:\n"
"1. 第一次按 Ctrl+C:自定义处理\n"
"2. 3秒后按 Ctrl+C:忽略信号(无响应)\n"
"3. 再等3秒后按 Ctrl+C:恢复默认(终止程序)\n";
write(STDOUT_FILENO, prompt, strlen(prompt));

// 保持进程运行,等待信号触发
while (1) sleep(1);
return 0;
}
  • 初始阶段:SIGINT 绑定自定义函数 fun_sig,按 Ctrl+C 输出信号编号;

  • 3 秒后:切换为忽略 SIGINT,按 Ctrl+C 无响应;

  • 再等 3 秒:恢复 SIGINT 默认行为,按 Ctrl+C 终止进程;

  • 全程使用 write() 确保异步安全,避免 printf() 引发的潜在问题。

发送信号与 kill () 函数

在 Linux 系统中,kill()函数是最核心的信号发送接口,用于向指定进程或进程组发送特定信号(如终止、暂停、自定义通知等)。无论是终端执行kill命令,还是程序内部实现进程间信号通信,本质上都依赖kill()系统调用。本文将详细介绍kill()函数的用法、参数逻辑、示例代码,以及结合fork()和SIGCHLD信号的子进程回收机制。

kill () 函数基础

kill()函数通过系统调用实现信号发送,支持向单个进程、进程组甚至所有有权限的进程传递信号,是进程间异步通信的关键工具。

函数原型与头文件

使用kill()函数需包含以下头文件,确保类型定义和函数声明有效:

1
2
#include <signal.h>       // 包含kill()函数声明及信号宏定义(如SIGTERM、SIGKILL)
#include <sys/types.h> // 包含pid_t类型定义(进程ID的标准类型)

kill()函数的原型如下:

1
int kill(pid_t pid, int sig);

参数详解

kill()函数的两个参数分别控制 “信号发送的目标”(pid)和 “发送的信号类型”(sig),每个参数的取值都有明确的场景含义。

目标标识:pid

pid(Process ID)指定信号的接收对象,支持四种取值,覆盖不同的发送范围:

pid 取值 含义与场景 示例
pid > 0 向 PID 为pid的单个进程发送信号(最常用场景) kill(1234, SIGTERM):向 PID=1234 的进程发终止信号
pid = 0 向当前进程所在进程组内的所有进程发送信号(包括当前进程) kill(0, SIGSTOP):暂停当前进程组的所有进程
pid = -1 向系统中所有有权限发送的进程发送信号(谨慎使用,可能影响系统稳定性) kill(-1, SIGUSR1):向所有可发送的进程发自定义信号
pid < -1 向进程组 ID 为 ` pid

补充:进程组是 Linux 中进程的组织单位,每个进程都属于一个进程组(默认继承父进程的进程组 ID)。通过pgrep -g <组ID>可查看指定进程组的所有进程。

信号类型:sig

sig指定要发送的信号,取值可为信号编号(如 9)或信号宏定义(如SIGKILL),推荐使用宏定义以提高代码可读性。常见信号及用途如下:

信号宏 编号 用途说明
SIGTERM 15 优雅终止请求(默认信号),进程可捕获并执行清理操作(如保存数据)
SIGKILL 9 强制终止进程(“必杀信号”),不可捕获 / 忽略,确保能终止失控进程
SIGINT 2 键盘中断(Ctrl+C),默认终止进程,可自定义处理
SIGSTOP 19 强制暂停进程,不可捕获 / 忽略,用于临时冻结进程执行
SIGCONT 18 恢复被暂停的进程(与SIGSTOP/SIGTSTP配合使用)
SIGUSR1 10 用户自定义信号 1,无默认行为,用于进程间自定义通信
SIGUSR2 12 用户自定义信号 2,功能与SIGUSR1类似,供开发者灵活使用

注意:SIGKILL(9)和SIGSTOP(19)无法被目标进程忽略或捕获,发送后必然触发默认行为(终止 / 暂停),是系统管控进程的 “最终手段”。

返回值与错误码

kill()函数的返回值仅反映 “信号是否成功提交给内核”,不代表目标进程已处理信号:

  • 成功:返回0,表示信号已被内核接收并准备递达目标进程;

  • 失败:返回-1,并设置errno标识错误原因,常见错误码如下:

errno 值 错误原因
EPERM 权限不足(如普通用户向 root 进程发送信号,或目标进程设置了信号屏蔽)
ESRCH 目标进程 / 进程组不存在(pid无效,或进程已退出)
EINVAL 信号类型无效(sig取值不在 1~64 范围内,或为未定义的信号)

可通过perror()函数打印具体错误信息,帮助调试问题,例如:

1
2
3
4
if (kill(1234, SIGTERM) == -1) {
perror("kill failed"); // 若进程不存在,输出"kill failed: No such process"
return 1;
}

kill () 的实际应用

以下通过多个示例代码,展示kill()函数在不同场景下的使用,包括父子进程通信、模拟系统kill命令、结合fork()的子进程回收等。

父子进程间发送信号

此示例通过fork()创建子进程,父进程使用kill()向子进程发送SIGTERM(优雅终止)或SIGKILL(强制终止)信号,演示信号发送的基本流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <stdio.h>
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>
#include <errno.h>

int main() {
// 创建子进程
pid_t pid = fork();
if (pid == -1) {
perror("fork 失败"); // 处理fork创建失败(如资源不足)
return 1;
}

if (pid == 0) { // 子进程逻辑:持续运行,等待信号
printf("子进程 (PID: %d) 启动,等待信号...\n", getpid());
while (1) {
sleep(1); // 每秒休眠,避免CPU占用过高
}
} else { // 父进程逻辑:等待1秒后发送信号
sleep(1); // 等待子进程完全启动,确保信号能被接收
printf("父进程 (PID: %d) 向子进程 (PID: %d) 发送信号...\n", getpid(), pid);

// 方式1:发送SIGTERM(15号信号,优雅终止,子进程可捕获)
int ret = kill(pid, SIGTERM);
// 方式2:发送SIGKILL(9号信号,强制终止,子进程不可捕获)
// int ret = kill(pid, SIGKILL);

if (ret == -1) {
perror("kill 失败");
return 1;
}
printf("信号发送成功,子进程将按默认行为终止\n");
}

return 0;
}
  1. 编译运行后,子进程启动并打印 PID;

  2. 父进程等待 1 秒后发送信号,子进程收到SIGTERM后默认终止(若发送SIGKILL,子进程会立即强制终止);

  3. 可通过ps -p <子进程PID>验证子进程是否已终止。

示例 :main.c(信号处理与 kill 配合)

此示例结合signal()注册信号处理函数,演示如何通过kill()向进程自身发送信号,或在外部通过kill命令触发自定义处理逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>

// 自定义信号处理函数:收到信号后打印编号,并恢复默认行为
void fun_sig(int sig) {
printf("\n收到信号:sig=%d\n", sig);
// 将信号响应方式恢复为默认(如SIGINT默认终止进程)
signal(sig, SIG_DFL);
}

int main() {
// 注册SIGINT信号(Ctrl+C或kill发送)的处理函数
signal(SIGINT, fun_sig);
// 可选:注册其他信号(如SIGTERM)的处理函数
// signal(SIGTERM, fun_sig);

printf("进程启动(PID: %d)\n", getpid());
printf("操作提示:\n");
printf("1. 按Ctrl+C发送SIGINT信号\n");
printf("2. 在另一个终端执行「kill -2 %d」发送SIGINT信号\n", getpid());
printf("3. 第二次按Ctrl+C将触发默认终止行为\n");

// 无限循环,保持进程运行
while (1) {
printf("运行中...(PID: %d)\n", getpid());
sleep(1);
}

return 0;
}
  1. 第一次按 Ctrl+C 或外部发送kill -2 ,进程执行fun_sig打印信号编号,不终止;

  2. 第二次按 Ctrl+C,因fun_sig已将SIGINT恢复为默认行为,进程会终止;

  3. 若外部发送kill -15 (SIGTERM),因未注册处理函数,进程默认终止。

示例 :mykill.c(模拟系统 kill 命令)

此示例实现一个简易版kill命令(mykill),通过命令行参数指定目标进程 ID 和信号编号,内部调用kill()发送信号,演示kill()在工具开发中的应用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>

// 功能:模拟系统kill命令,向指定进程发送指定信号
// 用法:./mykill <进程ID> <信号编号>
// 示例:./mykill 4322 2 → 向PID=4322的进程发送SIGINT(2号信号)
// ./mykill 4275 9 → 向PID=4275的进程发送SIGKILL(9号信号,强制终止)
// ./mykill 4275 15 → 向PID=4275的进程发送SIGTERM(15号信号,优雅终止)

int main(int argc, char* argv[]) {
// 检查命令行参数数量:需传入2个参数(进程ID、信号编号),总参数数为3
if (argc != 3) {
printf("参数错误!正确用法:./mykill <进程ID> <信号编号>\n");
printf("示例:./mykill 1234 9 (强制终止PID=1234的进程)\n");
exit(1); // 异常退出,退出码1表示参数错误
}

// 将命令行参数(字符串)转换为整数:
// argv[1] → 目标进程ID(pid_t类型,需强制转换)
pid_t pid = (pid_t)atoi(argv[1]);
// argv[2] → 要发送的信号编号(int类型)
int sig = atoi(argv[2]);

// 调用kill()发送信号
if (kill(pid, sig) == -1) {
perror("kill 失败"); // 打印错误原因(如"kill 失败: No such process")
exit(1);
}

// 信号发送成功,打印提示
printf("成功向PID=%d的进程发送信号%d\n", pid, sig);
exit(0); // 正常退出
}
  1. 编译:gcc mykill.c -o mykill;

  2. 查看目标进程 PID:ps aux | grep <程序名>(如ps aux | grep sleep);

  3. 发送信号:./mykill 4275 9(强制终止 PID=4275 的进程,等价于系统命令kill -9 4275);

  4. 若权限不足(如终止 root 进程),需加sudo:sudo ./mykill 123 9。

示例 :fork.c(fork 与子进程回收)

此示例结合fork()创建子进程,通过kill()终止子进程,并演示两种子进程回收机制(避免僵尸进程),核心是利用SIGCHLD信号通知父进程回收资源:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <sys/wait.h> // 包含wait()函数声明,用于回收子进程

// 自定义信号处理函数:子进程终止时触发,手动回收资源
void fun(int sig) {
printf("\n收到信号:sig=%d(SIGCHLD,子进程终止)\n", sig);
// wait(NULL):回收任意一个终止的子进程,忽略退出状态
// 若需获取子进程退出状态,可将NULL替换为int* status
wait(NULL);
printf("子进程资源已手动回收,避免僵尸进程\n");
}

int main() {
int n = 0; // 循环打印次数
char* s = NULL; // 标识进程类型("parent"或"child")

// 子进程回收方式1:忽略SIGCHLD信号(内核自动回收)
// 启用此方式时,子进程终止后内核会自动清理其PCB,无需父进程调用wait()
signal(SIGCHLD, SIG_IGN);

// 子进程回收方式2:注册自定义处理函数(手动回收)
// 启用此方式时,需注释方式1,子进程终止会触发fun(),通过wait()手动回收
// signal(SIGCHLD, fun);

// 创建子进程
pid_t pid = fork();
if (pid == -1) {
perror("fork 失败");
exit(1);
}

if (pid == 0) { // 子进程逻辑:打印3次后等待信号
s = "child";
n = 3;
} else { // 父进程逻辑:打印7次后向子进程发送终止信号
s = "parent";
n = 7;

// 父进程打印3次后,向子进程发送SIGTERM信号
sleep(3);
printf("\n父进程向子进程(PID: %d)发送SIGTERM信号\n", pid);
if (kill(pid, SIGTERM) == -1) {
perror("kill 失败");
exit(1);
}
}

// 循环打印进程标识,每秒1次
for (int i = 0; i < n; i++) {
printf("进程类型:%s,当前PID: %d,第%d次打印\n", s, getpid(), i+1);
sleep(1);
}

exit(0);
}
  1. 子进程终止后,若父进程未回收其 PCB(进程控制块),会变为僵尸进程(状态标记为Z),占用 PID 资源,需通过SIGCHLD信号处理;

  2. 两种回收方式的区别:

  • 方式 1(signal(SIGCHLD, SIG_IGN)):内核自动回收,代码简洁,无需手动调用wait();

  • 方式 2(自定义fun函数):灵活可控,可在回收时通过wait(&status)获取子进程退出状态(如正常退出码 0、异常退出码 128 + 信号编号)。

子进程回收机制与 SIGCHLD 信号

kill()函数常与fork()配合使用(如父进程终止子进程),而子进程终止后若未及时回收,会产生僵尸进程。SIGCHLD信号是 Linux 中专门用于通知父进程回收子进程的机制,是进程管理的核心知识点。

SIGCHLD 信号的作用

SIGCHLD(信号编号 17)是内核自动发送的信号,当子进程发生以下状态变化时,内核会向其父进程发送SIGCHLD:

  • 子进程正常终止(调用exit()或_exit());

  • 子进程异常终止(如被SIGKILL终止、段错误);

  • 子进程被暂停(收到SIGSTOP/SIGTSTP)或恢复(收到SIGCONT)。

父进程通过处理SIGCHLD信号,可及时回收子进程资源,避免僵尸进程。

僵尸进程的产生与危害

  • 产生原因:子进程终止后,其 PCB(包含进程 ID、退出状态等核心信息)会暂时保留,等待父进程调用wait()/waitpid()回收;若父进程未回收且持续运行,子进程会变为僵尸进程(状态Z)。

  • 危害:系统中 PID 数量有限(默认 32768),大量僵尸进程会耗尽 PID 资源,导致新进程无法创建。

通过ps aux | grep Z可查看系统中的僵尸进程,例如(表示僵尸进程):

1
user      1234  0.0  0.0      0     0 pts/0    Z+   10:00   0:00 [sleep] <defunct>

两种子进程回收方式

基于SIGCHLD信号,父进程有两种常用的子进程回收方式,适用于不同场景:

方式 1:忽略 SIGCHLD 信号(内核自动回收)

通过signal(SIGCHLD, SIG_IGN)将SIGCHLD设为忽略,此时内核会在子进程终止后自动回收其 PCB,无需父进程手动调用wait()。

  • 优点:代码简洁,无需处理信号回调,性能高效;

  • 缺点:无法获取子进程的退出状态(如是否正常终止、终止原因);

  • 适用场景:无需关注子进程退出状态的简单场景(如父进程仅需终止子进程,不关心结果)。

方式 2:自定义 SIGCHLD 处理函数(手动回收)

通过signal(SIGCHLD, fun)注册自定义处理函数,子进程终止时触发fun(),在函数中调用wait()/waitpid()手动回收子进程资源。

  • 优点:可获取子进程退出状态,灵活处理不同终止原因(如正常退出、信号终止);

  • 缺点:需编写信号处理函数,需注意wait()的非阻塞调用(避免父进程阻塞);

  • 适用场景:需关注子进程退出状态的场景(如服务器程序,需记录子进程崩溃原因)。

进阶:非阻塞回收多个子进程

若父进程有多个子进程,需用waitpid(-1, &status, WNOHANG)实现非阻塞回收,避免处理函数阻塞父进程:

1
2
3
4
5
6
7
8
9
10
11
12
13
void fun(int sig) {
int status;
// waitpid(-1, ...):回收任意子进程;WNOHANG:无终止子进程时立即返回0
while (waitpid(-1, &status, WNOHANG) > 0) {
if (WIFEXITED(status)) {
// 子进程正常终止,获取退出码
printf("子进程正常终止,退出码:%d\n", WEXITSTATUS(status));
} else if (WIFSIGNALED(status)) {
// 子进程被信号终止,获取终止信号
printf("子进程被信号终止,信号编号:%d\n", WTERMSIG(status));
}
}
}

回收方式对比

回收方式 核心 API 是否获取退出状态 适用场景 代码复杂度
内核自动回收(忽略 SIGCHLD) signal(SIGCHLD, SIG_IGN) 简单场景,无需关注退出状态
手动回收(自定义函数) signal(SIGCHLD, fun) + wait() 复杂场景,需获取退出状态