Linux下的五种IO模型

引言

本文咱们主要了解一下Unix/Linux下5种网络IO模型:blocking IO, nonblocking IO, IO multiplexing, signal driven IO, asynchronous IO以及select/poll/epoll的基本原理,更好的理解在高级语言中的异步编程,但以理解概念为主,并不会涉及到具体的C语言代码编写,若是想要深刻的朋友建议阅读Richard Stevens的Unix Network Programming。git

写在前面

为了更好的理解下面提到的Linux下5种网络IO的概念,咱们仍是有必要先理清几个概念。github

1.程序空间与内核空间

在Linux中,对于一次读取IO的操做,数据并不会直接拷贝到程序的程序缓冲区。它首先会被拷贝到操做系统内核的缓冲区中,而后才会从操做系统内核的缓冲区拷贝到应用程序的缓冲区。p.s: 最后一句话很是重要,重复一遍。算法

  1. Waiting for the data to be ready(等待数据到达内核缓冲区)
  2. Copying the data from the kernel to the process(从内核缓冲区拷贝数据到程序缓冲区)

2.阻塞与非阻塞

阻塞就是说咱们某一个请求不能当即获得返回应答,不然就能够理解为非阻塞。编程

3.同步IO与异步IO

这里先直接引用Stevens(POSIX)的定义:网络

A synchronous I/O operation causes the requesting process to be blocked until that I/O operation completes. An asynchronous I/O operation does not cause the requesting process to be blocked.异步

对于同步与异步,咱们能够用一个简单的生活场景来描述。当咱们排队在实体店买东西能够视做同步,而网购则能够视做异步。实体店排队这种同步情形显然是很是的浪费时间,等待的这段时间咱们被阻塞住了不能干其余的事情,而网购只要咱们提交一下订单以后其余什么都不用管了,商品到了,快递员给咱们发送一个信号(打电话)咱们直接到门口去拿,等待的这段时间咱们能够用来撸代码。socket

p.s: 等你阅读完文章的后面部分,回过头来看异步其实就是将等待的这段时间去处理IO操做,把CPU(咱们的大脑)让出来作其余更有价值的事情(撸代码),而不是像同步那样去傻傻地排队。更加详细准确的定义能够在阅读完本文后面部分后参考维基百科async

4.文件描述符

在Linux下面一切皆文件,文件描述符(file descriptor)是内核为文件所建立的索引,全部I/O操做都经过调用文件描述符(索引)来执行,包括下面咱们要提到的socket。Linux刚启动的时候会自动设置0是标准输入,1是标准输出,2是标准错误。异步编程

5种网络IO模型

你们仍是应该多结合Stevens的图片来理解,不要只看我枯燥的文字总结。函数

1.blocking IO(阻塞IO)

blocking IO

如图所示,进程调用一个recvfrom请求,可是它不能马上收到回复,直到数据返回,而后将数据从内核空间复制到程序空间。这里咱们再次回顾开篇提到的两个过程:

  1. Waiting for the data to be ready(等待数据到达内核缓冲区)
  2. Copying the data from the kernel to the process(从内核缓冲区拷贝数据到程序缓冲区)

注意到没有,在上面这两个过程当中,进程都处于blocked(阻塞)状态,在等待数据返回的过程当中不能空闲出来干其余的事情。

2.nonblocking IO(非阻塞IO)

当咱们设置一个socket为nonblocking(非阻塞),至关于告诉内核当咱们请求的IO操做不能当即获得返回结果,不要把进程设置为sleep状态,而是返回一个错误信息(下图中的EWOULDBLOCK)。

nonblocking IO

咱们来分析一下图片中的整个流程。前三次咱们调用recvfrom请求,可是并无数据返回,因此内核只能返回一个错误信息(EWOULDBLOCK)。可是当咱们第四次调用recvfrom,数据已经准备好了,而后将它从内核空间复制到程序空间。

在非阻塞状态下,咱们的过程一(wait for data)并非彻底的阻塞的,可是过程二(copy data from kernel to user)依然处于一个阻塞状态。

3.IO multiplexing(IO复用)

IO复用的好处是咱们能够经过(select/poll/epoll)一个时刻处理多个文件描述符,这里以select为例来分析一下。

IO multiplexing

IO复用实际上也是彻底阻塞的,请仔细看图(图中咱们有两个return,前面咱们都只有一个return),Stevens在书中提到这里并无阻塞在recfrom阶段而是阻塞在select阶段,其实这样说并非很是的严谨,由于recform其实也是一个阻塞过程(图中也描述了),recvfrom过程当中进程除了等待copy data from kernel to user之外,并不能空闲出来干其余事情。

两个过程的都是阻塞的,看起来IO复用和阻塞IO相比彷佛并无什么优点,并且还须要两个return,可是这里注意在IO复用中咱们能够同时监听多个文件描述符。

4.signal driven IO(信号驱动IO)

咱们也可使用信号驱动IO,要求内核通知咱们当文件描述符准备就绪之后发送相应的信号。

signal driven IO

咱们根据图片来分析一下。阶段1: 咱们首先设置socket为一个信号驱动IO,而且经过sigaction system call安装一个signal handler,注意这个过程是瞬时的,因此这个阶段是非阻塞的。阶段2: 当数据已经准备好了之后,一个SIGIO信号传送给咱们的进程告诉咱们数据准备好了,而后进程开始等待数据从内核空间复制到程序空间(copy data from kernel to user),这个过程是阻塞的,由于咱们的进程只能等待数据复制完毕。

5.asynchronous IO(异步IO)

咱们来看一下异步的概念,异步就是说对于上面两个步骤(wait for data 和copy of the data from the kernel to our buffer)当它们完成的时候会自动通知进程,在这段时间里面进程什么都不用操心,就像网购同样,下了单什么也不用管了等着快递员通知咱们(即咱们一般所说的callback)。相比前面的信号驱动IO,异步IO两个阶段都是非阻塞的。

asynchronous IO

6.小结

总结

阻塞式IO(默认),非阻塞式IO(nonblock),IO复用(select/poll/epoll),signal driven IO(信号驱动IO)都是属于同步型IO,由于在第二个阶段: 从内核空间拷贝数据到程序空间的时候不能干别的事。只有异步I/O模型(AIO)才是符合咱们上面对于异步型IO操做的含义,在1.wait for data,2.copy data from kernel to user,这两个等待/接收数据的时间段内进程能够干其余的事情,只要等着被通知就能够了。

select/poll/epoll

即便如今的各个Linux版本广泛引入了copy on write和线程,但实际上进程/线程之间的切换依然仍是一笔很大的开销,这个时候咱们能够考虑使用上面提到到多路IO复用,回顾一下咱们上面提到的多路IO复用模型的基本原理:一个进程能够监视多个文件描述符,一旦某个文件描述符就绪(读/写准备就绪),可以信号通知程序进行相应的读写操做。下面咱们就来简单的看一下多路IO复用的三种方式。

select

int select (int maxfdp1, fd_set *readset, fd_set *writeset, fd_set *exceptset,
            const struct timeval *timeout);

如上面的方法声明所示, select监听三类描述符: readset(读), writeset(写), exceptset(异常), 咱们编程的时候能够制定这三个参数监听对应的文件描述符。正如前面提到的,select调用后进程会阻塞, 当select返回后,能够经过遍历fdset,来找到就绪的描述符。

select优势在于它的跨平台,可是也有显著的缺点单个进程可以监视的文件描述符的数量存在最大限制,默认设置为1024/2048,虽然设置能够超过这一限制,可是这样也可能会形成效率的下降。并且select扫描的时候也是采用的轮循,算法复杂度为O(n),这在fdset不少时效率会较低。

下面总结一下select的三个缺点,在下面咱们来看epool是如何解决这些缺点的:

每次调用select,都须要将fd_set从用户态拷贝到内核态。
每次调用select都要在内核遍历全部传递过来的fd_set看哪些描述已经准备就绪。
select有1024的容量限制。

poll

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

poll和select并无太大的区别,可是它是基于链表实现的因此并无最大数量限制,它将用户传入的数据拷贝到内核空间,而后查询每一个fd对应的设备状态,若是设备就绪则在设备等待队列中加入一项并继续遍历,若是遍历完全部fd后没有发现就绪设备,则挂起当前进程,直到设备就绪或者主动超时,被唤醒后它又要再次遍历fd。这个过程经历了屡次的遍历。算法复杂度也是O(n)

epoll

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);

select和poll都只提供了一个函数。而epoll提供了三个函数: epoll_create是建立一个epoll句柄, epoll_ctl是注册要监听的事件类型, epoll_wait则是等待事件的产生。与select相比,epoll几乎没有描述符限制(cat /proc/sys/fs/file-max可查看)。它采用一个文件描述符管理多个描述符,将用户的文件描述符的事件存放到kernel的一个事件表中,这样在程序空间和内核空间的只要作一次拷贝。它去掉了遍历文件描述符这一步骤,采用更加先进的回调(callback)机制,算法复杂度降到了O(1)。p.s: 虽然表面看起来epoll很是好,可是对于链接数少而且链接都十分活跃的状况下,select和poll的性能可能比epoll好,由于epoll是创建在大量的函数回调的基础之上。

下面咱们来总结一下epoll是如何解决select的三个缺点的:

  1. 在epoll_ctl每次注册事件到epoll句柄的时候,会将fd拷贝到内核中,保证了每一个fd在整个过程当中只会拷贝一次。
  2. 对于第二缺点,epool_ctl为每一个fd指定一个回调函数,当设备就绪,就会调用这个回调函数,而这个回调函数会把准备就绪的fd加入一个就绪链表,而不用像select那样去从新遍历一次看有哪些准备就绪的文件描述符。
  3. 对于第三个缺点,咱们上面已经说起到了,epoll几乎没有容量限制,能够经过cat /proc/sys/fs/file-max来查看。

References

UNIX NETWORK PROGRAMMING

Contact

GitHub: https://github.com/ziwenxie
Blog: https://www.ziwenxie.site

本文为做者原创,转载请于文章开头明显处声明博客出处:)

相关文章
相关标签/搜索