异步处理方式之信号(一):基础知识和signal函数说明
阅读原文时间:2023年07月11日阅读:1

文章目录

​ 信号是一种软中断。很多比较重要的应用程序都需要处理信号。信号提供了一种异步处理事件的方法,例如:终端用户输入中断键,会通过信号机制终止一个程序等。早期的信号存在丢失的风险,且执行在临界代码区时无法关闭所选择的信号,后来一些系统便增加了可靠信号机制。下面的章节提供详细的说明。

​ 首先,每一个信号都有一个名字。这些名字都是以"SIG"开头的。Linux支持31种基本信号,不同的操作系统可能支持的信号数量略有不同。信号是在头文件中定义的,且每一种信号都被定义为整形常量(信号编号)。

toney@ubantu:~$ kill -l
 1) SIGHUP     2) SIGINT   3) SIGQUIT  4) SIGILL   5) SIGTRAP
 6) SIGABRT     7) SIGBUS   8) SIGFPE   9) SIGKILL 10) SIGUSR1
11) SIGSEGV    12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT    17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN    22) SIGTTOU 23) SIGURG  24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM    27) SIGPROF 28) SIGWINCH    29) SIGIO   30) SIGPWR
31) SIGSYS    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

​ 不存在编号为0的信号。在后面的章节中会说明编号为0的信号的特殊用途。

​ 产生信号的条件有很多:

  • 当用户按某些终端键时会产生信号。例如使用‘Delete’键会产生SIGINT信号(有些系统中组合键Ctrl+C也会产生相同的效果)。
  • 硬件异常产生信号。例如:除数为0、无法的内存访问(常见的有段错误)等。这些条件通常是由硬件检测到的,并通知内核,之后由内核产生适当的信号并通知该进程。
  • 进程调用kill(2)函数可将任意信号发送给另一个进程或者进程组。对此有一个限制:要么发送信号的进程所有者是超级用户,要么发送进程和接收进程拥有相同的所有者。
  • 用户调用kill(1)命令将信号发送给其他的进程。我们常用此命令终止(个人更喜欢说杀死)一个后台进程。
  • 当检测到某种软件条件发生时,系统也会产生相应的信号通知该进程。例如定时时间到产生SIGALRM信号、管道读进程已经关闭却任然要往管道中写数据时产生SIGPIPE信号。

​ 信号是异步事件的典型示例。产生信号的事件对进程而言是随机出现的。进程不能通过测试一个简单的变量(如errno)来判断是否有信号发生,而是应该告诉内核:“当此信号发生时,应该执行如下操作”。这里一共有三种方式可供选择:

(1)忽略此信号。

(2)捕捉此信号。

(3)执行系统默认操作。

2.1 信号操作之忽略信号

​ 首先来说忽略信号的用法。大多数的信号都可以使用这种方式来处理信号,但是有两种信号是绝不能被忽略的,它们分别是SIGKILLSIGSTOP信号。这有两种信号不能被忽略的原因是:它们向内核和用户提供了使进程终止或者停止的可靠方法。此外,如果忽略某些由硬件产生的信号(例如SIGSEG信号),会导致软件出现无法预料的问题。

2.2 信号操作之捕捉信号

​ 为了实现捕捉信号的目的,我们必须通知内核在某种信号发生时,调用一个用户函数。在用户函数中,我们可以执行我们希望对该信号的处理方式。例如我们可以捕捉SIGALRM信号,当定时时间到时打印某些提示信息等。注意:不能捕捉SIGKILL和SIGSTOP信号

2.3 信号操作之执行系统默认操作

​ 对于大多数信号的系统默认操作都是终止该进程。

2.4 常见的信号

​ 下表中列出了31中信号编号、信号名称,Linux系统的默认操作,并对其中常见或者常用到的信号做了一个简单的说明。如果以后用到再做详细补充说明。

序号

信号名称

说明

默认操作

1

SIGHUP

暂不介绍

terminate

2

SIGINT

当用户按中断键(一般是Ctrl+C或Delete键)时,驱动程序会产生此信号来终止进程。

terminate

3

SIGQUIT

当用户按退出键(一般是Ctrl+)时,中断驱动程序会产生该信号,发送给所有前台进程。

coredump

4

SIGILL

该信号表示已经执行一条非法硬件指令。

coredump

5

SIGTRAP

指示一个实现的硬件故障。

coredump

6

SIGABRT

调用abort()函数来终止进程时会产生该信号。

coredump

7

SIGBUS

指示一个已定义的硬件故障

coredump

8

SIGFPE

表示算数运算异常。例如除0操作,浮点溢出等。

coredump

9

SIGKILL

无法被忽略和捕捉的信号。它向系统提供一种可以杀死任意进程的可靠方法。

terminate

0

SIGUSR1

用户定义的信号,可用于应用程序

terminate

11

SIGSEGV

无效的内存访问。例如经典的“段错误”。

coredump

12

SIGUSR2

用户定义的另一个信号,可用于应用程序

terminate

13

SIGPIPE

管道的读进程已经终止时写管道会产生该信号。

terminate

14

SIGALRM

当使用alarm()函数,或者setitimer()设置的定时时间到时会产生此信号

terminate

15

SIGTERM

是由kill(1)命令发送的系统默认终止信号。该信号可被应用程序捕获,从而进行清理工作,完成优雅的终止(相对于SIGKILL而言,SIGKILL信号不能被捕获或者忽略)。

terminate

16

SIGSTKFLT

暂不介绍

17

SIGCHLD

一个进程终止或者停止时,SIGCHLD信号会发送给其父进程。按系统默认,将忽略此信号。如果父进程需要被告知该子进程退出状态,则需要捕捉此信号。一般在信号处理函数中调用wait()函数回收子进程的资源。

ignore

18

SIGCONT

作业控制信号。它用来发送给需要继续运行,但当前处于停止状态的进程。收到此信号后,挂起的进程继续运行。如果本来已经在运行则忽略该信号。

ignore

19

SIGSTOP

作业控制信号,它停止一个进程。不能被捕捉或忽略

stop

20

SIGTSTP

交互停止信号。当用户按挂起键(一般是Ctrl+z)时,中断驱动程序产生此信号。

stop

21

SIGTTIN

暂不介绍

stop

22

SIGTTOU

暂不介绍

stop

23

SIGURG

暂不介绍

ignore

24

SIGXCPU

暂不介绍

coredump

25

SIGXFSZ

暂不介绍

coredump

26

SIGVTALRM

暂不介绍

terminate

27

SIGPROF

暂不介绍

terminate

28

SIGWINCH

暂不介绍

ignore

29

SIGIO

暂不介绍

terminate

30

SIGPWR

暂不介绍

terminate

31

SIGUNUSED / SIGSYS

一个无效的系统调用

coredump

3.1 signal函数介绍

Unix系统信号机制最简单的接口是signal函数。

#include <signal.h>
void (*signal(int signo, void (*func)(int)))(int);
                        返回值:若成功,返回之前的信号处理配置;若失败,返回SIG_ERR.

代码中:

  • signo是指信号名称(详情参见2.4常见的信号

  • func是常量SIG_IGNSIG_DFL或者接收到信号时自定义的信号处理函数地址

    • 如果为SIG_IGN, 则向内核表示要忽略此信号
    • 如果为SIG_DFL, 则表示接收到此信号时执行系统的默认操作。
    • 当指定的是函数地址时,则在信号发生时,由内核调用该函数。我们称此函数为信号处理函数,或者信号捕捉函数

    说句心里话,signal的函数原型看起来有点看不懂:( 。下面我们也按前辈先人的说法再熟悉下(原话):

本节开头所示的signal函数原型太复杂了,如果使用下面的typedef,则可以使其简单些。

typedef void Sigfunc(int);            (3-1)

然后,可将signal函数原型写成:

Sigfunc *signal(int, Sigfunc *);    (3-2)

这样,signal的函数看起来就简单了很多:signal函数要求两个参数,并返回一个函数指针(如3-2所示),而该函数指针指向的函数有一个整型参数且无返回值(如3-1所示)。

​ 用通俗一点的话描述:定义一个信号处理函数,它有一个整型参数signo, 无返回值;当调用signal函数设置信号处理程序时,signal函数的第二个参数是指向该信号处理函数的指针,signal函数的返回值是指向未修改之前的信号处理函数指针。

​ 在上述的描述中,我们提到了三个宏定义: SIG_IGNSIG_DFLSIG_ERR。这三个宏Linux上的原型如下:

typedef void __sighandler_t(int);

#define SIG_DFL    ((__sighandler_t)0)     /* default signal handling */
#define SIG_IGN    ((__sighandler_t)1)     /* ignore signal */
#define SIG_ERR    ((__sighandler_t)-1)    /* error return from signal */

这三个常量可用于表示“指向函数的指针”。

3.2 signal函数示例

​ 该实例中定义了两个信号处理函数,捕获了三个信号(SIGUSR1, SIGUSR2共用一个信号处理函数)。

/*************************************************************************
             > File Name: signal_demo.c
             > Author: Toney Sun
             > Mail: vip_13031075266@163.com
       > Created Time: 2020年04月27日 星期一 11时50分47秒
 ************************************************************************/

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

static void sig_handler(int); /*自定义的信号处理函数*/
static void sig_usr(int);     /*自定义的信号处理函数*/
int signal_install()
{
     if(signal(SIGINT, sig_handler)==SIG_ERR){
           printf("SIGINT handle function register error\n");
     }
     if(signal(SIGUSR1, sig_usr)==SIG_ERR){
           printf("SIGUSR1 handle function register error\n");
     }
     if(signal(SIGUSR2, sig_usr)==SIG_ERR){
           printf("SIGUSR2 handle function register error\n");
     }
}

void sig_handler(int signo)
{
      if(signo == SIGINT){
            printf("Recieved SIGINT signal\n");
      }else{
            printf("sig_handler receieve Error signal\n");
      }
}
void sig_usr(int signo)
{
      if(signo == SIGUSR1){
            printf("Recieved SIGUSR1 signal\n");
      }else if(signo == SIGUSR2){
            printf("Recieved SIGUSR2 signal\n");
      }else{
            printf("sig_usr receieve Error signal\n");
      }
}
void main(int argc, char *argv[])
{
    //signal_demo();
    //exec_funcs();
    signal_install();
    while(1){
        pause();
    }
}

结果如下:

toney@ubantu:/mnt/hgfs/em嵌入式学习记录/schedule调度器$ ./demo.out 

^CRecieved SIGINT signal

^CRecieved SIGINT signal

^CRecieved SIGINT signal
^Z
[3]+  Stopped                 ./demo.out
toney@ubantu:/mnt/hgfs/em嵌入式学习记录/schedule调度器$
toney@ubantu:/mnt/hgfs/em嵌入式学习记录/schedule调度器$ ./demo.out &
[6] 19518
toney@ubantu:/mnt/hgfs/em嵌入式学习记录/schedule调度器$ kill -USR2 19518
toney@ubantu:/mnt/hgfs/em嵌入式学习记录/schedule调度器$ Recieved SIGUSR2 signal

toney@ubantu:/mnt/hgfs/em嵌入式学习记录/schedule调度器$ kill -USR1 19518
toney@ubantu:/mnt/hgfs/em嵌入式学习记录/schedule调度器$ Recieved SIGUSR1 signal

toney@ubantu:/mnt/hgfs/em嵌入式学习记录/schedule调度器$

3.3 signal函数的限制

  • 如果想使用signal函数来获取当前进程对某一信号的处理方式,会修改当前的处理方式,否则无法确定当前的处理方式。常见的用法如下:

    if(signal(SIGINT, SIG_IGN)!=SIG_IGN)
    signal(SIGINT, sig_handler);
    if(signal(SIGUSR1, SIG_IGN)!=SIG_IGN)
    signal(SIGINT, sig_usr);

后面我们将使用另一种信号处理方式:sigaction()函数,此函数无需修改便可以查询当前的处理方式。

  • 进程创建

    当一个进程调用fork时,其子进程继承了父进程的信号处理方式。因为子进程在创建时复制了父进程的内存映像,所以信号捕捉函数的地址在子进程中是有效的。