文章很長,而且持續更新,建議收藏起來,慢慢讀!瘋狂創客圈總目錄 博客園版 為您奉上珍貴的學習資源 :
免費贈送 :《尼恩Java面試寶典》 持續更新+ 史上最全 + 面試必備 2000頁+ 面試必備 + 大廠必備 +漲薪必備
免費贈送 經典圖書:《Java高並發核心編程(卷1)加強版》 面試必備 + 大廠必備 +漲薪必備 加尼恩免費領
免費贈送 經典圖書:《Java高並發核心編程(卷2)加強版》 面試必備 + 大廠必備 +漲薪必備 加尼恩免費領
免費贈送 經典圖書:《Java高並發核心編程(卷3)加強版》 面試必備 + 大廠必備 +漲薪必備 加尼恩免費領
免費贈送 經典圖書:《尼恩Java面試寶典 最新版》 面試必備 + 大廠必備 +漲薪必備 加尼恩免費領
免費贈送 資源寶庫: Java 必備 百度網盤資源大合集 價值>10000元 加尼恩領取
高可用中間件的 原理與實操 實戰 視頻背景:
下一個視頻版本,從架構師視角,尼恩為大家打造高可用、高並發中間件的原理與實操。
目標:通過視頻和博客的方式,為各位潛力架構師,徹底介紹清楚架構師必須掌握的高可用、高並發環境,包括但不限於:
-
高可用、高並發nginx架構的原理與實操
-
高可用、高並發mysql架構的原理與實操
-
高可用、高並發nacos架構的原理與實操
-
高可用、高並發rocketmq架構的原理與實操
-
高可用、高並發es架構的原理與實操
-
高可用、高並發minio架構的原理與實操
why 高可用、高並發中間件的原理與實操:
- 實際的開發過程中,很多小伙伴,常常是埋頭苦干,聚焦crud開發,復雜一點的環境出了問題,都不能自己去啟動,出了問題,就想熱鍋上的螞蟻,無從下手。
- 常常的現象是: 大家 低頭看路的時間多,抬頭看天的時間少,技術視野比較狹窄。常常是埋頭苦干業務開發,很少投入精力進行技術提升。
- 作為架構師,或者未來想走向高端開發,或者做架構,必須掌握高可用、高並發中間件的原理,掌握其實操。
本系列博客的具體內容,請參見 瘋狂創客圈總目錄 語雀版 | 總目錄 碼雲版| 總目錄 博客園版
插曲:由於高可用中間件 涉及到非常多的底層原理,作為鋪墊,先給大家來點 底層原理的知識
Java進程的用戶空間與內核空間如何區分? 如何區分Java進程的內核態與用戶態度?
社群的交流如下:
已知:java線程是和操作系統線程一一對應
問題:java線程在用戶空間還是在內核空間?
用戶空間的構成
-
運行時棧:由編譯器自動釋放,存放函數的參數值,局部變量和方法返回值等。每當一個函數被調用時,該函數的返回類型和一些調用的信息被存儲到棧頂,調用結束后調用信息會被彈出並釋放掉內存。
棧區是從高地址位向低地址位增長的,是一塊連續的內在區域,最大容量是由系統預先定義好的,申請的棧空間超過這個界限時會提示溢出,用戶能從棧中獲取的空間較小。
-
運行時堆:用於存放進程運行中被動態分配的內存段,位於 BSS 和棧中間的地址位。由卡發人員申請分配(malloc)和釋放(free)。堆是從低地址位向高地址位增長,采用鏈式存儲結構。
頻繁地 malloc/free 造成內存空間的不連續,產生大量碎片。當申請堆空間時,庫函數按照一定的算法搜索可用的足夠大的空間。因此堆的效率比棧要低的多。
-
代碼段:存放 CPU 可以執行的機器指令,該部分內存只能讀不能寫。通常代碼區是共享的,即其他執行程序可調用它。假如機器中有數個進程運行相同的一個程序,那么它們就可以使用同一個代碼段。
-
未初始化的數據段:存放未初始化的全局變量,BSS 的數據在程序開始執行之前被初始化為 0 或 NULL。
-
已初始化的數據段:存放已初始化的全局變量,包括靜態全局變量、靜態局部變量以及常量。
-
內存映射區域:例如將動態庫,共享內存等虛擬空間的內存映射到物理空間的內存,一般是 mmap 函數所分配的虛擬內存空間。
用戶空間與內核地址空間和划分(以32位CPU為例)
最高的 1G 字節(從虛擬地址 0xC0000000 到 0xFFFFFFFF)由內核態使用,稱為內核空間。而較低的 3G 字節(從虛擬地址 0x00000000 到 0xBFFFFFFF)由各個進程使用,稱為用戶空間。
32位Linux內核地址空間划分03G為用戶空間,34G為內核空間。
注意這里是32位內核地址空間划分,64位內核地址空間划分是不同的
用戶空間和內核空間的詳細內存布局:
為什么要划分用戶空間和系統空間呢?
- 第一點,分開存放,管理上很方便。
不同的身份,數據放置的位置必然不一樣,否則大混戰就會導致系統的數據和用戶的數據混在一起,系統就不能很好的運行了。
分開來存放,就讓系統的數據和用戶的數據互不干擾,保證系統的穩定性。
- 第二點,對兩部分的數據的訪問進行分開控制。
而更重要的是,將用戶的數據和系統的數據隔離開,就可以對兩部分的數據的訪問進行控制。
這樣就可以確保用戶程序不能隨便操作系統的數據,這樣防止用戶程序誤操作或者是惡意破壞系統。
區分內核空間和用戶空間本質上是要提高操作系統的穩定性及可用性。
誰來划分內存空間的呢?
在電腦開機之前,內存就是一塊原始的物理內存。什么也沒有。
- 划分的時機
開機加電,系統啟動后,就對物理內存進行了划分。
- 邏輯上划分
划分內存空間的規則這是系統的規定,物理內存條上並沒有划分好的地址和空間范圍。這些划分都是操作系統在邏輯上的划分。
不同版本的操作系統划分的結果都是不一樣的。
進程的內核態與用戶態
內核態:
當進程(或者線程)執行系統調用而陷入內核代碼中執行時,我們就稱進程處於內核運行態(或簡稱為內核態)。
此時處理器處於特權級最高的(0級)內核代碼中執行。
- 特點:
CPU可以訪問內存所有數據,包括外圍設備(硬盤、網卡),允許訪問內核空間、用戶空間,CPU也可以將自己從一個程序切換到另一個程序;
用戶態:
當進程(或者線程)在執行用戶自己的代碼時,則稱其處於用戶運行態(用戶態)。
即此時處理器在特權級最低的(3級)用戶代碼中運行。
當正在執行用戶程序而突然被中斷程序中斷時,此時用戶程序也可以象征性地稱為處於進程的內核態。因為中斷處理程序將使用當前進程的內核棧。這與處於內核態的進程的狀態有些類似。
- 特點:
只能受限的訪問內存,且不允許訪問外圍設備,不容許訪問內核空間,占用CPU的能力被剝奪,CPU資源可以被其他程序獲取;
進程上下文context:
上下文context簡單說來就是一個環境。
用戶空間的應用程序,通過系統調用,進入內核空間。
這個時候用戶空間的進程要傳遞 很多變量、參數的值給內核,內核態運行的時候也要保存用戶進程的一些寄存 器值、變量等。
所謂的“進程上下文”,可以看作是用戶進程傳遞給內核的這些參數以及內核要保存的那一整套的變量和寄存器值和當時的環境等。
相對於進程而言,就是進程執行時的環境。
具體來說就是各個變量和數據,包括所有的寄存器變量、進程打開的文件、內存信息等。
一個進程的上下文可以分為三個部分:用戶級上下文、寄存器上下文以及系統級上下文。
(1)用戶級上下文: 正文、數據、用戶堆棧以及共享存儲區;
(2)寄存器上下文: 通用寄存器、程序寄存器(IP)、處理器狀態寄存器(EFLAGS)、棧指針(ESP);
(3)系統級上下文: 進程控制塊task_struct、內存管理信息(mm_struct、vm_area_struct、pgd、pte)、內核棧。
當發生進程調度時,進行進程切換就是上下文切換(context switch).
操作系統必須對上面提到的全部信息進行切換,新調度的進程才能運行。
而系統調用進行的模式切換(mode switch)。
模式切換與進程切換比較起來,容易很多,而且節省時間,因為模式切換最主要的任務只是切換進程寄存器上下文的切換。
用戶態和內核態的切換涉及的CPU上下文切換
我們都知道,Linux 是一個多任務操作系統,它支持遠大於 CPU 數量的任務同時運行。當然,這些任務實際上並不是真的在同時運行,而是因為系統在很短的時間內,將 CPU 輪流分配給它們,造成多任務同時運行的錯覺。
而在每個任務運行前,CPU 都需要知道任務從哪里加載、又從哪里開始運行,也就是說,需要系統事先幫它設置好 CPU 寄存器和程序計數器(Program Counter,PC)。
CPU 寄存器,是 CPU 內置的容量小、但速度極快的內存。
而程序計數器,則是用來存儲 CPU 正在執行的指令位置、或者即將執行的下一條指令位置。
它們都是 CPU 在運行任何任務前,必須的依賴環境,因此也被叫做 CPU 上下文。
pc 程序計數器, 指向當前指令的下條指令的地址
lr 鏈接寄存器, 程序調用返回地址
psr 當前程序狀態寄存
知道了什么是 CPU 上下文,我想你也很容易理解 CPU 上下文切換。
CPU 上下文切換,就是先把前一個任務的 CPU 上下文(也就是 CPU 寄存器和程序計數器)保存起來,然后加載新任務的上下文到這些寄存器和程序計數器,最后再跳轉到程序計數器所指的新位置,運行新任務。
而這些保存下來的上下文,會存儲在系統內核中,並在任務重新調度執行時再次加載進來。這樣就能保證任務原來的狀態不受影響,讓任務看起來還是連續運行。
用戶態和內核態的切換涉及的CPU上下文切換
第一次切換:
CPU寄存器里原來用戶態的指令位置,需要先保存起來。接着,為了執行內核態代碼,CPU寄存器需要更新為內核態指令的新位置。最后才是跳轉到內核態運行內核任務。
第二次切換:
而系統調用結束后,CPU寄存器需要恢復原來保存的用戶態,然后再切換到用戶空間,繼續運行進程。所以,一次系統調用的過程,其實是發生了兩次CPU上下文切換。
不過,需要注意的是,系統調用過程中,並不會涉及到虛擬內存等進程用戶態的資源,也不會切換進程。
Linux 系統調用的中斷處理流程
Linux的系統調用概述
- 系統調用(陷阱)是特殊異常事件,是OS為用戶程序提供服務的手段
- Linux提供了幾百種系統調用,主要分為以下幾類:進程控制,文件操作,文件系統操作,系統控制,內存管理,網絡管理,用戶管理,進程通信等
- 系統調用號是系統調用跳轉表索引值,跳轉表給出系統調用服務例程首地址。
Linux的系統調用執行流程
-
Linux 系統為每一個系統調用准備了一個號碼,為系統調用號,對於32bit系統而言:exit 為 1, write 為4等。
-
每當應用程序調用系統調用函數時,如write,首先到 C 庫中,(glibc中),調用對應的函數 write。
-
C庫中的writehansh ,將對應的調用號保存到對應的寄存器中,一般是R7寄存器。然后調用 SWI(old)或者SVC(new),觸發一個異常(軟中斷),CPU需處理該異常(軟中斷),需跳轉到異常向量表的對應位置去處理。
-
Linux 系統的異常向量表是在內核啟動初始化時,會建立異常向量表(向量表初始地址是0XFFFF0000),軟中斷入口為 vector_swi,應用程序觸發了軟中斷,然后CPU會到該入口去處理這個軟中斷。
5,進入到軟中斷 vector_swi入口之后,從R7寄存器中取出之前保存的系統調用號,然后在內核已經准備好的以系統調用號為索引下標的系統調用表中,找到對應的系統調用函數,(如果是4號 write,則對應的系統調用是sys_write),然后執行,(此時已經進入了內核空間)。
- 返回用戶空間。
總結:用戶空間到內核空間,是通過軟中斷實現的,其中的橋梁就是C庫實現的對應的“系統調用”中產生的對應的系統調用號,已經使用該系統調用號作為索引的異常向量表。
實例:Linux系統中printf()函數的執行過程
軟中斷指令int $0x80的執行過程,它是陷阱類(編程異常)事件,因此它與異常響應過程一樣。
- 將IDTi(i=128)中段選擇符(0x60)所指GDT中的內核代碼段描述符取出, 其DPL=0,此時CPL=3(因為int $0x80指令在用戶進程中執行),因而CPL>DPL且IDTi 的 DPL=CPL,故未發生13號異常。
- 讀 TR 寄存器,以訪問TSS,從TSS中將內核棧的段寄存器內容和棧指針裝入SS和ESP;
- 依次將執行完指令int $0x80時的SS、ESP、EFLAGS、CS、EIP的內容(即斷點和程序狀態)保存到內核棧中,即當前SS∶ESP所指之處;
- 將IDTi(i=128)中段選擇符(0x60)裝入CS,偏移地址裝入EIP。這里,CS:EIP即是系統調用處理程序system_call(所有系統調用的入口程序)第一條指令的邏輯地址
執行int $0x80需一連串的一致性和安全性檢查,因而速度較慢。
從Pentium II開始,Intel引入了指令sysenter和sysexit,分別用於從用戶態到內核態、從用戶態到內核態的快速切換。
用戶態和內核態的切換耗費時間的原因
當程序中有系統調用語句,程序執行到系統調用時,首先使用類似int 80H的軟中斷指令,保存現場,去系統調用,在內核態執行,然后恢復現場,每個進程都會有兩個棧,一個內核態棧和一個用戶態棧。
當int中斷執行時就會由用戶態棧轉向內核態棧。
系統調用時需要進行棧的切換。而且內核代碼對用戶不信任,需要進行額外的檢查。系統調用的返回過程有很多額外工作,比如檢查是否需要調度等。
進程需要從用戶態切換到內核態,而這種切換是需要消耗很多時間的,有可能比用戶執行代碼的時間還要長。
常見的Java涉及用戶態和內核態的切換的操作
- io操作
- 線程操作,如線程的創建,線程切換
- java程序的加鎖和解鎖
- 內存分配 malloc()
Java堆內存的一次網絡 Send 操作
線程上下文切換
Linux內核在2.2版本中引入了類似線程的機制。
Linux提供的vfork函數可以創建線程,此外Linux還提供了clone來創建一個線程,通過共享原來調用進程的地址空間,clone能像獨立線程一樣工作。
Linux內核的獨特,允許共享地址空間,clone創建的進程指向了父進程的數據結構,從而完成了父子進程共享內存和其他資源。
clone的參數可以設置父子進程共享哪些資源,不共享哪些資源。
進程還是線程的創建都是由父進程/父線程調用系統調用接口實現的。
創建的主要工作實際上就是創建task_strcut結構體,並將該對象添加至工作隊列中去。
而線程和進程在創建時,通過CLONE_THREAD flag的不同,而選擇不同的方式共享父進程/父線程的資源,從而造成在用戶空間所看到的進程和線程的不同。
實質上Linux內核並沒有線程這個概念,或者說Linux不區分進程和線程。
Linux喜歡稱他們為任務。除了clone進程以外,Linux並不支持多線程,獨立數據結構或內核子程序。
線程上下文切換, 從內核的角度而已實質上,就是 進程的上下文切換,只是涉及的范圍稍微窄了一點而已。
進程的上下文切換
進程是現代操作系統的核心概念之一,用於分配系統(CPU,內存)資源的使用。
了解linux進程及進程切換的知識,首先要理解進程與程序的區別:
- 進程是執行流,是動態概念;
- 程序是數據與指令序列的集合,是靜態概念。
系統調用和進程上下文切換是不一樣的:
進程上下文切換,是指從一個進程切換到另一個進程運行。而系統調用過程中一直是同一個進程在運行。
所以,系統調用過程通常稱為特權模式切換,而不是上下文切換。但實際上,系統調用過程中,CPU 的上下文切換還是無法避免的。
那么,進程上下文切換跟系統調用又有什么區別呢?
首先,你需要知道,進程是由內核來管理和調度的,進程的切換只能發生在內核態。
所以,進程的上下文不僅包括了虛擬內存、棧、全局變量等用戶空間的資源,還包括了內核堆棧、寄存器等內核空間的狀態,這些都屬需要維護起來。
具體來說就是各個變量和數據,包括所有的寄存器變量、進程打開的文件、內存信息等。
一個進程的上下文可以分為三個部分:用戶級上下文、寄存器上下文以及系統級上下文。
(1)用戶級上下文: 正文、數據、用戶堆棧以及共享存儲區;
(2)寄存器上下文: 通用寄存器、程序寄存器(IP)、處理器狀態寄存器(EFLAGS)、棧指針(ESP);
(3)系統級上下文: 進程控制塊task_struct、內存管理信息(mm_struct、vm_area_struct、pgd、pte)、內核棧。
簡單來說:
進程上下文切換時間 = 用戶態到內核態時間 + 保存pre進程的時間+ 加載next進程的時間 + 內核態到用戶態切換時間
如下圖所示,保存上下文和恢復上下文的過程並不是“免費”的,需要內核在 CPU 上運行才能完成。
根據 行業的一些測試報告,每次上下文切換都需要幾十納秒到數微秒的 CPU 時間。
這個時間還是相當可觀的,特別是在進程上下文切換次數較多的情況下,很容易導致 CPU 將大量時間耗費在寄存器、內核棧以及虛擬內存等資源的保存和恢復上,進而大大縮短了真正運行進程的時間。
舉例:堆棧地址切換
linux的進程有兩種棧,用戶棧和內核棧,它們在不同的內存區域,用戶棧在用戶態中使用,在用戶地址空間分配(03G),內核棧在內核態中使用,在內核地址空間分配(3G4G)。
用戶棧主要用於函數調用和存儲局部變量,內核棧除此之外還要保存進程切換額外的信息,如通用寄存器等。
不管是用戶棧還是內核棧,CPU都是用ESP寄存器保存棧頂地址,因此早在進程切換前,進程進入內核態后,用戶棧就需要被切換出去,整個切換過程,都是在內核棧上工作,因而用戶棧與進程切換無關。
ESP:棧指針寄存器(extended stack pointer),其內存放着一個指針,該指針永遠指向系統棧最上面一個棧幀的棧頂。
(1)A的用戶態-->A的內核態, 這一過程是由中斷,異常或系統調用實現的。
每次從用戶態切換到內核態,內核棧都會被清空,ESP直接指向內核棧的棧底,而用戶棧的信息則會保存到內核棧中。
(2)A的內核態-->A的用戶態,
執行與(1)相反的過程,從內核棧中取出(1)中保存的用戶棧信息,裝載相應寄存器,切換到用戶棧。
回到線程上下文切換
一個進程的上下文可以分為三個部分:用戶級上下文、寄存器上下文以及系統級上下文。
(1)用戶級上下文: 正文、數據、用戶堆棧以及共享存儲區;
(2)寄存器上下文: 通用寄存器、程序寄存器(IP)、處理器狀態寄存器(EFLAGS)、棧指針(ESP);
(3)系統級上下文: 進程控制塊task_struct、內存管理信息(mm_struct、vm_area_struct、pgd、pte)、內核棧。
線程共享了父進程的系統級上下文,所以,線程的上下文切換的大致時間為:
進程上下文切換時間 = 進程上下文切換時間 - 保存pre進程的系統級上下文 - 加載pre進程的系統級上下文的時間
其中:
進程上下文切換時間 = 用戶態到內核態時間 + 保存pre進程的時間+ 加載next進程的時間 + 內核態到用戶態切換時間
數據結構 task_struct
以下是摘自Linux內核2.6.32版的task_struct源碼。
Linux中無論是進程還是線程,只要是調度單元,都通過 struct task_struct表示。
這也是為什么講說進程和線程在內核相同的原因。
struct task_struct有保存有關線程/進程中的一切信息,主要包括有線程/進程狀態、與其他線程/進程關系、虛擬內存相關、日志相關、線程/進程限制等。該結構體定義在include/linux/sched.h文件中,感興趣可以詳細閱讀
那么,進程和線程在task_struct結構體中是否有標識上的不同?
實際上,在struct task_struct中並沒有明確的標識(枚舉類型),區分該task是線程還是進程,不過可以通過pid和tgid簡單判斷當前task是哪種類型。
在該結構體中如下段code所示,全局pid和tgid保存在task_struct結構體中。pid_t一般為int型,即可以同時使用不同標識的id。
pid用於標識不同進程和線程。
tgid用於標識線程組id,在同一進程中的所有線程具有同一tgid。tgid值等於進程第一個線程(主線程)的pid值。接着以CLONE_THREAD來調用clone建立的線程,都具有同樣的tgid。(后文會詳細描述創建過程)
group_leader 線程組中的主線程的task_struct指針。
struct task_struct {
...
pid_t pid;
pid_t tgid;
...
struct *group_leader;
}
那么除了tgid和group_leadr是進程/線程的區別外,還有什么其他的區別么?
1.進程狀態
volatile long state;
表示進程運行時的狀態,-1表示不可運行,0表示可運行,>0表示已經停止。
state的可能取值如下:
TASK_RUNNING:表示進程要么正在執行,要么正要准備執行(已經就緒),正在等待cpu時間片的調度。
TASK_INTERRUPTIBLE:表示進程因為等待一些條件而被掛起(阻塞)而所處的狀態。這些條件主要包括:硬中斷、資源、一些信號……,一旦等待的條件成立,進程就會從該狀態(阻塞)迅速轉化成為就緒狀態TASK_RUNNING(執行或准備執行態)。
TASK_UNINTERRUPTIBLE:意義與上一個類似對於處於此狀態的進程,即使傳遞一個信號或者有一個外部中斷都不能喚醒他們。只有它所等待的資源可用的時候,它才會被喚醒。這個標志很少用,但是並不代表沒有任何用處,其實他的作用非常大,特別是對於驅動刺探相關的硬件過程很重要,這個刺探過程不能被一些其他的東西給中斷,否則就會讓進程進入不可預測的狀態。
TASK_STOPPED:表示進程被停止執行,當進程接收到SIGSTOP、SIGTTIN、SIGTSTP或者SIGTTOU信號之后就會進入該狀態。
TASK_TRACED:表示進程被debugger等進程監視,進程執行被調試程序所停止,當一個進程被另外的進程所監視,每一個信號都會讓進程進入該狀態。
2.進程標志
unsigned long flags;
進程當前的標志狀態,但不是運行狀態,用於內核識別進程當前的狀態,以備下一步操作。
flags可能取值如下:
PF_FORKNOEXEC 進程剛創建,但還沒執行。
PF_SUPERPRIV 超級用戶特權。
PF_DUMPCORE dumped core。
PF_SIGNALED 進程被信號(signal)殺出。
PF_EXITING 進程開始關閉。
3.進程優先級
int prio, static_prio, normal_prio;
unsigned int rt_priority;
表示此進程的運行優先級
prio:表示動態優先級,根據static_prio和交互性獎罰算出。
static_prio:進程的靜態優先級,在進程創建時確定,范圍從-20到19,越小優先級越高。
normal_prio的優先級取決於靜態優先級和調度策略。rt_priority用於保存實時優先級,范圍是0到MAX_RT_PRIO-1(即99)。
4.進程標識符
pid_t pid;
pid_t tgid;
pid進程標識符,相當於每一個學生的學號一樣,標識符唯一標識進程。
tpid是線程組號。
5.進程內核棧
void *stack;
進程內核棧。Linux內核通過thread_union聯合體來表示進程的內核棧,其中THREAD_SIZE宏的大小為8192。
通過alloc_thread_info函數分配它的內核棧,通過free_thread_info函數釋放所分配的內核棧。
union thread_union {
struct thread_info thread_info;
unsigned long stack[THREAD_SIZE/sizeof(long)];
};
6.進程的親屬關系
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 */
real_parent——是進程的“親生父親”。如果創建進程的父進程不再存在,則指向PID為1的init進程
parent——是進程的父進程。進程終止時,必須向它的父進程發送信號。它的值通常與real_parent相同。
children——表示鏈表的頭部,鏈表中的所有元素都是它的子進程。
sibling——用於把當前進程插入到兄弟鏈表中。
group_leader——指向其所在進程組的領頭進程
7.ptrace系統調用
unsigned int ptrace;
ptrace 提供了一種父進程可以控制子進程運行,並可以檢查和改變它的核心image。
它主要用於實現斷點調試。一個被跟蹤的進程運行中,直到發生一個信號,則進程被中止,並且通知其父進程。在進程中止的狀態下,進程的內存空間可以被讀寫。父進程還可以使子進程繼續執行,並選擇是否是否忽略引起中止的信號。成員ptrace被設置為0時表示不需要被跟蹤。
8.調度策略相關
const struct sched_class *sched_class;
struct sched_entity se;
struct sched_rt_entity rt;
sched_class:調度類
se:普通進程的調用實體,每個進程都有其中之一的實體
rt:實時進程的調用實體,每個進程都有其中之一的實體
9.進程地址空間
struct mm_struct *mm, *active_mm;
mm:進程所擁有的用戶空間內存描述符,內核線程無的mm為NULL
active_mm:指向進程運行時所使用的內存描述符, 對於普通進程而言,這兩個指針變量的值相同。但是內核線程kernel thread是沒有進程地址空間的,所以內核線程的tsk->mm域是空(NULL)。
但是內核必須知道用戶空間包含了什么,因此它的active_mm成員被初始化為前一個運行進程的active_mm值。
10.判斷標志
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;
exit_code:用於設置進程的終止代號,這個值要么是_exit()或exit_group()系統調用參數(正常終止),要么是由內核提供的一個錯誤代號(異常終止)。
exit_signal:被置為-1時表示是某個線程組中的一員。只有當線程組的最后一個成員終止時,才會產生一個信號,以通知線程組的領頭進程的父進程。
personality:用於處理不同的ABI,參見Linux-Man。
did_exec:用於記錄進程代碼是否被execve()函數所執行。
in_execve:用於通知LSM是否被do_execve()函數所調用。
in_iowait:用於判斷是否進行iowait計數。
11.時間
cputime_t utime, stime, utimescaled, stimescaled;
cputime_t gtime;
cputime_t prev_utime, prev_stime;
unsigned 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];
utime/stime:用於記錄進程在用戶態/內核態下所經過的節拍數(定時器)。
utimescaled/stimescaled:用於記錄進程在用戶態/內核態的運行時間,但它們以處理器的頻率為刻度。
gtime:以節拍計數的虛擬機運行時間(guest time)。
prev_utime/prev_stime:是先前的運行時間,
請參考補丁說明http://lkml.indiana.edu/hypermail/linux/kernel/1003.3/02431.html。
nvcsw/nivcsw:自願(voluntary)/非自願(involuntary)上下文切換計數。
start_time、real_start_time:都是進程創建時間,real_start_time還包含了進程睡眠時間,常用於/proc/pid/stat,補丁說明請參考http://lkml.indiana.edu/hypermail/linux/kernel/0705.0/2094.html
cputime_expires:用來統計進程或進程組被跟蹤的處理器時間,其中的三個成員對應着cpu_timers[3]的三個鏈表。
min_flt,maj_flt:缺頁統計。
12.信號處理
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;
signal:指向進程的信號描述符。
sighand:指向進程的信號處理程序描述符。
blocked:表示被阻塞信號的掩碼,real_blocked表示臨時掩碼。
pending:存放私有掛起信號的數據結構。
sas_ss_sp:是信號處理程序備用堆棧的地址,sas_ss_size表示堆棧的大小。
notifier_data/notifier_mask:設備驅動程序常用notifier指向的函數來阻塞進程的某些信號(notifier_mask是這些信號的位掩碼),notifier_data指的是notifier所指向的函數可能使用的數據。
13.其他
(1)用於保護資源分配或釋放的自旋鎖
/* Protection of (de-)allocation: mm, files, fs, tty, keyrings, mems_allowed,
* mempolicy */
spinlock_t alloc_lock;
(2)進程描述符使用計數,被置為2時,表示進程描述符正在被使用而且其相應的進程處於活動狀態。
atomic_t usage;
(3)用於表示獲取大內核鎖的次數,如果進程未獲得過鎖,則置為-1。
int lock_depth; /* BKL lock depth */
(4)在SMP上幫助實現無加鎖的進程切換(unlocked context switches)
#ifdef CONFIG_SMP
#ifdef __ARCH_WANT_UNLOCKED_CTXSW
int oncpu;
#endif
#endif
(5)preempt_notifier結構體鏈表
#ifdef CONFIG_PREEMPT_NOTIFIERS
/* list of struct preempt_notifier: */
struct hlist_head preempt_notifiers;
#endif
(6)FPU使用計數
unsigned char fpu_counter;
(7)、blktrace是一個針對Linux內核中塊設備I/O層的跟蹤工具。
#ifdef CONFIG_BLK_DEV_IO_TRACE
unsigned int btrace_seq;
#endif
(8)RCU同步原語
#ifdef CONFIG_PREEMPT_RCU
int rcu_read_lock_nesting;
char rcu_read_unlock_special;
struct list_head rcu_node_entry;
#endif /* #ifdef CONFIG_PREEMPT_RCU */
#ifdef CONFIG_TREE_PREEMPT_RCU
struct rcu_node *rcu_blocked_node;
#endif /* #ifdef CONFIG_TREE_PREEMPT_RCU */
#ifdef CONFIG_RCU_BOOST
struct rt_mutex *rcu_boost_mutex;
#endif /* #ifdef CONFIG_RCU_BOOST */
(9)用於調度器統計進程的運行信息
#if defined(CONFIG_SCHEDSTATS) || defined(CONFIG_TASK_DELAY_ACCT)
struct sched_info sched_info;
#endif
(10)、用於構建進程鏈表
struct list_head tasks;
(11)、防止內核堆棧溢出
#ifdef CONFIG_CC_STACKPROTECTOR
/* Canary value for the -fstack-protector gcc feature */
unsigned long stack_canary;
#endif
(12)PID散列表和鏈表
/* PID/PID hash table linkage. */
struct pid_link pids[PIDTYPE_MAX];
struct list_head thread_group; //線程組中所有進程的鏈表
(13)do_fork函數
struct completion *vfork_done; /* for vfork() */
int __user *set_child_tid; /* CLONE_CHILD_SETTID */
int __user *clear_child_tid; /* CLONE_CHILD_CLEARTID */
在執行do_fork()時,如果給定特別標志,則vfork_done會指向一個特殊地址。
如果copy_process函數的clone_flags參數的值被置為CLONE_CHILD_SETTID或CLONE_CHILD_CLEARTID,則會把child_tidptr參數的值分別復制到set_child_tid和clear_child_tid成員。這些標志說明必須改變子進程用戶態地址空間的child_tidptr所指向的變量的值。
(14)進程權能
const struct cred __rcu *real_cred; /* objective and real subjective task
* credentials (COW) */
const struct cred __rcu *cred; /* effective (overridable) subjective task
* credentials (COW) */
struct cred *replacement_session_keyring; /* for KEYCTL_SESSION_TO_PARENT */
(15)相應的程序名
char comm[TASK_COMM_LEN]
(16)文件
/* file system info */
int link_count, total_link_count;
/* filesystem information */
struct fs_struct *fs;
/* open file information */
struct files_struct *files;
fs:用來表示進程與文件系統的聯系,包括當前目錄和根目錄。\
files:表示進程當前打開的文件。
(17)進程通信(SYSVIPC)
#ifdef CONFIG_SYSVIPC
/* ipc stuff */
struct sysv_sem sysvsem;
#endif
(18)處理器特有數據
struct thread_struct thread;
(19)命名空間
struct nsproxy *nsproxy;
(20)進程審計
struct audit_context *audit_context;
#ifdef CONFIG_AUDITSYSCALL
uid_t loginuid;
unsigned int sessionid;
#endif
(21)secure computing
seccomp_t seccomp;
(22)用於copy_process函數使用CLONE_PARENT 標記時
/* Thread group tracking */
u32 parent_exec_id;
u32 self_exec_id;
(23)中斷
#ifdef CONFIG_GENERIC_HARDIRQS
/* IRQ handler threads */
struct irqaction *irqaction;
#endif
#ifdef CONFIG_TRACE_IRQFLAGS
unsigned 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
(24)task_rq_lock函數所使用的鎖
/* Protection of the PI data structures: */
raw_spinlock_t pi_lock;
(25)基於PI協議的等待互斥鎖,其中PI指的是priority inheritance(優先級繼承)
#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
(26)死鎖檢測
#ifdef CONFIG_DEBUG_MUTEXES
/* mutex deadlock detection */
struct mutex_waiter *blocked_on;
#endif
(27)lockdep,參見內核說明文檔linux-2.6.38.8/Documentation/lockdep-design.txt
#ifdef CONFIG_LOCKDEP
# define MAX_LOCK_DEPTH 48UL
u64 curr_chain_key;
int lockdep_depth;
unsigned int lockdep_recursion;
struct held_lock held_locks[MAX_LOCK_DEPTH];
gfp_t lockdep_reclaim_gfp;
#endif
(28)JFS文件系統
/* journalling filesystem info */
void *journal_info;
(29)塊設備鏈表
/* stacked block device info */
struct bio_list *bio_list;
(30)內存回收
struct reclaim_state *reclaim_state;
(31)存放塊設備I/O數據流量信息
struct backing_dev_info *backing_dev_info;
(32)I/O調度器所使用的信息
struct io_context *io_context;
(33)CPUSET功能
#ifdef CONFIG_CPUSETS
nodemask_t mems_allowed; /* Protected by alloc_lock */
int mems_allowed_change_disable;
int cpuset_mem_spread_rotor;
int cpuset_slab_spread_rotor;
#endif
(34)Control Groups
#ifdef CONFIG_CGROUPS
/* Control Group info protected by css_set_lock */
struct css_set __rcu *cgroups;
/* cg_list protected by css_set_lock and tsk->alloc_lock */
struct list_head cg_list;
#endif
#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
(35)futex同步機制
#ifdef CONFIG_FUTEX
struct robust_list_head __user *robust_list;
#ifdef CONFIG_COMPAT
struct compat_robust_list_head __user *compat_robust_list;
#endif
struct list_head pi_state_list;
struct futex_pi_state *pi_state_cache;
#endif
(36)非一致內存訪問(NUMA Non-Uniform Memory Access)
#ifdef CONFIG_NUMA
struct mempolicy *mempolicy; /* Protected by alloc_lock */
short il_next;
#endif
(37)文件系統互斥資源
atomic_t fs_excl; /* holding fs exclusive resources */
(38)RCU鏈表
struct rcu_head rcu;
(39)管道
struct pipe_inode_info *splice_pipe;
(40)延遲計數
1. #ifdef CONFIG_TASK_DELAY_ACCT
2. struct task_delay_info *delays;
3. #endif
(41)fault injection,參考內核說明文件linux-2.6.38.8/Documentation/fault-injection/fault-injection.txt
#ifdef CONFIG_FAULT_INJECTION
int make_it_fail;
#endif
task_struct的thread_struct
task_struct是進程在內核中對應的數據結構,它標識了進程的狀態等各項信息。
其中有一項thread_struct結構的變量thread,記錄了CPU相關的進程狀態信息,如內核控制的斷點和棧指針等。
malloc()調用的用戶態到內核態切換
C函數庫中的內存分配函數malloc(),它具體是使用sbrk()系統調用來分配內存,當malloc調用sbrk()的時候就涉及一次從用戶態到內核態的切換
Buddy 分配page內存
CPU看到的內存管理都是對page(4k)的管理,接下來我們看一下用來管理page的經典算法--Buddy
Buddy 是著名的伙伴分配算法:
我們通過一個簡單的例子來說明該算法的工作原理。
Linux的伙伴算法把所有的空閑頁面分為10個塊組,每組中塊的大小是2的冪次方個頁面,例如,第0組中塊的大小都為20 (1個頁面),第1組中塊的大小為都為21(2個頁面),第9組中塊的大小都為29(512個頁面)。也就是說,每一組中塊的大小是相同的,且這同樣大小的塊形成一個鏈表。
假設要求分配的塊其大小為128個頁面(由多個頁面組成的塊我們就叫做頁面塊)。
該算法先在塊大小為128個頁面的鏈表中查找,看是否有這樣一個空閑塊。如果有,就直接分配;如果沒有,該算法會查找下一個更大的塊,具體地說,就是在塊大小為256個頁面的鏈表中查找一個空閑塊。如果存在這樣的空閑塊,內核就把這256個頁面分為兩等份,一份分配出去,另一份插入到塊大小為128個頁面的鏈表中。
如果在塊大小為256個頁面的鏈表中也沒有找到空閑頁塊,就繼續找更大的塊,即512個頁面的塊。如果存在這樣的塊,內核就從512個頁面的塊中分出128個頁面滿足請求,然后從384個頁面中取出256個頁面插入到塊大小為256個頁面的鏈表中。然后把剩余的128個頁面插入到塊大小為128個頁面的鏈表中。
如果512個頁面的鏈表中還沒有空閑塊,該算法就放棄分配,並發出出錯信號。
對上面的文字,不了解的,沒有關系,我馬上要講netty源碼,會細致深入介紹 這個算法。
Slab分配小內存
采用伙伴算法分配內存時,每次至少分配一個頁面。但當請求分配的內存大小為幾十個字節或幾百個字節時應該如何處理?如何在一個頁面中分配小的內存區,小內存區的分配所產生的內碎片又如何解決?
在Linux中,伙伴系統(buddy system)是以頁為單位管理和分配內存。
但是現實的需求卻以字節為單位,假如我們需要申請20Bytes,總不能分配一頁吧!那豈不是嚴重浪費內存。
那么該如何分配呢?slab分配器就應運而生了,專為小內存分配而生。
slab分配器分配內存以Byte為單位。但是slab分配器並沒有脫離伙伴系統,而是基於伙伴系統分配的大內存進一步細分成小內存分配。
對上面的文字,不了解的,沒有關系,我馬上要講netty源碼,會細致深入介紹 這個算法。
參考文獻:
https://blog.csdn.net/pointer_y/article/details/54292093
https://www.cnblogs.com/fengdejiyixx/p/13285838.html
https://blog.csdn.net/u013291303/article/details/70199645
https://blog.csdn.net/frodocheng/article/details/106718244
https://blog.csdn.net/qq_43811102/article/details/103340748