Scheduler 翻译成中文叫做调度器。
调度器这个词对于学计算机的同学来说应该并不陌生,因为在大学操作系统的课堂上我们学过很多关于进程调度的知识。
- 先来先服务(FCFS)
- 短作业优先(SJF)
- 优先级调度
- 时间片轮转
- 多级反馈队列
等等。
在这篇文章中,我们将会讨论高通 WLAN Host Driver 中调度器的实现。
为什么先学习调度器呢?
因为调度器是 WLAN Host Driver 中相对简单的部分,仅有 4 个源代码文件,而且调度器是 WLAN Host Driver 的核心部分,是各个模块之间沟通的桥梁。
源代码我转存到了我的 GitHub 仓库中,今天要看的部分在这里:
也可以使用我的 opengrok 服务器来查看源代码,如果它还在线的话。
https://opengrok.dijk.eu.org/xref/wlan-driver/qca-wifi-host-cmn/scheduler/
数据部分
scheduler 上下文
代码位置:
⭐
根据类型,消息体指针可以是指向内存的 bodyptr 指针,也可以是作为 32 位数据的 bodyval。bodyptr 总是可以被释放的指针,应始终确保 bodyptr 是可以被释放的。消息应该使用 bodyptr 或 bodyval,但不应同时使用两者!
👀
关于链表节点,我曾经在这篇文章中讨论过。
sched_history_item
代码位置:
这个数据结构同于记录调度器的历史记录。
在 Scheduler 模块中,有一个全局变量 sched_history
,它是一个数组,用于记录最近的 WLAN_SCHED_HISTORY_SIZE
条历史记录。
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
的数据结构。
代码位置:
定义一个内存池,用下面这个宏函数。
DEFINE_QDF_FLEX_MEM_POOL(sched_pool, sizeof(struct scheduler_msg),
WLAN_SCHED_REDUCTION_LIMIT);
这个宏函数被写成了下面的样子。
/**
* 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)的灵活内存池。我们来详细解释一下这个宏的功能,然后应用到给定的例子中。
宏的详细解析
-
定义一个内存池结构体变量:
struct qdf_flex_mem_pool name;
这行代码定义了一个类型为
struct qdf_flex_mem_pool
的新变量name
,用于表示内存池。 -
为段头部分配字节:
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
是每个池分配项的大小。 -
定义初始段:
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_SINGLE
和QDF_LIST_ANCHOR
初始化为连接到内存池段列表的单节点。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), \ }
这行代码初始化名为
name
的内存池结构体:seg_list
: 初始化为一个只包含一个段(__name_head.node
)的列表。reduction_limit
: 设置为rm_limit
,表示在减少内存池时保留的最小段数。item_size
: 设置为size_of_item
,表示内存池管理的每个项的大小。
应用该宏
给定的用法:
DEFINE_QDF_FLEX_MEM_POOL(sched_pool, sizeof(struct scheduler_msg), WLAN_SCHED_REDUCTION_LIMIT);
name = sched_pool
: 这会创建一个名为sched_pool
的struct 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)” 指的是在创建内存池时,内存池初始化时包含一个内存段。每个段是内存池中的一个独立块,用于存储特定数量的内存项。
在这个宏的上下文中,一个“段”就是一个内存块,其中包含了若干个固定大小的内存项。段的用途在于管理内存的分配和释放,通常在需要动态分配和回收大量小内存块的情况下使用。通过使用段,可以减少内存碎片,优化内存使用,同时也便于内存管理操作(如扩展、缩减内存池)。
具体来说,“带有一个段”在这里的含义可以理解为:
-
初始内存分配: 宏定义的内存池在创建时,会先分配一段固定大小的内存,这段内存称为“段”。在代码中,使用一个字节数组
__name_head_bytes
来实际存储这些内存数据。 -
段的结构: 每个段都有一个管理节点(
node
),用于在池的段列表中进行链表管理;并且有一个指向实际数据的指针(bytes
),即__name_head_bytes
。 -
段的数量: 虽然这里初始化时只有一个段,但整个设计是支持多个段的(如通过链表结构管理多个段)。段的数量可以根据需要增加或减少,
reduction_limit
参数指定了在减少段时,池中最少需要保留的段数量。
总结来说,“带有一个段”意味着这个内存池一开始就包含了一个已经分配好的内存块,这个块被组织成段,用于存储和管理内存项。随着需求变化,可以动态调整段的数量来管理内存使用。
Q3:
当需要更多内存时,这个内存池会做哪些事情
A3:
当需要更多内存时,这个内存池可能会执行以下操作来分配更多的内存段。这些操作的具体实现取决于内存池的设计和用途。一般来说,灵活的内存池在内存需求增加时会按需扩展,并进行以下步骤:
-
分配新的内存段:
- 如果现有的段已满或无法满足新的内存请求,内存池会分配一个新的内存段(segment)。
- 这涉及为新段分配一个大小为
QDF_FM_BITMAP_BITS * size_of_item
的字节数组。QDF_FM_BITMAP_BITS
和size_of_item
共同决定了每个段中可以存储多少项。 - 新的段通常会使用类似于初始化第一个段的过程,即创建一个新字节数组来保存数据,并将其链接到段链表中。
-
将新段加入段链表:
- 新分配的段通过链表加入到内存池的段列表中。这些段是通过链表结构(如双向链表或单向链表)管理的,以便快速插入和删除。
- 在宏中,有
QDF_LIST_INIT_SINGLE
和QDF_LIST_NODE_INIT_SINGLE
这样的宏,可能用于初始化和操作链表节点,这些节点帮助管理不同段之间的关系。
-
更新内存池的管理结构:
- 内存池的元数据需要更新,例如:
- 段列表的头部指针或尾部指针。
- 内存池中当前段的数量。
- 可能还有内存池的总大小或已使用的内存总量。
- 内存池的元数据需要更新,例如:
-
扩展段的管理:
- 如果内存池有内存回收机制,当内存段不再需要时(例如,当内存项被释放后),池会检查是否可以回收某些段,以节省内存资源。
- 宏中提到的
reduction_limit
是用于减少内存段时的最小段数限制,在增加段时也会考虑到这个限制,以确保内存管理的稳定性和性能。
-
重分配失败时的错误处理:
- 如果内存池在尝试分配新段时失败(例如,没有足够的系统内存),内存池需要有一定的错误处理机制,如记录错误、触发报警、或执行垃圾回收来释放未使用的内存。
-
可能的内存对齐和优化:
- 在某些情况下,内存池可能会进行内存对齐操作,以确保内存访问的效率。
- 对齐和优化可以避免内存碎片,并确保高效的内存使用。
代码示例
虽然你提供的宏并没有完整展示内存池的动态扩展机制,但假设这部分逻辑在内存池实现的其他部分中可以是这样的:
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:
你的解释总体来说是正确的,清晰地描述了内存池的基本工作原理。内存池的确通过预先申请一大块内存,将其分割成小块来高效管理内存分配和释放。下面是一些具体的指正和补充,帮助更好地理解内存池的运作:
-
头部与数据部分的关系:
- 你提到每个小块包含两部分:实际使用的内存项和一个头部。这是准确的。头部通常包含管理信息,例如指向下一个块的指针、块的状态(空闲或已用),以及其他管理所需的元数据。
- 这种设计可以让内存池在不需要额外的内存分配和释放的情况下管理内存块,减少内存碎片的产生。
-
链表的使用:
- 你提到利用头部的指针信息把这些小块串联成一个链表。这也是正确的。链表结构在内存池中很常见,因为它允许高效地插入和删除块,适合动态内存分配的场景。
- 通常,空闲块会被组织成一个空闲链表。每次分配时,内存池从空闲链表中取出一个合适大小的块;每次释放时,将块插回空闲链表。
-
get 和 free API 的实现:
- 内存池的 get 和 free 操作类似于
malloc
和free
,但通常会更高效,因为它们不涉及系统调用。get
可能从空闲链表中找到一个适当大小的块并返回给调用者。free
则把块返回到空闲链表中。 - 内存池的好处是减少了频繁的系统级内存分配操作,系统调用通常较慢且可能引入碎片。而内存池管理的内存通常在堆上,是应用级的。
- 内存池的 get 和 free 操作类似于
-
内存碎片管理:
- 你提到内存池在释放时可能不会将内存返还给操作系统,而是通过调整头部指针来合并小的内存块。这种做法可以减少内存碎片,提高内存使用效率。
- 具体来说,内存池可能实现某种内存合并策略,如相邻空闲块的合并(coalescing),以此来减少内存碎片,使得内存池中有足够大的连续空间来满足未来的内存分配请求。
-
可能的扩展:
- 当内存池中没有足够的空闲块时,内存池可能会扩展,即再申请一块内存并将其加入内存池的管理结构中。这可以通过追加新的段来实现,如前面描述的那样。
总结
你对内存池的描述基本上是正确的,并且涵盖了主要概念。内存池通过预先申请一大块内存,并将其组织成多个小块来管理内存。这种方法避免了频繁的系统调用,从而提高了性能,并通过合并和管理内存块的策略来减少碎片。内存池的实现涉及对内存块的分配、释放、以及碎片管理策略的设计。总体来说,你对内存池的理解是准确的,描述也是很清楚的!
逻辑部分
初始化
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
。
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
:
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
。
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
。
这里也是调度器多线程开始的地方。
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 函数里面进行设置。
我先大概画了一下这两个线程的状态迁移过程,如下图。
然后我们再继续看代码。
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
位,这个位是用来通知调度器线程有新的消息需要处理。
考虑下面这一种情况。
- 调度器处理完成了第一个队列的所有消息,然后开始检查第二个队列。
- 此时有一个新消息被 post 到第一个消息队列,然后设置了
MC_POST_EVENT_MASK
位。 - 调度器依次检查了所有队列,处理完成了所有消息,然后执行了
qdf_atomic_clear_bit(MC_POST_EVENT_MASK, &sch_ctx->sch_event_flag);
,这就导致了再调度器线程进行循环检查消息队列时,会发现暂时没有MC_POST_EVENT_MASK被设置,就会持续等待。但请注意,此时第一个队列中有一个新消息,这个消息并没有被处理。它会等到下一次 post 消息到来时,才会被处理。这就导致了延迟。
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
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
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
。
/**
* 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。
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_id
,dest_id
,que_id
通过移位的方式放在 qid 的不同位置。
这样做可以理解为一种压缩内存的方式,将三个取值范围小于 1024 的数值压缩到一个 32 位的整数中。
#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
函数,这个函数会根据消息的优先级,将消息放入消息队列的不同位置中。
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。
这个函数的实现比较长,大家可以查看我的注释。
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
函数,向调度器模块发送消息。
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 函数,然后参数是消息本身。
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
函数的实现。
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
本身就可以继续追踪了。