1. Linux文件IO操作的核心概念
在Linux系统中,文件操作是系统编程的基础,也是理解操作系统工作原理的重要切入点。当我们谈论文件IO时,实际上是在讨论应用程序如何与存储设备交互的机制。这种交互可以通过两种主要方式实现:系统调用(System Calls)和库函数(Library Functions)。
系统调用是操作系统内核提供给用户空间的接口,它们是用户程序与内核通信的唯一途径。在Linux中,所有文件相关的系统调用都以"sys_"为前缀,例如sys_open、sys_read等。不过在实际编程中,我们使用的是它们的封装形式,如open()、read()等。
库函数则是对系统调用的进一步封装,提供了更高级的抽象和额外功能。例如,标准C库中的fopen()、fread()等函数最终都会调用底层的系统调用,但它们添加了缓冲机制等特性,使得文件操作更加高效和方便。
注意:理解系统调用和库函数的区别至关重要。系统调用直接与内核交互,每次调用都会导致用户态和内核态的切换,开销较大;而库函数可能在用户空间处理许多操作,减少实际系统调用的次数。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 文件操作的系统调用接口详解
2.1 文件打开与创建:open()
open()是最基础的文件操作系统调用,用于打开或创建文件。它的函数原型如下:
c复制#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
flags参数决定了文件的打开方式,常用的选项包括:
- O_RDONLY:只读打开
- O_WRONLY:只写打开
- O_RDWR:读写打开
- O_CREAT:文件不存在时创建
- O_TRUNC:文件存在且为普通文件时,将其长度截断为0
- O_APPEND:追加写入
当使用O_CREAT标志时,需要提供mode参数指定文件权限。权限通常用八进制表示,如0644表示所有者可读写,组用户和其他用户只读。
c复制int fd = open("test.txt", O_RDWR | O_CREAT | O_TRUNC, 0644);
if (fd == -1) {
perror("open failed");
exit(EXIT_FAILURE);
}
2.2 文件读写:read()和write()
read()和write()系统调用用于文件的读写操作:
c复制#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);
read()从文件描述符fd指向的文件中读取最多count字节到buf缓冲区,返回实际读取的字节数。write()则将buf中的count字节写入文件描述符fd指向的文件。
c复制char buffer[1024];
ssize_t bytes_read = read(fd, buffer, sizeof(buffer));
if (bytes_read == -1) {
perror("read failed");
close(fd);
exit(EXIT_FAILURE);
}
const char *message = "Hello, Linux IO!";
ssize_t bytes_written = write(fd, message, strlen(message));
if (bytes_written == -1) {
perror("write failed");
close(fd);
exit(EXIT_FAILURE);
}
2.3 文件定位:lseek()
lseek()用于改变文件偏移量(读写位置):
c复制#include <sys/types.h>
#include <unistd.h>
off_t lseek(int fd, off_t offset, int whence);
whence参数指定偏移量的基准位置:
- SEEK_SET:从文件开始处计算
- SEEK_CUR:从当前位置计算
- SEEK_END:从文件末尾计算
c复制// 将文件指针移动到文件开头后10字节处
off_t new_offset = lseek(fd, 10, SEEK_SET);
if (new_offset == -1) {
perror("lseek failed");
close(fd);
exit(EXIT_FAILURE);
}
2.4 文件关闭:close()
close()系统调用关闭文件描述符:
c复制#include <unistd.h>
int close(int fd);
关闭文件描述符后,该描述符不再引用任何文件,可以被后续的open()或其它创建文件描述符的操作重用。
c复制if (close(fd) == -1) {
perror("close failed");
exit(EXIT_FAILURE);
}
3. 标准库的文件操作函数
3.1 文件打开与关闭:fopen()和fclose()
标准C库提供了更高级的文件操作函数,fopen()用于打开文件:
c复制#include <stdio.h>
FILE *fopen(const char *pathname, const char *mode);
mode字符串指定打开方式:
- "r":只读
- "w":只写(截断或创建)
- "a":追加
- "r+":读写
- "w+":读写(截断或创建)
- "a+":读写(追加)
c复制FILE *file = fopen("test.txt", "w+");
if (file == NULL) {
perror("fopen failed");
exit(EXIT_FAILURE);
}
if (fclose(file) == EOF) {
perror("fclose failed");
exit(EXIT_FAILURE);
}
3.2 文件读写:fread()和fwrite()
fread()和fwrite()提供了缓冲的二进制IO:
c复制size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
这两个函数以记录为单位进行读写,size是每个记录的大小,nmemb是要读写的记录数。
c复制double data[5] = {1.1, 2.2, 3.3, 4.4, 5.5};
size_t written = fwrite(data, sizeof(double), 5, file);
if (written != 5) {
perror("fwrite failed");
fclose(file);
exit(EXIT_FAILURE);
}
rewind(file); // 回到文件开头
double read_data[5];
size_t read = fread(read_data, sizeof(double), 5, file);
if (read != 5) {
if (feof(file)) {
printf("End of file reached\n");
} else if (ferror(file)) {
perror("fread failed");
}
fclose(file);
exit(EXIT_FAILURE);
}
3.3 格式化IO:fprintf()和fscanf()
对于文本文件,可以使用格式化的IO函数:
c复制int fprintf(FILE *stream, const char *format, ...);
int fscanf(FILE *stream, const char *format, ...);
c复制fprintf(file, "Name: %s, Age: %d\n", "Alice", 25);
rewind(file);
char name[20];
int age;
fscanf(file, "Name: %s, Age: %d\n", name, &age);
printf("Read: Name=%s, Age=%d\n", name, age);
4. 高级文件操作技术
4.1 文件描述符与文件流的关系
在Linux中,标准库的FILE结构(文件流)底层实际上使用了文件描述符。可以通过fileno()和fdopen()在两者之间转换:
c复制int fileno(FILE *stream);
FILE *fdopen(int fd, const char *mode);
c复制FILE *stream = fdopen(fd, "r+");
if (stream == NULL) {
perror("fdopen failed");
close(fd);
exit(EXIT_FAILURE);
}
int new_fd = fileno(stream);
printf("Underlying file descriptor: %d\n", new_fd);
4.2 文件锁定:fcntl()
fcntl()提供了对文件描述符的各种控制操作,包括文件锁定:
c复制#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );
c复制struct flock fl;
fl.l_type = F_WRLCK; // 写锁
fl.l_whence = SEEK_SET;
fl.l_start = 0; // 锁定区域起始
fl.l_len = 0; // 0表示到文件末尾
fl.l_pid = getpid(); // 当前进程
if (fcntl(fd, F_SETLK, &fl) == -1) {
perror("fcntl F_SETLK failed");
close(fd);
exit(EXIT_FAILURE);
}
// 解锁
fl.l_type = F_UNLCK;
if (fcntl(fd, F_SETLK, &fl) == -1) {
perror("fcntl F_UNLCK failed");
close(fd);
exit(EXIT_FAILURE);
}
4.3 内存映射文件:mmap()
mmap()将文件直接映射到进程的地址空间,可以像访问内存一样访问文件:
c复制#include <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags,
int fd, off_t offset);
int munmap(void *addr, size_t length);
c复制struct stat sb;
if (fstat(fd, &sb) == -1) {
perror("fstat failed");
close(fd);
exit(EXIT_FAILURE);
}
char *mapped = mmap(NULL, sb.st_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (mapped == MAP_FAILED) {
perror("mmap failed");
close(fd);
exit(EXIT_FAILURE);
}
// 现在可以像操作内存一样操作文件内容
strcpy(mapped, "This is mmap example");
if (munmap(mapped, sb.st_size) == -1) {
perror("munmap failed");
close(fd);
exit(EXIT_FAILURE);
}
5. 性能考量与最佳实践
5.1 缓冲策略的选择
标准库函数默认使用缓冲机制,这可以显著提高IO性能。缓冲模式可以通过setvbuf()设置:
c复制int setvbuf(FILE *stream, char *buf, int mode, size_t size);
mode参数可以是:
- _IONBF:无缓冲
- _IOLBF:行缓冲
- _IOFBF:全缓冲
c复制char buffer[BUFSIZ];
setvbuf(file, buffer, _IOFBF, BUFSIZ);
对于系统调用,如果需要缓冲,需要手动实现或使用更高级的库。
5.2 直接IO与同步IO
Linux提供了绕过页面缓存的直接IO(O_DIRECT)和确保数据写入存储设备的同步IO(O_SYNC):
c复制int fd = open("data.bin", O_RDWR | O_CREAT | O_DIRECT, 0644);
// 或者
int fd = open("data.bin", O_RDWR | O_CREAT | O_SYNC, 0644);
注意:O_DIRECT有严格的对齐要求,缓冲区地址和大小必须是文件系统块大小的整数倍。
5.3 错误处理的最佳实践
文件操作中良好的错误处理至关重要:
- 总是检查系统调用和库函数的返回值
- 使用perror()或strerror(errno)输出有意义的错误信息
- 在错误情况下清理已分配的资源
- 考虑使用errno来判断具体的错误类型
c复制int fd = open("nonexistent.txt", O_RDONLY);
if (fd == -1) {
if (errno == ENOENT) {
printf("File does not exist\n");
} else if (errno == EACCES) {
printf("Permission denied\n");
} else {
perror("open failed");
}
exit(EXIT_FAILURE);
}
6. 实际应用示例
6.1 文件复制程序
下面是一个使用系统调用实现的简单文件复制程序:
c复制#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>
#define BUFFER_SIZE 4096
int main(int argc, char *argv[]) {
if (argc != 3) {
fprintf(stderr, "Usage: %s <source> <destination>\n", argv[0]);
exit(EXIT_FAILURE);
}
int src_fd = open(argv[1], O_RDONLY);
if (src_fd == -1) {
perror("open source failed");
exit(EXIT_FAILURE);
}
struct stat src_stat;
if (fstat(src_fd, &src_stat) == -1) {
perror("fstat failed");
close(src_fd);
exit(EXIT_FAILURE);
}
int dst_fd = open(argv[2], O_WRONLY | O_CREAT | O_TRUNC, src_stat.st_mode & 0777);
if (dst_fd == -1) {
perror("open destination failed");
close(src_fd);
exit(EXIT_FAILURE);
}
char buffer[BUFFER_SIZE];
ssize_t bytes_read;
while ((bytes_read = read(src_fd, buffer, BUFFER_SIZE)) > 0) {
ssize_t bytes_written = write(dst_fd, buffer, bytes_read);
if (bytes_written != bytes_read) {
perror("write failed");
close(src_fd);
close(dst_fd);
exit(EXIT_FAILURE);
}
}
if (bytes_read == -1) {
perror("read failed");
close(src_fd);
close(dst_fd);
exit(EXIT_FAILURE);
}
close(src_fd);
close(dst_fd);
return EXIT_SUCCESS;
}
6.2 使用标准库的日志记录器
下面是一个使用标准库函数实现的简单日志记录器:
c复制#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <stdarg.h>
void log_message(const char *format, ...) {
static FILE *log_file = NULL;
if (log_file == NULL) {
log_file = fopen("application.log", "a");
if (log_file == NULL) {
perror("Failed to open log file");
exit(EXIT_FAILURE);
}
setvbuf(log_file, NULL, _IOLBF, 0); // 行缓冲
}
time_t now = time(NULL);
struct tm *tm_info = localtime(&now);
char time_str[20];
strftime(time_str, sizeof(time_str), "%Y-%m-%d %H:%M:%S", tm_info);
fprintf(log_file, "[%s] ", time_str);
va_list args;
va_start(args, format);
vfprintf(log_file, format, args);
va_end(args);
fprintf(log_file, "\n");
}
int main() {
log_message("Application started");
log_message("Processing item %d with value %.2f", 42, 3.14159);
log_message("Application exiting");
return EXIT_SUCCESS;
}
7. 常见问题与解决方案
7.1 文件描述符耗尽问题
每个进程都有文件描述符的限制,可以通过ulimit -n查看。如果程序打开太多文件而不关闭,可能会遇到"Too many open files"错误。
解决方案:
- 总是检查并关闭不再需要的文件描述符
- 使用getrlimit()和setrlimit()管理文件描述符限制
- 考虑使用文件描述符池或重用机制
c复制#include <sys/resource.h>
struct rlimit rlim;
if (getrlimit(RLIMIT_NOFILE, &rlim) == 0) {
printf("Current file descriptor limit: %lld\n", (long long)rlim.rlim_cur);
rlim.rlim_cur = rlim.rlim_max; // 尝试提高到硬限制
setrlimit(RLIMIT_NOFILE, &rlim);
}
7.2 原子文件操作
在多进程环境中,文件操作需要考虑原子性。例如,使用O_EXCL标志可以确保文件创建的原子性:
c复制int fd = open("lockfile", O_RDWR | O_CREAT | O_EXCL, 0644);
if (fd == -1 && errno == EEXIST) {
// 文件已存在
} else if (fd == -1) {
perror("open failed");
} else {
// 成功创建并锁定文件
}
7.3 处理大文件
对于大于2GB的文件,需要使用大文件支持:
- 编译时定义_FILE_OFFSET_BITS=64
- 使用off_t类型(通常是64位)进行文件定位
- 使用特定的64位版本函数,如lseek64()
c复制#define _FILE_OFFSET_BITS 64
#include <sys/types.h>
off_t offset = lseek(fd, (off_t)4 * 1024 * 1024 * 1024, SEEK_SET);
7.4 跨平台兼容性问题
不同Unix系统在文件IO细节上可能有差异:
- 文件权限位可能不同(如S_IRUSR vs. 0400)
- 某些标志可能不可用(如O_NOATIME)
- 错误代码可能有差异
解决方案:
- 使用标准POSIX定义
- 使用功能测试宏(如_POSIX_C_SOURCE)
- 必要时添加条件编译
c复制#if defined(__APPLE__) || defined(__FreeBSD__)
// BSD特定的代码
#elif defined(__linux__)
// Linux特定的代码
#endif
8. 性能测试与比较
8.1 系统调用 vs 标准库性能
下面是一个简单的性能测试程序,比较系统调用和标准库函数的性能差异:
c复制#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <time.h>
#include <string.h>
#define TEST_SIZE (1024 * 1024 * 100) // 100MB
#define BUFFER_SIZE 4096
void test_syscall(const char *filename) {
char buffer[BUFFER_SIZE];
memset(buffer, 'A', BUFFER_SIZE);
int fd = open(filename, O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd == -1) {
perror("open failed");
exit(EXIT_FAILURE);
}
clock_t start = clock();
size_t total = 0;
while (total < TEST_SIZE) {
ssize_t written = write(fd, buffer, BUFFER_SIZE);
if (written == -1) {
perror("write failed");
close(fd);
exit(EXIT_FAILURE);
}
total += written;
}
close(fd);
clock_t end = clock();
printf("System calls: %.2f seconds\n", (double)(end - start) / CLOCKS_PER_SEC);
}
void test_stdio(const char *filename) {
char buffer[BUFFER_SIZE];
memset(buffer, 'A', BUFFER_SIZE);
FILE *file = fopen(filename, "wb");
if (file == NULL) {
perror("fopen failed");
exit(EXIT_FAILURE);
}
clock_t start = clock();
size_t total = 0;
while (total < TEST_SIZE) {
size_t written = fwrite(buffer, 1, BUFFER_SIZE, file);
if (written != BUFFER_SIZE) {
perror("fwrite failed");
fclose(file);
exit(EXIT_FAILURE);
}
total += written;
}
fclose(file);
clock_t end = clock();
printf("Standard I/O: %.2f seconds\n", (double)(end - start) / CLOCKS_PER_SEC);
}
int main() {
printf("Testing with buffer size: %d\n", BUFFER_SIZE);
test_syscall("syscall_test.bin");
test_stdio("stdio_test.bin");
return EXIT_SUCCESS;
}
典型输出可能如下:
code复制Testing with buffer size: 4096
System calls: 0.35 seconds
Standard I/O: 0.12 seconds
8.2 不同缓冲策略的影响
测试不同缓冲策略对性能的影响:
c复制void test_buffering(const char *filename, int mode, const char *mode_name) {
char buffer[BUFFER_SIZE];
memset(buffer, 'A', BUFFER_SIZE);
FILE *file = fopen(filename, "wb");
if (file == NULL) {
perror("fopen failed");
exit(EXIT_FAILURE);
}
setvbuf(file, NULL, mode, BUFSIZ);
clock_t start = clock();
size_t total = 0;
while (total < TEST_SIZE) {
size_t written = fwrite(buffer, 1, BUFFER_SIZE, file);
if (written != BUFFER_SIZE) {
perror("fwrite failed");
fclose(file);
exit(EXIT_FAILURE);
}
total += written;
}
fclose(file);
clock_t end = clock();
printf("%s buffering: %.2f seconds\n", mode_name, (double)(end - start) / CLOCKS_PER_SEC);
}
int main() {
test_buffering("unbuffered.bin", _IONBF, "Unbuffered");
test_buffering("line_buffered.bin", _IOLBF, "Line buffered");
test_buffering("fully_buffered.bin", _IOFBF, "Fully buffered");
return EXIT_SUCCESS;
}
典型结果:
code复制Unbuffered buffering: 0.38 seconds
Line buffered buffering: 0.14 seconds
Fully buffered buffering: 0.12 seconds
8.3 内存映射IO的性能优势
比较传统IO和内存映射IO的性能:
c复制#include <sys/mman.h>
void test_mmap(const char *filename) {
int fd = open(filename, O_RDWR | O_CREAT | O_TRUNC, 0644);
if (fd == -1) {
perror("open failed");
exit(EXIT_FAILURE);
}
// 扩展文件大小
if (ftruncate(fd, TEST_SIZE) == -1) {
perror("ftruncate failed");
close(fd);
exit(EXIT_FAILURE);
}
char *mapped = mmap(NULL, TEST_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (mapped == MAP_FAILED) {
perror("mmap failed");
close(fd);
exit(EXIT_FAILURE);
}
clock_t start = clock();
memset(mapped, 'A', TEST_SIZE);
if (msync(mapped, TEST_SIZE, MS_SYNC) == -1) {
perror("msync failed");
}
clock_t end = clock();
munmap(mapped, TEST_SIZE);
close(fd);
printf("Memory-mapped I/O: %.2f seconds\n", (double)(end - start) / CLOCKS_PER_SEC);
}
int main() {
test_syscall("syscall_test.bin");
test_mmap("mmap_test.bin");
return EXIT_SUCCESS;
}
典型结果:
code复制System calls: 0.35 seconds
Memory-mapped I/O: 0.08 seconds
9. 深入理解文件描述符
9.1 文件描述符的本质
在Linux中,文件描述符是一个非负整数,它实际上是进程文件描述符表的索引。这个表的每个条目包含:
- 文件描述符标志(如close-on-exec)
- 指向内核文件表的指针
内核文件表包含:
- 文件状态标志(读、写、追加等)
- 当前文件偏移量
- 指向v-node表的指针
v-node表包含:
- 文件类型和操作函数指针
- 文件属性(如inode)
- 文件系统特定信息
这种分层设计允许多个进程共享同一个文件描述,同时保持各自的文件偏移量和状态标志。
9.2 文件描述符的继承
文件描述符可以通过fork()和exec()继承:
- fork()后,子进程获得父进程文件描述符表的副本
- exec()默认保留所有打开的文件描述符,除非设置了FD_CLOEXEC标志
c复制// 设置close-on-exec标志
int flags = fcntl(fd, F_GETFD);
flags |= FD_CLOEXEC;
fcntl(fd, F_SETFD, flags);
9.3 文件描述符与标准流的关系
每个进程默认打开三个标准文件描述符:
- 0:标准输入(STDIN_FILENO)
- 1:标准输出(STDOUT_FILENO)
- 2:标准错误(STDERR_FILENO)
这些对应于标准库的FILE指针:
- stdin
- stdout
- stderr
可以通过dup2()重定向标准流:
c复制int fd = open("output.log", O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd == -1) {
perror("open failed");
exit(EXIT_FAILURE);
}
// 将标准输出重定向到文件
if (dup2(fd, STDOUT_FILENO) == -1) {
perror("dup2 failed");
close(fd);
exit(EXIT_FAILURE);
}
// 现在printf会输出到文件
printf("This will be written to output.log\n");
10. 文件IO的进阶话题
10.1 异步IO(AIO)
Linux提供了异步IO接口,允许程序发起IO操作后继续执行,而不必等待操作完成:
c复制#include <aio.h>
struct aiocb {
int aio_fildes; // 文件描述符
off_t aio_offset; // 文件偏移
volatile void *aio_buf; // 缓冲区
size_t aio_nbytes; // 传输字节数
int aio_reqprio; // 请求优先级
struct sigevent aio_sigevent; // 通知方式
int aio_lio_opcode; // 操作类型
};
int aio_read(struct aiocb *aiocbp);
int aio_write(struct aiocb *aiocbp);
int aio_error(const struct aiocb *aiocbp);
int aio_return(struct aiocb *aiocbp);
示例:
c复制struct aiocb cb = {0};
char buffer[4096];
cb.aio_fildes = fd;
cb.aio_buf = buffer;
cb.aio_nbytes = sizeof(buffer);
cb.aio_offset = 0;
if (aio_read(&cb) == -1) {
perror("aio_read failed");
close(fd);
exit(EXIT_FAILURE);
}
// 可以做其他工作...
while (aio_error(&cb) == EINPROGRESS) {
// 等待操作完成
}
ssize_t bytes_read = aio_return(&cb);
printf("Read %zd bytes asynchronously\n", bytes_read);
10.2 文件事件通知:inotify
inotify允许应用程序监控文件系统事件:
c复制#include <sys/inotify.h>
int inotify_init(void);
int inotify_add_watch(int fd, const char *pathname, uint32_t mask);
常见事件掩码:
- IN_ACCESS:文件被访问
- IN_MODIFY:文件被修改
- IN_CREATE:文件/目录被创建
- IN_DELETE:文件/目录被删除
示例:
c复制int inotify_fd = inotify_init();
if (inotify_fd == -1) {
perror("inotify_init failed");
exit(EXIT_FAILURE);
}
int watch_fd = inotify_add_watch(inotify_fd, "/tmp", IN_CREATE | IN_DELETE);
if (watch_fd == -1) {
perror("inotify_add_watch failed");
close(inotify_fd);
exit(EXIT_FAILURE);
}
char buffer[4096] __attribute__ ((aligned(__alignof__(struct inotify_event))));
const struct inotify_event *event;
while (1) {
ssize_t len = read(inotify_fd, buffer, sizeof(buffer));
if (len == -1 && errno != EAGAIN) {
perror("read failed");
break;
}
for (char *ptr = buffer; ptr < buffer + len; ptr += sizeof(struct inotify_event) + event->len) {
event = (const struct inotify_event *) ptr;
if (event->mask & IN_CREATE) {
printf("File created: %s\n", event->name);
} else if (event->mask & IN_DELETE) {
printf("File deleted: %s\n", event->name);
}
}
}
close(inotify_fd);
10.3 文件系统监控:fanotify
fanotify比inotify更强大,可以监控整个文件系统并控制访问:
c复制#include <fcntl.h>
#include <sys/fanotify.h>
int fanotify_init(unsigned int flags, unsigned int event_f_flags);
int fanotify_mark(int fanotify_fd, unsigned int flags,
uint64_t mask, int dirfd, const char *pathname);
示例:
c复制int fan_fd = fanotify_init(FAN_CLASS_NOTIF, O_RDONLY);
if (fan_fd == -1) {
perror("fanotify_init failed");
exit(EXIT_FAILURE);
}
if (fanotify_mark(fan_fd, FAN_MARK_ADD | FAN_MARK_MOUNT,
FAN_OPEN | FAN_CLOSE_WRITE, AT_FDCWD, "/") == -1) {
perror("fanotify_mark failed");
close(fan_fd);
exit(EXIT_FAILURE);
}
char buffer[4096];
struct fanotify_event_metadata *metadata;
while (1) {
ssize_t len = read(fan_fd, buffer, sizeof(buffer));
if (len == -1) {
perror("read failed");
break;
}
for (metadata = (struct fanotify_event_metadata *)buffer;
FAN_EVENT_OK(metadata, len);
metadata = FAN_EVENT_NEXT(metadata, len)) {
if (metadata->mask & FAN_OPEN) {
printf("File opened: %s\n", (char *)(metadata + 1));
}
if (metadata->mask & FAN_CLOSE_WRITE) {
printf("File closed after writing: %s\n", (char *)(metadata + 1));
}
close(metadata->fd);
}
}
close(fan_fd);
11. 安全考量
11.1 文件权限与所有权
正确处理文件权限和所有权至关重要:
c复制#include <sys/stat.h>
mode_t umask(mode_t mask);
int chmod(const char *pathname, mode_t mode);
int chown(const char *pathname, uid_t owner, gid_t group);
最佳实践:
- 使用umask()限制新创建文件的权限
- 避免使用过宽的权限(如0777)
- 检查文件所有权,特别是处理敏感文件时
c复制// 设置umask,确保新文件权限为644
mode_t old_umask = umask(S_IWGRP | S_IWOTH);
int fd = open("secure.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
// 实际权限将是 0666 & ~022 = 0644
umask(old_umask); // 恢复原来的umask
11.2 符号链接攻击防护
处理用户提供的路径时,需要防范符号链接攻击:
c复制int fd = open(pathname, O_WRONLY | O_NOFOLLOW);
if (fd == -1 && errno == ELOOP) {
// 路径是符号链接
}
或者使用O_PATH和openat():
c复制int dir_fd = open("/safe/dir", O_RDONLY | O_DIRECTORY);
if (dir_fd == -1) {
perror("open dir failed");
exit(EXIT_FAILURE);
}
int file_fd = openat(dir_fd, "file.txt", O_RDWR | O_NOFOLLOW);
if (file_fd == -1) {
perror("openat failed");
close(dir_fd);
exit(EXIT_FAILURE);
}
11.3 安全临时文件
创建临时文件时需要注意:
- 使用mkstemp()而不是tmpnam()或tempnam()
- 设置适当的权限
- 考虑使用O_EXCL标志
c复制char template[] = "/tmp/mytemp.XXXXXX";
int fd = mkstemp(template);
if (fd == -1) {
perror("mkstemp failed");
exit(EXIT_FAILURE);
}
// 立即unlink,文件会在关闭后自动删除
unlink(template);
// 使用文件描述符...
write(fd, "Temporary data", 14);
close(fd);
12. 调试与问题排查
12.1 使用strace跟踪系统调用
strace是强大的调试工具,可以显示程序执行的所有系统调用:
bash复制strace -o trace.log ./my_program
常见用法:
- -e trace=file:只跟踪文件相关系统调用
- -e trace=open,read,write:跟踪特定系统调用
- -f:跟踪子进程
- -p PID:附加到运行中的进程
12.2 使用lsof查看打开的文件
lsof可以列出进程打开的所有文件:
bash复制lsof -p PID
或者查找哪个进程打开了特定文件:
bash复制lsof /path/to/file
12.3 调试文件描述符泄漏
文件描述符泄漏是常见问题,可以通过以下方法调试:
- 在程序中定期检查/proc/self/fd
- 使用LD_PRELOAD挂钩close()等函数
- 使用valgrind的--track-fds选项
c复制void check_fds() {
DIR *dir = opendir("/proc/self/fd");
if (dir == NULL) {
perror("opendir failed");
return;
}
struct dirent *entry;
while ((entry = readdir(dir)) != NULL) {
if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) {
continue;
}
printf("Open FD: %s\n", entry->d_name);
}
closedir(dir);
}
13. 跨平台开发注意事项
13.1 Windows与Linux文件IO差异
-
文本模式与二进制模式:
- Windows区分文本模式和二进制模式
- Linux不区分
-
路径分隔符:
- Windows使用反斜杠()
- Linux使用正斜杠(/)
-
文件锁定机制不同
解决方案:
- 使用跨平台库如Boost.F
