Linux块设备驱动-MTD子系统

块设备驱动

块设备驱动的引入

1. 简单字符驱动程序思想

在这里插入图片描述
​当应用程序的 open,read,write 等函数要操作“硬件”时,自然引入了“驱动程序”的概念,最简单的方式是 APP 调用 open 时,驱动程序的“drv_open”函数被调用等等。

2. 块设备驱动程序思想

在这里插入图片描述
​若块设备驱动程序也按照以上字符驱动程序的简单思想来写:

  • 硬盘

在这里插入图片描述

​磁盘的读写其实非常快,慢在机械结构读写装置的定位上面,从一个“磁头”的某“柱面”某“扇区”读到数据后(步骤 R0),跳到另一个“磁头”的某“柱面”的某“扇区”去写(步骤 W),接着再跳回原“磁头”相同柱面的下一个“扇区”去读(步骤R1)。慢就慢在读写扇区的跳转过程中。若按“字符设备”中的“opne” ,“read”,"write"方式,则总体效率在硬盘的读写上会非常低。
​上面过程是R0 ->W->R1,这个步骤跳转 2 次。如何优化这个步骤,增加读写效率,步骤为: R0->R1->W。这个步骤跳转 1 次,这样效率就会高些。
​总结:先不执行而是放入队列,优化后再执行(对硬盘有这种要求)。用“字符设备驱动”程序那样读写时就会在硬盘上跳来跳去,整体效率会非常低。所以有必要引入“优化过程”,就是读写先不执行,先放到某个“队列”中去。(调整顺序)

  • Flash

在这里插入图片描述
对于Flash,是“块”里有一个一个的扇区,Flash一般是要先擦除再写,并且擦除是以块为单位的。假如现在要写“扇区0”和“扇区1”,若用字符设备驱动的读写方式来读写:
写扇区0的过程:
① 要写时,先把这整块读到一个 buf 中
② 然后修改 buf 中扇区 0 的数据
③ 这时再擦除整块
④ 再把修改过扇区 0 的数据的 buf 烧写到整块
写扇区1的过程:
① 要写时,先把这整块读到一个 buf 中
② 然后修改 buf 中扇区 1的数据
③ 这时再擦除整块
④ 再把修改过扇区 1的数据的 buf 烧写到整块
那么要修改多个扇区时,会擦除烧写多次,总体效率会很低

优化:
① 先不执行
② 合并操作后再执行
合并:合并后只需要一次。
a. 读出整块到 buf 中
b. 在 buf 中修改扇区 0 和扇区 1
c. 擦除
d. 烧写

综上,不论是Flash还是硬盘,块设备不能像字符设备那样提供读写函数,这样效率比较低。其读写优化的思想为:① 先把读写放入队列,先不执行。② 优化后再执行。

块设备驱动框架

1. 层次框架

在这里插入图片描述
对于一个普通文件 1.txt 的读写会转成对块设备的读写,即要读写哪个扇区。从文件的读写转成对扇区的读写,中间会涉及到“文件系统”。应用程序读写一个普通的文件,最终会转换成操作硬件,由“块设备驱动程序”来完成。
​普通的文件如何转换成对扇区的读写,是由“文件系统”转换完成的,最终会调用到ll_rw_block这个通用的函数,ll_rw_block会把“读写”放入队列,调用队列的处理函数去优化(调整顺序、合并)再执行。(如何知道是ll_rw_block,可以看《LINUX内核源代码情景分析》)文件系统不是我们关心的重点,分析块设备驱动程序,从ll_rw_block这个函数开始分析。

2. 分析ll_rw_block

​文件系统把一个普通文件的读写转换成块设备扇区的读写,最终会调用这个底层的函数ll_rw_block,作用为:

  • 把“读写”放入队列
  • 调用队列的处理函数

这个函数在内核“fs”目录下,该目录下有各种各样的文件系统以及很多通用的文件,该函数在buffer.c中,buffer.c是fs目录下的通用文件。

在这里插入图片描述

/*
 * 参数1 - op:表示是读或是写
 * 参数2 - op_flags:op的标志位
 * 参数3 - nr:bhs数组的个数
 * 参数4 - bhs[]:buffer_head类型的数组,表示数据传输的三要素(源,目的和长度)
 */
void ll_rw_block(int op, int op_flags,  int nr, struct buffer_head *bhs[]);

/* struct buffer_head 结构代表一个要进行读或者写的数据块 */
struct buffer_head {
	...
	sector_t b_blocknr;		/* 数据块号 */
	size_t b_size;			/* 数据块大小 */
	char *b_data;			/* 数据块内容 */
	struct block_device *b_bdev; /* 数据块所属设备 */
	...
};
/* ll_rw_block流程梳理 */
for (i = 0; i < nr; i++) //开始就是for循环
    struct buffer_head *bh = bhs[i]; //bh 等于这个参数4数组的某一项
	submit_bh(op, op_flags, bh); //提交buffer_head结构的bh
		struct bio *bio; //使用buffer_head来构造bio(block input/output)
		submit_bio(bio); //提交bio
			generic_make_request(bio); //使用bio构造请求,把请求放入队列
				struct request_queue *q = bdev_get_queue(bio->bi_bdev); //找到队列
				ret = q->make_request_fn(q, bio);  //调用队列中的make_request_fn函数

/* q->make_request_fn函数是什么 */
struct request_queue *blk_init_queue(request_fn_proc *rfn, spinlock_t *lock)
	blk_init_queue_node(rfn, lock, NUMA_NO_NODE);
		q = blk_init_allocated_queue(uninit_q, rfn, lock);
			q->request_fn = rfn;  //队列的处理函数为rfn
    		blk_queue_make_request(q, blk_queue_bio);
				q->make_request_fn = blk_queue_bio; //make_request_fn的默认函数为“blk_queue_bio”

/* blk_queue_bio函数流程梳理 */
static blk_qc_t blk_queue_bio(struct request_queue *q, struct bio *bio)
    /* 注:elv是电梯调度算法。以elv 算法把bio合并到请求队列 */
    /* 电梯调度算法:假如1楼A要上,2楼B要下,三楼C要上,则电梯不会让A上了再让B下了最后再跑到3楼让C上,而是让电梯从1楼上到头,再下来,一次传送同样方向的人,在一个方向上合并起来运输 */
	el_ret = elv_merge(q, &req, bio); //merge合并。这里先尝试合并。
	init_request_from_bio(req, bio); //若合并不成功,就用bio构造请求
	add_acct_request(q, req, where); //把请求放到队列中去
	__blk_run_queue(q); //执行队列
		__blk_run_queue_uncond(q);
			q->request_fn(q); //其实执行队列的方式就是调用队列的处理函数(request_fn)

块设备驱动程序编写

参考:drivers\block\z2ram.c,使用内存模拟硬盘
LINUX 驱动程序中各种驱动都构造了一个结构体 ,块设备驱动也有一个结构体gendisk ,表示磁盘设备。

1.分配 gendisk 结构体,使用allock_disk函数
static struct gendisk *z2ram_gendisk;
z2ram_gendisk = alloc_disk(1);

​alloc_disk(int minors)需要参数“minors”是指次设备号个数,即“分区个数+0”,0是指整个磁盘。当minors=1时,就是把整个磁盘当成一个分区,则不能再创建其他分区,如写成16,则最多可以创建15个分区。对于一个块设备,次设备为“0”时,表示整个磁盘。如“/dev/sda”。次设备号“1”、“2”等表示磁盘的第几个主分区,次设备号从“5”开始是“扩展分区”

在这里插入图片描述

2.设置gendisk结构体

​① 分配/设置一个队列:request_queue,提供读写能力,使用blk_init_queue函数

static struct request_queue *z2_queue;
static void do_z2_request(struct request_queue *q)
{
    struct request *req;
    req = blk_fetch_request(q);
    while (req) {
        ...
        if (rq_data_dir(req) == READ)
			memcpy(buffer, (char *)addr, size);
        else
			memcpy((char *)addr, buffer, size);
        ...
    }  
}
/*
 * 参数1,执行处理队列的函数
 * 参数2,一个自旋锁 static DEFINE_SPINLOCK(z2ram_lock);
 */
z2_queue = blk_init_queue(do_z2_request, &z2ram_lock);

/* 之前分析把“文件读写”转成“扇区读写”最终调用ll_rw_block,将“扇区的读写”会放入这个队列里面:
 * 把“buffer_head”构造为“bio”,把“bio”放入队列,调用队列的“q->make_request_fn”,即blk_queue_bio函数
 * 当我们初始化队列时,提供了一个默认的构造请求的函数blk_queue_bio。这个函数最终会执行到q->request_fn(q),即do_z2_request函数来处理读写
 */

​② 设置gendisk其他信息。(提供磁盘属性:磁盘容量,扇区大小等)

/* 主设备号 */
z2ram_gendisk->major = Z2RAM_MAJOR;
/* 第一个次设备号是什么和块设备的名字 */
z2ram_gendisk->first_minor = 0;
sprintf(z2ram_gendisk->disk_name, "z2ram");
/* fops:操作函数,可以是空函数 */
z2ram_gendisk->fops = &z2_fops;
/* 设置队列,将z2_queue放到z2ram_gendisk里 */
z2ram_gendisk->queue = z2_queue;
/* 容量:设置容量时,时以扇区为单位 */
set_capacity(z2ram_gendisk, z2ram_size >> 9);
3.注册gendisk结构体,使用add_disk函数
/* 注册gendisk结构体 */
add_disk(z2ram_gendisk);
/* 注册块设备,为块设备分配Z2RAM_MAJOR主设备号,使其在cat /proc/device能看到该块设备 */
blk_register_region(MKDEV(Z2RAM_MAJOR, 0), Z2MINOR_COUNT, THIS_MODULE, z2_find, NULL, NULL);

MTD块设备驱动

MTD块设备驱动框架

在这里插入图片描述
块设备驱动可以分为ramblock、硬盘、emmc和MTD等

  • 文件系统:知道怎么去优化读写 - ll_rw_block
  • 块设备驱动:知道怎么去优化读写 - gendisk结构体
  • MTD字符设备驱动:/dev/mtdchar.c
  • MTD块设备驱动:/dev/mtdblock.c和/dev/mtd_blkdevs.c
  • 硬件协议:知道发送什么去擦除、读写(发送什么指令)
  • 硬件操作:知道怎么发送命令和地址(NAND控制器、spi通讯等)

​对于不同的Flash,例如NOR、NAND和SPI_NOR,其硬件协议和硬件操作不同,但使用同一个MTD驱动框架,最终都是构造一个mtd_info结构体,然后调用mtd_device_parse_register函数去解析注册MTD设备,生成对应的字符设备和块设备。

struct mtd_info {
	u_char type;		/* MTD类型,包括MTD_NORFLASH,MTD_NANDFLASH等(可参考mtd-abi.h) */
	uint32_t flags;		/* MTD属性标志,MTD_WRITEABLE,MTD_NO_ERASE等(可参考mtd-abi.h) */
	uint64_t size;	 	/* MTD设备的大小 */
	uint32_t erasesize;	/* MTD设备的擦除单元大小 */
	uint32_t writesize;	/* 最小的可写单元的字节数,对nor是字节,对nand为一页 */
	uint32_t writebufsize; /* MTD写缓冲区大小 */
	uint32_t oobsize;   /* OOB字节数 */
	uint32_t oobavail;  /* 可用的OOB字节数 */
    ...
	const char *name;	/* 名字 */
	int index;			/* 索引号 */
	...
	int (*_erase) (struct mtd_info *mtd, struct erase_info *instr); /* 擦除函数 */
	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);  /* 写函数 */
    ...
	void *priv;
	struct module *owner;
	struct device dev;
};

分析mtd_device_parse_register

/*
 * 参数1,mtd-mtd_info原始设备结构体
 * 参数2,types-分区解析类型,可以是cmdlinepart和ofpart
 * 参数3,parser_data-代解析的分区数据,由cmdline或者设备树传入
 * 参数4,parts-分区信息(写死在代码中,不由cmdline或设备树决定)
 * 参数5,nr_parts-分区个数
 */
int mtd_device_parse_register(struct mtd_info *mtd, const char * const *types,
			      struct mtd_part_parser_data *parser_data,
			      const struct mtd_partition *parts,
			      int nr_parts)
    ret = parse_mtd_partitions(mtd, types, &parsed, parser_data); //解析分区
    ret = mtd_add_device_partitions(mtd, &parsed); // 添加mtd设备(分区)
    	ret = add_mtd_device(mtd); // 添加mtd设备
				struct mtd_notifier *not;
				/* drivers/mtd/mtdchar.c中已注册了主设备号为90的mtd字符设备 */
				/* #define MTD_DEVT(index) MKDEV(MTD_CHAR_MAJOR, (index)*2) */
				mtd->dev.devt = MTD_DEVT(i); 
				error = device_register(&mtd->dev); // 创建dev/mtd*设备
				device_create(&mtd_class, mtd->dev.parent, MTD_DEVT(i) + 1, NULL,"mtd%dro", i); //创建dev/mtd*ro设备
				list_for_each_entry(not, &mtd_notifiers, list)
					not->add(mtd);  //遍历链表mtd_notifiers,调用not的add函数

/* mtd_notifiers在哪设置? 看drivers/mtd/mtdblock.c和drivers/mtd/mtd_blkdevs.c*/
/* mtdblock.c的入口函数 */
static int __init init_mtdblock(void)
    register_mtd_blktrans(&mtdblock_tr);
		list_add(&tr->list, &blktrans_majors);  //tr即mtdblock_tr
		register_mtd_user(&blktrans_notifier);
			list_add(&new->list, &mtd_notifiers); //new即blktrans_notifier

/* not->add即blktrans_notifier的add函数blktrans_notify_add */
/* mtd_blkdevs.c中 */
static struct mtd_notifier blktrans_notifier = {
	.add = blktrans_notify_add,
	.remove = blktrans_notify_remove,
};

/* 分析blktrans_notify_add */
static void blktrans_notify_add(struct mtd_info *mtd)
    struct mtd_blktrans_ops *tr;
	list_for_each_entry(tr, &blktrans_majors, list)
		tr->add_mtd(tr, mtd);  //遍历链表blktrans_majors,调用tr的add_mtd函数
/* blktrans_majors在哪设置?同样看mtdblock.c的入口函数 */
/* tr->add_mtd即mtdblock_tr的add_mtd函数mtdblock_add_mtd */
/* mtdblock.c中 */
static struct mtd_blktrans_ops mtdblock_tr = {
	.name		= "mtdblock",
	.major		= MTD_BLOCK_MAJOR,
	.part_bits	= 0,
	.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_add_mtd */
static void mtdblock_add_mtd(struct mtd_blktrans_ops *tr, struct mtd_info *mtd)
    add_mtd_blktrans_dev(&dev->mbd)
    	gd = alloc_disk(1 << tr->part_bits);
		new->rq = blk_init_queue(mtd_blktrans_request, &new->queue_lock);//mtd_blktrans_request就是块设备最终的读写处理函数
		gd->queue = new->rq;
		device_add_disk(&new->mtd->dev, gd); /* 也就是add_disk */
/* add_disk实际也是调用device_add_disk */		
static inline void add_disk(struct gendisk *disk)
{
    device_add_disk(NULL, disk);
}
MTD字符设备读写

drivers/mtd/mtdchar.c

static const struct file_operations mtd_fops = {
	.owner		= THIS_MODULE,
	.read		= mtdchar_read,
	.write		= mtdchar_write,
	.unlocked_ioctl	= mtdchar_unlocked_ioctl,
	.open		= mtdchar_open,
	.release	= mtdchar_close,
	...
};

static ssize_t mtdchar_read(struct file *file, char __user *buf, size_t count,loff_t *ppos)
    ret = mtd_read(mtd, *ppos, len, &retlen, kbuf);
		ret_code = mtd->_read(mtd, from, len, retlen, buf); //最终调用mtd_info结构体中的_read函数

static ssize_t mtdchar_write(struct file *file, const char __user *buf, size_t count,loff_t *ppos)
    ret = mtd_write(mtd, *ppos, len, &retlen, kbuf);
		mtd->_write(mtd, to, len, retlen, buf);//最终调用mtd_info结构体中的_write函数

static int mtdchar_ioctl(struct file *file, u_int cmd, u_long arg)
    case MEMERASE64:
		ret = mtd_erase(mtd, erase);
			mtd->_erase(mtd, instr); //最终调用mtd_info结构体中的_erase函数
MTD块设备读写

drivers/mtd/mtd_blkdevs.c
最终调用mtd_blktrans_request处理函数

static void mtd_blktrans_request(struct request_queue *rq)
	queue_work(dev->wq, &dev->work); //将dev->work添加到dev->wq工作队列中,并唤醒相应的线程处理函数

/* 这个工作队列是在哪里设置,工作又是在哪里初始化的? */
add_mtd_blktrans_dev(&dev->mbd)
    new->wq = alloc_workqueue("%s%d", 0, 0, tr->name, new->mtd->index);
	INIT_WORK(&new->work, mtd_blktrans_work);

/* mtd_blktrans_work处理函数 */
static void mtd_blktrans_work(struct work_struct *work)
    do_blktrans_request(dev->tr, dev, req);
		if (rq_data_dir(req) == READ)
            tr->readsect(dev, block, buf)
       	else
            tr->writesect(dev, block, buf)
/* tr在哪里定义 */
/* mtdblock.c中 */
static struct mtd_blktrans_ops mtdblock_tr = {
	.name		= "mtdblock",
	.major		= MTD_BLOCK_MAJOR,
	.part_bits	= 0,
	.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,
};
tr->readsect(dev, block, buf)
    mtdblock_readsect
    	do_cached_read
    		ret = mtd_read(mtd, pos, size, &retlen, buf);
				mtd->_read //最终调用mtd_info结构体中的_read函数

tr->writesect(dev, block, buf)
    do_cached_write
    	erase_write
			ret = mtd_erase(mtd, &erase);
				mtd->_erase	//最终调用mtd_info结构体中的_erase函数
			ret = mtd_write(mtd, pos, len, &retlen, buf);
				mtd->_write //最终调用mtd_info结构体中的_write函数

MTD_FLASH驱动的编写

关键:设置mtd_info结构体,使用mtd_device_parse_register注册mtd设备

SPI_NOR驱动程序编写

参考drivers\mtd\devices\m25p80.c

① 分配spi_nor结构体

② 设置spi_nor结构体

...
nor->read = m25p80_read;
nor->write = m25p80_write;
nor->write_reg = m25p80_write_reg;
nor->read_reg = m25p80_read_reg;
...

③ 硬件相关设置,在m25p80_write_reg等函数中使用spi的通讯方式

④ 使用spi_nor_scan函数,在里面设置mtd_info结构体,同时也涉及到spi_nor的硬件协议,发送什么指令进行什么操作,设置了读、写、擦、使能spi_nor等指令

int spi_nor_scan(struct spi_nor *nor, const char *name, enum read_mode mode)
{
    struct mtd_info *mtd = &nor->mtd;
    ...
    
    info = spi_nor_read_id(nor); //读flash的id,从内核维护的spi_nor_ids表格中获取具体型号的flash信息
    ...
    /* 设置mtd_info结构体 */
    mtd->type = MTD_NORFLASH; 
	mtd->writesize = 1;
	mtd->flags = MTD_CAP_NORFLASH;
	mtd->size = info->sector_size * info->n_sectors;
	mtd->_erase = spi_nor_erase;
	mtd->_read = spi_nor_read;
    ...
    /* 设置操作指令 */
    nor->erase_opcode = SPINOR_OP_SE;
    nor->read_opcode = SPINOR_OP_READ;
    nor->program_opcode = SPINOR_OP_PP;
    ...
}

⑤ 使用mtd_device_parse_register注册mtd设备

NOR驱动编写

参考drivers\mtd\maps\physmap.c

① 分配map_info结构体

② 设置物理基地址(phys), 大小(size), 位宽(bankwidth), 虚拟基地址(virt),并初始化

info->map[i].name = dev_name(&dev->dev);
info->map[i].phys = dev->resource[i].start;
info->map[i].size = resource_size(&dev->resource[i]);
info->map[i].bankwidth = physmap_data->width;
info->map[i].virt = devm_ioremap(&dev->dev, info->map[i].phys,
                                 info->map[i].size);
simple_map_init(&info->map[i]);

③ 使用do_map_probe函数,返回mtd_info结构体

info->mtd[i] = do_map_probe(*probe_type, &info->map[i]);
//*probe_type表示使用不同的标准
static const char * const rom_probe_types[] = {
	"cfi_probe", "jedec_probe", "qinfo_probe", "map_rom", NULL };

do_map_probe
    ret = drv->probe(map);
		cfi_probe
            mtd_do_chip_probe(map, &cfi_chip_probe); //使用cfi标准从flash中读出相关信息
        jedec_probe
            mtd_do_chip_probe(map, &jedec_chip_probe);  //从内核维护的jedec_table匹配对应型号的nor

④ 使用mtd_device_parse_register注册mtd设备

NAND驱动编写

参考drivers\mtd\maps\nand\hifmc100\hifmc100_os.c

① 分配nand_chip结构体

② 设置nand_chip结构体

③ 使用nand_scan函数设置mtd_info结构体,使用mtd_device_parse_register注册mtd设备

static int hisi_spi_nand_probe(struct platform_device *pltdev)
    struct hifmc_host *host;
	struct nand_chip *chip;
    struct mtd_info *mtd;
	/* 分配nand_chip结构体 */
	host = devm_kzalloc(dev, len, GFP_KERNEL);
	host->chip = chip = (struct nand_chip *)&host[1];
    host->mtd  = mtd  = nand_to_mtd(chip);
	/* 设置nand_chip结构体 */
	result = hifmc100_spi_nand_init(chip);
	/* 使用nand_scan函数设置mtd_info结构体 */
	result = hifmc_nand_scan(mtd);
		nand_scan(mtd, chip_num)
    /* 使用mtd_device_parse_register注册mtd设备 */
  	result = mtd_device_register(mtd, NULL, 0);
		mtd_device_parse_register(mtd, NULL, NULL, NULL, 0)
  • 3
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值