CSDN上的的 APUE读书笔记之第十章 -- 信号

20 篇文章 0 订阅

第十章 信号


信号机制是本书或者说是 Unix 应用程序设计的重点和难点之一。要安全的编写一个信号捕捉函数,需要较为精细和周全的设计。既要防止异步信号意外丢失而无法捕捉,也要防止执行异步处理时出现的并发破坏进程数据,在处理异常信号时试图使用 siglongjmp(3)之类的函数恢复进程状态时,还要防止跳转到非法的栈空间。所以信号处理程序是 bug 常出现的地方之一。实践经验和多参考前人的例子都是很重要的。另外一本书 UNIX System Programming: Communication, Concurrency and Threads(中文书名为《UNIX 系统编程》)对信号处理也有较为深入的探讨和详例。


1、基本概念

信号(Signal)是一种事件驱动的软件中断机制。当进程触发了某些系统事件(多数是异步事件,例如键盘输入、软硬件异常、人为地通过其它进程来产生等,也可以通过kill(2)等函数或kill(1)命令使信号能显式的产生)时,内核将向进程发送相应的信号。信号的值是一个正整数,为 0 时是空信号,系统对此无定义。

要注意信号(Signal)和信号量(Semaphore)的区别,特别是阅读中文资料的时候。一般为示区别,比较靠谱的资料都将 Signal 译为信号,而 Semaphore(见第 15 章:进程间通信)通常译为信号量(也有些资料译为信号灯)。但这两个名词在中文资料中至今都不尽规范,把 Signal 也称为“信号量”的也不少见。例如 APUE2的中文译本在 435、704这几页的习题中估计是校对错误,也把 Signal 译成了“信号量”,造成名词翻译前后含义混淆的错误。所以关键还是要理解其含义用途而不应从它的中文名称判断。

早期 UNIX 的信号机制实现并不可靠。现代可靠的信号机制的术语主要包括:  

  • 产生(Generate):指内核生成一个信号;  
  • 未决(Pending):指信号已经产生,但尚未递送的状态。例如信号被进程阻塞;  
  • 递送(Deliver):指信号发送的目标进程已经针对信号作出了反应,这个反应包括忽略、以系统默认的方式处理、以自定义的信号捕捉函数处理三种方式之一;SIGSTOP 和SIGKILL 这两个信号不能被忽略;被信号终止的进程需要父进程通过 wait(2)收集其退出状态才真正在进程表中释放; 
  •  阻塞(Blocking):指产生的信号无法被处理,如果这个信号的动作不是忽略,则处于未决状态;  
  • 屏蔽字(Mask):也称掩码等,用于阻塞指定的信号;
对信号所采取的动作有忽略、按系统默认、自定义信号捕捉函数三种处理方式。只有使用自定义的信号捕捉函数处理信号的动作才叫做“捕捉”。进程调用了 exec(3)家族函数执行程序时,原先设置为捕捉的信号都将改为按系统默认方式处理。而fork(2)之后的子进程继承父进程设置的信号处理方式。
收到某些异常信号而终止的进程,将会将其内存中的内容作为调试信息转储(coredump)到工作目录(若进程对目录有写权限,创建模式为 0666  XOR 进程 umask值)下。若要禁用内核转储,可使用以下命令:
$ ulimit -c 0

2、常用信号略述
SIGABRT: 使进程异常终止,一般通过调用 abort(3)函数产生,默认为终止进程+core;
SIGALARM 进程闹钟定时器超时,闹钟定时器一般通过调用 alarm(2)函数设置,默认为终止进程;
SIGCHLD 子进程退出时向父进程产生,默认为忽略,wait(2)家族将捕捉该信号;
SIGCONT 默认为使处于暂停(stopped)状态的进程继续运行;SIGFPE 除 0、浮点溢出等算术运算异常时产生该信号,默认为终止进程+core;
SIGHUP 使终端连接断开,默认为终止进程。前台进程组的会话首进程(拥有控制终端)终止时,该信号将发送给进程组的每一个进程;守护进程也常通过捕捉该信号来重新读取配置文件;
SIGINT 键盘输入 Ctrl+C 等情况下产生此信号,发送给前台进程组的每一个进程;默认为终止;
SIGKILL 终止进程。该信号不能捕捉或忽略;
SIGPIPE 管道或面向流的套接字(SOCK_STREAM)断开时产生该信号,默认为终止进程;
SIGQUIT 用于 Ctrl + \等退出键(Ctrl+4 on Linux)终止前台进程组,同时 coredump;SIGSEGV 进行了无效的内存引用或者段错误(Segment Fault)时产生此信号,终止进程+core;SIGSTOP 此信号用于暂停进程的运行,不能捕捉或忽略;
SIGSYS 无效的系统调用。不兼容当前系统的二进制程序可能会产生这种异常,终止进程+core;我的 Ubuntu 8.10 上的 kill(1)工具指出该信号在某些 Linux 上可能未实现。SIGTSTP 由 tty 产生的停止信号,通常通过键入 Ctrl + Z 发送给前台进程组。将挂起进程;
SIGTTIN /SIGTTOU 后台进程组需要从终端输入/输出时产生,将挂起进程。如果进程组为孤儿进程组,此信号将导致 errno 被设置,除非忽略或者阻塞此信号。
SIGURG 紧急情况。常用于网络连接收到带外数据中(见第 16 章“网络 IPC:套接字”);

3、signal(2)函数
#include <signal.h>
void (*signal(int signo, void (*func)(int))(int);
signal(2)注册一个对指定的信号 signo 的信号处理动作 func。func 可以是 SIG_IGN(忽略指定信号)、SIG_DFL(用系统默认处理信号),或者是指定的信号捕捉函数(或称信号处理程序)地址。该指定的信号捕捉函数应有一个 int 类型参数(实际上这个参数基本上没有用),无返回值。signal(2)返回之前的信号捕捉函数的指针,用于备份。
函数调用成功后,在进程收到此信号时,进程将被中断并去执行指定的信号处理动作。在信号处理程序返回之前,进程不会继续执行;
信号处理程序执行时,进程将自动阻塞被处理的信号,并在信号处理程序执行结束后恢复。
signal 函数提供了一种简易的信号捕捉函数注册方式。但如果需要考虑可移植性(特别是用在不能提供可靠信号机制的早期UNIX 上)或者更具体的功能,则应使用sigaction(2)代替之以避免意外。

4、早期 UNIX 实现的不可靠信号机制
不可靠的原因包括:信号丢失、进程难以捕捉到信号等;
主要的问题: 
  •  系统处理信号时,总是立即将其信号的处理方式复位为系统默认方式,引起难以捕捉的问题;  
  • 没有阻塞机制,容易导致信号丢失;
现代 Unix-like 系统都提供了可靠的信号机制。

5、系统调用的自动重启动问题
低速系统调用:指由于等待异步事件而可能会使进程出现永久性阻塞的系统调用。阻塞期间进程一旦递送了信号,则该系统调用会马上返回,并置errno 为 EINTR。查看某函数的man手册的ERROR 小节时,如果有在调用失败时会出现返回-1并设置 errno 为 EINTR 的描述,则说明它会被信号中断,是一个低速系统调用。
现代 Unix 系统对因捕捉信号而返回的系统调用都有自动重启动的实现。如果拿不准的话,也可以自己通过包装系统调用而实现其自动重启库的版本。或者利用sigaction(2)函数显式的指定被信号中断时是否重启动系统调用。

6、可重入函数与异步信号安全
由于信号处理函数是可以被异步的调用的。所以要注意异步事件下在公共资源上产生并发的安全问题。重入性(re-entrant)和异步信号安全(async-signal safe)是描述安全的信号处理函数的重要术语。
可重入函数(reentrant functions)的意思是:函数在多次被调用时不会由于并发而出现不安全的动作。Wikipedia 上对可重入函数的特征描述为:
  • Must hold no static (global) non-constant data(使用的数据必须是非静态或非全局变量).   
  • Must not return the address to static (global) non-constant data(不能返回静态或全局变量的地址).   
  • Must work only on the data provided to it by the caller(只能使用调用者传递的数据).   
  • Must not rely on locks to singleton resources(不能依赖单例对象资源的锁——这是针对面向对象机制而言的).   
  • Must not call non-reentrant functions(不能调用不可重入函数). 
应注意的是,调用了 malloc(3)家族的函数及调用了标准 IO 库的函数都是不可重入的。因为它们都使用了公共的资源。
如果在信号处理函数中能安全的调用一个子函数,则称这个子函数是异步信号安全(async-signal safe)的。SUS 给出了能保证异步信号安全的库函数列表。
要安全的编写信号处理函数,则应防止调用不可重入函数,同时应能安全的备份和恢复 errno。尤其是程序在以不可重入的方式修改全局性的数据时,应阻塞会引起sigsetjmp(3)执行的信号。
书中在描述重入的概念时没有专门提及临界区(critical section)的概念。临界区实际上指的是要访问(读写)全决的公共资源的代码,而不是资源本身。不少资料也称之为 critical region(例如 APUE2的Figure 10.22 即中文版的程序清单 10-15,以及 LKD 的 Ch.8),但后者在 wikipedia 上指的是统计假设检验中区别原假设与备择假设的取值范围。这样看,critical section 比critical region 更靠谱更不容易混淆其含义,中文似乎也应该称为“临界代码段”更靠谱。但“临界区”这个词实际上俨然已经是业界接受的术语了,呵呵。

7、SIGCHLD 与 SIGCLD 信号 
  • 在 Solaris、Linux、Mac OS X、FreeBSD 中,SIGCHLD 等价于 SIGCLD; 
  • 在进程忽略 SIGCLD 信号时,子进程结束会变成僵死态因平台不同而异:4.4BSD 及其后代FreeBSD 会;而 SVR4、Solaris、Linux 和 Mac OS X 都不会;  
  • 进程设置对信号 SIGCLD 的动作为捕捉时,内核将立即检查是否已经有子进程准备好被等待。若是,则调用捕捉函数;——对于因系统自动复位信号动作的早期UNIX 实现可能会引起产生问题。 
  • wait(2)函数本身不处理 SIGCHLD 信号,但一般可以在捕捉 SIGCHLD 信号时 wait 之,或者忽略此信号直接wait。但对早期UNIX 的不可靠信号,应先wait 再用signal 处理SIGCHLD。
8、产生信号:kill 和 raise 函数
#include <stdio.h>
int kill(pid_t pid, int signo);
int raise(int signo);

其中
raise(signo);
等价于
kill(getoid(), signo);
对于 kill(2),pid 的参数为 0时用于对进程组发信号,即对所有 PGID 一样的进程发同一个信号;为负数时用于对指定进程组发信号;为-1时对所有进程发信号。使用kill(2)需要相关的进程用户权限;
raise(3)用来为调用进程产生一个信号。注意不少面向对象的语言中有“抛出异常 Throwing exception”的概念,用法就是类似于 C 中的 raise 函数;
当参数signo 取0时可用于检查进程是否存在,因为kill(2)发送一个信号给不存在的进程时将返回-1。但这种测试除非能保证原子的执行,否则并不可靠。

9、alarm 和 pause
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
该函数以秒为单位设置进程的闹钟定时器,超时时,内核将产生SIGALARM 信号并发送到调用进程。该信号的默认动作是终止进程。
如果未超时时再次调用alarm,进程闹钟将重新开始计时,并返回原先剩余的时间。
参数 seconds 为 0 时将直接清除进程闹钟的计时。
#include <unistd.h>
int pause(void);
该函数将使进程在调用处进入挂起状态等待该进程处理一个信号。处理后,pause(2)将返回-1并设置errno。

10、信号集及其处理函数
信号集顾名思义就是一组信号的集合,数据类型定义为 sigset_t,不应追究这个类型由什么基本类型封装的。对这个数据类型有如下操作函数:
#include <signal.h>
int sigemptyset(sigset_t *set);
int sigfillset(siget_t *set);
int sigaddset(sigset_t *set, int signo);
int sigdelset(setset_t *set, int signo);
int sigismember(const sigset_t *set, int signo);
依次分别是:清空指定的信号集、填满指定的信号集、在指定的信号集中增加一个信号,在指定的信号集中删除一个信号。成功返回 0,失败返回-1。最后一个测试指定信号是否信号集内,然则返回真值。
sigprocmask(2)用于维护进程的阻塞信号集(称为信号屏蔽字或信号掩码,signal mask):
#include <signal.h>
int sigprocmask(int how, const sigset_t *restrict set, sigset_t *restrict oset);
该函数以how 指定的方式将信号集set 设置为调用进程的信号屏蔽字,同时把原信号屏蔽字取值保存到 oset 中作为备份,用于以后需要时还原。
how 包括了 SIG_BLOCK(从当前信号屏蔽字中增加指定信号集中的信号)、SIG_UNBLOCK(从当前信号屏蔽字中删除指定信号集中的信号)、SIG_SETMASK(用指定的信号集替换信号屏蔽字)。
若指针set 为NULL,则how 不起作用,此时函数用来取当前信号屏蔽字到oset。
新的信号屏蔽字要等到该函数返回后才生效。
#include <signal.h>
int sigpending(sigset_t *set);
sigpending(2)函数用于取当前因阻塞而未决的信号集到指定的指针 set 中。
#include <signal.h>
int sigsuspend(const sigset_t *sigmask);
该函数实现了sigprocmask(2) + pause(2)的原子操作。它使进程挂起并等待信号,并使用指定的信号集 sigmask 决定是否阻塞还是处理相关信号。
注意:根据FreeBSD、Linux 及Solaris 的手册,均称 sigsuspending(2)中的参数sigmask 是用于“替换(set to 或者 replace)”而不是“加(add to)”到当前的进程信号屏蔽字中。实际编程验证也是这样。原书(“Section 10.16 Figure 10.22”之前一段)和中文译本(271 页)对此都有些表述不明。原文:We added SIGUSR1 to the mask installed when we called sigsuspend so that when the signal handler ran, we could tell that the mask had actually changed.
该函数常用于等待特定信号产生并处理,这种情况下调用前一般应先用sigprocmask(2)阻塞该特定信号,以防止该信号丢失,同时要注意前后对进程屏蔽字的备份和恢复,以及要考虑是否会影响对其它信号的处理等。
程序清单 10-15同时也说明了信号捕捉函数会阻塞当前信号。

11、sigaction 函数
该函数也用于注册信号处理的方式,但比signal(2)功能更强,更安全。
#include <signal.h>
int sigaction(int signo, const struct sigaction *restrict act, struct sigaction *restrict oact);
对于信号 signo,使用结构 sigaction 的对象 act 处理之,并把原 sigaction 对象的值保存备份到 oact。结构 sigaction 定义为:
struct sigactgion 
{
void (*sa_handler)(int);
sigset_t sa_mask;
int sa_flags;
void (*sa_sigaction)(int siginfo_t *, void *);
}
第一个字段定义了信号捕捉函数,用法同 signal(2)中的一样;
第二个字段定义了信号屏蔽字,注册后将加到进程原先的信号屏蔽字中,同时在信号捕捉函数被调用时,还将隐式的加上所处理的信号,在信号捕捉函数结束后再隐式的恢复调用前的阻塞状态;
第三个字段为可选参数,详细设置进程对信号的行为,例如选择是否会重启动低速系统调用等。略;
第四个字段在 sa_flags 中指定 SA_INFO 时用作信号捕捉函数,它与 sa_handler 同一个地址,但追加了更多的参数描述进程与信号的状态。

12、sigsetjmp 和 siglongjmp 函数
#include <setjmp.h>
int sigsetjmp(sigjmp_buf env, int savemask);
void siglongjmp(sigjmp_buf env, int val);
通过信号处理异常时,可能需要使进程恢复到某个状态。这两个函数在信号处理程序中,代替setjmp(3)和 longjmp(3)执行非局部转移的功能,使用方法类似。而 sigsetjmp(3)中的标志 savemask 取非 0值时将备份调用时进程的信号屏蔽字,这样 setlongjmp(3)在跳转后会将信号屏蔽字恢复到该备份的信号集。
应注意避免调用 sigsetjmp之前就因信号产生而执行 siglongjmp,导致了错误的跳转。可以利用sig_atomic_t 类型的变量作为判断条件,ISO C 定义这种类型的数据在任何时候访问时都是一个原子操作(只需要执行一条机器指令),即每次都可以可重入地读写,可以作为安全的全局数据。

13、实例:在 abort、system 及 sleep 函数的实现中使用信号机制

14、作业控制信号
对某一个进程产生 SIGSTOP、 SIGTSTP、SIGTTIN、SIGTTOU 信号使之停止执行时,如果有未决的SIGCONT 信号存在,则将被丢弃。反之亦然。
对一个停止的进程产生一个 SIGCONT 信号时,不管该信号是否会被进程阻塞或忽略,都会使进程恢复执行。

15、信号值与信号名字之间的转换
#include <signal.h>
void psignal(int signo, const char *msg);
char *strsignal(int signo);
psinal(3)的用法类似 perror(3);strsignal(3)类似 strerror(3);
另外 Solaris 的库函数中还提供了一对函数 sig2str 和 str2sig,但并非 POSIX 标准,其它如 FreeBSD 和Linux 等的系统函数库也没有实现这两个函数;








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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值