disgare 的博客
首页
博客
分类
标签
首页
博客
分类
标签
  • 网络

    • 计算机网络学习笔记
    • 网络安全相关
    • 域名和子网掩码
    • CORS 跨域资源共享
    • DNS、HTTP 与 HTTPS
    • Server-Sent Events (SSE)
    • WebSocket 长连接
  • 计算机基础

    • 操作系统 IO 相关知识
      • 阻塞与非阻塞
      • 同步与异步
      • IO 和系统调用
        • 传统的 IO
        • DMA
        • buffer 和 cache
      • 零拷贝技术
        • 传统拷贝
        • mmap 内存映射
        • sendfile
        • splice
      • 常用的 IO 模型
        • BIO:同步阻塞 IO
        • NIO:同步非阻塞 IO
        • IO 多路复用
        • 信号驱动 IO
        • AIO:异步 IO 模型
      • IO 多路复用机制
        • select
        • poll
        • epoll
    • 操作系统学习笔记
    • 程序的机器级表示
    • 音频文件基础
    • 正则表达式相关概念
    • ffmpeg 的安装以及实现音频切分功能
    • Hex 和 Base64 编码
    • XML 的使用
  • 数据结构与算法

    • 动态规划算法学习笔记
    • 基于比较的排序算法的最坏情况下的最优下界为什么是O(nlogn)
    • 集合与数据结构学习笔记
    • 面试常见算法总结
    • 算法导论第二部分排序学习笔记
    • 算法导论第一部分学习笔记
  • Java

    • 对象之间的映射与转换
    • 反射学习笔记
    • 泛型相关概念
    • 关于 boolean 类型的坑
    • 如何使用 lambda 表达式实现排序
    • CompletableFuture 相关用法
    • CompletableFuture 源码浅要阅读
    • FutureTask 源码阅读
    • Guava 常用 API
    • Guava 源码阅读:Multimap 相关
    • Jackson 的各种使用
    • Java 的 Excel 相关操作
    • java 的常见性能问题分析以及出现场景
    • java 基础知识
    • JAVA 枚举的基础和原理
    • Java 图片文件上传下载处理
    • Java 序列化
    • Java 异常
    • Java 语法糖
    • Java 中关于字符串处理的常用方法
    • Java 中强、软、弱、虚引用
    • JAVA 注解小结
    • Java Http 访问框架
    • Java Stream 的使用
    • Java8 新特性
    • netty 学习笔记
    • Scanner 的各种用法
    • Servlet 学习笔记
    • String、StringBuffer、StringBuilder 学习笔记
  • JVM

    • 虚拟机执行子系统
    • JVM 自动内存管理
    • Linux 中 JVM 常用工具以及常见问题解决思路
  • Linux

    • crontab 表达式
    • Linux 常见命令
    • Linux 文件系统
  • 中间件

    • 关于定时任务原理
    • 详解 kafka
    • ES 搜索引擎
    • flink 提交流程
    • Grape-RAG
    • Hadoop 基础原理
  • 多线程

    • 多线程基础学习笔记
    • 简单了解并发集合
    • 如何手写单例
    • 深入理解 java 多线程安全
    • 生产者消费者问题
    • 线程池作用、用法以及原理
    • AQS 组件
    • ThreadLocal 原理以及使用
  • 非关系型数据库

    • Redis 集群
    • Redis 数据结构、对象与数据库
    • Redis 学习笔记
  • 关系型数据库

    • B+ 树的插入、删除和数据页分裂机制
    • MySQL 的 binglog、redolog、undolog
    • MySQL 的记录存储结构、存储引擎与 Buffer Pool
    • MySQL 基本的特性
    • MySQL 开发规范
    • MySQL 事务与锁与 MVCC
    • MySQL 数据类型、字符集相关内容
    • MySQL 索引与索引优化
    • PostgreSQL 更新数据时 HOT优化
    • PostgreSQL 相关用法
  • Python

    • Python 基础语法
    • Python 学习
  • Spring 项目

    • Lombok 的常用注解
    • maven 小结
    • MyBatis 框架的使用
    • MyBatis 重要知识点总结
    • MybatisPlus 的使用
    • Spring 框架基础使用
    • Spring 事务相关
    • Spring IOC 的原理及源码
    • Spring AOP 的使用和原理
    • SpringBoot 的原理
    • SpringBoot 基础使用
    • SpringWeb 重要知识点
  • 分布式

    • 初步了解 docker
    • 从 ACID 到 BASE 事务处理的实现
    • 访问远程服务
    • 分布式 id
    • 分布式缓存相关问题
    • 分布式集群理论和分布式事务协议
    • 分布式架构的观测
    • 分布式一致性算法
    • 负载均衡 Load Balancing
    • 关于分布式系统 RPC 中高可用功能的实现
    • 集群间数据同步的目的
    • 三高问题下的系统优化
    • 数据库分库分表
    • 详解 Spring Cloud
    • Dubbo 基础概念
    • Gossip 协议
    • nginx 学习笔记
    • Protobuf 通信协议
    • Zookeeper 基础学习
  • 架构设计

    • 参数校验与异常处理
    • 抽象方法与设计模式
    • 代码整洁之道
    • 权限系统设计
    • 用低内存处理大量数据
    • 设计模式——策略模式
    • 设计模式——过滤器模式在 Spring 中的实践
    • 状态模式
    • 统一结果返回
    • 为什么要打日志?怎么打日志?打什么日志?
    • 运维监控常见指标含义
    • 资深研发进阶
    • DDD 架构学习笔记
    • Java 常用的规则引擎
    • MVC 架构学习笔记
  • AI

    • 如何编写 Prompt
    • Agent 工程架构
    • LLM 相关内容
    • NLP 相关知识
    • vibe coding 最佳实践
    • windows 下 ollama 迁移到 D 盘
  • 开发工具

    • 如何画时序图、流程图、状态流转图
    • excel 关于 =vlookup 的用法
    • git 的学习以及使用
    • IDEA 插件推荐
    • IDEA 常用快捷键以及调试
    • Shell 脚本
    • swagger 的使用
  • 前端

    • 简单了解前端页面开发
    • 伪静态是什么
    • GitHub Pages 部署教程
    • Vercel 部署教程
    • vue-admin-template 简单使用
    • VuePress 博客搭建指南
  • 项目

    • 面试刷题网——技术方案
    • 影视资源聚合站——技术方案
  • 问题记录

    • 定时任务单线程消费 redis 中数据导致消费能力不足
    • 提供可传递的易受攻击的依赖项
    • Liteflow 在 SpringBoot 启动时无法注入组件问题 couldn‘t find chain with the id[THEN(NodeComponent)]
  • 金融

    • 股票分析——关于电力
    • 股票技术面——量价关系
    • 股票技术面——盘口
    • 股票技术面——基础
    • 基础的金融知识
    • 基金与股票
    • 韭菜的自我总结
    • 聊聊价值投资
  • 其他

    • 程序员职场工作需要注意什么
    • 创业全链路SOP:从灵光一现到系统化增长的实战指南
    • 观罗翔讲刑法随笔
    • 价格和价值
    • 立直麻将牌效益理论
    • 梅花易数学习笔记
    • 压力管理
2024-07-08
计算机基础
目录

操作系统 IO 相关知识

IO 就是计算机内部与外部进行数据传输的过程,常见的 IO 包含网络 IO 与磁盘 IO。所有 IO 都需要系统调用,由操作系统代理执行,并经历从 IO 设备拷贝到内核空间拷到用户空间的环节。在内核收到调用请求之后,会有数据准备、数据就绪、数据拷贝的阶段。

每一个程序员面试的时候或多或少都会被问到相关知识,了解一下相关概念是非常重要的。

# 阻塞与非阻塞

阻塞与非阻塞指线程在等待调用结果(数据,消息,返回值)时的状态。

阻塞是指应用线程等待结果时会被挂起,等待内核(或者 IO)完成操作,实际上,内核所做的事情是将 CPU 时间切换给其他有需要的线程,网络应用程序在这种情况下是得不到 CPU 时间做该做的事情的,它会在那里睡觉。因此这里的阻塞应当理解成线程被其他线程阻塞,或者等待 IO 资源结果的时候阻塞

非阻塞是指该线程等待结果(数据准备)时可以执行其他操作,只有在拷贝数据的时候才会阻塞。此时该线程可以做一些其他的 CPU 计算,但是一般会伴随不停的访问请求结果这一操作

注意,阻塞与非阻塞的概念只有同步中才有,没有异步阻塞与异步非阻塞的说法,接下来说一下为什么

# 同步与异步

同步和异步关注的是消息通信机制

同步指发出调用请求后,请求跟随调用结果一起返回

异步指发出请求后,请求或者调用立即返回,在 IO 设备或者其他的机器操作完毕后,通过回调函数或者其他方式返回状态或者数据信息

比如我们使用消息队列来处理某个功能,一般来说发生消息后就不用管之后的操作了,该过程就是异步的。主线程不关心另一个线程怎么样,所以谈不上阻塞非阻塞

# IO 和系统调用

所有的 IO 操作都会有系统调用的过程,同时所有的系统 IO 都分为两个阶段:等待就绪和操作。举例来说,读函数,分为等待系统可读和真正的读;同理,写函数分为等待网卡可以写和真正的写

read(file, tmp_buf, len);
write(socket, tmp_buf, len);
1
2

为什么会这样分呢,因为当一个应用程序(位于用户空间)需要执行 I/O 操作,比如读写文件、网络通信等,它不能直接访问硬件设备,因为这样做可能会破坏系统的稳定性和安全性。因此,应用程序会通过系统调用来请求操作系统内核执行 I/O 操作。操作系统会将文件从 IO 设备(磁盘、网络)先复制到内存缓冲区,然后再将数据复制到用户空间缓冲区

需要说明的是等待就绪的阻塞是不使用 CPU 的,是在空等,这期间数据通过网线以电流的方式输入我们的电脑,并且被保存在缓冲区中;而真正的读写操作的阻塞是使用 CPU 的,即 CPU 将数据从缓冲区拷贝到目标位置的过程,这个时候 CPU 真正在干活。而且这个速度是非常快的,数据从内核缓冲区复制到用户空间缓冲区的时间通常是在纳秒(ns)到微秒(μs)级别。具体时间取决于复制的数据量和 CPU 的速度,复制几千字节的数据可能只需要几十微秒,而复制几兆字节的数据可能需要几百微秒到几毫秒(ms)。这点时间对于磁盘或者网络 IO 来说几乎可以忽略不计

# 传统的 IO

这里使用普通的 read 和 write 命令来展示一下传统 IO 的弊端

  • 用户进程通过 read 方法向操作系统发起调用,此时上下文从用户态转向内核态
  • DMA 控制器把数据从硬盘(或者套接字的缓冲区)中拷贝到内核读缓冲区
  • CPU 把内核读缓冲区数据拷贝到应用缓冲区,然后上下文从内核态转为用户态,read 和数据一起返回

其过程如下图所示: image-2026-01-31-20-38-04.png write 指令:

  • 用户进程通过 write 方法发起调用,上下文从用户态转为内核态
  • CPU 将应用缓冲区中数据拷贝到 socket 缓冲区
  • DMA 控制器把数据从 socket 缓冲区拷贝到网卡,上下文从内核态切换回用户态,write 返回

read 和 write 分别进行了两次内核态用户态切换和两次 CPU 复制操作,在操作系统中这些是可以被优化的

但是优化之前先处理一个问题,我们可以看到数据会先复制到中间的 socket buffer 中,我们很可能有一个问题就是为什么数据不能从磁盘直接复制到用户缓冲区?原因主要在下面两点:

1,权限隔离:操作系统的内核空间和用户空间是严格分离的,以防止恶意程序直接访问系统资源,造成系统不稳定或安全漏洞。内核负责管理硬件资源,包括磁盘读写,而用户空间的应用程序则运行在更高的抽象层次上 2,地址空间不同:内核空间和用户空间拥有各自的虚拟地址空间。用户缓冲区位于用户空间的虚拟地址范围内,而内核在执行I/O操作时使用的是内核缓冲区,这些缓冲区位于内核空间的地址范围内

# DMA

DMA 是 Direct Memory Access,直接内存存取,算零拷贝技术之一

DMA 是直接内存访问技术,他是一块主板上独立的芯片,是一种计算机硬件技术,允许外部设备(如网卡、硬盘、显卡)直接与系统内存进行数据传输,无需 CPU 全程参与。这种机制显著提高了数据传输效率,减少了 CPU 的负载,尤其适用于大量数据的快速传输场景 image-2026-01-31-20-49-00.png 早期计算机中,用户进程需要读取磁盘数据,需要 CPU 中断和 CPU 参与(就是整个数据的传输过程,都要需要 CPU 亲自参与搬运数据的过程,数据会先从磁盘拷贝到 CPU 寄存器,然后再写入对应的缓冲区),因此效率比较低,发起 IO 请求,每次的 IO 中断,都带来 CPU 的上下文切换。 DMA 控制器接管了数据读写请求。并且加快了 IO 拷贝速度,从而减少 CPU 的处理时间

举个例子说明:

// DMA 控制器负责数据搬运
1. 磁盘 → 内核缓冲区 (DMA直接搬运)
2. 内核缓冲区 → 应用缓冲区 (CPU拷贝)
3. 应用缓冲区 → 内核Socket缓冲区 (CPU拷贝)
4. 内核Socket缓冲区 → 网卡 (DMA直接搬运)
1
2
3
4
5

# buffer 和 cache

我们在 IO 的时候经常会遇到不同的组件速度不一样的问题,比如 CPU 运行是很快的,但是磁盘的写入或者读取速度是很慢的,因此我们需要引入中间层来兼容速度不一致的问题。一般引入 buffer 和 cache 来处理问题,但是两者的侧重点不一样

buffer:缓冲区,解决速度不匹配问题。例如,应用程序快速生成数据,而磁盘写入速度慢,缓冲区暂存数据后批量写入,避免阻塞。因此 buffer 重视处理多个组件之间读写速度不一致的问题

cache:缓存,通过存储热点数据减少对慢速设备的访问。例如,CPU 缓存避免频繁访问内存,文件缓存避免重复读磁盘,将数据从慢速的地方读出来,在快速的地方做缓存

# 零拷贝技术

# 传统拷贝

先看看普通文件传输的过程,以发送文件给网络客户端为例:

1,磁盘→内核缓冲区:从硬盘读取文件数据到内核空间。为什么要读到缓冲区中呢,我们可以想象一个现实场景:消防车(CPU)通过水管(总线)从水库(磁盘)抽水(数据)灭火(计算)

问题是消防车抽水速度是10吨/分钟,但是水库供水速度是1吨/分钟,直接连接会导致消防车90%时间在等待,所以消防车把这事情交给 DMA,让它从水库抽水,存到一个地方(缓冲区),然后消防车消费缓冲区里的数据

2,内核缓冲区→用户缓冲区:数据拷贝到应用程序内存,这一步是为了必须满足操作系统和用户空间的隔离性

3,用户缓冲区→Socket 缓冲区:应用程序再把数据拷贝到内核网络缓冲区,这一步也是为了处理网络传输和 CPU 处理速度不一致的问题

4,Socket 缓冲区→网卡:最后发送到网络。Socket 缓冲区在操作系统内核内存中,目的是临时存储待发送和接收的网络数据,而网卡是将数字信号转换为电信号或者光信号,它是物理硬件设备

# mmap 内存映射

接下来我们看看上面的过程中有哪些可以优化的点,零拷贝技术是优化拷贝次数的技术,以下的技术包括 mmap 都是零拷贝技术的一种

mmap 将用户空间的一段内存区域映射到内核空间,映射成功后,用户对这段内存区域的修改可以直接反映到内核空间,同样,内核空间对这段区域的修改也直接反映用户空间。即将用户空间和内核空间关联起来,例如堆外内存就是 mmap 零拷贝技术

内核缓冲区和应用缓冲区共享,从而减少了从内核缓冲区到用户缓冲区的一次 CPU 拷贝

# sendfile

sendfile 方法 IO 数据对用户空间完全不可见,所以只能适用于完全不需要用户空间处理的情况,比如将磁盘中的数据复制到网卡

整个过程发生了两次用户态和内核态的上下文切换和三次拷贝,数据不经过用户区,只在内核态进行复制。简单介绍一下流程,就是数据存在于内核中时,并未被真正复制到 socket 关联的缓冲区内。取而代之的是,只有记录数据位置和长度的描述符被加入到 socket 缓冲区中。DMA 模块将数据直接从内核缓冲区传递给协议引擎,从而消除了遗留的最后一次复制

Linux2.4内核版本之后对 sendfile 做了进一步优化,通过引入新的硬件支持,这个方式叫做 DMA Scatter/Gather 分散/收集功能,将整个过程优化到两次用户态和内核态的上下文切换和两次拷贝

# splice

Linux 从2.6.17支持 splice 数据从磁盘读取到 OS 内核缓冲区后,在内核缓冲区直接可将其转成内核空间其他数据 buffer,而不需要拷贝到用户空间,这是不是和 sendfile 有点像?

注意 splice 和 sendfile 的不同,sendfile 是 DMA 硬件设备不支持的情况下将磁盘数据加载到 kernel buffer(内核空间维护的缓存空间)后,需要一次 CPU copy,拷贝到 socket buffer

而 splice 是直接将两个内核空间的 buffer 进行 pipe 管道传输,不需要用户进程介入

#include <fcntl.h>
ssize_t splice(int fd_in, loff_t *off_in, int fd_out, loff_t *off_out, size_t len, unsigned int flags);
1
2

# 常用的 IO 模型

每个 IO 模型的区别 image-2026-01-31-20-49-28.png

image-2026-01-31-20-49-40.png

# BIO:同步阻塞 IO

同步阻塞 IO 模型,该模型用于描述一方发起调用后,阻塞等待另外一方返回的场景,举几个例子:

  • 应用程序发起 read 调用后,会一直阻塞,直到在内核把数据拷贝到用户空间
  • 用户请求服务器后,一直等待接口返回
  • HTTP 长轮询

当线程调用 recv() 或 read() 读取数据时,若客户端没有发送数据,线程会一直阻塞,直到以下情况发生:

  • 客户端发送数据
  • 客户端关闭连接(触发 recv() 返回 0)
  • 出现网络错误(触发异常)

以下是代码示例,平时我们可能会使用多线程来接受数据

int client_fd = accept(listen_fd, ...); // 接受新连接
char buffer[1024];
ssize_t n = recv(client_fd, buffer, sizeof(buffer), 0); // 线程在此阻塞
1
2
3

# NIO:同步非阻塞 IO

同步非阻塞 IO 模型中,该模型用于描述一方发起调用后,虽然等待该请求的结果,但是我们依然执行自己的一些逻辑流程,然后通过定时轮训来获取最终结果,此种方式有较高的 CPU 占用率

在普通的 NIO 中应用程序会一直轮询发起 read 调用(发起调用会降低 CPU 性能),在数据准备的时间中线程不挂起,直到磁盘准备好数据

数据就绪后,实际的 IO 操作会等待数据复制到应用进程的缓冲区中以后才返回

fcntl(client_fd, F_SETFL, O_NONBLOCK); // 设置非阻塞
ssize_t n = recv(client_fd, buffer, sizeof(buffer), 0);
if (n == -1 && (errno == EAGAIN || errno == EWOULDBLOCK)) {
    // 无数据,可继续处理其他逻辑
}
1
2
3
4
5

# IO 多路复用

java 中的 NIO、Radis 中的单线程、nginx 中的主进程就是 IO 多路复用(使用选择器,缓冲区、通道来实现,通道将用户数据拷贝到缓冲区中,选择器让程序读取缓冲区中数据),IO 多路复用是 NIO 的一种,但是它的好处是可以在一个线程上处理多个不同端口的监听并且不用轮询套接字

image-2026-01-31-20-50-46.png epoll 用了事件通知的方式,而不是轮询。也就是说,当某个文件描述符就绪时,内核会通知应用程序,而不是应用程序主动去检查

epoll 有三个主要的系统调用:epoll_create、epoll_ctl 和 epoll_wait。epoll_create 用来创建一个 epoll 实例,返回一个文件描述符。epoll_ctl 用来向这个实例中添加、修改或删除要监控的文件描述符及其事件类型。epoll_wait 则是等待事件的发生,返回就绪的事件列表

epoll 内部是使用红黑树和就绪队列来管理的。红黑树用来存储所有被监控的文件描述符,这样在添加、删除和查找的时候效率比较高,时间复杂度是 O(log n)。而就绪队列则用来存放已经就绪的事件,当某个文件描述符的事件发生时,内核会将其放入就绪队列中,这样 epoll_wait 只需要检查这个队列,而不需要遍历整个集合

当某个文件描述符上有数据到达时,网卡会通过中断通知内核,内核处理中断后将数据复制到对应的 socket 缓冲区,然后将对应的 epoll_item 结构体放入就绪队列。当应用程序调用 epoll_wait时,内核将就绪队列中的事件返回给用户空间,同时清空队列,以便下次等待新的事件

同时这里 epoll 使用了 mmap 内存映射做了优化,用户进程无需将数据从内核复制到用户空间中

据此我们实现了 IO 多路复用:通过一个线程(或少量线程)高效管理多个 I/O 通道(如套接字),其核心是事件驱动和资源复用

# 信号驱动 IO

这是同步 IO 的一种。信号驱动 I/O 与异步 I/O 的区别是从缓冲区获取数据这个步骤的处理,信号驱动 IO 的关键点在于,当文件描述符上有事件发生时,系统会发送一个信号(比如 SIGIO)给进程,通知进程可以开始进行 I/O 操作,将数据从内核复制到用户空间的过程还是需要阻塞,因此信号驱动 IO 还是属于同步 IO

image-2026-01-31-20-51-01.png

# AIO:异步 IO 模型

异步 IO 是基于事件或者回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,过一段时间才会收到结果。举个例子:

用户下单时会立马得到下单结果,但是要等待一段时间后才可以收到下单成功通知

# IO 多路复用机制

select,poll,epoll 都是操作系统中 IO 多路复用的机制。IO 多路复用的本质是通过某种方法,让单个进程可以监视多个描述符,当发现某个描述符就绪之后,能够通知程序进行相应的读写操作

这三种方法都是操作系统提供的,为了处理 c10k 问题。所谓 c10k 问题,就是指每台机器的并发连接过多(concurrent 10,000 connections)后,机器该如何支持

我们来说说他们的原理

# select

select 使用固定长度的数组(fd_set)存储监控的文件描述符,数组大小由宏 FD_SETSIZE 决定(默认 1024),限制了可监控的 FD 数量

应用程序将需要监控的 FD(fild descriptor,是一个套接字描述器),加入读 / 写 / 异常事件集合。调用 select 时,内核遍历所有 FD,检查是否有就绪事件

返回就绪事件的总数,但不明确具体是哪些 FD 就绪,需应用程序遍历整个集合手动判断(轮询方式),虽然已经比 read 好很多了,但是这当然可以优化的,所以有了 poll

# poll

poll 使用链表存储监控的 FD,突破了 select 的 FD 数量限制,理论上大小仅受限于系统内存

每个 pollfd 结构体(列表里的节点)包含 FD、监控事件(读 / 写 / 异常)和事件状态。调用 poll 时,内核同样遍历所有 FD 检查就绪状态。返回就绪事件的总数,需应用程序遍历整个数组判断具体就绪 FD(仍为轮询方式)

仅 Linux 可用,FD 数量限制比 select 更灵活,但性能仍受制于轮询。学过数据结构的同学都清楚,优化这种需要知道知道套接字和事件对应关系的问题,使用 map 最合适了,所以有了最终解决方案 epoll

# epoll

epoll 使用红黑树管理监控的 FD,并用链表存储就绪事件(epoll_event)

通过 epoll_ctl 将 FD 添加到内核的红黑树中,注册监控事件(读 / 写等)。 内核通过事件驱动机制(而非主动轮询),当 FD 就绪时将其加入就绪链表,应用程序只需要遍历该链表即可获取 FD、事件、数据内容等完整信息

调用 epoll_wait 时,直接返回就绪链表中的事件,无需遍历所有 FD,应用程序可直接处理就绪事件

epoll 在 Linux 2.6 内核后引入,高性能,适合高并发场景

#IO
最后更新: 2/28/2026, 11:31:29 AM
WebSocket 长连接
操作系统学习笔记

← WebSocket 长连接 操作系统学习笔记→

最近更新
01
vibe coding 最佳实践
02-24
02
立直麻将牌效益理论
02-23
03
伪静态是什么
02-08
更多文章>
Theme by Vdoing
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式