async函数跟踪总结
一些参考
关于rust async的很好的介绍:https://night-cruise.github.io/async-rust/
async rust debugging的tracking issue:https://github.com/rust-lang/rust/issues/73522
async函数的跟踪思路
stacktrace
在正在执行的async函数里,可以用栈帧直接进行stacktrace,与正常函数一样。如果想要完整的调用链可以引入debuginfo获取inline函数的信息。
得到的符号和一般函数会有区别,函数主体的符号会变成闭包,也会出现poll函数的符号以及使用的async runtime的相关函数。
如果想增强这些符号的可读性,可以加入编译选项-Csymbol-mangling-version=v0
,这个RFC也会让其他函数的符号更加可读。
在最新的编译器版本里,async函数生成时不再经过一层Genfuture,从而stacktrace更加简洁(https://github.com/rust-lang/rust/pull/104321)。
如果要跟踪已经yield的async函数就不能通过栈追踪的方式了。栈里是没有挂起的协程的执行信息的。追踪挂起的函数有几种思路:
静态追踪点
在async函数代码里插入静态追踪点是最直接的方法。tokio的async-backtrace就是这么实现的。给每个async函数用宏在外面套一层async函数,在poll的时候就可以通过自己套的async函数里的poll获取结果,汇报给全局tracer处理即可。在async函数上加入对应的跟踪宏即可自动汇报他们的执行情况。
但是从kprobe的角度来看,我们希望能够不修改代码,仅依赖编译器给出的信息定位async函数的执行流进行插桩,也就是动态的跟踪一个async函数是否执行完毕,是否在Yield中。
在目前的编译器的编译结果下,有几种思路:
跟踪poll函数
对poll函数进行插桩,用retprobe截取返回值理论上是可以判断一个future是否完成的,但是有几个问题:
-
使用.await生成的poll函数会生成比较复杂的符号,生成的结果还和具体async实现有关,需要手动去dwarf里查找,很难直接从原本的async函数名里获取(或者说,编译器不会emit这类信息),之前静态插桩就是通过插入代码获取了这个信息。
-
.await生成的poll函数很可能被inline优化掉,retprobe无法获取返回值,获取的信息有限。
-
对于非leaf的future,只能判断他是否完成,无法知道状态机的具体状态,而递归插桩子future还是需要编译器的信息。
如果想获取更多的信息,需要深入到函数的具体执行流里,也就是尝试通过跟踪闭包来实现。
跟踪生成的闭包
我们可以从DWARF里找到async/await生成的struct:
struct example::what::{async_fn_env#0}
size: 3
members:
0[1] __state: u8
0[3] <variant part>
Unresumed: <0>
Returned: <1>
Panicked: <2>+
Suspend0: <3>
0[1] <padding>
1[2] __awaitee: struct example::barrr::{async_fn_env#0}
Suspend1: <4>
0[1] <padding>
1[1] __awaitee: struct example::baz::{async_fn_env#0}
2[1] <padding>
对应(旧版)编译器里的逻辑:
/// Desugar `<expr>.await` into:
/// ```ignore (pseudo-rust)
/// match ::std::future::IntoFuture::into_future(<expr>) {
/// mut __awaitee => loop {
/// match unsafe { ::std::future::Future::poll(
/// <::std::pin::Pin>::new_unchecked(&mut __awaitee),
/// ::std::future::get_context(task_context),
/// ) } {
/// ::std::task::Poll::Ready(result) => break result,
/// ::std::task::Poll::Pending => {}
/// }
/// task_context = yield ();
/// }
/// }
/// ```
这也是为了改善async函数的debug支持而对编译器做的改动之一https://github.com/rust-lang/rust/pull/95011。
每个Suspend就是一句await语句,awaitee就对应了子future。所以可以从debuginfo里解析出代码里future的依赖关系。
有依赖树以后可以考虑根据闭包的执行情况跟踪。不过仍然有几个问题:
- 这个结构仅限于await语句,如果使用select!或者join!等宏或是手动实现Future的话,会构造新的future而不是使用await,这样的await树可能会断开。
- 用trait抽象出的awaitee是无法通过静态分析获取的。
- 如果一个closure被inline的话仍然没法通过retprobe知道具体的执行情况。
所以这么解析出的依赖关系也不是完整的,就算是完整的也没办法直接追踪。
写了一个小工具https://github.com/cubele/rust-async-tree-parser用来可视化这个依赖树,以zCore为例:
可以看到read相关的系统调用里面await的future被抽象了,所以依赖树找不到实体的future。
再比如join!的情况:
被join的await逻辑和join本身分离了。
所以说async函数根本的执行情况还是要通过poll获取的。
仅跟踪leaf future
__awaitee可以帮助我们找到.await生成的结构,但会在一些没有使用await的Future处断开。但是可以发现断开的地方肯定是代码(而不是编译器通过.await)实现了Future对应的poll函数的,所以这些"leaf"的poll函数是可以直接插桩的。也就是说我们可以退一步,不去自顶向下的关注async函数的整体流程,而是观测一些关键的poll函数。因为async真正的异步一般都出现在这种底层Future里,这样跟踪已经可以获取足够的信息了。
既然我们直接跟踪具体实现的一个poll函数,插桩的时候就没有之前的问题了。不过获取返回值的话可能还是需要no_inline。
而如果关注的不是Future而是async函数本身的执行,可以直接在函数生成的闭包上插桩,甚至可以直接用line2addr定位具体的一句话插桩。
换一个角度
既然我们看到了这个struct的结构,如果能在堆上找到他的话就可以直接知道async函数的执行情况了。
这么做还有一个好处:可以根据struct的地址区别不同的协程实例,更方便统计执行情况。
我能想到的方法只有用jprobe捕获poll函数的入参,不过这也是很困难的,一是之前说的async生成的poll函数并不好定位,二是rust里面的jprobe看起来会比C的实现难很多。
一个例子
zCore中的SleepFuture的poll函数没有被inline,直接可以在符号表里找到<kernel_hal::common::future::SleepFuture as core::future::future::Future>::poll
,插桩以后可以看到Waker触发前后两次poll分别返回Pending和Ready。
总结
如果是作为debug手段的话,用async-backtrace这个库插入静态追踪点肯定是最好的办法。而动态跟踪async函数具体的执行情况情况比较困难,根本原因是编译器没有做相关的debuginfo。但是可以通过解析编译器emit的awaitee信息找到找到leaf future,定位其中显式实现的poll函数,对这些poll进行probe,与插桩closure结合判断执行过程。
从另一个角度看,.await生成的poll依赖并不是probe动态跟踪最关心的部分,关键的是可能阻塞的底层Future。所以没有必要对await生成的代码进行动态追踪(编译器目前也没有支持),在debug的时候用静态插桩就足够了。
如果真的要做到完整的动态插桩,加入编译器支持以后使用jprobe应该是最可行的做法。具体来说我们要知道async函数生成的Future里面的poll函数的地址以及具体的参数类型(状态机对应的struct结构),然后在对应的地址插入jprobe直接访问这个struct,从而每次poll以后struct更新的状态都可以直接看到。
现在不能实现的原因是:
-
目前这个struct的结构只有在编译后才可以看到,可能要用别的手段实现jprobe的handler
-
async函数生成的poll函数的地址在debuginfo里面不太好找,需要编译器支持
-
rust想要实现jprobe可能会比较复杂。
因为async rust并不成熟,相关的编译流程以及debug支持都在不断的改进中。debuginfo可以关注tracking issuehttps://github.com/rust-lang/rust/issues/73522,async函数的编译过程改进可以看看https://swatinem.de/blog/improving-async-codegen/。