vlambda博客
学习文章列表

5种常见的IO模型及IO复用

背景

 在Redis的客户端连接时处理网络连接的模型为多路I/O复用,这个概念咋一看这么熟悉,哦,原来我之前面试某家公司时,吃了这个亏,哎,多少有些可惜。

前言

 在了解网络模型之前,对于一个程序和操作系统,我们还要准备个知识:用户空间(application)和内核空间(kernel)。用户空间是和其他进程共享,也就是进程的私有空间PCB,而进程无法访问内核空间,需要将数据从内核空间复制到用户空间,进程才能读取。

5种IO类型

 本文讨论的是Linux下的网络IO,一般的分为五种:

  • 阻塞IO    (Blocking IO)

  • 非阻塞IO  (nonBlocking IO)

  • 多路IO    (multiplexing)

  • 信号驱动   (signal driven)

  • 异步IO     (Asynchronous IO)


 以及介绍多路IO中的select、poll、和epoll

例1:小明在客厅看电视,然后厨房在烧水,你等着水煮泡面。


阻塞IO

 什么是阻塞IO,简言之,阻塞罢了,在例1中,阻塞IO就是,你一直等在厨房,而不能去看电视。在程序中,监听一个端口,当没有数据返回,就一直等在那,不能继续下面的操作。如下图:

 
 图中,程序调用recvfrom,从内核中获取数据,而内核一直没准备好,进程就一直阻塞于此,直到数据准备好datagram ready,复制数据到用户区,然后由具体的执行命令读取数据。

 在程序中,我们应对阻塞IO的解决办法,一般都线程池连接池等,具体是单个线程负责一个连接,但是对于并发连比较大,以上就不是用了;


非阻塞IO

 对于例1,小明的做法是,一边在客厅看电视,每隔一段时间去厨房看看。在程序中,就需要进程不断系统调用,去查看内核是否准备好数据,一旦准备好,执行上面阻塞IO的操作。
在Linux 中可以利用fcntl()方法将的套接字socket默认从阻塞改成非阻塞。

5种常见的IO模型及IO复用

在非阻塞下, recvfrom返回值为:

0,表示接收到的字节数;
=0,表示连接已断开;
==-1,且errno等于EAGAIN,表示内核还没准备好数据;
==-1,且errno不等于EAGAIN,表示调用出错。


多路IO复用

 在例1的基础上,小明同时烧多壶水,而且基于水壶的定制的通知机制,等到某壶水烧好了,发出独特的叫声,这样小明就可以具体处理哪一壶水。
 以上是epoll的多路IO复用模型,而对于select、poll水壶就没有定制的铃声,就需要去挨个查看。

以select为例:
5种常见的IO模型及IO复用

 程序是阻塞于select的,等到数据准备好了【将数据复制到了用户区】,然后挨个轮询,找出对应的文件描述符,处理相应的读写。

对于select、poll、epoll各自的优缺点呢?

select

#include <sys/select.h>
#include <sys/time.h>

int select (int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
/* Returns: positive count of ready descriptors, 0 on timeout, –1 on error */
  • 读写需要将文件描述符从用户区拷贝到内核区,开销大

  • 支持的客户端文件描述符有限。

poll

#include <sys/poll.h>

int poll (struct pollfd *fdarray, unsigned long nfds, int timeout);

/* Returns: count of ready descriptors, 0 on timeout, –1 on error */
  • 和select类似,不同的是,基于链表来存储的,但也需要去轮询。

epoll

#include <sys/epoll.h>

int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
  • epoll是线程安全的,而select、poll不是的;

  • epoll内部使用mmap()共享了内核和用户的部分空间,减少了来回复制;

  • epoll基于事件驱动,先制造一个句柄,再注册套接字,最后去轮询准备好了的事件,避免了整个的轮询;

信号驱动

 如下图所示:

5种常见的IO模型及IO复用


异步IO

 在例1中,小明就不自己烧水了,而是“雇”其他人去烧水,自己做自己的事了。在程序中,读写完全由内核来处理。当用户发起异步的读操作,aio_read,不再阻塞任何,直接返回,等待内核准备数据,直到数据拷贝到用户内存,通知程序,操作完成。


 如下图:


问题:

同步和异步:
  定义:【同步】直到IO操作完成之前,IO操作造成了请求程序的阻塞;
【异步】IO操作不会造成任何的阻塞。

以5中IO操作为具体例子,如图:

可以看到:

阻塞IO一直被阻塞,直到有数据;
非阻塞IO不被阻塞,直到有数据可读,程序执行具体的读写逻辑操作,此处被阻塞;
多路复用IO阻塞于epoll_wait,直到有数据,读写处再次被阻塞;
信号驱动,当内核出过来SIGIO后,执行读写逻辑操作处被阻塞;

异步IO完全交由内核,自己不做任何处理,不阻塞。

阻塞和非阻塞
同上。