Linux MTD子系统(二)——mtdblock驱动分析

在之前的文章Linux MTD子系统(一)中有提到过mtd块设备,mtd块设备是在MTD设备之上模拟的块设备。
它的作用实际上只有一个——便于我们使用mount(umount)挂载(卸载)MTD设备中的文件系统,例如yaffs2,JFFS2等等。

本文将介绍mtdblock是如何实现模拟块设备的,以及它与mtd设备之间的关系。
本文基于linux-5.10.181内核代码分析。

mtd设备节点

当我们查看/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-utilsnand_write等工具只能操作/dev/mtdX字符设备,因为只有字符设备才支持ioctl操作。

核心数据结构

mtdblock_tr

mtdblock_tr变量不仅定义了mtdblock相关ops,还定义了mtdblock

  • 名字(mtdblock)
  • 主设备号(MTD_BLOCK_MAJOR为31)
  • 块大小(固定为512字节)
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_openmtdblock_flush等函数都是标准的。其中mtdblock_tr .readsectmtdblock_tr.writesect是mtdblock的读写函数指针,最终对mtdblock的读写也是调用的这两个函数。

mtd_blktrans_dev

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

mtdblk_dev

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的缓存区示意图如下:
Linux MTD子系统(二)——mtdblock驱动分析_第1张图片

  • mtdblock_open函数中会设置缓存区大小,默认和mtd设备的一个块大小保持一致,本文中缓存区大小为4096字节
  • mtdblock的缓存区大小是固定的,但是内容不是固定的,通常与上一次写入的数据有关

如果现在需要对起始地址为5120的位置(对应block1)写入512字节数据,则有如下情况出现:

  • 如果当前缓存区里面存的正好是block1对应的数据,且cache1的状态不是STATE_DIRTY,那么数据将会被写入cache1,并将cache1的状态设置为STATE_DIRTY(下次写入之前会先将cache1的数据写到block1)
  • 如果当前缓存区里面存的不是block1对应的数据,那么需要先将block1对应的数据读取到cache1,然后再将数据写入cache1,最后将cache1的状态设置为STATE_DIRTY
    Linux MTD子系统(二)——mtdblock驱动分析_第2张图片

如果现在需要从起始地址为5120的位置(对应block1)读取512字节数据,则有如下情况出现:

Linux MTD子系统(二)——mtdblock驱动分析_第3张图片

  • 如果当前缓存区里面存的正好是block1对应的数据,且cache1的状态不是STATE_EMPTY,那么将会从cache1里面读取512字节数据
  • 如果当前缓存区里面存的不是block1对应的数据,那么就会直接调用mtd_read从硬件Flash中获取数据

mtd_info

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的注册流程大致如下图所示,核心成员有四个mtdblock_tr,mtdblk_dev,mtd_blktrans_dev,mtd_info搞清楚了这四个成员之间的关系,基本上就通晓了mtdblock和mtd之前的关系。

Linux MTD子系统(二)——mtdblock驱动分析_第4张图片

下面一步步剖析上图的过程:

1> register_mtd_blktrans

首先mtdblock驱动的入口函数调用了register_mtd_blktrans(&mtdblock_tr)
register_mtd_blktrans主要执行如下操作:

  • 注册mtd_notifier
  • 注册块设备(主设备号31,name:/dev/mtdblock)
  • 初始化mtdblock_tr->devs链表头
  • 将mtdblock_tr添加到blktrans_majors
  • 遍历mtd设备Table,依次注册mtdblk_dev

2> mtdblock_add_mtd

上一步的最后遍历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_mtdmtdblock_tr->add_mtd,也就是mtdblock_add_mtd,此函数的作用是建立mtdmtd_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);
}
  • 申请mtdblk_dev
  • 将mtdblk_dev下属成员mtd_blktrans_dev 与mtd设备关联起来
  • mtd_blktrans_dev的子设备号就是mtd的index,这也是为什么/dev/mtd0对应/dev/mtdblock0的原因
  • mtd_blktrans_dev大小设置为mtd->size/512,也就是按照512字节每个扇区计算大小
  • 如果mtd设备是只读的,mtd_blktrans_dev同样要设置只读标记
  • 调用add_mtd_blktrans_dev注册mtd_blktrans_dev

3> add_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由于是模拟的块设备,它的读写流程就是块设备读写流程,由于块设备读写比较复杂,这里不再详细介绍。
这里主要介绍块设备的读写请求到达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() 大致流程如下:

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_startpos对应mtdblock缓存区的起始地址
  • offset 是需要读取的数据对应mtdblock缓存区的偏移量
  • size 是mtdblock缓存区有效数据长度
  • size > len表示缓存区内有效是多于本次需要读取的数量,故只需要读取len个字节
  • mtdblk->cache_state != STATE_EMPTY 表示缓存区非空,mtdblk->cache_offset == sect_start表示实际的缓存区的起始地址和posmtdblock缓存区的起始地址一致
    • 如果能同时满足上述条件则直接读取缓存区的数据
    • 如果不能同时满足,则需要直接调用mtd_read从Flash获取数据

mtdblock_writesect

mtdblock_writesect() 大致流程如下:

mtdblock_writesect()
	do_cached_write()
		mtd_write() 
			mtd_write_oob() 
				mtd_write_oob_std() 
					sst25l_write()//后面调用芯片厂商自己实现的读取FLASH接口

总结

  • mtdX 和 mtdblockX实际上是同一个设备,mtdX是字符设备,mtdblockX是块设备
  • mtdblockX存在的目的主要是为了挂载存在Flash里面的文件系统(例如yaffs2,jffs2)
  • mtdblock设备的读写最终也是调用mtd设备的操作函数集

你可能感兴趣的:(Linux平台,linux,mtd,mtdblock)