Linux内核实时化改造:内核抢占模型(PREEMPT_RT)、中断线程化、自旋锁与互斥锁的实时化改造、高精度定时器(hrtimer)

做高频交易风控系统,最头疼的是什么?

不是策略逻辑有多复杂,而是你永远不知道内核什么时候会“走神”。

一次页面调度、一次中断处理、一次锁竞争——都可能让你的风控指令晚到几微秒。这几微秒,在纳秒级波动的行情里,可能就是爆仓和盈利的分界线。

所以,我们得对Linux内核动刀子。把它从一个“公平调度”的通用系统,改造成一个“指哪打哪”的硬实时系统。

内核抢占模型:从“自愿让出”到“随时打断”

标准Linux内核的抢占点,说白了就是“你主动让,我才抢”。

一个进程在系统调用里跑着,除非它自己调用schedule(),或者时间片到了,否则别人别想插队。这在服务器场景下没问题,但在高频交易里,这就是灾难。

想象一下:你的风控线程正在计算风险敞口,突然一个网卡中断进来,内核得先处理中断。等中断处理完,你的线程才能继续。这中间浪费了多少时间?

PREEMPT_RT补丁的核心思想:让内核变成完全可抢占的。任何地方,只要条件允许,高优先级任务都能打断低优先级任务。

我最早接触PREEMPT_RT是在一个期货套利项目里。当时我们的策略服务器跑在CentOS上,偶尔会出现几十毫秒的延迟尖峰。查了三天,最后发现是内核在遍历一个很大的链表时,关掉了抢占。嗯,那感觉就像高速公路上突然设了个收费站。

PREEMPT_RT做了三件关键的事:

  • 降低临界区粒度:把原来“关抢占”的大锁,拆成“关中断”的小锁。能关中断的地方,绝不关抢占。
  • 引入可抢占内核锁:自旋锁、读写锁这些,在PREEMPT_RT下都变成了可睡眠的互斥锁。
  • 中断线程化:这个我们后面细说。

配置起来其实不复杂。内核编译时选上:

CONFIG_PREEMPT_RT_FULL=y
CONFIG_HZ_1000=y
CONFIG_HIGH_RES_TIMERS=y

然后启动参数加上 preempt=full。重启后,你的内核就变成了“随时待命”的状态。

我的经验:别一上来就开PREEMPT_RT_FULL。先开PREEMPT_VOLUNTARY跑几天,看看有没有驱动兼容性问题。很多网卡驱动在RT内核下会报错,需要打补丁。

中断线程化:把“硬中断”变成“软线程”

标准Linux里,中断处理是最高优先级的。一旦中断来了,CPU立刻跳过去执行,谁都不能拦着。

这听起来很高效,对吧?

但你想过没有:一个网卡中断,可能只是告诉你“有数据包到了”。真正处理数据包的工作,其实可以等一等。可内核不管这些,它把整个中断处理过程都放在关中断的环境里执行。你的风控线程?等着吧。

中断线程化的思路很简单:把中断处理分成两部分。

  • 上半部(硬中断):只做最紧急的事,比如清空硬件寄存器、标记中断发生。这部分还是关中断的,但执行时间极短。
  • 下半部(软中断/线程):真正的数据处理,放到一个内核线程里执行。这个线程可以被调度,可以被抢占。

在PREEMPT_RT下,所有中断处理程序都默认变成可线程化的。你可以通过 /proc/irq/ 看到每个中断对应的线程:

# cat /proc/irq/48/smp_affinity
# ps -ef | grep irq
root      1234     2  0 10:00 ?        00:00:00 [irq/48-eth0]

这个 [irq/48-eth0] 就是中断线程。你可以给它设置实时优先级:

chrt -f -p 99 $(pgrep irq/48-eth0)

注意:中断线程化不是银弹。如果你的网卡中断频率极高(比如10Gbps线速),线程化反而会增加上下文切换开销。我见过一个案例,把中断线程化后,吞吐量掉了30%。后来我们改用NAPI轮询模式,才解决问题。

自旋锁与互斥锁的实时化改造

自旋锁,名字就说明了问题:拿不到锁?那就原地打转,一直轮询。

在非实时系统里,这没问题。反正自旋的时间很短,几微秒而已。但在实时系统里,一个高优先级任务如果因为自旋锁被低优先级任务持有,它就得傻等着。这叫“优先级反转”。

PREEMPT_RT怎么解决?

它把所有的自旋锁都改成了 可睡眠的互斥锁(rt_mutex)。拿不到锁?那就让出CPU,去睡一觉。等锁释放了,再被唤醒。

这样一来,高优先级任务就不会被低优先级任务阻塞了。因为低优先级任务在持有锁时,如果被高优先级任务抢占,它会立刻释放CPU,让高优先级任务先跑。

代码层面,你几乎不需要改什么。原来用 spin_lock() 的地方,在RT内核下自动变成 rt_spin_lock()。但有一个坑:

// 非RT内核下,这段代码没问题
spin_lock(&lock);
// 做一些需要关抢占的操作
local_irq_disable();
// ...
local_irq_enable();
spin_unlock(&lock);

// RT内核下,spin_lock可能睡眠,所以不能关中断!
// 正确的做法:
spin_lock_irqsave(&lock, flags);
// ...
spin_unlock_irqrestore(&lock, flags);

核心原则:在RT内核下,任何可能睡眠的锁(包括自旋锁)都不能在关中断的环境里使用。否则会触发内核警告,甚至死锁。

互斥锁的改造相对简单。PREEMPT_RT引入了优先级继承协议(Priority Inheritance Protocol)。简单说:当低优先级任务持有高优先级任务需要的锁时,低优先级任务会临时“继承”高优先级的优先级,尽快执行完临界区,释放锁。

我曾经在一个做市商系统里遇到过优先级反转导致的延迟抖动。当时我们的行情处理线程优先级是99,风控线程优先级是98。结果行情线程偶尔会等风控线程释放一个锁,而风控线程又被一个优先级更低的日志线程抢占了CPU。那叫一个乱。后来上了PREEMPT_RT,开了优先级继承,问题迎刃而解。

高精度定时器(hrtimer)

标准Linux的定时器,基于jiffies。jiffies是内核的“心跳”,通常每秒100到1000次。也就是说,定时器的精度最多1毫秒。

1毫秒?在高频交易里,这够你完成一次完整的订单生命周期了。我们需要微秒级的定时精度。

hrtimer就是干这个的。它不依赖jiffies,而是直接使用硬件的高精度定时器(比如TSC、HPET)。精度可以达到纳秒级。

使用hrtimer的API很直观:

#include <linux/hrtimer.h>

struct hrtimer my_timer;

// 初始化
hrtimer_init(&my_timer, CLOCK_MONOTONIC, HRTIMER_MODE_REL);

// 设置回调
my_timer.function = my_timer_callback;

// 启动定时器,10微秒后触发
ktime_t delay = ktime_set(0, 10000); // 0秒, 10000纳秒
hrtimer_start(&my_timer, delay, HRTIMER_MODE_REL);

// 回调函数
enum hrtimer_restart my_timer_callback(struct hrtimer *timer)
{
    // 这里做你的实时任务
    // 注意:这个回调在软中断上下文执行,不能睡眠
    
    return HRTIMER_NORESTART; // 一次性定时器
}

我的习惯:在风控系统里,我通常用hrtimer做“看门狗”定时器。比如,每100微秒检查一次订单状态,如果超时未成交,立刻撤单。这个精度,标准定时器根本做不到。

hrtimer有两种模式:

  • 绝对时间(HRTIMER_MODE_ABS):指定一个绝对时间点触发。适合做周期性任务。
  • 相对时间(HRTIMER_MODE_REL):从现在开始,过多少纳秒后触发。适合做超时控制。

还有一个容易被忽略的点:hrtimer的回调函数运行在 软中断上下文。这意味着你不能在里面调用任何可能睡眠的函数(比如kmalloc、mutex_lock)。如果你需要做复杂操作,得把工作推给一个内核线程。

我曾经踩过的坑:在hrtimer回调里直接调用了printk,结果导致系统锁死。因为printk可能触发控制台输出,而控制台驱动又需要睡眠。记住:hrtimer回调里,只做最轻量级的事。

知识体系总览

下面这张图,把本章的核心逻辑串起来了。你可以看到,从内核抢占模型到底层定时器,每一层都在为“确定性”服务。

Linux内核实时化改造知识体系 PREEMPT_RT 内核抢占模型 中断线程化 锁的实时化改造 高精度定时器 中断线程化 上半部:硬中断(极短) 下半部:线程化处理 可设置实时优先级 锁的实时化 自旋锁 → 可睡眠互斥锁 优先级继承协议 避免优先级反转 hrtimer 基于硬件定时器 纳秒级精度 软中断上下文 确定性:可预测的微秒级响应

这张图里,PREEMPT_RT是地基。它决定了整个系统的调度策略。上面三层是具体的改造手段。最终目标只有一个:让系统的行为变得可预测。在高频交易里,我们不怕慢,怕的是“有时候快,有时候慢”。确定性,比什么都重要。

好了,这一章的内容就到这里。Linux内核实时化改造是个系统工程,每个点都能深挖。但记住:不要为了实时而实时。先搞清楚你的瓶颈在哪,再对症下药。

一句话总结:PREEMPT_RT给你的是“确定性”,不是“更快”。但有了确定性,你才能在高频交易里做出更激进的策略。


公众号:蓝海资料掘金营,微信deep3321