Coder
分类: LINUX
2010-08-14 23:35:57
块设备的主要特点是,CPU和总线读写数据所化时间与磁盘所花时间与磁盘硬件的速度不匹配。块设备的平均访问时间很高。每个操作都需要几个毫秒才能完成,主要是因为磁盘控制器必须在磁盘表面将磁头移动到记录数据的确切位置。但是,当磁头到达正确的位置时,数据传送就可以稳定在每秒几十MB的速率。
块设备的处理
Linux块设备处理程序的组织是相当复杂的,块设备驱动程序上的每个操作都涉及很多内核组件。其中的一些如下图所示:
例如,我们假设一个进程在某个磁盘文件上发出一个read()系统调用——write()系统调用本质上采用同样的方式。下面是内核对进程请求给予回应的一般步骤:
1、read()系统调用的服务例程调用一个适当的函数,将文件描述符和文件内的偏移量传递给它。虚拟文件系统位于块设备处理体系结构的上层,他提供一个通用的文件模型,Linux支持的所有文件系统均采用该模型。
2、VFS函数确定所请求的数据是否存在,比如文件指针的位置的合法性等,如果有必要的话,他决定如何执行read操作(但均通过具体的文件系统提供的文件操作来执行)。有时候没有必要访问磁盘上的数据,因为内核将大多数最近从块设备读出或写入其中的数据保存在RAM总,及通过磁盘高速缓存来获得数据。
3、我们假设内核从块设备读数据,那么它就必须确定数据的物理位置。为了做到这点,内核依赖映射层(mapping layer),主要执行下面两步:
a.内核确定该文件所在文件系统的块大小,并根据文件块的大小计算所请求数据的长度。本质上,文件被看作许多数据块的集合,因此内核确定请求数据所在的块号(文件开始位置的块索引)。
b.接下来,映射层调用一个具体文件系统的函数,他访问文件的磁盘索引节点,然后根据逻辑块号确定所请求数据在磁盘上的位置。事实上,磁盘也被看作数据块的数组,因此内核必须确定存放所请求数据的块对应的号(磁盘或分区开始位置的相对索引)。由于一个文件可能存储在磁盘上的不连续块中,因此存放在磁盘索引节点中的数据结构将每个文件块号映射为一个逻辑块号。
4、现在内核可以对块设备发出读请求。内核利用通用块层(generic block layer)启动I/O操作来传送所请求的数据。一般而言,每个I/O操作只针对磁盘上一组连续的块。由于请求的数据不必位于相邻的块中,所以通用块层可能启动几次I/O操作。每次I/O操作是由一个“块I/O”(简称“bio”)结构描述,它收集底层组件需要的所有信息以满足所发出的请求。
通用块层为所有的块设备提供了一个抽象视图,因而隐藏了硬件块设备间的差异性。几乎所有的块设备都是磁盘。
5、通用块层下面的“I/O调度程序”根据预先定义的内核策略将待处理的I/O数据传送请求归类。调度程序的作用是把物理介质上相邻的数据请求聚集在一起。
6、最后块设备驱动程序向磁盘控制器的硬件接口发送适当的命令,从而进行实际的数据传送。
块设备中的数据存储涉及到了许多内核组件,每个组件采用不同长度的块来管理磁盘数据:
硬件块设备控制器采用称为“扇区”的固定长度的快来传送数据。因此,I/O调度程序和块设备驱动程序必须管理数据扇区。为了达到可接受的性能,硬盘和类似的设备快速传送几个相邻字节的数据。块设备的每次数据传送操作都作用于一组称为扇区的相邻字节。尽管磁盘的物理构造很复杂,但是硬盘控制器接收到的命令将磁盘看成一大组扇区。应该把扇区作为数据传送的基本单元;不允许传送少于一个扇区的数据,尽管大部分磁盘设备都可以同时传送几个相邻的扇区。在Linux中,扇区大小按惯例都设为512字节;如果一个块设备使用更大的扇区,那么相应的底层设备驱动程序将做些必要的变换。对存放在块设备中的一组数据是通过他们在磁盘上的位置来标识,即其首个512字节扇区的下标以及扇区的数目。扇区的下标存放在类型为sector_t的32位或64位的变量中。
虚拟文件系统、映射层和具体文件系统将磁盘数据存放在成为“块”的逻辑单元中。一个块对应文件系统中一个最小的磁盘存储单元。在Linux中,块的大小是2的次方倍,而且不能超过一个页框。此外,它必须是扇区大小的整数倍,因为每个块必须是包含整数个扇区。块设备的块大小不是唯一的,创建一个磁盘文件系统时,管理员可以选择合适的块大小。因此,同一个磁盘上的几个分区可能使用不同的块大小。此外,对块设备文件的每次读或写操作是一种“原始”访问,因为他绕过了磁盘文件系统;内核通过使用最大的块(4096字节)执行该操作。
每个块都需要自己的块缓冲区,它是内核用来存放块内容的RAM空间。当内核从磁盘读出一个块时,就用从硬件设备中所获得的数据来填充相应的缓冲区;同样,当内核向磁盘中写入一个块时,就用相关块缓冲区的数据来更新硬件设备上相应额一组相邻字节。块缓冲区的大小通常要与相应块的大小相匹配。
缓冲区首部是一个与每个缓冲区相关的buffer_head类型的描述符。它包含内核处理缓冲区需要了解的所有信息;因此,在对每个缓冲区进行操作之前,内核都要首先检查其缓冲区首部;因此,在对每个缓冲区进行操作之前,内核都要首先检查其缓冲区首部。
块设备驱动程序应该能够处理数据的“段”:一个段就是一个内存页或内存页的一部分,它们包含磁盘上物理相邻的数据块。
磁盘高速缓存作用于磁盘数据的“页”上,每页正好装在一个页框中。
通用块层将所有的上层和下层的组件组合在一起,因此它了解数据的扇区、块、段以及页。即使有许多不同的数据块,它们通常也是共享相同的物理RAM单元。例如,下图显示了一个具有4KB字节的页的构造。上层内核组件将页看成是4个1024字节组成的块缓冲区。块设备驱动程序正在传送页中的后三个块,因此这3块被插入到涵盖了后3072字节的段中。硬盘控制器将该段看成是由6个512字节的扇区组成。
通用块层
通用块层是一个内核组件,它处理来自系统中的所有对块设备发出的请求。由于该层所提供的函数,内核可以容易的做到:
将数据缓冲区放在高端内存——仅当CPU访问其数据时,才将页框映射为内核中的线性地址空间,并在数据访问完后取消映射。
通过一些附加额手段,实现一个所谓的“零-复制”模式,将磁盘数据直接存放在用户态地址空间而不是首先复制到内核内存区;事实上,内核为I/O数据传送使用的缓冲区所在的页框就映射在进程的用户态线性地址空间中。
管理逻辑卷,例如有LVM(逻辑卷管理)和RAID(磁盘冗余阵列)使用的逻辑卷:几个磁盘分区,即使位于不同的磁盘中,也可以被看做是一个单一的分区。
发挥大部分新磁盘控制器的高级特性。
bio结构
通用块层的核心数据结构是一个称为bio的描述符,它描述了块设备的I/O操作。在更上层的具体的文件系统的读写操作方法中,构造bio结构,并通过通用块层来提交各块设备驱动程序来进行实际的数据传输。每个bio结构都包含一个磁盘存储区表示符(存储区中的起始扇区号和扇区数目)和一个或多个描述与I/O操作相关的内存区的段。bio结构定义如下:
---------------------------------------------------------------------
include/linux/bio.h
62
struct bio {
63 sector_t
bi_sector; /* device address
in 512 byte
64 sectors
*/
65
struct bio *bi_next;
/* request queue link */
66 struct
block_device *bi_bdev;
67
unsigned long bi_flags; /* status, command, etc */
68 unsigned long bi_rw; /* bottom bits READ/WRITE,
69 * top bits priority
70 */
71
72
unsigned short bi_vcnt; /* how many bio_vec's */
73
unsigned short bi_idx; /* current index into bvl_vec */
74
75
/* Number of segments in this BIO after
76
* physical address coalescing is performed.
77
*/
78
unsigned int bi_phys_segments;
79
80 unsigned
int bi_size; /* residual I/O count */
81
82 /*
83
* To keep track of the max segment size, we account for the
84
* sizes of the first and last mergeable segments in this bio.
85
*/
86 unsigned
int bi_seg_front_size;
87
unsigned int bi_seg_back_size;
88
89
unsigned int bi_max_vecs; /*
max bvl_vecs we can hold */
90
91 unsigned
int bi_comp_cpu; /* completion CPU */
92 /* bio 的引用计数器 */
93
atomic_t bi_cnt; /* pin count */
94
95 struct
bio_vec *bi_io_vec; /* the actual vec list */
96 /* bio的I/O操作结束时调用的方法 */
97
bio_end_io_t *bi_end_io;
98 /* 通用块层和块设备驱动程序的I/O完成方法使用的指针 */
99
void *bi_private;
100
#if defined(CONFIG_BLK_DEV_INTEGRITY)
101 struct bio_integrity_payload
*bi_integrity; /* data integrity */
102
#endif
103
/* 释放bio时调用的析构方法(通常是bio_fs_destructor()函数)*/
104 bio_destructor_t *bi_destructor; /* destructor */
105
106 /*
107 *
We can inline a number of vecs at the end of the bio, to
108 * avoid double allocations for a small
number of bio_vecs.
109 * This member MUST obviously be kept at
the very end of the bio.
110 */
111 struct bio_vec bi_inline_vecs[0];
112
};
---------------------------------------------------------------------
bio结构中的每个段是由一个bio_vec数据结构描述的。其定义为:
---------------------------------------------------------------------
include/linux/bio.h
46 struct bio_vec {
47
struct page *bv_page; /* 指向段的页框中也描述符的指针*。
48
unsigned int bv_len; /* 段的字节长度 */
49 unsigned int
bv_offset; /* 页框中段数据的偏移量 */
50 };
---------------------------------------------------------------------
在块I/O操作期间bio描述符的内容一直保持更新。例如,如果块设备驱动程序在一次分散-聚集DMA操作中不能完成全部的数据传送,那么bio中的bi_idx字段会不断更新来指向待传送的第一个段。为了从索引bi_idx指向的当前段开始不断重复bio中的段,设备驱动程可以执行宏bio_for_each_segment().
当通用块层启动一次新的I/O操作时,调用bio_alloc()函数分配一个新的bio结构。内核使用fs_bio_set结构类型来管理bio结构相关内存分配的缓冲区,这个结构由几个用于引用内存池或slab缓冲的指针组成。fs_bio_set中的一个成员bio_pool用于引用分配bio结构的内存池,而在这个缓冲区中分配的内存块的单位并不是sizeof(struct bio),而是sizeof(struct bio) + BIO_INLINE_VECS
* sizeof(struct bio_vec)个字节。在分配了bio结构之后,通常要为bio分配bio_vec结构,当bio_vec结构数小于BIO_INLINE_VECS时则可以通过bio结构的最后一个成员bi_inline_vecs来引用这些bio_vec结构。
内核同时创建了6个slab缓冲区用于分配数量不等的bio_vec结构,当所需的bio_vec结构数大于BIO_INLINE_VECS,则从这些缓冲区中分配内存。
磁盘和磁盘分区的表示
磁盘是一个由通用块层处理的逻辑块设备。通常一个磁盘对应一个硬件块设备,例如硬盘、软盘或光盘。但是,磁盘也可以是一个虚拟设备,它建立在几个物理磁盘分区之上或一些RAM专用页中的内存上面。在任何情形中,借助通用块层提供的服务,上层内核组件可以以同样的方式工作在所有的磁盘上。
磁盘是由gendisk对象描述的,其定义为:
---------------------------------------------------------------------
include/linux/genhd.h
struct gendisk {
/*
major, first_minor and minors are input parameters only,
* don't use directly. Use disk_devt() and disk_max_parts().
*/
int
major; /* major number of driver
*/
int
first_minor; /* 与磁盘关联的第一个次设备号 */
int
minors; /* maximum number of
minors, =1 for
* disks that can't be partitioned. */
/* 磁盘的标准名称(通常是相应设备文件的规范名称
*/
char
disk_name[DISK_NAME_LEN]; /* name of major
driver */
char
*(*devnode)(struct gendisk *gd, mode_t *mode);
/*
Array of pointers to partitions indexed by partno.
*
Protected with matching bdev lock but stat and other
*
non-critical accesses use RCU. Always
access through
*
helpers.
*/
struct
disk_part_tbl *part_tbl;
struct
hd_struct part0;
const
struct block_device_operations *fops;
struct
request_queue *queue;
void
*private_data; /* 块设备驱动程序的私有数据 */
/* 描述磁盘类型的标志 */
int
flags;
struct
device *driverfs_dev; // FIXME: remove
struct
kobject *slave_dir;
/* 该类型指向的这个数据结构记录磁盘中断的定时;由内核内置的
* 随机数发生器使用*/
struct
timer_rand_state *random;
/* 写入磁盘的扇区数计数器 */
atomic_t
sync_io; /* RAID */
struct
work_struct async_notify;
#ifdef
CONFIG_BLK_DEV_INTEGRITY
struct
blk_integrity *integrity;
#endif
int
node_id;
};
---------------------------------------------------------------------
flags字段存放了关于磁盘的信息。其中最重要的标志是GENHD_FL_UP:如果该位置位,那么磁盘将被初始化并可以使用。另一个相关的标志是GENHD_FL_REMOVABLE,如果是诸如软盘或光盘这样可移动的磁盘,那么就要设置该标志。
gendisk对象的fops字段指向一个表block_device_operations,该表为块设备的主要操作存放了几个定制的方法。其定义为:
---------------------------------------------------------------------
include/linux/blkdev.h
1277 struct block_device_operations {
/* 打开块设备文件 */
1278 int
(*open) (struct block_device *, fmode_t);
/* 关闭对块设备文件的最后一个引用 */
1279 int
(*release) (struct gendisk *, fmode_t);
1280 int
(*locked_ioctl) (struct block_device *, fmode_t, unsigned, unsigned long);
/* 在块设备文件上发出ioctl()系统调用(使用大内核锁) */
1281 int
(*ioctl) (struct block_device *, fmode_t, unsigned, unsigned long);
/* 在块设备文件上发出ioctl()系统调用(不使用大内核锁) */
1282 int
(*compat_ioctl) (struct block_device *, fmode_t, unsigned, unsigned long);
1283 int
(*direct_access) (struct block_device *, sector_t,
1284
void **, unsigned long *);
/* 检查可移动介质是否已经变化(例如软盘)*/
1285 int
(*media_changed) (struct gendisk *);
1286
unsigned long long (*set_capacity) (struct gendisk *,
1287
unsigned long long);
/* 检查块设备是否持有有效数据 */
1288 int
(*revalidate_disk) (struct gendisk *);
1289 int
(*getgeo)(struct block_device *, struct hd_geometry *);
1290
struct module *owner;
1291 };
---------------------------------------------------------------------
通常硬盘被划分成几个逻辑分区。每个块设备文件要么代表整个磁盘,要么代表磁盘中的某一个分区。例如,一个主设备号为3的,次设备号为0的设备文件/dev/had代表的可能是一个主EIDE磁盘;该磁盘中的前两个分区分别由设备文件/dev/hda1和/dev/hda2代表,它们的主设备号都是3,而此设备号分别为1和2。一般而言,磁盘中的分区是由连续的次设备号来区分的。
如果将一个硬盘分成了几个区,那么其分区表保存在disk_part_tbl结构中,由gendisk结构的part_tbl字段来引用。disk_part_tbl结构定义为:
---------------------------------------------------------------------
include/linux/genhd.h
129 struct disk_part_tbl {
130 struct
rcu_head rcu_head;
131 int
len;
132 struct
hd_struct *last_lookup;
133 struct
hd_struct *part[];
134 };
---------------------------------------------------------------------
各个分区信息保存在hd_struct结构数组中,由part字段引用。通过磁盘内分区的相对索引对该数组进行索引。hd_struct结构定义如下:
---------------------------------------------------------------------
include/linux/genhd.h
90
struct hd_struct {
91 sector_t start_sect;
92 sector_t nr_sects;
93 sector_t alignment_offset;
94 unsigned int discard_alignment;
95 struct device __dev;
96 struct kobject *holder_dir;
97 int policy, partno;
98 #ifdef
CONFIG_FAIL_MAKE_REQUEST
99 int make_it_fail;
100 #endif
101
unsigned long stamp;
102 int
in_flight[2];
103 #ifdef
CONFIG_SMP
104 struct
disk_stats __percpu *dkstats;
105 #else
106 struct
disk_stats dkstats;
107 #endif
108 struct
rcu_head rcu_head;
109 };
---------------------------------------------------------------------
当内核发现系统中一个新的磁盘时(在启动阶段,或将一个可移动介质插入一个驱动器中时,或在运行期间附加一个外置式磁盘时),就调用alloc_disk()函数,该函数分配并初始化一个新的gendisk对象,如果新磁盘被分成了几个分区,那么alloc_disk()还会分配一个适当的disk_part_tbl结构。然后,内核调用add_disk()函数将新的gendisk对象插入到通用块层的数据结构中。
提交请求
我们假设被请求的数据块在磁盘上是相邻的,并且内核已经知道了它们的物理位置。比如btrfs文件系统提交bio的过程,在函数submit_extent_page()中完成,它会调用extent_bio_alloc()来分配bio。extent_bio_alloc()定义为:
---------------------------------------------------------------------
fs/btrfs/extent_io.c
1879
static struct bio *
1880
extent_bio_alloc(struct block_device *bdev, u64 first_sector, int
1881 nr_vecs, gfp_t gfp_flags)
1882
{
1883 struct bio *bio;
1884
1885 bio = bio_alloc(gfp_flags, nr_vecs);
1886
1887 if (bio == NULL &&
(current->flags & PF_MEMALLOC)) {
1888 while (!bio &&
(nr_vecs /= 2))
1889 bio =
bio_alloc(gfp_flags, nr_vecs);
1890 }
1891
1892 if (bio) {
1893 bio->bi_size = 0;
1894 bio->bi_bdev = bdev;
1895 bio->bi_sector =
first_sector;
1896 }
1897 return bio;
1898
}
---------------------------------------------------------------------
extent_bio_alloc()函数会调用bio_alloc()函数分配一个新的bio描述符。bio_alloc()函数同时分配bio_vec结构数组,并将bi_io_vec设为bio_vec结构数组的起始地址,数组中每个元素描述了I/O操作中的一个段(内存缓冲);此外,将bi_vcnt设为bio中总的段数。然后设置bio的一些字段来初始化bio描述符:
将bi_bdev设为块设备描述符的地址。
将bi_sector设为数据的起始扇区号(如果磁盘分成了几个区,那么扇区号是相对于分区的起始位置的)。
submit_extent_page()还会完成如下操作:
调用bio_add_page()来初始化bio_vec数组,也就是使bio_vec数组中各成员都指示数据的正确位置,都指向正确的页中的段。
设置bi_end_io为当bio上的I/O操作完成时所执行的完成程序的地址。
然后调用submit_one_bio()函数来提交bio,这个函数定义本质上主要调用submit_bio()函数来提交请求,而submit_bio()函数则主要为在设置了bi_rw为被请求的操作的标志之后,调用generic_make_request(bio)函数,它是通用块层的主要入口点。
其定义为:
---------------------------------------------------------------------
block/blk-core.c
1490
/*
1491 * We only want one ->make_request_fn to be
active at a time,
1492 * else stack usage with stacked devices could
be a problem.
1493 * So use current->bio_list to keep a list
of requests
1494 * submited by a make_request_fn function.
1495 * current->bio_list is also used as a flag
to say if
1496 * generic_make_request is currently active in
this task or not.
1497 * If it is NULL, then no make_request is
active. If it is non-NULL,
1498 * then a make_request is active, and new
requests should be added
1499 * at the tail
1500 */
1501
void generic_make_request(struct bio *bio)
1502
{
1503 struct bio_list bio_list_on_stack;
1504
1505 if (current->bio_list) {
1506 /* make_request is active */
1507
bio_list_add(current->bio_list, bio);
1508 return;
1509 }
1510 /* following loop may be a bit
non-obvious, and so deserves
1511 * some explanation.
1512 * Before entering the loop, bio->bi_next
is NULL (as all
1513 * callers ensure that) so we have a
list with a single bio.
1514 * We pretend that we have just taken
it off a longer list,
1515 * so we assign bio_list to a pointer
to the
1516 * bio_list_on_stack,thus initialising
the bio_list of new
1517 * bios to be added. __generic_make_request may indeed add
1518 * some more bios through a recursive
call to
1519 * generic_make_request. If it did, we find a non-NULL
1520 * value in bio_list and re-enter the
loop from the top. In
1521 * this case we really did just take
the bio of the top of
1522 * the list (no pretending) and so
remove it from bio_list,
1523 * and call into
__generic_make_request again.*
1524 * The loop was structured like this
to make only one call
1525 * to __generic_make_request (which is
important as it is
1526 * large and inlined) and to keep the
structure simple.
1527 */
1528 BUG_ON(bio->bi_next);
1529 bio_list_init(&bio_list_on_stack);
1530 current->bio_list =
&bio_list_on_stack;
1531 do {
1532 __generic_make_request(bio);
1533 bio =
bio_list_pop(current->bio_list);
1534 } while (bio);
1535 current->bio_list = NULL; /*
deactivate */
1536
}
1537
EXPORT_SYMBOL(generic_make_request);
---------------------------------------------------------------------
generic_make_request(bio)函数又主要调用__generic_make_request()函数来完成工作,__generic_make_request()定义为:
---------------------------------------------------------------------
block/blk-core.c
1404
static inline void __generic_make_request(struct bio *bio)
1405
{
1406 struct request_queue *q;
1407 sector_t old_sector;
1408 int ret, nr_sectors =
bio_sectors(bio);
1409 dev_t old_dev;
1410 int err = -EIO;
1411
1412 might_sleep();
1413
1414 if (bio_check_eod(bio, nr_sectors))
1415 goto end_io;
1416
1417 /*
1418 * Resolve the mapping until finished.
(drivers are
1419
* still free to implement/resolve
their own stacking
1420 * by explicitly returning 0)
1421 *
1422 * NOTE: we don't repeat the blk_size
check for each new device.
1423 * Stacking drivers are expected to know
what they are doing.
1424 */
1425 old_sector = -1;
1426 old_dev = 0;
1427 do {
1428 char b[BDEVNAME_SIZE];
1429
1430 q =
bdev_get_queue(bio->bi_bdev);
1431 if (unlikely(!q)) {
1432 printk(KERN_ERR
1433 "generic_make_request: Trying to access
"
1434
"nonexistent
block-device %s (%Lu)\n",
1435
bdevname(bio->bi_bdev,
b),
1436
(long long)
bio->bi_sector);
1437 goto end_io;
1438 }
1439
1440 if (unlikely(!bio_rw_flagged(bio,
BIO_RW_DISCARD) &&
1441 nr_sectors >
queue_max_hw_sectors(q))) {
1442 printk(KERN_ERR "bio too
big device %s (%u > %u)\n",
1443 bdevname(bio->bi_bdev, b),
1444 bio_sectors(bio),
1445 queue_max_hw_sectors(q));
1446 goto end_io;
1447 }
1448
1449 if (unlikely(test_bit(QUEUE_FLAG_DEAD,
&q->queue_flags)))
1450 goto end_io;
1451
1452 if (should_fail_request(bio))
1453 goto end_io;
1454
1455 /*
1456 * If this device has partitions,
remap block n
1457 * of partition p to block n+start(p)
of the disk.
1458 */
1459 blk_partition_remap(bio);
1460
1461 if (bio_integrity_enabled(bio)
&& bio_integrity_prep(bio))
1462 goto end_io;
1463
1464 if (old_sector != -1)
1465 trace_block_remap(q, bio,
old_dev, old_sector);
1466
1467 old_sector = bio->bi_sector;
1468 old_dev = bio->bi_bdev->bd_dev;
1469
1470 if (bio_check_eod(bio, nr_sectors))
1471 goto end_io;
1472
1473 if (bio_rw_flagged(bio, BIO_RW_DISCARD)
&&
1474 !blk_queue_discard(q)) {
1475 err = -EOPNOTSUPP;
1476 goto end_io;
1477 }
1478
1479 trace_block_bio_queue(q, bio);
1480
1481 ret = q->make_request_fn(q, bio);
1482 } while (ret);
1483
1484 return;
1485
1486
end_io:
1487 bio_endio(bio, err);
1488
}
---------------------------------------------------------------------
__generic_make_request()函数提交一个buffer给buffer的设备驱动来进行I/O,它接收唯一的一个参数,即描述内存和设备上的数据的位置的bio结构。它不返回任何状态值,除了关于完成的通知外,请求的success/failure都是通过在其他地方定义的bio->bi_end_io函数来异步传送的。generic_make_request()的调用者必须确保bi_io_vec被正确的设置来描述内存缓冲,bi_dev和bi_sector被设置用来描述设备上的地址,bi_end_io和可选的bi_private字段被设置用来描述如何发送完成信号。
如果bio 碰巧和其他的bio合并了,则generic_make_request()和它调用的驱动可能会使用bi_next字段,并且可能改变bi_dev和bi_sector来重映射。所以generic_make_request()的调用者不应该依赖这些字段的值。
__generic_make_request()函数执行下列操作:
1、调用bio_check_eod(bio,
nr_sectors)检查bio->bi_size没有超过块设备的扇区数。如果超过,则将设置bio->bi_flags的BIO_EOF位,然后打印一条内核错误消息,调用bio_endio()函数,并终止。bio_endio()清除bio->bi_flags的BIO_UPTODATE位,然后调用bio的bi_end_io()方法。bi_end_io()函数的实现本质上依赖于触发I/O数据传送的内核组件。
2、获取与块设备相关的请求队列q;其地址存放在块设备描述符的bd_disk->queue字段中,而块设备描述符地址则存放在bio->bi_bdev中。
3、调用blk_partition_remap(bio)函数检查块设备是否指向一个磁盘分区(bio->bi_bdev不等于bio->bdev->bd_contains)。如果是,则从bio->bi_bdev获取分区的hd_struct描述符,,从而执行下面的操作:
a.调整bio->bi_sector值使其原来的相对于分区的起始扇区号变为相对于整个磁盘的扇区号。
b.将bio->bi_bdev设置为整个磁盘的块设备描述符(bdev->bd_contains)。
从现在开始,通用块层、I/O调度程序以及设备驱动程序将忘记磁盘分区的存在,直接作用于整个磁盘。
4、调用q->make_request_fn(q,
bio)方法将bio插入请求队列q中。
5、返回。