S5P6818_驱动篇(25)块设备驱动

块设备是针对存储设备的,比如 SD 卡、 EMMC、 NAND Flash、 Nor Flash、 SPI Flash、机械硬盘、固态硬盘等。因此块设备驱动其实就是这些存储设备驱动,块设备驱动相比字符设备驱动的主要区别如下:
①、块设备只能以块为单位进行读写访问,块是 linux 虚拟文件系统(VFS)基本的数据传输单位。字符设备是以字节为单位进行数据传输的,不需要缓冲。
②、块设备在结构上是可以进行随机访问的,对于这些设备的读写都是按块进行的,块设备使用缓冲区来暂时存放数据,等到条件成熟以后在一次性将缓冲区中的数据写入块设备中。

这么做的目的为了提高块设备寿命,大家如果仔细观察的话就会发现有些硬盘或者 NAND Flash就会标明擦除次数(flash 的特性,写之前要先擦除),比如擦除 100000 次等。因此,为了提高块设备寿命而引入了缓冲区,数据先写入到缓冲区中,等满足一定条件后再一次性写入到真正的物理存储设备中,这样就减少了对块设备的擦除次数,提高了块设备寿命。字符设备是顺序的数据流设备,字符设备是按照字节进行读写访问的。字符设备不需要缓冲区,对于字符设备的访问都是实时的,而且也不需要按照固定的块大小进行访问。块设备结构的不同其 I/O 算法也会不同,比如对于 EMMC、 SD 卡、 NAND Flash 这类没有任何机械设备的存储设备就可以任意读写任何的扇区(块设备物理存储单元)。但是对于机械硬盘这样带有磁头的设备,读取不同的盘面或者磁道里面的数据,磁头都需要进行移动,因此对于机械硬盘而言,将那些杂乱的访问按照一定的顺序进行排列可以有效提高磁盘性能, linux 里面针对不同的存储设备实现了不同的 I/O 调度算法。

块设备驱动框架

block_device 结构体

linux 内 核 使 用 block_device 表 示 块 设 备 , block_device 为 一 个 结 构 体,结构体内容如下:

struct block_device {
	sector_t		bd_start_sect;
	sector_t		bd_nr_sectors;
	struct disk_stats __percpu *bd_stats;
	unsigned long		bd_stamp;
	bool			bd_read_only;	/* read-only policy */
	dev_t			bd_dev;
	atomic_t		bd_openers;
	struct inode *		bd_inode;	/* will die */
	struct super_block *	bd_super;
	void *			bd_claiming;
	struct device		bd_device;
	void *			bd_holder;
	int			bd_holders;
	bool			bd_write_holder;
	struct kobject		*bd_holder_dir;
	u8			bd_partno;
	spinlock_t		bd_size_lock; /* for bd_inode->i_size updates */
	struct gendisk *	bd_disk;
	struct request_queue *	bd_queue;

	/* The counter of freeze processes */
	int			bd_fsfreeze_count;
	/* Mutex for freeze */
	struct mutex		bd_fsfreeze_mutex;
	struct super_block	*bd_fsfreeze_sb;

	struct partition_meta_info *bd_meta_info;
#ifdef CONFIG_FAIL_MAKE_REQUEST
	bool			bd_make_it_fail;
#endif
}

gendisk 结构体

linux 内核使用 gendisk 来描述一个磁盘设备,这是一个结构体,内容如下:

struct gendisk {
	/*
	 * major/first_minor/minors should not be set by any new driver, the
	 * block core will take care of allocating them automatically.
	 */
	int major;
	int first_minor;
	int minors;

	char disk_name[DISK_NAME_LEN];	/* name of major driver */

	unsigned short events;		/* supported events */
	unsigned short event_flags;	/* flags related to event processing */

	struct xarray part_tbl;
	struct block_device *part0;

	const struct block_device_operations *fops;
	struct request_queue *queue;
	void *private_data;

	struct bio_set bio_split;

	int flags;
	unsigned long state;
#define GD_NEED_PART_SCAN		0
#define GD_READ_ONLY			1
#define GD_DEAD				2
#define GD_NATIVE_CAPACITY		3
#define GD_ADDED			4
#define GD_SUPPRESS_PART_SCAN		5
#define GD_OWNS_QUEUE			6

	struct mutex open_mutex;	/* open/close mutex */
	unsigned open_partitions;	/* number of open partitions */

	struct backing_dev_info	*bdi;
	struct kobject *slave_dir;
#ifdef CONFIG_BLOCK_HOLDER_DEPRECATED
	struct list_head slave_bdevs;
#endif
	struct timer_rand_state *random;
	atomic_t sync_io;		/* RAID */
	struct disk_events *ev;
#ifdef  CONFIG_BLK_DEV_INTEGRITY
	struct kobject integrity_kobj;
#endif	/* CONFIG_BLK_DEV_INTEGRITY */

#ifdef CONFIG_BLK_DEV_ZONED
	/*
	 * Zoned block device information for request dispatch control.
	 * nr_zones is the total number of zones of the device. This is always
	 * 0 for regular block devices. conv_zones_bitmap is a bitmap of nr_zones
	 * bits which indicates if a zone is conventional (bit set) or
	 * sequential (bit clear). seq_zones_wlock is a bitmap of nr_zones
	 * bits which indicates if a zone is write locked, that is, if a write
	 * request targeting the zone was dispatched.
	 *
	 * Reads of this information must be protected with blk_queue_enter() /
	 * blk_queue_exit(). Modifying this information is only allowed while
	 * no requests are being processed. See also blk_mq_freeze_queue() and
	 * blk_mq_unfreeze_queue().
	 */
	unsigned int		nr_zones;
	unsigned int		max_open_zones;
	unsigned int		max_active_zones;
	unsigned long		*conv_zones_bitmap;
	unsigned long		*seq_zones_wlock;
#endif /* CONFIG_BLK_DEV_ZONED */

#if IS_ENABLED(CONFIG_CDROM)
	struct cdrom_device_info *cdi;
#endif
	int node_id;
	struct badblocks *bb;
	struct lockdep_map lockdep_map;
	u64 diskseq;

	/*
	 * Independent sector access ranges. This is always NULL for
	 * devices that do not have multiple independent access ranges.
	 */
	struct blk_independent_access_ranges *ia_ranges;
};

major 为磁盘设备的主设备号。
first_minor 为磁盘的第一个次设备号。
minors 为磁盘的次设备号数量,也就是磁盘的分区数量,这些分区的主设备号一样, 次设备号不同。
part_tbl 为磁盘对应的分区表,为结构体 disk_part_tbl 类型, disk_part_tbl 的核心是一个 hd_struct 结构体指针数组,此数组每一项都对应一个分区信息。
fops 为块设备操作集,为 block_device_operations 结构体类型。和字符设备操作集 file_operations 一样,是块设备驱动中的重点!
queue 为磁盘对应的请求队列,所有针对该磁盘设备的请求都放到此队列中,驱动程序需要处理此队列中的所有请求。

数据结构关系

在 Linux 内核中,块设备的管理涉及一组紧密关联的数据结构,它们共同描述了物理磁盘、分区、逻辑卷等存储实体及其操作。核心结构包括 block_device、gendisk、hd_struct和 request_queue。

1. struct block_device
代表什么?一个已打开的块设备或分区的实例。
关键作用:
    *   作为文件系统挂载、open()系统调用操作的主要对象。
    *   存储特定于该打开实例的信息(如挂载标志、访问模式)。
    *   指向其对应的 gendisk和分区信息 (hd_struct)。
数据结构关系:
    *   bd_disk:指向该设备/分区所属的物理磁盘的 gendisk结构。
    *   bd_part:指向描述该分区的 hd_struct结构。如果是整个磁盘(如 /dev/sda),此项为 NULL。
    *   bd_inode:关联的 VFS inode (通常在 /dev/ 下)。
    *   bd_contains:如果这是一个分区,指向包含它的整个磁盘的 block_device。如果是整个磁盘,指向自己。
    *   bd_partno:分区号(0 表示整个磁盘)。
示例:
    *   /dev/sda (整个磁盘):一个 block_device,bd_part = NULL, bd_partno = 0, bd_contains 指向自己。
    *   /dev/sda1 (分区):一个 block_device,bd_part 指向描述 sda1 的 hd_struct, bd_partno = 1, bd_contains 指向 /dev/sda 的 block_device。

2. struct gendisk (Generic Disk)
代表什么?整个物理磁盘(或逻辑上等同于磁盘的实体,如 LVM 卷、RAID 设备)的元数据和操作接口。
关键作用:
    *   内核表示一个物理存储单元的核心结构。
    *   存储独立于打开实例的磁盘全局信息(容量、分区表、操作函数)。
    *   管理该磁盘上的分区 (hd_struct 数组)。
数据结构关系:
    *   major, first_minor:设备的主、次设备号范围。
    *   disk_name:磁盘名称 (e.g., sda, vda)。
    *   part_tbl:指向 struct disk_part_tbl的指针,该结构包含一个 hd_struct 指针数组 (part),描述该磁盘上的所有分区(索引 0 通常代表整个磁盘)。
    *   fops:指向 struct block_device_operations,包含设备特定的操作 (open, release, ioctl, media change 等)。
    *   queue:指向关联的 request_queue(I/O 请求队列)。
    *   private_data:驱动私有数据指针。
生命周期:在驱动探测时创建 (alloc_disk),移除时销毁 (del_gendisk)。

3. struct hd_struct
代表什么?描述磁盘上的一个分区(或整个磁盘本身作为分区 0)。
关键作用:
    *   存储分区静态元数据:起始扇区 (start_sect)、大小 (nr_sects)、分区号 (partno)、分区类型/标志。
    *   提供分区的引用计数 (refcnt)。
数据结构关系:
    *   嵌入在 gendisk->part_tbl->part[]数组中。
    *   被 block_device->bd_part引用。
示例:对于 /dev/sda:
    *   part[0]:代表整个磁盘 /dev/sda (partno = 0, start_sect = 0, nr_sects = 磁盘总大小)。
    *   part[1]:代表分区 /dev/sda1 (partno = 1, start_sect = 分区起始扇区, nr_sects = 分区大小)。
    *   … 以此类推。

4. struct request_queue
代表什么?管理针对某个块设备(通常是 gendisk 代表的整个磁盘)的 I/O 请求队列。
关键作用:
    *   接收来自文件系统、VM 等的 I/O 请求 (`struct request`)。
    *   实现 I/O 调度算法 (CFQ, Deadline, NOOP, mq-deadline)。
    *   包含块设备物理特性 (最大扇区大小、硬件扇区大小、对齐要求)。
    *   包含队列操作函数 (请求创建、合并、派发到驱动 ->request_fn 或通过 blk-mq)。
数据结构关系:
    *   gendisk->queue 指向它。
    *   驱动初始化时创建 (blk_init_queue 或 blk_mq_init_queue)。

总结:数据结构关系图

1.  gendisk 是核心:它代表物理磁盘(或逻辑磁盘实体),包含分区表和指向请求队列的指针。一个 gendisk 对应一个物理设备。
2.  block_device是访问接口:每次打开 /dev/sda, /dev/sda1 等都会创建一个 block_device 实例。它关联到对应的 gendisk 和具体的分区 (hd_struct)。
3.  hd_struct 描述分区:存储在 gendisk 的part_tbl中,包含分区的物理布局信息。分区号 0 (part[0]) 代表整个磁盘。
4.  request_queue 管理 I/O:与 gendisk 关联,处理针对该磁盘的所有 I/O 请求。分区设备的 I/O 最终也会路由到其所属磁盘的队列。
5.  分区 vs 设备:
    *   整个设备 (e.g., /dev/sda): 一个 block_device (.bd_part = NULL, .bd_partno = 0) -> 指向一个 gendisk -> gendisk->part_tbl->part[0] 描述整个磁盘。
    *   分区 (e.g., /dev/sda1):一个 block_device (.bd_part 指向 hd_struct for part1, .bd_partno = 1) -> 指向同一个 gendisk (sda) -> gendisk->part_tbl->part[1] 描述分区 1。.bd_contains 指向 /dev/sda 的 block_device。

操作函数

编写块设备驱动的时候需要分配并初始化一个 gendisk, linux 内核提供了一组 gendisk 操作函数,我们来看一下一些常用的 API 函数。

1、 申请 gendisk
使用 gendisk 之前要先申请, allo_disk 函数用于申请一个 gendisk,函数原型如下:

struct gendisk *alloc_disk(int minors)

函数参数和返回值含义如下:
minors: 次设备号数量, 也就是 gendisk 对应的分区数量。
返回值: 成功:返回申请到的 gendisk,失败: NULL。

2、删除 gendisk
如果要删除 gendisk 的话可以使用函数 del_gendisk,函数原型如下:

void del_gendisk(struct gendisk *gp)

函数参数和返回值含义如下:
gp: 要删除的 gendisk。
返回值: 无。

3、将 gendisk 添加到内核
使用 alloc_disk 申请到 gendisk 以后系统还不能使用,必须使用 add_disk 函数将申请到的gendisk 添加到内核中, add_disk 函数原型如下:

void add_disk(struct gendisk *disk)

函数参数和返回值含义如下:
disk: 要添加到内核的 gendisk。
返回值: 无。

4、设置 gendisk 容量
每一个磁盘都有容量,所以在初始化 gendisk 的时候也需要设置其容量,使用函数set_capacity,函数原型如下:

void set_capacity(struct gendisk *disk, sector_t size)

函数参数和返回值含义如下:
disk: 要设置容量的 gendisk。
size: 磁盘容量大小,注意这里是扇区数量。块设备中最小的可寻址单元是扇区,一个扇区一般是 512 字节,有些设备的物理扇区可能不是 512 字节。不管物理扇区是多少,内核和块设备驱动之间的扇区都是 512 字节。所以 set_capacity 函数设置的大小就是块设备实际容量除以512 字节得到的扇区数量。比如一个 2MB 的磁盘,其扇区数量就是(2*1024*1024)/512=4096。
返回值: 无。

5、调整 gendisk 引用计数
内核会通过 get_disk 和 put_disk 这两个函数来调整 gendisk 的引用计数,根据名字就可以知道, get_disk 是增加 gendisk 的引用计数, put_disk 是减少 gendisk 的引用计数,这两个函数原型如下所示:

struct kobject *get_disk(struct gendisk *disk)
void put_disk(struct gendisk *disk)

block_device_operations 结构体和字符设备的 file _operations 一样,块设备也有操作集,为结构体 block_device_operations,此结构体定义在 include/linux/blkdev.h 中,结构体内容如下:

struct block_device_operations {
	void (*submit_bio)(struct bio *bio);
	int (*poll_bio)(struct bio *bio, struct io_comp_batch *iob,
			unsigned int flags);
	int (*open) (struct block_device *, fmode_t);
	void (*release) (struct gendisk *, fmode_t);
	int (*rw_page)(struct block_device *, sector_t, struct page *, enum req_op);
	int (*ioctl) (struct block_device *, fmode_t, unsigned, unsigned long);
	int (*compat_ioctl) (struct block_device *, fmode_t, unsigned, unsigned long);
	unsigned int (*check_events) (struct gendisk *disk,
				      unsigned int clearing);
	void (*unlock_native_capacity) (struct gendisk *);
	int (*getgeo)(struct block_device *, struct hd_geometry *);
	int (*set_read_only)(struct block_device *bdev, bool ro);
	void (*free_disk)(struct gendisk *disk);
	/* this callback is with swap_lock and sometimes page table lock held */
	void (*swap_slot_free_notify) (struct block_device *, unsigned long);
	int (*report_zones)(struct gendisk *, sector_t sector,
			unsigned int nr_zones, report_zones_cb cb, void *data);
	char *(*devnode)(struct gendisk *disk, umode_t *mode);
	/* returns the length of the identifier or a negative errno: */
	int (*get_unique_id)(struct gendisk *disk, u8 id[16],
			enum blk_unique_id id_type);
	struct module *owner;
	const struct pr_ops *pr_ops;

	/*
	 * Special callback for probing GPT entry at a given sector.
	 * Needed by Android devices, used by GPT scanner and MMC blk
	 * driver.
	 */
	int (*alternative_gpt_sector)(struct gendisk *disk, sector_t *sector);
};

open 函数用于打开指定的块设备。
release 函数用于关闭(释放)指定的块设备。
rw_page 函数用于读写指定的页。
ioctl 函数用于块设备的 I/O 控制。
compat_ioctl 函数和 ioctl 函数一样,都是用于块设备的 I/O 控制。
getgeo 函数用于获取磁盘信息,包括磁头、柱面和扇区等信息。
owner 表示此结构体属于哪个模块,一般直接设置为 THIS_MODULE。

注册块设备

和字符设备驱动一样,我们需要向内核注册新的块设备、申请设备号,块设备注册函数为register_blkdev,函数原型如下:

int register_blkdev(unsigned int major, const char *name)

函数参数和返回值含义如下:
major: 主设备号。
name: 块设备名字。
返回值: 如果参数 major 在 1~255 之间的话表示自定义主设备号,那么返回 0 表示注册成功,如果返回负值的话表示注册失败。如果 major 为 0 的话表示由系统自动分配主设备号,那么返回值就是系统分配的主设备号(1~255),如果返回负值那就表示注册失败。

注销块设备

和字符设备驱动一样,如果不使用某个块设备了,那么就需要注销掉,函数为unregister_blkdev,函数原型如下:

void unregister_blkdev(unsigned int major, const char *name)

函数参数和返回值含义如下:
major: 要注销的块设备主设备号。
name: 要注销的块设备名字。
返回值: 无。

块设备 I/O 请求过程

大家如果仔细观察的话会在 block_device_operations 结构体中并没有找到 read 和 write 这样的读写函数,那么块设备是怎么从物理块设备中读写数据?这里就引出了块设备驱动中非常重要的 request_queue、 request 和 bio。

1、 请求队列 request_queue
内核将对块设备的读写都发送到请求队列 request_queue 中, request_queue 中是大量的request(请求结构体),而 request 又包含了 bio, bio 保存了读写相关数据,比如从块设备的哪个地址开始读取、读取的数据长度,读取到哪里,如果是写的话还包括要写入的数据等。我们先来看一下 request_queue,这是一个结构体,定义在文件 include/linux/blkdev.h 中:

struct request_queue {
	struct request		*last_merge;
	struct elevator_queue	*elevator;

	struct percpu_ref	q_usage_counter;

	struct blk_queue_stats	*stats;
	struct rq_qos		*rq_qos;

	const struct blk_mq_ops	*mq_ops;

	/* sw queues */
	struct blk_mq_ctx __percpu	*queue_ctx;

	unsigned int		queue_depth;

	/* hw dispatch queues */
	struct xarray		hctx_table;
	unsigned int		nr_hw_queues;

	/*
	 * 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;
	/*
	 * Number of contexts that have called blk_set_pm_only(). If this
	 * counter is above zero then only RQF_PM requests are processed.
	 */
	atomic_t		pm_only;

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

	spinlock_t		queue_lock;

	struct gendisk		*disk;

	/*
	 * queue kobject
	 */
	struct kobject kobj;

	/*
	 * mq queue kobject
	 */
	struct kobject *mq_kobj;

#ifdef  CONFIG_BLK_DEV_INTEGRITY
	struct blk_integrity integrity;
#endif	/* CONFIG_BLK_DEV_INTEGRITY */

#ifdef CONFIG_PM
	struct device		*dev;
	enum rpm_status		rpm_status;
#endif

	/*
	 * queue settings
	 */
	unsigned long		nr_requests;	/* Max # of requests */

	unsigned int		dma_pad_mask;

#ifdef CONFIG_BLK_INLINE_ENCRYPTION
	struct blk_crypto_profile *crypto_profile;
	struct kobject *crypto_kobject;
#endif

	unsigned int		rq_timeout;
	int			poll_nsec;

	struct blk_stat_callback	*poll_cb;
	struct blk_rq_stat	*poll_stat;

	struct timer_list	timeout;
	struct work_struct	timeout_work;

	atomic_t		nr_active_requests_shared_tags;

	struct blk_mq_tags	*sched_shared_tags;

	struct list_head	icq_list;
#ifdef CONFIG_BLK_CGROUP
	DECLARE_BITMAP		(blkcg_pols, BLKCG_MAX_POLS);
	struct blkcg_gq		*root_blkg;
	struct list_head	blkg_list;
#endif

	struct queue_limits	limits;

	unsigned int		required_elevator_features;

	int			node;
#ifdef CONFIG_BLK_DEV_IO_TRACE
	struct blk_trace __rcu	*blk_trace;
#endif
	/*
	 * for flush operations
	 */
	struct blk_flush_queue	*fq;

	struct list_head	requeue_list;
	spinlock_t		requeue_lock;
	struct delayed_work	requeue_work;

	struct mutex		sysfs_lock;
	struct mutex		sysfs_dir_lock;

	/*
	 * for reusing dead hctx instance in case of updating
	 * nr_hw_queues
	 */
	struct list_head	unused_hctx_list;
	spinlock_t		unused_hctx_lock;

	int			mq_freeze_depth;

#ifdef CONFIG_BLK_DEV_THROTTLING
	/* Throttle data */
	struct throtl_data *td;
#endif
	struct rcu_head		rcu_head;
	wait_queue_head_t	mq_freeze_wq;
	/*
	 * Protect concurrent access to q_usage_counter by
	 * percpu_ref_kill() and percpu_ref_reinit().
	 */
	struct mutex		mq_freeze_lock;

	int			quiesce_depth;

	struct blk_mq_tag_set	*tag_set;
	struct list_head	tag_set_list;

	struct dentry		*debugfs_dir;
	struct dentry		*sched_debugfs_dir;
	struct dentry		*rqos_debugfs_dir;
	/*
	 * Serializes all debugfs metadata operations using the above dentries.
	 */
	struct mutex		debugfs_mutex;

	bool			mq_sysfs_init_done;

	/**
	 * @srcu: Sleepable RCU. Use as lock when type of the request queue
	 * is blocking (BLK_MQ_F_BLOCKING). Must be the last member
	 */
	struct srcu_struct	srcu[];
};

大家回过头看一下 gendisk 结构体就会发现里面有一个 request_queue 结构体指针类型成员变量 queue,也就说在编写块设备驱动的时候,每个磁盘(gendisk)都要分配一个 request_queue。
1、初始化请求队列
我们首先需要申请并初始化一个 request_queue,然后在初始化 gendisk 的时候将这个request_queue 地址赋值给 gendisk 的 queue 成员变量。使用 blk_init_queue 函数来完成request_queue 的申请与初始化,函数原型如下:

request_queue *blk_init_queue(request_fn_proc *rfn, spinlock_t *lock)

函数参数和返回值含义如下:
rfn: 请求处理函数指针,每个 request_queue 都要有一个请求处理函数,请求处理函数request_fn_proc 原型如下:

void (request_fn_proc) (struct request_queue *q)

请求处理函数需要驱动编写人员自行实现。
lock: 自旋锁指针,需要驱动编写人员定义一个自旋锁,然后传递进来。,请求队列会使用这个自旋锁。
返回值: 如果为 NULL 的话表示失败,成功的话就返回申请到的 request_queue 地址。

2、删除请求队列
当卸载块设备驱动的时候我们还需要删除掉前面申请到的 request_queue,删除请求队列使用函数 blk_cleanup_queue,函数原型如下:

void blk_cleanup_queue(struct request_queue *q)

函数参数和返回值含义如下:
q: 需要删除的请求队列。
返回值: 无。

3、分配请求队列并绑定制造请求函数
blk_init_queue 函数完成了请求队列的申请已经请求处理函数的绑定,这个一般用于像机械硬盘这样的存储设备,需要 I/O 调度器来优化数据读写过程。但是对于 EMMC、 SD 卡这样的非机械设备,可以进行完全随机访问,所以就不需要复杂的 I/O 调度器了。对于非机械设备我们可以先申请 request_queue,然后将申请到的 request_queue 与“制造请求”函数绑定在一起。先来看一下 request_queue 申请函数 blk_alloc_queue,函数原型如下:

struct request_queue *blk_alloc_queue(gfp_t gfp_mask)

函数参数和返回值含义如下:
gfp_mask: 内存分配掩码,具体可选择的掩码值请参考 include/linux/gfp.h 中的相关宏定义,一般为 GFP_KERNEL。
返回值: 申请到的无 I/O 调度的 request_queue。
我们需要为 blk_alloc_queue 函数申请到的请求队列绑定一个“制造请求”函数。这里我们需要用到函数 blk_queue_make_request,函数原型如下:

void blk_queue_make_request(struct request_queue *q, make_request_fn *mfn)

函数参数和返回值含义如下:
q: 需要绑定的请求队列,也就是 blk_alloc_queue 申请到的请求队列。
mfn:需要绑定的“制造”请求函数,函数原型如下:

void (make_request_fn) (struct request_queue *q, struct bio *bio)

“制造请求”函数需要驱动编写人员实现。
返回值: 无。
一般 blk_alloc_queue 和 blk_queue_make_request 是搭配在一起使用的,用于非机械的存储设备、无需 I/O 调度器,比如 EMMC、 SD 卡等。 blk_init_queue 函数会给请求队列分配一个 I/O 调度器,用于机械存储设备,比如机械硬盘等。

请求 request
请求队列(request_queue)里面包含的就是一系列的请求(request), request 里面有一个名为“bio”的成员变量,类型为 bio 结构体指针。前面说了,真正的数据就保存在 bio 里面,所以我们需要从 request_queue 中取出一个一个的 request,然后再从每个 request 里面取出 bio,最后根据 bio 的描述讲数据写入到块设备,或者从块设备中读取数据。

1、 获取请求
我们需要从request_queue中依次获取每个request,使用blk_peek_request函数完成此操作,函数原型如下:

request *blk_peek_request(struct request_queue *q)

函数参数和返回值含义如下:
q: 指定 request_queue。
返回值: request_queue 中下一个要处理的请求(request),如果没有要处理的请求就返回NULL。

2、开启请求
使用 blk_peek_request 函数获取到下一个要处理的请求以后就要开始处理这个请求,这里要用到 blk_start_request 函数,函数原型如下:

void blk_start_request(struct request *req)

函数参数和返回值含义如下:
req: 要开始处理的请求。
返回值: 无。
③、一步到位处理请求
我们也可以使用 blk_fetch_request 函数来一次性完成请求的获取和开启, blk_fetch_request函数很简单,内容如下:

struct request *blk_fetch_request(struct request_queue *q)
{
    struct request *rq;

    rq = blk_peek_request(q);
    if (rq)
        blk_start_request(rq);
    return rq;
}

可以看出, blk_fetch_request 就是直接调用了 blk_peek_request 和 blk_start_request 这两个函数。

4、其他和请求有关的函数
关于请求的 API 还有很多:

函数 描述
blk_end_request() 请求中指定字节数据被处理完成。
blk_end_request_all() 请求中所有数据全部处理完成。
blk_end_request_cur() 当前请求中的 chunk。
blk_end_request_err() 处理完请求,直到下一个错误产生。
__blk_end_request() 和 blk_end_request 函数一样,但是需要持有队列锁。
__blk_end_request_all() 和 blk_end_request_all 函数一样,但是需要持有队列锁。
__blk_end_request_cur() 和 blk_end_request_cur 函数一样,但是需要持有队列锁。
__blk_end_request_err() 和 blk_end_request_err 函数一样,但是需要持有队列锁。

bio 结构
每个 request 里面里面会有多个 bio, bio 保存着最终要读写的数据、地址等信息。上层应用程序对于块设备的读写会被构造成一个或多个 bio 结构, bio 结构描述了要读写的起始扇区、要读写的扇区数量、是读取还是写入、页偏移、数据长度等等信息。上层会将 bio 提交给 I/O 调度器, I/O 调度器会将这些 bio 构造成 request 结构,而一个物理存储设备对应一个 request_queue,request_queue 里面顺序存放着一系列的 request。新产生的 bio 可能被合并到 request_queue 里现有的 request 中,也可能产生新的 request,然后插入到 request_queue 中合适的位置,这一切都
是由 I/O 调度器来完成的。 request_queue、 request 和 bio 之间的关系如图所示:

bio 是个结构体,定义在 include/linux/blk_types.h 中,结构体内容如下:

struct bio {
	struct bio		*bi_next;	/* request queue link */
	struct block_device	*bi_bdev;
	blk_opf_t		bi_opf;		/* bottom bits REQ_OP, top bits
						 * req_flags.
						 */
	unsigned short		bi_flags;	/* BIO_* below */
	unsigned short		bi_ioprio;
	blk_status_t		bi_status;
	atomic_t		__bi_remaining;

	struct bvec_iter	bi_iter;

	blk_qc_t		bi_cookie;
	bio_end_io_t		*bi_end_io;
	void			*bi_private;
#ifdef CONFIG_BLK_CGROUP
	/*
	 * Represents the association of the css and request_queue for the bio.
	 * If a bio goes direct to device, it will not have a blkg as it will
	 * not have a request_queue associated with it.  The reference is put
	 * on release of the bio.
	 */
	struct blkcg_gq		*bi_blkg;
	struct bio_issue	bi_issue;
#ifdef CONFIG_BLK_CGROUP_IOCOST
	u64			bi_iocost_cost;
#endif
#endif

#ifdef CONFIG_BLK_INLINE_ENCRYPTION
	struct bio_crypt_ctx	*bi_crypt_context;
#endif

	union {
#if defined(CONFIG_BLK_DEV_INTEGRITY)
		struct bio_integrity_payload *bi_integrity; /* data integrity */
#endif
	};

	unsigned short		bi_vcnt;	/* how many bio_vec's */

	/*
	 * Everything starting with bi_max_vecs will be preserved by bio_reset()
	 */

	unsigned short		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 */

	struct bio_set		*bi_pool;

	/*
	 * 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[];
};

bvec_iter 结构体类型的成员变量,bio_vec 结构体指针类型的成员变量。
bvec_iter 结构体描述了要操作的设备扇区等信息,结构体内容如下:

struct bvec_iter {
    sector_t bi_sector; /* I/O 请求的设备起始扇区(512 字节) */
    unsigned int bi_size; /* 剩余的 I/O 数量 */
    unsigned int bi_idx; /* blv_vec 中当前索引 */
    unsigned int bi_bvec_done; /* 当前 bvec 中已经处理完成的字节数 */
};

bio_vec 结构体描述了内容如下:

struct bio_vec {
    struct page *bv_page; /* 页 */
    unsigned int bv_len; /* 长度 */
    unsigned int bv_offset; /* 偏移 */
};

可以看出 bio_vec 就是“page,offset,len”组合, page 指定了所在的物理页, offset 表示所处页的偏移地址, len 就是数据长度。
我们对于物理存储设备的操作不外乎就是将 RAM 中的数据写入到物理存储设备中,或者将物理设备中的数据读取到 RAM 中去处理。数据传输三个要求:数据源、数据长度以及数据目的地,也就是你要从物理存储设备的哪个地址开始读取、读取到 RAM 中的哪个地址处、读取的数据长度是多少。既然 bio 是块设备最小的数据传输单元,那么 bio 就有必要描述清楚这些信息,其中 bi_iter 这个结构体成员变量就用于描述物理存储设备地址信息,比如要操作的扇区地址。 bi_io_vec 指向 bio_vec 数组首地址, bio_vec 数组就是 RAM 信息,比如页地址、页偏移以及长度,“页地址”是 linux 内核里面内存管理相关的概念,这里我们不深究 linux 内存管理,我们只需要知道对于 RAM 的操作最终会转换为页相关操作。bio、 bvec_iter 以及 bio_vec 这三个机构体之间的关系如图所示:
图片[1] - S5P6818_驱动篇(25)块设备驱动 - 宋马
1、遍历请求中的 bio
前面说了,请求中包含有大量的 bio,因此就涉及到遍历请求中所有 bio 并进行处理。遍历请求中的 bio 使用函数__rq_for_each_bio,这是一个宏,内容如下:

#define __rq_for_each_bio(_bio, rq) 
    if ((rq->bio)) 
        for (_bio = (rq)->bio; _bio; _bio = _bio->bi_next)

_bio 就是遍历出来的每个 bio, rq 是要进行遍历操作的请求, _bio 参数为 bio 结构体指针类型, rq 参数为 request 结构体指针类型。

2、遍历 bio 中的所有段
bio 包含了最终要操作的数据,因此还需要遍历 bio 中的所有段,这里要用到bio_for_each_segment 函数,此函数也是一个宏,内容如下:

#define bio_for_each_segment(bvl, bio, iter) 
    __bio_for_each_segment(bvl, bio, iter, (bio)->bi_iter)

第一个 bvl 参数就是遍历出来的每个 bio_vec,第二个 bio 参数就是要遍历的 bio,类型为bio 结构体指针,第三个 iter 参数保存要遍历的 bio 中 bi_iter 成员变量。

3、通知 bio 处理结束
如果使用“制造请求”,也就是抛开 I/O 调度器直接处理 bio 的话,在 bio 处理完成以后要通过内核 bio 处理完成,使用 bio_endio 函数,函数原型如下:

bvoid bio_endio(struct bio *bio, int error)

函数参数和返回值含义如下:
bio: 要结束的 bio。
error: 如果 bio 处理成功的话就直接填 0,如果失败的话就填个负值,比如-EIO。
返回值: 无

© 版权声明
THE END
如果内容对您有所帮助,就支持一下吧!
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容