(莱昂氏unix源代码分析导读-19)再谈进程swtch

我们已经涉及到了部分进程切换的概念,在本章中,我们会从更一般的意义上考察进程切换的行为。

首先,进程切换(也称作context switch)一定是在内核中完成的。


比如,以下为发生进程切换的最常见的情况:

(1)    active进程因等待某资源阻塞,自动让出cpu;

(2) 进程时间片用完;

 

情况1中,进程会通过系统调用进入内核,在内核态让出cpu;

而情况2的检查是在时钟中断处理程序中进行的。

 

就其原因来讲,进程switch分为两种情况:

(1) 自愿的进程切换,如上述第一种情形;

(2) 非自愿的进程切换,如除上述第二种情形。

 

本章主要讨论的是自愿进程切换。另外,进程管理中涉及了大量中断、信号(软中断)、换入

换出(swap)相关的内容,本章对这部分内容或者跳过,或者一笔带过,对它们的详细讲解

会在自己的专题中完成。

 

首先,看一看swtch()函数。从上一章中已经知道,进程的切换是在swtch()中完成的,Swtch()可分为3段,

每段分属一个进程:

 

2178: swtch()

2179: {

          ……

2189:     savu(u.u_rsav);                          /#进程 M,保存自己

2190:     /*

2191:      * Switch to scheduler's stack

2192:      */

2193:      retu(proc[0].p_addr);              /切换到#0进程

           ……

2200:      /*

2201:      * Search for highest-priority runnable process

2202:      */

          ……                                                /寻找最高优先级的进程N

2215: /*

2216: * If no process is runnable, idle.

2217: */

2218:     if(p == NULL) {                       /如没有可用进程,则idle

2219:         p = rp;             

2220:         idle();                                    /idle函数的核心是wait指令,陷入idle状态

2221:         goto loop;                             /显然,系统idle时,“active”进程为#0进程

2222:      }    

2223:      rp = p;

2224:      curpri = n;

2225:       /* Switch to stack of the new process and set up

2226:       * his segmentation registers.

2227:       */

2228:       retu(rp->p_addr);                   /切换kisa6,即切换到#N进程

2229:       sureg();                                   /这个函数大家应该比较熟悉了,它用来设置新进程的user态寄存器

                 ……                                         /著名的“you are not expected to understand this

                                                                 /我们暂时跳过这部分内容

2247:       return(1);                                                             

2248: }

 

 莱昂对stwch有着详细的解读,在此不再赘述。

【思考题】:考察swtch函数的实现,其使用的变量不是static的就是register的,为什么?

 

下面看一下sleep(chan, pri)函数,它是实现主动进程切换的重要函数之一。当前进程需要等待某资源时,就会

调用该函数。而sleep内部会直接调用swtch()函数让出cpu。该函数的第一个参数“chan”,为“休眠原因”

(也称为wait channel),sleep函数会将其记录p_wchan中。在叫醒进程时,会使用wakeup(chan)函数,

其参数同样也是这个休眠原因。wakeup函数会所有检查休眠的进程,唤醒所有以chan为原因休眠的进程。

 

 一般来说,unix往往会选用“代表该资源的结构的地址”来作为“休眠原因”。当然也有例外,比如

当父进程调用wait系统调用查看子进程的termination状态时,就有可能休眠。但,父进程可能有多个子进程,

所以不能以某子进程作为休眠原因。unix选用父进程的proc表项地址作为休眠原因,而子进程exit时,会以其

父进程的proc表项地址为参数调用wakeup,以激活其父进程(如果父进程休眠的话)。

 

等待资源是发生主动进程切换的重要原因,但不是全部。还有一种重要的情形,即进程退出。当进程退出时,也

需要进行主动进程切换。下面让我们看一下exit()函数:

3219: exit()
3220: {
3221:        register int *q, a;
3222:        register struct proc *p;
3223:
3224:        u.u_procp->p_flag =& ~STRC;
3225:        for(q = &u.u_signal[0]; q < &u.u_signal[NSIG];)             /屏蔽信号
3226:                 *q++ = 1;
3227:        for(q = &u.u_ofile[0]; q < &u.u_ofile[NOFILE]; q++)       /关闭打开的文件
3228:                 if(a = *q) {
3229:                        *q = NULL;
3230:                        closef(a);
3231:                 }
3232:       iput(u.u_cdir);                                             
3233:       xfree();
3234:       a = malloc(swapmap, 1);                                                   /----
3235:       if(a == NULL)
3236:               panic("out of swap");                            
3237:       p = getblk(swapdev, a);                                                              将u中部分内容swap出来
3238:       bcopy(&u, p->b_addr, 256);              
3239:       bwrite(p);
3240:       q = u.u_procp;                                                                       ---/
3241:       mfree(coremap, q->p_size, q->p_addr);                         /释放
3242:       q->p_addr = a;
3243:       q->p_stat = SZOMB;
3244:
3245: loop:
3246:       for(p = &proc[0]; p < &proc[NPROC]; p++)
3247:                if(q->p_ppid == p->p_pid) {                                     
3248:                       wakeup(&proc[1]);
3249:                       wakeup(p);                                                            /这个刚刚讲过,还记得吗?
3250:                       for(p = &proc[0]; p < &proc[NPROC]; p++)      /---
3251:                                if(q->p_pid == p->p_ppid) {
3252:                                         p->p_ppid = 1;                                           其子进程的父进程改为#1进程
3253:                                         if (p->p_stat == SSTOP)                           setrun那些因trace而stop的子进程
3254:                                                setrun(p);       
3255:                                }                                                                     ----/
3256:                        swtch();                                                                /调用swtch进行进程切换
3257:                        /* no return */
3258:                 }
3259:       q->p_ppid = 1;
3260:       goto loop;
3261: } 

 

同sleep一样,exit也是通过调用swtch函数来主动进行进程切换的。如果您足够细心的话,会注意到一个问题:

第3241行:      mfree(coremap, q->p_size, q->p_addr);                         /释放进程的私有空间


即在调用swtch()之前,exit就已经释放了进程的u空间。但是,在swtch()的入口处又操作了这部分内存:

2189:     savu(u.u_rsav);                          /#进程 M,保存自己

不会出什么岔子吧?

 

不会,因为mfree只是将u所占据的内存标记为“空闲”态,只要在执行2189行时,该部分内存没有被分配出去,对其进行

操作就不会有什么问题。而执行exit()函数的进程处于内核态,其执行不会被其他进程抢占。因此,其释放的内存不会分配出去。

当然,在exit()执行过程中,有可能发生硬件中断,而引起中断处理程序的执行——所以,必须小心设计,使期间的中断处理程序

不会调用malloc分配内存,这样就不会有影响。

 

sleep函数我们已经看过,现在看一下wakeup:

2113: wakeup(chan)

2114: {

       ……

2118:    c = chan;

2119:    p = &proc[0];

2120:    i = NPROC;

2121:    do {

2122:       if(p->p_wchan == c) {

2123:              setrun(p);           

2124:       }

2125:       p++;

2126:    } while(--i);

2127: }

 

2134: setrun(p)

2135: {

2136:    register struct proc *rp;

2137:

2138:    rp = p;

2139:    rp->p_wchan = 0;

2140:    rp->p_stat = SRUN;

2141:    if(rp->p_pri < curpri)

2142:    runrun++;

2143:    if(runout != 0 && (rp->p_flag&SLOAD) == 0) {   /涉及到swap,暂时不看

2144:       runout = 0;

2145:       wakeup(&runout);

2146:    }

2147: }

注意,setrun函数并没有切换进程,而是仅仅把休眠进程的status改为SRUN,使该进程拥有了被

switch函数再度schedule的能力。这样设计的一个重要原因是避免内核态的进程抢占——中断处理程序

很可能会调用wakeup函数,如果在wakeup中直接调用swtch进行进程切换的话,就有可能造成核态进程

被抢占。

 

 

另外,需要注意两个变量:

(1)   一是curpri,他是当前active进程的priority,如果某休眠进程的优先级高于current进程,

             就应该schedule该休眠进程;需要注意的是priority值越小,则优先级越高;

(2) 二是runrun计数——当它不为0时,表明当前有更高优先级的进程等待执行(因此,

            runrun可称作“再调度”标记)。显然,当发现有休眠进程的priority高于curpri后,

            就应该增加runrun计数。

 

最后,我们谈一下函数setpri()。显然,它的作用是根据进程运行时间等因素来调整进程的优先级。

但是,它的算法实在是让人一头雾水:

 

2156: setpri(up)

2157: {

2158:    register *pp, p;

2159:

2160:    pp = up;

2161:    p = (pp->p_cpu & 0377)/16;

2162:    p += PUSER + pp->p_nice;

2163:    if(p > 127)

2164:       p = 127;

2165:    if(p > curpri)

2166:       runrun++;

2167:    pp->p_pri = p;

2168: }

 

虽然我们难以彻底理解这个古怪的函数,但我们还是可以从中得到些有用的信息:

(1)    这个函数可以赋予的优先级最高超不过PUSER——而根据PUSER这个名字本身,

              我们有理由猜测,PUSER应该是user态进程所能获取的最高优先级;

 

(2)     简单的说,p_cpu就是进程active时占用的cpu时间(实际情况要复杂一些,对于长时间

               运行的进程,p_cpu会有一个衰减,这样做的目的是使该进程的优先级不至于降的太低)。

              显然,进程的优先级是随其执行时间的增长而减小的——这样,可以避免长时间的进程霸占cpu;

 

(3)     另一个能够影响到进程优先级的是p_nice,该变量可以通过nice系统调用进行设置,用户可以

             通过设置该值来影响进程的优先级;

 

(4)    setpri()还会设置“再调度”标志runrun,但糟糕的是,2165行的判断似乎是写反了——但莱昂告

              诉我们是我们错了,why?

 

       setpri函数设置的是进程优先级,所以它必须小心的进行设计,以使所有进程得到合理的执行

      时间,避免进程出现饿死的情形。

 

【思考题】:

2165行那个惊人的判断,我们容易想到的合理的解释是setpri()很多情况下用来更新“本进程”的优先级,

而一旦优先级降低,则表示“有可能”有更高级别的进程在等待,故将runrun++。

 

但时钟中断处理程序clock中对setpri()的调用显然不属于这种情况——它对所有优先级大于PUSER的进程调用此函数。

 

博客地址:http://blog.csdn.net/cszhao1980

博客专栏地址:http://blog.csdn.net/column/details/lions-unix.html

 

你可能感兴趣的:(unix,struct,user,null,Signal,代码分析)