一篇學(xué)會內(nèi)核線程的創(chuàng)建和運(yùn)行
上面講完了用戶進(jìn)程/線程的創(chuàng)建,這里我們看下內(nèi)核是如何創(chuàng)建線程的。
通過 ps 命令可以看到紅色方框標(biāo)出的都是父進(jìn)程為2號進(jìn)程的內(nèi)核線程,2號進(jìn)程即藍(lán)色方框標(biāo)出的進(jìn)程 kthreadd,1號進(jìn)程是綠色方框標(biāo)出的進(jìn)程 init,它們的父進(jìn)程號都是0。
下面我們一起看下,內(nèi)核的0號,1號,2號線程的創(chuàng)建過程。
0號線程
linux 內(nèi)核中為0號進(jìn)程專門定義了一個靜態(tài)的 task_struct 的結(jié)構(gòu),稱為 init_task:
- /* include/linux/init_task.h */
- #define INIT_TASK_COMM "swapper"
- /* init/init_task.c */
- struct task_struct init_task
- #ifdef CONFIG_ARCH_TASK_STRUCT_ON_STACK
- __init_task_data
- #endif
- __aligned(L1_CACHE_BYTES)
- = {
- #ifdef CONFIG_THREAD_INFO_IN_TASK
- .thread_info = INIT_THREAD_INFO(init_task),
- .stack_refcount = REFCOUNT_INIT(1),
- #endif
- .state = 0,
- .stack = init_stack,
- .usage = REFCOUNT_INIT(2),
- .flags = PF_KTHREAD,
- .prio = MAX_PRIO - 20,
- .static_prio = MAX_PRIO - 20,
- .normal_prio = MAX_PRIO - 20,
- .policy = SCHED_NORMAL,
- .cpus_ptr = &init_task.cpus_mask,
- .cpus_mask = CPU_MASK_ALL,
- .nr_cpus_allowed= NR_CPUS,
- .mm = NULL,
- .active_mm = &init_mm,
- ......
- .comm = INIT_TASK_COMM,
- .thread = INIT_THREAD,
- .fs = &init_fs,
- .files = &init_files,
- ......
- };
- EXPORT_SYMBOL(init_task);
這個結(jié)構(gòu)體中的成員都是靜態(tài)定義的,這里看幾個比較重要的變量:
- .thread_info = INIT_THREAD_INFO(init_task), 這個結(jié)構(gòu)在 “task_struct, thread_info 和內(nèi)核棧 sp 的關(guān)系” 中有詳細(xì)的描述
- .stack = init_stack, init_stack 是內(nèi)核棧的靜態(tài)定義,定義在鏈接腳本里
- /* include/asm-generic/vmlinux.lds.h */
- #define INIT_TASK_DATA(align) \
- . = ALIGN(align); \
- __start_init_task = .; \
- init_thread_union = .; \
- init_stack = .; \
- KEEP(*(.data..init_task)) \
- KEEP(*(.data..init_thread_info)) \
- . = __start_init_task + THREAD_SIZE; \
- __end_init_task = .;
可以看出,__start_init_task 是0號進(jìn)程的內(nèi)核棧的基地址,__end_init_task 是0號進(jìn)程的內(nèi)核棧的結(jié)束地址。注意:__start_init_task = init_thread_union = init_task
- .comm = INIT_TASK_COMM, 0號進(jìn)程的名稱是 swapper
下面結(jié)合 Linux 內(nèi)核啟動的部分代碼,看下是如何調(diào)用 __primary_switched 來設(shè)置0號進(jìn)程的運(yùn)行內(nèi)核棧:
- /* arch/arm64/kernel/head.S */
- SYM_FUNC_START_LOCAL(__primary_switched)
- adrp x4, init_thread_union ------(1)
- add sp, x4, #THREAD_SIZE ------(2)
- adr_l x5, init_task
- msr sp_el0, x5 // Save thread_info
- ......
- b start_kernel
- SYM_FUNC_END(__primary_switched) ------(3)
- init_thread_union 是0號進(jìn)程的內(nèi)核棧的基地址
- 設(shè)置堆棧指針 sp 的值,就是內(nèi)核棧的棧底 + THREAD_SIZE的大小。現(xiàn)在 sp 指到了內(nèi)核棧的頂端
- 跳轉(zhuǎn)到 linux 內(nèi)核的入口
至此0號進(jìn)程就已經(jīng)運(yùn)行起來了,0號進(jìn)程,通常也被稱為 idle 進(jìn)程,也稱為 swapper 進(jìn)程。當(dāng)系統(tǒng)中所有的進(jìn)程起來后,0號進(jìn)程也就蛻化為 idle 進(jìn)程,當(dāng)一個 CPU 上沒有任務(wù)可運(yùn)行時就會去運(yùn)行 idle 進(jìn)程。一旦運(yùn)行 idle 進(jìn)程,則此 CPU 就可以進(jìn)入低功耗模式了,在ARM上就是WFI。
1號線程
- asmlinkage __visible void __init __no_sanitize_address start_kernel(void)
- {
- ......
- arch_call_rest_init();
- ......
- }
- void __init __weak arch_call_rest_init(void)
- {
- rest_init();
- }
- noinline void __ref rest_init(void)
- {
- struct task_struct *tsk;
- int pid;
- rcu_scheduler_starting();
- pid = kernel_thread(kernel_init, NULL, CLONE_FS);
- rcu_read_lock();
- tsk = find_task_by_pid_ns(pid, &init_pid_ns);
- set_cpus_allowed_ptr(tsk, cpumask_of(smp_processor_id()));
- rcu_read_unlock();
- numa_default_policy();
- pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);
- rcu_read_lock();
- kthreadd_task = find_task_by_pid_ns(pid, &init_pid_ns);
- rcu_read_unlock();
- system_state = SYSTEM_SCHEDULING;
- complete(&kthreadd_done);
- schedule_preempt_disabled();
- /* Call into cpu_idle with preempt disabled */
- cpu_startup_entry(CPUHP_ONLINE);
- }
這里會創(chuàng)建1號,2號兩個線程:
- pid = kernel_thread(kernel_init, NULL, CLONE_FS);
- pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);
- 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);
- }
可以看出,kernel_thread 最終會調(diào)用 do_fork 根據(jù)參數(shù)的不同來創(chuàng)建一個進(jìn)程或者內(nèi)核線程。do_fork 的實(shí)現(xiàn)我們在后面會做詳細(xì)的介紹。當(dāng)內(nèi)核線程創(chuàng)建成功后就會調(diào)用設(shè)置的回調(diào)函數(shù)。
當(dāng) kernel_thread(kernel_init, NULL, CLONE_FS) 返回時,1號進(jìn)程已經(jīng)創(chuàng)建成功了。而且會回調(diào) kernel_init 函數(shù),接下來看下 kernel_init 主要做什么事情:
- static int __ref kernel_init(void *unused)
- {
- int ret;
- kernel_init_freeable();
- ......
- if (!try_to_run_init_process("/sbin/init") ||
- !try_to_run_init_process("/etc/init") ||
- !try_to_run_init_process("/bin/init") ||
- !try_to_run_init_process("/bin/sh"))
- return 0;
- panic("No working init found. Try passing init= option to kernel. "
- "See Linux Documentation/admin-guide/init.rst for guidance.");
- }
最主要的工作就是通過 execve,執(zhí)行init可執(zhí)行文件。init 就是1號線程,它最終會去創(chuàng)建所有的應(yīng)用進(jìn)程。確切來講,init 進(jìn)程是用戶態(tài)的,kernel_init 是1號進(jìn)程的內(nèi)核態(tài)。
2號線程
上面講到的 kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES) 就是用來創(chuàng)建2號線程,2號線程的執(zhí)行函數(shù)是 kthreadd:
kthreadd 處理流程
- int kthreadd(void *unused)
- {
- struct task_struct *tsk = current;
- /* Setup a clean context for our children to inherit. */
- set_task_comm(tsk, "kthreadd"); ------(1)
- ignore_signals(tsk);
- set_cpus_allowed_ptr(tsk, housekeeping_cpumask(HK_FLAG_KTHREAD));
- set_mems_allowed(node_states[N_MEMORY]);
- current->flags |= PF_NOFREEZE;
- cgroup_init_kthreadd();
- for (;;) {
- set_current_state(TASK_INTERRUPTIBLE); ------(2)
- if (list_empty(&kthread_create_list))
- schedule(); ------(3)
- __set_current_state(TASK_RUNNING);
- spin_lock(&kthread_create_lock);
- while (!list_empty(&kthread_create_list)) {
- struct kthread_create_info *create;
- create = list_entry(kthread_create_list.next,
- struct kthread_create_info, list);
- list_del_init(&create->list);
- spin_unlock(&kthread_create_lock);
- create_kthread(create); ------(4)
- spin_lock(&kthread_create_lock);
- }
- spin_unlock(&kthread_create_lock);
- }
- return 0;
- }
- 通過設(shè)置 task_struct 的 comm 字段,使當(dāng)前進(jìn)程的名字為"kthreadd"
- 設(shè)置當(dāng)前的進(jìn)程的狀態(tài)是 TASK_INTERRUPTIBLE
- 如果鏈表 kthread_create_list 是空,說明沒有創(chuàng)建內(nèi)核線程的請求,則直接調(diào)用 schedule 進(jìn)行睡眠
- 如果不是空,while循環(huán),從鏈表中取出一個,然后調(diào)用 create_kthread 去創(chuàng)建一個內(nèi)核線程
所以2號線程 kthreadd 通過 create_kthread 去創(chuàng)建內(nèi)核其它的線程,可謂是內(nèi)核線程的祖先。
至此,我們已經(jīng)知道 Linux 啟動的第一個線程,0號線程是靜態(tài)創(chuàng)建的。在0號線程啟動后會接連創(chuàng)建兩個線程,分別是1號線程和2和線程。1號進(jìn)程最終會去調(diào)用可init可執(zhí)行文件,init進(jìn)程最終會去創(chuàng)建所有的應(yīng)用進(jìn)程。2號進(jìn)程會在內(nèi)核中負(fù)責(zé)創(chuàng)建所有的內(nèi)核線程。所以說0號進(jìn)程是1號和2號進(jìn)程的父進(jìn)程,1號進(jìn)程是所有用戶態(tài)進(jìn)程的父進(jìn)程,2號進(jìn)程是所有內(nèi)核線程的父進(jìn)程。
kthread 處理流程
上面 kthreadd 線程會循環(huán)查看鏈表 kthread_create_list,如果有線程的創(chuàng)建申請,則從鏈表中取出一個,然后調(diào)用 create_kthread 去創(chuàng)建一個內(nèi)核線程。
- static void create_kthread(struct kthread_create_info *create)
- {
- int pid;
- #ifdef CONFIG_NUMA
- current->pref_node_fork = create->node;
- #endif
- /* We want our own signal handler (we take no signals by default). */
- pid = kernel_thread(kthread, create, CLONE_FS | CLONE_FILES | SIGCHLD);
- if (pid < 0) {
- /* If user was SIGKILLed, I release the structure. */
- struct completion *done = xchg(&create->done, NULL);
- if (!done) {
- kfree(create);
- return;
- }
- create->result = ERR_PTR(pid);
- complete(done);
- }
- }
可以看出,由 kthreadd 內(nèi)核線程創(chuàng)建的內(nèi)核線程的執(zhí)行函數(shù)是 kthread。
- static int kthread(void *_create)
- {
- /* Copy data: it's on kthread's stack */
- struct kthread_create_info *create = _create; ------(1)
- int (*threadfn)(void *data) = create->threadfn; ------(2)
- void *data = create->data; ------(3)
- struct completion *done;
- struct kthread *self;
- int ret;
- self = kzalloc(sizeof(*self), GFP_KERNEL); ------(4)
- set_kthread_struct(self);
- /* If user was SIGKILLed, I release the structure. */
- done = xchg(&create->done, NULL); ------(5)
- if (!done) {
- kfree(create);
- do_exit(-EINTR);
- }
- if (!self) {
- create->result = ERR_PTR(-ENOMEM);
- complete(done);
- do_exit(-ENOMEM);
- }
- self->threadfn = threadfn; ------(6)
- self->data = data; ------(7)
- init_completion(&self->exited);
- init_completion(&self->parked);
- current->vfork_done = &self->exited;
- /* OK, tell user we're spawned, wait for stop or wakeup */
- __set_current_state(TASK_UNINTERRUPTIBLE); ------(8)
- create->result = current; ------(9)
- /*
- * Thread is going to call schedule(), do not preempt it,
- * or the creator may spend more time in wait_task_inactive().
- */
- preempt_disable();
- complete(done); ------(10)
- schedule_preempt_disabled(); ------(11)
- preempt_enable(); ------(12)
- ret = -EINTR;
- if (!test_bit(KTHREAD_SHOULD_STOP, &self->flags)) {------(13)
- cgroup_kthread_ready();
- __kthread_parkme(self);
- ret = threadfn(data); ------(14)
- }
- do_exit(ret); ------(15)
- }
- 取出傳遞過來的線程創(chuàng)建信息
- 取出線程執(zhí)行函數(shù)
- 取出傳遞給線程執(zhí)行函數(shù)的參數(shù)
- 分配 kthread 結(jié)構(gòu)
- 獲得 done 完成量
- 賦值 self->threadfn 為線程執(zhí)行函數(shù)
- 賦值 self->data 為線程執(zhí)行函數(shù)的參數(shù)
- 設(shè)置內(nèi)核線程狀態(tài)為 TASK_UNINTERRUPTIBLE,但此時還沒有睡眠
- 用于返回當(dāng)前任務(wù)的 tsk
- 喚醒等待 done 完成量的任務(wù)
- 睡眠
- 喚醒的時候從此開始執(zhí)行
- 判斷 self->flags 是否為 KTHREAD_SHOULD_STOP (kthread_stop 會設(shè)置)
- 執(zhí)行真正的線程執(zhí)行函數(shù)
- 退出當(dāng)前任務(wù)
內(nèi)核線程的創(chuàng)建和運(yùn)行
現(xiàn)在我們知道 kthreadd 會從鏈表 kthread_create_list 中取出一個,然后調(diào)用 create_kthread 去創(chuàng)建一個內(nèi)核線程。kthreadd 是所有內(nèi)核線程的父線程,但是子線程如何把請求加入 kthread_create_list 鏈表,如何讓子線程運(yùn)行,還沒有深入介紹。
這里舉例看一個 peter 線程的創(chuàng)建和運(yùn)行的簡單例子:
- int my_kernel_thread(void *arg)
- {
- printk("%s: %d\n", __func__);
- return 0;
- }
- static int __init test_init_module(void)
- {
- printk("%s:\n", __func__);
- peter = kthread_create(my_kernel_thread, NULL, "practice task"); ------(1)
- if(!IS_ERR(peter))
- wake_up_process(peter); ------(2)
- return 0;
- }
- static void __exit test_exit_module(void)
- {
- printk("%s:\n", __func__);
- kthread_stop(peter);
- }
- module_init(test_init_module);
- module_exit(test_exit_module);
很簡單,通過 kthread_create 函數(shù)創(chuàng)建內(nèi)核線程,然后通過 wake_up_process 喚醒線程,使之運(yùn)行。
下面我們結(jié)合上面的 kthreadd,剖析下內(nèi)核線程創(chuàng)建和運(yùn)行的本質(zhì)。
kthread_create
kthread_create 的調(diào)用流程是:kthread_create->kthread_create_on_node->__kthread_create_on_node
- struct task_struct *__kthread_create_on_node(int (*threadfn)(void *data),
- void *data, int node,
- const char namefmt[],
- va_list args)
- {
- DECLARE_COMPLETION_ONSTACK(done); ------(1)
- struct task_struct *task;
- struct kthread_create_info *create = kmalloc(sizeof(*create),
- GFP_KERNEL); ------(2)
- if (!create)
- return ERR_PTR(-ENOMEM);
- create->threadfn = threadfn; ------(3)
- create->data = data;
- create->node = node;
- create->done = &done;
- spin_lock(&kthread_create_lock);
- list_add_tail(&create->list, &kthread_create_list); ------(4)
- spin_unlock(&kthread_create_lock);
- wake_up_process(kthreadd_task); ------(5)
- /*
- * Wait for completion in killable state, for I might be chosen by
- * the OOM killer while kthreadd is trying to allocate memory for
- * new kernel thread.
- */
- if (unlikely(wait_for_completion_killable(&done))) { ------(6)
- /*
- * If I was SIGKILLed before kthreadd (or new kernel thread)
- * calls complete(), leave the cleanup of this structure to
- * that thread.
- */
- if (xchg(&create->done, NULL))
- return ERR_PTR(-EINTR);
- /*
- * kthreadd (or new kernel thread) will call complete()
- * shortly.
- */
- wait_for_completion(&done);
- }
- task = create->result; ------(7)
- if (!IS_ERR(task)) {
- static const struct sched_param param = { .sched_priority = 0 };
- char name[TASK_COMM_LEN];
- /*
- * task is already visible to other tasks, so updating
- * COMM must be protected.
- */
- vsnprintf(name, sizeof(name), namefmt, args);
- set_task_comm(task, name); ------(8)
- /*
- * root may have changed our (kthreadd's) priority or CPU mask.
- * The kernel thread should not inherit these properties.
- */
- sched_setscheduler_nocheck(task, SCHED_NORMAL, ¶m); ------(9)
- set_cpus_allowed_ptr(task, ------(10)
- housekeeping_cpumask(HK_FLAG_KTHREAD));
- }
- kfree(create);
- return task;
- }
- 靜態(tài)定義并初始化一個完成量
- 分配 kthread_create_info 結(jié)構(gòu)
- 填充 kthread_create_info 結(jié)構(gòu)
- 將 kthread_create_info 結(jié)構(gòu)添加到 kthread_create_list 鏈表
- 喚醒 kthreadd 來處理創(chuàng)建內(nèi)核線程請求
- 等待 kthreadd 創(chuàng)建完成這個內(nèi)核線程
- 獲得創(chuàng)建完成的內(nèi)核線程的 tsk
- 設(shè)置內(nèi)核線程的名字
- 設(shè)置調(diào)度策略和優(yōu)先級
- 設(shè)置 CPU 親和性
wake_up_process
上面通過 kthread_create 分配填充 kthread_create_info 結(jié)構(gòu),然后將該結(jié)構(gòu)添加到 kthread_create_list 鏈表,喚醒 kthreadd 去創(chuàng)建 peter 線程,然后調(diào)用 schedule_preempt_disabled 使 peter 線程睡眠。等待被 wake_up_process 喚醒,一旦執(zhí)行 wake_up_process,則喚醒 peter 線程,去調(diào)用它的執(zhí)行函數(shù) threadfn(data)。
為了更好理解,這里用一張圖來總結(jié)父線程 kthreadd 和其子線程 peter 的關(guān)系: