1.5 块设备I/O调度层的处理
下面进入块设备I/O调度层,来看看q->make_request_fn方法。不过这个方法的具体函数是什么呢?别着急,要弄清这个问题还需要再补充一下块设备驱动的基础知识,不然就又走不下去了。块设备驱动程序是Linux块子系统中的最底层组件。它们从I/O调度程序中获得请求,然后按要求处理这些请求。
当然,块设备驱动程序是设备驱动程序模型的组成部分(也就是在sysfs中能够看到它)。因此,每个块设备驱动程序对应一个device_driver类型的描述符;此外,设备驱动程序处理的每个磁盘都与一个device类型的描述符相关联。但是,这些描述符没有什么特别的目的:只不过是为块I/O子系统必须在sysfs为系统中的每个块设备用来存放附加信息,后面等我们遇到了他们了,再来详细分析。
我们真正应该关注的,是块设备本身。每个块设备都是由一个block_device结构的描述符来表示,其字段如下:
struct block_device { dev_t bd_dev; /* 块设备的主设备号和次设备号 */ struct inode * bd_inode; /* 指向bdev文件系统中块设备对应的索引节点的指针 */ int bd_openers; /* 计数器,统计块设备已经被打开了多少次 */ struct mutex bd_mutex; /* 保护块设备的打开和关闭的信号量 */ struct mutex bd_mount_mutex; /* 禁止在块设备上进行新安装的信号量 */ struct list_head bd_inodes; /* 已打开的块设备文件的索引节点链表的首部 */ void * bd_holder; /* 块设备描述符的当前所有者 */ int bd_holders; /* 计数器,统计对bd_holder字段多次设置的次数 */ #ifdef CONFIG_SYSFS struct list_head bd_holder_list; #endif struct block_device * bd_contains; /* 如果块设备是一个分区, 则指向整个磁盘的块设备描述符; 否则,指向该块设备描述符 */ unsigned bd_block_size; /* 块大小 */ struct hd_struct * bd_part; /* 指向分区描述符的指针 (如果该块设备不是一个分区,则为NULL) */ /* number of times partitions within this device have been opened. */ unsigned bd_part_count; /* 计数器, 统计包含在块设备中的分区已经被打开了多少次 */ int bd_invalidated; /* 当需要读块设备的分区表时设置的标志 */ struct gendisk * bd_disk; /* 指向块设备中基本磁盘的gendisk结构的指针 */ struct list_head bd_list; /* 用于块设备描述符链表的指针 */ struct backing_dev_info *bd_inode_backing_dev_info; /*(通常为NULL)*/ unsigned long bd_private; /* 指向块设备持有者的私有数据的指针 */ }; |
所有的块设备描述符被插入一个全局链表中,链表首部是由变量all_bdevs表示的;链表链接所用的指针位于块设备描述符的bd_list字段中。
如果块设备描述符对应一个磁盘分区,那么bd_contains字段指向与整个磁盘相关的块设备描述符,而bd_part字段指向hd_struct分区描述符。否则,若块设备描述符对应整个磁盘,那么bd_contains字段指向块设备描述符本身,bd_part_count字段用于记录磁盘上的分区已经被打开了多少次。
bd_holder字段存放代表块设备持有者的线性地址。持有者并不是进行I/O数据传送的块设备驱动程序;准确地说,它是一个内核组件,使用设备并拥有独一无二的特权(例如,它可以自由使用块设备描述符的bd_private字段)。典型地,块设备的持有者是安装在该设备上的文件系统。当块设备文件被打开进行互斥访问时,另一个普遍的问题出现了:持有者就是对应的文件对象。
bdclaim()函数将bd_holder字段设置为一个特定的地址;相反,bd_release()函数将该字段重新设置为NULL。然而,值得注意的是,同一个内核组件可以多次调用bdclaim()函数,每调用一次都增加bd_holders的值。为了释放块设备,内核组件必须调用bd_release()函数bd_holders次。
ULK-3上有一个很经典的图对一个整盘进行了描述,它说明了块设备描述符是如何被链接到通用块层的其他重要数据结构上的。
那么,如何访问一个块设备呢?当内核接收一个打开块设备文件的请求时,必须首先确定该设备文件是否已经是打开的。事实上,如果文件已经是打开的,内核就没有必要创建并初始化一个新的块设备描述符;相反,内核应该更新这个已经存在的块设备描述符。然而,真正的复杂性在于具有相同主设备号和次设备号但有不同路径名的块设备文件被VFS看作不同的文件,但是它们实际上指向同一个块设备。因此,内核无法通过简单地在一个对象的索引节点高速缓存中检查块设备文件的存在就确定相应的块设备已经在使用。
主、次设备号和相应的块设备描述符之间的关系是通过bdev特殊文件系统(挂载在/dev目录的子目录中)来维护的。每个块设备描述符都对应一个bdev特殊文件:块设备描述符的bd_inode字段指向相应的bdev索引节点;而该索引节点则将块设备的主、次设备号和相应描述符的地址进行编码。
bdget()接收块设备的主设备号和次设备号作为其参数:在bdev文件系统中查寻相关的索引节点;如果不存在这样的节点,那么就分配一个新索引节点和新块设备描述block_device。在任何情形下,函数都返回一个与给定主、次设备号对应的块设备描述符的地址。
一旦找到了块设备的描述符,那么内核通过检查bd_openers字段的值来确定块设备当前是否在使用:如果值是正的,说明块设备已经在使用(可能通过不同的设备文件)。同时内核也维护一个与已打开的块设备文件对应的索引节点对象的链表。该链表存放在块设备描述符的bd_inodes字段中;索引节点对象的i_devices字段存放用于链接表中的前后元素的指针。
一个块设备驱动程序可能处理几个块设备。例如,IDE设备驱动程序可以处理几个IDE磁盘,其中的每个都是一个单独的块设备。而且,每个磁盘通常是被分区的,每个分区又可以被看做是一个逻辑磁盘。很明显,块设备驱动程序必须处理在块设备对应的块设备文件上发出所有的VFS系统调用。
1.5.1 块设备的初始化
q->make_request_fn方法的具体实现函数,取决于具体的块设备。下面我们就通过当前最流行的SCSI磁盘控制器来说明一个具体的块设备在Linux内核中是如何建立I/O调度和底层驱动体系的。Linux是在系统初始化的时候执行genhd_device_init()函数启动整个Block子系统的:
static int __init genhd_device_init(void) { …… bdev_map = kobj_map_init(base_probe, &block_class_lock); blk_dev_init();
register_blkdev(BLOCK_EXT_MAJOR, "blkext");
#ifndef CONFIG_SYSFS_DEPRECATED /* create top-level block dir */ block_depr = kobject_create_and_add("block", NULL); #endif return 0; } |
该函数主要是调用来自block/ll_rw_blk.c中的blk_dev_init()来对块设备进行初始化:
int __init blk_dev_init(void) { BUILD_BUG_ON(__REQ_NR_BITS > 8 * sizeof(((struct request *)0)->cmd_flags));
kblockd_workqueue = create_workqueue("kblockd"); if (!kblockd_workqueue) panic("Failed to create kblockd/n");
request_cachep = kmem_cache_create("blkdev_requests", sizeof(struct request), 0, SLAB_PANIC, NULL);
blk_requestq_cachep = kmem_cache_create("blkdev_queue", sizeof(struct request_queue), 0, SLAB_PANIC, NULL);
return 0; } |
首先第一个函数,create_workqueue("kblockd"),表示每个处理机给他分配一个工作队列。这里返回值赋给了kblocked_workqueue。接下来,使用两个kmem_cache_create()分别为请求描述符、请求队列描述符和I/O上下文建立两个slab分配器,他们分别是这里的request和request_queue。
我们再回到genhd_device_init()中来,很显然,这里还有两个函数我们并没有讲,一个是kobj_map_init(),一个是register_blkdev()。先说前者,kobj_map_init函数,sysfs体系里边的东西,定义于drivers/base/map.c:
struct kobj_map { struct probe { struct probe *next; dev_t dev; unsigned long range; struct module *owner; kobj_probe_t *get; int (*lock)(dev_t, void *); void *data; } *probes[255]; struct mutex *lock; }; static struct kobj_map *bdev_map; |
而genhd_device_init()中的kobj_map_init()函数是这样的:
struct kobj_map *kobj_map_init(kobj_probe_t *base_probe, struct mutex *lock){ struct kobj_map *p = kmalloc(sizeof(struct kobj_map), GFP_KERNEL); struct probe *base = kzalloc(sizeof(*base), GFP_KERNEL); int i; …… base->dev = 1; base->range = ~0; base->get = base_probe; for (i = 0; i < 255; i++) p->probes[i] = base; p->lock = lock; return p; } |
看得出,申请了一个struct kobj_map的指针p,然后最后返回的也是p,即最后把一切都献给了bdev_kmap。而这里真正干的事情无非就是让bdev_kmap->probes[]数组全都等于base。换言之,它们的get指针全都指向了这里传递进来的base_probe函数,这个函数主要是用来将块设备的与sysfs中的kobject建立联系,我们这里就不再赘述了。
相比之下,register_blkdev()函数更容易理解,注册Block系统,来自block/genhd.c:
55 int register_blkdev(unsigned int major, const char *name) 56 { 57 struct blk_major_name **n, *p; 58 int index, ret = 0; 59 60 mutex_lock(&block_subsys_lock); 61 62 /* temporary */ 63 if (major == 0) { 64 for (index = ARRAY_SIZE(major_names)-1; index > 0; index--) { 65 if (major_names[index] == NULL) 66 break; 67 } 68 69 if (index == 0) { 70 printk("register_blkdev: failed to get major for %s/n", 71 name); 72 ret = -EBUSY; 73 goto out; 74 } 75 major = index; 76 ret = major; 77 } 78 79 p = kmalloc(sizeof(struct blk_major_name), GFP_KERNEL); 80 if (p == NULL) { 81 ret = -ENOMEM; 82 goto out; 83 } 84 85 p->major = major; 86 strlcpy(p->name, name, sizeof(p->name)); 87 p->next = NULL; 88 index = major_to_index(major); 89 90 for (n = &major_names[index]; *n; n = &(*n)->next) { 91 if ((*n)->major == major) 92 break; 93 } 94 if (!*n) 95 *n = p; 96 else 97 ret = -EBUSY; 98 99 if (ret < 0) { 100 printk("register_blkdev: cannot get major %d for %s/n", 101 major, name); 102 kfree(p); 103 } 104 out: 105 mutex_unlock(&block_subsys_lock); 106 return ret; 107 } |
如果是一个基于scsi的块设备,咱们是指定了主设备号了的。换言之,这里的major是非零值,而struct blk_major_name的定义也在block/genhd.c中:
static struct blk_major_name { struct blk_major_name *next; int major; char name[16]; } *major_names[BLKDEV_MAJOR_HASH_SIZE]; |
注意这里也是顺便定义了一个数组major_names,咱们这里也用到了。这其中BLKDEV_MAJOR_HASH_SIZE是255。即数组major_names[]有255个元素,换言之,咱们定义了255个指针.
而88行这个内联函数同样来自block/genhd.c:
static inline int major_to_index(int major) { 36 return major % BLKDEV_MAJOR_HASH_SIZE; 37 } |
比如咱们传递的major是8,那么major_to_index就是8。
不难理解,register_blkdev()这个函数做的事情就是,为这255个指针找到归属,即先在79行调用kmalloc申请一个struct blk_major_name结构体并且让p指向它,接下来为p赋值,而n将指向major_names[index],比如index就是8,那么n就指向major_names[8],一开始它肯定为空,所以直接执行94行并进而95行,于是就把赋好值的p的那个结构体赋给了major_names[8],因此major_names[8]就既有major也有name了,name就是“sd”。
此时此刻,在/dev/目录下的sda,sdb之类的文件,我们就可以通过/proc/devices看到这个块设备驱动了。事实上,proc文件系统中,deivices文件的Block devices部分的内容也正是读major_names这个数组的内容,来显示出块设备驱动的名字的。