1.開場白
環境:
內核源碼:linux-5.11
ubuntu版本:20.04.1
代碼閱讀工具:vim+ctags+cscope
在linux系統中, 我們接觸最多的莫過于用戶空間的任務,像用戶線程或用戶進程,因為他們太活躍了,也太耀眼了以至于我們感受不到內核線程的存在,但是內核線程卻在背后默默地付出著,如內存回收,臟頁回寫,處理大量的軟中斷等,如果沒有內核線程那么linux世界是那么的可怕!本文力求與完整介紹完內核線程的整個生命周期,如內核線程的創建、調度等等,當然本文還是主要從內存管理和進程調度兩個維度來解析,且不會涉及到具體的內核線程如kswapd的實現,最后我們會以一個簡單的內核模塊來說明如何在驅動代碼中來創建使用內核線程。
在進入我們真正的主題之前,我們需要知道一下事實:
1. 內核線程永遠運行于內核態絕不會跑到用戶態去執行。
2.由于內核線程運行于內核態,所有它的權限很高,請注意這里說的是權限很高并不意味著它的優先級高,所有他可以直接做到操作頁表,維護cache, 讀寫系統寄存器等操作。
3.內核線性是沒有地址空間的概念,準確的來說是沒有用戶地址空間的概念,使用的是所有進程共享的內核地址空間,但是調度的時候會借用前一個進程的地址空間。
4.內核線程并沒有什么特別神秘的地方,他和普通的用戶任務一樣參與系統調度,也可以被遷移到任何cpu上運行。
5.每個cpu都有自己的idle進程,實質上也是內核線程,但是他們比較特殊,一來是被靜態創建,二來他們的優先級最低,cpu上沒有其他進程運行的時候idle進程才運行。
6.除了初始化階段0號內核線程和kthreadd本身,其他所有的內核線程都是被kthreadd內核線程來間接創建。
2.kthreadd的誕生
盤古開天辟地,我們知道linux所有任務的祖先是0號進程,然后0號進程創建了天字第一號的1號init進程,init進程是所有用戶任務的祖先,而內核線程同樣也有自己的祖先那就是kthreadd內核線程他的pid是2,我們通過top命令可以觀察到:紅色方框都是父進程為2號進程的內核線程,綠色方框為kthreadd,他的父進程為0號進程。
下面我們來看內核線程的祖先線程kthreadd如何創建的:
start_kernel //init/main.c
-》arch_call_rest_init
-》rest_init
-》pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES)
可以看的在rest_init中調用kernel_thread來創建kthreadd內核線程,實際上初始化階段有兩個內核線程比較特殊一個是0號的idle(唯一一個沒有通過fork創建的任務),一個是被idle創建的kthreadd內核線程(內核初始化階段可以看成idle進程在做初始化)。
我們再來看看kernel_thread是如何實現的:
/*
* Create a kernel thread.
*/
pid_t kernel_thread(int (*fn)(void *), void *arg, unsigned long flags)
{
struct kernel_clone_args args = {
.flags = ((lower_32_bits(flags) | CLONE_VM |
| CLONE_UNTRACED) & ~CSIGNAL),
.exit_signal = (lower_32_bits(flags) & CSIGNAL),
.stack = (unsigned long)fn,
.stack_size = (unsigned long)arg,
};
return kernel_clone(&args);
}
這里需要注意兩點:1.fork時傳遞了CLONE_VM標志 2.如何標識要創建出來的是內核線程不是普通的用戶任務
我們先來看看CLONE_VM標志對fork的影響:
kernel_clone
-》copy_process
-》copy_mm
-》dup_mm
-》。..。
1394 tsk-》mm = NULL;
1395 tsk-》active_mm = NULL;
1396
1397 /*
1398 |* Are we cloning a kernel thread?
1399 |*
1400 |* We need to steal a active VM for that.。
1401 |*/
1402 oldmm = current-》mm;
1403 if (!oldmm)
1404 return 0;
1405
1406 /* initialize the new vmacache entries */
1407 vmacache_flush(tsk);
1408
1409 if (clone_flags & CLONE_VM) {
1410 mmget(oldmm);
1411 mm = oldmm;
1412 goto good_mm;
1413 }
1414
1415 retval = -ENOMEM;
1416 mm = dup_mm(tsk, current-》mm);
1417 if (!mm)
1418 goto fail_nomem;
1419
1420 good_mm:
1421 tsk-》mm = mm;
1422 tsk-》active_mm = mm;
1423 return 0;
可以看的當我們傳遞了CLONE_VM標志之后,本來應該走到1409 行進程處理的,但是我們需要知道的是1403 行可能判斷為空,因為這里父進程為idle為內核線程,憑直覺我們知道代碼應該從 1404 返回了,但是不能光憑直覺要拿出證據,那就需要看看idle進程長啥樣了:
64 struct task_struct init_task //init/init_task.c
69 = {
。..
85 .mm = NULL,
86 .active_mm = &init_mm,
上面是靜態創建的idle進程,可以看的他的進程控制塊的 .mm 為空, .active_mm 為&init_mm,所有啊,我們的kthreadd內核線程的tsk-》mm = tsk-》active_mm =NULL;所以我們上面的猜想是對的代碼直接從 1404 返回了,這里也是他應該擁有的屬性,因為我們知道內核線程沒有用戶地址空間(使用tsk-》mm來描述),所以所有的內核線程的tsk-》mm都為空,這也是判斷任務是否為內核線程的一個條件,但是tsk-》active_mm 就不一定了,內核線程在每次進程切換的時候都會借用前一個進程的tsk-》active_mm 賦值到自己tsk-》active_mm 上,后面會講到。這里需要注意的是,有一個內核線程很特殊,特殊到他的tsk-》active_mm 不是在進程切換的時候被賦值而是靜態初始化號,他就是上面的idle線程 .active_mm = &init_mm。
我們來看下init_mm是什么內容,有什么貓膩:
mm/init-mm.c
struct mm_struct init_mm = {
.mm_rb = RB_ROOT,
.pgd = swapper_pg_dir,
。..
可以看到他的特殊之處在于它的tsk-》active_mm-》pgd為swapper_pg_dir,我們知道這是主內核頁表,我們知道系統初始化的時候,會出現3個特殊的任務0,1,2號,這幾個任務剛開始都是內核線程,他們之間進行切換的時候使用的都是swapper_pg_dir這個頁表,也很合理,因為都訪問內核空間,一旦有用戶進程介入參與調度了就不一樣了,就可以借用用戶的tsk-》active_mm-》pgd(這個時候不再是swapper_pg_dir,但是沒有關系,通過ttbr1_el1同樣可以訪問到swapper_pg_dir頁表來訪問內核空間)。
再來看看如何標識要創建的是內核線程的?
kernel_clone
-》copy_process
-》copy_thread //arch/arm64/kernel/process.c
-》 。..
if (likely(!(p-》flags & PF_KTHREAD))) { //創建用戶任務的情況
。..
} else { //創建內核線程的情況
memset(childregs, 0, sizeof(struct pt_regs));
childregs-》pstate = PSR_MODE_EL1h | PSR_IL_BIT;
p-》thread.cpu_context.x19 = stack_start;
p-》thread.cpu_context.x20 = stk_sz;
}
以上路徑是為創建任務準備調度上下文和異常返回現場,調度上下文由 p-》thread.cpu_context來描述,異常返回現場由保存在內核棧的struct pt_regs來描述,在這里判斷p-》flags & PF_KTHREAD))是否成立,也就是如果p-》flags設置了PF_KTHREAD標志則是創建內核線程,但是我們找了一圈貌似沒有找到在哪個位置設置這個標志的,那究竟在哪設置的呢?我們還是首先回到它的父進程也就是idle進程:
struct task_struct init_task
= {
。..
.flags = PF_KTHREAD,
。..
}
憑直覺,應該是父進程設置了然后賦值給了子進程,那我們就要看看合適賦值的:
copy_process
-》dup_task_struct
-》arch_dup_task_struct
-》*dst = *src;
我們看的會把父進程的的task的內容賦值給子進程,然后后面在進程一些個性化設置,.flags = PF_KTHREAD也被設置給了子進程。
ok, 分析到這里idle就創建好了kthreadd內核線程,通過wake_up_new_task喚醒kthreadd運行:當它喚醒被調度后,就會恢復調度上下文,就是上面說的 p-》thread.cpu_context,具體如何執行到內核線程指定的執行函數后面我們會講解!
但是我們需要知道的是,kthreadd被調度執行后執行kthreadd這個函數!!!這個函數實現在:kernel/kthread.c中。
3. kthreadd內核線程處理流程
上面我們介紹了kthreadd內核線程的創建過程,接下來看一下kthreadd做了哪些事情:
代碼路徑為:kernel/kthread.c
kthreadd函數中設置了線程名字和親和性屬性之后 進入下面給出的循環處理流程:
它首先將自己的狀態設置為TASK_INTERRUPTIBLE,然后判斷kthread_create_list鏈表是否為空,這個鏈表存放其他內核路徑的創建內核線程的請求結構struct kthread_create_info:
kernel/kthread.c
struct kthread_create_info
{
/* Information passed to kthread() from kthreadd. */
int (*threadfn)(void *data); //請求創建的內核線程處理函數
void *data; //傳遞給請求創建的內核線程的參數
int node;
/* Result passed back to kthread_create() from kthreadd. */
struct task_struct *result; //請求創建的內核線程的tsk結構
struct completion *done;
struct list_head list; //用于加 入kthread_create_list鏈表
};
有創建內核線程時,會封裝kthread_create_info結構然后加入到kthread_create_list鏈表中。
如果kthread_create_list鏈表為空,說明沒有創建內核線程的請求,則直接調用schedule進行睡眠。當某個內核路徑有kthread_create_info結構加入到kthread_create_list鏈表中并喚醒kthreadd后,kthreadd從__set_current_state(TASK_RUNNING)開始執行,設置狀態為運行狀態,然后進入一個循環,不斷的從kthread_create_list.next取出kthread_create_info結構,并從鏈表中刪除,調用create_kthread創建一個內核線程來執行剩余的工作。
create_kthread很簡單,就是創建內核線程,然后執行kthread函數,將取到的kthread_create_info結構傳遞給這個函數:
kernel/kthread.c
create_kthread
-》 pid = kernel_thread(kthread, create, CLONE_FS | CLONE_FILES | SIGCHLD)
4.kthread處理流程
當kthreadd內核線程創建內核線程之后就完成了它的使命,開始處理kthread_create_list鏈表上的下一個內核線程創建請求,主要工作交給了kthread函數來處理。實際上,kthreadd創建的內核線程就是請求創建的內核線程的外殼,只不過創建完成之后并沒有馬上執行線程的執行函數,這和用戶空間執行程序很相似:一般在shell中執行程序,首先shell進程通過fork創建一個子進程,然后子進程中調用exec來加載新的程序。而創建內核線程也必須首先要創建一個子進程,這是kthreadd通過kernel_thread來完成的,然后在kthread執行函數中在合適的時機來執行所請求的內核線程執行函數。這說起來有點繞,因為這里涉及到了三個任務:kthreadd內核線程,kthreadd內核線程通過kernel_thread創建的內核線程,往kthread_create_list鏈表加入創建請求的那個任務
注:執行kthread函數處于新創建的內核線程上下文!
下面我們來看下kthreadd內核線程創建的內核線程的執行函數kthread:這里傳遞給kthread的參數就是從kthread_create_list鏈表摘取的創建結構kthread_create_info,函數中又出現了一個新的結構struct kthread:
kernel/kthread.c
struct kthread {
unsigned long flags;
unsigned int cpu;
int (*threadfn)(void *); //線程執行函數
void *data; //線程執行函數傳遞的參數
mm_segment_t oldfs;
struct completion parked;
struct completion exited;
#ifdef CONFIG_BLK_CGROUP
struct cgroup_subsys_state *blkcg_css;
#endif
};
其中比較重要的是threadfn和data。kthread函數并不長,我們把代碼都羅列如下:
244 static int kthread(void *_create)
245 {
246 /* Copy data: it‘s on kthread’s stack */
247 struct kthread_create_info *create = _create; //獲取傳遞過來的線程創建信息
248 int (*threadfn)(void *data) = create-》threadfn; //取出 線程執行函數
249 void *data = create-》data; //取出 傳遞給 線程執行函數的參數
250 struct completion *done;
251 struct kthread *self;
252 int ret;
253
254 self = kzalloc(sizeof(*self), GFP_KERNEL); //分配 kthread 結構
255 set_kthread_struct(self); //current-》set_child_tid = (__force void __user *)kthread
256
257 /* If user was SIGKILLed, I release the structure. */
258 done = xchg(&create-》done, NULL); //獲得 done完成量
259 if (!done) {
260 kfree(create);
261 do_exit(-EINTR);
262 }
263
264 if (!self) {
265 create-》result = ERR_PTR(-ENOMEM);
266 complete(done);
267 do_exit(-ENOMEM);
268 }
269
270 self-》threadfn = threadfn; // 賦值 self-》threadfn 為 線程執行函數
271 self-》data = data; // 賦值 self-》data 線程執行函數的參數
272 init_completion(&self-》exited);
273 init_completion(&self-》parked);
274 current-》vfork_done = &self-》exited;
276 /* OK, tell user we‘re spawned, wait for stop or wakeup */
277 __set_current_state(TASK_UNINTERRUPTIBLE); //設置內核線程狀態為 TASK_UNINTERRUPTIBLE 但是此時還沒又睡眠
278 create-》result = current; //用于返回 當前任務的tsk
279 /*
280 |* Thread is going to call schedule(), do not preempt it,
281 |* or the creator may spend more time in wait_task_inactive()。
282 |*/
283 preempt_disable();
284 complete(done); //喚醒等待done完成量的任務
285 schedule_preempt_disabled(); //睡眠
286 preempt_enable(); //喚醒的時候從此開始執行
287
288 ret = -EINTR;
289 if (!test_bit(KTHREAD_SHOULD_STOP, &self-》flags)) { //判斷 self-》flags是否為 KTHREAD_SHOULD_STOP(kthread_stop會設置)
290 cgroup_kthread_ready();
291 __kthread_parkme(self);
292 ret = threadfn(data); //執行 真正的線程執行函數
293 }
294 do_exit(ret); //當前任務退出
295 }
可以看到,kthread函數用到了一些完成量和睡眠函數,如果單獨看這個函數肯定會一頭霧水,要理解這個函數需要回答一下幾個問題:
1.284行的complete(done) 是喚醒哪個任務的?
2.當前內核線程在285 行睡眠后 誰來喚醒我?
5.kthread_run函數
這里我們以kthread_run為例來解答這兩個問題:
kthread_run這個內核api用來創建內核線程并喚醒執行傳遞的執行函數。調用路徑如下:
include/linux/kthread.h
#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函數:
4.1 kthread_create函數
kthread_create
-》kthread_create_on_node
-》__kthread_create_on_node
__kthread_create_on_node函數并不長我們全部羅列:
330 struct task_struct *__kthread_create_on_node(int (*threadfn)(void *data),
331 | void *data, int node,
332 | const char namefmt[],
333 | va_list args)
334 {
335 DECLARE_COMPLETION_ONSTACK(done);
336 struct task_struct *task;
337 struct kthread_create_info *create = kmalloc(sizeof(*create),
338 | GFP_KERNEL); //分配 kthread_create_info結構
339
340 if (!create)
341 return ERR_PTR(-ENOMEM);
342 create-》threadfn = threadfn; //填充kthread_create_info結構 如執行函數等
343 create-》data = data;
344 create-》node = node;
345 create-》done = &done;
346
347 spin_lock(&kthread_create_lock);
348 list_add_tail(&create-》list, &kthread_create_list); //kthread_create_info結構添加到 kthread_create_list 鏈表
349 spin_unlock(&kthread_create_lock);
350
351 wake_up_process(kthreadd_task); //喚醒 kthreadd來處理創建內核線程請求
352 /*
353 |* Wait for completion in killable state, for I might be chosen by
354 |* the OOM killer while kthreadd is trying to allocate memory for
355 |* new kernel thread.
356 |*/
357 if (unlikely(wait_for_completion_killable(&done))) { //等待請求的內核線程創建完成
358 /*
359 |* If I was SIGKILLed before kthreadd (or new kernel thread)
360 |* calls complete(), leave the cleanup of this structure to
361 |* that thread.
362 |*/
363 if (xchg(&create-》done, NULL))
364 return ERR_PTR(-EINTR);
365 /*
366 |* kthreadd (or new kernel thread) will call complete()
367 |* shortly.
368 |*/
369 wait_for_completion(&done);
370 }
371 task = create-》result; //獲得 創建完成的 內核線程的tsk
372 if (!IS_ERR(task)) { // 內核線程創建成功后 進行后續的處理
373 static const struct sched_param param = { .sched_priority = 0 };
374 char name[TASK_COMM_LEN];
375
376 /*
377 |* task is already visible to other tasks, so updating
378 |* COMM must be protected.
379 |*/
380 vsnprintf(name, sizeof(name), namefmt, args);
381 set_task_comm(task, name); //設置 內核線程的名字
382 /*
383 |* root may have changed our (kthreadd’s) priority or CPU mask.
384 |* The kernel thread should not inherit these properties.
385 |*/
386 sched_setscheduler_nocheck(task, SCHED_NORMAL, ?m); //設置 調度策略和優先級
387 set_cpus_allowed_ptr(task,
388 | housekeeping_cpumask(HK_FLAG_KTHREAD)); //設置cpu親和性
389 }
390 kfree(create);
391 return task;
392 }
關于__kthread_create_on_node函數需要明白以下幾點:1.__kthread_create_on_node函數處于一個進程上下文如insmod進程2.__kthread_create_on_node函數需要與兩個任務交互,一個是kthreadd,一個是kthreadd的創建的內核線程(執行函數為kthread)
函數中已經做了詳細的注釋,這里在說明一下:首先函數將需要在內核線程中執行的函數等信息封裝在kthread_create_info結構中,然后加入到kthreadd的kthread_create_list鏈表,接著去喚醒kthreadd去處理創建內核線程請求,上面kthreadd函數我們分析過kthreadd函數會創建一個內核線程來執行kthread函數,并將kthread_create_info結構傳遞過去,在kthread函數中會通過complete(done)來喚醒357的完成等待(這就回答了第一個問題), 然后__kthread_create_on_node接著進行初始化,但是需要明白的是新創建的內核線程現在處于睡眠狀態,等待被喚醒。
4.2 wake_up_process喚醒
上面通過kthread_create創建完成內核線程之后,內核線程處于TASK_UNINTERRUPTIBLE狀態,等待被喚醒,這個時候kthread_run調用wake_up_process喚醒新創建的內核線程,內核線程愉快的執行,走到了kthread函數的threadfn(data)處,執行真正的線程處理,至此,新創建的內核線程開始完成實質性的工作。
6. kthread_stop函數
一般通過kthread_create創建的內核線程可以通過kthread_stop來停止:
609 int kthread_stop(struct task_struct *k)
610 {
611 struct kthread *kthread;
612 int ret;
613
614 trace_sched_kthread_stop(k);
615
616 get_task_struct(k);
617 kthread = to_kthread(k); //tsk中獲得kthread 結構
618 set_bit(KTHREAD_SHOULD_STOP, &kthread-》flags); //設置KTHREAD_SHOULD_STOP標志
619 kthread_unpark(k);
620 wake_up_process(k); //喚醒
621 wait_for_completion(&kthread-》exited); //等待退出完成
622 ret = k-》exit_code; //獲得退出碼
623 put_task_struct(k);
624
625 trace_sched_kthread_stop_ret(ret);
626 return ret;
627 }
一般內核線程會循環執行一些事務,每次循環開始會調用kthread_should_stop來判斷線程是否應該停止:
bool kthread_should_stop(void)
{
return test_bit(KTHREAD_SHOULD_STOP, &to_kthread(current)-》flags); //判斷KTHREAD_SHOULD_STOP標志是否置位
}
在某個內核路徑調用kthread_stop,內核線程每次循環開始的時候,如果檢查到KTHREAD_SHOULD_STOP標志置位,就會退出,然后調用do_exit完成退出操作。
上面講解到很多函數也涉及到很多任務,下面總結一下:1.涉及到的函數有:kthreadd, kthread,kthread_run,kthread_create, wake_up_process, kthread_stop, kthread_should_stopkthreadd:為kthreadd內核線程執行函數,處理內核線程創建任務。kthread:每次kthreadd創建新的內核線程都會執行kthread,里面會涉及到睡眠和喚醒后執行線程執行函數操作。kthread_run:創建并喚醒一個內核線程kthread_create:創建一個內核線程,創建之后處于TASK_UNINTERRUPTIBLE狀態wake_up_process:喚醒一個任務kthread_stop:停止一個內核線程kthread_should_stop:判斷一個內核線程是否應該停止2.涉及到的kthreadd內核線程,新創建的內核線程,發起創建內核線程請求的任務,他們直接通過完成量進行同步3.睡眠喚醒流程:先設置進程狀態為TASK_UNINTERRUPTIBLE這樣的狀態,然后調度出去,喚醒的時候在調度回來
好了,下面給出精心制作的調用圖示:
上面已經講解完了,內核線程是如何被創建的,又是如何執行處理函數的,涉及到多個任務直接同步問題,看代碼的時候需要多個窗口配合之看才行。
編輯:lyn
-
代碼
+關注
關注
30文章
4823瀏覽量
68899 -
LINUX內核
+關注
關注
1文章
316瀏覽量
21703
原文標題:深入理解Linux內核之內核線程(上)
文章出處:【微信號:LinuxDev,微信公眾號:Linux閱碼場】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論