2020-03-30-Linux内核23-工作队列

layouttitlesubtitledateauthorheader-imgcatalogtags
post
Linux内核23-工作队列
Linux内核是如何实现和处理工作队列的
2020-03-30
Tupelo Shen
img/post-bg-unix-linux.jpg
true
Linux
Linux内核
工作队列

1 工作队列

Linux2.6版本中引入了工作队列概念,代替Linux2.4版本中的任务队列。用以实现注册激活某些函数,留待稍后由工作线程执行(与tasklet的处理类似)。

虽然,tasklet之类的可延时函数和工作队列处理流程类似,但是却大有不同。主要的差别是可延时函数运行在中断上下文中,而工作队列中的函数运行在进程上下文中。在进程上下文运行是执行阻塞函数的唯一方式,因为中断上下文中不能发生进程切换。不论是可延时函数还是工作队列中的函数都不能访问进程的用户态地址空间,它们都运行在内核态。事实上,可延时函数并不知道当前正在运行的进程。另一方面,工作队列中函数由内核线程执行,所以也就没有用户态地址可以访问。

也就是说,工作队列的出现就是解决tasklet不能处理可阻塞函数的弊端。并且它们都是运行在内核态的程序,不能访问用户态地址空间。

1.1 工作队列数据结构

工作队列的主要数据结构是workqueue_struct,其中,包含一个具有NR_CPUS个元素的数组。每个元素都是一个类型为cpu_workqueue_struct的描述符,其成员如下表所示:

表4-12 cpu_workqueue_struct结构成员

名称描述
lock保护数据结构的自旋锁
remove_sequenceflush_workqueue()使用的序列号
insert_sequenceflush_workqueue()使用的序列号
worklist挂起函数列表的head
more_work休眠中的工作线程等待队列
work_done等待从工作队列中刷新的进程队列
wq指向包含描述符的workqueue_struct结构
thread该数据结构的工作线程的进程描述符
run_depthrun_workqueue()执行深度

worklist是一个双向链表,用来保存工作队列的待处理任务。每个待处理任务使用work_struct数据结构表示,成员如下表所示:

表4-13 work_struct成员

名称描述
pending1,表示处理函数已经在工作队列列表中
entry指向函数列表中下一项或前一项
func函数的地址
data传给函数的数据
wq_data指向父cpu_workqueue_struct描述符
timer软件定时器,用于函数的延时执行

1.2 工作队列操作函数

我们已经了解了工作队列的原理,以及其数据结构。那么,当我们想要使用工作队列的时候,如何创建呢?

使用create_workqueue("foo")创建一个工作队列。foo是工作队列的名称,函数返回新创建的workqueue_struct的地址。该函数还会创建n个工作线程,n是CPU的数量,这些线程命令方式就是在传递的字符串foo后面加数字n表示:比如foo/0foo/1等等。create_singlethread_workqueue()只创建一个工作线程,其余一样。销毁工作队列使用destroy_workqueue()函数,参数是一个指向workqueue_struct结构的指针。

queue_work()函数插入一个函数到工作队列中(该函数已经被包含在work_struct描述符中了)。它的参数是指向workqueue_struct类型描述符的指针wq和指向work_struct描述符的指针work。它所执行的主要工作是:

  1. 检查待插入的函数是否已经在工作队列中。

  2. 添加work_struct描述符到工作队列列表中,设置work->pending为1。

  3. 唤醒more_work等待队列中休眠的工作线程。

queue_delayed_work()函数与queue_work()类似,除了接收第3个参数-延时时间(单位是系统嘀嗒)之外。这个时间用来保证挂起函数执行之前最小延时时间。queue_delayed_work()依赖于work_struct描述符中的timer软件定时器,推迟将work_struct描述符插入到工作队列列表的时间。cancel_delayed_work()取消之前插入到工作队列中的函数,前提是work_struct描述符还没有被插入到工作队列中。

每个工作线程执行worker_thread函数,循环处理挂起的函数。但是,大部分时候,线程正在休眠并且需要处理的工作。一旦被唤醒,工作线程调用run_workqueue()函数,其实就是把work_struct描述符从该线程的工作队列列表中删除,并执行相应的函数。因为工作队列函数可以阻塞,所以工作线程可以休眠且当其被恢复执行时,可以切换到其它CPU上运行。

有时候,可能需要执行完所有的工作队列函数。可以调用flush_workqueue()函数,直到所有的函数执行完。但是,这个函数不会理会在它之后被加入工作队列的函数;对于新旧添加的函数可以通过cpu_workqueue_struct描述符中的remove_sequenceinsert_sequence成员标识。

2 预定义工作队列

大部分情况下,为了运行某个函数而创建一组工作线程是多余的。因此,内核提供了一个称为events的预定义工作队列,内核开发者可以自由使用。预定义工作队列不过就是一个标准工作队列,包含不同内核和驱动层的函数。它的workqueue_struct描述符存储在keventd_wq数组中。为了使用预定义工作队列,内核提供了一些辅助函数:

表4-14 预定义工作队列辅助函数

预定义工作队列函数等价的标准工作队列函数
schedule_work(w)queue_work(keventd_wq,w)
schedule_delayed_work(w,d)queue_delayed_work(keventd_wq,w,d)(任何CPU)
schedule_delayed_work_on(cpu,w,d)queue_delayed_work(keventd_wq,w,d)(给定CPU)
flush_scheduled_work()flush_workqueue(keventd_wq)

预定义工作队列节省了系统资源。但是另一方面,在预定义工作队列中的函数不应该阻塞较长时间:因为每个CPU中的工作队列的函数执行都是串行化的,所以,长时间的阻塞耽误其它用户的使用。

除了通用的events队列,在Linux2.6内核中还可以发现一些特定的工作队列。最重要的是kblockd工作队列,由阻塞设备层使用。

3 总结

工作队列的场合比较适用于驱动程序开发。比如说阻塞设备驱动程序(硬盘写一块数据等),这样的驱动写操作不需要立即响应,但是需要阻塞操作,其它的写硬盘动作等待这次操作完成。就可以将这样的任务放入到工作队列中,等待系统不忙的时候再进行处理。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值