一、开篇
线程是操作系统的重要组成部件之一,linux内核中,内核线程是如何创建的,在内核启动过程中,诞生了哪些支撑整个系统运转的线程,本文将带着这个疑问瞅一瞅内核源码,分析内核线程的创建机制。本文基于linux内核版本:4.1.15。
与linux内核1号init进程一样,在rest_init()函数中将调用kthread_init()函数创建kthreadd线程,如下代码:
pid=kernel_thread(kthreadd,NULL,CLONE_FS|CLONE_FILES);
下文将看看kthreadd()中完成了哪些事情。
二、kthreadd线程入口分析
kthreadd线程的线程入口为kthreadd(/kernel/kthread.c),如下定义:
intkthreadd(void*unused) { structtask_struct*tsk=current; //该函数会清除当前运行的可执行文件的所有trace,以便启动一个新的trace set_task_comm(tsk,"kthreadd"); //忽略tsk的信号 ignore_signals(tsk); //该行代码允许kthreadd在任何CPU上运行 set_cpus_allowed_ptr(tsk,cpu_all_mask); //设置由alloc_lock保护的内存空间 set_mems_allowed(node_states[N_MEMORY]); //设置kthreadd线程不应该被冻结 current->flags|=PF_NOFREEZE; for(;;){ set_current_state(TASK_INTERRUPTIBLE); if(list_empty(&kthread_create_list)) schedule(); __set_current_state(TASK_RUNNING); spin_lock(&kthread_create_lock); while(!list_empty(&kthread_create_list)){ structkthread_create_info*create; create=list_entry(kthread_create_list.next, structkthread_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); } return0; }
上述第3行代码:使用current获取线程控制块。current定义如下(/include/asm-generic/current.h):
#defineget_current()(current_thread_info()->task) #definecurrentget_current()
上述代码中16~36行代码:for(;;)是kthreadd的核心功能。使用set_current_state(TASK_INTERRUPTIBLE);将当前线程设置为TASK_INTERRUPTIBLE状态,如果当前没有要创建的线程(这一步由kthread_create_list实现),则主动调用schedule()执行调度,让出CPU,这部分由17~19行代码实现。否则,kthreadd将处于唤醒状态,那么就会执行对应的线程创建操作,这部分功能由23~34行代码实现。
上述代码中,出现了kthread_create_list这个待创建线程的链表,定义如下:
staticLIST_HEAD(kthread_create_list);
第26~27行代码,使用:
create=list_entry(kthread_create_list.next, structkthread_create_info,list);
从链表中取得 kthread_create_info 结构的地址。
第31行代码使用create_kthread()创建create代表的内核线程。定义如下(/kernel/kernel.c):
staticvoidcreate_kthread(structkthread_create_info*create) { intpid; #ifdefCONFIG_NUMA current->pref_node_fork=create->node; #endif /*Wewantourownsignalhandler(wetakenosignalsbydefault).*/ pid=kernel_thread(kthread,create,CLONE_FS|CLONE_FILES|SIGCHLD); if(pid< 0) { /* If user was SIGKILLed, I release the structure. */ struct completion *done = xchg(&create->done,NULL); if(!done){ kfree(create); return; } create->result=ERR_PTR(pid); complete(done); } }
从上述代码可知,在create_kthread()中创建线程同样由kernel_thread()函数完成:
pid=kernel_thread(kthread,create,CLONE_FS|CLONE_FILES|SIGCHLD);
可见新创建的线程的入口是kthread,下文将继续分析该线程函数。
三、kthread分析
该函数定义在(/kernel/kthead.c)中:
staticintkthread(void*_create) { //拷贝数据 //将_create代表的kthread_create_info赋值给create structkthread_create_info*create=_create; //设置线程执行的函数指针 int(*threadfn)(void*data)=create->threadfn; void*data=create->data; structcompletion*done; structkthreadself; intret; self.flags=0; self.data=data; init_completion(&self.exited); init_completion(&self.parked); current->vfork_done=&self.exited; /*IfuserwasSIGKILLed,Ireleasethestructure.*/ done=xchg(&create->done,NULL); if(!done){ kfree(create); do_exit(-EINTR); } /*OK,telluserwe'respawned,waitforstoporwakeup*/ /*创建的新的内核线程被置为TASK_UNINTERRUPTIBLE,需要显式的被唤醒才能运行*/ __set_current_state(TASK_UNINTERRUPTIBLE); create->result=current; complete(done); //运行到此处,线程已经创建完毕,调用schedule执行调度,主动让出CPU,唤醒的是调用kthread_create函数的进程。 schedule(); //当本线程(创建的线程)被唤醒后,将继续执行后续代码 ret=-EINTR; if(!test_bit(KTHREAD_SHOULD_STOP,&self.flags)){ __kthread_parkme(&self); ret=threadfn(data); } /*wecan'tjustreturn,wemustpreserve"self"onstack*/ do_exit(ret); }
上述函数中,创建新 thread 的进程恢复运行 kthread_create() 并且返回新创建线程的任务描述符,在创建线程的线程中由于执行了 schedule() 调度,此时并没有执行。需使用wake_up_process(p);唤醒新创建的线程,这时候新创建的线程才得以执行。当线程被唤醒后, 会接着执行threadfn(data)(即:对应线程的真正线程函数)(这一点后文会通过实践加以验证!):
if(!test_bit(KTHREAD_SHOULD_STOP,&self.flags)){ __kthread_parkme(&self); //执行创建线程对应的线程函数,传入的参数为data ret=threadfn(data); }
总结一下,在kthreadd线程函数中,将完成两件重要的事情:
1、如果kthread_create_list线程创建链表为空,则调用schedule()执行线程调度。
2、如果kthread_create_list线程创建链表不为空(即需要创建线程),则调用create_kthread()创建内核线程。
(1)动手玩一玩
基于上述分析,本小节对其加以实践。
image-20230709113909381
重新编译构建内核后启动运行,在启动阶段,会打印出下述信息,这属于正常的预期现象,因为内核在启动阶段会涉及到内核重要线程的创建和运行。例如:
接下来以模块方式设计两份代码:
(1)module_1.c:使用kthread_create()创建一个名为my_thread的线程,在线程执行函数中每隔1000ms打印出一行信息:my_thread running。并暴露出对my_thread线程的唤醒接口:
#include#include #include #include #include #include staticstructtask_struct*my_thread; voidwakeup_mythread(void) { //唤醒内核线程 wake_up_process(my_thread); } EXPORT_SYMBOL(wakeup_mythread); intmy_thread_function(void*data){ while(!kthread_should_stop()){ printk("my_threadrunning "); msleep(1000); } return0; } staticint__initmodule_1_init(void){ //创建内核线程 my_thread=kthread_create(my_thread_function,NULL,"my_thread"); if(IS_ERR(my_thread)){ printk(KERN_ERR"Failedtocreatekernelthread "); returnPTR_ERR(my_thread); } return0; } staticvoid__exitmodule_1_exit(void){ //停止内核线程 kthread_stop(my_thread); } module_init(module_1_init); module_exit(module_1_exit); MODULE_AUTHOR("iriczhao"); MODULE_LICENSE("GPL");
(2)module_2.c:调用module_1的wakeup_mythread()唤醒my_thread:
#include#include #include #include externvoidwakeup_mythread(void); staticintmodule2_init(void) { printk("wakeupmythread "); wakeup_mythread(); return0; } staticvoidmodule2_exit(void) { printk("module2_exitexiting "); } module_init(module2_init); module_exit(module2_exit); //定义模块相关信息 MODULE_AUTHOR("iriczhao"); MODULE_LICENSE("GPL");
将上述两份代码以模块方式构建。
从上图中可见:在加载module_1的时候,打印出了:
>>>>>>>>>>>>>>>>>>>>>>>>>>kthread>>>>>>>>>>>>>>>>>>>>>>>>
则证明执行kthread()函数创建了my_thread线程,但是该线程并没有唤醒执行,而由于在kthread()函数中调用schedule()让出了cpu,故而后面的代码没有执行。
当在加载module_2后,则唤醒了my_thread线程,打印出了:
>>>>>>>>>>>>>>>>>>>>>>>>>>I'mrunning>>>>>>>>>>>>>>>>>>>>>>>>
然后执行my_thread的线程函数,每隔一秒打印出:
my_threadrunning
综上,也验证了,当在kthread()中调用schedule()时执行线程调度,让出了cpu,当唤醒创建的线程时,会继续执行schedule()后面的代码。
四、总结与补充
(1)kthread_create()函数
通过以上代码分析,可见最重要的是kthread_create_list这个全局链表。当使用kthread_create()函数创建线程时,最终都会将线程相关资源添加到kthread_create_list链表中。如下代码(/include/linux/kthread.h):
#definekthread_create(threadfn,data,namefmt,arg...) kthread_create_on_node(threadfn,data,-1,namefmt,##arg)
由create_kthread()可知,通过kthread_create()入口进来的内核线程创建路径都具有统一的线程函数kthread()。
而linux内核的2号线程kthreadd正好负责内核线程的调度和管理。所以说创建的内核线程都是直接或间接的以kthreadd为父进程。
(2)kthread_create与kernel_thread的区别
在(init/mian.c)中,1号init线程和2号kthreadd线程都是通过kernel_thread()函数创建的,那么kernel_thread()后续会调用do_fork()实现线程相关的创建操作。kernel_thread()函数与kthread_create()创建的内核线程有什么区别呢?
1、kthread_create()创建的内核线程有干净的上下文环境,适合于驱动模块或用户空间的程序创建内核线程使用,不会把某些内核信息暴露给用户空间程序。
2、二者创建的进程的父进程不同: kthread_create()创建的进程的父进程被指定为kthreadd, 而kernel_thread()创建的进程的父进程可以是init或其他内核线程。
(3)kthread_run()函数
kthread_run()函数用于创建并唤醒一个线程,其本质上是调用kthread_create()创建一个线程,并使用wake_up_process()唤醒该线程。定义如下:
#definekthread_run(threadfn,data,namefmt,...) ({ structtask_struct*__k =kthread_create(threadfn,data,namefmt,##__VA_ARGS__); if(!IS_ERR(__k)) wake_up_process(__k); __k; })
(4)linux内核创建线程的整体过程
综上,linux内核创建线程的整体过程为:
1、创建kthread_create_info结构,为其分配空间,指定线程函数,线程相关描述数据等。
2、将线程的kthread_create_info结构添加到kthread_create_list全局线程创建链表中,并唤醒2号kthreadd线程。
3、2号kthreadd线程将从kthread_create_list全局线程创建链表中取出每一个kthread_create_info结构,然后调用create_kthread()函数创建一个线程函数为kthread的线程。在kthread线程函数中将执行创建线程指定的线程函数。
五、附录
【附录一】
kthread_create_on_cpu()创建一个绑定CPU的线程:
/** *kthread_create_on_cpu-Createacpuboundkthread *@threadfn:thefunctiontorununtilsignal_pending(current). *@data:dataptrfor@threadfn. *@cpu:Thecpuonwhichthethreadshouldbebound, *@namefmt:printf-stylenameforthethread.Formatisrestricted *to"name.*%u".Codefillsincpunumber. * *Description:Thishelperfunctioncreatesandnamesakernelthread *Thethreadwillbewokenandputintoparkmode. */ structtask_struct*kthread_create_on_cpu(int(*threadfn)(void*data), void*data,unsignedintcpu, constchar*namefmt) { structtask_struct*p; p=kthread_create_on_node(threadfn,data,cpu_to_node(cpu),namefmt, cpu); if(IS_ERR(p)) returnp; set_bit(KTHREAD_IS_PER_CPU,&to_kthread(p)->flags); to_kthread(p)->cpu=cpu; /*ParkthethreadtogetitoutofTASK_UNINTERRUPTIBLEstate*/ kthread_park(p); returnp; }
【附录二】
kthread_create_on_node()函数将操作kthread_create_list链表。kthread_create_on_node()函数的功能是:创建kthread,并将其添加到 kthread_create_list线程创建链表中,并返回对应的task_struct。
structtask_struct*kthread_create_on_node(int(*threadfn)(void*data), void*data,intnode, constcharnamefmt[], ...) { DECLARE_COMPLETION_ONSTACK(done); structtask_struct*task; structkthread_create_info*create=kmalloc(sizeof(*create), GFP_KERNEL); if(!create) returnERR_PTR(-ENOMEM); create->threadfn=threadfn; create->data=data; create->node=node; create->done=&done; spin_lock(&kthread_create_lock); /*注意这个全局链表kthread_create_list,所有通过kthread_create创建的内核线程都会挂在这*/ list_add_tail(&create->list,&kthread_create_list); spin_unlock(&kthread_create_lock); /*这是最重要的地方,从代码看是唤醒了kthreadd_task这个进程,该进程就是内核中的1号进程kthreadd 。因为kthreadd_task在rest_init()中通过find_task_by_pid_ns(pid, &init_pid_ns);进行了linux内核的早期赋值*/ wake_up_process(kthreadd_task); /* *Waitforcompletioninkillablestate,forImightbechosenby *theOOMkillerwhilekthreaddistryingtoallocatememoryfor *newkernelthread. */ if(unlikely(wait_for_completion_killable(&done))){ /* *IfIwasSIGKILLedbeforekthreadd(ornewkernelthread) *callscomplete(),leavethecleanupofthisstructureto *thatthread. */ if(xchg(&create->done,NULL)) returnERR_PTR(-EINTR); /* *kthreadd(ornewkernelthread)willcallcomplete() *shortly. */ wait_for_completion(&done); } task=create->result; if(!IS_ERR(task)){ staticconststructsched_paramparam={.sched_priority=0}; va_listargs; va_start(args,namefmt); vsnprintf(task->comm,sizeof(task->comm),namefmt,args); va_end(args); /* *rootmayhavechangedour(kthreadd's)priorityorCPUmask. *Thekernelthreadshouldnotinherittheseproperties. */ sched_setscheduler_nocheck(task,SCHED_NORMAL,¶m); set_cpus_allowed_ptr(task,cpu_all_mask); } kfree(create); returntask; }
kthread_create_on_node()函数的本质则是创建kthread_create_info结构,并将其添加到kthread_create_list全局链表中(/kernel/kthread.h):
structkthread_create_info { /*从kthreadd传递给kthread()的信息*/ int(*threadfn)(void*data); void*data; intnode; /*从kthreadd返回给kthread_create()的结果*/ structtask_struct*result; structcompletion*done; structlist_headlist; };
审核编辑:刘清
-
Module
+关注
关注
0文章
63浏览量
12837 -
LINUX内核
+关注
关注
1文章
316浏览量
21597 -
调度器
+关注
关注
0文章
98浏览量
5227
原文标题:毛毛雨,linux内核线程就这样诞生了么?
文章出处:【微信号:嵌入式小生,微信公众号:嵌入式小生】欢迎添加关注!文章转载请注明出处。
发布评论请先 登录
相关推荐
评论