观察者模式:内核钩子:eBPF 程序被自动调用

67 阅读4分钟

个人总结:具体一点就是:

  • 埋点:就是设置观察点
  • 观察点就是函数名
  • 将 ebpf 程序挂载到内核函数:其实就是将函数的埋点对应到 ebpf 程序
  • Linux内核钩子(hook)机制能够实现内核函数被调用的时候,把 ebpf 程序先跑起来,等内核函数调用结束之后再结束

要理解 eBPF 程序挂载到内核函数后为何会被自动调用,需要从内核机制、触发原理和设计模式三个层面来解析:

一、内核层面的触发原理

eBPF 程序能够被自动调用的核心是内核钩子(Hook)机制,具体过程如下:

  1. 挂载点注册
    当通过bpf_prog_attach()等系统调用将 eBPF 程序挂载到目标内核函数(如sys_writetcp_sendmsg)时,内核会在该函数的执行路径中插入一个触发点

    • 对于静态内核函数(编译时确定的函数),通常通过 kprobe(动态调试钩子)实现:内核会在目标函数的入口/出口处插入一条特殊指令(如 x86 的 int3 断点)。
    • 对于动态事件(如网络包、进程创建),则通过内核预设的静态钩子(如tracepointkretprobe)触发,这些钩子是内核开发者预先在关键路径中埋入的调用点。
  2. 执行流程拦截
    当目标内核函数被调用时,CPU 执行到预设的触发点指令时会暂停当前流程,转而执行以下操作:

    • 保存当前寄存器状态和栈信息(确保原函数能正常恢复)。
    • 调用内核中的 eBPF 虚拟机(bpf_prog_run()),加载并执行已挂载的 eBPF 程序。
    • eBPF 程序执行完毕后,恢复现场,继续执行原内核函数的剩余逻辑。
  3. 权限与安全控制
    内核会通过BPF 验证器(verifier) 确保 eBPF 程序不会破坏内核稳定性(如不访问非法内存、不执行无限循环),只有通过验证的程序才能被挂载和执行。

二、内核实现的关键机制

  1. 动态代码修改(kprobe机制)
    kprobe 通过动态修改内核代码段(在目标函数入口写入断点指令)实现拦截。当 CPU 执行到断点时,会触发内核异常处理流程(int3中断),进而调用 kprobe 注册的回调函数,而 eBPF 程序就通过这个回调被间接执行。

  2. 事件通知框架(tracepoint)
    tracepoint 是内核编译时静态插入的事件点(本质是一个函数指针),当内核运行到对应位置时会检查是否有注册的 eBPF 程序,若有则调用。相比 kprobe,tracepoint 更稳定(不会修改内核代码),但仅能用于内核预设的事件点。

  3. eBPF虚拟机
    内核中的 eBPF 虚拟机负责解释执行 eBPF 字节码,它限制了程序的执行时间(通过rlimit)和内存访问范围,确保 eBPF 程序无法干扰内核正常运行。

三、设计模式:观察者模式(Observer Pattern)

eBPF的挂载与触发机制本质上是观察者模式的内核级实现:

  • 被观察者(Subject):内核中的目标函数或事件(如sys_write、进程调度)。
  • 观察者(Observer):eBPF 程序,通过挂载操作注册到被观察者中。
  • 触发逻辑:当被观察者(内核函数)发生状态变化(被调用)时,自动通知所有注册的观察者(eBPF 程序)执行。

个人总结:具体一点就是:

  • 埋点:就是设置观察点
  • 观察点就是函数名
  • 将 ebpf 程序挂载到内核函数:其实就是将函数的埋点对应到 ebpf 程序
  • Linux内核钩子(hook)机制能够实现内核函数被调用的时候,把 ebpf 程序先跑起来,等内核函数调用结束之后再结束

这种模式的优势在于:

  1. 解耦:内核功能与 eBPF 程序的逻辑完全分离,无需修改内核源码即可扩展功能。
  2. 动态性:eBPF 程序可在运行时动态挂载/卸载,不影响内核整体稳定性。
  3. 高效性:通过内核级钩子直接拦截,避免了用户态与内核态的频繁切换开销。

总结

eBPF 程序被自动调用的核心是内核钩子机制:通过 kprobe/tracepoint 等技术在目标函数中插入触发点,当函数执行时触发 eBPF 虚拟机执行已挂载的程序。

其设计模式符合观察者模式,实现了内核事件与用户扩展逻辑的解耦,兼顾了灵活性与安全性。