Linux内核源代码分析 -- ramdisk内存盘
Linux内核源代码分析 – ramdisk内存盘
1. 设备的初始化与创建
在Linux内核模块中,设备的初始化和创建至关重要,负责设置设备参数、分配内存资源以及向内核注册设备。
1.1 设备初始化
static int __init main_init(void) {
int i, err;
for (i = 0; i < rd_nr; i++) {
err = brd_alloc(i); // 为每个设备分配资源
if (err) goto out_free;
}
return 0;
out_free:
// 清理逻辑
return err;
}
brd_alloc(i):核心函数,负责为第i个设备分配内存和创建磁盘。如果在任何步骤中出现错误,函数将通过goto out_free;跳转到清理逻辑并返回错误代码。
1.2 设备创建 brd_alloc
static int brd_alloc(int i) {
struct brd_device *brd;
struct gendisk *disk;
char buf[DISK_NAME_LEN];
int err = -ENOMEM;
brd = kzalloc(sizeof(*brd), GFP_KERNEL); // 分配内存
if (!brd) return -ENOMEM;
brd->brd_number = i;
list_add_tail(&brd->brd_list, &brd_devices); // 将设备添加到设备链表
xa_init(&brd->brd_pages); // 初始化页表
snprintf(buf, DISK_NAME_LEN, "ram%d", i); // 设置磁盘名称
disk = brd->brd_disk = blk_alloc_disk(NUMA_NO_NODE); // 分配磁盘
if (!disk) goto out_free_dev;
disk->major = RAMDISK_MAJOR; // 设置设备号
disk->first_minor = i * max_part;
disk->minors = max_part;
disk->fops = &brd_fops; // 指定文件操作集
disk->private_data = brd;
strscpy(disk->disk_name, buf, DISK_NAME_LEN);
set_capacity(disk, rd_size * 2); // 设置容量
err = add_disk(disk); // 注册磁盘
if (err) goto out_free_disk;
return 0;
out_free_disk:
put_disk(disk);
out_free_dev:
kfree(brd);
return err;
}
2. 关键函数解释
2.1 blk_alloc_disk()
功能:
blk_alloc_disk() 用于分配并初始化 struct gendisk 结构。它不仅为 gendisk 分配内存,还设置一些默认值,方便开发者后续自定义。
分配后的字段默认值:
disk->queue:未初始化,需显式设置。disk->fops:未初始化,需显式设置。disk->major和disk->first_minor:需要由开发者指定。
注意事项:
- 内核分配的
gendisk是使用kzalloc分配的,因此结构体内存已清零。 - 在调用
blk_alloc_disk()后,必须手动设置必要的字段,如major、minors、fops等。
2.2 set_capacity()
功能:
set_capacity() 设置磁盘设备的容量,单位为扇区。内核在处理 I/O 请求时,会根据容量进行边界检查。
使用示例:
set_capacity(disk, 1024 * 2); // 设置容量为 1024 KB
- 解释:上例中
1024 * 2表示 1024 KB 的容量,每扇区 512 字节,因此容量为 2048 扇区。
注意事项:
- 如果容量发生变化(如重新分区),可以调用
set_capacity()动态更新容量。 - 使用此函数后,内核会自动更新磁盘的 sysfs 属性。
2.3 add_disk()
功能:
add_disk() 将初始化完成的 gendisk 注册到内核块设备子系统,使其可被用户空间访问。注册成功后,设备会出现在 /dev 目录下。
函数定义:
位于 block/genhd.c 文件中:
void add_disk(struct gendisk *disk)
- 参数:
disk:指向已初始化的gendisk。
工作原理:
add_disk()内部会将磁盘添加到全局磁盘链表,并向 udev 发送事件,通知用户空间创建相应的设备节点。- 此函数还会将磁盘的信息注册到
/sys/block,便于用户通过sysfs查看设备信息。
注意事项:
- 在调用
add_disk()之前,必须确保disk->fops、disk->queue、disk->major等字段已正确设置。 - 如果注册失败,应调用
put_disk()释放资源。
3. 设备操作接口 block_device_operations
block_device_operations 定义了块设备驱动程序的各种操作接口,操作的是块设备(例如磁盘或虚拟块设备)。每个块设备驱动程序需要实现 block_device_operations 结构体中的函数指针,以告诉内核如何操作该设备。这些操作包括设备的读写、设备管理、获取设备信息等。
static const struct block_device_operations brd_fops = {
.owner = THIS_MODULE,
.submit_bio = brd_submit_bio, // 指定 I/O 提交函数
};
submit_bio指向brd_submit_bio,负责处理块设备的 I/O 请求。
4. 处理块I/O请求
brd_submit_bio 是 RAM 磁盘驱动程序中的核心函数,用于处理块设备的 I/O 请求。它接收从块层传递下来的 bio 结构,将其分解为 bvec(表示具体的内存块)进行逐个处理,最终完成数据的读写操作。
bio 和 bvec 的关系:
bio:块层传递的 I/O 请求,描述一次完整的读写操作。一个bio可以包含多个bvec。bvec:bio中的每个数据段,表示内存中的一块数据。通过bio_for_each_segment遍历所有bvec。
static void brd_submit_bio(struct bio *bio) {
struct brd_device *brd = bio->bi_bdev->bd_disk->private_data;
sector_t sector = bio->bi_iter.bi_sector;
struct bio_vec bvec;
struct bvec_iter iter;
bio_for_each_segment(bvec, bio, iter) {
unsigned int len = bvec.bv_len;
int err;
err = brd_do_bvec(brd, bvec.bv_page, len, bvec.bv_offset, bio->bi_opf, sector);
if (err) {
bio_io_error(bio); // 处理错误,通知上层 I/O 失败
return;
}
sector += len >> SECTOR_SHIFT; // 更新扇区偏移
}
bio_endio(bio); // 通知上层 I/O 完成
}
5. 处理 bio_vec 和页操作
在块设备驱动中,brd_do_bvec 是负责具体处理每个 bvec 的核心函数。它的主要任务是将用户数据从内存复制到设备模拟的内存区域,或者从设备读取数据返回给用户。
static int brd_do_bvec(struct brd_device *brd, struct page *page,
unsigned int len, unsigned int off, blk_opf_t opf,
sector_t sector) {
void *mem;
int err = 0;
if (op_is_write(opf)) {
gfp_t gfp = opf & REQ_NOWAIT ? GFP_NOWAIT : GFP_NOIO;
err = copy_to_brd_setup(brd, sector, len, gfp);
if (err) goto out;
}
mem = kmap_atomic(page);
if (!op_is_write(opf)) {
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;
}
5.1 copy_to_brd_setup
copy_to_brd_setup 是在向内存块设备写入数据之前的准备函数,负责确保内存页(page)已分配并可用。
if (op_is_write(opf)) {
gfp_t gfp = opf & REQ_NOWAIT ? GFP_NOWAIT : GFP_NOIO;
err = copy_to_brd_setup(brd, sector, len, gfp);
if (err) goto out;
}
op_is_write(opf):检查 I/O 请求是否为写操作。- 内存分配策略:
- 如果操作允许非阻塞分配(
REQ_NOWAIT),使用GFP_NOWAIT。 - 否则使用
GFP_NOIO,避免内存分配时触发 I/O。
- 如果操作允许非阻塞分配(
copy_to_brd_setup:在写入之前进行必要的初始化,确保设备内存页准备好。
5.2 内存映射与解除映射
mem = kmap_atomic(page);
kmap_atomic:将页面映射到内核地址空间,返回内核可访问的指针。- 为何使用原子映射?:
- 原子映射(
kmap_atomic)速度快,适用于短期映射,避免在高优先级上下文中被打断。
- 原子映射(
kunmap_atomic(mem);
kunmap_atomic:解除映射,确保资源被及时释放。
5.3 读写扇区
if (!op_is_write(opf)) {
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);
}
-
读操作(
copy_from_brd):- 将数据从设备(RAM 模拟)复制到内存。
- 调用
flush_dcache_page(page)确保缓存一致性。
-
写操作(
copy_to_brd):- 将用户数据从内存写入设备模拟的内存。
- 同样在写入前调用
flush_dcache_page(page)刷新缓存。
6. 读写扇区的实现
实际的读写操作由 copy_to_brd 和 copy_from_brd 完成。
ramdisk 的实际数据存储在内存中,在 struct brd_device 结构中的 brd_pages 字段,使用 xarray 存储。brd_lookup_page 和 brd_insert_page 封装了对 brd_pages 的查找和插入操作。
6.1 copy_to_brd 写数据到指定的扇区
static void copy_to_brd(struct brd_device *brd, const void *src,
sector_t sector, size_t n) {
struct page *page;
void *dst;
unsigned int offset = (sector & (PAGE_SECTORS-1)) << SECTOR_SHIFT;
size_t copy;
copy = min_t(size_t, n, PAGE_SIZE - offset);
page = brd_lookup_page(brd, sector);
BUG_ON(!page);
dst = kmap_atomic(page);
memcpy(dst + offset, src, copy);
kunmap_atomic(dst);
if (copy < n) {
src += copy;
sector += copy >> SECTOR_SHIFT;
copy = n - copy;
page = brd_lookup_page(brd, sector);
BUG_ON(!page);
dst = kmap_atomic(page);
memcpy(dst, src, copy);
kunmap_atomic(dst);
}
}
-
offset的计算:
unsigned int offset = (sector & (PAGE_SECTORS-1)) << SECTOR_SHIFT;-
sector:表示逻辑磁盘上的一个扇区。一个扇区通常为 512 字节(或其他大小,如 4KB),它是磁盘 I/O 操作的基本单位。 -
PAGE_SECTORS:表示每个内存页包含多少个扇区。PAGE_SECTORS的值通常是PAGE_SIZE / 512,其中PAGE_SIZE是一个内存页的字节大小(通常是 4KB)。例如,如果页面大小是 4KB,PAGE_SECTORS的值将是 8(4KB / 512B)。 -
PAGE_SECTORS-1:这步运算的目的是生成一个掩码,用来提取sector中的低位部分(即表示当前扇区所在内存页的位置)。 -
sector & (PAGE_SECTORS-1):通过按位与运算,从sector中提取出该扇区在页面中的位置。具体来说,(PAGE_SECTORS-1)是一个掩码,它只保留sector的低位(即,表示该扇区相对于当前页的偏移)。 -
<< SECTOR_SHIFT:SECTOR_SHIFT是用于将扇区转换为字节的位移量。通常,SECTOR_SHIFT的值为 9,因为每个扇区是 512 字节。通过<< SECTOR_SHIFT,我们将扇区位置转换为字节偏移量。假设:
- 一个内存页大小为 4KB(
PAGE_SIZE = 4096)。 一个扇区大小为 512 字节。 PAGE_SECTORS = PAGE_SIZE / 512 = 4096 / 512 = 8。
如果
sector = 10,表示磁盘上的第 10 个扇区(从 0 开始计数)。-
sector & (PAGE_SECTORS-1):PAGE_SECTORS-1为7(即0x07,表示低 3 位)。sector & 7会将sector的低 3 位保留,其他位清零。对于sector = 10,它的二进制表示为00001010,与0x07(即00000111)按位与之后结果为00000010,即值为2。
-
<< SECTOR_SHIFT:SECTOR_SHIFT = 9,表示每个扇区是 512 字节。2 << 9即2 * 512 = 1024,也就是该扇区相对于页面的字节偏移量。
- 一个内存页大小为 4KB(
-
-
查找扇区对应的 page:
brd_lookup_page:brd_lookup_page函数通过计算扇区索引,查找与给定扇区对应的内存页。具体来说,它会通过扇区号计算出对应的内存页索引 (pgoff_t idx),然后使用xa_load从xarray中查找该页。如果页不存在,它将返回NULL,如果页存在,则返回对应的struct page *。brd_lookup_page允许通过页索引定位内存中的数据页,这为copy_to_brd和copy_from_brd提供了对ramdisk中数据的读写操作基础。
-
min_t(size_t, n, PAGE_SIZE - offset):- 由于一个内存页的大小有限制(通常为 4KB),
copy_to_brd必须确保不超出单个页面的边界。因此,min_t(size_t, n, PAGE_SIZE - offset)会计算从给定扇区开始偏移量到当前页面末尾的字节数,并将要复制的数据长度限制在该范围内。这样可以避免越界写入,确保数据安全地写入到内存页中。
- 由于一个内存页的大小有限制(通常为 4KB),
-
kmap_atomic(page)和kunmap_atomic(dst):kmap_atomic和kunmap_atomic是用来进行页映射和取消映射的函数。由于内存页在内核空间管理,直接访问物理内存时需要通过映射将页映射到内核虚拟地址空间中。kmap_atomic将页映射为原子访问模式,以确保在访问期间不会被打断,适用于对内存的快速、短时间操作。操作完成后,通过kunmap_atomic解除映射。
-
memcpy(dst + offset, src, copy):- 这行代码使用
memcpy将数据从源地址src复制到目标地址dst,复制的字节数为copy。offset确保数据从正确的位置开始复制,避免覆盖内存页中的其他内容。
- 这行代码使用
-
处理跨页写入:
- 如果要写入的数据量超出了当前页的剩余空间(
copy < n),则copy_to_brd会继续处理剩余的数据。新的扇区被计算出来,跨页的数据会继续写入到下一个内存页。新的页通过brd_lookup_page查找并执行相同的写入操作。
- 如果要写入的数据量超出了当前页的剩余空间(
读取数据的过程和写入数据类似,只是方向相反,这里不再赘述。
总结
本文详细介绍了 Linux 内核中 RAM 磁盘的实现机制,包括设备的初始化、创建、关键函数的作用、设备操作接口、I/O 请求处理以及实际的读写操作。希望读者能够通过本文对 RAM 磁盘有一个全面而深入的理解。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐


所有评论(0)