%% # 纲要 > 主干纲要、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 系统编程手册》