Linux信号
1.信号的概念
信号是Linux系统提供的一种向指定进程发送特定事件的方式,进程收到信号后再对信号进行识别和处理。信号产生是异步的,即进程不知道自己什么时候会收到信号,但随时都要有处理信号的能力。
在Linux中一共有62个信号,其中1号到31号信号称为普通信号,34号到64号称为实时信号,没有32号和33号信号。31个普通信号中每个信号都有自己的特殊含义,进程在收到不同的信号后也会进行不同的处理。
2.信号的系统调用
sighandler_t signal(int signum, sighandler_t handler)
是一个用来对信号的自定义捕捉的函数,它允许用户在进程收到不同的信号时自定义其处理方式。且**信号只需要捕捉一次,后续就一直有效。**需要注意的是:6号信号(终止信号)即使自定义捕捉了也会把进程终止,9号信号不允许自定义捕捉
int signum
:是信号的编号
sighandler_t handler
:函数指针,向自定义的函数传入信号作为参数。
int kill(pid_t pid,int sig)
是一个向指定进程发送指定信号的系统调用
pid_t pid
:是指定进程的pid
int sig
:是需要向指定进程发送的信号
int rasie(int sig)
向调用这个函数的进程发送指定的信号
int sig
:是需要发送的信号
void abort(void)
强制终止进程,本质是向进程发送6 SIGABRT终止信号
unsigned int alarm(unsigned int seconds)
在n秒后向进程发送14 SIGALRM信号,14号信号也是一个终止信号,进程在接受到这个信号后会终止。
unsigned int seconds
:这个参数的单位是秒
6号信号即使自定义捕捉了也会把进程终止,9号信号不允许自定义捕捉
3.信号的产生
-
通过kill命令向指定的进程发送指定的信号
-
通过键盘可以产生信号
ctrl+c就是2 SIGINT,它本质就是给目标进程发生2号信号,表示终止进程
信号是进程PCB(task_struct)结构体的一个成员变量,信号在PCB中是以位图的形式存储的。用uint32_t signals
类型作为位图,其中的32个比特位,舍去第1个比特位,剩余31个比特位对应31个普通信号。向进程发送信号的本质就是修改进程PCB中信号的指定位图(对应的位置由0变为1表示发送了该编号的信号)
4. 程序崩溃的原理
程序崩溃有两种情况,一是进程非法访问,二是进程非法操作,导致OS向进程发送了终止信号。如进程越界访问,OS发送了11 SIGSEGV表示非法访问,进程进行了除与0的运算,OS发送了8 SIGFPE表示浮点数错误。
操作错误:
寄存器中有一个溢出标记位来检测进程的运算是否正确,如果溢出标记位由0改成1,那么OS就会向进程发送错误信号,但进程不会因为错误信号被杀死(因为杀死进程有可能造成数据丢失)。寄存器保存了进程的所有数据,当CPU下一次调度到该进程时,寄存器中的溢出标记位依然是1,OS再次向进程发送错误信号,以此往复,直到进程被杀死。所以杀死进程本质就是释放进程的上下文数据,包括进程的溢出标记位和其他异常数据。
越界访问:
CPU进行运算时,使用的地址都是虚拟地址。CPU中的CR3寄存器存放着页表的起始地址,虚拟地址通过MMU加上CR3产生物理地址。OS管理着CPU,当CPU发现处理的地址是非法的时,会将这个非法的地址存入**CR2寄存器(页故障线性地址寄存器)**中,然后OS向进程发送错误信号。同样的,错误信号不会杀死进程,而进程在CPU的轮询调度中不断读取CR2中的错误信息,OS也就不断地向进程发送错误信号,直到进程被杀死。
core、term的区别:
core异常终止并形成一个debug文件,云服务器默认关闭了core生成debug文件(本质是允许core的debug文件大小为0kb)
term异常终止
unlimited -a查看用户使用OS资源的限制
core终止形成的core文件是一种用于在程序异常终止后调试查找代码错误的文件。在gdb下使用core-file core(core文件的具体名称)
命令,调试将直接跳转到导致程序异常终止的代码位置。使用core文件配合gdb调试的方法被称为事后调试。
在Linux进程的控制中提及过wait()
和waitpid()
函数中的status参数,其中的core dump标记位为0或1,是在标记进程是以core终止还是以term终止。
5. 信号阻塞
5.1 信号常见概念
-
实际执行信号的处理动作称为信号递达(Delivery)
-
信号从产生到递达之间的状态,称为信号未决(Pending)
-
进程可以选择**阻塞(Block)**某个信号
-
被阻塞的信号产生时将保持在未决状态,直到进程解除对该信号的阻塞,才执行递达动作
如果一个信号被阻塞,这个信号一旦产生永不递达,将一直处于阻塞,直到解除阻塞状态。
-
注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作
5.2 信号的底层原理
进程识别信号,本质是在进程PCB中**通过管理一个函数指针数组(sighandler_t handler[32]
)和两张位图(pending
和block
)**来实现的。
sighandler_t handler[32]
是一个函数指针数组,信号的编号就是它的下标。**因此只需要找到信号对应的下标,就能索引信号处理方法。**其中SIG_DEL
表示默认处理信号,SIG_IGN
表示忽略信号。
pending
(未决信号集)是一个位图,比特位的位置表示信号的编号,比特位的内容表示信号是否收到。
block
(阻塞信号集)也是位图,比特位的位置表示信号的编号,比特位的内容表示信号是否阻塞。
一个信号产生后,首先pending里对应的比特位由0变1。然后再查看block对应的比特位是否为1(阻塞),若为1则会一直阻塞至其变为0。最后再查看handler里的标记,如果是SIG_IGN则该信号将会被忽略。如果是SIG_DEL那么就会进入用户层面查找该下标对应的函数并调用。
6. sigset_t和信号集操作函数
sigset_t
是Linux提供的一种位图数据结构,称为信号集。这个类型可以表示每个信号的“有效”或“无效”状态。其中阻塞信号集中的“有效”或“无效”状态指信号是否被阻塞,而未决信号集中的“有效”或“无效”状态指信号是否处于未决状态。阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask),但这里的“屏蔽”应该理解为阻塞,而不是忽略。
sigset_t
这个类型如何存储bit依赖于系统的实现,因此操作这个数据结构的函数都是系统调用,使用者只能通过以下函数来操作sigset_t
:
#include<signal.h>
函数声明 | 功能介绍 |
---|---|
int sigemptyset(sigset_t* set) | 用来将参数set信号集初始化并清空。 |
int sigfillset(sigset_t* set) | 用来将参数set信号集初始化,然后把所有的信号加入到此信号集里即将所有的信号标志位置为1,屏蔽所有的信号。 |
int sigaddset(sigset_t* set, int signo) | 用来将参数signum 代表的信号加入至参数set 信号集里。成功返回0,失败返回-1。 |
int sigdelset(sigset_t* set, int signo) | 用来将参数signum代表的信号从参数set信号集里删除。成功返回0,失败返回-1。 |
int sigismember(const sigset_t* set, int signo) | 布尔函数,用于判断一个信号集的有效信号中是否包含某种信号。若包含返回1,不包含返回0,出错返回-1 |
7. sigprocmask函数
调用函数sigprocmask可以读取或更改进程的信号屏蔽字(阻塞信号集)。
int sigprocmask(int how, const sigset_t* set, sigset_t* oldset)
int how
:指示如何更改
const sigset_t* set
:输入的信号集
sigset_t* oldset
:一个输出型参数,保存老的信号屏蔽返回给用户。返回值:若成功则为0, 若出错则为-1
参数的几种情况:
-
如果oldset是非空指针,则读取进程的当前信号屏蔽字通过oldset参数传出。
-
如果set是非空指针,则 更改进程的信号屏蔽字, 参数how指示如何更改。
-
如果oldset和set都是非空指针,则先将原来的信号 屏蔽字备份到oldset里,然后根据set和how参数更改信号屏蔽字。
how参数的可选值:
SIG_BLOCK | set包含了我们希望添加到当前信号屏蔽字的信号,相当于mask=mask|set |
---|---|
SIG_UNBLOCK | set包含了我们希望从当前信号屏蔽字中解除阻塞的信号,相当于mask=mask&~set |
SIG_SETMASK | 设置当前信号屏蔽字为set所指向的值,相当于mask=set |
如果调用sigprocmask解除了对当前若干个未决信号的阻塞,则在sigprocmask返回前,至少将其中一个信号递达。
8. sigpending函数
int sigpending(sigset_t* set)
这个函数用于获取当前进程的pending位图,输出型。返回值0为成功,出错则返回-1。
解除屏蔽时,一般会立即处理当前被解除的信号(如果被pending),且pending位图对应的信号也要被清0,这个操作在信号递达之前就已经完成了。
9. 捕捉信号
信号是异步的,会处于未决状态,所以信号有可能不会立即被处理,那么信号是什么时候处理的呢?
每当进程在用户和内核间切换时都会捕捉信号,但信号不是在切换时就会处理。而是在内核态切换回用户态时才处理(图中灰色点的时候)。操作系统不会直接去执行用户提供的handler方法,因为操作系统有系统调用的权限,如果让操作系统去执行用户的函数就有可能会破坏内核。
在进程的地址空间里,其中[0,3]GB 是用户空间,[3,4]GB是内核空间,用户访问[3,4]地址空间的时候,要受到一定的约束(只能通过系统调用)。
9.1 系统调用
在x86架构中,用户态和内核态本质就是CPU中的cs寄存器中的最低两位(RPL:Requested Privilege Level)控制的:
如果其表示为3级,那么这个寄存器只能处理用户态的数据;如果其表示为0级,那么这个寄存器可以处理内核级别的数据。由用户态切换到内核态,本质就是该寄存器由0级变为3级
10. sigaction函数
sigaction()是另一个自定义捕捉信号的函数
int sigaction(int signum, const struct sigaction* act,struct sigaction* oldact)
int signum: 除SIGKILL 和 SIGSTOP以外的任意信号
const struct sigaction act、struct sigaction oldact:**如果act非空,从act中输入信号集;如果oldact非空,那么将旧的信号级保存入oldact
当前如果正在对某信号进行处理,默认该信号就会被自动屏蔽。对该信号处理完成的时候,会自动解除该信号的屏蔽。
一个进程不能把所有的信号都屏蔽,设计者在设计OS时不但对一个进程能够屏蔽的信号数量设置了上限,也不允许用户端进程屏蔽九号信号,否则就会产生一个无法被杀死的进程。
11. 可重入函数
**只要代码中使用了全局的数据结构,那么这个函数就是不可重入的。**我们使用的大部分函数都是不可重入的。
CPU的运算分为算数运算和逻辑运算,CPU进行逻辑运算+判断后(true or false)然后去执行其他代码。编译器可能会对代码进行过度优化,CPU不直接从内存中读数据,而是从寄存器里读,当内存的数据发生变化时,寄存器隐瞒了内存的真实值,volatile关键字是用来让编译器不对该变量进行优化的关键字。
系统维护的SIG_IGN和用户的SIG_IGN不一样,所以用户要手动设置SIG_IGN