本文代码基于linux 4.19.195
笔者最近遇到了一个workqueue导致性能问题,引发了笔者对workqueue机制的探索和思考。
简单的抽象后,问题是这样的:
一共有两个进程,假设称之为a进程和b进程。a进程在等待b进程完成一些工作,b进程在完成工作后会调用相关接口通知a进程。b进程完成工作的流程的最末尾的位置,有如下两个关键步骤:
- 首先调用schedule_work_on触发一个work 1,b进程只需要触发即可,无需等待work 1完成;
- 然后再调用work_on_cpu(本质上也是schedule_work_on)完成触发一个work 2,然后等待work 2完成后再返回。
在之前的代码里,是没有步骤1的,性能是正常的,耗时约5s。
在近期的开发里,把步骤1加入后,发现性能下降了3倍,耗时15s。
触发了该问题后,先抓了正常代码和异常代码的on-cpu火焰图,发现火焰图长的几乎一模一样。看来,并不是代码执行的时候有性能问题。
其次,check了一下work 1的代码,发现work 1的代码是cpu消耗型,但是在循环里有cond_resched(),也就是说,按理不会一直占着cpu不放,即使是和其他进程抢着cpu使用,那也不至于多花10s的时间。
思考了一下,会不会是这样呢:work 1和work 2都在队列里排队,然后work1先执行,但是一直没有返回,然后导致了work 2一直得不到执行,从而造成了性能的下降。
那么,抓了一下top的输出,发现,每次a进程成功返回的时候,基本和work 1退出的时间吻合。
但是,想了想,还是说不通,因为,workqueue有着自己的管理机制,在任务多的时候,是会通过创建多个kworker去执行的。
static int worker_thread(void *__worker)
{
***
/* do we need to manage? */
if (unlikely(!may_start_working(pool)) && manage_workers(worker)) //may_start_working()判断pool中是否有idle状态工作线程。如果没有,那么manage_workers()创建一些工作线程。
goto recheck;
/*
* ->scheduled list can only be filled while a worker is
* preparing to process a work or actually processing it.
* Make sure nobody diddled with it while I was sleeping.
*/
WARN_ON_ONCE(!list_empty(&worker->scheduled));
/*
* Finish PREP stage. We're guaranteed to have at least one idle
* worker or that someone else has already assumed the manager
* role. This is where @worker starts participating in concurrency
* management if applicable and concurrency management is restored
* after being rebound. See rebind_workers() for details.
*/
worker_clr_flags(worker, WORKER_PREP | WORKER_REBOUND);
do {
struct work_struct *work =
list_first_entry(&pool->worklist,
struct work_struct, entry);
pool->watchdog_ts = jiffies;
if (likely(!(*work_data_bits(work) & WORK_STRUCT_LINKED))) {
/* optimization path, not strictly necessary */
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));
***
}
may_start_working()会判断pool中是否有idle状态工作线程。如果没有,那么会调用manage_workers()创建一些工作线程,在manage_workers()函数中会把这些kworker线程状态设置成idle。也就是说,workqueue这个系统,除了正在执行的kworker线程,至少会在该work_pool保留一个idle的kworker线程。当然,workqueue模块也不会放任idle的kworker数量不断增长,这个机制可以查看idle_worker_timeout函数,不过,根据too_many_workers()函数的逻辑,感觉一般情况下至少会保留2个idle的kworker
/**
* manage_workers - manage worker pool
* @worker: self
*
* Assume the manager role and manage the worker pool @worker belongs
* to. At any given time, there can be only zero or one manager per
* pool. The exclusion is handled automatically by this function.
*
* The caller can safely start processing works on false return. On
* true return, it's guaranteed that need_to_create_worker() is false
* and may_start_working() is true.
*
* CONTEXT:
* spin_lock_irq(pool->lock) which may be released and regrabbed
* multiple times. Does GFP_KERNEL allocations.
*
* Return:
* %false if the pool doesn't need management and the caller can safely
* start processing works, %true if management function was performed and
* the conditions that the caller verified before calling the function may
* no longer be true.
*/
static bool manage_workers(struct worker *worker)
{
struct worker_pool *pool = worker->pool;
if (pool->flags & POOL_MANAGER_ACTIVE)
return false;
pool->flags |= POOL_MANAGER_ACTIVE;
pool->manager = worker;
maybe_create_worker(pool);
pool->manager = NULL;
pool->flags &= ~POOL_MANAGER_ACTIVE;
wake_up(&wq_manager_wait);
return true;
}
理论上走不通了,那就继续复现抓线索。发现,在a进程等待的第5秒到第15秒中,一直有一个进程处于D状态,而且,这个进程就是b进程,并且,调用栈就是在等待work 2的完成。
看来,上面分析的没有错,确实就是work 1和work 2都在队列上,然后work 1一直没执行完,从而导致的性能下降。
看来,我对workqueue的机制还是没有理解透彻,继续翻看代码。
我们插入work的时候,调用的是schedule_work_on()函数,这个函数最终会调用到insert_work()函数
/**
* insert_work - insert a work into a pool
* @pwq: pwq @work belongs to
* @work: work to insert
* @head: insertion point
* @extra_flags: extra WORK_STRUCT_* flags to set
*
* Insert @work which belongs to @pwq after @head. @extra_flags is or'd to
* work_struct flags.
*
* CONTEXT:
* spin_lock_irq(pool->lock).
*/
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;
/* we own @work, set data and link */
set_work_pwq(work, pwq, extra_flags);
list_add_tail(&work->entry, head);
get_pwq(pwq);
/*
* Ensure either wq_worker_sleeping() sees the above
* list_add_tail() or we see zero nr_running to avoid workers lying
* around lazily while there are works to be processed.
*/
smp_mb();
if (__need_more_worker(pool))
wake_up_worker(pool);
}
注意最后两行代码,从字面上来看,插入这个work到队列里面后,先通过函数__need_more_worker(pool)判断是否需要一个新的kworker来处理他,如果需要,则调用wake_up_worker(pool)函数完成kworker的唤醒操作。
显然,在这个问题中,__need_more_worker(pool)函数返回的值是false。
/*
* Policy functions. These define the policies on how the global worker
* pools are managed. Unless noted otherwise, these functions assume that
* they're being called with pool->lock held.
*/
static bool __need_more_worker(struct worker_pool *pool)
{
return !atomic_read(&pool->nr_running);
}
看来,如果这个worker_pool里有kworker在跑,在insert_work()时就不会唤醒idle的kworker来处理下一个在排队的work。这样的策略,在这个问题的场景下,就会导致性能问题。这个策略,其实,也不是说不合理,毕竟,有些work只是不适合在中断里处理,从而才放到workqueue里面来做,并不是说每个work都会处理很长的时间。从而,这样的策略,能够减少kworker被唤醒的次数,从而可以为系统减压,当然,带来的结果就是潜在的效率的下降。感觉,这个策略是一个折中的策略,毕竟,如果唤醒一个kworker,起来做了那么多准备,最后只是执行几行代码这个work就退出了的话,确实也太浪费cpu资源了。
好吧,到此为止至少问题是分析清除了,那么该如何解决呢?
schedule_work_on函数使用的是system_wq这个workqueue,那我们自己调用create_workqueue()创建一个新的workqueue能不能解决呢?
答案是不行。因为基于workqueue的设计理念(这里就不讲了,请自行百度),create_workqueue()创建一个新的workqueue最后大概率也是会和system_wq用同一个work_pool,这样的话问题依然存在。
看了下代码,发现用WQ_CPU_INTENSIVE这个flag创建一个workqueue是最合适的,这个flag创建的workqueue,其work会在单独一个kworker上执行,而不会和其他work使用同一个kworker线程,详情见process_one_work函数中的cpu_intensive临时变量。
嗯,改代码,验证,性能问题成功fix。
趁周末时间,再次了解了一下workqueue调度的思想:
如果有 work 需要处理,保持一个 running 状态的 worker 处理,不多也不少。
但是这里有一个问题如果 work 是 CPU 密集型的,它虽然也没有进入 suspend 状态,但是会长时间的占用 CPU,让后续的 work 阻塞太长时间。
为了解决这个问题,CMWQ 设计了 WQ_CPU_INTENSIVE,如果一个 wq 声明自己是 CPU_INTENSIVE,则让当前 worker 脱离动态调度,像是进入了 suspend 状态,那么 CMWQ 会创建新的 worker,后续的 work 会得到执行
看来,workqueue确实不愿意使用多个kworker线程处理work,而是依靠一个kworker一个个的把work处理完。