Kthread
内核线程
为什么需要内核线程
Linux 内核可以看作一个服务进程(管理软硬件资源,响应用户进程的种种合理以及不合理的请求)。
内核需要多个执行流并行,为了防止可能的阻塞,支持多线程是必要的。
内核线程就是内核的分身,一个分身可以处理一件特定事情。内核线程的调度由内核负责,一个内核线程处于阻塞状态时不影响其他的内核线程,因为其是调度的基本单位。
这与用户线程是不一样的。因为内核线程只运行在内核态
因此,它只能使用大于 PAGE_OFFSET(传统的 x86_32 上是 3G)的地址空间。
内核线程概述
内核线程是直接由内核本身启动的进程。内核线程实际上是将内核函数委托给独立的进程,它与内核中的其他进程”并行”执行。内核线程经常被称之为内核守护进程。
他们执行下列任务
- 周期性地将修改的内存页与页来源块设备同步
- 如果内存页很少使用,则写入交换区
- 管理延时动作, 如2号进程接手内核进程的创建
- 实现文件系统的事务日志
内核线程主要有两种类型
- 线程启动后一直等待,直至内核请求线程执行某一特定操作。
- 线程启动后按周期性间隔运行,检测特定资源的使用,在用量超出或低于预置的限制时采取行动。
内核线程由内核自身生成,其特点在于
- 它们在 CPU 的管态执行,而不是用户态。
- 它们只可以访问虚拟地址空间的内核部分(高于 TASK_SIZE 的所有地址),但不能访问用户空间
内核线程的进程描述符 task_struct
task_struct 进程描述符中包含两个跟进程地址空间相关的字段 mm, active_mm,
|
|
大多数计算机上系统的全部虚拟地址空间分为两个部分: 供用户态程序访问的虚拟地址空间和供内核访问的内核空间。每当内核执行上下文切换时, 虚拟地址空间的用户层部分都会切换, 以便当前运行的进程匹配, 而内核空间不会放生切换。
对于普通用户进程来说,mm 指向虚拟地址空间的用户空间部分,而对于内核线程,mm 为 NULL。
这位优化提供了一些余地, 可遵循所谓的惰性 TLB 处理(lazy TLB handing)。active_mm 主要用于优化,由于内核线程不与任何特定的用户层进程相关,内核并不需要倒换虚拟地址空间的用户层部分,保留旧设置即可。由于内核线程之前可能是任何用户层进程在执行,故用户空间部分的内容本质上是随机的,内核线程决不能修改其内容,故将 mm 设置为 NULL,同时如果切换出去的是用户进程,内核将原来进程的 mm 存放在新内核线程的 active_mm 中,因为某些时候内核必须知道用户空间当前包含了什么。
为什么没有 mm 指针的进程称为惰性 TLB 进程?
假如内核线程之后运行的进程与之前是同一个, 在这种情况下, 内核并不需要修改用户空间地址表。地址转换后备缓冲器(即 TLB)中的信息仍然有效。只有在内核线程之后, 执行的进程是与此前不同的用户层进程时, 才需要切换(并对应清除 TLB 数据)。
内核线程和普通的进程间的区别在于内核线程没有独立的地址空间,mm 指针被设置为 NULL;它只在 内核空间运行,从来不切换到用户空间去;并且和普通进程一样,可以被调度,也可以被抢占。
内核线程的创建
创建内核线程接口的演变
内核线程可以通过两种方式实现:
-
古老的接口 kernel_create 和 daemonize
将一个函数传递给 kernel_thread 创建并初始化一个 task,该函数接下来负责帮助内核调用 daemonize 已转换为内核守护进程,daemonize 随后完成一些列操作, 如该函数释放其父进程的所有资源,不然这些资源会一直锁定直到线程结束。阻塞信号的接收, 将 init 用作守护进程的父进程
-
更加现在的方法 kthead_create 和 kthread_run
创建内核更常用的方法是辅助函数 kthread_create,该函数创建一个新的内核线程。最初线程是停止的,需要使用 wake_up_process 启动它。
使用 kthread_run,与 kthread_create 不同的是,其创建新线程后立即唤醒它,其本质就是先用 kthread_create 创建一个内核线程,然后通过 wake_up_process 唤醒它
2 号进程 kthreadd 的诞生
早期的 kernel_create 和 daemonize 接口
在早期的内核中, 提供了 kernel_create 和 daemonize 接口, 但是这种机制操作复杂而且将所有的任务交给内核去完成。
但是这种机制低效而且繁琐, 将所有的操作塞给内核, 我们创建内核线程的初衷不本来就是为了内核分担工作, 减少内核的开销的么
Workqueue 机制
因此在 linux-2.6 以后, 提供了更加方便的接口 kthead_create 和 kthread_run, 同时将内核线程的创建操作延后, 交给一个工作队列 workqueue, 参见http://lxr.linux.no/linux+v2.6.13/kernel/kthread.c#L21,
Linux 中的 workqueue 机制就是为了简化内核线程的创建。通过 kthread_create 并不真正创建内核线程, 而是将创建工作 create work 插入到工作队列helper_wq中, 随后调用 workqueue 的接口就能创建内核线程。并且可以根据当前系统 CPU 的个数创建线程的数量,使得线程处理的事务能够并行化。workqueue 是内核中实现简单而有效的机制,他显然简化了内核 daemon 的创建,方便了用户的编程.
工作队列(workqueue)是另外一种将工作推后执行的形式.工作队列可以把工作推后,交由一个内核线程去执行,也就是说,这个下半部分可以在进程上下文中执行。最重要的就是工作队列允许被重新调度甚至是睡眠。
具体的信息, 请参见
2 号进程 kthreadd
但是这种方法依然看起来不够优美, 我们何不把这种创建内核线程的工作交给一个特殊的内核线程来做呢?
于是 linux-2.6.22 引入了 kthreadd 进程, 并随后演变为 2 号进程, 它在系统初始化时同 1 号进程一起被创建(当然肯定是通过 kernel_thread), 参见 rest_init 函数, 并随后演变为创建内核线程的真正建造师, 参见 kthreadd和kthreadd 函数, 它会循环的是查询工作链表static LIST_HEAD(kthread_create_list);中是否有需要被创建的内核线程, 而我们的通过 kthread_create 执行的操作, 只是在内核线程任务队列 kthread_create_list 中增加了一个 create 任务, 然后会唤醒 kthreadd 进程来执行真正的创建操作
内核线程会出现在系统进程列表中, 但是在 ps 的输出中进程名 command 由方括号包围, 以便与普通进程区分。
如下图所示, 我们可以看到系统中, 所有内核线程都用[]标识, 而且这些进程父进程 id 均是 2, 而 2 号进程 kthreadd 的父进程是 0 号进程
使用 ps -eo pid,ppid,command
kernel_thread
kernel_thread 是最基础的创建内核线程的接口, 它通过将一个函数直接传递给内核来创建一个进程, 创建的进程运行在内核空间, 并且与其他进程线程共享内核虚拟地址空间
kernel_thread 的实现经历过很多变革 早期的 kernel_thread 执行更底层的操作, 直接创建了 task_struct 并进行初始化,
引入了 kthread_create 和 kthreadd 2 号进程后, kernel_thread 的实现也由统一的_do_fork(或者早期的 do_fork)托管实现
早期实现
早期的内核中, kernel_thread 并不是使用统一的 do_fork 或者_do_fork 这一封装好的接口实现的, 而是使用更底层的细节
参见
http://lxr.free-electrons.com/source/kernel/fork.c?v=2.4.37#L613
我们可以看到它内部调用了更加底层的 arch_kernel_thread 创建了一个线程
arch_kernel_thread
其具体实现请参见
http://lxr.free-electrons.com/ident?v=2.4.37;i=arch_kernel_thread
但是这种方式创建的线程并不适合运行,因此内核提供了 daemonize 函数, 其声明在 include/linux/sched.h 中
|
|
定义在 kernel/sched.c
http://lxr.free-electrons.com/source/kernel/sched.c?v=2.4.37#L1326
主要执行如下操作
- 该函数释放其父进程的所有资源,不然这些资源会一直锁定直到线程结束。
- 阻塞信号的接收
- 将 init 用作守护进程的父进程
我们可以看到早期内核的很多地方使用了这个接口, 比如
可以参见
我们将了这么多 kernel_thread, 但是我们并不提倡我们使用它, 因为这个是底层的创建内核线程的操作接口, 使用 kernel_thread 在内核中执行大量的操作, 虽然创建的代价已经很小了, 但是对于追求性能的 linux 内核来说还不能忍受
因此我们只能说 kernel_thread 是一个古老的接口, 内核中的有些地方仍然在使用该方法, 将一个函数直接传递给内核来创建内核线程
新版本的实现
于是 linux-3.x 下之后, 有了更好的实现, 那就是
延后内核的创建工作, 将内核线程的创建工作交给一个内核线程来做, 即 kthreadd 2 号进程
但是在 kthreadd 还没创建之前, 我们只能通过 kernel_thread 这种方式去创建,
同时 kernel_thread 的实现也改为由_do_fork(早期内核中是 do_fork)来实现, 参见kernel/fork.c
|
|
kthread_create
|
|
创建内核更常用的方法是辅助函数 kthread_create,该函数创建一个新的内核线程。最初线程是停止的,需要使用 wake_up_process 启动它。
kthread_run
|
|
使用 kthread_run,与 kthread_create 不同的是,其创建新线程后立即唤醒它,其本质就是先用 kthread_create 创建一个内核线程,然后通过 wake_up_process 唤醒它
内核线程的退出
线程一旦启动起来后,会一直运行,除非该线程主动调用 do_exit 函数,或者其他的进程调用 kthread_stop 函数,结束线程的运行。
|
|
kthread_stop() 通过发送信号给线程。
如果线程函数正在处理一个非常重要的任务,它不会被中断的。当然如果线程函数永远不返回并且不检查信号,它将永远都不会停止。
在执行 kthread_stop 的时候,目标线程必须没有退出,否则会 Oops。原因很容易理解,当目标线程退出的时候,其对应的 task 结构也变得无效,kthread_stop 引用该无效 task 结构就会出错。
为了避免这种情况,需要确保线程没有退出,其方法如代码中所示:
|
|
这种退出机制很温和,一切尽在 thread_func()的掌控之中,线程在退出时可以从容地释放资源,而不是莫名其妙地被人“暗杀”。
Kthreadd 是 Linux 系统中的 2 号进程,它的任务就是管理和调度其他内核线程 kernel_thread, 会循环执行一个 kthreadd 的函数,该函数的作用就是运行 kthread_create_list 全局链表中维护的 kthread, 当我们调用 kernel_thread 创建的内核线程会被加入到此链表中,因此所有的内核线程都是直接或者间接的以 kthreadd 为父进程。
前言
Linux 下有 3 个特殊的进程,idle 进程(PID = 0), init 进程(PID = 1)和 kthreadd(PID = 2)
- idle 进程由系统自动创建, 运行在内核态
idle 进程其 pid=0,其前身是系统创建的第一个进程,也是唯一一个没有通过 fork 或者 kernel_thread 产生的进程。完成加载系统后,演变为进程调度、交换
- init 进程由 idle 通过 kernel_thread 创建,在内核空间完成初始化后, 加载 init 程序, 并最终用户空间
由 0 进程创建,完成系统的初始化. 是系统中所有其它用户进程的祖先进程 Linux 中的所有进程都是有 init 进程创建并运行的。首先 Linux 内核启动,然后在用户空间中启动 init 进程,再启动其他系统进程。在系统启动完成完成后,init 将变为守护进程监视系统其他进程。
- kthreadd 进程由 idle 通过 kernel_thread 创建,并始终运行在内核空间, 负责所有内核线程的调度和管理
它的任务就是管理和调度其他内核线程 kernel_thread, 会循环执行一个 kthreadd 的函数,该函数的作用就是运行 kthread_create_list 全局链表中维护的 kthread, 当我们调用 kernel_thread 创建的内核线程会被加入到此链表中,因此所有的内核线程都是直接或者间接的以 kthreadd 为父进程
我们下面就详解分析 2 号进程 kthreadd
2 号进程
内核初始化 rest_init 函数中,由进程 0 (swapper 进程)创建了两个 process
- init 进程 (pid = 1, ppid = 0)
- kthreadd (pid = 2, ppid = 0)
所有其它的内核线程的 ppid 都是 2,也就是说它们都是由 kthreadd thread 创建的
所有的内核线程在大部分时间里都处于阻塞状态(TASK_INTERRUPTIBLE)只有在系统满足进程需要的某种资源的情况下才会运行
它的任务就是管理和调度其他内核线程 kernel_thread, 会循环执行一个 kthreadd 的函数,该函数的作用就是运行 kthread_create_list 全局链表中维护的 kthread, 当我们调用 kernel_thread 创建的内核线程会被加入到此链表中,因此所有的内核线程都是直接或者间接的以 kthreadd 为父进程
2 号进程的创建
在 rest_init 函数中创建 2 号进程的代码如下
|
|
2 号进程的事件循环
|
|
kthreadd 的核心是一 for 和 while 循环体。
在 for 循环中,如果发现 kthread_create_list 是一空链表,则调用 schedule 调度函数,因为此前已经将该进程的状态设置为 TASK_INTERRUPTIBLE,所以 schedule 的调用将会使当前进程进入睡眠。
如果 kthread_create_list 不为空,则进入 while 循环,在该循环体中会遍历该 kthread_create_list 列表,对于该列表上的每一个 entry,都会得到对应的类型为 struct kthread_create_info 的节点的指针 create.
然后函数在 kthread_create_list 中删除 create 对应的列表 entry,接下来以 create 指针为参数调用 create_kthread(create).
create_kthread 的过程如下
create_kthread 完成内核线程创建
|
|
在 create_kthread()函数中,会调用 kernel_thread 来生成一个新的进程,该进程的内核函数为 kthread,调用参数为
|
|
我们可以看到,创建的内核线程执行的事件kthread
此时回到 kthreadd thread,它在完成了进程的创建后继续循环,检查 kthread_create_list 链表,如果为空,则 kthreadd 内核线程昏睡过去
那么我们现在回想我们的操作 我们在内核中通过 kernel_create 或者其他方式创建一个内核线程, 然后 kthreadd 内核线程被唤醒, 来执行内核线程创建的真正工作,于是这里有三个线程
- kthreadd 已经光荣完成使命(接手执行真正的创建工作),睡眠
- 唤醒 kthreadd 的线程由于新创建的线程还没有创建完毕而继续睡眠 (在 kthread_create 函数中)
- 新创建的线程已经正在运行 kthread,但是由于还有其它工作没有做所以还没有最终创建完成.
新创建的内核线程 kthread 函数
|
|
线程创建完毕:
创建新 thread 的进程恢复运行 kthread_create() 并且返回新创建线程的任务描述符 新创建的线程由于执行了 schedule() 调度,此时并没有执行.
直到我们使用 wake_up_process(p);唤醒新创建的线程
线程被唤醒后, 会接着执行 threadfn(data)
|
|
总结
kthreadd 进程由 idle 通过 kernel_thread 创建,并始终运行在内核空间, 负责所有内核线程的调度和管理,它的任务就是管理和调度其他内核线程 kernel_thread, 会循环执行一个 kthreadd 的函数,该函数的作用就是运行 kthread_create_list 全局链表中维护的 kthread, 当我们调用 kernel_thread 创建的内核线程会被加入到此链表中,因此所有的内核线程都是直接或者间接的以 kthreadd 为父进程
我们在内核中通过 kernel_create 或者其他方式创建一个内核线程, 然后 kthreadd 内核线程被唤醒, 来执行内核线程创建的真正工作,新的线程将执行 kthread 函数, 完成创建工作,创建完毕后让出 CPU,因此新的内核线程不会立刻运行.需要手工 wake up, 被唤醒后将执行自己的真正工作函数
- 任何一个内核线程入口都是 kthread()
- 通过 kthread_create() 创建的内核线程不会立刻运行.需要手工 wake up.
- 通过 kthread_create() 创建的内核线程有可能不会执行相应线程函数 threadfn 而直接退出
-
No backlinks found.