'從C語言代碼分析Linux系統是如何創建進程的'

Linux C語言 操作系統 設計 技術 嵌入式時代 2019-08-09
"

前面兩節簡要地從C語言源代碼層面討論了Linux系統中進程的基本概念,我們知道了Linux內核如何描述和記錄進程的資源,以及進程的五種基本狀態和進程的家族樹。事實上,就進程管理而言,Linux還是有一些獨特之處的。

"

前面兩節簡要地從C語言源代碼層面討論了Linux系統中進程的基本概念,我們知道了Linux內核如何描述和記錄進程的資源,以及進程的五種基本狀態和進程的家族樹。事實上,就進程管理而言,Linux還是有一些獨特之處的。

從C語言代碼分析Linux系統是如何創建進程的

Linux 是如何創建進程的呢?

Linux 系統中的進程創建

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

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

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

"

前面兩節簡要地從C語言源代碼層面討論了Linux系統中進程的基本概念,我們知道了Linux內核如何描述和記錄進程的資源,以及進程的五種基本狀態和進程的家族樹。事實上,就進程管理而言,Linux還是有一些獨特之處的。

從C語言代碼分析Linux系統是如何創建進程的

Linux 是如何創建進程的呢?

Linux 系統中的進程創建

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

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

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

從C語言代碼分析Linux系統是如何創建進程的

fork() 函數和 exec() 函數族

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

“寫時拷貝”

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

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

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

"

前面兩節簡要地從C語言源代碼層面討論了Linux系統中進程的基本概念,我們知道了Linux內核如何描述和記錄進程的資源,以及進程的五種基本狀態和進程的家族樹。事實上,就進程管理而言,Linux還是有一些獨特之處的。

從C語言代碼分析Linux系統是如何創建進程的

Linux 是如何創建進程的呢?

Linux 系統中的進程創建

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

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

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

從C語言代碼分析Linux系統是如何創建進程的

fork() 函數和 exec() 函數族

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

“寫時拷貝”

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

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

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

從C語言代碼分析Linux系統是如何創建進程的

“寫時拷貝”機制

具體來說就是,Linux 在調用 fork() 創建子進程時,並不著急拷貝整個進程地址空間,而是暫時讓父子進程以只讀的方式共享同一個拷貝。拷貝動作只在子進程需要寫入時才會發生,以確保各個進程有自己獨立的內存空間。

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

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

創建進程時,內存地址空間裡常常包含數十 MB 的數據,如果每創建一次進程,就拷貝一次數據,開銷顯然是非常大的。

fork() 函數

Linux 中的 fork() 函數其實是基於 clone() 實現的,clone() 函數可以通過一系列參數標誌指定父子進程需要共享的資源,在 Linux 中輸入 man 命令可以查看 clone() 函數的C語言原型:

"

前面兩節簡要地從C語言源代碼層面討論了Linux系統中進程的基本概念,我們知道了Linux內核如何描述和記錄進程的資源,以及進程的五種基本狀態和進程的家族樹。事實上,就進程管理而言,Linux還是有一些獨特之處的。

從C語言代碼分析Linux系統是如何創建進程的

Linux 是如何創建進程的呢?

Linux 系統中的進程創建

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

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

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

從C語言代碼分析Linux系統是如何創建進程的

fork() 函數和 exec() 函數族

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

“寫時拷貝”

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

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

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

從C語言代碼分析Linux系統是如何創建進程的

“寫時拷貝”機制

具體來說就是,Linux 在調用 fork() 創建子進程時,並不著急拷貝整個進程地址空間,而是暫時讓父子進程以只讀的方式共享同一個拷貝。拷貝動作只在子進程需要寫入時才會發生,以確保各個進程有自己獨立的內存空間。

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

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

創建進程時,內存地址空間裡常常包含數十 MB 的數據,如果每創建一次進程,就拷貝一次數據,開銷顯然是非常大的。

fork() 函數

Linux 中的 fork() 函數其實是基於 clone() 實現的,clone() 函數可以通過一系列參數標誌指定父子進程需要共享的資源,在 Linux 中輸入 man 命令可以查看 clone() 函數的C語言原型:

從C語言代碼分析Linux系統是如何創建進程的

clone() 函數的C語言原型

以及相關的參數標誌:

"

前面兩節簡要地從C語言源代碼層面討論了Linux系統中進程的基本概念,我們知道了Linux內核如何描述和記錄進程的資源,以及進程的五種基本狀態和進程的家族樹。事實上,就進程管理而言,Linux還是有一些獨特之處的。

從C語言代碼分析Linux系統是如何創建進程的

Linux 是如何創建進程的呢?

Linux 系統中的進程創建

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

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

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

從C語言代碼分析Linux系統是如何創建進程的

fork() 函數和 exec() 函數族

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

“寫時拷貝”

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

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

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

從C語言代碼分析Linux系統是如何創建進程的

“寫時拷貝”機制

具體來說就是,Linux 在調用 fork() 創建子進程時,並不著急拷貝整個進程地址空間,而是暫時讓父子進程以只讀的方式共享同一個拷貝。拷貝動作只在子進程需要寫入時才會發生,以確保各個進程有自己獨立的內存空間。

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

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

創建進程時,內存地址空間裡常常包含數十 MB 的數據,如果每創建一次進程,就拷貝一次數據,開銷顯然是非常大的。

fork() 函數

Linux 中的 fork() 函數其實是基於 clone() 實現的,clone() 函數可以通過一系列參數標誌指定父子進程需要共享的資源,在 Linux 中輸入 man 命令可以查看 clone() 函數的C語言原型:

從C語言代碼分析Linux系統是如何創建進程的

clone() 函數的C語言原型

以及相關的參數標誌:

從C語言代碼分析Linux系統是如何創建進程的

相關的參數標誌

在Linux中,fork() 函數最終調用了 do_fork() 函數,它的C語言代碼如下,請看(do_fork() 函數的C語言代碼比較長,下面面只列出了一部分):

"

前面兩節簡要地從C語言源代碼層面討論了Linux系統中進程的基本概念,我們知道了Linux內核如何描述和記錄進程的資源,以及進程的五種基本狀態和進程的家族樹。事實上,就進程管理而言,Linux還是有一些獨特之處的。

從C語言代碼分析Linux系統是如何創建進程的

Linux 是如何創建進程的呢?

Linux 系統中的進程創建

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

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

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

從C語言代碼分析Linux系統是如何創建進程的

fork() 函數和 exec() 函數族

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

“寫時拷貝”

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

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

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

從C語言代碼分析Linux系統是如何創建進程的

“寫時拷貝”機制

具體來說就是,Linux 在調用 fork() 創建子進程時,並不著急拷貝整個進程地址空間,而是暫時讓父子進程以只讀的方式共享同一個拷貝。拷貝動作只在子進程需要寫入時才會發生,以確保各個進程有自己獨立的內存空間。

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

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

創建進程時,內存地址空間裡常常包含數十 MB 的數據,如果每創建一次進程,就拷貝一次數據,開銷顯然是非常大的。

fork() 函數

Linux 中的 fork() 函數其實是基於 clone() 實現的,clone() 函數可以通過一系列參數標誌指定父子進程需要共享的資源,在 Linux 中輸入 man 命令可以查看 clone() 函數的C語言原型:

從C語言代碼分析Linux系統是如何創建進程的

clone() 函數的C語言原型

以及相關的參數標誌:

從C語言代碼分析Linux系統是如何創建進程的

相關的參數標誌

在Linux中,fork() 函數最終調用了 do_fork() 函數,它的C語言代碼如下,請看(do_fork() 函數的C語言代碼比較長,下面面只列出了一部分):

從C語言代碼分析Linux系統是如何創建進程的

do_fork() 函數的C語言代碼

do_fork() 函數完成了進程創建的大部分工作,從相關的C語言源代碼可以看出,它調用了 copy_process() 函數,copy_process() 函數的C語言源代碼如下,請看:

"

前面兩節簡要地從C語言源代碼層面討論了Linux系統中進程的基本概念,我們知道了Linux內核如何描述和記錄進程的資源,以及進程的五種基本狀態和進程的家族樹。事實上,就進程管理而言,Linux還是有一些獨特之處的。

從C語言代碼分析Linux系統是如何創建進程的

Linux 是如何創建進程的呢?

Linux 系統中的進程創建

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

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

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

從C語言代碼分析Linux系統是如何創建進程的

fork() 函數和 exec() 函數族

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

“寫時拷貝”

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

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

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

從C語言代碼分析Linux系統是如何創建進程的

“寫時拷貝”機制

具體來說就是,Linux 在調用 fork() 創建子進程時,並不著急拷貝整個進程地址空間,而是暫時讓父子進程以只讀的方式共享同一個拷貝。拷貝動作只在子進程需要寫入時才會發生,以確保各個進程有自己獨立的內存空間。

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

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

創建進程時,內存地址空間裡常常包含數十 MB 的數據,如果每創建一次進程,就拷貝一次數據,開銷顯然是非常大的。

fork() 函數

Linux 中的 fork() 函數其實是基於 clone() 實現的,clone() 函數可以通過一系列參數標誌指定父子進程需要共享的資源,在 Linux 中輸入 man 命令可以查看 clone() 函數的C語言原型:

從C語言代碼分析Linux系統是如何創建進程的

clone() 函數的C語言原型

以及相關的參數標誌:

從C語言代碼分析Linux系統是如何創建進程的

相關的參數標誌

在Linux中,fork() 函數最終調用了 do_fork() 函數,它的C語言代碼如下,請看(do_fork() 函數的C語言代碼比較長,下面面只列出了一部分):

從C語言代碼分析Linux系統是如何創建進程的

do_fork() 函數的C語言代碼

do_fork() 函數完成了進程創建的大部分工作,從相關的C語言源代碼可以看出,它調用了 copy_process() 函數,copy_process() 函數的C語言源代碼如下,請看:

從C語言代碼分析Linux系統是如何創建進程的

copy_process() 函數的C語言源代碼

copy_process() 函數的代碼也是比較長的,在我手上的Linux系統中,達到了近 400 行,不過代碼的整體邏輯是清晰的:

(1)copy_process() 函數首先檢查了一些標誌位,接著調用 dup_task_struct() 函數為新進程創建內核棧,以及上一節提到的 thread_info 和 task_struct 結構:

"

前面兩節簡要地從C語言源代碼層面討論了Linux系統中進程的基本概念,我們知道了Linux內核如何描述和記錄進程的資源,以及進程的五種基本狀態和進程的家族樹。事實上,就進程管理而言,Linux還是有一些獨特之處的。

從C語言代碼分析Linux系統是如何創建進程的

Linux 是如何創建進程的呢?

Linux 系統中的進程創建

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

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

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

從C語言代碼分析Linux系統是如何創建進程的

fork() 函數和 exec() 函數族

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

“寫時拷貝”

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

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

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

從C語言代碼分析Linux系統是如何創建進程的

“寫時拷貝”機制

具體來說就是,Linux 在調用 fork() 創建子進程時,並不著急拷貝整個進程地址空間,而是暫時讓父子進程以只讀的方式共享同一個拷貝。拷貝動作只在子進程需要寫入時才會發生,以確保各個進程有自己獨立的內存空間。

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

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

創建進程時,內存地址空間裡常常包含數十 MB 的數據,如果每創建一次進程,就拷貝一次數據,開銷顯然是非常大的。

fork() 函數

Linux 中的 fork() 函數其實是基於 clone() 實現的,clone() 函數可以通過一系列參數標誌指定父子進程需要共享的資源,在 Linux 中輸入 man 命令可以查看 clone() 函數的C語言原型:

從C語言代碼分析Linux系統是如何創建進程的

clone() 函數的C語言原型

以及相關的參數標誌:

從C語言代碼分析Linux系統是如何創建進程的

相關的參數標誌

在Linux中,fork() 函數最終調用了 do_fork() 函數,它的C語言代碼如下,請看(do_fork() 函數的C語言代碼比較長,下面面只列出了一部分):

從C語言代碼分析Linux系統是如何創建進程的

do_fork() 函數的C語言代碼

do_fork() 函數完成了進程創建的大部分工作,從相關的C語言源代碼可以看出,它調用了 copy_process() 函數,copy_process() 函數的C語言源代碼如下,請看:

從C語言代碼分析Linux系統是如何創建進程的

copy_process() 函數的C語言源代碼

copy_process() 函數的代碼也是比較長的,在我手上的Linux系統中,達到了近 400 行,不過代碼的整體邏輯是清晰的:

(1)copy_process() 函數首先檢查了一些標誌位,接著調用 dup_task_struct() 函數為新進程創建內核棧,以及上一節提到的 thread_info 和 task_struct 結構:

從C語言代碼分析Linux系統是如何創建進程的

調用 dup_task_struct() 函數為新進程創建內核棧

創建後,接下來的 arch_dup_task_struct() 函數會將 orig 結構拷貝給新創建的結構,查看相關C語言代碼,這一過程是清晰的:

"

前面兩節簡要地從C語言源代碼層面討論了Linux系統中進程的基本概念,我們知道了Linux內核如何描述和記錄進程的資源,以及進程的五種基本狀態和進程的家族樹。事實上,就進程管理而言,Linux還是有一些獨特之處的。

從C語言代碼分析Linux系統是如何創建進程的

Linux 是如何創建進程的呢?

Linux 系統中的進程創建

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

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

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

從C語言代碼分析Linux系統是如何創建進程的

fork() 函數和 exec() 函數族

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

“寫時拷貝”

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

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

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

從C語言代碼分析Linux系統是如何創建進程的

“寫時拷貝”機制

具體來說就是,Linux 在調用 fork() 創建子進程時,並不著急拷貝整個進程地址空間,而是暫時讓父子進程以只讀的方式共享同一個拷貝。拷貝動作只在子進程需要寫入時才會發生,以確保各個進程有自己獨立的內存空間。

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

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

創建進程時,內存地址空間裡常常包含數十 MB 的數據,如果每創建一次進程,就拷貝一次數據,開銷顯然是非常大的。

fork() 函數

Linux 中的 fork() 函數其實是基於 clone() 實現的,clone() 函數可以通過一系列參數標誌指定父子進程需要共享的資源,在 Linux 中輸入 man 命令可以查看 clone() 函數的C語言原型:

從C語言代碼分析Linux系統是如何創建進程的

clone() 函數的C語言原型

以及相關的參數標誌:

從C語言代碼分析Linux系統是如何創建進程的

相關的參數標誌

在Linux中,fork() 函數最終調用了 do_fork() 函數,它的C語言代碼如下,請看(do_fork() 函數的C語言代碼比較長,下面面只列出了一部分):

從C語言代碼分析Linux系統是如何創建進程的

do_fork() 函數的C語言代碼

do_fork() 函數完成了進程創建的大部分工作,從相關的C語言源代碼可以看出,它調用了 copy_process() 函數,copy_process() 函數的C語言源代碼如下,請看:

從C語言代碼分析Linux系統是如何創建進程的

copy_process() 函數的C語言源代碼

copy_process() 函數的代碼也是比較長的,在我手上的Linux系統中,達到了近 400 行,不過代碼的整體邏輯是清晰的:

(1)copy_process() 函數首先檢查了一些標誌位,接著調用 dup_task_struct() 函數為新進程創建內核棧,以及上一節提到的 thread_info 和 task_struct 結構:

從C語言代碼分析Linux系統是如何創建進程的

調用 dup_task_struct() 函數為新進程創建內核棧

創建後,接下來的 arch_dup_task_struct() 函數會將 orig 結構拷貝給新創建的結構,查看相關C語言代碼,這一過程是清晰的:

從C語言代碼分析Linux系統是如何創建進程的

拷貝給新創建的結構

此時子進程和父進程的描述符是完全相同的。

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

"

前面兩節簡要地從C語言源代碼層面討論了Linux系統中進程的基本概念,我們知道了Linux內核如何描述和記錄進程的資源,以及進程的五種基本狀態和進程的家族樹。事實上,就進程管理而言,Linux還是有一些獨特之處的。

從C語言代碼分析Linux系統是如何創建進程的

Linux 是如何創建進程的呢?

Linux 系統中的進程創建

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

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

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

從C語言代碼分析Linux系統是如何創建進程的

fork() 函數和 exec() 函數族

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

“寫時拷貝”

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

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

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

從C語言代碼分析Linux系統是如何創建進程的

“寫時拷貝”機制

具體來說就是,Linux 在調用 fork() 創建子進程時,並不著急拷貝整個進程地址空間,而是暫時讓父子進程以只讀的方式共享同一個拷貝。拷貝動作只在子進程需要寫入時才會發生,以確保各個進程有自己獨立的內存空間。

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

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

創建進程時,內存地址空間裡常常包含數十 MB 的數據,如果每創建一次進程,就拷貝一次數據,開銷顯然是非常大的。

fork() 函數

Linux 中的 fork() 函數其實是基於 clone() 實現的,clone() 函數可以通過一系列參數標誌指定父子進程需要共享的資源,在 Linux 中輸入 man 命令可以查看 clone() 函數的C語言原型:

從C語言代碼分析Linux系統是如何創建進程的

clone() 函數的C語言原型

以及相關的參數標誌:

從C語言代碼分析Linux系統是如何創建進程的

相關的參數標誌

在Linux中,fork() 函數最終調用了 do_fork() 函數,它的C語言代碼如下,請看(do_fork() 函數的C語言代碼比較長,下面面只列出了一部分):

從C語言代碼分析Linux系統是如何創建進程的

do_fork() 函數的C語言代碼

do_fork() 函數完成了進程創建的大部分工作,從相關的C語言源代碼可以看出,它調用了 copy_process() 函數,copy_process() 函數的C語言源代碼如下,請看:

從C語言代碼分析Linux系統是如何創建進程的

copy_process() 函數的C語言源代碼

copy_process() 函數的代碼也是比較長的,在我手上的Linux系統中,達到了近 400 行,不過代碼的整體邏輯是清晰的:

(1)copy_process() 函數首先檢查了一些標誌位,接著調用 dup_task_struct() 函數為新進程創建內核棧,以及上一節提到的 thread_info 和 task_struct 結構:

從C語言代碼分析Linux系統是如何創建進程的

調用 dup_task_struct() 函數為新進程創建內核棧

創建後,接下來的 arch_dup_task_struct() 函數會將 orig 結構拷貝給新創建的結構,查看相關C語言代碼,這一過程是清晰的:

從C語言代碼分析Linux系統是如何創建進程的

拷貝給新創建的結構

此時子進程和父進程的描述符是完全相同的。

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

從C語言代碼分析Linux系統是如何創建進程的

檢查一些標誌位和統計信息

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

"

前面兩節簡要地從C語言源代碼層面討論了Linux系統中進程的基本概念,我們知道了Linux內核如何描述和記錄進程的資源,以及進程的五種基本狀態和進程的家族樹。事實上,就進程管理而言,Linux還是有一些獨特之處的。

從C語言代碼分析Linux系統是如何創建進程的

Linux 是如何創建進程的呢?

Linux 系統中的進程創建

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

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

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

從C語言代碼分析Linux系統是如何創建進程的

fork() 函數和 exec() 函數族

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

“寫時拷貝”

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

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

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

從C語言代碼分析Linux系統是如何創建進程的

“寫時拷貝”機制

具體來說就是,Linux 在調用 fork() 創建子進程時,並不著急拷貝整個進程地址空間,而是暫時讓父子進程以只讀的方式共享同一個拷貝。拷貝動作只在子進程需要寫入時才會發生,以確保各個進程有自己獨立的內存空間。

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

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

創建進程時,內存地址空間裡常常包含數十 MB 的數據,如果每創建一次進程,就拷貝一次數據,開銷顯然是非常大的。

fork() 函數

Linux 中的 fork() 函數其實是基於 clone() 實現的,clone() 函數可以通過一系列參數標誌指定父子進程需要共享的資源,在 Linux 中輸入 man 命令可以查看 clone() 函數的C語言原型:

從C語言代碼分析Linux系統是如何創建進程的

clone() 函數的C語言原型

以及相關的參數標誌:

從C語言代碼分析Linux系統是如何創建進程的

相關的參數標誌

在Linux中,fork() 函數最終調用了 do_fork() 函數,它的C語言代碼如下,請看(do_fork() 函數的C語言代碼比較長,下面面只列出了一部分):

從C語言代碼分析Linux系統是如何創建進程的

do_fork() 函數的C語言代碼

do_fork() 函數完成了進程創建的大部分工作,從相關的C語言源代碼可以看出,它調用了 copy_process() 函數,copy_process() 函數的C語言源代碼如下,請看:

從C語言代碼分析Linux系統是如何創建進程的

copy_process() 函數的C語言源代碼

copy_process() 函數的代碼也是比較長的,在我手上的Linux系統中,達到了近 400 行,不過代碼的整體邏輯是清晰的:

(1)copy_process() 函數首先檢查了一些標誌位,接著調用 dup_task_struct() 函數為新進程創建內核棧,以及上一節提到的 thread_info 和 task_struct 結構:

從C語言代碼分析Linux系統是如何創建進程的

調用 dup_task_struct() 函數為新進程創建內核棧

創建後,接下來的 arch_dup_task_struct() 函數會將 orig 結構拷貝給新創建的結構,查看相關C語言代碼,這一過程是清晰的:

從C語言代碼分析Linux系統是如何創建進程的

拷貝給新創建的結構

此時子進程和父進程的描述符是完全相同的。

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

從C語言代碼分析Linux系統是如何創建進程的

檢查一些標誌位和統計信息

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

從C語言代碼分析Linux系統是如何創建進程的

將一些統計量清零,以及初始化一些區別成員

(4)將新創建的子進程狀態設置為 TASK_UNINTERRUUPTIBLE,確保其暫時不會被投入運行,這一過程的C語言代碼相對簡單。

(5)調用 alloc_pid() 函數為新進程分配一個獨一無二的 pid,相關C語言代碼如下,請看:

"

前面兩節簡要地從C語言源代碼層面討論了Linux系統中進程的基本概念,我們知道了Linux內核如何描述和記錄進程的資源,以及進程的五種基本狀態和進程的家族樹。事實上,就進程管理而言,Linux還是有一些獨特之處的。

從C語言代碼分析Linux系統是如何創建進程的

Linux 是如何創建進程的呢?

Linux 系統中的進程創建

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

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

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

從C語言代碼分析Linux系統是如何創建進程的

fork() 函數和 exec() 函數族

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

“寫時拷貝”

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

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

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

從C語言代碼分析Linux系統是如何創建進程的

“寫時拷貝”機制

具體來說就是,Linux 在調用 fork() 創建子進程時,並不著急拷貝整個進程地址空間,而是暫時讓父子進程以只讀的方式共享同一個拷貝。拷貝動作只在子進程需要寫入時才會發生,以確保各個進程有自己獨立的內存空間。

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

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

創建進程時,內存地址空間裡常常包含數十 MB 的數據,如果每創建一次進程,就拷貝一次數據,開銷顯然是非常大的。

fork() 函數

Linux 中的 fork() 函數其實是基於 clone() 實現的,clone() 函數可以通過一系列參數標誌指定父子進程需要共享的資源,在 Linux 中輸入 man 命令可以查看 clone() 函數的C語言原型:

從C語言代碼分析Linux系統是如何創建進程的

clone() 函數的C語言原型

以及相關的參數標誌:

從C語言代碼分析Linux系統是如何創建進程的

相關的參數標誌

在Linux中,fork() 函數最終調用了 do_fork() 函數,它的C語言代碼如下,請看(do_fork() 函數的C語言代碼比較長,下面面只列出了一部分):

從C語言代碼分析Linux系統是如何創建進程的

do_fork() 函數的C語言代碼

do_fork() 函數完成了進程創建的大部分工作,從相關的C語言源代碼可以看出,它調用了 copy_process() 函數,copy_process() 函數的C語言源代碼如下,請看:

從C語言代碼分析Linux系統是如何創建進程的

copy_process() 函數的C語言源代碼

copy_process() 函數的代碼也是比較長的,在我手上的Linux系統中,達到了近 400 行,不過代碼的整體邏輯是清晰的:

(1)copy_process() 函數首先檢查了一些標誌位,接著調用 dup_task_struct() 函數為新進程創建內核棧,以及上一節提到的 thread_info 和 task_struct 結構:

從C語言代碼分析Linux系統是如何創建進程的

調用 dup_task_struct() 函數為新進程創建內核棧

創建後,接下來的 arch_dup_task_struct() 函數會將 orig 結構拷貝給新創建的結構,查看相關C語言代碼,這一過程是清晰的:

從C語言代碼分析Linux系統是如何創建進程的

拷貝給新創建的結構

此時子進程和父進程的描述符是完全相同的。

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

從C語言代碼分析Linux系統是如何創建進程的

檢查一些標誌位和統計信息

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

從C語言代碼分析Linux系統是如何創建進程的

將一些統計量清零,以及初始化一些區別成員

(4)將新創建的子進程狀態設置為 TASK_UNINTERRUUPTIBLE,確保其暫時不會被投入運行,這一過程的C語言代碼相對簡單。

(5)調用 alloc_pid() 函數為新進程分配一個獨一無二的 pid,相關C語言代碼如下,請看:

從C語言代碼分析Linux系統是如何創建進程的

為新進程分配一個獨一無二的 pid

(6)根據 clone() 函數的參數標誌位,拷貝或共享已經打開的文件、文件系統、信號處理函數、進程地址空間等資源,例如下面這段C語言代碼:

"

前面兩節簡要地從C語言源代碼層面討論了Linux系統中進程的基本概念,我們知道了Linux內核如何描述和記錄進程的資源,以及進程的五種基本狀態和進程的家族樹。事實上,就進程管理而言,Linux還是有一些獨特之處的。

從C語言代碼分析Linux系統是如何創建進程的

Linux 是如何創建進程的呢?

Linux 系統中的進程創建

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

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

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

從C語言代碼分析Linux系統是如何創建進程的

fork() 函數和 exec() 函數族

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

“寫時拷貝”

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

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

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

從C語言代碼分析Linux系統是如何創建進程的

“寫時拷貝”機制

具體來說就是,Linux 在調用 fork() 創建子進程時,並不著急拷貝整個進程地址空間,而是暫時讓父子進程以只讀的方式共享同一個拷貝。拷貝動作只在子進程需要寫入時才會發生,以確保各個進程有自己獨立的內存空間。

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

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

創建進程時,內存地址空間裡常常包含數十 MB 的數據,如果每創建一次進程,就拷貝一次數據,開銷顯然是非常大的。

fork() 函數

Linux 中的 fork() 函數其實是基於 clone() 實現的,clone() 函數可以通過一系列參數標誌指定父子進程需要共享的資源,在 Linux 中輸入 man 命令可以查看 clone() 函數的C語言原型:

從C語言代碼分析Linux系統是如何創建進程的

clone() 函數的C語言原型

以及相關的參數標誌:

從C語言代碼分析Linux系統是如何創建進程的

相關的參數標誌

在Linux中,fork() 函數最終調用了 do_fork() 函數,它的C語言代碼如下,請看(do_fork() 函數的C語言代碼比較長,下面面只列出了一部分):

從C語言代碼分析Linux系統是如何創建進程的

do_fork() 函數的C語言代碼

do_fork() 函數完成了進程創建的大部分工作,從相關的C語言源代碼可以看出,它調用了 copy_process() 函數,copy_process() 函數的C語言源代碼如下,請看:

從C語言代碼分析Linux系統是如何創建進程的

copy_process() 函數的C語言源代碼

copy_process() 函數的代碼也是比較長的,在我手上的Linux系統中,達到了近 400 行,不過代碼的整體邏輯是清晰的:

(1)copy_process() 函數首先檢查了一些標誌位,接著調用 dup_task_struct() 函數為新進程創建內核棧,以及上一節提到的 thread_info 和 task_struct 結構:

從C語言代碼分析Linux系統是如何創建進程的

調用 dup_task_struct() 函數為新進程創建內核棧

創建後,接下來的 arch_dup_task_struct() 函數會將 orig 結構拷貝給新創建的結構,查看相關C語言代碼,這一過程是清晰的:

從C語言代碼分析Linux系統是如何創建進程的

拷貝給新創建的結構

此時子進程和父進程的描述符是完全相同的。

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

從C語言代碼分析Linux系統是如何創建進程的

檢查一些標誌位和統計信息

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

從C語言代碼分析Linux系統是如何創建進程的

將一些統計量清零,以及初始化一些區別成員

(4)將新創建的子進程狀態設置為 TASK_UNINTERRUUPTIBLE,確保其暫時不會被投入運行,這一過程的C語言代碼相對簡單。

(5)調用 alloc_pid() 函數為新進程分配一個獨一無二的 pid,相關C語言代碼如下,請看:

從C語言代碼分析Linux系統是如何創建進程的

為新進程分配一個獨一無二的 pid

(6)根據 clone() 函數的參數標誌位,拷貝或共享已經打開的文件、文件系統、信號處理函數、進程地址空間等資源,例如下面這段C語言代碼:

從C語言代碼分析Linux系統是如何創建進程的

拷貝或共享已經打開的資源

(7)將為新進程創建的 task_struct 結構體的指針返回給調用者,也即 do_fork() 函數。此時新創建的進程還沒有被投入運行。

現在回到 do_fork() 函數。如果調用 clone() 函數時,沒有傳遞 CLONE_STOPPED 參數,新創建的進程將被喚醒,並投入運行,這一過程的C語言代碼如下:

"

前面兩節簡要地從C語言源代碼層面討論了Linux系統中進程的基本概念,我們知道了Linux內核如何描述和記錄進程的資源,以及進程的五種基本狀態和進程的家族樹。事實上,就進程管理而言,Linux還是有一些獨特之處的。

從C語言代碼分析Linux系統是如何創建進程的

Linux 是如何創建進程的呢?

Linux 系統中的進程創建

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

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

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

從C語言代碼分析Linux系統是如何創建進程的

fork() 函數和 exec() 函數族

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

“寫時拷貝”

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

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

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

從C語言代碼分析Linux系統是如何創建進程的

“寫時拷貝”機制

具體來說就是,Linux 在調用 fork() 創建子進程時,並不著急拷貝整個進程地址空間,而是暫時讓父子進程以只讀的方式共享同一個拷貝。拷貝動作只在子進程需要寫入時才會發生,以確保各個進程有自己獨立的內存空間。

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

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

創建進程時,內存地址空間裡常常包含數十 MB 的數據,如果每創建一次進程,就拷貝一次數據,開銷顯然是非常大的。

fork() 函數

Linux 中的 fork() 函數其實是基於 clone() 實現的,clone() 函數可以通過一系列參數標誌指定父子進程需要共享的資源,在 Linux 中輸入 man 命令可以查看 clone() 函數的C語言原型:

從C語言代碼分析Linux系統是如何創建進程的

clone() 函數的C語言原型

以及相關的參數標誌:

從C語言代碼分析Linux系統是如何創建進程的

相關的參數標誌

在Linux中,fork() 函數最終調用了 do_fork() 函數,它的C語言代碼如下,請看(do_fork() 函數的C語言代碼比較長,下面面只列出了一部分):

從C語言代碼分析Linux系統是如何創建進程的

do_fork() 函數的C語言代碼

do_fork() 函數完成了進程創建的大部分工作,從相關的C語言源代碼可以看出,它調用了 copy_process() 函數,copy_process() 函數的C語言源代碼如下,請看:

從C語言代碼分析Linux系統是如何創建進程的

copy_process() 函數的C語言源代碼

copy_process() 函數的代碼也是比較長的,在我手上的Linux系統中,達到了近 400 行,不過代碼的整體邏輯是清晰的:

(1)copy_process() 函數首先檢查了一些標誌位,接著調用 dup_task_struct() 函數為新進程創建內核棧,以及上一節提到的 thread_info 和 task_struct 結構:

從C語言代碼分析Linux系統是如何創建進程的

調用 dup_task_struct() 函數為新進程創建內核棧

創建後,接下來的 arch_dup_task_struct() 函數會將 orig 結構拷貝給新創建的結構,查看相關C語言代碼,這一過程是清晰的:

從C語言代碼分析Linux系統是如何創建進程的

拷貝給新創建的結構

此時子進程和父進程的描述符是完全相同的。

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

從C語言代碼分析Linux系統是如何創建進程的

檢查一些標誌位和統計信息

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

從C語言代碼分析Linux系統是如何創建進程的

將一些統計量清零,以及初始化一些區別成員

(4)將新創建的子進程狀態設置為 TASK_UNINTERRUUPTIBLE,確保其暫時不會被投入運行,這一過程的C語言代碼相對簡單。

(5)調用 alloc_pid() 函數為新進程分配一個獨一無二的 pid,相關C語言代碼如下,請看:

從C語言代碼分析Linux系統是如何創建進程的

為新進程分配一個獨一無二的 pid

(6)根據 clone() 函數的參數標誌位,拷貝或共享已經打開的文件、文件系統、信號處理函數、進程地址空間等資源,例如下面這段C語言代碼:

從C語言代碼分析Linux系統是如何創建進程的

拷貝或共享已經打開的資源

(7)將為新進程創建的 task_struct 結構體的指針返回給調用者,也即 do_fork() 函數。此時新創建的進程還沒有被投入運行。

現在回到 do_fork() 函數。如果調用 clone() 函數時,沒有傳遞 CLONE_STOPPED 參數,新創建的進程將被喚醒,並投入運行,這一過程的C語言代碼如下:

從C語言代碼分析Linux系統是如何創建進程的

喚醒,並投入運行

到這裡,一個新的進程就被 Linux 創建完畢了。

Linux 內核有意讓新創建的子進程先運行,因為子進程常常會立即調用 exec() 函數加載新的程序到內存中運行,這樣就避免了寫時拷貝的額外開銷。如果父進程首先執行,顯然極有可能開始往地址空間寫入操作,導致拷貝動作發生。

小結

本節詳細的從C語言代碼層面分析了Linux內核創建進程的過程,可見,即使是複雜的操作系統代碼,也是通過一系列基本C語言語法和函數實現的。那麼,Linux 是如何創建線程的呢?之前我們曾經提到,Linux 系統並不特別區分進程和線程,線程其實是一種特殊的進程,Linux 是如何實現這一“特殊”過程的呢?限於篇幅,下一節再說了,敬請關注。

"

前面兩節簡要地從C語言源代碼層面討論了Linux系統中進程的基本概念,我們知道了Linux內核如何描述和記錄進程的資源,以及進程的五種基本狀態和進程的家族樹。事實上,就進程管理而言,Linux還是有一些獨特之處的。

從C語言代碼分析Linux系統是如何創建進程的

Linux 是如何創建進程的呢?

Linux 系統中的進程創建

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

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

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

從C語言代碼分析Linux系統是如何創建進程的

fork() 函數和 exec() 函數族

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

“寫時拷貝”

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

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

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

從C語言代碼分析Linux系統是如何創建進程的

“寫時拷貝”機制

具體來說就是,Linux 在調用 fork() 創建子進程時,並不著急拷貝整個進程地址空間,而是暫時讓父子進程以只讀的方式共享同一個拷貝。拷貝動作只在子進程需要寫入時才會發生,以確保各個進程有自己獨立的內存空間。

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

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

創建進程時,內存地址空間裡常常包含數十 MB 的數據,如果每創建一次進程,就拷貝一次數據,開銷顯然是非常大的。

fork() 函數

Linux 中的 fork() 函數其實是基於 clone() 實現的,clone() 函數可以通過一系列參數標誌指定父子進程需要共享的資源,在 Linux 中輸入 man 命令可以查看 clone() 函數的C語言原型:

從C語言代碼分析Linux系統是如何創建進程的

clone() 函數的C語言原型

以及相關的參數標誌:

從C語言代碼分析Linux系統是如何創建進程的

相關的參數標誌

在Linux中,fork() 函數最終調用了 do_fork() 函數,它的C語言代碼如下,請看(do_fork() 函數的C語言代碼比較長,下面面只列出了一部分):

從C語言代碼分析Linux系統是如何創建進程的

do_fork() 函數的C語言代碼

do_fork() 函數完成了進程創建的大部分工作,從相關的C語言源代碼可以看出,它調用了 copy_process() 函數,copy_process() 函數的C語言源代碼如下,請看:

從C語言代碼分析Linux系統是如何創建進程的

copy_process() 函數的C語言源代碼

copy_process() 函數的代碼也是比較長的,在我手上的Linux系統中,達到了近 400 行,不過代碼的整體邏輯是清晰的:

(1)copy_process() 函數首先檢查了一些標誌位,接著調用 dup_task_struct() 函數為新進程創建內核棧,以及上一節提到的 thread_info 和 task_struct 結構:

從C語言代碼分析Linux系統是如何創建進程的

調用 dup_task_struct() 函數為新進程創建內核棧

創建後,接下來的 arch_dup_task_struct() 函數會將 orig 結構拷貝給新創建的結構,查看相關C語言代碼,這一過程是清晰的:

從C語言代碼分析Linux系統是如何創建進程的

拷貝給新創建的結構

此時子進程和父進程的描述符是完全相同的。

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

從C語言代碼分析Linux系統是如何創建進程的

檢查一些標誌位和統計信息

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

從C語言代碼分析Linux系統是如何創建進程的

將一些統計量清零,以及初始化一些區別成員

(4)將新創建的子進程狀態設置為 TASK_UNINTERRUUPTIBLE,確保其暫時不會被投入運行,這一過程的C語言代碼相對簡單。

(5)調用 alloc_pid() 函數為新進程分配一個獨一無二的 pid,相關C語言代碼如下,請看:

從C語言代碼分析Linux系統是如何創建進程的

為新進程分配一個獨一無二的 pid

(6)根據 clone() 函數的參數標誌位,拷貝或共享已經打開的文件、文件系統、信號處理函數、進程地址空間等資源,例如下面這段C語言代碼:

從C語言代碼分析Linux系統是如何創建進程的

拷貝或共享已經打開的資源

(7)將為新進程創建的 task_struct 結構體的指針返回給調用者,也即 do_fork() 函數。此時新創建的進程還沒有被投入運行。

現在回到 do_fork() 函數。如果調用 clone() 函數時,沒有傳遞 CLONE_STOPPED 參數,新創建的進程將被喚醒,並投入運行,這一過程的C語言代碼如下:

從C語言代碼分析Linux系統是如何創建進程的

喚醒,並投入運行

到這裡,一個新的進程就被 Linux 創建完畢了。

Linux 內核有意讓新創建的子進程先運行,因為子進程常常會立即調用 exec() 函數加載新的程序到內存中運行,這樣就避免了寫時拷貝的額外開銷。如果父進程首先執行,顯然極有可能開始往地址空間寫入操作,導致拷貝動作發生。

小結

本節詳細的從C語言代碼層面分析了Linux內核創建進程的過程,可見,即使是複雜的操作系統代碼,也是通過一系列基本C語言語法和函數實現的。那麼,Linux 是如何創建線程的呢?之前我們曾經提到,Linux 系統並不特別區分進程和線程,線程其實是一種特殊的進程,Linux 是如何實現這一“特殊”過程的呢?限於篇幅,下一節再說了,敬請關注。

從C語言代碼分析Linux系統是如何創建進程的

點個贊再走吧

歡迎在評論區一起討論,質疑。文章都是手打原創,每天最淺顯的介紹C語言、linux等嵌入式開發,喜歡我的文章就關注一波吧,可以看到最新更新和之前的文章哦。

Linux中的進程簡介

從C語言源代碼分析,神祕的Linux系統是如何記錄和描述進程的?

"

相關推薦

推薦中...