Are you kidding?

Mar 22, 2017 - 5 minute read

Linux Concurrency Managed Workqueue分析

概述

核心数据结构

框架分析

workqueue子系统初始化

workqueue_init_early

此函数对workqueue子系统做早期初始化。它会建立某些数据结构及创建系统的workquues,其他模块的早期初始化代码在这之后便可以queue/cancel work items了,但是这些work items只有在相关worker kthread建立(会在workqueue_init中做)之后才能够得到运行。

此函数中最核心一步是为每个cpu初始化了两个普通worker pools(一个nice=0的、一个nice=-20的),这些worker pool都存在per-cpu变量中:

/* the per-cpu worker pools */
static DEFINE_PER_CPU_SHARED_ALIGNED(struct worker_pool [NR_STD_WORKER_POOLS], cpu_worker_pools);

实际上,无论是普通worker-pool还是unbound/ordered worker-pool,都有低优先级和高优先级两种。

因为普通worker pool是per-cpu的,其cpumask也就只含有相应cpu,在创建work线程时会用它来指定线程的cpu亲和性,所以对于普通work poll来说,其work线程是只能在相应的cpu上运行,不能migrate

workqueue_init

此函数是workqueue子系统初始化的第二步(也是最后一步)。

它首先会为每个cpu的每个普通worker pool设置好其所在的node,存储在pool->node中;

然后通过create_worker为每个普通worker pool创建一个worker(包括一个工作线程),worker创建初始化完成后会通过worker_enter_idle进入WORKER_IDLE状态;

除了普通worker pool之外,系统中此时可能已有ubound worker pool(workqueue_init_early之后其他子系统早期代码中可能会创建),此时也需要通过create_worker为它们创建worker。

worker创建

create_worker

此函数用于为@pool创建一个worker。主要步骤如下:

1)通过alloc_worker分配一个worker对象;

2)调用kthread_create_on_node函数创建该worker的工作线程,工作线程的执行体是worker_thread;

注意:kthread_create_on_node会通过kthreadd_task线程在指定node上(具体哪个cpu上不可测)创建一个内核线程。

3)设置工作线程的优先级(set_user_nice);

4)将工作线程绑定至指定的cpumask(kthread_bind_mask);

注意:kthread_bind_mask中会无条件将线程的allowed_cpumask设置为新的cpumask,但即使线程当前所处的cpu不在新的cpumask中也不会做迁移操作。

5)通过worker_attach_to_pool函数将此worker与@pool关联起来,加入到@pool的workers链表中;

注意:worker_attach_to_pool中调用了set_cpus_allowed_ptr函数,但是对于worker kthread来说调用它是没有实际意义的,因为它会检查allowed_cpumask是否与new_cpumask相等,如果相等的话什么也不干,

int set_cpus_allowed_ptr(struct task_struct *p, const struct cpumask *new_mask)
{
	return __set_cpus_allowed_ptr(p, new_mask, false);
}

static int __set_cpus_allowed_ptr(struct task_struct *p,
				  const struct cpumask *new_mask, bool check)
{
	......
	if (cpumask_equal(&p->cpus_allowed, new_mask))
	goto out;
	......
}

但这里为什么需要调用呢?worker_attach_to_pool在rescuer_thread也会调用。一个workqueue可以对应多个worker pool,但是只有一个rescuer线程,当其某个worker pool需要“急救”时,rescuer_thread通过·将自己“派到一线”去救援。

对于worker thread来说,只有等到被调度运行的时候才有机会被迁移到合适的cpu上运行(try_to_wake_up–>select_task_rq)。

关键问题

worker的亲和性能改变吗

worker线程在创建中会通过kthread_bind_mask设置它的allowed_cpumask,并且会设置PF_NO_SETAFFINITY标记。

改变亲和性的内核函数是__set_cpus_allowed_ptr,

static int __set_cpus_allowed_ptr(struct task_struct *p,
				  const struct cpumask *new_mask, bool check)
{
	......
	if (check && (p->flags & PF_NO_SETAFFINITY)) {
		ret = -EINVAL;
		goto out;
	}
	......
}

如果check为真,则不能设置带PF_NO_SETAFFINITY标记的线程的亲和性。

特殊的,rescuer线程也是worker,它在运行过程中会调用worker_attach_to_pool根据具体情况而调整自己的亲和性,而:

static void worker_attach_to_pool(struct worker *worker,
				   struct worker_pool *pool)
{
	......
	set_cpus_allowed_ptr(worker->task, pool->attrs->cpumask);
	......
}

int set_cpus_allowed_ptr(struct task_struct *p, const struct cpumask *new_mask)
{
	return __set_cpus_allowed_ptr(p, new_mask, false);
}

可以看到传入的check=false,意味着即使带PF_NO_SETAFFINITY标记也要修改,所以,rescuer线程在运行过程中会动态调整自己的亲和性,除此之外内核不会主动调整其他worker线程的亲和性

用户态程序(例如taskset)可通过sched_setaffinity来改变线程的亲和性,但是它会首先判断线程是否有PF_NO_SETAFFINITY标记,有的话则不能修改亲和性,

long sched_setaffinity(pid_t pid, const struct cpumask *in_mask)
{
	......
	if (p->flags & PF_NO_SETAFFINITY) {
		retval = -EINVAL;
		goto out_put_task;
	}
	......
}

所以,用户态程序不能修改worker线程的亲和性。

worker何时唤醒/休眠

首先说说“休眠”,也就是让worker进入WORKER_IDLE状态。进入休眠状态的函数是worker_enter_idle,根据其被调用地方,我们可以得到如下结论:

  • worker刚创建完成后会进入休眠状态,因此此时在worker_thread中会进入sleep分支,然后进入休眠:
static int worker_thread(void *__worker)
{
	......
recheck:
	/* no more worker necessary? */
	if (!need_more_worker(pool))
		goto sleep;
	......
sleep:
	worker_enter_idle(worker);
	__set_current_state(TASK_INTERRUPTIBLE);
	spin_unlock_irq(&pool->lock);
	schedule();
	goto woke_up;
}

rescuer作用及运行时间

worker何种情况下会扩建

worker何时销毁

work太多了如何并发处理

timers的作用