我们继续学习 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. 功能
4. 参数
const char *pathname: 指向一个以空字符 (\0) 结尾的字符串,该字符串包含了要检查权限的文件或目录的路径名。这可以是相对路径或绝对路径。
int mode: 指定要检查的权限类型。这是一个位掩码,可以是以下值的按位或组合:
5. 返回值
失败时 (不具备指定权限或文件不存在):
返回 -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[]) { if (argc < 2) { fprintf(stderr, "Usage: %s <file1> [file2] ...\n", argv[0]); exit(EXIT_FAILURE); } // 对每个命令行参数进行检查 for (int i = 1; i < argc; i++) { check_access(argv[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开源软件路线图/