菜狗雜談----從linux fork()談面試

linux進程

進程的定義在這裏就避開不談,我們主要以進程模型爲起點開始

0x1 進程的創建

操作系統需要有一種方式來創建進程。
4種主要事件會導致的創建:

  1. 系統初始化
    OS啓動時,會創建若干個進程,分爲:前臺進程 和 後臺進程
    停留在後臺接收請求的進程叫守護進程
  2. 正在運行的程序執行了創建進程的系統調用
    進程可以通過系統調用再創建一個新的進程
  3. 用戶請求創建一個新進程
    在交互式系統中,鍵入一個命令或點(雙)擊一個圖標就可以啓動一個程序,這也意味着進程的產生
  4. 一個批處理作業的初始化
    大型機環境中,在用戶提交批處理作業時,OS在有資源可用時會創建一個新的進程進行作業

從技術層面而言,上述情況中,新進程都是由一個已存在的進程執行了用於創建進程的系統調用而創建的。在UNIX中,創建新進程的系統調用有且只有一個:fork()。
調用fork()後,父子進程擁有相同的內存映像、環境字符串、打開文件。通常子進程會調用execve或者類似系統調用,修改其內存映像並運行一個新的程序。
在UNIX中,子進程的初始地址空間是父進程的一個副本,這涉及兩個不同的地址空間,不可寫的內存區是共享的。某些UNIX實現程序正文在兩者之間共享,因爲不能被修改。或者子進程共享父進程的所有內存,通過寫時複製共享。總而言之,強調的點是:可寫的內存不能被共享的。

0x2 fork()概覽

fork()的作用是根據一個現有的進程複製出一個新進程,原來的進程稱爲父進程(Parent Process),新進程稱爲子進程(Child Process)

系統調用 描述
fork fork創造的子進程是父進程的完整副本,複製了父親進程的資源,包括內存的內容task_struct內容
vfork vfork創建的子進程與父進程共享數據段,而且由vfork()創建的子進程將先於父進程運行
clone Linux上創建線程一般使用的是pthread庫 實際上linux也給我們提供了創建線程的系統調用,就是clone

fork()原型:

#include <sys/types.h>
#include <unistd.h>


int main(void)
{
	pid_t fpid;  //fork() retern value
    int count = 1;
    
    fpid=fork(); 

    if(fpid < 0)
    {
        printf("fork error : ");
    }
    else if(fpid == 0)  // fork return 0 in the child process because child can get hid PID by getpid( )
    {
        printf("the child process, the count is: %d (%p),the pid is: %d\n", ++count, &count, getpid());
    }
    else  // the PID of the child process is returned in the parent’s thread of execution
    {
        printf("the parent process, the count is: %d (%p),the pid is: %d\n", count, &count, getpid());
    }
    return EXIT_SUCCESS;
}

fork()調用信息:
包含頭文件 <sys/types.h> 和 <unistd.h>
函數功能 : 創建一個子進程
函數原型 :pid_t fork(void);
參數 : 無參數。
返回值:
如果成功創建一個子進程,對於父進程來說返回子進程ID
如果成功創建一個子進程,對於子進程來說返回值爲0
如果爲-1表示創建失敗

示意圖如下:
在這裏插入圖片描述

fork()後父子進程形態
  1. 子進程繼承父進程屬性:
  • uid, gid, euid, egid
  • 附加組id(sgid, supplementary group id)
    /* sgid引入原因是有時候希望這個用戶屬於多個其他部門,這些其他部門的gid就是sgid */
  • 進程組id, 會話id
  • SUID標記, SGID標記
  • 控制終端
  • 當前工作目錄/根目錄
  • 文件創建時的umask
  • 文件描述符的文件標誌(close-on-exec)
  • 信號屏蔽和處理
  • 存儲映射
  • 資源限制
  1. 子進程獨有(與父進程不同的地方):
  • pid不同
  • 進程時間被清空
  • 文件鎖沒有繼承
  • 未處理信號被清空
  1. fork()後父子進程狀態:
  • fork系統調用之後,父子進程將交替執行
  • 任何一個進程都必須有父進程
  • 如果父進程先退出,子進程還沒退出----那麼子進程的父進程將變爲init進程。
  • 如果子進程先退出,父進程還沒退出----那麼子進程必須等到父進程捕獲到了子進程的退出狀態才真正結束,否則這個時候子進程就成爲僵進程
  • 子進程退出會發送SIGCHLD信號給父進程,可以選擇忽略或使用信號處理函數接收處理就可以避免殭屍進程
  1. copy on write(寫時複製)

定義:一個被使用在程式設計領域的最佳化策略。其基礎的觀念是,如果有多個呼叫者(callers)同時要求相同資源,他們會共同取得相同的指標指向相同的資源,直到某個呼叫者(caller)嘗試修改資源時,系統纔會真正複製一個副本(private copy)給該呼叫者,以避免被修改的資源被直接察覺到,這過程對其他的呼叫只都是通透的(transparently)。此作法主要的優點是如果呼叫者並沒有修改該資源,就不會有副本(private copy)被建立。

解釋:如果多個進程要讀取它們自己的那部分資源的副本,那麼複製是不必要的。每個進程只要保存一個指向這個資源的指針即可。如果一個進程要修改自己的那份資源的“副本”,那麼就會複製那份資源。

fork就是基於寫時複製,只讀代碼段是可以共享

若使用vfork 則子進程和父進程佔用同一個內存映像,在子進程修改會影響父進程。 同時只有在子進程執行exec/exit之後纔會運行父進程。實際上子進程佔用的棧空間就是父進程的棧空間,所以需要非常小心。如果vfork的子進程並沒有 exec或者是exit的話,那麼子進程就會執行直到程序退出之後,父進程纔開始執行。而這個時候父進程的內存已經完全被寫壞。

0x3 fork()實戰

現在來看一道linux開發的面試題

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);

已知從這個程序執行到這個程序的所有進程結束這個時間段內,沒有其它新進程執行.問題如下:

  • 請說出執行這個程序後,將一共運行幾個進程?
  • 如果其中一個進程的輸出結果是“pid1:1001, pid2:1002”,寫出其他進程的輸出結果(不考慮進程執行順序)

明顯這道題的目的是考察linux下fork的執行機制。下面我們通過分析這個題目,談談linux下fork的運行機制。

預備知識
  • 進程可以看做程序的一次執行過程。在linux下,每個進程有唯一的PID標識進程。PID是一個從1到32768的正整數,其中1一般是特殊進程init,其它進程從2開始依次編號。當用完32768後,從2重新開始
  • linux中有一個叫進程表的結構用來存儲當前正在運行的進程。可以使用ps aux命令查看所有正在運行的進程
  • 進程在linux中呈樹狀結構,init爲根節點,其它進程均有父進程,某進程的父進程就是啓動這個進程的進程,這個進程叫做父進程的子進程
  • fork的作用是複製一個與當前進程一樣的進程。新進程的所有數據(變量、環境變量、程序計數器等)數值都和原進程一致,但是是一個全新的進程,並作爲原進程的子進程
解答

程序執行過程:

  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. 4個(P0, P1, P2, P3)
  2. pid1: 1001, pid2: 1002
    pid1: 1001, pid2: 0
    pid1: 0, pid2: 1003
    pid1: 0, pid2: 0

DFS:


					    ------->pid2    (forth)
					    |
					    |
					    |
		------->pid1----------->pid1    (third)
		|      (1001)
		|
		|
		|               ------->pid2    (second)
		|		        |      (1002)
		|		        |
		|		        |
root----------->root----------->root    (first)

以P0爲root的進程數

驗證:

在這裏插入圖片描述

下圖即運行結果,注意以實際分配pid爲基數計算即可(本例中是15638)

在這裏插入圖片描述

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