原文地址: bio 与块设备驱动
 
   系统中能够随机访问固定大小数据片(chunk)的设备被称作块设备,这些数据片就称作块。块设备文件都是以安装文件系统的方式使用,此也是块设备通常的访问方式。块设备的访问方式是随机的,也就是可以在访问设备时,随意的从一个位置跳转到另一个位置。块设备的访问位置必须能够在介质的不同区间前后移动。
所以事实上内核不必提供一个专门的子系统来管理字符设备,但是对块设备的管理却必须要有一个专门的提供服务的子系统。块设备中,最小的可寻址单元是扇区。扇区大小一般是2的整数倍,而最常见的大小是512个字节。扇区的大小是设备的物理属性,扇区是所有块设备的基本单元——块设备无法对比它还小的单元进行 寻址和操作,许多块设备能够一次就传输多个扇区。块是文件系统的一种抽象——只能基于块来访问文件系统。虽然物理磁盘寻址是按照扇区级进行的,但是内核执 行的所有磁盘操作都是按照块进行的。由于扇区是设备的最小可寻址单元,所以块不能比扇区还小,只能数倍于扇区大小。内核还要求块大小是2的整数倍,而且不 能超过一个页的长度。所以对块大小的最重要求是:必须是扇区大小的2的整数倍,而且要小于页面大小,所以通常大小是512字节,1K或者4K。

在linux2.5之前,当一个块被调入内存时,要存储在一个缓冲区中,每个缓冲区与一个块对应,相当于是磁盘块在内存中的表示,由于内核在处理数据时需要 一些相关控制信息,所以每一个缓冲区都有一个对应的描述符。该描述符用buffer_head结构体表示,也称作缓冲区头。

struct buffer_head {

    unsigned long b_state; //缓冲区状态标志

struct buffer_head *b_this_page; //页面中的缓冲区

struct page *b_page; //存储缓冲区的页面

sector_t b_blocknr; //逻辑块号

size_t b_size; //块大小

char *b_data; //页面中的缓冲区

struct block_device *b_bdev; //块设备

bh_end_io_t *b_end_io; //I/O完成方法

void *b_private; //完成方法数据

struct list_head b_assoc_buffers; //相关映射链表
    /* mapping this buffer is associated with */
    struct address_space *b_assoc_map;    

    atomic_t b_count; //缓冲区使用计数

};

b_state域表示缓冲区的状态,合法的标志存放在bh_state_bits枚举中,定义在

enum bh_state_bits {
BH_Uptodate,该缓冲区包含可用数据
BH_Dirty,该缓冲区是脏的(缓存中的内容比磁盘中的块内容新,所以缓冲区内容必须被写回磁盘)
BH_Lock,该缓冲区正被I/O操作使用,被锁定以防被并发访问
BH_Req,该缓冲区有I/O请求操作
BH_Uptodate_Lock,
BH_Mapped,该缓冲区是映射磁盘块的可用缓冲区
BH_New,该缓冲区是通过get_block(0刚刚映射的,并且不能访问
BH_Async_Read,该缓冲区正通过end_buffer_async_read()被异步I/O读操作使用
BH_Async_Write,该缓冲区正通过end_buffer_async_write()被异步I/O写操作使用
BH_Delay,该缓冲区尚未和磁盘块关联
BH_Boundary,该缓冲区处于连续块区的边界——下一个块不再连续
BH_Write_EIO,
BH_Ordered,
BH_Eopnotsupp,
BH_Unwritten,
BH_PrivateStart,
};

   驱动程序可以在这些位中安全的定义自己的状态标志,只要保证自定义的状态标志不与块I/O层的专用位发生冲突就可以了。
而在b_count中,表示缓冲区的使用计数,则通过两个函数来进行增减:
get_bh(struct buffer_head *bh)-->atomic_inc(&bh->b_count)
put_bh(struct buffer_head *bh)-->atomic_dec(&bh->b_count)

一个块设备驱动程序主要通过传输固定大小的随机数据来访问设备。高效的块设备驱动程序在性能上是严格要求的,并不仅仅体现在用户应用程序的读写操作中。现代 操作系统使用虚拟内存工作,把不需要的数据转移到诸如磁盘等其他存储介质上,块驱动程序是在核心内存与其他存储介质之间的管道,因此它们可以认为是虚拟内 存子系统的组成部分。一个数据块指定的是固定大小的数据,而大小的值由内核确定,数据块的大小通常是4096个字节,但是可以根据体系结构和所使用的文件 系统进行改变。与数据块对应的是扇区,它是由底层硬件决定大小的一个块。内核所处理的设备扇区大小是512字节。如果用户的设备使用了不同的大小,需要对 内核进行修改,以避免产生硬件所不能处理的I/O请求。无论何时内核为用户提供了一个扇区编号,该扇区的大小就是512字节。如果要使用不同的硬件扇区大小,用户比对内核的扇区做相应的修改。同样,此部分也是由不少数据结构与相应方法组成,下面先来看相关数据结构:

内核使用gendisk结构来表示一个独立的磁盘设备。内核还使用gendisk结构表示分区,在此结构中,很多成员必须由驱动程序来进行初始化。此结构定义在


struct gendisk {
int major; //主设备号

int first_minor; //第一个从设备号

int minors;
/* 描述被磁盘使用的设备号的成员.一个驱动器必须使用最少一个次编号.如果你的驱动会是可分区的,但是(并且大部分应当是),你要分配一个次编号给每个可能 的分区.次编号的一个普通的值是 16, 它允许"全磁盘"设备盒 15 个分区. 一些磁盘驱动使用 64 个次编号给每个设备.*/

char disk_name[32]; //应当被设置为磁盘驱动器名子的成员. 它出现在 /proc/partitions 和 sysfs.

struct hd_struct **part; /* [indexed by minor] */
struct block_device_operations *fops;// 设备操作集合.

struct request_queue *queue;//被内核用来管理这个设备的 I/O 请求的结构;

void *private_data;//块驱动可使用这个成员作为一个指向它们自己内部数据的指针.

sector_t capacity;
//这个驱动器的容量,以512-字节扇区来计.sector_t类型可以是64位宽.驱动不应当直接设置这个成员;相反,传递扇区数目给set_capacity.

int flags;
// 一套标志(很少使用),描述驱动器的状态.如果你的设备有可移出的介质,你应当设置GENHD_FL_REMOVABLE.CD-ROM驱动器可设置 GENHD_FL_CD. 如果, 由于某些原因, 你不需要分区信息出现在 /proc/partitions, 设置 GENHD_FL_SUPPRESS_PARTITIONS_INFO.

struct device *driverfs_dev; // FIXME: remove

struct device dev;
struct kobject *holder_dir;
struct kobject *slave_dir;
struct timer_rand_state *random;
int policy;
atomic_t sync_io; /* RAID */
unsigned long stamp;
int in_flight;
#ifdef CONFIG_SMP
struct disk_stats *dkstats;
#else
struct disk_stats dkstats;
#endif
struct work_struct async_notify;
};

此结构是一个动态分配的结构。需要一些内核的特殊处理来进行初始化;驱动程序不能自己动态分配该结构,而是必须调用。
struct gendisk *alloc_disk(int minors);//参数是次设备号的数目。此后就无法改变minors成员。动态分配该结构。 
void del_gendisk(struct gendisk *gd);//卸载磁盘。参数是一个引用计数结构,包含kobject对象。
void add_disk(struct gendisk *gd); //初始化结构函数,一旦调用此函数,设备将被激活,并随时会调用它提供的方法。在驱动程序完全被初始化并且能够相应对磁盘的请求前,不要调用此函数。

当内核以文件系统、虚拟内存子系统或者系统调用的形式决定从块I/O设备输入、输出块数据时,它将再结合一个bio结构,用来描述这个操作。该结构被传递给 I/O代码,代码会把它合并到一个已经存在的request结构中,或者根据需要,再创建一个新的request结构。bio结构包含了驱动程序执行请求 的全部信息,而不必与初始化这个请求的用户空间的进程相关联。

内核中块I/O操作的基本容器由bio结构体表示,定义在中,该结构体代表了正在现场的(活动的)以片段(segment)链表形式组织的块I/O操作。一个片段是一小 块连续的内存缓冲区。这样的好处就是不需要保证单个缓冲区一定要连续。所以通过片段来描述缓冲区,即使一个缓冲区分散在内存的多个位置上,bio结构体也 能对内核保证I/O操作的执行,这样的就叫做聚散I/O.
bio为通用层的主要数据结构,既描述了磁盘的位置,又描述了内存的位置,是上层内核vfs与下层驱动的连接纽带。

struct bio {

//该bio结构所要传输的第一个(512字节)扇区:磁盘的位置
sector_t bi_sector;

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; //销毁方法

};

此结构体的目的主要就是代表正在现场执行的I/O操作,所以该结构体中的主要域都是用来相关的信息的,而其中bi_io_vec、bi_vcnt、bi_idx重要
这三者形成了这样一种关系:bio-->bi_io_vec,bi_idx(就如基地址加偏移量一般,可以轻易的找到具体的bio_vec)-->page(再通过vec找到page)
其 中bi_io_vec指向一个bio_vec结构体数组,该结构体链表包含了一个特定的I/O操作所需要使用到的所有片段。每个bio_vec都是<page,offset,len>的向量,描述的是一个特定的片段:片段所在的物理页,块在物理页中的偏移位置,从给定偏移量开始的块长度,整个bio_io_vec结构体数组表示了一个完整的缓冲区。

struct bio_vec {
struct page    *bv_page;指向整个缓冲区所驻留的物理页面
unsigned int    bv_len;这个缓冲区以字节为单位的大小
unsigned int    bv_offset;缓冲区所驻留的页中以字节为单位的偏移量。
};

bi_vcnt域用来描述bi_io_vec所指向的bio_vec数组中的向量数目。当I/O操作完成后,bi_idx指向数组的当前索引。一个块请求通过一个bio表示。每个请求包括多个或者一个块,而这些块有都存储在bio_vec结构体的数组中,这些结构描述了每个片段在物理页中的实际位置,并且如向量一样的组织在一起,I/O操作的第一个片段由b_io_vec结构体所指向,其他片段则在其后依次放置,共有bi_vcnt个片段,当I/O层开始执行请求,需要各个使用片段时,bi_idx会不断更新,从而总指向当前的片段。看,这就是在入门C语言中用到的最朴实的概念,数组寻址的概念相类似。

块设备将挂起的块请求保存在请求队列中,该队列由request_queue结构体表示,定义在文件中,包含一个双向请求队列以及相关控制信息。通过内核中像文件系统这样高层的代码将请求加入到队列中,请求队列只要不为空,队列对应的块设备驱动程序就会从队列头 获取请求,然后将其加入到对应的块设备中去,请求队列表中的每一项都是一个单独的请求,由request结构体表示。

而队列中的请求request,定义在中,一个请求可能要操作多个连续的磁盘块,所以每个请求可以由多个bio结构体组成。每个bio结构体都可以描述多个片段。下面就是request中比较常用的几个域。

struct request {
struct list_head queuelist;//连接这个请求到请求队列. 
//追踪请求硬件完成的扇区的成员.第一个尚未被传送的扇区被存储到 hard_sector,已经传送的扇区总数在hard_nr_sectors,并且在当前bio中剩余的扇区数是hard_cur_sectors.这些成员打算只用在块子系统;驱动不应当使用它们.
struct request_queue *q;
sector_t hard_sector;    
unsigned long hard_nr_sectors;    
unsigned int hard_cur_sectors;
struct bio *bio;//bio 是给这个请求的 bio 结构的链表. 你不应当直接存取这个成员; 使用 rq_for_each_bio(后面描述) 代替.
unsigned short nr_phys_segments;//被这个请求在物理内存中占用的独特段的数目, 在邻近页已被合并后
char *buffer;//随着深入理解,可见到这个成员仅仅是在当前 bio 上调用 bio_data 的结果.
};

而几个关键结构之间的关系是如何的呢?request_queue中是请求队列,通过它找到request,将这些请求连成一体,然后在request中包含bio,然后通过bio结构体找到对应的page,然后通过page读取物理内存中的信息。大体就是这样一个关系。

块驱动程序步骤与实例:

对于大多数块驱动程序来说,首先都该是向内核注册自己!这个任务的函数是register_blkdev(在中定义):
int register_blkdev(unsigned int major, const char *name); 
参数是设备要使用的主编号和关联的名子(内核将显示它在/proc/devices). 如果major传递为0,内核分配一个新的主编号并且返回它给调用者.
取消注册的对应函数是:int unregister_blkdev(unsigned int major, const char *name);参数必须匹配传递给 register_blkdev 的那些。
在2.6内核,register_blkdev所进行的功能已随时间正在减少;这个调用唯一的任务是如果需要,分配一个动态主编号,并且在/proc/devices创建一个入口.

描述虚拟设备的结构体,里面的结构体除去timer_list都在前面介绍:
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;//申请一个设备
memset (dev, 0, sizeof (struct sbull_dev));//申请内存空间
dev->size = nsectors*hardsect_size;//设备大小:1024*512
dev->data = vmalloc(dev->size);

switch (request_mode) {
case RM_NOQUEUE:
dev->queue = blk_alloc_queue(GFP_KERNEL);
blk_queue_make_request(dev->queue, sbull_make_request);
break;
case RM_FULL:
dev->queue = blk_init_queue(sbull_full_request, &dev->lock);
break;
default:
printk(KERN_NOTICE "Bad request mode %d, using simple\n", request_mode);
case RM_SIMPLE:
dev->queue = blk_init_queue(sbull_request, &dev->lock);
if (dev->queue == NULL)
goto out_vfree;
break;
}
使用bio结构编写的块设备驱动程序。
static void sbull_full_request(request_queue_t *q)
{
struct request *req;
int sectors_xferred;
struct sbull_dev *dev = q->queuedata;
while ((req = elv_next_request(q)) != NULL) {//获得队列中的下一个request
if (! blk_fs_request(req)) {
printk (KERN_NOTICE "Skip non-fs request\n");
end_request(req, 0);//配合elv_next_request使用,完成一个请求
continue;
}
sectors_xferred = sbull_xfer_request(dev, req);//返回数量
if (! end_that_request_first(req, 1, sectors_xferred)) {//驱动程序从前一次结束的地方开始,完成了规定数目的扇区的传输
blkdev_dequeue_request(req);//从队列中删除一个请求函数,当end_that_request_first都被传输后,则必须调用此函数
end_that_request_last(req);//通知任何等待已经完成请求的对象,并重复利用该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(dev, bio);
nsect += bio->bi_size/KERNEL_SECTOR_SIZE;//#define KERNEL_SECTOR_SIZE    512
}
return nsect;
}
static int sbull_xfer_bio(struct sbull_dev *dev, struct bio *bio)
{
int i;
struct bio_vec *bvec;
sector_t sector = bio->bi_sector;
bio_for_each_segment(bvec, bio, i) //用来遍历组成bio结构的段的伪控制结构
{
char *buffer = __bio_kmap_atomic(bio, i, KM_USER0);//底层函数直接映射了指定索引号为i的bio_vec中的缓冲区。
sbull_transfer(dev, sector, bio_cur_sectors(bio),buffer, bio_data_dir(bio) == WRITE);//完全简单的基于ram设备。完成实际传输。
//bio_cur_sectors用来访问bio结构中的当前段,bio_data_dir用来获得bio结构描述的大小和传输方向
sector += bio_cur_sectors(bio);
__bio_kunmap_atomic(bio, KM_USER0);
}
return 0; 
}
static void sbull_transfer(struct sbull_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 (write)
memcpy(dev->data + offset, buffer, nbytes);
else
memcpy(buffer, dev->data + offset, nbytes);
}
register_blkdev可用来获得一个主编号,但不使任何磁盘驱动器对系统可用.有一个分开的注册接口你必须使用来管理单独的驱动器.它是 struct 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系统调用的方法.大部分的块驱动 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,void **, unsigned long *);
int (*media_changed) (struct gendisk *);
//被内核调用来检查是否用户已经改变了驱动器中的介质的方法,如果是这样返回一个非零值.显然,这个方法仅适用于支持可移出的介质的驱动器(并且最好给驱动一个"介质被改变"标志); 在其他情况下可被忽略.
int (*revalidate_disk) (struct gendisk *);
//revalidate_disk方法被调用来响应一个介质改变;它给驱动一个机会来进行需要的任何工作使新介质准备好使用.这个函数返回一个int值,但是值被内核忽略.
int (*getgeo)(struct block_device *, struct hd_geometry *);
struct module *owner;//一个指向拥有这个结构的模块的指针; 它应当常常被初始化为 THIS_MODULE.
};
继续初始化:
dev->gd = alloc_disk(SBULL_MINORS);//动态分配gendisk结构(表是一个独立的磁盘设备)
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');
set_capacity(dev->gd, nsectors*(hardsect_size/KERNEL_SECTOR_SIZE));
//使用KERNEL_来进行内核512字节扇区到实际使用扇区大小的转换。
add_disk(dev->gd);//结束设置过程。
其余部分参见ldd3的sbull

【转】 bio 与块设备驱动的更多相关文章

  1. Linux 块设备驱动 (一)

    1.块设备的I/O操作特点 字符设备与块设备的区别: 块设备只能以块为单位接受输入和返回输出,而字符设备则以字符为单位. 块设备对于I/O请求有对应的缓冲区,因此它们可以选择以什么顺序进行响应,字符设 ...

  2. linux下的块设备驱动(二)

    上一章主要讲了请求队列的一系列问题.下面主要说一下请求函数.首先来说一下硬盘类块设备的请求函数. 请求函数可以在没有完成请求队列的中的所有请求的情况下就返回,也可以在一个请求都不完成的情况下就返回. ...

  3. Linux块设备驱动(一) _驱动模型

    块设备是Linux三大设备之一,其驱动模型主要针对磁盘,Flash等存储类设备,本文以3.14为蓝本,探讨内核中的块设备驱动模型 框架 下图是Linux中的块设备模型示意图,应用层程序有两种方式访问一 ...

  4. 乾坤合一~Linux设备驱动之块设备驱动

    1. 题外话 在蜕变成蝶的一系列学习当中,我们已经掌握了大部分Linux驱动的知识,在乾坤合一的分享当中,以综合实例为主要讲解,在一个月的蜕茧成蝶的学习探索当中,觉得数据结构,指针,链表等等占据了代码 ...

  5. linux块设备驱动

    块设备驱动程序<1>.块设备和字符设备的区别1.读取数据的单元不同,块设备读写数据的基本单元是块,字符设备的基本单元是字节.2.块设备可以随机访问,字符设备只能顺序访问. 块设备的访问:当 ...

  6. Linux块设备驱动详解

    <机械硬盘> a:磁盘结构 -----传统的机械硬盘一般为3.5英寸硬盘,并由多个圆形蝶片组成,每个蝶片拥有独立的机械臂和磁头,每个堞片的圆形平面被划分了不同的同心圆,每一个同心圆称为一个 ...

  7. Linux块设备驱动_WDS

    推荐书:<Linux内核源代码情景分析> 1.字符设备驱动和使用中等待某一事件的方法①查询方式②休眠唤醒,但是这种没有超时时间③poll机制,在休眠唤醒基础上加一个超时时间④异步通知,异步 ...

  8. linux 块设备驱动 (三)块设备驱动开发

    一: 块设备驱动注册与注销 块设备驱动中的第1个工作通常是注册它们自己到内核,完成这个任务的函数是 register_blkdev(),其原型为:int register_blkdev(unsigne ...

  9. linux块设备驱动(一)——块设备概念介绍

    本文来源于: 1. http://blog.csdn.net/jianchi88/article/details/7212370 2. http://blog.chinaunix.net/uid-27 ...

随机推荐

  1. 201521123016《Java程序设计》第10周学习总结

    1. 本周学习总结 2. 书面作业 本次PTA作业题集异常.多线程 1.finally 题目4-2 1.1 截图你的提交结果(出现学号) 1.2 4-2中finally中捕获异常需要注意什么? 只有执 ...

  2. MarkDown模板

    一个例子: 例子开始 1. 本章学习总结 今天主要学习了三个知识点 封装 继承 多态 2. 书面作业 Q1. java HelloWorld命令中,HelloWorld这个参数是什么含义? 今天学了一 ...

  3. java数据类型与二进制

    在java中 Int 类型的变量占 4个字节 Long 类型的变量占8个字节 一个程序就是一个世界,变量是这个程序的基本单位. Java基本数据类型 1.        整数类型 2.        ...

  4. 用户登陆注册【JDBC版】

    前言 在讲解Web开发模式的时候,曾经写过XML版的用户登陆注册案例!现在在原有的项目上,使用数据库版来完成用户的登陆注册!如果不了解的朋友,可以看看我Web开发模式的博文! 本来使用的是XML文件作 ...

  5. python实现算24的算法

    1.介绍 给定4个整数,数字范围在1-13之间,任意使用 + - * / ( ) ,构造出一个表达式,使得最终结果为24,这就是常见的算24的游戏.本文介绍用Python语言实现的两种方式.2.实现思 ...

  6. Java编程 “提高性能” 应尽力做到

    除了新增机器内存外,还应该好好review一下我们的代码,有很多代码编写过于随意化,这些不好的习惯或对程序语言的不了解是应该好好打压打压了. 下面是参考网络资源总结的一些在Java编程中尽可能要做到的 ...

  7. oracle pl/sql 函数

    函数用于返回特定的数据,当建立函数时,在函数头部必须包含return子句.而在函数体内必须包含return语句返回的数据.我们可以使用create function来建立函数. 1).接下来通过一个案 ...

  8. JAVA多线程---ThreadLocal<E>

    p.p1 { margin: 0.0px 0.0px 0.0px 0.0px; font: 13.0px ".SF NS Text" } tips: 1 当前ThreadLocal ...

  9. 02_Java运行环境搭建

    1.Java运行环境搭建,对于初学者来说,主要下载安装jdk即可,windows操作系统再配合记事本,即可进行java程序开发.后续的学习以及工作中需要使用IDE工具进行开发,常用IDE工具是ecli ...

  10. oracle 行转列 列转行

    行转列 这是一个Oracle的列转行函数:LISTAGG() 先看示例代码: with temp as( select 'China' nation ,'Guangzhou' city from du ...