第20章 信号:基本概念

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

本章包含一下主题。

  • 各种不同信号及用途。
  • 内核可能位进程产生信号的环境,以及某一进程能向另一进程发送信号所使用的系统调用。
  • 内核在默认情况下对信号的响应方式,以及进程改变对信号响应方式的手段,特别是借助于信号处理器程序的手段,即程序收到信号时去自动调用的函数,由程序员定义。
  • 使用进程信号掩码来阻塞信号,以及等待信号的相关概念
  • 如何暂停进程的执行,并等待信号的到达。

20.1概念和描述

          信号时事件发生时对进程的通知机制。有时也称之为软中断。信号与硬件终端的相似之处在于打断了程序执行的正常流程,大多数情况下,无法预测信号到达的精确事件

        一个具有合适权限的进程能够向另一进程发送信号。信号的这一用法可作为一种同步技术,甚至是进程间通信(IPC)的原始形式。进程也可以向自身发送信号。然而发往进程的诸多信号,通常都是源于内核。引发内核位进程产生信号的各类事件如下:

  • 硬件发生异常,即检测到一个错误条件并通知内核,随即再由内核发送响应信号给相关进程。硬件异常的例子也包括执行一条异常的机器语言指令,诸如,被0除,或者引用了无法访问的内存区域
  • 用户键入了能够产生信号的终端特殊字符。其中包括中断字符(通常是Control-C)、暂停字符(通常是Control-Z)。
  • 发生了软件时间。例如针对文件描述符的输出变为有效,调整了终端窗口大小,定时器到期,进程执行的CPU时间超限,或者该进程的某个子进程退出。

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

        信号分为两大类。第一组用于内核先进程通知事件,构成所谓传统或者标准信号。Linux中标准信号的编号范围为1~~31.本章将描述这些标准信号。另一组信号由实时信号构成,其与标准信号的差异将在22.8节中描述。

        信号因某些事件而产生。信号产生后,会于稍后被传递给某一进程,而进程也会采取某些措施来响应信号。在产生和到达期间,信号处于等待(pending)状态。

        通常,一旦(内核)接下来要调度该进程运行,等待信号会马上到达,或者如果进程正在运行,则会立即传递信号(例如,进程向自身发送信号)。然而,有时候需要确保一段代码不为传递来的信号所中断。为了做到这一点,可以将信号添加到进程的信号掩码中------目前会阻塞该组信号的到达。如果产生的信号术属于阻塞之列,那么该信号将保持等待状态,直至稍后对其解除则色(从信号掩码中移除)。进程可使用各种系统调用对其信号掩码添加和移除信号。

        信号到达后,进程视具体型号之心如下默认操作之一。

  • 忽略信号:也就是说,内核将该信号丢弃,信号对进城没有产生任何影响(进程永远不知道曾经出现过该信号)。
  • 终止(杀死进程):这时候是指进程异常终止,而不是进程因调用exit()而发生的正常终止。
  • 产生核心转储文件,同时进程终止:核心转出文件包含对进城虚拟内存的镜像,可将其加载到调试器中已检查进程终止时的状态。
  • 停止进程:暂停进程的执行。
  • 于之前暂停后再度恢复进程的执行。

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

  • 采取默认行为,这适用于撤销之前对信号处置的修改、恢复期默认处置的场景。
  • 忽略信号。适用于默认行为终止进程的信号。
  • 执行信号处理器程序。

        信号处理器函数时由程序员编写的函数,用于为响应传递来的信号而执行适当任务,例如,shell为SIGINT信号(由中断字符串Control-C产生)提供了一个处理器程序,令其停止当前正在执行的工作,并将控制返回到(shell的)主输入循环,并再次向用户呈现shell提示符。通知内核应当调用某一处理器处理程序的行为,通常称之为安装或者建立信号处理器程序。调用信号处理器程序以响应传递来的信号,则称之为信号已处理,或者已捕获。

        请注意无法将信号处置设置为终止进程或者转出核心(除非这是对信号的默认处置)。

效果最为近似的是为信号安装一个处理器程序,并于其中调用exit()或者abort()。abort()函数为进程产生一个SIGABRT信号,该信号引发进程转出核心文件并终止。

Linux特有的/poc/PID/status文件包含有各种位掩码字段,通过检查这些掩码确定对信号的处理。位掩码以十六进制数形式显示,最低有效位代表信号1,相邻的左边一位代表2,以此类推。这些自选分别为SigPnd(基于线程的等待信号)、ShdPnd(进程级等待信号,始于Linux2.6)、SigBlk(阻塞信号)、SigIgn(忽略信号)和SigCgt(捕获信号)。33.2节阐述可多线程对信号的处理,这将有助于澄清SigPnd与ShdPnd之间的差异。

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()所创建的内存映射时,如果试图访问的地址超出了底层内存映射文件的结尾,那么将产生该错误。

SIGCHILD

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

SIGCLD

        与SIGCHILD信号同义。

SIGCONT

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

SIGEMT

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

SIGFPE

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

SIGHUB

        当终端断开(挂机时),将发送信号给终端进程。34.6节将描述控制进程的概念以及产生SIGHUP信号的各种环境。SIGHUB信号还可用于守护进程(比如,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客户端将向这些进程发送此信号。(NFS规范并未对该特性进行标准化)

SIGPIPE

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

SIGPOLL

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

SIGPROF

        由setitimer()调用(参见23.1节)所设置的性能分析定时器刚一过期,内核就将产生该信号。性能分析定时器用于疾苦进程所使用的CPU时间。与虚拟定时器不同(参见下面的SIGVTALRM信号),性能分析定时器在对COU时间计数时回将用户态与内核态都包含在内。

SIGPWR

        这是电源故障信号。当系统配备有不间断电源(UPS)时,可以设置守护进程来监控电源发生故障时备用电池的剩余电量。如果电池电量行将耗尽(长时间停电之后),那么监控进程会将该信号发往init进程,而后者则将其解读为快速、有序关闭系统的一个请求。

SIRQUIT

        当用户在键盘上键入退出字符(通常为Control-\)时,该信号发往前台进程组。默认情况下,该信号终止进程,并生成看我用于调试的核心转储文件。进程如果陷入无限循环,或者不在响应时,使用SIGQUIT信号就很合适,键入Control-\。再用gdb调试器加载刚才生成的核心转储文件,记者用backtrace命令来获取堆栈跟踪信息,就能发现正在执行的时程序的哪部分代码

SIGSEGV

        这一信号非常常见,当应用程序对内存的引用无效时。引起对内存无效的原因很多,可能是因为要引用的页不存在(例如,该页位于堆和栈之间的未映射区域),或者金额和那个试图更新制度内存(比如,程序文本段或者标记为制度的一块内存映射区域)中某一位置的内容,又或者企图在用户态(参见2.1节)去访问内核的部分内存。C语言中引发这些事件的往往是解引用的指针里包含了错误地址(例如未初始化的指针),或者传递了一个无效参数功函数调用。

SIGSTJFLT

        signa(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)所执行的跟踪系统调用功能。

SIGSTP

        这是 作业控制的停止信号,当用户在键盘上输入挂起字符(通常时Control +Z)时,将发送送给前台进程组,使其停止运行。第34章详细描述了进程组作业和作业控制,以及应在合适以及如何去处理该信号

SIGTTIN

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

SIGTTOU

        该信号的目的与SIGTTIN信号类似,但所针对的是后台作业的终端输出。在作业控制shell下运行时,如果对终端启用了TOSTOP(终端输出停止)选项(可能时通过 tty tostop命令),而某一后台进程组试图对终端进行write()操作(参见34.7.1节),那么终端驱动程序将向该进程组发送SIGTTOU信号。该进程默认将停止进程。

SIGUNUSED

        顾名思义,该信号没有使用。在Linux2.4及其后续版本中,该信号名字啊在很多架构中与SIGSYS信号同义,换言之,尽管信号名还保持向后兼容,但信号编号在这些架构中不再处于未使用状态。

SIGURG

        系统发送该信号给一个进程,表示套接字上存在带外(也称作紧急)数据(见61.13.1)。

SIGUSR1

        该信号和SIGUSR2信号供程序员自定义使用,内核绝不会为进程产生这些信号。进程可以使用这些信号来互相通知事件的发生,或是彼此同步。在早期的UNIX实现中,这是可供应用随意使用的仅有的两个信号。(实际上,进程间可以相互发送任何信号,但如果内核也为进程产生了同类信号,这两种情况可能混淆。)现代UNIX实现提供了许多实时信号,也可用于陈程序员自定义的目的(参见22.8节)。

SIGUSR2

        参见SIGUSR1信号的描述

SIGVTALRM

        调用setitimer()(参见23.1节)设置的虚拟定时器刚一到期,内核就会产生该信号,虚拟定时器记录的时进程在用户态所使用的CPU时间

SIGWINCH

        在小黄口环境中,当终端窗口尺寸发生变化时(如62.9所示,要么是由于用户手动昂调用调整了大小,要么是因为程序调用ioctl()对大小做了调整),回想前台进程组发送该信号,借助于为该信号安装的处理器承租,诸如vi和less之类的程序会在窗口尺寸调整后重新绘制输出。

SIGXCPU

        当进程的CPU时间超出对应的资源限制时(参见36.3节对RLTMIT_CPU的描述),将发送此信号给进程。

SIGXFSZ

        如果进程因试图增大文件(调用write()或truncate())而突破对进程文件大小的资源限制(参见36.3节对 RLIMIT_FSIZE的描述)时,那么将发送此新高给进程。

        表20-1总结了Linu下与信号相关的一系列信息。关于此表,请注意以下几点。

  • 信号编号列所示为不同架构下对信号的编号。除非另有说明,信号在所有架构中编号相同。信号在架构上的差异会在括号中予以说明,所设计的架构包括Sun SPARC、SPARC64(S)、HP/Compaq/Digital Alpha(A)、MIPS(M)和HP PA-RISC(P)。刺裂中的undef表示在所示架构中未定义。
  • SUSv3 列则表示SUSv3是否定义了该信号。
  • 默认列显示了信号的默认行为。term表示信号终止进程,core表示进程产生核心转储文件并推出,igore表示忽略该信号,stop表示信号停止了进程,cont表示信号回复了一个已停止的进程。

某些前面列出的信号并未见诸于表20-1,如SIGCLD(SIGCHLD信号的同义词)、SIGINFO(未使用)、SIGIOT(SIGABRT信号的同义词)、SIGLOST(未使用)和SIGUNUSED(在许多架构中是SIGSYS的同义词)。

 针对表20-1中某些信号的默认行为,需要注意以下几点。

  • 在Linux2.2中,信号SIGXCPU、SIGXFSZ、SIGSYS和SIGBUS的默认行为是终止进程,但不会产生核心转储文件。资内核2.4以后Linux实现满足了SUSv3的要求,这些信号不但会引发进程终止,也会产生核心转储文件。在其他几个UNIX实现中,对信号SIGXCPU和SIGXFSZ的处理方式于Linux2.2相同。
  • 在其他几个UNIX实现中,对SIGPWR信号的默认行为通常是jiangqi-忽略。
  • 几个UNIX实现(特别是BSD 衍生系统)默认情况下将忽略SIGIO信号
  • 虽然SIGEMT信号尚未获得任何标准的接纳,但却得到大多数UNIX实现的支持。然而,在其他实现中,该进程通常回导致进程终止并产生核心转储文件。
  • SUSv1将SIGURG信号的默认行文定义为终止进程,这也是一些较老的UNIX实现的默认做法。而SUSv2则采用了现行规范(将其忽略)。

20.3 改变信号处置:signal()

        UNIX系统提供了两种方法来改变信号处置:signal()和sigaction().本节描述signal()系统调用,时设置信号处置的原始API,所提供的接口比sigaction()简单。另一方面,sigaction()提供了signal()所不具备的功能。进一步而言,signal()的行为在不同的UNIX实现间存在差异(22.7节),这也意味着对可移植性有所追求的程序据不能使用此调用来建立信号处理器函数。故此sigaction()时间里信号处理器的首选API

signal()函数虽然记录在LINUX手册页的第2部分,但实际却被实现为一个基于sifaction()系统调用的glibc函数。

#include <signal.h>

void (*signal(int sig,coid(*handler)(int)))(int);
    Returns previous signal disposition on success,or SIG_ERR on error

        第一个参数sig,标识希望修改处置的信号编号,第二个参数handler,则标识信号抵达时所调用函数的地址。该函数无返回值(void),并接受一个整形参数。因此信号处理器函数一般具有以下形式:

void handler(int sig)
{
    //Code for the handler
}

20.4节将描述处理器函数中sig参数的目的。

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

void(*oldHandler)(int);
oldHandler = signal(SIGINT,newHandler);
if(oldHandler == SIG_ERR)
{
    perror("signal:");
    return -1;
}

/*Do something else here,During this time, if SIGINT is delivered,
newHandler will be used to handler the signal.*/
if(signal(SIGINT,oldHandler) == SIG_ERR)
{
    perror("signal:");
    return -1;
}

使用signal()将无法在不改变信号处置的同事,还能获取当前的信号处置,要想做到这一点,必须使用sigaction().

针对信号处理器函数指针做如下定义,将有助于理解signal()的原型:

typedef void (sighandler_t)(int);

//signal()原型可以改写成如下形式

sighanler_t signal(int sig,sighandler_t handler);

如果定义了_GNU_SOURCE特性测试宏,那么glibc将在<signal.h>t头文件中暴露非标准的sighandler_t 数据类型

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

SIG_DFL

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

SIG_IGN

        忽略该信号。如果信号转为此进程而生,那么内核会默默将其丢弃。进程设置从未直到层间曾经产生了该信号。

        调用signal()成功将返回先前的信号处置,有可能时先前安装好的处理器函数地址,也可能是SIG_DFL和SIG_IGN之一。如果调用失败,signal()将返回SIG_ERR。

20.4 信号处理器简介

        信号处理器程序(也成为信号捕捉器)是当指定信号传递给进程时将会调用的一个函数。

本节鸟叔了信号处理器的基本原理,21章将做详细介绍。

        调用信号处理器程序,可能回随时大端主程序流程:内核代表进程来调用处理器程序,当处理器返回时,主程序会在处理器打断的位置恢复执行。这一工作序列可用图20-1来加以说明。

图20-1信号到达并执行处理器程序

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

程序清单20-1:为SIGINT信号安装一个处理器程序

#include<signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

static void sigHandler(int sig)
{
    printf("ouch!\n");   //unsafe see section 21.1.2
}

int main(int argc,char *argv[])
{
    int j;
    if(signal(SIGINT,sigHandler) == SIG_ERR)
    {
        perror("signal:");
        return -1;
    }

    for(j=0; ; j++)
    {
        printf("%d\n",j);
    sleep(3);
    }
}

        程序清单20-1所示为一个简单的信号处理器函数,由主程序SIGINT信号而建立。当键入中断字符(通常为Control--C)时,中断驱动程序将产生该信号。处理器只是简单打印一条信息,随机返回。

        主程序会持续循环。每次迭代,程序都将递增计数器并将计数器值打印出来,后买你休眠几秒中。(为了按这种方式休眠,程序使用了sleep()函数,该函数回令调用者处于暂停状态,持续时间则由指定的秒数决定。该函数再23.4.1节中进行描述。)

        运行程序清单20-1中程序的结果如下:

         内核再调用信号处理器程序时,会将引发调用的信号编号作为一个整型参数传递给处理器函数。(程序清单20-1中处理器函数的sig参数)。如果信号处理器程序支部或一种类型的信号,那么这个参数几乎无用。日晚二如果安装相同的处理器来捕获不同类型的信号,那么就可以利用此参数来判定引发对处理器调用的是何种信号。

        程序清单20-2展示了这一思路,为SIGINT和SIGQUIT信号建立了同意处理器程序。(当键入推出终端字符时,通常为Control--\,终端驱动程序将产生SIGQUIT信号。)处理器程序代码通过检查sig参数来区分这两种信号并为每种信号提供不同措施。main()函数则使用pause()函数来阻塞进程,直到捕获信号.

如下shell会话日志演示了对该程序的使用:

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

程序清单20-2:为两个不同信号建立同一处理器函数

#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

static void sigHandler(int sig)
{
    static int count = 0;
    /*UNSAFE:This handler uses non-async-signal-safe functions
        printf() exit();  see section 21.1.2*/
    if(sig == SIGINT){
        count ++;
        printf("Caught SIGINT (%d)\n",count);
        return;  //resume executio  at point of interruption
    }
    //Must be SIGQUIT -print a message and terminate the process
    printf("Caught SIGQUIT -that's all folks!\n");
    exit(EXIT_SUCCESS);
}

int main(int argc,char *argv[])
{
    /*Establish same handler for SIGINT and SIGQUIT*/

    if(signal(SIGINT,sigHandler) == SIG_ERR)
    {
        perror("SIGINTsignal:");
        return -1;
    }

    if(signal(SIGQUIT,sigHandler) == SIG_ERR)
    {
        perror("SIGQUITsignal:");
        return -1;
    }

    for(;;)      //loop forever waiting for signals
    {
        pause();  //block until a signal is caught
    }
}

20.5 发送信号:kill()

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

#include <signal.h>

int kill(pid_t pid,int sig);
    
    Return 0 onsuccess,or -1 on error

        pid参数标识一个或多个目标进程,而sig则指定了要发送的信号,如何解释pid,要视以下4种情况而定。

  • 如果pid大于0,那么发送该信号给pid指定的进程。
  • 如果pid等于0,那么发送信号给与调用进程同组的每个进程,包括调用进程本身。(SUSv3声明,除去”一组未予明确的系统进程”之外,应将信号发送给同一进程组中的所有进程,且这一排除条件同样使用于余下的两种情况)
  • 如果pid小于-1,那么会向组ID等于该pid绝对值的进程组内所有下属进程发送信号。像一个进程组的所有进程发送信号在shell作业控制中有特殊用途(参见34.7节)。
  • 如果pid等于-1,那么信号的发送范围是:调用进程有权将信号发往的每个目标进程,除去init进程(进程ID为1)和调用进程自身。如果特权级进程发起这一调用,那么会发送信号给系统中的所有进程,上述两个进程除外。显而易见,又是也将这种信号发送方式为广播信号。(SUv3并未要求将调用进程排除在信号的接受范围之外,Linux此处所遵循的是BSD系统语义。)

        如果并无进程与指定的pid相匹配,那么kill()调用失败,同时将errno置为ESRCH("查无此进程")。

        进程要发送信号给另一进程,还需要适当的权限。其权限规则如下。

  • 特权级(CAP_KILL)进程可以向任何进策划发送信号。
  • 以root用户和组运行的einit进程(进程号为1),是一种特里,仅能接受已安装了处理器啊含糊的信号。这可以防止系统管理员意外杀死init进程--这一系统运作的基石。
  • 如图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版本之前所遵循的规则略有不同。)
  • SIGCONT信号需要特殊处理。无论对用户ID检查结果如何,非特权进程可以向同一会话中的任何其他进程发送这一信号。利用这一规则,运行作业控制的shell可以重启已停止的作业(进程组),即使作业进程已经修改了他们的用户ID。(亦即,使用9.7节所述系统调用来改变其凭据,进而成为特权级进程。)
  • 图20-2:非特权进程发送信号所需的权限

如果进程无权发送送信号给锁清秋的pid,那么kill()调用将失败,且将errno置为EPERM.若pid所指为一系列进程(即pid是负值时),只要可以向其中之一发送信号,则kill()调用成功。

20.6检查进程的存在

        kill()系统调用还有另一重功用。若将参数sig指定为0(即所谓空心好),则无信号发送。相反,kill()仅会去执行错误检查,查看是否可以向目标进程发送信号。从另一角度来看,这意味着,可以使用空i新年好来检测具有特定进程ID的进程是否存在。若发送空信号失败,且errno为ESRCH,则表明目标进程不存在。如果调用失败,且errno为EPERM(表示进程存在,但无权向目标进程发送信号)或调用成功(有权向进程发送信号),那么就表示进程存在。

        验证一个特定进程ID的存在并不能保证特定程序仍在运行。因为内核会随着进程的生灭而循环使用ID。而一段时间之后,统一进程ID所指恐怕时另一进程了。此外,特定进程IDA可能存在,但是是一个僵尸进程(进程已死,但父进程尚未执行wait来获取其终止状态,如26.2节所述)。

        还可以使用其他技术来检查某一特定进程是否正在运行,其中包括如下技术。

  • wait系统调用:第26章将描述这些调用。这些调用仅用于监控调用者的子进程。
  • 信号量和排他文件锁:如果进程持续持有某一信号量或文件锁,并且一直处于被监控状态,那么如能获取到信号量或锁时,即表明改进昵称已经终止。55章将描述文件锁。
  • 诸如管道和FIFO之类的IPC通道:可对监控目标进程进行设置,令其在自身生命周期内持有对通道进行写操作的到开文件描述符。同时,令监控进程持有针对通道进行读操作的打开文件描述符,且当写入端关闭时(遭遇文件结束符),即可获知监控进程已经终止。监控进程对此情况的判定,即可借助于对文件自身描述符的读取,也可采用第63章所述的描述符监控之一。
  • /proc/PID接口:例如,如果进程ID为12345的进程存在,那么目录/proc/12345将存在,可以发起诸如stat之类的调用来进行检查。

        除去最后一项之外,循环使用进程ID不会影响上述所有技术。

        程序清单20-3展示了kill()的用法,该程序接受两个命令行参数,分别为信号编号和进程ID,并使用kill()将该信号发送给指定进程。如果指定了信号0(空信号),那么程序将报告目的进程是否存在。

20.7 发送信号的其他方式:raise()和killpg()

        有时,进程需要向自身发送信号(34.7.3节就有词一例)。raise()函数就执行了这一函数。

#include <signal.h>
int raise(int sig);
        Return 0 on succcess, or nozero on error

在单线程程序中,调用raise()相当于对kill()的如下调用:

kill(getpid(),sig);

支持线程的系统会将raise(sig)实现为:

pthread_kill(pthread_self(),sig)

    33.2.3节描述了pthread_kill(函数),但目前仅需了解一嗲就已足够,该信号意味着将信号传递给调用raise()的特定线程,相比之下,kill(getpid(),sig)调用会发送一个信号给调用进程,并可将该信号传递给进程的任一线程。

        raise()函数起源于C89.C语言标准不包含诸如进程ID之类的操作系统细节,raise(0函数之所以的一定义,是因为该函数不需要引用进程ID.

但是用raise()或者kill()向自身发送信号时,信号将立即传递(即,在raise()返回调用者之前)。

注意,raise()出错将返回非0值(不一定为-1).调用raise()唯一可能发生的错误为EINVAL,即sig无效。因此,在任何指定了某一SIGxxx常量的位置,都为检查该函数的返回状态。

程序清单20-3:使用kill()系统调用

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>

int main(int argc,char *argv[])
{
    int s,sig;
    if(argc !=3 || strcmp(argv[1],"--help") == 0)
    {
        printf("%s signum pid\n",argv[0]);
    }
    sig = atoi(argv[2]);

    s = kill(atoi(argv[1]),sig);
    if(sig !=0)
    {
        if(s==-1)
        {
            perror("kill:");
            return -1;
        }
    }else{
        if(s == 0){
            printf("Process exists and we can send it a signal\n");
        }else{
            if(errno == EPERM)
            {
                printf("Process exits,but wo don't have permission to send it a signal\n");
            }else if(errno == ESRCH)
            {
                printf("process does not exists\n");
            }else{
                perror("if exits kill:");
                return -1;
            }
        }
    }
    exit(EXIT_SUCCESS);
}

         killpg()调用相当于对kill()的如下调用:

        kill(-pgrp,sig);

        如果指定pgrp的值为0,那么会向调用者所属进程组的所有进程发送信号。SUSv3对此未作规范,但多数UNIX实现对该情况的处理方式相同。

        20.8 显示信号描述

        每个信号都以一串与之相关的可打印说明。这些位于数组sys_siglist中。例如可以用sys_siglist[SIGPIPE]来获取对SIGPIPE信号(管道断开)的描述。然而,较之于直接引用sys_siglist数组,还是推荐使用strsignal()函数

#define _BSD_SOURCE
#include <signal.h>

extern const char * const sys_siglist[];

#define _GNU_SOURCE
#include <string.h>

char *strsignal(int sig);
        Returns pointer to signal description string

        strsignal()函数对sig参数进行边界检查,然后返回一枚指针,指向对该信号的可打印描述字符串,或者时当秦昊编号无效按时指向错误字符串。(在其他一些UNIX实现中,strsignal()函数会对sig无效时返回空值。)

        除去边界检查之外,strsignal()函数较之于直接引用sys_siglist数组的优势时对本地(locale)设置敏感(10.4节),随意显示信号描述时会使用本地语言。

        程序清单20-4所示为使用strsignal()的例子之一。

        psignal()函数(在标准错误设备上)所示为msg参数所给定的字符串,后面跟有一个冒号,随后是对应于sig的信号描述。和strsignal()一样,psignal()函数也对本地设置敏感。

#include <signal.h>

    void psignal(int sig,const char *msg);

20.9 信号集

        许多信号相关的系统调用都需要能表示一组不同的信号。例如,sigaction()和sigprocmask()允许程序指定一组将由进程阻塞的信号,而sigpending()则返回一组目前正在等待送达给一进程的信号。

        多个信号可以用一个称之为信号集的数据结构来标识,其系统数据类型为sigset_t.。

像大多数UNIX实现中一样,sigset_t数据类型在Linux中是一个位掩码。然而,SUSv3对此并无要求。使用其他一些数据类型的数据结构来表述信号集也是有可能的。SUSv3仅要求可对sigset_t类型赋值即可。因此必须使用某些标量类型(比如一个整数)或者一个C语言结构(也许包含了一个整型数组)来实现该类型。

        sigemptyset()初始化一个未包含一个任何成员的信号集。sigfillset()则初始化一个信号集,使其包含所有信号(包括所有实时信号)。

#include <signal.h>

int sigemptyset(sigset_t *set);

int sigfillset(sigset_t *set);
                Both return 0 on sucess,or -1 on error

        必须使用sigemptyset()或者sigfillset()来初始化信号集这是因为C语言不会对自动变量进行初始化,并且借助于静态变量初始化为0的机制表示空信号集的做法在可移植性上存在问题,因为有可能使用位掩码之外的结构来实现信号集。(处于同一原因,为将信号集标记为空而是用memset(3)函数将其内容清零的做法也不正确。)

        信号集初始化后,可以分别使用sigaddset()和sigdelset()函数向一个集合中添加或者移除单个信号。

#include <signal.h>

int sigaddset(sigset_t *set,int sig);

int sigdelset(sigset_t *set,int sig);

        Both return 0 on sucess,or -1 on error

sigismember()函数用来测试信号sig是否是信号集set的成员。

#include <signal.h>
int sigismember(const sigset_t,int sig);
    Return 1 if sig is a member of set,otherwise 0

如果sig是set的一个成员,那么sigismember()函数将返回1(true),否则返回0(false).

GNU C 库还实现了3个非标准函数,是对上述信号集标准函数的补充。

        

#deifine _GNU_SOURCE
#include <signal.h>

int sigandset(sigset_t *dest,sigset_t  *left,sigset_t *right);
int sigorset(sigset_t *dest,sigset_t  *left,sigset_t *right);
        Both return 0 on success,or -1 on error

int sigisempty(const sigset_ set);
        Return 1 if sig is empty,otherwise 0

这些函数执行了如下任务。

  • sigandset()将left集和right集的交集置于dest集
  • sigorset()将left集和right集的并集置于dest集
  • 若set集未包含信号,则sigisempty()返回true.

示例程序

        程序清单20-4所示使用本节介绍的函数来表四的函数,共本书后续各程序调用。第一个函数printSigset()显示了执行信号集的成员信号。该函数使用了定义于<signal.h>文件中的NSIG常量,其值等于信号最大编号加1.当获取信号集成员是。会在测试所有信号编号的循环中将该值作为循环上限。

      虽然SUSv3并未定义NSIG,但是大多数UNIX实现都支持这一常量。只不过,要想使其可见,可能需要使用特定于实现的编译器选项。例如,在linux中,就必须定义如下功能的测试宏之一:BSG_SOURCE、_SVID_SOURCE和_GNU_SOURCE

        利用printSigset()函数,printSigMask()和printPendingSigs()函数分别用于显示进程的掩码和当前处于等待状态的信号集。这两个调用分别使用了sigprocmask()和sigpending()系统调用。

程序清单20-4:显示信号集的函数

#define _GNU_SOURCE
#include <string.h>
#include <signal.h>
#include <string.h>
//#include "signal_functions.h"   //declares functions defined here

/*NOTE:All of the following functions employ fprintf(),which is not 
async-signal-safe(see Section 21.1.2)As such, these functions are 
also not asyn-signal(i.e beware of indiscriminately calling them from 
signal handles).*/

void printSigset(FILE *of,const char *prefix,const sigset_t *sigset)
{
    //print list of signals within a signal set
    int sig, cnt;
    cnt = 0;
    for(sig=1;sig<NSIG;sig++){
        if(sigismember(sigset,sig)){
            cnt++;
            fprintf(of,"%s%d(%s)\n",prefix,sig,strsignal(sig));
        }
    }
    if(cnt == 0)
    {
        fprintf(of,"%s<empty signal set>\n",prefix);    
    }
}

int printSigMask(FILE *of,const char *msg)
{
    //print mask of blocked signals for this process
    sigset_t currMask;
    if(msg !=NULL)
    {
        fprintf(of,"%s",msg);
    }

    if(sigprocmask(SIG_BLOCK,NULL,&currMask) == -1)
        return -1;
    printSigset(of,"\t\t",&currMask);

    return 0;
}
int printPendingSigs(FILE *of,const char *msg)
{
    //Print signals currently pending for this process
    sigset_t pendingSigs;
    if(msg != NULL)
    {
        fprintf(of,"%s",msg);
    }
    if(sigpending(&pendingSigs) == -1)
        return -1;
    
    printSigset(of,"\t\t",&pendingSigs);

    return 0;
}

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

        内核会为每个进程维护一个信号掩码,即一组信号,并将阻塞其针对该进程的传递。如果将遭阻塞的信号发送给某进程,那么对该信号的传递将延后,直至从进程信号掩码中移除该信号,从而解除阻塞为止。(由33.2.1节可知,信号掩码实际属于线程属性,在多线程进程中,每个线程都可使用pthread_sigmask()函数来独立监察和修改器信号掩码。)

向信号掩码中添加一个信号,有如下集中方式。

  • 当调用信号处理器程序时,可将引发调用的信号自动添加到信号掩码中。是否发生这一情况,要视sigaction()函数在安装信号处理器程序时所使用的标志而定。
  • 使用sigaction()函数建立信号处理器程序时。可以指定一组额外信号,当调用该处理器程序时会将其阻塞。
  • 使用sigprocmask()系统调用,随时可以显式向信号掩码中添加或移除信号。

对前两种情况的讨论将推迟到20.13节对sigaction()函数介绍之后,现在先来讨论sigprocmask()函数。

#include <signal.h>
int sigprocmask(int how,const sigset_t *set,sigset_t *oldset);
        Return 0 onsucccess,or -1 on error

        使用sigprocmask()函数即可以修改进程的信号掩码,又可以获取现有掩码,或者两重功效兼具,how参数指定了sigprocmask()函数想给掩码带来的变化。

SIG_BLOCK

       将set指向信号集内的指定信号添加到信号掩码中换言之,将信号掩码设置为其当前值和set的并集。

SIG_UNBLOCK

        将set指向信号集中的信号从信号掩码中移除。即使要解除阻塞的信号当前并未处于阻塞装袋也不会返回错误

SIG_SETMASK

        将set指向的信号集赋给信号掩码。

        上述各种情况下oldset参数不为空,则其指向一个sigset_t结构缓冲区,用于返回之前的信号掩码。

        如果想获取信号掩码而由对其不做改动,那么可将set参数指定为空,这是将忽略how参数。要想暂时阻止信号的传递,可以使用程序清单20-5中一系列调用来阻塞信号,然后再将信号掩码重置为先前的状态以解除对信号的锁定。

程序清单20-5 :暂时阻塞信号传递

sigset_t blockSet,prevMask;

/*Initialize a signal set to contain SIINT*/

sigemptyset(&blockSet);
sigaddset(&blockSet,SIGINT);
/*block SIGINT,save previous signal mask*/

if(sigpromask(SIG_BLOCK,&blockSet,&prevMask) == -1)
{
    perror("sigpromask1:");
    return -1;
}

/*...Code that should no be interrupted by SIGINT...*/

/*Restore previous signal mask,unblocking SIGINT*/

if(sigprocmask(SIG_SETMASK,&preMask,NULL) == -1)
{
    perror("sigprocmask2:");
    return -1;
}

SUSv3规定如果有任何等待信号因对sigprocmask()调用而解除了锁定。那么再次调用返回前至少会传递一个信号。换言之,如果解除了对某个等待信号的锁定,那么会立刻将该信号传递给进程。

        系统将忽略试图阻塞SIGKILL和SIGSTOP信号的请求。如果试图阻塞这些信号,sigprocmask()函数既不会予以关注,也不会产生错误。这意味着,可以使用如下代码来阻塞SIGKILL和SIGSTOP之外的所有信号:

sigfillset(&blockSet);
if(sigprocmask(SIG_BLOCK,&blockSet,NULL) == -1)
{
    perror("sigprocmask:");
    return -1;
}

20.11 处于等待状态的信号

        如果某进程接受了一个该进程正在阻塞的信号,那么会将该信号添加到进程的等待信号集中。当(且如果) 之后解除了对该信号的锁定时,会随之将信号传递传递给此进程。为了确定进程中处于等待的是哪些信号,可以使用sigpending()。

#include <signal.h>
int sigpending(sigset_t *set);
        Return 0 on success,or -1 on error

sigpending()系统调用为调用进程返回处于等待状态的信号集,并将其置于set所指向sigset_t结构中。随后可以使用20.9节描述的sigismember()函数来检查set。

        如果修改了对等待信号的处置,那么后解除对信号的锁定时,将根据新的处置来处理信号。这项技术虽然不经常使用,但还是存在一个应用场景,即将对信号的处置置为SIN_IGN,或者置为SIG_DFL(如果信号的默认行为时忽略),从而阻止传递处于等待咋混太的信号。因此,会将信号从进程的等待信号集中删除,从而不传递该信号。

20.12 不对信号进行排队处理

         等待信号集只是一个掩码仅表明一个信号是否发生。而未表明发生的次数。换言之,如果同一信号在阻塞状态下产生多次,那么会将该信号记录在等待信号集中,并在稍后仅传递一次。(标准信号和实时信号之间的差异在于,如22.8节所述,对实时金浩进行了排队处理。)

        程序清单20-6和程序清单20-7显示了两个程序,可用于观察未做排队处理的信号。清单20-6的程序可以接受多达4个命令行参数,如下所示:

$ ./sig_sender PID num-sigs sig-num [sig-num-2]

第一个参数时程序发送信号的目标进程ID。第二个参数则指定发送给目标进程的信号数量。第三个参数指定发往目标进程的信号编号。如果还提供了一个信号编号作为第四个参数,那么当程序发送完之前参数所指定的信号之后,将发送该信号的一个实例。在如下的shell回话示例中,就是用了最后一个参数向目标进程发送一个SIGINT信号,发送该信号的目的稍后揭晓

程序清单20-6:发送多个信号  sig_sender.c

#include  <signal.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
int main(int argc,char *argv[])
{
    int numSigs,sig,j;
    pid_t pid;

    if(argc <4 || strcmp(argv[1],"--help") == 0)
    {
        printf("%s pid num-sigs sig-num [sig-num2]\n",argv[0]);
    }
    pid = atoi(argv[1]);

    numSigs = atoi(argv[2]);
    sig = atoi(argv[3]);

    /*send signals to receiver*/
    printf("%s: sending signal %d to proess %ld %d timen",
            argv[0],sig,(long)pid,numSigs);

    for(j=0;j<numSigs;j++)
    {
        if(kill(pid,sig) == -1)
        {
            perror("kill1:");
            return -1;
        }
    }
    /*if a fourtn command-line argument was specified, send that signal*/

    if(argc >4)
    {
        if(kill(pid,atoi(argv[4])) == -1)
        {
            perror("kill2");
            return -1;
        }
    }
    printf("%s:exiting\n",argv[0]);
    exit(EXIT_SUCCESS);

}

程序请打20-7中程序装备设计为去捕获20-6程序所发送的信号并汇总其统计数据。该程序执行了一下步骤。

  • 该程序建立了单个处理器来捕获所有信号。(捕获SIGKILL和SIGSTOP信号是不可能的,不过将忽略在尝试为这些信号建立处理器时所发生的错误。)对于大多数类型的信号,处理器程序只是简单的使用一个数组来对信号计数。如果收到的信号为SIGINT,那么处理器程序将对标志(gotSigint)置位,从而使程序退出主循环(下面所描述的while循环)。(至于volatitle修饰符以及声明gotSigint变量的sig_auto_t数据类型,将在21.1.3节解释其用途。)
  • 如果提供有一个命令和你参数给程序,那么程序对所有信号的阻塞秒数将由该参数指定,并在技术阻塞之前会显示待处理的信号集,从而使用户在进程执行下面的步骤向其发送信号。
  • 程序执行while循环以消耗CPU时间,直至将gotSigint标志置位。(20.14节和22.9节描述了pause()和sigsuspend()用法,二者在等待信号到来期间对CPU的使用方式都颇为高效。)
  • 退出while循环后,程序显示对所有接收信号的技术。

首先使用这两个程序来展示的是遭阻塞的信号无论产生了多少次,仅会传递一次。这里为接收者指定了一个睡眠间隔,并在醒来之前发送所有信号。

      发送程序的命令行参数指定了SIGUSR1和SIGINT信号,其在Linux/x86的编号分别为10和2.

从以上输出可知,即使一个信号发送了一百万次,但仅会传第一次个给接收者。

即使进程没有阻塞信号,其所受到的信号也可能比发送给它的要少的多,如果信号发送速度如此之快,以至于在内核考虑将执行权调度给接受线程之前,这些信号就已经到达,这事就会发生上述情况,从而导致多次发送的信号在进程等待信号集中只记录了一次。如果不带任何命令行参数来执行程序清单20-7程序(因此进程没有阻塞程序,也没有睡眠),那么将看到如下情况。

 在所发送的一百万次信号中接收进程捕获到34590次。(捕获信号的精确数目妹妹会不同,这取决于内核调度算法变化莫测的决策结果。)之所以如此,原因在于,发送程序会在每次获得调度而运行时发送多个信号给接收者。然而,当接收进程得以运行时,传递来的信号只有一个,因为只会讲这些信号的一个标记为等待状态。

程序清单20-7:捕获信号并对其计数

#define _GNU_SOURCE
#include <signal.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>

static int sigCnt[nsig]; /*Counts delivers of signal*/
static volatile sig_atomic_t gotSigint = 0;

static void handler(int sig)
{
    if(sig == SIGINT)
        gotSigint = 1;
    else
        sigCnt[sig]++;
}

int main(int argc,char *argv[])
{
    int n,numSecs;
    sigset_t pendingMask,blockingMask,emptyMask;

    printf("%s:PID is %ld\n",argv[0],(long)getpid());

    for(n=1;n<NSIG;n++) /*same handler for all signals*/
        (void)signal(n,handler); //ignore errors

    /*if a sleep time was specified,temporerily block all signals
    sleep (while another process sends us signals),and then
    display the mask of pending signals and unblock all signals*/
    if(argc >1)
    {
        numSecs = atoi(argv[1]);
        sigfillset(&blockingMask);
        if(sigpromask(SIG_SETMASK,&blockingMask,NULL) == -1)
        {
            perror("sigprocmask:");
            return -1;
        }

        printf("%s:sleeping for %d seconds\n",argv[0],numSecs);
        sleep(numSecs);

        if(sigpending(&pendingMask) == -1)
        {
            perror("sigpending:");
            return -1;
        }

        printf("%s:pending signals are:\n",argv[0]);
        printSet(stdout,"\t\t",&pendingMask);

        sigemptyset(&emptyMask);  //unblock all signals
        if(sigprocmask(SIG_SETMASK,&emptyMask,NULL) == -1)
        {
            perror("sigprocmask");
            return -1;
        }

        while(!gotSigint)  //loop until ISGINT caught
            continue;
        for(n = 1;n<NSIG;n++)
        {
            if(sigCnt[n] !=0)
            {
                printf("%s:signal %d caught %d time%s\n",argv[0],
                        n,sigCnt[n],(sigCnt[n] == -1)?"":"s");
            }
        }

        exit(EXIT_SUCCESS);
    }
}

20.13 改变信号处置:signalaction()

        除去signal()之外,sigaction()系统调用是设置信号处置的另一选择。虽然sigaction()的用法比signal()更为复杂,但作为回报,也更具灵活性。尤其是,sigaction()允许在获取信号处置的同时无需将其改变,并且,还可设置各种属性对调用信号处理器程序时的行为施以更加精准的控制。此外如22.7所示,,在建立信号处理器程序时,sigaction()较之signal()函数可移植性更佳。

#include <signal.h>
int sigaction(int sig,const struct sigaction *act,struct sigaction *oldact);
        Returns 0 on success, or -1 on error

        sig参数表示想要获取或改变的信号编号。该参数可以是除去SIGKILL和SIGSTOP之外的任何信号。

        act参数是一枚指针,指向描述信号新处置的数据结构。如果进队信号的现有处置感兴趣,那么可以将参数指定为NULL.oldact参数是指向同一结构类型的指针,用来返回之前信号处置相关的信息。如果无意获取此类信息,那么可将该参数指定为NULL.act和oldact所指向的结构类型如下所示:

struct sigaction{
    void (*sa_handler)(int);  //Address of handler
    sigset_t sa_mask;         //signals blocked during handler invocation
    int sa_flags;            //flags controlling handler invocation
    void (*sa_restorer)(void) //not for application use
};

sigaction 结构实际要比此处所展示的更为复杂,更多细节请参见21.4节。

sa_handler字段对应于signal()的handler参数。其所致定的值为信号处理器函数的地址。抑或是常量SIG_IGN、SIG_DFL之一。仅当sa_handler是信号处理程序的地址时,亦即sa_handlerd的取值在SIG_IGN和SIG_DFL之外。才会对sa_mask和sa_flags字段加以处理。余下的字段sa_restorer,则不适用于应用程序。

sa_restorer字段仅供内部使用,用以确保当信号处理器程序完成后,会去调用专用的sigreturn()系统调用,借此来会父进程的执行上下文,以便于进程从信号处理器终端的位置继续执行。

        sa_mask字段定义了一组信号,在调用由sa_handler所定义的处理器程序时将阻塞该信号。当调用信号处理器程序时,会在调用信号处理器之前,将改组信号中当前位处于进=进程掩码之列的任何信号自动添加到进程掩码中。这些信号将保留在进程掩码中,直至信号处理器函数返回,届时将自动删除这些信号。利用sa_mask字段可指定一组信号,不允许他们终端此处理器的执行。此外,引发对处理器程序调用的信号将自动添加到进程信号掩码中。这意味着,当正在执行处理器程序时,如果同一个信号第二次到达,信号处理器将不会递归中断自己。由于不会对早阻塞的信号进行排队处理,如果在处理器程序执行过程中重复产生这些信号中的任何信号,(稍后)对信号的传递是一次性的。

        sa_flags字段是一个位掩码,指定用于控制信号处理过程中的各种选项。该字段包含的位如下(可以相或(|)).

  SA_NOCLDSTOP

        若sig位SIGCHLD信号,则当因接受一信号而停止或恢复某一子进程时,将不会产生此信号。参见26.3.2节

SA_NOCLDWAIT

        (始于Linux2.6)若sig为SIGCHLD信号,则当子进程终止时不会将其转化为僵尸。更多细节参见26.3.3节

SA_NODEFER

        捕获该信号时不会再执行处理器程序时将该信号自动添加到进程掩码中。SA_NOMASK历史上曾是SA_NODEFER的代名词。之所以建议使用后者,是因为SUSv3将其纳入规范。

SA_ONSTACK

        针对此信号调用处理器函数时,使用了sigaltstack()安装的备选栈,参见21.3节。

SA_RESETHAND

        针对此信号调用处理器函数时,会在调用处理器函数之前将信号处置重置为默认值(即SIG_DFL)(默认情况下,信号处理器函数保持建立状态,直至进一步调用sigaction()将其显示解除。)SA_ONESHOT历史上曾是SA_RESETHAND所谓代名词,之所以建议使用后者是因为SUSv3将其纳入规范。

SA_RESTART

        自动重启有信号处理器程序中断的系统调用。参见21.5节。

SA_SIGINFO

        调用信号处理器程序时携带了额外参数,其中提供了关于信号的深入信息,详见21.4节

20.14 等待信号:pause()

        调用pause()将暂停进程的执行,直至信号处理器函数终端该调用为止(或者直至一个未处理信号终止进程为止)。

#include <unistd.h>
int pause(void);

        Always Returns -1 whith errno set to EINTR

处理信号时,pause()遭到终端,并总是返回-1,并将errno置为EINTER。21.5节描述了关于EINTR错误时的更多信息。

21.5 总结

        信号时发生某种时间的通知机制,可以由内核、另一进程或者进程自身发送给进程。存在一系列的标准信号类型,每一种都有唯一的编号和目的。

        信号的传递通常是异步行为,这意味着信号中断进程执行的为止是不可预测的。有时(比如,硬件产生的信号),信号也可以同步传递,这意味着在程序执行的某一点可以预期并重现信号的传递。

默认情况下,要么忽略信号,要么终止进程(生成或不生成核心转储文件),要么停止一个正在运行的进程,要么重启一个已经停止的进程。特定的默认行为取决于信号类型。此外,程序可以使用signal()或者sigaction()来显式忽略一个信号,或者建立一个由程序员自定义的信号处理器程序,以供信号到达时调用。处于可移植性考虑,最好使用sigaction()来建立信号处理程序。

        一个(具有适当权限的)进程可以使用kill()向另一进程发送信号,发送空信号(0)是判断特定进程ID是否在用的方式之一。

        每个进程都有一个信号掩码,代表当前传递遭到阻塞的一组信号,使用sigprocmask()可以从信号掩码中添加或者移除信号。

        如果接收的信号当前遇到阻塞,那么该信号将保持等地啊状态,直至接触对其阻塞。紫铜不会对标准信号进行排队处理,也就是说,将信号标记为等待状态(以及后续的传递)只会发生一次。进程能够使用sigpending()系统调用来获取等待信号集(用以描述多个不同信号的数据结构)。

       与signal()相比,sigaction()系统调用在设置信号处置方便提供了更多的控制,且具灵活性。首先,可以指定一组调用处理器函数时将阻塞的额外信号。此外,可以使用各种标志来控制调用处理器时所发生的行为。例如启用某些标志即可选择旧有的不可靠的信号语义(不阻塞引发处理器调用的信号,在调用信号处理器之前就将信号处置为默认值)。

        借助于pause(),进程可暂停执行,直至信号送达为止。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值