SerenityOS 中的 futimens 与 utimensat:文件时间戳更新机制全解析
导读
本文基于 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 0x100
AT_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 中的落地方式,也为自行编写时间戳管理工具提供了可直接复用的样板。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0631
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
video-shotcraftAI宣传片skill,使用 Remotion 制作电影级产品视频:提供106 张镜头配方卡和可复用的视频魔板。适用于 Claude Code 与 Codex以及所有其他智能体Markdown00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python09
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00