Linux 系统编程 学习:03-进程间通信1:Unix IPC(2)信号

Linux 系统编程 学习:03-进程间通信1:Unix IPC(2)信号

背景

上一讲我们介绍了Unix IPC中的2种管道。

回顾一下上一讲的介绍,IPC的方式通常有:

  • Unix IPC包括:管道(pipe)、命名管道(FIFO)与信号(Signal)
  • System V IPC:消息队列、信号量、共享内存
  • Socket(支持不同主机上的两个进程IPC)

我们在这一讲介绍Unix IPC,中有关信号(Signal)的处理。

信号(Signal)

Signal :进程给操作系统或进程的某种信息,让操作系统或者其他进程做出某种反应。

信号是进程间通信机制中唯一的异步通信机制,(一个进程不必通过任何操作来等待信号,然而,进程也不知道信号到底何时到达。)

进程之间可以互相通过系统调用kill发送软中断信号。内核也可以因为内部事件而给进程发送信号,通知进程发生了某个事件。

信号机制除了基本通知功能外,还可以传递附加信息(调用 sigaction函数)。

进程检查是否收到信号的时机是:一个进程在即将从内核态返回到用户态时;或者,在一个进程要进入或离开一个适当的低调度优先级睡眠状态时。

内核实现信号捕捉的过程:
1.用户态的程序,在执行流程中因为中断、异常或者系统调用进入内核态
2.内核处理完异常准备返回用户模式之前,先处理当前进程中可以递送的信号
3.如果信号的处理动作是自定义的信号处理函数,则回到用户模式执行信号处理函数(而不是返回用户态程序的主执行流程)
4.信号处理函数返回时,执行特殊的系统调用"sigreturn"再次进入内核
5.返回用户态,从上次被打断的地方继续向下执行

Linux下当向一个进程发出信号时,从信号产生到进程接收该信号并执行相应操作的过程称为信号的等待过程。如果某一个信号没有被进程屏蔽,则我们可以在程序中阻塞进程对该信号所相应的操作。

例如一个程序当接收到SIGUSR1信号时会进行一个操作,我们可以利用系统API阻塞(block)程序对该信号的操作,直到我们解除阻止。
再举个现实的例子:就好像一个同学让我帮他带饭,但是我现在有其他事要做,现在我先做我手头上的事,直到我把手上的事都完成才去帮他带饭。整个过程差不多就是这样子。

信号的特点:简单、不能携带大量信息、满足某个特设条件才发送。

利用信号来处理子进程的回收是非常方便和高效的,因为所有的工作你都可以交给内核。

例如:当子进程终止时,会发出一个信号,一旦你注册的信号捕捉器捕捉到了这个信号,那么就可以去回调自己的函数(处理处理子进程的函数),去回收子进程了。

信号的分类

Linux的signal.h中定义了很多信号,使用命令kill -l可以查看系统定义的信号列表。

1) SIGHUP       终端的控制进程结束,通知session内的各个作业,脱离关系 
2) SIGINT       程序终止信号(Ctrl+c)
3) SIGQUIT      和2号信号类似(Ctrl+\),产生core文件
4) SIGILL       执行了非法指令,可执行文件本身出现错误 
5) SIGTRAP      有断点指令或其他trap指令产生,有debugger使用
6) SIGABRT      调用abort函数生成的信号 
7) SIGBUS       非法地址(内存地址对齐出错)
8) SIGFPE       致命的算术错误(浮点数运算,溢出,及除数为0 错误)
9) SIGKILL      用来立即结束程序的运行(不能为阻塞,处理,忽略)
10) SIGUSR1     用户使用 

11) SIGSEGV     访问内存错误
12) SIGUSR2     用户使用
13) SIGPIPE     管道破裂
14) SIGALRM     时钟定时信号
15) SIGTERM     程序结束信号(可被阻塞,处理)
16) SIGSTKFLT   协处理器栈堆错误
17) SIGCHLD     子进程结束,父进程收到这个信号并进行处理,(wait也可以)否则僵尸进程
18) SIGCONT     让一个停止的进程继续执行(不能被阻塞)
19) SIGSTOP     让一个进程停止执行(不能被阻塞,处理,忽略)
20) SIGTSTP     停止进程的运行(可以被处理和忽略)

21) SIGTTIN     当后台作业要从用户终端读数据时, 该作业中的所有进程会收到SIGTTIN信号. 缺省时这些进程会停止执行.
22) SIGTTOU     类似SIGTTIN,但在写终端时收到
23) SIGURG      有紧急数据或者out—of—band 数据到达socket时产生
24) SIGXCPU     超过CPU资源限定,这个限定可改变
25) SIGXFSZ     当进程企图扩大文件以至于超过文件大小资源限制
26) SIGVTALRM   虚拟时钟信号(计算的是该进程占用的CPU时间)
27) SIGPROF     时钟信号(进程用的CPU时间及系统调用时间)
28) SIGWINCH    窗口大小改变时发出
29) SIGIO       文件描述符准备就绪,可以进行读写操作
30) SIGPWR      power failure
31) SIGSYS      非法的系统调用

(没有32与33信号)

34) SIGRTMIN    35) SIGRTMIN+1  36) SIGRTMIN+2  37) SIGRTMIN+3
38) SIGRTMIN+4  39) SIGRTMIN+5  40) SIGRTMIN+6  41) SIGRTMIN+7  42) SIGRTMIN+8
43) SIGRTMIN+9  44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9  56) SIGRTMAX-8  57) SIGRTMAX-7
58) SIGRTMAX-6  59) SIGRTMAX-5  60) SIGRTMAX-4  61) SIGRTMAX-3  62) SIGRTMAX-2
63) SIGRTMAX-1  64) SIGRTMAX

非实时信号(1-31)

非实时信号也叫不可靠信号,有如下特点:
1)不可靠信号(不可靠信号 和 可靠信号 的区别在于前者不支持排队,可能会造成信号丢失,而 可靠信号 不会)
2)信号是有可能丢失的
3)非实时信号的产生都对应一个系统事件、每一个信号都有一个默认的系统动作
4)嵌套执行(但是可能会造成丢失)
5)被阻塞信号是没有优先级
6)在挂起的信号中,信号的执行时乱序的

在以上列出的信号中,

  • 不可捕获、阻塞或忽略的信号有:SIGKILL,SIGSTOP
  • 不能恢复至默认动作的信号有:SIGILL,SIGTRAP
  • 默认会导致进程流产的信号有:SIGABRT,SIGBUS,SIGFPE,SIGILL,SIGIOT,SIGQUIT,SIGSEGV,SIGTRAP,SIGXCPU,SIGXFSZ
  • 默认会导致进程退出的信号有:SIGALRM,SIGHUP,SIGINT,SIGKILL,SIGPIPE,SIGPOLL,SIGPROF,SIGSYS,SIGTERM,SIGUSR1,SIGUSR2,SIGVTALRM
  • 默认会导致进程停止的信号有:SIGSTOP,SIGTSTP,SIGTTIN,SIGTTOU
  • 默认进程忽略的信号有:SIGCHLD,SIGPWR,SIGURG,SIGWINCH

此外,SIGIO在SVR4是退出,在4.3BSD中是忽略;SIGCONT在进程挂起时是继续,否则是忽略,不能被阻塞

实时信号(34-64)

实时信号也叫可靠信号,有如下特点:
1)可靠的信号(不管来几个都会响应)
2)信号是不会丢失的
3)嵌套执行
4)被挂起的信号是有优先级(值越大,优先级越高)
5)被挂起时实时信号信号比非实时信号优先级要高
6)被挂起顺序执行信号的响应

使用信号的注意事项

exec 与 信号
exec函数执行后, 把该进程所有信号设为默认动作。
exec函数执行后, 把原先要捕捉的信号设为默认,其他不变。

子进程 与 信号
1)信号初始化的设置是会被子进程继承的
2)信号的阻塞设置是会被子进程继承的
3)被挂起的信号是不会被子进程继承的

自定义信号最好从 SIGRTMIN 开始,但最好保留前几个。比如SIGRTMIN+10。

信号的使用

步骤:
0)事先对一个信号进行注册,事先对一个信号进行注册,改变信号的响应事件:执行默认动作、忽略(丢弃)、捕捉(调用户处理函数)
1)改变信号的阻塞方式
2)触发某种条件产生信号 或 用户自己发送一个信号;等待信号
3)处理信号:执行默认动作、忽略(丢弃)、捕捉(调用户处理函数)

在默认情况下,当一个事件触发了对应的信号时,会使进程发生对应的动作。我们也可以人为地改变进程对于某个信号的处理,达到控制响应信号的目的。

注册信号处理 函数

在系统中,提供了2个信号的注册处理函数:signal、sigaction;用于改变进程接收到特定信号后的行为。

希望能用相同方式处理一种信号的多次出现,最好用sigaction,信号只出现并处理一次,可以用signal。
由于历史原因,在不同版本内核中可能有不同的行为,使用signal调用会有兼容性问题,所以推荐使用信号注册函数sigaction。

信号处理函数里面应该注意的地方(讨论关于编写安全的信号处理函数):
1)局部变量的相关处理,尽量只执行简单的操作 (不应该在信号处理函数里面操作全局变量)
2)“volatile sig_atomic_t”类型的全局变量的相关操作
3)调用异步信号安全的相关函数(不可重入函数就不是异步信号安全函数)
4)errno 是线程安全,即每个线程有自己的 errno,但不是异步信号安全。如果信号处理函数比较复杂,且调用了可能会改变 errno 值的库函数,必须考虑在信号处理函数开始时保存、结束的时候恢复被中断线程的 errno 值;

异步信号安全函数(async-signal-safe)是指:“在该函数内部即使因为信号而正在被中断,在其他的地方该函数再被调用了也没有任何问题”。如果函数中存在更新静态区域里的数据的情况(例如,malloc),一般情况下都是不全的异步信号函数。但是,即使使用静态数据,如果在这里这个数据时候把信号屏蔽了的话,它就会变成异步信号安全函数了。

signal 函数

#include <signal.h>

typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);

// void (*signal(int signum, void (*hand)(int)))(int); // 如果将第二行和第三行拆分,展开以后就是这个样子

参数解析:
signum :要处理的信号值(不能是 9-SIGKILL,19-SIGSTOP;32、33不存在)
handler:处理方式

  • SIG_IGN :忽略该信号,即无动作
  • SIG_DFL :重置设为默认方式
  • function:当信号产生时,调用函数名为 函数名 的函数,该函数必须是 void function(int signum) 类似的一个带有1个整形参数,无返回值的函数。

返回值:

  • 成功返回之前的handler;
  • 失败返回 SIG_ERR,且设置errno。
#include <stdio.h>
#include <unistd.h>
#include <signal.h>

void signal_hander(int signum)
{
    printf("signum is %d\n", signum);
    sleep(1);
    return;
}

int main(int argc, char *argv[])
{
    signal(SIGINT, signal_hander); // 注册 SIGINT 的信号处理函数。
    while(1)
    {
        sleep(2);
        // printf("sleep loop\n");
    }
    return 0;
}

sigaction

sigaction是比signal更高级的信号注册响应函数,能够提供更多功能;可以搭配 sigqueue 来使用。

#include <signal.h>

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

参数解析(如果把 act、oldact 参数都设为NULL,那么该函数可用于检查信号的有效性。)

  • signum:注册信号
  • act:信号活动结构体,其中包含了对指定信号的处理、信号所传递的信息、信号处理函数执行过程中应屏蔽掉哪些函数等等
/* 需要用到以下结构体 */
struct sigaction {
    // sa_handler、sa_sigaction除了可以是用户自定义的处理函数外,还可以为SIG_DFL(采用缺省的处理方式),也可以为SIG_IGN(忽略信号)。
   void     (*sa_handler)(int); //只有一个参数,即信号值,所以信号不能传递除信号值之外的任何信息
   void     (*sa_sigaction)(int, siginfo_t *, void *);
            // 带参的信号处理函数,如果你想要使能这个信号处理函数,需要设置一下sa_flags为SA_SIGINFO
            // 带有三个参数,是为实时信号而设的(当然同样支持非实时信号),它指定一个3参数信号处理函数。
             - 第一个参数为信号值,第三个参数没有使用(posix没有规范该参数的标准),
             - 第二个参数是指向siginfo_t结构的指针,结构中包含信号携带的数据值
             siginfo_t {
               int      si_signo;     /* Signal number */ //信号编号
               int      si_errno;     /* An errno value */ //如果为非零值则错误代码与之关联 
               int      si_code;      /* Signal code */   // //说明进程如何接收信号以及从何处收到
                         SI_USER:通过 kill 函数收到
                         SI_KERNEL:来自kernel.
                         SI_QUEUE:来自 sigqueue 函数.
                         SI_TIMER:来自 POSIX 规范的 timer.
                         SI_MESGQ (since Linux 2.6.6): POSIX message queue state changed; see mq_notify(3).
                         SI_ASYNCIO: AIO completed.
                         SI_SIGIO:Queued  SIGIO  (only in kernels up to Linux 2.2; from Linux 2.4 onward SIGIO/SIGPOLL fills in si_code as described below).
                         SI_TKILL (since Linux 2.4.19): 来自 tkill 函数 或 tgkill 函数.


               int      si_trapno;    /* Trap number that caused
                                         hardware-generated signal
                                         (unused on most architectures) */
               pid_t    si_pid;       /* Sending process ID *///适用于SIGCHLD,代表被终止进程的PID 
               uid_t    si_uid;       /* Real user ID of sending process *///适用于SIGCHLD,代表被终止进程所拥有进程的UID 
               int      si_status;    /* Exit value or signal *///适用于SIGCHLD,代表被终止进程的状态 
               clock_t  si_utime;     /* User time consumed *///适用于SIGCHLD,代表被终止进程所消耗的用户时间 
               clock_t  si_stime;     /* System time consumed *///适用于SIGCHLD,代表被终止进程所消耗系统的时间
               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) */
               void    *si_call_addr; /* Address of system call instruction
                                         (since Linux 3.5) */
               int      si_syscall;   /* Number of attempted system call
                                         (since Linux 3.5) */
               unsigned int si_arch;  /* Architecture of attempted system call
                                         (since Linux 3.5) */
            }
                


   sigset_t   sa_mask;    //信号阻塞设置,用来设置在处理该信号时暂时将sa_mask 指定的信号集搁置
                                    (在信号处理函数执行的过程当中阻塞掉指定的信号集,指定的信号过来将会被挂起,等函数结束后再执行)

   int        sa_flags;   //信号的操作标识,可以是以下的值(设置为0,代表使用默认属性)
        SA_NOCLDSTOP:使父进程在它的子进程暂停或继续运行时不会收到 SIGCHLD 信号。(此标志只有在设立SIGCHLD的处理程序时才有意义)
        SA_NOCLDWAIT:使父进程在它的子进程退出时不会收到 SIGCHLD 信号,这时子进程如果退出也不会成为僵尸进程。(此标志只有在设立SIGCHLD的处理程序时才有意义 或 设置回 SIG_DFL)
        SA_NODEFER :如果设置了 SA_NODEFER标记, 那么在该信号处理函数运行时,内核将不会阻塞该信号。(一般情况下, 当信号处理函数运行时,内核将阻塞该给定信号)
        SA_ONSTACK:在sigaltstack提供的备用信号堆栈上调用信号处理程序。如果备用堆栈不可用,将使用默认堆栈。(此标志只有在建立信号处理程序时才有意义)
        SA_RESETHAND:当调用信号处理函数时,将信号的处理函数重置为缺省值SIG_DFL(此标志只有在建立信号处理程序时才有意义)
        SA_RESTART:如果信号中断了进程的某个系统调用,则系统自动启动该系统调用
        SA_RESTORER:不适用于程序使用。参考"sigreturn"
        SA_SIGINFO:使用 sa_sigaction 成员而不是 sa_handler 作为信号处理函数。

   void     (*sa_restorer)(void); //被遗弃的设置
};

  • oldact:原本的设置会被保存在这里,如果是NULL则不保存

返回值:

  • 成功返回 0;
  • 失败返回 -1,且设置errno。

我们来看一个 sigaction 与 sigqueue 配合的例程。

#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <stdlib.h>

void mysa_handler(int signum)
{
    printf("sa_handler %d\n", signum);
    return ;
}

void mysa_sigaction(int signo, siginfo_t *info,void *ctx)
{
    //以下两种方式都能获得sigqueue发来的数据
    printf("receive the data from siqueue by info->si_int is %d\n",info->si_int);
    printf("receive the data from siqueue by info->si_value.sival_int is %d\n",info->si_value.sival_int);
}

int main(void)
{
    struct sigaction act;
    // 下面的宏 区分了2种 信号响应方式
#if 1
    // act.sa_handler = SIG_DFL; 默认动作
    act.sa_handler = mysa_handler;
#else
    act.sa_sigaction = mysa_sigaction;
    act.sa_flags = SA_SIGINFO;//信息传递开关
#endif
    sigemptyset(&act.sa_mask);
    if(sigaction(SIGINT,&act,NULL) == -1){
        perror("sigaction error");
        exit(EXIT_FAILURE);
    }
    sleep(2);
    union sigval mysigval;
    mysigval.sival_int = 100;
    if(sigqueue(getpid(),SIGINT,mysigval) == -1){
        perror("sigqueue error");
        exit(EXIT_FAILURE);
    }

    return 0;
}

信号集 与 信号阻塞 、未决

在PCB中有两个非常重要的信号集。一个称之为“阻塞信号集”,另一个称之为“未决信号集”。这两个信号集都是内核使用位图机制来实现的。
操作系统不允许我们直接对其进行位操作。而需自定义另外一个集合,借助信号集操作函数来对PCB中的这两个信号集进行修改。

有关概念:

执⾏信号的处理动作称为信号递达(Delivery),信号从产⽣到递达之间的状态,称为信号未决(Pending)。
进程可以选择阻塞(Block)某个信号。被阻塞的信号产⽣时将保持在未决状态,直到进程解除对此信号的阻塞,才执⾏递达的动作。
注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,⽽忽略是在递达之后可选的⼀种处理动作。

每个进程都有一个用来描述哪些信号递送到进程时将被阻塞的信号集,该信号集中的所有信号在递送到进程后都将被阻塞。

  • block集(阻塞集、屏蔽集):一个进程所要屏蔽的信号,在对应要屏蔽的信号位置1
  • pending集(未决信号集):如果某个信号在进程的阻塞集中,则也在未决集中对应位置1,表示该信号不能被递达,不会被处理
  • handler(信号处理函数集):表示每个信号所对应的信号处理函数,当信号不在未决集中时,将被调用

阻塞信号:对指定的信号进行挂起,直到解除信号的阻塞状态以后,才去响应这个信号。
忽略信号:收到了信号,但忽略对其的响应。(不执行任何动作)

信号集有关的函数

以下是与信号阻塞及未决相关的函数操作:

#include <signal.h>

int sigemptyset(sigset_t *set);                 // 清空声明的信号集
int sigfillset(sigset_t *set);                  // 将所有信号登记进集合里面

int sigaddset(sigset_t *set, int signum);       // 往集合集里面添加signum信号
int sigdelset(sigset_t *set, int signum);       // 往集合里面删除signum信号

int sigismember(const sigset_t *set, int signum); // 测试信号集合里面有无signum信号

int sigprocmask(int  how,  const  sigset_t *set, sigset_t *oldset)); // 设置进程的信号掩码
int sigpending(sigset_t *set));                // 获得当前已递送到进程,却被阻塞的所有信号,在set指向的信号集中返回结果。
int sigsuspend(const sigset_t *mask));         // 用于在接收到某个信号之前, 临时用mask替换进程的信号掩码, 并暂停进程执行,直到收到信号为止。 我们会在竟态中讲到它

信号集使用步骤
A. 声明sigset_t *类型的信号集变量#set
B. 清空声明的信号集(sigemptyset)
C. 向#set 中增删信号,可以使用以下有关函数进行操作

改变信号的阻塞方式

更改进程的信号屏蔽字可以阻塞所选择的信号,或解除对它们的阻塞。使用这种技术可以保护不希望由信号中断的代码临界区。

#include <signal.h>

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

参数解析:
how :阻塞模式

  • SIG_BLOCK:添加信号集合里面的信号进行阻塞(原本的设置上添加设置),mask=mask|set
  • SIG_UNBLOCK:解除信号集合里面的信号的阻塞,mask=mask|~set
  • SIG_SETMASK:直接阻塞信号集合里面的信号,原本的设置直接被覆盖,mask=set

set :设置的信号集合
oldset :此前的设置

  • 填入:oldset,则将原本的设置保存在这里
  • 填入:NULL则不做任何操作

返回值:成功返回0;失败返回-1,设置errno:

  • EFAULT :set或oldset参数指向非法地址。
  • EINVAL :how中指定的值无效。

使用阻塞信号集阻塞信号的例程:

/*
    说明:程序首先将SIGINT信号加入进程阻塞集(屏蔽集)中,一开始并没有发送SIGINT信号,所以进程未决集中没有处于未决态的信号。
    当我们连续按下ctrl+c时,向进程发送SIGINT信号;由于SIGINT信号处于进程的阻塞集中,所以发送的SIGINT信号不能递达,也是就是处于未决状态,所以当我打印未决集合时发现SIGINT所对应的位为1。
    现在我们按下ctrl+\,发送SIGQUIT信号,由于此信号并没被进程阻塞,所以SIGQUIT信号直接递达,执行对应的处理函数,在该处理函数中解除进程对SIGINT信号的阻塞。
    所以之前发送的SIGINT信号递达了,执行对应的处理函数,但由于SIGINT信号是不可靠信号,不支持排队,所以最终只有一个信号递达。
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>

void printsigset(const sigset_t *pset)
{
    int i = 0;
    for (; i < 64; i++) //遍历64个信号,
    {
        //信号从1开始   判断哪些信号在信号未决状态字中
        if (sigismember(pset, i + 1))
            putchar('1');
        else
            putchar('0');
    }
    printf("\n");
}

void catch_signal(int sign)
{
    switch (sign)
    {
    case SIGINT:
        printf("accept SIGINT!\n");
        exit(0);
        break;
    case SIGQUIT:
        printf("accept SIGQUIT!\n");
        //取消信号阻塞
        
        sigset_t uset; //定义信号集
        sigemptyset(&uset); //清空信号集
        sigaddset(&uset,SIGINT); //将SIGINT信号加入到信号集中

        //进行位异或操作,将信号集uset更新到进程控制块PCB结构中,取消阻塞信号SIGINT
        sigprocmask(SIG_UNBLOCK,&uset,NULL);
        break;
    }
}

int main(int arg, char *args[])
{
    //定义未决信号集(pending)
    sigset_t pset;
    //定义阻塞信号集(block)
    sigset_t bset;
    //清空信号集
    sigemptyset(&bset);
    //将信号SIGINT加入到信号集中
    sigaddset(&bset, SIGINT);
    //注册信号
    if (signal(SIGINT, catch_signal) == SIG_ERR)
    {
        perror("signal error");
        return -1;
    }
    if (signal(SIGQUIT, catch_signal) == SIG_ERR)
    {
        perror("signal error");
        return -1;
    }
    //进行位或操作,将信号集bset更新到进程控制块PCB结构中,阻塞信号SIGINT(即使用户按下ctrl+c,信号也不会递达)
    sigprocmask(SIG_BLOCK, &bset, NULL);
    while (1)
    {
        /*
         * 获取当前信号未决信息,即使在sigprocmask()函数中设置了信号阻塞,
         * 但是如果没有信号的到来,信号未决状态字对应位依然是0
         * 只要有信号到来,并且被阻塞了,信号未决状态字对应位才会是1
         * */
        sigpending(&pset);
        //打印信号未决信息
        printsigset(&pset);
        sleep(2);
    }
    return 0;
}

发送一个信号

手动发送一个信号,有下面这些函数:killraisesigqueuealarmualarmabort

kill 函数

描述:向 进程/进程组 发送信号。

#include <sys/types.h>
#include <signal.h>

int kill(pid_t pid, int sig);

参数解析:

  • pid

如果pid为正,则将信号sig发送到由pid指定ID的进程。
如果pid等于0,那么sig将发送到调用进程的进程组中的每个进程。用kill(0,sig)发送自定义信号时,本进程和所有子进程(通过exec等方式启动的)都必须有对应的处理函数,否则所有进程都会退出。
如果pid等于-1,则sig被发送到调用进程有权发送信号的每个进程,进程1(init)除外,要使进程具有发送信号的权限,它必须具有特权(在Linux下:具有CAP_KILL功能),或者发送进程的真实或有效用户ID必须等于目标进程的真实或已保存的设置用户ID。在SIGCONT的情况下,当发送和接收进程属于同一会话时就足够了。
如果pid小于-1,那么sig将发送到进程组中ID为-pid的每个进程。

  • sig :如果sig为0,则不发送信号,但仍会执行错误检查;这可用于检查是否存在进程ID或进程组ID。

**返回值说明: **
成功执行时,返回0。
失败返回-1,errno被设为以下的某个值 :

  • EINVAL:指定的信号码无效(参数 sig 不合法)
  • EPERM;权限不够无法传送信号给指定进程
  • ESRCH:参数 pid 所指定的进程或进程组不存在

raise 函数

描述:对调用的进程/线程自身发送一个信号,相当于 kill(getpid(), sig);pthread_kill(pthread_self(), sig);

#include <signal.h>

int raise(int sig);

alarm 函数

描述:设置信号SIGALRM在经过参数seconds指定的秒数后传送给目前的进程。每个进程都有且只有唯一的一个定时器。无论进程处于何种状态,alarm都计时。

#include <unistd.h>

unsigned int alarm(unsigned int seconds);

返回值:返回值为上次定时调用到发送之间剩余的时间,或者因为没有前一次定时调用而返回0。

注意:

  • 如果指定的参数seconds为0,则不再发送 SIGALRM信号。后一次设定将取消前一次的设定。
  • 在使用时,alarm只设定为发送一次信号,如果要多次发送,就要多次使用alarm调用。
  • SIGALRM信号如果不处理(用户捕获 或者 忽略),会使进程exit
  • 在某些系统中,SIGALRM信号会默认中断系统调用(inturrupt),当然也有的系统,默认情况下是使系统调用被中断后自动重新开始(restart automatically)

ualarm 函数

描述:将使当前进程在指定时间(第一个参数,以us位单位)内产生SIGALRM信号,然后每隔指定时间(第2个参数,以us位单位)重复产生SIGALRM信号,如果执行成功,将返回0。

#include <unistd.h>

useconds_t ualarm(useconds_t usecs, useconds_t interval);

abort 函数

abort 函数:给自己发送异常终止信号 SIGABRT 信号,终止并产生core文件。该函数无返回

#include <stdlib.h>

void abort(void);

注意:如果SIGABRT信号被忽略,或被返回的处理程序捕获,它会在被捕获处理完成以后终止进程。(通过恢复SIGABRT的默认配置,然后再次发送SIGABRT信号来实现这一点。)

段坤我吃定了,谁也留不住他。

sigqueue 函数

描述:作为新的发送信号系统调用,主要是针对实时信号提出的支持信号带有参数,与函数sigaction()配合使用。在发送信号同时,就可以让信号传递一些附加信息。这对程序开发是非常有意义的。

#include <signal.h>

int sigqueue(pid_t pid, int sig, const union sigval value);

union sigval {
        int   sival_int;
        void *sival_ptr;
};

参数解析:
pid : 给定的进程号(PID只有为正数的情况,不比 signal 那么复杂)
sig :信号值
value:(联合体)可以是数值,也可以是指针;不同进程之间虚拟地址空间各自独立,将当前进程地址传递给另一进程没有实际意义。但是信号可以回调,在回调的时候,自己给自己捕捉住,传递地址就有意义了。

原理:

  • 当调用sigqueue时,参数 value 就 拷贝到信号处理函数的第二个参数sa_sigaction中。
  • 这样,sigaction参数中的 act->siginfo_t.si_value与sigqueue中的第三个参数value关联;
  • 所以通过siginfo_t.si_value可以获得sigqueue(pid_t pid, int sig, const union sigval val)第三个参数传递过来的数据。
  • 如:sigaction参数中的 act->siginfo_t.si_value.sival_int或sigaction参数中的 act->siginfo_t.si_value.sival_ptr

对于父子进程接收信号的处理方式

父子进程都会收到信号,按各自的方式处理。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>

void signal_handler_son(int signum)
{
    printf("signal_handler_son%d\n", signum);
}
void signal_handler_father(int signum)
{
    printf("signal_handler_father%d\n", signum);
}

int main(int argc, char *argv[])
{
    pid_t pid = fork();
    if(pid == 0)
    {
        signal(SIGINT, signal_handler_son);
        while(1)
        {
            sleep(2);  printf("Son loop\n");
        }
        exit(123);
    }else if(pid > 0)
    {
        signal(SIGINT, signal_handler_father);
        while(1)
        {
            sleep(2);  printf("Father loop\n");
        }
        wait(NULL);
    }

    return 0;
}

借助SIGCHLD信号回收子进程

子进程结束运行,其父进程会收到SIGCHLD信号。该信号的默认处理动作是忽略。可以捕捉该信号,在捕捉函数中完成子进程状态的回收。
SIGCHLD的产生条件:

  • 子进程终止时
  • 子进程接收到SIGSTOP信号停止时
  • 子进程处在停止态,接受到SIGCONT后唤醒时

下面的程序介绍了如何使用 SIGCHLD 回收子进程

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <signal.h>
 
void sys_err(char *str)
{
    perror(str);
    exit(1);
}
void do_sig_child(int signo)
{
    int status;    pid_t pid;
    while ((pid = waitpid(0, &status, WNOHANG)) > 0) {
        if (WIFEXITED(status))
            printf("child %d exit %d\n", pid, WEXITSTATUS(status));
        else if (WIFSIGNALED(status))
            printf("child %d cancel signal %d\n", pid, WTERMSIG(status));
    }
}
int main(void)
{
    pid_t pid;    int i;
    for (i = 0; i < 10; i++) {
        if ((pid = fork()) == 0)
            break;
        else if (pid < 0)
            sys_err("fork");
    }
    if (pid == 0) {   
        int n = 1;
        while (n--) {
            printf("child ID %d\n", getpid());
            sleep(1);
        }
        return i+1;
    } else if (pid > 0) {
        struct sigaction act;
        act.sa_handler = do_sig_child;
        sigemptyset(&act.sa_mask);
        act.sa_flags = 0;
        sigaction(SIGCHLD, &act, NULL);
       
        while (1) {
            printf("Parent ID %d\n", getpid());
            sleep(1);
        }
    }
    return 0;
}

SIGCHLD信号注意问题:

  • 子进程继承了父进程的信号屏蔽字和信号处理动作,但子进程没有继承未决信号集spending。
  • 注意注册信号捕捉函数的位置。
  • 应该在fork之前,阻塞SIGCHLD信号。注册完捕捉函数后解除阻塞。

信号引起的时序竞态

竞态条件,跟系统负载有很紧密的关系,体现出信号的不可靠性。系统负载越严重,信号不可靠性越强。

不可靠由其实现原理所致。信号是通过软件方式实现(跟内核调度高度依赖,延时性强),每次系统调用结束后,或中断处理处理结束后,需通过扫描PCB中的未决信号集,来判断是否应处理某个信号。当系统负载过重时,会出现时序混乱。

这种意外情况只能在编写程序过程中,提早预见,主动规避,而无法通过gdb程序调试等其他手段弥补。且由于该错误不具规律性,后期捕捉和重现十分困难。

pause()

pause() 等待信号来临之前主动挂起:

#include <unistd.h>

int pause(void);

注意:

  • pause收到的信号不能被屏蔽,如果被屏蔽,那么pause就不能被唤醒。
  • 如果信号的默认处理动作是终止进程,则进程终止,pause函数没有机会返回。
  • 如果信号的默认处理动作是忽略,进程继续处于挂起状态,pause函数不返回。
  • 如果信号的处理动作是捕捉,则【调用完信号处理函数之后,pause返回-1errno设置为EINTR,表示“被信号中断”。】

像这种返回值比较奇怪的 的函数还有 execl一族

时序竞态(竞态条件)

设想如下场景:

欲睡觉,定闹钟10分钟,希望10分钟后闹铃将自己唤醒。
正常:定时,睡觉,10分钟后被闹钟唤醒。
异常:闹钟定好后,被唤走,外出劳动,20分钟后劳动结束。回来继续睡觉计划,但劳动期间闹钟已经响过,不会再将我唤醒。

更改进程的信号屏蔽字可以保护不希望由信号中断的代码临界区。如果希望对一个信号解除阻塞,然后pause等待以前被阻塞的信号发生,则又将如何呢?
假定信号是SIGINT,实现这一点的一种不正确的方法是:

sigset_t    newmask, oldmask;

sigemptyset(&newmask);
sigaddset(&newmask, SIGINT);

/* block SIGINT and save current signal mask */
if(sigprocmask(SIG_BLOCK, &newmask, &oldmask) < 0)
    err_sys("SIG_BLOCK error");

/* critical region of code */
if(sigprocmask(SIG_SETMASK, &oldmask, NULL) < 0)
    err_sys("SIG_SETMASK error");

/* window is open */
pause();    /* wait for signal to occur */

/* continue processing */

如果在信号阻塞时将其发送给进程,那么该信号的传递就被推迟直到对它解除了阻塞。
对应用程序而言,该信号好像发生在解除对SIGINT的阻塞和pause之间。
如果发生了这种情况,或者如果在解除阻塞时刻和pause之间确实发生了信号,那么就产生了问题。
因为我们可能不会再见到该信号,所以从这种意义上而言,在此时间窗口(解除阻塞和pause之间)中发生的信号丢失了,这样就使pause永远阻塞。

为了纠正此问题,需要在一个原子操作中先恢复信号屏蔽字,然后使进程休眠。这种功能是由sigsuspend函数提供的。

如果在等待信号发生时希望去休眠,在对时序要求严格的场合下都应该使用sigsuspend替换pause

#include <signal.h>

int sigsuspend(const sigset_t *mask);

将进程的信号屏蔽字设置为由mask指向的值。在捕捉到一个信号或发生了一个会终止该进程的信号之前,该进程被挂起。如果捕捉到一个信号而且从该信号处理程序返回,则sigsuspend返回,并且将该进程的信号屏蔽字设置为调用sigsuspend之前的值。
注意,sigsuspend 函数没有成功返回值。如果它返回到调用者,则总是返回-1,并将errno设置为EINTR(表示一个被中断的系统调用)。
下面的程序显示了保护临界区,使其不被特定信号中断的正确方法:

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>

static void sig_int(int signo)
{
    printf("in sig_int: %d\n", signo);
}

int main(int argc, char *argv[])
{
    sigset_t     newmask, oldmask, waitmask;

    printf("program start: \n");

    if(signal(SIGINT, sig_int) == SIG_ERR)
        perror("signal(SIGINT) error");
    sigemptyset(&waitmask);
    sigaddset(&waitmask, SIGQUIT);
    sigemptyset(&newmask);
    sigaddset(&newmask, SIGINT);

    /*
    * Block SIGINT and save current signal mask.
    */
    if(sigprocmask(SIG_BLOCK, &newmask, &oldmask) < 0)
        perror("SIG_BLOCK error: ");

    /*
    * Critical region of code.
    */
    printf("in critical region: \n");

    /*
    * Pause, allowing all signals except SIGUSR1.
    */
    if(sigsuspend(&waitmask) != -1)
        perror("sigsuspend error");

    printf("after return from sigsuspend: \n");

    /*
    * Reset signal mask which unblocks SIGINT.
    */
    if(sigprocmask(SIG_SETMASK, &oldmask, NULL) < 0)
        perror("SIG_SETMASK error");

    /*
    * And continue processing...
    */

    exit(0);
}

sigsuspend的另一种应用是等待一个信号处理程序设置一个全局变量。
下面的例程用于捕捉中断信号和退出信号,但是希望仅当捕捉到退出信号时,才唤醒主程序。

#include <stdio.h>
#include <signal.h>
#include <stdlib.h>

volatile sig_atomic_t    quitflag;    /* set nonzero by signal handler */

static void sig_int(int signo)    /* one signal handler for SIGINT and SIGQUIT */
{
    // signal(SIGINT, sig_int);
    // signal(SIGQUIT, sig_int);
    if (signo == SIGINT)
        printf("\ninterrupt\n");
    else if (signo == SIGQUIT)
        quitflag = 1;    /* set flag for main loop */
}

int main(int argc, char *argv[])
{
    sigset_t    newmask, oldmask, zeromask;

    if(signal(SIGINT, sig_int) == SIG_ERR)
        perror("signal(SIGINT) error");
    if(signal(SIGQUIT, sig_int) == SIG_ERR)
        perror("signal(SIGQUIT) error");

    sigemptyset(&zeromask);
    sigemptyset(&newmask);
    sigaddset(&newmask, SIGQUIT);

    /*
    * Block SIGQUIT and save current signal mask.
    */
    if(sigprocmask(SIG_BLOCK, &newmask, &oldmask) < 0)
        perror("SIG_BLOCK error");

    while(!quitflag)
    {
        sigsuspend(&zeromask);
    }

    /*
    * SIGQUIT has been caught and is now blocked; do whatever.
    */
    quitflag = 0;

    /*
    * Reset signal mask which unblocks SIGQUIT.
    */
    if(sigprocmask(SIG_SETMASK, &oldmask, NULL) < 0)
        perror("SIG_SETMASK error");

    exit(0);
}

https://www.cnblogs.com/xiangtingshen/p/10885564.html

附录:可重入函数

一个函数在被调用执行期间(尚未调用结束),由于某种时序又被重复调用,称之为“重入”。
根据函数实现的方法可分为“可重入函数”和“不可重入函数”两种。
可重入函数:指一个可以被多个任务调用的过程,任务在调用时不必担心数据是否会出错

为了增强程序的稳定性,在信号处理函数中应使用可重入函数。

信号处理程序中应当使用可再入(可重入)函数。
因为进程在收到信号后,就将跳转到信号处理函数去接着执行。如果信号处理函数中使用了不可重入函数,那么信号处理函数可能会修改原来进程中不应该被修改的数据,这样进程从信号处理函数中返回接着执行时,可能会出现不可预料的后果。
不可再入函数在信号处理函数中被视为不安全函数。

满足下列条件的函数多数是不可再入的:

  • 使用静态的数据结构,如getlogin(),gmtime(),getgrgid(),getgrnam(),getpwuid()以及getpwnam()等等;
  • 函数实现时,调用了malloc()或者free()函数;(3)实现时使用了标准I/O函数的。

The Open Group视下列函数为可再入的:

_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()。

即使信号处理函数使用的都是"安全函数",同样要注意进入处理函数时,首先要保存errno的值,结束时,再恢复原值。因为,信号处理过程中,errno值随时可能被改变。

另外,longjmp()以及siglongjmp()没有被列为可再入函数,因为不能保证紧接着两个函数的其它调用是安全的。

附录:使用特殊的跳转函数sigsetjmp()和siglongjmp() 实现 try-catch

linux中特殊的跳转函数sigsetjmp()和siglongjmp(),在low-level subroutine中处理中断和错误的时候特别有用。

#include <setjmp.h>

void longjmp(jmp_buf env, int val);
void siglongjmp(sigjmp_buf env, int val);

int setjmp(jmp_buf env);
int sigsetjmp(sigjmp_buf env, int savesigs);

sigsetjmp会将当前的堆栈上下文保存在变量env中,这个变量会在后面的siglongjmp中用到。但是当调用个sigsetjmp的函数返回的时候,env变量将会失效;
如果savesigs非零,阻塞的信号集合也会保存在env变量中,当调用siglongjmp的时候,阻塞的信号集也会被恢复。
如果sigsetjmp本身直接返回,则返回值为0;若sigsetjmp在siglongjmp使用env之后返回,则返回值为非零。

#include <stdio.h>
#include <setjmp.h>
#include <signal.h>

static sigjmp_buf jmpbuf;

void sig_fpe(int signo)
{
    siglongjmp(jmpbuf, 1);
}

int main(int argc, char *argv[])
{
    signal(SIGFPE, sig_fpe);
    if (sigsetjmp(jmpbuf, 1) == 0) // try
    {
        int ret = 10 / 0;

    }else // catch
    {
        printf("catch exception\n");
    }

    return 0;
}
/*
    分析:在第一次调用sigsetjmp的时候,由于之前没有调用siglongjmp,所以sigsetjmp的返回值为0;
    故执行int ret = 10 / 0;的操作这时候产生了一个SIGFPE信号,然后会进入SIGFPE信号的handler中。
    在handler中调用了siglongjmp,恢复了env,这时候会回到保存env之处,继续重新执行if。
    由于在本次sigsetjmp调用之前已经有siglongjmp恢复了env,故返回值为非零。从而最终打印出捕捉到的异常信息。
    这个功能其实相当于cpp中的异常捕捉try...catch块。
*/
posted @ 2019-03-17 22:03  schips  阅读(706)  评论(0编辑  收藏  举报