10 - 信号
GitHub 地址
1. 信号
信号是 软中断 ,信号提供了一种处理异步事件的方法。
当造成信号的事件发生时,为进程 产生 一个信号(或向进程 发送 一个信号)。事件 可以是硬件异常(如除以 \(0\))、软件条件(如alarm定时器超时)、终端产生的信号或调用 kill 函数。
每个信号都有一个名字,以 \(3\) 个字符 SIG
开头,定义在头文件
中。信号名都被定义为 正整数常量(信号编号),不存在编号为 \(0\) 的信号(空信号)。
产生信号的事件对进程而言是随机出现的。进程不能简单地测试一个变量(如 errno)来判断是否发生了一个信号,而是必须告诉内核 “在此信号发生时,请执行以下操作” 。当对信号采取了这种操作时,称为向进程 递送 了一个信号。在信号产生和递送的时间间隔内,信号是 未决的 。
内核进行信号处理 有 \(3\) 种方式:
- 忽略此信号。大多数信号都可用这种方式进行处理,但是
SIGKILL
和SIGSTOP
不能被忽略,因为它们向内核和超级用户提供了使进程终止或停止的可靠方法。 - 捕捉信号。通知内核在某种信号发生时,调用一个用户函数,在用户函数中可执行用户希望对这种事件进程的处理。不能捕捉
SIGKILL
和SIGSTOP
信号。 - 执行系统默认动作。对于大多数信号的系统默认动作是终止该进程。终止+core 表示在进程当前工作目录的core文件中复制了该进程的内存镜像,UNIX系统调试程序使用core文件检查进程终止时的状态。
2. 函数 signal
signal 函数用于设置对应信号的处理方式:
#include
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
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
unsigned int alarm(unsigned int seconds);
//返回值:0 或以前设置的闹钟时间的余留秒数
参数 \(seconds\) 的值是产生信号 SIGALRM
需要经过的时钟秒数。当这一时刻到达时,信号由内核产生。
每个进程只能有一个闹钟时间。如果在调用 alarm 时,之前已为该进程注册的闹钟时间还没有超时,则该闹钟时间的余留值作为本次 alarm 函数调用的值返回。以前注册的闹钟时间则被新值代替。
pause 函数使调用进程挂起直至捕捉到一个信号:
#include
int pause(void); //返回值:-1,errno设置为EINTR
只有执行了一个信号处理程序并从其返回,pause 才返回。在这种情况下,pause 返回 \(-1\) ,errno 设置为 EINTR
。
8. 信号集
信号集 能表示多个信号,定义数据类型 sigset_t 以包含一个信号集,并有以下处理信号集的函数:
#include
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
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
int sigpending(sigset_t *set); //返回值:若成功,返回0;若出错,返回-1
11. 函数 sigaction
sigaction 函数的功能是检查或修改与指定信号相关联的处理动作。很多平台都用 sigaction 实现 signal 函数。
#include
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
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
int sigsuspend(const sigset_t *sigmask);
进程的信号屏蔽字设置为由 \(sigmask\) 指向的值。在捕捉到一个信号或发生了一个会终止该进程的信号之前,该进程被挂起。如果捕捉到一个信号而且从该信号处理程序返回,则 sigsuspend 返回,并且该进程的信号屏蔽字设置为调用 sigsuspend 之前的值。
此函数 没有返回值 ,如果它返回到调用者,则总是返回 \(-1\) ,并将 errno 设置为 EINTER
(表示一个被中断的系统调用)。
sigsuspend 函数可用于:
- 保护代码临界区,使其不被特定信号中断(在此功能中起到 pause 作用)
- 等待一个信号处理程序设置一个全局变量
- 实现父、子进程之间的同步
14. 函数 abort
abort 函数的功能是使程序异常终止:
#include
void abort(void);
abort 将 SIGABRT
信号发送给调用进程(进程不应忽略此信号)。实质上,abort 函数向主机环境递送一个 未成功终止 的通知。
让进程捕捉 SIGABRT 的意图是:在进程终止之前由其执行所需的清理操作。如果进程并不在信号处理程序中终止自己,当(SIGABRT)信号处理程序返回时,abort 终止该级才能拿,且对所有打开标准 I/O 流的效果应当与进程终止前对每个流调用 fclose 相同。
15. 函数 sleep、nanosleep 和 clock_nanosleep
#include
unsigned int sleep(unsigned int seconds);
此函数调用进程被挂起直到满足下面两个条件之一:
- 已经过了 \(seconds\) 所指定的墙上时钟时间(返回值为 \(0\) )
- 调用进程捕捉到一个信号并从信号处理程序返回(返回值为未休眠完的秒数)
nanosleep 函数提供了纳秒级的精度:
#include
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
,表示休眠时间是绝对的(例如,希望休眠时间到达某个特定的时间)