1. 程式人生 > >由一道fork面試題展開來

由一道fork面試題展開來

在酷殼部落格站裡,看到一篇部落格,講了一道關於fork的面試題,為了理解這個面試題背後的一些相關知識,我查找了資料  ,惡補了一下。然後把它記錄下來,方便以後的查閱。

先供出那道fork的面試題: 

題目:請問下面的程式一共輸出多少個“-”?

1 2 3 4 5 6 7 8 9 10 11 12 13 14 #include <stdio.h> #include <sys/types.h> #include <unistd.h> intmain(void) { inti; for(i=0; i<2; i++){
fork(); printf("-"); } return0; }
這道題不但考察了fork的相關知識,還考察了對C標準庫的I/O緩衝區的 理解。

fork的相關知識如下:

  • fork()系統呼叫是Unix下以自身程序建立子程序的系統呼叫,一次呼叫,兩次返回,如果返回是0,則是子程序,如果返回值>0,則是父程序(返回值是子程序的pid),這是眾為周知的。
  • 還有一個很重要的東西是,在fork()的呼叫處,整個父程序空間會原模原樣地複製到子程序中,包括指令,變數值,程式呼叫棧,環境變數,緩衝區,等等。
部落格作者陳皓還畫了一張示意圖,分析了這個fork的機制在這個程式中的體現。我認為還是很直觀,思路很清晰,能夠幫助理解。(注意:下圖中用了幾個色彩,相同顏色的是同一個程序。先將源程式中的printf(“-“) 替換成
printf(“-\n”),不考慮C標準庫的I/O緩衝區的問題對結果的影響


這裡printf列印了6次“_”。如果把for條件中的迴圈i<2,改成i<3,則列印14次“_”。計算公式為 列印次數 = 2 + 4 + 8 + 。。。+ 2 ^ i = 2 ^ (i+1) - 2  .

順便補充一下跟for迴圈的 相關知識:

for (控制表示式1; 控制表示式2; 控制表示式3) 語句

如果不考慮迴圈體中包含continue語句的情況(稍後介紹continue語句),這個for迴圈等價於下面的while迴圈:

控制表示式1;
while (控制表示式2) {
	語句
	控制表示式3;
}

從這種等價形式來看,控制表示式1和3都可以為空,但控制表示式2是必不可少的,例如for (;1;) {...}等價於while (1) {...}死迴圈。C語言規定,如果控制表示式2為空,則認為控制表示式2的值為真,因此死迴圈也可以寫成for (;;) {...}

現在來講跟C標準庫的I/O緩衝區相關的知識:

使用者程式呼叫C標準I/O庫函式讀寫檔案或裝置,而這些庫函式要通過系統呼叫把讀寫請求傳給核心(以後我們會看到與I/O相關的系統呼叫),最終由核心驅動磁碟或裝置完成I/O操作。C標準庫為每個開啟的檔案分配一個I/O緩衝區以加速讀寫操作,通過檔案的FILE結構體可以找到這個緩衝區,使用者呼叫讀寫函式大多數時候都在I/O緩衝區中讀寫,只有少數時候需要把讀寫請求傳給核心。以fgetc/fputc為例,當用戶程式第一次呼叫fgetc讀一個位元組時,fgetc函式可能通過系統呼叫進入核心讀1K位元組到I/O緩衝區中,然後返回I/O緩衝區中的第一個位元組給使用者,把讀寫位置指向I/O緩衝區中的第二個字元,以後使用者再調fgetc,就直接從I/O緩衝區中讀取,而不需要進核心了,當用戶把這1K位元組都讀完之後,再次呼叫fgetc時,fgetc函式會再次進入核心讀1K位元組到I/O緩衝區中。在這個場景中使用者程式、C標準庫和核心之間的關係就像在CPU、Cache和記憶體之間的關係一樣,C標準庫之所以會從核心預讀一些資料放在I/O緩衝區中,是希望使用者程式隨後要用到這些資料,C標準庫的I/O緩衝區也在使用者空間,直接從使用者空間讀取資料比進核心讀資料要快得多。另一方面,使用者程式呼叫fputc通常只是寫到I/O緩衝區中,這樣fputc函式可以很快地返回,如果I/O緩衝區寫滿了,fputc就通過系統呼叫把I/O緩衝區中的資料傳給核心,核心最終把資料寫回磁碟。有時候使用者程式希望把I/O緩衝區中的資料立刻傳給核心,讓核心寫回裝置,這稱為Flush操作,對應的庫函式是fflushfclose函式在關閉檔案之前也會做Flush操作。(注:printf屬於C標準I/O庫的一個函式)

下圖以fgets/fputs示意了I/O緩衝區的作用,使用fgets/fputs函式時在使用者程式中也需要分配緩衝區(圖中的buf1buf2),注意區分使用者程式的緩衝區和C標準庫的I/O緩衝區。


C標準庫的I/O緩衝區有三種類型:全緩衝、行緩衝和無緩衝。當用戶程式呼叫庫函式做寫操作時,不同型別的緩衝區具有不同的特性。

全緩衝

如果緩衝區寫滿了就寫回核心。常規檔案通常是全緩衝的。

行緩衝

如果使用者程式寫的資料中有換行符就把這一行寫回核心,或者如果緩衝區寫滿了就寫回核心。標準輸入和標準輸出對應終端裝置時通常是行緩衝的。

無緩衝

使用者程式每次調庫函式做寫操作都要通過系統呼叫寫回核心。標準錯誤輸出通常是無緩衝的,這樣使用者程式產生的錯誤資訊可以儘快輸出到裝置。

下面通過一個簡單的例子證明標準輸出對應終端裝置時是行緩衝的。

#include <stdio.h>

int main()
{
	printf("hello world");
	while(1);
	return 0;
}

執行這個程式,會發現hello world並沒有列印到螢幕上。用Ctrl-C終止它,去掉程式中的while(1);語句再試一次:

$ ./a.out
hello world$

hello world被列印到螢幕上,後面直接跟Shell提示符,中間沒有換行。

我們知道main函式被啟動程式碼這樣呼叫:exit(main(argc, argv));main函式return時啟動程式碼會呼叫exitexit函式首先關閉所有尚未關閉的FILE *指標(關閉之前要做Flush操作),然後通過_exit系統呼叫進入核心退出當前程序[35]

在上面的例子中,由於標準輸出是行緩衝的,printf("hello world");列印的字串中沒有換行符,所以只把字串寫到標準輸出的I/O緩衝區中而沒有寫回核心(寫到終端裝置),如果敲Ctrl-C,程序是異常終止的,並沒有呼叫exit,也就沒有機會Flush I/O緩衝區,因此字串最終沒有列印到螢幕上。如果把列印語句改成printf("hello world\n");,有換行符,就會立刻寫到終端裝置,或者如果把while(1);去掉也可以寫到終端裝置,因為程式退出時會呼叫exitFlush所有I/O緩衝區。在本書的其它例子中,printf列印的字串末尾都有換行符,以保證字串在printf呼叫結束時就寫到終端裝置。

事實上,最開始的關於fork的那個程式會輸出8個“-”,這是因為printf(“-”);語句有buffer,所以,對於上述程式,printf(“-”);把“-”放到了快取中,並沒有真正的輸出(參看《C語言的迷題》中的第一題),在fork的時候,快取被複制到了子程序空間,所以,就多了兩個,就成了8個,而不是6個。

另外,多說一下,我們知道,Unix下的裝置有“塊裝置”和“字元裝置”的概念,所謂塊裝置,就是以一塊一塊的資料存取的裝置,字元裝置是一次存取一個字元的裝置。磁碟、記憶體都是塊裝置,字元裝置如鍵盤和串列埠。塊裝置一般都有快取,而字元裝置一般都沒有快取

對於上面的問題,我們如果修改一下上面的printf的那條語句為:

1 printf("-\n");

或是

1 2 printf("-"); fflush(stdout);

就沒有問題了(就是6個“-”了),因為程式遇到“\n”,或是EOF,或是緩中區滿,或是檔案描述符關閉,或是主動flush,或是程式退出,就會把資料刷出緩衝區。需要注意的是,標準輸出是行緩衝,所以遇到“\n”的時候會刷出緩衝區,但對於磁碟這個塊裝置來說,“\n”並不會引起緩衝區刷出的動作,那是全緩衝,你可以使用setvbuf來設定緩衝區大小,或是用fflush刷快取。

這樣,對於printf(“-”);這個語句,我們就可以很清楚的知道,哪個子程序複製了父程序標準輸出緩中區裡的的內容,而導致了多次輸出了。(如下圖所示,就是陰影並雙邊框了那兩個子程序)

到此對這道fork面試題的探討告一段落。