引言
在 Linux 系统中,信号(Signal)作为一种异步通信机制,扮演着进程间交互与系统事件通知的关键角色。它如同软件层面的 "中断",能够打断进程的正常执行流,触发预设的处理逻辑。本文将从信号的基本概念出发,结合代码示例与实战场景,深入解析信号的发送、处理、屏蔽等核心机制,帮助读者掌握 Linux 信号的全流程管理。
一、信号基础:软件中断的本质
1. 信号的定义与作用
信号 是 Linux 系统中用于异步通知进程的事件机制,本质是一个整数(信号编号)。它可以由硬件事件(如键盘中断)、系统内核(如内存访问错误)或用户进程(如kill
命令)产生,用于:
-
通知进程异步事件(如文件就绪、定时器超时)。
-
强制进程终止或暂停(如
SIGKILL
、SIGSTOP
)。 -
实现进程间通信(轻量级 IPC)。
2. 信号的命名与分类
-
编号与名称 :通过
kill -l
命令可查看 62 个信号,前 31 个为非实时信号 (不可靠,可能丢失),后 31 个为实时信号(可靠,按顺序排队处理)。 -
常用信号
信号编号 名称 描述 2 SIGINT
Ctrl+C 终止进程(可捕获、忽略) 9 SIGKILL
强制终止进程(不可捕获、忽略) 14 SIGALRM
闹钟超时(常用于定时任务) 17 SIGCHLD
子进程状态改变(如终止、暂停,用于父进程回收僵尸) 15 SIGTERM
优雅终止进程(默认处理,可捕获)
3. 信号的生命周期
-
产生 :通过硬件、内核或
kill
/raise
函数触发。 -
未决(Pending):信号已产生但未被处理,存储于进程的未决信号集。
-
递送(Delivery):信号被递送给进程,触发处理逻辑(默认、忽略或捕获)。
二、信号处理:捕获、忽略与默认
1. 信号处理方式
-
默认处理 :系统预设行为(如
SIGINT
默认终止进程)。 -
忽略处理 :进程对信号不做响应(
SIGKILL
和SIGSTOP
不可忽略)。 -
捕获处理:进程通过自定义函数处理信号(需注册信号处理函数)。
2. 信号处理函数注册
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
-
参数
-
signum
:信号编号。 -
handler
:处理方式(SIG_IGN
忽略,SIG_DFL
默认,或自定义函数指针)。
-
-
返回值 :成功返回旧处理方式,失败返回
SIG_ERR
。
示例:捕获SIGINT
信号
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void sigint_handler(int signum) {
printf("Caught SIGINT (signal %d)\n", signum);
}
int main() {
signal(SIGINT, sigint_handler); // 注册捕获函数
printf("Press Ctrl+C to trigger SIGINT...\n");
while (1) sleep(1);
return 0;
}
三、僵尸进程与信号驱动回收
1. 僵尸进程的成因
子进程终止后未被父进程回收,残留进程描述符(状态为Z
),占用系统资源。
2. SIGCHLD
信号与异步回收
-
子进程状态改变时,内核向父进程发送
SIGCHLD
信号。 -
父进程通过捕获该信号,在处理函数中调用
waitpid
回收僵尸。
示例:信号驱动回收僵尸进程
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
void sigchld_handler(int signum) {
while (waitpid(-1, NULL, WNOHANG) > 0) { // 非阻塞回收所有僵尸
printf("Zombie reaped\n");
}
}
int main() {
signal(SIGCHLD, sigchld_handler); // 注册信号处理函数
for (int i = 0; i < 3; i++) {
if (fork() == 0) { // 子进程
_exit(0); // 立即终止,成为僵尸
}
}
while (1) sleep(1); // 父进程循环等待
return 0;
}
四、信号发送与进程控制
1. 命令行发送信号:kill
kill [-信号编号或名称] PID # 向指定PID进程发送信号
kill -9 PID # 强制终止进程(SIGKILL)
kill -SIGTERM PID # 优雅终止进程(默认信号)
2. 系统调用发送信号
kill()
:向指定进程发送信号
#include <signal.h>
int kill(pid_t pid, int signum);
-
pid > 0
:发送给指定 PID 进程。 -
pid = -1
:发送给所有有权限的进程(慎用)。
raise()
:向自身发送信号
#include <signal.h>
int raise(int signum); // 等价于 kill(getpid(), signum)
示例:父子进程信号交互
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
int main() {
pid_t pid = fork();
if (pid == 0) { // 子进程
sleep(2);
printf("Child sending SIGUSR1 to parent (PID %d)\n", getppid());
kill(getppid(), SIGUSR1); // 向父进程发送自定义信号
} else { // 父进程
signal(SIGUSR1, [](int s) { printf("Parent received SIGUSR1\n"); });
printf("Parent waiting for signal...\n");
pause(); // 阻塞等待信号
}
return 0;
}
五、信号与进程状态控制
1. 暂停与唤醒进程
pause()
:无限阻塞直到信号到达
#include <unistd.h>
int pause(); // 返回-1,errno设为EINTR(被信号中断)
kill -STOP/CONT
:暂停与恢复进程
kill -STOP PID # 暂停进程(状态变为T)
kill -CONT PID # 恢复进程(状态变为S/R)
2. 定时器与闹钟:alarm
与sleep
alarm(seconds)
:设置定时器,到期发送SIGALRM
#include <unistd.h>
unsigned int alarm(unsigned int seconds); // 返回旧闹钟剩余时间
sleep(seconds)
:睡眠并响应信号
#include <unistd.h>
unsigned int sleep(unsigned int seconds); // 提前唤醒返回剩余秒数
六、信号集与屏蔽机制
1. 信号集操作
信号集(sigset_t
)用于批量管理信号,常见操作:
#include <signal.h>
sigset_t set;
sigemptyset(&set); // 清空信号集
sigaddset(&set, SIGINT); // 添加信号
sigdelset(&set, SIGQUIT);// 删除信号
int is_member = sigismember(&set, SIGINT); // 检查信号是否存在
2. 信号屏蔽:sigprocmask
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
-
how
-
SIG_BLOCK
:添加信号到掩码(阻塞未决)。 -
SIG_UNBLOCK
:从掩码中移除信号(允许递送)。 -
SIG_SETMASK
:设置新掩码。
-
示例:屏蔽信号确保临界区安全
#include <stdio.h>
#include <signal.h>
void critical_section() {
sigset_t mask, old_mask;
sigemptyset(&mask);
sigaddset(&mask, SIGINT); // 屏蔽SIGINT
sigprocmask(SIG_BLOCK, &mask, &old_mask); // 应用屏蔽
printf("Entering critical section (masked SIGINT)\n");
sleep(5); // 模拟临界操作
sigprocmask(SIG_SETMASK, &old_mask, NULL); // 恢复旧掩码
printf("Exited critical section\n");
}
int main() {
critical_section();
return 0;
}
七、信号的继承与恢复
1. fork
后的信号处理继承
子进程继承父进程的信号处理方式:
-
父进程捕获的信号,子进程同样捕获。
-
父进程忽略的信号,子进程同样忽略。
2. exec
后的信号处理重置
新进程(exec
创建)的信号处理恢复为默认,除了被忽略的信号(继续忽略)。
八、实战场景:信号的典型应用
1. 定时任务:闹钟与信号结合
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void alarm_handler(int signum) {
printf("Alarm triggered!\n");
alarm(2); // 重置闹钟
}
int main() {
signal(SIGALRM, alarm_handler);
alarm(2); // 设置2秒闹钟
while (1) {
printf("Working...\n");
sleep(1);
}
return 0;
}
2. 优雅退出:捕获SIGTERM
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
volatile int running = 1;
void sigterm_handler(int signum) {
printf("Received SIGTERM, shutting down...\n");
running = 0;
}
int main() {
signal(SIGTERM, sigterm_handler);
while (running) {
printf("Running...\n");
sleep(1);
}
printf("Exited gracefully\n");
return 0;
}
九、总结:信号机制的核心脉络
模块 | 关键知识点 |
---|---|
基础概念 | 信号编号、分类(实时 / 非实时)、生命周期(产生 - 未决 - 递送)。 |
处理方式 | 默认、忽略、捕获(signal 函数),信号处理函数的异步执行。 |
僵尸回收 | SIGCHLD 信号驱动,waitpid 非阻塞回收。 |
发送与控制 | kill 命令、kill() /raise() 函数,进程暂停(STOP )与恢复(CONT )。 |
高级特性 | 信号集(sigset_t )、屏蔽机制(sigprocmask )、定时与临界区保护。 |
信号机制是 Linux 系统的重要组成部分,合理运用信号能显著提升程序的健壮性与交互性。在实际开发中,需注意信号的异步特性、不可靠信号的潜在丢失问题,以及多信号处理时的竞态条件,确保系统稳定运行。