🎯 课前思考题
老师:在开始前,让我们回顾几个关键问题:
-
为什么进程收到
SIGSEGV信号后如果不退出会不断重复收到? -
SIGKILL和SIGSTOP为什么不能被捕获、阻塞或忽略? -
信号处理过程中,内核态和用户态是如何切换的?
🔍 一、信号的产生:五种方式深度解析
1. 键盘产生信号
常见组合键:
| 组合键 | 信号 | 编号 | 默认动作 |
|---|---|---|---|
Ctrl+C |
SIGINT |
2 | 终止进程 |
Ctrl+\ |
SIGQUIT |
3 | 终止并core dump |
Ctrl+Z |
SIGTSTP |
20 | 暂停进程 |
关键点 :键盘信号只能发送给前台进程,因为只有前台进程拥有终端控制权。
cpp
// 演示:忽略SIGINT信号
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
int main() {
signal(SIGINT, SIG_IGN); // 忽略Ctrl+C
printf("PID: %d\n", getpid());
printf("尝试用Ctrl+C终止我...\n");
while(1) {
printf("我还在运行...\n");
sleep(1);
}
return 0;
}
2. 系统命令产生信号
bash
# 常用kill命令
kill -9 PID # 发送SIGKILL
kill -15 PID # 发送SIGTERM(默认)
kill -19 PID # 发送SIGSTOP
kill -18 PID # 发送SIGCONT
思考 :为什么kill -9被称为"强制杀死"?
3. 系统调用产生信号
核心函数:
cpp
#include <signal.h>
// 1. 向任意进程发信号
int kill(pid_t pid, int sig);
// 2. 向自己发信号
int raise(int sig); // 等价于kill(getpid(), sig)
// 3. 终止进程并产生core dump
void abort(void); // 发送SIGABRT
// 4. 设置闹钟
unsigned int alarm(unsigned int seconds);
示例 :实现一个简单的kill命令
cpp
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
int main(int argc, char* argv[]) {
if (argc != 3) {
printf("Usage: %s <signal> <pid>\n", argv[0]);
return 1;
}
int sig = atoi(argv[1]);
pid_t pid = atoi(argv[2]);
if (kill(pid, sig) == -1) {
perror("kill");
return 1;
}
printf("Signal %d sent to process %d\n", sig, pid);
return 0;
}
4. 硬件异常产生信号
常见硬件异常信号:
-
SIGFPE(8): 浮点异常(如除零) -
SIGSEGV(11): 段错误(非法内存访问) -
SIGILL(4): 非法指令 -
SIGBUS(7): 总线错误
底层原理 :
当CPU检测到异常(如除零、非法内存访问)时:
-
CPU设置标志寄存器相应位
-
触发硬件异常
-
操作系统捕获异常
-
通过
current指针找到当前进程 -
向进程发送相应信号
cpp// 演示:除零错误触发SIGFPE #include <stdio.h> #include <signal.h> #include <stdlib.h> void handler(int sig) { printf("捕获到信号 %d (SIGFPE)\n", sig); printf("除零错误!\n"); exit(1); } int main() { signal(SIGFPE, handler); int a = 10; int b = 0; int c = a / b; // 触发SIGFPE return 0; }
5. 软件条件产生信号
常见场景:
-
管道破裂:
SIGPIPE -
闹钟超时:
SIGALRM -
子进程状态改变:
SIGCHLD
alarm函数详解:
cpp
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void alarm_handler(int sig) {
printf("闹钟响了!信号:%d\n", sig);
// 重新设置闹钟,实现周期性
alarm(2);
}
int main() {
signal(SIGALRM, alarm_handler);
printf("设置2秒后响铃\n");
alarm(2); // 2秒后发送SIGALRM
while(1) {
printf("主程序运行中...\n");
sleep(1);
}
return 0;
}
📊 二、信号的保存:三张表与信号集
1. 内核中的三张表
每个进程的PCB中都有三张信号相关的表:
cpp
// 简化的task_struct信号部分
struct task_struct {
// ...
/* Signal handling */
sigset_t blocked; // 阻塞信号集(信号屏蔽字)
struct sigpending pending; // 未决信号集
struct sigaction action[NSIG]; // 信号处理动作数组
// ...
};
表格对比
| 表名 | 数据结构 | 作用 | 对应概念 |
|---|---|---|---|
| pending | 位图(sigset_t) | 记录已产生但未递达的信号 | 未决信号 |
| blocked | 位图(sigset_t) | 记录被阻塞的信号 | 信号屏蔽字 |
| action | 结构体数组 | 记录信号的处理方式 | 信号处理动作 |
2. 信号集操作函数
cpp
#include <signal.h>
// 初始化信号集
int sigemptyset(sigset_t *set); // 清空所有信号
int sigfillset(sigset_t *set); // 包含所有信号
// 修改信号集
int sigaddset(sigset_t *set, int signum); // 添加信号
int sigdelset(sigset_t *set, int signum); // 删除信号
// 查询信号集
int sigismember(const sigset_t *set, int signum); // 是否在集合中
// 进程信号屏蔽字操作
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
// 获取未决信号集
int sigpending(sigset_t *set);
3. 信号屏蔽字操作详解
sigprocmask参数说明:
cpp
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
| how值 | 含义 | 计算公式 |
|---|---|---|
SIG_BLOCK |
将set中的信号加入阻塞集 | new = current ∪ set |
SIG_UNBLOCK |
将set中的信号移出阻塞集 | new = current - set |
SIG_SETMASK |
直接设置阻塞集为set | new = set |
🧪 三、实验:信号的阻塞与未决
实验1:观察信号的阻塞效果
cpp
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <string.h>
// 打印信号集
void print_sigset(const sigset_t *set) {
printf("信号集: ");
for (int i = 1; i <= 31; i++) {
if (sigismember(set, i)) {
printf("%d ", i);
}
}
printf("\n");
}
void handler(int sig) {
printf("处理信号 %d\n", sig);
}
int main() {
sigset_t block_set, old_set, pending_set;
// 初始化信号集
sigemptyset(&block_set);
sigaddset(&block_set, SIGINT); // 阻塞Ctrl+C
sigaddset(&block_set, SIGQUIT); // 阻塞Ctrl+\
// 设置信号处理
signal(SIGINT, handler);
signal(SIGQUIT, handler);
// 设置信号屏蔽字
sigprocmask(SIG_BLOCK, &block_set, &old_set);
printf("PID: %d\n", getpid());
printf("已阻塞SIGINT(2)和SIGQUIT(3)\n");
printf("10秒内尝试发送这些信号...\n");
// 10秒内不断检查未决信号
for (int i = 0; i < 10; i++) {
sleep(1);
sigpending(&pending_set);
printf("第%d秒 - ", i + 1);
print_sigset(&pending_set);
}
// 解除阻塞
printf("\n解除阻塞...\n");
sigprocmask(SIG_SETMASK, &old_set, NULL);
// 再等待一会儿看信号是否被处理
sleep(2);
printf("程序结束\n");
return 0;
}
实验步骤:
-
编译运行程序
-
另开终端,用
kill -2 PID和kill -3 PID发送信号 -
观察未决信号集的变化
-
10秒后观察信号处理情况
实验2:证明信号处理是在解除阻塞后立即执行的
cpp
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
volatile int signal_received = 0;
void handler(int sig) {
signal_received = 1;
printf("信号 %d 被处理\n", sig);
// 打印当前未决信号集
sigset_t pending;
sigpending(&pending);
printf("处理时未决信号集: ");
for (int i = 1; i <= 31; i++) {
if (sigismember(&pending, sig)) {
printf("%d ", i);
}
}
printf("\n");
}
int main() {
sigset_t block_set, old_set;
// 设置信号处理
signal(SIGINT, handler);
// 阻塞SIGINT
sigemptyset(&block_set);
sigaddset(&block_set, SIGINT);
sigprocmask(SIG_BLOCK, &block_set, &old_set);
printf("PID: %d\n", getpid());
printf("SIGINT已被阻塞\n");
printf("5秒内发送SIGINT...\n");
// 给用户时间发送信号
sleep(5);
// 检查是否有未决信号
sigset_t pending;
sigpending(&pending);
if (sigismember(&pending, SIGINT)) {
printf("发现未决的SIGINT信号\n");
// 解除阻塞
printf("解除对SIGINT的阻塞\n");
sigprocmask(SIG_SETMASK, &old_set, NULL);
// 验证信号是否被处理
if (signal_received) {
printf("信号已被立即处理\n");
}
}
printf("程序结束\n");
return 0;
}
关键发现:
-
信号被阻塞时,即使产生也不会被递达
-
解除阻塞后,未决信号会立即被递达
-
信号处理函数执行时,该信号在未决信号集中已被清除
🔄 四、信号捕捉的完整流程
1. 用户态与内核态
2. 四次状态切换详解
老师:记住这个"无穷大"符号的流程:
用户态 → 内核态 → 用户态 → 内核态 → 用户态
(1) (2) (3) (4)
-
用户态→内核态:系统调用、中断或异常
-
内核态→用户态:执行信号处理函数
-
用户态→内核态 :处理函数通过
sigreturn返回 -
内核态→用户态:返回主程序继续执行
3. sigaction详解
老师 :signal函数简单但不稳定,sigaction才是工业级选择:
cpp
#include <stdio.h>
#include <signal.h>
#include <string.h>
#include <unistd.h>
void handler(int sig, siginfo_t *info, void *context) {
printf("收到信号 %d\n", sig);
printf("发送者PID: %d\n", info->si_pid);
printf("发送者UID: %d\n", info->si_uid);
}
int main() {
struct sigaction sa;
memset(&sa, 0, sizeof(sa));
sa.sa_sigaction = handler; // 使用扩展处理函数
sa.sa_flags = SA_SIGINFO; // 启用siginfo
// 设置执行处理函数时要阻塞的信号
sigemptyset(&sa.sa_mask);
sigaddset(&sa.sa_mask, SIGQUIT); // 处理SIGINT时阻塞SIGQUIT
if (sigaction(SIGINT, &sa, NULL) == -1) {
perror("sigaction");
return 1;
}
printf("PID: %d\n", getpid());
printf("等待SIGINT...\n");
while(1) {
pause(); // 等待信号
}
return 0;
}
sa_flags重要选项:
-
SA_RESTART:自动重启被信号中断的系统调用 -
SA_SIGINFO:使用三参数的处理函数 -
SA_NODEFER:不自动阻塞当前信号(允许嵌套) -
SA_RESETHAND:处理一次后重置为默认动作
🛡️ 五、核心转储(Core Dump)
1. 什么是Core Dump?
当进程异常终止时,操作系统会将进程的内存状态保存到磁盘文件(core文件),用于后续调试。
2. 启用Core Dump
bash
# 查看当前core文件大小限制
ulimit -c
# 设置为无限制
ulimit -c unlimited
# 设置core文件大小(KB)
ulimit -c 10240
3. 使用Core Dump调试
cpp
// segfault.c - 段错误示例
#include <stdio.h>
#include <stdlib.h>
void crash() {
int *p = NULL;
*p = 42; // 段错误!
}
int main() {
printf("PID: %d\n", getpid());
printf("3秒后触发段错误...\n");
sleep(3);
crash();
return 0;
}
调试步骤:
bash
# 1. 编译时添加调试信息
gcc -g segfault.c -o segfault
# 2. 运行程序(会产生core文件)
./segfault
# 3. 使用gdb调试core文件
gdb ./segfault core
# 4. 在gdb中查看堆栈
(gdb) bt
(gdb) f 0 # 查看第0帧
(gdb) list # 查看源代码
4. 为什么云服务器默认关闭Core Dump?
-
磁盘空间:Core文件可能很大,频繁产生会占满磁盘
-
安全风险:Core文件可能包含敏感信息
-
自动化运维:线上服务通常有自动重启机制,不需要手动调试
💡 六、信号最佳实践与常见陷阱
1. 信号处理函数的注意事项
不安全操作:
cpp
void unsafe_handler(int sig) {
// 这些函数在信号处理中不安全!
printf("收到信号\n"); // printf不可重入
malloc(100); // malloc不可重入
system("ls"); // system会创建新进程
}
安全操作:
cpp
#include <unistd.h>
#include <signal.h>
#include <string.h>
void safe_handler(int sig) {
// 只能使用异步信号安全函数
const char msg[] = "信号收到\n";
write(STDOUT_FILENO, msg, strlen(msg));
// 或者设置volatile标志,在主循环中处理
volatile sig_atomic_t flag = 1;
}
2. 信号丢失与实时信号
普通信号(1-31) :可能丢失,多次相同信号只记录一次
实时信号(34-64):不会丢失,支持排队
cpp
// 使用实时信号示例
#define SIGRT_CUSTOM (SIGRTMIN + 0)
void rt_handler(int sig, siginfo_t *info, void *context) {
printf("实时信号 %d,携带值: %d\n",
sig, info->si_value.sival_int);
}
// 发送带数据的实时信号
union sigval value;
value.sival_int = 123;
sigqueue(pid, SIGRT_CUSTOM, value);
3. 防止信号竞争条件
cpp
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
volatile sig_atomic_t flag = 0;
void handler(int sig) {
flag = 1; // 只设置标志,在主循环中处理
}
int main() {
struct sigaction sa;
sa.sa_handler = handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sigaction(SIGINT, &sa, NULL);
while(1) {
// 主循环检查标志
if (flag) {
printf("处理信号...\n");
flag = 0; // 重置标志
// 这里可以安全地执行复杂操作
// 因为不在信号处理函数中
}
// 正常业务逻辑
printf("主程序运行...\n");
sleep(1);
}
return 0;
}
📝 七、总结与思考题
关键知识点总结
-
信号产生:5种方式(键盘、命令、系统调用、硬件异常、软件条件)
-
信号保存:三张表(pending、blocked、action)
-
信号递达:内核态返回用户态时检查,自定义处理有4次状态切换
-
信号处理 :使用
sigaction,注意可重入性 -
Core Dump:用于事后调试,线上环境通常关闭
思考题
-
问题一 :如果在信号处理函数中调用
fork(),会发生什么? -
问题二:如何实现一个进程不能被普通信号杀死,但可以通过特定信号优雅退出?
-
问题三 :考虑以下代码,为什么
sleep(10)可能提前返回?cppvoid handler(int sig) { printf("信号处理中...\n"); } int main() { signal(SIGINT, handler); printf("开始睡眠10秒\n"); sleep(10); printf("睡眠结束\n"); return 0; } -
问题四 :如何让被
SIGSTOP暂停的进程继续运行?
实践任务
-
实现一个简单的shell :支持
Ctrl+C终止前台进程,Ctrl+Z暂停前台进程,fg恢复后台进程。 -
实现一个定时任务管理器 :使用
SIGALRM信号,支持添加、删除定时任务。 -
实现一个信号调试工具:可以监控进程的信号收发情况。