文件I/O学习笔记
文件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_FILENO
1
: 标准输出 (stdout) -STDOUT_FILENO
2
: 标准错误 (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 |
|