Linux信号的使用
进程间异步通信的核心机制
信号(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 |
信号的生命周期
信号从“产生”到“递达”需经历三个阶段:产生(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+C 后 SIGINT 会进入未决状态;当进程解除 SIGINT 的阻塞后,该信号才会递达并被处理。
信号递达(Deliver)
进程从“未决信号集”中取出信号并处理的过程称为“递达”。进程对信号的处理方式有三种,优先级从高到低依次为:
强制默认行为(不可修改)
仅针对 SIGKILL(9)和 SIGSTOP(19),这两个信号无法被忽略、捕获或修改处理方式,确保系统能强制控制进程(如终止无响应进程、暂停恶意进程)。
自定义处理(捕获信号)
进程通过 signal() 或 sigaction() 函数为信号注册“自定义处理函数”,信号递达时会执行该函数。例如,为 SIGTERM 注册处理函数,让进程在收到终止信号时先保存数据再退出。
忽略信号
进程明确指定对信号不做任何处理(通过 signal(sig, SIG_IGN) 实现)。除 SIGKILL 和 SIGSTOP 外,其他信号均可被忽略。例如,忽略 SIGCHLD 信号(默认行为),但需注意:若父进程忽略 SIGCHLD,子进程终止后会直接被内核回收,不会成为僵尸进程。
信号处理的基本操作
Linux 提供了一系列系统调用用于信号的发送、注册和等待,以下是核心操作的详细说明及示例代码。
发送信号:kill() 与 raise()
kill() 函数:向指定进程发送信号
kill() 是最常用的信号发送函数,可向任意进程(需权限)发送指定信号,原型如下:
1 |
|
参数说明:
pid:目标进程/进程组的标识,支持四种取值:pid > 0:向 PID 为pid的单个进程发送信号;pid = 0:向当前进程所在的“进程组”内所有进程发送信号;pid = -1:向系统中所有有权限发送的进程发送信号(谨慎使用,可能影响系统稳定性);pid < -1:向进程组 ID 为-pid的所有进程发送信号(如pid = -1234表示向进程组 1234 的所有进程发送信号)。
sig:要发送的信号(如SIGTERM、SIGKILL,取值为信号编号或宏定义)。
返回值:
- 成功:返回 0;
- 失败:返回 -1,并设置
errno(如EPERM表示无权限发送,ESRCH表示目标进程不存在)。
示例代码:父进程向子进程发送 SIGTERM 信号
1 |
|
raise() 函数:向当前进程发送信号
raise() 是简化版的 kill(),仅能向当前进程发送信号,原型如下:
1 |
|
功能:
等价于 kill(getpid(), sig),无需指定进程 ID,适合进程向自身发送信号(如异常时自我终止)。
示例代码:进程向自身发送 SIGINT 信号
1 |
|
注册信号处理函数:signal() 与 sigaction()
signal() 函数:简单注册处理函数
signal() 是最基础的信号处理注册函数,用于为指定信号绑定自定义处理函数,原型如下:
1 |
|
参数说明:
sig:要注册处理函数的信号(如SIGINT、SIGTERM,SIGKILL/SIGSTOP除外);handler:信号的处理方式,支持三种取值:- 自定义函数:符合
sighandler_t类型的函数(如void handle_sig(int sig)),信号递达时执行; SIG_IGN:宏,表示“忽略该信号”;SIG_DFL:宏,表示“恢复该信号的系统默认行为”。
- 自定义函数:符合
返回值:
- 成功:返回之前注册的处理函数(若之前未注册,返回
SIG_DFL); - 失败:返回
SIG_ERR(通常是(sighandler_t)-1),并设置errno。
示例代码:捕获 SIGINT 信号,自定义处理逻辑
1 |
|
注意:signal() 的局限性
signal() 是早期接口,存在兼容性问题(不同系统对信号重注册的处理不同),且无法处理“可靠信号”(34+)的排队问题。在复杂场景下,建议使用更强大的 sigaction() 函数。
sigaction() 函数:可靠的信号注册
sigaction() 是 signal() 的增强版,支持更精细的信号控制(如设置信号掩码、获取信号信息),且兼容可靠信号,原型如下:
1 |
|
核心结构体 struct sigaction:
1 | struct sigaction { |
示例代码:用 sigaction() 捕获 SIGCHLD 回收子进程
1 |
|
等待信号:pause() 函数
pause() 函数使当前进程暂停运行,直到收到一个“非阻塞且未被忽略”的信号。若信号被捕获且处理函数返回,进程会继续执行;若信号的默认行为是终止或暂停,进程会遵循默认行为。
原型:
1 |
|
返回值:
pause() 始终返回 -1,并设置 errno 为 EINTR(表示被信号中断)——因为只有收到信号后进程才会恢复,不会正常返回。
示例代码:pause() 配合 signal() 等待信号
1 |
|
操作步骤:
- 编译运行程序,终端显示进程 PID(如 5678);
- 打开另一个终端,执行
kill -USR1 5678(向进程发送SIGUSR1信号); - 原终端会打印信号处理信息,进程继续执行并退出。
信号的注意事项
在使用信号时,需关注以下细节,避免出现信号丢失、进程崩溃或资源泄漏等问题。
不可靠信号与可靠信号
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 | void handle_sigint(int sig) { |
正确示例(异步安全):
1 | void handle_sigint(int sig) { |
禁止捕获的信号
SIGKILL(9)和 SIGSTOP(19)是 Linux 中唯一两个“不可捕获、不可忽略、不可修改处理方式”的信号,原因如下:
SIGKILL:确保系统管理员能强制终止任何失控进程(如死循环、无响应进程);SIGSTOP:确保能临时冻结进程(如调试时暂停进程),避免进程规避调试。
任何尝试为这两个信号注册处理函数的操作都会失败,例如:
1 | // 尝试捕获 SIGKILL,会失败 |
信号与进程状态的交互
当进程处于以下状态时,收到信号的处理逻辑会有所不同:
阻塞状态(如 sleep()、wait()、read() 等待 I/O)
进程会立即被唤醒,优先处理信号。若信号处理函数返回,进程会继续执行原阻塞操作(如 sleep(5) 被信号中断后,会返回剩余睡眠时间,或重新进入阻塞)。
暂停状态(SIGSTOP/SIGTSTP 触发)
只有 SIGCONT 信号能恢复进程运行,其他信号会被暂存(进入未决状态),直到进程恢复后再递达。
僵尸状态
僵尸进程已终止,仅保留 PCB 信息,无法接收或处理任何信号。只有父进程调用 wait() 回收后,僵尸进程才会消失。
信号的实际应用场景
信号在 Linux 系统和应用开发中应用广泛,以下是几个典型场景:
进程间自定义通信
通过 SIGUSR1 和 SIGUSR2 两个用户自定义信号,实现进程间简单的状态同步。例如:
- 进程 A 完成数据准备后,向进程 B 发送
SIGUSR1信号; - 进程 B 收到信号后,开始读取数据并处理。
子进程回收
父进程注册 SIGCHLD 信号处理函数,在子进程终止时自动调用 waitpid() 回收,避免僵尸进程。这是服务器程序中常用的模式(如 Nginx、Apache 等多进程服务器)。
定时器与超时控制
通过 alarm(seconds) 函数设置定时器,seconds 秒后向进程发送 SIGALRM 信号。例如:
- 网络编程中,设置 5 秒超时,若 5 秒内未收到数据,触发
SIGALRM信号,关闭连接。
程序优雅退出
捕获 SIGTERM 信号(kill 命令默认信号),在处理函数中执行资源清理操作(如关闭文件、释放内存、保存配置),让程序优雅退出,避免数据丢失。例如:
1 | void handle_sigterm(int sig) { |
修改信号的响应方式
在 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 是系统定义的宏,表示 “忽略该信号”;
适用场景:
避免程序被无关信号中断,如后台服务进程忽略 SIGINT(禁止用户用 Ctrl+C 终止);
防止特定信号导致程序异常退出,如忽略 SIGPIPE(当管道读端已关闭、写端仍写入时,默认会终止程序,忽略后可自定义处理写失败逻辑);
- 限制:SIGKILL(信号 9)和 SIGSTOP(信号 19)无法被忽略,这两个信号是系统强制管控进程的 “最终手段”,确保能终止或暂停失控进程。
自定义处理(捕获信号)
自定义处理(又称 “捕获信号”)是指进程通过注册自定义函数,让信号递达时执行该函数(替代内核默认行为),灵活实现业务逻辑(如资源清理、状态同步)。
触发方式:通过 signal(sig, handler) 注册,其中 handler 为用户定义的函数指针,函数原型需符合 void (*sighandler_t)(int)(参数为信号编号,无返回值);
适用场景:
实现程序 “优雅退出”:捕获 SIGTERM 信号,在进程终止前执行资源清理(如关闭文件、释放动态内存、保存配置数据);
自动回收子进程:捕获 SIGCHLD 信号,当子进程终止时调用 waitpid() 回收资源,避免僵尸进程;
自定义事件响应:通过 SIGUSR1/SIGUSR2 等用户自定义信号,实现进程间简单的状态同步(如进程 A 完成数据准备后,向进程 B 发送 SIGUSR1 触发数据读取);
- 实现限制:
SIGKILL 和 SIGSTOP 无法被捕获,确保系统能强制终止或暂停任何进程;
推荐使用 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 | // 正确:使用异步安全函数 write() 输出信息 |
信号的可重入性问题
若同一信号在处理过程中再次触发(如信号处理函数执行时,再次收到相同信号),会导致处理函数 “嵌套执行”,引发数据不一致(不可重入)。
问题示例
1 | int global_count = 0; |
通过 sigprocmask() 函数解决,在处理信号时 “阻塞同类信号”,避免嵌套执行:
1 |
|
默认行为的恢复
若已通过 signal() 自定义信号的响应方式,可通过 signal(sig, SIG_DFL) 恢复该信号的内核默认行为。
适用场景
临时修改信号响应:如程序启动时忽略 SIGINT,完成初始化后恢复默认终止行为;
动态调整逻辑:如子进程继承父进程的信号处理方式后,按需恢复默认行为。
1 |
|
调整信号响应方式
以下通过具体代码示例,展示如何使用 signal() 函数实现 “忽略信号”“自定义处理”“恢复默认行为” 三种场景。
忽略信号(以 SIGPIPE 为例)
SIGPIPE 信号通常在 “管道读端已关闭、写端仍写入” 时触发,默认行为是终止程序。忽略该信号可避免程序意外退出,便于自定义处理写失败逻辑。
1 |
|
signal(SIGPIPE, SIG_IGN):将 SIGPIPE 信号的响应方式设为 “忽略”;
pause():使进程暂停运行,直到收到一个非忽略的信号(此处用于保持进程存活,观察信号处理效果)。
自定义处理(以 SIGINT 为例)
SIGINT 信号由 Ctrl+C 触发,默认行为是终止进程。通过自定义处理函数,可实现 “按 Ctrl+C 不退出,仅输出提示” 的逻辑。
1 |
|
sighandler_t:信号处理函数的类型定义(typedef void (*sighandler_t)(int)),用于接收 signal() 的返回值;
old_handler:存储之前的信号处理方式(如默认行为 SIG_DFL),可用于后续恢复。
恢复默认行为(以 SIGINT 为例)
先自定义 SIGINT 的处理方式,一段时间后恢复默认行为,实现 “临时忽略 Ctrl+C,之后允许终止” 的逻辑。
1 |
|
程序启动后,按 Ctrl+C 会输出提示,不终止;
3 秒后,再次按 Ctrl+C,进程会遵循默认行为终止。
完整示例:main.c
1 |
|
初始阶段: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 |
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 | if (kill(1234, SIGTERM) == -1) { |
kill () 的实际应用
以下通过多个示例代码,展示kill()函数在不同场景下的使用,包括父子进程通信、模拟系统kill命令、结合fork()的子进程回收等。
父子进程间发送信号
此示例通过fork()创建子进程,父进程使用kill()向子进程发送SIGTERM(优雅终止)或SIGKILL(强制终止)信号,演示信号发送的基本流程:
1 |
|
编译运行后,子进程启动并打印 PID;
父进程等待 1 秒后发送信号,子进程收到SIGTERM后默认终止(若发送SIGKILL,子进程会立即强制终止);
可通过ps -p <子进程PID>验证子进程是否已终止。
示例 :main.c(信号处理与 kill 配合)
此示例结合signal()注册信号处理函数,演示如何通过kill()向进程自身发送信号,或在外部通过kill命令触发自定义处理逻辑:
1 |
|
第一次按 Ctrl+C 或外部发送kill -2
,进程执行fun_sig打印信号编号,不终止; 第二次按 Ctrl+C,因fun_sig已将SIGINT恢复为默认行为,进程会终止;
若外部发送kill -15
(SIGTERM),因未注册处理函数,进程默认终止。
示例 :mykill.c(模拟系统 kill 命令)
此示例实现一个简易版kill命令(mykill),通过命令行参数指定目标进程 ID 和信号编号,内部调用kill()发送信号,演示kill()在工具开发中的应用:
1 |
|
编译:gcc mykill.c -o mykill;
查看目标进程 PID:ps aux | grep <程序名>(如ps aux | grep sleep);
发送信号:./mykill 4275 9(强制终止 PID=4275 的进程,等价于系统命令kill -9 4275);
若权限不足(如终止 root 进程),需加sudo:sudo ./mykill 123 9。
示例 :fork.c(fork 与子进程回收)
此示例结合fork()创建子进程,通过kill()终止子进程,并演示两种子进程回收机制(避免僵尸进程),核心是利用SIGCHLD信号通知父进程回收资源:
1 |
|
子进程终止后,若父进程未回收其 PCB(进程控制块),会变为僵尸进程(状态标记为Z),占用 PID 资源,需通过SIGCHLD信号处理;
两种回收方式的区别:
方式 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 | void fun(int sig) { |
回收方式对比
| 回收方式 | 核心 API | 是否获取退出状态 | 适用场景 | 代码复杂度 |
|---|---|---|---|---|
| 内核自动回收(忽略 SIGCHLD) | signal(SIGCHLD, SIG_IGN) | 否 | 简单场景,无需关注退出状态 | 低 |
| 手动回收(自定义函数) | signal(SIGCHLD, fun) + wait() | 是 | 复杂场景,需获取退出状态 | 中 |




