00-1010网络IO的本质是读写套接字。socket在Linux中抽象为流,IO可以理解为对流操作。
Linux的网络IO模型
IO本身可分为内存IO、网络IO、磁盘IO、缓存IO等。一般来说讨论IO的时候多指后面网络IO和磁盘IO,因为这两个是最慢的哈哈)。这里专门对网络IO进行分析说明。
00-1010阻塞/非阻塞
对于函数/方法的实现,即在数据准备好之前是立即返回还是等待,即在发起IO请求后是否阻塞。
阻塞输入输出机制
在IO阻塞的情况下,当用户调用read时,用户线程会被阻塞,直到内核数据准备好,数据从内核缓冲区复制到用户状态缓冲区,read才会返回。你可以看到堵塞的两部分。
将CPU数据从磁盘读取到内核缓冲区。将CPU数据从内核缓冲区复制到用户缓冲区。
无阻塞输入输出机制
非阻塞IO发出读请求后,发现数据没有准备好,会继续执行。此时,应用程序将不断轮询轮询内核,询问数据是否准备好了。当数据没有准备好时,内核会立即返回EWOULDBLOCK错误。直到数据被复制到应用程序缓冲区,读取请求才会得到结果。而且你要注意!这里,获取数据的最后一次读取调用是一个同步过程,需要等待。这里的同步指的是将内核模式数据复制到用户程序缓冲区的过程。
同步/异步
IO读操作指数据流:网络-内核缓冲区-用户内存。
同步和异步的主要区别在于数据从内核缓冲区到用户内存的过程是否需要用户进程等待。等待内核模式准备好数据后,会自动通知用户模式线程读取信息数据。在此之前,用户模式线程不需要等待,但可以做其他操作。对于网络IO,涉及两个系统对象,一个是调用这个IO的进程或线程)[用户状态],另一个是系统内核[内核状态]
当用户模式读取操作发生时,它将经历两个阶段:
第一阶段:用户模式线程在内核模式下等待数据准备就绪。第二阶段:用户模式线程,将数据从内核复制到进程。第一步:通常包括等待网络上的数据包到达,然后将其复制到内核中的缓冲区。步骤2:将数据从内核缓冲区复制到用户模式)应用程序进程缓冲区。网络应用处理两类问题:网络IO和数据计算。前者给应用带来了更多的性能瓶颈。00-1010同步IO阻塞IO模型阻塞IO)非阻塞IO模型非阻塞IO)复用IO模型复用IO)信号驱动IO模型异步io)
IO的分类和范畴
在Linux中,默认情况下所有套接字都是阻塞的。
如上所述,当用户进程调用recvfrom一个系统调用)时,有两个阶段。
00-1010数据输入的次数很多
一开始还没有到达,这个时候kernel就要等待足够的数据到来。而用户进程会一直阻塞。当kernel等到数据准备好了,它会将数据从kernel中拷贝到用户内存,然后kernel返回,用户进程结束block状态,重新运行。
Blocking IO的特点就是IO执行的两个阶段都是block了的。
非阻塞IO模型(non-blocking IO)[poll]
在Linux中,可以通过设置socket使其变为non-blocking,其流程如下:
多路复用IO模型(multiplexing IO)
select/epoll/evpoll,也被称作是Event-Driven IO。好处是单个process可以同时处理多个网络连接的IO。
基本原理可见下面的“IO复用技术”。也叫多路IO就绪通知。这是一种进程预先告知内核的能力,让内核发现进程指定的一个或多个IO条件就绪了,就通知用户进程。使得一个进程能在一连串的事件上等待。
Select/Epoll的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。
在IO多路复用模型中,实际中,对于每一个socket,一般都设置成为non-blocking,但是,如上图所示,整个用户的process其实是一直被block的。只不过process是被select这个函数block,而不是被socket IO给block。
异步IO(asynchronous IO)
用户进程发起read操作之后,立刻就可以开始去做其它的事。
比较
非阻塞和异步的区别
经过上面的介绍,会发现non-blocking IO和asynchronous IO的区别还是很明显的。
在non-blocking IO中,虽然进程大部分时间都不会被block,但是它仍然要求进程去主动的check,并且当数据准备完成以后,也需要进程主动的再次调用recvfrom来将数据拷贝到用户内存。asynchronous IO则完全不同。它就像是用户进程将整个IO操作交给了他人(kernel)完成,然后他人做完后发信号通知。在此期间,用户进程不需要去检查IO操作的状态,也不需要主动的去拷贝数据。
IO复用技术
在IO编程过程中,当需要处理多个请求时,可以使用多线程和IO复的方式进行处理。
IO复用是什么?
把多个IO的阻塞复用到一个select之类的阻塞上,从而使得系统在单线程的情况下同时支持处理多个请求。
IO复用常见的应用场景:
服务器需要同时处理多个处于监听状态和多个连接状态的套接字;服务器需要处理多种网络协议的套接字IO复用的实现方式目前主要有select、poll和epoll/evpoll。
select和poll的原理基本相同:
注册待侦听的fd这里的fd创建时最好使用非阻塞)每次调用都去检查这些fd的状态,当有一个或者多个fd就绪的时候返回返回结果中包括已就绪和未就绪的fd
select和poll与epoll机制的比较
Linux网络编程过程中,相比于select/poll,epoll是有着更明显优势的一种选择。
支持一个进程打开的socket描述符不受限制(仅受限于操作系统的最大文件句柄数 unlimit)。Select的缺陷:一个进程所打开的FD受限,默认是2048;尽管数值可以更改,但同样可能导致网络效率下降;可以选择多进程的解决方案,但是进程的创建本身代价不小,而且进程间数据同步远比不上线程间同步的高效。epoll所支持的FD上限是最大可以打开文件的数目:/proc/sys/fs/file-max
IO效率可能随着文件描述符数目的增加而线性下降。
epoll扫描系统的机制不同select/poll是线性扫描FD的集合;epoll是根据FD上面的回调函数实现的,活跃的socket会主动去调用该回调函数,其它socket则不会,相当于市是一个AIO,只不过推动力在OS内核。使用mmap加速内核与用户空间的消息传递,zero-copy的一种。epoll的API更加简单。IO复用还有一个 水平触发 和 边缘触发 的概念:水平触发:当就绪的fd未被用户进程处理后,下一次查询依旧会返回,这是select和poll的触发方式。边缘触发:无论就绪的fd是否被处理,下一次不再返回。理论上性能更高,但是实现相当复杂,并且任何意外的丢失事件都会造成请求处理错误。epoll默认使用水平触发,通过相应选项可以使用边缘触发。
IO模型的总结
最后,再举几个不是很恰当的例子来说明这四个IO Model,有A,B,C,D四个人在钓鱼:
A用的是最老式的鱼竿,所以呢,得一直守着,等到鱼上钩了再拉杆;(同步阻塞)B的鱼竿有个功能,能够显示是否有鱼上钩,所以呢,B就和旁边的MM聊天,隔会再看看有没有鱼上钩,有的话就迅速拉杆;(非阻塞)C用的鱼竿和B差不多,但他想了一个好办法,就是同时放好几根鱼竿,然后守在旁边,一旦有显示说鱼上钩了,它就将对应的鱼竿拉起来;(io多路复用机制)D是个hpdgb,干脆雇了一个人帮他钓鱼,一旦那个人把鱼钓上来了,就给D发个短信。(异步机制)