基础文章1:APUE chap10 信号

来源:互联网 发布:大狸子软件分享 编辑:程序博客网 时间:2024/05/20 23:31
 0.序
1)本文是我为了充分理解Nginx的信号处理方面的内容,我阅读了APUE chap10 信号的内容。
2)主要学习了以下章节,
详细学习了以下这几个章节
10.3signal函数
10.4 不可靠信号
10.5 中断系统调用
10.6 可重入函数
10.8 可靠信号术语与语义
10.11 信号集
10.12 sigpromask函数 
  10.13 sigpending函数
10.14 sigaction函数
10.16 sigsuspend函数

3)最后通过一个程序来详细说明了这几点。
简要一点说明这部分的学习方法:如果能够将本文的10.16处的程序弄清楚,也就明白了信号的处理机制。当然为了能够读懂该程序,你必须明白以下几点:
     1)信号是异步的  
     2)信号集中包括要阻塞的信号,通过sigprocmask设置到信号屏蔽字中。
     3)  当信号到来时,我们不立即向进程递送该信号,而是阻塞住。
     4)当信号被解除阻塞时,之前被阻塞住的信号才会被递送到进程中,被信号处理函数处理并返回到进程中或者终止进程。
4)既然学习信号机制是为了学习Nginx的信号处理方面的内容,当然不能少了Nginx中代码。详见文章0:Nginx中与信号有关的内容

10.1 引言
10.2 信号概念
     1)每个信号都有一个名字 ,每个名字都以三个字符SIG开头
     2)不存在编号为0的信号,kill函数对信号编号0有特殊的应用
     3)产生信号的条件
          
     4)信号是异步事件的经典实例。产生信号的事件对进程而言是随机出现的。举例说明:去银行办理业务,普通青年排成一列等待办理业务,每隔几分钟瞅瞅前面的人是否办理完毕;文艺青年则去前台取票,等待机器到了的时候喊你去办理业务;2B青年直接打道回府,不办理了。对于信号而言,就是文艺青年;告诉进程在此信号到来时,去执行某些操作。不然的话,就只能是普通青年的做法,进程会简单的去测试一个变量(例如errno)来判别是否出现了一个信号。
     当信号到来时,内核会去执行哪些操作呢?我们将之称为信号的处理或者与信号相关的动作。
     (1)忽略此信号。大多数信号可以使用这种方式进行处理。但有两种信号却决不能忽略。两种信号是SIGKILL和SIGSTOP。
     (2)执行系统默认操作:表10.1给出了信号的默认操作。
     (3)捕捉信号:通知内核在某种信号发生时调用一个用户函数。



10.3 signal 函数

// man signal
NAME
       signal - ANSI C signal handling

SYNOPSIS
       #include <signal.h>

       typedef void (*sighandler_t)(int);

       sighandler_t signal(int signum, sighandler_t handler);

 The  behavior of signal() varies across Unix versions, and has also varied historically across different versions of Linux.  Avoid its use: use sigaction(2) instead.


参数:signum:表示表10.1中信号名
         handler:常量SIG_IGN、常量SIG_DFL或当接到此信号后要调用的函数的地址。
                        SIG_IGN:内核忽略此信号。
                        SIG_DFL:内核采用默认操作。
                        要调用的函数地址:当信号发生时,调用该函数,我们称这种处理为“捕捉”该信号。称此函数为信号处理函数(signal handler)或信号捕捉函数(signal catching function)。
注意:The signals SIGKILL and SIGSTOP cannot be caught or ignored.
返回值:signal() returns the previous value of the signal handler, or SIG_ERR on error.//example:filename signal1.c
#include <unistd.h>
#include <signal.h>
#include <stdio.h>
void sig_func(int signo)
{
   if(signo == SIGUSR1)
       printf("received SIGUSR1\n");
   else if (signo == SIGUSR2)
       printf("received SIGUSR2\n");
   else
       printf("other signal\n");
}

int main(void)
{

   printf("before signal\n");
  if(  SIG_ERR ==( signal(SIGUSR1,sig_func) ))
            printf("signal error\n");
for(;;)
   sleep(5);
   printf("after signal");
   return 0;
}compile:$ gcc -Wall signal1.c  -o signal1
run:$./signal1 &
reuslt :./signal1 &
[2] 7559
root@hpnl-desktop:/home/hpnl/Downloads/test# before signal
kill -USR1 7559
root@hpnl-desktop:/home/hpnl/Downloads/test# received SIGUSR1
kill 7559

注意:
1)当执行一个程序时,所有信号的状态都是系统默认或忽略。不改变信号的处理方式就不能确定信号的当前处理方式。我们可以使用sigaction函数确定一个信号的处理方式,而无需改变它。
2)当一个进程调用fork时,其子进程继承父进程的信号处理方式。这是因为子进程在开始时复制了父进程的存储映像,所以信号捕捉函数的地址在子进程中是有意义的。


10.4 不可靠信号
     在早期的UNIX版本中信号是不可靠的。
     不可靠的意思是:1)信号可能丢失 2)用户希望内核阻塞一个信号:不要忽略这个信号,而是在其发生时记住它,然后等进程准备好了再去执行。
     阻塞信号的目的:不要忽略该信号,在其发生时记住它。然后在进程做好准备时再去执行它。



10.5 中断系统调用
    1) 早期的UNIX系统的一个特性:如果一个进程在执行一个低速系统调用而被阻塞期间,内核捕捉到一个信号,则该系统调用被中断而不再继续执行。
该系统调用返回出错,其errno被设置为EINTR。
这样做的理由是:因为一个信号发生了,进程捕捉到了她,这意味着已经发生了某种事件,所以这是唤醒被阻塞的系统调用的好机会。

     2)低速系统调用:是一类可能会使进程永远阻塞的一类系统调用。低速系统调用种类:详见APUEp244 10.5 中断的系统调用
     
     3)如果想要唤醒被阻塞的系统调用,那么就需要检测errno的取值,然后再次调用低速系统调用
一段典型代码:
again:
    if((n = read(fd,buf,BUFFSEIZE)) < 0){
          if(errno == EINTR)
              goto again;
    }
     4)为了帮助应用程序使其不必处理被中断的系统调用,4.2BSD引入了某些被中断系统调用的自动重启动。
     关于自动重启动有些疑惑,所谓的自动重启动,是不是不用像上面典型代码那般显式的去再次调用低速系统调用比如read,而是自动会再次调用低速系统调用。


10.6 可重入函数  
很重要的一个概念:参考文章  可重入函数   可重入函数与不可重入函数  使用可重入函数进行更安全的信号处理  对于可重入、线程安全、异步信号安全几个概念的理解

可重入函数:1)当被多个线程调用时,不会引起任何共享的数据
                    2)意味着这个函数可以被中断,其次意味着它除了使用自己栈上的变量以外,不依赖与任何环境(包括static),这样的函数就是purecode(纯代码)可重入。可以允许有该函数的多个副本在运行,由于他们使用的是分离的栈,所以不会相互干扰。
     实际上,可重入函数很少,APUE 10.6节中描述了Single UNIX Specification说明的可重入的函数,只有115个;APUE 12.5节中描述了POSIX.1中不能保证线程安全的函数,只有89个。

不可重入函数:1)已知他们使用静态数据结构
                         2)他们调用malloc和free。因为malloc通常会为所分配的存储区维护一个链接表,而插入执行信号处理函数的时候,进程可能正在修改此链接表。
                         3)使用标准I/O函数,因为标准IO库的很多实现都使用了全局数据结构。
不可重入函数不能由超过一个任务所共享,除非能确保函数的互斥(或者使用信号量,或者在代码的关键部分禁用中断)

     信号和不可重入函数

信号(signal) 是软件中断。它使得程序员可以处理异步事件。为了向进程发送一个信号, 内核在进程表条目的信号域中设置一个位,对应于收到的信号的类型。信号函数的 ANSI C 原型是:

void (*signal (int sigNum, void (*sigHandler)(int))) (int);

或者,另一种描述形式:

typedef void sigHandler(int);SigHandler *signal(int, sigHandler *);

当进程处理所捕获的信号时,正在执行的正常指令序列就会被信号处理器临时中断。然后进程继续执行, 但现在执行的是信号处理器中的指令。如果信号处理器返回,则进程继续执行信号被捕获时正在执行的 正常的指令序列。

现在,在信号处理器中您并不知道信号被捕获时进程正在执行什么内容。如果当进程正在使用 malloc 在它的堆上分配额外的内存时,您通过信号处理器调用 malloc,那会怎样?或者,调用了正在处理全局数据结构的某个函数,而 在信号处理器中又调用了同一个函数。如果是调用 malloc,则进程会 被严重破坏,因为 malloc 通常会为所有它所分配的区域维持一个链表,而它又 可能正在修改那个链表。

甚至可以在需要多个指令的 C 操作符开始和结束之间发送中断。在程序员看来,指令可能似乎是原子的 (也就是说,不能被分割为更小的操作),但它可能实际上需要不止一个处理器指令才能完成操作。 例如,看这段 C 代码:

temp += 1;

在 x86 处理器上,那个语句可能会被编译为:

mov ax,[temp]inc axmov [temp],ax

这显然不是一个原子操作。

这个例子展示了在修改某个变量的过程中运行信号处理器可能会发生什么事情:


清单 1. 在修改某个变量的同时运行信号处理器
                               #include <signal.h>#include <stdio.h>struct two_int { int a, b; } data;void signal_handler(int signum){   printf ("%d, %d\n", data.a, data.b);   alarm (1);}int main (void){ static struct two_int zeros = { 0, 0 }, ones = { 1, 1 }; signal (SIGALRM, signal_handler); data = zeros; alarm (1);while (1)  {data = zeros; data = ones;}}

这个程序向 data 填充 0,1,0,1,一直交替进行。同时,alarm 信号 处理器每一秒打印一次当前内容(在处理器中调用printf 是安全的,当信号发生时 它确实没有在处理器外部被调用)。您预期这个程序会有怎样的输出?它应该打印 0,0 或者 1,1。但是实际的输出 如下所示:

0, 01, 1(Skipping some output...)0, 11, 11, 01, 0...

在大部分机器上,在 data 中存储一个新值都需要若干个指令,每次存储一个字。 如果在这些指令期间发出信号,则处理器可能发现 data.a 为 0 而 data.b 为 1,或者反之。另一方面,如果我们运行代码的机器能够在一个 不可中断的指令中存储一个对象的值,那么处理器将永远打印 0,0 或 1,1。

使用信号的另一个新增的困难是,只通过运行测试用例不能够确保代码没有信号 bug。这一困难的原因在于 信号生成本质上异步的。




10.7 SIGCLD语义
     


10.8 可靠信号术语与语义
          当有事件引发信号时,该事件产生一个信号。在产生了信号时,内核通常在进程表中设置一个某种形式的标识。当内核在进程表中设置一个某种形式的标识时,我们称向进程递送了一个信号。
     在信号产生(generation)和递送(delivery)之间的时间间隔内,称信号是未决的(pending)。

     原文中的解释如上所示:
          一个进程可以阻塞一个信号的递送。对于进程,产生一个被阻塞的信号并且信号的处理方式是默认或者捕捉信号,那么信号就会保持未决状态,知道进程解除阻塞或者信号的处理方式变为忽略。
在递送之前,我们可以让进程改变信号的处理方式。sigpending函数可以用来判断哪个信号是被阻塞和未决的。
     
     这儿通俗的解释就是:递送就是当信号产生后,进程接收到了信号。但有时候,在信号产生后我们并不会立马让进程接收到信号,因此阻塞该信号的递送。



    信号屏蔽字(signal mask):规定了当前要阻塞递送到该进程的信号集。稍后在10.11信号集中介绍


10.9 kill 和raise函数



10.10 alarm和pause函数


10.11 信号集
     
     信号集(signal-set):一个可以用来表示多个信号的数据类型。
     POSIX.1定义了数据类型sigset_t以包含一个信号集,并且定义了下列五个处理信号集的函数
     
NAME
       sigemptyset, sigfillset, sigaddset, sigdelset, sigismember - POSIX signal set operations.

SYNOPSIS
       #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);
Feature Test Macro Requirements for glibc (see feature_test_macros(7)):

       sigemptyset(), sigfillset(), sigaddset(), sigdelset(), sigismember(): _POSIX_C_SOURCE >= 1 || _XOPEN_SOURCE || _POSIX_SOURCE

DESCRIPTION
       These functions allow the manipulation of POSIX signal sets.

       sigemptyset() initializes the signal set given by set to empty, with all signals excluded from the set.

       sigfillset() initializes set to full, including all signals.

       sigaddset() and sigdelset() add and delete respectively signal signum from set.

       sigismember() tests whether signum is a member of set.
Objects of type sigset_t must be initialized by a call to either sigemptyset() or sigfillset() before being passed to the functions sigaddset(), sigdelset()
       and sigismember() or the additional glibc functions described below (sigisemptyset(), sigandset(), and sigorset()).  The results are undefined  if  this  is
       not done.

RETURN VALUE
       sigemptyset(), sigfillset(), sigaddset(), and sigdelset() return 0 on success and -1 on error.

       sigismember() returns 1 if signum is a member of set, 0 if signum is not a member, and -1 on error.

     

    
10.12 sigpromask函数
     

信号屏蔽字(signal mask):规定了当前阻塞而不能递送给该进程的信号集。
调用函数sigpromask可以检测或更改器信号屏蔽字,或者在一个步骤中同时执行这两个操作。
NAME
       sigprocmask - examine and change blocked signals
检测或者改变阻塞信号。

SYNOPSIS
       #include <signal.h>

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

   Feature Test Macro Requirements for glibc (see feature_test_macros(7)):

       sigprocmask(): _POSIX_C_SOURCE >= 1 || _XOPEN_SOURCE || _POSIX_SOURCE

DESCRIPTION
       sigprocmask()  is  used  to  fetch  and/or  change the signal mask of the calling thread.  The signal mask is the set of signals whose delivery is currently  blocked for the caller (see also signal(7) for more details).
信号屏蔽字是一组信号递送被阻塞的信号集合。就是信号屏蔽字里面的信号都被阻塞递送了。

       The behavior of the call is dependent on the value of how, as follows.

       SIG_BLOCK
              The set of blocked signals is the union of the current set and the set argument.
被阻塞的信号集是参数set的信号集合

       SIG_UNBLOCK
              The signals in set are removed from the current set of blocked signals.  It is permissible to attempt to unblock a signal which is not blocked.
被设置的信号集合从当前阻塞信号集合中移除。

       SIG_SETMASK
              The set of blocked signals is set to the argument set
阻塞的信号集合被赋值为参数set。

对于SIG_UNBLOCK与SIG_SETMASK,我们尽量选择在SIGBLOCK时,保存当前的信号集,然后在恢复时采用SIG_SETMASK,恢复之前的信号集。之所以不采用SIG_UNBLOCK,是因为可能在这之前,已经阻塞过该信号。

 If oldset is non-null, the previous value of the signal mask is stored in oldset.

       If set is NULL, then the signal mask is unchanged (i.e., how is ignored), but the current value of the signal mask is nevertheless returned in oldset (if it
       is not NULL).

       The use of sigprocmask() is unspecified in a multithreaded process; see pthread_sigmask(3).

RETURN VALUE
       sigprocmask() returns 0 on success and -1 on error.

     10.13 sigpending函数

NAME
       sigpending - examine pending signals
检测被阻塞的未决的信号。
SYNOPSIS
       #include <signal.h>

       int sigpending(sigset_t *set);

   Feature Test Macro Requirements for glibc (see feature_test_macros(7)):

       sigpending(): _POSIX_C_SOURCE >= 1 || _XOPEN_SOURCE || _POSIX_SOURCE

DESCRIPTION
       sigpending()  returns  the set of signals that are pending for delivery to the calling thread (i.e., the signals which have been raised while blocked).  The  mask of pending signals is returned in set.
       sigpending函数返回一个信号集,该信号集内包含调用线程中处于未决状态的信号。也就是当前处于未决状态的信号的集合。

RETURN VALUE
       sigpending() returns 0 on success and -1 on error.

10.14 sigaction函数
NAME
       sigaction - examine and change a signal action
作用:检测和改变信号的动作。
SYNOPSIS
       #include <signal.h>

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


   Feature Test Macro Requirements for glibc (see feature_test_macros(7)):

       sigaction(): _POSIX_C_SOURCE >= 1 || _XOPEN_SOURCE || _POSIX_SOURCE
DESCRIPTION
       The sigaction() system call is used to change the action taken by a process on receipt of a specific signal.  (See signal(7) for an overview of signals.)
        sigaction系统调用用于改变进程在接收到一个特定信号时的行为。

       signum specifies the signal and can be any valid signal except SIGKILL and SIGSTOP.
       参数signum指定信号的类型,可以是除了SIGKILL和SIGSTOP以外的任何信号类型。这点和signal类似。
       If act is non-null, the new action for signal signum is installed from act.  If oldact is non-null, the previous action is saved in oldact.
      如果参数act非空,那么signum信号的新的动作由参数act决定。如果oldact非空,那么oldact保存之前的行为。
       The sigaction structure is defined as something like:

           struct sigaction {
               void     (*sa_handler)(int);//指定SIG_DFL,SIG_IGN,orsignal handling function。类似siganl函数的第二个参数。
               void     (*sa_sigaction)(int, siginfo_t *, void *);
               sigset_t   sa_mask;
               int        sa_flags;
               void     (*sa_restorer)(void);//不会被指定

           };
On some architectures a union is involved: do not assign to both sa_handler and sa_sigaction.

       The sa_restorer element is obsolete and should not be used.  POSIX does not specify a sa_restorer element.

       sa_handler  specifies  the  action  to be associated with signum and may be SIG_DFL for the default action, SIG_IGN to ignore this signal, or a pointer to a
       signal handling function.  This function receives the signal number as its only argument.
      sa_handler指定与signum相关的action。
       If SA_SIGINFO is specified in sa_flags, then sa_sigaction (instead of sa_handler) specifies the signal-handling function for signum.  This function receives
       the  signal  number as its first argument, a pointer to a siginfo_t as its second argument and a pointer to a ucontext_t (cast to void *) as its third argu‐ ment.
      如果sa_flags中指定了SA_SIGINFO字段,那么sa_sigaction而不是sa_handler为signum指定信号处理函数。这个函数接收signal number作为第一个参数,指向siginfo_t的指针作为第二个参数,一个执行void*作为第三个参数。
      注意:sa_sigaction和sa_handler这两个字段,这两个实现可能使用同一个存储区,所以应用程序只能一次使用这两个字段中的一个,也就是说,sa_handler与sa_sigaction只能二选一。

       sa_mask specifies a mask of signals which should be blocked (i.e., added to the signal mask of the thread in which the signal  handler  is  invoked)  during execution of the signal handler.  In addition, the signal which triggered the handler will be blocked, unless the SA_NODEFER flag is used.
     sa_mask字段说明了一个信号集,在调用该信号捕捉函数之前,这一信号集要加到进程的信号屏蔽字中。sa_mask指定了在信号执行函数执行期间,哪些信号将会被阻塞。

       sa_flags specifies a set of flags which modify the behavior of the signal.  It is formed by the bitwise OR of zero or more of the following:详见man sigaction。指定了一组调整信号行为的标识。
      siginfo_t结构包含了信号产生原因的有关信息。 The siginfo_t argument to sa_sigaction is a struct with the following elements:详见man sigaction

       
RETURN VALUE
       sigaction() returns 0 on success and -1 on error.

     10.15 sigsetjmp和siglongjmp函数


10.16 sigsuspend函数
NAME
       sigsuspend - wait for a signal
作用:等待信号
SYNOPSIS
       #include <signal.h>

       int sigsuspend(const sigset_t *mask);

   Feature Test Macro Requirements for glibc (see feature_test_macros(7)):

       sigsuspend(): _POSIX_C_SOURCE >= 1 || _XOPEN_SOURCE || _POSIX_SOURCE
DESCRIPTION
       sigsuspend()  temporarily replaces the signal mask of the calling process with the mask given by mask and then suspends the process until delivery of a signal whose action is to invoke a signal handler or to terminate a process.
      sigsuspend暂时用参数mask的信号集代替当前进程的信号集,然后暂停进程知道信号的处理程序被触发或者进程中止。
       If the signal terminates the process, then sigsuspend() does not return.  If the signal is caught,  then  sigsuspend()  returns  after  the  signal  handler    returns, and the signal mask is restored to the state before the call to sigsuspend().
       如果信号中止进程,那么不会返回sigsuspend。如果信号被捕获,那么在信号处理程序执行完毕后,返回sigsuspend,并且signal mask会恢复到调用sigsuspend之前的信号集。
       It is not possible to block SIGKILL or SIGSTOP; specifying these signals in mask, has no effect on the process's signal mask.


RETURN VALUE
       sigsuspend() always returns -1, normally with the error EINTRsigsuspend函数在一个原子操作中,先恢复信号屏蔽字,然后再使进程暂停。
注意:我个人理解,sigsuspend就是为了解除信号集的阻塞,并且进行一系列的处理,从而方便进程的进一步操作。

// signal2.c
/*该程序为了说明sigaction、sigemptyset、sigaddset、sigprocmask、sigsuspend、sigpending函数
  

*/

#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <stdlib.h>
#include <string.h>
int test_quit;
static void sig_quit(int signo)
{
printf("caught SIGQUIT\n");
test_quit = 1;
}
int main()
{
sigset_t oldmask;
sigset_t pendmask;
sigset_t waitmask;
struct sigaction act,oact;
memset(&act,0,sizeof(struct sigaction));

act.sa_handler = sig_quit;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
sigaction(SIGQUIT,&act,&oact);
//signal(SIGQUIT, sig_quit);

sigemptyset(&act.sa_mask);
sigaddset(&act.sa_mask, SIGQUIT);

sigprocmask(SIG_BLOCK, &act.sa_mask, &oldmask);
printf("Please entrl CTRL+\\ before sleep\n");
sleep (5);//休眠5秒钟
printf("after sleep\n");

sigpending(&pendmask);/*这儿是为了说明SIGQUIT信号的确是悬而未决的*/
if (sigismember(&pendmask, SIGQUIT))
{
printf("\nSIGQUIT pending\n");
}
//sigprocmask(SIG_SETMASK, &oldmask, NULL);/*恢复默认的信号集,此时解除了SIGQUIT信号的阻塞,SIGQUIT信号被送到进程。*/
sigemptyset(&waitmask);
sigsuspend(&waitmask);
printf("hello,world\n");
if(test_quit == 1)
    printf("test_quit == 1\n");
else
   printf("test_quit is not 1\n");
printf("before sleep\n");
sleep(5);
printf("after sleep\n");
return (0);
}说明:^\是ctrl+\在中断上的显示。
有两种运行方式,分别说明了sigpending和sigsuspend函数的作用。运行方式run1 and run2如下所示:
compile :$ gcc -Wall signal2.c -o signal2
 这个程序比较好的说明了sigpending的作用,在提示“Please entrl CTRL+\ before sleep”时输入CTRL+\,则出现了阻塞未决的SIGQUIT信号,因此sigpending的信号集中有SIGQUIT信号。
run1:$./signal2
Please entrl CTRL+\ before sleep/*出现这句话时,输入CTRL+\*/
^\after sleep

SIGQUIT pending
caught SIGQUIT
hello,world
test_quit == 1
before sleep
^\after sleep
run2:$./siganl2
Please entrl CTRL+\ before sleep /*出现这句话时,没有输入CTRL+\*/
after sleep
^\caught SIGQUIT/*在sigsuspend等待时,输入CTRL+\*/
hello,world
test_quit == 1
before sleep
after sleep



10.17 abort函数

10.18system函数

10.19sleep函数

10.20 作业控制信号
10.21其他特征
10.22小结

原创粉丝点击