Linux write 系统调用源码分析(Linux-6.6.46)
发布于 2024-10-14
|
更新于
2025-12-25
|
字数:11110
未完待续,暂作保存!
从用户态到内核态
此部分和 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
内核接收到一个系统调用后,根据系统调用号进行分发,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() 工作内容小结:
- 检查文件描述符的有效性。
- 获取当前文件的偏移量。
- 调用 VFS 层的 vfs_write() 进行写入操作。
- 更新文件偏移量。
- 释放资源并返回写入的字节数或错误代码。
这里有两个重要的结构体,定义在 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() 工作内容小结:
- 文件权限和写入区域检查。
- 根据文件系统的具体实现,调用适当的写操作函数来执行实际的数据写入。
- 如果写入成功,通知文件系统的监控机制,并更新进程的 I/O 统计数据。
- 返回写入的字节数或错误码。
关于 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() 工作内容小结:
- 初始化 I/O 控制块。
- 设置文件位置。
- 初始化迭代器。
- 调用文件系统的 write_iter 方法。
- 检查返回值并更新文件位置。
这里又出现了两个新结构体:
先看一下 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() 工作内容小结:
- 检查文件系统状态。
- 处理 DAX 模式。
- 根据标志位选择写入模式。
可以看到 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() 工作内容小结:
- 检查操作是否支持非阻塞模式
- 锁定 inode,确保写操作期间文件的完整性
- 调用 ext4 特定的检查函数验证写操作
- 执行实际的写操作,将数据写入页缓存。
- 解锁 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);
}
|
这里进行了两项检查
- IOCB_DSYNC 标志是否被设置
- 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