1、块设备
块设备将数据按照固定块大小的块中,每个块的大小通常在512字节到32768字节之间,磁盘、SD卡都是常见的块设备。
2、字符设备和块设备的区别:
字符设备 块设备
----------------------------------------------
按字节访问 按块进行访问
只能按照数据流访问 随机访问
直接访问设备 挂在文件系统的方式访问
3、Linux块设备处理模型
|-------------------------------------------------------------|
| VFS |
|-------------------------------------------------------------|
| | | |
|Disk Caches | |
| | | |
|-------------------------------------------------------------|
|Disk Filesystem | Disk Filesystem | Block Device File |
|----------------- -----------------------|
| Mapping Layer |
|-------------------------------------------------------------|
| Generic Block Layer |
|-------------------------------------------------------------|
| I/O Scheduler Layer |
|-------------------------------------------------------------|
| Block Device Driver | Block Device Driver |
|------------------------ | -------------------- |
| Hard Disk | Hard Disk |
|------------------------------------------------------------—|
VFS:虚拟文件系统,VFS是对各种具体文件系统的一种封装,为用户程序提供访问文件的统一接口。
Disk Cache:当用户发起文件访问请求的时候,首先会到Disk Cache中寻找文件是否被缓存,如果缓存中有则在Cache中读取,如果数据没有被缓存,那就必须到文件系统中去读取。
Mapping Layer:
1、首先确定文件系统的block size,然后计算所请求的数据包含多少个block,
2、调用具体文件系统的函数来访问文件的inode,确定所请求数据在磁盘上面的逻辑地址。
Generic block layer:
Linux内核为快设备抽象成了统一的模型,把块设备看做是若干个扇区组成的数据空间。来完成块设备的相关核心功能。
I/O scheduler layer:
I/O调度层负责将I/O操作顺序,采用电梯算法。提高 I/O 调度器的效率也是影响整个系统对块设备上数据管理效率的一个方面。
Block Device Driver:
快设备驱动程序,完成和硬件的具体交互,块设备相关数据结构
4、设备描述 gendisk结构体
内核使用 gendisk 结构来表示一个独立的磁盘设备,内核还使用 gendisk 结构来表示分区,在此结构中,很多程序必须由驱动程序来进行初始化。该结构体定义中。
4.1、gendisk结构体解析
struct gendisk {
int major; /*设备主设备号*/
int first_minor; /*起始次设备号*/
int minors; /*次设备号的数量,也称为分区数量,如果改值为1,表示无法分区*/
char disk_name[32]; /*设备名称*/
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字节为1个扇区,描述设备容量*/
....
};
struct gendisk *alloc_disk(int minors)
分配一个gendisk结构,minors为此设备好的个数 = 分区数+1
void del_gendisk(struct gendisk *disk)
当不需要这个磁盘的时候,释放gendisk结构
endisk中包含一个kobject成员, 它是一个可被引用计数的结构体。通过get_disk()和put_disk()函数可用来操作引用计数。驱动通常不需要做这个。
struct kobject *get_disk(struct gendisk *disk);
void put_disk(struct gendisk *disk);
void add_disk(struct gendisk *gd);
对gendisk初始化之后还不能使用和这个设备,应该使用add_disk注册磁盘设备
static inline void set_capacity(struct gendisk *disk, sector_t size)
设置gendisk容量,块设备中最小单位是扇区,扇区的大小一般是2的整数倍,最常见的大小是512kb (xx>>9)表示/512 (xx<<9)表示*512。扇区大小是物理设备所决定的。
5、block_device_operationse结构体
类似与字符设备驱动程序中的file_operations结构,该集合用于控制设备的操作,但是在大多数情况下都是以mount的方式进行访问,用户程序一般不会直接访问块设备中的文件。需要包含头文件。
struct block_device_operations {
/*打开这个设备时候调用*/
int (*open) (struct inode *, struct file *);
/*关闭/释放 时候调用*/
int (*release) (struct inode *, struct file *);
/*ioctl()系统调用的实现,块设备包含大量的标准请求,这些标准请求由 Linux 块设备层处理,因此大部分块设备驱动的*/
int (*ioctl) (struct inode *, struct file *, unsigned, unsigned long);
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 *);
/*内核周期调用检查驱动器介质有没有发生改变,改变返回非0,没有改变返回0。用于支持可移动设备,非可以移动设备不用实现*/
int (*media_changed) (struct gendisk *);
/*被调用响应介质被改变,驱动进行必要的工作*/
int (*revalidate_disk) (struct gendisk *);
/*根据驱动器的几何信息填充hd_geometry,包含磁头,柱面,扇区等信息.*/
int (*getgeo)(struct block_device *, struct hd_geometry *);
/*模块拥有者,一般初始化为THIS_MODULE*/
struct module *owner;
};
block_device_operations 结构中没有实际读或写数据的函数,在块 I/O 子系统中,这些操作由请求函数处理。
6、request 和 bio 结构体
struct request {
struct list_head queuelist;/*请求链表*/
struct list_head donelist;
request_queue_t *q; /*请求所属队列*/
unsigned int cmd_flags;
enum rq_cmd_type_bits cmd_type;
sector_t sector; /* 当前扇区 */
sector_t hard_sector; /*要传输的下一个扇区*/
unsigned long nr_sectors;/* 要传输的扇区数目*/
unsigned long hard_nr_sectors; /*要被完成扇区的数目 */
unsigned int current_nr_sectors;/*当前传送的扇区*/
unsigned int hard_cur_sectors; /*当前要被完成的扇区数目*/
struct bio *bio; /*请求的block i/o(bio)结构体链表首 request请求 第一个bio*/
struct bio *biotail;/*请求的bio结构体链表尾 request请求 最后个bio*/
char *buffer; /*request请求中断 第一个bio*/
int ref_count;/*引用计数*/
.....................
};
struct request_queue
{
..........................
/*自旋锁 保护队列结构*/
spinlock_t __queue_lock;
spinlock_t *queue_lock;
/*kobject队列 */
struct kobject kobj;
/* queue settings */
unsigned long nr_requests; /*最大的请求数量*/
unsigned int nr_congestion_on;
unsigned int nr_congestion_off;
unsigned int nr_batching;
unsigned short max_sectors; /*最大扇区数*/
unsigned short max_hw_sectors;
unsigned short max_phys_sectors;/*最大的段数*/
unsigned short max_hw_segments;
unsigned short hardsect_size; /*硬件扇区尺寸*/
unsigned int max_segment_size; /*最大的段尺寸*/
unsigned long seg_boundary_mask;/*段边界掩码*/
unsigned int dma_alignment; /*DMA传送内存对齐限制*/
struct blk_queue_tag* queue_tags;
atomic_t refcnt; /*引用计数*/
unsigned int in_flight;
unsigned int sg_timeout;
unsigned int sg_reserved_size;
int node;
struct list_head drain_list;
struct request* flush_rq;
unsigned char ordered;
};
关于request_queue的操作:
/*初始化请求队列*/
kernel elevator = deadline;/*给kernel添加启动参数*/
request_queue_t *blk_init_queue(request_fn_proc *rfn, spinlock_t *lock)
/*
*两个参数分别是请求处理函数指针 控制队列访问权限的自旋锁
*此函数会分配内存,需要判断返回值,在加载函数中调用
*/
/*清除请求队列*/
void blk_cleanup_queue(request_queue_t * q)
/*
* 此函数完成将请求队列返回给系统的任务,一般在卸载函数中调用.
* 此函数即bld_put_queue()的宏定义#define blk_put_queue(q) blk_cleanup_queue((q))
*/
/*分配请求队列*/
request_queue_t *blk_alloc_queue(gfp_t gfp_mask)
void blk_queue_make_request(request_queue_t * q, make_request_fn * mfn)
/*
* 前一个函数用于分配一个请求队列,后一个函数是将请求队列和"制造函数"进行绑定
* 但函数blk_alloc_queue实际上并不包含任何请求.
*/
/*去除请求*/
void blkdev_dequeue_request(struct request* req);
void elv_requeue_request(request_queue_t* queue, struct request* req);
/*启停请求*/
void blk_stop_queue(request_queue_t* queue);
void blk_start_queue(request_queue_t* queue);
//参数设置
void blk_queue_max_sectors(request_queue_t* q, unsigned short max);
/*请求可包含的最大扇区数.默认255*/
void blk_queue_max_phys_segments(request_queue_t* q, unsigned short max);
void blk_queue_max_hw_segments(request_queue_t* q, unsigned short max);
/*这两个函数设置一个请求可包含的最大物理段数(系统内存中不相邻的区),缺省是128*/
void blk_queue_max_segment_size(request_queue_t* q, unsigned int max);
/*告知内核请求短的最大字节数,默认2^16 = 65536*/
//通告内核
void blk_queue_bounce_limit(request_queue_t* queue, u64 dma_addr);
/*
* 此函数告知内核设备执行DMA时,可使用的最高物理地址dma_addr,常用的宏如下:
* BLK_BOUNCE_HIGH:对高端内存页使用反弹缓冲(缺省)
* BLK_BOUNCE_ISA:驱动只可以在MB的ISA区执行DMA
* BLK_BOUNCE_ANY:驱动可在任何地方执行DMA
*/
blk_queue_segment_boundary(request_queue_t* queue, unsigned long mask);
/*这个函数在设备无法处理跨越一个特殊大小内存边界的请求时,告知内核这个边界.*/
void blk_queue_dma_alignment(request_queue_t* q, int mask);
/*告知内核设备加于DMA传送的内存对齐限制*/
viod blk_queue_hardsect_size(request_queue_t* q, unsigned short max);
/*此函数告知内核块设备硬件扇区大小*/
块IO(bio)结构体:
通常一个bio对应一个IO请求,IO调度算法可以将连续的bio合并成一个请求,所以一个请求包含多个bio
struct bio {
sector_t bi_sector; /* device address in 512 byte sectors */
struct bio *bi_next; /* request queue link */
struct block_device *bi_bdev;
/*如果是一个写请求,最低有效位被置位,可使用bio_data_dir(bio)宏来获取读写方向*/
unsigned long bi_flags; /* status, command, etc */
unsigned long bi_rw; /* 低位代表:READ/WRITE,高位代表:优先级 */
unsigned short bi_vcnt; /* how many bio_vec's (bio_vec的数量)*/
unsigned short bi_idx; /* current index into bvl_vec(当前bio_vec的索引) */
/*不相邻的物理段的数目*/
unsigned short bi_phys_segments;
/*物理合并和DMA remap合并后不相邻的物理扇区*/
unsigned short bi_hw_segments;
/*被传送的数据大小(byte),用bio_sector(bio)获取扇区为单位的大小*/
unsigned int bi_size; /* residual I/O count */
/*被传送的数据大小(byte),用bio_sector(bio)获取扇区为单位的大小*/
/*为了明了最大的hw尺寸,考虑bio中第一个和最后一个虚拟的可合并的段的尺寸*/
unsigned int bi_hw_front_size;
unsigned int bi_hw_back_size;
unsigned int bi_max_vecs; /*能持有的最大bvl_vecs数*/
struct bio_vec *bi_io_vec; /*能持有的最大bvl_vecs数*/
...................
}
struct bio_vec {
struct page *bv_page; /*要操作的页指针*/
unsigned int bv_len; /*要传输的字节数*/
unsigned int bv_offset;/*偏移位置*/
};
/*一般不直接访问bio的bio_vec成员,而使用bio_for_each_segment()宏进行操作.
*该宏循环遍历整个bio中的每个段.
*/
#define __bio_for_each_segment(bvl, bio, i, start_idx)\
for(
bvl = bio_iovec_idx((bio),(start_idx)),i = (start_idx);\
i bi_vcnt;\
bvl++, i++\
)
#define bio_for_each_segment(bvl, bio, i)\
__bio_for_each_segment(bvl, bio, i, (bio)->bi_idx)
在内核中,提供了一组函数(宏)用于操作bio:
int bio_data_dir(struct bio* bio);
该函数用于获得数据传送方向.
struct page* bio_page(struct bio* bio);
该函数用于获得目前的页指针.
int bio_offset(struct bio* bio);
该函数返回操作对应的当前页的页内偏移,通常块IO操作本身就是页对齐的.
int bio_cur_sectors(struct bio* bio);
该函数返回当前bio_vec要传输的扇区数.
char* bio_data(struct bio* bio);
该函数返回数据缓冲区的内核虚拟地址.
char* bvec_kmap_irq(struct bio_vec* bvec, unsigned long* offset);
该函数也返回一个内核虚拟地址此地址可用于存取被给定的bio_vec入口指向的数据缓冲区.同时会屏蔽中断并返回一个原子kmap,因此,在此函数调用之前,驱动不应该是睡眠状态.
void bvec_kunmap_irq(char* buffer, unsigned long flags);
该函数撤销函数bvec_kmap_irq()创建的内存映射.
char* bio_kmap_irq(struct bio* bio, unsigned long* flags);
该函数是对bvec_kmap_irq函数的封装,它返回给定的比偶的当前bio_vec入口的映射.
char* __bio_kmap_atomic(struct bio* bio, int i, enum km_type type);
该函数是通过kmap_atomic()获得返回给定bio的第i个缓冲区的虚拟地址.
void __bio_kunmap_atomic(char* addr, enum km_type type);
该函数返还由函数__bio_kmap_atomic()获得的内核虚拟地址给系统.
void bio_get(struct bio* bio);
void bio_put(struct bio* bio);
7、块设备注册与取消
块设备驱动的第一个任务就是将他们自己注册到内核中,其函数原型如下:
☆int register_blkdev(unsigned int major, const char* name);
major参数是块设备要使用的主设备号,name为设备名,它会在/proc/devices中被现实.如果major为0,内核会自动分配一个新的主设备号,并由该函数返回.如果返回值为负值,则说明设备号分派失败.
注销函数是unregister_blkdev(),原型如下:
☆int unreister_blkdev(unsigned int major, const char* name);
这里unreister_blkdev与register_blkdev的参数必须匹配,否则这个函数会返回-EINVAL. 在Linux2.6中,对register_blkdev的调用是可选的.register_blkdev这个调用在Linux2.6中只完成了两件事情:
①如果需要,分派一个主设备号;
②在/proc/devices中创建一个入口.
8、块设备驱动程序编写模板
块驱动中相关相关模块模板
1.块设备驱动的模块加载与卸载
1)块设备驱动的模块加载完成的工作如下:
☆ 分配,初始化请求队列,绑定请求队列和请求函数
☆ 分配,初始化gendisk,给gendisk的major,fops,queue等成员赋值,最后添加gendisk.
☆ 注册块设备驱动.
代码1:使用blk_alloc_queue函数完成块设备驱动的模块加载模板
static int __init xxx_init(void){
//分配gendisk
xxx_disks = alloc_disk(1);
if(!xxx_disks){
goto out;
}
//块设备驱动注册
if(register_blkdev(xxx_MAJOR, "xxx"){
err = -EIO;
goto out;
}
//"请求队列"分配
xxx_queue = blk_alloc_queue(GFP_KERNEL);
if(!xxx_queue){
goto out_queue;
}
blk_queue_make_request(xxx_queue, &xxx_make_request);//绑定"制造请求"函数
blk_queue_hardsect_size(xxx_queue,xxx_blocksize);//告知内核硬件扇区尺寸
//gendisk初始化
xxx_disks->major = xxx_MAJOR;
xxx_disks->first_minor = 0;
xxx_disks->fops = &xxx_fop;
xxx_disks->queue = xxx_queue;
sprintf(xxx_disks->disk_name, "xxx%d", i);
set_capacity(xxx_disks, xxx_size);//设置gendisk容量为xxx_size个扇区大小
add_disk(xxx_disks);
return 0;
out_queue:unregister_blkdev(xxx_MAJOR, "xxx");
out:put_disk(xxx_disks);
blk_cleanup_queue(xxx_queue);
return -ENOMEM;
}
代码2:使用blk_init_queue函数完成块设备驱动的模块加载模板
static int __init xxx_init(void){
//块设备驱动注册
if(register_blkdev(xxx_MAJOR, "xxx"){
err = -EIO;
goto out;
}
//请求队列初始化
xxx_queue = blk_init_queue(xxx_request, xxx_lock);
if(!xxx_queue){
goto out_queue;
}
blk_queue_hardsect_size(xxx_queue, xxx_blocksize);//告知内核硬件扇区大小
//gendisk初始化
xxx_disks->major = xxx_MAJOR;
xxx_disks->first_minor = 0;
xxx_disks->fops = &xxx_fop;
xxx_disks->queue = xxx_queue;
sprintf(xxx_disks->disk_name, "xxx%d", i);
set_capacity(xxx_disks, xxx_size*2);//设置gendisk容量为xxx_size个扇区大小
add_disk(xxx_disks);
return 0;
out_queue:unregister_blkdev(xxx_MAJOR, "xxx");
out:put_disk(xxx_disks);
blk_cleanup_queue(xxx_queue);
return -ENOMEM;
}
2)块设备驱动的模块卸载完成的工作如下:
☆ 清除请求队列.
☆ 删除gendisk和gendisk的引用
☆ 删除对块设备的引用,注销块设备驱动.
代码3:块设备驱动模块卸载函数模板
static void __exit xxx_exit(void){
if(bdev){
invalidate_bdev(xxx_bdev, 1);
blkdev_put(xxx_bdev);
}
del_gendisk(xxx_disks);//删除gendisk
put_disk(xxx_disks);
blk_cleanup_queue(xxx_queue[i]);//清除请求队列
unregister_blkdev(xxx_MAJOR, "xxx");
}
2.块设备驱动的打开与释放
块设备驱动的open()和release()函数不是必须的,一个简单的块设备驱动可以不提供open()和release()函数.
块设备驱动的open()函数和字符设备驱动的open()和类似,都以相关inode和file结构体指针作为参数,当一个结点引用一个块设备时,inode->i_bdev->bd_disk包含一个指向关联gendisk的结构体的指针.因此类似字符设备,可将gendisk的private_data赋给file的private_data,private_data同样最好是指向描述该设备的设备结构体xxx_dev的指针.如下面的代码:
static int xxx_open(struct inode* inode, struct file* file){
struct xxx_dev* dev = inode->i_bdev->db_disk->private_data;
file->private_data = dev;
...
return 0;
}
3.块设备驱动的ioctl
块设备可以包含一个ioctl()函数,以提供对该设备的IO控制,实际上搞成的块设备层代码处理了绝大多数ioctl(),因此具体的块设备驱动中,通常不在需要实现很多ioctl()命令.下面的代码中只实现一个命令HDIO_GETGEO,用于获得磁盘的几何信息(geometry,指CHS,即Cylinder, Head, Sector/Track).
static int xxx_ioctl(struct inode* inode, struct file* file,\
unsigned int cmd, unsigned long arg){
long size;
struct hd_geometry geo;
struct xxx_dev* dev = file->private_data;
switch(cmd){
case HDIO_GETGEO:
size = dev->size * (hardsect_size / KERNEL_SECTOR_SIZE);
geo.cylinders = (size & ~0x3f) >> 6;
geo.heads = 4;
geo.sectors = 16;
if(copy_to_user((void __user*)arg, &geo, sizeof(geo)){
return -EFAULT;
}
return 0;
}
return -ENOTTY;//未知命令
}
4.块设备驱动的I/O请求
☆ 使用请求队列
块设备驱动请求函数的原型为:
void request(request_queue_t* q);
这个函数不能由驱动自己调用,只有当内核认为是时候让驱动处理对设备的读写等操作时,它才会调用这个函数.请求函数可以在没有完成请求队列中的所有请求的情况下返回,甚至它一个请求不完成都可以返回.但对大部分设备而言,一般会在请求函数中处理完所有请求后才返回.
static void xxx_request(request_queue_t* q){
struct request* req;
//elv_next_request()用于获取队列中第一个未完成的请求
//end_request()会将请求从请求队列中剥离
while((req = elv_next_request(q)) != NULL){
struct xxx_dev* dev = req->rq_disk->private_data;
if(!blk_fs_request(req)){//如果不是文件系统请求,直接清除,调用end_request().
printk(KERN_NOTICE "Skip non-fs request\n");
end_request(req, 0);//通知请求处理失败.第二个参数0代表请求失败.
continue;
}
xxx_transfer(dev, req->sector, req->current_nr_sectors, req->buffer,\
rq_data_dir(req));//处理这个请求.
end_request(req, 1);//通知成功完成这个请求.1,表示请求成功.
}
}
static void xxx_transfer(struct xxx_dev* dev, unsigned long sector,\
unsigned long nsect, char* buffer, int write){
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)
write_dev(offset, buffer, nbytes);//向设备写nbytes个字节的数据.
else
read_dev(offset, buffer, nbytes);//从设备读取nbytes个字节的数据.
}
下面是end_that_request_first()的源码和分析
//end_request()源码清单
void end_request(struct request* req, int uptodate){
//当设备完成一个IO请求的部分或全部扇区传输后,必须告知块设备层.end_that_request_first
//原型为:int end_that_request_first(struct request* req, int success, int count);
//此函数高数块设备层,已经完成count各扇区的传送.返回表示所有扇区传送完毕.
if(!end_that_request_first(req, uptodate, req->hard_cur_sectors)){
//add_disk_randomness()作用是使用块IO请求的定时来给系统的随机数池贡献熵,它不影响
//块设备,但仅当磁盘的操作时间是真正随机的时候,才调用它.
add_disk_randomness(req->rq_disk);
blkdev_dequeue_request(req);//清除此请求.
end_that_request_last(req);//通知等待此请求的对象,此请求已经完成
}
}
下面是一个更复杂的请求函数,分别遍历了request,bio,以及bio中的segment
//请求函数遍历请求,bio和段
static void xxx_full_request(request_queue_t* q){
struct request* req;
int sectors_xferred;
struct xxx_dev* dev = q->queuedata;
//XXX 遍历每个请求
while((req = elv_next_request(q)) != NULL){
if(!blk_fs_request(req)){
printk(KERN_NOTICE "Skip non-fs request\n");
end_request(req, 0);
continue;
}
sectors_xferred = xxx_xfer_reqeust(dev, req);
if(!end_that_request_first(req, 1, sectors_xferred)){
blkdev_dequeue_reqeust(req);
end_that_request_last(req);
}
}
}
//XXX 请求处理
static int xxx_xfer_request(struct xxx_dev* dev, struct reqeust* req){
struct bio* bio;
int nsect = 0;
//遍历请求中的每个bio
rq_for_each_bio(bio, req){
xxx_xfer_bio(dev, bio);
nsect += bio->bi_size / KERNEL_SECTOR_SIZE;
}
return nsect;
}
//XXX bio处理
static int xxx_xfer_bio(struct xxx_dev* dev, struct bio* bio){
int i;
struct bio_vec* bvec;
sector_t sector = bio->bi_sector;
//遍历每一个segment
bio_for_each_segment(bvec, bio, i){
char* buffer = __bio_kmap_atomic(bio, i, KM_USER0);
xxx_transfer(dev, sector, bio_cur_sectors(bio), buffer,\
bio_data_dir(bio) == WRITE);
sector += bio_cur_sectors(bio);
__bio_kunmap_atomic(bio, KMUSER0);
}
return 0;
}
☆ 不使用请求队
对于机械的磁盘设备而言,请求队列有助于提高系统性能.但对于如SD卡,RAM盘等可随机访问的块设备,请求队列无法获益.对于这些设备,块层支持"无队列"的操作模式,驱动为此必须提供一个"制造请求"函数(注意:这不是请求函数哦),"制造请求"函数的原型为:
typedef int (make_request_fn) (request_queue_t* q, struct bio* bio);
此函数的第一个参数,是一个"请求队列",但实际并不包含任何请求.所以主要参数是bio,它表示一个或多个要传送的缓冲区.此函数或直接进行传输,或将请求重定向给其他设备.在处理完成之后,应使用bio_endio()通知处理结束.bio_endio()原型如下:
void bio_endio(struct bio* bio, unsigned int byetes, int error);
bytes是已经传送的字节数(注意:bytes≤bio->bi_size),这个函数同时更新了bio的当前缓冲区指针.当设备进一步处理bio后,驱动应再次调用bio_endio(),如不能完成请求,将错误码赋给error参数,并在函数中得以处理.此函数无论处理IO成功与否都返回0,如果返回非零值,则bio将再次被提交:
static int xxx_make_request(request_queue_t* q, struct bio* bio){
struct xxx_dev* dev = q->queuedata;
int status = xxx_xfer_bio(dev, bio);//处理bio
bio_endio(bio, bio->bi_size, status);//报告结束
return 0;
}
说明:这里要指出,如果是无队列的IO请求处理,其加载模块应使用<>,否则应使用<>.