linux信号切换到用户态,Linux之信号第二谈

重提信号概念

前一篇中提到了信号的概念,并且对信号的产生及一些属性有了一定的认识,这一次,要从更加深入的角度看待信号。

之前提到过,当我的进程在接收到信号之前,就已经知道了,当我接收到某种信号之后就要发生某一项动作,换句话说,在进程内部,一定存在这某种结构,将这些信息都记录了下来,很明显,对于进程而言,这些信息都会保存在它的PCB当中。

首先我们来认识这样几个概念:

信号递达(Delivery):执行信号的处理动作;

信号未决(Pending):信号从产生到递达之间的状态;

阻塞(Block):被阻塞的信号被保存在未决状态,直到解除阻塞之后,才会执行递达动作。只要信号阻塞就永远不会递达;

忽略(Ignore):忽略完全不同于阻塞,忽略是在递达之后可选的一种动作;

这样的几个概念显得有点太过笼统,这里截取了一张信号在PCB中的示意图,如下:

aaeb910dd01c693ba28d31d5cbf01962.png

每个信号都有两个标志位分别表示阻塞(block)和未决(pending),还有一个函数指针表示处理动作。信号产生时,内核在进程控制块中设置该信号的未决状态,直到信号递达清除该标志位。

Linux为了节省内存空间,在设计的时候,使用了类似位图的结构,只留出一个bit的大小,分别用0和1表示阻塞或者未决状态,那么对于上图,我们就可以看到三张表:阻塞表,未决表,handler表。

对于阻塞表,该位为0,表示进程对该信号不阻塞,为1表示对该信号阻塞;

对于未决表,该位为0,表示该信号没有产生,为1表示该信号已经发生;

handler表就类似我们之前提到过的信号处理函数signal(),用来表示对于某一信号的处理方式。

了解了基本结构之后,有几点我们需要说明:

1、pending表和block表之间没有任何关系。信号的产生是异步的,对于进程而言完全随机,而阻塞状态是该进程对某一信号所做的限制;

2、信号的发生,对于进程而言,只是将该进程PCB中的pending表中的对应位置1,其他的操作和信号就不再有任何直接关系,这就解释了在信号来临之前进程就已知了某个信号对应的动作;

3、在Linux下,由于这里只是通过一个bit位来存储信息,所以在信号递达之前,信号发生多次只记一次。当然,更严格意义上说,常规信号是这样的,对于实时信号(34~64号信号),在递达之前,多次产生的信号会保存到某个队列当中,实时信号暂时不在我们的讨论范围之内。

4、任何一个信号,都不会是被立刻递达,这个后面解释。

由于阻塞标志和未决标志都是用一个bit位来表示,因此对于Linux,引入了一个用户类型sigset_t,两种标志都可以使用sigset_t数据类型来存储,sigset_t称为信号集。因此就有了阻塞信号集和未决信号集。阻塞信号集又叫做信号屏蔽字(有没有很熟悉的感觉)。

信号集操作函数

信号集操作函数,顾名思义,就是对上面的几种信号进行操作,之前我们提到的信号操作函数,实际上就是在更改这里的pending表,因此,我们这里提到的信号集操作函数,可以查询和修改阻塞信号集中的数据,对于pending表中的数据,这里只提供了查看的函数接口。具体函数声明如下:

// 信号集操作函数

#include

int sigemptyset(sigset_t *set);

# 初始化,清零所有信号对应的bit位

int sigfillset(sigset_t *set);

# 对所有信号的bit位置1

int sigaddset(sigset_t *set, int signum);

# 将指定信号bit位置1

int sigdelset(sigset_t *set, int signum);

# 将指定信号bit位清零

以上四个函数,成功返回0,失败返回-1

int sigismember(const sigset_t *set, int signum);

# 判断一个信号集的有效信号中,是否包含某个信号

# 包含返回1, 不包含返回-1

// 屏蔽信号集操作函数(写)

int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

# 读取或更改进程中的信号屏蔽字(阻塞信号集)

# 成功返回0, 失败返回-1

# 参数1,how有三种定义

SIG_BLOCK:添加对应位,mask = mask| set

SIG_UNBLOCK:清零对应位mask&~set

SIG_SETMASK:设置对于位mask=set

# 参数2,设置的SIG值

# 参数3, 输出型参数,用来获取修改之前的信号屏蔽字

当我们调用sigprocmask对某些信号解除屏蔽之后,在该函数返回之前,至少有一个信号被递达// 未决信号集操作函数(读)

#include

int sigpending(sigset_t *set);

# 输出型参数,将pending列表通过set传回

# 成功返回0,失败返回-1

说了这么多,下面通过代码做一简单验证。(以SIGINT信号为例)#include

#include

void printfPending(sigset_t *pending)

{

int i = 1;

for(;i <= 31; i++)

{

if(sigismember(pending, i)){

printf("1");

}

else{

printf("0");

}

}

printf("\n");

}

int main()

{

sigset_t block, oblock, pending;

sigemptyset(&block);

sigaddset(&block, SIGINT);    // 设置block值

sigprocmask(SIG_SETMASK, &block, &oblock);    // 设置信号屏蔽字

while(1){

sleep(1);

sigpending(&pending);

printfPending(&pending);    // 获取pending值

}

printf("hello world\n");

return 0;

}

因为SIGINT信号对应的操作是ctrl+c,但上面将SIGINT信号设置为屏蔽状态,因此,当我们输入ctrl+c之后并没有立即终止该进程,我们看到的第二为pending值由0变为1。如下图:

9c78342ccd4f3b0cb996a9b5adba46b8.png

接下来将代码做一简单调整,我们设置10秒之后,信号屏蔽字被自动清零,为了防止ctrl+c将信号终止,所以这里SIGINT信号执行自定义行为,代码如下:#include

#include

void printfPending(sigset_t *pending)

{

int i = 1;

for(;i <= 31; i++){

if(sigismember(pending, i)){

printf("1");

}

else{

printf("0");

}

}

printf("\n");

}

void runSig(int i)

{

printf("run SIGINT\n");

}

int main()

{

signal(SIGINT, runSig);

sigset_t block, oblock, pending;

sigemptyset(&block);

sigaddset(&block, SIGINT);

sigprocmask(SIG_SETMASK, &block, &oblock);

int count = 0;

while(1)

{

if(count == 10)

{

sigdelset(&block, SIGINT);

sigprocmask(SIG_SETMASK, &block, &oblock);

}

sleep(1);

sigpending(&pending);

printfPending(&pending);

count++;

}

printf("hello world\n");

return 0;

}

运行行结果如下:

c2b4ff3cfa13de247515e7d2f4443f19.png

由于这里已经设置了自定义SIGINT的动作,因此,即使10秒之后,ctrl+c也不会终止进程

信号捕捉

信号捕捉的过程

关于信号捕捉,其实前面一直在说,我们把对信号的自定义行为称为信号捕捉。对信号的处理有三种,忽略,默认,捕捉。

前两种算是比较简单的。站在操作系统的角度,忽略信号其实要做的就是将pending中的1改为0即可,不需要其他操作;对于默认动作,大部分的默认动作的最终结果都是终止进程,先有个简单简单认识,接下来看捕捉状态下的情况,看下面这张图:

0c314676bd43e0e2e3082d228c427f09.png

①:发生了外部终端,或者遇到了陷阱、异常,这个时候,会由用户态切换到内核态处理该异常;

②:内核处理完成异常之后,在返回用户态执行原代码之前,会检查该进程的PCB中有无未处理的信号(内核会在内核态切换到用户态的过程中检查有无未处理的信号);

③:这时发现了存在未处理的信号,不受阻塞,而且该信号的处理动作是捕捉的,就会切换到用户态去执行自定义的函数(因为这个函数是用户定义的,如果不切换用户,由内核态直接去执行,是不安全的);

④:在执行完自定义的信号处理函数之后,会受到系统调用再次切换到内核态;

⑤:再次进行检查,然后返回到用户态,从上次被中断的地方继续向下执行。

这就是捕捉的整个过程,一共发生了四次用户态到内核态之间的转化,这时候再看我们的忽略动作,当执行的第三步之后,发现该动作是忽略,于是在内核态直接将pending中的对应位清零,直接返回用户态终端的地方继续执行。对于默认动作,由于通常会终止进程,所以在内核态将对应位的pending值改0之后,同时销毁PCB,直接结束进程。(这个过程还是挺重要的)

sigaction()函数

sigaction函数可以设置和读取与指定信号相关联的动作,与signal函数功能类似,函数声明与注释如下:#include 

int sigaction(int signum, const struct sigaction *act,struct sigaction *oldact);

# 成功返回0, 失败返回-1

# 参数1,信号编号

# 参数2,若act非空,按照结构体中的信息修改处理动作

# 参数3,输出型参数,若非空,获取原来的struct结构。

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_handler有三种,SIG_DFL表示默认动作;SIG_IGN表示忽略信号;为函数指针,表示执行捕捉动作

# sa_mask表示当正在对该信号动作时,除了当前信号被屏蔽之外,还需要屏蔽的其他信号

# sa_flags这里直接设置为0即可,暂不关心

# 其他两个参数这里也暂不关心

这里给出测试代码:#include 

#include 

void IntRun(int i)

{

printf("my sigaction is running\n");

}

int main()

{

struct sigaction act, oact;

act.sa_handler = IntRun;

sigemptyset(&act.sa_mask);

act.sa_flags = 0;

sigaction(SIGINT, &act, &oact);

while(1){

sleep(1);

printf("hello world\n");

}

return 0;

}

输出结果如图:

f582c135668939ff6cf33115bf566765.png

sigaction与signal函数功能类似,这里只介绍用法,不在多说。

pause()函数

首先给出pause函数的定义#include 

int pause(void);

函数定义特别简单,pause函数的功能是将调用进程挂起,直到有信号递达。

如果到达的信号是将进程终止,那么进程直接结束,来不及返回;

如果到达信号被忽略,则继续挂起,无返回值;

如果调用动作是捕捉,那么调用信号处理函数之后,pause返回-1,同时设置errno为EINTR(被信号中断)。

可见,pause函数,只有当出错的情况下才会有返回值,这点和exec函数类似。

接下来,让我们写一段小代码,使用alarm函数和pause函数写一个自己的sleeep函数,函数名为mysleep。

实现原理:利用了pause函数的特性,会将进程挂起,直到有捕获(catch)的行为,才会将pause函数终止。利用alarm函数定时,闹钟时间到达之后,会调用自定义函数,发生捕获行为,导致pause函数终止,从而实现了sleep的功能。

这里给出了signal函数和sigaction函数版本的,两者基本一致,不同之处在于sigaction需要设置的参数较多。代码如下:#include 

#include 

#include 

void run_alarm(int i)

{}

/*

// signal版本

size_t mysleep(size_t second)

{

signal(SIGALRM, run_alarm);

alarm(second);

pause();

int ret = alarm(0);

return ret;

}

*/

// sigaction版本

size_t mysleep(size_t second)

{

struct sigaction act, oact;

act.sa_handler = run_alarm;

sigemptyset(&act.sa_mask);

act.sa_flags = 0;

sigaction(SIGALRM, &act, &oact);

alarm(second);

pause();

int ret = alarm(0);

sigaction(SIGALRM, &oact, NULL);

return ret;

}

int main()

{

while(1){

mysleep(2);

printf("this is mysleep\n");

}

return 0;

}

可重入函数

可重入函数的概念其实很好理解。有些函数,如果重入不会导致出错或不安全的话,我们把这些函数叫做可重入函数,反之,叫做不可重入函数。

举个例子,当我们对一个链表进行插入的时候,中途收到一个信号,该信号执行自定义动作,该动作也是在该结点处插入一个新节点,就会造成下图所示的情况,最终的2号结点并没有被插入,这就是所说的不可重入函数

c3036b3446083f331f2d13f46f493f43.png

问题来了,很容易可以发现,这个和线程安全有着很大的相似之处,都是由于重入导致的问题,这里做以简单区分。

区别:

1、前提不同:线程安全是在多线程情况下产生的,可重入函数可以是在单线程下由信号的捕获产生的的重入

2、范围不同:线程安全不一定可重入,可重入函数一定满足线程安全

3、对临界资源加锁可以实现线程安全,但依旧是不可重入的,因为加锁只能防止多线程的情况,单一线程的情况不一定安全。

4、线程安全要求不同线程访问同一块地址空间,而可重入函数要求不同的执行流对数据的操作互不影响。

可重入函数的几点必要条件

1、不在函数内部使用静态或全局数据 ;       2、不返回静态或全局数据,所有数据都由函数的调用者提供;       3、使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据;       4、不调用不可重入函数;

本文所有源代码,以打包上传,下载连接:

------muhuizz整理

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值