TLPI读书笔记第20章-信号:基本概念1

本章和接下来的两章将讨论信号。虽然基本概念较为简单,但因为要涵盖大量细节,所以篇幅较长。本章包括以下主题。

1.各种不同信号及其用途。

2.内核可能为进程产生信号的环境,以及某一进程向另一进程发送信号所使用的系统调用。

3.进程在默认情况下对信号的响应方式,以及进程改变对信号响应方式的手段,特别是借助于信号处理器程序的手段,即程序收到信号时去自动调用的函数,由程序员定义。

4.使用进程信号掩码来阻塞信号,以及等待信号的相关概念。

5.如何暂停进程的执行,并等待信号的到达

20.1 概念和概述

信号是事件发生时对进程的通知机制。有时也称之为软件中断。信号与硬件中断的相似之处在于打断了程序执行的正常流程,大多数情况下,无法预测信号到达的精确时间。 一个具有权限的进程能够向另一进程发送信号。信号的这一用法可作为一种同步技术,甚至是进程间通信( IPC)的原始形式。进程也可以向自身发送信号。然而,发往进程的诸多信号,通常都是源于内核。引发内核为进程产生信号的各类事件如下。

1.硬件发生异常,即硬件检测到一个错误条件并通知内核,随即再由内核发送相应信号给相关进程。硬件异常的例子包括执行一条异常的机器语言指令,诸如,被 0 除,或者引用了无法访问的内存区域。

2.用户键入了能够产生信号的终端特殊字符。包括中断(通常是 Control-C)、暂停(通常是 Control-Z)。

3.发生了软件事件。例如,针对文件描述符的输出变为有效,调整了终端窗口大小,定时器到期,进程执行的 CPU 时间超限,或者该进程的某个子进程退出。

针对每个信号,都定义了一个唯一的(小)整数,从 1 开始顺序展开。 <signal.h>以 SIGxxxx形式的符号名对这些整数做了定义。由于每个信号的实际编号随系统不同而不同,所以在程序中总是使用这些符号名。例如,当用户键入中断字符时,将传递给进程 SIGINT 信号(信号编号为 2)。

信号分为两大类。

第一组用于内核向进程通知事件,构成所谓传统或者标准信号。 Linux 中标准信号的编号范围为 1~31。本章将描述这些标准信号。

另一组信号由实时信号构成,其与标准信号的差异将在 22.8 节中描述。 信号因某些事件而产生。信号产生后,会于稍后被传递给某一进程,而进程也会采取某些措施来响应信号。在产生和到达期间,信号处于等待( pending)状态。 通常,一旦(内核)接下来要调度该进程运行,等待信号会马上送达,或者如果进程正在运行,则会立即传递信号(例如,进程向自身发送信号)。然而,有时需要确保一段代码不为传递来的信号所中断。为了做到这一点,可以将信号添加到进程的信号掩码中—目前会阻塞该组信号的到达。如果所产生的信号属于阻塞之列,那么信号将保持等待状态,直至稍后对其解除阻塞(从信号掩码中移除)。

进程可使用各种系统调用对其信号掩码添加和移除信号。信号到达后,进程视具体信号执行如下默认操作之一。

1.忽略信号:也就是说,内核将信号丢弃,信号对进程没有产生任何影响

2.终止(杀死)进程:这有时是指进程异常终止,而不是进程因调用 exit()而发生的正常终止。

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

4.停止进程:暂停进程的执行。

5.于之前暂停后再度恢复进程的执行

除了根据特定信号而采取默认行为之外,程序也能改变信号到达时的响应行为。也将此称之为对信号的处置( disposition)设置。程序可以将对信号的处置设置为如下之一。

1.采取默认行为。这适用于撤销之前对信号处置的修改、恢复其默认处置的场景。

2.忽略信号。这适用于默认行为为终止进程的信号。

3.执行信号处理器程序 信号处理器程序是由程序员编写的函数,用于为响应传递来的信号而执行适当任务。例如, shell 为 SIGINT 信号(由中断字符串 Control-C 产生)提供了一个处理器程序,令其停止当前正在执行的工作,并将控制返回(shell 的)主输入循环,并再次向用户呈现 shell 提示符。

通知内核应当去调用某一处理器程序的行为,通常称之为安装或者建立信号处理器程序。调用信号处理器程序以响应传递来的信号,则称之为信号已处理(handled),或者已捕获(caught)。 请注意,无法将信号处置设置为终止进程或者转储核心(除非这是对信号的默认处置)。 效果最为近似的是为信号安装一个处理器程序,并于其中调用 exit()或者 abort()。 abort()函数( 21.2.2 节)为进程产生一个 SIGABRT 信号,该信号将引发进程转储核心文件并终止。

信号在 UNIX 实现中出现很早,诞生之后又历经变革。在早期实现中,信号在特定场景下有可能会丢失(即,没有传递到目标进程)。此外,尽管系统提供了执行关键代码时阻塞信号传递的机制,但阻塞有时也不大可靠。 4.2BSD 利用所谓可靠信号解决了这些问题。 System V 后来也为信号增加了可靠语义,但采用的模型与 BSD 无法兼容。这一不兼容性直到 POSIX.1-1990 标准出台后才得以解决。 该标准针对可靠信号所采取的规范主要基于 BSD模型。

20.2 信号类型和默认行为

此前曾提及,Linux 对标准信号的编号为 1~31。然而, Linux 于 signal(7)手册页中列出的信号名称却超出了 31 个。名称超出的原因有多种。有些名称只是其他名称的同义词,之所以定义是为了与其他 UNIX 实现保持源码兼容。其他名称虽然有定义,但却并未使用。以下列表介绍了各种信号。

SIGABRT 当进程调用 abort()函数(21.2.2 节)时,系统向进程发送该信号。默认情况下,该信号会终止进程,并产生核心转储文件。这实现了调用 abort()的预期目标,产生核心转储文件用于调试。

SIGALRM 经调用 alarm()或 setitimer()而设置的实时定时器一旦到期,内核将产生该信号。实时定时器是根据挂钟时间进行计时的(即人类对逝去时间的概念)。

SIGBUS 产生该信号(总线错误, bus error)即表示发生了某种内存访问错误。如 49.4.3 节所述,当使用由 mmap()所创建的内存映射时,如果试图访问的地址超出了底层内存映射文件的结尾,那么将产生该错误。

SIGCHLD 当父进程的某一子进程终止(或者因为调用了 exit(),或者因为被信号杀死)时, (内核)将向父进程发送该信号。当父进程的某一子进程因收到信号而停止或恢复时,也可能会向父进程发送该信号。详情请参考 26.3 节。

SIGCLD 与 SIGCHLD 信号同义。

SIGCONT 将该信号发送给已停止的进程,进程将会恢复运行(即在之后某个时间点重新获得调度)。当接收信号的进程当前不处于停止状态时,默认情况下将忽略该信号。进程可以捕获该信号,以便在恢复运行时可以执行某些操作。关于该信号的更多细节请参考 22.2 节和 34.7 节。

SIGEMT UNIX 系统通常用该信号来标识一个依赖于实现的硬件错误。 Linux 系统仅在 Sun SPARC 实现中使用了该信号。后缀 EMT 源自仿真器陷阱( emulator trap), Digital PDP-11 的汇编程序助记符之一。

SIGFPE 该信号因特定类型的算术错误而产生,比如除以 0。后缀 FPE 是浮点异常的缩写,不过整型算术错误也能产生该信号。该信号于何时产生的精确细节取决于硬件架构和对 CPU 控制寄存器的设置。例如,在 x86-32 架构中,整数除以 0 总是产生 SIGFPE 信号,但是对浮点数除以 0 的处理则取决于是否启用了 FE_DIVBYZERO 异常。如果启用了该异常(使用feenableexcept()),那么浮点数除以 0 也将产生 SIGFPE 信号,否则,将为操作数产生符合 IEEE标准的结果(无穷大的浮点表示形式)。更多信息请参考 fenv(3)手册页和<fenv.h>文件。

SIGHUP 当终端断开(挂机)时,将发送该信号给终端控制进程。 34.6 节将描述控制进程的概念以及产生 SIGHUP 信号的各种环境。 SIGHUP 信号还可用于守护进程(比如, init、 httpd 和 inetd)。许多守护进程会在收到 SIGHUP 信号时重新进行初始化并重读配置文件。借助于显式执行kill命令或者运行同等功效的程序或脚本,系统管理员可向守护进程手工发送 SIGHUP 信号来触发这些行为。

SIGILL 如果进程试图执行非法的机器语言指令,系统将向进程发送该信号。

SIGINFO 在 Linux 中,该信号名与 SIGPWR 信号名同义。在 BSD 系统中,键入 Control-T 可产生SIGINFO 信号,用于获取前台进程组的状态信息。

SIGINT 当用户键入终端中断字符(通常为 Control-C)时,终端驱动程序将发送该信号给前台进程组。该信号的默认行为是终止进程。

SIGIO 利用 fcntl()系统调用,即可于特定类型(诸如终端和套接字)的打开文件描述符发生 I/O事件时产生该信号。

SIGIOT 在 Linux 中,该信号名与 SIGABRT 信号同义。在其他一些 UNIX 实现中,该信号表示发生了由实现定义的硬件错误。

SIGKILL 此信号为“必杀( sure kill)”信号,处理器程序无法将其阻塞、忽略或者捕获,故而“一击必杀”,总能终止进程。

SIGLOST Linux 中存在该信号名,但并未加以使用。在其他一些 UNIX 实现中,如果远端 NFS 服务器在崩溃之后重新恢复,而 NFS 客户端却未能重新获得由本地进程所持有的锁,那么NFS客户端将向这些进程发送此信号。

SIGPIPE 当某一进程试图向管道、 FIFO 或套接字写入信息时,如果这些设备并无相应的阅读进程,那么系统将产生该信号。之所以如此,通常是因为阅读进程已经关闭了其作为 IPC 通道的文件描述符。

SIGPOLL 该信号从 System V 派生而来,与 Linux 中的 SIGIO 信号同义。

SIGPROF 由 setitimer()调用所设置的性能分析定时器刚一过期,内核就将产生该信号。性能分析定时器用于记录进程所使用的 CPU 时间。与虚拟定时器不同,性能分析定时器在对 CPU 时间计数时会将用户态与内核态都包含在内。

SIGPWR 这是电源故障信号。当系统配备有不间断电源( UPS)时,可以设置守护进程来监控电源发生故障时备用电池的剩余电量。如果电池电量行将耗尽(长时间停电之后),那么监控进程会将该信号发往 init 进程,而后者则将其解读为快速、有序关闭系统的一个请求。 SIGQUIT 当用户在键盘上键入退出字符(通常为 Control-)时,该信号将发往前台进程组。默认情况下,该信号终止进程,并生成可用于调试的核心转储文件。进程如果陷入无限循环,或者不再响应时,使用 SIGQUIT 信号就很合适。键入 Control-,再调用 gdb 调试器加载刚才生成的核心转储文件,接着用 backtrace 命令来获取堆栈跟踪信息,就能发现正在执行的是程序的哪部分代码。 ( [Matloff, 2008]描述了 gdb 的用法。 ) SIGSEGV 这一信号非常常见,当应用程序对内存的引用无效时,就会产生该信号。引起对内存无效引用的原因很多,可能是因为要引用的页不存在(例如,该页位于堆和栈之间的未映射区域),或者进程试图更新只读内存(比如,程序文本段或者标记为只读的一块映射内存区域)中某一位置的内容,又或者进程企图在用户态(参见 2.1 节)去访问内核的部分内存。 C 语言中引发这些事件的往往是解引用的指针里包含了错误地址(例如,未初始化的指针),或者传递了一个无效参数供函数调用。该信号的命名源于术语“段违例”。

SIGSTKFLT signal(7)手册页中将其记载为“协处理器栈错误”, Linux 对该信号作了定义,但并未加以使用。

SIGSTOP 这是一个必停( sure stop)信号,处理器程序无法将其阻塞、忽略或者捕获,故而总是能停止进程。

SIGSYS 如果进程发起的系统调用有误,那么将产生该信号。这意味着系统将进程执行的指令视为一个系统调用陷阱( trap),但相关的系统调用编号却是无效的(参见 3.1 节)。

SIGTERM 这是用来终止进程的标准信号,也是 kill 和 killall 命令所发送的默认信号。用户有时会使用 kill-KILL 或者 kill-9 显式向进程发送 SIGKILL 信号。然而,这一做法通常是错误的。精心设计的应用程序应当为 SIGTERM 信号设置处理器程序,以便于其能够预先清除临时文件和释放其他资源,从而全身而退。发送 SIGKILL 信号可以杀掉某个进程,从而绕开了 SIGTERM信号的处理器程序。因此,总是应该首先尝试使用 SIGTERM 信号来终止进程,而把 SIGKILL 信号作为最后手段,去对付那些不响应 SIGTERM 信号的失控进程。 SIGTRAP 该信号用来实现断点调试功能以及 strace(1)命令(附录 A)所执行的跟踪系统调用功能。 SIGTSTP 这是作业控制的停止信号,当用户在键盘上输入挂起字符(通常是 Control-Z)时,将发送该信号给前台进程组,使其停止运行。第 34 章详细描述了进程组(作业)和作业控制,以及程序应在何时以及如何去处理该信号。该信号名源自“终端停止( terminal stop)”的术语。

SIGTTIN 在作业控制 shell 下运行时,若后台进程组试图对终端进行 read()操作,终端驱动程序则将向该进程组发送此信号。该信号默认将停止进程。

SIGTTOU 该信号的目的与 SIGTTIN 信号类似,但所针对的是后台作业的终端输出。在作业控制 shell下运行时,如果对终端启用了 TOSTOP(终端输出停止)选项(可能是通过 stty tostop 命令),而某一后台进程组试图对终端进行write()操作,那么终端驱动程序将向该进程组发送 SIGTTOU 信号。该信号默认将停止进程。 SIGUNUSED 顾名思义,该信号没有使用。在 Linux 2.4 及其后续版本中,该信号名在很多架构中与SIGSYS 信号同义。换言之,尽管信号名还保持向后兼容,但信号编号在这些架构中不再处于未使用状态。 SIGURG 系统发送该信号给一个进程,表示套接字上存在紧急数据。 SIGUSR1 该信号和 SIGUSR2 信号供程序员自定义使用。内核绝不会为进程产生这些信号。进程可以使用这些信号来相互通知事件的发生,或是彼此同步。在早期的 UNIX 实现中,这是可供应用随意使用的仅有的两个信号。(实际上,进程间可以相互发送任何信号,但如果内核也为进程产生了同类信号,这两种情况就有可能产生混淆。 )现代 UNIX 实现则提供了很多实时信号,也可用于程序员自定义的目的。 SIGUSR2 参见对 SIGUSR1 信号的描述。 SIGVTALRM 调用 setitimer()(参见 23.1 节)设置的虚拟定时器刚一到期,内核就会产生该信号。虚拟定时器计录的是进程在用户态所使用的 CPU 时间。 SIGWINCH 在窗口环境中,当终端窗口尺寸发生变化时,会向前台进程组发送该信号。借助于为该信号安装的处理器程序,诸如 vi 和 less 之类的程序会在窗口尺寸调整后重新绘制输出。

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

SIGXFSZ 如果进程因试图增大文件(调用 write()或 truncate())而突破对进程文件大小的资源限制时,那么将发送此信号给进程。

20.3 改变信号处置:signal

UNIX 系统提供了两种方法来改变信号处置: signal()和 sigaction()。本节描述的 signal()系统调用,是设置信号处置的原始 API,所提供的接口比 sigaction()简单。另一方面, sigaction()提供了 signal()所不具备的功能。进一步而言, signal()的行为在不同 UNIX 实现间存在差异(22.7节),这也意味着对可移植性有所追求的程序绝不能使用此调用来建立信号处理器函数。故此,sigaction()是建立信号处理器的首选 API(强力推荐)。自 20.13 节介绍了 sigaction()调用的用法之后,本书示例将一律采用该调用来建立信号处理器程序。

#include<signal.h>
void (*signal(int sig,void (*handler)(int)))(int)

这里需要对 signal()函数的原型做一些解释。第一个参数 sig,标识希望修改处置的信号编号,第二个参数 handler,则标识信号抵达时所调用函数的地址。该函数无返回值(void),并接收一个整型参数。因此,信号处理器函数一般具有以下形式:

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

signal()的返回值是之前的信号处置。像 handler 参数一样,这是一枚指针,所指向的是带有一个整型参数且无返回值的函数。换言之,编写如下代码,可以暂时为信号建立一个处理器函数,然后再将信号处置重置为其本来面目

void (*oldhandler)(int);
oldhandler=signal(SIGINT,newHandler);
if(oldhandler==SIGERR)
   errExit("signal")
/**/
if(signal(SIGINT,newHandler)==SIG_ERR)
   errExit("signal")

在为 signal()指定 handler 参数时,可以以如下值来代替函数地址:

SIG_DFL 将信号处置重置为默认值(表 20-1)。这适用于将之前 signal()调用所改变的信号处置还原。

SIG_IGN 忽略该信号。如果信号专为此进程而生,那么内核会默默将其丢弃。进程甚至从未知道曾经产生了该信号。 调用 signal()成功将返回先前的信号处置,有可能是先前安装的处理器函数地址,也可能是常量 SIG_DFL 和 SIG_IGN 之一。如果调用失败, signal()将返回 SIG_ERR

20.4 信号处理器简介

信号处理器程序(也称为信号捕捉器)是当指定信号传递给进程时将会调用的一个函数。本节描述了信号处理器的基本原理,而第 21 章将继续做详细介绍。 调用信号处理器程序,可能会随时打断主程序流程;内核代表进程来调用处理器程序,当处理器返回时,主程序会在处理器打断的位置恢复执行。这一工作序列可用图 20-1 来加以说明。

虽然信号处理器程序几乎可以为所欲为,但一般而言,设计应力求简单。 21.1 节将对这一点展开论述

程序清单 20-1 所示为一个简单的信号处理器函数,由主程序为 SIGINT 信号而建立。当键入中断字符(通常为 Control-C)时,终端驱动程序将产生该信号。处理器只是简单打印一条消息,随即返回。 主程序会持续循环。每次迭代,程序都将递增计数器值并将其打印出来,然后休眠几秒钟。 (为了按这种方式休眠,程序使用了 sleep()函数,该函数会令调用者处于暂停状态,持续时间则由指定的秒数决定。该函数将在 23.4.1 节中进行描述。 )

内核在调用信号处理器程序时,会将引发调用的信号编号作为一个整型参数传递给处理器函数。 (就是程序清单 20-1 中处理器函数的 sig 参数)。如果信号处理器程序只捕获一种类型的信号,那么这个参数几乎无用。然而,如果安装相同的处理器来捕获不同类型的信号,那么就可以利用此参数来判定引发对处理器调用的是何种信号。 程序清单 20-2 中程序展示了这一思路,为 SIGINT 和 SIGQUIT 信号建立了同一处理器程序。 (当键入终端退出字符时,通常为 Control-,终端驱动程序将产生 SIGQUIT 信号。 )处理器程序代码通过检查 sig 参数来区分这两种信号,并为每种信号采取不同措施。 main()函数则使用 pause()函数(参见 20.14 节的描述)来阻塞进程,直至捕获到信号。

程序清单 20-1 和程序清单 20-2 都在信号处理器程序中使用了 printf()函数来显示消息。现实世界的应用程序一般绝不会在信号处理器程序中使用 stdio 函数, 21.1.2 节将就其原因进行讨论。然而,本书各种示例仍然会在信号处理器程序中调用 printf()函数,作为观察处理器程序调用的一种简单手段

20.5 发送信号: kill()

与 shell 的 kill 命令相类似,一个进程能够使用 kill()系统调用向另一进程发送信号。 (之所以选择 kill 作为术语,是因为早期 UNIX 实现中大多数信号的默认行为是终止进程。 )

#include<signal.h>
int kill(pid_t pid,int sig);

pid 参数标识一个或多个目标进程,而 sig 则指定了要发送的信号。如何解释 pid,要视以下 4 种情况而定。 1.如果 pid 大于 0,那么会发送信号给由 pid 指定的进程。 2.如果 pid 等于 0,那么会发送信号给与调用进程同组的每个进程,包括调用进程自身。 3.如果 pid 小于-1,那么会向组 ID 等于该 pid 绝对值的进程组内所有下属进程发送信号。向一个进程组的所有进程发送信号在 shell 作业控制中有特殊用途 4.如果 pid 等于-1,那么信号的发送范围是: 调用进程有权将信号发往的每个目标进程,除去 init(进程 ID 为 1)和调用进程自身。如果特权级进程发起这一调用,那么会发送信号给系统中的所有进程,上述两个进程除外。显而易见,有时也将这种信号发送方式称之为广播信号。(SUSv3 并未要求将调用进程排除在信号的接收范围之外, Linux此处所遵循的是 BSD 系统的语义。 ) 如果并无进程与指定的 pid 相匹配,那么 kill()调用失败,同时将 errno 置为 ESRCH(“查无此进程”)。 进程要发送信号给另一进程,还需要适当的权限,其权限规则如下。

1.特权级( CAP_KILL)进程可以向任何进程发送信号。

2.以 root 用户和组运行的 init 进程(进程号为 1),是一种特例,仅能接收已安装了处理器函数的信号。这可以防止系统管理员意外杀死 init 进程—这一系统运作的基石。

3.如图 20-2 所示,如果发送者的实际或有效用户 ID 匹配于接受者的实际用户 ID 或者保存设置用户 ID(saved set-user-id),那么非特权进程也可以向另一进程发送信号。利用这一规则,用户可以向由他们启动的 set-user-ID 程序发送信号,而无需考虑目标进程有效用户 ID 的当前设置。将目标进程有效用户 ID 排除在检查范围之外,这一举措的辅助作用在于防止用户某甲向用户某乙的进程发送信号,而该进程正在执行的 set-user-ID程序又属于用户某甲。 (SUSv3 要求强制执行图 20-2 所示的规则,但如 kill(2)手册页所述, Linux 内核在 2.0 版本之前所遵循的规则略有不同。 )

4.SIGCONT 信号需要特殊处理。无论对用户 ID 的检查结果如何,非特权进程可以向同一会话中的任何其他进程发送这一信号。利用这一规则,运行作业控制的 shell 可以重启已停止的作业(进程组),即使作业进程已经修改了它们的用户 ID。 (亦即,使用9.7 节所述系统调用来改变其凭据,进而成为特权级进程。 )

如果进程无权发送信号给所请求的 pid,那么 kill()调用将失败,且将 errno 置为 EPERM。若 pid所指为一系列进程(即 pid 是负值)时,只要可以向其中之一发送信号,则 kill()调用成功。 程序清单 20-3 中展示了 kill()的用法

原文地址:https://www.cnblogs.com/wangbin2188/p/14691049.html