《電子技術應用》
您所在的位置:首頁 > 可編程邏輯 > 其他 > Linux進程是如何創(chuàng)建出來的,?

Linux進程是如何創(chuàng)建出來的,?

2022-11-18
作者:張彥飛allen
來源:電子技術應用專欄作家 一口Linux
關鍵詞: Linux進程

  在 Linux 中,進程是我們非常熟悉的東東了,,哪怕是只寫過一天代碼的人也都用過它,。但是你確定它不是你最熟悉的陌生人?我們今天通過深度剖析進程的創(chuàng)建過程,,幫助你提高對進程的理解深度,。

  在這篇文章中,我會用 Nginx 創(chuàng)建 worker 進程的例子作為引入,,然后帶大家了解一些進程的數據結構 task_struct,,最后再帶大家看一下 fork 執(zhí)行的過程。

  學習完本文,,你將深度理解進程中的那些關鍵要素,,諸如進程地址空間、當前目錄,、父子進程關系,、進程打開的文件 fd 表、進程命名空間等,。也能學習到內核在保存已經使用的 pid 號時是如何優(yōu)化內存占用的,。我們展開今天的拆解!

  一,、Nginx 之 fork 創(chuàng)建 worker

  在 Linux 進程的創(chuàng)建中,,最核心的就是 fork 系統(tǒng)調用。不過我們先不著急介紹它,,先拿多進程服務中的一個經典例子 - Nginx,,來看看他是如何使用 fork 來創(chuàng)建 worker 的。

  Nginx 服務采用的是多進程方式來工作的,,它啟動的時候會創(chuàng)建若干個 worker 進程出來,,來響應和處理用戶請求。創(chuàng)建 worker 子進程的源碼位于 nginx 源碼的 src/os/unix/ngx_process_cycle.c 文件中,。通過循環(huán)調用 ngx_spawn_process 來創(chuàng)建 n 個 worker 出來,。

 微信截圖_20221118145602.png

  在 ngx_spawn_process 中調用 fork 來創(chuàng)建進程,創(chuàng)建成功后 Worker 進程就將進入自己的入口函數中開始工作了。

  二,、Linux 中對進程的表示

  在深入理解進程創(chuàng)建之前,,我們先來看一下進程的數據結構,。

  在 Linux 中,是用一個 task_struct 來實現 Linux 進程的(其實 Linux 線程也同樣是用 task_struct 來表示的,,這個我們以后文章單獨再說)。

  微信截圖_20221118145630.png

  我們來看看 task_struct 具體的定義,,它位于 include/linux/sched.h

  微信截圖_20221118145648.png

  2.1 進程線程狀態(tài)

  進程線程都是有狀態(tài)的,它的狀態(tài)就保存在 state 字段中,。常見的狀態(tài)中 TASK_RUNNING 表示進程線程處于就緒狀態(tài)或者是正在執(zhí)行,。TASK_INTERRUPTIBLE 表示進程線程進入了阻塞狀態(tài),。

  一個任務(進程或線程)剛創(chuàng)建出來的時候是 TASK_RUNNING 就緒狀態(tài),,等待調度器的調度。調度器執(zhí)行 schedule 后,,任務獲得 CPU 后進入  執(zhí)行進行運行。當需要等待某個事件的時候,,例如阻塞式 read 某個 socket 上的數據,,但是數據還沒有到達的時候,,任務進入 TASK_INTERRUPTIBLE 或 TASK_UNINTERRUPTIBLE 狀態(tài),任務被阻塞掉,。

  當等待的事件到達以后,例如 socket 上的數據到達了,。內核在收到數據后會查看 socket 上阻塞的等待任務隊列,,然后將之喚醒,使得任務重新進入 TASK_RUNNING 就緒狀態(tài),。任務如此往復地在各個狀態(tài)之間循環(huán),,直到退出。

  一個任務(進程或線程)的大概狀態(tài)流轉圖如下,。

  微信截圖_20221118145721.png

  全部的狀態(tài)值在 include/linux/sched.h 中進行了定義,。

  微信截圖_20221118145747.png

  2.2 進程 ID

  我們知道,每一個進程都有一個進程 id 的概念,。在 task_struct 中有兩個相關的字段,,分別是 pid 和 tgid。

 微信截圖_20221118145836.png

  其中 pid 是 Linux 為了標識每一個進程而分配給它們的唯一號碼,,稱做進程 ID 號,,簡稱 PID。對于沒有創(chuàng)建線程的進程(只包含一個主線程)來說,,這個 pid 就是進程的 PID,,tgid 和 pid 是相同的。

  微信截圖_20221118145858.png

  2.3 進程樹關系

  在 Linux 下所有的進程都是通過一棵樹來管理的,。在操作系統(tǒng)啟動的時候,,會創(chuàng)建 init 進程,接下來所有的進程都是由這個進程直接或者間接創(chuàng)建的的,。通過 pstree 命令可以查看你當前服務器上的進程樹信息,。

微信截圖_20221118150005.png

  那么,這棵進程樹就是由 task_struct 下的 parent,、children,、sibling 等字段來表示的。這幾個字段將系統(tǒng)中的所有 task 串成了一棵樹,。

  微信截圖_20221118150036.png

  2.4 進程調度優(yōu)先級

  在 task_struct 中有幾個字段是表示進程優(yōu)先級的,,在進程調度的時候會根據這幾個字段來決定優(yōu)先讓哪個任務(進程或線程)開始執(zhí)行。

  static_prio: 用來保存靜態(tài)優(yōu)先級,,可以調用 nice 系統(tǒng)直接來修改取值范圍為 100~139

  rt_priority: 用來保存實時優(yōu)先級,取值范圍為 0~99

  prio: 用來保存動態(tài)優(yōu)先級

  normal_prio: 它的值取決于靜態(tài)優(yōu)先級和調度策略

  2.5 進程地址空間

  對于用戶進程來講,,內存描述符 mm_struct( mm 代表的是 memory descriptor)是非常核心的數據結構。整個進程的虛擬地址空間部分都是由它來表示的,。

  進程在運行的時候,,在用戶態(tài)其所需要的代碼,全局變量數據,,以及 mmap 內存映射等全部都是通過 mm_struct 來進行內存查找和尋址的,。這個數據結構的定義位于 include/linux/mm_types.h 文件下。

 微信截圖_20221118150057.png

  其中 start_code,、end_code 分別指向代碼段的開始與結尾,、start_data 和 end_data 共同決定數據段的區(qū)域、start_brk 和 brk 中間是堆內存的位置,、start_stack 是用戶態(tài)堆棧的起始地址,。整個 mm_struct 和地址空間、頁表,、物理內存的關系如下圖。

  微信截圖_20221118150126.png

  在內核內存區(qū)域,可以通過直接計算得出物理內存地址,,并不需要復雜的頁表計算,。而且最重要的是所有內核進程、以及用戶進程的內核態(tài),,這部分內存都是共享的,。

  微信截圖_20221118150145.png

  另外要注意的是,mm(mm_struct)表示的是虛擬地址空間,。而對于內核線程來說,,是沒有用戶態(tài)的虛擬地址空間的。所以內核線程的 mm 的值是 null,。

  2.6 進程文件系統(tǒng)信息(當前目錄等)

  進程的文件位置等信息是由 fs_struct 來描述的,,它的定義位于 include/linux/fs_struct.h 文件中。

 微信截圖_20221118150200.png

  通過以上代碼可以看出,,在 fs_struct 中包含了兩個 path 對象,,而每個 path 中都指向了一個 struct dentry。在 Linux 內核中,,denty 結構是對一個目錄項的描述,。

  微信截圖_20221118150225.png

  拿 pwd 來舉例,該指針指向的是進程當前目錄所處的 denty 目錄項,。假如我們在 shell 進程中執(zhí)行 pwd,,或者用戶進程查找當前目錄下的配置文件的時候,都是通過訪問 pwd 這個對象,,進而找到當前目錄的 denty 的,。

  2.7 進程打開的文件信息

  每個進程用一個 files_struct 結構來記錄文件描述符的使用情況, 這個 files_struct 結構稱為用戶打開文件表,。它的定義位于 include/linux/fdtable.h,。

  注意:飛哥用的內核源碼一直是 3.10.0, 所以本文也不例外。不同版本的源碼這里稍微可能有些出入,。

 微信截圖_20221118150248.png

  在 files_struct 中,,最重要的是在 fdtable 中包含的 file **fd 這個數組。這個數組的下標就是文件描述符,,其中 0,、1、2 三個描述符總是默認分配給標準輸入,、標準輸出和標準錯誤,。這就是你在 shell 命令中經常看到的 2>&1 的由來,。這幾個字符的含義就是把標準錯誤也一并打到標準輸出中來,。

  微信截圖_20221118150318.png

  在數組元素中記錄了當前進程打開的每一個文件的指針,。這個文件是 Linux 中抽象的文件,可能是真的磁盤上的文件,,也可能是一個 socket,。

  2.8 namespaces

  在 Linux 中,namespace 是用來隔離內核資源的方式,。通過 namespace 可以讓一些進程只能看到與自己相關的一部分資源,,而另外一些進程也只能看到與它們自己相關的資源,這兩撥進程根本就感覺不到對方的存在,。

  具體的實現方式是把一個或多個進程的相關資源指定在同一個 namespace 中,,而進程究竟是屬于哪個 namespace,都是在 task_struct 中由 *nsproxy 指針表明了這個歸屬關系,。

微信截圖_20221118150345.png

  命名空間包括PID命名空間,、掛載點命名空間、網絡命名空間等多個,。飛哥在咱們「開發(fā)內功修煉」前面的一篇文章《動手實驗+源碼分析,,徹底弄懂Linux網絡命名空間》這一文中詳細介紹過網絡命名空間,感興趣的同學可以詳細閱讀,。

  三,、解密 fork 系統(tǒng)調用

  前面我們看了 Nginx 使用 fork 來創(chuàng)建 worker 進程,也了解了進程的數據結構 task_struct ,,我們再來看看 fork 系統(tǒng)調用的內部邏輯,。

  這個 fork 在內核中是以一個系統(tǒng)調用來實現的,它的內核入口是在 kernel/fork.c 下,。

 微信截圖_20221118150411.png

  這里注意下調用 do_fork 時傳入的第一個參數,,這個參數是一個 flag 選項。它可以傳入的值包括 CLONE_VM,、CLONE_FS 和 CLONE_FILES 等等很多,,但是這里只傳了一個 SIGCHLD(子進程在終止后發(fā)送 SIGCHLD 信號通知父進程),并沒有傳 CLONE_FS 等其它 flag,。

  微信截圖_20221118150421.png

  在 do_fork 的實現中,,核心是一個 copy_process 函數,它以拷貝父進程的方式來生成一個新的 task_struct 出來,。

 微信截圖_20221118150435.png

  在創(chuàng)建完畢后,,調用 wake_up_new_task 將新創(chuàng)建的任務添加到就緒隊列中,等待調度器調度執(zhí)行,。

  copy_process 的代碼很長,,我對其進行了一定程度的精簡,參加下面的代碼,。

  微信截圖_20221118150457.png

  可見,,copy_process 先是復制了一個新的 task_struct 出來,,然后調用 copy_xxx 系列的函數對 task_struct 中的各種核心對象進行拷貝處理,還申請了 pid,。接下來我們分小節(jié)來查看該函數的每一個細節(jié),。

  3.1 復制進程 task_struct 結構體

  注意一下,,上面調用 dup_task_struct 時傳入的參數是 current,,它表示的是當前進程。在 dup_task_struct 里,,會申請一個新的 task_struct 內核對象,,然后將當前進程復制給它。需要注意的是,,這次拷貝只會拷貝 task_struct 結構體本身,,它內部包含的 mm_struct 等成員只是復制了指針,仍然指向和 current 相同的對象,。

微信截圖_20221118150548.png

  

  3.2 拷貝 files_struct

  由于進程之間都是獨立的,,所以創(chuàng)建出來的新進程需要拷貝一份獨立的 files 成員出來。

微信截圖_20221118150653.png

  我們看 copy_files 是如何申請和拷貝 files 成員的,。

  微信截圖_20221118150703.png

  看上面代碼中判斷了是否有 CLONE_FILES 標記,,如果有的話就不執(zhí)行 dup_fd 函數了,增加個引用計數就返回了,。前面我們說了,,do_fork 被調用時并沒有傳這個標記。所以還是會執(zhí)行到 dup_fd 函數:

 微信截圖_20221118150725.png

  這個函數就是到內核中申請一塊內存出來,,保存 files_struct 使用,。然后對新的 files_struct 進行各種初始化和拷貝。至此,,新進程有了自己獨立的 files 成員了,。

  3.3 拷貝 fs_struct

  同樣,新進程也需要一份獨立的文件系統(tǒng)信息 - fs_struct 成員的,。

微信截圖_20221118150753.png

  在創(chuàng)建進程的時候,,沒有傳遞 CLONE_FS 這個標志,所會進入到 copy_fs_struct 函數中申請新的 fs_struct 并進行賦值,。

  微信截圖_20221118150808.png

  3.4 拷貝 mm_struct

  前面我們說過,,對于進程來講,地址空間是一個非常重要的數據結構,。而且進程之間地址空間也必須是要隔離的,,所以還會新建一個地址空間。

 微信截圖_20221118150825.png

  do_fork 被調用時也沒有傳 CLONE_VM,,所以會調用 dup_mm 申請一個新的地址空間出來,。

  微信截圖_20221118150845.png

  在 dup_mm 中,,通過 allocate_mm 申請了新的 mm_struct,而且還將當前進程地址空間 current->mm 拷貝到新的 mm_struct 對象里了,。

  地址空間是進程線程最核心的東西,,每個進程都有獨立的地址空間

  3.5 拷貝進程的命名空間 nsproxy

  在創(chuàng)建進程或線程的時候,還可以讓內核幫我們創(chuàng)建獨立的命名空間,。在默認情況下,,創(chuàng)建進程沒有指定命名空間相關的標記,因此也不會創(chuàng)建,。新舊進程仍然復用同一套命名空間對象,。

微信截圖_20221118150906.png

  注意下,在調用 alloc_pid 的時候,,其參數傳遞的是新進程的 pid namespace,。我們來深看一下 alloc_pid 的執(zhí)行邏輯。

 微信截圖_20221118150926.png

  這里的 PID 并不是一個整數,,而是一個結構體,,所以先試用 kmem_cache_alloc 把它申請出來。接下來調用 alloc_pidmap 到 pid 命名空間中申請一個 pid 號出來,,申請完后賦值記錄,。

  回顧我們開篇提到的一個問題:操作系統(tǒng)是如何記錄使用過的進程號的?在 Linux 內部,,為了節(jié)約內存,,進程號是通過 bitmap 來管理的。

  微信截圖_20221118150942.png

  在每一個 pid 命名空間內部,,會有一個或者多個頁面來作為 bitmap,。其中每一個 bit 位(注意是 bit 位,不是字節(jié))的 0 或者 1 的狀態(tài)來表示當前序號的 pid 是否被占用,。

 微信截圖_20221118150957.png

  在各種語言中,,一般一個 int 都是 4 個字節(jié),換算成 bit 就是 32 bit,。而使用這種 bitmap 的思想的話,,只需要一個 bit 就可以表示一個整數,相當的節(jié)約內存,。所以,,在很多超大規(guī)模數據處理中都會用到這種思想來進行優(yōu)化內存占用的。

  3.7 進入就緒隊列

  當 copy_process 執(zhí)行完畢的時候,,表示新進程的一個新的 task_struct 對象就創(chuàng)建出來了,。接下來內核會調用 wake_up_new_task 將這個新創(chuàng)建出來的子進程添加到就緒隊列中等待調度。

 微信截圖_20221118151021.png

  等操作系統(tǒng)真正調度開始的時候,,子進程中的代碼就可以真正開始執(zhí)行了,。

  四,、總結

  在這篇文章中,我用 Nginx 創(chuàng)建 worker 進程的例子作為引入,,然后帶大家了解一些進程的數據結構 task_struct,,最后又帶大家看一下 fork 執(zhí)行的過程。

  在 fork 創(chuàng)建進程的時候,,地址空間 mm_struct,、掛載點 fs_struct、打開文件列表 files_struct 都要是獨立擁有的,,所以都去申請內存并初始化了它們,。但由于今天我們的例子父子進程是同一個命名空間,所以 nsproxy 還仍然是共用的,。

  微信截圖_20221118151037.png

  其中 mm_struct 是一個非常核心的數據結構,用戶進程的虛擬地址空間就是用它來表示的,。對于內核線程來講,,不需要虛擬地址空間,所以 mm 成員的值為 null,。

  另外還學到了內核是用 bitmap 來管理使用和為使用的 pid 號的,,這樣做的好處是極大地節(jié)約了內存開銷。而且由于數據存儲的足夠緊湊,,遍歷起來也是非常的快,。一方面原因是數據小,加載起來快,。另外一方面是會加大提高 CPU 緩存的命中率,,訪問非常快,。

  今天的進程創(chuàng)建過程就學習完了,。不過細心的同學可能發(fā)現了,我們這里只介紹了子進程的調用,。但是對于 Nginx 主進程如何加載起來執(zhí)行的還沒有講到,。我們將來還會展開敘述,敬請期待,!

  更多信息可以來這里獲取==>>電子技術應用-AET<<

微信圖片_20210517164139.jpg

本站內容除特別聲明的原創(chuàng)文章之外,,轉載內容只為傳遞更多信息,并不代表本網站贊同其觀點,。轉載的所有的文章,、圖片、音/視頻文件等資料的版權歸版權所有權人所有,。本站采用的非本站原創(chuàng)文章及圖片等內容無法一一聯系確認版權者,。如涉及作品內容,、版權和其它問題,請及時通過電子郵件或電話通知我們,,以便迅速采取適當措施,,避免給雙方造成不必要的經濟損失。聯系電話:010-82306118,;郵箱:[email protected],。