1、引言
- 信号是软件中断,提供了一种处理异步事件的方法。例如:终端用户键入中断键,会通过信号机制停止一个程序,或及早终止管道的下一个程序。
2、信号概念
- 每个信号都有一个名字,以3个字符
SIG
开头。如SIGABRT
是终止信号,进程调用abort
函数产生这种信号。void abort(void);
abort()
首先解除了对SIGABRT(6)
信号的阻止和忽略,然后为调用进程发送该信号(就像调用了raise(SIGABRT)
一样)。 导致进程的非正常终止,除非SIGABRT
信号被捕获,并且信号处理函数没有返回(调用exit
、_exit
、_Exit
、longjmp
或siglongjmp
使信号处理函数没有返回)。- 如果
abort()
函数导致进程终止,则关闭和刷新所有打开的文件流。
- Linux支持63种信号,不存在编号为0的信号(即为空信号),kill函数对信号编号0有特殊应用。1 ~ 31的信号为传统UNIX支持的信号,是不可靠信号(非实时的),编号为32 ~ 63的信号是后来扩充的,称做可靠信号(实时信号)。不可靠信号和可靠信号的区别在于前者不支持排队,可能会造成信号丢失,而后者不会。
2.1 产生信号的条件
- 当用户按某些终端键时,引发终端产生的信号。如在终端上按
delete
或Ctrl+C
键,通常产生中断信号SIGINT
。 - 硬件产生异常信号:除
0
、无效的内存引用等。因为这些条件通常由硬件检测到,并且通知内核。然后内核为该进程产生适当信号。 - 进程调用
kill(2)
函数将任意信号发送给另一个进程或进程组:接收信号进程和发送信号进程的所有者必须相同,或发送方必须是超级用户root
- 用
kill(1)
命令将信号(默认是SIGTERM
)发送给进程。此命令只是kill
函数的接口,可用此命令终止一个失控的后台程序。 - 当检测到某种软件条件发生时将产生信号通知相关进程。如
SIGURG
(在网络连接上传来带外的数据)、SIGPIPE
(在管道的读进程已终止后,一个进程写此管道)、SIGALRM
(进程设置的定时器已经超时)等
2.2 对信号的处理方式
-
忽略此信号
- 除了两种信号
SIGKILL
和SIGSTOP
绝对不能忽略以外,大多数信号都可使用这种方式处理。 SIGKILL
和SIGSTOP
不能被忽略的原因是,它们向内核和超级用户提供了使进程终止或停止的可靠方法(可以理解为终止、停止进程的终极方法)。- 并且如果忽略某些由硬件异常(如除0、非法内存引用)产生的信号,会发生未定义行为。
- 除了两种信号
-
捕捉该信号
- 通知内核在某种信号发生时,调用一个用户函数。同样
SIGKILL
和SIGSTOP
不能被捕捉
- 通知内核在某种信号发生时,调用一个用户函数。同样
-
执行系统默认动作
- 对大多数信号的系统默认动作是终止该进程
- 注意
终止+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中定义
SIGIOT
与SIGABRT
有相同值#define SIGIOT SIGABRT
- 表示发生了一个具体定义的硬件故障。Linux中定义
-
SIGALRM:
- 调用
alarm
、setitimer
函数设置的定时器、间隔时间超时产生此信号。
- 调用
-
SIGBUS:
- 表示发生了一个具体定义的硬件故障。当出现某种类型内存故障时,常产生此种信号
-
SIGCHLD:
- 子进程终止或停止时,
SIGCHLD
信号发送给父进程。系统默认会忽略此信号。如果父进程希望得知子进程的这种状态改变,那么通常在信号捕捉函数中调用wait
等函数获取子进程pid
及终止状态。
- 子进程终止或停止时,
-
SIGCONT:
- 用于作业控制。如果收到此信号的进程处于停止状态,则系统默认动作是该进程继续运行;否则忽略此信号
-
SIGEMT:
- 表示发生了一个具体定义的硬件故障。
-
SIGFPE:
- 算术运算异常,如除0、浮点溢出等
-
SIGHUP:挂断信号。
- 终端接口检测到一个连接断开,则将此信号送给与该终端相关的控制进程(会话首进程)。
session
首进程退出时,该信号被发送到该session
中的前台进程组和后台进程组中的每一个进程- 若进程的退出,导致一个进程组变成了孤儿进程组,新的孤儿进程组中处于停止(
stopped
)状态的每一个进程都会收到挂断(SIGHUP
)信号,接着又收到继续(SIGCONT
)信号。 - 系统对
SIGHUP
信号的默认处理是终止收到该信号的进程。所以若程序中没有捕捉该信号,当收到该信号时,进程就会退出。
-
SIGILL:
- 进程执行一条非法硬件指令
-
SIGINT:中断信号
- 用户按中断键(
delete
或Ctrl+C
),产生此信号发送至前台进程组中的每一个进程。当一个进程失控或者在终端产生大量不需要输出时,常通过此命令终止它。
- 用户按中断键(
-
SIGKILL:
- 这是两个不能被阻塞、捕捉或忽略的信号中的一个,向内核和超级用户提供了使进程终止的可靠方法
-
SIGPIPE:
- 管道读进程已经终止后,写进程写入管道产生此信号。
- 当类型为
SOCK_STREAM
的socket
已不再连接时,进程写入该套接字也产生此信号。
-
SIGPOLL:
- 当在一个可轮询设备上发生一个特定事件时产生此信号(如当系统发现有东西需要你读的时候,就发个SIGPOLL信号来通知)。在未来可能将此信号移除。
-
SIGIO:
- 指示发生一个异步I/O事件。Linux中定义
SIGIO
与SIGPOLL
有相同的值,默认行为是终止该进程。#define SIGIO SIGPOLL
- 指示发生一个异步I/O事件。Linux中定义
-
SIGPROF:
setitimer
函数设置的梗概统计间隔定时器超时产生此信号。在未来可能将此信号移除
-
SIGPWR:
- 电源故障信号。主要用于具有不间断电源的系统。如果电源失效,系统依靠蓄电池继续运行。但是如果蓄电池也将不能工作,此时发送
SIGPWR
信号。通常是接到蓄电池电压过低信息的进程将SIGPWR
发送给init
进程,然后由init
处理停机操作。 - Linux对
SIGPWR
的默认动作是终止相关进程。
- 电源故障信号。主要用于具有不间断电源的系统。如果电源失效,系统依靠蓄电池继续运行。但是如果蓄电池也将不能工作,此时发送
-
SIGQUIT:退出信号
- 用户在终端上按退出键(
Ctrl+\
)产生此信号,并发送给前台进程组的所有进程。此信号不仅终止前台进程组,还产生一个core
文件。
- 用户在终端上按退出键(
-
SIGSEGV:
- 进程进行了一次无效的内存引用(比如访问了一个未经初始化的指针),或发生段错误。
-
SIGSTOP:
- 作业控制信号,它停止一个进程。同
SIGKILL
,该信号不能被阻塞、忽略和捕获
- 作业控制信号,它停止一个进程。同
-
SIGSYS:
- 指示一个无效的系统调用。
-
SIGTERM:
- 是kill命令发出的默认信号(系统默认终止信号)。相比于
SIGKILL
,SIGTERM
可以被捕获或忽略,因此允许让程序有机会在退出之前做好清理工作,从而优雅地终止。
- 是kill命令发出的默认信号(系统默认终止信号)。相比于
-
SIGTRAP:
- 表示发生了一个具体定义的硬件故障。通常使用此信号将控制转移至调试程序
-
SIGTSTP:
- 交互停止信号。当用户在终端按挂起键(
Ctrl+Z
)时,将该信号发送到前台进程组中的所有进程加粗样式。SIGTSTP
与SIGSTOP
都是使进程暂停(都使用SIGCONT
让进程重新激活)。唯一的区别是SIGSTOP
不可以捕获和忽略,而SIGTSTP
可以。
- 交互停止信号。当用户在终端按挂起键(
-
SIGTTIN:
- 当一个后台进程组中的进程试图读控制终端时收到此信号,并使该进程暂停。注意,如果读进程属于孤儿进程组,那么
read
控制终端操作返回出错,不产生此信号,errno
设置为EIO
。
- 当一个后台进程组中的进程试图读控制终端时收到此信号,并使该进程暂停。注意,如果读进程属于孤儿进程组,那么
-
SIGTTOU:
- 如果禁止后台作业向控制终端写,此时当一个后台进程组进程试图写控制终端时收到此信号,并使该进程暂停。注意,如果写进程属于孤儿进程组,则写操作返回出错,不产生此信号,
errno
设置为EIO
。 - 除此之外,
tcsetattr
、tcsendbreak
、tcdrain
、tcflush
、tcflow
以及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
:忽略此信号(不能用于SIGKILL
和SIGSTOP
)#define SIG_IGN ((__sighandler_t) 1) /* Ignore signal. */
SIG_DFL
:系统默认动作#define SIG_DFL ((__sighandler_t) 0) /* Default action. */
- 指定函数地址(不能用于
SIGKILL
和SIGSTOP
):在信号发生时,调用该函数(该函数有一个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函数的一个缺陷:
- 以信号
SIGINT
和SIGQUIT
为例,很多捕捉这两个信号的交互程序有下列形式的代码:
这样处理后,仅当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);
SIGINT
和SIGQUIT
当前未被忽略时,进程才会捕捉它们。 - 通过上面叙述可知:不改变信号的处理方式就不能确定信号之前的处理方式(根据
signal
返回值知道之前对于指定信号的处理方式)。因此可以使用sigaction
函数确定一个信号的处理方式,而无需改变它。
- 以信号
-
4、不可靠的信号
- 在早期UNIX中,信号是不可靠的。意为信号可能会丢失,一个信号发生了但是进程可能一直不知道。同时不具备阻塞信号的能力(不要忽略该信号,在其发生时记住它,然后在进程做好准备时再通知它)。
- 不可靠信号的主要问题
- 进程每次处理信号后,就将对信号的响应设置为默认动作。在某些情况下,将导致对信号的错误处理;因此,用户如果不希望这样的操作,那么就要在信号处理函数结尾再一次调用signal(),重新安装该信号。
- 信号可能丢失,如果在进程对某个信号进行处理时,这个信号发生多次,对后到来的这类信号不排队,那么仅传送该信号一次,即发生了信号丢失。因此,早期unix下的不可靠信号主要指的是进程可能对信号做出错误的反应以及信号可能丢失。
- Linux支持不可靠信号,但是对不可靠信号机制做了改进:在调用完信号处理函数后,不必重新调用该信号的安装函数(信号安装函数是在可靠机制上的实现)。因此,Linux下的不可靠信号问题主要指的是信号可能丢失。
5、中断的系统调用
- 系统调用分类
- 将系统调用分为两类 -> 低速系统调用/其他系统调用
- 低速系统调用是可能会使进程永远阻塞的一类系统调用
- 如果某些类型文件(如读管道、终端设备、网络设备)数据不存在,则读操作可能会使调用者永远阻塞
- 如果这些数据不能被相同类型文件立即接受,则写操作可能会使调用者永远阻塞。
- 在某种条件发生之前打开某些类型文件,可能发生阻塞
pause
函数(它使进程挂起直到收到一个信号)和wait函数- 某些ioctl操作
- 某些进程间通信函数
- 在早期UNIX中,如果进程正在执行低速系统调用而阻塞期间捕捉到一个信号,则该系统调用就被中断不再执行。该系统调用返回出错,其
errno
设置为EINTR
(系统调用被中断)。可以理解为一个信号发生了,进程捕捉到它,这意味着已经发生了某种事情,所以是个好机会应当唤醒阻塞的系统调用。 - 对于处理已经
read
/write
部分数据量的相应系统调用,此时被信号中断,有两种方式(例如read
系统调用已经接收并传送数据至应用程序缓冲区,但尚未接收到应用程序请求的全部数据):- 认为系统调用失败,
errno
设置为EINTR
- (现在使用这一种):允许该系统调用成功返回,返回值是已读写的数据量。
- 认为系统调用失败,
- 当被信号中断时,下列系统调用支持自动重启动:
ioctl
、read
、readv
、write
、writev
、wait
和waitpid
- 有些应用程序不希望这些函数被中断后自动重启动,因此进程可以设置对每个信号禁用此功能。(sigaction函数中只有中断信号的SA_RESTART标志有效,才自动重启动系统调用)在Linux中,当信号处理程序是用signal函数注册时,被中断的系统调用会自动重启动。
- 注意,除了低速系统调用,信号也可以中断类似
sleep
这样的函数:调用sleep
函数的线程休眠seconds
秒。如果中间有一个未被忽略的信号到达则终止休眠。
6、可重入函数
- 一些问题:
- 如果进程正在执行
malloc
,在其堆中分配另外的存储空间,而此时由于捕捉到信号而插入执行该信号处理程序,其中又调用malloc
会发生什么?- 可能会对进程造成破坏。因为
malloc
通常为它所分配的存储区维护一个链表,而插入执行信号处理程序时进程可能正在更改此链表。
- 可能会对进程造成破坏。因为
- 如果进程正在执行
getpwnam
这种将其结果存放在静态存储单元中的函数,其间插入执行信号处理程序,它又调用这样的函数(如信号处理函数内部又调用getpwnam
)会发生什么?- 返回给正常调用者的信息可能会被返回给信号处理程序的信息覆盖。
- 如果进程正在执行
- 可重入函数
-
在信号处理程序中保证调用安全的函数,这些函数是可重入的并被称为是异步信号安全的(即为在函数A执行期间中断执行信号处理程序,在信号处理程序中可能再次调用函数A,但是不会造成问题)。这种函数除了可重入外(信号处理程序中再次调用被信号中断的函数),在执行信号处理操作期间会阻塞任何会引起不一致的信号发送。
-
下列函数都是异步信号安全即可重入的
-
没有列入上表的函数大多是不可重入的,不可重入函数通常有以下特点:
- 它们使用静态数据结构,重入可能导致结果被覆盖
- 它们调用
malloc
或free
- 它们是标准
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);
kill
的pid
参数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
返回-1
,errno
设置为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 <setjmp.h> #include <signal.h> #include <unistd.h> 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 <signal.h> #include <errno.h> #define sigemptyset(ptr) (*(ptr) = 0) /*sigfillset返回值必须为0,使用C语言的逗号算符,它将逗号算符后的值作为表达式的值返回。*/ #define sigfillset(ptr) (*(ptr) = ~(sigset_t )0,0) /* * <signal.h> 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); }
- 本书的后续部分都假定一种实现有31种信号和32种整型(没有编号为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
:如果signo
是SIGCHLD
,则在子进程停止时不产生此信号;在子进程终止时仍产生此信号SA_NOCLDWAIT
:如果signo
是SIGCHLD
,则调用进程的子进程终止时不会变成僵尸进程(不会发出SIGCHLD
信号)SA_NODEFER
:当捕捉到此信号时,在执行其信号捕捉函数时,系统不自动阻塞此信号(除非sa_mask
中包含此信号)。SA_RESETHAND
:在进入信号处理程序时,将该信号处理方式设置为SIG_DFL
,并清除SA_SIGINFO
标志SA_SIGINFO
:此选项对信号处理程序提供了附加信息,即使用sa_sigaction
信号处理程序而不是sa_handler
- 对指定信号进行处理的各个选项
sa_sigaction
:- 一个替代的信号处理程序。当该信号使用了
SA_SIGINFO
标志时,使用该信号处理程序。由于sa_handler
和sa_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
即为信号发生具体原因:
- 第三个参数
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
处,会导致信号屏蔽字无法恢复。解决方案是调用sigsetjmp
和siglongjmp
而不是使用setjmp
和longjmp
。int sigsetjmp(sigjmp_buf env, int savesigs); void siglongjmp(sigjmp_buf env, int val);
- 这两个函数和
setjmp
和longjmp
的唯一区别是sigsetjmp
增加了参数savesigs
。如果该参数非0
,则在env
参数中保存进程的当前信号屏蔽字,此时在信号处理函数中调用siglongjmp
进行非局部跳转到sigsetjmp
,会导致恢复保存的信号屏蔽字。
- 这两个函数和
-
实例:演示在信号处理程序被调用时,系统设置的信号屏蔽字如何自动地包括刚被捕捉到的信号。也展示了使用
sigsetjmp
和siglongjmp
函数的方法#include "apue.h" #include <setjmp.h> #include <time.h> 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;
-
下图表示了该程序的执行时间顺序
- 可将上图分为3个部分:左面部分(对应于
main()
),中间部分(sig_usr1
)和右面部分(sig_alrm
) - 左面部分:信号屏蔽字是0(没有信号是阻塞的)
- 中间部分:信号屏蔽字是
SIGUSR1
- 右边部分:信号屏蔽字是
SIGUSR1|SIGALRM
- 可将上图分为3个部分:左面部分(对应于
-
从命令行输出可以看到:当调用一个信号处理程序时,被捕捉到的信号加到进程的当前信号屏蔽字中。当从信号处理程序返回时,恢复原来的屏蔽字。另外,
siglongjmp
恢复了由sigsetjmp
所保存的信号屏蔽字。如果使用longjmp
和setjmp
则不会恢复。
-
16、函数sigsuspend
-
考虑
sigprocmask
函数中提出的一点:如果在调用该函数后如果有任何未决的、不再阻塞的信号,则在函数返回之前,至少将其中之一递送给该进程。 -
那么如果针对下面代码则会出现问题:
sigprocmask(SIG_SETMASK,&oldmask,NULL); pause();
- 如果
sigprocmask
解除了某个信号阻塞,而在此期间的确该信号被阻塞了,由上所述,那么就好像该信号发生在sigprocmask
和pause
函数之间。(或者在sigprocmask
和pause
函数之间的确有某个未阻塞的信号被递送了),那么将会导致pause
函数一直阻塞下去,即sigprocmask
和pause
函数之间的这个时间窗口中的信号丢失了。
- 如果
-
针对此问题,需要在一个原子操作中解除信号阻塞并使进程休眠。因此可以使用
sigsuspend
函数,该函数在一个原子操作中先恢复信号屏蔽字,然后使进程休眠。int sigsuspend(const sigset_t *mask);
- 进程的信号屏蔽字设置为
mask
,在捕捉到一个信号或发生了一个会终止该进程的信号之前,该进程被挂起。如果捕捉到一个信号并且从该信号处理程序返回,则sigsuspend
返回,并且恢复信号屏蔽字为sigsuspend
之前的值(并且返回-1
,errno
设为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
、_Exit
、longjmp
或siglongjmp
。即abort
导致进程的非正常终止,除非SIGABRT
信号被捕获,并且信号处理函数没有返回(使用了longjmp
等函数使信号处理函数没有返回)。 - 并且
abort
不理会进程对此信号的阻塞或忽略。 - 进程捕获
abort
调用信号处理程序的意图:
在进程终止之前执行所需的清理操作。如果进程并不在信号处理程序中终止自己,则当信号处理函数返回时,abort
终止该进程。 - 如果
abort
要终止进程,则它对所有打开标准I/O
流的效果应当与进程终止前对每个流调用fclose
相同。
- 注意,若捕获此信号并且由相应信号处理函数返回,
- 此函数将
- 实例:
abort
函数的实现#include <signal.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> 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
函数忽略SIGINT
和SIGQUIT
:
由之前的知识可知,当在终端键入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
函数实现,但是可能造成sleep
和alarm
函数互相影响。比如先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
:绝对的,即希望休眠到时钟到达某个特定的时间
request
和remain
:和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
参数向信号处理程序传递整数和指针值。除此之外,sigqueue
和kill
类似. -
注意,信号不能被无限次排队。到达相应限制后,
sigqueue
会失败,errno
设置为EAGAIN
。 -
在支持实时信号扩展的
Linux
之中,sigqueue
只能用于实时信号(SIGRTMIN
~SIGRTMAN
之间的信号,包括这两个限制值),这些实时信号默认操作是终止进程。对于非实时信号(1-31信号),不支持信号排队。
-
- 使用
21、作业控制信号
- 以下六个信号与作业控制有关:
SIGCHLD
:子进程停止或终止SIGCONT
:如果进程已停止,则使其继续运行SIGSTOP
:停止信号(不能被捕捉或忽略)SIGTSTP
:交互式停止信号SIGTTIN
:后台进程组成员读控制终端SIGTTOU
:后台进程组成员写控制终端
- 当一个进程产生4种停止信号(
SIGTSTP
、SIGSTOP
、SIGTTIN
、SIGTTOU
),对进程的任一未决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 */
- 实例: