Linux Kernel - 探索Ram Disk 驱动(2) - 源码探索

    xiaoxiao2021-12-14  20

    继续前行

    接着上一篇 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对象和函数回调进行了关联:

    1. brd_make_request

    static blk_qc_t brd_make_request(struct request_queue *q, struct bio *bio) { struct block_device *bdev = bio->bi_bdev; struct brd_device *brd = bdev->bd_disk->private_data; struct bio_vec bvec; sector_t sector; struct bvec_iter iter; //.... bio_for_each_segment(bvec, bio, iter) { unsigned int len = bvec.bv_len; int err; //lqp comment: 这里是重点, 根据bio结构的要求读取数据, 所以核心是brd_do_bvec err = brd_do_bvec(brd, bvec.bv_page, len, bvec.bv_offset, op_is_write(bio_op(bio)), sector); if (err) goto io_error; sector += len >> SECTOR_SHIFT; } out: bio_endio(bio); return BLK_QC_T_NONE; io_error: bio_io_error(bio); return BLK_QC_T_NONE; }

    2. brd_fops

    static const struct block_device_operations brd_fops = { .owner = THIS_MODULE, .rw_page = brd_rw_page, .ioctl = brd_ioctl, .direct_access = brd_direct_access, };

    可以发现, 里面也关联了一个读写接口: brd_rw_page, 和brd_make_request函数一样, 这个接口内部同样是调用brd_do_bvec来实现读写. 那么我们的问题就变得简单了, 只要分析brd_do_bvec函数就可以了.


    核心函数 - brd_do_bvec

    static int brd_do_bvec(struct brd_device *brd, struct page *page, unsigned int len, unsigned int off, bool is_write, sector_t sector) { void *mem; int err = 0; /*lqp comment: 这里如果是写的话, 需要提前准备一下. 为什么? 假设预设置brd的容量是1G, 而实际使用了1M, 那么只会分配1M的大小, 采取这种延迟分配的方式可以节省内存. 那如果是读取呢? 如果读取到未曾写 过的块, 则将读取到0*/ if (is_write) { err = copy_to_brd_setup(brd, sector, len); if (err) goto out; } mem = kmap_atomic(page); if (!is_write) { copy_from_brd(mem + off, brd, sector, len); flush_dcache_page(page); } else { flush_dcache_page(page); copy_to_brd(brd, mem + off, sector, len); } kunmap_atomic(mem); out: return err; }

    上的代码需要关注的地方主要是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的时候调用. 至于更具体的区别, 等以后研究到的时候再看吧.

    转载请注明原文地址: https://ju.6miu.com/read-964301.html

    最新回复(0)