资讯详情

Linux 内核初始化(九):RCU 读-复制-更新子系统的初始化全解析

📅 2026/10/4 15:32:49 | 华诺云谱 👁 阅读
Linux 内核初始化(九):RCU 读-复制-更新子系统的初始化全解析
文档教程操作系统【免费下载链接】linux-insides-zhLinux 内核揭秘项目地址https://gitcode.com/gh_mirrors/li/linux-insides-zh点击查看免费下载导读本文是《Linux 内核揭秘》linux-insides-zh初始化章节的第九篇承接上一篇调度器初始化深入剖析start_kernel中 RCURead-Copy Update读-复制-更新子系统的完整初始化过程。文章将从preempt_disable抢占禁用谈起依次讲解整数 ID 管理缓存idr_init_cache、树形 RCU 的节点层次结构、rcu_init的逐行实现、软中断softirq注册以及初始化流程后半段的其余关键函数读完后你将掌握 RCU 的基本术语临界区、静止态、宽限期、rcu_state/rcu_node层次几何的计算方法以及 RCU 如何以RCU_SOFTIRQ软中断形式融入内核下半部机制。初始化上下文从调度器到抢占禁用在init/main.c的start_kernel中紧接sched_init调度器初始化之后是preempt_disable()的调用。本仓库上一篇文章调度器初始化已经详细分析了setup_per_cpu_areas、smp_prepare_boot_cpu与sched_init等前置步骤而接下来我们要关注两个宏preempt_disablepreempt_enable它们分别用于关闭和开启抢占。**抢占preemption**在操作系统内核语境中的含义是内核有能力暂停当前任务去运行优先级更高的任务。在内核启动的早期阶段只有唯一一个init进程在运行在调用cpu_idle之前我们不需要也不希望它被抢占因此必须在这里关闭抢占。preempt_disable 的两种实现preempt_disable宏定义在include/linux/preempt.h其实现依赖内核配置项CONFIG_PREEMPT_COUNT。开启该选项时#define preempt_disable() \ do { \ preempt_count_inc(); \ barrier(); \ } while (0)未开启CONFIG_PREEMPT_COUNT时则退化为#define preempt_disable() barrier()两种实现的差别在于开启CONFIG_PREEMPT_COUNT后宏会调用preempt_count_inc递增一个特殊的 per-CPU 变量__preempt_count它记录当前 CPU 上持有的锁/关闭抢占的嵌套计数DECLARE_PER_CPU(int, __preempt_count);关于 per-CPU 变量的完整机制.data..percpu段、DEFINE_PER_CPU宏等可以参考本仓库的 Per-CPU 变量 一节。读取__preempt_count值的 API 是preempt_count函数。preempt_count_inc的展开链如下#define preempt_count_inc() preempt_count_add(1) #define preempt_count_add(val) __preempt_count_add(val)其中preempt_count_add最终通过raw_cpu_add_4宏把1加到 per-CPU 变量__preempt_count上。barrier 与内存屏障两种实现中都调用了barrier宏它插入一个优化屏障optimization barrier。为什么要这样做以 x86_64 架构为例独立的内存访问操作可能以任意顺序被处理器执行编译器也可能对指令重新排序。例如下面的代码preempt_disable(); foo(); preempt_enable();如果没有屏障编译器可能将其重排为preempt_disable(); preempt_enable(); foo();这样一来本应不可抢占的foo函数就可能被抢占。把barrier放进preempt_disable与preempt_enable宏中可以阻止编译器把preempt_count_inc与其它语句交换顺序从而保证递增计数与临界区的先后关系。这是内存屏障机制在内核中的典型应用场景。中断状态检查禁用抢占之后紧接着是if (WARN(!irqs_disabled(), Interrupts were enabled *very* early, fixing it\n)) local_irq_disable();该语句用irqs_disabled检查 IRQ中断请求当前是否处于关闭状态如果中断仍然开启这在内核启动早期是不正常的则通过local_irq_disable将其关闭——在 x86_64 上对应cli指令。至此抢占已禁用初始化流程继续前进。整数 ID 管理初始化idr_init_cache接下来调用的是idr_init_cache定义在lib/idr.c。内核中的idr 库被广泛用于把整数 ID 映射到对象指针以及通过 ID 查找对象这一模式。其初始化实现如下void __init idr_init_cache(void) { idr_layer_cache kmem_cache_create(idr_layer_cache, sizeof(struct idr_layer), 0, SLAB_PANIC, NULL); }这里调用kmem_cache_create创建名为idr_layer_cache的 slab 缓存。在init/main.c中我们已经调用过kmem_cache_init它使用kmem_cache_alloc创建广义缓存而这里创建的kmem_cache专供整数 ID 的idr_layer对象使用。kmem_cache_create接收五个参数参数含义本例取值name缓存名称idr_layer_cachesize缓存对象大小sizeof(struct idr_layer)offset页面中第一个对象的偏移0flags缓存标志SLAB_PANICctor对象构造函数NULL无构造器SLAB_PANIC标志意味着若缓存创建失败内核直接 panic而不是返回 NULL。整数 ID 是内核中非常常见的整数集合 ↔ 指针集合映射模式。一个经典用例是 i2c 驱动子系统drivers/i2c/i2c-core-base.c定义了 i2c 适配器 ID 管理器static DEFINE_IDR(i2c_adapter_idr);随后在声明 i2c 适配器时动态分配总线号static int __i2c_add_numbered_adapter(struct i2c_adapter *adap) { int id; ... id idr_alloc(i2c_adapter_idr, adap, adap-nr, adap-nr 1, GFP_KERNEL); ... }idr_alloc在i2c_adapter_idr中分配一个 ID 并把adap指针关联上去adap-nr即动态计算出的总线号。RCU 初始化RCU 是什么**RCURead-Copy Update**是 Linux 内核中一个可扩展、高性能的同步机制。早期 Linux 内核为并发应用提供支持但内核内部的执行被一把全局大锁串行化现代内核早已没有这种单一全局锁而是提供无锁数据结构、per-CPU 数据结构等多种机制RCU 就是其中之一。RCU 技术面向很少被修改的数据结构其核心思想非常直观假设有一个很少被修改的数据结构当有人想要修改它时先复制一份并在副本上完成所有修改与此同时其它读者继续使用旧版本。接下来需要选择一个安全时刻——此时旧版本已经没有读者在使用——再用修改后的副本替换掉旧版本。关键术语要理解 RCU必须先掌握四个术语临界区critical section数据读者执行读取的区域。进入临界区时调用rcu_read_lock退出时调用rcu_read_unlock。静止态quiescent state线程不在临界区中所处的状态。宽限期grace period所有线程都处于静止态的那一时刻。两阶段删除若某线程要从数据结构中移除一个元素分两步完成。第一步是移除removal——原子地把元素从数据结构中摘除但不释放物理内存此后该元素对线程读者仍然可见。第二步等待宽限期结束——此时没有读者再引用被移除的元素——才真正把它从物理内存中释放。注上述描述是对 RCU 的极大简化详细的理论与实现会在本仓库同步原语章节的后续部分展开。TINY 与 TREE 两种实现rcu_init的实现取决于两个内核配置选项CONFIG_TINY_RCUtiny RCU位于kernel/rcu/tiny.c同时要求CONFIG_SMPnCONFIG_TREE_RCUtree树形RCU位于kernel/rcu/tree.c是新的主流实现。本文以 tree RCU 为主线分析rcu_init。rcu_init 逐行解析kernel/rcu/tree.c中的rcu_init完整代码如下void __init rcu_init(void) { int cpu; rcu_bootup_announce(); rcu_init_geometry(); rcu_init_one(rcu_bh_state, rcu_bh_data); rcu_init_one(rcu_sched_state, rcu_sched_data); __rcu_init_preempt(); open_softirq(RCU_SOFTIRQ, rcu_process_callbacks); /* * We dont need protection against CPU-hotplug here because * this is called early in boot, before either interrupts * or the scheduler are operational. */ cpu_notifier(rcu_cpu_notify, 0); pm_notifier(rcu_pm_notify, 0); for_each_online_cpu(cpu) rcu_cpu_notify(NULL, CPU_UP_PREPARE, (void *)(long)cpu); rcu_early_boot_tests(); }下面按执行顺序逐个分析。1. rcu_bootup_announce启动横幅static void __init rcu_bootup_announce(void) { pr_info(Hierarchical RCU implementation.\n); rcu_bootup_announce_oddness(); }它用pr_info打印 Hierarchical RCU implementation. 以及rcu_bootup_announce_oddness输出的当前 RCU 配置信息——后者同样基于pr_info内容取决于CONFIG_RCU_TRACE、CONFIG_PROVE_RCU、CONFIG_RCU_FANOUT_EXACT等配置选项。2. rcu_init_geometry计算节点树几何RCU 之所以可扩展在于其内部 RCU 锁竞争极低。当同一数据结构被多个 CPU 并发读取时RCU 通过rcu_state结构维护全局状态其中包含节点层次hierarchystruct rcu_node node[NUM_RCU_NODES];rcu_node结构定义于kernel/rcu/tree.h保存当前宽限期信息、宽限期是否完成、哪些 CPU/组需要切换以推进当前宽限期等。每个rcu_node为若干个 CPU 提供一把锁。这些rcu_node被内嵌进rcu_state的一个线性数组中并组织成一棵树根节点是数组首元素覆盖所有 CPU。源码注释对这一层次结构的描述是层次结构的根第一层位于-node[0]由-level[0]引用第二层位于-node[1]到-node[m]-node[1]由-level[1]引用第三层位于-node[m1]及其后-node[m1]由-level[2]引用。层数由 CPU 数量和CONFIG_RCU_FANOUT决定。小系统将只有一个rcu_node构成的层次。节点总数由NUM_RCU_NODES决定它依赖可用 CPU 数量#define NUM_RCU_NODES (RCU_SUM - NR_CPUS) #define RCU_SUM (NUM_RCU_LVL_0 NUM_RCU_LVL_1 NUM_RCU_LVL_2 NUM_RCU_LVL_3 NUM_RCU_LVL_4)其中各层数值取决于CONFIG_RCU_FANOUT_LEAF配置选项。例如最简单的情形一台 8 核机器上一个rcu_node覆盖两个 CPU层次树如下所示----------------------------------------------------------------- | rcu_state | | ---------------------- | | | root | | | | rcu_node | | | ---------------------- | | | | | | ----v----- --v------- | | | | | | | | | rcu_node | | rcu_node | | | | | | | | | ------------------ ---------------- | | | | | | | | | | | | | | ----v----- -------v-- -v-------- -v-------- | | | | | | | | | | | | | rcu_node | | rcu_node | | rcu_node | | rcu_node | | | | | | | | | | | | | ---------- ---------- ---------- ---------- | | | | | | | | | | | | | | | | | | | | | | | | | ---------|-----------------|-------------|---------------|------- | | | | ---------v-----------------v-------------v---------------v-------- | | | | | | CPU1 | CPU3 | CPU5 | CPU7 | | | | | | | CPU2 | CPU4 | CPU6 | CPU8 | | | | | | ------------------------------------------------------------------rcu_init_geometry的任务就是计算总共需要多少个rcu_node结构。首先计算到第一次以及下一次强制静止态force-quiescent-statefqs的 jiffies 值d RCU_JIFFIES_TILL_FORCE_QS nr_cpu_ids / RCU_JIFFIES_FQS_DIV; if (jiffies_till_first_fqs ULONG_MAX) jiffies_till_first_fqs d; if (jiffies_till_next_fqs ULONG_MAX) jiffies_till_next_fqs d;其中#define RCU_JIFFIES_TILL_FORCE_QS (1 (HZ 250) (HZ 500)) #define RCU_JIFFIES_FQS_DIV 256jiffies_till_first_fqs与jiffies_till_next_fqs的默认值是ULONG_MAXstatic ulong jiffies_till_first_fqs ULONG_MAX; static ulong jiffies_till_next_fqs ULONG_MAX;只有在二者仍为默认值时才会被赋值为刚计算出的d。接着如果rcu_fanout_leaf未改变编译期即与CONFIG_RCU_FANOUT_LEAF相同且nr_cpu_ids NR_CPUS则直接返回if (rcu_fanout_leaf CONFIG_RCU_FANOUT_LEAF nr_cpu_ids NR_CPUS) return;随后计算在给定层数下一棵rcu_node树能够容纳的节点容量rcu_capacity[0] 1; rcu_capacity[1] rcu_fanout_leaf; for (i 2; i MAX_RCU_LVLS; i) rcu_capacity[i] rcu_capacity[i - 1] * CONFIG_RCU_FANOUT;最后在循环中按树中各层依次算出每一层的rcu_node数量。3. rcu_init_one初始化 rcu_state几何计算完成回到rcu_init下一步用rcu_init_one初始化两个rcu_statercu_init_one(rcu_bh_state, rcu_bh_data); rcu_init_one(rcu_sched_state, rcu_sched_data);rcu_init_one接收两个参数全局 RCU 状态rcu_state与该状态对应的 per-CPU 数据rcu_data。两个状态及其 per-CPU 数据定义在kernel/rcu/tree.hextern struct rcu_state rcu_bh_state; DECLARE_PER_CPU(struct rcu_data, rcu_bh_data);rcu_bh_state服务于 bottom-half软中断上下文的 RCU 读端rcu_sched_state服务于普通调度上下文。4. __rcu_init_preempt可抢占 RCU 状态随后调用__rcu_init_preempt它受CONFIG_PREEMPT_RCU配置选项控制做的事情与前面类似——用rcu_init_one初始化rcu_preempt_state这个rcu_state类型结构用于可抢占内核中的 RCU 读端保护。5. open_softirq注册 RCU 软中断open_softirq(RCU_SOFTIRQ, rcu_process_callbacks);**软中断softirq**机制允许把一部分中断处理工作推迟到系统负载较低时再执行。软中断由struct softirq_action表示定义于include/linux/interrupt.hstruct softirq_action { void (*action)(struct softirq_action *); };它只包含一个字段——中断处理函数指针。open_softirq接收两个参数软中断索引与处理函数实现就是把处理函数挂到softirq_vec数组中void open_softirq(int nr, void (*action)(struct softirq_action *)) { softirq_vec[nr].action action; }Linux 内核当前定义了十种软中断向量RCU 占其中之一。关于HI_SOFTIRQ、TIMER_SOFTIRQ、NET_TX_SOFTIRQ、NET_RX_SOFTIRQ、RCU_SOFTIRQ等完整枚举以及软中断与 tasklets、工作队列的关系可以参考本仓库的延后中断软中断Tasklets 和工作队列介绍。在本例中注册的处理函数是rcu_process_callbacks定义于kernel/rcu/tree.c它负责当前 CPU 上 RCU 的核心处理。运行中的系统可以通过/proc/softirqs观察各类软中断的累计次数其中RCU行即 RCU 软中断$ cat /proc/softirqs CPU0 CPU1 CPU2 CPU3 CPU4 CPU5 CPU6 CPU7 HI: 2 0 0 1 0 2 0 0 TIMER: 137779 108110 139573 107647 107408 114972 99653 98665 NET_TX: 1127 0 4 0 1 1 0 0 NET_RX: 334 221 132939 3076 451 361 292 303 BLOCK: 5253 5596 8 779 2016 37442 28 2855 BLOCK_IOPOLL: 0 0 0 0 0 0 0 0 TASKLET: 66 0 2916 113 0 24 26708 0 SCHED: 102350 75950 91705 75356 75323 82627 69279 69914 HRTIMER: 510 302 368 260 219 255 248 246 RCU: 81290 68062 82979 69015 68390 69385 63304 634736. cpu_notifier 与 pm_notifier注册软中断之后cpu_notifier(rcu_cpu_notify, 0); pm_notifier(rcu_pm_notify, 0); for_each_online_cpu(cpu) rcu_cpu_notify(NULL, CPU_UP_PREPARE, (void *)(long)cpu);这里注册了 CPU 通知器针对支持 CPU 热插拔的系统与电源管理通知器并遍历所有在线 CPU向每个 CPU 发送CPU_UP_PREPARE事件以完成 RCU 数据的初始化。注释明确指出此处不需要针对 CPU 热插拔加保护因为此时正处于启动早期中断与调度器都尚未运行。关于通知链机制的完整介绍notifier_block、notifier_fn_t、NOTIFY_DONE/NOTIFY_OK等返回值语义可以参考本仓库的 Linux 内核中的通知链。7. rcu_early_boot_testsRCU 自检rcu_init的最后一个函数是rcu_early_boot_testsvoid rcu_early_boot_tests(void) { pr_info(Running RCU self tests\n); if (rcu_self_test) early_boot_test_call_rcu(); if (rcu_self_test_bh) early_boot_test_call_rcu_bh(); if (rcu_self_test_sched) early_boot_test_call_rcu_sched(); }它根据rcu_self_test、rcu_self_test_bh、rcu_self_test_sched三个开关一般由CONFIG_RCU_SELF_TEST等测试配置驱动在启动早期对普通、bottom-half 与调度三种 RCU 读端调用call_rcu系列接口做自检。至此RCU 子系统的初始化完成。初始化进程的其余部分RCU 初始化是本文主线但start_kernel尚未结束。下面这些函数同样运行于初始化阶段本文只做概览不深入细节原因有三它们对通用初始化流程不重要且依赖不同内核配置它们带有调试性质它们会在后续章节中详细展开。RCU 之后首先是trace_init初始化内核**跟踪tracing**子系统。接着是radix_tree_init初始化内核的基数树Radix tree实现定义于lib/radix-tree.c这是一种压缩 trie 结构支持关键字-值的存储与查找。接下来是与中断处理子系统相关的三个函数early_irq_initinit_IRQsoftirq_init它们会在本仓库中断章节中专门讲解。其后是一系列与定时器相关的函数init_timers、hrtimers_init、time_init等详见定时器和时钟管理章节。再往后是与 perf 事件相关的perf_event_init后续有专门章节以及profile_init分析初始化。随后执行local_irq_enable();它展开为sti指令重新开启中断。紧接着调用kmem_cache_init_late对 SLAB 分配器做后期初始化SLAB 将在内存管理章节详解。之后是控制台初始化console_init来自drivers/tty/tty_io.c再往后依次是lockdep_info打印锁依赖校验器Lock dependency validator信息debug_objects_mem_init初始化调试对象的动态分配kmemleak_init初始化内核内存泄漏检测器setup_per_cpu_pageset设置 per-CPU pagesetnuma_policy_init设置 NUMA 内存策略sched_clock_init为调度器设置时钟pidmap_init为初始 PID 命名空间初始化 pidmapanon_vma_init为私有虚拟内存区域创建匿名映射缓存acpi_early_init对 ACPI 做早期初始化。acpi_early_init正是初始化结束第十篇的起点下一篇将以此为衔接点继续分析init_espfix_bsp、thread_info_cache_init、cred_init等函数并最终结束start_kernel、进入rest_init开启第一个进程。总结本篇完整走过了 Linux 内核初始化第九步的全程抢占禁用preempt_disable通过 per-CPU 变量__preempt_count与barrier优化屏障保证启动早期唯一 init 进程不被抢占整数 ID 管理idr_init_cache为idr_layer创建 slab 缓存支撑 i2c 等子系统的ID→指针映射RCU 初始化从 RCU 的理论临界区、静止态、宽限期、两阶段删除到 tree RCU 的实现rcu_state/rcu_node层次、rcu_init_geometry几何计算、rcu_init_one状态初始化、RCU_SOFTIRQ软中断注册、CPU/PM 通知器与启动自检初始化其余部分trace、radix tree、中断子系统、定时器、perf、SLAB 后期初始化、控制台、lockdep、NUMA、ACPI 等函数概览。RCU 的深层实现读端/写端 API、加速路径、宽限期推进等将在本仓库同步原语章节的 RCU 专篇中继续展开而内核初始化流程则将在下一篇初始化结束中画上句号。赞分享文档教程操作系统【免费下载链接】linux-insides-zhLinux 内核揭秘项目地址https://gitcode.com/gh_mirrors/li/linux-insides-zh点击查看免费下载相关推荐Linux 内核初始化九RCU 子系统的启动与初始化全解析linux-insidesLinux 内核初始化九RCU 子系统的启动与初始化全解析linux insides 导读 本文是 linux insides 仓库《Linux 内核文档教程操作系统内核初始化流程九RCU 与整数 ID 管理子系统的早期初始化内核初始化流程九RCU 与整数 ID 管理子系统的早期初始化 导读 本文是 Linux 内核揭秘linux insides zh「内核初始化」章节的第深入 Linux 内核 initcall 机制linux-insides 中的子系统初始化排序原理深入 Linux 内核 initcall 机制linux insides 中的子系统初始化排序原理 导读 initcall 是 Linux 内核中用于按正确文档教程操作系统上一篇IPS Group, Inc. 公司档案深度解读Remote In Tech 目录中智能停车科技公司的技术栈与远程招聘实践下一篇Umi-OCR 完整指南3 步上手免费离线 OCR截图、批量图片、PDF 一次识别创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑