前言
进程对于信号的处理和我们现实生活中的行为是一致的:
-
进程必须能识别对应的信号,同时也能合理的处理信号。也就是意味着:进程本身是具备处理信号的能力的,这是进程内置功能的一部分。
-
当一个进程受到了信号,进程可能不会马上处理这个信号,而是在一个“合适的时候”对信号进行处理。
-
进程在收到信号和处理信号的时间内,一定需要有一个时间窗口,在此期间。进程有能将信号进行保存的能力。
-
在Linux下的信号查看
kill -l
:我们主要探讨的信号是 1-31 号的信号。
在这篇文章,我们将先带大家认识信号,了解一个信号的保存。
1. PCB中的信号保存
在 Linux 中操作系统会为每一个进程创建一个 PCB 结构体:struct task_struct
。其中包含一个字段来描述当前进程的收到的信号:
struct task_struct
{
int sign;
}
一个整型有32位:0000 0000 0000 0000 0000 0000 0000 0000。其中我们管理下层的1 ~ 32号的信号只需要采用位图的形式:1 ~ 31下标的比特位对应的信号编号,其中 0代表未收到该信号;1代表收到了该信号。
小结:
- 比特位的0/1表示是否收到信号
- 比特位的下标表示收到了几号信号
- 所谓发送信号,在进程看来:就是操作系统修改
struct task_struct
中的这个字段的某一个比特位设置为了1。注意:修改PCB的状态只能由操作系统来完成,因为用户无权修改。操作系统是管理者!(这也解释了为什么需要陷入内核态) - 同时:这样有一个弊端,如果收到了多个信号,进程只会处理一次。
为什么要保存信号呢?
- 因为我们的进程在收到信号之后不会立即处理信号!需要等到一个“合适的时候”才会处理信号。
2. 信号在内核中的存储
-
信号在内核中的存储有三个重要字段:
-
block:阻塞,也被称为信号掩码(signal mask)。表示当前的信号是否是阻塞的。在阻塞状态下的信号不会执行对应的方法。主要目的是:避免 “关键操作被信号打断”。例如,进程在执行磁盘写入、数据同步等原子操作时,若被
SIGINT
打断可能导致数据损坏,此时可将SIGINT
加入 block 状态,暂时屏蔽该信号。注意:block 不是 “丢弃信号”,而是 “延迟处理”—— 被阻塞的信号若在阻塞期间收到,会进入 pending 集(未决状态),直到信号被从 block 集中移除(解除阻塞),才会被处理。
-
pending:未决。 是进程 “已收到但未处理” 信号的 “暂存区”。是进程维护的另一个信号状态,表示 “进程已经收到,但因以下原因暂未处理” 的信号:a、信号处于 block 状态 b、进程正在处理另一个信号(同一时间只能处理一个信号,新收到的信号会暂存到 pending)。
注意:1 ~ 31 号信号虽然进程可能受到多次,但是只可能处理一次。这样的触发机制被称为:边沿触发。32 ~ 64号信号是电平触发,多次收到会记录次数(处理多次)
-
handler:信号处理函数。handler 是进程为某个信号注册的处理函数,其定义了 当进程收到该信号时,应该执行什么操作。Linux 中,每个信号(如
SIGINT
、SIGQUIT
)都有一个默认的 handler(默认行为),进程也可以通过signal()
或sigaction()
函数自定义 handler。
注意:该字段的类型就是一个函数指针。其中默认定义了三个宏:
其中:0表示使用内核的默认处理函数,1表示忽略该信号,表示不做任何处理。当然操作系统是能够识别这个handler字段是0/1/用户程序。如果是0,那么操作系统就会调用自己的方法(比如说:维护了一张表存储信号编号和默认处理方法的映射);如果是1,那么操作系统就不再做处理;如果是用户的处理方法,就直接调用即可(存的是用户的代码的)。 -
普通信号的范围是:[1, 31]。所以对于不同的信号,我们利用数组进行维护就非常不错了:
进程PCB管理信号就是利用三张表。这三张表分别表示着上面我们介绍的内核维护信号的三个字段。Linux 中的 struct task_struct
其中有字段,就是指向了这三张表,通过这样的管理手段,就能很好地管理好进程收到的信号了。
3. 信号集操作函数
Linux 允许用户能够利用信号集设置阻塞,但是不允许用户利用信号集设置未决。因为我们的 Linux 的进程管理信号是通过编号/下标的方式,所以我们的内核提供了一种类型:sigset_t
。通过这个函数设置标志位0/1表示取消/启动……而这样的性质不就是我们的信号屏蔽字表所需要的吗?
这个类型还配有相应的函数,来实现类似于位图的操作。
正如上面说谈:sigset_t 是一个被包装的类型。是不允许进行位运算的。只能通过系统提供的运算方式。
3.1 sigprocmask
-
sigprocmask
:可用读取/修改当前进程的信号的 block (信号屏蔽字)-
how:设置的方式
SIG_BLOCK
:根据当前传入的信号集新增当前进程的屏蔽字。相当于:mask = mask | set
SIG_UNBLOCK
:根据当前传入的信号集取消当前进程的屏蔽字。相当于:mask = mask & ~set
SIG_SETMASK
:根据当前传入的信号集设置当前进程的屏蔽字。相当于:mask = set
-
set:输入型参数,表示需要传入的信号集。
-
oldset:输出型参数,表示执行
sigprocmask
函数之前的信号集。
-
3.2 sigpending
-
sigpending
:
demo:
//测试:将某某信号阻塞,然后发送对应信号,观察pending表的变化
void printpending(const sigset_t& pending)
{
for(int i = 31; i >= 1; i--)
{
if(sigismember(&pending, i))
cout << 1;
else
cout << 0;
}
cout << endl << endl;
}
int main()
{
//1、做数据的预备工作
sigset_t bset, oset;
sigemptyset(&bset); //全部置为0
sigemptyset(&oset);
sigaddset(&bset, 2); //设置2号信号为1
//2、将数据加载到内核中
sigprocmask(SIG_SETMASK, &bset, &oset); //屏蔽2号信号
//3、重复打印,观察现象:
sigset_t pending;
while(true)
{
int n = sigpending(&pending); //获取当前进程的pending表
if(n < 0) continue;
printpending(pending); //打印信号
sleep(1);
}
return 0;
}
上面的代码,我们将信号2进行了屏蔽:
现象:当我们打印的时候,开始信号2的未决表是为空的,然后我们对其进行 ctrl c。我们就能观察到信号2的位置由0变1的现象了。
完。