《UNUX环境高级编程》(10)信号

1、引言

  • 信号是软件中断,提供了一种处理异步事件的方法。例如:终端用户键入中断键,会通过信号机制停止一个程序,或及早终止管道的下一个程序。

2、信号概念

  • 每个信号都有一个名字,以3个字符SIG开头。如SIGABRT是终止信号,进程调用abort函数产生这种信号。
    void abort(void);
    
    • abort()首先解除了对SIGABRT(6)信号的阻止和忽略,然后为调用进程发送该信号(就像调用了 raise(SIGABRT)一样)。 导致进程的非正常终止,除非SIGABRT 信号被捕获,并且信号处理函数没有返回(调用exit_exit_Exitlongjmpsiglongjmp使信号处理函数没有返回)。
    • 如果abort()函数导致进程终止,则关闭和刷新所有打开的文件流。
  • Linux支持63种信号,不存在编号为0的信号(即为空信号),kill函数对信号编号0有特殊应用。1 ~ 31的信号为传统UNIX支持的信号,是不可靠信号(非实时的),编号为32 ~ 63的信号是后来扩充的,称做可靠信号(实时信号)。不可靠信号和可靠信号的区别在于前者不支持排队,可能会造成信号丢失,而后者不会。

2.1 产生信号的条件

  • 当用户按某些终端键时,引发终端产生的信号。如在终端上按deleteCtrl+C键,通常产生中断信号SIGINT
  • 硬件产生异常信号:除0、无效的内存引用等。因为这些条件通常由硬件检测到,并且通知内核。然后内核为该进程产生适当信号。
  • 进程调用kill(2)函数将任意信号发送给另一个进程或进程组:接收信号进程和发送信号进程的所有者必须相同,或发送方必须是超级用户root
  • kill(1)命令将信号(默认是SIGTERM)发送给进程。此命令只是kill函数的接口,可用此命令终止一个失控的后台程序。
  • 当检测到某种软件条件发生时将产生信号通知相关进程。如SIGURG(在网络连接上传来带外的数据)、SIGPIPE(在管道的读进程已终止后,一个进程写此管道)、SIGALRM(进程设置的定时器已经超时)等

2.2 对信号的处理方式

  • 忽略此信号

    • 除了两种信号SIGKILLSIGSTOP绝对不能忽略以外,大多数信号都可使用这种方式处理。
    • SIGKILLSIGSTOP不能被忽略的原因是,它们向内核和超级用户提供了使进程终止或停止的可靠方法(可以理解为终止、停止进程的终极方法)。
    • 并且如果忽略某些由硬件异常(如除0、非法内存引用)产生的信号,会发生未定义行为。
  • 捕捉该信号

    • 通知内核在某种信号发生时,调用一个用户函数。同样SIGKILLSIGSTOP不能被捕捉
  • 执行系统默认动作
    《UNUX环境高级编程》(10)信号_第1张图片

    • 对大多数信号的系统默认动作是终止该进程
    • 注意终止+core表示在进程当前工作目录的core文件中复制了该进程内存映像(默认文件名为core,这种行为即coredump),大多数Unix系统调试程序(如gdb)都使用core文件检查进程终止时的状态。
    • 注意硬件故障对应于具体定义的硬件故障,需要通过对应操作系统手册查看这些信号对应于哪些错误
  • Core Dump注解

    • 当程序运行的过程中异常终止或崩溃,操作系统会将程序当时的内存状态记录下来,保存在一个文件中,这种行为就叫做Core Dump(中文有的翻译成“核心转储”)。我们可以认为 core dump 是“内存快照”,但实际上,除了内存信息之外,还有些关键的程序运行状态也会同时 dump 下来,例如寄存器信息(包括程序指针、栈指针等)、内存管理信息、其他处理器和操作系统状态和信息。core dump 对于编程人员诊断和调试程序是非常有帮助的,因为对于有些程序错误是很难重现的,例如指针异常,而 core dump 文件可以再现程序出错时的情景。

    • 如果没有进行core dump 的相关设置,默认是不开启的。可以通过ulimit -c查看是否开启。如果输出为0,则没有开启,需要执行ulimit -c unlimited开启core dump功能。

  • 不产生core文件的条件:

    • ulimit -c查看core文件有没有限制大小。如果是0则说明禁止了core文件产生
    • 程序设置了用户id(即调用setuid),但当前用户并非该程序文件的所有者
    • 程序设置了组id(即调用setgid),但当前用户并非该程序文件的组所有者
    • 用户没有当前目录或指定core文件产生目录的写权限
    • 文件已存在,用户对该文件没有写权限
    • core文件太大,磁盘空间不足

2.3 各种信号详细信息

  • SIGABRT

    • 调用abort函数产生此信号,进程异常终止。
  • SIGIOT

    • 表示发生了一个具体定义的硬件故障。Linux中定义SIGIOTSIGABRT有相同值
      #define	SIGIOT		SIGABRT
      
  • SIGALRM

    • 调用alarmsetitimer函数设置的定时器、间隔时间超时产生此信号。
  • SIGBUS

    • 表示发生了一个具体定义的硬件故障。当出现某种类型内存故障时,常产生此种信号
  • SIGCHLD

    • 子进程终止或停止时,SIGCHLD信号发送给父进程。系统默认会忽略此信号。如果父进程希望得知子进程的这种状态改变,那么通常在信号捕捉函数中调用wait等函数获取子进程pid及终止状态。
  • SIGCONT

    • 用于作业控制。如果收到此信号的进程处于停止状态,则系统默认动作是该进程继续运行;否则忽略此信号
  • SIGEMT

    • 表示发生了一个具体定义的硬件故障。
  • SIGFPE

    • 算术运算异常,如除0、浮点溢出等
  • SIGHUP:挂断信号。

    • 终端接口检测到一个连接断开,则将此信号送给与该终端相关的控制进程(会话首进程)。
    • session首进程退出时,该信号被发送到该session中的前台进程组和后台进程组中的每一个进程
    • 若进程的退出,导致一个进程组变成了孤儿进程组,新的孤儿进程组中处于停止(stopped)状态的每一个进程都会收到挂断(SIGHUP)信号,接着又收到继续(SIGCONT)信号。
    • 系统对SIGHUP信号的默认处理是终止收到该信号的进程。所以若程序中没有捕捉该信号,当收到该信号时,进程就会退出。
  • SIGILL

    • 进程执行一条非法硬件指令
  • SIGINT:中断信号

    • 用户按中断键(deleteCtrl+C),产生此信号发送至前台进程组中的每一个进程。当一个进程失控或者在终端产生大量不需要输出时,常通过此命令终止它。
  • SIGKILL:

    • 这是两个不能被阻塞、捕捉或忽略的信号中的一个,向内核和超级用户提供了使进程终止的可靠方法
  • SIGPIPE

    • 管道读进程已经终止后,写进程写入管道产生此信号。
    • 当类型为SOCK_STREAMsocket已不再连接时,进程写入该套接字也产生此信号。
  • SIGPOLL

    • 当在一个可轮询设备上发生一个特定事件时产生此信号(如当系统发现有东西需要你读的时候,就发个SIGPOLL信号来通知)。在未来可能将此信号移除。
  • SIGIO

    • 指示发生一个异步I/O事件。Linux中定义SIGIOSIGPOLL有相同的值,默认行为是终止该进程。
      #define	SIGIO		SIGPOLL
      
  • SIGPROF

    • setitimer函数设置的梗概统计间隔定时器超时产生此信号。在未来可能将此信号移除
  • SIGPWR

    • 电源故障信号。主要用于具有不间断电源的系统。如果电源失效,系统依靠蓄电池继续运行。但是如果蓄电池也将不能工作,此时发送SIGPWR信号。通常是接到蓄电池电压过低信息的进程将SIGPWR发送给init进程,然后由init处理停机操作。
    • Linux对SIGPWR的默认动作是终止相关进程。
  • SIGQUIT:退出信号

    • 用户在终端上按退出键(Ctrl+\)产生此信号,并发送给前台进程组的所有进程。此信号不仅终止前台进程组,还产生一个core文件。
  • SIGSEGV

    • 进程进行了一次无效的内存引用(比如访问了一个未经初始化的指针),或发生段错误
  • SIGSTOP

    • 作业控制信号,它停止一个进程。同SIGKILL,该信号不能被阻塞、忽略和捕获
  • SIGSYS

    • 指示一个无效的系统调用。
  • SIGTERM

    • 是kill命令发出的默认信号(系统默认终止信号)。相比于SIGKILLSIGTERM可以被捕获或忽略,因此允许让程序有机会在退出之前做好清理工作,从而优雅地终止
  • SIGTRAP

    • 表示发生了一个具体定义的硬件故障。通常使用此信号将控制转移至调试程序
  • SIGTSTP

    • 交互停止信号。当用户在终端按挂起键(Ctrl+Z)时,将该信号发送到前台进程组中的所有进程加粗样式SIGTSTPSIGSTOP都是使进程暂停(都使用SIGCONT让进程重新激活)。唯一的区别是SIGSTOP不可以捕获和忽略,而SIGTSTP可以。
  • SIGTTIN

    • 当一个后台进程组中的进程试图读控制终端时收到此信号,并使该进程暂停。注意,如果读进程属于孤儿进程组,那么read控制终端操作返回出错,不产生此信号,errno设置为EIO
  • SIGTTOU

    • 如果禁止后台作业向控制终端写,此时当一个后台进程组进程试图写控制终端时收到此信号,并使该进程暂停。注意,如果写进程属于孤儿进程组,则写操作返回出错,不产生此信号,errno设置为EIO
    • 除此之外,tcsetattrtcsendbreaktcdraintcflushtcflow以及tcsetpgrp也能产生SIGTTOU信号。
  • SIGURG

    • 通知进程发生一个紧急情况(用于socket编程)。在网络连接上接到带外的数据时,可选择地产生此信号。
    • 带外数据
      • 带外数据用于迅速告知对方本端发生的重要的事件。它比普通的数据(带内数据)拥有更高的优先级,不论发送缓冲区中是否有排队等待发送的数据,它总是被立即发送。带外数据的传输可以使用一条独立的传输层连接,也可以映射到传输普通数据的连接中。
  • SIGUSR1

    • 用户定义的信号
  • SIGUSR2

    • 另一个用户定义的信号,与SIGUSR类似
  • SIGVTALRM

    • 当一个由setitimer函数设置的虚拟间隔时间超时产生此信号
  • SIGWINCH

    • 内核维护与每个终端或伪终端相关联窗口的大小。进程可以用ioctl得到或设置窗口大小。如果用ioctl设置窗口大小命令更改了窗口大小,则内核将该信号发送至前台进程组。
  • SIGXCPU

    • 如果进程超过其软CPU时间限制,产生此信号。
  • SIGXFSZ

    • 如果进程超过其软文件长度限制,产生此信号。

3、函数signal

  • 设置调用进程收到指定信号时的动作(忽略、使用系统默认动作或捕获)

    typedef void (*sighandler_t)(int);
    sighandler_t signal(int signum, sighandler_t handler);
    
    • signum参数:信号名,如SIGABRT
    • handler参数
      • SIG_IGN:忽略此信号(不能用于SIGKILLSIGSTOP
        #define	SIG_IGN	 ((__sighandler_t)  1)	/* Ignore signal.  */
        
      • SIG_DFL:系统默认动作
        #define	SIG_DFL	 ((__sighandler_t)  0)	/* Default action.  */
        
      • 指定函数地址(不能用于SIGKILLSIGSTOP):在信号发生时,调用该函数(该函数有一个int形参,即为该信号编号)。称这个操作是捕捉该信号,称此函数为信号处理程序或信号捕捉函数。
    • 返回值:返回之前的信号处理程序的地址,当发生错误时返回 SIG_ERR
      #define	SIG_ERR	 ((__sighandler_t) -1)	/* Error return.  */
      
  • signal函数的一些特点

    • 注意,exec一个程序后,通常所有信号的处理都是忽略或者使用系统默认操作。如果调用exec前对某个信号忽略,则exec后仍为忽略;但是如果调用exec前对某个信号捕获,则exec后对该信号更改为使用默认操作(因为信号捕捉函数的地址在exec的新程序中毫无意义)。
    • fork时,子进程继承父进程的信号处理方式,因为子进程复刻父进程内存映像,因此信号捕捉函数地址在子进程中有效
    • signal函数的一个缺陷:
      • 不改变信号的处理方式就不能确定信号之前的处理方式(根据signal返回值知道之前对于指定信号的处理方式)。因此可以使用sigaction函数确定一个信号的处理方式,而无需改变它
  • 实例:给出一个简单的信号处理程序,它捕获了两个用户定义的信号并打印信号编号。下面提及的pause函数使调用进程在接到一信号前挂起。

    #include "apue.h"
    
    static void	sig_usr(int);	/* one handler for both signals */
    
    int
    main(void)
    {
    	if (signal(SIGUSR1, sig_usr) == SIG_ERR)//handler参数sig_usr指定了信号处理程序的地址
    		err_sys("can't catch SIGUSR1");
    	if (signal(SIGUSR2, sig_usr) == SIG_ERR)//handler参数sig_usr指定了信号处理程序的地址
    		err_sys("can't catch SIGUSR2");
    	for ( ; ; )
    		pause();
    }
    
    static void
    sig_usr(int signo)		/* argument is signal number */
    {
    	if (signo == SIGUSR1)
    		printf("received SIGUSR1\n");
    	else if (signo == SIGUSR2)
    		printf("received SIGUSR2\n");
    	else
    		err_dump("received signal %d\n", signo);
    }
    

    命令行输出

    lh@LH_LINUX:~/桌面/apue.3e/signals$ ./sigusr &   (在后台启动进程)
    [1] 3381 						(作业控制shell打印作业编号和进程ID)
    lh@LH_LINUX:~/桌面/apue.3e/signals$ kill -USR1 3381 (向进程发送SIGUSR1)
    received SIGUSR1
    lh@LH_LINUX:~/桌面/apue.3e/signals$ kill -USR2 3381(向进程发送SIGUSR2)
    received SIGUSR2
    lh@LH_LINUX:~/桌面/apue.3e/signals$ kill 3381	(向进程发送SIGTERM)
    [1]+  已终止               ./sigusr
    
    • 我们使程序在后台运行,并用kill命令将信号发送给它。注意:kill并不指代杀死,只是将一个信号发送给一个进程或进程组。该信号是否终止取决于该信号的类型,以及进程是否安排了捕捉该信号。
    • 实例程序没有捕捉SIGTERM信号,而对该信号的系统默认动作是终止,所以当向该进程发送SIGTERM信号后,该进程就终止。
  • signal函数的一些特点

    • 注意,exec一个程序后,通常所有信号的处理都是忽略或者使用系统默认操作。如果调用exec前对某个信号忽略,则exec后仍为忽略;但是如果调用exec前对某个信号捕获,则exec后对该信号更改为使用默认操作(因为信号捕捉函数的地址在exec的新程序中毫无意义)。

      • 一个例子:对于一个非作业控制shell,当在后台执行一个进程,例如:
        cc main.c &
        
        shell自动将后台程序对中断和退出信号的处理方式设置为忽略。于是,当按下中断字符时就不会影响到后台进程。如果没有做这样的处理,那么当按下中断字符时,它不但终止前台进程,也终止所有后台进程。
    • fork时,子进程继承父进程的信号处理方式,因为子进程复刻父进程内存映像,因此信号捕捉函数地址在子进程中有效

    • signal函数的一个缺陷

      • 以信号SIGINTSIGQUIT为例,很多捕捉这两个信号的交互程序有下列形式的代码:
        void sig_int(int),sig_quit(int)
        if(signal(SIGINT,SIG_IGN) != SIG_IGN)
        	signal(SIGINT,sig_int);
        if(signal(SIGQUIT,SIG_IGN) != SIG_IGN)
        	signal(SIGINT,sig_quit);
        
        这样处理后,仅当SIGINTSIGQUIT当前未被忽略时,进程才会捕捉它们。
      • 通过上面叙述可知:不改变信号的处理方式就不能确定信号之前的处理方式(根据signal返回值知道之前对于指定信号的处理方式)。因此可以使用sigaction函数确定一个信号的处理方式,而无需改变它。

4、不可靠的信号

  • 在早期UNIX中,信号是不可靠的。意为信号可能会丢失,一个信号发生了但是进程可能一直不知道。同时不具备阻塞信号的能力(不要忽略该信号,在其发生时记住它,然后在进程做好准备时再通知它)。
  • 不可靠信号的主要问题
    • 进程每次处理信号后,就将对信号的响应设置为默认动作。在某些情况下,将导致对信号的错误处理;因此,用户如果不希望这样的操作,那么就要在信号处理函数结尾再一次调用signal(),重新安装该信号。
    • 信号可能丢失,如果在进程对某个信号进行处理时,这个信号发生多次,对后到来的这类信号不排队,那么仅传送该信号一次,即发生了信号丢失。因此,早期unix下的不可靠信号主要指的是进程可能对信号做出错误的反应以及信号可能丢失。
  • Linux支持不可靠信号,但是对不可靠信号机制做了改进:在调用完信号处理函数后,不必重新调用该信号的安装函数(信号安装函数是在可靠机制上的实现)。因此,Linux下的不可靠信号问题主要指的是信号可能丢失。

5、中断的系统调用

  • 系统调用分类
    • 将系统调用分为两类 -> 低速系统调用/其他系统调用
  • 低速系统调用是可能会使进程永远阻塞的一类系统调用
    • 如果某些类型文件(如读管道、终端设备、网络设备)数据不存在,则读操作可能会使调用者永远阻塞
    • 如果这些数据不能被相同类型文件立即接受,则写操作可能会使调用者永远阻塞。
    • 在某种条件发生之前打开某些类型文件,可能发生阻塞
    • pause函数(它使进程挂起直到收到一个信号)和wait函数
    • 某些ioctl操作
    • 某些进程间通信函数
  • 在早期UNIX中,如果进程正在执行低速系统调用而阻塞期间捕捉到一个信号,则该系统调用就被中断不再执行。该系统调用返回出错,errno设置为EINTR(系统调用被中断)。可以理解为一个信号发生了,进程捕捉到它,这意味着已经发生了某种事情,所以是个好机会应当唤醒阻塞的系统调用。
  • 对于处理已经read/write部分数据量的相应系统调用,此时被信号中断,有两种方式(例如read系统调用已经接收并传送数据至应用程序缓冲区,但尚未接收到应用程序请求的全部数据):
    • 认为系统调用失败,errno设置为EINTR
    • (现在使用这一种):允许该系统调用成功返回,返回值是已读写的数据量。
  • 当被信号中断时,下列系统调用支持自动重启动
    • ioctlreadreadvwritewritevwaitwaitpid
  • 有些应用程序不希望这些函数被中断后自动重启动,因此进程可以设置对每个信号禁用此功能。(sigaction函数中只有中断信号的SA_RESTART标志有效,才自动重启动系统调用)在Linux中,当信号处理程序是用signal函数注册时,被中断的系统调用会自动重启动。
    《UNUX环境高级编程》(10)信号_第2张图片
  • 注意,除了低速系统调用,信号也可以中断类似sleep这样的函数:调用sleep函数的线程休眠seconds秒。如果中间有一个未被忽略的信号到达则终止休眠

6、可重入函数

  • 一些问题
    • 如果进程正在执行malloc,在其堆中分配另外的存储空间,而此时由于捕捉到信号而插入执行该信号处理程序,其中又调用malloc会发生什么?
      • 可能会对进程造成破坏。因为malloc通常为它所分配的存储区维护一个链表,而插入执行信号处理程序时进程可能正在更改此链表。
    • 如果进程正在执行getpwnam这种将其结果存放在静态存储单元中的函数,其间插入执行信号处理程序,它又调用这样的函数(如信号处理函数内部又调用getpwnam)会发生什么?
      • 返回给正常调用者的信息可能会被返回给信号处理程序的信息覆盖。
  • 可重入函数
    • 在信号处理程序中保证调用安全的函数,这些函数是可重入的并被称为是异步信号安全的(即为在函数A执行期间中断执行信号处理程序,在信号处理程序中可能再次调用函数A,但是不会造成问题)。这种函数除了可重入外(信号处理程序中再次调用被信号中断的函数),在执行信号处理操作期间会阻塞任何会引起不一致的信号发送。

    • 下列函数都是异步信号安全即可重入
      《UNUX环境高级编程》(10)信号_第3张图片

    • 没有列入上表的函数大多是不可重入的,不可重入函数通常有以下特点

      • 它们使用静态数据结构,重入可能导致结果被覆盖
      • 它们调用mallocfree
      • 它们是标准I/O函数。标准I/O库的很多实现都不以可重入方式而是使用全局数据结构
    • 并且要注意,对于errno,因为信号处理程序可能会修改errno原先值,因此应当在调用前保存errno,在调用后恢复errno

    • 如果应用程序要做更新全局数据结构这样的事情,而同时要捕捉某些信号,而这些信号的处理程序又会引起执行siglongjmp,则在更新这种数据结构时要阻塞此类信号。(因为可能导致这些全局数据结构是部分更新的)

    • 综上所述,在信号处理函数中调用一个非可重入函数,其结果不可预知。因此在信号处理函数中不能调用非可重入函数

7、SIGCLD语义

  • 在Linux平台上,SIGCLD等同于SIGCHLD。在Linux平台的源码中有如下定义。
    #define SIGCLD SIGCHLD
    

8、可靠信号

  • 三种信号状态
    • 信号产生generation
      • 当造成信号的事件发生时,为进程产生一个信号(或向一个进程发送一个信号)。
      • 这些造成信号的事件可以是硬件异常(如除0)、软件条件(如alarm定时器超时)、终端产生的信号或调用kill函数。
      • 当一个信号产生时,内核通常在进程中以某种形式设置一个标志。
    • 信号递送delivery
      • 指信号发送给进程之后,对该信号进行了处理(无论是忽略、捕获还是使用系统默认操作)
    • 信号未决pending
      • 在信号产生和递送的时间间隔内称为未决
  • 阻塞信号递送
    • 进程可以选择对指定信号“阻塞信号递送”。如果为进程产生了一个阻塞的信号,并且对该信号的动作是系统默认或捕捉,则此信号保持为未决的。直到该进程解除对此信号的阻塞,或者将对此信号的动作更改为忽略,内核才会递送一个原来被阻塞的信号给进程(而不是解除阻塞后再产生的信号)。
    • 如果在进程解除对某个信号的阻塞之前这种信号发生了多次,允许系统能够递送该信号一次或多次。如果递送该信号多次,则称这些信号进行了排队
  • 信号屏蔽字
    • 每个进程都有信号屏蔽字(signal mask),它规定当前要阻塞递送到该进程的信号集。对于每一种可能的信号,该屏蔽字中都有一位与之对应。如果该位已设置,则它对应的信号是被阻塞的。

    • 注意,在信号处理函数被调用时,操作系统建立的新信号屏蔽字包含正被递送的信号(即触发本次捕获的信号),信号处理函数返回时再恢复信号屏蔽字。因此保证在处理一个给定信号时,如果该信号再次发生,那么它将被阻塞到前一个信号的处理结束为止

9、kill和raise函数

  • kill函数将信号发送给进程或进程组,raise函数向进程自身发送信号。调用raise(sig)等价于调用kill(getpid(),sig)
    int kill(pid_t pid, int sig);
    int raise(int sig);
    
    • killpid参数
      • pid > 0 :将信号发送给指定进程
      • pid == 0 : 将信号发送给与发送进程属于同一进程组的所有进程,并且发送进程具有权限向这些进程发送信号
      • pid < 0 : 将信号发送给进程组ID等于pid绝对值的所有进程,并且发送进程具有权限向这些进程发送信号
      • pid == -1 : 将信号发送给有权限向它们发送的所有进程。除了进程1(init)
    • 如果sig==0,则说明是空信号,kill仍然执行正常的错误检查但是不发送信号。常被用来确定一个特定进程是否存在。如果向一个不存在的进程发送空信号,kill函数返回-1
  • 发送信号的权限问题
    • 超级用户可以把信号发送给任一进程
    • 非超级用户,则要求发送的实际用户ID或有效用户ID等于接受者的实际用户ID或有效用户ID
    • 但是有一个特例:对于SIGCONT信号,则进程可以将它发送给属于同一会话的任一进程
  • 如果调用kill为调用进程产生信号,并且如果该信号是不被阻塞的,那么在kill函数返回之前,该信号或者其他某个未决、未阻塞信号就被递送给了该进程。

10、函数alarm和pause

  • alarm函数设置一个定时器(秒数),将来某个时刻定时器超时产生SIGALRM信号。如果忽略或不捕捉该信号,默认动作是终止该进程
     unsigned int alarm(unsigned int seconds);
    
    • 注意,每个进程只能有一个闹钟时间,因此调用alarm会覆盖之前的alarm。即如果在调用alarm时上一次为该进程注册的alarm还没有超时,则该闹钟时间的余留值用作本次调用的返回值,并且以前注册的闹钟时间被新值替代。
    • 如果要捕获SIGALRM,必须在alarm调用前安装信号捕获程序。
  • pause函数是一个慢速系统调用,使调用进程挂起直到捕捉到一个信号
    int pause(void);
    
    • 只有执行了信号处理程序并从其返回时,pause才返回。此时pause返回-1errno设置为EINTR
  • 注意,在信号处理函数中使用longjmp函数一定要小心,因为如果该信号中断了其他信号处理函数,那么longjmp将会提早终止这些信号处理函数。
  • 实例:验证如果SIGALRM中断了某个其他信号处理程序,调用longjmp是否会提早终止该信号处理程序。
    #include "apue.h"
    
    unsigned int	sleep2(unsigned int);
    static void		sig_int(int);
    
    int
    main(void)
    {
    	unsigned int	unslept;
    
    	if (signal(SIGINT, sig_int) == SIG_ERR)
    		err_sys("signal(SIGINT) error");
    	unslept = sleep2(5);
    	printf("sleep2 returned: %u\n", unslept);
    	exit(0);
    }
    
    static void
    sig_int(int signo)
    {
    	int				i, j;
    	volatile int	k;
    
    	/*
    	 * Tune these loops to run for more than 5 seconds
    	 * on whatever system this test program is run.
    	 */
    	printf("\nsig_int starting\n");
    	for (i = 0; i < 300000; i++)
    		for (j = 0; j < 4000; j++)
    			k += i * j;
    	printf("sig_int finished\n");
    }
    
    下面是sleep2函数的实现
    #include	
    #include	
    #include	
    
    static jmp_buf	env_alrm;
    
    static void
    sig_alrm(int signo)
    {
    	longjmp(env_alrm, 1);
    }
    
    unsigned int
    sleep2(unsigned int seconds)
    {
    	if (signal(SIGALRM, sig_alrm) == SIG_ERR)
    		return(seconds);
        /*
    		如果不使用setjmp(),则alarm()和pause()之间有一个竞争条件。
    		在一个繁忙的系统中,可能alarm()在调用pause()之前超时,并调用了信号处理程序。
    		如果发生了这种情况,则在调用pause()后,如果没有捕捉到其他信号,调用者将永远被挂起。
    		使用setjmp()可以解决这个问题,即使pause()从未执行,在发生SIGALRM时,sleep2函数也返回
    	*/
    	if (setjmp(env_alrm) == 0) {
    		alarm(seconds);		/* start the timer */
    		pause();			/* next caught signal wakes us up */
    	}
    	return(alarm(0));		/* turn off timer, return unslept time */
    }
    
    命令行输出
    lh@LH_LINUX:~/桌面/apue.3e/signals$ ./tsleep2 
    ^C          (键入中断字符)
    sig_int starting
    sig_int finished
    sleep2 returned: 2
    
    • 可以看出信号处理程序sig_int()提前终止,因为他的运行时间>5s,所以语句printf("sig_int finished\n");并未执行就退出了。

11、信号集

  • 使用sigset_t以包含一个信号集,该数据类型能够表示多个信号的集合,该数据类型会被sigprocmask等函数使用。
    int sigemptyset(sigset_t *set);
    int sigfillset(sigset_t *set);
    int sigaddset(sigset_t *set, int signum);
    int sigdelset(sigset_t *set, int signum);
    int sigismember(const sigset_t *set, int signum);
    
    • sigemptyset
      • 初始化set指向的信号集,清除其中所有信号
    • sigfillset
      • 初始化set指向的信号集,使其置位所有信号
    • 注意,所有应用程序在使用信号集前,都要对该信号集数据结构调用sigemptyset或者sigfillset
    • sigaddset
      • set指向的信号集中添加指定信号signum
    • sigdelset
      • set指向的信号集中删除指定信号signum
    • sigismember
      • 判断set信号集中是否有信号signum
  • 实例:上述五种函数的实现
    • 本书的后续部分都假定一种实现有31种信号和32种整型(没有编号为0的信号),可用一位代表一个信号的方法实现信号集。
      #include	
      #include	
      
      #define sigemptyset(ptr) (*(ptr) = 0)
      /*sigfillset返回值必须为0,使用C语言的逗号算符,它将逗号算符后的值作为表达式的值返回。*/
      #define sigfillset(ptr)  (*(ptr) = ~(sigset_t )0,0)
      
      /*
       *  usually defines NSIG to include signal number 0.
       */
      #define	SIGBAD(signo)	((signo) <= 0 || (signo) >= NSIG)
      
      int
      sigaddset(sigset_t *set, int signo)
      {
      	if (SIGBAD(signo)) {
      		errno = EINVAL;
      		return(-1);
      	}
      	*set |= 1 << (signo - 1);		/* turn bit on */
      	return(0);
      }
      
      int
      sigdelset(sigset_t *set, int signo)
      {
      	if (SIGBAD(signo)) {
      		errno = EINVAL;
      		return(-1);
      	}
      	*set &= ~(1 << (signo - 1));	/* turn bit off */
      	return(0);
      }
      
      int
      sigismember(const sigset_t *set, int signo)
      {
      	if (SIGBAD(signo)) {
      		errno = EINVAL;
      		return(-1);
      	}
      	return((*set & (1 << (signo - 1))) != 0);
      }
      

12、函数sigprocmask

  • 信号屏蔽字:阻塞而不能递送给该进程的信号集。
  • 可以通过sigprocmask函数检测、更改进程的信号屏蔽字
    int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
    
    • oldset非空,那么进程之前的信号屏蔽字通过oldset返回。

    • set非空,则how指示如何根据set修改当前信号屏蔽字

    • how参数:

      • SIG_BLOCK:阻塞信号,即之前的信号集和set做按位或操作,即并集。set包含了希望被阻塞的信号
      • SIG_UNBLOCK:解除信号阻塞,即和set的补集求交集。set包含了希望被解除阻塞的信号
      • SIG_SETMASK:赋值信号屏蔽字
    • 在调用该函数后如果有任何未决的、不再阻塞的信号,则在函数返回之前,至少将其中之一递送给该进程。

13、sigpending函数(未决的信号)

  • sigpending函数返回一个信号集,以指示当前处于未决状态的信号(即已经产生但是由于被阻塞而不能递送的信号)
    int sigpending(sigset_t *set);
    
  • 实例:使用目前提及到的信号函数
    #include "apue.h"
    
    static void	sig_quit(int);
    
    int
    main(void)
    {
    	sigset_t	newmask, oldmask, pendmask;
    	if (signal(SIGQUIT, sig_quit) == SIG_ERR)
    		err_sys("can't catch SIGQUIT");
    
    	/*
    	 * Block SIGQUIT and save current signal mask.
    	 */
    	sigemptyset(&newmask);
    	sigaddset(&newmask, SIGQUIT);
    	if (sigprocmask(SIG_BLOCK, &newmask, &oldmask) < 0)
    		err_sys("SIG_BLOCK error");
    
    	sleep(5);	/* SIGQUIT here will remain pending */
    
    	if (sigpending(&pendmask) < 0)
    		err_sys("sigpending error");
    	if (sigismember(&pendmask, SIGQUIT))
    		printf("\nSIGQUIT pending\n");
    
    	/*
    	 * Restore signal mask which unblocks SIGQUIT.
    	 */
    	if (sigprocmask(SIG_SETMASK, &oldmask, NULL) < 0)
    		err_sys("SIG_SETMASK error");
    	printf("SIGQUIT unblocked\n");
    
    	sleep(5);	/* SIGQUIT here will terminate with core file */
    	exit(0);
    }
    
    static void
    sig_quit(int signo)
    {
    	printf("caught SIGQUIT\n");
    	if (signal(SIGQUIT, SIG_DFL) == SIG_ERR)
    		err_sys("can't reset SIGQUIT");
    }
    
    命令行输出:
    lh@LH_LINUX:~/桌面/apue.3e/signals$ ./critical 
    ^\                     (终端输入退出字符Ctrl+\,产生信号一次)
    SIGQUIT pending        (从sleep返回后)
    caught SIGQUIT         (在信号处理程序中)
    SIGQUIT unblocked      (从sigprocmask返回后)
    ^\Quit(coredump)       (再次产生信号)
    lh@LH_LINUX:~/桌面/apue.3e/signals$ ./critical 
    ^\^\^\^\^\^\^\^\       (终端多次输入退出字符Ctrl+\,产生信号8次)
    SIGQUIT pending
    caught SIGQUIT         (只产生信号一次)
    SIGQUIT unblocked
    ^\Quit(coredump)       (再次产生信号)
    
    • 在该程序中:
      • 进程阻塞SIGQUIT信号,保存了当前信号屏蔽字(以便以后恢复),然后休眠5秒。在此期间所产生的退出信号SIGQUIT都被阻塞,不递送至该进程,直到该信号不再被阻塞。
      • 在5秒休眠结束后,检查该信号是否是未决的(打印SIGQUIT pending),然后通过SIG_SETMASK方法将SIGQUIT设置为不再阻塞。
      • 在休眠期间如果产生了退出信号,那么此时该信号是未决的,但是不再受阻塞,所以在sigprocmask返回之前,它被递送到调用进程。即:先打印caught SIGQUIT,再打印SIGQUIT unblocked
      • 最后进程再次休眠5秒后退出,如果在此期间再产生退出信号,那么因为上次捕捉到该信号时,已将其处理方式设置为默认动作,所以这一次他就会使该进程终止。
      • shell发现子进程异常终止时输出QUIT(coredump)信息。注意,第二次运行该程序时,在进程休眠期间使SIGQUIT信号产生了10次,但是解除了对信号的阻塞后,只向进程传送一次SIGQUIT。从中可以看出在此系统上没有将信号进行排队。

14、sigaction函数(信号动作)

  • 检查、修改指定信号的处理动作。此函数用于取代UNIX早期版本使用的signal函数。

    int sigaction(int signo, const struct sigaction *act, struct sigaction *oldact);
    
  • act指针非空,则表示要修改其动作。如果oldact非空,则通过该参数返回指定信号的上一个动作。

    struct sigaction {
        void     (*sa_handler)(int);
        void     (*sa_sigaction)(int, siginfo_t *, void *);
        sigset_t   sa_mask;
        int        sa_flags;
        void     (*sa_restorer)(void);
    };
    
    • sa_handler:信号捕捉函数地址,可以是 SIG_DFL 表示默认动作,SIG_IGN 表示忽略此信号
    • sa_mask
      • 一个信号集,在调用该信号捕捉函数之前,这一信号集要加到进程的信号屏蔽字中。仅当从信号捕捉函数返回时再将进程的信号屏蔽字恢复为原来值。
      • 注意,在信号处理函数被调用时,操作系统建立的新信号屏蔽字包含正被递送的信号(即触发本次捕获的信号),信号处理函数返回时再恢复信号屏蔽字。因此保证在处理一个给定信号时,如果该信号再次发生,那么它将被阻塞到前一个信号的处理结束为止。
    • sa_flags
      • 对指定信号进行处理的各个选项
        • SA_INTERRUPT:由此信号中断的系统调用不自动重启动
        • SA_RESTART:由此信号中断的系统调用自动重启动
        • SA_NOCLDSTOP:如果 signoSIGCHLD,则在子进程停止时不产生此信号;在子进程终止时仍产生此信号
        • SA_NOCLDWAIT:如果 signoSIGCHLD,则调用进程的子进程终止时不会变成僵尸进程(不会发出SIGCHLD信号)
        • SA_NODEFER:当捕捉到此信号时,在执行其信号捕捉函数时,系统不自动阻塞此信号(除非sa_mask中包含此信号)。
        • SA_RESETHAND:在进入信号处理程序时,将该信号处理方式设置为SIG_DFL,并清除SA_SIGINFO标志
        • SA_SIGINFO:此选项对信号处理程序提供了附加信息,即使用sa_sigaction信号处理程序而不是sa_handler
    • sa_sigaction
      • 一个替代的信号处理程序。当该信号使用了SA_SIGINFO标志时,使用该信号处理程序。由于sa_handlersa_sigaction的实现可能共用同一存储区,因此这两个字段只能有一个。
      • 之前信号处理函数是以下形式:
        void handler(int signo);
        
      • 若设置了SA_SIGINFO标志,则信号处理函数是以下形式
        void handler(int signo, siginfo_t *info, void *context);
        
        • 其中第二个参数siginfo_t结构体包含了信号产生原因的有关信息,这样在信号处理函数中我们就可以通过第二个参数知道更多具体与该信号相关的信息。其应该至少包含以下字段
           siginfo_t {
               int      si_signo;     /* Signal number */
               int      si_errno;     /* An errno value */
               int      si_code;      /* 发出信号具体原因 */
               pid_t    si_pid;       /* Sending process ID */
               uid_t    si_uid;       /* Real user ID of sending process */
               int      si_status;    /* Exit value or signal */
               sigval_t si_value;     /* Signal value */
               void    *si_addr;      /* Memory location which caused fault */
               ...
          }
          
        • 其中si_code即为信号发生具体原因:
          《UNUX环境高级编程》(10)信号_第4张图片
        • 第三个参数context可以被强制类型转换为ucontext_t类型,该结构体标识信号传递时进程上下文,该ucontext_t结构至少包含以下字段
          typedef struct ucontext_t
            {
              struct ucontext_t *uc_link;
              stack_t uc_stack;
              mcontext_t uc_mcontext;
              sigset_t uc_sigmask;
              ...
            } ucontext_t;
          
          • uc_link:为当前context执行结束之后要执行的下一个context,若uc_link为空,执行完当前context之后退出程序。
          • uc_sigmask : 执行当前上下文过程中需要阻塞的信号列表,即信号屏蔽字
          • uc_stack : 为当前context运行的栈信息。
          • uc_mcontext : 保存具体的程序执行上下文,如PC值,堆栈指针以及寄存器值等信息。它的实现依赖于底层,是平台硬件相关的。此实现不透明
  • 实例:用sigaction实现signal函数(这是POSIX所希望的,有些子系统支持老的不可靠信号语义signal函数,其目的是实现二进制向后兼容)

    #include "apue.h"
    /*Reliable version of signal(),using POSIX sigaction().*/
    sigfunc*
    signal(int signo,sigfunc* func)
    {
    	struct sigaction act,oact;
    	act.sa_handler = func; //设置信号捕捉函数地址
    	sigemptyset(&act.sa_mask); //初始化act结构体的sa_mask成员
    	act.sa_flags = 0;
    	/*不希望重启动由SIGALRM信号中断的系统调用。原因:希望对I/O操作可以设置时间限制*/
    	if(signo == SIGALRM){ 
    #define SA_INTERRUPT /*提高可移植性,Linux定义了这个标志*/
    		act.flags |= SA_INTERRUPT;//由此信号中断的系统调用不自动重启动
    #endif
    	}else{
    		act.flags |= SA_RESTART; //除了SIGALRM信号,被这些信号中断的系统调用都能自动重启动。
    	}
    	/*act指针非空,表示要修改其动作。oldact非空,通过该参数返回指定信号的上一个动作。*/
    	if(sigaction(signo,&act,&oact)<0)
    		return(SIG_ERR);
    	return(oact.sa_handler)//返回之前的信号处理程序的地址
    }
    
    • 注意:若信号处理程序是用sigaction设置的,那么其默认方式是不重新启动系统调用。除非说明了SA_RESTART标志,否则sigaction函数不再重启被中断的系统调用。

15、sigsetjmp和siglongjmp函数

  • 系统在进入信号处理程序时,会将该信号自动加入到信号屏蔽字中,这阻止了后来产生的这种信号中断该信号处理程序,然后再信号处理程序返回时恢复信号屏蔽字。

  • 但是如果在信号处理函数中使用longjmp非局部转移到setjmp处,会导致信号屏蔽字无法恢复。解决方案是调用sigsetjmpsiglongjmp而不是使用setjmplongjmp

    int sigsetjmp(sigjmp_buf env, int savesigs);
    void siglongjmp(sigjmp_buf env, int val);
    
    • 这两个函数和setjmplongjmp的唯一区别是sigsetjmp增加了参数savesigs。如果该参数非0,则在env参数中保存进程的当前信号屏蔽字,此时在信号处理函数中调用siglongjmp进行非局部跳转到sigsetjmp,会导致恢复保存的信号屏蔽字。
  • 实例:演示在信号处理程序被调用时,系统设置的信号屏蔽字如何自动地包括刚被捕捉到的信号。也展示了使用sigsetjmpsiglongjmp函数的方法

    #include "apue.h"
    #include 
    #include 
    
    static void						sig_usr1(int);
    static void						sig_alrm(int);
    static sigjmp_buf				jmpbuf;
    /*注意这里使用到了原子变量*/
    static volatile sig_atomic_t	canjump;
    
    int
    main(void)
    {
    	if (signal(SIGUSR1, sig_usr1) == SIG_ERR)
    		err_sys("signal(SIGUSR1) error");
    	if (signal(SIGALRM, sig_alrm) == SIG_ERR)
    		err_sys("signal(SIGALRM) error");
    
    	/*注意:该函数打印了屏蔽字*/
    	pr_mask("starting main: ");		/* {Prog prmask} */
    
    	if (sigsetjmp(jmpbuf, 1)) {
    
    		pr_mask("ending main: ");
    
    		exit(0);
    	}
    	/*防止在jmpbuf(跳转缓冲)尚未由sigsetjmp初始化时调用信号处理程序。*/
    	canjump = 1;	/* now sigsetjmp() is OK */
    
    	for ( ; ; )
    		pause();
    }
    
    static void
    sig_usr1(int signo)
    {
    	time_t	starttime;
    
    	if (canjump == 0)
    		return;		/* unexpected signal, ignore */
    
    	pr_mask("starting sig_usr1: ");
    
    	alarm(3);				/* SIGALRM in 3 seconds */
    	starttime = time(NULL);
    	for ( ; ; )				/* busy wait for 5 seconds */
    		if (time(NULL) > starttime + 5)
    			break;
    
    	pr_mask("finishing sig_usr1: ");
    
    	canjump = 0;
    	siglongjmp(jmpbuf, 1);	/* jump back to main, don't return */
    }
    
    static void
    sig_alrm(int signo)
    {
    	pr_mask("in sig_alrm: ");
    }
    

    命令行输出:

    lh@LH_LINUX:~/桌面/apue.3e/signals$ ./mask &           (在后台启动进程)
    [1] 9971
    lh@LH_LINUX:~/桌面/apue.3e/signals$ starting main: 	  (作业控制shell打印其进程ID)
    ^C
    lh@LH_LINUX:~/桌面/apue.3e/signals$ kill -USR1 9971	  (向该进程发送SIGUSR1)
    starting sig_usr1:  SIGUSR1
    lh@LH_LINUX:~/桌面/apue.3e/signals$ in sig_alrm:  SIGUSR1 SIGALRM
    finishing sig_usr1:  SIGUSR1
    ending main: 
                                                           (输入回车)
    [1]+  已完成               ./mask
    
    • 程序中使用到了sig_atomic_t类型

      • 当把变量声明为该类型会保证该变量在使用或赋值时, 无论是在32位还是64位的机器上都能保证操作是原子的, 它会根据机器的类型自动适应。
      • 在处理信号(signal)的时候,有时对于一些变量的访问希望不会被中断,无论是硬件中断还是软件中断,这就要求访问或改变这些变量需要在计算机的一条指令内完成。通常情况下,int类型的变量通常是原子访问的,也可以认为 sig_atomic_t就是int类型的数据,因为对这些变量要求一条指令完成,所以sig_atomic_t不可能是结构体,只会是数字类型。
      • sig_atomic_t类型总是用volatile修饰,因为该变量总是由两个不同的控制线程-main函数和异步执行的信号处理程序访问,因此必须保证每次需要存储或读取这个变量的时候,都会直接从变量地址中读取数据,如果没有volatile关键字,则编译器可能优化读取和存储,可能暂时使用寄存器中的值,导致将出现不一致的现象
      • 如这样使用:
        volatile sig_atomic_t    flag;
        
    • 下图表示了该程序的执行时间顺序
      《UNUX环境高级编程》(10)信号_第5张图片

      • 可将上图分为3个部分:左面部分(对应于main()),中间部分(sig_usr1)和右面部分(sig_alrm
      • 左面部分:信号屏蔽字是0(没有信号是阻塞的)
      • 中间部分:信号屏蔽字是SIGUSR1
      • 右边部分:信号屏蔽字是SIGUSR1|SIGALRM
    • 从命令行输出可以看到:当调用一个信号处理程序时,被捕捉到的信号加到进程的当前信号屏蔽字中。当从信号处理程序返回时,恢复原来的屏蔽字。另外,siglongjmp恢复了由sigsetjmp所保存的信号屏蔽字。如果使用longjmpsetjmp则不会恢复。

16、函数sigsuspend

  • 考虑sigprocmask函数中提出的一点:如果在调用该函数后如果有任何未决的、不再阻塞的信号,则在函数返回之前,至少将其中之一递送给该进程。

  • 那么如果针对下面代码则会出现问题:

    sigprocmask(SIG_SETMASK,&oldmask,NULL);
    pause();
    
    • 如果sigprocmask解除了某个信号阻塞,而在此期间的确该信号被阻塞了,由上所述,那么就好像该信号发生在sigprocmaskpause函数之间。(或者在sigprocmaskpause函数之间的确有某个未阻塞的信号被递送了),那么将会导致pause函数一直阻塞下去,即sigprocmaskpause函数之间的这个时间窗口中的信号丢失了。
  • 针对此问题,需要在一个原子操作中解除信号阻塞并使进程休眠。因此可以使用sigsuspend函数,该函数在一个原子操作中先恢复信号屏蔽字,然后使进程休眠。

    int sigsuspend(const sigset_t *mask);
    
    • 进程的信号屏蔽字设置为mask,在捕捉到一个信号或发生了一个会终止该进程的信号之前,该进程被挂起。如果捕捉到一个信号并且从该信号处理程序返回,则sigsuspend返回,并且恢复信号屏蔽字为sigsuspend之前的值(并且返回-1errno设为EINTR)。
  • 实例:保护代码临界区,使其不被特定信号中断的正确方法。

    #include "apue.h"
    
    static void	sig_int(int);
    
    int
    main(void)
    {
    	sigset_t	newmask, oldmask, waitmask;
        /*起初无信号被屏蔽,预期输出:program start:*/
    	pr_mask("program start: ");
    	
        /*注册sig_int信号处理函数*/
    	if (signal(SIGINT, sig_int) == SIG_ERR)
    		err_sys("signal(SIGINT) error");
        /*waitmask信号集中包含SIGUSR1信号*/
    	sigemptyset(&waitmask);
    	sigaddset(&waitmask, SIGUSR1);
        /*newmask信号集中包含SIGINT信号*/
    	sigemptyset(&newmask);
    	sigaddset(&newmask, SIGINT);
    
    	/*
    	 * Block SIGINT and save current signal mask.
    	 */
    	 /*阻塞newmask信号集,即阻塞SIGINT,并将老的信号屏蔽字保存在oldmask中*/
    	if (sigprocmask(SIG_BLOCK, &newmask, &oldmask) < 0)
    		err_sys("SIG_BLOCK error");
    
    	/*
    	 * Critical region of code.
    	 */
    	 /*打印临界区的信号屏蔽字,预期输出:in critical region:SIGINT*/
    	pr_mask("in critical region: ");
    
    	/*
    	 * Pause, allowing all signals except SIGUSR1.
    	 */
    	 /*将进程信号屏蔽字设置为SIGUSR1,并挂起进程*/
    	if (sigsuspend(&waitmask) != -1)
    		err_sys("sigsuspend error");
      /*通过按下Ctrl+c按键,将进入sig_int信号处理函数,预期输出:
      	in sig_int: SIGINT SIGUSR1 (进入信号处理程序自动阻塞当前信号)
      */
      /*从sigsuspend返回之后,进程的信号屏蔽字设置为sigsuspend之前的值。预期输出after return from sigsuspend: SIGINT 
    	*/
    	pr_mask("after return from sigsuspend: ");
    
    	/*
    	 * Reset signal mask which unblocks SIGINT.
    	 */
    	 /*将信号屏蔽字设置为最开始的状态*/
    	if (sigprocmask(SIG_SETMASK, &oldmask, NULL) < 0)
    		err_sys("SIG_SETMASK error");
    
    	/*
    	 * And continue processing ...
    	 */
    	 /*预期输出:program exit:*/
    	pr_mask("program exit: ");
    
    	exit(0);
    }
    
    static void
    sig_int(int signo)
    {
    	pr_mask("\nin sig_int: ");
    }
    

    命令行输出:

    lh@LH_LINUX:~/桌面/apue.3e/signals$ ./suspend1
    program start: 
    in critical region:  SIGINT
    ^C                           (键入中断字符)
    in sig_int:  SIGINT SIGUSR1
    after return from sigsuspend:  SIGINT
    program exit: 
    
  • 实例:等待信号处理程序设置一个全局变量,该程序用于捕捉中断信号和退出信号,希望仅当捕捉到退出信号时,才唤醒主例程。

    #include "apue.h"
    
    volatile sig_atomic_t	quitflag;	/* set nonzero by signal handler */
    
    static void
    sig_int(int signo)	/* one signal handler for SIGINT and SIGQUIT */
    {
    	if (signo == SIGINT)
    		printf("\ninterrupt\n");
    	else if (signo == SIGQUIT)
    		quitflag = 1;	/* set flag for main loop */
    }
    
    int
    main(void)
    {
    	sigset_t	newmask, oldmask, zeromask;
    
    	if (signal(SIGINT, sig_int) == SIG_ERR)
    		err_sys("signal(SIGINT) error");
    	if (signal(SIGQUIT, sig_int) == SIG_ERR)
    		err_sys("signal(SIGQUIT) error");
    
    	sigemptyset(&zeromask);
    	sigemptyset(&newmask);
    	sigaddset(&newmask, SIGQUIT);
    
    	/*
    	 * Block SIGQUIT and save current signal mask.
    	 */
    	if (sigprocmask(SIG_BLOCK, &newmask, &oldmask) < 0)
    		err_sys("SIG_BLOCK error");
    
    	while (quitflag == 0)
    		sigsuspend(&zeromask);
    
    	/*
    	 * SIGQUIT has been caught and is now blocked; do whatever.
    	 */
    	quitflag = 0;
    
    	/*
    	 * Reset signal mask which unblocks SIGQUIT.
    	 */
    	if (sigprocmask(SIG_SETMASK, &oldmask, NULL) < 0)
    		err_sys("SIG_SETMASK error");
    
    	exit(0);
    }
    

    命令行输出:

    lh@LH_LINUX:~/桌面/apue.3e/signals$ ./suspend2
    ^C
    interrupt
    ^C
    interrupt
    ^C
    interrupt
    ^C
    interrupt
    ^\lh@LH_LINUX:~/桌面/apue.3e/signals$ 
    

17、abort函数

  • 使进程异常终止
    void abort(void);
    
    • 此函数将SIGABRT信号发送给调用进程raise(SIGABRT),不应忽略此信号。
      • 注意,若捕获此信号并且由相应信号处理函数返回,abort仍不会返回到其调用者。如果捕捉到此信号,那么信号处理程序不能返回的唯一方法是调用exit_exit_Exitlongjmpsiglongjmp。即abort导致进程的非正常终止,除非SIGABRT信号被捕获,并且信号处理函数没有返回(使用了longjmp等函数使信号处理函数没有返回)。
      • 并且abort不理会进程对此信号的阻塞或忽略。
      • 进程捕获abort调用信号处理程序的意图:
        在进程终止之前执行所需的清理操作。如果进程并不在信号处理程序中终止自己,则当信号处理函数返回时,abort终止该进程。
      • 如果abort要终止进程,则它对所有打开标准I/O流的效果应当与进程终止前对每个流调用fclose相同。
  • 实例:abort函数的实现
    #include 
    #include 
    #include 
    #include 
    
    void
    abort(void)			/* POSIX-style abort() function */
    {
    	sigset_t			mask;
    	struct sigaction	action;
    
    	/* Caller can't ignore SIGABRT, if so reset to default */
    	sigaction(SIGABRT, NULL, &action);
    	/*查看是否执行默认动作,若是则冲洗所有的标准I/O实现,否则让他定义成默认动作*/
    	if (action.sa_handler == SIG_IGN) {
    		action.sa_handler = SIG_DFL;
    		sigaction(SIGABRT, &action, NULL);
    	}
    	if (action.sa_handler == SIG_DFL)
    		fflush(NULL);			/* flush all open stdio streams */
    
    	/* Caller can't block SIGABRT; make sure it's unblocked */
    	sigfillset(&mask);
    	sigdelset(&mask, SIGABRT);	/* mask has only SIGABRT turned off */
    	sigprocmask(SIG_SETMASK, &mask, NULL);
    	kill(getpid(), SIGABRT);	/* send the signal */
    
    	/* If we're here, process caught SIGABRT and returned */
    	fflush(NULL);				/* flush all open stdio streams */
    	action.sa_handler = SIG_DFL;
    	sigaction(SIGABRT, &action, NULL);	/* reset to default */
    	sigprocmask(SIG_SETMASK, &mask, NULL);	/* just in case ... */
    	kill(getpid(), SIGABRT);				/* and one more time */
    	exit(1);	/* this should never be executed ... */
    }
    
    • 本人不是很了解这个源码的写法,后面复习的时候可以分析一下

18、system函数

  • system函数阻塞SIGCHLD
    正在执行system函数时,应当阻塞对父进程递送SIGCHLD信号。否则,当system创建的子进程结束时,system的调用者可能错误的认为它自己的一个子进程结束了,然后在SIGCHLD信号处理程序中通过wait函数获取子进程终止状态。由于该子进程终止状态已被获取过了,因此就阻止了system函数获取子进程的终止状态并将其作为返回值。
  • system函数忽略SIGINTSIGQUIT
    由之前的知识可知,当在终端键入Ctrl+C会将SIGINT发送给前台进程组、在终端键入Ctrl+\会将SIGQUIT发送给前台进程组。但是system期间这两个信号应该只发送给正在运行的程序:即system函数中创建的子进程。因为由system执行的命令可能是交互式命令(如ed编辑器),以及system函数的调用者在system执行期间放弃了控制,等待该命令程序执行结束,所以system调用者就不应该接收这两个终端产生的信号。这也是为何规定system的调用者在等待命令完成时应当忽略这两个信号。
  • 同样,书上介绍了system函数的实现方法,有兴趣可以看一下。

19、sleep、nanosleep、clock_nanosleep函数

19.1、 sleep

  • sleep函数使调用进程被挂起直到满足以下条件:

    • 已经超过参数指定的秒数
    • 进程捕捉到一个信号并从信号处理程序返回。
  • 该函数返回未休眠剩余的秒数

    unsigned int sleep(unsigned int seconds);
    
  • 注意,sleep可以由alarm函数实现,但是可能造成sleepalarm函数互相影响。比如先alarm(10),然后再sleep(3),那么对SIGALRM信号的产生情况造成影响。

  • 因此Linux使用nanosleep实现sleep。因为nanosleep不涉及产生任何信号,即与闹钟定时器相互独立,所以该实现的sleep函数不会与其他时间相关函数如alarm产生交互影响

19.2、 nanosleep

int nanosleep(const struct timespec *req, struct timespec *rem);
 struct timespec {
     time_t tv_sec;        /* seconds */
     long   tv_nsec;       /* nanoseconds [0 .. 999999999] */
 };
  • 该函数与sleep类似,但是提供纳秒级别精度。
  • 该函数挂起进程,直到要求的时间超时或者某个信号中断该函数。req参数指定进程休眠时间,rem函数返回未休眠的剩余时间。
  • nanosleep函数并不涉及产生任何信号,所以不用担心与其他函数的交互。

19.3、clock_nanosleep

  • 该函数制定了基于特定时钟的延迟时间来挂起线程
    int clock_nanosleep(clockid_t clock_id, int flags,nconst struct timespec *request, struct timespec *remain);
    
    • clock_id:延迟时间基于的时钟
      • CLOCK_REALTIME:系统实时时间,即从1970年开始的时间
      • CLOCK_MONOTONIC:从系统启动这一刻起开始计时,不受系统时间被用户改变的影响
      • CLOCK_PROCESS_CPUTIME_ID:本进程到当前代码的CPU时间
      • CLOCK_THREAD_CPUTIME_ID:本线程到当前代码的CPU时间
    • flags:控制时间是绝对还是相对的
      • 0:相对的,即希望休眠的时间长度
      • TIMER_ABSTIME:绝对的,即希望休眠到时钟到达某个特定的时间
    • requestremain:和nanosleep一致。使用绝对时间时,remain参数无用
  • 调用clock_nanosleep(CLOCK_REALTIME,0,req,rem)相当于nanosleep(req,rem)

20、sigqueue函数:信号排队

  • 如果支持信号实时扩展,那么就支持信号排队。如果要使用排队信号,则必须遵循以下条件
    • 使用sigaction函数安装信号处理程序时指定SA_SIGINFO标志。如果没有此标志,信号会阻塞延迟,但是是否进入队列取决于具体实现
    • sigaction结构的sa_sigaction成员中(不是sa_handler)提供信号处理程序。实现可能允许用户使用sa_handler字段,但是不能获取sigqueue函数发出的额外信息
    • 使用sigqueue发出信号
      int sigqueue(pid_t pid, int sig, const union sigval value);
      
      • 该函数只把信号发给单个进程。可以通过value参数向信号处理程序传递整数和指针值。除此之外,sigqueuekill类似.

      • 注意,信号不能被无限次排队。到达相应限制后,sigqueue会失败,errno设置为EAGAIN

      • 在支持实时信号扩展的Linux之中,sigqueue只能用于实时信号(SIGRTMIN~SIGRTMAN之间的信号,包括这两个限制值),这些实时信号默认操作是终止进程。对于非实时信号(1-31信号),不支持信号排队
        《UNUX环境高级编程》(10)信号_第6张图片

21、作业控制信号

  • 以下六个信号与作业控制有关
    • SIGCHLD:子进程停止或终止
    • SIGCONT:如果进程已停止,则使其继续运行
    • SIGSTOP:停止信号(不能被捕捉或忽略)
    • SIGTSTP:交互式停止信号
    • SIGTTIN:后台进程组成员读控制终端
    • SIGTTOU:后台进程组成员写控制终端
  • 当一个进程产生4种停止信号(SIGTSTPSIGSTOPSIGTTINSIGTTOU),对进程的任一未决SIGCONT信号丢弃
  • 当对一个进程产生SIGCONT信号时,对同一进程的任一未决停止信号被丢弃
  • (需要继续学习)

22、信号名和编号

  • 通过数组sys_siglist获取信号编号和信号名间的映射
    extern const char *const sys_siglist[_NSIG];//其中数组下标即为信号编号
    
  • 使用psignal函数打印与信号编号对应的字符串
    void psignal(int sig, const char *msg);
    
    • 该函数类似于perror,通常是在stderr打印出msg参数,后面跟一个冒号一个空格,然后打印出该信号的说明。
  • 如果在sigaction信号处理程序中有siginfo_t结构,也可以通过siginfo_t结构,使用psiginfo函数打印出信号编号更多的信息
    void psiginfo(const siginfo_t *pinfo, const char *msg);
    
  • 使用strsignal获取与信号相关字符串(而不是写入到文件中),类似于strerror
    char *strsignal(int sig);
    
    • 实例:
      int main(int argc, char* argv[]) {
          cout << "sys_list数组 : " << sys_siglist[SIGCHLD] << endl;
          psignal(SIGCHLD,"psignal函数 ");
          cout << "strsignal函数 : " << strsignal(SIGCHLD) << endl;
      }
      /*打印:
      sys_list数组 : Child exited
      psignal函数 : Child exited
      strsignal函数 : Child exited
      */
      

你可能感兴趣的:(UNIX环境高级编程,开发语言,linux)