1. 信号是什么
在 Linux 中,信号(Signal)是一个非常重要的知识点。简单来说,信号是一种给进程发送的、用来进行事件异步通知的机制。
什么是异步通知? 举个例子,上课的时候老师发现张三还不在教室,如果此时继续上课而不等待张三,就是异步;如果老师等张三回来再上课,则是同步。
根据上述描述,可以得到关于信号的几个基本结论:
- 预先定义:进程在信号没有产生的时候,就知道信号该如何处理了。
- 非立即处理:信号的处理不是立即执行,而是可以等到合适的时候再进行。
- 内置识别:进程早已内置了对信号的识别与处理方式。
- 来源多样:信号源非常多,给进程产生信号的途径也非常多。
2. 信号的产生
信号需要先产生,然后保存,最后处理。下面探讨信号的产生方式。
1. 键盘产生信号
通过键盘输入特定组合键可以产生信号。例如 Ctrl + C 是终止当前前台进程的信号。
代码示例:
#include <iostream>
#include <unistd.h>
int main() {
int cnt = 0;
while (true) {
std::cout << "hello world : " << cnt++ << std::endl;
sleep(1);
}
return 0;
}
在 Linux 系统中,信号本质上是数字。为了便于表示,使用了宏替代数字。1~31 是普通信号,后面的是实时信号。Ctrl + C 触发的是 2 号信号(SIGINT)。
信号处理动作: 进程收到信号后,有三种处理方式:
- 默认处理:系统预设的动作(如 SIGINT 默认终止进程)。
- 自定义处理:用户定义特定的处理函数。
- 忽略处理:不理会该信号。
signal 替换信号处理
可以使用 signal 函数修改信号的默认行为。
代码示例:
#include <iostream>
#include <unistd.h>
#include <signal.h>
void handlerSign(int sign) {
std::cout << "获得一个信号:" << sign << std::endl;
}
int main() {
// 将 2 号信号(SIGINT)的处理函数设置为 handlerSign
signal(2, handlerSign);
int cnt = 0;
while (true) {
std::cout << "hello world : " << cnt++ << std::endl;
sleep(1);
}
return 0;
}
此时按下 Ctrl + C 不会直接结束进程,而是调用自定义的 handlerSign 函数。若要强制关闭进程,可使用 Ctrl + \。
前后台进程规则
在 Linux 终端里,程序分为'前台进程'和'后台进程'。
- 前台进程:直接运行在命令行,拥有键盘交互权。像
Ctrl+C这类由键盘触发的信号,只会发给当前的前台进程。 - 后台进程:以
&结尾启动,不占用键盘交互资源。它无法接收来自键盘的直接信号。
常用命令:
| 命令 / 操作 | 功能描述 | 用法示例 |
|---|---|---|
jobs | 查看当前终端所有后台任务 | 直接输入 jobs |
ctrl+z | 暂停当前前台进程并切换到后台 | 前台运行时按下 |
fg 任务号 | 将后台任务切换到前台 | fg 1 |
bg 任务号 | 让后台暂停的任务恢复运行 | bg 1 |
2. 进程发送信号的本质
给进程发送信号并非直接向进程传递数据,而是通过操作系统内核完成:
- 通过目标进程的 PID,找到该进程在内核中的管理结构(
struct task_struct)。 - 修改该结构中的'信号位图'(
sig字段),某一位对应一个信号。 - 当某信号被发送时,内核将对应位置 1,记录'进程收到了该信号'。
处理逻辑:
- 非立即处理:信号产生后先记录在位图中。
- 合适时机处理:进程从内核态返回用户态等安全时机,检查位图状态并执行处理逻辑。
3. 系统调用产生信号
kill
使用 kill 系统调用可以向指定进程发送信号。
代码示例:
#include <iostream>
#include <signal.h>
int main(int argc, char* argv[]) {
if (argc != 3) {
std::cout << "./mykill sig pid" << std::endl;
return 1;
}
int sig = std::stoi(argv[1]);
int pid = std::stoi(argv[2]);
int n = kill(pid, sig);
if (n == 0) {
std::cout << "kill -" << sig << " " << pid << std::endl;
}
return 0;
}
raise
raise() 是标准 C 库函数,向调用该函数的进程自身发送指定信号,等价于 kill(getpid(), sig)。
代码示例:
#include <iostream>
#include <unistd.h>
#include <signal.h>
void handlerSign(int sign) {
std::cout << "获得一个信号:" << sign << std::endl;
}
int main() {
for (int i = 1; i < 32; i++) {
signal(i, handlerSign);
}
for (int i = 1; i < 32; i++) {
raise(i);
}
int cnt = 0;
while (true) {
std::cout << "hello world : " << cnt++ << " pid : " << getpid() << std::endl;
sleep(1);
}
return 0;
}
abort
abort() 强制让当前进程异常终止,向进程自身发送 SIGABRT 信号(6 号信号)。
#include <stdlib.h>
void abort(void);
4. 产生异常
程序运行时的错误也会触发信号。例如除零错误或野指针访问。
代码示例:
#include <iostream>
#include <unistd.h>
#include <signal.h>
void handlerSign(int sign) {
std::cout << "捕获到信号:" << sign << std::endl;
}
int main() {
for (int i = 1; i < 32; i++) {
signal(i, handlerSign);
}
int cnt = 0;
while (true) {
std::cout << "hello world : " << cnt++ << " pid : " << getpid() << std::endl;
sleep(1);
// 模拟异常:除零错误会触发 SIGFPE
// int a = 10; a /= 0;
// 模拟异常:野指针访问会触发 SIGSEGV
// int* a = nullptr; *a = 10;
}
return 0;
}
当取消注释上述异常代码时,程序会在运行时触发相应的信号(如 SIGFPE 或 SIGSEGV)。若已注册信号处理函数,则执行自定义逻辑;否则执行默认动作(通常是终止进程)。

