一个workqueue导致的性能问题

本文代码基于linux 4.19.195
笔者最近遇到了一个workqueue导致性能问题,引发了笔者对workqueue机制的探索和思考。
简单的抽象后,问题是这样的:
一共有两个进程,假设称之为a进程和b进程。a进程在等待b进程完成一些工作,b进程在完成工作后会调用相关接口通知a进程。b进程完成工作的流程的最末尾的位置,有如下两个关键步骤:

  1. 首先调用schedule_work_on触发一个work 1,b进程只需要触发即可,无需等待work 1完成;
  2. 然后再调用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处理完。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值