进程间通信详解(三):Linux进程信号深度解析

文章目录

  • 一、Linux进程信号核心概念
    • 1.1 信号本质
    • 1.2 关键术语
    • 1.3 Linux 信号机制的核心流程:
  • 二、信号产生机制全景
    • 2.1 通过终端按键产生信号
      • 2.1.1 基本操作
    • 2.2 调用系统命令向进程发信号
      • 2.2.1 kill 命令:向指定进程发送信号
      • 2.2.2 killall 命令:按进程名发送信号
      • 2.2.3 pkill 命令:按进程名或属性发送信号
      • 2.2.4 发送信号的实际场景
      • 2.2.5 查看信号列表
    • 2.3 使用函数产生信号
      • 2.3.1 kill
      • 2.3.2 raise() 函数:向自身发送信号
      • 2.3.3 sigqueue() 函数:发送带数据的信号(实时信号)
      • 2.3.4 信号发送的错误处理与注意事项
      • 2.3.5 总结
    • 2.4 由软件条件产生信号
      • 2.4.1 alarm
      • 2.4.2 如何简单快速理解系统闹钟
    • 2.5 硬件异常产生信号
      • 2.5.1 常见的硬件异常信号
      • 2.5.2 硬件异常的处理流程
      • 2.5.3 调试硬件异常信号
  • 三、保存信号
    • 3.1 信号其他相关常见概念
    • 3.2 在内核中的表示
    • 3.3 sigset_t(信号集)
    • 3.4 信号集操作函数
      • 3.4.1 sigprocmask
        • 总结
      • 3.4.2 sigpending
  • 四、捕获信号
    • 4.1 信号捕捉的流程
    • 4.2 sigaction
    • 4.3 操作系统是怎么运行的
      • 4.3.1 硬件中断
      • 4.3.2 时钟中断
      • 4.3.3 死循环
      • 4.3.4 软中断
      • 4.3.5 缺页中断?内存碎片处理?除零野指针错误?
    • 4.4 如何理解内核态和用户态
  • 五、可重入函数
    • 5.1 什么是可重入函数?
    • 5.2 不可重入的典型场景与风险
    • 5.3 可重入函数的设计原则
    • 5.4 可重入函数的实现示例
    • 5.5 五、信号处理中的可重入性
  • 六、volatile
    • 6.1 volatile 的本质与作用
    • 6.2 信号处理函数中的全局变量
  • 七、SIGCHLD 信号:Linux 进程管理的 “子进程通知机制”
    • 7.1 SIGCHLD 信号的本质与作用
    • 7.2 SIGCHLD 的默认行为与问题
    • 7.3 处理 SIGCHLD 的三种方式
    • 7.4 调试与监控
    • 7.5 常见误区与注意事项
    • 7.6 总结

一、Linux进程信号核心概念

1.1 信号本质

* 异步通信机制:事件驱动的进程间通知
* 信号类型:预定义整数(1-31为常规信号,34+为实时信号)
* 生命周期:产生 → 保存 → 处理

1.2 关键术语

术语描述内核表示
递达(Delivery)信号实际处理过程task_struct->ksigaction
未决(Pending)信号产生到递达间的状态task_struct->signal->pending
阻塞(Block)进程主动屏蔽的信号task_struct->blocked 位图

1.3 Linux 信号机制的核心流程:

信号产生 — 信号保存 — 信号处理

二、信号产生机制全景

在这里插入图片描述

2.1 通过终端按键产生信号

2.1.1 基本操作

  • Ctrl + C(SIGINT)
    向当前正在运行的前台进程发送中断信号,使进程立即终止运行。不过,若进程对SIGINT信号进行了特殊处理,如捕获并忽略该信号,那么按下Ctrl + C可能无法终止进程。同时,Ctrl + C仅对前台进程有效,后台进程不会受其影响
  • Ctrl + \(SIGOUT)
    不仅会终止进程,还会让进程生成 核心转储文件(core dump),用于调试程序崩溃问题
  • Ctrl + Z(SIGSTP)
    将当前前台进程暂停(挂起) 并放入后台,使其暂时停止运行但不终止。

2.2 调用系统命令向进程发信号

2.2.1 kill 命令:向指定进程发送信号

基本语法:

kill [-信号名称/编号] <进程ID>

常用信号选项:

  • -9-SIGKILL:强制终止进程(不可被捕获或忽略)。
    -15-SIGTERM:正常终止进程(默认选项,可被捕获并执行清理)。
    -1-SIGHUP:重新加载配置(常用于守护进程,如 nginx)。
    -2-SIGINT:中断进程(等价于 Ctrl + C)。
    -3-SIGQUIT:终止进程并生成 core 文件(等价于 Ctrl + \)。
    -19-SIGSTOP:暂停进程(等价于 Ctrl + Z,不可被忽略)。
    -18-SIGCONT:恢复被暂停的进程。

示例:

# 正常终止进程(先尝试清理)
kill 1234# 强制终止进程(不执行清理)
kill -9 1234# 向多个进程发送信号
kill -15 1234 5678 9012# 发送自定义信号(如 SIGUSR1,编号 10)
kill -10 1234

2.2.2 killall 命令:按进程名发送信号

基本语法:

killall [-信号名称/编号] <进程名>

示例:

# 终止所有名为 "nginx" 的进程
killall nginx# 强制终止所有名为 "cpp" 的进程
killall -9 cpp# 重新加载所有名为 "httpd" 的进程的配置
killall -HUP httpd

2.2.3 pkill 命令:按进程名或属性发送信号

基本语法:

pkill [-信号名称/编号] [-选项] <匹配模式>

常用选项:

  • -u <用户>:按用户名筛选进程。
  • -t <终端>:按终端会话筛选进程。
  • -f:匹配进程全名(包括命令行参数)。

示例:

# 终止用户 "test" 运行的所有 "bash" 进程
pkill -u test bash# 暂停当前终端的所有 "vim" 进程
pkill -STOP -t pts/0 vim# 终止包含 "python script.py" 的进程
pkill -f "python script.py"

2.2.4 发送信号的实际场景

优雅重启服务

# 重新加载 Nginx 配置(不中断现有连接)
kill -HUP $(cat /run/nginx.pid)

批量管理进程

# 暂停所有用户 "alice" 的进程
pkill -STOP -u alice# 恢复所有被暂停的进程
pkill -CONT -u alice

终止顽固进程

# 先尝试正常终止(给进程清理资源的机会)
kill 1234# 若 5 秒后仍未终止,强制杀死
sleep 5 && kill -9 1234

2.2.5 查看信号列表

通过 kill -l 命令可查看系统支持的所有信号:
在这里插入图片描述
编号34以上的是实时信号,本章只讨论编号34以下的信号,不讨论实时信号。这些信号各自在什么条件下产生,默认的处理动作是什么,在signal(7)中都有详细说明:man 7 signal

2.3 使用函数产生信号

2.3.1 kill

函数原型:

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

参数说明:

  • pid:目标进程 ID(pid > 0),或特殊值:
    • pid = 0:向当前进程组的所有进程发送信号。
    • pid = -1:向所有有权限发送的进程发送信号。
    • sig:要发送的信号(如 SIGINTSIGKILL,或自定义信号如 SIGUSR1)。

返回值:

  • 成功返回 0,失败返回 -1(错误原因可通过 errno 获取)。

代码示例:向指定进程发送 SIGTERM 信号

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <signal.h>
#include <errno.h>
#include <string.h>int main(int argc, char *argv[]) {if (argc != 3) {printf("用法: %s <进程ID> <信号编号>\n", argv[0]);return 1;}pid_t target_pid = atoi(argv[1]);int signal_num = atoi(argv[2]);if (kill(target_pid, signal_num) == -1) {perror("kill 失败");printf("错误码: %d, 错误信息: %s\n", errno, strerror(errno));return 1;}printf("已向进程 %d 发送信号 %d\n", target_pid, signal_num);return 0;
}

编译与使用:

gcc -o kill_demo kill_demo.c
# 向进程1234发送SIGTERM(信号15)
./kill_demo 1234 15

2.3.2 raise() 函数:向自身发送信号

函数原型:

#include <signal.h>
int raise(int sig);

参数说明:

  • sig:要发送的信号(等价于 kill(getpid(), sig))。

返回值:

  • 成功返回 0,失败返回非零值。

代码示例:程序自中断(等价于 Ctrl + C)

#include <stdio.h>
#include <signal.h>
#include <unistd.h>void sigint_handler(int sig) {printf("捕获到SIGINT信号,程序即将退出\n");exit(0);  // 调用 exit 终止进程
}int main() {// 注册SIGINT信号处理函数signal(SIGINT, sigint_handler);printf("程序运行中,3秒后自发送SIGINT信号...\n");sleep(3);// 向自身发送SIGINT信号raise(SIGINT);printf("该语句不会执行,因为进程已处理信号并退出\n");return 0;
}

2.3.3 sigqueue() 函数:发送带数据的信号(实时信号)

函数原型:

#include <sys/types.h>
#include <signal.h>
int sigqueue(pid_t pid, int sig, const union sigval value);

参数说明:

  • pid:目标进程 ID。
  • sig:要发送的信号(推荐使用实时信号,如 SIGRTMIN + n)。
  • value:包含整数或指针数据的联合体,可随信号传递给目标进程。

返回值:

  • 成功返回 0,失败返回 -1

代码示例:发送带数据的实时信号

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <signal.h>
#include <unistd.h>
#include <string.h>// 目标进程(接收信号)
void target_process() {// 注册信号处理函数struct sigaction sa;memset(&sa, 0, sizeof(sa));sa.sa_flags = SA_SIGINFO;  // 支持接收信号附带的数据sa.sa_sigaction = [](int sig, siginfo_t *info, void *context) {if (sig == SIGRTMIN) {printf("接收实时信号SIGRTMIN,附带数据:%d\n", info->si_int);}};sigaction(SIGRTMIN, &sa, NULL);printf("目标进程运行中,等待信号...\n");while (1) sleep(1);
}// 发送信号的进程
void sender_process(pid_t target_pid) {union sigval value;value.sival_int = 100;  // 附带整数数据if (sigqueue(target_pid, SIGRTMIN, value) == -1) {perror("sigqueue 失败");exit(1);}printf("已向进程 %d 发送带数据的SIGRTMIN信号\n", target_pid);
}int main(int argc, char *argv[]) {if (argc != 2) {printf("用法: %s <0(目标)/1(发送者)>\n", argv[0]);return 1;}int mode = atoi(argv[1]);if (mode == 0) {target_process();} else if (mode == 1) {pid_t target_pid = 1234;  // 替换为实际目标进程IDsender_process(target_pid);} else {printf("模式错误,需输入0或1\n");}return 0;
}

2.3.4 信号发送的错误处理与注意事项

常见错误:

  • **权限不足:**普通用户只能向自己的进程发送信号,向其他用户进程发送信号需 root 权限。
  • **进程不存在:**目标进程已终止或 PID 错误时,kill 会返回 ESRCH 错误。
  • **信号被阻塞:**目标进程若阻塞了该信号,信号会暂存直至阻塞解除。

推荐方式:

  • 先检查进程是否存在:使用 kill(pid, 0) 可在不发送信号的情况下检查进程是否存在(sig=0 为 “空信号”)。
  • 区分信号类型:
    • 非实时信号(如 SIGINT):若多次发送且未处理,仅保留最后一次。
    • 实时信号(如 SIGRTMIN):会排队等待处理,适合需要可靠传递的场景。
  • **避免滥用 SIGKILL:**优先使用 SIGTERM 让进程优雅退出,仅在必要时使用 SIGKILL

2.3.5 总结

函数用途核心参数适用场景
kill() 向任意进程发送信号pid(进程 ID)、sig(信号)进程控制、常规信号发送
raise()向自身发送信号sig(信号)程序自中断、自定义退出
sigqueue()发送带数据的实时信号pidsigvalue(数据)进程间通信、需传递数据场景

2.4 由软件条件产生信号

常见的软件信号

  1. SIGALRM(闹钟信号)
    • 触发条件:通过alarm()setitimer()函数设置的定时器到期。
    • 应用场景:实现超时控制、周期性任务(如心跳检测)。
  2. SIGUSR1/SIGUSR2(用户自定义信号)
    • 触发条件:通过kill()raise()sigqueue()函数手动发送。
    • 应用场景:进程间自定义通信(如通知配置更新、优雅重启)。
  3. SIGPIPE(管道破裂信号)
    • 触发条件:向已关闭的管道或套接字写入数据。
    • 应用场景:网络编程中检测连接状态。
  4. SIGALRM/SIGVTALRM(虚拟定时器信号)
    触发条件:通过setitimer()设置的用户态或内核态 CPU 时间到期。
    应用场景:性能分析、CPU 时间统计。

SIGPIPE是一种由软件条件产生的信号,在“管道”中已经介绍过了。本节主要介绍alarm函数和SIGALRM信号。

2.4.1 alarm

  1. 基本功能与原型

函数原型:

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

参数说明:

  • seconds:设置的定时器秒数。
  • seconds = 0:取消之前设置的闹钟。
  • seconds > 0:在 seconds 秒后触发 SIGALRM 信号。

返回值:

  • 返回之前设置的闹钟剩余秒数(若之前未设置,则返回 0)。
  1. 默认行为与信号处理
  • 默认行为: 当定时器到期时,进程会收到 SIGALRM 信号,默认行为是终止进程。
  • 自定义处理: 可通过 signal()sigaction() 注册信号处理函数,避免进程被终止。
  1. 应用场景
  • 超时控制: 例如等待用户输入或网络请求时设置超时。
  • 周期性任务: 结合信号处理实现简单的定时器。
  • 资源监控: 定时检查系统资源使用情况。
  1. 代码示例

示例 1:基本超时控制

#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <stdlib.h>void timeout_handler(int sig) {printf("超时!程序已运行超过5秒\n");exit(1);
}int main() {// 注册SIGALRM信号处理函数signal(SIGALRM, timeout_handler);// 设置5秒后触发SIGALRM信号alarm(5);printf("程序开始运行,等待5秒...\n");sleep(10);  // 尝试休眠10秒,但会在5秒后被中断printf("该语句不会执行,因为进程已被信号中断\n");return 0;
}

示例 2:非阻塞超时读取用户输入

#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <string.h>volatile int input_received = 0;void alarm_handler(int sig) {printf("\n超时!请加快输入\n");input_received = 1;
}int main() {char buffer[100];// 注册信号处理函数signal(SIGALRM, alarm_handler);// 设置3秒超时alarm(3);printf("请在3秒内输入内容:");fgets(buffer, sizeof(buffer), stdin);// 取消闹钟(如果用户在超时前输入)alarm(0);if (!input_received) {printf("你输入了:%s", buffer);}return 0;
}

示例 3:实现周期性任务

#include <stdio.h>
#include <unistd.h>
#include <signal.h>void periodic_task(int sig) {printf("执行周期性任务:每秒打印一次\n");alarm(1);  // 重新设置1秒后触发
}int main() {// 注册信号处理函数signal(SIGALRM, periodic_task);// 启动第一个闹钟alarm(1);printf("程序运行中,按Ctrl+C终止...\n");while (1) {// 主循环保持程序运行pause();  // 等待信号}return 0;
}
  1. 注意事项
    1. 每个进程只能有一个闹钟:多次调用 alarm() 会覆盖之前的设置。
    2. 时间精度有限alarm() 基于秒级计时,不适合毫秒级精度场景。
    3. 信号处理函数应简洁:避免在信号处理函数中执行复杂操作,可能导致重入问题。
    4. 与 sleep() 冲突alarm() 会中断 sleep()pause() 等系统调用。

2.4.2 如何简单快速理解系统闹钟

系统闹钟,其实本质是OS必须自身具有定时功能,并能让用户设置这种定时功能,才可能实现闹钟这样的技术。

现代Linux是提供了定时功能的,定时器也要被管理:先描述,在组织。内核中的定时器数据结构是:

#include <linux/timer.h>struct timer_list {struct list_head entry;    // 内核链表结构unsigned long expires;     // 到期时间(jiffies)struct tvec_base *base;    // 内部使用的定时器基数void (*function)(unsigned long);  // 回调函数unsigned long data;        // 传递给回调函数的参数int slack;                 // 定时器执行的松弛时间// ...其他字段(内核版本不同可能有差异)
};

操作系统管理定时器,采用的是时间轮的做法。
核心原理:将时间划分为固定槽位,每个槽存储到期时间相同的定时器。指针随时间移动,到期时触发对应槽的定时器。

这里就简单提一下,感兴趣的自己去了解一下。如果难以理解,你就将它理解为一个时间轴,谁的过期时间更近那么就先调度谁

2.5 硬件异常产生信号

2.5.1 常见的硬件异常信号

SIGSEGV(段错误,信号 11)

  • 触发原因: 进程访问未分配给它的内存(如空指针解引用、数组越界)。
  • 硬件机制: MMU(内存管理单元)检测到非法地址,触发页错误(Page Fault)。
  • 示例场景:
int *ptr = NULL;
*ptr = 10;  // 触发SIGSEGV

SIGFPE(浮点异常,信号 8)

  • 触发原因: 数学运算错误(如除零、溢出)。
  • 硬件机制: CPU 的浮点运算单元(FPU)检测到错误。
  • 示例场景:
int a = 1 / 0;  // 触发SIGFPE(整数除零)
double b = 1.0 / 0.0;  // 可能触发(取决于编译器和硬件)

SIGILL(非法指令,信号 4)

  • 触发原因: CPU 执行了无效指令(如未实现的指令、错误的操作码)。
  • 硬件机制: 指令解码器检测到非法指令。
  • 示例场景:
// 手动构造非法指令(示例仅示意,实际不可执行)
unsigned char code[] = {0xFF, 0xFF, 0xFF};  // 无效操作码
((void (*)())code)();  // 触发SIGILL

SIGBUS(总线错误,信号 7)

  • 触发原因: 硬件访问错误(如未对齐内存访问、物理内存损坏)。
  • 硬件机制: 内存总线检测到错误。
  • 示例场景:
// 在某些架构上,访问未对齐的内存可能触发SIGBUS
struct {int a;char b;
} __attribute__((packed)) s;
int *p = (int*)&s.b;  // 未对齐的指针
*p = 10;  // 可能触发SIGBUS

2.5.2 硬件异常的处理流程

  • 异常发生:CPU 执行指令时检测到错误(如除零、无效内存访问)。
  • 硬件中断:CPU 切换到内核模式,执行对应的中断处理程序。
  • 信号生成:内核识别异常类型,构造对应的信号(如SIGSEGV)。
  • 信号传递:内核将信号添加到目标进程的未决信号队列。
  • 进程响应
    • 默认行为:终止进程,生成核心转储文件(core dump)。
    • 自定义处理:若进程通过signal()sigaction()注册了处理函数,则执行该函数。

2.5.3 调试硬件异常信号

子进程退出 core dump
在这里插入图片描述

核心转储文件(core dump)

  • 作用:保存进程崩溃时的内存状态,用于事后分析。
  • 启用方法
ulimit -c unlimited  # 允许生成core文件
  • 分析工具
gdb ./program core  # 用GDB加载程序和core文件

GDB 调试技巧

# 设置信号处理方式(捕获但不终止)
(gdb) handle SIGSEGV nostop print# 运行程序直到崩溃
(gdb) run# 查看堆栈跟踪
(gdb) backtrace# 查看变量值
(gdb) print variable

三、保存信号

3.1 信号其他相关常见概念

  • 实际执行信号的处理动作称为信号递达(Delivery)
  • 信号从产生到递达之间的状态,称为信号未决(Pending)。
  • 进程可以选择阻塞(Block)某个信号。
  • 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作。
  • 注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。

3.2 在内核中的表示

示意图:
在这里插入图片描述
task_struct 中的信号字段
每个进程的描述符(task_struct)包含以下信号相关字段:

struct task_struct {// 信号掩码(当前阻塞的信号)sigset_t blocked;// 未决信号(pending)struct signal_struct *signal;// 信号处理函数表struct k_sigaction ksigaction[_NSIG];// 其他字段...
};

signal_struct 结构

struct signal_struct {atomic_t count;                 // 引用计数struct sigpending pending;      // 未决信号队列spinlock_t siglock;             // 保护锁struct sigaction action[_NSIG]; // 用户空间信号处理函数// 其他字段...
};

sigpending 结构

struct sigpending {struct list_head list;  // 未决信号链表sigset_t signal;        // 未决信号位图
};

信号处理流程

  1. 信号产生:内核 / 其他进程通过系统调用(如 kill() )发送信号,标记 pending 对应位为 1
  2. 检查阻塞:进程调度或从内核态返回用户态时,检查 block,若信号被阻塞(block=1 ),则跳过处理,维持 pending=1;若未阻塞(block=0 ),进入下一步。
  3. 执行处理动作:根据 handler 配置,执行默认动作(SIG_DFL )、忽略(SIG_IGN )或自定义函数(sighandler ),处理后清零 pending 对应位。

3.3 sigset_t(信号集)

sigset_t 本质上是一个 位图(Bitmap),每个位对应一个信号编号:

  • 位数:通常为 64 位(对应 64 个信号)。
  • 实现:内核中定义为 unsigned long 数组:
typedef struct {unsigned long sig[_NSIG_WORDS];  // _NSIG_WORDS 通常为 2(64位系统)
} sigset_t;

信号表示

  • 若信号集中包含信号 sig,则对应位被置为 1
  • 例如:信号集包含 SIGINT(2),则第 2 位为 1

3.4 信号集操作函数

初始化与修改

#include <signal.h>// 清空信号集(所有位设为 0)
int sigemptyset(sigset_t *set);// 填充信号集(所有位设为 1)
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);
  • how 参数:
    • SIG_BLOCK:添加 set 中的信号到当前掩码(阻塞这些信号)。
    • SIG_UNBLOCK:从当前掩码中移除 set 中的信号(解除阻塞)。
    • SIG_SETMASK:用 set 替换当前掩码。

3.4.1 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 完全替换当前掩码。
  • set:信号集指针,指定要操作的信号。若为 NULL,则不修改掩码,仅获取当前掩码到 oldset
  • oldset:用于保存修改前的信号掩码(若不为 NULL),可用于后续恢复。

返回值

  • 成功:返回 0
  • 失败:返回 -1,并设置 errno(如 EFAULTEINVAL)。

信号掩码与未决信号

信号掩码(Signal Mask)

  • 本质是一个位图,每位对应一个信号(如 SIGINTSIGTERM)。
  • 被掩码标记的信号不会被进程接收,而是进入 未决状态Pending)。

未决信号(Pending Signals)

  • 已产生但被阻塞的信号会暂存到进程的未决队列。
  • 掩码解除后,未决信号会被立即处理(非实时信号可能合并,实时信号支持排队)。
总结

pending信号集记录已生成但未处理的信号,阻塞信号集决定哪些信号会被延迟处理。当信号产生时:

  1. 若阻塞信号集对应位为0(未阻塞),无论pending集状态如何,信号都会被立即递送
  2. 若阻塞信号集对应位为1(阻塞),信号会被加入pending集(pending位设为1),保持未决状态

当进程通过sigprocmask()解除信号阻塞(将阻塞位设为0)后:

  • 内核检查pending集
  • 若对应信号位为1(存在未决信号)
  • 在下次进程从内核态返回用户态的执行上下文中
  • 该信号会被递送处理

关键点说明

  1. 递送时机:信号处理发生在进程从内核态返回用户态时,这是Linux信号设计的核心机制
    • 系统调用返回时
    • 硬件中断处理完成后
    • 进程上下文切换时
  2. 特殊情形
    • 连续多次阻塞同一信号:只有第一次会进入pending(标准信号)
    • SIGKILLSIGSTOP不能被阻塞
    • 实时信号(RT信号)会排队,不丢失(FIFO

3.4.2 sigpending

函数原型

#include <signal.h>int sigpending(sigset_t *set);

参数

  • set:指向 sigset_t 类型的指针,用于存储当前未决信号集合。

返回值

  • 成功:返回 0,并将未决信号集复制到 set 中。
  • 失败:返回 -1,并设置 errno(通常为 EFAULT,表示 set 指针无效)。

四、捕获信号

4.1 信号捕捉的流程

在这里插入图片描述
信号捕捉时,进程执行主控制流遇中断、异常或系统调用进入内核态;内核处理完相关事务准备回用户态前,经 do_signal() 检查当前进程可递送信号,若为自定义处理函数的信号,内核保存进程主控制流上下文,让 CPU 跳转到用户态执行信号处理函数;处理函数返回时借 sigreturn 再次陷入内核,内核通过 sys_sigreturn 恢复进程之前保存的主控制流上下文,最终进程回到用户态,从主控制流上次被中断处继续执行 ,实现异步信号的 “中断 - 处理 - 恢复” 流程,保障主逻辑被打断后可无缝续行。

在这里插入图片描述
这条水平线就是用户层代码和内核底层逻辑的 “分界线”,程序在不同权限、功能区域执行时,会以此为界完成切换,是理解信号处理中用户态与内核态交互的基础标识。

4.2 sigaction

函数原型

#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

参数:

  • signum:要操作的信号编号(如 SIGINTSIGTERMSIGCHLD 等 ,SIGKILLSIGSTOP 这类内核强制处理的信号无法通过它修改行为 )。
  • act:指向 struct sigaction 结构体的指针,**设置新的信号处理行为 **。若为 NULL,则不修改信号行为,仅用于查询。
  • oldact:指向 struct sigaction 结构体的指针,用于保存信号原来的处理行为 。若为 NULL,则不保存。

返回值:

  • 成功返回 0,失败返回 -1 并设置 errno(如信号编号无效、指针参数非法等)。

功能:
允许进程设置、查询特定信号的处理逻辑,定义进程收到信号时应执行的操作,比如:

  • 捕获信号并执行自定义处理函数(如程序崩溃时记录日志)。
  • 恢复信号的默认行为(如让 SIGINT 恢复 “终止进程” 的默认动作 )。
  • 忽略特定信号(如忽略 SIGCHLD 避免子进程变成僵尸进程的场景优化 )。

4.3 操作系统是怎么运行的

4.3.1 硬件中断

在这里插入图片描述

  • 中断向量表就是操作系统的一部分,启动就加载到内存中
  • 通过外部硬件中断,操作系统就不需要对外设进行任何周期性的检测或者轮询
  • 由外部设备触发的,中断系统运行流程,叫做硬件中断

4.3.2 时钟中断

问题:

  • 进程可以在操作系统的指挥下,被调度,被执行,那么操作系统自己被谁指挥,被谁推动执行呢?
  • 外部设备可以触发硬件中断,但是这个是需要用户或者设备自己触发,有没有自己可以定期触发的设备?
    在这里插入图片描述

答:

  • 操作系统的执行依赖于硬件引导流程中断驱动机制,本质是 “被动响应事件” 的系统,而非被外部实体 “指挥”。
  • 定时器设备通过周期性中断为操作系统提供时间基准,是实现进程调度、时间管理的核心硬件基础,其作用如同系统的 “心跳”。理解这两点,有助于深入掌握计算机系统的底层运行逻辑。

这样操作系统就能在硬件的推动下,自动调度了。

4.3.3 死循环

如果是这样,操作系统不就可以躺平了吗?对,操作系统自己不做任何事情,需要什么功能,就向中断向量表里面添加方法即可。操作系统的本质:就是一个死循环!

  • 这样,操作系统,就可以在硬件时钟的推动下,自动调度了。
  • 所以,什么是时间片?CPU为什么会有主频?为什么主频越快,CPU越快?

答:

  • 时间片是多任务系统分配 CPU 时间的基本单位,决定了进程切换的粒度。
  • 主频是 CPU 每秒的时钟周期数,直接影响指令执行的理论上限(每秒指令数 = 主频 / CPI(指令周期))。
  • 主频越快 CPU 越快的前提是 CPI 不变,但实际性能还受架构、缓存、指令集等因素影响。

4.3.4 软中断

  • 上述外部硬件中断,需要硬件设备触发。
  • 有没有可能,因为软件原因,也触发上面的逻辑?有!
  • 为了让操作系统支持进行系统调用,CPU也设计了对应的汇编指令(int或者syscall),可以让CPU内部触发中断逻辑。

所以:
在这里插入图片描述
问题:

  • 用户层怎么把系统调用号给操作系统?-寄存器(比如EAX)
  • 操作系统怎么把返回值给用户?-寄存器或者用户传入的缓冲区地址
  • 系统调用的过程,其实就是先intOx80syscall陷入内核,本质就是触发软中断,CPU就会自动执行系统调用的处理方法,而这个方法会根据系统调用号,自动查表,执行对应的方法
  • 系统调用号的本质:数组下标
  • 可是为什么我们用的系统调用,从来没有见过什么intOx80或者syscall呢?都是直接调用上层的函数的啊?
  • 那是因为Linux的 gnu C 标准库,给我们把几乎所有的系统调用全部封装了。

4.3.5 缺页中断?内存碎片处理?除零野指针错误?

  • 缺页中断?内存碎片处理?除零野指针错误?这些问题,全部都会被转换成为CPU内部的软中断,然后走中断处理例程,完成所有处理。有的是进行申请内存,填充页表,进行映射的。有的是用来处理内存碎片的,有的是用来给目标进行发送信号,杀掉进程等等。

所以:

  • 操作系统就是躺在中断处理例程上的代码块!
  • CPU内部的软中断,比如int 0x80或者syscall,我们叫做陷阱
  • CPU内部的软中断,比如除零/野指针等,我们叫做异常。(所以,能理解“缺页异常”为什么这么叫了吗?)

4.4 如何理解内核态和用户态

在这里插入图片描述
在这里插入图片描述

结论:

  • 操作系统无论怎么切换进程,都能找到同一个操作系统!换句话说系统调用的内核代码在共享的内核地址空间执行,但会访问当前进程的用户地址空间资源,并使用该进程的内核栈
  • 关于特权级别,涉及到段,段描述符,段选择子,DPL,CPL,RPL等概念,而现在芯片为了保证兼容性,已经非常复杂了,进而导致OS也必须得照顾它的复杂性,这块我们不做深究了。
  • 用户态就是执行用户[0,3]GB时所处的状态
  • 内核态就是执行内核[3,4]GB时所处的状态
  • 区分就是按照CPU内的CPL决定,CPL的全称是Current PrivilegeLevel,即当前特权级别。
  • 一般执行int0x80或者syscall软中断,CPL会在校验之后自动变更

五、可重入函数

5.1 什么是可重入函数?

可重入函数是指在多个执行流同时调用时不会产生副作用的函数。其核心特点是:

  • 线程安全:在多线程环境下被并发调用时,不会因共享资源(如全局变量)导致数据竞争。
  • 信号安全:在信号处理函数中被调用时,不会破坏程序状态(如正在执行的操作被中断)。

5.2 不可重入的典型场景与风险

不可重入函数在多线程或信号处理中可能引发以下问题:

  1. 全局变量或静态变量污染
// 不可重入函数示例:依赖全局变量
int total = 0;
int add_to_total(int value) {total += value;  // 多线程访问时可能导致数据竞争return total;
}

风险:若两个线程同时调用add_to_total,可能因线程切换导致计算错误(如两个线程各加 1,但结果只加了 1)。

  1. 标准库函数的不可重入性
    许多标准库函数依赖静态缓冲区或状态,如:
  • strtok():使用静态指针保存分割位置。
  • gmtime()/localtime():返回静态缓冲区的指针。

示例

// 不可重入函数示例:使用静态缓冲区
char* format_time(void) {time_t now = time(NULL);return ctime(&now);  // ctime()返回静态缓冲区,多线程调用会覆盖结果
}
  1. 信号处理中的不可重入风险
    若信号处理函数调用不可重入函数,可能导致:
  • 主程序正在执行的操作被中断,数据结构被破坏。
  • 信号处理函数与主程序同时修改共享资源,引发竞态条件。

5.3 可重入函数的设计原则

  1. 避免共享资源
    • 不使用全局变量或静态变量。
    • 若必须使用,通过互斥锁(如pthread_mutex_t)保护。
  2. 使用局部变量和栈
    所有数据存储在栈上(如函数参数、局部变量),每个调用独立拥有副本。
  3. 避免调用不可重入函数
    例如:
    • strtok_r()替代strtok()(带_r后缀的通常是可重入版本)。
    • gmtime_r()替代gmtime()

5.4 可重入函数的实现示例

// 可重入版本:使用线程局部存储(TLS)
#include <pthread.h>// 线程局部变量,每个线程独立拥有副本
__thread int thread_total = 0;int add_to_total(int value) {thread_total += value;  // 线程安全:每个线程使用自己的副本return thread_total;
}// 可重入版本:使用互斥锁保护全局变量
#include <pthread.h>int global_total = 0;
pthread_mutex_t total_mutex = PTHREAD_MUTEX_INITIALIZER;int add_to_total_safe(int value) {pthread_mutex_lock(&total_mutex);  // 加锁global_total += value;pthread_mutex_unlock(&total_mutex);  // 解锁return global_total;
}

5.5 五、信号处理中的可重入性

在信号处理函数中,仅能调用可重入函数(如write()_exit()),避免调用:

  • 标准 IO 函数(如printf()fprintf())。
  • 内存分配函数(如malloc()free())。
  • 浮点运算函数(如sin()cos())。

六、volatile

6.1 volatile 的本质与作用

volatile是 C/C++ 中的一个类型修饰符,用于告诉编译器:

  • 不要对变量进行优化(如缓存到寄存器或重排序)。
  • 每次访问变量时都直接从内存读取,写入时立即刷新到内存。

其核心作用是确保变量的访问与物理内存直接交互,而非编译器的临时缓存。

6.2 信号处理函数中的全局变量

  • 场景:在信号处理函数中修改主程序使用的全局变量。
  • 原因:信号可能在任意时刻触发,编译器不能假设变量不变。
  • 示例
volatile sig_atomic_t signal_received = 0;void signal_handler(int signo) {signal_received = 1;  // 原子操作,确保可见性
}int main() {signal(SIGINT, signal_handler);while (!signal_received) {  // 每次检查都从内存读取// 主程序工作...}return 0;
}

七、SIGCHLD 信号:Linux 进程管理的 “子进程通知机制”

7.1 SIGCHLD 信号的本质与作用

SIGCHLD(信号编号 17)是 Linux 系统中由内核自动发送给父进程的信号,用于通知以下事件:

  • 子进程终止(正常退出或被信号终止)。
  • 子进程暂停(如收到 SIGSTOP 信号)。
  • 子进程恢复(如收到 SIGCONT 信号)。

其核心作用是让父进程能够异步处理子进程状态变化,避免父进程持续轮询(如通过wait()阻塞等待)。

7.2 SIGCHLD 的默认行为与问题

  • 默认行为:忽略(进程收到信号后无动作)。
  • 潜在问题:若父进程未处理 SIGCHLD,子进程终止后会变成僵尸进程(Zombie Process),占用系统资源(如进程表项)。

7.3 处理 SIGCHLD 的三种方式

  1. 忽略信号(最简单但有风险)
// 忽略SIGCHLD信号,子进程终止后直接释放资源
signal(SIGCHLD, SIG_IGN);  // 或使用sigaction// 子进程代码
if (fork() == 0) {// 子进程执行...exit(0);  // 退出后不会变成僵尸进程
}

注意:Linux 中忽略 SIGCHLD 会让内核自动回收子进程资源,但某些 UNIX 系统可能不支持,建议使用方式 2 或 3。

  1. 捕获信号并调用 wait ()/waitpid ()
#include <signal.h>
#include <sys/wait.h>void sigchld_handler(int signo) {int status;// 非阻塞等待所有子进程,避免wait()阻塞while (waitpid(-1, &status, WNOHANG) > 0) {// 处理子进程退出状态if (WIFEXITED(status)) {printf("子进程正常退出,状态码: %d\n", WEXITSTATUS(status));} else if (WIFSIGNALED(status)) {printf("子进程被信号终止,信号: %d\n", WTERMSIG(status));}}
}int main() {// 注册信号处理函数struct sigaction sa;sa.sa_handler = sigchld_handler;sigemptyset(&sa.sa_mask);sa.sa_flags = SA_RESTART | SA_NOCLDSTOP;  // SA_NOCLDSTOP忽略暂停/恢复信号sigaction(SIGCHLD, &sa, NULL);// 创建子进程pid_t pid = fork();if (pid == 0) {// 子进程执行...sleep(2);exit(42);}// 父进程继续执行...return 0;
}

关键点:

  • 使用waitpid(-1, &status, WNOHANG)非阻塞回收多个子进程。
  • SA_RESTART标志避免系统调用被信号中断。
  • SA_NOCLDSTOP忽略子进程暂停 / 恢复事件,仅关注终止。
  1. 使用 sigaction 的 SA_NOCLDWAIT 标志(现代方式)
struct sigaction sa;
sa.sa_handler = SIG_IGN;  // 或自定义处理函数
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_NOCLDWAIT;  // 内核自动回收子进程,不产生僵尸
sigaction(SIGCHLD, &sa, NULL);

优势:内核自动释放子进程资源,无需手动调用wait()

7.4 调试与监控

查看僵尸进程:

ps aux | grep Z  # 显示状态为Z的僵尸进程

跟踪信号处理:

strace -e signal your_program  # 跟踪信号处理系统调用

7.5 常见误区与注意事项

  1. 竞态条件
    若父进程未捕获 SIGCHLD,可能导致:
    • 子进程已终止,但父进程未及时回收,变成僵尸。
    • 父进程调用wait()时,子进程尚未终止,导致阻塞。
  2. 信号丢失
    非实时信号(如 SIGCHLD)不排队,若多个子进程同时终止,可能只收到一个信号。需在处理函数中循环调用waitpid()回收所有子进程。
  3. 与 fork ()/exec () 的关系
    • fork()创建的子进程继承父进程的 SIGCHLD 处理方式。
    • exec()后,子进程保留 SIGCHLD 的处理方式(除非设置了SA_RESETHAND)。

7.6 总结

  • SIGCHLD 的核心价值
    提供异步机制让父进程感知子进程状态变化,避免轮询或阻塞等待。
  • 最佳实践
    • 优先使用SA_NOCLDWAIT自动回收子进程。
    • 若需获取子进程状态,在信号处理函数中循环调用waitpid()
  • 应用场景
    • 守护进程(如 init 进程管理所有子进程)。
    • 多进程服务器(如 Web 服务器 fork 子进程处理请求)。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.pswp.cn/diannao/86779.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

C++ 日志系统实战第五步:日志器的设计

全是通俗易懂的讲解&#xff0c;如果你本节之前的知识都掌握清楚&#xff0c;那就速速来看我的项目笔记吧~ 本文项目代码编写收尾&#xff01; 日志器类 (Logger) 设计&#xff08;建造者模式&#xff09; 日志器主要用于和前端交互。当我们需要使用日志系统打印 log 时&…

Spring Boot + MyBatis日志前缀清除方法

在 Spring Boot 结合 MyBatis 的应用中&#xff0c;清空日志前缀&#xff08;如 > 、< 等&#xff09;需要通过 自定义 MyBatis 的日志实现 或 修改日志模板 来实现。以下是两种常用方法&#xff1a; 方法 1&#xff1a;自定义 MyBatis 日志实现&#xff08;推荐&#xf…

【消息队列】——如何实现消息保序

目录 一、哪些场景需要消息保序?二、如何实现消息保序?三、保序消息的常见问题和应对策略3.1、重复消息3.2、节点故障3.3、分区扩容四、小结本文来源:极客时间vip课程笔记 一、哪些场景需要消息保序? 消息保序问题指的是,在通过消息中间件传递消息过程中,我们希望消费者收…

Transformer模型详解

Transformer Transformer真是个细节满满的框架呢&#xff0c;大三读到根本不敢看&#xff0c;考研复试前看了看&#xff0c;以为懂了其实差得还远&#xff0c;两个多月前看了&#xff0c;还是一知半解&#xff0c;如今终于经过细细分析&#xff0c;算是知道了Transformer的基本…

火山引擎发布豆包大模型 1.6 与视频生成模型 Seedance 1.0 pro

6 月 11 日&#xff0c;在火山引擎 FORCE 原动力大会上&#xff0c;字节跳动旗下火山引擎正式发布豆包大模型 1.6、豆包・视频生成模型 Seedance 1.0 pro、豆包・语音播客模型&#xff0c;豆包・实时语音模型也在火山引擎全量上线&#xff0c;豆包大模型家族已成为拥有全模态、…

PH热榜 | 2025-06-12

1. Atlas 标语&#xff1a;几秒钟内了解定价情况 介绍&#xff1a;获取即插即用的定价页面&#xff0c;让你轻松赚钱&#xff0c;不再辛苦操劳。 产品网站&#xff1a; 立即访问 Product Hunt&#xff1a; View on Product Hunt 关键词&#xff1a;Atlas, 定价快速, 插件式…

ChatGPT革命升级!o3-pro模型重磅发布:开启AI推理新纪元

2025年6月10日&#xff0c;OpenAI以一场低调而震撼的发布&#xff0c;正式推出了新一代推理模型o3-pro&#xff0c;这标志着人工智能在复杂问题解决领域的重大突破。作为ChatGPT Pro和Team订阅用户的专属工具&#xff0c;o3-pro不仅重新定义了AI的可靠性标准&#xff0c;更以其…

NVIDIA Isaac GR00T N1.5 适用于 LeRobot SO-101 机械臂

系列文章目录 目录 系列文章目录 前言 一、简介 二、详细教程 2.1 数据集准备 2.1.1 创建或下载您的数据集 2.1.2 配置模态文件 2.2 模型微调 2.3 开环评估 2.4 部署 &#x1f389; 快乐编程&#xff01;&#x1f4bb;&#x1f6e0;️ 立即开始&#xff01; 前言 一…

【编译工具】(自动化)自动化测试工具:如何让我的开发效率提升300%并保证代码质量?

目录 引言&#xff1a;自动化测试在现代开发中的关键作用 一、自动化测试金字塔&#xff1a;构建高效的测试策略 &#xff08;1&#xff09;测试金字塔模型 &#xff08;2&#xff09;各层级代表工具 二、前端自动化测试实战&#xff1a;Jest Cypress &#xff08;1&…

R语言缓释制剂QBD解决方案之一

本文是《Quality by Design for ANDAs: An Example for Immediate-Release Dosage Forms》缓释制剂包衣处方研究的R语言解决方案。 ER聚合物包衣处方优化研究 基于初步风险评估和初始可行性研究&#xff0c;进行带3个中心点的24-1分式析因DOE。药物的释放被识别为CQA。本研究的…

行为模式-命令模式

定义&#xff1a; 命令模式是一个高内聚的模式&#xff0c;其定义为&#xff1a;Encapsulate a request as an object,thereby letting you parameterize clients with different requests,queue or log requests,and support undoable operations.&#xff08;将一个请求封装成…

Ubuntu 24.04 上安装与 Docker 部署 Sentinel

Ubuntu 24.04 上安装与 Docker 部署 Sentinel 一、Sentinel 简介 Sentinel 是阿里巴巴开源的分布式系统流量控制组件&#xff0c;提供流量控制、熔断降级和系统负载保护等功能。它通过可视化控制台&#xff08;Dashboard&#xff09;实现实时监控和规则管理&#xff0c;是微服…

IP 地址查询在证券交易中的应用方式

网络安全保障与IP地址查询 证券交易平台存储着海量投资者的敏感信息以及巨额资金的交易数据&#xff0c;是网络攻击的重点目标。IP 地址查询在检测异常登录行为方面至关重要。例如&#xff0c;当一个账户短时间内先在国内某城市登录&#xff0c;随后又在境外 IP 地址发起交易操…

Flutter 常用组件详解:Text、Button、Image、ListView 和 GridView

Flutter 作为 Google 推出的跨平台 UI 框架&#xff0c;凭借其高效的渲染性能和丰富的组件库&#xff0c;已经成为移动应用开发的热门选择。本文将深入探讨 Flutter 中最常用的五个基础组件&#xff1a;Text、Button、Image、ListView 和 GridView&#xff0c;帮助开发者快速掌…

docker 单机部署redis集群(一)

docker 部署redis集群 1、创建redis网卡 docker network create redis --subnet 172.38.0.0/16查看网卡信息 docker network ls docker network inspect redis2、创建redis配置 #使用脚本创建6个redis配置for port in $(seq

MySQL 索引学习笔记

1.二叉树&#xff0c;红黑树&#xff0c;B 树&#xff0c;B树 二叉树&#xff1a;就是每个节点最多只能有两个子节点的树&#xff1b; 红黑树&#xff1a;就是自平衡二叉搜索树&#xff0c;红黑树通过一下五个规则构建&#xff1a; 1.节点只能是红色或黑色&#xff1b; 2.根…

Windows安装docker及使用

下载 https://www.docker.com/ 安装 启动 此时拉取镜像会报错 Error response from daemon: Get "https://registry-1.docker.io/v2/": net/http: request canceled while waiting for connection (Client.Timeout exceeded while awaiting headers) 配置引擎 添加以…

多参表达式Hive UDF

支持的操作符 &#xff1a;跳过&#xff0c;即无条件筛选&#xff1a;等于!&#xff1a;不等于range&#xff1a;区间内&#xff0c;range[n,m]表示 between n and mnrange&#xff1a;区间外&#xff0c;即not between andin&#xff1a;集合内&#xff0c;in(n,m,j,k)表示 in…

GO后端开发内存管理及参考答案

什么是 Go 的逃逸分析&#xff08;Escape Analysis&#xff09;&#xff0c;为什么需要它&#xff1f; Go 的逃逸分析是一种编译时技术&#xff0c;用于确定变量的生命周期是否超出其创建的函数作用域。通过分析变量的使用方式&#xff0c;编译器能够判断变量是否需要在堆上分…

未来智能系统演进路线:从AGI到ASI的技术蓝图

引言&#xff1a;智能革命的下一个十年 在AI技术突破性发展的当下&#xff0c;我们正站在通用人工智能&#xff08;AGI&#xff09;向人工超级智能&#xff08;ASI&#xff09;跃迁的关键转折点。本文将系统解析未来3-10年的技术演进路径&#xff0c;通过模块化组件插件&#…