信号概念
信号是进程之间事件异步通知的一种方式,会打断当前的进程,使之去处理信号的事件,信号是一种软件中断
信号的种类
每个信号都有一个编号和一个宏定义名称,这些宏定义可以在signal.h中找到,同时,在Linux系统7号手册中也可以找到
可以使用kill -l命令可以查看Linux系统下所有的信号,其中:
总共有62中信号 :
1~31号信号是不可靠信号,即信号有可能丢失
34~64号信号是可靠信号,即该信号是不会被丢失的
1~31号信号是非实时信号,34~64号信号是实时信号
信号的产生
通过终端按键产生
ctrl + c 发送的是2号SIGINT信号
ctrl + z 发送的是20号SIGSTOP信号
ctrl + | 发送的是3号SIGQUIT信号
【注】:
SIGINT的默认处理动作是终止进程,SIGQUIT的默认处理动作是终止进程并Core Dump
Core Dump:当一个进程要异常终止时,可以选择把进程的用户空间内存数据全部保存到磁盘上,文件名通常是core。
进程异常终止通常是因为有Bug,比如非内存访问导致段错误,之后可以用调试器检查core文件已查清错误原因,这叫做事后调试。一个进程允许产生多大的core文件取决于进程的Resourse Limit(保存在PCB中)。默认是不允许产生core文件的,因为core文件中可能包含用户密码等敏感信息,不安全。在开发调试阶段可以使用ulimit这个命令改变这个限制,允许产生core文件。
调用系统函数产生
kill命令是调用kill函数实现的。kill函数可以给一个指定的进程发送指定的信号
#include <signal.h>
int kill (pid_t pid, int signal);
raise函数可以给当前进程发送指定的信号(自己给自己发送进程)
#include <signal.h>
int raise (int signo);
abort函数使当前进程接收到信号而异常终止
#include <stdlib.h>
void abort (void);
软件条件产生
alarm函数在进程中设置一个定时器,当定时器指定的时间到时,它向进程发送一个SIGALRM信号,可以设置忽略或不捕获该信号,如果采用默认处理方式是终止调用该alarm函数的进程
#include <unistd.h>
unsigned int alarm (unsigned int seconds);
在管道中,当管道读端关闭,写端继续尝试写的时候,会触发SIGPIPE信号
硬件异常产生
硬件异常被被硬件以某种方式检测到并通知内核,然后内核向当前进程发送适当的信号。例如当前进程执行了除以0的指令,CPU的运算单元会产生异常,内核将这个异常解释为SIGFPE信号发送给进程。又比如当前进程访问了非法的内存地址,MMU就会产生异常,内核将这个异常解释为SIGSEGV信号发送给进程。再比如当前进程对在堆上申请的一片内存释放了两次,就会触发SIGABRT信号。
信号的生命周期
信号在进程中的注册
// 进程PCB中关于本进程中的未决信号的成员变量的定义:
struct sigpending
{
struct sigqueue *head, *tail; //指向sigqueue类型的结构链的首尾
sigset_t signal; //位图,未决信号集
};
// 其中,struct sigqueue 结构体的定义:
struct sigqueue
{
struct sigqueue *next;
siginfo_t info;
};
对于非可靠信号:
如果待注册的信号,在pending位图中已经存在了,意味着已经增加了sigqueue节点,不需要再去添加当前的信号了,即不需要增加sigqueue节点
如果待注册的信号,在pending位图中不存在,则将pending位图中的对应的bit位置为1,然后添加sigqueue节点
对于可靠信号:
如果待注册的信号,在pending位图中已经存在,则需要增加sigqueue节点
如果待注册的节点,在pending位图中不存在,则更改pending位图中对应的bit位,并且增加sigqueue节点
信号在进程中的注销
对于非可靠信号:
将pending位图中的对应的bit位置为0,并且将sigqueue节点删除
对于可靠信号:
如果注册的信号存在sigqueue节点只有1个,则将pending位图中的对应的bit位置为0,并且将sigqueue节点删除
如果注册的信号存在sigqueue节点有多个,不能将pending位图中的对应的bit位置为0,并且删除一个sigqueue节点
信号的处理
默认处理方式:SIG_DFL
忽略处理方式:SIG_IGN
自定义处理方式:
signal函数:可以重置当前操作系统对信号的处理方式
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
struct sigaction
{
void (*sa_handler)(int); //默认调用的函数
void (*sa_handler)(int, siginfo_t *, void *); //当sa_flags的取值是SA_SIGINFO时是一个被调用的函数
sigset_t sa_mask; //当当前进程在处理信号的时候收到新的信号,会将新的信号放在sa_mask中去
int sa_flags;
void (*sa_restorer)(void); //预留的信息
};
参数:
act:表示将当前信号修改成指定的处理方式,即让操作系统接收到这个信号时调用哪一个函数进行处理
oldact:表示之前操作系统对收到该信号的时候的处理方式,即操作系统对接收到的信号的默认处理方式
自定义信号的处理流程
在PCB中有一个 sighand_struct 的指针,这个指针指向了一个 sighand_struct 的结构体,在这个结构体当中有一个 action 数组,数组的每一个元素都对应了一个信号的处理逻辑,每一个元素都是 struct_sigaction
在 struct_sigaction 中有一个元素struct sigaction sa ,在sa元素中有一个变量是 sa_handler ,这个变量保存了信号的处理函数的地址
处理方式:
- 默认处理信号:当 pending 位图中某个信号的bit位被置为1,则表示收到了该信号,当操作系统需要去处理该信号的时候,就会从PCB当中去寻找 sighand_struct 的指针,从而根据该指针找到 sa_handler ,进而到操作系统内核中调用该函数,至此完成信号的功能
- 自定义处理信号:
signal:相当于换掉了sa_handler,即换掉了保存的默认执行函数的地址,从而在处理该信号的时候,可以调用到自定义的函数
sigaction:相当于换掉了action数组中的元素,即直接换掉结构体,从而修改信号处理函数地址
在底层实现上,signal函数调用sigaction函数实现
信号的捕捉
信号的捕捉流程
如果信号的处理动作是用户自定义函数,在信号传达时就调用这个函数,这称为捕捉信号。
由于信号处理函数的代码是在用户空间中,处理过程比较复杂,如上图:
例如:用户程序注册了SIGQUIT信号的处理函数sighandler。当前正在执行main函数,这时发生中断或异常切换到内核态。在中断处理完毕要返回用户态的main函数之前检查到有信号SIGQUIT递达。内核决定返回用户态后不是恢复main函数的上下文继续执行,而是执行sighandler函数,sighandler和main函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是两个独立的控制流程。sighandler函数返回后自动执行特殊的系统调用sigreturn再次进入内核态。如果没有新的信号要递达,这次再返回用户态就是恢复main函数的上下文继续执行
sigaction
#include <signal.h>
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);
sigaction函数可以读取和修改与指定信号相关联的处理动作。调用成功则返回0,出错则返回-1,signo是指定信号的编号。若act指针非空,则根据act修改该信号的处理动作。若oact指针非空,则通过oact传出该信号原来的动作
信号的阻塞
信号阻塞,并不是说信号不能被注册,而是,操作系统在判断pending位图时发现接受到某个信号,则去查找block位图对应的bit位:
如果block对应的位置为1,则不处理该信号,sigqueue节点还是在的
如果block对应的位置为0,则处理该信号
sigprocmask
调用函数sigprocmask可以读取或更改进程的信号屏蔽字(阻塞信号集)
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
返回值:成功返回0,出错返回1
如果oset是非空指针,则读取进程的当前信号屏蔽字通过oset参数传出。如果set是非空指针,则更改进程的信号屏蔽字
参数how表明如何更改:有三个可选值:
可选值 | 描述 |
---|---|
SIG_BLOCK | set包含了我们希望添加到当前信号屏蔽字的信号,相当于mask=mask | set |
SIG_UNBLOCK | set包含了我们系统从当前信号屏蔽字中接触阻塞的信号,相当于mask=mask&(~set) |
SIG_SETMASK | 设置当前信号屏蔽字为set所指向的值,相当于mask=set |
【注】
9号、19号信号是不能被设置为阻塞的
可重入函数
竞态条件
程序的不同的执行流,执行顺序的不同,会导致程序结果的不同,这称之为竞态条件
重入
重入:不同的执行流可以访问同样的资源(同样的代码)
可重入:不同的执行流可以访问同样的资源,不会对程序的结果产生影响
不可重入:不同的执行流可以访问同样的资源,但是会对程序的结果产生影响
如果一个函数符合以下条件之一则是不可重入的:
调用了malloc或free,因为malloc也是全局链表来管理堆的
调用了标准库I/O库函数,标准I/O库的很多实现都以不可重入的方式使用全局数据结构