jklincn


Linux write 系统调用源码分析(Linux-6.6.46)


未完待续,暂作保存!


从用户态到内核态

此部分和 Linux open 系统调用源码分析(Linux-6.6.46) 提到的相似,write() 函数的定义位于 unistd.h(系统路径一般为 /usr/include/unistd.h)

1
2
extern ssize_t write (int __fd, const void *__buf, size_t __n) __wur
    __attr_access ((__read_only__, 2, 3));

在 glibc-2.35/sysdeps/unix/sysv/linux/write.c 中有

1
2
3
4
5
ssize_t
__libc_write (int fd, const void *buf, size_t nbytes)
{
  return SYSCALL_CANCEL (write, fd, buf, nbytes);
}

其系统调用号为 1

1
#define __NR_write 1

内核接收到一个系统调用后,根据系统调用号进行分发,write() 系统调用在内核的实现是 sys_write(),在文件 include/linux/syscalls.h 中定义

1
2
asmlinkage long sys_write(unsigned int fd, const char __user *buf,
			  size_t count);

其实现位于 fs/readwrite.c 中(宏 SYSCALL_DEFINE3 会生成一个带有 “sys” 前缀的函数)

1
2
3
4
5
SYSCALL_DEFINE3(write, unsigned int, fd, const char __user *, buf,
		size_t, count)
{
	return ksys_write(fd, buf, count);
}

可以看到 sys_write() 会调用 ksys_write() 来完成实际的工作。

虚拟文件系统(VFS)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
ssize_t ksys_write(unsigned int fd, const char __user *buf, size_t count)
{
	struct fd f = fdget_pos(fd); // 根据文件描述符获取文件相关的信息,返回 fd 结构体
	ssize_t ret = -EBADF; // 初始化返回值为 -EBADF,表示文件描述符错误

	if (f.file) { // 检查文件描述符的有效性
		loff_t pos, *ppos = file_ppos(f.file); // 获取当前文件偏移量
		if (ppos) { // 使用临时变量 pos 而不是 *ppos
			pos = *ppos;
			ppos = &pos;
		}
		ret = vfs_write(f.file, buf, count, ppos); // 执行写操作,这里偏移量使用的是 pos 变量而不是 file->f_pos
		if (ret >= 0 && ppos) // 如果写入成功
			f.file->f_pos = pos; // 更新文件偏移量
		fdput_pos(f); // 释放之前通过 fdget_pos(fd) 获取的资源
	}

	return ret; // 如果成功,则返回写入的字节数;如果失败,则返回负的错误代码
}

// 如果文件以流的模式打开(FMODE_STREAM),则文件没有明确的偏移量,此时 file_ppos 返回 NULL。
// 流式文件包括如管道、套接字等,它们没有明确的起始位置或偏移量,数据是按顺序处理的,而无需关心当前位置。
static inline loff_t *file_ppos(struct file *file)
{
	return file->f_mode & FMODE_STREAM ? NULL : &file->f_pos;
}

ksys_write() 工作内容小结:

  1. 检查文件描述符的有效性。
  2. 获取当前文件的偏移量。
  3. 调用 VFS 层的 vfs_write() 进行写入操作。
  4. 更新文件偏移量。
  5. 释放资源并返回写入的字节数或错误代码。

这里有两个重要的结构体,定义在 include/linux/file.h 中

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
struct fd {
	struct file *file; // 指向 file 结构体的指针,表示打开文件的具体信息
	unsigned int flags; // 存储与文件描述符相关的标志,比如 O_RDONLY
};

struct file {
	union { // 表示 file 在不同情况下可能使用的不同数据结构
		struct llist_node	f_llist; // 链表节点,可能用于将文件连接到一个链表中
		struct rcu_head 	f_rcuhead; // RCU 头部,用于处理文件的 RCU 机制
		unsigned int 		f_iocb_flags; // I/O 控制块的标志
	};

	spinlock_t		f_lock; // 自旋锁,用于保护文件的 f_ep 和 f_flags 字段,确保多线程环境下的安全访问
	fmode_t			f_mode; // 文件的打开模式(只读、只写、读写等)
	atomic_long_t		f_count; // 引用计数,当减为 0 时,文件将被关闭。
	struct mutex		f_pos_lock; // 互斥锁,用于保护文件的 f_pos 字段,避免并发读写操作时位置的混乱
	loff_t			f_pos; // 文件位置指针,宝石当前读/写的偏移量
	unsigned int		f_flags; // 文件标志位,记录文件的状态标志(例如是否为非阻塞操作)
	struct fown_struct	f_owner; // 所有者信息,通常用于信号通知和异步 I/O 操作
	const struct cred	*f_cred; // 文件的凭证,表示文件的访问权限相关信息
	struct file_ra_state	f_ra; // 文件的预读状态
	struct path		f_path; // 文件的路径信息
	struct inode		*f_inode; // 指向 inode 结构体,表示文件在文件系统中的具体对象
	const struct file_operations	*f_op; // 文件操作函数表的指针,这些操作包括读、写、打开、关闭等

	u64			f_version; // 文件的版本信息,通常用于检测文件的变化
#ifdef CONFIG_SECURITY
	void			*f_security; // 指向与安全相关的数据
#endif
	void			*private_data; // 驱动程序或文件系统特定的数据指针,允许文件系统或设备驱动在 file 结构体中存储私有数据

#ifdef CONFIG_EPOLL
	struct hlist_head	*f_ep; // 用于 epoll 机制,将文件与事件关联
#endif /* #ifdef CONFIG_EPOLL */
	struct address_space	*f_mapping; // 指向文件的地址空间,用于内存映射
	errseq_t		f_wb_err; // 记录文件系统写回相关的错误状态
	errseq_t		f_sb_err; // 记录文件系统同步相关的错误状态
} __randomize_layout // 增强安全性的编译器属性,随机化结构体成员的顺序
  __attribute__((aligned(4))); // 指定数据对齐方式的编译器属性,按照 4 字节对齐,提高访问速度

继续看 vfs_write() 函数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
ssize_t vfs_write(struct file *file, const char __user *buf, size_t count, loff_t *pos)
{
	ssize_t ret;

	if (!(file->f_mode & FMODE_WRITE)) // 检查文件是否以可写模式打开
		return -EBADF;
	if (!(file->f_mode & FMODE_CAN_WRITE)) // 检查文件是否允许写操作
		return -EINVAL;
	if (unlikely(!access_ok(buf, count))) // 检查用户提供的缓冲区是否在用户空间的有效内存范围内
		return -EFAULT;

    // 验证写操作的区域是否有效,比如是否超出文件大小限制。如果验证失败,返回对应的错误码
	ret = rw_verify_area(WRITE, file, pos, count);
	if (ret)
		return ret;

    // 限制写入大小。如果超出单次读写操作的最大字节数,则进行截断
	if (count > MAX_RW_COUNT)
		count =  MAX_RW_COUNT;

	file_start_write(file); // 通知文件系统层,开始写操作
	if (file->f_op->write) // 如果文件系统实现了 write,则调用这个函数来执行写入
		ret = file->f_op->write(file, buf, count, pos);
	else if (file->f_op->write_iter) // 如果文件系统没有实现传统的 write,但实现了 write_iter,则调用 new_sync_write 来处理
		ret = new_sync_write(file, buf, count, pos);
	else
		ret = -EINVAL; // 如果文件系统既没有实现 write 也没有实现 write_iter,则返回错误。
	if (ret > 0) { // 如果写入成功
		fsnotify_modify(file); // 通知内核的文件系统监控机制,文件内容已经被修改
		add_wchar(current, ret); // 更新进程统计数据:将写入的字节数累加到当前进程的写字节统计数据中
	}
	inc_syscw(current); // 递增当前进程的系统写操作计数
	file_end_write(file); // 通知文件系统层,写操作已完成
	return ret;
}

vfs_write() 工作内容小结:

  1. 文件权限和写入区域检查。
  2. 根据文件系统的具体实现,调用适当的写操作函数来执行实际的数据写入。
  3. 如果写入成功,通知文件系统的监控机制,并更新进程的 I/O 统计数据。
  4. 返回写入的字节数或错误码。

关于 write 和 write_iter

write 是传统的文件写操作接口,定义为 file_operations 结构体中的一个函数指针。它是最基本的写接口,用于将数据从用户空间写入到内核中的文件系统。

write_iter 是一个更为现代化和灵活的写操作接口,是 file_operations 结构体中的另一个函数指针。它支持分散/聚集 I/O(Scatter/Gather I/O),允许在一次操作中处理多个非连续的数据块。

特性 write write_iter
接口复杂度 简单,适用于基础写操作 复杂,适用于高级和高性能 I/O 操作
I/O 模式 基本的顺序写操作 支持分散/聚集 I/O,异步 I/O
数据来源 单一用户空间缓冲区 多个 I/O 向量,支持复杂数据结构
适用场景 适用于简单文件系统或基础场景 适用于复杂、高性能文件系统
性能 对基本操作性能足够 更高效,尤其在处理复杂 I/O 时

假设当前的文件系统是 ext4,可以在 fs/ext4/file.c 中找到其具体实现。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
const struct file_operations ext4_file_operations = {
	.llseek		= ext4_llseek,
	.read_iter	= ext4_file_read_iter,
	.write_iter	= ext4_file_write_iter,
	.iopoll		= iocb_bio_iopoll,
	.unlocked_ioctl = ext4_ioctl,
#ifdef CONFIG_COMPAT
	.compat_ioctl	= ext4_compat_ioctl,
#endif
	.mmap		= ext4_file_mmap,
	.mmap_supported_flags = MAP_SYNC,
	.open		= ext4_file_open,
	.release	= ext4_release_file,
	.fsync		= ext4_sync_file,
	.get_unmapped_area = thp_get_unmapped_area,
	.splice_read	= ext4_file_splice_read,
	.splice_write	= iter_file_splice_write,
	.fallocate	= ext4_fallocate,
};

可以看到 ext4 中没有定义 write 指针,而是使用了 write_iter 这个更通用的接口,所以 vfs_write() 之后是 new_sync_write() 函数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
static ssize_t new_sync_write(struct file *filp, const char __user *buf, size_t len, loff_t *ppos)
{
	struct kiocb kiocb;
	struct iov_iter iter;
	ssize_t ret;

	init_sync_kiocb(&kiocb, filp); // 初始化 kiocb 结构体
	kiocb.ki_pos = (ppos ? *ppos : 0); // 设置文件偏移量
	iov_iter_ubuf(&iter, ITER_SOURCE, (void __user *)buf, len); // 初始化 iov_iter 结构体

	ret = call_write_iter(filp, &kiocb, &iter); // 调用具体文件系统的 write_iter 函数
	BUG_ON(ret == -EIOCBQUEUED); // 确保写操作没有进入异步 I/O 队列(因为这是一个同步写操作函数)

	// 如果写入成功并且 ppos 非空,更新 ppos 为写入后的文件位置。
    if (ret > 0 && ppos)
		*ppos = kiocb.ki_pos;
	return ret;
}

new_sync_write() 工作内容小结:

  1. 初始化 I/O 控制块。
  2. 设置文件位置。
  3. 初始化迭代器。
  4. 调用文件系统的 write_iter 方法。
  5. 检查返回值并更新文件位置。

这里又出现了两个新结构体:

  • kiocb 是内核中用于描述异步 I/O 操作的结构体,定义在 include/linux/fs.h 中。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    
    struct kiocb {
    	struct file		*ki_filp; // 指向与该 I/O 操作关联的文件结构体指针
    	loff_t			ki_pos; // 表示 I/O 操作的文件位置(偏移量)
    	void (*ki_complete)(struct kiocb *iocb, long ret); // 完成处理函数指针。当 I/O 操作完成时,内核会调用这个函数
    	void			*private; // 私有数据指针。可以存储与该 I/O 操作相关的自定义数据
    	int			ki_flags; // I/O 操作的标志位,控制或指示 I/O 操作的特定行为(例如是否是直接 I/O、是否是异步操作等)
    	u16			ki_ioprio; // I/O 操作的优先级
    	union { // 这个联合体为不同的 I/O 操作模式提供额外的信息存储
            // 当执行异步缓冲读取时,如果需要等待某些页面完成,ki_waitq 指向相关的页面等待队列。
            // 只有在设置了IOCB_WAITQ 标志时,这个字段才有效。
    		struct wait_page_queue	*ki_waitq;
    		// 这是直接 I/O 操作的完成回调函数指针。
            // 它用于在 I/O 完成后通知调用者,并且该函数会处理直接 I/O 操作的完成逻辑。
            // 这个字段仅在 IOCB_DIO_CALLER_COMP 标志设置时有效。
    		ssize_t (*dio_complete)(void *data);
    	};
    };
    
  • iov_iter 是一个描述用户空间缓冲区的迭代器,用于管理和遍历写入操作的数据源,定义在 include/linux/uio.h 中

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    
    struct iov_iter {
    	u8 iter_type; // 迭代器的类型。不同的类型定义了 iov_iter 如何处理数据源,包括用户空间缓冲区、内核缓冲区、BIO 缓冲区等。
    	bool copy_mc; // 用于控制在数据复制过程中是否使用基于内存的错误检查。它主要用于增强数据传输的可靠性。
    	bool nofault; // 当设为 true 时,表示在访问数据源时不允许触发页面错误,这对实时性要求高的操作非常重要。
    	bool data_source; // 指示迭代器是用于读取数据还是写入数据。它帮助内核正确处理 I/O 操作的方向。
    	bool user_backed; // 表示数据源是否位于用户空间。如果是用户空间数据,内核在访问时需要特别注意安全性和有效性。
    	union { // 用于存储当前迭代的偏移量或最后一个偏移量,具体用途取决于迭代器的类型。
    		size_t iov_offset; // 表示在当前 I/O 向量中的偏移量。
    		int last_offset; // 通常用于某些特定类型的迭代器中,跟踪最后处理的偏移。
    	};
    	union { // 这是一个嵌套联合体,用于描述数据源的各种类型和结构。
    		struct iovec __ubuf_iovec; // 用于表示一个通用的用户空间 I/O 向量。
    		struct {
    			union { // 根据 iter_type 不同,包含了不同的数据源描述符。
    				// 可以使用 iter_iov() 获取当前向量
    				const struct iovec *__iov; // 表示用户空间的 I/O 向量数组
    				const struct kvec *kvec; // 表示内核空间的 I/O 向量数组
    				const struct bio_vec *bvec; // 表示块设备的 BIO 向量数组
    				struct xarray *xarray; // 表示一个通用的内核对象数组
    				void __user *ubuf; // 表示一个通用的用户空间缓冲区指针
    			};
    			size_t count; // 表示 iov_iter 迭代器中当前还剩余多少字节的数据需要处理
    		};
    	};
    	union { // 这个联合体用于存储与当前迭代相关的段数或起始偏移量
    		unsigned long nr_segs; // 表示当前 I/O 操作涉及的段数(如 I/O 向量的数量)
    		loff_t xarray_start; // 表示在 xarray 中的起始偏移量,用于处理复杂的内核对象数组。
    	};
    };
    

先看一下 init_sync_kiocb() 函数,它初始化了三个字段:文件指针、I/O 标志、当前的 I/O 优先级。

1
2
3
4
5
6
7
8
static inline void init_sync_kiocb(struct kiocb *kiocb, struct file *filp)
{
	*kiocb = (struct kiocb) {
		.ki_filp = filp,
		.ki_flags = filp->f_iocb_flags,
		.ki_ioprio = get_current_ioprio(),
	};
}

再来看一下 iov_iter_ubuf() 函数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
static inline void iov_iter_ubuf(struct iov_iter *i, unsigned int direction,
			void __user *buf, size_t count)
{
	WARN_ON(direction & ~(READ | WRITE));
	*i = (struct iov_iter) {
		.iter_type = ITER_UBUF, // 表示这是一个用户空间缓冲区的迭代器
		.copy_mc = false, // 表示这个迭代器不涉及复制内存缓存的操作
		.user_backed = true, // 表示这个迭代器的数据来自用户空间
		.data_source = direction, // 指定了数据的传输方向(读取或写入)
		.ubuf = buf, // 设置为传入的用户空间缓冲区地址 buf
		.count = count, // 设置为要操作的数据字节数
		.nr_segs = 1 // 表示这是一个单一的数据段
	};
}

以上两个结构体本质上都是对读写操作信息的封装,其目的是简化和标准化 I/O 操作的数据处理方式,提高了内核代码的灵活性和可重用性。从现在起,write 过程就不直接继续往下传递 buf、len、ppos 这些参数了(前两个封装在 iov_iter 中,最后一个在 kiocb 中) 。

ext4 文件系统

下一步的 call_write_iter() 是对具体文件系统 write_iter() 操作接口的调用。

1
2
3
4
5
static inline ssize_t call_write_iter(struct file *file, struct kiocb *kio,
				      struct iov_iter *iter)
{
	return file->f_op->write_iter(kio, iter);
}

个人想法:因为在 init_sync_kiocb() 中已经将 filp 赋给了 kio 的 ki_filp 字段,因此这个函数是不是可以简化成以下形式?

1
2
3
4
static inline ssize_t call_write_iter(struct kiocb *kio,struct iov_iter *iter)
{
	return kio->ki_filp->f_op->write_iter(kio, iter);
}

另外一个吐槽点:同样是文件结构体的指针,在 new_sync_write() 中参数命名为 filp,在 call_write_iter() 中却是 file ……

在 ext4 文件系统中会调用 ext4_file_write_iter()

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
static ssize_t
ext4_file_write_iter(struct kiocb *iocb, struct iov_iter *from)
{
	struct inode *inode = file_inode(iocb->ki_filp); // 获取 inode

    // 检查文件系统是否强制关闭
	if (unlikely(ext4_forced_shutdown(inode->i_sb)))
		return -EIO;

    // 如果文件系统支持 DAX(直接访问)模式,并且当前 inode 使用 DAX,则执行 DAX 模式下的写操作。
#ifdef CONFIG_FS_DAX
	if (IS_DAX(inode))
		return ext4_dax_write_iter(iocb, from);
#endif

    // 根据 kiocb 的标志位决定是执行直接 I/O(不经过页缓存)还是缓存 I/O(经过页缓存)
	if (iocb->ki_flags & IOCB_DIRECT)
		return ext4_dio_write_iter(iocb, from);
	else
		return ext4_buffered_write_iter(iocb, from);
}

ext4_file_write_iter() 工作内容小结:

  1. 检查文件系统状态。
  2. 处理 DAX 模式。
  3. 根据标志位选择写入模式。

可以看到 ext4_file_write_iter() 其实也是一个封装函数,真正的执行还要根据是否是缓存 I/O 分类继续向下执行。

这里我们看默认情况下没有设置 O_DIRECT 标志的情况,即写入操作是经过页缓存的缓存 I/O,那就来到了 ext4_buffered_write_iter() 函数。直接 I/O 后续再补坑。

页缓存处理

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
static ssize_t ext4_buffered_write_iter(struct kiocb *iocb,
					struct iov_iter *from)
{
	ssize_t ret;
	struct inode *inode = file_inode(iocb->ki_filp); // 获取 inode

    // 检查非阻塞标志
	if (iocb->ki_flags & IOCB_NOWAIT)
		return -EOPNOTSUPP;

	inode_lock(inode); // 对 inode 加锁,避免并发引发的冲突
	ret = ext4_write_checks(iocb, from); // 执行写前检查,包括检查写入的有效性、权限、文件系统状态等
	if (ret <= 0)
		goto out; // 如果检查失败,则跳过写操作,直接解锁 inode 并返回错误代码。

	ret = generic_perform_write(iocb, from); // 执行通用的写操作

out:
	inode_unlock(inode); // 解锁 inode
	if (unlikely(ret <= 0))
		return ret;
	return generic_write_sync(iocb, ret); // 进行写操作的同步,确保数据写入磁盘。
}

ext4_buffered_write_iter() 工作内容小结:

  1. 检查操作是否支持非阻塞模式
  2. 锁定 inode,确保写操作期间文件的完整性
  3. 调用 ext4 特定的检查函数验证写操作
  4. 执行实际的写操作,将数据写入页缓存。
  5. 解锁 inode,并调用通用函数进行数据同步,确保数据持久化到磁盘。

写入页缓存

在 Linux 内核中,文件系统的写操作通常涉及到多种处理步骤,包括从用户空间复制数据到页缓存、更新文件系统的元数据、同步数据到磁盘等。不同的文件系统(如 ext4、xfs、btrfs)可能会有特定的优化和处理逻辑,但它们也共享一些通用的写操作处理流程。使用通用写处理函数的好处有很多,比如代码重用、简化文件系统的实现、保持文件系统一致性等。

这里 ext4 就调用了内核的通用写操作处理函数 generic_perform_write(),其实现位于 mm/filemap.c 中。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
ssize_t generic_perform_write(struct kiocb *iocb, struct iov_iter *i)
{
	struct file *file = iocb->ki_filp; // 从 kiocb 中获取文件结构体
	loff_t pos = iocb->ki_pos; // 从 kiocb 中获取当前写入位置
	struct address_space *mapping = file->f_mapping; // 文件的地址空间映射
	const struct address_space_operations *a_ops = mapping->a_ops; // 地址空间操作函数指针,处理页缓存的读写操作
	long status = 0;
	ssize_t written = 0;

	do {
		struct page *page;
		unsigned long offset;	// 当前写入位置在页中的偏移
		unsigned long bytes;	// 当前写入的字节数
		size_t copied;		    // 从用户空间复制到页缓存的字节数
		void *fsdata = NULL;    // 用于在 write_begin 和 write_end 之间传递文件系统特定的数据

		offset = (pos & (PAGE_SIZE - 1)); // 计算当前写入位置在页中的偏移
        // 计算当前写入的字节数,确保不会超出页的大小和 iov_iter 中剩余的字节数。
		bytes = min_t(unsigned long, PAGE_SIZE - offset,
						iov_iter_count(i));

again:
        // 检查用户空间缓冲区的有效性
		if (unlikely(fault_in_iov_iter_readable(i, bytes) == bytes)) {
			status = -EFAULT;
			break;
		}

        // 检查当前线程是否有待处理的信号
		if (fatal_signal_pending(current)) {
			status = -EINTR;
			break;
		}

        // 开始写操作
		status = a_ops->write_begin(file, mapping, pos, bytes,
						&page, &fsdata);
		if (unlikely(status < 0))
			break;

        // 检查文件的地址空间是否以写入模式映射
        // 如果文件的页缓存被映射为写入模式(writably mapped),则必须调用 flush_dcache_page 函数来确保数据一致性,避免读到过时的数据。
		if (mapping_writably_mapped(mapping))
			flush_dcache_page(page);

        // 将数据从用户空间缓冲区复制到页缓存中
		copied = copy_page_from_iter_atomic(page, offset, bytes, i);
		flush_dcache_page(page); // 强制刷新页面缓存中的数据,确保数据缓存的一致性

        // 结束写操作
		status = a_ops->write_end(file, mapping, pos, bytes, copied,
						page, fsdata);

        // 检查 status 是否与 copied 不一致
        // 如果不一致(不太可能),表示 write_end 函数的结果与实际复制的数据量不符。
		if (unlikely(status != copied)) {
            // 恢复 iov_iter 状态,撤销已经完成的部分写入,以确保数据的一致性
			iov_iter_revert(i, copied - max(status, 0L));
            // 如果 status 为负数(不太可能),表示发生了错误,退出循环,停止进一步的写入操作
			if (unlikely(status < 0))
				break;
		}
        // 进行调度,让其他任务有机会运行
		cond_resched();

        // 处理 status 为 0 的特殊情况,这有可能是内存中毒、与 munmap 竞争、严重的内存压力
		if (unlikely(status == 0)) {
			// 如果在当前页缓存中实际复制了数据 (copied 非零),更新 bytes 为 copied。
            // 这表示虽然 write_end 失败了,但实际上可能已经有部分数据被复制了。
			if (copied)
				bytes = copied;
           	// 重新进入循环,尝试再次执行写入操作
			goto again;
		}
		pos += status; // 更新写入的位置
		written += status; // 更新累计写入的字节数

		balance_dirty_pages_ratelimited(mapping); // 脏页处理
	} while (iov_iter_count(i));

    // 如果写入了数据,更新 iocb 中的文件位置,并返回写入的字节数
    // 如果没有写入数据,返回状态码
	if (!written)
		return status;
	iocb->ki_pos += written;
	return written;
}

这里我们先看两个变量:

  • struct address_space *mapping:address_space 结构体表示文件在内存中的地址空间,它管理着文件的内存页(也就是页缓存)
  • const struct address_space_operations *a_ops:定义了文件系统中与文件的地址空间相关的一组操作方法。这些方法主要用于管理和操作文件的内存映射(即文件内容在内存中的表示),包括读取、写入、同步、释放页等操作。

在写操作的过程中分别调用了 a_ops->write_begin() 和 a_ops->write_end(),这些也是和具体的文件系统相关,在 ext4 中的定义位于 fs/ext4/inode.c 。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
static const struct address_space_operations ext4_journalled_aops = {
	.read_folio		= ext4_read_folio,
	.readahead		= ext4_readahead,
	.writepages		= ext4_writepages,
	.write_begin		= ext4_write_begin,
	.write_end		= ext4_journalled_write_end,
	.dirty_folio		= ext4_journalled_dirty_folio,
	.bmap			= ext4_bmap,
	.invalidate_folio	= ext4_journalled_invalidate_folio,
	.release_folio		= ext4_release_folio,
	.direct_IO		= noop_direct_IO,
	.migrate_folio		= buffer_migrate_folio_norefs,
	.is_partially_uptodate  = block_is_partially_uptodate,
	.error_remove_page	= generic_error_remove_page,
	.swap_activate		= ext4_iomap_swap_activate,
};

可以看到两个函数的实现分别是 ext4_write_begin() 和 ext4_journalled_write_end()(ext4 文件系统默认开启日志模式,下文会提及)

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
static int ext4_write_begin(struct file *file, struct address_space *mapping,
			    loff_t pos, unsigned len,
			    struct page **pagep, void **fsdata)
{
	struct inode *inode = mapping->host;
	int ret, needed_blocks;
	handle_t *handle;
	int retries = 0;
	struct folio *folio;
	pgoff_t index;
	unsigned from, to;

    // 检查文件系统是否被强制关闭
	if (unlikely(ext4_forced_shutdown(inode->i_sb)))
		return -EIO;

	trace_ext4_write_begin(inode, pos, len);

    // 计算需要的块数,+1 是为了预留可能需要的块,用于处理写入失败时将文件添加到孤儿列表中。
	needed_blocks = ext4_writepage_trans_blocks(inode) + 1;
	index = pos >> PAGE_SHIFT;
	from = pos & (PAGE_SIZE - 1);
	to = from + len;

    // 检查是否可以进行内联数据写入
	if (ext4_test_inode_state(inode, EXT4_STATE_MAY_INLINE_DATA)) {
		ret = ext4_try_to_write_inline_data(mapping, inode, pos, len,
						    pagep);
		if (ret < 0)
			return ret;
		if (ret == 1)
			return 0;
	}

	/*
	 * __filemap_get_folio() can take a long time if the
	 * system is thrashing due to memory pressure, or if the folio
	 * is being written back.  So grab it first before we start
	 * the transaction handle.  This also allows us to allocate
	 * the folio (if needed) without using GFP_NOFS.
	 */
retry_grab:
    // 获取文件页
	folio = __filemap_get_folio(mapping, index, FGP_WRITEBEGIN,
					mapping_gfp_mask(mapping));
	if (IS_ERR(folio))
		return PTR_ERR(folio);
	/*
	 * The same as page allocation, we prealloc buffer heads before
	 * starting the handle.
	 */
    // 准备缓冲区
	if (!folio_buffers(folio))
		create_empty_buffers(&folio->page, inode->i_sb->s_blocksize, 0);

	folio_unlock(folio);

retry_journal:
    // 启动事务,ext4 是日志文件系统,写入操作必须启动一个事务,以便在发生错误时可以回滚。如果事务启动失败,返回错误。
	handle = ext4_journal_start(inode, EXT4_HT_WRITE_PAGE, needed_blocks);
	if (IS_ERR(handle)) {
		folio_put(folio);
		return PTR_ERR(handle);
	}

	folio_lock(folio);
	if (folio->mapping != mapping) {
		/* The folio got truncated from under us */
		folio_unlock(folio);
		folio_put(folio);
		ext4_journal_stop(handle);
		goto retry_grab;
	}
	/* In case writeback began while the folio was unlocked */
	folio_wait_stable(folio);

#ifdef CONFIG_FS_ENCRYPTION
	if (ext4_should_dioread_nolock(inode))
		ret = ext4_block_write_begin(folio, pos, len,
					     ext4_get_block_unwritten);
	else
		ret = ext4_block_write_begin(folio, pos, len, ext4_get_block);
#else
	if (ext4_should_dioread_nolock(inode))
		ret = __block_write_begin(&folio->page, pos, len,
					  ext4_get_block_unwritten);
	else
		ret = __block_write_begin(&folio->page, pos, len, ext4_get_block);
#endif
	if (!ret && ext4_should_journal_data(inode)) {
		ret = ext4_walk_page_buffers(handle, inode,
					     folio_buffers(folio), from, to,
					     NULL, do_journal_get_write_access);
	}

	if (ret) {
		bool extended = (pos + len > inode->i_size) &&
				!ext4_verity_in_progress(inode);

		folio_unlock(folio);
		/*
		 * __block_write_begin may have instantiated a few blocks
		 * outside i_size.  Trim these off again. Don't need
		 * i_size_read because we hold i_rwsem.
		 *
		 * Add inode to orphan list in case we crash before
		 * truncate finishes
		 */
		if (extended && ext4_can_truncate(inode))
			ext4_orphan_add(handle, inode);

		ext4_journal_stop(handle);
		if (extended) {
			ext4_truncate_failed_write(inode);
			/*
			 * If truncate failed early the inode might
			 * still be on the orphan list; we need to
			 * make sure the inode is removed from the
			 * orphan list in that case.
			 */
			if (inode->i_nlink)
				ext4_orphan_del(NULL, inode);
		}

		if (ret == -ENOSPC &&
		    ext4_should_retry_alloc(inode->i_sb, &retries))
			goto retry_journal;
		folio_put(folio);
		return ret;
	}
	*pagep = &folio->page;
	return ret;
}

可以看到实际的写入是把数据从用户空间拷贝到页缓存中,即 copy_page_from_iter_atomic() 函数。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// lib/iov_iter.c
size_t copy_page_from_iter_atomic(struct page *page, size_t offset,
		size_t bytes, struct iov_iter *i)
{
	size_t n, copied = 0;

    // 确保页面和偏移量的组合是有效的
	if (!page_copy_sane(page, offset, bytes))
		return 0;

    // 确保 iov_iter 是数据源类型
	if (WARN_ON_ONCE(!i->data_source))
		return 0;

	do {
		char *p;

		n = bytes - copied;

        // 检查页面是否属于高内存区域。如果是,则需要调整页面指针和偏移量,以正确处理页面
		if (PageHighMem(page)) {
			page += offset / PAGE_SIZE;
			offset %= PAGE_SIZE;
			n = min_t(size_t, n, PAGE_SIZE - offset);
		}

        // 映射页面到内核地址空间以便操作
		p = kmap_atomic(page) + offset;

        // 在 iov_iter 中迭代并将数据复制到页面中
		iterate_and_advance(i, n, base, len, off,
			copyin(p + off, base, len),
			memcpy_from_iter(i, p + off, base, len)
		)

		kunmap_atomic(p); // 取消映射
		copied += n; // 累积已经复制的数据字节数
		offset += n; // 更新当前写入的位置
	} while (PageHighMem(page) && copied != bytes && n > 0);

	return copied; // 返回成功复制的字节数
}

这里涉及到 iterate_and_advance 以及其再包含的宏,这些宏用于处理用户空间缓冲区到内核空间的内存复制。它们还处理了不同的数据源类型,并在这些数据源之间进行迭代,更新偏移量和数据长度。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#define iterate_buf(i, n, base, len, off, __p, STEP) {		\
	size_t __maybe_unused off = 0;				\
	len = n;						\
	base = __p + i->iov_offset;				\
	len -= (STEP);						\
	i->iov_offset += len;					\
	n = len;						\
}

#define __iterate_and_advance(i, n, base, len, off, I, K) {	\
	if (unlikely(i->count < n))				\
		n = i->count;					\
	if (likely(n)) {					\
		if (likely(iter_is_ubuf(i))) {			\
			void __user *base;			\
			size_t len;				\
			iterate_buf(i, n, base, len, off,	\
						i->ubuf, (I)) 	\
		} else if (likely(iter_is_iovec(i))) {		\
			const struct iovec *iov = iter_iov(i);	\
			void __user *base;			\
			size_t len;				\
			iterate_iovec(i, n, base, len, off,	\
						iov, (I))	\
			i->nr_segs -= iov - iter_iov(i);	\
			i->__iov = iov;				\
		} else if (iov_iter_is_bvec(i)) {		\
			const struct bio_vec *bvec = i->bvec;	\
			void *base;				\
			size_t len;				\
			iterate_bvec(i, n, base, len, off,	\
						bvec, (K))	\
			i->nr_segs -= bvec - i->bvec;		\
			i->bvec = bvec;				\
		} else if (iov_iter_is_kvec(i)) {		\
			const struct kvec *kvec = i->kvec;	\
			void *base;				\
			size_t len;				\
			iterate_iovec(i, n, base, len, off,	\
						kvec, (K))	\
			i->nr_segs -= kvec - i->kvec;		\
			i->kvec = kvec;				\
		} else if (iov_iter_is_xarray(i)) {		\
			void *base;				\
			size_t len;				\
			iterate_xarray(i, n, base, len, off,	\
							(K))	\
		}						\
		i->count -= n;					\
	}							\
}

#define iterate_and_advance(i, n, base, len, off, I, K) \
	__iterate_and_advance(i, n, base, len, off, I, ((void)(K),0))

在 copy_page_from_iter_atomic 这个函数中 I 是 copyin(),而 K 是 memcpy_from_iter()。注意到之前 iov_iter 初始化时迭代器类型是 ITER_UBUF,因此这里只需要关注 if (likely(iter_is_ubuf(i))) 这个分支。也就是说,这里的 memcpy_from_iter() 并不会被使用,只需要查看 copyin() 函数。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// lib/iov_iter.c
static int copyin(void *to, const void __user *from, size_t n)
{
	size_t res = n; // 将 res 初始化为要复制的字节数 n

    // 检查用户空间复制操作是否应当失败
    // 这是一个内核调试或故障注入工具,用于模拟用户空间复制失败的情况。
	if (should_fail_usercopy())
		return n;

    // 检查用户空间地址的合法性
	if (access_ok(from, n)) {
		instrument_copy_from_user_before(to, from, n); // 在数据复制前进行插桩,用于调试
		res = raw_copy_from_user(to, from, n); // 实际执行数据复制的函数
		instrument_copy_from_user_after(to, from, n, res); // 在数据复制后进行插桩,用于调试
	}
	return res;
}

这个函数比较简单,我们继续看 raw_copy_from_user() 函数,这是 copy_user_generic() 的一个包装。这两个函数都位于 arch/x86/include/asm/uaccess_64.h 中,因此都是体系结构特定的函数,即实现取决于底层的处理器架构。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
static __always_inline __must_check unsigned long
raw_copy_from_user(void *dst, const void __user *src, unsigned long size)
{
	return copy_user_generic(dst, (__force void *)src, size);
}

static __always_inline __must_check unsigned long
copy_user_generic(void *to, const void *from, unsigned long len)
{
	stac();
	asm volatile(
		"1:\n\t"
		ALTERNATIVE("rep movsb",
			    "call rep_movs_alternative", ALT_NOT(X86_FEATURE_FSRM))
		"2:\n"
		_ASM_EXTABLE_UA(1b, 2b)
		:"+c" (len), "+D" (to), "+S" (from), ASM_CALL_CONSTRAINT
		: : "memory", "rax");
	clac();
	return len;
}

copy_user_generic() 的简单分析如下:

  • 这里 stac() 和 clac() 是两个特殊的指令,分别用于启用和禁用用户空间访问控制。
  • ALTERNATIVE 是一个内核提供的宏,用于根据 CPU 特性选择不同的代码路径:当 X86_FEATURE_FSRM 特性可用时,使用 rep movsb 指令进行数据复制,这个通过重复执行 movsb 来进行批量内存复制;否则,调用 rep_movs_alternative 这个替代函数来处理数据复制。
  • _ASM_EXTABLE_UA 宏用于处理在用户空间地址访问时可能发生的异常(如页面错误)。这里的 1b 和 2b 是标签,表示在发生异常时可以跳转的地址范围。
  • 最后是寄存器约束,定义了汇编代码的输出和输入寄存器。这里做一个简单介绍:
    • “+c” (len):使用 ecx/rcx 寄存器存储和更新 len。
    • “+D” (to):使用 rdi 寄存器存储目标地址 to。
    • “+S” (from):使用 rsi 寄存器存储源地址 from。
    • “memory”:表示这段汇编代码可能会修改内存状态,因此编译器需要在优化时考虑这一点
    • “rax”:汇编代码中使用的额外寄存器。

到这里,通过调用汇编代码终于把用户空间的数据拷贝到了页缓存中。接下来就是回过头去看看 generic_write_sync() 函数是怎么把页缓存同步到磁盘上,以完成真正的持久化操作。

页缓存同步

在 ext4_buffered_write_iter() 的最后会调用 generic_write_sync() 函数,这也是一个内核的通用处理函数。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
static inline ssize_t generic_write_sync(struct kiocb *iocb, ssize_t count)
{
    // 检查是否需要同步
	if (iocb_is_dsync(iocb)) {
		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;
}

它先使用 iocb_is_dsync() 函数判断了是否需要同步

1
2
3
4
5
static inline bool iocb_is_dsync(const struct kiocb *iocb)
{
	return (iocb->ki_flags & IOCB_DSYNC) ||
		IS_SYNC(iocb->ki_filp->f_mapping->host);
}

这里进行了两项检查

  1. IOCB_DSYNC 标志是否被设置
  2. iocb->ki_filp->f_mapping->host(inode 结构体) 是否被标记为同步(即文件系统层要求对这个文件进行同步写入)

然后调用下一层同步函数 vfs_fsync_range(),参数有

  • iocb->ki_filp:文件指针,表示需要同步的文件

  • iocb->ki_pos - count:同步范围的起始位置(写操作的开始位置)。

  • iocb->ki_pos - 1:同步范围的结束位置(写操作的结束位置)。

  • iocb->ki_flags & IOCB_SYNC:是否仅同步数据。如果 IOCB_SYNC 标志被设置,则结果为 false,表示需要同步文件元数据。

    关于 IOCB_SYNC 和 IOCB_DSYNC 两个标志的区别

    在执行文件同步时,有时只需要同步数据部分,而有时需要同步包括元数据在内的所有内容。这里可以类比 O_SYNC 和 O_DSYNC

    • O_SYNC:要求任何写入操作阻塞,直到所有数据和所有元数据都写入持久存储。
    • O_DSYNC:与 O_SYNC 类似,不同之处在于无需等待任何不必要的元数据更改来读取刚写入的数据。实际上,O_DSYNC 意味着应用程序无需等待辅助信息(例如文件修改时间)写入磁盘。使用 O_DSYNC 代替 O_SYNC 通常可以消除在写入时刷新文件 inode 的需要。

    因此 generic_write_sync() 先检查 IOCB_DSYNC 标志,再检查 IOCB_SYNC 标志来决定是否进行对文件元数据的同步。

    参考链接:O_*SYNC [LWN.net]

继续看 vfs_fsync_range() 函数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
int vfs_fsync_range(struct file *file, loff_t start, loff_t end, int datasync)
{
	struct inode *inode = file->f_mapping->host;

    // 检查文件是否支持 fsync 操作
	if (!file->f_op->fsync)
		return -EINVAL;

    // 处理元数据同步,再检查一下 inode 的状态,如果标志位包含 I_DIRTY_TIME ,则最终确定需要同步
	if (!datasync && (inode->i_state & I_DIRTY_TIME))
		mark_inode_dirty_sync(inode); // 将 inode 标记为需要同步
	return file->f_op->fsync(file, start, end, datasync);
}

函数最后调用文件系统操作集中的 fsync() 接口,在 ext4 中是 ext4_sync_file()

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
int ext4_sync_file(struct file *file, loff_t start, loff_t end, int datasync)
{
	int ret = 0, err;
	bool needs_barrier = false;
	struct inode *inode = file->f_mapping->host;

    // 检查文件系统是否处于强制关闭状态。如果是,无法执行同步操作,返回错误码
	if (unlikely(ext4_forced_shutdown(inode->i_sb)))
		return -EIO;

    // 断言确保当前没有活跃的日志句柄。即在进行同步操作时,不应有其他正在进行的日志操作。
	ASSERT(ext4_journal_current_handle() == NULL);

    // 记录进入同步函数的跟踪事件,帮助调试和性能分析
	trace_ext4_sync_file_enter(file, datasync);

    // 检查文件系统是否以只读模式挂载
	if (sb_rdonly(inode->i_sb)) {
		smp_rmb();
		if (ext4_forced_shutdown(inode->i_sb))
			ret = -EROFS;
		goto out;
	}

    // 如果文件系统没有启用日志(即处于无日志模式),调用 ext4_fsync_nojournal 进行同步
	if (!EXT4_SB(inode->i_sb)->s_journal) {
		ret = ext4_fsync_nojournal(file, start, end, datasync,
					   &needs_barrier);
		if (needs_barrier)
			goto issue_flush;
		goto out;
	}

    // 确保文件指定范围的数据已经写入磁盘,并等待写入完成。如果失败,则返回错误
	ret = file_write_and_wait_range(file, start, end);
	if (ret)
		goto out;

    // 用于在启用了日志的情况下,将元数据同步到磁盘。这包括等待相关事务提交,以确保数据和元数据的完整性。
	ret = ext4_fsync_journal(inode, datasync, &needs_barrier);

issue_flush:
	if (needs_barrier) {
		err = blkdev_issue_flush(inode->i_sb->s_bdev);
		if (!ret)
			ret = err;
	}
out:
    // 检查并更新写入缓冲区错误
	err = file_check_and_advance_wb_err(file);
    // 如果没有其他错误(ret == 0),函数会返回最后检查的错误码 err。
	if (ret == 0)
		ret = err;
    // 记录退出同步函数的跟踪事件
	trace_ext4_sync_file_exit(inode, ret);
    // 返回同步操作的最终状态
	return ret;
}

可以看到这里做了大量的检查,然后根据文件系统是否启用了日志模式进行分别的处理。

文件系统日志(也称为日志记录或写前日志)是一种机制,用于确保文件系统在发生崩溃或电源故障时能保持一致性。日志记录文件系统会将所有对文件系统元数据的修改操作首先记录到日志中,然后再实际应用到文件系统的主数据结构中。这样可以在系统崩溃时通过回放日志来恢复文件系统到一致状态。

可以在系统中使用以下命令确认是否启用了日志模式(has_journal),/dev/sda 需根据实际修改:

$ sudo tune2fs -l /dev/sda | grep "Filesystem features"
Filesystem features:      has_journal ext_attr resize_inode dir_index filetype needs_recovery extent 64bit flex_bg sparse_super large_file huge_file dir_nlink extra_isize metadata_csum

ext4 默认情况下只通过日志写入文件系统元数据 (data=ordered),参考:3. Global Structures — The Linux Kernel documentation

在没有启用日志模式的情况下,调用 ext4_fsync_nojournal() 函数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
static int ext4_fsync_nojournal(struct file *file, loff_t start, loff_t end,
				int datasync, bool *needs_barrier)
{
	struct inode *inode = file->f_inode;
	int ret;

    // 使用通用的缓冲区同步函数,不进行日志刷新
	ret = generic_buffers_fsync_noflush(file, start, end, datasync);
	if (!ret)
		ret = ext4_sync_parent(inode); // 同步父目录的状态

    // 如果启用了磁盘缓存屏障,则需要标记以发出屏障请求
	if (test_opt(inode->i_sb, BARRIER))
		*needs_barrier = true;

	return ret;
}

此时文件系统不需要等待事务的提交。同步操作只需要将数据写入磁盘并同步父目录状态。如果文件系统启用了屏障,则需要在同步后发出屏障请求。

文件系统屏障(Barrier)的作用就是在写入操作中插入一个“屏障”,确保在屏障之前的数据全部安全地写入磁盘之后,屏障之后的数据才能继续写入。这样可以确保数据按照预期的顺序被写入磁盘,从而避免因意外情况导致的数据丢失或文件系统不一致问题。

由于 ext4 文件系统默认开启日志模式,因此我们着重分析有日志的情况。

首先调用 file_write_and_wait_range() 函数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int file_write_and_wait_range(struct file *file, loff_t lstart, loff_t lend)
{
	int err = 0, err2;
	struct address_space *mapping = file->f_mapping; // 获取文件在内存中的映射

    // 检查写入范围的有效性,注意无效是返回 0,表示没有错误(因为没有实际的写入操作需要执行)
	if (lend < lstart)
		return 0;

    // 判断是否需要进行写回操作
	if (mapping_needs_writeback(mapping)) {
        // 将指定范围内的脏页写回磁盘。
        // 使用 WB_SYNC_ALL 表示这是一个同步写回操作,即函数在返回之前会确保所有脏页都已写入到磁盘。
		err = __filemap_fdatawrite_range(mapping, lstart, lend,
						 WB_SYNC_ALL);
		// 等待写入完成。确保写入操作在硬件层面上完成。
		if (err != -EIO)
			__filemap_fdatawait_range(mapping, lstart, lend);
	}
	err2 = file_check_and_advance_wb_err(file);
	if (!err)
		err = err2;
	return err;
}

然后调用 ext4_fsync_journal() 函数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
static int ext4_fsync_journal(struct inode *inode, bool datasync,
			     bool *needs_barrier)
{
	struct ext4_inode_info *ei = EXT4_I(inode);
	journal_t *journal = EXT4_SB(inode->i_sb)->s_journal;
	tid_t commit_tid = datasync ? ei->i_datasync_tid : ei->i_sync_tid;

    // 目录和特殊文件可能不适合快速提交(fastcommit,fc),因此需要强制提交。
	if (!S_ISREG(inode->i_mode))
		return ext4_force_commit(inode->i_sb);

    // 检查是否需要发出数据屏障请求
	if (journal->j_flags & JBD2_BARRIER &&
	    !jbd2_trans_will_send_data_barrier(journal, commit_tid))
		*needs_barrier = true;

    // 提交快速提交事务
	return ext4_fc_commit(journal, commit_tid);
}

文件系统需要确保不仅数据块已经写入磁盘,还需要确保相关的元数据事务已被提交到日志中。这是因为文件系统的元数据(如 inode、目录结构等)需要通过日志机制保证一致性。

图示:函数调用栈

最后梳理一下整个 write 过程的函数调用栈(暂时不太对)

函数调用栈

config: theme: default look: classic


flowchart TD; classDef asm fill:#f96 subgraph User Application write[“write(fd, buf, len)”] write –> **libc_write["**libcwrite(fd, buf, len)"] syscall:::asm __libc_write –> syscall[“asm volatile (syscall …)”] end subgraph vfs1[VFS            ] syscall –> sys_write[“sys_write(fd, buf, len)”] sys_write –> ksys_write[“ksys_write(fd, buf, len)”] ksys_write –> vfs_write[“vfs_write(file, buf, len, offset)”] vfs_write –> new_sync_write[“new_sync_write(file, buf, len, offset)”] new_sync_write –> call_write_iter[“call_write_iter(file, kiocb, iov_iter)”] end subgraph ext4[“ext4”] call_write_iter –> ext4_file_write_iter[“ext4_file_write_iter(kiocb, iov_iter)”] ext4_file_write_iter –> |Bufferd I/O| ext4_buffered_write_iter[“ext4_buffered_write_iter(kiocb, iov_iter)”] ext4_file_write_iter –> |Direct I/O| ext4_dio_write_iter[“ext4_dio_write_iter(kiocb, iov_iter)”] end subgraph vfs2[VFS            ] ext4_buffered_write_iter –> generic_perform_write[“generic_perform_write(kiocb, iov_iter)”] generic_perform_write –> copy_page_from_iter_atomic[“copy_page_from_iter_atomic(page, offset, len, iov_iter)”] copy_page_from_iter_atomic –> copyin[“copyin(to, from, len)”] copyin –> raw_copy_from_user[“raw_copy_from_user(to, from, len)”] raw_copy_from_user –> copy_user_generic[“copy_user_generic(to, from, len)”] rep_movsb:::asm copy_user_generic –> rep_movsb[“asm volatile (rep movsb …)”] end subgraph vfs3[“VFS”] ext4_buffered_write_iter –> generic_write_sync[“generic_write_sync(kiocb, ret)”] generic_write_sync –> vfs_fsync_range[“vfs_fsync_range(file, start, end, datasync)”] end subgraph ext4[“ext4”] vfs_fsync_range –> ext4_sync_file[“ext4_sync_file(file, start, end, datasync)”] ext4_sync_file –> file_write_and_wait_range[“file_write_and_wait_range(file, start, end)”] end


本站不记录浏览量,但如果您觉得本内容有帮助,请点个小红心,让我知道您的喜欢。