信号机制:基本概念与使用

发布于 2021-04-02 13:36:23   阅读量 10  点赞 0  

一、信号的本质

 信号是 软件中断,其与硬件中断的相似之处在于打断了程序执行的正常流程。

 信号的系统数据结构是一个唯一的 小整数,由 1 开始顺序展开,<signal.h>SIGxxx形式的符号名对这些整数做了定义。

 信号分为两大类:

  • 标准信号:内核向进程通知事件;

  • 实时信号


二、信号处置

 信号到达后,进程视具体信号执行以下的 默认操作 之一:

  • 忽略信号

  • 终止进程:有时指进程异常终止,而非调用exit()而发生的正常终止;

  • 产生核心转储文件,并终止进程:核心转储文件包含进程虚拟内存的镜像,可将其加载到调试器中以检查进程终止时的状态;

  • 暂停进程:暂停进程的执行;

  • 从暂停状态恢复进程的执行;

 除了根据特定信号采取默认行为外,程序也能将信号的处置设置为:

  • 采用默认行为:适用于将信号处置恢复默认

  • 忽略信号

  • 执行信号处理器程序

我们无法直接将信号处置设置为终止进程或产生核心转储(除非这是对信号的默认处置)。

与之效果近似的是为信号安装处理器程序,并于其中调用exit()abort()abort()为进程产生一个SIGABRT信号,该信号的默认行为是引发进程产生转储核心文件并终止。


三、信号类型与默认行为


四、改变信号处置:signal()/sigaction()

 我们无法改变信号SIGKILLSIGSTOP的处置。

signal()更为简单,但其可移植性不好。建立信号处理器更好的选择是sigaction()

① signal()
#include <signal.h>

void ( *signal(int sig, void (*handler)(int)) ) (int);      // Returns previous signals disposition on success, or SIG_ERR on error.
  • sig:表示希望修改处置的信号编号;

  • handler:信号抵达时所调用函数(信号处理器)的地址。该函数无返回值,接收一个整型参数:

      void handler(int sig){
          /* code for handler */
      }
    

    此外,除了为函数地址,handler还可以为以下值:

    • SIG_DFL:将信号处置重置为默认值

    • SIG_IGN:忽略该信号

  • ret:信号之前的处置。与handler参数一样,返回值是指向带有一个整型参数且无返回值的函数的指针或着SIG_DFLSIG_IGN之一。若调用失败,则返回SIG_ERR

使用signal()无法在不改变信号处置的同时获取当前的信号处置。想要做到这一点必须使用sigaction()


② sigaction()

sigaction()的用法更为复杂,但是也更具灵活性。此外,可移植性也更佳。

#include <signal.h>

int sigaction(int sig, const struct sigaction *act, struct sigaction *oldset);      // Returns 0 on success, or -1 on error
  • sig:想要获取或者修改的信号编号。该参数不能为SIGKILLSIGSTOP

  • act:指向描述新处置的sigaction数据结构,若指向获取现有处置,可将act置为NULL

  • oldact:指向一块sigaction结构缓冲区,获取信号之前的处置,若无意获取此信号,也可将oldact置为NULL

sigaction数据结构的定义为(简略版):

struct sigaction {
    void (*sa_handler)(int);
    sigset_t sa_mask;
    int sa_flags;
    void (*sa_restorer)(void);
}
  • sa_handler:指定信号处理器函数的地址,也可以是SIG_IGNSIG_DFL之一。当sa_handler的值为函数地址时,sa_masksa_flags字段才会生效;

  • sa_mask:当调用sa_handler所指定的信号处理函数时,会阻塞的信号集。在调用信号处理器程序之前,该组信号会被自动添加到进程掩码中,直至信号处理器函数返回,届时将自动从信号掩码中删除这些信号。

  • sa_flags:位掩码,由于控制信号处理过程的各种选项:

    • SA_NOCLDSTOP:若sigSIGCHLD,则当因接受一信号而停止或恢复某一子进程时,将不会产生此信号(即父进程不接受关于子进程终止的通知);

    • SA_NOCLDWAIT:若sigSIGCHLD,则当子进程终止时不会将其转换为僵尸;

    • SA_NODEFER:捕获当前信号时,不会再信号处理器执行的时将该信号自动添加到进程掩码中(可能引发递归调用);

    • SA_ONSTACK:针对此信号调用处理器函数时,使用了由sigaltstack()安装的备选栈;

    • SA_RESETHAND:捕获该信号时,会在调用信号处理器之前将信号处理重置为默认值(SIG_DFL),信号处理器将保持建立状态,直至进一步调用sigaction()将其解除;

    • SA_RESTART:自动重启被信号处理器中断的系统调用;

    • SA_SIGINFO:处理器函数被触发时可以获取该信号的附加信息。

除了sa_mask中指定的信号会在信号处理器调用前被添加到信号掩码外,引发调用的信号也将被自动添加到信号掩码中(适用于signal()建立的信号处理器),从而避免相同信号触发的递归调用。即信号处理器执行时,若同一个信号第二次抵达,则会将这个信号添加到等待信号集中,并于稍后传递。

若在等待信号的阻塞期间改变了对其的处置,则当之后解除阻塞时,将使用新的处置来处理信号。

若将等待中信号的处置置为SIG_IGNSIG_DFL(且信号的默认行为是忽略),则会将信号直接从进程的等待信号集中移除。


五、发送信号:kill()

 进程能够使用系统调用kill()向另一进程发送信号:

#include <signal.h>

int kill(pid_t pid, int sig);       // Returns 0 on success, or -1 on error.
  • sig:欲发送的信号编号。若指定为 0(空信号),则不真正发送信号,仅检查是否有权限向目标进程发送信号;

  • pid 标识一个或多个进程:

    • pid > 0:发送信号给指定进程;

    • pid = 0:发送信号给调用进程同组的所有进程,包括调用进程本身;

    • pid < -1:向组 ID 等于该pid绝对值的进程组内所有的进程发送信号;

    • pid = -1:调用进程有权将信号发往的每个目标进程(除了 init 进程与调用进程本身)。

  • ret:若无进程与pid相匹配,则调用失败,返回 -1 并将errno设为ESRCH;若无权向指定的pid发送信号,则调用失败,返回 -1 并将errno设为EPERM(当pid指定了一系列进程时,只要可以向其中之一发送信号,则调用成功)。

 进程要发送信号给另一进程,需要适当的权限,具体规则如下:

  • 特权级进程可以向任何进程发送信号;

  • 若发送者的实际或有效用户 ID 与接受者的实际用户 ID 或保存设置用户 ID(save set-user-id)相同,则发送者可以发送信号;

  • SIGCONT特殊,无论对用户 ID 的检查结果如何,进程可以向同一会话中的任何其他进程发送这一信号。


六、发送信号的其他方式:raise()/killpg()

raise()用于向自身发送信号,killpg()用于向某个进程组发送信号。

#include "signal.h"

int raise(int sig);     // Returns 0 on success, or nonzero on error.
int killpg(pid_t pgrp, int sig);        // Returns 0 on success, or -1 on error.

对于raise()

  • ret:当出错时,返回非零值,并设置errno。调用可能发生的唯一错误为EINVAL,即sig无效。

 在支持线程的系统中会将raise(sig)实现为pthread_kill(pthread_self(), sig),该实现意味着将信号传递给调用raise()的特定线程;相比之下,kill(getpid(), sig)调用会将信号发送给调用进程下的任一线程。

 调用raise()发送的信号将立即传递(在raise()调用返回之前)。

对于killpg():  相当于kill(-pgrp, sig)调用。


七、显示信号描述

 每个信号都由与之相关的可打印说明,想要获取信号SIGxxx的描述,可以通过:

  1. sys_siglist[SIGxxx]数组访问

  2. strsignal()函数,然后返回一枚指针,指向信号的描述字符串,或是当信号编号无效时指向错误字符串。更推荐使用这种方法,原因有:

    • 会对数组进行边界检查;

    • 本地设置敏感,显示信号描述时会使用本地语言。

#define _BSD_SOURCE
#include <signal.h>

extern const char *const sys_siglist[];

#define _GNU_SOURCE
#include <string.h>

char *strsignal(int sig);       // Returns pointer to signal description string


八、信号集

 多个信号可使用一个名为信号集的数据结构标识,其系统数据类型为sigset_t

 在使用信号集之前,必须进行初始化:

① 初始化空信号集 sigemptyset()
#include <signal.h>

int sigemptyset(sigset_t *set);     // Returns 0 on success, or -1 on error


② 初始化全信号集 sigfillset()

 初始化信号集,使其包含所有信号(包括实时信号)

#include <signal.h>

int sigfillset(sigset_t *set);      // Returns 0 on success, or -1 on error


③ 添加或移除信号 sigaddset()/sigdelset()

 信号集初始化后,可向其中添加或移除单个信号:

#include <signal.h>

int sigaddset(sigset_t *set, int sig);
int sigdelset(sigset_t *set, int sig);      // Both return 0 on success, or -1 on error


④ 测试信号是否存在 sigismember()

sigismember()用来测试信号是否是信号集的成员:

#include <signal.h>

int sigismember(const sigset_t *set, int sig);      // Returns 1 if sig is a member of set, otherwise 0


⑤ 信号集上的集合操作

 GNU C 库还实现了三个非标准函数,以在信号集上进行集合操作:

#define _GUN_SOURCE
#include <signal.h>

int sigandset(sigset_t *set, sigset_t *left, sigset_t *right);
int sigorset(sigset_t *dest, sigset_t *left, sigset_t *right);      // Both return 0 on success, or -1 on error

int sigisemptyset(const sigset_t *set);     // Returns 1 if empty, otherwise 0
  • sigandset():将left集与right集的交集置于dest集;

  • sigorset():将left集与right集的并集置于dest集;

  • sigisemptyset():若set未包含信号,则返回 true。


九、阻塞信号 sigprocmask()

 若想确保某一段代码不为传递来的信号所中断,可将信号添加到进程的 信号掩码 中。

 若某待传递的信号处于进程的阻塞队列中,则将一直保持等待状态,直到稍后解除阻塞(从信号掩码中移除)。且信号阻塞期间传递的多个相同信号将丢失,在解除阻塞后只传递一次。

 信号掩码实际属于 线程属性,在多线程程序中,每个线程都可使用pthread_sigmask()来独立检查和修改其信号掩码。

 向信号掩码中添加一个信号,有如下几种方式:

  • 当调用信号处理器程序时,可将引发调用的信号自动添加到信号掩码中,防止信号处理器的递归调用。是否发生这一情况,要视sigaction()函数安装信号处理器程序时指定的标志而定;

  • sigaction()安装信号处理器程序时,可指定一组额外的信号,当调用该处理器程序时,会阻塞这些信号;

  • 使用sigprocmask(),显式向进程信号掩码添加或移除信号

#include <signal.h>

int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);        // Returns 0 on success, or -1 on error
  • set:指定信号集。若只想获取信号掩码,则可将set置为空,此时将忽略how

  • how:指定了想给信号掩码带来的变化:

    • SIG_BLOCK:将set中的信号添加到信号掩码中;

    • SIG_UNBLOCK:将set中的信号从信号掩码中移除。即使将要接触阻塞的信号不在信号掩码中,也不会返回错误

    • SIG_SETMASK:将set指向的信号集赋给信号掩码

  • oldset:若不为空,则用于返回之前的信号掩码

若解除了某个等待信号的锁定,则在sigpromask()调用返回前会此信号传递给进程。

无法阻塞SIGKILLSIGSTOP信号,尝试的sigprocmask()调用将不会有任何影响,也不会产生错误。


十、获取等待信号集 sigpending()

 若某进程接受了一个正在阻塞的信号,则会将该信号添加到进程的等待信号集中。sigpending()系统调用获取调用进程的等待信号集:

#include <signal.h>

int sigpending(sigset_t *set);      // Returns 0 on success, or -1 on error


十一、等待信号 pause()

pause()系统调用将暂停进程的执行,直至信号处理器函数中断该调用,即等待信号的到来。

#include <unistd.h>

int pause(void);        // Always returns -1 with errno set to EINTR

 处理信号时,pause()遭遇中断,总是返回 -1,并将errno置为EINTR


Last Modified : 2021-04-16 10:32:39