内核线程源码分析(一)
线程是什么?按百度百科的定义:线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的执行流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。此文介绍Linux线程的基本逻辑,线程的主要数据结构,和线程的内核接口。
Linux线程基本逻辑
Linux内核在启动的时候是没有线程概念的,当内核初始化完成后将启动一系列的线程,之后,CPU的执行流就绑定在一个线程中运行了。如果绑定的是进程中的线程,那么执行的是进程的代码,如果绑定的是内核线程,将执行内核的服务代码。
线程是CPU执行与调度最基本的单位,每一个线程创建之初都是内核线程;创建之后如果与具体的进程上下文绑定,那线程就成了用户线程;如果此线程是进程的第一个线程,那么称之为进程的主线程;其实,在CPU执行和调度中并没有本质上的区别。
在用户角度,执行一个个程序就是创建一个个进程的主线程。当主线程创建时,同时创建用户空间,打开输入输出资源,载入依赖库等。主线程创建好后开始运行,进程也就同时存在了,这时程序可以根据需要创建用户线程,用户线程创建好后,共享主线程的用户空间,文件资源,依赖库等,用户线程退出后,主线程不会退出,进程也不会退出。当主线程退出时,内核开始关闭打开的文件,释放相关资源,主线程退出后进程也就退出了。这时,即使用户线程只执行到一半也无济于事了。
线程主要数据结构
线程结构体的数据元素多达几十项,全部学习和理解也没有必要,主要的数据结构如下图所示:
struct task_struct
线程结构体,包含整个线程相关的所有数据,里面的元素比较多,我们只说说和线程创建、调度相关的元素。当线程创建的时候,首先从高速内存kmem_cache中分配task_struct所需的空间,然后从内存分配器中再分配4个连续的页面作为堆栈区,接着从创建者那里复制而不是从头创建大部分的数据,最后设置相应的线程调度器,这样线程对象就创建好了。当下一次发生调度的时候,CPU就知道如果调度此线程了。
struct sched_class
此结构体定义了线程调度器使用的函数,与C++中的基类一样,这些接口都是函数指针类型,由具体的调度器实现。比如实时优先级调度器rt_sched_class和一般优先级调度器fair_sched_class,他们的实现文件分别在kernel/sched/rt.c和kernel/sched/fair.c中,我们讲到线程调度的时候再进去看具体的代码。
struct thread_struct
此结构体是线程相关上下文的具体实现,由于每种处理器对线程的运行方式不一样,所以每种CPU体系下都定义有各自的线程上下文结构体。arm64体系的定义如下图所示:
65-80行,保存相关寄存器的值。
83行,与线程相关的TLS寄存器的值。
线程内核接口
Linux线程的实现相当复杂,但使用却比较简单。使用接口封装成了几个函数,其他模块只需直接使用这些函数,比如只需调用一句宏即可,如图:
在需要使用线程的地方,定义一个线程函数:
int (*threadfn)(void *data)
然后调用kthread_run宏创建并运行线程,线程函数的返回值就是线程的返回值。
31行,此宏即为线程的内核接口宏,其他模块只需直接调用。
threadfin,线程函数,即线程创建好后执行的函数。
data,线程参数,传入线程函数的参数,由调用者传入。
namefmt,线程的名字,参数及后续参数是printf风格的。内核线程的名字,可以在shell中使用ps命令查看到。
34,13行,同样是宏,最终调用kthread_create_on_node函数实现。
7行,__printf(4, 5)是gcc扩展语法,展开后为__attribute__((format(printf, 4, 5))),使编译器检查函数声明和函数实际调用参数之间的格式化字符串是否匹配。这里,告诉编译器检查kthread_create_on_node函数的第4个和第5个参数是否按printf的格式使用,如果调用者没有按此格式调用,则编译报错。
8行,线程创建并运行的函数,用户也可以绕过接口宏,直接调用。此函数多了一个参数node,指定线程该分配的CPU节点,在SMP系统中,通常设置为NUMA_NO_NODE。
以上,介绍了Linux内核线程的基本逻辑,线程的主要数据结构,和线程的内核接口,线程的实际创建及调度另文再述。
相关文章