如何理解create_singlethread_workqueue是严格按照顺序执行的

我们知道工作队列有三种,分别是PerCpu, Unbound,以及ORDERED这三种类型,正如之前的文档分析:
1.PerCpu的工作队列:
API:

create_workqueue(name)

这种工作队列在queue_work的时候,首先检查当前的Cpu是哪一个,然后将work调度到该cpu下面的normal级别的线程池中运行。
注:针对PerCpu类型而言,系统在开机的时候会注册2个线程池,一个低优先级的,一个高优先级的。
2.Unbound的工作队列:
API:

create_freezable_workqueue(name)

这种工作队列在queue_work的时候,同样首先检查当前的Cpu是哪一个,随后需要计算当前的Cpu属于哪一个Node,因为对于Unbound的工作队列而言,线程池并不是绑定到cpu的而是绑定到Node的,随后找到该Node对应的线程池中运行。需要留意的是这种工作队列是考虑了功耗的,例如:当work调度的时候,调度器会尽量的让已经休眠的cpu保持休眠,而将当前的work调度到其他active的cpu上去执行。

注:对于NUMA没有使能的情况下,所有节点的线程池都会指向dfl的线程池。
3.Ordered的工作队列:
API:

create_singlethread_workqueue(name) 或者 alloc_ordered_workqueue(fmt,
flags, args…)

这种work也是Unbound中的一种,但是这种工作队列即便是在NUMA使能的情况下,所有Node的线程池都会被指向dfl的线程池,换句话说Ordered的工作队列只有一个线程池,因为只有这样才能保证Ordered的工作队列是顺序执行的,而这也是本文分析的切入点。


有关并发问题的总结性陈述:

   首先对于Ordered的工作队列(create_singlethread_workqueue,其他自定义的API则不一定了)这是严格顺序执行的,绝对不可能出现并发(无论提交给wq的是否是同一个work)。
   但是对于PerCpu的工作队列(create_workqueue),其中对于提交给wq的如果是同一个work,那么也不会并发,会顺序执行。但是如果提交给wq的不是同一个work,则会在不同的cpu间并发。需要特别留意的是,其并不会在同一个CPU的不同线程间并发,这是因为create_workqueue这个API定义的max_active为1,也就意味者,当前wq只能最多在每个cpu上并发1个线程。
#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)

    这里面特别要去留意的是alloc_workqueue的第二个参数是flags,第三个参数表示当前工作队列max_active的work个数,比如当前值为1,那么在当前工作队列中如果已经有work在执行中了,随后排队的work只能进入pwq->delayed_works的延迟队列中,等到当前的work执行完毕后再顺序执行。

    那么这儿有个疑问就是如果将active增大,是否意味着队列中的work可以并行执行了呢,也不全是,如果当前排队的work和正在执行的work是同一个的话则需要等待当前work执行完成后顺序执行。如果当前排队的work和正在执行的work不是同一个同时alloc_workqueue函数的第三个参数(max_active)大于1的话,那么内核会为你在线程池中开启一个新的线程来执行这个work。

OK,Read The Fuck Source.

kernel\Workqueue.c

static void __queue_work(int cpu, struct workqueue_struct *wq,
			 struct work_struct *work)
{
 	.....
 	/*
 	    1. 当第一次调度的时候,由于pwq->nr_active为0,低于max_active(1),则将work加入到线程池中的worklist中,pwq->nr_active自增.
 	    2. 当第二次调度的时候,且第一次调度的work正在执行中(进入function了),由于pwq->nr_active为1,不低于max_active(1),则将work加入到线程池的delayed_works延迟列表中,并设置当前work的flag为WORK_STRUCT_DELAYED.
 	*/
 	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;
	}
	//如上面的描述插入到对应的链表中	
	insert_work(pwq, work, worklist, work_flags);
	....
}

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的flag
	set_work_pwq(work, pwq, extra_flags);
	//将work加入到对应的线程池的worklist或者delayed_works链表中
	list_add_tail(&work->entry, head);
	...
	//从线程池中取出处于idle的线程,唤醒它
	if (__need_more_worker(pool))
			wake_up_worker(pool);
}

我们继续看看唤醒的线程中是怎么处理的,是使用这个唤醒的idle线程呢?还是在原有的线程处理结束后再执行?

static int worker_thread(void *__worker)
{
	...
woke_up:
		/*
			如下所示
			1.针对第一次调度的情况,pool的worklist不为NULL,且pool->nr_running为0(意味着所有的worker都进入了阻塞状态),则当前唤醒的线程将继续处理这个work。
			2.针对第二次调度的情况,且第一次调度的work正在执行中(进入function了),那么由于pool的worklist为NULL(该work进入了延迟队列),那么,当前唤醒的worker会直接睡眠。
		*/
		if (!need_more_worker(pool))
		goto sleep;
		if (unlikely(!may_start_working(pool)) && manage_workers(worker))
		goto recheck;
		...
		do {
			...
			process_one_work(worker, work);
			...
		} while (keep_working(pool));
		....
sleep:
		worker_enter_idle(worker);
		__set_current_state(TASK_INTERRUPTIBLE);
		spin_unlock_irq(&pool->lock);
		schedule();
		goto woke_up;		
}

我们再看看process_one_work的执行过程

static void process_one_work(struct worker *worker, struct work_struct *work)
{
	...
		//这里的理解也是非常的重要的
		/*
		  首先在线程池中正在运行的线程中取出正在运行的work和当前想要处理的work进行比对,如果是同一个work那么直接返回,等待原先的那个work处理结束后再紧接着处理。
		*/
		collision = find_worker_executing_work(pool, work);
		if (unlikely(collision)) {
			move_linked_works(work, &collision->scheduled, NULL);
			return;
		}
	...
		//真正处理这个work的地方
		worker->current_func(work);
	...
		//判断是否要处理延迟队列的work
		pwq_dec_nr_in_flight(pwq, work_color);
	...
}

看看延迟队列是怎么提取出来的

static void pwq_dec_nr_in_flight(struct pool_workqueue *pwq, int color)
{    
	...
	//当目前的work处理完成后,就可以将当前工作队列(ordered类型)active的work减1到0了,也就是说当前工作队列(ordered类型)又可以接收新的work了
	pwq->nr_active--;
	//如果之前的延迟队列有待处理的work,那么取出来加到pool->worklist,等到线程的下一次while循环的时候执行。
	/*
	   流程如下:
	   pwq_activate_first_delayed->pwq_activate_delayed_work->move_linked_works
	*/
	if (!list_empty(&pwq->delayed_works)) {
		/* one down, submit a delayed one */
		if (pwq->nr_active < pwq->max_active)
			pwq_activate_first_delayed(pwq);
	}
	...
}

static void pwq_activate_delayed_work(struct work_struct *work)
{
	struct pool_workqueue *pwq = get_work_pwq(work);

	trace_workqueue_activate_work(work);
	move_linked_works(work, &pwq->pool->worklist, NULL);
	__clear_bit(WORK_STRUCT_DELAYED_BIT, work_data_bits(work));
	pwq->nr_active++;
}

最后说明一下2个问题:

  1. pool->nr_running,这个flag表示当前线程池是否是阻塞或者Active状态。
    0: 阻塞状态,work的function中有可能调用了导致sleep的函数,例如msleep,wait_interrupt,mutex等。这种情况下如果再次insert_work的话,需要在当前线程池中,开启新的线程(这个线程有可能是在当前CPU的不同线程或者是不同的CPU上)去处理。
    1:Active状态,work的function还在执行中,且没有导致sleep的操作。这种情况下如果再次insert_work的话,不需要再开启新的线程了,直接在原有线程中处理即可。
  2. 第二个问题是针对unbound的工作队列,其线程池是否需要额外创建的原则是属性是否一致,属性匹配只关注2个地方,一个是优先级,一个是cpumask(当前工作是否可以在对应的cpu上运行)。

猜你喜欢

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