1. 程式人生 > >(萊昂氏unix原始碼分析導讀-19)再談程序swtch

(萊昂氏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的程序呼叫此函式。