Linux内核实现透视---kthread_work
内核线程工作队列
内核线程工作队列和普通工作队列看着十分相似,很多抽象概念如work和worker等都很相同并且执行对象也都是内核线程。不同的是内核线程工作队列没有普通工作队列的线程池概念一个 worker(工作者(工作组))对应到一个实际的内核线程,这个内核线程会按顺序依次执行worker上的每一个work。这个worker 对应的内核线程是当前CPU节点上的,因此可以推导出这个worker下的work都是在同一个CPU上执行的--TODO1:负载均衡会迁移吗?来看下数据结构:
工作组(工作者)
struct kthread_worker {
unsigned int flags;
// 队列操作的锁
spinlock_t lock;
// work 队列
struct list_head work_list;
// delay work 队列
struct list_head delayed_work_list;
//worker 对应的内核线程
struct task_struct *task;
// 当前正在处理的work
struct kthread_work *current_work;
};
一个工作(work)
struct kthread_work {
struct list_head node;
kthread_work_func_t func;
struct kthread_worker *worker;
/* Number of canceling calls that are running at the moment. */
int canceling;
};
一个工作组中包括两个工作list,一个是普通的工作list另一个是延时执行的工作队列。使用时user可以自己定义内核线程工作者线程的实现,而默认的内核工作队列处理程序由内核在kernel\kthread.c 文中实现在kthread_worker_fn 中如下,当调用kthread_create_worker_on_cpu 创建内核线程时实际上这个就是这个内核线程的执行体,当然也可以通过kthread_run 宏函数并将这个函数作为第一个入参传入,此时内核线程的执行体也是如下的接口。
内核线程工作队列处理
int kthread_worker_fn(void *worker_ptr)
{
struct kthread_worker *worker = worker_ptr;
struct kthread_work *work;
/*
* FIXME: Update the check and remove the assignment when all kthread
* worker users are created using kthread_create_worker*() functions.
*/
WARN_ON(worker->task && worker->task != current);
worker->task = current;
//休眠相关的
if (worker->flags & KTW_FREEZABLE)
set_freezable();
//未持有锁时内核线程可以被中断
repeat:
set_current_state(TASK_INTERRUPTIBLE); /* mb paired w/ kthread_stop */
//内核线程要停止必须检查这一项,负责就仅能使用使能信号处理的机制停止内核线程
if (kthread_should_stop()) {
__set_current_state(TASK_RUNNING);
spin_lock_irq(&worker->lock);
worker->task = NULL;
spin_unlock_irq(&worker->lock);
return 0;
}
//寻找第一个加入队列的work
work = NULL;
spin_lock_irq(&worker->lock);
if (!list_empty(&worker->work_list)) {
work = list_first_entry(&worker->work_list,
struct kthread_work, node);
list_del_init(&work->node);
}
worker->current_work = work;
spin_unlock_irq(&worker->lock);
//找到了第一个work开始处理
if (work) {
__set_current_state(TASK_RUNNING);
work->func(work);
//没有work需要处理则放弃时间片
} else if (!freezing(current))
schedule();
//未使能KTW_FREEZABLE时可能调用这里
try_to_freeze();
cond_resched();
goto repeat;
}
这是内核工作线程的工作的处理过程,细心点就挥发下这里仅仅处理了普通的work,而delay_workw未进行处理。实际上delay work不需要内核线程处理,相反的延时工作的调用依赖内核的定时器timer由定时器到期时回调从而进行处理。
使用
内核线程的工作队列使用时可以直接使用内核定义的worker,也可以自己创建对应的kworker 使用独立内核线程处理works 从而保证work的及时性。
示例:
struct kthread_worker kworker;
struct task_struct *kworker_task;
struct kthread_work work;
//初始化工作组
kthread_init_worker(&kworker);
//创建一个内核线程并运行kthread_worker_fn 参数是kworker 后面的参数是线程名称
kworker_task = kthread_run(kthread_worker_fn, &kworker,"%s", "xxxx");
//初始化线程
kthread_init_work(&work, kthread_work_func_t fun);
//向工作组添加一个工作,这个接口可能唤醒内核线程,执行完成后这个工作就被删除了
kthread_queue_work(kworker, &work);
//添加一个延时内核工作
kthread_queue_delayed_work(kworker, &work,delay)
//停止内核线程,依赖内核线程中调用(kthread_should_stop)检查,这个接口会阻塞直到内核线程退出
kthread_stop(kworker_task)
内部实现
- kworker的创建
当新建一个内核工作线程时,如kthread_run 接口,此时会唤醒内内核的kthreadadd 线程,有这个线程完成新内核工作线程的创建。 - kworker 销毁
当驱动 退出需要销毁kthread时,通过调用 kthread_destroy_worker接口来完成,这个接口的内部实现流程是
a. flush 这个worker queue上的所有work
b. 停止worker对应的线程
c. 释放worker mem
a 步骤的实现就是通过在当前worker上添加一个特殊的work,等待这个work处理完成,此时就说明所有的work都被处理完成了。然后就可以执行后续
的销毁动作。
API
除了上面的经常使用API接口外还有一些不常用的API 接口这里简单记录下
//释放进程控制块内存
void free_kthread_struct(struct task_struct *k);
//将线程绑定到指定cpu上
void kthread_bind(struct task_struct *k, unsigned int cpu);
//将线程绑定到几个cpu上用掩码
void kthread_bind_mask(struct task_struct *k, const struct cpumask *mask);
//标记内核线程停止并等待线程停止
int kthread_stop(struct task_struct *k);
//检查线程是否要停止
bool kthread_should_stop(void);
//检查线程是否要park
bool kthread_should_park(void);
//睡眠使用停止模式
bool kthread_freezable_should_stop(bool *was_frozen);
//获取线程data
void *kthread_data(struct task_struct *k);
//
void *kthread_probe_data(struct task_struct *k);
//标记线程park
int kthread_park(struct task_struct *k);
//取消标记park
void kthread_unpark(struct task_struct *k);
// 等待park标记是否清除
void kthread_parkme(void);
//内核线程的管理接口
int kthreadd(void *unused);
总结
总的来说内核线程队列的实现和普通工作队列[https://www.cnblogs.com/w-smile/p/13499279.html]的相比实现上简单的一些。使用内核线程的更加清晰明了,内核在kthreadd维护了内核线程队列,各个worker的执行体维护了自己的work list。内核线程工作队列和普通工作队列相比没有线程池的概念因此内核线程工作队列中的work是不支持并发,因此设计work 函数时不用考虑重入的场景。普通工作队列的的实现上增加了工作组的线程池的概念从而支持并发(不同work并发),二者的区别和共同点都很明显。
共同点:本质上都是内核线程来处理的;也都支持延迟工作。
不同点:增加了线程池的管理,所以工作队列中work可能会被多个内核线程处理,因此会出现work(不同work)并发,同时消耗的系统资源也更加多,但是对于多work并发的能力更强。