第22章 信号:高级特性

本章议题如下:

  • 核心转储文件。
  • 与信号的传递、处置以及处理相关的特殊情况。
  • 信号的同步产生和异步产生。
  • 信号的传递实际及传递顺序。
  • 信号处理器函数对系统调用的中断,以及如何自动重启遭到中断的系统调用。
  • 实时信号
  • 用sigsusspend()来设置信号掩码并等待信号到达。
  • 用sigwaitinfo()(和sigtimedwait())同步等待信号到达。
  • 用signalfd()从一个文件章接收信号。
  • 较老的BSD版信号API和System V版信号API

22.1 核心转储文件

        特定信号会因大津城创建一个核心转储文件并终止运行。所谓的核心转储时内含进程终止时内存映像的一个文件。(术语core源于一种老迈的内存技术。)将该内存映像加载到调试器中,即可查明信号到达时程序代码和数据的状态

        引发程序生成核心转储文件的方式之一是键入退出符(通常为 Control-\),从而生成SIGQUIT信号下面是自己测试的,按照书中给出方式,并没有发现core文件

下面是书中的测试:

本例中,当检测出子进程(运行sleep命令的进程)为SIGUIT所杀,并生成核心转储文件时,shell会显示“Quit (core dump)”消息。

        核心转储文件创建于进程的工作摩鹿钟,名为core。这是核心转储文件的默认文职和名称。稍后,系那个接受如何改变这些默认值。

借助于许多实现所提供的工具(例如FreeBSD和Solaris中的gcore),可获取某一正在运行进程的核心转储文件。Linux系统也有类似功能,使用gdb去连接(attach)一个正在运行的进程。然后运行gcore命令。

不产生核心转储文件的情况

        一些情况不会产生核心转储文件。

  • 进程对核心转储文件没有写权限。造成这种爱你概况的原因有进程对要创建核心转储文件的所在目录可能没有写权限,或者是因为存在同名(且不可泄,亦或是非常规类型,例如目录或符号链接)的文件。
  • 存在一个同名,可写的普通文件,胆汁性该文件的(硬)连接数朝贡国一个。
  • 将要创建核心转储文件的所在目录并不存在。
  • 把进程“核心转储文件大小”这一资源限制置为0,36.3节就将这一限制(RLIMIT_CORE)进行详细讨论。上例就使用了ulinit(C Shell 中为limit命令)来取笑对核心转储文件大小的任何限制。
  • 将进程“可创建的文件大小”这一资源限制设置为0。36.3节将描述这一限制(RLIMIT_FSIZE)。
  • 对进程正在执行的二进制可执行文件没有读权限。这样就放置了用户借助于核心转储文件来获取本无法获取的程序代码。
  • 以制度的方式华仔当前工作目录所在的文件系统,或者文件系统空间已满,又或者i-node资源耗尽。还有一种情况,及3用户已经达到其在该文件系统上的配额限制。
  • Set-user-ID(Set-group-ID)程序在份文件属主(属组)执行时,不会产生核心转储文件。这可以防止恶意用户将一个安全的内存转储出来,在针对诸如密码之类的敏感信息进行刺探。

借助于Linux专有系统调用prctl()的PR_SET_DUMPABLE操作,可以为进程设置dumpable标志。当非文件属主(或属组)运行set-user-ID(set-group-ID)程序时,如设置该标志即可生成核心转储文件。

        始于内核版本2.6.23,利用Linux特有的/proc/PID/coredump_filter,可以对写入核心转储文件的内存映射类型是一进程及控制及。该文件中的值是一个4位掩码,分别对应于4中类型的内存映射:私有匿名映射,私有文件映射、共享匿名映射以及共享文件映射。文件默认提供了传统的Linux行为:进队私有匿名映射和共享匿名映射进行转储。详见core(5)手册页。

为核心转储文件命名:/proc/sys/kernel/core_pattern

        从Linux版本2.6开始,可以根据Linux特有的/proc/sys/kernel/core_pattern文件所包含的格式化字符串来对系统上生成的所有核心转储文件的命名。默认情况下,该文件所含字符串为core。特权级用户可以将该文件内容定义为包含表22-1所列的任一格式的说明符,待实际命名时再以表中右列所显示相应值加以替换。此外允许字符串中包含斜线(/)。换言之,处在控制范围之内的,不仅包含核心文件的抿成,还饱和就恶心文件的所在(绝对或相对)目录。替换所有格式说明符后,由此生成的路径名字符串长度多至可达128个字符,超出部分将截断。

        Linux从内核版本2.6.19开始支持core_payyern文件的另一种说法。如果该文件包含一个管道符(|)为首的字符串,那么会将该文件的剩余字符串视为一个程序,其可选参数可包含表22-1所示的%说明符--当进程转储核心文件时,将执行该程序。并且会将核心转储至该程序的标准输入,而非一个文件。

其它一些UNIX实现也提供了类似于core_pattern的机制。例如,在BSD一派中,会将程序追加至文件名尾部,形如core.procname.SOlaris提供了一个工具(coreadm),允许由用户来选择核心转储文件的名称和存放目录。

表22-1:服务于proc/sys/kernel/core_pattern的文件说明符

22.2 传递、处置以及处理的特殊情况

        本节讨论针对特定信号,适用于其传递、处置以及处理方面的特殊规则。

SIGKILL和SIGSTOP

        SIGKILL信号的默认行为是种植一个进程,SIGSTOP信号的默认行为是停止一个进程,二者的默认行为均无法改变。当试图用siganl()和sigaction()来改变对这些信号的处置时,将总是返回错误。同样也不能将这两个信号阻塞。这时一个深思熟虑的设计决定。不允许修改这些信号的默认行为,这也意味着总是可以利用这些信号来杀死或者停止一个失控进程。

SIGCONT和停止信号

        如前所述,可使用SIGCONT信号来使某些(因接收SIGSTOP,SIGSTP、SIGTTIN和SIGTTOU信号而)处于停止状态的进程得以继续运行。由于这些信号 具有独特目的,所以在某些情况下内核对他们的处理方式将有别于其他信号。

        如果一个进程处于停止状态,那么一个SIGCONT信号的到来总是会促使其恢复运行,即使该进程正在阻塞挥着忽略SIGCONT信号,该特性之所以必要,是因为如果要回复这些处于停滞状态的进程,除此之外别无他法。(如果处于停滞状态的进程证字啊阻塞SIGCONT信号,并且已经为SIGCONT信号建立了处理器寒素,那么在进程恢复运行后,只有当取消了对SIGCONT的阻塞后,进程才会去调用响应的处理器函数。)

如果有任一其他信号发送给了一个已经停止的进程,那么进程收到SIGCONT信号而恢复运行之前,信号实际上并未传递。SIGKILL信号则属于例外,因为该信号总是会杀死进程,即使进程目前处于停止状态。

        当进程收到SIGCONT信号时,会将处于等待的停止信号丢弃(即进程根本不知道这些信号)。相反,如果任何停止信号传递给了进程,那么进程将自动丢弃任何处于等待状态的SIGCONT信号。之所以采取这些步骤,意在防止之前发送的一个停止信号会在随后撤销SIGCONT信号的行为,反之亦然。

由中断产生的信号若已被忽略,则不应改变其信号处置

果程序在执行时发现,已将对中断产生的信号处置置为了SIG_IGN(忽略),那么进程通常不应试图去改变信号处置。这非系统的硬性规定,而是编写应用程序时所应遵循的惯例。与之相关的信号有:SIGHUP、SIGQUIT、SIGINT、SIGTTIN、SIGTTOUT和SIGTSTP。

22.3 可中断和不可中断的进程睡眠状态

        前文指出,SIGKILL和SIGSTOP信号的作用是立竿见影的。遂于这一论断,自处要加入一条限制。内阁经常需要令进程进入休眠,而休眠状态又分为两种。

  • TASK_INTERRUPTIBLE:进程正在等待某一事件。例如,正在等待终端输入,等待数据写入当前的空管道,或者等待System V信号量值的增加。进程在该状态下所耗费的时间可长可短。如果为这种状态下的进程产生一个信号,那么操作将中断,而传递来的信号将唤醒进程。ps(1)命令在显示处于TASK_INTERRUPTIBLE状态的进程时,会将其STAT(进程状态)字段标记为字母S。
  • TASK_UNINTERRUPTIBLE:进程正在等待某些特定类型的事件,比如磁盘I/O的完成。如果为这种状态下的进程产生一个信号,那么在进程拜托这种状态之前,系统将不会把信号传递给进程。PS(1)命令在显示处于TASK_UNINTERRUPTIBLE状态的进程时,会将其STAT字段标记为字母D.

因为进程处于TASK_UNINTERRUPTIBLE状态的时间通常转瞬即逝,所以系统在进程脱离该状态时传递信号的现象页不易于被发现。然而在极少数情况下,进程可能因硬件故障、NFS问题或者内核缺陷而在该状态下保持挂起。这时,SIGKILL将不会终止挂起进程。如果问题诱因无法得到解决,那么就只能通过重启系统来消灭该进程。

        大所属UNIX系统实现都支持TASK_INTERRUPTIBLE和TASK_UNINTERRUPTIBLE状态。

从内核2.625开始,Linux加入第三种状态来解决上述进程挂起的问题。

  • TASK_KILLABLE: 该状态类似于TASK_UNINTERRUPTIBLE,但是会在进程收到一个致命信号(即杀死一个进程的信号)时将其唤醒。在对内核代码的线管部分进行改造后,就可以使用改装要来避免各种因进程挂起而重启系统的情况,。这时,向进程发送一个致命信号就能杀死进程。为使用TASK_KILLABLE而进行代码改造的首个模块是NFS.

22.4 硬件产生的信号

        硬件异常可以产生SIGBUS、SIGFPE、SIGILL和SIGSEGV信号,调用kill()函数来发送此类信号是另一种途径,但较为少见,SUSv3规定,在硬件异常的情况下,如果进程从此类信号的处理器函数中返回,抑或进程忽略或阻塞了此类信号,那么进程的行为未定义。原因如下。

  • 从信号处理器中返回:假设机器语言指令产生了上述信号之一,并因此而调用了信号处理器函数。当从处理器函数正常返回后,程序会尝试从其中段=断处恢复执行。可当初引发信号产生的恰恰是这条指令,所以信号会再次光临,故事的结局通常是,程序进入无限循环,重复调用信号处理器函数。
  • 忽略信号:忽略硬件而产生的信号于情理不合,试想苏安书异常之后,程序应当如何继续执行呢?无法明确。当由于硬件异常而产生上述信号之一时,Linux会强制传递信号,即使程序已经请求忽略此类信号。
  • 则色信号:与上一种情况一样,阻塞因硬件而产生的信号也不合情理:不清楚程序随后应当如何继续执行。在2.4以及更早的版本中,Linux 内核仅会将阻塞硬件产生的种种企图意义忽略,信号无论如何都会传递给进程,随后要么进程终止,要么信号处理器会捕获信号---在程序安装有信号处理器的情况下。始于Linux2.6如果信号遭到阻塞,那么该信号会总是立刻杀死进程,即使进程一i纪念馆为此信号安装可信号处理器函数。(对于因硬件产生的信号,Linux2.6之所以改变对其处于阻塞状态下的处理方式,是由于Linux2.4中的行为中隐藏有缺陷,并可能在所现称程序中引起死锁。)

        正确处理硬件产生信号的方法有二:要么接收信号的默认行为(进程终止);要么为其编写不会正常返回的处理器函数。除了正常返回之外,终结处理器执行的手段还包括调用_exit()以终止进程,或者调用siglobjum()。确保将控制传递回程序中(产生信号的指令位置之外)的某一位置。

22.5 信号的同步生成和异步生成

        进程一般无法预测其接收信号的时间。要证明这一点,需要对信号的同步生成和异步生成加以区分。

        截至目前所探讨的都是属于信号的异步生成,即引发信号产生(无论信号发送者是内核还是另一进程)的时间,其发生与进程的执行无关。(例如,用户输入中断字符,或者子进程终止。)对于异步产生的信号,本节起始出的论断并非虚言。

        然而有时候信号的产生是由进程本省的执行造成的。

  • 执行特定的而机器语言指令,可导致硬件异常,并因此而产生22.4节所述的硬件产生信号(SIGBUS、SIGFPE、SIGILL、SIGSEGV和SIGEMT)。
  • 进程可以使用raise()、kill()、或者killpg()向自身发送信号。

        在这些情况下,信号的产生就是同步的--会立即传递信号(除非该信号遭到阻塞,但还要参考22.4节就阻塞硬件产生信号而展开的讨论)。换言之,本节开始处的论断则并不成立。对于同步产生的信号而言,其传递不但可以预测,而且可以重现。

        注意,同步是对信号产生方式的秒数,并不针对信号本身。所有的信号即可以同步产生(kill()向自身发送信号),亦可以异步产生(例如由另一进程使用kill()来发送信号)。

22.6 信号传递的时机与顺序

何时传递一个信号

         如22.5节所述,同步产生的信号会立即传递。例如,硬件异常会发送一个即时信号,而当进程使用raise()箱子生发送信号时,信号会在调用返回前就已经发出。

        当异步产生一个信号时,即使并未将其阻塞,在信号产生和传递之间仍可能会存在一个瞬时延迟。在此期间,信号处于等待状态。这是因为内核将等待信号传递给进程的时机是,该进程正在执行,且发生由内核态到用户态的下一次切换时实际上,这意味着在以下时刻才会传递信号。

  • 进程在前度超时后,再度获得调度时(即,在一个时间片的开始处)。
  • 系统调用完成时(信号的传递可能引起证字啊苏泽的系统调用的过早完成)。

接触对多个信号的阻塞时,信号的传递顺序

        如果进程使用sigprocmask()解除了对多个等待信号的阻塞,那么所有这些信号会立即传递给进程。

        就目前的Linux实现而言,Linux内核按照信号编号的升序来传递信号。例如,如果对处于等待的信号SIGINT(信号编号为2)和SIGQUIT(信号编号为2)同时解除阻塞,那么无论这两个信号的产生次序如何,SIGINT都有娴熟SIGUIT而传递

然而,也不能对传递(标准)信号的特定顺序产生任何依赖,因为SUSv3规定,多个信号的传递顺序由系统实现决定。(该条款仅适用标准信号。如22.8节所述,实时信号的相关标准规定,对于接触阻塞的实时信号而言,其传递顺序必须得到保障。)

当多个解除了阻塞的信号正在等待传递时,如果新高处理器函数在执行期间发生了内核态和用户态之间的切换,那么中断此函数处理器函数的执行,转而曲迪奥用第二个处理器函数(如此递进),如图22-1所示。

22.7 signal()的实现及可移植性

        本节展示了如何使用sigaction()来实现signal()。但还需要顾及这一事实,由于历史沿革和UNIX实现之间的差异,signal()层具有各种不同的语义,信号的早期实现并不可靠,这意味着:

  • 刚已进入信号处理器,会将信号处置重置为其默认行为。(这对你关于20.13节秒数的SA_RESETHAND标志。)要想在同一信号"再度光临"时再次调用该信号处理器函数,程序员ixu在信号处理器内部迪奥用signal(),以显式重建处理器函数。这种情况存在一个问题:在进入信号处理器和重建处理器之间存在一个短暂的窗口期,而如果同意新高在次期间再度来袭,那么将只能按照其默认方式处理。
  • 在信号处理器执行期间,不会对新产生的信号进行阻塞。(这对应于20.12节描述的SA_NODEDEF标志。)这意味着,如果在某一信号处理器函数执行期间,同类型奥再度光顾,那么将对该处理器函数进行递归调用。假定一串信号中彼此的啊hi见间隔足够短,那么对处理器函数的递归调用将可能导致堆栈溢出。

        除了不可靠之外,早期的UNIX实现并未提供系统调用的自动重启功能(即,21.5节所述SA_RESTART标志的相关行为)。

        4.2BSD针对可靠信号的实现纠正了这些限制,其他一些UNIX实现也纷纷效仿。然而,时至今日,这些早期语义仍然存在于System V的signal实现之中。更有甚者,诸如SUSv3和C99之类的当代标准对signal()这些方便也有意部与规范。

        整合上述信息,对signal()的实现如青岛那22-1所示。将实现默认提供信号的现代语义,且不能启用系统调用的自动重启功能。

程序清单 22-1:signal()实现之一

#include <signal.h>

typedef void (*sighandler_t)(int);
sighandler_t signal(int sig,sighandler_t handler)
{
    struct sigaction newDisp, prevDisp;
    newDisp.sa_handler = handler;
    sigemptyset(&newDisp.sa_mask);
#ifdef OLD_SIGNAL
    newDisp.sa_flags = SA_RESETHAND | SA_NODEFER;
#else
    newDisp.sa_flags = SA_RESTART;
#endif
    if(sigaction(sig,&newDisp,&prevDisp) == -1)
        return SIG_ERR;
    else
        return prevDisp.sa_handler;

}

glibc的一些细节

        随着时间的推移,glibc对signal()库函数的实现也历经变化。较新版本(glibc 2及更高版本)的函数库默认提供现代语义。而老版本则提供早期的不可靠(System-V兼容语义)。

Linux内核将signal()实现为系统调用,并提供较老的、不可靠语义。然而glibc库则利用sigaction()实现了signal()库函数,从而将signal()系统调用旁路。

        如果执意在现代glibc版本中使用不可靠信号语义,那么可以显式以(非标准的)sysv_signal()函数来替代对signal()的调用。

#define _GNU_SOURCE
#include <signal.h>
void (*sysv_signal(int sig,void(*handler)(int)))(int);
    Return s prevoius signal disposition on success , or SIG_ERR on error

        sysv_signal()函数的参数与signal()函数相同。

        若编译程序时并未定义_BSD_SOURCE特性测试宏,则glibc会隐式将所有signal()调用重新定义为sysv_signal()调用,亦即启用signal()的不可靠语义。默认情况下会定义——BSD_SOURCE,但是(除非显式定义了_BSD_SOURCE)如果编译程序时定义了诸如SVID或_XOPEN_SOURCE之类的其他特性测试宏,那么对_BSD_SOURCE的默认定义将会失效。

sigaction()是建立信号处理器的首选API

        鉴于上述Ssystem和BSD之间(以及glibc新老版本之间)的可移植性问题,应当坚持使用sigaction()而非signal()来建立信号处理器,这不失为一种稳妥之举,不过还应注意,使用signal()将信号处置设置为SIG_IGN或者SIG_DFL的手法具有良好的移植性(程序更为简短),所以也很常用

22.8 实时信号

        定义于POSIX.1b中的实时的信号,意在弥补对标准信号的诸多限制。较之于标准信号,其又是如下所示。

  • 实时信号的信号范围有所扩大,可应用于应用程序自定义的目的。而标准信号可供应用随意使用的 信号仅有两个:SIGUSR1和SIGUSR2。
  • 对实时信号所采取的是队列话管理。如果见某一实时信号的多个实例发送给一进程,那么将多次传递信号。相反,如果将某一标准信号已经在等待某一进程,而此时即使再次向该进程发送此信号的实例,信号也会只传递一次。
  • 当发送一个实时信号时,可谓信号指定伴随数据(一整型数据或者指针),共接受进程的信号处理器获取。
  • 不同实时信号的传递顺序得到保障。如果有多个实时信号处于等待状态,那么将率先传递具有最小编号的信号。换言之,信号的编号越小,其优先级越高。如果是同一类型的多个信号在排队,那么信号(以及伴随数据)的传递顺序与信号发送来时的顺序保持一致。

SUSv3要求,实现所提供的各种实时信号不得少于_POSIX_RTSIG_MAX(定义为 8)个。Linux内核则定义了32个不同的实时信号,编号乏味为32~63。<signal.h>头文件所定义的RTSIG_MAX常量表征实时信号的可用数量,而此外所定义的常量SIGRTMIN和SIGRTMAX则分别表示可用实时信号编号的最小值和最大值。

        采用LinuxThreads线程实现的系统将SIGRTMIN定义为35(而非32),这时因为LinuxThreads内部使用了前三个实时信号,而采用NPTL线程实现的系统则将SIGRTMIN定义为34,因为NPTL内部使用了前两个实时信号。

对实时信号的区分方式有别于标准信号。不再依赖于所定义的常量不同。然而,程序护院不应该将实时信号编号的整型值在应用程序代码中写死,因为实时信号的范围因UNIX实现的不同而各异。与之相反,指代实时信号编号则可以采用SIGRTMIN+x的形式。形如,表达式(SIGRTMIN+1)就表示第二个实时信号。

        注意SUSv3并未要求SIGRTMAX和SIGRTMIN是简单的整数值,可以将其定义为函数。这也意味着,不能编写如下代码以供预处理器使用

#if SIGRTMIN +100 >SIGRTMAX   /*WRONG!*/
#error "not enough realtime signals"
#endif

        相反,必须在运行时执行有效检查。

对排队实时信号的数量限制

        排队的实时信号(及其相关数据),需要内核维护响应的数据结构,用于罗列每个进程的排队信号。由于这些数据结构会消耗内存,故而内核对排队实时信号数量设置了限制。

SUSv3允许实现为每个进程中可排队的(各类)实时信号数量设置上限,并要求不得少于_POSIX_RTSIG_MAX(定义为32)。实现可借助于对SIGQUEUE_MAX常量的定义来标识其所允许的排队实时信号数量。发起如下调用也能获得这一信息:

lim = sysconf(_SC_SIGQUEUE_MAX);

r若系统使用的glibc库版本在2.4之前,则该调用返回-1.从glibc2.4开始,其返回值由内核版本决定。在Linux2.6.8之前,调用将返回Linux专有文件/proc/kernel/rtsig-max 中的值。该文件所定义为针对搜友进程中可能排队的实时信号总数的系统级限制。默认为1024,不过特权级进程可以对其进行修改。至于当前的排队实时信号总数,可以从Linux专有文件/proc/kernel/rtsig-nr文件中读取。

  •         从版本呢2.6.8开始,Linux取消了这些、proc文件。取而代之的时资源限制RLIMIT_SIGPENDING(36.3节),针对某个特定实际用户ID下辖的所有进程,该限制限定了其可排队的信号总数。sysconf()调用从glibc2.10版本开始返回RLIMIT_SIGPENDING限制。(至于正在等待某一进程的实时信号数量,可以从Linux专有文件/proc/PID/status中的SigQ字段读取。)

使用实时信号

        为了能让一对进程收发实时信号,SUSv3提出以下几点要求:

  • 发送进程使用sigqueue()系统调用及其伴随数据。

        使用kill()、raise()和raise调用也呢个发送实时信号。然而,至于系统是否会利用此类接口发送的信号进行排队处理,SUSv3规定,由具体实现决定。这些接口皮在Linux中会对实时信号进行排队,但在其他许多UNIX实现中,情况则不然。

  • 要为该信号建立了一个处理器函数,接收进程应以SA_SIGINFO标志发起对sigaction()的调用。因此,调用信号处理器时2就会附带额外参数,其中之一是实时信号的伴随数据。

在Linux中,即使接收进程在建立信号处理器时并未指定SA_SIGINFO标志,也能对实时信号进行排队化管理(但在这种情况下,将不能获得信号的伴随数据)。然而,SUSv3也不要求实现确保这一行为,所以依赖这一点将有损于应用的可移植性。

22.8.1 发送实时信号

        系统调用sigqueue()将由sig指定的实时信号发送给pid指定的进程。

#define _POSIX_C_SOURCE 199309
#include <signal.h>

int sigqueue(pid_t pid,int sig,const union sigval value);
        Returns 0 on success,or -1 on error

        使用sigqueue()发送信号所需要的权限与kill()的要求一致。也可以发送空信号(即信号0),其寓意与kil()中的含义相同。(不同于kill(),sigqueue不能通过将pid指定为负值而向整个进程组发送信号)

程序请打22-2:使用sigqueue()发送实时信号

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

int main(int argc, char *argv[])
{
    int sig,numSigs,j,sigData;
    union sigval sv;
    if(argc <4 || strcmp(argv[1],"--help") == 0)
    {
        printf("%s pid sig-num data [num-sigs]\n",argv[0]);
        return -1;
    }

    /*Display our PID and UID,so that they can be compared with the
      corresponding fileds of the siginfo_t argument supplied to
      the handler in the receiving process*/
    printf("%s:PID is %ld,UID is %ld\n",argv[0],(long)getpid(),(long)getuid());
    sig = atoi(argv[2]);
    sigData = atoi(argv[3]);
    numSigs = (argc>4)?atoi(argv[4]):1;

    for(j = 0;j<numSigs;j++)
    {
        sv.sival_int = sigData+j;
        if(sigqueue(atoi(argv[1]),sig,sv) == -1)
        {
            printf("sigqueu: %d",j);
            return -1;
        }
    }
    exit(EXIT_SUCCESS);
}

参数value指定了信号的伴随数据,具有以下形式:

union sigval{
    int sival_int ;    /*Interger value for accopanying data*/
    void *sigval_ptr;  /*pointer value for accopanying data*/
}

        对参数的解释取决于应用程序,由其选择对联合体(union)中的sival_int属性还是sival_ptr属性进行设置。sigqueue()中很少使用sival_ptr,因为指针的作用范围在进程内部,对于另一进程几乎没有意义。该字段得以一展身手之处,应该是在使用sigval联合体的其它函数中,诸如23.6节的POSIX计时器和52.6节的消息队列通知。

        一旦触及对排队信号的数量显示,sigqueue()调用可能会失败,同时将errno置为EAGAIN,以示需要再次发送该信号(在当前队列中某些信号传递之后的某一时间点)。

        程序清单22-2提供了sigqueue()的应用示例。该程序最多接受4个参数,其中前三项为必填项:目标进程ID、信号编号以及伴随实时信号的整型值。如果需要为指定信号发送多个实例,那么可以用可选的第四个参数来制定实例数量,在这中情况下,会为每个信号的百度整型值依次加1。22.8.2将展示该程序的用法。

22.8.2 处理实时信号

可以像标准信号一样,使用常规(单参数)信号处理器来处理实时信号。此外,也可以带有3个参数的信号处理器函数来处理实时信号,期间里则会用到SA_SIGINFO标志(参见21.4节)。一下为使用SA_SIGINFO标志为第六个实时信号建立处理器函数的代码示例:

struct sigaction act;
sigemptyset(&act.sa_mask);
act.sa_sigaction = handler;
act.sa_flags = SA_RESTART | SA_SIGINFO;

if(sigaction(SIGRTMIN+5,&act,NULL) == -1)
{
    perror("sigaction");
}

        一旦采用了SA_SIGINFO标志,传递给信号处理器函数的第二个参数将是一个siginfo_t结构,内含实时信号的附加信息。21.4节详细描述了这一数据结构。对于一个实时信号而言,会在siginfo_t结构中设置如下字段。

  • si_signo字段,其值与传递信号处理器函数的第一个参数相同
  • si_code字段表示信号来源,内容为表21-2中所示各值之一。对于通过sigqueue()发送的实时信号来说,该字段值总是为SI_QUEUE。
  • si_value字段所含数据,由进程于使用aigqueue()发送信号时在value参数(sigval union)中指定。正如前文所说,对该数据的解释由应用程序决定。(若信号由kill()发送,则si_value字段所包含信息无效。)

程序清单22-3提供了处理实时信号的一个例子。该程序捕获信号并针对传递给信号处理器函数的siginfo_t结构,一一显示其中的各个字段值。该程序可接受两个整型命令行参数,均为可选项。如果提供了第一个参数,那么主程序将阻塞所有信号并进入休眠,休眠秒数由该程序指定。在此期间,将对进程的实时信号进行排队处理,并可观察解除对信号阻塞时所发生的情况。第二个参数指定了信号处理器函数在返回前所应休眠的秒数。指定一个非0值(默认为1秒)将有助于放缓程序的执行,便于看清处理多个信号时所发生的情况。

        可以将程序清单22-3中程序与程序清单22-2中成俗(t_sigqueue.c)结合起来探索实时信号的行为,正如一下shell会话日志所示: 

        最终,catch_resigs程序结束休眠,随着信号处理器捕获到各种信号而一一显示消息。(之所以看到shell提示符和下一行输出混杂在一起,是因为atch_rtsigs长须正在后台输出信息)可以看出,实时信号在传递时遵循低编号优先的原则,并且在处理给处理器函数的siginfo_t结构中包含了发送进程的进程ID和用户ID.

         接来输出由同一实时信号的三个示例产生。由si_value值可知,这些信号的传递顺序与发送顺序一致。

        继续使用shell的kill命令向程序actch_rtsigs发送信号。一如既往,处理器函数接收到的siginfo_t结构中包含了发送进程的进程ID和用户ID,但此时的si_code值为SI_USER。

程序清单22-3:处理实时信号

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

static volatile int handlerSleepTime;
static volatile int sigCnt;     /*Number of signals received*/
static volatile int allDone = 0;

static void siginfoHandler(int sig,siginfo_t *si,void *ucontext)
{
    /*UNSAFE: This handler uses non-async-signal-safe functions
        (printf()) see Section 21.1.2*/

    /*SIGINT or SIGTERM can be used to terminate program*/
    if(sig == SIGINT || sig == SIGTERM){
        allDone = 1;
        return;
    }  
    sigCnt ++;
    printf("caught signal %d\n",sig);
    printf("    si_signo=%d,si_code=%d(%s),",si->si_signo,si->si_code,
                (si->si_code == SI_USER)?"SI_USER":
                (si->si_code == SI_QUEUE)?"SI_QUEUE":"other");
    printf("si_value=%d\n",si->si_value.sival_int);
    printf("    si_pid=%ld,si_uid=%ld\n",(long)si->si_pid,(long)si->si_uid);

    sleep(handlerSleepTime);

}

int main(int argc,char *argv[])
{
    struct sigaction sa;
    int sig;
    sigset_t prevMask,blockMask;

    if(argc >1 && strcmp(argv[1],"--help") == 0)
    {
        printf("%s [block-time[handler-sleep-time]]\n",argv[0]);
        return -1;
    }
    handlerSleepTime = (argc >2)?atoi(argv[2]):1;

    /*Establish handler for most signals. During execution of the handler,
    mask all other signals to prevent handlers recursively interrupting
    each other (which would make the output hard to read).*/

    sa.sa_sigaction = siginfoHandler;
    sa.sa_flags = SA_SIGINFO;
    sigfillset(&sa.sa_mask);

    for(sig = 1;sig<NSIG;sig++)
    {
        if(sig != SIGTSTP && sig != SIGQUIT)
        {
            sigaction(sig,&sa,NULL);
        }
    }

    /*optionlly block signals and sleep,allowing signals to be
        sent to us before they are unblocked and handled*/

    if(argc >1)
    {
        sigfillset(&blockMask);
        sigdelset(&blockMask,SIGINT);
        sigdelset(&blockMask,SIGTERM);
        
        if(sigprocmask(SIG_SETMASK,&blockMask,&prevMask))
        {
            perror("sigaction:");
            return -1;
        }

        printf("%s:signals blocked - sleeping %s seconds\n",argv[0],argv[1]);

        sleep(atoi(argv[1]));

        printf("%s:sleep complete\n",argv[0]);

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

    while(!allDone)    //wait for incoming signals
        pause();

    printf("Caught %d signals\n", sigCnt);
    exit(EXIT_SUCCESS);
}

22.9 使用掩码来等待信号:sigsuspend()

         在信号编程时偶尔会遇到如下情况。

  1. 临时阻塞一个信号,以防止信号处理器不会将某些关键代码片段的执行中断。
  2. 解除岁信号的阻塞,然后暂停执行,直至信号到达。

为达这一目的,可能会尝试使用程序清单22-4中代码所示方法。

程序清单22-4:解除阻塞并等待信号的错误做法

sigset_t prevMask,intMask;
struct sigaction sa;
sigemptyset(&intMask);
sigaddset(&intMask,SIGINT);

sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sa.sa_handler = handler;

if(sigaction(SIGINT,&sa,NULL) == -1)
{
    perror("sigaction:");
    return -1;
}

/*Block SIGINT prior to executing critical section.(At this 
point we assume that SIGINT  is not already blocked.)*/

if(sigprocmask(SIG_BLOCK,&intMask,&prevMask) == -1)
{
    perror("sigprocmask - SIG_BLOCK");
    return -1;
}

/*Critical section:do some work here that must no be
interrupted by the SIGINT handler*/

/*End of critical section - restore old mask to unblock SIGINT*/

if(sigprocmask(SIG_BLOCK,&prevMask,NULL) == -1)
{
    perror("sigprocmask - SIG_SETMASK");
    return -1;
}

/*BUG:what if SIGINT arrives nows...*/

pause();  // wait for SIGINT

 程序清单22-4中存在一个问题。假设SIGINT信号的传递发生在第二次调用sigprocmsk()之后,调用pause()之前。(实际上,该信号可能产生执行关键片段的任意时刻,仅当解除对信号的阻塞之后才会随之而传递。)SIGINT信号的传递将导致对处理器函数的调用,而当处理器返回后,主程序恢复执行,pause()调用将陷入阻塞,直到SIGINT信号的第二个实例到达为止。这有违代码的本意:解除对SIGINT阻塞并等待其第一次出现

        即在关键片段的起始点(即首次调用sigprocmask())和pause()调用之间产生SIGINT信号的可能行不打,但这确实是上述代码的一处缺陷。者却绝育时间的却显示竞态条件(5.1节)的例子之一。通常,静态条件发生于两个进程或线程共享资源时。然而,此处的竞态条件却发生在主程序和其自身的信号处理器之间。

        要避免这一问题,需要将杰出信号阻塞和挂起进程这两个动作等装成一个原子操作。这正是sigsuspend()系统调用的目的所在。

#include <signal.h>
int sigsuspend(const sigset_t *mask);
            (Normally) Returns -1 whith errno set to EINTR

        sigsuspend()系统调用将以mask所指向的信号集来替换进程的信号掩码,然后挂起进程的执行,直到捕获到信号,并从信号处理器返回。一旦处理器返回,sigsuspend()会将进程信号掩码恢复为调用前的值。

        调用sigsuspend(),相当于以不可中断方式执行如下操作:

sigprocmask(SIG_SETMASK,&mask,&prevMask);//assign new mask
pause();
sigprocmask(SIG_SETMASK,&prevMask,NULL);  //Restore old mask

虽然恢复老的信号掩码乍看起来似乎麻烦,但为了在需要反复等待信号的情况下避免竞态条件,这一做法就至关重要。在这种情况下,除非是sigsuspend()调用,否则信号必须保持阻塞状态。如果稍后需要对在调用sigsuspend()之前遭到阻塞的信号解除阻塞,可以进一步调用sigprocmask()。

        若sigsuspend()因信号的传递而中断,则将返回-1,并将errno置为EINTR。如果mask指向的地址无效,则sigsuspend()调用失败,并将errno置为EFAULT。

示例程序

        程序清单22-5展示了对sigsuspend()的使用。该程序执行如下步骤。

  • 调用printSigMask()函数(程序清单20-4)来显示进程信号掩码的初始值。
  • 阻塞SIGINT和SIGQUIT信号,并保存原始的进程信号掩码。
  • 为SIGINT和SIGQUIT信号建立相同的处理器函数。该处理器显示一条消息,且若对其调用因SIGQUIT信号的传递而引起,则设置全局变量gotSigquit。
  • 循环执行,直至对gotSigquit进行了设置。每次循环都执行如下步骤。

        -使用printSigMask()函数显示信号掩码的当前值,

        -令CPU忙于循环并持续数秒钟,以此来模拟对一个关键片段的执行。

        -使用printPendingSigs()函数来显示等待信号的掩码(程序清单20-4)。

        -使用sigsuspend()来解除对SIGINT和SIGQUIT信号的阻塞,并等待信号(如果尚未有信号处于等待状态)。

        -使用sigprocmask()将进程信号掩码恢复为原始状态,然后在使用printSigMask()来显示信号掩码。

程序清单22-5:使用sigsuspend()

#define _GNU_SOURCE /*Get strsignal() declaration from <string.h>*/
#include <string.h>
#include <signal.h>
#include <time.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>



void                    /* Print list of signals within a signal set */
printSigset(FILE *of, const char *prefix, const sigset_t *sigset)
{
    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                     /* Print mask of blocked signals for this process */
printSigMask(FILE *of, const char *msg)
{
    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                     /* Print signals currently pending for this process */
printPendingSigs(FILE *of, const char *msg)
{
    sigset_t pendingSigs;

    if (msg != NULL)
        fprintf(of, "%s", msg);

    if (sigpending(&pendingSigs) == -1)
        return -1;

    printSigset(of, "\t\t", &pendingSigs);

    return 0;
}

static volatile sig_atomic_t gotSigquit = 0;

static void handler(int sig)
{
    printf("Caught signal %d(%s)\n",sig,strsignal(sig));

    if(sig == SIGQUIT)
        gotSigquit = 1;
}

int main(int argc,char *argv[])
{
    int loopNum;
    time_t startTime;
    sigset_t origMask,blockMask;
    struct sigaction sa;

    printSigMask(stdout,"Initial signal mask is : \n");

    sigemptyset(&blockMask);
    sigaddset(&blockMask,SIGINT);
    sigaddset(&blockMask,SIGQUIT);
    if(sigprocmask(SIG_BLOCK,&blockMask,&origMask) == -1)
    {
        perror("sigprocmask -SIG_BLOCK");
        return 1;
    }

    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;
    sa.sa_handler = handler;

    if(sigaction(SIGINT,&sa,NULL) == -1)
    {
        perror("sigaction:SIGINT");
        return -1;
    }

      if(sigaction(SIGQUIT,&sa,NULL) == -1)
    {
        perror("sigaction:SIGQUIT");
        return -1;
    }

    for(loopNum =1;!gotSigquit;loopNum++)
    {
        printf("=== LOOP %d\n",loopNum);

        /*simulate a critical section by  delaying a few seconds*/

        printSigMask(stdout,"Starting critical section,signal mask is :\n");
        for(startTime = time(NULL);time(NULL)<startTime +4;)
            continue;

        printPendingSigs(stdout,"before sigsuspend() - pending signals:\n");

        if(sigsuspend(&origMask) == -1 && errno !=EINTR)
        {
            perror("sigsuspend:");
            return -1;
        }
    }

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

    printSigMask(stdout,"===Exited loop\nRestored signal mask to:\n");

    /*Do other processing...*/

    exit(EXIT_SUCCESS);
}

         以下shell回话日志所示为程序清单22-5中程序的运行结果示例:

         程序调用sigsuspend()解除了对SIGINT的阻塞,还显示了最后一行输出。正式在那一点,调用了信号处理器,并显示了那一行输出。

主程序会继续循环。按下Control + \将导致信号处理器去设置gotSigquit标志,并转而引发主程序终止循环。

22.10 以同步方式等待信号

         22.9节描述了如何结合信号处理器和sigsuspend()来挂起一个进程的执行,直至传来一个信号,然而,这需要编写信号处理器函数,还需要应对异步信号传递所带类的复杂性。对于某些应用而言,这种方法过于繁杂。作为替代方案,可利用sigwaitinfo()系统调用来同步接受信号。

#define _POSIX_C_SOURCE 199309
#include <signal.h>

int sigwaitinfo(const sigset_t *set,siginfo_t *info);
    Returns number of delivered signal on success,or -2 on error

        sigwait info()系统调用挂起进程的执行,直至set指向信号集中的某一信号到达。如果调用siwaitinfo()时,set中的某一信号已经处于等待状态,那么sigwaitinfo()将立即返回。传递来的信号就从此进程的等待信号队列中移除,并且将返回信号编号作为函数结果。info参数如果不为空,则会指向经初始化处理的siginfo_t结构,其中所含信息与提供给信号处理器函数的siginfo_t参数(21.4节)相同。

        sigwaitinfo()所接受信号的传递顺序和排队特性与信号处理器所捕获的信号相同,就是说,不对标准信号进行排队处理,对实时信号进行排队处理,并且对实时信号的传递遵循低编号优先的原则。

        除了卸去编写信号的处理器负担之外,使用sigwaitinfo()来等待信号也比信号处理器外加sigsuspend()的组合稍快一些(见练习22-3)。

        将对set中信号集的阻塞与调用sigwait()结合起来,这当属明智之举。(即便某一信号遭到阻塞,仍然可以使用sigwait()来获取信号。)如果没有这么做,而信号在首次调用sigwaitinfo()之前,或者两次连续调用sigwaitinfo()之间到达,那么对信号的处理将只能依照当前处置。

SUSv3规定,调用sigwaitinfo()而不阻塞set中的信号将导致不可预知的行为(行为未定义)。

        程序清单22-6所示为使用sigwaitinfo()的例子之一。程序首先阻塞所有信号,然后延迟数秒时间,具体描述可由命令行参数来制定,从而允许在调用sigwaitinfo()之前向程序发送信号。程序随即持续循环调用sigwaitinfo()来接受输入信号,直至收到SIGINT或SIGTERM信号。

        如下shell会话日志展示了程序清单22-6程序的运行情况。程序在后台运行,并制定在执行sigwaitinfo()前演示60s,随后在想进程发送两个信号:

 最终程序完成睡眠,sigwaitinfo()调用循环接受排队信号。(由于t_sigwaitinfo程序正在后台输出笑消息,孤儿可以观察到shell提示符和程序的下一行输出混在一起。)至于处理器所捕获到的实时信号,可以看出,编号低的信号率先传递,而且,借助于传递给信号处理器函数的siginfo_t结构,还可以获得发送进程的进程ID和用户ID.

        继续使用shell的kill命令向进程发送信号。可以观察到,这次将si_code字段设置为SI_USER(非SI_QUEUE).

         收到SIGUSR1信号,由其输出可知,si_value字段为100.该值是由sigqueue()发送的迁移信号初始化而成。前文曾指出,进队sigqueue()所发送的信号。si_value字段所包含的信息才是可靠地。

程序清单22-6:使用sigwaitinfo()来同步等待信号

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

int main(int argc,char *argv[])
{
    int sig;
    siginfo_t si;
    sigset_t allSigs;

    if(argc >1 && strcmp(argv[1],"--help") == 0)
    {
        printf("%s [delay-secs]\n",argv[0]);
        return -1;
    }

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

    /*Block all signals (except SIGKILL and SIGSTOP)*/

    sigfillset(&allSigs);
    if(sigprocmask(SIG_SETMASK,&allSigs,NULL) == -1)
    {
        perror("sigprocmask:");
        return -1;
    }
    printf("%s:signals blocked\n",argv[0]);

    if(argc >1){ /*delay so that signals can be sent to us*/
        printf("%s:abput to delay %s second\n",argv[0],argv[1]);
        sleep(atoi(argv[1]));
        printf("%s finished delay\n",argv[0]);
    }

    for(;;){ /*fetch signals until SIGINT(^C) or SIGTERM*/
        sig = sigwaitinfo(&allSigs,&si);
        if(sig == -1)
        {
            perror("sigwaitinfo:");
            return -1;
        }
        if(sig == SIGINT || sig == SIGTERM)
        {
            exit(EXIT_SUCCESS);
        }

        printf("got signal:%d(%s\n)",sig,strsignal(sig));
        printf("    si_signo = %d,si_code = %d(%s),si_value = %d\n",
                si.si_signo,si.si_code,
                (si.si_code == SI_USER)?"SI_USER":
                (si.si_code == SI_QUEUE)?"SIGUEUE":"other",
                si.si_value.sival_int);
        printf("    si_pid%ld,si_uid=%ld\n",
                (long)si.si_pid,(long)si.si_uid);

    }
}

        sigtimedwait()系统调用是sigwaitinfo()调用的变体。唯一的区别是sigwitmedwait()允许指定等待时限。

#define _POSIX_C_SOURCE 199309
#include <signal.h>

int sigtimedwait(const sigset_t *set,siginfo_t *info,
                const struct timespec *timeout);

        Returns number of delivered signal on success,
    or -1 on error or timeout(EAGIN)

        timeout参数指定了允许sigtimedwait()等待一个信号的最大时长,是指向如下类型结构的一枚指针:

struct timespec{
time_t tv_sec;  //Seconds ('time_t' is an integer type)
long tv_nsec;    //Nanoseconds
};

        填写timespec结构的所属字段,也就置顶了允许sigtimedwait()等待的最大秒数和纳秒数。如果将这两个字段均指定为0,那么函数将立刻超时,就是说,会去轮询检查是否有指定信号几种的人一信号处于等待状态。如果调用超时而又没有收到信号,sigtimedwait()将调用失败,并将errno置为EAGAIN。

        如果将timeout参数指定为NULL,那么sigtimedwait()将完全等同于sigwaitinfo()。SUSv3遂于timeout的NULL值含义也语焉不详,而某些UNIX事项则将该值视为轮询请求并立即将其返回。

22.11 通过文件描述符来获取信号

        始于内核2.6.22,Linux提供了(非标准的)signalfd()系统调用;利用该系统调用可以创建一个特殊文件描述符,发往调用者的信号都可以从该描述符中读取。signalfd机制为同步接受信号提供了sigwaitinfo()之外的另一种选择。

#include <sys/signalfd.h>

int signalfd(int fd,const sigset_t *mask,int flags);
            Returns file desciptor on success,or -1 on error

        mask参数是一个信号集,指定了有意通过signalfd文件描述符来读取的信号。如同sigwaitinfo()一样,通常也应该使用sigprocmask()阻塞mask中的所有信号,以确保有机会读取这些信号之前,不会按照默认处置对他们进行处理。

        如果指定fd为-1,那么signalfd()会创建一个新的文件描述符,由于读取mask中的信号;否则,将修改与fd相关的mask值,且该fd一定是由之前对signalfd()的依次调用创建而成。

早期的flags参数保留下来供将来使用,且必须将其指定为0.然而Linux从2.6.27开始支持下面两个标志。

SFD_CLOEXEC

        为新的文件描述符设置 close-on exec(FD_CLOEXEC)标志。该标志之所以必要,与4.3.1节中描述的open() O_CLOEXEC标志设置的理由相同

SFD_NONBLOCK

             为低层的打开文件描述符设置O_NONBLOCK标志,以确保不会阻塞未来的读操作。既省去了一个额外的fcntl()调用,又获得了相同的结果。

        创建文件描述符之后,可以使用read()调用从中读取信号。提供给read()的缓冲区必须足够大,至少应能容纳一个signalfd_siginfo结构。<sys/signalfd.h>文件定义了该结构,如下所示:

        

struct signalfd_siginfo{
    uint32_t ssi_signo;    /*Signal number*/
    int32_t ssi_errno;    /*Error number (generally unused)*/
    int32_t ssi_code;    /*Signal Code*/
    uint32_t ssi_pid;      /*Process ID of sending process*/
    uint32_t ssi_uid;     /*Real user ID OF sender*/
    int32_t ssi_fd;    /*File descriptor (SIGPOLL/SIGIO)*/
    uint32_t ssi_tid;    /*kernel timer id(POSIX timers)*/
    uint32_t ssi_band;    /*Band event (SIGPOLL/SIGIO)*/
    uint32_t ssi_tid;    /*(kernel-internal) timer ID (POSIX timers)*/
    uint32_t ssi_overrun;    /*Overrun count (POSIX timers)*/
    uint32_t ssi_trapno;    /*Trap number*/
    int32_t ssi_status;    /*Exit status or signal (SIGCHILD)*/
    int32_t ssi_int;    /*Integer sent by sigqueue*/
    uint64_t ssi_ptr;    /*Pointer sent by sigqueue*/
    uint64_t ssi_utime;    /*User CPU time (SIGCHLD)*/
    uint64_t ssi_stime;    /*System CPU time (SIGCHLD)*/
    uint64_t ssi_addr;    /*Address that generated signal
                            (hardware-generated sinals only)*/
};

该结构中字段所返回的信息与传统siginfo_t结构(21.4节)中类似命名的字段信息相同。

        read()每次调用都将返回与等待信号数目相等的signalfd_siginfo结构,并填充到已提供的缓冲区中。如果调用时并无信号正在等待,那么read()将阻塞,直到有信号到达。也可以使用fcntl()的F_SETFL操作(5.3节)来为文件描述符设置O_NONBLOCK标志,使得读操作不再阻塞,且若无信号等待,则调用失败,errno为EAGAIN.。

        当从signalfd文件描述符中读取到一信号时,该信号获得接纳,且不再为该进程等待。

程序清单22-7:使用signalfd来获取信号

#include <sys/signalfd.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>

int main(int argc,char *argv[])
{
    sigset_t mask;
    int sfd,j;
    struct signalfd_siginfo fdsi;
    ssize_t s;

    if(argc <2 || strcmp(argv[1],"--help") == 0)
    {
        printf("%s sig-num...\n",argv[0]);
        return -1;
    }
    printf("%s: PID=%ld\n",argv[0],(long)getpid());

    sigemptyset(&mask);
    for(j=1;j<argc;j++)
    {
        sigaddset(&mask,atoi(argv[j]));
    }

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

    sfd = signalfd(-1,&mask,0);
    if(sfd == -1)
    {
        perror("signalfd:");
        return -1;
    }

    for(;;)
    {
        s = read(sfd,&fdsi,sizeof(struct signalfd_siginfo));
        if(s!=sizeof(struct signalfd_siginfo))
        {
            perror("read:");
            return -1;
        }

        printf("%s : got signal %d",argv[0],fdsi.ssi_signo);
        if(fdsi.ssi_code == SI_QUEUE){
            printf(";ssi_pid = %d; ",fdsi.ssi_pid);
            printf("ssi_int = %d",fdsi.ssi_int);
        }
        printf("\n");
    }
}

        select()、poll()和epoll()(参见第63章)可以将signalfd描述符和其他描述符混合起来进行监控。撇开其他用途不提,该特性可称为63.5.2节所述self-pipe技巧之外的另一选择。如果有信号正在等待,那么这些技术将文件描述符只是为可读取。

        当不再需要signalfd文件描述符时,应当关闭signalfd以释放相关内核资源。

        程序清单22-7展示了signalfd()的用法。程序为在命令函参数中指定的信号创建掩码,阻塞这些信号,然后创建用来读取这些信号的signalfd文件描述符,之后循环从该文件描述符中读取信号,并显示返回的signalfd_siginfo结构中的部分信息。如下shell会话在后台运行了程序清单22-7并使用和程序清单22-2程序(t_sigqueue.c)向该进程发送实时信号及伴随数据。

 22.12 利用信号进行进程间通信

        从某种角度,可将信号视为进程间通信(IPC)的方式之一。然而,信号作为一种IPCA机制却也保守限制。首先,与后续各章描述的其他IPC方法相比,对信号编程,既繁且难,具体原因如下。

  • 信号的异步本质就意味着需要面对各种问题,包括可重入性需求、静态条件及在信号处理器中正确处理全局变量。(如果用sigwaitinfo()或者signalfd()来同步获取信号,这些问题中的大部分都不会遇到。)
  • 没有对标准信号进行处理。即使是对于实时信号,也存在对信号排队数量的限制。这意味着,为了避免丢失信息,接受信号的进程必须想方设法通知发送者,自己为接受另一个信号做好了准备。要做到这一点,最显而易见的方法是由接收者像发送者发送信号。

        还有一个更深层次的问题,信号缩写嗲的信息量有限:信号编号以及实时信号情况下一字之长的附加数据(一个整数或者一个指针值)。与诸如管道之类的其他IPC方法相比,过低的带宽使得信号传输极为缓慢。

由于上述种种限制,很少将信号用于IPC。

22.13 早期的信号API(System V BSD)

        之前对信号的讨论一直着眼于POSIX信号API。本节简要回顾一下System V和BSD提供的历史API.虽然所有的新应用程序都应当使用POSIX API,但是在从其他UNIX实现移植(通常较为老迈的)应用时,可能会碰到这些过时的API.当移植这些使用老旧API的程序时,因为Linux(像许多其他UNIX实现一样)提供了System V 和BSD兼容的API,所以通常所要做的全部工作不过是在Linux平台上重新编译而已。

System V 信号 API

        如前所述,System V 中的信号API存在一个重要差异:当使用signal()建立处理器函数时,得到的是老版、不可靠的信号语义。这意味着不会讲信号添加到进程的信号掩码中,调用信号处理器时会将信号处置重置为默认行为,以及不会自动重启系统调用。

        下面,简单介绍一些System V信号API中的函数。手册页提供有全部的细节。SUSv3定义了所有这些函数,但指出应优先使用现代版的POSIX等价函数。SUSv4将这些函数标记为已废止。

#define _XOPEN_SOURCE 500

#include <signal.h>

void (*sigset(int sig,void(*handler)(int)))int();
    On success:returns the previous disposition of sig, or SIG_HOLD
    if sig was previously blocked; on error -1 is returned

        为建立一个具有可靠语义的信号处理器,System V提供了sigset()调用(原型类似于signal())。与signal()一样,可以将sigset()的handler参数指定为SIGINT、SIGDFL、或者信号处理器函数的地址。此外还可以将其指定为SIG_HOLD,在将信号添加到进程信号掩码的同时保持信号处置不变。

        如果指定handler参数为SIG_HOLD之外的其他值,那么会将sig从进程信号掩码中移除(即,如果sig遭到阻塞,那么将接触对其阻塞)。

#define _XOPEN_SOURCE 500
#include <signal.h>

int sighold(int sig);
int sigrelse(int sig);
int sigignore(int sig);

                    All return 0 on success, or -1 on error

int sigpause();
                    Always returns -1 with errno set to EINTR

        sighold()函数将一个信号添加到进程信号掩码中,sigrelse()函数则是从信号掩码中移除一个信号。sigignore()函数设定对某信号的处置为“忽略(ignore)”。sigpause()函数类似于sigsuspend()函数,但金聪进程信号掩码中移除一个信号,随后将暂停进程,直到有信号到达。

BSD信号API

        POSIX信号API从4.2BSDAPI中汲取了很多灵感,所以BSD函数与POSIX函数大体相仿。

        如同前文对System V信号API中的函数描述一样,首先给出BSD信号API中各函数的原型,随后简单解释一下每个函数的操作,手册页会包含全部细节。

#define _BSD_SOURCE
#include <signal.h>

int sigvec(int sig,struct sigvec *vec,struct sigvec *ovec);
            Return 0 on success, or -1 on error

sigvec()类似于sigaction()。vec和ovec参数是指向如下类型结构的指针:

struct sigvec{
    void (*sv_handler)();
    int sv_mask;
    int sv_flags;
};

        sigvec结构中的字段与sigaction结构中的那些字段紧密对应。第一个显著差异是sv_mask(类似于sa_mask)字段是一个整型,而非sigset_t类型。这意味着,在32位架构中,最多支持31个不同信号。另一个不同之处则在于sv_flags(类似于a_flags)字段中使用了SV_INTERRUPT标志。因为重启系统调用是4.2BSD的默认行为,该标志是用来指定应使用信号处理器来中断慢速系统调用。(这点与POSIX API 截然相反,在使用sigaction()建立信号处理器时。如果希望启用系统调用重启功能,就必须显式指定SA_RESTART标志。)

#define _BSD_SOURCE
#include <signal.h>

int sigblock(int mask);
int sigsetmask(int mask);
                Both return previos signal mask

int sigpause(int sigmask);
                Always returns -1 with errno set to EINTR

int sigmask(sig);
                Returns signal mask value with bit sig set

        sigblock()函数向进程信号掩码中添加一组信号。这类似于sigprocmask()的SIG_BLOCK操作。sigsetmask()调用则为信号掩码指定了一个绝对值。这类似于sigprocmask()的SIG_SETMASK操作。

        sigpause()类似于sigsuspend()。注意,随该函数的定义在System V 和BSD API中具有不同的调用签名。GNU C 函数库默认提供 System V 版本,除非在编译程序时指定了特性测试宏_BSD_SOURCE.

        sigmask()宏将信号编号转换成响应的32位掩码值。此类位掩码可以彼此相或,一起创建一组信号,如下所示:

sigblock(sigmask(SIGINT) | sigmask(SIGQUIT))

22.14 总结

        某些信号引发进程创建一个核心转储文件,并终止进程。核心转储所包含的信息可供tiaoshi-起检查进程终止时的状态。默认情况下,对核心转储文件的命名位core,但Linux提供了/proc/sys/kernel/core_pattern文件来控制对核心转储文件的命名。

        信号的产生方式可以是异步的,也可以是同步的。当由内核或者另一进程发送给进程时,信号可能时异步产生的。进程无法精确预测异步信号的传递时间。(文中曾指出,异步信号通常会在接收进程第二次从内核态切换到用户态时进行传递。)因进程滋生执行代码而直接产生的信号则属于是同步产生的,例如,执行了一个引发硬件异常的指令,或者去调用raise()。同步产生的信号,其传递可以精确预测(立即传递)。

        实时信号是POSIX对原始信号模型的扩展,不同之处包括对实时信号的队列化管理,具有特定的传递顺序,并且还可以伴随少量数据一同发送。设计实时信号意在供应用程序自定义使用。实时信号的发送使用sigqueue()系统调用,并且还想信号出口i其函数提供了一个附加参数(siginfo_t结构),以便其获得信号的伴随数据,以及发送进程的进程ID和实际用户ID。

        sigsuspend()系统调用在自动修改进程信号掩码的同时,还将挂起进程的执行直到信号到达,切尔这属于同一原子操作。为了避免执行上述功能时出现竞态条件,确保sigsuspend()的原子性至关重要。

        可以使用sigwaitinfo()和sigtimedwait()来同步等待一个信号。这省去了对信号处理器的设计和编码工作。对于以等待信号的传递为唯一目的的程序而言,使用信号处理器纯属多此一举

        像sigwaitinfo()和sigtimedwait()一样,可以使用Linux特有的signalfd()系统调用来同步等待一个信号。这一接口的独特之处在于可以通过文件描述符拉读取信号。还可以使用select()、poll()和epoll来对其进行监控。

        尽管可以将IPC视为IPC的方式之一,但诸多制约因素令其常常无法胜任这一目的,其中包括异步本质、不对信号进行排队处理的事实,以及较低带宽。信号更为常见的应用常见是用于进程同步,或是各种其他目的(比如时间通知、作业控制以及定时器到期)。

        信号的jibengainian-虽然简单,但因为设计的细节很多。信号在系统调用API的各部分中都扮演着重要角色。后面几张还将重温对信号的使用。此外,还有各种信号相关的函数时针对线程的(比如,pthread_kill()和pthread_sigmask()),将延后至33.2进行讨论。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值