在 Linux 里有很多工具可以用于追踪和记录内核运行期间发生的事件,例如 trace 和 ftrace 等工具提供了一种机制来追踪和记录在内核运行期间发生的事件,有助于理解系统的行为和性能瓶颈。内存流动(MEMORY FLUID)工具是专门为 BiscuitOS 系统开发,用于追踪和记录内存在内核运行期间发生的事件. 如果把其他追踪工具比喻为在一些特定点进行守株待兔,那么内存流动工具就是在兔子头上安装一个摄像头,可以看到内存在内核运行期间的所有事件. 那么接下来通过一个例子进行实例介绍:
上图是一个经典采用惰性分配的匿名内存,main 函数里 21 行调用 mmap 函数分配内存时,系统只为其分配了虚拟内存,并没有分配具体的物理内存,这就是所谓的惰性分配,只有在进程真正使用这段虚拟内存时,系统才会为其分配物理内存。那么 main 函数 32 行首次对虚拟内存进行访问,此时硬件 MMU 发现虚拟内存并没有映射物理内存,那么直接触发缺页异常。在缺页异常处理函数里,内核为其分配物理内存并建立页表,将虚拟内存映射到物理内存上,那么待缺页异常返回之后,进程再次执行发送缺页异常的指令,此时进程可以正确访问到内存. 以上就是一个最基础的惰性分配内存的过程,那么对于开发者来说,如果想看匿名内存全部缺页的整个过程该怎么办呢? 如果能看到是不是对开发者学习匿名内存缺页有极大的帮助(对于初学者来说黑盒永远有无限种恐惧),接下来先展示使用内存流动工具查看匿名内存发生缺页的完整过程:
上图就是通过内存流动工具追踪到匿名内存发生缺页的整个过程,图片不是内存流动工具制作的,而是根据内存流动工具追踪到的数据绘制出来的,如果开发者使用内存流动工具将一个匿名内存的缺页过程完整的追踪出来,并且制作成一个完整的调用图,那么这对匿名内存缺页原理的学习起到有效的帮助,让原本抽象难懂的技术变得直观易懂. 话不多说,接下来将介绍如何在 BiscuitOS 上使用内存流动工具.
测试用例是一个用户空间应用程序,其在 22 行调用 mmap 系统调用分配了一段匿名内存,内存只包含了虚拟内存部分,并没有映射物理内存,那么在 33 行首次访问虚拟内存时,MMU 发现物理内存不存在而直接触发缺页异常,缺页异常处理函数负责物理内存的分配和页表的建立,最后缺页异常返回进程可以正确访问到虚拟内存. 此时可以使用内存流动工具查看匿名内存的缺页过程,可以知道应用程序在 33 行处发生了缺页,那么首先在应用程序进行标记:
应用程序要使用内存流动工具检测某个内存行为时,需要在该行为的前后加上 syscall(600, 1); 和 syscall(600, 0); 将检测的区域标记出来,这样内存流动工具才知道检测的范围, 接下来是在内核里做标记.
这里先假设已经知道匿名内存缺页流程会调用 do_anonymous_page 函数,那么在 do_anonymous_page 函数的 4040 行添加打印,因此缺页匿名发生缺页时流动到这个函数,那么接下来在 BiscuitOS 上进行实践:
BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本,脚本里包含了实践案例运行的所有命令,可以看到实践案例运行之后,内核把刚刚添加的 bs_debug 语句打印了,那么说明匿名内存在缺页异常处理里确实流动到 do_anonymous_page 这个函数了,并且此时打印字符串显示的缺页地址正好是应用程序访问的地址 “0x6000000000”,以上便是内存流动工具的基本用法, 让开发者看到了内存流动到了 do_anonymous_page 这个函数,采用同样的方法可以追踪匿名内存在整个内核里的流动. 到这里可能有的童鞋就会说内核里使用的 bs_debug 不就是一个 printk 这类的函数,为什么不直接用 printk,这里我先不做解释直接动手:
根据需求,将 do_anonymous_page 函数里的 bs_debug 替换成 printk,然后重新编译内核是运行实践案例:
当 BiscuitOS 启动之后调用用户空间的程序之后,内核疯狂输出在 do_anonymous_page 函数添加的 printk 字符串,接着运行 RunBiscuitOS.sh 脚本,还是疯狂打印字符串,都无法看清哪行是和实践案例匿名内存缺页有关. 通过这个例子的对比,就可以知道内存流动工具是为了让你“看到”指定的内存在内核里的流动.