linux 系統調用fork

秒殺linux下系統調用fork()面試題

爲什麼fork執行兩次呢?

http://www.itmian4.com/forum.php?mod=viewthread&tid=3248&extra=page%3D2%26filter%3Dtypeid%26typeid%3D116%26typeid%3D116



第一道題(在之前博客也寫過這道題:http://blog.csdn.net/chdhust/article/details/8535915):

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

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()的機制比較熟悉的話,這個題並不難,輸出應該是6個“-”,但是,實際上這個程序會很tricky地輸出8個“-”。

要講清這個題,我們首先需要知道fork()系統調用的特性,

  • fork()系統調用是Unix下以自身進程創建子進程的系統調用,一次調用,兩次返回,如果返回是0,則是子進程,如果返回值>0,則是父進程(返回值是子進程的pid),這是衆爲周知的。
  • 還有一個很重要的東西是,在fork()的調用處,整個父進程空間會原模原樣地複製到子進程中,包括指令,變量值,程序調用棧,環境變量,緩衝區,等等。

所以,上面的那個程序爲什麼會輸入8個“-”,這是因爲printf(“-”);語句有buffer,所以,對於上述程序,printf(“-”);把“-”放到了緩存中,並沒有真正的輸出,在fork的時候,緩存被複制到了子進程空間,所以,就多了兩個,就成了8個,而不是6個。

另外,多說一下,我們知道,Unix下的設備有“塊設備”和“字符設備”的概念,所謂塊設備,就是以一塊一塊的數據存取的設備,字符設備是一次存取一個字符的設備。磁盤、內存都是塊設備,字符設備如鍵盤和串口。塊設備一般都有緩存,而字符設備一般都沒有緩存

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

1 printf("-\n");


或是

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


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

我估計有些朋友可能對於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有“n”
      printf("ppid=%d, pid=%d, i=%d n", getppid(), getpid(), i);
   }
   sleep(10); //讓進程停留十秒,這樣我們可以用pstree查看一下進程樹
   return 0;
}


於是,上面這段程序會輸出下面的結果,(注:編譯出的可執行的程序名爲fork)


1
2
3
4
5
6
7
8
9
10
ppid=8858, pid=8518, i=0
ppid=8858, pid=8518, i=1
ppid=8518, pid=8519, i=0
ppid=8518, pid=8519, i=1
ppid=8518, pid=8520, i=1
ppid=8519, pid=8521, i=1
    
$ pstree -p | grep fork
|-bash(8858)-+-fork(8518)-+-fork(8519)---fork(8521)
|            |            `-fork(8520)


面對這樣的圖你可能還是看不懂,沒事,我好事做到底,畫個圖給你看看:

注意:上圖中的我用了幾個色彩,相同顏色的是同一個進程。於是,我們的pstree的圖示就可以成爲下面這個樣子:(下圖中的顏色與上圖對應)

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



第二道題:

   給出如下C程序,在linux下使用gcc編譯:

#include "stdio.h"#include "sys/types.h"#include "unistd.h"int main(){    pid_t pid1;    pid_t pid2;    pid1 = fork();    pid2 = fork();    printf("pid1:%d, pid2:%d\n", pid1, pid2);}

      要求如下:

      已知從這個程序執行到這個程序的所有進程結束這個時間段內,沒有其它新進程執行。

      1、請說出執行這個程序後,將一共運行幾個進程。

      2、如果其中一個進程的輸出結果是“pid1:1001, pid2:1002”,寫出其他進程的輸出結果(不考慮進程執行順序)。

   這裏先列出一些必要的預備知識,對linux下進程機制比較熟悉的朋友可以略過。

      1、進程可以看做程序的一次執行過程。在linux下,每個進程有唯一的PID標識進程。PID是一個從1到32768的正整數,其中1一般是特殊進程init,其它進程從2開始依次編號。當用完32768後,從2重新開始。

      2、linux中有一個叫進程表的結構用來存儲當前正在運行的進程。可以使用“ps aux”命令查看所有正在運行的進程。

      3、進程在linux中呈樹狀結構,init爲根節點,其它進程均有父進程,某進程的父進程就是啓動這個進程的進程,這個進程叫做父進程的子進程。

      4、fork的作用是複製一個與當前進程一樣的進程。新進程的所有數據(變量、環境變量、程序計數器等)數值都和原進程一致,但是是一個全新的進程,並作爲原進程的子進程。

   有了上面的預備知識,我們再來看看解題的關鍵。我認爲,解題的關鍵就是要認識到fork將程序切成兩段。看下圖:

      上圖表示一個含有fork的程序,而fork語句可以看成將程序切爲A、B兩個部分。然後整個程序會如下運行:

      step1、設由shell直接執行程序,生成了進程P。P執行完Part. A的所有代碼。

      step2、當執行到pid = fork();時,P啓動一個進程Q,Q是P的子進程,和P是同一個程序的進程。Q繼承P的所有變量、環境變量、程序計數器的當前值。

      step3、在P進程中,fork()將Q的PID返回給變量pid,並繼續執行Part. B的代碼。

      step4、在進程Q中,將0賦給pid,並繼續執行Part. B的代碼。

      這裏有三個點非常關鍵:

      1、P執行了所有程序,而Q只執行了Part. B,即fork()後面的程序。(這是因爲Q繼承了P的PC-程序計數器)

      2、Q繼承了fork()語句執行時當前的環境,而不是程序的初始環境。

      3、P中fork()語句啓動子進程Q,並將Q的PID返回,而Q中的fork()語句不啓動新進程,僅將0返回。

解題

      下面利用上文闡述的知識進行解題。這裏我把兩個問題放在一起進行分析。

      1、從shell中執行此程序,啓動了一個進程,我們設這個進程爲P0,設其PID爲XXX(解題過程不需知道其PID)。

      2、當執行到pid1 = fork();時,P0啓動一個子進程P1,由題目知P1的PID爲1001。我們暫且不管P1。

      3、P0中的fork返回1001給pid1,繼續執行到pid2 = fork();,此時啓動另一個新進程,設爲P2,由題目知P2的PID爲1002。同樣暫且不管P2。

      4、P0中的第二個fork返回1002給pid2,繼續執行完後續程序,結束。所以,P0的結果爲“pid1:1001, pid2:1002”。

      5、再看P2,P2生成時,P0中pid1=1001,所以P2中pid1繼承P0的1001,而作爲子進程pid2=0。P2從第二個fork後開始執行,結束後輸出“pid1:1001, pid2:0”。

      6、接着看P1,P1中第一條fork返回0給pid1,然後接着執行後面的語句。而後面接着的語句是pid2 = fork();執行到這裏,P1又產生了一個新進程,設爲P3。先不管P3。

      7、P1中第二條fork將P3的PID返回給pid2,由預備知識知P3的PID爲1003,所以P1的pid2=1003。P1繼續執行後續程序,結束,輸出“pid1:0, pid2:1003”。

      8、P3作爲P1的子進程,繼承P1中pid1=0,並且第二條fork將0返回給pid2,所以P3最後輸出“pid1:0, pid2:0”。

      9、至此,整個執行過程完畢。

      所得答案:

      1、一共執行了四個進程。(P0, P1, P2, P3)

      2、另外幾個進程的輸出分別爲:

      pid1:1001, pid2:0

      pid1:0, pid2:1003

      pid1:0, pid2:0

      進一步可以給出一個以P0爲根的進程樹:

代碼驗證結果:




第三道題:



請補足橫線,使之輸出welcome xiyoulinux
if(_____)
printf(“xiyoulinux ”);
else
printf(“welcome”);



這道題如果在if中只寫fork(),也不知道打印出來什麼順序的,所以想到函數wait(),等待子進程結束。


程序改寫爲:






運行結果:




可見這樣纔得到了正確的運行結果。


第四道題:

int main()

{

    return fork()&&fork()||fork();

}

問題:1.一共產生幾個進程  2.返回值爲1的概率爲多少?這道題一看頭都暈了,但是抓其根本,再難也都能解決的!!
其實對於fork的分析,最直觀的做法也最有效,畫出進程關係圖,兩個問題便迎刃而解這裏要用到的有關fork的知識並不多,只需要知道fork的返回值:fork是一次調用,兩次返回;子進程的返回值爲0,父進程的返回值是新產生子進程的進程號(大於0)。對於&&操作符,若前操作數爲0,則直接跳過後操作數的判斷。基於以上兩個基本理論,通過圖示,即可得出結論:6597649209284344582.jpg 

圖中同一色彩的框圖代表同一進程,可見,此時共產生5個進程;結合表達式的最終值分佈情況可得,返回1的概率爲3/5

造成此種結果,最主要的誘因,是fork()後,父進程與子進程返回沒有先後順序,有時父進程先返回,有時子進程先返回。所以,根據父子進程返回的不同結果,邏輯運算符&&和||選擇是否執行其後的程序代碼(此處即爲fork)。


第五道題(EMC的一道筆試題)




  • int main(int argc, char* argv[])  
  • {  
  •    fork();  
  •    fork() && fork() || fork();  
  •    fork();  
  • }  
  • 不算main這個進程自身,到底創建了多少個進程啊?  


這道題和第四道題比較類似,第一個fork()和最後一個fork()一定會執行,中間的同上圖,在這裏不畫了。

所以這道題解法爲:加上前面的fork和最後的fork,所有的進程都會執行,會產生4個分支,總共4*5=20個分支,也就是20個進程,除去main主進程,就是19個進程了。



blog 來源http://www.itmian4.com/forum.php?mod=viewthread&tid=3364&extra=page%3D1%26filter%3Dtypeid%26typeid%3D116%26typeid%3D116





blog 來源http://www.itmian4.com/forum.php?mod=viewthread&tid=3364&extra=page%3D1%26filter%3Dtypeid%26typeid%3D116%26typeid%3D116


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章