Linux 进程信号补充知识点总结(按重要程度排序)

发布于:2025-09-03 ⋅ 阅读:(19) ⋅ 点赞:(0)

一、核心必掌握知识点(高频考点 + 实操重点)

1. 信号递达与阻塞的深度理解(含内核态存储逻辑)

关键概念区分
概念 定义 核心差异
信号未决(Pending) 信号产生后到递达前的状态,进程已感知但未处理 未决是 “待处理” 状态,阻塞是 “禁止处理” 开关
信号阻塞(Block) 进程主动设置 “屏蔽”,被阻塞信号即使产生也不会递达 阻塞不影响信号产生(仍会标记未决),仅阻止递达
信号递达(Delivery) 实际执行信号处理动作(默认 / 忽略 / 自定义) 递达是最终处理环节,需先解除阻塞(若被阻塞)
内核中存储逻辑(必须理解)

进程 PCB(task_struct)中通过 3 个核心结构管理信号:

  • blocked(信号屏蔽字)sigset_t类型,位图结构,某 bit 为 1 表示对应信号被阻塞。
  • pending(未决信号集)sigset_t类型,位图结构,某 bit 为 1 表示对应信号已产生但未递达。
  • sighand(信号处理动作):存储每个信号的处理方式(SIG_DFL/SIG_IGN/ 自定义函数指针)。

示例场景:若进程阻塞SIGINT(2 号信号),此时按下Ctrl+C

  1. 内核标记pendingSIGINT的 bit 为 1(未决);
  2. blockedSIGINT的 bit 为 1(阻塞),信号不递达;
  3. 当进程解除SIGINT阻塞后,内核检测到未决信号,立即执行递达(如默认终止)。
实操练习:验证阻塞与未决
#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;

// 打印未决信号集
void printPending() {
    sigset_t pending;
    sigpending(&pending); // 获取当前进程未决信号集
    cout << "Pending signals (31~1): ";
    for (int i = 31; i >= 1; i--) {
        if (sigismember(&pending, i)) cout << "1";
        else cout << "0";
    }
    cout << endl;
}

int main() {
    sigset_t blockSet, oldSet;
    sigemptyset(&blockSet);       // 初始化空信号集
    sigaddset(&blockSet, SIGINT); // 将SIGINT(2号)加入阻塞集
    // 设置阻塞信号集,备份原阻塞集到oldSet
    sigprocmask(SIG_BLOCK, &blockSet, &oldSet);

    cout << "Block SIGINT, press Ctrl+C (5s later unblock)..." << endl;
    for (int i = 5; i > 0; i--) {
        printPending();
        sleep(1);
    }

    // 解除阻塞(恢复原阻塞集)
    sigprocmask(SIG_SETMASK, &oldSet, nullptr);
    cout << "Unblock SIGINT, now Ctrl+C will terminate process" << endl;
    while (1) sleep(1);
    return 0;
}

运行结果:按下Ctrl+C后,未决信号集第 2 位(SIGINT)会显示 “1”;5 秒后解除阻塞,进程立即被SIGINT终止。

2. 信号捕捉的完整流程(含内核态 / 用户态切换)

核心流程(必须能复述)
  1. 用户态执行主流程:进程在用户态运行main函数,此时发生中断 / 异常 / 系统调用,切换到内核态。
  2. 内核态检测信号:内核处理完中断 / 异常后,准备返回用户态前,检查进程的pendingblocked
    • 若存在未被阻塞的信号,且处理方式为 “自定义函数”,则不返回主流程,而是切换到用户态执行自定义处理函数。
  3. 用户态执行处理函数:处理函数执行完毕后,自动调用sigreturn系统调用,再次切换到内核态。
  4. 内核态恢复主流程:内核确认无新信号需递达,恢复主流程的上下文,切换回用户态继续执行main函数。
关键函数:sigaction(比signal更稳定,推荐使用)

signal函数存在兼容性问题,sigaction是 POSIX 标准接口,支持更精细的信号控制:

#include <signal.h>
// 函数原型
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);
  • struct sigaction核心字段
    • sa_handler:处理函数指针(SIG_DFL/SIG_IGN/ 自定义函数)。
    • sa_mask:执行处理函数期间,额外阻塞的信号集(防止处理函数被其他信号打断)。
    • sa_flags:控制信号行为(如SA_RESTART使被信号打断的系统调用自动重启)。
实操练习:用sigaction捕捉SIGINT
#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;

void sigintHandler(int signo) {
    cout << "Catch SIGINT (signo=" << signo << "), wait 2s..." << endl;
    sleep(2); // 执行处理期间,若再次按Ctrl+C,因sa_mask会阻塞SIGINT,需等待处理完毕
}

int main() {
    struct sigaction act, oldAct;
    // 初始化act
    act.sa_handler = sigintHandler;  // 设置自定义处理函数
    sigemptyset(&act.sa_mask);       // 初始化额外阻塞集
    sigaddset(&act.sa_mask, SIGINT); // 执行处理函数时,额外阻塞SIGINT
    act.sa_flags = 0;                // 默认行为

    // 设置SIGINT的处理动作,备份原动作到oldAct
    sigaction(SIGINT, &act, &oldAct);

    cout << "Wait for SIGINT (press Ctrl+C), enter 'q' to quit" << endl;
    while (cin.get() != 'q');

    // 恢复SIGINT原处理动作
    sigaction(SIGINT, &oldAct, nullptr);
    return 0;
}

运行结果:第一次按Ctrl+C会执行处理函数(2 秒内再次按Ctrl+C无反应),处理完毕后恢复响应;输入q退出程序。

3. 可重入函数与竞态条件(避坑重点)

可重入函数定义
  • 可重入:多个控制流程(如主流程 + 信号处理函数)同时调用该函数,不会导致数据错乱(仅访问局部变量 / 参数,无全局 / 静态变量操作)。
  • 不可重入:调用后可能导致数据错乱,常见场景:
    1. 操作全局 / 静态变量(如全局链表插入);
    2. 调用malloc/free(堆管理用全局链表);
    3. 调用标准 I/O 函数(如printf,内部用全局缓冲区)。
竞态条件示例(不可重入函数问题)
#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;

struct Node { int val; Node* next; } node1, node2, *head = nullptr;

// 不可重入函数:操作全局链表head
void insert(Node* p) {
    p->next = head; // 步骤1:保存原head
    // 若此时发生信号,进入处理函数调用insert,会修改head
    sleep(1);       // 模拟信号打断
    head = p;       // 步骤2:更新head为p
}

void sigHandler(int signo) {
    insert(&node2); // 信号处理函数调用insert
    cout << "Signal handler insert node2" << endl;
}

int main() {
    node1.val = 1; node2.val = 2;
    signal(SIGINT, sigHandler); // 注册SIGINT处理函数

    insert(&node1); // 主流程调用insert
    cout << "Main insert node1" << endl;

    // 遍历链表(预期node1->node2,实际可能只有node2)
    Node* cur = head;
    while (cur) {
        cout << cur->val << " ";
        cur = cur->next;
    }
    return 0;
}

运行结果:主流程执行insert(&node1)sleep(1)时,按下Ctrl+C,信号处理函数插入node2并修改head;主流程恢复后继续执行head = &node1,最终链表仅node1(数据错乱)。

避坑方案
  1. 优先使用可重入函数(如自己实现无全局变量的工具函数);
  2. 若必须操作全局数据,在信号处理函数执行期间,通过sigprocmask阻塞相关信号(避免打断)。

4. SIGCHLD信号与僵尸进程处理(实战必备)

核心作用

子进程终止时,会向父进程发送SIGCHLD信号(默认处理动作是忽略),父进程可通过捕捉该信号,调用waitpid清理僵尸进程(避免轮询)。

关键特性
  • 子进程终止后若父进程未处理,会变为僵尸进程(Z状态);
  • 父进程若用sigactionSIGCHLD设为SIG_IGN,子进程终止后会自动清理(不产生僵尸进程,Linux 特有)。
实操练习:捕捉SIGCHLD清理僵尸进程
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <sys/types.h>
using namespace std;

// SIGCHLD处理函数:清理所有终止的子进程
void sigchldHandler(int signo) {
    pid_t id;
    // WNOHANG:非阻塞,有子进程终止则返回其PID,否则返回0
    while ((id = waitpid(-1, nullptr, WNOHANG)) > 0) {
        cout << "Clean zombie child, PID=" << id << endl;
    }
}

int main() {
    // 注册SIGCHLD处理函数
    struct sigaction act;
    act.sa_handler = sigchldHandler;
    sigemptyset(&act.sa_mask);
    act.sa_flags = 0;
    sigaction(SIGCHLD, &act, nullptr);

    // 创建3个子进程
    for (int i = 0; i < 3; i++) {
        pid_t cpid = fork();
        if (cpid == 0) {
            cout << "Child PID=" << getpid() << ", exit after 2s" << endl;
            sleep(2);
            exit(0); // 子进程终止,发送SIGCHLD
        }
    }

    // 父进程持续运行,等待子进程终止
    while (1) sleep(1);
    return 0;
}

运行结果:3 个子进程 2 秒后陆续终止,父进程通过SIGCHLD处理函数自动清理,无僵尸进程残留(可通过ps aux | grep 程序名验证)。

二、重要理解类知识点(原理性内容)

1. 内核态与用户态的切换(信号处理的底层支撑)

核心区别
维度 用户态(Ring 3) 内核态(Ring 0)
权限 仅能访问用户空间(0~3GB),无硬件操作权限 可访问内核空间(3~4GB),有所有硬件操作权限
执行代码 应用程序代码(如main函数) 内核代码(如系统调用、中断处理)
切换触发 系统调用(如fork)、异常(如除零)、中断(如键盘) 处理完毕后自动切换回用户态
与信号处理的关联

信号处理函数在用户态执行,但信号的检测、未决 / 阻塞标记的维护在内核态完成:

  • 当内核检测到可递达信号时,会 “跳转” 到用户态执行处理函数;
  • 处理函数返回后,通过sigreturn系统调用切回内核态,再恢复主流程。

2. volatile关键字(解决编译器优化导致的 bug)

作用

告知编译器:被修饰的变量不允许优化,每次访问必须从内存读取(而非寄存器缓存),保证内存可见性。

典型场景

信号处理函数修改全局变量,主流程循环判断该变量(无volatile会导致优化 bug):

#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;

// 必须加volatile,否则编译器可能将flag缓存到寄存器
volatile int flag = 0;

void sigHandler(int signo) {
    flag = 1;
    cout << "Signal set flag=1" << endl;
}

int main() {
    signal(SIGINT, sigHandler);
    cout << "Wait for SIGINT (press Ctrl+C)" << endl;
    // 若flag无volatile,编译器可能优化为while(1)(认为flag不会变)
    while (!flag);
    cout << "Main exit (flag=1)" << endl;
    return 0;
}

注意:编译时加-O2(优化选项),若不加volatile,程序按下Ctrl+C后仍会卡在while循环;加volatile则正常退出。

三、了解类知识点(扩展认知)

1. 实时信号与常规信号(编号差异)

  • 常规信号:编号 1~31,不支持排队(同一信号多次产生,递达时仅处理 1 次);
  • 实时信号:编号 34~64,支持排队(多次产生会依次递达),用于需要精确处理的场景(如工业控制),本章暂不涉及。

2. Core Dump(核心转储)

  • 作用:进程异常终止时(如SIGSEGV段错误),将用户空间内存数据保存到core文件,用于事后调试(gdb ./程序名 core.进程号)。
  • 开启方法:默认关闭,通过ulimit -c 1024(允许最大 1024KB 的 core 文件)开启,测试代码:
    #include <stdio.h>
    int main() {
        int* p = nullptr;
        *p = 100; // 非法内存访问,产生SIGSEGV,触发Core Dump
        return 0;
    }

    编译运行后会生成core.xxx文件,用gdb ./a.out core.xxx可定位到错误行。

3. 操作系统的中断驱动模型

  • 操作系统通过 “中断”(硬件中断如键盘、软中断如系统调用)驱动运行,核心是 “中断向量表(IDT)”:
    • 每个中断对应一个处理例程(如0x20是时钟中断,0x80是系统调用);
    • 时钟中断定期触发,推动进程调度(如时间片轮转),是操作系统的 “心脏”。