Linux内核线程kernel thread详解--Linux进程的管理与调度(十)

内核线程

为何须要内核线程

Linux内核能够看做一个服务进程(管理软硬件资源,响应用户进程的种种合理以及不合理的请求)。html

内核须要多个执行流并行,为了防止可能的阻塞,支持多线程是必要的。node

内核线程就是内核的分身,一个分身能够处理一件特定事情。内核线程的调度由内核负责,一个内核线程处于阻塞状态时不影响其余的内核线程,由于其是调度的基本单位。linux

这与用户线程是不同的。由于内核线程只运行在内核态编程

所以,它只能使用大于PAGE_OFFSET(传统的x86_32上是3G)的地址空间。c#

内核线程概述

内核线程是直接由内核自己启动的进程。内核线程其实是将内核函数委托给独立的进程,它与内核中的其余进程”并行”执行。内核线程常常被称之为内核守护进程。多线程

他们执行下列任务app

  • 周期性地将修改的内存页与页来源块设备同步
  • 若是内存页不多使用,则写入交换区
  • 管理延时动做, 如2号进程接手内核进程的建立
  • 实现文件系统的事务日志

内核线程主要有两种类型electron

  1. 线程启动后一直等待,直至内核请求线程执行某一特定操做。
  2. 线程启动后按周期性间隔运行,检测特定资源的使用,在用量超出或低于预置的限制时采起行动。

内核线程由内核自身生成,其特色在于ide

  1. 它们在CPU的管态执行,而不是用户态。
  2. 它们只能够访问虚拟地址空间的内核部分(高于TASK_SIZE的全部地址),但不能访问用户空间

内核线程的进程描述符task_struct

task_struct进程描述符中包含两个跟进程地址空间相关的字段mm, active_mm函数

struct task_struct
{
    // ...
    struct mm_struct *mm;
    struct mm_struct *avtive_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)是另一种将工做推后执行的形式.工做队列能够把工做推后,交由一个内核线程去执行,也就是说,这个下半部分能够在进程上下文中执行。最重要的就是工做队列容许被从新调度甚至是睡眠。
具体的信息, 请参见
Linux 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中

//  http://lxr.free-electrons.com/source/include/linux/sched.h?v=2.4.37#L800
extern void daemonize(void);

定义在kernel/sched.c

http://lxr.free-electrons.com/source/kernel/sched.c?v=2.4.37#L1326

主要执行以下操做

  1. 该函数释放其父进程的全部资源,否则这些资源会一直锁定直到线程结束。
  2. 阻塞信号的接收
  3. 将init用做守护进程的父进程

咱们能够看到早期内核的不少地方使用了这个接口, 好比

能够参见
http://lxr.free-electrons.com/ident?v=2.4.37;i=daemonize

咱们将了这么多kernel_thread, 可是咱们并不提倡咱们使用它, 由于这个是底层的建立内核线程的操做接口, 使用kernel_thread在内核中执行大量的操做, 虽然建立的代价已经很小了, 可是对于追求性能的linux内核来讲还不能忍受

所以咱们只能说kernel_thread是一个古老的接口, 内核中的有些地方仍然在使用该方法, 将一个函数直接传递给内核来建立内核线程

新版本的实现
因而linux-3.x下以后, 有了更好的实现, 那就是

延后内核的建立工做, 将内核线程的建立工做交给一个内核线程来作, 即kthreadd 2号进程

可是在kthreadd还没建立以前, 咱们只能经过kernel_thread这种方式去建立, 同时kernel_thread的实现也改成由_do_fork(早期内核中是do_fork)来实现, 参见kernel/fork.c

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);
}

kthread_create

struct task_struct *kthread_create_on_node(int (*threadfn)(void *data),
                                           void *data,
                                          int node,
                                          const char namefmt[], ...);

#define kthread_create(threadfn, data, namefmt, arg...) \
       kthread_create_on_node(threadfn, data, NUMA_NO_NODE, namefmt, ##arg)

建立内核更经常使用的方法是辅助函数kthread_create,该函数建立一个新的内核线程。最初线程是中止的,须要使用wake_up_process启动它。

kthread_run

/**
 * kthread_run - create and wake a thread.
 * @threadfn: the function to run until signal_pending(current).
 * @data: data ptr for @threadfn.
 * @namefmt: printf-style name for the thread.
 *
 * Description: Convenient wrapper for kthread_create() followed by
 * wake_up_process().  Returns the kthread or ERR_PTR(-ENOMEM).
 */
#define kthread_run(threadfn, data, namefmt, ...)                          \
({                                                                         \
    struct task_struct *__k                                            \
            = kthread_create(threadfn, data, namefmt, ## __VA_ARGS__); \
    if (!IS_ERR(__k))                                                  \
            wake_up_process(__k);                                      \
    __k;                                                               \
})

内核线程的退出

线程一旦启动起来后,会一直运行,除非该线程主动调用do_exit函数,或者其余的进程调用kthread_stop函数,结束线程的运行。

int kthread_stop(struct task_struct *thread);

kthread_stop() 经过发送信号给线程。

若是线程函数正在处理一个很是重要的任务,它不会被中断的。固然若是线程函数永远不返回而且不检查信号,它将永远都不会中止。

在执行kthread_stop的时候,目标线程必须没有退出,不然会Oops。缘由很容易理解,当目标线程退出的时候,其对应的task结构也变得无效,kthread_stop引用该无效task结构就会出错。

为了不这种状况,须要确保线程没有退出,其方法如代码中所示:

thread_func()
{
    // do your work here
    // wait to exit
    while(!thread_could_stop())
    {
           wait();
    }
}

exit_code()
{
     kthread_stop(_task);   //发信号给task,通知其能够退出了
}

这种退出机制很温和,一切尽在thread_func()的掌控之中,线程在退出时能够从容地释放资源,而不是莫名其妙地被人“暗杀”。

相关文章
相关标签/搜索