我们已经涉及到了部分进程切换的概念,在本章中,我们会从更一般的意义上考察进程切换的行为。
首先,进程切换(也称作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