第三章:操作系统内核调优——让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到内存,从内核到中断,每一层都有对应的优化手段:
3.7 本章小结
操作系统内核调优,说白了就是给高频交易程序“清场”——把CPU核心腾出来,把中断赶走,把内存绑近,把调度优先级拉满。每一步都能减少几微秒甚至几十纳秒的延迟,积少成多,就是竞争优势。
嗯,这里要注意:别一次性把所有参数都改了。我建议每次只改一个参数,用你的回测系统或者延迟测量工具(比如Solarflare的延迟统计)对比前后差异。这样你才知道哪个改动真正有效。
下一章,咱们聊聊网络栈的优化——从网卡驱动到协议栈,怎么把网络延迟从几十微秒压到个位数微秒。