什么是page fault
在Linux内核中,每一个进程都有一个独立的虚拟地址空间,而进程本身感知不到真正的物理内存的存在(比如某进程感知到的内存是连续的,但是实际上它被分配的内存是物理内存中分散的空间)。MMU(内存管理单元)负责完成对于这种虚拟地址-物理内存地址的转换工作。Linux为每一个进程维护了一张页表,用于记录虚拟地址和物理内存地址之间的关系,并在进程运行时实时进行地址转换从而使得进程访问到真正的物理内存。
图1 内存管理单元MMU
而我们要提到的page fault便是在这种场景下产生的,page fault大致可以分为三类:
- major page fault:进程要访问的页面不在真正的物理内存中,可能需要从磁盘中载入(比如Linux开启了swap机制,内核通过LRU页面置换算法将页面置换到了磁盘中暂存),这个时候将会产生一个major page fault。
图2 swap页面置换机制
- minor page fault:进程要访问的页面在物理内存中,但是MMU还没有建立进程的虚拟地址和相应物理地址的映射关系,这个时候就会触发soft page fault(比如说我们使用malloc函数在堆上申请内存,在进程不访问这片虚拟内存之前它的物理内存页是不会被实际分配的,当进程第一次访问使用malloc分配的内存空间时,由于会分配物理内存,这时候只需要再建立虚拟内存-物理内存的映射关系即可)。
- invalid fault:就是segment fault,进程的内存越界。
在kindling中,我们重点关注前两种page fault,事实上,结合实际需求,我们其实往往只需要关注major page fault即可,因为minor page fault从某种意义上来说并不算是一种"fault",它是内核中普遍且常见的一种现象,而major page fault的出现往往意味着系统开始我们内存可能不太够用了,这时候我们就需要重点关注是哪些线程触发了这些major page fault,从而帮助我们更好的定位和排查问题。
有关page fault的tracepoint
Linux内核为page fault提供了tracepoint,/sys/kernel/debug/tracing/events/exceptions里面有相关的tracepoint结构体描述,/sys/kernel/debug/tracing/events/exceptions分为pagefaultkernel和pagefaultuser,对应于内核和用户态的page fault(内核也可能page fault,比如copyfromuser的时候)。我们打开里面的format文件可以看到如下结构:
name: page_fault_kernel
ID: 115
format:
field:unsigned short common_type; offset:0; size:2; signed:0;
field:unsigned char common_flags; offset:2; size:1; signed:0;
field:unsigned char common_preempt_count; offset:3; size:1; signed:0;
field:int common_pid; offset:4; size:4; signed:1;
field:unsigned long address; offset:8; size:8; signed:0;
field:unsigned long ip; offset:16; size:8; signed:0;
field:unsigned long error_code; offset:24; size:8; signed:0;
print fmt: "address=%pf ip=%pf error_code=0x%lx", (void *)REC->address, (void *)REC->ip, REC->error_code
name: page_fault_user
ID: 116
format:
field:unsigned short common_type; offset:0; size:2; signed:0;
field:unsigned char common_flags; offset:2; size:1; signed:0;
field:unsigned char common_preempt_count; offset:3; size:1; signed:0;
field:int common_pid; offset:4; size:4; signed:1;
field:unsigned long address; offset:8; size:8; signed:0;
field:unsigned long ip; offset:16; size:8; signed:0;
field:unsigned long error_code; offset:24; size:8; signed:0;
print fmt: "address=%pf ip=%pf error_code=0x%lx", (void *)REC->address, (void *)REC->ip, REC->error_code
这个结构里面有个error_code的字段,含义如下:
- error_code:当异常发生时,硬件压入栈中的错误代码。
- 当第0位被置0时,则异常是由一个不存在的页所引起的,否则是由无效的访问权限引起的。
- 如果第1位被置0,则异常由读访问或者执行访问所引起,如果被设置,则异常由写访问引起。
如果第2位被置0,则异常发生在内核态,否则异常发生在用户态。
page fault事件可观测性实现方案
方案一 基于switch等事件
可在switch,execeve,fork,vfork,clone这些事件中引入pgftmaj,pgftmin等参数,区别于tracepoint的方式,其pgftmaj和pgftmin是通过内核的taskstruct结构体中的参数获取的。以switch为例,这种实现方式会在线程进行上下文切换时获取pgftmaj&pgft_min的值,这样做的好处是可以实时监测到每一次线程调度时的page fault的变化情况,对于单进程来说,这样的实现方式是没有太大问题的。但是在目前kindling的实现中,由于probe和collector之间通过unix socket domain来通信,经过文件系统进行跨进程的数据传输,经测试,通过文件系统进行跨进程通信对于大量的switch事件的性能敏感度是非常高的。
方案二 基于page fault事件
在kindling中,我们尝试在pagefaultuser和kernel的tracepoint点中获取进程结构体信息,从而实现这一部分功能,由于page_fault只在产生页错误时触发,因此事件数量得到了控制,避免了过多事件导致的性能问题。
图3 page fault可观测性实现原理
总结
为了兼顾性能,我们选择了基于page fault tracepoint的方式来实现,并在其中通过内核task_struct补充所需参数的信息。整个机制以事件为驱动,只有当内核产生了page fault事件时才会对数据进行获取。我们可以通过page fault信息(尤其是major page fault)帮助我们定位和排查问题。
Kindling是一款基于eBPF的云原生可观测性开源工具,旨在帮助用户更好更快的定界云原生系统问题,并致力于打造云原生全故障域的定界能力。欢迎这方面的使用者和爱好者与我们联系。
加入我们
关注我们
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。