1. 什么是信号
信号(Signal)是 Linux/Unix 中进程间通信的一种机制。
本质:
- 软件层面的"中断"
- 用于通知进程发生了某种事件
例如:
| 信号 | 含义 |
|---|---|
| SIGINT | Ctrl + C |
| SIGSEGV | 段错误 |
| SIGFPE | 除0异常 |
| SIGALRM | 闹钟超时 |
| SIGCHLD | 子进程退出 |
2. 信号的生命周期
一个信号从产生到处理:
text
产生 -> 保存(pending) -> 阻塞(block)判断 -> 递达(deliver) -> 处理
Linux 内核维护:
- pending 表(是否收到)
- block 表(是否阻塞)
3. 信号产生方式
Linux 中常见五种信号产生方式:
| 方式 | 示例 |
|---|---|
| 终端按键 | Ctrl + C |
| 硬件异常 | 除0、段错误 |
| 软件条件 | alarm |
| 显式发送 | kill |
| 系统事件 | 子进程退出 |
4. kill 发送信号
4.1 kill 系统调用
函数:
cpp
int kill(pid_t pid, int sig);
作用:
向指定进程发送信号。
4.2 示例:实现 mykill
源代码
cpp
//实现kill命令
void Usage(string proc)
{
cout << "Usage:\n\t " << proc << " <pid>\n" << endl;
}
//mykill signum pid
int main(int argc, char* argv[])
{
if(argc != 3)
{
Usage(argv[0]);
exit(1);
}
int signum = stoi(argv[1]);
pid_t pid = stoi(argv[2]);
int n = kill(pid, signum);
if(n == -1)
{
perror("kill");
cout << "kill process failed" << endl;
exit(2);
}
cout << "kill process success" << endl;
return 0;
}
4.3 使用方式
bash
./mykill 2 12345
表示:
向 pid 为 12345 的进程发送:
text
SIGINT(2)
5. signal 信号捕获
5.1 signal 函数
函数原型:
cpp
void (*signal(int signum, void (*handler)(int)))(int);
作用:
为指定信号注册处理函数。
5.2 示例
源代码
cpp
void myhandler(int signum)
{
cout << "process get a signal: " << signum << endl;
}
int main()
{
signal(SIGINT, myhandler);
int cnt = 10;
while(cnt--)
{
cout << "i am a process" << endl;
sleep(1);
}
return 0;
}
5.3 运行效果
按:
text
Ctrl + C
会触发:
text
SIGINT
然后执行:
cpp
myhandler()
6. alarm 闹钟信号
6.1 alarm 函数
cpp
unsigned int alarm(unsigned int seconds);
作用:
指定 seconds 秒后发送:
text
SIGALRM
6.2 示例
源代码
cpp
void work()
{
cout << "work..." << endl;
}
void MyHandler(int signum)
{
work();
int n = alarm(5);
cout << "剩余时间: " << n << endl;
}
int main()
{
signal(SIGALRM, MyHandler);
int n = alarm(50);
while(1)
{
cout << "running..." << endl;
sleep(1);
}
return 0;
}
6.3 特点
alarm 只触发一次
需要在处理函数中:
cpp
alarm(5);
重新设置。
6.4 返回值
cpp
alarm()
返回:
上一次闹钟剩余时间。
7. 硬件异常信号
7.1 除0异常
源代码
cpp
signal(SIGFPE, myhandler);
int a = 10;
a /= 0;
会产生:
text
SIGFPE
7.2 段错误
例如:
cpp
int* p = nullptr;
*p = 100;
产生:
text
SIGSEGV
8. 子进程退出信号 SIGCHLD
8.1 子进程退出
当子进程退出时:
内核向父进程发送:
text
SIGCHLD
8.2 waitpid 获取退出信息
源代码
cpp
pid_t rid = waitpid(id, &status, 0);
if(rid == id)
{
cout << "child quit info, rid: "
<< rid
<< ", exit code: "
<< ((status>>8)&0xFF)
<< ", exit signal: "
<< (status&0x7F)
<< ", core dump: "
<< ((status>>7)&1)
<< endl;
}
8.3 status 解析
| 内容 | 提取方式 |
|---|---|
| 退出码 | (status >> 8) & 0xFF |
| 终止信号 | status & 0x7F |
| core dump | (status >> 7) & 1 |
9. 信号阻塞与 pending
9.1 sigprocmask
函数:
cpp
int sigprocmask(int how,
const sigset_t *set,
sigset_t *oldset);
how 参数
| 参数 | 含义 |
|---|---|
| SIG_BLOCK | 添加阻塞 |
| SIG_UNBLOCK | 解除阻塞 |
| SIG_SETMASK | 替换阻塞集 |
9.2 sigpending
函数:
cpp
int sigpending(sigset_t *set);
作用:
获取 pending 信号集。
9.3 pending 打印函数
源代码
cpp
void PrintPending(sigset_t &pending)
{
for(int signo = 1; signo < 32; signo++)
{
if(sigismember(&pending, signo))
{
cout << "1";
}
else
{
cout << "0";
}
}
cout << endl;
}
9.4 屏蔽 SIGINT 示例
源代码
cpp
signal(2, handler);
sigset_t bset, oldset;
sigemptyset(&bset);
sigemptyset(&oldset);
sigaddset(&bset, 2);
sigprocmask(SIG_SETMASK, &bset, &oldset);
int cnt = 0;
sigset_t pending;
while(1)
{
sigpending(&pending);
PrintPending(pending);
sleep(1);
cnt++;
if(cnt == 20)
{
cout<<"unblock signal 2"<<endl;
sigprocmask(SIG_SETMASK,
&oldset,
nullptr);
}
}
9.5 现象
按:
text
Ctrl + C
后:
- 信号不会立即处理
- pending 对应位变成 1
解除阻塞后:
cpp
handler()
立即执行。
10. 哪些信号不能阻塞
以下信号不能被阻塞:
| 信号 | 含义 |
|---|---|
| SIGKILL(9) | 强制杀死 |
| SIGSTOP(19) | 强制暂停 |
原因:
内核必须保证:
text
进程一定能被终止
11. sigaction
相比 signal:
cpp
sigaction
更可靠、更强大。
11.1 struct sigaction
源代码
cpp
struct sigaction 结构体
struct sigaction
{
void (*sa_handler)(int); // 信号处理函数指针(类似 signal)
void (*sa_sigaction)(int, siginfo_t *, void *); // 扩展处理函数
sigset_t sa_mask; // 在处理该信号时额外要阻塞的信号集
int sa_flags; // 控制信号处理行为的标志位
void (*sa_restorer)(void); // 已废弃,不再使用
};
11.2 注册信号处理
源代码
cpp
struct sigaction act, oact;
memset(&act, 0, sizeof(act));
memset(&oact, 0, sizeof(oact));
sigemptyset(&act.sa_mask);
sigaddset(&act.sa_mask, 4);
sigaddset(&act.sa_mask, 3);
act.sa_handler = handler;
sigaction(2, &act, &oact);
11.3 sa_mask 作用
表示:
在处理当前信号期间:
额外阻塞哪些信号。
例如:
cpp
sigaddset(&act.sa_mask, 3);
则处理 SIGINT 时:
text
SIGQUIT
也会被阻塞。
12. pending 位什么时候清零?
问题:
text
pending 位什么时候从1变0?
答案:
text
在调用处理函数之前
即:
text
先清 pending
再执行 handler
13. 信号处理期间自动阻塞
Linux 规定:
当某信号正在处理时:
该信号会自动加入 block 表。
防止:
text
信号处理函数嵌套调用
14. volatile 与信号
volatile 关键字用于确保 flag 变量在每次访问时都从内存中读取, 而不是从寄存器中读取,这在多线程环境中特别有用, 因为多个线程可能会同时访问 flag 变量, 而优化器可能会假设 flag 变量的值不会改变
14.1 示例
源代码
cpp
volatile int flag = 0;
void handler_2(int signum)
{
cout<< "catch a signal, signal number: "
<< signum << endl;
flag = 1;
}
void test_2()
{
signal(2, handler_2);
while(!flag)
{
cout << "process quit normal" << endl;
sleep(2);
}
}
14.2 为什么需要 volatile
避免:
text
编译器优化
导致:
cpp
while(!flag)
死循环。
15. SIGCHLD 异步回收子进程
15.1 信号方式回收
源代码
cpp
void handler_3(int signum)
{
pid_t rid;
while((rid = waitpid(-1,
nullptr,
WNOHANG)) > 0)
{
cout<< "child process quit: "
<< rid
<< endl;
}
}
15.2 WNOHANG
表示:
text
非阻塞回收
避免:
text
handler 阻塞
15.3 自动回收子进程
Linux 支持:父进程调用sigaction,将SIGCHLD信号处理为SIG_IGN, 则父进程不会收到SIGCHLD信号, 子进程结束时, 会自动回收子进程的资源
cpp
signal(SIGCHLD, SIG_IGN);
效果:
text
子进程退出自动回收
无需:
cpp
wait()
15.4 示例
源代码
cpp
void test_3()
{
signal(17, SIG_IGN);
for(int i = 0; i < 10; i++)
{
pid_t id = fork();
if(id == 0)
{
while(1)
{
cout<< "I am a child process: "
<< getpid()
<< endl;
sleep(5);
break;
}
cout << "child process quit !!!"
<< endl;
exit(0);
}
sleep(rand() % 5 + 1);
}
while(1)
{
cout<< "I am a parent process: "
<< getpid()
<< endl;
sleep(1);
}
}
16. Linux 信号核心知识总结
16.1 信号处理流程
text
产生
↓
pending
↓
block 判断
↓
递达
↓
handler
16.2 signal 与 sigaction
| 函数 | 特点 |
|---|---|
| signal | 简单 |
| sigaction | 推荐使用 |
16.3 pending 与 block
| 名称 | 含义 |
|---|---|
| pending | 已收到未处理 |
| block | 是否阻塞 |
16.4 常考问题
Q1:SIGKILL 能捕获吗?
不能。
Q2:SIGSTOP 能屏蔽吗?
不能。
Q3:信号会排队吗?
普通信号:
text
不会
实时信号:
text
会
Q4:handler 中能调用 printf 吗?
严格来说:
text
不安全
因为:
text
printf 不是异步信号安全函数