【Linux】进程信号
一、信号的基本认知
1.1 生活中的信号类比
信号的处理流程可以用"收快递"来理解:
- 识别信号:你知道快递来了该怎么处理(进程内置信号识别能力)。
- 信号产生:快递员通知你快递到了(信号由内核或其他进程发送)。
- 信号保存:你正在打游戏,暂时没法取,先记住有快递(信号未决状态)。
- 信号处理:游戏结束后取快递,处理方式有三种------默认(自己用)、忽略(放一边)、自定义(送给朋友)。
核心结论:
- 信号是异步的,进程无法预知信号何时到达。
- 信号的处理方法在信号产生前已确定(默认/忽略/自定义)。
- 信号不一定立即处理,会在进程执行的"合适时机"递达。
1.2 技术视角的信号本质
信号是进程之间事件通知的方式,属于软中断,用于处理异步事件。例如:
- 用户按下Ctrl+C,内核会向前台进程发送
SIGINT(2号信号),默认动作是终止进程。 - 程序执行除零操作,CPU检测到异常后,内核会向进程发送
SIGFPE(8号信号),默认动作是终止进程并生成core dump。
1.3 查看系统信号
Linux系统支持64种信号(34以下为常规信号,34及以上为实时信号),可通过kill -l命令查看:
bash
kill -l
# 输出示例(部分):
# 1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL
# 5) SIGTRAP 6) SIGABRT 7) SIGBUS 8) SIGFPE
# 9) SIGKILL 10) SIGUSR1 11) SIGSEGV 12) SIGUSR2
# 13) SIGPIPE 14) SIGALRM 15) SIGTERM 17) SIGCHLD
关键信号说明:
| 信号 | 编号 | 默认动作 | 场景 |
|---|---|---|---|
| SIGINT | 2 | 终止进程 | Ctrl+C触发 |
| SIGQUIT | 3 | 终止进程+Core Dump | Ctrl+\触发 |
| SIGKILL | 9 | 强制终止进程 | 无法捕捉/忽略 |
| SIGSEGV | 11 | 终止进程+Core Dump | 非法内存访问 |
| SIGCHLD | 17 | 忽略 | 子进程终止/停止时通知父进程 |
| SIGALRM | 14 | 终止进程 | alarm函数超时触发 |
可通过man 7 signal查看每个信号的详细说明。
二、信号的产生方式
信号的产生源于硬件事件、软件条件或用户操作,主要有以下5种方式:
2.1 终端按键产生信号
通过键盘输入特定组合键,内核会将其解释为信号并发送给前台进程:
- Ctrl+C:发送
SIGINT(2号),默认终止进程。 - Ctrl+\:发送
SIGQUIT(3号),默认终止进程并生成core dump文件(用于调试)。 - Ctrl+Z:发送
SIGTSTP(20号),默认暂停进程(挂起至后台)。
实战验证 :捕捉SIGQUIT信号
cpp
#include <iostream>
#include <unistd.h>
#include <signal.h>
void handler(int signo) {
std::cout << "进程[" << getpid() << "]捕获到信号:" << signo << "(SIGQUIT)" << std::endl;
}
int main() {
std::cout << "进程PID:" << getpid() << ",等待信号(按Ctrl+\\触发)" << std::endl;
signal(SIGQUIT, handler); // 自定义捕捉SIGQUIT
while (true) sleep(1); // 死循环等待信号
return 0;
}
运行结果:
bash
./sig_quit
进程PID:12345,等待信号(按Ctrl+\触发)
^\进程[12345]捕获到信号:3(SIGQUIT) # 按下Ctrl+\后输出
2.2 命令行发送信号
使用kill命令向指定进程发送信号,格式为kill -信号编号/名称 进程PID:
bash
# 向PID为12345的进程发送SIGKILL(9号),强制终止
kill -9 12345
# 等价于
kill -SIGKILL 12345
实战验证 :向后台进程发送SIGSEGV(11号,段错误信号)
bash
# 后台运行死循环程序
./sig_loop &
# 查看进程PID
ps ajx | grep sig_loop
# 发送SIGSEGV信号
kill -11 12345
# 输出:[1]+ Segmentation fault (core dumped) ./sig_loop
2.3 函数调用产生信号
通过系统函数主动发送信号,常用函数有kill、raise、abort:
2.3.1 kill函数:向指定进程发送信号
c
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
pid:目标进程PID(正数);pid=-1表示向所有有权限的进程发送信号。sig:信号编号(0表示检测进程是否存在,不发送信号)。- 返回值:成功返回0,失败返回-1。
实战:实现简易版kill命令
cpp
#include <iostream>
#include <sys/types.h>
#include <signal.h>
#include <cstdlib>
int main(int argc, char* argv[]) {
if (argc != 3) {
std::cerr << "用法:" << argv[0] << " -信号编号 进程PID" << std::endl;
return 1;
}
int sig = std::stoi(argv[1] + 1); // 去掉前缀"-"
pid_t pid = std::stoi(argv[2]);
int ret = kill(pid, sig);
if (ret == 0) std::cout << "向进程" << pid << "发送信号" << sig << "成功" << std::endl;
else perror("kill失败");
return 0;
}
2.3.2 raise函数:向当前进程发送信号
c
#include <signal.h>
int raise(int sig); // 等价于kill(getpid(), sig)
实战 :进程自我发送SIGINT信号
cpp
#include <iostream>
#include <unistd.h>
#include <signal.h>
void handler(int sig) {
std::cout << "捕获到信号:" << sig << std::endl;
}
int main() {
signal(SIGINT, handler);
while (true) {
sleep(1);
raise(SIGINT); // 每秒自我发送一次SIGINT
}
return 0;
}
2.3.3 abort函数:强制进程异常终止
c
#include <stdlib.h>
void abort(void); // 发送SIGABRT(6号信号),无返回值
实战 :捕获SIGABRT信号
cpp
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <stdlib.h>
void handler(int sig) {
std::cout << "捕获到信号:" << sig << "(SIGABRT)" << std::endl;
}
int main() {
signal(SIGABRT, handler);
sleep(1);
abort(); // 发送SIGABRT,即使捕获也会终止进程
return 0;
}
运行结果:
bash
./sig_abort
捕获到信号:6(SIGABRT)
Aborted (core dumped)
2.4 软件条件产生信号
由软件内部状态触发的信号,常见场景:
- 管道破裂(
SIGPIPE):向无读端的管道写入数据。 - 定时器超时(
SIGALRM):alarm函数设置的超时时间到达。
alarm函数:设置定时器
c
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
- 功能:
seconds秒后向当前进程发送SIGALRM信号。 - 返回值:之前设置的定时器剩余秒数(无则返回0)。
- 注意:一个进程同时只能有一个活跃的alarm定时器。
实战1:1秒后终止进程
cpp
#include <iostream>
#include <unistd.h>
int main() {
alarm(1); // 1秒后发送SIGALRM(默认终止进程)
int count = 0;
while (true) {
std::cout << "count:" << ++count << std::endl;
}
return 0;
}
实战2:重复设置定时器
cpp
#include <iostream>
#include <unistd.h>
#include <signal.h>
int g_count = 0;
void handler(int sig) {
std::cout << "定时器触发,count:" << g_count << std::endl;
g_count = 0;
alarm(1); // 重新设置1秒定时器
}
int main() {
signal(SIGALRM, handler);
alarm(1); // 首次设置定时器
while (true) {
g_count++;
usleep(1000); // 微秒级延迟
}
return 0;
}
2.5 硬件异常产生信号
硬件检测到异常后,通知内核,内核将其转换为信号发送给当前进程:
- 除零错误:触发
SIGFPE(8号)。 - 非法内存访问(野指针):触发
SIGSEGV(11号)。 - 总线错误:触发
SIGBUS(7号)。
实战:模拟非法内存访问
cpp
#include <iostream>
#include <signal.h>
void handler(int sig) {
std::cout << "捕获到信号:" << sig << "(非法内存访问)" << std::endl;
}
int main() {
signal(SIGSEGV, handler);
int* p = nullptr;
*p = 100; // 写空指针,触发SIGSEGV
return 0;
}
运行结果:
bash
./sig_segv
捕获到信号:11(非法内存访问)
捕获到信号:11(非法内存访问)
... # 循环触发,因异常状态未清除
2.6 Core Dump:核心转储
当进程因信号异常终止时,可选择将进程的用户空间内存数据保存到磁盘(文件名为core.进程PID),用于事后调试,这就是Core Dump。
开启Core Dump
默认情况下,系统禁用Core Dump(避免敏感信息泄露),可通过ulimit命令开启:
bash
ulimit -c 1024 # 允许Core文件最大为1024块(每块512字节)
ulimit -c unlimited # 无限制
验证Core Dump
bash
# 开启Core Dump
ulimit -c unlimited
# 运行触发SIGQUIT的程序
./sig_quit
# 按下Ctrl+\,生成core文件
ls -l core.* # 查看生成的core文件
# 用gdb调试core文件
gdb ./sig_quit core.12345
三、信号的保存与状态
信号从产生到递达之间存在"时间窗口",期间信号处于未决状态 (Pending)。进程可通过阻塞信号集(Block)屏蔽某些信号,被阻塞的信号会保持未决状态,直到阻塞解除。
3.1 关键概念
- 未决(Pending):信号已产生,但尚未递达(进程未处理)。
- 阻塞(Block):进程暂时不处理该信号,即使信号已产生,也会保持未决状态。
- 递达(Delivery):进程实际执行信号的处理动作(默认/忽略/自定义)。
- 注意:阻塞≠忽略,忽略是递达后的处理动作,阻塞是阻止信号递达。
3.2 内核中的信号表示
进程控制块(task_struct)中存储了信号相关的三个核心数据结构:
blocked:阻塞信号集(sigset_t类型),每一位表示对应信号是否被阻塞。pending:未决信号集(sigset_t类型),每一位表示对应信号是否未决。sighand:信号处理动作表,存储每个信号的处理方式(默认/忽略/自定义函数)。
sigset_t是信号集类型,本质是位图,用于表示多个信号的状态(有效/无效)。
3.3 信号集操作函数
用户不能直接操作sigset_t的内部结构,需通过以下函数操作:
c
#include <signal.h>
// 初始化信号集为空
int sigemptyset(sigset_t* set);
// 初始化信号集为包含所有信号
int sigfillset(sigset_t* set);
// 向信号集添加信号
int sigaddset(sigset_t* set, int signo);
// 从信号集删除信号
int sigdelset(sigset_t* set, int signo);
// 判断信号是否在信号集中
int sigismember(const sigset_t* set, int signo);
3.4 修改进程的阻塞信号集(sigprocmask)
c
#include <signal.h>
int sigprocmask(int how, const sigset_t* set, sigset_t* oset);
how:修改方式(SIG_BLOCK/SIG_UNBLOCK/SIG_SETMASK)。set:新的阻塞信号集(NULL表示不修改)。oset:保存原阻塞信号集(NULL表示不保存)。- 返回值:成功返回0,失败返回-1。
how参数说明:
| how | 含义 | 等价操作 |
|---|---|---|
| SIG_BLOCK | 添加阻塞信号 | mask = mask ∪ set |
| SIG_UNBLOCK | 解除阻塞信号 | mask = mask ∩ ~set |
| SIG_SETMASK | 设置阻塞信号集 | mask = set |
3.5 读取未决信号集(sigpending)
c
#include <signal.h>
int sigpending(sigset_t* set); // 读取当前进程的未决信号集
3.6 实战:信号阻塞与未决状态验证
cpp
#include <iostream>
#include <unistd.h>
#include <signal.h>
// 打印未决信号集
void print_pending(const sigset_t& pending) {
std::cout << "进程[" << getpid() << "]未决信号:";
for (int sig = 1; sig <= 31; ++sig) {
if (sigismember(&pending, sig)) std::cout << "1";
else std::cout << "0";
}
std::cout << std::endl;
}
void handler(int sig) {
std::cout << "捕获到信号:" << sig << std::endl;
}
int main() {
signal(SIGINT, handler); // 自定义捕捉SIGINT(2号)
// 1. 初始化阻塞信号集,添加SIGINT
sigset_t block_set, old_set;
sigemptyset(&block_set);
sigaddset(&block_set, SIGINT);
// 2. 设置阻塞信号集(屏蔽SIGINT)
sigprocmask(SIG_BLOCK, &block_set, &old_set);
std::cout << "已屏蔽SIGINT,按Ctrl+C发送信号" << std::endl;
// 3. 循环打印未决信号集
int cnt = 10;
while (cnt--) {
sigset_t pending;
sigpending(&pending);
print_pending(pending);
sleep(1);
}
// 4. 解除阻塞
std::cout << "解除SIGINT阻塞" << std::endl;
sigprocmask(SIG_SETMASK, &old_set, nullptr);
while (true) sleep(1);
return 0;
}
运行结果:
bash
./sig_block
已屏蔽SIGINT,按Ctrl+C发送信号
进程[12345]未决信号:0000000000000000000000000000000
进程[12345]未决信号:0000000000000000000000000000000
^C进程[12345]未决信号:0000000000000000000000000000010 # SIGINT未决
进程[12345]未决信号:0000000000000000000000000000010
...
解除SIGINT阻塞
捕获到信号:2 # 信号递达,执行handler
四、信号的捕捉与处理
信号的处理动作有三种:
- 默认动作(
SIG_DFL):内核预设的处理方式(如终止、Core Dump、忽略)。 - 忽略(
SIG_IGN):进程不处理该信号。 - 自定义捕捉(
signal/sigaction):执行用户定义的处理函数。
4.1 signal函数:简单信号捕捉
c
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
signum:信号编号。handler:处理函数指针(SIG_DFL/SIG_IGN或自定义函数)。- 返回值:成功返回原处理方式,失败返回
SIG_ERR。
实战 :忽略SIGINT信号
cpp
#include <iostream>
#include <unistd.h>
#include <signal.h>
int main() {
signal(SIGINT, SIG_IGN); // 忽略Ctrl+C
std::cout << "忽略SIGINT,按Ctrl+C无反应" << std::endl;
while (true) sleep(1);
return 0;
}
4.2 sigaction函数:高级信号捕捉
signal函数接口简单,但行为在不同系统中可能不一致,推荐使用sigaction函数,功能更强大、可移植性更好。
c
#include <signal.h>
int sigaction(int signum, const struct sigaction* act, struct sigaction* oact);
struct sigaction结构体
c
struct sigaction {
void (*sa_handler)(int); // 处理函数(同signal)
sigset_t sa_mask; // 处理信号时,额外阻塞的信号集
int sa_flags; // 标志位(0表示默认行为)
void (*sa_sigaction)(int, siginfo_t*, void*); // 实时信号处理函数
};
关键特性:
- 执行信号处理函数时,内核会自动将当前信号加入
sa_mask,避免递归触发。 sa_mask可指定额外阻塞的信号,确保处理过程不被干扰。
实战 :用sigaction捕捉SIGINT
cpp
#include <iostream>
#include <unistd.h>
#include <signal.h>
void handler(int sig) {
std::cout << "捕获到信号:" << sig << std::endl;
sleep(2); // 模拟处理耗时操作
}
int main() {
struct sigaction act, oact;
act.sa_handler = handler;
sigemptyset(&act.sa_mask);
sigaddset(&act.sa_mask, SIGQUIT); // 处理SIGINT时,额外阻塞SIGQUIT
act.sa_flags = 0;
sigaction(SIGINT, &act, &oact); // 设置SIGINT的处理方式
std::cout << "原SIGINT处理方式:" << (void*)oact.sa_handler << std::endl;
while (true) sleep(1);
return 0;
}
运行结果:
bash
./sig_action
原SIGINT处理方式:0x0 # 原处理方式为默认(SIG_DFL)
^C捕获到信号:2 # 按下Ctrl+C,进入handler
^\ # 此时按下Ctrl+\(SIGQUIT),被阻塞
捕获到信号:2 # handler执行完毕后,SIGQUIT才递达(若未忽略)
4.3 信号捕捉的内核流程
当进程触发信号捕捉时,内核会执行以下步骤:
- 进程在用户态执行主流程,因中断/异常/系统调用进入内核态。
- 内核处理完原任务后,检查当前进程的未决信号集。
- 若存在可递达信号且处理方式为自定义函数,内核会切换到用户态执行处理函数。
- 处理函数执行完毕后,通过
sigreturn系统调用再次进入内核态。 - 内核检查无新信号后,恢复主流程的上下文,回到用户态继续执行。
核心特点:处理函数与主流程是两个独立的控制流程,使用不同的堆栈空间。
五、补充:可重入函数与volatile
5.1 可重入函数
若一个函数被多个控制流程(如主流程和信号处理函数)同时调用,且不会因共享资源导致数据错乱,则称为可重入函数。
不可重入函数的条件(满足其一)
- 调用
malloc/free(堆内存由全局链表管理)。 - 调用标准I/O库函数(如
printf,使用全局缓冲区)。 - 访问全局变量/静态变量。
示例:不可重入函数导致的数据错乱
cpp
#include <iostream>
#include <signal.h>
int g_count = 0;
void insert() {
g_count++; // 访问全局变量,不可重入
}
void handler(int sig) {
insert(); // 信号处理函数调用insert
}
int main() {
signal(SIGINT, handler);
while (true) {
insert(); // 主流程调用insert
sleep(1);
std::cout << "g_count:" << g_count << std::endl;
}
return 0;
}
运行时按下Ctrl+C,可能导致g_count计数异常(主流程和信号处理函数同时修改全局变量)。
5.2 volatile关键字
编译器优化可能会将变量缓存到CPU寄存器中,导致信号处理函数修改的变量值无法被主流程感知(数据不一致)。volatile关键字可禁止编译器优化,确保变量的读写直接操作内存。
问题示例(无volatile)
cpp
#include <iostream>
#include <signal.h>
#include <unistd.h>
int flag = 0; // 未加volatile
void handler(int sig) {
flag = 1;
std::cout << "flag设置为1" << std::endl;
}
int main() {
signal(SIGINT, handler);
while (!flag); // 编译器优化后,可能一直读取寄存器中的flag(0)
std::cout << "进程退出" << std::endl;
return 0;
}
编译时开启优化(-O2),即使按下Ctrl+C,flag被设置为1,主流程的while循环仍可能无限执行(因读取的是寄存器缓存)。
解决:添加volatile
cpp
volatile int flag = 0; // 禁止优化,直接操作内存
重新编译运行,按下Ctrl+C后,主流程能立即感知flag的变化,正常退出。
六、实战:SIGCHLD信号处理僵尸进程
子进程终止时会向父进程发送SIGCHLD信号(默认忽略),父进程可通过捕捉该信号,在处理函数中调用waitpid清理僵尸进程,避免轮询。
僵尸进程的危害
子进程终止后,若父进程未及时清理,子进程会变成僵尸进程,占用PID和系统资源,直至父进程退出。
实战:捕捉SIGCHLD清理僵尸进程
cpp
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <cstdlib>
// 信号处理函数:清理僵尸进程
void sigchld_handler(int sig) {
pid_t id;
// WNOHANG:非阻塞等待,有僵尸进程则清理,无则返回0
while ((id = waitpid(-1, nullptr, WNOHANG)) > 0) {
std::cout << "清理僵尸进程:" << id << std::endl;
}
}
int main() {
// 捕捉SIGCHLD信号
struct sigaction act;
act.sa_handler = sigchld_handler;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
sigaction(SIGCHLD, &act, nullptr);
// 创建5个子进程
for (int i = 0; i < 5; ++i) {
pid_t pid = fork();
if (pid == 0) {
std::cout << "子进程:" << getpid() << " 启动" << std::endl;
sleep(2); // 子进程运行2秒后退出
exit(0);
}
}
// 父进程持续运行
while (true) sleep(1);
return 0;
}
运行结果:
bash
./sig_chld
子进程:12346 启动
子进程:12347 启动
子进程:12348 启动
子进程:12349 启动
子进程:12350 启动
清理僵尸进程:12346
清理僵尸进程:12347
清理僵尸进程:12348
清理僵尸进程:12349
清理僵尸进程:12350
七、总结
Linux信号是进程间异步通信的核心,核心要点如下:
- 信号产生:支持终端按键、命令、函数、软件条件、硬件异常5种方式。
- 信号状态:未决(Pending)、阻塞(Block)、递达(Delivery),阻塞会阻止信号递达。
- 信号处理:默认、忽略、自定义捕捉(推荐
sigaction函数)。 - 关键注意:避免在信号处理函数中调用不可重入函数,共享变量需加
volatile。 - 实战场景:清理僵尸进程(
SIGCHLD)、定时器(SIGALRM)、异常处理(SIGSEGV)。