SerenityOS 中的 futimens 与 utimensat:文件时间戳更新机制全解析
【免费下载链接】serenityThe Serenity Operating System 🐞项目地址: https://gitcode.com/GitHub_Trending/se/serenity
导读
本文基于 SerenityOS 官方手册页 utimensat(3),深入讲解futimens()与utimensat()两个系统级接口如何精确更新文件的访问时间(atime)与修改时间(mtime)。你将从用户态 API 的语义、特殊标记值UTIME_NOW/UTIME_OMIT的用法、错误码含义,一路追踪到 LibC 封装与内核系统调用的完整调用链,最终掌握用 C++ 在 SerenityOS 上"只改访问时间、不动修改时间"等精细时间戳操作能力,并理解touch、lutimes等既有工具/API 如何复用这一机制。
概述:一对用于时间戳更新的接口
futimens()和utimensat()用于将文件的访问时间与修改时间设置为times数组指定的值。二者在 SerenityOS 中声明于 Userland/Libraries/LibC/sys/stat.h,原型如下:
#include <sys/stat.h> int futimens(int fd, struct timespec const times[2]); #include <fcntl.h> int utimensat(int dirfd, char const* path, struct timespec const times[2], int flag);两者的关键区别在于目标文件的指定方式:
futimens():通过文件描述符fd指定目标文件,更新该描述符所关联文件的时间戳。utimensat():通过路径指定目标文件,且拥有两种工作模式(见下文)。
times是一个长度为 2 的timespec数组,times[0]对应访问时间(atime),times[1]对应修改时间(mtime),每个元素由秒(tv_sec)与纳秒(tv_nsec)两个字段组成。
utimensat 的两种工作模式
utimensat()依据path与dirfd的组合表现出两种截然不同的行为,这也是该接口设计上最值得注意的部分:
模式一:目录描述符 + 相对路径(标准 POSIX 行为)
给定一个指向目录的有效文件描述符dirfd以及一个非空路径path时,utimensat()更新相对于该目录描述符所定位的文件的时间戳。若dirfd被设为AT_FDCWD,则相对路径基于当前工作目录解析。
模式二:普通文件描述符 + 空路径(SerenityOS 扩展行为)
给定一个指向普通文件的有效文件描述符fd以及空路径path时,utimensat()更新该描述符所关联文件的时间戳。这一行为并非标准 POSIX 语义,其存在价值在于:它允许futimens()完全基于utimensat()实现,从而复用同一套参数校验与系统调用路径,减少重复逻辑。
这一点可以在 SerenityOS 的 LibC 实现中得到印证:Userland/Libraries/LibC/stat.cpp 中futimens()与utimensat()最终都汇入同一个内部函数__utimens():
int futimens(int fd, struct timespec const times[2]) { return __utimens(fd, nullptr, times, 0); } int utimensat(int dirfd, char const* path, struct timespec const times[2], int flag) { if (!path) { errno = EFAULT; return -1; } return __utimens(dirfd, path, times, flag); }futimens()将path传为nullptr,而utimensat()先对空路径做EFAULT检查——这正好对应了手册中EFAULT错误码的含义。随后__utimens()依据path是否为空来决定调用内核的SC_utimensat还是SC_futimens系统调用(Userland/Libraries/LibC/stat.cpp)。
特殊标记值:UTIME_NOW 与 UTIME_OMIT
times数组中tv_nsec字段支持两个特殊常量,用于表达"取当前时间"与"保持不变"两种语义:
tv_nsec取值 | 语义 | tv_sec字段 |
|---|---|---|
UTIME_NOW | 对应时间戳设置为当前时间 | 被忽略 |
UTIME_OMIT | 对应时间戳保持不变 | 被忽略 |
这两个常量的实际取值定义在 Kernel/API/POSIX/sys/stat.h:
#define UTIME_OMIT -1 #define UTIME_NOW -2使用负值作为特殊标记,可以保证与任何合法的纳秒数(0 ~ 999,999,999)都不会冲突。当times为**空指针(nullptr)**时,两个时间戳都会被设置为当前时间,等价于将数组中两个timespec的tv_nsec都设为UTIME_NOW。
LibC 层在 Userland/Libraries/LibC/stat.cpp 中提前做了三件优化与校验工作:
- 双 OMIT 短路:若两个
tv_nsec均为UTIME_OMIT,则任何修改都不需要发生,直接返回 0,避免一次无意义的系统调用。 - 双 NOW 归一化:若两个
tv_nsec均为UTIME_NOW,则将times归一化为nullptr,注释明确指出"避免内核中复制它的需要"。 - 纳秒范围校验:对非特殊值逐一检查
tv_nsec是否落在[0, 1'000'000'000)区间内,越界即返回EINVAL——这正是手册中EINVAL错误码的底层实现。
flag 参数与符号链接处理
utimensat()的flag参数可取值0或AT_SYMLINK_NOFOLLOW:
flag = 0:默认行为,若路径是符号链接则跟随链接,更新链接指向的目标文件。flag = AT_SYMLINK_NOFOLLOW:不跟随符号链接,直接更新符号链接自身的时间戳。
而futimens()由于操作对象是已打开的文件描述符,总是作用于描述符所关联的文件(即始终"跟随"符号链接语义)。常量定义于 Kernel/API/POSIX/fcntl.h:
#define AT_FDCWD -100 #define AT_SYMLINK_NOFOLLOW 0x100AT_FDCWD用负值(-100)表示"当前工作目录",用于将相对路径的基准从某个目录描述符切换为进程的当前工作目录。
LibC 层对flag的校验同样严格:__utimens()中仅允许AT_SYMLINK_NOFOLLOW位被置位,其余任何位组合都会返回EINVAL(Userland/Libraries/LibC/stat.cpp):
// POSIX allows AT_SYMLINK_NOFOLLOW flag or no flags. if (flag & ~AT_SYMLINK_NOFOLLOW) { errno = EINVAL; return -1; }在内核端,sys$utimensat会把AT_SYMLINK_NOFOLLOW映射为O_NOFOLLOW_NOERROR语义再交给 VFS 处理(Kernel/Syscalls/utimensat.cpp),而sys$futimens则直接通过描述符对应的 custody 定位文件(Kernel/Syscalls/utimensat.cpp)。
返回值
两个函数在成功时返回0,失败时返回-1。失败时还会设置errno以指示具体错误,并且保证文件的访问时间与修改时间保持原样、不被部分修改——即要么完整生效,要么完全不生效。
错误码详解
| 错误码 | 触发条件 |
|---|---|
EFAULT | utimensat()的path为空指针 |
EINVAL | path长度过长 |
EINVAL | flag不是0或AT_SYMLINK_NOFOLLOW |
EINVAL | 文件系统不支持该时间戳 |
EINVAL | times的tv_nsec字段小于 0 或大于等于 10 亿(1,000,000,000),且不是UTIME_NOW/UTIME_OMIT |
EACCES | 当前用户对文件没有写权限 |
EROFS | 包含该文件的文件系统为只读 |
ENOTDIR | path不是绝对路径,且dirfd不是与目录关联的文件描述符 |
其中"path长度过长"在 Userland/Libraries/LibC/stat.cpp 中的判定阈值为INT32_MAX,一旦超过即返回EINVAL。
完整示例:仅更新访问时间
手册中给出了一个极具代表性的示例——只把当前目录下README.md的访问时间更新为当前时间,同时保持修改时间不变:
#include <fcntl.h> #include <sys/stat.h> int main() { timespec times[2]; auto& atime = times[0]; auto& mtime = times[1]; atime.tv_sec = 0; atime.tv_nsec = UTIME_NOW; mtime.tv_sec = 0; mtime.tv_nsec = UTIME_OMIT; // Update only last access time of file "README.md" in current working // directory to current time. Leave last modification time unchanged. if (utimensat(AT_FDCWD, "README.md", times, 0) == -1) { return 1; } return 0; }示例要点拆解:
times[0](atime)设置tv_nsec = UTIME_NOW,其tv_sec = 0会被忽略;times[1](mtime)设置tv_nsec = UTIME_OMIT,其tv_sec = 0同样被忽略,修改时间保持原值;dirfd = AT_FDCWD让相对路径"README.md"基于当前工作目录解析;flag = 0表示遵循默认的符号链接跟随语义。
从用户态到内核的完整调用链
将手册、LibC 与内核代码串起来,一次utimensat()调用的完整旅程如下:
- 用户程序调用
utimensat(dirfd, path, times, flag); - LibC 层
utimensat()(Userland/Libraries/LibC/stat.cpp)检查path非空后进入__utimens(); __utimens()依次完成:路径长度检查、flag 位校验、双 OMIT 短路、双 NOW 归一化、纳秒范围校验,然后组装SC_utimensat_params发起SC_utimensat系统调用(Userland/Libraries/LibC/stat.cpp);- 内核
sys$utimensat(Kernel/Syscalls/utimensat.cpp)首先通过require_promise(Pledge::fattr)执行 pledge 权限检查,用kgettimeofday()解析UTIME_NOW,将AT_SYMLINK_NOFOLLOW翻译为O_NOFOLLOW_NOERROR,然后基于dirfd+path构造CustodyBase; - VFS 层调用
VirtualFileSystem::utimensat()解析出目标 custody 后进入VirtualFileSystem::do_utimens(),由文件系统驱动最终完成 inode 时间戳的写入(Kernel/FileSystem/VirtualFileSystem.cpp)。
sys$futimens的路径类似,但由于目标由文件描述符直接指定,省去了路径解析,直接通过open_file_description()获取 custody 后调用do_utimens()(Kernel/Syscalls/utimensat.cpp)。
从实现细节还可以看到,UTIME_NOW的解析发生内核侧:sys$futimens/sys$utimensat在复制用户态times后,用kgettimeofday().to_timespec()统一替换UTIME_NOW,而UTIME_OMIT则交由 VFS 层的do_utimens()判断是否跳过对应字段的写入。
生态联动:touch、lutimes 与 futimes
这两个接口并非孤立存在,SerenityOS 中多处既有设施直接构建于其上:
touch命令:手册的 See also 节将 touch(1) 列为关联工具。touch通过-a、-m选项分别只改访问/修改时间,通过-t、-d、-r指定具体时间或参考文件,其时间戳写入语义正是由utimensat()/futimens()提供的。lutimes():在 Userland/Libraries/LibC/time.cpp 中,lutimes()将timeval转换为timespec后,以AT_SYMLINK_NOFOLLOW标志调用utimensat(AT_FDCWD, pathname, ts, AT_SYMLINK_NOFOLLOW),从而实现对符号链接自身时间戳的更新。futimes():同样在 Userland/Libraries/LibC/time.cpp 中,futimes()以nullptr路径调用utimensat(fd, nullptr, nullptr, 0)或直接委托给futimens(),体现了"以 utimensat 实现 futimens"这一设计思想在相邻 API 上的延伸复用。
这些上游 API 的存在说明:理解utimensat()/futimens()的语义,是掌握 SerenityOS 全套时间戳相关操作(utime、utimes、lutimes、futimes、touch)的共同基础。
小结
futimens()与utimensat()是 SerenityOS 中设置文件访问/修改时间的核心接口:前者面向文件描述符,后者面向路径并支持AT_FDCWD相对路径、AT_SYMLINK_NOFOLLOW不跟随链接两种扩展;UTIME_NOW/UTIME_OMIT提供了"取当前时间/保持不变"的精细控制。从 LibC 的参数校验与归一化,到内核系统调用的 pledge 检查与 VFS 落盘,再到touch、lutimes等上层设施,一条完整、可验证的调用链清晰展示了这一 POSIX 接口在 SerenityOS 中的落地方式,也为自行编写时间戳管理工具提供了可直接复用的样板。
【免费下载链接】serenityThe Serenity Operating System 🐞项目地址: https://gitcode.com/GitHub_Trending/se/serenity
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考