一、将块设备添加到系统
register_blkdev并没有真正将设备添加到系统中,想要将设备添加到系统中,需要使用如下API:void blk_register_region(dev_t devt, unsigned long range, struct module *module,
struct kobject *(*probe)(dev_t, int *, void *),
int (*lock)(dev_t, void *), void *data)
该函数会将块设备添加到bdev_map中,这是一个由内核维护的数据库,包含了系统所有的块设备。在打开块设备时,必然会调用blkdev_get,而blkdev_get会查询该数据库来获取块设备,这个过程类似于字符设备,字符设备在内核中也有一个数据库cdev_map,在打开字符设备时会查询cdev_map。不过很少需要直接调用该函数,add_disk会自动调用该函数。
1.1 添加磁盘和分区到系统中
为了将一个磁盘添加到系统中,对系统可用,必须初始化磁盘数据结构并调用add_disk方法。需要特别注意的是一旦调用了add_disk,磁盘就被“激活”了,系统随时都可能会调用该磁盘提供的各种方法,甚至在该函数返回之前就会调用,因而在完成磁盘结构的初始化之前,不要调用add_disk。add_disk的原型如下:void add_disk(struct gendisk *disk);
它完成的工作主要包括:
- 根据磁盘的主次设备号信息为磁盘分配设备号
- 调用disk_alloc_events初始化磁盘的事件(alloc|add|del|release)处理机制。在最开始磁盘事件会被设置为被阻塞的。
- 调用bdi_register_dev将磁盘注册到bdi
- 调用blk_register_region将磁盘添加到bdev_map中
- 调用register_disk将磁盘添加到系统中。主要完成
- 将主设备的分区(第0个分区)信息标记设置为分区无效
- 调用device_add将设备添加到系统中
- 在sys文件系统中为设备及其属性创建目录及文件
- 发出设备添加到系统的uevent事件(如果能获取分区的信息,则也为分区发送uevent事件)。
- 调用blk_register_queue注册磁盘的请求队列。主要是为队列和队列的调度器在设备的sys文件系统目录中创建相应的sys目录/文件,并且发出uevent事件。
- 调用__disk_unblock_events完成
- 在/sys文件系统的设备目录下创建磁盘的事件属性文件
- 将磁盘事件添加到全局链表disk_events中
- 解除对磁盘事件的阻塞。
当扫描到一个分区时,需要将它添加到磁盘中,这是通过以下API实现的:
struct hd_struct *add_partition(struct gendisk *disk, int partno,
sector_t start, sector_t len, int flags,
struct partition_meta_info *info);
- disk:分区所属的磁盘
- partno:分区在磁盘的分区号
- start:起始扇区号
- len:该分区包括多少个扇区
- flags:该扇区的标志
- info:该分区的partition_meta_info信息
- 扩展磁盘的分区表
- 分配分区数据结构并进行初始化
- 调用device_initialize初始化分区的设备数据结构
- 设置分区的设备号
- 调用device_add将分区添加到系统中
- 创建分区设备相关的sys文件系统文件
- 发送添加分区的uevent事件
- 初始化分区的引用计数
二、块设备操作
2.1 打开块设备
在所有的文件系统的实现中,在获取文件的inode时,对于不是常规文件、目录文件、连接文件的特殊文件都会调用init_special_inode,该函数的代码在学习字符设备时已经贴出来过,对于块设备文件,该函数会将inode的文件操作函数结构设置为def_blk_fops,其中的打开文件函数为blkdev_open。其原型为:int blkdev_open(struct inode * inode, struct file * filp);
参数的含义很明显。它完成的工作有:
- 调用bd_acquire获取块设备文件的block_device结构。该函数会调用bdget尝试从bdev文件系统中查找设备文件对应的inode,如果有就直接返回,如果没有会分配一个新的inode并且初始化该inode再返回。设备文件的inode会被添加到block_device的bd_inodes链表中。块设备对应的block_device也会在这一步被添加到全局的all_bdevs中。
- 设置file结构的f_mapping为bdev->bd_inode->i_mapping。bdev->bd_inode在inode的创建和初始化中北初始化,具体的函数为alloc_inode和bdget。其中的address_space_operations被设置为def_blk_aops,这是后续要用到的函数,这是和设备交互的接口。
- 调用blkdev_get。该函数最主要的工作时完成块设备的打开动作,同时根据传入的模式还可能声明设备的持有者。
- 调用get_gendisk获取块设备所对应的通用磁盘结构,这里可能需要查询bdev_map数据库。
- 阻塞磁盘的事件处理
- 如果是第一次打开该块设备,则
- 填充块设备数据结构的bd_disk,bd_queue,bd_contains(它会设置为自身)
- 如果是主设备(即不是分区),则
- 设置块设备数据结构的bd_part
- 如果提供了disk->fops->open,则调用它
- 如果分区无效,则调用rescan_partitions重新扫描分区
- 如果打开设备时返回了ENOMEDIUM错误,则调用invalidate_partitions将所有分区设置为无效
- 否则,如果是分区设备,则
- 获取主设备的块设备数据结构
- 递归调用__blkdev_get,但是这次传入的是主设备的块设备数据结构。本次调用会走第一次打开设备并且是主设备的分支,由于是第一次打开,因而分区信息应该是无效的,这就会走到重新扫描分区的分支。
- 设置块设备数据结构的bd_contains(它被设置为主设备的block_device),bd_part
- 调用bd_set_size设置分区的大小信息
- 否则如果不是第一次打开设备,则
- 如果是主设备(这里是通过bdev->bd_contains == bdev判断的,因为根据该函数的前边流程,只有主设备的这个条件才能成立),则
- 如果提供了disk->fops->open,则调用它
- 如果分区无效,则调用rescan_partitions重新扫描分区
- 如果打开设备时返回了ENOMEDIUM错误,则调用invalidate_partitions将所有分区设置为无效
- 如果是主设备(这里是通过bdev->bd_contains == bdev判断的,因为根据该函数的前边流程,只有主设备的这个条件才能成立),则
- 增加设备的打开计数
- 解除对设备事件的阻塞
2.2 读写操作
在打开块设备文件后,块设备文件的操作函数集也被设置为def_blk_fops,随后即可用其中的函数进行读写。其读函数为do_sync_read,写函数为do_sync_write,但是它们最终分别调用generic_file_aio_read和blkdev_aio_write来完成实际的读写操作。generic_file_aio_read的原型为:
ssize_t generic_file_aio_read(struct kiocb *iocb, const struct iovec *iov, unsigned long nr_segs, loff_t pos);
- iocb:内核I/O控制块
- iov:I/O请求向量
- nr_segs:I/O请求向量中有多少个请求
- pos:当前文件位置
- 如果是直接IO,则调用filp->f_mapping->a_ops->direct_IO进行直接IO。在open时已经将filp->f_mapping->a_ops设置def_blk_aops了。
- 对于请求向量中的每一个请求,创建一个read_descriptor_t并调用do_generic_file_read进行处理
ssize_t blkdev_aio_write(struct kiocb *iocb, const struct iovec *iov, unsigned long nr_segs, loff_t pos);
其参数和读的类似,其处理流程为:
- 调用__generic_file_aio_write进行处理。该函数也会分别对待直接IO和常规的写。流程和读类似。
无论是读写都是和缓冲区交互,缓冲区位于文件数据结构的struct address_space类型的变量f_mapping中,并以radix树的形式被管理。内核在合适的时机会向设备发起实际的IO操作,这是通过文件数据结构的struct address_space类型的成员变量f_mapping中的address_space_operations类型的成员中的函数来实现的,在打开块设备文件时,该成员被设置为了def_blk_aops。对于读会调用该地址空间操作集的readpage(对于块设备readpage成员函数为blkdev_readpage)成员函数或者其它读成员函数,对于写会调用该地址空间操作集的writepage(对于块设备为blkdev_writepage)成员函数或者其它相关成员函数函数。在def_blk_aops提供的这些函数中会将读写转变成IO请求提交给设备,到了此时才真正是要和设备进行数据交换。
因此对于块设备来说,用户是和缓冲区交互(直接IO除外),而内核负责在合适的时机完成缓冲区和设备之间的交互。操作缓冲区的函数,缓冲区本身,以及缓冲区与设备之间的交互方式都保存在file结构中。
2.3 请求结构
当内核通过address_space_operations中的成员函数向设备发起读写操作时,读写操作都会被转变成一个对块设备的IO请求提交给设备。内核使用数据结构struct bio来表示一个对块设备的IO,其定义如下:struct bio {
sector_t bi_sector; /* device address in 512 byte sectors */
struct bio *bi_next; /* request queue link */
struct block_device *bi_bdev;
unsigned long bi_flags; /* status, command, etc */
unsigned long bi_rw; /* bottom bits READ/WRITE,
* top bits priority
*/
unsigned short bi_vcnt; /* how many bio_vec's */
unsigned short bi_idx; /* current index into bvl_vec */
/* Number of segments in this BIO after
* physical address coalescing is performed.
*/
unsigned int bi_phys_segments;
unsigned int bi_size; /* residual I/O count */
/*
* To keep track of the max segment size, we account for the
* sizes of the first and last mergeable segments in this bio.
*/
unsigned int bi_seg_front_size;
unsigned int bi_seg_back_size;
unsigned int bi_max_vecs; /* max bvl_vecs we can hold */
atomic_t bi_cnt; /* pin count */
struct bio_vec *bi_io_vec; /* the actual vec list */
bio_end_io_t *bi_end_io;
void *bi_private;
#if defined(CONFIG_BLK_DEV_INTEGRITY)
struct bio_integrity_payload *bi_integrity; /* data integrity */
#endif
bio_destructor_t *bi_destructor; /* destructor */
/*
* We can inline a number of vecs at the end of the bio, to avoid
* double allocations for a small number of bio_vecs. This member
* MUST obviously be kept at the very end of the bio.
*/
struct bio_vec bi_inline_vecs[0];
};
关键域及其含义:
- bi_sector:传输开始的扇区号
- bi_next:将与一个请求相关的bio连接到同一个链表中
- bi_bdev:与请求相关联的设备的数据结构
- bi_phys_segments:在经过合并之后,该BIO所对应的的段数目
- bi_size:该BIO涉及到的数据的长度
- bi_io_vec:指向了包含了实际的IO数据结构的数组。
- bi_end_io:当IO完成时,将被调用用于完成此次IO
- bi_destructor:析构函数,当从内存删除一个BIO结构时被调用
bi_io_vec的每个数组项都指向一个内存页,这个内存页用于从设备读取数据或者向设备传输数据。这些内存页可以是连续的也可以不是连续的。其结构如图所示:
BIO是内核用于表示一个IO请求的结构,它会被提交给设备,当需要和设备交互时,内核会先准备BIO结构,然后通过目标设备的请求队列上的make_request_fn函数将BIO转变成一个请求,内核使用数据结构struct request来表示一个对块设备的请求,其数据结构定义如下:
struct request {
struct list_head queuelist;
struct call_single_data csd;
struct request_queue *q;
unsigned int cmd_flags;
enum rq_cmd_type_bits cmd_type;
unsigned long atomic_flags;
int cpu;
/* the following two fields are internal, NEVER access directly */
unsigned int __data_len; /* total data len */
sector_t __sector; /* sector cursor */
struct bio *bio;
struct bio *biotail;
struct hlist_node hash; /* merge hash */
/*
* The rb_node is only used inside the io scheduler, requests
* are pruned when moved to the dispatch queue. So let the
* completion_data share space with the rb_node.
*/
union {
struct rb_node rb_node; /* sort/lookup */
void *completion_data;
};
/*
* Three pointers are available for the IO schedulers, if they need
* more they have to dynamically allocate it. Flush requests are
* never put on the IO scheduler. So let the flush fields share
* space with the elevator data.
*/
union {
struct {
struct io_cq *icq;
void *priv[2];
} elv;
struct {
unsigned int seq;
struct list_head list;
rq_end_io_fn *saved_end_io;
} flush;
};
struct gendisk *rq_disk;
struct hd_struct *part;
unsigned long start_time;
#ifdef CONFIG_BLK_CGROUP
unsigned long long start_time_ns;
unsigned long long io_start_time_ns; /* when passed to hardware */
#endif
/* Number of scatter-gather DMA addr+len pairs after
* physical address coalescing is performed.
*/
unsigned short nr_phys_segments;
#if defined(CONFIG_BLK_DEV_INTEGRITY)
unsigned short nr_integrity_segments;
#endif
unsigned short ioprio;
int ref_count;
void *special; /* opaque pointer available for LLD use */
char *buffer; /* kaddr of the current segment if available */
int tag;
int errors;
/*
* when request is used as a packet command carrier
*/
unsigned char __cmd[BLK_MAX_CDB];
unsigned char *cmd;
unsigned short cmd_len;
unsigned int extra_len; /* length of alignment and padding */
unsigned int sense_len;
unsigned int resid_len; /* residual count */
void *sense;
unsigned long deadline;
struct list_head timeout_list;
unsigned int timeout;
int retries;
/*
* completion callback.
*/
rq_end_io_fn *end_io;
void *end_io_data;
/* for bidi */
struct request *next_rq;
};
- queuelist:用于将请求连接到请求队列上
- q:请求所属的请求队列
- cmd_flags:请求的标志
- cmd_type:请求的类型
- bio:该请求的多个bio中当前正被处理的bio
- biotail:该请求的最后一个bio。一个请求上的所有BIO会保存在一个链表中。
- __data_len:请求所涉及到的数据的总长度
- __sector:扇区游标
- elv:IO调度器相关信息。
- rq_disk:请求对应的磁盘
- part:请求所对应的磁盘分区
- end_io:该请求被完成时被调用,用于完成该请求
- end_io_data:回调end_io时的参数
请求所支持的标志及其含义如下:
enum rq_flag_bits {
/* common flags */
__REQ_WRITE, /* not set, read. set, write */
__REQ_FAILFAST_DEV, /* no driver retries of device errors */
__REQ_FAILFAST_TRANSPORT, /* no driver retries of transport errors */
__REQ_FAILFAST_DRIVER, /* no driver retries of driver errors */
__REQ_SYNC, /* request is sync (sync write or read) */
__REQ_META, /* metadata io request */
__REQ_PRIO, /* boost priority in cfq */
__REQ_DISCARD, /* request to discard sectors */
__REQ_SECURE, /* secure discard (used with __REQ_DISCARD) */
__REQ_NOIDLE, /* don't anticipate more IO after this one */
__REQ_FUA, /* forced unit access */
__REQ_FLUSH, /* request for cache flush */
/* bio only flags */
__REQ_RAHEAD, /* read ahead, can fail anytime */
__REQ_THROTTLED, /* This bio has already been subjected to
* throttling rules. Don't do it again. */
/* request only flags */
__REQ_SORTED, /* elevator knows about this request */
__REQ_SOFTBARRIER, /* may not be passed by ioscheduler */
__REQ_NOMERGE, /* don't touch this for merging */
__REQ_STARTED, /* drive already may have started this one */
__REQ_DONTPREP, /* don't call prep for this one */
__REQ_QUEUED, /* uses queueing */
__REQ_ELVPRIV, /* elevator private data attached */
__REQ_FAILED, /* set if the request failed */
__REQ_QUIET, /* don't worry about errors */
__REQ_PREEMPT, /* set for "ide_preempt" requests */
__REQ_ALLOCED, /* request came from our alloc pool */
__REQ_COPY_USER, /* contains copies of user pages */
__REQ_FLUSH_SEQ, /* request for flush sequence */
__REQ_IO_STAT, /* account I/O stat */
__REQ_MIXED_MERGE, /* merge of different types, fail separately */
__REQ_NR_BITS, /* stops here */
};
请求的类型及其含义如下:
enum rq_cmd_type_bits {
REQ_TYPE_FS = 1, /* fs request */
REQ_TYPE_BLOCK_PC, /* scsi command */
REQ_TYPE_SENSE, /* sense request */
REQ_TYPE_PM_SUSPEND, /* suspend request */
REQ_TYPE_PM_RESUME, /* resume request */
REQ_TYPE_PM_SHUTDOWN, /* shutdown request */
REQ_TYPE_SPECIAL, /* driver defined type */
/*
* for ATA/ATAPI devices. this really doesn't belong here, ide should
* use REQ_TYPE_SPECIAL and use rq->cmd[0] with the range of driver
* private REQ_LB opcodes to differentiate what type of request this is
*/
REQ_TYPE_ATA_TASKFILE,
REQ_TYPE_ATA_PC,
};
2.4 提交请求
当内核需要和设备进行交互时,它都会首先准备相关的bio,然后调用submit_bio将bio提交给设备。该函数最终会调用设备相关连的请求队列上的make_request_fn函数将BIO转变成一个请求,其处理逻辑很简单:- 更新统计信息
- 调用generic_make_request提交bio
- 做合法性检查
- 如果current->bio_list不为NULL,则将新的bio添加到current->bio_list上并返回
- 将current->bio_list 初始化为 &bio_list_on_stack
- 获取所请求设备的请求队列
- 调用请求队列上的make_request_fn产生一个请求
- 如果current->bio_list不为空,就回到第四步
- 将current->bio_list设置为NULL
如果没有修改过队列的make_request_fn,则它使用内核提供的默认版本blk_queue_bio。
blk_queue_bio的大致处理流程:
- 调用blk_queue_bounce进行一些特殊处理(如果底层驱动表示它想要将在某个限制之上的页地址回弹到低地址)
- 调用attempt_plug_merge尝试将新的请求同已经被plugged的请求进行合并,已经被plugged的请求会被保存在current->plug链表中
- 调用elv_merge判断新的bio是否可以同请求队列上已经存在的请求的bio进行合并,如果可以合并,就进行合并。这里的是否可以合并以及如何合并都取决于所采用的IO调度算法。
- 走到这一步就说明无法进行合并,开始创建一个新的请求,调用get_request_wait来获取一个新的请求结构
- 调用init_request_from_bio来使用bio中的数据来初始化这个新的请求。
- 如果current->plug不空,则表示当前队列是plug的,如果该链表上已经有足够数目的请求,则调用blk_flush_plug_list进行处理(该函数会调用__elv_add_request将 请求添加到请求队列,还可能调用queue_unplugged进行实际的请求处理),最后会将新的请求添加到current->plug上并更新统计信息。
- 如果current->plug为空,则调用__blk_run_queue直接处理请求,这会调用请求队列上的request_fn,也就是要求驱动必须提供的那个函数来进行请求的处理。
到了这一步,IO的读写已经被提交给了硬件,由驱动所提供的request_fn进行处理。