access系统调用及示例
我们继续学习 Linux 系统编程中的重要函数。这次我们介绍 access
函数,它用于检查调用进程是否对指定的文件路径具有特定的访问权限(如读、写、执行)或检查文件是否存在。
1. 函数介绍
access
是一个 Linux 系统调用,用于根据调用进程的实际用户 ID (UID) 和组 ID (GID) 来检查对文件的权限。它回答了这样的问题:“我(当前运行这个程序的用户)能否读/写/执行这个文件?” 或者更简单地,“这个文件存在吗?”。
这在程序需要在尝试打开或执行文件之前,先确认是否具备相应权限时非常有用,可以避免因权限不足而导致后续操作(如 open
, execve
)失败。
需要注意的是,access
检查的是调用 access
时的实际权限,即使程序后续通过 setuid
或 setgid
改变了有效用户 ID 或组 ID,access
仍然基于最初的 UID/GID 进行检查。
2. 函数原型
#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
时遇到符号链接环。- 其他错误…
- 返回 -1,并设置全局变量
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
检查文件是否存在、是否可读、是否可写、是否可执行。
#include <unistd.h> // access
#include <stdio.h> // perror, printf
#include <stdlib.h> // exitvoid 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
。
编译和运行:
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
检查文件是否存在以及是否可写,以提供更友好的错误信息。
#include <unistd.h> // access
#include <fcntl.h> // open, O_WRONLY, O_CREAT, O_EXCL
#include <stdio.h> // perror, printf
#include <stdlib.h> // exitint 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;
}
代码解释:
1. 定义了一个 safe_write_file
函数,它接受文件名和要写入的数据。
2. 首先使用 access(pathname, F_OK)
检查文件是否存在。
3. 如果文件存在,再使用 access(pathname, W_OK)
检查是否可写。
4. 如果文件不存在,则检查当前工作目录(.
)是否可写,以此判断是否有权限创建新文件(这是一个简化的检查)。
5. 根据检查结果,决定是以 O_WRONLY | O_TRUNC
(覆盖)还是 O_WRONLY | O_CREAT | O_EXCL
(新建)模式打开文件。
6. 打开文件后,执行写入操作。
7. 最后关闭文件。
8. 通过这种方式,可以在实际执行可能导致失败的操作(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 问题是正确使用它的关键。