Linux: 内核线程简析

本文详细分析了Linux内核线程的创建过程,包括通过__kthread_create_on_node()将请求放入列表,由kthreadd线程处理创建请求。内核线程的运行始于kthread()函数,调用指定的入口函数。泊停机制通过kthread_park()和kthread_unpark()控制线程的暂停与恢复。文章还介绍了如何通过kthread_stop()终止内核线程及其流程。
摘要由CSDN通过智能技术生成

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() 处 */
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值