Linux kernel thread 内核线程
Linux中无论是用户态进程、线程,还是内核线程,在内核中都使用同样的结构
task_struct
,可以看出内核都是以
任务
这个概念对待这些不同名字的事物。至于为什么会有kernel thread或者说内核线程这个词,个人认为应该是因为所有内核线程共享内核空间资源,因此有线程之名。
本文内容参考内核版本 3.10.0-862.el7.x86_64
例子
因为内核线程上下文不属于中断上下文,因此可以使用调度类睡眠操作主动让出cpu,这点在输出中可以看到在没有设定cpu亲和的情况下存在唤醒在不同cpu的情况。
1 |
|
1 |
obj-m := kthread_test.o |
1 |
[57272.252152] my init. |
内核线程 api
以下api位于include/linux/kthread.h。
kthread_create_on_node
创建一个内核线程执行指定的函数,从指定numa node上分配内存,如果不指定则传入-1。内核线程创建完毕后进入
TASK_UNINTERRUPTIBLE
状态并让出cpu,等待人为唤醒。
kthread_create
kthread_create_on_node
的一个宏包装,numa node指定为-1。
kthread_run
调用
kthread_create
创建内核线程后调用
wake_up_process
唤醒该线程。
kthread_stop
通知一个内核线程可以停止运行,并等待其停止。这个函数不是强制性的,需要线程内自身代码检查
kthread_should_stop
返回是否为真并主动返回或退出。如果线程函数自身调用了
do_exit
,那么需要
kthread_stop
的调用者确保线程的task_struct结构内存依然被持有,否则会访问无效内存。
kthread_should_stop
线程内自身调用该函数检查是否外部调用了
kthread_stop
。
kthread_freezable_should_stop
应用于可以冻结的线程,由内核线程自身调用,当系统处于挂起状态时,该函数可以冻结自身直到挂起状态解除。参数的引用用于表示是否从冻结状态返回,返回值与
kthread_should_stop
一致。
kthread_park
通知内核线程进入park(停靠,可以理解为暂定)状态,并等待其park完成。这个函数同样不是强制性的,需要线程自身代码检查
kthread_should_park
返回真后调用
kthread_parkme
将自身置为park状态并让出cpu,直到park状态解除。
kthread_should_park
线程自身调用该函数检查是否应当进入park状态。
kthread_parkme
线程自身调用该函数令自身进入park状态。
kthread_unpark
与
kthread_park
对应,解除线程park状态。
kthread_data
返回内核线程创建时设定的运行函数的参数。调用者需要确保传入的task_struct的确是一个kthread。
probe_kthread_data
返回内核线程创建时设定的运行函数的参数。如果传入的task_struct不是一个kthread或参数无法访问那么返回NULL。
kthread_bind
将一个内核线程绑定到指定的cpu上。该线程必须处于
TASK_UNINTERRUPTIBLE
状态,
kthread_create
刚刚创建的线程符合该要求。获取cpu相关的api在include/linux/cpumask.h文件中,numa node相关api在include/linux/nodemask.h中,用户态查看cpu和numa node相关信息可以使用命令
lscpu
和
cat /proc/cpuinfo
。
生命周期流程
内核线程创建
内核线程的创建与中断处理的处理思路很相似,尽量将占用cpu时间长的处理逻辑推到下半部在另外的时间执行,尽快完成上半部操作,以让出cpu执行优先级更高的任务。
上半部
通过上文的api可以看到创建内核线程最后都会落到函数
kthread_create_on_node
,这个函数的逻辑很简单:
kthread_create_info
类型结构体,填充必要成员,将其放入队列
kthread_create_list
中
kthreadd
线程(
kthreadd_task
就是
kthreadd
守护线程的
task_struct
)
kthread_create_info
中成员done标记被设置(这里将由
kthreadd
守护线程设置)
task_struct
结构。设置线程名、调度策略和运行cpu。
task_struct
结构体指针。此时新内核线程处于
TASK_UNINTERRUPTIBLE
状态等待被
wake_up_process
。
1 |
/** |
这里有一个结构体
kthread_create_info
,看一下。
1 |
struct kthread_create_info |
下半部
内核线程创建的下半部分工作在kthreadd守护线程中完成,这里介绍其创建位置及工作逻辑。
kthreadd是一个内核守护线程,pid为2,用于处理创建内核线程的请求,是其他内核线程的父线程。一个例外是1号线程,后面可以看到为什么。(也可以不通过kthreadd创建内核线程,
kernel_thread
函数就用于创建内核线程,kthreadd也是调用该函数,但是并不推荐直接使用,而且此文参考的内核版本没有导出该符号,内核模块无法直接调用)
kthreadd的启动代码调用路径是 start_kernel -> rest_init。再之前的部分涉及系统启动,这里不关注。
1 |
static noinline void __init_refok rest_init(void) |
先不关注kernel_thread的具体实现,只需要知道创建了新的线程并执行传入的函数指针就可以了。2号线程执行的函数为
kthreadd
。
1 |
int kthreadd(void *unused) |
可以看到
kthreadd
中调用
create_kthread
创建内核线程,参数为
kthread_create_info
结构体(位于
kthread_create_on_node
栈上内存)。继续看
create_kthread
。
1 |
static void create_kthread(struct kthread_create_info *create) |
可以看到
create_kthread
的代码很简单,调用
kernel_thread
创建一个内核线程并执行函数
kthread
。额外说一句
kthread_create_info
中类型为
task_struct
指针的成员result是在
kernel_thread
中创建并设置的。继续看
kthread
。
1 |
static int kthread(void *_create) |
1 |
struct kthread { |
到这里kthreadd的实现逻辑已经全部完成了。
内核线程停止
task_struct
结构体的引用计数,避免在内核线程停止后直接释放其内存,因为我们还需要其返回值。
kthread
函数上栈上的
kthread
结构体。
KTHREAD_SHOULD_STOP
。
kthread_should_stop
函数检查的就是该标记。
KTHREAD_SHOULD_PARK
,因为线程之前可能处于park状态。
kthread
->
do_exit
中对
task_struct
成员
vfork_done
的设置,该指针指向了这里的exited标识变量。
task_struct
结构体引用计数,根据情况释放其占用资源。