Linux|【Linux】信号(1)认识、记录和产生信号

认识信号:
当进程发生错误时,OS 会中止进程,它是如何做到的?这就需要了解一个重要的元素:信号。OS 会发出信号使得进程状态改变,比如我们常用的 kill -9、ctrl + c 等等,那么 OS 是怎么识别这些信号的,下面我们来认识一下信号。
当信号还没产生时,我们都知道信号的结果,为什么,因为我们学习了,那操作系统怎么学习呢,在产生时我们得先识别出来。且信号产生的种类有很多种情况,进程的运行和信号的产生是属于一种异步关系,也就是说,信号产生的同时不会打搅进程的运行。
当信号产生时,不一定立马区处理信号,因为可能有更高优先级的事。当信号已经到来,暂时没有处理,一定要有某种方式记下来这个信号,等到“合适”的时候处理。
准备处理信号有3种接收方式:
1、默认行为(正常记录)
2、自定义行为(handler)
3、忽略信号(但忽略也是一种接收)
那么进程内部一定能识别信号,程序员设计进程时,已经内置了处理方案,信号属于进程内部特有的特征。
当输入指令 kill -l 时,我们会看到一个有 62 个信号,没有0、32、33,所以是 62 种,其中普通信号是 1 - 31,实时信号是 34 - 64。常用的是普通信号。
如何记录信号:
信号的记录是在进程的 PCB 中的结构体变量,本质更多是为了记录信号是否产生,细心的可以观察到 1 - 31 相对于一个整型的 32 个比特位,所以通常采用位图来记录,是一个 unsigned int 无符号整形。可以通过比特位的位置对应信号的编号,假设从右往左,第一位不算,下标是一的比特位对应的是一号信号,而比特位的内容,1 或者 0,代表是否收到信号。
所以,进程收到信号,本质是进程内的位图被修改了!只有谁资格修改进程的数据呢?OS!因为操作系统是进程的管理者,是软硬件系统的管理者。
信号是如何发送的:
本质是 OS 直接修改目标进程 task_struct 中的成员信号位图,信号只有 OS 有这发送,别的进程无法修改其他进程。
我们经常遇到的信号就算 ctrl + c,它是代表着 2 号信号,那么我们在键盘上敲出 ctrl + c 时,系统怎么识别呢?这里我们需要介绍两个函数:
signal(signo, handler); // 捕捉信号函数,signo 代表几号信号,handler 是一个函数指针,是一个回调函数; typedef void (*sighandler_t)(int)void handler(int signo) // 会工具 signal 函数自动传递 signo 的值

handler 函数是一种捕捉方法,通过映射方式,但有的信号不一定产生,它就像妈妈教孩子遇到小偷时,要大喊“捉小偷!”,相对于自定义了信号,当进程接收到改自定义信号时,就输出自定义后的内容。
而我们在 ctrl + c 后,就怕被产生一个硬件中断,被 OS 获取,包装成信号发送给进程,进程收到信号后退出。而后台进程,就是 ./exe文件 + & 让当前进程变为后台进程,后台进程无法通过键盘杀掉,只能通过指令。这里除了 ps axj 外还可以使用指令 jobs,可以查看后台进程,随后 fg + 进程号,可以把进程放到前台,就可以杀掉了。
需要注意的是,前提进程在运行过程中,用户随时可能按下 ctrl + c 而产生信号,就算说可以在如何地方获取,所以信号相对于进程的控制流是异步的。信号是进程之间时间异步通知的方式,属于软中断,键盘属于硬中断。
我们可以通过捕捉信号来查看哪些信号的可以捕捉的,哪些信号是不能捕捉的,比如 9 号信号,如果所有的信号都能被捕捉,那这个进程岂不是无敌了!!
void handler(int signo){ printf("got a signo : %d\n", signo); signal(signo, handler); }int main() { int i = 1; for (; i < 32; i++){ signal(i, handler); } waitpid(-1, NULL, 0); while (1){ ; } return 0; }

所以,为什么要有信号?因为有许多突发事情,要具有应对事件的能力。
产生信号:
代码运行时出错,我们该如何判断?
第一种:调试;
第二种:核心转储。
核心转储:是把进程在内存中的核心数据,转储到磁盘上,记录具体运行到哪,哪一行出错了等信息,一般叫 core.pid 核心转储文件,它的目的是为了更好的调式,定位到错误行。一般云服务器这类线上生产环境,默认关闭,虚拟机有。
怎么打开呢?输入命令:
ulimit -a // 显示系统目前资源的限定

其中 core file size 默认是 0,表示关闭状态,我们可以将核心转储打开,输入指令:
ulimit -c 10240

这时 core 会发生改变,完成打开。当我们运行含有错误的进程时,会显示(core dump),意思是生成 core 文件,这时我们就可以看到目录下多了一个 core.pid 文件,这个文件叫做核心转储文件。
当返回 core 文件后,我们可以用 gdb + 可执行程序调试,再输入 core-file core.pid 后,会自动定位到位置错误行上。这种调试方式叫做事后调试,代码实在找不到 bug 时可以打开 core。
Linux|【Linux】信号(1)认识、记录和产生信号
文章图片

还记得当时我们在讲进程中止时的 status 信息吗,其中第 8 位就算 core dump,它表示是否有核心转储。我们可以通过 status & 0x80,看到 core dump 位是 1。
发送信号的方式:
发送信号一共有四种方式:
1、通过键盘产生硬件信号
2、进程异常,通过软硬件产生信号
3、通过系统调用接口产生信号
4、软件条件产生信号
前两种在前面已经讲了,下面先讲第三种,系统调用。通过系统调用接口,也可以产生信号,通常使用的接口是:
int kill (int pid, int signo) // 自成一体,形成进程调用int main(int argc, int *argv[]) // 注意的是,这里调用 kill 需要带上main参数。 // 通常用法是 ./myproc pid 2// 可以采用辅助函数,提醒接口 viod usage (const char *proc)raise(int signo) // 给自己发信号abort() // 自己调用6号信号,随后会中止进程,没有返回值,和exit()不同,abort总是成功alarmc (second) // 闹钟信号,设定时间后返回14号信号

这里的 alarmc() 属于第四种软件条件,还包括下面要讲的,阻塞信号!
阻塞信号:
下面先来了解一下理论:
实际执行信号的处理动作称为信号递达;信号从产生到递达之间的状态叫未决;忽略也是一种处理方式,而阻塞与忽略不同,它是不要递达,知道解除阻塞,不是处理方式。
在接收信号时,不一定立即执行,所以的需要一个地方来保存信号,所以有了保存信号的数据结构,是一个叫做 pending 的位图,是 unsigned int 类型,发几就标记哪个位置,是接收未处理的信号的数据结构。
还有一个是 handler,在上面我们认识到 handler 是自定义信号,在这里我们可以看作是一个函数数组,每一个下标都是函数指针。其下标中对应了 pending 的处理方法,也可以自定义。
SIG_DFL:代表默认信号;
SIG_IGN:代表信号忽略;
最后一种就算自定义 handler;
此处未完更新中。。。

进程识别信号的必经之路 -- OS:
为什么进程会崩溃,因为收到了信号,那信号是怎么传递进来的,OS怎么知道出错了?
举两个例子,第一个是除 0 错误:
当 cpu 计算时,寄存器计算数值返回,cpu 发现有除 0 异常,就会给 cpu 中的状态寄存器发送信号,状态寄存器更改对应的标志位的值,因为 cpu 属于硬件,所以这是一种硬件错误。OS 发现了 cpu 中的状态寄存器发生变化,随后将这个硬件错误包装成信号发送给目标进程,这里本质是找到对应的 PCB 中信号的位图,随后置 1,进程就可以接收到异常信号了。
第二种情况:空指针等
进程通过页表映射到物理内存当中,当发现有人对 NULL 解引用时,会返回错误信息,页表是一种软件,还有一个硬件叫 MMU:是硬件单元 + 页表,做映射工作,在 MMU 中也有状态信息,OS 会看到这个信息发生变化,随后发送给进程。
总的来说,这些错误最终一定会在硬件层面上有所表现,进而被 OS 识别到。
【Linux|【Linux】信号(1)认识、记录和产生信号】

    推荐阅读