接着上一篇 Linux Kernel - 探索Ram Disk 驱动(1) - 体验使用 , 我们来看一看Ram Disk驱动的实现.
代码源文件位于: drivers/block/brd.c, 总共不到700行, 至少从数字上来说, 看起来不麻烦. 在看代码之前, 我们可以思考一下磁盘设备应该具有的功能, 这样带着问题思考可能更有效率. 对于块设备来说, 无非就是读取一个块, 写入数据到一个块. 因此, 按照这个功能组织来分析代码, 基本就能了解个大概了. 要找到读写函数, 有两种方式, 我们可以直接按直觉搜索带read和write字眼的函数, 也可以从模块入口按图索骥. 不过直接找函数多少有点靠运气的感觉, 还是按一般思路来, 从模块初始化看起更好点.
对于驱动模块, 要找到其入口, 只要找到module_init就可以了, 在文件的末尾, 发现目标:
module_init(brd_init); module_exit(brd_exit);一般来说, 要找模块入口一般直接拖到文件最后就行. 为什么入口放在最后呢, 因为这样方便阅读, 那为什么一定要放在末尾而不是放在开头呢, 因为C语言里要引用一个符号必须先遇到声明或者定义, 如果写在开头, 就必须先写几行入口和出口函数声明, 然后是模块入口和出口的引用, 然后跟着入口函数和出口函数的定义; 而如果直接将出入口引用放在末尾的话, 就可以省去声明, 因为编译器在遇到引用之前已经发现出入口函数的定义了. —以上纯属个人推理, 请自行判断, ^-^
来看module_init(brd_init), 找到brd_init函数:
static int __init brd_init(void) { struct brd_device *brd, *next; int i; //.... //lqp comment: 注册RAMDISK_MAJOR(值为1)的为ramdisk的主设备号 if (register_blkdev(RAMDISK_MAJOR, "ramdisk")) return -EIO; if (unlikely(!max_part)) max_part = 1; //lqp comment: 预分配brd设备 for (i = 0; i < rd_nr; i++) { brd = brd_alloc(i); if (!brd) goto out_free; list_add_tail(&brd->brd_list, &brd_devices); } /* point of no return */ //lqp comment: add_disk把预分配的disk注册到系统磁盘管理层 list_for_each_entry(brd, &brd_devices, brd_list) add_disk(brd->brd_disk); /*lqp comment: 这里注册整个RAMDISK_MAJOR的区域, 根据上下文的注释来看, 通过(RAMDISK_MAJOR, X)来访问brd设备的话, 如果X的值位于预分配的范围则 直接从预分配的设备中读取, 否则从brd_probe中分配, 对于我们研究brd操作来说, 只需要分析brd_alloc即可 */ blk_register_region(MKDEV(RAMDISK_MAJOR, 0), 1UL << MINORBITS, THIS_MODULE, brd_probe, NULL, NULL); pr_info("brd: module loaded\n"); return 0; //.... }下面看brd_alloc函数:
static struct brd_device *brd_alloc(int i) { struct brd_device *brd; struct gendisk *disk; //.... //lqp comment: 这里给brd_queue设置处理请求队列的回调brd_make_request //这个函数需要重点关注 blk_queue_make_request(brd->brd_queue, brd_make_request); blk_queue_max_hw_sectors(brd->brd_queue, 1024); blk_queue_bounce_limit(brd->brd_queue, BLK_BOUNCE_ANY); //.... disk = brd->brd_disk = alloc_disk(max_part); if (!disk) goto out_free_queue; disk->major = RAMDISK_MAJOR; disk->first_minor = i * max_part; /*lqp comment: 这里把brd_fops和disk对象关联, brd_fops是一个函数指针表, 里面的函数是一组回调接口, 需要重点关注 */ disk->fops = &brd_fops; disk->private_data = brd; /*lqp comment: 这里将brd_queue和disk进行了关联*/ disk->queue = brd->brd_queue; disk->flags = GENHD_FL_EXT_DEVT; sprintf(disk->disk_name, "ram%d", i); set_capacity(disk, rd_size * 2); return brd; //.... }从上面的代码可以发现在两个地方将disk对象和函数回调进行了关联:
可以发现, 里面也关联了一个读写接口: brd_rw_page, 和brd_make_request函数一样, 这个接口内部同样是调用brd_do_bvec来实现读写. 那么我们的问题就变得简单了, 只要分析brd_do_bvec函数就可以了.
上的代码需要关注的地方主要是copy_to_brd_setup, 在这里会分配和维护brd设备的数据区
static int copy_to_brd_setup(struct brd_device *brd, sector_t sector, size_t n) { /*lqp comment:计算page内偏移, sector是扇区号, 一个扇区对应512字节; 而一个page一般是4096字节 */ unsigned int offset = (sector & (PAGE_SECTORS-1)) << SECTOR_SHIFT; size_t copy; /*lqp comment: 下面出现两次brd_insert_page的情况可能在写的数据横跨两个 page的时候发生, brd_insert_page内会检查sector对应的page是否存在, 不存在 则创建*/ copy = min_t(size_t, n, PAGE_SIZE - offset); if (!brd_insert_page(brd, sector)) return -ENOSPC; if (copy < n) { sector += copy >> SECTOR_SHIFT; if (!brd_insert_page(brd, sector)) return -ENOSPC; } return 0; }再看看brd_insert_page:
static struct page *brd_insert_page(struct brd_device *brd, sector_t sector) { pgoff_t idx; struct page *page; gfp_t gfp_flags; //lqp comment: 检查是否已经存在了, 如果存在就直接返回 page = brd_lookup_page(brd, sector); if (page) return page; //.... //lqp comment: 分配一个内存物理页面 page = alloc_page(gfp_flags); if (!page) return NULL; //.... /*lqp comment: 将分配的页插入brd_pages的树中, 可以看到, 树的索引是page号, 对应 idx = sector >> PAGE_SECTORS_SHIFT; */ spin_lock(&brd->brd_lock); idx = sector >> PAGE_SECTORS_SHIFT; page->index = idx; /*lqp comment: brd设备内部采用radix_tree来管理数据page*/ if (radix_tree_insert(&brd->brd_pages, idx, page)) { __free_page(page); page = radix_tree_lookup(&brd->brd_pages, idx); BUG_ON(!page); BUG_ON(page->index != idx); } spin_unlock(&brd->brd_lock); radix_tree_preload_end(); return page; }可以看到, brd设备内部采用Radix Tree来管理数据页面, 以实现高效查找. 内核中的Radix Tree类似于多级哈希表, 每一级通过index的不同的比特域来确定, 也和MMU的分级页表有些类似.
找到相应的数据页后, 接下来就是往里面写数据或者读取数据了, 数据的读写也就完成了.
通过以上的分析我们知道了brd内部的大致实现. 同时有一个问题, 读写的地方有两个入口: brd_make_request和brd_fops中关联的brd_rw_page, 前者接口参数是bio结构, bio结构内可以包含多个segment, 后者参数是page. 为什么会提供两种接口呢, 目前还没仔细分析block dev这一层. 猜想是brd_make_request是用来实现合并的io请求的, 因为它和请求队列相关联, 对于磁盘这样的设备, 随机读写的开销是很大的,因此有必要对上层的请求做合并和排序, 然后提交给设备驱动, 以提高效率; 而brd_rw_page可能是文件系统中的其他地方需要直接读取一个page的时候调用. 至于更具体的区别, 等以后研究到的时候再看吧.