《UNIX环境高级编程》(APUE) 笔记第十章 - 信号
10 - 信号
1. 信号
信号是 软中断 ,信号提供了一种处理异步事件的方法。
当造成信号的事件发生时,为进程 产生 一个信号(或向进程 发送 一个信号)。事件 可以是硬件异常(如除以 \(0\))、软件条件(如alarm定时器超时)、终端产生的信号或调用 kill 函数。
每个信号都有一个名字,以 \(3\) 个字符 SIG
开头,定义在头文件 <signal.h>
中。信号名都被定义为 正整数常量(信号编号),不存在编号为 \(0\) 的信号(空信号)。
产生信号的事件对进程而言是随机出现的。进程不能简单地测试一个变量(如 errno)来判断是否发生了一个信号,而是必须告诉内核 “在此信号发生时,请执行以下操作” 。当对信号采取了这种操作时,称为向进程 递送 了一个信号。在信号产生和递送的时间间隔内,信号是 未决的 。
内核进行信号处理 有 \(3\) 种方式:
- 忽略此信号。大多数信号都可用这种方式进行处理,但是
SIGKILL
和SIGSTOP
不能被忽略,因为它们向内核和超级用户提供了使进程终止或停止的可靠方法。 - 捕捉信号。通知内核在某种信号发生时,调用一个用户函数,在用户函数中可执行用户希望对这种事件进程的处理。不能捕捉
SIGKILL
和SIGSTOP
信号。 - 执行系统默认动作。对于大多数信号的系统默认动作是终止该进程。终止+core 表示在进程当前工作目录的core文件中复制了该进程的内存镜像,UNIX系统调试程序使用core文件检查进程终止时的状态。
2. 函数 signal
signal 函数用于设置对应信号的处理方式:
#include <signal.h>
void (*signal(int signo, void (*func)(int)))(int);
//返回值:若成功,返回 以前 的信号处理配置;若出错,返回 SIG_ERR
\(signo\) 参数是信号名。
\(func\) 的值:
SIG_IGN
,向内核表示忽略此信号SIG_DFL
,表示接收到此信号后的动作是系统默认动作- (捕捉该信号)接到此信号后要调用的函数(信号处理程序 或叫 信号捕捉函数)的地址
返回值 是一个函数指针,指向在此之前的信号处理程序的指针。
程序启动 时,所有信号的状态都是系统默认或忽略。若 exec 函数被调用,其将原先设置为要捕捉的信号都更改为默认动作,其他信号状态则不变(一个进程原先要捕捉的信号,当其执行一个新程序后,就不能再捕捉了,因为信号捕捉函数的地址很可能在所执行的新程序文件中已无意义)。
进程创建 时,子进程继承父进程的信号处理方式,因为子进程在开始时复制了父进程内存映像。
3. 中断的系统调用
如果进程在执行一个 低速系统调用 而阻塞期间捕捉到一个信号,则该系统调用被中断。
系统调用分成两类:低速系统调用 和 其他系统调用 ,低速系统调用是可能会使进程永远阻塞的一类系统调用。
sigaction 函数使用标志 SA_RESTART
允许应用程序请求重启动被中断的系统调用。
Linux 系统中,当信号处理程序是用 signal 函数时,被中断的系统调用会重启。
自动重启的系统调用包括:ioctl 、read 、readv 、write 、writev 、wait 和 waitpid 。前五个函数只有对低俗设备进行操作时才会被信号中断。而 wait 和 waitpid 在捕捉到信号时总是被中断。
4. 可重入函数
进程捕捉到信号并对其进行处理时,进程正在执行的正常指令序列就被信号处理程序临时中断,它首先执行该信号处理程序中的指令。如果从信号处理程序返回,则继续执行在捕捉到信号时进程正在执行的正常指令序列。但在信号处理程序中,不能判断捕捉到信号时进程执行到何处,所以其结果是不可预知的。
信号处理程序中保证调用安全的函数是 可重入的 并被称为是 异步信号安全的 ,在信号处理操作期间,它会阻塞任何会引起不一致的信号发送。
不可重入函数 的原因:
- 已知它们使用静态数据结构
- 它们调用 malloc 或 free
- 他们是标准 I/O 函数(标准I/O库的很多实现都以不可重入方式使用全局数据结构)
5. SIGCLD 语义
在 Linux 中,SIGCLD 等同于 SIGCHLD ,在一个进程终止或停止时,此信号将被送给其父进程。按系统默认,将忽略此信号。
对此信号的 处理方式 是:
- 按系统默认( SIG_DFL ),SIGCLD 被忽略,则子进程可能产生僵死进程,需父进程对其等待。
- 若 SIGCLD 的配置被设置 SIG_IGN ,则调用进程的子进程终止时丢弃状态,将不产生僵死程序。如果调用进程随后调用一个 wait 函数,那么它将阻塞直到所有子进程都终止,然后该 wait 会返回 \(-1\) ,并将其 errno 设置为
ECHILD
。 - 如果将 SIGCLD 的配置设置为捕捉,则内核检查是否有子进程准备好被等待,如果有,则调用 SIGCLD 处理程序。(若在进程被安排捕捉 SIGCLD 之前已有子进程准备好被等待,此时系统不调用 SIGCLD 信号的处理程序)
6. 函数 kill 和 raise
kill 函数将信号发送给进程或进程组。raise 函数则允许进程向自身发送信号。
#include <signal.h>
int kill(pid_t pid, int signo);
int raise(int signo); //raise(signo) = kill(getpid(), signo)
//两个函数返回值:若成功,返回 0;若出错,返回 -1
\(pid\) 参数取值:
- \(pid>0\):将该信号发送给进程 ID 为 \(pid\) 的进程
- \(pid==0\):将信号发送给与发送进程属于同一进程组的所有进程(不包括系统进程集,即内核进程与init),且发送进程具有权限向这些进程发送信号
- \(pid<0\):将该信号发送给其进程组ID等于 \(pid\) 绝对值的所有进程(不包括系统进程集),且发送进程具有权限向这些进程发送信号
- \(pid==-1\):将信号发送给发送进程有权限向它们发送信号的所有进程(不包括系统进程集)
权限:
- 超级用户可将信号发送给任一进程
- 对于非超级用户,基本规则是发送者的实际用户ID或有效用户ID必须等于接受者的实际用户ID或有效用户ID。如果实现
_POSIX_SAVED_IDS
,则检查接收者的保存设置用户ID(而不是有效用户ID)
如果调用 kill 为调用进程产生信号,而且此信号是不被阻塞的,那么在 kill 返回之前,\(signo\) 或者某个其他未决的、非阻塞信号被传送至该进程。
7. 函数 alarm 和 pause
使用 alarm 函数可以设置一个定时器(闹钟时间),在将来的某个时刻该定时器会超时。当定时器超时时,产生 SIGALRM
信号。如果忽略或不捕捉此信号,则其默认动作是终止调用该 alarm 函数的进程:
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
//返回值:0 或以前设置的闹钟时间的余留秒数
参数 \(seconds\) 的值是产生信号 SIGALRM
需要经过的时钟秒数。当这一时刻到达时,信号由内核产生。
每个进程只能有一个闹钟时间。如果在调用 alarm 时,之前已为该进程注册的闹钟时间还没有超时,则该闹钟时间的余留值作为本次 alarm 函数调用的值返回。以前注册的闹钟时间则被新值代替。
pause 函数使调用进程挂起直至捕捉到一个信号:
#include <unistd.h>
int pause(void); //返回值:-1,errno设置为EINTR
只有执行了一个信号处理程序并从其返回,pause 才返回。在这种情况下,pause 返回 \(-1\) ,errno 设置为 EINTR
。
8. 信号集
信号集 能表示多个信号,定义数据类型 sigset_t 以包含一个信号集,并有以下处理信号集的函数:
#include <signal.h>
int sigemptyset(sigset_t *set); //初始化由set指向的信号集,清除其中所有信号
int sigfillset(sigset_t *set); //初始化由set指向的信号集,使其包括所有信号
//所有应用程序使用信号集之前,要对该信号集调用sigemptyset或sigfillset一次
int sigaddset(sigset_t *set, int signo); //将一个信号添加到已有的信号集中
int sigdelset(sigset_t *set, int signo); //从信号集中删除一个信号
//以上四个函数返回值:若成功,返回0;若出错,返回-1
int sigimember(const sigset_t *set, int signo); //判断是否已包含某信号。若真,返回1;若假,返回-1
9. 函数 sigprocmask
一个进程的 信号屏蔽字 ,规定了当前阻塞而不能递送给该进程的信号集。调用函数 sigprocmask 可以检测或更改进程的信号屏蔽字:
#include <signal.h>
int sigprocmask(int how, const sigset_t *restrict set, sigset_t *restrict oset);
//返回值:若成功,返回0;若出错,返回-1
若 \(oset\) 是非空指针,那么进程的当前信号屏蔽字通过 \(oset\) 返回。
若 \(set\) 是一个非空指针,则参数 \(how\) 指示如何修改当前信号屏蔽字。
在调用 sigprocmask 后如果有任何未决的、不再阻塞的信号,则在此函数返回前,至少将其中之一递送给该进程。
sigprocmask 是仅为单线程进程定义的。
10. sigpending
sigpending 函数返回一信号集,对于调用进程而言,其中的各信号是 阻塞不能递送 的,因而也一定是当前未决的。该信号通过 \(set\) 参数返回:
#include <signal.h>
int sigpending(sigset_t *set); //返回值:若成功,返回0;若出错,返回-1
11. 函数 sigaction
sigaction 函数的功能是检查或修改与指定信号相关联的处理动作。很多平台都用 sigaction 实现 signal 函数。
#include <signal.h>
int sigaction(int signo, const struct sigaction *restrict act, struct sigaction *restrict oact);
//返回值:若成功,返回0;若出错,返回-1
\(signo\) 是要检测或修改其具体动作的信号编号。若 \(act\) 指针非空,则要修改其动作。如果 \(oact\) 指针非空,则系统经由 \(oact\) 指针返回该信号的上一个动作。
关于连续发送同一信号的处理:
若更改后的信号动作是 捕捉函数 而不是 SIG_IGN 或 SIG_DFL ,则调用该信号捕捉函数之前,此信号被添加到进程的信号屏蔽字中,仅当从信号捕捉函数返回时再将进程的信号屏蔽字恢复为原先值。因此保证了在处理一个给定的信号时,如果这种信号再次发生,那么它会被阻塞到对前一个信号的处理结束为止。若同一种信号多次发生,通常并不将它们加入队列,所以如果某种信号在被阻塞时多次发生,其信号处理函数只会被调用一次。
若更改后的信号动作是 捕捉函数 而不是 SIG_IGN
或 SIG_DFL
,则调用该信号捕捉函数之前,此信号被添加到进程的信号屏蔽字中,仅当从信号捕捉函数返回时再将进程的信号屏蔽字恢复为原先值。因此保证了在处理一个给定的信号时,如果这种信号再次发生,那么它会被阻塞到对前一个信号的处理结束为止。
12. 函数 sigsetjmp 和 siglongjmp
信号处理程序中调用 longjmp 有一个问题:当捕捉到一个信号时,进入信号捕捉函数,此时当前信号被自动地加到进程的信号屏蔽字中。这阻止了后来产生的这种信息中断该信号处理程序。如果用 longjmp 跳出信号处理程序,进程的信号屏蔽字可能无法恢复。
信号处理程序使用 sigsetjmp 和 siglongjmp 进行非局部转移:
#include <setjmp.h>
int sigsetjmp(sigjmp_buf env, int savemask); //返回值:若直接调用,返回0;若从siglongjmp调用返回,则返回非0
void siglongjmp(sigjmp_buf env, int val);
这两个函数和 setjmp 、longjmp 之间的唯一 区别 是 sigsetjmp 增加了一个参数。如果 \(savemask\) 非 \(0\) ,则 sigsetjmp 在 \(env\) 中保存进程的当前信号屏蔽字。调用 siglongjmp 时,如果非 \(0\) \(savemask\) 的 sigsetjmp 调用已经保存了 \(env\) ,则 siglongjmp 从其中恢复保存的信号屏蔽字。
13. 函数 sigsuspend
sigsuspend 函数在一个原子操作中设置信号屏蔽字,然后使进程休眠:
#include <signal.h>
int sigsuspend(const sigset_t *sigmask);
进程的信号屏蔽字设置为由 \(sigmask\) 指向的值。在捕捉到一个信号或发生了一个会终止该进程的信号之前,该进程被挂起。如果捕捉到一个信号而且从该信号处理程序返回,则 sigsuspend 返回,并且该进程的信号屏蔽字设置为调用 sigsuspend 之前的值。
此函数 没有返回值 ,如果它返回到调用者,则总是返回 \(-1\) ,并将 errno 设置为 EINTER
(表示一个被中断的系统调用)。
sigsuspend 函数可用于:
- 保护代码临界区,使其不被特定信号中断(在此功能中起到 pause 作用)
- 等待一个信号处理程序设置一个全局变量
- 实现父、子进程之间的同步
14. 函数 abort
abort 函数的功能是使程序异常终止:
#include <stdlib.h>
void abort(void);
abort 将 SIGABRT
信号发送给调用进程(进程不应忽略此信号)。实质上,abort 函数向主机环境递送一个 未成功终止 的通知。
让进程捕捉 SIGABRT 的意图是:在进程终止之前由其执行所需的清理操作。如果进程并不在信号处理程序中终止自己,当(SIGABRT)信号处理程序返回时,abort 终止该级才能拿,且对所有打开标准 I/O 流的效果应当与进程终止前对每个流调用 fclose 相同。
15. 函数 sleep、nanosleep 和 clock_nanosleep
#include <unistd.h>
unsigned int sleep(unsigned int seconds);
此函数调用进程被挂起直到满足下面两个条件之一:
- 已经过了 \(seconds\) 所指定的墙上时钟时间(返回值为 \(0\) )
- 调用进程捕捉到一个信号并从信号处理程序返回(返回值为未休眠完的秒数)
nanosleep 函数提供了纳秒级的精度:
#include <time.h>
int nanosleep(const struct timespec *reqtp, struct timespec *remtp);
\(reqtp\) 参数用秒和纳秒指定了需要休眠的时间长度。如果某个信号中断了休眠间隔,进程并没有终止,\(remtp\) 参数指向的 timespec 结构就会被设置为未休眠完的时间长度。
clock_nanosleep 函数可使用相对于特定时钟的延迟时间来挂起调用线程:
int clock_nanosleep(clockid_t clock_id, int flags, const struct timespec *reqtp, struct timespec *remtp);
\(clock_id\) 参数指定了计算延迟时间基于的时钟。
\(flags\) 参数用于控制延迟是相对的还是绝对的:
- \(flags\) 为 \(0\) 时表示休眠时间是相对的
- 若 \(flags\) 值设置为
TIMER_ABSTIME
,表示休眠时间是绝对的(例如,希望休眠时间到达某个特定的时间)