Linux中的中断处理机制[八]--任务工厂workqueue机制(2)

adtxl
2023-04-26 / 0 评论 / 278 阅读 / 正在检测是否收录...

转载自https://zhuanlan.zhihu.com/p/94561631 作者兰新宇

imageb5b4870572d28e74.png

在多个 worker 线程的 cmwq 模式下,按理一个 CPU 依然只需要一个 worker pool,所有任务由该 worker pool 里的线程共同服务,但别忘了任务是有优先级的,虽不说像完整的系统那样将优先级划分地很细,至少要分成低优先级和高优先级两类吧。

imageeab8e058865eba74.png

为此,目前的设计是一个 CPU 有两个 worker pool,分别服务于这两种优先级的 work(nice 值分别是 0 和 -20)。

DEFINE_PER_CPU_SHARED_ALIGNED(struct worker_pool [NR_STD_WORKER_POOLS], cpu_worker_pools);

DEFINE_PER_CPU_SHARED_ALIGNED(struct worker_pool [NR_STD_WORKER_POOLS], cpu_worker_pools);
用 "ps" 命令来看下系统中都有哪些 worker 线程,worker 线程被命名成了 "kworker/n:x" 的格式,其中 n 是worker 线程所在的 CPU 的编号,x 是其在 worker pool 中的编号,如果带了 "H" 后缀,说明它属于高优先级的worker pool。

用 "ps" 命令来看下系统中都有哪些 worker 线程,worker 线程被命名成了 "kworker/n:x" 的格式,其中 n 是worker 线程所在的 CPU 的编号,x 是其在 worker pool 中的编号,如果带了 "H" 后缀,说明它属于高优先级的worker pool。

image51720c66778b48ff.png

还有一些带 "u" 前缀的,它表示 "unbound",意思是这个 worker 线程不和任何的 CPU 绑定,而是被所有 CPU 共享,这种设计主要是为了增加灵活性。"u" 后面的这个数字也不再表示 CPU 的编号,而是表示由这些 unbound 的 worker 线程组成的 worker pool 的 ID。

一个系统至少存在几十个 workqueues,“插入”一个 workqueue 的 work 可能在不同的 CPU 上运行(一般执行一个 work 的 CPU 都是将该 work 插入 wq 的线程所在的 CPU),而一个 CPU 的两个 worker pool 里处理的 work 也可能来自不同的 workqueue。

workqueue 和 CPU 实际形成了一种“多对多”的关系,为此创建了一个名为"pool_workqueue" 的结构体(简称 pwq)来连接这两者(可理解为一个 workqueue 在一个 CPU 上的代理,如果有 50 个 workqueues,4 个CPU,那么就有 200 个 pwq):

struct pool_workqueue {
    struct  worker_pool       *pool;     /* the associated pool */
    struct  workqueue_struct   *wq;          /* the owning workqueue */
    struct list_head    pwqs_node;    /* node on wq->pwqs */
        ...
}

这时再来看描述 wq 的"workqueue_struct" 结构体,就能发现它们是怎么关联的:

struct workqueue_struct {
    struct list_head    list;    /* list of all workqueues */
    struct list_head    pwqs;    /* all pwqs of this wq */
        ...
}

"list" 是系统中所有 workqueue 串接而成的链表,以方便内核管理。
"pwqs" 是该 workqueue 对应的所有 pwq 组成的链表(head),而 pwq 结构体中的 "pwqs_node" 就是这个链表的组成节点。

imageba7770fb2b065852.png

选取 "system_wq" 这个 workqueue,获取其地址后,利用 crash 工具,可遍历 wq 所在链表,获取系统包含的 wq 数目及各自名称(比如 "events_xxx")。然后可以看下一个 wq 对应几个 pwqs(笔者实验机器 CPU 数目为 4,所以为 4 个),且这几个 pwqs 的 "wq" 是不是都指向同一个 workqueue。

image234220a91e22d049.png

再来看下这 4 个 pwqs 指向的 "pool",是不是分属于 4 个不同的 CPU:

image9f8c5f8f50c165ad.png

初始化

至此,有关 workqueue 机制的 5 个结构体以及它们之间的相互关系就介绍完了,如果做个类比的话,那么 work item 就是工件,worker 线程就是工人,worker pool 就是一个班组的工人构成的集合。

假设一个集团有 4 个工厂(4 个 CPU),每个工厂都有两个班组,两条流水线,一个班组是由高级工构成的,负责高优先级的流水线,另一个班组是由初级工构成的,负责普通优先级的流水线。

至于 unbound 的生产班组,就理解为外包吧,它们从组织关系上不属于任何一个工厂,但是可以综合这 4 个工厂的生产任务的波动,提供对人力资源更机动灵活的配置。

来看一下 workqueue 机制所需的这一套东西都是怎么创建出来的:

int __init workqueue_init_early(void)
{
    // 创建worker pool 
    for_each_possible_cpu(cpu) {
        struct worker_pool *pool;
        for_each_cpu_worker_pool(pool, cpu) {
                        init_worker_pool(pool);
            pool->cpu = cpu;
                        worker_pool_assign_id(pool);
    ...                    
}

workqueue_init_early() 初始化了 per-CPU 的 worker pool,并为这些 worker pool 指定了 ID 和关联了 CPU,工厂是工人劳动的场所,这一步相当于是把工厂的基础设施建好了。

接下来就是调用 alloc_workqueue() 创建各种 workqueue_struct,大致可分为三类:普通优先级的,高优先级的,以及不和 CPU 绑定的。

system_wq = alloc_workqueue("events", 0, 0);
system_highpri_wq = alloc_workqueue("events_highpri", WQ_HIGHPRI, 0);
system_unbound_wq = alloc_workqueue("events_unbound", WQ_UNBOUND, WQ_UNBOUND_MAX_ACTIVE);

针对每一个 workqueue,在 alloc_and_link_pwqs() 中,创建 per-CPU 的 pwq,并把 pwq 和该 workqueue 关联起来。这一步相当于是把工件送往工厂的通道建立起来了。

alloc_workqueue() -->

int alloc_and_link_pwqs(struct workqueue_struct *wq)
{
    bool highpri = wq->flags & WQ_HIGHPRI;

    if (!(wq->flags & WQ_UNBOUND)) {
        wq->cpu_pwqs = alloc_percpu(struct pool_workqueue);
        for_each_possible_cpu(cpu) {
            init_pwq(pwq, wq, &cpu_pools[highpri]);
    ...
}

一个 CPU 有两个 worker pool,而针对一个 workqueue 只有一个 pwq,如何关联?这取决于 pwq 对应 workqueue 的属性,如果 workqueue 是 high priority 的,就关联 highpri 的 worker pool,否则关联 normal 的 worker pool。

然后就是各个工厂根据市场上来的订单(产生的 work items)确立生产任务的要求,并根据订单生产所需的技能级别,招募高级工或者初级工,安排到对应的流水线上。在这个过程中,还需要根据订单量的变化,动态地调整工人的数目,以实现效益的最大化(CPU 资源最充分的利用)。

0

评论 (0)

取消