在这里插入图片描述

[toc]

fs/stat.c 文件元数据获取(File Metadata Retrieval) stat系列系统调用的实现核心

历史与背景

这项技术是为了解决什么特定问题而诞生的?

这项技术是为了解决一个操作系统中最基本的需求:在不读取文件内容的情况下,获取文件的属性或元数据(Metadata)。几乎所有的用户空间程序和系统工具在操作文件之前或之后,都需要了解文件的基本信息。

stat.c 及其实现的系统调用解决了以下具体问题:

  • 权限检查:程序需要知道文件的所有者、所属组以及权限位,以判断当前用户是否有权读取、写入或执行该文件。
  • 文件类型识别:程序需要区分一个路径指向的是普通文件、目录、符号链接、还是设备文件等。
  • 状态比较:工具如 make 需要比较源文件和目标文件的最后修改时间,以决定是否需要重新编译。
  • 信息展示:命令如 ls -l 需要获取文件的所有元数据来向用户展示详细列表。
  • 资源管理:程序需要知道文件的大小以预估存储占用或网络传输时间。

它的发展经历了哪些重要的里程碑或版本迭代?

stat() 系统调用是Unix的基石之一,自早期版本就已存在。其发展主要体现在为解决新出现的问题而引入的变体和扩展。

  • stat() 和 fstat():最初的系统调用。stat() 通过文件路径获取元数据,fstat() 通过一个已经打开的文件描述符获取元数据。
  • lstat() 的引入:随着符号链接(Symbolic Link)的出现,需要一种方法来获取符号链接本身的元数据,而不是它所指向的文件的元数据。为此,lstat() 被引入,它和stat()功能几乎一样,但不会跟随(dereference)符号链接。
  • 64位扩展:随着文件大小和时间戳超出32位整数的表示范围,引入了支持64位字段的 stat64 结构体和相应的系统调用。在现代64位系统中,stat 结构体本身就使用64位字段,这些调用被统一。
  • *at() 系列系统调用的出现:为了解决基于路径的操作中存在的竞争条件(TOCTOU - Time-of-check to time-of-use)和简化相对路径操作,fstatat() 和 newfstatat() 被引入。它们允许相对于一个目录的文件描述符来查找目标文件,增强了安全性和灵活性。

目前该技术的社区活跃度和主流应用情况如何?

fs/stat.c 是Linux内核VFS(虚拟文件系统)层中最稳定、最核心的部分之一。它不是一个频繁变动的功能,而是一个必须为所有文件系统提供支持的基础接口。它的活跃度主要体现在为新的文件系统提供支持、修复跨架构的兼容性问题以及进行微小的性能优化上。它是所有Linux系统和符合POSIX标准的系统的基础,被几乎每一个与文件交互的应用程序所使用。

核心原理与设计

它的核心工作原理是什么?

fs/stat.c 本身是VFS层的一个前端,其核心原理是将用户空间的请求分派给底层具体文件系统的实现。

  1. 系统调用入口:用户空间程序调用stat(), lstat() 或 fstat(),触发系统调用,进入内核态。
  2. 参数处理:内核从用户空间拷贝路径字符串或获取文件描述符。
  3. 路径解析(对于 stat/lstat):内核的路径查找代码会逐级解析路径。这个过程会遍历目录项(dentry),查找与路径组件匹配的条目,并最终定位到目标文件的inode。stat()和lstat()在处理符号链接时会采取不同的行为。
  4. 获取inode(对于 fstat):fstat() 操作的是一个已打开的文件描述符,内核可以直接通过struct file找到关联的inode,无需进行路径解析,因此效率更高。
  5. 调用文件系统后端实现:VFS层不直接存储元数据。它会通过inode->i_op->getattr函数指针,调用具体文件系统(如ext4, XFS, NFS)提供的getattr函数。
  6. 填充 kstat 结构体:底层文件系统的getattr函数会从其内部的数据结构(通常是磁盘inode的内存缓存)中读取元数据,并填充一个内核内部的、与文件系统无关的struct kstat结构体。
  7. 数据拷贝回用户空间:fs/stat.c中的代码最后负责将struct kstat中的信息转换并拷贝到用户空间程序提供的struct stat缓冲区中。这个过程通过copy_to_user()完成。

它的主要优势体现在哪些方面?

  • 抽象与统一:通过VFS层,为用户空间提供了单一、稳定的接口,而无需关心底层文件系统的具体实现是ext4, Btrfs还是NFS。
  • 效率:获取元数据是一个比读取文件内容快得多的操作,是文件操作性能优化的基础。
  • 功能完备:stat结构体提供了详尽的文件信息,满足了绝大多数应用场景的需求。

它存在哪些已知的劣势、局限性或在特定场景下的不适用性?

  • 性能开销:对于stat()和lstat(),每次调用都可能涉及多次磁盘I/O(如果路径上的目录项或inode不在缓存中)。对于网络文件系统(如NFS),每次调用都可能是一次网络往返,延迟很高。
  • 竞争条件(Race Condition):经典的TOCTOU问题。程序先调用stat()检查文件权限或属性,然后基于检查结果调用open()。在这两次调用之间,文件可能已经被其他进程修改、删除或替换,导致open()的结果与预期不符。*at()系列调用部分缓解了此问题。
  • 时间戳精度:stat结构体中的时间戳精度受限于底层文件系统的支持。虽然内核支持纳秒级精度,但并非所有文件系统都能提供。

使用场景

在哪些具体的业务或技术场景下,它是首选解决方案?请举例说明。

它是获取文件元数据的唯一标准解决方案。

  • ls -l:这是最直观的例子。ls命令对目录中的每个文件都调用lstat(),以获取并显示其权限、链接数、所有者、大小、修改时间等信息。
  • make 等构建系统:make在决定是否需要重新生成一个目标文件时,会分别stat()源文件和目标文件,比较它们的最后修改时间(st_mtime)。
  • find 命令:find命令使用stat()的结果来匹配各种条件,如-type f(文件类型为普通文件)、-size +1M(文件大小超过1MB)、-mtime -7(7天内被修改过)。
  • Web服务器 (Nginx, Apache):当客户端请求一个静态文件时,服务器会stat()该文件以获取其大小,用于设置HTTP响应头中的Content-Length字段;同时获取最后修改时间,用于Last-Modified和缓存验证。
  • Shell脚本:脚本中常用的文件测试操作符,如[ -f file ](检查是否为普通文件)或[ -d dir ](检查是否为目录),其底层实现就是调用stat()系统调用。

是否有不推荐使用该技术的场景?为什么?

  • 替代访问控制检查:不应使用 stat() 来检查权限,然后再 open() 文件。正确的做法是直接尝试 open() 文件,然后检查返回值和errno来判断操作是否被允许。这可以避免TOCTOU竞争条件。
  • 监控文件变化:如果需要实时监控文件的内容或元数据变化,反复调用stat()进行轮询是一种非常低效的方式。此时应该使用inotify或fanotify等专门的事件通知机制,由内核在文件发生变化时主动通知应用程序。

对比分析

请将其 与 其他相似技术 进行详细对比。

stat系列调用在功能上是独特的,但可以和其变体以及其他相关机制进行对比。

stat 系列调用内部对比

特性 stat(const char *path, ...) lstat(const char *path, ...) fstat(int fd, ...) fstatat(int dirfd, ...)
输入参数 文件路径 文件路径 文件描述符 目录FD + 相对/绝对路径
符号链接处理 跟随 (Dereference) 不跟随 不适用 (文件已打开) 可通过flag控制是否跟随
性能 中等 (需要路径解析) 中等 (需要路径解析) 高 (无需路径解析) 中等 (需要部分路径解析)
安全性 易受TOCTOU竞争条件影响 易受TOCTOU竞争条件影响 不受路径相关的竞争条件影响 减少了TOCTOU风险

stat (轮询) vs. inotify (事件通知)

特性 stat (Polling) inotify (Event-driven)
工作模式 同步/请求-响应。应用程序主动查询文件状态。 异步/事件通知。内核在文件状态改变时,主动通知应用程序。
资源消耗 在需要频繁检查时,会消耗大量CPU和I/O资源。 非常低。应用程序可以睡眠,直到被内核唤醒,几乎没有空闲开销。
实时性 实时性取决于轮询的频率。频率越高,资源消耗越大。 高。事件几乎是实时推送给应用程序的。
适用场景 单次、按需获取文件元数据。 需要持续监控文件或目录变化的场景,如文件同步工具、代码热重载、日志监控等。

include/linux/fs.h

Inode ID到VFS ID转换:支持用户命名空间的核心抽象

本代码片段定义了两个小巧但极为关键的内-内联(inline)辅助函数。它们的核心功能是将一个inode上存储的持久化、全局性的UID和GID,根据一个特定挂载点的ID映射规则(mnt_idmap),转换为VFS层所见的、可能是虚拟化的UID和GID(vfsuid_t, vfsgid_t)。这是实现Linux用户命名空间(User Namespaces)和ID映射挂载(Idmapped Mounts)功能的基石,是容器等隔离技术能够在文件系统层面正确处理权限的核心机制。

实现原理分析

这两个函数本身是简单的封装器,其复杂性隐藏在它们调用的 make_vfsuid 和 make_vfsgid 函数中。它们的工作流程如下:

  1. 获取上下文: 函数接收两个关键的上下文信息:

    • idmap: 来自特定vfsmount结构的ID映射表。这个表定义了如何将一个用户命名空间中的ID转换成另一个命名空间中的ID。
    • inode: 目标文件。
  2. 提取源信息: 从inode中提取出进行映射所需的源信息:

    • inode->i_uid 或 inode->i_gid: 这是要被转换的原始ID值。
    • i_user_ns(inode): 这是一个辅助函数,用于获取该inode的UID/GID所属的用户命名空间。这一点至关重要,因为一个ID值只有在其所属的命名空间中才有确切的含义。
  3. 执行映射: 将上述三个参数(idmap, inode的用户命名空间, inode的ID)传递给核心映射函数 make_vfsuid 或 make_vfsgid。make_vfs*id 函数会执行实际的查找和转换逻辑。如果 idmap 表中存在一个适用于该源命名空间和ID的映射规则,它就会返回转换后的 vfs*id_t。如果找不到合适的映射,它会返回一个特殊的无效值(INVALID_VFSUID / INVALID_VFSGID)。

代码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
static inline struct user_namespace *i_user_ns(const struct inode *inode)
{
return inode->i_sb->s_user_ns;
}

// i_uid_into_vfsuid: 根据ID映射表,将inode的i_uid进行转换。
// @idmap: 挂载点的ID映射表。
// @inode: 需要映射其UID的inode。
// 返回值: 经过@idmap映射后的vfsuid_t。如果无法映射,则返回INVALID_VFSUID。
static inline vfsuid_t i_uid_into_vfsuid(struct mnt_idmap *idmap,
const struct inode *inode)
{
// 调用核心函数make_vfsuid,传入三个关键信息:
// 1. idmap: 目标映射规则。
// 2. i_user_ns(inode): inode的UID所属的源用户命名空间。
// 3. inode->i_uid: 要映射的UID值。
return make_vfsuid(idmap, i_user_ns(inode), inode->i_uid);
}

// i_gid_into_vfsgid: 根据ID映射表,将inode的i_gid进行转换。
// @idmap: 挂载点的ID映射表。
// @inode: 需要映射其GID的inode。
// 返回值: 经过@idmap映射后的vfsgid_t。如果无法映射,则返回INVALID_VFSGID。
static inline vfsgid_t i_gid_into_vfsgid(struct mnt_idmap *idmap,
const struct inode *inode)
{
// 逻辑与i_uid_into_vfsuid完全相同,只是处理的是组ID(GID)。
return make_vfsgid(idmap, i_user_ns(inode), inode->i_gid);
}

include/uapi/linux/stat.h

statx() 请求掩码:定义用户空间与内核的文件属性查询接口

本代码片段来自一个内核头文件,其核心功能是定义了一组名为 STATX_* 的宏。这些宏共同构成了一个位掩码(bitmask),用于 statx() 这个现代化的文件状态查询系统调用。用户空间程序通过在调用 statx() 时传递这些标志的组合,来精确地告知内核它需要查询哪些文件属性(例如,只需要文件大小,或者只需要文件类型和权限)。这提供了一个比传统的 stat() 系统调用更灵活、更高效、更具扩展性的文件元数据获取机制。

实现原理分析

该接口的设计遵循了经典的位掩码原则,旨在创建一个稳定且高效的应用程序二进制接口(ABI)。

  1. 位掩码设计: 每一个 STATX_* 宏都被定义为一个唯一的、2的幂的无符号整数(例如 0x00000001U, 0x00000002U, 0x00000004U…)。这确保了每个宏在二进制表示中只占用一个比特位。因此,用户可以通过按位或(|)运算将多个请求组合成一个单一的整数掩码,而内核可以通过按位与(&)运算来快速检查某个特定的属性是否被请求。

  2. 请求与结果: 注释中的 “Want/got”(想要/得到)表明这个掩码具有双重作用。在调用 statx() 时,用户在 mask 参数中设置这些位来表示“想要”哪些信息。当系统调用返回时,内核会在 struct statx 结构体的 stx_mask 成员中设置这些位,来表示它“得到”并成功填充了哪些信息。这允许应用程序验证它所请求的字段是否都有效。

  3. 查询粒度: 与总是返回所有信息的 stat() 不同,statx() 提供了极高的查询粒度。例如,它可以区分文件类型(STATX_TYPE,即 S_IFMT 部分)和文件权限模式(STATX_MODE),而 stat() 会将它们合并在 st_mode 中返回。

  4. 便利性与兼容性:

    • STATX_BASIC_STATS: 这个宏是所有对应于旧 struct stat 结构体中字段的标志的集合。它为那些需要与旧 stat 行为兼容的程序提供了一个方便的快捷方式。
    • STATX_ALL 和 #ifndef __KERNEL__: STATX_ALL 是一个已废弃的宏,为了向后兼容而保留在用户空间的头文件中。#ifndef __KERNEL__ 条件编译块确保了这个废弃的宏不会在内核代码内部被使用,鼓励内核开发者使用更精确的掩码。

代码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
/*
* stx_mask 的标志位
*
* 用于 statx() 和 struct statx::stx_mask 的查询请求/结果掩码。
*
* 在调用 statx() 时,应在 mask 参数中设置这些位来请求特定的项目。
*/
// 请求/获取 文件类型 (stx_mode 中的 S_IFMT 部分)
#define STATX_TYPE 0x00000001U
// 请求/获取 文件权限模式 (stx_mode 中的 S_IFMT 之外的部分)
#define STATX_MODE 0x00000002U
// 请求/获取 硬链接数 (stx_nlink)
#define STATX_NLINK 0x00000004U
// 请求/获取 用户ID (stx_uid)
#define STATX_UID 0x00000008U
// 请求/获取 组ID (stx_gid)
#define STATX_GID 0x00000010U
// 请求/获取 最后访问时间 (stx_atime)
#define STATX_ATIME 0x00000020U
// 请求/获取 最后修改时间 (stx_mtime)
#define STATX_MTIME 0x00000040U
// 请求/获取 最后状态变更时间 (stx_ctime)
#define STATX_CTIME 0x00000080U
// 请求/获取 Inode号 (stx_ino)
#define STATX_INO 0x00000100U
// 请求/获取 文件大小 (stx_size)
#define STATX_SIZE 0x00000200U
// 请求/获取 分配的块数 (stx_blocks)
#define STATX_BLOCKS 0x00000400U
// 一个便利宏,包含了所有传统 stat 结构体中的字段。
#define STATX_BASIC_STATS 0x000007ffU
// 请求/获取 文件创建时间 (stx_btime)
#define STATX_BTIME 0x00000800U
// 获取 挂载点ID (stx_mnt_id)
#define STATX_MNT_ID 0x00001000U
// 请求/获取 Direct I/O 对齐信息
#define STATX_DIOALIGN 0x00002000U
// (以下为更高级或特定文件系统的标志,此处省略部分注释)
#define STATX_MNT_ID_UNIQUE 0x00004000U
#define STATX_SUBVOL 0x00008000U
#define STATX_WRITE_ATOMIC 0x00010000U
#define STATX_DIO_READ_ALIGN 0x00020000U

// 为未来 struct statx 扩展保留的位。
#define STATX__RESERVED 0x80000000U

// 仅为用户空间头文件定义,内核中不使用。
#ifndef __KERNEL__
/*
* 这个宏已废弃,其值未来将保持不变。
* 为避免混淆,请使用等效的 (STATX_BASIC_STATS | STATX_BTIME) 代替。
*/
#define STATX_ALL 0x00000fffU
#endif

fs/stat.c

多粒度时间戳填充:实现按需高精度的时间戳更新

本代码片段定义了 fill_mg_cmtime 函数,它是内核“多粒度时间戳(Multigrain Timestamps)”机制的核心实现。其主要功能是:当有进程查询一个文件的 ctime 或 mtime 时,该函数不仅返回当前的时间戳,还会原子地在 inode 中设置一个 I_CTIME_QUERIED 标志。这个标志的作用是“通知”内核,该文件正在被“观察”,因此下一次对该文件的修改必须生成一个与当前时间戳不同的、更高精度的时间戳。这解决了在同一个系统时钟节拍内发生多次文件修改而导致时间戳不变的问题,对于NFS客户端等依赖时间戳进行缓存一致性判断的应用至关重要。

实现原理分析

该函数的实现巧妙地利用了一个标志位和原子操作,来平衡性能和精度。

  1. 原子访问: 函数首先将 inode->i_ctime_nsec 字段的地址强制转换为 atomic_t *。i_ctime_nsec 的最高位被用作 I_CTIME_QUERIED 标志。为了在多核或可抢占环境下无竞争地读取和设置这个标志,必须使用原子操作。

  2. 请求检查 (优化): 首先检查调用者的 request_mask。如果用户空间的 stat 调用既没有请求 STATX_CTIME 也没有请求 STATX_MTIME,函数会直接返回,避免了不必要的工作。

  3. 读取与标志设置:

    • 函数首先使用 atomic_read 原子地读取 i_ctime_nsec 的当前值(包含标志位)。
    • 然后,它检查 I_CTIME_QUERIED 标志是否未被设置。
    • 如果标志未被设置,它会调用 atomic_fetch_or(I_CTIME_QUERIED, pcn)。这是一个关键的原子操作,它执行两个步骤:
      a. 将 I_CTIME_QUERIED 位设置到 inode->i_ctime_nsec 中。
      b. 返回设置之前的旧值。
    • 这个“检查并设置”的过程确保了 I_CTIME_QUERIED 标志只被设置一次,直到下一次文件被修改时由内核清除。
  4. 清理并返回: 无论 I_CTIME_QUERIED 标志之前是否被设置,在将纳秒值填充到 stat 结构体之前,都必须通过 & ~I_CTIME_QUERIED 将其清除。这确保了返回给用户空间的是一个纯净的、不含标志位的纳秒值。

  5. 追踪: trace_fill_mg_cmtime 是一个追踪点,用于内核开发者调试或分析该功能的行为。

代码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// fill_mg_cmtime: 填充mtime和ctime,并将ctime标记为“已被查询”。
// @stat: 用于存储结果值的kstat结构体。
// @request_mask: STATX_*掩码,指示请求了哪些时间戳。
// @inode: 属性来源的inode。
void fill_mg_cmtime(struct kstat *stat, u32 request_mask, struct inode *inode)
{
// 将i_ctime_nsec字段的地址转换为atomic_t指针,以便进行原子操作。
atomic_t *pcn = (atomic_t *)&inode->i_ctime_nsec;

// 优化:如果调用者既没请求ctime也没请求mtime,则直接返回。
if (!(request_mask & (STATX_CTIME|STATX_MTIME))) {
// 在结果掩码中清除相应的位,表明未填充这些字段。
stat->result_mask &= ~(STATX_CTIME|STATX_MTIME);
return;
}

// 获取mtime(mtime没有标志位,直接获取即可)。
stat->mtime = inode_get_mtime(inode);
// 获取ctime的秒部分。
stat->ctime.tv_sec = inode->i_ctime_sec;
// 原子地读取ctime的纳秒部分(此时可能包含I_CTIME_QUERIED标志)。
stat->ctime.tv_nsec = (u32)atomic_read(pcn);

// 检查I_CTIME_QUERIED标志是否 *未* 被设置。
if (!(stat->ctime.tv_nsec & I_CTIME_QUERIED))
// 如果未设置,则原子地设置该标志,并获取设置前的旧值。
// 这是核心的“test-and-set”逻辑。
stat->ctime.tv_nsec = ((u32)atomic_fetch_or(I_CTIME_QUERIED, pcn));

// 无论之前标志是否被设置,在返回给用户前,必须清除标志位,得到纯净的纳秒值。
stat->ctime.tv_nsec &= ~I_CTIME_QUERIED;
// 调用追踪点,用于内核调试。
trace_fill_mg_cmtime(inode, &stat->ctime, &stat->mtime);
}
// 导出符号,使得其他内核模块可以调用此函数。
EXPORT_SYMBOL(fill_mg_cmtime);

Inode 属性访问器:安全高效地读取文件元数据

本代码片段定义了一组核心的、性能攸关的inline函数,用于从inode结构体中安全地读取关键属性,如文件大小(i_size)、块大小以及各种时间戳。这些函数是VFS层的基础组件,被generic_fillattr等更高层函数调用。其中,i_size_read的设计尤为关键,它通过多种平台特定的技术来保证在并发或抢占环境下读取的原子性和一致性。

实现原理分析

这些函数作为inode字段的直接访问器,其实现重点在于确保数据一致性和效率。

  1. 原子读取文件大小 (i_size_read):
    这是片段中最复杂的函数,其核心目标是原子地读取一个64位的loff_t类型的文件大小。在不同体系结构和内核配置下,实现原子性的方式不同:

    • 32位SMP系统 (BITS_PER_LONG==32 && CONFIG_SMP): 在32位多核系统上,一个64位读操作不是原子的,可能被其他CPU上的写操作中断,导致“撕裂读”(Torn Read)。这里使用seqcount(顺序锁)机制。读取方在读取数据前后检查一个序列号。如果在读取期间有写入发生,序列号会改变,读取方就会发现并重试,直到成功完成一次未被干扰的读取。
    • 32位可抢占单核系统 (BITS_PER_LONG==32 && CONFIG_PREEMPTION): 在32位单核系统上,虽然没有其他CPU的干扰,但当前任务可能在执行64位读操作(由两条32位读指令组成)的中间被抢占。如果被抢占后,新的任务修改了i_size,那么当原始任务恢复执行时,它会读到一半旧值和一半新值,同样导致数据损坏。通过preempt_disable()/preempt_enable()来临时禁止内核抢占,可以保证这两条读指令连续执行,从而实现原子性。
    • 64位系统或其他情况: 在64位系统上,64位读操作本身就是原子的,不需要特殊保护。在32位非抢占单核系统上,也不会发生任务切换,因此也是安全的。smp_load_acquire()在这里用作内存屏障,它确保在此次读取之后的所有内存操作不会被编译器或CPU乱序执行到它前面,这对于保证数据依赖关系的正确性至关重要。
  2. 块大小计算 (i_blocksize):
    这是一个高效的计算函数。inode中存储的是i_blkbits,即块大小以2为底的对数(log2(blocksize))。因此,通过位移运算1 << node->i_blkbits可以极快地计算出实际的块大小(2^i_blkbits),避免了开销较大的乘法或除法运算。

  3. 时间戳读取:

    • inode_get_atime, inode_get_ctime: 这些函数将存储为秒(tv_sec)和纳秒(tv_nsec)两部分的inode成员,组合成一个标准的timespec64结构体返回。
    • 多粒度时间戳 (I_CTIME_QUERIED): 在inode_get_ctime_nsec中,有一个特殊的& ~I_CTIME_QUERIED操作。inode的i_ctime_nsec字段的最高位(bit 31)被用作一个标志位(I_CTIME_QUERIED)。这个机制允许内核追踪是否有进程正在“积极观察”这个时间戳。内核可以据此决定是提供高精度的时间戳更新还是较为粗略的更新,以优化性能。当读取纳秒值时,必须通过位掩码将这个标志位清除,以获得真实的纳秒数值。

代码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
// i_size_read: 以原子方式安全地读取inode的文件大小。
static inline loff_t i_size_read(const struct inode *inode)
{
// 条件编译:针对32位对称多处理(SMP)系统。
#if BITS_PER_LONG==32 && defined(CONFIG_SMP)
loff_t i_size;
unsigned int seq;

// 使用seqcount锁来防止多核并发读写导致的“撕裂读”。
do {
// 读取开始前的序列号。
seq = read_seqcount_begin(&inode->i_size_seqcount);
// 读取64位的文件大小。
i_size = inode->i_size;
// 检查读取期间序列号是否发生变化,如果变化则重试。
} while (read_seqcount_retry(&inode->i_size_seqcount, seq));
return i_size;
// 条件编译:针对32位、单核、但开启内核抢占的系统。
#elif BITS_PER_LONG==32 && defined(CONFIG_PREEMPTION)
loff_t i_size;

// 临时禁止内核抢占。
preempt_disable();
// 在禁止抢占期间读取64位大小,保证两条32位读指令不会被中断。
i_size = inode->i_size;
// 重新允许内核抢占。
preempt_enable();
return i_size;
#else
// 针对64位系统或32位非抢占系统(这些情况下64位读取是原子的)。
// smp_load_acquire提供内存屏障,确保有序性。
return smp_load_acquire(&inode->i_size);
#endif
}

// i_blocksize: 从inode计算文件系统的块大小。
static inline unsigned int i_blocksize(const struct inode *node)
{
// inode->i_blkbits 存储的是块大小的log2值,通过左移1位实现快速的2的幂次方计算。
return (1 << node->i_blkbits);
}

// inode_get_atime_sec: 获取inode的最后访问时间的秒部分。
static inline time64_t inode_get_atime_sec(const struct inode *inode)
{
return inode->i_atime_sec;
}

// inode_get_atime_nsec: 获取inode的最后访问时间的纳秒部分。
static inline long inode_get_atime_nsec(const struct inode *inode)
{
return inode->i_atime_nsec;
}

// inode_get_atime: 将秒和纳秒部分组合成一个timespec64结构体。
static inline struct timespec64 inode_get_atime(const struct inode *inode)
{
struct timespec64 ts = { .tv_sec = inode_get_atime_sec(inode),
.tv_nsec = inode_get_atime_nsec(inode) };

return ts;
}

// I_CTIME_QUERIED: 定义一个宏,表示i_ctime_nsec字段的最高位,用作“时间戳被查询”的标志。
#define I_CTIME_QUERIED ((u32)BIT(31))

// inode_get_ctime_sec: 获取inode的最后变更时间的秒部分。
static inline time64_t inode_get_ctime_sec(const struct inode *inode)
{
return inode->i_ctime_sec;
}

// inode_get_ctime_nsec: 获取inode的最后变更时间的纳秒部分。
static inline long inode_get_ctime_nsec(const struct inode *inode)
{
// 返回纳秒值,并使用位掩码清除I_CTIME_QUERIED标志位,以得到真实的纳秒值。
return inode->i_ctime_nsec & ~I_CTIME_QUERIED;
}

// inode_get_ctime: 将变更时间的秒和纳秒部分组合成一个timespec64结构体。
static inline struct timespec64 inode_get_ctime(const struct inode *inode)
{
struct timespec64 ts = { .tv_sec = inode_get_ctime_sec(inode),
.tv_nsec = inode_get_ctime_nsec(inode) };

return ts;
}

通用属性填充函数:从Inode构建文件状态信息

本代码片段定义了 VFS 层的一个核心辅助函数 generic_fillattr。其主要功能是从一个给定的 inode 结构体(文件的内存抽象)中提取出标准的文件属性(如模式、大小、链接数、所有者、时间戳等),并用这些信息填充一个 kstat 结构体。这个函数是 stat、fstat、lstat 等一系列文件状态查询系统调用的基础,为用户空间提供了获取文件元数据的标准途径。

实现原理分析

generic_fillattr 是文件系统中 .getattr inode 操作的默认或基础实现,它封装了从 inode 到 kstat 的通用转换逻辑。

  1. 所有者ID映射 (User Namespace): 函数首先处理用户ID(UID)和组ID(GID)。它不直接拷贝 inode->i_uid,而是通过 i_uid_into_vfsuid 和 vfsuid_into_kuid 等一系列函数进行转换。这是为了支持ID映射挂载(idmapped mounts)和用户命名空间(user namespaces)。在一个容器或命名空间中,一个文件在物理上可能属于UID 1000,但在命名空间内部可能需要显示为UID 0 (root)。idmap 参数承载了这种映射关系,确保了在正确的上下文中显示正确的UID/GID。如果不存在ID映射,则会传入一个 nop_mnt_idmap,此时转换就等同于直接拷贝。
  2. 基础属性拷贝: 函数直接将 inode 中的一些基础字段拷贝到 kstat 结构中,包括:
    • dev, ino: 设备号和inode号,唯一标识一个文件。
    • mode: 文件类型和权限。
    • nlink: 硬链接数量。
    • rdev: 对于特殊设备文件,表示其主/次设备号。
  3. 大小与时间戳:
    • 通过 i_size_read 读取文件大小,这是一个安全的读取函数,可以处理并发更新。
    • 通过 inode_get_atime, inode_get_ctime, inode_get_mtime 获取访问、变更和修改时间。这些也是封装了底层复杂性(如 lazytime 更新策略)的辅助函数。
  4. 块信息: 填充 blksize(文件系统块大小)和 blocks(文件占用的块数),这些信息对磁盘空间分析工具很有用。
  5. 变更凭证 (Change Cookie): 如果请求掩码包含 STATX_CHANGE_COOKIE 并且 inode 支持 i_version,函数会填充 change_cookie 字段。i_version 是一个每次文件内容或元数据变更时都会递增的计数器,它比时间戳更可靠地用于检测文件变化,常用于NFS和Samba等网络文件系统客户端的缓存一致性判断。

代码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
// generic_fillattr: 从inode结构体中填充基本的文件属性。
// @idmap: 挂载点的ID映射,用于UID/GID转换。
// @request_mask: statx请求掩码,指示需要填充哪些字段。
// @inode: 作为属性来源的inode结构体。
// @stat: 用于填充属性的目标kstat结构体。
void generic_fillattr(struct mnt_idmap *idmap, u32 request_mask,
struct inode *inode, struct kstat *stat)
{
// 根据idmap将inode的UID和GID转换为VFS层面的UID/GID。
vfsuid_t vfsuid = i_uid_into_vfsuid(idmap, inode);
vfsgid_t vfsgid = i_gid_into_vfsgid(idmap, inode);

// 拷贝设备号和inode号。
stat->dev = inode->i_sb->s_dev;
stat->ino = inode->i_ino;
// 拷贝文件模式(类型和权限)。
stat->mode = inode->i_mode;
// 拷贝硬链接数。
stat->nlink = inode->i_nlink;
// 将VFS层面的UID/GID转换为内核的kuid/kgid类型并拷贝。
stat->uid = vfsuid_into_kuid(vfsuid);
stat->gid = vfsgid_into_kgid(vfsgid);
// 拷贝特殊设备文件的设备号。
stat->rdev = inode->i_rdev;
// 使用辅助函数安全地读取文件大小。
stat->size = i_size_read(inode);
// 使用辅助函数获取文件最后访问时间。
stat->atime = inode_get_atime(inode);

// 检查inode是否使用挂载点粒度的时间戳,这是一种优化。
if (is_mgtime(inode)) {
// 如果是,则使用专门的函数填充变更和修改时间。
fill_mg_cmtime(stat, request_mask, inode);
} else {
// 否则,使用标准的辅助函数获取变更和修改时间。
stat->ctime = inode_get_ctime(inode);
stat->mtime = inode_get_mtime(inode);
}

// 获取文件系统的块大小。
stat->blksize = i_blocksize(inode);
// 获取文件占用的磁盘块数。
stat->blocks = inode->i_blocks;

// 如果请求了STATX_CHANGE_COOKIE并且inode支持版本号。
if ((request_mask & STATX_CHANGE_COOKIE) && IS_I_VERSION(inode)) {
// 在结果掩码中标记change_cookie已填充。
stat->result_mask |= STATX_CHANGE_COOKIE;
// 查询并填充inode的版本号作为变更凭证。
stat->change_cookie = inode_query_iversion(inode);
}

}
// 导出符号,使得其他内核模块(主要是文件系统驱动)可以调用此函数。
EXPORT_SYMBOL(generic_fillattr);