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%。

配置大页内存需要两步:

  1. 系统层面配置:在启动时预留大页
  2. 应用层面使用:通过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_TotalHugePages_Free,确认有可用的大页再分配。

大页内存的典型配置参数:

参数 说明 推荐值
nr_hugepages 预留的大页数量 根据内存大小,通常为物理内存的10%-20%
hugepagesz 大页大小 x86_64默认2MB,也可配置1GB
shmmax 共享内存最大大小 至少大于大页总大小

4.3 DMA缓冲区锁定:数据直达内存

DMA(Direct Memory Access)是硬件设备直接读写内存的技术,不经过CPU。在高频交易里,网卡通过DMA把网络数据包直接写入内存,延迟极低。

但这里有个坑:DMA操作的是物理地址,而应用程序用的是虚拟地址。如果DMA缓冲区所在的物理页被换出,DMA操作就会写到错误的地方,数据就乱了。

所以,DMA缓冲区必须锁定在物理内存中。通常使用mlockmlockall来实现。但更专业的做法是使用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缓冲区不仅要锁定,还要确保物理地址连续。有些硬件要求DMA缓冲区在物理上连续,这时需要分配大块连续内存,或者使用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,双重保险。

⚠️ 注意:预热会触发大量缺页中断,但只在初始化阶段发生一次。如果系统内存不足,预热可能导致OOM。建议在系统负载低的时候做预热。

知识体系总览

下面这张图展示了本章的核心逻辑:

内存锁定与DMA:知识体系 内存锁定与DMA mlockall 大页内存 (HugeTLB) DMA缓冲区锁定 避免缺页中断 MCL_CURRENT MCL_FUTURE 系统配置 mmap分配 物理地址连续 mlock锁定 预分配 预热 目标:消除所有运行时内存延迟抖动

这张图把四个核心知识点串起来了。你想想看,从mlockall锁定所有内存,到HugeTLB减少TLB缺失,再到DMA缓冲区锁定保证数据完整性,最后通过预分配和预热消除缺页中断——每一步都是在消除内存子系统的不确定性。

在高频交易里,确定性比什么都重要。宁可慢一点,也要稳一点。内存锁定就是帮你把「稳」字落到实处。

好了,这一章的内容就到这里。记住,内存管理是风控系统的地基,地基不稳,上层建筑再漂亮也没用。


专注资料整理