【Linux】进程信号

【Linux】进程信号

一、信号的基本认知

1.1 生活中的信号类比

信号的处理流程可以用"收快递"来理解:

  • 识别信号:你知道快递来了该怎么处理(进程内置信号识别能力)。
  • 信号产生:快递员通知你快递到了(信号由内核或其他进程发送)。
  • 信号保存:你正在打游戏,暂时没法取,先记住有快递(信号未决状态)。
  • 信号处理:游戏结束后取快递,处理方式有三种------默认(自己用)、忽略(放一边)、自定义(送给朋友)。

核心结论:

  • 信号是异步的,进程无法预知信号何时到达。
  • 信号的处理方法在信号产生前已确定(默认/忽略/自定义)。
  • 信号不一定立即处理,会在进程执行的"合适时机"递达。

1.2 技术视角的信号本质

信号是进程之间事件通知的方式,属于软中断,用于处理异步事件。例如:

  • 用户按下Ctrl+C,内核会向前台进程发送SIGINT(2号信号),默认动作是终止进程。
  • 程序执行除零操作,CPU检测到异常后,内核会向进程发送SIGFPE(8号信号),默认动作是终止进程并生成core dump。

1.3 查看系统信号

Linux系统支持64种信号(34以下为常规信号,34及以上为实时信号),可通过kill -l命令查看:

bash 复制代码
kill -l
# 输出示例(部分):
#  1) SIGHUP       2) SIGINT       3) SIGQUIT      4) SIGILL
#  5) SIGTRAP      6) SIGABRT      7) SIGBUS       8) SIGFPE
#  9) SIGKILL     10) SIGUSR1     11) SIGSEGV     12) SIGUSR2
# 13) SIGPIPE     14) SIGALRM     15) SIGTERM     17) SIGCHLD

关键信号说明:

信号 编号 默认动作 场景
SIGINT 2 终止进程 Ctrl+C触发
SIGQUIT 3 终止进程+Core Dump Ctrl+\触发
SIGKILL 9 强制终止进程 无法捕捉/忽略
SIGSEGV 11 终止进程+Core Dump 非法内存访问
SIGCHLD 17 忽略 子进程终止/停止时通知父进程
SIGALRM 14 终止进程 alarm函数超时触发

可通过man 7 signal查看每个信号的详细说明。

二、信号的产生方式

信号的产生源于硬件事件、软件条件或用户操作,主要有以下5种方式:

2.1 终端按键产生信号

通过键盘输入特定组合键,内核会将其解释为信号并发送给前台进程:

  • Ctrl+C:发送SIGINT(2号),默认终止进程。
  • Ctrl+\:发送SIGQUIT(3号),默认终止进程并生成core dump文件(用于调试)。
  • Ctrl+Z:发送SIGTSTP(20号),默认暂停进程(挂起至后台)。

实战验证 :捕捉SIGQUIT信号

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

void handler(int signo) {
    std::cout << "进程[" << getpid() << "]捕获到信号:" << signo << "(SIGQUIT)" << std::endl;
}

int main() {
    std::cout << "进程PID:" << getpid() << ",等待信号(按Ctrl+\\触发)" << std::endl;
    signal(SIGQUIT, handler);  // 自定义捕捉SIGQUIT
    while (true) sleep(1);     // 死循环等待信号
    return 0;
}

运行结果:

bash 复制代码
./sig_quit
进程PID:12345,等待信号(按Ctrl+\触发)
^\进程[12345]捕获到信号:3(SIGQUIT)  # 按下Ctrl+\后输出

2.2 命令行发送信号

使用kill命令向指定进程发送信号,格式为kill -信号编号/名称 进程PID

bash 复制代码
# 向PID为12345的进程发送SIGKILL(9号),强制终止
kill -9 12345
# 等价于
kill -SIGKILL 12345

实战验证 :向后台进程发送SIGSEGV(11号,段错误信号)

bash 复制代码
# 后台运行死循环程序
./sig_loop &
# 查看进程PID
ps ajx | grep sig_loop
# 发送SIGSEGV信号
kill -11 12345
# 输出:[1]+  Segmentation fault (core dumped)  ./sig_loop

2.3 函数调用产生信号

通过系统函数主动发送信号,常用函数有killraiseabort

2.3.1 kill函数:向指定进程发送信号
c 复制代码
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
  • pid:目标进程PID(正数);pid=-1表示向所有有权限的进程发送信号。
  • sig:信号编号(0表示检测进程是否存在,不发送信号)。
  • 返回值:成功返回0,失败返回-1。

实战:实现简易版kill命令

cpp 复制代码
#include <iostream>
#include <sys/types.h>
#include <signal.h>
#include <cstdlib>

int main(int argc, char* argv[]) {
    if (argc != 3) {
        std::cerr << "用法:" << argv[0] << " -信号编号 进程PID" << std::endl;
        return 1;
    }
    int sig = std::stoi(argv[1] + 1);  // 去掉前缀"-"
    pid_t pid = std::stoi(argv[2]);
    int ret = kill(pid, sig);
    if (ret == 0) std::cout << "向进程" << pid << "发送信号" << sig << "成功" << std::endl;
    else perror("kill失败");
    return 0;
}
2.3.2 raise函数:向当前进程发送信号
c 复制代码
#include <signal.h>
int raise(int sig);  // 等价于kill(getpid(), sig)

实战 :进程自我发送SIGINT信号

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

void handler(int sig) {
    std::cout << "捕获到信号:" << sig << std::endl;
}

int main() {
    signal(SIGINT, handler);
    while (true) {
        sleep(1);
        raise(SIGINT);  // 每秒自我发送一次SIGINT
    }
    return 0;
}
2.3.3 abort函数:强制进程异常终止
c 复制代码
#include <stdlib.h>
void abort(void);  // 发送SIGABRT(6号信号),无返回值

实战 :捕获SIGABRT信号

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

void handler(int sig) {
    std::cout << "捕获到信号:" << sig << "(SIGABRT)" << std::endl;
}

int main() {
    signal(SIGABRT, handler);
    sleep(1);
    abort();  // 发送SIGABRT,即使捕获也会终止进程
    return 0;
}

运行结果:

bash 复制代码
./sig_abort
捕获到信号:6(SIGABRT)
Aborted (core dumped)

2.4 软件条件产生信号

由软件内部状态触发的信号,常见场景:

  • 管道破裂(SIGPIPE):向无读端的管道写入数据。
  • 定时器超时(SIGALRM):alarm函数设置的超时时间到达。
alarm函数:设置定时器
c 复制代码
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
  • 功能:seconds秒后向当前进程发送SIGALRM信号。
  • 返回值:之前设置的定时器剩余秒数(无则返回0)。
  • 注意:一个进程同时只能有一个活跃的alarm定时器。

实战1:1秒后终止进程

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

int main() {
    alarm(1);  // 1秒后发送SIGALRM(默认终止进程)
    int count = 0;
    while (true) {
        std::cout << "count:" << ++count << std::endl;
    }
    return 0;
}

实战2:重复设置定时器

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

int g_count = 0;

void handler(int sig) {
    std::cout << "定时器触发,count:" << g_count << std::endl;
    g_count = 0;
    alarm(1);  // 重新设置1秒定时器
}

int main() {
    signal(SIGALRM, handler);
    alarm(1);  // 首次设置定时器
    while (true) {
        g_count++;
        usleep(1000);  // 微秒级延迟
    }
    return 0;
}

2.5 硬件异常产生信号

硬件检测到异常后,通知内核,内核将其转换为信号发送给当前进程:

  • 除零错误:触发SIGFPE(8号)。
  • 非法内存访问(野指针):触发SIGSEGV(11号)。
  • 总线错误:触发SIGBUS(7号)。

实战:模拟非法内存访问

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

void handler(int sig) {
    std::cout << "捕获到信号:" << sig << "(非法内存访问)" << std::endl;
}

int main() {
    signal(SIGSEGV, handler);
    int* p = nullptr;
    *p = 100;  // 写空指针,触发SIGSEGV
    return 0;
}

运行结果:

bash 复制代码
./sig_segv
捕获到信号:11(非法内存访问)
捕获到信号:11(非法内存访问)
...  # 循环触发,因异常状态未清除

2.6 Core Dump:核心转储

当进程因信号异常终止时,可选择将进程的用户空间内存数据保存到磁盘(文件名为core.进程PID),用于事后调试,这就是Core Dump。

开启Core Dump

默认情况下,系统禁用Core Dump(避免敏感信息泄露),可通过ulimit命令开启:

bash 复制代码
ulimit -c 1024  # 允许Core文件最大为1024块(每块512字节)
ulimit -c unlimited  # 无限制
验证Core Dump
bash 复制代码
# 开启Core Dump
ulimit -c unlimited
# 运行触发SIGQUIT的程序
./sig_quit
# 按下Ctrl+\,生成core文件
ls -l core.*  # 查看生成的core文件
# 用gdb调试core文件
gdb ./sig_quit core.12345

三、信号的保存与状态

信号从产生到递达之间存在"时间窗口",期间信号处于未决状态 (Pending)。进程可通过阻塞信号集(Block)屏蔽某些信号,被阻塞的信号会保持未决状态,直到阻塞解除。

3.1 关键概念

  • 未决(Pending):信号已产生,但尚未递达(进程未处理)。
  • 阻塞(Block):进程暂时不处理该信号,即使信号已产生,也会保持未决状态。
  • 递达(Delivery):进程实际执行信号的处理动作(默认/忽略/自定义)。
  • 注意:阻塞≠忽略,忽略是递达后的处理动作,阻塞是阻止信号递达。

3.2 内核中的信号表示

进程控制块(task_struct)中存储了信号相关的三个核心数据结构:

  1. blocked:阻塞信号集(sigset_t类型),每一位表示对应信号是否被阻塞。
  2. pending:未决信号集(sigset_t类型),每一位表示对应信号是否未决。
  3. sighand:信号处理动作表,存储每个信号的处理方式(默认/忽略/自定义函数)。

sigset_t是信号集类型,本质是位图,用于表示多个信号的状态(有效/无效)。

3.3 信号集操作函数

用户不能直接操作sigset_t的内部结构,需通过以下函数操作:

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);

3.4 修改进程的阻塞信号集(sigprocmask)

c 复制代码
#include <signal.h>
int sigprocmask(int how, const sigset_t* set, sigset_t* oset);
  • how:修改方式(SIG_BLOCK/SIG_UNBLOCK/SIG_SETMASK)。
  • set:新的阻塞信号集(NULL表示不修改)。
  • oset:保存原阻塞信号集(NULL表示不保存)。
  • 返回值:成功返回0,失败返回-1。

how参数说明:

how 含义 等价操作
SIG_BLOCK 添加阻塞信号 mask = mask ∪ set
SIG_UNBLOCK 解除阻塞信号 mask = mask ∩ ~set
SIG_SETMASK 设置阻塞信号集 mask = set

3.5 读取未决信号集(sigpending)

c 复制代码
#include <signal.h>
int sigpending(sigset_t* set);  // 读取当前进程的未决信号集

3.6 实战:信号阻塞与未决状态验证

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

// 打印未决信号集
void print_pending(const sigset_t& pending) {
    std::cout << "进程[" << getpid() << "]未决信号:";
    for (int sig = 1; sig <= 31; ++sig) {
        if (sigismember(&pending, sig)) std::cout << "1";
        else std::cout << "0";
    }
    std::cout << std::endl;
}

void handler(int sig) {
    std::cout << "捕获到信号:" << sig << std::endl;
}

int main() {
    signal(SIGINT, handler);  // 自定义捕捉SIGINT(2号)

    // 1. 初始化阻塞信号集,添加SIGINT
    sigset_t block_set, old_set;
    sigemptyset(&block_set);
    sigaddset(&block_set, SIGINT);

    // 2. 设置阻塞信号集(屏蔽SIGINT)
    sigprocmask(SIG_BLOCK, &block_set, &old_set);
    std::cout << "已屏蔽SIGINT,按Ctrl+C发送信号" << std::endl;

    // 3. 循环打印未决信号集
    int cnt = 10;
    while (cnt--) {
        sigset_t pending;
        sigpending(&pending);
        print_pending(pending);
        sleep(1);
    }

    // 4. 解除阻塞
    std::cout << "解除SIGINT阻塞" << std::endl;
    sigprocmask(SIG_SETMASK, &old_set, nullptr);

    while (true) sleep(1);
    return 0;
}

运行结果:

bash 复制代码
./sig_block
已屏蔽SIGINT,按Ctrl+C发送信号
进程[12345]未决信号:0000000000000000000000000000000
进程[12345]未决信号:0000000000000000000000000000000
^C进程[12345]未决信号:0000000000000000000000000000010  # SIGINT未决
进程[12345]未决信号:0000000000000000000000000000010
...
解除SIGINT阻塞
捕获到信号:2  # 信号递达,执行handler

四、信号的捕捉与处理

信号的处理动作有三种:

  1. 默认动作(SIG_DFL):内核预设的处理方式(如终止、Core Dump、忽略)。
  2. 忽略(SIG_IGN):进程不处理该信号。
  3. 自定义捕捉(signal/sigaction):执行用户定义的处理函数。

4.1 signal函数:简单信号捕捉

c 复制代码
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
  • signum:信号编号。
  • handler:处理函数指针(SIG_DFL/SIG_IGN或自定义函数)。
  • 返回值:成功返回原处理方式,失败返回SIG_ERR

实战 :忽略SIGINT信号

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

int main() {
    signal(SIGINT, SIG_IGN);  // 忽略Ctrl+C
    std::cout << "忽略SIGINT,按Ctrl+C无反应" << std::endl;
    while (true) sleep(1);
    return 0;
}

4.2 sigaction函数:高级信号捕捉

signal函数接口简单,但行为在不同系统中可能不一致,推荐使用sigaction函数,功能更强大、可移植性更好。

c 复制代码
#include <signal.h>
int sigaction(int signum, const struct sigaction* act, struct sigaction* oact);
struct sigaction结构体
c 复制代码
struct sigaction {
    void (*sa_handler)(int);  // 处理函数(同signal)
    sigset_t sa_mask;         // 处理信号时,额外阻塞的信号集
    int sa_flags;             // 标志位(0表示默认行为)
    void (*sa_sigaction)(int, siginfo_t*, void*);  // 实时信号处理函数
};

关键特性

  • 执行信号处理函数时,内核会自动将当前信号加入sa_mask,避免递归触发。
  • sa_mask可指定额外阻塞的信号,确保处理过程不被干扰。

实战 :用sigaction捕捉SIGINT

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

void handler(int sig) {
    std::cout << "捕获到信号:" << sig << std::endl;
    sleep(2);  // 模拟处理耗时操作
}

int main() {
    struct sigaction act, oact;
    act.sa_handler = handler;
    sigemptyset(&act.sa_mask);
    sigaddset(&act.sa_mask, SIGQUIT);  // 处理SIGINT时,额外阻塞SIGQUIT
    act.sa_flags = 0;

    sigaction(SIGINT, &act, &oact);  // 设置SIGINT的处理方式
    std::cout << "原SIGINT处理方式:" << (void*)oact.sa_handler << std::endl;

    while (true) sleep(1);
    return 0;
}

运行结果:

bash 复制代码
./sig_action
原SIGINT处理方式:0x0  # 原处理方式为默认(SIG_DFL)
^C捕获到信号:2  # 按下Ctrl+C,进入handler
^\  # 此时按下Ctrl+\(SIGQUIT),被阻塞
捕获到信号:2  # handler执行完毕后,SIGQUIT才递达(若未忽略)

4.3 信号捕捉的内核流程

当进程触发信号捕捉时,内核会执行以下步骤:

  1. 进程在用户态执行主流程,因中断/异常/系统调用进入内核态。
  2. 内核处理完原任务后,检查当前进程的未决信号集。
  3. 若存在可递达信号且处理方式为自定义函数,内核会切换到用户态执行处理函数。
  4. 处理函数执行完毕后,通过sigreturn系统调用再次进入内核态。
  5. 内核检查无新信号后,恢复主流程的上下文,回到用户态继续执行。

核心特点:处理函数与主流程是两个独立的控制流程,使用不同的堆栈空间。

五、补充:可重入函数与volatile

5.1 可重入函数

若一个函数被多个控制流程(如主流程和信号处理函数)同时调用,且不会因共享资源导致数据错乱,则称为可重入函数

不可重入函数的条件(满足其一)
  • 调用malloc/free(堆内存由全局链表管理)。
  • 调用标准I/O库函数(如printf,使用全局缓冲区)。
  • 访问全局变量/静态变量。
示例:不可重入函数导致的数据错乱
cpp 复制代码
#include <iostream>
#include <signal.h>

int g_count = 0;

void insert() {
    g_count++;  // 访问全局变量,不可重入
}

void handler(int sig) {
    insert();  // 信号处理函数调用insert
}

int main() {
    signal(SIGINT, handler);
    while (true) {
        insert();  // 主流程调用insert
        sleep(1);
        std::cout << "g_count:" << g_count << std::endl;
    }
    return 0;
}

运行时按下Ctrl+C,可能导致g_count计数异常(主流程和信号处理函数同时修改全局变量)。

5.2 volatile关键字

编译器优化可能会将变量缓存到CPU寄存器中,导致信号处理函数修改的变量值无法被主流程感知(数据不一致)。volatile关键字可禁止编译器优化,确保变量的读写直接操作内存。

问题示例(无volatile)
cpp 复制代码
#include <iostream>
#include <signal.h>
#include <unistd.h>

int flag = 0;  // 未加volatile

void handler(int sig) {
    flag = 1;
    std::cout << "flag设置为1" << std::endl;
}

int main() {
    signal(SIGINT, handler);
    while (!flag);  // 编译器优化后,可能一直读取寄存器中的flag(0)
    std::cout << "进程退出" << std::endl;
    return 0;
}

编译时开启优化(-O2),即使按下Ctrl+C,flag被设置为1,主流程的while循环仍可能无限执行(因读取的是寄存器缓存)。

解决:添加volatile
cpp 复制代码
volatile int flag = 0;  // 禁止优化,直接操作内存

重新编译运行,按下Ctrl+C后,主流程能立即感知flag的变化,正常退出。

六、实战:SIGCHLD信号处理僵尸进程

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

僵尸进程的危害

子进程终止后,若父进程未及时清理,子进程会变成僵尸进程,占用PID和系统资源,直至父进程退出。

实战:捕捉SIGCHLD清理僵尸进程

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

// 信号处理函数:清理僵尸进程
void sigchld_handler(int sig) {
    pid_t id;
    // WNOHANG:非阻塞等待,有僵尸进程则清理,无则返回0
    while ((id = waitpid(-1, nullptr, WNOHANG)) > 0) {
        std::cout << "清理僵尸进程:" << id << std::endl;
    }
}

int main() {
    // 捕捉SIGCHLD信号
    struct sigaction act;
    act.sa_handler = sigchld_handler;
    sigemptyset(&act.sa_mask);
    act.sa_flags = 0;
    sigaction(SIGCHLD, &act, nullptr);

    // 创建5个子进程
    for (int i = 0; i < 5; ++i) {
        pid_t pid = fork();
        if (pid == 0) {
            std::cout << "子进程:" << getpid() << " 启动" << std::endl;
            sleep(2);  // 子进程运行2秒后退出
            exit(0);
        }
    }

    // 父进程持续运行
    while (true) sleep(1);
    return 0;
}

运行结果:

bash 复制代码
./sig_chld
子进程:12346 启动
子进程:12347 启动
子进程:12348 启动
子进程:12349 启动
子进程:12350 启动
清理僵尸进程:12346
清理僵尸进程:12347
清理僵尸进程:12348
清理僵尸进程:12349
清理僵尸进程:12350

七、总结

Linux信号是进程间异步通信的核心,核心要点如下:

  1. 信号产生:支持终端按键、命令、函数、软件条件、硬件异常5种方式。
  2. 信号状态:未决(Pending)、阻塞(Block)、递达(Delivery),阻塞会阻止信号递达。
  3. 信号处理:默认、忽略、自定义捕捉(推荐sigaction函数)。
  4. 关键注意:避免在信号处理函数中调用不可重入函数,共享变量需加volatile
  5. 实战场景:清理僵尸进程(SIGCHLD)、定时器(SIGALRM)、异常处理(SIGSEGV)。
相关推荐
wanhengidc1 小时前
云手机的出现意味着什么
运维·服务器·web安全·智能手机·云计算
TTc_1 小时前
Jenkins设置定时发布
运维·jenkins
wanhengidc1 小时前
云手机的硬件技术
运维·服务器·web安全·游戏·智能手机
Thexhy1 小时前
CentOS快速安装DockerCE指南
linux·docker
路人甲ing..1 小时前
Android Studio 快速的制作一个可以在 手机上跑的app
android·java·linux·智能手机·android studio
骇客野人2 小时前
Spring Cloud Gateway解析和用法
运维·网络
拾忆,想起2 小时前
Dubbo超时问题排查与调优指南:从根因到解决方案
服务器·开发语言·网络·微服务·架构·php·dubbo
code monkey.2 小时前
【Linux之旅】深入 Linux Ext 系列文件系统:从磁盘物理结构到软硬链接的底层逻辑
linux·文件系统·ext2
晨非辰3 小时前
数据结构排序系列指南:从O(n²)到O(n),计数排序如何实现线性时间复杂度
运维·数据结构·c++·人工智能·后端·深度学习·排序算法