今天遇到一个问题,需要探测内核中buffer cache block的大小。我想到了Kprobe这个神奇的工具,并且很好的探测到了内核中的变量值,非常的方便,在此分享一下。
 
采用dd等工具写设备的时候,是需要经过块设备层的buffer cache,当请求块大小小于buffer cache的block_size时,Linux的策略是首先需要从磁盘load数据至buffer cache,然后再将新写入的“局部数据”写入buffer cache。这一步骤完成之后,会将整个buffer cache标识成dirty,挂载到设备所属的radix tree上,然后定时唤醒后台writeback线程刷新dirty block至磁盘。今天对linux-3.2和linux-2.6.23的顺序写进行了对比测试,发现请求大小在512至2048之间时,Linux-3.2的性能居然比Linux-2.6.23还差。测试后得到的性能特征似乎与buffer cache的块大小有关系,因此,我采用Kprobe对两个版本的块大小进行了探测验证。
 
为了探测这个值,首先需要找一个合适的探测点,根据代码分析的结果,我选择在__block_write_begin函数中调用create_empty_buffers函数时的机会点,采用Kprobe插入一段代码,打印buffer cache block_size的值。探测点位置的源代码如下所示:
  1. int __block_write_begin(struct page *page, loff_t pos, unsigned len,  
  2.         get_block_t *get_block)  
  3. {  
  4.     。。。  
  5.  
  6.     blocksize = 1 << inode->i_blkbits;  
  7.     if (!page_has_buffers(page))  
  8.         create_empty_buffers(page, blocksize, 0);  
  9.     head = page_buffers(page);  
  10.  
  11.     bbits = inode->i_blkbits;  
  12.     block = (sector_t)page->index << (PAGE_CACHE_SHIFT - bbits);  
  13.     。。。  
通过上面函数,我们知道blocksize就是buffer cache block块大小,因此,我们可以截获create_empty_buffers函数之后,打印传入的第二个参数就可以得到buffer cache块大小值了。截获create_empty_buffers函数很简单,通过kallsyms_lookup_name函数或者/proc/kallsyms就可以得到截获函数对应的内存地址。关键的问题在于截获这个函数之后,我们如果得到他的第二个参数,这就关系到函数的参数传递问题了。
 
在X86_64平台上,Linux的参数传递通过如下9个寄存器完成,分别为:RDI,RSI,RDX,RCX,RAX,R8,R9,R10,R11。在pre_handler函数中,我们可以得到寄存器组变量,通过寄存器组变量我们可以通过RSI寄存器得到create_empty_buffers函数传入的第二个参数值。对于Linux-2.6.23版本,函数调用过程中寄存器在栈中布局定义如下:
  1. struct pt_regs {  
  2.     unsigned long r15;  
  3.     unsigned long r14;  
  4.     unsigned long r13;  
  5.     unsigned long r12;  
  6.     unsigned long rbp;  
  7.     unsigned long rbx;  
  8. /* arguments: non interrupts/non tracing syscalls only save upto here*/  
  9.     unsigned long r11;  
  10.     unsigned long r10;  
  11.     unsigned long r9;  
  12.     unsigned long r8;  
  13.     unsigned long rax;  
  14.     unsigned long rcx;  
  15.     unsigned long rdx;  
  16.     unsigned long rsi;  
  17.     unsigned long rdi;  
  18.     unsigned long orig_rax;  
  19. /* end of arguments */  
  20. /* cpu exception frame or undefined */  
  21.     unsigned long rip;  
  22.     unsigned long cs;  
  23.     unsigned long eflags;  
  24.     unsigned long rsp;  
  25.     unsigned long ss;  
  26. /* top of stack page */  
  27. }; 
对于Linux-3.2版本,寄存器的组织结构是相同的,但是名字定义有所差别,新版本的寄存器定义如下:
 
  1. struct pt_regs {  
  2.     unsigned long r15;  
  3.     unsigned long r14;  
  4.     unsigned long r13;  
  5.     unsigned long r12;  
  6.     unsigned long bp;  
  7.     unsigned long bx;  
  8. /* arguments: non interrupts/non tracing syscalls only save up to here*/  
  9.     unsigned long r11;  
  10.     unsigned long r10;  
  11.     unsigned long r9;  
  12.     unsigned long r8;  
  13.     unsigned long ax;  
  14.     unsigned long cx;  
  15.     unsigned long dx;  
  16.     unsigned long si;  
  17.     unsigned long di;  
  18.     unsigned long orig_ax;  
  19. /* end of arguments */  
  20. /* cpu exception frame or undefined */  
  21.     unsigned long ip;  
  22.     unsigned long cs;  
  23.     unsigned long flags;  
  24.     unsigned long sp;  
  25.     unsigned long ss;  
  26. /* top of stack page */  
  27. }; 
知道如何访问截获函数的输入参数之后,probe程序就可以很容易编写了,我的实验程序如下所示,仅供参考。
 
  1. /*  
  2.  * kprobe_jiffies.c  
  3.  */  
  4.  
  5. #include <linux/module.h> 
  6. #include <linux/kernel.h> 
  7. #include <linux/string.h> 
  8. #include <linux/init.h> 
  9. #include <linux/kprobes.h> 
  10. #include <linux/kallsyms.h> 
  11. #include "asm/ptrace.h"  
  12. #include "asm/current.h"  
  13. #include "linux/utsname.h"  
  14.  
  15. /* global probe object */  
  16. struct kprobe probe;  
  17.  
  18. /*  
  19.  * enter the probe pointer  
  20.  */  
  21. static int pre_probe(struct kprobe *probe, struct pt_regs *regs)  
  22. {  
  23.     printk("block_size = %d.\n", regs->si);  
  24.     return 0;  
  25. }  
  26. /*  
  27.  * exit the probe pointer  
  28.  */  
  29. static void post_probe(struct kprobe *probe, struct pt_regs *regs, unsigned long flags)  
  30. {}  
  31.  
  32. static int __init kprobe_ init(void)  
  33. {  
  34.     probe.pre_handler = pre_probe;  
  35.     probe.post_handler = post_probe;  
  36.  
  37.     probe.addr = (kprobe_opcode_t *) kallsyms_lookup_name("create_empty_buffers");  
  38.     if (probe.addr == NULL) {  
  39.         return 1;  
  40.     }  
  41.  
  42.     register_kprobe(&probe);  
  43.     printk("register probe driver.\n");  
  44.     return 0;  
  45. }  
  46.  
  47. static void __exit kprobe_exit(void)  
  48. {  
  49.     unregister_kprobe(&probe);  
  50.     printk("unregister probe driver.\n");  
  51.     return;  
  52. }  
  53.  
  54. module_init(kprobe_ init);  
  55. module_exit(kprobe_ exit);  
  56.  
  57. MODULE_AUTHOR("xxx");  
  58. MODULE_DESCRIPTION("kernel probe driver");  
  59. MODULE_LICENSE("GPL"); 

 

Kprobe的确是个非常不错的调试工具,我们可以不断地发掘他的功能,从而更好地调试、探测内核代码。