access() 是一个标准 C 库函数(在 <unistd.h> 头文件中定义),用于检查一个进程对某个文件或目录的访问权限,它提供了一种简单的方式来验证你是否有权执行某个操作,而不会真正地打开文件,这在某些场景下非常有用。

(图片来源网络,侵删)
函数原型
#include <unistd.h> int access(const char *pathname, int mode);
参数说明
pathname
- 这是一个以 null 结尾的字符串,指定了你要检查的文件或目录的路径。
- 可以是相对路径(如
./myfile.txt)或绝对路径(如/home/user/myfile.txt)。
mode
- 这是一个整型值,指定了要检查的访问模式,它由以下宏定义组合而成(通过按位或 组合)。
- 你至少需要指定一个模式,通常使用
F_OK来检查文件是否存在,然后组合其他模式来检查权限。
| 宏 | 值 | 含义 |
|---|---|---|
F_OK |
0 | 检查文件是否存在,这是最基本的一种检查。 |
R_OK |
4 | 检查文件是否可读。 |
W_OK |
2 | 检查文件是否可写。 |
X_OK |
1 | 检查文件是否可执行。 |
组合示例:
R_OK | W_OK:检查文件是否同时可读和可写。F_OK | R_OK:检查文件是否存在并且可读。
返回值
- 成功时:返回
0。 - 失败时:返回
-1,并设置errno来指示具体的错误原因。
常见的 errno 值:
errno 宏值 |
含义 | 描述 |
|---|---|---|
EACCES |
权限不够 | pathname 所请求的访问权限被拒绝。 |
ENOENT |
文件或目录不存在 | pathname 指定的路径不存在。 |
EFAULT |
地址错误 | pathname 指向了无效的内存地址。 |
ELOOP |
符号链接过多 | 解析 pathname 时遇到了太多的符号链接。 |
ENAMETOOLONG |
文件名过长 | pathname 过长。 |
ENOMEM |
内存不足 | 内核内存不足,无法完成操作。 |
ENOTDIR |
不是目录 | pathname 的一个组成部分不是目录。 |
使用示例
下面通过几个代码示例来演示 access() 的用法。
示例 1:检查文件是否存在
#include <stdio.h>
#include <unistd.h> // for access()
#include <errno.h> // for errno
int main() {
const char *filename = "test.txt";
// 检查文件是否存在
if (access(filename, F_OK) == 0) {
printf("文件 '%s' 存在,\n", filename);
} else {
// 使用 perror 打印错误信息,它会自动输出 errno 对应的描述
perror("文件不存在或发生其他错误");
}
return 0;
}
示例 2:检查文件的读写权限
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
int main() {
const char *filename = "test.txt";
// 检查文件是否存在
if (access(filename, F_OK) != 0) {
perror("错误:文件不存在");
return 1;
}
// 检查文件是否可读
if (access(filename, R_OK) == 0) {
printf("文件 '%s' 可读,\n", filename);
} else {
printf("文件 '%s' 不可读,错误码: %d\n", filename, errno);
}
// 检查文件是否可写
if (access(filename, W_OK) == 0) {
printf("文件 '%s' 可写,\n", filename);
} else {
printf("文件 '%s' 不可写,错误码: %d\n", filename, errno);
}
return 0;
}
示例 3:检查程序是否有执行权限
这个例子检查当前目录下的 my_program 是否有执行权限。

(图片来源网络,侵删)
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h> // for exit()
int main() {
const char *program = "./my_program";
if (access(program, F_OK | X_OK) == 0) {
printf("程序 '%s' 存在并且有执行权限,\n", program);
// 在这里可以使用 system() 或 exec 族函数来运行它
// system(program);
} else {
perror("错误:程序不存在或没有执行权限");
exit(EXIT_FAILURE);
}
return 0;
}
重要注意事项和局限性
虽然 access() 很方便,但在某些情况下使用它可能会引入竞态条件。
竞态条件问题
access() 的一个典型使用场景是:检查文件是否存在并有写权限,然后打开它进行写入。
// 这是一个有问题的代码示例
if (access(filename, W_OK) == 0) {
// 此时检查通过,认为可以写入
FILE *fp = fopen(filename, "w");
if (fp == NULL) {
// 但这里可能打开失败!
perror("fopen failed");
} else {
// ... 写入文件 ...
fclose(fp);
}
}
问题出在哪里?
在 access() 和 fopen() 这两个系统调用之间,可能发生以下情况:
- 你的程序调用
access(filename, W_OK),返回0,表示你有写权限。 - 就在这之后,在
fopen()被调用之前,另一个进程或用户删除了这个文件,或者修改了它的权限,使得你不再有写入权限。 - 你的程序接着调用
fopen(filename, "w"),尝试打开一个你已经没有权限写入的文件(或者一个不存在的文件),导致失败。
这种“检查-使用”(Check-Then-Use)的模式是产生竞态条件的温床。

(图片来源网络,侵删)
如何避免竞态条件?
解决这个问题的方法是原子操作,原子操作是指一个操作一旦开始,就不会被中断,直到完成。
-
对于文件:使用
open()函数并配合O_CREAT和O_EXCL标志。open(filename, O_WRONLY | O_CREAT | O_EXCL, 0644);- 这个调用的含义是:“以只写方式打开文件
filename,如果文件不存在,则创建它(权限为 0644);如果文件已经存在,则立即失败,并返回EEXIST错误。” - 这个操作是原子的,它保证了在文件被创建的那一刻,它就是全新的,不会有其他进程同时写入。
-
对于目录:使用
mkdir()或open()配合O_DIRECTORY和O_EXCL等标志。
access() vs. euidaccess()
在 Linux 系统中,还有一个相关的函数叫 eaccess()(POSIX 标准)或 euidaccess()(GNU 扩展)。
#include <unistd.h> int euidaccess(const char *pathname, int mode); int eaccess(const char *pathname, int mode); // eaccess 是 euidaccess 的别名
区别:
access()检查的是实际用户 ID (Real User ID, RUID) 和实际组 ID (Real Group ID, RGID) 对应的权限,这通常是你登录时所用的身份。euidaccess()检查的是有效用户 ID (Effective User ID, EUID) 和**有效组 ID (Effective Group ID, EGID)对应的权限,这通常是程序运行时临时拥有的权限(setuid` 程序)。
什么时候用 euidaccess()?
当你编写一个设置了 setuid 或 setgid 位的程序时,你应该使用 euidaccess() 来检查权限,因为程序的权限(EUID)和运行它的人的权限(RUID)是不同的,对于普通程序,两者效果相同。
| 特性 | 描述 |
|---|---|
| 功能 | 检查文件/目录是否存在及权限(读、写、执行)。 |
| 头文件 | <unistd.h> |
| 优点 | 简单、轻量,无需打开文件即可检查权限。 |
| 最大缺点 | 存在竞态条件风险,不应用于“检查-使用”模式。 |
| 替代方案 | 对于需要“检查后立即操作”的场景(如创建文件),应使用 open() + O_CREAT + O_EXCL 等原子操作。 |
| 特殊函数 | euidaccess() 用于检查 setuid/setgid 程序的有效权限。 |
access() 是一个有用的工具,但你需要清楚地了解它的局限性,避免在需要高安全性的场景下使用它来引入竞态条件。
