【Linux】进程信号篇Ⅱ:信号的阻塞及保存(sigset_t, sigprocmask, sigpending)、信号的处理、信号的捕捉(sigaction)

  • Post author:
  • Post category:linux





🔗 接上篇


👉🔗进程信号篇Ⅰ:信号的产生(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 的代码了?即,什么时候 用户态 切换到 内核态 过了?

    1. 进程时间片到了,需要切换,就要执行进程切换逻辑
    1. 系统调用(库函数包装了大量的系统接口)



2. 重新认识 虚拟地址空间

在这里插入图片描述

  1. 所有的进程 [0, 3] GB 是不同的,每一个进程都有自己的

    用户级页表

  2. 所有的进程 [3, 4] GB 是一样的,每一个进程都可以看到同一张

    内核级页表

    ,所有进程都可以通过统一的窗口, 看到同一个 OS


  3. OS 运行的本质:其实都是在进程的地址空间内运行的

每个进程无论如何切换,看到 [3, 4] GB 的内容不变,也就是说,看到 OS 的内容,与进程切换无关


  1. 系统调用的本质:其实就如同调用 .so 中的方法,在自己的地址空间中进行函数跳转并返回即可。

CPU 中有一个寄存器:

CR3 寄存器

,用来表征程序的执行级别

  • 3:表征正在运行的程序执行级别是

    用户态
  • 0:表征正在运行的程序执行级别是

    内核态

OS 提供的所有的系统调用,内部在正式执行调用逻辑的时候,都会去修改执行级别!

  1. 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信号




🥰如果本文对你有些帮助,请给个赞或收藏,你的支持是对作者大大莫大的鼓励!!(✿◡‿◡) 欢迎评论留言~~




版权声明:本文为m0_67470729原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。