信号

1. 信号概念

信号的定义

信号是软中断,它提供了一种处理异步事件的方法。在Linux系统中,每个信号都有一个名字,这些名字都已SIG开头,通过kill -l命令可以查看所有信号。

注意上图:

  • 信号的编号从1到64,但是没有32,33,所以信号共有62个
  • 前31个信号称为普通信号,后31个称为实时信号
  • 使用信号的时候可以直接使用编号,也可以使用这些宏

本文内容仅限于前31个普通信号,且仅讨论用的相对较多的部分信号。

信号的产生

信号的产生有多种方式:

  • 终端按键产生信号,如Ctrl+C产生SIGINT信号,Ctrl+\产生SIGQUIT信号
  • 硬件异常产生信号,如除0操作、无效的内存引用等
  • 进程调用kill函数或终端执行kill命令产生信号
  • 当检测到某种软件条件已经发生,将其通知给有关进程时也要产生信号,如alarm超时产生SIGALARM信号

信号的处理

信号的处理一般有三种方式:

  • 忽略信号。大多数信号都可采用该方式处理
  • 执行系统默认动作。每一种信号都有系统默认动作,而大部分信号的默认动作都是终止进程
  • 捕捉信号。此种处理方式为执行用户自定义的处理函数

有两个信号比较特殊:SIGKILL和SIGSTOP,它们既不能被忽略,也不能被捕捉。

可靠信号术语

  • 信号递送(Delivery):当一个信号产生时,内核通常在进程表中以某种形式设置一个标志,当对信号采取了该动作时,我们就说向进程递送了一个信号
  • 信号未决(Pending):信号从产生到递送之间的时间间隔内,我们称信号是未决的
  • 信号阻塞(Block):进程可以选择阻塞某个信号,即屏蔽不接收该信号

注意:

  • 如果为进程产生了一个阻塞的信号,且进程对该信号的处理方式为系统默认动作或捕捉,那么该信号将一直保持在未决状态
  • 阻塞和忽略是不同的,阻塞是进程没有收到该信号,而忽略是进程收到信号后的一种处理方式

Linux常用信号及默认动作

信号 产生条件及功能说明 系统默认动作
SIGABRT 调用abort() 进程终止 + coredump
SIGALRM alarm或settimer超时 进程终止
SIGCHLD 子进程终止时,发送该信号给父进程 忽略
SIGFPE 算术运行异常,如除0、浮点溢出等 进程终止 + coredump
SIGINT 命令行终端按Ctrl+C时,发送该信号给所有的前台进程,常用于终止运行失控的进程 进程终止
SIGQUIT 命令行终端按Ctrl+\时,发送该信号给所有的前台进程,作用和SIGINT相同,但会产生cordump文件 进程终止 + coredump
SIGTSTP 命令行终端按Ctrl+Z时,发送该信号给所有的前台进程,用于停止进程 停止进程
SIGIO 指示一个异步IO事件 进程终止/忽略
SIGKILL 不能被捕捉和忽略,只能采取系统默认动作 进程终止
SIGSTOP 不能被捕捉和忽略,只能采取系统默认动作 进程终止
SIGTERM 由kill函数或kill命令产生,发送给应用程序捕获,reboot命令就用到了该信号 进程终止
SIGSEGV 无效内存引用,段错误 进程终止 + coredump
SIGUSR1 用户自定义信号,可用于应用程序 进程终止
SIGUSR2 用户自定义信号,可用于应用程序 进程终止

2. signal函数

signal函数用于给某个信号指定信号处理方式。

#include <signal.h>

typedef void (*sighandler_t)(int);

//成功返回旧的信号处理函数,失败返回SIG_ERR
sighandler_t signal(int signum, sighandler_t handler);

参数说明:

  • signum:信号编号
  • handler:SIG_IGN(忽略信号)、SIG_DFL(默认动作)或信号处理函数(捕捉信号)

3. 信号集与信号屏蔽字

信号集

信号集sigset_t是一种能表示多个信号的数据类型,signal.h提供了下列5个处理信号集的函数。

#include <signal.h>

//4个函数返回值:成功返回0,失败返回-1
int sigemptyset(sigset_t *set);                    //初始化由set指向的信号集,清除其中所有信号
int sigfillset(sigset_t *set);                     //初始化由set指向的信号集,使其包含所有信号
int sigaddset(sigset_t *set, int signum);          //将一个信号signum添加到信号集set中
int sigdelset(sigset_t *set, int signum);          //将一个信号signum从信号集set中删除

//若信号signum在信号集set中,则返回1,否则返回0
int sigismember(const sigset_t *set, int signum);  //判断信号signum是否在信号集set中

应用程序在使用信号集前,必须要先对该信号集调用一次sigemptyset或sigfillset,之后就可以调用sigaddset或sigdelset在该信号集中增删特定的信号。

信号屏蔽字

  • 每个进程都有一个信号屏蔽字,它规定了当前要阻塞而不能递送到该进程的信号集
  • 对于每种信号,该屏蔽字中都有一位与之对应,若信号的对应位已设置,则该信号是被阻塞的
  • sigprocmask函数可以检测或更改,或同时检测和更改进程的信号屏蔽字
#include <signal.h>

//成功返回0,失败返回-1
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

参数说明:

  • 若oldset不为NULL,则进程的当前信号屏蔽字将通过oldset返回
  • 若set不为NULL,则how将指示如何修改当前信号屏蔽字
  • 若set为NULL,则忽略参数how,设为0即可
参数how 说 明
SIG_BLOCK 新的信号屏蔽字是其当前信号屏蔽字和set指向信号集的并集,set包含了希望阻塞的附加信号
SIG_UNBLOCK 新的信号屏蔽字是当前信号屏蔽字和set指向信号集补集的交集,set包含了希望解除阻塞的信号
SIG_SETMASK 新的信号屏蔽字是set指向的信号集

4. sigaction函数

sigaction用于获取某个信号的现有handler,或者为其设置新的handler,或者二者都有,它改善了signal要想获得当前handler,必须先设置新的handler的缺陷。
APUE推荐用sigaction代替signal,但这个函数用起来比signal麻烦的多,目前实际工程中还是以使用signal居多。

#include <signal.h>

struct sigaction
{
    void       (*sa_handler)(int);
    void       (*sa_sigaction)(int, siginfo_t *, void *);
    sigset_t   sa_mask;
    int        sa_flags;
    void      (*sa_restorer)(void);
};

//成功返回0,失败返回-1
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

参数说明:

  • signum:信号编号
  • act:若act不为NULL,则设置新的handler
  • oldact:若oldact不为NULL,则返回旧的handler

当act不为NULL时:

  • act->sa_handler应设为新的信号处理函数
  • act->sa_mask表示一个信号集,用于指定想要阻塞的信号
  • 在调用信号处理函数前,内核会自动将act->sa_mask加入信号屏蔽字;当从信号处理函数返回时,再将信号屏蔽字恢复为原先的值

5. kill和raise函数

kill函数将信号发送给进程或进程组,raise函数用于进程向自身发送信号。

#include <signal.h>

//成功返回0,失败返回-1
int kill(pid_t pid, int signum);
int raise(int signum);  //raise(signum) <==> kill(getpid(), signum)

kill的pid参数有以下4种不同的情况:

  • pid > 0:将信号发送给进程ID为pid的进程
  • pid = 0:将信号发送给调用进程所属的进程组
  • pid < 0:将信号发送给进程组ID等于abs(pid),且调用进程有权限发送的所有进程
  • pid = -1:将信号发送给调用进程有权限发送的所有进程

6. alarm函数

使用alarm可以设置一个定时器(闹钟时间),当定时器超时时,会产生SIGALRM信号,如果应用程序忽略或不捕捉该信号,则会采用系统默认动作——终止调用alarm的进程。

#include <unistd.h>

//返回值:0或者以前设置的定时器剩余秒数
unsigned int alarm(unsigned int seconds);
  • 每个进程只能有一个闹钟时间,闹钟时间只能以秒为单位
  • 如果在调用alarm时,之前设置的闹钟时间还没到,则之前闹钟的剩余时间将作为本次alarm的返回值,并使用新的闹钟时间重新计时
  • 如果在调用alarm时,之前设置的闹钟时间还没到,且本次调用的seconds为0,则取消之前的闹钟时间,其剩余时间作为本次alarm的返回值

7. pause函数

pause函数使调用进程阻塞直到捕捉到一个信号。

#include <unistd.h>

//返回-1,errno设置为EINTR
int pause(void);

只有执行了一个信号处理函数并从其返回时,pause才返回,在这种情况下,pause返回-1,errno设置为EINTR。

8. 信号可重入函数

信号可重入函数是指在信号处理程序中保证调用安全的函数,也叫做异步信号安全函数,下图列出了所有的信号可重入函数,其中所有的系统调用都是信号可重入函数。

没有列入上图的大多数函数是信号不可重入函数,主要原因有:

  • 它们使用静态数据结构
  • 它们调用malloc或free
  • 它们是标准IO函数

9. 信号的应用

应用程序使用信号的步骤其实很简单:

  • 调用signal或sigaction为某个信号指定处理方式
  • 如果是捕捉信号,则编写信号处理函数,在函数中实现需要的处理

应用程序使用信号的步骤其实也很复杂:

  • 信号的数量繁多
  • 每种信号的处理方式都有3种
  • 很多系统函数内部也有对信号的要求,我们在使用信号时可能会在无意间与之冲突,比如下一篇笔记system与信号的深层特性中讲的案例
  • 一些信号产生原因来自硬件问题或系统故障,这对于应用程序是不可预知、不可控的,要不要处理这些信号、如何处理都是比较麻烦的事情
posted @ 2019-08-29 22:18  原野追逐  阅读(478)  评论(0编辑  收藏  举报