- 了解linux block driver
1.Block Registration
Block drivers, like char drivers, must use a set of registration interfaces to make their devices available to the kernel. The concepts are similar, but the details of block device registration are all different.
块设备驱动中注册函数是 register_blkdev(),其原型为:
269 * @major: the requested major device number [1…255]. If @major=0, try to
270 * allocate any unused major number.
271 * @name: the name of the new block device as a zero terminated string
int register_blkdev(unsigned int major, const char *name);
- The arguments are the major number that your device will be using and the associated name (which the kernel will display in /proc/devices). If major is passed as 0, the kernel allocates a new major number and returns it to the caller. As always, a negative return value from register_blkdev indicates that an error has occurred.
与register_blkdev()对应的注销函数是unregister_blkdev(),其原型为:
int unregister_blkdev(unsigned int major, const char *name);
- the arguments must match those passed to register_blkdev, or the function
returns -EINVAL and not unregister anything.
Linux 内核为块设备驱动维护了一个全局哈希表 major_names这个哈希表的 bucket 是 [0…255] 的整数索引的指向 blk_major_name 的结构指针数组。
static struct blk_major_name {
struct blk_major_name *next;
int major;
char name[16];
} *major_names[BLKDEV_MAJOR_HASH_SIZE];
而 register_blkdev 的 major 参数不为 0 时,其实现就尝试在这个哈希表中寻找指定的 major 对应的 bucket 里的空闲指针,分配一个新的 blk_major_name,按照指定参数初始化 major 和 name。 假如指定的 major 已经被别人占用(指针非空),则表示 major 号冲突,反回错误。
当 major 参数为 0 时,则由内核从 [1…255] 的整数范围内分配一个未使用的反回给调用者。因此,虽然 Linux 内核的主设备号 (Major Number) 是 12 位的,不指定 major 时,仍旧从 [1…255] 范围内分配。
例如:Sampleblk 驱动通过指定 major 为 0,让内核为其分配和注册一个未使用的主设备号,其代码如下,
sampleblk_major = register_blkdev(0, "sampleblk");
if (sampleblk_major < 0)
return sampleblk_major;
2.Disk Registration
While register_blkdev can be used to obtain a major number, it does not make any disk drives available to the system. There is a separate registration interface that you must use to manage individual drives. Using this interface requires familiarity with a pair of new structures, so that is where we start.
2.1.struct block_device_operations
include/linux/blkdev.h:
1294 struct block_device_operations {
1295 int (*open) (struct block_device *, fmode_t);
1296 int (*release) (struct gendisk *, fmode_t);
1297 int (*ioctl) (struct block_device *, fmode_t, unsigned, unsigned long);
1298 int (*compat_ioctl) (struct block_device *, fmode_t, unsigned, unsigned long);
1299 int (*direct_access) (struct block_device *, sector_t,
1300 void **, unsigned long *);
1301 unsigned int (*check_events) (struct gendisk *disk,
1302 unsigned int clearing);
1303 /* ->media_changed() is DEPRECATED, use ->check_events() instead */
1304 int (*media_changed) (struct gendisk *);
1305 void (*unlock_native_capacity) (struct gendisk *);
1306 int (*revalidate_disk) (struct gendisk *);
1307 int (*getgeo)(struct block_device *, struct hd_geometry *);
1308 /* this callback is with swap_lock and sometimes page table lock held */
1309 void (*swap_slot_free_notify) (struct block_device *, unsigned long);
1310 struct module *owner;
1311 };
块设备驱动可以通过定义这个操作函数表来实现对标准块设备驱动操作函数的定制。如果驱动没有实现这个操作表定义的方法,块设备层的代码也会按照块设备公共层的代码缺省的行为工作。例如定义如下:
static const struct block_device_operations sampleblk_fops = {
.owner = THIS_MODULE,
.open = sampleblk_open,
.release = sampleblk_release,
.ioctl = sampleblk_ioctl,
};
2.2.struct gendisk
155 struct gendisk {
156 /* major, first_minor and minors are input parameters only,
157 * don't use directly. Use disk_devt() and disk_max_parts().
158 */
159 int major; /* major number of driver */
160 int first_minor;
161 int minors; /* maximum number of minors, =1 for
162 * disks that can't be partitioned. */
163
164 char disk_name[DISK_NAME_LEN]; /* name of major driver */
165 char *(*devnode)(struct gendisk *gd, mode_t *mode);
166
167 unsigned int events; /* supported events */
168 unsigned int async_events; /* async events, subset of all */
169
170 /* Array of pointers to partitions indexed by partno.
171 * Protected with matching bdev lock but stat and other
172 * non-critical accesses use RCU. Always access through
173 * helpers.
174 */
175 struct disk_part_tbl __rcu *part_tbl;
176 struct hd_struct part0;
177
178 const struct block_device_operations *fops;
179 struct request_queue *queue;
180 void *private_data;
181
182 int flags;
183 struct device *driverfs_dev; // FIXME: remove
184 struct kobject *slave_dir;
185
186 struct timer_rand_state *random;
187 atomic_t sync_io; /* RAID */
188 struct disk_events *ev;
189 #ifdef CONFIG_BLK_DEV_INTEGRITY
190 struct blk_integrity *integrity;
191 #endif
192 int node_id;
193 };
磁盘创建和初始化:
Linux 内核使用 struct gendisk 来抽象和表示一个磁盘。也就是说,块设备驱动要支持正常的块设备操作,必需分配和初始化一个 struct gendisk。
- struct gendisk alloc_disk(int minors); / 分配gendisk */
- void del_gendisk(struct gendisk gd); / 删除gendisk */
- void add_disk(struct gendisk gd); / 增加gendisk */
1.首先,使用 alloc_disk 分配一个 struct gendisk,
disk = alloc_disk(minor);
if (!disk) {
rv = -ENOMEM;
goto fail_queue;
}
sampleblk_dev->disk = disk;
2.然后,初始化 struct gendisk 的重要成员,尤其是块设备操作函数表,Rquest Queue,和容量设置。最终调用 add_disk 来让磁盘在系统内可见,触发磁盘热插拔的 uevent。
disk->major = sampleblk_major;
disk->first_minor = minor;
disk->fops = &sampleblk_fops;
disk->private_data = sampleblk_dev;
disk->queue = sampleblk_dev->queue;
sprintf(disk->disk_name, "sampleblk%d", minor);
set_capacity(disk, sampleblk_nsects);
add_disk(disk);
2.3.Request Queue 初始化
从驱动模型的角度来说, 块设备主要分为两类:需要IO调度的和不需要IO调度的, 前者包括磁盘, 光盘等, 后者包括Flash, SD卡等, 为了保证模型的统一性 , Linux中对这两种使用同样的模型,但是通过不同的API来完成上述的初始化和绑定。
- 有IO调度类设备API
//初始化+绑定
struct request_queue *blk_init_queue(request_fn_proc *rfn, spinlock_t *lock)
- 无IO调度类设备API
//初始化
struct request_queue *blk_alloc_queue(gfp_t gfp_mask)
//绑定
void blk_queue_make_request(struct request_queue *q, make_request_fn *mfn)
- 共用API
//清除请求队列, 通常在卸载函数中使用
void blk_cleanup_queue(struct request_queue *q)
//从队列中去除请求
blkdev_dequeue_request()
//提取请求
struct request *blk_fetch_request(struct request_queue *q)
//从队列中去除请求
struct request *blk_peek_request(struct request_queue *q)
//启停请求队列, 当设备进入到不能处理请求队列的状态时,应通知通用块层
void blk_stop_queue(struct request_queue *q)
void blk_start_queue(struct request_queue *q)
使用 blk_init_queue 初始化 Request Queue 需要先声明一个所谓的策略 (Strategy) 回调和保护该 Request Queue 的自旋锁,然后将该策略回调的函数指针和自旋锁指针做为参数传递给该函数。
当执行 blk_init_queue 时,其内部实现会做如下的处理:
- 从内存中分配一个 struct request_queue 结构。
- 初始化 struct request_queue 结构。对调用者来说,其中以下部分的初始化格外重要:
- blk_init_queue 指定的策略函数指针会赋值给 struct request_queue 的 request_fn 成员。
- blk_init_queue 指定的自旋锁指针会赋值给 struct request_queue 的 queue_lock 成员。
- 与这个request_queue 关联的 IO 调度器的初始化。
Linux 内核提供多种分配和初始化 Request Queue 的方法:
- blk_mq_init_queue 主要用于使用多队列技术的块设备驱动
- blk_alloc_queue 和 blk_queue_make_request 主要用于绕开内核支持的 IO 调度器的合并和排序,使用自定义的实现。
- blk_init_queue 则使用内核支持的 IO 调度器,驱动只专注于策略函数的实现。
Note:
如果块设备驱动需要使用标准的 IO 调度器对 IO 请求进行合并或者排序时,必需使用 blk_init_queue 来分配和初始化 Request Queue.
3.策略函数实现
3.1 struct request_queue
struct request_queue {
struct list_head queue_head;
struct request *last_merge;
struct elevator_queue *elevator;
int nr_rqs[2]; /* # allocated [a]sync rqs */
int nr_rqs_elvpriv; /* # allocated rqs w/ elvpriv */
struct request_list root_rl;
request_fn_proc *request_fn;
make_request_fn *make_request_fn;
prep_rq_fn *prep_rq_fn;
unprep_rq_fn *unprep_rq_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;
struct blk_mq_ops *mq_ops;
unsigned int *mq_map;
/* sw queues */
struct blk_mq_ctx __percpu *queue_ctx;
unsigned int nr_queues;
/* hw dispatch queues */
struct blk_mq_hw_ctx **queue_hw_ctx;
unsigned int nr_hw_queues;
/*
* Dispatch queue sorting
*/
sector_t end_sector;
struct request *boundary_rq;
...
};
每一个gendisk对象都有一个request_queue对象,块设备有两种访问接口,一种是/dev下,一种是通过文件系统,后者经过IO调度在这个gendisk->request_queue上增加请求,最终回调与request_queue绑定的处理函数,将这些请求向下变成具体的硬件操作。
设备驱动待处理的 IO 请求队列结构。如果该队列是利用 blk_init_queue 分配和初始化的,则该队里内的 IO 请求( struct request )需要经过 IO 调度器的处理(排序或合并),由 blk_queue_bio 触发。
当块设备策略驱动函数被调用时,request 是通过其 queuelist 成员链接在 struct request_queue 的 queue_head 链表里的。 一个 IO 申请队列上会有很多个 request 结构。
3.2 struct bio
一个 bio 逻辑上代表了上层某个任务对通用块设备层发起的 IO 请求。来自不同应用,不同上下文的,不同线程的 IO 请求在块设备驱动层被封装成不同的 bio 数据结构。
同一个 bio 结构的数据是由块设备上从起始扇区开始的物理连续扇区组成的。由于在块设备上连续的物理扇区在内存中无法保证是物理内存连续的,因此才有了段 (Segment)的概念。 在 Segment 内部的块设备的扇区是物理内存连续的,但 Segment 之间却不能保证物理内存的连续性。Segment 长度不会超过内存页大小,而且总是扇区大小的整数倍。
扇区 (Sector),块 (Block) 和段 (Segment) 在内存页 (Page) 内部的布局:
因此,一个 Segment 可以用 [page, offset, len] 来唯一确定。一个 bio 结构可以包含多个 Segment。而 bio 结构通过指向 Segment 的指针数组来表示了这种一对多关系。
struct bio {
[...snipped..]
struct bio_vec *bi_io_vec; /* the actual vec list */
[...snipped..]
}
//描述一个 Segment 的数据结构
struct bio_vec {
struct page *bv_page; /* Segment 所在的物理页的 struct page 结构指针 */
unsigned int bv_len; /* Segment 长度,扇区整数倍 */
unsigned int bv_offset; /* Segment 在物理页内起始的偏移地址 */
};
在 struct bio 中,成员 bi_io_vec 就是前文所述的“指向 Segment 的指针数组” 的基地址,而每个数组的元素就是指向 struct bio_vec 的指针。在 struct bio 中的另一个成员 bi_vcnt 用来描述这个 bio 里有多少个 Segment,即指针数组的元素个数。一个 bio 最多包含的 Segment/Page 数是由如下内核宏定义决定的,
#define BIO_MAX_PAGES 256
多个 bio 结构可以通过成员 bi_next 链接成一个链表。bio 链表可以是某个做 IO 的任务 task_struct 成员 bio_list 所维护的一个链表。也可以是某个 struct request 所属的一个链表。
下图展现了 bio 结构通过 bi_next 链接组成的链表。其中的每个 bio 结构和 Segment/Page 存在一对多关系:
3.3.struct request
135 struct request {
136 struct list_head queuelist;
137 union {
138 struct __call_single_data csd;
139 u64 fifo_time;
140 };
141
142 struct request_queue *q;
143 struct blk_mq_ctx *mq_ctx;
144
145 int cpu;
146 unsigned int cmd_flags; /* op and common flags */
147 req_flags_t rq_flags;
148
149 int internal_tag;
150
151 unsigned long atomic_flags;
152
153 /* the following two fields are internal, NEVER access directly */
154 unsigned int __data_len; /* total data len */
155 int tag;
156 sector_t __sector; /* sector cursor */
157
158 struct bio *bio;
159 struct bio *biotail;
160 ...
161 }
一个 request 逻辑上代表了块设备驱动层收到的 IO 请求。该 IO 请求的数据在块设备上是从起始扇区开始的物理连续扇区组成的。
在 struct request 里可以包含很多个 struct bio,主要是通过 bio 结构的 bi_next 链接成一个链表。这个链表的第一个 bio 结构,则由 struct request 的 bio 成员指向。 而链表的尾部则由 biotail 成员指向。
通用块设备层接收到的来自不同线程的 bio 后,通常根据情况选择如下两种方案之一:
-
将 bio 合并入已有的 request
blk_queue_bio 会调用 IO 调度器做 IO 的合并 (merge)。多个 bio 可能因此被合并到同一个 request 结构里,组成一个 request 结构内部的 bio 结构链表。 由于每个 bio 结构都来自不同的任务,因此 IO 请求合并只能在 request 结构层面通过链表插入排序完成,原有的 bio 结构内部不会被修改。 -
分配新的 request
如果 bio 不能被合并到已有的 request 里,通用块设备层就会为这个 bio 构造一个新 request 然后插入到 IO 调度器内部的队列里。 待上层任务通过 blk_finish_plug 来触发 blk_run_queue 动作,块设备驱动的策略函数 request_fn 会触发 IO 调度器的排序操作,将 request 排序插入块设备驱动的 IO 请求队列。
不论以上哪种情况,通用块设备的代码将会调用块驱动程序注册在 request_queue 的 request_fn 回调,这个回调里最终会将合并或者排序后的 request 交由驱动的底层函数来做 IO 操作。
3.4.3.4 策略函数 request_fn
当块设备驱动使用 blk_run_queue 来分配和初始化 request_queue 时,这个函数也需要驱动指定自定义的策略函数 request_fn 和所需的自旋锁 queue_lock。 驱动实现自己的 request_fn 时,需要了解如下特点:
-
当通用块层代码调用 request_fn 时,内核已经拿了这个 request_queue 的 queue_lock。 因此,此时的上下文是 atomic 上下文。在驱动的策略函数退出 queue_lock 之前,需要遵守内核在 atomic 上下文的约束条件。
-
进入驱动策略函数时,通用块设备层代码可能会同时访问 request_queue。为了减少在 request_queue 的 queue_lock 上的锁竞争, 块驱动策略函数应该尽早退出 queue_lock,然后在策略函数返回前重新拿到锁。
-
策略函数是异步执行的,不处在用户态进程所对应的内核上下文。因此实现时不能假设策略函数运行在用户进程的内核上下文中。
4.块设备驱动框架
Linux内核中,用gendisk结构体表示一个磁盘设备或分区,块设备驱动程序的设计主要就是围绕gendisk这个数据结构展开的。一个简单的块设备驱动程序框架如下:
- 在init函数中分配、设置、添加一个gendisk;
- 设计gendisk结构体fops成员包含的操作函数;
- 设计gendisk结构体queue(请求队列)成员的请求处理函数。
Example:
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/interrupt.h>
#include <linux/mm.h>
#include <linux/fs.h>
#include <linux/kernel.h>
#include <linux/timer.h>
#include <linux/genhd.h>
#include <linux/hdreg.h>
#include <linux/ioport.h>
#include <linux/init.h>
#include <linux/wait.h>
#include <linux/blkdev.h>
#include <linux/blkpg.h>
#include <linux/delay.h>
#include <linux/io.h>
#include <asm/system.h>
#include <asm/uaccess.h>
#include <asm/dma.h>
static struct gendisk *ramblock_disk;
static request_queue_t *ramblock_queue;
static int major;
static DEFINE_SPINLOCK(ramblock_lock);
static struct block_device_operations ramblock_fops = {
.owner = THIS_MODULE,
};
#define RAMBLOCK_SIZE (1024*1024)
static void do_ramblock_request(request_queue_t * q)
{
static int cnt = 0;
printk("do_ramblock_request %d\n", ++cnt);
}
static int ramblock_init(void)
{
/* 1. 分配一个gendisk结构体 */
ramblock_disk = alloc_disk(16); /* 次设备号个数: 分区个数+1 */
/* 2. 设置 */
/* 2.1 分配/设置队列: 提供读写能力 */
ramblock_queue = blk_init_queue(do_ramblock_request, &ramblock_lock);
ramblock_disk->queue = ramblock_queue;
/* 2.2 设置其他属性: 比如容量 */
major = register_blkdev(0, "ramblock"); /* cat /proc/devices */
ramblock_disk->major = major;
ramblock_disk->first_minor = 0;
sprintf(ramblock_disk->disk_name, "ramblock");
ramblock_disk->fops = &ramblock_fops;
set_capacity(ramblock_disk, RAMBLOCK_SIZE / 512);
/* 3. 注册 */
add_disk(ramblock_disk);
return 0;
}
static void ramblock_exit(void)
{
unregister_blkdev(major, "ramblock");
del_gendisk(ramblock_disk);
put_disk(ramblock_disk);
blk_cleanup_queue(ramblock_queue);
}
module_init(ramblock_init);
module_exit(ramblock_exit);
5.创建文件系统
1).格式化分区
$ sudo mkfs.ext4 /dev/sampleblk1
2).挂载
~$ df
Filesystem 1K-blocks Used Available Use% Mounted on
udev 1895372 12 1895360 1% /dev
tmpfs 401272 1112 400160 1% /run
/dev/sda1 94823260 61374760 28608724 69% /
$sudo mount /dev/sampleblk1 /mnt //挂载
$ df -h | grep /mnt
/dev/sampleblk1 3.9M 34K 3.5M 1% /mnt
3).创建文件
$touch a
4).卸载
$sudo umount /dev/sampleblk1
refer to
- https://lwn.net/Articles/738449/
来源:CSDN
作者:Hacker_Albert
链接:https://blog.csdn.net/weixin_41028621/article/details/103755329