Linux中基础IO
2026/7/13 16:24:38 网站建设 项目流程

一、回顾C语言文件接口

1. 打开文件

#include <stdio.h> int main() { FILE *fp = fopen("myfile", "w"); if (!fp) { printf("fopen error!\n"); return 1; } fclose(fp); return 0; }

问题myfile会被创建在哪里?
在当前进程的“当前工作目录”下

那么系统如何知道进程的当前路径?
Linux 提供了/proc文件系统,可以查看运行中进程的信息:

ps ajx | grep myProc ls /proc/533463 -l

重点关注两个符号链接:

  • cwd→ 当前进程的工作目录

  • exe→ 当前进程的可执行文件路径

✅ 打开文件,本质是进程打开文件,因此即使不带路径,进程也能通过cwd知道文件应该放在哪里。

2. 写文件

const char *msg = "hello bit!\n"; int count = 5; while (count--) { fwrite(msg, strlen(msg), 1, fp); }
  • fwrite是按二进制块写入,适合结构体、固定长度数据。

  • 写入的是原始内存数据,不自动加\0

3. 读文件

char buf[1024]; while (1) { size_t s = fread(buf, 1, sizeof(buf), fp); if (s > 0) { buf[s] = 0; printf("%s", buf); } if (feof(fp)) break; }
  • fread返回实际读取的字节数。

  • feof判断是否读到文件末尾。

4. 输出到显示器:三种标准方式

fwrite(msg, strlen(msg), 1, stdout); // 二进制写 printf("hello printf\n"); // 格式化输出 fprintf(stdout, "hello fprintf\n"); // 文件流方式
  • stdin/stdout/stderr都是FILE*类型,默认已打开。

5. 打开方式总结

模式含义
r只读,位置在文件开头
r+读写,位置在文件开头
w只写,文件长度截断或创建
w+读写,截断或创建
a追加,位置在文件末尾
a+读写追加,读从开头,写从末尾

用w文件会先被清空然后再进行写入

二、系统文件I/O

1. 标志位的传递方式

系统调用中经常需要传递多个选项,比如O_WRONLY | O_CREAT。这种写法依赖位掩码技术:

#define ONE 0001 // 二进制 0001 #define TWO 0002 // 二进制 0010 #define THREE 0004 // 二进制 0100 void func(int flags) { if (flags & ONE) printf("ONE "); if (flags & TWO) printf("TWO "); if (flags & THREE) printf("THREE "); } func(ONE | TWO); // 输出:ONE TWO

每个标志位独占一个 bit,通过|组合,通过&检测,高效且直观。

2. 接口介绍

2.1 open接口

函数原型

#include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> int open(const char *pathname, int flags); int open(const char *pathname, int flags, mode_t mode);

参数详解

参数类型说明
pathnameconst char*要打开或创建的文件路径(相对或绝对路径)
flagsint文件打开方式标志位(见下表)
modemode_t创建文件时的权限(仅当flags包含O_CREAT时使用)

flags 常用标志位

标志含义说明
访问模式(三选一)
O_RDONLY只读打开不能写
O_WRONLY只写打开不能读
O_RDWR读写打开可读可写
可选标志(可组合)
O_CREAT文件不存在则创建需提供mode参数
O_APPEND追加模式每次写入自动到文件末尾
O_TRUNC截断打开文件时清空内容
O_EXCL排他创建O_CREAT一起用,文件已存在则失败
O_SYNC同步写入write等待数据落盘才返回
O_NONBLOCK非阻塞模式用于设备文件或管道
O_CLOEXEC执行时关闭forkexec时自动关闭

mode 权限详解

// mode 格式:八进制数,三位分别表示:所有者、所属组、其他人 0644 // 二进制:110 100 100 // 所有者:rw- (6) // 所属组:r-- (4) // 其他人:r-- (4)

返回值

返回值含义
>= 0成功,返回文件描述符(非负小整数)
-1失败,设置errno,可用perror()查看错误

完整示例

#include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <errno.h> #include <string.h> int main() { // 读写打开,不存在则创建,存在则清空 int fd = open("test.txt", O_RDWR | O_CREAT | O_TRUNC, 0644); if (fd == -1) { fprintf(stderr, "open failed: %s\n", strerror(errno)); return 1; } printf("open success, fd = %d\n", fd); // ... 使用 fd 进行读写 ... close(fd); return 0; }
2.2close接口

函数原型

#include <unistd.h> int close(int fd);

参数

参数说明
fd要关闭的文件描述符(由open返回)

返回值

返回值含义
0成功关闭
-1失败(如 fd 无效),设置errno
2.3read接口
#include <unistd.h> ssize_t read(int fd, void *buf, size_t count);

参数详解

参数类型说明
fdint已打开的文件描述符
bufvoid*用户缓冲区指针,用于存放读取的数据
countsize_t要读取的字节数(不能超过buf大小)

返回值详解

返回值含义
> 0成功读取的字节数(可能小于count
0读到文件末尾(EOF)
-1失败,设置errno
2.4 write接口

函数原型

#include <unistd.h> ssize_t write(int fd, const void *buf, size_t count);

参数详解

参数类型说明
fdint已打开的文件描述符(必须是写模式打开)
bufconst void*要写入的用户数据缓冲区
countsize_t要写入的字节数

返回值

返回值含义
> 0实际写入的字节数(可能小于count
-1失败,设置errno

3. files_struct文件管理

核心结构:每个进程的task_struct中包含files_struct,它管理进程打开的所有文件

进程 task_struct │ └── files_struct │ └── fd_array[] (文件描述符表) │ ┌─────────────┼─────────────┬─────────────┐ │ │ │ │ fd[0] fd[1] fd[2] fd[3] │ │ │ │ ▼ ▼ ▼ ▼ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ │struct│ │struct│ │struct│ │struct│ │ file │ │ file │ │ file │ │ file │ │ (终端│ │ (终端│ │ (终端│ │(文件 │ │ 输入)│ │ 输出)│ │ 错误)│ │ out) │ └──────┘ └──────┘ └──────┘ └──────┘ │ │ │ │ ▼ ▼ ▼ ▼ 键盘 屏幕 屏幕 磁盘文件

说明

  • 每个fd是数组下标(0,1,2,3...)

  • 数组元素指向内核中的struct file对象

  • struct file再指向具体的物理设备或文件

磁盘文件会将数据放在文件缓冲区中,一个struct file对应一个文件缓冲区,对文件内容做任何操作,都必须先把文件加载到内核对应的文件缓冲区内。

4. 重定向原理

【执行前 - 终端输出】 ┌─────────────────┐ 进程 files_struct │ 内核文件表 │ │ │ fd[0] ────────────→│ stdin (键盘) │ fd[1] ────────────→│ stdout (终端) │ ← ls 写入这里 fd[2] ────────────→│ stderr (终端) │ fd[3] ────────────→│ out.txt (文件) │ └─────────────────┘ 【执行 dup2(3, 1) 后】 ┌─────────────────┐ 进程 files_struct │ 内核文件表 │ │ │ fd[0] ────────────→│ stdin (键盘) │ fd[1] ────────────→│ out.txt (文件) │ ← ls 重定向写到这里 fd[2] ────────────→│ stderr (终端) │ fd[3] ────────────→│ out.txt (文件) │ └─────────────────┘ 【结果】ls 写入 fd[1] → 实际写入 out.txt 文件

重定向的本质是:改变文件描述符与文件之间的映射关系。

5. dup2操作

dup2可以让我们显式地“复制”一个文件描述符:

#include <unistd.h> int dup2(int oldfd, int newfd);

它的作用:让newfd指向oldfd所指向的文件。如果newfd已经打开,会先关闭它。

输出重定向示例

int fd = open("output.txt", O_CREAT | O_WRONLY | O_TRUNC, 0644); dup2(fd, 1); // 让 fd=1 指向 output.txt printf("这条消息会写入文件,而不是屏幕"); fflush(stdout);

输入重定向示例

int fd = open("input.txt", O_RDONLY); dup2(fd, 0); // 让 fd=0 指向 input.txt char buf[256]; fgets(buf, sizeof(buf), stdin); // 实际从文件读取

追加重定向

只需在open时加上O_APPEND标志,再用dup2重定向1

int fd = open("log.txt", O_CREAT | O_WRONLY | O_APPEND, 0644); dup2(fd, 1);

6. 重定向操作

在 Linux 中,每个程序运行时默认打开三个特殊文件:

  • 标准输入 (stdin)→ 文件描述符0

  • 标准输出 (stdout)→ 文件描述符1coutprintf

  • 标准错误 (stderr)→ 文件描述符2cerrperror

通过重定向,我们可以把它们分别导向文件或合并。

1. 只重定向标准输出
./stream > log.txt //也可以写成 ./stream 1>log.txt
  • >等价于1>

  • 仅将stdout写入log.txtstderr仍显示在终端

2. 有标准输出和标准错误,只重定向标准输出
./stream hello cout hello printf hello cerr hello stderr ./stream > log.txt hello cerr hello stderr

./stream正常运行时,cout/printf(标准输出)和cerr/stderr(标准错误)都打印到终端。

./stream > log.txt只把标准输出(1>)重定向到log.txt文件,没有重定向标准错误(2>,所以:

hello couthello printf写入log.txt(不在屏幕显示)

hello cerrhello stderr仍然直接输出到终端(屏幕)

3. 分别重定向 stdout 和 stderr
./stream 1>log.normal 2>log.error
  • 1>:标准输出 →log.normal

  • 2>:标准错误 →log.error

4. 将 stdout 和 stderr 重定向到同一个文件(覆盖/追加)
# 方法一:分别重定向但使用同一个文件(追加模式) ./stream 1>log.normal 2>>log.normal # 方法二:将 stderr 合并到 stdout(常用) ./stream 1>log.normal 2>&1
  • 2>>表示追加到文件,避免覆盖

  • 2>&1表示将stderr重定向到stdout当前指向的位置

三、理解“一切皆文件”

什么是“一切皆文件”?

在 Windows 中,只有常规文件才是“文件”。
但在 Linux 中:

  • 普通文件是文件(如文本、二进制)

  • 目录是文件

  • 硬件设备(如磁盘、显示器、键盘)也被抽象为文件

  • 进程信息(如/proc下的内容)是文件

  • 管道、Socket也是文件

你能用read()读取一个文件,也能用read()读取键盘输入、管道数据、甚至网络数据。

统一接口带来的巨大好处

开发者只需掌握一套 API(如openreadwriteclose),就能操作 Linux 中绝大部分资源。

  • 几乎所有读取操作:read()

  • 几乎所有写入操作:write()

这一统一抽象极大降低了学习成本和开发复杂性。

内核中的关键数据结构:struct filestruct file_operations

Linux 内核为每个打开的文件维护一个struct file,其中包含文件的状态、位置、标志等。

struct file { struct inode *f_inode; const struct file_operations *f_op; unsigned int f_flags; fmode_t f_mode; loff_t f_pos; // ... };

其中最关键的成员是f_op,它指向一个struct file_operations,这个结构体里几乎全是函数指针:

struct file_operations { ssize_t (*read)(struct file *, char __user *, size_t, loff_t *); ssize_t (*write)(struct file *, const char __user *, size_t, loff_t *); int (*open)(struct inode *, struct file *); int (*release)(struct inode *, struct file *); // ... };

系统调用与驱动的桥梁

  • 每个设备、文件类型都可以自定义自己的read/write实现

  • 当你调用read()时,内核会通过file->f_op->read找到真正属于该设备的读取方法

  • 这就是多态在内核中的体现

尽管键盘、显示器、普通文件的读写方式完全不同,但开发者看到的接口是统一的。

一张图理解

进程调用 read() ↓ 内核中的 struct file ↓ f_op 指向 struct file_operations ↓ 调用该结构中的 read() 函数指针 ↓ 真正执行:普通文件 / 键盘 / 管道 / Socket 的读取逻辑

四、缓冲区

什么是缓冲区?

缓冲区是内存中预留的一片区域,用于暂存输入或输出数据。根据用途,可分为输入缓冲区输出缓冲区。它像是一个“中转站”,在 CPU 与慢速设备(如磁盘、打印机)之间协调数据流动。

为什么要引入缓冲区?

如果没有缓冲区,每次读写文件都需要直接调用系统调用(如read/write),这会频繁触发 CPU 从用户态切换到内核态,导致上下文切换开销巨大,严重影响程序性能。

引入缓冲区后,可以:

  • 减少系统调用次数:一次读取大量数据到缓冲区,后续操作直接从缓冲区读取。

  • 提高 CPU 利用率:CPU 无需等待慢速 I/O 设备,可以继续处理其他任务。

  • 协调速度差异:缓冲区的读写速度远快于磁盘,显著提升整体效率。

💡 举个例子:打印文档时,数据先写入缓冲区,打印机慢慢处理,CPU 则可以继续执行其他任务。

缓冲区刷新触发条件:

1. 强制刷新

2. 刷新条件满足(满足这个条件有下面三种情况)

3. 进程退出

缓冲区的三种类型(标准 I/O)

类型刷新条件典型场景
全缓冲缓冲区满后才执行系统调用磁盘文件操作
行缓冲遇到换行符或缓冲区满(默认 1024 字节)终端输入输出(如 stdin、stdout)
无缓冲立即执行系统调用标准错误流stderr
  • 全缓冲的效率最高,普通文件一般用这种方式
  • 行缓冲一般是显示器用
  • 无缓冲就是立即刷新,写透模式WT

一个小实验:重定向带来的缓冲陷阱

close(1); int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666); printf("hello world: %d\n", fd); close(fd);

现象log.txt中没有任何内容。

原因:文件描述符 1(stdout)被重定向到磁盘文件后,缓冲方式从行缓冲变为全缓冲printf的内容未填满缓冲区,因此未写入文件。

解决方法:调用fflush(stdout)强制刷新。

stderr是无缓冲的,因此重定向 2 号描述符后,perror的内容会立即写入文件。

两级缓冲机制

在 Linux 系统中,数据从应用程序写入磁盘通常要经过两层缓冲区:

  1. 用户级缓冲区(C 标准库提供,如FILE结构中的缓冲区)

  2. 内核级缓冲区(页缓存 Page Cache,由操作系统内核管理)

数据流向如下:

应用程序 ↓ 用户级缓冲区(printf/fwrite 写入) ↓ 内核缓冲区(页缓存) ↓ 磁盘设备

为什么需要两层缓冲?

层级提供者作用刷新方式
用户级缓冲区C 标准库减少系统调用次数行缓冲/全缓冲/无缓冲,fflush强制刷新
内核级缓冲区OS 内核减少磁盘 I/O 次数,提高磁盘读写性能由内核管理(如fsync强制落盘)

💡关键区别

  • fflush只将数据从用户级缓冲区刷新到内核缓冲区,并不保证数据写入磁盘。

  • fsync/sync才会强制将内核缓冲区的数据写入磁盘。

图示:数据写入全流程

+-------------------+ +-------------------+ | 应用程序代码 | | | | printf("hello") | | | +--------+----------+ | 用户空间 | | | | v | | +-------------------+ | | | C 标准库 FILE 缓冲区 | | | | (用户级缓冲) | | | +--------+----------+ | | | | | | fflush() | | v | | +-------------------+ | | | 系统调用 | | | | write(fd, ...) | | | +--------+----------+ +-------------------+ | | 系统调用(用户态 → 内核态) v +-------------------+ +-------------------+ | 内核页缓存 | | 内核空间 | | (Page Cache) | | | +--------+----------+ | | | | | | fsync() | | v | | +-------------------+ | | | 磁盘设备 | | | | (物理存储) | | | +-------------------+ +-------------------+

数据交给计算机,交给硬件本质全是拷贝

计算机数据流动的本质:一切皆拷贝

五、FILE 结构与用户级缓冲区

FILE 是什么?

FILE是 C 标准库中定义的一个结构体,用于管理文件流。它封装了:

  • 文件描述符_fileno

  • 用户级缓冲区的指针(读/写指针、基址、边界等)

  • 缓冲区大小、标志位、锁等

FILE 的形成过程(简化)

  1. 调用fopen

    FILE *fp = fopen("file.txt", "r");
  2. fopen内部执行

    • 调用open系统调用,获得一个文件描述符fd

    • 动态分配FILE结构体(通常在堆上)

    • fd赋值给FILE_fileno字段

    • 根据打开模式(如行缓冲、全缓冲)初始化缓冲区(可选,默认可能分配 BUFSIZ 字节)

    • 返回FILE*指针

  3. 使用时(如fread/fwrite/printf):

    • 先操作FILE内部的用户缓冲区

    • 必要时调用write/read系统调用

  4. 调用fclose

    • 刷新用户缓冲区(fflush

    • 调用close系统调用关闭文件描述符

    • 释放FILE结构体内存

图示:

用户调用 fopen("log.txt", "w") │ ▼ ┌──────────────────────────────────────────────┐ │ glibc 中的 fopen 实现 │ │ 1. 调用 open() 系统调用 │ │ └─→ 内核创建 struct file,分配 fd(如 3) │ │ 2. 分配 FILE 结构体(用户态堆内存) │ │ └─→ 填充 _fileno = fd │ │ └─→ 分配缓冲区(如 1024 字节) │ │ └─→ 设置缓冲类型(默认全缓冲或行缓冲) │ │ 3. 返回 FILE* 指针 │ └──────────────────────────────────────────────┘

FILE 结构体关键成员

// /usr/include/libio.h struct _IO_FILE { int _flags; // 文件状态标志 char* _IO_read_ptr; // 读缓冲区当前指针 char* _IO_read_end; // 读缓冲区结束位置 char* _IO_read_base; // 读缓冲区基址 char* _IO_write_base; // 写缓冲区基址 char* _IO_write_ptr; // 写缓冲区当前指针 char* _IO_write_end; // 写缓冲区结束位置 char* _IO_buf_base; // 缓冲区基址 char* _IO_buf_end; // 缓冲区结束位置 int _fileno; // ← 封装的文件描述符(核心!) // ... 其他成员 };

一个有趣的 fork 现象

printf("hello printf\n"); fwrite("hello fwrite\n", ...); write(1, "hello write\n", ...); fork();

正常运行输出一次,但重定向到文件后,printffwrite输出两次,write只输出一次。

原因

  • 重定向到文件后,库函数缓冲变为全缓冲。

  • fork时,子进程复制了父进程的缓冲区(写时拷贝)。

  • 进程退出时,父子进程各自刷新缓冲区,导致重复输出。

  • write无缓冲区,不受影响。

✅ 这说明:用户级缓冲区由 C 标准库提供,位于系统调用之上。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询