读书笔记-APUE第三版-(10)信号

试用一下CSDN博客的在线markdown编辑器,感觉还不错:)

信号概念

信号是一种软件中断,用于提供异步事件处理机制。以下情形会产生信号:

  1. 终端键盘输入,比如Ctrl+c(SIGINT)。
  2. 硬件异常,比如除零&浮点数溢出(SIGFPE),非法内存引用(SIGSEGV)等。硬件探测到异常后通知内核,内核向应用进程发送信号。
  3. kill函数或者kill命令给指定进程/进程组发送各种信号(不局限于杀死进程);abort函数发送SIGABRT信号导致进程非正常退出等。
  4. 软件产生信号,比如网络连接中的带外数据(SIGURG),向管道写入数据但无读取方(SIGPIPE),闹钟定时器alarm超时(SIGALARM),还有前一章中作业控制中的一些场景也会产生信号,子进程退出向父进程发送信号(SIGCHLD,可以在处理SIGCHLD信号时使用wait),Ctrl+z停止前台进程组(SIGTSTP),后台进程组需要进行输入/输出(SIGTTIN/SIGTTOU)等。

对信号的处理方式有忽略(不能忽略SIGKILL、SIGSTOP和硬件异常)、默认动作(大部分都是终止进程,部分还会产生core)和设置自定义处理函数三种。

signal函数

void (*signal(int signo, void (*func)(int))(int);
或者:
typedef void Sigfunc(int);
Sigfunc *signal(int, Sigfunc *);
#define SIG_ERR   (void (*)())-1    
#define SIG_DFL   (void (*)())0     默认动作
#define SIG_IGN   (void (*)())1     无视信号

signal函数用于对信号signo设置信号函数func,返回之前的信号处理函数。signal函数在ISO C中定义,不涉及到多进程,定义够简明+含糊,所以基本上没什么用,一般用sigaction(后面介绍)

不可靠信号处理

早期UNIX系统中信号机制是不可靠的,主要存在以下几个问题:

  1. 每次信号处理时,信号的处理方式会被重置为默认动作。常见的处理方式是在信号处理函数中重新设置,但如果信号发生在重新设置生效之前,信号会被丢失掉。
  2. 不能某些时候选择阻塞信号,只能选择SIG_IGN。

自动重启被中断的系统调用

一些“低速”系统调用在信号发生时,可能会被中断,比如被阻塞的对管道或网络设备的读写操作、pause函数和某些ioctl函数。为了减少以下检查代码,操作系统对ioctl、read、write和wait等系统调用都会进行自动重启。

again:
    if ((n = read(fd, buf, BUFFSIZE)) < 0) {
        if (errno == EINTR)
            goto again;     /* just an interrupted system call */               
    }

下表总结了使用signal和sigaction函数时,各系统是否重置默认动作、能否阻塞信号和自动重启动被中断的系统调用等行为。
读书笔记-APUE第三版-(10)信号_第1张图片

可重入函数

因为信号处理函数是异步的,信号处理时中断程序正在运行的方法,所以要求在信号处理函数中被调用的方法具有可重入性(reentrant)或异步信号安全性(async-signalsafe),很多函数都不具备可重入性,比如:

  1. 使用了static数据结构(静态数据结构可能在主程序中被覆写)。
  2. 调用了malloc或者free(malloc中维护了已分配区域链表,被中断时可能正处于交换链表的过程中)。
  3. 标准I/O库(使用了全局数据结构)

可靠信号

几个术语:

  1. generated:事件发生,生成信号。
  2. delivered:信号已经传递到进程,开始被处理
  3. pending:未决,信号介于genaerated和delivered之间
  4. blocking:进程可以选择阻塞信号,被阻塞的信号处于pending状态,直到进程unbloc信号或者设置SIG_IGN(是的,只要在delivered之前都还重新设置信号处理函数)。
  5. queued:被阻塞后信号多次生成,一些系统支持信号队列,后续多次deliver信号。(使用sigqueue函数依次发送信号给指定进程)

alarm&pause&sleep函数

unsigned int alarm(unsigned int seconds);
int pause(void);
unsigned int sleep(unsigned int seconds)

alarm用于设置闹钟,超时后,内核产生SIGALARM信号,默认行为是终止进程。每个处理器只有一个闹钟时钟,调用alarm时,如果上次闹钟还没有超时,返回它剩余时间。pause函数挂起当前进程,直到信号生成并被处理后才返回。早期UNIX版本有用alarm和pause实现sleep,但alarm和pause调用存在竞争条件,如果alarm超时之后pause才被调用,进程可能被永久挂起,更好的方案是使用sigprocmask和sigsuspend。

信号集

因为各个系统信号数量都不同,比如Linux的信号数量已经超过40中,超出整型位数,所以POSIX定义了sigset_t用来代表信号集合,并提供了一系列函数用来操纵信号集。

    #include  
    int sigemptyset(sigset_t *set);
    int sigfillset(siget_t *set); 
    int sigaddset(sigset_t *set, int signo);
    int sigdelset(setset_t *set, int signo); 
    int sigismember(const sigset_t *set, int signo);
    int sigprocmask(int how, const sigset_t *restrict set, sigset_t *restrict oset);
    int sigpending(sigset_t *set);
    int sigsuspend(const sigset_t *sigmask);

其中:

  1. sigprocmask被进程用于屏蔽(阻塞)/解除屏蔽信号集。
  2. sigpending返回当前被阻塞的信号集。
  3. sigsupend等于sigprocmask和sigpending的原子操作,用于挂起进程并等待特定信号。sigsuspend和kill(SIGUSR1和SIGUSR2信号)函数组合是有那个可以用来实现TELL_WAIT,TELL_PARENT,TELL_CHILD,WAIT_PARENT和WAIT_CHILD父子进程同步

sigaction函数

int sigaction(int signo, const struct sigaction *restrict act,  struct sigaction        *restrict oact);
struct sigactgion { 
    void (*sa_handler)(int);
    sigset_t sa_mask;
    int sa_flags; 
    void (*sa_sigaction)(int siginfo_t *, void *);
}

sigaction函数非常强大,能够定义信号处理函数(sa_handler),同时设置屏蔽信号集(sa_mask),是否重启被中断的系统调用(sa_flags),更多信号和进程的信息(siginfo_t)。系统一般使用sigaction实现signal函数。

include "apue.h"
/* Reliable version of signal(), using POSIX sigaction().  */
Sigfunc *
signal(int signo, Sigfunc *func)
{
    struct sigaction    act, oact;

    act.sa_handler = func;
    sigemptyset(&act.sa_mask);
    act.sa_flags = 0;
    if (signo == SIGALRM) {
#ifdef  SA_INTERRUPT
    act.sa_flags |= SA_INTERRUPT;
#endif
    } else {
        act.sa_flags |= SA_RESTART;
    }
    if (sigaction(signo, &act, &oact) < 0)
        return(SIG_ERR);
    return(oact.sa_handler);
}

你可能感兴趣的:(Fundamental,Basic,~APUE,3rd)