utimensat 和 futimens 系统调用及示例

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

1. 函数介绍

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

  1. 访问时间 (atime): 文件上一次被读取的时间。
  2. 修改时间 (mtime): 文件内容上一次被修改的时间。
  3. 状态改变时间 (ctime): 文件的元数据(如权限、所有者、链接数等)上一次被改变的时间。

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

utimensat 和 futimens 的核心优势

  1. 纳秒级精度:它们使用 struct timespec,可以精确到纳秒(虽然底层文件系统可能不支持这么高的精度,但接口提供了)。
  2. 更灵活的控制:它们引入了特殊的标记,允许你精确控制对每个时间戳的操作:
    • 设置为当前时间
    • 保持不变
    • 设置为指定时间
  3. 更灵活的路径解析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: (对于 futimensfd 不是有效的文件描述符。
  • EBADF: (对于 utimensatdirfd 不是有效的文件描述符,且不等于 AT_FDCWD
  • EFAULTpathname 或 times 指向了调用进程无法访问的内存地址。
  • EINVALtimes 数组中的时间值无效(例如,纳秒数超出范围或 flags 无效)。
  • EIO: I/O 错误。
  • ELOOP: 解析 pathname 时遇到符号链接循环。
  • ENAMETOOLONGpathname 太长。
  • ENOENTpathname 指定的文件或目录不存在。
  • ENOMEM: 内核内存不足。
  • ENOTDIR: (对于 utimensatpathname 的某个前缀不是目录,或者 dirfd 是一个文件描述符但不是目录。
  • EPERMtimes 中指定的时间早于文件的 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。它们是当前的标准,功能强大且设计良好。
此条目发表在linux文章分类目录。将固定链接加入收藏夹。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注