一、信号及其处理过程
1、概述
信号是事件发生时对进程的通知机制,有时也称之为软件中断。它的执行机制有点类似于中断,
打断正常的主程序流的执行,但没有进程切换的过程。
1.1、发送信号
- 可来自进程本身(属于同步信号,是由本进程的特定代码或代码错误产生);也可以来自其他进程(属于异步信号,也就是我们无法预测本进程代码执行的什么时候,此信号会发生或被接收处理)。
1.2、待处理信号集合
- 信号发送成功后,会加入目标进程的待处理信号集合;待处理信号集合中各种信号不会排队,每种信号最多有一个,后面发送来的信号会被丢弃,这是信号处理程序需要注意的一个重要问题。
1.3、接收信号与阻塞信号
- 信号处理的时机:
- 进度在前度超时后,再度获得调度时;
- 系统调用完成时(信号的传递可能一起正在阻塞的系统调用过早的完成)
- 在信号处理的时机,系统会检查进程的待定且未阻塞的信号集合,从中挑选一个信号来接收并处理,此处应注意:
- 我们不能对信号处理的顺序进行任何假设;
- 阻塞可以阻止信号被接收和处理;而信号处理程序本身也可以被信号打断;所以合理阻塞信号使我们编写安全信号处理程序的关键之一;
- 信号处理程序执行过程中,一般会阻塞相同类型的信号,但这不是一定发生的;我们可以通过设置来改变这一属性
1.4 信号处理函数的终止
- 如果程序从信号处理函数返回,那么他会自动调用sigreturn()函数,该函数会恢复信号调用前保存的上下文,继续执行主函数的代码;
- 使用_exit()终止函数;
- 使用kill发送来的信号杀死进程;
- 从信号处理器函数中执行非本地跳转,转到sigsetjump处执行;
- 使用abort函数,终止进程,并产生核心转储。
2、发送信号
2.1、kill函数
#include<signal.h>
/* Return 0 on success, or -1 on error */
int kill(pid_t pid, int sig);
2.1.1、其中pid的值:
- pid >0:信号发送给pid指定的那个进程;
- pid == 0:发送信号给调用进程通知的每个进程(包含调用进程自己);
- pid < -1:发送信号给进程组ID与该pid绝对值相等的进程组内的每个进程;
- pid == -1:发送信号给调用信号有权发送的所有进程;
2.1.2、信号发送的权限:
- 特权进程可以发送信号给所有进程;
- 以root用户和组运行的init进程是一种特例,仅能接收已安装了处理函数的信号;
- 如果发送者的实际或有效用户ID匹配于接受者的实际用户ID或者保存的设置用户ID,那么非特权进程可以向接受者进程发送信号。
- SIGCONT信号需要特殊处理;非特权进程可以向同一个会话中的任何一个进程发送这一信号。
2.1.3、错误返回的errno标志
- EINVAL:非法信号
- EPERM:没有权限
- ESRCH:进程不存在(可以用来检查进程,但进程存在不表示进程在执行,僵尸进程也会显示进程存在)
2.1.4、Linux权限管理(拓展)
- 权限管理的概念:
- 实际用户ID:顾名思义,就是运行进程时的实际用户的ID
- 有效用户ID:当进程尝试各种操作时,将根据有效用户ID、有效组ID连同辅助组ID一起来确定授予进程的权限。也就是说有效用户ID跟进程的实际权限有关。
- 设置用户ID程序:文件的属主可以通过chmod程序设置此ID,其他用户ID在运行此程序时,可以将文件属主ID作为有效用户ID来执行。
- 保存的设置用户ID:由对应的有效ID复制而来。
- 有效用户ID的处理方式(调用了设置用户ID程序的程序文件):
- 设置用户ID的程序,设置为执行文件的属主ID;
- 根据进程使用的权限:如果需要用到文件属主权限,就设置成文件的属主ID;否则设置成进程的实际用户ID。
- 组ID的处理方式跟上面相似。
2.2、发送信号的其他方式
2.2.1、raise函数(向自身发送信号)
#include<signal.h>
int raise(int sig);
单线程程序相当于:kill(getpid(),sig);
2.2.2、killpg函数
#include<signal.h>
int killpg( int pgrp, int sig);
相当于kill(-pgrp, sig);
3、信号集、待处理信号、阻塞信号
3.1、信号集处理:
设置空信号集、全信号集、增加一个信号、减少一个信号、检查信号是否属于信号集;得到两个信号集的交集、并集、检查信号集是否为空;
#include<signal.h>
int sigemptyset(sigset_t *set );
int sigfillset(sigset_t *set );
int sigaddset(sigset_t *set, int sig);
int sigdelset(sigset_t *set, int sig);
int sigismember(const sigset_t *set, int sig);
#define _GNU_SOURCE
#include<signal.h>
int sigaddset(sigset_t *set, sigset_t *left, sigset_t *right);
int sigorset(sigset_t *set, sigset_t *left, sigset_t *right);
int sigisempty(const sigset_t *set );
3.2、处于等待状态的信号
- 该函数通过set返回当前等待集合中的信号
#include<signal.h>
int sigpending(sigset_t *set);
3.3、阻塞信号传递(信号掩码)
3.3.1、概述
- 信号掩码实际上属于线程属性,每一个线程有自己独立的信号掩码集合;
- 当调用信号处理函数时,会将引发调用的信号自动添加到信号掩码中。可以在使用sigaction函数安装处理函数时,通过标志位使这个添加过程不发生。
- 使用sigprocmask函数可以随时向信号掩码添加或移除信号。
- 如果有任何等待信号因对sigprocmask()的调用而接触阻塞,那么在此调用返回前,那么进程会接收此信号。?
3.3.2、sigprocmask函数
#include<signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
其中 how可为如下值:
SIG_BLOCK:将set指定的信号集添加到信号掩码
SIG_UNBLOCK:将set指定的信号集从信号掩码中移除
SIG_SETMASK:将set指定的信号集赋值给信号掩码。
4、改变信号处理
4.1、信号的默认行为
- 进程终止(注意进程终止有很多方式,不一定通过exit函数退出)
- 进程终止其转储内存
- 进程停止(挂起)直到被SIGCONT信号重启
- 进程忽略此信号
4.2、signal()函数
#include<signal.h>
typedef void(*sighandler_t )(int);
/*返回之前的处理函数 */
sighandler_t signal(int sig, sighandler_t handler);
其中:
- sig是待处理的信号;
- handler是标识信号到达时所调用函数的地址,可以是SIG_DFL(使信号处置重置为默认值)、SIG_IGN(忽略该信号)或者我们自定义的行为
- 如果失败返回SIG_ERR
4.2.1、可移植性说明
- 不同UNIX系统中signal函数的行为有所不同,主要表现为改为自定义处置函数时,执行后是否阻塞信号及恢复默认行为,所以最好使用后面介绍的sigaction函数来改变信号处理的行为
- signal函数使用SIG_DFL、SIG_IGN是不存在可移植性问题的,我们可以放心使用。
4.3、sigaction()函数
4.3.1、与signal函数的比较
- sigaction是标准函数,不存在可移植性的问题;sigaction函数更复杂,也更灵活。
- sigaction可以实现调用时阻塞信号、重启系统调用、设置信号默认值、传递更多信息等功能
#include<signal.h>
/*正确返回0,出错返回-1;oldact用来返回之前的处理器结构;*/
int sigaction(int sig, const struct sigaction *act, struct sigaction *oldact);
struct sigaction{
union{
void (*sa_handler)(int);
void (*sa_sigaction)(int,siginfo_t *, void *);
}__sigaction_handler;
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
}
#define sa_handler __sigaction_handler.sa_handler;
#define sa_sigaction __sigaction_handler.sa_sigaction;
其中:
- __sigaction_handler可以选择两种方式之一,其中sa_handler与signal中handler相同,而sa_sigaction用于sa_flags选择SA_SIGINFO时,此时可以传递更多信息(后面详细叙述)
- sa_mask:定义一组信号,调用处理函数时,阻塞这些信号。注意正在处理的信号是默认阻塞的,除非sa_flags设置SA_NODEFER标志,此时不会阻塞正在处理的信号。
- sa_flags:用来指定用于处理过程的各种选项
- sa_restorer字段供内部使用,用来在处理函数返回时,恢复进程上下文。
4.3.2、sa_flag信号说明
- SA_NOCLDSTOP:sig为SIGCHLD时,当信号导致子进程停止或继续时,父进程不会收到SIGCHLD信号。
- SA_NOCLDWAIT:sig为SIGCHLD时,子进程终止时,不会转换为僵尸进程,直接被回收
- SA_NODEFER:执行信号的处理函数时,不会将该信号类型加入进程掩码中;也就是信号处理函数可以被自身类型信号中断,并调用自身信号处理函数
- SA_ONSTACT:此标识置位时,针对该sig的处理函数在由sigaltstack()安装的备用栈上执行。
- SA_RESETHAND:只有第一次使用当前定义的信号处理函数,调用后恢复信号处理函数的默认值。
- SA_RESTART:自动重启由信号处理器中断的系统调用
- SA_SIGINFO:调用信号处理器程序时,会携带额外的信息。
4.3.2.1、signal函数实现——使用sa_flag的实例
#include <signal.h>
#include <unistd.h>
sighandler_t mysignal(int sig, sighandler_t handler) {
struct sigaction newDisp, prevDisp;
newDisp.sa_handler = handler;
sigemptyset(&newDisp.sa_mask);
write(1, "anfengchen", 10);
#ifdef OLD_SIGNAL
/* 设置恢复默认值和不阻塞当前处理信号的标志位*/
newDisp.sa_flags = SA_RESETHAND | SA_NODEFER;
#else
/* 设置重启系统调用的标志位 */
newDisp.sa_flags = SA_RESTART;
#endif
if (sigaction(sig, &newDisp, &prevDisp) == -1)
return SIG_ERR;
else
return prevDisp.sa_handler;
}
- 通过OLD_SIGNAL宏的定义来区分signal函数的新老语义;
- 老的signal语义:不阻塞同种信号,处理完当前信号,将处理器恢复为默认值,不重启阻塞的系统调用;
- 新的signal语义:阻塞同种信号,处理器一经定义,直到重新定义为止,重启阻塞的系统调用。
4.3.2.2、abort函数实现——使用sa_flag的实例
4.3.2.2.1、abort函数的功能
- 函数abort终止其调用进程,并生产核心转储;
- 函数abort通过差生SIGABRT信号来终止调用进程
- 无论阻塞或者忽略SIGABRT信号,abort()函数均不受影响;
- 除非进程捕获SIGABRT信号后处理信号尚未返回,否则abort()必须终止进程,也就是说非本地跳转退出函数可以阻止abort函数的效果
4.3.2.2.2、abort函数的实现
#define _GNU_SOURCE
#include "tlpi_hdr.h"
#include <setjmp.h>
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
static sigjmp_buf env;
void handler1(int sig) { write(1, "handler1 was called\n", 20); }
void handler2(int sig) {
write(1, "handler2 was called\n", 20);
siglongjmp(env, 1);
}
void Myabort() {
sigset_t set;
/*去除对SIGABRT信号的阻塞,按照之前自定义的SIGABRT的处理函数,来处理这个信号 */
sigprocmask(SIG_BLOCK, NULL, &set);
sigdelset(&set, SIGABRT);
raise(SIGABRT);
sigsuspend(&set);
/*如果信号处理函数并没有终止程序,且从信号处理函数返回,那么程序能运行到这里;
此时,将处理器改为默认值,重新发送信号。 */
printf("abort was called\n");
signal(SIGABRT, SIG_DFL);
raise(SIGABRT);
sigsuspend(&set);
}
int main() {
sigset_t set;
sigemptyset(&set);
sigaddset(&set, SIGABRT);
sigprocmask(SIG_BLOCK, &set, NULL);
printf("Now begin\n");
switch (sigsetjmp(env, 1)) {
case 0:
if (signal(SIGABRT, handler2) != SIG_ERR) {
printf("sigsetjmp was called\n");
Myabort();
}
break;
case 1:
if (signal(SIGABRT, handler1) != SIG_ERR)
Myabort();
break;
default:
break;
}
printf("Can come here?\n");
return 0;
}
- 上面函数是Myabord的实现和其验证
- 接收信号前要去掉SIGABRT的阻塞;
- 发送一次信号之后,是信号处理器恢复到默认值,重复发送信号。
4.3.2.3、sigaltstack函数——实现SA_ONSTACK的基础
- 为了防止进程主栈区超出限制时,进程无法接收和处理信号,尤其是进程对栈的拓展试图突破其上限时,内核将为此进程产生SIGSEGV信号。
- 每个进程只能定义一个备用栈,所以我们在指定使用备用栈时,只需要指定SA_ONSTACK即可;而不用传递栈地址给sigaction函数
#include<signal.h>
int sigaltstack(const stact_t *sigstack, stack_t *old_sigstack);
typedef struct{
void *ss_sp; /* Starting address of alternate stack */
int ss_flags; /* Flags: SS_ONSTACK, SS_DISABLE */
size_t ss_size; /* Size of alternate stack */
} stack_t;
其中:
- 备用栈既可以静态分配,也可以在堆上动态分配;
- 首先,我们得到一段空间;然后用ss_sp指向空间的开始地址,ss_size描述它的大小;
- 将ss_flags设置为SS_AUTODISARM(此备用栈只使用一次,被handler使用完后,备用栈恢复之前的备用栈设置)或者0(正常设置当前备用栈)
- 调用此函数,在使用sigaction改变信号处理器时,使用SA_ONSTACK标志。
- 返回的时候old_sigstack中的flags可能为SS_ONSTACK(备用栈正在使用,不能被设置),SS_DISABLE(备用栈当前不可用,可能因为它在创建时flags用了SS_AUTODISARM标志。
5、常用信号及其说明
5.1 SIGKILL(终止进程)和SIGSTOP(停止进程)
- 这两个信号既不能改变其默认行为,也不可以被阻塞和忽略;这意味着系统总能通过这两个信号来终止或停止一个进程。
5.1.1、进程睡眠状态与SIGKILL
- TASK_INTERRUPTIBLE:进程正在等待某一事件;此状态时间可长可短;此时进程可以被信号中断,唤醒进程。(可中断系统调用就属于这种情况)
- TASK_UNINTERRUPTIBLE:进程正在等待某些特定类型的事件,如磁盘I/O的完成。此状态一般转瞬即逝;在此状态下进程产生的信号,内核不会传给进程,进程无法被中断。此时如果硬件故障,而使事件无法完成,那么进程无法被中断,只能通过重启系统的办法来结束进程。(不可中断系统调用属于这种情况;系统调用是原子操作)。
- TASK_KILLABLE:为解决上面问题实现的状态,在上面的情况下,进程可以被SIGKILL信号杀死。
5.2、SIGCONT和停止信号
5.2.1、 停止进程的信号
- SIGSTOP:必停信号
- SIGTSIP:作业控制的停止信号Control + z
- SIGTTIN:后台进程请求read()操作时,发送此信号停止前台进程组
- SIGTTOU:后台进程请求write()操作时,发送此信号停止前台进程组
5.2.2、SIGCONT的特别之处
- SIGCONT总能恢复一个处于停止状态的进程,即使该进程正在阻塞或忽略SIGCONT这个进程。
- 如果一个信号发给一个停止的进程,它只能在进程收到SIGCONT信号之后收到。SIGKILL是个特例。
- 每当进程收到SIGCONT信号,它总会丢弃处于等待状态的停止信号;而如果停止信号传递给了进程,那么进程会自动丢弃任何处于等待状态的SIGCONT信号。
5.3、硬件产生的信号
5.3.1、硬件信号的类型
- SIGBUS:发生某种内存访问错误
- SIGFPE:特定类型的算数错误
- SIGILL:进程试图执行非法的机器指令
- SIGSEGV:应用程序对内存的引用无效。
5.3.2、硬件信号处理的非法操作
以下三种情况都会造成未定义的行为
- 从信号处理函数返回;
- 忽略信号;
- 阻塞信号;
5.3.3、正确处理硬件信号的方法
- 使用信号的默认行为(终止进程)
- 为其编写不正常返回的处理函数:程序执行跳转到不会产生硬件错误的代码处执行
6、等待信号
6.1、pause函数
调用pause()将暂停进程的执行,直到信号处理器函数中断该调用或终止进程为止;
#include<unistd.h>
/* Always return -1 with errno set to EINTR */
int pause(void);
- pause函数存在,在pause函数调用之前,信号已经到达,那么pause函数调用将无法返回的问题;
- 即使在信号发送前阻塞信号,在pause函数调用前取消信号阻塞,依然无法解决信号恰好在取消阻塞后,而pause调用前达到的问题。
6.2、使用掩码来等待信号:sigsuspend()
#include<signal.h>
int sigsuspend(const sigset_t *mask);
/*相当于以不间断的方式执行如下操作 */
sigprocmask(SIG_SETMASK, &mask, &prevMask);
pause();
sigprocmask(SIG_SETMASK, &prevMask, NULL);
此函数就解决了上面pause所面临的问题。
二、设计信号处理器函数
1、造成信号处理器函数复杂的原因
- 信号处理器函数和主程序处于同一个进程中,他们共享各种全局变量、静态变量,全局数据结构;
- 我们不能对主程序和信号处理器函数的执行顺序有任何假设,也就是说信号处理函数可以在任意位置中断主程序的指令执行;那么处理器函数就可能改变了主程序本来要检查的全局变量的值,或者打断主程序对全局数据结构的修改,而处理器函数又正好修改了全局数据结构,那么处理器函数返回时,主程序无法处理这种不一致。
- 信号的非队列化处理:一方面,我们无法假设信号的接收顺序;另一方面,同一个信号的等待信号不排队,我们无法倚赖接收信号的个数来匹配信号发送的次数。
2、设计信号处理函数的基本原则
- 处理程序要尽可能的简单;例如处理程序只是设置一个全局变量并立即返回,这样我们既可以通过这个全局变量的值将信号捕捉告诉主程序,而又没有造成太多的副作用;主程序通过检查这个全局变量来执行相应的处理代码。
- 在处理程序中尽量只调用异步安全的函数;异步安全函数有两种可能:要么它是可重入的;要么它態被信号处理程序中断。Linux系统中大约有130多个异步安全的系统调用,我们使用的大部分C标准库包装函数都不在此列,涉及动态内存管理的函数都不在此列。这也就说明只使用异步安全函数是不可能的。但尽量使用异步安全函数是必要的,这样我们就不用考虑我们的信号处理函数会被中断等问题,从而减少了考虑信号阻塞策略的问题。
- 保存和恢复errno;如果信号处理程序中调用的函数可能会设置errno,那么就应该保存和恢复errno。涉及到其他主程序和信号处理程序都使用的全局或静态变量,也应采用这种方式。
- 阻塞所有的信号,保护对共享全局数据结构的访问;全局数据结构的访问需要多条指令才能完成,而如果在访问未完成的某步被中断,而中断调用的程序又访问了该全局数据结构并做了修改,那么当中断返回,继续访问此数据结构时,会造成此次访问与之前访问时的不一致,从而导致未定义的错误。而确认该阻塞哪些信号、何时阻塞、何时解除阻塞是信号处理程序设计的重要问题。
- 用volatile声明全局变量;这个是为了防止编译器优化造成错误的方式,必须遵守。
- 用sig_atomic_t声明标志标志;sig_atomic_t是一种整型数据类型;对他们的读和写是原子的;但不能有更多假设,比如++flag,flag += 10等操作,不是简单的读写,就不能保证原子性。
3、设计信号处理函数需要考虑的其他方面
3.1、信号不排队处理的问题
- 未处理信号不排队的特性:如果存在一个未处理的信号就表明至少有一个信号到达了。
- 这就要求在我们处理一个未处理信号时,我们应该通过while循环处理完所有同类信号的问题。而不是只处理一个信号。
3.2、可移植性问题
主要针对signal函数在不同系统可能存在不同语义的问题和慢速系统调用被中断是否会重启的问题
- 前面已经介绍过POSIX标准定义了sigaction函数,它允许用户在设置信号处理时,明确指定想要的信号处理语义。
- 但sigaction函数的参数设置有些复杂,因为它需要定义一个复制的数据结构;为此,如果我们只需要语义明确的类似于signal函数的信号处理函数,我们可以通过sigaction来包装实现各种我们需要的语义明确的signal函数。
- 通过上面的方法,我们就既解决的语义不明确的问题,也解决了使用不方便的问题。
3.3、解决同步问题可以使用实时信号
- 所谓实时信号,只能所部分实时信号;它增加了信号排队,但队长有限制,20个信号;它使用sigaction中的SA_SIGINFO标志,携带更多的额外信息,更多内容静载后面的博客中介绍。