Wiki LogoWiki - The Power of Many

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. 思考题

  1. 缓冲对 I/O 性能有什么影响?
  2. 什么时候使用低级 I/O?
  3. 文件锁的作用是什么?
  4. mmap 有什么优势?
  5. 文件描述符泄漏有什么危害?

10. 本周小结

  • 标准 I/O: fopen, fread, fwrite, 缓冲.
  • 低级 I/O: open, read, write, 文件描述符.
  • 文件元数据: stat, 权限, 类型.
  • 目录操作: opendir, readdir.
  • 文件锁: fcntl, flock.
  • 内存映射: mmap.

文件 I/O 是系统编程的基础. 理解缓冲和文件描述符, 是编写高效程序的关键.

On this page