CppGuide社区 CppGuide社区
首页
  • 🔥最新谷歌C++风格指南(含C++17/20)
  • 🔥C++17详解
  • 🔥C++20完全指南
  • 🔥C++23快速入门
🔥C++面试
  • 第1章 C++ 惯用法与Modern C++篇
  • 第2章 C++开发工具与调试进阶
  • 第3章 C++多线程编程从入门到进阶
  • 第4章 C++网络编程重难点解析
  • 第5章 网络通信故障排查常用命令
  • 第6章 网络通信协议设计
  • 第7章 高性能服务结构设计
  • 第8章 Redis网络通信模块源码分析
  • 第9章 服务其他模块设计
  • 🚀 全部章节.pdf 下载 (opens new window)
  • 🔥C++游戏编程入门(零基础学C++)
  • 🔥使用C++17从零开发一个调试器 (opens new window)
  • 🔥使用C++20从零构建一个完整的低延迟交易系统 (opens new window)
  • 🔥使用C++从零写一个C语言编译器 (opens new window)
  • 🔥从零用C语言写一个Redis
  • leveldb源码分析
  • libevent源码分析
  • Memcached源码分析
  • TeamTalk源码分析
  • 优质源码分享 (opens new window)
  • 🔥远程控制软件gh0st源码分析
  • 🔥Windows 10系统编程
  • 🔥Linux 5.x内核开发与调试 完全指南 (opens new window)
  • TCP源码实现超详细注释版.pdf (opens new window)
  • 高效Go并发编程
  • Go性能调优
  • Go项目架构设计
  • 🔥使用Go从零开发一个数据库
  • 🔥使用Go从零开发一个编译器 (opens new window)
  • 🔥使用Go从零开发一个解释器 (opens new window)
Rust编程指南
  • SQL零基础指南
  • MySQL开发与调试指南
GitHub (opens new window)
首页
  • 🔥最新谷歌C++风格指南(含C++17/20)
  • 🔥C++17详解
  • 🔥C++20完全指南
  • 🔥C++23快速入门
🔥C++面试
  • 第1章 C++ 惯用法与Modern C++篇
  • 第2章 C++开发工具与调试进阶
  • 第3章 C++多线程编程从入门到进阶
  • 第4章 C++网络编程重难点解析
  • 第5章 网络通信故障排查常用命令
  • 第6章 网络通信协议设计
  • 第7章 高性能服务结构设计
  • 第8章 Redis网络通信模块源码分析
  • 第9章 服务其他模块设计
  • 🚀 全部章节.pdf 下载 (opens new window)
  • 🔥C++游戏编程入门(零基础学C++)
  • 🔥使用C++17从零开发一个调试器 (opens new window)
  • 🔥使用C++20从零构建一个完整的低延迟交易系统 (opens new window)
  • 🔥使用C++从零写一个C语言编译器 (opens new window)
  • 🔥从零用C语言写一个Redis
  • leveldb源码分析
  • libevent源码分析
  • Memcached源码分析
  • TeamTalk源码分析
  • 优质源码分享 (opens new window)
  • 🔥远程控制软件gh0st源码分析
  • 🔥Windows 10系统编程
  • 🔥Linux 5.x内核开发与调试 完全指南 (opens new window)
  • TCP源码实现超详细注释版.pdf (opens new window)
  • 高效Go并发编程
  • Go性能调优
  • Go项目架构设计
  • 🔥使用Go从零开发一个数据库
  • 🔥使用Go从零开发一个编译器 (opens new window)
  • 🔥使用Go从零开发一个解释器 (opens new window)
Rust编程指南
  • SQL零基础指南
  • MySQL开发与调试指南
GitHub (opens new window)
  • 前言
  • 第1章:C++23中可变参数功能的潜力
  • 第2章:函数与lambda表达式变形
  • 第3章:掌控低级输入输出操作
    • 概述
    • 精确掌控文件描述符
      • 打开和管理文件描述符
      • 控制文件访问和非阻塞操作
      • 使用文件描述符进行读写操作
      • 精确处理文件系统资源
    • 通过直接文件操作突破界限
      • 在原始级别读写字节
      • 为直接文件操作管理缓冲区
      • 定位和操作文件偏移量
      • 与存储设备的直接交互
    • 简化低级输入/输出流
      • 低级输入/输出流概述
      • 示例程序:将文件描述符包装在流缓冲区中
      • 使用underflow()读取
      • 使用overflow()写入
      • 使用sync()刷新
    • 高级流缓冲区与输入输出流定制
      • 为提升性能进行流缓冲区管理
      • 为复杂I/O定制std::streambuf
      • 总结
  • 第4章:掌握缓冲与异步IO
  • 第5章:优化内存管理
  • 第6章:优化内存性能
  • 第7章:面向专家的高级多线程编程
  • 第8章:线程同步与原子操作精通
  • 第9章:优化浮点数和整数运算
  • 后记
目录

第3章:掌控低级输入输出操作

# 第3章:掌控低级输入/输出操作

# 概述

本章重点是熟练掌握低级输入/输出(I/O)操作。涵盖的主题将使你能够精确控制程序与文件和数据流的交互方式。我们首先介绍文件描述符(file descriptor),它是低级I/O的基本组成部分。你将学习如何精确管理和操作文件描述符,这能让你比使用高级抽象更深入地与文件系统和资源进行交互。

接下来,我们将探讨直接文件操作,这超越了传统的文件I/O。本节将介绍如何绕过标准缓冲机制直接访问数据,从而提高性能并实现对文件操作的细粒度控制。然后,我们将对低级I/O流进行优化。你将学习如何简化数据传输过程,使I/O操作运行得更快、更高效。

最后,我们将研究自定义流缓冲区和微调I/O流的高级技术。在本章结束时,你将了解如何高效管理低级I/O操作,从而完全掌控文件和流的处理。

# 精确掌控文件描述符

文件描述符是一个小整数,代表一个打开的文件、套接字或管道。在低级I/O操作中,文件描述符是类UNIX系统中管理输入和输出的重要组成部分。C++23虽然主要是高级语言,但它能与文件描述符等系统级资源无缝集成,使你能够更直接、更可控地与操作系统的I/O子系统进行交互。

文件描述符让你可以细粒度地控制数据如何从这些资源读取或写入,支持非阻塞操作、资源监控,以及在对性能要求严苛的应用程序中更高效地处理I/O。现在,我们将探索一些掌握文件描述符的高级技术。

# 打开和管理文件描述符

处理文件描述符的核心操作之一是打开文件。虽然C++流使用诸如std::ifstream和std::ofstream等抽象来处理文件输入和输出,但文件描述符提供了一种更直接的方式,通过像open()和close()这样的系统调用来与文件交互。<fcntl.h>中的open()函数用于打开文件,并返回一个文件描述符,这个整数将作为对文件执行读/写操作的句柄。

以下是在C++中打开和管理文件描述符的实际示例:

#include <iostream>
#include <fcntl.h> // For open()
#include <unistd.h> // For close()

int main() {
    // Open a file with read-only permissions
    int fd = open("example.txt", O_RDONLY);
    // Check if the file descriptor is valid
    if (fd == -1) {
        std::cerr << "Failed to open file" << std::endl;
        return 1;
    }
    std::cout << "File opened with file descriptor: " << fd << std::endl;
    // Perform operations with the file descriptor (e.g., reading, writing)
    // Close the file descriptor
    if (close(fd) == -1) {
        std::cerr << "Failed to close file descriptor" << std::endl;
        return 1;
    }
    std::cout << "File descriptor closed successfully" << std::endl;

    return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

在上述代码中,open()函数用于以只读模式打开example.txt文件,并返回一个文件描述符。如果文件成功打开,将打印文件描述符,随后使用close()函数关闭它,以释放与之关联的系统资源。

在处理文件描述符时,确保正确管理它们至关重要——必须关闭文件以避免资源泄漏,未能关闭文件描述符可能会导致内存或资源耗尽,尤其是在打开许多文件或连接的应用程序中。

# 控制文件访问和非阻塞操作

使用文件描述符的最大优势之一是能够控制文件的访问方式。通过向open()系统调用传递不同的标志(flag),你可以指定文件在I/O操作期间的行为。例如,你可以以只读、只写或读写模式打开文件。

另一个概念是非阻塞I/O,这意味着当你尝试从文件(或套接字)读取或写入数据时,如果数据不能立即使用,该操作不会导致程序等待。相反,系统会立即返回,让程序继续执行其他任务。文件描述符使你能够为对性能敏感的应用程序(如网络服务器或需要实时响应的应用程序)执行非阻塞I/O操作。

为了更好地理解,我们将修改前面的示例,展示如何以非阻塞模式打开文件:

#include <iostream>
#include <fcntl.h>
#include <unistd.h>

int main() {
    // Open the file in read-only and non-blocking mode
    int fd = open("example.txt", O_RDONLY | O_NONBLOCK);
    // Check if the file descriptor is valid
    if (fd == -1) {
        std::cerr << "Failed to open file in non-blocking mode" << std::endl;
        return 1;
    }
    std::cout << "File opened in non-blocking mode with file descriptor: " << fd << std::endl;
    // Perform non-blocking operations with the file descriptor (e.g., read, write)
    // Close the file descriptor
    if (close(fd) == -1) {
        std::cerr << "Failed to close file descriptor" << std::endl;
        return 1;
    }
    std::cout << "File descriptor closed successfully" << std::endl;

    return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

在这里,O_NONBLOCK标志被传递给open()函数。这个标志指示系统以非阻塞模式打开文件,这意味着后续的读/写操作在数据不可用时不会阻塞程序。当处理大文件、管道或套接字时,这种技术非常有用,因为I/O延迟可能会导致延迟。

# 使用文件描述符进行读写操作

打开文件描述符(file descriptor)后,就可以使用read()和write()系统调用来直接对文件执行输入/输出(I/O)操作。这些系统调用提供了对文件系统的底层访问,允许以高效的方式读取和写入原始数据。下面看看如何使用文件描述符对文件进行读写。

以下是一个使用read()从文件读取数据的示例程序:

#include <iostream> 
#include <fcntl.h>    
#include <unistd.h>  
int main() {
    // 以只读模式打开文件
    int fd = open("example.txt", O_RDONLY); 
    if (fd == -1) {
        std::cerr << "Failed to open file" << std::endl; 
        return 1;
    }
    // 用于存储从文件读取的数据的缓冲区
    char buffer[128];
    // 从文件描述符读取最多128字节的数据
    ssize_t bytesRead = read(fd, buffer, sizeof(buffer) - 1); 
    if (bytesRead == -1) {
        std::cerr << "Failed to read from file" << std::endl; 
        close(fd);
        return 1; 
    }
    // 在缓冲区末尾添加空字符并打印内容
    buffer[bytesRead] = '\0';
    std::cout << "Read " << bytesRead << " bytes: " << buffer << std::endl;
    // 关闭文件描述符
    close(fd);
    return 0; 
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

在上述程序中,打开了一个文件,并使用read()系统调用将最多128字节的数据读取到缓冲区中。read()函数返回成功读取的字节数,然后打印缓冲区内容以显示文件内容。

同样,使用文件描述符写入文件也很简单。以下示例程序展示了如何向文件写入数据:

#include <iostream> 
#include <fcntl.h>    
#include <unistd.h>  
int main() {
    // 以只写模式打开文件(如果文件不存在则创建)
    int fd = open("output.txt", O_WRONLY | O_CREAT, 0644); 
    if (fd == -1) {
        std::cerr << "Failed to open file for writing" << std::endl;
        return 1; 
    }
    // 要写入文件的数据
    const char* data = "Hello, File Descriptors!";
    // 将数据写入文件
    ssize_t bytesWritten = write(fd,data, strlen(data)); 
    if (bytesWritten == -1) {
        std::cerr << "Failed to write to file" << std::endl; 
        close(fd);
        return 1; 
    }
    std::cout << "Wrote " << bytesWritten << " bytes to file" << std::endl;
    // 关闭文件描述符
    close(fd);
    return 0; 
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

在上述程序中,open()函数打开output.txt文件进行写入。如果文件不存在,会为文件所有者创建具有读写权限,为组和其他用户创建具有读权限(0644)的文件。write()函数将字符串“Hello, File Descriptors!”写入文件,并打印写入的字节数。

# 精确处理文件系统资源

文件描述符不仅限于文件访问,它们还可以表示其他资源,如套接字(sockets)、管道(pipes)和设备。在需要处理多个I/O流的服务器应用程序或实时系统中,精确管理这些资源至关重要。处理文件描述符的一种高级技术是使用select()或poll()来监控多个文件描述符的事件,比如数据可读或可无阻塞写入。

以下是使用select()监控多个文件描述符的简要示例:

#include <iostream>
#include <sys/select.h> 
#include <fcntl.h>
#include <unistd.h> 
int main() {
    // 打开两个文件进行监控
    int fd1 = open("file1.txt", O_RDONLY | O_NONBLOCK); 
    int fd2 = open("file2.txt", O_RDONLY | O_NONBLOCK); 
    if (fd1 == -1 || fd2 == -1) {
        std::cerr << "Failed to open files" << std::endl;
        return 1;
    }
    // 设置文件描述符集
    fd_set readfds;
    FD_ZERO(&readfds);
    FD_SET(fd1, &readfds); 
    FD_SET(fd2, &readfds);
    // 监控两个文件描述符是否可读
    int max_fd = std::max(fd1, fd2);
    int result = select(max_fd + 1, &readfds,nullptr, nullptr, nullptr);
    if (result == -1) {
        std::cerr << "Error with select()" << std::endl; 
        close(fd1);
        close(fd2); 
        return 1;
    }
    if (FD_ISSET(fd1, &readfds)) {
        std::cout << "Data available in file1.txt" << std::endl; 
    }
    if (FD_ISSET(fd2, &readfds)) {
        std::cout << "Data available in file2.txt" << std::endl; 
    }
    // 关闭文件描述符
    close(fd1);
    close(fd2);
    return 0; 
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

这里,select()函数用于监控两个文件描述符(fd1和fd2)。程序会等待,直到其中一个文件准备好读取。总的来说,通过精确控制文件访问模式、非阻塞操作,以及像read()、write()和select()这样的直接系统级调用,在非常需要高效I/O处理的环境中,可以优化性能。

# 通过直接文件操作突破界限

在字节级别进行直接文件操作,能让你精确控制程序与存储设备的交互方式。当你需要优化输入/输出(I/O)性能、高效处理原始数据,或者在不依赖高级抽象的情况下访问文件的特定部分时,这种控制就显得尤为重要。在本节中,我们将重点介绍如何通过处理原始文件描述符(raw file descriptors)、管理缓冲区(buffers)以及实现对文件操作的细粒度控制,来直接操作文件。直接文件操作在对性能要求极高的应用程序中,或者在处理非标准文件格式、二进制数据或低级设备接口时特别有用。你可以绕过高级I/O抽象(如流)带来的开销,直接与底层文件系统交互,这使得它非常适合需要完全掌控数据读写方式的场景。

# 在原始级别读写字节

直接文件操作的核心在于处理原始字节的能力。我们不使用高级文件I/O机制,而是依靠像read()和write()这样的系统调用,在字节级别执行操作。这样一来,我们就能从特定偏移量开始,准确读写所需数量的字节,而无需依赖像std::ifstream或std::ofstream这类高级抽象所使用的自动缓冲功能。

现在,我们先编写一个小程序,在字节级别直接读写文件:

#include <iostream> 
#include <fcntl.h>
#include <unistd.h>
#include <cstring> // 用于memset()
int main() {
    // 打开一个文件用于读写
    int fd = open("data.bin", O_RDWR | O_CREAT, 0644);
    
    if (fd == -1) {
        std::cerr << "Failed to open file" << std::endl;
        return 1; 
    }
    // 用于存储数据的缓冲区
    char buffer[10];
    // 用值0xAA填充缓冲区(仅用于演示目的)
    memset(buffer, 0xAA, sizeof(buffer)); 
    // 将缓冲区内容写入文件
    ssize_t bytesWritten = write(fd, buffer, sizeof(buffer));
    
    if (bytesWritten == -1) {
        std::cerr << "Failed to write to file" << std::endl; 
        close(fd);
        return 1; 
    }
    std::cout << "Wrote " << bytesWritten << " bytes to the file." << std::endl;
    // 将文件偏移量设置到文件开头
    lseek(fd, 0, SEEK_SET);
    // 清空缓冲区以便读取
    memset(buffer, 0, sizeof(buffer)); 
    // 从文件中读取数据
    ssize_t bytesRead = read(fd, buffer, sizeof(buffer)); 
    if (bytesRead == -1) {
        std::cerr << "Failed to read from file" << std::endl; 
        close(fd);
        return 1; 
    }
    std::cout << "Read " << bytesRead << " bytes from the file." << std::endl;
    // 以十六进制格式打印读取的数据
    for (ssize_t i = 0; i < bytesRead; ++i) {
        std::cout << "0x" << std::hex << (int)(unsigned char)buffer[i] << " ";
    }
    std::cout << std::endl;
    // 关闭文件描述符
    close(fd);
    return 0; 
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46

在上述示例中,我们打开一个二进制文件data.bin用于读写。我们创建一个10字节的缓冲区,并使用memset()函数将其填充为值0xAA(这只是一个用于说明的模式)。然后,我们使用write()系统调用,直接将缓冲区的内容写入文件,绕过了任何高级抽象。写入之后,我们使用lseek()重置文件偏移量,将文件指针移回开头,再使用read()将数据读回缓冲区。

你得到的输出展示了原始字节级别的操作,以十六进制格式准确显示了写入文件和从文件读取的内容。

# 为直接文件操作管理缓冲区

缓冲区本质上是一个内存空间,数据在写入文件之前或从文件读取之后会临时存储在这里。缓冲区的大小和管理方式会显著影响性能。例如,较小的缓冲区大小可能会导致更频繁的系统调用,从而增加开销;而较大的缓冲区虽然可以减少调用次数,但需要更多的内存。与高级I/O不同,在直接文件操作中,缓冲区管理需要你自己完成,而不是由系统自动处理。

我们将通过实现动态缓冲区管理来改进程序,根据文件大小分配缓冲区:

#include <iostream> 
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h> // 用于获取文件大小
int main() {
    // 打开文件用于读取
    int fd = open("data.bin", O_RDONLY);
    
    if (fd == -1) {
        std::cerr << "Failed to open file" << std::endl;
        return 1; 
    }
    // 使用fstat()获取文件大小
    struct stat fileInfo;
    if (fstat(fd, &fileInfo) == -1) {
        std::cerr << "Failed to get file size" << std::endl; 
        close(fd);
        return 1; 
    }
    off_t fileSize = fileInfo.st_size;
    std::cout << "File size: " << fileSize << " bytes" << std::endl;
    // 根据文件大小动态分配缓冲区
    char* buffer = new char[fileSize];
    // 将整个文件读入缓冲区
    ssize_t bytesRead = read(fd, buffer, fileSize); 
    if (bytesRead == -1) {
        std::cerr << "Failed to read from file" << std::endl; 
        delete[] buffer;
        close(fd);
        return 1; 
    }
    std::cout << "Read " << bytesRead << " bytes from the file." << std::endl;
    // 以十六进制格式打印读取的数据
    for (ssize_t i = 0; i < bytesRead; ++i) {
        std::cout << "0x" << std::hex << (int)(unsigned char)buffer[i] << " ";
    }
    std::cout << std::endl;
    // 清理资源
    delete[] buffer; 
    close(fd);
    return 0; 
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

在这里,我们使用fstat()来确定文件data.bin的大小,然后动态分配一个足够大的缓冲区,以容纳整个文件的内容。这种方法让你能够控制缓冲区大小和内存使用情况,确保可以根据文件大小和内存限制来优化性能。

# 定位和操作文件偏移量

直接文件操作的另一个重要方面是控制文件偏移量。这种能力对于随机访问文件操作至关重要,例如在大型数据库文件中操作特定记录,或者更新二进制文件的某些部分。

下面是一个示例程序,展示了如何将文件指针移动到不同位置,并操作特定字节:

#include <iostream>
#include <fcntl.h>
#include <unistd.h>
int main() {
    // 打开文件用于读写
    int fd = open("data.bin", O_RDWR); 
    if (fd == -1) {
        std::cerr << "Failed to open file" << std::endl;
        return 1; 
    }
    // 移动文件指针到文件的第5个字节处
    if (lseek(fd, 5, SEEK_SET) == -1) {
        std::cerr << "Failed to seek to position" << std::endl; 
        close(fd);
        return 1; 
    }
    // 从第5个字节位置读取1个字节
    char byte;
    if (read(fd, &byte, 1) == -1) {
        std::cerr << "Failed to read byte" << std::endl;
        close(fd);
        return 1; 
    }
    std::cout << "Byte at position 5: 0x" << std::hex << (int)(unsigned char)byte << std::endl;
    // 修改字节并写回
    byte = 0xFF; // 将值改为0xFF
    lseek(fd, 5, SEEK_SET); // 移回第5个字节
    if (write(fd, &byte, 1) == -1) {
        std::cerr << "Failed to write byte" << std::endl; 
        close(fd);
        return 1; 
    }
    std::cout << "Modified byte at position 5." << std::endl;
    // 关闭文件描述符
    close(fd);
    return 0; 
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

在上述示例中,我们使用lseek()将文件指针移动到文件的第5个字节处。然后读取该位置的字节,将其修改为0xFF,再将新值写回文件。这种技术常用于低级设备交互,或者在处理需要在文件内精确定位的自定义文件格式时使用。

# 与存储设备的直接交互

除了处理常规文件外,直接文件操作技术还可用于与原始存储设备进行交互。例如,在类UNIX系统中,诸如硬盘驱动器之类的存储设备被视为文件,你可以使用文件描述符打开它们并执行字节级的输入/输出操作。在构建诸如磁盘分区工具、文件系统检查器或低级设备驱动程序等实用程序时,这非常有用。

以下是一个简单示例,展示如何打开并读取原始存储设备:

#include <iostream>
#include <fcntl.h>
#include <unistd.h>

int main() {
    // 打开原始设备(替换为实际设备路径,例如/dev/sda)
    int fd = open("/dev/sda", O_RDONLY);
    if (fd == -1) {
        std::cerr << "Failed to open the device" << std::endl;
        return 1;
    }

    // 读取前512字节(通常是一个扇区的大小)
    char buffer[512];
    if (read(fd, buffer, sizeof(buffer)) == -1) {
        std::cerr << "Failed to read from device" << std::endl;
        close(fd);
        return 1;
    }

    std::cout << "Read 512 bytes from the device." << std::endl;
    // 以十六进制格式打印前16字节
    for (int i = 0; i < 16; ++i) {
        std::cout << "0x" << std::hex << (int)(unsigned char)buffer[i] << " ";
    }
    std::cout << std::endl;
    // 关闭设备
    close(fd);

    return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

在上述示例中,我们打开原始设备/dev/sda并读取前512字节,这通常对应一个扇区。这是一个在存储设备上执行直接字节级输入/输出的简单示例,让你可以完全控制与硬件的交互方式。

# 简化低级输入/输出流

# 低级输入/输出流概述

低级输入/输出流是指与文件、套接字和其他系统资源进行直接且无缓冲的交互,通常使用文件描述符或其他特定于系统的标识符。C++提供了高级输入/输出抽象,例如用于文件输入/输出操作的std::ifstream和std::ofstream。这些通常用于文本或二进制数据,提供自动缓冲、类型安全和易用性。另一方面,低级输入/输出操作(如使用read()、write()和文件描述符的操作)在性能方面提供了更多控制,但缺乏高级输入/输出的便利性。

在处理低级输入/输出流时,我们通常处理的是原始数据,没有格式化且未缓冲。这让我们对数据的处理方式有更多控制,但也需要更多地手动管理缓冲区、文件描述符和错误处理。然而,当我们需要与要求使用基于C++流的API(如std::ostream或std::istream)的库或应用程序部分进行交互时,可以通过将低级操作与C++流相结合来简化交互过程。

为了实现这一点,我们需要了解C++流在内部是如何工作的。在下面的示例程序中,我们创建了一个自定义流缓冲区类,用于从文件描述符读取数据和向文件描述符写入数据。这使我们能够在保留文件描述符提供的低级控制的同时,使用C++流(std::istream、std::ostream)进行文件输入/输出。

# 示例程序:将文件描述符包装在流缓冲区中

以下是实现方法:

#include <iostream>
#include <streambuf>
#include <unistd.h>
#include <fcntl.h>

// 与文件描述符配合使用的自定义流缓冲区
class FdStreamBuffer : public std::streambuf {
public:
    FdStreamBuffer(int fd) : fd_(fd) {
        setg(in_buffer_, in_buffer_, in_buffer_);
        setp(out_buffer_, out_buffer_ + buffer_size_);
    }
protected:
    // 重写underflow()以处理从文件描述符的读取
    int underflow() override {
        if (gptr() == egptr()) { // 检查缓冲区是否为空
            ssize_t bytesRead = read(fd_, in_buffer_, buffer_size_);
            if (bytesRead <= 0) {
                return traits_type::eof(); // 如果没有更多数据,返回EOF
            }
            setg(in_buffer_, in_buffer_, in_buffer_ + bytesRead);
        }
        return traits_type::to_int_type(*gptr());
    }
    // 重写overflow()以处理向文件描述符的写入
    int overflow(int ch) override {
        if (ch!= traits_type::eof()) {
            *pptr() = ch;
            pbump(1);
        }
        return sync() == 0? ch : traits_type::eof();
    }
    // 重写sync()以将缓冲区内容刷新到文件描述符
    int sync() override {
        ssize_t bytesWritten = write(fd_, pbase(), pptr() - pbase());
        if (bytesWritten < 0) {
            return -1; // 出错时返回 -1
        }
        setp(out_buffer_, out_buffer_ + buffer_size_);
        return 0;
    }
private:
    static constexpr std::size_t buffer_size_ = 1024;
    int fd_;
    char in_buffer_[buffer_size_];
    char out_buffer_[buffer_size_];
};

// 辅助函数,为文件描述符创建输入流
std::istream createInputStream(int fd) {
    static FdStreamBuffer buffer(fd);
    return std::istream(&buffer);
}

// 辅助函数,为文件描述符创建输出流
std::ostream createOutputStream(int fd) {
    static FdStreamBuffer buffer(fd);
    return std::ostream(&buffer);
}

int main() {
    // 使用文件描述符打开文件
    int fd = open("example.txt", O_RDWR | O_CREAT, 0644);
    if (fd == -1) {
        std::cerr << "Failed to open file" << std::endl;
        return 1;
    }
    // 使用自定义缓冲区创建C++输入流和输出流
    std::istream in = createInputStream(fd);
    std::ostream out = createOutputStream(fd);
    // 使用输出流向文件写入数据
    out << "Writing data using std::ostream with file descriptor integration!" << std::endl;
    out.flush(); // 确保数据写入文件
    // 将文件指针重置到开头
    lseek(fd, 0, SEEK_SET);
    // 使用输入流从文件读取数据
    std::string line;
    std::getline(in, line);
    std::cout << "Read from file: " << line << std::endl;
    // 关闭文件描述符
    close(fd);

    return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84

在上述程序中,我们创建了一个自定义流缓冲区类FdStreamBuffer,用于处理低级文件描述符操作。该类重写了std::streambuf的关键函数,以管理对文件描述符的读取和写入,具体如下:

# 使用underflow()读取

当输入缓冲区为空时,会调用此方法。它从文件描述符将数据读取到内部缓冲区,并将此数据提供给输入流。如果没有更多数据可读(到达文件末尾),它会返回traits_type::eof()以表示输入结束。

# 使用overflow()写入

此方法处理向输出缓冲区写入单个字符的操作。如果缓冲区已满,它会将内容刷新到文件描述符并重置缓冲区。

# 使用sync()刷新

当输出缓冲区已满或流被显式刷新时,sync()使用write()系统调用将缓冲的数据写入文件描述符。

这种将低级控制与高级易用性相结合的方式兼具两者的优点。你既可以访问原始文件描述符操作,又能受益于C++流抽象的灵活性和易用性。这在既需要性能又需要高级输入/输出处理便利性的应用程序中特别有用,例如日志系统、网络服务器和数据处理管道。

# 高级流缓冲区与输入输出流定制

现在,我们将重点转向掌握流缓冲区管理,尤其是在对性能优化至关重要的高输入输出(I/O)环境中。C++ 流的核心是由std::streambuf类支持的,该类负责处理实际的输入和输出操作。在高I/O环境中,如文件服务器、网络应用程序或大型数据处理系统,高效的缓冲区管理会显著影响吞吐量和性能。

在本主题中,我们将首先探究掌握流缓冲区管理如何优化性能,然后学习如何根据特定的I/O需求定制std::streambuf。

# 为提升性能进行流缓冲区管理

在高I/O环境中,管理不善的缓冲区会导致频繁且不必要的I/O操作,进而降低性能。相反,管理良好的缓冲区会尽量减少这些操作,让你能够一次性读取或写入更大的数据块,降低开销并提高吞吐量。

为了说明这一点,我们将回顾上一主题中的自定义FdStreamBuffer类,并对其进行修改,以在高I/O场景中获得更好的性能。

#include <iostream>
#include <streambuf>
#include <unistd.h>
#include <fcntl.h>

class HighPerformanceStreamBuffer : public std::streambuf {
public:
    HighPerformanceStreamBuffer(int fd, std::size_t bufferSize = 8192)
        : fd_(fd), buffer_size_(bufferSize), buffer_(new char[bufferSize]) {
        setg(buffer_.get(), buffer_.get(), buffer_.get());
        setp(buffer_.get(), buffer_.get() + buffer_size_);
    }

    ~HighPerformanceStreamBuffer() {
        sync(); // 确保任何待处理的输出都被刷新
    }

protected:
    // 处理从文件描述符读取数据到缓冲区
    int underflow() override {
        if (gptr() == egptr()) { // 检查读取区域是否为空
            ssize_t bytesRead = read(fd_, buffer_.get(), buffer_size_);
            if (bytesRead <= 0) {
                return traits_type::eof(); // 文件结束或出错
            }
            setg(buffer_.get(), buffer_.get(), buffer_.get() + bytesRead);
        }
        return traits_type::to_int_type(*gptr());
    }

    // 处理从缓冲区写入数据到文件描述符
    int overflow(int ch) override {
        if (ch!= traits_type::eof()) {
            *pptr() = ch;
            pbump(1);
        }
        return sync() == 0? ch : traits_type::eof();
    }

    // 将缓冲区刷新到文件描述符
    int sync() override {
        ssize_t bytesWritten = write(fd_, pbase(), pptr() - pbase());
        if (bytesWritten < 0) {
            return -1; // 写入时出错
        }
        setp(buffer_.get(), buffer_.get() + buffer_size_);
        return 0;
    }

private:
    int fd_;
    std::size_t buffer_size_;
    std::unique_ptr<char[]> buffer_;
};

// 辅助函数,用于创建输入流和输出流
std::istream createHighPerformanceInputStream(int fd, std::size_t bufferSize = 8192) {
    static HighPerformanceStreamBuffer buffer(fd, bufferSize);
    return std::istream(&buffer);
}

std::ostream createHighPerformanceOutputStream(int fd, std::size_t bufferSize = 8192) {
    static HighPerformanceStreamBuffer buffer(fd, bufferSize);
    return std::ostream(&buffer);
}

int main() {
    // 使用文件描述符打开一个文件
    int fd = open("largefile.txt", O_RDWR | O_CREAT, 0644);
    if (fd == -1) {
        std::cerr << "Failed to open file" << std::endl;
        return 1;
    }

    // 创建具有大缓冲区大小的高性能流
    std::istream in = createHighPerformanceInputStream(fd, 16384); // 16KB输入缓冲区
    std::ostream out = createHighPerformanceOutputStream(fd, 16384); // 16KB输出缓冲区

    // 向文件写入数据
    out << "This is a large file. Writing efficiently with a custom stream buffer!" << std::endl;
    out.flush(); // 确保数据被写入
    // 将文件指针重置到开头
    lseek(fd, 0, SEEK_SET);

    // 从文件读取数据
    std::string line;
    std::getline(in, line);
    std::cout << "Read from file: " << line << std::endl;
    // 关闭文件描述符
    close(fd);

    return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93

在上述程序中,我们对FdStreamBuffer进行了增强,使其能够处理更大的缓冲区大小,这在高I/O环境中非常重要,因为读取或写入小块数据可能会导致性能瓶颈。缓冲区是根据bufferSize参数动态分配的。在main()函数中,我们创建了用于读取和写入的、缓冲区大小为16KB的高性能输入流和输出流。在处理大文件或高吞吐量环境时,这显著提高了性能,因为I/O操作被批量处理成更大的数据块。

# 为复杂I/O定制std::streambuf

除了为提升性能管理缓冲区之外,定制std::streambuf还能让你处理标准流类可能无法适应的独特I/O场景。例如,你可能需要:

  • 从文件、套接字或内存缓冲区的组合中读取数据。
  • 创建一个从一个源读取数据并写入到另一个源的流缓冲区,比如从文件读取并写入到套接字的网络流。
  • 在读取或写入数据时对其进行操作或过滤,比如在将数据写入文件之前对其进行压缩。

为此,我们现在将创建一个自定义的std::streambuf,它可以从多个输入源(如两个不同的文件)读取数据,并将它们的数据交错到单个流中。

#include <iostream>
#include <streambuf>
#include <unistd.h>
#include <fcntl.h>

// 从两个不同文件描述符读取数据的自定义流缓冲区
class InterleavedStreamBuffer : public std::streambuf {
public:
    InterleavedStreamBuffer(int fd1, int fd2)
        : fd1_(fd1), fd2_(fd2), current_fd_(fd1_) {
        setg(buffer_, buffer_, buffer_);
    }

protected:
    // 以交错方式从两个文件读取数据
    int underflow() override {
        if (gptr() == egptr()) { // 缓冲区为空
            ssize_t bytesRead = read(current_fd_, buffer_, buffer_size_);
            if (bytesRead <= 0) {
                return traits_type::eof(); // 文件结束或出错
            }
            setg(buffer_, buffer_, buffer_ + bytesRead);

            // 切换两个文件描述符以进行交错读取
            current_fd_ = (current_fd_ == fd1_)? fd2_ : fd1_;
        }
        return traits_type::to_int_type(*gptr());
    }

private:
    static constexpr std::size_t buffer_size_ = 1024;
    int fd1_, fd2_;
    int current_fd_;
    char buffer_[buffer_size_];
};

// 辅助函数,用于创建交错读取的输入流
std::istream createInterleavedInputStream(int fd1, int fd2) {
    static InterleavedStreamBuffer buffer(fd1, fd2);
    return std::istream(&buffer);
}

int main() {
    // 打开两个文件进行读取
    int fd1 = open("file1.txt", O_RDONLY);
    int fd2 = open("file2.txt", O_RDONLY);
    if (fd1 == -1 || fd2 == -1) {
        std::cerr << "Failed to open files" << std::endl;
        return 1;
    }

    // 创建一个交错读取两个文件数据的输入流
    std::istream in = createInterleavedInputStream(fd1, fd2);

    // 从交错流中读取数据
    std::string line;
    while (std::getline(in, line)) {
        std::cout << "Interleaved line: " << line << std::endl;
    }

    // 关闭文件描述符
    close(fd1);
    close(fd2);

    return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66

在上述程序中,我们创建了一个自定义的InterleavedStreamBuffer,它以交替或交错的方式从两个文件描述符(fd1_和fd2_)读取数据。每次缓冲区为空时,它会从一个文件读取数据,然后切换到另一个文件进行下一次读取。这使你能够将来自多个源的数据交错到单个输入流中。无论是在高I/O环境中优化性能、交错来自多个源的数据,还是实时转换数据,定制std::streambuf都能让你完全控制应用程序中输入和输出的处理方式。这使你能够构建即使在苛刻条件下也能表现出色的先进高效系统。

# 总结

最终,本章深入探讨了C++ 低级I/O操作的内部机制,以及如何利用它们实现对文件处理的精确控制。主要目的是了解文件描述符,以及它们如何让你在字节级别直接编辑文件。通过实际示例展示了文件描述符提供了一种更全面的系统级文件处理方式,能够实现高效的读写操作、对文件偏移量的精确控制以及无阻塞操作。本章还讨论了用于高性能I/O的缓冲区管理。我们学习了如何使用自定义的std::streambuf实现,将低级文件描述符与高级C++ 流相结合。本章还涵盖了针对特定输入和输出场景定制std::streambuf,比如交错来自多个源的数据以及管理对性能要求严格的流。总体而言,本章在掌握流缓冲区、在I/O密集型应用程序中优化性能,以及针对特定复杂场景定制C++ 流处理方面提供了大量建议。

第2章:函数与lambda表达式变形
第4章:掌握缓冲与异步IO

← 第2章:函数与lambda表达式变形 第4章:掌握缓冲与异步IO→

最近更新
01
C++语言面试问题集锦 目录与说明
03-27
02
第四章 Lambda函数
03-27
03
第二章 关键字static及其不同用法
03-27
更多文章>
Copyright © 2024-2025 沪ICP备2023015129号 张小方 版权所有
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式