06 · 内存分配器:无锁快路径失效与上下文限制
结论:非 RT 的 SLUB 快路径靠「抢占关闭 +
this_cpu_cmpxchg」做到无锁,慢路径关中断;页分配器的 per-CPU 页列表用preempt_disable + spin_lock。这些在 RT 上都不成立:local_lock既不关中断也不关抢占,无法排除并发的慢路径,于是USE_LOCKLESS_FAST_PATH()恒为 false,每次kmalloc/kfree都拿cpu_slab->lock(一把 rt_mutex);页分配器改用migrate_disable + spin_trylock,失败退回伙伴系统。此外 RT 的策略是 hardirq 里不允许任何分配/释放,THP、NUMA_BALANCING直接不可选,compact_unevictable_allowed默认 0。
第一层
先看结论:RT 上分配器全程可抢占,但 SLUB 无锁快路径失效、每次分配都拿锁,且 hardirq 内禁止分配;THP 与 NUMA balancing 不可用。
第二层
先看结论:变化集中在三个宏——
USE_LOCKLESS_FAST_PATH()=false、slub_get_cpu_ptr()=migrate_disable、pcpu_task_pin()=migrate_disable;其余是local_lock与preempt_disable_nested的点状替换。
SLUB
设计注释 mm/slub.c:119-149 讲清了两种模式:非 RT 下 cpu_slab->lock 只保护慢路径,快路径靠 tid 事务号检测被抢占/迁移;RT 下(:126-129)「local lock 既不关中断也不关抢占,所以无锁快路径不能用,local lock 总是被拿,但仍然利用 freelist」。
切换宏(:186-205):
| 宏 | 非 RT | RT |
|---|---|---|
slub_get_cpu_ptr() |
get_cpu_ptr() = preempt_disable() |
migrate_disable(); this_cpu_ptr() |
USE_LOCKLESS_FAST_PATH() |
true |
false |
后果:
- 分配(
:4034-4036):if (!USE_LOCKLESS_FAST_PATH() || …) object = __slab_alloc(…)——RT 上每次分配都进___slab_alloc(),在local_lock_irqsave(&s->cpu_slab->lock)(RT 下 =spin_lock一把 rt_mutex,不关中断)下操作 freelist。 - 释放(
:4621-4646):RT 分支/* Update the free list under the local lock */。 - 慢路径要新 slab 时先
slub_put_cpu_ptr()(migrate_enable())再进页分配器(:3891-3893)。 struct kmem_cache_cpu里的local_lock_t lock(:396)。- 策略声明(
:688-692):「RT 上的策略是不允许在 hardirq 上下文做任何分配/释放」——这在 6.12.107 里是约定而非运行时检查(mm/slub.c、mm/page_alloc.c没有in_hardirq()检查,也没有后来的allow_spin参数),违反者会被 lockdep 的「原子上下文睡眠」告警抓住。 SLUB_CPU_PARTIAL(mm/Kconfig:319-328)在 6.12.107 并不depends on !PREEMPT_RT,只是 help 建议实时系统选 N:溢出时清空 per-CPU partial 要拿锁,造成延迟尖峰。
页分配器
mm/page_alloc.c:109-123 的注释与宏:
preempt_disable is used on !RT because it is faster than migrate_disable. migrate_disable is used on RT because otherwise RT spinlock usage is interfered with and a high priority task cannot preempt the allocator.
pcpu_task_pin():非 RTpreempt_disable(),RTmigrate_disable()(:117-123)。per_cpu_pages.lock与zone->lock都是spinlock_t(include/linux/mmzone.h:683-684, 948-949)→ RT 上是睡眠锁。rmqueue_pcplist()(:3086-3117)与free_unref_page()(:2709-2751)用pcp_spin_trylock(),失败直接退到zone->lock路径(rmqueue_buddy()/free_one_page()),不等待。- 页分配器在 6.12.107 也没有
in_hardirq()的 RT 检查。
其它 mm 子系统
| 子系统 | RT 变化 | 位置 |
|---|---|---|
| vmstat | RMW 计数不能再依赖关中断;preempt_disable_nested() 在 RT 上是真 preempt_disable() |
mm/vmstat.c:351-357;include/linux/preempt.h:438-478 |
| memcg | per-CPU stock 用 local_lock_t stock_lock;memcg_stats_lock() 用 preempt_disable_nested() |
mm/memcontrol.c:535-554, 1700-1717 |
| cgroup v1 memory | memcg1_check_events() 直接返回;soft_limit_in_bytes、cgroup.event_control 返回 -EOPNOTSUPP |
mm/memcontrol-v1.c:1523-1524, 1946-1947, 2576-2577;Documentation/admin-guide/cgroup-v1/memory.rst:68, 80 |
| LRU batch | cpu_fbatches 两把 local_lock_t(lock / lock_irq) |
mm/swap.c:49-70, 213-233 |
| mlock / zsmalloc | local_lock_t 保护 per-CPU batch / 映射区 |
mm/mlock.c:31-38;mm/zsmalloc.c:261-266, 1224-1276 |
| kmap_atomic | RT 用 migrate_disable() 代替 preempt_disable()(arm64 无 HIGHMEM,走 highmem-internal.h:205-235 的非 highmem 分支,实际只是 migrate_disable + pagefault_disable) |
include/linux/highmem-internal.h:97-132, 205-235;include/linux/highmem.h:136-147 |
| THP | 不可选 | mm/Kconfig:816-818 |
| NUMA balancing | 不可选 | init/Kconfig:949-953 |
| compaction | compact_unevictable_allowed 默认 0,改动时告警 |
mm/Kconfig:648-652;mm/compaction.c:3285-3294;Documentation/admin-guide/sysctl/vm.rst:144-146 |
| debugobjects | RT 上若任务正阻塞在 rt_mutex 上则不能填充池(避免破坏 PI 状态) | lib/debugobjects.c:608-624 |
| 调度器 | 持 pi_lock 时不能 kfree(),改 kfree_rcu() |
kernel/sched/core.c:2734-2739 |
Documentation/locking/locktypes.rst:470-489 给出了使用侧规则:持 raw_spinlock_t 时不能分配(分配器完全可抢占,不能在真原子上下文调用);持普通 spinlock_t 时 kmalloc(GFP_ATOMIC) 完全没问题。
ARM64:DMA、CMA 与 SMMU(自动驾驶 SoC 的主要内存噪声)
| 机制 | 行为 | 对延迟的影响 | 位置 |
|---|---|---|---|
| 非一致性 DMA 的缓存维护 | 设备不与 CPU 缓存一致时,每次 dma_map/dma_sync 要按缓冲区大小做 dcache_clean_poc() / dcache_inval_poc() |
与数据量成正比:一帧 8 MB 图像清缓存约 1–3 ms,且在驱动(RT 下是 irq 线程或调用线程)里执行 | arch/arm64/mm/dma-mapping.c:15-31 |
dma-coherent |
DT 里声明设备一致性后跳过缓存维护 | 消除上一项;取决于 SoC 互连(CCI/CMN)是否真的一致 | drivers/of/address.c:1000-1010 |
| CMA | 大块连续内存按需从 CMA 区迁移页面后分配 | __cma_alloc() 可能迁移/回收,毫秒级尖峰;应启动期一次性申请 |
mm/cma.c:406;kernel/dma/contiguous.c:91 cma= |
| swiotlb 弹跳 | DMA 掩码不覆盖目标地址时拷贝到弹跳缓冲 | 隐式 memcpy;确保 64 位掩码或 IOMMU 映射 | kernel/dma/swiotlb.c:206 swiotlb= |
| SMMU 严格/惰性失效 | 严格模式每次 unmap 同步 TLB 失效(cmdq sync);惰性模式批量延后 | iommu.strict=0(或 IOMMU_DEFAULT_DMA_LAZY)降低 unmap 路径延迟;iommu.passthrough=1 彻底绕过(安全性换延迟) |
drivers/iommu/iommu.c:44, 207-208;drivers/iommu/Kconfig:110-135;kernel-parameters.txt:2345, 2360 |
| SMMUv3 MSI 轮询 | disable_msipolling 影响 cmdq 同步等待方式 |
平台相关 | drivers/iommu/arm/arm-smmu-v3/arm-smmu-v3.c:36-37 |
| MPAM(缓存/带宽分区) | 6.12.107 无 MPAM 驱动 | GPU/NPU 对 DDR 带宽的挤占只能靠 SoC QoS 寄存器与固件 | 全树无 mpam 驱动 |
这些机制与 RT 无关,但在 RT 上更显眼:因为内核其它不可抢占区都被消除后,一次 2 ms 的缓存清理会成为最坏延迟的主导项。它们的共同解法是把分配与映射挪到启动期(预分配 + 固定映射 + dma-coherent)。
性能影响
净影响:kmalloc/kfree 密集负载退化 10%–30%;分配路径不再制造不可抢占区;RT 应用仍应预分配。
| 指标 | 非 RT | RT | 说明 |
|---|---|---|---|
kmalloc 快路径 |
~20–40 ns 无锁 | 拿/放一把 rt_mutex + migrate_disable/enable:约 2–3 倍 |
分配密集负载(网络 skb、文件系统元数据)可见 |
| 慢路径关中断时长 | 关中断 | 不关中断、可抢占 | 最坏延迟收益 |
| pcp 争用 | 自旋 | trylock 失败直接走 zone->lock(也可抢占) |
更多退回伙伴系统的次数 |
| 直接回收 / 压缩 | 可能让分配挂 ms 级 | 同样存在但可被抢占;THP 折叠不存在 | RT 应用仍需预分配 |
| 大页收益 | THP 可用 | 只能显式 hugetlbfs | TLB 压力可能上升 |
| NUMA 局部性 | 自动 balancing | 无;靠手工 numactl |
|
| hardirq 内分配 | 允许 | 禁止(约定) | 驱动移植点 |
源码定位
| 主题 | 位置 |
|---|---|
| SLUB 设计注释 | mm/slub.c:119-149, 688-700 |
| 切换宏 | mm/slub.c:186-205 |
kmem_cache_cpu.lock |
mm/slub.c:385-400, 3081-3089 |
| 快路径判定 | mm/slub.c:4034-4054, 4621-4646 |
local_lock_irqsave 调用点 |
mm/slub.c:3222-3317, 3717-3951, 3969-3987, 4945-5009 |
SLUB_CPU_PARTIAL |
mm/Kconfig:319-328 |
| 页分配器宏 | mm/page_alloc.c:95-190 |
rmqueue_pcplist / free_unref_page |
mm/page_alloc.c:3086-3117, 2709-2751 |
| 锁类型 | include/linux/mmzone.h:683-684, 948-949 |
| vmstat / memcg / swap / kmap | 见上表 |
| THP / NUMA / compaction Kconfig | mm/Kconfig:648-652, 816-818;init/Kconfig:949-953 |
| arm64 DMA / CMA / SMMU | arch/arm64/mm/dma-mapping.c:15-31;drivers/of/address.c:1000-1010;mm/cma.c:406;kernel/dma/contiguous.c:91;kernel/dma/swiotlb.c:206;drivers/iommu/iommu.c:44, 207-208;drivers/iommu/Kconfig:110-135 |
调优要点
- RT 线程:启动期
mlockall(MCL_CURRENT|MCL_FUTURE)、预触碰栈与堆、热路径不malloc;glibc 用mallopt(M_TRIM_THRESHOLD, -1)/M_MMAP_MAX 0防止归还内存后再缺页。 - 需要大页时用 hugetlbfs(arm64 4K 页下
hugepagesz=2M/1G;64K 页下512M),不要期待 THP。 - ARM64 独有的补偿手段:
CONFIG_ARM64_64K_PAGES(或 16K)直接把基础页放大 16 倍,TLB 压力显著下降,一定程度弥补 THP 缺失;代价是内存碎片与部分驱动/用户态假设 4K 页的兼容问题。 vm.stat_interval=120、关掉khugepaged(本来就没有)、vm.compact_unevictable_allowed保持 0。- 分配密集的内核路径(如高 pps 网络)在 RT 上退化明显,优先靠硬件卸载 / 大包 / GRO 减少分配次数。
小结
- 「local_lock 既不关中断也不关抢占」这一句话,直接废掉了 SLUB 的无锁快路径。
- 页分配器用
migrate_disable+spin_trylock,失败直接退回伙伴系统而不等待。 - hardirq 内禁止分配在 6.12.107 只是注释里的策略,靠 lockdep 抓;
SLUB_CPU_PARTIAL也只是 help 建议关。 - 承上启下:分配器用的
local_lock与migrate_disable,正是 RT 保护所有 per-CPU 数据的通用机制——下一章。