目录
abort 函数(向自己发送 SIGABRT (6号)信号)
作者和朋友建立的社区:非科班转码社区-CSDN社区云💖💛💙
期待hxd的支持哈🎉 🎉 🎉
最后是打鸡血环节:想多了都是问题,做多了都是答案🚀 🚀 🚀
最近作者和好友建立了一个公众号
公众号介绍:
专注于自学编程领域。由USTC、WHU、SDU等高校学生、ACM竞赛选手、CSDN万粉博主、双非上岸BAT学长原创。分享业内资讯、硬核原创资源、职业规划等,和大家一起努力、成长。(二维码在文章底部哈!)
什么是信号
生活中的角度
快递到来的整个过程,对你来讲是异步的,你不能准确断定快递员什么时候给你打电话比如过红绿灯,比如收快递。比如你买了快递,即使没来,你也知道快递来了该怎么处理。当快递员来了要你去取,但是你又有更重要的事情去做的时候,就可以不立刻去取,可以等合适时间再去,并且你知道并记住了有一个快递要去取。当你时间合适,顺利拿到快递之后,就要开始处理快递了。而处理快递一般方式有三种:1. 执行默认动作(打开快递,使用商品)2. 执行自定义动作(快递是零食,你要送给你的女朋友)3. 忽略快递(快递拿上来之后,做其他事情)进程就是你,操作系统就是快递员,信号就是快递总结说,首先你能识别信号,其次即使信号没有产生,你也有处理信号的能力,至于处理,就是在你觉得合适的时候再去,处理有三种情况,一是默认,而是忽略,三是自定义。技术中的角度
用户输入命令 , 在 Shell 下启动一个前台进程。用户按下 Ctrl - C , 这个键盘输入产生一个硬件中断,被 OS 获取,解释成信号,发送给目标前台进,前台进程因为收到信号,进而引起进程退出。PS
1. Ctrl - C 产生的信号只能发给前台进程。一个命令后面加个 & 可以放到后台运行 , 这样 Shell 不必等待进程结束就可以接受新的命令, 启动新的进程。2. Shell 可以同时运行一个前台进程和任意多个后台进程 , 只有前台进程才能接到像 Ctrl - C 这种控制键产生的信号。3. 前台进程在运行过程中用户随时可能按下 Ctrl - C 而产生一个信号 , 也就是说该进程的用户空间代码执行到任何地方都有可能收到 SIGINT 信号而终止 , 所以信号相对于进程的控制流程来说是异步 (Asynchronous)的。放到后台进程
ctl + c 就终止不了了
终止:1. kill
2. 先 jobs 再 fg(front ground) 1 把这个后台进程放到前台 然后就可以ctl + c 了把前台任务放到后台除了 +& 还可以 bg n信号概念
信号是进程之间事件异步通知的一种方式,属于软中断。kiil -l 查看所有信号
每个信号都有一个编号和一个宏定义名称,这些宏定义可以在signal.h中找到,例如其中有定义 #define SIGINT 2编号34以上的是实时信号,本章只讨论编号34以下的信号,不讨论实时信号。这些信号各自在什么条件下产生,默认的处理动作是什么,在signal(7)中都有详细说明: man 7 signal1 - 31 属于普通信号 (分时操作系统,更强的的是公平调度)
34 - 64 属于实时信号(RT)
实时就是任务到来的时候就立马去处理(实时操作系统)(比如车载系统就需要实时操作系统)(这里信号也是一样的)
对于普通信号的特点:
1. 可以不用立马处理
2. 多相同信号来时可能只处理一个
而实时信号是里面处理且不能信号丢失对于信号的分析
因为信号产生是异步的,所以在产生信号的时候,进程可能在做更重要的事情,可以暂时不处理这个信号,但是不代表这个信号不被处理,即我们要记住这个信号。这就是存在了进程的PCB中,那么既然是在PCB中,也就是说至于OS有权利去处理PCB里面的数据,因为OS是进程的管理者,进程的所有属性的获取和设置,只能是OS来的。对于如何记住其实是PCB里面有各个信号对应的位图,OS通过写入对应的位来记录进程收到的信号。
关于信号的产生(信号产生前)
1. 通过终端按键产生信号
从硬件角度(在控制信号角度),外设是可以直接和CPU直接沟通的(之前是说的数据层面是不可以的),是通过硬件中断实现的
PS:老式计算机是CPU负责把外设的数据拷贝到内存里,现在是有一个叫DMA的硬件(芯片)负责。显卡里面也有自己的芯片叫做GPU。2. 调用系统函数向进程发信号
signal 对特定信号进行捕捉动作
signal代码
还有比如 在后台执行死循环程序,然后用kill命令给它发SIGSEGV信号。
4568是 test 进程的 id 。之所以要再次回车才显示 Segmentation fault , 是因为在 4568 进程终止掉之前已经回到了Shell 提示符等待用户输入下一条命令 ,Shell 不希望 Segmentation fault 信息和用 户的输入交错在一起, 所以等用户输入命令之后才显示。指定发送某种信号的kill 命令可以有多种写法 , 上面的命令还可以写成 kill - SIGSEGV 4568 或 kill - 11 4568 , 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函数总是会成功的,所以没有返回值。
abort 函数(向自己发送 SIGABRT (6号)信号)
捕捉号信号并且执行特定代码
这就很有意思了,相比与 9号 信号(kill)不可以被捕捉而言, SIGABRT信号可以被捕捉,但是捕捉完了之后程序依旧会终止。
3. 由软件条件产生信号
#include <unistd.h> unsigned int alarm(unsigned int seconds); 调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前 进程发SIGALRM信号, 该信号的默认处理动作是终止当前进程。
这个函数的返回值是 0 或者是以前设定的闹钟时间还余下的秒数。打个比方 , 某人要小睡一觉 , 设定闹钟为 30 分钟之后响,20 分钟后被人吵醒了 , 还想多睡一会儿 , 于是重新设定闹钟为 15 分钟之后响 ,“ 以前设定的闹钟时间还余下的时间 ” 就是10 分钟。如果 seconds 值为 0, 表示取消以前设定的闹钟 , 函数的返回值仍然是以前设定的闹钟时间还余下的秒数。代码演示1s后
这个程序的作用是1秒钟之内不停地数数,1秒钟到了就被SIGALRM信号终止。
加上一个信号捕捉
去掉printf,顺便改一下handler要他调用就终止
结果
发现比之前大了很多
所以说明IO非常慢!(比之前没有进行printf)PS:操作系统就是一个死循环,是硬件推动操作系统工作(不要感觉矛盾,把操作系统比作老板,把硬件比作秘书=。=,就是秘书受老板管,但又要推动老板)
(硬件里面有时钟硬件,会每隔一小段时间就给OS(CPU)发送时钟中断,CPU就会去调用OS提供的对应中段方法,来执行操作系统的代码然后完成调度,切换,检查,内存置换等各种算法)4. 硬件异常产生信号
即因为硬件异常,而导致OS向目标进程发送信号,进而导致进程终止的现象。
就比如除0就会崩溃,又或者越界野指针访问。
崩溃的本质?
进程崩溃的本质就是收到了信号!
除0为例:
CPU内部,状态寄存器,当我们除0的时候,CPU内核的状态寄存器会被设置为有报错:浮点数越界,CPU内部的寄存器(硬件),OS就会识别到CPU内有报错了 -》1. 谁干的?
2. 是什么报错?(OS-》构建信号)-》向目标进程发送信号-》目标进程在合适的时候,去处理这个信号-》终止进程。
但是崩溃了进程不一定会终止,因为上面都是默认的行为,现在我们自己捕捉
捕捉了之后因为没有exit,而我们现在程序出现了问题,而我们并没,有去解决所以OS会一直帮我们发送这个信号(这就是崩溃,即不一定会终止程序)
关于 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(核心转储),我们之前是没有讲的,关于进程等待返回的状态码,前7位是信号的,后8位是退出状态码的,第8位就是 core dump 。
core dump 我们默认是关闭的。他的作用是:
会把进程运行中,对应的异常上下文数据,core dump 到磁盘上,方便调试。
man 7 signal(查看更详细的信号手册) 比如这些就是可以打开core dump位的
ulimit -a(默认是关闭的)
ulimit -c 100000
设置了之后就有core了
然后我们对应之前那个表发现8号信号是产生core文件的
然后我们发现多了一个这个这个文件是说引起这个问题的进程是谁,并且此进程core cump标志位会被置1,这种机制就是核心转储(打开发现会是乱码)(core dump 默认是关的)
核心转储的本质就是会把进程运行中,对应的异常上下文数据,core dump 到磁盘上,方便调试。
core dump 打开之后因为异常产生的文件,makedile带上-g选项就可以之间定位到产生异常的地方
回车(直接定位问题)
信号产生中
信号相关概念
实际执行信号的处理动作称为信号递达 (Delivery)信号从产生到递达之间的状态 , 称为信号未决 (Pending) 。进程可以选择阻塞 (Block ) 某个信号。被阻塞的信号产生时将保持在未决状态 , 直到进程解除对此信号的阻塞 , 才执行递达的动作 .注意 , 阻塞和忽略是不同的 , 只要信号被阻塞就不会递达 , 而忽略是在递达之后可选的一种处理动作。在内核中的表示
每个信号都有两个标志位分别表示阻塞 (block) 和未决 (pending), 还有一个函数指针表示处理动作。信号产生时, 内核在进程控制块中设置该信号的未决标志 , 直到信号递达才清除该标志。在上图的例子中,SIGHUP 信号未阻塞也未产生过 , 当它递达时执行默认处理动作。SIGINT 信号产生过 , 但正在被阻塞 , 所以暂时不能递达。虽然它的处理动作是忽略 , 但在没有解除阻塞之前不能忽略这个信号, 因为进程仍有机会改变处理动作之后再解除阻塞。SIGQUIT 信号未产生过 , 一旦产生 SIGQUIT 信号将被阻塞 , 它的处理动作是用户自定义函数 sighandler 。如果在进程解除对某信号的阻塞之前这种信号产生过多次, 将如何处理 ?POSIX.1 允许系统递送该信号一次或多次。Linux 是这样实现的 : 常规信号在递达之前产生多次只计一次, 而实时信号在递达之前产生多次可以依次放在一个队列里。本章不讨论实时信号。pending表让我们可以知道信号来了没有,哪个信号来了。
handler表就是对应着pending表的处理方法是一个函数指针数组(之前说的OS会修改PCB中的数据改的就是pending表)(之前用的signal函数就是修改的handler表的内容)
block是阻塞信号集,ture就是不处理该信号,也是位图和pending的一样接口 sigset_t
从上图来看 , 每个信号只有一个 bit 的未决标志 , 非 0 即 1, 不记录该信号产生了多少次 , 阻塞标志也是这样表示的。因此, 未决和阻塞标志可以用相同的数据类型 sigset_t 来存储 ,sigset_t 称为信号集 , 这个类型可以表示每个信号的“ 有效 ” 或 “ 无效 ” 状态 , 在阻塞信号集中 “ 有效 ” 和 “ 无效 ” 的含义是该信号是否被阻塞 , 而在未决信号集中 “ 有效” 和 “ 无效 ” 的含义是该信号是否处于未决状态。下面将详细介绍信号集的各种操作。 阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask), 这里的“屏蔽” 应该理解为阻塞而不是忽略。sigset_t定义
信号集操作函数
sigset_t 类型对于每种信号用一个 bit 表示 “ 有效 ” 或 “ 无效 ” 状态 , 至于这个类型内部如何存储这些 bit 则依赖于系统实现, 从使用者的角度是不必关心的 , 使用者只能调用以下函数来操作 sigset_ t 变量 , 而不应该对它的内部数据做任何解释, 比如用 printf 直接打印 sigset_t 变量是没有意义的#include <signal.h> int sigemptyset(sigset_t *set); int sigfillset(sigset_t *set); int sigaddset (sigset_t *set, int signo); int sigdelset(sigset_t *set, int signo); int sigismember(const sigset_t *set, int signo);
使用直接 man 查询详细信息
函数 sigemptyset 初始化 set 所指向的信号集 , 使其中所有信号的对应 bit 清零 , 表示该信号集不包含任何有效信号。函数 sigfifillset 初始化 set 所指向的信号集 , 使其中所有信号的对应 bit 置位 , 表示 该信号集的有效信号包括系统支持的所有信号。注意 , 在使用 sigset_ t 类型的变量之前 , 一定要调 用 sigemptyset 或 sigfifillset 做初始化 , 使信号集处于确定的状态。初始化sigset_t 变量之后就可以在调用 sigaddset 和 sigdelset在该信号集中添加或删除某种有效信号。这四个函数都是成功返回 0, 出错返回 -1 。 sigismember 是一个布尔函数 , 用于判断一个信号集的有效信号中是否包含某种 信号, 若包含则返回 1, 不包含则返回 0, 出错返回 -1 。sigprocmask
调用函数 sigprocmask 可以读取或更改进程的信号屏蔽字 ( 阻塞信号集 )(block表)。#include <signal.h> int sigprocmask(int how, const sigset_t *set, sigset_t *oset); 返回值:若成功则为0,若出错则为-1
第三个参数是返回以前的信号屏蔽字(为了想恢复)(输出型参数,不用设置为nullptr)如果 oset 是非空指针 , 则读取进程的当前信号屏蔽字通过 oset 参数传出。如果 set 是非空指针 , 则 更改进程的信号屏蔽字, 参数 how 指示如何更改。如果 oset 和 set 都是非空指针 , 则先将原来的信号 屏蔽字备份到 oset 里 , 然后根据set 和 how 参数更改信号屏蔽字。假设当前的信号屏蔽字为 mask, 下表说明了how参数的可选值。如果调用 sigprocmask 解除了对当前若干个未决信号的阻塞 , 则在 sigprocmask 返回前 , 至少将其中一个信号递达。sigpending
#include <signal.h> sigpending 读取当前进程的未决信号集,通过set参数传出。调用成功则返回0,出错则返回-1。 下面用刚学的几个函数做个实验。程 序如下:
sigpengding获取信号的pending信号集(课件有点问题)(也是输出型参数)
使用(去gitee上copy完善的11111位置)
程序运行时 , 每秒钟把各信号的未决状态打印一遍 , 由于我们阻塞了 SIGINT 信号 , 按 Ctrl-C 将会 使 SIGINT 信号处于未决状态, 按 Ctrl-\ 仍然可以终止程序 , 因为 SIGQUIT 信号没有阻塞。捕捉信号(信号产生后)
内核如何实现信号的捕捉
如果信号的处理动作是用户自定义函数 , 在信号递达时就调用这个函数 , 这称为捕捉信号。由于信号处理函数的代码是在用户空间的, 处理过程比较复杂 , 举例如下 : 用户程序注册了 SIGQUIT 信号的处理函数 sighandler 。 当前正在执行main函数 , 这时发生中断或异常切换到内核态。 在中断处理完毕后要返回用户态的 main 函数之前检查到有信号SIGQUIT递达。 内核决定返回用户态后不是恢复 main 函数的上下文继续执行 , 而是执行 sighandler 函 数 ,sighandler 和main 函数使用不同的堆栈空间 , 它们之间不存在调用和被调用的关系 , 是 两个独立的控制流程。 sighandler 函数返回后自动执行特殊的系统调用sigreturn 再次进入内核态。 如果没有新的信号要递达 , 这次再返回用户态就是恢复main函数的上下文继续执行了。用几幅图理解
图一
图二
signal 函数
signal 对特定信号进行捕捉动作
sigaction 函数
#include <signal.h>int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);sigaction 修改hander表sigaction 结构体目前只考虑 handler和masksigaction 函数可以读取和修改与指定信号相关联的处理动作。调用成功则返回 0, 出错则返回 - 1 。 signo 是指定信号的编号。若act 指针非空 , 则根据 act 修改该信号的处理动作。若 oact 指针非 空 , 则通过 oact 传出该信号原来的处理动作。act 和 oact 指向 sigaction 结构体。将 sa_handler 赋值为常数 SIG_IGN 传给 sigaction 表示忽略信号 , 赋值为常数 SIG_DFL 表示执行系统默认动作, 赋值为一个函数指针表示用自定义函数捕捉信号 , 或者说向内核注册了一个信号处理函 数 , 该函数返回值为void, 可以带一个 int 参数 , 通过参数可以得知当前信号的编号 , 这样就可以用同一个函数处理多种信 号。显然, 这也是一个回调函数 , 不是被 main 函数调用 , 而是被系统所调用。这样就可以一下把 2 3号都拦住
sigaction的更大的意义就在于防止操作系统嵌套式的去执行同一个信号处理
关于mask
当某个信号的处理函数被调用时 , 内核自动将当前信号加入进程的信号屏蔽字 , 当信号处理函数返回时自动恢复原来的信号屏蔽字, 这样就保证了在处理某个信号时 , 如果这种信号再次产生 , 那么 它会被阻塞到当前处理结束为止。 如果在调用信号处理函数时, 除了当前信号被自动屏蔽之外 , 还希望自动屏蔽另外一些信号 , 则用 sa_mask 字段说明这些需要额外屏蔽的信号, 当信号处理函数返回时自动恢复原来的信号屏蔽字。 sa_flflags 字段包含一些选项 , 本章的代码都把 sa_flflags 设为 0,sa_sigaction 是实时信号的处理函数。就是说不允许我们的普通信号被重复提交
PS:
删掉一个进程 可以 kill -9 +pid
killall + 进程名也可以kill 函数(给任意进程发任意信号)
raise 函数(给自己发任意信号)
abort 函数(向自己发送 SIGABRT (6号)信号)
这就很有意思了,相比与 9号 信号不可以被捕捉而言, SIGABRT信号可以被捕捉,但是捕捉完了之后程序依旧会终止
alarm 函数
1s后
(进程收到信号默认是结束进程的,所以进程跑1s就结束了)
加上一个信号捕捉顺便改一下handler要他调用就终止结果
发现比之前大了很多
所以说明IO非常慢!(比之前没有进行printf)PS
操作系统就是一个死循环,是硬件推动操作系统工作(不要感觉矛盾,把操作系统比作老板,把硬件比作秘书=。=,就是秘书受老板管,但又要推动老板)
(硬件里面有时钟硬件,会每隔一小段时间就给OS(CPU)发送时钟中断,CPU就会去调用OS提供的对应中段方法,来执行操作系统的代码然后完成调度,切换,检查,内存置换等各种算法)可重入函数
main 函数调用 insert 函数向一个链表 head 中插入节点 node1, 插入操作分为两步 , 刚做完第一步的 时候 , 因为硬件中断使进程切换到内核, 再次回用户态之前检查到有信号待处理 , 于是切换 到 sighandler 函数,sighandler 也调用 insert 函数向同一个链表 head 中插入节点 node2, 插入操作的 两步都做完之后从sighandler返回内核态 , 再次回到用户态就从 main 函数调用的 insert 函数中继续 往下执行 , 先前做第一步之后被打断, 现在继续做完第二步。结果是 ,main 函数和 sighandler 先后 向链表中插入两个节点 , 而最后只有一个节点真正插入链表中了。像上例这样 ,insert 函数被不同的控制流程调用 , 有可能在第一次调用还没返回时就再次进入该函数 , 这称为重入,insert 函数访问一个全局链表 , 有可能因为重入而造成错乱 , 像这样的函数称为 不可重入函数 , 反之 ,如果一个函数只访问自己的局部变量或参数, 则称为可重入 (Reentrant) 函数。如果一个函数符合以下条件之一则是不可重入的 :调用了 malloc 或 free, 因为 malloc 也是用全局链表来管理堆的。调用了标准 I/O 库函数。标准 I/O 库的很多实现都以不可重入的方式使用全局数据结构。volatile 关键字
因为一直使CPU高速计算而且发现没有改变,所以就可能优化,直接从寄存器里面拿值,但是当我们发送信号后改变的是内存的值,这就可能会出问题
volatile -- 保持内存的可见性(强制编译器访问内存数据)
SIGCHLD 信号
讲过用 wait 和 waitpid 函数清理僵尸进程 , 父进程可以阻塞等待子进程结束 , 也可以非阻 塞地查询是否有子进程结束等待清理( 也就是轮询的方式 ) 。采用第一种方式 , 父进程阻塞了就不 能处理自己的工作了 ; 采用第二种方式 , 父进程在处理自己的工作的同时还要记得时不时地轮询一 下, 程序实现复杂。其实 , 子进程在终止时会给父进程发 SIGCHLD 信号 , 该信号的默认处理动作是忽略 , 父进程可以自 定义 SIGCHLD 信号的处理函数, 这样父进程只需专心处理自己的工作 , 不必关心子进程了 , 子进程 终止时会通知父进程 , 父进程在信号处理函数中调用wait 清理子进程即可。( 事实上 , 由于 UNIX 的历史原因 , 要想不产生僵尸进程还有另外一种办法 : 父进程调 用 sigaction 将 SIGCHLD 的处理动作置为SIG_IGN, 这样 fork 出来的子进程在终止时会自动清理掉 , 不 会产生僵尸进程 , 也不会通知父进程。系统默认的忽略动作和用户用sigaction 函数自定义的忽略 通常是没有区别的 , 但这是一个特例。此方法对于 Linux 可用 , 但不保证在其它UNIX 系统上都可 用。请编写程序验证这样做不会产生僵尸进程。)(虽然都是忽略(不调用和调用这个函数),但是他毕竟是个函数,还会进程后续处理,不会让子进程僵尸,会让子进程退出,并且不会给父进程发送信号)直接就自动回收了
经过测试发现(验证了)暂停和终止都会给父进程发这个信号
而且解除暂停也会先捕捉子进程退出后的信号,然后执行捕捉的函数,不去影响父进程
但是上面有问题
有多个子进程的话上面只会执行一次(因为同时退出的时候发送的都是同一个信号,所以OS会把blocks表置为1(收到一个信号就会把那个信号对应的block置1),pengding表置为1,只会执行一次)
改法,循环去waitpid当没有需要等待的时候就等待失败就退出了
但是还有问题,比如但是我们发现前几个进程退出之后,虽然后3个子进程还在跑,但是父进程不跑了,是因为后面的子进程没有退出的话父进程是阻塞等待,改成WNOHANG就可以了。
改一改代码
最后的最后,创作不易,希望读者三连支持💖
赠人玫瑰,手有余香💖