chmod系统调用及示例

我们继续学习 Linux 系统编程中的重要函数。这次我们介绍 chmod 函数,它用于改变文件的访问权限。

chmod 函数简介


1. 函数介绍

chmod 是一个 Linux 系统调用,用于改变文件或目录的访问权限(也称为文件模式位)。这些权限决定了哪些用户可以读取、写入或执行文件。

文件权限是 Unix/Linux 系统安全模型的基础。每个文件都有三组权限位:所有者(user)、所属组(group)和其他用户(others)。每组权限又包含三种基本权限:读(read, r)、写(write, w)和执行(execute, x)。

通过 chmod,具有适当权限的用户(通常是文件所有者或 root)可以调整这些权限,以控制对文件的访问。例如,一个用户可能希望保护一个私密文件,使其只能被自己读取;或者希望让一个脚本文件对所有用户都可执行。


2. 函数原型

#include <sys/stat.h> // 必需

int chmod(const char *pathname, mode_t mode);

3. 功能

  • 改变文件权限: 将由 pathname 指定的文件或目录的访问权限设置为 mode 参数指定的值。
  • 设置绝对权限mode 参数通常是一个八进制数(如 0644, 0755)或通过位运算组合的符号常量(如 S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)。

4. 参数

  • const char *pathname: 指向一个以空字符 (\0) 结尾的字符串,该字符串包含了要更改权限的文件或目录的路径名。这可以是相对路径或绝对路径。
  • mode_t mode: 指定新的文件权限。这个参数可以有两种表示方式:
    1. 八进制表示法:
      • 最常见的形式,如 064407550600
      • 第一个数字 0 表示这是一个八进制数。
      • 接下来的三位数字分别代表所有者(user)、组(group)、其他用户(others)的权限。
      • 每一位的值是读(4)、写(2)、执行(1)的组合:
        • 7 (4+2+1) = 读+写+执行 (rwx)
        • 6 (4+2) = 读+写 (rw-)
        • 5 (4+1) = 读+执行 (r-x)
        • 4 (4) = 只读 (r–)
        • 0 = 无权限 (—)
      • 例如:
        • 0644 表示所有者:读写 (6),组和其他用户:只读 (4)。常用于普通文件。
        • 0755 表示所有者:读写执行 (7),组和其他用户:读执行 (5)。常用于可执行文件或目录。
        • 0600 表示所有者:读写 (6),组和其他用户:无权限 (0)。常用于私密文件。
    2. 符号常量表示法:
      • 使用 <sys/stat.h> 中定义的宏进行位运算组合。
      • 用户类别:
        • S_IRWXU: 所有者的读、写、执行权限
        • S_IRUSR: 所有者的读权限
        • S_IWUSR: 所有者的写权限
        • S_IXUSR: 所有者的执行权限
      • 组类别:
        • S_IRWXG: 组的读、写、执行权限
        • S_IRGRP: 组的读权限
        • S_IWGRP: 组的写权限
        • S_IXGRP: 组的执行权限
      • 其他用户类别:
        • S_IRWXO: 其他用户的读、写、执行权限
        • S_IROTH: 其他用户的读权限
        • S_IWOTH: 其他用户的写权限
        • S_IXOTH: 其他用户的执行权限
      • 特殊位:
        • S_ISUID: 设置用户ID位 (set-user-ID)
        • S_ISGID: 设置组ID位 (set-group-ID)
        • S_ISVTX: 粘滞位 (sticky bit)
      • 例如:
        • S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH 等价于 0644
        • S_IRWXU | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH 等价于 0755

5. 返回值

  • 成功时: 返回 0。
  • 失败时:
    • 返回 -1,并设置全局变量 errno 来指示具体的错误原因:
      • EACCES: 搜索路径名中的某个目录被拒绝。
      • EROFS: 路径名存在于只读文件系统上。
      • EIO: 执行 I/O 错误。
      • ELOOP: 解析 pathname 时遇到符号链接环。
      • ENAMETOOLONG: 路径名过长。
      • ENOENT: 文件不存在。
      • ENOMEM: 路径名无法分配内存。
      • ENOTDIR: 路径名前缀不是一个目录。
      • EPERM: 操作不被文件系统或操作系统允许。例如,尝试在某些文件系统上设置 set-group-ID 位。
      • EFAULTpathname 指针指向进程地址空间之外。
      • EINVALmode 参数无效。

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

  • fchmod(int fd, mode_t mode): 与 chmod 功能相同,但通过已打开的文件描述符而不是路径名来指定文件。这可以避免路径解析。
  • fchmodat(int dirfd, const char *pathname, mode_t mode, int flags): 更现代的函数,允许使用相对路径(相对于 dirfd 描述符对应的目录)并提供额外的标志。
  • umask(mode_t mask): 设置进程的文件权限掩码,它会影响后续创建的文件的默认权限。
  • statlstatfstat: 这些函数可以用来获取文件的当前权限,而不是设置它们。

7. 示例代码

示例 1:基本的权限更改

这个例子演示了如何使用 chmod 来更改文件的权限,包括八进制和符号常量两种方式。

#include <sys/stat.h>  // chmod, stat, struct stat
#include <stdio.h>     // perror, printf
#include <stdlib.h>    // exit
#include <errno.h>     // errno
#include <string.h>    // strerror

// 辅助函数:将 mode_t 转换为可读的权限字符串
void print_permissions(mode_t mode) {
    char perms[11];
    // 初始化字符数组
    strcpy(perms, "----------");

    // 用户权限
    if (mode & S_IRUSR) perms[1] = 'r';
    if (mode & S_IWUSR) perms[2] = 'w';
    if (mode & S_IXUSR) perms[3] = 'x';

    // 组权限
    if (mode & S_IRGRP) perms[4] = 'r';
    if (mode & S_IWGRP) perms[5] = 'w';
    if (mode & S_IXGRP) perms[6] = 'x';

    // 其他用户权限
    if (mode & S_IROTH) perms[7] = 'r';
    if (mode & S_IWOTH) perms[8] = 'w';
    if (mode & S_IXOTH) perms[9] = 'x';

    // 特殊位
    if (mode & S_ISUID) perms[3] = (perms[3] == 'x') ? 's' : 'S';
    if (mode & S_ISGID) perms[6] = (perms[6] == 'x') ? 's' : 'S';
    if (mode & S_ISVTX) perms[9] = (perms[9] == 'x') ? 't' : 'T';

    printf("%s", perms);
}

// 辅助函数:打印文件的详细信息
void print_file_info(const char *pathname) {
    struct stat sb;
    if (stat(pathname, &sb) == -1) {
        perror("stat");
        return;
    }

    printf("文件 '%s' 的信息:\n", pathname);
    printf("  Inode: %ld\n", sb.st_ino);
    printf("  权限: ", pathname);
    print_permissions(sb.st_mode);
    printf(" (八进制: %o)\n", sb.st_mode & 0777);
    printf("  大小: %ld 字节\n", sb.st_size);
}

int main(int argc, char *argv[]) {
    if (argc != 3) {
        fprintf(stderr, "用法: %s <文件路径> <新权限>\n", argv[0]);
        fprintf(stderr, "      权限可以是八进制 (如 0644) 或符号 (如 u+r)\n");
        fprintf(stderr, "      示例: %s myfile.txt 0644\n", argv[0]);
        fprintf(stderr, "            %s script.sh 0755\n", argv[0]);
        exit(EXIT_FAILURE);
    }

    const char *pathname = argv[1];
    const char *mode_str = argv[2];

    printf("准备更改文件 '%s' 的权限\n", pathname);

    // 打印更改前的信息
    print_file_info(pathname);

    // 解析权限模式
    mode_t new_mode;
    char *endptr;

    // 尝试解析为八进制数
    new_mode = (mode_t) strtol(mode_str, &endptr, 8);
    if (*endptr != '\0') {
        fprintf(stderr, "错误: 不支持的权限格式 '%s'。请使用八进制数(如 0644)。\n", mode_str);
        exit(EXIT_FAILURE);
    }

    printf("\n新的权限模式: ");
    print_permissions(new_mode);
    printf(" (八进制: %o)\n", new_mode);

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

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

    // 打印更改后的信息
    print_file_info(pathname);

    return 0;
}

代码解释:

  1. 定义了两个辅助函数:
    • print_permissions: 将 mode_t 类型的权限值转换为人类可读的字符串(如 -rw-r--r--)。
    • print_file_info: 使用 stat 获取并打印文件的详细信息,包括权限。
  2. main 函数接受文件路径和权限字符串作为参数。
  3. 它使用 strtol 将权限字符串解析为八进制数。
  4. 调用 print_file_info 显示更改前的状态。
  5. 调用 chmod(pathname, new_mode) 执行权限更改。
  6. 如果成功,再次调用 print_file_info 显示更改后的状态。

示例 2:批量权限管理

这个例子模拟了一个简单的批量权限管理工具,可以同时更改多个文件的权限。

#define _GNU_SOURCE
#include <sys/stat.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <dirent.h>
#include <fnmatch.h> // 用于模式匹配

// 递归更改目录下匹配模式的文件权限
int change_permissions_recursive(const char *dir_path, const char *pattern, mode_t mode, int verbose) {
    DIR *dir;
    struct dirent *entry;
    char full_path[1024];
    int changed_count = 0;
    int error_count = 0;

    dir = opendir(dir_path);
    if (!dir) {
        fprintf(stderr, "无法打开目录 '%s': %s\n", dir_path, strerror(errno));
        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 (fnmatch(pattern, entry->d_name, 0) == 0) {
            // 匹配,尝试更改权限
            if (chmod(full_path, mode) == -1) {
                fprintf(stderr, "警告: 无法更改 '%s' 的权限: %s\n", full_path, strerror(errno));
                error_count++;
            } else {
                if (verbose) {
                    printf("已更改 '%s' 的权限\n", full_path);
                }
                changed_count++;
            }
        }

        // 如果是目录,则递归处理
        struct stat sb;
        if (stat(full_path, &sb) == 0 && S_ISDIR(sb.st_mode)) {
            int sub_result = change_permissions_recursive(full_path, pattern, mode, verbose);
            if (sub_result >= 0) {
                changed_count += sub_result;
            } else {
                error_count++;
            }
        }
    }

    closedir(dir);
    
    if (error_count > 0) {
        return -1;
    }
    return changed_count;
}

// 打印权限更改的摘要
void print_mode_summary(mode_t mode) {
    printf("权限设置为: ");
    // 用户权限
    printf((mode & S_IRUSR) ? "r" : "-");
    printf((mode & S_IWUSR) ? "w" : "-");
    printf((mode & S_IXUSR) ? "x" : "-");
    // 组权限
    printf((mode & S_IRGRP) ? "r" : "-");
    printf((mode & S_IWGRP) ? "w" : "-");
    printf((mode & S_IXGRP) ? "x" : "-");
    // 其他用户权限
    printf((mode & S_IROTH) ? "r" : "-");
    printf((mode & S_IWOTH) ? "w" : "-");
    printf((mode & S_IXOTH) ? "x" : "-");
    printf(" (八进制: %04o)\n", mode);
}

int main(int argc, char *argv[]) {
    if (argc < 4) {
        fprintf(stderr, "用法: %s <目录路径> <文件模式> <权限> [-r] [-v] [-p pattern]\n", argv[0]);
        fprintf(stderr, "      -r: 递归处理子目录\n");
        fprintf(stderr, "      -v: 详细输出\n");
        fprintf(stderr, "      -p pattern: 只处理匹配模式的文件 (支持通配符)\n");
        fprintf(stderr, "      示例: %s /home/user *.txt 0644 -r -v\n", argv[0]);
        fprintf(stderr, "            %s /var/log 0600 -p \"*.log\"\n", argv[0]);
        exit(EXIT_FAILURE);
    }

    const char *dir_path = argv[1];
    const char *pattern = argv[2];
    const char *mode_str = argv[3];
    int recursive = 0;
    int verbose = 0;
    const char *file_pattern = "*"; // 默认匹配所有文件

    // 解析选项
    for (int i = 4; i < argc; i++) {
        if (strcmp(argv[i], "-r") == 0) {
            recursive = 1;
        } else if (strcmp(argv[i], "-v") == 0) {
            verbose = 1;
        } else if (strcmp(argv[i], "-p") == 0 && i + 1 < argc) {
            file_pattern = argv[++i];
        } else {
            fprintf(stderr, "未知选项: %s\n", argv[i]);
            exit(EXIT_FAILURE);
        }
    }

    // 解析权限模式
    mode_t mode;
    char *endptr;
    mode = (mode_t) strtol(mode_str, &endptr, 8);
    if (*endptr != '\0') {
        fprintf(stderr, "错误: 无效的权限模式 '%s'。请使用八进制数(如 0644)。\n", mode_str);
        exit(EXIT_FAILURE);
    }

    printf("=== 批量权限更改工具 ===\n");
    printf("目录: %s\n", dir_path);
    printf("文件模式: %s\n", pattern);
    printf("文件名匹配: %s\n", file_pattern);
    print_mode_summary(mode);
    printf("递归: %s\n", recursive ? "是" : "否");
    printf("详细输出: %s\n", verbose ? "是" : "否");

    // 执行权限更改
    int result;
    if (recursive) {
        result = change_permissions_recursive(dir_path, file_pattern, mode, verbose);
    } else {
        // 非递归处理
        DIR *dir = opendir(dir_path);
        if (!dir) {
            perror("opendir");
            exit(EXIT_FAILURE);
        }
        
        struct dirent *entry;
        char full_path[1024];
        int changed_count = 0;
        int error_count = 0;
        
        while ((entry = readdir(dir)) != NULL) {
            if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) {
                continue;
            }
            
            // 检查文件名是否匹配模式
            if (fnmatch(pattern, entry->d_name, 0) == 0) {
                // 检查文件名是否匹配文件模式
                if (fnmatch(file_pattern, entry->d_name, 0) == 0) {
                    snprintf(full_path, sizeof(full_path), "%s/%s", dir_path, entry->d_name);
                    if (chmod(full_path, mode) == -1) {
                        fprintf(stderr, "警告: 无法更改 '%s' 的权限: %s\n", full_path, strerror(errno));
                        error_count++;
                    } else {
                        if (verbose) {
                            printf("已更改 '%s' 的权限\n", full_path);
                        }
                        changed_count++;
                    }
                }
            }
        }
        closedir(dir);
        
        if (error_count > 0) {
            result = -1;
        } else {
            result = changed_count;
        }
    }

    if (result == -1) {
        fprintf(stderr, "\n权限更改过程中遇到错误。\n");
        exit(EXIT_FAILURE);
    } else {
        printf("\n权限更改完成。成功更改了 %d 个文件的权限。\n", result);
    }

    return 0;
}

代码解释:

  1. change_permissions_recursive 函数实现了递归的权限更改功能,使用 opendir 和 readdir 遍历目录。
  2. 它使用 fnmatch 函数来支持通配符模式匹配(如 *.txt*.log)。
  3. print_mode_summary 函数以人类可读的方式显示权限设置。
  4. main 函数处理命令行参数,支持递归(-r)、详细输出(-v)和文件名模式匹配(-p)选项。
  5. 程序会统计成功更改的文件数量和错误数量。

示例 3:权限安全和最佳实践

这个例子重点演示权限设置的安全考虑和最佳实践。

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

// 创建具有特定权限的测试文件
void create_test_files() {
    printf("=== 创建测试文件 ===\n");
    
    // 1. 创建普通文件
    FILE *fp = fopen("normal_file.txt", "w");
    if (fp) {
        fprintf(fp, "This is a normal file for permission testing.\n");
        fclose(fp);
        printf("创建普通文件: normal_file.txt\n");
    }
    
    // 2. 创建私密文件
    fp = fopen("private_file.txt", "w");
    if (fp) {
        fprintf(fp, "This is a private file containing sensitive data.\n");
        fclose(fp);
        printf("创建私密文件: private_file.txt\n");
    }
    
    // 3. 创建脚本文件
    fp = fopen("test_script.sh", "w");
    if (fp) {
        fprintf(fp, "#!/bin/bash\necho \"This is a test script.\"\n");
        fclose(fp);
        printf("创建脚本文件: test_script.sh\n");
    }
    
    // 4. 创建目录
    if (mkdir("test_directory", 0755) == 0) {
        printf("创建目录: test_directory\n");
    }
    
    printf("\n");
}

// 演示安全的权限设置
void demonstrate_secure_permissions() {
    printf("=== 安全权限设置演示 ===\n");
    
    // 1. 设置普通文件权限 (0644)
    printf("1. 设置普通文件权限为 0644 (rw-r--r--)\n");
    if (chmod("normal_file.txt", 0644) == 0) {
        printf("   成功: normal_file.txt 现在是 rw-r--r--\n");
    } else {
        printf("   失败: %s\n", strerror(errno));
    }
    
    // 2. 设置私密文件权限 (0600)
    printf("2. 设置私密文件权限为 0600 (rw-------)\n");
    if (chmod("private_file.txt", 0600) == 0) {
        printf("   成功: private_file.txt 现在是 rw-------\n");
    } else {
        printf("   失败: %s\n", strerror(errno));
    }
    
    // 3. 设置脚本文件权限 (0755)
    printf("3. 设置脚本文件权限为 0755 (rwxr-xr-x)\n");
    if (chmod("test_script.sh", 0755) == 0) {
        printf("   成功: test_script.sh 现在是 rwxr-xr-x\033[0m\n");
    } else {
        printf("   失败: %s\n", strerror(errno));
    }
    
    // 4. 设置目录权限 (0755)
    printf("4. 设置目录权限为 0755 (rwxr-xr-x)\n");
    if (chmod("test_directory", 0755) == 0) {
        printf("   成功: test_directory 现在是 rwxr-xr-x\033[0m\n");
    } else {
        printf("   失败: %s\n", strerror(errno));
    }
    
    printf("\n");
}

// 演示危险的权限设置
void demonstrate_dangerous_permissions() {
    printf("=== 危险权限设置警告 ===\n");
    
    printf("以下权限设置可能存在安全风险:\n");
    
    // 1. 世界可写的文件
    printf("1. 世界可写的普通文件 (0666)\n");
    printf("   风险: 任何用户都可以修改文件内容\n");
    printf("   建议: 使用 0644 代替\n");
    
    // 2. 世界可执行的文件
    printf("2. 世界可执行的敏感脚本 (0777)\n");
    printf("   风险: 任何用户都可以执行,可能存在安全漏洞\n");
    printf("   建议: 使用 0755 并确保脚本安全\n");
    
    // 3. 私密文件设置不当
    printf("3. 私密文件权限过于宽松 (0644)\n");
    printf("   风险: 组用户和其他用户可以读取私密信息\n");
    printf("   建议: 使用 0600 确保只有所有者可访问\n");
    
    printf("\n");
}

// 演示权限检查
void demonstrate_permission_checking() {
    printf("=== 权限检查最佳实践 ===\n");
    
    struct stat sb;
    
    // 检查私密文件权限
    if (stat("private_file.txt", &sb) == 0) {
        mode_t mode = sb.st_mode & 0777;
        printf("检查 private_file.txt 当前权限: %04o\n", mode);
        
        if (mode == 0600) {
            printf("✓ 权限设置正确,只有所有者可读写\n");
        } else if (mode & (S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH)) {
            printf("✗ 警告: 权限过于宽松,组用户或其他用户有访问权限\n");
        } else {
            printf("- 权限设置合理\n");
        }
    }
    
    // 检查脚本文件权限
    if (stat("test_script.sh", &sb) == 0) {
        mode_t mode = sb.st_mode & 0777;
        printf("检查 test_script.sh 当前权限: %04o\n", mode);
        
        if (mode & S_IXUSR) {
            printf("✓ 所有者有执行权限\n");
        } else {
            printf("✗ 所有者没有执行权限,脚本可能无法运行\n");
        }
    }
    
    printf("\n");
}

// 清理测试文件
void cleanup_test_files() {
    printf("=== 清理测试文件 ===\n");
    unlink("normal_file.txt");
    unlink("private_file.txt");
    unlink("test_script.sh");
    rmdir("test_directory");
    printf("清理完成\n");
}

int main() {
    printf("当前用户: UID=%d, GID=%d\n", getuid(), getgid());
    printf("\n");
    
    // 创建测试文件
    create_test_files();
    
    // 演示安全权限设置
    demonstrate_secure_permissions();
    
    // 演示危险权限设置
    demonstrate_dangerous_permissions();
    
    // 演示权限检查
    demonstrate_permission_checking();
    
    // 清理
    cleanup_test_files();
    
    printf("\n=== 权限设置最佳实践总结 ===\n");
    printf("普通文件: 0644 (rw-r--r--)\n");
    printf("私密文件: 0600 (rw-------)\n");
    printf("可执行文件: 0755 (rwxr-xr-x)\n");
    printf("私密可执行文件: 0700 (rwx------)\n");
    printf("目录: 0755 (rwxr-xr-x)\n");
    printf("私密目录: 0700 (rwx------)\n");
    printf("\n安全建议:\n");
    printf("1. 遵循最小权限原则\n");
    printf("2. 定期检查重要文件的权限\n");
    printf("3. 避免使用 0777 或 0666 等过于宽松的权限\n");
    printf("4. 对于敏感文件,使用 0600 或 0700\n");
    printf("5. 理解权限位的含义,避免误操作\n");
    
    return 0;
}

代码解释:

  1. create_test_files 函数创建了几种不同类型的测试文件。
  2. demonstrate_secure_permissions 演示了如何为不同类型的文件设置安全的权限。
  3. demonstrate_dangerous_permissions 警告了一些常见的危险权限设置。
  4. demonstrate_permission_checking 展示了如何检查现有文件的权限是否合理。
  5. cleanup_test_files 负责清理创建的测试文件。
  6. main 函数协调整个演示过程,并在最后总结权限设置的最佳实践。

编译和运行:

# 编译示例
gcc -o chmod_example1 chmod_example1.c
gcc -o chmod_example2 chmod_example2.c -lpthread
gcc -o chmod_example3 chmod_example3.c

# 运行示例
# 示例1: 基本用法
touch testfile.txt
./chmod_example1 testfile.txt 0644
./chmod_example1 script.sh 0755

# 示例2: 批量处理
mkdir testdir
touch testdir/file1.txt testdir/file2.log
./chmod_example2 testdir "*.txt" 0644 -r -v

# 示例3: 安全演示
./chmod_example3

总结:

chmod 函数是 Linux 文件系统权限管理的核心工具。掌握其使用方法对于系统安全和文件访问控制至关重要。在使用时应遵循最小权限原则,根据文件的实际用途设置合适的权限,并定期检查重要文件的权限设置,以防止安全漏洞。

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

发表回复

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