Scheduler 翻译成中文叫做调度器

调度器这个词对于学计算机的同学来说应该并不陌生,因为在大学操作系统的课堂上我们学过很多关于进程调度的知识。

  • 先来先服务(FCFS)
  • 短作业优先(SJF)
  • 优先级调度
  • 时间片轮转
  • 多级反馈队列

等等。

在这篇文章中,我们将会讨论高通 WLAN Host Driver 中调度器的实现。

为什么先学习调度器呢?

因为调度器是 WLAN Host Driver 中相对简单的部分,仅有 4 个源代码文件,而且调度器是 WLAN Host Driver 的核心部分,是各个模块之间沟通的桥梁。

源代码我转存到了我的 GitHub 仓库中,今天要看的部分在这里:

https://github.com/WANG-Guangxin/wlan-driver/tree/5113495b16420b49004c444715d2daae2066e7dc/qca-wifi-host-cmn/scheduler

也可以使用我的 opengrok 服务器来查看源代码,如果它还在线的话。

https://opengrok.dijk.eu.org/xref/wlan-driver/qca-wifi-host-cmn/scheduler/

Scheduler translates to 调度器 (dispatcher) in Chinese.

The word scheduler should be familiar to anyone in computer science — in university OS classes we learned a lot about process scheduling.

  • First Come First Served (FCFS)
  • Shortest Job First (SJF)
  • Priority scheduling
  • Round-robin
  • Multi-level feedback queue

and so on.

In this article, we’ll discuss the scheduler implementation in Qualcomm’s WLAN Host Driver.

Why study the scheduler first?

Because the scheduler is one of the simpler parts of the WLAN Host Driver — only 4 source files — and it’s also the core of the driver, acting as the bridge between all modules.

I’ve mirrored the source code to my GitHub repo; the part we’re looking at today is here:

https://github.com/WANG-Guangxin/wlan-driver/tree/5113495b16420b49004c444715d2daae2066e7dc/qca-wifi-host-cmn/scheduler

You can also browse the source on my opengrok server, if it’s still online.

https://opengrok.dijk.eu.org/xref/wlan-driver/qca-wifi-host-cmn/scheduler/

数据部分

scheduler 上下文

代码位置:

https://github.com/WANG-Guangxin/wlan-driver/blob/5113495b16420b49004c444715d2daae2066e7dc/qca-wifi-host-cmn/scheduler/inc/scheduler_core.h#L113

https://opengrok.dijk.eu.org/xref/wlan-driver/qca-wifi-host-cmn/scheduler/inc/scheduler_core.h?r=70a19e16789e308182f63b15c75decec7bf0b342#113


根据类型,消息体指针可以是指向内存的 bodyptr 指针,也可以是作为 32 位数据的 bodyval。bodyptr 总是可以被释放的指针,应始终确保 bodyptr 是可以被释放的。消息应该使用 bodyptr 或 bodyval,但不应同时使用两者!

👀
关于链表节点,我曾经在这篇文章中讨论过。

sched_history_item

代码位置:

https://github.com/WANG-Guangxin/wlan-driver/blob/5113495b16420b49004c444715d2daae2066e7dc/qca-wifi-host-cmn/scheduler/src/scheduler_core.c#L52

https://opengrok.dijk.eu.org/xref/wlan-driver/qca-wifi-host-cmn/scheduler/src/scheduler_core.c?r=8b3dca18206e1a0461492f082fa6e270b092c035#52

这个数据结构同于记录调度器的历史记录。

在 Scheduler 模块中,有一个全局变量 sched_history,它是一个数组,用于记录最近的 WLAN_SCHED_HISTORY_SIZE 条历史记录。

1
static struct sched_history_item sched_history[WLAN_SCHED_HISTORY_SIZE];

提醒一下,这些代码是否生效取决于 WLAN_SCHED_HISTORY_SIZE 是否被定义。如果没有定义 WLAN_SCHED_HISTORY_SIZE,那么和调度器历史有关的代码都不会参与编译。

sched_pool 🌶️

又到了内核代码极其抽象的宏定义时间了 🤣

这部分由 ChatGPT 给我们协助讲解。

其实内存池应该在一个单独的文章中讲解。

如果你学习过 malloc 的原理的话,其实可以把这里的 sched_pool 理解为用于分配 scheduler_msg 的数据结构。

代码位置:

https://github.com/WANG-Guangxin/wlan-driver/blob/5113495b16420b49004c444715d2daae2066e7dc/qca-wifi-host-cmn/scheduler/src/scheduler_core.c#L27

https://opengrok.dijk.eu.org/xref/wlan-driver/qca-wifi-host-cmn/scheduler/src/scheduler_core.c?r=8b3dca18206e1a0461492f082fa6e270b092c035#27

定义一个内存池,用下面这个宏函数。

1
2
DEFINE_QDF_FLEX_MEM_POOL(sched_pool, sizeof(struct scheduler_msg),
WLAN_SCHED_REDUCTION_LIMIT);

这个宏函数被写成了下面的样子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* DEFINE_QDF_FLEX_MEM_POOL() - define a new flex mem pool with one segment
* @name: the name of the pool variable
* @size_of_item: size of the items the pool will allocate
* @rm_limit: min number of segments to keep during reduction
*/
#define DEFINE_QDF_FLEX_MEM_POOL(name, size_of_item, rm_limit) \
struct qdf_flex_mem_pool name; \
uint8_t __ ## name ## _head_bytes[QDF_FM_BITMAP_BITS * (size_of_item)];\
struct qdf_flex_mem_segment __ ## name ## _head = { \
.node = QDF_LIST_NODE_INIT_SINGLE( \
QDF_LIST_ANCHOR(name.seg_list)), \
.bytes = __ ## name ## _head_bytes, \
}; \
struct qdf_flex_mem_pool name = { \
.seg_list = QDF_LIST_INIT_SINGLE(__ ## name ## _head.node), \
.reduction_limit = (rm_limit), \
.item_size = (size_of_item), \
}

这些代码被写在 qdf 模块里,已经超出了本文所介绍的范围,关于 QDF_FM_BITMAP_BITS, QDF_LIST_NODE_INIT_SINGLE, QDF_LIST_ANCHOR, QDF_LIST_INIT_SINGLE 等宏定义就不再继续展开了。
我们可以简单理解一下这个宏定义做了什么事。

对话 GPT-4o

Q1:

这个宏函数做了什么?

A1:

这个宏 DEFINE_QDF_FLEX_MEM_POOL 用于在C程序中定义并初始化一个带有一个段(segment)的灵活内存池。我们来详细解释一下这个宏的功能,然后应用到给定的例子中。

宏的详细解析

  1. 定义一个内存池结构体变量:

    1
    struct qdf_flex_mem_pool name;

    这行代码定义了一个类型为 struct qdf_flex_mem_pool 的新变量 name,用于表示内存池。

  2. 为段头部分配字节:

    1
    uint8_t __ ## name ## _head_bytes[QDF_FM_BITMAP_BITS * (size_of_item)];

    这行代码定义了一个名为 __name_head_bytes 的字节数组,大小为 QDF_FM_BITMAP_BITS * (size_of_item)。这个数组用于存储内存池的第一个段的数据。其中,QDF_FM_BITMAP_BITS 可能是一个常量,表示位图的位数,而 size_of_item 是每个池分配项的大小。

  3. 定义初始段:

    1
    2
    3
    4
    5
    struct qdf_flex_mem_segment __ ## name ## _head = { \
    .node = QDF_LIST_NODE_INIT_SINGLE( \
    QDF_LIST_ANCHOR(name.seg_list)), \
    .bytes = __ ## name ## _head_bytes, \
    };

    这段代码定义了一个名为 __name_head 的段结构体:

    • node: 使用 QDF_LIST_NODE_INIT_SINGLEQDF_LIST_ANCHOR 初始化为连接到内存池段列表的单节点。
    • bytes: 指向前面定义的 __name_head_bytes 数组,这是这个段实际的内存区域。
  4. 初始化内存池:

    1
    2
    3
    4
    5
    struct qdf_flex_mem_pool name = { \
    .seg_list = QDF_LIST_INIT_SINGLE(__ ## name ## _head.node), \
    .reduction_limit = (rm_limit), \
    .item_size = (size_of_item), \
    }

    这行代码初始化名为 name 的内存池结构体:

    • seg_list: 初始化为一个只包含一个段(__name_head.node)的列表。
    • reduction_limit: 设置为 rm_limit,表示在减少内存池时保留的最小段数。
    • item_size: 设置为 size_of_item,表示内存池管理的每个项的大小。

应用该宏

给定的用法:

1
DEFINE_QDF_FLEX_MEM_POOL(sched_pool, sizeof(struct scheduler_msg), WLAN_SCHED_REDUCTION_LIMIT);
  • name = sched_pool: 这会创建一个名为 sched_poolstruct qdf_flex_mem_pool 类型的变量。
  • size_of_item = sizeof(struct scheduler_msg): 该池中每个项的大小为 struct scheduler_msg 的大小。
  • rm_limit = WLAN_SCHED_REDUCTION_LIMIT: 在减少内存池时保留的最小段数由 WLAN_SCHED_REDUCTION_LIMIT 定义。

宏的功能总结

DEFINE_QDF_FLEX_MEM_POOL:

  • 定义一个带有指定名称的新的灵活内存池。
  • 根据它将管理的项的大小为第一个段分配预定义数量的内存。
  • 初始化内存池,设置链表和其他属性,比如项的大小和减少时保留的最小段数。

这个宏简化了灵活内存池的创建和初始化,使得能够更容易地使用设置好的结构和参数动态管理内存。

Q2:

如果理解“带有一个段(segment)”

A2:

“带有一个段(segment)” 指的是在创建内存池时,内存池初始化时包含一个内存段。每个段是内存池中的一个独立块,用于存储特定数量的内存项。

在这个宏的上下文中,一个“段”就是一个内存块,其中包含了若干个固定大小的内存项。段的用途在于管理内存的分配和释放,通常在需要动态分配和回收大量小内存块的情况下使用。通过使用段,可以减少内存碎片,优化内存使用,同时也便于内存管理操作(如扩展、缩减内存池)。

具体来说,“带有一个段”在这里的含义可以理解为:

  1. 初始内存分配: 宏定义的内存池在创建时,会先分配一段固定大小的内存,这段内存称为“段”。在代码中,使用一个字节数组 __name_head_bytes 来实际存储这些内存数据。

  2. 段的结构: 每个段都有一个管理节点(node),用于在池的段列表中进行链表管理;并且有一个指向实际数据的指针(bytes),即 __name_head_bytes

  3. 段的数量: 虽然这里初始化时只有一个段,但整个设计是支持多个段的(如通过链表结构管理多个段)。段的数量可以根据需要增加或减少,reduction_limit 参数指定了在减少段时,池中最少需要保留的段数量。

总结来说,“带有一个段”意味着这个内存池一开始就包含了一个已经分配好的内存块,这个块被组织成段,用于存储和管理内存项。随着需求变化,可以动态调整段的数量来管理内存使用。

Q3:

当需要更多内存时,这个内存池会做哪些事情

A3:

当需要更多内存时,这个内存池可能会执行以下操作来分配更多的内存段。这些操作的具体实现取决于内存池的设计和用途。一般来说,灵活的内存池在内存需求增加时会按需扩展,并进行以下步骤:

  1. 分配新的内存段

    • 如果现有的段已满或无法满足新的内存请求,内存池会分配一个新的内存段(segment)。
    • 这涉及为新段分配一个大小为 QDF_FM_BITMAP_BITS * size_of_item 的字节数组。QDF_FM_BITMAP_BITSsize_of_item 共同决定了每个段中可以存储多少项。
    • 新的段通常会使用类似于初始化第一个段的过程,即创建一个新字节数组来保存数据,并将其链接到段链表中。
  2. 将新段加入段链表

    • 新分配的段通过链表加入到内存池的段列表中。这些段是通过链表结构(如双向链表或单向链表)管理的,以便快速插入和删除。
    • 在宏中,有 QDF_LIST_INIT_SINGLEQDF_LIST_NODE_INIT_SINGLE 这样的宏,可能用于初始化和操作链表节点,这些节点帮助管理不同段之间的关系。
  3. 更新内存池的管理结构

    • 内存池的元数据需要更新,例如:
      • 段列表的头部指针或尾部指针。
      • 内存池中当前段的数量。
      • 可能还有内存池的总大小或已使用的内存总量。
  4. 扩展段的管理

    • 如果内存池有内存回收机制,当内存段不再需要时(例如,当内存项被释放后),池会检查是否可以回收某些段,以节省内存资源。
    • 宏中提到的 reduction_limit 是用于减少内存段时的最小段数限制,在增加段时也会考虑到这个限制,以确保内存管理的稳定性和性能。
  5. 重分配失败时的错误处理

    • 如果内存池在尝试分配新段时失败(例如,没有足够的系统内存),内存池需要有一定的错误处理机制,如记录错误、触发报警、或执行垃圾回收来释放未使用的内存。
  6. 可能的内存对齐和优化

    • 在某些情况下,内存池可能会进行内存对齐操作,以确保内存访问的效率。
    • 对齐和优化可以避免内存碎片,并确保高效的内存使用。

代码示例

虽然你提供的宏并没有完整展示内存池的动态扩展机制,但假设这部分逻辑在内存池实现的其他部分中可以是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void expand_memory_pool(struct qdf_flex_mem_pool *pool) {
// 分配新的内存段
struct qdf_flex_mem_segment *new_segment = allocate_new_segment(pool->item_size);

if (new_segment == NULL) {
// 处理内存分配失败
handle_memory_allocation_failure();
return;
}

// 将新段加入段链表
QDF_LIST_INSERT_TAIL(&pool->seg_list, &new_segment->node);

// 更新段计数器等管理结构
pool->num_segments++;
}

总结

当需要更多内存时,这个内存池会通过分配新的内存段、将新段加入管理链表、更新元数据,以及可能的错误处理来满足需求。这种机制使得内存池能够灵活地适应内存需求的变化,同时有效管理内存的分配和回收。

Q4:

现在让我来解释一下内存池,你来给我指正:

这个内存池就是预先申请了一大块内存,然后把这个内存组织成若干小块。每个小块包含两部分,一部分是实际使用的内存项,另一部分相当于一个头部,利用头部的指针信息把这些小块串联成一个链表。这个内存池可以对外部提供 get 内存项 和 free 内存项的 api,就像 malloc 和 free 一样。当一个内存项被 free 给内存池时,内存池不一定把这个内存交给操作系统,而是通过调整"头部"的指针信息来合并一些小的内存块,避免内存碎片。

A4:

你的解释总体来说是正确的,清晰地描述了内存池的基本工作原理。内存池的确通过预先申请一大块内存,将其分割成小块来高效管理内存分配和释放。下面是一些具体的指正和补充,帮助更好地理解内存池的运作:

  1. 头部与数据部分的关系

    • 你提到每个小块包含两部分:实际使用的内存项和一个头部。这是准确的。头部通常包含管理信息,例如指向下一个块的指针、块的状态(空闲或已用),以及其他管理所需的元数据。
    • 这种设计可以让内存池在不需要额外的内存分配和释放的情况下管理内存块,减少内存碎片的产生。
  2. 链表的使用

    • 你提到利用头部的指针信息把这些小块串联成一个链表。这也是正确的。链表结构在内存池中很常见,因为它允许高效地插入和删除块,适合动态内存分配的场景。
    • 通常,空闲块会被组织成一个空闲链表。每次分配时,内存池从空闲链表中取出一个合适大小的块;每次释放时,将块插回空闲链表。
  3. get 和 free API 的实现

    • 内存池的 get 和 free 操作类似于 mallocfree,但通常会更高效,因为它们不涉及系统调用。get 可能从空闲链表中找到一个适当大小的块并返回给调用者。free 则把块返回到空闲链表中。
    • 内存池的好处是减少了频繁的系统级内存分配操作,系统调用通常较慢且可能引入碎片。而内存池管理的内存通常在堆上,是应用级的。
  4. 内存碎片管理

    • 你提到内存池在释放时可能不会将内存返还给操作系统,而是通过调整头部指针来合并小的内存块。这种做法可以减少内存碎片,提高内存使用效率。
    • 具体来说,内存池可能实现某种内存合并策略,如相邻空闲块的合并(coalescing),以此来减少内存碎片,使得内存池中有足够大的连续空间来满足未来的内存分配请求。
  5. 可能的扩展

    • 当内存池中没有足够的空闲块时,内存池可能会扩展,即再申请一块内存并将其加入内存池的管理结构中。这可以通过追加新的段来实现,如前面描述的那样。

总结

你对内存池的描述基本上是正确的,并且涵盖了主要概念。内存池通过预先申请一大块内存,并将其组织成多个小块来管理内存。这种方法避免了频繁的系统调用,从而提高了性能,并通过合并和管理内存块的策略来减少碎片。内存池的实现涉及对内存块的分配、释放、以及碎片管理策略的设计。总体来说,你对内存池的理解是准确的,描述也是很清楚的!

逻辑部分

初始化

Scheduler 模块的初始化函数是 scheduler_init, 它在 hdd_module_init 中被调用。

整个函数的调用链如下:

module_init(hdd_module_init);
–>
hdd_module_init
–>
hdd_driver_load
–>
hdd_component_init
–>
dispatcher_init
–>
/*

  • scheduler INIT has to be the last as each component’s
  • initialization has to happen first and then at the end
  • scheduler needs to start accepting the service.
    */
    if (QDF_STATUS_SUCCESS != scheduler_init())
    goto scheduler_init_fail;

然后进入我们的主题:scheduler_init

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
QDF_STATUS scheduler_init(void)
{
QDF_STATUS status;
struct scheduler_ctx *sched_ctx;

sched_debug("Initializing Scheduler");

status = scheduler_create_ctx();
/*
* 创建上下文就只有两行代码:
* qdf_flex_mem_init(&sched_pool);
* gp_sched_ctx = &g_sched_ctx
*
* qdf 的函数暂时看作黑盒就可以,它的作用就是初始化了一个 sched_pool 内存池。
* 同时初始化了一个全局上下文指针 gp_sched_ctx 指向了一个 scheduler_ctx 结构体。
*/
if (QDF_IS_STATUS_ERROR(status)) {
sched_fatal("Failed to create context; status:%d", status);
return status;
}

sched_ctx = scheduler_get_context();
QDF_BUG(sched_ctx);
if (!sched_ctx) {
status = QDF_STATUS_E_FAILURE;
goto ctx_destroy;
}

status = scheduler_queues_init(sched_ctx);
/*
* 初始化队列,对 sched_ctx 中的消息队列进行初始化。
* 在函数内部调用了 scheduler_all_queues_init, 下面我们跟踪一下这个函数内容。
*/
if (QDF_IS_STATUS_ERROR(status)) {
sched_fatal("Failed to init queues; status:%d", status);
goto ctx_destroy;
}

status = qdf_event_create(&sched_ctx->sch_start_event);
if (QDF_IS_STATUS_ERROR(status)) {
sched_fatal("Failed to create start event; status:%d", status);
goto queues_deinit;
}

status = qdf_event_create(&sched_ctx->sch_shutdown);
if (QDF_IS_STATUS_ERROR(status)) {
sched_fatal("Failed to create shutdown event; status:%d",
status);
goto start_event_destroy;
}

status = qdf_event_create(&sched_ctx->resume_sch_event);
if (QDF_IS_STATUS_ERROR(status)) {
sched_fatal("Failed to create resume event; status:%d", status);
goto shutdown_event_destroy;
}

qdf_spinlock_create(&sched_ctx->sch_thread_lock);
qdf_init_waitqueue_head(&sched_ctx->sch_wait_queue);
sched_ctx->sch_event_flag = 0;
sched_ctx->timeout = SCHEDULER_WATCHDOG_TIMEOUT;
qdf_timer_init(NULL,
&sched_ctx->watchdog_timer,
&scheduler_watchdog_timeout,
sched_ctx,
QDF_TIMER_TYPE_SW);

qdf_register_mc_timer_callback(scheduler_mc_timer_callback);

return QDF_STATUS_SUCCESS;

shutdown_event_destroy:
qdf_event_destroy(&sched_ctx->sch_shutdown);

start_event_destroy:
qdf_event_destroy(&sched_ctx->sch_start_event);

queues_deinit:
scheduler_queues_deinit(sched_ctx);

ctx_destroy:
scheduler_destroy_ctx();

return status;
}

scheduler_all_queues_init

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
static qdf_atomic_t __sched_queue_depth; 
static qdf_atomic_t __sched_dup_fail_count;

static QDF_STATUS scheduler_all_queues_init(struct scheduler_ctx *sched_ctx)
{
QDF_STATUS status;
int i;

sched_enter();

QDF_BUG(sched_ctx);
if (!sched_ctx)
return QDF_STATUS_E_FAILURE;

qdf_atomic_set(&__sched_queue_depth, 0);
/* 这里为静态原子变量队列深度设置为 0 */

/* Initialize all message queues */
/* 前面介绍过 SCHEDULER_NUMBER_OF_MSG_QUEUE 的数量默认是 6
* scheduler_mq_init 这里本质也是调用了 qdf 的函数来初始化一个 list 的数据结构,暂时也不考虑细节了
*/
for (i = 0; i < SCHEDULER_NUMBER_OF_MSG_QUEUE; i++) {
status = scheduler_mq_init(&sched_ctx->queue_ctx.sch_msg_q[i]);
if (QDF_STATUS_SUCCESS != status)
return status;
}

/* Initialize all qid to qidx mapping to invalid values */
for (i = 0; i < QDF_MODULE_ID_MAX; i++)
sched_ctx->queue_ctx.scheduler_msg_qid_to_qidx[i] =
SCHEDULER_NUMBER_OF_MSG_QUEUE;

sched_exit();

return status;
}

Enable

开启调度器的入口是 Kernel 来调用驱动程序的 probe 函数,在这里是 wlan_hdd_pld_probe

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
struct pld_driver_ops wlan_drv_ops = {
.probe = 'wlan_hdd_pld_probe',
.remove = wlan_hdd_pld_remove,
.idle_shutdown = wlan_hdd_pld_idle_shutdown,
.idle_restart = wlan_hdd_pld_idle_restart,
.shutdown = wlan_hdd_pld_shutdown,
.reinit = wlan_hdd_pld_reinit,
.crash_shutdown = wlan_hdd_pld_crash_shutdown,
.suspend = wlan_hdd_pld_suspend,
.resume = wlan_hdd_pld_resume,
.suspend_noirq = wlan_hdd_pld_suspend_noirq,
.resume_noirq = wlan_hdd_pld_resume_noirq,
.reset_resume = wlan_hdd_pld_reset_resume,
.modem_status = wlan_hdd_pld_notify_handler,
.uevent = wlan_hdd_pld_uevent,
#ifdef WLAN_FEATURE_SSR_DRIVER_DUMP
.collect_driver_dump = wlan_hdd_pld_collect_driver_dump,
#endif
#ifdef FEATURE_RUNTIME_PM
.runtime_suspend = wlan_hdd_pld_runtime_suspend,
.runtime_resume = wlan_hdd_pld_runtime_resume,
#endif
.set_curr_therm_cdev_state = wlan_hdd_pld_set_thermal_mitigation,
};

整个函数的调用链如下:

wlan_hdd_pld_probe
–>
hdd_soc_probe
–>
__hdd_soc_probe
–>
hdd_wlan_startup
–>
hdd_wlan_start_modules
–>
cds_open
–>
dispatcher_enable
–>
scheduler_enable

到这里就进入了本节主题:scheduler_enable

这里也是调度器多线程开始的地方。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
QDF_STATUS scheduler_enable(void)
{
struct scheduler_ctx *sched_ctx;

sched_debug("Enabling Scheduler");

sched_ctx = scheduler_get_context();
QDF_BUG(sched_ctx);
if (!sched_ctx)
return QDF_STATUS_E_INVAL;

qdf_atomic_clear_bit(MC_SHUTDOWN_EVENT_MASK,
&sched_ctx->sch_event_flag);
qdf_atomic_clear_bit(MC_POST_EVENT_MASK,
&sched_ctx->sch_event_flag);

/* create the scheduler thread */
/* 这里创建一个调度器线程,这个线程执行的函数是 scheduler_thread, 数据是 sched_ctx, 名字是 "scheduler_thread" */
sched_ctx->sch_thread = qdf_create_thread(scheduler_thread, sched_ctx,
"scheduler_thread");
if (!sched_ctx->sch_thread) {
sched_fatal("Failed to create scheduler thread");
return QDF_STATUS_E_RESOURCES;
}

sched_debug("Scheduler thread created");

/* wait for the scheduler thread to startup */
/* 这里首先唤醒了调度器线程 , 也就是在另一个线程里开始执行 scheduler_thread 函数*/
qdf_wake_up_process(sched_ctx->sch_thread);

/* 这行代码是使当前线程等待一个特定的事件:sch_start_event
* 所谓的等待 sch_start_event 事件,其实可以理解为等待 sch_start_event 这个变量的值变为非 0。
*
* 第二个参数是超时时间,0 表示无限等待。
*
*/
qdf_wait_single_event(&sched_ctx->sch_start_event, 0);

sched_debug("Scheduler thread started");

return QDF_STATUS_SUCCESS;
}

那么 sch_start_event 什么时候会变成非 0 呢?应该在 scheduler_thread 函数里面进行设置。

我先大概画了一下这两个线程的状态迁移过程,如下图。

然后我们再继续看代码。

主线程
状态: 活跃
主线程 状态: 活跃
调度器线程
状态:非活跃
调度器线程 状态:非活跃
创建一个新线程
创建一个新线程
主线程
状态: 活跃
主线程 状态: 活跃
调度器线程
状态:活跃
调度器线程 状态:活跃
唤醒新线程
唤醒新线程
主线程
状态: 非活跃
等待 sch_start_event变成非 0
主线程状态: 非活跃…
调度器线程
状态:活跃
做事中…
调度器线程状态:活跃做事中…
主线程
状态: 非活跃
等待 sch_start_event变成非 0
主线程状态: 非活跃…
调度器线程
状态:活跃
设置 sch_start_event 为非 0
调度器线程状态:活跃设置 sch_start_event 为非 0…
至此,两个线程
就各自做事了
至此,两个线程 就各自做事了
主线程
状态: 活跃
等待事件已经发生,继续向下执行
主线程状态: 活跃…
调度器线程
状态:活跃
做事中…
调度器线程状态:活跃做事中…
Text is not SVG - cannot display

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
int scheduler_thread(void *arg)
{
struct scheduler_ctx *sch_ctx = (struct scheduler_ctx *)arg;
int retWaitStatus = 0;
bool shutdown = false;

if (!arg) {
QDF_DEBUG_PANIC("arg is null");
return 0;
}
// 这里莫名其妙出现了一个 current
// 在 Linux 内核编程中,current 是一个宏,它指向正在运行的进程的 task_struct 结构。这个结构包含了关于进程的所有信息,如进程 ID、进程状态、虚拟内存信息等。
qdf_set_user_nice(current, -2);
// 这里的 qdf_set_user_nice(current, -2); 是设置当前线程的 nice 值,nice 值是用来决定 Linux 调度优先级的,数值范围是 -20 到 19,数值越小优先级越高。

/* Ack back to the context from which the main controller thread
* has been created
*/
// 这里是通知主线程,调度线程已经启动
// 也就是设置 sch_start_event 变量非 0,这行代码执行完成后,主线程就会继续往下执行
qdf_event_set(&sch_ctx->sch_start_event);
sched_debug("scheduler thread %d (%s) starting up",
current->pid, current->comm);

while (!shutdown) {
/* This implements the execution model algorithm */
// 这里是调度线程的主要逻辑, 非常像 WebServer 里面的线程池
// qdf_wait_queue_interruptible(sch_ctx->sch_wait_queue, condition) 这个函数会使当前线程进入睡眠状态,
// 直到满足指定的条件。这里的条件是
// qdf_atomic_test_bit(MC_POST_EVENT_MASK, &sch_ctx->sch_event_flag) || qdf_atomic_test_bit(MC_SUSPEND_EVENT_MASK, &sch_ctx->sch_event_flag)
// 也就是当 sch_ctx->sch_event_flag 的 MC_POST_EVENT_MASK 或者 MC_SUSPEND_EVENT_MASK 位被设置时,线程会被唤醒
retWaitStatus = qdf_wait_queue_interruptible(
sch_ctx->sch_wait_queue,
qdf_atomic_test_bit(MC_POST_EVENT_MASK,
&sch_ctx->sch_event_flag) ||
qdf_atomic_test_bit(MC_SUSPEND_EVENT_MASK,
&sch_ctx->sch_event_flag));

if (retWaitStatus == -ERESTARTSYS)
QDF_DEBUG_PANIC("Scheduler received -ERESTARTSYS");

// 这里是清除 sch_ctx->sch_event_flag 的 MC_POST_EVENT_MASK 位
// 这里为什么要先清楚这个 bit 而不是等到处理完消息再清除,我想应该是为了防止在处理消息的过程中,又有新的消息进来
qdf_atomic_clear_bit(MC_POST_EVENT_MASK, &sch_ctx->sch_event_flag);

// 调度器线程处理所有消息队列
scheduler_thread_process_queues(sch_ctx, &shutdown);
}

/* If we get here the scheduler thread must exit */
sched_debug("Scheduler thread exiting");
qdf_event_set(&sch_ctx->sch_shutdown);

return 0;
}

scheduler_thread_process_queues 用于处理 6 个队列中的消息,这 6 个队列按优先级排序,首先处理第一个队列的所有消息,如果第一个队列的所有消息处理完毕,然后检查第二个队列,以此类推,直到所有队列的消息都处理完毕,或者收到 shutdown 信号。

这里再解释一下上面为什么要先清除 MC_POST_EVENT_MASK 位,再处理消息。

我们首先要知道 MC_POST_EVENT_MASK 位是在哪里设置的,这个位是在 scheduler_post_message 函数中设置的,这个函数是用来向调度器线程发送消息的,当发送消息时,会设置 MC_POST_EVENT_MASK 位,这个位是用来通知调度器线程有新的消息需要处理。

考虑下面这一种情况。

  1. 调度器处理完成了第一个队列的所有消息,然后开始检查第二个队列。
  2. 此时有一个新消息被 post 到第一个消息队列,然后设置了 MC_POST_EVENT_MASK 位。
  3. 调度器依次检查了所有队列,处理完成了所有消息,然后执行了 qdf_atomic_clear_bit(MC_POST_EVENT_MASK, &sch_ctx->sch_event_flag);,这就导致了再调度器线程进行循环检查消息队列时,会发现暂时没有MC_POST_EVENT_MASK被设置,就会持续等待。但请注意,此时第一个队列中有一个新消息,这个消息并没有被处理。它会等到下一次 post 消息到来时,才会被处理。这就导致了延迟。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
static void scheduler_thread_process_queues(struct scheduler_ctx *sch_ctx,
bool *shutdown)
{
int i;
QDF_STATUS status;
struct scheduler_msg *msg;

if (!sch_ctx) {
QDF_DEBUG_PANIC("sch_ctx is null");
return;
}

/* start with highest priority queue : timer queue at index 0 */
i = 0;
while (i < SCHEDULER_NUMBER_OF_MSG_QUEUE) {
/* Check if MC needs to shutdown */
// 如果设置的 MC_SHUTDOWN_EVENT_MASK 位被设置,就会设置 shutdown 为 true
// shutdown 为 true 时,scheduler_thread 线程中的 while 循环会退出
if (qdf_atomic_test_bit(MC_SHUTDOWN_EVENT_MASK,
&sch_ctx->sch_event_flag)) {
sched_debug("scheduler thread signaled to shutdown");
*shutdown = true;

/* Check for any Suspend Indication */
if (qdf_atomic_test_and_clear_bit(MC_SUSPEND_EVENT_MASK,
&sch_ctx->sch_event_flag)) {
/* Unblock anyone waiting on suspend */
if (gp_sched_ctx->hdd_callback)
gp_sched_ctx->hdd_callback();
}

break;
}

// 这里的本质就是从第 i 个消息队列中取出头部的消息
msg = scheduler_mq_get(&sch_ctx->queue_ctx.sch_msg_q[i]);
if (!msg) {
/* check next queue */
// 如果消息是空,就检查下一个消息队列。
i++;
continue;
}

// 每个消息队列有一个处理函数 handler,这里调用 handler 处理消息
// 关于 handler 的注册,本文后面会讲到
if (sch_ctx->queue_ctx.scheduler_msg_process_fn[i]) {
// 首先设置 watchdog 相关的变量
// 消息类型,以及 watchdog 回调函数
// 这里解释一下 watchdog 是用来干什么的,watchdog 是用来监控消息处理的时间的,如果消息处理时间超过了预设的时间,就会触发 watchdog 回调函数
sch_ctx->watchdog_msg_type = msg->type;
sch_ctx->watchdog_callback = msg->callback;

sched_history_start(msg);

// 开启一个定时器,定时器的时间是 sch_ctx->timeout
// watchdog_timer 的回调函数是什么?
// 可以搜索一下 qdf_timer_init 在哪里被调用,我们上文已经介绍过,定时器的回调函数是 scheduler_watchdog_timeout
qdf_timer_start(&sch_ctx->watchdog_timer,
sch_ctx->timeout);

// 调用 handler 处理消息
status = sch_ctx->queue_ctx.
scheduler_msg_process_fn[i](msg);

// 如果上一行代码执行成功,就停止 watchdog 定时器
qdf_timer_stop(&sch_ctx->watchdog_timer);
sched_history_stop();

if (QDF_IS_STATUS_ERROR(status))
sched_err("Failed processing Qid[%d] message",
sch_ctx->queue_ctx.sch_msg_q[i].qid);

// 释放消息
scheduler_core_msg_free(msg);
}

/* start again with highest priority queue at index 0 */
i = 0;
}

/* Check for any Suspend Indication */
if (qdf_atomic_test_and_clear_bit(MC_SUSPEND_EVENT_MASK,
&sch_ctx->sch_event_flag)) {
qdf_spin_lock(&sch_ctx->sch_thread_lock);
qdf_event_reset(&sch_ctx->resume_sch_event);
/* controller thread suspend completion callback */
if (gp_sched_ctx->hdd_callback)
gp_sched_ctx->hdd_callback();
qdf_spin_unlock(&sch_ctx->sch_thread_lock);
/* Wait for resume indication */
qdf_wait_single_event(&sch_ctx->resume_sch_event, 0);
}

return; /* Nothing to process wait on wait queue */
}

如果 sch_ctx->timeout 时间内 handler 没有处理完成,那么就会调用 scheduler_watchdog_timeout

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
static void scheduler_watchdog_timeout(void *arg)
{
struct scheduler_ctx *sched = arg;

if (qdf_is_recovering()) {
sched_debug("Recovery is in progress ignore timeout");
return;
}

scheduler_watchdog_notify(sched);
if (sched->sch_thread)
qdf_print_thread_trace(sched->sch_thread);

/* avoid crashing during shutdown */
if (qdf_atomic_test_bit(MC_SHUTDOWN_EVENT_MASK, &sched->sch_event_flag))
return;

sched_err("Triggering self recovery on sheduler timeout");
qdf_trigger_self_recovery(NULL, QDF_SCHED_TIMEOUT);
}

static inline void scheduler_watchdog_notify(struct scheduler_ctx *sched)
{
char symbol[QDF_SYMBOL_LEN];

// sched->watchdog_callback 在 scheduler_thread_process_queues 中被设置为 msg->callback
// 其实并不是要执行 watchdog_callback,而是要打印 watchdog_callback 的名字
if (sched->watchdog_callback)
qdf_sprint_symbol(symbol, sched->watchdog_callback);

sched_fatal("Callback %s (type 0x%x) exceeded its allotted time of %ds",
sched->watchdog_callback ? symbol : "<null>",
sched->watchdog_msg_type,
sched->timeout / 1000);
}

handler 注册

上一节中提到了每个消息队列有自己的handler,这个 handler 是在哪里注册的呢?

在上一节中的 cds_open 函数中,调用了 cds_register_all_modules 函数,这个函数会调用scheduler_register_module来为每个队列注册 handler。

cds_open
–>
dispatcher_enable
cds_register_all_modules
–>
scheduler_register_module

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
static QDF_STATUS cds_register_all_modules(void)
{
QDF_STATUS status;

scheduler_register_wma_legacy_handler(&wma_mc_process_handler);
scheduler_register_sys_legacy_handler(&sys_mc_process_handler);

/* Register message queues in given order such that queue priority is
* intact:
* 1) QDF_MODULE_ID_SYS: Timer queue(legacy SYS queue)
* 2) QDF_MODULE_ID_TARGET_IF: Target interface queue
* 3) QDF_MODULE_ID_PE: Legacy PE message queue
* 4) QDF_MODULE_ID_SME: Legacy SME message queue
* 5) QDF_MODULE_ID_OS_IF: OS IF message queue for new components
*/
status = scheduler_register_module(QDF_MODULE_ID_SYS,
&scheduler_timer_q_mq_handler);
status = scheduler_register_module(QDF_MODULE_ID_TARGET_IF,
&scheduler_target_if_mq_handler);
status = scheduler_register_module(QDF_MODULE_ID_PE,
&pe_mc_process_handler);
status = scheduler_register_module(QDF_MODULE_ID_SME,
&sme_mc_process_handler);
status = scheduler_register_module(QDF_MODULE_ID_OS_IF,
&scheduler_os_if_mq_handler);
status = scheduler_register_module(QDF_MODULE_ID_SCAN,
&scheduler_scan_mq_handler);
return status;
}

QDF_STATUS scheduler_register_module(QDF_MODULE_ID qid,
scheduler_msg_process_fn_t callback)
{
struct scheduler_mq_ctx *ctx;
struct scheduler_ctx *sched_ctx = scheduler_get_context();

sched_enter();

QDF_BUG(sched_ctx);
if (!sched_ctx)
return QDF_STATUS_E_FAILURE;

if (sched_ctx->sch_last_qidx >= SCHEDULER_NUMBER_OF_MSG_QUEUE) {
sched_err("Already registered max %d no of message queues",
SCHEDULER_NUMBER_OF_MSG_QUEUE);
return QDF_STATUS_E_FAILURE;
}

ctx = &sched_ctx->queue_ctx;
ctx->scheduler_msg_qid_to_qidx[qid] = sched_ctx->sch_last_qidx;
ctx->sch_msg_q[sched_ctx->sch_last_qidx].qid = qid;
ctx->scheduler_msg_process_fn[sched_ctx->sch_last_qidx] = callback;
sched_ctx->sch_last_qidx++;

sched_exit();

return QDF_STATUS_SUCCESS;
}

每个 handler 都会调用 msg 中设置的 callback 函数,但针对不同队列的消息,可能会有属于这个队列的特殊处理,所以要为每个队列注册一个 handler。

本文写到这里我已经发现我写的很烂。想要用文字讲解代码还是很困难。有时我又比较拘泥于细节,但仍然还有很多细节没有讲到。我觉的我继续讲下去只会更加混乱,所以本文暂时讲到这里。

希望对你有所帮助!

外部 API

2024-09-22 更新

最后再补充一下外部模块如何和调度器模块交互的。

最频繁被调用的外部 API 就是 scheduler_post_message,这个函数用来向调度器线程发送消息。

它是一个宏定义,参数分别是 源模块 ID,目标模块 ID,队列 ID,消息指针。

它被定义为 scheduler_post_message_debug,这个函数会在原始参数的基础上加上行号和函数名两个参数,这样在调试的时候可以知道是哪个函数调用了 scheduler_post_message

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* scheduler_post_message() - post normal messages(no priority)
* @src_id: Source module of the message
* @dest_id: Destination module of the message
* @que_id: Queue to which the message has to posted.
* @msg: message pointer
*
* This function will mask the src_id, and destination id to qid of
* scheduler_post_msg
*
* Return: QDF status
*/
#define scheduler_post_message(src_id, dest_id, que_id, msg) \
scheduler_post_message_debug(src_id, dest_id, que_id, msg, \
__LINE__, __func__)

scheduler_post_message_debug 会继续调用 scheduler_post_msg,这个函数会将消息放入到消息队列中。
只不过这里做了一个简单的加工,将消息的源模块 ID,目标模块 ID,队列 ID 转换为 qid。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
QDF_STATUS scheduler_post_message_debug(QDF_MODULE_ID src_id,
QDF_MODULE_ID dest_id,
QDF_MODULE_ID que_id,
struct scheduler_msg *msg,
int line,
const char *func)
{
QDF_STATUS status;

status = scheduler_post_msg(scheduler_get_qid(src_id, dest_id, que_id),
msg);

if (QDF_IS_STATUS_ERROR(status))
sched_err("couldn't post from %d to %d - called from %d, %s",
src_id, dest_id, line, func);

return status;
}

scheduler_get_qid 极其相关的宏定义如下。
0x3FF 的二进制表示就是 10 个 1,也就是说,scheduler_get_src_id 取的是 qid 的高 10 位,scheduler_get_dest_id 取的是 qid 的中间 10 位,scheduler_get_que_id 取的是 qid 的低 10 位。

scheduler_get_qid 则是一种逆运算,将 src_iddest_idque_id 通过移位的方式放在 qid 的不同位置。

这样做可以理解为一种压缩内存的方式,将三个取值范围小于 1024 的数值压缩到一个 32 位的整数中。

1
2
3
4
5
#define scheduler_get_src_id(qid)       (((qid) >> 20) & 0x3FF)
#define scheduler_get_dest_id(qid) (((qid) >> 10) & 0x3FF)
#define scheduler_get_que_id(qid) ((qid) & 0x3FF)
#define scheduler_get_qid(src, dest, que_id) ((que_id) | ((dest) << 10) |\
((src) << 20))

继续向下追踪,

scheduler_post_msg 会调用 scheduler_post_msg_by_priority 函数,这个函数会根据消息的优先级,将消息放入消息队列的不同位置中。

1
2
3
4
5
static inline QDF_STATUS scheduler_post_msg(uint32_t qid,
struct scheduler_msg *msg)
{
return scheduler_post_msg_by_priority(qid, msg, false);
}

scheduler_post_msg_by_priority 才是最通用的函数,它的第三个参数是一个布尔值,用来决定是否按照优先级插入消息队列。

在常规模式下,如果外部模块调用的是 scheduler_post_message,那么这个参数就是 false。

这个函数的实现比较长,大家可以查看我的注释。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
QDF_STATUS scheduler_post_msg_by_priority(uint32_t qid,
struct scheduler_msg *msg,
bool is_high_priority)
{
// 关于这些可以去看前面的数据部分
uint8_t qidx;
struct scheduler_mq_type *target_mq;
struct scheduler_msg *queue_msg;
struct scheduler_ctx *sched_ctx;
uint16_t src_id;
uint16_t dest_id;
uint16_t que_id;

QDF_BUG(msg);
if (!msg)
return QDF_STATUS_E_INVAL;

// 拿到调度器上下文指针
sched_ctx = scheduler_get_context();
QDF_BUG(sched_ctx);
if (!sched_ctx)
return QDF_STATUS_E_INVAL;

if (!sched_ctx->sch_thread) {
sched_err("Cannot post message; scheduler thread is stopped");
return QDF_STATUS_E_FAILURE;
}

// 检查消息是否初始化
// 外部模块 post message 时,会在它的函数内部初始化一个局部变量 msg,此时保留位就全都是 0
// 如果是给 SYS 模块发送消息,那么保留位应该被填入 SYS_MSG_COOKIE
if (msg->reserved != 0 && msg->reserved != SYS_MSG_COOKIE) {
QDF_DEBUG_PANIC("Scheduler messages must be initialized");
return QDF_STATUS_E_FAILURE;
}

// 解包 qid
dest_id = scheduler_get_dest_id(qid);
src_id = scheduler_get_src_id(qid);
que_id = scheduler_get_que_id(qid);

if (que_id >= QDF_MODULE_ID_MAX || src_id >= QDF_MODULE_ID_MAX ||
dest_id >= QDF_MODULE_ID_MAX) {
sched_err("Src_id/Dest_id invalid, cannot post message");
return QDF_STATUS_E_FAILURE;
}
/* Target_If is a special message queue in phase 3 convergence because
* its used by both legacy WMA and as well as new UMAC components which
* directly populate callback handlers in message body.
* 1) WMA legacy messages should not have callback
* 2) New target_if message needs to have valid callback
* Clear callback handler for legacy WMA messages such that in case
* if someone is sending legacy WMA message from stack which has
* uninitialized callback then its handled properly. Also change
* legacy WMA message queue id to target_if queue such that its always
* handled in right order.
*/
// 这里为 QDF_MODULE_ID_WMA 进行了特殊处理
// 如果是 WMA 模块发送的消息,那么 callback 会被清空
// 然后将 WMA 模块的消息队列 ID 转换为 target_if 模块的消息队列 ID
// 这样就可以保证消息的处理顺序是正确的
if (QDF_MODULE_ID_WMA == que_id) {
msg->callback = NULL;
/* change legacy WMA message id to new target_if mq id */
que_id = QDF_MODULE_ID_TARGET_IF;
}

// src_id, dest_id 的唯一用处就是这里,用来打印日志
// 在调度器模块内部,消息的处理是根据 que_id 来处理的
qdf_mtrace(src_id, dest_id, msg->type, 0xFF, 0);

// 根据队列 ID 拿到队列的索引
qidx = sched_ctx->queue_ctx.scheduler_msg_qid_to_qidx[que_id];
if (qidx >= SCHEDULER_NUMBER_OF_MSG_QUEUE) {
sched_err("Scheduler is deinitialized ignore msg");
return QDF_STATUS_E_FAILURE;
}

if (!sched_ctx->queue_ctx.scheduler_msg_process_fn[qidx]) {
sched_err("callback not registered for qid[%d]", que_id);
return QDF_STATUS_E_FAILURE;
}

// 拿到目标队列
target_mq = &(sched_ctx->queue_ctx.sch_msg_q[qidx]);

// 复制消息
// 这里为什么要复制消息呢?
// 因为消息是在外部模块的栈上分配的,当外部模块的函数返回时,消息就会被销毁
// 所以这里需要复制一份消息,然后放入消息队列中
// 这里的消息是在调度器模块的堆上分配的,所以不会被销毁
// 具体的“堆”就是在调度器模块初始化时,申请的一块内存池
queue_msg = scheduler_core_msg_dup(msg);
if (!queue_msg)
return QDF_STATUS_E_NOMEM;

// 将消息放入队列
// 如果是高优先级消息,就放在队列的头部
// 否则放在队列的尾部
if (is_high_priority)
scheduler_mq_put_front(target_mq, queue_msg);
else
scheduler_mq_put(target_mq, queue_msg);

// 设置 MC_POST_EVENT_MASK 位
// 这个位是用来通知调度器线程有新的消息需要处理
// 这个位是在调度器模块的线程中被检查的,前面在调度器线程中我们介绍过
qdf_atomic_set_bit(MC_POST_EVENT_MASK, &sched_ctx->sch_event_flag);

// 唤醒调度器线程
// 这个函数会唤醒调度器线程,让它从睡眠状态中醒来
// 然后检查消息队列,处理消息
qdf_wake_up_interruptible(&sched_ctx->sch_wait_queue);

return QDF_STATUS_SUCCESS;
}

至此,关于 scheduler_post_message 的调用过程就介绍完了。

下面看一个例子,在 scan 模块中,如何向调度器模块发送消息。

wlan_scan_start 函数内部调用了 scheduler_post_message 函数,向调度器模块发送消息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
QDF_STATUS wlan_scan_start(struct scan_start_request *req)
{
// 首先初始化一个 scheduler_msg 结构体
// 它是一个栈区的变量,在函数结束时会被销毁
// 所以 scheduler 的消息队列中会复制一份这个消息
struct scheduler_msg msg = {0};
QDF_STATUS status;

// 省略一些代码

// 填充这个消息
msg.bodyptr = req;
msg.callback = scm_scan_start_req;
msg.flush_callback = scm_scan_start_flush_callback;

// 然后向 scheduler 模块发送消息
// 源模块是 QDF_MODULE_ID_OS_IF,目标模块是 QDF_MODULE_ID_SCAN,也就是说从 操作系统的接口 模块发送消息到 扫描 模块
// 队列 ID 是 QDF_MODULE_ID_OS_IF
status = scheduler_post_message(QDF_MODULE_ID_OS_IF,
QDF_MODULE_ID_SCAN,
QDF_MODULE_ID_OS_IF, &msg);
if (QDF_IS_STATUS_ERROR(status)) {
wlan_objmgr_vdev_release_ref(req->vdev, WLAN_SCAN_ID);
scm_scan_free_scan_request_mem(req);
}

return status;
}

根据上面的介绍,这个消息被放到了 QDF_MODULE_ID_OS_IF 的消息队列中,然后调度器线程会处理这个消息。

在调度器模块中,为 QDF_MODULE_ID_OS_IF 消息队列注册的 handler 是 scheduler_os_if_mq_handler

也就是说调度器线程会调用 scheduler_os_if_mq_handler 函数来处理这个消息。

这个函数的内部实现是这样的。

本质就是调用消息的 callback 函数,然后参数是消息本身。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
QDF_STATUS scheduler_os_if_mq_handler(struct scheduler_msg *msg)
{
QDF_STATUS (*os_if_msg_handler)(struct scheduler_msg *);

QDF_BUG(msg);
if (!msg)
return QDF_STATUS_E_FAILURE;

os_if_msg_handler = msg->callback;

QDF_BUG(os_if_msg_handler);
if (!os_if_msg_handler)
return QDF_STATUS_E_FAILURE;

os_if_msg_handler(msg);

return QDF_STATUS_SUCCESS;
}

对于上面的 scan 消息,当执行完 scheduler_post_message 后,调度器线程会被唤醒,然后调用 scheduler_os_if_mq_handler 函数,这个函数会调用 scm_scan_start_req 函数,然后将消息作为参数传递给 scm_scan_start_req 函数。

我们可以继续看一下 scm_scan_start_req 函数的实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
QDF_STATUS
scm_scan_start_req(struct scheduler_msg *msg)
{
// 入参是 scheduler_msg 结构体
struct wlan_serialization_command cmd = {0, };
enum wlan_serialization_status ser_cmd_status;
struct scan_start_request *req = NULL;
struct wlan_scan_obj *scan_obj;
QDF_STATUS status = QDF_STATUS_SUCCESS;


if (!msg) {
scm_err("msg received is NULL");
QDF_ASSERT(0);
return QDF_STATUS_E_NULL_VALUE;
}
if (!msg->bodyptr) {
scm_err("bodyptr is NULL");
QDF_ASSERT(0);
return QDF_STATUS_E_NULL_VALUE;
}

// 这里将 msg->bodyptr 取出来继续处理
req = msg->bodyptr;

// 省略一些代码

return status;
}

当我起初查看 WLAN 驱动的代码时,每次追踪函数调用时,看到 scheduler_post_message 函数我总是很困惑。因为这里发生了线程切换,无法继续向下追踪。

但好在 scheduler 模块的代码比较简单,只要理解了消息队列的概念,就能很好的理解这个模块的作用。

下次再看到 scheduler_post_message 函数时,只需要知道接下来将切换到调度器线程中执行 msg->callback 函数,参数是 msg 本身就可以继续追踪了。

Data structures

Scheduler context

Code location:

https://github.com/WANG-Guangxin/wlan-driver/blob/5113495b16420b49004c444715d2daae2066e7dc/qca-wifi-host-cmn/scheduler/inc/scheduler_core.h#L113

https://opengrok.dijk.eu.org/xref/wlan-driver/qca-wifi-host-cmn/scheduler/inc/scheduler_core.h?r=70a19e16789e308182f63b15c75decec7bf0b342#113


根据类型,消息体指针可以是指向内存的 bodyptr 指针,也可以是作为 32 位数据的 bodyval。bodyptr 总是可以被释放的指针,应始终确保 bodyptr 是可以被释放的。消息应该使用 bodyptr 或 bodyval,但不应同时使用两者!

👀
关于链表节点,我曾经在这篇文章中讨论过。

sched_history_item

Code location:

https://github.com/WANG-Guangxin/wlan-driver/blob/5113495b16420b49004c444715d2daae2066e7dc/qca-wifi-host-cmn/scheduler/src/scheduler_core.c#L52

https://opengrok.dijk.eu.org/xref/wlan-driver/qca-wifi-host-cmn/scheduler/src/scheduler_core.c?r=8b3dca18206e1a0461492f082fa6e270b092c035#52

This structure records the scheduler’s history.

In the Scheduler module, there’s a global variable sched_history — an array used to record the most recent WLAN_SCHED_HISTORY_SIZE history entries.

1
static struct sched_history_item sched_history[WLAN_SCHED_HISTORY_SIZE];

A reminder: whether this code takes effect depends on whether WLAN_SCHED_HISTORY_SIZE is defined. If it isn’t, all scheduler-history-related code won’t be compiled.

sched_pool 🌶️

Here come the notoriously abstract kernel macro definitions again 🤣

ChatGPT will help us explain this part.

Actually, the memory pool deserves an article of its own.

If you’ve learned how malloc works, you can simply think of sched_pool here as the data structure used to allocate scheduler_msg.

Code location:

https://github.com/WANG-Guangxin/wlan-driver/blob/5113495b16420b49004c444715d2daae2066e7dc/qca-wifi-host-cmn/scheduler/src/scheduler_core.c#L27

https://opengrok.dijk.eu.org/xref/wlan-driver/qca-wifi-host-cmn/scheduler/src/scheduler_core.c?r=8b3dca18206e1a0461492f082fa6e270b092c035#27

A memory pool is defined with this macro function.

1
2
DEFINE_QDF_FLEX_MEM_POOL(sched_pool, sizeof(struct scheduler_msg),
WLAN_SCHED_REDUCTION_LIMIT);

This macro is written as follows.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* DEFINE_QDF_FLEX_MEM_POOL() - define a new flex mem pool with one segment
* @name: the name of the pool variable
* @size_of_item: size of the items the pool will allocate
* @rm_limit: min number of segments to keep during reduction
*/
#define DEFINE_QDF_FLEX_MEM_POOL(name, size_of_item, rm_limit) \
struct qdf_flex_mem_pool name; \
uint8_t __ ## name ## _head_bytes[QDF_FM_BITMAP_BITS * (size_of_item)];\
struct qdf_flex_mem_segment __ ## name ## _head = { \
.node = QDF_LIST_NODE_INIT_SINGLE( \
QDF_LIST_ANCHOR(name.seg_list)), \
.bytes = __ ## name ## _head_bytes, \
}; \
struct qdf_flex_mem_pool name = { \
.seg_list = QDF_LIST_INIT_SINGLE(__ ## name ## _head.node), \
.reduction_limit = (rm_limit), \
.item_size = (size_of_item), \
}

This code lives in the qdf module, which is beyond the scope of this article, so I won’t expand on macros like QDF_FM_BITMAP_BITS, QDF_LIST_NODE_INIT_SINGLE, QDF_LIST_ANCHOR, QDF_LIST_INIT_SINGLE.
Let’s just understand at a high level what this macro does.

Conversation with GPT-4o

Q1:

What does this macro do?

A1:

The macro DEFINE_QDF_FLEX_MEM_POOL is used to define and initialize a flexible memory pool with one segment in C programs. Let’s explain the macro in detail and then apply it to the given example.

Detailed analysis of the macro

  1. Define a memory pool struct variable:

    1
    struct qdf_flex_mem_pool name;

    This line defines a new variable name of type struct qdf_flex_mem_pool, representing the memory pool.

  2. Allocate bytes for the head segment:

    1
    uint8_t __ ## name ## _head_bytes[QDF_FM_BITMAP_BITS * (size_of_item)];

    This defines a byte array named __name_head_bytes with size QDF_FM_BITMAP_BITS * (size_of_item). This array stores the data of the pool’s first segment. QDF_FM_BITMAP_BITS is likely a constant representing the number of bits in the bitmap, and size_of_item is the size of each pooled item.

  3. Define the initial segment:

    1
    2
    3
    4
    5
    struct qdf_flex_mem_segment __ ## name ## _head = { \
    .node = QDF_LIST_NODE_INIT_SINGLE( \
    QDF_LIST_ANCHOR(name.seg_list)), \
    .bytes = __ ## name ## _head_bytes, \
    };

    This defines a segment struct named __name_head:

    • node: initialized with QDF_LIST_NODE_INIT_SINGLE and QDF_LIST_ANCHOR as a single node linked into the pool’s segment list.
    • bytes: points to the __name_head_bytes array defined above — the actual memory region of this segment.
  4. Initialize the memory pool:

    1
    2
    3
    4
    5
    struct qdf_flex_mem_pool name = { \
    .seg_list = QDF_LIST_INIT_SINGLE(__ ## name ## _head.node), \
    .reduction_limit = (rm_limit), \
    .item_size = (size_of_item), \
    }

    This initializes the memory pool struct named name:

    • seg_list: initialized as a list containing a single segment (__name_head.node).
    • reduction_limit: set to rm_limit, the minimum number of segments to keep when reducing the pool.
    • item_size: set to size_of_item, the size of each item the pool manages.

Applying the macro

The given usage:

1
DEFINE_QDF_FLEX_MEM_POOL(sched_pool, sizeof(struct scheduler_msg), WLAN_SCHED_REDUCTION_LIMIT);
  • name = sched_pool: creates a struct qdf_flex_mem_pool variable named sched_pool.
  • size_of_item = sizeof(struct scheduler_msg): each item in the pool is the size of struct scheduler_msg.
  • rm_limit = WLAN_SCHED_REDUCTION_LIMIT: the minimum number of segments to keep during reduction.

Summary of the macro

The macro DEFINE_QDF_FLEX_MEM_POOL:

  • Defines a new flexible memory pool with the given name.
  • Allocates a predefined amount of memory for the first segment based on the size of the items it will manage.
  • Initializes the pool, setting up the linked list and other attributes like item size and minimum segments to keep during reduction.

This macro simplifies the creation and initialization of a flexible memory pool, making it easier to dynamically manage memory with ready-made structures and parameters.

Q2:

What does “with one segment” mean?

A2:

“With one segment” means that when the memory pool is created, it is initialized with one memory segment. Each segment is an independent block in the pool, used to store a certain number of memory items.

In the context of this macro, a “segment” is a memory block containing several fixed-size memory items. Segments exist to manage allocation and deallocation, typically used when large numbers of small memory blocks need to be dynamically allocated and recycled. By using segments, memory fragmentation is reduced, memory usage is optimized, and memory management operations (like expanding or shrinking the pool) become easier.

Specifically, “with one segment” can be understood as:

  1. Initial memory allocation: when created, the macro’s pool allocates one fixed-size memory block called a “segment”. In the code, a byte array __name_head_bytes stores this data.

  2. Segment structure: each segment has a management node (node) for linked-list management within the pool’s segment list, and a pointer to actual data (bytes), namely __name_head_bytes.

  3. Number of segments: although only one segment exists at initialization, the design supports multiple segments (managed via the linked-list structure). The number of segments can grow or shrink as needed; the reduction_limit parameter specifies the minimum number of segments the pool must keep during reduction.

In short, “with one segment” means the pool starts with one pre-allocated memory block, organized as a segment, for storing and managing memory items. As requirements change, the pool can dynamically adjust the number of segments to manage memory usage.

Q3:

What does this pool do when more memory is needed?

A3:

When more memory is needed, this memory pool may do the following to allocate more segments. The exact implementation depends on the pool’s design and purpose. Generally, a flexible memory pool expands on demand when memory demand grows:

  1. Allocate a new segment:

    • If existing segments are full or can’t satisfy the new request, the pool allocates a new segment.
    • This involves allocating a byte array of size QDF_FM_BITMAP_BITS * size_of_item. Together, QDF_FM_BITMAP_BITS and size_of_item determine how many items each segment can store.
    • The new segment is typically created similarly to the first, i.e., a new byte array is created for the data and linked into the segment list.
  2. Add the new segment to the segment list:

    • The newly allocated segment is added to the pool’s segment list via the linked list. These segments are managed by a linked-list structure (doubly or singly linked) for fast insertion and removal.
    • In the macro, macros like QDF_LIST_INIT_SINGLE and QDF_LIST_NODE_INIT_SINGLE likely initialize and manipulate list nodes that help manage the relationships between segments.
  3. Update the pool’s management structures:

    • Pool metadata needs updating, e.g.:
      • Head or tail pointers of the segment list.
      • The current number of segments in the pool.
      • Possibly the pool’s total size or total used memory.
  4. Segment management for shrinking:

    • If the pool has a reclamation mechanism, when segments are no longer needed (e.g., after items are freed), the pool may check whether to reclaim segments to save memory.
    • The reduction_limit in the macro limits the minimum number of segments during reduction; it’s also considered when growing, to keep memory management stable and performant.
  5. Error handling on allocation failure:

    • If the pool fails to allocate a new segment (e.g., insufficient system memory), the pool needs error handling, such as logging the error, raising an alarm, or running garbage collection to free unused memory.
  6. Possible memory alignment and optimization:

    • In some cases, the pool may align memory to ensure efficient access.
    • Alignment and optimization avoid fragmentation and ensure efficient memory usage.

Code example

Although the macro doesn’t show the pool’s dynamic expansion logic in full, that logic might look like this elsewhere in the pool implementation:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void expand_memory_pool(struct qdf_flex_mem_pool *pool) {
// Allocate a new memory segment
struct qdf_flex_mem_segment *new_segment = allocate_new_segment(pool->item_size);

if (new_segment == NULL) {
// Handle memory allocation failure
handle_memory_allocation_failure();
return;
}

// Add the new segment to the segment list
QDF_LIST_INSERT_TAIL(&pool->seg_list, &new_segment->node);

// Update management structures like the segment counter
pool->num_segments++;
}

Summary

When more memory is needed, the pool allocates new segments, adds them to the management list, updates metadata, and handles errors as needed. This mechanism lets the pool flexibly adapt to changing memory requirements while efficiently managing allocation and reclamation.

Q4:

Now let me explain the memory pool, and you correct me:

The pool pre-allocates a large chunk of memory and organizes it into many small blocks. Each small block has two parts: the actual memory item in use, and a header. The header’s pointer info links these small blocks into a linked list. The pool provides get and free APIs externally, like malloc and free. When an item is freed back to the pool, the pool doesn’t necessarily return that memory to the OS; instead it adjusts the header pointers to merge some small blocks, avoiding fragmentation.

A4:

Your explanation is generally correct and clearly describes the basic workings of a memory pool. A pool indeed pre-allocates a large chunk of memory and splits it into small blocks to manage allocation and deallocation efficiently. Some specific corrections and additions:

  1. Header-data relationship:

    • You mentioned each small block has two parts: the actual memory item and a header. That’s accurate. The header usually contains management info, such as a pointer to the next block, block state (free or in use), and other metadata.
    • This design lets the pool manage memory blocks without extra allocations, reducing fragmentation.
  2. Linked-list usage:

    • Using header pointers to link small blocks into a list is also correct. Linked lists are common in pools because they allow efficient insertion/removal, ideal for dynamic allocation.
    • Typically, free blocks form a free list. On each allocation, the pool takes a suitably sized block from the free list; on each free, the block goes back into the free list.
  3. get and free API implementation:

    • The pool’s get/free operations resemble malloc/free but are usually more efficient since they don’t involve system calls. get may find a suitably sized block in the free list and return it; free returns the block to the free list.
    • The benefit: fewer frequent system-level allocations, which are slow and can introduce fragmentation. Pool memory is usually app-level heap memory.
  4. Fragmentation management:

    • You noted the pool may not return memory to the OS on free, instead merging small blocks by adjusting header pointers. This reduces fragmentation and improves efficiency.
    • Specifically, the pool may implement a merging strategy, such as coalescing adjacent free blocks, to keep enough contiguous space for future requests.
  5. Possible expansion:

    • When the pool has no free blocks, it may expand — allocate another chunk and add it to the pool’s management structure, e.g., by appending a new segment as described above.

Summary

Your description is essentially correct and covers the main concepts. A pool pre-allocates a large chunk and organizes it into small blocks for management, avoiding frequent system calls for better performance, and reduces fragmentation via merging and management strategies. The implementation involves designing allocation, deallocation, and fragmentation strategies. Overall, your understanding is accurate and clearly described!

Logic

Initialization

The Scheduler module’s init function is scheduler_init, called in hdd_module_init.

The call chain:

module_init(hdd_module_init);
–>
hdd_module_init
–>
hdd_driver_load
–>
hdd_component_init
–>
dispatcher_init
–>
/*

  • scheduler INIT has to be the last as each component’s
  • initialization has to happen first and then at the end
  • scheduler needs to start accepting the service.
    */
    if (QDF_STATUS_SUCCESS != scheduler_init())
    goto scheduler_init_fail;

Then we get to our topic: scheduler_init.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
QDF_STATUS scheduler_init(void)
{
QDF_STATUS status;
struct scheduler_ctx *sched_ctx;

sched_debug("Initializing Scheduler");

status = scheduler_create_ctx();
/*
* Creating the context is just two lines:
* qdf_flex_mem_init(&sched_pool);
* gp_sched_ctx = &g_sched_ctx
*
* Treat the qdf functions as black boxes for now; their job is to
* initialize a sched_pool memory pool.
* Also initialize a global context pointer gp_sched_ctx pointing to a
* scheduler_ctx struct.
*/
if (QDF_IS_STATUS_ERROR(status)) {
sched_fatal("Failed to create context; status:%d", status);
return status;
}

sched_ctx = scheduler_get_context();
QDF_BUG(sched_ctx);
if (!sched_ctx) {
status = QDF_STATUS_E_FAILURE;
goto ctx_destroy;
}

status = scheduler_queues_init(sched_ctx);
/*
* Initialize the queues — initialize the message queues in sched_ctx.
* Inside, it calls scheduler_all_queues_init; let's trace that function below.
*/
if (QDF_IS_STATUS_ERROR(status)) {
sched_fatal("Failed to init queues; status:%d", status);
goto ctx_destroy;
}

status = qdf_event_create(&sched_ctx->sch_start_event);
if (QDF_IS_STATUS_ERROR(status)) {
sched_fatal("Failed to create start event; status:%d", status);
goto queues_deinit;
}

status = qdf_event_create(&sched_ctx->sch_shutdown);
if (QDF_IS_STATUS_ERROR(status)) {
sched_fatal("Failed to create shutdown event; status:%d",
status);
goto start_event_destroy;
}

status = qdf_event_create(&sched_ctx->resume_sch_event);
if (QDF_IS_STATUS_ERROR(status)) {
sched_fatal("Failed to create resume event; status:%d", status);
goto shutdown_event_destroy;
}

qdf_spinlock_create(&sched_ctx->sch_thread_lock);
qdf_init_waitqueue_head(&sched_ctx->sch_wait_queue);
sched_ctx->sch_event_flag = 0;
sched_ctx->timeout = SCHEDULER_WATCHDOG_TIMEOUT;
qdf_timer_init(NULL,
&sched_ctx->watchdog_timer,
&scheduler_watchdog_timeout,
sched_ctx,
QDF_TIMER_TYPE_SW);

qdf_register_mc_timer_callback(scheduler_mc_timer_callback);

return QDF_STATUS_SUCCESS;

shutdown_event_destroy:
qdf_event_destroy(&sched_ctx->sch_shutdown);

start_event_destroy:
qdf_event_destroy(&sched_ctx->sch_start_event);

queues_deinit:
scheduler_queues_deinit(sched_ctx);

ctx_destroy:
scheduler_destroy_ctx();

return status;
}

scheduler_all_queues_init:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
static qdf_atomic_t __sched_queue_depth; 
static qdf_atomic_t __sched_dup_fail_count;

static QDF_STATUS scheduler_all_queues_init(struct scheduler_ctx *sched_ctx)
{
QDF_STATUS status;
int i;

sched_enter();

QDF_BUG(sched_ctx);
if (!sched_ctx)
return QDF_STATUS_E_FAILURE;

qdf_atomic_set(&__sched_queue_depth, 0);
/* Here the static atomic variable queue depth is set to 0 */

/* Initialize all message queues */
/* As mentioned earlier, SCHEDULER_NUMBER_OF_MSG_QUEUE defaults to 6.
* scheduler_mq_init essentially calls qdf functions to initialize a list
* data structure; we won't go into the details here.
*/
for (i = 0; i < SCHEDULER_NUMBER_OF_MSG_QUEUE; i++) {
status = scheduler_mq_init(&sched_ctx->queue_ctx.sch_msg_q[i]);
if (QDF_STATUS_SUCCESS != status)
return status;
}

/* Initialize all qid to qidx mapping to invalid values */
for (i = 0; i < QDF_MODULE_ID_MAX; i++)
sched_ctx->queue_ctx.scheduler_msg_qid_to_qidx[i] =
SCHEDULER_NUMBER_OF_MSG_QUEUE;

sched_exit();

return status;
}

Enable

The entry point to enabling the scheduler is the kernel calling the driver’s probe function, which is wlan_hdd_pld_probe here.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
struct pld_driver_ops wlan_drv_ops = {
.probe = 'wlan_hdd_pld_probe',
.remove = wlan_hdd_pld_remove,
.idle_shutdown = wlan_hdd_pld_idle_shutdown,
.idle_restart = wlan_hdd_pld_idle_restart,
.shutdown = wlan_hdd_pld_shutdown,
.reinit = wlan_hdd_pld_reinit,
.crash_shutdown = wlan_hdd_pld_crash_shutdown,
.suspend = wlan_hdd_pld_suspend,
.resume = wlan_hdd_pld_resume,
.suspend_noirq = wlan_hdd_pld_suspend_noirq,
.resume_noirq = wlan_hdd_pld_resume_noirq,
.reset_resume = wlan_hdd_pld_reset_resume,
.modem_status = wlan_hdd_pld_notify_handler,
.uevent = wlan_hdd_pld_uevent,
#ifdef WLAN_FEATURE_SSR_DRIVER_DUMP
.collect_driver_dump = wlan_hdd_pld_collect_driver_dump,
#endif
#ifdef FEATURE_RUNTIME_PM
.runtime_suspend = wlan_hdd_pld_runtime_suspend,
.runtime_resume = wlan_hdd_pld_runtime_resume,
#endif
.set_curr_therm_cdev_state = wlan_hdd_pld_set_thermal_mitigation,
};

The call chain:

wlan_hdd_pld_probe
–>
hdd_soc_probe
–>
__hdd_soc_probe
–>
hdd_wlan_startup
–>
hdd_wlan_start_modules
–>
cds_open
–>
dispatcher_enable
–>
scheduler_enable

That brings us to this section’s topic: scheduler_enable.

This is also where the scheduler’s multithreading begins.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
QDF_STATUS scheduler_enable(void)
{
struct scheduler_ctx *sched_ctx;

sched_debug("Enabling Scheduler");

sched_ctx = scheduler_get_context();
QDF_BUG(sched_ctx);
if (!sched_ctx)
return QDF_STATUS_E_INVAL;

qdf_atomic_clear_bit(MC_SHUTDOWN_EVENT_MASK,
&sched_ctx->sch_event_flag);
qdf_atomic_clear_bit(MC_POST_EVENT_MASK,
&sched_ctx->sch_event_flag);

/* create the scheduler thread */
/* Here a scheduler thread is created; the thread executes scheduler_thread,
* with data sched_ctx, and is named "scheduler_thread" */
sched_ctx->sch_thread = qdf_create_thread(scheduler_thread, sched_ctx,
"scheduler_thread");
if (!sched_ctx->sch_thread) {
sched_fatal("Failed to create scheduler thread");
return QDF_STATUS_E_RESOURCES;
}

sched_debug("Scheduler thread created");

/* wait for the scheduler thread to startup */
/* This first wakes up the scheduler thread — i.e., the scheduler_thread
* function starts executing in another thread */
qdf_wake_up_process(sched_ctx->sch_thread);

/* This line makes the current thread wait for a specific event:
* sch_start_event.
* Waiting for sch_start_event essentially means waiting for this variable
* to become non-zero.
*
* The second parameter is the timeout; 0 means wait forever.
*/
qdf_wait_single_event(&sched_ctx->sch_start_event, 0);

sched_debug("Scheduler thread started");

return QDF_STATUS_SUCCESS;
}

So when does sch_start_event become non-zero? It should be set inside the scheduler_thread function.

I roughly drew the state transitions of these two threads below.

Then let’s continue with the code.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
int scheduler_thread(void *arg)
{
struct scheduler_ctx *sch_ctx = (struct scheduler_ctx *)arg;
int retWaitStatus = 0;
bool shutdown = false;

if (!arg) {
QDF_DEBUG_PANIC("arg is null");
return 0;
}
// A "current" suddenly appears here
// In Linux kernel programming, current is a macro pointing to the
// task_struct of the currently running process, which holds all info
// about the process: PID, state, virtual memory info, etc.
qdf_set_user_nice(current, -2);
// qdf_set_user_nice(current, -2) sets the current thread's nice value,
// which determines Linux scheduling priority, ranging -20 to 19;
// the lower the value, the higher the priority.

/* Ack back to the context from which the main controller thread
* has been created
*/
// This notifies the main thread that the scheduler thread has started
// i.e., sets sch_start_event to non-zero; after this line executes,
// the main thread continues
qdf_event_set(&sch_ctx->sch_start_event);
sched_debug("scheduler thread %d (%s) starting up",
current->pid, current->comm);

while (!shutdown) {
/* This implements the execution model algorithm */
// This is the scheduler thread's main logic — very much like a
// thread pool in a web server
// qdf_wait_queue_interruptible(sch_ctx->sch_wait_queue, condition)
// puts the current thread to sleep until the condition is met.
// The condition here is:
// qdf_atomic_test_bit(MC_POST_EVENT_MASK, &sch_ctx->sch_event_flag) ||
// qdf_atomic_test_bit(MC_SUSPEND_EVENT_MASK, &sch_ctx->sch_event_flag)
// i.e., the thread wakes up when either the MC_POST_EVENT_MASK or
// MC_SUSPEND_EVENT_MASK bit of sch_ctx->sch_event_flag is set
retWaitStatus = qdf_wait_queue_interruptible(
sch_ctx->sch_wait_queue,
qdf_atomic_test_bit(MC_POST_EVENT_MASK,
&sch_ctx->sch_event_flag) ||
qdf_atomic_test_bit(MC_SUSPEND_EVENT_MASK,
&sch_ctx->sch_event_flag));

if (retWaitStatus == -ERESTARTSYS)
QDF_DEBUG_PANIC("Scheduler received -ERESTARTSYS");

// Clear the MC_POST_EVENT_MASK bit of sch_ctx->sch_event_flag here
// Why clear it before processing rather than after? Probably to avoid
// new messages arriving while processing
qdf_atomic_clear_bit(MC_POST_EVENT_MASK, &sch_ctx->sch_event_flag);

// The scheduler thread processes all message queues
scheduler_thread_process_queues(sch_ctx, &shutdown);
}

/* If we get here the scheduler thread must exit */
sched_debug("Scheduler thread exiting");
qdf_event_set(&sch_ctx->sch_shutdown);

return 0;
}

scheduler_thread_process_queues processes messages in the 6 queues, which are ordered by priority: first process all messages in queue 1; when queue 1 is done, check queue 2, and so on, until all queues are drained or a shutdown signal is received.

Let me also explain why the MC_POST_EVENT_MASK bit is cleared before processing messages, as mentioned above.

  1. The scheduler finished processing all messages in the first queue, then started checking the second queue.
  2. At this moment, a new message is posted to the first queue, and the MC_POST_EVENT_MASK bit is set.
  3. The scheduler checked all queues in turn and processed all messages, then executed qdf_atomic_clear_bit(MC_POST_EVENT_MASK, &sch_ctx->sch_event_flag); — as a result, when the scheduler thread loops back to check the message queues, it finds no MC_POST_EVENT_MASK set for now and keeps waiting. But note: at this point there’s a new message in the first queue that hasn’t been processed. It will only be processed when the next post arrives. This causes a delay.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
static void scheduler_thread_process_queues(struct scheduler_ctx *sch_ctx,
bool *shutdown)
{
int i;
QDF_STATUS status;
struct scheduler_msg *msg;

if (!sch_ctx) {
QDF_DEBUG_PANIC("sch_ctx is null");
return;
}

/* start with highest priority queue : timer queue at index 0 */
i = 0;
while (i < SCHEDULER_NUMBER_OF_MSG_QUEUE) {
/* Check if MC needs to shutdown */
// If the MC_SHUTDOWN_EVENT_MASK bit is set, shutdown is set to true
// When shutdown is true, the while loop in the scheduler_thread exits
if (qdf_atomic_test_bit(MC_SHUTDOWN_EVENT_MASK,
&sch_ctx->sch_event_flag)) {
sched_debug("scheduler thread signaled to shutdown");
*shutdown = true;

/* Check for any Suspend Indication */
if (qdf_atomic_test_and_clear_bit(MC_SUSPEND_EVENT_MASK,
&sch_ctx->sch_event_flag)) {
/* Unblock anyone waiting on suspend */
if (gp_sched_ctx->hdd_callback)
gp_sched_ctx->hdd_callback();
}

break;
}

// This essentially takes the head message from queue i
msg = scheduler_mq_get(&sch_ctx->queue_ctx.sch_msg_q[i]);
if (!msg) {
/* check next queue */
// If the message is empty, check the next message queue
i++;
continue;
}

// Each message queue has a handler; the handler is called here to process the message
// Handler registration is covered later in this article
if (sch_ctx->queue_ctx.scheduler_msg_process_fn[i]) {
// First set the watchdog-related variables: message type and watchdog callback
// What is the watchdog for? It monitors how long message processing takes;
// if processing exceeds the preset time, the watchdog callback is triggered
sch_ctx->watchdog_msg_type = msg->type;
sch_ctx->watchdog_callback = msg->callback;

sched_history_start(msg);

// Start a timer with sch_ctx->timeout
// What's the watchdog_timer callback?
// Search for where qdf_timer_init is called — as introduced earlier,
// the timer callback is scheduler_watchdog_timeout
qdf_timer_start(&sch_ctx->watchdog_timer,
sch_ctx->timeout);

// Call the handler to process the message
status = sch_ctx->queue_ctx.
scheduler_msg_process_fn[i](msg);

// If the line above succeeded, stop the watchdog timer
qdf_timer_stop(&sch_ctx->watchdog_timer);
sched_history_stop();

if (QDF_IS_STATUS_ERROR(status))
sched_err("Failed processing Qid[%d] message",
sch_ctx->queue_ctx.sch_msg_q[i].qid);

// Free the message
scheduler_core_msg_free(msg);
}

/* start again with highest priority queue at index 0 */
i = 0;
}

/* Check for any Suspend Indication */
if (qdf_atomic_test_and_clear_bit(MC_SUSPEND_EVENT_MASK,
&sch_ctx->sch_event_flag)) {
qdf_spin_lock(&sch_ctx->sch_thread_lock);
qdf_event_reset(&sch_ctx->resume_sch_event);
/* controller thread suspend completion callback */
if (gp_sched_ctx->hdd_callback)
gp_sched_ctx->hdd_callback();
qdf_spin_unlock(&sch_ctx->sch_thread_lock);
/* Wait for resume indication */
qdf_wait_single_event(&sch_ctx->resume_sch_event, 0);
}

return; /* Nothing to process wait on wait queue */
}

If the handler doesn’t finish within sch_ctx->timeout, scheduler_watchdog_timeout is called.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
static void scheduler_watchdog_timeout(void *arg)
{
struct scheduler_ctx *sched = arg;

if (qdf_is_recovering()) {
sched_debug("Recovery is in progress ignore timeout");
return;
}

scheduler_watchdog_notify(sched);
if (sched->sch_thread)
qdf_print_thread_trace(sched->sch_thread);

/* avoid crashing during shutdown */
if (qdf_atomic_test_bit(MC_SHUTDOWN_EVENT_MASK, &sched->sch_event_flag))
return;

sched_err("Triggering self recovery on sheduler timeout");
qdf_trigger_self_recovery(NULL, QDF_SCHED_TIMEOUT);
}

static inline void scheduler_watchdog_notify(struct scheduler_ctx *sched)
{
char symbol[QDF_SYMBOL_LEN];

// sched->watchdog_callback is set to msg->callback in scheduler_thread_process_queues
// Actually, the goal isn't to execute watchdog_callback, but to print its name
if (sched->watchdog_callback)
qdf_sprint_symbol(symbol, sched->watchdog_callback);

sched_fatal("Callback %s (type 0x%x) exceeded its allotted time of %ds",
sched->watchdog_callback ? symbol : "<null>",
sched->watchdog_msg_type,
sched->timeout / 1000);
}

Handler registration

In the previous section, each message queue has its own handler. Where is it registered?

In cds_open from the previous section, the cds_register_all_modules function is called, which in turn calls scheduler_register_module to register a handler for each queue.

cds_open
–>
dispatcher_enable
cds_register_all_modules
–>
scheduler_register_module

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
static QDF_STATUS cds_register_all_modules(void)
{
QDF_STATUS status;

scheduler_register_wma_legacy_handler(&wma_mc_process_handler);
scheduler_register_sys_legacy_handler(&sys_mc_process_handler);

/* Register message queues in given order such that queue priority is
* intact:
* 1) QDF_MODULE_ID_SYS: Timer queue(legacy SYS queue)
* 2) QDF_MODULE_ID_TARGET_IF: Target interface queue
* 3) QDF_MODULE_ID_PE: Legacy PE message queue
* 4) QDF_MODULE_ID_SME: Legacy SME message queue
* 5) QDF_MODULE_ID_OS_IF: OS IF message queue for new components
*/
status = scheduler_register_module(QDF_MODULE_ID_SYS,
&scheduler_timer_q_mq_handler);
status = scheduler_register_module(QDF_MODULE_ID_TARGET_IF,
&scheduler_target_if_mq_handler);
status = scheduler_register_module(QDF_MODULE_ID_PE,
&pe_mc_process_handler);
status = scheduler_register_module(QDF_MODULE_ID_SME,
&sme_mc_process_handler);
status = scheduler_register_module(QDF_MODULE_ID_OS_IF,
&scheduler_os_if_mq_handler);
status = scheduler_register_module(QDF_MODULE_ID_SCAN,
&scheduler_scan_mq_handler);
return status;
}

QDF_STATUS scheduler_register_module(QDF_MODULE_ID qid,
scheduler_msg_process_fn_t callback)
{
struct scheduler_mq_ctx *ctx;
struct scheduler_ctx *sched_ctx = scheduler_get_context();

sched_enter();

QDF_BUG(sched_ctx);
if (!sched_ctx)
return QDF_STATUS_E_FAILURE;

if (sched_ctx->sch_last_qidx >= SCHEDULER_NUMBER_OF_MSG_QUEUE) {
sched_err("Already registered max %d no of message queues",
SCHEDULER_NUMBER_OF_MSG_QUEUE);
return QDF_STATUS_E_FAILURE;
}

ctx = &sched_ctx->queue_ctx;
ctx->scheduler_msg_qid_to_qidx[qid] = sched_ctx->sch_last_qidx;
ctx->sch_msg_q[sched_ctx->sch_last_qidx].qid = qid;
ctx->scheduler_msg_process_fn[sched_ctx->sch_last_qidx] = callback;
sched_ctx->sch_last_qidx++;

sched_exit();

return QDF_STATUS_SUCCESS;
}

Each handler calls the callback set in the message, but messages on different queues may need queue-specific handling, so each queue gets its own registered handler.

By this point in the article, I’ve realized I’m writing poorly. Explaining code with text is really hard. Sometimes I get hung up on details, yet there are still plenty of details I haven’t covered. I think continuing would only make things more confusing, so this article ends here for now.

Hope it helps!

External APIs

2024-09-22 update

Finally, let me add how external modules interact with the scheduler module.

The most frequently called external API is scheduler_post_message, which sends a message to the scheduler thread.

It’s a macro whose parameters are: source module ID, destination module ID, queue ID, and message pointer.

It’s defined as scheduler_post_message_debug, which adds the line number and function name to the original parameters, so when debugging you can tell which function called scheduler_post_message.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* scheduler_post_message() - post normal messages(no priority)
* @src_id: Source module of the message
* @dest_id: Destination module of the message
* @que_id: Queue to which the message has to posted.
* @msg: message pointer
*
* This function will mask the src_id, and destination id to qid of
* scheduler_post_msg
*
* Return: QDF status
*/
#define scheduler_post_message(src_id, dest_id, que_id, msg) \
scheduler_post_message_debug(src_id, dest_id, que_id, msg, \
__LINE__, __func__)

scheduler_post_message_debug continues by calling scheduler_post_msg, which puts the message into the message queue.
It just does a simple transformation: converting the message’s source module ID, destination module ID, and queue ID into a qid.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
QDF_STATUS scheduler_post_message_debug(QDF_MODULE_ID src_id,
QDF_MODULE_ID dest_id,
QDF_MODULE_ID que_id,
struct scheduler_msg *msg,
int line,
const char *func)
{
QDF_STATUS status;

status = scheduler_post_msg(scheduler_get_qid(src_id, dest_id, que_id),
msg);

if (QDF_IS_STATUS_ERROR(status))
sched_err("couldn't post from %d to %d - called from %d, %s",
src_id, dest_id, line, func);

return status;
}

The macros related to scheduler_get_qid are as follows.
0x3FF in binary is 10 ones — that is, scheduler_get_src_id takes the top 10 bits of qid, scheduler_get_dest_id takes the middle 10 bits, and scheduler_get_que_id takes the low 10 bits.

scheduler_get_qid is the inverse operation, placing src_id, dest_id, and que_id into different bit positions of qid via shifts.

This is a way to compress memory: three values each less than 1024 are packed into a single 32-bit integer.

1
2
3
4
5
#define scheduler_get_src_id(qid)       (((qid) >> 20) & 0x3FF)
#define scheduler_get_dest_id(qid) (((qid) >> 10) & 0x3FF)
#define scheduler_get_que_id(qid) ((qid) & 0x3FF)
#define scheduler_get_qid(src, dest, que_id) ((que_id) | ((dest) << 10) |\
((src) << 20))

Tracing further down,

scheduler_post_msg calls scheduler_post_msg_by_priority, which places the message at different positions in the queue based on its priority.

1
2
3
4
5
static inline QDF_STATUS scheduler_post_msg(uint32_t qid,
struct scheduler_msg *msg)
{
return scheduler_post_msg_by_priority(qid, msg, false);
}

scheduler_post_msg_by_priority is the most general function; its third parameter is a boolean deciding whether to insert into the queue by priority.

In normal mode, if an external module calls scheduler_post_message, this parameter is false.

This function is fairly long; check my comments.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
QDF_STATUS scheduler_post_msg_by_priority(uint32_t qid,
struct scheduler_msg *msg,
bool is_high_priority)
{
// See the data section earlier for these
uint8_t qidx;
struct scheduler_mq_type *target_mq;
struct scheduler_msg *queue_msg;
struct scheduler_ctx *sched_ctx;
uint16_t src_id;
uint16_t dest_id;
uint16_t que_id;

QDF_BUG(msg);
if (!msg)
return QDF_STATUS_E_INVAL;

// Get the scheduler context pointer
sched_ctx = scheduler_get_context();
QDF_BUG(sched_ctx);
if (!sched_ctx)
return QDF_STATUS_E_INVAL;

if (!sched_ctx->sch_thread) {
sched_err("Cannot post message; scheduler thread is stopped");
return QDF_STATUS_E_FAILURE;
}

// Check whether the message is initialized
// When an external module posts a message, it initializes a local msg
// variable inside its function; at that point all reserved bits are 0
// If sending to the SYS module, the reserved field should be SYS_MSG_COOKIE
if (msg->reserved != 0 && msg->reserved != SYS_MSG_COOKIE) {
QDF_DEBUG_PANIC("Scheduler messages must be initialized");
return QDF_STATUS_E_FAILURE;
}

// Unpack the qid
dest_id = scheduler_get_dest_id(qid);
src_id = scheduler_get_src_id(qid);
que_id = scheduler_get_que_id(qid);

if (que_id >= QDF_MODULE_ID_MAX || src_id >= QDF_MODULE_ID_MAX ||
dest_id >= QDF_MODULE_ID_MAX) {
sched_err("Src_id/Dest_id invalid, cannot post message");
return QDF_STATUS_E_FAILURE;
}
/* Target_If is a special message queue in phase 3 convergence because
* its used by both legacy WMA and as well as new UMAC components which
* directly populate callback handlers in message body.
* 1) WMA legacy messages should not have callback
* 2) New target_if message needs to have valid callback
* Clear callback handler for legacy WMA messages such that in case
* if someone is sending legacy WMA message from stack which has
* uninitialized callback then its handled properly. Also change
* legacy WMA message queue id to target_if queue such that its always
* handled in right order.
*/
// Special handling for QDF_MODULE_ID_WMA here
// If the message is from the WMA module, its callback is cleared
// Then the WMA message queue ID is converted to the target_if queue ID
// This guarantees messages are handled in the right order
if (QDF_MODULE_ID_WMA == que_id) {
msg->callback = NULL;
/* change legacy WMA message id to new target_if mq id */
que_id = QDF_MODULE_ID_TARGET_IF;
}

// src_id and dest_id are only used here — for logging
// Inside the scheduler module, messages are processed by que_id
qdf_mtrace(src_id, dest_id, msg->type, 0xFF, 0);

// Get the queue index from the queue ID
qidx = sched_ctx->queue_ctx.scheduler_msg_qid_to_qidx[que_id];
if (qidx >= SCHEDULER_NUMBER_OF_MSG_QUEUE) {
sched_err("Scheduler is deinitialized ignore msg");
return QDF_STATUS_E_FAILURE;
}

if (!sched_ctx->queue_ctx.scheduler_msg_process_fn[qidx]) {
sched_err("callback not registered for qid[%d]", que_id);
return QDF_STATUS_E_FAILURE;
}

// Get the target queue
target_mq = &(sched_ctx->queue_ctx.sch_msg_q[qidx]);

// Duplicate the message
// Why duplicate?
// Because the message is allocated on the external module's stack;
// when that module's function returns, the message is destroyed.
// So a copy is made and put into the message queue.
// The copy lives on the scheduler module's heap, so it won't be destroyed.
// That "heap" is the memory pool allocated during scheduler init
queue_msg = scheduler_core_msg_dup(msg);
if (!queue_msg)
return QDF_STATUS_E_NOMEM;

// Put the message into the queue
// High-priority messages go to the front of the queue,
// otherwise to the tail
if (is_high_priority)
scheduler_mq_put_front(target_mq, queue_msg);
else
scheduler_mq_put(target_mq, queue_msg);

// Set the MC_POST_EVENT_MASK bit
// This bit notifies the scheduler thread that a new message needs processing
// It's checked in the scheduler module's thread, as covered earlier
qdf_atomic_set_bit(MC_POST_EVENT_MASK, &sched_ctx->sch_event_flag);

// Wake up the scheduler thread
// This wakes the scheduler thread from sleep
// so it can check the message queues and process messages
qdf_wake_up_interruptible(&sched_ctx->sch_wait_queue);

return QDF_STATUS_SUCCESS;
}

That concludes the call path of scheduler_post_message.

Let’s look at an example: how the scan module sends a message to the scheduler module.

Inside wlan_scan_start, scheduler_post_message is called to send a message to the scheduler.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
QDF_STATUS wlan_scan_start(struct scan_start_request *req)
{
// First initialize a scheduler_msg struct
// It's a stack variable, destroyed when the function returns
// So the scheduler's message queue keeps a copy of this message
struct scheduler_msg msg = {0};
QDF_STATUS status;

// Some code omitted

// Fill in the message
msg.bodyptr = req;
msg.callback = scm_scan_start_req;
msg.flush_callback = scm_scan_start_flush_callback;

// Then send the message to the scheduler module
// Source module is QDF_MODULE_ID_OS_IF, destination is QDF_MODULE_ID_SCAN,
// i.e. from the OS interface module to the scan module
// The queue ID is QDF_MODULE_ID_OS_IF
status = scheduler_post_message(QDF_MODULE_ID_OS_IF,
QDF_MODULE_ID_SCAN,
QDF_MODULE_ID_OS_IF, &msg);
if (QDF_IS_STATUS_ERROR(status)) {
wlan_objmgr_vdev_release_ref(req->vdev, WLAN_SCAN_ID);
scm_scan_free_scan_request_mem(req);
}

return status;
}

Based on the above, this message goes into the QDF_MODULE_ID_OS_IF message queue, and then the scheduler thread processes it.

In the scheduler module, the handler registered for the QDF_MODULE_ID_OS_IF queue is scheduler_os_if_mq_handler,

meaning the scheduler thread calls scheduler_os_if_mq_handler to process this message.

The implementation is essentially: call the message’s callback with the message itself as the argument.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
QDF_STATUS scheduler_os_if_mq_handler(struct scheduler_msg *msg)
{
QDF_STATUS (*os_if_msg_handler)(struct scheduler_msg *);

QDF_BUG(msg);
if (!msg)
return QDF_STATUS_E_FAILURE;

os_if_msg_handler = msg->callback;

QDF_BUG(os_if_msg_handler);
if (!os_if_msg_handler)
return QDF_STATUS_E_FAILURE;

os_if_msg_handler(msg);

return QDF_STATUS_SUCCESS;
}

For the scan message above, after scheduler_post_message completes, the scheduler thread wakes up and calls scheduler_os_if_mq_handler, which calls scm_scan_start_req, passing the message as the argument.

Let’s look at scm_scan_start_req.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
QDF_STATUS
scm_scan_start_req(struct scheduler_msg *msg)
{
// The argument is a scheduler_msg struct
struct wlan_serialization_command cmd = {0, };
enum wlan_serialization_status ser_cmd_status;
struct scan_start_request *req = NULL;
struct wlan_scan_obj *scan_obj;
QDF_STATUS status = QDF_STATUS_SUCCESS;


if (!msg) {
scm_err("msg received is NULL");
QDF_ASSERT(0);
return QDF_STATUS_E_NULL_VALUE;
}
if (!msg->bodyptr) {
scm_err("bodyptr is NULL");
QDF_ASSERT(0);
return QDF_STATUS_E_NULL_VALUE;
}

// Here msg->bodyptr is extracted for further processing
req = msg->bodyptr;

// Some code omitted

return status;
}

When I first looked at the WLAN driver code, every time I traced a function call and saw scheduler_post_message, I was confused. A thread switch happens there, so I couldn’t keep tracing.

Fortunately, the scheduler module’s code is simple — once you understand the message queue concept, you can understand this module well.

Next time you see scheduler_post_message, just know that execution will switch to the scheduler thread, which runs msg->callback with msg as the argument — then you can keep tracing.