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