第四章:操作系统调优——Linux内核参数调优、CPU亲和性与隔离、内存与中断优化
各位同学,欢迎来到第四章。
上一章我们把网络协议栈和网卡驱动折腾得差不多了,但你以为这样就能跑高频交易了?太天真了。操作系统本身,才是最大的「猪队友」。你想想看,Linux 默认是为通用场景设计的——它要照顾数据库、Web 服务器、桌面用户。但咱们做高频交易,要的是极致的低延迟和确定性。
所以这一章,咱们就来给 Linux 动手术。把那些花里胡哨的「公平调度」、「节能降频」、「内存换页」统统关掉,或者调到最激进的状态。
我个人习惯,每搭建一套新的交易系统,第一件事就是跑一遍这套调优脚本。不调优,你连 10 微秒的延迟都稳不住。
4.1 内核参数调优:把「公平」扔进垃圾桶
Linux 内核默认是「老好人」。它希望每个进程都能分到 CPU,每个网络包都能被温柔对待。但咱们要的是「特权阶级」——交易进程必须插队,必须独占资源。
4.1.1 网络栈参数:让数据包飞得更快
先看几个最关键的 net.core 参数。我在项目中遇到过,默认的 socket 缓冲区太小,导致高频行情数据被丢弃,回测时发现少了好几个 tick,差点把策略逻辑搞错。
# 最大 socket 读缓冲区 (字节)
net.core.rmem_max = 134217728
# 最大 socket 写缓冲区 (字节)
net.core.wmem_max = 134217728
# 默认的 TCP 读写缓冲区 (字节)
net.ipv4.tcp_rmem = 4096 87380 134217728
net.ipv4.tcp_wmem = 4096 65536 134217728
# 关闭 TCP 时间戳,减少 CPU 开销
net.ipv4.tcp_timestamps = 0
# 关闭 TCP SACK,避免重传时的复杂计算
net.ipv4.tcp_sack = 0
# 关闭 TCP 窗口缩放,减少头部开销
net.ipv4.tcp_window_scaling = 0
嗯,这里要注意:tcp_timestamps 和 tcp_sack 关闭后,会降低长肥网络下的吞吐量。但咱们做的是局域网高频交易,延迟比吞吐量重要一万倍。所以放心关。
4.1.2 虚拟内存参数:别让交易进程「卡住」
Linux 有个坏毛病——内存紧张时,它会触发 swap,把进程的部分内存换到磁盘上。这对高频交易是致命的。一次缺页中断,可能让你错过一个 tick,损失几十万。
我曾经在一次实盘环境中,发现交易进程偶尔会「卡顿」几十毫秒。查了半天,发现是 swap 在作祟。从那以后,我所有交易服务器都强制关闭 swap。
# 关闭 swap
vm.swappiness = 0
# 减少脏页刷新的频率,避免突然的 I/O 抖动
vm.dirty_background_ratio = 5
vm.dirty_ratio = 10
# 允许的最大内存映射数量
vm.max_map_count = 655360
vm.swappiness = 0 的意思是:除非内存彻底用尽,否则绝不使用 swap。但注意,这并不能完全禁用 swap,你还需要在系统层面 swapoff -a。
4.1.3 调度器与内核抢占
默认的 CFS(完全公平调度器)对高频交易来说太「温柔」了。我们需要的是实时调度策略,让交易进程拥有最高优先级。
# 内核抢占模式:低延迟
kernel.sched_latency_ns = 100000
kernel.sched_min_granularity_ns = 10000
# 允许实时进程使用更多 CPU 时间
kernel.sched_rt_runtime_us = 950000
kernel.sched_rt_period_us = 1000000
说白了,就是让内核尽可能频繁地检查是否需要切换进程。但代价是 CPU 的上下文切换开销会增加。不过对于高频交易来说,这个代价值得。
perf stat 观察延迟分布。改完后重启应用,对比前后 99.9% 分位延迟的变化。
4.2 CPU 亲和性与隔离:给交易进程「包间」
你有没有遇到过这种情况:交易进程明明跑在 CPU 0 上,但延迟突然飙升?原因很可能是其他进程(比如系统守护进程、网卡中断)抢占了同一个 CPU 核心。
解决方案很简单:给交易进程一个「包间」,把其他无关进程赶出去。
4.2.1 CPU 隔离:把核心「藏」起来
在 Linux 启动参数中,我们可以指定哪些 CPU 核心不被普通进程使用。这些核心只运行我们指定的交易进程。
# 在 /etc/default/grub 中修改 GRUB_CMDLINE_LINUX
GRUB_CMDLINE_LINUX="isolcpus=2,3 nohz_full=2,3 rcu_nocbs=2,3"
解释一下:
isolcpus=2,3:将 CPU 2 和 3 从普通调度器中隔离出来nohz_full=2,3:关闭这两个核心的周期性时钟中断,减少抖动rcu_nocbs=2,3:将 RCU 回调从这两个核心上卸载
改完后记得 update-grub 并重启。重启后,你会发现 CPU 2 和 3 上几乎没有任何进程在跑(除了内核线程)。
4.2.2 设置 CPU 亲和性:把进程「绑」在核心上
隔离完核心后,我们需要把交易进程绑定到这些核心上。可以用 taskset 命令,或者在代码里用 sched_setaffinity 系统调用。
# 命令行方式
taskset -c 2,3 ./trading_engine
# C++ 代码方式
#include <sched.h>
cpu_set_t cpuset;
CPU_ZERO(&cpuset);
CPU_SET(2, &cpuset); // 绑定到 CPU 2
CPU_SET(3, &cpuset); // 也可以绑定到多个核心
sched_setaffinity(0, sizeof(cpuset), &cpuset);
我个人习惯把交易进程绑定到两个核心上:一个用于网络收发包,一个用于策略计算。这样即使一个核心被中断打断,另一个核心还能继续工作。
4.3 内存优化:大页与内存锁定
内存访问延迟是高频交易中的隐形杀手。一次 TLB 未命中,可能浪费几十个纳秒。在纳秒级竞争中,这已经很大了。
4.3.1 使用大页(Huge Pages)
默认的 4KB 内存页,对于高频交易这种需要大量内存访问的场景来说,TLB 覆盖范围太小了。2MB 甚至 1GB 的大页,可以显著减少 TLB 未命中。
# 预留 1024 个 2MB 大页
echo 1024 > /proc/sys/vm/nr_hugepages
# 或者使用透明大页(但我不推荐,因为它可能引入抖动)
echo always > /sys/kernel/mm/transparent_hugepage/enabled
嗯,这里我要多说一句:透明大页(THP)虽然方便,但它在后台进行内存合并时,可能会让进程卡住。我在项目中遇到过,THP 导致交易进程偶尔出现 1-2 毫秒的延迟尖峰。所以,我建议使用静态预留的大页,而不是透明大页。
在代码中使用大页:
// 使用 mmap 分配大页内存
#include <sys/mman.h>
void *addr = mmap(NULL, 2 * 1024 * 1024,
PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_HUGETLB,
-1, 0);
4.3.2 内存锁定:防止被换出
即使关闭了 swap,Linux 仍然可能因为内存压力而将进程的某些页面换出。我们需要用 mlockall 把交易进程的整个地址空间锁在物理内存中。
#include <sys/mman.h>
// 锁定当前和未来的所有内存页
mlockall(MCL_CURRENT | MCL_FUTURE);
这个调用应该在进程启动时尽早执行。我一般在 main 函数的第一行就调用它。
4.4 中断优化:把网卡中断「绑」到指定核心
网卡收到数据包后,会触发硬件中断。默认情况下,Linux 会把中断分配到所有 CPU 核心上。这会导致一个问题:交易进程所在的 CPU 核心,可能被网卡中断频繁打断。
解决方案:把网卡中断绑定到非交易核心上,或者绑定到专门的中断处理核心。
4.4.1 查看和设置中断亲和性
# 查看网卡中断号
cat /proc/interrupts | grep eth0
# 假设中断号是 78,将其绑定到 CPU 0
echo 1 > /proc/irq/78/smp_affinity
# 或者绑定到 CPU 1(二进制 10,即十六进制 2)
echo 2 > /proc/irq/78/smp_affinity
smp_affinity 是一个位图,每一位代表一个 CPU 核心。比如 echo 1 表示只绑定到 CPU 0,echo 3 表示绑定到 CPU 0 和 CPU 1。
我曾经犯过一个错误:把中断和交易进程绑到了同一个核心上。结果交易进程频繁被中断打断,延迟反而更高了。正确的做法是:把中断绑到 CPU 0,交易进程绑到 CPU 2 和 3,让 CPU 0 专门处理中断。
4.4.2 使用 RPS/RFS 加速软中断
如果网卡不支持多队列(RSS),我们可以用 RPS(Receive Packet Steering)来模拟多队列,将软中断处理分散到多个核心。
# 为 eth0 启用 RPS,将软中断分散到 CPU 0-3
echo f > /sys/class/net/eth0/queues/rx-0/rps_cpus
但注意,RPS 会增加 CPU 之间的缓存一致性开销。对于单队列网卡,我建议直接绑定到一个核心,避免跨核心通信。
4.5 本章知识体系图
下面这张图,是我梳理的本章核心逻辑。你可以把它当作调优的检查清单。
这张图把本章的三个核心模块串起来了。内核参数调优是基础,CPU 亲和性是关键,内存与中断优化是保障。三者缺一不可。
好了,这一章的内容就到这里。调优操作系统是个细致活,每个参数背后都有 trade-off。我的建议是:先在测试环境验证,再上生产。别一上来就全改,出了问题都不知道是哪一步导致的。
下一章,我们会进入更底层的领域——内存屏障与无锁编程。那才是真正考验 C++ 功底的地方。