摘要
在 Linux 操作系统中,“一切皆文件”(Everything is a file)是极其核心的设计哲学。除了普通的文本与二进制文件外,网络 Socket、管道、硬件设备(如串口、显示屏、触摸屏等)在应用层均被抽象为文件进行操作。
本文将聚焦于 Linux 系统层面的 文件 I/O(File I/O,又称系统调用 I/O),深入讲解其核心 API(open、read、write、lseek、close)、文件描述符(FD)的分配机制,并深度对比标准 I/O与文件 I/O在实现原理、缓冲区机制及适用场景上的本质区别。
一、什么是文件 I/O?
文件 I/O 是 Linux 内核直接提供的文件操作接口,属于系统调用(System Call)。
与 ANSI C 标准库提供的标准 I/O(如 fopen、fread、fwrite)不同,文件 I/O 不经过 C 库缓冲,直接由内核处理,因此实时性更高,常用于设备文件操作或底层的网络编程中。
标准 I/O vs 文件 I/O 架构对比
[file-tag: code-generated-file-fdtablepng]
二、关键概念:文件描述符(File Descriptor, FD)
在标准 I/O 中,我们使用文件流指针 FILE * 来标记打开的文件;而在文件 I/O 中,内核使用 文件描述符(fd) 来表示被打开的文件。
1. 文件描述符的本质
●文件描述符是一个小的非负整数。
●每当进程打开一个文件,内核会在进程的“文件描述符表”中分配一个索引,并将其返回给应用层。
●Linux 操作系统默认可分配的文件描述符范围通常为 0 ~ 1023(共 1024 个)。
2. 系统默认打开的三个文件描述符
每个 Linux 进程在启动时,默认都已经打开了三个文件描述符,分别对应标准 I/O 中的三个流:
|
标准 I/O 文件流指针 |
文件 I/O 描述符 |
宏定义(<unistd.h>) |
对应设备 |
|
stdin |
0 |
STDIN_FILENO |
标准输入(键盘) |
|
stdout |
1 |
STDOUT_FILENO |
标准输出(显示屏/终端) |
|
stderr |
2 |
STDERR_FILENO |
标准错误(显示屏/终端) |
3. FD 的分配原则
最小未被使用原则:操作系统分配 FD 时,总是挑选当前列表中最小的、且未被占用的非负整数。
[file-tag: code-generated-file-fileioarchitecturepng]
三、文件 I/O 核心 API 详解
需要包含的头文件:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
1. 打开/创建文件:open()
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
●参数:
pathname: 要打开的文件路径。
flags: 打开方式(必选其一 + 可选组合):
■O_RDONLY: 只读打开
■O_WRONLY: 只写打开
■O_RDWR: 读写打开
■O_CREAT: 若文件不存在则创建
■O_TRUNC: 打开时清空文件
■O_APPEND: 追加写入
mode: 当创建新文件(指定 O_CREAT)时设置权限(如 0664)。
●返回值:成功返回 文件描述符 (fd),失败返回 -1。
标准 I/O 与 文件 I/O 模式对应关系
|
标准 I/O (fopen) |
文件 I/O 标志 (open flags) |
说明 |
|
"r" |
O_RDONLY |
只读,文件必须存在 |
|
"r+" |
O_RDWR |
读写,文件必须存在 |
|
"w" |
O_WRONLY | O_CREAT | O_TRUNC, 0664 |
只写,不存在则创建,存在则清空 |
|
"w+" |
O_RDWR | O_CREAT | O_TRUNC, 0664 |
读写,不存在则创建,存在则清空 |
|
"a" |
O_WRONLY | O_CREAT | O_APPEND, 0664 |
只写,不存在则创建,追加写入 |
|
"a+" |
O_RDWR | O_CREAT | O_APPEND, 0664 |
读写,不存在则创建,末尾追加 |
2. 关闭文件:close()
int close(int fd);
●功能:关闭指定的文件描述符,释放内核资源。
●注意:如果程序中打开文件后未进行 close(),会导致 文件描述符泄漏(进程可用的 FD 资源被耗尽)。
3. 读取文件:read()
ssize_t read(int fd, void *buf, size_t count);
●参数:
fd: 打开的文件描述符。
buf: 存放读取数据的内存首地址。
count: 期望读取的字节数。
●返回值:
> 0: 实际读取到的字节数。
= 0: 读到文件末尾(EOF)。
-1: 读取失败。
4. 写入文件:write()
ssize_t write(int fd, const void *buf, size_t count);
●参数:
fd: 打开的文件描述符。
buf: 待写入数据的内存首地址。
count: 期望写入的字节数。
●返回值:成功返回实际写入的字节数,失败返回 -1。
5. 文件读写位置定位:lseek()
off_t lseek(int fd, off_t offset, int whence);
●参数:
-
- offset: 相对于起始位置的偏移量(字节,可正可负)。
- whence: 基准位置:
■SEEK_SET: 文件开头
■SEEK_CUR: 当前读写位置
■SEEK_END: 文件末尾
●返回值:成功返回当前读写位置到文件开头的总字节数,失败返回 -1。
常用实用技巧:
1.获取文件大小:
off_t filesize = lseek(fd, 0, SEEK_END);
2.创建空洞文件(抢占磁盘空间):
int fd = open("hole.file", O_WRONLY | O_CREAT | O_TRUNC, 0664);
lseek(fd, 1024 * 1024 * 10, SEEK_SET); // 偏移到 10MB 处
write(fd, "\\0", 1); // 写入一个字节,形成空洞文件
四、实战演练:基于文件 I/O 实现文件复制(cp)
以下示例演示如何利用 open / read / write / close 实现高效的文件拷贝工具:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int file_copy(const char *srcfile, const char *dstfile)
{
// 1. 打开源文件(只读)
int fdsrc = open(srcfile, O_RDONLY);
if (fdsrc == -1) {
perror("open srcfile error");
return -1;
}
// 2. 打开目标文件(只写,不存在则创建,存在则清空,权限0664)
int fddst = open(dstfile, O_WRONLY | O_CREAT | O_TRUNC, 0664);
if (fddst == -1) {
perror("open dstfile error");
close(fdsrc);
return -1;
}
// 3. 循环读写
char buff[1024] = {0};
ssize_t size = 0;
while ((size = read(fdsrc, buff, sizeof(buff))) > 0) {
write(fddst, buff, size);
}
// 4. 关闭文件描述符
close(fdsrc);
close(fddst);
return 0;
}
int main(int argc, const char *argv[])
{
if (argc < 3) {
printf("Usage: %s <srcfile> <dstfile>\\n", argv[0]);
return -1;
}
if (file_copy(argv[1], argv[2]) == 0) {
printf("File copy successfully!\\n");
}
return 0;
}
五、文件 I/O 与 标准 I/O 全方位对比
|
对比维度 |
标准 I/O (Standard I/O) |
文件 I/O (File I/O) |
|
操作对象 |
FILE *(文件流指针) |
int fd(文件描述符) |
|
实现层级 |
ANSI C 标准库函数(高层抽象) |
Linux 内核系统调用(底层接口) |
|
可移植性 |
高(支持 POSIX、Windows、macOS 等) |
弱(特定于 POSIX / Linux 系统) |
|
缓冲区机制 |
有缓冲区(行缓冲、全缓冲、无缓冲),减少内核切换,提高小数据频繁读写效率 |
无用户缓冲区,实时性高 |
|
适用场景 |
普通文本/二进制文件的顺序/格式化读写 |
设备文件(串口、网络 Socket、驱动硬件) |
补充:标准 I/O 的三种缓冲区模式
1.行缓冲(Line Buffered):大小通常为 1024 字节 (1KB)。常见于终端设备(stdin、stdout)。刷新条件:遇到 \\n、缓冲区满、程序结束或调用 fflush()。
2.全缓冲(Fully Buffered):大小通常为 4096 字节 (4KB)。常见于普通磁盘文件操作。刷新条件:缓冲区满、关闭文件、程序结束或调用 fflush()。
3.无缓冲(Unbuffered):数据直接写入目的地,如标准错误流 stderr。
六、辅助拓展函数:格式化与字符串截取
在 Linux C 编程中,结合文件操作常常用到以下辅助函数:
1. 格式化输出函数
●fprintf(FILE *stream, const char *format, …):向指定流写入格式化数据。
●dprintf(int fd, const char *format, …):向指定文件描述符写入格式化数据。
●sprintf(char *str, const char *format, …):格式化输出到字符串数组中,用于字符串拼接与格式转换。
2. 字符串截取:strtok()
char *strtok(char *str, const char *delim);
●功能:根据分隔符 delim 切割字符串 str。
●注意:strtok 会修改原字符串(将分隔符替换为 \\0),首次调用传 str,后续继续切割需传 NULL。
总结
1.Linux 下一切皆文件,硬件设备、文件、管道、Socket 均可使用文件 I/O 统一操作。
2.文件 I/O 返回并使用的是 文件描述符(fd),分配遵从最小未被使用原则。
3.进行普通文件读写优先考虑标准 I/O(含缓冲区,减少系统调用开销);而在硬件交互、设备驱动、实时性要求高的场景下,则应使用文件 I/O。





