access系统调用及示例

我们继续学习 Linux 系统编程中的重要函数。这次我们介绍 access 函数,它用于检查调用进程是否对指定的文件路径具有特定的访问权限(如读、写、执行)或检查文件是否存在。

1. 函数介绍

 access 函数是一个 Linux 系统调用,用于根据调用进程的实际用户 ID (UID) 和组 ID (GID) 来检查对文件的权限。它回答了这样的问题:“我(当前运行这个程序的用户)能否读/写/执行这个文件?” 或者更简单地,“这个文件存在吗?”。

data-ad-format="fluid" data-ad-layout-key="-7k+ex-4a-9w+4a">

这在程序需要在尝试打开或执行文件之前,先确认是否具备相应权限时非常有用,可以避免因权限不足而导致后续操作(如 open, execve)失败。

需要注意的是,access 检查的是调用 access 时的实际权限,即使程序后续通过 setuid 或 setgid 改变了有效用户 ID 或组 ID,access 仍然基于最初的 UID/GID 进行检查。

2. 函数原型

1
2
3
4
#include <unistd.h> // 必需

int access(const char *pathname, int mode);

3. 功能

  • 权限检查: 检查调用进程对由 pathname 指定的文件是否拥有 mode 参数指定的访问权限。

  • 存在性检查: 特别地,当 mode 设置为 F_OK 时,access 仅检查文件是否存在,而不关心具体的读/写/执行权限。

4. 参数

  • const char *pathname: 指向一个以空字符 (\0) 结尾的字符串,该字符串包含了要检查权限的文件或目录的路径名。这可以是相对路径或绝对路径。

int mode: 指定要检查的权限类型。这是一个位掩码,可以是以下值的按位或组合:

  • F_OK: 检查文件是否存在。

  • R_OK: 检查文件是否可读。

  • W_OK: 检查文件是否可写。

  • X_OK: 检查文件是否可执行。例如:

  • F_OK: 仅检查文件是否存在。

  • R_OK: 检查文件是否可读。

  • R_OK | W_OK: 检查文件是否可读且可写。

  • X_OK: 检查文件(或目录)是否可执行(对于目录,可执行意味着可以进入该目录)。

5. 返回值

  • 成功时 (具备指定权限或文件存在): 返回 0。

失败时 (不具备指定权限或文件不存在):

返回 -1,并设置全局变量 errno 来指示具体的错误原因:

  • EACCES: 请求的权限被拒绝。文件存在,但调用进程没有指定的权限。

  • ENOENT: 文件不存在(或路径名指向的目录不存在)。

  • ELOOP: 解析 pathname 时遇到符号链接环。

  • 其他错误…

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

  • stat, lstat, fstat: 这些函数可以获取文件的详细状态信息,包括权限位 (st_mode)。程序可以手动检查这些权限位来判断权限,但这需要自己实现权限检查逻辑(考虑用户、组、其他用户的权限位以及 UID/GID)。access 提供了更直接、符合系统安全策略的检查方式。

  • open, execve 等: 这些函数在执行时也会进行权限检查。使用 access 可以提前检查,但需要注意“检查与使用之间存在竞争条件 (TOCTOU)”的问题(见下方注意事项)。

  • euidaccess / eaccess: 这些是 GNU 扩展函数,它们根据有效用户 ID (EUID) 和有效组 ID (EGID) 进行检查,而不是实际用户 ID。在 setuid/setgid 程序中可能更有意义。

7. 示例代码

示例 1:基本的文件存在性和权限检查

这个例子演示了如何使用 access 检查文件是否存在、是否可读、是否可写、是否可执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
#include <unistd.h>  // access
#include <stdio.h> // perror, printf
#include <stdlib.h> // exit

void check_access(const char *pathname) {
printf("\n--- Checking access for '%s' ---\n", pathname);

// 1. 检查文件是否存在
if (access(pathname, F_OK) == 0) {
printf(" File exists.\n");
} else {
if (errno == ENOENT) {
printf(" File does NOT exist.\n");
} else {
perror(" access F_OK failed for other reason");
}
// 如果文件不存在,后续检查无意义,但为了演示,我们仍进行
// (实际上,通常会在这里 return)
}

// 2. 检查是否可读
if (access(pathname, R_OK) == 0) {
printf(" File is readable.\n");
} else {
if (errno == EACCES) {
printf(" File exists but is NOT readable.\n");
} else if (errno == ENOENT) {
printf(" File does not exist (so not readable).\n");
} else {
perror(" access R_OK failed for other reason");
}
}

// 3. 检查是否可写
if (access(pathname, W_OK) == 0) {
printf(" File is writable.\n");
} else {
if (errno == EACCES) {
printf(" File exists but is NOT writable.\n");
} else if (errno == ENOENT) {
printf(" File does not exist (so not writable).\n");
} else {
perror(" access W_OK failed for other reason");
}
}

// 4. 检查是否可执行
if (access(pathname, X_OK) == 0) {
printf(" File is executable.\n");
} else {
if (errno == EACCES) {
printf(" File exists but is NOT executable.\n");
} else if (errno == ENOENT) {
printf(" File does not exist (so not executable).\n");
} else {
perror(" access X_OK failed for other reason");
}
}
}

int main(int argc, char *argv&#91;]) {
if (argc < 2) {
fprintf(stderr, "Usage: %s <file1> &#91;file2] ...\n", argv&#91;0]);
exit(EXIT_FAILURE);
}

// 对每个命令行参数进行检查
for (int i = 1; i < argc; i++) {
check_access(argv&#91;i]);
}

return 0;
}

代码解释:

定义了一个 check_access 函数,它接受一个文件路径作为参数。

在 check_access 函数内部:

  • 首先调用 access(pathname, F_OK) 检查文件是否存在。

  • 然后分别调用 access(pathname, R_OK), access(pathname, W_OK), access(pathname, X_OK) 检查读、写、执行权限。

  • 每次调用后都检查返回值。如果返回 0,表示检查通过;如果返回 -1,则检查 errno 来区分是“文件不存在”还是“权限不足”等其他原因。

main 函数遍历所有命令行参数,并对每个参数调用 check_access。

编译和运行:

1
2
3
4
5
6
7
8
9
gcc -o check_access check_access.c
touch test_file
chmod 644 test_file # rw-r--r--
chmod 755 test_script.sh # 创建一个可执行脚本用于测试
echo '#!/bin/bash\necho "Hello from script"' > test_script.sh
chmod +x test_script.sh

./check_access test_file test_script.sh /etc/passwd /nonexistent_file

示例 2:在打开文件前进行检查

这个例子展示了如何在尝试打开文件进行写入之前,先使用 access 检查文件是否存在以及是否可写,以提供更友好的错误信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
#include <unistd.h>  // access
#include <fcntl.h> // open, O_WRONLY, O_CREAT, O_EXCL
#include <stdio.h> // perror, printf
#include <stdlib.h> // exit

int safe_write_file(const char *pathname, const char *data) {
int fd;

// 1. 检查文件是否存在
if (access(pathname, F_OK) == 0) {
printf("File '%s' already exists.\n", pathname);

// 2. 如果存在,检查是否可写
if (access(pathname, W_OK) != 0) {
if (errno == EACCES) {
fprintf(stderr, "Error: Permission denied. Cannot write to '%s'.\n", pathname);
} else {
perror("Error checking write permission");
}
return -1; // Failure
}
printf("File exists and is writable.\n");
// 注意:即使可写,open 时仍可能因为其他原因失败(如磁盘满)

} else {
// 文件不存在,检查目录是否可写 (间接判断能否创建文件)
// 这里简化处理,实际可能需要解析路径
printf("File '%s' does not exist. Checking if we can create it...\n", pathname);
// 一个简单的检查:检查当前目录是否可写
if (access(".", W_OK) != 0) {
if (errno == EACCES) {
fprintf(stderr, "Error: Permission denied. Cannot create file in current directory.\n");
} else {
perror("Error checking current directory write permission");
}
return -1;
}
printf("Current directory is writable. Proceeding to create file.\n");
}

// 3. 尝试打开文件进行写入
// 使用 O_CREAT | O_EXCL 确保仅在文件不存在时创建,防止覆盖
// 如果前面检查了存在性,这里可能用 O_WRONLY | O_TRUNC 更合适
// 这里演示结合检查的逻辑
if (access(pathname, F_OK) == 0) {
// 文件存在,以只写和截断模式打开
fd = open(pathname, O_WRONLY | O_TRUNC);
} else {
// 文件不存在,创建它
fd = open(pathname, O_WRONLY | O_CREAT | O_EXCL, 0644);
}

if (fd == -1) {
perror("open");
return -1; // Failure
}

printf("File '%s' opened successfully for writing.\n", pathname);

// 4. 写入数据 (简化)
ssize_t data_len = 0;
const char *p = data;
while (*p++) data_len++;

if (write(fd, data, data_len) != data_len) {
perror("write");
close(fd);
return -1;
}

printf("Successfully wrote data to '%s'.\n", pathname);

// 5. 关闭文件
if (close(fd) == -1) {
perror("close");
return -1;
}

return 0; // Success
}

int main() {
const char *filename = "output_from_safe_write.txt";
const char *content = "This is data written by the safe_write_file function.\n";

if (safe_write_file(filename, content) == 0) {
printf("Operation completed successfully.\n");
} else {
printf("Operation failed.\n");
exit(EXIT_FAILURE);
}

return 0;
}

代码解释:

定义了一个 safe_write_file 函数,它接受文件名和要写入的数据。

首先使用 access(pathname, F_OK) 检查文件是否存在。

如果文件存在,再使用 access(pathname, W_OK) 检查是否可写。

如果文件不存在,则检查当前工作目录(.)是否可写,以此判断是否有权限创建新文件(这是一个简化的检查)。

根据检查结果,决定是以 O_WRONLY | O_TRUNC(覆盖)还是 O_WRONLY | O_CREAT | O_EXCL(新建)模式打开文件。

打开文件后,执行写入操作。

最后关闭文件。

通过这种方式,可以在实际执行可能导致失败的操作(open, write)之前,提供更具体、更早的错误反馈。

重要注意事项:TOCTOU 竞争条件

使用 access 时需要特别注意一个潜在的安全问题:TOCTOU (Time-of-Check to Time-of-Use) 竞争条件。

  • 问题: access 检查权限和后续使用文件(如 open, execve)之间存在时间差。在这段时间内,文件的权限或存在性可能被其他进程改变。

  • 例子: 一个程序用 access(“myfile”, W_OK) 检查 myfile 是否可写,返回 0(表示可写)。但在程序调用 open(“myfile”, O_WRONLY) 之前,另一个有权限的进程删除了 myfile 并创建了一个指向敏感文件(如 /etc/passwd)的符号链接,并命名为 myfile。此时,程序的 open 调用将会打开并可能修改 /etc/passwd,这显然不是预期行为。

缓解方法:

  • 尽量避免使用 access: 最好的方法是直接尝试执行操作(如 open, execve),并根据其返回的错误码来处理权限或存在性问题。内核会在 open/execve 时进行原子性的权限检查。

  • 如果必须使用 access: 要意识到这种风险,并确保在权限检查和文件使用之间的时间窗口尽可能短。在高安全性要求的程序中,应避免依赖 access 的结果来做关键决策。

总结:

access 函数提供了一种方便的方式来检查文件权限和存在性。虽然它有其用途,但在涉及安全性的场景中,直接尝试操作并处理错误通常是更安全、更可靠的做法。理解其工作原理和潜在的 TOCTOU 问题是正确使用它的关键。

https://www.calcguide.tech/2025/08/03/access系统调用及示例/

https://www.calcguide.tech/2025/08/26/linux开源软件路线图/

data-ad-format="auto" data-full-width-responsive="true">