信号Ⅱ
🔗 接上篇
👉🔗进程信号篇Ⅰ:信号的产生(signal、kill、raise、abort、alarm)、信号的保存(core dump)
四、信号的阻塞及保存
1. 一些概念
信号递达(Delivery)
:实际执行信号的处理动作
-
方式1. 进程对信号的
默认
处理:进程对信号递达的动作默认是终止进程(term、core) -
方式2. signal 函数
自定义
捕捉信号 -
方式3.
忽略
信号,是递达后,选择的一种处理方式
信号未决(Pending)
:信号从产生到递达之间的状态
进程可以选择
阻塞(Block)
某个信号
-
被阻塞的信号产生时将保持在未决状态,直到进程
解除
对此信号的
阻塞
,才执行递达的动作 -
注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而
忽略是
在递达之后可选的
一种处理动作
2. pcb 对 信号的管理
pcb 里面有三张表,记录着 block、pending、handler
pending 表
:位图结构储存,比特位的位置,表示哪一个信号,比特位的内容,代表
是否收到该信号
000000000000...0001000
uint32_ t pending = 0;
pending |= (1<<(signo-1));
block 表
:位图结构储存,比特位的位置,表示哪一个信号,比特位的内容,代表
是否对应的信号被阻塞
0000...0010
handler 表
:函数指针数组,该数组的下标,表示信号编号,数组的特定下标的内容,表示该信号的递达动作
void (*sighandler_t) (int);
// signal 函数第二个参数的一些处理动作:
// 报错处理
#define SIG_ERR ((__sighandler_t) -1) /* Error return. */
// 默认(不写也是默认)
#define SIG_DFL ((__sighandler_t) 0) /* Default action. */
// 忽略处理
#define SIG_IGN ((__sighandler_t) 1) /* Ignore signal. */
-
每个信号都有两个标志位分别表示阻塞(block)和未决(pending),还有一个函数指针表示处理动作。信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。在上图的例子中,SIGHUP 信号未阻塞也未产生过,当它递达时执行默认处理动作。
-
SIGINT 信号产生过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略,但在没有解除阻塞之前不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞。
-
SIGQUIT 信号未产生过,一旦产生SIGQUIT信号将被阻塞,它的处理动作是用户自定义函数 sighandler。 如果在进程解除对某信号的阻塞之前这种信号产生过多次,将如何处理?POSIX.1 允许系统递送该信号一次或多次。Linux是这样实现的:常规信号在递达之前产生多次只计一次,而实时信号在递达之前产生多次可以依次放在一个队列里。本章不讨论实时信号。
3. 数据类型 sigset_t
sigset_t 是由 OS 提供的一种位图数据类型
/* A `sigset_t' has a bit for each signal. */
# define _SIGSET_NWORDS (1024 / (8 * sizeof (unsigned long int)))
typedef struct
{
unsigned long int __val[_SIGSET_NWORDS];
} __sigset_t;
未决和阻塞标志可以用相同的数据类型 sigset_t 来存储,sigset_t 称为信号集。
控制 block 表的叫:信号屏蔽字、阻塞信号集
控制 pending 表的叫:pending 信号集
4. 信号集操作函数
//【头文件】
#include <signal.h>
// 对信号集全置 0
int sigemptyset(sigset_t *set);
// 对信号集全置 1
int sigfillset(sigset_t *set);
// 把一个信号添加到信号集里
int sigaddset (sigset_t *set, int signo);
// 把一个信号从信号集里删除
int sigdelset(sigset_t *set, int signo);
// 上述四个函数返回值:成功返回 0,失败返回 -1
// 判断信号是否在信号集中
int sigismember(const sigset_t *set, int signo);
// 返回值:若包含则返回1,不包含则返回0,出错返回-1。
4.1 sigprocmask 函数:读取或更改进程的信号屏蔽字(阻塞信号集)
针对 block 表的 。
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
参数 how:
- 用选项指示如何更改,见下文
参数 set:
–
更改
当前进程的信号屏蔽字,可以为 nullptr 则没有动作
参数 oset:
读取
当前进程的信号屏蔽字,可以为 nullptr 则没有动作
返回值:
- 若成功则为 0,出错则为 -1
假设当前的信号屏蔽字为mask,下表说明了how参数的可选值
-
参数 how:
-
SIG_BLOCK
:set 包含了我们希望添加到当前信号屏蔽字的信号,相当于 mask=mask|set -
SIG_UNBLOCK
:set 包含了我们希望从当前信号屏蔽字中解除阻塞的信号,相当于 mask =mask&~set -
SIG_SETMASK
:设置当前信号屏蔽字为 set 所指向的值,相当于 mask=set
如果调用 sigprocmask 解除了对当前若干个未决信号的阻塞,则在 sigprocmask 返回前,至少将其中一个信号递达。
🌰使用举例:
实现:让进程只屏蔽 2 号信号,并保存旧的信号方案
void showblock(sigset_t *oset)
{
int signo = 1;
for(; signo <= 31; signo++)
{
if(sigismember(oset, signo))
cout << "1";
else
cout << "0";
}
cout << endl;
}
int main()
{
// 只是在用户层面上进行设置
sigset_t set, oset;
sigemptyset(&set);
sigemptyset(&oset);
sigaddset(&set,2) // SIGINT
// 设置进入进程,谁调用设置谁
sigprocmask(SIG_SETMASK, &set, &oset); // 1.键入ctrl+c信号没有反应 2.我们看到老的oset会全为0
int cnt = 0;
while(true)
{
showblock(&oset);
sleep(1);
cnt++;
if(cnt == 10)
{
cout << "recover block" <<endl;
sigprocmask(SIG_SETMASK, &oset, &set); // 对 2 号信号进行解除阻塞
showblock(&set); // set 在旧信号集参数位置就会把 2 带出来???不把 2 号信号捕捉,是会执行默认退出动作滴
}
}
return 0;
}
4.2 sigpending 函数:读取当前进程的未决信号集
针对 pending 表。
#include <signal.h>
int sigpending(sigset_t *set);
参数 set:
- 当前进程的未决信号集,从 set 传出
返回值:
- 若成功则为 0,若出错则为 -1
🌰使用举例:
#include <iostream>
#include <cassert>
#include <cstring>
#include <unistd.h>
#include <signal.h>
using namespace std;
static void PrintPending(const sigset_t &pending) // 本文件内有效
{
cout << "当前进程的pending位图: ";
for(int signo = 1; signo <= 31; signo++)
{
if(sigismember(&pending, signo)) cout << "1";
else cout << "0";
}
cout << "\n";
}
static void handler(int signo)
{
cout << "对特定信号:"<< signo << "执行捕捉动作" << endl;
int cnt = 30;
while(cnt)
{
cnt--;
sigset_t pending;
sigemptyset(&pending); // 不是必须的
sigpending(&pending);
PrintPending(pending);
sleep(1);
}
}
int main()
{
// 2.0 设置对2号信号的的自定义捕捉
signal(2, handler);
int cnt = 0;
// 1. 屏蔽 2号信号
sigset_t set, oset;
// 1.1 初始化
sigemptyset(&set);
sigemptyset(&oset);
// 1.2 将2号信号添加到set中
sigaddset(&set, SIGINT/*2*/);
// 1.3 将新的信号屏蔽字设置进程
sigprocmask(SIG_BLOCK, &set, &oset);
// 2. while获取进程的pending信号集合,并01打印
while(true)
{
// 2.1 先获取pending信号集
sigset_t pending;
sigemptyset(&pending); // 不是必须的
int n = sigpending(&pending);
assert(n == 0);
(void)n; //保证不会出现编译是的 warning
// 2.2 打印,方便我们查看
PrintPending(pending);
// 2.3 休眠一下
sleep(1);
// 2.4 10s之后,恢复对所有信号的 block 动作
if(cnt++ == 10)
{
cout << "解除对 2 号信号的屏蔽" << endl; //先打印
sigprocmask(SIG_SETMASK, &oset, nullptr); // 要执行这一步后继续 while 循环,需要对 2 号信号进行捕捉,否则阻塞接触后,执行默认的程序退出动作了
}
}
}
五、信号的处理
我们已经知道的是,如果一个信号之前被 block,当他解除 block 的时候,对应的信号会被立即递达。
这是因为,信号的产生是异步的,当前进程可能在做更重要的事。
合适处理信号的时候,是 当进程从
内核态
切换回
用户态
的时候,进程会在 OS 的指导下,进行信号的检测与处理(1. 默认 2. 忽略 3. 自定义捕捉)
1. 用户态 和 内核态
用户态:执行你写代码的时候,进程所处的状态
内核态:执行OS的代码的时候,进程所处的状态
我们什么时候执行 OS 的代码了?即,什么时候 用户态 切换到 内核态 过了?
-
- 进程时间片到了,需要切换,就要执行进程切换逻辑
-
- 系统调用(库函数包装了大量的系统接口)
2. 重新认识 虚拟地址空间
-
所有的进程 [0, 3] GB 是不同的,每一个进程都有自己的
用户级页表
-
所有的进程 [3, 4] GB 是一样的,每一个进程都可以看到同一张
内核级页表
,所有进程都可以通过统一的窗口, 看到同一个 OS -
OS 运行的本质:其实都是在进程的地址空间内运行的
每个进程无论如何切换,看到 [3, 4] GB 的内容不变,也就是说,看到 OS 的内容,与进程切换无关
-
系统调用的本质:其实就如同调用 .so 中的方法,在自己的地址空间中进行函数跳转并返回即可。
CPU 中有一个寄存器:
CR3 寄存器
,用来表征程序的执行级别
- 3:表征正在运行的程序执行级别是
用户态
- 0:表征正在运行的程序执行级别是
内核态
OS 提供的所有的系统调用,内部在正式执行调用逻辑的时候,都会去修改执行级别!
-
OS 是如何调度的呢?
OS 的本质是什么呢?
-
- OS 是软件,本质是一个死循环
-
- OS 内有时钟硬件,每隔很短的时间会向 OS 发送时钟中断(像键盘发送的中断一样),OS 就要执行对应的中断处理方法,即,检测当前进程的时间片,检测到时间片到了,自己这个进程,也是 OS,调用 schedule 函数 进行保存切换等工作。
-
故,
进程被调度,就是时间片到了,然后将进程对应上下文等进行保存并切换,选择合适的进程。这个过程本质就是
schedule
函数 完成的。
也就是说,当进程调用了系统接口的时候,转化为内核态,此时已经是 OS 在完成代码了,只是 OS 完成后将结果交给进程继续完成后续代码!!
那 什么时候适合处理信号?
六、信号的捕捉
1. 内核实现信号的捕捉
合适处理信号的时候,是 当进程从
内核态
切换回
用户态
的时候,进程会在 OS 的指导下,进行信号的检测与处理(检测到 block[i] == 0, pending[i] == 1,且有自定义信号捕捉行为才会处理!)
如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。
由于信号处理函数的代码是在用户空间的,处理过程比较复杂,举例如下:
- 用户程序注册了 SIGQUIT 信号的处理函数 sighandler。
- 当前正在执行 main 函数,这时发生中断或异常切换到内核态。
- 在中断处理完毕后要返回用户态的 main 函数之前检查到有信号 SIGQUIT 递达。
- 内核决定返回用户态后不是恢复 main 函数的上下文继续执行,而是执行 sighandler 函数,sighandler 和 main 函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是两个独立的控制流程。
- sighandler 函数返回后自动执行特殊的系统调用 sigreturn 再次进入内核态。 如果没有新的信号要递达,这次再返回用户态就是恢复 main 函数的上下文继续执行了。
画图解释上述过程:
下面是简图:
有一个问题:pending里面的1什么时候被置零呢?是在调用 handler 前还是之后呢?之前~
2. sigaction 函数:检测和更改信号动作
头文件
#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
参数 signum、act:
- 同 signal
参数 oldact:
- 返回老的处理方法
返回值:
- 调用成功则返回0,出错则返回- 1。
//The sigaction structure is defined as something like:
struct sigaction {
void (*sa_handler)(int); // 常用
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask; // 常用
int sa_flags; // 常用
void (*sa_restorer)(void);
};
- 当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生,那么它会被阻塞到当前处理结束为止。
-
如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用
sa_mask
字段说明这些需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字。
- sa_flags 字段包含一些选项,本章的代码都把 sa_flags 设为 0
- sa_sigaction 是实时信号的处理函数,本章不详细解释这两个字段。
🌰使用举例:
static void PrintPending(const sigset_t &pending) // 本文件内有效
{
cout << "当前进程的pending位图: ";
for(int signo = 1; signo <= 31; signo++)
{
if(sigismember(&pending, signo)) cout << "1";
else cout << "0";
}
cout << "\n";
}
static void handler(int signo)
{
cout << "对特定信号:"<< signo << "执行捕捉动作" << endl;
int cnt = 20;
while(cnt)
{
cnt--;
sigset_t pending;
sigemptyset(&pending); // 不是必须的
sigpending(&pending);
PrintPending(pending);
sleep(1);
}
}
int main()
{
struct sigaction act, oldact;
memset(&act, 0, sizeof(act));
memset(&oldact, 0, sizeof(oldact));
act.sa_handler = handler;
act.sa_flags = 0;
sigemptyset(&act.sa_mask);
sigaddset(&act.sa_mask,3);
sigaddset(&act.sa_mask,4);
sigaddset(&act.sa_mask,5);
sigaction(2, &act, &oldact);
while(true)
{
cout << getpid() << endl;
sleep(1);
}
return 0;
}
🔗 接下篇
👉🔗进程信号篇Ⅲ:可重入函数、volatile关键字、SIGCHLD信号
🥰如果本文对你有些帮助,请给个赞或收藏,你的支持是对作者大大莫大的鼓励!!(✿◡‿◡) 欢迎评论留言~~