DAY 9-信号机制

4 阅读7分钟

Linux 信号机制完全指南


一、信号的本质

什么是信号?

信号是 Linux 下的一种软中断机制,用于异步通知进程发生了某个事件。

类比:信号就像老师上课时给你使个眼色——告诉你"待会有事",但你不用立刻停下正在做的事,等一个合适的时机(比如下课后)再去处理。

概念说明
软中断不是硬件中断,是软件层面模拟的中断
异步通知信号产生的时间是随机的,进程无法预知
紧急程度比普通函数调用更紧急,会打断当前执行流

二、信号的四大产生方式

方式示例产生的信号
① 键盘输入Ctrl+CSIGINT (2)
Ctrl+\SIGQUIT (3)
Ctrl+ZSIGTSTP (20)
② 命令/函数kill -9 PIDSIGKILL (9)
kill -15 PIDSIGTERM (15)
raise(SIGSTOP)任意信号
③ 硬件异常除 0SIGFPE (8)
段错误(访问空指针)SIGSEGV (11)
非法指令SIGILL (4)
④ 软件条件alarm(5) 定时到SIGALRM (14)
写已关闭的管道SIGPIPE (13)
子进程退出SIGCHLD (17)

三、信号的处理时机(重点!)

信号产生后立刻处理吗?

不是! 信号处理有延迟。

具体时机

用户态 ──────────────► 内核态 ──────────────► 用户态
                          │
                          ▼
                   检查待处理信号
                          │
                          ▼
              有信号?→ 执行信号处理函数
                          │
                          ▼
                    恢复用户态执行

核心原则:信号在进程从内核态返回用户态之前被检查和处理。

为什么这样设计?

  1. 安全:内核态不能执行用户态代码(信号处理函数属于用户态代码)
  2. 及时性:系统调用返回、中断处理完成、进程调度切换时都会检查

被阻塞的信号怎么办?

// 使用 sigprocmask 阻塞信号后,信号被挂起
sigprocmask(SIG_BLOCK, &set, NULL);

// 解除阻塞后,信号才会被处理
sigprocmask(SIG_UNBLOCK, &set, NULL);

四、可重入函数(高频面试题)

什么是可重入函数?

定义:一个函数可以在执行过程中被中断,并在中断返回后安全地再次执行,而不会导致数据损坏或逻辑错误。

可重入函数的条件

条件说明
✅ 不修改静态/全局变量只使用局部变量(在栈上分配)
✅ 不调用 malloc/free堆操作可能被中断
✅ 不调用标准 I/O(printf/scanf)使用全局缓冲区
✅ 不调用非可重入函数链式传染

典型的不可重入函数

// ❌ 不可重入:使用了全局变量
int global = 0;
int func() {
    global++;  // 被中断时 global 处于中间状态
    return global;
}

// ❌ 不可重入:printf 使用全局缓冲区
printf("hello");  // 中断时可能正在写入缓冲区

// ❌ 不可重入:malloc 操作全局堆链表
malloc(100);  // 中断时堆结构可能不完整

✅ 可重入函数示例

// ✅ 可重入:只用局部变量
int func(int a, int b) {
    int c = a + b;
    return c;
}

// ✅ 可重入:只读取全局变量(不修改)
int func() {
    return global_readonly;  // 只读是安全的
}

五、中断中为什么不能用 printf?

❌ 中断服务函数中能否用 printf?为什么?

不能! 原因有三:

原因详细说明
① 不可重入printf 使用全局缓冲区,中断发生时主程序可能正在执行 printf,再次调用会导致缓冲区状态错乱
② 可能阻塞printf 需要写终端,可能等待 I/O 完成,中断上下文不能阻塞
③ 浮点运算printf 格式化浮点数时可能触发浮点协程操作,中断中禁用浮点

✅ 中断中应该做什么?

void interrupt_handler() {
    // 1. 清中断标志
    clear_interrupt_flag();
    
    // 2. 读取硬件状态(只读寄存器)
    int status = read_hardware_status();
    
    // 3. 记录状态到共享变量
    g_interrupt_flag = 1;
    
    // 4. 触发下半部(软中断/tasklet/工作队列)
    schedule_work(&work);
}

六、常用信号速查表

信号名编号产生方式默认动作能否捕获
SIGINT2Ctrl+C终止
SIGQUIT3Ctrl+\终止 + core
SIGILL4非法指令终止 + core
SIGTRAP5断点/调试终止 + core
SIGFPE8除 0/浮点异常终止 + core
SIGKILL9kill -9终止不可捕获
SIGSEGV11段错误(空指针)终止 + core
SIGPIPE13写已关闭管道终止
SIGALRM14alarm 定时到终止
SIGTERM15kill(默认)终止
SIGCHLD17子进程退出/暂停忽略
SIGSTOP19Ctrl+Z 或 kill暂停进程不可捕获
SIGCONT18继续暂停的进程继续

七、SIGKILL vs SIGTERM(必考)

对比项SIGKILL (9)SIGTERM (15)
可否捕获❌ 不可捕获✅ 可捕获
可否忽略❌ 不可忽略✅ 可忽略
默认动作立即终止终止
能否清理资源❌ 不能✅ 可以(在信号处理函数中)
使用场景强制杀死优雅退出

生产环境最佳实践

// 1. 先发 SIGTERM,让进程优雅退出
kill(pid, SIGTERM);

// 2. 等待 3 秒,如果进程还在,再发 SIGKILL
sleep(3);
kill(pid, SIGKILL);
// 进程端:捕获 SIGTERM,清理资源
void sigterm_handler(int sig) {
    // 关闭文件描述符
    close(fd);
    // 释放内存
    free(buffer);
    // 通知其他进程
    notify_others();
    // 退出
    exit(0);
}

signal(SIGTERM, sigterm_handler);

八、信号处理函数相关 API

1. signal() — 最简单的注册方式

#include <signal.h>
typedef void (*sighandler_t)(int);

sighandler_t signal(int signum, sighandler_t handler);

// 示例
void handler(int sig) {
    printf("收到信号: %d\n", sig);
}
signal(SIGINT, handler);

2. sigaction() — 更强大的注册方式

#include <signal.h>
struct sigaction {
    void (*sa_handler)(int);      // 处理函数
    void (*sa_sigaction)(int, siginfo_t*, void*);  // 扩展版
    sigset_t sa_mask;             // 处理时阻塞的信号
    int sa_flags;                 // 标志位
};

int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

3. sigprocmask() — 信号屏蔽

#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

// how 参数:
// SIG_BLOCK   - 将 set 中的信号加入阻塞集
// SIG_UNBLOCK - 将 set 中的信号从阻塞集移除
// SIG_SETMASK - 将阻塞集设为 set

// 示例:屏蔽 SIGINT
sigset_t set, oldset;
sigemptyset(&set);
sigaddset(&set, SIGINT);
sigprocmask(SIG_BLOCK, &set, &oldset);
// ... 临界区代码 ...
sigprocmask(SIG_SETMASK, &oldset, NULL);  // 恢复

4. kill() — 发送信号

#include <signal.h>
int kill(pid_t pid, int sig);

// 示例
kill(1234, SIGTERM);   // 向 PID 1234 发送 SIGTERM
kill(0, SIGINT);       // 向当前进程组所有进程发送
kill(getpid(), SIGUSR1); // 向自己发送 SIGUSR1

5. alarm() — 定时发送 SIGALRM

#include <unistd.h>
unsigned int alarm(unsigned int seconds);

// 示例:5 秒后收到 SIGALRM
alarm(5);

九、常见信号处理场景

1. 忽略 SIGPIPE(防止程序退出)

// 网络编程中,写已关闭的 socket 会收到 SIGPIPE
signal(SIGPIPE, SIG_IGN);  // 忽略,写操作返回 -1,errno = EPIPE

2. 子进程退出不产生僵尸进程

// 方式1:忽略 SIGCHLD
signal(SIGCHLD, SIG_IGN);  // 子进程退出后由内核自动回收

// 方式2:捕获 SIGCHLD,调用 wait
void sigchld_handler(int sig) {
    while (waitpid(-1, NULL, WNOHANG) > 0) {
        // 循环回收所有已退出的子进程
    }
}
signal(SIGCHLD, sigchld_handler);

3. 超时功能(alarm + SIGALRM)

void alarm_handler(int sig) {
    printf("超时了!\n");
    // 做超时处理
}

signal(SIGALRM, alarm_handler);
alarm(5);  // 5 秒后超时

十、信号机制的局限性

问题说明
不可靠信号signal() 在不同系统行为不一致,建议用 sigaction()
信号丢失相同信号多次产生,可能只处理一次
系统调用中断信号可能打断系统调用,需处理 EINTR
线程间信号信号是进程级,多线程时信号发给任意线程

处理 EINTR

ssize_t ret;
do {
    ret = read(fd, buf, size);
} while (ret == -1 && errno == EINTR);  // 被信号中断则重试

汇总

题目答案要点
信号是什么?软中断,异步通知机制
信号产生方式?键盘、kill、硬件异常、软件条件
信号处理时机?内核态返回用户态前检查
什么是可重入函数?可被中断并安全再次调用
printf 为什么不能用在信号处理函数中?不可重入、可能阻塞、可能引发浮点运算
SIGKILL 和 SIGTERM 区别?SIGKILL 不可捕获,SIGTERM 可捕获
如何优雅退出进程?先 SIGTERM,超时再 SIGKILL
僵尸进程如何避免?忽略 SIGCHLD 或捕获并 wait
alarm() 有什么作用?定时发送 SIGALRM,可用于超时检测

🎯 总结

信号是 Linux 下重要的异步通知机制,掌握产生方式处理时机可重入性是面试高频考点。记住:

信号处理函数中只做最轻量、最安全的操作:设置标志位、记录状态、触发下半部。