原文:https://evian-zhang.github.io/introduction-to-linux-x86_64-syscall/src/filesystem/open-openat-name_to_handle_at-open_by_handle_at-open_tree.html
系统调用号
open
为2,openat
为257。
函数原型
内核接口
asmlinkage long sys_open(const char __user *filename, int flags, umode_t mode);
asmlinkage long sys_openat(int dfd, const char __user *filename, int flags, umode_t mode);
glibc封装
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
int openat(int dirfd, const char *pathname, int flags);
int openat(int dirfd, const char *pathname, int flags, mode_t mode);
简介
我们知道,绝大多数文件相关的系统调用都是直接操作文件描述符(file descriptor),而open
和openat
这两个系统调用是一种创建文件描述符的方式。open
系统调用将打开路径为filename
的文件,而openat
则将打开相对描述符为dirfd
的目录,路径为filename
的文件。
详细来说,open
和openat
的行为是
filename
是绝对路径open
打开位于filename
的文件openat
打开位于filename
的文件,忽略dirfd
filename
是相对路径open
打开相对于当前目录,路径为filename
的文件openat
打开相对于dirfd
对应的目录,路径为filename
的文件;若dirfd
是定义在fcntl.h
中的宏AT_FDCWD
,则打开相对于当前目录,路径为filename
的文件。
接着,是“怎么打开”的问题。open
和openat
的参数flags
, mode
控制了打开文件的行为(mode
详情请见creat
系统调用。TODO:增加超链接)。
flags
用于打开文件的标志均定义在fcntl.h
头文件中。
文件访问模式标志(file access mode flag)
-
O_RDONLY
以只读方式打开。创建的文件描述符不可写。
-
O_WDONLY
以只写方式打开。创建的文件描述符不可读。
-
O_RDWR
以读写方式打开。创建的文件描述符既可读也可写。
-
O_EXEC
以只执行方式打开。创建的文件描述符只可以被执行。只能用于非目录路径。
-
O_SEARCH
以只搜索方式打开。创建的文件描述符值可以被用于搜索。只能用于目录路径。
POSIX标准要求在打开文件时,必须且只能使用上述标志位中的一个。而glibc的封装则要求在打开文件时,必须且只能使用前三个标志位(只读、只写、读写)中的一个。
文件创建标志(file creation flag)
文件创建标志控制的是open
和openat
在打开文件时的行为。部分比较常见的标志位有:
O_CLOEXEC
-
文件描述符将在调用
exec
函数族时关闭。 -
我们知道,当一个Linux进程使用
fork
创建子进程后,父进程原有的文件描述符也会复制给子进程。而常见的模式是在fork
之后使用exec
函数族替换当前进程空间。此时,由于替换前的所有变量都不会被继承,所以文件描述符将丢失,而丢失之后就无法关闭相应的文件描述符,造成泄露。如以下代码:#include <fcntl.h> #include <unistd.h> int main() { int fd = open("./text.txt", O_RDONLY); if (fork() == 0) { // child process char *const argv[] = {"./child", NULL}; execve("./child", argv, NULL); // fd left opened } else { // parent process sleep(30); } return 0; }
其中
./child
在启动30秒后会自动退出。在启动这个程序之后,我们使用
ps -aux | grep child
找到child
对应的进程ID,然后使用readlink /proc/xxx/fd/yyy
查看,其中
xxx
为进程ID,yyy
是fd
中的任意一个文件。我们调查fd
中的所有文件,一定能发现一个文件描述符对应text.txt
。也就是说,在执行execve
之后,子进程始终保持着text.txt
的描述符,且没有任何方法关闭它。 -
解决这个问题的方法一般有两种:
- 在
fork
之后,execve
之前使用close
关闭所有文件描述符。但是如果该进程在此之前创建了许多文件描述符,在这里就很容易漏掉,也不易于维护。 - 在使用
open
创建文件描述符时,加入O_CLOEXEC
标志位:
通过这种方法,在子进程使用int fd = open("./text.txt", O_RDONLY | O_CLOEXEC);
execve
时,文件描述符会自动关闭。
- 在
-
O_CREAT
- 当
filename
路径不存在时,创建相应的文件。 - 使用此标志时,
mode
参数将作为创建的文件的文件模式标志位。详情请见creat
系统调用。TODO: 增加超链接
- 当
O_EXCL
- 该标志位一般会与
O_CREAT
搭配使用。 - 如果
filename
路径存在相应的文件(包括符号链接),则open
会失败。
- 该标志位一般会与
O_DIRECTORY
- 如果
filename
路径不是一个目录,则失败。 - 这个标志位是用来替代
opendir
函数的。TODO: 解释其受拒绝服务攻击的原理。
- 如果
O_TRUNC
- 如果
filename
路径存在相应的文件,且以写的方式打开(即O_WDONLY
或O_RDWR
),那么将文件内容清空。
- 如果
文件状态标志(file status flag)
文件状态标志控制的是打开文件后的后续IO操作。
-
O_APPEND
- 使用此标志位时,在后续每一次
write
操作前,会将文件偏移移至文件末尾。(详情请见write)。
- 使用此标志位时,在后续每一次
-
O_ASYNC
- 使用信号驱动的IO。后续的IO操作将立即返回,同时在IO操作完成时发出相应的信号。
- 这种方式在异步IO模式中较少使用,主要由于这种基于中断的信号处理机制比系统调用的耗费更大,并且无法处理多个文件描述符同时完成IO操作。参考What's the difference between async and nonblocking in unix socket?。
- 对正常文件的描述符无效,对套接字等文件描述符有效。
-
O_NONBLOCK
-
后续的IO操作立即返回,而不是等IO操作完成后返回。
-
对正常文件的描述符无效,对套接字等文件描述符有效。
-
对于以此种方式打开的文件,后续的
read
和write
操作可能会产生特殊的错误——EAGAIN
(对于套接字文件还可能产生EWOULDBLOCK
)。这种错误的含义是接下来的读取或写入会阻塞,常见的原因可能是已经读取完毕了,或者写满了。比如说,当客户端发送的数据被服务器端全部读取之后,再次对以非阻塞形式打开的套接字文件进行
read
操作,就会返回EAGAIN
或EWOULDBLOCK
错误。
-
-
O_SYNC
与O_DSYNC
- 使用
O_SYNC
时,每一次write
操作结束前,都会将文件内容和元信息写入相应的硬件。 - 使用
O_DSYNC
时,每一次write
操作结束前,都会将文件内容写入相应的硬件(不保证元信息)。 - 这两种方法可以看作是在每一次
write
操作后使用fsync
。
- 使用
-
O_PATH
- 仅以文件描述符层次打开相应的文件。
- 我们使用
open
和openat
打开文件通常有两个目的:一是在文件系统中找到相应的文件,二是打开文件对其内容进行查看或修改。如果传入O_PATH
标志位,则只执行第一个目的,不仅耗费更低,同时所需要的权限也更少。 - 通过
O_PATH
打开的文件描述符可以传递给close
,fchdir
,fstat
等只在文件层面进行的操作,而不能传递给read
,write
等需要对文件内容进行查看或修改的操作。
其他注意点
此外,还有一些需要注意的。
在新的Linux内核(版本不低于2.26)中,glibc的封装open
底层调用的是openat
系统调用而不是open
系统调用(dirfd
为AT_FDCWD
)。我们可以在glibc源码的sysdeps/unix/sysv/linux/open.c
中看到:
int
__libc_open (const char *file, int oflag, ...)
{
int mode = 0;
if (__OPEN_NEEDS_MODE (oflag))
{
va_list arg;
va_start (arg, oflag);
mode = va_arg (arg, int);
va_end (arg);
}
return SYSCALL_CANCEL (openat, AT_FDCWD, file, oflag, mode);
}
open
的glibc封装实际上就是系统调用openat(AT_FDCWD, file, oflag, mode)
。
open
和openat
返回的是文件描述符(file descriptor),在Linux内核中,还有一个概念为文件描述(file description)。操作系统会维护一张全局的表,记录所有打开的文件的信息,如文件偏移、打开文件的状态标志等。这张全局的表的表项即为文件描述。而文件描述符则是对文件描述的引用。
每一次成功调用open
和openat
,都会在文件描述表中创建一个新的表项,返回的文件描述符即是对该表项的引用。而我们常见的dup
, fork
等复制的文件描述符,则会指向同一个文件描述。
文件描述创建之后,不会随着文件路径的修改而修改。也就是说,当我们通过open
打开了某个特定路径下的文件,然后我们将该文件移动到别的路径上,我们后续的read
, write
等操作仍能成功。
用法
我们在使用open
和openat
时,可以有如下的思考顺序:
- 打开文件的路径是绝对路径还是相对路径
- 绝对路径使用
open
和openat
都可以 - 对于相对路径而言,如果相对于当前目录,则可以使用
open
,但大部分情况而言还是openat
适用性更广(相对于当前目录可以传递AT_FDCWD
给dirfd
参数)
- 绝对路径使用
- 打开文件是否需要读、写
- 只需要读,
flags
加入标志位O_RDONLY
- 只需要写,
flags
加入标志位O_WDONLY
- 既需要读,又需要写,
flags
加入标志位O_RDWR
- 只需要读,
- 对于可能会产生子进程并使用
exec
函数族的程序,flags
加入标志位O_CLOEXEC
- 如果需要对文件进行写入:
- 如果需要在写之前清空文件内容,
flags
加入标志位O_TRUNC
- 如果需要在文件末尾追加,
flags
加入标志位O_APPEND
- 如果文件不存在的时候需要创建文件,
flags
加入标志位O_CREAT
,并且传递相应的文件模式标志位给mode
- 如果需要在写之前清空文件内容,
以下几种都是合理的使用方式:
int fd1 = open(filename, O_RDONLY);
int fd2 = open(filename, O_RDWR | O_APPEND);
int fd3 = open(filename, O_WDONLY | O_CLOEXEC | O_TRUNC);
实现
open
和openat
的实现均位于fs/open.c
文件中,与其相关的函数是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;
}
由其实现可知,无论路径是否一样,flag
是否相同,open
总会使用新的文件描述符。也就是说:
int a = open("./text.txt", O_RDONLY);
int b = open("./text.txt", O_RDONLY);
尽管调用参数一样,a
和b
依然是不同的。
此外,这个函数调用了do_filp_open
函数作为真正的操作,而其核心是实现在fs/namei.c
的函数path_openat
:
static struct file *path_openat(struct nameidata *nd, const struct open_flags *op, unsigned flags)
{
struct file *file;
int error;
file = alloc_empty_file(op->open_flag, current_cred());
if (IS_ERR(file))
return file;
if (unlikely(file->f_flags & __O_TMPFILE)) {
error = do_tmpfile(nd, flags, op, file);
} else if (unlikely(file->f_flags & O_PATH)) {
error = do_o_path(nd, flags, file);
} else {
const char *s = path_init(nd, flags);
while (!(error = link_path_walk(s, nd)) &&
(error = do_last(nd, file, op)) > 0) {
nd->flags &= ~(LOOKUP_OPEN|LOOKUP_CREATE|LOOKUP_EXCL);
s = trailing_symlink(nd);
}
terminate_walk(nd);
}
if (likely(!error)) {
if (likely(file->f_mode & FMODE_OPENED))
return file;
WARN_ON(1);
error = -EINVAL;
}
fput(file);
if (error == -EOPENSTALE) {
if (flags & LOOKUP_RCU)
error = -ECHILD;
else
error = -ESTALE;
}
return ERR_PTR(error);
}
可见对于大部分情况而言,就是按照符号链接的路径找到最终的文件,然后用do_last
打开文件。