在《Futex 概述》中已经介绍了 Futex 的用户态快速路径,以及发生竞争后进入内核等待、唤醒的过程。本文继续讨论一个更偏实践的问题:怎样利用 eBPF 观测 Futex,并从采集结果中识别锁竞争和调度延迟。

本文关注观测方案、指标和实验结果。libbpf 程序加载、attach、Map 读取以及 probe 没有触发等问题,单独整理在《使用 libbpf 调试 Futex eBPF 程序》中。

一、观测目标与追踪点

Futex 没有竞争时主要在用户态通过原子操作完成,只有等待或唤醒等慢路径才会进入内核。因此,仅追踪 Futex 系统调用无法得到所有加锁操作,却很适合回答以下问题:

  1. 哪些线程频繁进入 FUTEX_WAIT
  2. 一次等待从进入内核到返回经历了多长时间?
  3. FUTEX_WAKE 实际唤醒了多少线程,是否存在无效唤醒?
  4. 哪些 Futex 地址形成热点?
  5. 锁竞争是否导致延迟关键任务长时间等待?
eBPF 只能直接看见进入内核的 Futex 慢路径。没有进入系统调用的用户态快速路径不会被统计,因此文中的“竞争次数”不能等同于应用执行的全部加锁次数。

Tracepoint

优先检查系统是否提供 syscalls:sys_enter_futexsyscalls:sys_exit_futex。进入时保存 pid/tiduaddrfutex_op 和时间戳;退出时找回开始时间并结合返回值计算耗时。

Tracepoint 接口相对稳定,适合先做系统调用级观测。

fentry/fexit 或 kprobe/kretprobe

如果需要区分 futex_waitfutex_wake 和 PI Futex 等内核路径,可以追踪对应内核函数。该方案能取得更贴近内核实现的参数,但函数名和签名可能随内核版本变化。

SEC("fentry/__futex_wait")
int BPF_PROG(trace_futex_wait, u32 *uaddr, unsigned int flags, u32 val)
{
    /* 保存地址、线程和开始时间 */
    return 0;
}

Uprobe

Uprobe 可以观察 pthread_mutex_lock() 等用户态函数,理论上能够补充用户态快速路径,但依赖具体 libc 或应用二进制。对于高频锁操作,其观测开销也更值得警惕,因此本文暂不把它作为默认方案。

当前开销记录

下面的数据来自现有实验笔记。发表前必须补充测试命令、内核版本、重复次数和统计方法,并重新验证。

技术 当前记录的单次开销 稳定性 用途
Tracepoint 约 130 ns 系统调用级观测
fentry/fexit fentry 约 48 ns,fexit 约 76 ns 取决于内核函数 内核函数级观测
Uprobe 尚无可靠结果 依赖用户态符号 补充用户态快速路径

二、从事件中得到哪些指标

等待次数与等待延迟

对每个 FUTEX_WAIT 记录进入和返回时间,统计等待次数、平均值、P50、P95、P99 和最大等待时间。相比平均值,长尾分位数更容易暴露少量但严重的锁竞争。

唤醒次数与返回值

FUTEX_WAKE 记录请求唤醒数量和系统调用返回值。返回值表示实际唤醒的线程数,可以识别无效唤醒、惊群风险以及 wait/wake 数量长期不匹配的场景。

热点 Futex

将进程与 uaddr 组合成 key,按地址累计等待次数、总延迟和直方图。直接记录地址时要考虑地址复用和信息暴露;跨进程共享 Futex 也不能仅凭虚拟地址判断是否对应同一个内核 Futex key。

struct futex_key {
    __u32 tgid;
    __u64 uaddr;
};

struct hist {
    char comm[16];
    __u64 slots[MAX_SLOTS];
    __u64 count;
    __u64 total_ns;
};

PI Futex 与优先级反转

FUTEX_LOCK_PIFUTEX_UNLOCK_PI 单独分类,观察高优先级线程等待低优先级持锁线程的时间。仅凭 wait/wake 事件无法准确恢复全部 owner 关系,还需要结合调度事件或 PI Futex 内核状态验证。

三、Map、数据处理与观测开销

一类 Map 保存线程进入 Futex 时的时间,另一类 Map 保存按锁地址聚合的统计结果。

start = bpf_ktime_get_ns();
bpf_map_update_elem(&starts, &tid, &start, BPF_ANY);

startp = bpf_map_lookup_elem(&starts, &tid);
if (startp) {
    delta = bpf_ktime_get_ns() - *startp;
    /* 更新 count、total_ns 和 log2 histogram */
    bpf_map_delete_elem(&starts, &tid);
}

bpf_map_update_elem() 的标志含义如下:

标志 行为
BPF_ANY key 存在时覆盖,不存在时插入
BPF_NOEXIST 仅在 key 不存在时插入
BPF_EXIST 仅在 key 已存在时更新

开始时间通常允许覆盖,使用 BPF_ANY 更符合“保存最近一次进入时间”的语义;如果业务要求发现重复进入,可使用 BPF_NOEXIST 并单独统计冲突。

控制观测开销

Futex 是高频事件,观测工具本身可能改变系统行为:

  1. 尽早按 tgid、cgroup 或操作类型过滤。
  2. 在 BPF Map 中聚合直方图,避免逐条发送事件。
  3. 只在定位问题时采集调用栈。
  4. 对极高频地址进行抽样。
  5. 记录丢失事件,避免把不完整数据当成真实结果。

参考实现可以查看 BCC/libbpf-tools 中的 futexctn

四、实验设计、结果与调度联系

发表前需要补齐 CPU 型号、SMT 状态、内核配置、libbpf/clang/bpftool 版本、测试负载、采样时长、绑核方式和重复次数。

建议至少比较:不加载工具的基线、只加载不输出、Map 聚合、Ring Buffer 逐条输出,以及开启/关闭 PID 或 cgroup 过滤。

场景 Futex 调用次数 平均等待 P99 等待 工具 CPU 开销 丢失事件
基线 待补 待补 待补 待补 -
Map 聚合 待补 待补 待补 待补 待补
Ring Buffer 输出 待补 待补 待补 待补 待补
目前已有观测方案和指标设计,但尚没有足以发表的完整结果。正式发布前应以可复现数据替换“待补”,并说明测量误差。

与 LAVD 的联系

如果能可靠识别锁持有者,可以尝试让 LAVD 优先调度阻塞延迟关键任务的持锁线程。LAVD 的用户态控制流程可以参考《sched_ext 与 scx_lavd 用户态前端解析》。不过,普通 Futex 很难只依靠 wait/wake 恢复 owner,仍需结合用户态锁语义、调度事件和历史获取模式。

重点评估:Futex holder boost 是否降低关键任务 P99/P999 等待时间,以及它是否导致普通任务吞吐下降或产生新的优先级反转。

五、小结

Futex 性能观测的重点不是简单统计系统调用次数,而是建立等待线程、Futex 地址、唤醒结果和调度延迟之间的关系。下一步需要补齐可复现实验数据,再判断 Futex holder boost 对 LAVD 是否确实有效。

参考资料


文章作者: 易百分
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 易百分 !
 上一篇
使用 libbpf 调试 Futex eBPF 程序 使用 libbpf 调试 Futex eBPF 程序
以 futexlat 为例,记录 eBPF 程序从编译、加载、attach 到 BPF Map 读取和 probe 触发的排查方法。
2026-03-23
下一篇 
GCC、G++ 与 Windows SDK:C/C++ 混合编译和头文件排查 GCC、G++ 与 Windows SDK:C/C++ 混合编译和头文件排查
从编译与链接流程出发,说明 gcc 和 g++ 的区别、C/C++ 混合工程的构建方式,以及 Windows SDK 头文件无法被 MinGW 找到或使用时的排查方法。
  目录