chown系统调用及示例

我们继续学习 Linux 系统编程中的重要函数 chown 函数,它用于改变文件的所有者和所属组。

chrown 函数


1. 函数介绍

chown 是一个 Linux 系统调用,用于改变文件的所有者用户 ID (UID) 和/或组 ID (GID)。这使得具有适当权限的用户(通常是 root 或文件的当前所有者)可以将文件的归属权转移给其他用户或组。

这对于系统管理、权限控制和文件共享非常重要。例如,系统管理员可能需要将一个文件的所有权从一个用户转移到另一个用户,或者将文件的组所有权更改为一个特定的组,以便该组的成员可以访问它。

需要注意的是,只有特权进程(有效用户 ID 为 0,通常是 root)可以将文件的所有者更改为任意用户。非特权进程通常只能将文件的所有者设置为进程的有效用户 ID(即,不能将文件给别人,但可以放弃文件的所有权给自己,或者在已经是所有者时更改组)。


2. 函数原型

#include <unistd.h> // 必需

int chown(const char *pathname, uid_t owner, gid_t group);

3. 功能

  • 改变文件所有者: 将由 pathname 指定的文件的所有者 UID 更改为 owner
  • 改变文件所属组: 将由 pathname 指定的文件的组 GID 更改为 group
  • 同时改变: 可以同时改变所有者和所属组。
  • 选择性改变: 如果 owner 或 group 被设置为特殊值 -1(或 (uid_t) -1 / (gid_t) -1),则相应的 ID 不会被更改。

4. 参数

  • const char *pathname: 指向一个以空字符 (\0) 结尾的字符串,该字符串包含了要更改所有权的文件或目录的路径名。这可以是相对路径或绝对路径。
  • uid_t owner: 新的所有者用户 ID。
    • 如果是 (uid_t) -1,则不更改文件的所有者。
    • 如果是有效的 UID(如 0, 1000, 1001 等),则尝试将文件所有者更改为该 UID。
  • gid_t group: 新的所属组 ID。
    • 如果是 (gid_t) -1,则不更改文件的所属组。
    • 如果是有效的 GID(如 0, 100, 1001 等),则尝试将文件所属组更改为该 GID。

5. 返回值

  • 成功时: 返回 0。
  • 失败时:
    • 返回 -1,并设置全局变量 errno 来指示具体的错误原因:
      • EACCES: 搜索路径名中的某个目录被拒绝。
      • EIO: 执行 I/O 错误。
      • ELOOP: 解析 pathname 时遇到符号链接环。
      • ENAMETOOLONG: 路径名过长。
      • ENOENT: 文件不存在。
      • ENOMEM: 路径名无法分配内存。
      • ENOTDIR: 路径名前缀不是一个目录。
      • EPERM: 调用进程没有权限更改所有权。最常见的原因是非特权用户试图将文件所有者更改为其他用户。
      • EROFS: 路径名存在于只读文件系统上。
      • EFAULTpathname 指针指向进程地址空间之外。

6. 相似函数,或关联函数

  • fchown(int fd, uid_t owner, gid_t group): 与 chown 功能相同,但通过已打开的文件描述符而不是路径名来指定文件。这可以避免路径解析,并且在某些情况下更高效或更安全。
  • lchown(const char *pathname, uid_t owner, gid_t group): 与 chown 类似,但如果 pathname 是一个符号链接,lchown 会更改符号链接本身的所有权,而不是它指向的目标文件的所有权。chown 会跟随符号链接。
  • fchownat(int dirfd, const char *pathname, uid_t owner, gid_t group, int flags): 更现代的函数,允许使用相对路径(相对于 dirfd 描述符对应的目录)并提供额外的标志(如 AT_SYMLINK_NOFOLLOW)。

7. 示例代码

示例 1:基本的所有权更改

这个例子演示了如何使用 chown 来更改文件的所有者和/或组。

#include <unistd.h>     // chown
#include <stdio.h>      // perror, printf
#include <stdlib.h>     // exit
#include <sys/stat.h>   // struct stat, stat
#include <pwd.h>        // getpwuid
#include <grp.h>        // getgrgid
#include <errno.h>      // errno
#include <string.h>     // strerror

// 辅助函数:打印文件的当前所有权
void print_file_owner(const char *pathname) {
    struct stat sb;
    if (stat(pathname, &sb) == -1) {
        perror("stat");
        return;
    }

    struct passwd *pw = getpwuid(sb.st_uid);
    struct group  *gr = getgrgid(sb.st_gid);

    printf("文件 '%s' 的当前所有权:\n", pathname);
    printf("  UID: %d", sb.st_uid);
    if (pw) {
        printf(" (用户: %s)", pw->pw_name);
    }
    printf("\n");

    printf("  GID: %d", sb.st_gid);
    if (gr) {
        printf(" (组: %s)", gr->gr_name);
    }
    printf("\n");
}

int main(int argc, char *argv[]) {
    if (argc != 4) {
        fprintf(stderr, "用法: %s <文件路径> <新UID> <新GID>\n", argv[0]);
        fprintf(stderr, "      使用 -1 表示不更改相应的ID。\n");
        fprintf(stderr, "      示例: %s myfile.txt 1000 1000\n", argv[0]);
        fprintf(stderr, "            %s myfile.txt -1 1000  (仅更改组)\n", argv[0]);
        exit(EXIT_FAILURE);
    }

    const char *pathname = argv[1];
    uid_t new_uid;
    gid_t new_gid;

    // 解析 UID 和 GID 参数
    if (strcmp(argv[2], "-1") == 0) {
        new_uid = (uid_t) -1; // 不更改 UID
    } else {
        new_uid = (uid_t) atoi(argv[2]);
    }

    if (strcmp(argv[3], "-1") == 0) {
        new_gid = (gid_t) -1; // 不更改 GID
    } else {
        new_gid = (gid_t) atoi(argv[3]);
    }

    printf("准备更改文件 '%s' 的所有权:\n", pathname);
    printf("  新 UID: %d (不更改则为-1)\n", (int)new_uid);
    printf("  新 GID: %d (不更改则为-1)\n", (int)new_gid);

    // 打印更改前的所有权
    print_file_owner(pathname);

    // 执行 chown 操作
    if (chown(pathname, new_uid, new_gid) == -1) {
        perror("chown 失败");
        exit(EXIT_FAILURE);
    }

    printf("\nchown 操作成功!\n");

    // 打印更改后的所有权
    print_file_owner(pathname);

    return 0;
}

代码解释:

  1. 定义了一个辅助函数 print_file_owner,它使用 stat 获取文件信息,并使用 getpwuid 和 getgrgid 将 UID/GID 解析为用户名和组名,然后打印出来。
  2. main 函数接受三个命令行参数:文件路径、新 UID、新 GID。
  3. 它解析 -1 为 “不更改”,其他值转换为对应的 uid_t/gid_t
  4. 调用 print_file_owner 显示更改前的状态。
  5. 调用 chown(pathname, new_uid, new_gid) 执行所有权更改。
  6. 如果成功,再次调用 print_file_owner 显示更改后的状态。

示例 2:系统管理脚本示例

这个例子模拟了一个简单的系统管理场景,其中需要批量更改一组文件的所有权。

#define _GNU_SOURCE // 为了使用一些 GNU 扩展
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/stat.h>
#include <pwd.h>
#include <grp.h>
#include <dirent.h> // 用于遍历目录

// 检查当前用户是否为 root (UID 0)
int is_root() {
    return (geteuid() == 0);
}

// 递归更改目录下所有文件的所有权
int change_ownership_recursive(const char *dir_path, uid_t uid, gid_t gid) {
    DIR *dir;
    struct dirent *entry;
    char full_path[1024];

    dir = opendir(dir_path);
    if (!dir) {
        perror("opendir");
        return -1;
    }

    while ((entry = readdir(dir)) != NULL) {
        // 跳过 . 和 ..
        if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) {
            continue;
        }

        // 构造完整路径
        snprintf(full_path, sizeof(full_path), "%s/%s", dir_path, entry->d_name);

        // 更改当前文件/目录的所有权
        if (chown(full_path, uid, gid) == -1) {
            fprintf(stderr, "警告: 无法更改 '%s' 的所有权: %s\n", full_path, strerror(errno));
            // 不要因为单个文件失败而停止整个过程
        } else {
            printf("已更改 '%s' 的所有权\n", full_path);
        }

        // 如果是目录,则递归处理
        struct stat sb;
        if (stat(full_path, &sb) == 0 && S_ISDIR(sb.st_mode)) {
            change_ownership_recursive(full_path, uid, gid);
        }
    }

    closedir(dir);
    return 0;
}

int main(int argc, char *argv[]) {
    if (argc < 4) {
        fprintf(stderr, "用法: %s <目录路径> <用户名或UID> <组名或GID> [-r]\n", argv[0]);
        fprintf(stderr, "      -r: 递归更改子目录中所有文件\n");
        fprintf(stderr, "      使用 -1 表示不更改 UID 或 GID\n");
        fprintf(stderr, "      示例: %s /home/newuser alice developers\n", argv[0]);
        fprintf(stderr, "            %s /data -1 mygroup -r\n", argv[0]);
        exit(EXIT_FAILURE);
    }

    if (!is_root()) {
        fprintf(stderr, "警告: 你可能需要 root 权限来更改文件所有权。\n");
        fprintf(stderr, "当前有效 UID: %d\n", geteuid());
    }

    const char *path = argv[1];
    const char *user_str = argv[2];
    const char *group_str = argv[3];
    int recursive = 0;

    // 检查是否有 -r 标志
    for (int i = 4; i < argc; i++) {
        if (strcmp(argv[i], "-r") == 0) {
            recursive = 1;
            break;
        }
    }

    uid_t uid = (uid_t) -1;
    gid_t gid = (gid_t) -1;

    // 解析用户
    if (strcmp(user_str, "-1") != 0) {
        struct passwd *pw = getpwnam(user_str); // 按用户名查找
        if (pw) {
            uid = pw->pw_uid;
        } else {
            // 尝试按 UID 解析
            char *endptr;
            uid = (uid_t) strtoul(user_str, &endptr, 10);
            if (*endptr != '\0') {
                fprintf(stderr, "错误: 无效的用户名或 UID: %s\n", user_str);
                exit(EXIT_FAILURE);
            }
        }
    }

    // 解析组
    if (strcmp(group_str, "-1") != 0) {
        struct group *gr = getgrnam(group_str); // 按组名查找
        if (gr) {
            gid = gr->gr_gid;
        } else {
            // 尝试按 GID 解析
            char *endptr;
            gid = (gid_t) strtoul(group_str, &endptr, 10);
            if (*endptr != '\0') {
                fprintf(stderr, "错误: 无效的组名或 GID: %s\n", group_str);
                exit(EXIT_FAILURE);
            }
        }
    }

    printf("准备更改 '%s' 的所有权:\n", path);
    printf("  UID: %d (%s)\n", (int)uid, (uid==(uid_t)-1) ? "不更改" : user_str);
    printf("  GID: %d (%s)\n", (int)gid, (gid==(gid_t)-1) ? "不更改" : group_str);
    printf("  递归: %s\n", recursive ? "是" : "否");

    // 执行所有权更改
    int result;
    if (recursive) {
        result = change_ownership_recursive(path, uid, gid);
    } else {
        result = chown(path, uid, gid);
        if (result != -1) {
            printf("已更改 '%s' 的所有权\n", path);
        }
    }

    if (result == -1) {
        perror("chown 失败");
        exit(EXIT_FAILURE);
    }

    printf("所有权更改操作完成。\n");
    return 0;
}

代码解释:

  1. is_root 函数检查当前进程的有效用户 ID 是否为 0 (root)。
  2. change_ownership_recursive 函数使用 opendirreaddir 遍历目录,并对每个文件/子目录递归调用 chown
  3. main 函数处理命令行参数,支持按用户名/组名或 UID/GID 指定,并支持递归选项 -r
  4. 它使用 getpwnam 和 getgrnam 将用户名和组名解析为 UID/GID。
  5. 根据是否指定 -r 标志,选择调用普通的 chown 或递归函数。

示例 3:错误处理和权限检查

这个例子重点演示 chown 可能遇到的各种错误情况及其处理。

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <sys/stat.h>

void demonstrate_chown_errors() {
    printf("=== Chown 错误处理演示 ===\n");

    // 1. 尝试更改不存在的文件
    printf("\n1. 尝试更改不存在的文件:\n");
    if (chown("/nonexistent/file.txt", 1000, 1000) == -1) {
        printf("   错误: %s\n", strerror(errno));
        // 通常返回 ENOENT
    }

    // 2. 创建一个测试文件
    const char *test_file = "chown_test.txt";
    FILE *fp = fopen(test_file, "w");
    if (fp) {
        fprintf(fp, "Test file for chown\n");
        fclose(fp);
        printf("\n2. 创建测试文件: %s\n", test_file);
    } else {
        perror("创建测试文件失败");
        return;
    }

    // 3. 非特权用户尝试将文件给其他用户 (通常会失败)
    printf("\n3. 非特权用户尝试将文件所有权转移给其他用户:\n");
    uid_t current_uid = getuid();
    uid_t target_uid = (current_uid == 1000) ? 1001 : 1000; // 假设另一个用户
    
    printf("   当前用户 UID: %d\n", current_uid);
    printf("   尝试更改为 UID: %d\n", target_uid);
    
    if (chown(test_file, target_uid, (gid_t)-1) == -1) {
        printf("   错误: %s\n", strerror(errno));
        if (errno == EPERM) {
            printf("   说明: 非特权用户不能将文件所有权转移给其他用户\n");
        }
    } else {
        printf("   更改成功 (这在非 root 用户下不太可能)\n");
    }

    // 4. 尝试更改只读文件系统上的文件
    printf("\n4. 尝试更改只读文件系统上的文件:\n");
    // 注意:这需要一个实际的只读文件系统挂载点来测试
    // 在 /proc 或 /sys 上尝试通常会返回 EROFS
    if (chown("/proc/version", 0, 0) == -1) {
        printf("   错误: %s\n", strerror(errno));
        if (errno == EROFS) {
            printf("   说明: 不能更改只读文件系统上的文件所有权\n");
        }
    }

    // 5. 正常的组更改(如果可能)
    printf("\n5. 尝试更改文件组所有权:\n");
    gid_t current_gid = getgid();
    printf("   当前组 GID: %d\n", current_gid);
    
    // 尝试更改为自己所在的组(更可能成功)
    if (chown(test_file, (uid_t)-1, current_gid) == -1) {
        printf("   更改组失败: %s\n", strerror(errno));
    } else {
        printf("   组所有权更改成功\n");
    }

    // 清理测试文件
    unlink(test_file);
    printf("\n6. 清理完成\n");
}

int main() {
    printf("当前进程信息:\n");
    printf("  实际 UID: %d\n", getuid());
    printf("  有效 UID: %d\n", geteuid());
    printf("  实际 GID: %d\n", getgid());
    printf("  有效 GID: %d\n", getegid());
    
    demonstrate_chown_errors();
    
    printf("\n=== 总结 ===\n");
    printf("chown 常见错误:\n");
    printf("  EPERM: 权限不足(非 root 用户试图更改所有者)\n");
    printf("  ENOENT: 文件不存在\n");
    printf("  EROFS: 只读文件系统\n");
    printf("  EACCES: 搜索路径被拒绝\n");
    printf("  EIO: I/O 错误\n\n");
    
    printf("权限规则:\n");
    printf("  - Root 用户可以更改任何文件的所有者和组\n");
    printf("  - 普通用户通常只能更改自己拥有的文件的组\n");
    printf("  - 普通用户不能将文件所有权转移给其他用户\n");
    
    return 0;
}

代码解释:

  1. demonstrate_chown_errors 函数依次演示了 chown 可能遇到的各种典型错误。
  2. 首先尝试操作不存在的文件,展示 ENOENT 错误。
  3. 创建测试文件用于后续演示。
  4. 演示非特权用户尝试将文件所有权转移给其他用户的 EPERM 错误。
  5. 尝试更改只读文件系统上文件的 EROFS 错误。
  6. 展示正常的组更改操作。
  7. 最后清理测试文件并总结常见的错误类型和权限规则。

编译和运行:

# 编译示例
gcc -o chown_example1 chown_example1.c
gcc -o chown_example2 chown_example2.c
gcc -o chown_example3 chown_example3.c

# 运行示例 (需要适当权限)
# 示例1: 基本用法
touch testfile.txt
./chown_example1 testfile.txt 1000 1000  # 需要 root 权限
./chown_example1 testfile.txt -1 1000    # 可能不需要 root 权限

# 示例2: 系统管理
./chown_example2 /tmp/mydir alice developers -r

# 示例3: 错误处理
./chown_example3

总结:

chown 函数是 Linux 系统管理中不可或缺的工具,用于精确控制文件和目录的归属权。理解其参数、返回值和权限模型对于编写健壮的系统程序至关重要。务必注意权限限制和潜在的错误情况,并在实际使用中谨慎操作,特别是在生产环境中。

此条目发表在未分类分类目录。将固定链接加入收藏夹。

发表回复

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