网络IO入门
初识网络IO
IO是input 和 output的缩写,根据所指对象的不同,目前有网络IO和磁盘IO。网络IO即是对网络数据的读写操作
网络IO会涉及到两个系统对象
- 用户空间调用IO的线程或进程
- 另一个是内核空间的系统内核
在上篇socket通信基础篇中,我们注意到获取对方网络数据使用的是read()函数。而IO操作这个函数时,会经历两个阶段:
- 等待数据准备就绪
- 将数据从内核拷贝到进程或线程
根据两个不同阶段的不同情况,出现了多种的IO网络模型。以下,我们介绍一些常见的5种IO模型
五种IO网络模型
我们将介绍的五种网络IO模型分别是阻塞IO(blocking io)、非阻塞IO(non-blocking io)、多路复用IO(IO multiplexing)、异步IO(Asynchronous io)以及信号驱动IO(SIGIO)。
阻塞IO(blocking io)
所有的socket创建的时候默认是阻塞的。在阻塞状态下,网络io调用read函数时,会一直阻塞,直到有数据接收。
非阻塞IO(non-blocking io)
由于阻塞会影响到线程的继续,因此有个非阻塞IO。可以通过设置来配置它的非阻塞状态
以下设置句柄fd为非阻塞状态
fcntl( fd, F_SETFL, O_NONBLOCK );
设置完非阻塞后,当使用read函数获取数据时,如kernel中数据准备就绪,则直接返回-1,不会阻塞进程。但也意味着如果你需要获取数据,那就需要一直read。直到kernel数据准备就绪,就会将数据拷贝到用户内存,并且返回。
非阻塞情况下,read()返回值含义如下
返回值 | errno值 | 含义 |
---|---|---|
>0 | - | 接收数据完毕,返回值为接收数据的字节数 |
=0 | - | 连接正常断开 |
-1 | EAGAIN | 操作没执行完成 |
-1 | 不等于EAGAIN | 操作遇到系统错误 |
多路复用IO(IO multiplexing)
在阻塞IO的情况下,如果需要处理成千上万个网络IO,那么有两种方式:
- 开多个线程
- 使用循环去遍历多个IO
无论哪种方式,都会占用较高的cpu资源
在非阻塞IO的情况下,循环调用的recv()会极大的消耗cpu资源,并且recv在大部分情况下,只是用于判断是否有数据准备就绪的功能。
多路复用IO大家可能听的不是很多,但是它所概括的select/poll/epoll应该就熟悉了。
select/epoll的优势在于一个线程内可以同时处理多个网络IO。基本原理是用一个组件不断的轮询所有的socket,当某个socket有数据到达后就通知用户进程。
由上图可以看出在select阶段,当用户调用了select时,整个线程是被阻塞状态,而内核会监视所有select负责的socket, 直到其中一个数据准备就绪而返回。
具体的select/epoll的实现,将在select传送门和epoll传送门中做讲解。
异步IO(Asynchronous io)
linux的异步IO用在磁盘IO读写操作,直到内核2.6版本才开始引入网络IO
异步io 用户操作发起read后,内核会立即回复,且不会阻塞进程。在数据接收完毕,并且将内核数据拷贝到内存空间后,再给用户进程发送signal,表示read操作完成
信号驱动IO(SIGIO)
用过linux的朋友,相信也用过kill -9 pid 去干掉一些进程吧。这就是一个典型的信号驱动案例
信号驱动原理
- 套接字进行信号驱动IO,并安装一个信号处理函数(即设置一个回调函数)。
- 数据准备好的时候,进程会接收到一个SIGIO信号
- 在信号处理函数中调用I/O函数操作数据
信号驱动的优势在于等待数据报到达期间,进程不会被阻塞。缺点在于过多的连接时,如果使用信号驱动,则内核消耗过大,效率不高。
总结
- 处理不多的连接数时,多路复用的效率不一定比多线程+阻塞io模型高;多路复用的优势不在于单个连接的时候能处理的更快,而在于能够处理更多的连接
- 非阻塞IO即non-blocking IO在真正的IO操作中也是会有阻塞的。当内核数据准备好的时候,read将数据从内核拷贝到内存的时候会出现阻塞。
- select()事件驱动模型在建立一个简单的事件驱动服务程序时具有很大的参考意义。但是这个模型还是有很多问题
- select()接口并不是实现“事件驱动”的最好选择。当需要探测的句柄值较大时,select()接口本身需要消耗大量时间去轮询各个句柄。很多操作系统提供了更为高效的接口,如linux提供了epoll,BSD提供了kqueue,Solaris提供了/dev/poll,…。如果需要实现更高效的服务器程序,类似 epoll 这样的接口更被推荐。缺点是不同的操作系统特供的 epoll 接口有很大差异,所以使用类似于 epoll 的接口实现具有较好跨平台能力的服务器会比较困难。
- 该模型将事件探测和事件响应夹杂在一起,一旦事件响应的执行体庞大,则对整个模型是灾难性的。在很大程度上降低了事件探测的及时性。幸运的是,有很多高效的事件驱动库可以屏蔽上述的困难,常见的事件驱动库有libevent 库,还有作为 libevent 替代者的 libev 库。这些库会根据操作系统的特点选择最合适的事件探测接口,并且加入了信号(signal) 等技术以支持异步响应,这使得这些库成为构建事件驱动模型的不二选择。
经过以上描述,相信大家对于这5种IO模型有一个大致的了解。在开发过程中,我们应根据实际情况去选择对应的IO模型。