zoukankan      html  css  js  c++  java
  • Libev源码分析06:异步信号同步化--sigwait、sigwaitinfo、sigtimedwait和signalfd

    一:信号简述

             信号是典型的异步事件。内核在某个信号出现时有三种处理方式:

    a:忽略信号,除了SIGKILL和SIGSTOP信号不能忽略外,其他大部分信号都可以被忽略;

    b:捕捉信号,也就是在信号发生时调用一个用户函数,注意不能捕捉SIGKILL和SIGSTOP;

    c:执行系统默认动作,注意大多数信号的系统默认动作是终止进程。

             调用execve执行一个新的进程时,新进程的信号处理方式要么是忽略,要么是系统默认方式。如果调用进程忽略该信号,则新进程也忽略该信号,如果调用进程捕捉该信号,或者执行系统默认动作,新进程则按照默认行为处理该信号,这是因为调用进程的信号处理函数在新进程中已经无效了。

             如果是调用fork产生子进程,则子进程的信号处理方式完全继承父进程的处理方式。这是因为子进程复制了父进程的地址空间,因此父进程中的信号处理函数在子进程中同样被复制了。

            

             当信号产生以后,对信号采取了动作时,称为向进程“递送”了一个信号,在信号产生(generation)和递送(delivery)之间的时间间隔内,称信号是未决的(pending)。

             进程可以“阻塞”信号,如果为进程产生了一个阻塞的信号,而且对该信号的动作是调用信号处理函数捕捉该信号,或者是系统默认动作,则会为该进程将此信号保持为未决状态,直到a:对此信号解除了阻塞,或者b:对此信号的动作改为忽略。内核在递送一个原来被阻塞的信号给进程时(而不是产生信号时),才决定对它的处理方式,因此,进程在信号被递送之前仍可改变对该信号的动作。

             如果在进程解除某个信号的阻塞之前,该信号发生了多次,则系统可以递送该信号一次或多次。如果递送多次,则称对这些信号进行了排队。但是除非支持POSIX.1实时扩展,否则大多数UNIX并不对信号排队,也就是指递送该信号一次。

             如果有多个信号要递送给一个进程,POSIX.1并未规定这些信号的递送顺序。

             每个进程都有一个信号屏蔽字,它规定了当前要阻塞的信号集。POSIX.1使用数据类型sigset_t表示一个信号集,进程可以调用sigprocmask来检测和更改当前信号屏蔽字。

     

             对信号的处理是以进程为单位的,也就是说在多线程环境中,信号的处理是进程中所有线程共享的。但是每个线程可以有自己的信号屏蔽字,所以,单个线程可以阻塞某些信号,当某个线程修改了某个信号的处理方式后,所有线程共享这个处理方式的改变。

             进程中的信号是递送到单个线程的。如果信号与硬件故障相关,该信号就被递送到引起该事件的线程中去,而其他信号则被递送到任意一个没有阻塞该信号的线程。sigprocmask的行为在多线程中没有定义,线程必须使用相应的pthread_sigmask。

     

             除了使用信号处理函数异步的捕捉信号之外,还可以将这种异步行为变得同步,有两种方法:

             a:调用sigwaitinfo、sigtimedwait或sigwait,这些函数会阻塞调用线程,直到信号集set中的某个信号被递送为止,这些函数都会返回递送信号的信息。

             b:调用signalfd,它返回一个文件描述符,针对该描述符的read的操作将会阻塞,直到signalfd指定的信号集set中的某个信号递送给调用者为止,此时read返回一个描述该信号的结构。

     

    二:sigwaitinfo、sigtimedwait和sigwait

    1:sigwait

    #include <signal.h>
    int sigwait(const sigset_t *set, int *sig);

             sigwait从set中选择一个未决信号(pending),从进程的未决信号集合中移除该信号,并在sig中返回该信号值。如果set中的所有信号都不是pending状态,则sigwait会阻塞调用它的线程,直到set中的信号变为pending。

             为了避免错误动作发生,线程在调用sigwait之前,必须阻塞那些它正在等待的信号,否则行为是未定义的。sigwait函数会自动取消信号集的阻塞状态,直到有新的信号被递送。在返回之前,sigwait将恢复线程的信号屏蔽字。如果信号在sigwait调用的时候没有被阻塞,则在sigwait调用之前会出现一个时间窗,在这个窗口期,某个信号可能在线程调用sigwait之前就被递送了。

             sigwait只是取消set中信号的阻塞,其他信号则维持原状态,比如,在sigwait调用之前阻塞了所有信号,而set中只包含了SIGINT,则发送SIGABRT也不会有效果。

     

             如果多个线程在sigwait调用时,等待的是同一个信号,当信号递送的时候,只有一个线程可以从sigwait中返回,具体是那个线程则是未定义的。   

             如果信号被捕获(进程通过使用sigaction建立了一个信号处理程序),而且线程正在sigwait调用中等待同一信号,那么这时将由操作系统实现来决定以何种方式递送信号。在这种情况下,操作系统实现可以让sigwait返回,也可以激活信号处理程序,但不可能出现两者皆可的情况。

     

             sigwait成功返回0,失败时返回表示错误的值。

     

             使用sigwait的好处在于它可以简化信号处理,允许把异步产生的信号用同步的方式处理。为了防止信号中断线程,可以把信号加到每个线程的信号屏蔽字中,然后安排专用线程作信号处理。这些专用线程可以进行函数调用,不需要担心在信号处理程序中调用哪些函数是安全的,因为这些函数调用来自正常的线程环境,而非传统的信号处理程序,传统信号处理程序通常会中断线程的正常执行。

     

    2:sigwaitinfo和sigtimedwait

    #include <signal.h>
    
    int sigwaitinfo(const sigset_t *set, siginfo_t *info);
    int sigtimedwait(const sigset_t *set, siginfo_t *info, const struct timespec *timeout);

            除了返回信息方面,sigwaitinfo的行为基本上与sigwait类似。sigwait在sig中返回触发的信号值;而sigwaitinfo的返回值就是触发的信号值,并且如果info不为NULL,则sigwaitinfo返回时,还会在siginfo_t *info中返回更多该信号的信息,siginfo_t的结构体定义如下:

    siginfo_t 
    {
        int      si_signo;    /* Signal number */
        int      si_errno;    /* An errno value */
        int      si_code;     /* Signal code */
        int      si_trapno;   /* Trap number that caused hardware-generated signal (unused on most architectures) */
        pid_t    si_pid;      /* Sending process ID */
        uid_t    si_uid;      /* Real user ID of sending process */
        int      si_status;   /* Exit value or signal */
        clock_t  si_utime;    /* User time consumed */
        clock_t  si_stime;    /* System time consumed */
        sigval_t si_value;    /* Signal value */
        int      si_int;      /* POSIX.1b signal */
        void    *si_ptr;      /* POSIX.1b signal */
        int      si_overrun;  /* Timer overrun count; POSIX.1b timers */
        int      si_timerid;  /* Timer ID; POSIX.1b timers */
        void    *si_addr;     /* Memory location which caused fault */
        long     si_band;     /* Band event (was int in glibc 2.3.2 and earlier) */
        int      si_fd;       /* File descriptor */
        short si_addr_lsb;    /*Least significant bit of address (since Linux 2.6.32)*/
    }
    

            比如:info->si_signo是信号值;info->si_code是该信号产生的原因 

     

            sigtimedwait的行为又与sigwaitinfo的行为类似,只是它多了一个超时参数timeout,也就是可以设置该函数的最大阻塞时间。struct timespec的定义如下:

    struct timespec 
    {
        long    tv_sec;         /* seconds */
        long    tv_nsec;        /* nanoseconds */
    }
    

            如果该结构体中的成员都置为0的话,则sigtimedwait将会立即返回,此时如果确实有pending信号的话,就返回该信号的信息,否则返回超时错误。

            POSIX没有规定,如果timeout为NULL的话sigtimedwait的行为,Linux下,timeout为NULL时,sigtimedwait的行为与sigwaitinfo是一样的。

     

    三:signalfd

    #include <sys/signalfd.h>
    
    int signalfd(int fd, const sigset_t *mask, int flags);

            signalfd用来创建一个接收信号的文件描述符,该文件描述符可用于select、poll和epoll。mask参数中指定了调用者希望接收的信号集,一般而言,信号集中的信号在调用signalfd之前,应该使用sigprocmask进行阻塞,这样这些信号就不会按照原来的处理方式进行处理了。注意不能通过signalfd的文件描述符接收SIGKILL或SIGSTOP信号,如果在mask中指定了这俩信号,则会默认忽略掉。

             如果fd参数为-1,则signalfd创建新的文件描述符,并将信号集mask与该文件描述符相关联。如果fd不为-1,则fd必须是一个已经存在的signalfd文件描述符,mask用于更新与之对应的信号集。

             在Linux2.6.26之前的版本,flags参数是无用的,必须置为0。从Linux2.6.27开始,可以在flags中指定下面的值以改变signalfd的行为:

    SFD_NONBLOCK:在新创建的文件描述符上设置O_NONBLOCK文件状态标志;

    SFD_CLOEXEC:在新建的文件描述符上设置FD_CLOEXEC标志。

              注意上面的两个标志,都是针对新建描述符而言的(fd为-1),如果是一个已经存在的描述符,设置这两个标志都没有作用(已通过程序验证)。

     

             signalfd创建描述符支持下列操作:

             read:如果mask中的一个或多个信号是未决状态(pending)的话,则read将会在其buffer参数中返回一个或多个描述信号的signalfd_siginfo结构。read操作尽可能多的返回未决信号,并将其信息填充到buffer。buffer至少应该是sizeof(struct signalfd_siginfo) 个字节,read的返回值就是读取的字节数。

             在read之后,read返回的信号就被消费掉了,不再处于未决状态(也就是不能再由信号处理函数捕捉,也不能使用sigwaitinfo接收)。

             如果mask中的信号都不是未决状态,若文件描述符置为非阻塞的话,read返回EAGAIN错误,否则read会一直阻塞,直到mask中的信号产生。

            

             select、poll、epoll等:如果mask中有一个或多个信号处于未决状态的话,则这些文件描述符就是可读的,从而可以使得select、poll或者epoll返回。

     

             close:当不再需要该文件描述符时,应该使用close关闭。与其他文件描述符一样,如果底层signalfd对象关联的所有文件描述符都关闭之后,则该对象的资源就会被内核释放掉。

     

             结构体signalfd_siginfo的定义如下:

    struct signalfd_siginfo 
    {
        uint32_t ssi_signo;   /* Signal number */
        int32_t  ssi_errno;   /* Error number (unused) */
        int32_t  ssi_code;    /* Signal code */
        uint32_t ssi_pid;     /* PID of sender */
        uint32_t ssi_uid;     /* Real UID of sender */
        int32_t  ssi_fd;      /* File descriptor (SIGIO) */
        uint32_t ssi_tid;     /* Kernel timer ID (POSIX timers) */
        uint32_t ssi_band;    /* Band event (SIGIO) */
        uint32_t ssi_overrun; /* POSIX timer overrun count */
        uint32_t ssi_trapno;  /* Trap number that caused signal */
        int32_t  ssi_status;  /* Exit status or signal (SIGCHLD) */
        int32_t  ssi_int;     /* Integer sent by sigqueue(3) */
        uint64_t ssi_ptr;     /* Pointer sent by sigqueue(3) */
        uint64_t ssi_utime;   /* User CPU time consumed (SIGCHLD) */
        uint64_t ssi_stime;   /* System CPU time consumed (SIGCHLD) */
        uint64_t ssi_addr;    /* Address that generated signal (for hardware-generated signals) */
        uint8_t  pad[X];      /* Pad size to 128 bytes (allow for additional fields in the future) */
    };
    

             该结构体中的每个成员都类似于结构体siginfo_t中的成员。对于一个特定的信号,read返回的signalfd_siginfo结构体中,并非所有的成员都是有效的;具体哪些成员是有意义的,可以通过成员ssi_code来检测,该成员类似于siginfo_t中的si_code成员。

     

             fork之后,子进程拥有了父进程的signalfd文件描述符的副本,在子进程的文件描述符上调用read,将会返回子进程的未决信号。

             类似于其他文件描述符,除非设置了close-on-exec标志,否则signalfd文件描述符在execve之后还是保持打开的,在execve之前可读的信号,在新的进程中依然可读。

             在多线程环境中,signalfd文件描述符与标准的多线程中信号的语义是一样的。也就是说,当一个线程读取signalfd文件描述符时,它会读取到针对该线程的信号,以及针对整个进程的信号。

     

             调用signalfd成功时,返回一个signalfd文件描述符,该描述符要么是一个新的signalfd描述符(fd为-1的情况下),要么就是fd(fd已经是个有效的signalfd描述符的情况下)。

             调用signalfd错误时,返回-1,并设置相应的errno。

     

              signalfd是linux特有的,自linux2.6.22才开始引入,在glibc的2.8版本中开始被支持。

     

              一个进程可以创建多个signalfd文件描述符,这样可以在不同的signalfd描述符上等待不同的信号。如果多个文件描述符中的mask上具有同一个信号,当信号发生时,所有描述符都变得可读,可以从任意一个描述符读取该信号。这种情况下,如果使用select、poll或者epoll等待这些signalfd描述符,则这些描述符都会变成可读的,从其中之一读取之后,该信号就被消费掉,读取其他的描述符就会阻塞,所以要避免这种情况(已通过程序验证)。

     

             例子:

    #include <sys/signalfd.h>
    #include <signal.h>
    #include <unistd.h>
    #include <stdlib.h>
    #include <stdio.h>
    
    #define handle_error(msg) 
       do { perror(msg); exit(EXIT_FAILURE); } while (0)
    
    int main(int argc, char *argv[])
    {
       sigset_t mask;
       int sfd;
       struct signalfd_siginfo fdsi;
       ssize_t s;
    
       sigemptyset(&mask);
       sigaddset(&mask, SIGINT);
       sigaddset(&mask, SIGQUIT);
    
       if (sigprocmask(SIG_BLOCK, &mask, NULL) == -1)
           handle_error("sigprocmask");
    
       sfd = signalfd(-1, &mask, 0);
       if (sfd == -1)
           handle_error("signalfd");
    
       for (;;) {
           s = read(sfd, &fdsi, sizeof(struct signalfd_siginfo));
           if (s != sizeof(struct signalfd_siginfo))
               handle_error("read");
    
           if (fdsi.ssi_signo == SIGINT) {
               printf("Got SIGINT
    ");
           } else if (fdsi.ssi_signo == SIGQUIT) {
               printf("Got SIGQUIT
    ");
               exit(EXIT_SUCCESS);
           } else {
               printf("Read unexpected signal
    ");
           }
       }
    }
    

             结果:

    $ ./signalfd_demo
    ^C                   # Control-C generates SIGINT
    Got SIGINT
    ^C
    Got SIGINT
    ^                    # Control- generates SIGQUIT
    Got SIGQUIT
    

             参考:

    《Unix环境高级编程》

    man手册

  • 相关阅读:
    (Java实现) 洛谷 P1106 删数问题
    (Java实现) 洛谷 P1603 斯诺登的密码
    (Java实现) 洛谷 P1036 选数
    (Java实现) 洛谷 P1012 拼数
    (Java实现) 洛谷 P1028 数的计算
    (Java实现) 洛谷 P1553 数字反转(升级版)
    (Java实现) 洛谷 P1051 谁拿了最多奖学金
    (Java实现) 洛谷 P1051 谁拿了最多奖学金
    (Java实现) 洛谷 P1106 删数问题
    目测ZIP的压缩率
  • 原文地址:https://www.cnblogs.com/gqtcgq/p/7247097.html
Copyright © 2011-2022 走看看