第三章:操作系统内核调优——让Linux为高频交易“让路”

做高频交易的人都知道,硬件再快,如果操作系统不配合,延迟照样下不去。我见过不少团队,花大价钱买了FPGA网卡、低延迟交换机,结果一查系统,内核参数还是默认的——那感觉就像开着法拉利在泥巴路上跑。

这一章,咱们就聊聊怎么把Linux内核“掰”成适合高频交易的样子。说白了,就是让CPU专心算你的策略,别被乱七八糟的中断、调度、内存分配给打扰了。

核心目标:将操作系统引入的延迟抖动控制在微秒级以下,甚至纳秒级。

3.1 CPU隔离:把核心“私有化”

默认情况下,Linux的调度器会把进程分配到任意CPU核心上。这对普通应用没问题,但对高频交易来说,这就是灾难。你想啊,你的交易线程正在CPU0上跑,突然系统把网卡中断也扔到CPU0上——你的线程就得等着,延迟瞬间飙升。

解决方案:isolcpus 内核参数把某些核心隔离出来,专门跑你的交易线程。

我个人习惯在 /etc/default/grub 里这样配:

GRUB_CMDLINE_LINUX="isolcpus=2,3,4,5 nohz_full=2,3,4,5 rcu_nocbs=2,3,4,5"

这里我把CPU 2-5隔离了。系统调度器不会把普通进程扔到这些核心上。但注意,隔离不等于禁用——你的交易进程还是可以手动绑定到这些核心上跑的。

避坑指南:我曾经在某个项目里只配了 isolcpus,忘了配 nohz_full,结果发现隔离的核心还在每秒几百次地接收时钟中断。后来加上 nohz_full 才彻底安静下来。

3.2 中断亲和性:让中断“绕道走”

CPU隔离只是第一步。网卡、磁盘、定时器的中断,默认还是会随机分配到所有核心上。你得手动把这些中断“赶”到非隔离的核心上去。

具体做法是修改 /proc/irq/{IRQ号}/smp_affinity。比如,把网卡中断绑定到CPU0和CPU1:

echo "03" > /proc/irq/130/smp_affinity

这里的 03 是十六进制掩码,表示CPU0和CPU1(bit0和bit1置1)。

我一般写个脚本,在系统启动时自动设置:

#!/bin/bash
# 把所有非隔离核心的中断都移到CPU0-1
for irq in $(ls /proc/irq/); do
    if [ -f "/proc/irq/$irq/smp_affinity" ]; then
        echo "03" > /proc/irq/$irq/smp_affinity 2>/dev/null
    fi
done

注意:别把关键中断(比如时钟中断)全绑到一个核心上,否则那个核心会过载。我建议把中断分散到2-3个非隔离核心上。

3.3 内存大页:减少TLB缺失

高频交易程序通常需要锁定大量内存(防止被换出)。默认的4KB内存页,意味着每1GB内存需要26万个页表项。TLB(页表缓存)根本装不下,每次访问内存都可能触发TLB缺失,延迟增加几十纳秒。

大页(HugePages) 把页大小提升到2MB甚至1GB。1GB内存只需要512个2MB页,TLB命中率大幅提升。

配置方法:

# 预留1024个2MB大页
echo 1024 > /proc/sys/vm/nr_hugepages

# 或者用sysctl永久配置
echo "vm.nr_hugepages=1024" >> /etc/sysctl.conf

然后在程序里用 mmap 加上 MAP_HUGETLB 标志来分配大页内存:

void *buf = mmap(NULL, size, PROT_READ | PROT_WRITE,
                 MAP_PRIVATE | MAP_ANONYMOUS | MAP_HUGETLB, -1, 0);

我的经验:大页不是越大越好。1GB大页虽然TLB命中率最高,但分配粒度太粗,容易浪费内存。我一般用2MB大页,配合内存池管理,效果最好。

3.4 实时内核(RT-Preempt):让线程“说到就到”

标准Linux内核的调度延迟,通常在几十微秒到几毫秒之间。这对高频交易来说太不可控了。RT-Preempt补丁把内核改造成完全可抢占的,调度延迟能降到10微秒以内。

安装RT内核(以Ubuntu为例):

apt-get install linux-image-rt-amd64

重启后选RT内核启动。然后你可以用 chrt 命令把你的交易线程设为实时优先级:

chrt -f 99 ./your_trading_program

-f 99 表示FIFO调度策略,优先级99(最高)。这意味着只要你的线程就绪,内核会立即抢占其他所有普通进程来运行它。

警告:实时优先级不是越高越好。我曾经把某个监控线程也设成99,结果它和交易线程抢CPU,导致交易线程被阻塞。建议只给核心交易线程设最高优先级,其他辅助线程设低一些。

3.5 NUMA架构优化:让数据“就近”访问

现代服务器都是NUMA架构——每个CPU有自己的本地内存,访问本地内存快,访问远端内存慢(延迟可能差1.5-2倍)。

高频交易程序如果跨NUMA节点访问内存,延迟会不稳定。我建议用 numactl 把进程绑定到单个NUMA节点上:

numactl --cpunodebind=0 --membind=0 ./your_trading_program

这表示进程只在NUMA节点0的CPU上运行,并且只从节点0的内存分配内存。

你也可以在代码里用 libnuma 的API来精细控制:

#include <numa.h>

// 只允许在节点0分配内存
struct bitmask *mask = numa_allocate_nodemask();
numa_bitmask_setbit(mask, 0);
numa_set_membind(mask);
numa_free_nodemask(mask);

避坑指南:我曾经遇到一个坑——程序启动时绑定了NUMA节点0,但后来动态分配的内存却落到了节点1上。原因是 numa_set_membind 只影响调用之后的分配,之前分配的内存不会移动。所以一定要在程序最开头就设置好内存绑定。

3.6 知识体系总览

下面这张图,把本章的核心知识点串起来了。你可以看到,从CPU到内存,从内核到中断,每一层都有对应的优化手段:

Linux内核调优知识体系 高频交易内核优化 CPU隔离 isolcpus / nohz_full 中断亲和性 smp_affinity / 中断绑定 内存大页 HugePages / MAP_HUGETLB RT-Preempt内核 实时调度 / chrt NUMA架构优化 numactl / 内存绑定 目标:将延迟抖动控制在微秒级以下 CPU专注 + 中断绕道 + 内存就近 + 调度实时

3.7 本章小结

操作系统内核调优,说白了就是给高频交易程序“清场”——把CPU核心腾出来,把中断赶走,把内存绑近,把调度优先级拉满。每一步都能减少几微秒甚至几十纳秒的延迟,积少成多,就是竞争优势。

嗯,这里要注意:别一次性把所有参数都改了。我建议每次只改一个参数,用你的回测系统或者延迟测量工具(比如Solarflare的延迟统计)对比前后差异。这样你才知道哪个改动真正有效。

下一章,咱们聊聊网络栈的优化——从网卡驱动到协议栈,怎么把网络延迟从几十微秒压到个位数微秒。


专注资料整理