资料合集
链接:https://pan.quark.cn/s/770d9387db5f
在Linux系统编程中,“信号”(Signal)是一个无处不在却又容易被忽视的概念。当我们按下
Ctrl+C 结束一个卡死的程序,或者使用
kill -9 强行终止进程时,背后运作的正是信号机制。
本文将结合课堂笔记,从生活类比入手,深入内核结构,带你理解Linux信号的本质、特质以及其在PCB(进程控制块)中的实现,并附带详细的代码演示。
课堂笔记中提到了一个非常生动的比喻:摔杯为号。
在古代战争或影视剧中,主帅摔下酒杯,埋伏的刀斧手便一拥而上。在这个场景中,我们可以提炼出信号的三大共性,这完全对应计算机中的信号机制:
简单性:信号的传递方式非常直接(摔杯、鸣枪、发射信号弹),不需要复杂的握手协议。信息量小:信号只传递最基本的指令(“进攻”、“开始”、“暂停”),不能携带大量数据(比如你无法通过摔杯告诉刀斧手具体的战术布局)。条件触发:信号不能乱发。运动员必须就位才能鸣枪,时机未到乱发会导致系统(战场/赛场)混乱。在计算机中: 信号是一种软件层面的中断。它模拟了硬件中断,强制打断进程当前的执行流,去处理特定的事件,处理完毕后再回到断点继续执行。
要掌握信号,必须理解以下四个维度:
产生方式: 终端按键:如
Ctrl+C (SIGINT),
Ctrl+ (SIGQUIT)。硬件异常:如除以0错误 (SIGFPE), 非法内存访问 (SIGSEGV)。函数调用:如
kill() 函数,
raise() 函数。软件条件:如定时器
alarm() (SIGALRM), 管道读端关闭写端写入 (SIGPIPE)。
内核管控:无论信号是如何产生的,最终都由内核统一管理并发送给进程。编号体系:每个信号都有唯一的ID(如
SIGINT 是 2 号)。Linux 定义了约 30 个常规信号。异步机制:信号何时到达是不确定的,进程无法预知。
信号不仅仅是一个简单的通知,它在内核的进程控制块(PCB,
task_struct)中有着严密的存储结构。PCB 位于内核空间(3G-4G),其中包含与信号相关的核心“铁三角”:
为了验证上述理论,我们通过代码演示信号的捕捉、软件条件产生以及异步特性。
这个例子展示了信号的强制中断性和处理方式的修改。我们将修改
Ctrl+C 的默认行为(终止进程),使其打印一句话。
/* signal_catch.c */
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
// 自定义信号处理函数
void my_handler(int signo) {
printf("
[内核提示] 捕获到信号: %d (SIGINT)
", signo);
printf("想杀我?没那么容易!(摔杯为号失败)
");
}
int main() {
// 注册信号处理函数
// 当收到 SIGINT (Ctrl+C) 时,调用 my_handler
if (signal(SIGINT, my_handler) == SIG_ERR) {
perror("signal error");
return 1;
}
printf("进程运行中 (PID: %d)...
", getpid());
printf("请尝试按下 Ctrl+C 查看效果。
");
// 模拟长时间运行的任务
while (1) {
printf(".");
fflush(stdout);
sleep(1);
}
return 0;
}
运行结果:
$ gcc signal_catch.c -o signal_catch
$ ./signal_catch
进程运行中 (PID: 12345)...
请尝试按下 Ctrl+C 查看效果。
.....^C
[内核提示] 捕获到信号: 2 (SIGINT)
想杀我?没那么容易!(摔杯为号失败)
.....^C
[内核提示] 捕获到信号: 2 (SIGINT)
想杀我?没那么容易!(摔杯为号失败)
.....^Quit (core dumped) <-- 按下 Ctrl+ (SIGQUIT) 才能结束
解析:
程序正常打印点号。按下
Ctrl+C 时,内核暂停
main 函数的
while 循环,跳转执行
my_handler。处理完后,自动回到断点继续执行
while 循环。这完美印证了信号的软中断特质。
这个例子演示信号的软件触发条件。
alarm() 函数就像一个闹钟,时间到了内核会发送
SIGALRM 给进程。
/* signal_alarm.c */
#include <stdio.h>
#include <unistd.h>
int main() {
int i = 0;
// 设定定时器,1秒后发送 SIGALRM 信号
// 默认处理动作是终止进程
alarm(1);
printf("开始疯狂计数,看1秒能数多少...
");
while (1) {
printf("%d
", i++);
}
return 0;
}
运行结果(部分):
$ gcc signal_alarm.c -o signal_alarm
$ ./signal_alarm
开始疯狂计数,看1秒能数多少...
0
1
2
...
156789
156790
Alarm clock <-- 1秒后,进程被内核终止,输出 "Alarm clock"
解析:
alarm(1) 并没有阻塞程序,程序继续向下执行
while(异步性)。1秒时间到,内核检测到条件满足,发送信号。进程收到信号,执行默认动作(终止)。
演示内核作为唯一信号发送者的角色,即使是代码调用,也是请求内核发信号。
/* signal_kill.c */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
int main() {
pid_t pid = fork();
if (pid > 0) {
// --- 父进程 ---
printf("父进程: 子进程 (PID: %d) 已创建,它只有 2 秒寿命。
", pid);
sleep(2);
printf("父进程: 时间到!发送 SIGKILL 信号。
");
// 向子进程发送 9 号信号 (SIGKILL,不可被捕捉或忽略)
kill(pid, SIGKILL);
} else if (pid == 0) {
// --- 子进程 ---
while (1) {
printf("子进程: 我还活着...
");
sleep(1);
}
} else {
perror("fork error");
}
return 0;
}
运行结果:
$ gcc signal_kill.c -o signal_kill
$ ./signal_kill
父进程: 子进程 (PID: 12346) 已创建,它只有 2 秒寿命。
子进程: 我还活着...
子进程: 我还活着...
父进程: 时间到!发送 SIGKILL 信号。
$ Killed <-- 子进程被杀死
通过笔记与代码,我们总结出以下核心知识点:
| 知识点 | 核心内容 | 难度系数 | 备注 |
|---|---|---|---|
| 信号共性 | 简单、信息量小、条件触发 | ⭐⭐ | 就像摔杯为号,不能传递复杂数据 |
| 信号特质 | 软件中断、强制性、内核控制 | ⭐⭐⭐ | 信号处理会打断当前执行流 |
| PCB关联 | 未决集、阻塞集、处理表 | ⭐⭐⭐ | 理解信号在内核中的“排队”与“屏蔽”机制 |
| 时效性 | 软中断有微秒级延迟 | ⭐⭐ | 虽然比硬件中断慢,但对人类来说是瞬时的 |
易混淆点提醒:
信号不是立即处理的:信号产生后,会先在 PCB 的“未决信号集”中标记,当进程从内核态返回用户态时才进行检查和处理。硬件中断 vs 软件中断:硬件中断由硬件设备触发(如键盘、网卡),信号由内核软件机制模拟。虽然都有“中断”效果,但可靠性和层级不同。阻塞 vs 忽略: 阻塞:信号到了,暂时不处理(在未决集中保留),解除阻塞后会处理。忽略:信号到了,直接丢弃,处理完毕。掌握信号机制,是理解 Linux 进程间通信(IPC)和系统稳定性的基石。希望本文的代码案例能帮你更好地消化课堂笔记中的理论知识!
¥478.00
STM32F407ZGT6开发板 ARM开发板 STM32学习板实验板 嵌入式开发板
¥28.00
STM32F030F4P6核心板 开发板 小系统板子 单片机 CORTEX-M0内核
¥134.00
STM32F407VET6开发板Cortex-M4 STM32 小系统板arm核心板学习板
¥188.00
STM32F407VGT6开发板Cortex-M4 STM32 小系统板arm开发板
¥146.00
STM32F103VCT6核心板 小系统板STM32 ARM开发板Cortex-m3
¥164.00
STM32F407ZET6/ZGT6开发板 Cortex-M4 STM32 小系统板 ARM核心板