细节分析 Linux 中五种 IO 模型和三种实现方式
I/O 介绍
操作系统分为两种 I/O
网络 IO:本质是 socket 读取
磁盘 IO:DMA 操作读取
C/C++Linux 服务器开发知识点 内容包括 C/C++,Linux,Nginx,ZeroMQ,MySQL,Redis,MongoDB,ZK,流媒体,音视频开发,Linux 内核,P2P,K8S,Docker,TCP/IP,协程,DPDK 多个高级知识点,点击:C/C++Linux服务器开发高级架构师/Linux后台架构师-学习视频
每次 I/O 过程
(DMA)将数据从磁盘文件先加载至内核内存空间(缓冲区),等待数据准备完成,时间较长
(CPU)将数据从内核缓冲区复制到用户空间的进程的内存中,时间较短
I/O 模型
同步/异步:关注的是消息通信机制
同步:synchronous,调用者等待被调用者返回消息,才能继续执行
异步:asynchronous,被调用者通过状态、通知或回调机制主动通知调用者被调用者的运行状态
阻塞/非阻塞:关注调用者在等待结果返回之前所处的状态
阻塞:blocking,指 IO 操作需要彻底完成后才返回到用户空间,调用结果返回之前,调用者被挂起
非阻塞:nonblocking,指 IO 操作被调用后立即返回给用户一个状态值,无需等到 IO 操作彻底完成,最终的调用结果返回之前,调用者不会被挂起
五种 I/O 模型:
同步阻塞型、同步非阻塞型、IO 多路复用型、信号驱动 I/O 型、异步 I/O 型
同步阻塞型
同步阻塞 IO 模型是最简单的 IO 模型,用户线程在内核进行 IO 操作时被阻塞
用户线程通过系统调用 read 发起 IO 读操作,由用户空间转到内核空间。内核等到数据包到达后,然后将接收的数据拷贝到用户空间,完成 read 操作
用户需要等待 read 将数据读取到 buffer 后,才继续处理接收的数据。整个 IO 请求的过程中,用户线程是被阻塞的,这导致用户在发起 IO 请求时,不能做任何事情,对 CPU 的资源利用率不够
同步非阻塞模型
用户线程发起 IO 请求时立即返回。但并未读取到任何数据,用户线程需要不断地发起 IO 请求,直到数据到达后,才真正读取到数据,继续执行。即“轮询”机制
整个 IO 请求的过程中,虽然用户线程每次发起 IO 请求后可以立即返回,但是为了等到数据,仍需要不断地轮询、重复请求,消耗了大量的 CPU 的资源
是比较浪费 CPU 的方式,一般很少直接使用这种模型,而是在其他 IO 模型中使用非阻塞 IO 这一特性
IO 多路复用型
IO 多路复用是指内核一旦发现进程指定的一个或者多个 IO 条件准备读取,就通知该进程
多个连接共用一个等待机制,本模型会阻塞进程,但是进程是阻塞在 select 或者 poll 这两个系统调用上,而不是阻塞在真正的 IO 操作上
用户首先将需要进行 IO 操作添加到 select 中,继续执行做其他的工作(异步),同时等待 select 系统调用返回。当数据到达时,IO 被激活,select 函数返回。用户线程正式发起 read 请求,读取数据并继续执行
从流程上来看,使用 select 函数进行 IO 请求和同步阻塞模型没有太大的区别,甚至还多了添加监视 IO,以及调用 select 函数的额外操作,效率更差。并且阻塞了两次,但是第一次阻塞在 select 上时,select 可以监控多个 IO 上是否已有 IO 操作准备就绪,即可达到在同一个线程内同时处理多个 IO 请求的目的。而不像阻塞 IO 那种,一次只能监控一个 IO
虽然上述方式允许单线程内处理多个 IO 请求,但是每个 IO 请求的过程还是阻塞的(在 select 函数上阻塞),平均时间甚至比同步阻塞 IO 模型还要长。如果用户线程只是注册自己需要的 IO 请求,然后去做自己的事情,等到数据到来时再进行处理,则可以提高 CPU 的利用率
IO 多路复用是最常使用的 IO 模型,但是其异步程度还不够“彻底”,因它使用了会阻塞线程的 select 系统调用。因此 IO 多路复用只能称为异步阻塞 IO 模型,而非真正的异步 IO
在校生,转行 linux 平台开发人员,Linux 基础知识点学习资料点击 Linux学习资料 获取
在职人员学习提升,有一定基础的转行人员,Linux 后台服务器开发高级进阶知识点学习资料点击 服务器进阶学习资料 获取
信号驱动 I/O 型
信号驱动 IO:signal-driven I/O
用户进程可以通过 sigaction 系统调用注册一个信号处理程序,然后主程序可以继续向下执行,当有 IO 操作准备就绪时,由内核通知触发一个 SIGIO 信号处理程序执行,然后将用户进程所需要的数据从内核空间拷贝到用户空间
此模型的优势在于等待数据报到达期间进程不被阻塞。用户主程序可以继续执行,只要等待来自信号处理函数的通知
该模型并不常用
异步 I/O 型
异步 IO 与信号驱动 IO 最主要的区别是信号驱动 IO 是由内核通知何时可以进行 IO 操作,而异步 IO 则是由内核告诉用户线程 IO 操作何时完成。信号驱动 IO 当内核通知触发信号处理程序时,信号处理程序还需要阻塞在从内核空间缓冲区复制数据到用户空间缓冲区这个阶段,而异步 IO 直接是在第二个阶段完成后,内核直接通知用户线程可以进行后续操作了
相比于 IO 多路复用模型,异步 IO 并不十分常用,不少高性能并发服务程序使用 IO 多路复用模型+多线程任务处理的架构基本可以满足需求。目前操作系统对异步 IO 的支持并非特别完善,更多的是采用 IO 多路复用模型模拟异步 IO 的方式(IO 事件触发时不直接通知用户线程,而是将数据读写完毕后放到用户指定的缓冲区中)
五种 I/O 模型
常用的是 IO 多路复用型,比如 Nginx
实现 I/O 模型的方式
Select:Linux 实现对应,I/O 复用模型,BSD4.2 最早实现,POSIX 标准,一般操作系统均有实现
Poll:Linux 实现,对应 I/O 复用模型,System V unix 最早实现
Epoll:Linux 特有,对应 I/O 复用模型,具有信号驱动 I/O 模型的某些特性
Kqueue:FreeBSD 实现,对应 I/O 复用模型,具有信号驱动 I/O 模型某些特性
/dev/poll:SUN 的 Solaris 实现,对应 I/O 复用模型,具有信号驱动 I/O 模型的某些特性
Iocp Windows 实现,对应第 5 种(异步 I/O)模型
Apache 使用的是 Select,Nginx 使用的是 Epoll。所以 Nginx 支持 C10K(10K Connections)
Select
POSIX 所规定,目前几乎在所有的平台上支持,其良好跨平台支持也是它的一个优点,本质上是通过设置或者检查存放 fd 标志位的数据结构来进行下一步处理
缺点:
单个进程能够监视的文件描述符的数量存在最大限制,在 Linux 上一般为 1024(多年生产环境得到的最佳值),可以通过修改宏定义 FD_SETSIZE,再重新编译内核实现,但是这样也会造成效率的降低
单个进程可监视的 fd 数量被限制,默认是 1024,修改此值需要重新编译内核
对 socket 是线性扫描,即采用轮询的方法,效率较低
select 采取了内存拷贝方法来实现内核将 FD 消息通知给用户空间,这样一个用来存放大量 fd 的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大
Poll
本质上和 select 没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个 fd 对应的设备状态
其没有最大连接数的限制,原因是它是基于链表来存储的
大量的 fd 的数组被整体复制于用户态和内核地址空间之间,而不管这样的复制是不是有意义
poll 特点是“水平触发”,如果报告了 fd 后,没有被处理,那么下次 poll 时会再次报告该 fd
边缘触发:只通知一次
Epoll
在 Linux 2.6 内核中提出的 select 和 poll 的增强版本
支持水平触发 LT 和边缘触发 ET,最大的特点在于边缘触发,它只告诉进程哪些 fd 刚刚变为就需态,并且只会通知一次
使用“事件”的就绪通知方式,通过 epoll_ctl 注册 fd,一旦该 fd 就绪,内核就会采用类似 callback 的回调机制来激活该 fd,epoll_wait 便可以收到通知
优点::
没有最大并发连接的限制:能打开的 FD 的上限远大于 1024(1G 的内存能监听约 10 万个端口),具体查看/proc/sys/fs/file-max,此值和系统内存大小相关
效率提升:非轮询的方式,不会随着 FD 数目的增加而效率下降;只有活跃可用的 FD 才会调用 callback 函数,即 epoll 最大的优点就在于它只管理“活跃”的连接,而跟连接总数无关
内存拷贝,利用 mmap(Memory Mapping)加速与内核空间的消息传递;即 epoll 使用 mmap 减少复制开销
评论