Linux异步之信号(signal)机制分析

From:http://www.cnblogs.com/hoys/archive/2012/08/19/2646377.html

From:http://kenby.iteye.com/blog/1173862

Linux下的信号详解及捕捉信号:http://www.jb51.net/article/90695.htm

linux信号详解:http://blog.csdn.net/fanyun_01/article/details/52704408

Linux环境进程间通信(二) 信号(上):https://www.ibm.com/developerworks/cn/linux/l-ipc/part2/index1.html

嵌入式 最全面的linux信号量解析:http://blog.csdn.net/skdkjzz/article/details/38444999



【摘要】本文分析了Linux内核对于信号的实现机制和应用层的相关处理。首先介绍了软中断信号的本质及信号的两种不同分类方法尤其是不可靠信号的原理。接着分析了内核对于信号的处理流程包括信号的触发/注册/执行及注销等。最后介绍了应用层的相关处理,主要包括信号处理函数的安装、信号的发送、屏蔽阻塞等,最后给了几个简单的应用实例。

【关键字】软中断信号,signal,sigaction,kill,sigqueue,settimer,sigmask,sigprocmask,sigset_t



1. Linux信号介绍


信号机制是进程之间相互传递消息的一种方法,信号全称为软中断信号,也有人称作软中断。从它的命名可以看出,它的实质和使用很象中断。所以,信号可以说是进程控制的一部分

软中断(signal,又简称为信号)用来通知进程发生了异步事件。进程之间可以互相通过系统调用kill发送软中断信号。内核也可以因为内部事件而给进程发送信号,通知进程发生了某个事件。注意,信号只是用来通知某进程发生了什么事件,并不给该进程传递任何数据。 在软件层次上,软中断信号 是对中断机制的一种模拟,一个进程接收到信号之后,有相应的信号的处理程序,而一个进程也可以给另外一个(或一组)进程发送信号。在原理上,一个进程收到一个信号与处理器收到一个中断请求可以说是一样的。信号是进程间通信机制中唯一的异步通信机制一个进程不必通过任何操作来等待信号的到达,事实上,进程也不知道信号到底什么时候到达。进程之间可以互相通过系统调用kill发送软中断信号。内核也可以因为内部事件而给进程发送信号,通知进程发生了某个事件。信号机制除了基本通知功能外,还可以传递附加信息。

收到信号的进程,对各种信号有不同的处理方法。处理方法可以分为三类

(参见:Unix环境高级编程(APUE) 信号 章节。电子书下载地址:http://download.csdn.net/download/freeking101/10012610):

  1. 第一种:捕捉信号。类似中断的处理程序,对于需要处理的信号,进程可以指定处理函数,由该函数来处理。
  2. 第二种:忽略。忽略某个信号,对该信号不做任何处理,就象未发生过一样。
  3. 第三种:执行该信号的默认处理动作。对该信号的处理保留系统的默认值,这种缺省操作,对大部分的信号的缺省操作是使得进程终止。

进程通过系统调用signal来指定进程对某个信号的处理行为。

在进程表的表项中有一个软中断信号域,该域中每一位对应一个信号,当有信号发送给进程时,对应位置位。由此可以看出,进程对不同的信号可以同时保留,但对于同一个信号,进程并不知道在处理之前来过多少个。

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

进程收到一个信号后不会被立即处理,而是在恰当时机进行处理!即内核态返回用户态之前 !

但是由于信号处理函数的代码在用户空间,所以这增加了内核处理信号捕捉的复杂度。



2. 信号的种类


可以从两个不同的分类角度对信号进行分类:

  1. 可靠性方面:可靠信号与不可靠信号;
  2. 与时间的关系上:实时信号与非实时信号。


可靠信号与不可靠信号

        Linux信号机制基本上是从Unix系统中继承过来的。早期Unix系统中的信号机制比较简单和原始,信号值小于SIGRTMIN的信号都是不可靠信号。这就是"不可靠信号"的来源。它的主要问题是信号可能丢失。

        随着时间的发展,实践证明了有必要对信号的原始机制加以改进和扩充。由于原来定义的信号已有许多应用,不好再做改动,最终只好又新增加了一些信号,并在一开始就把它们定义为可靠信号,这些信号支持排队,不会丢失。

        信号值位于SIGRTMIN和SIGRTMAX之间的信号都是可靠信号,可靠信号克服了信号可能丢失的问题。Linux在支持新版本的信号安装函数sigation()以及信号发送函数sigqueue()的同时,仍然支持早期的signal()信号安装函数,支持信号发送函数kill()。

        信号的可靠与不可靠只与信号值有关,与信号的发送及安装函数无关。目前linux中的signal()是通过sigation()函数实现的,因此,即使通过signal()安装的信号,在信号处理函数的结尾也不必再调用一次信号安装函数。同时,由signal()安装的实时信号支持排队,同样不会丢失。

        对于目前linux的两个信号安装函数:signal()及sigaction()来说,它们都不能把SIGRTMIN以前的信号变成可靠信号(都不支持排队,仍有可能丢失,仍然是不可靠信号),而且对SIGRTMIN以后的信号都支持排队。这两个函数的最大区别在于,经过sigaction安装的信号都能传递信息给信号处理函数,而经过signal安装的信号不能向信号处理函数传递信息。对于信号发送函数来说也是一样的。

可靠信号与不可靠信号的区别:

  • 这里的不可靠主要是不支持信号队列,就是当多个信号发生在进程中的时候(收到信号的速度超过进程处理的速度的时候),这些没来的及处理的信号就会被丢掉,仅仅留下一个信号。
  • 可靠信号是多个信号发送到进程的时候(收到信号的速度超过进程处理信号的速度的时候),这些没来的及处理的信号就会排入进程的队列。等进程有机会来处理的时候,依次再处理,信号不丢失。

实时信号与非实时信号

早期Unix系统只定义了32种信号,前32种信号已经有了预定义值,每个信号有了确定的用途及含义,并且每种信号都有各自的缺省动作。如按键盘的CTRL ^C时,会产生SIGINT信号,对该信号的默认反应就是进程终止。后32个信号表示实时信号,等同于前面阐述的可靠信号。这保证了发送的多个实时信号都被接收。

非实时信号都不支持排队,都是不可靠信号;实时信号都支持排队,都是可靠信号。


使用kill -l可以查看信号列表。可以看到Linux中系统一共支持64种信号。

1-31是普通信号( 用于内核向进程通知事件。即传统或者标准信号,也称为不可靠信号); 34-64是实时信号(可靠信号)。

0 号信号用来测试对应进程是否存在或者是否由权限给其发送信号


平时可以接触的是普通信号,其各个信号的含义如下:

  1.  SIGHUP :当用户退出Shell时,由该Shell启的发所有进程都退接收到这个信号,默认动作为终止进程。
  2.  SIGINT :用户按下组合键时,用户端时向正在运行中的由该终端启动的程序发出此信号。默认动作为终止进程。
  3.  SIGQUIT :当用户按下组合键时产生该信号,用户终端向正在运行中的由该终端启动的程序发出此信号。默认动作为终止进程并产生core文件。
  4.  SIGILL  :CPU检测到某进程执行了非法指令。默认动作为终止进程并产生core文件。
  5. SIGTRAP:该信号由断点指令或其他trap指令产生。默认动作为终止进程并产生core文件。
  6.  SIGABRT :调用abort函数时产生该信号。默认动作为终止进程并产生core文件。
  7. SIGBUS:非法访问内存地址,包括内存地址对齐(alignment)出错,默认动作为终止进程并产生core文件。
  8. SIGFPE:在发生致命的算术错误时产生。不仅包括浮点运行错误,还包括溢出及除数为0等所有的算术错误。默认动作为终止进程并产生core文件。
  9.  SIGKILL :无条件终止进程。本信号不能被忽略、处理和阻塞。默认动作为终止进程。它向系统管理员提供了一种可以杀死任何进程的方法。
  10. SIGUSR1:用户定义的信号,即程序可以在程序中定义并使用该信号。默认动作为终止进程。
  11. SIGSEGV:指示进程进行了无效的内存访问。默认动作为终止进程并使用该信号。默认动作为终止进程。
  12. SIGUSR2:这是另外一个用户定义信号,程序员可以在程序中定义并使用该信号。默认动作为终止进程。
  13.  SIGPIPE :Broken pipe:向一个没有读端的管道写数据。默认动作为终止进程。
  14.  SIGALRM :定时器超时,超时的时间由系统调用alarm设置。默认动作为终止进程。
  15.  SIGTERM :程序结束(terminate)信号,与SIGKILL不同的是,该信号可以被阻塞和处理。通常用来要求程序正常退出。执行Shell命令kill时,缺少产生这个信号。默认动作为终止进程。
  16.  SIGCHLD :子程序结束时,父进程会收到这个信号。默认动作为忽略该信号。
  17. SIGCONT:让一个暂停的进程继续执行。
  18. SIGSTOP:停止(stopped)进程的执行。注意它和SIGTERM以及SIGINT的区别:该进程还未结束,只是暂停执行。本信号不能被忽略、处理和阻塞。默认作为暂停进程。
  19. SIGTSTP:停止进程的动作,但该信号可以被处理和忽略。按下组合键时发出该信号。默认动作为暂停进程。
  20. SIGTTIN:当后台进程要从用户终端读数据时,该终端中的所有进程会收到SIGTTIN信号。默认动作为暂停进程。
  21. SIGTTOU:该信号类似于SIGTIN,在后台进程要向终端输出数据时产生。默认动作为暂停进程。
  22.  SIGURG :套接字(socket)上有紧急数据时,向当前正在运行的进程发出此信号,报告有紧急数据到达。默认动作为忽略该信号。
  23. SIGXCPU:进程执行时间超过了分配给该进程的CPU时间,系统产生该信号并发送给该进程。默认动作为终止进程。
  24. SIGXFSZ:超过文件最大长度的限制。默认动作为yl终止进程并产生core文件。
  25. SIGVTALRM:虚拟时钟超时时产生该信号。类似于SIGALRM,但是它只计算该进程占有用的CPU时间。默认动作为终止进程。
  26. SIGPROF:类似于SIGVTALRM,它不仅包括该进程占用的CPU时间还抱括执行系统调用的时间。默认动作为终止进程。
  27. SIGWINCH:窗口大小改变时发出。默认动作为忽略该信号。
  28. SIGIO:此信号向进程指示发出一个异步IO事件。默认动作为忽略。
  29. SIGPWR:关机。默认动作为终止进程。

上面标注出来的信号是我们在学习过程中重点关注的信号,在Linux后期的学习中这些信号的身影将经常出现

信号实质上是软中断,中断有优先级,信号也有优先级。如果一个进程有多个未决信号,则对于同一个未决的实时信号,内核将按照发送的顺序来递送信号。如果存在多个未决信号,则值(或者说编号)越小的越先被递送。如果即存在不可靠信号,又存在可靠信号(实时信号),虽然POSIX对这一情况没有明确规定,但Linux系统和大多数遵循POSIX标准的操作系统一样,将优先递送不可靠信号。


Linux支持的信号列表如下。很多信号是与机器的体系结构相关的,首先列出的是POSIX.1中列出的信号:

信号 值 处理动作 发出信号的原因 
--------------------------------------------------------------------- 
SIGHUP 1 A 终端挂起或者控制进程终止 
SIGINT 2 A 键盘中断(如break键被按下) 
SIGQUIT 3 C 键盘的退出键被按下 
SIGILL 4 C 非法指令 
SIGABRT 6 C 由abort(3)发出的退出指令 
SIGFPE 8 C 浮点异常 
SIGKILL 9 AEF Kill信号 
SIGSEGV 11 C 无效的内存引用 
SIGPIPE 13 A 管道破裂: 写一个没有读端口的管道 
SIGALRM 14 A 由alarm(2)发出的信号 
SIGTERM 15 A 终止信号 
SIGUSR1 30,10,16 A 用户自定义信号1 
SIGUSR2 31,12,17 A 用户自定义信号2 
SIGCHLD 20,17,18 B 子进程结束信号 
SIGCONT 19,18,25 进程继续(曾被停止的进程) 
SIGSTOP 17,19,23 DEF 终止进程 
SIGTSTP 18,20,24 D 控制终端(tty)上按下停止键 
SIGTTIN 21,21,26 D 后台进程企图从控制终端读 
SIGTTOU 22,22,27 D 后台进程企图从控制终端写 

下面的信号没在POSIX.1中列出,而在SUSv2列出 

信号 值 处理动作 发出信号的原因 
-------------------------------------------------------------------- 
SIGBUS 10,7,10 C 总线错误(错误的内存访问) 
SIGPOLL A Sys V定义的Pollable事件,与SIGIO同义 
SIGPROF 27,27,29 A Profiling定时器到 
SIGSYS 12,-,12 C 无效的系统调用 (SVID) 
SIGTRAP 5 C 跟踪/断点捕获 
SIGURG 16,23,21 B Socket出现紧急条件(4.2 BSD) 
SIGVTALRM 26,26,28 A 实际时间报警时钟信号(4.2 BSD) 
SIGXCPU 24,24,30 C 超出设定的CPU时间限制(4.2 BSD) 
SIGXFSZ 25,25,31 C 超出设定的文件大小限制(4.2 BSD) 

(对于SIGSYS,SIGXCPU,SIGXFSZ,以及某些机器体系结构下的SIGBUS,Linux缺省的动作是A (terminate),SUSv2 是C (terminate and dump core))。 

下面是其它的一些信号 

信号 值 处理动作 发出信号的原因 
--------------------------------------------------------------------- 
SIGIOT 6 C IO捕获指令,与SIGABRT同义 
SIGEMT 7,-,7 
SIGSTKFLT -,16,- A 协处理器堆栈错误 
SIGIO 23,29,22 A 某I/O操作现在可以进行了(4.2 BSD) 
SIGCLD -,-,18 A 与SIGCHLD同义 
SIGPWR 29,30,19 A 电源故障(System V) 
SIGINFO 29,-,- A 与SIGPWR同义 
SIGLOST -,-,- A 文件锁丢失 
SIGWINCH 28,28,20 B 窗口大小改变(4.3 BSD, Sun) 
SIGUNUSED -,31,- A 未使用的信号(will be SIGSYS) 

(在这里,- 表示信号没有实现;有三个值给出的含义为,第一个值通常在Alpha和Sparc上有效,中间的值对应i386和ppc以及sh,最后一个值对应mips。信号29在Alpha上为SIGINFO / SIGPWR ,在Sparc上为SIGLOST。) 

处理动作一项中的字母含义如下 
A 缺省的动作是终止进程 
B 缺省的动作是忽略此信号 
C 缺省的动作是终止进程并进行内核映像转储(dump core) 
D 缺省的动作是停止进程 
E 信号不能被捕获 
F 信号不能被忽略 

上面介绍的信号是常见系统所支持的。以表格的形式介绍了各种信号的名称、作用及其在默认情况下的处理动作。各种默认处理动作的含义是:
终止程序是指进程退 出;忽略该信号是将该信号丢弃,不做处理;
停止程序是指程序挂起,进入停止状况以后还能重新进行下去,一般是在调试的过程中(例如ptrace系统调 用);
内核映像转储是指将进程数据在内存的映像和进程在内核结构中存储的部分内容以一定格式转储到文件系统,并且进程退出执行,这样做的好处是为程序员提供了方便,使得他们可以得到进程当时执行时的数据值,允许他们确定转储的原因,并且可以调试他们的程序。 

注意信号SIGKILL和SIGSTOP既不能被捕捉,也不能被忽略。信号SIGIOT与SIGABRT是一个信号。可以看出,同一个信号在不同的系统中值可能不一样,所以建议最好使用为信号定义的名字,而不要直接使用信号的值



3. 信号处理流程


用户进程提供的信号处理函数是在用户态里的,而我们发现信号,找到信号处理函数的时刻处于内核态中,所以我们需要从内核态跑到用户态去执行信号处理程序,执行完毕后还要返回内核态。这个过程如下图所示:


处理信号的整个过程是这样的进程由于 系统调用或者中断 进入内核,完成相应任务返回用户空间的前夕,检查信号队列,如果有信号,则根据信号向量表找到信号处理函数,设置好“堆栈”后,跳到用户态执行信号处理函数。信号处理函数执行完毕后,返回内核态,设置“堆栈”,再返回到用户态继续执行程序。


内核实现信号捕捉的步骤:

      1、用户为某信号注册一个信号处理函数sighandler

      2、当前正在执行主程序,这时候因为中断、异常或系统调用进入内核态。

      3、在处理完异常要返回用户态的主程序之前,检查到有信号未处理,并发现该信号需要按照用户自定义的函数来处理。

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

      5、sighandler函数返回后,执行特殊的系统调用sigreturn从用户态回到内核态

      6、检查是否还有其它信号需要递达,如果没有 则返回用户态并恢复主程序的上下文信息继续执行。



进程收到一个信号后不会被立即处理,而是在恰当 时机进行处理!

什么是适当的时候呢?比如说中断返回的时候,或者内核态返回用户态的时候(这个情况出现的比较多)。

信号不一定会被立即处理,操作系统不会为了处理一个信号而把当前正在运行的进程挂起(切换进程),挂起(进程切换)的话消耗太大了,如果不是紧急信号,是不会立即处理的。操作系统多选择在内核态切换回用户态的时候处理信号,这样就利用两者的切换来处理了(不用单独进行进程切换以免浪费时间)。
总归是不能避免的,因为很有可能在睡眠的进程就接收到信号,操作系统肯定不愿意切换当前正在运行的进程,于是就得把信号储存在进程唯一的PCB(task_struct)当中。

对于一个完整的信号生命周期(从信号发送到相应的处理函数执行完毕)来说,可以分为三个阶段

  1. 信号诞生
  2. 信号在进程中注册
  3. 信号的执行和注销

信号诞生

信号事件的发生有两个来源:硬件来源(比如我们按下了键盘或者其它硬件故障);软件来源,最常用发送信号的系统函数是kill, raise, alarm和setitimer以及sigqueue函数,软件来源还包括一些非法运算等操作。

产生信号的条件

1.用户在终端按下某些键时,终端驱动程序会发送信号给前台程序。

     例如:Ctrl-c产生SIGINT信号,Ctrl-\产生SIGQUIT信号,Ctrl-z产生SIGTSTP信号

2.硬件异常产生信号。

     这类信号由硬件检测到并通知内核,然后内核向当前进程发送适当的信号。

     例如:当前进程执行除以0的指令,CPU的运算单元会产生异常,内核将这个进程解释为SIGFPE信号发送给当前进程。
               当前进程访问了非法内存地址,MMU会产生异常,内核将这个异常解释为SIGSEGV信号发送给进程。

3.一个进程调用kill(2)函数可以发送信号给另一个进程。

     可以用kill(1)命令发送信号给某个进程,kill(1)命令也是调用kill(2)函数实现的,如果不明确指定信号则发送SIGTERM信号,该信号的默认处理动作是终止进程。


这里按发出信号的原因简单分类,以了解各种信号:

(1) 与进程终止相关的信号。当进程退出,或者子进程终止时,发出这类信号。

(2) 与进程例外事件相关的信号。如进程越界,或企图写一个只读的内存区域(如程序正文区),或执行一个特权指令及其他各种硬件错误。

(3) 与在系统调用期间遇到不可恢复条件相关的信号。如执行系统调用exec时,原有资源已经释放,而目前系统资源又已经耗尽。

(4) 与执行系统调用时遇到非预测错误条件相关的信号。如执行一个并不存在的系统调用。

(5) 在用户态下的进程发出的信号。如进程调用系统调用kill向其他进程发送信号

(6) 与终端交互相关的信号。如用户关闭一个终端,或按下break键等情况。

(7) 跟踪进程执行的信号

通过终端产生信号示例


        当CPU正在执行这个进程的代码 , 终端驱动程序发送了一 个 SIGINT 信号给该进程,记录在该进程的 PCB中,则该进程的用户空间代码暂停执行,CPU从用户态 切换到内核态处理硬件中断。

        从内核态回到用户态之前, 会先处理 PCB中记录的信号 ,发现有一个 SIGINT 信号待处理, 而这个信号的默认处理动作是终止进程,所以直接终止进程而不再返回它的用户空间代码执行。


调用系统函数向进程发信号


kill命令是调用kill函数实现的。kill函数可以给一个指定的进程发送指定信号

raise函数可 以给当前进程发送指定的信号 (自己给自己发信号 )

#include<signal.h>
int kill(pid_t pid,int signo);
int raise(int signo);

这两个函数都是成功返回0,错误返回-1.

除此之外,abort函数使当前进程接收到SIGABRT信号而异常终止

#include<stdlib.h>
void abort(void);

就像 exit函数一样 ,abort 函数总是会成功的 ,所以没有返回值

由软件条件产生信号


调用alarm函数可以设定一个闹钟,告诉内核在seconds秒之后给当前进程发SIGALRM信号, 该信号的默认处理动作是终止当前进程。

该程序会在1秒钟之内不停地数数,并打印计数器,1秒钟到了就被SIGALRM信号终止。
由于电脑配置等的不同,每台电脑一秒钟之内计数值一般是不同的。

#include <unistd.h>
unsigned int alarm(unsigned int seconds);
alarm 函数的返回值是0或上次设置闹钟剩余的时间。



信号在目标进程中注册

进程表的表项中有一个软中断信号域,该域中每一位对应一个信号。内核给一个进程发送软中断信号的方法,是在进程所在的进程表项的信号域设置对应于该信号的位。如果信号发送给一个正在睡眠的进程,如果进程睡眠在可被中断的优先级上,则唤醒进程;否则仅设置进程表中信号域相应的位,而不唤醒进程。如果发送给一个处于可运行状态的进程,则只置相应的域即可。

进程的task_struct结构中有关于本进程中未决信号的数据成员: struct sigpending pending:

struct sigpending{
        struct sigqueue *head, *tail;
        sigset_t signal;
};

第三个成员是进程中所有未决信号集,第一、第二个成员分别指向一个sigqueue类型的结构链(称之为"未决信号信息链")的首尾,信息链中的每个sigqueue结构刻画一个特定信号所携带的信息,并指向下一个sigqueue结构:

struct sigqueue{
        struct sigqueue *next;
        siginfo_t info;
}

信号在进程中注册指的就是信号值加入到进程的未决信号集sigset_t signal(每个信号占用一位)中,并且信号所携带的信息被保留到未决信号信息链的某个sigqueue结构中。只要信号在进程的未决信号集中,表明进程已经知道这些信号的存在,但还没来得及处理,或者该信号被进程阻塞。

当一个实时信号发送给一个进程时,不管该信号是否已经在进程中注册,都会被再注册一次,因此,信号不会丢失,因此,实时信号又叫做"可靠信号"。这意味着同一个实时信号可以在同一个进程的未决信号信息链中占有多个sigqueue结构(进程每收到一个实时信号,都会为它分配一个结构来登记该信号信息,并把该结构添加在未决信号链尾,即所有诞生的实时信号都会在目标进程中注册)。

当一个非实时信号发送给一个进程时,如果该信号已经在进程中注册(通过sigset_t signal指示),则该信号将被丢弃,造成信号丢失。因此,非实时信号又叫做"不可靠信号"。这意味着同一个非实时信号在进程的未决信号信息链中,至多占有一个sigqueue结构。

总之信号注册与否,与发送信号的函数(如kill()或sigqueue()等)以及信号安装函数(signal()及sigaction())无关,只与信号值有关(信号值小于SIGRTMIN的信号最多只注册一次,信号值在SIGRTMIN及SIGRTMAX之间的信号,只要被进程接收到就被注册)


信号的执行和注销

内核处理一个进程收到的软中断信号是在该进程的上下文中,因此,进程必须处于运行状态。当其由于被信号唤醒或者正常调度重新获得CPU时,在其从内核空间返回到用户空间时会检测是否有信号等待处理。如果存在未决信号等待处理且该信号没有被进程阻塞,则在运行相应的信号处理函数前,进程会把信号在未决信号链中占有的结构卸掉。

对于非实时信号来说,由于在未决信号信息链中最多只占用一个sigqueue结构,因此该结构被释放后,应该把信号在进程未决信号集中删除(信号注销完毕);而对于实时信号来说,可能在未决信号信息链中占用多个sigqueue结构,因此应该针对占用sigqueue结构的数目区别对待:如果只占用一个sigqueue结构(进程只收到该信号一次),则执行完相应的处理函数后应该把信号在进程的未决信号集中删除(信号注销完毕)。否则待该信号的所有sigqueue处理完毕后再在进程的未决信号集中删除该信号。

当所有未被屏蔽的信号都处理完毕后,即可返回用户空间。对于被屏蔽的信号,当取消屏蔽后,在返回到用户空间时会再次执行上述检查处理的一套流程。

内核处理一个进程收到的信号的时机是在一个进程从内核态返回用户态时。所以,当一个进程在内核态下运行时,软中断信号并不立即起作用,要等到将返回用户态时才处理。进程只有处理完信号才会返回用户态,进程在用户态下不会有未处理完的信号。

处理信号有三种类型:进程接收到信号后退出;进程忽略该信号;进程收到信号后执行用户设定用系统调用signal的函数。当进程接收到一个它忽略的信号时,进程丢弃该信号,就象没有收到该信号似的继续运行。如果进程收到一个要捕捉的信号,那么进程从内核态返回用户态时执行用户定义的函数。而且执行用户定义的函数的方法很巧妙,内核是在用户栈上创建一个新的层,该层中将返回地址的值设置成用户定义的处理函数的地址,这样进程从内核返回弹出栈顶时就返回到用户定义的函数处,从函数返回再弹出栈顶时,才返回原先进入内核的地方。这样做的原因是用户定义的处理函数不能且不允许在内核态下执行(如果用户定义的函数在内核态下运行的话,用户就可以获得任何权限)。


4. 信号的安装


如果进程要处理某一信号,那么就要在进程中安装该信号。安装信号主要用来确定信号值及进程针对该信号值的动作之间的映射关系。
即进程将要处理哪个信号;该信号被传递给进程时,将执行何种操作。

linux主要有两个函数实现信号的安装:signal()、sigaction()。其中signal()只有两个参数,不支持信号传递信息,主要是用于前32种非实时信号的安装;而sigaction()是较新的函数(由两个系统调用实现:sys_signal以及sys_rt_sigaction),有三个参数,支持信号传递信息,主要用来与 sigqueue() 系统调用配合使用,当然,sigaction()同样支持非实时信号的安装。sigaction()优于signal()主要体现在支持信号带有参数。


signal()

#include <signal.h>

void (*signal(int signum, void (*handler))(int)))(int);

如果该函数原型不容易理解的话,可以参考下面的分解方式来理解:

typedef void (*sighandler_t)(int);

sighandler_t signal(int signum, sighandler_t handler));

第一个参数:signum指定要安装的信号。
第二个参数:handler指定信号的处理函数。
            可以忽略该信号(参数设为SIG_IGN);可以采用系统默认方式处理信号(参数设为SIG_DFL);也可以自己实现处理方式(参数指定一个函数地址)。

如果signal()调用成功,返回最后一次为安装信号signum而调用signal()时的handler值,如果失败则返回SIG_ERR。即 该函数的返回值是一个函数指针, 指向上次安装的handler

经典安装方式:

if (signal(SIGINT, SIG_IGN) != SIG_IGN) 
{
    signal(SIGINT, sig_handler);
}
//先获得上次的handler, 如果不是忽略信号, 就安装此信号的handler

由于信号被交付后, 系统自动的重置handler为默认动作, 为了使信号在handler处理期间, 仍能对后继信号做出反应, 往往在handler的第一条语句再次调用 signal

sig_handler(ing signum)
{
    /* 重新安装信号 */
    signal(signum, sig_handler);
    ......
}

传递给信号处理例程的整数参数是信号值,这样可以使得一个信号处理例程处理多个信号。

#include <signal.h>
#include <unistd.h>
#include <stdio.h>

void sigroutine(int dunno)
{ /* 信号处理例程,其中dunno将会得到信号的值 */

        switch (dunno) {
        case 1:
            printf("Get a signal -- SIGHUP ");
            break;
        case 2:
            printf("Get a signal -- SIGINT ");
            break;
        case 3:
            printf("Get a signal -- SIGQUIT ");
            break;
        }
        return;
}
 

int main() {

        printf("process id is %d ",getpid());
        signal(SIGHUP, sigroutine); //* 下面设置三个信号的处理方法
        signal(SIGINT, sigroutine);
        signal(SIGQUIT, sigroutine);
        for (;;) ;
}

其中信号SIGINT由按下Ctrl-C发出,信号SIGQUIT由按下Ctrl-发出。该程序执行的结果如下:

localhost:~$ ./sig_test
process id is 463
Get a signal -SIGINT //按下Ctrl-C得到的结果
Get a signal -SIGQUIT //按下Ctrl-得到的结果
//按下Ctrl-z将进程置于后台
 [1]+ Stopped ./sig_test
localhost:~$ bg
 [1]+ ./sig_test &
localhost:~$ kill -HUP 463 //向进程发送SIGHUP信号
localhost:~$ Get a signal – SIGHUP
kill -9 463 //向进程发送SIGKILL信号,终止进程
localhost:~$

示例

#include <unistd.h>
#include <stdio.h>
#include <signal.h>

void handler(int sig)
{
    printf("get a sig,num is %d\n",sig);
}
  
int main()
{
    signal(2,handler);
    while(1)
    {
        sleep(1);
        printf("hello\n");
    }
    return 0;
}

修改了2号信号(Ctrl-c)的默认处理动作为handler函数的内容,则当该程序在前台运行时,键入Ctrl-c后不会执行它的默认处理动作(终止该进程)

此时,可以使用 (Ctrl-z)终止程序的运行



我们知道在程序的任意执行点上, 信号随时可能发生, 如果信号在sig_handler重新安装信号之前产生, 这次信号就会执行默认动作, 而不是sig_handler. 这种问题是不可预料的.

为了克服非可靠信号并同一SVR4和BSD之间的差异, 产生了 POSIX 信号安装方式, 使用sigaction安装信号的动作后, 该动作就一直保持, 直到另一次调用 sigaction建立另一个动作为止. 这就克服了古老的 signal 调用存在的问题



sigaction()

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

#include <signal.h>

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

sigaction函数用于改变进程接收到特定信号后的行为。该函数的第一个参数为信号的值,可以为除SIGKILL及SIGSTOP外的任何一个特定有效的信号(为这两个信号定义自己的处理函数,将导致信号安装错误)。第二个参数是指向结构sigaction的一个实例的指针,在结构sigaction的实例中,指定了对特定信号的处理,可以为空,进程会以缺省方式对信号处理;第三个参数oldact指向的对象用来保存返回的原来对相应信号的处理,可指定oldact为NULL如果把第二、第三个参数都设为NULL,那么该函数可用于检查信号的有效性。

第二个参数最为重要,其中包含了对指定信号的处理、信号所传递的信息、信号处理函数执行过程中应屏蔽掉哪些信号等等。

可以通过 man sigaction 查看 sigaction 相关信息。


#include <signal.h>

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


struct sigaction
{
  void (*sa_handler)(int);  //信号处理方式
  void (*sa_sigaction)(int, siginfo_t *, void *); //实时信号的处理方式 暂不讨论
  sigset_t sa_mask; //额外屏蔽的信号
  int sa_flags;
  void (*sa_restorer)(void); 
};

signum是指定信号的编号。

处理方式:

     1、若act指针非空,则根据act结构体中的信号处理函数来修改该信号的处理动作。

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

     3、现将原来的处理动作备份到oldact里,然后根据act修改该信号的处理动作。

(注:后两个参数都是输入输出型参数!)

sa_handler三种可选方式:

     1、赋值为常数SIG_IGN传给sigaction表示忽略信号;

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

     3、赋值为一个函数指针表示用自定义函数捕捉信号,或者说向内核注册一个信号处理函 数,该函数返回值为void,可以带一个int参数,通过参数可以得知当前信号的编号,这样就可以用同一个函数处理多种信号。

(注:这是一个回调函数,不是被main函数调用,而是被系统所调用)

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


1、*sa_handler 以及 *sa_sigaction 指定信号关联函数,即用户指定的信号处理函数。除了可以是用户自定义的处理函数外,还可以为SIG_DFL(采用缺省的处理方式),也可以为SIG_IGN(忽略信号)。

2、由 *sa_sigaction是指定的信号处理函数带有三个参数,是为实时信号而设的(当然同样支持非实时信号),它指定一个3参数信号处理函数。第一个参数为信号值,第三个参数没有使用,第二个参数是指向siginfo_t 结构的指针,结构中包含信号携带的数据值,参数所指向的结构如下:

siginfo_t {
                  int      si_signo;  /* 信号值,对所有信号有意义*/
                  int      si_errno;  /* errno值,对所有信号有意义*/
                  int      si_code;   /* 信号产生的原因,对所有信号有意义*/
                               union{                               /* 联合数据结构,不同成员适应不同信号 */
                                       //确保分配足够大的存储空间
                                       int _pad[SI_PAD_SIZE];

                                       //对SIGKILL有意义的结构
                                       struct{
                                                      ...
                                                 }...
                                               ... ...                      
                                       //对SIGILL, SIGFPE, SIGSEGV, SIGBUS有意义的结构
                                  struct{
                                                      ...
                                                 }...
                                               ... ...
                                         }
}

前面在讨论系统调用sigqueue发送信号时,sigqueue的第三个参数就是sigval联合数据结构,当调用sigqueue时,该数据结构中的数据就将拷贝到信号处理函数的第二个参数中。这样,在发送信号同时,就可以让信号传递一些附加信息。信号可以传递信息对程序开发是非常有意义的。

3、sa_mask指定在信号处理程序执行过程中,哪些信号应当被阻塞。缺省情况下当前信号本身被阻塞,防止信号的嵌套发送,除非指定SA_NODEFER或者SA_NOMASK标志位。

注:请注意sa_mask指定的信号阻塞的前提条件,是在由sigaction()安装信号的处理函数执行过程中由sa_mask指定的信号才被阻塞。

4、sa_flags中包含了许多标志位,包括刚刚提到的SA_NODEFER及SA_NOMASK标志位。另一个比较重要的标志位是SA_SIGINFO,当设定了该标志位时,表示信号附带的参数可以被传递到信号处理函数中,因此,应该为sigaction结构中的sa_sigaction指定处理函数,而不应该为sa_handler指定信号处理函数,否则,设置该标志变得毫无意义。即使为sa_sigaction指定了信号处理函数,如果不设置SA_SIGINFO,信号处理函数同样不能得到信号传递过来的数据,在信号处理函数中对这些信息的访问都将导致段错误(Segmentation fault)。


经典安装方式:

struct sigaction action, old_action;

/* 设置SIGINT */
action.sa_handler = sig_handler;
sigemptyset(&action.sa_mask);
sigaddset(&action.sa_mask, SIGTERM);
action.sa_flags = 0;

/* 获取上次的handler, 如果不是忽略动作, 则安装信号 */
sigaction(SIGINT, NULL, &old_action);
if (old_action.sa_handler != SIG_IGN) 
{
    sigaction(SIGINT, &action, NULL);
}

基于 sigaction 实现的库函数: signal
sigaction 自然强大, 但安装信号很繁琐, 目前linux中的signal()是通过sigation()函数实现的,因此,即使通过signal()安装的信号,在信号处理函数的结尾也不必再调用一次信号安装函数。


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

#include <unistd.h>
int pause(void);


处理方式: 

     如果信号的处理动作是终止进程,则进程终止,pause函数没有机会返回;

     如果信号的处理动作是忽略,则进程继续处于挂起状态,pause不返回;

     如果信号的处理动作是捕捉,则调用了信号处理函数之后pause返回-1,errno设置为EINTR。

     所以pause只有出错的返回值(类似exec函数家族)。错误码EINTR表示“被信号中断”。

 举个栗子

     1、定义一个闹钟,约定times秒后,内核向该进程发送一个SIGALRM信号;

     2、调用pause函数将进程挂起,内核切换到别的进程运行;

     3、times秒后,内核向该进程发送SIGALRM信号,发现其处理动作是一个自定义函数,于是切回用户态执行该自定义处理函数;

     4、进入sig_alrm函数时SIGALRM信号被自动屏蔽,从sig_alrm函数返回时SIGALRM信号自动解除屏蔽。然后自动执行特殊的系统调用sigreturn再次进入内核,之后再返回用户态继续执行进程的主控制流程(main函数调用的mytest函数)。

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


#include<stdio.h>
#include<signal.h>
#include<unistd.h>
void sig_alarm(int signum)
{
 printf("I am a custom handler!\n");
}
void mysleep(unsigned int times)
{
 //注册两个信号处理动作
 struct sigaction new,old;
 new.sa_handler=sig_alarm; //信号处理函数
 sigemptyset(&new.sa_mask);//不屏蔽任何信号屏蔽字
 new.sa_flags=0;
  
 //对SIGALRM 信号的默认处理动作修改为自定义处理动作
 sigaction(SIGALRM,&new,&old);
 alarm(times);
 pause(); //挂起等待
 alarm(1);
 sleep(2);
 alarm(0); //取消闹钟
 //恢复SIGALRM 信号到默认处理动作
 sigaction(SIGALRM,&old,NULL);
 alarm(1);
 sleep(2);
}
int main()
{
 while(1)
 {
 mysleep(2);
 printf("many seconds passed\n");
 printf("###################\n");
 }
 return 0;
}


定义一个闹钟并挂起等待,收到信号后执行自定义处理动作,在没有恢复默认处理动作前,收到SIGALRM信号都会按照其自定义处理函数来处理。恢复自定义处理动作之后收到SIGALRM信号则执行其默认处理动作即终止进程!



5. 信号的发送


发送信号的主要函数有:kill()、raise()、 sigqueue()、alarm()、setitimer()以及abort()。


kill()

#include <sys/types.h>
#include <signal.h>

int kill(pid_t pid,int signo)

该系统调用可以用来向任何进程或进程组发送任何信号。参数pid的值为信号的接收进程

pid>0 进程ID为pid的进程

pid=0 同一个进程组的进程

pid<0 pid!=-1 进程组ID为 -pid的所有进程

pid=-1 除发送进程自身外,所有进程ID大于1的进程

Sinno是信号值,当为0 (即空信号),实际不发送任何信号,但照常进行错误检查,因此,可用于检查目标进程是否存在,以及当前进程是否具有向目标发送信号的权限root权限的进程可以向任何进程发送信号,非root权限的进程只能向属于同一个session或者同一个用户的进程发送信号)。

Kill()最常用于pid>0时的信号发送。该调用执行成功时,返回值为0;错误时,返回-1,并设置相应的错误代码errno。下面是一些可能返回的错误代码:

EINVAL:指定的信号sig无效。

ESRCH:参数pid指定的进程或进程组不存在。注意,在进程表项中存在的进程,可能是一个还没有被wait收回,但已经终止执行的僵死进程。

EPERM: 进程没有权力将这个信号发送到指定接收信号的进程。因为,一个进程被允许将信号发送到进程pid时,必须拥有root权力,或者是发出调用的进程的UID 或EUID与指定接收的进程的UID或保存用户ID(savedset-user-ID)相同。如果参数pid小于-1,即该信号发送给一个组,则该错误表示组中有成员进程不能接收该信号。


sigqueue()

#include <sys/types.h>
#include <signal.h>

int sigqueue(pid_t pid, int sig, const union sigval val)

调用成功返回 0;否则,返回 -1。

sigqueue()是比较新的发送信号系统调用,主要是针对实时信号提出的(当然也支持前32种),支持信号带有参数,与函数sigaction()配合使用.

sigqueue的第一个参数是指定接收信号的进程ID,第二个参数确定即将发送的信号,第三个参数是一个联合数据结构union sigval,指定了信号传递的参数,即通常所说的4字节值.

typedef union sigval {
               int  sival_int;
               void *sival_ptr;
}sigval_t;

sigqueue()比kill()传递了更多的附加信息,但sigqueue()只能向一个进程发送信号,而不能发送信号给一个进程组。如果signo=0,将会执行错误检查,但实际上不发送任何信号,0值信号可用于检查pid的有效性以及当前进程是否有权限向目标进程发送信号。

在调用sigqueue时,sigval_t指定的信息会拷贝到对应sig 注册的3参数信号处理函数的siginfo_t结构中,这样信号处理函数就可以处理这些信息了。由于sigqueue系统调用支持发送带参数信号,所以比kill()系统调用的功能要灵活和强大得多。


alarm()

#include <unistd.h>

unsigned int alarm(unsigned int seconds)

系统调用alarm安排内核为调用进程在指定的seconds秒后发出一个SIGALRM的信号。
如果指定的参数seconds为0,则不再发送 SIGALRM信号。后一次设定将取消前一次的设定。
该调用返回值为上次定时调用到发送之间剩余的时间,或者因为没有前一次定时调用而返回0。

注意,在使用时,alarm只设定为发送一次信号,如果要多次发送,就要多次使用alarm调用。


setitimer()

现在的系统中很多程序不再使用alarm调用,而是使用setitimer调用来设置定时器,用getitimer来得到定时器的状态,这两个调用的声明格式如下:

nt getitimer(int which, struct itimerval *value);

int setitimer(int which, const struct itimerval *value, struct itimerval *ovalue);

在使用这两个调用的进程中加入以下头文件:
#include <sys/time.h>

该系统调用给进程提供了三个定时器,它们各自有其独有的计时域,当其中任何一个到达,就发送一个相应的信号给进程,并使得计时器重新开始。三个计时器由参数which指定,如下所示:

TIMER_REAL:按实际时间计时,计时到达将给进程发送SIGALRM信号。

ITIMER_VIRTUAL:仅当进程执行时才进行计时。计时到达将发送SIGVTALRM信号给进程。

ITIMER_PROF:当进程执行时和系统为该进程执行动作时都计时。与ITIMER_VIR-TUAL是一对,该定时器经常用来统计进程在用户态和内核态花费的时间。计时到达将发送SIGPROF信号给进程。

定时器中的参数value用来指明定时器的时间,其结构如下:
struct itimerval {
        struct timeval it_interval; /* 下一次的取值 */
        struct timeval it_value; /* 本次的设定值 */
};

该结构中timeval结构定义如下:
struct timeval {
        long tv_sec; /* 秒 */
        long tv_usec; /* 微秒,1秒 = 1000000 微秒*/
};

在setitimer 调用中,参数ovalue如果不为空,则其中保留的是上次调用设定的值。定时器将it_value递减到0时,产生一个信号,并将it_value的值设定为it_interval的值,然后重新开始计时,如此往复。当it_value设定为0时,计时器停止,或者当它计时到期,而it_interval 为0时停止。调用成功时,返回0;错误时,返回-1,并设置相应的错误代码errno:

EFAULT:参数value或ovalue是无效的指针。

EINVAL:参数which不是ITIMER_REAL、ITIMER_VIRT或ITIMER_PROF中的一个。

下面是关于setitimer调用的一个简单示范,在该例子中,每隔一秒发出一个SIGALRM,每隔0.5秒发出一个SIGVTALRM信号:

#include <signal.h>
#include <unistd.h>
#include <stdio.h>
#include <sys/time.h>

int sec;

void sigroutine(int signo) {

        switch (signo) {

        case SIGALRM:
        printf("Catch a signal -- SIGALRM ");
        break;

        case SIGVTALRM:
        printf("Catch a signal -- SIGVTALRM ");
        break;
        }
        return;
}

int main()
{
        struct itimerval value,ovalue,value2;

        sec = 5;

        printf("process id is %d ",getpid());
        signal(SIGALRM, sigroutine);
        signal(SIGVTALRM, sigroutine);

        value.it_value.tv_sec = 1;
        value.it_value.tv_usec = 0;
        value.it_interval.tv_sec = 1;
        value.it_interval.tv_usec = 0;

        setitimer(ITIMER_REAL, &value, &ovalue);

        value2.it_value.tv_sec = 0;
        value2.it_value.tv_usec = 500000;
        value2.it_interval.tv_sec = 0;
        value2.it_interval.tv_usec = 500000;

        setitimer(ITIMER_VIRTUAL, &value2, &ovalue);

        for (;;) ;
}

该例子的屏幕拷贝如下:

localhost:~$ ./timer_test
process id is 579
Catch a signal – SIGVTALRM
Catch a signal – SIGALRM
Catch a signal – SIGVTALRM
Catch a signal – SIGVTALRM
Catch a signal – SIGALRM
Catch a signal –GVTALRM


abort()

#include <stdlib.h>

void abort(void);

向进程发送SIGABORT信号,默认情况下进程会异常退出,当然可定义自己的信号处理函数。即使SIGABORT被进程设置为阻塞信号,调用abort()后,SIGABORT仍然能被进程接收。该函数无返回值。


raise()

#include <signal.h>

int raise(int signo)

向进程本身发送信号,参数为即将发送的信号值。调用成功返回 0;否则,返回 -1。


6. 信号集及信号集操作函数


信号集被定义为一种数据类型:

typedef struct {
        unsigned long sig[_NSIG_WORDS];
} sigset_t

信号集用来描述信号的集合,每个信号占用一位。Linux所支持的所有信号可以全部或部分的出现在信号集中,主要与信号阻塞相关函数配合使用。下面是为信号集操作定义的相关函数。

#include <signal.h>

int sigemptyset(sigset_t *set);

int sigfillset(sigset_t *set);

int sigaddset(sigset_t *set, int signum)

int sigdelset(sigset_t *set, int signum);

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

sigemptyset(sigset_t *set)初始化由set指定的信号集,信号集里面的所有信号被清空;

sigfillset(sigset_t *set)调用该函数后,set指向的信号集中将包含linux支持的64种信号;

sigaddset(sigset_t *set, int signum)在set指向的信号集中加入signum信号;

sigdelset(sigset_t *set, int signum)在set指向的信号集中删除signum信号;

sigismember(const sigset_t *set, int signum)判定信号signum是否在set指向的信号集中。



7. 信号阻塞与信号未决


未决信号:已经产生,但是没有给进程递送的信号(即还未处理)

已被递送信号:已经递送给进程并处理过。


阻塞信号

 1.信号在内核中的表示:

信号递达delivery:实际执行信号处理信号的动作

信号未决pending:信号从产生到抵达之间的状态,信号产生了但是未处理

忽略:抵达之后的一种 动作

阻塞block:收到信号不立即处理     被阻塞的信号将保持未决状态,直到进程解除对此信号的阻塞,才执行抵达动作

信号产生和阻塞没有直接关系 抵达和解除阻塞没有直接关系!

进程收到一个信号后,不会立即处理,它会在恰当的时机被处理。

每个信号都由两个标志位分别表示阻塞和未决,以及一个函数指针表示信号的处理动作。

在上图的例子中,

  1. SIGHUP信号未阻塞也未产生过,当它递达时执行默认处理动作。

  2. SIGINT信号产生过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略,但在没 有解除阻塞之前不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞。

  3. SIGQUIT信号未产生过,一旦产生SIGQUIT信号将被阻塞,它的处理动作是用户自定义函数sighandler。阻塞信号集也叫作信号屏蔽字。

信号产生但是不立即处理,前提条件是要把它保存在pending表中,表明信号已经产生。


信号集操作函数

#include <signal.h>
int sigemptyset(sigset_t *set); //初始化set所指向的信号集,使所有信号的对应位清0
int sigfillset(sigset_t *set); //初始化set所指向的信号集,表示该信号集的有效信号包括系统支持的所有信号
int sigaddset(sigset_t *set, int signo); //在该信号集中添加有效信号
int sigdelset(sigset_t *set, int signo); //在该信号集中删除有效信号
int sigismember(const sigset_t *set, int signo); //用于判断一个信号集的有效信号中是否包含某种信号

参数解析:

sigset_t结构体的参数表示信号集,信号操作的时候都是以信号集合的方式进行操作,需要事先创建一个该结构体的对象,然后把想要操作的信号添加到信号集合对象当中去。signo就是信号的标号了。


调用函数sigprocmask可以读取或更改进程的信号屏蔽字(阻塞信号集)

#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);

一个进程的信号屏蔽字规定了当前阻塞而不能递送给该进程的信号集。调用函数sigprocmask可以检测或更改(或两者)进程的信号屏蔽字。如果调用sigprocmask解除了对当前若干个未决信号的阻塞,则在sigprocmask返回前,至少将其中 一个信号递达

参数解析:

how,有三个宏

     SIG_BLOCK      添加到block表当中去

     SIG_UNBLOCK  从block表中删除

     SIG_SETMASK  设置block表 设置当前信号屏蔽字为set所指向的值

 set表示新设置的信号屏蔽字,oset表示当前信号屏蔽字

处理方式:

      set 非空, oset 为NULL :按照how指示的方法更改set指向信号集的信号屏蔽字。

      set 为NULL,oset 非空:读取oset指向信号集的信号屏蔽字,通过oset参数传出。

      set 和 oset 都非空 :现将原来的信号屏蔽字备份到oset里,然后根据sethow参数更改信号屏蔽字。

sigpending读取当前进程的未决信号集,通过set参数传出

#include <signal.h>
int sigpending(sigset_t *set);
这是一个输出型参数,会把当前进程的 pending 表打印到传入的set集中

实例验证上面几个函数:



一开始没有任何信号,所以pending表中全是0,我通过Ctrl+C传入2号信号,看到pending表中有2号被置位了,经过10秒取消阻塞,2号信号被处理(经过我自定义的函数)



每个进程都有一个用来描述哪些信号递送到进程时将被阻塞的信号集,该信号集中的所有信号在递送到进程后都将被阻塞。下面是与信号阻塞相关的几个函数。

#include <signal.h>

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

sigprocmask()函数能够根据参数how来实现对信号集的操作,操作主要有三种:

        SIG_BLOCK 在进程当前阻塞信号集中添加set指向信号集中的信号
        SIG_UNBLOCK 如果进程阻塞信号集中包含set指向信号集中的信号,则解除对该信号的阻塞
        SIG_SETMASK 更新进程阻塞信号集为set指向的信号集

sigpending(sigset_t *set))获得当前已递送到进程,却被阻塞的所有信号,在set指向的信号集中返回结果。

sigsuspend(const sigset_t *mask))用于在接收到某个信号之前, 临时用mask替换进程的信号掩码, 并暂停进程执行,直到收到信号为止。sigsuspend 返回后将恢复调用之前的信号掩码。信号处理函数完成后,进程将继续执行。该系统调用始终返回-1,并将errno设置为EINTR。


如何屏蔽信号

所谓屏蔽, 并不是禁止递送信号, 而是暂时阻塞信号的递送, 解除屏蔽后, 信号将被递送, 不会丢失. 相关API为

int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset(sigset_t *set, int signum);
int sigdelset(sigset_t *set, int signum);
int sigismember(const sigset_t *set, int signum);
int sigsuspend(const sigset_t *mask);
int sigpending(sigset_t *set);
-----------------------------------------------------------------
int  sigprocmask(int  how,  const  sigset_t *set, sigset_t *oldset));
sigprocmask()函数能够根据参数how来实现对信号集的操作,操作主要有三种:
* SIG_BLOCK	在进程当前阻塞信号集中添加set指向信号集中的信号
* SIG_UNBLOCK	如果进程阻塞信号集中包含set指向信号集中的信号,则解除
   对该信号的阻塞
* SIG_SETMASK	更新进程阻塞信号集为set指向的信号集

屏蔽整个进程的信号:

#include <signal.h>  
#include <stdio.h>  
#include <stdlib.h>  
#include <error.h>  
#include <string.h>  
  
void sig_handler(int signum)  
{  
    printf("catch SIGINT\n");  
}  
  
int main(int argc, char **argv)  
{  
    sigset_t block;  
    struct sigaction action, old_action;  
  
    /* 安装信号 */  
    action.sa_handler = sig_handler;  
    sigemptyset(&action.sa_mask);  
    action.sa_flags = 0;  
  
    sigaction(SIGINT, NULL, &old_action);  
    if (old_action.sa_handler != SIG_IGN) {  
        sigaction(SIGINT, &action, NULL);  
    }  
  
    /* 屏蔽信号 */  
    sigemptyset(&block);  
    sigaddset(&block, SIGINT);  
  
    printf("block SIGINT\n");  
    sigprocmask(SIG_BLOCK, &block, NULL);  
  
    printf("--> send SIGINT -->\n");  
    kill(getpid(), SIGINT);  
    printf("--> send SIGINT -->\n");  
    kill(getpid(), SIGINT);  
    sleep(1);  
  
    /* 解除信号后, 之前触发的信号将被递送, 
     * 但SIGINT是非可靠信号, 只会递送一次 
     */  
    printf("unblock SIGINT\n");  
    sigprocmask(SIG_UNBLOCK, &block, NULL);  
  
    sleep(2);  
  
    return 0;  
}  

运行结果:

work> ./a.out   
block SIGINT  
--> send SIGINT -->  
--> send SIGINT -->  
unblock SIGINT  
catch SIGINT  

这里发送了两次SIGINT信号 可以看到, 屏蔽掉SIGINT后,信号无法递送, 解除屏蔽后, 才递送信号, 但只被递送一次,因为SIGINT是非可靠信号, 不支持排队。

只在信号处理期间, 屏蔽其它信号在信号的handler执行期间, 系统将自动屏蔽此信号, 但如果还想屏蔽其它信号怎么办? 可以利用 struct sigaction 结构体的 sa_mask 属性.

#include <signal.h>  
#include <stdio.h>  
#include <stdlib.h>  
#include <error.h>  
#include <string.h>  
  
void sig_handler(int signum)  
{  
    printf("in handle, SIGTERM is blocked\n");  
    /* 在此handler内将屏蔽掉SIGTERM, 直到此handler返回 */  
    printf("--> send SIGTERM -->\n");  
    kill(getpid(), SIGTERM);  
    sleep(5);  
    printf("handle done\n");  
}  
  
void handle_term(int signum)  
{  
    printf("catch sigterm and exit..\n");  
    exit(0);  
}  
  
int main(int argc, char **argv)  
{  
    struct sigaction action, old_action;  
  
    /* 设置SIGINT */  
    action.sa_handler = sig_handler;  
    sigemptyset(&action.sa_mask);  
    /* 安装handler的时候, 设置在handler 
     * 执行期间, 屏蔽掉SIGTERM信号 */  
    sigaddset(&action.sa_mask, SIGTERM);  
    action.sa_flags = 0;  
  
    sigaction(SIGINT, NULL, &old_action);  
    if (old_action.sa_handler != SIG_IGN) {  
        sigaction(SIGINT, &action, NULL);  
    }  
  
    /* 设置SIGTERM */  
    action.sa_handler = handle_term;  
    sigemptyset(&action.sa_mask);  
    action.sa_flags = 0;  
  
    sigaction(SIGTERM, NULL, &old_action);  
    if (old_action.sa_handler != SIG_IGN) {  
        sigaction(SIGTERM, &action, NULL);  
    }  
  
    printf("--> send SIGINT -->\n");  
    kill(getpid(), SIGINT);  
  
    while (1) {  
        sleep(1);  
    }  
  
    return 0;  
}  

运行结果:

work> ./a.out  
--> send SIGINT -->  
in handle, SIGTERM is blocked  
--> send SIGTERM -->  
handle done  
catch sigterm and exit..  

收到SIGINT后, 进入sig_handler,此时发送SIGTERM信号将被屏蔽。等sig_handler返回后, 才收到SIGTERM信号, 然后退出程序


另一个示例

//忽略,屏蔽信号
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <sys/types.h>
#include <signal.h>

int main(int arg, char *args[])
{
    pid_t pid=fork();
    if(pid==-1)
    {
        printf("fork() failed! error message:%s\n",strerror(errno));
        return -1;
    }
    //注册信号,屏蔽SIGCHLD信号,子进程退出,将不会给父进程发送信号,因此也不会出现僵尸进程
    signal(SIGCHLD,SIG_IGN);
    if(pid>0)
    {
        printf("father is runing !\n");
        sleep(10);
    }
    if(pid==0)
    {
        printf("i am child!\n");
        exit(0);
    }
    printf("game over!\n");
    return 0;
}

恢复信号

//恢复信号
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <sys/types.h>
#include <signal.h>

void catch_signal(int sign)
{
    switch (sign)
    {
    case SIGINT:
        printf("ctrl + C 被执行了!\n");
        //exit(0);
        break;
    }
}

int main(int arg, char *args[])
{
    //注册终端中断信号
    signal(SIGINT, catch_signal);
    char tempc = 0;
    while ((tempc = getchar()) != 'a')
    {
        printf("tempc=%d\n", tempc);
        //sleep()
    }
    //恢复信号
    signal(SIGINT, SIG_DFL);
    while (1)
    {
        pause();
    }
    printf("game over!\n");
    return 0;
}

返回值

//signal()函数的返回值
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <sys/types.h>
#include <signal.h>

void catch_signal(int sign)
{
    switch (sign)
    {
    case SIGINT:
        printf("ctrl + C 被执行了!\n");
        //exit(0);
        break;
    }
}

int main(int arg, char *args[])
{
    /*
     * signal()函数的返回值是signal()函数上一次的行为
     * */
    typedef void (*sighandler_t)(int);
    //因为第一次注册信号SIGINT,所以上一次的行为就是默认行为
    sighandler_t old=signal(SIGINT, catch_signal);
    if(old==SIG_ERR)
    {
        //注册信号失败
        perror("signal error");
    }
    /*正规写法*/
    if(signal(SIGQUIT,catch_signal)==SIG_ERR)
    {
        //注册新号失败
        perror("signal error");
    }
    char tempc = 0;
    while ((tempc = getchar()) != 'a')
    {
        printf("tempc=%d\n", tempc);
        //sleep()
    }
    //把默认行为重新注册,不就是恢复默认信号了
    signal(SIGINT, old);
    while (1)
    {
        pause();
    }
    printf("game over!\n");
    return 0;
}


如何获取未决信号

所谓未决信号, 是指被阻塞的信号, 等待被递送的信号. 

int sigsuspend(const sigset_t *mask))
sigpending(sigset_t *set))    //获得当前已递送到进程,却被阻塞的所有信号,在set指向的信号集中返回结果。

示例代码

#include <signal.h>  
#include <stdio.h>  
#include <stdlib.h>  
#include <error.h>  
#include <string.h>  
  
/* 版本1, 可靠信号将被递送多次 */  
//#define MYSIGNAL SIGRTMIN+5  
/* 版本2, 不可靠信号只被递送一次 */  
#define MYSIGNAL SIGTERM  
  
void sig_handler(int signum)  
{  
    psignal(signum, "catch a signal");  
}  
  
int main(int argc, char **argv)  
{  
    sigset_t block, pending;  
    int sig, flag;  
  
    /* 设置信号的handler */  
    signal(MYSIGNAL, sig_handler);  
  
    /* 屏蔽此信号 */  
    sigemptyset(&block);  
    sigaddset(&block, MYSIGNAL);  
    printf("block signal\n");  
    sigprocmask(SIG_BLOCK, &block, NULL);  
  
    /* 发两次信号, 看信号将会被触发多少次 */  
    printf("---> send a signal --->\n");  
    kill(getpid(), MYSIGNAL);  
    printf("---> send a signal --->\n");  
    kill(getpid(), MYSIGNAL);  
  
    /* 检查当前的未决信号 */  
    flag = 0;  
    sigpending(&pending);  
    for (sig = 1; sig < NSIG; sig++) {  
        if (sigismember(&pending, sig)) {  
            flag = 1;  
            psignal(sig, "this signal is pending");  
        }   
    }  
    if (flag == 0) {  
        printf("no pending signal\n");  
    }  
  
    /* 解除此信号的屏蔽, 未决信号将被递送 */  
    printf("unblock signal\n");  
    sigprocmask(SIG_UNBLOCK, &block, NULL);  
  
    /* 再次检查未决信号 */  
    flag = 0;  
    sigpending(&pending);  
    for (sig = 1; sig < NSIG; sig++) {  
        if (sigismember(&pending, sig)) {  
            flag = 1;  
            psignal(sig, "a pending signal");  
        }   
    }  
    if (flag == 0) {  
        printf("no pending signal\n");  
    }  
  
    return 0;  
}  

这个程序有两个版本:
可靠信号版本, 运行结果:

work> ./a.out   
block signal  
---> send a signal --->  
---> send a signal --->  
this signal is pending: Unknown signal 39  
unblock signal  
catch a signal: Unknown signal 39  
catch a signal: Unknown signal 39  
no pending signal  
发送两次可靠信号, 最终收到两次信号

非可靠信号版本, 运行结果:

work> ./a.out   
block signal  
---> send a signal --->  
---> send a signal --->  
this signal is pending: Terminated  
unblock signal  
catch a signal: Terminated  
no pending signal  
发送两次非可靠信号, 最终只收到一次



8. 被中断的系统调用


一些IO系统调用执行时, 如 read 等待输入期间, 如果收到一个信号,系统将中断read, 转而执行信号处理函数. 当信号处理返回后, 系统遇到了一个问题: 是重新开始这个系统调用, 还是让系统调用失败?早期UNIX系统的做法是, 中断系统调用, 并让系统调用失败, 比如read返回 -1, 同时设置 errno 为 EINTR。中断了的系统调用是没有完成的调用, 它的失败是临时性的, 如果再次调用则可能成功, 这并不是真正的失败, 所以要对这种情况进行处理, 典型的方式为:

while (1) {
    n = read(fd, buf, BUFSIZ);
    if (n == -1 && errno != EINTR) {
        printf("read error\n");
        break;
    }
    if (n == 0) {
        printf("read done\n");
        break;
    }
}

这样做逻辑比较繁琐, 事实上, 我们可以从信号的角度来解决这个问题, 安装信号的时候, 设置 SA_RESTART属性, 那么当信号处理函数返回后, 被该信号中断的系统
调用将自动恢复. 

#include <signal.h>  
#include <stdio.h>  
#include <stdlib.h>  
#include <error.h>  
#include <string.h>  
  
void sig_handler(int signum)  
{  
    printf("in handler\n");  
    sleep(1);  
    printf("handler return\n");  
}  
  
int main(int argc, char **argv)  
{  
    char buf[100];  
    int ret;  
    struct sigaction action, old_action;  
  
    action.sa_handler = sig_handler;  
    sigemptyset(&action.sa_mask);  
    action.sa_flags = 0;  
    /* 版本1:不设置SA_RESTART属性 
     * 版本2:设置SA_RESTART属性 */  
    //action.sa_flags |= SA_RESTART;  
  
    sigaction(SIGINT, NULL, &old_action);  
    if (old_action.sa_handler != SIG_IGN) {  
        sigaction(SIGINT, &action, NULL);  
    }  
  
    bzero(buf, 100);  
  
    ret = read(0, buf, 100);  
    if (ret == -1) {  
        perror("read");  
    }  
  
    printf("read %d bytes:\n", ret);  
    printf("%s\n", buf);  
  
    return 0;  
}  

版本1, 不设置 SA_RESTART 属性 :

work> gcc signal.c   
work> ./a.out   
^Cin handler  
handler return  
read: Interrupted system call  
read -1 bytes:  

在 read 等待数据期间, 按下ctrl + c, 触发 SIGINT 信号, handler 返回后, read 被中断, 返回 -1

版本2, 设置 SA_RESTART 属性:

work> gcc signal.c   
work> ./a.out   
^Cin handler  
handler return  
hello, world  
read 13 bytes:  
hello, world  

handler 返回后, read 系统调用被恢复执行, 继续等待数据.



9. 非局部控制转移


int setjmp(jmp_buf env);
int sigsetjmp(sigjmp_buf env, int savesigs);
void longjmp(jmp_buf env, int val);
void siglongjmp(sigjmp_buf env, int val);
--------------------------------------------------------
setjmp()会保存目前堆栈环境,然后将目前的地址作一个记号,
而在程序其他地方调用 longjmp 时便会直接跳到这个记号位置,
然后还原堆栈,继续程序好执行。
setjmp调用有点fork的味道, setjmp()return 0 if returning directly, and non-zero when returning from longjmp using the saved context.
if (setjmp(jmpbuf)) {
   printf("return from jmp\n");
} else {
   printf("return directly\n");
}
setjmp 和 sigsetjmp 的唯一区别是: setjmp 不一定会恢复信号集合, 而sigsetjmp可以保证恢复信号集合
#include <signal.h>  
#include <stdio.h>  
#include <stdlib.h>  
#include <errno.h>  
#include <string.h>  
#include <setjmp.h>  
  
void sig_alrm(int signum);  
void sig_usr1(int signum);  
void print_mask(const char *str);  
  
static sigjmp_buf jmpbuf;  
static volatile sig_atomic_t canjmp;  
static int sigalrm_appear;  
  
int main(int argc, char **argv)  
{  
    struct sigaction action, old_action;  
  
    /* 设置SIGUSR1 */  
    action.sa_handler = sig_usr1;  
    sigemptyset(&action.sa_mask);  
    action.sa_flags = 0;  
  
    sigaction(SIGUSR1, NULL, &old_action);  
    if (old_action.sa_handler != SIG_IGN) {  
        sigaction(SIGUSR1, &action, NULL);  
    }  
  
    /* 设置SIGALRM */  
    action.sa_handler = sig_alrm;  
    sigemptyset(&action.sa_mask);  
    action.sa_flags = 0;  
  
    sigaction(SIGALRM, NULL, &old_action);  
    if (old_action.sa_handler != SIG_IGN) {  
        sigaction(SIGALRM, &action, NULL);  
    }  
  
    print_mask("starting main:");  
  
    if (sigsetjmp(jmpbuf, 1) != 0) {  
        print_mask("exiting main:");  
    } else {  
        printf("sigsetjmp return directly\n");  
        canjmp = 1;  
        while (1) {  
            sleep(1);  
        }  
    }  
  
    return 0;  
}  
  
void sig_usr1(int signum)  
{  
    time_t starttime;  
    if (canjmp == 0) {  
        printf("please set jmp first\n");  
        return;  
    }  
  
    print_mask("in sig_usr1:");  
  
    alarm(1);  
    while (!sigalrm_appear);  
    canjmp = 0;  
    siglongjmp(jmpbuf, 1);  
}  
  
void sig_alrm(int signum)  
{  
    print_mask("in sig_alrm:");  
    sigalrm_appear = 1;  
  
    return;  
}  
  
void print_mask(const char *str)   
{  
    sigset_t sigset;  
    int i, errno_save, flag = 0;  
  
    errno_save = errno;  
  
    if (sigprocmask(0, NULL, &sigset) < 0) {  
        printf("sigprocmask error\n");  
        exit(0);  
    }  
  
    printf("%s\n", str);  
    fflush(stdout);  
  
    for (i = 1; i < NSIG; i++) {  
        if (sigismember(&sigset, i)) {  
            flag = 1;  
            psignal(i, "a blocked signal");  
        }  
    }  
  
    if (!flag) {  
        printf("no blocked signal\n");  
    }  
  
    printf("\n");  
    errno = errno_save;  
}  
运行结果: 
work> ./a.out &  
[4] 28483  
starting main:  
no blocked signal  
  
sigsetjmp return directly  
  
kill -USR1 28483  
  
in sig_usr1:  
a blocked signal: User defined signal 1  
  
in sig_alrm:  
a blocked signal: User defined signal 1  
a blocked signal: Alarm clock  
  
exiting main:  
no blocked signal  



10. 信号的生命周期


从信号发送到信号处理函数的执行完毕,对于一个完整的信号生命周期(从信号发送到相应的处理函数执行完毕)来说,可以分为三个重要的阶段,这三个阶段由四个重要事件来刻画:信号诞生;信号在进程中注册完毕;信号在进程中的注销完毕;信号处理函数执行完毕。


下面阐述四个事件的实际意义:

信号"诞生"

信号的诞生指的是触发信号的事件发生(如检测到硬件异常、定时器超时以及调用信号发送函数kill()或sigqueue()等)。信号在目标进程中"注册";
进程的task_struct结构中有关于本进程中未决信号的数据成员:

struct sigpending pending:
struct sigpending{
	struct sigqueue *head, **tail;
	sigset_t signal;
};
第三个成员是进程中所有未决信号集,第一、第二个成员分别指向一个sigqueue类型的结构链(称之为"未决信号链表")的首尾,链表中的每个sigqueue结构刻画一个特定信号所携带的信息,并指向下一个sigqueue结构:
struct sigqueue{
	struct sigqueue *next;
	siginfo_t info;
}


信号的注册

信号在进程中注册指的就是信号值加入到进程的未决信号集中(sigpending结构的第二个成员sigset_t signal),并且加入未决信号链表的末尾。 只要信号在进程的未决信号集中,表明进程已经知道这些信号的存在,但还没来得及处理,或者该信号被进程阻塞。当一个实时信号发送给一个进程时,不管该信号是否已经在进程中注册,都会被再注册一次,因此,信号不会丢失,因此,实时信号又叫做"可靠信号"。这意味着同一个实时信号可以在同一个进程的未决信号链表中添加多次. 当一个非实时信号发送给一个进程时,如果该信号已经在进程中注册,则该信号将被丢弃,造成信号丢失。因此,非实时信号又叫做"不可靠信号"。这意味着同一个非实时信号在进程的未决信号链表中,至多占有一个sigqueue结构.

一个非实时信号诞生后,
(1)、如果发现相同的信号已经在目标结构中注册,则不再注册,对于进程来说,相当于不知道本次信号发生,信号丢失.
(2)、如果进程的未决信号中没有相同信号,则在进程中注册自己。

信号的注销
在进程执行过程中,会检测是否有信号等待处理(每次从系统空间返回到用户空间时都做这样的检查)。如果存在未决信号等待处理且该信号没有被进程阻塞,则在运行相应的信号处理函数前,进程会把信号在未决信号链中占有的结构卸掉。是否将信号从进程未决信号集中删除对于实时与非实时信号是不同的。对于非实时信号来说,由于在未决信号信息链中最多只占用一个sigqueue结构,因此该结构被释放后,应该把信号在进程未决信号集中删除(信号注销完毕);而对于实时信号来说,可能在未决信号信息链中占用多个sigqueue结构,因此应该针对占用sigqueue结构的数目区别对待:如果只占用一个sigqueue结构(进程只收到该信号一次),则应该把信号在进程的未决信号集中删除(信号注销完毕)。否则,不应该在进程的未决信号集中删除该信号(信号注销完毕)。 进程在执行信号相应处理函数之前,首先要把信号在进程中注销。

信号生命终止

进程注销信号后,立即执行相应的信号处理函数,执行完毕后,信号的本次发送对进程的影响彻底结束。



11. 关于可重入函数


在信号处理函数中应使用可重入函数。信号处理程序中应当使用可重入函数(注:所谓可重入函数是指一个可以被多个任务调用的过程,任务在调用时不必担心数据是否会出错)。因为进程在收到信号后,就将跳转到信号处理函数去接着执行。如果信号处理函数中使用了不可重入函数,那么信号处理函数可能会修改原来进程中不应该被修改的数据,这样进程从信号处理函数中返回接着执行时,可能会出现不可预料的后果。不可再入函数在信号处理函数中被视为不安全函数。

满足下列条件的函数多数是不可再入的:

  1. 使用静态的数据结构,如getlogin(),gmtime(),getgrgid(),getgrnam(),getpwuid()以及getpwnam()等等;
  2. 函数实现时,调用了malloc()或者free()函数;
  3. 实现时使用了标准I/O函数的。The Open Group视下列函数为可再入的:
    _exit()、access()、alarm()、cfgetispeed()、cfgetospeed()、
    cfsetispeed()、cfsetospeed()、chdir()、chmod()、chown() 、
    close()、creat()、dup()、dup2()、execle()、execve()、
    fcntl()、fork()、fpathconf()、fstat()、fsync()、getegid()、
    geteuid()、getgid()、getgroups()、getpgrp()、getpid()、
    getppid()、getuid()、kill()、link()、lseek()、mkdir()、
    mkfifo()、 open()、pathconf()、pause()、pipe()、raise()、
    read()、rename()、rmdir()、setgid()、setpgid()、setsid()、
    setuid()、 sigaction()、sigaddset()、sigdelset()、sigemptyset()、
    sigfillset()、sigismember()、signal()、sigpending()、
    sigprocmask()、sigsuspend()、sleep()、stat()、sysconf()、
    tcdrain()、tcflow()、tcflush()、tcgetattr()、tcgetpgrp()、
    tcsendbreak()、tcsetattr()、tcsetpgrp()、time()、times()、
    umask()、uname()、unlink()、utime()、wait()、waitpid()、
    write()。

即使信号处理函数使用的都是"安全函数",同样要注意进入处理函数时,首先要保存errno的值,结束时,再恢复原值。因为,信号处理过程中,errno值随时可能被改变。另外,longjmp()以及siglongjmp()没有被列为可重入函数,因为不能保证紧接着两个函数的其它调用是安全的。



12. 如何设计信号处理函数


信号处理函数最好遵从下面原则

  1. 一般而言,信号处理函数设计的越简单越好,因为当前代码的执行逻辑被打断,最好尽快恢复到刚才被打断之前的状态。从而避免竞争条件的产生。
  2. 在信号处理函数中,建议不要调用printf等与I/O相关的函数。以及一些慢设备操作。这样会使得信号处理函数的执行时间变长,可能,操作系统就会切换其它程序去在CPU上执行。但如果有特殊需要,则也可以使用
  3. 在信号处理函数中,不要使用任何不可重入的函数后面会说到。保证信号处理函数可以安全地执行完。并不会影响主逻辑执行


一个简单例子:

当键入Ctrl-C时候即程序收到SIGINT信号时进行处理。

//  signal 的函数原型
//  void (*signal(int sig , void (*func)(int)))(int);
//  至于这个怎么理解,这里就不再赘述了,请参考 《C Traps and Pitfalls》2.1节即理解函数声明。
// filename : simple_signal.cpp
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <errno.h>
#include <fcntl.h>

#define     MSG         "Catch signal SIGINT processing \n"
#define     MSG_END     "Finished process SIGINT return \n"

void  do_too_heavy_work () {
    long long s = 0 ;

    for (long long i = 0 ; i < 500000000L ; i++ ) {
        s += i ;    
    }
}

void sig_handler (int signuum ) {
    // 本程序只是为了来进行演示,
    // 在信号处理程序中,尽量不要调用与标准IO相关的和不可重入的函数。

    write ( STDOUT_FILENO , MSG , strlen (MSG) ) ;
    do_too_heavy_work();
    write ( STDOUT_FILENO , MSG_END , strlen (MSG_END) ) ;
}

int main() {

    // 注册信号处理函数

    if ( SIG_ERR == signal ( SIGINT , sig_handler ) ) {
        fprintf (stderr , "signal error ") , perror ("") ;
        exit (1) ;
    }

    // 让主程序不退出,挂起,等待信号产生
    while (1) {
        pause () ;
    }

    return EXIT_SUCCESS ;
}

程序会一直停着等待用户行为。当我们键入Ctrl-C时程序打印相关信息,之后程序自己退出。那么程序的执行流程就类似这样:

[tutu@localhost Linux-Book]$ gcc simple_signal.cpp
[tutu@localhost Linux-Book]$ ./a.out 
^CCatch signal SIGINT processing 
Finished process SIGINT return 
^CCatch signal SIGINT processing 
Finished process SIGINT return 
^CCatch signal SIGINT processing 
Finished process SIGINT return

这是一种古老的注册信号并设置信号处理函数的方式。现在我们使用新的信号注册函数即sigaction函数。它提供了更多的控制字段(旧的signal已经使用sigaction进行了实现。祥见glibc源码


为什么不用signal来进行信号注册

  1. signal 无法设置在执行信号处理程序时要屏蔽哪些信号的产生。
  2. signal 函数注册的信号处理函数只能携带很少的信息(也不常用),在信号处理函数进行信号处理时。
  3. signal 无法设置一些标志位来执行一些动作(后面再讲)。
  4. signal 只能设置所给信号的处理方式但sigaction还可以获取之前这个信号的处理方式


另一个例子,和上面的程序功能一样,但是使用sigaction进行处理。

// filename : simple_sigaction.cpp
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <errno.h>
#include <fcntl.h>

#define     MSG         "Catch signal SIGINT processing \n"
#define     MSG_END     "Finished process SIGINT return \n"

void  do_too_heavy_work () {
    long long s = 0 ;
    for (long long i = 0 ; i < 500000000L ; i++ ) {
        s += i ;    
    }
}
void sig_handler (int signuum ) {
    // 本程序只是为了来进行演示,
    // 在信号处理程序中,尽量不要调用与标准IO相关的,不可重入的函数。

    write ( STDOUT_FILENO , MSG , strlen (MSG) ) ;
    do_too_heavy_work();
    write ( STDOUT_FILENO , MSG_END , strlen (MSG_END) ) ;
}
int main() {
    // 注册信号处理函数
    struct sigaction  newact ;

    // 将信号处理函数执行期间掩码设置为空
    sigemptyset (&newact.sa_mask ) ;
    // 将标志设置为0即默认
    newact.sa_flags = 0 ;
    // 注册信号处理函数
    newact.sa_handler = sig_handler ;

    if ( 0 > sigaction ( SIGINT , &newact , NULL ) ) {
        fprintf (stderr , "sigaction error ") , perror ("") ;
        exit (1) ;
    }
    // 让主程序不退出,挂起,等待信号产生
    while (1) {
        pause () ;
    }
    return EXIT_SUCCESS ;
}

执行效果和刚才的一样


示例

[tutu@localhost Linux-Book]$ cat sigqueue_post_signal.cpp
// 信号发送端:
// filename : sigqueue_post_signal.cpp
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <string.h>
#include <strings.h>
#include <sys/types.h>
#include <errno.h>
#include <unistd.h>

int main (int argc , char * argv[]) {

    if ( 2 != argc ) {
        fprintf (stderr , "Bad argument!\nUsage ./post_signal pid\n") ;
        exit (1) ;
    }

    pid_t     pid = atoi ( argv[argc-1] ) ;
    printf ("Sending signal to %d , by using sigqueue\n" , pid) ;
    sigval_t     sigval ;
    sigval.sival_int = 8888 ;
    int errcode = 0 ;

    if ( 0 > ( errcode = sigqueue ( pid , SIGUSR1 , sigval ) )) {
        if ( ESRCH == errcode ) {
            fprintf (stderr , "No such process!\n") ;
            exit (1) ;
        } else {
            fprintf (stderr , "sigqueue error "),perror ("")  ;
            exit (1) ;
        }
    }
    printf ("Finished!\n") ;
    return 0 ;
}
[tutu@localhost Linux-Book]$ gcc sigqueue_post_signal.cpp -o post_signal
[tutu@localhost Linux-Book]$ cat sigqueue_wait.cpp
// filename sigqueue_wait.cpp
// 信号接收端
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>

// 注册SIGUSR1的信号处理函数
// man sigaction 显示,当前第三个参数无卵用,第一个参数是信号编号,第二个是携带的信息
void sig_handler (int signo , siginfo_t * info , void * extra ) {
    // print signo
    printf ("Catch SIGUSR1\n") ;
    printf ("signo is %d\n" , signo) ;    
    // print info -> si_value.sival_ptr
    printf ("sigval is %d\n" , info->si_value.sival_int ) ;
}

int main () {

    struct sigaction act ;
    act.sa_sigaction = sig_handler ;
    act.sa_flags = 0 ;
    act.sa_flags |= SA_SIGINFO ;
    sigemptyset (&act.sa_mask) ;

    printf ("My pid is %d\n" , getpid() ) ;

    if ( 0 > sigaction (SIGUSR1 , &act , NULL ) ) {
        fprintf (stderr , "sigaction error ") , perror ("") ;
        exit (1) ;
    }
    while (1) {
        pause () ;
    }
    return 0 ;
}

首先执行wait_signal,wait_signal 会打印自己的pid,再执行post_signal并传参数即pid,之后就可以看到效果了






13. 信号应用实例


linux下的信号应用并没有想象的那么恐怖,程序员所要做的最多只有三件事情:

  1. 安装信号(推荐使用sigaction());
  2. 实现三参数信号处理函数,handler(int signal,struct siginfo *info, void *);
  3. 发送信号,推荐使用sigqueue()。

实际上,对有些信号来说,只要安装信号就足够了(信号处理方式采用缺省或忽略)。其他可能要做的无非是与信号集相关的几种操作。


实例一:信号发送及处理

// 实现一个信号接收程序sigreceive(其中信号安装由sigaction())。

#include <signal.h>
#include <sys/types.h>
#include <unistd.h>

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

int main(int argc,char**argv)
{
        struct sigaction act;  
        int sig;
        sig=atoi(argv[1]);   
        sigemptyset(&act.sa_mask);
        act.sa_flags=SA_SIGINFO;
        act.sa_sigaction=new_op;
       
        if(sigaction(sig,&act,NULL) < 0)
        {
                printf("install sigal error\n");
        }
       
        while(1)
        {
                sleep(2);
                printf("wait for the signal\n");
        }
}

void new_op(int signum,siginfo_t *info,void *myact)
{
        printf("receive signal %d", signum);
        sleep(5);
}

说明,命令行参数为信号值,后台运行sigreceive signo &,可获得该进程的ID,假设为pid,然后再另一终端上运行kill -s signo pid验证信号的发送接收及处理。同时,可验证信号的排队问题.


实例二:信号传递附加信息

主要包括两个实例:

// 向进程本身发送信号,并传递指针参数

#include <signal.h>
#include <sys/types.h>
#include <unistd.h>

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

int main(int argc,char**argv)
{
        struct sigaction act;  
        union sigval mysigval;
        int i;
        int sig;
        pid_t pid;         
        char data[10];
        memset(data,0,sizeof(data));
        for(i=0;i < 5;i++)
                data[i]='2';

        mysigval.sival_ptr=data;      
        sig=atoi(argv[1]);
        pid=getpid();
       
        sigemptyset(&act.sa_mask);
        act.sa_sigaction=new_op;//三参数信号处理函数
        act.sa_flags=SA_SIGINFO;//信息传递开关,允许传说参数信息给new_op
        if(sigaction(sig,&act,NULL) < 0)
        {
                printf("install sigal error\n");
        }
        while(1)
        {
                sleep(2);
                printf("wait for the signal\n");
                sigqueue(pid,sig,mysigval);//向本进程发送信号,并传递附加信息
        }
}

void new_op(int signum,siginfo_t *info,void *myact)//三参数信号处理函数的实现
{
        int i;
        for(i=0;i<10;i++)
        {
                printf("%c\n ",(*( (char*)((*info).si_ptr)+i)));
        }
        printf("handle signal %d over;",signum);
}

这个例子中,信号实现了附加信息的传递,信号究竟如何对这些信息进行处理则取决于具体的应用。


不同进程间传递整型参数:

把上面例子的信号发送和接收放在两个程序中,并且在发送过程中传递整型参数

// 信号接收程序:

#include <signal.h>
#include <sys/types.h>
#include <unistd.h>

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

int main(int argc,char**argv)
{
        struct sigaction act;
        int sig;
        pid_t pid;               
        pid=getpid();
        sig=atoi(argv[1]);           

        sigemptyset(&act.sa_mask);

        act.sa_sigaction=new_op;
        act.sa_flags=SA_SIGINFO;
        if(sigaction(sig,&act,NULL)<0)
        {
                printf("install sigal error\n");
        }
        while(1)
        {
                sleep(2);
                printf("wait for the signal\n");
        }
}

void new_op(int signum,siginfo_t *info,void *myact)
{
        printf("the int value is %d \n",info->si_int);
}

 

// 信号发送程序:
// 命令行第二个参数为信号值,第三个参数为接收进程ID。

#include <signal.h>
#include <sys/time.h>
#include <unistd.h>
#include <sys/types.h>

int main(int argc,char**argv)
{
        pid_t pid;
        int signum;
        union sigval mysigval;
        signum=atoi(argv[1]);
        pid=(pid_t)atoi(argv[2]);
        mysigval.sival_int=8;//不代表具体含义,只用于说明问题
        if(sigqueue(pid,signum,mysigval)==-1)
                printf("send error\n");
        sleep(2);
}

注:实例2的两个例子侧重点在于用信号来传递信息,目前关于在linux下通过信号传递信息的实例非常少,倒是Unix下有一些,但传递的基本上都是关于传递一个整数.


实例三:信号阻塞及信号集操作

#include "signal.h"
#include "unistd.h"

static void my_op(int);

int main()
{
        sigset_t new_mask,old_mask,pending_mask;
        struct sigaction act;
        sigemptyset(&act.sa_mask);
        act.sa_flags=SA_SIGINFO;
        act.sa_sigaction=(void*)my_op;
        if(sigaction(SIGRTMIN+10,&act,NULL))
                printf("install signal SIGRTMIN+10 error\n");
        sigemptyset(&new_mask);
        sigaddset(&new_mask,SIGRTMIN+10);
        if(sigprocmask(SIG_BLOCK, &new_mask,&old_mask))
                printf("block signal SIGRTMIN+10 error\n");
        sleep(10);
        printf("now begin to get pending mask and unblock SIGRTMIN+10\n");
        if(sigpending(&pending_mask)<0)
                printf("get pending mask error\n");
        if(sigismember(&pending_mask,SIGRTMIN+10))
                printf("signal SIGRTMIN+10 is pending\n");
        if(sigprocmask(SIG_SETMASK,&old_mask,NULL)<0)
                printf("unblock signal error\n");
        printf("signal unblocked\n");
        sleep(10);
}

static void my_op(int signum)
{
        printf("receive signal %d \n",signum);
}

编译该程序,并以后台方式运行。在另一终端向该进程发送信号(运行kill -s 42 pid,SIGRTMIN+10为42),查看结果可以看出几个关键函数的运行机制,信号集相关操作比较简单。


其他示例

#include <stdio.h>  
#include <unistd.h>  
#include <stdlib.h>  
#include <errno.h>  
#include <signal.h>  
  
void do_sig(int a)  
{  
    printf("Hi, SIGINT, how do you do !\n");  
}  
  
int main(void)  
{  
    if (signal(SIGINT, do_sig) == SIG_ERR) {  
        perror("signal");  
        exit(1);  
    }  
  
    while (1) {  
        printf("---------------------\n");  
        sleep(1);  
    }  
  
    return 0;  
}

捕捉信号

#include <signal.h>  
#include <stdio.h>  
#include <errno.h>  
#include <stdlib.h>  
#include <unistd.h>  
  
typedef void (*sighandler_t) (int);  
  
void catchsigint(int signo)  
{  
    printf("-----------------catch\n");  
}  
  
int main(void)  
{  
    sighandler_t handler;  
  
    handler = signal(SIGINT, catchsigint);  
    if (handler == SIG_ERR) {  
        perror("signal error");  
        exit(1);  
    }  
  
    while (1);  
      
    return 0;  
}  

捕捉信号

#include <stdio.h>
#include <signal.h>

//捕捉函数
void dosig(int num)
{
    printf("num = %d\n", num);
    printf("dosig function process finish\n");
}

int main(void)
{
    int i = 0;
    struct sigaction act, oldact;
    /*构造sigaction结构体*/
    act.sa_handler = dosig;         
    sigemptyset(&act.sa_mask);
    act.sa_flags = 0;

    sigaction(SIGINT, &act, NULL);//linux系统函数 //注册信号处理函数
    //signal(SIGINT, dosig);   //C标准库   signal -》sigaction

    while (1) 
    {
        printf("****************** %d\n", i++);
        if (i == 5) 
        {
            oldact.sa_handler = SIG_IGN; //SIG_DFL
            sigemptyset(&oldact.sa_mask);
            oldact.sa_flags = 0;
            sigaction(SIGINT, &oldact, NULL);
        }
            //sigaction(SIGINT, &oldact, NULL);
        sleep(1);
    }
    return 0;
}

捕捉信号并传递参数

#include <signal.h>
#include <stdio.h>
#include <unistd.h>

void dosig(int num, siginfo_t *siginfo, void *p)
{
    printf("num = %d\n", num);
    printf("siginfo->si_int = %d\n", siginfo->si_int);
}
int main(void)
{
    struct sigaction act;
    pid_t pid;

    act.sa_sigaction = dosig;
    sigemptyset(&act.sa_mask);
    act.sa_flags = SA_SIGINFO;

    pid = fork();

    if (pid == 0) {
        union sigval s;
        s.sival_int = 20;
        sleep(5);
        sigqueue(getppid(), SIGINT, s);
        return 0;
    }

    sigaction(SIGINT, &act, NULL);
    while (1) {
        printf("******\n");
        sleep(1);
    }
    return 0;
}

为某个信号设置捕捉函数 【sigaction1.c】

#include <stdio.h>  
#include <stdlib.h>  
#include <unistd.h>  
#include <signal.h>  
  
/*自定义的信号捕捉函数*/  
void sig_int(int signo)  
{  
    printf("catch signal SIGINT\n");//单次打印  
    sleep(10);  
    printf("----slept 10 s\n");  
}  
  
int main(void)  
{  
    struct sigaction act;         
  
    act.sa_handler = sig_int;  
    act.sa_flags = 0;  
    sigemptyset(&act.sa_mask);      //不屏蔽任何信号  
    sigaddset(&act.sa_mask, SIGQUIT);  
  
    sigaction(SIGINT, &act, NULL);  
  
    printf("------------main slept 10\n");  
    sleep(10);  
  
    while(1);//该循环只是为了保证有足够的时间来测试函数特性  
  
    return 0;  
}  

验证在信号处理函数执行期间,该信号多次递送,那么只在处理函数之行结束后,处理一次。 【sigaction2.c】

/*自动屏蔽本信号,调用完毕后屏蔽自动解除*/  
  
#include <stdio.h>  
#include <signal.h>  
#include <unistd.h>  
  
/*自定义的信号捕捉函数*/  
void sig_int(int signo)  
{  
    printf("catch signal SIGINT\n");  
    sleep(10);          //模拟信号处理函数执行很长时间  
    printf("end of handler\n");  
}  
  
int main(void)  
{  
    struct sigaction act, old;        
  
    act.sa_handler = sig_int;  
    sigemptyset(&act.sa_mask);      //依然不屏蔽任何信号  
    act.sa_flags = 0;  
  
    sigaction(SIGINT, &act, &old);  //注册信号处理函数  
  
    while(1);  
  
    sigaction(SIGINT, &old, NULL);  //注册信号处理函数  
  
    return 0;  
}  

验证sa_mask在捕捉函数执行期间的屏蔽作用。 【sigaction3.c】

/*当执行SIGINT信号处理函数期间 
 *多次收到SIGQUIT信号都将被屏蔽(阻塞) 
 *SIGINT信号处理函数处理完,立刻解除对 
 *SIGQUIT信号的屏蔽,由于没有捕捉该信号, 
 *将立刻执行该信号的默认动作,程序退出 
 */  
#include <stdio.h>  
#include <signal.h>  
#include <unistd.h>  
  
void sig_int(int signo)  
{  
    printf("catch signal SIGINT\n");  
    sleep(10);          //模拟信号处理函数执行很长时间  
    printf("end of handler\n");  
}  
  
int main(void)  
{  
    struct sigaction act;         
  
  
    act.sa_handler = sig_int;  
    sigemptyset(&act.sa_mask);        
    sigaddset(&act.sa_mask, SIGQUIT);     
  
    /*将SIGQUIT加入信号屏蔽集,这就导致,在调用信号处理函数期间 
     *不仅不响应SIGINT信号本身,还不响应SIGQUIT*/  
    act.sa_flags = 0;  
  
    sigaction(SIGINT, &act, NULL);      //注册信号SIGINT捕捉函数  
  
    while(1);  
  
    return 0;  
}  


打印未决信号集

/**************************************************************/
#include <stdio.h>
#include <signal.h>

void printsigpend(void)
{
    int i;
    sigset_t sigpend;

    sigpending(&sigpend);
    for (i = 1; i < 32; i++) {
        if (sigismember(&sigpend, i) == 1)
            printf("1");
        else
            printf("0");
    }
    printf("\n");
}
int main(void)
{
    sigset_t set, oldset;
    sigemptyset(&set);
    sigaddset(&set, SIGINT);
    sigaddset(&set, SIGQUIT);
    sigaddset(&set, 15);
    sigaddset(&set, 9);
    sigprocmask(SIG_SETMASK, &set, &oldset);

    while (1) {
        printsigpend();
        sleep(1);
    }
    return 0;
}


信号等待

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>

void dowait(int num)
{
    while (waitpid(0, NULL, WNOHANG) > 0)
        ;    
}
void sys_err(char *str)
{
    perror(str);
    exit(-1);
}
int main(void)
{
    int i;
    pid_t pid;
    srand(time(NULL));

    struct sigaction act;
    act.sa_handler = dowait;
    sigemptyset(&act.sa_mask);
    act.sa_flags = 0;
    sigaction(SIGCHLD, &act, NULL);

    for (i = 0; i < 100; i++) 
    {
        pid = fork();
        if (pid == 0)
            break;
        else if (pid < 0)
            sys_err("fork");
    }
    if (pid == 0) {
        /*in child*/
        int n = rand() % 5;
        while (n--) {
            printf("I am %u\n", getpid());
            sleep(1);
        }
        return 0;
    }

    /* in parent*/
    while (1) {
        printf("I am Parent\n");
        sleep(1);
    }
    return 0;
}






  • 3
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值