linux磁盘写操作实时跟踪

      事实上,我总是对linux开源社区的无名英雄们怀着无限的敬意,因此除了完成工作中需要的功能以外,首先想到的是分享,本篇文章以

GPL发布,在你转发的时候,请遵循GPL协议的规定,在此首先贴出GPL公共许可证,或许你会觉得这过于啰嗦,事实上这是必要的。请谅

解。为了不妨碍大家的阅读,在此我给出GPLv3的连接地址。 (GPLv3)

本文最初我发布在CSDN(http://blog.csdn.net/zhanleewo/article/details/6368405)

有人将这篇文章发布到了百度文库(http://wenku.baidu.com/view/a8d9fd25482fb4daa58d4b8c.html)

    下面进入正题,我打算分几个步骤来说明:

        第一磁盘写操作的过程分析;

        第二模块导出符号的利用;

        第三jprobe和kprobe介绍;

        第四磁盘写操作跟踪;

        最后还将给出一个简单的示例程序。

 

第一 磁盘写操作过程分析      

     在linux内核中,发生一次写操作,从调用write函数到数据发起一个写数据到具体块设备请求之间,大致需要以下几个过程。

 

    1.如果用户态调用了一个write函数,内核执行blkdev_file_write函数,如果不是direct io操作方式,那么执行buffered write操作

       过程,直接调用generic_file_buffered_write函数。Buffered write操作方法会将数据直接写入Cache,并进行Cache的替换操

       作,在替换操作过程中需要对实际的快设备进行操作,address_space->a_ops提供了块设备操作的方法。当数据被写入到Cache之

       后,write函数就可以返回了,后继异步写入的任务绝大部分交给了pdflush daemon(有一部分在替换的时候做了)。

 

    2.读操作在没有命中Cache的情况下通过address_space_operations方法中的readpage函数发起块设备读请求;写操作在替换

       Cache或者Pdflush唤醒时发起块设备请求。发起块设备请求的过程都一样,首先根据需求构建bio结构,bio结构中包含了读写地址、

       长度、目的设备、回调函数等信息。构造完bio之后,通过简单的submit_bio函数将请求转发给具体的块设备。从这里可以看出,块设

       备接口很简单,接口方法为submit_bio(更底层函数为generic_make_request),数据结构为struct bio。

 

    3.submit_bio函数通过generic_make_request转发bio,generic_make_request是一个循环,其通过每个块设备下注册的

       q->make_request_fn函数与块设备进行交互。如果访问的块设备是一个有queue的设备,那么会将系统的__make_request函数

       注册到q->make_request_fn中;否则块设备会注册一个私有的方法。在私有的方法中,由于不存在queue队列,所以不会处理具体

       的请求,而是通过修改bio中的方法实现bio的转发,在私有make_request方法中,往往会返回1,告诉generic_make_request继

       续转发比bio。generic_make_request的执行上下文可能有两种,一种是用户上下文,另一种为pdflush所在的内核线程上下文。

 

    4.接下来generic_make_request再往下发就该到驱动层了。这里不属于我们讨论的范畴了

 

    我们需要监控所有要经过generic_make_request发起到驱动的bio,因此只有在bio被产生或者submit_bio的位置去拦截,读到这里,你能够想明白这个事情就足够了。

 

 

第二 模块导出符号的利用

    模块函数是可以用EXPORT_SYMBOL宏导出的,其本来的目的是导出之后便于模块与模块之间的代码重用,已经模块间通讯。在这里我

们要讨论其另外一个用法,模块函数被导出之后,我们可以根据模块名称获取到模块函数对应的位置偏移量。这个值也就是这个模块函数的

首地址。如果我们要想利用submit_bio,那首先就要保证submit_bio函数的符号是被导出来了的。事实上我们查看linux内核源代码(blk-core.c:Line1620):

 1 /** 
 2  * submit_bio - submit a bio to the block device layer for I/O 
 3  * @rw: whether to %READ or %WRITE, or maybe to %READA (read ahead) 
 4  * @bio: The &struct bio which describes the I/O 
 5  * 
 6  * submit_bio() is very similar in purpose to generic_make_request(), and 
 7  * uses that function to do most of the work. Both are fairly rough 
 8  * interfaces; @bio must be presetup and ready for I/O. 
 9  * 
10  */  
11 void submit_bio(int rw, struct bio *bio)  
12 {  
13         int count = bio_sectors(bio);  
14         bio->bi_rw |= rw;  
15         /* 
16          * If it's a regular read/write or a barrier with data attached, 
17          * go through the normal accounting stuff before submission. 
18          */  
19         if (bio_has_data(bio) && !(rw & REQ_DISCARD)) {  
20                 if (rw & WRITE) {  
21                         count_vm_events(PGPGOUT, count);  
22                 } else {  
23                         task_io_account_read(bio->bi_size);  
24                         count_vm_events(PGPGIN, count);  
25                 }  
26                 if (unlikely(block_dump)) {  
27                         char b[BDEVNAME_SIZE];  
28                         printk(KERN_DEBUG "%s(%d): %s block %Lu on %s (%u sectors)/n",  
29                         current->comm, task_pid_nr(current),  
30                                 (rw & WRITE) ? "WRITE" : "READ",  
31                                 (unsigned long long)bio->bi_sector,  
32                                 bdevname(bio->bi_bdev, b),  
33                                 count);  
34                 }  
35         }  
36         generic_make_request(bio);  
37 }  
38 EXPORT_SYMBOL(submit_bio);

 

 

    确实这个submit_bio函数是被导出了符号的,EXPORT_SYMBOL(submit_bio);或许你觉得这来的太轻松了,事实上要分析到这 一步真的需要很多耐心的,所有磁盘读写操作最终要真正的往磁盘上写文件,所有真实要到达磁盘的数据bio都必将经过这个函数。虽然是导出来符合,我们能直 接调用,可是直接调用似乎不能达到我们想要的效果获取到这个bio结构。反而要我们传给它一个bio结构。对这个函数如何处理呢?

    正如你所想的那样,我们如果能在这个函数执行的时候插入一个函数到这里拦截就对了,就像c语言的setjmp和longjmp那样工作。可是在编译过了的linux内核中能实现吗?答案是肯定的。这就是下一节所讲的kprobe机制。

 

第三 jprobe和kprobe介绍

 

    顾名思义probe就是探头的意思,即是说在函数f1调用的时候,设置一个探头f2到这个函数f1的位置,并且获取到f1的参数,抛给f2,然

后,跳到f2的位置执行,完了之后再回到到f1执行。

    更多关于kprobe的介绍请自行google查找,在linux内核的sample目录下也有一个使用的例子。这里只做简单的介绍,其功能简单说,

就是在函数被调用的时候,能拿到这个函数的参数,做一些处理。而如果我们设置一个jprobe到submit_bio这个函数上,那么我们就可以

获取到bio结构的信息了。jprobe结构定义如下:

   

1     struct jprobe {  
2         struct kprobe kp;  
3         void *entry;  
4     }  

 

    那么总结一下设计思路,一句话,构建一个jprobe探头插入到submit_bio处。事实上要得出这句话的结论要经历很多伤感的事情。哈

哈。

   kprobe结构中有两个重要的成员symbol_name, addr,symbol_name就是那个函数的符号,这里就应该是submit_bio的符号。

addr就是这个函数的地址,就是内核函数kallsyms_lookup_name("submit_bio")返回的值。需要注意的是在jprobe结构中的kprobe

只能是addr或是symbol_name其中一个填入了值,如果两个都填入,在注册这个探头的时候就会出现错误-21非法符号。举例说明,如果

addr为0x1000000,那么symbol_name就该是NULL;如果symbol_name为"submit_bio"那么addr就该为NULL;不能是两个都是有

效的数据。具体设置哪一个值,根据内核版本而定。(很多发型版本并没有导出kallsyms_lookup_name).

 

    jprobe的另一个成员是entry,这就是我们自己定义的那个探头程序。

    有一点必须说明,就是注册进去的探头程序应该和被注册的函数的参数列表一致,比如void submit_bio(int rw, struct bio * bio)

那么注册进去的探头程序也该是 void submit_bio_probe(int rw, struct bio * bio).

 

    因此对于submit_bio这个函数,要想注册一个jprobe探头函数给它,那么这个jprobe结构就应该类似于下面这个样子。

   

1     struct jprobe submit_bio_jprobe {  
2         .entry = (kprobe_opcode_t *) submit_bio_jprobe,  
3         kp = {  
4             .addr = NULL, .symbol_name = "submit_bio"  
5         }  
6     };  

 

 

 

第四 磁盘写操作跟踪

    磁盘操作的跟踪,逻辑已经很清楚了。那么具体如何去做?

   首先这是linux内核编程,因此我们需要写一个模块,当然你也可以傻到去修改内核,实现一个系统调用。这里我们按照正常人的思维去

做。在实现的模块中,初始化的时候把这个探头程序注册进去,然后在模块释放的时候卸载这个探头程序。注册探头程序用

register_jprobe,卸载用unresister_jprobe仅此而已。似乎讲到了这里的时候,你会发现这是一件特别简单的事情了。内核调用也不会

超过10个。也实在没有必要再详细的说下去了,这里给出一个简单的实例程序,其功能是打印出,哪个设备的那一个扇区之后的多少个扇区

发生了写操作。打印的格式为:

 

    device: dm-0,    command: write,    start10240,    count: 8

 

    其中device为对应的设备名,command为动作,是读还是写, start为动作发生的起始扇区, count是start这个位置之后的多少个扇

会发生command类型的操作。

    下面给出源代码和makefile文件

 

    dwm_mod.c

   

 1 #include <linux/kernel.h>  
 2 #include <linux/module.h>  
 3 #include <linux/kprobes.h>  
 4 #include <linux/bio.h>  
 5   
 6 static void submit_bio_probe(int rw, struct bio * bio) {  
 7         if(bio && bio->bi_io_vec != NULL) {  
 8                 char b[BDEVNAME_SIZE];  
 9                 printk(KERN_INFO "device: %s, command: %s, start: %10lld, count: %d /n",  
10                         bdevname(bio->bi_bdev, b), rw & WRITE ? "write" : "read",  
11                                 bio->bi_sector, bio_sectors(bio));  
12         }  
13         jprobe_return();  
14 }  
15   
16 static struct jprobe my_jprobe = {  
17         .entry = (kprobe_opcode_t *) submit_bio_probe,  
18         .kp = {  
19                 // can not set both addr and symbo_name  
20                 // either set addr or symbol_name  
21                 // if not -21 while retured  
22                 .addr = NULL, //(kprobe_opcode_t *) 0xc04e6e4,  
23                 .symbol_name = "submit_bio",  
24         },  
25 };  
26 static int __init my_init(void) {  
27         int ret = 0;  
28         printk(KERN_INFO "submit_bio jprobe module install.../n");  
29         ret = register_jprobe(&my_jprobe);  
30         if(ret < 0) {  
31                 printk(KERN_INFO "register_jprobe failed, returned %d/n", ret);  
32                 return ret;  
33         }  
34         printk(KERN_INFO "Planted jprobe at %p, handler addr %p/n",  
35                 my_jprobe.kp.addr, my_jprobe.entry);  
36         return ret;  
37 }  
38 static void __exit my_exit(void) {  
39         printk(KERN_INFO "submit_bio jprobe module uninstall.../n");  
40         unregister_jprobe(&my_jprobe);  
41         printk(KERN_INFO "jprobe at %p unregistered/n", my_jprobe.kp.addr);  
42 }  
43 module_init(my_init);  
44 module_exit(my_exit);  
45 MODULE_LICENSE("GPL"); 

 


 

   Makefile:

    ifneq ($(KERNELRELEASE),)  
            obj-m := dwm_mod.o  
    else  
            KDIR := /lib/modules/$(shell uname -r)/build  
            PWD := $(shell pwd)  
    default:  
            $(MAKE) -C $(KDIR) M=$(PWD) modules  
    endif  

 

   大家可以编译运行,看看运行结果。编译的时候,如果你的linux系统没有安装内核开发环境,请先安装。

   我的测试系统是CentOS 5.1,内核版本是2.6.18-238.9.1.el5

 

   【注】:第一磁盘写操作的过程分析,我是在chinaunix blog http://blogold.chinaunix.net/u3/103428/showart_2471002.html

博主吴栓的博客中看到的,在此感谢博主分析了磁盘读写的整个过程。事实上我发现很多地方都有这一篇文章,我无法确定真正发这篇博客的作者是谁,也无法与你联系。原作者发现我有侵权行为,请至邮件到zhanleewo@gmail.com.我将第一时间与你取得联系。

posted @ 2013-04-27 00:53  Kappia  阅读(1653)  评论(0编辑  收藏  举报