由一道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>
int main( void )
{
int i;
for (i=0;
i<2; i++){ fork();
printf ( "-" );
}
return 0;
}
|
fork的相關知識如下:
- fork()系統呼叫是Unix下以自身程序建立子程序的系統呼叫,一次呼叫,兩次返回,如果返回是0,則是子程序,如果返回值>0,則是父程序(返回值是子程序的pid),這是眾為周知的。
- 還有一個很重要的東西是,在fork()的呼叫處,整個父程序空間會原模原樣地複製到子程序中,包括指令,變數值,程式呼叫棧,環境變數,緩衝區,等等。
這裡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操作,對應的庫函式是fflush
,fclose
函式在關閉檔案之前也會做Flush操作。(注:printf屬於C標準I/O庫的一個函式)
下圖以fgets
/fputs
示意了I/O緩衝區的作用,使用fgets
/fputs
函式時在使用者程式中也需要分配緩衝區(圖中的buf1
和buf2
),注意區分使用者程式的緩衝區和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
時啟動程式碼會呼叫exit
,exit
函式首先關閉所有尚未關閉的FILE
*
指標(關閉之前要做Flush操作),然後通過_exit
系統呼叫進入核心退出當前程序[35]。
在上面的例子中,由於標準輸出是行緩衝的,printf("hello world");
列印的字串中沒有換行符,所以只把字串寫到標準輸出的I/O緩衝區中而沒有寫回核心(寫到終端裝置),如果敲Ctrl-C,程序是異常終止的,並沒有呼叫exit
,也就沒有機會Flush
I/O緩衝區,因此字串最終沒有列印到螢幕上。如果把列印語句改成printf("hello world\n");
,有換行符,就會立刻寫到終端裝置,或者如果把while(1);
去掉也可以寫到終端裝置,因為程式退出時會呼叫exit
Flush所有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面試題的探討告一段落。