utimensat和futimens 系统调用及示例

好的,我们来深入学习 utimensat 和 futimens 系统调用

1. 函数介绍

在 Linux 系统中,每个文件都关联着一些重要的时间属性:

data-ad-format="fluid" data-ad-layout-key="-7k+ex-4a-9w+4a">

访问时间 (atime): 文件上一次被读取的时间。

修改时间 (mtime): 文件内容上一次被修改的时间。

状态改变时间 (ctime): 文件的元数据(如权限、所有者、链接数等)上一次被改变的时间。

我们之前学过 utimes 可以用来修改 atime 和 mtime。utimensat 和 futimens 是更现代、更强大的系统调用,用于完成相同的任务。

utimensat 和 futimens 的核心优势:

纳秒级精度:它们使用 struct timespec,可以精确到纳秒(虽然底层文件系统可能不支持这么高的精度,但接口提供了)。

更灵活的控制:它们引入了特殊的标记,允许你精确控制对每个时间戳的操作:

  • 设置为当前时间。

  • 保持不变。

  • 设置为指定时间。

更灵活的路径解析:utimensat 可以像 openat 一样,相对于一个目录文件描述符解析路径,并且可以选择是否跟随符号链接。

简单来说,utimensat 和 futimens 是 utimes 的“升级版”,提供了更高的精度和更灵活的操作方式。

2. 函数原型

1
2
3
4
5
6
7
8
9
#include <fcntl.h>      // 包含 AT_FDCWD 等常量
#include <sys/stat.h> // 包含 utimensat, futimens 函数声明和 timespec 结构体

// 通过路径名设置时间戳 (更灵活)
int utimensat(int dirfd, const char *pathname, const struct timespec times&#91;2], int flags);

// 通过文件描述符设置时间戳
int futimens(int fd, const struct timespec times&#91;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 结构体:

1
2
3
4
5
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 来修改文件时间戳,并展示它们的灵活性。

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
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
#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&#91;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&#91;0].tv_sec = fixed_time_sec; // atime
new_times&#91;0].tv_nsec = 123456789; // atime 纳秒
new_times&#91;1].tv_sec = fixed_time_sec; // mtime
new_times&#91;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&#91;0].tv_sec = 0; // 无关紧要
new_times&#91;0].tv_nsec = UTIME_NOW; // 设置为当前时间
new_times&#91;1].tv_sec = 0; // 无关紧要
new_times&#91;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&#91;0].tv_sec = fixed_time_sec + 3600; // atime + 1 小时
new_times&#91;0].tv_nsec = 111111111;
new_times&#91;1].tv_sec = fixed_time_sec + 7200; // mtime + 2 小时
new_times&#91;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&#91;0].tv_sec = fixed_time_sec + 10800; // atime + 3 小时
new_times&#91;0].tv_nsec = 333333333;
new_times&#91;1].tv_sec = fixed_time_sec + 14400; // mtime + 4 小时
new_times&#91;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&#91;2]): Sets atime/mtime for a file via its file descriptor.\n");
printf("2. utimensat(dirfd, pathname, times&#91;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. 编译和运行

1
2
3
4
5
6
# 假设代码保存在 utimensat_futimens_example.c 中
gcc -o utimensat_futimens_example utimensat_futimens_example.c

# 运行程序
./utimensat_futimens_example

10. 预期输出 (片段)

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
--- 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&#91;2]): Sets atime/mtime for a file via its file descriptor.
2. utimensat(dirfd, pathname, times&#91;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。它们是当前的标准,功能强大且设计良好。

data-ad-format="auto" data-full-width-responsive="true">