工作队列详解

1.工作队列的创建

INIT_WORK(&work_demo, work_demo_func);
workqueue_demo = create_singlethread_workqueue("workqueue demo");
queue_work(workqueue_demo,&work_demo);

#define alloc_workqueue(fmt, flags, max_active, args...)		\
	__alloc_workqueue_key((fmt), (flags), (max_active),		\
			      NULL, NULL, ##args)
#define alloc_ordered_workqueue(fmt, flags, args...)			\
	alloc_workqueue(fmt, WQ_UNBOUND | __WQ_ORDERED | (flags), 1, ##args)
#define create_singlethread_workqueue(name)				\
	alloc_ordered_workqueue("%s", WQ_MEM_RECLAIM, name)

这里面有三个参数需要说明:
WQ_UNBOUND:
这个flag的workqueue说明其work的处理不需要绑定在特定的CPU上执行,workqueue需要关联一个系统中的unbound worker thread pool。如果系统中能找到匹配的线程池(根据workqueue的属性(attribute)),那么就选择一个,如果找不到适合的线程池,workqueue就会创建一个worker thread pool来处理work。
WQ_MEM_RECLAIM:
如果该workqueue的work相互之间没有相关性,则不需要这个flag
这个flag相关的概念是rescuer thread。前面我们描述解决并发问题的时候说到:对于A B C D四个work,当正在处理的B work被阻塞后,worker pool会创建一个新的worker thread来处理其他的work,但是,在memory资源比较紧张的时候,创建worker thread未必能够成功,这时候,如果B work是依赖C或者D work的执行结果的时候,系统进入dead lock。这种状态是由于不能创建新的worker thread导致的,如何解决呢?对于每一个标记WQ_MEM_RECLAIM flag的work queue,系统都会创建一个rescuer thread,当发生这种情况的时候,C或者D work会被rescuer thread接手处理,从而解除了dead lock。
如果设置了这个flag,那么工作队列在创建的时候会创建一个救助者内核线程备用,线程的名称为工作队列的名字
__WQ_ORDERED:
这个flag说明工作队列中的work都是顺序执行的,不存在并发的情况。

__alloc_workqueue_key
	if (flags & WQ_UNBOUND) {
	   //先分配一个属性
		wq->unbound_attrs = alloc_workqueue_attrs(GFP_KERNEL);
	}
	alloc_and_link_pwqs(wq)
	...
	将当前的工作队列加入到全局的workqueues链表中
	list_add_tail_rcu(&wq->list, &workqueues);
	...

static int alloc_and_link_pwqs(struct workqueue_struct *wq)
{
	bool highpri = wq->flags & WQ_HIGHPRI;
	int cpu, ret;

	if (!(wq->flags & WQ_UNBOUND)) {   ---针对常规的工作队列(per cpu类型)
			.........
	} else if (wq->flags & __WQ_ORDERED) {  ---针对orderd类型,顺序执行的工作队列
		ret = apply_workqueue_attrs(wq, ordered_wq_attrs[highpri]);
		return ret;
	} else { ---针对UNBOUND类型的工作队列,能够节省功耗
	.....................
	}
}

其中ordered_wq_attrs这个全局的指针变量是在linux启动的时候就已经完成了初始化的,如下:

BUG_ON(!(attrs = alloc_workqueue_attrs(GFP_KERNEL)));
attrs->nice = std_nice[i];
attrs->no_numa = true;
ordered_wq_attrs[i] = attrs;

关于属性结构的定义如下:

struct workqueue_attrs { 
    int            nice;        /* nice level */ 
    cpumask_var_t        cpumask;    /* allowed CPUs */ 
    bool            no_numa;    /* disable NUMA affinity */ 
}; 

nice是一个和thread优先级相关的属性,nice越低则优先级越高。cpumask是该workqueue挂入的work允许在哪些cpu上运行。no_numa是一个和NUMA affinity相关的设定。
OK,言归正传

apply_workqueue_attrs_locked
	->apply_wqattrs_prepare
	    ->apply_wqattrs_commit

先看apply_wqattrs_prepare函数:

apply_wqattrs_prepare(struct workqueue_struct *wq,
		      const struct workqueue_attrs *attrs)
{
	struct apply_wqattrs_ctx *ctx;
	struct workqueue_attrs *new_attrs, *tmp_attrs;
	int node;
    
	ctx = kzalloc(sizeof(*ctx) + nr_node_ids * sizeof(ctx->pwq_tbl[0]),
		      GFP_KERNEL);
		      
	//首先分配2个临时的属性指针
	new_attrs = alloc_workqueue_attrs(GFP_KERNEL);
	tmp_attrs = alloc_workqueue_attrs(GFP_KERNEL);
	//拷贝用户设置的属性到new_attrs
	copy_workqueue_attrs(new_attrs, attrs);
	/*
		1. 对于4核的CPU,wq_unbound_cpumask代表4个核;
		2. 如果用户没有设置这个属性,那么最后相与的结果就是new_attrs->cpumask为空
	*/
	cpumask_and(new_attrs->cpumask, new_attrs->cpumask, wq_unbound_cpumask);
	if (unlikely(cpumask_empty(new_attrs->cpumask)))
		cpumask_copy(new_attrs->cpumask, wq_unbound_cpumask);
	//如果用户没有配置cpumask的属性那么,使用默认属性(当前芯片CPU的个数)
	
	//拷贝当前的cpumask属性到临时属性变量中
	copy_workqueue_attrs(tmp_attrs, new_attrs);

	/*
		1.创建默认的pwq,针对unbound的情况下是先通过属性在unbound_pool_hash中寻找是否存在属性相同的线程池,如果存在则直接取出线程池pool,再申请pwq的结构体,建立pwq,wq,pool之间的关系,最后返回pwq指针给ctx->dfl_pwq.
		2.如果该属性在unbound_pool_hash中找不到与之对应属性的线程池,那么就通过init_worker_pool创建一个线程池,将当前的unbound的属性拷贝到pool->attrs中,再通过create_worker创建该线程池的第一个worker线程,同时将该线程池加入到全局的unbound_pool_hash表中。然后返回这个新创建的线程池pool,随后申请pwq的结构体,建议pwq,wq,pool之间的关系,最后返回pwq指针给ctx->dfl_pwq.
	*/
	ctx->dfl_pwq = alloc_unbound_pwq(wq, new_attrs);
	if (!ctx->dfl_pwq)
		goto out_free;
    //针对numa的情况,每个node都会对应一个pwq,这里需要特别注意的是numa是需要重新计算cpumask的,因此才会在最初申请了2个属性指针。
	for_each_node(node) {
		if (wq_calc_node_cpumask(new_attrs, node, -1, tmp_attrs->cpumask)) {
			ctx->pwq_tbl[node] = alloc_unbound_pwq(wq, tmp_attrs);
			if (!ctx->pwq_tbl[node])
				goto out_free;
		} else {
			//特别关注这个地方,如果是非numa的情况下,或者order的workqueue,那么所有node的pwq都会指向dfl_pwq.
			ctx->dfl_pwq->refcnt++;
			ctx->pwq_tbl[node] = ctx->dfl_pwq;
		}
	}

	/* 检查用户配置的属性,检查合法性,最后返回ctx指针 */
	copy_workqueue_attrs(new_attrs, attrs);
	cpumask_and(new_attrs->cpumask, new_attrs->cpumask, cpu_possible_mask);
	ctx->attrs = new_attrs;

	ctx->wq = wq;
	return ctx;

}

再来看apply_wqattrs_commit

static void apply_wqattrs_commit(struct apply_wqattrs_ctx *ctx)
{
	int node;
	//拷贝用户配置的属性到ctx->wq->unbound_attrs
	copy_workqueue_attrs(ctx->wq->unbound_attrs, ctx->attrs);

	/* save the previous pwq and install the new one */
	for_each_node(node)
		ctx->pwq_tbl[node] = numa_pwq_tbl_install(ctx->wq, node,
							  ctx->pwq_tbl[node]);

	/* @dfl_pwq might not have been used, ensure it's linked */
	link_pwq(ctx->dfl_pwq);
	swap(ctx->wq->dfl_pwq, ctx->dfl_pwq);

}

上面这个函数简单来说就是调用link_pwq,将pool_workqueue挂入它所属的workqueue的链表中

link_pwq
	list_add_rcu(&pwq->pwqs_node, &wq->pwqs);

2. 工作队列的调度

INIT_WORK(&work_demo, work_demo_func);
queue_work(workqueue_demo,&work_demo);

include\linux\Workqueue.h

queue_work
	queue_work_on(WORK_CPU_UNBOUND, wq, work);

bool queue_work_on(int cpu, struct workqueue_struct *wq,
		   struct work_struct *work)
{
	//如果当前的工作已经在workqueue中排队了(但是没有进入执行),那么再次queue的work将会被丢弃掉,但是需要特别留意的是,只要这个work进入执行状态后(即便没有执行完),相同的work就可以再次进入排队状态了。
	if (!test_and_set_bit(WORK_STRUCT_PENDING_BIT, work_data_bits(work))) {
		__queue_work(cpu, wq, work);
		ret = true;
	}
	return ret;
}

继续

static void __queue_work(int cpu, struct workqueue_struct *wq,
			 struct work_struct *work)
{
	struct pool_workqueue *pwq;
	struct worker_pool *last_pool;
	struct list_head *worklist;
	unsigned int work_flags;
	unsigned int req_cpu = cpu;


	//如果当前的工作队列已经处于销毁状态了,那么就不要再调动了
	if (unlikely(wq->flags & __WQ_DRAINING) &&
	    WARN_ON_ONCE(!is_chained_work(wq)))
		return;
retry:
   //默认通过queue_work调度的work都是不指定cpu的,系统获取当前的cpu
	if (req_cpu == WORK_CPU_UNBOUND)
		cpu = raw_smp_processor_id();

	/*
		这里有2种情况:
		1.对于percpu类型,通过cpu获取绑定在该cpu的pwq,从而获取到对应的线程池;
		2.对于unbound类型,需要先通过cpu获取到node,再通过node获取到该node下的pwq,从而得到线程池;
	*/
	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的线程池*/
	last_pool = get_work_pool(work);
	
	/*如果这个线程池存在,且该线程池和当前cpu或者node对应的线程池不是同一个,同时这个work还在执行中,那么新加入的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);
	}

	...
   //找到线程池对应的worklist
	if (likely(pwq->nr_active < pwq->max_active)) {
		trace_workqueue_activate_work(work);
		pwq->nr_active++;
		worklist = &pwq->pool->worklist;
	} else {
		work_flags |= WORK_STRUCT_DELAYED;
		worklist = &pwq->delayed_works;
	}

	//将当前的work插入到worklist,等待被调度
	insert_work(pwq, work, worklist, work_flags);

	spin_unlock(&pwq->pool->lock);
}

再看看insert_work的实现

static void insert_work(struct pool_workqueue *pwq, struct work_struct *work,
			struct list_head *head, unsigned int extra_flags)
{
	struct worker_pool *pool = pwq->pool;

	/*设置work的flags*/
	set_work_pwq(work, pwq, extra_flags);
	/*真正将work插入到worklist中*/
	list_add_tail(&work->entry, head);
	/*增加pwq的引用计数*/
	get_pwq(pwq);
	
	smp_mb();
	/* 
		确认当前的线程池是否还处于活跃状态:
		1. 如果当前的线程池已经没有在运行了,那么首先需要找到线程池中处于idle的线程,随后唤醒这个task。
		2. 如果当前的线程池依然在运行,那么很简单,找到active的线程,运行即可。
	*/
	if (__need_more_worker(pool))
		wake_up_worker(pool);
}

猜你喜欢

转载自blog.csdn.net/zhuyong006/article/details/82987884