Linux进程信号:从基础概念到内核底层原理

前言:信号是Linux系统中最古老的进程间通信方式之一,理解信号对于系统编程至关重要。本文将从生活实例出发,逐步深入到内核实现,涵盖信号概念、产生、保存、处理、常见陷阱以及工程实践,帮你彻底掌握Linux进程信号。


Linux进程信号:从基础概念到内核底层原理

    • 一、信号的基本概念
      • [1.1 什么是信号](#1.1 什么是信号)
      • [1.2 信号的三大处理方式](#1.2 信号的三大处理方式)
      • [1.3 常用信号速查表](#1.3 常用信号速查表)
      • [1.4 标准信号 vs 实时信号](#1.4 标准信号 vs 实时信号)
      • [1.5 可靠信号与不可靠信号](#1.5 可靠信号与不可靠信号)
    • 二、信号的完整生命周期
    • 三、信号的产生方式
      • [3.1 终端按键产生(仅限前台进程)](#3.1 终端按键产生(仅限前台进程))
      • [3.2 硬件异常产生](#3.2 硬件异常产生)
      • [3.3 系统调用/函数产生](#3.3 系统调用/函数产生)
        • [3.3.1 `kill()` --- 向任意进程发送信号](#3.3.1 kill() — 向任意进程发送信号)
        • [3.3.2 `raise()` --- 给当前进程发信号](#3.3.2 raise() — 给当前进程发信号)
        • [3.3.3 `abort()` --- 异常终止(给自己发SIGABRT)](#3.3.3 abort() — 异常终止(给自己发SIGABRT))
      • [3.4 软件条件产生](#3.4 软件条件产生)
        • [3.4.1 `alarm()` --- 闹钟信号](#3.4.1 alarm() — 闹钟信号)
        • [3.4.2 SIGPIPE --- 管道断裂](#3.4.2 SIGPIPE — 管道断裂)
      • [3.5 命令行发送信号](#3.5 命令行发送信号)
    • 四、信号的保存(Pending与Block)
      • [4.1 内核数据结构](#4.1 内核数据结构)
      • [4.2 信号集操作函数](#4.2 信号集操作函数)
      • [4.3 修改阻塞掩码:`sigprocmask`](#4.3 修改阻塞掩码:sigprocmask)
      • [4.4 获取pending信号集:`sigpending`](#4.4 获取pending信号集:sigpending)
      • [4.5 完整示例:阻塞2号信号并观察pending变化](#4.5 完整示例:阻塞2号信号并观察pending变化)
    • 五、信号的捕捉与处理
      • [5.1 信号捕捉函数](#5.1 信号捕捉函数)
        • [5.1.1 `signal()`(简单但不可靠)](#5.1.1 signal()(简单但不可靠))
        • [5.1.2 `sigaction()`(推荐,POSIX标准)](#5.1.2 sigaction()(推荐,POSIX标准))
        • [5.1.3 信号嵌套与 sa_mask 的深入解释](#5.1.3 信号嵌套与 sa_mask 的深入解释)
        • [5.1.4 `sigqueue()` --- 发送带数据的信号(实时信号专用)](#5.1.4 sigqueue() — 发送带数据的信号(实时信号专用))
      • [5.2 暂停进程等待信号:`pause()` 与 `sigsuspend()`](#5.2 暂停进程等待信号:pause()sigsuspend())
        • [5.2.1 `pause()` --- 简单挂起](#5.2.1 pause() — 简单挂起)
        • [5.2.2 `sigsuspend()` --- 原子挂起(解决竞态)](#5.2.2 sigsuspend() — 原子挂起(解决竞态))
      • [5.3 信号处理流程(内核态↔用户态切换)](#5.3 信号处理流程(内核态↔用户态切换))
      • [5.4 穿插话题 - 操作系统是怎么运行的?](#5.4 穿插话题 - 操作系统是怎么运行的?)
        • [5.4.1 硬件中断](#5.4.1 硬件中断)
        • [5.4.2 时钟中断](#5.4.2 时钟中断)
        • [5.4.3 死循环](#5.4.3 死循环)
        • [5.4.4 软中断(系统调用)](#5.4.4 软中断(系统调用))
        • [5.4.5 缺页中断与异常](#5.4.5 缺页中断与异常)
      • [5.5 如何理解内核态和用户态](#5.5 如何理解内核态和用户态)
        • [5.5.1 特权级别 (Ring 0 & Ring 3)](#5.5.1 特权级别 (Ring 0 & Ring 3))
        • [5.5.2 虚拟地址空间划分 (32位为例)](#5.5.2 虚拟地址空间划分 (32位为例))
        • [5.5.3 用户态与内核态的切换流程](#5.5.3 用户态与内核态的切换流程)
        • [5.5.4 追踪 `fopen` 调用 (系统调用封装的体现)](#5.5.4 追踪 fopen 调用 (系统调用封装的体现))
    • 六、可重入函数与volatile
      • [6.1 不可重入函数](#6.1 不可重入函数)
      • [6.2 volatile关键字](#6.2 volatile关键字)
        • [6.2.1 `volatile` 的局限性](#6.2.1 volatile 的局限性)
    • 七、SIGCHLD信号与僵尸进程回收
      • [7.1 问题背景](#7.1 问题背景)
      • [7.2 SIGCHLD机制](#7.2 SIGCHLD机制)
        • [`waitpid()` 详解](#waitpid() 详解)
      • [7.3 SA_NOCLDSTOP 与 SIGCONT 的联动](#7.3 SA_NOCLDSTOP 与 SIGCONT 的联动)
      • [7.4 Linux扩展:忽略SIGCHLD自动回收](#7.4 Linux扩展:忽略SIGCHLD自动回收)
      • [7.5 查看子进程退出状态](#7.5 查看子进程退出状态)
    • 八、常见易错点与最佳实践
      • [8.1 易错点总结](#8.1 易错点总结)
      • [8.2 最佳实践清单](#8.2 最佳实践清单)
    • 九、总结
      • [9.1 Linux进程信号知识点汇总表](#9.1 Linux进程信号知识点汇总表)
      • [9.2 经典代码模板](#9.2 经典代码模板)

一、信号的基本概念

1.1 什么是信号

信号(Signal)是操作系统向进程发送的异步通知事件,属于软中断机制。当进程收到信号时,操作系统会中断当前执行流,转而执行预设的信号处理动作。

🚚 生活类比:你在网上买了快递,快递员打电话通知你(信号产生),你正在打游戏不能立刻取(不立即处理),但你记下了有快递到了(信号保存),等游戏结束再去取(合适时机处理)。整个通知过程是异步的------你无法精确预判快递员何时打电话。

1.2 信号的三大处理方式

每个信号都预先绑定了默认行为,但用户可以修改(除了SIGKILL和SIGSTOP):

处理方式 说明 示例
默认动作 (SIG_DFL) 系统预定义行为:终止、忽略、暂停、Core Dump SIGINT默认终止进程
忽略 (SIG_IGN) 收到信号后直接丢弃,不执行任何操作 signal(SIGINT, SIG_IGN)
自定义捕捉 注册回调函数,收到信号时执行用户代码 signal(SIGINT, handler)

⚠️ 特权信号SIGKILL(9)SIGSTOP(19) 既不能被忽略,也不能被捕获,更不能被阻塞------这是系统留给管理员最后的"核武器"。

1.3 常用信号速查表

信号宏 编号 触发场景 默认动作
SIGHUP 1 终端挂起或控制进程死亡 Term
SIGINT 2 Ctrl+C Term
SIGQUIT 3 Ctrl+\ Core
SIGILL 4 非法指令 Core
SIGTRAP 5 断点/调试陷阱 Core
SIGABRT 6 abort() 调用 Core
SIGFPE 8 除零、浮点溢出 Core
SIGKILL 9 kill -9(不可捕获) Term
SIGSEGV 11 段错误(野指针) Core
SIGPIPE 13 向关闭的管道写数据 Term
SIGALRM 14 alarm() 超时 Term
SIGTERM 15 kill默认信号(优雅终止) Term
SIGCHLD 17 子进程状态变化 Ignore
SIGCONT 18 继续暂停的进程 Cont
SIGSTOP 19 Ctrl+Z(不可捕获) Stop

Core vs Term:Core表示进程崩溃时会生成core dump文件(内存镜像),可用于gdb事后调试;Term仅表示正常终止。

1.4 标准信号 vs 实时信号

Linux 共有 64 个信号(1-64),分为两类:

特性 标准信号(1-31) 实时信号(34-64)
排队机制 不排队,多个相同信号可能合并为 1 个 排队,每个信号都独立保存
传递顺序 无保证 优先级:编号小的先递达
附带数据 不能携带额外数据 可通过 sigqueue() 附带 union sigval 数据
用途 系统预定义含义 应用程序自定义
cpp 复制代码
// 实时信号发送(可附带数据)
union sigval value;
value.sival_int = 42;
sigqueue(pid, SIGRTMIN, value);   // 需要包含 <signal.h>

// 接收时取出数据(需使用 SA_SIGINFO)
void handler(int sig, siginfo_t *info, void *ctx) {
    int data = info->si_value.sival_int;  // 取出附带数据
}

1.5 可靠信号与不可靠信号

这是理解信号行为的核心概念,直接影响程序的健壮性。

特性 不可靠信号 可靠信号
信号范围 1~31(标准信号) 34~64(实时信号)
排队行为 不排队。同一信号多次产生,pending位图只记录1次(可能丢失) 排队。每次产生都会独立记录,不会丢失
历史来源 早期Unix只有pending位图,无法计数 POSIX.1b扩展,改用队列结构
典型问题 快速连续发送3次SIGINT,进程可能只收到1次 快速连续发送3次SIGRTMIN,进程收到3次

工程启示

  • 对于关键通知(如进程退出指令),不应依赖标准信号的"多发必达"
  • 若需要可靠通知,使用实时信号 + sigqueue()
  • 在标准信号处理中,应采用容错设计(如循环检查、标志位 + 轮询)

二、信号的完整生命周期

信号从产生到处理,经历三个核心阶段:

复制代码
产生 → 保存(未决)→ 处理(递达)
  1. 产生 :触发条件生成信号,内核将其记录到目标进程的task_struct
  2. 保存(未决Pending) :信号已抵达但尚未处理,内核用pending位图 标记(1 bit/信号)。对于实时信号,内核使用队列存储
  3. 处理(递达Delivery):进程从内核态返回用户态前,内核检查pending位图,执行对应处理函数

📌 关键理解:信号不是立即处理的!进程可能正在执行更高优先级的任务(如内核态操作),所以信号先被"保存",等"合适时机"再处理。

信号生命周期示意图:

三、信号的产生方式

3.1 终端按键产生(仅限前台进程)

按键 信号 作用
Ctrl+C SIGINT(2) 中断前台进程
Ctrl+\ SIGQUIT(3) 退出并生成core文件
Ctrl+Z SIGSTOP(19) 暂停前台进程(挂起)
bash 复制代码
# 前台运行程序
./a.out

# 后台运行(无法接收键盘信号)
./a.out &

# 查看后台任务
jobs

# 将后台任务切到前台
fg %1

# 让暂停的任务后台继续运行
bg %1

键盘硬件中断/CPU/OS 处理流程图:

3.2 硬件异常产生

当CPU或MMU检测到异常时,内核会向当前进程发送相应信号:

cpp 复制代码
// 除零错误 → SIGFPE(8)
int a = 10;
a /= 0;   // 触发SIGFPE

// 野指针 → SIGSEGV(11)
int *p = nullptr;
*p = 100; // 触发SIGSEGV

原理:CPU内部有状态寄存器(如溢出标志),当除零发生时,CPU触发陷阱(trap),进入内核异常处理程序,内核识别异常类型后向当前进程的pending位图写入对应信号。

3.3 系统调用/函数产生

3.3.1 kill() --- 向任意进程发送信号
c 复制代码
#include <sys/types.h>
#include <signal.h>

int kill(pid_t pid, int sig);

参数详解

参数 说明
pid > 0 信号发送给进程ID为pid的进程
pid == 0 信号发送给与调用进程属于同一进程组的所有进程(包括调用进程自身)
pid == -1 信号发送给调用进程有权发送信号的所有进程(除init(PID=1)和自身外,需有权限)
pid < -1 信号发送给**进程组ID等于
sig 要发送的信号编号。若sig==0,则不发送信号,仅用于检测进程是否存在(空信号)

返回值 :成功返回 0;失败返回 -1,并设置 errno(常见错误:ESRCH进程不存在、EPERM权限不足)。

cpp 复制代码
// 示例:实现自己的kill命令
#include <iostream>
#include <signal.h>
#include <sys/types.h>
#include <cstdlib>

int main(int argc, char *argv[]) {
    if (argc != 3) {
        std::cerr << "Usage: " << argv[0] << " -signum pid" << std::endl;
        return 1;
    }
    int signum = std::stoi(argv[1] + 1);  // 去掉'-'前缀
    pid_t pid = std::stoi(argv[2]);
    if (kill(pid, signum) == -1) {
        perror("kill failed");
        return 1;
    }
    return 0;
}
3.3.2 raise() --- 给当前进程发信号
c 复制代码
#include <signal.h>

int raise(int sig);

参数sig --- 要发送的信号编号。

返回值:成功返回 0;失败返回非零值(注意:不是 -1,而是非零)。

raise(sig) 等价于 kill(getpid(), sig)

3.3.3 abort() --- 异常终止(给自己发SIGABRT)
c 复制代码
#include <stdlib.h>

void abort(void);

说明

  • 向调用进程发送 SIGABRT(6) 信号
  • 该函数从不返回(除非捕获SIGABRT且处理函数不退出)
  • 若SIGABRT被捕获且处理函数返回,abort() 仍会再次发送SIGABRT并终止进程(保证最终退出)
  • 会刷新stdio缓冲区并关闭流(未捕获时)
cpp 复制代码
// abort 的典型行为
void handler(int sig) {
    // 做一些清理...
    // 即使返回,abort() 仍会终止进程
}
signal(SIGABRT, handler);
abort();  // 永远不会返回

3.4 软件条件产生

3.4.1 alarm() --- 闹钟信号
c 复制代码
#include <unistd.h>

unsigned int alarm(unsigned int seconds);

参数seconds --- 定时秒数。若为0,则取消当前闹钟。

返回值

  • 返回上一次闹钟剩余的秒数(无论新设置是否成功)
  • 若之前没有闹钟,返回0

重要特性

  • 一个进程同一时间只能有一个闹钟,重复调用会覆盖上一次
  • 闹钟超时后,内核向当前进程发送 SIGALRM(14) 信号,默认终止进程
  • 闹钟超时是一次性的,不会重复触发

返回值的使用场景

cpp 复制代码
// 场景:先设置10秒闹钟,3秒后修改为5秒闹钟
alarm(10);                // 设置10秒倒计时
sleep(3);                 // 3秒后...
int remain = alarm(5);   // 重置为5秒,remain = 7(10-3)
// 此时闹钟还有5秒

// 场景:取消闹钟并获取剩余时间
int remain = alarm(0);   // remain = 上一次闹钟剩余秒数,同时闹钟被取消
// 这在实现"限时操作但允许提前完成"时很有用
3.4.2 SIGPIPE --- 管道断裂

当管道的读端关闭,写端继续调用write()时,内核会向写进程发送SIGPIPE(13),默认终止进程。这个信号在网络编程管道通信中非常常见。

典型场景 :客户端向已关闭的 socket 连接执行 send()/write()

解决方案(三选一):

cpp 复制代码
// 方法1:忽略 SIGPIPE,通过 write 返回 -1 和 errno=EPIPE 处理
signal(SIGPIPE, SIG_IGN);

// 方法2:设置 send 的 flags 为 MSG_NOSIGNAL(仅限Linux socket)
send(fd, buf, len, MSG_NOSIGNAL);

// 方法3:捕获信号,自行处理(可记录日志)
void pipe_handler(int sig) {
    // 记录错误,然后可以忽略或优雅关闭
}
signal(SIGPIPE, pipe_handler);

推荐做法 :对于服务器程序,通常使用方法1(全局忽略),然后依据 write 的返回值进行错误处理。

3.5 命令行发送信号

bash 复制代码
kill -9 1234        # 发送SIGKILL强制杀死
kill -15 1234       # 发送SIGTERM优雅终止(默认)
kill -STOP 1234     # 发送SIGSTOP暂停
kill -CONT 1234     # 发送SIGCONT继续
kill -SIGSEGV 1234  # 发送段错误信号(模拟崩溃)

四、信号的保存(Pending与Block)

4.1 内核数据结构

每个进程的task_struct(PCB)中维护了两个关键位图和一个处理函数表:

复制代码
┌─────────────────────────────────────────┐
│            task_struct (PCB)            │
├─────────────────────────────────────────┤
│  pending 位图  │  block 位图  │  handler[] │
│  (未决信号)     │  (阻塞掩码)    │  (处理动作) │
└─────────────────────────────────────────┘
  • pending位图 :记录哪些信号已经产生但尚未递达。bit=1表示该信号处于未决状态。对于实时信号(34~64),内核使用额外的队列来保证排队。
  • block位图(信号屏蔽字):记录哪些信号被阻塞。bit=1表示该信号被阻塞,即使产生也不会递达,直到解除阻塞。
  • handler数组:存储每个信号的处理函数指针(SIG_DFL、SIG_IGN或自定义函数)。

⚠️ 阻塞 ≠ 忽略:忽略是信号递达后直接丢弃;阻塞是信号暂时不能递达,依然保存在pending中,解除阻塞后才会递达。

内核数据结构示意图 task_struct 指向 pending/block/handler:

4.2 信号集操作函数

sigset_t 是信号集合类型,使用前必须初始化。所有操作函数返回 0 表示成功,-1 表示失败(并设置 errno),sigismember 返回 1/0/-1。

c 复制代码
#include <signal.h>

int sigemptyset(sigset_t *set);      // 清空集合
int sigfillset(sigset_t *set);       // 填满所有信号(包括实时信号)
int sigaddset(sigset_t *set, int signo);   // 添加指定信号
int sigdelset(sigset_t *set, int signo);   // 删除指定信号
int sigismember(const sigset_t *set, int signo); // 判断:1在集合中,0不在,-1错误

4.3 修改阻塞掩码:sigprocmask

c 复制代码
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

参数

  • how:修改方式
    • SIG_BLOCK:将 set 中的信号添加到 当前阻塞集(block |= set
    • SIG_UNBLOCK:将 set 中的信号 当前阻塞集中移除(block &= ~set
    • SIG_SETMASK:直接将阻塞集设置为 setblock = set
  • set:新设置的信号集(若为NULL,则不修改,仅通过oldset获取当前值)
  • oldset:非空时,返回旧的阻塞集(用于事后恢复)

返回值:成功返回 0;失败返回 -1 并设置 errno。

⚠️ 若调用sigprocmask解除了某些信号的阻塞,且这些信号正处于pending状态,那么在返回前会至少递达其中一个信号。

4.4 获取pending信号集:sigpending

c 复制代码
int sigpending(sigset_t *set);
  • 参数set 输出参数,返回当前进程的未决信号集
  • 返回值:成功返回 0;失败返回 -1

4.5 完整示例:阻塞2号信号并观察pending变化

cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <signal.h>

void PrintPending(sigset_t &pending) {
    std::cout << "pending: ";
    for (int signo = 31; signo >= 1; signo--) {
        std::cout << (sigismember(&pending, signo) ? "1" : "0");
    }
    std::cout << std::endl;
}

void handler(int signo) {
    std::cout << "捕获到信号 " << signo << ",正在处理..." << std::endl;
}

int main() {
    // 1. 自定义捕捉2号信号
    signal(SIGINT, handler);
    
    // 2. 设置阻塞集,阻塞2号信号
    sigset_t block_set, old_set;
    sigemptyset(&block_set);
    sigaddset(&block_set, SIGINT);
    sigprocmask(SIG_BLOCK, &block_set, &old_set);
    
    std::cout << "已屏蔽SIGINT(Ctrl+C),请尝试按Ctrl+C..." << std::endl;
    
    int cnt = 10;
    while (cnt--) {
        sigset_t pending;
        sigpending(&pending);
        PrintPending(pending);
        
        if (cnt == 5) {
            std::cout << "5秒后解除屏蔽..." << std::endl;
            sigprocmask(SIG_SETMASK, &old_set, nullptr);
        }
        sleep(1);
    }
    return 0;
}

运行效果:前5秒按Ctrl+C,pending位图中2号信号对应的bit会变为1;解除屏蔽后,pending位图清零,并执行自定义handler。


五、信号的捕捉与处理

5.1 信号捕捉函数

5.1.1 signal()(简单但不可靠)
c 复制代码
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);

参数

  • signum:信号编号
  • handler:可以是 SIG_IGN(忽略)、SIG_DFL(默认)或自定义函数指针

返回值 :成功返回之前的 信号处理函数指针(或SIG_ERR),失败返回 SIG_ERR 并设置 errno。

⚠️ signal() 在不同Unix系统上行为不一致(有的系统在handler执行后会自动重置为默认),且缺乏 sa_mask 等精细控制,推荐使用 sigaction()

cpp 复制代码
// 示例:捕获SIGINT,不退出程序
#include <iostream>
#include <signal.h>
#include <unistd.h>

void sigint_handler(int sig) {
    std::cout << "收到SIGINT(" << sig << "),但我不退出!" << std::endl;
}

int main() {
    if (signal(SIGINT, sigint_handler) == SIG_ERR) {
        perror("signal");
        return 1;
    }
    while (true) {
        std::cout << "Running... pid=" << getpid() << std::endl;
        sleep(1);
    }
    return 0;
}
5.1.2 sigaction()(推荐,POSIX标准)
c 复制代码
#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);

参数

  • signum:要设置的信号编号(不可为 SIGKILL 或 SIGSTOP)
  • act:新的配置(非NULL时)
  • oldact:输出旧的配置(非NULL时)

返回值:成功返回 0,失败返回 -1 并设置 errno。

结构体成员详解

成员 说明
sa_handler 信号处理函数,取值 SIG_IGN, SIG_DFL 或自定义函数
sa_sigaction sa_flags 包含 SA_SIGINFO 时使用此字段,可接收更多信息(来源、数据等)
sa_mask 执行信号处理函数期间 ,内核会额外阻塞 sa_mask 中的信号(加上当前信号本身)
sa_flags 见下表

sa_flags 常用选项

标志 含义
SA_RESTART 被信号中断的系统调用自动重启(如read、write、accept等)
SA_NODEFER 处理信号期间不自动阻塞当前信号(允许递归/嵌套)
SA_SIGINFO 使用 sa_sigaction 代替 sa_handler,可获得更多信息
SA_RESETHAND 处理完信号后恢复默认动作(类似旧版signal的自动重置行为)
SA_NOCLDSTOP 若信号为 SIGCHLD,则子进程停止/继续时不产生信号(仅终止时产生)
cpp 复制代码
// sigaction 完整示例 + 参数/返回值说明
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <cstring>

void handler(int sig) {
    std::cout << "捕获信号: " << sig << std::endl;
    sleep(3);  // 模拟耗时处理
    std::cout << "处理完毕" << std::endl;
}

int main() {
    struct sigaction act, oldact;
    memset(&act, 0, sizeof(act));
    act.sa_handler = handler;
    sigemptyset(&act.sa_mask);
    sigaddset(&act.sa_mask, SIGQUIT);  // 处理SIGINT时额外屏蔽SIGQUIT
    act.sa_flags = SA_RESTART;         // 自动重启被中断的系统调用
    
    if (sigaction(SIGINT, &act, &oldact) == -1) {
        perror("sigaction");
        return 1;
    }
    
    // 可查看旧的处理方式
    if (oldact.sa_handler != SIG_DFL) {
        std::cout << "原处理方式不是默认" << std::endl;
    }
    
    while (true) {
        std::cout << "主循环... pid=" << getpid() << std::endl;
        sleep(1);
    }
    return 0;
}
5.1.3 信号嵌套与 sa_mask 的深入解释

当信号处理函数执行时,当前信号会被自动屏蔽 (防止递归触发)。但其他信号仍可打断当前处理函数

复制代码
主程序 ──> SIGINT handler 执行中
                 │
                 ├──> SIGQUIT 到达
                 │        └──> 中断当前 handler,执行 SIGQUIT handler
                 │             └──> 返回继续执行 SIGINT handler
                 └──> SIGINT handler 继续

这会导致问题:如果两个 handler 操作了共享数据,就会出现不可重入问题。

sa_mask 的真正威力 :在处理 SIGINT 期间,内核会自动将 sa_mask 中指定的信号临时加入进程阻塞掩码,防止它们打断当前处理函数。

cpp 复制代码
struct sigaction act;
act.sa_handler = handler;
sigemptyset(&act.sa_mask);
sigaddset(&act.sa_mask, SIGQUIT);  // 处理期间也屏蔽 SIGQUIT
act.sa_flags = 0;
sigaction(SIGINT, &act, nullptr);

若需要允许嵌套 (不推荐,除非明确需要),可设置 SA_NODEFER

cpp 复制代码
act.sa_flags = SA_NODEFER;  // 当前信号也不屏蔽,允许递归
5.1.4 sigqueue() --- 发送带数据的信号(实时信号专用)
c 复制代码
#include <signal.h>

int sigqueue(pid_t pid, int sig, const union sigval value);

参数

  • pid:目标进程ID(与kill相同规则)
  • sig:要发送的信号编号(通常为实时信号 SIGRTMIN 至 SIGRTMAX)
  • value:附带数据,类型为 union sigval(可包含整型或指针)
c 复制代码
union sigval {
    int   sival_int;
    void *sival_ptr;
};

返回值:成功返回 0;失败返回 -1 并设置 errno(常见:EINVAL 信号无效、EPERM 权限不足等)。

特点 :信号会进入目标进程的队列(排队),并附带用户数据。

cpp 复制代码
// 发送端
union sigval val;
val.sival_int = 42;
sigqueue(target_pid, SIGRTMIN+1, val);

5.2 暂停进程等待信号:pause()sigsuspend()

5.2.1 pause() --- 简单挂起
c 复制代码
#include <unistd.h>

int pause(void);

说明 :使调用进程挂起 (睡眠),直到捕获到一个信号。当信号处理函数返回后,pause() 返回 -1,并设置 errno 为 EINTR

⚠️ pause() 存在竞态条件 (见下文),推荐使用 sigsuspend()

cpp 复制代码
// 配合alarm实现定时等待
alarm(5);
pause();   // 等待5秒,直到SIGALRM唤醒
5.2.2 sigsuspend() --- 原子挂起(解决竞态)
c 复制代码
#include <signal.h>

int sigsuspend(const sigset_t *mask);

参数mask --- 临时使用的信号屏蔽字(调用期间替代当前屏蔽字)

说明sigsuspend() 执行以下原子操作:

  1. 将进程的信号屏蔽字设置为 mask
  2. 挂起进程直到收到未被屏蔽的信号
  3. 信号处理函数返回后,恢复原来的屏蔽字,然后返回 -1(errno = EINTR)

为什么需要它? pause() 存在竞态窗口:

cpp 复制代码
// 危险代码:信号可能在 sigprocmask 和 pause 之间到达
sigprocmask(SIG_UNBLOCK, &set, nullptr);  // 解除阻塞
// ⚡ 若信号在这里到达并处理完毕
pause();  // 将永远挂起!

安全做法

cpp 复制代码
sigset_t new_mask, old_mask;
sigemptyset(&new_mask);
sigaddset(&new_mask, SIGINT);
sigprocmask(SIG_BLOCK, &new_mask, &old_mask);  // 阻塞 SIGINT

// 做一些需要被保护的操作...

// 原子地:将信号掩码替换为 old_mask 并挂起进程
sigsuspend(&old_mask);

sigsuspend 是编写可靠信号处理逻辑的基石,常用于等待特定信号的同时不丢失其他信号。

5.3 信号处理流程(内核态↔用户态切换)

复制代码
用户态 main() ──┬──> 系统调用/异常/中断 ──> 内核态处理
                │                              │
                │                              ↓
                │                        处理完成,准备返回用户态
                │                              │
                │                        检查 pending & block
                │                              │
                │                    ┌─────────┴─────────┐
                │                    │ 有未阻塞的信号?   │
                │                    └─────────┬─────────┘
                │                          yes │
                │                              ↓
                │                     ┌─────────────────┐
                │                     │ 执行信号处理动作  │
                │                     └────────┬────────┘
                │                              │
                │               ┌──────────────┼──────────────┐
                │               │              │              │
                │            SIG_DFL        SIG_IGN      自定义handler
                │               │              │              │
                │           终止/暂停       丢弃          ┌─────┴─────┐
                │                                         │ 切回用户态 │
                │                                         │ 执行handler│
                │                                         │  返回内核  │
                │                                         └─────┬─────┘
                │                                               │
                └───────────────────────────────────────────────┘
                                ↓
                        恢复main函数上下文继续执行

关键点

  • 信号处理函数在用户态执行(安全原因)
  • 处理函数返回后,通过特殊的sigreturn系统调用再次进入内核,然后恢复原执行流

信号捕捉流程 用户态↔内核态切换示意图:


5.4 穿插话题 - 操作系统是怎么运行的?

说明:理解这部分内容,您将洞悉信号产生的底层硬件机制,以及为什么信号处理要从内核态切回用户态。

5.4.1 硬件中断

操作系统不需要轮询外设。外部设备(键盘、网卡等)就绪时,会通过中断控制器向CPU发送硬件中断信号。CPU接受到中断信号后,停止当前工作,根据中断号(如键盘对应IRQ1)去查询中断向量表(Interrupt Descriptor Table, IDT),找到对应的处理函数并执行。这是操作系统"活"起来的根本动力。

中断控制器与操作系统服务图:

5.4.2 时钟中断

操作系统自己不会被自动指挥,它需要时钟(Timer)来推动。在Linux 0.11内核中,调度程序初始化会设置时钟中断:

c 复制代码
// 来自 Linux 0.11 源码
void sched_init(void) {
    set_intr_gate(0x20, &timer_interrupt); // 设置时钟中断
    outb(inb_p(0x21) & ~0x01, 0x21); // 允许时钟中断
}

每次时钟中断发生时,CPU会自动执行 timer_interrupt,进而调用 do_timer,最终触发 schedule() 函数进行进程切换与调度。换句话说,操作系统是靠硬件时钟的节拍来跑起来的!

5.4.3 死循环

操作系统本质上就是一个死循环!在 Linux 0.11 的 main.c 中:

c 复制代码
void main(void) { 
    // ... 初始化代码 ...
    for (;;) pause(); 
}

当没有其他任务需要运行时,CPU会一直执行 pause() 指令,直到被中断唤醒。

5.4.4 软中断(系统调用)

用户层程序无法直接调用内核功能。它通过汇编指令 int 0x80syscall 触发软中断 ,陷入内核。

内核中有一张系统调用函数指针表 sys_call_table

c 复制代码
fn_ptr sys_call_table[] = { sys_setup, sys_exit, sys_fork, sys_read, ... };

当用户调用系统函数(如 write),实际上会触发软中断,根据 eax 寄存器中的系统调用号(如 __NR_write),sys_call_table 会索引到对应函数执行。系统调用号就是数组下标!

5.4.5 缺页中断与异常

除零、野指针、缺页等都属于异常(也是软中断的一种)。内核在 trap_init 中设置了这些陷阱门:

c 复制代码
void trap_init(void) {
    set_trap_gate(0, &divide_error);   // 除零异常
    set_trap_gate(14, &page_fault);    // 缺页异常
}

缺页异常发生时,内核会去分配物理内存并填充页表;除零/段错误发生时,内核会向当前进程发送 SIGFPE/SIGSEGV 信号来终止它。

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

说明:这些内容解释了为什么信号处理函数需要在用户态执行,以及系统调用切换的代价。

5.5.1 特权级别 (Ring 0 & Ring 3)
  • Ring 0 (内核态):拥有最高权限,可以执行所有CPU指令,访问所有内存和外设。
  • Ring 3 (用户态):权限最低,只能访问常规内存,无法直接读写硬件。
  • Linux 仅使用 Ring 0 和 Ring 3。
5.5.2 虚拟地址空间划分 (32位为例)
  • 用户空间 (0~3GB):每个进程独有,存储用户代码和数据。
  • 内核空间 (3~4GB):所有进程共享,存储操作系统核心代码。这里的页表映射是所有进程共享的。

3G 用户态 / 1G 内核态 虚拟内存布局图:

5.5.3 用户态与内核态的切换流程

当用户程序需要 IO(读写文件、发送信号等)时,必须发生提权(从 Ring 3 到 Ring 0):

  1. 保存现场:用户态寄存器状态被保存。
  2. 切换栈 :从用户栈切换为内核栈。CPU 会从 TSS(任务状态段)中读取 SS0ESP0(内核栈基址和栈顶指针)来设置 ssesp 寄存器。
  3. 执行内核代码:调用内核函数。
  4. 降权返回:内核函数执行完毕后,CPU 将特权级降回 Ring 3,恢复用户栈的现场,继续执行用户代码。
5.5.4 追踪 fopen 调用 (系统调用封装的体现)

glibc 源码追踪中:

c 复制代码
_IO_new_fopen -> _fopen_internal -> _IO_file_open 
    -> __open (系统调用封装) -> INLINE_SYSCALL(open, 3, ...)
        -> INTERNAL_SYSCALL_NCS( ... asm volatile ("syscall\n\t" ... ) )

标准C库将底层的 int 0x80syscall 指令完全封装成了普通的函数调用,这就是为什么我们在写用户程序时看不到汇编指令的原因。


六、可重入函数与volatile

6.1 不可重入函数

当同一个函数被多个执行流(主程序和信号处理函数)同时调用,可能导致数据错乱,这种函数称为不可重入函数

cpp 复制代码
// 危险示例:在链表中插入节点时被信号打断
struct Node {
    int data;
    Node* next;
};

Node* head = nullptr;

void insert(int val) {
    Node* new_node = new Node{val, nullptr};
    // 第一步:new_node->next = head;
    // 第二步:head = new_node;
    // 如果在第一步和第二步之间收到信号,信号处理函数也调用insert
    // 会导致链表断裂或内存泄漏
}

不可重入函数的特征

  • 使用了全局变量或静态变量
  • 调用了malloc/free(堆管理使用全局链表)
  • 调用了标准I/O函数(如printf,内部有缓冲区)
  • 调用了其他不可重入函数

可重入函数只使用局部变量或参数,不修改共享数据。

信号处理函数中只能调用可重入函数 (异步信号安全函数),如write_exit,不能调用printfmalloc。完整的异步信号安全函数列表见man 7 signal-safety

6.2 volatile关键字

当信号处理函数修改全局变量,而主循环检查该变量时,编译器可能优化导致变量被缓存到寄存器,内存中的新值无法被读取。

cpp 复制代码
// 不加volatile可能出问题
int flag = 0;  // 编译器可能优化,将其放到寄存器

void handler(int sig) {
    flag = 1;  // 修改内存中的flag
}

int main() {
    signal(SIGINT, handler);
    while (!flag);  // 如果flag被缓存到寄存器,死循环
    printf("quit\n");
    return 0;
}

解决 :使用volatile关键字,强制每次从内存读取变量。

cpp 复制代码
volatile int flag = 0;  // 告诉编译器:不要优化,每次都从内存取值
6.2.1 volatile 的局限性

volatile 只保证内存可见性不保证原子性 。对于需要原子读写的变量,不可过度依赖 volatile

cpp 复制代码
volatile int flag = 0;              // 读写通常原子,OK
volatile long long counter = 0;     // ❌ 64位变量在32位系统上读写可能不是原子的

最佳实践

  • 信号处理函数中,尽量使用 sig_atomic_t 类型(C标准保证对该类型的读写是原子的,或尽可能原子)
  • 在 C++11 及以后,推荐使用 std::atomic<T>,兼顾原子性与可见性
cpp 复制代码
#include <atomic>
std::atomic<int> flag{0};  // C++ 推荐做法

void handler(int sig) {
    flag.store(1, std::memory_order_release);
}

int main() {
    signal(SIGINT, handler);
    while (!flag.load(std::memory_order_acquire));
    return 0;
}

七、SIGCHLD信号与僵尸进程回收

7.1 问题背景

父进程fork子进程后,如果子进程先退出,会变成僵尸进程(Z状态),占用PCB资源。传统方案:

  • 父进程wait/waitpid阻塞等待(影响父进程工作效率)
  • 父进程轮询(非阻塞,但浪费CPU)

7.2 SIGCHLD机制

子进程在终止、暂停、恢复 时,内核会自动向父进程发送SIGCHLD(17)信号。父进程可以捕获该信号,在handler中异步回收子进程。

waitpid() 详解
c 复制代码
#include <sys/types.h>
#include <sys/wait.h>

pid_t waitpid(pid_t pid, int *status, int options);

参数

  • pid
    • pid == -1:等待任意子进程
    • pid > 0:等待进程ID为pid的子进程
    • pid == 0:等待与调用进程同进程组的任意子进程
    • pid < -1:等待进程组ID等于 |pid| 的任意子进程
  • status:输出子进程的退出状态(若为NULL则不保存)
  • options:选项标志
    • WNOHANG:如果没有子进程退出,立即返回0(非阻塞)
    • WUNTRACED:也返回已停止(未继续)的子进程
    • WCONTINUED:也返回已继续的子进程

返回值

  • 成功:返回退出的子进程PID
  • 若设置了WNOHANG且无子进程退出:返回0
  • 失败:返回 -1,设置 errno
cpp 复制代码
#include <iostream>
#include <signal.h>
#include <sys/wait.h>
#include <unistd.h>

void sigchld_handler(int sig) {
    // 重要:使用循环 + WNOHANG,一次性回收所有已退出的子进程
    // 原因:多个子进程同时退出时,信号可能被合并(只触发一次handler)
    pid_t pid;
    int status;
    while ((pid = waitpid(-1, &status, WNOHANG)) > 0) {
        std::cout << "回收子进程: " << pid << std::endl;
        if (WIFEXITED(status)) {
            std::cout << "  退出码: " << WEXITSTATUS(status) << std::endl;
        } else if (WIFSIGNALED(status)) {
            std::cout << "  被信号: " << WTERMSIG(status) << std::endl;
        }
    }
}

int main() {
    signal(SIGCHLD, sigchld_handler);
    
    for (int i = 0; i < 5; i++) {
        pid_t pid = fork();
        if (pid == 0) {
            // 子进程
            std::cout << "子进程 " << getpid() << " 启动" << std::endl;
            sleep(2);
            exit(i + 1);
        }
    }
    
    // 父进程继续做自己的事
    while (true) {
        std::cout << "父进程工作中... pid=" << getpid() << std::endl;
        sleep(1);
    }
    return 0;
}

7.3 SA_NOCLDSTOP 与 SIGCONT 的联动

默认情况下,SIGCHLD 在子进程的终止、停止、继续 三种状态变化时都会产生。如果你只关心子进程退出,不关心暂停/恢复,应设置 SA_NOCLDSTOP

三种情况对比

子进程事件 默认SIGCHLD行为 设置 SA_NOCLDSTOP 后
子进程终止 (exit) ✅ 发送SIGCHLD ✅ 发送SIGCHLD
子进程停止 (收到SIGSTOP/SIGTSTP) ✅ 发送SIGCHLD ❌ 不发送
子进程继续 (收到SIGCONT) ✅ 发送SIGCHLD ❌ 不发送

为什么要排除停止/继续?

cpp 复制代码
// 没有 SA_NOCLDSTOP 的麻烦场景:
// 1. 父进程 fork 子进程
// 2. 用户按 Ctrl+Z → 子进程被 SIGTSTP 停止 → 父进程收到 SIGCHLD
// 3. handler 中 waitpid(WNOHANG) 无法回收(子进程没退出,只是暂停)
// 4. 用户执行 bg 让子进程继续 → 父进程又收到 SIGCHLD
// 结果:多次无意义的 handler 触发

// 正确做法:仅关心终止
struct sigaction sa;
sa.sa_handler = sigchld_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART | SA_NOCLDSTOP;  // 只接收终止通知
sigaction(SIGCHLD, &sa, nullptr);

SIGCONT 的特殊性 :即使设置了 SA_NOCLDSTOP,当子进程因 SIGCONT 而继续时,父进程仍不会 收到 SIGCHLD。这是预期行为------SA_NOCLDSTOP 同时屏蔽了"停止"和"继续"两种通知。

7.4 Linux扩展:忽略SIGCHLD自动回收

在Linux上,可以将SIGCHLD设置为SIG_IGN,内核会自动回收子进程,不产生僵尸进程。

cpp 复制代码
signal(SIGCHLD, SIG_IGN);  // 设置后,子进程退出时内核自动回收

⚠️ 此方法并非所有Unix系统都支持,可移植性较差。跨平台代码请使用 waitpid 方案。

7.5 查看子进程退出状态

status 的低7位存储终止信号,第8位是core dump标志,高8位是退出码:

复制代码
┌──────────────┬───┬─────────────┐
│  退出码(8bit) │core│ 终止信号(7bit)│
└──────────────┴───┴─────────────┘
  bit 15-8      bit7    bit 6-0

获取方式:

  • 终止信号:status & 0x7F
  • core dump标志:(status >> 7) & 1
  • 退出码:(status >> 8) & 0xFF

也可使用宏:

  • WIFEXITED(status):子进程正常退出?若为真,WEXITSTATUS(status) 返回退出码
  • WIFSIGNALED(status):子进程被信号终止?若为真,WTERMSIG(status) 返回信号编号
  • WIFSTOPPED(status):子进程已停止?若为真,WSTOPSIG(status) 返回停止信号
  • WCOREDUMP(status):是否产生core dump

子进程退出状态位图/回收示意图:


八、常见易错点与最佳实践

8.1 易错点总结

易错点 说明 正确做法
信号处理函数中调用非可重入函数 如printf、malloc,可能导致死锁或数据错乱 只调用异步信号安全函数(write、_exit)
忘记volatile导致死循环 编译器优化缓存全局变量 volatile修饰,或用std::atomic
多子进程退出时只回收一个 SIGCHLD可能被合并,只触发一次handler 使用while(waitpid(...WNOHANG)>0)循环回收
阻塞信号后忘记解除 导致特定信号永远无法递达 使用oldset备份,处理完后恢复
使用signal()注册信号 不同Unix行为不一致,且不安全 推荐使用sigaction()
忽略SIGKILL/SIGSTOP 这两个信号无法被忽略/捕获/阻塞 无解,系统保留特权
alarm(0)误解 以为会立即触发闹钟 alarm(0)用于取消当前闹钟,返回剩余秒数
信号处理函数破坏 errno handler 中调用系统调用会覆盖全局 errno 在 handler 入口保存 errno,出口恢复
pause()的竞态条件 信号可能在解除阻塞和pause之间到达 使用sigsuspend()替代
未设置 SA_NOCLDSTOP 子进程暂停/恢复也会触发SIGCHLD handler,造成无效调用 设置 SA_NOCLDSTOP,仅在子进程终止时通知
误用标准信号实现可靠通知 标准信号不排队,多次发送可能丢失 需要可靠通知时使用实时信号 + sigqueue

errno 保存恢复模板:

cpp 复制代码
void handler(int sig) {
    int saved_errno = errno;   // 入口保存
    // ... 可能修改 errno 的操作(如 write、fork 等)
    errno = saved_errno;       // 出口恢复
}

8.2 最佳实践清单

信号处理函数原则

  • 尽可能简单,只设置标志位
  • 调用_exit()而不是exit()(exit不可重入)
  • 不要调用printfmalloc

使用sigaction代替signal,明确设置sa_mask和sa_flags

合理使用阻塞机制避免信号丢失和重入问题

回收子进程时循环调用waitpid + WNOHANG

在信号处理函数中保护 errno

使用sigsuspend替代pause以避免竞态条件

设置 SA_NOCLDSTOP 避免被 SIGCHLD 频繁中断


九、总结

9.1 Linux进程信号知识点汇总表

分类 子分类 具体内容 / 细节
概念 核心定义 异步通知机制(软中断)
生命周期 产生 → 保存(pending) → 处理(delivery)
处理方式 1. 默认 (SIG_DFL) 2. 忽略 (SIG_IGN) 3. 自定义捕获 (Catch)
信号分类 标准信号 (1-31):不可靠,不排队 实时信号 (34-64):可靠,排队,可携带数据
传递特性 可靠信号 :排队,不丢失 不可靠信号:不排队,相同信号合并
信号产生 终端按键 Ctrl+C (SIGINT) Ctrl+\ (SIGQUIT) Ctrl+Z (SIGSTOP)
硬件异常 除零 SIGFPE 段错误 (野指针) SIGSEGV
系统调用 kill()raise()abort()sigqueue()
软件条件 alarm() (SIGALRM) 管道断裂 (SIGPIPE)
命令行 kill -信号 PID
信号保存 数据结构 PCB中维护:pending位图(未决) + block位图(阻塞)
操作函数 sigemptysetsigfillsetsigaddsetsigdelsetsigismember
阻塞控制 sigprocmask (修改阻塞集)
获取未决集 sigpending (获取pending集)
信号捕捉 简易接口 signal():简单但不可靠(行为因系统而异)
POSIX标准 sigaction():推荐使用,支持 sa_masksa_flagsSA_SIGINFO
数据传递 sigqueue():发送带数据的信号(实时信号专用)
防嵌套 sa_mask:处理期间额外屏蔽信号,防止嵌套中断
原子挂起 sigsuspend():原子操作,解决 pause() 的竞态条件
捕捉流程 用户态 → 内核态 → 检查pending → 返回用户态执行handler → 再次进入内核
操作系统底层 硬件中断 外设触发,CPU查询中断向量表 (IDT) 处理
时钟中断 推动进程调度(如 Linux 0.11 中 timer_interruptdo_timerschedule
软中断 int 0x80 / syscall:系统调用入口 (查 sys_call_table)
异常 缺页、除零、段错误 (在 trap_init 中设置陷阱门)
切换机制 内核态与用户态切换(特权级切换、栈切换、提权/降权)
进阶话题 函数安全性 可重入函数 vs 不可重入函数(信号处理函数只能调用异步信号安全函数)
内存可见性 volatile 的局限性与 sig_atomic_t / std::atomic
子进程回收 SIGCHLD与僵尸进程回收(waitpid详解、SA_NOCLDSTOP
工程实践 SIGPIPE 处理(忽略、MSG_NOSIGNAL、捕获)
易错与最佳实践 编码规范 ✅ 信号处理函数中只调用异步信号安全函数
资源管理 ✅ 子进程回收用 while 循环 + WNOHANG
错误处理 ✅ 在信号处理函数中保护 errno
接口选择 ✅ 优先使用 sigactionsigsuspend
配置优化 ✅ 设置 SA_NOCLDSTOP 避免无效 SIGCHLD

大综合架构图/虚拟内存图:

9.2 经典代码模板

cpp 复制代码
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <sys/wait.h>
#include <atomic>
#include <cstring>

std::atomic<int> g_flag{0};   // 原子标志位

void sigint_handler(int sig) {
    int saved_errno = errno;
    g_flag.store(1);
    errno = saved_errno;
}

void sigchld_handler(int sig) {
    int saved_errno = errno;
    while (waitpid(-1, nullptr, WNOHANG) > 0);
    errno = saved_errno;
}

int main() {
    // 设置 SIGINT 处理
    struct sigaction sa;
    memset(&sa, 0, sizeof(sa));
    sa.sa_handler = sigint_handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = SA_RESTART;
    if (sigaction(SIGINT, &sa, nullptr) == -1) {
        perror("sigaction SIGINT");
        return 1;
    }
    
    // 设置 SIGCHLD 处理(仅终止时通知)
    struct sigaction sa_chld;
    memset(&sa_chld, 0, sizeof(sa_chld));
    sa_chld.sa_handler = sigchld_handler;
    sigemptyset(&sa_chld.sa_mask);
    sa_chld.sa_flags = SA_RESTART | SA_NOCLDSTOP;
    if (sigaction(SIGCHLD, &sa_chld, nullptr) == -1) {
        perror("sigaction SIGCHLD");
        return 1;
    }
    
    // 创建子进程示例
    for (int i = 0; i < 3; i++) {
        if (fork() == 0) {
            sleep(2);
            _exit(0);
        }
    }
    
    while (!g_flag.load()) {
        std::cout << "Working... pid=" << getpid() << std::endl;
        sleep(1);
    }
    std::cout << "Exiting gracefully." << std::endl;
    return 0;
}

📖 进一步学习

  • man 7 signal - 信号总览
  • man 2 sigaction - sigaction详细文档
  • man 7 signal-safety - 异步信号安全函数列表
  • man 2 sigqueue - 带数据信号发送
  • Linux内核源码:kernel/signal.c

希望通过本文,你能全面掌握Linux进程信号的原理与实践。如有疑问,欢迎评论区交流!

相关推荐
广州灵眸科技有限公司1 小时前
瑞芯微RV1126B开发板(EASY-EAI-PI2) 开发(编译)方式说明
linux·服务器·单片机·嵌入式硬件·电脑
土星云SaturnCloud1 小时前
土星云AI边缘计算SE110S系列模型部署实战-YOLOv5
服务器·人工智能·yolo·docker·边缘计算
北山有鸟1 小时前
用开发板的.config替换ubuntu中内核源码目录的.config
linux·运维·ubuntu
qq_452396232 小时前
第二十篇:《Docker 故障排查常用命令与技巧》
运维·docker·容器
jcbut2 小时前
离线安装dify 1.7
linux·运维·dify
艾iYYY2 小时前
string 类的模拟实现
android·服务器·c语言·c++·算法
cjp5602 小时前
003.LINQ在WEB API中的应用
服务器·linq
云计算磊哥@2 小时前
运维开发宝典024-Linux云计算运维入门阶段总结
linux·运维·运维开发
江华森2 小时前
《Linux内核技术实战:从Page Cache到CPU调度的深度解构》博客大纲(26讲精编版)
linux