回覆列表
  • 1 # IT劉小虎

    Linux 系統中的程序建立

    許多作業系統都提供了專門的程序產生機制,比較典型的過程是:首先在記憶體新的地址空間裡建立程序,然後讀取可執行程式,裝載到記憶體中執行。

    Linux 系統建立執行緒並未使用上述經典過程,而是將建立過程拆分到兩組獨立的函式中執行:fork() 函式和 exec() 函式族。

    基本流程是這樣的:首先,fork() 函式拷貝當前程序建立子程序。產生的子程序與父程序的區別僅在與 PID 與 PPID 以及某些資源和統計量,例如掛起的訊號等。準備好程序執行的地址空間後,exec() 函式族負責讀取可執行程式,並將其載入到相應的位置開始執行。

    Linux 系統建立程序使用的這兩組函式效果與其他作業系統的經典程序建立方式效果是相似的,可能有讀者會覺得這麼做會讓程序建立過於繁瑣,其實不是的,Linux 這麼做的其中一個原因是為了提高程式碼的複用率,這得益於 Linux 高度概括的抽象,無需再額外設計一套機制用於建立程序。

    “寫時複製”

    早期 Linux 中的 fork() 函式直接把父程序的所有資源賦值給創建出的子程序,這樣的機制自然是簡單的,但是效率卻比較低下。

    原因是顯而易見的:子程序並不一定要使用父程序的資源,或者子程序可能僅需以只讀的方式訪問父程序的資源,這時“複製一份資源”就純屬多餘的開銷了。

    針對這樣的問題,Linux 後續版本中的 fork() 函式開始採用“寫時複製”機制。寫時複製技術可以將複製需求延遲,甚至免除複製,減小開銷。

    具體來說就是,Linux 在呼叫 fork() 建立子程序時,並不著急複製整個程序地址空間,而是暫時讓父子程序以只讀的方式共享同一個複製。複製動作只在子程序需要寫入時才會發生,以確保各個程序有自己獨立的記憶體空間。

    如果子程序用不到或者只需要讀取共享空間資料,那麼複製動作就被省去了,Linux 就減小了開銷。例如,系統呼叫 fork() 後立即呼叫 exec(),此時 exec() 會載入新的映像覆蓋 fork() 的地址空間,複製動作完全可以省去。

    事實上,fork() 函式的實際開銷就是複製父程序的頁表以及給子程序建立唯一的程序描述符。在大多數情況下,Linux 建立程序後都會馬上執行新的可執行程式,因此“寫時複製”機制可以避免相當多的資料複製。建立程序速度快是 Linux 系統的一個特徵,因此“寫時複製”是一種相當重要的最佳化。

    建立程序時,記憶體地址空間裡常常包含數十 MB 的資料,如果每建立一次程序,就複製一次資料,開銷顯然是非常大的。

    fork() 函式

    Linux 中的 fork() 函式其實是基於 clone() 實現的,clone() 函式可以透過一系列引數標誌指定父子程序需要共享的資源,在 Linux 中輸入 man 命令可以檢視 clone() 函式的C語言原型,以及相關的引數標誌:在Linux中,fork() 函式最終呼叫了 do_fork() 函式,它的C語言程式碼如下,請看(do_fork() 函式的C語言程式碼比較長,下面面只列出了一部分):

    do_fork() 函式完成了程序建立的大部分工作,從相關的C語言原始碼可以看出,它呼叫了 copy_process() 函式,copy_process() 函式的C語言原始碼如下,請看:copy_process() 函式的程式碼也是比較長的,在我手上的Linux系統中,達到了近 400 行,不過程式碼的整體邏輯是清晰的:

    (1)copy_process() 函式首先檢查了一些標誌位,接著呼叫 dup_task_struct() 函式為新程序建立核心棧,以及上一節提到的 thread_info 和 task_struct 結構:

    建立後,接下來的 arch_dup_task_struct() 函式會將 orig 結構複製給新建立的結構,檢視相關C語言程式碼,這一過程是清晰的:此時子程序和父程序的描述符是完全相同的。

    (2)接下來,需要檢查一些標誌位和統計資訊,相關的C語言程式碼如下,請看:

    (3)將一些統計量清零,以及初始化一些區別成員,此時雖然新程序的 task_struct 結構體大多成員未被修改,但是父子程序已經有所區別。這一過程的相關C語言程式碼片段如下,請看:

    (4)將新建立的子程序狀態設定為 TASK_UNINTERRUUPTIBLE,確保其暫時不會被投入執行,這一過程的C語言程式碼相對簡單。(5)呼叫 alloc_pid() 函式為新程序分配一個獨一無二的 pid,相關C語言程式碼如下,請看:

    (6)根據 clone() 函式的引數標誌位,複製或共享已經開啟的檔案、檔案系統、訊號處理函式、程序地址空間等資源,例如下面這段C語言程式碼:(7)將為新程序建立的 task_struct 結構體的指標返回給呼叫者,也即 do_fork() 函式。此時新建立的程序還沒有被投入執行。

    現在回到 do_fork() 函式。如果呼叫 clone() 函式時,沒有傳遞 CLONE_STOPPED 引數,新建立的程序將被喚醒,並投入執行,這一過程的C語言程式碼如下:到這裡,一個新的程序就被 Linux 建立完畢了。

    Linux 核心有意讓新建立的子程序先執行,因為子程序常常會立即呼叫 exec() 函式載入新的程式到記憶體中執行,這樣就避免了寫時複製的額外開銷。如果父程序首先執行,顯然極有可能開始往地址空間寫入操作,導致複製動作發生。

    小結

    本節詳細的從C語言程式碼層面分析了Linux核心建立程序的過程,可見,即使是複雜的作業系統程式碼,也是透過一系列基本C語言語法和函式實現的。那麼,Linux 是如何建立執行緒的呢?之前我們曾經提到,Linux 系統並不特別區分程序和執行緒,執行緒其實是一種特殊的程序,Linux 是如何實現這一“特殊”過程的呢?敬請關注。

  • 2 # 使用者6255694147972

    這跟execvp函式的實現方式有關:

    int execvp(const char *file ,char * const argv []);

    execvp()會從PATH 環境變數所指的目錄中查詢符合引數file的檔名,找到後便執行該檔案,然後將第二個引數argv傳給該欲執行的檔案。如果執行成功則函式不會返回,執行失敗則直接返回-1,失敗原因存於errno中。

    之所以顯示“fail to exec”,是因為在PATH環境變數所指的目錄中沒有名為“hello”的程式。建議進行如下操作:

    1、執行“echo $PATH”,檢視一下PATH環境變數指向那些目錄

    2、編寫一個輸出“hello world”的程式,並命名為hello,即執行命令:

    gcc -o hello

    hello.c

    3、把名為”hello“的程式複製到PATH變數所指的其中一個目錄中

  • 中秋節和大豐收的關聯?
  • 範廷鈺農心杯大殺局翻盤再勝許家元,豪取五連勝,本局精彩在哪?