一篇学会内核线程的创建和运行

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

[[407481]]

上面讲完了用户进程/线程的创建,这里我们看下内核是如何创建线程的。

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

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

0号线程

linux 内核中为0号进程专门定义了一个静态的 task_struct 的结构,称为 init_task:

  1. /* include/linux/init_task.h */ 
  2. #define INIT_TASK_COMM "swapper" 
  3.  
  4. /* init/init_task.c */ 
  5. struct task_struct init_task 
  6. #ifdef CONFIG_ARCH_TASK_STRUCT_ON_STACK 
  7.         __init_task_data 
  8. #endif 
  9.         __aligned(L1_CACHE_BYTES) 
  10. = { 
  11. #ifdef CONFIG_THREAD_INFO_IN_TASK 
  12.         .thread_info    = INIT_THREAD_INFO(init_task), 
  13.         .stack_refcount = REFCOUNT_INIT(1), 
  14. #endif 
  15.         .state          = 0, 
  16.         .stack          = init_stack, 
  17.         .usage          = REFCOUNT_INIT(2), 
  18.         .flags          = PF_KTHREAD, 
  19.         .prio           = MAX_PRIO - 20, 
  20.         .static_prio    = MAX_PRIO - 20, 
  21.         .normal_prio    = MAX_PRIO - 20, 
  22.         .policy         = SCHED_NORMAL, 
  23.         .cpus_ptr       = &init_task.cpus_mask, 
  24.         .cpus_mask      = CPU_MASK_ALL, 
  25.         .nr_cpus_allowed= NR_CPUS, 
  26.         .mm             = NULL
  27.         .active_mm      = &init_mm, 
  28.   ...... 
  29.         .comm           = INIT_TASK_COMM, 
  30.         .thread         = INIT_THREAD, 
  31.         .fs             = &init_fs, 
  32.         .files          = &init_files, 
  33.   ...... 
  34. }; 
  35. EXPORT_SYMBOL(init_task); 

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

  • .thread_info = INIT_THREAD_INFO(init_task), 这个结构在 “task_struct, thread_info 和内核栈 sp 的关系” 中有详细的描述
  • .stack = init_stack, init_stack 是内核栈的静态定义,定义在链接脚本里
  1. /* include/asm-generic/vmlinux.lds.h */ 
  2. #define INIT_TASK_DATA(align)                                           \ 
  3.         . = ALIGN(align);                                               \ 
  4.         __start_init_task = .;                                          \ 
  5.         init_thread_union = .;                                          \ 
  6.         init_stack = .;                                                 \ 
  7.         KEEP(*(.data..init_task))                                       \ 
  8.         KEEP(*(.data..init_thread_info))                                \ 
  9.         . = __start_init_task + THREAD_SIZE;                            \ 
  10.         __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号进程的运行内核栈:

  1. /* arch/arm64/kernel/head.S */ 
  2. SYM_FUNC_START_LOCAL(__primary_switched) 
  3.         adrp    x4, init_thread_union        ------(1) 
  4.         add     sp, x4, #THREAD_SIZE         ------(2) 
  5.         adr_l   x5, init_task 
  6.         msr     sp_el0, x5                      // Save thread_info 
  7.         ...... 
  8.         b       start_kernel 
  9. 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号线程

  1. asmlinkage __visible void __init __no_sanitize_address start_kernel(void) 
  2.   ...... 
  3.   arch_call_rest_init(); 
  4.   ...... 
  5.  
  6. void __init __weak arch_call_rest_init(void) 
  7.         rest_init(); 
  8.  
  9. noinline void __ref rest_init(void) 
  10.         struct task_struct *tsk; 
  11.         int pid; 
  12.  
  13.         rcu_scheduler_starting(); 
  14.  
  15.         pid = kernel_thread(kernel_init, NULL, CLONE_FS); 
  16.  
  17.         rcu_read_lock(); 
  18.         tsk = find_task_by_pid_ns(pid, &init_pid_ns); 
  19.         set_cpus_allowed_ptr(tsk, cpumask_of(smp_processor_id())); 
  20.         rcu_read_unlock(); 
  21.  
  22.         numa_default_policy(); 
  23.         pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES); 
  24.         rcu_read_lock(); 
  25.         kthreadd_task = find_task_by_pid_ns(pid, &init_pid_ns); 
  26.         rcu_read_unlock(); 
  27.  
  28.         system_state = SYSTEM_SCHEDULING; 
  29.  
  30.         complete(&kthreadd_done); 
  31.  
  32.         schedule_preempt_disabled(); 
  33.         /* Call into cpu_idle with preempt disabled */ 
  34.         cpu_startup_entry(CPUHP_ONLINE); 

这里会创建1号,2号两个线程:

  • pid = kernel_thread(kernel_init, NULL, CLONE_FS);
  • pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);
  1. pid_t kernel_thread(int (*fn)(void *), void *arg, unsigned long flags) 
  2.  return _do_fork(flags|CLONE_VM|CLONE_UNTRACED, (unsigned long)fn, 
  3.   (unsigned long)arg, NULLNULL, 0); 

可以看出,kernel_thread 最终会调用 do_fork 根据参数的不同来创建一个进程或者内核线程。do_fork 的实现我们在后面会做详细的介绍。当内核线程创建成功后就会调用设置的回调函数。

当 kernel_thread(kernel_init, NULL, CLONE_FS) 返回时,1号进程已经创建成功了。而且会回调 kernel_init 函数,接下来看下 kernel_init 主要做什么事情:

  1. static int __ref kernel_init(void *unused) 
  2.         int ret; 
  3.  
  4.         kernel_init_freeable(); 
  5.         ...... 
  6.         if (!try_to_run_init_process("/sbin/init") || 
  7.             !try_to_run_init_process("/etc/init") || 
  8.             !try_to_run_init_process("/bin/init") || 
  9.             !try_to_run_init_process("/bin/sh")) 
  10.                 return 0; 
  11.  
  12.         panic("No working init found.  Try passing init= option to kernel. " 
  13.               "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 处理流程

  1. int kthreadd(void *unused) 
  2.         struct task_struct *tsk = current
  3.  
  4.         /* Setup a clean context for our children to inherit. */ 
  5.         set_task_comm(tsk, "kthreadd");                 ------(1) 
  6.         ignore_signals(tsk); 
  7.         set_cpus_allowed_ptr(tsk, housekeeping_cpumask(HK_FLAG_KTHREAD)); 
  8.         set_mems_allowed(node_states[N_MEMORY]); 
  9.  
  10.         current->flags |= PF_NOFREEZE; 
  11.         cgroup_init_kthreadd(); 
  12.  
  13.         for (;;) { 
  14.                 set_current_state(TASK_INTERRUPTIBLE);  ------(2) 
  15.                 if (list_empty(&kthread_create_list))    
  16.                         schedule();                     ------(3) 
  17.                 __set_current_state(TASK_RUNNING); 
  18.  
  19.                 spin_lock(&kthread_create_lock); 
  20.                 while (!list_empty(&kthread_create_list)) { 
  21.                         struct kthread_create_info *create
  22.  
  23.                         create = list_entry(kthread_create_list.next
  24.                                             struct kthread_create_info, list); 
  25.                         list_del_init(&create->list); 
  26.                         spin_unlock(&kthread_create_lock); 
  27.  
  28.                         create_kthread(create);         ------(4) 
  29.  
  30.                         spin_lock(&kthread_create_lock); 
  31.                 } 
  32.                 spin_unlock(&kthread_create_lock); 
  33.         } 
  34.  
  35.         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 去创建一个内核线程。

  1. static void create_kthread(struct kthread_create_info *create
  2.         int pid; 
  3.  
  4. #ifdef CONFIG_NUMA 
  5.         current->pref_node_fork = create->node; 
  6. #endif 
  7.         /* We want our own signal handler (we take no signals by default). */ 
  8.         pid = kernel_thread(kthread, create, CLONE_FS | CLONE_FILES | SIGCHLD); 
  9.         if (pid < 0) { 
  10.                 /* If user was SIGKILLed, I release the structure. */ 
  11.                 struct completion *done = xchg(&create->done, NULL); 
  12.  
  13.                 if (!done) { 
  14.                         kfree(create); 
  15.                         return
  16.                 } 
  17.                 create->result = ERR_PTR(pid); 
  18.                 complete(done); 
  19.         } 

可以看出,由 kthreadd 内核线程创建的内核线程的执行函数是 kthread。

  1. static int kthread(void *_create) 
  2.         /* Copy data: it's on kthread's stack */ 
  3.         struct kthread_create_info *create = _create;      ------(1) 
  4.         int (*threadfn)(void *data) = create->threadfn;    ------(2) 
  5.         void *data = create->data;                         ------(3) 
  6.         struct completion *done; 
  7.         struct kthread *self; 
  8.         int ret; 
  9.  
  10.         self = kzalloc(sizeof(*self), GFP_KERNEL);         ------(4) 
  11.         set_kthread_struct(self); 
  12.  
  13.         /* If user was SIGKILLed, I release the structure. */ 
  14.         done = xchg(&create->done, NULL);                  ------(5) 
  15.         if (!done) { 
  16.                 kfree(create); 
  17.                 do_exit(-EINTR); 
  18.         } 
  19.  
  20.         if (!self) { 
  21.                 create->result = ERR_PTR(-ENOMEM); 
  22.                 complete(done); 
  23.                 do_exit(-ENOMEM); 
  24.         } 
  25.  
  26.         self->threadfn = threadfn;                         ------(6) 
  27.         self->data = data;                                 ------(7) 
  28.         init_completion(&self->exited); 
  29.         init_completion(&self->parked); 
  30.         current->vfork_done = &self->exited; 
  31.  
  32.         /* OK, tell user we're spawned, wait for stop or wakeup */ 
  33.         __set_current_state(TASK_UNINTERRUPTIBLE);         ------(8) 
  34.         create->result = current;                          ------(9) 
  35.         /* 
  36.          * Thread is going to call schedule(), do not preempt it, 
  37.          * or the creator may spend more time in wait_task_inactive(). 
  38.          */ 
  39.         preempt_disable(); 
  40.         complete(done);                                    ------(10) 
  41.         schedule_preempt_disabled();                       ------(11) 
  42.         preempt_enable();                                  ------(12) 
  43.  
  44.         ret = -EINTR; 
  45.         if (!test_bit(KTHREAD_SHOULD_STOP, &self->flags)) {------(13) 
  46.                 cgroup_kthread_ready(); 
  47.                 __kthread_parkme(self); 
  48.                 ret = threadfn(data);                      ------(14) 
  49.         } 
  50.         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 线程的创建和运行的简单例子:

  1. int my_kernel_thread(void *arg)   
  2. {   
  3.  printk("%s: %d\n", __func__);   
  4.   
  5.  return 0;   
  6. }   
  7. static int __init test_init_module(void)   
  8. {   
  9.  printk("%s:\n", __func__);   
  10.   
  11.  peter = kthread_create(my_kernel_thread, NULL"practice task");  ------(1) 
  12.   
  13.  if(!IS_ERR(peter))   
  14.   wake_up_process(peter);                                          ------(2) 
  15.   
  16.  return 0;   
  17. }   
  18.    
  19. static void __exit test_exit_module(void)   
  20. {   
  21.  printk("%s:\n", __func__);   
  22.  kthread_stop(peter);   
  23. }   
  24.    
  25. module_init(test_init_module);   
  26. 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

  1. struct task_struct *__kthread_create_on_node(int (*threadfn)(void *data), 
  2.                                                     void *data, int node, 
  3.                                                     const char namefmt[], 
  4.                                                     va_list args) 
  5.         DECLARE_COMPLETION_ONSTACK(done);                               ------(1) 
  6.         struct task_struct *task; 
  7.         struct kthread_create_info *create = kmalloc(sizeof(*create), 
  8.                                                      GFP_KERNEL);       ------(2) 
  9.  
  10.         if (!create
  11.                 return ERR_PTR(-ENOMEM); 
  12.         create->threadfn = threadfn;                                    ------(3) 
  13.         create->data = data; 
  14.         create->node = node; 
  15.         create->done = &done; 
  16.  
  17.         spin_lock(&kthread_create_lock); 
  18.         list_add_tail(&create->list, &kthread_create_list);             ------(4) 
  19.         spin_unlock(&kthread_create_lock); 
  20.  
  21.         wake_up_process(kthreadd_task);                                 ------(5) 
  22.         /* 
  23.          * Wait for completion in killable state, for I might be chosen by 
  24.          * the OOM killer while kthreadd is trying to allocate memory for 
  25.          * new kernel thread. 
  26.          */ 
  27.         if (unlikely(wait_for_completion_killable(&done))) {            ------(6) 
  28.                 /* 
  29.                  * If I was SIGKILLed before kthreadd (or new kernel thread) 
  30.                  * calls complete(), leave the cleanup of this structure to 
  31.                  * that thread. 
  32.                  */ 
  33.                 if (xchg(&create->done, NULL)) 
  34.                         return ERR_PTR(-EINTR); 
  35.                 /* 
  36.                  * kthreadd (or new kernel thread) will call complete() 
  37.                  * shortly. 
  38.                  */ 
  39.                 wait_for_completion(&done); 
  40.         } 
  41.         task = create->result;                                          ------(7) 
  42.         if (!IS_ERR(task)) { 
  43.                 static const struct sched_param param = { .sched_priority = 0 }; 
  44.                 char name[TASK_COMM_LEN]; 
  45.  
  46.                 /* 
  47.                  * task is already visible to other tasks, so updating 
  48.                  * COMM must be protected. 
  49.                  */ 
  50.                 vsnprintf(name, sizeof(name), namefmt, args); 
  51.                 set_task_comm(task, name);                              ------(8) 
  52.                 /* 
  53.                  * root may have changed our (kthreadd's) priority or CPU mask. 
  54.                  * The kernel thread should not inherit these properties. 
  55.                  */ 
  56.                 sched_setscheduler_nocheck(task, SCHED_NORMAL, &param); ------(9) 
  57.                 set_cpus_allowed_ptr(task,                              ------(10) 
  58.                                      housekeeping_cpumask(HK_FLAG_KTHREAD)); 
  59.         } 
  60.         kfree(create); 
  61.         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 的关系:

 

 

责任编辑:武晓燕 来源: 人人都是极客
相关推荐

2021-12-14 08:28:08

Java多线程线程

2022-01-02 08:43:46

Python

2022-02-07 11:01:23

ZooKeeper

2021-08-01 07:19:16

语言OpenrestyNginx

2022-06-30 22:53:18

数据结构算法

2021-07-26 05:07:23

Swift万花尺代码

2021-10-26 10:40:26

代理模式虚拟

2021-12-04 22:05:02

Linux

2022-05-17 08:02:55

GoTryLock模式

2021-07-02 09:45:29

MySQL InnoDB数据

2021-07-05 22:11:38

MySQL体系架构

2023-01-03 08:31:54

Spring读取器配置

2021-07-06 08:59:18

抽象工厂模式

2021-05-11 08:54:59

建造者模式设计

2022-08-26 09:29:01

Kubernetes策略Master

2023-11-28 08:29:31

Rust内存布局

2022-08-23 08:00:59

磁盘性能网络

2021-07-16 22:43:10

Go并发Golang

2022-04-12 08:30:52

回调函数代码调试

2021-10-27 09:59:35

存储
点赞
收藏

51CTO技术栈公众号