在之前的文章Linux MTD子系统(一)中有提到过mtd块设备,mtd块设备是在MTD设备之上模拟的块设备。
它的作用实际上只有一个——便于我们使用mount(umount)挂载(卸载)MTD设备中的文件系统,例如yaffs2,JFFS2等等。
本文将介绍mtdblock是如何实现模拟块设备的,以及它与mtd设备之间的关系。
本文基于linux-5.10.181内核代码分析。
当我们查看/dev/mtd*
时,通常情况下,我们可以看下类似如下的设备:
root@OpenWrt:~# ls /dev/mtd* -alh
crw------- 1 root root 90, 0 Jan 1 1970 /dev/mtd0
crw------- 1 root root 90, 1 Jan 1 1970 /dev/mtd0ro
crw------- 1 root root 90, 2 Jan 1 1970 /dev/mtd1
crw------- 1 root root 90, 3 Jan 1 1970 /dev/mtd1ro
crw------- 1 root root 90, 4 Jan 1 1970 /dev/mtd2
crw------- 1 root root 90, 5 Jan 1 1970 /dev/mtd2ro
brw------- 1 root root 31, 0 Jan 1 1970 /dev/mtdblock0
brw------- 1 root root 31, 4 Jan 1 1970 /dev/mtdblock1
brw------- 1 root root 31, 8 Jan 1 1970 /dev/mtdblock2
实际上/dev/mtd0
,/dev/mtd0ro
,/dev/mtdblock0
代表的是同一个MTD分区,但是/dev/mtd0
,/dev/mtd0ro
都是字符设备,其中/dev/mtd0ro
是只读字符设备,/dev/mtdblock0
是块设备。
常见的mtd-utils
,nand_write
等工具只能操作/dev/mtdX
字符设备,因为只有字符设备才支持ioctl
操作。
mtdblock_tr
变量不仅定义了mtdblock
相关ops
,还定义了mtdblock
的
static struct mtd_blktrans_ops mtdblock_tr = {
.name = "mtdblock",
.major = MTD_BLOCK_MAJOR,
#ifdef CONFIG_FIT_PARTITION
.part_bits = 2,
#else
.part_bits = 0,
#endif
.blksize = 512,
.open = mtdblock_open,
.flush = mtdblock_flush,
.release = mtdblock_release,
.readsect = mtdblock_readsect,
.writesect = mtdblock_writesect,
.add_mtd = mtdblock_add_mtd,
.remove_dev = mtdblock_remove_dev,
.owner = THIS_MODULE,
};
mtdblock_open
、mtdblock_flush
等函数都是标准的。其中mtdblock_tr .readsect
和mtdblock_tr.writesect
是mtdblock的读写函数指针,最终对mtdblock的读写也是调用的这两个函数。
mtd_blktrans_dev
是一个抽象的设备,可以把它称为转换设备,它用于记录将 MTD 设备转换为块设备的一些基本信息。
struct mtd_blktrans_dev {
struct mtd_blktrans_ops *tr;
struct list_head list;
struct mtd_info *mtd;//指向mtd设备
struct mutex lock;
int devnum;
bool bg_stop;
unsigned long size;//块转换设备的大小(以字节为单位)
int readonly;//块转换设备是否是只读的,如果是则为1,否则为0
int open;//块转换设备open 引用计数器
struct kref ref;
struct gendisk *disk;//指向磁盘设备或分区
struct attribute_group *disk_attributes;
struct request_queue *rq;
struct list_head rq_list;
struct blk_mq_tag_set *tag_set;
spinlock_t queue_lock;
void *priv;
fmode_t file_mode;
};
mtdblock模拟块设备用到的缓存区就是mtdblk_dev->cache_data
,cache_size
是缓存区的大小,通常等于MTD设备的一个擦除块大小,cache_offset
是缓存区偏移量,cache_state
是缓存区的状态标志,当状态为STATE_DIRTY
就需要调用flush
把缓存区的数据写到Flash。
struct mtdblk_dev {
struct mtd_blktrans_dev mbd;
int count;
struct mutex cache_mutex;
unsigned char *cache_data;
unsigned long cache_offset;
unsigned int cache_size;
enum { STATE_EMPTY, STATE_CLEAN, STATE_DIRTY } cache_state;//缓存状态
};
mtdblock_open
函数中会设置缓存区大小,默认和mtd
设备的一个块大小保持一致,本文中缓存区大小为4096字节mtdblock
的缓存区大小是固定的,但是内容不是固定的,通常与上一次写入的数据有关如果现在需要对起始地址为5120的位置(对应block1)写入512字节数据,则有如下情况出现:
如果现在需要从起始地址为5120的位置(对应block1)读取512字节数据,则有如下情况出现:
mtd_read
从硬件Flash中获取数据mtd_info 不做过多介绍,它代表一个mtd设备或者分区,重要的是它是字符设备。
struct mtd_info {
u_char type;
uint32_t flags;
uint64_t size; // Total size of the MTD
uint32_t erasesize;
/* "Minor" (smallest) erase size supported by the whole device */
uint32_t erasesize_minor;
......
}
mtdblock的注册流程大致如下图所示,核心成员有四个mtdblock_tr
,mtdblk_dev
,mtd_blktrans_dev
,mtd_info
搞清楚了这四个成员之间的关系,基本上就通晓了mtdblock和mtd之前的关系。
下面一步步剖析上图的过程:
首先mtdblock
驱动的入口函数调用了register_mtd_blktrans(&mtdblock_tr)
register_mtd_blktrans
主要执行如下操作:
上一步的最后遍历mtd设备table注册mtdblk_dev对应的源码如下:
int register_mtd_blktrans(struct mtd_blktrans_ops *tr)
mtd_for_each_device(mtd)
if (mtd->type != MTD_ABSENT)
tr->add_mtd(tr, mtd);
tr->add_mtd
即mtdblock_tr->add_mtd
,也就是mtdblock_add_mtd
,此函数的作用是建立mtd
和mtd_blktrans_dev
之间的联系。
static void mtdblock_add_mtd(struct mtd_blktrans_ops *tr, struct mtd_info *mtd)
{
struct mtdblk_dev *dev = kzalloc(sizeof(*dev), GFP_KERNEL);
if (!dev)
return;
dev->mbd.mtd = mtd;
dev->mbd.devnum = mtd->index;
dev->mbd.size = mtd->size >> 9;
dev->mbd.tr = tr;
if (!(mtd->flags & MTD_WRITEABLE))
dev->mbd.readonly = 1;
if (add_mtd_blktrans_dev(&dev->mbd))
kfree(dev);
}
mtd_blktrans_dev
add_mtd_blktrans_dev()
是注册mtdblock
最关键的函数,它的作用是申请并注册块设备,并建立块设备和mtd_blktrans_dev
之间的联系。
其中块设备申请与注册部分的源码如下:
int add_mtd_blktrans_dev(struct mtd_blktrans_dev *new)
** /* Create gendisk */
gd = alloc_disk(1 << tr->part_bits);
if (!gd)
goto error2;
new->disk = gd; //建立块设备和mtd_blktrans_dev 的关联
gd->private_data = new;
gd->major = tr->major;
gd->first_minor = (new->devnum) << tr->part_bits;
gd->fops = &mtd_block_ops;
snprintf(gd->disk_name, sizeof(gd->disk_name),
"%s%d", tr->name, new->devnum);
set_capacity(gd, ((u64)new->size * tr->blksize) >> 9);//设置块设备容量,单位是扇区,默认扇区大小是512
/* Create the request queue */
spin_lock_init(&new->queue_lock);
INIT_LIST_HEAD(&new->rq_list);
new->tag_set = kzalloc(sizeof(*new->tag_set), GFP_KERNEL);
if (!new->tag_set)
goto error3;
new->rq = blk_mq_init_sq_queue(new->tag_set, &mtd_mq_ops, 2,
BLK_MQ_F_SHOULD_MERGE | BLK_MQ_F_BLOCKING);//初始化块设备软件队列
if (IS_ERR(new->rq)) {
ret = PTR_ERR(new->rq);
new->rq = NULL;
goto error4;
}
if (tr->flush)
blk_queue_write_cache(new->rq, true, false);//启用写缓存,禁止强制单元访问
new->rq->queuedata = new;
//设置逻辑块大小,为了减少存储开销和提高读写性能,可能会将逻辑块大小设置得比较大,如4KB或8KB。
//而在一些需要频繁访问小文件或小数据结构的应用场景中,则可以将逻辑块大小调整到更小的值(如256字节或128字节)。
blk_queue_logical_block_size(new->rq, tr->blksize);
//指示块设备为非旋转介质,即不是机械硬盘,不需要IO调度
blk_queue_flag_set(QUEUE_FLAG_NONROT, new->rq);
//允许块设备驱动程序根据需要添加随机数种子,以帮助分散IO请求并提高系统性能
blk_queue_flag_clear(QUEUE_FLAG_ADD_RANDOM, new->rq);
if (tr->discard) {
blk_queue_flag_set(QUEUE_FLAG_DISCARD, new->rq);//队列支持 TRIM 和 DISCARD 命令,可以在需要时向底层存储介质发出删除数据块的指令
blk_queue_max_discard_sectors(new->rq, UINT_MAX);//设置队列支持的最大 DISCARD 命令扇区数目
}
gd->queue = new->rq;//
if (new->readonly)
set_disk_ro(gd, 1);
mtdblock由于是模拟的块设备,它的读写流程就是块设备读写流程,由于块设备读写比较复杂,这里不再详细介绍。
这里主要介绍块设备的读写请求到达mtdblock设备层之后的处理流程。
前面有提到注册mtd_blktrans_dev
设备时会初始化一个块设备的请求队列(new->rq)。
static const struct blk_mq_ops mtd_mq_ops = {
.queue_rq = mtd_queue_rq,
};
new->rq = blk_mq_init_sq_queue(new->tag_set, &mtd_mq_ops, 2,
BLK_MQ_F_SHOULD_MERGE | BLK_MQ_F_BLOCKING);//初始化块设备软件队列
这里面有个非常重要的结构体——mtd_mq_ops
,此数据结构用于块层与块设备层进行通信,总结就是上层的块请求最终都是调用这个mtd_mq_ops ->queue_rq
进行处理。
mtd_queue_rq
处理流程大致如下(并非完整流程,完整流程请参考源码)
mtd_queue_rq()
mtd_blktrans_work()
do_blktrans_request()
if REQ_OP_FLUSH
mtdblock_flush
case REQ_OP_DISCARD
tr->discard
case REQ_OP_READ
mtdblock_readsect
case REQ_OP_WRITE
mtdblock_writesect
由于是模拟的块设备,实际上最多只支持REQ_OP_FLUSH
,REQ_OP_DISCARD
,REQ_OP_READ
,REQ_OP_WRITE
这4中块设备IO请求。
mtdblock_readsect()
大致流程如下:
mtdblock_readsect()
do_cached_read()
mtd_read()
mtd_read_oob()
mtd_read_oob_std()
sst25l_read()//后面调用芯片厂商自己实现的读取FLASH接口
这里着重讲一下do_cached_read
static int mtdblock_readsect(struct mtd_blktrans_dev *dev,
unsigned long block, char *buf)
{
struct mtdblk_dev *mtdblk = container_of(dev, struct mtdblk_dev, mbd);
return do_cached_read(mtdblk, block<<9, 512, buf);
}
static int do_cached_read (struct mtdblk_dev *mtdblk, unsigned long pos,
int len, char *buf)
{
struct mtd_info *mtd = mtdblk->mbd.mtd;
unsigned int sect_size = mtdblk->cache_size;
size_t retlen;
int ret;
if (!sect_size)
return mtd_read(mtd, pos, len, &retlen, buf);
while (len > 0) {
unsigned long sect_start = (pos/sect_size)*sect_size;
unsigned int offset = pos - sect_start;
unsigned int size = sect_size - offset;
if (size > len)
size = len;
if (mtdblk->cache_state != STATE_EMPTY &&
mtdblk->cache_offset == sect_start) {
memcpy (buf, mtdblk->cache_data + offset, size);
} else {
ret = mtd_read(mtd, pos, size, &retlen, buf);
if (ret)
return ret;
if (retlen != size)
return -EIO;
}
buf += size;
pos += size;
len -= size;
}
return 0;
}
pos
指的是需要读取的起始地址len
指需要读取的数据长度,固定为 512字节(对应一个扇区大小)sect_size
是mtdblock的缓存区大小,如果为0,表示没有缓存区,则需要直接调用mtd_read
从Flash获取数据sect_start
是pos
对应mtdblock缓存区的起始地址offset
是需要读取的数据对应mtdblock缓存区的偏移量size
是mtdblock缓存区有效数据长度size > len
表示缓存区内有效是多于本次需要读取的数量,故只需要读取len
个字节mtdblk->cache_state != STATE_EMPTY
表示缓存区非空,mtdblk->cache_offset == sect_start
表示实际的缓存区的起始地址和pos
mtdblock缓存区的起始地址一致
mtd_read
从Flash获取数据mtdblock_writesect()
大致流程如下:
mtdblock_writesect()
do_cached_write()
mtd_write()
mtd_write_oob()
mtd_write_oob_std()
sst25l_write()//后面调用芯片厂商自己实现的读取FLASH接口