文件权限与创建:umask()、chmod()、access() 函数的使用,以及 Linux 文件权限模型

聊到文件操作,有个话题绕不开——权限。你想想看,在 Linux 下跑程序,动不动就 "Permission denied",是不是很头疼?

我刚开始做嵌入式开发那会儿,就吃过这个亏。程序写得好好的,一部署到目标板上,读写文件直接崩了。查了半天,原来是文件权限没设对。从那以后,我对这套权限模型就格外上心。

今天咱们就把 umask()chmod()access() 这三个函数彻底讲明白。顺便把 Linux 文件权限模型也捋一遍。

Linux 文件权限模型:先搞懂底层逻辑

Linux 的权限模型,说白了就三组权限:所有者(User)所属组(Group)其他人(Others)。每组又分三种操作:读(r=4)写(w=2)执行(x=1)

嗯,这里要注意:数字权限是用八进制表示的。比如 755,就是所有者有读写执行(7),组用户有读和执行(5),其他人也有读和执行(5)。

我习惯用一张图来理解这个模型,你看下面这个:

Linux 文件权限模型 所有者 (User) 读 (r=4) | 写 (w=2) | 执行 (x=1) 示例: rwx (7) 所属组 (Group) 读 (r=4) | 写 (w=2) | 执行 (x=1) 示例: r-x (5) 其他人 (Others) 读 (r=4) | 写 (w=2) | 执行 (x=1) 示例: r-x (5) 特殊权限位 SUID (4) | SGID (2) | Sticky Bit (1) 权限表示例 chmod 755 file → rwxr-xr-x | chmod 644 file → rw-r--r--

umask():创建文件时的默认权限遮罩

每次你用 open()creat() 创建新文件时,系统都会拿你指定的权限减去 umask 值。说白了,umask 就是个「权限过滤器」。

举个例子:你调用 open("test.txt", O_CREAT, 0666),如果当前 umask 是 0022,那实际创建出来的文件权限是 0666 & ~0022 = 0644,也就是 rw-r--r--

我个人习惯把 umask 设成 0027,这样组用户只有读权限,其他人啥权限都没有。安全嘛。

核心公式:实际权限 = 请求权限 & (~umask)

注意是按位取反后做与运算,不是简单的减法。

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>

int main() {
    // 设置新的 umask 值
    mode_t old_mask = umask(0027);
    printf("旧的 umask: %04o\n", old_mask);
    
    // 创建文件,请求 0666 权限
    int fd = open("test.txt", O_CREAT | O_WRONLY, 0666);
    if (fd == -1) {
        perror("open");
        return 1;
    }
    close(fd);
    
    // 实际权限会是 0640 (rw-r-----)
    // 因为 0666 & ~0027 = 0666 & 0750 = 0640
    
    // 恢复原来的 umask
    umask(old_mask);
    return 0;
}

注意:umask() 是进程级别的设置,会影响当前进程及其子进程创建的所有文件。多线程环境下要小心,因为 umask 是进程共享的。

chmod():修改已有文件的权限

文件创建完了,想改权限怎么办?用 chmod()。这个函数直接修改文件的权限位,不受 umask 影响。

我在项目中遇到过一个问题:程序需要写一个日志文件,但日志文件是之前用 root 权限创建的,普通用户写不了。后来我在程序初始化时调用了 chmod(),把权限改成 0666,问题就解决了。

#include <sys/stat.h>
#include <stdio.h>

int main() {
    // 将文件权限改为 rwxr-xr-x (755)
    if (chmod("test.txt", 0755) == -1) {
        perror("chmod");
        return 1;
    }
    
    // 也可以使用符号模式
    // S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH
    // 等价于 0755
    
    printf("权限修改成功\n");
    return 0;
}
符号常量 含义 八进制值
S_IRUSR 所有者读 0400
S_IWUSR 所有者写 0200
S_IXUSR 所有者执行 0100
S_IRGRP 组用户读 0040
S_IWGRP 组用户写 0020
S_IXGRP 组用户执行 0010
S_IROTH 其他人读 0004
S_IWOTH 其他人写 0002
S_IXOTH 其他人执行 0001

小技巧:fchmod() 可以通过文件描述符修改权限,适合已经打开的文件。原型是 int fchmod(int fd, mode_t mode);

access():检查文件权限,别等出错了再后悔

有时候你需要在操作文件之前,先确认一下自己有没有权限。这就是 access() 的用武之地。

我曾经写过一个备份工具,每次备份前都要检查源文件是否可读、目标目录是否可写。用 access() 提前检查,比直接操作然后捕获错误要优雅得多。

#include <unistd.h>
#include <stdio.h>

int main() {
    const char *filename = "test.txt";
    
    // 检查文件是否存在
    if (access(filename, F_OK) == -1) {
        printf("文件不存在\n");
        return 1;
    }
    
    // 检查是否可读
    if (access(filename, R_OK) == -1) {
        printf("没有读权限\n");
        return 1;
    }
    
    // 检查是否可写
    if (access(filename, W_OK) == -1) {
        printf("没有写权限\n");
        return 1;
    }
    
    // 检查是否可执行
    if (access(filename, X_OK) == -1) {
        printf("没有执行权限\n");
    } else {
        printf("文件可执行\n");
    }
    
    printf("一切正常,可以操作\n");
    return 0;
}
模式 含义
F_OK 检查文件是否存在
R_OK 检查是否可读
W_OK 检查是否可写
X_OK 检查是否可执行

注意:access() 检查的是进程的实际用户ID实际组ID,而不是有效用户ID。如果你的程序是 setuid 程序,access() 的结果可能和实际操作时的权限不一致。这种情况我建议用 euidaccess() 或自己手动检查。

避坑指南:我踩过的那些坑

  • umask 的线程安全问题:我曾经在一个多线程程序里动态修改 umask,结果其他线程创建的文件权限全乱了。后来我改成在进程启动时统一设置,再也不动了。
  • chmod 的符号链接问题:chmod() 会直接修改目标文件的权限,不会影响符号链接本身。如果你想修改符号链接的权限,得用 lchmod()(但很多系统不支持)。
  • access() 的竞态条件:检查完权限和实际操作之间,权限可能被其他进程改了。所以 access() 只能作为辅助检查,不能完全依赖它来保证安全。

总结一下

这三个函数,说白了就是一套组合拳:

  • umask():控制新文件的默认权限,适合在程序初始化时设置。
  • chmod():修改已有文件的权限,适合在运行时动态调整。
  • access():提前检查权限,避免操作失败,适合做前置校验。

嗯,掌握了这些,你在 Linux 下操作文件权限应该就游刃有余了。记住,权限问题往往是嵌入式开发中最容易被忽略的坑,提前处理好,能省不少调试时间。

公众号:蓝海资料掘金营,微信deep3321