Linux系统:进程信号的处理

系列文章目录


前言

学习进程的信号是因为信号是 Linux 中 进程和内核之间异步通信的重要机制。它可以用来控制进程(如 Ctrl+C 发送 SIGINT 终止程序)、实现进程间事件通知(如子进程退出触发 SIGCHLD)、支持定时器和调试(如 alarm()gdb 的断点),并帮助程序优雅处理错误和安全退出(如捕获 SIGSEGVSIGTERM)。掌握信号能让我们理解操作系统如何管理进程,写出健壮的系统级程序,并为并发与网络编程打下基础。


一、保存信号

在学习本篇之前需要学习进程信号的产生明白信号是如何产生的

在task_struct中存在三个数据结构pendingblockhandler,当我们发送一个信号后,信号会依次在这三个数据结构中显示,至于怎么显示,我们来详细讲解一下。

task_structpendingblockhandler类似于三张表

在这里插入图片描述
三者结构和功能
Handler(信号处理函数表)

  • 定义:每个信号在内核里都有一个对应的处理动作(默认、忽略、自定义函数)。
  • 底层实现:
    • 在 内核进程控制块(task_struct) 里,有一张 信号动作表。
    • 这张表本质上是一个“数组”或“哈希表”,下标是信号编号,值是一个 struct sigaction 结构。
struct sigaction {
    void     (*sa_handler)(int);  // handler 指针: SIG_DFL / SIG_IGN / 自定义函数
    void     (*sa_sigaction)(int, siginfo_t *, void *);
    sigset_t sa_mask;             // 处理信号时临时阻塞的信号集合
    int      sa_flags;            // 标志位 (SA_RESTART 等)
};
  • 所以 Handler 实际上是:
    • 数组形式的映射表(每个信号编号 → 一个 struct sigaction 结构)。
    • 每个信号只能对应一个 handler

Block(阻塞信号集合)

  • 定义:告诉内核“哪些信号现在不要立即处理,先等一等”。
  • 底层实现:
    • 位图(bitmask) 表示集合。
    • 每一位对应一个信号编号,1 表示阻塞,0 表示不阻塞。
  • 存放位置:
    • 每个进程的 PCB(task_struct)里有一个 blocked 字段,类型就是 sigset_t
typedef struct {
    unsigned long __val[_SIGSET_NWORDS];  // 位数组
} sigset_t;
  • 例如:
    • 阻塞 SIGINT (2) → 位图第 2 位 = 1
    • 阻塞 SIGQUIT (3) → 位图第 3 位 = 1

Pending(挂起信号集合)

  • 定义:信号已经送到了,但由于在 Block 集合里,暂时不能处理 → 放到 Pending 集合里“排队”。
  • 底层实现:
    • 位图(bitmask) 表示集合
    • 存在进程控制块 task_structsignal_pending 里。
  • 特点:
    • 一个信号挂起只会在位图上标 1,不会排队累积(大部分信号只记录一次,不是 FIFO 队列)。
    • 实时信号(SIGRTMIN ~ SIGRTMAX)例外,它们可以有队列。

当一个信号发送给进程时,它会先进入 pending 集合,在位图上标记为 1。如果这个信号没有被阻塞(block mask 里是 0),内核会立刻把它递送给进程,并调用对应的 handler;执行完后,pending 位会被清0。如果这个信号被阻塞 (block mask 里是 1),那么它会留在 pending 集合中,不会被递送。对普通信号来说,不管来多少次,pending 只会保持为 1;对实时信号来说,pending 位=1 的同时,还会把每次信号都排队。当进程解除阻塞后,内核会立即把 pending 里的信号递送给 handler,然后再清除 pending 位


1-1 sigset_t

sigset_t 是 Linux 系统里定义的一种 数据类型,用来表示一组信号。它本质上是一个 位图(bitmap),里面的每一位对应一个信号(比如 SIGINT、SIGKILL 等)

  • 如果某一位是 1,就表示这个信号处于某种“有效”状态;
  • 如果是 0,就表示这个信号处于“无效”状态。
  • 未决信号集(pending) 里,某位为 1 表示这个信号已经来了但还没处理
  • 阻塞信号集(block / signal mask) 里,某位为 1 表示这个信号被阻塞,不能递送给进程

定义
在源码里,它通常是个结构体或整型数组,比如在 glibc 里:

typedef struct {
    unsigned long __val[_SIGSET_NWORDS];
} sigset_t;

也就是说,它其实就是一堆 long 类型的数组,每个 bit 对应一个信号。


1-2 信号集操作函数

你可以把 sigset_t 想象成一个黑盒子,里面就是一张“信号表”,但我们看不到内部结构。系统不让我们直接在里面乱涂乱改,而是给了一些“官方函数”,比如“清空”“添加”“删除”“查询”,我们只能按这些按钮来修改它。

  • 推荐的方式 是通过系统提供的库函数来操作,比如:
sigemptyset(&set);
sigfillset(&set);
sigaddset(&set, SIGINT);
sigdelset(&set, SIGINT);
sigismember(&set, SIGINT);

各自的作用

  • sigemptyset(sigset_t *set)
    • 清空一个信号集,把所有位都置为 0。
    • 表示“集合里没有任何信号”。
  • sigfillset(sigset_t *set)
    • 填满一个信号集,把所有位都置为 1。
    • 表示“集合里包含所有信号”。
  • sigaddset(sigset_t *set, int signo)
    • 向信号集里 添加一个指定信号。
    • 例如:sigaddset(&set, SIGINT) → 把 SIGINT 加入集合。
  • sigdelset(sigset_t *set, int signo)
    • 从信号集里 删除一个指定信号。
    • 例如:sigdelset(&set, SIGINT) → 把 SIGINT 从集合中去掉。
  • sigismember(const sigset_t *set, int signo)
    • 判断一个信号是否在集合里。

上述函数都是成功返回 1 ,失败返回0,-1 表示出错


1-3 sigprocmask

sigprocmask的作用就是 修改或查询当前进程的信号屏蔽字(signal mask,也就是阻塞信号集)成功返回 0,失败返回 -1
函数原型:

#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:指向一个 sigset_t,表示要修改的信号集

    • 如果传 NULL,就表示不修改屏蔽字,只查询。
  • oldset:如果不为 NULL,则把修改前的旧屏蔽字保存到这里。

    • 常用于“保存原来的状态,稍后恢复”。

演示代码:

#include<stdio.h>
#include<signal.h>
#include<unistd.h>
void print_sigset(const sigset_t*set)
{
    for(int i=0;i<31;i++)
    {
        if(sigismember(set,i))
        {
            printf("1");
        }
        else
        {
            printf("0");
        }
    }
    printf("\n");
}
void show_pending()
{
    sigset_t pending;
    sigpending(&pending);
    printf("当前pending信号:");
    print_sigset(&pending);
}
void show_block()
{
    sigset_t block;
    sigprocmask(SIG_BLOCK,NULL,&block);
    printf("当前block信号:");
    print_sigset(&block);
}
int main()
{
    sigset_t set,oldset;
    sigemptyset(&set);
    sigaddset(&set,SIGINT);
    sigprocmask(SIG_BLOCK,&set,&oldset);
    printf("SIGINT已经被阻塞,按Ctrl+C多次试试...\n");
    for(int i=0;i<5;i++)
    {
        show_pending();
        show_block();
        sleep(3);
    }
    sigprocmask(SIG_SETMASK,&oldset,NULL);
    printf("SIGINT已恢复,可以被捕捉/递送了,再按Ctrl+C试试...\n");
    for(int i=0;i<5;i++)
    {
        show_pending();
        show_block();
        sleep(3);
    }
    return 0;
}

演示结果:

gch@hcss-ecs-f59a:/gch/code/HaoHao/learn2/day3$ ./exe
SIGINT已经被阻塞,按Ctrl+C多次试试...
当前pending信号:1000000000000000000000000000000
当前block信号:1010000000000000000000000000000
^C
当前pending信号:1010000000000000000000000000000
当前block信号:1010000000000000000000000000000
...
当前pending信号:1000000000000000000000000000000
当前block信号:1000000000000000000000000000000
  • 我们在程序里把进程的 block 集合里把 SIGINT (编号2) 加进去,相当于把 2 号位置成了 1。这样,当我们按 Ctrl+C 触发 SIGINT 时,这个信号并不会马上交给进程处理,而是会被放到 pending 集合里,对应的 2 号位就变成了 1
  • 接着,当程序运行到后面,我们把block 集合里的 2 号位清零(也就是允许 SIGINT 递送)。这时候,内核会立刻把 pending 里的 SIGINT 交给进程的 handler 去处理。等到 handler 执行完,对应的 pending2 号位就会被清零。最后程序继续往下运行,直到结束。

1-4 sigpending

sigpending 用来查看当前进程有哪些信号处在 pending(未决)状态
函数原型:

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

参数说明:

set:传出参数,函数会把当前进程的未决信号集写到 set 里,成功返回 0,失败返回 -1

演示代码:

#include<stdio.h>
#include<signal.h>
#include<unistd.h>
void handler(int signo)
{
    printf("捕获到信号 %d\n",signo);
}
int main()
{
    signal(SIGINT,handler);
    sigset_t blockset;
    sigemptyset(&blockset);
    sigaddset(&blockset,SIGINT);
    sigprocmask(SIG_BLOCK,&blockset,NULL);
    printf("请按Ctrl+C(SIGINT)\n");
    sleep(5);
    sigset_t pendingset;
    sigpending(&pendingset);
    if(sigismember(&pendingset,SIGINT))
    {
        printf("SIGINT信号在pending集合中!\n");
    }
    else
    {
        printf("SIGINT信号不在pending集合中!\n");
    }
    sigprocmask(SIG_UNBLOCK,&blockset,NULL);
    printf("接触阻塞,SIGINT会立即递达handler\n");
    sleep(2);
    return 0;
}

演示结果:

gch@hcss-ecs-f59a:/gch/code/HaoHao/learn2/day3$ ./exe
请按Ctrl+C(SIGINT)
^C
SIGINT信号在pending集合中!
捕获到信号 2
接触阻塞,SIGINT会立即递达handler
gch@hcss-ecs-f59a:/gch/code/HaoHao/learn2/day3$ 
gch@hcss-ecs-f59a:/gch/code/HaoHao/learn2/day3$ ./exe
请按Ctrl+C(SIGINT)
SIGINT信号不在pending集合中!
接触阻塞,SIGINT会立即递达handler

程序先阻塞 SIGINT,按下 Ctrl+CSIGINT 会进入 pending 集合,sigpending 检查到 SIGINTpending 中,解除阻塞后,信号立即传递到 handler


1-5 sigaction

sigaction 是比 signal 更强大、更可控的信号处理接口,用来 检查或修改某个信号的处理方式。在 Linux编程里,推荐使用 sigaction 来替代 signal,成功:0,失败:-1

函数原型:

#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
  • 参数说明:
    signum:要操作的信号编号,比如 SIGINT、SIGTERM 等
    act:指定新的处理方式(传入一个 struct sigaction 结构体指针)
    oldact:如果不为 NULL,会把之前的处理方式保存到这里

  • struct sigaction 结构体

struct sigaction {
    void     (*sa_handler)(int);    // 处理函数(简化版)
    void     (*sa_sigaction)(int, siginfo_t *, void *); // 处理函数(带更多信息)
    sigset_t sa_mask;               // 处理函数执行期间要屏蔽的信号
    int      sa_flags;              // 行为标志,比如 SA_SIGINFO
};

参数解析:

  • sa_handler:普通信号处理函数,参数是信号编号
  • sa_sigaction:带扩展信息的处理函数,可以获取信号发送者的 PID、UID 等,需要配合 SA_SIGINFO 使用
  • sa_mask:在处理信号时临时阻塞的信号集
  • sa_flags:行为标志,比如:
    • SA_RESTART:被信号中断的系统调用会自动重启
    • SA_SIGINFO:使用 sa_sigaction 而不是 sa_handler

演示代码:

#include<stdio.h>
#include<signal.h>
#include<unistd.h>
void handler(int signo)
{
    printf("捕获到信号:%d\n",signo);
}
int main()
{
    struct sigaction act;
    act.sa_handler=handler;
    sigemptyset(&act.sa_mask);
    act.sa_flags=0;
    if(sigaction(SIGINT,&act,NULL)==-1)
    {
        perror("sigaction");
        return 1;
    }
    printf("运行中...请按Ctrl+C\n");
    while(1)
    {
        sleep(1);
    }
    return 0;
}

演示结果:

gch@hcss-ecs-f59a:/gch/code/HaoHao/learn2/day3$ ./exe
运行中...请按Ctrl+C
^C捕获到信号:2
^C捕获到信号:2

这里我们设置了一个自定义的信号处理,不过进程是死循环的。


1-6 volatile

volatile 是 C/C++ 里的一个 类型修饰符,它主要用来告诉编译器:不要对这个变量进行优化,每次都要从内存里重新读取它的值。

  • 为什么要这样做?
    一般情况下,编译器会为了优化性能,把变量的值放在寄存器里缓存住,不会每次都去内存读。但有些场景下,变量的值可能会被程序以外的东西修改,如果没有 volatile,编译器可能会误以为变量值没变,从而导致逻辑出错。

演示代码:

#include <signal.h>
#include <stdio.h>
#include <unistd.h>

volatile sig_atomic_t flag = 0;

void handler(int sig) {
    flag = 1; // 在信号处理函数里改变量
}

int main() {
    signal(SIGINT, handler);
    while (!flag) {
        // 如果 flag 不是 volatile,可能优化成死循环
    }
    printf("收到信号,退出\n");
    return 0;
}

flag 不是 volatile,可能优化成死循环,但是这个程序并不是一个死循环,当我们发送SIGINT信号后循环需要结束。
volatile 的作用就是 禁止编译器优化,保证每次访问变量都直接读写内存。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值