首先抛出我的问题:
当我在编译一个kprobe内核插桩模块时,我是这样实现的,先动态申请一块内存,用于存放我的kprobe结构体,然后执行kprobe的注册,完成一系列的内核数据收集之后,执行kprobe注销操作,最后释放内存,那么大家觉得我这个设计有问题吗?
对于初学者可能觉得:没有什么问题呀?kprobe有注册和有注销,内存有申请有释放,怎么出问题的,实际上正确答案就是没有问题。。。哈哈,是不是感觉被我忽悠了一下。
下面我来说说我当时的顾虑,由于我是动态申请的内存,kprobe机制是这样的,注册了对应kprobe handler,那么kprobe找到对应的地址,并且替换int3指令,(当然也可以使用基于ftrace实现的,这里暂时不考虑)。那么当我执行了注销kprobe操作,那么就相当于这个函数又恢复了,入口处的int3指令又被替换回去了,那么后续对该函数与的调用就不会执行我注册的hook函数。那么假设这样一种场景:
如果有一个CPU上正在执行我注销的kprobe handler,那么我的注销动作实际上并不会阻止这个handler的继续运行,那么如果我紧接着把kprobe handler要使用的动态内存给释放掉了,是不是会引发异常?
以上就是我的疑惑,那么到底是不是这样呢?实际上面我也给出了答案了,内核工程师足够优秀,做的足够稳定了。实际上unregister_kprobe在实现时就已经考虑到这种场景了。它在恢复了函数入口处的指令后,并没有立即退出,而是执行了一个函数synchronize_sched,该函数就是保证没有其他在运行的handler的一个关键所在。
那么此时你就更加疑惑了,synchronize_sched到底做了什么操作,它如何知道另外一个handler运行何时退出呢?
这个函数在静默期状态开始执行,并且会等待一个宽限期的时间后返回。所谓的静默期和宽限期,熟悉RCU的应该都多少有些了解,我这里就不做展开了,只针对我的问题场景。
首先需要明确的是synchronize_sched是属于RCU功能的其中一个API,它属于RCU的形式之一(Sched Flavor)。参考如下内核文档:
https://www.kernel.org/doc/Documentation/RCU/Design/Requirements/Requirements.html
内核对于他的实现由两种,一个针对UP架构上使用的,另一种是SMP架构上使用的,也就是多核心机器,现在基本上都是SMP系统了,去看一下相关的实现:
void synchronize_sched(void) { rcu_lockdep_assert(!lock_is_held(&rcu_bh_lock_map) && !lock_is_held(&rcu_lock_map) && !lock_is_held(&rcu_sched_lock_map), "Illegal synchronize_sched() in RCU-sched read-side critical section"); if (rcu_blocking_is_gp()) return; if (rcu_expedited) synchronize_sched_expedited(); else wait_rcu_gp(call_rcu_sched); } EXPORT_SYMBOL_GPL(synchronize_sched);对于sched RCU来说,这个同步操作会等待所有处于读临界区的操作。对于常规的RCU来说,我们可以根据rcu相关的API来找到临界区,而在sched RCU中,则是指的就是所有禁止了抢占的临界区(disable_preempt),因此很多API都可以成为一个RCU读临界区,比如local_irq_save() 、 preempt_disable() 等等以及包含有它们的一些函数。
上面解释了这么多,实际上都是做铺垫,保证kprobe不出问题的关键就是kprobe handler的执行是禁止抢占的,所以它属于上述的一个RCU读临界区。因此它的执行是可以由synchronize_sched来保证同步的。
参考:
https://www.kernel.org/doc/Documentation/RCU/Design/Requirements/Requirements.html https://manpages.debian.org/jessie-backports/linux-manual-4.8/synchronize_sched.9.en.html http://linuxperf.com/?p=38