今天遇到一個問題,需要探測內核中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的值。探測點位置的源代碼如下所示:
int __block_write_begin(struct page *page, loff_t pos, unsigned len, get_block_t *get_block) { 。。。 blocksize = 1 << inode->i_blkbits; if (!page_has_buffers(page)) create_empty_buffers(page, blocksize, 0); head = page_buffers(page); bbits = inode->i_blkbits; block = (sector_t)page->index << (PAGE_CACHE_SHIFT - bbits); 。。。 }
通過上面函數,我們知道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版本,函數調用過程中寄存器在棧中布局定義如下:
struct pt_regs { unsigned long r15; unsigned long r14; unsigned long r13; unsigned long r12; unsigned long rbp; unsigned long rbx; /* arguments: non interrupts/non tracing syscalls only save upto here*/ unsigned long r11; unsigned long r10; unsigned long r9; unsigned long r8; unsigned long rax; unsigned long rcx; unsigned long rdx; unsigned long rsi; unsigned long rdi; unsigned long orig_rax; /* end of arguments */ /* cpu exception frame or undefined */ unsigned long rip; unsigned long cs; unsigned long eflags; unsigned long rsp; unsigned long ss; /* top of stack page */ };
對於Linux-3.2版本,寄存器的組織結構是相同的,但是名字定義有所差別,新版本的寄存器定義如下:
struct pt_regs { unsigned long r15; unsigned long r14; unsigned long r13; unsigned long r12; unsigned long bp; unsigned long bx; /* arguments: non interrupts/non tracing syscalls only save up to here*/ unsigned long r11; unsigned long r10; unsigned long r9; unsigned long r8; unsigned long ax; unsigned long cx; unsigned long dx; unsigned long si; unsigned long di; unsigned long orig_ax; /* end of arguments */ /* cpu exception frame or undefined */ unsigned long ip; unsigned long cs; unsigned long flags; unsigned long sp; unsigned long ss; /* top of stack page */ };
知道如何訪問截獲函數的輸入參數之後,probe程序就可以很容易編寫了,我的實驗程序如下所示,僅供參考。
/* * kprobe_jiffies.c */ #include <linux/module.h> #include <linux/kernel.h> #include <linux/string.h> #include <linux/init.h> #include <linux/kprobes.h> #include <linux/kallsyms.h> #include "asm/ptrace.h" #include "asm/current.h" #include "linux/utsname.h" /* global probe object */ struct kprobe probe; /* * enter the probe pointer */ static int pre_probe(struct kprobe *probe, struct pt_regs *regs) { printk("block_size = %d.\n", regs->si); return 0; } /* * exit the probe pointer */ static void post_probe(struct kprobe *probe, struct pt_regs *regs, unsigned long flags) {} static int __init kprobe_ init(void) { probe.pre_handler = pre_probe; probe.post_handler = post_probe; probe.addr = (kprobe_opcode_t *) kallsyms_lookup_name("create_empty_buffers"); if (probe.addr == NULL) { return 1; } register_kprobe(&probe); printk("register probe driver.\n"); return 0; } static void __exit kprobe_exit(void) { unregister_kprobe(&probe); printk("unregister probe driver.\n"); return; } module_init(kprobe_ init); module_exit(kprobe_ exit); MODULE_AUTHOR("xxx"); MODULE_DESCRIPTION("kernel probe driver"); MODULE_LICENSE("GPL");
Kprobe的確是個非常不錯的調試工具,我們可以不斷地發掘他的功能,從而更好地調試、探測內核代碼。