上面讲完了用户进程/线程的创建,这里我们看下内核是如何创建线程的。
通过 ps 命令可以看到红色方框标出的都是父进程为2号进程的内核线程,2号进程即蓝色方框标出的进程 kthreadd,1号进程是绿色方框标出的进程 init,它们的父进程号都是0。
下面我们一起看下,内核的0号,1号,2号线程的创建过程。
0号线程
linux 内核中为0号进程专门定义了一个静态的 task_struct 的结构,称为 init_task:
-
- #define INIT_TASK_COMM "swapper"
-
-
- 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);
这个结构体中的成员都是静态定义的,这里看几个比较重要的变量:
- .thread_info = INIT_THREAD_INFO(init_task), 这个结构在 “task_struct, thread_info 和内核栈 sp 的关系” 中有详细的描述
- .stack = init_stack, init_stack 是内核栈的静态定义,定义在链接脚本里
-
- #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号进程的内核栈的基地址,__end_init_task 是0号进程的内核栈的结束地址。注意:__start_init_task = init_thread_union = init_task
- .comm = INIT_TASK_COMM, 0号进程的名称是 swapper
下面结合 Linux 内核启动的部分代码,看下是如何调用 __primary_switched 来设置0号进程的运行内核栈:
-
- 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号进程的内核栈的基地址
- 设置堆栈指针 sp 的值,就是内核栈的栈底 + THREAD_SIZE的大小。现在 sp 指到了内核栈的顶端
- 跳转到 linux 内核的入口
至此0号进程就已经运行起来了,0号进程,通常也被称为 idle 进程,也称为 swapper 进程。当系统中所有的进程起来后,0号进程也就蜕化为 idle 进程,当一个 CPU 上没有任务可运行时就会去运行 idle 进程。一旦运行 idle 进程,则此 CPU 就可以进入低功耗模式了,在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();
-
- cpu_startup_entry(CPUHP_ONLINE);
- }
这里会创建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 最终会调用 do_fork 根据参数的不同来创建一个进程或者内核线程。do_fork 的实现我们在后面会做详细的介绍。当内核线程创建成功后就会调用设置的回调函数。
当 kernel_thread(kernel_init, NULL, CLONE_FS) 返回时,1号进程已经创建成功了。而且会回调 kernel_init 函数,接下来看下 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,执行init可执行文件。init 就是1号线程,它最终会去创建所有的应用进程。确切来讲,init 进程是用户态的,kernel_init 是1号进程的内核态。
2号线程
上面讲到的 kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES) 就是用来创建2号线程,2号线程的执行函数是 kthreadd:
kthreadd 处理流程
- int kthreadd(void *unused)
- {
- struct task_struct *tsk = current;
-
-
- 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;
- }
- 通过设置 task_struct 的 comm 字段,使当前进程的名字为"kthreadd"
- 设置当前的进程的状态是 TASK_INTERRUPTIBLE
- 如果链表 kthread_create_list 是空,说明没有创建内核线程的请求,则直接调用 schedule 进行睡眠
- 如果不是空,while循环,从链表中取出一个,然后调用 create_kthread 去创建一个内核线程
所以2号线程 kthreadd 通过 create_kthread 去创建内核其它的线程,可谓是内核线程的祖先。
至此,我们已经知道 Linux 启动的第一个线程,0号线程是静态创建的。在0号线程启动后会接连创建两个线程,分别是1号线程和2和线程。1号进程最终会去调用可init可执行文件,init进程最终会去创建所有的应用进程。2号进程会在内核中负责创建所有的内核线程。所以说0号进程是1号和2号进程的父进程,1号进程是所有用户态进程的父进程,2号进程是所有内核线程的父进程。
kthread 处理流程
上面 kthreadd 线程会循环查看链表 kthread_create_list,如果有线程的创建申请,则从链表中取出一个,然后调用 create_kthread 去创建一个内核线程。
- static void create_kthread(struct kthread_create_info *create)
- {
- int pid;
-
- #ifdef CONFIG_NUMA
- current->pref_node_fork = create->node;
- #endif
-
- pid = kernel_thread(kthread, create, CLONE_FS | CLONE_FILES | SIGCHLD);
- if (pid < 0) {
-
- struct completion *done = xchg(&create->done, NULL);
-
- if (!done) {
- kfree(create);
- return;
- }
- create->result = ERR_PTR(pid);
- complete(done);
- }
- }
可以看出,由 kthreadd 内核线程创建的内核线程的执行函数是 kthread。
- static int kthread(void *_create)
- {
-
- 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);
-
-
- 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;
-
-
- __set_current_state(TASK_UNINTERRUPTIBLE); ------(8)
- create->result = current; ------(9)
-
- 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)
- }
- 取出传递过来的线程创建信息
- 取出线程执行函数
- 取出传递给线程执行函数的参数
- 分配 kthread 结构
- 获得 done 完成量
- 赋值 self->threadfn 为线程执行函数
- 赋值 self->data 为线程执行函数的参数
- 设置内核线程状态为 TASK_UNINTERRUPTIBLE,但此时还没有睡眠
- 用于返回当前任务的 tsk
- 唤醒等待 done 完成量的任务
- 睡眠
- 唤醒的时候从此开始执行
- 判断 self->flags 是否为 KTHREAD_SHOULD_STOP (kthread_stop 会设置)
- 执行真正的线程执行函数
- 退出当前任务
内核线程的创建和运行
现在我们知道 kthreadd 会从链表 kthread_create_list 中取出一个,然后调用 create_kthread 去创建一个内核线程。kthreadd 是所有内核线程的父线程,但是子线程如何把请求加入 kthread_create_list 链表,如何让子线程运行,还没有深入介绍。
这里举例看一个 peter 线程的创建和运行的简单例子:
- 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 函数创建内核线程,然后通过 wake_up_process 唤醒线程,使之运行。
下面我们结合上面的 kthreadd,剖析下内核线程创建和运行的本质。
kthread_create
kthread_create 的调用流程是: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)
-
- if (unlikely(wait_for_completion_killable(&done))) { ------(6)
-
- if (xchg(&create->done, NULL))
- return ERR_PTR(-EINTR);
-
- 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];
-
-
- vsnprintf(name, sizeof(name), namefmt, args);
- set_task_comm(task, name); ------(8)
-
- sched_setscheduler_nocheck(task, SCHED_NORMAL, ¶m); ------(9)
- set_cpus_allowed_ptr(task, ------(10)
- housekeeping_cpumask(HK_FLAG_KTHREAD));
- }
- kfree(create);
- return task;
- }
- 静态定义并初始化一个完成量
- 分配 kthread_create_info 结构
- 填充 kthread_create_info 结构
- 将 kthread_create_info 结构添加到 kthread_create_list 链表
- 唤醒 kthreadd 来处理创建内核线程请求
- 等待 kthreadd 创建完成这个内核线程
- 获得创建完成的内核线程的 tsk
- 设置内核线程的名字
- 设置调度策略和优先级
- 设置 CPU 亲和性
wake_up_process
上面通过 kthread_create 分配填充 kthread_create_info 结构,然后将该结构添加到 kthread_create_list 链表,唤醒 kthreadd 去创建 peter 线程,然后调用 schedule_preempt_disabled 使 peter 线程睡眠。等待被 wake_up_process 唤醒,一旦执行 wake_up_process,则唤醒 peter 线程,去调用它的执行函数 threadfn(data)。
为了更好理解,这里用一张图来总结父线程 kthreadd 和其子线程 peter 的关系: