Linux信号

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.信号的产生

  1. 通过kill命令向指定的进程发送指定的信号

  2. 通过键盘可以产生信号

    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 信号常见概念

  1. 实际执行信号的处理动作称为信号递达(Delivery)

  2. 信号从产生到递达之间的状态,称为信号未决(Pending)

  3. 进程可以选择**阻塞(Block)**某个信号

  4. 被阻塞的信号产生时将保持在未决状态,直到进程解除对该信号的阻塞,才执行递达动作

    如果一个信号被阻塞,这个信号一旦产生永不递达,将一直处于阻塞,直到解除阻塞状态。

  5. 注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作

5.2 信号的底层原理

进程识别信号,本质是在进程PCB中**通过管理一个函数指针数组(sighandler_t handler[32])和两张位图(pendingblock)**来实现的。

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

参数的几种情况:

  1. 如果oldset是非空指针,则读取进程的当前信号屏蔽字通过oldset参数传出。

  2. 如果set是非空指针,则 更改进程的信号屏蔽字, 参数how指示如何更改。

  3. 如果oldset和set都是非空指针,则先将原来的信号 屏蔽字备份到oldset里,然后根据set和how参数更改信号屏蔽字。

how参数的可选值:

SIG_BLOCKset包含了我们希望添加到当前信号屏蔽字的信号,相当于mask=mask|set
SIG_UNBLOCKset包含了我们希望从当前信号屏蔽字中解除阻塞的信号,相当于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

  • 17
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值