Linux块设备驱动
块设备驱动是用来操作硬盘类的存储设备的磁头:一个磁盘有多少个面就有多少个磁头磁道:在一个磁头上可以有很多环,这些环就叫做磁道扇区:磁道上访问数据的最小的单位就是扇区,一个扇区的大小就是512字节1block = 512字节1024字节2048字节4096字节1扇区= 512字节块设备的能存储的数据=磁头*磁道*扇区*5121.5块设备驱动的API1.6块设备驱动的实例1.7队列处理相关的结构体、关
·
文章目录
1.块设备驱动
块设备驱动是用来操作硬盘类的存储设备的
1.1什么是块设备驱动
- 块设备驱动的概念:系统中能够随机访问固定大小(1block 512byte)数据片的设备被称之为块设备。块设备文件一般都是以安装文件系统的方式使用,这也是块设备通常的访问方式。块设备的方式访问方式是随机的。
- 块设备中最小的可寻址单位是扇区,扇区大小一般是2的整数倍。最常见的大小是512字节。块是文件系统的一种抽象,只能基于块来访问文件系统。物理磁盘寻址是按照扇区的级别进行的,内核访问的所有磁盘操作又都是按照块进行的。扇区是设备的最小可寻址单位,所以块不能比扇区还小,只能数倍与扇区大小。
- 内核对块大小的要求是:必须是扇区大小的整数倍,并且小于页面的大小,所以块的大小通常是512字节、1K或者4K。
1.2块设备驱动和字符设备驱动的对比
- 块设备接口相对复杂,不如字符设备明晰易用
- 块设备驱动程序对整个系统的性能影响较大,速度和效率是设计块设备驱动程要重点考虑的问题
- 系统中使用缓冲区与访问请求的优化管理(合并与重新排序)来提高系统性能
1.3块设备驱动的相关知识简介
磁头:一个磁盘有多少个面就有多少个磁头
磁道:在一个磁头上可以有很多环,这些环就叫做磁道
扇区:磁道上访问数据的最小的单位就是扇区,一个扇区的大小就是512字节
1block = 512字节 1024字节 2048字节 4096字节
1扇区 = 512字节
块设备的能存储的数据=磁头*磁道*扇区*512
1.4块设备驱动的框架图
- 虚拟文件系统(VFS):隐藏了各种硬件的具体细节,为用户操作不同的硬件提供了一个统一的接口。其基于不同的文件系统格式,比如EXT,FAT等。用户程序对设备的操作都通过VFS来完成,在VFS上面就是诸如open、close、write和read的函数API。
- Disk Cache:硬盘的告诉缓存,用户缓存最近访问的文件数据,如果能在高速缓存中找到,就不必去访问硬盘,毕竟硬盘的访问速度慢很多。
- 映射层(mapping layer):这一层主要用于确定文件系统的block size,然后计算所请求的数据包含多少个block。同时调用具体文件系统函数来访问文件的inode,确定所请求的数据在磁盘上面的逻辑地址。
- Generic Block Layer:通用块层,Linux内核把块设备看做是由若干个扇区组成的数据空间,上层的读写请求在通用块层被构造成一个或多个bio结构
- I/O Scheduler Layer :I/O调度层,负责将通用块层的块I/O操作进行(电梯调度算法)调度、插入、暂存、排序、合并、分发等操作,对磁盘的操作更为高效。负责将通用块层的块I/O操作进行
- 块设备驱动层:在块系统架构的最底层,由块设备驱动根据排序好的请求,对硬件进行数据访问。
user:
open read write close
-------------------(io请求)-----------------------------------
kernel |中间层: (block_device)
| 将用户的io请求转化成BIO(block,input ,output),
| 在物理内存上连续的bio会被合成request,这个request
| 会被放到内核的一个队列上。
|---------------------------------------------------------
|driver:gendisk
| 1.分配对象
| 2.对象初始化
| 3.初始化一个队列 head----request(read)----request(write)---...
| //4.硬盘设备的初始化
| 5.注册、注销
------------------------------------------------------------------
haredware : 分配的内存(模拟真实的设备)(1M)
1.5块设备驱动的API
1.gendisk的结构体对象
struct gendisk {
int major; //块设备的主设备号
int first_minor; //起始的次设备号
int minors; //设备的个数,分区的个数
char disk_name[DISK_NAME_LEN]; //磁盘的名字
struct disk_part_tbl *part_tbl;
//磁盘的分区表的首地址
struct hd_struct part0;
//part0分区的描述
const struct block_device_operations *fops;
//块设备的操作方法结构体
struct request_queue *queue;
//队列(重要)
void *private_data;
//私有数据
};
分区的结构体
struct hd_struct {
sector_t start_sect; //起始的扇区号
sector_t nr_sects; //扇区的个数
int partno; //分区号
};
//块设备的操作方法结构体
struct block_device_operations {
int (*open) (struct block_device *, fmode_t);
int (*release) (struct gendisk *, fmode_t);
int (*ioctl) (struct block_device *, fmode_t, unsigned, unsigned long);
int (*getgeo)(struct block_device *, struct hd_geometry *);
//设置磁盘的磁头,磁道,扇区的个数的。hd_geometry
}
2.结构体对象的初始化
struct gendisk *mydisk;
struct gendisk *alloc_disk(int minors)
//void put_disk(struct gendisk *disk)
//归还引用计数
功能:分配gendisk的内存,然后完成必要的初始化
参数:
@minors:分区的个数
返回值:成功返回分配到的内存的首地址,失败返回NULL
int register_blkdev(unsigned int major, const char *name)
//void unregister_blkdev(unsigned int major, const char *name)
功能:申请设备设备驱动的主设备号
参数:
@major : 0:自动申请
>0 :静态指定
@name :名字 cat /proc/devices
返回值:
major=0 ;成功返回主设备号,失败返回错误码
major>0 :成功返回0 ,失败返回错误码
void set_capacity(struct gendisk *disk, sector_t size)
功能:设置磁盘的容量
struct request_queue *blk_mq_init_sq_queue(struct blk_mq_tag_set *set,const struct blk_mq_ops
*ops,unsigned int queue_depth,unsigned int set_flags)
//void blk_cleanup_queue(struct request_queue *q)
功能:用于在给定队列深度的情况下使用mq ops设置队列的助手,以及通过mq ops标志传递的助手
参数:
@被初始化的tag对象,tag被上层使用,里面包含硬件队列的个数,队列的操作方法结构体,标志位等
@放入到tag中的操作方法结构体
@ tag中指定支持的队列深度
@将tag中队列的处理标志位,例如BLK_MQ_F_SHOULD_MERGE, BLK_MQ_F_BLOCKING等
返回值:成功返回队列指针,失败返回错误码指针
3.注册、注销
void add_disk(struct gendisk *disk)
//注册
void del_gendisk(struct gendisk *disk)
//注销
1.6块设备驱动的实例
#include <linux/blkdev.h>
#include <linux/genhd.h>
#include <linux/hdreg.h>
#include <linux/init.h>
#include <linux/module.h>
#define BLKSIZE (1 * 1024 * 1024)
#define BLKNAME "mydisk"
struct gendisk* disk;
int major;
char *dev_addr;
int mydisk_open(struct block_device* blkdev, fmode_t mode)
{
return 0;
}
int mydisk_getgeo(struct block_device* blkdev,
struct hd_geometry* hd)
{
//磁盘的大小 = 磁头*磁道*扇区个数*512
// struct hd_geometry {
// unsigned char heads; //磁头的个数
// unsigned char sectors; //扇区个数
// unsigned short cylinders; //磁道的个数
// unsigned long start; //0
// };
hd->heads = 4;
hd->cylinders = 16;
hd->sectors = BLKSIZE / hd->heads / hd->cylinders / 512;
return 0;
}
void mydisk_close(struct gendisk* dk, fmode_t mode)
{
}
struct block_device_operations fops = {
.open = mydisk_open,
.getgeo = mydisk_getgeo,
.release = mydisk_close,
};
static int __init mydisk_init(void)
{
// 1.分配对象
disk = alloc_disk(4);
if (disk == NULL) {
printk("alloc disk memory error\n");
return -ENOMEM;
}
// 2.初始化对象
major = register_blkdev(0, BLKNAME);
if (major < 0) {
printk("register blk device error\n");
return major;
}
disk->major = major;
disk->first_minor = 0;
strcpy(disk->disk_name, BLKNAME);
set_capacity(disk, BLKSIZE / 512); //将磁盘默认先全部给part0
disk->fops = &fops;
//disk->queue =
//3申请内存当成硬盘使用
dev_addr = vmalloc(BLKSIZE);
if(dev_addr == NULL){
printk("alloc dev memory error\n");
return -ENOMEM;
}
// 4注册
add_disk(disk);
return 0;
}
static void __exit mydisk_exit(void)
{
del_gendisk(disk);
vfree(dev_addr);
unregister_blkdev(major,BLKNAME);
put_disk(disk);
}
module_init(mydisk_init);
module_exit(mydisk_exit);
MODULE_LICENSE("GPL");
1.7队列处理相关的结构体、关系、相关函数
struct request_queue
{
/*双向链表数据结构,将所有加入到队列的IO请求组建成一个双向链表*/
struct list_head queue_head;
struct list_head requeue_list; //request队列
spinlock_t requeue_lock; //队列自旋锁
unsigned long nr_requests; /* 最大的请求数量 */
unsigned long queue_flags;/*当前请求队列的状QUEUE_FLAG_STOPPED*/
…
};
struct request
{
struct list_head queuelist;/* 请求对象中的链表元素*/
struct request_queue *q; /* 指向存放当前请求的请求队列*/
unsigned int __data_len; /* 当前请求要求数据传输的总的数据量 */
sector_t __sector; /* 当前请求要求数据传输的块设备的起始扇区 */
struct bio *bio; /* bio对象所携带的信息转存至请求对象中*/
struct bio *biotail; /* bio链表*/
…
};
通常一个request请求可以包含多个bio,一个bio对应一个I/O请求
struct bio {
struct bio *bi_next; /* 指向当前bio的下一个对象*/
unsigned long bi_flags; /* 状态、命令等 */
unsigned long bi_rw; /* 表示READ/WRITE*/
struct block_device *bi_bdev; /* 与请求相关联的块设备对象指针*/
unsigned short bi_vcnt; /* bi_io_vec数组中元素个数 */
unsigned short bi_idx; /* 当前处理的bi_io_vec数组元素索引 */
unsigned int bi_size; /* 本次传输需要传输的数据总量,byte(扇区大小整数倍) */
struct bio_vec *bi_io_vec;/* 指向一个IO向量的数组,数组中的内各元素对应一个物理页的page对象 */
};
struct bio_vec {
struct page *bv_page; //指向用于数据传输的页面所对应的struct page对象
unsigned int bv_len; //表示当前要传输的数据大小
unsigned int bv_offset;//表示数据在页面内的偏移量
};
reque_queue和request及bio的关系
队列处理相关的函数
blk_mq_start_request(rq); //开始处理队列
blk_mq_end_request(rq, BLK_STS_OK); //结束队列处理
rq_for_each_segment(bvec, rq, iter) //从request->bio_vec
void* b_buf = page_address(bvec.bv_page) + bvec.bv_offset; //将页地址转换为线性地址(内地址)
rq_data_dir(rq)) //从request获取本次读写的方向 WRITE 1 READ 0
dev_addr+(rq->__sector *512) //磁盘设备的地址
1.8虚拟块设备驱动的实例
mydisk.c
#include <linux/blk-mq.h>
#include <linux/blkdev.h>
#include <linux/genhd.h>
#include <linux/hdreg.h>
#include <linux/init.h>
#include <linux/module.h>
#define BLKSIZE (1 * 1024 * 1024)
#define BLKNAME "mydisk"
struct gendisk* disk;
int major;
struct request_queue* queue;
char* dev_addr;
struct blk_mq_tag_set tag;
int mydisk_open(struct block_device* blkdev, fmode_t mode)
{
return 0;
}
int mydisk_getgeo(struct block_device* blkdev,
struct hd_geometry* hd)
{
//磁盘的大小 = 磁头*磁道*扇区个数*512
// struct hd_geometry {
// unsigned char heads; //磁头的个数
// unsigned char sectors; //扇区个数
// unsigned short cylinders; //磁道的个数
// unsigned long start; //0
// };
hd->heads = 4;
hd->cylinders = 16;
hd->sectors = BLKSIZE / hd->heads / hd->cylinders / 512;
return 0;
}
void mydisk_close(struct gendisk* dk, fmode_t mode)
{
}
struct block_device_operations fops = {
.open = mydisk_open,
.getgeo = mydisk_getgeo,
.release = mydisk_close,
};
blk_status_t disk_queue_rq(struct blk_mq_hw_ctx* ctx,
const struct blk_mq_queue_data* data)
{
blk_status_t status = BLK_STS_OK;
struct request* rq = data->rq;
struct bio_vec bvec; //循环取到的成员
struct req_iterator iter; //迭代器(依次取下一个成员),内核使用的
loff_t pos = blk_rq_pos(rq) << SECTOR_SHIFT;
//队列处理过程在这里完成
blk_mq_start_request(rq);
rq_for_each_segment(bvec, rq, iter)
{
//长度 每个片段的长度
unsigned long b_len = bvec.bv_len;
//内核的地址
void* b_buf = page_address(bvec.bv_page) + bvec.bv_offset;
//校验越界
if ((pos + b_len) > BLKSIZE)
b_len = (unsigned long)(BLKSIZE - pos);
if (rq_data_dir(rq)) // WRITE
//用户想向磁盘写数据
//目的:磁盘地址 dev_addr+pos
//源: 用户地址 b_buf
//长度:b_len
memcpy(dev_addr+pos, b_buf, b_len);
else // READ
//用户想从磁盘读数据
//目的:用户地址 b_buf
//源: 磁盘地址 dev_addr+pos
//长度:b_len
memcpy(b_buf,dev_addr+pos, b_len);
pos += b_len;
}
blk_mq_end_request(rq, status);
return status;
}
struct blk_mq_ops qops = {
.queue_rq = disk_queue_rq,
};
static int __init mydisk_init(void)
{
// 1.分配对象
disk = alloc_disk(4);
if (disk == NULL) {
printk("alloc disk memory error\n");
return -ENOMEM;
}
// 2.初始化对象
major = register_blkdev(0, BLKNAME);
if (major < 0) {
printk("register blk device error\n");
return major;
}
queue = blk_mq_init_sq_queue(&tag, &qops, 2, BLK_MQ_F_SHOULD_MERGE);
if (IS_ERR(queue)) {
printk("blk queue get error\n");
return PTR_ERR(queue);
}
disk->major = major;
disk->first_minor = 0;
strcpy(disk->disk_name, BLKNAME);
set_capacity(disk, BLKSIZE / 512); //将磁盘默认先全部给part0
disk->fops = &fops;
disk->queue = queue;
// 3申请内存当成硬盘使用
dev_addr = vmalloc(BLKSIZE);
if (dev_addr == NULL) {
printk("alloc dev memory error\n");
return -ENOMEM;
}
// 4注册
add_disk(disk);
return 0;
}
static void __exit mydisk_exit(void)
{
del_gendisk(disk);
vfree(dev_addr);
blk_cleanup_queue(queue);
unregister_blkdev(major, BLKNAME);
put_disk(disk);
}
module_init(mydisk_init);
module_exit(mydisk_exit);
MODULE_LICENSE("GPL");
1.9块设备驱动测试方法
1.安装块设备驱动
sudo insmod mydisk.ko
2.查看设备节点
ls -l /dev/mydisk
brw-rw---- 1 root disk 252, 0 6月 22 14:14 /dev/mydisk
3.查看磁盘相关的命令
sudo fdisk -l
Disk /dev/mydisk:1 MiB,1048576 字节,2048 个扇区
单元:扇区 / 1 * 512 = 512 字节
扇区大小(逻辑/物理):512 字节 / 512 字节
I/O 大小(最小/最佳):512 字节 / 512 字节
4.分区
sudo fdisk /dev/mydisk
m 获取帮助
d 删除分区
p 打印分区表
n 添加新分区
w 将分区表写入磁盘并退出
q 退出而不保存更改
在dev下产生/dev/mydisk1节点
5.分区格式化
sudo mkfs.ext2 /dev/mydisk1
6.挂载硬盘到某个目录下
sudo mount -t ext2 /dev/mydisk1 ~/udisk
7.向磁盘中存放文件
sudo cp ~/work/day10/02mydisk/mydisk.c .
8.取消挂载
cd
sudo umount udisk
9.将块设备驱动的数据读取到ram.bin文件中
sudo cat /dev/mydisk1 > ram.bin
10.重新挂载查看
sudo mount -o loop ram.bin ~/udisk
去udisk目录下查看是否有刚才拷贝的文件,如果有就说明成功了
p 打印分区表
n 添加新分区
w 将分区表写入磁盘并退出
q 退出而不保存更改
在dev下产生/dev/mydisk1节点
5.分区格式化
sudo mkfs.ext2 /dev/mydisk1
6.挂载硬盘到某个目录下
sudo mount -t ext2 /dev/mydisk1 ~/udisk
7.向磁盘中存放文件
sudo cp ~/work/day10/02mydisk/mydisk.c .
8.取消挂载
cd
sudo umount udisk
9.将块设备驱动的数据读取到ram.bin文件中
sudo cat /dev/mydisk1 > ram.bin
10.重新挂载查看
sudo mount -o loop ram.bin ~/udisk
去udisk目录下查看是否有刚才拷贝的文件,如果有就说明成功了
更多推荐
已为社区贡献1条内容
所有评论(0)