內(nèi)核線程
為什么需要內(nèi)核線程
Linux內(nèi)核可以看作一個服務(wù)進程(管理軟硬件資源儿奶,響應(yīng)用戶進程的種種合理以及不合理的請求)。
內(nèi)核需要多個執(zhí)行流并行罢坝,為了防止可能的阻塞廓握,支持多線程是必要的。
內(nèi)核線程就是內(nèi)核的分身嘁酿,一個分身可以處理一件特定事情隙券。內(nèi)核線程的調(diào)度由內(nèi)核負責,一個內(nèi)核線程處于阻塞狀態(tài)時不影響其他的內(nèi)核線程闹司,因為其是調(diào)度的基本單位娱仔。
這與用戶線程是不一樣的。因為內(nèi)核線程只運行在內(nèi)核態(tài)
因此游桩,它只能使用大于PAGE_OFFSET(傳統(tǒng)的x86_32上是3G)的地址空間牲迫。
內(nèi)核線程概述
內(nèi)核線程是直接由內(nèi)核本身啟動的進程耐朴。內(nèi)核線程實際上是將內(nèi)核函數(shù)委托給獨立的進程,它與內(nèi)核中的其他進程”并行”執(zhí)行盹憎。內(nèi)核線程經(jīng)常被稱之為內(nèi)核守護進程筛峭。
他們執(zhí)行下列任務(wù)
周期性地將修改的內(nèi)存頁與頁來源塊設(shè)備同步
如果內(nèi)存頁很少使用,則寫入交換區(qū)
管理延時動作, 如2號進程接手內(nèi)核進程的創(chuàng)建
實現(xiàn)文件系統(tǒng)的事務(wù)日志
內(nèi)核線程主要有兩種類型
線程啟動后一直等待陪每,直至內(nèi)核請求線程執(zhí)行某一特定操作影晓。
線程啟動后按周期性間隔運行,檢測特定資源的使用檩禾,在用量超出或低于預(yù)置的限制時采取行動挂签。
內(nèi)核線程由內(nèi)核自身生成,其特點在于
它們在CPU的管態(tài)執(zhí)行盼产,而不是用戶態(tài)饵婆。
它們只可以訪問虛擬地址空間的內(nèi)核部分(高于TASK_SIZE的所有地址),但不能訪問用戶空間
內(nèi)核線程的進程描述符task_struct
task_struct進程描述符中包含兩個跟進程地址空間相關(guān)的字段mm, active_mm戏售,
struct task_struct
{
// ...
struct mm_struct *mm;
struct mm_struct *avtive_mm;
//...
};
大多數(shù)計算機上系統(tǒng)的全部虛擬地址空間分為兩個部分: 供用戶態(tài)程序訪問的虛擬地址空間和供內(nèi)核訪問的內(nèi)核空間侨核。每當內(nèi)核執(zhí)行上下文切換時, 虛擬地址空間的用戶層部分都會切換, 以便當前運行的進程匹配, 而內(nèi)核空間不會放生切換。
對于普通用戶進程來說灌灾,mm指向虛擬地址空間的用戶空間部分芹关,而對于內(nèi)核線程,mm為NULL紧卒。
這位優(yōu)化提供了一些余地, 可遵循所謂的惰性TLB處理(lazy TLB handing)。active_mm主要用于優(yōu)化诗祸,由于內(nèi)核線程不與任何特定的用戶層進程相關(guān)跑芳,內(nèi)核并不需要倒換虛擬地址空間的用戶層部分,保留舊設(shè)置即可直颅。由于內(nèi)核線程之前可能是任何用戶層進程在執(zhí)行博个,故用戶空間部分的內(nèi)容本質(zhì)上是隨機的,內(nèi)核線程決不能修改其內(nèi)容功偿,故將mm設(shè)置為NULL盆佣,同時如果切換出去的是用戶進程,內(nèi)核將原來進程的mm存放在新內(nèi)核線程的active_mm中械荷,因為某些時候內(nèi)核必須知道用戶空間當前包含了什么共耍。
為什么沒有mm指針的進程稱為惰性TLB進程?
假如內(nèi)核線程之后運行的進程與之前是同一個, 在這種情況下, 內(nèi)核并不需要修改用戶空間地址表。地址轉(zhuǎn)換后備緩沖器(即TLB)中的信息仍然有效吨瞎。只有在內(nèi)核線程之后, 執(zhí)行的進程是與此前不同的用戶層進程時, 才需要切換(并對應(yīng)清除TLB數(shù)據(jù))痹兜。
內(nèi)核線程和普通的進程間的區(qū)別在于內(nèi)核線程沒有獨立的地址空間,mm指針被設(shè)置為NULL颤诀;它只在 內(nèi)核空間運行字旭,從來不切換到用戶空間去对湃;并且和普通進程一樣,可以被調(diào)度遗淳,也可以被搶占拍柒。
內(nèi)核線程的創(chuàng)建
創(chuàng)建內(nèi)核線程接口的演變
內(nèi)核線程可以通過兩種方式實現(xiàn):
1、古老的接口 kernel_create和daemonize
將一個函數(shù)傳遞給kernel_thread創(chuàng)建并初始化一個task屈暗,該函數(shù)接下來負責幫助內(nèi)核調(diào)用daemonize已轉(zhuǎn)換為內(nèi)核守護進程拆讯,daemonize隨后完成一些列操作, 如該函數(shù)釋放其父進程的所有資源,不然這些資源會一直鎖定直到線程結(jié)束恐锦。阻塞信號的接收, 將init用作守護進程的父進程
2往果、更加現(xiàn)在的方法kthead_create和kthread_run
創(chuàng)建內(nèi)核更常用的方法是輔助函數(shù)kthread_create,該函數(shù)創(chuàng)建一個新的內(nèi)核線程一铅。最初線程是停止的陕贮,需要使用wake_up_process啟動它。
使用kthread_run潘飘,與kthread_create不同的是肮之,其創(chuàng)建新線程后立即喚醒它,其本質(zhì)就是先用kthread_create創(chuàng)建一個內(nèi)核線程卜录,然后通過wake_up_process喚醒它
2號進程kthreadd的誕生
早期的kernel_create和daemonize接口
在早期的內(nèi)核中, 提供了kernel_create和daemonize接口, 但是這種機制操作復(fù)雜而且將所有的任務(wù)交給內(nèi)核去完成戈擒。
但是這種機制低效而且繁瑣, 將所有的操作塞給內(nèi)核, 我們創(chuàng)建內(nèi)核線程的初衷不本來就是為了內(nèi)核分擔工作, 減少內(nèi)核的開銷的么
Workqueue機制
因此在linux-2.6以后, 提供了更加方便的接口kthead_create和kthread_run, 同時將內(nèi)核線程的創(chuàng)建操作延后, 交給一個工作隊列workqueue。
Linux中的workqueue機制就是為了簡化內(nèi)核線程的創(chuàng)建艰毒。通過kthread_create并不真正創(chuàng)建內(nèi)核線程, 而是將創(chuàng)建工作create work插入到工作隊列helper_wq中, 隨后調(diào)用workqueue的接口就能創(chuàng)建內(nèi)核線程筐高。并且可以根據(jù)當前系統(tǒng)CPU的個數(shù)創(chuàng)建線程的數(shù)量,使得線程處理的事務(wù)能夠并行化丑瞧。workqueue是內(nèi)核中實現(xiàn)簡單而有效的機制柑土,他顯然簡化了內(nèi)核daemon的創(chuàng)建,方便了用戶的編程.
工作隊列(workqueue)是另外一種將工作推后執(zhí)行的形式.工作隊列可以把工作推后绊汹,交由一個內(nèi)核線程去執(zhí)行稽屏,也就是說,這個下半部分可以在進程上下文中執(zhí)行西乖。最重要的就是工作隊列允許被重新調(diào)度甚至是睡眠狐榔。
2號進程kthreadd
但是這種方法依然看起來不夠優(yōu)美, 我們何不把這種創(chuàng)建內(nèi)核線程的工作交給一個特殊的內(nèi)核線程來做呢?
于是linux-2.6.22引入了kthreadd進程, 并隨后演變?yōu)?號進程, 它在系統(tǒng)初始化時同1號進程一起被創(chuàng)建(當然肯定是通過kernel_thread), 并隨后演變?yōu)閯?chuàng)建內(nèi)核線程的真正建造師, 它會循環(huán)的是查詢工作鏈表static LIST_HEAD(kthread_create_list);中是否有需要被創(chuàng)建的內(nèi)核線程, 而我們的通過kthread_create執(zhí)行的操作, 只是在內(nèi)核線程任務(wù)隊列kthread_create_list中增加了一個create任務(wù), 然后會喚醒kthreadd進程來執(zhí)行真正的創(chuàng)建操作
內(nèi)核線程會出現(xiàn)在系統(tǒng)進程列表中, 但是在ps的輸出中進程名command由方括號包圍, 以便與普通進程區(qū)分获雕。
如下圖所示, 我們可以看到系統(tǒng)中, 所有內(nèi)核線程都用[]標識, 而且這些進程父進程id均是2, 而2號進程kthreadd的父進程是0號進程
使用ps -eo pid,ppid,command
kernel_thread
kernel_thread是最基礎(chǔ)的創(chuàng)建內(nèi)核線程的接口, 它通過將一個函數(shù)直接傳遞給內(nèi)核來創(chuàng)建一個進程, 創(chuàng)建的進程運行在內(nèi)核空間, 并且與其他進程線程共享內(nèi)核虛擬地址空間
kernel_thread的實現(xiàn)經(jīng)歷過很多變革
早期的kernel_thread執(zhí)行更底層的操作, 直接創(chuàng)建了task_struct并進行初始化,
引入了kthread_create和kthreadd 2號進程后, kernel_thread的實現(xiàn)也由統(tǒng)一的_do_fork(或者早期的do_fork)托管實現(xiàn)
早期實現(xiàn)
早期的內(nèi)核中, kernel_thread并不是使用統(tǒng)一的do_fork或者_do_fork這一封裝好的接口實現(xiàn)的, 而是使用更底層的細節(jié)薄腻,它內(nèi)部調(diào)用了更加底層的arch_kernel_thread創(chuàng)建了一個線程,但是這種方式創(chuàng)建的線程并不適合運行届案,因此內(nèi)核提供了daemonize函數(shù)被廓。
extern void daemonize(void);
主要執(zhí)行如下操作
該函數(shù)釋放其父進程的所有資源,不然這些資源會一直鎖定直到線程結(jié)束萝玷。
阻塞信號的接收
將init用作守護進程的父進程
我們將了這么多kernel_thread, 但是我們并不提倡我們使用它, 因為這個是底層的創(chuàng)建內(nèi)核線程的操作接口, 使用kernel_thread在內(nèi)核中執(zhí)行大量的操作, 雖然創(chuàng)建的代價已經(jīng)很小了, 但是對于追求性能的linux內(nèi)核來說還不能忍受
因此我們只能說kernel_thread是一個古老的接口, 內(nèi)核中的有些地方仍然在使用該方法, 將一個函數(shù)直接傳遞給內(nèi)核來創(chuàng)建內(nèi)核線程
新版本的實現(xiàn)
于是linux-3.x下之后, 有了更好的實現(xiàn), 那就是
延后內(nèi)核的創(chuàng)建工作, 將內(nèi)核線程的創(chuàng)建工作交給一個內(nèi)核線程來做, 即kthreadd 2號進程
但是在kthreadd還沒創(chuàng)建之前, 我們只能通過kernel_thread這種方式去創(chuàng)建,
同時kernel_thread的實現(xiàn)也改為由_do_fork(早期內(nèi)核中是do_fork)來實現(xiàn)
pid_t kernel_thread(int (*fn)(void *), void *arg, unsigned long flags)
{
return _do_fork(flags|CLONE_VM|CLONE_UNTRACED, (unsigned long)fn,
(unsigned long)arg, NULL, NULL, 0);
}
kthread_create
struct task_struct *kthread_create_on_node(int (*threadfn)(void *data),
void *data,
int node,
const char namefmt[], ...);
#define kthread_create(threadfn, data, namefmt, arg...) \
kthread_create_on_node(threadfn, data, NUMA_NO_NODE, namefmt, ##arg)
創(chuàng)建內(nèi)核更常用的方法是輔助函數(shù)kthread_create嫁乘,該函數(shù)創(chuàng)建一個新的內(nèi)核線程昆婿。最初線程是停止的,需要使用wake_up_process啟動它蜓斧。
kthread_run
/**
* kthread_run - create and wake a thread.
* @threadfn: the function to run until signal_pending(current).
* @data: data ptr for @threadfn.
* @namefmt: printf-style name for the thread.
*
* Description: Convenient wrapper for kthread_create() followed by
* wake_up_process(). Returns the kthread or ERR_PTR(-ENOMEM).
*/
#define kthread_run(threadfn, data, namefmt, ...) \
({ \
struct task_struct *__k \
= kthread_create(threadfn, data, namefmt, ## __VA_ARGS__); \
if (!IS_ERR(__k)) \
wake_up_process(__k); \
__k; \
})
使用kthread_run仓蛆,與kthread_create不同的是,其創(chuàng)建新線程后立即喚醒它挎春,其本質(zhì)就是先用kthread_create創(chuàng)建一個內(nèi)核線程看疙,然后通過wake_up_process喚醒它
內(nèi)核線程的退出
線程一旦啟動起來后,會一直運行直奋,除非該線程主動調(diào)用do_exit函數(shù)能庆,或者其他的進程調(diào)用kthread_stop函數(shù),結(jié)束線程的運行脚线。
int kthread_stop(struct task_struct *thread);
kthread_stop() 通過發(fā)送信號給線程搁胆。
如果線程函數(shù)正在處理一個非常重要的任務(wù),它不會被中斷的邮绿。當然如果線程函數(shù)永遠不返回并且不檢查信號渠旁,它將永遠都不會停止。
在執(zhí)行kthread_stop的時候船逮,目標線程必須沒有退出顾腊,否則會Oops。原因很容易理解挖胃,當目標線程退出的時候杂靶,其對應(yīng)的task結(jié)構(gòu)也變得無效,kthread_stop引用該無效task結(jié)構(gòu)就會出錯酱鸭。
為了避免這種情況伪煤,需要確保線程沒有退出,其方法如代碼中所示:
thread_func()
{
// do your work here
// wait to exit
while(!thread_could_stop())
{
wait();
}
}
exit_code()
{
kthread_stop(_task); //發(fā)信號給task凛辣,通知其可以退出了
}
這種退出機制很溫和,一切盡在thread_func()的掌控之中职烧,線程在退出時可以從容地釋放資源扁誓,而不是莫名其妙地被人“暗殺”。