1. 前言
限于作者能力水平,本文可能存在谬误,因此而给读者带来的损失,作者不做任何承诺。
2. 分析背景
本文基于 Linux 4.14
内核源码进行分析。
3. 内核线程
3.1 内核线程的生命周期
3.1.1 创建
内核线程的建立,分为 请求 + 创建
两步。
请求
通过 __kthread_create_on_node()
将内核线程创建请求数据放入列表kthread_create_list
:
static __printf(4, 0)
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);
struct task_struct *task;
struct kthread_create_info *create = kmalloc(sizeof(*create), GFP_KERNEL);
create->threadfn = threadfn;
create->data = data;
create->node = node;
create->done = &done;
/* 将创建内核线程的请求放入列表 kthread_create_list 尾部 */
spin_lock(&kthread_create_lock);
list_add_tail(&create->list, &kthread_create_list);
spin_unlock(&kthread_create_lock);
/*
* 内核线程创建请求处理线程 kthreadd 在没有请求的时候,
* 会陷入睡眠,需要发起唤醒动作。
*/
wake_up_process(kthreadd_task);
if (unlikely(wait_for_completion_killable(&done))) { /* 等待内核线程创建完成 */
if (xchg(&create->done, NULL))
return ERR_PTR(-EINTR);
wait_for_completion(&done);
}
task = create->result;
...
kfree(create);
return task;
}
创建
通过 kernel_thread() -> _do_fork()
调用链创建内核线程:
/*
* 处理内核线程创建请求的是一个特殊内核线程 "kthreadd" 。
* 该线程用于处理其它内核线程创建请求,其建立流程如下:
*/
start_kernel()
rest_init()
pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);
return _do_fork(flags|CLONE_VM|CLONE_UNTRACED, (unsigned long)fn,
(unsigned long)arg, NULL, NULL, 0)
内核线程 "kthreadd"
用来处理内核线程的创建请求,其具体流程如下:
int kthreadd(void *unused)
{
struct task_struct *tsk = current;
set_task_comm(tsk, "kthreadd"); /* ps -ef 可观察到 */
ignore_signals(tsk); /* 忽略所有信号 */
set_cpus_allowed_ptr(tsk, cpu_all_mask);
set_mems_allowed(node_states[N_MEMORY]);
current->flags |= PF_NOFREEZE; /* 标识 kthreadd 不应被冻结 */
...
/* 处理内核线程创建请求的循环 */
for (;;) {
/* 如果没有创建请求,则将自身陷入可中断睡眠,让出 CPU */
set_current_state(TASK_INTERRUPTIBLE);
if (list_empty(&kthread_create_list))
schedule();
/* 有新的内核线程创建请求时被唤醒,状态重新置为 TASK_RUNNING 态 */
__set_current_state(TASK_RUNNING);
spin_lock(&kthread_create_lock);
while (!list_empty(&kthread_create_list)) { /* 为列表 @kthread_create_list 的每个请求创建1个内核线程 */
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);
spin_lock(&kthread_create_lock);
}
spin_unlock(&kthread_create_lock);
}
return 0;
}
create_kthread()
创建内核线程的流程:
static void create_kthread(struct kthread_create_info *create)
{
int pid;
pid = kernel_thread(kthread, create, CLONE_FS | CLONE_FILES | SIGCHLD)
return _do_fork(flags|CLONE_VM|CLONE_UNTRACED, (unsigned long)fn,
(unsigned long)arg, NULL, NULL, 0);
...
}
3.1.2 运行
内核线程创建并被唤醒后,进入内核线程公共入口 kthread()
,再由 kthread()
调用内核线程的入口 xxx_kthread_entry()
。具体流程如下:
static int kthread(void *_create)
{
struct kthread_create_info *create = _create; /* 内核线程创建请求信息 */
int (*threadfn)(void *data) = create->threadfn; /* 内核线程入口 */
void *data = create->data; /* 内核线程数据 */
struct completion *done;
struct kthread *self;
int ret;
/* 创建内核对象 struct kthread */
self = kmalloc(sizeof(*self), GFP_KERNEL);
set_kthread_struct(self)
/*
* 内核线程经由 task_struct 对象参与调度,用 kthread 对象描述。
* 将内核线程的数据 kthread 记录到 task_struct::set_child_tid ,
* 后续需要时可通过接口 to_kthread() 从 task_struct 获取。
*/
current->set_child_tid = (__force void __user *)kthread;
done = xchg(&create->done, NULL);
self->flags = 0;
self->data = data; /* 设定内核线程数据 */
init_completion(&self->exited);
init_completion(&self->parked);
current->vfork_done = &self->exited;
__set_current_state(TASK_UNINTERRUPTIBLE);
create->result = current; /* 返回给内核线程创建进程新线程的 task_struct */
/*
* 发出内核线程创建请求的进程,在 __kthread_create_on_node()
* 等待创建完成。在这里唤醒等待进程继续运行。
*/
complete(done);
if (!test_bit(KTHREAD_SHOULD_STOP, &self->flags)) { /* 没有请求终止当前内核线程 */
/*
* 处理内核线程的 泊停(PARK):
* 如果有对内核线程的泊停请求(通过 kthread_park()),这里
* 将执行实际地泊停动作。
*/
__kthread_parkme(self);
ret = threadfn(data); /* 进入内核线程入口函数 xxx_kthread_entry() */
}
do_exit(ret); /* 内核线程退出 */
}
3.1.3 泊停(PARK)
泊停(PARK)
是指暂停内核线程的执行,但并不终止内核线程。通过接口 kthread_park()
泊停内核线程,接口 kthread_unpark()
重启被泊停的内核线程。
和内核线程的创建类似,泊停内核线程也分为 请求 + 泊停
两部分。先看发起内核线程泊停请求的流程:
/*
* 请求将内核线程 @k 泊停:
* 先发出泊停请求 (即置位 KTHREAD_SHOULD_PARK) ,然后睡眠等待,
* 直到内核线程在别处完成泊停后,函数返回。
*/
int kthread_park(struct task_struct *k)
{
struct kthread *kthread = to_kthread(k);
if (WARN_ON(k->flags & PF_EXITING))
return -ENOSYS;
if (!test_bit(KTHREAD_IS_PARKED, &kthread->flags)) { /* 内核线程 @k 当前不处于泊停状态 */
/*
* 通过置位 KTHREAD_SHOULD_PARK ,请求泊停内核线程。
* 在内核线程代码路径的下列两处处理泊停请求:
* . 内核线程公共入口 kthread() 中,如果检查到泊停请求 (即 KTHREAD_SHOULD_PARK 标记),则泊停内核线程;
* . 内核线程入口函数中,如果检查到泊停请求 (通过 kthread_should_park() 检查),则泊停内核线程。
*/
set_bit(KTHREAD_SHOULD_PARK, &kthread->flags);
if (k != current) { /* 如果请求被泊停的内核线程当前没在运行 */
wake_up_process(k); /* 唤醒请求被泊停的内核线程,以便完成泊停请求 */
wait_for_completion(&kthread->parked); /* 等待内核线程的泊停完成 */
}
}
return 0;
}
内核线程泊停的实际执行是通过 __kthread_parkme()
或 kthread_parkme()
完成。
内核线程执行到公共入口 kthread()
期间,如果检查到对内核线程的泊停请求,通过 __kthread_parkme()
泊停内核线程,前面列出的代码已经描述过,在这里再重复下其流程:
static int kthread(void *_create)
{
struct kthread_create_info *create = _create; /* 内核线程创建请求信息 */
int (*threadfn)(void *data) = create->threadfn; /* 内核线程入口 */
...
struct completion *done;
...
done = xchg(&create->done, NULL);
...
init_completion(&self->parked);
if (!test_bit(KTHREAD_SHOULD_STOP, &self->flags)) {
/* 处理内核线程的 泊停:
* 如果有通过 kthread_park() 请求泊停内核线程,这里将执行实际的泊停动作。
*/
__kthread_parkme(self)
}
}
/* 泊停内核线程 @self */
static void __kthread_parkme(struct kthread *self)
{
for (;;) {
set_current_state(TASK_PARKED);
if (!test_bit(KTHREAD_SHOULD_PARK, &self->flags))
break;
/*
* KTHREAD_SHOULD_PARK 标记要将内核线程泊停,这里泊停内核线程,
* 然后唤醒等待内核线程泊停完成的进程。
* 泊停内核线程从接口 kthread_park() 发起,它所在的进程会等待
* 泊停完成。
*
* 可通过 kthread_unpark() 唤醒泊停的内核线程继续运行。
*/
if (!test_and_set_bit(KTHREAD_IS_PARKED, &self->flags))
complete(&self->parked);
schedule();
}
/* 继续运行 */
clear_bit(KTHREAD_IS_PARKED, &self->flags); /* 清除泊停标记位 */
__set_current_state(TASK_RUNNING); /* 进程重置为可运行态 */
}
然后在内核线程入口 xxx_kthread_entry()
后,如果检测到泊停请求,执行泊停操作。典型的流程如下:
int xxx_kthread_entry(void *arg)
{
...
while (!kthread_should_stop()) {
...
if (kthread_should_park()) /* 检查泊停请求 */
kthread_parkme(); /* 泊停内核线程 */
...
}
...
}
kthread_parkme()
是对 __kthread_parkme()
的简单封装:
void kthread_parkme(void)
{
__kthread_parkme(to_kthread(current));
}
3.1.4 终止
通过接口 kthread_stop()
请求终止内核线程。看一下 kthread_stop()
的实现:
int kthread_stop(struct task_struct *k)
{
struct kthread *kthread;
int ret;
...
get_task_struct(k);
kthread = to_kthread(k);
set_bit(KTHREAD_SHOULD_STOP, &kthread->flags); /* 请求终止内核线程 */
/*
* 内核线程当前可能处于泊停状态,重启它继续执行,
* 以便它从内核线程函数返回到 kthread() ,然后执行退出流程 do_exit() 。
*/
kthread_unpark(k);
wake_up_process(k); /* 唤醒 */
wait_for_completion(&kthread->exited); /* 等待内核线程退出 */
ret = k->exit_code; /* 获取线程退出码 */
put_task_struct(k);
...
return ret; /* 返回线程退出码 */
}
内核线程函数在通过 kthread_should_stop()
检测到终止请求时,应该退出函数,流程返回到 kthread()
,然后执行线程退出函数 do_exit()
。
从 kthread_stop()
代码看到,它会睡眠等待内核线程退出,我们看一下这个唤醒流程。它由两个代码片段组成。一个代码片段,是进程创建期间,初始化唤醒等待的数据:
static int kthread(void *_create)
{
struct kthread *self;
...
self = kmalloc(sizeof(*self), GFP_KERNEL);
...
init_completion(&self->exited);
...
current->vfork_done = &self->exited; /* 正是通过 kthread::exited ,唤醒发出内核线程终止的进程 */
....
}
另一个代码片段,用来唤醒在 kthread_stop()
中等待内核线程退出的进程:
kthread()
...
if (!test_bit(KTHREAD_SHOULD_STOP, &self->flags)) {
...
ret = threadfn(data); /* 进入内核线程入口函数 */
}
/* 从内核线程函数返回后,执行终止退出流程 */
do_exit()
...
exit_mm()
mm_release(current, mm)
...
/* 唤醒在 kthread_stop() 中等待内核线程退出的进程 */
if (tsk->vfork_done)
complete_vfork_done(tsk);
...
...
3.2 内核线程逻辑模板
执行流程从内核线程公共入口进入线程函数入口: kthread() -> xxx_kthread_entry()
。
int xxx_kthread_entry(void *arg)
{
int ret = 0;
/*
* 通过接口 kthread_should_stop() 测试,是否有要求终止内核线
* 程。
* 其它进程可通过接口 kthread_stop() 发出终止内核线程的请求。
*/
while (!kthread_should_stop()) {
// 代码逻辑
if (kthread_should_park()) /* 检查泊停请求 */
kthread_parkme(); /* 泊停内核线程 */
if (因错误应该终止内核线程) {
ret = 错误码;
break;
}
}
return ret; /* 流程返回到 kthread() 中 do_exit() 处 */
}