☰
Linux 内核 CPU 拓扑:topology.c 与 sysfs 导出原理
2026/9/27 0:55:40 网站建设 项目流程

跑过一次lscpu,看到输出里的 Socket、Core、Thread、NUMA node,你就已经间接遇到了 Linux 内核里的drivers/base/topology.c。它不是一个具体的设备驱动,而是设备驱动模型基础层里的一个“地图导出器”:把体系结构层算好的 CPU 拓扑关系,整理成/sys/devices/system/cpu/cpuX/topology/下的一组只读属性。做性能优化、排查绑核问题、学习内核调度的人,迟早会翻到这个文件。

这篇笔记是我实际走读drivers/base/topology.c的记录,同时把周边知识串起来:sysfs 里每个字段怎么读、数据从哪里来、能用来解决什么问题、以及学习时最容易被绕进去的坑。如果你是想搞清楚lscpu -p背后原理的人,或者刚接触内核设备模型的读者,这篇内容可以直接当作一份带注释的路线图。

1. 先搞清楚 topology 在驱动模型里的位置

1.1 drivers/base 是内核设备模型的“底盘”

drivers/base这层目录,是把 Linux 设备驱动模型撑起来的地方。里面有core.c、bus.c、driver.c、class.c、platform.c,还有cpu.c、memory.c、node.c、topology.c这类和具体硬件总线无关的子系统文件。简单说,总线驱动、设备驱动负责和硬件打交道,而drivers/base负责维护设备、驱动、总线和类之间的关系,提供一个统一的框架。

topology.c放在这个目录,本身就说明了一件事:CPU 拓扑不是某个外设总线的私有能力,而是和 CPU 设备、内存节点平级的系统级信息。它不直接 read/write 某个寄存器,也不做中断处理,它的任务只有一个:把“CPU 和 CPU 之间怎么排列”这件事,从内核计算好的数据里提取出来,挂到每个 CPU 设备对应的 sysfs 目录下。

很多刚看内核的人会下意识找一个probe函数,但topology.c没有传统的struct platform_driver注册。它更像是在subsys_initcall阶段,遍历所有可能的 CPU,给每个cpuX设备补上一组属性。这种“设备目录已经存在,我往里面挂属性”的形式,在驱动模型里很典型,和普通驱动“注册自己、等匹配、然后初始化”的思路不太一样。

1.2 没有统一抽象时会乱成什么样

为什么不能每个架构单独导出一套拓扑 sysfs?因为不同 CPU 架构描述拓扑的方式差得太远了。

x86 有完整的 APIC ID 体系,可以从中解码出 package、die、core、thread 层级;ARM/ARM64 靠 MPIDR 寄存器里的亲和层级;s390 还有 book、drawer 这类更复杂的层级。如果每个架构都按自己的习惯暴露节点,用户态工具就惨了:lscpu得为每一种平台维护一套解析逻辑,内核主线也没办法对这套用户 API 做稳定承诺。

topology.c的价值,是把“暴露哪些属性”和“这些属性值怎么算”分开。前者在drivers/base/topology.c里统一固定成physical_package_id、core_id、thread_siblings、core_siblings等名字;后者交给架构层实现,通过topology_xxx(cpu)这样的钩子函数提供具体数值。

换句话说,架构层只需要回答“某个 CPU 的 package 是多少、core 是多少、哪些 CPU 和它共享同一个核”,剩下的 sysfs 格式、位图转换、属性可见性判断,都由topology.c统一处理。这也是它在基础层而不是在具体平台驱动目录里的根本原因。

2. 手把手看懂 sysfs 里的 CPU 拓扑节点

2.1 cpuX/topology 下的属性到底代表什么

先找一台 Linux 机器,随便挑一个 CPU 看目录:

ls /sys/devices/system/cpu/cpu0/topology/

你会看到一堆文件。常见的有下面这些。

属性名含义补充说明
physical_package_id物理封装 ID一般对应一个 socket;在单 socket 多 die 封装中也用来区分物理封装
die_id封装内 die 的编号老内核不一定有这个属性,多 die 处理器上更常见
core_id物理 core 在 package/die 内的编号注意不等于全局 CPU 编号
thread_siblings与当前 CPU 共享同一个物理 core 的 CPU 位图也就是同一个核上的超线程兄弟
thread_siblings_list上面的可读列表格式比如0,1
core_siblings与当前 CPU 处于同一个物理 package 的所有 CPU 位图历史命名容易误解,它表示的是 package 范围
core_siblings_list上面的可读列表格式比如0-7
die_cpus与当前 CPU 处于同一个 die 的所有 CPU 位图新内核常用
die_cpus_list上面的可读列表格式比如0-15
package_cpus与当前 CPU 处于同一个 package 的所有 CPU 位图和core_siblings兼容,但语义更明确
package_cpus_list上面的可读列表格式按 CPU 编号排序的列表

我第一次看到core_siblings的时候,被名字坑过:以为它表示“同一个 core 的兄弟”,结果发现里面装的是一个物理 package 内的所有 CPU。后来看Documentation/admin-guide/cputopology.rst才确认,这是历史命名带来的历史包袱,理解的时候直接把它当成“package 内 CPU 集合”就好。

2.2 用命令把这些字段和 lscpu 对上

直接读文件,是最直观的验证方式:

topo=/sys/devices/system/cpu/cpu0/topology for f in core_id physical_package_id thread_siblings_list core_siblings_list; do printf "%-24s: %s\n" "$f" "$(cat "$topo/$f")" done

在一台双核超线程机器上,输出可能是:

core_id : 0 physical_package_id : 0 thread_siblings_list : 0,1 core_siblings_list : 0-3

这说明 CPU0 和 CPU1 共享一个物理 core,而 CPU0、CPU1、CPU2、CPU3 都在同一个物理 package 里。再用lscpu -e看:

lscpu -e

输出里的 CORE 列、SOCKET 列、CPU 列,正好能对应上core_id、physical_package_id和全局 CPU 编号。看到这里再回来看lscpu -p的机器可解析格式,你会发现它基本就是把 sysfs 的_list文件重新拆开了一遍。

thread_siblings这类位图字段用的是内核 cpumask 格式,比如00000000,00000003。它不是一个普通十进制数,而是按 bit 位映射 CPU 编号:bit 0 对应 CPU0,bit 1 对应 CPU1,以此类推。两个位图文件内容和_list文件是一回事,只是表达方式不同。用户态程序处理_list更省事,内核内部处理位图更快。

3. 源码走查:topology.c 到底做了哪几件事

3.1 属性组、可见性和初始化流程

打开drivers/base/topology.c,代码量不大,核心结构是“一批属性宏 + 一个属性组 + 一个初始化函数”。

属性宏的套路大概是这样的:

#define define_id_show_file(name) \ static ssize_t name##_show(struct device *dev, \ struct device_attribute *attr, \ char *buf) \ { \ return sysfs_emit(buf, "%d\n", topology_##name(dev->id)); \ } \ static DEVICE_ATTR_RO(name)

物理 package、die、core 这类单值属性,都是靠这个宏生成的。dev->id在这里就是 CPU 编号,topology_physical_package_id(cpu)这些函数负责去问架构层要具体值。

然后定义属性组:

static struct attribute *topology_attrs[] = { &physical_package_id_attr.attr, &die_id_attr.attr, &core_id_attr.attr, &thread_siblings_attr.attr, &core_siblings_attr.attr, ... NULL, }; static umode_t topology_is_visible(struct kobject *kobj, struct attribute *attr, int i) { struct device *dev = kobj_to_dev(kobj); if (attr == &die_id_attr.attr && !topology_die_id(dev->id)) return 0; if (attr == &book_id_attr.attr && !topology_book_id(dev->id)) return 0; ... return attr->mode; } static const struct attribute_group topology_attr_group = { .attrs = topology_attrs, .is_visible = topology_is_visible, };

初始化时遍历每一个可能的 CPU,把属性组挂到对应的cpuX设备上:

static int __init topology_sysfs_init(void) { int cpu; for_each_possible_cpu(cpu) { struct device *dev = get_cpu_device(cpu); if (dev) { if (sysfs_create_group(&dev->kobj, &topology_attr_group)) pr_warn("failed to register topology sysfs for cpu%d\n", cpu); } } return 0; } subsys_initcall(topology_sysfs_init);

这里比较关键的一点是:初始化用的是subsys_initcall,而不是普通的module_init。拓扑信息在系统早期就可能有用户空间进程读取,而且它不应该依赖某个设备驱动是否 probe 成功。即使属性挂载失败,也只是打一条警告,不让系统启动失败。这种“尽力而为”的初始化风格,在系统级功能里很常见。

3.2 topology_xxx 钩子背后的架构实现

drivers/base/topology.c读的是topology_xxx(cpu)这类接口,但真正实现可能藏在不同的地方。以 x86 为例,arch/x86/kernel/topology.c很早就把 APIC ID 换算成 package ID、core ID,存在cpuinfo_x86结构体里。topology_physical_package_id(cpu)最后会落到读取这个结构体对应字段。

ARM64 那边的情况不太一样,arch/arm64/kernel/topology.c负责从 MPIDR 寄存器解析亲和层级,然后通过相同语义的topology_xxx钩子喂给通用层。s390 还会有 book、drawer 这样的额外层级,所以在通用属性组里能看到这几个“罕见”属性,但topology_is_visible会判断当前架构是否提供了非 0 值,没有就直接隐藏,避免每个 sysfs 目录里出现一堆无意义文件。

这其实就是内核常见的“通用框架 + 架构钩子”模式。你在drivers/base/topology.c里看不到任何ifdef x86,因为平台差异已经被接口盖住了。真要看懂一个属性的数值怎么来的,不能只停在base目录,还得跟着topology_xxx跳到具体架构源码里。我读这个文件最大的心得,就是“属性名越通用,背后依赖的架构细节越妖”。

另外要特别注意:sysfs 里这些文件是只读导出,不是调度的配置开关。你往physical_package_id里 echo 数字会被拒绝,因为DEVICE_ATTR_RO没有写方法。它对内核调度的真实影响,是通过架构启动时填充的 cpu topology mask 进入调度域计算流程的,sysfs 只是同一个数据面向用户空间的一个窗口。

4. 实战:用拓扑信息优化绑核和排查问题

4.1 超线程、同核兄弟与“表面上很近,实际上抢资源”

拓扑信息最实在的用途,是判断哪些 CPU 离得近、哪些 CPU 会抢同一组执行资源。

看thread_siblings_list,如果它是0,1,说明 CPU0 和 CPU1 是同一个物理 core 上的两个逻辑处理器。它们共享 L1/L2 缓存,也共享解码单元和大部分执行资源。意味着它们之间的线程切换延迟很低,但两个重计算线程同时绑在 0 和 1 上,吞吐未必比绑在 0 和 2 上高。

反过来,如果应用是延迟敏感的,而且两个线程之间有大量共享数据要交换,绑在同一对thread_siblings上又有好处,因为共享缓存能帮忙,减少了跨核同步开销。所以“绑核好不好”没有唯一答案,得先说清楚是追求吞吐还是追求延迟,再看拓扑信息做取舍。

core_siblings_list则帮你划定了物理 package 的边界。同一个 package 内的不同物理 core,共享的是更大的三级缓存和内存控制器路径;跨 package 则要走到更远的互连总线。尤其在多路服务器上,跨 socket 的缓存一致性流量是很贵的。

4.2 写个小脚本快速画出 CPU 分布图

我经常在排查问题时先跑一遍这个脚本,把每颗 CPU 的 package 和 core 关系打出来:

#!/bin/bash for cpu in /sys/devices/system/cpu/cpu[0-9]*; do t="$cpu/topology" [ -d "$t" ] || continue echo "${cpu##*/} package=$(cat "$t/physical_package_id") core=$(cat "$t/core_id") threads=$(cat "$t/thread_siblings_list")" done | sort -V

输出大概是这样:

cpu0 package=0 core=0 threads=0,1 cpu1 package=0 core=0 threads=0,1 cpu2 package=0 core=1 threads=2,3 cpu3 package=0 core=1 threads=2,3 cpu4 package=0 core=2 threads=4,5 cpu5 package=0 core=2 threads=4,5 cpu6 package=0 core=3 threads=6,7 cpu7 package=0 core=3 threads=6,7

再配合 NUMA 信息:

cat /sys/devices/system/node/node0/cpulist

就能知道某个线程如果要从 CPU0 迁到 CPU7,是不是跨了 NUMA 节点。像数据库、DPDK、音视频服务这类场景,如果业务允许,我通常会把一组常驻线程限制在同一个 NUMA 节点内,避免每访问一块内存都要走远程路径。

绑核用taskset就能做:

taskset -c 0,2 ./app

更细的实时线程可以用pthread_setaffinity_np,但前提是应用自己有设置亲和性的入口。不管用哪种方式,第一步都是想清楚:哪些 CPU 能作为一组,依据就是thread_siblings_list、core_siblings_list、NUMA node 的cpulist。

4.3 排查 CPU 数量对不上、拓扑信息异常

实际工作中,拓扑信息不对的场景我碰到过几种。

一种是在虚拟机里。虚拟化平台给访客呈现的 CPU 拓扑不一定是真实的物理拓扑,cpuX/topology/core_id甚至可能全部显示 0。这不一定是内核 bug,可能只是 Hypervisor 用一颗物理核模拟出了多个 vCPU。做性能分析时,不要拿虚拟机的core_siblings_list去推断宿主机真实 cache 拓扑。

另一种是 BIOS/ACPI 的赋值问题。少数双路服务器会出现physical_package_id和实际 socket 数量对不上的情况,或者core_id在某个 package 内不唯一。排查时先把dmesg里 CPU 拓扑相关的启动日志拉出来:

dmesg | grep -i "smpboot\|topology\|cpu"

再看/proc/cpuinfo里的physical id、core id、siblings、cpu cores字段。如果 sysfs 和/proc/cpuinfo对不上,大概率是启动阶段拓扑解析或 ACPI 表有问题,需要往固件方向查,而不是去改 sysfs。

容器场景还有个容易踩的坑:容器里直接挂载宿主机/sys,看到的拓扑可能是整机的,不是容器被 cpuset 限制后的拓扑。某些运行时会用 lxcfs 这类文件系统做重定向,让容器内看到的 CPU 列表和 cgroup 限制一致。所以容器内做绑核之前,先确认/sys/devices/system/cpu/online是否真的只包含允许使用的 CPU。

现象可能原因排查方向
thread_siblings_list只有单个 CPU超线程被关闭,或架构不支持lscpu看Thread(s) per core
所有core_id都为 0虚拟化拓扑不真实,或平台未提供 core 信息对比宿主环境,检查dmesg
physical_package_id大于实际 socket 数固件赋值不规范用dmidecode查物理槽位描述
容器内发现 CPU 列表和整机一致/sys未经隔离用 lxcfs 或重新挂载隔离后的 sysfs

这些坑大多不是内核代码报错,而是“sysfs 只是信息投影,不是物理事实”,数据源在固件、架构解析和虚拟化层。拓扑文件本身就是一个中间产物。

5. 学习时容易踩的坑与资料线索

5.1 千万别把 sysfs 拓扑文件当成调度器拓扑

看到“topology”这个词,很容易顺手翻到内核里的kernel/sched/topology.c,然后觉得两者是同一套东西。它们有关系,但不是一个文件。

kernel/sched/topology.c负责构造调度域(scheduling domain),根据 CPU 之间的共享关系建立层级,用于负载均衡和任务迁移决策。它会读取架构提供的 CPU mask、cache 共享关系等数据,再生成带标志位的调度域结构。而drivers/base/topology.c只是把同样来源的一部分拓扑信息用 sysfs 属性暴露给用户空间。

最直接的迷惑发生在修改 sysfs 文件时:如果尝试对thread_siblings做写操作,会收到权限报错;就算你通过特殊手段改了,也不会改变调度器的行为。因为调度器看的是内部数据,不是 sysfs 文件。学习的时候,应该把这两者分开记台账:驱动模型在这里负责“对外展示”,调度器在这里负责“内部决策”。

看代码的时候建议执行一次:

git log --oneline drivers/base/topology.c

能看到这个文件随内核演进的变化:一开始属性比较少,后来补了 die、package 相关字段,再后来调整了可见性判断逻辑。对照这个提交历史去理解“为什么现在有这些属性”,比死记硬背字段列表更有用。

5.2 建议按这个顺序读拓扑相关代码

如果之前没接触过内核拓扑,别一上来读drivers/base/topology.c。我建议的顺序是:先看文档,再看 sysfs 实测数据,再看架构层实现,最后回到base层看属性怎么挂。

文档入口是内核里的Documentation/admin-guide/cputopology.rst,内容不多,但把每个属性都解释得很清楚。然后打开真机的/sys/devices/system/cpu/cpu0/topology/,把core_id、physical_package_id、thread_siblings_list、core_siblings_list和lscpu -e对照一遍。有疑惑时再进arch/x86/kernel/topology.c或arch/arm64/kernel/topology.c,找到对应钩子函数,看目标值是启动时怎么算出来的。最后回到drivers/base/topology.c,你会发现它本身几乎不藏逻辑,真正值得琢磨的是“为什么架构层把某个数放在那个字段里”。

工具方面,hwloc里的lstopo值得一试,它会把拓扑渲染成一张图,cache、package、NUMA node 层级一目了然。图上看到某个 CPU 的位置,再回 sysfs 找对应数据,理解速度会快很多。不过要注意,lstopo默认会合并很多表示细节,和内核 sysfs 的原始字段不是一对一关系,适合做宏观概览,不适合作为唯一信息来源。

我个人读这个文件最大的感受是:topology.c本身平平无奇,真正复杂的是它背后的架构差异。它像一张挂在墙上的地图,地图不决定城市怎么建,但你要在这片城市里调度车辆、规划路线时,这张地图能帮你少走太多弯路。做性能优化前先扫一眼 CPU 拓扑,再决定绑核和内存分配策略,是我每次在新机器上部署服务时的固定动作。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询