Week 15: 文件 I/O
掌握标准 I/O、低级 I/O、文件描述符与文件锁.
1. 标准 I/O
1.1 文件打开与关闭
#include <stdio.h>
FILE *fp = fopen("file.txt", "r");
if (fp == NULL) {
perror("fopen");
return 1;
}
// 使用文件...
fclose(fp);1.2 打开模式
| 模式 | 描述 |
|---|---|
| "r" | 只读 |
| "w" | 写入 (清空/创建) |
| "a" | 追加 |
| "r+" | 读写 |
| "w+" | 读写 (清空/创建) |
| "a+" | 读写追加 |
| "rb" | 二进制读 |
| "wb" | 二进制写 |
1.3 读取函数
// 字符读取
int ch = fgetc(fp);
// 行读取
char line[256];
if (fgets(line, sizeof(line), fp) != NULL) {
printf("%s", line);
}
// 格式化读取
int x;
char name[50];
fscanf(fp, "%s %d", name, &x);
// 块读取
char buffer[1024];
size_t n = fread(buffer, 1, sizeof(buffer), fp);1.4 写入函数
// 字符写入
fputc('A', fp);
// 字符串写入
fputs("Hello\n", fp);
// 格式化写入
fprintf(fp, "Name: %s, Age: %d\n", "Alice", 30);
// 块写入
fwrite(buffer, 1, sizeof(buffer), fp);1.5 文件定位
// 获取位置
long pos = ftell(fp);
// 设置位置
fseek(fp, 0, SEEK_SET); // 文件开头
fseek(fp, 0, SEEK_END); // 文件末尾
fseek(fp, -10, SEEK_CUR); // 向前 10 字节
// 重置到开头
rewind(fp);1.6 缓冲控制
// 设置缓冲模式
setvbuf(fp, NULL, _IONBF, 0); // 无缓冲
setvbuf(fp, NULL, _IOLBF, 0); // 行缓冲
setvbuf(fp, buf, _IOFBF, 1024); // 全缓冲
// 刷新缓冲
fflush(fp);2. 低级 I/O (POSIX)
2.1 文件描述符
#include <fcntl.h>
#include <unistd.h>
// 打开文件
int fd = open("file.txt", O_RDONLY);
if (fd == -1) {
perror("open");
return 1;
}
// 读取
char buf[100];
ssize_t n = read(fd, buf, sizeof(buf));
// 写入
ssize_t written = write(fd, "Hello", 5);
// 关闭
close(fd);2.2 打开标志
// 访问模式
O_RDONLY // 只读
O_WRONLY // 只写
O_RDWR // 读写
// 文件创建
O_CREAT // 不存在则创建
O_EXCL // 与 O_CREAT 一起使用, 文件存在则失败
O_TRUNC // 清空文件
// 其他
O_APPEND // 追加模式
O_NONBLOCK // 非阻塞
// 示例
int fd = open("file.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);2.3 文件定位
off_t pos = lseek(fd, 0, SEEK_CUR); // 当前位置
lseek(fd, 0, SEEK_SET); // 开头
lseek(fd, 0, SEEK_END); // 末尾2.4 文件描述符复制
// 复制 fd
int fd2 = dup(fd);
// 复制到指定描述符
int fd3 = dup2(fd, 10);3. 标准 I/O vs 低级 I/O
| 特性 | 标准 I/O | 低级 I/O |
|---|---|---|
| 缓冲 | 有 | 无 |
| 可移植性 | 高 | POSIX |
| 格式化 | 支持 | 不支持 |
| 性能 | 一般 | 高 (直接系统调用) |
3.1 转换
// 文件指针 → 文件描述符
int fd = fileno(fp);
// 文件描述符 → 文件指针
FILE *fp = fdopen(fd, "r");3.2 pread / pwrite (原子定位读写)
无需 lseek 的定位 I/O, 线程安全:
#include <unistd.h>
// pread: 在指定偏移量读取, 不改变文件偏移量
ssize_t pread(int fd, void *buf, size_t count, off_t offset);
// pwrite: 在指定偏移量写入, 不改变文件偏移量
ssize_t pwrite(int fd, const void *buf, size_t count, off_t offset);
// 示例: 多线程并发读取文件不同部分
void *thread_read(void *arg) {
struct read_task *task = arg;
pread(task->fd, task->buf, task->len, task->offset);
// 无需锁, 每个线程读取独立区域
return NULL;
}
// 对比 lseek + read (非原子, 需要锁)
// lseek(fd, offset, SEEK_SET);
// read(fd, buf, count);为什么需要 pread/pwrite:
- 原子操作: 定位和读写合一
- 线程安全: 不修改共享的文件偏移量
- 性能: 减少系统调用
4. 文件元数据
4.1 stat 函数
#include <sys/stat.h>
struct stat st;
if (stat("file.txt", &st) == 0) {
printf("Size: %lld\n", (long long)st.st_size);
printf("Mode: %o\n", st.st_mode & 07777);
printf("UID: %d\n", st.st_uid);
printf("Inode: %llu\n", (unsigned long long)st.st_ino);
}
// fstat: 使用文件描述符
// lstat: 不跟随符号链接4.2 文件类型判断
if (S_ISREG(st.st_mode)) printf("Regular file\n");
if (S_ISDIR(st.st_mode)) printf("Directory\n");
if (S_ISLNK(st.st_mode)) printf("Symbolic link\n");
if (S_ISFIFO(st.st_mode)) printf("FIFO\n");
if (S_ISSOCK(st.st_mode)) printf("Socket\n");5. 目录操作
5.1 遍历目录
#include <dirent.h>
DIR *dir = opendir(".");
if (dir == NULL) {
perror("opendir");
return 1;
}
struct dirent *entry;
while ((entry = readdir(dir)) != NULL) {
printf("%s\n", entry->d_name);
}
closedir(dir);5.2 目录操作函数
mkdir("newdir", 0755); // 创建目录
rmdir("olddir"); // 删除空目录
chdir("/tmp"); // 改变工作目录
char cwd[PATH_MAX];
getcwd(cwd, sizeof(cwd)); // 获取当前目录6. 文件锁
6.1 fcntl 锁
#include <fcntl.h>
int fd = open("file.txt", O_RDWR);
struct flock lock;
lock.l_type = F_WRLCK; // 写锁 (F_RDLCK 读锁)
lock.l_whence = SEEK_SET;
lock.l_start = 0;
lock.l_len = 0; // 0 = 整个文件
lock.l_pid = getpid();
// 加锁 (阻塞)
if (fcntl(fd, F_SETLKW, &lock) == -1) {
perror("fcntl");
}
// 操作文件...
// 解锁
lock.l_type = F_UNLCK;
fcntl(fd, F_SETLK, &lock);6.2 flock (BSD)
#include <sys/file.h>
flock(fd, LOCK_EX); // 排他锁
flock(fd, LOCK_SH); // 共享锁
flock(fd, LOCK_UN); // 解锁7. 内存映射
7.1 mmap
#include <sys/mman.h>
int fd = open("file.txt", O_RDWR);
struct stat st;
fstat(fd, &st);
// 映射文件到内存
void *addr = mmap(NULL, st.st_size,
PROT_READ | PROT_WRITE,
MAP_SHARED, fd, 0);
if (addr == MAP_FAILED) {
perror("mmap");
return 1;
}
// 直接像数组一样访问
char *data = (char *)addr;
data[0] = 'X';
// 同步到磁盘
msync(addr, st.st_size, MS_SYNC);
// 取消映射
munmap(addr, st.st_size);
close(fd);8. 练习
8.1 文件复制
使用低级 I/O 实现高效的文件复制.
8.2 目录遍历
递归遍历目录并统计文件大小.
8.3 日志文件
实现一个带文件锁的日志写入函数.
9. 思考题
- 缓冲对 I/O 性能有什么影响?
- 什么时候使用低级 I/O?
- 文件锁的作用是什么?
- mmap 有什么优势?
- 文件描述符泄漏有什么危害?
10. 本周小结
- 标准 I/O: fopen, fread, fwrite, 缓冲.
- 低级 I/O: open, read, write, 文件描述符.
- 文件元数据: stat, 权限, 类型.
- 目录操作: opendir, readdir.
- 文件锁: fcntl, flock.
- 内存映射: mmap.
文件 I/O 是系统编程的基础. 理解缓冲和文件描述符, 是编写高效程序的关键.