好的,我们来学习一下 renameat 这个函数。它是一个用于重命名或移动文件和目录的系统调用,是 rename 函数的增强版,提供了更灵活的相对路径处理能力。

1. 函数介绍 Link to heading

renameat 是一个 Linux 系统调用(System Call),它的主要作用是重命名文件或目录,或者将文件或目录移动到同一个文件系统内的另一个位置 。它与标准的 rename 函数功能相似,但关键区别在于它允许你指定相对路径是相对于哪个目录文件描述符来解析的 。这使得在处理复杂目录结构或受限环境(如沙箱)时更加方便和安全。

简单来说,如果你想把一个文件从 “old_name” 改成 “new_name”,或者把它从一个目录移动到另一个目录,就可以使用 renameat。它特别适用于需要基于特定目录上下文进行操作的场景。

2. 函数原型 Link to heading

#include <stdio.h> // 或者 <fcntl.h> 在某些系统上

int renameat(int olddirfd, const char *oldpath,
             int newdirfd, const char *newpath);

3. 功能 Link to heading

这个函数的功能是将由 oldpath 指定的文件或目录重命名为由 newpath 指定的名称,或者将其移动到 newpath 指定的位置 。如果 newpath 已经存在,并且它不是 oldpath 本身,那么 newpath 会被原子地替换(如果类型允许,例如用新文件替换旧文件)。oldpathnewpath 都是相对于指定的目录文件描述符来解析的。

4. 参数 Link to heading

  • olddirfd:
    • int 类型。
    • 这是一个文件描述符,指向包含 oldpath 的目录。如果 oldpath 是绝对路径(以 / 开头),这个参数会被忽略。特殊的值 AT_FDCWD 可以用来表示当前工作目录,此时行为类似于 rename 函数 。
  • oldpath:
    • const char * 类型。
    • 指向一个以 null 结尾的字符串,表示要被重命名或移动的文件或目录的路径名。这个路径是相对于 olddirfd 解析的(除非它是绝对路径)。
  • newdirfd:
    • int 类型。
    • 这是一个文件描述符,指向包含 newpath 的目录。如果 newpath 是绝对路径(以 / 开头),这个参数会被忽略。特殊的值 AT_FDCWD 可以用来表示当前工作目录 。
  • newpath:
    • const char * 类型。
    • 指向一个以 null 结尾的字符串,表示新的文件或目录名,或者是目标位置的路径名。这个路径是相对于 newdirfd 解析的(除非它是绝对路径)。

5. 返回值 Link to heading

  • 成功: 返回 0。
  • 失败: 返回 -1,并设置全局变量 errno 来指示具体的错误类型 。
    • EACCES: 搜索路径上的目录不允许搜索权限,或者 oldpathnewpath 的父目录不允许写权限,或者需要覆盖但不允许写权限。
    • EBUSY: 要重命名的文件正被使用(例如是当前工作目录或根目录),或者文件系统不支持重命名操作。
    • EDQUOT: 用户磁盘配额不足。
    • EEXISTENOTEMPTY: newpath 是一个非空目录,不能被 oldpath 替换。
    • EFAULT: oldpathnewpath 指向无效的地址空间。
    • EINVAL: oldpathnewpath 的参数无效(例如,oldpathnewpath 是目录,但新路径是该目录的子目录)。
    • EISDIR: newpath 是一个目录,但 oldpath 是一个非目录。
    • ELOOP: 解析 oldpathnewpath 时遇到符号链接循环。
    • EMLINK: oldpathnewpath 的父目录链接数超出限制。
    • ENAMETOOLONG: oldpathnewpath 太长。
    • ENOENT: oldpath 不存在,或者 olddirfdnewdirfd 无效且不是 AT_FDCWD
    • ENOMEM: 内核内存不足。
    • ENOSPC: 设备空间不足。
    • ENOTDIR: olddirfdnewdirfd 不是目录文件描述符,或者 oldpathnewpath 的某个前缀不是目录。
    • ENOTEMPTY: newpath 是一个非空目录。
    • EPERM: 父目录的挂载点不支持重命名,或者 oldpathnewpath 是目录且调用者没有权限,或者文件系统不允许该操作。
    • EROFS: 文件系统是只读的。
    • EXDEV: olddirfdnewdirfd 指向不同的文件系统挂载点(renameat 不能跨文件系统移动文件)。

6. 相似函数或关联函数 Link to heading

  • rename: 标准的重命名函数,路径是相对于当前工作目录解析的。rename(oldpath, newpath) 等价于 renameat(AT_FDCWD, oldpath, AT_FDCWD, newpath)
  • renameat2: renameat 的扩展版本,提供了额外的标志位(如 RENAME_EXCHANGE, RENAME_NOREPLACE),需要 Linux 3.15+ 内核支持。
  • link / linkat: 创建硬链接。linkatlink 的增强版,类似 renameatrename 的关系。
  • unlink: 删除文件的链接(如果这是最后一个链接,则删除文件)。
  • mkdir / rmdir: 创建和删除目录。

7. 示例代码 Link to heading

这个示例将演示几种使用 renameat 的场景:

#define _GNU_SOURCE // 启用 GNU 扩展,以使用 AT_FDCWD 等
#include <stdio.h>
#include <fcntl.h>    // open, O_RDONLY, O_DIRECTORY
#include <unistd.h>   // close
#include <sys/stat.h> // mkdir
#include <string.h>
#include <errno.h>

int main() {
    const char *src_dir = "source_dir";
    const char *dst_dir = "dest_dir";
    const char *file_name = "test_file.txt";
    const char *renamed_file_name = "moved_and_renamed_file.txt";
    const char *dir_name = "test_subdir";
    const char *renamed_dir_name = "moved_subdir";
    int src_dir_fd, dst_dir_fd;
    int result;

    // 1. 创建源目录和目标目录
    if (mkdir(src_dir, 0755) == -1 && errno != EEXIST) {
        perror("mkdir source_dir");
        return 1;
    }
    if (mkdir(dst_dir, 0755) == -1 && errno != EEXIST) {
        perror("mkdir dest_dir");
        return 1;
    }

    // 2. 在源目录中创建一个测试文件
    char file_path[256];
    snprintf(file_path, sizeof(file_path), "%s/%s", src_dir, file_name);
    FILE *file = fopen(file_path, "w");
    if (!file) {
        perror("fopen test_file.txt");
        return 1;
    }
    fprintf(file, "This is a test file for renameat.\n");
    fclose(file);
    printf("Created file: %s\n", file_path);

    // 3. 在源目录中创建一个子目录
    char dir_path[256];
    snprintf(dir_path, sizeof(dir_path), "%s/%s", src_dir, dir_name);
    if (mkdir(dir_path, 0755) == -1) {
        perror("mkdir test_subdir");
        return 1;
    }
    printf("Created directory: %s\n", dir_path);

    // 4. 打开源目录和目标目录,获取文件描述符
    // O_RDONLY: 只读打开
    // O_DIRECTORY: 确保打开的是目录
    src_dir_fd = open(src_dir, O_RDONLY | O_DIRECTORY);
    if (src_dir_fd == -1) {
        perror("open source_dir");
        return 1;
    }

    dst_dir_fd = open(dst_dir, O_RDONLY | O_DIRECTORY);
    if (dst_dir_fd == -1) {
        perror("open dest_dir");
        close(src_dir_fd); // 关闭已打开的
        return 1;
    }

    // 5. 使用 renameat 将文件从源目录移动到目标目录并重命名
    // olddirfd=src_dir_fd, oldpath=file_name (相对路径)
    // newdirfd=dst_dir_fd, newpath=renamed_file_name (相对路径)
    printf("Moving and renaming file using renameat...\n");
    result = renameat(src_dir_fd, file_name, dst_dir_fd, renamed_file_name);
    if (result == -1) {
        perror("renameat file");
        close(src_dir_fd);
        close(dst_dir_fd);
        return 1;
    }
    printf("File moved from %s/%s to %s/%s\n", src_dir, file_name, dst_dir, renamed_file_name);

    // 6. 使用 renameat 将目录从源目录移动到目标目录并重命名
    printf("Moving and renaming directory using renameat...\n");
    result = renameat(src_dir_fd, dir_name, dst_dir_fd, renamed_dir_name);
    if (result == -1) {
        perror("renameat directory");
        close(src_dir_fd);
        close(dst_dir_fd);
        return 1;
    }
    printf("Directory moved from %s/%s to %s/%s\n", src_dir, dir_name, dst_dir, renamed_dir_name);

    // 7. 使用 renameat 在同一目录内重命名文件 (使用 AT_FDCWD)
    // 将目标目录中的文件重命名回原始名称
    printf("Renaming file within destination directory using AT_FDCWD...\n");
    result = renameat(AT_FDCWD, // 当前工作目录 (或指定 dst_dir 的绝对路径)
                      "dest_dir/moved_and_renamed_file.txt", // oldpath (绝对路径)
                      dst_dir_fd,                           // newdirfd
                      file_name);                           // newpath (相对路径)
    if (result == -1) {
        perror("renameat within dest_dir");
        close(src_dir_fd);
        close(dst_dir_fd);
        return 1;
    }
    printf("File renamed within %s from 'moved_and_renamed_file.txt' to '%s'\n", dst_dir, file_name);

    // 8. 清理: 关闭目录文件描述符
    close(src_dir_fd);
    close(dst_dir_fd);

    printf("Example completed successfully.\n");
    printf("Check the contents of '%s' and '%s' directories.\n", src_dir, dst_dir);

    return 0;
}

编译和运行:

# 假设代码保存在 renameat_example.c 中
gcc -o renameat_example renameat_example.c

# 运行
./renameat_example

# 查看结果
ls -R source_dir dest_dir

# 清理 (如果需要)
# rm -rf source_dir dest_dir