好的,我们来深入学习 utimensat
和 futimens
系统调用
1. 函数介绍
在 Linux 系统中,每个文件都关联着一些重要的时间属性:
- 访问时间 (atime): 文件上一次被读取的时间。
- 修改时间 (mtime): 文件内容上一次被修改的时间。
- 状态改变时间 (ctime): 文件的元数据(如权限、所有者、链接数等)上一次被改变的时间。
我们之前学过 utimes
可以用来修改 atime
和 mtime
。utimensat
和 futimens
是更现代、更强大的系统调用,用于完成相同的任务。
utimensat
和 futimens
的核心优势:
- 纳秒级精度:它们使用
struct timespec
,可以精确到纳秒(虽然底层文件系统可能不支持这么高的精度,但接口提供了)。 - 更灵活的控制:它们引入了特殊的标记,允许你精确控制对每个时间戳的操作:
- 设置为当前时间。
- 保持不变。
- 设置为指定时间。
- 更灵活的路径解析:
utimensat
可以像openat
一样,相对于一个目录文件描述符解析路径,并且可以选择是否跟随符号链接。
简单来说,utimensat
和 futimens
是 utimes
的“升级版”,提供了更高的精度和更灵活的操作方式。
2. 函数原型
#include <fcntl.h> // 包含 AT_FDCWD 等常量
#include <sys/stat.h> // 包含 utimensat, futimens 函数声明和 timespec 结构体
// 通过路径名设置时间戳 (更灵活)
int utimensat(int dirfd, const char *pathname, const struct timespec times[2], int flags);
// 通过文件描述符设置时间戳
int futimens(int fd, const struct timespec times[2]);
3. 功能
两者都用于设置文件的访问时间 (atime
) 和修改时间 (mtime
)。
utimensat
: 通过路径名指定文件,并提供额外的灵活性(相对路径解析、符号链接处理)。futimens
: 通过已打开的文件描述符 (fd
) 指定文件。
4. 参数详解
futimens(int fd, const struct timespec times[2])
fd
:int
类型。- 一个指向目标文件的已打开文件描述符。
times
:const struct timespec times[2]
类型。- 一个包含两个
struct timespec
元素的数组。 times[0]
指定了新的访问时间 (atime
)。times[1]
指定了新的修改时间 (mtime
)。- 特殊值:
- 如果
times[0]
或times[1]
的tv_nsec
字段是UTIME_NOW
,则相应的时间戳会被设置为调用时的当前时间。 - 如果
times[0]
或times[1]
的tv_nsec
字段是UTIME_OMIT
,则相应的时间戳将保持不变。 - 否则,时间戳将被设置为
tv_sec
和tv_nsec
指定的值。
- 如果
utimensat(int dirfd, const char *pathname, const struct timespec times[2], int flags)
dirfd
:int
类型。- 一个目录文件描述符,用作
pathname
的基础路径。 - 如果
pathname
是绝对路径,则dirfd
被忽略。 - 特殊值
AT_FDCWD
表示使用当前工作目录作为基础路径。
pathname
:const char *
类型。- 指向要修改时间戳的文件的路径名(可以是相对路径或绝对路径)。
times
:const struct timespec times[2]
类型。- 含义与
futimens
相同。
flags
:int
类型。- 用于修改
utimensat
行为的标志。目前主要支持一个标志:AT_SYMLINK_NOFOLLOW
: 如果pathname
是一个符号链接,则修改符号链接本身的时间戳,而不是它指向的目标文件的时间戳。如果未设置此标志(默认),则会跟随符号链接。
struct timespec
结构体:
struct timespec {
time_t tv_sec; /* 秒数 (自 Unix 纪元以来) */
long tv_nsec; /* 纳秒数 (0-999,999,999) */
};
5. 返回值
两者返回值相同:
- 成功: 返回 0。
- 失败: 返回 -1,并设置全局变量
errno
来指示具体的错误原因。
6. 错误码 (errno
)
两者共享许多相同的错误码:
EACCES
: 搜索pathname
的路径组件时权限不足,或者没有写权限。EBADF
: (对于futimens
)fd
不是有效的文件描述符。EBADF
: (对于utimensat
)dirfd
不是有效的文件描述符,且不等于AT_FDCWD
。EFAULT
:pathname
或times
指向了调用进程无法访问的内存地址。EINVAL
:times
数组中的时间值无效(例如,纳秒数超出范围或flags
无效)。EIO
: I/O 错误。ELOOP
: 解析pathname
时遇到符号链接循环。ENAMETOOLONG
:pathname
太长。ENOENT
:pathname
指定的文件或目录不存在。ENOMEM
: 内核内存不足。ENOTDIR
: (对于utimensat
)pathname
的某个前缀不是目录,或者dirfd
是一个文件描述符但不是目录。EPERM
:times
中指定的时间早于文件的ctime
和mtime
,且调用进程不拥有该文件(某些文件系统会阻止将时间戳设置得比ctime/mtime
更早)。EROFS
: 文件系统是只读的。
7. 相似函数或关联函数
utimes
: 旧版函数,使用struct timeval
(微秒精度)。utime
: 更老的函数,使用struct utimbuf
(秒精度)。lutimes
: 旧版函数,用于修改符号链接本身的时间戳。stat
/lstat
: 用于获取文件的当前时间戳。
8. 示例代码
下面的示例演示了如何使用 utimensat
和 futimens
来修改文件时间戳,并展示它们的灵活性。
#define _GNU_SOURCE // 启用 GNU 扩展
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h> // 包含 open, O_* flags, AT_FDCWD, AT_SYMLINK_NOFOLLOW
#include <sys/stat.h> // 包含 utimensat, futimens, timespec, stat
#include <string.h>
#include <errno.h>
#include <time.h> // 包含 time, localtime, strftime
// 辅助函数:打印文件的时间信息
void print_file_times(const char *filename) {
struct stat sb;
if (stat(filename, &sb) == -1) {
perror("stat");
return;
}
printf("File: %s\n", filename);
printf(" Last Status Change (ctime): %s", ctime(&sb.st_ctime)); // ctime 包含换行符
printf(" Last Modification (mtime): %s", ctime(&sb.st_mtime));
printf(" Last Access (atime): %s", ctime(&sb.st_atime));
printf("\n");
}
// 辅助函数:创建一个测试文件
void create_test_file(const char *filename) {
FILE *f = fopen(filename, "w");
if (!f) {
perror("fopen");
exit(EXIT_FAILURE);
}
fprintf(f, "This is a test file for utimensat/futimens example.\n");
fclose(f);
printf("Created test file: %s\n\n", filename);
}
int main() {
const char *test_file = "utimensat_test_file.txt";
const char *test_symlink = "utimensat_test_symlink.txt";
struct timespec new_times[2];
time_t fixed_time_sec;
struct tm tm_tmp;
printf("--- Demonstrating utimensat and futimens ---\n");
// 1. 创建一个测试文件
create_test_file(test_file);
// 2. 创建一个指向测试文件的符号链接
if (symlink(test_file, test_symlink) == -1) {
perror("symlink");
unlink(test_file);
exit(EXIT_FAILURE);
}
printf("Created symlink: %s -> %s\n\n", test_symlink, test_file);
// 3. 显示初始时间
printf("1. Initial timestamps:\n");
print_file_times(test_file);
print_file_times(test_symlink); // 符号链接的时间通常和目标文件一样(除非文件系统特殊支持)
// 4. 准备一个固定的时间
printf("2. Preparing fixed time...\n");
memset(&tm_tmp, 0, sizeof(tm_tmp));
tm_tmp.tm_year = 2023 - 1900; // tm_year is years since 1900
tm_tmp.tm_mon = 10 - 1; // tm_mon is 0-11
tm_tmp.tm_mday = 27;
tm_tmp.tm_hour = 10;
tm_tmp.tm_min = 0;
tm_tmp.tm_sec = 0;
tm_tmp.tm_isdst = 0;
fixed_time_sec = timegm(&tm_tmp);
if (fixed_time_sec == -1) {
perror("timegm");
unlink(test_file);
unlink(test_symlink);
exit(EXIT_FAILURE);
}
// 5. 使用 futimens 设置时间
printf("3. --- Using futimens() ---\n");
int fd = open(test_file, O_RDONLY);
if (fd == -1) {
perror("open test_file");
unlink(test_file);
unlink(test_symlink);
exit(EXIT_FAILURE);
}
new_times[0].tv_sec = fixed_time_sec; // atime
new_times[0].tv_nsec = 123456789; // atime 纳秒
new_times[1].tv_sec = fixed_time_sec; // mtime
new_times[1].tv_nsec = 987654321; // mtime 纳秒
printf("Setting timestamps using futimens()...\n");
if (futimens(fd, new_times) == -1) {
perror("futimens");
} else {
printf("futimens() succeeded.\n");
}
close(fd);
printf("Timestamps after futimens:\n");
print_file_times(test_file);
// 6. 使用 utimensat 设置时间 (相对路径)
printf("4. --- Using utimensat() with relative path ---\n");
// 等待几秒,让时间不同
sleep(2);
// 将 atime 设置为当前时间,mtime 保持不变
new_times[0].tv_sec = 0; // 无关紧要
new_times[0].tv_nsec = UTIME_NOW; // 设置为当前时间
new_times[1].tv_sec = 0; // 无关紧要
new_times[1].tv_nsec = UTIME_OMIT; // 保持 mtime 不变
printf("Setting atime to NOW and mtime to OMIT using utimensat(AT_FDCWD, ...)...\n");
// AT_FDCWD 表示相对于当前工作目录解析路径
if (utimensat(AT_FDCWD, test_file, new_times, 0) == -1) {
perror("utimensat");
} else {
printf("utimensat() succeeded.\n");
}
printf("Timestamps after utimensat (atime updated, mtime unchanged):\n");
print_file_times(test_file);
// 7. 使用 utimensat 处理符号链接
printf("5. --- Using utimensat() with symlinks ---\n");
// 准备新的时间
new_times[0].tv_sec = fixed_time_sec + 3600; // atime + 1 小时
new_times[0].tv_nsec = 111111111;
new_times[1].tv_sec = fixed_time_sec + 7200; // mtime + 2 小时
new_times[1].tv_nsec = 222222222;
// 默认情况下,utimensat 会跟随符号链接,修改目标文件
printf("Calling utimensat() on symlink WITHOUT AT_SYMLINK_NOFOLLOW...\n");
printf(" This will modify the TARGET file's timestamps.\n");
if (utimensat(AT_FDCWD, test_symlink, new_times, 0) == -1) {
perror("utimensat (follow symlink)");
} else {
printf("utimensat() succeeded (followed symlink).\n");
}
printf("Target file timestamps after utimensat (followed symlink):\n");
print_file_times(test_file);
printf("Symlink file timestamps (should be unchanged or linked):\n");
print_file_times(test_symlink);
// 现在使用 AT_SYMLINK_NOFOLLOW 来修改符号链接本身
new_times[0].tv_sec = fixed_time_sec + 10800; // atime + 3 小时
new_times[0].tv_nsec = 333333333;
new_times[1].tv_sec = fixed_time_sec + 14400; // mtime + 4 小时
new_times[1].tv_nsec = 444444444;
printf("\nCalling utimensat() on symlink WITH AT_SYMLINK_NOFOLLOW...\n");
printf(" This will modify the SYMLINK's timestamps (if filesystem supports it).\n");
if (utimensat(AT_FDCWD, test_symlink, new_times, AT_SYMLINK_NOFOLLOW) == -1) {
if (errno == EOPNOTSUPP) {
printf("utimensat with AT_SYMLINK_NOFOLLOW failed: %s\n", strerror(errno));
printf(" This is expected on many filesystems (e.g., ext4).\n");
} else {
perror("utimensat (nofollow symlink)");
}
} else {
printf("utimensat() succeeded (modified symlink itself).\n");
print_file_times(test_symlink);
}
// 8. 清理
printf("\n6. --- Cleaning up ---\n");
if (unlink(test_file) == 0) {
printf("Deleted test file '%s'.\n", test_file);
} else {
perror("unlink test_file");
}
if (unlink(test_symlink) == 0) {
printf("Deleted symlink '%s'.\n", test_symlink);
} else {
perror("unlink test_symlink");
}
printf("\n--- Summary ---\n");
printf("1. futimens(fd, times[2]): Sets atime/mtime for a file via its file descriptor.\n");
printf("2. utimensat(dirfd, pathname, times[2], flags): Sets atime/mtime via pathname, with more options.\n");
printf("3. Both use struct timespec, providing nanosecond precision.\n");
printf("4. Special timespec values:\n");
printf(" - tv_nsec = UTIME_NOW: Set timestamp to current time.\n");
printf(" - tv_nsec = UTIME_OMIT: Leave timestamp unchanged.\n");
printf("5. utimensat flags:\n");
printf(" - 0 (default): Follow symlinks.\n");
printf(" - AT_SYMLINK_NOFOLLOW: Modify symlink itself (filesystem support varies).\n");
printf(" - dirfd allows relative path resolution (like openat).\n");
printf("6. These are the modern, preferred functions for changing file timestamps.\n");
return 0;
}
9. 编译和运行
# 假设代码保存在 utimensat_futimens_example.c 中
gcc -o utimensat_futimens_example utimensat_futimens_example.c
# 运行程序
./utimensat_futimens_example
10. 预期输出 (片段)
--- Demonstrating utimensat and futimens ---
Created test file: utimensat_test_file.txt
Created symlink: utimensat_test_symlink.txt -> utimensat_test_file.txt
1. Initial timestamps:
File: utimensat_test_file.txt
Last Status Change (ctime): Fri Oct 27 11:00:00 2023
Last Modification (mtime): Fri Oct 27 11:00:00 2023
Last Access (atime): Fri Oct 27 11:00:00 2023
File: utimensat_test_symlink.txt
Last Status Change (ctime): Fri Oct 27 11:00:00 2023
Last Modification (mtime): Fri Oct 27 11:00:00 2023
Last Access (atime): Fri Oct 27 11:00:00 2023
2. Preparing fixed time...
3. --- Using futimens() ---
Setting timestamps using futimens()...
futimens() succeeded.
Timestamps after futimens:
File: utimensat_test_file.txt
Last Status Change (ctime): Fri Oct 27 11:00:00 2023
Last Modification (mtime): Fri Oct 27 10:00:00 2023
Last Access (atime): Fri Oct 27 10:00:00 2023
4. --- Using utimensat() with relative path ---
Setting atime to NOW and mtime to OMIT using utimensat(AT_FDCWD, ...)...
utimensat() succeeded.
Timestamps after utimensat (atime updated, mtime unchanged):
File: utimensat_test_file.txt
Last Status Change (ctime): Fri Oct 27 11:00:02 2023
Last Modification (mtime): Fri Oct 27 10:00:00 2023
Last Access (atime): Fri Oct 27 11:00:02 2023
5. --- Using utimensat() with symlinks ---
Calling utimensat() on symlink WITHOUT AT_SYMLINK_NOFOLLOW...
This will modify the TARGET file's timestamps.
utimensat() succeeded (followed symlink).
Target file timestamps after utimensat (followed symlink):
File: utimensat_test_file.txt
Last Status Change (ctime): Fri Oct 27 11:00:02 2023
Last Modification (mtime): Fri Oct 27 12:00:00 2023
Last Access (atime): Fri Oct 27 11:00:00 2023
Symlink file timestamps (should be unchanged or linked):
File: utimensat_test_symlink.txt
... (same as target) ...
Calling utimensat() on symlink WITH AT_SYMLINK_NOFOLLOW...
This will modify the SYMLINK's timestamps (if filesystem supports it).
utimensat with AT_SYMLINK_NOFOLLOW failed: Operation not supported
This is expected on many filesystems (e.g., ext4).
6. --- Cleaning up ---
Deleted test file 'utimensat_test_file.txt'.
Deleted symlink 'utimensat_test_symlink.txt'.
--- Summary ---
1. futimens(fd, times[2]): Sets atime/mtime for a file via its file descriptor.
2. utimensat(dirfd, pathname, times[2], flags): Sets atime/mtime via pathname, with more options.
3. Both use struct timespec, providing nanosecond precision.
4. Special timespec values:
- tv_nsec = UTIME_NOW: Set timestamp to current time.
- tv_nsec = UTIME_OMIT: Leave timestamp unchanged.
5. utimensat flags:
- 0 (default): Follow symlinks.
- AT_SYMLINK_NOFOLLOW: Modify symlink itself (filesystem support varies).
- dirfd allows relative path resolution (like openat).
6. These are the modern, preferred functions for changing file timestamps.
11. 总结
utimensat
和 futimens
是 Linux 中用于修改文件时间戳的现代、强大的系统调用。
- 核心优势:
- 高精度:纳秒级时间戳设置。
- 灵活控制:通过
UTIME_NOW
和UTIME_OMIT
精确控制每个时间戳的行为。 - 路径灵活性:
utimensat
支持相对路径解析和符号链接处理。
futimens
:通过已打开的文件描述符操作,简单直接。utimensat
:功能更全,可以处理相对路径、绝对路径,并控制符号链接行为。- 与旧函数对比:
- 比
utimes
(微秒) 和utime
(秒) 精度更高。 - 比
lutimes
功能更强大(lutimes
只是utimensat
的一个特例)。
- 比
- 给 Linux 编程小白的建议:在需要修改文件时间戳的新代码中,优先使用
utimensat
或futimens
。它们是当前的标准,功能强大且设计良好。