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-dirver/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-dirver/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-dirver/qca-wifi-host-cmn/scheduler/src/scheduler_core.c?r=8b3dca18206e1a0461492f082fa6e270b092c035#52

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

在 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 的数据结构。

代码位置:

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-dirver/qca-wifi-host-cmn/scheduler/src/scheduler_core.c?r=8b3dca18206e1a0461492f082fa6e270b092c035#27

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

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)的灵活内存池。我们来详细解释一下这个宏的功能,然后应用到给定的例子中。

宏的详细解析

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

    struct qdf_flex_mem_pool name;

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

  2. 为段头部分配字节:

    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. 定义初始段:

    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. 初始化内存池:

    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_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. 可能的内存对齐和优化

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

代码示例

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

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

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 函数里面进行设置。

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

然后我们再继续看代码。

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

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 消息到来时,才会被处理。这就导致了延迟。
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。

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

希望对你有所帮助!