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