netty学习笔记01——IO多路复用模型
阻塞I/O模型
阻塞i/o模型就是常见的io模型,请看如下的一个例子:
server = new ServerSocket(port);
server.accept();
上述的代码会被阻塞在server.accept()
,因为服务器在监听访问端口的请求,直到有访问到来,这个进程才继续进行。如果同时有大量的请求来访问服务器,后一个请求必须等前一个请求完成才能进行处理,这会造成服务器的效率极低。
一个解决办法是每当一个请求来临,都创建新的线程来处理,主线程继续监听服务器端口。这种办法导致创建和销毁线程的开销非常大。解决办法是使用线程池和队列。有些问题依旧没有得到解决,当网络传输较慢的时候,读取输入流的一方线程也将会被同步阻塞60s。在此期间,其他输入消息只能在消息队列中排队。
关于这个问题的解决放在下一篇文章中讨论,本节继续阐述阻塞i/o。
阻塞i/o模型在进程空间调用recvfrom,它的系统调用直到数据包到达且被复制到应用进程的缓冲区中或者发生错误时才返回,在此期间一直会等待,进程在从调用recvfrom开始到它返回的整段时间内都是被阻塞的,因此被称为阻塞i/o模型。——《netty权威指南》
非阻塞I/O模型
recvfrom从应用层到内核的时候,如果该缓冲区没有数据的话,就直接返回一个EWOULDBLOCK错误,一般都对非阻塞I/O模型进行轮询检查这个状态,看内核是不是有数据到来。——《netty权威指南》
当内核准备好数据的时候,调用recvfrom方法会立即返回状态码;如果内核已经准备好数据,调用recvfrom则会在数据报从内核中复制到用户空间后返回。阻塞和非阻塞io的主要区别就是在调用recvfrom方法的时候,如果内核没有准备好的话阻塞io会等它准备好,而非阻塞io会返回一个错误。由于非阻塞io需要不断的轮询状态,可能会带来更多额外的开销。
i/o多路复用
linux系统提供select/poll/epoll方法,可以同时监听多个频道的数据是否准备就绪,这个方法本质上也是阻塞的。如下图:
poll方法
函数原型:
int poll(struct pollfd fds[], nfds_t nfds, int timeout);
fds[]
是输入待返回的文件数组,nfds是数组中的元素总数量,timeout是该函数阻塞的时间,返回值是数组fds中可读,或者可写,或者发生异常的总数量。在源码文件poll.h
中,struct pollfd
具有如下结构体:
struct pollfd {
int fd;
short events;
short revents;
};
其中fd表示文件描述符,events表示请求检测的事件,revents表示检测之后返回的事件。该函数经过给定的阻塞时间timeout
后,返回数组fds中可读,或者可写,或者发生异常的总数量。结构体数组fds中的每个元素,如果revents不为空,表示状态发生了变化,当阻塞结束后,遍历数组fds,查看每个元素的revents,就可以了解该元素是否发生了改变。
select方法
函数原型:
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *errorfds, struct timeval *timeout);
其中nfds就是要监听的元素总数。readfds,writefds,errorfds就是要监控的文件数组,timeout是阻塞时间,如果是0的话就一直监控到有文件的状态发生变化。返回值如果为1表示有文件发生状态变化,如果返回值是0表示三种情况都没有发生,并且超过了阻塞时间timeout。如果返回-1表示发生错误。当函数结束阻塞的时候,readfds中剩余可读的文件集合,writefds剩余可写的文件集合,errorfds剩余发生异常的文件集合。也就是说select和poll的不同在于他的参数每次调用之后都会发生改变,如果要不断轮询文件集合,那么在每次调用select的时候都要重新初始化readfds、writefds、errorfds。当输入参数readfds,writefds,errorfds均为空的时候,select函数是一个比sleep更精确的函数。因为select中的timeout是一个精确到微秒的结构体,而sleep仅精确到毫秒。
epoll方法
epoll是linux特有的i/o复用函数。它在实现和使用上和select,poll有很大的差异,要复杂不少。首先,epoll使用一组函数来完成任务,而不是单个函数。其次,epoll把用户关心的文件描述符上的事件放在内核里的一个事件表里,从而无须像select和poll那样每次调用都要重复传入文件描述符集。但epoll需要使用一个额外的文件描述符,来唯一标识内核中的这个事件表。简单来说epoll通过内核的反射机制,使监听文件中发生状态改变后回调一组函数。
java NIO的核心类库多路复用器Selector就是基于epoll的多路复用技术实现。epoll有以下四点改进
- 支持要给进程打开的socket描述符不受限制
- I/O效率不会随着FD数目的增加而线性下降
- 使用mmap加速内核与用户空间的消息传递
- epoll的API更加简单(我觉得好像要复杂一些,可能因为内核的知识缺乏)
信号驱动I/O模型
首先开启套接口信号驱动I/O功能,并通过系统调用sigaction执行一个信号处理函数(此系统调用立即返回,进程继续工作,它是非阻塞的)。当数据准备就绪时,就为该进程生成一个SIGIO信号,通过信号回调通知应用程序调用recvfrom来读取数据,并通知主循环函数处理数据。——《netty权威指南》
异步I/O
告知内核启动某个操作,并让内核在整个操作完成后(包括将数据从内核复制到用户自己的缓冲区)通知我们。这种模型与信号驱动模型的主要区别是:信号驱动IO由内核通知我们何时可以开始一个IO操作;异步IO模型由内核通知我们IO操作何时已完成。