【Linux】—— Linux中进程信号1

信号的概念

生活中的信号

  • 为了理解一下信号这个概念,我们来举一些生活中的例子
  • 比如:你在淘宝上买了一件商品,你在等这些商品来,虽然这些商品没有来,但是你却知道他来了之后你应该怎么做。之后当我收到快递公司给我通知我们的快递到了之后,我们有些人会立刻去拿,有的可能选择过一会去拿,我们可以理解为"在合适的时间去完成取快递这件事"
  • 再举个栗子,平时我们睡觉之前会设置一个闹钟,定时叫我们起床,虽然这个闹钟没响,但是我们也知道闹钟响了我们应该怎么做。再闹钟响了之后,我们要处理这个事件,有三种方式:1.执行默认动作(把闹钟关了,然后起床)。2、执行自定义动作(把闹钟关了,继续睡觉)。3.忽略闹钟(不关闹钟让它继续响)。

技术应用角度的信号

信号其实我们也见过,当我们在shell上写出一个死循环退不出来的时候,只需要一个组合键,ctrl+c,就可以解决了,这就是一个信号,但是真正的过程并不是那么简单的。

  • 1、当用户按下这一对组合键时,这个键盘输入会产生一个硬件中断,如果CPU正在执行这个进程的代码时,则该进程的用户代码先暂停执行,用户从用户态切换到内核态处理硬件中断
  • 2、终端驱动程序将这一对组合键翻译成一个SIGINT信号记在该进程的PCB中(也就是发送了一个SIGINT信号给该进程)
  • 3、当某个时刻要从内核态回到该进程的用户·空间代码继续执行之前,首先处理PCB中的信号,发现有一个SIGINT信号需要处理,而这个信号的默认处理方式是终止进程,所以直接终止进程,不再返回用户空间执行代码。

注意:ctrl+c只能终止前台进程。一个命令可以加&可以将进程放在后台执行,这样shell就不必等待进程结束就可以接收新的命令,启动新的进程。

信号的概念

  • 信号是进程之间实践异步通知的一种方式,属于软中断

通过我们刚刚举的生活中信号的例子和我们讲到的技术应用角度的信号,我们类比在进程间的信号,我们同样可以得到以下结论:

  • 1、进程必须学会辨认信号,必须知道遇到信号之后应该如何处理。
  • 2、进程收到信号之后不一定会立即处理,而是等到合适的时间去处理,但是他记住了自己收到了一个信号,等待处理。
  • 3、进程收到信号之后处理方式有三种:1.执行默认动作 2.执行自定义动作 3.忽略该信号
  • 4、进程储存信号的地方是进程的PCB,储存信号可以使用一个int位图(有该信号对应位置为一,没有为0).
  • 5、操作系统向进程法信号,其实是到进程的PCB的存储信号位图中找到对应的信号所在的位置将该位的0变为1,因此其实可以形象说操作系统是想进程写信号

系统信号列表

  • kill -l 查看系统定义的信号列表
    信号列表
  • 编号34以上的都为实时信号,我们这里只讨论编号34一下的信号
  • 这些信号各自在什么条件下产生,默认的处理方式是什么在signal(7)中都有详细说明,我们可以通过命令 man 7 signal
    signal(7)

产生信号的方式

1.通过终端按键产生信号

SIGINT的默认处理动作是终止进程,SIGQUIT的默认处理动作是终止进程并且Core Dump,核心转储,这个概念我们在之前博客 进程控制 进程等待部分我们提到过,现在我们来验证一下。
Core Dump

  • 首先解释什么是Core Dump。当一个进程要异常终止时,可以选择把进程的用户空间内存数据全部 保存到磁盘上,文件名通常是core,这叫做Core Dump
  • 进程异常终止通常是因为有Bug,比如非法内存访问导致段错误,事后可以用调试器检查core文件以查清错误原因,这叫做Post-mortem Debug(事后调试)。
  • 一个进程允许产生多大的core文件取决于进程的Resource Limit(这个信息保存 在PCB中)。默认是不允许产生core文件的,因为core文件中可能包含用户密码等敏感信息,不安全。在开发调试阶段可以用ulimit命令改变这个限制,允许产生core文件。 首先用ulimit命令改变Shell进程的Resource Limit,允许core文件最大为1024K: $ ulimit -c1024
    Core Dump
  • 写一个死循环程序
    在这里插入图片描述
  • 前台运行该程序,然后再终端键入Ctrl + \,终止该程序
    信号
  • ulimit命令改变了Shell进程的Resource Limit,test进程的PCB由Shell进程复制而来,所以也具 有和Shell进程相同的Resource Limit值,这样就可以产生Core Dump了。

2.系统函数向进程发信号

  • 首先我们运行我们刚刚编写的死循环进程,在通过kill命令向其发送SIGSEGV信号
    段信号

  • 29225是test进程的id。之所以要再次回车才显示 Segmentation fault(段错误) ,是因为在29225进程终止掉之前已经回到了Shell提示符等待用户输入下一条命令,Shell不希望Segmentation fault信息和用 户的输入交错在一起,所以等用户输入命令之后才显示

  • 指定发送某种信号的kill命令可以有多种写法,上面的命令还可以写成 kill -SIGSEGV 29225kill -11 29225 , 11是信号SIGSEGV的编号。以往遇到的段错误都是由非法内存访问产生的,而这个程序本身没错,给它发SIGSEGV也能产生段错误。

  • kill命令是调用kill函数实现的。kill函数可以给一个指定的进程发送指定的信号。raise函数可以给当前进程发送指定的信号(自己给自己发信号)。

#include <signal.h>
int kill(pid_t pid, int signo);
int raise(int signo);
这两个函数都是成功返回0,错误返回-1。
  • abort函数使当前进程接收到信号而异常终止。
#include <stdlib.h>
void abort(void);
就像exit函数一样,abort函数总是会成功的,所以没有返回值。

3.由软件条件产生信号

SIGPIPE是一种由软件条件产生的信号,在“管道”中已经介绍过了。这里主要主要介绍alarm函数SIGALRM信号

#include <unistd.h>
unsigned int alarm(unsigned int seconds);
调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发SIGALRM信号, 
该信号的默认处理动作是终止当前进程。

这个函数的返回值是0或者是以前设定的闹钟时间还余下的秒数。打个比方,某人要小睡一觉,设定闹钟为30分钟之后响,20分钟后被人吵醒了,还想多睡一会儿,于是重新设定闹钟为15分钟之后响,“以前设定的闹钟时间还余下的时间”就是10分钟。如果seconds值为0,表示取消以前设定的闹钟,函数的返回值仍然是以前设定的闹钟时间还余下的秒数

  • 来段代码验证一下alarm函数
    alarm
  • 运行结果为
    alarm

4.硬件异常产生的信号

  • 硬件异常被硬件以某种方式被硬件检测到并通知内核,然后内核向当前进程发送适当的信号。

    扫描二维码关注公众号,回复: 8893296 查看本文章
  • 例如当前进程执行了除以0的指令,CPU的运算单元会产生异常,内核将这个异常解释 为SIGFPE信号发送给进程。再比如当前进程访问了非法内存地址,MMU会产生异常,内核将这个异常解释为SIGSEGV信号发送给进程。

  • 模拟一下野指针异常默认行为下
    默认
    默认

  • 模拟野指针异常捕捉行为
    默认行为野指针
    在这里插入图片描述
    由此可以确认,我们在C/C++当中除零,内存越界等异常,在系统层面上,是被当成信号处理的。

总结思考一下

  • 上面所说的所有信号产生,最终都要有OS来进行执行,为什么?OS是进程的管理者
  • 信号的处理是否是立即处理的?在合适的时候
  • 信号如果不是被立即处理,那么信号是否需要暂时被进程记录下来?记录在哪里最合适呢?
  • 一个进程在没有收到信号的时候,能否能知道,自己应该对合法信号作何处理呢?
    如何理解OS向进程发送信号?能否描述一下完整的发送处理过程?
发布了167 篇原创文章 · 获赞 175 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/chenxiyuehh/article/details/95000038