07 · per-CPU 数据:preempt_disable → local_lock + migrate_disable
结论:非 RT 保护 per-CPU 数据的方式是关抢占(或关中断),几乎零成本但制造不可抢占区。RT 把
local_lock_t定义成 per-CPU 的spinlock_t(rt_mutex),加锁 =migrate_disable()+spin_lock(this_cpu_ptr(lock)):任务被钉在当前 CPU 但仍可被抢占,同 CPU 想进同一临界区的任务睡眠并享受 PI。代价是每次加解锁贵一个量级,且「被钉住但可被抢占」的任务给负载均衡制造了一类新麻烦——调度器只能把别的任务推走,不能搬它。内核维护者自己说这是「临时的权宜之计,终极目标是消灭migrate_disable()」。
第一层
先看结论:RT 用「per-CPU 睡眠锁 + 禁止迁移」替代「关抢占」来保护 per-CPU 数据:临界区可抢占了,但被钉住的任务会干扰负载均衡。
第二层
先看结论:
migrate_disable只加计数,亲和性在被调度出去时才懒惰收缩;CFS 靠cpus_ptr排除钉住的任务,RT 类改推当前任务,DL 类直接放弃。
local_lock 两种实现
include/linux/local_lock_internal.h:
| API | 非 RT(:9-119) |
RT(:121-170) |
|---|---|---|
local_lock_t |
无 lockdep 时是空结构体(:11-16) |
typedef spinlock_t local_lock_t;(:127) |
local_lock() |
preempt_disable()(:76-80) |
migrate_disable(); spin_lock(this_cpu_ptr(lock))(:136-140) |
local_lock_irq() |
local_irq_disable()(:82-86) |
同 local_lock(),不关中断(:142) |
local_lock_irqsave() |
local_irq_save()(:88-92) |
flags = 0; local_lock()(:144-149) |
local_lock_nested_bh() |
只做 lockdep 断言(:112-116) |
真 spin_lock(),不再 migrate_disable(外层 bh 已钉住)(:161-165) |
文档 Documentation/locking/locktypes.rst:158-206, 324-393 列出了 RT 上会「碎」的写法:local_lock_irq() 后再 raw_spin_lock() 不行(前者不关中断也不关抢占);get_cpu_ptr() + spin_lock() 要改成 migrate_disable(); this_cpu_ptr(); spin_lock()(:420-441);单靠 migrate_disable() 不防重入,必须用 local_lock(:443-467)。
6.12.107 里 local_lock_t 共 26 处实例,关键的:mm/slub.c:396(cpu_slab)、mm/swap.c:55,64、mm/memcontrol.c:1701、mm/mlock.c:32、mm/zsmalloc.c:262、kernel/softirq.c:121(RT 的 bh 锁本身)、net/core/skbuff.c:284(napi_alloc_cache)、include/linux/netdevice.h:3250(backlog process_queue)、net/core/filter.c:1679、net/ipv4/tcp_sigpool.c:14、include/net/sock.h:557、include/linux/radix-tree.h:29、drivers/char/random.c:215,500、drivers/md/raid5.h:567、drivers/block/zram/zcomp.h:34 等。
migrate_disable 内部
kernel/sched/core.c:
migrate_disable()(:2323-2342):嵌套只加计数;首次进入this_rq()->nr_pinned++; p->migration_disabled = 1;。- 亲和性收缩是懒惰的:任务被调度出去时
__schedule()→migrate_disable_switch()(:2304-2321,调用点:6724)才把p->cpus_ptr指向cpumask_of(rq->cpu)。注释自嘲「违反了锁规则」(:2317-2320, 2672-2691)。 migrate_enable()(:2344-2382):cpus_ptr != &cpus_mask时__set_cpus_allowed_ptr()还原(可能触发 stopper 迁移),然后migration_disabled = 0; nr_pinned--。affine_move_task()的长注释(:2807-2874):「Migrate-Disable 踩坏了我们精心的沙堡」——set_cpus_allowed_ptr()必须等目标任务最外层migrate_enable()。- 设计说明
include/linux/preempt.h:373-426:migrate_disable只是把干扰项从「高优先级等低优先级」换成「低优先级被钉住、高优先级需要把它推走」,干扰仍在系统里;「the end goal must be to get rid of migrate_disable()」。
对调度器的影响
| 调度类 | 行为 | 位置 |
|---|---|---|
| CFS 负载均衡 | can_migrate_task() 没有显式检查,靠 cpus_ptr 已收缩到单 CPU 使 cpumask_test_cpu(env->dst_cpu, p->cpus_ptr) 失败 → LBF_SOME_PINNED |
kernel/sched/fair.c:9467-9525 |
| RT push | 要推的任务被钉住 → 改推当前正在运行的任务:find_lowest_rq(rq->curr) + stop_one_cpu_nowait(push_cpu_stop) |
kernel/sched/rt.c:2006-2045 |
| RT pull | 源任务被钉住 → get_push_task(src_rq) |
kernel/sched/rt.c:2364-2371 |
| DL push | 被钉住 → 直接放弃 | kernel/sched/deadline.c:2947-2948 |
| 唤醒选核 | is_migration_disabled(p) 时跳过 |
kernel/sched/core.c:3540 |
| CPU 热插拔 | rq_has_pinned_tasks() 非零时 CPU 不能下线 |
kernel/sched/core.c:7939-7952, 8328 |
set_task_cpu() |
WARN_ON_ONCE(is_migration_disabled(p)) |
kernel/sched/core.c:3291 |
__cant_migrate() |
might_sleep() 的迁移版断言 |
kernel/sched/core.c:8728-8757 |
get_push_task()(kernel/sched/sched.h:2631-2648)对 migration_disabled 任务返回 NULL;6.12.107 没有 task_is_pushable() 这个后来的助手。
preempt_disable_nested()
include/linux/preempt.h:438-478:在非 RT 上只是 lockdep 断言(因为持 spinlock/在 softirq 里本来就关了抢占),在 RT 上是真正的 preempt_disable()——用于 seqcount 写侧、vmstat 这类需要 CPU 本地原子性的 RMW。用户:mm/vmstat.c、mm/memcontrol.c、include/linux/u64_stats_sync.h:156,163、net/core/gen_estimator.c:93,98、net/ipv4/inet_connection_sock.c:1202,1212、fs/dcache.c:2440,2453。
ARM64 注
this_cpu_*在 arm64 上用TPIDR_EL1保存 per-CPU 偏移(arch/arm64/include/asm/percpu.h:50-52__my_cpu_offset),读偏移是一条mrs;migrate_disable()保证的正是这个偏移在临界区内不变。local_lock的 RT 实现与架构无关;差异只在底层spin_lock(rt_mutex)的 cmpxchg 是否有 LSE(第 02 章)。- big.LITTLE 平台上被
migrate_disable钉住的任务若在小核,push_rt_task()只能把别的任务推走,不能把它挪到大核——RT 线程一开始就该绑在大核(第 12 章)。
性能影响
净影响:每次
local_lock从约 1 ns 变成几十 ns;被钉住的任务让均衡短期失真;内核维护者视之为待消灭的权宜之计。
| 指标 | 非 RT | RT | 说明 |
|---|---|---|---|
| 单次 lock+unlock | ~1 ns(计数) | 函数调用 + rq 计数 + cmpxchg ×2:数十 ns;争用时 µs 级 | 网络/内存热路径累积 |
| 临界区期间本 CPU | 独占,不可抢占 | 可抢占 | 延迟收益 |
| 同 CPU 争用 | 不存在(不可抢占) | 睡眠 + PI | 新增的一类等待 |
| 负载均衡 | 不受影响 | 被钉住的任务不能搬,短期不均;RT push 退化为推走当前任务(stopper 开销) | |
| CPU 下线 | 正常 | 需等所有钉住任务离开 |
源码定位
| 主题 | 位置 |
|---|---|
| local_lock 公共 API | include/linux/local_lock.h:10-73 |
| 非 RT / RT 实现 | include/linux/local_lock_internal.h:9-119 / 121-170 |
| 文档 | Documentation/locking/locktypes.rst:158-206, 324-470, 525-533 |
migrate_disable/enable |
kernel/sched/core.c:2304-2401 |
| 设计说明 | include/linux/preempt.h:371-436 |
| task/rq 字段 | include/linux/sched.h:888-891;kernel/sched/sched.h:1306-1308, 1365-1374, 2594-2648 |
| 负载均衡 / push-pull | kernel/sched/fair.c:9328-9525, 11835-12049;kernel/sched/rt.c:1946-1957, 2006-2045, 2364-2371;kernel/sched/deadline.c:2887-2896, 2947-2948, 3069-3076 |
preempt_disable_nested |
include/linux/preempt.h:438-478 |
调优要点
- RT 系统尽量避免热插拔 CPU;需要的话先把负载迁走。
- 内核代码审查关键词:
get_cpu()/put_cpu()、this_cpu_ptr()后接spin_lock、local_irq_save保护的 per-CPU 数据——在 RT 上都要换成local_lock系列。 - 在非 RT 内核上开
CONFIG_DEBUG_LOCK_ALLOC,local_lock会带 lockdep map(LD_LOCK_PERCPU)提前抓重入。
小结
typedef spinlock_t local_lock_t一行定义了 RT 的 per-CPU 保护模型。- 26 处
local_lock_t实例覆盖 slab、LRU、memcg、softirq、网络缓存、随机数、RAID5、zram。 - 调度器为 migrate-disabled 任务加了一整套特判:push/pull、热插拔、
__cant_migrate()。 - 承上启下:per-CPU 数据的另一大用户是 RCU 的回调链表——下一章看 RCU 在 RT 上的整体改造。