numactl内存绑定中代码段的问题

在一个典型的 NUMA 架构 Linux 服务器中,我们常常使用类似

类似的命令来绑定一定进程的 memory,比如上面的例子,进程的 memory 被绑定到 NUMA1。

但是这个时候,我们用 numastat 命令去查看进程 a.out 的内存分布,很可能会发现它有少部分内存不在 NUMA1:

有极少量 0.75MB 在 NUMA0。这是不是说 numactl -m 1 没有起作用呢?瞎猜没用,眼见为实,我们来调查一下这个在 NUMA0 的内存属于进程的哪一部分。

基本上可以看出,有3个地方有位于 N0 的内存,比如:

开始地址是 0x40000 的,文件背景为 /root/a.out 的部分;

开始地址是0x7fb9afc000,文件背景为/lib/aarch64-linux-gnu/libc-2.23.so的部分;

开始地址为 0x7fb9c42000,文件背景为/lib/aarch64-linux-gnu/ld-2.23.so的部分。

如果我们进一步探究,会发现上面这三段,都是代码段:

为什么会这样呢?看起来 numactl -m 对代码段不起作用?

代码段为啥没进入指定numa?

原因其实是比较清晰的。上述代码段对应的内存,在 Linux 内核中,都属于有文件背景的页面,受 page cache 机制管理。

想象一个场景,如果 a.out 曾经运行过一次( 其实我开机后已经在没有用numactl绑定内存的情况下,运行过一次 a.out,上面的数据是第二次运行 a.out 的时候采集的 ),然后系统也加载了一些动态库,那么 a.out 本身的代码段,库的代码段可能进入到了 numa 节点 m,从而在内存命中。接下来,如果我们用 numactl -m ./a.out 去运行 a.out 并绑定 numa 节点 n,势必要再次需要 a.out 的代码段以及 a.out 依赖的动态库的代码段。但是前一次,这些代码段都进入了 page cache(位于 NUMA node m),所以第2次在numa node n 运行的时候,其实是命中了 numa node m 里面的内存。

假设我们运行4个 a.out,这4个 a.out 分别运行于4个不同的 numa,然后 a.out 依赖 a.out 的代码段、libx.so 代码段,liby.so 代码段。那么,完全有可能出现下图的情况,a.out 的代码段位于 numa0,libcx.so 代码段位于numa1,liby.so 的代码段位于 numa2,这样4份运行中的 a.out, 都各自有跨 NUMA 的代码段内存访问,这样在 icache 替换的时候,都需要跨 NUMA 访问内存。

内核为什么这样做呢?原因在于,page cache 的管理机制是以 inode 为单位的,每个page inode唯一!一个 inode(比如a.out对应的inode)的 page cache 在内存命中的情况下,内核会直接用这部分 page cache。这个 page cache,不会为每个 NUMA 单独复制一份。从 page cache 的管理角度来讲,这没有问题。

我们把前面的 a.out kill 掉,然后 drop 一次 cache,再看 a.out 的内存分布,发现在 node0 的部分减少了(0.75->0.63)

为什么呢?因为我 drop 掉部分 page cache 后(echo 3 也不可能 drop 掉全部的所有的代码段,毕竟这里面很多代码是“活跃”代码),我们再运行 a.out 并绑定 numa1 的时候,这次这些没有命中的代码段 page cache,会进入到 numa1。

如果我们重启系统,开机第一次运行 a.out 就绑定 numa1 呢?这个时候,我们会看到 a.out 的代码段在 numa1:

然后我们把 a.out kill 掉,第二次绑定 numa node0 运行 a.out,会发现这次的 a.out 的代码段还是在 numa node1 而不是 node0:

原因是它命中了第一次运行 a.out 已经进入 node1 的代码段 page cache。

初恋为什么如此刻骨铭心,你终究还是错过了那个人,而多少年以后,常常回想起来,你依然泪流满面?因为,它命中了你的 page cache。但是终究,一个人,一生可能不会只运行一次 a.out。我们终究也要学会放手,把全部的爱,献给你身边与你相濡以沫的那个人。

内存管理的改进方向

2020年8月,我在Linux内核里面提交和合入了 per-numa CMA的支持:

dma-contiguous: provide the ability to reserve per-numa CMA

https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=b7176c261cdbc

这样让每个 NUMA 里面的外设申请连续内存的时候,可以申请到本 NUMA 的近地址内存,而不用跑到远端去,从而提高 I/O的性能:

考虑到代码段以及其他 page cache 的跨 NUMA 特点,这里我想提一个可能性,就是 per-numa Page cache。内核可以支持让关键的代码段,文件背景页面,在每个NUMA单独获得一份 page cache:

它的缺点是显而易见的,page cache 可能会用多份内存。它的优点也是显而易见的,就是代码段不用跨 NUMA 了。这属于典型的以空间换时间!

这个事情行不行得通呢?技术上是行得通的,实践上,我是不敢做的,因为需要大量的benchmark,加上patch至少得发20,30个版本,前后一两年至少的。别的不说,宋牧春童鞋的省vmemmap内存的patch已经发到了22版:

[PATCH v22 0/9] Free some vmemmap pages of HugeTLB page

要是干这个 page cache 的优化,不得至少发个30版?通常这种有利于全世界,而不利于自己的 KPI 的事情,是没有多少工程师愿意投入的 :-) 细思恐极,这需要极大的耐心、投入和奉献精神。

那么,前期是不是可以从一个小点开始优化呢?我觉得是可能的。

比如 a.out 本身在 numa0 运行,kill 后再在 numa1 运行,这个时候,内核感知到 a.out 独一份,没有 share 的情况,是不是直接在内核态把 page cache 直接 migrate 到 numa1 呢?我这里还是打个嘴炮就好,把想象空间留给读者。

60+专家,13个技术领域,CSDN 《IT 人才成长路线图》重磅来袭!