Linux常用信号(进程间通信)详解
在Linux系统中,信号(Signal) 是一种进程间通信(IPC)机制,用于通知进程发生了某种事件(如用户中断、错误异常、定时器到期等)。信号本质上是一种“软件中断”,它允许内核或其他进程异步地通知目标进程执行预定义的操作(如终止、暂停、忽略等)。
与管道(Pipe)、消息队列等需要显式数据传输的IPC方式不同,信号更轻量,适用于简单的事件通知(而非大量数据传递)。例如:用户按下Ctrl+C终止程序(触发SIGINT)、进程访问非法内存(触发SIGSEGV)、后台进程被挂起后恢复(SIGCONT)等场景,都依赖信号机制实现。
本文将从信号的基本概念出发,详细介绍Linux常用信号、信号处理机制、核心系统调用,并通过实例演示信号在进程通信中的应用,最后总结最佳实践。
目录#
- 信号基本概念
- 信号的生命周期
- 信号的分类
- Linux常用信号详解
- 标准信号(1-31)
- 常用信号表及默认行为
- 信号处理机制
- 默认行为
- 忽略信号
- 自定义信号处理函数
- 核心信号系统调用
kill():发送信号sigaction():注册信号处理函数sigprocmask():阻塞/解除阻塞信号pause():等待信号alarm():定时器信号
- 实践案例
- 案例1:捕获
Ctrl+C(SIGINT)信号 - 案例2:父子进程通过
SIGUSR1通信 - 案例3:信号阻塞保护临界区
- 案例1:捕获
- 信号使用最佳实践
- 参考资料
1. 信号基本概念#
1.1 信号的生命周期#
一个信号从产生到被处理,经历以下阶段:
-
产生(Generation):信号由内核、其他进程或进程自身触发。
- 内核触发:如硬件错误(
SIGSEGV)、定时器到期(SIGALRM)。 - 进程触发:通过
kill()系统调用向其他进程发送信号。 - 自身触发:通过
raise()向自身发送信号,或abort()触发SIGABRT。
- 内核触发:如硬件错误(
-
传递(Delivery):内核将信号发送到目标进程。若进程当前被阻塞(如等待I/O),信号会暂存,待进程恢复运行后传递。
-
未决(Pending):若目标进程暂时无法处理信号(如信号被阻塞),信号会处于“未决”状态,直到阻塞解除。
-
处理(Handling):进程根据预设规则处理信号(默认行为、忽略、自定义处理函数)。
1.2 信号的分类#
Linux信号分为两类:
- 标准信号(Standard Signals):编号1-31,继承自早期Unix,不支持排队(同一信号多次产生可能被合并),携带信息有限。
- 实时信号(Real-Time Signals):编号34-64(
SIGRTMIN至SIGRTMAX),支持排队(多次产生不会丢失),可携带更多信息(如信号发送者PID、附加数据)。
本文重点讨论标准信号(最常用)。
2. Linux常用信号详解#
2.1 标准信号(1-31)#
标准信号的编号和名称在<signal.h>中定义,部分信号有固定含义(如SIGINT),部分可由用户自定义(如SIGUSR1/SIGUSR2)。以下是最常用的标准信号:
2.2 常用信号表及默认行为#
| 信号编号 | 信号名称 | 默认行为 | 描述及触发场景 |
|---|---|---|---|
| 1 | SIGHUP | 终止 | 终端断开连接(如SSH会话关闭),常用于通知守护进程重读配置文件(需自定义处理)。 |
| 2 | SIGINT | 终止 | 用户按下Ctrl+C,请求进程中断。 |
| 3 | SIGQUIT | 终止并生成core dump | 用户按下Ctrl+\,比SIGINT更强制,常用于调试(生成内存转储文件)。 |
| 4 | SIGILL | 终止并生成core dump | 进程执行非法指令(如代码损坏、硬件故障)。 |
| 6 | SIGABRT | 终止并生成core dump | 进程调用abort()函数(如assert断言失败)。 |
| 8 | SIGFPE | 终止并生成core dump | 浮点运算错误(如除零、溢出)。 |
| 9 | SIGKILL | 终止 | 强制终止进程,无法被捕获或忽略(“必杀信号”)。 |
| 11 | SIGSEGV | 终止并生成core dump | 进程访问非法内存地址(如空指针解引用、数组越界)。 |
| 13 | SIGPIPE | 终止 | 向已关闭的管道(Pipe)写入数据(如`echo "a" |
| 14 | SIGALRM | 终止 | alarm()函数设置的定时器到期。 |
| 15 | SIGTERM | 终止 | kill命令默认发送的信号(kill pid等价于kill -15 pid),可被捕获。 |
| 17 | SIGCHLD | 忽略 | 子进程终止或暂停时,内核发送给父进程的通知信号(默认忽略,需自定义处理以避免僵尸进程)。 |
| 19 | SIGSTOP | 暂停进程 | 强制暂停进程,无法被捕获或忽略。 |
| 20 | SIGTSTP | 暂停进程 | 用户按下Ctrl+Z,可被捕获(如自定义暂停逻辑)。 |
| 28 | SIGWINCH | 忽略 | 终端窗口大小改变时触发(如top命令根据窗口大小调整输出)。 |
| 10/12 | SIGUSR1/SIGUSR2 | 终止 | 用户自定义信号,无默认语义,需通过代码实现逻辑(如进程间自定义事件通知)。 |
注意:
SIGKILL(9)和SIGSTOP(19)是唯一无法被捕获、忽略或阻塞的信号,用于强制终止/暂停进程。core dump:默认行为为“终止并生成core dump”时,进程会在当前目录生成core.pid文件(需开启系统core dump限制,ulimit -c unlimited),用于调试。
3. 信号处理机制#
进程对信号的处理有三种方式:
3.1 默认行为(Default Action)#
内核预定义的处理逻辑,如“终止”“暂停”等(见2.2节表格)。
3.2 忽略信号(Ignore)#
进程显式忽略信号(通过代码设置),内核会丢弃该信号,进程无任何反应。
例外:SIGKILL和SIGSTOP无法被忽略。
3.3 自定义信号处理函数(Catch)#
进程注册自定义函数(“信号处理器”),当信号触发时,内核中断当前执行流程,跳转到处理器函数执行,完成后返回原流程。
关键函数:signal()与sigaction()#
注册自定义处理器的常用接口有两个:
-
signal()(简单但功能有限):
原型:void (*signal(int signum, void (*handler)(int)))(int);
作用:为信号signum注册处理器handler(SIG_DFL表示默认,SIG_IGN表示忽略)。
缺点:行为在不同系统中可能不一致(如是否自动重启被中断的系统调用),不推荐用于生产环境。 -
sigaction()(推荐,功能更强大):
原型:int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
作用:为信号signum设置处理动作act,并可通过oldact获取旧动作。
优势:支持设置标志(如SA_RESTART自动重启系统调用)、获取信号发送者信息、原子化设置/获取动作等。
struct sigaction结构体定义:
struct sigaction {
void (*sa_handler)(int); // 简单处理器(无附加信息)
void (*sa_sigaction)(int, siginfo_t *, void *); // 高级处理器(带信号信息)
sigset_t sa_mask; // 处理期间阻塞的信号集
int sa_flags; // 标志(如SA_SIGINFO、SA_RESTART)
void (*sa_restorer)(void); // 已废弃,无需关注
};sa_handler与sa_sigaction二选一:使用SA_SIGINFO标志时,内核会调用sa_sigaction并传入信号详细信息(如发送者PID、信号值等)。sa_mask:处理信号期间,内核会自动阻塞sa_mask中的信号,避免嵌套处理(默认阻塞当前信号本身,除非设置SA_NODEFER标志)。
4. 核心信号系统调用#
4.1 kill():发送信号#
向指定进程/进程组发送信号。
原型:int kill(pid_t pid, int sig);
参数:
pid:目标进程ID(>0:指定进程;0:当前进程组;-1:所有进程;< -1:进程组ID为-pid)。sig:信号编号(0为“空信号”,用于检测进程是否存在)。
返回值:成功返回0,失败返回-1(如目标进程不存在)。
示例:
// 向进程1234发送SIGTERM(终止信号)
kill(1234, SIGTERM);
// 向当前进程组所有进程发送SIGKILL
kill(0, SIGKILL);4.2 sigaction():注册信号处理器#
如3.3节所述,sigaction()是设置信号处理逻辑的推荐接口。
示例:注册SIGINT处理器,打印提示后退出:
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
void sigint_handler(int signum) {
printf("捕获到SIGINT(%d),程序即将退出...\n", signum);
exit(0); // 退出进程
}
int main() {
struct sigaction act;
act.sa_handler = sigint_handler; // 设置处理器函数
sigemptyset(&act.sa_mask); // 处理期间不阻塞其他信号
act.sa_flags = 0; // 默认标志
// 为SIGINT注册处理器
if (sigaction(SIGINT, &act, NULL) == -1) {
perror("sigaction failed");
return 1;
}
while (1); // 无限循环,等待信号
return 0;
}4.3 sigprocmask():阻塞/解除阻塞信号#
控制进程的“信号掩码”(阻塞信号集),暂时阻止指定信号被传递。
原型:int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
参数:
how:操作类型(SIG_BLOCK:添加到掩码;SIG_UNBLOCK:从掩码移除;SIG_SETMASK:替换掩码)。set:待操作的信号集(NULL表示不修改)。oldset:输出旧信号集(NULL表示不获取)。
示例:阻塞SIGINT,执行临界区后解除阻塞:
sigset_t mask, oldmask;
sigemptyset(&mask);
sigaddset(&mask, SIGINT); // 要阻塞的信号:SIGINT
// 阻塞SIGINT,并保存旧掩码
if (sigprocmask(SIG_BLOCK, &mask, &oldmask) == -1) {
perror("sigprocmask failed");
return 1;
}
// 临界区:执行期间不会被SIGINT中断
printf("执行临界区...\n");
sleep(5); // 模拟耗时操作
// 恢复旧掩码(解除SIGINT阻塞)
if (sigprocmask(SIG_SETMASK, &oldmask, NULL) == -1) {
perror("sigprocmask failed");
return 1;
}4.4 pause():等待信号#
使进程暂停运行,直到收到一个未被阻塞的信号(若信号被忽略,进程继续暂停;若信号终止进程,pause()不返回;若信号被捕获,处理器执行后pause()返回-1并设置errno=EINTR)。
原型:int pause(void);
示例:等待任意信号唤醒:
printf("等待信号...\n");
pause(); // 阻塞直到收到信号
printf("被信号唤醒!\n"); // 若信号被捕获,处理器执行后返回此处4.5 alarm():定时器信号#
设置一个定时器,到期后内核向进程发送SIGALRM信号。
原型:unsigned int alarm(unsigned int seconds);
返回值:剩余秒数(若之前有未到期的定时器),否则0。
示例:5秒后触发SIGALRM:
#include <signal.h>
#include <stdio.h>
void handle_alarm(int signum) {
printf("5秒到!\n");
}
int main() {
signal(SIGALRM, handle_alarm);
alarm(5); // 5秒后发送SIGALRM
pause(); // 等待信号
return 0;
}5. 实践案例#
案例1:捕获Ctrl+C(SIGINT)信号#
默认情况下,Ctrl+C触发SIGINT,进程直接终止。以下代码自定义处理逻辑,要求用户确认后再退出:
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>
void sigint_handler(int signum) {
char buf[32];
printf("\n确定要退出吗?(y/n) ");
fflush(stdout); // 刷新缓冲区(printf默认行缓冲,此处无换行需手动刷新)
// 读取用户输入(注意:read是异步信号安全的,printf不是,但此处简化演示)
if (read(STDIN_FILENO, buf, sizeof(buf)) > 0 && (buf[0] == 'y' || buf[0] == 'Y')) {
printf("退出中...\n");
exit(0);
} else {
printf("继续运行...\n");
}
}
int main() {
struct sigaction act;
act.sa_handler = sigint_handler;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
sigaction(SIGINT, &act, NULL);
while (1) {
printf("运行中... (按下Ctrl+C测试)\n");
sleep(1);
}
return 0;
}运行效果:
按下Ctrl+C后,程序不会直接退出,而是提示用户确认,输入y才退出。
案例2:父子进程通过SIGUSR1通信#
SIGUSR1/SIGUSR2是用户自定义信号,可用于进程间传递自定义事件。以下示例中,父进程创建子进程后,向子进程发送SIGUSR1,子进程捕获并打印消息:
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>
// 子进程的SIGUSR1处理器
void child_handler(int signum) {
printf("子进程(PID: %d)收到SIGUSR1信号!\n", getpid());
}
int main() {
pid_t pid = fork();
if (pid == -1) {
perror("fork failed");
return 1;
}
if (pid == 0) { // 子进程
struct sigaction act;
act.sa_handler = child_handler;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
sigaction(SIGUSR1, &act, NULL); // 注册处理器
printf("子进程(PID: %d)等待信号...\n", getpid());
pause(); // 等待父进程信号
exit(0);
} else { // 父进程
sleep(1); // 等待子进程准备就绪
printf("父进程(PID: %d)向子进程(PID: %d)发送SIGUSR1...\n", getpid(), pid);
kill(pid, SIGUSR1); // 发送信号
wait(NULL); // 等待子进程退出
printf("子进程已退出\n");
}
return 0;
}运行输出:
子进程(PID: 12345)等待信号...
父进程(PID: 12344)向子进程(PID: 12345)发送SIGUSR1...
子进程(PID: 12345)收到SIGUSR1信号!
子进程已退出
案例3:信号阻塞保护临界区#
在多线程/多进程环境中,若某段代码(如写文件)不希望被信号中断,可通过sigprocmask()阻塞信号,执行完毕后解除阻塞:
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
void sigint_handler(int signum) {
printf("\n捕获到SIGINT,但先完成文件写入!\n");
}
int main() {
// 注册SIGINT处理器(确保信号不被忽略)
signal(SIGINT, sigint_handler);
int fd = open("test.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd == -1) {
perror("open failed");
return 1;
}
sigset_t mask, oldmask;
sigemptyset(&mask);
sigaddset(&mask, SIGINT); // 阻塞SIGINT
// 开始临界区:阻塞SIGINT
if (sigprocmask(SIG_BLOCK, &mask, &oldmask) == -1) {
perror("sigprocmask failed");
return 1;
}
// 写入文件(期间按Ctrl+C不会中断)
const char *msg = "这是一段需要完整写入的内容\n";
write(fd, msg, strlen(msg));
printf("文件写入中...(按Ctrl+C测试)\n");
sleep(5); // 模拟耗时写入
// 结束临界区:解除SIGINT阻塞
if (sigprocmask(SIG_SETMASK, &oldmask, NULL) == -1) {
perror("sigprocmask failed");
return 1;
}
close(fd);
printf("文件写入完成!\n");
return 0;
}运行效果:
执行程序后5秒内按下Ctrl+C,信号会被阻塞,直到文件写入完成后才触发处理器函数。
6. 信号使用最佳实践#
-
优先使用
sigaction()而非signal()
signal()在不同系统中的行为不一致(如是否自动重启被中断的系统调用),sigaction()是POSIX标准接口,功能更全面(支持设置标志、获取旧动作等)。 -
信号处理器中仅使用异步信号安全函数
信号处理是异步的(可能在任意时刻中断进程),因此处理器函数中严禁调用非异步信号安全的函数(如printf、malloc、fopen等,这些函数可能导致数据竞争或死锁)。
安全函数列表见man 7 signal-safety,常用的有:write()、_exit()、signal()、sigprocmask()等。
示例:用write(STDOUT_FILENO, ...)替代printf:void handler(int signum) { const char *msg = "信号触发\n"; write(STDOUT_FILENO, msg, strlen(msg)); // 安全 } -
处理被中断的系统调用(
EINTR)
某些系统调用(如read、write、sleep)在收到信号时会中断并返回-1,同时设置errno=EINTR。需在代码中处理这种情况,例如重试系统调用:while ((n = read(fd, buf, sizeof(buf))) == -1 && errno == EINTR);或通过
sigaction的SA_RESTART标志自动重启被中断的系统调用:act.sa_flags = SA_RESTART; // 自动重启部分系统调用 -
谨慎使用信号阻塞
长时间阻塞信号可能导致信号丢失(标准信号不排队),仅在临界区(如文件写入、数据更新)短暂阻塞,并及时解除。 -
避免在信号处理器中执行复杂逻辑
信号处理器应尽量简洁(如设置全局标志),复杂逻辑(如日志记录、状态更新)应放在主程序中通过轮询标志处理:volatile sig_atomic_t flag = 0; // 全局标志(需用volatile和sig_atomic_t确保原子性) void handler(int signum) { flag = 1; // 处理器仅设置标志 } int main() { // ... 注册处理器 ... while (1) { if (flag) { // 主程序中处理复杂逻辑 printf("处理信号事件\n"); flag = 0; } sleep(1); } }volatile:防止编译器优化将变量缓存到寄存器,确保每次读取最新值。sig_atomic_t:确保标志的读写是原子操作,避免数据竞争。
-
自定义信号(
SIGUSR1/SIGUSR2)需明确文档化
若使用SIGUSR1/SIGUSR2实现自定义逻辑,需在代码或文档中明确说明信号的用途(如“SIGUSR1用于触发配置重载”),避免与其他组件冲突。
参考资料#
- Linux man-pages: signal(7)(信号概述)
- Linux man-pages: sigaction(2)(
sigaction系统调用) - Linux man-pages: signal-safety(7)(异步信号安全函数列表)
- 《The Linux Programming Interface》(Michael Kerrisk):第20-22章详细讲解信号机制。
- 《UNIX环境高级编程》(W. Richard Stevens):第10章信号相关内容。
通过本文,相信你已对Linux信号机制有了深入理解。信号作为轻量级IPC工具,在进程控制、异常处理等场景中不可或缺,但也需注意其异步特性带来的复杂性。合理使用信号,可让程序更健壮、更灵活。