您的位置:首页 > 其它

信号

2016-05-06 23:33 246 查看
一.基础知识
信号产生的条件
a. 终端按键产生。如:ctrl+c(SIGINT信号),ctrl+\(SIGQUIT信号),ctrl+z(SIGTSTP信号)......
b. 系统命令和函数。如:kill(2)函数,raise函数,abort函数(SIGABRT信号)(就像exit函数一样,abort函数总是会成功的,所以没有返回值)。
c.软硬件产生。如:当前进程执行了除以0的指令,CPU的运算单元会产生异常,内核将这个异常解释 为SIGFPE信号发送给进程;闹钟超时产生SIGALRM信号;向读端已关闭的管道写数据时产生SIGPIPE信号; 若不想按默认动作处理信号,可调用sigaction(2)函数告诉内核如何处理某种信号......

信号处理动作
a.忽略
b.执行默认动作
c.自定义动作(捕捉信号)

阻塞信号
a.信号递达(Delivery):实际执行信号的处理动作。
b.信号未决(Pending):信号从产生到递达之间的状态。
c.信号阻塞: 进程可以选择阻塞(Block)某个信号。被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作。

注意:阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略表示信号已经递达,因为忽略是在递达之后可选的一种处理动作。

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




(1). SIGHUP信号未阻塞也未产生过,当它递达时执行默认处理动作。
(2). SIGINT信号产生过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略,但在没有解除 阻塞之前不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞。
(3). SIGQUIT信号未产生过,一旦产生SIGQUIT信号将被阻塞,它的处理动作是用户自定义函数sighandler。

在进程解决对某信号的阻塞状态前,信号可能过多次,Linux是这样实现的:常规信号是采用只记录一次,而实时信号将这些信号保存在一个队列中。

问:如果在进程解除对某信号的阻塞之前这种信号产生过多次,如何处理?
(1)POSIX.1允许系统递送该信号一次或多次。
(2)Linux是这样实现的:常规信号在递达之前产生多次只计一次,而实时信号在递达之前产生多次可以依次放在一个队列里。从上图来看,每个信号只有一 个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。未决和阻塞标志可以用sigset_t(信号集)数据类型来存储。

5.函数









1)sigemptyset初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含任何 有效信号。 2)sigfillset初始化set所指向的信号集。 在使用sigset_t类型的变量之前,一定要调用sigemptyset或sigfillset做初始化,使信号集处于确定 的状态。 3)sigaddset和sigdelset在该信号集中添加或删除某种有效信号。 以上4个函数成功返回0,出错返回-1。 4)sigismember是一个布尔函数,用于判断一个信号集的有效信号中是否包含某种信号,若包含则返 回1,不包含则返回0,出错返回-1。 5)sigprocmask可以读取或更改进程的信号屏蔽字(阻塞信号集)。成功返回0,出错回-1。 6)sigpending读取当前进程的未决信号集。调用成功则返回0,出错则返回-1。

6.代码验证
1 #include<stdio.h>
2 #include<signal.h>
3 void printsigset(sigset_t* set)
4 {
5     int i=1;
6     for(;i<32;i++)
7     {
8         if(sigismember(set,i))
9         {
10             putchar('1');
11         }
12         else
13         {
14             putchar('0');
15         }
16     }
17     printf("\n");     //效果等价于puts("");      puts()函数用来向标准输出设备(屏幕)写字符串并换行。
18 }
19 int main()
20 {
21     sigset_t s,p;
22     sigemptyset(&s);
23     sigaddset(&s,SIGINT);
24     sigprocmask(SIG_BLOCK,&s,NULL);
25     while(1)
26     {
27         sigpending(&p);
28         printsigset(&p);
29         sleep(1);
30     }
31     return 0;
32 }
输出结果:



结果分析:
每秒钟把各信号的未决状态打印一遍,由于我们阻塞了SIGINT信号,按Ctrl-C将会使SIGINT信号处于未决状态,按Ctrl-\,因为SIGQUIT信号没有阻塞,仍然可以终止程序。

二.捕捉
1.内核如何实现信号的捕捉
如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。由于信号处理函数的代码是在用户空间的,处理过程比较复杂,举例如下:
(1). 用户程序注册了SIGQUIT信号的处理函数sighandler。
(2). 当前正在执行main函数,这时发生中断或异常切换到内核态。
(3). 在中断处理完毕后要返回用户态的main函数之前检查到有信号SIGQUIT递达。
(4). 内核决定返回用户态后不是恢复main函数的上下文继续执行,而是执行sighandler数,sighandler和main函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是两个独立的控制流程。
(5). sighandler函数返回后自动执行特殊的系统调用sigreturn再次进入内核态。
(6). 如果没有新的信号要递达,这次再返回用户态就是恢复main函数的上下文继续执行了。
就像是倒写的8,如图




2.函数






(1)a.sigaction函数可以读取和修改与指定信号相关联的处理动作;
b.成功返回0,出错返回- 1。
(2)a.signum是指定信号的编号;
b.若act指针非空,则根据act修改该信号的处理动作;
c.若oldact指针非空,则通过oldact传出该信号原来的处理动作;
d.act和oldact指向sigaction结构体。
(3)a.将sa_handler赋值为常数SIG_IGN传给sigaction表示忽略信号;
b.赋值为常数SIG_DFL表示执行系统默认动作;
c.赋值为一个函数指针表示用自定义函数捕捉信号,或者说向内核注册了一个信号处理函数。(该函数返回值为void,可以带一个int参数,通过参数可以得知当前信号的编号,这样就可以用同一个函数处理多种信号。显然,这也是一个回调函数,不是被main函数调用,而是被系统所调用。)



(1)pause函数使调用进程挂起直到有信号递达。
(2)如果信号的处理动作是终止进程,则进程止,pause函数没有机会返回;
(3)如果信号的处理动作是忽略,则进程继续处于挂起状态,pause不返回;
(4)如果信号的处理动作是捕捉,则调用了信号处理函数之后pause返回-1,errno设置为EINTR, 所以pause只有出错的返回值。(错误码EINTR表示“被信号中断”)。

3.代码实现
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<signal.h>
4
5 void sig_alrm(int signum)
6 {}
7
8 unsigned int mysleep(unsigned int nsecs)
9 {
10     struct sigaction new,old;
11     unsigned int unslept=0;
12     new.sa_handler=sig_alrm;
13     sigemptyset(&new.sa_mask);
14     new.sa_flags=0;
15
16     sigaction(SIGALRM,&new,&old);
17     alarm(nsecs);
18     pause();
19     unslept=alarm(0);
20     sigaction(SIGALRM,&old,NULL);
21     return unslept;
22 }
23 int main()
24 {
25     while(1)
26     {
27         mysleep(3);
28         printf("3 seconds passed!\n");
29     }
30     return 0;
31 }
输出结果:



结果分析:
(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_atomic_t类型与volatile限定符
(1)sig_atomic_t类型
例:对全局数据的访问只有一行代码,是不是原子操作呢?查看汇编



虽然C代码只有一行,但是在32位机上对一个64位的long long变量赋值需要两条指令完成,因此不是原子操作。同样地,读取这个变量到寄存器需要两个32位寄存器才放得下,也需要两条指令,不是原子操作。若main和sighandler都对这个变量a赋值,最后变量a的值可能会发生错乱。

如果在程序中需要使用一个变量,要保证对它的读写都是原子操作,C标准定义了一个类型sig_atomic_t,在不同平台的C语言库中取不同的类型,例如在32位机 上定义sig_atomic_t为int类型。

(2)sig_atomic_t类型 -> volatile限定符



在main函数中首先要注册某个信号的处理函 数sighandler,然后在一个while死循环中等待信号发生,如果有信号递达则执行sighandler, 在sighandler中将a改为1,这样再次回到main函数时就可以退出while循环,执行后续处理。用上面的方法编译和反汇编这个程序,在main函数的指令中有:



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



优化之后省去了每次循环读内存的操作。第一条指令将全局变量a的内存单元直接和0比较,如果相等,则第二条指令成了一个死循环,注意,这是一个真正的死循环:即使sighandler将a改为1,只要没有影响Zero标志位,回到main函数后仍然死在第二条指令上,因为不会再次从内存读取变量a的值。

编译器无法识别程序中存在多个执行流程。之所以程序中存在多个执行流程,是因为调用了特定平台上的特定库函数,比如sigaction、pthread_create,这些不是C语言本 身的规范,不归编译器管,程序员应该自己处理这些问题。C语言提供了volatile限定符,如果将 上述变量定义为volatile sig_atomic_t a=0;那么即使指定了优化选项,编译器也不会优化掉对变 量a内存单元的读写。

(3)必须使用volatile限定符
a.程序中存在多个执行流程访问同一全局变量
b.变量的内存单元中的数据不需要写操作就可以自己发生变化,每次读上来的值都可能不一样
c.即使多次向变量的内存单元中写数据,只写不读,也并不是在做无用功,而是有特殊意义的什么样的内存单元会具有这样的特性呢?肯定不是普通的内存,而是映射到内存地址空间的硬件寄存器,例如串口的接收寄存器属于上述第一种情况,而发送寄存器属于上述第二种情况。sig_atomic_t类型的变量应该总是加上volatile限定符,因为要使用sig_atomic_t类型的理由也正 是要加volatile限定符的理由。

2.竞态条件
(1)竞态条件(Race Condition): 如果我们写程序时考虑不周密,就可能由于时序问题而导致错误。
(2)sigsuspend:包含了pause的挂起等待功能,同时解决了竞态条件的问题,在对时序要求严格的场合下,应调用sigsuspend而不是pause。



a.sigsuspend没有成功返回值,只有执行了一个信号处理函数之后sigsuspend才返回,返回值为-1,errno设置为EINTR。
b.调用sigsuspend时,进程的信号屏蔽字由sigmask参数指定,可以通过指定sigmask来临时解除 对某 个信号的屏蔽,然后挂起等待,当sigsuspend返回时,进程的信号屏蔽字恢复为原来的值,如果原来对该信号是屏蔽的,从sigsuspend返回后仍然是屏蔽的。

3.代码实现(用sigsuspend重新实现my_sleep函数)
1 #include<stdio.h>
2 #include<signal.h>
3 #include<unistd.h>
4 void sig_alrm(int signum)
5 {}
6 unsigned int my_sleep(unsigned int nsecs)
7 {
8     struct sigaction new,old;
9     sigset_t newmask,oldmask,suspmask;
10     unsigned int unslept;
11
12     new.sa_handler=sig_alrm;
13     sigemptyset(&new.sa_mask);
14     new.sa_flags=0;
15     sigaction(SIGALRM,&new,&old);
16
17     sigemptyset(&newmask);
18     sigaddset(&newmask,SIGALRM);
19     sigprocmask(SIG_BLOCK,&newmask,&oldmask);
20
21     alarm(nsecs);
22
23     suspmask=oldmask;
24     sigsuspend(&suspmask);
25
26     unslept=alarm(0);
27     sigaction(SIGALRM,&old,NULL);
28
29     sigprocmask(SIG_SETMASK,&oldmask,NULL);
30     return unslept;
31 }
32 int main()
33 {
34     my_sleep(3);
35     printf("3 seconds passed!\n");
36     return 0;
37 }
输出结果:





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

3.可重入函数(Reentrant)
(1)重入:函数被不同的控制流程调用,有可能在第一次调用还没返回时就再次进入该函数,这称为重入。
(2)定义:函数访问一个全局链表,有可能因为重入而造成错乱,像这样的函数称为不可重入函数,反之,如果一个函数只访问自己的局部变量或参数,则称为可重入函数。
(3)如果一个函数符合以下条件之一则是不可重入的:
调用了malloc或free,因为malloc也是用全局链表来管理堆的。

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

SUS规定有些系统函数必须以线程安全的方式实现
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  管道 如何