Linux进程信号(贰):保存信号

61 阅读7分钟

1.信号相关常见概念

什么是信号保存?

信号保存是指内核在信号产生递送之间,对信号状态和信息进行维护的过程。当信号无法立即被进程处理时,内核需要暂时保存这些信号。

信号的生命周期:

信号产生信号保存信号递送(处理)

信号的三个状态

产生(Generation):信号被某个事件或进程创建

未决(Pending):信号已产生但尚未递送给目标进程

递送(Delivery):信号被目标进程接收并处理

• 实际执行信号的处理动作称为信号递达(Delivery)

• 信号从产生到递达之间的状态,称为信号未决(Pending)。

• 进程可以选择阻塞 (Block )某个信号。

被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作.

• 注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。

2.在内核中的表示

信号在内核中的表示示意图

进程可以识别信号,本质靠的是这三张表,进程对信号的操作实际上都是对这三张表的操作

block表和pending表本质是结构体中包含数组表示位图,数组下标表示信号编号,下标中的内容1/0表示是否

• 每个信号都有两个标志位分别表示阻塞(block)和未决(pending),还有一个函数指针表示处理动作。信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。在上图的例子中,SIGHUP信号未阻塞也未产生过,当它递达时执行默认处理动作。

• SIGINT信号产生过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略,但在没有解除阻塞之前不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞。

• SIGQUIT信号未产生过,一旦产生SIGQUIT信号将被阻塞,它的处理动作是用户自定义函数sighandler。如果在进程解除对某信号的阻塞之前这种信号产生过多次,将如何处理?Linux是这样实现的:常规信号在递达之前产生多次只计一次,而实时信号在递达之前产生多次可以依次放在一个队列里。本章不讨论实时信号。

3.关键数据结构

sigset_t - 信号集

#define _NSIG 64
#define _NSIG_BPW 64
#define _NSIG_WORDS (_NSIG / _NSIG_BPW)

typedef struct {
    unsigned long sig[_NSIG_WORDS];
} sigset_t;

使用位图表示信号集合

每个bit代表一个信号(1-64)

用于阻塞掩码、未决信号集等

从上图来看,每个信号只有一个bit的未决标志, 非0即1, 不记录该信号产生了多少次,阻塞标志也是这样表示的。因此, 未决和阻塞标志可以用相同的数据类型sigset_t来存储, sigset_t称为信号集, 这个类型可以表示每个信号的“有效”或“无效”状态, 在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞, 而在未决信号集中“有 效”和“无效”的含义是该信号是否处于未决状态。阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask), 这里的“屏蔽”应该理解为阻塞而不是忽略。

struct sigpending - 待处理信号

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

4.信号集操作函数

这些函数都是同一个头文件

1. 基本信号集操作函数

sigemptyset

#include <signal.h>

int sigemptyset(sigset_t *set);
  • 功能:初始化信号集,清空所有信号
  • 参数:set - 指向信号集的指针
  • 返回值:成功返回0,失败返回-1

sigfillset

int sigfillset(sigset_t *set);
  • 功能:初始化信号集,包含所有信号
  • 参数:set - 指向信号集的指针
  • 返回值:成功返回0,失败返回-1

sigaddset

int sigaddset(sigset_t *set, int signum);
  • 功能:向信号集中添加指定信号
  • 参数:
    • set - 信号集指针
    • signum - 要添加的信号编号
  • 返回值:成功返回0,失败返回-1

sigdelset

int sigdelset(sigset_t *set, int signum);
  • 功能:从信号集中删除指定信号
  • 参数:
    • set - 信号集指针
    • signum - 要删除的信号编号
  • 返回值:成功返回0,失败返回-1

sigismember

int sigismember(const sigset_t *set, int signum);
  • 功能:检查信号是否在信号集中
  • 参数:
    • set - 信号集指针
    • signum - 要检查的信号编号
  • 返回值:在集合中返回1,不在返回0,错误返回-1

2. 进程信号掩码操作函数

int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
  • 功能:调用函数sigprocmask 可以读取或更改进程的信号屏蔽字(阻塞信号集)。
  • 参数:
    • how - 操作方式:假设当前的信号屏蔽字为mask,下表说明了how参数的可选值。

- <font style="color:rgb(15, 17, 21);">set - 新的信号集</font>
- <font style="color:rgb(15, 17, 21);">oset - 保存原来的信号集</font>
  • 返回值:成功返回0,失败返回-1

如果oset是非空指针,则读取进程的当前信号屏蔽字通过oset参数传出。如果set是非空指针,则 更改进程的信 号屏蔽字,参数how指示如何更改。如果oset和set都是非空指针,则先将原来的信号 屏蔽字备份到oset里,然后 根据set和how参数更改信号屏蔽字。

如果调用sigprocmask解除了对当前若干个未决信号的阻塞,则在sigprocmask返回前,至少将其中一个信号递达。

3. 等待信号函数

int sigpending(sigset_t *set);
  • 功能:读取当前进程的未决信号集,通过set参数传出。
  • 参数:set - 用于返回未决信号的信号集
  • 返回值:成功返回0,失败返回-1

5.代码实践

test:test.cpp
	g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
	rm -f test
#include <iostream>
#include <signal.h>
#include <unistd.h>
using namespace std;

void PrintPending(sigset_t &pending)
{
    // 左->右=高位->低位,从左向右高位到低位打印0000……0000
    cout << "当前进程pending信号集:";
    for (int i = 31; i > 0; i--)
    {
        // 检测i号信号是否在信号集中,如果在,打印1,否则,打印0
        if (sigismember(&pending, i))
        {
            cout << "1";
        }
        else
            cout << "0";
    }
    cout << endl;
}
void handler(int signal)
{
    // 防止解除屏蔽后信号递达进程直接退出看不到现象
    // 验证pending信号集是在递达前变化的
    // 定义pending信号集
    sigset_t pending;
    sigemptyset(&pending);
    // 获取当前进程的pending信号集,实际上就是将内核中的pending表拷贝到栈上的pending信号集中
    sigpending(&pending);
    // 打印pending信号集
    cout<<"##################################"<<endl;
    PrintPending(pending);
    cout<<"##################################"<<endl;
    //如果pending信号集在handler函数结束
    cout << "信号递达了" << endl;
}
int main()
{
    signal(2, handler);
    // 设置信号屏蔽字block,oblock为原来的信号屏蔽字
    // 当前设置的信号集都是用户层面的,在栈上定义的,要想设置内核层面的,需系统调用
    sigset_t block, oblock;
    // 变量定义时为随机值,为确保信号集准确性需将其清空
    sigemptyset(&block);
    sigemptyset(&oblock);
    // 将2号信号添加到block中
    sigaddset(&block, 2);
    // 设置内核的信号屏蔽字
    sigprocmask(SIG_SETMASK, &block, &oblock);
    int cnt = 5;
    while (true)
    {
        // 定义pending信号集
        sigset_t pending;
        sigemptyset(&pending);
        // 获取当前进程的pending信号集,实际上就是将内核中的pending表拷贝到栈上的pending信号集中
        sigpending(&pending);
        // 每隔一秒打印一次pending信号集
        PrintPending(pending);
        sleep(1);
        cnt--;
        // 5秒后解除屏蔽
        if (cnt == 0)
        {
            cout << "当前进程解除屏蔽了" << endl;
            sigprocmask(SIG_SETMASK, &oblock, nullptr);
        }
    }
    return 0;
}

补充知识:

1.和信号捕捉一样,进程并不能屏蔽所有信号,9号信号和19号信号不可被屏蔽

2.在解除屏蔽后,pending信号集是在信号递达前变化还是在在信号递达后变化?

答案是在信号递达前变化

信号处理的时间线:

1. 信号产生 → 2. 信号加入 pending 集 → 3. 解除屏蔽 → 4. 从 pending 集移除 → 5. 信号递达

我们想一下就可以理解,大部分信号的默认处理都是终止进程,信号递达后,进程已经终止了,还这么改变pending信号集