一文帶你搞懂Linux內(nèi)核之內(nèi)核線程(一)超詳細(xì)~
1.前言
環(huán)境:
處理器架構(gòu):arm64
內(nèi)核源碼:linux-5.11
ubuntu版本:20.04.1
代碼閱讀工具:vim+ctags+cscope
在linux系統(tǒng)中, 我們接觸最多的莫過于用戶空間的任務(wù),像用戶線程或用戶進(jìn)程,因?yàn)樗麄兲钴S了,也太耀眼了以至于我們感受不到內(nèi)核線程的存在,但是內(nèi)核線程卻在背后默默地付出著,如內(nèi)存回收,臟頁(yè)回寫,處理大量的軟中斷等,如果沒有內(nèi)核線程那么linux世界是那么的可怕!本文力求與完整介紹完內(nèi)核線程的整個(gè)生命周期,如內(nèi)核線程的創(chuàng)建、調(diào)度等等,當(dāng)然本文還是主要從內(nèi)存管理和進(jìn)程調(diào)度兩個(gè)維度來(lái)解析,且不會(huì)涉及到具體的內(nèi)核線程如kswapd的實(shí)現(xiàn),最后我們會(huì)以一個(gè)簡(jiǎn)單的內(nèi)核模塊來(lái)說(shuō)明如何在驅(qū)動(dòng)代碼中來(lái)創(chuàng)建使用內(nèi)核線程。
在進(jìn)入我們真正的主題之前,我們需要知道一下事實(shí):
內(nèi)核線程永遠(yuǎn)運(yùn)行于內(nèi)核態(tài)絕不會(huì)跑到用戶態(tài)去執(zhí)行。2.由于內(nèi)核線程運(yùn)行于內(nèi)核態(tài),所有它的權(quán)限很高,請(qǐng)注意這里說(shuō)的是權(quán)限很高并不意味著它的優(yōu)先級(jí)高,所有他可以直接做到操作頁(yè)表,維護(hù)cache, 讀寫系統(tǒng)寄存器等操作。3.內(nèi)核線性是沒有地址空間的概念,準(zhǔn)確的來(lái)說(shuō)是沒有用戶地址空間的概念,使用的是所有進(jìn)程共享的內(nèi)核地址空間,但是調(diào)度的時(shí)候會(huì)借用前一個(gè)進(jìn)程的地址空間。4.內(nèi)核線程并沒有什么特別神秘的地方,他和普通的用戶任務(wù)一樣參與系統(tǒng)調(diào)度,也可以被遷移到任何cpu上運(yùn)行。5.每個(gè)cpu都有自己的idle進(jìn)程,實(shí)質(zhì)上也是內(nèi)核線程,但是他們比較特殊,一來(lái)是被靜態(tài)創(chuàng)建,二來(lái)他們的優(yōu)先級(jí)最低,cpu上沒有其他進(jìn)程運(yùn)行的時(shí)候idle進(jìn)程才運(yùn)行。6.除了初始化階段0號(hào)內(nèi)核線程和kthreadd本身,其他所有的內(nèi)核線程都是被kthreadd內(nèi)核線程來(lái)間接創(chuàng)建。
【文章福利】小編推薦自己的Linux內(nèi)核技術(shù)交流群:【891587639】整理了一些個(gè)人覺得比較好的學(xué)習(xí)書籍、視頻資料共享在群文件里面,有需要的可以自行添加哦?。?!前100名進(jìn)群領(lǐng)取,額外贈(zèng)送一份價(jià)值699的內(nèi)核資料包(含視頻教程、電子書、實(shí)戰(zhàn)項(xiàng)目及代碼)? ?


2.kthreadd的誕生
盤古開天辟地,我們知道linux所有任務(wù)的祖先是0號(hào)進(jìn)程,然后0號(hào)進(jìn)程創(chuàng)建了天字第一號(hào)的1號(hào)init進(jìn)程,init進(jìn)程是所有用戶任務(wù)的祖先,而內(nèi)核線程同樣也有自己的祖先那就是kthreadd內(nèi)核線程他的pid是2,我們通過top命令可以觀察到:紅色方框都是父進(jìn)程為2號(hào)進(jìn)程的內(nèi)核線程,綠色方框?yàn)閗threadd,他的父進(jìn)程為0號(hào)進(jìn)程。

下面我們來(lái)看內(nèi)核線程的祖先線程kthreadd如何創(chuàng)建的:
可以看的在rest_init中調(diào)用kernel_thread來(lái)創(chuàng)建kthreadd內(nèi)核線程,實(shí)際上初始化階段有兩個(gè)內(nèi)核線程比較特殊一個(gè)是0號(hào)的idle(唯一一個(gè)沒有通過fork創(chuàng)建的任務(wù)),一個(gè)是被idle創(chuàng)建的kthreadd內(nèi)核線程(內(nèi)核初始化階段可以看成idle進(jìn)程在做初始化)。
我們?cè)賮?lái)看看kernel_thread是如何實(shí)現(xiàn)的:
這里需要注意兩點(diǎn):1.fork時(shí)傳遞了CLONE_VM標(biāo)志 2.如何標(biāo)識(shí)要?jiǎng)?chuàng)建出來(lái)的是內(nèi)核線程不是普通的用戶任務(wù)
我們先來(lái)看看CLONE_VM標(biāo)志對(duì)fork的影響:
可以看的當(dāng)我們傳遞了CLONE_VM標(biāo)志之后,本來(lái)應(yīng)該走到1409 行進(jìn)程處理的,但是我們需要知道的是1403 行可能判斷為空,因?yàn)檫@里父進(jìn)程為idle為內(nèi)核線程,憑直覺我們知道代碼應(yīng)該從 1404 返回了,但是不能光憑直覺要拿出證據(jù),那就需要看看idle進(jìn)程長(zhǎng)啥樣了:
上面是靜態(tài)創(chuàng)建的idle進(jìn)程,可以看的他的進(jìn)程控制塊的 .mm 為空, .active_mm 為&init_mm,所有啊,我們的kthreadd內(nèi)核線程的tsk->mm = tsk->active_mm =NULL;所以我們上面的猜想是對(duì)的代碼直接從 1404 返回了,這里也是他應(yīng)該擁有的屬性,因?yàn)槲覀冎纼?nèi)核線程沒有用戶地址空間(使用tsk->mm來(lái)描述),所以所有的內(nèi)核線程的tsk->mm都為空,這也是判斷任務(wù)是否為內(nèi)核線程的一個(gè)條件,但是tsk->active_mm 就不一定了,內(nèi)核線程在每次進(jìn)程切換的時(shí)候都會(huì)借用前一個(gè)進(jìn)程的tsk->active_mm 賦值到自己tsk->active_mm 上,后面會(huì)講到。這里需要注意的是,有一個(gè)內(nèi)核線程很特殊,特殊到他的tsk->active_mm 不是在進(jìn)程切換的時(shí)候被賦值而是靜態(tài)初始化號(hào),他就是上面的idle線程 .active_mm = &init_mm。
我們來(lái)看下init_mm是什么內(nèi)容,有什么貓膩:
可以看到他的特殊之處在于它的tsk->active_mm->pgd為swapper_pg_dir,我們知道這是主內(nèi)核頁(yè)表,我們知道系統(tǒng)初始化的時(shí)候,會(huì)出現(xiàn)3個(gè)特殊的任務(wù)0,1,2號(hào),這幾個(gè)任務(wù)剛開始都是內(nèi)核線程,他們之間進(jìn)行切換的時(shí)候使用的都是swapper_pg_dir這個(gè)頁(yè)表,也很合理,因?yàn)槎荚L問內(nèi)核空間,一旦有用戶進(jìn)程介入?yún)⑴c調(diào)度了就不一樣了,就可以借用用戶的tsk->active_mm->pgd(這個(gè)時(shí)候不再是swapper_pg_dir,但是沒有關(guān)系,通過ttbr1_el1同樣可以訪問到swapper_pg_dir頁(yè)表來(lái)訪問內(nèi)核空間)。
再來(lái)看看如何標(biāo)識(shí)要?jiǎng)?chuàng)建的是內(nèi)核線程的?
以上路徑是為創(chuàng)建任務(wù)準(zhǔn)備調(diào)度上下文和異常返回現(xiàn)場(chǎng),調(diào)度上下文由 p->thread.cpu_context來(lái)描述,異常返回現(xiàn)場(chǎng)由保存在內(nèi)核棧的struct pt_regs來(lái)描述,在這里判斷p->flags & PF_KTHREAD))是否成立,也就是如果p->flags設(shè)置了PF_KTHREAD標(biāo)志則是創(chuàng)建內(nèi)核線程,但是我們找了一圈貌似沒有找到在哪個(gè)位置設(shè)置這個(gè)標(biāo)志的,那究竟在哪設(shè)置的呢?我們還是首先回到它的父進(jìn)程也就是idle進(jìn)程:
憑直覺,應(yīng)該是父進(jìn)程設(shè)置了然后賦值給了子進(jìn)程,那我們就要看看合適賦值的:
我們看的會(huì)把父進(jìn)程的的task的內(nèi)容賦值給子進(jìn)程,然后后面在進(jìn)程一些個(gè)性化設(shè)置,.flags = PF_KTHREAD也被設(shè)置給了子進(jìn)程。
ok, 分析到這里idle就創(chuàng)建好了kthreadd內(nèi)核線程,通過wake_up_new_task喚醒kthreadd運(yùn)行:當(dāng)它喚醒被調(diào)度后,就會(huì)恢復(fù)調(diào)度上下文,就是上面說(shuō)的 p->thread.cpu_context,具體如何執(zhí)行到內(nèi)核線程指定的執(zhí)行函數(shù)后面我們會(huì)講解!
但是我們需要知道的是,kthreadd被調(diào)度執(zhí)行后執(zhí)行kthreadd這個(gè)函數(shù)?。?!這個(gè)函數(shù)實(shí)現(xiàn)在:kernel/kthread.c中。
3. kthreadd內(nèi)核線程處理流程
上面我們介紹了kthreadd內(nèi)核線程的創(chuàng)建過程,接下來(lái)看一下kthreadd做了哪些事情:
代碼路徑為:kernel/kthread.c
kthreadd函數(shù)中設(shè)置了線程名字和親和性屬性之后 進(jìn)入下面給出的循環(huán)處理流程:

它首先將自己的狀態(tài)設(shè)置為TASK_INTERRUPTIBLE,然后判斷kthread_create_list鏈表是否為空,這個(gè)鏈表存放其他內(nèi)核路徑的創(chuàng)建內(nèi)核線程的請(qǐng)求結(jié)構(gòu)struct kthread_create_info:
有創(chuàng)建內(nèi)核線程時(shí),會(huì)封裝kthread_create_info結(jié)構(gòu)然后加入到kthread_create_list鏈表中。
如果kthread_create_list鏈表為空,說(shuō)明沒有創(chuàng)建內(nèi)核線程的請(qǐng)求,則直接調(diào)用schedule進(jìn)行睡眠。當(dāng)某個(gè)內(nèi)核路徑有kthread_create_info結(jié)構(gòu)加入到kthread_create_list鏈表中并喚醒kthreadd后,kthreadd從__set_current_state(TASK_RUNNING)開始執(zhí)行,設(shè)置狀態(tài)為運(yùn)行狀態(tài),然后進(jìn)入一個(gè)循環(huán),不斷的從kthread_create_list.next取出kthread_create_info結(jié)構(gòu),并從鏈表中刪除,調(diào)用create_kthread創(chuàng)建一個(gè)內(nèi)核線程來(lái)執(zhí)行剩余的工作。
create_kthread很簡(jiǎn)單,就是創(chuàng)建內(nèi)核線程,然后執(zhí)行kthread函數(shù),將取到的kthread_create_info結(jié)構(gòu)傳遞給這個(gè)函數(shù):
4.kthread處理流程
當(dāng)kthreadd內(nèi)核線程創(chuàng)建內(nèi)核線程之后就完成了它的使命,開始處理kthread_create_list鏈表上的下一個(gè)內(nèi)核線程創(chuàng)建請(qǐng)求,主要工作交給了kthread函數(shù)來(lái)處理。實(shí)際上,kthreadd創(chuàng)建的內(nèi)核線程就是請(qǐng)求創(chuàng)建的內(nèi)核線程的外殼,只不過創(chuàng)建完成之后并沒有馬上執(zhí)行線程的執(zhí)行函數(shù),這和用戶空間執(zhí)行程序很相似:一般在shell中執(zhí)行程序,首先shell進(jìn)程通過fork創(chuàng)建一個(gè)子進(jìn)程,然后子進(jìn)程中調(diào)用exec來(lái)加載新的程序。而創(chuàng)建內(nèi)核線程也必須首先要?jiǎng)?chuàng)建一個(gè)子進(jìn)程,這是kthreadd通過kernel_thread來(lái)完成的,然后在kthread執(zhí)行函數(shù)中在合適的時(shí)機(jī)來(lái)執(zhí)行所請(qǐng)求的內(nèi)核線程執(zhí)行函數(shù)。這說(shuō)起來(lái)有點(diǎn)繞,因?yàn)檫@里涉及到了三個(gè)任務(wù):kthreadd內(nèi)核線程,kthreadd內(nèi)核線程通過kernel_thread創(chuàng)建的內(nèi)核線程,往kthread_create_list鏈表加入創(chuàng)建請(qǐng)求的那個(gè)任務(wù)
注:執(zhí)行kthread函數(shù)處于新創(chuàng)建的內(nèi)核線程上下文!
下面我們來(lái)看下kthreadd內(nèi)核線程創(chuàng)建的內(nèi)核線程的執(zhí)行函數(shù)kthread:這里傳遞給kthread的參數(shù)就是從kthread_create_list鏈表摘取的創(chuàng)建結(jié)構(gòu)kthread_create_info,函數(shù)中又出現(xiàn)了一個(gè)新的結(jié)構(gòu)struct kthread:
其中比較重要的是threadfn和data。kthread函數(shù)并不長(zhǎng),我們把代碼都羅列如下:
可以看到,kthread函數(shù)用到了一些完成量和睡眠函數(shù),如果單獨(dú)看這個(gè)函數(shù)肯定會(huì)一頭霧水,要理解這個(gè)函數(shù)需要回答一下幾個(gè)問題:
284行的complete(done) 是喚醒哪個(gè)任務(wù)的?
當(dāng)前內(nèi)核線程在285 行睡眠后 誰(shuí)來(lái)喚醒我?
5.kthread_run函數(shù)
這里我們以kthread_run為例來(lái)解答這兩個(gè)問題:
kthread_run這個(gè)內(nèi)核api用來(lái)創(chuàng)建內(nèi)核線程并喚醒執(zhí)行傳遞的執(zhí)行函數(shù)。調(diào)用路徑如下:
kthread_run這個(gè)宏傳遞三個(gè)參數(shù):執(zhí)行函數(shù),執(zhí)行函數(shù)傳遞的參數(shù),格式化線程名字
我們先來(lái)看下kthread_create函數(shù):
5.1 kthread_create函數(shù)
__kthread_create_on_node函數(shù)并不長(zhǎng)我們?nèi)苛_列:
關(guān)于__kthread_create_on_node函數(shù)需要明白以下幾點(diǎn):1.__kthread_create_on_node函數(shù)處于一個(gè)進(jìn)程上下文如insmod進(jìn)程 2.__kthread_create_on_node函數(shù)需要與兩個(gè)任務(wù)交互,一個(gè)是kthreadd,一個(gè)是kthreadd的創(chuàng)建的內(nèi)核線程(執(zhí)行函數(shù)為kthread)
函數(shù)中已經(jīng)做了詳細(xì)的注釋,這里在說(shuō)明一下:首先函數(shù)將需要在內(nèi)核線程中執(zhí)行的函數(shù)等信息封裝在kthread_create_info結(jié)構(gòu)中,然后加入到kthreadd的kthread_create_list鏈表,接著去喚醒kthreadd去處理創(chuàng)建內(nèi)核線程請(qǐng)求,上面kthreadd函數(shù)我們分析過kthreadd函數(shù)會(huì)創(chuàng)建一個(gè)內(nèi)核線程來(lái)執(zhí)行kthread函數(shù),并將kthread_create_info結(jié)構(gòu)傳遞過去,在kthread函數(shù)中會(huì)通過complete(done)來(lái)喚醒357的完成等待(這就回答了第一個(gè)問題), 然后__kthread_create_on_node接著進(jìn)行初始化,但是需要明白的是新創(chuàng)建的內(nèi)核線程現(xiàn)在處于睡眠狀態(tài),等待被喚醒。
5.2 wake_up_process喚醒
上面通過kthread_create創(chuàng)建完成內(nèi)核線程之后,內(nèi)核線程處于TASK_UNINTERRUPTIBLE狀態(tài),等待被喚醒,這個(gè)時(shí)候kthread_run調(diào)用wake_up_process喚醒新創(chuàng)建的內(nèi)核線程,內(nèi)核線程愉快的執(zhí)行,走到了kthread函數(shù)的threadfn(data)處,執(zhí)行真正的線程處理,至此,新創(chuàng)建的內(nèi)核線程開始完成實(shí)質(zhì)性的工作。
6. kthread_stop函數(shù)
一般通過kthread_create創(chuàng)建的內(nèi)核線程可以通過kthread_stop來(lái)停止:
一般內(nèi)核線程會(huì)循環(huán)執(zhí)行一些事務(wù),每次循環(huán)開始會(huì)調(diào)用kthread_should_stop來(lái)判斷線程是否應(yīng)該停止:
在某個(gè)內(nèi)核路徑調(diào)用kthread_stop,內(nèi)核線程每次循環(huán)開始的時(shí)候,如果檢查到KTHREAD_SHOULD_STOP標(biāo)志置位,就會(huì)退出,然后調(diào)用do_exit完成退出操作。
上面講解到很多函數(shù)也涉及到很多任務(wù),下面總結(jié)一下:1.涉及到的函數(shù)有:kthreadd, kthread,kthread_run,kthread_create, wake_up_process, kthread_stop, kthread_should_stop kthreadd:為kthreadd內(nèi)核線程執(zhí)行函數(shù),處理內(nèi)核線程創(chuàng)建任務(wù)。kthread:每次kthreadd創(chuàng)建新的內(nèi)核線程都會(huì)執(zhí)行kthread,里面會(huì)涉及到睡眠和喚醒后執(zhí)行線程執(zhí)行函數(shù)操作。kthread_run:創(chuàng)建并喚醒一個(gè)內(nèi)核線程 kthread_create:創(chuàng)建一個(gè)內(nèi)核線程,創(chuàng)建之后處于TASK_UNINTERRUPTIBLE狀態(tài) wake_up_process:?jiǎn)拘岩粋€(gè)任務(wù) kthread_stop:停止一個(gè)內(nèi)核線程 kthread_should_stop:判斷一個(gè)內(nèi)核線程是否應(yīng)該停止2.涉及到的kthreadd內(nèi)核線程,新創(chuàng)建的內(nèi)核線程,發(fā)起創(chuàng)建內(nèi)核線程請(qǐng)求的任務(wù),他們直接通過完成量進(jìn)行同步 3.睡眠喚醒流程:先設(shè)置進(jìn)程狀態(tài)為TASK_UNINTERRUPTIBLE這樣的狀態(tài),然后調(diào)度出去,喚醒的時(shí)候在調(diào)度回來(lái)
好了,下面給出精心制作的調(diào)用圖示:

上面已經(jīng)講解完了,內(nèi)核線程是如何被創(chuàng)建的,又是如何執(zhí)行處理函數(shù)的,涉及到多個(gè)任務(wù)直接同步問題,看代碼的時(shí)候需要多個(gè)窗口配合之看才行。
