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

news2024/11/27 14:37:46

文章目录

  • 1. 前言
  • 2. 案例背景
  • 3. 案例问题
  • 4. 案例分析
    • 4.1 普通文件写入流程概要
    • 4.2 dd 写 NAND 时,会不会使用 page cache ?
    • 4.3 dd 写 NAND 时,对比 U-Boot 读 NAND,是否采用了相同的坏块策略 ?
      • 4.3.1 U-Boot 读 NAND 过程中遇坏块的处理策略
      • 4.3.2 Linux 写 NAND 过程中遇坏块的处理策略
  • 5. 解决方案
    • 5.1 避免数据在 page cache 逗留
    • 5.2 数据写入同步
    • 5.3 将 Linux 内核写入坏块处理 和 U-Boot 保持一直:跳过坏快
  • 6. 参考资料

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1603928.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【经验总结】Jupyter 配置内核

1. 背景描述 使用 国家超算互联网中心 的服务器&#xff0c;创建 jupyterlab 容器&#xff0c;想在之前 conda 创建的环境中运行&#xff0c;可是不行&#xff0c;进入容器就直接进入 jupyterlab 2. 解决方法 配置内核 2.1 激活环境 conda activate peft2.2 安装内核 pip…

【Python基础】字典

文章目录 [toc]什么是字典键值对示例键异常 遍历列表什么是遍历遍历字典的键keys()方法 遍历字典的值values()方法 遍历字典的键值对items()方法 字典操作增加键值对修改键值对查询键值对get()方法 删除键值对delclear()方法 个人主页&#xff1a;丷从心 系列专栏&#xff1a;…

牛客Linux高并发服务器开发学习第二天

Gcc编译 利用gcc 生成应用时如果不加-o 和应用名&#xff0c;默认生成a.out 可以用./ a.out打开 Gcc工作流程 可执行程序Windows系统中为.exe Linux系统中为.out g也可以编辑c程序 gcc也可以编译cpp代码&#xff0c;只是在编译阶段gcc不能自动共和C程序使用的库进行联接&…

kettle从入门到精通 第五十三课 ETL之kettle MQTT/RabbitMQ producer 实战

1、MQTT介绍 MQTT (Message Queuing Telemetry Transport) 是一种轻量级的消息传输协议&#xff0c;设计用于连接低带宽、高延迟或不可靠网络的设备。 MQTT 是基于发布/订阅模式&#xff08;Publish/Subscribe&#xff09;的协议&#xff0c;其中设备可以发布消息到一个主题&…

【Linux系列】Ctrl + R 的使用

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…

Eland上传bge-large-zh-v1.5向量化模型到ElasticSearch中

最近需要做一些向量检索&#xff0c;试试ES 一、准备 系统&#xff1a;MacOS 14.3.1 ElasticSearch&#xff1a;8.13.2 Kibana&#xff1a;8.13.2 本地单机环境&#xff0c;无集群&#xff0c;也不基于Docker BGE是一个常见的文本转向量的模型&#xff0c;在很多大模型RAG应…

SpringBootSpringCloud升级可能会出现的问题

1.背景 之前负责过我们中台的SpringBoot和Cloud的升级&#xff0c;特次记录分享一下项目中可能出现的问题&#xff0c;方便后续的人快速定位问题。以及下述选择的解决方案都是基于让升级的服务影响和改动最小以及提供通用的解决方案的提前进行选择的。 1.1版本说明 升级前&a…

js进阶 事件循环(持续更新)

导入 js是单线程&#xff0c;同一时间只能做一件事&#xff0c;事件循环(EventLoop)来打破这个局面 异步任务 ajax网络请求setTimeout定时函数 简易粗糙的事件循环 同步任务进入主线程主执行栈异步任务进入任务队列主任务栈任务执行完毕&#xff0c;从任务队列读取对应任务…

npm怎么迁移到pnpm

下载的vue3模板用到了pnpm&#xff0c;就安装了一下 但是安装之后使用pnpm install 就发现包全被移动到ignored文件夹下面了,还报错 PS G:\Projects\gitProeject\TS_front> pnpm installWARN  Moving commitlint/config-conventional that was installed by a different …

中兴F7607P自启动程序,关闭JAVA插件

本文目的&#xff1a;关闭光猫内自动运行的JAVA插件&#xff0c;并实现开机自动调用用户的程序启动 移动定制版F7607P不带LXC容器&#xff0c;取而代之的是JAVA虚拟机&#xff0c;内置多个插件&#xff0c;包括名为CMCCDPI的插件&#xff0c;用途可以从名字上窥见。机器rootfs分…

快速上手Linux核心命令

Linux 的重要性不用我多说了吧&#xff0c;大多数互联网公司&#xff0c;服务器都是采用的Linux操作系统 Linux是一个主要通过命令行来进行管理的操作系统。 只有熟练掌握Linux核心命令&#xff0c;在使用起来我们才会得心应手 这里给大家整理了Linux一些核心命令&#xff0…

一些docker安装配置以及常见命令

​常用命令 docker 命令 //进去容器内部&#xff0c;找到需要拷贝的文件及目录 docker exec -it 2c2600fb60f8 /bin/bash ​ //将container id为4db8edd86202的容器内elasticsearch.yml文件拷贝到宿主机指定目录下&#xff1a; docker cp 4db8edd86202:/usr/share/elasticsea…

Spring Boot:Web应用开发之登录与退出的实现

Spring Boot 前言实现登录功能配置拦截器 实现退出功能 前言 登录与退出功能作为 Web 应用中的基础且重要的组成部分&#xff0c;直接关系到用户的安全和隐私保护。通过实现登录与退出功能&#xff0c;可以对用户的身份进行验证和授权&#xff0c;确保只有合法的用户才能访问特…

数据链路层(上):以太网、二层交换机和网络风暴

目录 数据链路层知识概览 数据链路层设备 1、二层交换机 2、拓展&#xff1a;二层交换机与三层交换机有啥区别&#xff1f; 3、广播风暴 4、交换机以太网接口的工作模式 数据链路层的功能 数据链路层--以太网 1、以太网是什么&#xff1f; 2、以太网地址 数据链路层知…

MediaStream使用webRtc多窗口传递

最近在做音视频通话&#xff0c;有个需求是把当前会话弄到另一个窗口单独展示&#xff0c;但是会话是属于主窗口的&#xff0c;多窗口通信目前不能直接传递对象&#xff0c;所以想着使用webRtc在主窗口和兄弟窗口建立连接&#xff0c;把主窗口建立会话得到的MediaStream传递给兄…

系统稳定性建设

说到系统稳定性&#xff0c;不知道大家会想起什么&#xff1f;大多数人会觉得这个词挺虚的&#xff0c;不知道系统稳定性指的是什么。 一年前看到这个词&#xff0c;也是类似于这样的感受&#xff0c;大概只知道要消除单点、做好监控报警&#xff0c;但却并没有一个体系化的方…

ChatGLM-6B的部署步骤

2022年8月&#xff0c;清华背景的智谱AI基于GLM框架&#xff0c;正式推出拥有1300亿参数的中英双语稠密模型 GLM-130B(论文地址、代码地址&#xff0c;论文解读之一&#xff0c;GLM-130B is trained on a cluster of 96 DGX-A100 GPU (840G) servers with a 60-day&#xff0c;…

【Excel如何在表格中筛选重复的值之条件格式】

在使用excel进行统计时经常会遇到&#xff0c;数据统计出现重复的现象&#xff0c;为了确保数据的唯一性&#xff0c;可以用到条件格式筛选出重复值&#xff0c;以确保数据的正确性。 筛选重复值&#xff1a; 选中要筛选的范围&#xff0c;行或列或整个表选中【开始】-【条件…

记录一次k8s pod之间ip无法访问,问题排查与定位

记录一次k8s pod之间ip无法访问&#xff0c;问题排查与定位 问题展现现象 node之间通信正常 部分node上的pod无法通信 排查有问题node 使用启动网络测试工具 环境准备 docker 数据库mysql 使用有状态副本集合 --- apiVersion: apps/v1 kind: StatefulSet metadata:anno…

MATLAB实现图片栅格化

MATLAB实现图片栅格化 1.读取图片&#xff1a;首先&#xff0c;你需要使用imread函数读取要栅格化的图片。 2.设置栅格大小&#xff1a;确定你希望将图片划分成的栅格大小&#xff0c;即每个栅格的宽度和高度。 3.计算栅格数量&#xff1a;根据图片的总尺寸和栅格大小&#…