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 和 file 结构都可以无锁查找,因此必须使用 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),因此 fdtable 指针 (fdt) 必须在 locate_fd() 之后加载。

在较新的内核中,基于 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。