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