Linux 存储:NAND 写入异常案例 (1)

1. 前言

限于作者能力水平,本文可能存在谬误,因此而给读者带来的损失,作者不做任何承诺。

2. 案例背景

TI AM335x + Linux 4.19 内核,存储设备为 NAND FLASH,其包含 11 个分区,如下:

[    1.635286] omap-gpmc 50000000.gpmc: GPMC revision 6.0
[    1.640473] gpmc_mem_init: disabling cs 0 mapped at 0x0-0x1000000
[    1.648388] nand: device found, Manufacturer ID: 0x2c, Chip ID: 0xda
[    1.654908] nand: Micron MT29F2G08AAD
[    1.658589] nand: 256 MiB, SLC, erase size: 128 KiB, page size: 2048, OOB size: 64
[    1.666252] nand: using OMAP_ECC_BCH8_CODE_HW ECC scheme
[    1.671692] 11 fixed-partitions partitions found on MTD device omap2-nand.0
[    1.678704] Creating 11 MTD partitions on "omap2-nand.0":
[    1.684146] 0x000000000000-0x000000020000 : "NAND.SPL"
[    1.690415] 0x000000020000-0x000000040000 : "NAND.SPL.backup1"
[    1.697268] 0x000000040000-0x000000060000 : "NAND.SPL.backup2"
[    1.704018] 0x000000060000-0x000000080000 : "NAND.SPL.backup3"
[    1.710719] 0x000000080000-0x0000000c0000 : "NAND.u-boot-spl-os"
[    1.717798] 0x0000000c0000-0x0000001c0000 : "NAND.u-boot"
[    1.724932] 0x0000001c0000-0x0000001e0000 : "NAND.u-boot-env"
[    1.731558] 0x0000001e0000-0x000000200000 : "NAND.u-boot-env.backup1"
[    1.738956] 0x000000200000-0x000000a00000 : "NAND.kernel"
[    1.752614] 0x000000a00000-0x00000e000000 : "NAND.rootfs"
[    1.958515] 0x00000e000000-0x000010000000 : "NAND.userdata"

11 个分区,分别对应 Linux 下块设备节点 /dev/mtdblock[0~10]

3. 案例问题

在应用场景下,通过 dd 命令升级内核,即:

dd if=kernel.img of=/dev/mtdblock8

通过上面的命令写入内核后,接着执行 reboot 命令重启系统。启动初期,U-Boot 读取内核并加载它。这样升级内核后,测试发现,某些次数的升级,内核无法启动。

4. 案例分析

要搞清楚故障的原因,需要回答下列 2 个问题:

1. dd 写 NAND 时,会不会使用 page cache ?
2. dd 写 NAND 时,对比 U-Boot 读 NAND,是否采用了相同的坏块策略 ?

4.1 普通文件写入流程概要

文件系统 IO 栈 的工作过程,概要流程如下图:
在这里插入图片描述
用户发起读写操作时,并不是直接操作存储设备,而是需要经过较长的 IO 栈 才能完成数据的读写。读写操作大体上需依次经过虚拟文件系统 vfs磁盘文件系统block 层设备驱动层,最后到达存储器件,器件处理完成后发送中断通知驱动程序。
来看一下 普通文件 经由 page cache 写入的概要流程(假定普通文件经由 ext4fs 管理,同时 open()没带 O_DIRECT, O_SYNC 标志位),这里的代码流程将用来和后面通过 dd 操作 /dev/mtdblockN 设备的写入过程做对比。普通文件 经由 page cache 写入的概要流程代码如下:

// vfs
sys_write(fd, buf, count) /* fs/read_write.c */
	vfs_write(f.file, buf, count, &pos);
		// disk file system: ext4
		ext4_file_write_iter() /* fs/ext4/file.c */
			__generic_file_write_iter(iocb, from); /* mm/filemap.c */
				generic_perform_write(file, from, iocb->ki_pos);
					struct page *page;
					...
					// 首次,分配用于写入的 page cache 页面
					a_ops->write_begin(file, mapping, pos, bytes, flags, &page, &fsdata);
						ext4_write_begin() /* fs/ext4/inode.c */
							...
							/*
							 * 分配 或 寻找 一个符合条件的用于写入的 page cache 的页面。
							 * 首次,分配 page cache 页面。
							 */
							page = grab_cache_page_write_begin(mapping, index, flags);
							...
							__block_write_begin(page, pos, len, ext4_get_block);
							...
							*pagep = page; /* 返回 page cache 页面 */
					...
					/* 将数据从用户空间拷贝到内核空间 */
					copied = iov_iter_copy_from_user_atomic(page, i, offset, bytes);
					...
					/* 提交数据写入请求 */
					a_ops->write_end(file, mapping, pos, bytes, copied, page, fsdata);
						block_write_end(file, mapping, pos, len, copied, page, fsdata);
							__block_commit_write(inode, page, start, start+copied); /* fs/buffer.c */

从上面的代码流程可以看到,整个 write() 过程中,都没有涉及到任何存储设备相关的操作,这个实际的写入最终经由 submit_bio() 提交写入请求到存储设备,譬如 shrink_page_list() 回收 page cache 页面触发等。这些细节不是本文关注的重点,在此不再细表。
通过上面的代码分析,我们只需要了解到,文件一般情形下的写入过程会经过 page cache, write() 调用返回并不意味着数据已经写入到存储设备

4.2 dd 写 NAND 时,会不会使用 page cache ?

我们为什么要知道,dd 写入 NAND 时会不会经过 page cache ?因为如果经过 page cache,可能会因为 写入内容没有回写到 NAND ,而导致数据异常。
dd 不太熟悉的朋友,可能会误解,认为 dd 对块设备节点 /dev/mtdblockN 的操作,就是对存储设备的直接操作,不会经过 page cache,常常会误以为 page cache 仅用于普通文件操作。然而事实并非如此,后面的代码分析会揭示这一点。
接着从代码细节来看一看,dd 命令对块设备 /dev/mtdblockN 的写入过程,到底会不会经过 page cache ?这里只关注 dd if=kernel.img of=/dev/mtdblock8 命令的写操作,其读操作是否经过 page cache 和分析的问题无涉,故忽略。先看一下 open("/dev/mtdblock8") 的过程:

// 先看一下 open("/dev/mtdblock8") 的过程,主要关注 fops 和 a_ops 的绑定
open("/dev/mtdblock8")
	...
	vfs_open(&nd->path, file);
		file->f_path = *path;
		do_dentry_open(file, d_backing_inode(path->dentry), NULL);
			...
			f->f_op = fops_get(inode->i_fop); /* 绑定 fops: &def_blk_fops */
			...
			if (!open)
				open = f->f_op->open; /* blkdev_open() */
			if (open) {
				open(inode, f) = blkdev_open(inode, f); /* fs/block_dev.c */
					struct block_device *bdev;
					...
					bdev = bd_acquire(inode);
						bdev = bdget(inode->i_rdev);
							struct inode *inode;
							inode = iget5_locked(blockdev_superblock, hash(dev),
								bdev_test, bdev_set, &dev);
							...
							if (inode->i_state & I_NEW) {
								...
								inode->i_mode = S_IFBLK;
								...
								/* 绑定 address space 操作接口 */
								inode->i_data.a_ops = &def_blk_aops;
								...
							}
							...
					...
					return blkdev_get(bdev, filp->f_mode, filp);
			}

上面的流程,重点关注 f_op(struct file_operations) 绑定到了 def_blk_fops,而 a_ops(struct address_space_operations) 绑定到了 def_blk_aopsdd 的写操作也没什么稀奇的,仍然是从 write() 发起:

// vfs
sys_write(fd, buf, count) /* fs/read_write.c */
	vfs_write(f.file, buf, count, &pos);
		...
		blkdev_write_iter() /* fs/block_dev.c */
			__generic_file_write_iter(iocb, from) /* mm/filemap.c */
				/* 
				 * 后面的写入流程,在没有指定 oflag=direct 的前提下,
				 * 和前面分析的 普通文件 的写入流程一样,都将会使用
				 * page cache !!!
				 */
				generic_perform_write(file, from, iocb->ki_pos);
					struct page *page;
					/* 首次,分配 page cache 页面 */
					a_ops->write_begin(file, mapping, pos, bytes, flags, &page, &fsdata);
						blkdev_write_begin()
							block_write_begin(mapping, pos, len, flags, pagep, blkdev_get_block);
								/*
								 * 分配 或 寻找 一个符合条件的用于写入的 page cache 的页面。
								 * 首次,分配 page cache 页面。
								 */
								page = grab_cache_page_write_begin(mapping, index, flags);
								...
								*pagep = page; /* 返回 page cache 页面 */
								...
					...
					/* 将数据从用户空间拷贝到内核空间 */
					copied = iov_iter_copy_from_user_atomic(page, i, offset, bytes);
					...
					a_ops->write_end(file, mapping, pos, bytes, copied, page, fsdata);
						blkdev_write_end()
					...

到此,我们可以给出本小节提出问题的答案。dd 写 NAND 时,会不会使用 page cache ?在 dd 命令未指定 oflag=direct 的前提下dd 写入过程中的数据,会经过 page cache

4.3 dd 写 NAND 时,对比 U-Boot 读 NAND,是否采用了相同的坏块策略 ?

4.3.1 U-Boot 读 NAND 过程中遇坏块的处理策略

从博文 U-Boot: NAND 驱动简介 章节 3.3 NAND 写操作3.4 NAND 读操作 分析知道,U-Boot 在 NAND 读写过程,遇到坏块会跳过,直到读取到要求数目的 block 为止,且坏块不会包含在读写 block 计数内。这就意味着,在 Linux 下通过 dd 写入 kernel.img ,也必须和 U-Boot 读操作保持相同的策略,即写入 NAND 时遇到坏块跳过,这样才使得启动期间 U-Boot 能正确读到 kernel.img 的完整内容,也才能正确加载内核。

4.3.2 Linux 写 NAND 过程中遇坏块的处理策略

首先,dd不对坏块做处理,这就要看 Linux 内核 NAND 驱动读写过程中,对坏块的处理策略。这里只关注和本文相关写入过程中 NAND 驱动坏块处理过程。这里的分析,刻意跳过了前面冗长的无关部分,即前面分析提到的提交写操作请求到设备的部分(submit_bio()),我们重点关注块类设备拉取写操作请求,并进行实际对设备写入的部分。分析从 mtd_blktrans_work() 开始(Linux 将 NAND 设备抽象为 MTD 设备进行访问):

// 在不影响主干逻辑的前提下,为方便查看,对代码有做一些改动。
mtd_blktrans_work() /* drivers/mtd/mtd_blkdevs.c */
	// 拉取一个写入请求
	req = blk_fetch_request(rq);
	...
	// 处理写入请求
	do_blktrans_request(dev->tr, dev, req);
		for (; nsect > 0; nsect--, block++, buf += tr->blksize) // (1)
			tr->writesect(dev, block, buf) = mtdblock_writesect() /* drivers/mtd/mtdblock.c */
				if (unlikely(!mtdblk->cache_data && mtdblk->cache_size)) {
					/*
					 * 第一次写入, 分配 cache 缓存。
					 * 
					 * 注意,这里的 cache 不同于 page cache,主要是为了一些
					 * 非对齐数据操作等。
					 */
					mtdblk->cache_data = vmalloc(mtdblk->mbd.mtd->erasesize);
				}
				do_cached_write(mtdblk, block<<9, 512, buf); /* drivers/mtd/mtdblock.c */
					// 这里只取主干逻辑
					...
					while (len > 0) { // (2)
						erase_write (mtd, pos, size, buf);
							...
							// 写入之前总是要先擦除
							mtd_erase(mtd, &erase); /* drivers/mtd/mtdcore.c */
							if (ret) { /* 擦除出错 */
								...
								// 打印擦除出错的范围区间
								printk (KERN_WARNING "mtdblock: erase of region [0x%lx, 0x%x] "
										"on \"%s\" failed\n",
									pos, len, mtd->name);
								return ret; // 擦除失败,终止写入过程
							}
							...
							// 擦除成功后再写入
							mtd_write(mtd, pos, len, &retlen, buf);
					}
					...
	...

// 展开一下前面的 mtd_erase()
mtd_erase(mtd, &erase); /* drivers/mtd/mtdcore.c */
	mtd->_erase(mtd, instr) = part_erase() /* drivers/mtd/mtdpart.c */
		part->parent->_erase(part->parent, instr)
			nand_erase() /* drivers/mtd/nand/nand_base.c */
				nand_erase_nand(mtd, instr, 0)
					...
					instr->state = MTD_ERASING;
					
					while (len) { // (3)
						/* Check if we have a bad block, we do not erase bad blocks! */
						// 检查擦除位置是不是有坏块:无法对坏块进行擦除操作
						if (nand_block_checkbad(mtd, ((loff_t) page) <<
								chip->page_shift, allowbbt)) {
							// 打印擦除出错的 NAND PAGE 位置
							pr_warn("%s: attempt to erase a bad block at page 0x%08x\n",
								__func__, page);
							instr->state = MTD_ERASE_FAILED; // 标记擦除出错
							goto erase_exit; // 擦除出错,终止擦除过程
						}
					}
					instr->state = MTD_ERASE_DONE;

				erase_exit:
					// 擦除正常完成为 MTD_ERASE_DONE 状态, 如果不是, 返回 EIO 错误码
					ret = instr->state == MTD_ERASE_DONE ? 0 : -EIO;
					...

					return ret;

上面的代码分析告诉我们,在 Linux 下,试图写入 NAND 坏块位置,NAND 驱动将会终止写入过程,并报告错误信息。如下是试图用 dd 写入坏块时的一个例子,内核爆出如下日志:

$ dd if=/mnt/nfs-remote/t.img bs=128K seek=416 count=8 of=/dev/mtdblock9 # 执行 dd 试图往坏块位置写入
[ 6002.927447] nand: nand_erase_nand: attempt to erase a bad block at page 0x00007c80
[ 6002.935145] mtdblock: erase of region [0x3440000, 0x20000] on "NAND.rootfs" failed
[ 6002.943396] print_req_error: I/O error, dev mtdblock9, sector 107264
[ 6002.949794] Buffer I/O error on dev mtdblock9, logical block 13408, lost async page write
[ 6002.958470] nand: nand_erase_nand: attempt to erase a bad block at page 0x00007c80
[ 6002.966143] mtdblock: erase of region [0x3440000, 0x20000] on "NAND.rootfs" failed
[ 6002.973973] print_req_error: I/O error, dev mtdblock9, sector 107272
[ 6002.980360] Buffer I/O error on dev mtdblock9, logical block 13409, lost async page write
......
[ 6003.231379] nand: nand_erase_nand: attempt to erase a bad block at page 0x00007c80
[ 6003.239003] mtdblock: erase of region [0x3440000, 0x20000] on "NAND.rootfs" failed
[ 6003.246899] nand: nand_erase_nand: attempt to erase a bad block at page 0x00007c80
[ 6003.254636] mtdblock: erase of region [0x3440000, 0x20000] on "NAND.rootfs" failed
......
8+0 records in
8+0 records out

# echo $? # 查询 dd 命令的执行退出码,惊不惊喜?意不意外?
0

从上面 dd 测试写坏块的例子看到,即使往坏块位置去写,dd 的退出码依然是 0 ,这可能违反我们的直觉。从前面的代码分析可以知道,出现这种现象的根本原因是因为,写入 NAND 的操作实际上分为两步write() 给出数据写入需求后返回;然后内核在某个不确定的时间点将写入请求提交给 NAND 设备,然后 NAND 设备驱动取出写入请求做实际的写入操作。也就是说,写入的发起和实际写入,是一个异步过程write() 返回成功并不表示写入成功,在 NAND 设备写入时如果出错,已经没法将这个错误返回给 write(),因为 write() 可能已经(成功)返回用户空间。
到此,仍然没有对 Linux 下 NAND 坏块管理策略做出说明。下面来分析说明这一点:

// 上面判断坏块的代码
nand_block_checkbad(mtd, ((loff_t) page) << chip->page_shift, allowbbt)) /* drivers/mtd/nand/nand_base.c */
	struct nand_chip *chip = mtd_to_nand(mtd);

	if (!chip->bbt) /* 没有在驱动加载初期, 扫描 NAND 建立位于内存 BBT 表的情形下, */
		/*
		 * 用 NAND 的 OOB 区域 标记 判断 是不是 NAND 坏块.
		 * drivers/mtd/nand/nand_base.c, nand_block_bad()
		 * ...
		 */
		return chip->block_bad(mtd, ofs);
			nand_block_bad()
				...
				for (; page < page_end; page++) {
					/*
					 * 读取 NAND 不带 ECC 的 OOB 数据。
					 * drivers/mtd/nand/nand_base.c: nand_read_oob_std() [default]
					 * ...
					 */
  					res = chip->ecc.read_oob(mtd, chip, page);
  						nand_read_oob_std()
  							// 从 NAND 设备读取 OOB
  							chip->read_buf(mtd, chip->oob_poi, mtd->oobsize);
  					...
  					bad = chip->oob_poi[chip->badblockpos];

					/* 从 NAND 读取的 OOB 数据判定是否为坏块 */
					if (likely(chip->badblockbits == 8))
						res = bad != 0xFF;
					else
						res = hweight8(bad) < chip->badblockbits;
					if (res)
						return res; /* 坏块 */
				}

				return 0; /* 非坏块 */
	
	/* Return info from the table */
	return nand_isbad_bbt(mtd, ofs, allowbbt); /* 从已经建立的内存 BBT 表判定是否为坏块 */

从上面的代码分析知道,在 U-Boot 读时、Linux 写时遇到坏块的处理策略上存在差异:U-Boot 读时会跳过坏块,直到读取到要求的大小为止;而 Linux 写时,遇到坏块会报错并终止对 NAND 设备的写入过程,但由于实际对 NAND 设备的写操作相对 write() 调用是异步的,而不是在 write() 调用中完成,因此实际写操作过程中的错误状况无法反馈给 write(),即 ddwrite() 的调用在写坏块时不会返回错误码,导致即使实际的 NAND 设备写操作发生了错误,dd 仍会继续完成所有的写操作,并最终以错误码 0 成功退出。

5. 解决方案

针对本文讨论的 2 个问题:

1. dd 写 NAND 时,会不会使用 page cache ?
2. dd 写 NAND 时,对比 U-Boot 读 NAND,是否采用了相同的坏块策略 ?

分别对它们给出解决方案。

5.1 避免数据在 page cache 逗留

对于问题 1. ,可以通过将 dd 命令行选项 oflag=direct 来解决,这样写入数据可以绕过 page cache 。来看看代码实现细节:

/*
 * coreutils 的 dd 命令将 oflag=direct 转换为 O_DIRECT
 */

/* src/dd.c */
/* Flags, for iflag="..." and oflag="...".  */
static struct symbol_value const flags[] =
{
	...
	{"direct",   O_DIRECT},
	...
	{"",  0}
};
/*
 * write() 时,如果设置了 O_DIRECT 的处理流程
 */

static inline bool io_is_direct(struct file *filp)
{
	return (filp->f_flags & O_DIRECT) || IS_DAX(filp->f_mapping->host);
}

static inline int iocb_flags(struct file *file)
{
	int res = 0;
	...
	if (io_is_direct(file))
		res |= IOCB_DIRECT; // O_DIRECT ==> IOCB_DIRECT
	...
	return res;
}

// vfs
sys_write(fd, buf, count) /* fs/read_write.c */
	vfs_write(f.file, buf, count, &pos);
		// 这里比前面的分析细化了一点,主要是标志位 O_DIRECT 的处理
 		__vfs_write(file, buf, count, pos);
 			new_sync_write(file, p, count, pos);
 				...
 				init_sync_kiocb(&kiocb, filp);
 					*kiocb = (struct kiocb) {
 						...
 						.ki_flags = iocb_flags(filp), // O_DIRECT ==> IOCB_DIRECT
 						...
 					};
 				...
 				call_write_iter(filp, &kiocb, &iter);
 					blkdev_write_iter()
 						__generic_file_write_iter(iocb, from);
 							...
 							if (iocb->ki_flags & IOCB_DIRECT) { /* O_DIRECT */
 								loff_t pos, endbyte;
 								
 								/* 不通过 page cache 的 direct IO */
 								written = generic_file_direct_write(iocb, from); 
 								
 								/*
 								 * If the write stopped short of completing, fall back to
 								 * buffered writes.  Some filesystems do this for writes to
 								 * holes, for example.  For DAX files, a buffered write will
 								 * not succeed (even if it did, DAX does not handle dirty
 								 * page-cache pages correctly).
 								 */
 								if (written < 0 || !iov_iter_count(from) || IS_DAX(inode))
 									goto out;

								/*
								 * 如果不带 page cache 的 direct 没有写入所有数据 或 失败,
								 * 我们用带 page cache 的 写入操作,此时,为了保持 O_DIRECT
								 * 的语义,需要将 page cache 中写入的内容刷入到存储设备,然
								 * 后将这些 page cache 页面置为无效状态。
								 */
								status = generic_perform_write(file, from, pos = iocb->ki_pos);
								...
								endbyte = pos + status - 1;
								err = filemap_write_and_wait_range(mapping, pos, endbyte);
								if (err == 0) {
									iocb->ki_pos = endbyte + 1;
									written += status;
									/* 将 page cache 页面置为无效状态 */
									invalidate_mapping_pages(mapping,
											pos >> PAGE_SHIFT,
											endbyte >> PAGE_SHIFT);
								} else {
									...
								}
 							} else {
 								// 没有设置 O_DIRECT 的时候执行路径,也就是前面分析的执行路径
 								...
 							}

// 展开 generic_file_direct_write()
written = generic_file_direct_write(iocb, from); /* 不通过 page cache 的 direct IO */
	...
	// 这里代码有做些调整,不影响主干逻辑
	written = filemap_write_and_wait_range(mapping, pos, pos + write_len - 1);
	...
	written = invalidate_inode_pages2_range(mapping, pos >> PAGE_SHIFT, end);
	...
	written = mapping->a_ops->direct_IO(iocb, from);
		blkdev_direct_IO(iocb, from)
			__blkdev_direct_IO(iocb, iter, min(nr_pages, BIO_MAX_PAGES));
				...
				/* 分配当前数据的第一个 bio */
				bio = bio_alloc_bioset(GFP_KERNEL, nr_pages, blkdev_dio_pool);
				bio_get(bio); /* extra ref for the completion handler */
				...
				dio->is_sync = is_sync = is_sync_kiocb(iocb);
				if (dio->is_sync)
					dio->waiter = current;
				else
					...
				...
				for (;;) {
					bio_set_dev(bio, bdev);
					bio->bi_iter.bi_sector = pos >> 9;
					...
					/* 设置 bio 已经提交到 io 请求队列时回调, 但数据还没有写到存储设备 */
					bio->bi_end_io = blkdev_bio_end_io;
					...
					nr_pages = iov_iter_npages(iter, BIO_MAX_PAGES);
					if (!nr_pages) {
						qc = submit_bio(bio); /* 提交当前数据的最后一个 bio 请求 */
						break; /* 退出循环 */
					}
					...
					submit_bio(bio); /* 提交一个 bio 请求 */
					bio = bio_alloc(GFP_KERNEL, nr_pages); /* 分配一个新的 bio */
				}
				...
				/* 等待所有 bio 进入 io 请求队列 */
				for (;;) {
					// bio 进入 io 请求队列时触发的回调 blkdev_bio_end_io()
					//  修改  dio->waiter 并唤醒在这里等待的进程。
					set_current_state(TASK_UNINTERRUPTIBLE);
					if (!READ_ONCE(dio->waiter))
						break;

					if (!(iocb->ki_flags & IOCB_HIPRI) ||
						!blk_mq_poll(bdev_get_queue(bdev), qc))
						io_schedule();
				}
				
				__set_current_state(TASK_RUNNING);
				...
	...
	iov_iter_revert(from, write_len - iov_iter_count(from));
out:
	return written; // 返回 direct IO 写入字节数

blkdev_bio_end_io()
	if (dio->multi_bio && !atomic_dec_and_test(&dio->ref)) {
		...
	} else {
		if (!dio->is_sync) {
			...
		} else {
			struct task_struct *waiter = dio->waiter;

			/*
			 * 唤醒在 __blkdev_direct_IO() 等待 bio 进入 io 请求队列的进程:
			 * __blkdev_direct_IO()
			 *  for (;;) {
			 *	set_current_state(TASK_UNINTERRUPTIBLE);
			 *	if (!READ_ONCE(dio->waiter))
			 *	    break;
			 *	...
			 *  }
			 */
			WRITE_ONCE(dio->waiter, NULL);
			wake_up_process(waiter);
		}
	}
	...

从上面的分析我们了解到,dd 带上 oflag=direct (即 open() 带上 O_DIRECT 标志),告诉内核不使用 page cache(或同等操作,详见代码分析)进行写入操作,这样可以避免数据逗留在 page cache 而导致数据异常的情况。但要知道的是,write() 调用返回时,只是保证写入的 bio 已经进行设备的 io 请求队列;至于这些 io 请求什么时候兑现,即真正的将数据写入存储设备,这是不确定的,write() 仍然无法知道存储设备的写入错误
前面提到了 bio,有必要对 bio 做一些简单说明。按照 io 请求的生命周期io 请求被抽象成了 biorequest(简称 rq)cmdbio 由前面提到的 submit_bio() 接口提交。访问存储器件上相邻区域的 biorequest 可能会被合并,称为 bio mergerequest merge。若 bio 的长度超过软件或者硬件的限制,bio 会被拆分成多个,称为 bio split。block 层接收到一个 bio 后,这个 bio 将生成一个新的 request,或者合并到已有的 request 中。如下图所示:
在这里插入图片描述
更多关于 bio 的细节,可参考博文 linux IO Block layer 解析

5.2 数据写入同步

章节 5.1 中,给 dd 加上 oflag=direct 避免了写入数据在 page cache 逗留的问题。如果同时给 dd 带上 oflag=sync 标记(即 dd oflag=direct,sync) ,则可以认为在没有写入错误的情况下(没遇到坏块), write() 调用返回时数据已经写入设备(实际上这也只是软件层面的保证,存储设备本身可能也会有写入缓存)。来看一下 dd 带上 oflag=sync 标记时发生了什么。首先 ddoflag=sync 转换为 O_SYNC 标记:

/* src/dd.c */

/* Flags, for iflag="..." and oflag="...".  */
static struct symbol_value const flags[] =
{
	...
	{"sync",   O_SYNC},
	...
	{"",  0}
};

O_SYNC 标记对写入过程的影响:

/*
 * write() 时,如果设置了 O_SYNC 的处理流程
 */

#ifndef O_SYNC
#define __O_SYNC 04000000
#define O_SYNC  (__O_SYNC|O_DSYNC)
#endif

static inline int iocb_flags(struct file *file)
{
	int res = 0;
	...
	// O_SYNC ==> IOCB_DSYNC | IOCB_SYNC
	if ((file->f_flags & O_DSYNC) || IS_SYNC(file->f_mapping->host))
		res |= IOCB_DSYNC;
	if (file->f_flags & __O_SYNC)
		res |= IOCB_SYNC;
	return res;
}

// vfs
sys_write(fd, buf, count) /* fs/read_write.c */
	vfs_write(f.file, buf, count, &pos);
		__vfs_write(file, buf, count, pos);
			new_sync_write(file, p, count, pos);
				init_sync_kiocb(&kiocb, filp);
					*kiocb = (struct kiocb) {
						...
						// O_SYNC ==> IOCB_DSYNC | IOCB_SYNC
						.ki_flags = iocb_flags(filp),
						...
					};
				call_write_iter(filp, &kiocb, &iter);
					blkdev_write_iter()
						ret = __generic_file_write_iter(iocb, from);
						if (ret > 0) /* 写入成功, 根据 sync 设置看是否要做数据同步操作 */
							ret = generic_write_sync(iocb, ret);
								if (iocb->ki_flags & IOCB_DSYNC) { /* 如果需要做数据同步操作 */
									/* 做数据同步操作 */
									int ret = vfs_fsync_range(iocb->ki_filp,
										iocb->ki_pos - count, iocb->ki_pos - 1,
										(iocb->ki_flags & IOCB_SYNC) ? 0 : 1);
									if (ret) /* 数据同步操作出错 */
										return ret;
								}
								
								return count; /* 数据同步操作成功 */
						...

// 展开一下 vfs_fsync_range()
int ret = vfs_fsync_range(iocb->ki_filp,
			iocb->ki_pos - count, iocb->ki_pos - 1,
			(iocb->ki_flags & IOCB_SYNC) ? 0 : 1);
	...
	return file->f_op->fsync(file, start, end, datasync);
		blkdev_fsync(file, start, end, datasync)
			error = file_write_and_wait_range(filp, start, end);
			...
			error = blkdev_issue_flush(bdev, GFP_KERNEL, NULL);
				...
				bio = bio_alloc(gfp_mask, 0);
				bio_set_dev(bio, bdev);
				bio->bi_opf = REQ_OP_WRITE | REQ_PREFLUSH; // flush 数据到存储设备

				ret = submit_bio_wait(bio);
				...

5.3 将 Linux 内核写入坏块处理 和 U-Boot 保持一直:跳过坏快

从前面分析了解到,直接使用 dd无法自动跳过坏块,那么就需要通过 MTD 子系统提供的字符类设备 /dev/mtdN 来进行编程:

1. 通过 ioctl(MEMGETBADBLOCK) 判定设备的坏块,写入遇到坏块时跳过;
2. 写数据时进行数据同步,从软件层面确保写入操作完成时,数据已经到达存储设备。

到此,对于本文所叙问题,已经给出了一个解决方案,这不一定是唯一或者最好的解决方案,甚至在实际场景都不一定是正确的解决方案,一切都需要时间来验证。要注意的是,syncdirect IO 都可能导致性能损失,这需要权衡是否能够用在实际环境中。

6. 参考资料

[1] linux IO Block layer 解析
[2] https://www.man7.org/linux/man-pages/man1/dd.1.html

相关推荐

  1. Linux 网络: 网卡速度异常案例(1)

    2024-04-23 15:20:03       31 阅读
  2. Linux Nano 编辑器

    2024-04-23 15:20:03       27 阅读

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-04-23 15:20:03       98 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-04-23 15:20:03       106 阅读
  3. 在Django里面运行非项目文件

    2024-04-23 15:20:03       87 阅读
  4. Python语言-面向对象

    2024-04-23 15:20:03       96 阅读

热门阅读

  1. Thinkphp封装统一响应

    2024-04-23 15:20:03       33 阅读
  2. linux 驱动-匹配1 (platform_bus_type)

    2024-04-23 15:20:03       38 阅读
  3. 【根据消息类型实现订阅发布模型】

    2024-04-23 15:20:03       36 阅读
  4. vue中动态引入图片地址需要用require

    2024-04-23 15:20:03       31 阅读
  5. layui数据表格横向滚动条不显示问题

    2024-04-23 15:20:03       27 阅读
  6. cmake命令使用总结

    2024-04-23 15:20:03       28 阅读
  7. 大数据分析:使用Spark和Hadoop的实用指南

    2024-04-23 15:20:03       32 阅读
  8. R语言 数据的整理与清洗(第一篇)

    2024-04-23 15:20:03       35 阅读
  9. 信号量Semaphore

    2024-04-23 15:20:03       28 阅读
  10. 4.2 Python列表(list)

    2024-04-23 15:20:03       38 阅读
  11. OSPF的七种LSA类型

    2024-04-23 15:20:03       34 阅读
  12. 美国公司注册及美国MSB牌照申请流程

    2024-04-23 15:20:03       33 阅读
  13. MyBatis `<foreach>`

    2024-04-23 15:20:03       40 阅读