Linux信号详解

<h1>1. 信号及信号来源</h1> <p><strong>信号本质</strong></p> <p>信号是在软件层次上对中断机制的一种模拟。在原理上,一个进程收到一个信号与处理器收到一个中断请求可以说是一样的。信号是异步的,一个进程不必通过任何操作来等待信号的到达。事实上,进程也不知道信号到底什么时候到达。</p> <p>信号是进程间通信机制中唯一的异步通信机制,可以看作是异步通知,通知接收信号的进程有哪些事情发生了。信号机制经过POSIX实时扩展后,功能更加强大,除了基本通知功能外,还可以传递附加信息。</p> <p><strong>信号来源</strong></p> <p>信号事件的发生有两个来源:硬件来源(比如我们按下了键盘或者其它硬件故障);软件来源,最常用发送信号的系统函数是kill、raise、 alarm和setitimer以及sigqueue函数,软件来源还包括一些非法运算等操作。</p> <h1>2. 信号的种类</h1> <p>可以从两个不同的分类角度对信号进行分类:(1)可靠性方面:可靠信号与不可靠信号;(2)与时间的关系上:实时信号与非实时信号。在《Linux环境进程间通信(一):管道及有名管道》的附1中列出了系统所支持的所有信号。</p> <h2>2.1. 可靠信号与不可靠信号</h2> <p><strong>&quot;</strong><strong>不可靠信号</strong><strong>&quot;</strong></p> <p>Linux信号机制基本上是从Unix系统中继承过来的。早期Unix系统中的信号机制比较简单和原始,后来在实践中暴露出一些问题。因此,把那些建立在早期机制上的信号叫做&quot;不可靠信号&quot;,信号值小于SIGRTMIN(Red hat 7.2中,SIGRTMIN=32,SIGRTMAX=63)的信号都是不可靠信号。这就是&quot;不可靠信号&quot;的来源。它的主要问题是:</p> <ul> <li>进程每次处理信号后,就将对信号的响应设置为默认动作。在某些情况下,将导致对信号的错误处理;因此,用户如果不希望这样的操作,那么就要在信号处理函数结尾再一次调用signal(),重新安装该信号。</li> <li>信号可能丢失,后面将对此详细阐述。</li> </ul> <p>因此,早期unix下的不可靠信号主要指的是进程可能对信号做出错误的反应以及信号可能丢失。</p> <p>Linux支持不可靠信号,但是对不可靠信号机制做了改进:在调用完信号处理函数后,不必重新调用该信号的安装函数(信号安装函数是在可靠机制上的实现)。因此,Linux下的不可靠信号问题主要指的是信号可能丢失。</p> <p><strong>&quot;</strong><strong>可靠信号</strong><strong>&quot;</strong></p> <p>随着时间的发展,实践证明了有必要对信号的原始机制加以改进和扩充。所以,后来出现的各种Unix版本分别在这方面进行了研究,力图实现&quot;可靠信号&quot;。由于原来定义的信号已有许多应用,不好再做改动,最终只好又新增加了一些信号,并在一开始就把它们定义为可靠信号,这些信号支持排队,不会丢失。同时,信号的发送和安装也出现了新版本:信号发送函数sigqueue()及信号安装函数sigaction()。POSIX.4对可靠信号机制做了标准化。但是,POSIX只对可靠信号机制应具有的功能以及信号机制的对外接口做了标准化,对信号机制的实现没有作具体的规定。</p> <p>信号值位于SIGRTMIN和SIGRTMAX之间的信号都是可靠信号,可靠信号克服了信号可能丢失的问题。Linux在支持新版本的信号安装函数sigation()以及信号发送函数sigqueue()的同时,仍然支持早期的signal()信号安装函数,支持信号发送函数kill()。</p> <p>注:不要有这样的误解:由sigqueue()发送、sigaction安装的信号就是可靠的。事实上,可靠信号是指后来添加的新信号(信号值位于SIGRTMIN及SIGRTMAX之间);不可靠信号是信号值小于SIGRTMIN的信号。信号的可靠与不可靠只与信号值有关,与信号的发送及安装函数无关。目前linux中的signal()是通过sigation()函数实现的,因此,即使通过signal()安装的信号,在信号处理函数的结尾也不必再调用一次信号安装函数。同时,由signal()安装的实时信号支持排队,同样不会丢失。</p> <p>对于目前linux的两个信号安装函数:signal()及sigaction()来说,它们都不能把SIGRTMIN以前的信号变成可靠信号(都不支持排队,仍有可能丢失,仍然是不可靠信号),而且对SIGRTMIN以后的信号都支持排队。这两个函数的最大区别在于,经过sigaction安装的信号都能传递信息给信号处理函数(对所有信号这一点都成立),而经过signal安装的信号却不能向信号处理函数传递信息。对于信号发送函数来说也是一样的。</p> <h2>2.2. 实时信号与非实时信号</h2> <p>早期Unix系统只定义了32种信号,Ret hat7.2支持64种信号,编号0-63(SIGRTMIN=31,SIGRTMAX=63),将来可能进一步增加,这需要得到内核的支持。前32种信号已经有了预定义值,每个信号有了确定的用途及含义,并且每种信号都有各自的缺省动作。如按键盘的CTRL ^C时,会产生SIGINT信号,对该信号的默认反应就是进程终止。后32个信号表示实时信号,等同于前面阐述的可靠信号。这保证了发送的多个实时信号都被接收。实时信号是POSIX标准的一部分,可用于应用进程。</p> <p>非实时信号都不支持排队,都是不可靠信号;实时信号都支持排队,都是可靠信号。</p> <h2>2.3. 信号含义</h2> <p>在POSIX.1-1990标准中的信号列表 <table cellspacing="0" cellpadding="0" border="1"><tbody> <tr> <td valign="top"> <p>信号</p> </td> <td valign="top"> <p>值</p> </td> <td valign="top"> <p>动作</p> </td> <td valign="top"> <p>说明</p> </td> </tr> <tr> <td valign="top"> <p>SIGHUP</p> </td> <td valign="top"> <p>1</p> </td> <td valign="top"> <p>Term</p> </td> <td valign="top"> <p>终端控制进程结束(终端连接断开)</p> </td> </tr> <tr> <td valign="top"> <p>SIGINT</p> </td> <td valign="top"> <p>2</p> </td> <td valign="top"> <p>Term</p> </td> <td valign="top"> <p>用户发送INTR字符(Ctrl+C)触发</p> </td> </tr> <tr> <td valign="top"> <p>SIGQUIT</p> </td> <td valign="top"> <p>3</p> </td> <td valign="top"> <p>Core</p> </td> <td valign="top"> <p>用户发送QUIT字符(Ctrl+/)触发</p> </td> </tr> <tr> <td valign="top"> <p>SIGILL</p> </td> <td valign="top"> <p>4</p> </td> <td valign="top"> <p>Core</p> </td> <td valign="top"> <p>非法指令(程序错误、试图执行数据段、栈溢出等)</p> </td> </tr> <tr> <td valign="top"> <p>SIGABRT</p> </td> <td valign="top"> <p>6</p> </td> <td valign="top"> <p>Core</p> </td> <td valign="top"> <p>调用abort函数触发</p> </td> </tr> <tr> <td valign="top"> <p>SIGFPE</p> </td> <td valign="top"> <p>8</p> </td> <td valign="top"> <p>Core</p> </td> <td valign="top"> <p>算术运行错误(浮点运算错误、除数为零等)</p> </td> </tr> <tr> <td valign="top"> <p>SIGKILL</p> </td> <td valign="top"> <p>9</p> </td> <td valign="top"> <p>Term</p> </td> <td valign="top"> <p>无条件结束程序(不能被捕获、阻塞或忽略)</p> </td> </tr> <tr> <td valign="top"> <p>SIGSEGV</p> </td> <td valign="top"> <p>11</p> </td> <td valign="top"> <p>Core</p> </td> <td valign="top"> <p>无效内存引用(试图访问不属于自己的内存空间、对只读内存空间进行写操作)</p> </td> </tr> <tr> <td valign="top"> <p>SIGPIPE</p> </td> <td valign="top"> <p>13</p> </td> <td valign="top"> <p>Term</p> </td> <td valign="top"> <p>消息管道损坏(FIFO/Socket通信时,管道未打开而进行写操作)</p> </td> </tr> <tr> <td valign="top"> <p>SIGALRM</p> </td> <td valign="top"> <p>14</p> </td> <td valign="top"> <p>Term</p> </td> <td valign="top"> <p>时钟定时信号</p> </td> </tr> <tr> <td valign="top"> <p>SIGTERM</p> </td> <td valign="top"> <p>15</p> </td> <td valign="top"> <p>Term</p> </td> <td valign="top"> <p>结束程序(可以被捕获、阻塞或忽略)</p> </td> </tr> <tr> <td valign="top"> <p>SIGUSR1</p> </td> <td valign="top"> <p>30,10,16</p> </td> <td valign="top"> <p>Term</p> </td> <td valign="top"> <p>用户保留</p> </td> </tr> <tr> <td valign="top"> <p>SIGUSR2</p> </td> <td valign="top"> <p>31,12,17</p> </td> <td valign="top"> <p>Term</p> </td> <td valign="top"> <p>用户保留</p> </td> </tr> <tr> <td valign="top"> <p>SIGCHLD</p> </td> <td valign="top"> <p>20,17,18</p> </td> <td valign="top"> <p>Ign</p> </td> <td valign="top"> <p>子进程结束(由父进程接收)</p> </td> </tr> <tr> <td valign="top"> <p>SIGCONT</p> </td> <td valign="top"> <p>19,18,25</p> </td> <td valign="top"> <p>Cont</p> </td> <td valign="top"> <p>继续执行已经停止的进程(不能被阻塞)</p> </td> </tr> <tr> <td valign="top"> <p>SIGSTOP</p> </td> <td valign="top"> <p>17,19,23</p> </td> <td valign="top"> <p>Stop</p> </td> <td valign="top"> <p>停止进程(不能被捕获、阻塞或忽略)</p> </td> </tr> <tr> <td valign="top"> <p>SIGTSTP</p> </td> <td valign="top"> <p>18,20,24</p> </td> <td valign="top"> <p>Stop</p> </td> <td valign="top"> <p>停止进程(可以被捕获、阻塞或忽略)</p> </td> </tr> <tr> <td valign="top"> <p>SIGTTIN</p> </td> <td valign="top"> <p>21,21,26</p> </td> <td valign="top"> <p>Stop</p> </td> <td valign="top"> <p>后台程序从终端中读取数据时触发</p> </td> </tr> <tr> <td valign="top"> <p>SIGTTOU</p> </td> <td valign="top"> <p>22,22,27</p> </td> <td valign="top"> <p>Stop</p> </td> <td valign="top"> <p>后台程序向终端中写数据时触发</p> </td> </tr> </tbody></table> </p> <p><strong>注</strong>:其中<code>SIGKILL</code>和<code>SIGSTOP</code>信号不能被捕获、阻塞或忽略。</p> <h1>3. 信号机制</h1> <p>函数运行在用户态,当遇到系统调用、中断或是异常的情况时,程序会进入内核态。信号涉及到了这两种状态之间的转换,过程可以先看一下下面的示意图:</p> <p><a href="http://hutaow.com/images/articles/201310/linux_signal_flow.png"><img title="clip_image002" style="border-top: 0px; border-right: 0px; background-image: none; border-bottom: 0px; padding-top: 0px; padding-left: 0px; margin: 0px; border-left: 0px; display: inline; padding-right: 0px" border="0" alt="clip_image002" src="http://static.oschina.net/uploads/img/201412/26083025_NcVy.jpg" width="244" height="89" /></a></p> <h1>4. 进程对信号的响应</h1> <p>进程可以通过三种方式来响应一个信号:(1)忽略信号,即对信号不做任何处理,其中,有两个信号不能忽略:SIGKILL及SIGSTOP;(2)捕捉信号。定义信号处理函数,当信号发生时,执行相应的处理函数;(3)执行缺省操作,Linux对每种信号都规定了默认操作,详细情况请参考[2]以及其它资料。注意,进程对实时信号的缺省反应是进程终止。</p> <p>Linux究竟采用上述三种方式的哪一个来响应信号,取决于传递给相应API函数的参数。</p> <h1>5. 信号的发送</h1> <p>发送信号的主要函数有:kill()、raise()、 sigqueue()、alarm()、setitimer()以及abort()。</p> <h2>5.1. kill</h2> <p>#include &lt;sys/types.h&gt;</p> <p>#include &lt;signal.h&gt;</p> <p>int kill(pid_t pid,int signo) <table cellspacing="0" cellpadding="0" border="1"><tbody> <tr> <td valign="top" width="29%"> <p>参数pid的值</p> </td> <td valign="top" width="70%"> <p>信号的接收进程</p> </td> </tr> <tr> <td valign="top" width="29%"> <p>pid&gt;0</p> </td> <td valign="top" width="70%"> <p>进程ID为pid的进程</p> </td> </tr> <tr> <td valign="top" width="29%"> <p>pid=0</p> </td> <td valign="top" width="70%"> <p>同一个进程组的进程</p> </td> </tr> <tr> <td valign="top" width="29%"> <p>pid&lt;0 pid!=-1</p> </td> <td valign="top" width="70%"> <p>进程组ID为 -pid的所有进程</p> </td> </tr> <tr> <td valign="top" width="29%"> <p>pid=-1</p> </td> <td valign="top" width="70%"> <p>除发送进程自身外,所有进程ID大于1的进程</p> </td> </tr> </tbody></table> </p> <p>Sinno是信号值,当为0时(即空信号),实际不发送任何信号,但照常进行错误检查。因此,可用于检查目标进程是否存在,以及当前进程是否具有向目标发送信号的权限(root权限的进程可以向任何进程发送信号,非root权限的进程只能向属于同一个session或者同一个用户的进程发送信号)。</p> <p>Kill()最常用于pid&gt;0时的信号发送,调用成功返回 0; 否则,返回 -1。 注:对于pid&lt;0时的情况,对于哪些进程将接受信号,各种版本说法不一,其实很简单,参阅内核源码kernal/signal.c即可,上表中的规则是参考red hat 7.2。</p> <h2>5.2. raise</h2> <p>#include &lt;signal.h&gt;</p> <p>int raise(int signo)</p> <p>向进程本身发送信号,参数为即将发送的信号值。调用成功返回 0;否则,返回 -1。</p> <h2>5.3. sigqueue</h2> <p>#include &lt;sys/types.h&gt;</p> <p>#include &lt;signal.h&gt;</p> <p>int sigqueue(pid_t pid, int sig, const union sigval val)</p> <p>调用成功返回 0;否则,返回 -1。</p> <p>sigqueue()是比较新的发送信号系统调用,主要是针对实时信号提出的(当然也支持前32种),支持信号带有参数,与函数sigaction()配合使用。</p> <p>sigqueue的第一个参数是指定接收信号的进程ID,第二个参数确定即将发送的信号,第三个参数是一个联合数据结构union sigval,指定了信号传递的参数,即通常所说的4字节值。</p> <p>typedef union sigval {</p> <p>int sival_int;</p> <p>void *sival_ptr;</p> <p>}sigval_t;</p> <p>sigqueue()比kill()传递了更多的附加信息,但sigqueue()只能向一个进程发送信号,而不能发送信号给一个进程组。如果signo=0,将会执行错误检查,但实际上不发送任何信号,0值信号可用于检查pid的有效性以及当前进程是否有权限向目标进程发送信号。</p> <p>在调用sigqueue时,sigval_t指定的信息会拷贝到3参数信号处理函数(3参数信号处理函数指的是信号处理函数由sigaction安装,并设定了sa_sigaction指针,稍后将阐述)的siginfo_t结构中,这样信号处理函数就可以处理这些信息了。由于sigqueue系统调用支持发送带参数信号,所以比kill()系统调用的功能要灵活和强大得多。</p> <p>注:sigqueue()发送非实时信号时,第三个参数包含的信息仍然能够传递给信号处理函数; sigqueue()发送非实时信号时,仍然不支持排队,即在信号处理函数执行过程中到来的所有相同信号,都被合并为一个信号。</p> <h2>5.4. alarm</h2> <p>#include &lt;unistd.h&gt;</p> <p>unsigned int alarm(unsigned int seconds)</p> <p>专门为SIGALRM信号而设,在指定的时间seconds秒后,将向进程本身发送SIGALRM信号,又称为闹钟时间。进程调用alarm后,任何以前的alarm()调用都将无效。如果参数seconds为零,那么进程内将不再包含任何闹钟时间。</p> <p>返回值,如果调用alarm()前,进程中已经设置了闹钟时间,则返回上一个闹钟时间的剩余时间,否则返回0。</p> <h2>5.5. setitimer</h2> <p>#include &lt;sys/time.h&gt;</p> <p>int setitimer(int which, const struct itimerval *value, struct itimerval *ovalue));</p> <p>setitimer()比alarm功能强大,支持3种类型的定时器:</p> <ul> <li>ITIMER_REAL:设定绝对时间;经过指定的时间后,内核将发送SIGALRM信号给本进程;</li> <li>ITIMER_VIRTUAL:设定程序执行时间;经过指定的时间后,内核将发送SIGVTALRM信号给本进程;</li> <li>ITIMER_PROF:设定进程执行以及内核因本进程而消耗的时间和,经过指定的时间后,内核将发送ITIMER_VIRTUAL信号给本进程;</li> </ul> <p>Setitimer()第一个参数which指定定时器类型(上面三种之一);第二个参数是结构itimerval的一个实例,结构itimerval形式见附录1。第三个参数可不做处理。</p> <p>Setitimer()调用成功返回0,否则返回-1。</p> <h2>5.6. abort</h2> <p>#include &lt;stdlib.h&gt;</p> <p>void abort(void);</p> <p>向进程发送SIGABORT信号,默认情况下进程会异常退出,当然可定义自己的信号处理函数。即使SIGABORT被进程设置为阻塞信号,调用abort()后,SIGABORT仍然能被进程接收。该函数无返回值。</p> <h1>6. 信号的安装(设置信号关联动作)</h1> <p>如果进程要处理某一信号,那么就要在进程中安装该信号。安装信号主要用来确定信号值及进程针对该信号值的动作之间的映射关系,即进程将要处理哪个信号;该信号被传递给进程时,将执行何种操作。</p> <p>linux主要有两个函数实现信号的安装:signal()、sigaction()。其中signal()在可靠信号系统调用的基础上实现,是库函数。它只有两个参数,不支持信号传递信息,主要是用于前32种非实时信号的安装;而sigaction()是较新的函数(由两个系统调用实现:sys_signal以及sys_rt_sigaction),有三个参数,支持信号传递信息,主要用来与 sigqueue() 系统调用配合使用,当然,sigaction()同样支持非实时信号的安装。sigaction()优于signal()主要体现在支持信号带有参数。</p> <h2>6.1. signal</h2> <p>#include &lt;signal.h&gt;</p> <p>void (*signal(int signum, void (*handler))(int)))(int);</p> <p>如果该函数原型不容易理解的话,可以参考下面的分解方式来理解:</p> <p>typedef void (*sighandler_t)(int);</p> <p>sighandler_t signal(int signum, sighandler_t handler));</p> <p>第一个参数指定信号的值,第二个参数指定针对前面信号值的处理,可以忽略该信号(参数设为SIG_IGN);可以采用系统默认方式处理信号(参数设为SIG_DFL);也可以自己实现处理方式(参数指定一个函数地址)。</p> <p>如果signal()调用成功,返回最后一次为安装信号signum而调用signal()时的handler值;失败则返回SIG_ERR。</p> <h2>6.2. sigaction</h2> <p>#include &lt;signal.h&gt;</p> <p>int sigaction(int signum,const struct sigaction *act,struct sigaction *oldact));</p> <p>sigaction函数用于改变进程接收到特定信号后的行为。该函数的第一个参数为信号的值,可以为除SIGKILL及SIGSTOP外的任何一个特定有效的信号(为这两个信号定义自己的处理函数,将导致信号安装错误)。第二个参数是指向结构sigaction的一个实例的指针,在结构sigaction的实例中,指定了对特定信号的处理,可以为空,进程会以缺省方式对信号处理;第三个参数oldact指向的对象用来保存原来对相应信号的处理,可指定oldact为NULL。如果把第二、第三个参数都设为NULL,那么该函数可用于检查信号的有效性。</p> <p>第二个参数最为重要,其中包含了对指定信号的处理、信号所传递的信息、信号处理函数执行过程中应屏蔽掉哪些函数等等。</p> <p>sigaction结构定义如下:</p> <p>struct sigaction {</p> <p>void (*sa_handler)(int);</p> <p>void (*sa_sigaction)(int, siginfo_t *, void *);</p> <p>sigset_t sa_mask;</p> <p>int sa_flags;</p> <p>void (*sa_restorer)(void);</p> <p>};</p> <p>其中,sa_restorer,已过时,POSIX不支持它,不应再被使用。</p> <p>1、sa_handler以及sa_sigaction指定信号关联函数,即用户指定的信号处理函数。可以是用户自定义的处理函数外,还可以为SIG_DFL(采用缺省的处理方式),也可以为SIG_IGN(忽略信号)。</p> <p>2、由sa_handler指定的处理函数只有一个参数,即信号值,所以信号不能传递除信号值之外的任何信息;由sa_sigaction是指定的信号处理函数带有三个参数,是为实时信号而设的(当然同样支持非实时信号),它指定一个3参数信号处理函数。第一个参数为信号值,第三个参数没有使用(posix没有规范使用该参数的标准),第二个参数是指向siginfo_t结构的指针,结构中包含信号携带的数据值,参数所指向的结构如下:</p> <p>siginfo_t {</p> <p>int si_signo; /* 信号值,对所有信号有意义*/</p> <p>int si_errno; /* errno值,对所有信号有意义*/</p> <p>union{ /* 联合数据结构,不同成员适应不同信号 */</p> <p>//确保分配足够大的存储空间</p> <p>int _pad[SI_PAD_SIZE];</p> <p>//对SIGKILL有意义的结构</p> <p>struct{</p> <p>...</p> <p>}...</p> <p>... ...</p> <p>... ...</p> <p>//对SIGILL, SIGFPE, SIGSEGV, SIGBUS有意义的结构</p> <p>struct{</p> <p>...</p> <p>}...</p> <p>... ...</p> <p>}</p> <p>}</p> <p>注:为了更便于阅读,在说明问题时常把该结构表示为附录2所表示的形式。</p> <p>siginfo_t结构中的联合数据成员确保该结构适应所有的信号,比如对于实时信号来说,则实际采用下面的结构形式:</p> <p>typedef struct {</p> <p>int si_signo;</p> <p>int si_errno;</p> <p>int si_code;</p> <p>union sigval si_value;</p> <p>} siginfo_t;</p> <p>结构的第四个域同样为一个联合数据结构:</p> <p>union sigval {</p> <p>int sival_int;</p> <p>void *sival_ptr;</p> <p>}</p> <p>采用联合数据结构,说明siginfo_t结构中的si_value要么持有一个4字节的整数值,要么持有一个指针,这就构成了与信号相关的数据。在信号的处理函数中,包含这样的信号相关数据指针,但没有规定具体如何对这些数据进行操作,操作方法应该由程序开发人员根据具体任务事先约定。</p> <p>前面在讨论系统调用sigqueue发送信号时,sigqueue的第三个参数就是sigval联合数据结构,当调用sigqueue时,该数据结构中的数据就将拷贝到信号处理函数的第二个参数中。这样,在发送信号同时,就可以让信号传递一些附加信息。信号可以传递信息对程序开发是非常有意义的。</p> <p>信号参数的传递过程可图示如下:</p> <p><a href="http://static.oschina.net/uploads/img/201412/26083025_f9tq.gif"><img title="clip_image004" style="border-top: 0px; border-right: 0px; background-image: none; border-bottom: 0px; padding-top: 0px; padding-left: 0px; margin: 0px; border-left: 0px; display: inline; padding-right: 0px" border="0" alt="clip_image004" src="http://static.oschina.net/uploads/img/201412/26083025_ACyd.gif" width="244" height="77" /></a></p> <p>3、sa_mask指定在信号处理程序执行过程中,哪些信号应当被阻塞。缺省情况下当前信号本身被阻塞,防止信号的嵌套发送,除非指定SA_NODEFER或者SA_NOMASK标志位。</p> <p>注:请注意sa_mask指定的信号阻塞的前提条件,是在由sigaction()安装信号的处理函数执行过程中由sa_mask指定的信号才被阻塞。</p> <p>4、sa_flags中包含了许多标志位,包括刚刚提到的SA_NODEFER及SA_NOMASK标志位。另一个比较重要的标志位是SA_SIGINFO,当设定了该标志位时,表示信号附带的参数可以被传递到信号处理函数中,因此,应该为sigaction结构中的sa_sigaction指定处理函数,而不应该为sa_handler指定信号处理函数,否则,设置该标志变得毫无意义。即使为sa_sigaction指定了信号处理函数,如果不设置SA_SIGINFO,信号处理函数同样不能得到信号传递过来的数据,在信号处理函数中对这些信息的访问都将导致段错误(Segmentation fault)。</p> <p>注:很多文献在阐述该标志位时都认为,如果设置了该标志位,就必须定义三参数信号处理函数。实际不是这样的,验证方法很简单:自己实现一个单一参数信号处理函数,并在程序中设置该标志位,可以察看程序的运行结果。实际上,可以把该标志位看成信号是否传递参数的开关,如果设置该位,则传递参数;否则,不传递参数。</p> <h1>7. 信号集及信号集操作函数</h1> <p>信号集被定义为一种数据类型:</p> <p>typedef struct {</p> <p>unsigned long sig[_NSIG_WORDS];</p> <p>} sigset_t;</p> <p>信号集用来描述信号的集合,linux所支持的所有信号可以全部或部分的出现在信号集中,主要与信号阻塞相关函数配合使用。下面是为信号集操作定义的相关函数:</p> <p>#include &lt;signal.h&gt;</p> <p>int sigemptyset(sigset_t *set);</p> <p>int sigfillset(sigset_t *set);</p> <p>int sigaddset(sigset_t *set, int signum)</p> <p>int sigdelset(sigset_t *set, int signum);</p> <p>int sigismember(const sigset_t *set, int signum);</p> <p>sigemptyset(sigset_t *set)初始化由set指定的信号集,信号集里面的所有信号被清空;</p> <p>sigfillset(sigset_t *set)调用该函数后,set指向的信号集中将包含linux支持的64种信号;</p> <p>sigaddset(sigset_t *set, int signum)在set指向的信号集中加入signum信号;</p> <p>sigdelset(sigset_t *set, int signum)在set指向的信号集中删除signum信号;</p> <p>sigismember(const sigset_t *set, int signum)判定信号signum是否在set指向的信号集中。</p> <h1>8. 信号阻塞与信号未决</h1> <p>每个进程都有一个用来描述哪些信号递送到进程时将被阻塞的信号集,该信号集中的所有信号在递送到进程后都将被阻塞。下面是与信号阻塞相关的几个函数:</p> <p>#include &lt;signal.h&gt;</p> <p>int sigprocmask(int how, const sigset_t *set, sigset_t *oldset));</p> <p>int sigpending(sigset_t *set));</p> <p>int sigsuspend(const sigset_t *mask));</p> <p>sigprocmask()函数能够根据参数how来实现对信号集的操作,操作主要有三种: <table cellspacing="0" cellpadding="0" border="1"><tbody> <tr> <td valign="top" width="25%"> <p>参数how</p> </td> <td valign="top" width="74%"> <p>进程当前信号集</p> </td> </tr> <tr> <td valign="top" width="25%"> <p>SIG_BLOCK</p> </td> <td valign="top" width="74%"> <p>在进程当前阻塞信号集中添加set指向信号集中的信号</p> </td> </tr> <tr> <td valign="top" width="25%"> <p>SIG_UNBLOCK</p> </td> <td valign="top" width="74%"> <p>如果进程阻塞信号集中包含set指向信号集中的信号,则解除对该信号的阻塞</p> </td> </tr> <tr> <td valign="top" width="25%"> <p>SIG_SETMASK</p> </td> <td valign="top" width="74%"> <p>更新进程阻塞信号集为set指向的信号集</p> </td> </tr> </tbody></table> </p> <p>sigpending(sigset_t *set))获得当前已递送到进程,却被阻塞的所有信号,在set指向的信号集中返回结果。</p> <p>sigsuspend(const sigset_t *mask))用于在接收到某个信号之前,临时用mask替换进程的信号掩码,并暂停进程执行,直到收到信号为止。sigsuspend 返回后将恢复调用之前的信号掩码。信号处理函数完成后,进程将继续执行。该系统调用始终返回-1,并将errno设置为EINTR。</p> <p>结构itimerval:</p> <p>struct itimerval {</p> <p>struct timeval it_interval; /* next value */</p> <p>struct timeval it_value; /* current value */</p> <p>};</p> <p>struct timeval {</p> <p>long tv_sec; /* seconds */</p> <p>long tv_usec; /* microseconds */</p> <p>};</p> <p>参数信号处理函数中第二个参数的说明性描述:</p> <p>siginfo_t {</p> <p>int si_signo; /* 信号值,对所有信号有意义*/</p> <p>int si_errno; /* errno值,对所有信号有意义*/</p> <p>int si_code; /* 信号产生的原因,对所有信号有意义*/</p> <p>pid_t si_pid; /* 发送信号的进程ID,对kill(2),实时信号以及SIGCHLD有意义 */</p> <p>uid_t si_uid; /* 发送信号进程的真实用户ID,对kill(2),实时信号以及SIGCHLD有意义 */</p> <p>int si_status; /* 退出状态,对SIGCHLD有意义*/</p> <p>clock_t si_utime; /* 用户消耗的时间,对SIGCHLD有意义 */</p> <p>clock_t si_stime; /* 内核消耗的时间,对SIGCHLD有意义 */</p> <p>sigval_t si_value; /* 信号值,对所有实时有意义,是一个联合数据结构,</p> <p>可以为一个整数(由si_int标示,也可以为一个指针,由si_ptr标示)*/ </p> <p>void * si_addr; /* 触发fault的内存地址,对SIGILL,SIGFPE,SIGSEGV,SIGBUS 信号有意义*/</p> <p>int si_band; /* 对SIGPOLL信号有意义 */</p> <p>int si_fd; /* 对SIGPOLL信号有意义 */</p> <p>}</p> <p>实际上,除了前三个元素外,其他元素组织在一个联合结构中,在联合数据结构中,又根据不同的信号组织成不同的结构。注释中提到的对某种信号有意义指的是,在该信号的处理函数中可以访问这些域来获得与信号相关的有意义的信息,只不过特定信号只对特定信息感兴趣而已。</p> <h1>9. 信号生命周期</h1> <p>从信号发送到信号处理函数的执行完毕。</p> <p>对于一个完整的信号生命周期(从信号发送到相应的处理函数执行完毕)来说,可以分为三个重要的阶段,这三个阶段由四个重要事件来刻画:信号诞生;信号在进程中注册完毕;信号在进程中的注销完毕;信号处理函数执行完毕。相邻两个事件的时间间隔构成信号生命周期的一个阶段。</p> <p><a href="http://static.oschina.net/uploads/img/201412/26083025_eCr7.gif"><img title="clip_image005" style="border-top: 0px; border-right: 0px; background-image: none; border-bottom: 0px; padding-top: 0px; padding-left: 0px; margin: 0px; border-left: 0px; display: inline; padding-right: 0px" border="0" alt="clip_image005" src="http://static.oschina.net/uploads/img/201412/26083025_oadb.gif" width="244" height="18" /></a></p> <p>下面阐述四个事件的实际意义:</p> <p>1. 信号&quot;诞生&quot;。信号的诞生指的是触发信号的事件发生(如检测到硬件异常、定时器超时以及调用信号发送函数kill()或sigqueue()等)。</p> <p><b></b></p> <p>2.信号在目标进程中&quot;注册&quot;;进程的task_struct结构中有关于本进程中未决信号的数据成员:</p> <p>struct sigpending pending:</p> <p>struct sigpending{</p> <p>struct sigqueue *head, **tail;</p> <p>sigset_t signal;</p> <p>};</p> <p>第三个成员是进程中所有未决信号集,第一、第二个成员分别指向一个sigqueue类型的结构链(称之为&quot;未决信号信息链&quot;)的首尾,信息链中的每个sigqueue结构刻画一个特定信号所携带的信息,并指向下一个sigqueue结构:<b></b></p> <p>struct sigqueue{</p> <p>struct sigqueue *next;</p> <p>siginfo_t info;</p> <p>}</p> <p>信号在进程中注册指的就是信号值加入到进程的未决信号集中(sigpending结构的第二个成员sigset_t signal),并且信号所携带的信息被保留到未决信号信息链的某个sigqueue结构中。 只要信号在进程的未决信号集中,表明进程已经知道这些信号的存在,但还没来得及处理,或者该信号被进程阻塞。<b></b></p> <p><strong>注:</strong>当一个实时信号发送给一个进程时,不管该信号是否已经在进程中注册,都会被再注册一次,因此,信号不会丢失。因此,实时信号又叫做&quot;可靠信号&quot;。这意味着同一个实时信号可以在同一个进程的未决信号信息链中占有多个sigqueue结构(进程每收到一个实时信号,都会为它分配一个结构来登记该信号信息,并把该结构添加在未决信号链尾,即所有诞生的实时信号都会在目标进程中注册);<b> </b></p> <p>当一个非实时信号发送给一个进程时,如果该信号已经在进程中注册,则该信号将被丢弃,造成信号丢失。因此,非实时信号又叫做&quot;不可靠信号&quot;。这意味着同一个非实时信号在进程的未决信号信息链中,至多占有一个sigqueue结构(一个非实时信号诞生后,(1)、如果发现相同的信号已经在目标结构中注册,则不再注册,对于进程来说,相当于不知道本次信号发生,信号丢失;(2)、如果进程的未决信号中没有相同信号,则在进程中注册自己)。</p> <p>3.信号在进程中的注销。在目标进程执行过程中,会检测是否有信号等待处理(每次从系统空间返回到用户空间时都做这样的检查)。如果存在未决信号等待处理且该信号没有被进程阻塞,则在运行相应的信号处理函数前,进程会把信号在未决信号链中占有的结构卸掉。是否将信号从进程未决信号集中删除对于实时与非实时信号是不同的。对于非实时信号来说,由于在未决信号信息链中最多只占用一个sigqueue结构,因此该结构被释放后,应该把信号在进程未决信号集中删除(信号注销完毕);而对于实时信号来说,可能在未决信号信息链中占用多个sigqueue结构,因此应该针对占用sigqueue结构的数目区别对待:如果只占用一个sigqueue结构(进程只收到该信号一次),则应该把信号在进程的未决信号集中删除(信号注销完毕)。否则,不应该在进程的未决信号集中删除该信号(信号注销完毕)。 <br />进程在执行信号相应处理函数之前,首先要把信号在进程中注销。<b></b></p> <p>4.信号生命终止。进程注销信号后,立即执行相应的信号处理函数,执行完毕后,信号的本次发送对进程的影响彻底结束。</p> <p><strong>注:</strong>1)信号注册与否,与发送信号的函数(如kill()或sigqueue()等)以及信号安装函数(signal()及sigaction())无关,只与信号值有关(信号值小于SIGRTMIN的信号最多只注册一次,信号值在SIGRTMIN及SIGRTMAX之间的信号,只要被进程接收到就被注册)。<b></b></p> <p>2)在信号被注销到相应的信号处理函数执行完毕这段时间内,如果进程又收到同一信号多次,则对实时信号来说,每一次都会在进程中注册;而对于非实时信号来说,无论收到多少次信号,都会视为只收到一个信号,只在进程中注册一次。</p> <h1>10. 信号编程注意事项</h1> <ol> <li>防止不该丢失的信号丢失。如果对提到的信号生命周期理解深刻的话,很容易知道信号会不会丢失,以及在哪里丢失。</li> <li>程序的可移植性</li> </ol> <p>考虑到程序的可移植性,应该尽量采用POSIX信号函数,POSIX信号函数主要分为两类:</p> <p>l POSIX 1003.1信号函数: Kill()、sigaction()、sigaddset()、sigdelset()、sigemptyset()、sigfillset()、sigismember()、sigpending()、sigprocmask()、sigsuspend()。</p> <p>l POSIX 1003.1b信号函数。POSIX 1003.1b在信号的实时性方面对POSIX 1003.1做了扩展,包括以下三个函数: sigqueue()、sigtimedwait()、sigwaitinfo()。 其中,sigqueue主要针对信号发送,而sigtimedwait及sigwaitinfo()主要用于取代sigsuspend()函数,后面有相应实例。</p> <p>该函数与sigsuspend()类似,阻塞一个进程直到特定信号发生,但信号到来时不执行信号处理函数,而是返回信号值。因此为了避免执行相应的信号处理函数,必须在调用该函数前,使进程屏蔽掉set指向的信号,因此调用该函数的典型代码是:</p> <p>sigset_t newmask;</p> <p>int rcvd_sig; </p> <p>siginfo_t info;</p> <p>sigemptyset(&amp;newmask);</p> <p>sigaddset(&amp;newmask, SIGRTMIN);</p> <p>sigprocmask(SIG_BLOCK, &amp;newmask, NULL);</p> <p>rcvd_sig = sigwaitinfo(&amp;newmask, &amp;info) </p> <p>if (rcvd_sig == -1) {</p> <p>..</p> <p>}</p> <p>调用成功返回信号值,否则返回-1。sigtimedwait()功能相似,只不过增加了一个进程等待的时间。</p> <ol start="start"> <li>程序的稳定性。</li> </ol> <p>为了增强程序的稳定性,在信号处理函数中应使用可重入函数。</p> <p>信号处理程序中应当使用可再入(可重入)函数(注:所谓可重入函数是指一个可以被多个任务调用的过程,任务在调用时不必担心数据是否会出错)。因为进程在收到信号后,就将跳转到信号处理函数去接着执行。如果信号处理函数中使用了不可重入函数,那么信号处理函数可能会修改原来进程中不应该被修改的数据,这样进程从信号处理函数中返回接着执行时,可能会出现不可预料的后果。不可再入函数在信号处理函数中被视为不安全函数。</p> <p>满足下列条件的函数多数是不可再入的:(1)使用静态的数据结构,如getlogin(),gmtime(),getgrgid(),getgrnam(),getpwuid()以及getpwnam()等等;(2)函数实现时,调用了malloc()或者free()函数;(3)实现时使用了标准I/O函数的。The Open Group视下列函数为可再入的:</p> <p>_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()。</p> <p>即使信号处理函数使用的都是&quot;安全函数&quot;,同样要注意进入处理函数时,首先要保存errno的值,结束时,再恢复原值。因为,信号处理过程中,errno值随时可能被改变。另外,longjmp()以及siglongjmp()没有被列为可再入函数,因为不能保证紧接着两个函数的其它调用是安全的。</p> <h1>11. Linux多线程应用</h1> <p>在开发多线程应用时,开发人员一般都会考虑线程安全,会使用 <code>pthread_mutex</code> 去保护全局变量。如果应用中使用了信号,而且信号的产生不是因为程序运行出错,而是程序逻辑需要,譬如 SIGUSR1、SIGRTMIN 等,信号在被处理后应用程序还将正常运行。在编写这类信号处理函数时,应用层面的开发人员却往往忽略了信号处理函数执行的上下文背景,没有考虑编写安全的信号处理函数的一些规则。本文首先介绍编写信号处理函数时需要考虑的一些规则;然后举例说明在多线程应用中如何构建模型让因为程序逻辑需要而产生的异步信号在指定的线程中以同步的方式处理。</p> <p>Linux 多线程应用中,每个线程可以通过调用 <code>pthread_sigmask()</code> 设置本线程的信号掩码。一般情况下,被阻塞的信号将不能中断此线程的执行,除非此信号的产生是因为程序运行出错如 SIGSEGV;另外不能被忽略处理的信号 SIGKILL 和 SIGSTOP 也无法被阻塞。</p> <p>当一个线程调用 <code>pthread_create()</code> 创建新的线程时,此线程的信号掩码会被新创建的线程继承。</p> <h2>11.1. 编写安全的异步信号处理函数</h2> <p>信号的产生可以是:</p> <ul> <li>用户从控制终端终止程序运行,如 Ctrk + C 产生 SIGINT;</li> <li>程序运行出错时由硬件产生信号,如访问非法地址产生 SIGSEGV;</li> <li>程序运行逻辑需要,如调用 kill、raise 产生信号。</li> </ul> <p>因为信号是异步事件,即信号处理函数执行的上下文背景是不确定的,譬如一个线程在调用某个库函数时可能会被信号中断,库函数提前出错返回,转而去执行信号处理函数。对于上述第三种信号的产生,信号在产生、处理后,应用程序不会终止,还是会继续正常运行,在编写此类信号处理函数时尤其需要小心,以免破坏应用程序的正常运行。</p> <p>关于编写安全的信号处理函数主要有以下一些规则:</p> <ul> <li>信号处理函数尽量只执行简单的操作,譬如只是设置一个外部变量,其它复杂的操作留在信号处理函数之外执行;</li> <li>errno 是线程安全,即每个线程有自己的 errno,但不是异步信号安全。如果信号处理函数比较复杂,且调用了可能会改变 errno 值的库函数,必须考虑在信号处理函数开始时保存、结束的时候恢复被中断线程的 errno 值;</li> <li>信号处理函数只能调用可以重入的 C 库函数;譬如不能调用 malloc(),free()以及标准 I/O 库函数等;</li> <li>信号处理函数如果需要访问全局变量,在定义此全局变量时须将其声明为 volatile,以避免编译器不恰当的优化。</li> </ul> <p>从整个 Linux 应用的角度出发,因为应用中使用了异步信号,程序中一些库函数在调用时可能被异步信号中断,此时必须根据errno 的值考虑这些库函数调用被信号中断后的出错恢复处理,譬如socket 编程中的读操作:</p> <p>rlen = recv(sock_fd, buf, len, MSG_WAITALL); </p> <p>if ((rlen == -1) &amp;&amp; (errno == EINTR)){</p> <p>// this kind of error is recoverable, we can set the offset change </p> <p>//‘rlen’ as 0 and continue to recv</p> <p>}</p> <h2>11.2. 在指定的线程中以同步的方式处理异步信号</h2> <p>如上文所述,不仅编写安全的异步信号处理函数本身有很多的规则束缚;应用中其它地方在调用可被信号中断的库函数时还需考虑被中断后的出错恢复处理。这让程序的编写变得复杂,幸运的是,POSIX.1 规范定义了<code>sigwait()</code><code>、</code><code> sigwaitinfo()</code> 和 <code>pthread_sigmask()</code> 等接口,可以实现:</p> <ul> <li>以同步的方式处理异步信号;</li> <li>在指定的线程中处理信号。</li> </ul> <p>这种在指定的线程中以同步方式处理信号的模型可以避免因为处理异步信号而给程序运行带来的不确定性和潜在危险。</p> <p><code>sigwait()</code> 提供了一种等待信号的到来,以串行的方式从信号队列中取出信号进行处理的机制。<code>sigwait</code><code>(</code>)只等待函数参数中指定的信号集,即如果新产生的信号不在指定的信号集内,则 <code>sigwait</code><code>()</code>继续等待。对于一个稳定可靠的程序,我们一般会有一些疑问:</p> <ul> <li>多个相同的信号可不可以在信号队列中排队?</li> <li>如果信号队列中有多个信号在等待,在信号处理时有没有优先级规则?</li> <li>实时信号和非实时信号在处理时有没有什么区别?</li> </ul> <p>测试 <code>sigwait</code> 在信号处理时的一些规则:</p> <p>#include &lt;signal.h&gt;</p> <p>#include &lt;errno.h&gt;</p> <p>#include &lt;pthread.h&gt;</p> <p>#include &lt;unistd.h&gt;</p> <p>#include &lt;sys/types.h&gt;</p> <p>void sig_handler(int signum)</p> <p>{</p> <p>printf(&quot;Receive signal. %d\n&quot;, signum);</p> <p>}</p> <p>void* sigmgr_thread()</p> <p>{</p> <p>sigset_t waitset, oset;</p> <p>int sig;</p> <p>int rc;</p> <p>pthread_t ppid = pthread_self();</p> <p>pthread_detach(ppid);</p> <p>sigemptyset(&amp;waitset);</p> <p>sigaddset(&amp;waitset, SIGRTMIN);</p> <p>sigaddset(&amp;waitset, SIGRTMIN+2);</p> <p>sigaddset(&amp;waitset, SIGRTMAX);</p> <p>sigaddset(&amp;waitset, SIGUSR1);</p> <p>sigaddset(&amp;waitset, SIGUSR2);</p> <p>while (1) {</p> <p>rc = sigwait(&amp;waitset, &amp;sig);</p> <p>if (rc != -1) {</p> <p>sig_handler(sig);</p> <p>} else {</p> <p>printf(&quot;sigwaitinfo() returned err: %d; %s\n&quot;, errno, strerror(errno));</p> <p>}</p> <p>}</p> <p>} </p> <p>int main()</p> <p>{</p> <p>sigset_t bset, oset;</p> <p>int i;</p> <p>pid_t pid = getpid();</p> <p>pthread_t ppid;</p> <p>sigemptyset(&amp;bset);</p> <p>sigaddset(&amp;bset, SIGRTMIN);</p> <p>sigaddset(&amp;bset, SIGRTMIN+2);</p> <p>sigaddset(&amp;bset, SIGRTMAX);</p> <p>sigaddset(&amp;bset, SIGUSR1);</p> <p>sigaddset(&amp;bset, SIGUSR2);</p> <p>if (pthread_sigmask(SIG_BLOCK, &amp;bset, &amp;oset) != 0) {</p> <p>printf(&quot;!! Set pthread mask failed\n&quot;);</p> <p>}</p> <p>kill(pid, SIGRTMAX);</p> <p>kill(pid, SIGRTMAX);</p> <p>kill(pid, SIGRTMIN+2);</p> <p>kill(pid, SIGRTMIN);</p> <p>kill(pid, SIGRTMIN+2);</p> <p>kill(pid, SIGRTMIN);</p> <p>kill(pid, SIGUSR2);</p> <p>kill(pid, SIGUSR2);</p> <p>kill(pid, SIGUSR1);</p> <p>kill(pid, SIGUSR1);</p> <p>// Create the dedicated thread sigmgr_thread() which will handle signals synchronously</p> <p>pthread_create(&amp;ppid, NULL, sigmgr_thread, NULL);</p> <p>sleep(10);</p> <p>exit (0);</p> <p>}</p> <p>sigwait 测试程序执行结果</p> <p><a href="http://static.oschina.net/uploads/img/201412/26083025_O4Jv.jpg"><img title="clip_image007" style="border-top: 0px; border-right: 0px; background-image: none; border-bottom: 0px; padding-top: 0px; padding-left: 0px; margin: 0px; border-left: 0px; display: inline; padding-right: 0px" border="0" alt="clip_image007" src="http://static.oschina.net/uploads/img/201412/26083026_aoKM.jpg" width="244" height="77" /></a></p> <p>从以上测试程序发现以下规则:</p> <ul> <li>对于非实时信号,相同信号不能在信号队列中排队;对于实时信号,相同信号可以在信号队列中排队。</li> <li>如果信号队列中有多个实时以及非实时信号排队,实时信号并不会先于非实时信号被取出,信号数字小的会先被取出:如 SIGUSR1(10)会先于 SIGUSR2 (12),SIGRTMIN(34)会先于 SIGRTMAX (64), 非实时信号因为其信号数字小而先于实时信号被取出。</li> </ul> <p>sigwaitinfo() 以及 sigtimedwait() 也提供了与 sigwait() 函数相似的功能。</p> <h2>11.3. Linux 多线程应用中的信号处理模型</h2> <p>在基于 Linux 的多线程应用中,对于因为程序逻辑需要而产生的信号,可考虑调用 <code>sigwait</code><code>()</code>使用同步模型进行处理。其程序流程如下:</p> <ul> <li>主线程设置信号掩码,阻碍希望同步处理的信号;主线程的信号掩码,会被其创建的线程继承;</li> <li>主线程创建信号处理线程;信号处理线程将希望同步处理的信号集设为 <code>sigwait</code><code>()</code>的第一个参数。</li> <li>主线程创建工作线程。</li> </ul> <p><a href="http://static.oschina.net/uploads/img/201412/26083026_5xoC.gif"><img title="clip_image008" style="border-top: 0px; border-right: 0px; background-image: none; border-bottom: 0px; padding-top: 0px; padding-left: 0px; border-left: 0px; display: inline; padding-right: 0px" border="0" alt="clip_image008" src="http://static.oschina.net/uploads/img/201412/26083026_gsnU.gif" width="244" height="110" /></a></p> <p>图:在指定的线程中以同步方式处理异步信号的模型</p> <p>以下为一个完整的在指定的线程中以同步的方式处理异步信号的程序。</p> <p>主线程设置信号掩码阻碍 SIGUSR1 和 SIGRTMIN 两个信号,然后创建信号处理线程sigmgr_thread()和五个工作线程 worker_thread()。主线程每隔10秒调用 kill() 对本进程发送 SIGUSR1 和 SIGTRMIN 信号。信号处理线程 sigmgr_thread()在接收到信号时会调用信号处理函数 sig_handler()。</p> <p>程序编译:gcc -o signal_sync signal_sync.c -lpthread</p> <p>程序执行:./signal_sync</p> <p>从程序执行输出结果可以看到主线程发出的所有信号都被指定的信号处理线程接收到,并以同步的方式处理。</p> <p>signal_sync.c</p> <p>#include &lt;signal.h&gt;</p> <p>#include &lt;errno.h&gt;</p> <p>#include &lt;pthread.h&gt;</p> <p>#include &lt;unistd.h&gt;</p> <p>#include &lt;sys/types.h&gt;</p> <p>void sig_handler(int signum)</p> <p>{</p> <p>static int j = 0;</p> <p>static int k = 0;</p> <p>pthread_t sig_ppid = pthread_self(); </p> <p>// used to show which thread the signal is handled in.</p> <p>if (signum == SIGUSR1) {</p> <p>printf(&quot;thread %d, receive SIGUSR1 No. %d\n&quot;, sig_ppid, j);</p> <p>j++;</p> <p>//SIGRTMIN should not be considered constants from userland, </p> <p>//there is compile error when use switch case</p> <p>} else if (signum == SIGRTMIN) {</p> <p>printf(&quot;thread %d, receive SIGRTMIN No. %d\n&quot;, sig_ppid, k);</p> <p>k++;</p> <p>}</p> <p>}</p> <p>void* worker_thread()</p> <p>{</p> <p>pthread_t ppid = pthread_self();</p> <p>pthread_detach(ppid);</p> <p>while (1) {</p> <p>printf(&quot;I'm thread %d, I'm alive\n&quot;, ppid);</p> <p>sleep(10);</p> <p>}</p> <p>}</p> <p>void* sigmgr_thread()</p> <p>{</p> <p>sigset_t waitset, oset;</p> <p>siginfo_t info;</p> <p>int rc;</p> <p>pthread_t ppid = pthread_self();</p> <p>pthread_detach(ppid);</p> <p>sigemptyset(&amp;waitset);</p> <p>sigaddset(&amp;waitset, SIGRTMIN);</p> <p>sigaddset(&amp;waitset, SIGUSR1); </p> <p>while (1) {</p> <p>rc = sigwaitinfo(&amp;waitset, &amp;info);</p> <p>if (rc != -1) {</p> <p>printf(&quot;sigwaitinfo() fetch the signal - %d\n&quot;, rc);</p> <p>sig_handler(info.si_signo);</p> <p>} else {</p> <p>printf(&quot;sigwaitinfo() returned err: %d; %s\n&quot;, errno, strerror(errno));</p> <p>}</p> <p>}</p> <p>}</p> <p>int main()</p> <p>{</p> <p>sigset_t bset, oset;</p> <p>int i;</p> <p>pid_t pid = getpid();</p> <p>pthread_t ppid;</p> <p>// Block SIGRTMIN and SIGUSR1 which will be handled in </p> <p>//dedicated thread sigmgr_thread()</p> <p>// Newly created threads will inherit the pthread mask from its creator </p> <p>sigemptyset(&amp;bset);</p> <p>sigaddset(&amp;bset, SIGRTMIN);</p> <p>sigaddset(&amp;bset, SIGUSR1);</p> <p>if (pthread_sigmask(SIG_BLOCK, &amp;bset, &amp;oset) != 0)</p> <p>printf(&quot;!! Set pthread mask failed\n&quot;);</p> <p>// Create the dedicated thread sigmgr_thread() which will handle </p> <p>// SIGUSR1 and SIGRTMIN synchronously</p> <p>pthread_create(&amp;ppid, NULL, sigmgr_thread, NULL);</p> <p>// Create 5 worker threads, which will inherit the thread mask of</p> <p>// the creator main thread</p> <p>for (i = 0; i &lt; 5; i++) {</p> <p>pthread_create(&amp;ppid, NULL, worker_thread, NULL);</p> <p>}</p> <p>// send out 50 SIGUSR1 and SIGRTMIN signals</p> <p>for (i = 0; i &lt; 50; i++) {</p> <p>kill(pid, SIGUSR1);</p> <p>printf(&quot;main thread, send SIGUSR1 No. %d\n&quot;, i);</p> <p>kill(pid, SIGRTMIN);</p> <p>printf(&quot;main thread, send SIGRTMIN No. %d\n&quot;, i);</p> <p>sleep(10);</p> <p>}</p> <p>exit (0);</p> <p>}</p> <h2>11.4. 注意事项</h2> <p>在基于 Linux 的多线程应用中,对于因为程序逻辑需要而产生的信号,可考虑使用同步模型进行处理;而对会导致程序运行终止的信号如 SIGSEGV 等,必须按照传统的异步方式使用 signal()、 sigaction()注册信号处理函数进行处理。这两种信号处理模型可根据所处理的信号的不同同时存在一个 Linux 应用中:</p> <ul> <li>不要在线程的信号掩码中阻塞不能被忽略处理的两个信号 SIGSTOP 和 SIGKILL。</li> <li>不要在线程的信号掩码中阻塞 SIGFPE、SIGILL、SIGSEGV、SIGBUS。</li> <li>确保 sigwait() 等待的信号集已经被进程中所有的线程阻塞。</li> <li>在主线程或其它工作线程产生信号时,必须调用 kill() 将信号发给整个进程,而不能使用 pthread_kill() 发送某个特定的工作线程,否则信号处理线程无法接收到此信号。</li> <li>因为 sigwait()使用了串行的方式处理信号的到来,为避免信号的处理存在滞后,或是非实时信号被丢失的情况,处理每个信号的代码应尽量简洁、快速,避免调用会产生阻塞的库函数。</li> </ul> <h2>11.5. 多线程经验</h2> <p>1. 在多线程环境下,产生的信号是传递给整个进程的,一般而言,所有线程都有机会收到这个信号,进程在收到信号的的线程上下文执行信号处理函数,具体是哪个线程执行的难以获知。</p> <p>2. signal函数BSD/Linux的实现并不在信号处理函数调用时,恢复信号的处理为默认,而是在信号处理时阻塞此信号,直到信号处理函数返回。其他实现可能在调用信号处理函数时,恢复信号的处理为默认方式,因而需要在信号处理函数中重建信号处理函数为我们定义的处理函数,在这些系统中,较好的方法是使用sigaction来建立信号处理函数。</p> <p>3. 发送信号给进程,哪个线程会收到?APUE说,在多线程的程序中,如果不做特殊的信号阻塞处理,当发送信号给进程时,由系统选择一个线程来处理这个信号。</p> <p>4. 如果进程中,有的线程可以屏蔽了某个信号,而某些线程可以处理这个信号,则当我们发送这个信号给进程或者进程中不能处理这个信号的线程时,系统会将这个信号投递到进程号最小的那个可以处理这个信号的线程中去处理。</p> <p>5. 如果我们同时注册了信号处理函数,同时又用sigwait来等待这个信号,谁会取到信号?经过实验,Linux上sigwait的优先级高。</p> <p>6. 在Linux中的posix线程模型中,线程拥有独立的进程号,可以通过getpid()得到线程的进程号,而线程号保存在pthread_t的值中。而主线程的进程号就是整个进程的进程号,因此向主进程发送信号只会将信号发送到主线程中去。如果主线程设置了信号屏蔽,则信号会投递到一个可以处理的线程中去。</p> <p>7. 当调用SYSTEM函数去执行SHELL命令时,可以放心的阻塞SIGCHLD,因为SYSTEM会自己处理子进程终止的问题。</p> <p>8. 使用sleep()时,要以放心的去阻塞SIGALRM信号,目前sleep函数都不会依赖于ALRM函数的SIGALRM信号来工作。</p> <p>9. 默认情况下,信号将由主进程接收处理,就算信号处理函数是由子线程注册的。</p> <p>10. 每个线程均有自己的信号屏蔽字,可以使用sigprocmask函数来屏蔽某个线程对该信号的响应处理,仅留下需要处理该信号的线程来处理指定的信号。</p> <p>11. 对某个信号处理函数,以程序执行时最后一次注册的处理函数为准,即在所有的线程里,同一个信号在任何线程里对该信号的处理一定相同。</p> <p>12. 可以使用pthread_kill对指定的线程发送信号APUE的说法:每个线程都有自己的信号屏蔽字,但是信号的处理是进程中所有的线程共享的,这意味着尽管单个线程可以阻止某些信号,但当线程修改了与某个信号相关的处理行为后,所有的线程都共享这个处理行为的改变。这样如果一个线程选择忽略某个信号,而其他线程可以恢复信号的默认处理行为,或者为信号设置一个新的处理程序,从而可以撤销上述线程的信号选择。</p> <p>13. 进程中的信号是送到单个线程的,如果信号与硬件故障或者计时器超时有关,该型号就被发送到引起该事件的线程中去,而其他的信号则被发送到任意一个线程。</p> <p>14. sigprocmask的行为在多线程的进程中没有定义,线程必须使用pthread_sigmask总结:一个信号可以被没屏蔽它的任何一个线程处理,但是在一个进程内只有一个多个线程共用的处理函数。</p> <p>15. Linux多线程应用中,每个线程可以通过调用pthread_sigmask()设置本线程的信号掩码。一般情况下,被阻塞的信号将不能中断此线程的执行,除非此信号的产生是因为程序运行出错如SIGSEGV;另外不能被忽略处理的信号SIGKILL和SIGSTOP也无法被阻塞。</p> <p>16. 当一个线程调用pthread_create()创建新的线程时,此线程的信号掩码会被新创建的线程继承。</p> <p>17. 信号安装最好采用sigaction方式,sigaction,是为替代signal来设计的较稳定的信号处理,signal的使用比较简单。signal(signalNO,signalproc);不能完成的任务是:1.不知道信号产生的原因;2.处理信号中不能阻塞其他的信号而signaction,则可以设置比较多的消息。尤其是在信号处理函数过程中接受信号,进行何种处理。</p> <p>18. sigaction函数用于改变进程接收到特定信号后的行为。</p> <p>19. sigprocmask函数只能用于单线程,在多线程中使用pthread_sigmask函数。</p> <p>20. 信号是发给进程的特殊消息,其典型特性是具有异步性。</p> <p>21. 信号集代表多个信号的集合,其类型是sigset_t。</p> <p>22. 每个进程都有一个信号掩码(或称为信号屏蔽字),其中定义了当前进程要求阻塞的信号集。</p> <p>23. 所谓阻塞,指Linux内核不向进程交付在掩码中的所有信号。于是进程可以通过修改信号掩码来暂时阻塞特定信号的交付,被阻塞的信号不会影响进程的行为直到该信号被真正交付。</p> <p>24. 忽略信号不同于阻塞信号,忽略信号是指Linux内核已经向应用程序交付了产生的信号,只是应用程序直接丢弃了该信号而已。</p> <h1>12. 信号应用实例</h1> <p>linux下的信号应用并没有想象的那么恐怖,程序员所要做的最多只有三件事情:</p> <ol> <li>安装信号(推荐使用sigaction());</li> <li>实现三参数信号处理函数,handler(int signal,struct siginfo *info, void *);</li> <li>发送信号,推荐使用sigqueue()。</li> </ol> <p>实际上,对有些信号来说,只要安装信号就足够了(信号处理方式采用缺省或忽略)。其他可能要做的无非是与信号集相关的几种操作。</p> <h2>12.1. <strong>信号发送及处理</strong></h2> <p>实现一个信号接收程序sigreceive(其中信号安装由sigaction())。</p> <pre>#include &lt;signal.h&gt;</pre>

<pre>#include &lt;sys/types.h&gt;</pre>

<pre>#include &lt;unistd.h&gt;</pre>

<pre>void new_op(int,siginfo_t*,void*);</pre>

<pre>int main(int argc,char**argv)</pre>

<pre>{</pre>

<pre>&#160;&#160;&#160;&#160; struct sigaction act;&#160;&#160;&#160;&#160; </pre>

<pre>&#160;&#160;&#160;&#160; int sig;</pre>

<pre>&#160;&#160;&#160;&#160; sig=atoi(argv[1]);</pre>

<pre>&#160;&#160;&#160;&#160; </pre>

<pre>&#160;&#160;&#160;&#160; sigemptyset(&amp;act.sa_mask);</pre>

<pre>&#160;&#160;&#160;&#160; act.sa_flags=SA_SIGINFO;</pre>

<pre>&#160;&#160;&#160;&#160; act.sa_sigaction=new_op;</pre>

<pre>&#160;&#160;&#160;&#160; </pre>

<pre>&#160;&#160;&#160;&#160; if(sigaction(sig,&amp;act,NULL) &lt; 0)</pre>

<pre>&#160;&#160;&#160;&#160; {</pre>

<pre>&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; printf(&quot;install sigal error\n&quot;);</pre>

<pre>&#160;&#160;&#160;&#160; }</pre>

<pre>&#160;&#160;&#160;&#160; </pre>

<pre>&#160;&#160;&#160;&#160; while(1)</pre>

<pre>&#160;&#160;&#160;&#160; {</pre>

<pre>&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; sleep(2);</pre>

<pre>&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; printf(&quot;wait for the signal\n&quot;);</pre>

<pre>&#160;&#160;&#160;&#160; }</pre>

<pre>}</pre>

<pre>void new_op(int signum,siginfo_t *info,void *myact)</pre>

<pre>{</pre>

<pre>&#160;&#160;&#160;&#160; printf(&quot;receive signal %d&quot;, signum);</pre>

<pre>&#160;&#160;&#160;&#160; sleep(5);</pre>

<pre>}</pre>

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

<p><strong>注:</strong>可以用sigqueue实现一个命令行信号发送程序sigqueuesend。</p>

<h2>12.2. <strong>信号传递附加信息</strong></h2>

<p>主要包括两个实例:</p>

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

<p>#include &lt;signalh&gt;</p>

<p>#include &lt;sys/typesh&gt;</p>

<p>#include &lt;unistdh&gt;</p>

<p>void new_op(int,siginfo_t*,void*);</p>

<p>int main(int argc,char**argv)</p>

<p>{</p>

<p>struct sigaction act;</p>

<p>union sigval mysigval;</p>

<p>int i;</p>

<p>int sig;</p>

<p>pid_t pid;</p>

<p>char data[10];</p>

<p>memset(data,0,sizeof(data));</p>

<p>for(i=0;i &lt; 5;i++) </p>

<p>data[i]='2';</p>

<p>mysigvalsival_ptr=data;</p>

<p>sig=atoi(argv[1]);</p>

<p>pid=getpid();</p>

<p>sigemptyset(&amp;actsa_mask);</p>

<p>actsa_sigaction=new_op;//三参数信号处理函数</p>

<p>actsa_flags=SA_SIGINFO;//信息传递开关</p>

<p>if(sigaction(sig,&amp;act,NULL) &lt; 0)</p>

<p>{</p>

<p>printf(&quot;install sigal error\n&quot;);</p>

<p>}</p>

<p>while(1)</p>

<p>{</p>

<p>sleep(2);</p>

<p>printf(&quot;wait for the signal\n&quot;);</p>

<p>sigqueue(pid,sig,mysigval);//向本进程发送信号,并传递附加信息</p>

<p>}</p>

<p>}</p>

<p>void new_op(int signum,siginfo_t *info,void *myact)//三参数信号处理函数的实现</p>

<p>{</p>

<p>int i;</p>

<p>for(i=0;i&lt;10;i++)</p>

<p>{</p>

<p>printf(&quot;%c\n &quot;,(*( (char*)((*info)si_ptr)+i)));</p>

<p>}</p>

<p>printf(&quot;handle signal %d over;&quot;,signum);</p>

<p>}</p>

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

<p>不同进程间传递整型参数:把1中的信号发送和接收放在两个程序中,并且在发送过程中传递整型参数。</p>

<p>信号接收程序:</p>

<p>#include &lt;signal.h&gt;</p>

<p>#include &lt;sys/types.h&gt;</p>

<p>#include &lt;unistd.h&gt;</p>

<p>void new_op(int,siginfo_t*,void*);</p>

<p>int main(int argc,char**argv)</p>

<p>{</p>

<p>struct sigaction act;</p>

<p>int sig;</p>

<p>pid_t pid;</p>

<p>pid=getpid();</p>

<p>sig=atoi(argv[1]);</p>

<p>sigemptyset(&amp;act.sa_mask);</p>

<p>act.sa_sigaction=new_op;</p>

<p>act.sa_flags=SA_SIGINFO;</p>

<p>if(sigaction(sig,&amp;act,NULL)&lt;0)</p>

<p>{</p>

<p>printf(&quot;install sigal error\n&quot;);</p>

<p>}</p>

<p>while(1)</p>

<p>{</p>

<p>sleep(2);</p>

<p>printf(&quot;wait for the signal\n&quot;);</p>

<p>}</p>

<p>}</p>

<p>void new_op(int signum,siginfo_t *info,void *myact)</p>

<p>{</p>

<p>printf(&quot;the int value is %d \n&quot;,info-&gt;si_int);</p>

<p>}</p>

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

<p>#include &lt;signal.h&gt;</p>

<p>#include &lt;sys/time.h&gt;</p>

<p>#include &lt;unistd.h&gt;</p>

<p>#include &lt;sys/types.h&gt;</p>

<p>main(int argc,char**argv)</p>

<p>{</p>

<p>pid_t pid;</p>

<p>int signum;</p>

<p>union sigval mysigval;</p>

<p>signum=atoi(argv[1]);</p>

<p>pid=(pid_t)atoi(argv[2]);</p>

<p>mysigval.sival_int=8;//不代表具体含义,只用于说明问题</p>

<p>if(sigqueue(pid,signum,mysigval)==-1)</p>

<p>printf(&quot;send error\n&quot;);</p>

<p>sleep(2);</p>

<p>}</p>

<p><strong>注:</strong>实例2的两个例子侧重点在于用信号来传递信息,目前关于在linux下通过信号传递信息的实例非常少,倒是Unix下有一些,但传递的基本上都是关于传递一个整数,传递指针的我还没看到。我一直没有实现不同进程间的指针传递(实际上更有意义),也许在实现方法上存在问题吧,请实现者email我。</p>

<h2>12.3. <strong>信号阻塞及信号集操作</strong></h2>

<pre>#include &quot;signal.h&quot;</pre>

<pre>#include &quot;unistd.h&quot;</pre>

<pre>static void my_op(int);</pre>

<pre>main()</pre>

<pre>{</pre>

<pre>&#160;&#160;&#160;&#160; sigset_t new_mask,old_mask,pending_mask;</pre>

<pre>&#160;&#160;&#160;&#160; struct sigaction act;</pre>

<pre>&#160;&#160;&#160;&#160; sigemptyset(&amp;act.sa_mask);</pre>

<pre>&#160;&#160;&#160;&#160; act.sa_flags=SA_SIGINFO;</pre>

<pre>&#160;&#160;&#160;&#160; act.sa_sigaction=(void*)my_op;</pre>

<pre>&#160;&#160;&#160;&#160; if(sigaction(SIGRTMIN+10,&amp;act,NULL))</pre>

<pre>&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; printf(&quot;install signal SIGRTMIN+10 error\n&quot;);</pre>

<pre>&#160;&#160;&#160;&#160; sigemptyset(&amp;new_mask);</pre>

<pre>&#160;&#160;&#160;&#160; sigaddset(&amp;new_mask,SIGRTMIN+10);</pre>

<pre>&#160;&#160;&#160;&#160; if(sigprocmask(SIG_BLOCK, &amp;new_mask,&amp;old_mask))</pre>

<pre>&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; printf(&quot;block signal SIGRTMIN+10 error\n&quot;);</pre>

<pre>&#160;&#160;&#160;&#160; sleep(10);&#160;&#160;&#160;&#160;&#160;&#160; </pre>

<pre>&#160;&#160;&#160;&#160; printf(&quot;now begin to get pending mask and unblock SIGRTMIN+10\n&quot;);</pre>

<pre>&#160;&#160;&#160;&#160; if(sigpending(&amp;pending_mask)&lt;0)</pre>

<pre>&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; printf(&quot;get pending mask error\n&quot;);</pre>

<pre>&#160;&#160;&#160;&#160; if(sigismember(&amp;pending_mask,SIGRTMIN+10))</pre>

<pre>&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; printf(&quot;signal SIGRTMIN+10 is pending\n&quot;);</pre>

<pre>&#160;&#160;&#160;&#160; if(sigprocmask(SIG_SETMASK,&amp;old_mask,NULL)&lt;0)</pre>

<pre>&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; printf(&quot;unblock signal error\n&quot;);</pre>

<pre>&#160;&#160;&#160;&#160; printf(&quot;signal unblocked\n&quot;);</pre>

<pre>&#160;&#160;&#160;&#160; sleep(10);</pre>

<pre>}</pre>

<pre>static void my_op(int signum)</pre>

<pre>{</pre>

<pre>&#160;&#160;&#160;&#160; printf(&quot;receive signal %d \n&quot;,signum);</pre>

<pre>}</pre>

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

<p><strong>注:</strong>在上面几个实例中,使用了printf()函数,只是作为诊断工具,pringf()函数是不可重入的,不应在信号处理函数中使用。</p>

转载于:https://my.oschina.net/lovecxx/blog/360706

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值