4. 内存锁定与DMA:让数据在物理内存上扎根
各位同学,今天我们来聊聊高频交易风控系统里一个特别容易被忽视,但一旦出事就让你欲哭无泪的话题——内存管理。说白了,就是怎么让你的数据老老实实待在物理内存里,别被操作系统偷偷换到磁盘上去。
我刚开始做量化系统那会儿,就吃过这个亏。系统跑得好好的,突然一个交易信号延迟了几十微秒,查了半天,发现是内存页被换出了。嗯,从那以后,我对内存锁定这件事就特别上心。
4.1 mlockall:给内存上把锁
先说说最基础的操作——mlockall。这个系统调用的作用,就是把进程的所有虚拟内存页都锁定在物理内存中。你想想看,操作系统有个虚拟内存机制,它觉得某些内存页不常用,就会把它们换到磁盘上。这在普通应用里没问题,但在高频交易里,一次缺页中断可能就要几毫秒,黄花菜都凉了。
我个人习惯,在风控系统启动的第一时间就调用mlockall。代码很简单:
#include <sys/mman.h>
// 锁定当前和未来的所有内存页
if (mlockall(MCL_CURRENT | MCL_FUTURE) != 0) {
perror("mlockall failed");
exit(EXIT_FAILURE);
}
这里有两个标志位要注意:
MCL_CURRENT:锁定当前已分配的所有内存页MCL_FUTURE:锁定未来分配的内存页
mlockall会锁定所有内存页,包括堆、栈、代码段等。如果锁定的内存总量超过物理内存,系统可能会OOM(内存溢出)。我曾经见过一个团队,因为没控制好内存使用,直接把交易服务器搞挂了。那什么时候调用最合适?我建议在进程初始化阶段,分配完所有关键数据结构后,立即调用。这样能确保关键数据从一开始就待在物理内存里。
4.2 大页内存(HugeTLB):减少TLB缺失
接下来聊大页内存。你可能要问,为什么需要大页?
现代CPU使用TLB(Translation Lookaside Buffer)来缓存虚拟地址到物理地址的映射。默认的内存页大小是4KB,TLB条目有限。如果你的工作集很大,TLB会频繁缺失,每次都要去查页表,这开销可不小。
大页内存(HugeTLB)把页大小提升到2MB甚至1GB。这样一来,同样的TLB条目能覆盖更大的内存区域。我在项目中遇到过,使用大页后,内存访问延迟降低了约15%。
配置大页内存需要两步:
- 系统层面配置:在启动时预留大页
- 应用层面使用:通过mmap分配大页内存
先看系统配置:
# 预留1024个2MB的大页
echo 1024 > /proc/sys/vm/nr_hugepages
# 或者使用sysctl
sysctl -w vm.nr_hugepages=1024
然后在代码里使用:
#include <sys/mman.h>
// 分配大页内存
void* addr = mmap(NULL, size,
PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_HUGETLB,
-1, 0);
if (addr == MAP_FAILED) {
perror("HugeTLB mmap failed");
// 回退到普通内存
}
mmap会失败。我建议先检查/proc/meminfo里的HugePages_Total和HugePages_Free,确认有可用的大页再分配。大页内存的典型配置参数:
| 参数 | 说明 | 推荐值 |
|---|---|---|
| nr_hugepages | 预留的大页数量 | 根据内存大小,通常为物理内存的10%-20% |
| hugepagesz | 大页大小 | x86_64默认2MB,也可配置1GB |
| shmmax | 共享内存最大大小 | 至少大于大页总大小 |
4.3 DMA缓冲区锁定:数据直达内存
DMA(Direct Memory Access)是硬件设备直接读写内存的技术,不经过CPU。在高频交易里,网卡通过DMA把网络数据包直接写入内存,延迟极低。
但这里有个坑:DMA操作的是物理地址,而应用程序用的是虚拟地址。如果DMA缓冲区所在的物理页被换出,DMA操作就会写到错误的地方,数据就乱了。
所以,DMA缓冲区必须锁定在物理内存中。通常使用mlock或mlockall来实现。但更专业的做法是使用get_user_pages(内核态)或vmsplice(用户态)来固定页面。
我见过一个案例,某团队用DPDK做网络收包,但没锁定DMA缓冲区。结果系统内存压力大时,网卡DMA写到了被换出的页上,导致数据包内容错乱。排查了整整两天才发现问题。
正确的做法:
// 分配DMA缓冲区
void* dma_buf = aligned_alloc(4096, BUF_SIZE);
// 锁定缓冲区
if (mlock(dma_buf, BUF_SIZE) != 0) {
perror("mlock DMA buffer failed");
free(dma_buf);
exit(EXIT_FAILURE);
}
// 获取物理地址(需要root权限)
// 这里省略了具体实现,通常通过/proc/self/pagemap获取
dma_alloc_coherent(内核态)。4.4 避免缺页中断:预分配与预热
缺页中断(Page Fault)是性能杀手。当程序访问一个尚未加载到物理内存的虚拟页时,CPU会触发缺页中断,操作系统从磁盘加载数据。这个过程可能耗时几毫秒,在高频交易里完全不可接受。
避免缺页中断的核心策略有两个:
- 预分配:在初始化阶段一次性分配所有需要的内存
- 预热:分配后立即访问每个页面,强制加载到物理内存
预热代码示例:
// 分配大块内存
size_t page_size = sysconf(_SC_PAGESIZE);
size_t total_pages = (total_size + page_size - 1) / page_size;
void* mem = mmap(NULL, total_size,
PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS,
-1, 0);
// 预热:访问每个页的第一个字节
for (size_t i = 0; i < total_pages; i++) {
volatile char* page = (volatile char*)mem + i * page_size;
*page = 0; // 写操作触发缺页
}
这里用volatile防止编译器优化掉写操作。我习惯在预热后调用mlockall,双重保险。
知识体系总览
下面这张图展示了本章的核心逻辑:
这张图把四个核心知识点串起来了。你想想看,从mlockall锁定所有内存,到HugeTLB减少TLB缺失,再到DMA缓冲区锁定保证数据完整性,最后通过预分配和预热消除缺页中断——每一步都是在消除内存子系统的不确定性。
在高频交易里,确定性比什么都重要。宁可慢一点,也要稳一点。内存锁定就是帮你把「稳」字落到实处。
好了,这一章的内容就到这里。记住,内存管理是风控系统的地基,地基不稳,上层建筑再漂亮也没用。