块设备的处理
1.VFS虚拟文件系统位于块设备处理体系结构的上层,它提供一个通用的文件模型
2.VFS可以从磁盘中读取数据,也可以在磁盘高速缓存中读取数据,因为内核将大多数最近从块设备读出或写入其中的数据保存在RAM中。
3.内核通过映射层找到所请求数据在磁盘中的具体位置。
4.内核利用通用块层启动I/O操作来传送所请求的数据。每个I/O操作是由一个“块I/O”结构描述(简称“bio”)。
5.IO调用层使用调度程序把物理介质上相邻的数据请求聚集在一起。
6.块设备驱动向磁盘控制器的硬件接口发送适当的命令,从而进行实际的数据传送。
扇区
磁盘上的数据传送以扇区为基本单位,在大多设备中,扇区的大小是512字节,大部分磁盘设备可以同时传送几个相邻的扇区,扇区的下标存放在类型为sector_t的32位或64位的变量中。扇区是硬件设备传送数据的基本单位。
块
块是VFS和文件系统传送数据的基本单位,内核访问一个文件的内容时,必须首先从磁盘上读文件的磁盘索引节点所在的块,块对应磁盘上的一个或多个相邻的扇区,而VFS将其看成一个单一的数据单元。块的大小必须是2的幂,而且不能超过系统所设定的最大的页框,必须是扇区大小的整数倍。
每个块都需要自己的缓冲区,它是内核用来存放内容的RAM内存区,当内核从磁盘读出一个块时,就用从硬件设备中所获得的值来填充相应的块缓冲区。缓冲区首部是一个与每个缓冲区相关的buffer_head类型的描述符。(缓冲的东西后面会写一篇来介绍)
通用块层
通用块层是一个内核组件,它处理来自系统中的所有块设备发出的请求。
通用块层的核心数据结构(bio)
struct bio {
sector_t sector //块IO操作的第一个磁盘扇区
struct bio * bi_next //链接到请求队列中的下一个bio
struct block_device *bi_bdev //指向块设备描述符的指针
unsigned long bi_flags //bio的状态标志
unsigned long bi_rw //I/O状态标志
unsigned short bi_vcnt //bio的bio_vec数组中段的数目
unsigned short bi_idx //bio的bio_vec数组中段的当前索引值
unsigned short bi_phys_segments //合并之后bio中物理段的数目
unsigned short bi_hw_segments //合并之后硬件段的数目
unsigned int bi_size //需要传送的字节数
struct bio_vec * bi_io_vec //指向bio的bio_vec数组中段的指针
bio_endio_t *bi_end_io //bio的io操作结束时调用的方法
atomic_t bi_cnt //bio的引用计数器
void *bi_private //通用块层和块设备驱动程序的io完成方法使用的指针
bio_destructor_t *bi_destructor //释放bio时调用的析构方法
}
每个段都用有个bio_vec数据结构来描述
strcut bio_vec{
struct page * bv_page //指向段的页框中页描述符的指针
unsigned int bv_len //段的字节长度
unsigned int bv_offset //页框中段数据的偏移量
}
当通用块层启动一次新的IO操作时,调用bio_alloc()函数分配一个新的bio结构,通常,bio结构是由slab分配器分配的,内存不足时,内核也会使用一个小的备用的bio小内存池。
struct bio *bio_alloc(int gfp_mask, int nr_iovecs)
{
struct bio *bio = mempool_alloc(bio_pool, gfp_mask);
if (likely(bio)) {
struct bio_vec *bvl = NULL;
bio_init(bio);
if (likely(nr_iovecs)) {
unsigned long idx;
bvl = bvec_alloc(gfp_mask, nr_iovecs, &idx);
if (unlikely(!bvl)) {
mempool_free(bio, bio_pool);
bio = NULL;
goto out;
}
bio->bi_flags |= idx << BIO_POOL_OFFSET;
bio->bi_max_vecs = bvec_array[idx].nr_vecs;
}
bio->bi_io_vec = bvl;
bio->bi_destructor = bio_destructor;
}
out:
return bio;
}
磁盘和磁盘分区的表示
磁盘是一个由通用块层处理的逻辑块设备,通常一个磁盘对应一个硬件块设备。
struct gendisk{
int major //Major磁盘主设备号
int first_minor //与磁盘关联的第一个次设备号
int minors //与磁盘关联的次设备号范围
char [32] disk_name //磁盘的标准名称
struct hd_struct ** part //磁盘的分区描述符数组
struct blokc_device_operations fops //指向块设备操作表的指针
struct request_queue *queue //指向磁盘请求队列的指针
void * private_data //块设备驱动程序的私有数据
sector_t capacity //磁盘内存去的大小(扇区数目)
int flags //描述磁盘类型的标志
struct device * driverfs_dev //指向磁盘的硬件设备的device对象的指针
struct kobject kobj //内嵌的kobject
int policy //如果磁盘是只读的,则置为1,否则为0
atomic_t sync_io //写入磁盘的扇区数计数器
int in_flight //正在进行的IO操作数
strcut disk_stats * dkstats //统计每个cpu使用磁盘的情况
}
strcut block_device_operations{
void (*open) //打开块设备文件
void (*release) //关闭对块设备文件的最后一个引用
void (*ioctl) //在块设备文件上发出ioctl()系统调用(使用大内核锁)
void (*compat_ioctl) //在块设备文件发出ioctl()系统调用(不使用大内核锁)
void (*media_changed) //检查可移动介质是否已经变化
void (*revalidate_disk) //检查块设备是否持有有效数据
}
//磁盘分区描述符
struct hd_struct{
sector_t start_sect //磁盘中分区的起始扇区
sector_t nr_sects //分区的长度
struct kobject kobj //内嵌的kobject
unisgned int reads //对分区发出的读操作次数
unsigned int read_sectors //从分区读取的扇区数
unsigned int writes //对分区发出的写操作次数
unsigned int write_sectos //写进分区的扇区数
int policy //如果分区是只读的,则置为1,否则为0
int partno //磁盘中分区的相对索引
}
当内核发现一个新的磁盘时,就调用alloc_disk函数,该函数分配并初始化一个新的gendisk对象,如果新磁盘被分成了几个分区,那么alloc_disk还会分配并初始化一个适当的hd_struct类型的数组,然后调用add_disk函数将新的gendisk对象插入到通用块层的数据结构中。
/* minors 此设备号 */
struct gendisk *alloc_disk(int minors)
{
struct gendisk *disk = kmalloc(sizeof(struct gendisk), GFP_KERNEL);
if (disk) {
memset(disk, 0, sizeof(struct gendisk));
if (!init_disk_stats(disk)) {
kfree(disk);
return NULL;
}
if (minors > 1) {
int size = (minors - 1) * sizeof(struct hd_struct *);
disk->part = kmalloc(size, GFP_KERNEL);
if (!disk->part) {
kfree(disk);
return NULL;
}
memset(disk->part, 0, size);
}
disk->minors = minors;
kobj_set_kset_s(disk,block_subsys);
kobject_init(&disk->kobj);
rand_initialize_disk(disk);
}
return disk;
}
提交请求
第一步指向bio_alloc()函数分配一个新的bio描述符并设置一些字段值来初始化bio描述符。
bi_sector //设置为数据的起始扇区号,如果块设备分成了几个分区,那么扇区号是相对于分区的起始位置的
bi_size //涵盖整个数据的扇区数目
bi_bdev //块设备描述符的地址
bi_io_vec //将bi_io_vec设置为bio_vec结构数组的起始地址,数组中的每个元素描述了IO操作中的一个段(内存缓存),将bi_vcnt设为bio中总的段数
bi_rw //被请求操作的标志,为数据传送的方向
bi_end_io //当bio上的IO操作完成时所执行的完成程序的地址
bio描述符初始化后,内核调用generic_make_request()函数,它是通用块层的主要入口点,在介绍这个函数之前,先介绍一下请求队列
请求队列
当内核组件要读或写一些磁盘数据时,实际上创建一个块设备请求,从本质上说,请求描述的是所请求的扇区以及要对它执行的操作类型,然而,并不是请求一发出,内核就满足它,当请求传送一个新的数据块时,内核检查能否通过稍微扩展前一个一直处于等待状态的请求而满足新请求。判断是否可以合并提高效率。
通用块层的io处理都是异步进行的,每个块设备驱动都维持着自己的请求队列,它包含设备待处理的请求链表。
//请求队列描述符(列出了一些常用的参数)
//实际上,请求队列就是一个双向链表,其元素就是请求描述符
struct request_queue{
struct list_head queue_head //待处理请求的链表
struct request * last_merge //指向队列中首先可能合并的请求描述符
elevator_t * elevator //与IO调度有关
struct request_list rq //为分配请求描述符所使用的数据结构
request_fn_proc * request_fn //实现驱动程序的策略例程入口点的方法
merge_request_fn * back_merge_fn //检查是否能够将bio合并到请求队列最后
merge_request_fn * front_merge_fn //检查是否能够将bio合并到请求队列第一个
merge_requset_fn * merge_request_fn //合并请求队列中两个相邻请求的方法
make_request_fn * make_request_fn //将一个新请求插入请求队列时调用的方法
unplug_fn * unplug_fn //去掉块设备的方法
activity_fn * activity_fn //将某个请求加入请求队列时调用的方法
void * queuedata //指向块设备驱动程序的私有数据的指针
struct kobject kobj //请求队列的内嵌kobject结构
}
//请求描述符
struct request{
struct list_head queuelist //请求队列链表的指针
unsigned long flags //请求标志
sector_t sector //要传送的下一个扇区号
unsigneed long nr_sector //整个请求中要传送的扇区数
//每个请求包含一个或多个bio结构。组成一个链表,而且根据io是否完成时一直变化的
struct bio *bio //请求中第一个没有完成传送操作的bio
strcut bio *biotail //请求链表末尾的bio
strcut gendisk * rq_disk //请求所引用的磁盘描述符
int error //用于记录当前传送中发生的io失败次数的技术器
undigned long start_time //请求的起始时间
unsigned short nr_phys_segments //请求的物理段数
unsigned short nr_he_segments //请求的硬段数
char * buffer //指向当前数据传送的内存缓冲区的指针
request_queue_t * q //指向包含请求的请求队列描述符的指针
struct request_list *r1 //指向request_list结构的指针
struct completion * waiting //等待数据传送终止的Completion结构
void *data //设备驱动程序为了跟踪所传送的数据而使用的指针
}
请求描述符中的几个字段值可能是动态变化的,例如,一旦bio中引用的数据块全部传送完毕,bio字段立即更新从而指向请求链表中的下一个bio,在此期间,新的bio可能被加入到请求链表的尾部,所以biotail的值也可能改变。
对请求描述符的分配进行管理
固定数目的动态内存将成为进程想要把新请求加入请求队列q的瓶颈。为了解决这种问题,每个request_queue描述符包含一个request_list数据结构。
blk_get_request()函数试图从一个特定请求队列的内存池中获取一个空闲的请求描述符,如果内存区不足并且内存池已经用完,要么挂起当前进程,要么返回NULL,如果分配成功,则将请求队列的request_list数据结构的地址存放在请求描述符的r1字段中.blk_put_request函数则释放一个请求描述符,如果该描述符的引用计数器的值为0,则将描述符归还回它原来所在的内存池。
提交请求
/* bio 块io描述符 */
//这个函数主要负责通过判断bio中的sector和bd_bdev,找到具体设备后,转发bio,加入到指定设备的请求队列中。
void generic_make_request(struct bio *bio)
{
request_queue_t *q;
sector_t maxsector;
int ret, nr_sectors = bio_sectors(bio);
might_sleep();
/* Test device or partition size, when known. */
maxsector = bio->bi_bdev->bd_inode->i_size >> 9;
if (maxsector) {
sector_t sector = bio->bi_sector;
if (maxsector < nr_sectors ||
maxsector - nr_sectors < sector) {
char b[BDEVNAME_SIZE];
/* This may well happen - the kernel calls
* bread() without checking the size of the
* device, e.g., when mounting a device. */
printk(KERN_INFO
"attempt to access beyond end of device\n");
printk(KERN_INFO "%s: rw=%ld, want=%Lu, limit=%Lu\n",
bdevname(bio->bi_bdev, b),
bio->bi_rw,
(unsigned long long) sector + nr_sectors,
(long long) maxsector);
set_bit(BIO_EOF, &bio->bi_flags);
goto end_io;
}
}
/*
* Resolve the mapping until finished. (drivers are
* still free to implement/resolve their own stacking
* by explicitly returning 0)
*
* NOTE: we don't repeat the blk_size check for each new device.
* Stacking drivers are expected to know what they are doing.
*/
do {
char b[BDEVNAME_SIZE];
q = bdev_get_queue(bio->bi_bdev);
if (!q) {
printk(KERN_ERR
"generic_make_request: Trying to access "
"nonexistent block-device %s (%Lu)\n",
bdevname(bio->bi_bdev, b),
(long long) bio->bi_sector);
end_io:
bio_endio(bio, bio->bi_size, -EIO);
break;
}
if (unlikely(bio_sectors(bio) > q->max_hw_sectors)) {
printk("bio too big device %s (%u > %u)\n",
bdevname(bio->bi_bdev, b),
bio_sectors(bio),
q->max_hw_sectors);
goto end_io;
}
if (test_bit(QUEUE_FLAG_DEAD, &q->queue_flags))
goto end_io;
/*
* If this device has partitions, remap block n
* of partition p to block n+start(p) of the disk.
*/
blk_partition_remap(bio);
ret = q->make_request_fn(q, bio);
} while (ret);
}
generic_make_request详解http://blog.51cto.com/alanwu/1294034