%%
# 纲要
> 主干纲要、Hint/线索/路标
# Q&A
#### 已明确
#### 待明确
> 当下仍存有的疑惑
**❓<font color="#c0504d"> 有什么问题?</font>**
# Buffer
## 闪念
> sudden idea
## 候选资料
> Read it later
%%
# 文件 IO 总结
**POSIX 库**、**C 标准库**、**C++标准库**均提供了**文件 I/O 相关的接口**,主要区别如下:
| | POSIX 库 | C 标准库 | C++ 标准库 |
| ---- | ------------------------------------------------------ | ------------------------------------------------------ | ----------------------------------------- |
| 说明 | 对**底层系统调用**的直接封装 | 高层抽象,函数接口的形式 | 高层抽象,面向对象的形式 |
| 操作对象 | **文件描述符**(`int`类型) | **`FILE*` 指针** | **文件流类**(`ifstream`, `ofstream`) |
| 缓冲机制 | **无用户进程层面的缓冲** | 带缓冲 | 带缓冲 |
| 示例接口 | `open`, `create`, `read`, `write`, `lseek`, `close` 函数 | `fopen`, `fclose`, `fgets`, `fprintf`, `fscanf` 函数 | `ofstream`, `ifstream` 对象, `getline` 函数 |
> [!NOTE] "无缓冲" 与 "带缓冲" 的区别
>
> - 无缓冲:在 "**用户进程**" 层面不会进行缓冲。
> - 带缓冲:在 "**用户进程**" 层面存在缓冲。
>
> 注:所有磁盘 I/O 都会经过 "**==内核的块缓冲区==**"(也称内核的高速缓存),数据首先被存入""缓冲区",然后才被**实际写入文件**或**被程序读取**。
>
<br><br>
# 缓冲区
系统 I/O 调用、 C/C++ 标准库 I/O 函数在 "**不同层面**" 为 I/O 操作提供了缓冲区:
- **==内核==缓冲区**:所有**磁盘 I/O** 都会经过 "**==内核的块缓冲区==**",也称**内核的高速缓存**(buffer cache)。
- **C/C++ 标准 I/O 缓冲区**:标准 I/O 函数所提供的,**==用户程序==层面的缓冲区**,旨在**减少系统调用 `read`、`write` 的调用次数**。
> [!faq] <font color="#c00000">为什么需要输入输出缓冲区?</font>
>
> 目的:**提高输入输出设备的读写效率**,**降低输入输出设备的读写次数**
>
> 硬盘的数据访问是以 "**==块==**" 为单位进行的,而应用程序则是以 "**字节**" 为单位进行处理。
> 缓冲区即用于**协调二者间的不同传输速率**:
>
> - 读取数据时,**从硬盘上一次性读取大量信息到缓冲区中,再由程序从缓冲区中按需读取**,
> - 写出数据时,程序先将数据写到缓冲区中,**待缓冲区填满后再将整块数据写入到硬盘**并清空缓冲区。
>
<br><br>
## I/O 缓冲总结
完整的 **I/O 过程**及**缓冲机制**[^2]:
- (1)用户数据 => 标准 I/O 缓冲区(`stdio` 缓冲区,`iostream` 流缓冲区);
- (2)标准 I/O 缓冲区 => 内核缓冲区;
- (3)内核缓冲区 => 磁盘文件
![[_attachment/02-开发笔记/99-Unix 环境编程/文件 IO.assets/IMG-文件 IO-0D6B009B8C3E33A65EBCB57D5912E025.png|556]]
<br>
## 内核缓冲区
`read()`、`write()` 系统调用在**操作磁盘文件**时不会直接发起磁盘访问,而是仅仅在 "**用户空间**" 与 "**内核缓冲区**" 之间复制数据,之后再**由内核将其缓冲区数据刷新至磁盘**。
内核缓冲区有多种,大小取决于**具体场景**,例如:
- **文件系统缓冲区**:例如**页缓存**,也即 "**==文件系统块大小==**"(Linux 下为 `4KB`)
- **TCP 发送/接收缓冲区**:
- 查看缓冲区大小:`sysctl net.ipv4.tcp_rmem`、`sysctl net.ipv4.tcp_wmem`,将显示三项值(最小、默认、最大缓冲区大小,单位字节)
- ![[_attachment/02-开发笔记/99-Unix 环境编程/文件 IO.assets/IMG-文件 IO-00FE8F6F704DDA3050007E0D0E278A16.png|497]]
- 修改缓冲区大小:可通过修改 **socket 选项 `SO_RCVBUF` 和 `SO_SNDBUF`** 指定
- **UDP 缓冲区**
<br><br>
## 标准 I/O 库提供的缓冲区
C 标准 I/O 函数提供的**流** `FILE`,以及 C++标准 I/O 提供的 **`iostream` 流** 背后的缓冲区。
标准 I/O 提供了**三种类型的缓冲**,差异在于 "**缓冲区刷新/清空时机**" 不同:
- 「**全缓冲**」:仅当**标准 I/O 缓冲区被==填满==后**才执行**实际 I/O 操作**。
- 「**行缓冲**」:当在输入和输出中遇到 **==换行符==** 时,标准 I/O 库才执行 I/O 操作。
- 「**无缓冲**」:标准 I/O 库**不对字符进行缓冲存储**。
> [!info] Unix 系统下各场景的**默认缓冲类型**
>
> - 关联于 "**其他设备**" 的标准 I/O 流默认为**全缓冲**,例如 **==磁盘文件==**。
> - 关联于 "**==终端设备==**" 的标准 I/O 流默认为**行缓冲**,例如默认的标准输入`stdin` 与标准输出 `stdout`。
> - **标准错误 `stderr`** 默认为**无缓冲**。
>
> [!NOTE] 缓冲区可开启、关闭,设置缓冲类型,详情见 `setvbuf()` 和 `setbuf ()` 函数。
<br>
### 标准 I/O 缓冲区的大小
标准 I/O 缓冲区的大小由 **`<stdio.h>` 头文件中的宏 `BUFSIZ` 定义**,可在引入头文件后打印该值查看: `printf("%d", BUFSIZ)`。
64 位系统下通常定义为 8192 字节(8KB)。
<br>
### 标准 I/O 缓冲区的刷新时机
下列情况会触发缓冲区的刷新:
- **缓冲区已满**时: 缓冲区将会在写下一个值之前刷新。
- **行缓冲机制下,遇到回车/换行符时** ;
- 使用特定函数**手动刷新缓冲区**时 (例如C 中的函数 `fflush`, C++中的操控符 `flush`、`endl`)
- **关闭文件**时;
- **程序正常关闭**时:
- **程序正常结束**时,作为 main 返回工作的一部分,将**清空所有输出缓冲区**。
- 如果**程序意外崩溃**,则**输出缓冲区不会被自动刷新**,输出数据仍可能**停留在输出缓冲区中**等待打印。
> [!NOTE] C/C++ 中,标准输入&标准输出关联于同一终端时,从标准输入读取数据将会默认刷新 "标准输出" 缓冲区。
>
> 以 C 为例,包括 glibc 库在内的许多 **C 函数库**实现中,若 `stdin` 和 `stdout` 指向一终端,
> 那么**无论何时从 `stdin` 中读取输入时,都将隐含调用一次 `fflush(stdout)` 函数**。
>
> C++ 标准库提供的 I/O 流对象 `cout` 和 `cin` 也类似,因此**调用 `cin` 会导致 `cout` 的输出缓冲区被刷新**。 ^015hqk
<br>
## 关联于终端的输入流阻塞机制
当从**关联于终端(例如标准输入)的流**中读取数据时(包括 C 的 `getchar`, `fgets` 以及 C++的 `cin` 等):
- 若**输入缓冲区**为空,则 **==阻塞==** 并**请求键盘输入**;
- 当键盘输入**键入回车键**后,「键盘缓冲区」被刷新,其中数据被**存储到 `istream` 流的「输入缓冲区」** 中, `instream` 流**检测到缓冲区非空**于是开始读取;
- 若**输入缓冲区**不为空,**直接从缓冲区中读取数据**而不会请求键盘输入。
> [!NOTE]
>
> 从键盘输入字符串,键入回车键后,输入字节被存储到**输入缓冲区**中,回车键(`\r`)会被转换为一个换行符`\n` 同样记录在输入缓冲区中。
> 例如,在键盘上输入 123456 后敲击回车,此时缓冲区中的字节数是 7(包含换行符),而不是 6。
<br><br><br>
# POSIX 库 IO 接口
> 相关头文件:`<unistd.h>`,`<fcntl.h>`
![[_attachment/02-开发笔记/99-Unix 环境编程/文件 IO.assets/IMG-文件 IO-0D652FB6421EAFD259CBAC7DDD6C0CF4.png|532]]
所有 I/O 相关的系统调用都以 **==文件描述符==** 来**指代打开的文件**,各函数均以 "文件描述符" 作为参数。
- `<unistd.h>`
- 标准 I/O 的**文件描述符常量**:`STDIN_FILENO`, `STDOUT_FILENO` , `STDERR_FILENO`
- **读写**:
- 基本读写
- `read(fd, buf, sizeof(buf) - 1)`: 从 `fd` 读取**指定字节数**到 `buffer` 中。对于**字符数组**,末尾**至少需要留一位填入 `\0`**。
- `write(fd, str, strlen(str))`:将 `str` 中的**指定字节数**写入到 `fd` 中。
- 定位读写:**不改变当前文件的偏移量**
- `pread`,`pwrite` `
- **定位**:`lseek(fd, offset, whence)` (设置**当前文件的字节偏移量**)
- **文件截断**:
- `truncate(path, length)`,`ftruncate(fd, length)`;
- **关闭文件**:`close`;
- **复制文件描述符**:`dup`、`dup2`
- **刷新内核缓冲区**:`sync`、`fsync`、`fdatasync`
- `<fcntl.h>`
- **打开文件**: `open(path, flag, mode)`
- **创建新文件**:`create(path, mode)`
- **文件控制**(获取/设置文件属性,如**访问模式**和**状态标志**):`fcntl(fd, cmd, ...)`
- `<sys/ioctl.h>`
- 多用途 I/O 控制:`ioctl(fd, request, ...)`
- `<sys/uio.h>`
- **==scatter / gather IO==**—— "**分散输入**" 与 "**集中输出**"
- `readv(fd, iov, iovcnt)`
- `writev(fd, iov, iovcnt)
### 示例
```c
#include <unistd.h> // read, write, close, lseek
#include <fcntl.h> // open
#include <stdlib.h> // exit
#include <stdio.h> // perror
#include <string.h>
#define BUF_SIZE 1024
// POSIX接口提供的是"无缓冲的文件IO", 直接通过文件描述符引用打开的文件.
int main() {
const char* fname = "./example.txt";
char buffer[BUF_SIZE];
// `open`打开文件, 返回文件描述符.
// `O_RDWR | OCREAT`为<fcntl.h>中提供的标志位, 分别表示"只写", 不存在时创建文件.
int fd = open(fname, O_RDWR | O_CREAT, 0644); // 读写模式打开
if (fd == -1) {
perror("open");
exit(EXIT_FAILURE);
}
// 向文件写入内容
const char* text = "Hello, POSIX I/O!";
ssize_t b_wri = write(fd, text, strlen(text));
if (b_wri == -1) {
perror("b_wri");
close(fd); // 出错时, 关闭文件.
exit(EXIT_FAILURE);
}
// 将文件偏移量重置到文件开头
if (lseek(fd, 0, SEEK_SET) == -1) {
perror("lseek");
close(fd);
exit(EXIT_FAILURE);
}
// 读取文件中的内容
ssize_t b_reads = read(fd, buffer, sizeof buffer);
if (b_reads == -1) {
perror("read");
close(fd);
exit(EXIT_FAILURE);
}
printf("%s\n", buffer);
// 关闭文件
if (close(fd) == -1) {
perror("close");
exit(EXIT_FAILURE);
}
return 0;
}
```
### scatter / gather IO
`readv` 与 `writev` 函数用于在一次函数调用中**读、写多个==非连续缓冲区==**,也称之为 "**scatter read**" 和 "**gather write**"。
![[_attachment/02-开发笔记/99-Unix 环境编程/文件 IO.assets/IMG-文件 IO-4A8178E34965511C3F542BCC1DAA626B.png|719]]
![[_attachment/02-开发笔记/99-Unix 环境编程/文件 IO.assets/IMG-文件 IO-BE60370C613E15B6DD23B2A7249294A3.png|602]]
<br><br><br>
# C 标准库 IO 接口
参见 [[02-开发笔记/02-c/c-输入输出相关|c-输入输出相关]]。
<br><br>
# C++标准库文件 I/O
> 下图有误:
![[_attachment/02-开发笔记/99-Unix 环境编程/文件 IO.assets/IMG-文件 IO-AA5018F0A8036001D4F92D63D9E43F0E.png|468]]
#### 示例
```cpp
#include <iostream>
#include <fstream>
#include <string>
using namespace std;
int file_write(const char* filename) {
// 打开文件
ofstream outFile(filename);
if (!outFile) {
cerr << "文件打开失败" << endl;
return 1;
}
outFile << "Hello, C++ Standard I/O!\n";
outFile << "This is a sample file I/O program.\n";
outFile.close(); // 关闭文件
return 0;
}
int file_read(const char* filename) {
// 打开文件进行读操作
std::ifstream inFile("example.txt");
if (!inFile) {
cerr << "文件打开失败" << endl;
return 1;
}
string line;
while (getline(inFile, line)) { // 从文件中读取各行并输出
cout << line << endl;
}
inFile.close(); // 关闭文件.
return 0;
}
int main() {
const char* filename = "example.txt";
int rc = 0;
rc = file_write(filename);
if (rc != 0) return rc;
rc = file_read(filename);
if (rc != 0) return rc;
}
```
<br><br>
# 参考资料
# Footnotes
[^1]: 《UNIX 环境高级编程》
[^2]: 《Linux-UNIX 系统编程手册》