[特殊字符] 深入理解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. 实现一个信号调试工具:可以监控进程的信号收发情况。

相关推荐
JY.yuyu2 小时前
Linux磁盘管理 / 硬盘分区、创建逻辑卷
linux·运维·服务器
翼龙云_cloud2 小时前
亚马逊云渠道商:AWS EC2 实战案例解析
服务器·云计算·aws
云和数据.ChenGuang3 小时前
运维故障之MySQL 连接授权错误
运维·数据库·人工智能·mysql
~黄夫人~3 小时前
Kubernetes Pod 初始化容器(InitContainer)起不来的排错思路
linux·运维·服务器
运维有小邓@3 小时前
如何在 Linux 中查看系统日志消息
linux·运维·服务器
TroubleBoy丶3 小时前
麒麟V10-ARM架构Docker启动报错
运维·docker·容器·arm·麒麟v10
Allen-Steven3 小时前
群辉NAS 部署小雅 SSH指令版
运维·ssh
PyHaVolask3 小时前
Linux零基础入门:文件系统结构与文件管理命令详解
运维·文件管理·linux命令·linux文件系统·目录结构·fhs
m0_738120723 小时前
渗透测试——y0usef靶机渗透提权详细过程(插件伪造请求头)
服务器·网络·web安全·ssh·php