I.MX6ULL_Linux_驱动篇(54)linux 块设备驱动

前面我们都是在学习字符设备驱动,本章我们来学习一下块设备驱动框架,块设备驱动是Linux 三大驱动类型之一。块设备驱动要远比字符设备驱动复杂得多,不同类型的存储设备又对应不同的驱动子系统,本章我们重点学习一下块设备相关驱动概念,不涉及到具体的存储设备。最后,我们使用开发板板载 RAM 模拟一个块设备,学习块设备驱动框架的使用。

什么是块设备?

块设备是针对存储设备的,比如 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为一个结构体,定义在include/linux/fs.h 文件中,结构体内容如下:

1 struct block_device {
2     dev_t bd_dev; /* not a kdev_t - it's a search key */
3     int bd_openers;
4     struct inode *bd_inode; /* will die */
5     struct super_block *bd_super;
6     struct mutex bd_mutex; /* open/close mutex */
7     struct list_head bd_inodes;
8     void * bd_claiming;
9     void * bd_holder;
10     int bd_holders;
11     bool bd_write_holder;
12     #ifdef CONFIG_SYSFS
13     struct list_head bd_holder_disks;
14     #endif
15     struct block_device *bd_contains;
16     unsigned bd_block_size;
17     struct hd_struct *bd_part;
18 /*number of times partitions within this device have been opened.*/
19     unsigned bd_part_count;
20     int bd_invalidated;
21     struct gendisk *bd_disk;
22     struct request_queue *bd_queue;
23     struct list_head bd_list;
24 /*
25 * Private data. You must have bd_claim'ed the block_device
26 * to use this. NOTE: bd_claim allows an owner to claim
27 * the same device multiple times, the owner must take special
28 * care to not mess up bd_private for that case.
29 */
30     unsigned long bd_private;
31
32 /* The counter of freeze processes */
33     int bd_fsfreeze_count;
34 /* Mutex for freeze */
35     struct mutex bd_fsfreeze_mutex;
36 };

对于 block_device 结构体,我们重点关注一下第 21 行的 bd_disk 成员变量,此成员变量为gendisk 结构体指针类型。内核使用 block_device 来表示一个具体的块设备对象,比如一个硬盘或者分区,如果是硬盘的话 bd_disk 就指向通用磁盘结构 gendisk。

注册块设备

和字符设备驱动一样,我们需要向内核注册新的块设备、申请设备号,块设备注册函数为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: 要注销的块设备名字。
返回值: 无。

gendisk 结构体

linux 内核使用 gendisk 来描述一个磁盘设备,这是一个结构体,定义在 include/linux/genhd.h中,内容如下:

1 struct gendisk {
2 /* major, first_minor and minors are input parameters only,
3 * don't use directly. Use disk_devt() and disk_max_parts().
4 */
5     int major; /* major number of driver */
6     int first_minor;
7     int minors; /* maximum number of minors, =1 for
8 * disks that can't be partitioned. */
9
10     char disk_name[DISK_NAME_LEN]; /* name of major driver */
11     char *(*devnode)(struct gendisk *gd, umode_t *mode);
12
13     unsigned int events; /* supported events */
14     unsigned int async_events; /* async events, subset of all */
15
16 /* Array of pointers to partitions indexed by partno.
17 * Protected with matching bdev lock but stat and other
18 * non-critical accesses use RCU. Always access through
19 * helpers.
20 */
21     struct disk_part_tbl __rcu *part_tbl;
22     struct hd_struct part0;
23
24     const struct block_device_operations *fops;
25     struct request_queue *queue;
26     void *private_data;
27
28     int flags;
29     struct device *driverfs_dev; // FIXME: remove
30     struct kobject *slave_dir;
31
32     struct timer_rand_state *random;
33     atomic_t sync_io; /* RAID */
34     struct disk_events *ev;
35     #ifdef CONFIG_BLK_DEV_INTEGRITY
36     struct blk_integrity *integrity;
37     #endif
38     int node_id;
39 };

我们简单看一下 gendisk 结构体中比较重要的几个成员变量:
第 5 行, major 为磁盘设备的主设备号。
第 6 行, first_minor 为磁盘的第一个次设备号。
第 7 行, minors 为磁盘的次设备号数量,也就是磁盘的分区数量,这些分区的主设备号一样, 次设备号不同。
第 21 行, part_tbl 为磁盘对应的分区表,为结构体 disk_part_tbl 类型, disk_part_tbl 的核心是一个 hd_struct 结构体指针数组,此数组每一项都对应一个分区信息。
第 24 行, fops 为块设备操作集,为 block_device_operations 结构体类型。和字符设备操作集 file_operations 一样,是块设备驱动中的重点!
第 25 行, queue 为磁盘对应的请求队列,所以针对该磁盘设备的请求都放到此队列中,驱动程序需要处理此队列中的所有请求。

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

申请 gendisk

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

struct gendisk *alloc_disk(int minors)

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

删除 gendisk

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

void del_gendisk(struct gendisk *gp)

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

将 gendisk 添加到内核

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

void add_disk(struct gendisk *disk)

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

设置 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。
返回值: 无。

调整 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 中,结构体内容如下:

1 struct block_device_operations {
2     int (*open) (struct block_device *, fmode_t);
3     void (*release) (struct gendisk *, fmode_t);
4     int (*rw_page)(struct block_device *, sector_t, struct page *, int rw);
5     int (*ioctl) (struct block_device *, fmode_t, unsigned, unsigned long);
6     int (*compat_ioctl) (struct block_device *, fmode_t, unsigned, unsigned long);
7     long (*direct_access)(struct block_device *, sector_t,
8         void **, unsigned long *pfn, long size);
9     unsigned int (*check_events) (struct gendisk *disk,
10         unsigned int clearing);
11 /* ->media_changed() is DEPRECATED, use ->check_events() instead */
12     int (*media_changed) (struct gendisk *);
13     void (*unlock_native_capacity) (struct gendisk *);
14     int (*revalidate_disk) (struct gendisk *);
15     int (*getgeo)(struct block_device *, struct hd_geometry *);
16 /* this callback is with swap_lock and sometimes page table lock held */
17     void (*swap_slot_free_notify) (struct block_device *, unsigned long);
18         struct module *owner;
19 };

可以看出, block_device_operations 结构体里面的操作集函数和字符设备的 file_operations操作集基本类似,但是块设备的操作集函数比较少,我们来看一下其中比较重要的几个成员函数:
第 2 行, open 函数用于打开指定的块设备。
第 3 行, release 函数用于关闭(释放)指定的块设备。
第 4 行, rw_page 函数用于读写指定的页。
第 5 行, ioctl 函数用于块设备的 I/O 控制。
第 6 行, compat_ioctl 函数和 ioctl 函数一样,都是用于块设备的 I/O 控制。区别在于在 64位系统上, 32 位应用程序的 ioctl 会调用 compat_iotl 函数。在 32 位系统上运行的 32 位应用程
序调用的就是 ioctl 函数。
第 15 行, getgeo 函数用于获取磁盘信息,包括磁头、柱面和扇区等信息。
第 18 行, owner 表示此结构体属于哪个模块,一般直接设置为 THIS_MODULE。

块设备 I/O 请求过程

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

请求队列 request_queue

内核将对块设备的读写都发送到请求队列 request_queue 中, request_queue 中是大量的request(请求结构体),而 request 又包含了 bio, bio 保存了读写相关数据,比如从块设备的哪个地址开始读取、读取的数据长度,读取到哪里,如果是写的话还包括要写入的数据等。我们先来看一下 request_queue,这是一个结构体,定义在文件 include/linux/blkdev.h 中,由于request_queue 结构体比较长,这里就不列出来了。大家回过头看一下gendisk结构体就会发现里面有一个 request_queue 结构体指针类型成员变量 queue,也就说在编写块设备驱动的时候,每个磁盘(gendisk)都要分配一个 request_queue。

①、初始化请求队列
我们首先需要申请并初始化一个 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 地址。

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

void blk_cleanup_queue(struct request_queue *q)

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

③、分配请求队列并绑定制造请求函数
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 是一个结构体,定义在 include/linux/blkdev.h 里面,这里就不展开 request 结构体了,太长了。 request 里面有一个
名为“bio”的成员变量,类型为 bio 结构体指针。前面说了,真正的数据就保存在 bio 里面,所以我们需要从 request_queue 中取出一个一个的 request,然后再从每个 request 里面取出 bio,
最后根据 bio 的描述讲数据写入到块设备,或者从块设备中读取数据。

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

request *blk_peek_request(struct request_queue *q)

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

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

void blk_start_request(struct request *req)

函数参数和返回值含义如下:
req: 要开始处理的请求。
返回值: 无。

③、一步到位处理请求
我们也可以使用 blk_fetch_request 函数来一次性完成请求的获取和开启, blk_fetch_request函数很简单,内容如下:

1 struct request *blk_fetch_request(struct request_queue *q)
2 {
3    struct request *rq;
4 
5    rq = blk_peek_request(q);
6    if (rq)
7         blk_start_request(rq);
8    return rq;
9 }

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

④、其他和请求有关的函数
关于请求的 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 中,结构体内容如下:

1 struct bio {
2     struct bio *bi_next; /* 请求队列的下一个 bio */
3     struct block_device *bi_bdev; /* 指向块设备 */
4     unsigned long bi_flags; /* bio 状态等信息 */
5     unsigned long bi_rw; /* I/O 操作,读或写 */
6     struct bvec_iter bi_iter; /* I/O 操作,读或写 */
7     unsigned int bi_phys_segments;
8     unsigned int bi_seg_front_size;
9     unsigned int bi_seg_back_size;
10    atomic_t bi_remaining;
11    bio_end_io_t *bi_end_io;
12    void *bi_private;
13 #ifdef CONFIG_BLK_CGROUP
14 /*
15 * Optional ioc and css associated with this bio. Put on bio
16 * release. Read comment on top of bio_associate_current().
17 */
18     struct io_context *bi_ioc;
19     struct cgroup_subsys_state *bi_css;
20 #endif
21 union {
22 #if defined(CONFIG_BLK_DEV_INTEGRITY)
23     struct bio_integrity_payload *bi_integrity;
24 #endif
25 };
26
27     unsigned short bi_vcnt; /* bio_vec 列表中元素数量 */
28     unsigned short bi_max_vecs; /* bio_vec 列表长度 */
29     atomic_t bi_cnt; /* pin count */
30     struct bio_vec *bi_io_vec; /* bio_vec 列表 */
31     struct bio_set *bi_pool;
32     struct bio_vec bi_inline_vecs[0];
33 };

重点来看一下第 6 行和第 30 行,第 6 行为 bvec_iter 结构体类型的成员变量,第 30 行为bio_vec 结构体指针类型的成员变量。
bvec_iter 结构体描述了要操作的设备扇区等信息,结构体内容如下:

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

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

1 struct bio_vec {
2     struct page *bv_page; /* 页 */
3     unsigned int bv_len; /* 长度 */
4     unsigned int bv_offset; /* 偏移 */
5 };

可以看出 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 这三个机构体之间的关系如下图所示:

①、遍历请求中的 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 结构体指针类型。

②、遍历 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 成员变量。

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

bvoid bio_endio(struct bio *bio, int error)

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

使用请求队列实验

关于块设备架构就讲解这些,接下来我们使用开发板上的 RAM 模拟一段块设备,也就是ramdisk,然后编写块设备驱动。

实验程序编写

1 #include <linux/types.h>
......
2 #include <asm/io.h>
3
4 #define RAMDISK_SIZE (2 * 1024 * 1024) /* 容量大小为 2MB */
5 #define RAMDISK_NAME "ramdisk" /* 名字 */
6 #define RADMISK_MINOR 3 /* 表示三个磁盘分区!不是次设备号为 3! */
7
8 /* ramdisk 设备结构体 */
9 struct ramdisk_dev{
10     int major; /* 主设备号 */
11     unsigned char *ramdiskbuf; /* ramdisk 内存空间,用于模拟块设备 */
12     spinlock_t lock; /* 自旋锁 */
13     struct gendisk *gendisk; /* gendisk */
14     struct request_queue *queue;/* 请求队列 */
15 };
16
17 struct ramdisk_dev ramdisk; /* ramdisk 设备 */

第 4~6 行, 实验相关宏定义, RAMDISK_SIZE 就是模拟块设备的大小,这里设置为 2MB,也 就 是 说 本 实 验 中 的 虚 拟 块 设 备 大 小 为 2MB 。 RAMDISK_NAME 为 本 实 验 名 字 ,
RADMISK_MINOR 是本实验此设备号数量,注意不是次设备号!此设备号数量决定了本块设备的磁盘分区数量。
第 9~15 行, ramdisk 的设备结构体。
第 17 行,定义一个 ramdisk 示例。
接下来看一下驱动模块的加载与卸载,内容如下:

1 /*
2 * @description : 驱动入口函数
3 * @param : 无
4 * @return : 无
5 */
6 static int __init ramdisk_init(void)
7 {
8     int ret = 0;
9
10 /* 1、申请用于 ramdisk 内存 */
11     ramdisk.ramdiskbuf = kzalloc(RAMDISK_SIZE, GFP_KERNEL);
12     if(ramdisk.ramdiskbuf == NULL) {
13         ret = -EINVAL;
14         goto ram_fail;
15     }
16
17 /* 2、初始化自旋锁 */
18     spin_lock_init(&ramdisk.lock);
19
20 /* 3、注册块设备 */
21     ramdisk.major = register_blkdev(0, RAMDISK_NAME); /* 自动分配 */
22     if(ramdisk.major < 0) {
23         goto register_blkdev_fail;
24     }
25     printk("ramdisk major = %d\r\n", ramdisk.major);
26
27 /* 4、分配并初始化 gendisk */
28     ramdisk.gendisk = alloc_disk(RADMISK_MINOR);
29     if(!ramdisk.gendisk) {
30         ret = -EINVAL;
31         goto gendisk_alloc_fail;
32     }
33
34 /* 5、分配并初始化请求队列 */
35     ramdisk.queue = blk_init_queue(ramdisk_request_fn, &ramdisk.lock);
36     if(!ramdisk.queue) {
37         ret = -EINVAL;
38         goto blk_init_fail;
39     }
40
41 /* 6、添加(注册)disk */
42     ramdisk.gendisk->major = ramdisk.major; /* 主设备号 */
43     ramdisk.gendisk->first_minor = 0; /*起始次设备号) */
44     ramdisk.gendisk->fops = &ramdisk_fops; /* 操作函数 */
45     ramdisk.gendisk->private_data = &ramdisk; /* 私有数据 */
46     ramdisk.gendisk->queue = ramdisk.queue; /* 请求队列 */
47     sprintf(ramdisk.gendisk->disk_name, RAMDISK_NAME);/* 名字 */
48     set_capacity(ramdisk.gendisk, RAMDISK_SIZE/512); /* 设备容量(单位为扇区)*/
49     add_disk(ramdisk.gendisk);
50
51     return 0;
52
53 blk_init_fail:
54     put_disk(ramdisk.gendisk);
55 gendisk_alloc_fail:
56     unregister_blkdev(ramdisk.major, RAMDISK_NAME);
57 register_blkdev_fail:
58     kfree(ramdisk.ramdiskbuf); /* 释放内存 */
59 ram_fail:
60     return ret;
61 }
62
63 /*
64 * @description : 驱动出口函数
65 * @param : 无
66 * @return : 无
67 */
68 static void __exit ramdisk_exit(void)
69 {
70 /* 释放 gendisk */
71     del_gendisk(ramdisk.gendisk);
72     put_disk(ramdisk.gendisk);
73
74 /* 清除请求队列 */
75     blk_cleanup_queue(ramdisk.queue);
76
77 /* 注销块设备 */
78     unregister_blkdev(ramdisk.major, RAMDISK_NAME);
79
80 /* 释放内存 */
81     kfree(ramdisk.ramdiskbuf);
82 }
83
84 module_init(ramdisk_init);
85 module_exit(ramdisk_exit);
86 MODULE_LICENSE("GPL");
87 MODULE_AUTHOR("xxx");

ramdisk_init 和 ramdisk_exit 这两个函数分别为驱动入口以及出口函数,我们依次来看一下这两个函数。
第 11 行,因为本实验是使用一块内存模拟真实的块设备,因此这里先使用 kzalloc 函数申请用于 ramdisk 实验的内存,大小为 2MB。
第 18 行,初始化一个自旋锁, blk_init_queue 函数在分配并初始化请求队列的时候需要用到一次自旋锁。
第 21 行,使用 register_blkdev 函数向内核注册一个块设备,返回值就是注册成功的块设备主设备号。这里我们让内核自动分配一个主设备号,因此 register_blkdev 函数的第一个参数为0。
第 28 行,使用 alloc_disk 分配一个 gendisk。
第 35 行,使用 blk_init_queue 函数分配并初始化一个请求队列,请求处理函数为ramdisk_request_fn,具体的块设备读写操作就在此函数中完成,这个需要驱动开发人员去编写,稍后讲解。
第 42~47 行,初始化第 28 行申请到的 gendisk,重点是第 44 行设置 gendisk 的 fops 成员变量,也就是设置块设备的操作集。这里设置为 ramdisk_fops,需要驱动开发人员自行编写实现,
稍后讲解。
第 48 行,使用 set_capacity 函数设置本块设备容量大小,注意这里的大小是扇区数,不是字节数,一个扇区是 512 字节。
第 49 行, gendisk 初始化完成以后就可以使用 add_disk 函数将 gendisk 添加到内核中,也就是向内核添加一个磁盘设备。

ramdisk_exit 函数就比较简单了,在卸载块设备驱动的时候需要将前面申请的内容都释放掉。
第 71 和 72 行使用 put_disk 和 del_gendis 函数释放前面申请的 gendisk
第 75 行使用blk_cleanup_queue 函数消除前面申请的请求队列
第 78 行使用 unregister_blkdev 函数注销前面注册的块设备,最后调用 kfree 来释放掉申请的内存。

在 ramdisk_init 函数中设置了 gendisk 的 fops 成员变量,也就是块设备的操作集,具体内容如下:

1 /*
2 * @description : 打开块设备
3 * @param - dev : 块设备
4 * @param - mode : 打开模式
5 * @return : 0 成功;其他 失败
6 */
7 int ramdisk_open(struct block_device *dev, fmode_t mode)
8 {
9     printk("ramdisk open\r\n");
10    return 0;
11 }
12
13 /*
14 * @description : 释放块设备
15 * @param - disk : gendisk
16 * @param - mode : 模式
17 * @return : 0 成功;其他 失败
18 */
19 void ramdisk_release(struct gendisk *disk, fmode_t mode)
20 {
21     printk("ramdisk release\r\n");
22 }
23
24 /*
25 * @description : 获取磁盘信息
26 * @param - dev : 块设备
27 * @param - geo : 模式
28 * @return : 0 成功;其他 失败
29 */
30 int ramdisk_getgeo(struct block_device *dev, struct hd_geometry *geo)
31 {
32 /* 这是相对于机械硬盘的概念 */
33     geo->heads = 2; /* 磁头 */
34     geo->cylinders = 32; /* 柱面 */
35     geo->sectors = RAMDISK_SIZE / (2 * 32 *512); /* 磁道上的扇区数量 */
36     return 0;
37 }
38
39 /*
40 * 块设备操作函数
41 */
42 static struct block_device_operations ramdisk_fops =
43 {
44     .owner = THIS_MODULE,
45     .open = ramdisk_open,
46     .release = ramdisk_release,
47     .getgeo = ramdisk_getgeo,
48 };

第 42~48 行就是块设备的操作集 block_device_operations,本例程实现的比较简单,仅仅实现了 open、 release 和 getgeo,其中 open 和 release 函数都是空函数。重点是 getgeo 函数,第
30~37 行就是 getgeo 的具体实现,此函数用户获取磁盘信息,信息保存在参数 geo 中,为结构体 hd_geometry 类型,如下:

1 struct hd_geometry {
2     unsigned char heads; /* 磁头 */
3     unsigned char sectors; /*一个磁道上的扇区数量 */
4     unsigned short cylinders; /* 柱面 */
5     unsigned long start;
6 };

本例程中设置 ramdisk 有 2 个磁头(head)、一共有 32 个柱面(cylinderr)。 知道磁盘总容量、磁头数、柱面数以后我们就可以计算出一个磁道上有多少个扇区了,也就是 hd_geometry 中的
sectors 成员变量。最后就是非常重要的请求处理函数,使用 blk_init_queue 函数初始化队列的时候需要指定一个请求处理函数,本例程中注册的请求处理函数如下所示:

1 /*
2 * @description : 处理传输过程
3 * @param-req : 请求
4 * @return : 无
5 */
6 static void ramdisk_transfer(struct request *req)
7 {
8     unsigned long start = blk_rq_pos(req) << 9; /* blk_rq_pos 获取到的是扇区地址,左移 9 位转换为字节地址 */
9     unsigned long len = blk_rq_cur_bytes(req); /* 大小 */
10
11 /* bio 中的数据缓冲区
12 * 读:从磁盘读取到的数据存放到 buffer 中
13 * 写: buffer 保存这要写入磁盘的数据
14 */
15     void *buffer = bio_data(req->bio);
16
17     if(rq_data_dir(req) == READ) /* 读数据 */
18         memcpy(buffer, ramdisk.ramdiskbuf + start, len);
19     else if(rq_data_dir(req) == WRITE) /* 写数据 */
20         memcpy(ramdisk.ramdiskbuf + start, buffer, len);
21
22 }
23
24 /*
25 * @description : 请求处理函数
26 * @param-q : 请求队列
27 * @return : 无
28 */
29 void ramdisk_request_fn(struct request_queue *q)
30 {
31     int err = 0;
32     struct request *req;
33
34 /* 循环处理请求队列中的每个请求 */
35     req = blk_fetch_request(q);
36     while(req != NULL) {
37
38 /* 针对请求做具体的传输处理 */
39         ramdisk_transfer(req);
40
41 /* 判断是否为最后一个请求,如果不是的话就获取下一个请求
42 * 循环处理完请求队列中的所有请求。
43 */
44     if (!__blk_end_request_cur(req, err))
45         req = blk_fetch_request(q);
46     }
47 }

请求处理函数的重要内容就是完成从块设备中读取数据,或者向块设备中写入数据。首先来看一下 29~47 行的 ramdisk_request_fn 函数, 这个就是请求处理函数。此函数只要一个参数q,为 request_queue 结构体指针类型,也就是要处理的请求队列,因此 ramdisk_request_fn 函数的主要工作就是依次处理请求队列中的所有请求。第 35 行,首先使用 blk_fetch_request 函数获取请求队列中第一个请求,如果请求不为空的话就调用 ramdisk_transfer 函数进行对请求做进一步的处理,然后就是 while 循环依次处理完请求队列中的每个请求。第 44 行使用__blk_end_request_cur 函数检查是否为最后一个请求,如果不是的话就继续获取下一个,直至整个请求队列处理完成。
ramdisk_transfer 函数完成请求的数据处理,第 8 行调用 blk_rq_pos 函数从请求中获取要操作的块设备扇区地址,第 9 行使用 blk_rq_cur_bytes 函数获取请求要操作的数据长度,第 15行使用 bio_data 函数获取请求中 bio 保存的数据。第 17~20 行调用 rq_data_dir 函数判断当前是读还是写,如果是写的话就将 bio 中的数据拷贝到 ramdisk 指定地址(扇区),如果是读的话就从ramdisk 中的指定地址(扇区)读取数据放到 bio 中。

不使用请求队列实验

前面我们学习了如何使用请求队列,请求队列会用到 I/O 调度器,适合机械硬盘这种存储设备。对于 EMMC、 SD、 ramdisk 这样没有机械结构的存储设备,我们可以直接访问任意一个扇区,因此可以不需要 I/O 调度器,也就不需要请求队列了,这个我们前面已经说过了。本实验就来学习一下如何使用“制造请求”方法,本实验在上一个实验的基础上修改而来,参考了linux 内核 drivers/block/zram/zram_drv.c。重点来看一下与上一个实验不同的地方,首先是驱动入口函数 ramdisk_init, ramdisk_init 函数大部分和上一个实验相同,只是本实验中改为使用blk_queue_make_request 函数设置“制造请求”函数,修改后的 ramdisk_init 函数内容如下(有省略):

1 static int __init ramdisk_init(void)
2 {
......
29
30 /* 5、分配请求队列 */
31     ramdisk.queue = blk_alloc_queue(GFP_KERNEL);
32     if(!ramdisk.queue){
33         ret = -EINVAL;
34         goto blk_allo_fail;
35     }
36
37 /* 6、设置“制造请求”函数 */
38     blk_queue_make_request(ramdisk.queue, ramdisk_make_request_fn);
39
40 /* 7、添加(注册)disk */
41     ramdisk.gendisk->major = ramdisk.major; /* 主设备号 */
42     ramdisk.gendisk->first_minor = 0; /* 起始次设备号 */
43     ramdisk.gendisk->fops = &ramdisk_fops; /* 操作函数 */
44     ramdisk.gendisk->private_data = &ramdisk; /* 私有数据 */
45     ramdisk.gendisk->queue = ramdisk.queue; /* 请求队列 */
46     sprintf(ramdisk.gendisk->disk_name, RAMDISK_NAME); /* 名字 */
47     set_capacity(ramdisk.gendisk, RAMDISK_SIZE/512); /* 设备容量*/
48     add_disk(ramdisk.gendisk);
49
......
60     return ret;
61 }

ramdisk_init 函数中第 31~38 行就是与上一个实验不同的地方,这里使用 blk_alloc_queue和 blk_queue_make_request 这两个函数取代了上一个实验的 blk_init_queue 函数。
第 31 行,使用 blk_alloc_queue 函数申请一个请求队列。
第 38 行,使用 blk_queue_make_request 函数设置“制造请求”函数,这里设置的制造请求函数为 ramdisk_make_request_fn,这个需要驱动编写人员去实现,稍后讲解。
第 43 行,设置块设备操作集为 ramdisk_fops,和上一个实验一模一样,这里就不讲解了。
接下来重点看一下“制造请求”函数 ramdisk_make_request_fn,函数内容如下:

1 /*
2 * @description : “制造请求”函数
3 * @param-q : 请求队列
4 * @return : 无
5 */
6 void ramdisk_make_request_fn(struct request_queue *q, struct bio *bio)
7 {
8      int offset;
9      struct bio_vec bvec;
10     struct bvec_iter iter;
11     unsigned long len = 0;
12
13     offset = (bio->bi_iter.bi_sector) << 9; /* 获取设备的偏移地址 */
14
15 /* 处理 bio 中的每个段 */
16     bio_for_each_segment(bvec, bio, iter){
17         char *ptr = page_address(bvec.bv_page) + bvec.bv_offset;
18         len = bvec.bv_len;
19
20         if(bio_data_dir(bio) == READ) /* 读数据 */
21             memcpy(ptr, ramdisk.ramdiskbuf + offset, len);
22         else if(bio_data_dir(bio) == WRITE) /* 写数据 */
23             memcpy(ramdisk.ramdiskbuf + offset, ptr, len);
24         offset += len;
25     }
26     set_bit(BIO_UPTODATE, &bio->bi_flags);
27     bio_endio(bio, 0);
28 }

虽然 ramdisk_make_request_fn 函数第一个参数依旧是请求队列,但是实际上这个请求队列不包含真正的请求,所有的处理内容都在第二个 bio 参数里面,所以 ramdisk_make_request_fn函数里面是全部是对 bio 的操作。
第 13 行,直接读取 bio 的 bi_iter 成员变量的 bi_sector 来获取要操作的设备地址(扇区)。
第 16~25 行,使用 bio_for_each_segment 函数循环获取 bio 中的每个段,然后对其每个段进行处理。
第 17 行,根据 bio_vec 中页地址以及偏移地址转换为真正的数据起始地址。
第 18 行,获取要出来的数据长度,也就是 bio_vec 的 bv_len 成员变量。
第 20~23 行,和上一个实验一样,要操作的块设备起始地址知道了,数据的存放地址以及长度也知道,接下来就是根据读写操作将数据从块设备中读出来,或者将数据写入到块设备中。
第 27 行,调用 bio_endio 函数,结束 bio。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/722953.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【控制台警告】npm WARN EBADENGINE Unsupported engine

今天用webpack下载几个loader依赖&#xff0c;爆出了三个警告&#xff0c;大概的意思就是本地安装的node和npm的版本不是很匹配&#xff1f; 我的解决思路是&#xff1a; 先检查node和npm版本 然后去官网查找版本的对应 靠&#xff0c;官网404 Node.js (nodejs.org) 就找到…

操作系统:初识操作系统

目录 1.冯诺依曼体系结构 2.操作系统 2.1什么是操作系统 2.2为什么需要操作系统 2.3怎么实现操作系统 1.冯诺依曼体系结构 对于上图&#xff1a; 输入设备完成的是写入工作&#xff0c;输出设备完成输出工作&#xff0c;这两部分包含磁盘这类的外存。 存储器一般指的是内存…

Win UI3开发笔记(四)设置主题续2

本机深色主题下设置的背景颜色可以作用于整个对话框&#xff0c;本机浅色模式下设置的背景颜色只作用与下边的部分。 如果本机选深色&#xff0c;程序选浅色&#xff0c;设置为light只对上部分管用&#xff0c;下部分不管用。如图&#xff0c;左边那个hello按钮要看不见了。。…

183基于matlab的非线性调频模态分解(VNCMD)

基于matlab的非线性调频模态分解(VNCMD)&#xff0c;一种基于变分方法的信号分解技术&#xff0c;它将信号分解为多个模式。能够处理非线性调频信号&#xff0c;且对噪声具有较好的鲁棒性。VNCMD的基本原理是通过最小化信号与模式之间的差异来实现信号的分解。程序已调通&#…

Latte:一个类似Sora的开源视频生成项目

前段时间OpenAI发布的Sora引起了巨大的轰动&#xff0c;最长可达1分钟的高清连贯视频生成能力秒杀了一众视频生成玩家。因为Sora没有公开发布&#xff0c;网上对Sora的解读翻来覆去就那么多&#xff0c;我也不想像复读机一样再重复一遍了。 本文给大家介绍一个类似Sora的视频生…

最简单的基于 FFmpeg 的 AVDevice 例子(屏幕录制)

最简单的基于 FFmpeg 的 AVDevice 例子&#xff08;屏幕录制&#xff09; 最简单的基于 FFmpeg 的 AVDevice 例子&#xff08;屏幕录制&#xff09;简介libavdevice 使用抓屏方法gdigrabdshow 源程序结果工程文件下载参考链接 最简单的基于 FFmpeg 的 AVDevice 例子&#xff08…

ASUS华硕天选2锐龙版笔记本电脑FA506ICB/FA706IC原装出厂Windows11系统,预装OEM系统恢复安装开箱状态

链接&#xff1a;https://pan.baidu.com/s/122iHHEOtNUu4azhVPnxNuA?pwdsqk7 提取码&#xff1a;sqk7 适用型号&#xff1a; FA506IM、FA506IE、FA506IC、FA506IHR FA506IR、FA506IHRB、FA506ICB、FA506IEB FA706IM、FA706IE、FA706IC、FA706IHR FA706IR、FA706IHRB、F…

CSS的浮动属性,微信web开发

面试前的准备 在这部分&#xff0c;我将详细讲解面试前我们需要做哪些方面的工作&#xff0c;以保证我们在面试过程中更加顺利。 准备一份漂亮的简历 一份漂亮的简历就是你进入大厂的敲门砖。 网上有很多教程教大家如何写出一份漂亮的简历&#xff0c;这里我就不做重复劳动了…

开源模型应用落地-工具使用篇-Ollama(六)

一、前言 在AI大模型百花齐放的时代&#xff0c;很多人都对新兴技术充满了热情&#xff0c;都想尝试一下。但是&#xff0c;实际上要入门AI技术的门槛非常高。除了需要高端设备&#xff0c;还需要面临复杂的部署和安装过程&#xff0c;这让很多人望而却步。不过&#xff0c;随着…

LiveNVR监控流媒体Onvif/RTSP功能-视频广场点击在线或离线时展示状态记录快速查看通道离线原因

LiveNVR视频广场点击在线或离线时展示状态记录快速查看通道离线原因 1、状态记录1.1、点击在线查看1.2、点击离线查看 2、RTSP/HLS/FLV/RTMP拉流Onvif流媒体服务 1、状态记录 1.1、点击在线查看 可以点击视频广场页面中&#xff0c; 在线 两个字查看状态记录 1.2、点击离线查…

Thinkphp5.1中,将数组赋值给js使用

一、例如Thinkphp5.1中的的代码是这样的 $data [status > 1,msg > 加载成功,data > [id > 1,username > 小洪帽,] ];$this->assign(data,$data);二、JS代码接收PHP中的数组 注意 <> 符号是不需要放引号的。 let arr <?json_encode($data)?>…

【Godot4自学手册】第二十节增加游戏的打击感,镜头震颤、冻结帧和死亡特效

这节我主要学习增加游戏的打击感。我们通过镜头震颤、冻结帧、增加攻击点特效&#xff0c;增加死亡。开始了。 一、添加攻击点特效 增加攻击点特效就是&#xff0c;在攻击敌人时&#xff0c;会在敌人受击点显示一个受击动画。 1.添加动画。 第一步先做个受击点动画。切换到…

交叉编译qt5.14.2

qt源码下载地址&#xff1a;qt-everywhere-src-5.14.2.tar.xz 1.修改qt-everywhere-src-5.14.2/qtbase/mkspecs/linux-arm-gnueabi-g/qmake.conf文件&#xff1a; # # qmake configuration for building with arm-linux-gnueabi-g #MAKEFILE_GENERATOR UNIX CONFIG …

第三篇【传奇开心果系列】Python的自动化办公库技术点案例示例:深度解读Pandas股票市场数据分析

传奇开心果博文系列 系列博文目录Python的自动化办公库技术点案例示例系列 博文目录前言一、Pandas进行股票市场数据分析常见步骤和示例代码1. 加载数据2. 数据清洗和准备3. 分析股票价格和交易量4. 财务数据分析 二、扩展思路介绍1. 技术指标分析2. 波动性分析3. 相关性分析4.…

STM32CubeIDE基础学习-基础外设初始化配置

STM32CubeIDE基础学习-基础外设初始化配置步骤 前言 前面的文章介绍了基础工程的创建步骤&#xff0c;这篇文章就接着在基础工程的基础上来配置相关外设了&#xff0c;下面以STM32F103C8T6的主芯片为例进行简单配置。 基础工程创建步骤回顾 具体的配置步骤流程如下&#xff1…

【Linux】访问文件的本质|文件描述符|文件重定向

文章目录 文件的结构文件描述符标准输入输出文件描述符的规则 文件重定向输出重定向(对应符号>)echo的输出重定向 输入重定向&#xff08;对应符号<&#xff09;追加重定向&#xff08;对应符号‘>>’&#xff09;实现文件重定向的函数dup2()参数测试 前言&#xf…

could not publish server configuration for tomcat at localhost

1&#xff0c;报错信息如图&#xff1a; 2&#xff0c;找到servers双击&#xff0c;选择Modules&#xff0c;如果有两个webModules ,remove一个&#xff0c; 3&#xff0c;如果重启还是报错&#xff0c;干脆两个都remove&#xff0c;双击tomcat服务add And Remove重新添加

【Python】深度学习基础知识——梯度下降详解和示例

尽管梯度下降&#xff08;gradient descent&#xff09;很少直接用于深度学习&#xff0c;但它是随机梯度下降算法的基础&#xff0c;也是很多问题的来源&#xff0c;如由于学习率过大&#xff0c;优化问题可能会发散&#xff0c;这种现象早已在梯度下降中出现。本文通过原理和…

Docker知识点总结

二、Docker基本命令&#xff1a; Docker支持CentOs 6 及以后的版本; CentOs7系统可以直接通过yum进行安装&#xff0c;安装前可以 1、查看一下系统是否已经安装了Docker: yum list installed | grep docker 2、安装docker&#xff1a; yum install docker -y -y 表示自动确认…

flutter旋转动画,算法题+JVM+自定义View

在很多的博客或者书上&#xff0c;说有三种&#xff0c;除了上述的两种以外&#xff0c;还有一种是实现Callable接口。但是这种并不是&#xff0c;因为&#xff0c;我们检查JDK中Thread的源码&#xff0c;看它的注释&#xff1a; There are two ways to create a new thread o…