| |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
-> 系统运维 -> Linux信号详解 -> 正文阅读 |
|
[系统运维]Linux信号详解 |
目录 前言哈喽,小伙伴们大家好,本篇文章我将和大家一起学习进程信号。进程在某种意义上其实和我们人很像。在大多数情况下,我们的日常生活就是起床、上课、学习、休闲、休息,按部就班的度过每一天。但有时候也会遇到突发状况,比如生病啦,或者家里突然有事需要回家啦,这些突发状况是不可避免的,因为往往是事情推着人走的,我们要有处理突发状况的能力。进程也是如此,通常情况下按部就班的运行着,但保不齐什么时候就遇到突发状况,为了保证进程能在突发状况中做出相应的处理,信号由此产生。操作系统会根据突发状况给进程传递不同的信号,让进程做出相应的处理。看到这里,大家是不是感觉到了信号是一个非常有用的东西呢,那么事不宜迟,拿好小本本,和我一起开始信号的学习吧。 一、信号概念1、生活中的信号信号不仅仅存在计算机中,同样存在我们的日常生活中。在谈计算机中的信号前,我们先来聊聊生活中的信号。 在日常生活中,信号与我们的关系有如下特征:
2、计算机中的信号?在计算机中,进程就好比生活中的人,信号有以下特征:
可以看到,计算机中的信号和生活中的信号其实是很相似的。下面我们思考两个问题: 信号是如何记录的? 我们先来谈谈信号是如何记录的,在学习进程时候,我们应该了解了每个进程都对应一个task_struct(PCB),在这个task_struct中,记录着进程的各种信息,各种信息中同样也包括信号的记录。信号在task_struct中是以位图的方式记录的,task_struct有变量signal,可以把它的类型理解成无符号整数。比特位的位置为信号编号,比特位的内容为是否收到信号,假如收到6号信号就会把第六个比特位置1。我们可以通过下面的命令查看信号编号。
?信号是如何发送的? 进程收到信号,本质上是进程的信号位图被改了。那么谁有权限改进程内的东西呢?答案当然是OS,操作系统是进程的管理者,拥有绝对的权限。所以信号发送的本质就是操作系统修改了进程的信号位图,进程根据修改后位图的值做出相应处理。 3、实现信号捕捉signal函数
代码如下: ?代码运行结果如下,ctrl+c对应的是信号2,终止进程。我们可以发现信号2被我们自定义了。 注意:有些信号是不能被捕捉的,因为如果所有的信号都被捕捉,那么操作系统就再也没有办法杀死进程了。 二、产生信号的方式1. 通过终端按键产生信号通过终端按键也就是通过键盘产生信号,比如我们常用的ctrl+c。ctrl+/。 注意:
格外拓展:核心转储(core dump)
从上面我们可以看出:SIGINT的默认处理动作是终止进程,SIGQUIT的默认处理动作是终止进程并且Core Dump,接下来我们来验证一下。 ?再验证之前我们先来学习一个概念:Core Dump
核心转储功能一般在云服务器,线上生产时是关闭的。
可以发现core文件大小为0。想要使用此功能,我们首先要改一下core文件大小。 ?修改后运行进程,然后发送信号让它异常终止: 可以发现在发送信号后进行了(core dumped),并且多出了一个core.6229文件。我们使用gdb调试myproc,再查看core文件,可以得到进程异常终止信息。这种方法也适用于代码中内存越界,除0等错误,可以快速定位到第几行。 status?大家是否还记得在进程等待的时候有一个status,被信号所杀时候,其中第低八位就是core dump标志,如果发生了core dump,则该位为1,否则为0。 2、调用系统函数向进程发信号kill函数
kill命令是调用kill函数实现的。kill函数可以给一个指定的进程发送指定的信号。
我们也可以尝试自己来写一个kill命令。代码如下:?
raise函数 raise函数可以给当前进程发送指定的信号(自己给自己发信号)。
abort函数
3. 由软件条件产生信号alarm函数
alarm的返回值是0或者是剩余秒数。如果闹钟被提前唤醒,返回值为剩余秒数,否则是0。 写一个计数程序,看看cou一秒钟可以计多少次数。 结果如下:? ?4、硬件异常产生信号在代码中有野指针,除0等操作时程序会出现异常,这时候会产生信号,然后程序崩溃。程序崩溃的本质就是收到了信号。 下面对这个过程进行具体解释:以除0操作为例,我们知道计算都是在cpu中的,cpu中有一个状态寄存器,当进行除0操作后,状态寄存器会异常。os是软硬件的管理者,当检测到cpu状态异常后,会定位到相应的进程,更改进程的信号位图,进程通过位图识别到信号发生崩溃。 再比如当前进程访问了非法内存地址,负责虚拟地址与物理内存的一个硬件MMU会产生异常,之后和上述过程一样。 三、阻塞信号1、概念
?2、内核中的表示
sigset_t类型 从上图来看,每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。 3、信号集操作函数3.1用户层函数sigset_t每一位都代表一个信号的“有效”或“无效”状态,至于sigset_t内部是如何存储这些数据的我们不需要关心,是由操作系统去维护的。所以我们想要操作sigset_t变量要通过下面的这些函数,而不应该对内部数据有任何操作,因为这是没有意义的。
sigismember是一个bool函数,用来判断某个信号集中是否包含某个有效信号,包含返回1,不包含返回0。 3.2 系统接口我们上面定义的sigset_t类型变量是在栈中的,本质上是在用户层,没有进操作系统。我们对它的操作仅仅是改变这个变量的值,并不会影响进程的任何行为。我们想借助sigset_t类型变量影响系统和进程需要调用下面的接口。 sigprocmask函数
如果oset是非空指针,则读取进程的当前信号屏蔽字通过oset参数传出。如果set是非空指针,则 更改进程的信号屏蔽字,参数how指示如何更改。假设当前的信号屏蔽字为mask,下表说明了how参数的可选值。 sigpending函数
下面我们写一个进程来完成以下操作:
代码如下:
四、信号捕获1、用户态和内核态?用户态:执行用户自己的代码,系统所处的状态叫做用户态。用户态是一种受监管的普通状态。 内核态:有时候我们写的代码中,调用了系统接口,本质上就是调用了内核级的代码,这时候就需要内核态权限。内核态通常用来执行os代码,是一种权限非常高的状态。 用户态->内核态:系统调用,时间片到了导致进程切换,异常,中断,陷阱,这些情况会切换到内核态。 内核态->用户态:系统调用,进程切换,异常,中断,陷阱处理完毕后,会切换回用户态。 在用户态时,访问用户的代码和数据,切换到内核态后,比如调用系统接口后,往往会执行操作系统内核中的代码。那么进程是怎么找到操作系统的呢?首先要明确一个概念,操作系统也是一款软件,既然是软件运行的时候就会被加载到内存中。每个进程都有一张地址空间表,这张表下半部分为用户区,通过用户页表映射找到内存中用户的代码和数据。上方为内核空间,保存了内核的虚拟地址,可以通过内核页表映射找到内存中操作系统内核的代码和数据。 进程无论如何切换,都能看到操作系统,但不一定能访问,只有处在内核态时才能够访问。那么处于内核态还是用户态的标志是什么呢?我们知道代码是加载到cpu中进行运算的,在cpu中有一个cr3寄存器,里面记录了是否此时的状态是用户态还是内核态。同时cpu中还有寄存器记录着用户页表和内核页表的值,当在某种状态下访问越界,cpu可以检索到。 2、内核如何捕获进程信号2.1捕获信号过程信号递达时处理的动作为用户自己定义的函数,这称为捕捉信号。 通过前文的学习我们知道,进程在收到信号后不一定是立刻处理的,而是等到适合的时候。那么什么是适合的时候呢?答案就是系统从用户态切到内核态后。 假设用户程序注册了信号2的处理函数sighandler。下面我来阐述捕获进程信号的过程:
思考一个问题:在内核态是否能调用用户的代码和数据呢? 理论上以内核的权限是可以的,但实际上并不能这么做,因为操作系统不信任任何人,它担心用户会越权执行一些非法操作。 2.2 sigaction
act和oact指向sigaction结构体:? sa_handler为我们自己定义的处理函数。当某个信号的处理函数被调用时,内核会自动将该信号加入当前进程的阻塞信号集,等调用结束后再恢复。这就保证了在处理某个信号时,如果这种信号再次产生,会被阻塞到当前信号处理完毕为止。如果在调用信号处理函数时,除了阻塞当前信号外,还想阻塞其它信号,则用sa_mask设置,调用完毕后自动恢复。sa_flags涉及一些选项,而siginfo是实时信号的处理函数,本章不做解释。 测试代码如下:
五、函数的重入我们来看下面的过程: mian函数正在调用insert函数向链表中插入节点。insert函数分为两步,刚刚执行完第一步时此时硬件发生中断,使进程切换到内核。中断处理完毕切换到用户态之前发现有信号未决,于是进入了信号的处理函数,信号的处理函数中同样有insert操作,于是向链表中插入了一个新节点。进行完毕后返回到main函数,按照上下文接着执行main函数中insert函数的第二步,令头节点指向node1。从图中可以看出,这时候出现了一个问题,明明我们想要两个节点插入,可实际仅仅插入了一个,node2丢失了,也就是我们平常说的内存泄漏。 ?像上例这样,insert函数被不同的控制流调用,在第一次调用还没有返回时就再次进入该函数,这称为重入。如果insert访问的是一个全局链表,就可能因为重入而产生错误,这称为不可重入函数。相反,如果一个函数只访问自己的参数和局部变量,则称为可重入函数。 如果一个函数符合以下条件之一则是不可重入的:
我们平常所调用的函数,自己写的函数,STL中的库函数基本都是不可重入的。 六、c语言关键字volatilevolatile是c语言中的一个关键字,在语言层面我们很难理解它,今天从信号角度我们来理解一下。 我们来看下面一段代码: 从代码表面看上去运行逻辑应该是,flag为0时候一直while循环,当有信号2产生进入到信号2的处理函数中,flag改成1,循环结束,进程退出。但是事实真的是如此吗? 我们用编译器O3的优化级别来编译(默认是O1或O2),然后运行,结果如下: 我们发现收到信号后进程并没有退出。原因是main函数和和信号处理函数是两个执行流,while循环在main函数中,编译器只会检查main函数,发现flag并没有main函数中改变。在优化级别较高的时候,flag会被直接写进cpu的寄存器中,cpu读取flag时直接从寄存器里读,信号处理函数改变flag改变的是flag在内存中的值,寄存器中flag的值始终保持0。 为了解决这个问题,我们可以在定义flag的时候前面加上volatile。
再次编译运行,发现可以退出: volatile的作用: 避免变量被写到寄存器中,就算写到内存器中,读取时也要先读取内存中的值,然后刷新到寄存器里。? 总结本文主要介绍了linux系统下信号的相关知识,希望能给大家带来帮助。如果觉得写的还不错的话可以点赞支持一下博主,我也将努力为大家带来更加优质的内容。感谢阅读,山高路远,来日方长,我们下次见。 |
|
|
上一篇文章 下一篇文章 查看所有文章 |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年12日历 | -2024/12/28 19:47:23- |
|
网站联系: qq:121756557 email:121756557@qq.com IT数码 |
数据统计 |