四种常用IO:
对于一次IO访问(以read举例),数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。所以说,当一个read操作发生时,它会经历两个阶段:
- 等待数据准备 (Waiting for the data to be ready)
- 将数据从内核拷贝到进程中 (Copying the data from the kernel to the process)
正式因为这两个阶段,linux系统产生了下面五种网络模式的方案。
- 阻塞 I/O(blocking IO)
- 非阻塞 I/O(nonblocking IO)
- I/O 多路复用( IO multiplexing)
- 信号驱动 I/O( signal driven IO)
- 异步 I/O(asynchronous IO)
注:由于signal driven I/O在实际中并不常用,所以我这只提及剩下的四种I/O Model。
1、阻塞IO
在linux中,默认情况下所有的socket都是blocking,一个典型的读操作流程大概是这样:
当用户进程调用了recvfrom这个系统调用,kernel就开始了I/O的第一个阶段:准备数据(对于网络IO来说,很多时候数据在一开始还没有到达。比如,还没有收到一个完整的UDP包。这个时候kernel就要等待足够的数据到来)。这个过程需要等待,也就是说数据被拷贝到操作系统内核的缓冲区中是需要一个过程的。而在用户进程这边,整个进程会被阻塞(当然,是进程自己选择的阻塞)。当kernel一直等到数据准备好了,它就会将数据从kernel中拷贝到用户内存,然后kernel返回结果,用户进程才解除block的状态,重新运行起来。
所以,blocking I/O的特点就是在I/O执行的两个阶段都被block了。
2、非阻塞IO
linux下,可以通过设置socket使其变为non-blocking。当对一个non-blocking socket执行读操作时,流程是这个样子:
当用户进程发出read操作时,如果kernel中的数据还没有准备好,那么它并不会block用户进程,而是立刻返回一个error。从用户进程角度讲 ,它发起一个read操作后,并不需要等待,而是马上就得到了一个结果。用户进程判断结果是一个error时,它就知道数据还没有准备好,于是它可以再次发送read操作。一旦kernel中的数据准备好了,并且又再次收到了用户进程的system call,那么它马上就将数据拷贝到了用户内存,然后返回。
所以,nonblocking I/O的特点是用户进程需要不断的主动询问kernel数据好了没有。
3、多路复用IO
IO multiplexing就是我们说的select,poll,epoll,有些地方也称这种IO方式为event driven IO。select/epoll的好处就在于单个process就可以同时处理多个网络连接的IO。它的基本原理就是select,poll,epoll这个function会不断的轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程。
当用户进程调用了select,那么整个进程会被block,而同时,kernel会“监视”所有select负责的socket,当任何一个socket中的数据准备好了,select就会返回。这个时候用户进程再调用read操作,将数据从kernel拷贝到用户进程。
所以,I/O 多路复用的特点是通过一种机制一个进程能同时等待多个文件描述符,而这些文件描述符(套接字描述符)其中的任意一个进入读就绪状态,select()函数就可以返回。
这个图和blocking IO的图其实并没有太大的不同,事实上,还更差一些。因为这里需要使用两个system call (select 和 recvfrom),而blocking IO只调用了一个system call (recvfrom)。但是,用select的优势在于它可以同时处理多个connection。
所以,如果处理的连接数不是很高的话,使用select/epoll的web server不一定比使用multi-threading + blocking IO的web server性能更好,可能延迟还更大。select/epoll的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。
在IO multiplexing Model中,实际中,对于每一个socket,一般都设置成为non-blocking,但是,如上图所示,整个用户的process其实是一直被block的。只不过process是被select这个函数block,而不是被socket IO给block。
4、异步IO
Linux下的asynchronous I/O其实用得很少。先看一下它的流程:
用户进程发起read操作之后,立刻就可以开始去做其它的事。而另一方面,从kernel的角度,当它受到一个asynchronous read之后,首先它会立刻返回,所以不会对用户进程产生任何block。然后,kernel会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后,kernel会给用户进程发送一个signal,告诉它read操作完成了。
其他
0、四种IO对比:
就好比去买一件商品
- 阻塞I/O:你自己跑去商店下单(只能一个一个来),等有了物品还要自己去拿回来
- 非阻塞I/O:你可以网上下单了,而且网上看有没有货,有的话自己去拿回来
- I/O多路复用:和第一种情况差不多,但是这个商店下单窗口多,可以同时多个人跑来下单,谁的货到了,自己来取
- 异步I/O:你只要网上下个单,其余就不管了。商家不仅发快递,快递小哥还把商品直接送到你家里(把门撬开放在你家客厅)
1、blocking和non-blocking区别:
调用blocking IO会一直block住对应的进程直到操作完成,而non-blocking IO在kernel还准备数据的情况下会立刻返回。
2、synchronous IO和asynchronous IO的区别
在说明synchronous IO和asynchronous IO的区别之前,需要先给出两者的定义。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;
两者的区别就在于synchronous IO做”IO operation”的时候会将process阻塞。按照这个定义,之前所述的blocking IO,non-blocking IO,IO multiplexing都属于synchronous IO。
有人会说,non-blocking IO并没有被block啊。这里有个非常“狡猾”的地方,定义中所指的”IO operation”是指真实的IO操作,就是例子中的recvfrom这个system call。non-blocking IO在执行recvfrom这个system call的时候,如果kernel的数据没有准备好,这时候不会block进程。但是,当kernel中数据准备好的时候,recvfrom会将数据从kernel拷贝到用户内存中,这个时候进程是被block了,在这段时间内,进程是被block的。
而asynchronous IO则不一样,当进程发起IO 操作之后,就直接返回再也不理睬了,直到kernel发送一个信号,告诉进程说IO完成。在这整个过程中,进程完全没有被block。
3、现状:
目前aio技术在linux上不成熟。
- glibc 的 aio采用多线程模拟,而且有bug;
- kernel 的 aio是真正的能做到内核的异步通知,目前nginx有添加AIO,但它同样有一些缺陷(只能以 O_DIRECT 方式做);
- libeio 也是 beta 阶段。
所以高并发系统多采用成熟的epoll。
二、select、poll、epoll
IO多路复用是指内核一旦发现进程指定的一个或者多个IO条件准备读取,它就通知该进程。与多进程和多线程技术相比,I/O多路复用技术的最大优势是系统开销小,系统不必创建进程/线程,也不必维护这些进程/线程,从而大大减小了系统的开销。目前支持I/O多路复用的系统调用有 select,pselect,poll,epoll。
1、select
select是POSIX所规定(跨平台),本质上是通过设置或者检查存放fd(文件描述符)标志位的数据结构来进行下一步处理。这样所带来的缺点是:
- 单个进程所打开的FD是有一定限制的,它由FD_SETSIZE设置,默认1024.
- 对socket进行扫描采用轮询的方法,效率较低,当socket比较多的时候,每次select()都要通过遍历FD_SETSIZE个Socket来完成调度,不管哪个Socket是活跃的,都遍历一遍。
- 每一次轮询,要将这些所有的socket描述符从用户态拷贝到内核态,在内核态,进行轮询,查看是否有事件发生,这是select的底层需要做的。而这些拷贝完全是可以避免的。
2、poll
poll的实现机制和select是一样的,也是采用轮询机制来查看有事件发生的socket描述符,改进是没有select的FD限制(因为采用了链表)
poll还有一个特点是“水平触发”,如果报告了fd后,没有被处理,那么下次poll时会再次报告该fd。
3、epoll
epoll是在2.6内核中提出的,是之前的select和poll的增强版本,改进了上面的缺点。在实现上引入了三个核心点:
- mmap
- 红黑树
- 就绪文件描述符双向链表
1)mmap:
对于poll来说需要将用户传入的 pollfd 数组拷贝到内核空间,因为拷贝操作和数组长度相关,时间上这是一个O(n)操作,当事件发生,poll返回将获得的数据传送到用户空间并执行释放内存和剥离等待队列等善后工作,向用户空间拷贝数据与剥离等待队列等操作的的时间复杂度同样是O(n)。
而epoll是共享内存,拷贝都不用,相对来说应该会更快。epoll是通过内核与用户空间mmap同一块内存实现的。
2)红黑树:
红黑树是用来存储这些描述符的,因为红黑树的特性,就是良好的插入,查找,删除性能O(lgN)。
3)就绪文件描述符双向链表:
当内核创建了红黑树之后,同时也会建立一个双向链表rdlist,用于存储准备就绪的描述符,当调用epoll_wait的时候在timeout时间内,只是简单的去管理这个rdlist中是否有数据,如果没有则睡眠至超时,如果有数据则立即返回并将链表中的数据赋值到events数组中。这样就能够高效的管理就绪的描述符,而不用去轮询所有的描述符。
对与rdlist的维护:当执行epoll_ctl时除了把socket描述符放入到红黑树中之外,还会给内核中断处理程序注册一个回调函数,告诉内核,当这个描述符上有事件到达(或者说中断了)的时候就调用这个回调函数。这个回调函数的作用就是将描述符放入到rdlist中,所以当一个socket上的数据到达的时候内核就会把网卡上的数据复制到内核,然后把socket描述符插入就绪链表rdlist中。
4)epoll的工作模式ET和LT
都知道epoll有两个工作模式,ET和LT,其中ET模式是高速模式,叫做边缘触发模式,LT模式是默认模式,叫做水平触发模式。
- LT模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用epoll_wait时,会再次响应应用程序并通知此事件。
- ET模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait时,不会再次响应应用程序并通知此事件。