大家好,我是仲一。
本篇文章从 open() 系统调用切入,跟进去看内核里一步步做了什么,然后 close() 又是怎么收场的。
open 总览
用户程序调 open:
int fd = open("/home/zhongyi/test.c", O_RDWR, 0);
C 库封装后通过软中断进内核,到达 SYSCALL_DEFINE3 定义的入口(SYSCALL_DEFINE 怎么展开的见前文)。最终实际调用的是 do_sys_open:
long do_sys_open(int dfd, const char __user *filename, int flags, umode_t mode)
{
struct open_flags op;
int fd = build_open_flags(flags, mode, &op);
struct filename *tmp;
if (fd)
return fd;
tmp = getname(filename);
if (IS_ERR(tmp))
return PTR_ERR(tmp);
fd = get_unused_fd_flags(flags);
if (fd >= 0) {
struct file *f = do_filp_open(dfd, tmp, &op);
if (IS_ERR(f)) {
put_unused_fd(fd);
fd = PTR_ERR(f);
} else {
fsnotify_open(f);
fd_install(fd, f);
}
}
putname(tmp);
return fd;
}
流程拆开:
build_open_flags() — 把用户传的 flags/mode 转成内核的
struct open_flagsgetname() — 把文件路径从用户空间拷到内核空间
get_unused_fd_flags() — 拿一个空闲的文件描述符
do_filp_open() — 真正打开文件,拿到
struct filefsnotify_open() — 通知文件系统有人开了文件
fd_install() — 把 fd 和 file 结构体绑到一起
putname() — 释放内核里存路径名的内存
按这个顺序往下看。
build_open_flags()
struct open_flags {
int open_flag;
umode_t mode;
int acc_mode;
int intent;
int lookup_flags;
};
static inline int build_open_flags(int flags, umode_t mode, struct open_flags *op)
{
int lookup_flags = 0;
int acc_mode = ACC_MODE(flags);
flags &= VALID_OPEN_FLAGS;
if (flags & (O_CREAT | __O_TMPFILE))
op->mode = (mode & S_IALLUGO) | S_IFREG;
else
op->mode = 0;
flags &= ~FMODE_NONOTIFY & ~O_CLOEXEC;
if (flags & __O_SYNC)
flags |= O_DSYNC;
if (flags & __O_TMPFILE) {
...
} else if (flags & O_PATH) {
flags &= O_DIRECTORY | O_NOFOLLOW | O_PATH;
acc_mode = 0;
}
op->open_flag = flags;
if (flags & O_TRUNC)
acc_mode |= MAY_WRITE;
if (flags & O_APPEND)
acc_mode |= MAY_APPEND;
op->acc_mode = acc_mode;
op->intent = flags & O_PATH ? 0 : LOOKUP_OPEN;
if (flags & O_CREAT) {
op->intent |= LOOKUP_CREATE;
if (flags & O_EXCL)
op->intent |= LOOKUP_EXCL;
}
if (flags & O_DIRECTORY)
lookup_flags |= LOOKUP_DIRECTORY;
if (!(flags & O_NOFOLLOW))
lookup_flags |= LOOKUP_FOLLOW;
op->lookup_flags = lookup_flags;
return 0;
}
做的事情:各种标志位检查,翻译成 open_flags 结构体,后面给路径查找和文件打开函数用。
getname()
struct filename {
const char *name; /* 内核空间路径指针 */
const __user char *uptr; /* 用户空间原始指针 */
struct audit_names *aname;
int refcnt;
const char iname[]; /* 实际文件名 */
};
struct filename *getname(const char __user *filename)
{
return getname_flags(filename, 0, NULL);
}
内核从 slab 分配器拿一个 struct filename,用 strncpy_from_user() 把用户空间的路径拷到内核的 iname 数组里。如果路径超过 EMBEDDED_NAME_MAX(256 字节),再单独给路径字符串分配空间。
get_unused_fd_flags()
拿一个空闲 fd:
int get_unused_fd_flags(unsigned flags)
{
return __alloc_fd(current->files, 0, rlimit(RLIMIT_NOFILE), flags);
}
__alloc_fd 做的事情:
从当前进程的 files_struct 拿到 fdtable
在位图里 find_next_fd() 找一个空闲 fd(最小可用原则)槽位不够就 expand_files() 扩容标记该 fd 已使用设置了 O_CLOEXEC 的话,标记 exec 时关闭
do_filp_open()
前面都是准备,到这里才真正开文件。核心是把路径名和 inode 节点对上。
struct file 是文件读写的上下文,最终会挂到进程的 files_struct 数组里:
struct file {
struct path f_path; /* 文件路径 */
struct inode *f_inode; /* inode 节点 */
const struct file_operations *f_op;
spinlock_t f_lock;
atomic_long_t f_count; /* 引用计数 */
unsigned int f_flags;
fmode_t f_mode;
loff_t f_pos; /* 当前读写位置 */
...
};
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);
if (unlikely(filp == ERR_PTR(-ECHILD)))
filp = path_openat(&nd, op, flags);
if (unlikely(filp == ERR_PTR(-ESTALE)))
filp = path_openat(&nd, op, flags | LOOKUP_REVAL);
restore_nameidata();
return filp;
}
path_openat 最多调三次,一次比一次慢:
RCU-walk — RCU 模式下查 dcache,最快
ref-walk — 引用模式再查 dcache
force-reval — 强制文件系统从磁盘重新读
path_openat
static struct file *path_openat(struct nameidata *nd,
const struct open_flags *op, unsigned flags)
{
struct file *file;
int opened = 0;
file = get_empty_filp();
if (IS_ERR(file))
return file;
file->f_flags = op->open_flag;
if (unlikely(file->f_flags & __O_TMPFILE)) {
error = do_tmpfile(nd, flags, op, file, &opened);
goto out2;
}
if (unlikely(file->f_flags & O_PATH)) {
error = do_o_path(nd, flags, file);
...
goto out2;
}
s = path_init(nd, flags);
...
while (!(error = link_path_walk(s, nd)) &&
(error = do_last(nd, file, op, &opened)) > 0) {
...
}
...
return file;
}
几步:
get_empty_filp() — 分配
struct filepath_init() — 确定从哪里开始找
link_path_walk() — 逐层路径遍历
do_last() — 处理路径最后一个分量(文件名)
path_init
三种情况:
绝对路径(/ 开头)
— set_root() 从根目录开始
相对路径(AT_FDCWD) — 从 current->fs->pwd(当前工作目录)开始
指定 fd — 从 fdget_raw() 拿到的目录开始
link_path_walk
路径用 / 分隔,逐层解析。比如 /root/hello/world/data,前半部分 /root/hello/world 在这解析完,最后剩个 data 留给 do_last。
大循环里每次都 may_lookup() 检查权限,hash_name() 算哈希,然后 walk_component() 走一步。
walk_component
处理每一步的路径分量:
. — 原地不动
.. — 退回父目录普通目录
— 先 lookup_fast() 查 dcache,没命中就 lookup_slow() 调文件系统的 lookup 函数
do_last
最后一步处理文件名。
决策逻辑:
如果是目录类型(. 或 ..)→ 返回失败符号链接 → 返回 1,让上层跟进文件不存在但有 O_CREAT → 创建 inode 然后打开文件不存在且无 O_CREAT → 返回 -ENOENT文件存在 → 检查权限后打开
调用链:
do_last()
→ lookup_fast() / lookup_open() // 找 dentry
→ may_open() // 检查权限
→ vfs_open() // 实际打开
→ open_check_o_direct() / ima_file_check()
fd_install()
dentry、inode、file 结构体都就位了,最后一步:把 fd 和 file 结构体绑起来。
void __fd_install(struct files_struct *files, unsigned int fd,
struct file *file)
{
struct fdtable *fdt;
...
fdt = rcu_dereference_sched(files->fdt);
BUG_ON(fdt->fd[fd] != NULL);
rcu_assign_pointer(fdt->fd[fd], file);
...
}
核心就一行:fdt->fd[fd] = file。
putname()
释放 getname() 分配的内核路径缓冲区,引用计数减到 0 就真正释放。
close
close() 的入口是 SYSCALL_DEFINE1(close, ...):
SYSCALL_DEFINE1(close, unsigned int, fd)
{
int retval = __close_fd(current->files, fd);
...
return retval;
}
__close_fd 的流程:
-
- 检查 fd 是否超范围从 fdtable 取出 file 指针把 fdtable 槽位置 NULL清除 close-on-exec 标记标记 fd 为空闲调
filp_close()
- 做实际关闭
int __close_fd(struct files_struct *files, unsigned fd)
{
struct file *file;
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);
__clear_close_on_exec(fd, fdt);
__put_unused_fd(files, fd);
spin_unlock(&files->file_lock);
return filp_close(file, files);
...
}
filp_close
int filp_close(struct file *filp, fl_owner_t id)
{
if (!file_count(filp)) {
printk(KERN_ERR "VFS: Close: file count is 0n");
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;
}
做了三件事:
-
-
- 如果文件系统有
flush
-
-
-
- 方法,把内容刷到磁盘释放 POSIX 锁
fput()
-
-
-
- 减引用计数,减到 0 时真正释放
struct file
-
完整链路
open()
→ SYSCALL_DEFINE3(open, ...)
→ do_sys_open()
→ build_open_flags() // 解析 flags/mode
→ getname() // 复制文件名到内核
→ get_unused_fd_flags() // 分配 fd
→ do_filp_open() // 路径查找 + 文件创建
→ path_init() // 确定起点
→ link_path_walk() // 逐层遍历
→ do_last() // 处理最后一级
→ vfs_open() // 实际打开
→ fd_install() // 绑定 fd 和 file
→ putname() // 释放路径名
→ return fd
close(fd)
→ SYSCALL_DEFINE1(close, ...)
→ __close_fd() // 释放 fd 槽位
→ filp_close() // flush + 释放锁
→ fput() // 释放 file
参考:
- https://blog.csdn.net/lickylin/article/details/101619601https://cloud.tencent.com/developer/article/1439044https://zhuanlan.zhihu.com/p/471175983以及文内列出的其他参考链接
仲一做嵌入式已经六七年了,现在在某芯片公司做驱动开发,之前带过团队、也面试招过人。简历看多了,面试也面多了,所以职业怎么走、简历哪里不对、面试卡在哪儿,这些事我算是有点发言权。
职业方向、简历、面试,哪块有疑惑都可以找我聊聊。想聊的,公众号后台回复「咨询」。
132