Linux|IO|File IO源码剖析
文件的open、close、read、write是最基本的文件抽象,描述了对于设备的操作。本文将结合用户态的接口以及内核态的实现剖析文件IO。
Reference: The Linux Programming Interface: Chapter 4/14/15, Kernel/fs
通用接口
通用IO包含open/read/write/close,大部分文件系统和设备驱动都支持对应接口(或者iter版本)
Open
接口
open用于创建或打开VFS路径下的文件并且获得fd
- pathname为vfs文件路径
- flags为标志
- mode则具体描述了O_CREATE下的文件权限,平时可省略。
分为三个部分:
- 文件访问模式标志 - 互斥,不可位或。通过fcntl(F_GETFL)可读。
- 文件创建标志 - 创建以及后续IO的选项,不可读写。
- 文件状态标志 - IO的方式,可读可写
实现
在fs/open.c和fs/namei.c中可见相关部分代码,省略了次要代码,保留关键路径
syscall - 64位syscall默认能打开大文件
SYSCALL_DEFINE4(openat, int, dfd, const char __user *, filename, int, flags,
umode_t, mode)
{
if (force_o_largefile())
flags |= O_LARGEFILE;
return do_sys_open(dfd, filename, flags, mode);
}
do_sys_open - 通过flags和mode构建open_how
long do_sys_open(int dfd, const char __user *filename, int flags, umode_t mode)
{
struct open_how how = build_open_how(flags, mode);
return do_sys_openat2(dfd, filename, &how);
}
do_sys_openat2 - 通过open_how获取open_flags
static long do_sys_openat2(int dfd, const char __user *filename,
struct open_how *how)
{
struct open_flags op;
int fd = build_open_flags(how, &op);
tmp = getname(filename);
struct file *f = do_filp_open(dfd, tmp, &op);
putname(tmp);
return fd;
}
do_filp_open - 设置查找上下文
struct file *do_filp_open(int dfd, struct filename *pathname,
const struct open_flags *op)
{
struct nameidata nd;
int flags = op->lookup_flags;
struct file *filp;
set_nameidata(&nd, dfd, pathname);
filp = path_openat(&nd, op, flags | LOOKUP_RCU);
restore_nameidata();
return filp;
}
path_openat - 进行名称查找
static struct file *path_openat(struct nameidata *nd,
const struct open_flags *op, unsigned flags)
{
while (!(error = link_path_walk(s, nd)) &&
(s = open_last_lookups(nd, file, op)) != NULL);
if (!error)
error = do_open(nd, file, op);
terminate_walk(nd);
}
}
do_open - 如果当前没有打开文件,则进行打开,需要处理truncate的情况
static int do_open(struct nameidata *nd,
struct file *file, const struct open_flags *op)
{
error = may_open(&nd->path, acc_mode, open_flag);
if (!error && !(file->f_mode & FMODE_OPENED))
error = vfs_open(&nd->path, file);
if (!error)
error = ima_file_check(file, op->acc_mode);
if (!error && do_truncate)
error = handle_truncate(file);
if (do_truncate)
mnt_drop_write(nd->path.mnt);
return error;
}
vfs_open - VFS打开文件
int vfs_open(const struct path *path, struct file *file)
{
file->f_path = *path;
return do_dentry_open(file, d_backing_inode(path->dentry), NULL);
}
do_dentry_open - 利用实际文件系统或者驱动的open函数打开,增加引用计数,结合文件本身设定的权限f_mode以及文件系统提供的权限f_op获得实际权限。
static int do_dentry_open(struct file *f,
struct inode *inode,
int (*open)(struct inode *, struct file *))
{
/* normally all 3 are set; ->open() can clear them if needed */
f->f_mode |= FMODE_LSEEK | FMODE_PREAD | FMODE_PWRITE;
if (!open)
open = f->f_op->open;
if (open) {
error = open(inode, f);
if (error)
goto cleanup_all;
}
f->f_mode |= FMODE_OPENED;
if ((f->f_mode & (FMODE_READ | FMODE_WRITE)) == FMODE_READ)
i_readcount_inc(inode);
if ((f->f_mode & FMODE_READ) &&
likely(f->f_op->read || f->f_op->read_iter))
f->f_mode |= FMODE_CAN_READ;
if ((f->f_mode & FMODE_WRITE) &&
likely(f->f_op->write || f->f_op->write_iter))
f->f_mode |= FMODE_CAN_WRITE;
}
Read
接口
read用于从当前的文件偏移量处读取一定数目的字节
- fd为文件描述符
- count为最大读取字节数,最大为MAX_RW_COUNT
- buffer为用户态缓冲区
需要注意的是,read并不遵循C语言 结尾的约定,因此应该显式在buffer末尾增加 ,buffer size >= count + 1
实现
在fs/read_write.c中可见相关部分代码,省略了次要代码,保留关键路径
syscall - 不说了
SYSCALL_DEFINE3(read, unsigned int, fd, char __user *, buf, size_t, count)
{
return ksys_read(fd, buf, count);
}
ksys_read - 根据文件描述符获取文件的偏移量
ssize_t ksys_read(unsigned int fd, 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_read(f.file, buf, count, ppos);
if (ret >= 0 && ppos)
f.file->f_pos = pos;
fdput_pos(f);
}
return ret;
}
vfs_read - 先进行校验读取是否合法,然后看文件系统或者驱动有没有提供read接口,否则通过read_iter(stuct kiocb *iocb, struct iov_iter *to)读取。
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)
ret = file->f_op->read(file, buf, count, pos);
else if (file->f_op->read_iter)
ret = new_sync_read(file, buf, count, pos);
else
ret = -EINVAL;
}
Write
接口
write用于从当前的文件偏移量处写入一定数目的字节
- fd为文件描述符
- count为最大写入字节数,最大为MAX_RW_COUNT
- buffer为用户态缓冲区
需要注意的是,我们在使用C++时往往会使用flush,用于刷新缓冲区。问题在于,这个操作仅仅是刷新用户态的缓冲区!内核依然会对write进行缓存,需要手动进行刷新,通过vfs调用磁盘驱动提供的flush原语。
- fsync(fd)强制其刷新到磁盘上
- fdatasync(fd)不刷新metadata的时间戳
- sync()刷新所有的缓冲区(Linux要求等待所有操作完成才能返回)。
因为这个原因,写操作并不能实时的进行持久化,需要linux使用journal机制来保证文件系统的崩溃一致性,然而journal机制本身又需要进行flush。我们必须保证JC写入前,Data和JM 都已经被写入磁盘。 同时保证Metadata写入前,JC被写入磁盘。
SOSP13有人提出了乐观的崩溃一致性,减少了Flush的开销。
实现
在fs/read_write.c中可见相关部分代码,省略了次要代码,保留关键路径
和read的逻辑一模一样,区别在于vfs_write使用临界区保护防止race condition
file_start_write(file);
if (file->f_op->write)
ret = file->f_op->write(file, buf, count, pos);
else if (file->f_op->write_iter)
ret = new_sync_write(file, buf, count, pos);
else
ret = -EINVAL;
if (ret > 0) {
fsnotify_modify(file);
add_wchar(current, ret);
}
inc_syscw(current);
file_end_write(file);
Close
接口
close用于释放文件描述符资源和关闭文件,进程结束时也会自动释放。
实现
在fs/open.c和fs/file.c中可见相关部分代码,省略了次要代码,保留关键路径
syscall - 做Retval的转换
/*
* Careful here! We test whether the file pointer is NULL before
* releasing the fd. This ensures that one clone task can't release
* an fd while another clone is opening it.
*/
SYSCALL_DEFINE1(close, unsigned int, fd)
{
int retval = __close_fd(current->files, fd);
/* can't restart close syscall because file table entry was cleared */
if (unlikely(retval == -ERESTARTSYS ||
retval == -ERESTARTNOINTR ||
retval == -ERESTARTNOHAND ||
retval == -ERESTART_RESTARTBLOCK))
retval = -EINTR;
return retval;
}
_close_fd - 从当前的文件中取出fd表,在fd索引处获取文件并且RCU地赋值为null,然后归还fd资源。此时已经离开临界区,原进程能够利用fd了。然后filp_close关闭文件。
这里需要注意,先释放fd资源,再释放文件资源。
/*
* The same warnings as for __alloc_fd()/__fd_install() apply here...
*/
int __close_fd(struct files_struct *files, unsigned fd)
{
struct file *file;
struct fdtable *fdt;
spin_lock(&files->file_lock);
fdt = files_fdtable(files);
if (fd >= fdt->max_fds)
goto out_unlock;
file = fdt->fd[fd];
if (!file)
goto out_unlock;
rcu_assign_pointer(fdt->fd[fd], NULL);
__put_unused_fd(files, fd);
spin_unlock(&files->file_lock);
return filp_close(file, files);
out_unlock:
spin_unlock(&files->file_lock);
return -EBADF;
}
flip_close - 调用flush将文件缓冲全部刷新,然后释放当前文件(引用计数--)
/*
* "id" is the POSIX thread ID. We use the
* files pointer for this..
*/
int filp_close(struct file *filp, fl_owner_t id)
{
int retval = 0;
if (!file_count(filp)) {
printk(KERN_ERR "VFS: Close: file count is 0
");
return 0;
}
if (filp->f_op->flush)
retval = filp->f_op->flush(filp, id);
if (likely(!(filp->f_mode & FMODE_PATH))) {
dnotify_flush(filp, id);
locks_remove_posix(filp, id);
}
fput(filp);
return retval;
}
Lseek
文件的读写共用相同的pos,在读写时自动从内核的文件状态中获取
whence表示参考基
- SEEK_SET 以文件头部为基点
- SEEK_CUR 以当前偏移量为基点
- SEEK_END 以文件尾部为基点
显然,对于没有头部的文件,lseek显然不可行。lseek的适用范围是块设备。
通过间接层处理空洞 - 当我们进行SEEK_END时,END到当前的pos会存在空洞,那么Linux并不会为空洞分配block存储,空洞通过为inode系统中的指针打上标记0表明其并未指向实际磁盘块即可。这个思想和多级页表是相同的。
通过压缩处理空洞 - 类似的,在bitmap中也有可能存在空洞,谷歌的EWAH Compressed Bitmap就采取了压缩的方式将连续的1/0压缩成length + 1/0。
Ioctl
非通用的IO操作,通过指定的request值表示操作,后续传递参数的类型通过request的值进行解释。
总结
系统调用都通过VFS层来进行文件,而实际的操作通过背后的设备驱动完成。
fd的本质是进程fdt->fd的索引,元素为内核中的file结构体,存储打开文件的状态。
由内核在file中隐式维护偏移量,并在读写时自动更新。
read/write操作不一定直接调用read/write,可能是iter;write操作也无法保证实时更新到磁盘上。
close时,文件描述符的释放先进行,然后才进行实际文件的释放。
相关文章
- 挨踢部落直播课堂第六期:精益化数据分析—如何让你的企业具有BAT一样的分析能力
- 企业数据化的「额外一公里」
- 什么是云计算数据中心?云计算数据中心和传统IDC有何区别?
- 大数据为何这么重要?
- 挨踢部落坐诊第十一期:三千万数据如何做到秒查
- 得益于AI,这五个行业岗位需求将呈现显著增长趋势
- 海量数据流的最佳算法实战
- Go语言如何实现遗传算法
- 分析非结构化数据的10个步骤
- 蠕虫病毒疯狂肆虐 数据中心当如何防范?
- 数据结构与算法–图论之寻找连通分量、强连通分量
- 如何分析博客中最流行的编程语言
- 可视化学习 Go并发编程
- 大数据时代,软件工程师渐退,算法工程师崛起!
- 数据缺失的坑,无监督学习这样帮你补了
- 大数据、人工智能、机器人的血缘关系?
- 如何在 Linux 平台下看蓝光影碟
- 年关将至,服务器被入侵了怎么办?
- 红帽企业版 Linux 6.4 试用体验
- Linux应用总结:自动删除n天前日志