详解 | Linux系统是如何实现存储并读写文件的?

2020-09-29 10:04:18 浏览数 (1)

1. 概述

Linux系统文件操作主要是通过块设备驱动来实现的。 块设备主要指的是用来存储数据的设备,类似于SD卡、U盘、Nor Flash、Nand Flash、机械硬盘和固态硬盘等。块设备驱动就是用来访问这些存储设备的,其与字符设备驱动不同的是:

  • 块设备只能以块为基本单位实现读写,块是 linux 虚拟文件系统(VFS)基本的数据传输单位。字符设备是以字节为单位进行数据传输的,不需要缓冲。
  • 块设备在结构上是可以进行随机访问的,对于这些设备的读写都是按块进行的,块设备使用缓冲区来暂时存放数据,等到条件成熟以后在一次性将缓冲区中的数据写入块设备中;字符设备是按照字节进行读写访问的。不需要缓冲区,对于字符设备的访问都是实时的,而且也不需要按照固定的块大小进行访问。

2. 代码框架

在记录块设备驱动的基本框架之前,先大致了解一下块设备驱动要实现的工作:在Linux驱动编程中,每一类驱动都会有一个对应的结构体。具体场景应用时,上层应用代码经过一系列虚拟文件系统API后最终会调用到驱动的这个结构体。应用所有对硬件的操作,都是通过调用此结构体的成员功能函数实现的。

对应设备驱动结构体定义于:include/linux/genhd.h

代码语言:javascript复制
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结构体成员的填充,并注册到系统中去,供上层调用。


在了解到块设备驱动需要做的大致工作后,就要在块设备驱动基础框架上实现这些工作。块设备驱动代码主要分为以下几个部分:

声明入口、出口函数

代码语言:javascript复制
module_init();
module_exit();

入口函数

在入口函数中,实现的功能比较多: ① 申请数据缓存区

代码语言:javascript复制
ramdisk.block_buf = kzalloc(RAMDISK_SIZE, GFP_KERNEL)

② 向文件系统注册块设备

代码语言:javascript复制
ramdisk.major = register_blkdev(0, DEVICE_NAME);

③ 初始化请求队列

代码语言:javascript复制
ramdisk.queue = blk_init_queue(ramdisk_request, &ramdisk.lock);

④ 申请gendisk结构体,实例成员,并注册到系统中

代码语言:javascript复制
    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来实现数据读写功能。

代码语言:javascript复制
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

0 人点赞