欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  科技

信号

程序员文章站 2022-07-04 17:35:49
1. 信号概念 信号的定义 信号是软中断,它提供了一种处理异步事件的方法。在Linux系统中,每个信号都有一个名字,这些名字都已SIG开头,通过kill l命令可以查看所有信号。 注意上图: 信号的编号从1到64,但是没有32,33,所以信号共有62个 前31个信号称为 普通信号 ,后31个称为 实 ......

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与信号的深层特性中讲的案例
  • 一些信号产生原因来自硬件问题或系统故障,这对于应用程序是不可预知、不可控的,要不要处理这些信号、如何处理都是比较麻烦的事情