一、块设备概述

linux支持的两种重要的设备类型分别是字符设备和块设备,块设备可以随机地以固定大小的块传送数据。与字符设备相比,块设备有以下几个特殊之处:
  1. 块设备可以从数据的任何位置进行访问
  2. 块数据总是以固定长度进行传输,即便请求的这是一个字节
  3. 对块设备的访问有大量的缓存。当进行读时,如果已经缓存了,就直接使用缓存中的数据,而不再读设备,对于写也通过缓存来进行延迟处理。
在块系统中,数据块指的是固定大小的数据,该固定大小由内核规定,通常是4096个字节。与数据块对应的是扇区,它是由设备硬件所决定的一个块,其大小取决于硬件,常见的硬件的扇区大多都是512个字节。数据块的大小都是扇区大小的整数倍。
由于文件多存储在块设备上,而且现代操作系统使用虚拟内存来工作,为了实现虚拟内存就需要将数据在内存和存储设备(块设备)之间进行交换。因而高效的块驱动对系统性能是至关重要的,块驱动的框架要比字符设备复杂的多,其中引入了伪文件系统bdev,缓存,预读算法,IO调度器等,下图是一个简单的图示:

struct block_device被内核用来表示块设备,它是块设备的基础数据结构。系统中所有的块设备都被添加到了伪文件系统bdev中,虚拟文件系统VFS通过该文件系统来访问块设备。bdev使用的核心数据结构为:

struct bdev_inode {
	struct block_device bdev;
	struct inode vfs_inode;
};
由此可见,只要找到块设备在文件系统中的inode(它被保存在bdev_inode的vfs_inode域中),就可以找到该块设备对应的struct block_device,进而就可以对设备进行操作了。从VFS到块设备的关联关系就是通过伪文件系统bdev建立起来的。
块设备的设备文件的file_operations结构中提供的是通用的块读写函数,而不是驱动自己的读写。用户发出的设备读写是和缓冲区交互,也就是说用户发出的读写请求读写的是缓冲区。块框架负责(根据读写请求)完成缓冲区和物理设备之间的数据交换。提交到设备的操作都以请求的方式出现,所有的真实的设备的读写都在请求被处理时被执行,因而块设备驱动最主要的工作就是完成请求的处理。
块设备框架提供了复杂的队列功能用于对请求进行排队,调度。请求队列包含了一些基本的信息,比如:

  • 指向该队列调度器的指针,调度器用于管理该请求队列上的请求
  • 为该请求队列分配请求结构的内存池。该内存池用于为该设备分配读写请求结构。
  • 请求队列的设置信息。
而请求则包含了本请求的基本信息:
  • 比如该请求的gendisk和hd_struct
  • 该请求的bio信息
  • 请求完成时的回调函数指针 

二、注册块设备

类似于字符设备,对于块设备驱动来说,第一件事是向内核注册自己。其对应的API为
int register_blkdev(unsigned int major, const char *name);
  • major:该设备的主设备号,如果为0,则该函数会为该设备分配一个主设备号。
  • name:该设备的名字
如果注册时指定了主设备号,则成功返回0,否则返回负的错误码
如果注册时指定的主设备号为0,则成功会返回系统自动分配的主设备号,失败返回负的错误码。
如果不再使用设备,可以用以下API解除注册:
int unregister_blkdev(unsigned int major, const char *name);
参数和注册时的要匹配。
对于块设备来说,注册实际上就完成了两个动作:
  1. 如果没有指定主设备号,则分配一个
  2. 将该设备使用的设备号信息更新到数据库major_names中,major_names中的信息会出现在/proc/devices中
因此可以看出,注册做的事情实际上非常少。注册完成后,除了能够在/proce/devices中看到设备之外,不能对设备做任何事情,设备还无法使用。

三、块设备数据结构

将块设备注册到系统中并不能使得块设备可用,而且其中并没有涉及到我们提到的块设备的请求队列,调度器等内容。块设备的这些复杂特性是由大量复杂的数据结构来实现的。

3.1 块设备

块设备的核心数据结构是struct block_device,其定义如下:
struct block_device {
	dev_t			bd_dev;  /* not a kdev_t - it's a search key */
	int			bd_openers;
	struct inode *		bd_inode;	/* will die */
	struct super_block *	bd_super;
	struct mutex		bd_mutex;	/* open/close mutex */
	struct list_head	bd_inodes;
	void *			bd_claiming;
	void *			bd_holder;
	int			bd_holders;
	bool			bd_write_holder;
#ifdef CONFIG_SYSFS
	struct list_head	bd_holder_disks;
#endif
	struct block_device *	bd_contains;
	unsigned		bd_block_size;
	struct hd_struct *	bd_part;
	/* number of times partitions within this device have been opened. */
	unsigned		bd_part_count;
	int			bd_invalidated;
	struct gendisk *	bd_disk;
	struct request_queue *  bd_queue;
	struct list_head	bd_list;
	/*
	 * Private data.  You must have bd_claim'ed the block_device
	 * to use this.  NOTE:  bd_claim allows an owner to claim
	 * the same device multiple times, the owner must take special
	 * care to not mess up bd_private for that case.
	 */
	unsigned long		bd_private;


	/* The counter of freeze processes */
	int			bd_fsfreeze_count;
	/* Mutex for freeze */
	struct mutex		bd_fsfreeze_mutex;
};
  • bd_dev:设备号
  • bd_openers:设备打开次数
  • bd_inode:该设备的inode
  • bd_super:指向该设备所在文件系统的超级块,即bdev的超级块
  • bd_mutex:互斥锁
  • bd_inodes:链表头,该链表包含了表示该块设备的所有设备文件的inode
  • bd_claiming:申请获取设备者
  • bd_holder:当前持有设备者
  • bd_holders:设备有多少个持有者
  • bd_write_holder:是否是写持有
  • bd_contains:该设备所属的块设备,如果该设备就表示整个块设备,则它为NULL。
  • bd_block_size:该设备的块大小
  • bd_part:指向该块设备的hd_struct
  • bd_part_count:该块设备上的分区被引用的次数,如果不为0,则不能重新扫描分区,因为分区正被使用
  • bd_invalidated:该设备上分区是否有效,1表示无效,如果分区无效,则下次打开时会重新扫描分区表
  • bd_disk:指向该设备所对应的gendisk
  • bd_queue:该设备对应的请求队列
  • bd_list:用于将所有的块设备添加到all_bdevs中
  • bd_private:给设备的当前持有者使用的私有数据结构

3.2 通用磁盘和分区数据结构

struct block_device用于向驱动程序呈现一个块设备,而另外一个数据结构struct gendisk则表示整个磁盘,一个磁盘可能包括很多个struct block_device类型的块设备,其定义如下:
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, umode_t *mode);


	unsigned int events;		/* supported events */
	unsigned int async_events;	/* async events, subset of all */


	/* 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 __rcu *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_unchecked_t sync_io;	/* RAID */
	struct disk_events *ev;
#ifdef  CONFIG_BLK_DEV_INTEGRITY
	struct blk_integrity *integrity;
#endif
	int node_id;
};
  • major:磁盘的主设备号
  • first_minor:磁盘的第一个次设备号
  • minors:磁盘的最大次设备号数目,如果为1,则磁盘不能分区
  • disk_name:磁盘名字
  • devnode:获取设备的devnode
  • events:支持的事件
  • part_tbl:一个数组,包含了该磁盘的所有分区
  • part0:该磁盘的第0个分区。实际上它不代表真正的分区,它代表整个磁盘(或许可以称为主设备),分区数组的0号元素指向了它,当磁盘包含分区时,分区在分区数组中的下标从1开始。
  • fops:该磁盘的操作函数集
  • queue:用于队列管理
  • flags:磁盘的标志,表示磁盘的状态
  • private_data:磁盘的私有数据
  • driverfs_dev:表示该磁盘所属的device
  • slave_dir:用于在sys文件系统中创建一个该磁盘文件的slaves目录。
  • ev:用于检测磁盘的事件
  • node_id:该数据结构所使用的NUMA节点
分区数据结构struct hd_struct定义如下
struct hd_struct {
	sector_t start_sect;
	sector_t nr_sects;
	sector_t alignment_offset;
	unsigned int discard_alignment;
	struct device __dev;
	struct kobject *holder_dir;
	int policy, partno;
	struct partition_meta_info *info;
#ifdef CONFIG_FAIL_MAKE_REQUEST
	int make_it_fail;
#endif
	unsigned long stamp;
	atomic_t in_flight[2];
#ifdef	CONFIG_SMP
	struct disk_stats __percpu *dkstats;
#else
	struct disk_stats dkstats;
#endif
	atomic_t ref;
	struct rcu_head rcu_head;
};
其中关键数据域的含义如下:
  • start_sect:起始扇区号
  • nr_sects:该分区扇区数目。分区0的该域保存的是整个磁盘的扇区数目,也就是磁盘的容量。
  • __dev:该分区所对应的设备数据结构
  • holder_dir:指向分区所在的父设备的kobject
  • ref:分区的引用计数
struct gendisk的实例不能由驱动程序分配,必须使用API alloc_disk来分配,该API完成struct gendisk数据结构的分配和初始化,并调用设备模型的API device_initialize完成设备数据结构的初始化。在使用完后,必须使用del_gendisk来释放它。

3.3 块设备、通用磁盘以及分区数据结构之间的关系

这三者的关系如图所示


  • 对于一个磁盘其上的每一个分区都对应一个struct block_device数据结构,分区的struct block_device会通过bd_contains指向整个磁盘的struct block_device
  • 每一个磁盘只有唯一的一个struct gendisk结构
  • 磁盘上的所有的struct block_device都通过bd_disk指向表示磁盘的struct gendisk结构
  • 磁盘的struct gendisk的part指向一个struct hd_struct的指针数组,每个数组项对应一个分区的信息
  • 如果一个struct block_device表示的是分区,则它的bd_part指向对应的分区数据结构。
分区信息中的__dev以及holder_dir将磁盘的分层信息呈现到了kobject中。

3.4 块设备操作

字符设备使用了file_operations作为底层所使用的操作函数集,但是块设备不再使用该结构,块设备使用的数据结构是struct block_device_operations,其定义如下:
struct block_device_operations {
	int (*open) (struct block_device *, fmode_t);
	int (*release) (struct gendisk *, fmode_t);
	int (*ioctl) (struct block_device *, fmode_t, unsigned, unsigned long);
	int (*compat_ioctl) (struct block_device *, fmode_t, unsigned, unsigned long);
	int (*direct_access) (struct block_device *, sector_t,
						void **, unsigned long *);
	unsigned int (*check_events) (struct gendisk *disk,
				      unsigned int clearing);
	/* ->media_changed() is DEPRECATED, use ->check_events() instead */
	int (*media_changed) (struct gendisk *);
	void (*unlock_native_capacity) (struct gendisk *);
	int (*revalidate_disk) (struct gendisk *);
	int (*getgeo)(struct block_device *, struct hd_geometry *);
	/* this callback is with swap_lock and sometimes page table lock held */
	void (*swap_slot_free_notify) (struct block_device *, unsigned long);
	struct module *owner;
} __do_const;
其中特殊的api的含义如下
  • direct_access:直接访问设备
  • check_events:检查是否有事件发生
  • media_changed:当介质改变时调用,正如注释所说,建议不再使用更改API,而是使用check_events
  • unlock_native_capacity:解除本地的容量限制,用于支持超过EOD的访问(设备可以不提供该功能)。
  • revalidate_disk:使设备重新生效
  • getgeo:获取设备的物理信息
通用磁盘数据结构中包含了该数据结构的实例。

3.5 请求队列

请求队列使用数据结构struct request_queue来表示,其定义如下
struct request_queue {
	/*
	 * Together with queue_head for cacheline sharing
	 */
	struct list_head	queue_head;
	struct request		*last_merge;
	struct elevator_queue	*elevator;


	/*
	 * the queue request freelist, one for reads and one for writes
	 */
	struct request_list	rq;


	request_fn_proc		*request_fn;
	make_request_fn		*make_request_fn;
	prep_rq_fn		*prep_rq_fn;
	unprep_rq_fn		*unprep_rq_fn;
	merge_bvec_fn		*merge_bvec_fn;
	softirq_done_fn		*softirq_done_fn;
	rq_timed_out_fn		*rq_timed_out_fn;
	dma_drain_needed_fn	*dma_drain_needed;
	lld_busy_fn		*lld_busy_fn;


	/*
	 * Dispatch queue sorting
	 */
	sector_t		end_sector;
	struct request		*boundary_rq;


	/*
	 * Delayed queue handling
	 */
	struct delayed_work	delay_work;


	struct backing_dev_info	backing_dev_info;


	/*
	 * The queue owner gets to use this for whatever they like.
	 * ll_rw_blk doesn't touch it.
	 */
	void			*queuedata;


	/*
	 * various queue flags, see QUEUE_* below
	 */
	unsigned long		queue_flags;


	/*
	 * ida allocated id for this queue.  Used to index queues from
	 * ioctx.
	 */
	int			id;


	/*
	 * queue needs bounce pages for pages above this limit
	 */
	gfp_t			bounce_gfp;


	/*
	 * protects queue structures from reentrancy. ->__queue_lock should
	 * _never_ be used directly, it is queue private. always use
	 * ->queue_lock.
	 */
	spinlock_t		__queue_lock;
	spinlock_t		*queue_lock;


	/*
	 * queue kobject
	 */
	struct kobject kobj;


	/*
	 * queue settings
	 */
	unsigned long		nr_requests;	/* Max # of requests */
	unsigned int		nr_congestion_on;
	unsigned int		nr_congestion_off;
	unsigned int		nr_batching;


	unsigned int		dma_drain_size;
	void			*dma_drain_buffer;
	unsigned int		dma_pad_mask;
	unsigned int		dma_alignment;


	struct blk_queue_tag	*queue_tags;
	struct list_head	tag_busy_list;


	unsigned int		nr_sorted;
	unsigned int		in_flight[2];


	unsigned int		rq_timeout;
	struct timer_list	timeout;
	struct list_head	timeout_list;


	struct list_head	icq_list;


	struct queue_limits	limits;


	/*
	 * sg stuff
	 */
	unsigned int		sg_timeout;
	unsigned int		sg_reserved_size;
	int			node;
#ifdef CONFIG_BLK_DEV_IO_TRACE
	struct blk_trace	*blk_trace;
#endif
	/*
	 * for flush operations
	 */
	unsigned int		flush_flags;
	unsigned int		flush_not_queueable:1;
	unsigned int		flush_queue_delayed:1;
	unsigned int		flush_pending_idx:1;
	unsigned int		flush_running_idx:1;
	unsigned long		flush_pending_since;
	struct list_head	flush_queue[2];
	struct list_head	flush_data_in_flight;
	struct request		flush_rq;


	struct mutex		sysfs_lock;


#if defined(CONFIG_BLK_DEV_BSG)
	bsg_job_fn		*bsg_job_fn;
	int			bsg_job_size;
	struct bsg_class_device bsg_dev;
#endif


#ifdef CONFIG_BLK_DEV_THROTTLING
	/* Throttle data */
	struct throtl_data *td;
#endif
};
该数据结构比较庞大,其主要成员及其含义如下:
  • queue_head:将该队列上的请求连接到一起的链表。链表上的每个元素都是一个struct request类型的结构,代表一个读写请求。
  • elevator:指向该队列使用的调度算法。该调度算法用于对请求队列上的请求进行重排、优化以得到最好的性能。
  • rq:struct request的缓存,分配和释放struct request时都通过它进行
  • request_fn:请求处理函数。当内核期望驱动程序执行某些动作时,比如写数据到设备或者从设备读取数据时,内核会自动调用该函数。因此驱动程序必须提供该函数,它是块驱动框架和设备的接口。
  • make_request_fn:创建新请求。内核提供有该函数的默认版本,在默认版本中,内核会向请求队列添加请求,如果队列中有足够多的请求,则就调用request_fn来处理请求。如果不想使用内核提供的默认实现,驱动开发者就要自己实现(这是可能的,因为驱动开发者更了解自己的硬件是如何工作的)。blk_queue_make_request用于设置队列的创建新请求函数。
  • prep_rq_fn:请求预备函数。大多数驱动不适用该功能,而是将它设置为NULL。如果实现了该函数,则它的功能应该是在发出请求之前预先准备好一个请求。blk_queue_prep_rq用于设置队列的请求预备函数。
  • unprep_rq_fn:取消请求的准备,在请求被处理完成时可能会被调用。如果在请求预备函数中分配了一些资源,这是一个释放的好地方。blk_queue_unprep_rq用于设置请求队列的该函数。
  • merge_bvec_fn:用于确定一个现存的请求是否允许添加更多的数据。由于请求队列的长度是有限的,因而提供该检测可以在队列已满时用于检测是否可以往已存请求添加数据,如果可以,则就可以添加新的数据到已存请求中。blk_queue_merge_bvec用于设置请求队列的该函数
  • softirq_done_fn:当使用软中断异步完成请求时用于通知驱动程序请求已经完成
  • rq_timed_out_fn:当请求超时时执行的函数
  • dma_drain_needed:判断dma是否被耗光,如果是,则返回非0(fn which returns non-zero if drain is necessary)
  • lld_busy_fn:当设备忙时调用该函数
  • queue_flags:队列的状态标志
  • nr_requests:请求队列上可以添加的最大请求数目
  • queue_limits:包含了请求队列的各种限制,比如硬件的扇区大小
  • kobj:请求队列的kobject
该数据结构的其它一些域的含义参见注释及其名字。
API blk_init_queue用于初始化一个请求队列,它的原型如下:
struct request_queue *blk_init_queue(request_fn_proc *rfn, spinlock_t *lock);
  • rfn:驱动必须提供的请求处理函数。
  • lock:请求队列的自旋锁
该函数会完成
  • 请求队列的创建和一些域的初始化。其中队列的kobject的kobj_type被设置为blk_queue_ktype,最终队列会在/sys下的设备目录下创建一个子目录queue(在blk_register_queue中)
  • 初始化队列的请求处理函数的初始化,最主要的是将request_fn设置为驱动提供的函数
  • 将队列的make_request_fn设置为blk_queue_bio
  • 初始化队列的超时处理函数为blk_rq_timed_out_timer
blk_init_queue相当于先调用了blk_alloc_queue_node,后调用了blk_queue_make_request

GitHub 加速计划 / li / linux-dash
10.39 K
1.2 K
下载
A beautiful web dashboard for Linux
最近提交(Master分支:2 个月前 )
186a802e added ecosystem file for PM2 4 年前
5def40a3 Add host customization support for the NodeJS version 4 年前
Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐