Linux块设备驱动架构

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;
30 unsigned long bd_private;
32 /* The counter of freeze processes */
33 int bd_fsfreeze_count;
34 /* Mutex for freeze */
35 struct mutex bd_fsfreeze_mutex;
36 };

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

1、注册块设备

和字符设备驱动一样,我们需要向内核注册新的块设备、申请设备号,块设备注册函数为
register_blkdev,函数原型如下:
int register_blkdev(unsigned int major, const char *name)
函数参数和返回值含义如下:
major:主设备号。
name:块设备名字。
返回值:如果参数 major 在 1-255 之间的话表示自定义主设备号,那么返回 0 表示注册成
功,如果返回负值的话表示注册失败。如果 major 为 0 的话表示由系统自动分配主设备号,那
么返回值就是系统分配的主设备号(1-255),如果返回负值那就表示注册失败。

2、注销块设备

和字符设备驱动一样,如果不使用某个块设备了,那么就需要注销掉,函数为
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 函数。

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 的磁盘,其扇区数量就是(210241024)/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 中,结构体内容如下:
block_device_operations 结构体

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。

块设备IO过程分析

块设备驱动中非常重要的 request_queue、request 和 bio。

1、请求队列 request_queue

内核将对块设备的读写都发送到请求队列 request_queue 中,request_queue 中是大量的
request(请求结构体),而 request 又包含了 bio,bio 保存了读写相关数据,比如从块设备的哪个
地址开始读取、读取的数据长度,读取到哪里,如果是写的话还包括要写入的数据等。我们先
来看一下 request_queue,这是一个结构体,定义在文件 include/linux/blkdev.h 中,由于
request_queue 结构体比较长,这里就不列出来了。大家回过头看一下示例代码 68.2.2.1 的 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 调度器,用于机械存储设备,比如机械硬盘等。

2、请求 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:要开始处理的请求。
返回值:无。

3、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 之间的关系如图 所示:

1663058735672

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 };

bvec_iter 结构体描述了要操作的设备扇区等信息

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 的操作最终会转换为页相关操作。

1663059013975

常见块设备种类

MTD

Memory Technology Device,内存技术设备,是用于访问memory设备(ROM、flash)的 Linux 子系统。MTD的主要目的是为了使新的memory设备的驱动更加简单,为此它在硬件和上层之间提供了一个抽象的接口,并进行了一个层次划分,层次从上到下大致为:设备文件、MTD设备层、MTD原始设备层、硬件驱动层。MTD的所有源代码在/drivers/mtd子目录下。

MTD设备文件

~ $ ls /dev/mtd* -l

crw-rw—- 1 root root 90, 0 Jan 1 00:00 /dev/mtd0

crw-rw—- 1 root root 90, 1 Jan 1 00:00 /dev/mtd0ro

crw-rw—- 1 root root 90, 2 Jan 1 00:00 /dev/mtd1

crw-rw—- 1 root root 90, 3 Jan 1 00:00 /dev/mtd1ro

crw-rw—- 1 root root 90, 4 Jan 1 00:00 /dev/mtd2

crw-rw—- 1 root root 90, 5 Jan 1 00:00 /dev/mtd2ro

crw-rw—- 1 root root 90, 6 Jan 1 00:00 /dev/mtd3

crw-rw—- 1 root root 90, 7 Jan 1 00:00 /dev/mtd3ro

brw-rw—- 1 root root 31, 0 Jan 1 00:00 /dev/mtdblock0

brw-rw—- 1 root root 31, 1 Jan 1 00:00 /dev/mtdblock1

brw-rw—- 1 root root 31, 2 Jan 1 00:00 /dev/mtdblock2

brw-rw—- 1 root root 31, 3 Jan 1 00:00 /dev/mtdblock3

/dev/mtd:

crw-rw-rw- 1 root root 90, 0 Jan 1 00:00 0

cr–r–r– 1 root root 90, 1 Jan 1 00:00 0ro

crw-rw-rw- 1 root root 90, 2 Jan 1 00:00 1

cr–r–r– 1 root root 90, 3 Jan 1 00:00 1ro

crw-rw-rw- 1 root root 90, 4 Jan 1 00:00 2

cr–r–r– 1 root root 90, 5 Jan 1 00:00 2ro

crw-rw-rw- 1 root root 90, 6 Jan 1 00:00 3

cr–r–r– 1 root root 90, 7 Jan 1 00:00 3ro

/dev/mtdblock:

brw——- 1 root root 31, 0 Jan 1 00:00 0

brw——- 1 root root 31, 1 Jan 1 00:00 1

brw——- 1 root root 31, 2 Jan 1 00:00 2

brw——- 1 root root 31, 3 Jan 1 00:00 3

可以看到有mtdN和对应的/dev/mtd/N、mtdblockN和对应的/dev/mtdblock/N两类MTD设备,分别是字符设备,主设备号90和块设备,主设备号31。其中/dev/mtd0和/dev/mtd/0是完全等价的,/dev/mtdblock0和/dev/mtdblock/0是完全等价的,而/dev/mtd0和/dev/mtdblock0则是同一个MTD分区的两种不同应用描述,操作上是有区别的。

/dev/mtdN设备

/dev/mtdN 是MTD架构中实现的mtd分区所对应的字符设备(将mtd设备分成多个区,每个区就为一个字符设备),其里面添加了一些ioctl,支持很多命令,如MEMGETINFO,MEMERASE等。

mtd-utils中的flash_eraseall等工具,就是以这些ioctl为基础而实现的工具,实现一些关于Flash的操作。比如,mtd 工具中 flash_eraseall中:

if (ioctl(fd, MEMGETINFO, &meminfo) != 0)

{

fprintf(stderr, “%s: %s: unable to get MTD device infon”,exe_name, mtd_device);

return 1;

}

MEMGETINFO是Linux MTD中的drivers/mtd/mtdchar.c中的ioctl命令,使用mtd字符设备需要加载mtdchar内核模块。该代码解释了上面的第一个现象。

/dev/mtdblockN设备

/dev/mtdblockN,是 Flash驱动 中用add_mtd_partitions()添加MTD设备分区,而生成的对应的块设备。MTD块设备驱动程序可以让flash器件伪装成块设备,实际上它通过把整块的erase block放到ram里面进行访问,然后再更新到flash,用户可以在这个块设备上创建通常的文件系统。

而对于MTD块设备,MTD设备层是不提供ioctl的实现方法的,也就不会有对应的MEMGETINFO命令之类,因此不能使用nandwrite,flash_eraseall,flash_erase等工具去对/dev/mtdblockN去进行操作,否则就会出现上面的现象一,同时也解释了现象3——用mtd2擦除分区后,在用mtdblock2进行umount就会造成混乱。

mtd块设备的大小可以通过proc文件系统进行查看:

~ $ cat /proc/partitions

major minor #blocks name

31 0 512 mtdblock0

31 1 1024 mtdblock1

31 2 5632 mtdblock2

31 3 9216 mtdblock3

254 0 30760960 mmcblk0

254 1 30756864 mmcblk0p1

后面的两个是SD块设备的分区大小。每个block的大小是1KB。

MTD设备分区

通过proc文件系统查看mtd设备的分区情况:

~ $ cat /proc/mtd

dev: size erasesize name

mtd0: 00080000 00020000 “boot”

mtd1: 00100000 00020000 “kernel”

mtd2: 00580000 00020000 “roofs70”

mtd3: 00900000 00020000 “app”

可以发现,实际上mtdN和mtdblockN描述的是同一个MTD分区,对应同一个硬件分区,两者的大小是一样的,只不过是MTD设备层提供给上层的视图不一样,给上层提供了字符和块设备两种操作视图——为了上层使用的便利和需要,比如mount命令的需求,你只能挂载块设备(有文件系统),而不能对字符设备进行挂载,否则会出现上面的现象2:无效参数。

这里对于mtd和mtdblock设备的使用场景进行简单总结:

mtd-utils工具只能应用与/dev/mtdN的MTD字符设备

mount、umount命令只对/dev/mtdblockN的MTD块设备有效

/dev/mtdN和/dev/mtdblockN是同一个MTD设备的同一个分区(N一样)

eMMC

Embedded MultiMedia Card

分区信息可以从 /proc/emmc

cat /proc/emmc

dev: size erasesize name

mmcblk0p17: 00040000 00000200 “misc”

mmcblk0p21: 0087f400 00000200 “recovery”

mmcblk0p22: 00400000 00000200 “boot”

mmcblk0p25: 22dffe00 00000200 “system”

mmcblk0p29: 002ffc00 00000200 “local”

mmcblk0p27: 090ffe00 00000200 “cache”

mmcblk0p26: 496ffe00 00000200 “userdata”

mmcblk0p30: 014bfe00 00000200 “devlog”

mmcblk0p31: 00040000 00000200 “pdata”

mmcblk0p28: 09800000 00000200 “lib”

来获取。

MMC

MultiMedia Card

它的分区信息只能从 /proc/partitions 获得:

cat /proc/partitions

MTD 设备驱动架构

1663232746013

1-> 硬件驱动层: Flash 硬件驱动层负责 Flash 硬件设备的读、写、擦除, LInux MTD 设备的 NOR Flash 芯片驱动位于 drivers/mtd/chips 子目录下, NAND Flash

的驱动程序则 位于 drivers/mtd/nand 子目录下。

2->MTD 原始设备层: MTD原始设备层由两部分组成, 一部分是MTD 原始设备的通用代码, 另一部分是各个特定 Flash 的数据,例如分区。

3->MTD设备层: 基于MTD 原始设备,Linux 系统可以定义出 MTD 的块设备的结构(主设备号 31) 和 字符设备 (设备号 90) ,构成MTD 设备层, MTD 字符设备定义

在mtdchar.c 中实现,MTD 块设备则是定义在一个描述MTD 块设备的结构 mtdblk_dev ,并声明了一个名为 mtdblks 的指针数组,这个数组 中的每个mtdblk_dev

和 mtd_table 中的每一个mtd_info 一一对应。

4->设备节点: 通过mknod 在/dev 子目录下建立MTD字符设备节点 和 块设备节点,用户通过访问此此设备节点即可访问 MTD 字符设备和块设备。

Flash 驱动中使用如下两个函数来注册 和注销MTD 设备:

int add_mtd_device(struct mtd_info *mtd);

int del_mtd_device (struct mtd_info *mtd)

仔细分析mtd中的源码可以发现其定义了一个

mtdblock_tr结构体

最后在mtd源码中还是会使用register_blkdev(tr->major,tr->name)注册一个块设备

而tr->major=31

struct mtd_info {
    u_char type;
    uint32_t flags;
    uint64_t size;   // Total size of the MTD

    /* "Major" erase size for the device. Naïve users may take this
     * to be the only erase size available, or may use the more detailed
     * information below if they desire
     */
    uint32_t erasesize;
    /* Minimal writable flash unit size. In case of NOR flash it is 1 (even
     * though individual bits can be cleared), in case of NAND flash it is
     * one NAND page (or half, or one-fourths of it), in case of ECC-ed NOR
     * it is of ECC block size, etc. It is illegal to have writesize = 0.
     * Any driver registering a struct mtd_info must ensure a writesize of
     * 1 or larger.
     */
    uint32_t writesize;

    /*
     * Size of the write buffer used by the MTD. MTD devices having a write
     * buffer can write multiple writesize chunks at a time. E.g. while
     * writing 4 * writesize bytes to a device with 2 * writesize bytes
     * buffer the MTD driver can (but doesn't have to) do 2 writesize
     * operations, but not 4. Currently, all NANDs have writebufsize
     * equivalent to writesize (NAND page size). Some NOR flashes do have
     * writebufsize greater than writesize.
     */
    uint32_t writebufsize;

    uint32_t oobsize;   // Amount of OOB data per block (e.g. 16)
    uint32_t oobavail;  // Available OOB bytes per block

    /*
     * If erasesize is a power of 2 then the shift is stored in
     * erasesize_shift otherwise erasesize_shift is zero. Ditto writesize.
     */
    unsigned int erasesize_shift;
    unsigned int writesize_shift;
    /* Masks based on erasesize_shift and writesize_shift */
    unsigned int erasesize_mask;
    unsigned int writesize_mask;

    /*
     * read ops return -EUCLEAN if max number of bitflips corrected on any
     * one region comprising an ecc step equals or exceeds this value.
     * Settable by driver, else defaults to ecc_strength.  User can override
     * in sysfs.  N.B. The meaning of the -EUCLEAN return code has changed;
     * see Documentation/ABI/testing/sysfs-class-mtd for more detail.
     */
    unsigned int bitflip_threshold;

    // Kernel-only stuff starts here.
    const char *name;
    int index;

    /* ECC layout structure pointer - read only! */
    struct nand_ecclayout *ecclayout;

    /* the ecc step size. */
    unsigned int ecc_step_size;

    /* max number of correctible bit errors per ecc step */
    unsigned int ecc_strength;

    /* Data for variable erase regions. If numeraseregions is zero,
     * it means that the whole device has erasesize as given above.
     */
    int numeraseregions;
    struct mtd_erase_region_info *eraseregions;

    /*
     * Do not call via these pointers, use corresponding mtd_*()
     * wrappers instead.
     */
    int (*_erase) (struct mtd_info *mtd, struct erase_info *instr);
    int (*_point) (struct mtd_info *mtd, loff_t from, size_t len,
               size_t *retlen, void **virt, resource_size_t *phys);
    int (*_unpoint) (struct mtd_info *mtd, loff_t from, size_t len);
    unsigned long (*_get_unmapped_area) (struct mtd_info *mtd,
                         unsigned long len,
                         unsigned long offset,
                         unsigned long flags);
    int (*_read) (struct mtd_info *mtd, loff_t from, size_t len,
              size_t *retlen, u_char *buf);
    int (*_write) (struct mtd_info *mtd, loff_t to, size_t len,
               size_t *retlen, const u_char *buf);
    int (*_panic_write) (struct mtd_info *mtd, loff_t to, size_t len,
                 size_t *retlen, const u_char *buf);
    int (*_read_oob) (struct mtd_info *mtd, loff_t from,
              struct mtd_oob_ops *ops);
    int (*_write_oob) (struct mtd_info *mtd, loff_t to,
               struct mtd_oob_ops *ops);
    int (*_get_fact_prot_info) (struct mtd_info *mtd, size_t len,
                    size_t *retlen, struct otp_info *buf);
    int (*_read_fact_prot_reg) (struct mtd_info *mtd, loff_t from,
                    size_t len, size_t *retlen, u_char *buf);
    int (*_get_user_prot_info) (struct mtd_info *mtd, size_t len,
                    size_t *retlen, struct otp_info *buf);
    int (*_read_user_prot_reg) (struct mtd_info *mtd, loff_t from,
                    size_t len, size_t *retlen, u_char *buf);
    int (*_write_user_prot_reg) (struct mtd_info *mtd, loff_t to,
                     size_t len, size_t *retlen, u_char *buf);
    int (*_lock_user_prot_reg) (struct mtd_info *mtd, loff_t from,
                    size_t len);
    int (*_writev) (struct mtd_info *mtd, const struct kvec *vecs,
            unsigned long count, loff_t to, size_t *retlen);
    void (*_sync) (struct mtd_info *mtd);
    int (*_lock) (struct mtd_info *mtd, loff_t ofs, uint64_t len);
    int (*_unlock) (struct mtd_info *mtd, loff_t ofs, uint64_t len);
    int (*_is_locked) (struct mtd_info *mtd, loff_t ofs, uint64_t len);
    int (*_block_isreserved) (struct mtd_info *mtd, loff_t ofs);
    int (*_block_isbad) (struct mtd_info *mtd, loff_t ofs);
    int (*_block_markbad) (struct mtd_info *mtd, loff_t ofs);
    int (*_suspend) (struct mtd_info *mtd);
    void (*_resume) (struct mtd_info *mtd);
    void (*_reboot) (struct mtd_info *mtd);
    /*
     * If the driver is something smart, like UBI, it may need to maintain
     * its own reference counting. The below functions are only for driver.
     */
    int (*_get_device) (struct mtd_info *mtd);
    void (*_put_device) (struct mtd_info *mtd);

    /* Backing device capabilities for this device
     * - provides mmap capabilities
     */
    struct backing_dev_info *backing_dev_info;

    struct notifier_block reboot_notifier;  /* default mode before reboot */

    /* ECC status information */
    struct mtd_ecc_stats ecc_stats;
    /* Subpage shift (NAND) */
    int subpage_sft;

    void *priv;

    struct module *owner;
    struct device dev;
    int usecount;
};

分析8197H平台 NAND FLASH驱动

8197H平台的nand flash驱动位于mtd/nand/rtkn_nand下

board_nand_init(linuxdriversmtdnandrtkn_nandmain.c)–>

rtknflash_lowinit(linuxdriversmtdnandrtkn_nandops_rtknand.c)—>

mtd_device_parse_register((linuxdriversmtdmtdcore.c)–>

mtd_add_device_partitions(linuxdriversmtdmtdcore.c)–>

add_mtd_device(linuxdriversmtdmtdcore.c)

串联他们的关键结构体就是mtd_info rtkn_mtd_info

对于瑞昱平台的nand驱动中出现的大量的oob, bbt ,ecc进行分析

nand 最基本的存储层次是页

一页有528bytes (Page) = 528 (Bytes) =数据块大小(512Bytes) + OOB 块大小(16Bytes)

在每一页中,最后16个字节(又称OOB)用于NandFlash命令执行完后设置状态用,剩余512个字节又分为前半部分和后半部分。可以通过NandFlash命令00h/01h/50h分别对前半部、后半部、OOB进行定位通过
Nand Flash内置的指针指向各自的首地址。

存储操作特点:
1.擦除操作的最小单位是块。

  1. NandFlash芯片每一位(bit)只能从1变为0,而不能从0变为1,所以在对其进行写入操作之前要一定将相应块擦除(擦除即是将相应块得位全部变为1).
    3.OOB部分的第六字节(即517字节)标志是否是坏块,如果不是坏块该值为FF,否则为坏块。
    4.除OOB第六字节外,通常至少把OOB的前3个字节存放Nand Flash硬件ECC码。

BBT:bad blocktable,即坏块表。各家对nand的坏块管理方法都有差异。比如专门用nand做存储的,会把bbt放到block0,因为第0块一定是好的块。但是如果nand本身被用来boot,那么第0块就要存放程序,不能放bbt了。

有的把bbt放到最后一块,当然,这一块不能为坏块。

NANDFlash出错的时候一般不会造成整个Block或是Page不能读取或是全部出错,而是整个Page(例如512Bytes)中只有一个或几个bit出错。一般使用一种比较专用的校验——ECC。ECC能纠正单比特错误和检测双比特错误,而且计算速度很快,但对1比特以上的错误无法纠正,对2比特以上的错误不保证能检测。

ECC一般每256字节原始数据生成3字节ECC校验数据,这三字节共24比特分成两部分:6比特的列校验和16比特的行校验,多余的两个比特置1.

当往NANDFlash的page中写入数据的时候,每256字节我们生成一个ECC校验和,称之为原ECC校验和,保存到PAGE的OOB(out-of-band)数据区中。其位置就是eccpos[]。
校验的时候,根据上述ECC生成原理不难推断:将从OOB区中读出的原ECC校验和新ECC校验和按位异或,若结果为0,则表示不存在错(或是出现了ECC无法检测的错误);若3个字节异或结果中存在11个比特位为1,表示存在一个比特错误,且可纠正;若3个字节异或结果中只存在1个比特位为1,表示OOB区出错;其他情况均表示出现了无法纠正的错误。

RAMDISK驱动

/*************************************************************************
 Copyright 2022 Hikvision Digital Technology Co.,Ltd

 FileName: ramdisk.c

 Description: 内存模拟硬盘驱动

 Author: yangyue19@hikvision.com.cn

 Date: 2022-09-19

 Modification History: <version>    <author>     <desc>
                      v1.0         yangyue19    created  file 
**************************************************************************/
#include <linux/major.h>
#include <linux/vmalloc.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/blkdev.h>
#include <linux/bitops.h>
#include <linux/mutex.h>
#include <linux/slab.h>
#include <linux/types.h>
#include <linux/hdreg.h>
#include <asm/io.h>

#define RAMDISK_SIZE    (2*1024*1024)   //ramdisk容量大小
#define RAMDISK_NAME    "ramdisk"       //ramdisk名称
#define RAMDISK_NUMS    3               //ramdisk分区数量

typedef enum{QUEUE_MODE,NO_QUEUE_MODE}ramdisk_ctl_mode_t;
typedef struct
{
    int major;
    unsigned char *diskbuf;
    spinlock_t lock;
    struct gendisk *gendisk;
    struct request_queue *queue;
    ramdisk_ctl_mode_t mode;
}ramdisk_ctl_dev_t;

static ramdisk_ctl_dev_t ramdisk_ctl_dev={
    .mode=NO_QUEUE_MODE,
};

/****************************************************************************
 Function:   ramdisk_open

 Description: 打开块设备

 Input:  null

 output: null

 Return: 0: success
        other: failed
******************************************************************************/
int ramdisk_open(struct block_device *dev ,fmode_t mode)
{
    printk("ramdisk_open n");
    return 0;
}

/****************************************************************************
 Function:   ramdisk_release

 Description: 释放块设备

 Input:  null

 output: null

 Return: 0: success
        other: failed
******************************************************************************/
int ramdisk_release(struct block_device *dev ,fmode_t mode)
{
    printk("ramdisk_release n");
    return 0;
}

/****************************************************************************
 Function:   ramdisk_getgeo

 Description: 获取块设备信息

 Input:  null

 output: null

 Return: 0: success
        other: failed
******************************************************************************/
int ramdisk_getgeo(struct block_device *dev ,struct hd_geometry *geo)
{
    geo->heads = (unsigned char)2;
    geo->cylinders=(unsigned short)32;
    geo->sectors = (unsigned char) RAMDISK_SIZE/(2 * 32 * 32);
    return 0;
}

//块设备操作函数
static struct block_device_operations ramdisk_fops = 
{
    .owner  = THIS_MODULE,
    .open   = ramdisk_open,
    .release= ramdisk_release,
    .getgeo = ramdisk_getgeo,
};

/****************************************************************************
 Function:   ramdisk_transfer

 Description: 传输数据处理

 Input:  null

 output: null

 Return: 0: success
        other: failed
******************************************************************************/
static void ramdisk_transfer(struct request *req)
{
    unsigned long start = blk_rq_pos(req) << 9;//通过扇区地址 得到开始的字节地址 
    unsigned long len   = blk_rq_cur_bytes(req);//需要传输空间大小

    void *buffer =bio_data(req->bio);

    if (rq_data_dir(req) == READ)
        memcpy(buffer, ramdisk_ctl_dev.diskbuf+start ,len );
    else if (rq_data_dir(req) == WRITE)
        memcpy( ramdisk_ctl_dev.diskbuf+start ,buffer ,len);
}

/****************************************************************************
 Function:   ramdisk_request_func

 Description: 请求队列对调函数

 Input:  null

 output: null

 Return: 0: success
        other: failed
******************************************************************************/
void ramdisk_request_func(struct request_queue * queue)
{
    int err =0;
    struct request *req;
    req = blk_fetch_request(queue);
    while(req != NULL )
    {
        ramdisk_transfer(req);
        //判断是不是最后一个请求
        if (!__blk_end_request_cur(req , err) )
            req= blk_fetch_request(queue);
    }
}

/****************************************************************************
 Function:   ramdisk_make_request_func

 Description: 手动请求

 Input:  null

 output: null

 Return: 0: success
        other: failed
******************************************************************************/
void ramdisk_make_request_func(struct request_queue * queue , struct bio *bio)
{
    int offset;
    struct bio_vec bvec;
    struct bvec_iter iter;
    unsigned long len;
    char * ptr;
    offset= (bio->bi_iter.bi_sector) <<9;

    bio_for_each_segment(bvec , bio , iter){
        ptr = page_address(bvec.bv_page) + bvec.bv_offset;
        len = bvec.bv_len;
        if(bio_data_dir(bio) == READ )
            memcpy(ptr, ramdisk_ctl_dev.diskbuf +offset , len);
        else if (bio_data_dir(bio) == WRITE )
            memcpy(ramdisk_ctl_dev.diskbuf +offset, ptr , len);
        offset += len;
    }
    set_bit(BIO_UPTODATE, &bio->bi_flags);
    bio_endio(bio, 0);
}

/****************************************************************************
 Function:   ramdisk_init

 Description: 模块入口

 Input:  null

 output: null

 Return: 0: success
        other: failed
******************************************************************************/
static int __init ramdisk_init(void)
{
    int ret;
    //1.申请磁盘空间
    ramdisk_ctl_dev.diskbuf = kmalloc(RAMDISK_SIZE , GFP_KERNEL);
    if( ramdisk_ctl_dev.diskbuf == NULL){
        printk(KERN_EMERG"kmalloc mem failedn");
        ret  = -1;
        goto ramdisk_init_malloc_failed;
    }
    //2. 初始化自旋锁
    spin_lock_init(&ramdisk_ctl_dev.lock);

    //3.注册块设备
    ramdisk_ctl_dev.major = register_blkdev(0 , RAMDISK_NAME);
    if(ramdisk_ctl_dev.major < 0)
    {
        ret = -2;
        goto ramdisk_init_register_blkdev_failed;
    }
    printk(KERN_EMERG"major %dn",ramdisk_ctl_dev.major);

    //4. 分配gendisk
    ramdisk_ctl_dev.gendisk = alloc_disk(RAMDISK_NUMS);
    if(ramdisk_ctl_dev.gendisk == NULL){
        ret = -3;
        goto ramdisk_init_alloc_disk_failed;
    }

    //5. 初始化请求队列
    if(ramdisk_ctl_dev.mode == QUEUE_MODE){
        ramdisk_ctl_dev.queue = blk_init_queue(ramdisk_request_func,&ramdisk_ctl_dev.lock);
        if(ramdisk_ctl_dev.queue==NULL){
            ret = -4;
            goto ramdisk_init_init_queue_failed;      
        }
    }else{
        ramdisk_ctl_dev.queue = blk_alloc_queue(GFP_KERNEL);
        if(! ramdisk_ctl_dev.queue)
        {
            ret =-4;
            goto ramdisk_init_init_queue_failed;
        }
        blk_queue_make_request(ramdisk_ctl_dev.queue, ramdisk_make_request_func);
    }

    //6. 注册gendisk
    ramdisk_ctl_dev.gendisk->major= ramdisk_ctl_dev.major;
    ramdisk_ctl_dev.gendisk->first_minor=0;
    ramdisk_ctl_dev.gendisk->fops=&ramdisk_fops;
    ramdisk_ctl_dev.gendisk->private_data = &ramdisk_ctl_dev;
    ramdisk_ctl_dev.gendisk->queue=ramdisk_ctl_dev.queue;
    sprintf(ramdisk_ctl_dev.gendisk->disk_name,RAMDISK_NAME);
    set_capacity(ramdisk_ctl_dev.gendisk ,RAMDISK_SIZE/512);
    add_disk(ramdisk_ctl_dev.gendisk);

    return 0;

ramdisk_init_init_queue_failed:
    put_disk(ramdisk_ctl_dev.gendisk );
ramdisk_init_alloc_disk_failed:
    unregister_blkdev(ramdisk_ctl_dev.major,RAMDISK_NAME);
ramdisk_init_register_blkdev_failed:
    kfree(ramdisk_ctl_dev.diskbuf);
ramdisk_init_malloc_failed:
    return ret;
}

/****************************************************************************
 Function:   ramdisk_exit

 Description: 模块出口

 Input:  null

 output: null

 Return: 0: success
        other: failed
******************************************************************************/
static void __exit ramdisk_exit(void)
{
    del_gendisk(ramdisk_ctl_dev.gendisk);
    put_disk(ramdisk_ctl_dev.gendisk );
    blk_cleanup_queue(ramdisk_ctl_dev.queue);
    unregister_blkdev(ramdisk_ctl_dev.major,RAMDISK_NAME);
    kfree(ramdisk_ctl_dev.diskbuf);
}

module_init(ramdisk_init);
module_exit(ramdisk_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("YURI");

加载模块之后

fdisk -l //查看磁盘信息
mkfs.vfat /dev/ramdisk
mount /dev/ramdisk /tmp

最后修改日期: 2024年11月17日

作者

留言

撰写回覆或留言

发布留言必须填写的电子邮件地址不会公开。