详解 | Linux系统是如何实现存储并读写文件的?
1. 概述
Linux系统文件操作主要是通过块设备驱动来实现的。 块设备主要指的是用来存储数据的设备,类似于SD卡、U盘、Nor Flash、Nand Flash、机械硬盘和固态硬盘等。块设备驱动就是用来访问这些存储设备的,其与字符设备驱动不同的是:
- 块设备只能以块为基本单位实现读写,块是 linux 虚拟文件系统(VFS)基本的数据传输单位。字符设备是以字节为单位进行数据传输的,不需要缓冲。
- 块设备在结构上是可以进行随机访问的,对于这些设备的读写都是按块进行的,块设备使用缓冲区来暂时存放数据,等到条件成熟以后在一次性将缓冲区中的数据写入块设备中;字符设备是按照字节进行读写访问的。不需要缓冲区,对于字符设备的访问都是实时的,而且也不需要按照固定的块大小进行访问。
2. 代码框架
在记录块设备驱动的基本框架之前,先大致了解一下块设备驱动要实现的工作:在Linux驱动编程中,每一类驱动都会有一个对应的结构体。具体场景应用时,上层应用代码经过一系列虚拟文件系统API后最终会调用到驱动的这个结构体。应用所有对硬件的操作,都是通过调用此结构体的成员功能函数实现的。
对应设备驱动结构体定义于:include/linux/genhd.h
struct gendisk {
/* major, first_minor and minors are input parameters only,
* don't use directly. Use disk_devt() and disk_max_parts().
*/
int major; /* major number of driver */
int first_minor;
int minors; /* maximum number of minors, =1 for
* disks that can't be partitioned. */
char disk_name[DISK_NAME_LEN]; /* name of major driver */
char *(*devnode)(struct gendisk *gd, umode_t *mode);
unsigned int events; /* supported events */
unsigned int async_events; /* async events, subset of all */
struct disk_part_tbl __rcu *part_tbl;
struct hd_struct part0;
const struct block_device_operations *fops;
struct request_queue *queue;
void *private_data;
int flags;
struct kobject *slave_dir;
struct timer_rand_state *random;
atomic_t sync_io; /* RAID */
struct disk_events *ev;
#ifdef CONFIG_BLK_DEV_INTEGRITY
struct kobject integrity_kobj;
#endif /* CONFIG_BLK_DEV_INTEGRITY */
int node_id;
struct badblocks *bb;
};
在设备驱动中,主要的工作就是在入口中实现对gendisk结构体成员的填充,并注册到系统中去,供上层调用。
在了解到块设备驱动需要做的大致工作后,就要在块设备驱动基础框架上实现这些工作。块设备驱动代码主要分为以下几个部分:
声明入口、出口函数
module_init();
module_exit();
入口函数
在入口函数中,实现的功能比较多: ① 申请数据缓存区
ramdisk.block_buf = kzalloc(RAMDISK_SIZE, GFP_KERNEL)
② 向文件系统注册块设备
ramdisk.major = register_blkdev(0, DEVICE_NAME);
③ 初始化请求队列
ramdisk.queue = blk_init_queue(ramdisk_request, &ramdisk.lock);
④ 申请gendisk结构体,实例成员,并注册到系统中
ramdisk.gendisk = alloc_disk(3);
ramdisk.gendisk->major = ramdisk.major;
ramdisk.gendisk->first_minor = 0;
ramdisk.gendisk->fops = &ramdisk_fops;
ramdisk.gendisk->queue = ramdisk.queue;
sprintf(ramdisk.gendisk->disk_name, "dx_ramdisk");
set_capacity(ramdisk.gendisk, RAMDISK_SIZE/512);
add_disk(ramdisk.gendisk);
出口函数
注销在入口函数中申请的结构体空间以及释放获取的动态内存。
3. 主要功能实现
内存操作
既然涉及到数据的读取与存储,必然需要实现对存储设备内存的操作。由于内存数据的读写都是以块为单位,故读写操作放在队列中实现。内存操作的接口ramdisk_request放在blk_init_queue初始化队列中,开发人员只需要实现ramdisk_request函数的功能即可。
这里简单地用内存来模拟磁盘,故用memcpy来实现数据读写功能。
static void ramdisk_transfer(struct request *req)
{
unsigned long start = blk_rq_pos(req) << 9;
unsigned long len = blk_rq_cur_bytes(req);
void *buffer = bio_data(req->bio);
if(rq_data_dir(req) == READ)
memcpy(buffer, ramdisk.block_buf + start, len);
else if(rq_data_dir(req) == WRITE)
memcpy(ramdisk.block_buf + start, buffer, len);
}
void ramdisk_request(struct request_queue *q)
{
int err = 0;
struct request *req;
req = blk_fetch_request(q);
while(req != NULL) {
ramdisk_transfer(req);
if (!__blk_end_request_cur(req, err))
req = blk_fetch_request(q);
}
}
至于其他存储设备,就需要在ramdisk_request中实现对该存储设备的块读写操作。
4. 测试
① 注册驱动: insmod ramdisk.ko
② 查询磁盘状态:fdisk -l
③ 格式化磁盘:mkfs.vfat /dev/dx_ramdisk
④挂载磁盘:mount /dev/dx_ramdisk /dx_tmp1
由第④步即可看到,磁盘已经挂载到创建的dx_tmp1空文件夹上了。表明本次测试成功,系统就可以直接使用此磁盘来存储文件数据,
5. 总结
到这里,一个简单的块设备驱动就完成了。总结一下:在块设备驱动编程时,与字符设备驱动类似,需要实例操作系统提供的设备结构体成员,然后再将实例后的结构体注册到系统中,以供上层应用定向调用。需要注意的是,本篇实例是通过内存来模拟的块设备驱动,所以在实现存储区读写操作就比较简单。如果是针对具体的SPI FLASH、Nor FLASH、EEPROM等存储设备,还需要打通硬件读写功能。
参考:《【正点原子】I.MX6U嵌入式Linux驱动开发指南.pdf》
后记:
源码:https://github.com/LinuxTaoist/Linux_drivers/blob/master/block_driver/ramdisk.c
- 一条sql语句的改进探索(r5笔记第70天)
- 【专业技术】Node.js 究竟是什么?
- Github 项目推荐 | 用 Pytorch 实现的 WaveNet-Vocoder
- 重启数据库的一场闹剧(r5笔记第68天)
- 【C语言系列】基础语法案例分析(初级篇)
- 一次ORA-00600问题的排查和分析(r5笔记第64、65天)
- SpringMVC入门就这么简单
- pl/sql中的forall简单测试(r5笔记第63天)
- SpringMVC【开发Controller】详解
- 巧用外部表备份历史数据(r5笔记第62天)
- Github 项目推荐 | 最小化类 AlphaGo Zero 引擎 —— Nochi
- 半自动化运维之快速连接到指定环境(一) (r5笔记第61天)
- Spring【DAO模块】知识要点
- 浅谈exp/imp(上) (r5笔记第81天)
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法