做Linux服务器运维、内核调试的同学,大概率都遇到过这种头疼事:业务突然卡顿、网络延迟飙升,CPU负载却不高,查了半天找不到问题根源。其实很多时候,罪魁祸首不是CPU太忙,而是内核里的“中断”或“软中断”被长时间关闭,导致网卡、时钟等关键任务“喊不醒”CPU,进而引发一系列异常。
今天要聊的,就是一款专门“抓”这种隐形问题的工具——中断与软中断延迟追踪器(Interrupts-off or softirqs-off latency tracer)。它就像内核里的“监控摄像头”,能精准记录中断/软中断被关闭的时长、触发进程,甚至定位到具体的代码位置,让原本隐蔽的问题一目了然。
一、先搞懂核心:这工具到底解决什么问题?
在聊工具之前,先用大白话讲清楚两个关键概念,不然后面的原理都听不懂——毕竟工具的核心,就是盯着这两个“家伙”。
1. 中断(硬中断):CPU的“紧急呼叫”
中断是硬件(比如网卡、磁盘、时钟)给CPU发的“紧急信号”,优先级最高。比如网卡收到数据,会立刻给CPU发中断,让CPU暂停当前工作,先处理数据接收——就像你正在工作,突然接到一个紧急电话,必须先接电话再继续干活。 内核里有个操作叫“关闭中断”(比如用local_irq_disable函数),目的是保护一段关键代码不被打断(比如处理核心数据,一打断就会出错)。但问题来了:如果关闭时间太长,硬件的“紧急呼叫”就会被忽略,网卡数据堆积会导致丢包,时钟不准会导致进程调度混乱,最终就是业务延迟飙升。
2. 软中断:CPU的“课后作业”
软中断是内核自己的“轻量级任务”,优先级比硬中断低,比如网络数据的后续处理、定时器任务等。它是内核为了不耽误硬中断处理,把一些非紧急的工作“延后做”——就像你接完紧急电话,再处理之前没做完的工作。 和硬中断一样,软中断也能被关闭(比如用local_bh_disable函数)。如果软中断关闭太久,就会导致任务堆积,负责处理软中断的内核线程(比如ksoftirqd)没机会运行,同样会引发网络延迟、IO抖动。
3. 工具的核心价值:让“隐形延迟”现形
很多时候,我们怀疑是中断/软中断关闭太久导致的问题,但没有证据——不知道是哪个进程、哪段代码导致的,也不知道关闭了多久。这款工具的作用,就是帮我们:
- 量化:中断/软中断被关闭了多长时间,是否超过合理阈值;
- 统计:不同时长的关闭情况出现了多少次,方便排查规律。 简单说,它就是内核延迟问题的“侦探”,把原本看不见、摸不着的“关闭延迟”,变成看得见、可分析的具体数据。
二、必备知识点:搞懂这些,才能理解工具原理
工具的设计和实现,离不开Linux内核的几个核心知识点,不用死记硬背,理解大白话就行,后面讲原理会反复用到:
1. 内核定时器:工具的“心跳”
工具的核心是两个定时器(可以理解为“闹钟”):一个是高精度定时器(hrtimer),一个是普通定时器(timer)。高精度定时器精度高(纳秒级),负责监测硬中断;普通定时器负责监测软中断,两者配合,避免漏测。
2. PerCPU变量:多核CPU的“专属空间”
Linux服务器大多是多核CPU,每个CPU的工作是独立的。工具为每个CPU分配了专属的内存空间(PerCPU变量),用来存储自己的定时器、时间戳、栈信息,这样多个CPU同时工作时,不会互相干扰,也不用加锁,性能更高——就像每个员工有自己的办公桌,不用抢一张桌子办公。
3. 栈回溯:定位代码的“导航仪”
当检测到中断/软中断关闭超时,工具会抓取当前的内核调用栈(就是函数调用的顺序),通过栈信息,我们能直接找到导致超时的具体函数——就像通过导航记录,找到你走错路的具体路段。
4. Proc文件系统:用户和内核的“沟通窗口”
工具会在/proc目录下创建专属目录,里面有4个文件,用来控制工具启停、配置参数、查看结果。我们不用操作复杂的命令,只要读写这些文件,就能控制工具——就像通过控制面板,操作一台设备。
5. 内核版本兼容:跨版本的“适配术”
Linux内核版本很多,不同版本的接口(比如文件操作、定时器函数)不一样。工具做了兼容处理,不管是低版本还是高版本内核,都能正常运行——就像一款软件,既能在Windows 10上用,也能在Windows 11上用。
三、设计思路:用“心跳监测”抓出隐形延迟
工具的设计思路特别简单,核心就是“定时心跳+超时检测”,我们用生活化的例子类比,一下子就能懂: 假设你要监测一个人有没有“长时间走神”,你可以每隔10秒(采样周期)喊他一声,如果他立刻回应,说明没走神;如果过了30秒(阈值)才回应,说明他走神了,你就记录下他走神的时间、当时在做什么(对应进程和代码)。
工具的逻辑和这个完全一样,只不过监测的是CPU的“中断/软中断状态”,具体流程如下(简易流程图):
简易流程原理图
加载工具(内核模块) → 为每个CPU绑定两个定时器(高精度+普通) → 定时器每隔固定时间(采样周期)记录一次时间戳 → 计算两次时间戳的差值(delta) → 如果delta超过采样周期的2倍(说明中断/软中断被关闭,定时器没及时触发) → 若delta超过设定阈值,记录进程信息、CPU编号、调用栈 → 通过Proc文件展示结果 关键设计亮点(为什么这么设计?)
1. 双定时器设计:避免漏测
为什么用两个定时器?因为如果只用一个,万一这个定时器被中断/软中断关闭给“卡住”了,就无法监测了。比如:高精度定时器运行在硬中断上下文,负责监测硬中断——如果硬中断被关闭,高精度定时器也会被卡住,这时候普通定时器(运行在软中断上下文)就能补位,继续监测;反之亦然。双定时器互补,实现无死角监测。
2. PerCPU无锁设计:不影响业务性能
工具本身不能成为新的性能瓶颈,所以用了PerCPU变量——每个CPU独立存储数据,不用和其他CPU抢资源、加锁,操作速度快,对业务的影响几乎可以忽略(非侵入式设计)。
3. 阈值控制:不记录无效信息
不是所有中断/软中断关闭都需要记录,只有超过设定阈值(默认50ms)的才记录,这样能减少无效数据,让我们专注于真正的问题。同时,我们也能根据业务需求,修改阈值和采样周期。
4. 栈回溯+进程信息:直接定位元凶
工具不仅记录延迟时长,还会抓取当前进程的名称、PID,以及内核调用栈——不用再猜是哪个进程、哪段代码出问题,直接定位到具体函数,大大减少排查时间。
四、代码实现原理
...
staticinttrace_latency_open(struct inode *inode, struct file *file)
{
return single_open(file, trace_latency_show, inode->i_private);
}
#if LINUX_VERSION_CODE < KERNEL_VERSION(5, 6, 0)
staticconststructfile_operationstrace_latency_fops = {
.owner = THIS_MODULE,
.open = trace_latency_open,
.read = seq_read,
.write = trace_latency_write,
.llseek = seq_lseek,
.release = single_release,
};
#else
staticconststructproc_opstrace_latency_fops = {
.proc_open = trace_latency_open,
.proc_read = seq_read,
.proc_write = trace_latency_write,
.proc_lseek = seq_lseek,
.proc_release = single_release,
};
#endif
staticintenable_show(struct seq_file *m, void *ptr)
{
seq_printf(m, "%s\n", trace_enable ? "enabled" : "disabled");
return0;
}
staticintenable_open(struct inode *inode, struct file *file)
{
return single_open(file, enable_show, inode->i_private);
}
staticvoidtrace_irqoff_start_timers(void)
{
int cpu;
for_each_online_cpu(cpu) {
structhrtimer *hrtimer;
structtimer_list *timer;
hrtimer = per_cpu_ptr(&cpu_stack_trace->hrtimer, cpu);
hrtimer_init(hrtimer, CLOCK_MONOTONIC, HRTIMER_MODE_PINNED);
hrtimer->function = trace_irqoff_hrtimer_handler;
timer = per_cpu_ptr(&cpu_stack_trace->timer, cpu);
#if LINUX_VERSION_CODE < KERNEL_VERSION(4, 7, 0)
__setup_timer(timer, trace_irqoff_timer_handler,
(unsignedlong)timer, TIMER_IRQSAFE);
#elif LINUX_VERSION_CODE < KERNEL_VERSION(4, 15, 0)
timer->flags = TIMER_PINNED | TIMER_IRQSAFE;
setup_timer(timer, trace_irqoff_timer_handler,
(unsignedlong)timer);
#else
timer_setup(timer, trace_irqoff_timer_handler,
TIMER_PINNED | TIMER_IRQSAFE);
#endif
smp_call_function_single(cpu, smp_timers_start,
per_cpu_ptr(cpu_stack_trace, cpu),
true);
}
}
staticvoidtrace_irqoff_cancel_timers(void)
{
int cpu;
for_each_online_cpu(cpu) {
structhrtimer *hrtimer;
structtimer_list *timer;
hrtimer = per_cpu_ptr(&cpu_stack_trace->hrtimer, cpu);
hrtimer_cancel(hrtimer);
timer = per_cpu_ptr(&cpu_stack_trace->timer, cpu);
del_timer_sync(timer);
}
}
#if LINUX_VERSION_CODE < KERNEL_VERSION(4, 4, 0)
#include<linux/string.h>
staticintkstrtobool_from_user(constchar __user *s, size_t count, bool *res)
{
/* Longest string needed to differentiate, newline, terminator */
char buf[4];
count = min(count, sizeof(buf) - 1);
if (copy_from_user(buf, s, count))
return -EFAULT;
buf[count] = '\0';
return strtobool(buf, res);
}
#endif
staticssize_tenable_write(struct file *file, constchar __user *buf,
size_t count, loff_t *ppos)
{
bool enable;
if (kstrtobool_from_user(buf, count, &enable))
return -EINVAL;
if (!!enable == !!trace_enable)
return count;
if (enable)
trace_irqoff_start_timers();
else
trace_irqoff_cancel_timers();
trace_enable = enable;
return count;
}
#if LINUX_VERSION_CODE < KERNEL_VERSION(5, 6, 0)
staticconststructfile_operationsenable_fops = {
.open = enable_open,
.read = seq_read,
.write = enable_write,
.llseek = seq_lseek,
.release = single_release,
};
#else
staticconststructproc_opsenable_fops = {
.proc_open = enable_open,
.proc_read = seq_read,
.proc_write = enable_write,
.proc_lseek = seq_lseek,
.proc_release = single_release,
};
#endif
staticintsampling_period_show(struct seq_file *m, void *ptr)
{
seq_printf(m, "%llums\n", sampling_period / (1000 * 1000UL));
return0;
}
staticintsampling_period_open(struct inode *inode, struct file *file)
{
return single_open(file, sampling_period_show, inode->i_private);
}
staticssize_tsampling_period_write(struct file *file, constchar __user *buf,
size_t count, loff_t *ppos)
{
unsignedlong period;
if (trace_enable)
return -EINVAL;
if (kstrtoul_from_user(buf, count, 0, &period))
return -EINVAL;
period *= 1000 * 1000UL;
if (period > (trace_irqoff_latency >> 1))
trace_irqoff_latency = period << 1;
sampling_period = period;
return count;
}
#if LINUX_VERSION_CODE < KERNEL_VERSION(5, 6, 0)
staticconststructfile_operationssampling_period_fops = {
.open = sampling_period_open,
.read = seq_read,
.write = sampling_period_write,
.llseek = seq_lseek,
.release = single_release,
};
#else
staticconststructproc_opssampling_period_fops = {
.proc_open = sampling_period_open,
.proc_read = seq_read,
.proc_write = sampling_period_write,
.proc_lseek = seq_lseek,
.proc_release = single_release,
};
#endif
staticint __init trace_irqoff_init(void)
{
structproc_dir_entry *parent_dir;
cpu_stack_trace = alloc_percpu(struct per_cpu_stack_trace);
if (!cpu_stack_trace)
return -ENOMEM;
stack_trace_skip_hardirq_init();
parent_dir = proc_mkdir("latency_tracer", NULL);
if (!parent_dir)
goto free_percpu;
if (!proc_create("distribute", S_IRUSR, parent_dir, &distribute_fops))
goto remove_proc;
if (!proc_create("trace_latency", S_IRUSR | S_IWUSR, parent_dir,
&trace_latency_fops))
goto remove_proc;
if (!proc_create("enable", S_IRUSR | S_IWUSR, parent_dir, &enable_fops))
goto remove_proc;
if (!proc_create("sampling_period", S_IRUSR | S_IWUSR, parent_dir,
&sampling_period_fops))
goto remove_proc;
return0;
remove_proc:
remove_proc_subtree("latency_tracer", NULL);
free_percpu:
free_percpu(cpu_stack_trace);
return -ENOMEM;
}
staticvoid __exit trace_irqoff_exit(void)
{
if (trace_enable)
trace_irqoff_cancel_timers();
remove_proc_subtree("latency_tracer", NULL);
free_percpu(cpu_stack_trace);
}
...
If you need the complete source code, please add the WeChat number (c17865354792)
下面我们拆解工具的核心代码(避开复杂语法,只讲逻辑),结合前面的设计思路,让你明白工具是怎么“工作”的。代码的核心就是实现上面说的“心跳监测+超时记录”,主要分为5个部分:
1. 数据结构设计:存储监测数据
首先定义几个关键的数据结构,用来存储每个CPU的定时器、时间戳、栈信息、进程信息等——就像给每个CPU准备一个“笔记本”,记录监测过程中的所有数据。 核心数据结构:
- per_cpu_stack_trace:每个CPU的专属数据,包含两个定时器(高精度+普通)、硬中断监测数据、软中断监测数据;
- stack_trace_metadata:存储具体的监测信息,比如上次的时间戳、延迟次数统计、进程名称、PID、延迟时长、调用栈;
- irqoff_trace:存储调用栈的具体内容(函数调用顺序)。 代码片段(简化后,只看逻辑):
// 每个CPU的专属数据结构
structper_cpu_stack_trace {
structtimer_listtimer;// 普通定时器(监测软中断)
structhrtimerhrtimer;// 高精度定时器(监测硬中断)
structstack_trace_metadatahardirq_trace;// 硬中断监测数据
structstack_trace_metadatasoftirq_trace;// 软中断监测数据
};
// 存储监测详情(时间戳、进程信息、栈信息等)
structstack_trace_metadata {
u64 last_timestamp; // 上次定时器触发的时间戳
char comms[][TASK_COMM_LEN]; // 触发超时的进程名称
pid_t pids[]; // 触发超时的进程PID
u64 latency[][2]; // 延迟时长(nsecs:时长,more:标记是否为软中断)
structirqoff_tracetrace[];// 调用栈信息
};
2. 定时器初始化:启动“心跳”
工具加载时(内核模块初始化),会为每个CPU初始化两个定时器,设置采样周期(默认10ms),并绑定定时器的“处理函数”——就像给每个CPU的“闹钟”设置好时间,并且规定“闹钟响了之后要做什么”。 核心逻辑:
- 高精度定时器(hrtimer):初始化后,设置为“周期性触发”,每次触发后调用处理函数,记录时间戳、计算延迟;
- 普通定时器(timer):同样设置为周期性触发,专门处理软中断的监测;
3. 延迟检测:判断是否超时
这是工具的核心逻辑,定时器每次触发时,都会执行以下操作(对应代码中的处理函数):
- 计算当前时间戳与上次时间戳的差值(delta)——这个差值就是“中断/软中断可能被关闭的时长”;
- 如果delta小于采样周期的2倍,说明正常(定时器准时触发,没有被阻塞);
- 如果delta大于采样周期的2倍,说明中断/软中断被关闭了,开始统计次数;
- 如果delta大于设定的阈值(默认50ms),就调用“记录函数”,保存进程信息、调用栈。 代码片段(简化后,核心处理函数):
// 高精度定时器处理函数(监测硬中断)
staticenum hrtimer_restart hrtimer_handler(struct hrtimer *hrtimer)
{
u64 now = local_clock(); // 获取当前时间
// 计算当前与上次的时间差(延迟时长)
u64 delta = now - __this_cpu_read(cpu_stack_trace->hardirq_trace.last_timestamp);
// 更新上次时间戳
__this_cpu_write(cpu_stack_trace->hardirq_trace.last_timestamp, now);
// 检测是否超时,超时则记录信息
trace_record(delta, true, true);
// 重新启动定时器,继续监测
hrtimer_forward_now(hrtimer, ns_to_ktime(sampling_period));
return HRTIMER_RESTART;
}
4. 栈回溯与信息保存:记录“元凶”
当检测到超时后,工具会调用save_trace函数,保存关键信息——就像“侦探”发现嫌疑人后,记录下嫌疑人的姓名、动作、位置。 核心操作:
- 保存当前进程的名称(current->comm)和PID(current->pid);
- 调用栈回溯函数(stack_trace_save),抓取当前的内核调用栈,保存到数据结构中;
- 更新统计次数(比如“硬中断关闭80-159ms的次数+1”)。
5. Proc文件接口:用户交互入口
工具在/proc目录下创建专属目录,里面有4个文件,对应4个功能,方便用户操作:
- enable:控制工具启停(echo 1开启,echo 0关闭);
- trace_latency:查看/设置超时阈值(默认50ms),也能清除历史记录(echo 0);
- sampling_period:查看/设置采样周期(默认10ms,必须在工具关闭时修改);
- distribute:查看中断/软中断关闭的次数分布(比如“硬中断关闭80-159ms出现4次”)。 这些文件的实现,本质上是内核通过Proc文件系统,向用户态暴露接口,让我们能读写内核中的数据——比如我们写入“100”到trace_latency,内核就会把阈值修改为100ms。
五、加载模块与测试
insmod latency_tracer.ko
加载成功后,会自动出现:
/proc/tracker/
里面有 4 个文件:
最简单的测试方法
1)开启追踪
echo 1 > /proc/tracker/enable
2)手动制造“关中断延迟”
新建一个测试文件 test_irqoff.c:
#include<linux/module.h>
#include<linux/kernel.h>
#include<linux/delay.h>
#include<linux/interrupt.h>
staticint __init test_init(void)
{
local_irq_disable();
mdelay(120); // 关闭硬中断 120ms
local_irq_enable();
printk("test done\n");
return0;
}
staticvoid __exit test_exit(void)
{
}
module_init(test_init);
module_exit(test_exit);
MODULE_LICENSE("GPL");
编译:
make -f Makefile.test
加载测试模块:
insmod test_irqoff.ko
这会制造一个 120ms 关中断延迟。
查看结果
1)看统计分布
cat /proc/latency_tracer/distribute
2)看超时栈(直接看到哪个函数导致延迟)
cat /proc/latency_tracer/trace_latency
你会看到类似:
cpu: 0
COMMAND: bash PID: 1234 LATENCY: 127ms
disable_hardirq+xxx
说明工具成功抓到了关中断超时!
常用操作命令
# 开启追踪
echo 1 > /proc/latency_tracer/enable
# 关闭追踪
echo 0 > /proc/latency_tracer/enable
# 设置阈值为 100ms(只记录超过 100ms 的延迟)
echo 100 > /proc/latency_tracer/trace_latency
# 清除所有记录
echo 0 > /proc/latency_tracer/trace_latency
# 修改采样周期为 5ms(必须先关闭工具)
echo 0 > /proc/latency_tracer/enable
echo 5 > /proc/latency_tracer/sampling_period
# 查看统计
cat /proc/latency_tracer/distribute
# 查看详细栈
cat /proc/latency_tracer/trace_latency
六、知识总结:工具背后的核心领域要点
这款工具看似简单,实则融合了Linux内核多个领域的知识,总结下来,核心要点有5个,不管是运维还是内核开发,掌握这些都很有帮助:
1. 中断管理:内核的“优先级调度核心”
中断是内核处理硬件请求的核心机制,硬中断优先级高于软中断,关闭中断的时长必须严格控制——这是内核性能优化的关键知识点,也是工具的监测核心。
2. 定时器机制:内核的“时间管理工具”
高精度定时器(hrtimer)和普通定时器(timer)是内核实现时间监测、任务调度的基础,不同场景选择不同的定时器,平衡精度和性能——工具的“心跳”全靠它们。
3. 多核编程:PerCPU无锁设计的实践
多核CPU环境下,无锁设计是提升性能的关键,PerCPU变量让每个CPU独立工作,避免锁竞争——这是内核工具开发的常用技巧,也是工具高性能的原因。
4. 栈回溯与调试:内核问题排查的“终极手段”
内核调用栈能直接反映函数的执行顺序,定位到具体的耗时代码——这是内核调试的核心技能,工具把这个技能封装起来,让非内核开发人员也能轻松使用。
5. Proc文件系统:内核与用户态的“通信桥梁”
Proc文件系统是内核向用户态暴露信息、接收控制指令的常用接口,很多内核工具(比如top、free)都依赖它——工具通过Proc文件,实现了“易用性”,让运维人员不用深入内核代码,就能操作工具。
七、总结
这款中断与软中断延迟追踪器,看似是一个“小工具”,却解决了Linux内核调试中最隐蔽、最头疼的延迟问题。它的设计思路简洁高效,核心是“用定时器监测心跳,用栈回溯定位元凶,用PerCPU保证性能,用Proc提升易用性”。
对于运维人员来说,它是排查业务延迟、IO抖动的“神器”,能帮我们快速找到问题根源,减少排查时间;对于内核开发人员来说,它是学习中断管理、定时器、多核编程的“实战案例”,融合了内核多个核心领域的知识。 说到底,这款工具的价值,就是把内核中“看不见的延迟”,变成“看得见、可分析、可解决”的问题——这也是Linux内核工具开发的核心思路:简单、实用、直击痛点。
Welcome to follow WeChat official account【程序猿编码】