linux异步信号handle浅析

在初学linux编程的时候,一直觉得异步信号handle是个很神奇的东西,用户程序可以使用singal之类的系统调用为某某信号注册一个信号处理函数(handle函数)。
程序的二进制代码在内存中都有着确定的执行流程,为什么收到异步信号以后,程序会被“中断”,然后跳转到这个handle函数里面去运行呢?内核怎么有能力让程序做这样的跳转呢,总不可能临时修改程序的可执行代码吧?

后来学习了一些内核知识,才知道原来进程收到信号以后,并不是立即就被“中断”的,而是先在进程的控制结构(task_struct)中记录下收到了某某信号,然后等到进程即将从内核态返回用户态的时候,流程才被“中断”,handle函数才被调用。
用户进程什么时候会从内核态返回用户态呢?一般主要是三种情况:系统调用(用户进程主动进入内核)、中断(用户进程被动进入内核)、被调度执行(用户进程从等待执行变为正在执行)。
进程从收到信号到它从内核态返回用户态的过程,是需要一定时间的。但是这个时间一般会很短,至少时钟中断会以较大的频率(比如1毫秒一次)将用户进程带入内核(当然,只针对正在执行的进程)。

在进程即将从内核态返回用户态时,如果有信号需要处理,对应的handle函数将被调用(当然,可能没有注册handle,这时内核对信号进行默认的处理)。注意,现在进程还在内核态,内核是怎么调用用户态的handle函数的呢?
直接调用可以吗?当然不行。内核代码运行在高CPU特权级别下,如果直接调用handle函数,则handle函数也将在相同的CPU特权下被执行。那么用户将可以在handle函数里面为所欲为。
所以,调用handle必须先返回用户态。但是返回用户态后,程序流程又不受内核控制了,难不成内核还真的把用户进程的可执行代码临时改掉?

内核实际的做法还是比较巧妙。用户进程进入内核以后,都会在其对应的内核栈上留下返回地址,以便流程返回。内核调用handle函数的办法就是临时改掉栈上的返回地址,然后按原有的返回用户态的流程去返回。结果这一返回,就到了handle函数去了。(当然,需要修改的并不止是返回地址,而是一整个调用栈。)
虽然现在临时把返回地址改了,但是用户进程最终还是要返回到原先那个返回地址去的。那么,原先的返回地址及其调用栈应该保存在哪里呢?进程的内核栈空间有限,并且还需要应付handle函数中可能发生的系统调用,所以内核把这些信息放在内核栈上是不现实的,只能压到了用户栈上去。

当handle函数执行完毕,执行流程要返回到内核去。同样,由于CPU特权级别不同,从handle函数返回内核时不能单纯地利用RET指令去返回的。需要执行一次系统调用。

在handle执行完后,为什么要回到内核,再从内核返回到原始返回地址呢?如果直接返回到原始的返回地址那自然是很便捷。并且要这么做也不难,原始返回地址及其调用栈已经被压到了用户栈上,内核只需要在handle函数的调用栈上稍做手脚就行了。
1、返回到原始返回地址并不是回到那个地址就行了,需要把整个现场都恢复(主要是寄存器什么的)。当然,内核也可以在用户栈上面压一些代码,来完成这些事情;
2、现在可能不止一个信号要处理,最好让用户进程返回内核,继续处理其他信号;

为了返回内核,首先,内核在返回到handle函数之前,先将某个返回地址压到用户栈上,以便从handle返回时能够返回到指定的地址上。这个指定的地址其实也在进程的用户栈上,内核又在这个地址上放了几条指令(在栈上放置可执行代码),让进程去调用一个名叫sigreturn的系统调用。

返回到handle函数前的用户栈大致如下:
原有数据 -> 调用sigreturn的指令(设其地址为a) -> 原始返回地址及其调用栈 -> 返回地址(值为a) -> handle的栈变量

内核在handle函数的调用栈上放置sigreturn指令,这是在linux 2.4时的做法。每次调用用户的handle函数都需要向用户栈拷贝这么几条指令,这并不太好。
linux 2.6有一个叫vsyscall page的页面,上面包含了内核为用户程序准备的一些指令,其中就包括调用sigreturn指令。这个vsyscall页被映射到每个进程的虚拟地址空间靠近末尾的部分,被所有用户进程共享,对于用户进程是只读的。这样,handle函数的调用栈上就不需要再塞入sigreturn指令了,直接将handle函数的返回地址设为vsyscall页中对应的代码即可。

为了让handle执行完以后自动调用sigreturn返回内核,内核做了很多事情。那么可不可以约定好,让用户自己去调用sigreturn呢?
当然,这是可以的。只是为了让信号处理机制成为一套完整的机制,内核并没有这么做。否则用户在handle函数里面忘记调用sigreturn的话,可能莫名其妙地进程就崩溃了。而编译器也很难找出这样的错误。

进程调用sigreturn系统调用重新进入内核后,压在用户栈上的原始返回地址及其调用栈被获取。最终内核又会修改栈,让进程返回用户空间时返回到这个原始返回地址上。

时间: 2024-09-20 07:53:18

linux异步信号handle浅析的相关文章

LINUX异步信号集合示例代码

主要以sigemptyset(),sigaddset()函数为基础查看信号集的存储结构. 用于进程间的异步通信. 1 #include <signal.h> 2 #include <stdio.h> 3 #include <stdlib.h> 4 5 int output(sigset_t set); 6 7 int main() 8 { 9 sigset_t set; 10 printf("after empty the set:\n"); 11

linux可重入、异步信号安全和线程安全

一 可重入函数 当一个被捕获的信号被一个进程处理时,进程执行的普通的指令序列会被一个信号处理器暂时地中断.它首先执行该信号处理程序中的指令.如果从信号处理程序返回(例如没有调用exit或longjmp),则继续执行在捕获到信号时进程正在执行的正常指令序列(这和当一个硬件中断发生是所发生的事情相似.)但是在信号处理器里,我们并不知道当信号被捕获时进程正在执行哪里的代码. 如果进程正使用malloc在它的堆上分配额外的内存,而此时由于捕捉到信号而插入执行该信号处理程序,其中又调用了malloc,这会

LINUX 中的mmap浅析

原创LINUX系统编程水平有限,参考UNIX系统编程手册 LINUX 中的mmap浅析 一.mmap基本原理和分类 在LINUX中我们可以使用mmap用来在进程虚拟地址空间中分配创建一片虚拟内存地址映射 其可以是 1.文件映射    使用文件内容初始化内存 2.匿名映射    初始化全为0的内存空间(calloc也可以) 下面配图来自UNIX系统编程手册 而对于是否共享又分为 1.私有映射(MAP_PRIVATE)    多进程间数据共享,修改不反应到磁盘实际文件,    私有写时复制实现 2.

arm驱动linux异步通知与异步IO【转】

  转自:http://blog.csdn.net/chinazhangzhong123/article/details/51638793 <[ arm驱动] linux异步通知与 异步IO>涉及内核驱动函数二个,内核结构体一个,分析了内核驱动函数二个:可参考的相关应用程序模板或内核驱动模板二个,可参考的相关应用程序模板或内核驱动三个 描述:设备文件IO访问:阻塞与非阻塞io访问,poll函数提供较好的解决设备访问的机制,但是如果有了异步通知整套机制就更加完整了 一.阻塞 I/O,非阻塞IO,

Linux异步IO【转】

  转自:http://blog.chinaunix.net/uid-24567872-id-87676.html Linux 中最常用的输入/输出(I/O)模型是同步 I/O.在这个模型中,当请求发出之后,应用程序就会阻塞,直到请求满足为止.这是很好的一种解决方案,因为调用应用程序在等待 I/O 请求完成时不需要使用任何中央处理单元(CPU).但是在某些情况中,I/O 请求可能需要与其他进程产生交叠.可移植操作系统接口(POSIX)异步 I/O(AIO)应用程序接口(API)就提供了这种功能.

Linux进程间通信——信号集函数

一.什么是信号 用过Windows的我们都知道,当我们无法正常结束一个程序时,可以用任务管理器强制结束这个进程,但这其实是怎么实现的呢?同样的功能在Linux上是通过生成信号和捕获信号来实现的,运行中的进程捕获到这个信号然后作出一定的操作并最终被终止.   信号是UNIX和Linux系统响应某些条件而产生的一个事件,接收到该信号的进程会相应地采取一些行动.通常信号是由一个错误产生的.但它们还可以作为进程间通信或修改行为的一种方式,明确地由一个进程发送给另一个进程.一个信号的产生叫生成,接收到一个

linux异步IO浅析

知道异步IO已经很久了,但是直到最近,才真正用它来解决一下实际问题(在一个CPU密集型的应用中,有一些需要处理的数据可能放在磁盘上.预先知道这些数据的位置,所以预先发起异步IO读请求.等到真正需要用到这些数据的时候,再等待异步IO完成.使用了异步IO,在发起IO请求到实际使用数据这段时间内,程序还可以继续做其他事情). 假此机会,也顺便研究了一下linux下的异步IO的实现. linux下主要有两套异步IO,一套是由glibc实现的(以下称之为glibc版本).一套是由linux内核实现,并由l

Linux异步信号处理函数引发的死锁及解决方法

死锁的发生 自己所在的团队在开发新版本过程中,一次测试环境发生了server死锁,整个server的任务线程都被hang住.而死锁的代码就在我负责的程序日志部分中localtime_r函数调用处. 程序日记需要记录打印日志的时间,而localtime_r函数就是用于将系统时间转换为本地时间.同样功能的函数还有localtime.两个函数的区别是:localtime_r是thread-safe,其返回的结果存在由用户提供的buffer中:而localtime返回的结果是指向static变量,多线程

linux IPv4报文处理浅析

在<linux网络报文接收发送浅析>一文中介绍了数据链路层关于网络报文的处理. 对于接收到的报文,如果不被丢弃.不被网桥转发,会调用netif_receive_skb()提交给IP层: 而对于IP层向外发送的报文,则通过调用dev_queue_xmit()提交给数据链路层. 本文就以netif_receive_skb()和dev_queue_xmit()为起始,简要介绍一下报文在IP层的处理过程. 先来一张图: 报文接收(图中橙色箭头所指) netif_receive_skb()对每一种已注册