[特殊字符] 深入理解Linux信号机制:信号的产生,保存和捕捉

🎯 课前思考题

老师:在开始前,让我们回顾几个关键问题:

  1. 为什么进程收到SIGSEGV信号后如果不退出会不断重复收到?

  2. SIGKILLSIGSTOP为什么不能被捕获、阻塞或忽略?

  3. 信号处理过程中,内核态和用户态是如何切换的?

🔍 一、信号的产生:五种方式深度解析

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检测到异常(如除零、非法内存访问)时:

  1. CPU设置标志寄存器相应位

  2. 触发硬件异常

  3. 操作系统捕获异常

  4. 通过current指针找到当前进程

  5. 向进程发送相应信号

    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;
}

实验步骤

  1. 编译运行程序

  2. 另开终端,用kill -2 PIDkill -3 PID发送信号

  3. 观察未决信号集的变化

  4. 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. 解除阻塞后,未决信号会立即被递达

  3. 信号处理函数执行时,该信号在未决信号集中已被清除


🔄 四、信号捕捉的完整流程

1. 用户态与内核态

2. 四次状态切换详解

老师:记住这个"无穷大"符号的流程:

复制代码
用户态 → 内核态 → 用户态 → 内核态 → 用户态
      (1)       (2)       (3)       (4)
  1. 用户态→内核态:系统调用、中断或异常

  2. 内核态→用户态:执行信号处理函数

  3. 用户态→内核态 :处理函数通过sigreturn返回

  4. 内核态→用户态:返回主程序继续执行

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?

  1. 磁盘空间:Core文件可能很大,频繁产生会占满磁盘

  2. 安全风险:Core文件可能包含敏感信息

  3. 自动化运维:线上服务通常有自动重启机制,不需要手动调试


💡 六、信号最佳实践与常见陷阱

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;
}

📝 七、总结与思考题

关键知识点总结

  1. 信号产生:5种方式(键盘、命令、系统调用、硬件异常、软件条件)

  2. 信号保存:三张表(pending、blocked、action)

  3. 信号递达:内核态返回用户态时检查,自定义处理有4次状态切换

  4. 信号处理 :使用sigaction,注意可重入性

  5. Core Dump:用于事后调试,线上环境通常关闭

思考题

  1. 问题一 :如果在信号处理函数中调用fork(),会发生什么?

  2. 问题二:如何实现一个进程不能被普通信号杀死,但可以通过特定信号优雅退出?

  3. 问题三 :考虑以下代码,为什么sleep(10)可能提前返回?

    cpp 复制代码
    void handler(int sig) {
        printf("信号处理中...\n");
    }
    
    int main() {
        signal(SIGINT, handler);
        printf("开始睡眠10秒\n");
        sleep(10);
        printf("睡眠结束\n");
        return 0;
    }
  4. 问题四 :如何让被SIGSTOP暂停的进程继续运行?

实践任务

  1. 实现一个简单的shell :支持Ctrl+C终止前台进程,Ctrl+Z暂停前台进程,fg恢复后台进程。

  2. 实现一个定时任务管理器 :使用SIGALRM信号,支持添加、删除定时任务。

  3. 实现一个信号调试工具:可以监控进程的信号收发情况。

相关推荐
A小辣椒1 天前
TShark:Wireshark CLI 功能
linux
A小辣椒1 天前
TShark:基础知识
linux
AlfredZhao1 天前
OCI 明明分配了 200G 系统盘,为什么 df 只看到 30G?
linux·oci
AlfredZhao2 天前
vi 删除指定范围的行,不用再反复按 dd
linux·vi
用户9718356334662 天前
银河麒麟 KY10 申威(SW64) 安装 nginx-1.16.1-2.p01.ky10.sw_64.rpm 详细步骤
linux
猪脚踏浪2 天前
linux 拷贝文件或目录到指定的位置
linux
大树883 天前
金刚石散热越强,管路越先见顶
大数据·运维·服务器·人工智能·ai
摇滚侠3 天前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
霸道流氓气质3 天前
领域驱动设计(DDD)在 Spring Boot 微服务中的实践指南
运维·spring boot·微服务
bush43 天前
嵌入式linux学习记录十四、术语
linux·嵌入式