Linux 内核中的文件管理

本文档描述了文件(struct file)和文件描述符表(struct files)的锁定工作原理。

在 2.6.12 版本之前,文件描述符表使用锁(files->file_lock)和引用计数(files->count)进行保护。->file_lock 保护对表的所有文件相关字段的访问。->count 用于在通过 CLONE_FILES 标志克隆的任务之间共享文件描述符表。通常,这适用于 POSIX 线程。与内核中常见的引用计数模型一样,最后一个执行 put_files_struct() 的任务会释放文件描述符 (fd) 表。文件 (struct file) 本身使用引用计数 (->f_count) 进行保护。

在新的无锁文件描述符管理模型中,引用计数类似,但锁定基于 RCU。文件描述符表包含多个元素 - fd 集合(open_fds 和 close_on_exec)、文件指针数组、集合和数组的大小等。为了使更新对无锁读取器呈现原子性,文件描述符表的所有元素都位于单独的结构体中 - struct fdtable。files_struct 包含一个指向 struct fdtable 的指针,通过该指针访问实际的 fd 表。最初,fdtable 嵌入在 files_struct 本身中。在后续扩展 fdtable 时,会分配一个新的 fdtable 结构,files->fdtab 指向新结构。fdtable 结构使用 RCU 释放,无锁读取器要么看到旧的 fdtable,要么看到新的 fdtable,从而使更新呈现原子性。以下是 fdtable 结构的锁定规则 -

  1. 所有对 fdtable 的引用都必须通过 files_fdtable() 宏完成。

    struct fdtable *fdt;
    
    rcu_read_lock();
    
    fdt = files_fdtable(files);
    ....
    if (n <= fdt->max_fds)
            ....
    ...
    rcu_read_unlock();
    

    files_fdtable() 使用 rcu_dereference() 宏,该宏负责无锁解引用的内存屏障要求。fdtable 指针必须在读侧临界区内读取。

  2. 如上所述读取 fdtable 必须由 rcu_read_lock()/rcu_read_unlock()保护。

  3. 对于 fd 表的任何更新,必须持有 files->file_lock。

  4. 要查找给定 fd 的文件结构,读取器必须使用 lookup_fdget_rcu() 或 files_lookup_fdget_rcu() API。这些 API 负责无锁查找的屏障要求。

    一个例子

    struct file *file;
    
    rcu_read_lock();
    file = lookup_fdget_rcu(fd);
    rcu_read_unlock();
    if (file) {
            ...
            fput(file);
    }
    ....
    
  5. 由于 fdtable 和文件结构都可以无锁查找,因此必须使用 rcu_assign_pointer() API 安装它们。如果它们是无锁查找的,则必须使用 rcu_dereference()。但是,建议使用 files_fdtable() 和 lookup_fdget_rcu()/files_lookup_fdget_rcu(),它们会处理这些问题。

  6. 更新时,必须在持有 files->file_lock 的情况下查找 fdtable 指针。如果放弃 ->file_lock,则另一个线程可能会扩展文件,从而创建一个新的 fdtable 并使之前的 fdtable 指针过时。

    例如

    spin_lock(&files->file_lock);
    fd = locate_fd(files, file, start);
    if (fd >= 0) {
            /* locate_fd() may have expanded fdtable, load the ptr */
            fdt = files_fdtable(files);
            __set_open_fd(fd, fdt);
            __clear_close_on_exec(fd, fdt);
            spin_unlock(&files->file_lock);
    .....
    

    由于 locate_fd() 可以放弃 ->file_lock (并重新获取 ->file_lock),因此必须在 locate_fd() 之后加载 fdtable 指针 (fdt)。

在较新的内核上,基于 rcu 的文件查找已切换为依赖 SLAB_TYPESAFE_BY_RCU 而不是 call_rcu()。仅仅使用 atomic_long_inc_not_zero() 在 rcu 下获取对相关文件的引用已不再足够,因为该文件可能已被回收,并且其他人可能已经增加了引用。换句话说,调用者可能会看到来自较新用户的引用计数增加。因此,有必要在引用计数递增之前和之后验证指针是否相同。这种模式可以在 get_file_rcu() 和 __files_get_rcu() 中看到。

此外,在 rcu 查找下首次获取对 struct file 的引用之前,无法访问或检查其中的字段。不这样做总是很冒险,它仅适用于 struct file 中的非指针数据。使用 SLAB_TYPESAFE_BY_RCU,调用者必须首先获取引用,或者他们必须持有 fdtable 的 files_lock。