kthread_worker和kthread_work机制
1、概述
在阅读内核源码时,可以看到kthread_worker、kthread_work两个数据结构配合内核线程创建函数一起使用的场景。刚开始看到这块时,比较困惑,紧接着仔细分析源码后,终于弄清楚了其中的机制,也不由的感叹内核的设计者内功之深厚以及生活处处皆学问。其实,这块使用机制就是抽象了现实生活中的经常看到的现象——工人(worker)和工作(work)间的关系。我们知道,在上班期间每个工人会被不定时的分配到一些不同的工作,当有工作来了,工人会对自己接到的工作进行处理,当手头上的工作做完后可以进行稍微的休息,等待后面任务的到来时再继续的处理接到的工作。
内核中要实现这种机制其实也不难,内核线程创建函数创建一个内核线程,该线程模仿工人的这个特性,它去判断属于这个线程的kthread_worker中是否有要处理的kthread_work,如果有,就取出这个kthread_work,然后调用kthread_work上面指定的处理函数,如果没有这个线程就进行休眠,当有新的kthread_work添加到kthread_worker上时,会再次唤醒kthread_worker的处理线程重复上述工作。
2、内核使用场景
前面总的概况了kthread_worker和kthread_work这一机制,让大家有了个大致的了解,下面结合内核中对这一机制的实际使用场景的源码再进行详细的分析。
内核在SPI驱动的SPI主机控制器这块使用了这一机制,先来看下源码中对kthread_worker、kthread_work这一机制进行初始化的函数
static int spi_init_queue(struct spi_controller *ctlr) { ... kthread_init_worker(&ctlr->kworker); ctlr->kworker_task = kthread_run(kthread_worker_fn, &ctlr->kworker, "%s", dev_name(&ctlr->dev)); ... kthread_init_work(&ctlr->pump_messages, spi_pump_messages); ... }
spi_init_queue函数中调用了kthread_init_worker、kthread_run和kthread_init_work函数,下面按照顺序依次对这三个函数进行分析
2.1 kthread_init_worker函数
#define kthread_init_worker(worker) \ do { \ static struct lock_class_key __key; \ __kthread_init_worker((worker), "("#worker")->lock", &__key); \ } while (0)
void __kthread_init_worker(struct kthread_worker *worker, const char *name, struct lock_class_key *key) { memset(worker, 0, sizeof(struct kthread_worker)); raw_spin_lock_init(&worker->lock); lockdep_set_class_and_name(&worker->lock, key, name); INIT_LIST_HEAD(&worker->work_list); INIT_LIST_HEAD(&worker->delayed_work_list); }
kthread_init_worker是一个宏,它内部接着调用了__kthread_init_worker,主要的工作是在__kthread_init_worker函数中完成的
__kthread_init_worker函数初始化传入的kthread_worker结构体的成员变量,如初始化了该结构体内部的链表和自旋锁
2.2 kthread_run函数
#define kthread_run(threadfn, data, namefmt, ...) \ ({ \ struct task_struct *__k \ = kthread_create(threadfn, data, namefmt, ## __VA_ARGS__); \ if (!IS_ERR(__k)) \ wake_up_process(__k); \ __k; \ })
kthread_run也是一个宏,它内部调用kthread_create创建一个内核线程。当内核线程创建成功后,立刻调用wake_up_process去唤醒创建的这个线程,让创建的线程进入就绪态等待内核调度
thread_fn这个参数传入的是这个内核线程要执行的函数,data是传给内核线程函数的参数。在上面spi_init_queue函数中,thread_fn参数对应的是kthread_worker_fn,data对应的是spi_controller结构中的kthread_worker类型的对象
这里的传入的kthread_worker_fn是由内核实现的函数,我们来看下它的作用是否和我们前面想象的一样,取出kthread_worker中挂接的kthread_work,并调用每个kthread_work中指定的处理函数
int kthread_worker_fn(void *worker_ptr) { struct kthread_worker *worker = worker_ptr; struct kthread_work *work; ... repeat: ... work = NULL; raw_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; raw_spin_unlock_irq(&worker->lock); if (work) {---------------------------------------->③ __set_current_state(TASK_RUNNING); work->func(work); } else if (!freezing(current)) schedule(); ... goto repeat; ------------------------------------>④ }
① 判断kthread_worker上是否有要处理的kthread_work,kthread_work挂接kthread_worker的work_list链表上
② 从kthread_worker的work_list链表中删除①中取出kthread_work节点
③ 如果在kthread_worker的work_list链表中找到了kthread_work,就执行kthread_work上的处理函数,如果没有,就让本线程进入休眠状态
④ 跳转到repeat处,重复执行①~④之间操作
果然kthread_worker_fn函数和我们之前预想的一样,从kthread_worker中取出挂接的kthread_work,并调用每个kthread_work中指定的处理函数
2.3 kthread_init_work函数
#define kthread_init_work(work, fn) \ do { \ memset((work), 0, sizeof(struct kthread_work)); \ INIT_LIST_HEAD(&(work)->node); \ (work)->func = (fn); \ } while (0)
kthread_init_work和kthread_init_worker一样,也是一个宏,它有两个参数,第一个参数work传入的是要初始化的kthread_work结构,第二个参数fn传入的是给kthread_work指定的处理函数。kthread_init_work初始化tkhread_work,设置了tkread_work的处理函数。
前面虽然初始化了kthread_worker、kthread_work结构,创建了处理kthread_worker的内核线程,但似乎还不够完整,还没有向kthread_worker中添加kthread_work呢?
创建的内核线程只有kthread_worker中挂接有kthread_work后,内核线程被唤醒工作,那么我们如何将kthread_work挂接到kthread_worker中并且唤醒休眠的内核线程呢?同样,内核中也提供了相应的函数,我们来看SPI的驱动中是怎么用的。
__spi_queued_transfer:
static int __spi_queued_transfer(struct spi_device *spi, struct spi_message *msg, bool need_pump) { struct spi_controller *ctlr = spi->controller; ... list_add_tail(&msg->queue, &ctlr->queue); if (!ctlr->busy && need_pump) kthread_queue_work(&ctlr->kworker, &ctlr->pump_messages);----------->① ... }
当SPI设备驱动程序访问SPI设备时最终会调用到__spi_queued_transfer函数,该函数将构造的消息挂入SPI主机控制器的链表上,然后调用kthread_queue_work将kthread_work挂入到kthread_woke链表中。
①中的ctlr->kworker和ctlr->pump_messages是描述SPI主机控制器的结构体里的成员,其实对应的就是kthread_worker和kthread_work类型数据结构。
可以看出kthread_queue_work就是我们想要找将wok(工作)交给worker(工人)的函数,下面我们来进行详细分析
2.2 kthread_queue_work函数
bool kthread_queue_work(struct kthread_worker *worker, struct kthread_work *work) { ... if (!queuing_blocked(worker, work)) { kthread_insert_work(worker, work, &worker->work_list); ret = true; } ... }
kthread_queue_work函数中继续调用了kthread_insert_work函数,具体的工作是在这个函数中完成了,我们直接来看kthread_insert_work这个函数
static void kthread_insert_work(struct kthread_worker *worker, struct kthread_work *work, struct list_head *pos) { kthread_insert_work_sanity_check(worker, work); list_add_tail(&work->node, pos);-------------------->① work->worker = worker; if (!worker->current_work && likely(worker->task)) wake_up_process(worker->task);---------------->② }
① 将传入的kthread_work挂接到对应的kthread_worker链表中
② 调用wake_up_process函数,唤醒休眠的处理kthread_worker的内核线程
可以看出,当有work要处理时,直接调用kthread_queue_work将work交给所属的worker,worker的处理线程会被唤醒去处理它里面的work
3、小结
好了,到此我们也将内核中kthread_worker和kthread_work机制讲解清楚了。这里引用了内核中SPI驱动对该机制的使用,如果想更深入的了解可以去阅读driver/spi目录下的内容。SPI驱动中巧妙的运用了这一机制,在后面未讲解的kthread_work的处理函数spi_pump_messages中,它会不断的去取出SPI设备驱动程序挂接到SPI主机控制器上的消息,将这些消息内容的通过硬件一个个的发送出去。
我们在内核编程中也可以通过内核提供的接口函数去使用这种机制,步骤如下:
- 使用kthread_init_worker初始化一个kthread_worker结构
- 调用kthread_run创建一个处理kthread_worker的内核线程,线程的执行函数一定是kthread_worker_fn
- 使用kthread_work_init初始化一个kthread_work,并指定它的处理函数
- 当有kthread_work要处理时,调用kthread_queue_work将kthread_work挂接到kthread_worker上