首页
/ SerenityOS 中的 futimens 与 utimensat:文件时间戳更新机制全解析

SerenityOS 中的 futimens 与 utimensat:文件时间戳更新机制全解析

2026-09-09 19:10:44作者:昌雅子Ethen

导读

本文基于 SerenityOS 官方手册页 utimensat(3),深入讲解 futimens()utimensat() 两个系统级接口如何精确更新文件的访问时间(atime)与修改时间(mtime)。你将从用户态 API 的语义、特殊标记值 UTIME_NOW/UTIME_OMIT 的用法、错误码含义,一路追踪到 LibC 封装与内核系统调用的完整调用链,最终掌握用 C++ 在 SerenityOS 上"只改访问时间、不动修改时间"等精细时间戳操作能力,并理解 touchlutimes 等既有工具/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() 依据 pathdirfd 的组合表现出两种截然不同的行为,这也是该接口设计上最值得注意的部分:

模式一:目录描述符 + 相对路径(标准 POSIX 行为)

给定一个指向目录的有效文件描述符 dirfd 以及一个非空路径 path 时,utimensat() 更新相对于该目录描述符所定位的文件的时间戳。若 dirfd 被设为 AT_FDCWD,则相对路径基于当前工作目录解析。

模式二:普通文件描述符 + 空路径(SerenityOS 扩展行为)

给定一个指向普通文件的有效文件描述符 fd 以及空路径 path 时,utimensat() 更新该描述符所关联文件的时间戳。这一行为并非标准 POSIX 语义,其存在价值在于:它允许 futimens() 完全基于 utimensat() 实现,从而复用同一套参数校验与系统调用路径,减少重复逻辑。

这一点可以在 SerenityOS 的 LibC 实现中得到印证:Userland/Libraries/LibC/stat.cppfutimens()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)**时,两个时间戳都会被设置为当前时间,等价于将数组中两个 timespectv_nsec 都设为 UTIME_NOW

LibC 层在 Userland/Libraries/LibC/stat.cpp 中提前做了三件优化与校验工作:

  1. 双 OMIT 短路:若两个 tv_nsec 均为 UTIME_OMIT,则任何修改都不需要发生,直接返回 0,避免一次无意义的系统调用。
  2. 双 NOW 归一化:若两个 tv_nsec 均为 UTIME_NOW,则将 times 归一化为 nullptr,注释明确指出"避免内核中复制它的需要"。
  3. 纳秒范围校验:对非特殊值逐一检查 tv_nsec 是否落在 [0, 1'000'000'000) 区间内,越界即返回 EINVAL——这正是手册中 EINVAL 错误码的底层实现。

flag 参数与符号链接处理

utimensat()flag 参数可取值 0AT_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 位被置位,其余任何位组合都会返回 EINVALUserland/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 不是 0AT_SYMLINK_NOFOLLOW
EINVAL 文件系统不支持该时间戳
EINVAL timestv_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() 调用的完整旅程如下:

  1. 用户程序调用 utimensat(dirfd, path, times, flag)
  2. LibC 层 utimensat()Userland/Libraries/LibC/stat.cpp)检查 path 非空后进入 __utimens()
  3. __utimens() 依次完成:路径长度检查、flag 位校验、双 OMIT 短路、双 NOW 归一化、纳秒范围校验,然后组装 SC_utimensat_params 发起 SC_utimensat 系统调用(Userland/Libraries/LibC/stat.cpp);
  4. 内核 sys$utimensatKernel/Syscalls/utimensat.cpp)首先通过 require_promise(Pledge::fattr) 执行 pledge 权限检查,用 kgettimeofday() 解析 UTIME_NOW,将 AT_SYMLINK_NOFOLLOW 翻译为 O_NOFOLLOW_NOERROR,然后基于 dirfd + path 构造 CustodyBase
  5. 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 全套时间戳相关操作(utimeutimeslutimesfutimestouch)的共同基础。

小结

futimens()utimensat() 是 SerenityOS 中设置文件访问/修改时间的核心接口:前者面向文件描述符,后者面向路径并支持 AT_FDCWD 相对路径、AT_SYMLINK_NOFOLLOW 不跟随链接两种扩展;UTIME_NOW/UTIME_OMIT 提供了"取当前时间/保持不变"的精细控制。从 LibC 的参数校验与归一化,到内核系统调用的 pledge 检查与 VFS 落盘,再到 touchlutimes 等上层设施,一条完整、可验证的调用链清晰展示了这一 POSIX 接口在 SerenityOS 中的落地方式,也为自行编写时间戳管理工具提供了可直接复用的样板。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
docsdocs
暂无描述
Markdown
899
5.83 K
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.76 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
860
1.35 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
925
1.85 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.84 K
1.02 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
533
601
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.37 K
1.46 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
395
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.04 K
525