麦子学院 2017-01-17 19:18
Linux学习之常见的5种IO 模型
回复:0 查看:2639
本文和大家分享的主要是linux中常见的5种IO 模型相关内容,一起来看看吧,希望对大家学习linux有所帮助。
IO模型
用一幅图表示所支持的I/O
模型
纵向维度是“
阻塞(
Blocking
)
”
、
“
非阻塞(
Non-blocking
)
”
;横向维度是
“
同步
”
、
“
异步
”
。总结起来是四种模型
同步阻塞、同步非阻塞;异步阻塞、异步非阻塞
。《Unix
网络编程》中划分出了
“
第五种
”
模型
——“
信号驱动式
IO”
其实属于异步阻塞类型,这种模型的通知方式有多种多样后面展开说明。
同步/异步、阻塞/非阻塞
从内核角度看I/O
操作分为两步:用户层
API
调用;内核层完成系统调用(发起
I/O
请求)。所以
“
异步
/
同步
”
的是指
API
调用;
“
阻塞
/
非阻塞
”
是指内核完成
I/O
调用的模式。用一幅图表示更加明显
同步是指函数完成之前会一直等待;
阻塞
是指系统调用的时候进程会被设置为Sleep
状态直到等待的事件发生(比如有新的数据)。明白这一点之后再看这五种模型相信就会清晰很多,我们挨个分析:
同步阻塞
这种模型最为常见,用户空间调用API
(
read
、
write )
会转化成一个
I/O
请求,一直等到
I/O
请求完成
API
调用才会完成。这意味着: 在
API
调用期间用户程序是同步的的;这个
API
调用会导致系统以阻塞的模式执行
I/O
,如果此时没有数据则一直
“
等待
”
(放弃
CPU
主动挂起
——Sleep
状态) (注意,对于硬盘来说是不会出现阻塞的,无论是什么时候读它总是有数据。常见的阻塞设备是终端、网卡之类的)。
以
read
为例子,它由三个参数组成,第一个函数是文件描述符;第二个是
应用缓冲
;第三个参数是需要读取的字节数。经过系统调用会以阻塞模式执行I/O
,
I/O
模块读取数据后会放入到
PageCache
中;最后一步是把数据从
PageCache
复制到
应用缓冲
。如果I/O
请求无法得到满足
——
没有数据,则主动让出
CPU
直到
有数据
(注意,即便系统调用让出CPU
也未必真的就让出。
read
函数是同步的,所以
CPU
还是会被用户空间代码占用)。
同步非阻塞
这种模式通过调用
read
、
write
的时候指定
O_NONBLOCK
参数。和“
同步阻塞
”
模式的区别在于系统调用的时候它是以非阻塞的方式执行,无论是否有数据都会立即返回。 以
read
为例,如果成功读取到数据它返回读取到的字节数;如果此时没有数据则返回-1
,同时设置
errno
为
EAGAIN
(或者
EWOULDBLOCK
,二者相同)。所以这种模式下我们一般会用一个
“
循环
”
不停的尝试读取数据,处理数据。
异步阻塞
同步模型最主要的问题是占用CPU
, 阻塞
I/O
会主动让出
CPU
但是用户空间的系统调用还是不会返回依然耗费
CPU
;非阻塞
I/O
必须不停的
“
轮询
”
不断尝试读取数据(会耗费更多
CPU
更加低效)。如果仔细分析同步模型霸占
CPU
的原因不难得出结论
——
都是在等待数据到来。异步模式正是意识到这一点所以把
I/O
读取细化为 订阅
I/O
事件,实际
I/O
读写,在
“
订阅
I/O
事件
”
事件部分会主动让出
CPU
直到事件发生 。异步模式下的
I/O
函数和同步模式下的
I/O
函数是一样的(都是
read
、
write
)唯一的区别是异步模式
“读”必有数据
而同步模式则未必。
常见的异步阻塞函数包括
select
,
poll
,
epoll
,这些函数的用法需要花费相当大的篇幅介绍而这篇文章我想集中精力介绍“I/O
模型
”
。以
select
为例我们看一下大致原理
异步模式下我们的API
调用分为两步,第一步是通过
select
订阅读写事件
这个函数会主动让出CPU
直到事件发生(设置为
Sleep
状态,等待事件发生) ;
select
一旦返回就证明可以开始读了所以第二部是通过
read
读取数据(
“读”必有数据 )
。
异步阻塞模型之信号驱动
“
完美主义者
”
看了上面的
select
之后会有点不爽——
我还要
“
等待
”
读写事件(即便
select
会主动让出CPU
),能不能有读写事件的时候主动通知我啊?。借助
“
信号
”
机制我们可以实现这个,但是这并不完美而且有点弄巧成拙的意思。 具体用法:通过
fcntl
函数设置一个
F_GETFL|O_ASYNC
(
曾经信号驱动I/O
也叫
“
异步
I/O”
所以才有
O_ASYNC
的说法),当有I/O
时间的时候操作系统会触发
SIGIO
信号。在程序里只需要绑定
SIGIO
信号的处理函数就可以了。但是这里有个问题——
信号处理函数由哪个进程执行呢?
,答案是:“
属主
”
进程。操作系统只负责参数信号而实际的信号处理函数必须由用户空间的进程实现。(这就是设置
F_SETOWN
为当前进程PID
的原因) 信号驱动性能要比
select
、
poll
高(避免文件描述符的复制)但是缺点是致命的——* Linux
中信号队列是有限制的如果操过这个数字问题就完全无法读取数据。
异步非阻塞
这种模型是最“
省事
”
的模型,系统调用完成之后就只要坐等数据就可以了。是不是特别爽?其实不然,问题出在实现上。
Linux
上的
AIO
两个实现版本,
POSIX
的实现最烂(蓝色巨人的锅)性能很差而且是基于
“
事件驱动
”
还会出现
“
信号队列不足
”
的问题
(
所以它就偷偷的创建线程,导致线程也不可控了
)
;一个是
Linux
自己实现的(
redhat
贡献)
Native AIO
。
Native AIO
主要涉及到的两个函数
io_submit
设置需要I/O
动作(读、写,数据大小,应用缓冲区等);
io_getevents
等待I/O
动作完成。没错,即便你的整个
I/O
行为是非阻塞的还是需要有一个办法知道数据是否读取
/
写入成功。
注意图中,内核不再为I/O
分配
PageCache
,所有的数据必须有用户自己读取到应用缓冲中维护。所以
AIO
一定是和
“
直接
I/O”
配合使用。
AIO
针对网卡设备的意义不大,首先它的实现本质上和
epoll
差不多;其次它在
Linux
中的作用更多的是用于磁盘
I/O
(异步非阻塞可以不用多线程就造成大量的
I/O
请求便于
I/O
模块
“
合并
”
优化会提高整体
I/O
的吞吐率
——
而且对
CPU
开销比较少)。 在
Nginx
中用了一个技巧,可以实现
AIO
和
epoll
联动,
AIO
读取到数据后触发
epoll
发送数据。(这个特性是非常尴尬的,如果是磁盘文件完全可以用
sendfile
搞定)。
Direct I/O和Buffered I/O
Linux
在进行
I/O
操作的时候会先把数据放到
PageCache
中然后通过
“
内存映射
”
的方式返回给应用程序,这样做的好处是可以预读数据也能在多个进程读取相同数据的时候起到
Cache
的作用。应用程序不能直接使用
PageCache
中的数据,通常是复制到一块
“
用户空间
”
的内存中再使用。
·Direct I/O
是指数据不落在
PageCache
,直接从设备读取到数据后放到用户空间中
·Buffered I/O
是指数据竞购
PageCache
同步I/O
只能使用
Buffered I/O
;异步阻塞
I/O
可以
Buffered I/O
也可以使用
Direct I/O
;异步非阻塞
I/O
只能使用
Direct I/O
Zero Copy
考虑从磁盘读取文件经过网卡发送出去,会有
四次内存复制
:1. DMA
会复制磁盘数据到内核空间,
2.
应用程序复制内核空间的数据到用户空间;
3.
应用程序用户空间的数据复制到
Socket
缓冲(内核空间);
4.
协议栈把数据复制到网卡的中发送。 简单来说
Zero Copy
就是
节省这个过程中的内存复制次数
。有几种做法:
·Direct I/O
直接把磁盘数据复制到内核空间; 但是
Direct I/O
没有办法直接把数据放到网卡中
——
必须要经过协议栈 。所以可以节省一次内存复制;
·sendfile
,磁盘数据通过
DMA
读取到内核空间后直接交给
TCP/IP
协议栈;真正的不需要内存复制;
除此之外还可以利用
splice
、
mmap
做一些优化,根据不同的设备需要采用不同的方式此处不再展开。
最后的礼物
我是一个
Java
程序员,出于职业的良知(公众账号里遍地的
Python
、
C
、
Erlang
都让我遗忘了自己的本性)最后我决定回归本性看一下
Java
的
AIO
。
JDK7.0
引入了
AIO
,但是
Java的AIO其实内部是用Epoll实现
(自己去看LinuxAsynchronousChannelProvider.java
文件吧)。所以不要以为
Java AIO
会比
NIO
快多少多多,压根没有什么性能变化。
来源:公众号