1、概述
2、信号的一些概念
2.1 什么是信号?
信号是信息的载体,他能在进程间传递一个信息,这个信息极其简单,而且只有满足特定条件才能被发送。
A给B发送信号,B收到信号之前执行自己的代码,收到信号后,不管执行到什么位置,都要暂停运行,去处理信号,处理完毕再继续执行,这叫做中断。
学过嵌入式的应该都知道中断,单片机里的中断主要由时钟(硬件)触发并实现,也叫时钟中断。但是信号中的中断不太一样,这里的中断是由软件方法实现的,也称为软中断,这种实现方法导致信号由很强的延时性。但对于人来说,这个延时非常短,基本察觉不到。
上面说是A给B发送信号,其实A是请求Linux内核产生信号并发给B,而B处理信号,其实也是B请求内核进行处理。所以严禁来说,A不能给B发信号。
2.2 信号的状态
递达: 执行信号的处理动作叫做信号抵达
未决: 信号从产生到递达之间的状态,称为信号未决
阻塞: 信号产生时,保持未决状态,当进程解除对此信号的阻塞,才会执行抵达动作
(阻塞和忽略不同,忽略是一种信号处理动作,而阻塞的信号还没到处理那一步呢)
2.3 两个信号集
阻塞信号集(信号屏蔽字): 是当前进程要阻塞的信号的集合(信号可以还没有产生)
未决信号集: 当前进程中还处于未处理状态的信号的集合(信号必须存在)。未决的信号又称为被挂起的信号
这两个信号集使用set
存储在内核的PCB中。下面详细说明这两个信号集的联系。
(下面描述的例子适用于任何信号,为方便描述,我们用SIGINT
信号举例)
只要产生一个SIGINT
信号(信号编号为2),未决信号集中对应的2号编号的位置上值为1,表示目前处于未决状态;在这个信号要被处理之前,需要查看阻塞信号集中的编号为2的位置上是否为1:
———如果为1,表示SIGINT
信号被当前进程阻塞了,所以暂不处理,未决信号集上的该位置只能还保持1,当解除阻塞后,该信号被处理
———如果为0,表示SIGINT
信号没被当前进程阻塞,需要进行处理,完成处理后未决信号集上该位置0,表示已经处理了。如果是这种情况,信号的产生到处理一帆风顺,其未决信号集由1到0的改变是极快的。
综上,未决信号集上的状态位一定程度上由阻塞信号集上的状态位决定,不过前提是产生了该信号,未决信号集的状态位只有在信号存在时才有意义。
2.4 处理信号
执行默认处理
忽略: 丢弃,注意:这不是不处理,忽略动作也会使未决信号集的状态位置0
捕捉: 调用用户处理函数
所有信号的默认处理动作共有五种:
Term 结束进程
Ign 忽略信号(如子进程死亡触发 SIGCHLD 信号,父进程收到该信号进行忽略处理,然后继续运行)
Core 结束进程并生成核心传输文件(用于检查进程死亡原因,用于 gdb 调试)
Stop 暂停进程
Cont 继续进程
2.5 信号4要素
每个信号都包含下面四个属性:
——信号名
——信号编号
——信号默认处理方式
——信号所对应的事件
3、Linux信号概述
3.1 发送信号
使用kill
给其他进程发送信号,其定义如下:
#include<sys/types.h>
#include<signal.h>
int kill(pid_t pid, int sig);
该函数吧信号sig
发送到进程pid
。
参数说明:
-
pid
可能的取值如下:
-
对于
sig
:
Linux定义的信号值都大于0,如果sig
取值为0,kill
不发送任何信号,但这种情况调用kill
可用来检测目标进程或进程组是否存在,因为检查工作在信号发送前执行。
不过这种检测方式是不靠谱的,一方面由于进程PID的回绕,可能导致被检测的PID不是我们期望的进程的PID;另一方面,这种检测方法不是原子操作。
返回值:
成功返回0,失败返回-1并设置errno
。errno
的可能值如下:
3.2 处理信号
目标信号收到信号时,要定义接收函数处理他。其原型如下:
#include<signal.h>
typedef void (*__sighandler_t)(int);
这是个自定义的函数指针,只含一个整型参数,该参数指明信号类型。信号处理函数应该是可重入的,否则容易引发竞态条件。在信号处理函数中严禁定义不安全的函数。
除了自定义信号处理函数,bits/signum.h
头文件中定义了信号的两种其他处理方式:SIG_IGN
和SIG_DFL
#include<bits/signum.h>
#define SIG_DFL((__sighandler_t) 0)
#define SIG_IGN((__sighandler_t) 1)
SIG_IGN表示忽略目标信号,SIG_DFL表示使用信号的默认处理方式。信号的默认处理方式有如下几种:
Term 结束进程
Ign 忽略信号
Core 结束进程并生成核心传输文件
Stop 暂停进程
Cont 继续进程
3.3 Linux 信号
Linux 的可用信号都定义在 bits/signum.h
头文件中,其中包括标准信号 和 POSIX实时信号。我们仅讨论标准信号,如下所示:
3.4 中断系统调用
某些信号可以在特定情况下中断系统调用。比如:程序在执行处于阻塞状态的系统调用时接收到信号,并且我们为该信号设置了信号处理函数,则默认情况下系统调用被中断,并且errno
被设置为EINTR
。我们可以使用sigaction
函数(后面讲)为信号设置SA_RESTART
标志,从而自动重启被该信号中断的系统调用。
对于默认行为是暂停进程的信号(比如SIGSTOP
、SIGTTIN
),如果我们没有为他们设置信号处理函数,则它们也可以中断某些系统调用(比如:connect
、epoll_wait
)。这是Linux独有的,POSIX没有规定这种行为。
4、信号函数
4.1 signal
系统调用
signal
能够为一个信号注册处理函数:
#include<signal.h>
_sighandler_t signal(int sig, _sighandler_t _handler);
sig
参数是想要注册的信号,_handler
参数是函数指针,他指向该信号的处理函数。
signal
函数的行为是:给某个信号注册一个处理函数。真正捕捉信号的是Linux内核,内核捕捉到了该信号以后,调用我们注册的函数。所以,我们实际上不会主动调用_handler
函数,这种 定义了的,但代码里没调用过的函数成为回调函数。
返回值说明
signal
函数成功返回函数指针,他指向上一次给该信号注册的处理函数(也就是上一次调用signal
时传入的_handler
参数)。如果是第一次调用signal
,则返回信号对应的默认处理函数指针SIG_DEF
。
signal
调用失败返回SIG_ERR
,并设置errno
。
4.2 sigaction
系统调用
sigaction
是注册信号处理函数的更健壮的接口:
#include<signal.h>
int sigaction(int sig, const struct sigaction* act, struct sigaction* oldact);
参数说明:
sig
仍然是想要注册的信号act
参数是结构体指针,指向的结构体描述了该信号的处理方式oldact
也是结构体指针,指向该信号先前的处理方式(如果不为NULL
的话)。
sigaction
结构体定义如下:
struct sigaction
{
void (*sa_handler)(int); 老朋友,指向信号处理函数的指针
void (*sa_sigaction)(int, siginfo_t*, void*); 这个参数不用管
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void); 该参数已被废弃,不用管
};
① sa_mask
: 是一个信号集,该信号集指定当前信号被处理期间所阻塞的信号集。也就是当你处理该信号处理了一半时,有其他的信号来了,是去套娃处理其他的信号呢,还是先阻塞其他信号,继续处理该信号。
注:该阻塞信号集仅在处理函数执行期间生效。
② sa_flags
设置程序收到信号时的行为,其值如下所示:
如果传入sa_flags
的值为0,表示使用默认行为。默认行为是:当来了一个该信号,那么使用处理函数处理它,处理的途中,又来了一个该信号,那么不管前面的sa_mask
里面有没有该信号,该信号都将被阻塞。直到第一个来的该信号处理完毕。
返回值说明:
成功返回0,失败返回-1并设置errno
。
5、信号集
5.1 信号集
前面经常出现sigset_t
类型的对象,他就是信号集,其定义如下:
#include<bits/sigset.h>
#define _SIGSET_NWORDS (1024 / (8 * sizeof(unsigned long int)))
typedef struct
{
unsigned long int __val[_SIGSET_nwords];
}sigset_t;
有定义可知,sigset_t
实际上是一个长整型数组,数组每个元素的每一位表示一个信号。这种定义方式可以类比文件描述符集fd_set
。
5.2 操作信号集的函数
Linux提供如下函数置1,清0,或查询信号集:
#include<signal.h>
int sigemptyset(sigset_t* set); 将 set 的所有位全部清0,成功返回0,失败返回-1
int sigfillset(sigset_t* set); 将 set 的所有位全部置1,成功返回0,失败返回-1
int sigaddset(sigset_t* set, int signum); 将 set 的 signum 置1,即加入 signum,成功返回0,失败返回-1
int sigdelset(sigset_t* set, int signum); 将 set 的 signum 清0,即删除 signum,成功返回0,失败返回-1
int sigismember(const sigset_t* set, int signum); 查询 set 中的是否存在 signum,存在返回1,不存在返回0,错误返回-1
但是这些函数操作的仅仅是我们自己定义出来的信号集,和PCB中的阻塞信号集、未决信号集完全没有关系。不过,未决信号集由阻塞信号集来决定。那么问题就变成了:我们怎么用自己的信号集去影响阻塞信号集?
即答:使用如下函数:
#include<signal.h>
int sigprocmask(int how, const sigset_t* set, sigset_t* oldset);
该函数能供通过我们自己的信号集去改变阻塞信号集,既能用于阻塞信号,也能解除阻塞。
参数说明:
① set
:
set
就是我们自己的信号集,信号集中哪位为1,就表示想要更改阻塞信号集中的哪个信号的值。至于具体改成0
还是1
,要看第一个参数how
。
② how
:
该参数仅有三种取值:
SIG_BLOCK
SIG_UNBLOCK
SIG_SETMASK
第一个宏表示,把我们的信号集里面的信号设置为阻塞,即阻塞信号集里的相应位置1。
第二个宏表示,把我们的信号集里面的信号解除阻塞,即阻塞信号集里的响应为清0。
第三个宏表示,直接用我们的信号集 覆盖阻塞信号集。一般不推荐用这个。
③ oldset
:
是个传出参数,保存旧的阻塞信号集(如果不为NULL
的话)。
返回值说明:
成功返回0,失败返回-1并设置errno
。
5.3 读取未决(挂起)信号集
既然未决信号集由阻塞信号集决定,那么我们也就没有必要改变未决信号集。那么总有办法读到未决信号集里的内容吧!
还真有:
int sigpending(sigset_t* set);
该函数很简单,就是把未决信号集读到传出参数set
中。成功返回0,失败返回-1并设置errno
。
6、统一事件源
统一事件源: 信号事件和其他I/O事件都使用相同的方式(epoll
实现的多路复用)处理,就是统一事件源。
信号是一种异步事件:信号处理函数和程序的主函数是两条不同的执行路线。很显然,信号处理函数需要尽快执行完毕,从而保证后来到达的信号不被阻塞太久。
一种解决办法就是,把信号的主要处理逻辑放到程序的主循环中,而当信号处理函数被触发时,他只是简单的把接收到的信号值传递给主循环,主循环根据接收到的信号值执行相应逻辑代码。
信号处理函数通常使用管道将信号传递给主循环:信号处理函数把信号值写入管道,主循环从管道读出信号值。
那么主循环如何知道管道上何时有数据?使用I/O复用系统监听管道读端的可读事件即可。这样,信号事件就能像其他I/O事件一样被处理,即统一事件源。
这是一个服务器程序,它使用统一事件源的方式同时处理I/O和信号
7、网络编程相关信号
下面探讨三个和网络编程模切相关的信号。
7.1 SIGHUP
对于与终端脱离关系的守护进程(没有控制终端的后台进程),这个信号用于通知该进程重新读取配置文件。
比如,xinetd超级服务程序。
当xinetd程序在接收到SIGHUP
信号之后调用hard_reconfig
函数,它将循环读取/etc/xinetd.d/
目录下的每个子配置文件(这些文件是xinetd服务的子服务的配置文件),并检测其变化。如果某个正在运行的子服务的配置文件被修改以停止服务,则xinetd主进程讲给该子服务进程发送SIGTERM
信号来结束它。如果某个子服务的配置文件被修改以开启服务,则xinetd将创建新的socket
并将其绑定到该服务对应的端口上。
7.2 SIGPIPE
默认情况,往读端关闭的管道或socket连接中写数据将引发SIGPIPE
信号。程序接收到该信号的默认行为是结束进程,但我们通常不会想因为区区错误的写操作而导致进程结束,所以我们需要在代码中捕获并处理该信号,或至少忽略他。
还有一种办法是:使用send
函数的MSG_NOSIGNAL
标志来禁止写操作触发SIGPIPE
信号。这时,需要使用send
函数反馈的errno
值来判断管道或socket连接的读端是否已经关闭,向这样的读端调用写操作会设置errno
为EPIPE
。
还有一种办法:利用I/O复用系统调用检测管道和socket链接的读端是否已关闭。以poll
为例,当管道的读端关闭时,写段文件描述符上的POLLHUP
事件将被触发;当socket连接被对方关闭时,socket上的POLLRDHUP
事件被触发。
7.3 SIGURG
内核使用SIGURG
信号通知应用程序带外数据到达。内核通知带外数据到达的另一种方法是是I/O复用技术,对于select
来说是异常事件,对于poll
和epoll
来说分别是:POLLPRI
和 EPOLLPRI
。
8、信号相关函数
8.1 alarm
函数
#inlcude<unistd.h>
unsigned int alarm(unsigned int seconds);
每隔seconds
秒给当前进程发送一个SIGALRM
信号。
返回上一次调用alarm
定时剩余的秒数。没有失败。
例如:
alarm(5); 第一次调用返回0
刚过了2秒又调用
alarm(5); 这次调用返回3,因为上次调用还差3秒就到5秒
alarm(0); 这意思是取消闹钟
无论进程处于什么状态(就绪、运行、阻塞、暂停、终止、僵尸),alarm
都会计时。
9、实例程序
9.1 定时器 与 信号相结合实现超时处理
这是一个服务器程序,使用定时器链表处理非活动socket连接,本程序中是直接将其关闭。