一台刚上架的机器,lspci里明明白白躺着一个 class 为 0502 的内存控制器设备,dmesg也打了几行 CXL 的探测日志,可free -g一点没变,numactl -H还是那两个老节点,/proc/iomem里也找不到那块地址窗口——这种"硬件在位、内核也在、但内存没被装配起来"的状态,是我接触 CXL 内存扩展时遇到的第一个坎。问题不在驱动,而在于 CXL 的内存必须经过一层显式的"组卷"动作:把端点设备的容量、解码器的地址窗口、根端口的内存窗口按交织规则拼起来,才会变成一个能被上层使用的东西。这套活儿在 Linux 上就靠三件工具接力完成:cxl-cli负责把裸设备和解码器拼成 region,daxctl负责把 region 落成 device-dax 并按需切换成系统内存或独占设备,ndctl负责更传统的 NVDIMM/pmem 那条线——建命名空间、配 fsdax/devdax、管标签。
这篇内容面向的是手上有 CXL Type-3 内存设备、准备在 Linux 上把它跑起来的人:不管你是要给数据库加一层大容量池、还是想拿它做内存分层实验,或者只是接手了一台别人配了一半的机器需要收拾残局。下面这套流程是我自己在几台机器上反复走过、也踩过一圈坑之后整理出来的路子,命令都是能直接抄的,但我会把"为什么这么写"和"这么写错在哪"一并说清,因为这套东西最大的特点就是:命令本身很短,参数背后的约束很长。
1. 先把设备链路摸清:CXL 内存在内核里到底长成了什么
1.1 四个身份:memdev、decoder、region、namespace
很多人第一次cxl list会晕,是因为同一块物理内存设备在内核里同时存在四种形态,名字还都长得差不多。理顺它们的关系,后面所有命令就都能对上号了。
最底层是memdev,出现在/sys/bus/cxl/devices/mem0这种位置,代表一个 CXL Type-3 内存设备的邮箱接口和容量信息。cxl list -M列的就是它,能看到ram_size(易失容量)和pmem_size(持久容量)两块。注意这两个容量是设备分区策略决定的,不是你想改就改,后面排错章节会专门讲这个坑。
memdev 上面挂着一串decoder,端点上叫mem0/decoder0.0之类,根端口那边还有一个root decoder(通常在root0/decoder0.0)。解码器本质上是"地址翻译规则的载体":它决定了这一段内存映射到主机物理地址的哪一段、交织粒度多大、几路交织。没有解码器被编程,设备容量再大也只是挂在总线上的一坨东西。
解码器配置完成并绑定之后才会出现region,也就是region0。region 是"一段已经被分配了主机地址窗口的 CXL 内存",到这一步它才开始有资格被上层驱动接管。
region 之后分叉:如果 region 是 ram 类型,内核会生成dax_region0并自动派生一个dax0.0设备,落到/dev/dax0.0,后面交给daxctl;如果 region 是 pmem 类型,它会走 libnvdimm 那条老路,在/sys/bus/nd/devices/下出现一个新 region,再由ndctl建namespace,最终变成/dev/pmem0。
所以整条链路是:设备 → memdev → decoder 编程 → region → dax 设备或 nd 命名空间。你在哪一步卡住,报错信息的样子是很好认的。
1.2 为什么非得有 decoder 这一层
刚接触的人常问:内存条插上就能用,凭什么 CXL 内存还要我手动配一遍?这个疑问很合理,但 CXL 的定位和 DRAM 完全不是一回事。
DRAM 挂在内存控制器上,地址是固定的、容量是枚举出来的,属于"平台怎么焊就怎么用"。CXL 内存是走串行链路的外部设备,一块设备可能被多台主机共享,也可能是几块设备交织成一个大条带,还可能只把一部分容量划给这台机器用。这些可能性必须有地方表达,于是就有了 decoder:它把"设备内部地址"(DPA)翻译成"主机物理地址"(HPA)。翻译规则包括窗口起点、窗口大小、交织的粒度(granularity)和交织的路数(ways)。
交织这事值得多说一句。单块 CXL 设备的链路带宽是有限的,想要带宽叠加,就得让相邻的地址块轮流落到不同设备上——比如 2 路、粒度为 256 字节,那 0-255 字节在设备 A,256-511 在设备 B,如此交替。读一大片连续内存时,硬件会同时从两块设备上取数,有效带宽接近叠加。代价是:交织路数必须和参与设备数量严格匹配,粒度必须落在所有设备都支持的值上。这就是为什么create-region里的-w和-g不是随便填的——填错了不是性能差一点,而是直接创建失败。
1.3 三件工具的分工边界
这三件工具的边界很容易混,我见过有人拿ndctl去管 CXL 的 dax 设备,然后对着"没找到设备"的报错发呆。下面这张表是我自己贴在笔记里的版本:
| 工具 | 主要管什么 | 典型对象 | 常见动作 |
|---|---|---|---|
cxl-cli | CXL 拓扑与 region 组卷 | memdev、port、decoder、region | 列拓扑、建/毁 region、开关端口 |
daxctl | device-dax 设备的形态切换 | dax0.0这类 dax 设备 | 切 system-ram / devdax、上线/下线内存 |
ndctl | NVDIMM 与 pmem 命名空间 | nd region、namespace、/dev/pmemX | 建/毁 namespace、设 fsdax、管标签 |
要强调的是:cxl-cli和daxctl、ndctl其实是同一个上游项目的不同前端,装的时候通常一起装,版本号也是同一个。所以"我 ndctl 是新的、cxl 是旧的"这种情况基本不会出现,但版本整体偏旧会非常常见,这一点在下一节会讲。
2. 动手前的核对:内核配置、固件窗口和工具版本
2.1 内核配置与 /sys/bus/cxl 的落地检查
第一件事不是敲命令,而是确认这个内核到底有没有把 CXL 编进去。判断方法很简单:
ls /sys/bus/cxl/devices/ 2>/dev/null || echo "CXL 总线没起来" dmesg | grep -i -E "cxl|cedt" | head -30如果/sys/bus/cxl/devices/里能看到root0、若干portX、memX,说明 CXL 核心、端口驱动、内存设备驱动都到位了。如果目录存在但里面只有root0没有memX,那多半是设备侧没被识别,往下要看 PCIe 层。
内核配置上需要关注这些项(不同版本名字略有出入,用grep在/boot/config-$(uname -r)里对):
grep -E "CONFIG_CXL|CONFIG_DEV_DAX|CONFIG_NVDIMM|CONFIG_ZONE_DEVICE|CONFIG_MEMORY_HOTPLUG|CONFIG_MEMORY_HOTREMOVE|CONFIG_NUMA" /boot/config-$(uname -r)关键的是CONFIG_CXL_BUS、CONFIG_CXL_PCI、CONFIG_CXL_ACPI、CONFIG_CXL_MEM、CONFIG_CXL_PORT、CONFIG_CXL_REGION,以及CONFIG_DEV_DAX、CONFIG_ZONE_DEVICE、CONFIG_MEMORY_HOTPLUG。少了CONFIG_CXL_REGION,你会看到 memdev 正常但create-region直接报不支持——这个错误提示相当不友好,我第一次遇到时以为是参数问题,折腾了半天才发现是内核没开。
PCIe 层确认设备在不在:
lspci -nn | grep -i "0502" lspci -vvv -s <上面的 BDF> | grep -i -A5 cxlType-3 内存设备通常枚举成 class 0502 的内存控制器。lspci -vvv里能看到 CXL 相关的扩展能力(DVSEC)说明,以及链路的协商速率和宽度——顺手记一下这两项,后面评估带宽要用。
提示:如果
lspci里根本没有 0502 设备,先别怀疑 Linux。多半是固件里 CXL 相关选项没开、或者设备没被枚举上。这一步查不通,后面所有操作都是空谈。
2.2 固件里的 CEDT 与 CFMWS 决定了你能建几路交织
CXL 在 ACPI 里靠 CEDT 表描述拓扑,其中最重要的子结构叫CFMWS(固定内存窗口)。它告诉操作系统:"在这段主机物理地址区间里,可能长出 CXL 内存,交织路数是 N,粒度是 M。"内核读这张表,据此生成 root decoder。
查表的方式:
ls /sys/firmware/acpi/tables/ | grep -i cedt cat /sys/firmware/acpi/tables/CEDT > /tmp/cedt.bin && ls -l /tmp/cedt.bin表在不在是一回事,窗口配得对不对是另一回事。我遇到过最典型的情况是:固件只留了一个ways=1的窗口,机器上却插了两块设备,于是一块被正常建了 region,另一块怎么都建不起来——因为剩下的窗口容量不够,或者交织路数对不上。此时cxl list -D能看到 root decoder 的interleave_ways和剩余空间,一眼就能确认是不是这个原因。
另一个常见设置项是设备侧的容量分区(volatile / persistent 各占多少)。有些设备出厂默认全划给易失容量,pmem_size是 0,那你走 pmem 路线一定失败。改分区要么在固件里改,要么用工具改,但改分区通常要求设备上没有任何活动 region,顺序搞反了就是一连串-EBUSY。
2.3 工具版本决定了你能用哪些子命令
cxl子命令不是一开始就齐的。早期版本只能list,看个拓扑;region 创建能力是后来才补齐的,而且参数名在不同版本之间有过调整。所以拿到一台机器,第一件事是报版本:
ndctl --version daxctl --version cxl --version cxl create-region --help--help的输出比版本号更有价值,因为它直接告诉你这个版本支持哪些参数。我个人的经验是:如果你的cxl create-region --help里没有-t/--type之类的类型选项,那这个版本大概率只能建默认类型的 region,走 pmem 路线会有障碍。这时候要么升级工具(注意要和内核版本配套,太新的工具配太老的内核一样用不了),要么换思路走 ram 路线。
工具和内核的配套关系很值得强调一次:region 相关的能力是内核和用户态两边配合的产物。内核提供了create-region需要的 sysfs 接口,用户态才有得可用。单升级一边往往没用,会出现"命令能跑但立刻返回 EINVAL"的诡异现象。
3. 用 cxl-cli 把裸设备拼成一个可用区域
3.1 从 cxl list 的几种视图读懂拓扑
我习惯按"从下往上"的顺序看,一共三条命令:
cxl list -M -v # 内存设备:容量、序列号、分区情况 cxl list -P -D -M # 端口 → 解码器 → 设备的树状关系 cxl list -R -v # 已经建好的 regioncxl list -M -v的输出里,我重点关注三个字段:ram_size、pmem_size和设备的物理位置。前者决定了我能走哪条路线,后者决定了交织时哪些设备能配成一组——同一 root port 下的设备交织效率更高,跨端口交织也行,但要确认中间各级 port 的 decoder 都支持所需路数。
cxl list -P -D -M用来确认"路径是否通畅"。CXL 的端口层级可能很深:root → switch 的上游口 → 下游口 → 设备。每级端口都有自己的 decoder,region 创建时逐级都要能容纳这个窗口。如果某级 port 是 disable 状态,或者某个 decoder 已经被别的 region 占用了,这里能直接看出来。
cxl list -R -v是查已建 region 的:起点地址、大小、交织方式、目标设备列表。当一台机器被别人配过一半,这条命令能救你——先看清已经存在什么,再决定是复用还是推倒重来。
3.2 create-region 的参数是怎么算出来的
单设备、单路交织是最简单的场景,也是我第一次跑通时用的:
cxl create-region -d decoder0.0 -m mem0 -w 1这里-d指的是 root decoder,不是端点 decoder——这一点极其容易搞错,我当初就写成mem0/decoder0.0报了一堆错。-m是参与交织的 memdev 列表。-w 1是交织路数,必须等于-m后面设备的数量,也必须和 root decoder 支持的路数兼容。
如果是两块设备做一个 2 路交织:
cxl create-region -d decoder0.0 -m mem0,mem1 -w 2 -g 256 -s 64G参数含义拆开讲:
-g 256:交织粒度 256 字节,这是 CXL 规范推荐值,也是绝大多数固件窗口配的值。填错会直接失败,因为粒度必须落在 CFMWS 声明的范围内。-s 64G:region 总大小。注意这里有个容易翻车的点——总大小要能被"路数 × 粒度"整除,而且每块设备分到的部分不能超过它的可用容量。-w 2:两路。如果两块设备容量不一致,以小的为准,多出来的部分会闲置。
不写-s的时候,工具会尽量用满可用空间,但"尽量"的逻辑在不同版本里不太一样,所以我个人的习惯是永远显式写大小。这样即使失败,报错也能定位到具体是容量不匹配还是对齐不匹配。
关于区型(ram / pmem),不同版本写法有差异,有的是-t ram,有的是别的形态,甚至有的版本直接根据端点 decoder 的状态推断。所以这一步建议先--help对一下,别照抄网上抄来的参数表。设备支持持久容量、你也确实要 pmem 语义时,才去折腾类型选择;只是想把它当普通内存用,默认走 ram 最省事。
3.3 建完之后立刻验证的三件事
region 建完不等于能用,我一般立刻做三步确认。
第一步,看 region 是否真的 bind 上了驱动:
cxl list -R -v ls /sys/bus/cxl/devices/ | grep -E "region|dax"期望结果是能看到region0,以及配套的dax_region0。如果只有region0没有 dax,说明 region 建出来了但没有驱动接住——最常见的原因是内核没开CONFIG_DEV_DAX,或者 region 类型和预期不一致。
第二步,确认块设备/字符设备节点出来了:
ls -l /dev/dax* daxctl list -D -Rdaxctl list会告诉你 dax 设备的size、mode(初始一般是devdax)、chardev路径。到这里,这块内存已经可以被应用程序以 mmap 方式直接使用了,只不过还没进内核的页分配器。
第三步,看资源树对不对:
cat /proc/iomem | grep -i -A2 -B2 "CXL\|Persistent\|reserved" dmesg | tail -40确认 region 的物理地址窗口和 CFMWS 声明的区间对得上。如果地址落在窗口外面,说明 root decoder 选择错了——装了多路设备或者多个窗口的机器上,这情况不算罕见。
4. 两条落地路线:ram 模式走 daxctl,pmem 模式走 ndctl
4.1 system-ram 路线:把 CXL 内存变成一块 NUMA 节点
如果目标是"让系统多一块内存",那daxctl是主力。核心命令就两条,但中间的顺序很重要:
daxctl reconfigure-device --mode=system-ram --no-online dax0.0 daxctl online-memory dax0.0第一条把 dax 设备从独占模式切成系统内存模式,--no-online的意思是"先挂上,但别急着把内存段上线"。第二条才是真正把内存段上线到页分配器。
为什么要分两步?因为上线方式直接影响这块内存能被怎么用。热插拔内存在内核里可以按"可迁移"或"不可迁移"两类区段上线,前者进ZONE_MOVABLE,只能放用户页、能迁移、不能被内核直接长期占用;后者进普通区,内核数据结构、slab、页表都能往上放,但一旦放上去就很难回收,热拔的时候会失败。
我的经验是:CXL 内存默认按可迁移方式上线,这是更稳妥的选择。原因很实在——CXL 设备会故障、会热拔、会被重新分区,如果内核的不可迁移对象趴在上面,回收时会非常痛苦,甚至只能重启。代价是可迁移区不能承接 slab 和页缓存的一部分内容,某些场景下利用率不如普通区。
具体命令在版本间有差异,有的版本online-memory直接按可迁移上线,有的版本提供了切换参数。所以先daxctl online-memory --help看一眼,别硬抄。
上线完成后验证:
numactl -H cat /proc/zoneinfo | grep -E "^Node|pages free|managed" daxctl list -Dnumactl -H里应该多出一个节点,容量对得上你的 region 大小,距离值(distance)通常比本地节点大——这个距离值决定了内核默认的分配倾向。想确认这块内存属于哪个节点:
cat /sys/bus/dax/devices/dax0.0/target_node拿到节点号之后,就能用numactl --membind=N或mbind()把进程绑上去。
这里必须说个反直觉的点:加了内存不等于应用会用上。内核的 NUMA 平衡策略倾向把页放在离 CPU 近的本地节点,CXL 节点距离大,只有在本地内存吃紧、或者你显式绑定时才会真正吃到量。很多人加完内存看着free涨了,但业务指标一点没动,原因就在这。
4.2 devdax 路线:把地址空间的支配权交给应用
另一条路是把 dax 设备保持devdax形态,应用程序直接 mmap/dev/dax0.0,自己做地址管理。《daxctl` 在这里负责的是"对齐"和"模式确认":
daxctl reconfigure-device --mode=devdax --map=dev --align=2M dax0.0 daxctl list -D--map=dev表示这段内存的元数据(struct page)随设备常驻,--align=2M是最小对齐要求——不给够对齐,命令会直接拒绝,因为 device-dax 对映射粒度有硬性限制,这是页表层面的事,不是工具刁难人。
devdax 路线的适用场景很明确:你要自己管内存池、要绕过内核页分配器、要极低的管理开销。代价是所有事情都自己来——没有 swap、没有 page cache、进程崩了内存也不会自动回收,错误处理全在应用侧。我见过有人拿它当普通内存用,然后抱怨"内存泄漏了检查不出来",这就是选错路线了。
需要切回系统内存时,前提是这块设备上没有任何映射:
daxctl reconfigure-device --mode=system-ram --no-online dax0.0如果报设备忙,基本就是还有进程开着那个字符设备,lsof /dev/dax0.0或者fuser一查便知。
4.3 pmem 路线:region 之后还要过 ndctl 这一关
走持久内存语义时,region 建成之后会出现一个 nd 层的 region,接下来交给ndctl:
ndctl list -RN ndctl create-namespace -r region0 -m fsdax -a 4k -s 32G ndctl list -N -v-m fsdax是给文件系统用的直接访问模式,-a 4k是对齐粒度(fsdax 至少要等于页大小),-s是命名空间大小,不写就占满 region。
建完之后是/dev/pmem0,可以格式化和挂载。要真正吃到 DAX 的好处,文件系统和挂载参数都得对:
mkfs.ext4 -b 4096 /dev/pmem0 mount -o dax=always /dev/pmem0 /mnt/cxlpmem块大小必须是 4K(x86 上的页大小),否则 fsdax 无法生效。挂载时用dax=always让所有文件走直接访问路径;日常调试时也可以只对特定文件开dax属性,这样能对比出效果差异。
需要独占设备给应用时,命名空间模式换成 devdax:
ndctl destroy-namespace -f namespace0.0 ndctl create-namespace -r region0 -m devdax -a 2M这里-a 2M和上面daxctl里的--align是一回事。用错对齐会报一个"alignment 不满足"的错误,提示信息里其实写清楚了需要多少,只是很容易被忽略。
另外补充一个细节:namespace 建好之后设备节点名带后缀(比如/dev/pmem0.0或者/dev/pmem0s)的情况,取决于模式和命名空间划分,别看到名字和预期不一样就以为出问题了,ndctl list -N -v里的chardev/blockdev字段才是准的。
5. 排错现场:几种能把人卡一整天的报错
5.1 create-region 报 EBUSY:固件已经替你建过了
这个坑我踩得最深。机器一上电,某些固件会按预设策略"自动建 region",把设备容量直接映射进系统。此时你再手动执行create-region,端点 decoder 已经被占用,直接返回-EBUSY。
正确的处理顺序是:先看已有什么,再决定怎么办。
cxl list -R -v cxl list -D -m mem0如果cxl list -R里已经有一个 region 覆盖了你要用的设备,那就不要重复建。要么直接用这个 region(看它的类型是否合你意),要么先把它拆掉:
cxl destroy-region -f region0拆之前务必确认上面没有挂载的文件系统、没有开着的 dax 设备,否则拆到一半失败,状态会更乱。
顺带一提,自动建 region 的机器往往也把 region 类型定死了。我曾经遇到一台机器自动建的是 pmem region,但我需要的是普通内存,最后只能拆掉重建。所以拿到机器第一件事就是cxl list -R,别急着敲 create。
5.2 interleave 不匹配:ways 和窗口对不上
报错形态通常是-EINVAL加一句关于 interleave 的描述。可能的原因有三个,逐个查:
首先,-w的值和-m后面设备数量不一致。这个最容易发现,也最容易手滑——写了-w 2却只给了一个 memdev。
其次,-w和 root decoder 支持的路数不兼容。查:
cxl list -D -p root0 -v输出里的interleave_ways是固件窗口定死的能力。窗口是 1 路,你就不能建 2 路交织——这不是软件限制,是硬件地址窗口本身的属性。遇到这种情况只有换窗口(如果固件提供了多个 CFMWS)或者在固件里改配置。
第三,中间的端口 decoder 容量或路数不够。跨 switch 交织时,每一级 port 的 decoder 都要能容纳整个窗口。查cxl list -P -D -v逐级看容量,哪个不够一目了然。
5.3 容量对不上:region 大小被悄悄截断
有时候 region 建成功了,但大小比你预期的小。原因通常是:root decoder 的剩余容量不够,或者某块 memdev 的可用容量比另一块小,交织后取最小值。
排查方法:
cxl list -D -v | grep -E "decoder|size|max_available" cxl list -R -v我会把 root decoder 的max_available_size和每块 memdev 的 decodersize都抄出来,做一次简单的算术:region 大小 ≤ 参与者最小容量 × 路数,且 ≤ root decoder 可用容量,且要能被"路数 × 粒度"整除。四个条件里只要有一个不满足,要么失败要么缩水。
还有一个隐蔽的情况:设备的分区策略。设备的ram_size和pmem_size加起来才等于总容量,但某个 region 只能使用其中一种。如果你看到"设备有 512G,但只能建出 256G 的 region",去查设备的分区设置,八成就明白了。
5.4 内存上线了但行为不对:节点亲和与区段类型
症状是numactl -H里节点有了,容量也对,但应用性能没变化,或者申请大内存时依然 OOM。
先确认区段类型:
cat /proc/zoneinfo | grep -E "^Node|^ pages free|managed"如果新节点的空闲页全在Movable区、普通区是 0,那内核的不可迁移分配(slab、页表、内核栈)一点都不会落到这块内存上。这意味着它只能承接匿名页和文件页的一部分,对某些场景来说利用率会明显低于预期。
再确认亲和:
numactl -H cat /sys/bus/dax/devices/dax0.0/target_node numastat -p <pid>numastat能告诉你目标进程实际把页放在哪。如果N0一大堆、N1几乎为零,那就是没绑上去,或者绑了但内核平衡又迁回去了。
修正办法分两种:显式绑定(numactl --membind=N、mbind、set_mempolicy),或者调整节点距离让内核更愿意往上放。后者要动 ACPI 里的 HMAT 距离表,属于固件层面的活儿,我不建议在没有明确需求时碰。绝大多数场景下显式绑定就够了,也更可控。
6. 性能与运维上的实话:CXL 内存不是 DRAM 的替代品
6.1 延迟和带宽的量级感
先给个数:本机 DRAM 的访问延迟通常在 80~100 纳秒这个量级,CXL 内存经过链路和协议转换之后,实测多在 150~250 纳秒区间,具体看跳数、链路速率和设备实现。带宽方面,单设备 x16 链路跑出来的有效值大致在 30~40 GB/s,交织多块能叠加,但单位带宽的成本和能耗跟本机内存通道不是一个算法。
这个差距意味着什么?意味着把热数据放到 CXL 上一定是亏的。它的价值在于容量和成本的比值:那些访问频率低但占地方的数据(冷分区、归档、备份缓冲、训练数据集的边角料),放在 CXL 上比放 DRAM 省得多,比放本地盘又快到不知道哪里去了。
我自己的判断标准很粗暴:如果一份数据的访问间隔在秒级以上,放 CXL 基本无感;如果访问间隔在毫秒级,就得实测;如果是每请求都要碰的热点,别放。数据库的缓冲池扩容是典型的"看着很香、实际要小心"的场景——扩大到 CXL 上之后,命中率上去了,但每次 miss 的代价也上去了,最终的 QPS 可能不升反降。这类场景一定要做 A/B,别拍脑袋。
6.2 上线顺序和开机脚本
CXL 的配置默认不持久。重启之后,region 可能没了,dax 设备可能回到 devdax 模式,命名空间可能还在也可能不在,具体行为取决于内核和固件。指望"配一次管一辈子"是不现实的。
我的做法是写一个开机脚本,按固定顺序执行,并且每一步都做幂等判断:
#!/bin/bash set -e # 1. 已有 region 就跳过,避免 EBUSY if ! cxl list -R -v | grep -q '"region"'; then cxl create-region -d decoder0.0 -m mem0 -w 1 -g 256 -s 64G fi # 2. 等 udev 把 dax 节点创建出来 for i in $(seq 1 20); do [ -e /dev/dax0.0 ] && break sleep 0.5 done # 3. 切成系统内存,先不上线 daxctl reconfigure-device --mode=system-ram --no-online dax0.0 || true # 4. 上线 daxctl online-memory dax0.0几个注意点:set -e配合|| true是为了让"已经切过了"这种情况不中断脚本;等待/dev/dax0.0的循环不能省,因为 udev 建节点有延迟,脚本跑太快会找不到设备;region 创建的幂等判断不要用退出码,用cxl list的输出判断更可靠。
如果要做成开机自启,别放在 initramfs 阶段——那个阶段 CXL 的 ACPI 解析可能还没走完。放到多用户目标之后比较稳妥。另外这类脚本建议加日志,写清每一步的返回值,出问题的时候能省掉大量排查时间。
6.3 监控要点与容量回收
长期跑起来之后,我最关注的三个指标:
一是设备错误计数,走cxl list -M -v或者直接看对应 sysfs 节点的错误统计。CXL 链路的可纠正错误会累积,累积到一定量就该关注了,别等到不可纠正错误把整块内存打掉。
二是内存段的实际使用情况,numastat -m按节点看。如果新节点长期空着,说明绑没生效,或者策略被覆盖了,属于白配。
三是内核的回收压力。如果 CXL 内存按可迁移区上线,当系统内存吃紧时,内核会把可迁移页往回收——也就是说这块内存在压力下"能顶一阵但顶不长久"。这一点和真内存有本质区别,做容量规划时别把它当等价容量算。
顺带提一个可以玩的接口:较新内核里有内存分层的抽象,/sys/devices/virtual/memory_tiering/下面能看到层级划分,配合内核的页迁移机制,可以在不同层级之间按热度搬页。这个方向比手工绑定更自动化,但也更依赖内核版本,早期版本里这块不完整甚至没有,用之前先确认目录存不存在。
最后分享一个我踩过的小坑:daxctl reconfigure-device切回devdax时,如果之前没有把内存段下线,命令会静默地只做一半——设备节点变回字符设备了,但内核里那段内存还没完全回收,后续再想上线就会报资源冲突。稳妥的顺序永远是:先daxctl offline-memory,确认numactl -H里节点容量归零,再切模式。这个顺序多花十秒,能省下后面半小时的困惑。