小弧光的博客

公众号:小弧光黑板报

导航

open与openat

原文: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),而openopenat这两个系统调用是一种创建文件描述符的方式。open系统调用将打开路径为filename的文件,而openat则将打开相对描述符为dirfd的目录,路径为filename的文件。

详细来说,openopenat的行为是

  • filename是绝对路径
    • open打开位于filename的文件
    • openat打开位于filename的文件,忽略dirfd
  • filename是相对路径
    • open打开相对于当前目录,路径为filename的文件
    • openat打开相对于dirfd对应的目录,路径为filename的文件;若dirfd是定义在fcntl.h中的宏AT_FDCWD,则打开相对于当前目录,路径为filename的文件。

接着,是“怎么打开”的问题。openopenat的参数flagsmode控制了打开文件的行为(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)

文件创建标志控制的是openopenat在打开文件时的行为。部分比较常见的标志位有:

  • 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,yyyfd中的任意一个文件。我们调查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_WDONLYO_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操作完成后返回。

    • 对正常文件的描述符无效,对套接字等文件描述符有效。

    • 对于以此种方式打开的文件,后续的readwrite操作可能会产生特殊的错误——EAGAIN(对于套接字文件还可能产生EWOULDBLOCK)。

      这种错误的含义是接下来的读取或写入会阻塞,常见的原因可能是已经读取完毕了,或者写满了。比如说,当客户端发送的数据被服务器端全部读取之后,再次对以非阻塞形式打开的套接字文件进行read操作,就会返回EAGAINEWOULDBLOCK错误。

  • O_SYNCO_DSYNC

    • 使用O_SYNC时,每一次write操作结束前,都会将文件内容和元信息写入相应的硬件。
    • 使用O_DSYNC时,每一次write操作结束前,都会将文件内容写入相应的硬件(不保证元信息)。
    • 这两种方法可以看作是在每一次write操作后使用fsync
  • O_PATH

    • 仅以文件描述符层次打开相应的文件。
    • 我们使用openopenat打开文件通常有两个目的:一是在文件系统中找到相应的文件,二是打开文件对其内容进行查看或修改。如果传入O_PATH标志位,则只执行第一个目的,不仅耗费更低,同时所需要的权限也更少。
    • 通过O_PATH打开的文件描述符可以传递给closefchdirfstat等只在文件层面进行的操作,而不能传递给readwrite等需要对文件内容进行查看或修改的操作。

其他注意点

此外,还有一些需要注意的。

在新的Linux内核(版本不低于2.26)中,glibc的封装open底层调用的是openat系统调用而不是open系统调用(dirfdAT_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)

openopenat返回的是文件描述符(file descriptor),在Linux内核中,还有一个概念为文件描述(file description)。操作系统会维护一张全局的表,记录所有打开的文件的信息,如文件偏移、打开文件的状态标志等。这张全局的表的表项即为文件描述。而文件描述符则是对文件描述的引用。

每一次成功调用openopenat,都会在文件描述表中创建一个新的表项,返回的文件描述符即是对该表项的引用。而我们常见的dupfork等复制的文件描述符,则会指向同一个文件描述。

文件描述创建之后,不会随着文件路径的修改而修改。也就是说,当我们通过open打开了某个特定路径下的文件,然后我们将该文件移动到别的路径上,我们后续的readwrite等操作仍能成功。

用法

我们在使用openopenat时,可以有如下的思考顺序:

  1. 打开文件的路径是绝对路径还是相对路径
    • 绝对路径使用openopenat都可以
    • 对于相对路径而言,如果相对于当前目录,则可以使用open,但大部分情况而言还是openat适用性更广(相对于当前目录可以传递AT_FDCWDdirfd参数)
  2. 打开文件是否需要读、写
    • 只需要读,flags加入标志位O_RDONLY
    • 只需要写,flags加入标志位O_WDONLY
    • 既需要读,又需要写,flags加入标志位O_RDWR
  3. 对于可能会产生子进程并使用exec函数族的程序,flags加入标志位O_CLOEXEC
  4. 如果需要对文件进行写入:
    • 如果需要在写之前清空文件内容,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);

实现

openopenat的实现均位于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);

尽管调用参数一样,ab依然是不同的。

此外,这个函数调用了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打开文件。

posted on 2023-05-11 13:45  小弧光  阅读(597)  评论(0编辑  收藏  举报