通江網(wǎng)站建設國際最新新聞熱點事件
Linux 內(nèi)核系列文章
Linux 內(nèi)核設計與實現(xiàn)
深入理解 Linux 內(nèi)核
深入理解 Linux 內(nèi)核(二)
Linux 設備驅動程序
Linux設備驅動開發(fā)詳解
文章目錄
- Linux 內(nèi)核系列文章
- 前言
- 一、緒論
- 二、內(nèi)存尋址
- 1、內(nèi)存地址
- 2、硬件中的分段
- (1)段選擇符
- 3、Linux 中的分段
- (1)Linux GDT
- (2)Linux LDT
- 4、硬件中的分頁
- 5、Linux 中的分頁
- (1)進程頁表
- (2)內(nèi)核頁表
- (3)臨時內(nèi)核頁表
- (4)當 RAM 小于 896MB時的最終內(nèi)核頁表
- (5)當 RAM 大小在 896MB 和 4096MB 之間時的最終內(nèi)核頁表
- (6)當 RAM 大于 4096MB 時的最終內(nèi)核頁表
- (7)固定映射的線性地址
- (8)處理硬件高速緩存和 TLB
- 三、進程
- 1、進程、輕量級進程和線程
- 2、進程描述符
- (1)標識一個進程
- (2)進程描述符處理
- (3)標識當前進程
- 3、進程切換
- (1)switch_to 宏
- (a)分析
- (2)__switch_to() 函數(shù)
- (a)分析
- 4、創(chuàng)建進程
- (1)do_fork() 函數(shù)
- (2)copy_process() 函數(shù)
- (a)分析
- (b)do_fork 之后
- (3)內(nèi)核線程
- (a)創(chuàng)建一個內(nèi)核線程
- (b)進程 0
- (c)進程 1
- (d)其他內(nèi)核線程
- (4)撤消進程
- (5)進程終止
- (a)do_group_exit() 函數(shù)
- 描述
- (b)do_exit() 函數(shù)
- 源碼
- 描述
- 5、進程刪除
- 四、中斷和異常
- 1、中斷和異常
- (1)IRQ 和中斷
- (2)高級可編程中斷控制器
- (3)異常
- (4)中斷描述符表
- (5)中斷和異常的硬件處理
- 2、中斷和異常處理程序的嵌套執(zhí)行
- (1)中斷門、陷阱門及系統(tǒng)門
- 3、異常處理
- 4、中斷處理
- (1)I/O 中斷處理
- (a)中斷向量
- (b)IRQ 數(shù)據(jù)結構
- 五、內(nèi)核同步
- 1、同步原語
- (1)每CPU 變量
- (2)原子操作
- (3)優(yōu)化和內(nèi)存屏障
- (a)優(yōu)化屏障(optimization barrier)
- (b)內(nèi)存屏障(memory barrier)
- (4)自旋鎖
- (a)具有內(nèi)核搶占的 spin_lock 宏
- (b)非搶占式內(nèi)核中的 spin_lock 宏
- (c)spin_unlock 宏
- (5)順序鎖
- (6)讀 - 拷貝 - 更新(RCU)
- (7)信號量
前言
??本文主要用來摘錄《深入理解 Linux 內(nèi)核》一書中學習知識點,本書基于 Linux 2.6.11 版本,源代碼摘錄基于 Linux 2.6.34 ,兩者之間可能有些出入。
一、緒論
二、內(nèi)存尋址
1、內(nèi)存地址
??可參考 ? 1、內(nèi)存尋址
2、硬件中的分段
??可參考 ? 五、分段機制
(1)段選擇符
??80x86 中有 6 個段寄存器,分別為 cs,ss,ds,es,fs 和 gs。 6 個寄存器中 3 個有專門的用途:可參考 ? 3、段選擇符
- cs 代碼段寄存器,指向包含程序指令的段。
- ss 棧段寄存器,指向包含當前程序棧的段。
- ds 數(shù)據(jù)段寄存器,指向包含靜態(tài)數(shù)據(jù)或者全局數(shù)據(jù)段。
??其它 3 個段寄存器做一般用途,可以指向任意的數(shù)據(jù)段。
3、Linux 中的分段
??分段可以給每一個進程分配不同的線性地址空間,而分頁可以把同一線性地址空間映射到不同的物理空間。與分段相比,Linux 更喜歡使用分頁方式,因為:
- 當所有進程使用相同的段寄存器值時,內(nèi)存管理變得更簡單,也就是說它們能共享同樣的一組線性地址。
- Linux 設計目標之一是可以把它移植到絕大多數(shù)流行的處理器平臺上。然而,如 RISC 體系結構對分段的支持很有限。
??2.6 版的 Linux 只有在 80x86 結構下才需要使用分段。
??運行在用戶態(tài)的所有 Linux 進程都使用一對相同的段來對指令和數(shù)據(jù)尋址。這兩個段就是所謂的用戶代碼段和用戶數(shù)據(jù)段。類似地,運行在內(nèi)核態(tài)的所有 Linux 進程都使用一對相同的段對指令和數(shù)據(jù)尋址:它們分別叫做內(nèi)核代碼段和內(nèi)核數(shù)據(jù)段。下圖顯示了這四個重要段的段描述符字段的值??蓞⒖?? 4、段描述符 一文。
??相應的段選擇符由宏 __USER_CS,__USER_DS,__KERNEL_CS 和 __KERNEL_DS 分別定義。例如,為了對內(nèi)核代碼段尋址,內(nèi)核只需要把 __KERNEL_CS 宏產(chǎn)生的值裝進 cs 段寄存器即可。
??注意,與段相關的線性地址從 0 開始,達到 232 - 1 的尋址限長。這就意味著在用戶態(tài)或內(nèi)核態(tài)下的所有進程可以使用相同的邏輯地址。
??所有段都從 0x00000000 開始,這可以得出另一個重要結論,那就是在 Linux 下邏輯地址與線性地址是一致的,即邏輯地址的偏移字段的值與相應的線性地址的值總是一致的。
??如前所述,CPU 的當前特權級(CPL)反映了進程是在用戶態(tài)還是內(nèi)核態(tài),并由存放在 CS 寄存器中的段選擇符的 RPL 字段指定。只要當前特權級被改變,一些段寄存器必須相應地更新。例如,當 CPL=3 時(用戶態(tài)),ds 寄存器必須含有用戶數(shù)據(jù)段的段選擇符,而當 CPL=0 時,ds 寄存器必須含有內(nèi)核數(shù)據(jù)段的段選擇符。
??類似的情況也出現(xiàn)在 ss 寄存器中。當 CPL 為 3 時,它必須指向一個用戶數(shù)據(jù)段中的用戶棧,而當 CPL 為 0 時,它必須指向內(nèi)核數(shù)據(jù)段中的一個內(nèi)核棧。當從用戶態(tài)切換到內(nèi)核態(tài)時,Linux 總是確保 ss 寄存器裝有內(nèi)核數(shù)據(jù)段的段選擇符。
??當對指向指令或者數(shù)據(jù)結構的指針進行保存時,內(nèi)核根本不需要為其設置邏輯地址的段選擇符,因為 cs 寄存器就含有當前的段選擇等。例如,當內(nèi)核調用一個函數(shù)時,它執(zhí)行一條 call 匯編語言指令,該指令僅指定其邏輯地址的偏移量部分,而段選擇符不用設置,它已經(jīng)隱含在 cs 寄存器中了。因為"在內(nèi)核態(tài)執(zhí)行"的段只有一種,叫做代碼段,由宏 __KERNEL_CS 定義,所以只要當 CPU 切換到內(nèi)核態(tài)時將 __KERNEL_CS 裝載進 cs 就足夠了。同樣的道理也適用于指向內(nèi)核數(shù)據(jù)結構的指針(隱含地使用 ds 寄存器)以及指向用戶數(shù)據(jù)結構的指針(內(nèi)核顯式地使用 es 寄存器)。
??除了剛才描述的 4 個段以外,Linux 還使用了其他幾個專門的段。我們將在下一節(jié)講述 Linux GDT 的時候介紹它們。
(1)Linux GDT
??在單處理器系統(tǒng)中只有一個 GDT,而在多處理器系統(tǒng)中每個 CPU 對應一個 GDT。 所有的 GDT 都存放在 cpu_gdt_table 數(shù)組中,而所有 GDT 的地址和它們的大小(當初始化 gdtr 寄存器時使用)被存放在 cpu_gdt_descr 數(shù)組中。如果你到源代碼索引中查看,可以看到這些符號都在文件 arch/i386/kernel/head.S 中被定義。本書中的每一個宏、函數(shù)和其他符號都被列在源代碼索引中,所以能在源代碼中很方便地找到它們。
??圖 2-6 是 GDT 的布局示意圖。每個 GDT 包含 18 個段描述符和 14 個空的,未使用的,或保留的項。插入未使用的項的目的是為了使經(jīng)常一起訪問的描述符能夠處于同一個 32 字節(jié)的硬件高速緩存行中(參見本章 后面"硬件高速緩存"一節(jié))。
??每一個 GDT 中包含的 18 個段描述符指向下列的段:
- 用戶態(tài)和內(nèi)核態(tài)下的代碼段和數(shù)據(jù)段共 4 個(參見前面一節(jié))。
- 任務狀態(tài)段(TSS),每個處理器有 1 個。每個 TSS 相應的線性地址空間都是內(nèi)核數(shù)據(jù)段相應線性地址空間的一個小子集。所有的任務狀態(tài)段都順序地存放在 init_tss 數(shù)組中,值得特別說明的是,第 n 個 CPU 的 TSS 描述符的 Base 字段指向 init_tss 數(shù)組的第 n 個元素。G(粒度)標志被清 0 ,而 Limit 字段置為 0xeb,因為 TSS 段是 236 字節(jié)長。Type 字段置為 9 或 11(可用的 32 位 TSS),且 DPL 置為 0,因為不允許用戶態(tài)下的進程訪問 TSS 段。在第三章"任務狀態(tài)段"一節(jié)你可以找到 Linux 是如何使用 TSS 的細節(jié)。參考 ==> 3.1 任務狀態(tài)段
- 1 個包括缺省局部描述符表的段,這個段通常是被所有進程共享的段 (參見下一節(jié))。
- 3 個局部線程存儲(Thread-Local Storage,TLS) 段:這種機制允許多線程應用程序使用最多 3 個局部于線程的數(shù)據(jù)段。系統(tǒng)調用 set_thread_area() 和 get_thread_area() 分別為正在執(zhí)行的進程創(chuàng)建和撤消一個 TLS 段。
- 與高級電源管理(AMP)相關的 3 個段:由于 BIOS 代碼使用段,所以當 Linux APM 驅動程序調用 BIOS 函數(shù)來獲取或者設置 APM 設備的狀態(tài)時,就可以使用自定義的代碼段和數(shù)據(jù)段。
- 與支持即插即用(PnP)功能的 BIOS 服務程序相關的 5 個段:在前一種情況下,就像前述與 AMP 相關的 3 個段的情況一樣,由于 BIOS 例程使用段,所以當 Linux 的 PnP 設備驅動程序調用 BIOS 函數(shù)來檢測 PnP 設備使用的資源時,就可以使用自定義的代碼段和數(shù)據(jù)段。
- 被內(nèi)核用來處理"雙重錯誤"(譯注 1)異常的特殊 TSS 段(參見第四章的"異常"一節(jié))。
??如前所述,系統(tǒng)中每個處理器都有一個 GDT 副本。除少數(shù)幾種情況以外,所有 GDT 的副本都存放相同的表項。首先,每個處理器都有它自己的 TSS 段,因此其對應的 GDT 項不同。其次,GDT 中只有少數(shù)項可能依賴于 CPU 正在執(zhí)行的進程(LDT 和 TLS 段描述符)。最后,在某些情況下,處理器可能臨時修改 GDT 副本里的某個項,例如,當調用 APM 的 BIOS 例程時就會發(fā)生這種情況。
(2)Linux LDT
??大多數(shù)用戶態(tài)下的 Linux 程序不使用局部描述符表,這樣內(nèi)核就定義了一個缺省的 LDT 供大多數(shù)進程共享。缺省的局部描述符表存放在 default_ldt 數(shù)組中。它包含 5 個項,但內(nèi)核僅僅有效地使用了其中的兩個項:用于 iBCS 執(zhí)行文件的調用門和 Solaris/x86 可執(zhí)行文件的調用門(參見第二十章的"執(zhí)行域"一節(jié))。調用門是 80x86 微處理器提供的一種機制,用于在調用預定義函數(shù)時改變 CPU 的特權級,由于我們不會再更深入地討論它們,所以請參考 Intel 文檔以獲取更多詳情。
??在某些情況下,進程仍然需要創(chuàng)建自己的局部描述符表。這對有些應用程序很有用,像 Wine 那樣的程序,它們執(zhí)行面向段的微軟 Windows 應用程序。modify_ldt() 系統(tǒng)調用允許進程創(chuàng)建自己的局部描述符表。
??任何被 modify_ldt() 創(chuàng)建的自定義局部描述符表仍然需要它自己的段。當處理器開始執(zhí)行擁有自定義局部描述符表的進程時,該 CPU 的 GDT 副本中的 LDT 表項相應地就被修改了。
??用戶態(tài)下的程序同樣也利用 modify_ldt() 來分配新的段,但內(nèi)核卻從不使用這些段,它也不需要了解相應的段描述符,因為這些段描述符被包含在進程自定義的局部描述符表中了。
4、硬件中的分頁
??參考 ? 六、分頁機制
5、Linux 中的分頁
??Linux 采用了一種同時適用于 32 位和 64 位系統(tǒng)的普通分頁模型。正像前面 “64 位系統(tǒng)中的分頁” 一節(jié)所解釋的那樣,兩級頁表對 32 位系統(tǒng)來說已經(jīng)足夠了,但 64 位系統(tǒng)需要更多數(shù)量的分頁級別。直到 2.6.10 版本,Linux 采用三級分頁的模型。從 2.6.11 版本開始,采用了四級分頁模型(注 5)。圖 2-12 中展示的 4 種頁表分別被為:
- 頁全局目錄(Page Global Directory)
- 頁上級目錄(Page Upper Directory)
- 頁中間目錄(Page Middle Directory)
- 頁表(Page Table)
??頁全局目錄包含若干頁上級目錄的地址,頁上級目錄又依次包含若干頁中間目錄的地址,而頁中間目錄又包含若干頁表的地址。每一個頁表項指向一個頁框。線性地址因此被分成五個部分。圖 2-12 沒有顯示位數(shù),因為每一部分的大小與具體的計算機體系結構有關。
??對于沒有啟用物理地址擴展的 32 位系統(tǒng),兩級頁表已經(jīng)足夠了。Linux 通過使 “頁上級目錄” 位和 “頁中間目錄” 位全為 0,從根本上取消了頁上級目錄和頁中間目錄字段。不過,頁上級目錄和頁中間目錄在指針序列中的位置被保留,以便同樣的代碼在 32 位系統(tǒng)和 64 位系統(tǒng)下都能使用。內(nèi)核為頁上級目錄和頁中間目錄保留了一個位置,這是通過把它們的頁目錄項數(shù)設置為 1,并把這兩個目錄項映射到頁全局目錄的一個適當?shù)哪夸涰椂鴮崿F(xiàn)的。
??啟用了物理地址擴展的 32 位系統(tǒng)使用了三級頁表。Linux 的頁全局目錄對應 80x86 的頁目錄指針表(PDPT),取消了頁上級目錄,頁中間目錄對應 80x86 的頁目錄,Linux 的頁表對應 80x86 的頁表。
??最后,64 位系統(tǒng)使用三級還是四級分頁取決于硬件對線性地址的位的劃分(見表 2-4)。
??Linux 的進程處理很大程度上依賴于分頁。事實上,線性地址到物理地址的自動轉換使下面的設計目標變得可行:
- 給每一個進程分配一塊不同的物理地址空間,這確保了可以有效地防止尋址錯誤。
- 區(qū)別頁(即一組數(shù)據(jù))和頁框(即主存中的物理地址)之不同。這就允許存放在某個頁框中的一個頁,然后保存到磁盤上,以后重新裝入這同一頁時又可以被裝在不同的頁框中。這就是虛擬內(nèi)存機制的基本要素(參見第十七章)。
??在本章剩余的部分,為了具體起見,我們將涉及 80x86 處理器使用的分頁機制。
??我們將在第九章看到,每一個進程有它自己的頁全局目錄和自己的頁表集。當發(fā)生進程切換時(參見第三章 “進程切換” 一節(jié)),Linux 把 CR3 控制寄存器的內(nèi)容保存在前一個執(zhí)行進程的描述符中,然后把下一個要執(zhí)行進程的描述符的值裝入 CR3 寄存器中。因此,當新進程重新開始在 CPU 上執(zhí)行時,分頁單元指向一組正確的頁表。
(1)進程頁表
??進程的線性地址空間分成兩部分:
- 從 0x00000000 到 0xbfffffff 的線性地址,無論進程運行在用戶態(tài)還是內(nèi)核態(tài)都可以尋址。
- 從 0xc0000000 到 0xffffffff 的線性地址,只有內(nèi)核態(tài)的進程才能尋址。
??當進程運行在用戶態(tài)時,它產(chǎn)生的線性地址小于 0xc0000000 ;當進程運行在內(nèi)核態(tài)時,它執(zhí)行內(nèi)核代碼,所產(chǎn)生的地址大于等于 0xc0000000 。但是,在某些情況下,內(nèi)核為了檢索或存放數(shù)據(jù)必須訪問用戶態(tài)線性地址空間。
(2)內(nèi)核頁表
??內(nèi)核維持著一組自己使用的頁表,駐留在所謂的主內(nèi)核頁全局目錄(master kernel Page Global Directory)中。系統(tǒng)初始化后,這組頁表還從未被任何進程或任何內(nèi)核線程直接使用;更確切地說,主內(nèi)核頁全局目錄的最高目錄項部分作為參考模型,為系統(tǒng)中每個普通進程對應的頁全局目錄項提供參考模型。
??我們在第八章 “非連續(xù)內(nèi)存區(qū)的線性地址” 一節(jié)將會解釋,內(nèi)核如何確保對主內(nèi)核頁全局目錄的修改能傳遞到由進程實際使用的頁全局目錄中。
??我們現(xiàn)在描述內(nèi)核如何初始化自己的頁表。這個過程分為兩個階段。事實上,內(nèi)核映像剛剛被裝入內(nèi)存后,CPU 仍然運行于實模式,所以分頁功能沒有被啟用。
??第一個階段,內(nèi)核創(chuàng)建一個有限的地址空間,包括內(nèi)核的代碼段和數(shù)據(jù)段、初始頁表和用于存放動態(tài)數(shù)據(jù)結構的共 128KB 大小的空間。這個最小限度的地址空間僅夠將內(nèi)核裝入 RAM 和對其初始化的核心數(shù)據(jù)結構。
??第二個階段,內(nèi)核充分利用剩余的 RAM 并適當?shù)亟⒎猪摫怼?/p>
(3)臨時內(nèi)核頁表
??臨時頁全局目錄是在內(nèi)核編譯過程中靜態(tài)地初始化的,而臨時頁表是由 startup_32() 匯編語言函數(shù)(定義于 arch/i386/kernel/head.S)初始化的。我們不再過多提及頁上級目錄和頁中間目錄,因為它們相當于頁全局目錄項。在這個階段 PAE 支持并未激活。
??臨時頁全局目錄放在 swapper_pg_dir 變量中。臨時頁表在 pg0 變量處開始存放,緊接在內(nèi)核未初始化的數(shù)據(jù)段(圖 2-13 中的 _end 符號)后面。為簡單起見,我們假設內(nèi)核使用的段、臨時頁表和 128KB 的內(nèi)存范圍能容納于 RAM 前 8MB 空間里。為了映射 RAM 前 8MB 的空間,需要用到兩個頁表。
??分頁第一個階段的目標是允許在實模式下和保護模式下都能很容易地對這 8MB 尋址。因此,內(nèi)核必須創(chuàng)建一個映射,把從 0x00000000 到 0x007fffff 的線性地址和從 0xc0000000 到 0xc07fffff 的線性地址映射到從 0x00000000 到 0x007fffff 的物理地址。換句話說,內(nèi)核在初始化的第一階段,可以通過與物理地址相同的線性地址或者通過從 0xc0000000 開始的 8MB 線性地址對 RAM 的前 8MB 進行尋址。
??內(nèi)核通過把 swapper_pg_dir 所有項都填充為 0 來創(chuàng)建期望的映射,不過,0,1,0x300(十進制 768)和 0x301(十進制 769)這四項除外:后兩項包含了從 0xc0000000 到 0xc07fffff 間的所有線性地址。0、1、0x300 和 0x301 按以下方式初始化:
-
0 項和 0x300 項的地址字段置為 pg0 的物理地址,而 1 項和 0x301 項的地址字段置為緊隨 pg0 后的頁框的物理地址。
-
把這四個項中的 Present、Read/Write 和 User/Supervisor 標志置位。
-
把這四個項中的 Accessed、Dirty、PCD、PWD 和 Page Size 標志清 0。
??匯編語言函數(shù) startup_32() 也啟用分頁單元,通過向 cr3 控制寄存器裝入 swapper_pg_dir 的地址及設置 cr0 控制寄存器的 PG 標志來達到這一目的。下面是等價的代碼片段:
movl $swapper_pg_dir-0xc0000000, %eax
movl %eax, %cr3 # /* 設置頁表指針*****/
movl %cr0, %eax
orl $0x80000000, %eax
movl %eax, %cr0 # /*-----設置分頁(PG)位 * /
(4)當 RAM 小于 896MB時的最終內(nèi)核頁表
??由內(nèi)核頁表所提供的最終映射必須把從 0xc0000000 開始的線性地址轉化為從 0 開始的物理地址。
// arch/x86/include/asm/page.h
#define __pa(x) __phys_addr((unsigned long)(x))
#define __va(x) ((void *)((unsigned long)(x)+PAGE_OFFSET))// arch/x86/mm/physaddr.c 32位系統(tǒng)下的實現(xiàn)
unsigned long __phys_addr(unsigned long x)
{/* VMALLOC_* aren't constants */VIRTUAL_BUG_ON(x < PAGE_OFFSET);VIRTUAL_BUG_ON(__vmalloc_start_set && is_vmalloc_addr((void *) x));return x - PAGE_OFFSET;
}
EXPORT_SYMBOL(__phys_addr);
??宏 __pa 用于把從 PAGE_OFFSET 開始的線性地址轉換成相應的物理地址,而去 __va 做相反的轉化。
??主內(nèi)核頁全局目錄仍然保存在 swapper_pg_dir 變量中。它由 paging_init() 函數(shù)初始化。該函數(shù)進行如下操作:
- 調用 pagetable_init() 適當?shù)亟㈨摫眄棥?/li>
- 把 swapper_pg_dir 的物理地址寫入 cr3 控制寄存器中。
- 如果 CPU 支持 PAE 并且如果內(nèi)核編譯時支持 PAE,則將 cr4 控制寄存器的 PAE 標志置位。
- 調用 __flush_tlb_all() 使 TLB 的所有項無效。
??pagetable_init() 執(zhí)行的操作既依賴于現(xiàn)有 RAM 的容量,也依賴于 CPU 模型。讓我們從最簡單的情況開始。我們的計算機有小于 896MB(注 7)的 RAM,32 位物理地址足以對所有可用 RAM 進行尋址,因而沒有必要激活 PAE 機制 [參見前面 “物理地址擴展(PAE)分頁機制” 一節(jié)]。
??swapper_pg_dir 頁全局目錄由如下等價的循環(huán)重新初始化:
pgd = swapper_pg_dir + pgd_index(PAGE_OFFSET); /* 768 */
phys_addr = 0x00000000;
while (phys_addr < (max_low_pfn * PAGE_SIZE)) {pmd = one_md_table_init(pgd); /* 返回 pgd */set_pmd(pmd, __pmd(phys_addr | pgprot_val(__pgprot(0xle3))));/* Ox1e3 == Present, Accessed, Dirty, Read/Write,Page Size, Global */phys_addr += PTRS_PER_PTE * PAGE_SIZE; /* 0x400000 */++pgd;
}
??我們假定 CPU 是支持 4MB 頁和 “全局(global)” TLB 表項的最新 80x86 微處理器。注意如果頁全局目錄項對應的是 0xc0000000 之上的線性地址,則把所有這些項的 User/Supervisor 標志清 0,由此拒絕用戶態(tài)進程訪問內(nèi)核地址空間。還要注意 Page Size 被置位使得內(nèi)核可以通過使用大型頁來對 RAM 進行尋址(參見本章先前的 “擴展分頁” 一節(jié))。
??由 startup_32() 函數(shù)創(chuàng)建的物理內(nèi)存前 8MB 的恒等映射用來完成內(nèi)核的初始化階段。當這種映射不再必要時,內(nèi)核調用 zap_low_mappings() 函數(shù)清除對應的頁表項。
??實際上,這種描述并未說明全部事實。我們將在后面 “固定映射的線性地址” 一節(jié)看到,內(nèi)核也調整與 “固定映射的線性地址” 對應的頁表項。
??注 7:線性地址的最高 128MB 留給幾種映射去用(參見本章后面 “固定映射的線性地址” 一節(jié)和第八章 “非連續(xù)內(nèi)存區(qū)的線性地址” 一節(jié))。因此此映射 RAM 所剩空間為 1GB - 128MB= 896MB 。
(5)當 RAM 大小在 896MB 和 4096MB 之間時的最終內(nèi)核頁表
??在這種情況下,并不把 RAM 全部映射到內(nèi)核地址空間。Linux 在初始化階段可以做的最好的事是把一個具有 896MB 的 RAM 窗口(window)映射到內(nèi)核線性地址空間。如果一個程序需要對現(xiàn)有 RAM 的其余部分尋址,那就必須把某些其他的線性地址間隔映射到所需的 RAM。這意味著修改某些頁表項的值。我們將在第八章討論這種動態(tài)重映射是如何進行的。
??內(nèi)核使用與前一種情況相同的代碼來初始化頁全局目錄。
(6)當 RAM 大于 4096MB 時的最終內(nèi)核頁表
??現(xiàn)在讓我們考慮 RAM 大于 4GB 計算機的內(nèi)核頁表初始化,更確切地說,我們處理以下發(fā)生的情況:
- CPU 模型支持物理地址擴展(PAE)
- RAM 容量大于 4GB
- 內(nèi)核以 PAE 支持來編譯
??盡管 PAE 處理 36 位物理地址,但是線性地址依然是 32 位地址。如前所述,Linux 映射一個 896MB 的 RAM 窗口到內(nèi)核線性地址空間;剩余 RAM 留著不映射,并由動態(tài)重映射來處理,第八章將對此進行描述。與前一種情況的主要差異是使用三級分頁模型,因此頁全局目錄按以下循環(huán)代碼來初始化:
pgd_idx = pgd_index(PAGE_OFFSET); /* 3 */
for (i=0; i<pgd_idx; i++)set_pgd(swapper_pg_dir + i, __pgd(__pa(empty_zero_page) + 0x001));/* 0x001 == Present. */
pgd = swapper_pg_dir + pgd_idx;
phys_addr = 0x00000000;
for (; i<PTRS_PER_PGD; ++i, ++pgd) {pmd = (pmd_t *) alloc_bootmem_low_pages(PAGE_SIZE);set_pgd(pgd, __pgd(__pa(pmd) | 0x001)): /* 0x001 == Present */if (phys_addr < max_low_pfn * PAGE_SIZE) {for (j=0; j < PTRS_PER_PMD /* 512 */&& phys_addr < max_low_pfn*PAGE_SIZE; ++j) {set_pmd(pmd, __pmd(phys_addr |pgprot_val(__pgprot(0x1e3))));/* 0x1e3 == Present, Accessed, Dirty, Read/Write,Page Size, Global */phys_addr += PTRS_PER_PTE * PAGE_SIZE; /* 0x200000 */}}
}
swapper_pg_dir[0] = swapper_pg_dir[pgd_idx];
??頁全局目錄中的前三項與用戶線性地址空間相對應,內(nèi)核用一個空頁(empty_zero_page)的地址對這三項進行初始化。第四項用頁中間目錄(pmd)的地址初始化,該頁中間目錄是通過調用 alloc_bootmem_low_pages() 分配的。頁中間目錄中的前 448 項(有 512 項,但后 64 項留給非連續(xù)內(nèi)存分配;參見第八章的 “非連續(xù)內(nèi)存區(qū)管理” 一節(jié))用 RAM 前 896MB 的物理地址填充。
??注意,支持 PAE 的所有 CPU 模型也支持大型 2MB 頁和全局頁。正如前一種情況一樣,只要可能,Linux 使用大型頁來減少頁表數(shù)。
??然后頁全局目錄的第四項被拷貝到第一項中,這樣好為線性地址空間的前 896MB 中的低物理內(nèi)存映射作鏡像。為了完成對 SMP 系統(tǒng)的初始化,這個映射是必需的:當這個映射不再必要時,內(nèi)核通過調用 zap_low_mappings() 函數(shù)來清除對應的頁表項,正如先前的情況一樣。
(7)固定映射的線性地址
??我們看到內(nèi)核線性地址第四個GB 的初始部分映射系統(tǒng)的物理內(nèi)存。但是,至少 128MB 的線性地址總是留作他用,因為內(nèi)核使用這些線性地址實現(xiàn)非連續(xù)內(nèi)存分配和固定映射的線性地址。
??非連續(xù)內(nèi)存分配僅僅是動態(tài)分配和釋放內(nèi)存頁的一種特殊方式,將在第八章 “非連續(xù)內(nèi)存區(qū)的線性地址” 一節(jié)描述。本節(jié)我們集中討論固定映射的線性地址。
??固定映射的線性地址(fix-mapped linear address)基本上是一種類似于 0xffffc000(4G - 16K) 這樣的常量線性地址,其對應的物理地址不必等于線性地址減去 0xc000000,而是可以以任意方式建立。因此,每個固定映射的線性地址都映射一個物理內(nèi)存的頁框。我們將會在后面的章節(jié)看到,內(nèi)核使用固定映射的線性地址來代替指針變量,因為這些指針變量的值從不改變。
??固定映射的線性地址概念上類似于對 RAM 前 896MB 映射的線性地址。不過,固定映射的線性地址可以映射任何物理地址,而由第 4GB 初始部分的線性地址所建立的映射是線性的(線性地址 X 映射物理地址 X - PAGE_OFFSET)。
??就指針變量而言,固定映射的線性地址更有效。事實上,間接引用一個指針變量比間接引用一個立即常量地址要多一次內(nèi)存訪問。此外,在間接引用一個指針變量之前對其值進行檢查是一個良好的編程習慣;相反,對一個常量線性地址的檢查則是沒有必要的。
??每個固定映射的線性地址都由定義于 enum fixed_addresses 數(shù)據(jù)結構中的整型索引來表示:
enum fixed_addresses {FIX_HOLE,FIX_VSYSCALL,FIX_APIC_BASE,FIX_IO_APIC_BASE_0,[...]__end_of_fixed_addresses
}
??每個固定映射的線性地址都存放在線性地址第四個 GB 的末端。fix_to_virt() 函數(shù)計算從給定索引開始的常量線性地址:
inline unsigned long fix_to_virt(const unsigned int idx)
{_if (idx >= __end_of_fixed_addresses)__this_fixmap_does_not_exist();return (0xfffff000UL - (idx << PAGE_SHIFT));
}
??讓我們假定某個內(nèi)核函數(shù)調用 fix_to_virt(FIX_IO_APIC_BASE_0) 。因為該函數(shù)聲明為 “inline”,所以C 編譯程序不調用 fix_to_virt(),而是僅僅把它的代碼插入到調用函數(shù)中。此外,運行時從不對這個索引值執(zhí)行檢查。事實上,FIX_IO_APIC_BASE_0 是個等于 3 的常量,因此編譯程序可以去掉 if 語句,因為它的條件在編譯時為假。相反,如果條件為真,或者 fix_to_virt() 的參數(shù)不是一個常量,則編譯程序在連接階段產(chǎn)生一個錯誤,因為符號 _this_fixmap_does_not_exist 在別處沒有定義。最后,編譯程序計算 0xfffff000-(3<<PAGE_SHIFT),并用常量線性地址 0xffffc000 代替 fix_to_virt() 函數(shù)調用。
??為了把一個物理地址與固定映射的線性地址關聯(lián)起來,內(nèi)核使用set_fixmap(idx, phys) 和 set_fixmap_nocache(idx,phys) 去。這兩個函數(shù)都把 fix_to_virt(idx) 線性地址對應的一個頁表項初始化為物理地址 phys;不過,第二個函數(shù)也把頁表項的 PCD 標志置位,因此,當訪問這個頁框中的數(shù)據(jù)時禁用硬件高速緩存(參見本章前面 “硬件高速緩存” 一節(jié))。反過來,clear_fixmap(idx) 用來撤銷固定映射線性地址 idx 和物理地址之間的連接。
// arch/x86/include/asm/fixmap.h
#define set_fixmap(idx, phys) \__set_fixmap(idx, phys, PAGE_KERNEL)#define set_fixmap_nocache(idx, phys) \__set_fixmap(idx, phys, PAGE_KERNEL_NOCACHE)static inline void __set_fixmap(enum fixed_addresses idx,phys_addr_t phys, pgprot_t flags)
{native_set_fixmap(idx, phys, flags);
}// arch/x86/mm/pgtable.c
void native_set_fixmap(enum fixed_addresses idx, phys_addr_t phys,pgprot_t flags)
{__native_set_fixmap(idx, pfn_pte(phys >> PAGE_SHIFT, flags));
}
(8)處理硬件高速緩存和 TLB
??內(nèi)存尋址的最后一個主題是關于內(nèi)核如何使用硬件高速緩存來達到最佳效果。硬件高速緩存和轉換后援緩沖器(TLB)在提高現(xiàn)代計算機體系結構的性能上扮演著重要角色。
內(nèi)核開發(fā)者采用一些技術來減少高速緩存和 TLB 的未命中次數(shù)。
- 處理硬件高速緩存
- 處理 TLB
三、進程
1、進程、輕量級進程和線程
??當一個進程創(chuàng)建時,它幾乎與父進程相同。它接受父進程地址空間的一個(邏輯)拷貝,并從進程創(chuàng)建系統(tǒng)調用的下一條指令開始執(zhí)行與父進程相同的代碼。盡管父子進程可以共享含有程序代碼(正文)的頁,但是它們各自有獨立的數(shù)據(jù)拷貝(棧和堆),因此子進程對一個內(nèi)存單元的修改對父進程是不可見的(反之亦然)。
??盡管早期 Unix 內(nèi)核使用了這種簡單模式,但是,現(xiàn)代 Unix 系統(tǒng)并沒有如此使用。它們支持多線程應用程序 —— 擁有很多相對獨立執(zhí)行流的用戶程序共享應用程序的大部分數(shù)據(jù)結構。在這樣的系統(tǒng)中,一個進程由幾個用戶線程(或簡單地說,線程)組成,每個線程都代表進程的一個執(zhí)行流?,F(xiàn)在,大部分多線程應用程序都是用 pthread(POSIX thread)庫的標準庫函數(shù)集編寫的。
??Linux 內(nèi)核的早期版本沒有提供多線程應用的支持。從內(nèi)核觀點看,多線程應用程序僅僅是一個普通進程。多線程應用程序多個執(zhí)行流的創(chuàng)建、處理、調度整個都是在用戶態(tài)進行的(通常使用 POSIX 兼容的 pthread 庫)。
??Linux 使用輕量級進程(lightweight process)對多線程應用程序提供更好的支持。兩個輕量級進程基本上可以共享一些資源,諸如地址空間、打開的文件等等。只要其中一個修改共享資源,另一個就立即查看這種修改。當然,當兩個線程訪問共享資源時就必須同步它們自己。
??實現(xiàn)多線程應用程序的一個簡單方式就是把輕量級進程與每個線程關聯(lián)起來。這樣,線程之間就可以通過簡單地共享同一內(nèi)存地址空間、同一打開文件集等來訪問相同的應用程序數(shù)據(jù)結構集,同時,每個線程都可以由內(nèi)核獨立調度,以便一個睡眠的同時另一個仍然是可運行的。POSIX 兼容的 pthread 庫使用 Linux 輕量級進程有 3 個例子,它們是 LinuxThreads、 Native Posix Thread Library(NPTL) 和 IBM 的下一代 Posix 線程包 NGPT(Next Generation Posix Threading Package)。
??POSIX 兼容的多線程應用程序由支持 “線程組” 的內(nèi)核來處理最好不過。在 Linux 中,一個線程組基本上就是實現(xiàn)了多線程應用的一組輕量級進程,對于像 getpid(),kill(),和 _exit() 這樣的一些系統(tǒng)調用,它像一個組織,起整體的作用。
2、進程描述符
// include/linux/sched.h
struct task_struct {volatile long state; /* -1 unrunnable, 0 runnable, >0 stopped */void *stack;atomic_t usage;unsigned int flags; /* per process flags, defined below */unsigned int ptrace;int lock_depth; /* BKL lock depth */#ifdef CONFIG_SMP
#ifdef __ARCH_WANT_UNLOCKED_CTXSWint oncpu;
#endif
#endifint prio, static_prio, normal_prio;unsigned int rt_priority;const struct sched_class *sched_class;struct sched_entity se;struct sched_rt_entity rt;#ifdef CONFIG_PREEMPT_NOTIFIERS/* list of struct preempt_notifier: */struct hlist_head preempt_notifiers;
#endif/** fpu_counter contains the number of consecutive context switches* that the FPU is used. If this is over a threshold, the lazy fpu* saving becomes unlazy to save the trap. This is an unsigned char* so that after 256 times the counter wraps and the behavior turns* lazy again; this to deal with bursty apps that only use FPU for* a short time*/unsigned char fpu_counter;
#ifdef CONFIG_BLK_DEV_IO_TRACEunsigned int btrace_seq;
#endifunsigned int policy;cpumask_t cpus_allowed;#ifdef CONFIG_TREE_PREEMPT_RCUint rcu_read_lock_nesting;char rcu_read_unlock_special;struct rcu_node *rcu_blocked_node;struct list_head rcu_node_entry;
#endif /* #ifdef CONFIG_TREE_PREEMPT_RCU */#if defined(CONFIG_SCHEDSTATS) || defined(CONFIG_TASK_DELAY_ACCT)struct sched_info sched_info;
#endifstruct list_head tasks;struct plist_node pushable_tasks;struct mm_struct *mm, *active_mm;
#if defined(SPLIT_RSS_COUNTING)struct task_rss_stat rss_stat;
#endif
/* task state */int exit_state;int exit_code, exit_signal;int pdeath_signal; /* The signal sent when the parent dies *//* ??? */unsigned int personality;unsigned did_exec:1;unsigned in_execve:1; /* Tell the LSMs that the process is doing an* execve */unsigned in_iowait:1;/* Revert to default priority/policy when forking */unsigned sched_reset_on_fork:1;pid_t pid;pid_t tgid;#ifdef CONFIG_CC_STACKPROTECTOR/* Canary value for the -fstack-protector gcc feature */unsigned long stack_canary;
#endif/* * pointers to (original) parent process, youngest child, younger sibling,* older sibling, respectively. (p->father can be replaced with * p->real_parent->pid)*/struct task_struct *real_parent; /* real parent process */struct task_struct *parent; /* recipient of SIGCHLD, wait4() reports *//** children/sibling forms the list of my natural children*/struct list_head children; /* list of my children */struct list_head sibling; /* linkage in my parent's children list */struct task_struct *group_leader; /* threadgroup leader *//** ptraced is the list of tasks this task is using ptrace on.* This includes both natural children and PTRACE_ATTACH targets.* p->ptrace_entry is p's link on the p->parent->ptraced list.*/struct list_head ptraced;struct list_head ptrace_entry;/** This is the tracer handle for the ptrace BTS extension.* This field actually belongs to the ptracer task.*/struct bts_context *bts;/* PID/PID hash table linkage. */struct pid_link pids[PIDTYPE_MAX];struct list_head thread_group;struct completion *vfork_done; /* for vfork() */int __user *set_child_tid; /* CLONE_CHILD_SETTID */int __user *clear_child_tid; /* CLONE_CHILD_CLEARTID */cputime_t utime, stime, utimescaled, stimescaled;cputime_t gtime;
#ifndef CONFIG_VIRT_CPU_ACCOUNTINGcputime_t prev_utime, prev_stime;
#endifunsigned long nvcsw, nivcsw; /* context switch counts */struct timespec start_time; /* monotonic time */struct timespec real_start_time; /* boot based time */
/* mm fault and swap info: this can arguably be seen as either mm-specific or thread-specific */unsigned long min_flt, maj_flt;struct task_cputime cputime_expires;struct list_head cpu_timers[3];/* process credentials */const struct cred *real_cred; /* objective and real subjective task* credentials (COW) */const struct cred *cred; /* effective (overridable) subjective task* credentials (COW) */struct mutex cred_guard_mutex; /* guard against foreign influences on* credential calculations* (notably. ptrace) */struct cred *replacement_session_keyring; /* for KEYCTL_SESSION_TO_PARENT */char comm[TASK_COMM_LEN]; /* executable name excluding path- access with [gs]et_task_comm (which lockit with task_lock())- initialized normally by setup_new_exec */
/* file system info */int link_count, total_link_count;
#ifdef CONFIG_SYSVIPC
/* ipc stuff */struct sysv_sem sysvsem;
#endif
#ifdef CONFIG_DETECT_HUNG_TASK
/* hung task detection */unsigned long last_switch_count;
#endif
/* CPU-specific state of this task */struct thread_struct thread;
/* filesystem information */struct fs_struct *fs;
/* open file information */struct files_struct *files;
/* namespaces */struct nsproxy *nsproxy;
/* signal handlers */struct signal_struct *signal;struct sighand_struct *sighand;sigset_t blocked, real_blocked;sigset_t saved_sigmask; /* restored if set_restore_sigmask() was used */struct sigpending pending;unsigned long sas_ss_sp;size_t sas_ss_size;int (*notifier)(void *priv);void *notifier_data;sigset_t *notifier_mask;struct audit_context *audit_context;
#ifdef CONFIG_AUDITSYSCALLuid_t loginuid;unsigned int sessionid;
#endifseccomp_t seccomp;/* Thread group tracking */u32 parent_exec_id;u32 self_exec_id;
/* Protection of (de-)allocation: mm, files, fs, tty, keyrings, mems_allowed,* mempolicy */spinlock_t alloc_lock;#ifdef CONFIG_GENERIC_HARDIRQS/* IRQ handler threads */struct irqaction *irqaction;
#endif/* Protection of the PI data structures: */raw_spinlock_t pi_lock;#ifdef CONFIG_RT_MUTEXES/* PI waiters blocked on a rt_mutex held by this task */struct plist_head pi_waiters;/* Deadlock detection and priority inheritance handling */struct rt_mutex_waiter *pi_blocked_on;
#endif#ifdef CONFIG_DEBUG_MUTEXES/* mutex deadlock detection */struct mutex_waiter *blocked_on;
#endif
#ifdef CONFIG_TRACE_IRQFLAGSunsigned int irq_events;unsigned long hardirq_enable_ip;unsigned long hardirq_disable_ip;unsigned int hardirq_enable_event;unsigned int hardirq_disable_event;int hardirqs_enabled;int hardirq_context;unsigned long softirq_disable_ip;unsigned long softirq_enable_ip;unsigned int softirq_disable_event;unsigned int softirq_enable_event;int softirqs_enabled;int softirq_context;
#endif
#ifdef CONFIG_LOCKDEP
# define MAX_LOCK_DEPTH 48ULu64 curr_chain_key;int lockdep_depth;unsigned int lockdep_recursion;struct held_lock held_locks[MAX_LOCK_DEPTH];gfp_t lockdep_reclaim_gfp;
#endif/* journalling filesystem info */void *journal_info;/* stacked block device info */struct bio_list *bio_list;/* VM state */struct reclaim_state *reclaim_state;struct backing_dev_info *backing_dev_info;struct io_context *io_context;unsigned long ptrace_message;siginfo_t *last_siginfo; /* For ptrace use. */struct task_io_accounting ioac;
#if defined(CONFIG_TASK_XACCT)u64 acct_rss_mem1; /* accumulated rss usage */u64 acct_vm_mem1; /* accumulated virtual memory usage */cputime_t acct_timexpd; /* stime + utime since last update */
#endif
#ifdef CONFIG_CPUSETSnodemask_t mems_allowed; /* Protected by alloc_lock */int cpuset_mem_spread_rotor;
#endif
#ifdef CONFIG_CGROUPS/* Control Group info protected by css_set_lock */struct css_set *cgroups;/* cg_list protected by css_set_lock and tsk->alloc_lock */struct list_head cg_list;
#endif
#ifdef CONFIG_FUTEXstruct robust_list_head __user *robust_list;
#ifdef CONFIG_COMPATstruct compat_robust_list_head __user *compat_robust_list;
#endifstruct list_head pi_state_list;struct futex_pi_state *pi_state_cache;
#endif
#ifdef CONFIG_PERF_EVENTSstruct perf_event_context *perf_event_ctxp;struct mutex perf_event_mutex;struct list_head perf_event_list;
#endif
#ifdef CONFIG_NUMAstruct mempolicy *mempolicy; /* Protected by alloc_lock */short il_next;
#endifatomic_t fs_excl; /* holding fs exclusive resources */struct rcu_head rcu;/** cache last used pipe for splice*/struct pipe_inode_info *splice_pipe;
#ifdef CONFIG_TASK_DELAY_ACCTstruct task_delay_info *delays;
#endif
#ifdef CONFIG_FAULT_INJECTIONint make_it_fail;
#endifstruct prop_local_single dirties;
#ifdef CONFIG_LATENCYTOPint latency_record_count;struct latency_record latency_record[LT_SAVECOUNT];
#endif/** time slack values; these are used to round up poll() and* select() etc timeout values. These are in nanoseconds.*/unsigned long timer_slack_ns;unsigned long default_timer_slack_ns;struct list_head *scm_work_list;
#ifdef CONFIG_FUNCTION_GRAPH_TRACER/* Index of current stored address in ret_stack */int curr_ret_stack;/* Stack of return addresses for return function tracing */struct ftrace_ret_stack *ret_stack;/* time stamp for last schedule */unsigned long long ftrace_timestamp;/** Number of functions that haven't been traced* because of depth overrun.*/atomic_t trace_overrun;/* Pause for the tracing */atomic_t tracing_graph_pause;
#endif
#ifdef CONFIG_TRACING/* state flags for use by tracers */unsigned long trace;/* bitmask of trace recursion */unsigned long trace_recursion;
#endif /* CONFIG_TRACING */
#ifdef CONFIG_CGROUP_MEM_RES_CTLR /* memcg uses this to do batch job */struct memcg_batch_info {int do_batch; /* incremented when batch uncharge started */struct mem_cgroup *memcg; /* target memcg of uncharge */unsigned long bytes; /* uncharged usage */unsigned long memsw_bytes; /* uncharged mem+swap usage */} memcg_batch;
#endif
};
??下圖示意性地描述了 Linux 的進程描述符。
(1)標識一個進程
??一般來說,能被獨立調度的每個執(zhí)行上下文都必須擁有它自己的進程描述符:因此,即使共享內(nèi)核大部分數(shù)據(jù)結構的輕量級進程,也有它們自己的 task_struct 結構。
??進程和進程描述符之間有非常嚴格的一一對應關系,這使得用 32 位進程描述符地址(注 3)標識進程成為一種方便的方式。進程描述符指針指向這些地址,內(nèi)核對進程的大部分引用是通過進程描述符指針進行的。
??另一方面,類 Unix 操作系統(tǒng)允許用戶使用一個叫做進程標識符 process ID(或 PID)的數(shù)來標識進程,PID 存放在進程描述符的 pid 字段中。PID 被順序編號,新創(chuàng)建進程的 PID 通常是前一個進程的 PID 加 1。不過,PID 的值有一個上限,當內(nèi)核使用的 PID 達到這個上限值的時候就必須開始循環(huán)使用已閑置的小 PID 號。在缺省情況下,最大的 PID 號是 32767(PID_MAX_DEFAULT-1):系統(tǒng)管理員可以通過往 /proc/sys/kernel/pid_max 這個文件中寫入一個更小的值來減小 PID 的上限值,使 PID 的上限小于 32767。( /proc 是一個特殊文件系統(tǒng)的安裝點,參看第十二章"特殊文件系統(tǒng)"一節(jié)。)在 64 位體系結構中,系統(tǒng)管理員可以把 PID 的上限擴大到 4194303 。
??由于循環(huán)使用 PID 編號,內(nèi)核必須通過管理一個 pidmap-array 位圖來表示當前已分配的 PID 號和閑置的 PID 號。因為一個頁框包含 32768 個位,所以在 32 位體系結構中 pidmap-array 位圖存放在一個單獨的頁中。然而,在 64 位體系結構中,當內(nèi)核分配了超過當前位圖大小的 PID 號時,需要為 PID 位圖增加更多的頁。系統(tǒng)會一直保存這些頁不被釋放。
??Linux 把不同的 PID 與系統(tǒng)中每個進程或輕量級進程相關聯(lián)(本章后面我們會看到,在多處理器系統(tǒng)上稍有例外)。這種方式能提供最大的靈活性,因為系統(tǒng)中每個執(zhí)行上下文都可以被唯一地識別。
??另一方面,Unix 程序員希望同一組中的線程有共同的 PID。例如,把指定 PID 的信號發(fā)給組中的所有線程。事實上,POSIX 1003.1c 標準規(guī)定一個多線程應用程序中的所有線程都必須有相同的 PID。
??遵照這個標準,Linux 引入線程組的表示。一個線程組中的所有線程使用和該線程組的領頭線程(thread group leader)相同的 PID,也就是該組中第一個輕量級進程的 PID,它被存入進程描述符的 tgid 字段中。getpid() 系統(tǒng)調用返回當前進程的 tgid 值而不是 pid 的值,因此,一個多線程應用的所有線程共享相同的 PID。絕大多數(shù)進程都屬于一個線程組,包含單一的成員線程組的領頭線程其 tgid 的值與 pid 的值相同,因而 getpid() 系統(tǒng)調用對這類進程所起的作用和一般進程是一樣的。
??下面,我們將向你說明如何從進程的 PID 中有效地導出它的描述符指針。效率至關重要,因為像 kill() 這樣的很多系統(tǒng)調用使用 PID 表示所操作的進程。
(2)進程描述符處理
??進程是動態(tài)實體,其生命周期范圍從幾毫秒到幾個月。因此,內(nèi)核必須能夠同時處理很多進程,并把進程描述符存放在動態(tài)內(nèi)存中,而不是放在永久分配給內(nèi)核的內(nèi)存區(qū)(譯注1)。對每個進程來說,Linux 都把兩個不同的數(shù)據(jù)結構緊湊地存放在一個單獨為進程分配的存儲區(qū)域內(nèi):一個是內(nèi)核態(tài)的進程堆棧,另一個是緊挨進程描述符的小數(shù)據(jù)結構 thread_info,叫做線程描述符,這塊存儲區(qū)域的大小通常為 8192 個字節(jié)(兩個頁框)??紤]到效率的因素,內(nèi)核讓這 8K 空間占據(jù)連續(xù)的兩個頁框并讓第一個頁框的起始地址是 213 的倍數(shù)。當幾乎沒有可用的動態(tài)內(nèi)存空間時,就會很難找到這樣的兩個連續(xù)頁框,因為空閑空間可能存在大量碎片(見第八章 “伙伴系統(tǒng)算法” 一節(jié))。因此,在 80x86 體系結構中,在編譯時可以進行設置,以使內(nèi)核棧和線程描述符跨越一個單獨的頁框(4096 個字節(jié))。
??在第二章 “Linux 中的分段” 一節(jié)中我們已經(jīng)知道,內(nèi)核態(tài)的進程訪問處于內(nèi)核數(shù)據(jù)段的棧,這個棧不同于用戶態(tài)的進程所用的棧。因為內(nèi)核控制路徑使用很少的棧,因此只需要幾千個字節(jié)的內(nèi)核態(tài)堆棧。所以,對棧和 thread_info 結構來說,8KB 足夠了。不過,當使用一個頁框(一頁內(nèi)存,4K)存放內(nèi)核態(tài)堆棧和 thread_info 結構時,內(nèi)核要采用一些額外的棧以防止中斷和異常的深度嵌套而引起的溢出(見第四章)。
??圖 3-2 顯示了在 2 頁(8KB)內(nèi)存區(qū)中存放兩種數(shù)據(jù)結構的方式。線程描述符駐留于這個內(nèi)存區(qū)的開始,而棧從末端向下增長。該圖還顯示了分別通過 task 和 thread_info 字段使 thread_info 結構與 task_struct 結構互相關聯(lián)。
??esp 寄存器是 CPU 棧指針,用來存放棧頂單元的地址。在 80x86 系統(tǒng)中,棧起始于末端,并朝這個內(nèi)存區(qū)開始的方向增長。從用戶態(tài)剛切換到內(nèi)核態(tài)以后,進程的內(nèi)核??偸强盏?#xff0c;因此,esp 寄存器指向這個棧的頂端。
??一旦數(shù)據(jù)寫入堆棧,esp 的值就遞減。因為 thread_info 結構是 52 個字節(jié)長,因此內(nèi)核棧能擴展到 8140 個字節(jié)。
C 語言使用下列的聯(lián)合結構方便地表示一個進程的線程描述符和內(nèi)核棧:
union thread_union {struct thread_info thread_info;unsigned long stack[2048]; /* 對 4K 的核數(shù)組下標是 1024 */
};
??如圖 3-2 所示,thread_info 結構從 0x015fa000 地址處開始存放,而棧從 0x015fc000 地址處開始存放。esp 寄存器的值指向地址為 0x015fa878 的當前棧頂。
??內(nèi)核使用 alloc_thread_info 和 free_thread_info 宏分配和釋放存儲 thread_info 結構和內(nèi)核棧的內(nèi)存區(qū)。
(3)標識當前進程
??從效率的觀點來看,剛才所講的 thread_info 結構與內(nèi)核態(tài)堆棧之間的緊密結合提供的主要好處是:內(nèi)核很容易從 esp 寄存器的值獲得當前在 CPU 上正在運行進程的 thread_info 結構的地址。事實上,如果 thread_union 結構長度是 8K(213 字節(jié)),則內(nèi)核屏蔽掉 esp 的低 13 位有效位就可以獲得 thread_info 結構的基地址; 而如果 thread_union 結構長度是 4K,內(nèi)核需要屏蔽掉 esp 的低 12 位有效位。這項工作由 current_thread_info() 函數(shù)來完成,它產(chǎn)生如下一些匯編指令:
movl $0xffffe000, %ecx # /* 或者是用于 4K 堆棧的 0xfffff000 */
andl %esp, %ecx
movl %ecх, p
??這三條指令執(zhí)行以后,p 就包含在執(zhí)行指令的 CPU 上運行的進程的 thread_info 結構的指針。
??進程最常用的是進程描述符的地址而不是 thread_info 結構的地址。為了獲得當前在 CPU 上運行進程的描述符指針,內(nèi)核要調用 current 宏,該宏本質上等價于 current_thread_info()->task ,它產(chǎn)生如下匯編語言指令:
movl $0xffffe000, %ecx # /* 或者是用于 4K堆棧的 0xfffff000 */
andl %esp, %ecx
movl (%ecx), p
??因為 task 字段在 thread_info 結構中的偏移量為 0,所以執(zhí)行完這三條指令之后,p 就包含在 CPU 上運行進程的描述符指針。
??current 宏經(jīng)常作為進程描述符字段的前綴出現(xiàn)在內(nèi)核代碼中,例如,current->pid 返回在 CPU 上正在執(zhí)行的進程的 PID。
??用棧存放進程描述符的另一個優(yōu)點體現(xiàn)在多處理器系統(tǒng)上:如前所述,對于每個硬件處理器,僅通過檢查棧就可以獲得當前正確的進程。早先的 Linux 版本沒有把內(nèi)核棧與進程描述符存放在一起,而是強制引入全局靜態(tài)變量 current 來標識正在運行進程的描述符。在多處理器系統(tǒng)上,有必要把 current 定義為一個數(shù)組,每一個元素對應一個可用 CPU 。
3、進程切換
??進程切換可能只發(fā)生在精心定義的點:schedule() 函數(shù)。從本質上說,每個進程切換由兩步組成:
- 切換頁全局目錄以安裝一個新的地址空間;
- 切換內(nèi)核態(tài)堆棧和硬件上下文,因為硬件上下文提供了內(nèi)核執(zhí)行新進程所需要的所有信息,包含 CPU 寄存器。
(1)switch_to 宏
// arch/x86/include/asm/system.h
#define switch_to(prev, next, last) \asm volatile(SAVE_CONTEXT \"movq %%rsp,%P[threadrsp](%[prev])\n\t" /* save RSP */ \"movq %P[threadrsp](%[next]),%%rsp\n\t" /* restore RSP */ \"call __switch_to\n\t" \"movq "__percpu_arg([current_task])",%%rsi\n\t" \__switch_canary \"movq %P[thread_info](%%rsi),%%r8\n\t" \"movq %%rax,%%rdi\n\t" \"testl %[_tif_fork],%P[ti_flags](%%r8)\n\t" \"jnz ret_from_fork\n\t" \RESTORE_CONTEXT \: "=a" (last) \__switch_canary_oparam \: [next] "S" (next), [prev] "D" (prev), \[threadrsp] "i" (offsetof(struct task_struct, thread.sp)), \[ti_flags] "i" (offsetof(struct thread_info, flags)), \[_tif_fork] "i" (_TIF_FORK), \[thread_info] "i" (offsetof(struct task_struct, stack)), \[current_task] "m" (current_task) \__switch_canary_iparam \: "memory", "cc" __EXTRA_CLOBBER)
(a)分析
??進程切換的第二步由 switch_to 宏執(zhí)行。它是內(nèi)核中與硬件關系最密切的例程之一,要理解它到底做了些什么我們必須下些功夫。
??首先,該宏有三個參數(shù),它們是 prev,next 和 last 。你可能很容易猜到 prev 和 next 的作用:它們僅是局部變量 prev 和 next 的占位符,即它們是輸入?yún)?shù),分別表示被替換進程和新進程描述符的地址在內(nèi)存中的位置。
??那第三個參數(shù) last 呢 ?在任何進程切換中,涉及到三個進程而不是兩個。假設內(nèi)核決定暫停進程 A 而激活進程 B。在 schedule() 函數(shù)中,prev 指向 A 的描述符而 next 指向 B 的描述符。switch_to 宏一但使 A 暫停,A 的執(zhí)行流就凍結。
??隨后,當內(nèi)核想再次此激活 A,就必須暫停另一個進程 C(這通常不同于 B),于是就要用 prev 指向 C 而 next 指向 A 來執(zhí)行另一個 switch_to 宏。當 A 恢復它的執(zhí)行流時,就會找到它原來的內(nèi)核棧,于是 prev 局部變量還是指向 A 的描述符而 next 指向 B 的描述符。此時,代表進程 A 執(zhí)行的內(nèi)核就失去了對 C 的任何引用。但是,事實表明這個引用對于完成進程切換是很有用的(更多細節(jié)參見第七章)。
??switch_to 宏的最后一個參數(shù)是輸出參數(shù),它表示宏把進程 C 的描述符地址寫在內(nèi)存的什么位置了(當然,這是在 A 恢復執(zhí)行之后完成的)。在進程切換之前,宏把第一個輸入?yún)?shù) prev(即在 A 的內(nèi)核堆棧中分配的 prev 局部變量)表示的變量的內(nèi)容存入 CPU 的 eax 寄存器。在完成進程切換,A 已經(jīng)恢復執(zhí)行時,宏把 CPU 的 eax 寄存器的內(nèi)容寫入由第三個輸出參數(shù) —— last 所指示的 A 在內(nèi)存中的位置。因為 CPU 寄存器不會在切換點發(fā)生變化,所以 C 的描述符地址也存在內(nèi)存的這個位置。在 schedule() 執(zhí)行過程中,參數(shù) last 指向 A 的局部變量 prev,所以 prev 被 C 的地址覆蓋。
??圖 3-7 顯示了進程 A,B,C 內(nèi)核堆棧的內(nèi)容以及 eax 寄存器的內(nèi)容。必須注意的是:圖中顯示的是在被 eax 寄存器的內(nèi)容覆蓋以前的 prev 局部變量的值。
- 在 eax 和 edx 寄存器中分別保存 prev 和 next 的值:
movl prev, %eax
movl next, %edx
- 把 eflags 和 ebp 寄存器的內(nèi)容保存在 prev 內(nèi)核棧中。必須保存它們的原因是編譯器認為在 switch_to 結束之前它們的值應當保持不變。
pushfl
pushl %ebp
- 把 esp 的內(nèi)容保存到 prev->thread.esp 中以使該字段指向 prev 內(nèi)核棧的棧頂:
movl %esp,484(%eax)
??484(%eax) 操作數(shù)表示內(nèi)存單元的地址為 eax 內(nèi)容加上 484。
- 把 next->thread.esp 裝入 esp。此時,內(nèi)核開始在 next 的內(nèi)核棧上操作,因此這條指令實際上完成了從 prev 到 next 的切換。由于進程描述符的地址和內(nèi)核棧的地址緊挨著(就像我們在本章前面"標識一個進程"一節(jié)所解釋的),所以改變內(nèi)核棧意味著改變當前進程。
movl 484(%edx), %esp
- 把標記為 1 的地地址(本節(jié)后面所示)存入 prev->thread.eip。當被替換的進程重新恢復執(zhí)行時,進程執(zhí)行被標記為 1 的那條指令:
movl $1f, 480(%eax)
- 宏把 next->thread.eip 的值(絕大多數(shù)情況下是一個被標記為 1 的地址)壓入 next 的內(nèi)核棧:
pushl 480(%edx)
- 跳到 __switch_to() ? C 函數(shù)(見下面):
jmp __switch_to
- 這里被進程 B 替換的進程 A 再次獲得 CPU:它執(zhí)行一些保存 eflags 和 ebp 寄存器內(nèi)容的指令,這兩條指令的第一條指令被標記為 1 。
1:popl %ebppopfl
??注意這些 pop 指令是怎樣引用 prev 進程的內(nèi)核棧的。當進程調度程序選擇了 prev 作為新進程在 CPU 上運行時,將執(zhí)行這些指令。于是,以 prev 作為第二個參數(shù)調用 switch_to。因此,esp 寄存器指向 prev 的內(nèi)核棧。
- 拷貝 eax 寄存器(上面步驟 1 中被裝載)的內(nèi)容到 switch_to 宏的第三個參數(shù) last 標識的內(nèi)存區(qū)域中:
movl %eax, last
??正如先前討論的,eax 寄存器指向剛被替換的進程的描述符(注 6)。
(2)__switch_to() 函數(shù)
// arch/x86/kernel/process_64.c
struct task_struct *
__switch_to(struct task_struct *prev_p, struct task_struct *next_p)
{struct thread_struct *prev = &prev_p->thread;struct thread_struct *next = &next_p->thread;int cpu = smp_processor_id();struct tss_struct *tss = &per_cpu(init_tss, cpu);unsigned fsindex, gsindex;bool preload_fpu;/** If the task has used fpu the last 5 timeslices, just do a full* restore of the math state immediately to avoid the trap; the* chances of needing FPU soon are obviously high now*/preload_fpu = tsk_used_math(next_p) && next_p->fpu_counter > 5;/* we're going to use this soon, after a few expensive things */if (preload_fpu)prefetch(next->xstate);/** Reload esp0, LDT and the page table pointer:*/load_sp0(tss, next);/** Switch DS and ES.* This won't pick up thread selector changes, but I guess that is ok.*/savesegment(es, prev->es);if (unlikely(next->es | prev->es))loadsegment(es, next->es);savesegment(ds, prev->ds);if (unlikely(next->ds | prev->ds))loadsegment(ds, next->ds);/* We must save %fs and %gs before load_TLS() because* %fs and %gs may be cleared by load_TLS().** (e.g. xen_load_tls())*/savesegment(fs, fsindex);savesegment(gs, gsindex);load_TLS(next, cpu);/* Must be after DS reload */unlazy_fpu(prev_p);/* Make sure cpu is ready for new context */if (preload_fpu)clts();/** Leave lazy mode, flushing any hypercalls made here.* This must be done before restoring TLS segments so* the GDT and LDT are properly updated, and must be* done before math_state_restore, so the TS bit is up* to date.*/arch_end_context_switch(next_p);/** Switch FS and GS.** Segment register != 0 always requires a reload. Also* reload when it has changed. When prev process used 64bit* base always reload to avoid an information leak.*/if (unlikely(fsindex | next->fsindex | prev->fs)) {loadsegment(fs, next->fsindex);/** Check if the user used a selector != 0; if yes* clear 64bit base, since overloaded base is always* mapped to the Null selector*/if (fsindex)prev->fs = 0;}/* when next process has a 64bit base use it */if (next->fs)wrmsrl(MSR_FS_BASE, next->fs);prev->fsindex = fsindex;if (unlikely(gsindex | next->gsindex | prev->gs)) {load_gs_index(next->gsindex);if (gsindex)prev->gs = 0;}if (next->gs)wrmsrl(MSR_KERNEL_GS_BASE, next->gs);prev->gsindex = gsindex;/** Switch the PDA and FPU contexts.*/prev->usersp = percpu_read(old_rsp);percpu_write(old_rsp, next->usersp);percpu_write(current_task, next_p);percpu_write(kernel_stack,(unsigned long)task_stack_page(next_p) +THREAD_SIZE - KERNEL_STACK_OFFSET);/** Now maybe reload the debug registers and handle I/O bitmaps*/if (unlikely(task_thread_info(next_p)->flags & _TIF_WORK_CTXSW_NEXT ||task_thread_info(prev_p)->flags & _TIF_WORK_CTXSW_PREV))__switch_to_xtra(prev_p, next_p, tss);/** Preload the FPU context, now that we've determined that the* task is likely to be using it. */if (preload_fpu)__math_state_restore();return prev_p;
}
(a)分析
??_switch_to() 函數(shù)執(zhí)行大多數(shù)開始于 switch_to() 宏的進程切換。這個函數(shù)作用于 prev_p 和 next_p 參數(shù),這兩個參數(shù)表示前一個進程和新進程。這個函數(shù)的調用不同于一般函數(shù)的調用,因為 _switch_to() 從 eax 和 edx 取參數(shù) prev_p 和 next_p(我們在前面已看到這些參數(shù)就是保存在那里),而不像大多數(shù)函數(shù)一樣從棧中取參數(shù)。為了強迫函數(shù)從寄存器取它的參數(shù),內(nèi)核利用 __attribute__ 和 regparm 關鍵字,這兩個關鍵字是 C 語言非標準的擴展名,由 gcc 編譯程序實現(xiàn)。在 include/asm-i386/system.h 頭文件中,__switch_to() 函數(shù)的聲明如下:
__switch_to(struct task_struct *prev_p,struct task_struct *next_p)
_attribute__(regparm(3));
??函數(shù)執(zhí)行的步驟如下:
- 執(zhí)行由 __unlazy_fpu() 宏產(chǎn)生的代碼(參見本章稍后 “保存和加載 FPU、MMX 及 XMM 寄存器” 一節(jié)),以有選擇地保存 prev_p 進程的 FPU、MMX 及 XMM 寄存器的內(nèi)容。
__unlazy_fpu(prev_p);
- 執(zhí)行 smp_processor_id() 宏獲得本地(local) CPU 的下標,即執(zhí)行代碼的 CPU。該宏從當前進程的 thread_info 結構的 cpu 字段獲得下標并將它保存到 cpu 局部變量。
- 把 next_p->thread.esp0 裝入對應于本地 CPU 的 TSS 的 esp0 字段,我們將在第十章的 “通過 sysenter 指令發(fā)生系統(tǒng)調用” 一節(jié)看到,以后任何由 sysenter 匯編指令產(chǎn)生的從用戶態(tài)到內(nèi)核態(tài)的特權級轉換將把這個地址拷貝到 esp 寄存器中:
init_tss[cpu].esp0 = next_p->thread.esp0;
- 把 next_p 進程使用的線程局部存儲(TLS)段裝入本地 CPU 的全局描述符表; 三個段選擇符保存在進程描述符內(nèi)的 tls_array 數(shù)組中(參見第二章的 “Linux 中的分段” 一節(jié))。
cpu_gdt_table[cpu][6] = next_p->thread.tls_array[0];
cpu_gdt_table[cpu][7] = next_p->thread.tls_array[1];
cpu_gdt_table[cpu][8] = next_p->thread.tls_array[2];
- 把 fs 和 gs 段寄存器的內(nèi)容分別存放 prev_p->thread.fs 和 prev_p->thread.gs 中,對應的匯編語言指令是:
movl %fs, 40(%esi)
movl %gs, 44(%esi)
??esi 寄存器指向 prev_p->thread 結構。
- 如果 fs 或 gs 段寄存器已經(jīng)被 prev_p 或 next_p 進程中的任意一個使用(也就是說如果它們有一個非 0 的值),則將 next_p 進程的 thread_struct 描述符中保存的值裝入這些寄存器中。這一步在邏輯上補充了前一步中執(zhí)行的操作。主要的匯編語言指令如下:
movl 40(%ebx),%fs
movl 44(%ebx),%gs
??ebx 寄存器指向 next_p->thread 結構。代碼實際上更復雜,因為當它檢測到一個無效的段寄存器值時,CPU 可能產(chǎn)生一個異常。代碼采用一種 “修正(fix-up)” 途徑來考慮這種可能性(參見第十章"動態(tài)地址檢查:修正代碼" 一節(jié))。
- 用 next_p->thread.debugreg 數(shù)組的內(nèi)容裝載 dr0,…,dr7 中的 6 個調試寄存器(注 7)。只有在 next_p 被掛起時正在使用調試寄存器(也就是說,next_p->thread.debugreg[7] 字段不為 0),這種操作才能進行。這些寄存器不需要被保存,因為只有當一個調試器想要監(jiān)控 prev 時 prev_p->thread.debugreg 才會被修改。
if (next_p->thread.debugreg[7]) {loaddebug(&next_p->thread, 0);loaddebug(&next_p->thread, 1);loaddebug(&next_p->thread, 2);loaddebug(&next_p->thread, 3);/* 沒有 4 和 5*/loaddebug(&next_p->thread, 6);loaddebug(&next_p->thread, 7);
}
- 如果必要,更新 TSS 中的 I/O 位圖。當 next_p 或 prev_p 有其自己的定制 I/O 權限位圖時必須這么做:
if (prev_p->thread.io_bitmap_ptr || next_p->thread.io_bitmap_ptr)handle_io_bitmap(&next_p->thread, &init_tss[cpu]);
??因為進程很少修改 I/O 權限位圖,所以該位圖在"懶"模式中被處理:當且僅當一個進程在當前時間片內(nèi)實際訪問 I/O 端口時,真實位圖才被拷貝到本地 CPU 的 TSS中。進程的定制 I/O 權限位圖被保存在 thread_info 結構的 io_bitmap_ptr 字段指向的緩沖區(qū)中。 handle_io_bitmap() 函數(shù)為 next_p 進程設置本地 CPU 使用的 TSS 的 io_bitmap 字段如下:
- 如果 next_p 進程不擁有自己的 I/O 權限位圖,則 TSS 的 io_bitmap字段被設為 0x8000。
- 如果 next_p 進程擁有自己的 I/O 權限位圖,則 TSS 的 io_bitmap 字段被設為 0x9000。
??TSS 的 io_bitmap 字段應當包含一個在 TSS 中的偏移量,其中存放實際位圖。無論何時用戶態(tài)進程試圖訪問一個 I/O 端口,0x8000 和 0x9000 指向 TSS 界限之外并將因此引起 “General protection” 異常(參見第四章的 “異?!?一節(jié))。do_general_protection() 異常處理程序將檢查保存在 io_bitmap 字段的值;如果是 0x8000,函數(shù)發(fā)送一個 SIGSEGV 信號給用戶態(tài)進程; 如果是 0x9000,函數(shù)把進程位圖(由 thread_info 結構中的 io_bitmap_ptr 字段指示)拷貝到本地 CPU 的 TSS 中,把 io_bitmap 字段設為實際位圖的偏移(104),并強制再一次執(zhí)行有缺陷的匯編語言指令。
- 終止。 _switch_to() C函數(shù)通過使用下列聲明結束:
return prev_p;
??由編譯器產(chǎn)生的相應匯編語言指令是:
movl %edi,%eax
ret
??prev_p 參數(shù) (現(xiàn)在在 edi 中) 被拷貝到 eax,因為缺省情況下任何 C 函數(shù)的返回值被傳遞給 eax 寄存器。注意 eax 的值因此在調用 __switch_to() 的過程中被保護起來;這非常重要,因為調用 switch_to 宏時會假定 eax 總是用來存放將被替換的進程描述符的地址。
??匯編語言指令 ret 把棧頂保存的返回地址裝入eip 程序計數(shù)器。不過,通過簡單地跳轉到 __switch_to() 函數(shù)來調用該函數(shù)。因此,ret 匯編指令在棧中找到標號為 1 的指令的地址,其中標號為 1 的地址是由 switch_to() 宏推入棧中的。如果因為 next_p 第一次執(zhí)行而以前從未被掛起,__switch_to() 就找到 ret_from_fork() 函數(shù)的起始地址。
4、創(chuàng)建進程
(1)do_fork() 函數(shù)
// kernel/fork.c
long do_fork(unsigned long clone_flags,unsigned long stack_start,struct pt_regs *regs,unsigned long stack_size,int __user *parent_tidptr,int __user *child_tidptr)
{struct task_struct *p;int trace = 0;long nr;/** Do some preliminary argument and permissions checking before we* actually start allocating stuff*/if (clone_flags & CLONE_NEWUSER) {if (clone_flags & CLONE_THREAD)return -EINVAL;/* hopefully this check will go away when userns support is* complete*/if (!capable(CAP_SYS_ADMIN) || !capable(CAP_SETUID) ||!capable(CAP_SETGID))return -EPERM;}/** We hope to recycle these flags after 2.6.26*/if (unlikely(clone_flags & CLONE_STOPPED)) {static int __read_mostly count = 100;if (count > 0 && printk_ratelimit()) {char comm[TASK_COMM_LEN];count--;printk(KERN_INFO "fork(): process `%s' used deprecated ""clone flags 0x%lx\n",get_task_comm(comm, current),clone_flags & CLONE_STOPPED);}}/** When called from kernel_thread, don't do user tracing stuff.*/if (likely(user_mode(regs)))trace = tracehook_prepare_clone(clone_flags);p = copy_process(clone_flags, stack_start, regs, stack_size,child_tidptr, NULL, trace);/** Do this prior waking up the new thread - the thread pointer* might get invalid after that point, if the thread exits quickly.*/if (!IS_ERR(p)) {struct completion vfork;trace_sched_process_fork(current, p);nr = task_pid_vnr(p);if (clone_flags & CLONE_PARENT_SETTID)put_user(nr, parent_tidptr);if (clone_flags & CLONE_VFORK) {p->vfork_done = &vfork;init_completion(&vfork);}audit_finish_fork(p);tracehook_report_clone(regs, clone_flags, nr, p);/** We set PF_STARTING at creation in case tracing wants to* use this to distinguish a fully live task from one that* hasn't gotten to tracehook_report_clone() yet. Now we* clear it and set the child going.*/p->flags &= ~PF_STARTING;if (unlikely(clone_flags & CLONE_STOPPED)) {/** We'll start up with an immediate SIGSTOP.*/sigaddset(&p->pending.signal, SIGSTOP);set_tsk_thread_flag(p, TIF_SIGPENDING);__set_task_state(p, TASK_STOPPED);} else {wake_up_new_task(p, clone_flags);}tracehook_report_clone_complete(trace, regs,clone_flags, nr, p);if (clone_flags & CLONE_VFORK) {freezer_do_not_count();wait_for_completion(&vfork);freezer_count();tracehook_report_vfork_done(p, nr);}} else {nr = PTR_ERR(p);}return nr;
}
(2)copy_process() 函數(shù)
// kernel/fork.c
static struct task_struct *copy_process(unsigned long clone_flags,unsigned long stack_start,struct pt_regs *regs,unsigned long stack_size,int __user *child_tidptr,struct pid *pid,int trace)
{int retval;struct task_struct *p;int cgroup_callbacks_done = 0;if ((clone_flags & (CLONE_NEWNS|CLONE_FS)) == (CLONE_NEWNS|CLONE_FS))return ERR_PTR(-EINVAL);/** Thread groups must share signals as well, and detached threads* can only be started up within the thread group.*/if ((clone_flags & CLONE_THREAD) && !(clone_flags & CLONE_SIGHAND))return ERR_PTR(-EINVAL);/** Shared signal handlers imply shared VM. By way of the above,* thread groups also imply shared VM. Blocking this case allows* for various simplifications in other code.*/if ((clone_flags & CLONE_SIGHAND) && !(clone_flags & CLONE_VM))return ERR_PTR(-EINVAL);/** Siblings of global init remain as zombies on exit since they are* not reaped by their parent (swapper). To solve this and to avoid* multi-rooted process trees, prevent global and container-inits* from creating siblings.*/if ((clone_flags & CLONE_PARENT) &¤t->signal->flags & SIGNAL_UNKILLABLE)return ERR_PTR(-EINVAL);retval = security_task_create(clone_flags);if (retval)goto fork_out;retval = -ENOMEM;p = dup_task_struct(current);if (!p)goto fork_out;ftrace_graph_init_task(p);rt_mutex_init_task(p);#ifdef CONFIG_PROVE_LOCKINGDEBUG_LOCKS_WARN_ON(!p->hardirqs_enabled);DEBUG_LOCKS_WARN_ON(!p->softirqs_enabled);
#endifretval = -EAGAIN;if (atomic_read(&p->real_cred->user->processes) >=task_rlimit(p, RLIMIT_NPROC)) {if (!capable(CAP_SYS_ADMIN) && !capable(CAP_SYS_RESOURCE) &&p->real_cred->user != INIT_USER)goto bad_fork_free;}retval = copy_creds(p, clone_flags);if (retval < 0)goto bad_fork_free;/** If multiple threads are within copy_process(), then this check* triggers too late. This doesn't hurt, the check is only there* to stop root fork bombs.*/retval = -EAGAIN;if (nr_threads >= max_threads)goto bad_fork_cleanup_count;if (!try_module_get(task_thread_info(p)->exec_domain->module))goto bad_fork_cleanup_count;p->did_exec = 0;delayacct_tsk_init(p); /* Must remain after dup_task_struct() */copy_flags(clone_flags, p);INIT_LIST_HEAD(&p->children);INIT_LIST_HEAD(&p->sibling);rcu_copy_process(p);p->vfork_done = NULL;spin_lock_init(&p->alloc_lock);init_sigpending(&p->pending);p->utime = cputime_zero;p->stime = cputime_zero;p->gtime = cputime_zero;p->utimescaled = cputime_zero;p->stimescaled = cputime_zero;
#ifndef CONFIG_VIRT_CPU_ACCOUNTINGp->prev_utime = cputime_zero;p->prev_stime = cputime_zero;
#endif
#if defined(SPLIT_RSS_COUNTING)memset(&p->rss_stat, 0, sizeof(p->rss_stat));
#endifp->default_timer_slack_ns = current->timer_slack_ns;task_io_accounting_init(&p->ioac);acct_clear_integrals(p);posix_cpu_timers_init(p);p->lock_depth = -1; /* -1 = no lock */do_posix_clock_monotonic_gettime(&p->start_time);p->real_start_time = p->start_time;monotonic_to_bootbased(&p->real_start_time);p->io_context = NULL;p->audit_context = NULL;cgroup_fork(p);
#ifdef CONFIG_NUMAp->mempolicy = mpol_dup(p->mempolicy);if (IS_ERR(p->mempolicy)) {retval = PTR_ERR(p->mempolicy);p->mempolicy = NULL;goto bad_fork_cleanup_cgroup;}mpol_fix_fork_child_flag(p);
#endif
#ifdef CONFIG_TRACE_IRQFLAGSp->irq_events = 0;
#ifdef __ARCH_WANT_INTERRUPTS_ON_CTXSWp->hardirqs_enabled = 1;
#elsep->hardirqs_enabled = 0;
#endifp->hardirq_enable_ip = 0;p->hardirq_enable_event = 0;p->hardirq_disable_ip = _THIS_IP_;p->hardirq_disable_event = 0;p->softirqs_enabled = 1;p->softirq_enable_ip = _THIS_IP_;p->softirq_enable_event = 0;p->softirq_disable_ip = 0;p->softirq_disable_event = 0;p->hardirq_context = 0;p->softirq_context = 0;
#endif
#ifdef CONFIG_LOCKDEPp->lockdep_depth = 0; /* no locks held yet */p->curr_chain_key = 0;p->lockdep_recursion = 0;
#endif#ifdef CONFIG_DEBUG_MUTEXESp->blocked_on = NULL; /* not blocked yet */
#endif
#ifdef CONFIG_CGROUP_MEM_RES_CTLRp->memcg_batch.do_batch = 0;p->memcg_batch.memcg = NULL;
#endifp->bts = NULL;/* Perform scheduler related setup. Assign this task to a CPU. */sched_fork(p, clone_flags);retval = perf_event_init_task(p);if (retval)goto bad_fork_cleanup_policy;if ((retval = audit_alloc(p)))goto bad_fork_cleanup_policy;/* copy all the process information */if ((retval = copy_semundo(clone_flags, p)))goto bad_fork_cleanup_audit;if ((retval = copy_files(clone_flags, p)))goto bad_fork_cleanup_semundo;if ((retval = copy_fs(clone_flags, p)))goto bad_fork_cleanup_files;if ((retval = copy_sighand(clone_flags, p)))goto bad_fork_cleanup_fs;if ((retval = copy_signal(clone_flags, p)))goto bad_fork_cleanup_sighand;if ((retval = copy_mm(clone_flags, p)))goto bad_fork_cleanup_signal;if ((retval = copy_namespaces(clone_flags, p)))goto bad_fork_cleanup_mm;if ((retval = copy_io(clone_flags, p)))goto bad_fork_cleanup_namespaces;retval = copy_thread(clone_flags, stack_start, stack_size, p, regs);if (retval)goto bad_fork_cleanup_io;if (pid != &init_struct_pid) {retval = -ENOMEM;pid = alloc_pid(p->nsproxy->pid_ns);if (!pid)goto bad_fork_cleanup_io;if (clone_flags & CLONE_NEWPID) {retval = pid_ns_prepare_proc(p->nsproxy->pid_ns);if (retval < 0)goto bad_fork_free_pid;}}p->pid = pid_nr(pid);p->tgid = p->pid;if (clone_flags & CLONE_THREAD)p->tgid = current->tgid;if (current->nsproxy != p->nsproxy) {retval = ns_cgroup_clone(p, pid);if (retval)goto bad_fork_free_pid;}p->set_child_tid = (clone_flags & CLONE_CHILD_SETTID) ? child_tidptr : NULL;/** Clear TID on mm_release()?*/p->clear_child_tid = (clone_flags & CLONE_CHILD_CLEARTID) ? child_tidptr: NULL;
#ifdef CONFIG_FUTEXp->robust_list = NULL;
#ifdef CONFIG_COMPATp->compat_robust_list = NULL;
#endifINIT_LIST_HEAD(&p->pi_state_list);p->pi_state_cache = NULL;
#endif/** sigaltstack should be cleared when sharing the same VM*/if ((clone_flags & (CLONE_VM|CLONE_VFORK)) == CLONE_VM)p->sas_ss_sp = p->sas_ss_size = 0;/** Syscall tracing and stepping should be turned off in the* child regardless of CLONE_PTRACE.*/user_disable_single_step(p);clear_tsk_thread_flag(p, TIF_SYSCALL_TRACE);
#ifdef TIF_SYSCALL_EMUclear_tsk_thread_flag(p, TIF_SYSCALL_EMU);
#endifclear_all_latency_tracing(p);/* ok, now we should be set up.. */p->exit_signal = (clone_flags & CLONE_THREAD) ? -1 : (clone_flags & CSIGNAL);p->pdeath_signal = 0;p->exit_state = 0;/** Ok, make it visible to the rest of the system.* We dont wake it up yet.*/p->group_leader = p;INIT_LIST_HEAD(&p->thread_group);/* Now that the task is set up, run cgroup callbacks if* necessary. We need to run them before the task is visible* on the tasklist. */cgroup_fork_callbacks(p);cgroup_callbacks_done = 1;/* Need tasklist lock for parent etc handling! */write_lock_irq(&tasklist_lock);/* CLONE_PARENT re-uses the old parent */if (clone_flags & (CLONE_PARENT|CLONE_THREAD)) {p->real_parent = current->real_parent;p->parent_exec_id = current->parent_exec_id;} else {p->real_parent = current;p->parent_exec_id = current->self_exec_id;}spin_lock(¤t->sighand->siglock);/** Process group and session signals need to be delivered to just the* parent before the fork or both the parent and the child after the* fork. Restart if a signal comes in before we add the new process to* it's process group.* A fatal signal pending means that current will exit, so the new* thread can't slip out of an OOM kill (or normal SIGKILL).*/recalc_sigpending();if (signal_pending(current)) {spin_unlock(¤t->sighand->siglock);write_unlock_irq(&tasklist_lock);retval = -ERESTARTNOINTR;goto bad_fork_free_pid;}if (clone_flags & CLONE_THREAD) {atomic_inc(¤t->signal->count);atomic_inc(¤t->signal->live);p->group_leader = current->group_leader;list_add_tail_rcu(&p->thread_group, &p->group_leader->thread_group);}if (likely(p->pid)) {tracehook_finish_clone(p, clone_flags, trace);if (thread_group_leader(p)) {if (clone_flags & CLONE_NEWPID)p->nsproxy->pid_ns->child_reaper = p;p->signal->leader_pid = pid;tty_kref_put(p->signal->tty);p->signal->tty = tty_kref_get(current->signal->tty);attach_pid(p, PIDTYPE_PGID, task_pgrp(current));attach_pid(p, PIDTYPE_SID, task_session(current));list_add_tail(&p->sibling, &p->real_parent->children);list_add_tail_rcu(&p->tasks, &init_task.tasks);__get_cpu_var(process_counts)++;}attach_pid(p, PIDTYPE_PID, pid);nr_threads++;}total_forks++;spin_unlock(¤t->sighand->siglock);write_unlock_irq(&tasklist_lock);proc_fork_connector(p);cgroup_post_fork(p);perf_event_fork(p);return p;bad_fork_free_pid:if (pid != &init_struct_pid)free_pid(pid);
bad_fork_cleanup_io:if (p->io_context)exit_io_context(p);
bad_fork_cleanup_namespaces:exit_task_namespaces(p);
bad_fork_cleanup_mm:if (p->mm)mmput(p->mm);
bad_fork_cleanup_signal:if (!(clone_flags & CLONE_THREAD))__cleanup_signal(p->signal);
bad_fork_cleanup_sighand:__cleanup_sighand(p->sighand);
bad_fork_cleanup_fs:exit_fs(p); /* blocking */
bad_fork_cleanup_files:exit_files(p); /* blocking */
bad_fork_cleanup_semundo:exit_sem(p);
bad_fork_cleanup_audit:audit_free(p);
bad_fork_cleanup_policy:perf_event_free_task(p);
#ifdef CONFIG_NUMAmpol_put(p->mempolicy);
bad_fork_cleanup_cgroup:
#endifcgroup_exit(p, cgroup_callbacks_done);delayacct_tsk_free(p);module_put(task_thread_info(p)->exec_domain->module);
bad_fork_cleanup_count:atomic_dec(&p->cred->user->processes);exit_creds(p);
bad_fork_free:free_task(p);
fork_out:return ERR_PTR(retval);
}
(a)分析
??copy_process() 創(chuàng)建進程描述符以及子進程執(zhí)行所需要的所有其他數(shù)據(jù)結構。它的參數(shù)與 do_fork() 的參數(shù)相同,外加子進程的 PID。下面描述 copy process() 的最重要的步驟:
-
檢查參數(shù) clone_flags 所傳遞標志的一致性。尤其是,在下列情況下,它返回錯誤代號:
- CLONE_NEWNS 和 CLONE_FS 標志都被設置。
- CLONE_THREAD 標志被設置,但 CLONE_SIGHAND 標志被清 0(同一線程組中的輕量級進程必須共享信號)。
- CLONE_SIGHAND 標志被設置,但 CLONE_VM 被清 0(共享信號處理程序的輕量級進程也必須共享內(nèi)存描述符)。
-
通過調用 security_task_create() 以及稍后調用的 security_task_alloc() 執(zhí)行所有附加的安全檢查。Linux 2.6 提供擴展安全性的鉤子函數(shù),與傳統(tǒng) Unix 相比,它具有更加強壯的安全模型。詳情參見第二十章。
-
調用 dup_task_struct() 為子進程獲取進程描述符。該函數(shù)執(zhí)行如下操作:
-
如果需要,則在當前進程中調用 __unlazy_fpu(),把 FPU、MMX 和 SSE/SSE2 寄存器的內(nèi)容保存到父進程的 thread_info 結構中。稍后,dup_task_struct() 將把這些值復制到子進程的 thread_info 結構中。
-
執(zhí)行 alloc_task_struct() 宏,為新進程獲取進程描述符(task_struct 結構),并將描述符地址保存在 tsk 局部變量中。
-
執(zhí)行 alloc_thread_info 宏以獲取一塊空閑內(nèi)存區(qū),用來存放新進程的 thread_info 結構和內(nèi)核棧,并將這塊內(nèi)存區(qū)字段的地址存在局部變量 ti 中。正如在本章前面 “標識一個進程” 一節(jié)中所述:這塊內(nèi)存區(qū)字段的大小是 8KB 或 4KB 。
-
將 current 進程描述符的內(nèi)容復制到 tsk 所指向的 task_struct 結構中,然后把 tsk->thread_info 置為 ti 。
-
把 current 進程的 thread_info 描述符的內(nèi)容復制到 ti 所指向的結構中,然后把 ti->task 置為 tsk。
-
把新進程描述符的使用計數(shù)器(tsk->usage)置為 2,用來表示進程描述符正在被使用而且其相應的進程處于活動狀態(tài)(進程狀態(tài)即不是 EXIT_ZOMBIE,也不是 EXIT_DEAD)。
-
返回新進程的進程描述符指針(tsk)。
-
-
檢查存放在 current->signal->rlim[RLIMIT_NPROC].rlim_cur 變量中的值是否小于或等于用戶所擁有的進程數(shù)。如果是,則返回錯誤碼,除非進程沒有 root 權限。該函數(shù)從每用戶數(shù)據(jù)結構 user_struct 中獲取用戶所擁有的進程數(shù)。通過進程描述符 user 字段的指針可以找到這個數(shù)據(jù)結構。
-
遞增 user_struct 結構的使用計數(shù)器 (tsk->user->__count 字段)和用戶所擁有的進程的計數(shù)器(tsk->user->processes)。
-
檢查系統(tǒng)中的進程數(shù)量(存放在 nr_threads 變量中)是否超過 max_threads 變量的值。這個變量的缺省值取決于系統(tǒng)內(nèi)存容量的大小。總的原則是:所有 thread_info 描述符和內(nèi)核棧所占用的空間不能超過物理內(nèi)存大小的 1/8。不過,系統(tǒng)管理員可以通過寫 /proc/sys/kernel/threads-max 文件來改變這個值。
-
如果實現(xiàn)新進程的執(zhí)行域和可執(zhí)行格式的內(nèi)核函數(shù)(參見第二十章)都包含在內(nèi)核模塊中,則遞增它們的使用計數(shù)器(參見附錄二)。
-
設置與進程狀態(tài)相關的幾個關鍵字段:
- 把大內(nèi)核鎖計數(shù)器 tsk->lock_depth 初始化為 -1(參見第五章 “大內(nèi)核鎖” 一節(jié))。
- 把 tsk->did_exec 字段初始化為 0 :它記錄了進程發(fā)出的 execve() 系統(tǒng)調用的次數(shù)。
- 更新從父進程復制到 tsk->flags 字段中的一些標志:首先清除 PF_SUPERPRIV 標志,該標志表示進程是否使用了某種超級用戶權限。然后設置 PF_FORKNOEXEC 標志,它表示子進程還沒有發(fā)出 execve() 系統(tǒng)調用。
-
把新進程的 PID 存入 tsk->pid 字段。
-
如果 clone_flags 參數(shù)中的 CLONE_PARENT_SETTID 標志被設置,就把子進程的 PID 復制到參數(shù) parent_tidptr 指向的用戶態(tài)變量中。
-
初始化子進程描述符中的 list_head 數(shù)據(jù)結構和自旋鎖,并為與掛起信號、定時器及時間統(tǒng)計表相關的幾個字段賦初值。
-
調用 copy_semundo(), copy_files(),copy_fs(),copy_sighand(),copy_signa1(), copy_mm() 和 copy_namespace() 來創(chuàng)建新的數(shù)據(jù)結構,并把父進程相應數(shù)據(jù)結構的值復制到新數(shù)據(jù)結構中,除非 clone_flags 參數(shù)指出它們有不同的值。
-
調用 copy_thread(),用發(fā)出 clone() 系統(tǒng)調用時 CPU 寄存器的值(正如第十章所述,這些值已經(jīng)被保存在父進程的內(nèi)核棧中)來初始化子進程的內(nèi)核棧。不過,copy_thread() 把 eax 寄存器對應字段的值 [這是 fork() 和 clone() 系統(tǒng)調用在子進程中的返回值] 字段強行置為 0。子進程描述符的 thread.esp 字段初始化為子進程內(nèi)核棧的基地址,匯編語言函數(shù)(ret_from_fork())的地址存放在 thread.eip 字段中。如果父進程使用 I/O 權限位圖,則子進程獲取該位圖的一個拷貝。最后,如果 CLONE_SETTLS 標志被設置,則子進程獲取由 clone() 系統(tǒng)調用的參數(shù) tls 指向的用戶態(tài)數(shù)據(jù)結構所表示的 TLS 段(注 9)。
-
如果 clone_flags 參數(shù)的值被置為 CLONE_CHILD_SETTID 或CLONE_CHILD_CLEARTID,就把 child_tidptr 參數(shù)的值分別復制到 tsk->set_chid_tid 或 tsk->clear_child_tid 字段。這些標志說明:必須改變子進程用戶態(tài)地址空間的 child_tidptr 所指向的變量的值,不過實際的寫操作要稍后再執(zhí)行。
-
清除子進程 thread_info 結構的 TIF_SYSCALL_TRACE 標志,以使 ret_from_fork() 函數(shù)不會把系統(tǒng)調用結束的消息通知給調試進程(參見第十章 “進入和退出系統(tǒng)調用” 一節(jié))。(為對子進程的跟蹤是由 tsk->ptrace 中的 PTRACE_SYSCALL 標志來控制的,所以子進程的系統(tǒng)調用跟蹤不會被禁用。)
-
用 clone_flags 參數(shù)低位的信號數(shù)字編碼初始化 tsk->exit_signal 字段,如果 CLONE_THREAD 標志被置位,就把 tsk->exit_signal 字段初始化為 -1。正如我們將在本章稍后 “進程終止” 一節(jié)所看見的,只有當線程組的最后一個成員(通常是線程組的領頭)“死亡”,才會產(chǎn)生一個信號,以通知線程組的領頭進程的父進程。
-
調用 sched_fork() 完成對新進程調度程序數(shù)據(jù)結構的初始化。該函數(shù)把新進程的狀態(tài)設置為 TASK_RUNNING,并把 thread_info 結構的 preempt_count 字段設置為 1,從而禁止內(nèi)核搶占(參見第五章 “內(nèi)核搶占” 一節(jié))。此外,為了保證公平的進程調度,該函數(shù)在父子進程之間共享父進程的時間片(參見第七章 “scheduler_tick() 數(shù)” 一節(jié))。
-
把新進程的 thread_info 結構的 cpu 字段設置為由 smp_processor_id() 所返回的本地 CPU 號。
-
初始化表示親子關系的字段。尤其是,如果 CLONE_PARENT 或 CLONE_THREAD ,被設置,就用 current->real_parent 的值初始化 tsk->real_parent 和 tsk->parent,因此,子進程的父進程似乎是當前進程的父進程。否則,把 tsk->real_parent 和 tsk->parent 置為當前進程。
-
如果不需要跟蹤子進程(沒有設置 CLONE_PTRAC 標志),就把 tsk->ptrace 字段設置為 0 。tsk->ptrace 字段會存放一些標志,而這些標志是在一個進程被另外一個進程跟蹤時才會用到的。采用這種方式,即使當前進程被跟蹤,子進程也不會被跟蹤。
-
執(zhí)行 SET_LINKS 宏,把新進程描述符插入進程鏈表。
-
如果子進程必須被跟蹤(tsk->ptrace 字段的 PT_PTRACED 標志被設置),就把 current->parent 賦給 tsk->parent,并將子進程插入調試程序的跟蹤鏈表中。
-
調用 attach_pid() 把新進程插述符的 PID 插入 pidhash[PIDTYPE_PID] 散列表。
-
如果子進程是線程組的領頭進程(CLONE_THREAD 標志被清 0):
- 把 tsk->tgid 的初值置為 tsk->pid。
- 把 tsk->group_leader 的初值置為 tsk。
- 調用三次 attach_pid() ,把子進程分別插入 PIDTYPE_TGID,PIDTYPE_PGID 和 PIDTYPE_SID 類型的 PID 散列表。
-
否則,如果子進程屬于它的父進程的線程組(CLONE_THREAD 標志被設置):
- 把 tsk->tgid 的初值置為 tsk->current->tgid。
- 把 tsk->group_leader 的初值置為 current->group_leader 的值。
- 調用 attach_pid(),把子進程插入 PIDTYPE_TGID 類型的散列表中(更具體地說,插入 current->group_leader 進程的每個 PID 鏈表)。
-
現(xiàn)在,新進程已經(jīng)被加入進程集合:遞增 nr_threads 變量的值。
-
遞增 total_forks 變量以記錄被創(chuàng)建的進程的數(shù)量。
-
終止并返回子進程描述符指針(tsk)。
(b)do_fork 之后
??讓我們回頭看看在 do_fork() 結束之后都發(fā)生了什么?,F(xiàn)在,我們有了處于可運行狀態(tài)的完整的子進程。但是,它還沒有實際運行,調度程序要決定何時把 CPU 交給這個子進程。在以后的進程切換中,調度程序繼續(xù)完善子進程:把子進程描述符 thread 字段的值裝入幾個 CPU 寄存器。特別是把 thread.esp(即把子進程內(nèi)核態(tài)堆棧的地址)裝入 esp 寄存器,把函數(shù) ret_from_fork() 的地址裝入eip 寄存器。這個匯編語言函數(shù)調用 schedule_tail() 函數(shù)(它依次調用 finish_task_switch() 來完成進程切換,參見第七章 “schedule() 函數(shù)” 一節(jié)),用存放在棧中的值再裝載所有的寄存器,并強迫 CPU 返回到用戶態(tài)。然后,在 fork()、vfork() 或 clone() 系統(tǒng)調用結束時,新進程將開始執(zhí)行系統(tǒng)調用的返回值放在 eax 寄存器中:返回給子進程的值是 0,返回給父進程的值是子進程的 PID?;仡?copy_thread() 對子進程的 eax 寄存器所執(zhí)行的操作(copy_process() 的第 13 步),就能理解這是如何實現(xiàn)的。
??除非 fork 系統(tǒng)調用返回 0,否則,子進程將與父進程執(zhí)行相同的代碼(參見 copy_process() 的第 13 步)。應用程序的開發(fā)者可以按照 Unix 編程者熟悉的方式利用這一事實,在基于 PID 值的程序中插入一個條件語句使子進程與父進程有不同的行為。
(3)內(nèi)核線程
??傳統(tǒng)的 Unix 系統(tǒng)把一些重要的任務委托給周期性執(zhí)行的進程,這些任務包括刷新磁盤高速緩存,交換出不用的頁框,維護網(wǎng)絡連接等等。事實上,以嚴格線性的方式執(zhí)行這些任務的確效率不高,如果把它們放在后臺調度,不管是對它們的函數(shù)還是對終端用戶進程都能得到較好的響應。因為一些系統(tǒng)進程只運行在內(nèi)核態(tài),所以現(xiàn)代操作系統(tǒng)把它們的函數(shù)委托給內(nèi)核線程(kernel thread),內(nèi)核線程不受不必要的用戶態(tài)上下文的拖累。在 Linux 中,內(nèi)核線程在以下幾方面不同于普通進程:
- 內(nèi)核線程只運行在內(nèi)核態(tài),而普通進程既可以運行在內(nèi)核態(tài),也可以運行在用戶態(tài)。
- 因為內(nèi)核線程只運行在內(nèi)核態(tài),它們只使用大于 PAGE_OFFSET 的線性地址空間。另一方面,不管在用戶態(tài)還是在內(nèi)核態(tài),普通進程可以用 4GB 的線性地址空間。
(a)創(chuàng)建一個內(nèi)核線程
// arch/x86/kernel/process.c
/** Create a kernel thread*/
int kernel_thread(int (*fn)(void *), void *arg, unsigned long flags)
{struct pt_regs regs;memset(®s, 0, sizeof(regs));regs.si = (unsigned long) fn;regs.di = (unsigned long) arg;#ifdef CONFIG_X86_32regs.ds = __USER_DS;regs.es = __USER_DS;regs.fs = __KERNEL_PERCPU;regs.gs = __KERNEL_STACK_CANARY;
#elseregs.ss = __KERNEL_DS;
#endifregs.orig_ax = -1;regs.ip = (unsigned long) kernel_thread_helper;regs.cs = __KERNEL_CS | get_kernel_rpl();regs.flags = X86_EFLAGS_IF | 0x2;/* Ok, create the new process.. */return do_fork(flags | CLONE_VM | CLONE_UNTRACED, 0, ®s, 0, NULL, NULL);
}
EXPORT_SYMBOL(kernel_thread);
??kernel_thread() 函數(shù)創(chuàng)建一個新的內(nèi)核線程,它接受的參數(shù)有:所要執(zhí)行的內(nèi)核函數(shù)的地址(fn)、要傳遞給函數(shù)的參數(shù)(arg)、一組 clone 標志(flags)。 該函數(shù)本質上以下面的方式調用 do_fork() :
do_fork(flags | CLONE_VM | CLONE_UNTRACED, 0, ®s, 0, NULL, NULL);
??CLONE_VM 標志避免復制調用進程的頁表:由于新內(nèi)核線程無論如何都不會訪問用戶態(tài)地址空間,所以這種復制無疑會造成時間和空間的浪費。CLONE_UNTRACED 標志保證不會有任何進程跟蹤新內(nèi)核線程,即使調用進程被跟蹤。
??傳遞給 do_fork() 的參數(shù) regs 表示內(nèi)核棧的地址,copy_thread() 函數(shù)將從這里找到為新線程初始化 CPU 寄存器的值。kernel_thread() 函數(shù)在這個棧中保留寄存器值的目的是:
- 通過 copy_thread() 把 ebx 和 edx 分別設置為參數(shù) fn 和 arg 的值。
- 把 eip 寄存器的值設置為下面匯編語言代碼段的地址:
movl %edx, %eax
pushl %edx
call *%ebx
pushl %eax
call do_exit
??因此,新的內(nèi)核線程開始執(zhí)行 fn(arg) 函數(shù),如果該函數(shù)結束,內(nèi)核線程執(zhí)行系統(tǒng)調用 _exit(),并把 fn() 的返回值傳遞給它(參見本章稍后 “撤消進程” 一節(jié))。
(b)進程 0
// arch/x86/kernel/init_task.c
/** Initial thread structure.** We need to make sure that this is THREAD_SIZE aligned due to the* way process stacks are handled. This is done by having a special* "init_task" linker map entry..*/
union thread_union init_thread_union __init_task_data ={ INIT_THREAD_INFO(init_task) };/** Initial task structure.** All other task structs will be allocated on slabs in fork.c*/
struct task_struct init_task = INIT_TASK(init_task);
EXPORT_SYMBOL(init_task);// include/linux/init_task.h
#define INIT_TASK(tsk) \
{ \.state = 0, \.stack = &init_thread_info, \.usage = ATOMIC_INIT(2), \.flags = PF_KTHREAD, \.lock_depth = -1, \.prio = MAX_PRIO-20, \.static_prio = MAX_PRIO-20, \.normal_prio = MAX_PRIO-20, \.policy = SCHED_NORMAL, \.cpus_allowed = CPU_MASK_ALL, \.mm = NULL, \.active_mm = &init_mm, \.se = { \.group_node = LIST_HEAD_INIT(tsk.se.group_node), \}, \.rt = { \.run_list = LIST_HEAD_INIT(tsk.rt.run_list), \.time_slice = HZ, \.nr_cpus_allowed = NR_CPUS, \}, \.tasks = LIST_HEAD_INIT(tsk.tasks), \.pushable_tasks = PLIST_NODE_INIT(tsk.pushable_tasks, MAX_PRIO), \.ptraced = LIST_HEAD_INIT(tsk.ptraced), \.ptrace_entry = LIST_HEAD_INIT(tsk.ptrace_entry), \.real_parent = &tsk, \.parent = &tsk, \.children = LIST_HEAD_INIT(tsk.children), \.sibling = LIST_HEAD_INIT(tsk.sibling), \.group_leader = &tsk, \.real_cred = &init_cred, \.cred = &init_cred, \.cred_guard_mutex = \__MUTEX_INITIALIZER(tsk.cred_guard_mutex), \.comm = "swapper", \.thread = INIT_THREAD, \.fs = &init_fs, \.files = &init_files, \.signal = &init_signals, \.sighand = &init_sighand, \.nsproxy = &init_nsproxy, \.pending = { \.list = LIST_HEAD_INIT(tsk.pending.list), \.signal = {{0}}}, \.blocked = {{0}}, \.alloc_lock = __SPIN_LOCK_UNLOCKED(tsk.alloc_lock), \.journal_info = NULL, \.cpu_timers = INIT_CPU_TIMERS(tsk.cpu_timers), \.fs_excl = ATOMIC_INIT(0), \.pi_lock = __RAW_SPIN_LOCK_UNLOCKED(tsk.pi_lock), \.timer_slack_ns = 50000, /* 50 usec default slack */ \.pids = { \[PIDTYPE_PID] = INIT_PID_LINK(PIDTYPE_PID), \[PIDTYPE_PGID] = INIT_PID_LINK(PIDTYPE_PGID), \[PIDTYPE_SID] = INIT_PID_LINK(PIDTYPE_SID), \}, \.dirties = INIT_PROP_LOCAL_SINGLE(dirties), \INIT_IDS \INIT_PERF_EVENTS(tsk) \INIT_TRACE_IRQFLAGS \INIT_LOCKDEP \INIT_FTRACE_GRAPH \INIT_TRACE_RECURSION \INIT_TASK_RCU_PREEMPT(tsk) \
}#define INIT_CPU_TIMERS(cpu_timers) \
{ \LIST_HEAD_INIT(cpu_timers[0]), \LIST_HEAD_INIT(cpu_timers[1]), \LIST_HEAD_INIT(cpu_timers[2]), \
}
??所有進程的祖先叫做進程 0,idle 進程或因為歷史的原因叫做 swapper 進程,它是在 Linux 的初始化階段從無到有創(chuàng)建的一個內(nèi)核線程(參見附錄一)。這個祖先進程使用下列靜態(tài)分配的數(shù)據(jù)結構(所有其他進程的數(shù)據(jù)結構都是動態(tài)分配的):
-
存放在 init_task 變量中的進程描述符,由 INIT_TASK 宏完成對它的初始化。
-
存放在 init_thread_union 變量中的 thread_info 描述符和內(nèi)核堆棧,由 INIT_THREAD_INFO 宏完成對它們的初始化。
-
由進程描述符指向的下列表:
- init_mm
- init_fs
- init_files
- init_signals
- init_sighand
這些表分別由下列宏初始化:
- INIT_MM
- INIT_FS
- INIT_FILES
- INIT_SIGNALS
- INIT_SIGHAND
-
主內(nèi)核頁全局目錄存放在 swapper_pg_dir 中(參見第二章"內(nèi)核頁表"一節(jié))。 start_kernel() 函數(shù)初始化內(nèi)核需要的所有數(shù)據(jù)結構,激活中斷,創(chuàng)建另一個叫進程 1 的內(nèi)核線程(一般叫做 init 進程):
kernel_thread(init, NULL, CLONE_FS | CLONE_SIGHAND);
??新創(chuàng)建內(nèi)核線程的 PID 為 1,并與進程 0 共享每進程所有的內(nèi)核數(shù)據(jù)結構。此外,當調度程序選擇到它時,init 進程開始執(zhí)行 init() 函數(shù)。
??創(chuàng)建 init 進程后,進程 0 執(zhí)行 cpu_idle() 函數(shù),該函數(shù)本質上是在開中斷的情況下重復執(zhí)行 hlt 匯編語言指令(參見第四章)。只有當沒有其他進程處于 TASK_RUNNING 狀態(tài)時,調度程序才選擇進程 0 。
??在多處理器系統(tǒng)中,每個 CPU 都有一個進程 0 。只要打開機器電源,計算機的 BIOS 就啟動某一個 CPU,同時禁用其他 CPU。運行在 CPU 0 上的 swapper 進程初始化內(nèi)核數(shù)據(jù)結構,然后激活其他的 CPU,并通過 copy_process() 函數(shù)創(chuàng)建另外的 swapper 進程,把 0 傳遞給新創(chuàng)建的 swapper 進程作為它們的新 PID。此外,內(nèi)核把適當?shù)?CPU 索引賦給內(nèi)核所創(chuàng)建的每個進程的 thread_info 描述符的 cpu 字段。
(c)進程 1
??由進程 0 創(chuàng)建的內(nèi)核線程執(zhí)行 init() 函數(shù),init() 依次完成內(nèi)核初始化。init() 調用 execve() 系統(tǒng)調用裝入可執(zhí)行程序 init。結果,init 內(nèi)核線程變?yōu)橐粋€普通進程,且擁有自己的每進程(per-process)內(nèi)核數(shù)據(jù)結構(參見第二十章)。在系統(tǒng)關閉之前,init 進程一直存活,因為它創(chuàng)建和監(jiān)控在操作系統(tǒng)外層執(zhí)行的所有進程的活動。
(d)其他內(nèi)核線程
??Linux 使用很多其他內(nèi)核線程。其中一些在初始化階段創(chuàng)建,一直運行到系統(tǒng)關閉:而其他一些在內(nèi)核必須執(zhí)行一個任務時 “按需” 創(chuàng)建,這種任務在內(nèi)核的執(zhí)行上下文中得到很好的執(zhí)行。
??一些內(nèi)核線程的例子(除了進程 0 和進程 1)是:
-
keventd(也被稱為事件)
執(zhí)行 keventd_wq 工作隊列(參見第四章)中的函數(shù)。 -
kapmd
處理與高級電源管理(APM)相關的事件。 -
kswapd
執(zhí)行內(nèi)存回收,在第十七章"周期回收"一節(jié)將進行描述。 -
pdflush
刷新 “臟” 緩沖區(qū)中的內(nèi)容到磁盤以回收內(nèi)存,在第十五章 “pdflush內(nèi)核線程” 一節(jié)將進行描述。 -
kblockd
執(zhí)行 kblockd_workqueue 工作隊列中的函數(shù)。實質上,它周期性地激活塊設備驅動程序,將在第十四章 “激活塊設備驅動程序” 一節(jié)給予描述。 -
ksoftirqd
運行 tasklet(參看第四章 “軟中斷及 tasklet” 一節(jié)):系統(tǒng)中每個 CPU 都有這樣一個內(nèi)核線程。
(4)撤消進程
??很多進程終止了它們本該執(zhí)行的代碼,從這種意義上說,這些進程"死"了。當這種情況發(fā)生時,必須通知內(nèi)核以便內(nèi)核釋放進程所擁有的資源,包括內(nèi)存、打開文件及其他我們在本書中講到的零碎東西,如信號量。
??進程終止的一般方式是調用 exit() 庫函數(shù),該函數(shù)釋放 C 函數(shù)庫所分配的資源,執(zhí)行編程者所注冊的每個函數(shù),并結束從系統(tǒng)回收進程的那個系統(tǒng)調用。exit() 函數(shù)可能由編程者顯式地插入。另外,C 編譯程序總是把 exit() 函數(shù)插入到 main() 函數(shù)的最后一條語句之后。
??內(nèi)核可以有選擇地強迫整個線程組死掉。這發(fā)生在以下兩種典型情況下:當進程接收到一個不能處理或忽視的信號時(參見十一章),或者當內(nèi)核正在代表進程運行時在內(nèi)核態(tài)產(chǎn)生一個不可恢復的 CPU 異常時(參見第四章)。
(5)進程終止
??在 Linux 2.6 中有兩個終止用戶態(tài)應用的系統(tǒng)調用:
-
exit_group() 系統(tǒng)調用,它終止整個線程組,即整個基于多線程的應用。do_group_exit() 是實現(xiàn)這個系統(tǒng)調用的主要內(nèi)核函數(shù)。這是 C 庫函數(shù) exit() 應該調用的系統(tǒng)調用。
-
exit() 系統(tǒng)調用,它終止某一個線程,而不管該線程所屬線程組中的所有其他進程。do_exit() 是實現(xiàn)這個系統(tǒng)調用的主要內(nèi)核函數(shù)。這是被諸如 pthread_exit() 的 Linux 線程庫的函數(shù)所調用的系統(tǒng)調用。
(a)do_group_exit() 函數(shù)
??應用層調用:
#include <linux/unistd.h>void exit_group(int status);
??內(nèi)核層響應函數(shù):
// kernel/exit.c
void do_group_exit(int exit_code)
{struct signal_struct *sig = current->signal;BUG_ON(exit_code & 0x80); /* core dumps don't get here */if (signal_group_exit(sig))exit_code = sig->group_exit_code;else if (!thread_group_empty(current)) {struct sighand_struct *const sighand = current->sighand;spin_lock_irq(&sighand->siglock);if (signal_group_exit(sig))/* Another thread got here before we took the lock. */exit_code = sig->group_exit_code;else {sig->group_exit_code = exit_code;sig->flags = SIGNAL_GROUP_EXIT;zap_other_threads(current);}spin_unlock_irq(&sighand->siglock);}do_exit(exit_code);/* NOTREACHED */
}
描述
??do_group_exit() 函數(shù)殺死屬于 current 線程組的所有進程。它接受進程終止代號作為參數(shù),進程終止代號可能是系統(tǒng)調用 exit_group()(正常結束)指定的一個值,也可能是內(nèi)核提供的一個錯誤代號(異常結束)。該函數(shù)執(zhí)行下述操作:
- 檢查退出進程的 SIGNAL_GROUP_EXIT 標志是否不為 0,如果不為 0,說明內(nèi)核已經(jīng)開始為線程組執(zhí)行退出的過程。在這種情況下,就把存放在 current->signal->group_exit_code 中的值當作退出碼,然后跳轉到第 4 步。
- 否則,設置進程的 SIGNAL_GROUP_EXIT 標志并把終止代號存放到 current->signal->group_exit_code 字段。
- 調用 zap_other_threads() 函數(shù)殺死 current 線程組中的其他進程(如果有的話)。為了完成這個步驟,函數(shù)掃描與 current->tgid 對應的 PIDTYPE_TGID 類型的散列表中的每個 PID 鏈表,向表中所有不同于 current 的進程發(fā)送 SIGKILL 信號(參見第十一章),結果,所有這樣的進程都將執(zhí)行 do_exit() 函數(shù),從而被殺死。
- 調用 do_exit() 函數(shù),把進程的終止代號傳遞給它。正如我們將在下面看到的,do_exit() 殺死進程而且不再返回。
(b)do_exit() 函數(shù)
源碼
// kernel/exit.c
void do_exit(long code)
{struct task_struct *tsk = current;int group_dead;profile_task_exit(tsk);WARN_ON(atomic_read(&tsk->fs_excl));if (unlikely(in_interrupt()))panic("Aiee, killing interrupt handler!");if (unlikely(!tsk->pid))panic("Attempted to kill the idle task!");tracehook_report_exit(&code);validate_creds_for_do_exit(tsk);/** We're taking recursive faults here in do_exit. Safest is to just* leave this task alone and wait for reboot.*/if (unlikely(tsk->flags & PF_EXITING)) {printk(KERN_ALERT"Fixing recursive fault but reboot is needed!\n");/** We can do this unlocked here. The futex code uses* this flag just to verify whether the pi state* cleanup has been done or not. In the worst case it* loops once more. We pretend that the cleanup was* done as there is no way to return. Either the* OWNER_DIED bit is set by now or we push the blocked* task into the wait for ever nirwana as well.*/tsk->flags |= PF_EXITPIDONE;set_current_state(TASK_UNINTERRUPTIBLE);schedule();}exit_irq_thread();exit_signals(tsk); /* sets PF_EXITING *//** tsk->flags are checked in the futex code to protect against* an exiting task cleaning up the robust pi futexes.*/smp_mb();raw_spin_unlock_wait(&tsk->pi_lock);if (unlikely(in_atomic()))printk(KERN_INFO "note: %s[%d] exited with preempt_count %d\n",current->comm, task_pid_nr(current),preempt_count());acct_update_integrals(tsk);/* sync mm's RSS info before statistics gathering */if (tsk->mm)sync_mm_rss(tsk, tsk->mm);group_dead = atomic_dec_and_test(&tsk->signal->live);if (group_dead) {hrtimer_cancel(&tsk->signal->real_timer);exit_itimers(tsk->signal);if (tsk->mm)setmax_mm_hiwater_rss(&tsk->signal->maxrss, tsk->mm);}acct_collect(code, group_dead);if (group_dead)tty_audit_exit();if (unlikely(tsk->audit_context))audit_free(tsk);tsk->exit_code = code;taskstats_exit(tsk, group_dead);exit_mm(tsk);if (group_dead)acct_process();trace_sched_process_exit(tsk);exit_sem(tsk);exit_files(tsk);exit_fs(tsk);check_stack_usage();exit_thread();cgroup_exit(tsk, 1);if (group_dead)disassociate_ctty(1);module_put(task_thread_info(tsk)->exec_domain->module);proc_exit_connector(tsk);/** FIXME: do that only when needed, using sched_exit tracepoint*/flush_ptrace_hw_breakpoint(tsk);/** Flush inherited counters to the parent - before the parent* gets woken up by child-exit notifications.*/perf_event_exit_task(tsk);exit_notify(tsk, group_dead);
#ifdef CONFIG_NUMAmpol_put(tsk->mempolicy);tsk->mempolicy = NULL;
#endif
#ifdef CONFIG_FUTEXif (unlikely(current->pi_state_cache))kfree(current->pi_state_cache);
#endif/** Make sure we are holding no locks:*/debug_check_no_locks_held(tsk);/** We can do this unlocked here. The futex code uses this flag* just to verify whether the pi state cleanup has been done* or not. In the worst case it loops once more.*/tsk->flags |= PF_EXITPIDONE;if (tsk->io_context)exit_io_context(tsk);if (tsk->splice_pipe)__free_pipe_info(tsk->splice_pipe);validate_creds_for_do_exit(tsk);preempt_disable();exit_rcu();/* causes final put_task_struct in finish_task_switch(). */tsk->state = TASK_DEAD;schedule();BUG();/* Avoid "noreturn function does return". */for (;;)cpu_relax(); /* For when BUG is null */
}EXPORT_SYMBOL_GPL(do_exit);
描述
??所有進程的終止都是由 do_exit() 函數(shù)來處理的,這個函數(shù)從內(nèi)核數(shù)據(jù)結構中刪除對終止進程的大部分引用。do_exit() 函數(shù)接受進程的終止代號作為參數(shù)并執(zhí)行下列操作:
-
把進程描述符的 flag 字段設置為 PF_EXITING 標志,以表示進程正在被刪除。
-
如果需要,通過函數(shù) del_timer_sync()(參見第六章)從動態(tài)定時器隊列中刪除進程描述符。
-
分別調用 exit_mm()、exit_sem()、__exit_files()、__exit_fs()、exit_namespace() 和 exit_thread() 函數(shù)從進程描述符中分離出與分頁、信號量、文件系統(tǒng)、打開文件描述符、命名空間以及 I/O 權限位圖相關的數(shù)據(jù)結構。如果沒有其他進程共享這些數(shù)據(jù)結構,那么這些函數(shù)還刪除所有這些數(shù)據(jù)結構中。
-
如果實現(xiàn)了被殺死進程的執(zhí)行域和可執(zhí)行格式(參見第二十章)的內(nèi)核函數(shù)包含在內(nèi)核模塊中,則函數(shù)遞減它們的使用計數(shù)器。
-
把進程描述符的 exit_code 字段設置成進程的終止代號,這個值要么是 _exit() 或 exit_group() 系統(tǒng)調用參數(shù)(正常終止),要么是由內(nèi)核提供的一個錯誤代號(異常終止)。
-
調用 exit_notify() 函數(shù)執(zhí)行下面的操作:
-
更新父進程和子進程的親屬關系。如果同一線程組中有正在運行的進程,就讓終止進程所創(chuàng)建的所有子進程都變成同一線程組中另外一個進程的子進程,否則讓它們成為 init 的子進程。
-
檢查被終止進程其進程描述符的 exit_signal 字段是否不等于 -1,并檢查進程是否是其所屬進程組的最后一個成員(注意:正常進程都會具有這些條件,參見前面 “clone()、fork() 和 vfork() 系統(tǒng)調用” 一節(jié)中對 copy_process() 的描述,第 16 步)。在這種情況下,函數(shù)通過給 正被終止進程的父進程發(fā)送一個信號(通常是 SIGCHLD),以通知父進程子進程死亡。
-
否則,也就是 exit_signal 字段等于 -1,或者線程組中還有其他進程,那么只要進程正在被跟蹤,就向父進程發(fā)送一個 SIGCHLD 信號(在這種情況下,父進程是調試程序,因而,向它報告輕量級進程死亡的信息)。
-
如果進程描述符的 exit_signal 字段等于 -1,而且進程沒有被跟蹤,就把進程描述符的 exit_state 字段置為 EXIT_DEAD,然后調用 release_task() 回收進程的其他數(shù)據(jù)結構占用的內(nèi)存,并遞減進程描述符的使用計數(shù)器(見下一節(jié))。使用記數(shù)變?yōu)闉?1(參見 copy_process() 函數(shù)的第 3f 步),以使進程描述符本身正好不會被釋放。
-
否則,如果進程描述符的 exit_signal 字段不等于 -1,或進程正在被跟蹤,就把 exit_state 字段置為 EXIT_ZOMBIE。在下一節(jié)我們將看到如何處理僵死進程。
-
把進程描述符的 flags 字段設置為 PF_DEAD 標志(參見第七章 " schedule() 函數(shù)" 一節(jié))。
-
-
調用 schedule() 函數(shù)(參見第七章)選擇一個新進程運行。調度程序忽略處于 EXIT_ZOMBIE 狀態(tài)的進程,所以這種進程正好在 schedule() 中的宏 switch_to 被調用之后停止執(zhí)行。正如在第七章我們將看到的:調度程序將檢查被替換的僵死進程描述符的 PF_DEAD 標志并遞減使用計數(shù)器,從而說明進程不再存活的事實。
5、進程刪除
??Unix 允許進程查詢內(nèi)核以獲得其父進程的 PID,或者其任何子進程的執(zhí)行狀態(tài)。例如,進程可以創(chuàng)建一個子進程來執(zhí)行特定的任務,然后調用諸如 wait() 這樣的一些庫函數(shù)檢查子進程是否終止。如果子進程已經(jīng)終止,那么,它的終止代號將告訴父進程這個任務是否已成功地完成。
??為了遵循這些設計選擇,不允許 Unix 內(nèi)核在進程一終止后就丟棄包含在進程描述符字段中的數(shù)據(jù)。只有父進程發(fā)出了與被終止的進程相關的 wait() 類系統(tǒng)調用之后,才允許這樣做。這就是引入僵死狀態(tài)的原因:盡管從技術上來說進程已死,但必須保存它的描述符,直到父進程得到通知。
??如果父進程在子進程結束之前結束會發(fā)生什么情況呢? 在這種情況下,系統(tǒng)中會到處是僵死的進程,而且它們的進程描述符永久占據(jù)著 RAM。如前所述,必須強迫所有的孤兒進程成為 init 進程的子進程來解決這個問題。這樣,init 進程在用 wait() 類系統(tǒng)調用檢查其合法的子進程終止時,就會撤消僵死的進程。
??release_task() 函數(shù)從僵死進程的描述符中分離出最后的數(shù)據(jù)結構,對僵死進程的處理有兩種可能的方式:如果父進程不需要接收來自子進程的信號,就調用 do_exit() ;如果已經(jīng)給父進程發(fā)送了一個信號,就調用 wait4() 或 waitpid() 系統(tǒng)調用。在后一種情況下,函數(shù)還將回收進程描述符所占用的內(nèi)存空間,而在前一種情況下,內(nèi)存的回收將由進程調度程序來完成(參見第七章)。該函數(shù)執(zhí)行下述步驟:
- 遞減終止進程擁有者的進程個數(shù)。這個值存放在本章前面提到的 user_struct 結構中(參見 copy_process() 的第 4 步)。
- 如果進程正在被跟蹤,函數(shù)將它從調試程序的 ptrace_children 鏈表中刪除,并讓該進程重新屬于初始的父進程。
- 調用 __exit_signal() 刪除所有的掛起信號并釋放進程的 signal_struct 描述符。如果該描述符不再被其他的輕量級進程使用,函數(shù)進一步刪除這個數(shù)據(jù)結構。此外,函數(shù)調用 exit_itimers() 從進程中剝離掉所有的 POSIX 時間間隔定時器。
- 調用 __exit_sighand() 刪除信號處理函數(shù)。
- 調用 _unhash_process(),該函數(shù)依次執(zhí)行下面的操作:
- 變量 nr_threads 減 1。
- 兩次調用 detach_pid(),分別從 PIDTYPE_PID 和 PIDTYPE_TGID 類型的 PID 散列表中刪除進程描述符。
- 如果進程是線程組的領頭進程,那么再調用兩次 detach_pid(),從 PIDTYPE_PGID 和 PIDTYPE_SID 類型的散列表中刪除進程描述符。
- 用宏 REMOVE_LINKS 從進程鏈表中解除進程描述符的鏈接。
- 如果進程不是線程組的領頭進程,領頭進程處于僵死狀態(tài),而且進程是線程組的最后一個成員,則該函數(shù)向領頭進程的父進程發(fā)送一個信號,通知它進程已死亡。
- 調用 sched_exit() 函數(shù)來調整父進程的時間片(這一步在邏輯上作為對 copy_process() 第 17 步的補充)。
- 調用 put_task_struct() 遞減進程描述符的使用計數(shù)器,如果計數(shù)器變?yōu)?0,則函數(shù)終止所有殘留的對進程的引用。
- 遞減進程所有者的 user_struct 數(shù)據(jù)結構的使用計數(shù)器(__count 字段)(參見 copy_process() 的第 5 步),如果使用計數(shù)器變?yōu)?0,就釋放該數(shù)據(jù)結構。
- 釋放進程描述符以及 thread_info 描述符和內(nèi)核態(tài)堆棧所占用的內(nèi)存區(qū)域。
四、中斷和異常
??中斷處理是由內(nèi)核執(zhí)行的最敏感的任務之一,因為它必須滿足下列約束:
- 當內(nèi)核正打算去完成一些別的事情時,中斷隨時會到來。因此,內(nèi)核的目標就是讓中斷盡可能快地處理完,盡其所能把更多的處理向后推遲。例如,假設一個數(shù)據(jù)塊已到達了網(wǎng)線,當硬件中斷內(nèi)核時,內(nèi)核只簡單地標志數(shù)據(jù)到來了,讓處理器恢復到它以前運行的狀態(tài),其余的處理稍后再進行(如把數(shù)據(jù)移入一個緩沖區(qū),它的接收進程可以在緩沖區(qū)找到數(shù)據(jù)并恢復這個進程的執(zhí)行)。因此,內(nèi)核響應中斷后需要進行的操作分為兩部分:關鍵而緊急的部分,內(nèi)核立即執(zhí)行,其余推遲的部分,內(nèi)核隨后執(zhí)行。
- 因為中斷隨時會到來,所以內(nèi)核可能正在處理其中的一個中斷時,另一個中斷(不同類型)又發(fā)生了。應該盡可能多地允許這種情況發(fā)生,因為這能維持更多的 I/O 設備處于忙狀態(tài)(參見 “中斷和異常處理程序的嵌套執(zhí)行” 一節(jié))。因此,中斷處理程序必須編寫成使相應的內(nèi)核控制路徑能以嵌套的方式執(zhí)行。當最后一個內(nèi)核控制路徑終止時,內(nèi)核必須能恢復被中斷進程的執(zhí)行,或者,如果中斷信號已導致了重新調度,內(nèi)核能切換到另外的進程。
- 盡管內(nèi)核在處理前一個中斷時可以接受一個新的中斷,但在內(nèi)核代碼中還是存在一些臨界區(qū),在臨界區(qū)中,中斷必須被禁止。必須盡可能地限制這樣的臨界區(qū),因為根據(jù)以前的要求,內(nèi)核,尤其是中斷處理程序,應該在大部分時間內(nèi)以開中斷的方式運行。
1、中斷和異常
??Intel 文檔把中斷和異常分為以下幾類:
- 中斷:
- 可屏蔽中斷 (maskable interrupt)
I/O 設備發(fā)出的所有中斷請求(IRQ)都產(chǎn)生可屏蔽中斷??善帘沃袛嗫梢蕴幱趦煞N狀態(tài):屏蔽的(masked)或非屏蔽的(unmasked),一個屏蔽的中斷只要還是屏蔽的,控制單元就忽略它。 - 非屏蔽中斷 (nonmaskable Interrupt)
只有幾個危急事件(如硬件故障)才引起非屏蔽中斷。非屏蔽中斷總是由 CPU 辨認。
- 可屏蔽中斷 (maskable interrupt)
- 異常:
- 處理器探測異常 (processor-detected exception)
當 CPU 執(zhí)行指令時探測到的一個反常條件所產(chǎn)生的異常。可以進一步分為三組,這取決于 CPU 控制單元產(chǎn)生異常時保存在內(nèi)核態(tài)堆棧 eip 寄存器中的值。- 故障 (fault)
通??梢约m正,一旦糾正,程序就可以在不失連貫性的情況下重新開始。保存在 eip 中的值是引起故障的指令地址,因此,當異常處理程序終止時,那條指令會被重新執(zhí)行。我們將在第九章的 “缺頁異常處理程序” 一節(jié)中看到,只要處理程序能糾正引起異常的反常條件,重新執(zhí)行同一指令就是必要的。 - 陷阱(trap)
在陷阱指令執(zhí)行后立即報告;內(nèi)核把控制權返回給程序后就可以繼續(xù)它的執(zhí)行而不失連貫性。保存在 eip 中的值是一個隨后要執(zhí)行的指令地址。只有當沒有必要重新執(zhí)行已終止的指令時,才觸發(fā)陷阱。陷阱的主要用途是為了調試程序。在這種情況下,中斷信號的作用是通知調試程序一條特殊指令已被執(zhí)行(例如到了一個程序內(nèi)的斷點)。一旦用戶檢查到調試程序所提供的數(shù)據(jù),她就可能要求被調試程序從下一條指令重新開始執(zhí)行。 - 異常中止(abort)
發(fā)生一個嚴重的錯誤:控制單元出了問題,不能在 eip 寄存器中保存引起異常的指令所在的確切位置。異常中止用于報告嚴重的錯誤,如硬件故障或系統(tǒng)表中無效的值或不一致的值。由控制單元發(fā)送的這個中斷信號是緊急信號,用來把控制權切換到相應的異常中止處理程序,這個異常中止處理程序除了強制受影響的進程終止外,沒有別的選擇。 - 編程異常 (programmed exception)
在編程者發(fā)出請求時發(fā)生。是由 int 或 int3 指令觸發(fā)的;當 into(檢查溢出)和 bound(檢查地址出界)指令檢查的條件不為真時,也引起編程異常。控制單元把編程異常作為陷阱來處理。編程異常通常也叫做軟中斷(software interrupt)。這樣的異常有兩種常用的用途:執(zhí)行系統(tǒng)調用及給調試程序通報一個特定的事件(參見第十章)。
- 故障 (fault)
- 處理器探測異常 (processor-detected exception)
??每個中斷和異常是由 0 ~ 255 之間的一個數(shù)來標識。因為一些未知的原因,Intel 把這個 8 位的無符號整數(shù)叫做一個向量(vector)。非屏蔽中斷的向量和異常的向量是固定的,而可屏蔽中斷的向量可以通過對中斷控制器的編程來改變(參見下一節(jié))。
(1)IRQ 和中斷
??每個能夠發(fā)出中斷請求的硬件設備控制器都有一條名為 IRQ(Interrupt ReQuest)的輸出線(注1)。所有現(xiàn)有的 IRQ 線(IRQ line)都與一個名為可編程中斷控制器(Programmable Interrpt Controuer PIC)的硬件電路的輸入引腳相連。可編程中斷控制器執(zhí)行下列動作:
- 監(jiān)視 IRQ 線。檢查產(chǎn)生的信號(raised signal)。如果有條或兩條以上的 IRQ 線上產(chǎn)生信號,就選擇引腳編號較小的 IRQ 線。
- 如果一個引發(fā)信號出現(xiàn)在 IRQ 線上:
- 把接收到的引發(fā)信號轉換成對應的向量。
- 把這個向量存放在中斷控制器的一個I/O 端口,從而允許 CPU 通過數(shù)據(jù)總線讀此向量
- 把引發(fā)信號發(fā)送到處理器的 INTR 引腳,即產(chǎn)生一個中斷。
- 等待,直到 CPU 通過把這個中斷信號寫進可編程中斷控制器的一個 I/O 端口來確認它:當這種情況發(fā)生時,清 INTR 線。
- 返回到第 1 步。
??IRQ 線是從 0 開始順序編號的,因此,第一條 IRQ 線通常表示成 IRQ0。與 IRQn 關聯(lián)的 Intel 的缺省向量是 n+32。如前所述,通過向中斷控制器端口發(fā)布合適的指令,就可以修改 IRQ 和向量之間的映射。
??可以有選擇地禁止每條 IRQ 線。因此,可以對 PIC 編程從而禁止 IRQ,也就是說,可以告訴 PIC 停止對給定的 IRQ 線發(fā)布中斷,或者激話它們。禁止的中斷是丟失不了的,它們一旦被激活,PIC 就又把它們發(fā)送到 CPU。這個特點被大多數(shù)中斷處理程序使用,因為這允許中斷處理程序逐次地處理同一類型的 IRQ。
??有選擇地激活/禁止 IRQ 線不同于可屏蔽中斷的全局屏蔽 /非屏蔽。當 eflags 寄存器的 IF 標志被清 0 時,由 PIC 發(fā)布的每個可屏蔽中斷都由 CPU 暫時忽略。cli 和 sti 匯編指令分別清除和設置該標志??蓞⒖?==> 1、標志寄存器
??傳統(tǒng)的 PIC 是由兩片 8259A 風格的外部芯片以 “級聯(lián)” 的方式連接在一起的。每個芯片可以處理多達 8 個不同的 IRQ 輸入線。因為從 PIC 的 INT 輸出線連接到主 PIC 的 IRQ2 引腳,因此,可用 IRQ 線的個數(shù)限制為 15。
(2)高級可編程中斷控制器
??以前的描述僅涉及為單處理器系統(tǒng)設計的 PIC。如果系統(tǒng)只有一個單獨的 CPU,那么主 PIC 的輸出線以直接了當?shù)姆绞竭B接到 CPU 的 INTR 引腳。然而,如果系統(tǒng)中包含兩個或多個 CPU,那么這種方式不再有效,因而需要更復雜的 PIC。
??為了充分發(fā)揮 SMP 體系結構的并行性,能夠把中斷傳遞給系統(tǒng)中的每個 CPU 至關重要。基于此理由,Intel 從 Pentiun III 開始引入了一種名為 I/O 高級可編程控制器(I/O Advanced Programmable Interrupt Controller,I/O APIC)的新組件,用以代替老式的 8259A 可編程中斷控制器。新近的主板為了支持以前的操作系統(tǒng)都包括兩種芯片。此外,80x86 微處理器當前所有的 CPU 都含有一個本地 APIC。每個本地 APIC 都有 32 位的寄存器、一個內(nèi)部時鐘、一個本地定時設備及為本地 APIC 中斷保留的兩條額外的 IRQ 線 LINT0 和 LINT1。所有本地 APIC 都連接到一個外部 I/O APIC,形成一個多 APIC 的系統(tǒng)。
??圖 4-1 以示意圖的方式顯示了一個多 APIC 系統(tǒng)的結構。一條 APIC 總線把 “前端” I/O APIC 連接到本地 APIC。來自設備的 IRQ 線連接到 I/O APIC,因此,相對于本地 APIC,I/O APIC 起路由器的作用。在 Pentium III 和早期處理器的母板上,APIC 總線是一個串行三線總線;從 Pentium 4 開始,APIC 總線通過系統(tǒng)總線來實現(xiàn)。不過,因為 APIC 總線及其信息對軟件是不可見的,因此,我們不做進一步的詳細討論。
??I/O APIC 的組成為:一組 24 條 IRQ 線、一張 24 項的中斷重定向表(Interrupt Redirection Table)、可編程寄存器,以及通過 APIC 總線發(fā)送和接收 APIC 信息的一個信息單元。與 8259A 的 IRQ 引腳不同,中斷優(yōu)先級并不與引腳號相關聯(lián):中斷重定向表中的每一項都可以被單獨編程以指明中斷向量和優(yōu)先級、目標處理器及選擇處理器的方式。重定向表中的信息用于把每個外部 IRQ 信號轉換為一條消息,然后,通過 APIC 總線把消息發(fā)送給一個或多個本地 APIC 單元。
??來自外部硬件設備的中斷請求以兩種方式在可用 CPU 之間分發(fā):
-
靜態(tài)分發(fā)
IRQ 信號傳遞給重定向表相應項中所列出的本地 APIC。中斷立即傳遞給一個特定的 CPU,或一組 CPU,或所有 CPU(廣播方式)。 -
動態(tài)分發(fā)
如果處理器正在執(zhí)行最低優(yōu)先級的進程,IRQ 信號就傳遞給這種處理器的本地 APIC。每個本地 APIC 都有一個可編程任務優(yōu)先級寄存器(task priority register,TPR), TPR 用來計算當前運行進程的優(yōu)先級。Intel 希望在操作系統(tǒng)內(nèi)核中通過每次進程切換對這個寄存器存器進行修改。
如果兩個或多個 CPU 共享最低優(yōu)先級,就利用仲裁(arbitration)技術在這些 CPU 之間分配負荷。在本地 APIC 的仲裁優(yōu)先級寄存器中,給每個 CPU 都分配一個 0(最低)~ 15(最高) 范圍內(nèi)的值。
每當中斷傳遞給一個 CPU 時、其相應的仲裁優(yōu)先級就自動置為 0,而其他每個 CPU 的仲裁優(yōu)先級都增加 1 。當仲裁優(yōu)先級寄存器大于 15 時,就把它置為獲勝 CPU 的前一個仲裁優(yōu)先級加 1。因此,中斷以輪轉方式在 CPU 之間分發(fā),且具有相同的任務優(yōu)先級(注 2)。
??除了在處理器之間分發(fā)中斷外,多 APIC 系統(tǒng)還允許 CPU 產(chǎn)生處理器間中斷(interprocessor interrupt)。當一個 CPU 希望把中斷發(fā)給另一個 CPU 時,它就在自己本地 APIC 的中斷指令寄存器 (Interrupt Command Register ,ICR)中存放這個中斷向量和目標本地 APIC 的標識符。然后,通過 APIC 總線向目標本地 APIC 發(fā)送一條消息,從而向自己的 CPU 發(fā)出一個相應的中斷。
??處理器間中斷(簡稱 IPI)是 SMP 體系結構至關重要的組成部分,并由 Linux 有效地用來在 CPU 之間交換信息(參見本章后面)。
??目前大部分單處理器系統(tǒng)都包含一個 I/O APIC 芯片,可以用以下兩種方式對這種芯片進行配置:
- 作為一種標準 8259A 方式的外部 PIC 連接到 CPU。本地 APIC 被禁止,兩條 LINT0 和 LINT1 本地 IRQ 線分別配置為 INTR 和 NMI 引腳。
- 作為一種標準外部 I/O APIC 。本地 APIC 被激活,且所有的外部中斷都通過 I/O APIC 接收。
(3)異常
??80x86 微處理器發(fā)布了大約 20 種不同的異常(注3)。內(nèi)核必須為每種異常提供一個專門的異常處理程序。對于某些異常,CPU 控制單元在開始執(zhí)行異常處理程序前會產(chǎn)生一個硬件出錯碼(hardware error code),并且壓入內(nèi)核態(tài)堆棧。
??下面的列表給出了在 80x86 處理器中可以找到的異常的向量、名字、類型及其簡單描述。更多的信息可以在 Intel 的技術文擋中找到。
-
0 —— “Divide error”(故障)
當一個程序試圖執(zhí)行整數(shù)被 0 除操作時產(chǎn)生。 -
1 —— “Debug”(陷阱或故障)
產(chǎn)生于:(1)設置 eflags 的 TF 標志時(對于實現(xiàn)調試程序的單步執(zhí)行是相當有用的),(2)一條指令或操作數(shù)的地址落在一個活動 debug 寄存器的范圍之內(nèi)(參見第三章的 “硬件上下文” 一節(jié))。 -
2 —— 未用
為非屏蔽中斷保留(利用 NMI 引腳的那些中斷)。 -
3 —— “Breakpoint”(陷阱)
由 int3 (斷點)指令(通常由 debugger 插入)引起。 -
4 —— “Overflow”(陷阱)
當 eflags 的 OF (overflow)標志被設置時,into(檢查溢出)指令被執(zhí)行。 -
5 —— “Bounds check”(故障)
對于有效地址范圍之外的操作數(shù),bound(檢查地址邊界)指令被執(zhí)行。 -
6 ——“Invalid opcode” (故障)
CPU 執(zhí)行單元檢測到一個無效的操作碼(決定執(zhí)行操作的機器指令部分)。 -
7 —— “Device not available”(故障)
隨著 cr0 的 TS 標志被設置,ESCAPE、MMX 或 XMM 指令被執(zhí)行(參見第三章的"保存和加載 FPU、MMX 及 XMM 寄存器"一節(jié))。 -
8 —— “Double fault”(異常中止)
正常情況下,當 CPU 正試圖為前一個異常調用處理程序時,同時又檢測到一個異常,兩個異常能被串行地處理。然而,在少數(shù)情況下,處理器不能串行地處理它們,因而產(chǎn)生這種異常。 -
9 —— “Coprocessor segment overrun”(異常中止)
因外部的數(shù)學協(xié)處理器引起的問題(僅用于 80386 微處理器)。 -
10 —— “Invalid TSS”(故障)
CPU 試圖讓一個上下文切換到有無效的 TSS 的進程。 -
11 ——“Segment not present”(故障)
引用一個不存在的內(nèi)存段(段描述符的 Segment -Presert 標志被清 0)。 -
12 —— “Stack segment fault”(故障)
試圖超過棧段界限的指令,或者由 ss 標識的段不在內(nèi)存。 -
13 —— "General protection" (故障)
違反了 80x86 保護模式下的保護規(guī)則之一。 -
14 —— “Page fault” (故障)
尋址的頁不在內(nèi)存,相應的頁表項為空,或者違反了一種分頁保護機制。 -
15 —— 由 Intel 保留
-
16 —— “Floating point error” (故障)
集成到 CPU 芯片中的浮點單元用信號通知一個錯誤情形,如數(shù)字溢出,或被 0 除(注4)。 -
17 —— “Alignment check”(故障)
操作數(shù)的地址沒有被正確地對齊(例如,一個長整數(shù)的地址不是 4 的倍數(shù))。 -
18 —— “Machine check”(異常中止)
機器檢查機制檢測到一個 CPU 錯誤或總線錯誤。 -
19 —— “SIMD floating point exception”(故障)
集成到 CPU 芯片中的 SSE 或 SSE2 單元對浮點操作用信號通知一個錯誤情形。 -
20~31 這些值由 Intel 留作將來開發(fā)。如表 4-1 所示,每個異常都由專門的異常處理程序來處理(參見本章后面的"異常處理"一節(jié)),它們通常把一個 Unix 信號發(fā)送到引起異常的進程。
(4)中斷描述符表
??可參考 ? 5、中斷描述符表
??Linux 利用中斷門處理中斷,利用陷阱門處理異常。
(5)中斷和異常的硬件處理
??我們現(xiàn)在描述 CPU 控制單元如何處理中斷和異常。我們假定內(nèi)核已被初始化,因此,CPU 在保護模式下運行。
??當執(zhí)行了一條指令后,cs 和 eip 這對寄存器包含下一條將要執(zhí)行的指令的邏輯地址。在處理那條指令之前,控制單元會檢查在運行前一條指令時是否已經(jīng)發(fā)生了一個中斷或異常。如果發(fā)生了一個中斷或異常,那么控制單元執(zhí)行下列操作:
-
確定與中斷或異常關聯(lián)的向量 i(0 < i < 255)。
-
讀由 idtr 寄存器指向的 IDT 表中的第 i 項(在下面的描述中,我們假定 IDT 表項中包含的是一個中斷門或一個陷阱門)。
-
從 gdtr 寄存器獲得 GDT 的基地址 并在 GDT 中查找,以讀取 IDT 表項中的選擇符所標識的段描述符。這個描述符指定中斷或異常處理程序所在段的基地址。
-
確信中斷是由授權的(中斷)發(fā)生源發(fā)出的。首先將當前特權級 CPL(存放在 cs 寄存器的低兩位)與段描述符(存放在 GDT 中)的描述符特權級 DPL 比較,如果 CPL 小于 DPL,就產(chǎn)生一個 “General protection” 異常,因為中斷處理程序的特權不能低于引起中斷的程序的特權。對于編程異常,則做進一步的安全檢查:比較 CPL 與處于 IDT 中的門描述符的 DPL,如果 DPL 小于 CPL ,就產(chǎn)生一個 “General protection” 異常。這最后一個檢查可以避免用戶應用程序訪問特殊的陷阱門或中斷門。
-
檢查是否發(fā)生了特權級的變化,也就是說,CPL 是否不同于所選擇的段描述符的 DPL。如果是,控制單元必須開始使用與新的特權級相關的棧。通過執(zhí)行以下步驟來做到這點:
- 讀 tr 寄存器,以訪問運行進程的 TSS 段。
- 用與新特權級相關的棧段和棧指針的正確值裝載 ss 和 esp 寄存器。這些值可以在 TSS 中找到(參見第三章的"任務狀態(tài)段"一節(jié))
- 在新的棧中保存 ss 和 esp 以前的值,這些值定義了與舊特權級相關的棧的邏輯地址。
-
如果故障已發(fā)生,用引起異常的指令地址裝載 cs 和 eip 寄存器,從而使得這條指令能再次被執(zhí)行。
-
在棧中保存 eflags、cs 及 eip 的內(nèi)容。
-
如果異常產(chǎn)生了一個硬件出錯碼,則將它保存在棧中。
-
裝載 cs 和 eip 寄存器,其值分別是 IDT 表中第 i 項門描述符的段選擇符和偏移量字段。這些值給出了中斷或者異常處理程序的第一條指令的邏輯地址。
??控制單元所執(zhí)行的最后一步就是跳轉到中斷或者異常處理程序。換句話說,處理完中斷信號后,控制單元所執(zhí)行的指令就是被選中處理程序的第一條指令。
??中斷或異常被處理完后,相應的處理程序必須產(chǎn)生一條 iret 指令,把控制權轉交給被中斷的進程,這將迫使控制單元:
- 用保存在棧中的值裝載 cs、eip 或 eflags 寄存器。如果一個硬件出錯碼曾被壓入棧中,并且在 eip 內(nèi)容的上面,那么,執(zhí)行 iret 指令前必須先彈出這個硬件出錯碼。
- 檢查處理程序的 CPL 是否等于 cs 中最低兩位的值(這意味著被中斷的進程與處理程序運行在同一特權級)。如果是,iret 終止執(zhí)行;否則,轉入下一步。
- 從棧中裝載 ss 和 esp 寄存器,因此,返回到與舊特權級相關的棧。
- 檢查 ds、es、fs 及 gs 段寄存器的內(nèi)容,如果其中一個寄存器包含的選擇符是一個段描述符,并且其 DPL 值小于 CPL,那么,清相應的段寄存器??刂茊卧@么做是為了禁止用戶態(tài)的程序(CPL=3)利用內(nèi)核以前所用的段寄存器(DPL=0)。如果不清這些寄存器,懷有惡意的用戶態(tài)程序就可能利用它們來訪問內(nèi)核地址空間。
2、中斷和異常處理程序的嵌套執(zhí)行
??每個中斷或異常都會引起一個內(nèi)核控制路徑,或者說代表當前進程在內(nèi)核態(tài)執(zhí)行單獨的指令序列。內(nèi)核控制路徑可以任意嵌套。一個中斷處理程序可以被另一個中斷處理程序 “中斷” 。
??允許內(nèi)核控制路徑嵌套執(zhí)行必須付出代價,那就是中斷處理程序必須永不阻塞,換句話說,中斷處理程序運行期間不能發(fā)生進程切換。
??假定內(nèi)核沒有 bug,那么大多數(shù)異常就只在 CPU 處于用戶態(tài)時發(fā)生。事實上,異常要么是由編程錯誤引起,要么是由調試程序觸發(fā)。然而,“Page Fault(缺頁)” 異常發(fā)生在內(nèi)核態(tài)。這發(fā)生在當進程試圖對屬于其地址空間的頁進行尋址,而該頁現(xiàn)在不在 RAM 中時。當處理這樣的一個異常時,內(nèi)核可以掛起當前進程,并用另一個進程代替它,直到請求的頁可以使用為止。只要被掛起的進程又獲得處理器,處理缺頁異常的內(nèi)核控制路徑就恢復執(zhí)行。
??因為 “Page Fault” 異常處理程序從不進一步引起異常,所以與異常相關的至多兩個內(nèi)核控制路徑(第一個由系統(tǒng)調用引起,第二個由缺頁引起)會堆疊在一起,一個在另一個之上。
??與異常形成對照的是,盡管處理中斷的內(nèi)核控制路徑代表當前進程運行,但由 I/O 設備產(chǎn)生的中斷并不引用當前進程的專有數(shù)據(jù)結構。事實上,當一個給定的中斷發(fā)生時,要預測哪個進程將會運行是不可能的。
??一個中斷處理程序既可以搶占其他的中斷處理程序,也可以搶占異常處理程序。相反,異常處理程序從不搶占中斷處理程序。
在內(nèi)核態(tài)能觸發(fā)的唯一異常就是剛剛描述的缺頁異常。但是,中斷處理程序從不執(zhí)行可以導致缺頁(因此意味著進程切換)的操作。
??基于以下兩個主要原因,Linux 交錯執(zhí)行內(nèi)核控制路徑:
-
為了提高可編程中斷控制器和設備控制器的吞吐量。假定設備控制器在一條 IRQ 線上產(chǎn)生了一個信號,PIC 把這個信號轉換成一個外部中斷,然后 PIC 和設備控制器保持阻塞,一直到 PIC 從 CPU 處接收到一條應答信息。由于內(nèi)核控制路徑的交錯執(zhí)行,內(nèi)核即使正在處理前一個中斷,也能發(fā)送應答。
-
為了實現(xiàn)一種沒有優(yōu)先級的中斷模型。因為每個中斷處理程序都可以被另一個中斷處理程序延緩,因此,在硬件設備之間沒必要建立預定義優(yōu)先級。這就簡化了內(nèi)核代碼,提高了內(nèi)核的可移植性。
??在多處理器系統(tǒng)上,幾個內(nèi)核控制路徑可以并發(fā)執(zhí)行。此外,與異常相關的內(nèi)核控制路徑可以開始在一個 CPU 上執(zhí)行,并且由于進程切換而移往另一個 CPU 上執(zhí)行。
(1)中斷門、陷阱門及系統(tǒng)門
??與在前面 “中斷描述符表” 中所提到的一樣,Intel 提供了三種類型的中斷描述符:任務門、中斷門及陷阱門描述符。Linux 使用與 Intel 稍有不同的細目分類和術語,把它們?nèi)缦逻M行分類:
-
中斷門 (interrupt gate)
用戶態(tài)的進程不能訪問的一個 Intel 中斷門(門的 DPL 字段為 0)。所有的 Linux 中斷處理程序都通過中斷門激活,并全部限制在內(nèi)核態(tài)。 -
系統(tǒng)門 (system gate)
用戶態(tài)的進程可以訪問的一個 Intel 陷阱門(門的 DPL 字段為 3)。通過系統(tǒng)門來激活三個 Linux 異常處理程序,它們的向量是 4,5 及 128,因此,在用戶態(tài)下,可以發(fā)布 into、bound 及 int $0x80 三條匯編語言指令。 -
系統(tǒng)中斷門 (system interrupt gate)
能夠被用戶態(tài)進程訪問的 Intel 中斷門(門的 DPL 字段為 3)。與向量 3 相關的異常處理程序是由系統(tǒng)中斷門激活的,因此,在用戶態(tài)可以使用匯編話言指令 int3。 -
陷阱門 (trap gate)
用戶態(tài)的進程不能訪問的一個 Intel 陷阱門(門的 DPL 字段為 0)。大部分 Linux 異常處理程序都通過陷阱門來激活。 -
任務門 (task gate)
不能被用戶態(tài)進程訪問的 Intel 任務門(門的 DPL 字段為 0)。Linux 對 “Double fault” 異常的處理程序是由任務門激活的。
下列體系結構相關的函數(shù)用來在 IDT 中插入門:
- set_intr_gate(n,addr)
在 IDT 的第 n 個表項插入一個中斷門。門中的段選擇符設置成內(nèi)核代碼的段選擇符,偏移量設置為中斷處理程序的地址 addr,DPL 字段設置為 0。 - set_system_gate(n,addr)
在 IDT 的第 n 個表項插入一個陷阱門。門中的段選擇符設置成內(nèi)核代碼的段選擇符,偏移量設置為異常處理程序的地址 addr,DPL 字段設置為 3。 - set_system_intr_gate(n,addr)
在 IDT 的第 n 個表項插入一個中斷門。門中的段選擇符設置成內(nèi)核代碼的段選擇符,偏移量設置為異常處理程序的地址 addr,DPL 字段設置為 3。 - set_trap_gate(n,addr)
與前一個函數(shù)類似,只不過 DPL 的字段設置成 0。 - set_task_gate(n, gdt)
在 IDT 的第 n 個表項插入一個中斷門。門中的段選擇符中存放一個 TSS 的全局描述符表的指針,該 TSS 中包含要被激活的函數(shù)。偏移量設置為 0,而 DPL 字段設置為 3。
3、異常處理
??CPU 產(chǎn)生的大部分異常都由 Linux 解釋為出錯條件。當其中一個異常發(fā)生時,內(nèi)核就向引起異常的進程發(fā)送一個信號向它通知一個反常條件。例如,如果進程執(zhí)行了一個被 0 除的操作,CPU 就產(chǎn)生一個 “Divide error” 異常,并由相應的異常處理程序向當前進程發(fā)送一個 SIGFPE 信號,這個進程將采取若干必要的步驟來(從出錯中)恢復或者中止運行(如果沒有為這個信號設置處理程序的話)。
??但是,在兩種情況下,Linux 利用 CPU 異常更有效地管理硬件資源。第一種情況已經(jīng)在第三章 “保存和加載 FPU、MMX 及 XMM 寄存器” 一節(jié)描述過,“Device not availeble” 異常與 cr0 寄存器的 TS 標志一起用來把新值裝入浮點寄存器。第二種情況指的是 “Page Fault” 異常,該異常推遲給進程分配新的頁框,直到不能再推遲為止。相應的處理程序比較復雜,因為異??赡鼙硎疽粋€錯誤條件,也可能不表示一個錯誤條件(參見第九章 “缺頁異常處理程序” 一節(jié))。
??異常處理程序有一個標準的結構,由以下三部分組成:
- 在內(nèi)核堆棧中保存大多數(shù)寄存器的內(nèi)容(這部分用匯編語言實現(xiàn))。
- 用高級的 C 函數(shù)處理異常。
- 通過 ret_from_exception() 函數(shù)從異常處理程序退出。
??為了利用異常,必須對 IDT 進行適當?shù)某跏蓟?#xff0c;使得每個被確認的異常都有一個異常處理程序。trap_init() 函數(shù)的工作是將一些最終值(即處理異常的函數(shù))插入到 IDT 的非屏蔽中斷及異常表項中。這是由函數(shù) set_trap_gate()、set_intr_gate()、set_system_gate()、set_system_intr_gate() 和 set_task_gate() 來完成的。
// arch/x86/kernel/traps.c
void __init trap_init(void)
{int i;#ifdef CONFIG_EISAvoid __iomem *p = early_ioremap(0x0FFFD9, 4);if (readl(p) == 'E' + ('I'<<8) + ('S'<<16) + ('A'<<24))EISA_bus = 1;early_iounmap(p, 4);
#endifset_intr_gate(0, ÷_error);set_intr_gate_ist(1, &debug, DEBUG_STACK);set_intr_gate_ist(2, &nmi, NMI_STACK);/* int3 can be called from all */set_system_intr_gate_ist(3, &int3, DEBUG_STACK);/* int4 can be called from all */set_system_intr_gate(4, &overflow);set_intr_gate(5, &bounds);set_intr_gate(6, &invalid_op);set_intr_gate(7, &device_not_available);
#ifdef CONFIG_X86_32set_task_gate(8, GDT_ENTRY_DOUBLEFAULT_TSS);
#elseset_intr_gate_ist(8, &double_fault, DOUBLEFAULT_STACK);
#endifset_intr_gate(9, &coprocessor_segment_overrun);set_intr_gate(10, &invalid_TSS);set_intr_gate(11, &segment_not_present);set_intr_gate_ist(12, &stack_segment, STACKFAULT_STACK);set_intr_gate(13, &general_protection);set_intr_gate(14, &page_fault);set_intr_gate(15, &spurious_interrupt_bug);set_intr_gate(16, &coprocessor_error);set_intr_gate(17, &alignment_check);
#ifdef CONFIG_X86_MCEset_intr_gate_ist(18, &machine_check, MCE_STACK);
#endifset_intr_gate(19, &simd_coprocessor_error);/* Reserve all the builtin and the syscall vector: */for (i = 0; i < FIRST_EXTERNAL_VECTOR; i++)set_bit(i, used_vectors);#ifdef CONFIG_IA32_EMULATIONset_system_intr_gate(IA32_SYSCALL_VECTOR, ia32_syscall);set_bit(IA32_SYSCALL_VECTOR, used_vectors);
#endif#ifdef CONFIG_X86_32if (cpu_has_fxsr) {printk(KERN_INFO "Enabling fast FPU save and restore... ");set_in_cr4(X86_CR4_OSFXSR);printk("done.\n");}if (cpu_has_xmm) {printk(KERN_INFO"Enabling unmasked SIMD FPU exception support... ");set_in_cr4(X86_CR4_OSXMMEXCPT);printk("done.\n");}set_system_trap_gate(SYSCALL_VECTOR, &system_call);set_bit(SYSCALL_VECTOR, used_vectors);
#endif/** Should be a barrier for any external CPU state:*/cpu_init();x86_init.irqs.trap_init();
}
4、中斷處理
??正如前面解釋的那樣,內(nèi)核只要給引起異常的進程發(fā)送一個 Unix 信號就能處理大多數(shù)異常。因此,要采取的行動被延遲,直到進程接收到這個信號。所以,內(nèi)核能很快地處理異常。
??這種方法并不適合中斷,因為經(jīng)常會出現(xiàn)一個進程(例如,一個請求數(shù)據(jù)傳輸?shù)倪M程)被掛起好久后中斷才到達的情況,因此,一個完全無關的進程可能正在運行。所以,給當前進程發(fā)送一個 Unix 信號是毫無意義的。
??中斷處理依賴于中斷類型。就我們的目的而言,我們將討論三種主要的中斷類型:
- I/O 中斷
某些 I/O 設備需要關注:相應的中斷處理程序必須查詢設備以確定適當?shù)牟僮鬟^程。我們在后面 “I/O 中斷處理” 一節(jié)將描述這種中斷。 - 時鐘中斷
某種時鐘(或者是一個本地 APIC 時鐘,或者是一個外部時鐘)產(chǎn)生一個中斷;這種中斷告訴內(nèi)核一個固定的時間間隔已經(jīng)過去。這些中斷大部分是作為 I/O 中斷來處理的,我們將在第六章討論時鐘中斷的具體特征。 - 處理器間中斷
多處理器系統(tǒng)中一個 CPU 對另一個 CPU 發(fā)出一個中斷。我們在后面 “處理器間中斷處理” 一節(jié)將討論這種中斷。
(1)I/O 中斷處理
??一般而言,I/O 中斷處理程序必須足夠靈活以給多個設備同時提供服務。例如在 PCI 總線的體系結構中,幾個設備可以共享同一個 IRQ 線。這就意味著僅僅中斷向量不能說明所有問題。在表 4-3 所示的例子中,同一個向量 43 既分配給 USB 端口,也分配給聲卡。然而,在老式 PC 體系結構(像 ISA)中發(fā)現(xiàn)的一些硬件設備,當它們的 IRQ 與其他設備共享時,就不能可靠地運轉。
中斷處理程序的靈活性是以兩種不同的方式實現(xiàn)的,討論如下:
-
IRQ 共享
中斷處理句柄執(zhí)行多個中斷服務例程(interrupt service routine, ISR)。每個 ISR 是一個與單獨設備(共享 IRQ 線)相關的函數(shù)。因為不可能預先知道哪個特定的設備產(chǎn)生 IRQ,因此,每個 ISR 都被執(zhí)行,以驗證它的設備是否需要關注:如果是,當設備產(chǎn)生中斷時,就執(zhí)行需要執(zhí)行的所有操作。 -
IRQ 動態(tài)分配
一條 IRQ 線在可能的最后時刻才與一個設備驅動程序相關聯(lián):例如,軟盤設備的 IRQ 線只有在用戶訪問軟盤設備時才被分配。這樣,即使幾個硬件設備并不共享 IRQ 線、同一個 IRQ 向量也可以由這幾個設備在不同時刻使用(見本節(jié)最后一部分的討論)。
??當一個中斷發(fā)生時,并不是所有的操作都具有相同的急迫性。事實上,把所有的操作都放進中斷處理程序本身并不合適。需要時間長的、非重要的操作應該推后,因為當一個中斷處理程序正在運行時,相應的 IRQ 線上發(fā)出的信號就被暫時忽略。更重要的是,中斷處理程序是代表進程執(zhí)行的,它所代表的進程必須總處于 TASK_RUNNING 狀態(tài),否則,就可能出現(xiàn)系統(tǒng)僵死情形。因此,中斷處理程序不能執(zhí)行任何阻塞過程,如磁盤 I/O 操作。因此,Linux 把緊隨中斷要執(zhí)行的操作分為三類:
-
緊急的(Critical)
這樣的操作諸如:對 PIC 應答中斷,對 PIC 或設備控制器重編程,或者修改由設備和處理器同時訪問的數(shù)據(jù)結構。這些都能被很快地執(zhí)行,而之所以說它們是緊急的是因為它們必須被盡快地執(zhí)行。緊急操作要在一個中斷處理程序內(nèi)立即執(zhí)行,而且是在禁止可屏蔽中斷的情況下。 -
非緊急的 (Noncritical)
這樣的操作諸如:修改那些只有處理器才會訪問的數(shù)據(jù)結構(例如,按下一個鍵后讀掃描碼)。這些操作也要很快地完成,因此,它們在開中斷的情況下,由中斷處理程序立即執(zhí)行。 -
非緊急可延遲的(Noncritical deferrable)
這樣的操作諸如:把緩沖區(qū)的內(nèi)容拷貝到某個進程的地址空間(例如,把鍵盤行緩沖區(qū)的內(nèi)容發(fā)送到終端處理程序進程)。這些操作可能被延遲較長的時間間隔而不影響內(nèi)核操作,有興趣的進程將會等待數(shù)據(jù)。非緊急可延遲的操作由獨立的函數(shù)來執(zhí)行,我們將在 “軟中斷及 tasklet” 一節(jié)討論。
??不管引起中斷的電路種類如何,所有的 I/O 中斷處理程序都執(zhí)行四個相同的基本操作:
- 在內(nèi)核態(tài)堆棧中保存 IRQ 的值和寄存器的內(nèi)容。
- 為正在給 IRQ 線服務的 PIC 發(fā)送一個應答、這將允許 PIC 進一步發(fā)出中斷。
- 執(zhí)行共享這個 IRQ 的所有設備的中斷服務例程(ISR)。
- 跳到 ret_from_intr() 的地址后終止。
??當中斷發(fā)生時,需要用幾個描述符來表示 IRQ 線的狀態(tài)和需要執(zhí)行的通數(shù)。圖 4-4 以示意圖的方式展示了處理一個中斷的硬件電路和軟件函數(shù)。下面幾節(jié)會討論這些函數(shù)。
(a)中斷向量
(b)IRQ 數(shù)據(jù)結構
??每個中斷向量都有它自己的 irq_desc_t 描述符,所有的這些描述符組織在一起形成 irq_desc 數(shù)組。
// include/linux/irq.h
/*** struct irq_desc - interrupt descriptor* @irq: interrupt number for this descriptor* @timer_rand_state: pointer to timer rand state struct* @kstat_irqs: irq stats per cpu* @irq_2_iommu: iommu with this irq* @handle_irq: highlevel irq-events handler [if NULL, __do_IRQ()]* @chip: low level interrupt hardware access* @msi_desc: MSI descriptor* @handler_data: per-IRQ data for the irq_chip methods* @chip_data: platform-specific per-chip private data for the chip* methods, to allow shared chip implementations* @action: the irq action chain* @status: status information* @depth: disable-depth, for nested irq_disable() calls* @wake_depth: enable depth, for multiple set_irq_wake() callers* @irq_count: stats field to detect stalled irqs* @last_unhandled: aging timer for unhandled count* @irqs_unhandled: stats field for spurious unhandled interrupts* @lock: locking for SMP* @affinity: IRQ affinity on SMP* @node: node index useful for balancing* @pending_mask: pending rebalanced interrupts* @threads_active: number of irqaction threads currently running* @wait_for_threads: wait queue for sync_irq to wait for threaded handlers* @dir: /proc/irq/ procfs entry* @name: flow handler name for /proc/interrupts output*/
struct irq_desc {unsigned int irq;struct timer_rand_state *timer_rand_state;unsigned int *kstat_irqs;
#ifdef CONFIG_INTR_REMAPstruct irq_2_iommu *irq_2_iommu;
#endifirq_flow_handler_t handle_irq;struct irq_chip *chip;struct msi_desc *msi_desc;void *handler_data;void *chip_data;struct irqaction *action; /* IRQ action list */unsigned int status; /* IRQ status */unsigned int depth; /* nested irq disables */unsigned int wake_depth; /* nested wake enables */unsigned int irq_count; /* For detecting broken IRQs */unsigned long last_unhandled; /* Aging timer for unhandled count */unsigned int irqs_unhandled;raw_spinlock_t lock;
#ifdef CONFIG_SMPcpumask_var_t affinity;unsigned int node;
#ifdef CONFIG_GENERIC_PENDING_IRQcpumask_var_t pending_mask;
#endif
#endifatomic_t threads_active;wait_queue_head_t wait_for_threads;
#ifdef CONFIG_PROC_FSstruct proc_dir_entry *dir;
#endifconst char *name;
} ____cacheline_internodealigned_in_smp;
五、內(nèi)核同步
1、同步原語
(1)每CPU 變量
(2)原子操作
(3)優(yōu)化和內(nèi)存屏障
??當使用優(yōu)化的編譯器時,你千萬不要認為指令會嚴格按它們在源代碼中出現(xiàn)的順序執(zhí)行。例如,編譯器可能重新安排匯編語言指令以使寄存器以最優(yōu)的方式使用。此外,現(xiàn)代 CPU 通常并行地執(zhí)行若干條指令,且可能重新安排內(nèi)存訪問。這種重新排序可以極大地加速程序的執(zhí)行。
??然而,當處理同步時,必須避免指令重新排序。如果放在同步原語之后的一條指令在同步原語本身之前執(zhí)行,事情很快就會變得失控。事實上,所有的同步原語起優(yōu)化和內(nèi)存屏障的作用。
(a)優(yōu)化屏障(optimization barrier)
??優(yōu)化屏障(optimization barrier)原語保證編譯程序不會混淆放在原語操作之前的匯編語言指令和放在原語操作之后的匯編語言指令,這些匯編語言指令在C中都有對應的語句。在 Linux 中,優(yōu)化屏障就是 barrier() 宏 。
// include/linux/compiler.h
# define barrier() __memory_barrier()// 展開為:
asm volatile("":::"memory")
??指令 asm 告訴編譯程序要插入?yún)R編語言片段(這種情況下為空)。volatile 關鍵字禁止編譯器把 asm 指令與程序中的其他指令重新組合。memory 關鍵字強制編譯器假定 RAM 中的所有內(nèi)存單元已經(jīng)被匯編語言指令修改了。因此,編譯器不能使用存放在 CPU 寄存器中的內(nèi)存單元的值來優(yōu)化 asm 指令前的代碼。
??注意,優(yōu)化屏障并不保證不使當前 CPU 把匯編語言指令混在一起執(zhí)行——這是內(nèi)存屏障的工作。
(b)內(nèi)存屏障(memory barrier)
??內(nèi)存屏障(memory barrier)原語確保,在原語之后的操作開始執(zhí)行之前,原語之前的操作已經(jīng)完成。因此,內(nèi)存屏障類似于防火墻,讓任何匯編話言指令都不能通過。
??在 80x86 處理器中,下列種類的匯編語言指令是 “串行的” ,因為它們起內(nèi)存屏障的作用:
- 對 I/O 端口進行操作的所有指令。
- 有 lock 前綴的所有指令(參見 “原子操作” 一節(jié))。
- 寫控制寄存器、系統(tǒng)寄存器或調試寄存器的所有指令(例如,cli 和 sti,用于修改 eflags 寄存器的 IF 標志的狀態(tài))。
- 在 Pentium 4 微處理器中引入的匯編語言指令 lfence,sfence 和 mfence,它們分別有效地實現(xiàn)讀內(nèi)存屏障、寫內(nèi)存屏障和讀-寫內(nèi)存屏障。
- 少數(shù)專門的匯編語言指令,終止中斷處理程序或異常處理程序的 iret 指令就是其中的一個。
??Linux 使用六個內(nèi)存屏障原語,如表 5-6 所示。這些原語也被當作優(yōu)化屏障,因為我們必須保證編譯程序不在屏障前后移動匯編語言指令。“讀內(nèi)存屏障” 僅僅作用于從內(nèi)存讀的指令,而 “寫內(nèi)存屏障” 僅僅作用于寫內(nèi)存的指令。內(nèi)存屏障既用于多處理器系統(tǒng),也用于單處理器系統(tǒng)。當內(nèi)存屏障應該防止僅出現(xiàn)于多處理器系統(tǒng)上的競爭條件時,就使用 smp_xxx() 原語;在單處理器系統(tǒng)上,它們什么也不做。其他的內(nèi)存屏障防止出現(xiàn)在單處理器和多處理器系統(tǒng)上的競爭條件。
??內(nèi)存屏障原語的實現(xiàn)依賴于系統(tǒng)的體系結構。在 80x86 微處理器上,
#ifdef NN
// 把 0 加到棧頂?shù)膬?nèi)存單元;這條指令本身沒有價值,
// 但是,lock 前綴使得這條指令成為 CPU 的一個內(nèi)存屏障。
#define rmb() asm volatile("lock; addl $0,0(%%esp)":::"memory")
#else
// 如果 CPU 支持 lfence 匯編語言指令,展開成如下形式
#define rmb() asm volatile("lfence":::"memory")
#endif
??asm 指令告訴編譯器插入一些匯編語言指令并起優(yōu)化屏障的作用。Intel 上的 wmbb() 宏實際上更簡單,因為它展開為 barrier()。這是因為 Intel 處理器從不對寫內(nèi)存訪問重新排序,因此,沒有必要在代碼中插入一條串行化匯編指令。不過,這個宏禁止編譯器重新組合指令。
??注意,在多處理器系統(tǒng)上,在前一節(jié) “原子操作” 中描述的所有原子操作都起內(nèi)存屏障的作用,因為它們使用了 lock 字節(jié)。
(4)自旋鎖
可參考 ==> 2、自旋鎖
// include/linux/spinlock_types.h
typedef struct spinlock {union {struct raw_spinlock rlock;#ifdef CONFIG_DEBUG_LOCK_ALLOC
# define LOCK_PADSIZE (offsetof(struct raw_spinlock, dep_map))struct {u8 __padding[LOCK_PADSIZE];struct lockdep_map dep_map;};
#endif};
} spinlock_t;typedef struct raw_spinlock {arch_spinlock_t raw_lock;
#ifdef CONFIG_GENERIC_LOCKBREAKunsigned int break_lock;
#endif
#ifdef CONFIG_DEBUG_SPINLOCKunsigned int magic, owner_cpu;void *owner;
#endif
#ifdef CONFIG_DEBUG_LOCK_ALLOCstruct lockdep_map dep_map;
#endif
} raw_spinlock_t;// arch/x86/include/asm/spinlock_types.h
typedef struct arch_spinlock {unsigned int slock;
} arch_spinlock_t;
(a)具有內(nèi)核搶占的 spin_lock 宏
??讓我們來詳細討論用于請求自旋鎖的 spin_lock 宏。下面的描述都是針對支持 SMP 系統(tǒng)的搶占式內(nèi)核的。該宏獲取自旋瑣的地址 slp 作為它的參數(shù),并執(zhí)行下面的操作:
- 調用 preempt_disable() 以禁用內(nèi)核搶占。
- 調用函數(shù) _raw_spin_trylock(),它對自旋鎖的 slock 字段執(zhí)行原子性的測試和設置操作。該函數(shù)首先執(zhí)行等價于下列匯編語言片段的一些指令:
movb $0, %al
xchgb %al, slp->slock
??匯編語言指令 xchg 原子性地交換 8 位寄存器 %al(存 0)和 slp->slock 指示的內(nèi)存單元的內(nèi)容。隨后,如果存放在自旋鎖中的舊值(在 xchg 指令執(zhí)行之后存放在 %al 中)是正數(shù),函數(shù)就返回 1,否則返回 0。
??注 2: 具有識刺意味的是,自旋鎖是全局的、因此對它本身必須進行保護以防止并發(fā)訪問。
- 如果自旋鎖中的舊值是正數(shù),宏結束:內(nèi)核控制路徑已經(jīng)獲得自旋鎖。
- 否則,內(nèi)核控制路徑無法獲得自旋鎖,因此宏必須執(zhí)行循環(huán)一直到在其他 CPU 上運行的內(nèi)核控制路徑釋放自旋鎖。調用 preempt_enable() 遞減在第 1 步遞增了的搶占計數(shù)器。如果在執(zhí)行 spin_lock 宏之前內(nèi)核搶占被啟用,那么其他進程此時可以取代等待自旋鎖的進程。
- 如果 break_lock 字段等于 0,則把它設置為 1。通過檢測該字段,擁有鎖并在其他 CPU 上運行的進程可以知道是否有其他進程在等待這個鎖。如果進程把持某個自旋鎖的時間太長,它可以提前釋放鎖以使等待相同自旋鎖的進程能夠繼續(xù)向前運行。
- 執(zhí)行等待循環(huán):
while (spin_is_locked(slp) && slp->break_lock)cpu_relax();
??宏 cpu_relax() 簡化為一條 pause 匯編語言指令。在 Pentium 4 模型中引入了這條指令以優(yōu)化自旋鎖循環(huán)的執(zhí)行。通過引入一個很短的延遲,加快了緊跟在鎖后面的代碼的執(zhí)行并減少了能源消耗。pause 與早先的 80x86 微處理器模型是向后兼容的,因為它對應 rep; nop 指令,也就是對應空操作。
7. 跳轉回到第 1 步,再次試圖獲取自旋鎖。
(b)非搶占式內(nèi)核中的 spin_lock 宏
??如果在內(nèi)核編譯時沒有選擇內(nèi)核搶占選項,spin_lock 宏就與前面描述的 spin_lock 宏有很大的區(qū)別。在這種情況下,宏生成一個匯編語言程序片段,它本質上等價于下面緊湊的忙等待(注 3):
1: lock; decb slp->slockjns 3f
2: pausecmpb $0, slp->slockjle 2bjmp 1b
3:
??匯編語言指令 decb 進減自旋鎖的值,該指令是原子的,因為它帶有 lock 字節(jié)前綴。隨后檢測符號標志,如果它被清 0,說明自旋鎖被設置為 1(未鎖),因此從標記 3 處繼續(xù)正常執(zhí)行(后綴 f 表示標簽是 “向前的” ,它在程序的后面出現(xiàn))。否則,在標簽 2 處(后綴 b 表示 “向后的” 標簽)執(zhí)行緊湊循環(huán)直到自旋鎖出現(xiàn)正值。然后從標簽 1 處開始重新執(zhí)行,因為不檢查其他的處理器是否搶占了鎖就繼續(xù)執(zhí)行是不安全的。
(c)spin_unlock 宏
??spin_unlock 宏釋放以前獲得的自旋鎖,它本質上執(zhí)行下列匯編語言指令:
movb $1, slp->slock
??并在隨后調用 preempt_enable()(如果不支持內(nèi)核搶占,preempt_enable() 什么都不做)。注意,因為現(xiàn)在的 80x86 微處理器總是原子地執(zhí)行內(nèi)存中的只寫訪問,所以不使用 lock 字節(jié)。
(5)順序鎖
// include/linux/seqlock.h
typedef struct {unsigned sequence;spinlock_t lock;
} seqlock_t;// include/linux/seqlock.h
static __always_inline unsigned read_seqbegin(const seqlock_t *sl)
{unsigned ret;repeat:ret = sl->sequence;smp_rmb();if (unlikely(ret & 1)) {cpu_relax();goto repeat;}return ret;
}
??當使用讀 / 寫自旋鎖時,內(nèi)核控制路徑發(fā)出的執(zhí)行 read_lock 或 write_lock 操作的請求具有相同的優(yōu)先權:讀者必須等待,直到寫操作完成。同樣地,寫者也必須等待,直到讀操作完成。
??Linux 2.6 中引入了順序鎖(seqlock),它與讀 / 寫自旋鎖非常相似,只是它為寫者賦予了較高的優(yōu)先級:事實上,即使在讀者正在讀的時候也允許寫者繼續(xù)運行。這種策略的好處是寫者永遠不會等待(除非另外一個寫者正在寫),缺點是有些時候讀者不得不反復多次讀相同的數(shù)據(jù)直到它獲得有效的副本。
??每個順序鎖都是包括兩個字段的 seqlock_t 結構:一個類型為 spinlock_t 的 lock 字段和一個整型的 sequence 字段,第二個字段是一個順序計數(shù)器。每個讀者都必須在讀數(shù)據(jù)前后兩次讀順序計數(shù)器,并檢查兩次讀到的值是否相同,如果不相同,說明新的寫者已經(jīng)開始寫并增加了順序計數(shù)器, 因此暗示讀者剛讀到的數(shù)據(jù)是無效的。
??通過把 SEQLOCK_UNLOCKED 賦給變量 seqlock_t 或執(zhí)行 seqlock_init 宏,把 seqlock_t 變量初始化為 “未上鎖” 。寫者通過調用 write_seqlock() 和 write_sequnlock() 獲取和釋放順序鎖。第一個函數(shù)獲取 seqlock_t 數(shù)據(jù)結構中的自旋鎖,然后使順序計數(shù)器加 1;第二個函數(shù)再次增加順序計數(shù)器,然后釋放自旋鎖。這樣可以保證寫者在寫的過程中,計數(shù)器的值是奇數(shù),并且當沒有寫者在改變數(shù)據(jù)的時候,計數(shù)器的值是偶數(shù)。
(6)讀 - 拷貝 - 更新(RCU)
??讀-拷貝-更新(RCU)是為了保護在多數(shù)情況下被多個 CPU 讀的數(shù)據(jù)結構而設計的另一種同步技術。RCU 允許多個讀者和寫者并發(fā)執(zhí)行(相對于只允許一個寫者執(zhí)行的順序鎖有了改進)。而且,RCU 是不使用鎖的,就是說,它不使用被所有 CPU 共享的鎖或計數(shù)器,在這一點上與讀 / 寫自旋鎖和順序鎖(由于高速緩存行竊用和失效而有很高的開銷)相比,RCU 具有更大的優(yōu)勢。
??RCU 是如何不使用共享數(shù)據(jù)結構而令人驚訝地實現(xiàn)多個 CPU 同步呢? 其關鍵的思想包括限制 RCU 的范圍,如下所述:
- RCU 只保護被動態(tài)分配并通過指針引用的數(shù)據(jù)結構。
- 在被 RCU 保護的臨界區(qū)中,任何內(nèi)核控制路徑都不能睡眠。
??當內(nèi)核控制路徑要讀取被 RCU 保護的數(shù)據(jù)結構時,執(zhí)行宏 rcu_read_lock(),它等同于 preempt_disable() 。接下來,讀者間接引用該數(shù)據(jù)結構指針所對應的內(nèi)存單元并開始讀這個數(shù)據(jù)結構。正如在前面所強調的,讀者在完成對數(shù)據(jù)結構的讀操作之前,是不能睡眠的。用等同于 preempt_enable() 的宏 rcu_read_unlock() 標記臨界區(qū)的結束。
??我們可以想象,由于讀者幾乎不做任何事情來防止競爭條件的出現(xiàn),所以寫者不得不做得更多一些。事實上,當寫者要更新數(shù)據(jù)結構時,它間接引用指針并生成整個數(shù)據(jù)結構的副本。接下來,寫者修改這個副本。一旦修改完畢,寫者改變指向數(shù)據(jù)結構的指針,以使它指向被修改后的副本。由于修改指針值的操作是一個原子操作,所以舊副本和新副本對每個讀者或寫者都是可見的,在數(shù)據(jù)結構中不會出現(xiàn)數(shù)據(jù)奔潰。盡管如此,還需要內(nèi)存屏障來保證:只有在數(shù)據(jù)結構被修改之后,已更新的指針對其他 CPU 才是可見的。如果把自旋鎖與 RCU 結合起來以禁止寫者的并發(fā)執(zhí)行,就隱含地引入了這樣的內(nèi)存屏障。
??然而,使用 RCU 技術的真正困難在于:寫者修改指針時不能立即釋放數(shù)據(jù)結構的舊副本。實際上,寫者開始修改時,正在訪問數(shù)據(jù)結構的讀者可能還在讀舊副本。只有在 CPU 上的所有(潛在的)讀者都執(zhí)行完宏 rcu_read_unlock() 之后,才可以釋放舊副本。內(nèi)核要求每個潛在的讀者在下面的操作之前執(zhí)行 rcu_read_unlock() 宏:
- CPU 執(zhí)行進程切換(參見前面的約束條件 2)
- CPU 開始在用戶態(tài)執(zhí)行
- CPU 執(zhí)行空循環(huán)(參見第三章"內(nèi)核線程"一節(jié))
??對上述每種情況,我們說 CPU 已經(jīng)經(jīng)過了靜止狀態(tài)(quiescent state)。寫者調用函數(shù) call_rcu() 來釋放數(shù)據(jù)結構的舊副本。當所有的 CPU 都通過靜止狀態(tài)之后,call_rcu() 接受 rcu_head 描述符(通常嵌在要被釋放的數(shù)據(jù)結構中)的地址和將要調用的回調函數(shù)的地址作為參數(shù)。一旦回調函數(shù)被執(zhí)行,它通常釋放數(shù)據(jù)結構的舊副本。
??函數(shù) call_rcu() 把回調函數(shù)和其參數(shù)的地址存放在 rcu_head 描述符中,然后把描述符插入回調函數(shù)的每 CPU(per-CPU)鏈表中。內(nèi)核每經(jīng)過一個時鐘滴答(參見第六章 “更新本地 CPU 統(tǒng)計數(shù)” 一節(jié))就周期性地檢查本地 CPU 是否經(jīng)過了一個靜止狀態(tài)。如果所有CPU 都經(jīng)過了靜止狀態(tài),本地 tasklet(它的描述符存放在每 CPU 變量 rcu_tasklet 中)就執(zhí)行鏈表中的所有回調函數(shù)。
??RCU 是 Linux 2.6 中新加的功能,用在網(wǎng)絡層和虛擬文件系統(tǒng)中。
(7)信號量
??Linux 提供兩種信號量:
- 內(nèi)核信號量,由內(nèi)核控制路徑使用
- System V IPC 信號量,由用戶態(tài)進程使用
可參考 ==> 4、信號量