快捷搜索:  汽车  科技

内核启动的第一个线程是啥:一篇搞懂内核线程的创建和运行

内核启动的第一个线程是啥:一篇搞懂内核线程的创建和运行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();

这里我们看下内核是如何创建线程的。

通过 ps 命令可以看到红色方框标出的都是父进程为2号进程的内核线程,2号进程即蓝色方框标出的进程 kthreadd,1号进程是绿色方框标出的进程 init,它们的父进程号都是0。

内核启动的第一个线程是啥:一篇搞懂内核线程的创建和运行(1)

下面我们一起看下,内核的0号,1号,2号线程的创建过程。

0号线程

linux 内核中为0号进程专门定义了一个静态的 task_struct 的结构,称为 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);

这个结构体中的成员都是静态定义的,这里看几个比较重要的变量:

  • .thread_info = INIT_THREAD_INFO(init_task) 这个结构在 “task_struct thread_info 和内核栈 sp 的关系” 中有详细的描述
  • .stack = init_stack init_stack 是内核栈的静态定义,定义在链接脚本里

/* 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号进程的内核栈的基地址,__end_init_task 是0号进程的内核栈的结束地址。注意:__start_init_task = init_thread_union = init_task

  • .comm = INIT_TASK_COMM 0号进程的名称是 swapper

下面结合 Linux 内核启动的部分代码,看下是如何调用 __primary_switched 来设置0号进程的运行内核栈:

/* 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)

  1. init_thread_union 是0号进程的内核栈的基地址
  2. 设置堆栈指针 sp 的值,就是内核栈的栈底 THREAD_SIZE的大小。现在 sp 指到了内核栈的顶端
  3. 跳转到 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(); /* Call into cpu_idle with 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号进程的内核态。

更多linux内核视频教程文档资料免费领取后台私信【内核】自行获取.

内核启动的第一个线程是啥:一篇搞懂内核线程的创建和运行(2)

内核启动的第一个线程是啥:一篇搞懂内核线程的创建和运行(3)

Linux内核源码/内存调优/文件系统/进程管理/设备驱动/网络协议栈-学习视频教程-腾讯课堂

2号线程

上面讲到的 kernel_thread(kthreadd NULL CLONE_FS | CLONE_FILES) 就是用来创建2号线程,2号线程的执行函数是 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; }

  1. 通过设置 task_struct 的 comm 字段,使当前进程的名字为"kthreadd"
  2. 设置当前的进程的状态是 TASK_INTERRUPTIBLE
  3. 如果链表 kthread_create_list 是空,说明没有创建内核线程的请求,则直接调用 schedule 进行睡眠
  4. 如果不是空,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 /* 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 内核线程创建的内核线程的执行函数是 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) }

  1. 取出传递过来的线程创建信息
  2. 取出线程执行函数
  3. 取出传递给线程执行函数的参数
  4. 分配 kthread 结构
  5. 获得 done 完成量
  6. 赋值 self->threadfn 为线程执行函数
  7. 赋值 self->data 为线程执行函数的参数
  8. 设置内核线程状态为 TASK_UNINTERRUPTIBLE,但此时还没有睡眠
  9. 用于返回当前任务的 tsk
  10. 唤醒等待 done 完成量的任务
  11. 睡眠
  12. 唤醒的时候从此开始执行
  13. 判断 self->flags 是否为 KTHREAD_SHOULD_STOP (kthread_stop 会设置)
  14. 执行真正的线程执行函数
  15. 退出当前任务
内核线程的创建和运行

现在我们知道 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) /* * 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; }

  1. 静态定义并初始化一个完成量
  2. 分配 kthread_create_info 结构
  3. 填充 kthread_create_info 结构
  4. 将 kthread_create_info 结构添加到 kthread_create_list 链表
  5. 唤醒 kthreadd 来处理创建内核线程请求
  6. 等待 kthreadd 创建完成这个内核线程
  7. 获得创建完成的内核线程的 tsk
  8. 设置内核线程的名字
  9. 设置调度策略和优先级
  10. 设置 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 的关系:

内核启动的第一个线程是啥:一篇搞懂内核线程的创建和运行(4)

内核启动的第一个线程是啥:一篇搞懂内核线程的创建和运行(5)

猜您喜欢: