III Linux系统编程 33 信号 4 捕捉信号.docx

上传人:b****5 文档编号:28302324 上传时间:2023-07-10 格式:DOCX 页数:14 大小:102.52KB
下载 相关 举报
III Linux系统编程 33 信号 4 捕捉信号.docx_第1页
第1页 / 共14页
III Linux系统编程 33 信号 4 捕捉信号.docx_第2页
第2页 / 共14页
III Linux系统编程 33 信号 4 捕捉信号.docx_第3页
第3页 / 共14页
III Linux系统编程 33 信号 4 捕捉信号.docx_第4页
第4页 / 共14页
III Linux系统编程 33 信号 4 捕捉信号.docx_第5页
第5页 / 共14页
点击查看更多>>
下载资源
资源描述

III Linux系统编程 33 信号 4 捕捉信号.docx

《III Linux系统编程 33 信号 4 捕捉信号.docx》由会员分享,可在线阅读,更多相关《III Linux系统编程 33 信号 4 捕捉信号.docx(14页珍藏版)》请在冰豆网上搜索。

III Linux系统编程 33 信号 4 捕捉信号.docx

IIILinux系统编程33信号4捕捉信号

第 33 章 信号

4. 捕捉信号

4.1. 内核如何实现信号的捕捉

如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。

由于信号处理函数的代码是在用户空间的,处理过程比较复杂,举例如下:

1.用户程序注册了SIGQUIT信号的处理函数sighandler。

2.当前正在执行main函数,这时发生中断或异常切换到内核态。

3.在中断处理完毕后要返回用户态的main函数之前检查到有信号SIGQUIT递达。

4.内核决定返回用户态后不是恢复main函数的上下文继续执行,而是执行sighandler函数,sighandler和main函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是两个独立的控制流程。

5.sighandler函数返回后自动执行特殊的系统调用sigreturn再次进入内核态。

6.如果没有新的信号要递达,这次再返回用户态就是恢复main函数的上下文继续执行了。

图 33.2. 信号的捕捉

上图出自[ULK]。

4.2. sigaction

#include

intsigaction(intsigno,conststructsigaction*act,structsigaction*oact);

sigaction函数可以读取和修改与指定信号相关联的处理动作。

调用成功则返回0,出错则返回-1。

signo是指定信号的编号。

若act指针非空,则根据act修改该信号的处理动作。

若oact指针非空,则通过oact传出该信号原来的处理动作。

act和oact指向sigaction结构体:

structsigaction{

void(*sa_handler)(int);/*addrofsignalhandler,*/

/*orSIG_IGN,orSIG_DFL*/

sigset_tsa_mask;/*additionalsignalstoblock*/

intsa_flags;/*signaloptions,Figure10.16*/

/*alternatehandler*/

void(*sa_sigaction)(int,siginfo_t*,void*);

};

将sa_handler赋值为常数SIG_IGN传给sigaction表示忽略信号,赋值为常数SIG_DFL表示执行系统默认动作,赋值为一个函数指针表示用自定义函数捕捉信号,或者说向内核注册了一个信号处理函数,该函数返回值为void,可以带一个int参数,通过参数可以得知当前信号的编号,这样就可以用同一个函数处理多种信号。

显然,这也是一个回调函数,不是被main函数调用,而是被系统所调用。

当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生,那么它会被阻塞到当前处理结束为止。

如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用sa_mask字段说明这些需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字。

sa_flags字段包含一些选项,本章的代码都把sa_flags设为0,sa_sigaction是实时信号的处理函数,本章不详细解释这两个字段,有兴趣的读者参考[APUE2e]。

4.3. pause

#include

intpause(void);

pause函数使调用进程挂起直到有信号递达。

如果信号的处理动作是终止进程,则进程终止,pause函数没有机会返回;如果信号的处理动作是忽略,则进程继续处于挂起状态,pause不返回;如果信号的处理动作是捕捉,则调用了信号处理函数之后pause返回-1,errno设置为EINTR,所以pause只有出错的返回值(想想以前还学过什么函数只有出错返回值?

)。

错误码EINTR表示“被信号中断”。

下面我们用alarm和pause实现sleep(3)函数,称为mysleep。

例 33.2. mysleep

#include

#include

#include

voidsig_alrm(intsigno)

{

/*nothingtodo*/

}

unsignedintmysleep(unsignedintnsecs)

{

structsigactionnewact,oldact;

unsignedintunslept;

newact.sa_handler=sig_alrm;

sigemptyset(&newact.sa_mask);

newact.sa_flags=0;

sigaction(SIGALRM,&newact,&oldact);

alarm(nsecs);

pause();

unslept=alarm(0);

sigaction(SIGALRM,&oldact,NULL);

returnunslept;

}

intmain(void)

{

while

(1){

mysleep

(2);

printf("Twosecondspassed\n");

}

return0;

}

1.main函数调用mysleep函数,后者调用sigaction注册了SIGALRM信号的处理函数sig_alrm。

2.调用alarm(nsecs)设定闹钟。

3.调用pause等待,内核切换到别的进程运行。

4.nsecs秒之后,闹钟超时,内核发SIGALRM给这个进程。

5.从内核态返回这个进程的用户态之前处理未决信号,发现有SIGALRM信号,其处理函数是sig_alrm。

6.切换到用户态执行sig_alrm函数,进入sig_alrm函数时SIGALRM信号被自动屏蔽,从sig_alrm函数返回时SIGALRM信号自动解除屏蔽。

然后自动执行系统调用sigreturn再次进入内核,再返回用户态继续执行进程的主控制流程(main函数调用的mysleep函数)。

7.pause函数返回-1,然后调用alarm(0)取消闹钟,调用sigaction恢复SIGALRM信号以前的处理动作。

以下问题留给读者思考:

1、信号处理函数sig_alrm什么都没干,为什么还要注册它作为SIGALRM的处理函数?

不注册信号处理函数可以吗?

2、为什么在mysleep函数返回前要恢复SIGALRM信号原来的sigaction?

3、mysleep函数的返回值表示什么含义?

什么情况下返回非0值?

4.4. 可重入函数

当捕捉到信号时,不论进程的主控制流程当前执行到哪儿,都会先跳到信号处理函数中执行,从信号处理函数返回后再继续执行主控制流程。

信号处理函数是一个单独的控制流程,因为它和主控制流程是异步的,二者不存在调用和被调用的关系,并且使用不同的堆栈空间。

引入了信号处理函数使得一个进程具有多个控制流程,如果这些控制流程访问相同的全局资源(全局变量、硬件资源等),就有可能出现冲突,如下面的例子所示。

图 33.3. 不可重入函数

main函数调用insert函数向一个链表head中插入节点node1,插入操作分为两步,刚做完第一步的时候,因为硬件中断使进程切换到内核,再次回用户态之前检查到有信号待处理,于是切换到sighandler函数,sighandler也调用insert函数向同一个链表head中插入节点node2,插入操作的两步都做完之后从sighandler返回内核态,再次回到用户态就从main函数调用的insert函数中继续往下执行,先前做第一步之后被打断,现在继续做完第二步。

结果是,main函数和sighandler先后向链表中插入两个节点,而最后只有一个节点真正插入链表中了。

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

想一下,为什么两个不同的控制流程调用同一个函数,访问它的同一个局部变量或参数就不会造成错乱?

如果一个函数符合以下条件之一则是不可重入的:

∙调用了malloc或free,因为malloc也是用全局链表来管理堆的。

∙调用了标准I/O库函数。

标准I/O库的很多实现都以不可重入的方式使用全局数据结构。

SUS规定有些系统函数必须以线程安全的方式实现,这里就不列了,请参考[APUE2e]。

4.5. sig_atomic_t类型与volatile限定符

在上面的例子中,main和sighandler都调用insert函数则有可能出现链表的错乱,其根本原因在于,对全局链表的插入操作要分两步完成,不是一个原子操作,假如这两步操作必定会一起做完,中间不可能被打断,就不会出现错乱了。

下一节线程会讲到如何保证一个代码段以原子操作完成。

现在想一下,如果对全局数据的访问只有一行代码,是不是原子操作呢?

比如,main和sighandler都对一个全局变量赋值,会不会出现错乱呢?

比如下面的程序:

longlonga;

intmain(void)

{

a=5;

return0;

}

带调试信息编译,然后带源代码反汇编:

$gccmain.c-g

$objdump-dSa.out

其中main函数的指令中有:

a=5;

8048352:

c7055095040805movl$0x5,0x8049550

8048359:

000000

804835c:

c7055495040800movl$0x0,0x8049554

8048363:

000000

虽然C代码只有一行,但是在32位机上对一个64位的longlong变量赋值需要两条指令完成,因此不是原子操作。

同样地,读取这个变量到寄存器需要两个32位寄存器才放得下,也需要两条指令,不是原子操作。

请读者设想一种时序,main和sighandler都对这个变量a赋值,最后变量a的值发生错乱。

如果上述程序在64位机上编译执行,则有可能用一条指令完成赋值,因而是原子操作。

如果a是32位的int变量,在32位机上赋值是原子操作,在16位机上就不是。

如果在程序中需要使用一个变量,要保证对它的读写都是原子操作,应该采用什么类型呢?

为了解决这些平台相关的问题,C标准定义了一个类型sig_atomic_t,在不同平台的C语言库中取不同的类型,例如在32位机上定义sig_atomic_t为int类型。

在使用sig_atomic_t类型的变量时,还需要注意另一个问题。

看如下的例子:

#include

sig_atomic_ta=0;

intmain(void)

{

/*registerasighandler*/

while(!

a);/*waituntilachangesinsighandler*/

/*dosomethingaftersignalarrives*/

return0;

}

为了简洁,这里只写了一个代码框架来说明问题。

在main函数中首先要注册某个信号的处理函数sighandler,然后在一个while死循环中等待信号发生,如果有信号递达则执行sighandler,在sighandler中将a改为1,这样再次回到main函数时就可以退出while循环,执行后续处理。

用上面的方法编译和反汇编这个程序,在main函数的指令中有:

/*registerasighandler*/

while(!

a);/*waituntilachangesinsighandler*/

8048352:

a13c950408mov0x804953c,%eax

8048357:

85c0test%eax,%eax

8048359:

74f7je8048352

将全局变量a从内存读到eax寄存器,对eax和eax做AND运算,若结果为0则跳回循环开头,再次从内存读变量a的值,可见这三条指令等价于C代码的while(!

a);循环。

如果在编译时加了优化选项,例如:

$gccmain.c-O1-g

$objdump-dSa.out

则main函数的指令中有:

8048352:

833d3c95040800cmpl$0x0,0x804953c

/*registerasighandler*/

while(!

a);/*waituntilachangesinsighandler*/

8048359:

74feje8048359

第一条指令将全局变量a的内存单元直接和0比较,如果相等,则第二条指令成了一个死循环,注意,这是一个真正的死循环:

即使sighandler将a改为1,只要没有影响Zero标志位,回到main函数后仍然死在第二条指令上,因为不会再次从内存读取变量a的值。

是编译器优化得有错误吗?

不是的。

设想一下,如果程序只有单一的执行流程,只要当前执行流程没有改变a的值,a的值就没有理由会变,不需要反复从内存读取,因此上面的两条指令和while(!

a);循环是等价的,并且优化之后省去了每次循环读内存的操作,效率非常高。

所以不能说编译器做错了,只能说编译器无法识别程序中存在多个执行流程。

之所以程序中存在多个执行流程,是因为调用了特定平台上的特定库函数,比如sigaction、pthread_create,这些不是C语言本身的规范,不归编译器管,程序员应该自己处理这些问题。

C语言提供了volatile限定符,如果将上述变量定义为volatilesig_atomic_ta=0;那么即使指定了优化选项,编译器也不会优化掉对变量a内存单元的读写。

对于程序中存在多个执行流程访问同一全局变量的情况,volatile限定符是必要的,此外,虽然程序只有单一的执行流程,但是变量属于以下情况之一的,也需要volatile限定:

∙变量的内存单元中的数据不需要写操作就可以自己发生变化,每次读上来的值都可能不一样

∙即使多次向变量的内存单元中写数据,只写不读,也并不是在做无用功,而是有特殊意义的

什么样的内存单元会具有这样的特性呢?

肯定不是普通的内存,而是映射到内存地址空间的硬件寄存器,例如串口的接收寄存器属于上述第一种情况,而发送寄存器属于上述第二种情况。

sig_atomic_t类型的变量应该总是加上volatile限定符,因为要使用sig_atomic_t类型的理由也正是要加volatile限定符的理由。

4.6. 竞态条件与sigsuspend函数

现在重新审视例 33.2“mysleep”,设想这样的时序:

1.注册SIGALRM信号的处理函数。

2.调用alarm(nsecs)设定闹钟。

3.内核调度优先级更高的进程取代当前进程执行,并且优先级更高的进程有很多个,每个都要执行很长时间

4.nsecs秒钟之后闹钟超时了,内核发送SIGALRM信号给这个进程,处于未决状态。

5.优先级更高的进程执行完了,内核要调度回这个进程执行。

SIGALRM信号递达,执行处理函数sig_alrm之后再次进入内核。

6.返回这个进程的主控制流程,alarm(nsecs)返回,调用pause()挂起等待。

7.可是SIGALRM信号已经处理完了,还等待什么呢?

出现这个问题的根本原因是系统运行的时序(Timing)并不像我们写程序时所设想的那样。

虽然alarm(nsecs)紧接着的下一行就是pause(),但是无法保证pause()一定会在调用alarm(nsecs)之后的nsecs秒之内被调用。

由于异步事件在任何时候都有可能发生(这里的异步事件指出现更高优先级的进程),如果我们写程序时考虑不周密,就可能由于时序问题而导致错误,这叫做竞态条件(RaceCondition)。

如何解决上述问题呢?

读者可能会想到,在调用pause之前屏蔽SIGALRM信号使它不能提前递达就可以了。

看看以下方法可行吗?

1.屏蔽SIGALRM信号;

2.alarm(nsecs);

3.解除对SIGALRM信号的屏蔽;

4.pause();

从解除信号屏蔽到调用pause之间存在间隙,SIGALRM仍有可能在这个间隙递达。

要消除这个间隙,我们把解除屏蔽移到pause后面可以吗?

1.屏蔽SIGALRM信号;

2.alarm(nsecs);

3.pause();

4.解除对SIGALRM信号的屏蔽;

这样更不行了,还没有解除屏蔽就调用pause,pause根本不可能等到SIGALRM信号。

要是“解除信号屏蔽”和“挂起等待信号”这两步能合并成一个原子操作就好了,这正是sigsuspend函数的功能。

sigsuspend包含了pause的挂起等待功能,同时解决了竞态条件的问题,在对时序要求严格的场合下都应该调用sigsuspend而不是pause。

#include

intsigsuspend(constsigset_t*sigmask);

和pause一样,sigsuspend没有成功返回值,只有执行了一个信号处理函数之后sigsuspend才返回,返回值为-1,errno设置为EINTR。

调用sigsuspend时,进程的信号屏蔽字由sigmask参数指定,可以通过指定sigmask来临时解除对某个信号的屏蔽,然后挂起等待,当sigsuspend返回时,进程的信号屏蔽字恢复为原来的值,如果原来对该信号是屏蔽的,从sigsuspend返回后仍然是屏蔽的。

以下用sigsuspend重新实现mysleep函数:

unsignedintmysleep(unsignedintnsecs)

{

structsigactionnewact,oldact;

sigset_tnewmask,oldmask,suspmask;

unsignedintunslept;

/*setourhandler,savepreviousinformation*/

newact.sa_handler=sig_alrm;

sigemptyset(&newact.sa_mask);

newact.sa_flags=0;

sigaction(SIGALRM,&newact,&oldact);

/*blockSIGALRMandsavecurrentsignalmask*/

sigemptyset(&newmask);

sigaddset(&newmask,SIGALRM);

sigprocmask(SIG_BLOCK,&newmask,&oldmask);

alarm(nsecs);

suspmask=oldmask;

sigdelset(&suspmask,SIGALRM);/*makesureSIGALRMisn'tblocked*/

sigsuspend(&suspmask);/*waitforanysignaltobecaught*/

/*somesignalhasbeencaught,SIGALRMisnowblocked*/

unslept=alarm(0);

sigaction(SIGALRM,&oldact,NULL);/*resetpreviousaction*/

/*resetsignalmask,whichunblocksSIGALRM*/

sigprocmask(SIG_SETMASK,&oldmask,NULL);

return(unslept);

}

如果在调用mysleep函数时SIGALRM信号没有屏蔽:

1.调用sigprocmask(SIG_BLOCK,&newmask,&oldmask);时屏蔽SIGALRM。

2.调用sigsuspend(&suspmask);时解除对SIGALRM的屏蔽,然后挂起等待待。

3.SIGALRM递达后suspend返回,自动恢复原来的屏蔽字,也就是再次屏蔽SIGALRM。

4.调用sigprocmask(SIG_SETMASK,&oldmask,NULL);时再次解除对SIGALRM的屏蔽。

4.7. 关于SIGCHLD信号

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

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

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

请编写一个程序完成以下功能:

父进程fork出子进程,子进程调用exit

(2)终止,父进程自定义SIGCHLD信号的处理函数,在其中调用wait获得子进程的退出状态并打印。

事实上,由于UNIX的历史原因,要想不产生僵尸进程还有另外一种办法:

父进程调用sigaction将SIGCHLD的处理动作置为SIG_IGN,这样fork出来的子进程在终止时会自动清理掉,不会产生僵尸进程,也不会通知父进程。

系统默认的忽略动作和用户用sigaction函数自定义的忽略通常是没有区别的,但这是一个特例。

此方法对于Linux可用,但不保证在其它UNIX系统上都可用。

请编写程序验证这样做不会产生僵尸进程。

展开阅读全文
相关资源
猜你喜欢
相关搜索
资源标签

当前位置:首页 > 初中教育 > 语文

copyright@ 2008-2022 冰豆网网站版权所有

经营许可证编号:鄂ICP备2022015515号-1