块设备驱动之二

一、将块设备添加到系统

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中
    • 解除对磁盘事件的阻塞。
在使用alloc_disk分配分区数据结构时,该函数只创建了第一个分区的数据结构,磁盘分区表中也只包含了一个分区数据结构。
当扫描到一个分区时,需要将它添加到磁盘中,这是通过以下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事件
  • 初始化分区的引用计数
磁盘的事件处理函数为disk_events_workfn,它会检测磁盘是否有事件发生,并在有事件发生且需要处理时发送uevent事件到用户空间。disk_events_workfn是基于定时器实现的,如果磁盘支持check_events才会被启动。

二、块设备操作

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。该函数最主要的工作时完成块设备的打开动作,同时根据传入的模式还可能声明设备的持有者。
blkdev_get打开设备的动作由__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将所有分区设置为无效
  • 增加设备的打开计数
  • 解除对设备事件的阻塞
从打开的细节也可以看到,blkdev_open确实会调用驱动所提供的open函数,驱动可以在open中完成打开设备的必要工作。在打开之后设备就可以被使用了。 

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进行处理
blkdev_aio_write的原型为:
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时的参数
由这两个数据结构可以看出,由于块的读写请求是由设备异步完成的,因而都提供了一个用于通知IO完成的函数指针。每个请求还包含有调度器相关的信息,调度器决定了BIO如何被处理,BIO可能被调度器合并、重排以获得最优性能。
请求所支持的标志及其含义如下:
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转变成一个请求,其处理逻辑很简单:
  1. 更新统计信息
  2. 调用generic_make_request提交bio
generic_make_request的处理过程:
  1.  做合法性检查
  2.  如果current->bio_list不为NULL,则将新的bio添加到current->bio_list上并返回
  3.  将current->bio_list 初始化为 &bio_list_on_stack
  4.  获取所请求设备的请求队列
  5.  调用请求队列上的make_request_fn产生一个请求
  6.  如果current->bio_list不为空,就回到第四步
  7.  将current->bio_list设置为NULL
该函数通过将current->bio_list 视作一个标记保证了任意时刻只有一个make_request_fn在运行,新的generic_make_request请求所传递的bio会被添加到链表中在之后被转换成一个请求。
如果没有修改过队列的make_request_fn,则它使用内核提供的默认版本blk_queue_bio。
blk_queue_bio的大致处理流程:
  1.  调用blk_queue_bounce进行一些特殊处理(如果底层驱动表示它想要将在某个限制之上的页地址回弹到低地址)
  2.  调用attempt_plug_merge尝试将新的请求同已经被plugged的请求进行合并,已经被plugged的请求会被保存在current->plug链表中
  3.  调用elv_merge判断新的bio是否可以同请求队列上已经存在的请求的bio进行合并,如果可以合并,就进行合并。这里的是否可以合并以及如何合并都取决于所采用的IO调度算法。
  4.  走到这一步就说明无法进行合并,开始创建一个新的请求,调用get_request_wait来获取一个新的请求结构
  5.  调用init_request_from_bio来使用bio中的数据来初始化这个新的请求。
  6.  如果current->plug不空,则表示当前队列是plug的,如果该链表上已经有足够数目的请求,则调用blk_flush_plug_list进行处理(该函数会调用__elv_add_request将 请求添加到请求队列,还可能调用queue_unplugged进行实际的请求处理),最后会将新的请求添加到current->plug上并更新统计信息。
  7.  如果current->plug为空,则调用__blk_run_queue直接处理请求,这会调用请求队列上的request_fn,也就是要求驱动必须提供的那个函数来进行请求的处理。
从其处理逻辑可以看到,这里又对请求进行了一次缓冲,如果current->plug不空,则新的请求都会被添加到该链表上,只有请求的数目超过一定的值后才会被处理(被加入到请求队列中或者更进一步的被实际处理掉)。显然只有这里的处理逻辑是不完善的,假如系统不是很忙,只有很少量的请求需要被处理,则这里的处理条件可能很长时间都不能被满足,这时就需要另外一个机制来触发blk_flush_plug_list的动作,这个机制就是schedule,该机制的路径如下: schedule->sched_submit_work->blk_schedule_flush_plug->blk_flush_plug_list
到了这一步,IO的读写已经被提交给了硬件,由驱动所提供的request_fn进行处理。

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值