《UNIX环境高级编程》第10章 信号

10.1 信号

信号是软中断。很多比较重要的应用程序都需处理信号。信号提供了一种处理异步事件的方法。例如,终端用户键入中断键,会通过信号机制停止一个程序,或及早地终止管道中的下一个程序。

10.2 信号概念

首先,每个信号都有一个名字。这些名字都是以3个字符SIG开头。例如:

  • SIGABRT 是夭折信号,当进程调用abort函数时产生这种信号。
  • SIGALRM 是闹钟信号,由alarm函数设定的定时器超时后将产生此信号。
    各种系统实现的信号数量不同,linux支持31种信号。

不存在编号为0的信号。很多条件可以产生信号。

  • 当用户按某些终端键时,引发终端产生信号。在终端上按Delet键(很多系统上时是Ctrl+c)通常产生中断信号(SIGINT)。这是停止一个已经失去控制的程序的方法。

  • 硬件异常产生信号:除数为0、无效的内存引用等。这些条件通常由硬件检测到,并通知内核。然后由内核为该条件正在运行时的进程产生适当的信号。例如,对执行一个无效内存引用的进程产生SIGSEGV信号。

  • 进程调用kill函数可以将任意信号发送给另一个进程或进程组。限制是:接收信号进程和发送信号进程的所有者必须相同,或发送信号的进程必须是超级用户。

  • 用户可用kill命令将信号发送给其他进程。此命令只是kill函数的接口。常用此命令终止一个失控的后台进程。

  • 当检测到某种软件条件已经发生,并将其通知有关进程时也产生信号。这里不是指硬件产生条件,而是软件条件。


信号是异步事件的经典实例。产生信号的事件对进程而言是随机出现的。进程不能简单地测试一个变量来判断是否发生了一个信号(同步处理),而是必须告诉内核“此信号发生时,请执行下列操作”(异步处理)。

在某个信号出现时,可以告诉内核按下列3种方式之一进行处理,我们称之为信号的处理或与信号相关的动作。

  1. 忽略此信号。大多数信号都可以使用这种方式进行处理,但有两种信号却绝不能被忽略。它们是SIGKILL和SIGSTOP。这两种信号不能被忽略的原因是:它们向内核和超级用户提供了使进程终止或停止的可靠方法。另外,如果忽略某些有硬件异常产生的信号(如除以0),则信号的运行行为是未定义的。

  2. 捕捉信号。为了做到这一点,要通知内核在某种信号发生时,调用一个用户函数。在用户函数中,可执行用户希望对这种事件进行的处理。注意,不能捕捉SIGKILL和SIGSTOP信号。

  3. 执行系统默认动作。对大多数信号的默认动作是终止该进程。

下面列出了所有信号的名字,以及其默认动作。“终止+core”表示在进程当前目录的core文件中复制了该进程的内存映像,该文件名为core大多数UNIX系统调试程序都使用core文件检查进程终止时的状态

名字 说明 默认动作
SIGABRT 异常终止(abort) 终止+core
SIGALRM 定时器超时(abort) 终止
SIGBUS 硬件故障 终止+core
SIGCHLD 子进程状态改变 忽略
SIGCONT 使暂停进程继续 继续/忽略
SIGEMT 硬件故障 终止+core
SIGFPE 算术异常 终止+core
SIGHUP 连接断开 终止
SIGILL 非法硬件质量 终止+core
SIGITN 终端中断符 终止
SIGIO 异步IO 终止/忽略
SIGIOT 硬件故障 终止+core
SIGKILL 终止 终止
SIGPIPE 写至无读进程的管道 终止
SIGPOLL 可轮询事件(poll) 终止
SIGPROF 梗概时间超时(setitimer) 终止
SIGPWR 电源失效/重启动 终止/忽略
SIGQUIT 终端退出符 终止+core
SIGSEGV 无效内存引用 终止+core
SIGSTKFLT 协处理器栈故障 终止
SIGSTOP 停止 停止进程
SIGSYS 无效系统调用 终止+core
SIGTERM 终止 终止
SIGTRAP 硬件故障 终止+core
SIGTSTP 终端停止符 停止进程
SIGTTIN 后台读控制tty 停止进程
SIGTTOU 后台写向控制tty 停止进程
SIGGURG 紧急情况(套接字) 忽略
SIGGUSR1 用户定义信号,可用于应用程序 终止
SIGGUSR2 用户定义信号,可用于应用程序 终止
SIGVTALRM 虚拟时间闹钟(setitimer) 终止
SIGWINCH 终端窗口大小改变 忽略
SIGXCPU 超过CPU限制(setrlimit) 终止或终止+core
SIGXFSZ 超过文件长度限制(setrlimit) 终止或终止+core

下面详细地说明一下个信号的含义:

  • SIGABRT 调用abort函数时产生此信号。进程异常终止。
  • SIGALRM 当alarm函数设置的定时器超时时,产生此信号。若由setitimer函数设置的间隔时间已经超时时,也产生此信号。
  • SIGBUS 指示一个实现定义的硬件故障。当出现某些类型的内存故障时,实现常常产生此种信号。
  • SIGCHLD 在一个进程终止或停止时,SIGCHLD信号被送给其父进程。按系统默认,将忽略此信号。如果父进程希望被告知其子进程的这种状态改变,则应该捕捉此信号。
  • SIGCONT 此信号发送给需要继续运行,但当前处于停止状态的进程。如果当前进程处于停止状态,则系统默认动作是使该进程继续运行。
  • SIGEMT 指示一个实现定义的硬件故障。
  • SIGFPE 此信号表示一个算术运算异常,如除以0、浮点溢出等。
  • SIGHUP 如果终端接口检测到一个连接断开,则将此信号送给与该终端相关的控制进程(会话首进程)
  • SIGILL 此信号表示进程已执行一条非法硬件指令。
  • SIGITN 当用户按中断键,终端驱动程序产生此信号并发送至前台进程组中的每一个进程。
  • SIGIO 此信号指示一个异步IO事件。
  • SIGIOT 指示一个实现定义的硬件故障。
  • SIGKILL 这是两个不能被捕捉或忽略信号中的一个。它向系统管理员提供了一种可以杀死任一进程的可靠方法。
  • SIGPIPE 如果在管道进程的读进程已终止时写管道,则产生此信号。
  • SIGPWR 这是一种依赖于系统的信号。它主要应用于具有不间断电源(UPS)的系统。如果电池电压过低信息的进程将信号SIGPWR发送给init进程,然后由init处理停机操作。
  • SIGQUIT 类似于SIGINT信号的功能,但同时产生一个core文件。
  • SIGSEGV 指示进程进行了一次无效的内存引用(通常说明程序有错误,比如访问了一个未接初始化的指针)。SEGV代表“段违例”(segmentation violation)。
  • SIGSTKFLT 这是早期有linux定义的,用于数学协处理器的栈故障。
  • SIGSTOP 这是一个作业控制信号,它停止一个进程。它不能被捕捉或忽略。
  • SIGSYS 该信号指示一个无效的系统调用。
  • SIGTERM 这是由kill命令发送的系统默认终止信号,这是由应用程序捕获的,使用SIGTERM可以使程序有机会在退出前做好清理工作,从而优雅的终止(相对SIGKILL而言,SIGKILL不能被捕获或忽略)。
  • SIGTRAP 指示一个实现定义的硬件故障。
  • SIGTSTP 交互停止信号,当用户在终端上按挂起键(Ctrl+Z),终端驱动程序发出此信号,给前台进程组中所有进程。
  • SIGTTIN 当一个后台进程组试图读其控制终端时,终端驱动程序产生此信号。
  • SIGTTOU 当一个后台进程组试图写其控制终端时,终端驱动程序产生此信号。
  • SIGGURG 此信号通知进程已经发生一个紧急情况,在网络连接上接到带外数据(带外数据(out—of—band data),有时也称为加速数据(expedited data),是指连接双方中的一方发生重要事情,想要迅速地通知对方。 )时,可选择性地产生此信号。
  • SIGGUSR1 这是一个用户定义的信号,可用于应用程序。
  • SIGGUSR2 这是一个用户定义的信号,可用于应用程序。
  • SIGVTALRM 当一个有setitimer函数设置的虚拟间隔时间已经超时时,产生此信号。
  • SIGWINCH 内核维持每个终端或伪终端相关联窗口的大小。进程可以用ioctl函数得到或设置窗口的大小。如果进程用ioctl的设置窗口大小命令更改了窗口大小,则内核将SIGWINCH信号发送至前台进程组。
  • SIGXCPU 如果进程超过了其软CPU时间限制,则产生此信号。
  • SIGXFSZ 如果进程超过了其软文件长度限制,则产生此信号。

10.3 函数signal

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

#include <signal.h>
void (*signal (int signo,void(*func)(int)))(int);
//参数1:信号编号;参数2:信号处理函数指针;同时该函数返回之前该信号的信号处理函数

signal函数由ISO C定义。因为ISO C不涉及多进程、进程组以及终端IO等,所有它对信号的定义非常含糊,以至于对UNIX系统而言几乎毫无用处。
signo参数是信号名;
func的值是常量SIG_IGN、常量SIG_DFL或当连接到此信号后要调用的函数的地址。
SIG_IGN,向内核表示忽略此信号,有两个信号不能忽略。
SIG_DFL,向内核表示接到此信号后的动作是系统默认动作。
当指定函数地址时,则在信号发生时,调用该函数,我们称这种处理为捕捉该信号,称此函数为信号处理程序(signal handler)或信号捕捉函数(signal-catching function)


之前的signal函数写法比较复杂,可以简化为以下:

typedef void sigfunc(int);
sigfunc *signal(int ,sigfunc *);

如果查看signal.h头文件可以看到:

#define SIG_ERR (void (*)())-1
#define SIG_DFL(void (*)())0
#define SIG_IGN(void (*)())1

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

static void sig_usr(int sign);           //信号处理函数

int main(void)
{
    if(signal(SIGUSR1,sig_usr)==SIG_ERR) //返回SIG_ERR说明系统不支持SIGUSR1
        printf("cant catch SIGUSR1 \n");
    if(signal(SIGUSR2,sig_usr)==SIG_ERR)
        printf("cant catch SIGUSR2 \n");
    while(1);//等待信号被捕获
    return 0;
}

static void sig_usr(int signo)//信号处理函数
{
    if(signo==SIGUSR1)
        printf("received SIGUSR1\n");
    else if(signo==SIGUSR2)
        printf("received SIGUSR2\n");
    else
        printf("received signal %d \n",signo);

}

我们可以在后台运行此程序,并使用kill命令向其发送信号:
这里写图片描述

———-重点内容
1. 启动程序
当执行一个程序时,所有信号的状态都是系统默认或忽略。通常所有信号都被设置为它们的默认动作,除非调用exec的进程忽略该信号。确切的讲,exec函数将原先设置为要捕捉的信号都更改为默认动作,其他信号状态则不变。(因为原先进程的信号处理函数的地址在新的程序中已经没有意义了。)
从signal函数可以看出,不更改信号的处理方式就不能确定信号的当前处理方式。在后面,我们将说明sigaction函数,可以确定一个函数的处理方式,而无需改变它。
2. 进程创建
当一个进程调用fork时,其子进程继承父进程的信号处理方式。因为子进程在开始时复制了父进程内存映像,所有信号捕捉函数的地址在子进程中是有意义的。

10.4 不可靠的信号

早期的某些实现,信号是不可靠的。

10.5 被中断的系统调用

早期的UNIX系统的一个特性是:如果进程在执行一个低速系统调用而阻塞期间捕获到一个信号,则该系统调用就被中断不再继续执行。该系统调用返回出错,其errno设置为EINTR。(这里要分清系统调用和函数。当捕捉到某个信号时,被中断的是内核中执行的系统调用。)
为了支持这种特性,将系统调用分为两类:低速系统调用其他系统调用。低速系统调用是可能会使进程永远阻塞的一类系统调用,包括:

  • 如果某些类型的文件的数据不存在,则读操作可能会使调用者永远阻塞;
  • 如果这些数据不能被相同的类型文件立即接受,则写操作可能会使调用者永远阻塞;
  • pause函数和wait函数;
  • 某些ioctl操作;
  • 某些进程间通信函数。
    可以使用中断系统调用的一个例子是:如果已经进程启动了读终端操作,而该终端设备的用户却离开终端很长时间。这种情况下,进程可能被一直阻塞。

被中断的系统调用必须显式的处理出错返回。例如:

again:
    if((n=fread(fd,buf,BUFSIZE)<0)){
    if(errno==EINTER)
    goto again;
    }

在系统调用被信号中断后我们希望重新启动它。
为了帮助应用程序不必处理被中断的系统调用,4.2BSD引进了某些被中断
的系统调用自动重启。自动重启的系统调用包括:ioctl、read、readv、write、writev、wait和waitpid。
某些应用程序并不希望这些函数被中断后重启,为此4.3BSD允许进程基于每个信号禁用此功能。

10.6 可重入函数

SUS说明了在信号处理程序中保证调用安全的函数。这些函数时可重入的并被称为是异步信号安全的(async-signal safe)。除了可重入外,在信号处理操作期间,它会阻塞任何会引起不一致信号的发送。异步信号安全的函数如下:

这里写图片描述

以下形式的函数是不可重入的

  • 使用了静态数据结构;
  • 调用了malloc或free;
  • 是标准IO函数;标准IO库很多实现都以不可重入方式使用全局数据结构。虽然很多时候信号处理程序也调用了printf函数,但这并不保证产生所期望的结果,信号处理程序可能中断主程序中的printf函数调用。
    注意:如果即使使用以上可重入函数,由于每个线程只有一个errno变量,所以信号处理程序可能会修改原先值。因此,作为通用规则,应该在信号处理程序调用可重入函数之前保存errno值,再调用后恢复errno。

10.7 SIGCLD语义

SIGCLD和SIGCHLD这两个信号很容易被混淆。

  • SIGCLD是System V的一个信号名。
  • SIGCHLD是BSD的信号名。
    BSD的SIGCHLD信号语义为:子进程状态改变后产生此信号,父进程需要调用一个wait函数已检测发生了什么。
    System V的就不说了。

10.8 可靠信号术语和语义

我们需要先定义一些在讨论信号时会用到的术语。
首先,当造成信号的事件发生时,为进程产生一个信号(或向进程发送一个信号)。事件可以是硬件异常(如除以0)、软件条件(如alarm定时器超时)、终端产生的信号或调用kill函数。
当一个信号发生时,内核通常在进程表中以某种形式设置一个标志。在信号产生(generation)和递送(delivery)之间的时间间隔内,称信号是未决的(pending)
进程可以选用“阻塞信号递送”。如果为进程产生了一个阻塞的信号,而且对该信号的动作是系统默认动作或捕捉该信号,则为该进程将此信号保持为未决状态,直到该进程对此信号解除了阻塞,或者将对此信号的动作更改为忽略。(如果信号是阻塞递送的,则不调用处理函数,直到解除阻塞或忽略该信号)
内核在信号递送时(而不是在信号产生时)决定对它的处理方式。于是进程在信号达到前仍可以改变对信号的动作。进程调用sigpending函数来判断哪些信号是设置为阻塞并处于未决状态的。


如果系统实现支持POSIX.1实时扩展,那么信号多次递送给一个进程,就会排队。如果不支持,那么这些信号值递送一次。

10.9 函数kill和raise

kill函数将信号发送给进程或进程组。
raise函数则允许进程向自身发送信号。

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

raise(signo)等价于kill(getpid(),signo);
kill的pid参数有以下4种不同的情况:

  • pid>0 将该信号发送给进程ID为pid的进程。
  • pid==0 将该信号发送给与发送进程属于同一进程组的所有进程(这些进程 的进程组ID等于发送进程的进程组ID),而且发送进程具有权限向这些进程发送信号。
  • pid<0 将该信号发送给其进程组ID等于pid绝对值,而且发送进程具有权限向其发送信号的所有进程。
  • pid==-1 将该信号发送给发送进程有权限向他们发送信号的所以进程。

10.10 函数alarm和pause

使用alarm函数可以设置一个定时器(闹钟时间),在将来的某个时刻定时器会超时,当定时器超时时,产生SIGALRM信号。如果忽略或不捕捉此信号,则其默认动作是终止调用该alarm函数的进程。

#include <unistd.h>
unsigend int alarm(unsigend int seconds);//返回上次alarm未超时的剩余时间。如果second为0,则取消上次为超时的闹钟。

pause函数使调用进程挂起直至捕捉到一个信号。

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

只有执行了一个信号处理程序并从其返回时,pause才返回。在这种情况下,pause返回-1,errno设置为EINTR.

10.11 信号集

我们需要一个能表示多个信号-信号集(signal set)的数据类型。我们将在sigprocmask类函数中使用这种数据类型,以便告诉内核不允许发生该信号集中的信号
POSIX.1 定义数据类型sigset_t以包含一个信号集,并定义了下列5个处理信号集的函数:

#include <signal.h>

int sigemptyset(sigset_t *set); //初始化set,清除所有信号。必须初始化一次,以屏蔽不同系统之间的差异。
int sigfillset(sigset_t *set); //初始化set,添加所有信号。

int sigaddset(sigset_t *set,int signo); //添加一个指定的信号。
int sigdelset(sigset_t *set,int signo); //删除一个指定的信号。
int sigismember(const sigset_t *set ,int signo); //判断一个信号是否在信号集中。

10.12 函数sigprocmask

一个进程的信号屏蔽字规定了当前阻塞而不能递送给该进程信号集
函数sigprocmask函数可以检测或更改,或同时进行检测和更改进程的信号屏蔽字:

#include <signal.h>

int sigprocmask(int how,const sigset_t *restrict set,sigset_t *restrict oset);
  • 若oset是非空指针,那么进程的当前信号屏蔽字通过oset返回。
  • 若set是一个非空指针,则参数how指示如何修改当前信号屏蔽字。
  • 如果set是个空指针,则不改变进程的信号屏蔽字,how的值也无意义。
how 说明
SIG_BLOCK 该进程新的信号屏蔽字是当前信号屏蔽字和set指向信号集的并集。set包含了希望阻塞的附加信号
SIG_UNBLOCK 该进程新的信号屏蔽字是当前信号屏蔽字和set指向信号集的补集的交集。set包含了希望解除阻塞的附加信号
SIG_SETMASK 该进程新的信号屏蔽是set指向的值

在调用sigprocmask后如果有任何未决的、不再阻塞的信号,则在sigprocmask返回前,至少将其中之一递送给该进程。
sigprocmask是仅为单线程进程定义的。多线程中信号的屏蔽使用另一个函数。

10.13 函数sigpending

sigpending函数返回一信号集,对于调用进程而言,其中各信号是阻塞不能递送的,因而也一定是当前未决的。该信号集通过set参数返回。

#include <signal.h>
int sigpending(sigset_t *set);

 static void sig_quit()
 {
     printf("caught SIGQUIT. \n");
     if(signal(SIGQUIT,SIG_DFL)==SIG_ERR)
     printf("cant reset SIGQUIT. \n");
 }

 int main(void)
 {
     sigset newmask, oldmask, pendmask;
     if(signal(SIGQUIT,sig_quit)==SIG_ERR)//注册SIGQUIT信号处理函数
        printf("cant catch SIGQUIT. \n");    
    /*
    阻塞SIGQUIT信号并保存当前信号屏蔽字
    */
    sigemptyset(&newmansk);  //初始化newmask信号集
    sigaddset(&newmansk,SIGQUIT);  //新增SIGQUIT信号
    if(sigprocmask(SET_BLOCK,&newmask,&oldmask)<0)  //阻塞newmask信号集
        printf("SIG_BLOCK error. \n");
    sleep(5);
    if(sigpending(&pendingmask)<0)  //读取当前未递送信号
     printf("pending error. \n");    
    if(sigismember(&pendmaks,SIGQUIT))  //判断当前未递送信号是否包括SIGQUIT信号
     printf("SIGQUIT pending. \n"); 
    /*
    恢复没有屏蔽SIGQUIT的信号屏蔽字
    */
    if(sigprocmask(SET_SETMASK,&oldmansk,NULL)<0) //恢复信号屏蔽字设置
        printf("SIG_SETMAKS error. \n");     
    printf("SIGQUIT unblocked. \n");
    sleep(5);
    exit(0);
 }

我们执行此程序:

$ ./a.out
^\          //产生信号一次(5s内)
SIGQUIT pending     //从sleep返回后
caught SIGQUIT      //在信号处理程序中捕获(sigprocmask(SET_SETMASK,&oldmansk,NULL)返回前)
SIGQUIT unblocked   //从sigprocmask返回后
^\Quit(coredump)    //再次产生信号

$ ./a.out
^\^\^\^\^\^\        //产生多次信号(5s内)
SIGQUIT pending     //从sleep返回后
caught SIGQUIT      //在信号处理程序中捕获(sigprocmask(SET_SETMASK,&oldmansk,NULL)返回前)
SIGQUIT unblocked   //从sigprocmask返回后
^\Quit(coredump)    //再次产生信号

shell发现其子进程异常终止时输出QUIT(coredump)信息。在第二次运行程序时,在休眠期间使SIGQUIT信号产生了多次,但解除对该信号的阻塞后,只向进程传递一次SIGQUIT信号。从中可以看到在此系统上没有将信号进行排队。

10.14 函数sigaction

sigaction函数的功能是检查或修改(或检查并修改)与指定信号相关联的处理动作。此函数取代了UNIX早期版本使用的signal函数。在本节末尾使用sigaction函数实现了signal。

#include <signal.h>
int sigaction(int signo,const struct sigaction *restrict act,struct sigaction *restrict oact);
  • 其中参数signo是要检测或修改具体动作的信号编号。
  • 若act非空指针,则要修改其动作。
  • 若oact非空指针,则系统经由oact指针返回该信号的上一个动作。
struct sigaction{
    void (*sa_handler)(int);//信号处理函数,或SIG_IGN或SIG_DFL
    sigset_t sa_mask;//信号集屏蔽字
    int sa_flags;//信号选项
    void (*sa_sigaction)(int,siginfo_t *,void *);
};

当更改信号动作时,如果sa_handler字段包括一个信号捕捉函数的地址(不是常量SIG_IGN或SIG_DFL),则sa_mask字段说明了一个信号集,在调用该信号捕捉函数之前,这一信号集要加到进程的信号屏蔽字中。仅当从信号捕捉函数返回时再将进程的信号屏蔽字恢复为原先值。
这样在调用信号处理程序时就能阻塞某些信号。在信号处理程序被调用时,操作系统建立的新信号屏蔽字包括正被递送的信号。因此保证了在处理一个给定信号时,如果这种信号再次发生,那么它会被阻塞到对前一个信号处理结束为止。
若同一信号多次发生,通常并不将它们加入队列,所以如果在某种信号被阻塞时,这种信号发生了多次,那么对这种信号解除阻塞后,其信号处理函数通常只会被调用一次。

act结构的sa_flag字段指定对信号进行处理的各个选项。以下列出了这些选项的意义:

选项 说明
SA_INTERRUPT 由此信号中断的系统调用不自动重启动。
SA_NOCLDSTOP 若signo是SIGCHLD,当子进程停止,不产生此信号;当子进程停止后继续运行,不产生此信号;当子进程终止时产生此信号。
SA_NOCLDWAIT 若signo是SIGCHLD,当调用进程的子进程终止时,不创建僵死进程。若调用进程随后调用wait,则阻塞到它所有子进程都终止,此时返回-1,errno设置为ECHILD。
SA_NODEFER 当捕捉到此信号时,在执行器信号捕捉函数时,系统不自动阻塞此信号(除非sa_mask包括了此信号)
SA_NOSTACK 若signalstack已声明了一个替换栈,则此信号递送给替换栈上的进程。(没懂)
SA_RESETHAND 在此信号捕捉函数的入口处,将此信号的处理方式重置为SIG_DFL,并清除SA_SIGINFO标志。
SA_RESTART 被此信号中断的系统调用自动重启
SA_SIGINFO 此选项对信号处理程序提供了附加信息:一个指向siginfo结构的指针以及一个指向进程上下文标识符的指针。

sa_sigaction字段是一个替代的信号处理程序,在sigaction结构中使用了SA_SIGINFO标志时,使用该信号处理程序。
若使用了SA_SIGINFO标志,那么按这种方式调用信号处理程序:void (*sa_sigaction)(int,siginfo_t *,void *);
这里的siginfo结构包含了信号产生原因的有关信息。

struct siginfo{
int si_signo;  //信号编号
int si_erron;  //包含错误编号,它对应于造成信号产生的条件
int si_code;   //附加信息
pid_t si_pid;  //发送的进程ID
uid_t si_uid;  //发送的用户ID
void *si_addr; //导致故障的根源地址,该地址可能不准
int si_status; //退出号或信号编号
union sigval si_value;
};

sigval联合包含以下字段:

int sival_int;
void *sival_ptr;

应用程序在传递信号时,在si_value.sival_int中传递一个整型数或在si_value.sival_ptr中传递一个指针值。
(这一节不太懂)

10.15 函数sigsetjmp和siglognjmp

7.10节说明了用于非局部转移的setjmp和longjmp函数。
在信号处理程序中经常调用longjmp函数以返回到程序的主循环中,而不是从该程序返回。但是longjmp有一个问题。当捕捉到一个信号时,进入信号捕捉函数,此时当前信号被自动地加到进程的信号屏蔽字中。这阻止了后来产生的这种信号中断该信号处理程序。如果在信号处理程序中使用longjmp跳出信号处理程序,那么对此进程的信号屏蔽字会发生什么,不同的实现处理方法并不相同。
因此POSIX.1并没有指定setjmp和longjmp对信号屏蔽字的作用,而是定义了两新函数sigsetjmp和siglongjmp。在信号处理程序中进行非局部转移时应当使用这两个函数。

#include <setjmp.h>
int simsetjmp(sigjmp_buf env,int savemask);//直接调用返回0;从siglongjmp调用返回非0
void siglongjmp(sigjmp_buf env,int val);

这两个函数和setjmp、longjmp之间的唯一区别是sigsetjmp增加了一个参数。如果savemask非0,则sigsetjmp在env中保存当前信号屏蔽字。掉siglongjmp时,如果带非0 sacemask的sigsetjmp调用已经保存了env,则siglongjmp从其中恢复保存的信号屏蔽字。
当调用一个信号处理程序时,被捕捉到的信号加到进程的当前信号屏蔽字中。当从信号处理程序返回时,返回原来的屏蔽字。

10.16 函数sigsuspend

上面已经说明,更改进程的信号屏蔽字可以阻塞所选择的信号,或解除对它们的阻塞。使用这种技术可以保护不希望由信号中断的代码临界区。
函数sigsuspend可以挂起进程,直到其信号屏蔽字之外的信号被捕获并从其信号处理程序返回。返回之后系统的信号屏蔽字恢复为调用sigsuspend之前的信号屏蔽字。

#include <signal.h>
int sigsuspend(const sigset_t *sigmask);
#include <stdio.h>
#include <signal.h>

static void sig_int(int sign)
{
    printf("in sig_int handler. \n");
}

static void sig_usr1(int sign)
{
    printf("in sig_usr1 handler. \n");
}


int main(void)
{

    sigset_t newmask,oldmask,waitmask;
    printf("program start.\n");

    if(signal(SIGINT,sig_int)==SIG_ERR) printf("signal err. \n");
    if(signal(SIGUSR1,sig_usr1)==SIG_ERR) printf("signal err. \n");

    sigemptyset(&waitmask);
    sigaddset(&waitmask,SIGUSR1);   //将SIGUSR1信号添加到信号屏蔽字waitmask

    sigemptyset(&newmask);
    sigaddset(&newmask,SIGINT);     //将SIGINT信号添加到信号屏蔽字newmask

//阻塞屏蔽字中的信号,此后屏蔽了信号SIGUSR1
    if(sigprocmask(SIG_BLOCK,&waitmask,&oldmask)<0) printf("SIG_BLOCK err. \n");

   //挂起进程,直到屏蔽字以外的信号被捕获到并从其信号处理程序返回后,进程继续运行
    if(sigsuspend(&newmask)!= -1) printf("sigsuspend error. \n");
    printf("after return sigsuspend . \n");
  //此时阻塞了信号SIGINT
    pause();

    return 0;
}

这里写图片描述


sigsuspend的另一种应用是等待一个信号处理程序设置一个全局变量。例如:

static volatile sig_atmoic_t sigflag;

int sig_proc(int signo)
{
    if(signo==SIGINT)
        printf("interrupt. \n");
    else if(signo==SIGQUIT)
        sigflag=1;
}

int main(void)
{
    sigset_t newmask,oldmask,zeromask;
    if(signal(SIGINT,sig_proc)==SIGERR)printf("sig error.\n");
    if(signalSIGQUIT,sig_procSIGERR)printf("sig error.\n");
//intilization sigset 
sigemptyset(&zeromask);
sigemptyset(&newmask);
sigaddset(&newmask,SIGQUIT);

//block SIGQUIT and save current signal mask
if(sigprocmask(SIG_BLOCK,&newmask,&oldmask)<0) printf("sigprocmask error. \n");
//sigsuspend zeromask and release SIG_QUIT
    while(sigflag==0)//这里会一直循环,直到sigflag=1。也就是SIGQUIT信号,程序继续往下走。
        sigsuspend(&zeromask);
    sigflag=0;
if(sigprocmask(SIG_SETMASK,&oldmaks,NULL)<0) printf("sigprocmask error. \n");
}

10.17 函数abort

前面已经提及abort函数的功能是使程序异常终止。

#include <stdlib.h>
void abort(void);

此函数将SIGABRT信号发送给调用进程(不应忽略此信号)。调用abort将向主机环境递送一个未成功终止的通知,其方法是raise(SIGABRT)函数。
ISO C要求若捕捉到此信号而且相应信号处理程序返回,abort仍不会返回到其调用者。如果捕捉到此信号,则信号处理程序不能返回的唯一方法是它调用exit、_exit、longjmp或siglongjmp。POSIX.1也说明abort并不理会进程对此信号的阻塞和忽略。
让进程捕捉SIGABRT的意图是:在进程终止之前由其执行所需的清理操作。如果进程并不在信号处理程序中终止自己,POSIX.1声明当信号处理程序返回时,abort终止该线程。
ISO C针对此函数的规范将是否冲洗数据留个实现决定。
POSIX.1则要求当调用abort终止进程前,则它对所有打开的标准IO流执行冲洗操作。

10.18 函数system

POSIX.1要求system忽略SIGINT和SIGQUIT,阻塞SIGCHLD.


ed编辑器不熟悉,看的晕晕的。以后再说。

10.19 函数sleep、nanosleep和clock_nanosleep

#include <unistd.h>
unsigned int sleep(unsigned int seconds);

此函数使调用进程挂起直到满足下面两个条件之一:

  • 已经经过了seconds所指定的墙上时钟时间。
  • 调用进程捕捉到一个信号并从信号处理程序返回。
    如同alarm函数一样,由于其他系统活动,实际返回时间比所要求的会迟一些。
    在第一种情形下,返回值是0;
    在第二种情形下,由于捕捉到某个信号sleep提早返回时,返回值是未休眠完的秒数(所要求的时间减去实际休眠时间)。

nanosleep函数与sleep函数类似,但提供了纳秒级的精度。

#include <unistd.h>
int nanosleep(const struct timespec *reqtp,struct timespec *remtp);

这个函数调用挂起进程,直到要求的时间已经超时或者某个信号中断了该函数。
reqtp参数用秒和纳秒指定了需要休眠的时间长度。
remtp参数指向的timespec结构就会被设置为未休眠完的时间长度。如果对未休眠完的时间并不感兴趣,可以把该参数设置为NULL
如果系统并不支持纳秒这一精度,要求的时间就会取整。因为nanosleep函数并不涉及产生任何信号,所以不需要担心与其他函数的交互。


随着多个系统时钟的引入,需要使用相对于特定时钟的延时时间来挂起调用线程clock_nanosleep函数提供了这种功能。

#include <time.h>
int clock_nanosleep(clockid_t clock_id,int flags,const struct timespec *reqtp,struct timespec *remtp);
  • clock_id参数指定了计算延时时间基于的时钟;
  • flag参数用于控制延迟是相对的还是绝对的,flags为0:表示休眠时间是相对的。如果flags是TIMER_ABSTIME:表示休眠时间是绝对的,希望休眠到某个特定的时间。
  • reqtp和remtp参数和nanosleep中的相同。
    除了出错返回,调用clock_nanosleep(CLOCK_REALTIME,0,reqtp,remtp);nanosleep(reqtp,remtp);作用是相同的。

使用绝对时间改善了延时精度。因为相对时间取决于系统调度。

10.20 函数sigqueue

有些系统开始增加对信号排队的支持。除了信号排队以外,这些扩展允许应用程序在递交信号时传递更多的信息。这些信息嵌入在siginfo结构中。 除了系统提供的信息,应用程序还可以向信号处理程序传递整数或者包含更多信息的缓冲区指针。
使用排队信号必须做以下几个操作:

  • 使用sigaction函数安装信号处理程序时指定SA_SIGINFO标志。如果没有给出这个标志,信号会延迟,但信号是否进入队列要取决于具体实现。
  • 在sigaction结构的sa_sigaction成员中提供信号处理程序。
  • 使用sigqueue函数发送信号。
#include <signal.h>
int sigqueue(pid_t pid,int signo,const union sigval value);

sigque函数只能把信号发送给单个进程,可以使用value参数向信号处理程序传递整数和指针,除此之外,sigqueue函数与kill函数类似。

10.21 作业控制信号

所有的信号中,POSIX.1认为有以下6个与作业控制有关。

  • SIGCHLD 子进程已经停止或终止。
  • SIGCONT 如果进程已经停止,则使其继续运行。
  • SIGSTOP 停止信号(不能被捕捉或忽略)。
  • SIGTSTP 交互式停止信号。
  • SIGTTIN 后台进程组成员读控制终端。
  • SIGTTOU 后台进程组成员写控制终端。
    出了SIGCHLD以外,大多数应用程序并不处理这些信号,交互式shell通常会处理这些信号的所有工作

  • 当键入挂起字符(Ctrl+Z)时,SIGTSTP信号被送至前台进程组的所有进程。

  • 当我们通知shell在前台或后台恢复一个作业时,shell向该作业中所有进程发送SIGCONT信号。
  • 与此类似,如果一个进程递送了SIGTTIN或SIGTTOU信号,则根据系统默认的方式,停止此进程,作业控制shell了解到这一点后就通知我们。

10.22 信号名和编号

本节介绍如何在信号编号和信号名之间进行映射。某些系统提供数组
extern char *sys_siglist[];
数组下标是信号编号,数组中的元素是指向信号名字符串的指针。
可以使用psignal函数打印与信号编号对应的字符串。

#include <signal.h>
void psignal(int signp,char *msg);

如果在sigaction信号处理程序中有siginfo结构,可以使用psiginfo函数打印信号信息。

#include <string.h>
void *strsignal(int signp); //说明描述信号的字符串是全局变量。

给出一个信号编号,strsignal将返回描述该信号的字符串。引用程序可以用该字符串打印关于接收到信号的出错消息。

10.23 小结

信号用于大多数复杂的应用程序中。理解进行信号处理的原因和方式对于高级UNIX编程极为重要。
本章首先说明了早起信号实现的问题以及它们是如何显现出来的

  • 介绍了POSIX.1的可靠信号概念以及所有相关的信号函数
  • 在此基础上提供了abort、system和sleep函数的实现
  • 最后以观察分析作业控制信号以及信号名和信号编号之间的转换结束。
发布了55 篇原创文章 · 获赞 13 · 访问量 3万+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章