By cszhao1980
下面来看一下信号的处理过程。
首先,进程需要不时的检查自己是否收到signal,此时需要调用issig()函数,该函数会返回
进程此时收到的signal的signal type(如果没有signal,或忽略此signal,则返回0):
3991: issig()
3992: {
3993: register n;
3994: register struct proc *p;
3995:
3996: p = u.u_procp;
3997: if(n = p->p_sig) {
3998: if (p->p_flag&STRC) { /进程处于trace模式,以后再详细解释
3999: stop();
4000: if ((n = p->p_sig) == 0)
4001: return(0);
4002: }
4003: if((u.u_signal[n]&1) == 0) /偶数表示,不可忽略此信号,故返回signal type
4004: return(n);
4005: }
4006: return(0);
4007: }
当确定有收到信号后,会调用psig()进行处理:
(1) 如果有指定信号处理程序地址,则执行之;
(2) 否则,结束进程。
听起来很简单,但令人深感意外的是,pisg()程序比想象中复杂的多。
这是“信号处理程序”身处user空间,而目前,进程在kernel态。
在讲解之前,首先补充一点知识:psig()函数只被中断/陷入处理程序调用,
因此,如果调用到了该函数,证明此时肯定发生了一次中断/陷入。
4055: n = u.u_ar0[R6] - 4;
4057: suword(n+2, u.u_ar0[RPS]);
4058: suword(n, u.u_ar0[R7]
这三条语句直接操纵user stack,在user stack中插入“中断/陷入”时的user PS和pc;
4059: u.u_ar0[R6] = n; /user态的r6(sp),使其指向新栈顶
4060: u.u_ar0[RPS] =& ~TBIT; /修改user态的PS
4061: u.u_ar0[R7] = p; /修改user态r7(pc),使其指向“信号处理程序”
4062: return; /return回 “中断/陷入”程序入口call中,然后rtt
/即跳回到user态,而前三条语句造成的register的改变
/将生效,即会马上执行“信号处理程序”
而信号处理程序执行完毕后,return时,会跳回到栈顶执行的地址——而这个地址已被我们更新
为“中断/陷入”前的地址,以恢复被中断的程序。巧妙的设计!
刚才的说明忽略了第4056行:“4056 grow(n)”——它的存在是为了处理一种概率很低的情形,即user
stack空间已耗尽,无法容纳要插入的PS和pc的情景。
grow()函数会检查user stack segment,如果空间不足,就会扩展stack segment空间。理解grow()的关键是
要了解stack segment在进程逻辑地址空间的分配——它总是会被放置在user态逻辑地址空间的最后,而
栈底就是“最大地址”,即0177777。理解了这一点,再结合莱昂的注释,相信此函数不难理解。
【思考题】:4054行将该信号的处理程序清0,会有什么后果?
如果user程序希望此种信号一直由某个程序处理,应该怎么办?
显然,进行信号处理典型的写法为:if(issig()) psig(),如:
(1) trap(2693)函数,执行完陷入例程后,会检查信号,并处理;
(2) clock(3725),时间片处理时会检查信号,并处理。
但是,sleep(2066)的处理方法不同:
2072: if(pri >= 0) {
2073: if(issig())
2074: goto psig;
……
2084: swtch();
2085: if(issig())
2086: goto psig;
…..
2105: psig:
2106: aretu(u.u_qsav);
(隐含的return语句)
当以低优先级(大于0的priority)调用sleep时,在睡眠前后,都会检查信号。一旦有信号发生,
会执行aretu(u.u_qsav)——会以u_qsav[ ]恢复sp和r5,但不会进行进程switch——然后执行隐含的return语句。
会return到哪里去呢?
首先,虽然调用sleep的地方很多,但最终必然是通过sys call进行调用的,才会以低优先级调用sleep()。
在trap(2693)程序中:2771 trap1(callp->call),而trap1程序如下:
2841: trap1(f)
2842: int (*f)();
2843: {
2844:
2845: u.u_intflg = 1;
2846: savu(u.u_qsav);
2847: (*f)();
2848: u.u_intflg = 0;
2849: }
呵呵,会return回trap1的调用者——trap中,而我们刚刚看到的,trap函数随后会检查信号,并加以处理。
由于sleep在调用switch前也会检查信号,所以,如果此时进程已经收到信号了,进程不会睡眠,
而是直接返回trap,进行信号处理。
上面描述的是用户指定了信号处理程序的情形,否则,psig()会调用core()写core dump,然后调用exit()退出。
考察进程退出时的动作是个很有趣的话题,让我们看一下exit(3219)函数:
(1)3224:清理STRC标志;
(2)3225~3226:忽略所有信号;
(3)关于io等等操作(略过);
(4)3243:将状态(p_stat)设置为SZOMB(僵尸态);
(5)Loop进程表,找到其父进程
3249: wakeup(p); /唤醒“父进程”,但父进程何时sleep(进程表项地址, )的?
(6)Loop进程表,找到其所有子进程
1)将子进程的父进程id设置为#1进程;
2)如果进程处于SSTOP状态,则setrun它——事关trace,我们将在后面介绍。
显然,调用exit之后,进程没有彻底消亡——它变成了僵尸进程,并仍然占据进程表项。那么,是
谁真正清理了进程表呢?是wait(3270)函数。wait()函数实现第7号sys call wait的功能,它会:
(1) loop进程表,找到所有的状态为SZOMB的子进程,进行清理工作——主要是清理进程表项;
(2) 处理状态为SSTOP的子进程——事关trace,我们下节再介绍;
(3) 如果无SZOMB状态的子进程,则调用“3314:sleep(u.u_procp, PWAIT)”睡眠;
可见,以u.u_procp(进程表项地址)睡眠原因的进程,进入睡眠是为了处理子进程exit和子进程
trace——即会被此两种情况唤醒。
(4) 如果根本没有子进程,则出错return。
【思考题】:如果某父进程早于子进程结束,那么其子进程结束后会否因无人清理而一直占据proc表项?
博客地址:http://blog.csdn.net/cszhao1980
博客专栏地址:http://blog.csdn.net/column/details/lions-unix.html