最近有日子沒寫部落格了,這段時間有點事忙活一陣子,好在已經接近尾聲。也該輪到投些時間好好研究下真刀真槍的東西,幹些有意義的事。這兩天抽時間繼續往下看了看 Linux 核心和 Unix 編程的書,邊看邊琢磨,想到個關於進程在 fork 子進程或 pthread 出 lwp 時父親進程的棧段是如何處理的問題,結合 Linux 核心的說明對這個問題有了明確的理解,在此做個筆記。大家也一起研究、分享下~
曆史上來說,*nix 裡的 C 程式進程由以下幾部分組成:
- 本文段。也有叫程式碼片段的。存放著 CPU 執行的機器指令。它一般是共用、唯讀。
- 初始化資料區段。存放著程式中明確賦初值的變數。
- 非初始化資料區段。也叫 bss ,存放著 C 函數之外的變數,核心會初始它為 0 填充。
- 棧段。存放 auto 變數或函數調用傳遞的資訊。包括傳回值、實參,調用者上下文(忽然想到,閉包變數會用這個傳遞嗎?請教 guru),這些變數會存在一個 stack frame 中。
- 堆段。用於 sbrk (malloc) 等動態記憶體分配的。
這圖說明了這些段的典型的布局。在 x86 CPU 的 Linux 中棧底位於 0xc0000000 開始,該地址以上儲存的就是核心代碼了(那段線性地址直接映射到物理地址)。當我看到這裡的時候,困惑的問題是,在這種段結構中,當我們父進程產生多進程/線程的子進程時,linux 核心對這個父進程的 stack 段是怎麼處理了,來保證每個不同進程/線程中的方法調用時,用 stack 來傳遞的調用資訊不會混亂,怎麼保證競爭條件下的正確入/出棧順序?呵呵,現在看來當時的想法比較可笑了。正確理解如下所述。
*nix 有3種進程建立方式:
- fork。核心為父進程棄置站台,即子進程。傳統情況下,該子進程將獲得父進程完整複製,包括資料區段(初始化和bss 段)和堆棧段。但是,由於 fork 之後經常跟著就是 execve 系統調用,因此現在的 *nix 會使用 COW(寫時複製) 方式來 fork 子進程,也就是這些地區暫時不複製,並由核心將它們唯讀,當父/子進程對他們修改時,就將修改的部分儲存在本次修改操作的進程地址空間中,通常單位是一頁。核心為了提高效能,如果父子進程在同一顆 CPU 上的話呢(同 CPU 進程隊列),會在 TASK_RUNNING 進程鏈中將子進程放在放在父進程的前邊,這樣可減少不必要的 COW 開銷。此外還有一些其它屬性也將由子進程繼承,如實際、有效使用者/組 ID,會話 ID,工作目錄,環境,串連的共用儲存段,資源限制等等。
- lwp。輕量進程允許父子進程共用核心的在部分資料,頁表(可共用全域資料),檔案描述符表等。pthead 就是通過 lwp 實現的。
- vfork。apue2中將 vfork 單提了來,個人覺得實際和 fork 是一要的,只不過是簡版的。通過它建立的子進程能夠共用父進程記憶體位址空間,但為了避免父子混亂,子進程暫時阻塞了父進程執行,直到子進程退出父進程再在此基礎上繼續執行。
實際上看到這裡,已經能夠很清楚的解釋我上面的疑問了,呵呵。fork 方式會獨立出父子進程,vfork 會順序執行。那麼 fork/lwp 的具體細節核心是怎麼做到的呢,繼續挖。
Linux 通過 clone 系統調用來建立 lwp,而 clone、fork、vfork 都是由 do_fork 核心功能來統一處理完成的,而 do_fork 又是由 copy_process 函數來完成功能的。再看一眼上面我最初想到的問題,已經知道 fork 會出來兩個獨立的使用者進程空間,因此父子進程的棧段肯定不重複。但 lwp 怎麼保證棧段不重複的呢,就是通過 clone 系統調用的 child_stack、tls 和 flags 參數來控制的,詳細說明看文檔和核心源碼吧。Linux 中子進程的具體建立步驟說明可見《Understanding The Linux Kernel》 3th 的 P123 很詳盡有看起來也有力道,呵呵。
現在關於這個 tls 還有沒理解的地方,在 GDT 中共有 3 個 TLS ,段選擇符 0x33 - 0x43(那也就是說只允許最多 3 個線程局部資料區段),這個 clone 的 tls 參數是傳遞的什麼值,是 GDT 中 tls 的段描述符地址嗎?