打开文件的最基本的目的是为了对文件进行访问,而访问中最基本的操作是读和写。文件的内容被保存在磁盘块,但是这些磁盘块极有可能是不连续的而且往往是动态变化的。而访问文件时我们往往又需要将文件看作一个连续的字节流,要解决这样的矛盾,核心在于地址空间(Address Space)的引入。
地址空间的目的就是将其属主对象在磁盘或其他介质中的(可能不连续的)数据以页面为单位连续地呈现出来。根据需要,这些页面可以,也可以不在系统内存,称为页面缓存,对属主对象的读写操作可以在页面缓存中进行,只是在需要时按照属主对象特定的操作语义从磁盘(或其他介质)读,或像磁盘(或其他介质)写。
简而言之就是维护了文件页和物理页的关系,并决定它们该在什么时候读出和写回,这里已经是我们整个IO链条的最上层部分了。
数据结构
尽管在上文已经贴过了address_space及其操作表的结构,但是这里还是再粘贴一次:
struct address_space {
struct inode *host; // inode或块设备,属主
struct xarray i_pages; // 缓存页们
struct rw_semaphore invalidate_lock;
// 保护(页面缓存内容)和(文件偏移与磁盘块映射关系)的一致性
// 它还用于通过内存映射阻止对页面缓存内容的修改。
gfp_t gfp_mask;
atomic_t i_mmap_writable; // 地址空间中共享内存映射的数目
#ifdef CONFIG_READ_ONLY_THP_FOR_FS
/* number of thp, only for non-shmem files */
atomic_t nr_thps;
#endif
struct rb_root_cached i_mmap; // 私有和共享映射树根
struct rw_semaphore i_mmap_rwsem; // 保护 i_mmap
unsigned long nrpages; // 地址空间中页面总数
pgoff_t writeback_index; // 写回开始处
const struct address_space_operations *a_ops; // 地址空间操作表
unsigned long flags; // 错误标记
errseq_t wb_err; // 最近一个错误
spinlock_t private_lock; // 保护地址空间私有链表的锁
struct list_head private_list; // 地址空间私有链表
void *private_data;
} __attribute__((aligned(sizeof(long)))) __randomize_layout;
文件地址空间被分割为一个个以页面大小为单元的数据块,这些数据块(页)被组织成一个多叉树,被称为基(radix)树,树中所有叶子节点为一个个页面结构(struct page),表示用于缓存该文件的每一个页,叶子层最左端的第一个页面保存着该文件的前4096字节(若页大小为4K),接下来的页面保存着第二个4K字节,以此类推。在5.15的内核版本中,这棵树被组织为一棵红黑树:
struct rb_root_cached {
struct rb_root rb_root;
struct rb_node *rb_leftmost;
};
这已经是内存管理的知识了,我们目前主要关注的是内存管理和文件管理打交道的那一部分。
和address_space相关的操作方法都在a_ops表中(昨天也贴过了,但是还是再贴一遍):
struct address_space_operations {
/* 更新内存到磁盘,调用前page已是最新,第一个参数为指向待写内存页描述符指针,第二个参数为指向控制回写行为结构的指针*/
int (*writepage)(struct page *page, struct writeback_control *wbc);
/* 读取文件file数据到内存page */
int (*readpage)(struct file *, struct page *);
/* 回写多个脏页时调用,第一个参数为指向地址空间的指针,第二个参数指向回写控制结构,
如果同步模式为WBC_SYNC_ALL,回写控制中会指定要写出页面的范围 */
/* Write back some dirty pages from this mapping. */
int (*writepages)(struct address_space *, struct writeback_control *);
/* Set a page dirty. Return true if this dirtied it 标记传入page为脏*/
int (*set_page_dirty)(struct page *page);
/*
* Reads in the requested pages. Unlike ->readpage(), this is
* PURELY used for read-ahead!. 用于预读场合,读取多个页面到地址空间
*/
int (*readpages)(struct file *filp, struct address_space *mapping,
struct list_head *pages, unsigned nr_pages);
void (*readahead)(struct readahead_control *);
/* 要求具体文件系统准备将指定偏移和长度的数据写入到文件
* VFS通过调用write_begin告诉具体文件系统,准备写文件字节pos~pos+len到给定页面
* 具体文件系统要确保本次操作能完成,必要时分配空间,非覆写时从磁盘读取数据到页面等
* 第一个参数为指向文件的指针,第二个参数为指向地址空间的指针,第三个第四个参数如上介绍
* 第五个参数为flag,第六个参数为page指针数组,如果为NULL,则需要具体文件系统分配,并通过它返回线程安全的页面
* 第七个参数为由具体文件系统解释的数据结构
*/
int (*write_begin)(struct file *, struct address_space *mapping,
loff_t pos, unsigned len, unsigned flags,
struct page **pagep, void **fsdata);
/* 在成功调用write_begin,并完成数据复制后必须调用,
* VFS通过调用write_end告诉具体文件系统,文件字节pos~pos+len数据已被复制到页面,现在可以提交磁盘
* 具体文件系统要确保本次操作能完成,必要时分配空间,非覆写时从磁盘读取数据到页面等
* 第一个参数为指向文件的指针,第二个参数为指向地址空间的指针,第三个第四个参数如上介绍
* 第五个参数为已写字节数,第六个参数为指向页面的指针
* 第七个参数为由具体文件系统解释的数据结构
* 具体文件系统需要负责解锁页面,释放引用计数,并更新i_size。
* 失败返回负的错误码;否则返回能够被复制到页面缓存页面的字节数(<=copied)
*/
int (*write_end)(struct file *, struct address_space *mapping,
loff_t pos, unsigned len, unsigned copied,
struct page *page, void *fsdata);
/* Unfortunately this kludge is needed for FIBMAP. Don't use it
* 将文件中逻辑扇区编号映射为物理扇区编号
*/
sector_t (*bmap)(struct address_space *, sector_t);
/* 使某个页面全部或部分失效,用在截断文件时 */
void (*invalidatepage) (struct page *, unsigned int, unsigned int);
/* 用于释放页面 */
int (*releasepage) (struct page *, gfp_t);
void (*freepage)(struct page *);
ssize_t (*direct_IO)(struct kiocb *, struct iov_iter *iter);
/*
* migrate the contents of a page to the specified target. If
* migrate_mode is MIGRATE_ASYNC, it must not block.
* 将页面内容移动到指定的目标
*/
int (*migratepage) (struct address_space *,
struct page *, struct page *, enum migrate_mode);
bool (*isolate_page)(struct page *, isolate_mode_t);
void (*putback_page)(struct page *);
/* 在释放一个页面之前被调用,回写一个脏页 */
int (*launder_page) (struct page *);
/* 在处理缓冲IO请求时,被调用以判断要读取的这部分数据在页面中是否为最新的 */
int (*is_partially_uptodate) (struct page *, unsigned long,
unsigned long);
void (*is_dirty_writeback) (struct page *, bool *, bool *);
int (*error_remove_page)(struct address_space *, struct page *);
/* swapfile support */
int (*swap_activate)(struct swap_info_struct *sis, struct file *file,
sector_t *span);
void (*swap_deactivate)(struct file *file);
};
读写操作是在内核IO控制块的控制下进行的,实际上这样的结构是为了支持异步IO(Asychronous IO,AIO),实现用户空间和内核之间的异步,对应系统调用是io_setup, io_submit, io_cancel, io_destory等,在f2fs的读写流程中,vfs向f2fs会传入一个内核控制块及io向量。
struct kiocb {
struct file *ki_filp;
/* The 'ki_filp' pointer is shared in a union for aio */
randomized_struct_fields_start
loff_t ki_pos; // 当前进行io操作的文件位置
void (*ki_complete)(struct kiocb *iocb, long ret, long ret2);
void *private; // 可以被文件系统层自由使用
int ki_flags; // kiocb描述符标志位
u16 ki_hint;
u16 ki_ioprio; /* See linux/ioprio.h */
union {
unsigned int ki_cookie; /* for ->iopoll */
struct wait_page_queue *ki_waitq; /* for async buffered IO */
};
randomized_struct_fields_end
};
---iov_iter是对iovec的迭代器
struct iov_iter {
u8 iter_type; // 迭代器类型 判断union是啥
bool nofault;
bool data_source;
size_t iov_offset; // iov指针指向的第一个iovec结构体中数据的第一个字节偏移
size_t count; // iov数组存放个数(有几个缓冲区段)
union {
const struct iovec *iov;
const struct kvec *kvec;
const struct bio_vec *bvec;
struct xarray *xarray;
struct pipe_inode_info *pipe;
};
union {
unsigned long nr_segs;
struct {
unsigned int head;
unsigned int start_head;
};
loff_t xarray_start;
};
};
--- 一个iovec就是一个用户空间缓冲区地址+长度(==一段用户缓冲区范围)
struct iovec
{
void __user *iov_base; /* BSD uses caddr_t (1003.1g requires void *) */
__kernel_size_t iov_len; /* Must be size_t (1003.1g) */
};
read系统调用
SYSCALL_DEFINE3(read, unsigned int, fd, char __user *, buf, size_t, count)
{
return ksys_read(fd, buf, count); // 从fd 读 count 到 buf里面
}
---ssize_t ksys_read(unsigned int fd, char __user *buf, size_t count)
{
struct fd f = fdget_pos(fd); // 根据fd取出file
ssize_t ret = -EBADF;
if (f.file) {
loff_t pos, *ppos = file_ppos(f.file); // 获得当前读写位置
if (ppos) {
pos = *ppos;
ppos = &pos;
}
ret = vfs_read(f.file, buf, count, ppos); // 读,从file的ppos开始读count到buf里面
if (ret >= 0 && ppos)
f.file->f_pos = pos; // 修改当前读写位置
fdput_pos(f); // 引用计数
}
return ret;
}
---
ssize_t vfs_read(struct file *file, char __user *buf, size_t count, loff_t *pos)
{
ssize_t ret;
if (!(file->f_mode & FMODE_READ))
return -EBADF;
if (!(file->f_mode & FMODE_CAN_READ))
return -EINVAL;
if (unlikely(!access_ok(buf, count))) // 确认缓冲区可访问
return -EFAULT;
ret = rw_verify_area(READ, file, pos, count); // 检查锁冲突
if (ret)
return ret;
if (count > MAX_RW_COUNT)
count = MAX_RW_COUNT;
if (file->f_op->read) // 调用f_op中的方法
ret = file->f_op->read(file, buf, count, pos);
else if (file->f_op->read_iter) // 没有read,但有read_iter,eg. f2fs
ret = new_sync_read(file, buf, count, pos);
else
ret = -EINVAL;
if (ret > 0) {
fsnotify_access(file);
add_rchar(current, ret);
}
inc_syscr(current);
return ret;
}
---
static ssize_t new_sync_read(struct file *filp, char __user *buf, size_t len, loff_t *ppos)
{
struct iovec iov = { .iov_base = buf, .iov_len = len };
struct kiocb kiocb;
struct iov_iter iter;
ssize_t ret;
init_sync_kiocb(&kiocb, filp);
kiocb.ki_pos = (ppos ? *ppos : 0);
iov_iter_init(&iter, READ, &iov, 1, len);
ret = call_read_iter(filp, &kiocb, &iter); // == file->f_op->read_iter(kio, iter);
BUG_ON(ret == -EIOCBQUEUED);
if (ppos)
*ppos = kiocb.ki_pos;
return ret;
}
---iov_iter进行初始化:
void iov_iter_init(struct iov_iter *i, unsigned int direction,
const struct iovec *iov, unsigned long nr_segs,
size_t count)
{
WARN_ON(direction & ~(READ | WRITE));
*i = (struct iov_iter) {
.iter_type = ITER_IOVEC,
.nofault = false,
.data_source = direction,
.iov = iov,
.nr_segs = nr_segs,
.iov_offset = 0,
.count = count
};
}
---kiocb初始化:
static inline void init_sync_kiocb(struct kiocb *kiocb, struct file *filp)
{
*kiocb = (struct kiocb) {
.ki_filp = filp,
.ki_flags = iocb_flags(filp),
.ki_hint = ki_hint_validate(file_write_hint(filp)),
.ki_ioprio = get_current_ioprio(),
};
}
最终,配置好的kiocb和iov_iter被派发到f2fs等具体操作系统中执行。
write系统调用
了解read系统调用之后,write系统调用是十分相似的过程。
SYSCALL_DEFINE3(write, unsigned int, fd, const char __user *, buf,
size_t, count)
{
return ksys_write(fd, buf, count);
}
---
ssize_t ksys_write(unsigned int fd, const char __user *buf, size_t count)
{
struct fd f = fdget_pos(fd);
ssize_t ret = -EBADF;
if (f.file) {
loff_t pos, *ppos = file_ppos(f.file); // 获取文件读写位置
if (ppos) {
pos = *ppos;
ppos = &pos;
}
ret = vfs_write(f.file, buf, count, ppos); // 调用vfs层write
if (ret >= 0 && ppos)
f.file->f_pos = pos; // 更新文件读写位置
fdput_pos(f);
}
return ret;
}
---
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)
ret = file->f_op->write(file, buf, count, pos); // 如果定义了write
else if (file->f_op->write_iter)
ret = new_sync_write(file, buf, count, pos); // 定义write_iter
else
ret = -EINVAL;
if (ret > 0) {
fsnotify_modify(file);
add_wchar(current, ret);
}
inc_syscw(current);
file_end_write(file);
return ret;
}
---
static ssize_t new_sync_write(struct file *filp, const char __user *buf, size_t len, loff_t *ppos)
{
struct iovec iov = { .iov_base = (void __user *)buf, .iov_len = len };
struct kiocb kiocb;
struct iov_iter iter;
ssize_t ret;
init_sync_kiocb(&kiocb, filp);
kiocb.ki_pos = (ppos ? *ppos : 0);
iov_iter_init(&iter, WRITE, &iov, 1, len); // 写
ret = call_write_iter(filp, &kiocb, &iter);
BUG_ON(ret == -EIOCBQUEUED);
if (ret > 0 && ppos)
*ppos = kiocb.ki_pos;
return ret;
}
可见在vfs层,读写流程几乎一模一样,主要功能是将kiocb和iov_iter下发到具体文件系统执行。后面我们将以F2FS为具体文件系统继续分析。