一道值得思考的fork()面試題
程式如下,判斷輸出多少個'_'
./a.out
int main(){ for(int i = 0; i < 2; ++i){ fork(); printf("_"); } }
熟悉fork的話,這裡很容易就能知道,一共產生了3個子程序,還有一個父程序,所以一共是四個程序;每次fork之後都會輸出一個'_',那麼在這裡應當的輸出是6個'_'
但是實際輸出卻是 8個'_'; 但是如果在printf("_")之後使用fflush(stdout),或者使用printf("\n")清空輸出緩衝區來輸出則結果就是6個了,這就不得不再返回來談一談printf的輸出機制。
- 標準I/O對待快取的資料採用3種不同的策略,全緩衝、行緩衝、無緩衝。
- 對於沒有互動的終端,例如塊裝置檔案,系統採用全緩衝;(比如輸出到檔案)
- 對於標準輸入,標準輸出這樣互動裝置採用行緩衝;(輸出到螢幕或控制檯)
- 對於需要立即響應的裝置,例如標準錯誤採用無緩衝;
printf函式只有當緩衝區被重新整理的時候才會輸出資料,在此之前只是將資料存放到緩衝區。
理解了這點後,再去分析上述程式碼;首先只有一個程序a.out,緩衝區為空;第一次fork()後,父程序a.out產生了一個子程序“子1”,子程序複製父程序的緩衝區(此時為空),然後printf將"_"先放入緩衝區內,a.out與"子1"的緩衝區內各有一個"_";
第二次fork()後,a.out產生了一個子程序"子2",“子1”產生了一個子程序“子3”,“子2”和“子3”均從各自的父程序複製了緩衝區的“_”,此時,a.out,"子1",“子2”,“子3”各自的緩衝區內均有一個“_”,接下來的printf語句又將”_“分別放入了四個程序的緩衝區內(圖中用黑色標識的_),此時,迴圈結束,四個程序的各自緩衝區內都分別有兩個"_",總共8個,特殊之處在於,”子2“與”子3“的緩衝區內的第一個”_“是複製各自的父程序緩衝區得到的,a.out與"子1"的緩衝區內的第一個"_"是printf累積”_“的結果。如此一來,迴圈結束後,程式就要結束了,此時緩衝區重新整理輸出了8個"_".
到這裡,這道題已經講的很明白了,為了深入理解,再通過幾個程式碼詳細理解一下緩衝區的重新整理機制:
int main(){ printf("hello");
//fflush(stdout); sleep(5); int m = 5; m = 7; while(m-->0){ printf("*"); } //printf("\n"); sleep(3); printf("world"); }
這個程式將如何輸出呢?如果去掉註釋輸出結果又是什麼?先思考一下吧。
理想中的輸出應該是先輸出 "hello",等待5秒,輸出7個'*',再等待3秒,輸出"world";但是實際情況卻是等待 8秒,一瞬間輸出”hello*******world“.
如果去掉註釋呢?就是我們所期盼的第一種情況了;造成這種現象的原因上面已經提到了,就是printf並非直接輸出,而是先攢到緩衝區裡,等待緩衝區重新整理再輸出;這段程式中的兩條註釋語句均可以重新整理行緩衝策略的緩衝區(行緩衝與全緩衝在文章末尾)。
那麼來看一下緩衝區重新整理的時機吧
- 1.遇到“\n”,立即重新整理緩衝區。(行緩衝)
- 2.程式呼叫fflush函式重新整理緩衝區
- 3.程式以exit結束,緩衝區會重新整理。如果以_exit結束,緩衝區資料會被直接清空。
- 4.緩衝區滿,也會將緩衝區資料刷新出來。
這樣子便不難理解了吧。可惜剛開始學習printf的時候總是習慣性在後面加一個'\n',所以這麼久了這麼重要的一個緩衝區機制居然才知道。
最後再用一個例子補充一下行緩衝與全緩衝的區別
int main(){ printf("hello world\r\n"); if(0 == fork()){ printf("son\r\n"); }else{ printf("father\r\n"); } }
先普通執行一下輸出結果為一條"hello world",一條"son",以及一條"father";
再將輸出位置重定向到檔案,輸出結果多了一條"hello world"
同樣的程式,只因為輸出位置的不同,結果也就不同,這是因為第一種使用了標準輸出也就是控制檯和螢幕,採用行緩衝策略;第二種將標準輸出重定向到檔案,採用的是全緩衝策略;行緩衝策略中的'\n'對全緩衝無效;所以又回到了文章開始的那道面試題的緩衝區複製。由於是全緩衝,並不會重新整理緩衝區,因此fork時子程序複製了父程序的緩衝區,裡面有一條”hello world“,此時父子程序再各自往緩衝區中攢了"father"和'son';因此就呈現出最終的輸出結果啦。
(這種緩衝區機制在C++中的cout也是一樣的)