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主机控制器上的消息,将这些消息内容的通过硬件一个个的发送出去。

我们在内核编程中也可以通过内核提供的接口函数去使用这种机制,步骤如下:

  1. 使用kthread_init_worker初始化一个kthread_worker结构
  2. 调用kthread_run创建一个处理kthread_worker的内核线程,线程的执行函数一定是kthread_worker_fn
  3. 使用kthread_work_init初始化一个kthread_work,并指定它的处理函数
  4. 当有kthread_work要处理时,调用kthread_queue_work将kthread_work挂接到kthread_worker上

kthread_worker和kthread_work机制的更多相关文章

  1. 笔记:Binder通信机制

    TODO: 待修正 Binder简介 Binder是android系统中实现的一种高效的IPC机制,平常接触到的各种XxxManager,以及绑定Service时都在使用它进行跨进程操作. 它的实现基 ...

  2. JAVA回调机制(CallBack)详解

    序言 最近学习java,接触到了回调机制(CallBack).初识时感觉比较混乱,而且在网上搜索到的相关的讲解,要么一言带过,要么说的比较单纯的像是给CallBack做了一个定义.当然了,我在理解了回 ...

  3. 谈谈DOMContentLoaded:Javascript中的domReady引入机制

    一.扯淡部分 回想当年,在摆脱写页面时js全靠从各种DEMO中copy出来然后东拼西凑的幽暗岁月之后,毅然决然地打算放弃这种处处“拿来主义”的不正之风,然后开启通往高大上的“前端攻城狮”的飞升之旅.想 ...

  4. 路由的Resolve机制(需要了解promise)

    angular的resovle机制,实际上是应用了promise,在进入特定的路由之前给我们一个做预处理的机会 1.在进入这个路由之前先懒加载对应的 .js $stateProvider .state ...

  5. Android权限管理之Permission权限机制及使用

    前言: 最近突然喜欢上一句诗:"宠辱不惊,看庭前花开花落:去留无意,望天空云卷云舒." 哈哈~,这个和今天的主题无关,最近只要不学习总觉得生活中少了点什么,所以想着围绕着最近面试过 ...

  6. Java学习之反射机制及应用场景

    前言: 最近公司正在进行业务组件化进程,其中的路由实现用到了Java的反射机制,既然用到了就想着好好学习总结一下,其实无论是之前的EventBus 2.x版本还是Retrofit.早期的View注解框 ...

  7. .NET Core采用的全新配置系统[10]: 配置的同步机制是如何实现的?

    配置的同步涉及到两个方面:第一,对原始的配置文件实施监控并在其发生变化之后从新加载配置:第二,配置重新加载之后及时通知应用程序进而使后者能够使用最新的配置.要了解配置同步机制的实现原理,先得从认识一个 ...

  8. Go结构体实现类似成员函数机制

    Go语言结构体成员能否是函数,从而实现类似类的成员函数的机制呢?答案是肯定的. package main import "fmt" type stru struct { testf ...

  9. 操作系统篇-分段机制与GDT|LDT

    || 版权声明:本文为博主原创文章,未经博主允许不得转载. 一.前言     在<操作系统篇-浅谈实模式与保护模式>中提到了两种模式,我们说在操作系统中,其实大部分时间是待在保护模式中的. ...

随机推荐

  1. PWA All In One

    PWA All In One chrome://apps/ PWA Progressive Web App 可安装,添加到主屏 离线使用 轻量,快速 基于 Web 技术一套代码多端复用(移动端,桌面端 ...

  2. vue3 deep dive

    vue3 deep dive vue core vnode vue core render / mount / patch refs https://www.vuemastery.com/course ...

  3. Enums & JavasScript & TypeScript

    Enums & JavasScript & TypeScript ES6 & TS https://www.typescriptlang.org/docs/handbook/e ...

  4. Flutter: 设置简单的启动屏

    更多代码参考 有短暂的白屏时间 import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter ...

  5. NGK公链如何构建区块链数字经济商业帝国?

    2020年对于区块链市场来说,重大的利好消息莫过于NGK公链的上线了.NGK公链其广泛的市场前景.顶尖的技术,一直备受众多大型机构以及投资者所看好.同时,NGK公链也不负众望,在上线以后,就开始落地到 ...

  6. ASP.NET Core中如何对不同类型的用户进行区别限流

    老板提出了一个新需求,从某某天起,免费用户每天只能查询100次,收费用户100W次. 这是一个限流问题,聪明的你也一定想到了如何去做:记录用户每一天的查询次数,然后根据当前用户的类型使用不同的数字做比 ...

  7. JAVA 8 新特性实用总JAVA 8 新特性实用总结结

    JAVA 8 新特性实用总结 作为一个工作两年多的 老 程序猿,虽然一开始就使用 jdk1.8 作为学习和使用的版本,随着技术的迭代,现有的 JDK 版本从两年前到现在,已经飞速发展到了 JDK 15 ...

  8. 小记一下递归通过id寻找一条链路的数据

    Array.prototype.remove = function(val) { var index = this.indexOf(val); if (index > -1) { this.sp ...

  9. 阿里云linux安装nginx,亲测有效

    系统平台:CentOS release 6.6 (Final) 64位. 一.安装编译工具及库文件 yum -y install make zlib zlib-devel gcc-c++ libtoo ...

  10. Docker备份迁移

    目录 Docker备份迁移 1.容器保存为镜像 2.镜像打包成压缩文件 3.把压缩文件恢复成镜像 Docker备份迁移 1.容器保存为镜像 将已经装好各种软件的容器再次打包为镜像,这样下次直接装这个镜 ...