一文帶你搞懂Linux內(nèi)核之內(nèi)核線程(二)超詳細~
雖然講解完了內(nèi)核線程的創(chuàng)建過程,但是似乎又少點什么,那么下面我們來看兩個細節(jié):內(nèi)核線程執(zhí)行處理函數(shù)和內(nèi)核線程上下文切換細節(jié):
7.內(nèi)核線程執(zhí)行處理函數(shù)細節(jié)
內(nèi)核線程執(zhí)行到處理函數(shù)要從fork說起:
7.1 fork準備調(diào)度上下文
上面fork 對于創(chuàng)建內(nèi)核線程已經(jīng)注釋的很清楚,這是為內(nèi)核線程第一次被調(diào)度執(zhí)行做準備。
7.2 使用調(diào)度上下文
當內(nèi)核線程被喚醒,在合適的時機被調(diào)度時,會執(zhí)行如下內(nèi)核路徑:

會將內(nèi)核線程的 p->thread.cpu_context.pc 恢復到pc,然后就執(zhí)行了ret_from_fork:
首先調(diào)用schedule_tail對前一個進程進程收尾工作,然后就判斷x19寄存器的值是否為0, 其實有一個細節(jié)在copy_thread中首先就對p->thread.cpu_context做了清零操作
上面copy_thread中我們已經(jīng)看到對 p->thread.cpu_context.x19 設置為了線程執(zhí)行函數(shù),調(diào)度的時候,設置進了x19 中,接著ret_from_fork將 x20賦值到x0, 就做了內(nèi)核線程參數(shù)的傳遞動作,接著就執(zhí)行958行跳轉(zhuǎn)到了線程執(zhí)行函數(shù)中執(zhí)行了,于是新創(chuàng)建的內(nèi)核線程才開始真正的歡樂執(zhí)行。
【文章福利】小編推薦自己的Linux內(nèi)核技術交流群:【891587639】整理了一些個人覺得比較好的學習書籍、視頻資料共享在群文件里面,有需要的可以自行添加哦?。?!前100名進群領取,額外贈送一份價值699的內(nèi)核資料包(含視頻教程、電子書、實戰(zhàn)項目及代碼)? ??


8.內(nèi)核線程上下文切換細節(jié)
現(xiàn)在來說下內(nèi)核線程進行上下文切換時的技術細節(jié):
8.1 關于mm_struct的借用
我們知道內(nèi)核線程比較特殊沒有用戶地址空間的概念,共享內(nèi)核地址空間,而mm_struct結(jié)構(gòu)專門用來描述用戶地址空間的,我們知道對于arm64架構(gòu)來說,有兩個頁表基址寄存器ttbr0_el1和ttbr1_el1, ttbr0_el1用來存放用戶地址空間的頁表基地址,在每次調(diào)度上下文切換的時候從tsk->mm->pgd加載,ttbr1_el1是內(nèi)核地址空間的頁表基地址,內(nèi)核初始化完成之后存放swapper_pg_dir的地址。
所以,當切換下一個任務為內(nèi)核線程的時候不需要切換用戶地址空間:
內(nèi)核線程的tsk->mm永遠為空,因為它沒有用戶地址空間的概念,但是他在調(diào)度的時候需要借用前一個用戶任務的active_mm賦值到自己的active_mm,為什么要這樣做呢?
這個問題可能很多人都想搞明白,那就還從地址空間切換說起:我們知道對于用戶任務來說(用戶進程或者線程),他們的tsk->mm = tsk->active_mm,并且在fork的時候已經(jīng)分配好mm_struct結(jié)構(gòu),而且申請好了私有的pgd頁賦值到tsk->mm->pgd。如果是user1 -> user2 這樣的切換,因為是兩個不同的用戶進程,所以必須切換地址空間,但是如果是user1 -> kernel1 ->user1 這樣的情況會是怎樣?首先我們知道的是:user1 -> kernel1的時候不需要切換地址空間,但是需要做kernel1->active_mm = user1->active_mm的處理,而當 kernel1 ->user1切換時,情況就不一樣了,這個時候next->mm!= NULL, 所有會走下面的邏輯switch_mm_irqs_off(prev->active_mm, next->mm, next):
switch_mm中,prev=prev->active_mm 而next= next->mm,在我們上面分析的場景user1 -> kernel1 ->user1,則prev= kernel1->active_mm =user1->active_mm=user1->mm,而next= user1->mm,可以發(fā)現(xiàn)兩者相等,所以這種情況下是不需要切換地址空間的。 以下場景都不會導致地址空間切換:user1 -> kernel1 -> kernel2 -> kernel3 ->user1 user1 -> kernel1 -> user1 -> kernel2 -> kernel3
下圖給出了地址空間切換圖示:

我們只關注內(nèi)核線程的切換情況,從Ub->ka->kb->Ub切換過程中,都不需要切換地址空間。
8.2 內(nèi)核線程虛擬地址轉(zhuǎn)換情況
下面我們來看下,內(nèi)核線程虛擬地址轉(zhuǎn)換的情況,我們都知道,對于用戶任務,調(diào)度時會切換地址空間,即是將tsk->mm->pgd放到ttbr0_el1(對于arm64來說)中,我們訪問用戶虛擬地址的時候,mmu通過ttbr0_el1查詢各級頁表最終找到物理地址(當然mmu首先會從tlb中查詢頁表項查詢不到才進行多級頁表遍歷),那么對于內(nèi)核線程怎么辦,它可沒有tsk->mm結(jié)構(gòu),那么它是如何進程地址轉(zhuǎn)換的呢?
答案就是:內(nèi)核線程共享內(nèi)核地址空間,也只能訪問內(nèi)核地址空間,使用swapper_pg_dir去查詢頁表就可以,而對于arm64來說swapper_pg_dir在內(nèi)核初始化的時候被加載到ttbr1_le1中,一旦內(nèi)核線程訪問內(nèi)核虛擬地址,則mmu就會從ttbr1_le1指向的頁表基地址開始查詢各級頁表,進行正常的虛實地址轉(zhuǎn)換。當然,上面是arm64這種架構(gòu)的處理,它有兩個頁表基地址寄存器,其他很多處理器如x86, riscv處理器架構(gòu)都只有一個頁表基址寄存器,如x86的cr3,那么這個時候怎么辦呢?答案是:使用內(nèi)核線程借用的prev->active_mm來做,實際上前一個用戶任務(記住:不一定是上一個,有可能上上個任務才是用戶任務)的active_mm=mm,當切換到前一個用戶任務的時候就會將tsk->mm->pgd放到cr3, 對于x86這樣的只有一個頁表基址寄存器的處理器架構(gòu)來說,tsk->mm->pgd存放的是整個虛擬地址空間的頁表基地址,在fork的時候會將主內(nèi)核頁表的pgd表項拷貝到tsk->mm->pgd對于表項中(有興趣可以查看fork的copy_mm相關代碼,對于arm64這樣的架構(gòu)沒有做內(nèi)核頁表同步)。
9. 內(nèi)核中創(chuàng)建內(nèi)核線程用例
下面我們來看下,內(nèi)核中創(chuàng)建內(nèi)核線程為系統(tǒng)服務的用例,我們只提及不講解具體的服務邏輯。
用例1:linux系統(tǒng)中,當內(nèi)存不足時,會喚醒kswapd內(nèi)核線程來進行異步內(nèi)存回收,下面我們來看他的創(chuàng)建過程:
用例2:Linux軟中斷是下半部的一種機制,一般對效率要求較高的場景會使用到,如網(wǎng)卡收發(fā)包,每當上半部執(zhí)行完了會執(zhí)行到軟中斷,軟中斷會搶占進程上下文執(zhí)行,但是如果軟中斷處理太頻繁,會導致高優(yōu)先級的進程得不到執(zhí)行,所以在軟中斷執(zhí)行的時候會對執(zhí)行次數(shù)和執(zhí)行時間做限制,會將超過限制的軟中斷處理推到ksoftirqd來執(zhí)行。
我們來看下ksoftirqd內(nèi)核線程的創(chuàng)建:
可以看到這里雖然沒有使用kthread_run這樣的api創(chuàng)建內(nèi)核線程,但是還是和kthread_run實現(xiàn)一樣將內(nèi)核線程創(chuàng)建信息添加到kthread_create_list鏈表 然后喚醒kthreadd來創(chuàng)建內(nèi)核線程,最后會綁定到對應的cpu上去。
10.實踐環(huán)節(jié)
前面我們分析了內(nèi)核線程的創(chuàng)建過程,也分析了很多的源代碼,最后我們來實戰(zhàn)一下,來使用內(nèi)核的api來創(chuàng)建內(nèi)核線程為我們服務(這里我們創(chuàng)建一個內(nèi)核線程,然后每隔一秒打印一串字符 :I am kernel thread: 小寫字母循環(huán))。
內(nèi)核模塊代碼:kthread_demo.c
Makefile代碼:
測試:
