5种网络IO模型(有图,很清楚)( 三 )


3、多路复用IO(IO multiplexing)
IO multiplexing这个词可能有点陌生 , 但是如果我说select/epoll , 大概就都能明白了 。 有些地方也称这种IO方式为事件驱动IO(event driven IO) 。 我们都知道 , select/epoll的好处就在于单个process就可以同时处理多个网络连接的IO 。 它的基本原理就是select/epoll这个function会不断的轮询所负责的所有socket , 当某个socket有数据到达了 , 就通知用户进程 。 它的流程如图:
5种网络IO模型(有图,很清楚)文章插图
图6 多路复用IO
当用户进程调用了select , 那么整个进程会被block , 而同时 , kernel会“监视”所有select负责的socket , 当任何一个socket中的数据准备好了 , select就会返回 。 这个时候用户进程再调用read操作 , 将数据从kernel拷贝到用户进程 。
这个图和blocking IO的图其实并没有太大的不同 , 事实上还更差一些 。 因为这里需要使用两个系统调用(select和recvfrom) , 而blocking IO只调用了一个系统调用(recvfrom) 。 但是 , 用select的优势在于它可以同时处理多个connection 。 (多说一句:所以 , 如果处理的连接数不是很高的话 , 使用select/epoll的web server不一定比使用multi-threading + blocking IO的web server性能更好 , 可能延迟还更大 。 select/epoll的优势并不是对于单个连接能处理得更快 , 而是在于能处理更多的连接 。 )
在多路复用模型中 , 对于每一个socket , 一般都设置成为non-blocking , 但是 , 如上图所示 , 整个用户的process其实是一直被block的 。 只不过process是被select这个函数block , 而不是被socket IO给block 。 因此select()与非阻塞IO类似 。
大部分Unix/Linux都支持select函数 , 该函数用于探测多个文件句柄的状态变化 。 下面给出select接口的原型:
FD_ZERO(int fd, fd_set* fds)
FD_SET(int fd, fd_set* fds)
FD_ISSET(int fd, fd_set* fds)
FD_CLR(int fd, fd_set* fds) int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds,
struct timeval *timeout)
这里 , fd_set 类型可以简单的理解为按 bit 位标记句柄的队列 , 例如要在某 fd_set 中标记一个值为16的句柄 , 则该fd_set的第16个bit位被标记为1 。 具体的职位、验证可使用 FD_SET、FD_ISSET等宏实现 。 在select()函数中 , readfds、writefds和exceptfds同时作为输入参数和输出参数 。 如果输入的readfds标记了16号句柄 , 则select()将检测16号句柄是否可读 。 在select()返回后 , 可以通过检查readfds有否标记16号句柄 , 来判断该“可读”事件是否发生 。 另外 , 用户可以设置timeout时间 。
下面将重新模拟上例中从多个客户端接收数据的模型 。
5种网络IO模型(有图,很清楚)文章插图
图7 使用select()的接收数据模型
述模型只是描述了使用select()接口同时从多个客户端接收数据的过程;由于select()接口可以同时对多个句柄进行读状态、写状态和错误状态的探测 , 所以可以很容易构建为多个客户端提供独立问答服务的服务器系统 。 如下图 。
5种网络IO模型(有图,很清楚)文章插图
图8 使用select()接口的基于事件驱动的服务器模型
这里需要指出的是 , 客户端的一个 connect() 操作 , 将在服务器端激发一个“可读事件” , 所以 select() 也能探测来自客户端的 connect() 行为 。
上述模型中 , 最关键的地方是如何动态维护select()的三个参数readfds、writefds和exceptfds 。 作为输入参数 , readfds应该标记所有的需要探测的“可读事件”的句柄 , 其中永远包括那个探测 connect() 的那个“母”句号;同时 , writefds 和 exceptfds 应该标记所有需要探测的“可写事件”和“错误事件”的句柄 ( 使用 FD_SET() 标记 ) 。
作为输出参数 , readfds、writefds和exceptfds中的保存了 select() 捕捉到的所有事件的句柄值 。 程序员需要检查的所有的标记位 ( 使用FD_ISSET()检查 ) , 以确定到底哪些句柄发生了事件 。
上述模型主要模拟的是“一问一答”的服务流程 , 所以如果select()发现某句柄捕捉到了“可读事件” , 服务器程序应及时做recv()操作 , 并根据接收到的数据准备好待发送数据 , 并将对应的句柄值加入writefds , 准备下一次的“可写事件”的select()探测 。 同样 , 如果select()发现某句柄捕捉到“可写事件” , 则程序应及时做send()操作 , 并准备好下一次的“可读事件”探测准备 。 下图描述的是上述模型中的一个执行周期 。
5种网络IO模型(有图,很清楚)文章插图