我们继续学习 Linux 系统编程中的重要函数。这次我们介绍 access 函数,它用于检查调用进程是否对指定的文件路径具有特定的访问权限(如读、写、执行)或检查文件是否存在。
1. 函数介绍 access 函数是一个 Linux 系统调用,用于根据调用进程的实际用户 ID (UID) 和组 ID (GID) 来检查对文件的权限。它回答了这样的问题:“我(当前运行这个程序的用户)能否读/写/执行这个文件?” 或者更简单地,“这个文件存在吗?”。
这在程序需要在尝试打开或执行文件之前,先确认是否具备相应权限时非常有用,可以避免因权限不足而导致后续操作(如 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> #include <stdio.h> #include <stdlib.h> void check_access (const char *pathname) { printf ("\n--- Checking access for '%s' ---\n" , pathname); 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" ); } } 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" ); } } 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" ); } } 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.ctouch test_filechmod 644 test_file chmod 755 test_script.sh echo '#!/bin/bash\necho "Hello from script"' > test_script.shchmod +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> #include <fcntl.h> #include <stdio.h> #include <stdlib.h> int safe_write_file (const char *pathname, const char *data) { int fd; if (access (pathname, F_OK) == 0 ) { printf ("File '%s' already exists.\n" , pathname); 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 ; } printf ("File exists and is writable.\n" ); } 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" ); } 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 ; } printf ("File '%s' opened successfully for writing.\n" , pathname); 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); if (close (fd) == -1 ) { perror ("close" ); return -1 ; } return 0 ; }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开源软件路线图/