9.3.信号(信号捕捉和信号处理的原理,4次内核态和用户态的切换)

信号的捕捉、处理

“合适的时候”处理信号,指的是什么时候?

(根据流程pending->block->handler分析)

什么时候处理信号,一定是在内核态中处理信号,“合适的时候”就是指从内核态返回用户态的时候(即执行完系统调用接口的代码或者内核处理完异常,准备返回用户态时),这个时候进行信号检测(检查pending表)和处理。

为什么会进入内核态,程序执行自己编写的代码的时候是用户态,执行到系统调用接口或者遇到缺陷陷阱异常时,就由用户态切换到内核态。

如何进入内核态或者用户态,在汇编语言上有一个中断编号叫 80,汇编指令为int 80(这里的int不是整型的意思,是中断单词的缩写),用这个中断可以陷入内核态

ecs寄存器

CPU里寄存器有两套,一套可见,一套不可见,供CPU自用。
其中的ECS寄存器是不可被外部可见的,它的最低2位比特位表示当前CPU的执行权限,如果该寄存器最低2位里面是00,表示此时处于内核态;是11(即十进制整数3),表示此时是用户态。
当int 80(陷入内核)时,会把ecs寄存器最低2位比特位由3变为0。
检查最低2位,是否是0,就能给与能访问内核代码的权限

一.进程地址空间中的内核地址空间

在这里插入图片描述
有几个进程,就有几个用户级页表,因为进程具有独立性,但是内核级页表只有1份,所有的进程看到的内核地址空间都是相同的。

在进程视角:调用系统中的方法,比如说调用open函数,就是在我自己的进程地址空间中向高地址偏移,到内核地址空间找open函数的具体代码,执行完后再回到用户地址空间的正文代码区,这样在进程看来,我执行系统调用都是在我自己的进程地址空间执行的。

在操作系统视角:任何时刻都有进程执行,任何时刻进程需要执行操作系统的代码时,进程只需来到内核地址空间中找系统调用的虚拟地址,经过内核级页表映射,找到代码的物理内存空间即可,这样进程想执行系统调用,就能随时执行。

二.用户态和内核态

用户态是一个受管控的状态,比如说用户态受访问权限的约束、受到资源限制(有些资源你不能用)

内核态是os执行自己代码的一个状态,不受资源限制或者权限约束,具备非常高的优先级。

每个进程有自己的进程地址空间(不仅有自己的用户地址空间,也有自己的内核地址空间),每个进程的地址空间映射到物理内存的用户级页表不同。

内核地址空间整个操作系统只有一份,还有一个内核级页表,这两个能被所有进程看到。

当进程的时间片到了,底层硬件向os发送时钟中断,os在cpu内找当前正在运行的进程,通过它的进程地址空间找到进程切换的函数,然后在这个进程的地址空间的上下文里进行切换。因为是在这个进程地址空间的上下文进行切换,所以os能直接访问该进程在cpu里的上下文数据,然后把所有数据压入到进程pcb中,进而把进程放下去,换另外一个进程上来。

三.信号的捕捉和处理原理(4次内核态和用户态的切换)

一次信号捕捉并处理的过程
在这里插入图片描述

上图可以简化为这样:
在这里插入图片描述
可以看出,从信号的捕捉到信号处理完,总共4次内核态和用户态的切换

1.有人可能有疑问,在第3步的时候,做信号处理函数一定会返回用户态吗?
答:不一定的,因为需要检测到当前进程的pending表有信号没处理,且也没有被屏蔽,如果有就会根据handler表,看看是默认处理动作还是忽略,还是自定义动作。

如果是默认动作,在内核态执行信号的默认动作,该终止就终止,该暂停就暂停;
如果是忽略,则在内核态什么都不做,把pending表对应的比特位从1变为0,然后返回用户态,并从上一次中断的地方继续往下执行;
如果是自定义动作,返回用户态,执行信号的自定义处理函数。

2.处于内核态时,为什么不是os执行用户自定义的信号处理函数?

OS有权限执行user handler的方法,但是它不想去执行用户自己编写的任何代码,因为如果用户在自定义函数里写了一些非法操作,比如说盗取信息,以内核态,OS是一定会执行这些非法操作的,所以必须回到用户态来做。
执行自定义的信号处理函数,os会先切换到用户态,在用户态执行用户自定义的信号处理函数。

执行完自定义信号处理函数,os需要从用户态切换到内核态来知道上一次被中断的地方在哪(通过程序计数器知道地点),恢复函数栈帧结构,然后再从内核态切换为用户态,从上次被中断的地方继续向下执行,自此完成信号捕捉动作

信号捕捉除了siganl(int,void()(int))函数外,还有sigaction()

四.信号捕捉–sigaction()函数

在这里插入图片描述
注意sigaction名字,在Linux下即是一个结构体的名字,也是一个系统调用。

sigaction函数可以读取和修改与指定信号相关联的处理动作。调用成功则返回0,出错则返回- 1。
第一个参数signum是指定信号的编号。若act指针非空,则根据act修改该信号的处理动作。若oact指针非空,则通过oact传出该信号原来的处理动作。act和oact指向sigaction结构体。

第一个参数对应的信号编号,第二个是个结构体,是输入型参数,第三个参数是输出型参数,用来记录老的结构体数据。

sigaction结构体含有的成员变量
在这里插入图片描述

struct sigaction act, oact;//定义了两个sigaction结构体变量act和oact。sigaction结构体用于描述信号处理的方式。

act.sa_flags = 0;//将act的sa_flags字段设置为0,表示使用默认的信号处理方式。

sigemptyset(&act.sa_mask);//将act的sa_mask字段清空,表示不屏蔽任何信号

act.sa_handler = handler; //将自定义的信号处理函数handler赋值给act的sa_handler字段,表示当接收到相应的信号时,会调用该函数进行处理。

将sigacntion结构体里的sa_handler赋值为常数SIG_IGN(宏),表示忽略信号;

赋值为常数SIG_DFL表示执行系统默认动作;

赋值为一个函数指针表示用自定义函数捕捉信号,或者说向内核注册了一个信号处理函数,该函数类型是返回值为void,可以带一个int参数,通过参数可以得知当前信号的编号。显然,这是一个回调函数。

例子

act.sa_handler = handler
sigaction(2, &act, &oact);

sigaction(2, &act, &oact);这行代码的作用是设置信号处理函数,在act结构体里由于设置了自定义的信号处理函数(act.sa_handler = handler),2号信号就可以通过sigaction函数和这个信号处理函数关联起来,同时将2号信号的默认信号处理方式(act.sa_handler变量没有被赋值前的处理方式)的结果返回给oact结构体

cout << "default action is:" << (int)(oact.sa_handler) << endl;语句打印出原先的信号处理方式,以验证是否成功设置了新的信号处理函数。

五.先后接收到同类型信号的处理方法

将2号信号的处理方式注册为自定义处理方式,当一个进程第一次收到2号信号,进程会执行对应的信号处理方式,正在执行的同时将2号信号加入屏蔽信号集(block表),这时候如果进程再次接收到第二次2号信号,那么这次的2号信号(新发生的相同信号),先把它记录在pending表中,然后阻塞这次的2号信号,直到信号处理函数返回且从信号屏蔽字中移除该信号后(代表上一次2号信号处理完),才会再次触发处理2号信号的流程。

当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生,那么它会被阻塞到当前处理结束为止。 如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用sa_mask字段说明这些需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字

可重入函数和不可重入函数

因为进程调度(也叫做因为时许问题)导致的数据丢失、内存泄漏的问题

在这里插入图片描述

上面的代码没有问题,插入结点的代码也没有问题,现在我想把node1头插进链表。

main函数调用insert函数向一个链表head中插入节点node1,插入操作分为两步,刚做完第一步的时候,因
为硬件中断使进程切换到内核,再次回用户态之前检查到有信号待处理,于是切换到sighandler函数。

sighandler也调用insert函数向同一个链表head中插入节点node2,插入操作的两步都做完之后从sighandler返回内核态,再次回到用户态就从main函数调用的insert函数中继续往下执行,先前做第一步之后被打断,现在继续做完第二步。结果是,main函数和sighandler先后向链表中插入两个节点,而最后只有一个节点真正插入链表中了。

像上例这样,insert函数被不同的控制流程调用,有可能在第一次调用还没返回时就再次进入该函数,这称为重入,insert函数访问一个全局链表,有可能因为重入而造成错乱,像这样的函数称为不可重入函数,反之,
如果一个函数只访问自己的局部变量或参数,则称为可重入(Reentrant) 函数。想一下,为什么两个不同的控制流程调用同一个函数,访问它的同一个局部变量或参数就不会造成错乱?

如果一个函数被重入后不会发生问题,那这种函数就是可重入函数,如果被重入后发生严重问题,那这种函数就是不可重入函数

如果一个函数符合以下条件之一则是不可重入的:
1.调用了malloc或free,因为malloc也是用全局链表来管理堆的。
2.调用了标准I/O库函数。标准I/O库的很多实现都以不可重入的方式使用全局数据结构。

volatile关键字

编译器如果不执行优化动作,代码中的某个变量需要修改,cpu会访问内存,从内存中读取该变量到寄存器中,然后修改。

当编译器对代码进行了一定程度的优化之后,如果在main函数里发现没有要修改全局变量flag的代码,编译器会自作聪明的把flag第一次的值0放到寄存器里,从此往后cpu就只在cpu里自己检测flag,不访问内存。这时候来了2号信号,信号处理函数把flag值修改为1,但是这修改的是内存里的flag值,cpu里的flag值依然是0。

因为编译器优化的存在,让cpu无法看到内存,引起内存被遮盖的情况。优化是在编译阶段就优化好的,不是在加载到内存的时候才优化的

为了避免这种情况,程序员需要显式的告诉编译器不要优化我的某个子段,cpu必须访问内存,保持内存的可见性,这就是关键字volatile的作用

volatile int flag = 0;

void changeFlag(int signum)
{
    cout << "更改flag:" << flag;
    flag = 1;
    cout << "->" << flag << endl;
}

int main()
{
    signal(2, changeFlag);
    int time = 10;
    while (time--)
        sleep(1);

    cout << "进程正常退出后,flag为:" << flag << endl;
}

volatile的作用包括:

  1. 保证变量的可见性:在多线程或中断服务例程等情况下,若一个变量由不同线程或程序共享,并且可能被异步修改,将其声明为volatile可以确保每次访问该变量时都从内存中读取最新值,防止使用缓存中的过期值。
  2. 防止编译器优化:通常编译器会进行各种优化以提高代码的执行效率,例如将频繁使用的变量缓存在寄存器中。然而,对于被volatile修饰的变量,编译器会禁止这种优化,确保每次读写都直接与内存交互,从而在正确的时间点获取正确的值。
  3. 维持访问顺序:即使在代码优化过程中,编译器对代码进行了重排,volatile也能保证对该变量的访问顺序得以保持。

SIGCHLD信号

用于子进程退出,给父进程发送的信号(只有linux这样干,其他环境不确定),不过默认处理动作是忽略。

进程一章讲过用wait和waitpid函数清理僵尸进程,父进程可以阻塞等待子进程结束,也可以非阻塞地查询是否有子进程结束等待清理(也就是轮询的方式)。

采用第一种方式,父进程阻塞了就不能处理自己的工作了;采用第二种方式,父
进程在处理自己的工作的同时还要记得时不时地轮询一 下,程序实现复杂。

其实,子进程在终止时会给父进程发SIGCHLD信号,该信号的默认处理动作是忽略,父进程可以自定义SIGCHLD信号的处理函数,这样父进程只需专心处理自己的工作,不必关心子进程了,子进程终止时会通知父进程,父进程在信号处理函数中调用wait清理子进程即可。

请编写一个程序完成以下功能:父进程fork出子进程,子进程调用exit(2)终止,父进程自定义SIGCHLD信号的处理函数,在其中调用wait获得子进程的退出状态并打印。

事实上,由于UNIX 的历史原因,要想不产生僵尸进程还有另外一种办法:父进程调用sigaction将SIGCHLD的处理动作置为SIG_IGN,这样fork出来的子进程在终止时会自动清理掉,不会产生僵尸进程,也不会通知父进程。
系统默认的忽略动作和用户用sigaction函数自定义的忽略通常是没有区别的,但这是一个特例。此方法对于Linux可用,但不保证在其它UNIX系统上都可用。请编写程序验证这样做不会产生僵尸进程。

测试代码

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
void handler(int sig)
{
	pid_t id;
	while( (id = waitpid(-1, NULL, WNOHANG)) > 0){
		printf("wait child success: %d\n", id);
	}
	printf("child is quit! %d\n", getpid());
}

int main()
{
	signal(SIGCHLD, handler);
	pid_t cid;
	if((cid = fork()) == 0){//child
		printf("child : %d\n", getpid());
		sleep(3);
		exit(1);
	}
	while(1){
		printf("father proc is doing some thing!\n");
	sleep(1);
	}
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值