在前一章,我们具体的介绍了简单字符设备驱动的编写,在具体的工程中,需要考虑的比我们写的复杂得多,还要考虑电源管理,以及定时器的一些东西。
这一章,我们会介绍linux三种设备驱动中的块设备驱动。块设备与字符设备有很大的不同。
字符设备与块设备I/O操作的不同如下:
1)块设备只能以块为单位接受输入和返回输出,而字符设备则是以字节为单位;大多数的设备是字符设备,因为他们不需要缓冲而且不易固定块大小进行操作;
2)块设备对于I/O请求有对应的缓冲区,因此他们可以选择以什么顺序进行响应,字符设备无需缓冲且被直接读写;
对于存储设备而言,调整读写的顺序作用巨大,因为读写连续的扇区比分离的扇区更快。
3)字符设备只能被顺序读写,而块设备可以随机访问。虽然块设备可以随机访问,但是对于磁盘这类机械设备设备而言,顺序的组织块设备的访问可以提高性能。
分析主要的数据结构、函数及其关系
在块设备驱动中,有一个类似于字符设备驱动中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
long(*unlocked_ioctl)(struct file *, unsigned, unsigned long);
long(*compat_ioctl)(struct file *, unsigned, unsigned long);
int(*direct_access)(struct block_device *, sector_t, unsigned long*);
int(*media_changed)(struct gendisk*); //介质被改变?
int(*revalidate_disk)(struct gendisk*); //使介质有效
int(*getgeo)(struct block_device *, struct hd_geometry*);//填充驱动器信息
struct module *owner; //模块拥有者
};
int (*open)(struct inode *inode, struct file *filp);
int (*release)(struct inode *inode, struct file *filp);
//与字符设备驱动类似,当设备被打开和关闭时将调用它们
int (*ioctl)(struct inode *inode, struct file *filp, unsigned int cmd,unsigned long arg);
//上述函数是ioctl()系统调用的实现,块设备包含大量的标准请求,这些标准请求由Linux 块设备层处理,因此大部分块设备驱动的ioctl()函数相当短。
int (*media_changed) (struct gendisk *gd);
//被内核调用来检查是否驱动器中的介质已经改变,如果是,则返回一个非0 值,否则返回0。这个函数仅适用于支持可移动介质的驱动器,通常需要在驱动中增加一个表示介质状态是否改变的标志变量,非可移动设备的驱动不需要实现这个方法。
int (*revalidate_disk) (struct gendisk *gd);
//revalidate_disk()函数被调用来响应一个介质改变,它给驱动一个机会来进行必要的工作以使新介质准备好。
int (*getgeo)(struct block_device *, struct hd_geometry *);
//该函数根据驱动器的几何信息填充一个hd_geometry 结构体,hd_geometry 结构体包含磁头、扇区、柱面等信息。
struct module *owner;
//一个指向拥有这个结构体的模块的指针,它通常被初始化为THIS_MODULE。
在Linux内核中,使用gendisk(通用磁盘)结构体表示一个独立的磁盘设备(或者分区),结构体定义如下:
struct gendisk {
int major; //设备的主设备号,和register_blkdev()中的major相同
int first_minor; //起始次设备号,dengyu0,则表示次设备是从0开始的
int minors; //分区(次设备)的数量,当使用alloc_disk()时,就会自动设置该成员
char disk_name[32]; //块设备名称,和register_blkdev()中的name相同
struct hd_struct **part; //分区表的信息
int part_uevent_suppress;
struct block_device_operations *fops; //块设备操作函数的集合
struct request_queue *queue; //请求队列,用于管理该设备IO请求队列的指针
void *private_data; //私有数据
sector_t capacity; //扇区数,512字节为一个扇区,描述的是设备的容量
...
};
struct request{
struct list_head queuelist; //用于挂载请求队列链表的节点,使用函数elv_next_request()访问他,而不能直接访问
struct list_head donelist; //用于挂载已完成的请求链表的节点
struct request_queue *q; //指向请求队列
unsigned int cmd_flags; //命令标识
enum rq_cmd_type_bits cmd_type; //读写命令标志,为0(READ)表示为读,为1(WRITE)表示为写
sector_t sector; //要提交的下一个扇区的偏移位置(offset)
...
unsigned int current_nr_sectors; //当前需要传送的扇区数(长度)
...
char *buffer; //当前请求队列链表的申请里面的数据,用来读写扇区数据(源地址)
...
}
int register_blkdev(unsigned int major, const char *name);
//创建一个块设备,
unregister_blkdev(unsigned int major, const char *name);
struct gendisk *alloc_disk(int minor);
void del_gendisk(struct gendisk *disk);
request_queue_t *blk_init_queue(request_fn_proc *rfn, spinlock_t *lock);
request_queue_t * blk_alloc_ueue(int gfd_mask);
void blk_cleanup_queue(request_queue_t *q);
static DEFINE_SPINLOCK(spinlock_t lock);
static inline void set_capacity(struct gendisk *disk, sector_t size);
disk->capacity = size
void add_disk(struct gendisk *gd);
void put_disk(struct gendisk *disk);
struct request *elv_next_request(request_queue_t *q);
获取成功返回一个request结构体,不成功返回NULL
void end_request(struct request *req, int uptodate);
结束获取申请,当uptodate=0时,表示该申请读写扇区失败,uptodate=1,表示读写成功
static inline void *kzalloc(size_t size, gfp_t flags);
用来当做我们的磁盘扇区用,分配成功返回缓存地址,分配失败会返回0
void kfree(const void *block);
rq_data_dir(rq);
void blk_queue_hardsect_size(request_queue_t *queue, unsigned short max);
参数1:申请队列
参数2:扇区大小
1)使用register_blkdev()
创建一个块设备
2)blk_init_queue()
使用分配一个申请队列,并赋申请队列处理函数
3)使用alloc_disk()
分配一个gendisk
结构体
4)设置gendisk
结构体成员
4.1)设置成员函数(major、first_minor、disk_name、fops)
4.2)设置queue
成员,等于之前分配的申请队列
4.3)通过set_capacity()
设置capacity
成员,等于扇区数
5)使用kzalloc()
来获取缓存地址,用作扇区
6)使用add_disk()
注册gendisk
结构体
1)在while()循环中使用elv_next_request()
获取申请队列中的每个未处理的函数
2)使用rq_data_dir()
来获取每个申请的读写命令标志,为0(READ)表示读,为1(WRITE)表示写
3)使用memcp()
来读或者写扇区(缓存)
4)使用end_request()
来结束获取的每个申请
1)使用put_disk()
和del_gendisk()
来注销,释放gendisk
结构体
2)使用kfree()
释放磁盘扇区缓存
3)使用blk_cleanup_queue()
清除内存中的申请队列
4)使用unregister_blkdev()
卸载块设备
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include /* size_t */
#include /* O_ACCMODE */
#include /* HDIO_GETGEO */
#include
#include
#include
#include
#include /* invalidate_bdev */
#include
//主设备号
static int sbull_major = 0;
module_param(sbull_major,int,0);
//扇区的大小
static int hardsect_size =512;
module_param(hardsect_size,int,0);
//扇区数
static int nsectors = 1024;
module_param(nsectors,int,0);
//设备数量
static int ndevices = 1;
module_param(ndevices,int,0);
//这是选择用哪一种处理request的方法
enum {
RM_SIMPLE = 0,
RM_FULL = 1,
RM_NOQUEUE = 2,
};
//请求的模式
static int request_mode = RM_SIMPLE;
module_param(request_mode, int, 0);
#define SBULL_MINORS 16 //块设备数
#define KERNEL_SECTOR_SIZE 512 //扇区的大小
#define INVALIDATE_DELAY 30*HZ
//所有 Linux 内核驱动都会声明一个数据结构来存储驱动需要频繁访问的状态信息
//做一个全局变量
struct sbull_dev{
int size; //以扇区为单位, 设备的大小
u8 *data; // 数据数组
short users;
short media_change; //
spinlock_t lock; //自旋锁
struct request_queue *queue; //申请队列
struct gendisk *gd; //gendisk结构体
struct timer_list timer; //内核定时器
};
static struct sbull_dev *Devices = NULL;
//具体的块设备I/O操作,在xxx_request中调用
static void sbull_transfer(struct sbull_dev *dev,unsigned long sector,unsigned long nsect,char *buffer,int write)
{
//sector将要提交的下一个扇区, nsect在当前段中将完成的扇区数
unsigned long offset = sector * KERNEL_SECTOR_SIZE; //偏移量
unsigned long nbytes = nsect * KERNEL_SECTOR_SIZE; //长度
if((offset + nbytes) > dev->size) {
//如果当前段的长度加上下一个扇区的长度(要传递的数据) 超过了 设备的长度(即允许范围)
printk(KERN_NOTICE "Beyond-end write (%ld %ld)\n",offset,nbytes);
return;
}
if(write)
memcpy(dev->data + offset,buffer,nbytes);
else
memcpy(buffer,dev->data + offset,nbytes);
}
//块设备请求函数,简单的请求函数
//RM_SIMPLE 使用blk_init_queue()是会调用xxx_queue()
static void sbull_request(struct request_queue *q)
{
struct request *req; //定义请求结构体
//电梯调度算法,获得队列中第一个未完成的请求
req = blk_fetch_request(q);
while(req != NULL){
//设备指针指向私有数据
struct sbull_dev *dev = req->rq_disk->private_data;
if(req->cmd_type != REQ_TYPE_FS){
printk(KERN_NOTICE "Skip non-fs request\n");
blk_end_request_all(req,-EIO);
continue;
}
sbull_transfer(dev,blk_rq_pos(req),blk_rq_cur_sectors(req), req->buffer,rq_data_dir(req));
// blk_rq_pos(req), 扇区光标所在的位置,
//blk_rq_cur_sectors(req), 需要传输的扇区数目
//req->buffer, 要传输或者接受的数据缓存区
//rq_data_dir(req), 获取传输方向,0表示读,1表示写
if(!__blk_end_request_cur(req,0)){
//下一个请求
req = NULL;
}
}
}
//bio的操作,传输一个单独的io请求
//在xxx_make_request中调用
static int sbull_xfer_bio(struct sbull_dev *dev, struct bio *bio)
{
int i;
struct bio_vec *bvec; //定义实际的 vec 列表
sector_t sector = bio->bi_sector; //定义要传输的第一个扇区
bio_for_each_segment(bvec,bio,i){
//下面的宏遍历bio的每一段,获得一个内核虚拟地址来存取缓冲
char *buffer = __bio_kmap_atomic(bio,i,KM_USER0); //通过kmap_atomic()函数获得返回bio的第i个缓冲区的虚拟地址
sbull_transfer(dev,sector,bio_cur_bytes(bio)/KERNEL_SECTOR_SIZE,buffer,bio_data_dir(bio) == WRITE);
//sector, 开始扇区的索引号
//bio_cur_bytes(bio)/KERNEL_SECTOR_SIZE,需要传输的扇区数
//buffer, 传输数据的缓冲区指针
//bio_data_dir(bio) == WRITE); 传输方向,0表述从设备读,非0从设备写
sector += bio_cur_bytes(bio)/KERNEL_SECTOR_SIZE;;//返回扇区数
__bio_kunmap_atomic(bio,KM_USER0); //返回由 __bio_kmap_atomic()获得的内核虚拟地址
}
return 0;
}
//bio的操作,传输一个完整的request,其中会包含传输一个单独的io请求
//在xxx_full_request中调用
static int sbull_xfer_request(struct sbull_dev *dev,struct request *req)
{
struct bio *bio;
int nsect = 0;
__rq_for_each_bio(bio,req){
//此宏遍历请求中的每个bio,传递用于sbull_xfer_bio()传输的指针
sbull_xfer_bio(dev,bio);//调用 bio 处理函数
nsect += bio->bi_size / KERNEL_SECTOR_SIZE; //传递的字节数/扇区大小等于扇区数
}
return nsect;
}
//更为强大的请求函数,
//RM_FULL时,在blk_init_queue函数中使用,
static void sbull_full_request(struct request_queue *q)
{
struct request *req;
struct sbull_dev *dev = q->queuedata;
req = blk_fetch_request(q);
//遍历每个请求
while(req != NULL){
//判断请求是否是个文件系统请求
if(req->cmd_type != REQ_TYPE_FS){
printk(KERN_NOTICE "Skip non-fs request\n");
blk_end_request_all(req,-EIO); //该请求是一个I/O error
continue;
}
//调用请求处理函数
sbull_xfer_request(dev,req);
if(!blk_end_request_cur(req,0)){
//req指向下一个请求
req = NULL;
}
}
}
//制造请i去函数
//RM_NOQUEUE,使用blk_alloc_queue时,在blk_queue_make_request函数中使用
static int sbull_make_request(struct request_queue *q,struct bio *bio)
{
struct sbull_dev *dev = q->queuedata;
int status;
status = sbull_xfer_bio(dev,bio);
bio_endio(bio,status); //函数通知处理结束
return 0;
}
//open函数
static int sbull_open(struct block_device *bd, fmode_t mode)
{
struct sbull_dev *dev = bd->bd_disk->private_data;
del_timer_sync(&dev->timer); //介质移除定时器: 销毁定时器
spin_lock(&dev->lock);
if(!dev->users)
check_disk_change(bd);
dev->users++;
spin_unlock(&dev->lock);
return 0;
}
static int sbull_release(struct gendisk *gd, fmode_t mode)
{
struct sbull_dev *dev = gd->private_data;
spin_lock(&dev->lock);
dev->users--;
if(!dev->users){
dev->timer.expires = jiffies + INVALIDATE_DELAY;
add_timer(&dev->timer); //介质移除定时器: 加载定时器 ,30s
}
spin_unlock(&dev->lock);
return 0;
}
//介质改变
int sbull_media_change(struct gendisk *gd)
{
struct sbull_dev *dev = gd->private_data;
return dev->media_change;
}
/*调用此函数内核将试着重新读取分区表,在这里这个函数这是简单的重置 media_change 的标志位,并
* 清除内存空间以模拟插入一张磁盘
*/
int sbull_revalidate(struct gendisk *gd)
{
struct sbull_dev *dev = gd->private_data;
if(dev->media_change){
dev->media_change = 0;
memset(dev->data,0,dev->size);
}
return 0;
}
/* The "invalidte"function runs out of the device timer;it sets a flag to
* simulate the removal of the media.
*/
void sbull_invalidate(unsigned long ldev)
{
struct sbull_dev *dev = (struct sbull_dev *)ldev;
spin_lock(&dev->lock);
if(dev->users || !dev->data)
printk (KERN_WARNING "sbull: timer sanity check failed\n");
else
dev->media_change = 1;
spin_unlock(&dev->lock);
}
/* *ioctl: *暂时处理一个命令: 对设备的物理信息的查询请求 */
int sbull_ioctl(struct block_device *bd, fmode_t mode, unsigned cmd, unsigned long arg)
{
return 0;
}
/* 获取几何信息 */
static int sbull_getgeo(struct block_device *bd, struct hd_geometry *geo)
{
long size;
struct sbull_dev *dev = bd->bd_disk->private_data;
size = dev->size *(hardsect_size / KERNEL_SECTOR_SIZE);
geo->cylinders = (size & ~0x3f) >> 6;
geo->heads = 4;
geo->sectors = 16;
geo->start = 4;
return 0;
}
static struct block_device_operations sbull_ops = {
.owner = THIS_MODULE,
.open = sbull_open,
.release = sbull_release,
.media_changed = sbull_media_change,
.revalidate_disk = sbull_revalidate,
.ioctl = sbull_ioctl,
.getgeo = sbull_getgeo,
};
/*设置设备
为sbull_dev结构体分配内存
分配申请队列
*/
/*初始化 sbull_dev 数据结构的具体实现*/
static void setup_device(struct sbull_dev *dev,int which)
{
printk(KERN_WARNING "sbull: step into into setup_device\n");
memset(dev,0,sizeof(struct sbull_dev));
dev->size = nsectors * hardsect_size; //整个块设备大小1024(扇区个数) * 512(扇区大小)
dev->data = vmalloc(dev->size); /* 开辟虚拟存储空间 */
if(dev->data == NULL){
printk(KERN_NOTICE "vmalloc failure\n");
return;
}
spin_lock_init(&dev->lock); /* 初始化自旋锁 */
init_timer(&dev->timer); /* 初始化定时器 */
dev->timer.data = (unsigned long)dev;
dev->timer.function = sbull_invalidate; /* 超时处理函数 */
/* * The I/O queue, depending on whether we are using our own * make_request function or not. */
switch(request_mode){
case RM_NOQUEUE:
dev->queue = blk_alloc_queue(GFP_KERNEL); /* 分配“请求队列” */
if(dev->queue == NULL)
goto out_vfree;
blk_queue_make_request(dev->queue,sbull_make_request); /*绑定"制造请求"函数 */
break;
case RM_FULL:
dev->queue = blk_init_queue(sbull_full_request,&dev->lock); /*请求队列初始化*/
if(dev->queue == NULL)
goto out_vfree;
break;
case RM_SIMPLE:
dev->queue = blk_init_queue(sbull_request,&dev->lock); /*请求队列初始化*/
if(dev->queue == NULL)
goto out_vfree;
break;
default:
printk(KERN_NOTICE "Bad request mode %d, using simple\n", request_mode);
}
blk_queue_logical_block_size(dev->queue,hardsect_size); /* 硬件扇区尺寸设置 */
dev->queue->queuedata = dev;
dev->gd = alloc_disk(1); /* 动态分配 gendisk 结构体*/
if(!dev->gd){
printk(KERN_NOTICE "alloc_disk failure\n");
goto out_vfree;
}
dev->gd->major = sbull_major; //主设备号
dev->gd->first_minor = which * SBULL_MINORS; //次设备号
dev->gd->fops = &sbull_ops; //块设备操作函数结构体
dev->gd->queue = dev->queue; //请求队列”
dev->gd->private_data = dev; //私有数据
snprintf(dev->gd->disk_name,32,"sbull%c",which + 'a'); /* 次设备的名字 */
/* 每个请求的大小都是扇区大小的整数倍,内核总是认为扇区大小是512字节,因此必须进行转换 */
set_capacity(dev->gd,nsectors * (hardsect_size / KERNEL_SECTOR_SIZE));
add_disk(dev->gd); /* 完成以上初始化后,调用 add_disk 函数来注册这个磁盘设备 */
return;
out_vfree:
if(dev->data)
vfree(dev->data);
}
static int __init sbull_init(void)
{
int i;
printk(KERN_WARNING "sbull: start init\n");
// 注册块设备,第一个参数是设备号,0为动态分配,第二个参数是设备名
sbull_major = register_blkdev(sbull_major,"sbull");
if (sbull_major <= 0) {
printk(KERN_WARNING "sbull: unable to get major number\n");
return -EBUSY;
}
printk(KERN_WARNING "sbull: start kmalloc\n");
Devices = kmalloc(ndevices * sizeof(struct sbull_dev),GFP_KERNEL); /* 为块核心数据结构 sbull_dev 分配空间 */
if(Devices == NULL)
goto out_unregister;
printk(KERN_WARNING "sbull: start setup_device\n");
for(i = 0; i < ndevices; i++)
setup_device(Devices + i,i); /* 初始化 sbull_dev 核心数据结构 ,并add_disk*/
return 0;
out_unregister:
unregister_blkdev(sbull_major,"sbd");
return -ENOMEM;
}
static void sbull_exit(void)
{
int i;
for (i = 0; i < ndevices; i++) {
struct sbull_dev *dev = Devices + i;
del_timer_sync(&dev->timer);
if (dev->gd) {
del_gendisk(dev->gd);
put_disk(dev->gd);
}
if (dev->queue) {
if (request_mode == RM_NOQUEUE)
kobject_put(&(dev->queue)->kobj);
else
blk_cleanup_queue(dev->queue);
}
if (dev->data)
vfree(dev->data);
}
unregister_blkdev(sbull_major, "sbull");
kfree(Devices);
}
module_init(sbull_init);
module_exit(sbull_exit);
MODULE_LICENSE("Dual BSD/GPL");