Linux 信号

目录

本章将讨论信号,虽然信号的基本概念比较简单,但是其所涉及到的细节内容比较多,所以本章篇幅也会相对比较长。事实上,在很多应用程序当中,都会存在处理异步事件这种需求,而信号提供了一种处理异步事件的方法,所以信号机制在Linux 早期版本中就已经提供了支持,随着Linux 内核版本的更新迭代,其对信号机制的支持更加完善。

基本概念

信号是事件发生时对进程的通知机制,也可以把它称为软件中断。信号与硬件中断的相似之处在于能够打断程序当前执行的正常流程,其实是在软件层次上对中断机制的一种模拟。大多数情况下,是无法预测信号达到的准确时间,所以,信号提供了一种处理异步事件的方法。

信号的目的是用来通信的

一个具有合适权限的进程能够向另一个进程发送信号,信号的这一用法可作为一种同步技术,甚至是进程间通信(IPC)的原始形式。信号可以由"谁"发出呢?以下列举的很多情况均可以产生信号:

⚫ 硬件发生异常,即硬件检测到错误条件并通知内核,随即再由内核发送相应的信号给相关进程。硬件检测到异常的例子包括执行一条异常的机器语言指令,诸如,除数为0、数组访问越界导致引用了无法访问的内存区域等,这些异常情况都会被硬件检测到,并通知内核、然后内核为该异常情况发生时正在运行的进程发送适当的信号以通知进程。

⚫ 用于在终端下输入了能够产生信号的特殊字符。譬如在终端上按下CTRL + C 组合按键可以产生中断信号(SIGINT),通过这个方法可以终止在前台运行的进程;按下CTRL + Z 组合按键可以产生暂停信号(SIGCONT),通过这个方法可以暂停当前前台运行的进程。

⚫ 进程调用kill()系统调用可将任意信号发送给另一个进程或进程组。当然对此是有所限制的,接收信号的进程和发送信号的进程的所有者必须相同,亦或者发送信号的进程的所有者是root 超级用户。

⚫ 用户可以通过kill 命令将信号发送给其它进程。kill 命令想必大家都会使用,通常我们会通过kill

命令来"杀死"(终止)一个进程,譬如在终端下执行"kill -9 xxx"来杀死PID 为xxx 的进程。kill

命令其内部的实现原理便是通过kill()系统调用来完成的。

⚫ 发生了软件事件,即当检测到某种软件条件已经发生。这里指的不是硬件产生的条件(如除数为0、引用无法访问的内存区域等),而是软件的触发条件、触发了某种软件条件(进程所设置的定时器已经超时、进程执行的CPU 时间超限、进程的某个子进程退出等等情况)。

进程同样也可以向自身发送信号,然而发送给进程的诸多信号中,大多数都是来自于内核。

以上便是可以产生信号的多种不同的条件,总的来看,信号的目的都是用于通信的,当发生某种情况下,通过信号将情况"告知"相应的进程,从而达到同步、通信的目的。

信号由谁处理、怎么处理

信号通常是发送给对应的进程,当信号到达后,该进程需要做出相应的处理措施,通常进程会视具体信号执行以下操作之一:

⚫ 忽略信号。也就是说,当信号到达进程后,该进程并不会去理会它、直接忽略,就好像是没有出该信号,信号对该进程不会产生任何影响。事实上,大多数信号都可以使用这种方式进行处理,但有两种信号却决不能被忽略,它们是SIGKILL 和SIGSTOP,这两种信号不能被忽略的原因是:它们向内核和超级用户提供了使进程终止或停止的可靠方法。另外,如果忽略某些由硬件异常产生的信号,则进程的运行行为是未定义的。

⚫ 捕获信号。当信号到达进程后,执行预先绑定好的信号处理函数。为了做到这一点,要通知内核在某种信号发生时,执行用户自定义的处理函数,该处理函数中将会对该信号事件作出相应的处理,

Linux 系统提供了signal()系统调用可用于注册信号的处理函数,将会在后面向大家介绍。

⚫ 执行系统默认操作。进程不对该信号事件作出处理,而是交由系统进行处理,每一种信号都会有其对应的系统默认的处理方式,8.3 小节中对此有进行介绍。需要注意的是,对大多数信号来说,系统默认的处理方式就是终止该进程。

信号是异步的

信号是异步事件的经典实例,产生信号的事件对进程而言是随机出现的,进程无法预测该事件产生的准确时间,进程不能够通过简单地测试一个变量或使用系统调用来判断是否产生了一个信号,这就如同硬件中断事件,程序是无法得知中断事件产生的具体时间,只有当产生中断事件时,才会告知程序、然后打断当前程序的正常执行流程、跳转去执行中断服务函数,这就是异步处理方式。

信号本质上是int 类型数字编号

信号本质上是int 类型的数字编号,这就好比硬件中断所对应的中断号。内核针对每个信号,都给其定义了一个唯一的整数编号,从数字1 开始顺序展开。并且每一个信号都有其对应的名字(其实就是一个宏),信号名字与信号编号乃是一一对应关系,但是由于每个信号的实际编号随着系统的不同可能会不一样,所以在程序当中一般都使用信号的符号名(也就是宏定义)。

这些信号在<signum.h>头文件中定义,每个信号都是以SIGxxx 开头,如下所示:

c 复制代码
/* Signals. */
#define SIGHUP 1 /* Hangup (POSIX). */
#define SIGINT 2 /* Interrupt (ANSI). */
#define SIGQUIT 3 /* Quit (POSIX). */
#define SIGILL 4 /* Illegal instruction (ANSI). */
#define SIGTRAP 5 /* Trace trap (POSIX). */
#define SIGABRT 6 /* Abort (ANSI). */
#define SIGIOT 6 /* IOT trap (4.2 BSD). */
#define SIGBUS 7 /* BUS error (4.2 BSD). */
#define SIGFPE 8 /* Floating-point exception (ANSI). */
#define SIGKILL 9 /* Kill, unblockable (POSIX). */
#define SIGUSR1 10 /* User-defined signal 1 (POSIX). */
#define SIGSEGV 11 /* Segmentation violation (ANSI). */
#define SIGUSR2 12 /* User-defined signal 2 (POSIX). */
#define SIGPIPE 13 /* Broken pipe (POSIX). */
#define SIGALRM 14 /* Alarm clock (POSIX). */
#define SIGTERM 15 /* Termination (ANSI). */
#define SIGSTKFLT 16 /* Stack fault. */
#define SIGCLD SIGCHLD /* Same as SIGCHLD (System V). */
#define SIGCHLD 17 /* Child status has changed (POSIX). */
#define SIGCONT 18 /* Continue (POSIX). */
#define SIGSTOP 19 /* Stop, unblockable (POSIX). */
#define SIGTSTP 20 /* Keyboard stop (POSIX). */
#define SIGTTIN 21 /* Background read from tty (POSIX). */
#define SIGTTOU 22 /* Background write to tty (POSIX). */
#define SIGURG 23 /* Urgent condition on socket (4.2 BSD). */
#define SIGXCPU 24 /* CPU limit exceeded (4.2 BSD). */
#define SIGXFSZ 25 /* File size limit exceeded (4.2 BSD). */
#define SIGVTALRM 26 /* Virtual alarm clock (4.2 BSD). */
#define SIGPROF 27 /* Profiling alarm clock (4.2 BSD). */
#define SIGWINCH 28 /* Window size change (4.3 BSD, Sun). */
#define SIGPOLL SIGIO /* Pollable event occurred (System V). */
#define SIGIO 29 /* I/O now possible (4.2 BSD). */
#define SIGPWR 30 /* Power failure restart (System V). */
#define SIGSYS 31 /* Bad system call. */
#define SIGUNUSED 31

不存在编号为0 的信号,从示例代码8.1.1 中也可以看到,信号编号是从1 开始的,事实上kill()函数对信号编号0 有着特殊的应用,关于这个文件将会在后面的内容向大家介绍。

信号的分类

Linux 系统下可对信号从两个不同的角度进行分类,从可靠性方面将信号分为可靠信号与不可靠信号;而从实时性方面将信号分为实时信号与非实时信号,本小节将对这些信号的分类进行简单地介绍。

可靠信号与不可靠信号

Linux 信号机制基本上是从UNIX 系统中继承过来的,早期UNIX 系统中的信号机制比较简单和原始,后来在实践中暴露出一些问题,它的主要问题是:

⚫ 进程每次处理信号后,就将对信号的响应设置为系统默认操作。在某些情况下,将导致对信号的错误处理;因此,用户如果不希望这样的操作,那么就要在信号处理函数结尾再一次调用signal(),重新为该信号绑定相应的处理函数。

⚫ 因此导致,早期UNIX 下的不可靠信号主要指的是进程可能对信号做出错误的反应以及信号可能丢失(处理信号时又来了新的信号,则导致信号丢失)。

Linux 支持不可靠信号,但是对不可靠信号机制做了改进:在调用完信号处理函数后,不必重新调用

signal()。因此,Linux 下的不可靠信号问题主要指的是信号可能丢失。在Linux 系统下,信号值小于SIGRTMIN

(34)的信号都是不可靠信号,这就是"不可靠信号"的来源,所以示例代码8.1.1 中所列举的信号都是不可靠信号。

随着时间的发展,实践证明,有必要对信号的原始机制加以改进和扩充,所以,后来出现的各种UNIX

版本分别在这方面进行了研究,力图实现"可靠信号"。由于原来定义的信号已有许多应用,不好再做改动,最终只好又新增加了一些信号(SIGRTMIN~SIGRTMAX),并在一开始就把它们定义为可靠信号,在Linux 系统下使用"kill -l"命令可查看到所有信号,如下所示:

Tips:括号" ) "前面的数字对应该信号的编号,编号1~31 所对应的是不可靠信号,编号34~64 对应的是可靠信号,从图中可知,可靠信号并没有一个具体对应的名字,而是使用了SIGRTMIN+N 或SIGRTMAX-N 的方式来表示。

可靠信号支持排队,不会丢失,同时,信号的发送和绑定也出现了新版本,信号发送函数sigqueue()及信号绑定函数sigaction()。

早期UNIX 系统只定义了31 种信号,而Linux 3.x 支持64 种信号,编号1-64(SIGRTMIN=34,

SIGRTMAX=64),将来可能进一步增加,这需要得到内核的支持。前31 种信号已经有了预定义值,每个信号有了确定的用途、含义以及对应的名字,并且每种信号都有各自的系统默认操作。如按键盘的CTRL+C

时,会产生SIGINT 信号,对该信号的系统默认操作就是终止进程,后32 个信号表示可靠信号。

实时信号与非实时信号

实时信号与非实时信号其实是从时间关系上进行的分类,与可靠信号与不可靠信号是相互对应的,非实时信号都不支持排队,都是不可靠信号;实时信号都支持排队,都是可靠信号。实时信号保证了发送的多个信号都能被接收,实时信号是POSIX 标准的一部分,可用于应用进程。

一般我们也把非实时信号(不可靠信号)称为标准信号,如果文档中用到了这个词,那么大家要知道,这里指的就是非实时信号(不可靠信号)。关于更多实时信号相关内容将会在8.10 小节中介绍。

常见信号与默认行为

前面说到,Linux 下对标准信号(不可靠信号、非实时信号)的编号为1~31,如示例代码8.1.1 所示,接下来将介绍这些信号以及这些信号所对应的系统默认操作。

⚫ SIGINT

当用户在终端按下中断字符(通常是CTRL + C)时,内核将发送SIGINT 信号给前台进程组中的每一个进程。该信号的系统默认操作是终止进程的运行。所以通常我们都会使用CTRL + C 来终止一个占用前台的进程,原因在于大部分的进程会将该信号交给系统去处理,从而执行该信号的系统默认操作。

⚫ SIGQUIT

当用户在终端按下退出字符(通常是CTRL + \)时,内核将发送SIGQUIT 信号给前台进程组中的每一个进程。该信号的系统默认操作是终止进程的运行、并生成可用于调试的核心转储文件。进程如果陷入无限循环、或不再响应时,使用SIGQUIT 信号就很合适。所以对于一个前台进程,既可以在终端按下中断字符

CTRL + C、也可以按下退出字符CTRL + \来终止,当然前提条件是,此进程会将SIGINT 信号或SIGQUIT

信号交给系统处理(也就是没有将信号忽略或捕获),进入执行该信号所对应的系统默认操作。

⚫ SIGILL

如果进程试图执行非法(即格式不正确)的机器语言指令,系统将向进程发送该信号。该信号的系统默认操作是终止进程的运行。

⚫ SIGABRT

当进程调用abort()系统调用时(进程异常终止),系统会向该进程发送SIGABRT 信号。该信号的系统默认操作是终止进程、并生成核心转储文件。

⚫ SIGBUS

产生该信号(总线错误,bus error)表示发生了某种内存访问错误。该信号的系统默认操作是终止进程。

⚫ SIGFPE

该信号因特定类型的算术错误而产生,譬如除以0。该信号的系统默认操作是终止进程。

⚫ SIGKILL

此信号为"必杀(sure kill)"信号,用于杀死进程的终极办法,此信号无法被进程阻塞、忽略或者捕获,故而"一击必杀",总能终止进程。使用SIGINT 信号和SIGQUIT 信号虽然能终止进程,但是前提条件是该进程并没有忽略或捕获这些信号,如果使用SIGINT 或SIGQUIT 无法终止进程,那就使用"必杀信号"SIGKILL 吧。Linux 下有一个kill 命令,kill 命令可用于向进程发送信号,我们会使用"kill -9 xxx"命令来终止一个进程(xxx 表示进程的pid),这里的-9 其实指的就是发送编号为9 的信号,也就是SIGKILL 信号。

⚫ SIGUSR1

该信号和SIGUSR2 信号供程序员自定义使用,内核绝不会为进程产生这些信号,在我们的程序中,可以使用这些信号来互通通知事件的发生,或是进程彼此同步操作。该信号的系统默认操作是终止进程。

⚫ SIGSEGV

这一信号非常常见,当应用程序对内存的引用无效时,操作系统就会向该应用程序发送该信号。引起对内存无效引用的原因很多,C 语言中引发这些事件往往是解引用的指针里包含了错误地址(譬如,未初始化的指针),或者传递了一个无效参数供函数调用等。该信号的系统默认操作是终止进程。

⚫ SIGUSR2

与SIGUSR1 信号相同。

⚫ SIGPIPE

涉及到管道和socket,当进程向已经关闭的管道、FIFO 或套接字写入信息时,那么系统将发送该信号给进程。该信号的系统默认操作是终止进程。

⚫ SIGALRM

与系统调用alarm()或setitimer()有关,应用程序中可以调用alarm()或setitimer()函数来设置一个定时器,当定时器定时时间到,那么内核将会发送SIGALRM 信号给该应用程序,关于alarm()或setitimer()函数的使用,后面将会进行讲解。该信号的系统默认操作是终止进程。

⚫ SIGTERM

这是用于终止进程的标准信号,也是kill 命令所发送的默认信号(kill xxx,xxx 表示进程pid),有时我们会直接使用"kill -9 xxx"显式向进程发送SIGKILL 信号来终止进程,然而这一做法通常是错误的,精心设计的应用程序应该会捕获SIGTERM 信号、并为其绑定一个处理函数,当该进程收到SIGTERM 信号时,会在处理函数中清除临时文件以及释放其它资源,再而退出程序。如果直接使用SIGKILL 信号终止进程,从而跳过了SIGTERM 信号的处理函数,通常SIGKILL 终止进程是不友好的方式、是暴力的方式,这种方式应该作为最后手段,应首先尝试使用SIGTERM,实在不行再使用最后手段SIGKILL。

⚫ SIGCHLD

当父进程的某一个子进程终止时,内核会向父进程发送该信号。当父进程的某一个子进程因收到信号而停止或恢复时,内核也可能向父进程发送该信号。注意这里说的停止并不是终止,你可以理解为暂停。该信号的系统默认操作是忽略此信号,如果父进程希望被告知其子进程的这种状态改变,则应捕获此信号。

⚫ SIGCLD

与SIGCHLD 信号同义。

⚫ SIGCONT

将该信号发送给已停止的进程,进程将会恢复运行。当进程接收到此信号时并不处于停止状态,系统默认操作是忽略该信号,但如果进程处于停止状态,则系统默认操作是使该进程继续运行。

⚫ SIGSTOP

这是一个"必停"信号,用于停止进程(注意停止不是终止,停止只是暂停运行、进程并没有终止),应用程序无法将该信号忽略或者捕获,故而总能停止进程。

⚫ SIGTSTP

这也是一个停止信号,当用户在终端按下停止字符(通常是CTRL + Z),那么系统会将SIGTSTP 信号发送给前台进程组中的每一个进程,使其停止运行。

⚫ SIGXCPU

当进程的CPU 时间超出对应的资源限制时,内核将发送此信号给该进程。

⚫ SIGVTALRM

应用程序调用setitimer()函数设置一个虚拟定时器,当定时器定时时间到时,内核将会发送该信号给进程。

⚫ SIGWINCH

在窗口环境中,当终端窗口尺寸发生变化时(譬如用户手动调整了大小,应用程序调用ioctl()设置了大小等),系统会向前台进程组中的每一个进程发送该信号。

⚫ SIGPOLL/SIGIO

这两个信号同义。这两个信号将会在高级IO 章节内容中使用到,用于提示一个异步IO 事件的发生,譬如应用程序打开的文件描述符发生了I/O 事件时,内核会向应用程序发送SIGIO 信号。

⚫ SIGSYS

如果进程发起的系统调用有误,那么内核将发送该信号给对应的进程。

以上就是关于这些信号的简单介绍内容,以上所介绍的这些信号并不包括Linux 下所有的信号,仅给大家介绍了一下常见信号,表8.3.1 将对这些信号进行总结。

Tips:上表中,term 表示终止进程;core 表示生成核心转储文件,核心转储文件可用于调试,这个便不再给介绍了;ignore 表示忽略信号;cont 表示继续运行进程;stop 表示停止进程(注意停止不等于终止,而是暂停)。

进程对信号的处理

当进程接收到内核或用户发送过来的信号之后,根据具体信号可以采取不同的处理方式:忽略信号、捕获信号或者执行系统默认操作。Linux 系统提供了系统调用signal()和sigaction()两个函数用于设置信号的处理方式,本小节将向大家介绍这两个系统调用的使用方法。

signal()函数

本节描述系统调用signal(),signal()函数是Linux 系统下设置信号处理方式最简单的接口,可将信号的处理方式设置为捕获信号、忽略信号以及系统默认操作,此函数原型如下所示:

c 复制代码
#include <signal.h>
typedef void (*sig_t)(int);
sig_t signal(int signum, sig_t handler);

使用该函数需要包含头文件<signal.h>。

函数参数和返回值含义如下:

signum:此参数指定需要进行设置的信号,可使用信号名(宏)或信号的数字编号,建议使用信号名。

handler:sig_t 类型的函数指针,指向信号对应的信号处理函数,当进程接收到信号后会自动执行该处理函数;参数handler 既可以设置为用户自定义的函数,也就是捕获信号时需要执行的处理函数,也可以设置为SIG_IGN 或SIG_DFL,SIG_IGN 表示此进程需要忽略该信号,SIG_DFL 则表示设置为系统默认操作。

sig_t 函数指针的int 类型参数指的是,当前触发该函数的信号,可将多个信号绑定到同一个信号处理函数上,此时就可通过此参数来判断当前触发的是哪个信号。

Tips:SIG_IGN、SIG_DFL 分别取值如下:

/* Fake signal functions. /
#define SIG_ERR ((sig_t) -1) /
Error return. /
#define SIG_DFL ((sig_t) 0) /
Default action. /
#define SIG_IGN ((sig_t) 1) /
Ignore signal. */

返回值:此函数的返回值也是一个sig_t 类型的函数指针,成功情况下的返回值则是指向在此之前的信号处理函数;如果出错则返回SIG_ERR,并会设置errno。

由此可知,signal()函数可以根据第二个参数handler 的不同设置情况,可对信号进行不同的处理。

测试

signal()函数的用法其实非常简单,为信号设置相应的处理方式,接下来编写一个简单地示例代码对

signal()函数进行测试。

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
static void sig_handler(int sig)
{
    printf("Received signal: %d\n", sig);
}
int main(int argc, char *argv[])
{
    sig_t ret = NULL;
    ret = signal(SIGINT, (sig_t)sig_handler);
    if (SIG_ERR == ret)
    {
        perror("signal error");
        exit(-1);
    }
    /* 死循环*/
    for (;;)
    {
    }
    exit(0);
}

在上述示例代码中,我们通过signal()函数将SIGINT(2)信号绑定到了一个用户自定的处理函数上

sig_handler(int sig),当进程收到SIGINT 信号后会执行该函数然后运行printf 打印语句。当运行程序之后,程序会卡在for 死循环处,此时在终端按下中断符CTRL + C,系统便会给前台进程组中的每一个进程发送

SIGINT 信号,我们测试程序便会收到该信号。

运行测试:

当运行程序之后,程序会占用终端称为一个前台进程,此时按下中断符便会打印出信息(^C 表示按下了中断符)。平时大家使用CTRL + C 可以终止一个进程,而这里却不能通过这种方式来终止这个测试程序,原因在于测试程序中捕获了该信号,而对应的处理方式仅仅只是打印一条语句、而并不终止进程。

那此时该怎么关闭这个测试程序呢?前面给大家介绍了"一击必杀"信号SIGKILL(编号为9),可向该进程发送SIGKILL 暴力终止该进程,当然一般不推荐大家这样使用,如果实在没办法才采取这种措施。新打开一个终端,使用ps 命令找到该进程的pid 号,再使用kill 命令,如下所示:

此时测试程序就会强制终止:

Tips:普通用户只能杀死该用户自己的进程,无权限杀死其它用户的进程。

我们再执行一次测试程序,这里将测试程序放在后台运行,然后再按下中断符:

按下中断符发现进程并没有收到SIGINT 信号,原因很简单,因为进程并不是前台进程、而是一个后台进程,按下中断符时系统并不会给后台进程发送SIGINT 信号。可以使用kill 命令手动发送信号给我们的进程:

两种不同状态下信号的处理方式

通过上面的介绍,以及我们的测试实验,不知大家是否出现了一个疑问?如果程序中没有调用signal()

函数为信号设置相应的处理方式,亦或者程序刚启动起来并未运行到signal()处,那么这时进程接收到一个信号后是如何处理的呢?带着这个问题来聊一聊。

⚫ 程序启动

当一个应用程序刚启动的时候(或者程序中没有调用signal()函数),通常情况下,进程对所有信号的处理方式都设置为系统默认操作。所以如果在我们的程序当中,没有调用signal()为信号设置处理方式,则默认的处理方式便是系统默认操作。

所以为什么大家平时都可以使用CTRL + C 中断符来终止一个进程,因为大部分情况下,应用程序中并不会为SIGINT 信号设置处理方式,所以该信号的处理方式便是系统默认操作,当接收到信号之后便执行系统默认操作,而SIGINT 信号的系统默认操作便是终止进程。

⚫ 进程创建

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

sigaction()函数

除了signal()之外,sigaction()系统调用是设置信号处理方式的另一选择,事实上,推荐大家使用sigaction()

函数。虽然signal()函数简单好用,而sigaction()更为复杂,但作为回报,sigaction()也更具灵活性以及移植性。

sigaction()允许单独获取信号的处理函数而不是设置,并且还可以设置各种属性对调用信号处理函数时的行为施以更加精准的控制,其函数原型如下所示:

c 复制代码
#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

使用该函数需要包含头文件<signal.h>。

函数参数和返回值含义如下:

signum:需要设置的信号,除了SIGKILL 信号和SIGSTOP 信号之外的任何信号。

act:act 参数是一个struct sigaction 类型指针,指向一个struct sigaction 数据结构,该数据结构描述了信号的处理方式,稍后介绍该数据结构;如果参数act 不为NULL,则表示需要为信号设置新的处理方式;如果参数act 为NULL,则表示无需改变信号当前的处理方式。

oldact:oldact 参数也是一个struct sigaction 类型指针,指向一个struct sigaction 数据结构。如果参数

oldact 不为NULL,则会将信号之前的处理方式等信息通过参数oldact 返回出来;如果无意获取此类信息,那么可将该参数设置为NULL。

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

struct sigaction 结构体

c 复制代码
struct sigaction
{
    void (*sa_handler)(int);
    void (*sa_sigaction)(int, siginfo_t *, void *);
    sigset_t sa_mask;
    int sa_flags;
    void (*sa_restorer)(void);
};

结构体成员介绍:

⚫ sa_handler:指定信号处理函数,与signal()函数的handler 参数相同。

⚫ sa_sigaction:也用于指定信号处理函数,这是一个替代的信号处理函数,他提供了更多的参数,可以通过该函数获取到更多信息,这些信号通过siginfo_t 参数获取,稍后介绍该数据结构;sa_handler 和

sa_sigaction 是互斥的,不能同时设置,对于标准信号来说,使用sa_handler 就可以了,可通过

SA_SIGINFO 标志进行选择。

⚫ sa_mask:参数sa_mask 定义了一组信号,当进程在执行由sa_handler 所定义的信号处理函数之前,会先将这组信号添加到进程的信号掩码字段中,当进程执行完处理函数之后再恢复信号掩码,将这组信号从信号掩码字段中删除。当进程在执行信号处理函数期间,可能又收到了同样的信号或其它信号,从而打断当前信号处理函数的执行,这就好点像中断嵌套;通常我们在执行信号处理函数期间不希望被另一个信号所打断,那么怎么做呢?那么就是通过信号掩码来实现,如果进程接收到了信号掩码中的这些信号,那么这个信号将会被阻塞暂时不能得到处理,直到这些信号从进程的信号掩码中移除。在信号处理函数调用时,进程会自动将当前处理的信号添加到信号掩码字段中,这样保证了在处理一个给定的信号时,如果此信号再次发生,那么它将会被阻塞。如果用户还需要在阻塞其它的信号,则可以通过设置参

数sa_mask 来完成(此参数是sigset_t 类型变量,关于该类型的介绍信息请看8.6.1 小节内容,关于信号掩码还会在8.7.1 小节中进一步介绍),信号掩码可以避免一些信号之间的竞争状态(也称为竞态)。

⚫ sa_restorer:该成员已过时,不要再使用了。

⚫ sa_flags:参数sa_flags 指定了一组标志,这些标志用于控制信号的处理过程,可设置为如下这些标志(多个标志使用位或" | "组合):

SA_NOCLDSTOP

如果signum 为SIGCHLD,则子进程停止时(即当它们接收到SIGSTOP、SIGTSTP、SIGTTIN 或SIGTTOU

中的一种时)或恢复(即它们接收到SIGCONT)时不会收到SIGCHLD 信号。

SA_NOCLDWAIT

如果signum 是SIGCHLD,则在子进程终止时不要将其转变为僵尸进程。

SA_NODEFER

不要阻塞从某个信号自身的信号处理函数中接收此信号。也就是说当进程此时正在执行某个信号的处理函数,默认情况下,进程会自动将该信号添加到进程的信号掩码字段中,从而在执行信号处理函数期间阻塞该信号,默认情况下,我们期望进程在处理一个信号时阻塞同种信号,否则引起一些竞态条件;如果设置了SA_NODEFER 标志,则表示不对它进行阻塞。

SA_RESETHAND

执行完信号处理函数之后,将信号的处理方式设置为系统默认操作。

SA_RESTART

被信号中断的系统调用,在信号处理完成之后将自动重新发起。

SA_SIGINFO

如果设置了该标志,则表示使用sa_sigaction 作为信号处理函数、而不是sa_handler,关于sa_sigaction

信号处理函数的参数信息。

以上就是关于struct sigaction 结构体相关的内容介绍了,接下编写程序进行实战测试。

siginfo_t 结构体

c 复制代码
siginfo_t
{
    int si_signo;         /* Signal number */
    int si_errno;         /* An errno value */
    int si_code;          /* Signal code */
    int si_trapno;        /* Trap number that caused hardware-generated signal(unused on most architectures) */
    pid_t si_pid;         /* Sending process ID */
    uid_t si_uid;         /* Real user ID of sending process */
    int si_status;        /* Exit value or signal */
    clock_t si_utime;     /* User time consumed */
    clock_t si_stime;     /* System time consumed */
    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) */
}

这个结构体就不给大家介绍了,使用man 手册查看sigaction()函数帮助信息时,在下面会有介绍。

测试

这里使用sigaction()函数实现与示例代码8.4.1 相同的功能。

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
static void sig_handler(int sig)
{
    printf("Received signal: %d\n", sig);
}
int main(int argc, char *argv[])
{
    struct sigaction sig = {0};
    int ret;
    sig.sa_handler = sig_handler;
    sig.sa_flags = 0;
    ret = sigaction(SIGINT, &sig, NULL);
    if (-1 == ret)
    {
        perror("sigaction error");
        exit(-1);
    }
    /* 死循环*/
    for (;;)
    {
    }
    exit(0);
}

运行测试:

关于信号处理函数说明

一般而言,将信号处理函数设计越简单越好,这就好比中断处理函数,越快越好,不要在处理函数中做大量消耗CPU 时间的事情,这一个重要的原因在于,设计的越简单这将降低引发信号竞争条件的风险。

向进程发送信号

与kill 命令相类似,Linux 系统提供了kill()系统调用,一个进程可通过kill()向另一个进程发送信号;除了kill()系统调用之外,Linux 系统还提供了系统调用killpg()以及库函数raise(),也可用于实现发送信号的功能,本小节将向大家进行介绍。

kill()函数

kill()系统调用可将信号发送给指定的进程或进程组中的每一个进程,其函数原型如下所示:

c 复制代码
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);

使用该函数需要包含头文件<sys/types.h>和<signal.h>。

函数参数和返回值含义如下:

pid:参数pid 为正数的情况下,用于指定接收此信号的进程pid;除此之外,参数pid 也可设置为0 或

-1 以及小于-1 等不同值,稍后给说明。

sig:参数sig 指定需要发送的信号,也可设置为0,如果参数sig 设置为0 则表示不发送信号,但任执行错误检查,这通常可用于检查参数pid 指定的进程是否存在。

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

参数pid 不同取值含义:

⚫ 如果pid 为正,则信号sig 将发送到pid 指定的进程。

⚫ 如果pid 等于0,则将sig 发送到当前进程的进程组中的每个进程。

⚫ 如果pid 等于-1,则将sig 发送到当前进程有权发送信号的每个进程,但进程1(init)除外。

⚫ 如果pid 小于-1,则将sig 发送到ID 为-pid 的进程组中的每个进程。

进程中将信号发送给另一个进程是需要权限的,并不是可以随便给任何一个进程发送信号,超级用户

root 进程可以将信号发送给任何进程,但对于非超级用户(普通用户)进程来说,其基本规则是发送者进程的实际用户ID 或有效用户ID 必须等于接收者进程的实际用户ID 或有效用户ID。

从上面介绍可知,当sig 为0 时,仍可进行正常执行的错误检查,但不会发送信号,这通常可用于确定一个特定的进程是否存在,如果向一个不存在的进程发送信号,kill()将会返回-1,errno 将被设置为ESRCH,表示进程不存在。

测试

(1)使用kill()函数向一个指定的进程发送信号。

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <signal.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
    int pid;
    /* 判断传参个数*/
    if (2 > argc)
        exit(-1);
    /* 将传入的字符串转为整形数字*/
    pid = atoi(argv[1]);
    printf("pid: %d\n", pid);
    /* 向pid 指定的进程发送信号*/
    if (-1 == kill(pid, SIGINT))
    {
        perror("kill error");
        exit(-1);
    }
    exit(0);
}

以上代码通过kill()函数向指定进程发送SIGINT 信号,可通过外部传参将接收信号的进程pid 传入到程序中,再执行该测试代码之前,需要运行先一个用于接收此信号的进程,接收信号的进程直接使用示例代码8.4.4 程序。

运行测试:

testApp1 是示例代码8.4.4 对应的程序,testApp 则是示例代码8.5.1 对应的程序,首先执行"./testApp1

&"将接收信号的程序置于后台运行(其进程pid 为21825),接着执行"./testApp 21825"向接收信号的进程发送SIGINT 信号。

raise()

有时进程需要向自身发送信号,raise()函数可用于实现这一要求,raise()函数原型如下所示(此函数为C

库函数):

c 复制代码
#include <signal.h>
int raise(int sig);

使用该函数需要包含头文件<signal.h>。

函数参数和返回值含义如下:

sig:需要发送的信号。

返回值:成功返回0;失败将返回非零值。

raise()其实等价于:

c 复制代码
kill(getpid(), sig);

Tips:getpid()函数用于获取进程自身的pid。

测试

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
static void sig_handler(int sig)
{
    printf("Received signal: %d\n", sig);
}
int main(int argc, char *argv[])
{
    struct sigaction sig = {0};
    int ret;
    sig.sa_handler = sig_handler;
    sig.sa_flags = 0;
    ret = sigaction(SIGINT, &sig, NULL);
    if (-1 == ret)
    {
        perror("sigaction error");
        exit(-1);
    }
    for (;;)
    {
        /* 向自身发送SIGINT 信号*/
        if (0 != raise(SIGINT))
        {
            printf("raise error\n");
            exit(-1);
        }
        sleep(3); // 每隔3 秒发送一次
    }
    exit(0);
}

alarm()和pause()函数

本小节向大家介绍两个系统调用alarm()和pause()。

alarm()函数

使用alarm()函数可以设置一个定时器(闹钟),当定时器定时时间到时,内核会向进程发送SIGALRM

信号,其函数原型如下所示:

c 复制代码
#include <unistd.h>
unsigned int alarm(unsigned int seconds);

函数参数和返回值:

seconds:设置定时时间,以秒为单位;如果参数seconds 等于0,则表示取消之前设置的alarm 闹钟。

返回值:如果在调用alarm()时,之前已经为该进程设置了alarm 闹钟还没有超时,则该闹钟的剩余值作为本次alarm()函数调用的返回值,之前设置的闹钟则被新的替代;否则返回0。

参数seconds 的值是产生SIGALRM 信号需要经过的时钟秒数,当这一刻到达时,由内核产生该信号,每个进程只能设置一个alarm 闹钟;虽然SIGALRM 信号的系统默认操作是终止进程,但是如果程序当中设置了alarm 闹钟,但大多数使用闹钟的进程都会捕获此信号。

需要注意的是alarm 闹钟并不能循环触发,只能触发一次,若想要实现循环触发,可以在SIGALRM 信号处理函数中再次调用alarm()函数设置定时器。

测试

使用alarm()来设计一个闹钟。

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
static void sig_handler(int sig)
{
    puts("Alarm timeout");
    exit(0);
}
int main(int argc, char *argv[])
{
    struct sigaction sig = {0};
    int second;
    /* 检验传参个数*/
    if (2 > argc)
        exit(-1);
    /* 为SIGALRM 信号绑定处理函数*/
    sig.sa_handler = sig_handler;
    sig.sa_flags = 0;
    if (-1 == sigaction(SIGALRM, &sig, NULL))
    {
        perror("sigaction error");
        exit(-1);
    }
    /* 启动alarm 定时器*/
    second = atoi(argv[1]);
    printf("定时时长: %d 秒\n", second);
    alarm(second);
    /* 循环*/
    for (;;)
        sleep(1);
    exit(0);
}

运行测试:

pause()函数

pause()系统调用可以使得进程暂停运行、进入休眠状态,直到进程捕获到一个信号为止,只有执行了信号处理函数并从其返回时,pause()才返回,在这种情况下,pause()返回-1,并且将errno 设置为EINTR。其函数原型如下所示:

c 复制代码
#include <unistd.h>
int pause(void);

测试

通过alarm()和pause()函数模拟sleep 功能。

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
static void sig_handler(int sig)
{
    puts("Alarm timeout");
}
int main(int argc, char *argv[])
{
    struct sigaction sig = {0};
    int second;
    /* 检验传参个数*/
    if (2 > argc)
        exit(-1);
    /* 为SIGALRM 信号绑定处理函数*/
    sig.sa_handler = sig_handler;
    sig.sa_flags = 0;
    if (-1 == sigaction(SIGALRM, &sig, NULL))
    {
        perror("sigaction error");
        exit(-1);
    }
    /* 启动alarm 定时器*/
    second = atoi(argv[1]);
    printf("定时时长: %d 秒\n", second);
    alarm(second);
    /* 进入休眠状态*/
    pause();
    puts("休眠结束");
    exit(0);
}

运行测试:

信号集

通常我们需要有一个能表示多个信号(一组信号)的数据类型---信号集(signal set),很多系统调用都使用到了信号集这种数据类型来作为参数传递,譬如sigaction()函数、sigprocmask()函数、sigpending()函数等。本小节向大家介绍信号集这个数据类型。

信号集其实就是sigset_t 类型数据结构,来看看:

c 复制代码
# define _SIGSET_NWORDS (1024 / (8 * sizeof (unsigned long int)))
typedef struct
{
	unsigned long int __val[_SIGSET_NWORDS];
} sigset_t;

使用这个结构体可以表示一组信号,将多个信号添加到该数据结构中,当然Linux 系统了用于操作

sigset_t 信号集的API,譬如sigemptyset()、sigfillset()、sigaddset()、sigdelset()、sigismember(),接下来向大家介绍。

初始化信号集

sigemptyset()和sigfillset()用于初始化信号集。sigemptyset()初始化信号集,使其不包含任何信号;而

sigfillset()函数初始化信号集,使其包含所有信号(包括所有实时信号),函数原型如下:

c 复制代码
#include <signal.h>
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);

使用这些函数需要包含头文件<signal.h>。

函数参数和返回值含义如下:

set:指向需要进行初始化的信号集变量。

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

使用示例

初始化为空信号集:

c 复制代码
sigset_t sig_set;
sigemptyset(&sig_set);

初始化信号集,使其包含所有信号:

c 复制代码
sigset_t sig_set;
sigfillset(&sig_set);

向信号集中添加/删除信号

分别使用sigaddset()和sigdelset()函数向信号集中添加或移除一个信号,函数原型如下:

c 复制代码
#include <signal.h>
int sigaddset(sigset_t *set, int signum);
int sigdelset(sigset_t *set, int signum);

函数参数和返回值含义如下:

set:指向信号集。

signum:需要添加/删除的信号。

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

使用示例

向信号集中添加信号:

c 复制代码
sigset_t sig_set;
sigemptyset(&sig_set);
sigaddset(&sig_set, SIGINT);

从信号集中移除信号:

c 复制代码
sigset_t sig_set;
sigfillset(&sig_set);
sigdelset(&sig_set, SIGINT);

测试信号是否在信号集中

使用sigismember()函数可以测试某一个信号是否在指定的信号集中,函数原型如下所示:

c 复制代码
#include <signal.h>
int sigismember(const sigset_t *set, int signum);

函数参数和返回值含义如下:

set:指定信号集。

signum:需要进行测试的信号。

返回值:如果信号signum 在信号集set 中,则返回1;如果不在信号集set 中,则返回0;失败则返回-

1,并设置errno。

使用示例

判断SIGINT 信号是否在sig_set 信号集中:

c 复制代码
sigset_t sig_set;
......
if (1 == sigismember(&sig_set, SIGINT))
puts("信号集中包含SIGINT 信号");
else if (!sigismember(&sig_set, SIGINT))
puts("信号集中不包含SIGINT 信号");

获取信号的描述信息

在Linux 下,每个信号都有一串与之相对应的字符串描述信息,用于对该信号进行相应的描述。这些字符串位于sys_siglist 数组中,sys_siglist 数组是一个char *类型的数组,数组中的每一个元素存放的是一个字符串指针,指向一个信号描述信息。譬如,可以使用sys_siglistSIGINT来获取对SIGINT 信号的描述。我们编写一个简单地程序进行测试:

Tips:使用sys_siglist 数组需要包含<signal.h>头文件。

c 复制代码
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
    printf("SIGINT 描述信息: %s\n", sys_siglist[SIGINT]);
    printf("SIGQUIT 描述信息: %s\n", sys_siglist[SIGQUIT]);
    printf("SIGBUS 描述信息: %s\n", sys_siglist[SIGBUS]);
    exit(0);
}

运行结果:

从图中打印信息可知,这个描述信息其实非常简洁,没什么太多的信息。

strsignal()函数

除了直接使用sys_siglist 数组获取描述信息之外,还可以使用strsignal()函数。较之于直接引用sys_siglist

数组,更推荐使用strsignal()函数,其函数原型如下所示:

c 复制代码
#include <string.h>
char *strsignal(int sig);

使用strsignal()函数需要包含头文件<string.h>,这是一个库函数。

调用strsignal()函数将会获取到参数sig 指定的信号对应的描述信息,返回该描述信息字符串的指针;函数会对参数sig 进行检查,若传入的sig 无效,则会返回"Unknown signal"信息。

使用示例

c 复制代码
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void)
{
    printf("SIGINT 描述信息: %s\n", strsignal(SIGINT));
    printf("SIGQUIT 描述信息: %s\n", strsignal(SIGQUIT));
    printf("SIGBUS 描述信息: %s\n", strsignal(SIGBUS));
    printf("编号为1000 的描述信息: %s\n", strsignal(1000));
    exit(0);
}

测试结果:

psignal()函数

psignal()可以在标准错误(stderr)上输出信号描述信息,其函数原型如下所示:

c 复制代码
#include <signal.h>
void psignal(int sig, const char *s);

调用psignal()函数会将参数sig 指定的信号对应的描述信息输出到标准错误,并且还允许调用者添加一些输出信息,由参数s 指定;所以整个输出信息由字符串s、冒号、空格、描述信号编号sig 的字符串和尾随的换行符组成。

使用示例

c 复制代码
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void)
{
    psignal(SIGINT, "SIGINT 信号描述信息");
    psignal(SIGQUIT, "SIGQUIT 信号描述信息");
    psignal(SIGBUS, "SIGBUS 信号描述信息");
    exit(0);
}

运行结果:

信号掩码(阻塞信号传递)

内核为每一个进程维护了一个信号掩码(其实就是一个信号集),即一组信号。当进程接收到一个属于信号掩码中定义的信号时,该信号将会被阻塞、无法传递给进程进行处理,那么内核会将其阻塞,直到该信号从信号掩码中移除,内核才会把该信号传递给进程从而得到处理。

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

⚫ 当应用程序调用signal()或sigaction()函数为某一个信号设置处理方式时,进程会自动将该信号添加到信号掩码中,这样保证了在处理一个给定的信号时,如果此信号再次发生,那么它将会被阻塞;当然对于sigaction()而言,是否会如此,需要根据sigaction()函数是否设置了SA_NODEFER 标志而定;当信号处理函数结束返回后,会自动将该信号从信号掩码中移除。

⚫ 使用sigaction()函数为信号设置处理方式时,可以额外指定一组信号,当调用信号处理函数时将该组信号自动添加到信号掩码中,当信号处理函数结束返回后,再将这组信号从信号掩码中移除;通过sa_mask 参数进行设置,参考8.4.2 小节内容。

⚫ 除了以上两种方式之外,还可以使用sigprocmask()系统调用,随时可以显式地向信号掩码中添加/

移除信号。

sigprocmask()函数原型如下所示:

c 复制代码
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

使用该函数需要包含头文件<signal.h>。

函数参数和返回值含义如下:

how:参数how 指定了调用函数时的一些行为。

set:将参数set 指向的信号集内的所有信号添加到信号掩码中或者从信号掩码中移除;如果参数set 为

NULL,则表示无需对当前信号掩码作出改动。

oldset:如果参数oldset 不为NULL,在向信号掩码中添加新的信号之前,获取到进程当前的信号掩码,存放在oldset 所指定的信号集中;如果为NULL 则表示不获取当前的信号掩码。

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

参数how 可以设置为以下宏:

⚫ SIG_BLOCK:将参数set 所指向的信号集内的所有信号添加到进程的信号掩码中。换言之,将信号掩码设置为当前值与set 的并集。

⚫ SIG_UNBLOCK:将参数set 指向的信号集内的所有信号从进程信号掩码中移除。

⚫ SIG_SETMASK:进程信号掩码直接设置为参数set 指向的信号集。

使用示例

将信号SIGINT 添加到进程的信号掩码中:

c 复制代码
int ret;
/* 定义信号集*/
sigset_t sig_set;
/* 将信号集初始化为空*/
sigemptyset(&sig_set);
/* 向信号集中添加SIGINT 信号*/
sigaddset(&sig_set, SIGINT);
/* 向进程的信号掩码中添加信号*/
ret = sigprocmask(SIG_BLOCK, &sig_set, NULL);
if (-1 == ret)
{
    perror("sigprocmask error");
    exit(-1);
}

从信号掩码中移除SIGINT 信号: int ret;

c 复制代码
/* 定义信号集*/
sigset_t sig_set;
/* 将信号集初始化为空*/
sigemptyset(&sig_set);
/* 向信号集中添加SIGINT 信号*/
sigaddset(&sig_set, SIGINT);
/* 从信号掩码中移除信号*/
ret = sigprocmask(SIG_UNBLOCK, &sig_set, NULL);
if (-1 == ret)
{
    perror("sigprocmask error");
    exit(-1);
}

下面我们编写一个简单地测试代码,验证信号掩码的作用,测试代码如下所示:

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
static void sig_handler(int sig)
{
    printf("执行信号处理函数...\n");
}
int main(void)
{
    struct sigaction sig = {0};
    sigset_t sig_set;
    /* 注册信号处理函数*/
    sig.sa_handler = sig_handler;
    sig.sa_flags = 0;
    if (-1 == sigaction(SIGINT, &sig, NULL))
        exit(-1);
    /* 信号集初始化*/
    sigemptyset(&sig_set);
    sigaddset(&sig_set, SIGINT);
    /* 向信号掩码中添加信号*/
    if (-1 == sigprocmask(SIG_BLOCK, &sig_set, NULL))
        exit(-1);
    /* 向自己发送信号*/
    raise(SIGINT);
    /* 休眠2 秒*/
    sleep(2);
    printf("休眠结束\n");
    /* 从信号掩码中移除添加的信号*/
    if (-1 == sigprocmask(SIG_UNBLOCK, &sig_set, NULL))
        exit(-1);
    exit(0);
}

上述代码中,我们为SIGINT 信号注册了一个处理函数sig_handler,当进程接收到该信号之后就会执行它;然后调用sigprocmask 函数将SIGINT 信号添加到信号掩码中,然后再调用raise(SIGINT)向自己发送一个SIGINT 信号,如果信号掩码没有生效、也就意味着SIGINT 信号不会被阻塞,那么调用raise(SIGINT)之后应该就会立马执行sig_handler 函数,从而打印出"执行信号处理函数..."字符串信息;如果设置的信号掩码生效了,则并不会立马执行信号处理函数,而是在2 秒后才执行,因为程序中使用sleep(2)休眠了2 秒钟之后,才将SIGINT 信号从信号掩码中移除,故而进程才会处理该信号,在移除之前接收到该信号会将其阻塞。

编译测试结果如下:

阻塞等待信号sigsuspend()

上一小节已经说明,更改进程的信号掩码可以阻塞所选择的信号,或解除对它们的阻塞。使用这种技术可以保护不希望由信号中断的关键代码段。如果希望对一个信号解除阻塞后,然后调用pause()以等待之前被阻塞的信号的传递,这将如何?譬如有如下代码段:

c 复制代码
sigset_t new_set, old_set;
/* 信号集初始化*/
sigemptyset(&new_set);
sigaddset(&new_set, SIGINT);
/* 向信号掩码中添加信号*/
if (-1 == sigprocmask(SIG_BLOCK, &new_set, &old_set))
    exit(-1);
/* 受保护的关键代码段*/
......
    /**********************/
    /* 恢复信号掩码*/
    if (-1 == sigprocmask(SIG_SETMASK, &old_set, NULL))
        exit(-1);
pause(); /* 等待信号唤醒*/

执行受保护的关键代码时不希望被SIGINT 信号打断,所以在执行关键代码之前将SIGINT 信号添加到进程的信号掩码中,执行完毕之后再恢复之前的信号掩码。最后调用了pause()阻塞等待被信号唤醒,如果此时发生了信号则会被唤醒、从pause 返回继续执行;考虑到这样一种情况,如果信号的传递恰好发生在第二次调用sigprocmask()之后、pause()之前,如果确实发生了这种情况,就会产生一个问题,信号传递过来就会导致执行信号的处理函数,而从处理函数返回后又回到主程序继续执行,从而进入到pause()被阻塞,知道下一次信号发生时才会被唤醒,这有违代码的本意。

虽然信号传递发生在这个时间段的可能性并不大,但并不是完全没有可能,这必然是一个缺陷,要避免这个问题,需要将恢复信号掩码和pause()挂起进程这两个动作封装成一个原子操作,这正是sigsuspend()系统调用的目的所在,sigsuspend()函数原型如下所示:

c 复制代码
#include <signal.h>
int sigsuspend(const sigset_t *mask);

使用该函数需要包含头文件#include <signal.h>。

函数参数和返回值含义如下:

mask:参数mask 指向一个信号集。

返回值:sigsuspend()始终返回-1,并设置errno 来指示错误(通常为EINTR),表示被信号所中断,如果调用失败,将errno 设置为EFAULT。

sigsuspend()函数会将参数mask 所指向的信号集来替换进程的信号掩码,也就是将进程的信号掩码设置为参数mask 所指向的信号集,然后挂起进程,直到捕获到信号被唤醒(如果捕获的信号是mask 信号集中的成员,将不会唤醒、继续挂起)、并从信号处理函数返回,一旦从信号处理函数返回,sigsuspend()会将进程的信号掩码恢复成调用前的值。

调用sigsuspend()函数相当于以不可中断(原子操作)的方式执行以下操作:

c 复制代码
sigprocmask(SIG_SETMASK, &mask, &old_mask);
pause();
sigprocmask(SIG_SETMASK, &old_mask, NULL);

使用示例

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
static void sig_handler(int sig)
{
    printf("执行信号处理函数...\n");
}
int main(void)
{
    struct sigaction sig = {0};
    sigset_t new_mask, old_mask, wait_mask;
    /* 信号集初始化*/
    sigemptyset(&new_mask);
    sigaddset(&new_mask, SIGINT);
    sigemptyset(&wait_mask);
    /* 注册信号处理函数*/
    sig.sa_handler = sig_handler;
    sig.sa_flags = 0;
    if (-1 == sigaction(SIGINT, &sig, NULL))
        exit(-1);
    /* 向信号掩码中添加信号*/
    if (-1 == sigprocmask(SIG_BLOCK, &new_mask, &old_mask))
        exit(-1);
    /* 执行保护代码段*/
    puts("执行保护代码段");
    /******************/
    /* 挂起、等待信号唤醒*/
    if (-1 != sigsuspend(&wait_mask))
        exit(-1);
    /* 恢复信号掩码*/
    if (-1 == sigprocmask(SIG_SETMASK, &old_mask, NULL))
        exit(-1);
    exit(0);
}

在上述代码中,我们希望执行受保护代码段时不被SIGINT 中断信号打断,所以在执行保护代码段之前将SIGINT 信号添加到进程的信号掩码中,执行完受保护的代码段之后,调用sigsuspend()挂起进程,等待被信号唤醒,被唤醒之后再解除SIGINT 信号的阻塞状态。

实时信号

如果进程当前正在执行信号处理函数,在处理信号期间接收到了新的信号,如果该信号是信号掩码中的成员,那么内核会将其阻塞,将该信号添加到进程的等待信号集(等待被处理,处于等待状态的信号)中,为了确定进程中处于等待状态的是哪些信号,可以使用sigpending()函数获取。

sigpending()函数

其函数原型如下所示:

c 复制代码
#include <signal.h>
int sigpending(sigset_t *set);

使用该函数需要包含头文件<signal.h>。

函数参数和返回值含义如下:

set:处于等待状态的信号会存放在参数set 所指向的信号集中。

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

使用示例

判断SIGINT 信号当前是否处于等待状态

c 复制代码
/* 定义信号集*/
sigset_t sig_set;
/* 将信号集初始化为空*/
sigemptyset(&sig_set);
/* 获取当前处于等待状态的信号*/
sigpending(&sig_set);
/* 判断SIGINT 信号是否处于等待状态*/
if (1 == sigismember(&sig_set, SIGINT))
    puts("SIGINT 信号处于等待状态");
else if (!sigismember(&sig_set, SIGINT))
    puts("SIGINT 信号未处于等待状态");

发送实时信号

等待信号集只是一个掩码,仅表明一个信号是否发生,而不能表示其发生的次数。换言之,如果一个同一个信号在阻塞状态下产生了多次,那么会将该信号记录在等待信号集中,并在之后仅传递一次(仅当做发生了一次),这是标准信号的缺点之一。

实时信号较之于标准信号,其优势如下:

⚫ 实时信号的信号范围有所扩大,可应用于应用程序自定义的目的,而标准信号仅提供了两个信号可用于应用程序自定义使用:SIGUSR1 和SIGUSR2。

⚫ 内核对于实时信号所采取的是队列化管理。如果将某一实时信号多次发送给另一个进程,那么将会多次传递此信号。相反,对于某一标准信号正在等待某一进程,而此时即使再次向该进程发送此信号,信号也只会传递一次。

⚫ 当发送一个实时信号时,可为信号指定伴随数据(一整形数据或者指针值),供接收信号的进程在它的信号处理函数中获取。

⚫ 不同实时信号的传递顺序得到保障。如果有多个不同的实时信号处于等待状态,那么将率先传递具有最小编号的信号。换言之,信号的编号越小,其优先级越高,如果是同一类型的多个信号在排队,那么信号(以及伴随数据)的传递顺序与信号发送来时的顺序保持一致。

Linux 内核定义了31 个不同的实时信号,信号编号范围为34~64,使用SIGRTMIN 表示编号最小的实时信号,使用SIGRTMAX 表示编号最大的实时信号,其它信号编号可使用这两个宏加上一个整数或减去一个整数。

应用程序当中使用实时信号,需要有以下的两点要求:

⚫ 发送进程使用sigqueue()系统调用向另一个进程发送实时信号以及伴随数据。

⚫ 接收实时信号的进程要为该信号建立一个信号处理函数,使用sigaction 函数为信号建立处理函数,并加入SA_SIGINFO,这样信号处理函数才能够接收到实时信号以及伴随数据,也就是要使用

sa_sigaction 指针指向的处理函数,而不是sa_handler,当然允许应用程序使用sa_handler,但这样就不能获取到实时信号的伴随数据了。

使用sigqueue()函数发送实时信号,其函数原型如下所示:

c 复制代码
#include <signal.h>
int sigqueue(pid_t pid, int sig, const union sigval value);

使用该函数需要包含头文件<signal.h>。

函数参数和返回值含义如下:

pid:指定接收信号的进程对应的pid,将信号发送给该进程。

sig:指定需要发送的信号。与kill()函数一样,也可将参数sig 设置为0,用于检查参数pid 所指定的进程是否存在。

value:参数value 指定了信号的伴随数据,union sigval 数据类型。

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

union sigval 数据类型(共用体)如下所示:

c 复制代码
typedef union sigval
{
	int sival_int;
	void *sival_ptr;
} sigval_t;

携带的伴随数据,既可以指定一个整形的数据,也可以指定一个指针。

使用示例

(1)发送进程使用sigqueue()系统调用向另一个进程发送实时信号

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
int main(int argc, char *argv[])
{
    union sigval sig_val;
    int pid;
    int sig;
    /* 判断传参个数*/
    if (3 > argc)
        exit(-1);
    /* 获取用户传递的参数*/
    pid = atoi(argv[1]);
    sig = atoi(argv[2]);
    printf("pid: %d\nsignal: %d\n", pid, sig);
    /* 发送信号*/
    sig_val.sival_int = 10; // 伴随数据
    if (-1 == sigqueue(pid, sig, sig_val))
    {
        perror("sigqueue error");
        exit(-1);
    }
    puts("信号发送成功!");
    exit(0);
}

(2)接收进程使用sigaction()函数为信号绑定处理函数

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
static void sig_handler(int sig, siginfo_t *info, void *context)
{
    sigval_t sig_val = info->si_value;
    printf("接收到实时信号: %d\n", sig);
    printf("伴随数据为: %d\n", sig_val.sival_int);
}
int main(int argc, char *argv[])
{
    struct sigaction sig = {0};
    int num;
    /* 判断传参个数*/
    if (2 > argc)
        exit(-1);
    /* 获取用户传递的参数*/
    num = atoi(argv[1]);
    /* 为实时信号绑定处理函数*/
    sig.sa_sigaction = sig_handler;
    sig.sa_flags = SA_SIGINFO;
    if (-1 == sigaction(num, &sig, NULL))
    {
        perror("sigaction error");
        exit(-1);
    }
    /* 死循环*/
    for (;;)
        sleep(1);
    exit(0);
}

异常退出abort()函数

在3.3 小节中给大家介绍了应用程序中结束进程的几种方法,譬如使用exit()、_exit()或_Exit()这些函数来终止进程,然后这些方法使用于正常退出应用程序,而对于异常退出程序,则一般使用abort()库函数,使用abort()终止进程运行,会生成核心转储文件,可用于判断程序调用abort()时的程序状态。

abort()函数原型如下所示:

c 复制代码
#include <stdlib.h>
void abort(void);

函数abort()通常产生SIGABRT 信号来终止调用该函数的进程,SIGABRT 信号的系统默认操作是终止进程运行、并生成核心转储文件;当调用abort()函数之后,内核会向进程发送SIGABRT 信号。

使用示例

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
static void sig_handler(int sig)
{
    printf("接收到信号: %d\n", sig);
}
int main(int argc, char *argv[])
{
    struct sigaction sig = {0};
    sig.sa_handler = sig_handler;
    sig.sa_flags = 0;
    if (-1 == sigaction(SIGABRT, &sig, NULL))
    {
        perror("sigaction error");
        exit(-1);
    }
    sleep(2);
    abort(); // 调用abort
    for (;;)
        sleep(1);
    exit(0);
}

运行测试:

从打印信息可知,即使在我们的程序当中捕获了SIGABRT 信号,但是程序依然会无情的终止,无论阻塞或忽略SIGABRT 信号,abort()调用均不收到影响,总会成功终止进程。

相关推荐
夜听莺儿鸣8 小时前
502-003_Linux 中断与异常(一):从Linux角度理解中断与异常
linux·中断与异常·linux中断
杨云龙UP9 小时前
TDengine 3.4.2.8 Community 三节点三副本生产集群部署实战(DNode/MNode/taosAdapter/Explorer)
大数据·linux·运维·数据库·tdengine·时序库
向成科技10 小时前
XC3576H工控主板|深度适配Ubuntu 26.04 LTS,释放边缘AI与工业开发新潜能
linux·人工智能·ubuntu·机器人·硬件·主板·边缘ai
Fcy64810 小时前
Linux下 进程间关系与守护进程
linux·运维·服务器·守护进程
码农小韩11 小时前
Linux驱动理论(二)——Linux字符设备驱动
linux·嵌入式软件开发·linux操作系统·linux应用开发·linux驱动理论
well061211 小时前
Linux粘滞位与Makefile机制深度解析
linux·运维·服务器
再写一行代码就下班11 小时前
linux sh脚本在windows修改导致无法使用解决方式
java·linux·centos
额额额对了11 小时前
SPI通信
linux·c语言·汇编·单片机·嵌入式硬件·arm
子木HAPPY阳VIP12 小时前
Ubuntu 关闭防火墙操作步骤
linux·运维·ubuntu
王振超wzc12 小时前
嵌入式开发环境搭建--WM软件安装,Ubuntu操作系统安装
linux·运维·ubuntu