Linux(内核剖析):26---中断下半部之(工作队列机制(workqueue_struct、cpu_workqueue_struct))
发布日期:2021-06-29 22:33:06 浏览次数:3 分类:技术文章

本文共 5560 字,大约阅读时间需要 18 分钟。

一、工作队列概述

  • 工作队列(work queue)是另外一种将工作推后执行的形式,它和我们前面讨论的所有其他形式都不相同。工作队列可以把工作推后,交由一个内核线程去执行——这个下半部分总是会在进程上下文中执行。这样,通过工作队列执行的代码能占尽进程上下文的所有优势。最重要的就是工作队列允许重新调度甚至是睡眠
  • 通常,在工作队列和软中断/tasklet中做出选择非常容易。如果推后执行的任务需要睡眠,那么就选择工作队列。如果推后执行的任务不需要睡眠,那么就选择软中断或tasklet。实际上,工作队列通常可以用内核线程替换。但是由于内核开发者们非常反对创建新的内 线 (在有些场合,使用这种冒失的方法可能会吃到苦头),所以我们也推荐使用工作队列。当然,这种接口也的确很容易使用
  • 如果你需要用一个可以重新调度的实体来执行你的下半部处理,你应该使用工作队列。它是唯一能在进程上下文中运行的下半部实现机制,也只有它才可以睡眠。这意味着在你需要获得大量的内存时,在你需要获取信号量时,在你需要执行阻塞式的I/O操作时,它都会非常有用。如果你不需要用一个内核线程来推后执行工作,那么就考虑使用tasklet吧

二、工作队列的实现机制

工作者线程

  • 位于最高一层的是工作者线程
  • 系统允许有多种类型的工作者线程存在。对于指定的一个类型,系统的每个CPU上都有一个该类的工作者线程。内核中有些部分可以根据需要来创建工作者线程,而在默认情况下内核只有event这一种类型的工作者线程
  • 每个工作者线程都由一个cpu_ workequeue_struct结构体表示。而workqueue_struct结构体则表示给定类型的所有工作者线程
  • 演示说明:除系统默认的通用events工作者类型之外,我自己还加入了一种falcon工作者类型。 并且使用的是一个拥有四个处理器的计算机。那么,系统中现在有四个event类型的线程(因而也就有四个cpu_workqueue_sruct结构体)和四个falcon类型的线程(因而会有另外四个cpu_ workqueue_struct结构体)

workqueue_struct

  • 一个工作者线程类型,不论其在CPU上有多少个,其最终都只有一个workqueue_struct类型
  • 例如,在上面的演示说明中,有一个对应event类型的workqueue_struct和一个对应的falcon类型的workqueue_struct

工作(work_struct)

  • 工作处于最底层,让我们从这里开始。你的驱动程序创建这些需要推后执行的工作。它们用work_struct结构来表示
  • 这个结构体中最重要的部分是一个指针,它指向一个函数,而正是该函数负责处理需要推后执行的具体任务。工作会被提交给某个具体的工作者线程——在上面的演示说明中,就是特殊的falcon线程。然后这个工作者线程会被唤醒并执行这些排好的工作
  • 大部分驱动程序都使用的是现存的默认工作者线程。它们使用起來简单、方便。可是,在有些要求更严格的情况下,驱动程序需要自己的工作者线程。比如说XFS文件系统就为自己创建了两种新的工作者线程

三、工作队列的具体实现细节

①工作者线程(events/n)

  • 工作队列子系统是一个用于创建内核线程的接口,通过它创建的进程负责执行由内核其他部分排到队列里的任务。它创建的这些内核线程称作工作者线程(worker thread)。工作队列可以让你的驱动程序创建一个专门的工作者线程来处理需要推后的工作。不过,工作队列子系统提供了一个缺省的工作者线程来处理这些工作。因此,工作队列最基本的表现形式,就转变成了一个把需要推后执行的任务交给特定的/通用线程的这样一种接口
  • 缺省的工作者线程叫做events/n,这里n是处理器的编号;每个处理器对应一个线程。例如,单处理器的系统只有everits/0这样一个线程,而双处理器的系统就会多一个events/1线程。 缺省的工作者线程会从多个地方得到被推后的工作。许多内核驱动程序都把它们的下半部交给缺省的工作者线程去做。除非一个驱动程序或者子系统必须建立一个属于它自己的内核线程,否则最好使用缺省线程
  • 不过并不存在什么东西能够阻止代码创建属于自己的工作者线程。如果你需要在工作者线程中执行大量的处理操作,这样做或许会带来好处。处理器密集型和性能要求严格的任务会因为拥有自己的工作者线程而获得好处。此时这么做也有助于减轻缺省线程的负担,避免工作队列中其他需要完成的工作处于饥饿状态

②表示线程的数据结构(struct workqueue_struct、struct cpu_workqueue_struct)

  • 工作者线程用workqueue_struct结构表示:
    • 该结构内是一个由cpu_workqueue_stuct结构组成的数组,它定义在kernel/workqueue.c中, 数组的每一项对应系统中的一个处理器。由于系统中每个处理器对应一个工作者线程,所以对于给定的某台计算机来说,就是每个处理器,每个工作者线程对应一个这样的cpu_workqueue_ struct结构体

  • cpu_work queue_struct是kernel/workqueue.c中的核心数据结构:
    • 注意,每个工作者线程类型关联一个自己的workqueue_struct。在该结构体里面,给每个线程程分配一个cpu_workqueue_struct,因而也就是给每个处理器分配一个,因为每个处理器都有一个该类型的工作者线程

③表示工作的数据结构(struct work_struct)

  • 工作用<linux/workqueue.h>中定义的work_struct结构体表示:

  • 这些结构体被连接成链表,在每个处理器上的每种类型的队列都对应这样一个链表
  • 比如,每个处理器上用于执行被推后的工作的那个通用线程就有一个这样的链表。当一个工作者线程被唤醒时,它会执行它的链表上的所有工作。工作被执行完毕,它就将相应的work_struct对象从链表上移去。当链表上不再有对象的时候,它就会继续休眠

④工作者线程核心函数(worker_thread()、run_workqueue())

  • 所有的工作者线程都是用普通的内核线程实现的,它们都要执行worker_thread()函数。在它初始化完以后,这个函数执行一个死循环并开始休眠。当有操作被插入到队列里的时候,线程就会被唤醒,以便执行这些操作。当没有剩余的操作时,它又会继续休眠
  • 我们看一下worker_thread()函数的核心流程,简化如下,该函数在死循环中完成了以下功能:
    • 1.线程将自己设置为休眠状态(state被设成TASK_INTERRUPTIBLE),并把自己加入到等待队列中
    • 2.如果工作链表是空的,线程调用schedule()函数进入睡眠状态
    • 3.如果链表中有对象,线程不会睡眠。相反,它将自己设置成TASK_RUNNING,脱离等待队列
    • 4.如果链表非空,调用run_workqueue()函数执行被推后的工作

  • 下一步,由run_workqueue()函数来实际完成推后到此的工作,该函数循环遍历链表上每个待处理的工作,执行链表每个节点上的workqueue_struct中的func成员函数
    • 1.当链表不为空时,选取下一个节点对象
    • 2.获取我们希望执行的函数func及其参数data
    • 3.把该节点从链表上接下来,将待处理标志位pending清零
    • 4.调用函数
    • 5.重复执行

四、使用工作队列

  • 工作队列的使用非常简单。我们先来看一下缺省的events任务队列,然后再看看创建新的工作者线程

①创建推后的工作(DECLARE_WORK、INIT_WORK)

  • 静态创建:可以通过DECLARE_WORK在编译时静态地建该结构体:
    • 这样就会静态地创建一个名为name,处理函数为func,参数为data的work_stuct结构体

  • 动态创建:同样,也可以在运行时通过指针创建一个工作:
    • 这会动态地初始化一个由work指向的工作,处理函数为func,参数为data

②工作队列处理函数(work_handler)

  • 工作队列处理函数的原型是:

  • 这个函数会由一个工作者线程执行,因此,函数会运行在进程上下文中。默认情况下,允许响应中断,并且不持有任何锁。如果需要,函数可以睡眠
  • 需要注意的是,尽管操作处理函数运行在进程上下文中,但它不能访问用户空间,因为内核线程在用户空间没有相关的内存映射。通常在发生系统调用时,内核会代表用户空间的进程运行,此时它才能访问用户空间,也只有在此时它才会映射用户空间的内存
  • 在工作队列和内核其他部分之间使用锁机制就像在其他的进程上下文中使用锁机制一样方便。这使编写处理函数变得相对容易

③对工作进行调度/延时调度(schedule_work()、schedule_delayed_work())

  • schedule_work:现在工作已经被创建,我们可以调度它了。想要把给定工作的处理函数提交给缺省的events工作线程,只需调用:
    • work马上就会被调度,一旦其所在的处理器上的工作者线程被唤醒,它就会被执行

  • schedule_delayed_work:有时候你并不希望工作马上就被执行,而是希望它经过一段延迟以后再执行。在这种情况下,你可以调度它在指定的时间执行:
    • 这时,&work指向的work_struct直到delay指定的时钟节拍用完以后才会执行。在后面“内核同步”的文章将介绍这种使用时钟节拍作为时间单位的方法。

④刷新操作/取消延迟(flush_schedule_work()、cancel_delayed_work())

  • 排入队列的工作会在工作者线程下一次被唤醒的时候执行。有时,在继续下一步工作之前, 你必须保证一些操作已经执行完毕了。这一点对模块来说就很重要,在卸载之前,它就有可能需要调用下面的函数。而在内核的其他部分,为了防止竞争条件的出现,也可能需要确保不再有待处理的工作
  • 出于以上目的,内核准备了一个用于刷新指定工作队列的函数:
    • 该函数会一直等待,直到队列中所有对象都被执行以后才返回。在等待所有待处理的工作执行的时候,该函数会进入休眠状态,所以只能在进程上下文中使用它

  • 注意,该函数并不取消任何延迟执行的工作。就是说,任何通过schedule_deayed_work()调度的工作,如果其延迟时间未结束,它并不会因为调用flush_schedule_work()而被刷新掉。取消延迟执行的工作应该调用:
    • 这个函数可以取消任何与work_struct相关的挂起工作

五、创建新的工作队列(create_workqueue)

  • 如果缺省的队列不能满足你的需要,你应该创建一个新的工作队列和与之相应的工作者线程。由于这么做会在每个处理器上都创建一个工作者线程,所以只有在你明确了必须要靠自己的 一套线程来提高性能的情况下,再创建自己的工作队列

创建工作队列对应的工作者线程(create_workqueue())

  • 创建一个新的任务队列和与之相关的工作者线程,你只需调用一个简单的函数(name参数用于该内核线程的命名):

  • 比如,缺省的events队列的创建就调用的是:
    • 这样就会创建所有的工作者线 (系统中的每个处理器都有一个),并且做好所有开始处理工作之前的准备工作

  • 创建一个工作的时候无须考虑工作队列的类型
  • 在创建之后,可以调用下面列举的函数。这些函数与schedule_work()以及schedule_delayed_work()相近,唯一的区别就在于它们针对给定的工作队列而不是缺省的events队列进行操作

  • 最后,你可以调用下面的函数刷新指定的工作队列:
    • 该函数和前面讨论过的flush_scheduled_work()作用相同,只是它在返回前等待清空的是给定的队列

六、老的任务队列机制

  • 像BH接口被软中断和tasklet代替一样,由于任务队列接口存在的种种缺陷,它也被工作队列接口取代了。像tasklet—样,任务队列接口(内核中常常称tq)其实也和进程没有什么相关之处。任务队列接口的使用者在2.5开发版中分为两部分,其中一部分转向了使用tasklet,还有另外一部分继续使用任务队列接口。而目前任务队列接口剩余的部分已经演化成了工作队列接口。由干任务队列在内核中曾经使用过一段时间,出于了解历史的目的,我们对它进行一个大体回顾
  • 任务队列机制通过定义一组队列来撒娇哦厦门其功能。每个队列都有自己的名字,比如调度程序队列、立即队列和定时器队列。不同的队列在内核中的不同场合使用。keventd内核线程负责执行 调度程序队列的相关任务。它是整个工作队列接口的先驱。定时器队列会在系统定时器的每个时间节拍时执行,而立即队列能够得到双倍的运行机会,以保证它能“立即”执行。当然,还有其他一些队列。此外,你还可以动态地创建自己的新队列
  • 这些听起来都挺有用,但任务队列接口实际上是一团乱麻。这些队列基本上都是些随意创建的抽象概念,散落在内核各处,就像飘散在空气中。唯有调度队列有点意义,它能用来把工作推后到进程上下文完成
  • 任务队列的另一好处就是接口特别简单。如果不考虑这些队列的数量和执行时随心所欲的规则,它的接口确实够简单。但这也就是全部意义所在了——任务队列剩下的东西乏善可陈
  • 许多任务队列接口的使用者都已经转向使用其他的下半部实现机制了,大部分选择了tasklet,只有调度程序队列的使用者在苦苦支撑。最终,keventd代码演化成了我们今天使用的工作队列机制,而任务队列最终退出了历史舞台

转载地址:https://dongshao.blog.csdn.net/article/details/104004388 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!

上一篇:TCP/IP卷一:63---TCP基础之(ARQ和重传、分组窗口和滑动窗口、流量控制和拥塞控制、设置重传超时)
下一篇:Linux(内核剖析):25---中断下半部之(tasklet机制(struct tasklet_struct)、BH机制)

发表评论

最新留言

能坚持,总会有不一样的收获!
[***.219.124.196]2024年04月09日 19时58分10秒