Concurrency Managed Workqueue(四)workqueue如何处理work
一、前言
本文主要讲述下面两部分的内容:
1、将work挂入workqueue的处理过程
2、如何处理挂入workqueue的work
二、用户将一个work挂入workqueue
1、queue_work_on函数
使用workqueue机制的模块可以调用queue_work_on(有其他变种的接口,这里略过,其实思路是一致的)将一个定义好的work挂入workqueue,具体代码如下:
bool queue_work_on(int cpu, struct workqueue_struct *wq, struct work_struct *work)
{
……if (!test_and_set_bit(WORK_STRUCT_PENDING_BIT, work_data_bits(work))) {
__queue_work(cpu, wq, work);---挂入work list并通知worker thread pool来处理
ret = true;
}……
}
work_struct的data
member中的WORK_STRUCT_PENDING_BIT这个bit标识了该work是处于pending状态还是正在处理中,pending状态的work只会挂入一次。大部分的逻辑都是在__queue_work函数中,下面的小节都是描述该函数的执行过程。
2、__WQ_DRAINING的解释
__queue_work函数一开始会校验__WQ_DRAINING这个flag,如下:
if (unlikely(wq->flags & __WQ_DRAINING) && WARN_ON_ONCE(!is_chained_work(wq)))
return;
__WQ_DRAINING这个flag表示该workqueue正在进行draining的操作,这多半是发送在销毁workqueue的时候,既然要销毁,那么挂入该workqueue的所有的work都要处理完毕,才允许它消亡。当想要将一个workqueue中所有的work都清空的时候,如果还有work挂入怎么办?一般而言,这时候当然是不允许新的work挂入了,毕竟现在的目标是清空workqueue中的work。但是有一种特例(通过is_chained_work判定),也就是正在清空的work(隶属于该workqueue)又触发了一个queue
work的操作(也就是所谓chained work),这时候该work允许挂入。
3、选择pool workqueue
if (req_cpu == WORK_CPU_UNBOUND)
cpu = raw_smp_processor_id();if (!(wq->flags & WQ_UNBOUND))
pwq = per_cpu_ptr(wq->cpu_pwqs, cpu);
else
pwq = unbound_pwq_by_node(wq, cpu_to_node(cpu));
WORK_CPU_UNBOUND表示并不指定cpu,这时候,选择当前代码运行的那个cpu了。一旦确定了cpu了,对于非unbound的workqueue,当然使用per
cpu的pool workqueue。如果是unbound的workqueue,那么要根据numa node
id来选择。cpu_to_node可以从cpu id获取node id。需要注意的是:这里选择的pool
wq只是备选的,可能用也可能不用,它有可能会被替换掉,具体参考下一节描述。
4、选择worker thread pool
与其说挂入workqueue,不如说挂入worker thread
pool,因为毕竟是线程池来处理具体的work。pool_workqueue有一个相关联的worker thread pool(struct
pool_workqueue的pool成员),因此看起来选择了pool wq也就选定了worker pool了,但是,不是当前选定的那个pool
wq对应的worker pool就适合该work,因为有时候该work可能正在其他的worker
thread上执行中,在这种情况下,为了确保work的callback function不会重入,该work最好还是挂在那个worker
thread pool上,具体代码如下:
last_pool = get_work_pool(work);
if (last_pool && last_pool != pwq->pool) {
struct worker *worker;spin_lock(&last_pool->lock);
worker = find_worker_executing_work(last_pool, work);
if (worker && worker->current_pwq->wq == wq) {
pwq = worker->current_pwq;
} else {
/* meh... not running there, queue here */
spin_unlock(&last_pool->lock);
spin_lock(&pwq->pool->lock);
}
} else {
spin_lock(&pwq->pool->lock);
}
last_pool记录了上一次该work是被哪一个worker pool处理的,如果last_pool就是pool wq对应的worker
pool,那么皆大欢喜,否则只能使用last pool了。使用last pool的例子比较复杂一些,因为这时候需要根据last worker
pool找到对应的pool
workqueue。find_worker_executing_work函数可以找到具体哪一个worker线程正在处理该work,如果没有找到,那么还是使用第3节中选定的pool
wq吧,否则,选择该worker线程当前的那个pool workqueue(其实也就是选定了线程池)。
5、选择work挂入的队列
队列有两个,一个是被推迟执行的队列(pwq->delayed_works),一个是线程池要处理的队列(pwq->pool->worklist),如果挂入线程池要处理的队列,也就意味着该work进入active状态,线程池会立刻启动处理流程,如果挂入推迟执行的队列,那么该work还是pending状态:
pwq->nr_in_flight[pwq->work_color]++;
work_flags = work_color_to_flags(pwq->work_color);if (likely(pwq->nr_active < pwq->max_active)) {
pwq->nr_active++;
worklist = &pwq->pool->worklist;
} else {
work_flags |= WORK_STRUCT_DELAYED;
worklist = &pwq->delayed_works;
}insert_work(pwq, work, worklist, work_flags);
具体的挂入队列的动作是在insert_work函数中完成的。
6、唤醒idle的worker来处理该work
在insert_work函数中有下面的代码:
if (__need_more_worker(pool))
wake_up_worker(pool);
当线程池中正在运行状态的worker线程数目等于0的时候,说明需要wakeup线程池中处于idle状态的的worker线程来处理work。
三、线程池如何创建worker线程?
1、per cpu worker pool什么时候创建worker线程?
对于per-CPU workqueue,每个cpu有两个线程池,一个是normal,一个是high priority的。在初始化函数init_workqueues中有对这两个线程池的初始化:
for_each_online_cpu(cpu) {
struct worker_pool *pool;for_each_cpu_worker_pool(pool, cpu) {
pool->flags &= ~POOL_DISASSOCIATED;
BUG_ON(!create_worker(pool));
}
}
因此,在系统初始化的时候,per cpu workqueue共享的那些线程池(2 x cpu nr)就会通过create_worker创建一个initial worker。
一旦initial worker启动,该线程会执行worker_thread函数来处理work,在处理过程中,如果有需要, worker会创建新的线程。
2、unbound thread pool什么时候创建worker线程?
我们先看看unbound thread pool的建立,和per-CPU不同的是unbound thread
pool是全局共享的,因此,每当创建不同属性的unbound
workqueue的时候,都需要创建pool_workqueue及其对应的worker
pool,这时候就会调用get_unbound_pool函数在当前系统中现存的线程池中找是否有匹配的worker
pool,如果没有就需要创建新的线程池。在创建新的线程池之后,会立刻调用create_worker创建一个initial worker。和per
cpu worker pool一样,一旦initial
worker启动,随着work不断的挂入以及worker处理work的具体情况,线程池会动态创建worker。
3、如何创建worker。代码如下:
static struct worker *create_worker(struct worker_pool *pool)
{
struct worker *worker = NULL;
int id = -1;
char id_buf[16];id = ida_simple_get(&pool->worker_ida, 0, 0, GFP_KERNEL);----分配ID
worker = alloc_worker(pool->node);-----分配worker struct的内存
worker->pool = pool;
worker->id = id;if (pool->cpu >= 0)---------worker的名字
snprintf(id_buf, sizeof(id_buf), "%d:%d%s", pool->cpu, id, pool->attrs->nice < 0 ? "H" : "");
else
snprintf(id_buf, sizeof(id_buf), "u%d:%d", pool->id, id);worker->task = kthread_create_on_node(worker_thread, worker, pool->node, "kworker/%s", id_buf);
set_user_nice(worker->task, pool->attrs->nice); ---创建task并设定nice value
worker->task->flags |= PF_NO_SETAFFINITY;
worker_attach_to_pool(worker, pool); -----建立worker和线程池的关系spin_lock_irq(&pool->lock);
worker->pool->nr_workers++;
worker_enter_idle(worker);
wake_up_process(worker->task);------让worker运行起来
spin_unlock_irq(&pool->lock);return worker;
}
代码不复杂,通过线程池(struct worker_pool)绑定的cpu信息(struct
worker_pool的cpu成员)可以知道该pool是per-CPU还是unbound,对于per-CPU线程池,pool->cpu是大于等于0的。对于对于per-CPU线程池,其worker线程的名字是kworker/cpu:worker id,如果是high priority的,后面还跟着一个H字符。对于unbound线程池,其worker线程的名字是kworker/u pool id:worker id。
四、work的处理
本章主要描述worker_thread函数的执行流程,部分代码有删节,保留主干部分。
1、PF_WQ_WORKER标记
worker线程函数一开始就会通过PF_WQ_WORKER来标注自己:
worker->task->flags |= PF_WQ_WORKER;
有了这样一个flag,调度器在调度当前进程sleep的时候可以检查这个准备sleep的进程是否是一个worker线程,如果是的话,那么调度器不能鲁莽的调度到其他的进程,这时候,还需要找到该worker对应的线程池,唤醒一个idle的worker线程。通过workqueue模块和调度器模块的交互,当work
A被阻塞后(处理该work的worker线程进入sleep),调度器会唤醒其他的worker线程来处理其他的work B,work C……
2、管理线程池中的线程
recheck:
if (!need_more_worker(pool))
goto sleep;if (unlikely(!may_start_working(pool)) && manage_workers(worker))
goto recheck;
如何判断是否需要创建更多的worker线程呢?原则如下:
(1)有事情做:挂在worker pool中的work list不能是空的,如果是空的,那么当然sleep就好了
(2)比较忙:worker
pool的nr_running成员表示线程池中当前正在干活(running状态)的worker线程有多少个,当nr_running等于0表示所有的worker线程在处理work的时候阻塞了,这时候,必须要启动新的worker线程来处理worker
pool上处于active状态的work链表上的work们。
3、worker线程开始处理work
worker_clr_flags(worker, WORKER_PREP | WORKER_REBOUND);
do {
struct work_struct *work = list_first_entry(&pool->worklist, struct work_struct, entry);if (likely(!(*work_data_bits(work) & WORK_STRUCT_LINKED))) {
process_one_work(worker, work);
if (unlikely(!list_empty(&worker->scheduled)))
process_scheduled_works(worker);
} else {
move_linked_works(work, &worker->scheduled, NULL);
process_scheduled_works(worker);
}
} while (keep_working(pool));worker_set_flags(worker, WORKER_PREP);
按理说worker线程处理work应该比较简单,从线程池的worklist中取一个work,然后调用process_one_work处理之就OK了,不过现实稍微复杂一些,work和work之间并不是独立的,也就是说,work
A和work B可能是linked work,这些linked
work应该被一个worker来处理。WORK_STRUCT_LINKED标记了work是属于linked work,如果是linked
work,worker并不直接处理,而是将其挂入scheduled work
list,然后调用process_scheduled_works来处理。毫无疑问,process_scheduled_works也是调用process_one_work来处理一个一个scheduled
work list上的work。
scheduled work list并非仅仅应用在linked
work,在worker处理work的时候,有一个原则要保证:同一个work不能被同一个cpu上的多个worker同时执行。这时候,如果worker发现自己要处理的work正在被另外一个worker线程处理,那么本worker线程将不处理该work,只需要挂入正在执行该work的worker线程的scheduled
work list即可。
Concurrency Managed Workqueue(四)workqueue如何处理work的更多相关文章
- Concurrency Managed Workqueue(一)workqueue基本概念
一.前言 workqueue是一个驱动工程师常用的工具,在旧的内核中(指2.6.36之前的内核版本)workqueue代码比较简单(大概800行),在2.6.36内核版本中引入了CMWQ(Concur ...
- Concurrency Managed Workqueue(三)创建workqueue代码分析
一.前言 本文主要以__alloc_workqueue_key函数为主线,描述CMWQ中的创建一个workqueue实例的代码过程. 二.WQ_POWER_EFFICIENT的处理 __alloc_w ...
- 十天学Linux内核之第四天---如何处理输入输出操作
原文:十天学Linux内核之第四天---如何处理输入输出操作 真的是悲喜交加呀,本来这个寒假早上8点都去练车,两个小时之后再来实验室陪伴Linux内核,但是今天教练说没名额考试了,好纠结,不过想想就可 ...
- Concurrency Managed Workqueue(二)CMWQ概述
一.前言 一种新的机制出现的原因往往是为了解决实际的问题,虽然linux kernel中已经提供了workqueue的机制,那么为何还要引入cmwq呢?也就是说:旧的workqueue机制存在什么样的 ...
- Linux中断管理 (3)workqueue工作队列
目录: <Linux中断管理> <Linux中断管理 (1)Linux中断管理机制> <Linux中断管理 (2)软中断和tasklet> <Linux中断管 ...
- 工作队列workqueue应用
工作队列是另一种将工作推后执行的形式,它可以把工作交给一个内核线程去执行,这个下半部是在进程上下文中执行的,因此,它可以重新调度还有睡眠. 区分使用软中断/tasklet还是工作队列比较简单,如果推后 ...
- Linux kernel workqueue机制分析
Linux kernel workqueue机制分析 在内核编程中,workqueue机制是最常用的异步处理方式.本文主要基于linux kernel 3.10.108的workqueue文档分析其基 ...
- linux工作队列 - workqueue总览【转】
转自:https://blog.csdn.net/cc289123557/article/details/52551176 版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载 ...
- Linux kernel workqueue机制分析【转】
转自:http://www.linuxsir.org/linuxjcjs/15346.html 在内核编程中,workqueue机制是最常用的异步处理方式.本文主要基于linux kernel 3.1 ...
随机推荐
- 同一页面的两个Iframe,其中一个iframe获取另一个iframe内的iframe中的元素值
公共父页面(主页面): <%@ page language="java" import="java.util.*" pageEncoding=" ...
- 理解JavaScript私有作用域
私有作用域:跟外界的变量方法毫不冲突,豪无关系 var str ="javascript"; (function(){ alert(str); //undefined var st ...
- scala 学习笔记十三 特质(转载)
转载地址:https://blog.csdn.net/dwb1015/article/details/51761510 1,介绍 Scala和java一样不允许类从多个超类继承:从多个超类继承可能会导 ...
- Leaf - 一个由 Go 语言编写的开发效率和执行效率并重的开源游戏服务器框架
转自:https://toutiao.io/posts/0l7l7n/preview Leaf 游戏服务器框架简介 Leaf 是一个由 Go 语言(golang)编写的开发效率和执行效率并重的开源游戏 ...
- Linux c 屏蔽信号、切换信号
信号导致的问题 不是任何信号我们都需要的,如果遇到我们不想处理的信号,我们怎么避免这个信号? 1. 信号屏蔽 intsigprocmask(int how,//操作方式 SIG_BLOCK屏 ...
- [转] ssh穿透??未验证。。。
一. SSH ProxyCommand 实践 http://www.cnblogs.com/shanpow/p/4264867.html 二. SSH穿越跳板机:一条命令跨越跳板机直接登陆远程计算机 ...
- OpenSSL 中 RSA 加密解密实现源代码分析
1.RSA 公钥和私钥的组成.以及加密和解密的公式: 2.模指数运算: 先做指数运算,再做模运算.如 5^3 mod 7 = 125 mod 7 = 6 3.RSA加密算法流程: 选择一对不同的.而且 ...
- Jsp之神笔记
JSP笔记 Tomcatserver port: port就是指的某一个程序网络入口,Tomcat的初始化port为:8080: port的个数:256*256=65536个: 一般常见协议的缺省po ...
- java多线程解决应用挂死的问题
这两天为了定位JBOSS老是挂死的问题,学习了一下JAVA多线程方面的知识,在此总结一下 1.在Java程序中,JVM负责线程的调度.线程调度是指按照特定的机制为多个线程分配CPU的使用权. 调度的模 ...
- poj 2226 二分图 最小点覆盖 , 最大流
题目就是问怎样用最小的板覆盖全部的草地.能够横着放.也能够竖着放,同意一个草地放多个点. 建图方法就是 每一个横向的草地作为X,纵向连续的草地作为Y. X连接Y的边表示, 这里有他们的公共点 ...