本系列博文是《現代操做系統(英文第三版)》(Modern Operating Systems,簡稱MOS)的閱讀筆記,定位是正文精要部分的摘錄和課後習題精解,所以不會事無鉅細的全面摘抄,僅僅根據我的狀況進行記錄和推薦。因爲是英文版,部份內容會使用英文原文。html
課後習題的選擇標準:儘可能避免單純的概念考察(如:What is spooling?)或者簡單的數值計算,而是可以引發思考加深理解的題目。爲了保證解答的正確性,每道題都會附上原書解答,而中文部分會適當加入本身的看法。原書答案下載地址(需註冊) 算法
從本節開始,新增「概念名稱回顧」,不會展開來寫,僅供按圖索驥、查漏補缺,方便回顧。上一節也會補上。數組
進程:相關API、狀態、實現;app
線程:和進程的區別與聯繫、內核級線程和用戶級線程區別和實現及優劣;ide
進程通訊:競爭條件、臨界區、互斥與忙等、信號量、互斥鎖、管程、消息傳遞、屏障函數
調度算法:不一樣系統的不一樣調度算法、機制與策略分離;oop
典型IPC問題:哲學家進餐問題、讀者—寫者問題學習
原書P94假設進程用於等待I/O結束的時間爲它運行的總時間中的p(好比,p=80%表明這個進程I/O花費的時間是總運行時間的80%),同一時間內內存中一共有n個進程在運行,那麼這n個進程都在等待I/O的機率爲pn——這意味着全部的進程都沒用使用CPU,也即CPU處在空閒狀態——那麼,根據對立事件的機率,CPU利用率就能夠表示爲:測試
CPU 利用率 = 1 - pnui
利用這個公式作圖表,會獲得不少有價值的結論。
能夠看出,(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。
原書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=一、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個元素的數組)而非單一變量來提供更好的解法,這個思路很是值得學習。
原書P161,假設一個實時系統中全部任務都是週期性的,一共有m個任務,第i個任務週期爲Pi,須要時間Ci的CPU才能處理完,那麼這些任務可調度的條件是
\[\sum_{i=1}^{m}\frac{C_{i}}{P_{i}}\leq 1\]
原書沒有解釋這個式子怎麼推導的,我這裏作一個簡單的解釋:首先,對全部分式進行通分,其分母就表示了一個「大週期」,在這個「大週期」裏,任務i被執行的次數就是它在分子中的係數。只要分子小於等於分母,也即分數值小於等於1,這些任務即可以在這個「大週期」中執行所需執行的次數,而且不會逾期。同時,對於某個任務,其C/P大於1,那麼不管如何這些任務都是不可調度的。
這部分完整描述在原書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[]數組,只有合法的狀態轉換才容許繼續,若是兩邊的哲學家至少有一邊在吃,就會被阻塞,而這個阻塞會在兩邊發生狀態轉化時被測試是否能夠被喚醒,很是巧妙的解法。課後4五、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.
譯:
當中斷或系統調用使得系統的控制權交給操做系統時,所用的內核棧一般與用戶棧是分開的,爲何這麼設計?
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.
答案譯文:
這有緣由,其中兩個是:第一,用戶棧可能很小,若是內核棧不是獨立的而是使用用戶棧的一部分,那麼系統會因空間不足崩潰;第二,當返回用戶空間時,惡意用戶能夠經過本身的棧(由中斷或系統調用時產生的)殘留的內容獲取別的進程的信息。
譯:兩個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(這是本章的假設之一)在某個時刻而非某一個時期內仍然只能執行一個任務。
譯:
如何用二值信號量和一些機器指令實現計數信號量。
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