《Linux驱动开发(一)—环境搭建与hello world》
《Linux驱动开发(二)—驱动与设备的分离设计》
《Linux驱动开发(三)—设备树》
《Linux驱动开发(四)—树莓派内核编译》
《Linux驱动开发(五)—树莓派设备树配合驱动开发》
《Linux驱动开发(六)—树莓派配合硬件进行字符驱动开发》
《Linux驱动开发(七)—树莓派按键驱动开发》
《Linux驱动开发(八)—树莓派SR04驱动开发》
《Linux驱动开发(九)—树莓派I2C设备驱动开发(BME280)》
《Linux驱动开发(十)—树莓派输入子系统学习(红外接收)》
《Linux驱动开发(十一)—树莓派SPI驱动学习(OLED)》
《Linux驱动开发(十二)—树莓派framebuffer学习(改造OLED)》
《Linux驱动开发(十三)—USB驱动HID开发学习(鼠标)》
《Linux驱动开发(十四)—USB驱动开发学习(键盘+鼠标)》
《Linux驱动开发(十五)—如何使用内核现有驱动(显示屏)》
今天来学习一下linux驱动中的另一种关键驱动,块设备驱动。
不过在我的树莓派系统下,5.15的内核中,这块的函数已经大幅度进行了修改,所以前半部分内容来自于网络的博客学习,后面有专门介绍高版本内核的ramdisk开发。
是一种具有一定结构的随机存取设备,对这种设备的读写是按块进行的,他使用缓冲区来存放暂时的数据,待条件成熟后,从缓存一次性写入设备或者从设备一次性读到缓冲区。
块设备中有如下几个大小概念的定义
页、段、块、扇区之间的关系图如下:

这里我们按照驱动的不同实现,将块设备分成了两类:
分类的原因就在于非机械设备,可以任意读取,不会有性能的差异。但是机械设备就需要内核实现专门的算法,将杂乱的访问按照一定的顺序进行排列,可以有效提高磁盘性能。

这里的驱动过程,也是低版本内核块设备驱动的写法。
那么在分配请求队列处理上,根据前面的设备分类不同,有两种处理方法,后面再讲。
先讲一下:请求队列结构request_queue
每个块设备都有一个请求队列,每个请求队列单独执行I/O调度。
每个请求又由多个bio组成。
每个bio对应1个I/O请求,
大概如下图所示

IO调度算法可将连续的bio合并成1个请求。

块设备对象结构 block_device
struct block_device {
dev_t bd_dev; /* not a kdev_t - it's a search key */
int bd_openers;
struct inode * bd_inode; /* will die */
struct super_block * bd_super;
struct mutex bd_mutex; /* open/close mutex */
void * bd_claiming;
void * bd_holder;
int bd_holders;
bool bd_write_holder;
#ifdef CONFIG_SYSFS
struct list_head bd_holder_disks;
#endif
struct block_device * bd_contains;
unsigned bd_block_size;
u8 bd_partno;
struct hd_struct * bd_part;
/* number of times partitions within this device have been opened. */
unsigned bd_part_count;
int bd_invalidated;
struct gendisk * bd_disk;
struct request_queue * bd_queue;
struct backing_dev_info *bd_bdi;
struct list_head bd_list;
/*
* Private data. You must have bd_claim'ed the block_device
* to use this. NOTE: bd_claim allows an owner to claim
* the same device multiple times, the owner must take special
* care to not mess up bd_private for that case.
*/
unsigned long bd_private;
/* The counter of freeze processes */
int bd_fsfreeze_count;
/* Mutex for freeze */
struct mutex bd_fsfreeze_mutex;
} __randomize_layout;
磁盘结构体
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 short events; /* supported events */
unsigned short event_flags; /* flags related to event processing */
/* Array of pointers to partitions indexed by partno.
* Protected with matching bdev lock but stat and other
* non-critical accesses use RCU. Always access through
* helpers.
*/
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 rw_semaphore lookup_sem;
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;
struct lockdep_map lockdep_map;
};
磁盘结构体中的操作描述符,类似于file_operations
struct block_device_operations {
int (*open) (struct block_device *, fmode_t);
void (*release) (struct gendisk *, fmode_t);
int (*rw_page)(struct block_device *, sector_t, struct page *, unsigned int);
int (*ioctl) (struct block_device *, fmode_t, unsigned, unsigned long);
int (*compat_ioctl) (struct block_device *, fmode_t, unsigned, unsigned long);
unsigned int (*check_events) (struct gendisk *disk,
unsigned int clearing);
/* ->media_changed() is DEPRECATED, use ->check_events() instead */
int (*media_changed) (struct gendisk *);
void (*unlock_native_capacity) (struct gendisk *);
int (*revalidate_disk) (struct gendisk *);
int (*getgeo)(struct block_device *, struct hd_geometry *);
/* this callback is with swap_lock and sometimes page table lock held */
void (*swap_slot_free_notify) (struct block_device *, unsigned long);
int (*report_zones)(struct gendisk *, sector_t sector,
struct blk_zone *zones, unsigned int *nr_zones);
struct module *owner;
const struct pr_ops *pr_ops;
};
请求request结构
struct request {
//用于挂在请求队列链表的节点,使用函数blkdev_dequeue_request访问它,而不能直接访
问
struct list_head queuelist;
struct list_head donelist; /*用于挂在已完成请求链表的节点*/
struct request_queue *q; /*指向请求队列*/
unsigned int cmd_flags; /*命令标识*/
enum rq_cmd_type_bits cmd_type; /*命令类型*/
/*各种各样的扇区计数*/
/*为提交i/o维护bio横断面的状态信息,hard_*成员是块层内部使用的,驱动程序不应该改变
它们*/
sector_t sector; /*将提交的下一个扇区*/
sector_t hard_sector; /* 将完成的下一个扇区*/
unsigned long nr_sectors; /* 整个请求还需要传送的扇区数*/
unsigned long hard_nr_sectors; /* 将完成的扇区数*/
/*在当前bio中还需要传送的扇区数 */
unsigned int current_nr_sectors;
/*在当前段中将完成的扇区数*/
unsigned int hard_cur_sectors;
struct bio *bio; /*请求中第一个未完成操作的bio*、
struct bio *biotail; /*请求链表中末尾的bio*、
struct hlist_node hash; /*融合 hash */
/* rb_node仅用在I/O调度器中,当请求被移到分发队列中时,
请求将被删除。因此,让completion_data与rb_node分享空间*/
union {
struct rb_node rb_node; /* 排序/查找*/
void *completion_data;
}; 作者:补给站Linux内核 https://www.bilibili.com/read/cv17063262/ 出处:bilibili
bio结构
struct bio {
sector_t bi_sector;//该bio结构所要传输的第一个(512字节)扇区:磁盘的位置
struct bio *bi_next; //请求链表
struct block_device *bi_bdev;//相关的块设备
unsigned long bi_flags//状态和命令标志
unsigned long bi_rw; //读写
unsigned short bi_vcnt;//bio_vesc偏移的个数
unsigned short bi_idx; //bi_io_vec的当前索引
unsigned short bi_phys_segments;//结合后的片段数目
unsigned short bi_hw_segments;//重映射后的片段数目
unsigned int bi_size; //I/O计数
unsigned int bi_hw_front_size;//第一个可合并的段大小;
unsigned int bi_hw_back_size;//最后一个可合并的段大小
unsigned int bi_max_vecs; //bio_vecs数目上限
struct bio_vec *bi_io_vec; //bio_vec链表:内存的位置
bio_end_io_t *bi_end_io;//I/O完成方法
atomic_t bi_cnt; //使用计数
void *bi_private; //拥有者的私有方法
bio_destructor_t *bi_destructor; //销毁方法
};
写出这种框架的人,也是厉害,光看就头疼了

注意,这里都是低版本内核的关键操作,高版本好多函数都找不到了。

注册块设备
int register_blkdev(unsigned int major, const char *name)
申请磁盘
#define alloc_disk(minors) alloc_disk_node(minors, NUMA_NO_NODE)
设置磁盘容量
void set_capacity(struct gendisk *disk, sector_t size)
添加磁盘
void add_disk(struct gendisk *disk)
释放磁盘
void del_gendisk(struct gendisk *gd)
根据不同类型的块设备,请求队列的使用不相同
机械类块设备
需要内核帮忙处理算法,所以接口采用
blk_init_queue(ramdisk_request_fn, &ramdisk.lock);
非机械类的
首先申请队列
struct request_queue *blk_alloc_queue(gfp_t gfp_mask)
然后制造请求函数
void blk_queue_make_request(struct request_queue *q, make_request_fn *mfn)
代码参考自 栋哥修炼日记的 《linux块设备驱动简述(Linux驱动开发篇)》
这里都是低版本内核之前的写法。

机械类
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define RAMDISK_SIZE (2 * 1024 * 1024) /* 容量大小为2MB */
#define RAMDISK_NAME "ramdisk" /* 名字 */
#define RADMISK_MINOR 3 /* 表示有三个磁盘分区!不是次设备号为3! */
/* ramdisk设备结构体 */
struct ramdisk_dev{
int major; /* 主设备号 */
unsigned char *ramdiskbuf; /* ramdisk内存空间,用于模拟块设备 */
spinlock_t lock; /* 自旋锁 */
struct gendisk *gendisk_des; /* gendisk */
struct request_queue *queue;/* 请求队列 */
};
struct ramdisk_dev ramdisk; /* ramdisk设备 */
/*
* @description : 打开块设备
* @param - dev : 块设备
* @param - mode : 打开模式
* @return : 0 成功;其他 失败
*/
int ramdisk_open(struct block_device *dev, fmode_t mode)
{
printk("ramdisk open\r\n");
return 0;
}
/*
* @description : 释放块设备
* @param - disk : gendisk
* @param - mode : 模式
* @return : 0 成功;其他 失败
*/
void ramdisk_release(struct gendisk *disk, fmode_t mode)
{
printk("ramdisk release\r\n");
}
/*
* @description : 获取磁盘信息
* @param - dev : 块设备
* @param - geo : 模式
* @return : 0 成功;其他 失败
*/
int ramdisk_getgeo(struct block_device *dev, struct hd_geometry *geo)
{
/* 这是相对于机械硬盘的概念 */
geo->heads = 2; /* 磁头 */
geo->cylinders = 32; /* 柱面 */
geo->sectors = RAMDISK_SIZE / (2 * 32 *512); /* 一个磁道上的扇区数量 */
return 0;
}
/*
* 块设备操作函数
*/
static struct block_device_operations ramdisk_fops =
{
.owner = THIS_MODULE,
.open = ramdisk_open,
.release = ramdisk_release,
.getgeo = ramdisk_getgeo,
};
/*
* @description : 处理传输过程
* @param-req : 请求
* @return : 无
*/
static void ramdisk_transfer(struct request *req)
{
unsigned long start = blk_rq_pos(req) << 9; /* blk_rq_pos获取到的是扇区地址,左移9位转换为字节地址 */
unsigned long len = blk_rq_cur_bytes(req); /* 大小 */
/* bio中的数据缓冲区
* 读:从磁盘读取到的数据存放到buffer中
* 写:buffer保存这要写入磁盘的数据
*/
void *buffer = bio_data(req->bio);
if(rq_data_dir(req) == READ) /* 读数据 */
memcpy(buffer, ramdisk.ramdiskbuf + start, len);
else if(rq_data_dir(req) == WRITE) /* 写数据 */
memcpy(ramdisk.ramdiskbuf + start, buffer, len);
}
/*
* @description : 请求处理函数
* @param-q : 请求队列
* @return : 无
*/
void ramdisk_request_fn(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);
}
}
/*
* @description : 驱动出口函数
* @param : 无
* @return : 无
*/
static int __init ramdisk_init(void)
{
int ret = 0;
printk("ramdisk init\r\n");
/* 1、申请用于ramdisk内存 */
ramdisk.ramdiskbuf = kzalloc(RAMDISK_SIZE, GFP_KERNEL);
if(ramdisk.ramdiskbuf == NULL) {
ret = -EINVAL;
goto ram_fail;
}
/* 2、初始化自旋锁 */
spin_lock_init(&ramdisk.lock);
/* 3、注册块设备 */
ramdisk.major = register_blkdev(0, RAMDISK_NAME); /* 由系统自动分配主设备号 */
if(ramdisk.major < 0) {
goto register_blkdev_fail;
}
printk("ramdisk major = %d\r\n", ramdisk.major);
/* 4、分配并初始化gendisk */
ramdisk.gendisk_des = alloc_disk(RADMISK_MINOR);
if(!ramdisk.gendisk_des) {
ret = -EINVAL;
goto gendisk_alloc_fail;
}
/* 5、分配并初始化请求队列 */
ramdisk.queue = blk_init_queue(ramdisk_request_fn, &ramdisk.lock);
if(!ramdisk.queue) {
ret = EINVAL;
goto blk_init_fail;
}
/* 6、添加(注册)disk */
ramdisk.gendisk_des->major = ramdisk.major; /* 主设备号 */
ramdisk.gendisk_des->first_minor = 0; /* 第一个次设备号(起始次设备号) */
ramdisk.gendisk_des->fops = &ramdisk_fops; /* 操作函数 */
ramdisk.gendisk_des->private_data = &ramdisk; /* 私有数据 */
ramdisk.gendisk_des->queue = ramdisk.queue; /* 请求队列 */
sprintf(ramdisk.gendisk_des->disk_name, RAMDISK_NAME); /* 名字 */
set_capacity(ramdisk.gendisk_des, RAMDISK_SIZE/512); /* 设备容量(单位为扇区) */
add_disk(ramdisk.gendisk_des);
return 0;
blk_init_fail:
put_disk(ramdisk.gendisk_des);
//del_gendisk(ramdisk.gendisk_des);
gendisk_alloc_fail:
unregister_blkdev(ramdisk.major, RAMDISK_NAME);
register_blkdev_fail:
kfree(ramdisk.ramdiskbuf); /* 释放内存 */
ram_fail:
return ret;
}
/*
* @description : 驱动出口函数
* @param : 无
* @return : 无
*/
static void __exit ramdisk_exit(void)
{
printk("ramdisk exit\r\n");
/* 释放gendisk */
del_gendisk(ramdisk.gendisk_des);
put_disk(ramdisk.gendisk_des);
/* 清除请求队列 */
blk_cleanup_queue(ramdisk.queue);
/* 注销块设备 */
unregister_blkdev(ramdisk.major, RAMDISK_NAME);
/* 释放内存 */
kfree(ramdisk.ramdiskbuf);
}
module_init(ramdisk_init);
module_exit(ramdisk_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("zuozhongkai");
非机械类
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define RAMDISK_SIZE (2 * 1024 * 1024) /* 容量大小为2MB */
#define RAMDISK_NAME "ramdisk" /* 名字 */
#define RADMISK_MINOR 3 /* 表示有三个磁盘分区!不是次设备号为3! */
/* ramdisk设备结构体 */
struct ramdisk_dev{
int major; /* 主设备号 */
unsigned char *ramdiskbuf; /* ramdisk内存空间,用于模拟块设备 */
spinlock_t lock; /* 自旋锁 */
struct gendisk *gendisk_des; /* gendisk */
struct request_queue *queue;/* 请求队列 */
};
struct ramdisk_dev ramdisk; /* ramdisk设备 */
/*
* @description : 打开块设备
* @param - dev : 块设备
* @param - mode : 打开模式
* @return : 0 成功;其他 失败
*/
int ramdisk_open(struct block_device *dev, fmode_t mode)
{
printk("ramdisk open\r\n");
return 0;
}
/*
* @description : 释放块设备
* @param - disk : gendisk
* @param - mode : 模式
* @return : 0 成功;其他 失败
*/
void ramdisk_release(struct gendisk *disk, fmode_t mode)
{
printk("ramdisk release\r\n");
}
/*
* @description : 获取磁盘信息
* @param - dev : 块设备
* @param - geo : 模式
* @return : 0 成功;其他 失败
*/
int ramdisk_getgeo(struct block_device *dev, struct hd_geometry *geo)
{
/* 这是相对于机械硬盘的概念 */
geo->heads = 2; /* 磁头 */
geo->cylinders = 32; /* 柱面 */
geo->sectors = RAMDISK_SIZE / (2 * 32 *512); /* 一个磁道上的扇区数量 */
return 0;
}
/*
* 块设备操作函数
*/
static struct block_device_operations ramdisk_fops =
{
.owner = THIS_MODULE,
.open = ramdisk_open,
.release = ramdisk_release,
.getgeo = ramdisk_getgeo,
};
/*
* @description : “制造请求”函数
* @param-q : 请求队列
* @return : 无
*/
void ramdisk_make_request_fn(struct request_queue *q, struct bio *bio)
{
int offset;
struct bio_vec bvec;
struct bvec_iter iter;
unsigned long len = 0;
offset = (bio->bi_iter.bi_sector) << 9; /* 获取要操作的设备的偏移地址 */
/* 处理bio中的每个段 */
bio_for_each_segment(bvec, bio, iter){
char *ptr = page_address(bvec.bv_page) + bvec.bv_offset;
len = bvec.bv_len;
if(bio_data_dir(bio) == READ) /* 读数据 */
memcpy(ptr, ramdisk.ramdiskbuf + offset, len);
else if(bio_data_dir(bio) == WRITE) /* 写数据 */
memcpy(ramdisk.ramdiskbuf + offset, ptr, len);
offset += len;
}
set_bit(BIO_UPTODATE, &bio->bi_flags);
bio_endio(bio, 0);
}
/*
* @description : 驱动出口函数
* @param : 无
* @return : 无
*/
static int __init ramdisk_init(void)
{
int ret = 0;
printk("ramdisk init\r\n");
/* 1、申请用于ramdisk内存 */
ramdisk.ramdiskbuf = kzalloc(RAMDISK_SIZE, GFP_KERNEL);
if(ramdisk.ramdiskbuf == NULL) {
ret = -EINVAL;
goto ram_fail;
}
/* 2、初始化自旋锁 */
spin_lock_init(&ramdisk.lock);
/* 3、注册块设备 */
ramdisk.major = register_blkdev(0, RAMDISK_NAME); /* 由系统自动分配主设备号 */
if(ramdisk.major < 0) {
goto register_blkdev_fail;
}
printk("ramdisk major = %d\r\n", ramdisk.major);
/* 4、分配并初始化gendisk */
ramdisk.gendisk_des = alloc_disk(RADMISK_MINOR);
if(!ramdisk.gendisk_des) {
ret = -EINVAL;
goto gendisk_alloc_fail;
}
/* 5、分配请求队列 */
ramdisk.queue = blk_alloc_queue(GFP_KERNEL);
if(!ramdisk.queue){
ret = -EINVAL;
goto blk_allo_fail;
}
/* 6、设置“制造请求”函数 */
blk_queue_make_request(ramdisk.queue, ramdisk_make_request_fn);
/* 7、添加(注册)disk */
ramdisk.gendisk_des->major = ramdisk.major; /* 主设备号 */
ramdisk.gendisk_des->first_minor = 0; /* 第一个次设备号(起始次设备号) */
ramdisk.gendisk_des->fops = &ramdisk_fops; /* 操作函数 */
ramdisk.gendisk_des->private_data = &ramdisk; /* 私有数据 */
ramdisk.gendisk_des->queue = ramdisk.queue; /* 请求队列 */
sprintf(ramdisk.gendisk_des->disk_name, RAMDISK_NAME); /* 名字 */
set_capacity(ramdisk.gendisk_des, RAMDISK_SIZE/512); /* 设备容量(单位为扇区) */
add_disk(ramdisk.gendisk_des);
return 0;
blk_allo_fail:
put_disk(ramdisk.gendisk_des);
//del_gendisk(ramdisk.gendisk_des);
gendisk_alloc_fail:
unregister_blkdev(ramdisk.major, RAMDISK_NAME);
register_blkdev_fail:
kfree(ramdisk.ramdiskbuf); /* 释放内存 */
ram_fail:
return ret;
}
/*
* @description : 驱动出口函数
* @param : 无
* @return : 无
*/
static void __exit ramdisk_exit(void)
{
printk("ramdisk exit\r\n");
/* 释放gendisk */
del_gendisk(ramdisk.gendisk_des);
put_disk(ramdisk.gendisk_des);
/* 清除请求队列 */
blk_cleanup_queue(ramdisk.queue);
/* 注销块设备 */
unregister_blkdev(ramdisk.major, RAMDISK_NAME);
/* 释放内存 */
kfree(ramdisk.ramdiskbuf);
}
module_init(ramdisk_init);
module_exit(ramdisk_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("zuozhongkai");
这个代码没有运行过,要是出错了也别来找我啊。

格式化ramdisk
需要将ramdisk用一种文件系统格式化,由于没有必要采用日志文件系统,因此仅用EXT2格式即可,以仅需要使用ram0为例:
[root]# mke2fs -m 0 /dev/ramdisk
创建挂载点,挂载ramdisk
在已经格式化了ramdisk之后,必须为其创建一个挂载点,然后将ramdisk挂载到该挂载点后使用。
[root]# mkdir /mnt/rd; mount /dev/ramdisk /mnt/rd
查看验证挂载是否成功及文件系统信息
[root]# mount | grep ramdisk
[root]# df -h | grep ramdisk
进一步查看ram0的详细信息
[root]# tune2fs -l /dev/ramdisk
修改挂载点的使用权限
[root]# chown van:root /mnt/rd
[root]# chmod 0770 /mnt/rd
验证并查看挂载点的权限是否修改
[root]# ls -ald /mnt/rd
使用ramdisk
完成以上工作后,就可以像在磁盘分区上一样在ramdisk上进创建、复制、移动、删除、编辑文件了。如果需要移除ramdisk,采用以下命令解除挂载即可:
[root]# umount -v /mnt/rd

我用的树莓派版本是5.1.55。在内核的源码中,已经没有了那几个核心函数

思路已经改变了。
现在的ramdisk已经是通过块设备操作来进行读写操作了。代码可以参考brd.c。
/*
* 块设备操作函数
*/
static struct block_device_operations ramdisk_fops =
{
.owner = THIS_MODULE,
.open = myrd_open,
.release = myrd_release,
.submit_bio = myrd_submit_bio,
.rw_page = myrd_rw_page,
.getgeo = myrd_getgeo,
};
我也写了一个类似前面的自定义方式产生myramdisk。

《代码下载地址》
厉不厉害,全网独一份的代码呢

昨天也是情人节,我和姐姐两家人去看古仔的新电影《明日战记》,场面还可以,不过情节单调,人物也单调,为什么还要找两个五六十岁的人来扮演两个战士,普通市民刘先生都快六十岁的人了。香港电影能拿得出来的,还是那些老面孔,真的是没落了好多。

不过最后没有人牺牲,也算是一个好的结尾,适合带着小孩子观看。