背景:内核刚刚处理完中断和异常.现在它要返回到@current进程了.然而,在返回以前,内核总会习惯性检查一下@current的TIF_SIGPENDING标志,以便确定是否有尚未处理的信号.很不幸,确实有信号正在挂起队列上排队.于是,内核开始启动do_signal()函数…
一. do_signal()函数
遍历@current的挂起信号队列:
1.如果信号的传递方式是SIG_IGN,则忽略这个信号;
2.如果信号的传递方式是SIG_DFL,则根据具体的信号执行相应的默认操作;
3.如果信号有一个处理函数,终止遍历,执行这个信号处理函数;
4.如果信号是0,则检查并处理系统调用的重新执行.
信号处理函数或系统调用的重新执行会返回到内核.紧接着,当内核尝试恢复原程序的执行时,又会陷入do_signal()函数.最终结果是,do_signal()将处理挂起信号队列中的每一个信号.
1.参数
- regs 栈,current在用户态下寄存器的内容存于此处
- oldset 阻塞信号的位掩码数组(已被删除)
2.说明
1. 通常只在CPU返回到用户态时才调用此函数:TIF_SIGPENDING标志的检查总是在内核准备返回到用户态时进行.
2. 反复调用dequeue_signal()直到pending和shared_pending队列为空
3. 调用栈
- do_signal()
在返回到用户态前,处理@current的每一个未阻塞的挂起信号.- get_signal_to_deliver()
遍历挂起信号队列,自行忽略信号或为信号执行默认操作. - handle_signal()
为执行信号处理程序做准备.- setup_rt_frame()
复制,修改@current的硬件上下文.
- setup_rt_frame()
- get_signal_to_deliver()
3.复杂性
- 竞争条件,冻结系统,产生内存信息转储,停止/杀死整个线程组
- 中断处理程序调用此函数(可能性?)
- 当current正受到其他进程监控的时候怎么办?
do_notify_parent_cldstop()和schedule() - 待处理的信号是一个被忽略的信号(可能性?)
- 待处理西信号需要被执行缺省操作
- 待处理信号有一个信号处理函数
二.get_signal_to_deliver()函数
这个函数遍历挂起信号队列,处理被显式忽略的信号并并为具体信号执行相应的默认操作.如果遇到信号0(处理系统调用的重新执行)或者遇到注册了信号处理程序的信号,则终止遍历,返回到do_signal()
1.函数的执行过程
- try_to_freeze()
linux冻结系统在信号系统中的钩子函数.linux冻结系统利用信号系统完成自己的功能. - if(signal->flags & SIGNAL_CLD_MASK)
每一个停止的进程在苏醒后都会运行这个检查.在这里,我们检查一下是不是需要通知@current的父进程 - 陷入一个无限循环
- 在循环的开始,检查是不是需要停止整个线程组.
- 从挂起队列上摘下一个信号
- 如果信号是0,则返回0
- 如果信号被显式忽略,continue
- 如果信号有一个处理程序,终止循环,返回信号ID
- 执行默认操作.为具体的信号执行相应的信号处理程序
2.信号的缺省操作
- 当接收进程是init()时,丢弃信号;
- 当信号是SIGCONT, SIGCHLD, SIGWINCH, SIGURG时,忽略信号;
- 当信号是SIGSTOP, SIGTSTP, SIGTTIN, SIGTTOU时,停止整个线程组;
- 缺省操作:Dump
- 缺省操作: Terminate: do_group_exit()(组退出)
do_signal_stop()
组停止P440
三.handle_signal()函数
信号处理程序必须在用户态执行:
原程序– –信号处理程序— –信号处理程序– –原程序
|1(中断) |2 |3(系统调用) |4 |5 |
–内核态产生信号– –系统调用—- –内核–
多次状态切换引起复杂性:
- 内核态向用户态向用户态切换时,内核态堆栈被清空.因此,第2次特权级切换以后,原程序的硬件上下文丢失
- 信号处理程序调用系统调用后,第4次特却级切换时,内核必须返回到信号处理程序而非原程序信号的处理过程回顾
一个非阻塞的信号被发送给一个进程.当中断或异常发生时,进程切换到内核态.正要返回到用户态前(return_from_intr),内核执行do_signal()函数,这个函数从get_signal_to_deliver()得到一个注册信号.于是,handle_signal()函数被调用来为这个注册信号的处理搭建环境.
当进程又切换回用户态时,因为信号处理程序的起始地址已经被放进程序计数器中, 因此开始执行信号处理程序.当信号处理程序终止时, setup_frame()函数放在用户态堆栈中的返回代码就被执行.这个代码调用sigreturn()系统调用,相应的服务例程把原程序的用户态堆栈的硬件上下文复制到内核态堆栈,并把用户态堆栈恢复到它原来的状态(restore_sigcontext()).当这个系统调用结束时,普通进程就因此能恢复自己的执行.
1.背景
CPU处在内核态,即将返回到用户态.但是,此时捕捉到了一个注册信号.内核必须做些什么,以保证:
- 内核不会返回到原程序,而是返回到信号处理程序;
- 信号处理程序执行结束后必须返回内核;
- 从内核再返回到原程序的时候,原程序的硬件上下文不能丢失;
思考一: 在中断或异常发生的时候,程序是如何陷入内核的?
当执行了一条指令后,cs和eip这对寄存器包含下一条将要执行的指令的逻辑地址.在处理那条指令前,控制单元会检查是不是已经发生了一个中断或异常.如果有的话,控制器就会依次执行下列步骤:
1.确定中断向量;
2.访问IDT,找到与中断向量对应的中断门或陷阱门;
3.根据IDT中的门描述符,借助GDT,找到中断或异常处理程序的逻辑地址.
4.进行特权级检查.要求CPL大于等于中断处理程序逻辑地址的DPL,即引起中断程序的特权必须低于或等于中断处理程序的特权;
5.如果CPL与DPL不同,这通常意味着在用户态请求内核态的中断或用户处理.此时,利用TSS段把栈切换到内核态.
注意,上述过程是具体于Linux的.ULK145的叙述则不针对任何具体的操作系统,是单纯的描述Intel的硬件处理过程,因此叙述过程始终没有出现”内核态”和”用户态”等具体于操作系统的术语,十分严谨.思考二: 中断或异常发生的时候,用户态进程的硬件上下文需要保存吗?保存于何处?
不管是中断还是异常,用户态进程的硬件上下文都会保存在当前的堆栈中.由于在保存以前已经进行了硬件处理,所当前的堆栈通常是内核栈.具体的:
@error_code的第一步就是”把高级C函数可能用到的寄存器保存在栈中”;
@common_interrupt的第一步就是”SAVE_ALL”思考三: do_signal()结束以后,如何返回到信号处理程序而不是原程序?
只需要对保存的硬件上下文做一些修改即可.思考四: 当信号处理程序恢复执行的时候,如何保证原程序的硬件上下文不会丢失?
原程序的硬件上下文会被复制两份.一份压入用户态堆栈中保存起来.一份稍作修改以后,用作信号处理程序的硬件上下文.思考五: 信号处理程序结束以后如何返回到内核?
信号处理程序被调用的时候,它的返回地址(通常就是下一条指令的地址)会被压栈保存(是吗?).信号处理程序在它的返回地址之上建立自己的栈.
当信号处理程序执行结束以后,它的返回地址从栈中弹出.CPU开始从这个地址处继续执行因此,为了让信号处理程序结束以后返回到内核,只需要修改信号处理程序所使用的堆栈即可.
2.这个函数做了什么?
- 将原程序的硬件上下文复制成两份:
setup_sigcontext(&frame->sc, fpstate, regs, set->sig[0])- 其中一份保存在帧中,并随帧一起压入用户态堆栈,以备将来恢复原程序的执行.恢复原程序执行的任务由sigreturn()系统调用完成,信号处理程序会通过pretcode返回到这个系统调用.
- 另一份上下文(regs)作为信号处理程序的硬件上下文使用.handle_signal()会修改这份硬件上下文,将上下文中的返回地址改为信号处理程序的地址,并设置正确的栈顶地址;
- 把帧放在信号处理程序的下面.帧的顶部存放着sigreturn()系统调用的入口地址,这样当信号处理程序返回时,这个入口地址会被当作返回地址使用,于是系统陷入sigreturn系统调用.
- 检查信号标志.在执行信号的时候,新来的信号是要被阻塞的.哪些信号要被阻塞呢?
- 进程描述符里面规定要阻塞的信号是要被阻塞的: current->blocked
- 这个信号处理函数规定要阻塞的信号,是要被阻塞的: ka->->sa.sa_mask
- 当前信号,是要被阻塞的: sig
3.帧(sigframe)
- pretcode
- 这个地址被放在帧的最顶部,由于内存中栈是倒着用的,所以以内存的角度叙述的话,这个地址也是帧的起始地址.帧被”悄悄地”放在信号处理程序的下面,因此pretcode会被当作信号处理程序的返回地址使用. sig
- 信号编号,这是信号处理程序所需要的参数. sc
- 用户态进程的上下文,这个上下文是原程序第一次陷入内核的时候从用户态堆栈复制而来的.现在,这个上下文又被内核放入帧中,随帧一起压入用户态堆栈. fpstate
- 用户态进程的浮点寄存器内容.这个也算是硬件上下文的一部分把? extramask
- 被阻塞的实时信号的位数组.这是位数组,这里面都是实时信号,这些实时信号都被阻塞了.问题是,这个帧域有什么用? retcode
- sigreturn()系统调用的8字节代码.已不再使用.
关于函数调用时栈状态的细节,可以百度一下x86上的函数调用.
4.set_frame()函数
计算帧在用户态堆栈的起始地址,然后认真填入帧的每一个字段.特别地,原程序的硬件上下文会在这个过程中被复制进用户态对战.
修改依然留在内核态堆栈中的硬件上下文.在这个过程中,esp和eip会被修改为正确的值.esp指向帧的起始地址,eip指向信号处理程序的起始地址.
- 参数
- sig: 信号ID
- ka: k_sigaction表
- oldset: 阻塞信号掩码
- regs: 内核态堆栈中的用户态硬件上下文的地址
至此,handle_signal()返回到do_signal(), do_signal()也立即返回.do_signal()返回时,当前进程恢复它在用户态的执行.而由于setup_frame()函数已经偷天换日,所以eip寄存器指向了信号处理程序的第一条指令,esp寄存器已压入用户栈顶.所以,信号处理程序开始执行.
当信号处理程序执行结束时,返回到栈顶地址,也就是pretcode.pretcode会稍作准备,然后陷入sigreturn系统调用,这个系统调用结束时,CPU控制权返回到原程序.
四.返回到原程序
信号处理程序结束时,返回栈顶地址.栈顶地址指向帧的pretcode字段所引用的vsyscall页中的代码,这段代码发出0x80中断,开始调用sigreturn()系统调用.
首先,sys_sigreturn()首先找到帧在用户态堆栈的地址,这可以通过esp字段轻易完成.
然后,恢复current->blocked字段.何为恢复?刚才,为了执行信号处理程序,我们修改了current->blocked字段,强迫它吞并了信号处理程序的阻塞位和所处理信号的阻塞位.现在,我们要将这个字段恢复到以前的状态.这样,为信号处理函数执行而屏蔽的所有信号被解除阻塞
接下来,我们重新调用recalc_sigpending()函数,.如果有新的阻塞信号,我们就传递它们.这里,我认为,所谓”阻塞信号”指的是,信号可以产生,但是不会被传递.解除阻塞意味着信号终于可以被传递了.
最后,我们要访问帧的sc字段,这个字段指向原程序在用户栈中的硬件上下文,我们把这个上下文拷贝到内核栈.所有这一切交给一个函数来完成:restore_sigcontext(),这个函数还会将用户栈中的硬件上下文删除.
五. 系统调用的重新执行
有的时候,进程通过系统调用向内核请求服务,比如读写一个文件.然而,内核并不能总是满足进程的请求,此时,内核将这个进程置为TASK_INTERRUPTIBLE或TASK_UNINTERRUPTIBLE状态.
问题来了,如果进程由于不能完成系统调用而处在I或UI状态下,一个信号产生在这个进程上,会发生什么呢?
先来说明处在I状态的进程把.如果一个进程处在I状态,并且收到一个信号.那么内核不会等待系统调用完成,而是直接将进程置为R态.进程苏醒后,切换会用户态,此时信号被传递给进程.当这种情况发生的时候,系统调用没有完成它的工作,系统调用历程会向内核返回一个错误码.注意,用户进程并不会收到这个错误码,用户进程获得的唯一错误吗是EINTER,这个错误码告诉用户进程系统调用没有执行完.
让我们回到内核错误码上来.内核掌握着两点关键信息:
- 从系统调用服务历程返回的错误码
- 用户进程对信号的传递方式.
根据这两点信息,内核从如下动作中选择一个:
- 不重新执行系统调用;
- 重新执行系统调用;
- 根据SA_RESTART标志的值决定是否重新执行系统调用.
上述这一切的前提是,进程因执行系统调用失败而挂起.那么,内核是怎么知道进程挂起的原因是系统调用执行失败的呢?这就是regs硬件上下文的orig_eax字段发挥作用的地方了.P446
系统调用的重新执行需要分情况讨论:
- 系统调用被未捕获的信号中断
- 系统调用被捕获的信号中断
这是自然的,因为前者不需要执行信号处理程序,后者则需要执行信号处理程序.
1.系统调用被未捕获的信号中断
在这种情况下,do_signal()修改regs硬件上下文,让eip指向int $0x80或者sysenter指令.这样,当原程重新开始执行的时候,它会直接重新开始执行系统调用.
有一种特殊情况,eax中存放的是restart_syscall()的系统调用号,系统调用服务例程返回RESTART_RESTARTBLOCK,这个错误代码仅仅用于与时间有关的系统调用.为什么?如果一个系统调用要求进程睡眠20ms,10ms后进程被信号中断,如果重新执行这个系统调用,那么进程最终会睡眠30ms.
怎么解决这个问题呢?这种情况发生时,内核不会忠实地完全重新执行系统调用.当这种情况发生时,正在执行系统调用服务历程的内核会将一个特别定制的系统调用服务历程的地值放在thread_info的restart_block字段,并在返回错误码-ERESTART_RESTARTBLOCK.这样,sys_restart_syscall()服务例程只执行这个特别定制的函数.
2.系统调用被捕获的信号中断.
梳理思路.进程因系统调用失败而挂起.此时捕获到一个信号,进程被唤醒,并从系统调用服务例程返回,注意,应该不会回到用户态,而是直接处理信号.
handle_signal()函数会根据内核收到的出错码和sigaction表的SA_RESTART标志来决定是否必须重新执行未完成的系统调用.
如果系统调用需要重新执行,那么,handle_signal()会修改eip:
regs->ip -= 2; //重新指向刚才的系统调用指令然后继续完成handle_signal()剩余的工作,即更新内核栈与用户栈,为信号处理程序搭建环境.注意,在setup_frame()的时候,被复制进帧中保存起来的@ip是修改(-2)以后的ip,这个ip指向刚刚执行的系统调用指令.
这样do_signal()在handle_handle()后紧接着返回用户态执行信号处理程序.信号处理程序执行结束以后,通过sigreturn()系统调用陷入内核,当sigreturn()再次返回用户态时,原程序的系统调用会被重新执行.用户程序紧接着陷入内核执行系统调用,不会执行信号处理程序.
否则,系统调用不需要重新执行.这时候,handle_signal()会在@reg->ax中放入EINTR以向原程序返回错误代码.然后继续进行andle_signal()接下来的工作.即更新内核栈与用户栈,为信号处理程序的执行搭建环境.