IO模型
一个输入操作通常包括两个阶段:
- 等待数据准备好
- 从内核向进程复制数据
对于一个套接字上的输入操作
第一步通常涉及等待数据从网络中到达。当所等待数据到达时,它被复制到内核
中的某个缓冲区
第二步就是把数据从内核缓冲区复制到应用进程缓冲区
什么是阻塞
网络交互,系统可预见的没有读到数据,是阻塞
磁盘交互,系统不可预见的抖动,导致IO阻塞一会,不是阻塞
BIO和NIO
BIO
每个连接都需要占用一个线程,没有读到IO流的时候都处于阻塞
IO阻塞会导致线程无法释放,即便是不需要IO时也会阻塞
会导致服务端线程增大,线程池也无法解决
NIO
每个连接都需要注册一个事件监听(select/poll/epoll),任何时候立即
返回IO流的当前数据
线程不会因为IO阻塞而无法释放,可以去做额外的事情
线程只有在需要IO的时候才会被阻塞住
IO多路复用
操作系统提供一种机制(poll、select、epoll
),允许注册IO请求,当有任何一个请求被触发,会有反馈
poll、select
每次都要遍历所有的注册,并且轮询
epoll
只会返回对应被触发的注册时间(并且提供了边缘触发,允许有条件的获取数据),并轮询
NIO实现
NIO = 非阻塞(立即放回)IO + IO多路复用(通知IO就绪了) + 缓存(ByteBuffer)
NIO特有的是 selector(实现了IO多路复用策略)
+ buffer
只是更加优化的方式
- 单Reactor单线程模型
- 单Reactor多线程模型:有一个Thread-Acceptor线程用于监听接收客户端的
TCP连接事件/IO读写事件
(N条链路)。有一个ThreadPool-负责网络IO的读写操作
-
主从Reactor多线程模型:有一个Thread-Acceptor线程用于监听接收客户端的
TCP连接事件
,有一个ThreadPool专门负责IO读写事件
。有一个ThreadPool-负责网络IO的读写操作
- Netty线程模型(对主从Reactor多线程模型做出细微改造)
MainReactor负责客户端的连接请求
,并将请求转交给 SubReactor
SubReactor负责相应通道的IO读写请求
非 IO 请求(具体逻辑处理)的任务则会直接写入队列,等待 worker threads 进行处理
BIO & NIO 优势
BIO:少量的连接
使用非常高的带宽,一次发送大量的数据
NIO:(交易信息上传
)如果需要管理同时打开的成千上万个连接
,这些连接每次只是发送少量的数据
磁盘IO
磁盘的最小操作单位是簇(sector),对于Linux来说,虚拟文件系统(VFS)抽象了磁盘设备,统一称为“块设备”(block device)。数据是按照一块块来组织的。操作系统可以随机的定位到某个“块”,读写某个“块”
块
->簇
的转换,是由设备驱动来完成的
在VFS上层的应用是感受不到“簇”的,他们只能感受到“块”
即使你只想读取1个Byte,磁盘也至少要读取1个块;要写入1个Byte,磁盘也至少要写入一个块
image.png在
虚拟文件系统层VFS
之上,是内存。这一层被称为Page Cache(在内核态)
在内存之上的是应用程序,应用程序总是需要在用户态分配一段内存空间作为buffer
,然后将Page Cache中的数据copy出来进行处理。处理完成后,将数据写回(copy回)到Page Cache
PageCache和BlockDevice.png
Page Cache对于磁盘IO的性能表现极度重要。比如,当通过write API写入数据到磁盘时,数据先会被写入到Page Cache。此时,这个Page被称为“dirty page”。dirty page会最终被写入到磁盘上,这个过程为称之为“写回”(writeback)。写回往往不会立刻发生。写回可能由于调用者直接使用类似于fsync这样的API,也有可能因为操作系统根据某种策略和算法决定自动写回。写回发生之前,如果机器挂了,就有可能丢失数据。这也是为什么有持久性要求的程序都需要用fsync来保证数据落地的原因。
上图会出现两次CPU copy
,分别是从内核态->用户态
,用户态->内核态
AIO(异步IO)
AIO在操作系统层面, 只支持磁盘IO, 不支持网络IO, 并且上层(nodejs,Java NIO)都会选择用线程池+BIO来模拟文件AIO
网络IO,需要用epoll
这样的IO多路复用技术结合
为了避免2次CPU copy
,可以采用mmap
和sendfile
技术
同步 I/O:将数据从内核缓冲区
复制到应用进程缓冲区
的阶段,应用进程会阻塞(通知复制是阻塞和非阻塞IO来简化)
异步 I/O:不会阻塞
mmap
mmap.png采用Page Cache中的
内核空间
内存地址直接映射到用户空间
中
sendfile
sendfile可以直接将Page Cache中某个fd的一部分数据传递给另外一个fd, 目标的fd可以是socket,而不用经过到应用层的两次copy
,无法做任何修改
,称这种实现为“Zero Copy”
Direct IO
Linux允许应用程序在执行磁盘IO时绕过缓冲区高速缓存,从用户空间直接将数据传递到文件或磁盘设备,称为直接IO(direct IO)或者裸IO(raw IO
)
DirectBuffer 需要 AlignedBlock对齐块
相比“Buffered IO”,Direct IO必然会带来性能上的降低。所以Direct IO有特定的应用场景。比如,在数据库的实现中,为了保证数据持久,写入新数据到WAL(Write Ahead Log)
必须直接写入到磁盘,不能等待。这里用Direct IO来实现WAL就非常理想。
使用Direct IO的另外一种场景是,应用程序对磁盘数据缓存有特别定制的需要,而常规的Page Cache的各种策略并不能满足这种需要。于是开发人员可以自己设计和实现一套“Cache”,配合Direct IO。毕竟最熟悉数据访问场景的,是应用程序自己的需求。
- 用于传递数据的缓冲区(buffer),其内存边界必须对齐(aligned block)为块大小(block size)的整数倍
- 数据传输的开始点,即文件和设备的偏移量offset,必须是块大小(block size)的整数倍
- 待传递数据的长度必须是块大小(block size)的整数倍
以上约束的是内存写入磁盘时, 内存的规定. 不遵守上述任一限制均将导致
EINVAL
错误
网友评论