一、Linux内核的块设备驱动
1.申请块设备设备号
//动态申请或者跟踪块设备号
int register_blkdev(unsigned int major,const char *name);
//释放块设备号
int unregister_blkdev(unsigned int major,const char *name) ;
2. 块设备驱动结构
抽象:
内核使用struct block_device结构体,其既可以来表示一个逻辑块设备对象又可以表示逻辑块设备中的某一分区,主要沟通文件系统与实际的块设备驱动程序。
struct block_device {
dev_t bd_dev;/*块设备号*/
struct inode * bd_inode;/* will die */
struct super_block * bd_super;
struct list_head bd_inodes;
struct block_device * bd_contains ; /*块设备分区所在的块设备*/
struct hd_struct * bd_part;/*块设备分区结构信息*/
struct gendisk * bd_disk;/*实际磁盘设备的抽象*/
struct request_queue * bd_queue;
struct list_head
bd_list;
};
分区信息:
struct hd_struct表示块设备上的某一分区信息
struct hd_struct {
sector_t start_sect; /*起始扇区*/
sector_t nr_sects; /*扇区的数量*/
struct device _dev; /*磁盘上的分区也被视为一个设备*/
int policy,partno; /*分区编号*/
};
实际的块设备:
在Linux内核中,struct gendisk用来表示一个实际磁盘设备(分区、未分区)的抽象,gendisk将直接被块设备驱动序分配及操控,磁盘的一个独立分区被看做是一个设备,对应/dev目录下的一个设备节点。
struct gendisk
{
int major; /*主设备号*/
int first_minor ; /*第1个次设备号*/
int minors; /*最大的次设备数,如果不能分区,则为1*/
char disk_name [32]; /*设备名称*/
struct hd_struct**part; /*磁盘上的分区信息*/
struct block_device_operations *fops; /*块设备操作结构体*/
struct request_queue *queue; /*请求队列*/
void *private_data; /*私有数据*/
sector_t capacity; /*扇区数,512字节为1个扇区*/
};
3.如何构造一个gendisk
(1) 分配gendisk
gendisk结构体是一个动态分配的结构体,它需要特别的内核操作来初始化,驱动不能自己分配这个结构体,而应该使用下列函数来分配gendisk:
struct gendisk *alloc_disk(int minors)
功能:动态分配并部分初始化gendisk对象参数: minors:该gendisk最大可分区的个数
返回值:
成功: struct gendisk指针对象首地址
失败:NULL
(2) 注册gendisk到内核
gendisk结构体被分配之后,系统还不能使用这磁盘,需要调用如下函数来注册这个磁盘设备。
void add_disk(struct gendisk *disk)
功能:注册struct gendisk对象
参数: disk:struct gendisk对象指针
返回值:无
要注意的是对add_disk()的调用必须发生在驱动程序的初始化工作完成并能响应磁盘的请求之后。
(3) 释放gendisk
当不再需要一个磁盘时,应当使用如下函数释放gendisk。
void del_gendisk(struct gendisk *disk)
功能:释放删除struct gendisk对象
参数: disk:struct gendisk对象指针
返回值:无
(4)gendisk引用计数
gendisk中包含一个kobject成员,因此,它是一个可被引用计数的结构体。
通过get_disk()和put_disk()函数可用来操作引用计数
(5)设置gendisk容量
块设备中最小的可寻址单元是扇区,扇区大小一般是2的整数倍,最常见的大小是512字节。扇区的大小是设备的物理属性,扇区是所有块设备的基本单元
void set_capacity(struct' gendisk *disk,sector_t size) ;
(6)操作函数集合(除读写访问的其他操作)
在块设备驱动中,有1个类似于字符设备驱动中file_operations结构block_device_operations结构体,它是对块设备操作的集合
struct block_device_operations
{
int(*open)(struct inode *,struct file*);//打开
int(*release)(struct inode *,struct file*);//释放
int(*ioctl)(struct inode *, struct file *, unsigned,unsigned long);//ioctl
int(*media_changed)(struct gendisk*);//介质被改变
int(*revalidate_disk)(struct gendisk*);//使介质有效
int(*getgeo)(struct block_device *, struct hd_geometry*);//填充驱动器信息
struct module *owner;//模块拥有者
};
4. gendisk中的请求队列
- 请求队列跟踪等候的块I/O请求,它存储用于描述这个设备能够支持的请求的类型信息、它们的最大大小、硬件扇区大小、对齐要求等参数,其结果是:如果请求队列被配置正确了,它不会交给该设备一个不能处理的请求。
- 请求队列还实现一个插入接口,这个接口允许使用多个I/O调度器,I/O调度器(也称电梯)的工作是以最优性能的方式向驱动提交I/O请求。大部分I/O调度器累积批量的I/O请求,并将它们排列为递增(或递减)的块索引顺序后提交给驱动。
(1)请求队列结构
一个块请求队列是一个块I/O请求的队列
struct request_queue
{
/*双向链表数据结构,将所有加入到队列的IO请求组建成一个双向链表*/
struct list_head queue_head;
/*块设备驱动程序需要实现的请求处理函数*/
request_fn_proc *request_fn;
make_request_fn *make_request_fn;
unsigned long nr_requests; /*最大的请求数量*/
unsigned long queue_flags ; /*当前请求队列的状QUEUE_FLAG_STOPPED*/
};
(2)请求的结构体
Linux块设备驱动中使用request结构体来描述的I/O请求
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链表*/
char *buffer : /*kaddr of the current segment ifavailable */
};
(3)构造请求队列
方法一:make_request
struct request_queue *blk_alloc_queue(gfp_tgfp_mask);
功能:分配请求队列
参数:gfp_mask :分配标志
返回值:
成功:struct request_queue指针对象
失败: NULL
void blk_queue_make_request(structrequest_queue *queue,make_request_fn *mfn);
功能:填充请求队列中的的make_request_fn成员提供一个实现
参数: queue :请求队列
make_request_fn:make_request_fn
函数返回值:无
方法二:request
struct request_queue *blk_init_queue (request_fn_proc *rfn,spinlock_t*lock)
功能:分配请求队列并安装请求处理函数
参数: rfn:处理请求队列中各个请求的处理函数
lock:自旋锁
返回值:成功:struct request_queue指针对象
失败:NULL
typedef void (request_fn_proc) (struct request_queue *q);
void blk_cleanup_queue(struct request_queue *q)
功能:将请求队列占用的资源释放
参数: q:请求队列的指针对象
返回值:无
构建请求队列两种方式比较.
-
make_request
-
此时的make_request_fn是驱动程序自己完成的而非系统提供,驱动程序实现的make_request_fn直接面对bio,而不是请求对象req
-
bio——————————————————>make_request_fn
-
request
-
外部bio对象先被make_request_fn拦截处理,该函数被系统提供blk_queue_bio [mfn]
-
make_request_fn :优化目标对象的请求队列中的各个请求,将bio对象中的相关数据转存到请求对象req中,然后将其作为参数传递给请求处理的回调函数request_fn。
-
bio——————————>req——————————>request_fn
构建请求的处理函数
struct request *blk_fetch_request(struct request_queue *q)
功能:获取请求队列中的第一个请求对象
参数:q:请求队列对象指针
返回值:
成功:struct request指针
失败:NULL
static inline sector_t blk_rq_pos(const struct request *rq)
功能:获取当前读写扇区的位置
参数: rq:请求对象指针
返回值:正在操作的扇区号
static inline unsigned int blk_rq_cur_sectors(const struct request *rq)
功能: bytes left in the current segment
#define rq_data_dir(rq) ((rq)->cmd_flags & 1)
功能:获取当前请求的读写方向READ = 0 ,WRITE = 1
bool _blk_end_request_cur(struct request *rq, int error)
功能:将当前请求处理完毕后结束请求
参数: rq:请求结构体指针对象
error :0成功<0:失败
返回值:
0:处理完成该请求
1: buffer中的请求挂起未被处理完成
5.请求中的 bio结构体
(1)bio结构体
通常一个request请求可以包含多个bio,一个bio对应一个I/O请求
struct bio
{
sector_t bi_sector; /*指定了本次传输的起始扇区号*/
struct bio *bi_next; /*指向当前bio的下一个对象*/
unsigned long bi_flags; /*状态、命令等*/
unsigned long bi_rw; /*表示READ/WRITE*/
struct block_device bi_bdev; /*与请求相关联的块设备对象指针*/
unsigned short bi_vcnt; /* birio_vec数组中元素个数*/
unsigned short bi_idx; /*当前处理的bi_io_vec数组元素索引*/
unsigned int bi_size; /*本次传输需要传输的数据总量,byte(扇区大小整数倍)*/
struct bio_vec *bi_io_vec;
/* 指向一个IO向量的数组,数组中的内各元素对应一个物理页的page对象*/
} ;
(2)bio相关的操作函数
内核传输数据或者命令时需要向对应块设备所属的请求队列发送请求对象时,使用submit_bio.
void submit_bio(int rw,struct bio *bio)
功能:向请求队列发送请求对象
参数: rw:读写标识
bio:struct bio的指针对象
返回值:无
(1) #define bio_for_each_segment(bvl, bio,i)\
_bio_for_each_segment(bvl, bio, i,(bio)->bi_idx)
//遍历bio成员中的bi_io_vec数组中的各个bio_vec元素
(2) #define bio_iovec_idx(bio, idx) (&((bio)->bi_io_vec[(idx)]))
//得到bi_io_vec数组中第idx个元素对象的指针
(3) #define bio_iovec(bio) bio_iovec_idx((bio),(bio)->bi_idx)
//得到当前正在处理的(由bi_idx做索引) bi_io_vec数组元素的指针
(4) #define bio_page(bio) bio_iovec((bio))->bv_page
//获取当前正在处理的bi_io_vec数组元素的页面对象page
(5)#define bio_offset(bio) bio_iovec((bio))->bv_offset
//获取当前缓冲区的偏移量
(6) #define bio_sectors(bio) ((bio)->bi_size >>9)
//获取用于获得当前bio对象要传输的扇区总数
(3) bio_vec数组元素
bio_vec结构体
- bio的核心是一个称为bi_io_vec的数组
struct bio_vec {
struct page *bv_page;//指向用于数据传输的页面所对应的struct page对象
unsigned int bv_len;//表示当前要传输的数据大小
unsigned int bv_offset;//表示数据在页面内的偏移量
};
6.块设备驱动的总体逻辑结构
block_deivce(抽象块设备)—>gendisk(实际磁盘设备)—>request queve(请求队列)—>request(请求)—>bio链表——bio_vec数组
二、编写网卡设备驱动
1.先写个模块
#include <linux/init.h>
#include <linux/module.h>
//加载函数
static int __init virblk_init(void)
{
return 0;
}
//卸载函数
static void __exit virblk_exit(void)
{
}
module_init(virblk_init);
module_exit(virblk_exit);
MODULE_LICENSE("GPL");//GPL模块许可证
MODULE_AUTHOR("YYD");//作者信息
MODULE_VERSION("1.0");//软件版本
MODULE_DESCRIPTION("virtual block driver");//描述信息
2.写个Makefile编译一下
ifeq ($(arch),arm)
KERNELDIR :=/home/linux/linux-5.10.61
CROSS_COMPILE ?=arm-linux-gnueabihf-
else
KERNELDIR :=/lib/modules/$(shell uname -r)/build
CROSS_COMPILE ?=
endif
modname ?=
PWD :=$(shell pwd)
CC :=$(CROSS_COMPILE)gcc
all:
make -C $(KERNELDIR) M=$(PWD) modules
# $(CC) test.c -o test
clean:
make -C $(KERNELDIR) M=$(PWD) clean
# rm test
install:
cp *.ko ~/nfs/rootfs/
# cp test ~/nfs/rootfs/
help:
echo "make arch = arm or x86 modname= dirvers file name"
obj-m:=$(modname).o
3. 再写块设备驱动框架
#include <linux/init.h>
#include <linux/module.h>
#include <linux/slab.h>
#include <linux/spinlock.h>
#include <linux/blkdev.h>
#include <linux/genhd.h>
#include <linux/hdreg.h>
#include <linux/vmalloc.h>
//块设备结构
struct _virblk_dev
{
struct gendisk *gd;
u8 *data;
unsigned long size; //大小
spinlock_t lock;
};
struct _virblk_dev virblk_device;
int blk_dev; //设备号
struct request_queue *virblk_queue; //请求队列
//扇区大小和扇区个数
int per_sector_size = 512;
int nsectors = 1024;
// gendisk 操作函数集合
struct block_device_operations virblk_fops = {
.owner = THIS_MODULE,
};
//请求处理函数
/*********************************************
* sector 所在扇区
* nbytes 数据大小
* rdwr 读写方向
* buf 读写的数据
* *******************************************/
void virblk_transfer(sector_t sector, unsigned int nbytes, int rdwr, char *buf)
{
unsigned int offset = sector * per_sector_size;
if (offset + nbytes >= virblk_device.size)
{
printk("out of range!\n");
return;
}
//读写操作
if (rdwr)
{
//写
//实际块设备处理bio操作,这里用内存模拟
memcpy(virblk_device.data + offset, buf, nbytes);
}
else
{
memcpy(buf, virblk_device.data + offset, nbytes);
}
}
//请求队列处理请求函数
void virblk_request_func(struct request_queue *q)
{
struct request *req = NULL;
//从请求队列中取出一个请求
req = blk_fetch_request(q);
while (req)
{
//处理取出的请求
virblk_transfer(blk_rq_pos(req),blk_rq_cur_sectors(req),rq_data_dir(req),req->buffer);
if (!__blk_end_request_cur(req, 0))
{
//请求处理完取出下一个请求
req = blk_fetch_request(q);
}
}
}
//加载函数
static int __init virblk_init(void)
{
//申请内存空间模拟磁盘空间
virblk_device.size = per_sector_size * nsectors;
virblk_device.data = vmalloc(virblk_device.size);
// 1.申请设备号
blk_dev = register_blkdev(0, "virblk");
// 2.申请gendisk
virblk_device.gd = alloc_disk(1);
// 3.初始化 gendisk
virblk_device.gd->major = blk_dev;
virblk_device.gd->first_minor = 0;
virblk_device.gd->fops = &virblk_fops;
strcpy(virblk_device.gd->disk_name, "virblock0");
//设置容量
set_capacity(virblk_device.gd, nsectors);
// 4.创建请求队列
spin_lock_init(&virblk_device.lock);
virblk_queue = blk_init_queue(virblk_request_func,&virblk_device.lock);
//关联请求队列和gendisk
virblk_device.gd->queue = virblk_queue;
//5. 将gendisk添加到内核
add_disk(virblk_device.gd);
return 0;
}
//卸载函数
static void __exit virblk_exit(void)
{
//从内核中删除gedisk
del_gendisk(virblk_device.gd);
//引用计数减一
put_disk(virblk_device.gd);
//清空等待队列
blk_cleanup_queue(virblk_queue);
//注销块设备号
unregister_blkdev(blk_dev,"virblk");
//释放内存空间
vfree(virblk_device.data);
}
module_init(virblk_init);
module_exit(virblk_exit);
MODULE_LICENSE("GPL"); // GPL模块许可证
MODULE_AUTHOR("YYD"); //作者信息
MODULE_VERSION("1.0"); //软件版本
MODULE_DESCRIPTION("virtual block driver"); //描述信息