本系列博文是《現代操作系統(英文第三版)》(Modern Operating Systems,簡稱MOS)的閱讀筆記,定位是正文精要部分的摘錄和課后習題精解,因此不會事無巨細的全面摘抄,僅僅根據個人情況進行記錄和推薦。由於是英文版,部分內容會使用英文原文。
課后習題的選擇標准:盡量避免單純的概念考察(如:What is spooling?)或者簡單的數值計算,而是能夠引起思考加深理解的題目。為了保證解答的正確性,每道題都會附上原書解答,而中文部分會適當加入自己的見解。原書答案下載地址(需注冊)
從本節開始,新增“概念名稱回顧”,不會展開來寫,僅供按圖索驥、查漏補缺,方便回顧。上一節也會補上。
概念名稱回顧
進程:相關API、狀態、實現;
線程:和進程的區別與聯系、內核級線程和用戶級線程區別和實現及優劣;
進程通信:競爭條件、臨界區、互斥與忙等、信號量、互斥鎖、管程、消息傳遞、屏障
調度算法:不同系統的不同調度算法、機制與策略分離;
典型IPC問題:哲學家進餐問題、讀者—寫者問題
1.關於進程數目、進程平均I/O頻率與CPU利用率的推導
原書P94假設進程用於等待I/O結束的時間為它運行的總時間中的p(比如,p=80%代表這個進程I/O花費的時間是總運行時間的80%),同一時間內內存中一共有n個進程在運行,那么這n個進程都在等待I/O的概率為pn——這意味着所有的進程都沒用使用CPU,也即CPU處在空閑狀態——那么,根據對立事件的概率,CPU利用率就可以表示為:
CPU 利用率 = 1 - pn
利用這個公式做圖表,會得到很多有價值的結論。
可以看出,(1)在進程數相同時,I/O頻率越高,CPU的利用率越低;(2)為了提高CPU的利用率,可以采取的方法是增加同時運行的進程數。
雖然這個公式是把I/O頻率平均化所得的近似公式,並且假定這n個進程之間的運行互不干擾;更准確的模型需要用排隊論的知識進行構建,但是,“增加進程數以使得CPU更不常處於空閑狀態”的結論,依然正確,並與上圖僅僅有着輕微的不同。
這個簡單模型甚至還能得到另一個推斷:如果計算機擁有512MB內存,操作系統占用了128MB,其余進程I/O頻率都是80%,並且每個占用128MB,那么此時的CPU利用率是1-0.83,也即49%;而當增加512MB內存時,意味着可以運行更多的4個進程,這時CPU的利用率會提升到79%,也即這額外的512MB內存帶來了30%的CPU利用率提升。
更神奇的是,這個簡單模型還可以用來計算多個進程並行時所需使用的總時間,例子請參考習題5。
2.Peterson解法
原書P123,為了能夠保證進程互斥地進入臨界區(critical_region),並要求只使用軟件編碼實現(不提供額外的硬件機制),最初可以想到的解決方法如下,它被稱為嚴格輪換法(Strict Alternation):
//process 0 while(TURE) { while(turn != 0) ;/*loop*/ critical_region(); turn = 1; noncritical_region(); } //process 1 while(TURE) { while(turn != 1) ;/*loop*/ critical_region(); turn = 0; noncritical_region(); }
雖然可以保證互斥這一要求,但是這兩個進程只能輪流進入臨界區,這意味着較快的一個進程會被較慢的另一個所拖慢。也即,當process0剛退出臨界區而使得turn=1、process1在非臨界區時,process0不能馬上再次進入臨界區,這違反了(原文中的前文“一個好的臨界區訪問方式”的)條件3,這4個條件如下:
1.任何兩個進程不能同時進入同一個臨界區;
2.不應做出有關CPU速度或個數的任何假設;
3.任何不在它的臨界區的進程不應阻塞其他進程;
4.不應有任何進程無限地等待進入臨界區。
看來這個根據直覺寫出的解法並不令人滿意。為了從軟件角度解決這個問題,不同人提出了不同的算法。Peterson於1981年發現了一個相當簡單的解法:
#define FALSE 0 #define TRUE 1 #define N 2 /* number of processes */ int turn; /* whose turn is it? */ int interested[N]; /* all values initially 0 (FALSE) */ void enter_region(int process) /* process is 0 or 1 */ { int other; /* number of the other process*/ other = 1 - process; /* the opposite of process */ interested[process] = TRUE; /* show that you are interested */ turn = process; /* set flag */ while(turn == process && interested[other] == TRUE) ; /* null statement */ } void leave_region(int process) /* process: who is leaving */ { interested[[process] = FALSE; /* indicate departure from critical region */ }
稍作分析可以看出,如果只有一個進程,可以無限運行下去;如果有兩個進程,它們都標記了自己對臨界區感興趣(interested[process] = TRUE),但此時turn由后設置者決定。而先設置者p0正好可以退出while循環,執行臨界區代碼,並於退出臨界區時聲明自己對臨界區無興趣(interested[[process] = FALSE)。這時,如果另一個進程p1在忙等,則可以進入臨界區;如果不在忙等,它的interseted要么非真(未執行到這個語句),要么為真但尚未開始進行忙等:對於前者,如果p0足夠快,可以再次進入臨界區;對於后者,p0就只能開始忙等,等待p1先進入臨界區了。
讀者可能對上面我的轉述看的一頭霧水,沒關系,自行根據代碼和情形假設推導下各個情況才能理解這個解法的妙處。我個人認為這段代碼妙處在於,它用了兩個變量(一個整型和一個2個元素的數組)而非單一變量來提供更好的解法,這個思路非常值得學習。
3.實時系統中周期任務可調度的條件的推導
原書P161,假設一個實時系統中所有任務都是周期性的,一共有m個任務,第i個任務周期為Pi,需要時間Ci的CPU才能處理完,那么這些任務可調度的條件是
\[\sum_{i=1}^{m}\frac{C_{i}}{P_{i}}\leq 1\]
原書沒有解釋這個式子怎么推導的,我這里做一個簡單的解釋:首先,對所有分式進行通分,其分母就表示了一個“大周期”,在這個“大周期”里,任務i被執行的次數就是它在分子中的系數。只要分子小於等於分母,也即分數值小於等於1,這些任務便可以在這個“大周期”中執行所需執行的次數,並且不會逾期。同時,對於某個任務,其C/P大於1,那么無論如何這些任務都是不可調度的。
4.哲學家進餐問題
這部分完整描述在原書P164~167。當初學習湯子瀛版《計算機操作系統》時,這個問題的解決一種是加了限制條件的普通信號量(《現代操作系統》圖2-45的解法與之類似),另一種是同時對兩個數據操作的AND型信號量。下面看看圖2-46給出的既能保證不死鎖,又能最大化同時進餐的哲學家數目的解法:
#define N 5 /* number of philosophers */ #define LEFT (i+N-1)%N /* number of i's left neighbor */ #define RIGHT (i+ 1)%N /* number of i's right neighbor */ #define THINKING 0 /* philosopher is thinking */ #define HUNGRY 1 /* philosopher is trying to get forks */ #define EATING 2 /* philosopher is eating */ typedef int semaphore; /* semaphores are a special kind of int */ int state[N]; /* array to keep track of everyone's state */ semaphore mutex = 1 ; /* mutual exclusion for critical regions */ semaphore s[N]; /* one semaphore per philosopher */ void philosopher(int i) /* i: philosopher number, from 0 to N-1 */ { while (TRUE) { /* repeat forever */ think(); /* philosopher is thinking */ take_ forks(i); /* acquire two forks or block */ eat(); /* yum-yum, spaghetti */ put_ forks(i); /* put both forks back on table */
} } void take_forks(int i) /* i: philosopher number, from 0 to N-1 */ { down(&mutex); /* enter critical region */ state[i] = HUNGRY; /* record fact that philosopher i is hungry */ test(i); /* try to acquire 2 forks */ up(&mutex); /* exit critical region */ down(&s[i]); /* block if forks were not acquired */ } void put_forks(i) /* i: philosopher number, from 0 to N-1 */ { down(&mutex); /* enter critical region */ state[i] =THINKING; /* philosopher has finished eating */ test(LEFT); /* see if left neighbor can now eat */ test( RIGHT); /* see if right neighbor can now eat */ up(&mutex); /* exit critical region */ } void test(i) /* i: philosopher number, from 0 to N-1 */ { if (state[i] ==HUNGRY && state[LEFT] != EATING && state[RIGHT] != EATING) { state[i] = EATING; up(&s[i]); } }
這種對自身狀態的標記,與上面提到的Perterson解法思路很類似。這里的臨界區,是所有哲學家的狀態state[]數組,只有合法的狀態轉換才允許繼續,如果兩邊的哲學家至少有一邊在吃,就會被阻塞,而這個阻塞會在兩邊發生狀態轉化時被測試是否可以被喚醒,非常巧妙的解法。課后45、46題是對這個解法的進一步討論,直接列在下面,由於不難理解,沒有翻譯。
45. In the solution to the dining philosophers problem (Fig. 2-46), why is the state variable set to HUNGRY in the procedure take_forks?
Answer:
The change would mean that after a philosopher stopped eating, neither of his neighbors could be chosen next. In fact, they would never be chosen. Suppose that philosopher 2 finished eating. He would runtest for philosophers 1 and 3, and neither would be started, even though both were hungry and both forks were available. Similarly, if philosopher 4 finished eating, philosopher 3 would not be started. Nothing would start him.
46. Consider the procedure put_forks in Fig. 2-46. Suppose that the variable state[i] was set to THINKING after the two calls to test, rather than before. How would this change affect the solution?Answer:
If a philosopher blocks, neighbors can later see that she is hungry by checking his state, in test , so he can be awakened when the forks are available.
課后習題選
4.When an interrupt or a system call transfers control to the operating system, a kernel stack area separate from the stack of the interrupted process is generally used. Why?
譯:
當中斷或系統調用使得系統的控制權交給操作系統時,所用的內核棧通常與用戶棧是分開的,為什么這么設計?
Answer:
There are several reasons for using a separate stack for the kernel. Two of them are as follows. First, you do not want the operating system to crash because a poorly written user program does not allow for enough stack space. Second, if the kernel leaves stack data in a user program’s memory space
upon return from a system call, a malicious user might be able to use this data to find out information about other processes.
答案譯文:
這有原因,其中兩個是:第一,用戶棧可能很小,如果內核棧不是獨立的而是使用用戶棧的一部分,那么系統會因空間不足崩潰;第二,當返回用戶空間時,惡意用戶可以通過自己的棧(由中斷或系統調用時產生的)殘留的內容獲取別的進程的信息。
5.Multiple jobs can run in parallel and finish faster than if they had run sequentially. Suppose that two jobs, each of which needs 10 minutes of CPU time, start simultaneously. How long will the last one take to complete if they run sequentially? How long if they run in parallel? Assume 50% I/0 wait.
譯:兩個I/O頻率都為50%、都需要10分鍾CPU時間的任務,順序運行需要多少時間?並行運行需要多少時間?
Answer:
If each job has 50% I/O wait, then it will take 20 minutes to complete in the absence of competition. If run sequentially, the second one will finish 40 minutes after the first one starts. Withtwo jobs, the approximate CPU utilization is 1 − 0.52. Thus each one gets 0.375 CPU minute per minute of real time. To accumulate 10 minutes of CPU time, a job must run for 10/0.375 minutes, or about 26.67 minutes. Thus running sequentially the jobs finish after 40 minutes, but running in parallel they finish after 26.67 minutes.
分析:
順序執行的時候,根據I/O頻率為50%、CPU時間10分鍾,那么分別需要20分鍾,一共就是40分鍾。
並行時,根據正文文第2條介紹,CPU利用率為1 − 0.52,這意味着每分鍾每個進程並行時實際獲得用於處理的CPU時間是0.75/2 = 0.375分鍾,那么執行完需要10分鍾的CPU時間的工作實際使用了10/0.375 = 26.67分鍾。又由於是並行的,相當於二者同時完成,因此是26.67分鍾。
我一開始簡單地認為並行時只需要20分鍾,但是明顯是沒有依據的,即使是並行,單個單核心單線程CPU(這是本章的假設之一)在某個時刻而非某一個時期內仍然只能執行一個任務。
26.Show how counting semaphores (i.e., semaphores that can hold an arbitrary value) can be implemented using only binary semaphores and ordinary machine instructions.
譯:
如何用二值信號量和一些機器指令實現計數信號量。
Answer:
Associated with each counting semaphore are two binary semaphores, M ,used for mutual exclusion, and B , used for blocking. Also associated with each counting semaphore is a counter that holds the number of ups minus the number of downs, and a list of processes blocked on that semaphore. To implement down, a process first gains exclusive access to the semaphores, counter, and list by doing a down on M . It then decrements the counter. If it is zero or more, it just does an uponM and exits. If M is negative, the process is put on the list of blocked processes. Then an upis done on M and a down is done on B to block the process. To implement up, first M is downed to get mutual exclusion, and then the counter is incremented. Ifit is more than zero, no one was blocked, so all that needs to be done is to up M. If, however, the counter is now negative or zero, some process must be removed from the list. Finally, an upis done on B and M in that order.
答案譯文:
用兩個二值信號量和一個計數器counter實現一個計數信號量:M用於互斥,B用於阻塞,counter用於記錄up減去down的次數,再用一個鏈表來記錄阻塞在這個計數信號量上的進程。
down的實現:進程先對M進行down來獲得counter、鏈表的獨占訪問權,並把counter減1。如果counter大於等於0,直接對M進行up即可;否則,記錄在鏈表再up,然后對B進行down從而阻塞這個進程。
up的實現:進程同樣先對M進行down,count加1,若其大於0,直接對M進行up即可;否則count小於等於0,把鏈表中一個進程移出,然后對B、M依次up。
勘誤:
1.P163的2.5標題下方,"... we will examine three of the ...",而本節實際上一共只介紹了兩種
2.P166的圖2-46,put_forks(i)的函數定義缺少i的類型int
3.P173.習題46應為Fig.2-46而非2-20