文件 I/O(Input/Output)是与外部存储(如硬盘、SSD)中的文件进行数据交换的基础。
C 语言提供了两套主要的文件操作 API:标准 I/O和系统 I/O。
- 标准 I/O (Standard I/O): C 语言标准库 (
<stdio.h>) 提供的函数。它们具有良好的可移植性,并内置了用户态缓冲区机制以提高效率。是日常应用开发的首选。 - 系统 I/O (System I/O): 操作系统内核直接提供的系统调用(System Call),在 Linux/Unix 中遵循 POSIX 标准(如
<unistd.h>,<fcntl.h>)。它们更接近底层,功能更原始,不带用户态缓冲区(数据直达内核缓冲区)。
| 特性对比 | 标准 I/O (stdio.h) | 系统 I/O (unistd.h, fcntl.h) |
|---|---|---|
| 核心标识 | FILE * (文件指针) | int (文件描述符) |
| 缓冲机制 | 有 (全/行/无缓冲) | 无 (数据直达内核缓冲区) |
| 可移植性 | 高 (遵循 C 标准) | 中 (遵循 POSIX 标准,Windows 有差异) |
| 操作对象 | 任何 C 程序能识别的“流” | 操作系统层面的一切皆“文件” |
| 效率 | 读写大块数据时,因缓冲而效率高 | 频繁进行小数据量读写时效率低 |
| 常用函数 | fopen, fclose, fread, fwrite, fgets | open, close, read, write, lseek |
| 比喻 | 精装的饭店: 提供现成菜品,服务周到 | 原始的菜市场: 提供原生食材,需自行加工 |
第一部分:标准 I/O <stdio.h>
标准 I/O 通过FILE结构体来管理文件流,并利用缓冲区优化性能
文件的打开与关闭 fopen, fclose
FILE *fopen(const char *path, const char *mode);: 打开一个文件,返回一个指向FILE结构体的指针(文件指针)。如果失败,返回NULL。int fclose(FILE *fp);: 关闭文件,将缓冲区剩余数据刷入文件并释放资源。
常用打开模式 mode:
| 模式 | 描述 | 文件不存在 | 文件存在 |
|---|---|---|---|
r | 读模式 (Read) | 打开失败 | 从头读取 |
w | 写模式 (Write) | 创建新文件 | 清空并从头写入 |
a | 追加模式 (Append) | 创建新文件 | 从末尾追加写入 |
r+ | 读写模式 | 打开失败 | 从头读写 |
w+ | 读写模式 | 创建新文件 | 清空并从头读写 |
a+ | 读写模式 | 创建新文件 | 从末尾追加,可读 |
多种读写方式
1.0 错误处理 feof,ferror
feof:(file end of) 判断是否到文件末尾
ferror:(file error) 判断是否遇到文件错误
1.1 按字符读写 fgetc, fputc
fgetc
- 相当于标准输入的
getchar() - 成功时返回读取的字符,失败或到文件末尾时返回
EOF。 - 所以必须用
int类型变量接收,以区分EOF(-1)和值为 255 的字符。
当fgetc返回EOF时,必须使用feof()和ferror()来进一步区分是文件正常结束还是读写中途发生错误。
feof(fp): 如果是因为到达文件末尾而失败,返回真(非零)。ferror(fp): 如果是因为发生 I/O 错误而失败,返回真(非零)。
1 | // 从src.txt拷贝到dest.txt,带完整错误处理 |
1.2 按字符串(行)读写 fgets, fputs
fgets
- 是读取字符串的安全选择,因为它有边界检查,能有效防止缓冲区溢出
- 最多读取
size-1个字符,或读到换行符\n为止 - 会保留
\n
fputs(buf, fp)
- 将字符串写入文件,不会自动添加
\n
1 | FILE *fp1 = fopen(argv[1], "r"); |
1.3 按数据块读写 fread, fwrite
按指定大小的数据块读写,是读写二进制文件或结构体的最常用、最高效的方式
返回值:
fread和fwrite都返回成功读/写的数据块(nmemb)数量。当到达文件末尾时,
fread可能没有读最后一块数据(不足一块),此时返回值会小于预期,甚至为 0此时实际读入缓冲区的数据字节数可能不是
size * n必须结合
ftell或自己计算字节数来处理文件末尾不足一个块的数据
1 | // 1. 只读打开源文件、只写目标文件(若不存在则创建,若存在则清空) |
1.4 按格式化 I/O 读写 fscanf, fprintf,snprintf
可以像scanf/printf一样,对文件或字符串进行格式化 I/O。
| 函数 | 源 / 目标 | 描述 |
|---|---|---|
scanf / printf | 键盘 / 终端 | 标准输入/输出 |
fscanf / fprintf | 文件 (FILE*) | 从/向指定文件流进行格式化 I/O |
sscanf / sprintf | 字符串 (char*) | 从/向字符串进行格式化 I/O。sprintf不安全! |
snprintf | 字符串 (char*) | sprintf的安全版本,指定最大写入长度,强烈推荐 |
1 | FILE *fp1 = fopen(argv[1], "r"); |
(字符串格式化):
1 | char buf[100]; |
文件定位 fseek, ftell, rewind
int fseek(FILE *stream, long offset, int whence);: 移动文件读写指针。
whence:SEEK_SET(文件头),SEEK_CUR(当前位置),SEEK_END(文件尾)。
long ftell(FILE *stream);: 返回当前读写指针相对于文件头的字节偏移量。
void rewind(FILE *stream);: 将指针移回文件头,等价于 fseek(stream, 0, SEEK_SET)。
fp (获取文件大小):
1 | FILE *fp = fopen(file.txt, "rb"); |
缓冲区
stdout默认行缓冲,stderr默认无缓冲,普通文件默认全缓冲
缓冲区刷新的 6 大时机:
- 缓冲区已满
- 遇到换行符
\n(仅限行缓冲) - 程序正常退出 (
return,exit())。abort()等异常退出不会刷新 - 手动调用
fflush(fp) - 关闭文件
fclose(fp) - 在一个流上执行任何输入操作前,会先刷新该流的输出缓冲区
setvbuf(fp, buf, mode, size): 自定义缓冲区刷新策略
fp: 文件指针buf: 缓冲区地址mode:_IOFBF(全),_IOLBF(行),_IONBF(无)。size: 缓冲区大小
1 | fprintf(stdout, "abcd"); // 标准输出模式,默认放入缓冲区 |
第二部分:系统 I/O <fcntl.h>
系统 I/O 直接与操作系统内核交互,操作的是一个整数——文件描述符 (File Descrip, fd)
它是访问文件的底层句柄,更加强大和灵活,是理解操作系统工作原理的基石
0: 标准输入 (stdin) -STDIN_FILENO1: 标准输出 (stdout) -STDOUT_FILENO2: 标准错误 (stderr) -STDERR_FILENO
错误处理机制: errno, perror, strerror
包含在errno.h里
当系统调用失败时,通常会返回 -1,并设置一个全局整型变量 errno 来表示具体的错误原因。
errno 的值只在系统调用或库函数发生错误时才会被设置,因此只有在函数返回错误时检查errno才有意义
perror(const char *s): 先打印自己的字符串 s,然后输出 errno`对应的错误描述
char *strerror(int errnum): 返回错误码对应字符串的指针,可以自定义输出格式
1 | int fd = open("non_existent_file.txt", O_RDONLY); |
文件的打开与关闭 open,close
包含在fcntl.h里
int open(const char *pathname, int flags, ...);: 打开或创建一个文件。
pathname: 文件路径。flags: 通过|组合的标志位,决定操作模式。...: 可变参数,当flags包含O_CREAT时,必须提供第三个参数mode_t mode来指定新文件的权限(如0644)。
int close(int fd);: 关闭文件描述符,释放相关内核资源。一个进程能打开的文件描述符数量是有限的,务必及时关闭不再使用的fd。
flags 标志位详解
flags 参数通过按位或 | 操作符组合,用于精确控制文件的打开方式。
| 标志 (Flag) | 英文全称 / 释义 | 描述 |
| :----------- | :---------------- | :------------------------------------------------------------------------------------------------------------------ | -------------------- |
| 访问模式 | | 以下三个标志是互斥的,必须选择其一 |
| O_RDONLY | Open Read Only | 以只读模式打开文件。 |
| O_WRONLY | Open Write Only | 以只写模式打开文件。 |
| O_RDWR | Open Read/Write | 以读写模式打开文件。 |
| 可选标志 | | **以下标志可通过 | 与访问模式组合** |
| O_CREAT | Open Create | 如果文件不存在,则创建它。使用此标志时必须提供第三个mode参数。 |
| O_TRUNC | Open Truncate | 如果文件已存在并且是可写模式打开,则将其长度截断为 0(即清空内容)。 |
| O_APPEND | Open Append | 每次写入数据时,都自动将文件指针移动到文件末尾进行追加操作。 |
| O_NONBLOCK | Open Non-blocking | 以非阻塞模式打开文件。主要用于设备文件和管道,对它们的读写操作不会被阻塞。 |
| O_EXCL | Open Exclusive | 与 O_CREAT 配合使用。如果文件已存在,open调用会失败并返回错误。这是实现“文件锁”或确保文件唯一性的原子操作。 |
1 | // 场景1: 读写方式打开,不存在则创建,权限为 rw-r--r-- |
文件内容读、写与定位
3.1 核心函数: read / write / lseek
存在于unistd.h
ssize_t read(int fd, void *buf, size_t count);:
- 从
fd读取最多count字节到buf。返回实际读取的字节数,0表示文件结束,-1表示错误。
ssize_t write(int fd, const void *buf, size_t count);:
- 将
buf中的count字节写入fd。返回实际写入的字节数,可能小于count,需循环写入。
off_t lseek(int fd, off_t offset, int whence);:
- 移动文件读写指针。
- whence 可以是
SEEK_SET(文件头),SEEK_CUR(当前位置),SEEK_END(文件尾)。
3.2 实践案例: 获取文件大小
1 | int fd = open("file.txt", O_RDONLY); |
3.3 实践案例: 创建文件空洞
1 | int fd = open("file.txt", O_RDWR | O_CREAT | O_TRUNC, 0644); |
高级文件描述符操作
4.1 I/O 重定向: dup / dup2
存在于unistd.h
int dup(int oldfd);: 复制oldfd,返回一个新的、当前未被使用的最小文件描述符
int dup2(int oldfd, int newfd);: 将newfd重定向到oldfd。如果newfd已打开,会先关闭它
这是实现 Shell 重定向(如 > 和 <)的关键。
在 shell 脚本中,0 是标准输入(键盘),1 是标准输出(终端),2 是错误输出
用 dup2(fd,0);就吧标准输入改为 fd 里,类似于 shell 里的
1 | int log_fd = open("file.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644); |
4.2 文件状态控制: fcntl
存在于fcntl.h
int fcntl(int fd, int cmd, ...);: fcntl (file control) 是一个多功能工具箱,能对文件描述符执行多种控制操作
cmd决定了fcntl的功能,常用的有F_GETFL (获取状态) 和 F_SETFL (设置状态)
具体的参数详解可以查看此博客文章:Linux fcntl 函数详解
fp (设置非阻塞 I/O):
1 | int fd = STDIN_FILENO; // 用标准输入做演示 |
特殊文件 I/O
5.1 设备 I/O 控制: ioctl
ioctl (input/output control) 是一个专用于设备 I/O 的“后门”,允许应用程序直接与设备驱动程序通信。
通过这个接口,可以实现在 linux 应用层控制内核层下的驱动,进而控制具体的外设
- 应用程序通过代码片段与设备节点交互,来操作具体硬件如 LED、读取 ADC 和控制 PWM
- 设备节点作为接口连接应用和驱动程序,完成用户空间与内核空间的通信,每个节点通过系统调用(如 open())、read()、ioctl())与应用程序交互
- 驱动程序封装具体的硬件操作逻辑,由设备节点调用,每个驱动程序专注于特定功能模块
linux 系统通过这样的分层封装实现功能分离,硬件工程师负责底层设备操作,应用软件工程师只需使用抽象化设备,无需了解底层原理
ioctl 的行为完全由request命令码和设备驱动的实现决定,具体应用可以查看 linux 编程笔记
5.2 内存映射 I/O: mmap
存在于sys/mman.h里
*mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);: 将文件或设备直接映射到进程的虚拟内存空间
之后对这块内存的读写就等同于对文件的读写,省去了read/write的系统调用开销和内核/用户态数据拷贝,效率极高
prot: 内存保护标志 (PROT_READ, PROT_WRITE)。
flags: MAP_SHARED(修改会同步到文件)或 MAP_PRIVATE(写时复制,修改不影响文件)。
munmap(void *addr, size_t length) : Memory UNMAP,解除内存映射。
内存映射文件:
1 | int fd = open("mmap_test.txt", O_RDWR | O_CREAT, 0644); |
第三部分:文件与目录管理
在 linux 系统编程中,除了对文件内容进行读写,我们还经常需要获取和管理文件本身的属性(如大小、权限、类型)以及组织这些文件的目录结构。
这部分操作主要依赖于 POSIX 标准提供的系统调用。
1. 获取文件元数据: stat / fstat / lstat
这组函数用于获取文件的元数据,并将其填充到一个struct stat结构体中
int stat(const char *pathname, struct stat *statbuf);: 通过文件名获取属性
int fstat(int fd, struct stat *statbuf);: 通过文件描述符获取属性
int lstat(const char *pathname, struct stat *statbuf);:
- 类似
stat,但如果pathname是一个符号链接 ,lstat获取的是链接本身的属性,而不是它所指向的文件的属性。 stat则会“穿透”链接,获取目标文件的属性。
1.0 struct stat 结构体
不用记这个结构体,了解大概有什么就行
1 | struct stat { |
这些字段中要注意的:
st_mode: 一个 16 位的字段,包含了文件类型和权限信息。需要使用宏来解析。st_rdev: 仅当文件是字符设备或块设备时才有意义,表示该设备的设备号。st_size: 对于普通文件,是文件的字节大小。对于设备文件,此值无意义。
1.1 获取文件信息
不用管前面的那么多参数,只需要下面三个步骤就可以获取上面的信息
具体需要获取什么信息 就用 man 手册查一下结构体内容就行
1 | struct stat st;//申请一个空间存文件信息 |
1.2 实践案例: 实现一个功能完整的 ls -l
这个案例是stat函数最经典的用例,完美展示了如何解析struct stat中的各种信息。
代码示例:
1 |
|
2. 目录操作
2.1 核心函数: opendir / readdir / closedir / chdir
核心知识点:
DIR *opendir(const char *name);: 打开一个目录,返回一个指向DIR结构体的目录流指针。struct dirent *readdir(DIR *dirp);: 从目录流中读取下一个目录项。当读取完毕或发生错误时,返回NULL。struct dirent中最重要的成员是d_name(文件名)。int closedir(DIR *dirp);: 关闭目录流,释放资源。int chdir(const char *path);: 改变当前进程的工作目录 (Change Directory)。这是一个非常有用的函数,在遍历目录时,可以先chdir到目标目录,然后直接对目录内的文件名使用stat,避免了手动拼接完整路径的麻烦。
2.2 实践案例: 遍历目录并获取文件信息
这个案例展示了如何结合目录遍历和文件属性获取,实现一个简单的目录列表程序。
代码示例:
1 |
|
3. 目录的创建与删除: mkdir / rmdir
核心知识点:
int mkdir(const char *pathname, mode_t mode);: 创建一个新目录。mode参数指定目录的权限,但会受到umask的影响。int rmdir(const char *pathname);: 删除一个空目录。如果目录非空,rmdir会失败。
代码示例:
1 |
|
