五、进阶 | 内核初始化流程(8)

内核初始化。第八部分。

调度器初始化

这是Linux内核初始化过程的第八部分,我们在上一部分setup_nr_cpu_ids函数处暂停了。

这部分的重点是调度器初始化。但在我们开始学习调度器的初始化过程之前,我们需要做一些工作。在init/main.c中的下一步是setup_per_cpu_areas函数。这个函数为percpu变量设置内存区域,你可以在关于每个CPU变量的特别部分中阅读更多信息。在percpu区域启动并运行后,下一步是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(目前是引导处理器,其id为零)。我不会解释smp_processor_id的工作原理,因为我们在内核入口点部分已经看到了。得到处理器id后,我们使用switch_to_new_gdt函数为给定CPU重新加载全局描述符表

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结构的定义)。我们为给定idCPU获取GDT描述符的地址和大小。GDT_SIZE256或:

#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编号的gdt_page percpu变量的值(在我们的情况下是引导处理器,id为0)。

你可能会问:那么,如果我们可以直接访问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变量的更多信息,你可以在每个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寄存器(或x86fs寄存器),所以我们使用loadsegment宏并传递gs。接下来我们写入IRQ栈的基地址并设置栈canary(这只适用于x86_32)。在我们加载新的GDT之后,我们用当前cpu填充cpu_callout_mask位图,并将cpu状态设置为在线,通过设置当前处理器的cpu_state percpu变量为CPU_ONLINE

cpumask_set_cpu(me, cpu_callout_mask);
per_cpu(cpu_state, me) = CPU_ONLINE;

那么,什么是cpu_callout_mask位图呢?当我们初始化引导处理器(在x86上首先启动的处理器)时,多处理器系统中的其他处理器被称为secondary processors。Linux内核使用以下两个位掩码:

  • cpu_callout_mask
  • cpu_callin_mask

在引导处理器初始化后,它会更新cpu_callout_mask以指示接下来可以初始化哪个次要处理器。所有其他或次要处理器可以做一些初始化工作,并在引导处理器的cpu_callout_mask上检查位。只有在引导处理器用这个次要处理器填充了cpu_callout_mask之后,它才会继续其余的初始化。之后,当某个处理器完成其初始化过程后,该处理器在cpu_callin_mask中设置位。一旦引导处理器为当前次要处理器找到了cpu_callin_mask中的位,这个处理器就会重复相同的过程来初始化剩余的次要处理器之一。简而言之,它的工作方式正如我描述的,但我们将在关于SMP的章节中看到更多细节。

就这样了。我们做了所有的SMP启动准备。

构建zonelists

下一步我们可以看到调用了build_all_zonelists函数。这个函数设置了区域的顺序,即在分配请求不能被选定的区域或节点满足时,分配将优先从哪些区域进行。什么是区域以及什么是顺序我们很快就会明白。首先让我们看看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内核中,每个nodestruct pglist_data表示。每个节点被分成许多特殊的块,称为zones。每个区域由Linux内核中的zone struct表示,并且具有以下类型之一:

  • ZONE_DMA - 0-16M;
  • ZONE_DMA32 - 用于只能对低于4G的DMA区域进行操作的32位设备;
  • ZONE_NORMAL - x86_64上所有4GB以上的RAM;
  • ZONE_HIGHMEM - 在x86_64上不存在;
  • ZONE_MOVABLE - 包含可移动页面的区域。

这些都由zone_type枚举表示。我们可以通过以下方式获取有关区域的信息:

$ 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_datapg_data_t结构描述。这个结构在include/linux/mmzone.h中定义。build_all_zonelists函数在mm/page_alloc.c中构建了一个有序的zonelist(由不同的区域DMADMA32NORMALHIGH_MEMORYMOVABLE组成),它指定了当选定的区域或节点不能满足分配请求时,要访问的区域/节点。就这些。关于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);
}

它为CPUHP_PAGE_ALLOC_DEAD cpu 热插拔状态设置了启动拆除回调(第二个和第三个参数)。当然,这个函数的实现取决于CONFIG_HOTPLUG_CPU内核配置选项,如果设置了这个选项,这样的回调将根据系统的热插拔状态为所有cpu(s)设置。热插拔机制是一个大主题,本书不会描述。

在这个函数之后,我们可以看到内核命令行在初始化输出中:

图片上传失败,请重新上传

还有一些函数,如parse_early_paramparse_args,用于处理Linux内核命令行。你可能记得我们在内核初始化章节的第六部分已经看到了parse_early_param函数的调用,那么我们为什么再次调用它呢?答案很简单:我们在架构特定的代码(我们的情况是x86_64)中调用了这个函数,但并非所有架构都调用这个函数。我们需要调用第二个函数parse_args来解析和处理非早期命令行参数。

接下来我们可以看到调用了kernel/jump_label.c中的jump_label_init,并初始化了跳转标签

在此之后,我们可以看到调用了setup_log_buf函数,该函数设置了printk日志缓冲区。我们已经在Linux内核初始化过程的第七部分中看到了这个函数。

PID哈希初始化

接下来是pidhash_init函数。如你所知,每个进程都被分配了一个唯一的号码,称为进程识别号PID。每个通过fork或clone生成的进程都会由内核自动分配一个新的唯一PID值。PID的管理围绕着两个特殊的数据结构:struct pidstruct upid。第一个结构代表内核中的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^42^12之间。pidhash_init计算大小并分配所需的存储空间(在我们的情况下是hlist - 与双向链表相同,但是包含一个指向struct hlist_head的指针)。alloc_large_system_hash函数使用memblock_virt_alloc_nopanic分配一个大的系统哈希表,如果我们传递了HASH_EARLY标志(就像我们的情况一样)或者如果没有传递这个标志,就使用__vmalloc

我们在dmesg输出中可以看到结果:

$ dmesg | grep hash
[    0.000000] PID hash table entries: 4096 (order: 3, 32768 bytes)
...
...
...

就这样了。调度器初始化之前的其他工作是以下函数:vfs_caches_init_early进行了虚拟文件系统的早期初始化(更多内容将在描述虚拟文件系统的章节中介绍),sort_main_extable对内核内置的异常表条目进行了排序,这些条目位于__start___ex_table__stop___ex_table之间,trap_init初始化了陷阱处理程序(我们将在关于中断的单独章节中了解最后两个函数的更多信息)。

调度器初始化之前的最后一步是使用init/main.c中的mm_init函数初始化内存管理器:

page_ext_init_flatmem();
mem_init();
kmem_cache_init();
percpu_init_late();
pgtable_init();
vmalloc_init();

第一个是page_ext_init_flatmem,它取决于CONFIG_SPARSEMEM内核配置选项,并初始化每页扩展数据处理。mem_init释放所有bootmemkmem_cache_init初始化内核缓存,percpu_init_lateslub分配的percpu块替换,pgtable_init初始化page->ptl内核缓存,vmalloc_init初始化vmalloc。请注意,我们不会深入探讨所有这些函数和概念的细节,但我们将在整个Linux内核内存管理章节中看到它们。

就这样了。现在我们可以看看调度器

调度器初始化

现在我们来到这部分的主要内容 - 任务调度器的初始化。我想再次强调,正如我已经多次做的那样,你不会在这里看到调度器的完整解释,将有一个专门的章节来讲述这个。这里将描述首先初始化的初始调度器机制。让我们开始。

我们当前的点是内核源代码文件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

这两个选项提供了两种不同的调度模型。正如我们从文档中读到的,当前的调度器 - CFSCompletely Fair Scheduler使用了一个简单的概念。它将进程调度建模为如果系统有一个理想的多任务处理器,每个进程将获得1/n处理器时间,其中n是可运行进程的数量。调度器使用一套特殊的规则。这些规则决定了何时以及如何选择要运行的新进程,它们被称为调度策略

Completely Fair Scheduler支持以下normal或换句话说非实时调度策略:

  • SCHED_NORMAL
  • SCHED_BATCH
  • SCHED_IDLE

SCHED_NORMAL用于大多数普通应用程序,每个进程消耗的CPU量主要由nice值决定,SCHED_BATCH用于100%非交互式任务,SCHED_IDLE只在处理器没有其他任务要运行时才运行任务。

调度器还支持针对时间关键型应用程序的实时策略:SCHED_FIFOSCHED_RR。如果你读过有关Linux内核调度器的内容,你可以知道它是模块化的。这意味着它支持不同的算法来调度不同类型的进程。通常这种模块化被称为调度器类。这些模块封装了调度策略细节,由调度器核心处理,而不需要了解太多。

现在让我们回到我们的代码,看看两个配置选项:CONFIG_FAIR_GROUP_SCHEDCONFIG_RT_GROUP_SCHED。调度器工作的最小单位是单个任务或线程。然而,进程并不是调度器可以操作的唯一实体类型。这两个选项都提供了组调度的支持。第一个选项提供了与完全公平调度策略的组调度支持,第二个选项分别提供了与实时策略的组调度支持。

用简单的话说,组调度是一个功能,它允许我们将一组任务安排得就像它们是一个单独的任务一样。例如,如果你创建了一个包含两个任务的组,那么从内核的角度来看,这个组就像一个普通任务一样。在组被调度之后,调度器将从这个组中挑选一个任务,它将在组内被调度。因此,这种机制允许我们构建层次结构并管理它们的资源。尽管调度的最小单位是一个进程,但Linux内核调度器在内部并不使用task_struct结构。有一个特殊的sched_entity结构被Linux内核调度器用作调度单元。

所以,当前的目标是计算为根任务组的sched_entity(ies)分配空间,我们用以下方式做两次:

#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_entityrunqueues的指针:

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任务组结构。这个结构包含许多字段,但目前我们对sert_secfs_rqrt_rq感兴趣:

前两个是sched_entity结构的实例。它在include/linux/sched.h内核头文件中定义,并被调度器用作调度单元。

struct task_group {
    ...
    ...
    struct sched_entity **se;
    struct cfs_rq **cfs_rq;
    ...
    ...
}

cfs_rqrt_rq表示run queuesrun queue是Linux内核调度器用来存储active线程或换句话说,一组可能被调度器选中运行的线程的特殊的per-cpu结构。

空间分配后,下一步是为实时截止时间任务初始化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_bandwidthdef_dl_bandwidth代表实时截止时间任务的带宽默认值。我们现在不会看这些结构的定义,因为目前这并不重要,但我们对以下两个值感兴趣:

  • sched_rt_period_us
  • sched_rt_runtime_us

第一个代表周期,第二个代表在sched_rt_period_us期间为实时任务分配的量子。你可以在以下位置看到这些参数的全局值:

$ 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_bandwidthdef_dl_bandwidth将使用默认值初始化,这些值将由global_rt_periodglobal_rt_runtime函数返回。

就这样了,完成了实时截止时间任务的带宽,下一步是根据启用的SMP,我们初始化根域

#ifdef CONFIG_SMP
    init_defrootdomain();
#endif

实时调度器需要全局资源来做出调度决策。但不幸的是,随着CPU数量的增加,会出现可扩展性瓶颈。为了提高可扩展性和避免这种瓶颈,引入了根域的概念。调度器不是遍历所有的run queues,而是从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(s)分配slab缓存,并初始化根任务组的siblingschildren列表。正如我们从文档中读到的,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调用创建新会话期间自动创建和填充新的任务组。

在此之后,我们将遍历所有可能的CPU(你可以记得可能的CPU存储在cpu_possible_mask位图中,该位图可能在系统中可用)并为每个可能的CPU初始化一个runqueue

for_each_possible_cpu(i) {
    struct rq *rq;
    ...
    ...
    ...

在Linux内核中,rq结构在kernel/sched/sched.h中定义。正如我已经提到的,run queue是调度过程中的一个基本数据结构。调度器使用它来确定接下来谁将被运行。如你所见,这个结构有许多不同的字段,我们不会在这里覆盖所有这些字段,但当它们被直接使用时,我们将看到它们。

在用默认值初始化per-cpu运行队列之后,我们需要设置系统中第一个任务的负载权重

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;
};

你可能已经知道,系统中的每个进程都有优先级。更高的优先级允许获得更多的运行时间。进程的负载权重是进程优先级和该进程的时间片之间的关系。每个进程都有以下三个与优先级相关的字段:

struct task_struct {
    ...
    ...
    int            prio;
    int            static_prio;
    int            normal_prio;
    ...
    ...
}

第一个是动态优先级,这是在进程的生命周期内基于其静态优先级和进程的互动性不能改变的。static_prio包含初始优先级,很可能是众所周知的nice值。这个值在用户不更改它的情况下不会被内核更改。最后一个是基于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_weightsched_prio_to_wmult数组的索引来设置weightinv_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
}

在这里,我们注册了一个软IRQ,它将调用run_rebalance_domains处理程序。在触发SCHED_SOFTIRQ之后,将调用run_rebalance来重新平衡当前CPU上的运行队列。

sched_init函数的最后两步是初始化调度器统计信息和设置scheduler_running变量:

scheduler_running = 1;

就这样了。Linux内核调度器已经初始化。当然,我们在这里跳过了许多不同的细节和解释,因为我们需要了解和理解Linux内核中不同概念(如进程和进程组、运行队列、rcu等)的工作方式,但我们对调度器初始化过程进行了简短的了解。我们将在专门讨论调度器的单独部分中看到所有其他细节。

结论

这是关于Linux内核初始化过程的第八部分的结尾。在这部分中,我们看了调度器的初始化过程,我们将在下一部分继续深入了解Linux内核初始化过程,并在下一部分看到RCU的初始化和许多其他初始化工作。

如果你有任何问题或建议,请在评论中告诉我,或者在twitter上联系我。

请注意,英语不是我的第一语言,我对任何不便表示歉意。如果你发现任何错误,请向我发送PR到linux-insides

链接

#嵌入式##面经#
Linux嵌入式必考必会 文章被收录于专栏

&quot;《Linux嵌入式必考必会》专栏,专为嵌入式开发者量身打造,聚焦Linux环境下嵌入式系统面试高频考点。涵盖基础架构、内核原理、驱动开发、系统优化等核心技能,实战案例与理论解析并重。助你快速掌握面试关键,提升竞争力。

全部评论

相关推荐

做黑夜里的那道光:两年电赛完赛没必要写,纯扣分
双非本科求职如何逆袭
点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务