Linux下的进程(五):信号 - HeavyYuan/A-CD-Record-Management-System GitHub Wiki

信号(Signal)

信号是软件中断。

信号提供了一种处理异步事件的方法。常见的如:

键盘输入ctrl + c,(终端)给前台进程发送SIGINT信号,终止了进程运行;

键盘键入ctrl + z ,终端发送SIGTSPT给前台进程,挂起进程。

一、基本概念

每个信号都有一个编号名字

编号是从1开始的正整数常量(不存在编号为0的信号)

名字是以SIG打头字符,如SIGINT,SIGTSPT,SIGQUIT

给进程发送信号,可以指定信号编号,也可以指定信号名 kill -INT/-2 pid

产生信号的场景

  1. 用户在终端输入某些组合键(如:CTRL + C),引发终端产生信号
  2. 硬件异常产生信号(由硬件检测到),如除数为0,无效的内存引用。

SIGSEGV信号让进程产生Segmentation fault

  1. kill函数或命令向任意进程发送信号
  2. 达到既定的软件条件后,通过发送信号的方式通知其他进程

信号的处理方式

  1. 忽略信号。SIGKILL和SIGSTOP不能忽略(系统限制)
  2. 执行系统默认动作。大多数信号的默认处理是终止该进程
  3. 捕捉信号。SIGKILL和SIGSTOP不能捕捉(系统限制)

信号相关概念

中断的系统调用: 早期的UNIX系统中,系统调用被信号中断后不在继续执行

自动重启动的系统调用: 被信号中断的系统调用,在信号处理程序运行完成后,会自动重启动继续运行。

可重入函数: 函数A在执行过程中,被信号中断,而信号处理程序中也会执行函数A,并且不会出现任何问题。

​ 则,函数A是可重入函数 ,也称这种函数为异步信号安全的(async-singal safe)

未决(Pending): 已经产生(generation),但是还没有递送(delivery)给进程的信号,此种状态称之为Pending.

sigpending函数可以获取Pending状态的信号

信号集(Signal Set): 表示多个信号的集合,对应数据类型为sigset_t

对信号集操作的函数有:sigemptyset,sigfillset,sigaddset,sigdelset,sigismember

信号屏蔽字(Signal Mask): 是一个信号集合,该集合内的信号都会被阻塞而不被递送给进程

sigprocmask函数可以返回当前信号屏蔽字和设置信号屏蔽字

二、信号应用相关问题

信号的应用可以实现多进程控制,进程协作等能力。

这可以帮助用户实现更为人性化的功能,但是也增加了一下复杂度,以下问题需要在信号应用中作细致考虑

问题一:竞争条件的影响(race condition)

一个实例是sleep函数的实现,一个实例是临界代码的保护

  1. sleep实现

    #include    <stdio.h>
    #include    <signal.h>
    #include    <unistd.h>
    
    static void
    sig_alrm(int signo)
    {
        /* nothing to do, just return to wake up the pause */
    }
    
    unsigned int
    sleep1(unsigned int seconds)
    {
        if (signal(SIGALRM, sig_alrm) == SIG_ERR)
            return(seconds);
        alarm(seconds);     /* start the timer */
        pause();            /* next caught signal wakes us up */
        return(alarm(0));   /* turn off timer, return unslept time */
    }

    alarm和pause之间存在竞争条件。

    在一个繁忙的系统中,可能存在alarm在pause之前就超时,并调用信号处理程序。

    如果后续没有捕捉到其他信号,程序将永远被挂起。

    解决办法是引入setjmp和longjmp,如下:

    #include    <setjmp.h>
    #include    <signal.h>
    #include    <unistd.h>
    #include    <stdio.h>
    
    static jmp_buf  env_alrm;
    
    static void
    sig_alrm(int signo)
    {
        longjmp(env_alrm, 1);
    }
    
    unsigned int
    sleep2(unsigned int seconds)
    {
        if (signal(SIGALRM, sig_alrm) == SIG_ERR)
            return(seconds);
        if (setjmp(env_alrm) == 0) {
            alarm(seconds);     /* start the timer */
            pause();            /* next caught signal wakes us up */
        }
        return(alarm(0));       /* turn off timer, return unslept time */
    }

    即使出现sleep1的情况,longjmp返回后不再满足执行pause的条件,sleep2会返回。

  2. 临界代码保护

保护临界代码不受信号中断的影响

int main()
{
	sigset_t newmask, oldmask;
	sigemptyset(&newmask);
	sigaddset($newmask, SIGINT);
  /*block SIGINT and save current signal mask*/
  if(sigprocmask(SIG_BLOCK, &newmask, &oldmask) < 0)
    	err_says("SIG_BLOCK error");
  
  /*Critical region of code*/
  
  /*restore signal mask, which unblocks SIGINT*/
  if(sigprocmask(SIG_SETMASK, &oldmask, NULL) < 0)
    err_says("SIG_SETMASK error");
  
  pause(); /*wait for signal to occur*/
  
  /*continue processing*/
  
}

第二个sigprocmask解除对SIGINT的阻塞,其和pause之间存在竞争条件。

如果在解除阻塞时 (还没有解除)到 执行pause前,信号到达,此时信号会被丢弃。

而后续可能不会再有信号到达,因此会永远pause下去。

解决办法是引入sigsuspend函数,该函数在一个原子操作中先恢复信号屏蔽字,然后是进程进入休眠,直到捕捉到

一个信号,该函数返回。上述代码修改成:

int main()
{
	sigset_t waitmask;
  ...
    
  /*Pause , allowing all signals except signals in `waitmask`*/
 if(sigsuspend(&waitmask) != -1)  
   err_says("sigsuspend error");
  
/*restore signal mask, which unblocks SIGINT*/
  if(sigprocmask(SIG_SETMASK, &oldmask, NULL) < 0)
    err_says("SIG_SETMASK error");
}

sigsuspend函数返回后,其将信号屏蔽字恢复成调用它之前的值。

sigsuspend函数的局限性在于,无法处理在等待信号期间希望调用其他系统函数的场景,其只适用于在等待信号

期间希望去休眠的场景。

问题二:信号处理程序之间的交互

在问题一中,sleep2用到setjmplongjmp来解决条件竞争导致的进程永久pause问题。

setjmplongjmp带来另外的问题是,在有多个信号处理程序在执行时,xxxjmp实现的信号处理程序的执行,

会中断其他信号处理程序的执行,让其他信号处理程序提早结束。如:

#include "apue.h"

unsigned int    sleep2(unsigned int);
static void     sig_int(int);

int
main(void)
{
    unsigned int    unslept;

    if (signal(SIGINT, sig_int) == SIG_ERR)
        err_sys("signal(SIGINT) error");
    unslept = sleep2(5);
    printf("sleep2 returned: %u\n", unslept);
    exit(0);
}

static void
sig_int(int signo)
{
    int             i, j;
    volatile int    k;

    /*
     * Tune these loops to run for more than 5 seconds
     * on whatever system this test program is run.
     */
    printf("\nsig_int starting\n");
    for (i = 0; i < 300000; i++)
        for (j = 0; j < 4000; j++)
            k += i * j;
    printf("sig_int finished\n");
}

​ sleep2的返回打断了sig_int的执行,使其没有执行第2个printf.

问题存在于sleep2的实现中用到了xxxjmp非局部跳转函数,sleep的可靠实现如下:

#include "apue.h"

static void
sig_alrm(int signo)
{       
    /* nothing to do, just returning wakes up sigsuspend() */
}
unsigned int
sleep(unsigned int seconds)
{
    struct sigaction    newact, oldact;
    sigset_t            newmask, oldmask, suspmask;
    unsigned int        unslept;

    /* set our handler, save previous information */
    newact.sa_handler = sig_alrm;
    sigemptyset(&newact.sa_mask);
    newact.sa_flags = 0;
    sigaction(SIGALRM, &newact, &oldact);

    /* block SIGALRM and save current signal mask */
    sigemptyset(&newmask);
    sigaddset(&newmask, SIGALRM);
    sigprocmask(SIG_BLOCK, &newmask, &oldmask);

    alarm(seconds);
    suspmask = oldmask;

    /* make sure SIGALRM isn't blocked */
    sigdelset(&suspmask, SIGALRM);

    /* wait for any signal to be caught */
    sigsuspend(&suspmask);

    /* some signal has been caught, SIGALRM is now blocked */

    unslept = alarm(0);

    /* reset previous action */
    sigaction(SIGALRM, &oldact, NULL);

    /* reset signal mask, which unblocks SIGALRM */
    sigprocmask(SIG_SETMASK, &oldmask, NULL);
    return(unslept);
}

其摒弃了非局部跳转(xxxjmp),所以不存在对其他信号处理程序有任何影响。

但程序没有处理与以前设置的闹钟的交互作用(POSIX.1没有显示定义交互标准)(P269有自定义的处理:取小值)

问题三:系统调用自动重启动

在应用慢速系统调用时,需要对其执行时间做限制,并且在时间到时中断系统调用的执行。

但是有些系统调用时自动重启动的,此时有如下解决方案:

  1. 非局部跳转函数xxxjmp

    #include "apue.h"
    #include <setjmp.h>
    
    static void     sig_alrm(int);
    static jmp_buf  env_alrm;
    
    int
    main(void)
    {
        int     n;
        char    line[MAXLINE];
    
        if (signal(SIGALRM, sig_alrm) == SIG_ERR)
            err_sys("signal(SIGALRM) error");
        if (setjmp(env_alrm) != 0)
            err_quit("read timeout");
    
        alarm(10);
        if ((n = read(STDIN_FILENO, line, MAXLINE)) < 0)
            err_sys("read error");
        alarm(0);
    
        write(STDOUT_FILENO, line, n);
        exit(0);
    }
    
    static void
    sig_alrm(int signo)
    {
        longjmp(env_alrm, 1);
    }

    信号递送后,直接回跳到err_sys("read timeout")

  2. sigaction函数参数feature

    #include <signal.h>
    int sigaction(int sigon,const struct sigaction *restrict act,
    						struct sigaction *restrict oact)
      
      struct sigaction{
        void (*sa_handler)(int);
        sigset_t sa_mask;
        int sa_flags; /*signal options*/
        void (*sa_sigaction)(int, siginfo_t *, void *);
      }
    
    if(sa_flag & SA_INTERRUPT == 1)
       /*则由此信号中断的系统调用不自动重启动*/

问题四:信号环境恢复

在执行了业务逻辑之后,应该恢复原来的信号环境

这里的信号环境包括(可能没有覆盖完全):

  1. 对一个或多个信号的处理方式,执行业务逻辑后要恢复原貌

    如:在业务逻辑中对SIGINT做了捕获,那在业务逻辑之后要恢复其原来的处理方式。

  2. 信号屏蔽字,执行业务逻辑后要将信号屏蔽字恢复到原貌

对于第1点,sigactionsignal更方便和效率。

对于第2点,sigprocmask对信号屏蔽字的管理非常充分

另,在信号场景中有非局部跳转时,应该用

sigsetjmp(sigjmp_buf env, int savemask)siglongjmp(sigjmp_buf env, int val)

以上函数会确保调用前后的信号屏蔽字保持不变。

三、拥抱高级信号接口

sigaction等接口对信号由更强的控制力度。本节总结这些接口功能和特性。

函数sigaction

#include <signal.h>
int sigaction(int sigon,const struct sigaction *restrict act,
						struct sigaction *restrict oact)
  
  struct sigaction{
    void (*sa_handler)(int);
    sigset_t sa_mask;
    int sa_flags; /*signal options*/
    void (*sa_sigaction)(int, siginfo_t *, void *);
  }

功能:检查或修改(或检查并修改)于指定信号相关联的处理动作。

信号集合操作函数

#include <signal.>

int sigemptyset(sigset_t *set);             //初始化信号集set,清除其中的所有信号
int sigfillset(sigset_t *set);              //初始化信号集set,使其包括所有信号
int sigaddset(sigset_t *set, int signo);    //添加信号signo到集合set中
int sigdelset(sigset_t *set, int signo);    //从set中删除信号signo
																					4个函数返回值成功返回0出错返回-1int sigismember(sigset_t *set, int signo);
																					返回值真返回1假返回0/*-------------------------------------------------------------*/
                                            
int sigprocmask(int how, const sigset_t *restrict set, sigset_t *restrict oset);
  																				返回值成功返回0出错返回-1
/*																					
oset非空,则返回当前信号屏蔽字存于oset.
set非空,how = SIG_BLOCK, 屏蔽set指向的信号集合
        how = SIG_UNBLOCK, 解除屏蔽set指向的信号集合
        how = SIG_SETMASK, 设置进程新的信号屏蔽字为set指向的值
 */
/*sigpromask的常规用法是:*/
sigset_t newmask,oldmask;
sigaddset(&newmask, SIGINT);
sigprocmask(SIG_BLOCK, &newmask, &oldmask);  //屏蔽并且保存当前屏蔽字
...
sigprocmask(SIG_SETMASK, &oldmask,NULL);  //恢复原来的屏蔽字


/*----------------------------------------------------------------------------*/
int sigdepending(sigset_t *set); //获取depending状态的信号,并添加到set中
                                       返回值成功返回0出错返回-1

/*通常用法*/
sigset_t pendmask;
if(sigpending(&pendmask) < 0)
  printf("sigpending error\n");
if(sigismember(&pendmask,SIGQUIT))
  printf("SIGQUIT pending\n");


/*-------------------------------------------------------------------------------------*/
int sigsuspend(const sigset_t *sigmask); //恢复信号屏蔽字,并进入休眠,直到收到一个信号
                                      返回值-1并将error设置为EINTR
/*通常用法*/
sigset_t waitmask;
...

 /*Pause, allowing all signals except ones in waitmask*/
if(sigsuspend(&waitmask) != -1)
  printf("sigsuspend error");

函数sigqueue

#include <signal.h>
int sigqueue(pid_t pid, int signo, const union sigval value);
                                    返回值成功返回0出错返回-1

在激活信号排队特性后需要用`sigqueue`来发送信号该单个进程
⚠️ **GitHub.com Fallback** ⚠️