linux将创建内核线程的工作交给了一个专门的内核线程kthreadd来完成,该线程会检查全局链表kthread_create_list,如果为NULL,就会调schedule()放弃cpu进入睡眠状态,否则就取下该链表中的一项创建对应的线程。本文就从khtreadd内核线程的创建开始来展示一下内核线程的创建过程。
1 kthreadd
linux2.6.30,创建内核线程是通过kethradd内核守护线程来完成的,虽然机制上有所变化,但是最终还是调用do_fork来完成线程的创建。Kthreadd守护线程是在linux内核启动时就已经创建的内核线程。在start_kernel调用的结束位置,会调用rest_init接口,而kthreadd就是在这个接口中创建的,代码如下所示:
asmlinkage void __init start_kernel(void)
{
。。。。。。。。。。。。。。。。。。。。。。
rest_init();
}
static noinline void __init_refok rest_init(void)
__releases(kernel_lock)
{
int pid;
/*在启动时创建内核线程,该线程主要用来创建内核线程*/
pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);
kthreadd_task = find_task_by_pid_ns(pid, &init_pid_ns);
}
Kthreadd守护线程本身也是一个内核线程,这个内核线程本质上与其他内核线程都一样,只不过是这个内核线程所做的工作是用来创建内核线程的。函数的调用关系如下所示:
Start_kernel=》rest_init=》kernel_thread=》do_fork
没有内核线程创建请求时,Kthreadd守护线程就处于睡眠状态,一旦有内核线程创建请求,则唤醒kthreadd守护线程,完成内核线程的创建工作。
kthreadd守护线程主要通过传入内核线程创建参数来创建特定的内核线程,因此,在讲述内核线程的创建之前,有必要了解一下内核线程的创建参数。内核线程创建相关的数据结构主要是struct kthread_create_info,这个数据结构表示创建一个内核线程所需要的信息。结构体定义如下所示:
structkthread_create_info
{
/* Information passed to kthread() fromkthreadd. */
int (*threadfn)(void *data);/*内核线程的回调函数*/
void *data;/*回调函数的参数*/
struct completion started;/*kthreadd等待新创建的线程启动*/
/* Result passed back to kthread_create()from kthreadd. */
struct task_struct *result;/*创建成功之后返回的task_struct*/
struct completion done;/*用户等待线程创建完成*/
struct list_head list;/*主要用于将创建参数结构放在一个全局链表中*/
};
用户需要创建一个内核线程时,会填充该数据结构,并将该结构体挂在全局的kthread_create_list链表中,然后唤醒kthreadd守护线程来创建内核线程,而用户线程则阻塞等待内核线程创建完成。详细的用户接口下文会介绍,这里就不在赘述。
根据前面对内核线程创建结构体的描述,kthreadd守护线程需要完成的工作就比较简单,主要就是遍历kthread_create_list,如果链表中有需要创建的内核线程,则调用create_kthread完成内核线程的创建工作。反之,没有内核线程需要创建,那么kthreadd守护线程将睡眠,等待下次内核线程创建请求的唤醒。
static voidcreate_kthread(struct kthread_create_info *create)
{
int pid;
/* We want our own signal handler (wetake no signals by default). */
pid =kernel_thread(kthread, create, CLONE_FS | CLONE_FILES | SIGCHLD);
if (pid < 0)
create->result = ERR_PTR(pid);
else
wait_for_completion(&create->started);/*等待新创建的线程启动*/
complete(&create->done);/*通知用户请求创建的线程已经完成*/
}
create_kthread主要开始调用kernel_thread来创建内核线程,到这里,内核线程的创建工作跟kthreadd内核守护线程创建过程就一致了,可谓是殊途同归。如果Kthreadd创建线程失败,则直接通知用户请求创建动作已经完成,至于是不是创建成功,需要用户自己判断。反之,内核线程创建成功,kthreadd守护线程会等待新创建的线程启动,然后才通知用户请求创建内核线程的动作已经完成。完成了一个内核线程的创建之后,从kthread_create_list链表中摘下下一个需要创建的内核线程,指定所有请求的内核线程创建成功,ktheadd线程睡眠。
2 kthread_create and kthread_run
既然linux2.6.30采用了kthreadd守护线程来创建内核线程,那么在内核编程的时候,用户就不应该直接调用kernel_thread来创建内核线程。(这里只是猜测,具体细节有待研究)linux内核提供给用户用来创建内核线程的接口有两个,一个是kthread_create,另一个是kthread_run。kthread_run接口其实也就是kthread_create的封装。所不同的是,kthread_run在内核线程创建成功之后,就直接调用了wake_up_process,来唤醒该内核线程,使其能够立刻得到调度,而通过kthread_create创建的内核线程默认是睡眠的,并不会得到调度,而是需要显示的唤醒该内核线程才能得到调度。代码如下所示:
structtask_struct * kthread_create(int (*threadfn)(void *data),
void *data,
const char namefmt[],
...)
{
struct kthread_create_info create;
/*填充create结构体*/
create.threadfn = threadfn;
create.data = data;
init_completion(&create.started);
init_completion(&create.done);
/*将create结构体挂到全局的kthread_create_list链表中*/
spin_lock(&kthread_create_lock);
list_add_tail(&create.list,&kthread_create_list);
spin_unlock(&kthread_create_lock);
/*唤醒kthradd守护线程来创建内核线程*/
wake_up_process(kthreadd_task);
/*等待线程的创建结束*/
wait_for_completion(&create.done);
/*kthreadd线程并不能保证100%创建成功,这里需要在做验证*/
if (!IS_ERR(create.result)) {
struct sched_param param = {.sched_priority = 0 };
va_list args;
* root may have changed our (kthreadd‘s)priority or CPU mask.
* The kernel thread should not inherit theseproperties.
*/
/*设置线程的优先级和cpu亲和性*/
sched_setscheduler_nocheck(create.result,SCHED_NORMAL, ?m);
set_user_nice(create.result,KTHREAD_NICE_LEVEL);
set_cpus_allowed_ptr(create.result,cpu_all_mask);
}
return create.result;
}
由前文可知,创建一个内核线程,首先要做的就是填充kthread_create_info结构体,将线程的处理函数以及相关参数传递给kthreadd守护线程,这样才能完成用户需要新创建线程所完成的工作。填充完结构体之后,还需要将结构体挂到全局的kthread_create_list链表中,然后唤醒kthreadd线程开始创建内核线程,用户线程睡眠等待新线程创建动作的完成,新线程完成之后,设置线程的相关属性,此时,线程就可以完成相关的工作了。
3 内核线程处理函数
内核线程的线程处理函数并不是用户自定义的接口,而是内核实现的一个统一的接口。这个统一的接口就是kthread,而用户自定义的线程处理函数是通过该统一接口进行回调的。Kthread接口完成了很多方面的工作,首先,就是我们刚刚说的回调用户自定义的接口,以便于内核线程能够完成用户想让该线程完成的工作。其次,就是通知kthreadd守护线程,新创建的线程已经开始启动,khtreadd线程可以创建下一个内核线程,第三,就是实现新创建的线程在没有显式调用唤醒之前,该线程是睡眠的;第四,就是可以很方便的将新建的内核线程终止,释放相关的资源。代码如下所示:
static int kthread(void *_create)
{
struct kthread_create_info *create =_create;
int (*threadfn)(void *data);
void *data;
int ret = -EINTR;
/* Copy data: it‘s on kthread‘s stack*/
threadfn = create->threadfn;
data = create->data;
/* OK, tell user we‘re spawned, waitfor stop or wakeup */
__set_current_state(TASK_UNINTERRUPTIBLE);
create->result = current;
complete(&create->started);
schedule();
/*线程没有被终止,则调用用户的回调接口完成相关的工作*/
if (!kthread_should_stop())
ret = threadfn(data);
/* It might have exited on its own, w/okthread_stop. Check. */
/*如果用户希望终止该线程,则通知用户已经完成终止的准备动作*/
if(kthread_should_stop()) {
kthread_stop_info.err = ret;
complete(&kthread_stop_info.done);
}
return 0;
}
4 终止内核线程
内核线程的终止是通过kthread_stop接口完成的。用户一旦调用了kthread_stop接口,首先会唤醒该内核线程,并设置需要终止的线程,然后睡眠,等待线程处理函数的唤醒。由前面的内核线程的处理函数可以看到,就不会在执行用户自定义的回调接口,同时会唤醒用户线程,完成终止内核线程的工作。Kthread_stop用来trace_point来完成内核线程的清理工作,trace_point的工作原理还不清楚,姑且就认为其可以完成我们需要的清理工作吧。
5 绑定内核线程到指定的cpu
对应SMP架构的CPU,可以通过kthread_bind接口将创建的内核线程绑定到某个指定的CPU上执行。通过绑定,可以减少内核线程在CPU之间的迁移,提高内核线程的工作效率。绑定CPU,主要原理是通过CPU的亲和性来实现的。这也是LINUX调度器天生就支持的,这里只不过是利用了调度器的亲和性功能实现了内核线程的绑定功能。
6总结
内核线程在linux内核中经常被使用,了解linux内核线程的创建以及销毁过程,可以帮助我们理解内核中利用内核线程完成的某些功能的工作原理。例如,work_queue,work_queue就是通过创建内核线程来完成相关的功能,如果我们知道内核线程的工作原理,在利用work_queue的时候,就能做到心中有底,可以知道什么时候work_queue在工作,什么时候在休眠等等。总之,从点滴开始学习linxu内核,可以帮助我们刚好的理解内核的一些功能的机制和原理。