声明,本文的建立在 https://www.cnblogs.com/aspirant/p/6877350.html?utm_source=itdadao&utm_medium=referral
之上的,为加深个人的理解,才梳理一下
1.什么是i/o操作
在unix(like)世界里,一切皆文件,而文件就是一串二进制流,不管socket,还是管道、终端,对我们来说是文件,一切都是流
在信息交换中,我们都是对这些流进行数据的收发操作,简称i/o操作。
2.同步异步,阻塞非阻塞区别联系
实际上同步与异步是针对应用程序与内核的交互而言的。同步过程中进程触发io操作并等待(也就是我们说的阻塞)或者轮询的去
查看io操作(也就是我们说的非阻塞)是否完成,异步过中进程触发io操作以后,直接返回,做自己的十强,io交给内核来处理,完成后内核通知进程io完成。
同步和异步针对应用程序来说,关注的程序中间的协作关系,阻塞和非阻塞更关注的是单个进程的执行状态。
同步有阻塞和非阻塞之分,异步没有,它一定是非阻塞的。
只有用户线程在操作io的时候不去考虑io的执行全部都交给cpu去完成,而自己只等待一个完成的信号的时候,才是真正的异步IO,所以,拉一个子线程去轮询、去死循环,或者使用select、poll、epool,都不是异步。
io分为两个部分,(a)是数据通过网关到达内核,内核准备好数据,(b)数据从内核缓存写入用户缓存
同步:不管是BIO,还是NIO,还是IO多路复用,第二部数据从内核缓存写入用户缓存一定是由用户线程自行读取数据,处理数据
异步:第二部数据是内核写入的,并放在了用户线程指定的缓存区,写入完毕后通知用户线程。
二、阻塞
什么是程序的阻塞,想象这种情形,比如有快递,但快递一直没来,你会怎么做
1.去睡觉,快递到了打电话叫我去取
2.不停的打电话或一直在那等,直到快递到了
在计算机里就是
非阻塞忙轮询:数据没来,进程就不停的去检测数据,直到数据来
阻塞;数据没来,啥都不做,直到数据来了,体现在代码上就是代码不能往下执行
阻塞的io是一个线程只能处理一个套接字的i/o事件,那么如何转化为非阻塞
可以利用非阻塞忙轮询的方式
while true
{
for i in stream[]
{
if i has data
read until unavailable
}
}
我们只要把所有的io从头到尾查一遍,就可以处理多个流了,但没有i/o操作的时候,cpu会空转,未解决这个问题
我们不让这个线程亲自去检查流中是否有事件,而是引进了一个代理(一开始是select,后来是poll),这个代理很牛,它可以同时观察许多流的I/O事件,如果没有事件,代理就阻塞,线程就不会挨个挨个去轮询了,伪代码如下:
while true
{
select(streams[]) //这一步死在这里,知道有一个流有I/O事件时,才往下执行
for i in streams[]
{
if i has data
read until unavailable
}
}
但依然有个问题是,虽然从select中知道了有io操作,但却不知道是哪几个流,或是全部的流,我们只能无差别的轮询,当我们处理的流非常多的时候,这种无差别的轮询的方式时间复杂度比较
epoll可以理解为event poll,不同于忙轮询和无差别轮询,epoll会把哪个流发生了怎样的I/O事件通知我们。所以我们说epoll实际上是事件驱动(每个事件关联上fd)的,此时我们对这些流的操作都是有意义的。(复杂度降低到了O(1))伪代码如下
while true
{
active_stream[] = epoll_wait(epollfd)
for i in active_stream[]
{
read or write till
}
}
可以看到,select和epoll最大的区别就是:select 只是告诉你一定数目的流有事件了,至于哪个事件,还得你一个一个去轮询,而epoll会把发生的事件告诉你,通过发生的事件,就自然而然定位到哪个流,不得不说epoll跟select相比,是质的飞越。
三、i/o多路复用
输入操作一般包含两个步骤,
1.等待数据准备好,对于一个套接口上的操作,这一步步骤关系到数据从网络到达,并将其复制到内核的某个缓冲区
2.将数据从内核缓冲区复制到进程缓冲区
了解一下三种i/o模型
1.阻塞i/o模型(BIO)
默认情况下,所有套接口都是阻塞的,进程调用recvfrom系统调用,整个过程是阻塞的,直到数据复制到进程缓冲区时才返回
(当然,系统调用被终端也会返回)
2.非阻塞i/o模型(NIO)
当我们把一个套接口设置为非阻塞事,就是告诉内核,当请求i/o操作无法完成时,不要讲进程睡眠,而是返回一个错误。当没有数据准备好时,内核立即返回EWOULDBLOCK错误,第n次调用系统调用时,数据已经存在,这是将数据复制到进程缓冲区中,这其中有一个操作时轮询(polling)
3.i/o复用模型
此模型用到select和poll函数,这两个函数也会使进程阻塞,select先阻塞,有活动套接字才返回,但是和阻塞I/O不同的是,这两个函数可以同时阻塞多个I/O操作,而且可以同时对多个读操作,多个写操作的I/O函数进行检测,直到有数据可读或可写(就是监听多个socket)。select被调用后,进程会被阻塞,内核监视所有select负责的socket,当有任何一个socket的数据准备好了,select就会返回套接字可读,我们就可以调用recvfrom处理数据。
正因为阻塞I/O只能阻塞一个I/O操作,而I/O复用模型能够阻塞多个I/O操作,所以才叫做多路复用。
4.信号驱动I/O模型(signal driven i/o ,SIGIO)
首先我们允许套接口进行信号驱动I/O,并安装一个信号处理函数,进程继续运行并不阻塞。当数据准备好时,进程会收到一个SIGIO信号,可以在信号处理函数中调用I/O操作函数处理数据。当数据报准备好读取时,内核就为该进程产生一个SIGIO信号。我们随后既可以在信号处理函数中调用recvfrom读取数据报,并通知主循环数据已准备好待处理,也可以立即通知主循环,让它来读取数据报。无论如何处理SIGIO信号,这种模型的优势在于等待数据报到达(第一阶段)期间,进程可以继续执行,不被阻塞。免去了select的阻塞与轮询,当有活跃套接字时,由注册的handler处理。
5、异步I/O模型(AIO, asynchronous I/O)
进程发起read操作之后,立刻就可以开始去做其它的事。而另一方面,从kernel的角度,当它受到一个asynchronous read之后,首先它会立刻返回,所以不会对用户进程产生任何block。然后,kernel会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后,kernel会给用户进程发送一个signal,告诉它read操作完成了。
这个模型工作机制是:告诉内核启动某个操作,并让内核在整个操作(包括第二阶段,即将数据从内核拷贝到进程缓冲区中)完成后通知我们。
这种模型和前一种模型区别在于:信号驱动I/O是由内核通知我们何时可以启动一个I/O操作,而异步I/O模型是由内核通知我们I/O操作何时完成。
高性能IO模型浅析
服务器端编程经常需要构造高性能的IO模型,常见的IO模型有四种:
(1)同步阻塞IO(Blocking IO):即传统的IO模型。
(2)同步非阻塞IO(Non-blocking IO):默认创建的socket都是阻塞的,非阻塞IO要求socket被设置为NONBLOCK。注意这里所说的NIO并非Java的NIO(New IO)库 (相当于轮询的方式)。
(3)IO多路复用(IO Multiplexing):即经典的Reactor设计模式,Java中的Selector和Linux中的epoll都是这种模型。
(4)异步IO(Asynchronous IO):即经典的Proactor设计模式,也称为异步非阻塞IO。
这里详细介绍一下io多路复用
IO多路复用模型是建立在内核提供的多路分离函数select基础之上的,使用select函数可以避免同步非阻塞IO模型中轮询等待的问题。
户首先将需要进行IO操作的socket添加到select中,然后阻塞等待select系统调用返回。当数据到达时,socket被激活,select函数返回。用户线程正式发起read请求,读取数据并继续执行。
从流程上来看,使用select函数进行IO请求和同步阻塞模型没有太大的区别,甚至还多了添加监视socket,以及调用select函数的额外操作,效率更差。但是,使用select以后最大的优势是用户可以在一个线程内同时处理多个socket的IO请求。用户可以注册多个socket,然后不断地调用select读取被激活的socket,即可达到在同一个线程内同时处理多个IO请求的目的。而在同步阻塞模型中,必须通过多线程的方式才能达到这个目的。
用户线程使用select函数的伪代码描述为:
{
select(socket);
while(1) {
sockets = select();
for(socket in sockets) {
if(can_read(socket)) {
read(socket, buffer);
process(buffer);
}
}
}
}
其中while循环前将socket添加到select监视中,然后在while内一直调用select获取被激活的socket,一旦socket可读,便调用read函数将socket中的数据读取出来。
然而,使用select函数的优点并不仅限于此。虽然上述方式允许单线程内处理多个IO请求,但是每个IO请求的过程还是阻塞的(在select函数上阻塞),平均时间甚至比同步阻塞IO模型还要长。如果用户线程只注册自己感兴趣的socket或者IO请求,然后去做自己的事情,等到数据到来时再进行处理,则可以提高CPU的利用率。
IO多路复用模型使用了Reactor设计模式实现了这一机制
如图4所示,EventHandler抽象类表示IO事件处理器,它拥有IO文件句柄Handle(通过get_handle获取),以及对Handle的操作handle_event(读/写等)。继承于EventHandler的子类可以对事件处理器的行为进行定制。Reactor类用于管理EventHandler(注册、删除等),并使用handle_events实现事件循环,不断调用同步事件多路分离器(一般是内核)的多路分离函数select,只要某个文件句柄被激活(可读/写等),select就返回(阻塞),handle_events就会调用与文件句柄关联的事件处理器的handle_event进行相关操作。
如图5所示,通过Reactor的方式,可以将用户线程轮询IO操作状态的工作统一交给handle_events事件循环进行处理。用户线程注册事件处理器之后可以继续执行做其他的工作(异步),而Reactor线程负责调用内核的select函数检查socket状态。当有socket被激活时,则通知相应的用户线程(或执行用户线程的回调函数),执行handle_event进行数据读取、处理的工作。由于select函数是阻塞的,因此多路IO复用模型也被称为异步阻塞IO模型。注意,这里的所说的阻塞是指select函数执行时线程被阻塞,而不是指socket。一般在使用IO多路复用模型时,socket都是设置为NONBLOCK的,不过这并不会产生影响,因为用户发起IO请求时,数据已经到达了,用户线程一定不会被阻塞。
相比于IO多路复用模型,异步IO并不十分常用,不少高性能并发服务程序使用IO多路复用模型+多线程任务处理的架构基本可以满足需求。况且目前操作系统对异步IO的支持并非特别完善,更多的是采用IO多路复用模型模拟异步IO的方式(IO事件触发时不直接通知用户线程,而是将数据读写完毕后放到用户指定的缓冲区中)