异步信号处理机制
信号与中断
信号是由用户、系统或者进程发送给目标进程的信息,以通知目标进程某个状态的改变或系统异常。
信号是事件发生时对进程的通知机制,有时也称之为软件中断,它是在软件层次上对中断机制的一种模拟,是一种异步通信的方式。
信号可以导致一个正在运行的进程被另一个正在运行的异步进程中断,转而处理某一个突发事件。
编号 | 信号名 | 信号含义 |
---|---|---|
1 | SIGHUP | 如果终端接口检测到一个连接断开,则会将此信号发送给与该终端相关的控制进程,该信号的默认处理动作是终止进程。 |
2 | SIGINT | 当用户按组合键(一般采用Ctrl+C)时,终端驱动程序产生此信号并发送至前台进程组中的每一个进程,该信号的默认处理动作是终止进程。 |
3 | SIGQUIT | 当用户按组合键(一般采用Ctrl+\)时,终端驱动程序产生此信号并发送至前台进程组中的每一个进程,该信号不仅终止前台进程组,同时会产生一个core文件。 |
4 | SIGILL | 此信号表示进程已执行一条非法指令,该信号的默认处理动作是终止进程,同时产生一个core文件。 |
5 | SIGTRAP | 该信号由断点指令或其他trap指令产生,该信号的默认处理动作是终止进程,同时会产生一个core文件。 |
6 | SIGABRT | 调用abort函数是产生此信号,进程异常终止,同时会产生一个core文件。 |
7 | SIGBUS | 当出现某些类型的内存故障时,常常产生该信号,,该信号的默认处理动作是终止进程,同时产生一个core文件。 |
8 | SIGFPE | 此信号表示一个算术运算异常,比如除0、浮点溢出等,该信号的默认处理动作是终止进程,同时产生一个core文件。 |
9 | SIGKILL | 该信号不能被捕捉或忽略,它向系统管理员提供了一种可以杀死任一进程的可靠方法。 |
10 | SIGUSR1 | 这是一个用户定义的信号,即程序员可以在程序中定义并使用该信号,该信号的默认处理动作是终止进程。 |
11 | SIGSEGV | 指示进程进行了一次无效的内存访问(比如访问了一个未初始化的指针),该信号的默认处理动作是终止进程并产生一个core文件。 |
12 | SIGUSR2 | 这是另一个用户定义的信号,与SIGUSR1相似,该信号的默认处理动作是终止进程。 |
13 | SIGPIPE | 如果在管道的读进程已终止时对管道进行写入操作,则会收到此信号,该信号的默认处理动作是终止进程。 |
14 | SIGALRM | 当用alarm函数设置的定时器超时时产生此信号,或由setitimer函数设置的间隔时间已经超时时也产生会此信号。 |
15 | SIGTERM | 该信号是由应用程序捕获的,使用该信号让程序有机会在退出之前做好清理工作。与SIGKILL信号不同的是,该信号可以被捕捉或忽略,通常用来表示程序正常退出。 |
16 | SIGSTKFLT | 该信号指示协处理器上的堆栈故障(未使用),该信号的默认处理动作是终止进程。 |
17 | SIGCHLD | 在一个进程终止或停止时,SIGCHLD信号被发送给其父进程。按系统默认,将忽略此信号。如果父进程希望被告知其子进程的这种状态改变,则应捕捉此信号。信号捕捉函数中通常要调用一种wait函数以取得子进程ID及其终止状态。 |
18 | SIGCONT | 可以通过发送该信号让一个停止的进程继续运行。 |
19 | SIGSTOP | 这时一个作业控制信号,该信号用于停止一个进程,类似于交互停止信号(SIGTSTP),但是该信号不能被捕捉或忽略。 |
20 | SIGTSTP | 交互停止信号,当用户按组合键(一般采用Ctrl+Z)时,终端驱动程序产生此信号并发送至前台进程组中的每一个进程。 |
21 | SIGTTIN | 后台进程读终端控制台时,由终端驱动程序产生此信号并发送给该后台进程,该信号的默认处理动作是暂停进程。 |
22 | SIGTTOU | 后台进程向终端控制台输出数据,由终端驱动程序产生此信号并发送给该后台进程,该信号的默认处理动作是暂停进程。 |
23 | SIGURG | 套接字上有紧急数据时,向当前正在运行的进程发出此信号,报告有紧急数据到达,该信号的默认处理动作是忽略。 |
24 | SIGXCPU | 进程执行时间超过了分配给该进程的CPU时间,系统产生该信号并发送给该进程,该信号的默认处理动作是终止进程,同时会产生一个core文件。 |
25 | SIGXFSZ | 如果进程写文件时超过了文件的最大长度设置,则会收到该信号,该信号的默认处理动作是终止进程,同时会产生一个core文件。 |
26 | SIGVTALRM | 虚拟时钟超时时产生该信号,与SIGALRM信号类似,但是该信号只计算该进程占用CPU的使用时间,该信号的默认处理动作是终止进程。 |
27 | SIGPROF | 该信号类似与SIGVTALRM,它不仅包括该进程占用CPU的时间还包括执行系统调用的时间,该信号的默认处理动作是终止进程。 |
28 | SIGWINCH | 当窗口大小发生变化时,内核会将该信号发送至前台进程组,该信号的默认处理动作是忽略。 |
29 | SIGIO | 此信号指示一个异步I/O事件,该信号的默认处理动作是终止进程。 |
30 | SIGPWR | 电源故障,该信号的默认处理动作是终止进程。 |
31 | SIGSYS | 该信号指示一个无效的系统调用,该信号的默认处理动作是终止进程,同时会产生一个core文件。 |
两种不能被忽略的信号 and 两种不能被捕捉的信号
SIGKILL和SIGSTOP
信号的生命周期
生命周期:产生信号->在进程中注册信号->在进程中注销进行->处理信号
信号的产生:
- 通过终端按键产生信号
例如常用的ctrl + z、ctrl + c、ctrl + \就是分别产生了SIGTSTP、SIGINT、SIGQUIT信号。 - 通过调用系统函数向进程发送信号
例如kill -x函数,x就是对应的信号的序号,如果不知名则发送15号信号SIGTERM。kill杀死进程的原理就是通过发送一个信号,让这个进程中断并去处理这个信号,然而这个信号的处理结果就是让这个进程退出。 - 通过软件异常产生信号
例如上一篇说过管道如果读端全部关闭,而写端没关闭时就会发送一个SIGPIPE的信号 - 通过硬件异常产生信号
例如当运算中以0为除数,则CPU的运算单元会检测到除0异常,并发送SIGFPE信号
信号的注册
信号注册的流程主要是修改pcb中的pending位图并向pcb中的sigqueue链表中添加新的节点,但根据信号的种类不同操作也不同。
不可靠信号的注册:
首先查看pending位图该信号的标志位是否为0,如果为0则将标志位修改为1,并向sigqueue链表添加新的节点。如果为1则说明该信号已经注册过,则忽略此次事件,什么都不做,也正是因为这样会导致事件的丢失,才被称为不可靠信号。
可靠信号的注册:
可靠信号注册时则不管该信号是否注册过,都会往sigqueue链表中添加新的节点并修改位图,这样就保证了每一个发送的事件都会被处理,这也是被称为可靠的原因。
信号的注销
为了保证每一种信号只被处理一次,所以需要先注销再处理。
注销就是消除这个信号存在的痕迹,即修改位图,删除sigqueue中的节点。
不可靠信号的注销:
因为不可靠信号只注册了一次,只需要删除sigqueue中的节点,然后所以将位图对应的标志位置零。
可靠信号的注销:
因为可靠信号注册了多次,添加了多个节点,所以需要删除该信号添加的所有相同节点,才将位图对应的标志位置零。
信号的处理
因为信号是操作系统发给进程来通知某个事件的到来,所以对信号的处理也就是对事件的处理。
信号的处理方式:
- 默认处理方式: 就是操作系统为每一种信号准备的对应的处理方式
- 忽略处理方式: 和名字一样,忽略,什么都不做
- 自定义处理方式: 我们可以自己写一个回调函数来替换原来的处理方法,完成我们想要对这个信号的处理方式。
信号的发送
kill() :传递一个信号到指定进程
int kill(pid_t pid, int sig);
- pid > 0 将信号发送给ID为pid的进程
- pid == 0 将信号发送给与发送进程属于同个进程组的所有进程
- pid < 0 将信号发送给进程组ID等于pid绝对值的所有进程
- pid == -1 将信号发送给该进程有权限发送的系统里的所有进程
sig:准备发送的信号代码,假如其值为零则没有任何信号送出,但是系统会执行错误检查,通常会利用sig值为零来检验某个进程是否仍在执行。
int raise(int sig) :传递一个信号到当前进程
- sig – 要发送的信号码。下面是一些重要的标准信号常量:
SIGABRT | (Signal Abort) 程序异常终止。 |
---|---|
SIGFPE | (Signal Floating-Point Exception) 算术运算出错,如除数为 0 或溢出(不一定是浮点运算)。 |
SIGILL | (Signal Illegal Instruction) 非法函数映象,如非法指令,通常是由于代码中的某个变体或者尝试执行数据导致的。 |
SIGINT | (Signal Interrupt) 中断信号,如 ctrl-C,通常由用户生成。 |
SIGSEGV | (Signal Segmentation Violation) 非法访问存储器,如访问不存在的内存单元。 |
SIGTERM | (Signal Terminate) 发送给本程序的终止请求信号。 |
unsigned int alarm(unsigned int seconds) :唤醒一个进程和设置定时
-
alarm函数的作用是设置一个定时器,在seconds秒之后,将会发送SIGALRM信号给当前的进程,故而alarm函数也被称为闹钟函数。
如果不对SIGALRM信号进行忽略或者捕捉,默认情况下会退出进程。 -
如果second的值为0的话,那么定时器将会被取消。
-
如果在seconds秒内再次调用了alarm函数设置了新的闹钟,那么之前设置的秒数将会被新的闹钟时间所取代。
函数的返回值
1.如果seconds的值被设置为0,那么返回值将会是0.
2.如果在当前的定时器之前设置过定时器,那么当前定时器返回的值将是之前定时器剩余的秒数。
useconds_t ualarm(useconds_t usecs, useconds_t interval);
在usecs微秒后,将SIGALRM信号发送给进程,并且之后每隔interval微秒再发送一次SIGALRM信号。如果不对SIGALRM信号进程处理,默认操作是终止进程
延迟可能会因任何系统活动、处理调用所花费的时间或系统计时器的粒度而略微延长
-
参数:
usecs:第一次触发SIGALRM信号的时间
interval:第一次触发SIGALRM信号之后每隔interval微秒再触发一次SIGALRM信号
以微秒为单位 -
返回值:此函数返回以前设置的任何警报的剩余微秒数。如果没有挂起警报,则返回0(第一次调用该函数也返回0)。常见的errno如下:
-
EINTR:被一个信号打断了
-
EINVAL:usecs或interval不小于1000000(在被认为是错误的系统上)
-
int getitimer(int which, struct itimerval *value);
int setitimer(int which, const struct itimerval *value, struct itimerval *ovalue);
获取或设定间歇计时器的值。系统为进程提供三种类型的计时器,每一类以不同的时间域递减其值。当计时器超时,信号被发送到进程,之后计时器重启动。
linux系统给每个进程提供了3个定时器,每个定时器在各自不同的域里面计数。当任何一个timer计数到结束了,系统就发送一个信号(signal)给该进程,同时计数器重置。
一共支持以下3中计数器形式:
- ITIMER_REAL 在real time中计数器减1,然后等计数往比后发送SIGALRM信号。
- ITIMER_VIRTUAL 当进程在执行的过程中计数,然后当计数完毕后发送SIGVTALRM信号给该进程。
- ITIMER_PROF 在该进程被执行和系统在代表该进程执行的时间都进行计数
setitimer() 不支持在同一进程中同时使用多次以支持多个定时器。
参数:
which:间歇计时器类型,有三种选择
ITIMER_REAL //数值为0,计时器的值实时递减,发送的信号是SIGALRM。
ITIMER_VIRTUAL //数值为1,进程执行时递减计时器的值,发送的信号是SIGVTALRM。
ITIMER_PROF //数值为2,进程和系统执行时都递减计时器的值,发送的信号是SIGPROF。
value,ovalue:时间参数,原型如下
struct itimerval
{
struct timeval it_interval; //间隔值
struct timeval it_value; //当前剩余时间
};
struct timeval
{
long tv_sec; //s
long tv_usec; //ms
};
getitimer()用计时器的当前值填写value指向的结构体。
setitimer()将value指向的结构体设为计时器的当前值,如果ovalue不是NULL,将返回计时器原有值。
返回说明:
成功执行时,返回0。失败返回-1,errno被设为以下的某个值
- EFAULT:value或ovalue是不有效的指针
- EINVAL:其值不是ITIMER_REAL,ITIMER_VIRTUAL 或 ITIMER_PROF之一
安装信号与捕获信号
信号处理办法
1.忽略该信号。大多数信号都可使用这种方式进行处理,但有两种信号却决不能被忽略。它们是:SIGKILL和SIGSTOP。这两种信号不能被忽略的原因是:它们向超级用户提供一种使用进程或停止的可靠方法。另外,如果忽略某些由硬件异常产生的信号,则进程的行为是未定义的;
2.捕捉信号。为了做到这一点要通知内核在某种信号发生时,调用一个用户函数。在用户函数中,可执行用户希望对这种事件进行的处理。例如,若编写一个命令解释器,当用户用键盘产生中断信号时,很可能希望返回到程序的主循环,终止系统正在为该用户执行的命令。如果捕捉到SIGCHLD信号,则表示子进程已经停止,所以此信号的捕捉函数可以调用waitpid,以取得该子进程的进程ID以及它的终止状态。又例如,如果进程创建了临时文件,那么可能要为SIGTERM信号编写一个信号捕捉函数以清除临时文件(kill 命令传送的系统默认信号是终止信号);
3.执行系统默认动作。对大多数信号的系统默认动作是终止该进程。还有忽略、继续、暂停。
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
void (*signal(int sig, void (*func)(int)))(int)
- sig – 在信号处理程序中作为变量使用的信号码。下面是一些重要的标准信号常量:
宏 | 信号 |
---|---|
SIGABRT | (Signal Abort) 程序异常终止。 |
SIGFPE | (Signal Floating-Point Exception) 算术运算出错,如除数为 0 或溢出(不一定是浮点运算)。 |
SIGILL | (Signal Illegal Instruction) 非法函数映象,如非法指令,通常是由于代码中的某个变体或者尝试执行数据导致的。 |
SIGINT | (Signal Interrupt) 中断信号,如 ctrl-C,通常由用户生成。 |
SIGSEGV | (Signal Segmentation Violation) 非法访问存储器,如访问不存在的内存单元。 |
SIGTERM | (Signal Terminate) 发送给本程序的终止请求信号。 |
- func – 一个指向函数的指针。它可以是一个由程序定义的函数,也可以是下面预定义函数之一:
SIG_DFL | 默认的信号处理程序。 |
---|---|
SIG_IGN | 忽视信号。 |
SIG_ERR | 返回错误 |
返回值:
该函数返回信号处理程序之前的值,当发生错误时返回 SIG_ERR。
int sigaction(int signum, const struct sigaction *act, struct sigaction *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);
}
sigaction函数的功能是检查或修改与指定信号相关联的处理动作(可同时两种操作)
signum参数指出要捕获的信号类型,act参数指定新的信号处理方式,oldact参数输出先前信号的处理方式(如果不为NULL的话)。
-
sa_handler此参数和signal()的参数handler相同,代表新的信号处理函数
-
sa_mask 用来设置在处理该信号时暂时将sa_mask 指定的信号集搁置-
-
sa_flags 用来设置信号处理的其他相关操作,下列的数值可用:
- SA_RESETHAND:当调用信号处理函数时,将信号的处理函数重置为缺省值SIG_DFL
- SA_RESTART:如果信号中断了进程的某个系统调用,则系统自动启动该系统调用
- SA_NODEFER :一般情况下, 当信号处理函数运行时,内核将阻塞该给定信号。但是如果设置了 SA_NODEFER标记, 那么在该信号处理函数运行时,内核将不会阻塞该信号
-
void (*sa_restorer)(void); 没有使用
#include <signal.h>
// 使用这个函数修改内核中的阻塞信号集
// sigset_t 被封装之后得到的数据类型, 原型:int[32], 里边一共有1024给标志位, 每一个信号对应一个标志位
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
参数:
how
:
SIG_BLOCK
: 将参数 set 集合中的数据追加到阻塞信号集中
SIG_UNBLOCK
: 将参数 set 集合中的信号在阻塞信号集中解除阻塞
SIG_SETMASK
: 使用参 set 结合中的数据覆盖内核的阻塞信号集数据
oldset
: 通过这个参数将设置之前的阻塞信号集数据传出,如果不需要可以指定为NULL
返回值
:函数调用成功返回0,调用失败返回-1
sigprocmask()
函数有一个 sigset_t
类型的参数,对这种类型的数据进行初始化需要调用一些相关的操作函数:
#include <signal.h>
// 如果在程序中读写 sigset_t 类型的变量
// 阻塞信号集和未决信号集都存储在 sigset_t 类型的变量中, 这个变量对应一块内存
// 阻塞信号集和未决信号集, 对应的内存中有1024bit = 128字节
// 将set集合中所有的标志位设置为0
int sigemptyset(sigset_t *set);
// 将set集合中所有的标志位设置为1
int sigfillset(sigset_t *set);
// 将set集合中某一个信号(signum)对应的标志位设置为1
int sigaddset(sigset_t *set, int signum);
// 将set集合中某一个信号(signum)对应的标志位设置为0
int sigdelset(sigset_t *set, int signum);
// 判断某个信号在集合中对应的标志位到底是0还是1, 如果是0返回0, 如果是1返回1
int sigismember(const sigset_t *set, int signum);
未决信号集不需要程序猿修改, 如果设置了某个信号阻塞, 当这个信号产生之后, 内核会将这个信号的未决状态记录到未决信号集中,当阻塞的信号被解除阻塞, 未决信号集中的信号随之被处理, 内核再次修改未决信号集将该信号的状态修改为递达状态(标志位置0)。因此,写未决信号集的动作都是内核做的,这是一个读未决信号集的操作函数:
#include <signal.h>
// 这个函数的参数是传出参数, 传出的内核未决信号集的拷贝
// 读一下这个集合就指定哪个信号是未决状态
int sigpending(sigset_t *set);
int pause(void); 调用该函数(系统调用)的进程将处于阻塞状态(主动放弃cpu),直到有信号递达将其唤醒。
返回值:-1 并设置errno为EINTR 该函数只有一个返回值,可以理解为只有成功返回值,且为-1,同时errno的值置为EINTR。注意,只有当一个信号递达且处理方式被捕捉时,pause函数引起挂起操作的进程才会被唤醒,而且只有当信号处理完后(调用完用户处理函数),pause函数才返回-1,且errno置EINTR,进程被唤醒继续执行后面的程序。如果信号的处理方式为默认处理方式或者忽略(丢弃),那么pause函数不会返回值,且进程也不会被激活,而是一直阻塞(挂起)。
pause收到的信号不能被屏蔽,如果被屏蔽,那么pause就不能被唤醒。
#include <signal.h>
int sigsuspend( const sigset_t *sigmask );
返回值:-1,并将errno设置为EINTR
进程的信号掩码被设为由sigmask所指的值。然后进程被挂起,直到一个信号被捕获,或一个终止进程的信号发生。如果一个信号被捕获且信号处理器返回,那么sigsuspend返回,而且进程的信号掩码在sigsuspend调用之前设为它的值。
这个函数没有成功返回。如果它返回到调用者,它总是返回-1,errno设置为EINTR(表示一个中断的系统调用)。