调度器初始化
Kernel initialization. Part 8.
调度器初始化
这是 Linux 内核初始化过程章节的第八部分,在上一部分中我们停在了 setup_nr_cpu_ids
函数处。
本部分的重点是调度器的初始化。但在开始学习调度器的初始化过程之前,我们需要完成一些准备工作。接下来是在 init/main.c 中的 setup_per_cpu_areas
函数,该函数为 percpu
变量设置内存区域。更多相关信息您可以在关于 Per-CPU 变量 的专门章节中阅读。在 percpu
区域启动并运行后,下一步是 smp_prepare_boot_cpu
函数。
smp_prepare_boot_cpu
函数为对称多处理做一些准备工作。由于此函数是针对特定架构的,它位于 arch/x86/include/asm/smp.h Linux 内核头文件中。让我们看看这个函数的定义:
static inline void smp_prepare_boot_cpu(void)
{
smp_ops.smp_prepare_boot_cpu();
}
可以看到,这里实际上调用了 smp_ops
结构体的 smp_prepare_boot_cpu
回调函数。如果我们查看 arch/x86/kernel/smp.c 源文件中该结构体实例的定义,会发现 smp_prepare_boot_cpu
展开为对 native_smp_prepare_boot_cpu
函数的调用:
struct smp_ops smp_ops = {
...
...
...
smp_prepare_boot_cpu = native_smp_prepare_boot_cpu,
...
...
...
}
EXPORT_SYMBOL_GPL(smp_ops);
函数 native_smp_prepare_boot_cpu
形式如下:
void __init native_smp_prepare_boot_cpu(void)
{
int me = smp_processor_id();
switch_to_new_gdt(me);
cpumask_set_cpu(me, cpu_callout_mask);
per_cpu(cpu_state, me) = CPU_ONLINE;
}
该函数主要执行以下操作:首先通过 smp_processor_id
函数获取当前 CPU id
(此时为引导处理器 BSP,其 id
为 0)。关于 smp_processor_id
的工作原理,此处不再赘述,因为我们已在内核入口点部分详细分析过。获取处理器 id
后,函数通过 switch_to_new_gdt
为指定 CPU 重新加载全局描述符表 GDT:
void switch_to_new_gdt(int cpu)
{
struct desc_ptr gdt_descr;
gdt_descr.address = (long)get_cpu_gdt_table(cpu);
gdt_descr.size = GDT_SIZE - 1;
load_gdt(&gdt_descr);
load_percpu_segment(cpu);
}
这里的 gdt_descr
变量表示指向 GDT
描述符的指针(desc_ptr
结构定义已在早期中断和异常处理部分见过)。我们通过指定 id
获取对应 CPU
的 GDT
描述符地址和大小。其中 GDT_SIZE
的值为 256
,或者更准确地说:
#define GDT_SIZE (GDT_ENTRIES * 8)
而描述符的地址将通过 get_cpu_gdt_table
函数获取:
static inline struct desc_struct *get_cpu_gdt_table(unsigned int cpu)
{
return per_cpu(gdt_page, cpu).gdt;
}
get_cpu_gdt_table
函数通过 per_cpu
宏来获取指定 CPU 编号(本例中为 ID
0 的 BSP)对应的 gdt_page
percpu 变量值。
您可能会问:既然我们能访问 gdt_page
这个 percpu 变量,它是在哪里定义的呢?实际上我们在本书前文已经见过它。如果您阅读过本章第一部分,会记得我们在 arch/x86/kernel/head_64.S 中看到过 gdt_page
的定义:
early_gdt_descr:
.word GDT_ENTRIES*8-1
early_gdt_descr_base:
.quad INIT_PER_CPU_VAR(gdt_page)
如果我们查看链接器脚本,可以看到它被放在 __per_cpu_load
符号之后:
#define INIT_PER_CPU(x) init_per_cpu__##x = x + __per_cpu_load
INIT_PER_CPU(gdt_page);
并且在 arch/x86/kernel/cpu/common.c 中完成了对 gdt_page
的初始化:
DEFINE_PER_CPU_PAGE_ALIGNED(struct gdt_page, gdt_page) = { .gdt = {
#ifdef CONFIG_X86_64
[GDT_ENTRY_KERNEL32_CS] = GDT_ENTRY_INIT(0xc09b, 0, 0xfffff),
[GDT_ENTRY_KERNEL_CS] = GDT_ENTRY_INIT(0xa09b, 0, 0xfffff),
[GDT_ENTRY_KERNEL_DS] = GDT_ENTRY_INIT(0xc093, 0, 0xfffff),
[GDT_ENTRY_DEFAULT_USER32_CS] = GDT_ENTRY_INIT(0xc0fb, 0, 0xfffff),
[GDT_ENTRY_DEFAULT_USER_DS] = GDT_ENTRY_INIT(0xc0f3, 0, 0xfffff),
[GDT_ENTRY_DEFAULT_USER_CS] = GDT_ENTRY_INIT(0xa0fb, 0, 0xfffff),
...
...
...
关于 percpu
变量的更多细节,您可以在 Per-CPU 变量章节中阅读。当我们获取到 GDT
描述符的地址和大小后,通过 load_gdt
函数重新加载 GDT
,该函数实际执行 lgdt
指令,并通过以下函数加载 percpu_segment
:
void load_percpu_segment(int cpu) {
loadsegment(gs, 0);
wrmsrl(MSR_GS_BASE, (unsigned long)per_cpu(irq_stack_union.gs_base, cpu));
load_stack_canary_segment();
}
percpu
区域的基地址必须存放在 gs
寄存器(x86 架构下也可能是 fs
寄存器)中,因此我们使用 loadsegment
宏并传入 gs
。接下来的步骤会写入 IRQ 栈的基地址,并设置栈 canary(仅针对 x86_32
)。在加载新的 GDT
后,我们会用当前 CPU 填充 cpu_callout_mask
位图,并通过设置当前处理器的 cpu_state
percpu 变量为 CPU_ONLINE
来标记 CPU 状态为在线。
cpumask_set_cpu(me, cpu_callout_mask);
per_cpu(cpu_state, me) = CPU_ONLINE;
那么,什么是 cpu_callout_mask
位图?在初始化引导处理器(x86 架构中第一个启动的处理器)后,多处理器系统中的其他处理器被称为次级处理器
。Linux 内核使用以下两个位掩码:
cpu_callout_mask
cpu_callin_mask
当 BSP 初始化完成后,它会更新 cpu_callout_mask
来指示接下来可以初始化哪个次级处理器。所有其他次级处理器在初始化前会检查引导处理器设置的 cpu_callout_mask
位图。只有当引导处理器在 cpu_callout_mask
中标记了某个次级处理器后,该次级处理器才会继续其剩余的初始化工作。完成初始化流程后,该次级处理器会在 cpu_callin_mask
中设置自己的位。当引导处理器检测到当前次级处理器在 cpu_callin_mask
中的位被置位后,就会对剩余的次级处理器重复相同的初始化流程。简而言之,其工作原理如我所描述,但我们将在关于 SMP
(对称多处理)的章节中看到更多细节。
至此,我们已完成所有的 SMP
启动准备工作。
Zonelists 构建
接下来,我们会看到 build_all_zonelists
函数的调用。该函数设置了内存分配时优先从哪些内存区域(zone)获取的顺序。我们稍后会解释什么是 zone 以及这个顺序的含义。首先让我们看看 Linux 内核如何管理物理内存。物理内存被划分为称为节点(nodes
)的存储单元。如果您的硬件不支持 NUMA
(非统一内存访问架构),您将只会看到一个节点:
$ cat /sys/devices/system/node/node0/numastat
numa_hit 72452442
numa_miss 0
numa_foreign 0
interleave_hit 12925
local_node 72452442
other_node 0
在 Linux 内核中,每个 node
都由 struct pglist_data
结构体表示,其又被划分为若干特殊的区块,称为内存区域(zones
)。每个 zone 在内核中由 zone struct
表示,并属于以下类型之一(通过 zone_type
枚举定义):
ZONE_DMA
- 0-16MB 内存区域;ZONE_DMA32
- 32 位 DMA 设备专用区域(仅能访问 4GB 以下内存);ZONE_NORMAL
-x86_64
架构上 4GB 以上普通内存区域;ZONE_HIGHMEM
- 在 x86_64 架构上不存在(仅用于 32 位系统);ZONE_MOVABLE
- 包含可移动页面的特殊区域。
可以通过以下方式获取 zone 的详细信息:
$ cat /proc/zoneinfo
Node 0, zone DMA
pages free 3975
min 3
low 3
...
...
Node 0, zone DMA32
pages free 694163
min 875
low 1093
...
...
Node 0, zone Normal
pages free 2529995
min 3146
low 3932
...
...
如前所述,所有内存节点在内核中均通过 pglist_data
(即 pg_data_t
)结构体描述,该结构定义于 include/linux/mmzone.h。mm/page_alloc.c 中的 build_all_zonelists
函数会构建一个有序的 zonelist
(包含 DMA
、DMA32
、NORMAL
、HIGH_MEMORY
、MOVABLE
等不同内存区域),用于指定当所选 zone
或 node
无法满足分配请求时应尝试访问的备用区域/节点顺序。关于 NUMA
和多处理器系统的更多细节,将在后续专题章节深入讨论。
调度器初始化前的剩余步骤
在深入探讨 Linux 内核调度器初始化过程之前,我们需要完成几项准备工作。首先是位于 mm/page_alloc.c 中的 page_alloc_init
函数。这个函数看似简单:
void __init page_alloc_init(void)
{
int ret;
ret = cpuhp_setup_state_nocalls(CPUHP_PAGE_ALLOC_DEAD,
"mm/page_alloc:dead", NULL,
page_alloc_cpu_dead);
WARN_ON(ret < 0);
}
该函数为 CPU 热插拔状态 CPUHP_PAGE_ALLOC_DEAD
设置 startup
和 teardown
回调函数(第二和第三个参数)。当然,这个函数的实现取决于内核配置选项 CONFIG_HOTPLUG_CPU
——若启用该选项,系统将根据各个 CPU 的热插拔状态为其设置相应的回调函数。CPU 热插拔机制是一个复杂主题,本书将不做详细探讨。
完成此函数后,我们能在初始化输出中看到内核命令行信息:

接下来会调用几个处理 Linux 内核命令行的函数,包括 parse_early_param
和 parse_args
。您可能记得我们在内核初始化章节的第六部分已经见过 parse_early_param
的调用,那么为什么这里需要再次调用呢?原因很简单:之前是在架构特定代码(如 x86_64)中调用的,但并非所有架构都会调用此函数。而这里调用 parse_args
是为了解析和处理非早期的命令行参数。
随后我们可以看到来自 kernel/jump_label.c 的 jump_label_init
函数调用,它用于初始化跳转标签(jump label)机制。
接着是 setup_log_buf
函数调用,该函数设置 printk 日志缓冲区。我们在 Linux 内核初始化流程的第七部分已经分析过这个函数。
PID 哈希初始化
接下来是 pidhash_init
函数。如您所知,每个进程都会被分配一个唯一的编号,称为进程标识符(Process Identification Number
,简称 PID
)。内核会为通过 fork 或 clone 生成的每个新进程自动分配一个唯一的 PID
值。PID
的管理主要围绕两个特殊数据结构展开:struct pid
与 struct upid
。第一个结构表示内核中 PID
信息。第二个结构表示特定命名空间可见的 PID
信息。所有 PID
实例都存储在专门的哈希表中:
static struct hlist_head *pid_hash;
该哈希表用于通过数字 PID
值查找对应的 pid
实例。因此,pidhash_init
函数负责初始化这个哈希表。在 pidhash_init
函数的开头,我们可以看到调用了 alloc_large_system_hash
函数:
pid_hash = alloc_large_system_hash("PID", sizeof(*pid_hash), 0, 18,
HASH_EARLY | HASH_SMALL,
&pidhash_shift, NULL,
0, 4096);
pid_hash
哈希表的元素数量取决于 RAM
内存配置,其范围介于 $2^4$ 到 $2^{12}$ 之间。pidhash_init
函数会计算所需大小并分配存储空间(此处使用的是 hlist
结构——与双向链表类似,但在 struct hlist_head 中仅包含单个指针)。如果传递 HASH_EARLY
标志时(如当前场景),alloc_large_system_hash
函数使用 memblock_virt_alloc_nopanic
分配大型系统哈希表,否则改用 __vmalloc
进行分配。
执行结果可通过 dmesg
命令查看:
$ dmesg | grep hash
[ 0.000000] PID hash table entries: 4096 (order: 3, 32768 bytes)
...
...
...
以上就是调度器初始化前的所有准备工作。剩余需要执行的函数包括:
vfs_caches_init_early
执行虚拟文件系统(VFS)的早期初始化(详细分析将在 VFS 专题章节展开);sort_main_extable
对内核内置的异常表项(位于__start___ex_table
和__stop___ex_table
之间)进行排序;trap_init
初始化陷阱处理程序(后两个函数将在中断相关章节详细探讨)。
调度器初始化前最后一步是通过 init/main.c 中的 mm_init
函数完成内存管理器的初始化。我们可以看到,该函数负责初始化 Linux 内核内存管理器各个部分:
page_ext_init_flatmem();
mem_init();
kmem_cache_init();
percpu_init_late();
pgtable_init();
vmalloc_init();
首先是 page_ext_init_flatmem
函数,其功能取决于内核配置选项 CONFIG_SPARSEMEM
,主要用于初始化每页的扩展数据处理。mem_init
负责释放所有 bootmem
内存,kmem_cache_init
初始化内核缓存,percpu_init_late
将 percpu
内存块替换为 SLUB 分配器分配的内存块,pgtable_init
初始化 page->ptl
内核缓存,而 vmalloc_init
则用于初始化 vmalloc
机制。请注意,我们不会深入探讨这些函数和概念的具体细节,但您可以在 Linux 内核内存管理章节中详细了解它们。
至此,准备工作已全部完成。接下来,我们可以开始探讨 scheduler
(调度器)的初始化过程。
调度器初始化
现在,我们来到本部分的核心目标——任务调度器的初始化。需要再次说明的是(正如我已多次强调),这里不会完整解释调度器的全部内容,后续会有专门的章节来详细介绍。这里主要描述最先初始化的调度机制。让我们开始吧。
当前我们关注的是位于 kernel/sched/core.c 内核源代码文件中的 sched_init
函数,顾名思义,它负责初始化调度器。让我们开始深入这个函数,尝试理解调度器是如何初始化的。在 sched_init
函数的开头,我们可以看到以下调用:
sched_clock_init();
sched_clock_init
是一个相当简单的函数,其主要功能是设置 sched_clock_init
变量:
void sched_clock_init(void)
{
sched_clock_running = 1;
}
该变量将在后续流程中使用。接下来的步骤是初始化 waitqueues
数组:
for (i = 0; i < WAIT_TABLE_SIZE; i++)
init_waitqueue_head(bit_wait_table + i);
其中 bit_wait_table
定义如下:
#define WAIT_TABLE_BITS 8
#define WAIT_TABLE_SIZE (1 << WAIT_TABLE_BITS)
static wait_queue_head_t bit_wait_table[WAIT_TABLE_SIZE] __cacheline_aligned;
bit_wait_table
是一个等待队列数组,用于根据特定位的值来等待/唤醒进程。初始化 waitqueues
数组后,下一步是计算为 root_task_group
分配的内存大小。可以看到,这个大小取决于以下两个内核配置选项:
#ifdef CONFIG_FAIR_GROUP_SCHED
alloc_size += 2 * nr_cpu_ids * sizeof(void **);
#endif
#ifdef CONFIG_RT_GROUP_SCHED
alloc_size += 2 * nr_cpu_ids * sizeof(void **);
#endif
CONFIG_FAIR_GROUP_SCHED
;CONFIG_RT_GROUP_SCHED
。
这两个选项提供了两种不同的调度模型。根据文档说明,当前使用的调度器——完全公平调度器(CFS
)采用了一个简洁的设计理念。它将进程调度建模为理想的多任务处理器,其中每个可运行进程都能获得 $\displaystyle\frac 1 n$ 的处理器时间(n
代表可运行进程数量)。该调度器遵循特定的规则集,这些规则决定了何时以及如何选择新进程运行,被称为调度策略
。
完全公平调度器
支持以下普通
(即非实时
)调度策略:
SCHED_NORMAL
;SCHED_BATCH
;SCHED_IDLE
.
SCHED_NORMAL
用于普通应用程序,每个进程占用的 CPU 时间主要由 nice 值决定;SCHED_BATCH
用于 100% 非交互式任务;而 SCHED_IDLE
仅在处理器没有其他任务可运行时才会调度。
对于时间敏感的实时应用,内核也支持两种策略:SCHED_FIFO
和 SCHED_RR
。了解 Linux 内核调度器的读者会知道其采用模块化设计,这意味着它支持不同算法来调度不同类型的进程。这种模块化通常称为调度类
(scheduler classes),这些模块封装了调度策略细节,由调度器核心统一处理而无需了解具体实现。
现在回到代码,我们关注两个配置选项:CONFIG_FAIR_GROUP_SCHED
和 CONFIG_RT_GROUP_SCHED
。虽然调度器调度的基本单位是单个任务或线程,但进程并非唯一可调度实体。这两个选项均提供了对组调度的支持——前者针对完全公平调度器
策略,后者则对应实时
策略。
简单来说,组调度是一种功能,允许我们将一组任务视为单个任务进行调度。例如,如果你创建一个包含两个任务的组,从内核的角度来看,这个组就像一个普通任务。当组被调度时,调度器会从该组中选择一个任务在组内进行调度。这种机制让我们能够构建层级结构并管理它们的资源。虽然调度的最小单位是进程,但 Linux 内核调度器在底层并不直接使用 task_struct
结构,而是通过专门的 sched_entity
结构作为调度单元。
因此,当前的目标是计算为根任务组的 sched_entity
分配的空间大小,我们通过以下两种情况进行计算:
#ifdef CONFIG_FAIR_GROUP_SCHED
alloc_size += 2 * nr_cpu_ids * sizeof(void **);
#endif
#ifdef CONFIG_RT_GROUP_SCHED
alloc_size += 2 * nr_cpu_ids * sizeof(void **);
#endif
第一种情况是启用了完全公平
调度器的任务组调度功能,第二种情况则是针对实时
调度器的相同用途。这里我们计算的大小等于指针大小乘以系统中的 CPU 数量,再乘以 2
。需要乘以 2
是因为我们要为以下两项分配空间:
调度实体结构;
runqueue
。
计算完大小后,我们通过 kzalloc
函数分配空间,并在此设置 sched_entity
和 runqueues
的指针:
ptr = (unsigned long)kzalloc(alloc_size, GFP_NOWAIT);
#ifdef CONFIG_FAIR_GROUP_SCHED
root_task_group.se = (struct sched_entity **)ptr;
ptr += nr_cpu_ids * sizeof(void **);
root_task_group.cfs_rq = (struct cfs_rq **)ptr;
ptr += nr_cpu_ids * sizeof(void **);
#endif
#ifdef CONFIG_RT_GROUP_SCHED
root_task_group.rt_se = (struct sched_rt_entity **)ptr;
ptr += nr_cpu_ids * sizeof(void **);
root_task_group.rt_rq = (struct rt_rq **)ptr;
ptr += nr_cpu_ids * sizeof(void **);
#endif
如前所述,Linux 的组调度机制支持层级结构定义。这类层级结构的根节点是 root_runqueuetask_group
任务组结构体。该结构体包含多个字段,但目前我们重点关注 se
、rt_se
、cfs_rq
和 rt_rq
这四个成员:
其中 se
和 rt_se
是 sched_entity
结构体的实例,该结构定义于 include/linux/sched.h 头文件,是调度器的基本调度单元。
struct task_group {
...
...
struct sched_entity **se;
struct cfs_rq **cfs_rq;
...
...
}
cfs_rq
和 rt_rq
表示运行队列(run queues
)。运行队列是一种特殊的 per-cpu
结构,Linux 内核调度器用它来存储 active
线程,换句话说,这些线程集合中的线程可能会被调度器选中运行。
空间分配完成后,下一步是初始化 real-time
(实时)和 deadline
(截止时间)任务的 CPU 带宽:
init_rt_bandwidth(&def_rt_bandwidth,
global_rt_period(), global_rt_runtime());
init_dl_bandwidth(&def_dl_bandwidth,
global_rt_period(), global_rt_runtime());
所有任务组都必须能够依赖预设的 CPU 时间配额。以下两个结构体:def_rt_bandwidth
和 def_dl_bandwidth
,分别表示实时任务和截止时间任务的默认带宽配置。虽然这些结构体的具体定义目前并不重要,但我们重点关注以下两个参数值:
sched_rt_period_us
;sched_rt_runtime_us
.
第一个参数表示周期长度,第二个参数表示在 sched_rt_period_us
周期内为 real-time
任务分配的时间量(quantum)。这些参数的全局默认值可以在以下位置查看:
$ cat /proc/sys/kernel/sched_rt_period_us
1000000
$ cat /proc/sys/kernel/sched_rt_runtime_us
950000
与任务组相关的参数可通过 <cgroup>/cpu.rt_period_us
和 <cgroup>/cpu.rt_runtime_us
进行配置。由于此时尚未挂载任何文件系统,def_rt_bandwidth
和 def_dl_bandwidth
将使用 global_rt_period
与 global_rt_runtime
函数返回的默认值进行初始化。
至此已完成 real-time
和 deadline
任务的带宽设置。接下来,根据是否启用 SMP,我们将初始化 root domain
:
#ifdef CONFIG_SMP
init_defrootdomain();
#endif
实时调度器需要全局资源来做出调度决策。但随着 CPU 数量增加,可扩展性瓶颈就会出现。引入根域(root domains
)概念正是为了提升可扩展性并避免此类瓶颈。调度器不再需要遍历所有运行队列,而是通过 root_domain
结构获取可以推送/拉取实时任务的 CPU 信息。该结构定义于 kernel/sched/sched.h 头文件,主要跟踪可用于进程迁移的 CPU 集合。
完成根域初始化后,我们像之前一样为根任务组的实时任务初始化带宽控制:
#ifdef CONFIG_RT_GROUP_SCHED
init_rt_bandwidth(&root_task_group.rt_bandwidth,
global_rt_period(), global_rt_runtime());
#endif
并使用相同的默认值进行初始化。
接下来,根据内核配置选项 CONFIG_CGROUP_SCHED
的设置,我们会为 task_group
结构分配 slab
缓存,并初始化根任务组的 siblings
和 children
链表。根据内核文档说明,CONFIG_CGROUP_SCHED
选项的作用是:
允许通过 cgroup 伪文件系统创建任意任务组,并控制分配给每个任务组的 CPU 带宽。
完成链表初始化后,我们可以看到调用了 autogroup_init
函数:
#ifdef CONFIG_CGROUP_SCHED
list_add(&root_task_group.list, &task_groups);
INIT_LIST_HEAD(&root_task_group.children);
INIT_LIST_HEAD(&root_task_group.siblings);
autogroup_init(&init_task);
#endif
该函数用于初始化自动进程组调度机制。autogroup
特性的作用是通过 setsid 系统调用创建新会话时,自动创建并填充新的任务组。
完成此操作后,我们会遍历所有 possible
CPU(您可能记得 possible
CPU 存储在 cpu_possible_mask
位图中,表示系统中可能存在的所有 CPU),并为每个 possible
CPU 初始化运行队列:
for_each_possible_cpu(i) {
struct rq *rq;
...
...
...
Linux 内核中的 rq
结构体定义于 kernel/sched/sched.h。如之前所述,运行队列是调度过程中的核心数据结构,调度器通过它决定下一个要运行的任务。该结构体包含众多字段,我们不会在此逐一说明,而是在后续实际使用时具体分析。
在为每个 CPU 的运行队列完成默认值初始化后,我们需要设置系统中第一个任务的负载权重
(load weight
):
set_load_weight(&init_task);
首先我们来理解什么是进程的负载权重
。如果查看 sched_entity
结构的定义,会发现它起始于 load
字段:
struct sched_entity {
struct load_weight load;
...
...
...
}
load weight
结构仅包含两个字段,分别表示调度实体的实际权重及其优化计算的固定倒数:
struct load_weight {
unsigned long weight;
u32 inv_weight;
};
您可能知道系统中每个进程都有优先级
(priority
)。更高的优先级意味着可以获得更多的运行时间。进程的负载权重
实际上反映了该进程优先级与其时间片配额之间的关系。每个进程都包含以下三个与优先级相关的字段:
struct task_struct {
...
...
...
int prio;
int static_prio;
int normal_prio;
...
...
...
}
第一个是动态优先级
(dynamic priority
),它基于进程的静态优先级和交互性,在进程生命周期内不可更改。static_prio
包含初始优先级,即您可能熟知的 nice
值。除非用户主动修改,否则内核不会改变这个值。最后一个 normal_priority
虽然也基于 static_prio
的值,但同时还取决于进程的调度策略。
set_load_weight
函数的主要目标是为 init
任务初始化 load_weight
字段:
static void set_load_weight(struct task_struct *p)
{
int prio = p->static_prio - MAX_RT_PRIO;
struct load_weight *load = &p->se.load;
if (idle_policy(p->policy)) {
load->weight = scale_load(WEIGHT_IDLEPRIO);
load->inv_weight = WMULT_IDLEPRIO;
return;
}
load->weight = scale_load(sched_prio_to_weight[prio]);
load->inv_weight = sched_prio_to_wmult[prio];
}
可以看到,我们根据 init
任务的初始 static_prio
值计算出初始 prio
,并将其作为 sched_prio_to_weight
和 sched_prio_to_wmult
数组的索引来设置 weight
和 inv_weight
值。这两个数组包含了基于优先级值的负载权重
。对于 idle
进程,我们设置最小负载权重。
至此,我们已完成 Linux 内核调度器的初始化过程。最后的步骤包括:将当前进程(即首个 init
进程)设为 idle
状态(当 CPU 没有其他进程可运行时执行),计算下一次 CPU 负载计算的时间周期,以及初始化 fair
调度类:
__init void init_sched_fair_class(void)
{
#ifdef CONFIG_SMP
open_softirq(SCHED_SOFTIRQ, run_rebalance_domains);
#endif
}
这里我们注册了一个软中断,该中断将调用 run_rebalance_domains
处理程序。当 SCHED_SOFTIRQ
被触发时,run_rebalance
函数会被调用来重新平衡当前 CPU 的运行队列。
sched_init
函数的最后两个步骤是初始化调度器统计信息和设置 scheduler_running
变量:
scheduler_running = 1;
至此,Linux 内核调度器已完成初始化。当然,我们在此跳过了许多细节和解释,因为需要先理解 Linux 内核中各种概念(如进程和进程组、运行队列、RCU 等)的工作原理。不过我们已经对调度器初始化过程有了基本了解,更详细的内容将在专门讨论调度器的独立章节中深入分析。
总结
这是关于 Linux 内核初始化过程的第八部分的结尾。在本部分中,我们探讨了调度器的初始化流程,并将在下一部分继续深入 Linux 内核初始化过程,重点分析 RCU 机制及其他核心组件的初始化工作。
如果您有任何疑问或建议,请通过 Twitter 或者 email 与我联系,或者创建一个 issue。
Please note that English is not my first language, And I am really sorry for any inconvenience. If you find any mistakes please send me PR to linux-insides.
链接
最后更新于