1、信号特点
- 简单
- 不能携带大量信息
- 满足某个特设条件才发送
信号可以直接进行用户空间和内核空间进程的交互,内核进程可以利用它来通知用户空间进程发生了哪些系统事件
一个完整的信号周期包括三个部分:信号的产生,信号在进程中的注册,信号在进程中的注销,执行信号处理函数。
注意:信号的产生、注册、注销是信号的内部机制,而不是信号的函数实现。
2、信号的编号
Unix早期版本就提供了信号机制,但不可靠,信号可能丢失。Berkeley和AT&T都对信号模型做了更改,增加了可靠信号机制。但彼此不兼容。POSIX.1对可靠信号例程进行了标准号。
Linux可使用命令kill -l查看相应的信号。
不存在编号为0的信号。其中1-31号信号称之为常规信号(普通信号或标准信号),34-64称为实时信号(没有固定含义,可以由用户自定义),驱动编程与硬件相关,名字上区别不大。而前31个名字各不相同。
3、信号四要素
- 编号
- 名称
- 事件
- 默认处理动作
可以通过man 7 signal查看帮助文档获取。
在标准信号中,有一些信号由三个Value,第一个值通常对alpha和sparc架构有效,中间值针对x86、arm和其他架构,最后一个应用于mips架构,一个'-'表示在对应架构上尚未定义该信号。只有一个Value说明在所有架构都有效。
不同操作系统定义了不同的系统信号。因此有些信号出现在Unix系统内,也出现在Linux中,而有的信号出现在FreeBSD或Mac OS中却没有出现在Linux下(所以一般我们使用信号名称,而不直接使用编号)。这里我们只研究Linux系统中的信号。
另外,只有每个信号对应的事件发生了,该信号才会被递送(但不一定递达)。
4、信号状态
1)产生
- 当用户按某些终端键时,将产生信号,如按下"ctrl + c"通常产生中断信号SIGINT; "ctrl + \"通常产生中断信号SIGQUIT。
- 硬件异常将产生信号。如除数为0,无效的内存访问等。这些情况通常由硬件检测到,并通知内核,然后内核产生适当的信号发送给相应的进程
- 软件异常将产生信号。当检测到某种软件条件已发生(如:定时器alarm),并将其通知有关进程时,产生信号。
- 调用系统函数(如:kill、raise、abort)将发送信号。注意:接受信号进程和发送信号进程的所有者必须相同,或发送信号进程的所有者必须是超级用户
- 运行kill/killall命令将发送信号。此程序实际上是使用kill函数来发送信号。也常用此命令种植一个失控的后台进程。
2)未决状态:没有被处理
3)递达状态:信号被处理了
5、阻塞信号集和未决信号集
阻塞信号集和未决信号集一般为64位位图表示,这是为了节省空间,每一位表示一种信号。
5.1 阻塞信号集
将某些信号加入集合,对他们设置屏蔽,当屏蔽x信号后,再收到该信号,该信号的处理将退后(处理发生再解除屏蔽后)。阻塞信号集有点类似于手机电话的黑名单。
5.2 未决信号集
信号产生,未决信号集中描述该信号的位立刻翻转为1,表示信号处于未决状态。当信号被处理对应位翻转回位0。这一时刻往往非常短暂。
注意:当我们在终端输入ctrl+c键,内核会向进程发送2号信号SIGINT,进程并不会立马执行,首先会先传递给未决信号集,信号产生了进其值为1,表示该信号产生,然后它会继续向阻塞信号集传递,如果阻塞信号集的值是1,则表示不能通过,2号信号就不能响应。如果阻塞信号集的值为0,则处理该信号,并在未决信号集中的对应位翻转回0。
5.3 自定义信号集函数
信号集是一个能表示多个信号的数据类型,sigset_t set, set即一个信号集。既然是一个集合,就需要对集合进行添加/删除等操作。相关函数说明如下:
#include<signal.h>
int sigemptyset(sigset_t * set); // 将set集合置空
int sigfillset(sigset_t * set); // 将所有信号加入set集合
int sigaddset(sigset_t * set, int signo); // 将signo信号加入到set集合
int sigdelset(sigset_t * set, int signo); // 从set集合中移除signo信号
int sigismember(const sigset_t * set, int signo); // 判断信号是否存在
除sigismember外,其余操作函数中的set均位传出参数。sigset_t类型的本质是位图。但不应该直接使用位操作,而应该使用上述函数,保证跨系统操作有效。
5.4 sigprocmask函数
每个进程都有一个阻塞集,创建子进程时子进程将继承父进程的阻塞集。信号阻塞集用来描述哪些信号递送到该进程的时候被阻塞(在信号发生时记住它,直到进程准备好时再将信号通知进程)。
所谓阻塞并不是禁止传送信号,而是暂缓信号的传送。若将被阻塞的信号从信号阻塞集中删除,且对应的信号在被阻塞时发生了,进程将会收到相应的信号。
我们可以通过sigprocmask()修改当前的信号掩码来改变信号的阻塞情况。
#include<signal.h>
int sigprocmask(int how, const sigset_t * set, sigset_t * oldset);
功能:
检查或修改信号阻塞集,根据how指定的方法对进程的阻塞集合进行修改,新的信号阻塞集由set指定,而原先的信号阻塞集合由oldset保存。
参数:
how: 信号阻塞集合的修改方法, 有3种情况:
SIG_BLOCK: 向信号阻塞集合中添加set信号集,新的信号掩码是set和旧信号掩码的并集。相当于mask=mask|set。
SIG_UNBLOCK: 从信号阻塞集合中删除set信号集,从当前信号掩码中去除set中的信号。相当于mask=mask&~set。
SIG_SETMASK: 将信号阻塞集和设为set信号集,相当于原来信号阻塞集的内容清空,然后按照set中的信号重新设置信号阻塞集。相当于mask=set。
set :要操作的信号集地址。
若set为NULL,则不改变信号阻塞集合,函数只把当前信号阻塞集合保存到oldset中。
oldset:保存原先信号阻塞集地址
6、信号产生函数
6.1 kill函数
#include<sys/types.h>
#include<signal.h>
int kill(pid_t pid, int sig);
功能:给指定进程发送指定信号(不一定杀死)
参数:
pid : 取值有4种情况:
pid > 0 : 将信号传送给进程ID为pid的进程。
pid = 0 : 将信号传送给当前进程所在进程组的所有进程。
pid = -1 : 将信号传送给系统内的所有进程。
pid < -1 : 将信号传送给指定进程组的所有进程,这个进程组号等于pid的绝对值。
sig : 信号的编号,或者信号的宏定义。推荐使用宏定义
返回值:
成功:0
失败:-1
super用户(root)可以发送给任意用户,普通用户是不能向系统用户发送信号的。
普通用户基本规则:发送者实际或有效用户ID == 接收者实际或有效用户ID
6.2 raise函数
#include<signal.h>
int raise(int sig);
功能:给当前进程发送指定信号(自己给自己发),等价于kill(getpid(), sig)
参数:
sig : 信号编号
返回值:
成功:0
失败:非0值
6.3 abort函数
#include<stdlib.h>
void abort(void);
功能: 给自己发送异常终止信号
6.4 alarm函数(闹钟)
#include<unistd.h>
unsigned int alarm(unsigned int seconds);
功能:
设置定时器(闹钟)。再指定seconds后,内核会给当前进程发送 SIGALRM 信号。进程收到该信号,默认动作终止。每个进程都有且只有唯一的一个定时器。
取消定时器alarm(0),返回旧闹钟余下秒数。
参数:
seconds:指定的事件,以秒为单位
返回值:
返回0(之前没有设置闹钟)或上一次闹钟剩余的秒数。
6.5 setitimer函数(定时器)
#include<sys/time.h>
int setitimer(int which, const struct itimerval * new_value, struct itimerval * old_value);
功能:
设置定时器(闹钟)。可代替alarm函数。精度微秒us,可以实现周期定时。
参数:
which:指定定时方式
a.自然定时(常用):ITIMER_REAL -> SIGALRM计算自然时间
b.虚拟空间计时(用户空间):ITIMER_VIRTUAL -> SIGVTALRM只计算进程占用cpu时间
c.运行时即使(用户+内核):ITIMER_PROF -> SIGPROF计算占用cpu及执行系统调用的时间
new_value: struct itimerval, 负责设定timeout时间
struct itimerval {
struct timerval it_interval; // 闹钟触发周期
struct timerval it_value; // 闹钟触发时间
};
struct timeval {
long tv_sec; // 秒
long tv_usec; // 微秒
};
itimerval.it_value: 设定第一次执行function所延迟的秒数
itimerval.it_interval: 设定以后每几秒执行function
old_value: 存放旧的timeout值,一般指定为NULL
返回值:
成功:0
失败:-1
7、信号的捕捉
信号处理方式:
一个进程收到一个信号的时候,可以用如下方法进行处理:
- 执行系统默认动作:对大多数信号来说,系统默认动作是用来终止该进程
- 忽略此信号(丢弃):接收到此信号后没有任何动作
- 执行自定义信号处理函数(捕捉):用用户自定义的信号处理函数处理该信号。
注意:SIGKILL和SIGSTOP不能更改信号的处理方式,因为它们向用户提供了一种使进程终止的可靠方法。
内核实现信号捕捉过程:
- (用户区)在执行主控制流程的某条指令时因为中断、异常或系统调用进入内核。
- (内核区)内核处理完异常准备回用户模式之前先处理当前进程中可以递送的信号。
- (内核区)do_signal() 如果信号的处理动作自定义的信号处理函数则回到用户模式执行信号处理函数(而不是回到主控制流程)
- (用户区)信号处理函数返回时执行特殊的系统调用sigreturn再次进内核。
- (内核区)返回用户模式从主控制流程中上次被中断的地方继续向下执行。
7.1 signal函数
#include<siuignal.h>
typedef void(*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
功能:
注册信号处理函数(不可用于SIIGKILL、SIGSTOP信号),即确定收到信号后处理函数的入口地址。此函数不会阻塞。
参数:
signum: 信号的编号,这里可以填数字编号,也可以填信号的宏定义,可以通过命令kill - l 进行相应查看。
handler:取值有3种情况:
SIG_IGN: 忽略该信号
SIG_DFL: 执行系统默认动作
信号处理函数名: 自定义信号处理函数,如:func
回调函数的定义如下:
void func(int signo)
{
// signo 为触发的信号,为signal()第一个参数的值
}
返回值:
成功:第一个返回NULL, 下一次返回此信号上一次注册的信号处理函数的地址。如果需要使用此返回值,必须在前面先声明此函数指针的类型。
失败:返回SIG_ERR
该函数由ANSI定义,由于历史原因在不同版本的Unix和不同版本的Linux中可能有不同的行为。因此应该尽量避免使用它,取而代之使用sigaction函数。
7.2 sigaction函数
#include<signal.h>
int sigaction(int signum, const struct sigaction * act, struct sigaction * oldact);
功能:
检查或修改指定信号的设置(或同时执行这两种操作)
参数:
signum: 要操作的信号。
act: 要设置的对信号的新处理方式(传入参数)。
oldact: 原来对信号的处理方式(传出参数)。
如果act指针非空,则要改变指定信号的处理方式(设置),如果oldact指针非空,则系统将此前指定信号的处理方式存入oldact。
返回值:
成功:0
失败:-1
struct 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); //已弃用
}
1)sa_handler、sa_sigaction: 信号处理函数指针,和signal()里的函数指针用法一样,应根据情况给sa_sigaction、sa_handler两者之一赋值,其取值如下:
a)SIG_IGN: 忽略该信号
b)SIG_DFL: 执行系统默认动作
c)处理函数名:自定义信号处理函数
2)sa_mask: 信号阻塞集,在信号处理函数执行过程中,临时屏蔽指定的信号
3)sa_flags: 用于指定信号处理的行为,通常设置为0,表使用默认属性。它可以时以下值得"按位或"组合:
a)SA_RESTART: 使被信号打断的系统调用自动重新发起(已经废弃)
b)SA_NOCLDSTOP: 使父进程在它的子进程暂停或继续运行时不会收到SIGCHILD信号。
c)SA_NOCLDWAIT: 使父进程在它的子进程退出时不会收到SIGCHILD信号,这时子进程如果退出也不会成为僵尸进程。
d)SA_NODEFER: 使对信号的屏蔽无效,即在信号处理函数执行期间仍能发出这个信号。
e)SA_RESETHAND: 信号处理之后重新设置为默认的处理方式。
f)SA_SIGINFO: 使用sa_sigaction成员而不是sa_handler作为信号处理函数
新的信号处理函数:
void(*sa_sigaction)(int signum, siginfo_t * info, void * context);
参数说明:
signum: 信号的编号。
info: 记录信号发送进程信息的结构体
context:可以赋给指向ucontext_t类型的一个对象的指针,以引用在传递信号时被中断的接收进程或线程的上下文。
8、SIGCHLD信号
8.1 SIGCHLD信号产生的条件
- 子进程终止时
- 子进程接收到SIGSTOP信号停止时
- 子进程处在停止态,接收到SIGCONT后唤醒
8.2 如何避免僵尸进程
- 最简单的方法,父进程通过wait()和waitpid()等函数等待子进程结束,但是,这会导致父进程挂起。
- 如果父进程要处理的事情很多,不能够挂起,通过signal()函数认为处理信号SIGCHLD,只要有子进程退出自动调用指定好的回调函数,因为子进程结束后,父进程会收到该信号SIGCHLD,可以在其回调函数里调用wait()或waitpid()回收。
- 如果父进程不关心子进程什么时候结束,那么可以用signal(SIGCHLD, SIG_IGN)通知内核,自己对子进程结束不感兴趣,父进程忽略此信号,那么子进程结束后,内核会回收,并不再给父进程发送信号。