四种 IO 模型:
首先需要明确,IO发生在 用户进程 与 操作系统 之间。可以是客户端IO也可以是服务器端IO。
- 阻塞IO(blocking IO):
在linux中,默认情况下所有socket都是blocking:kernel和用户都在阻塞等待数据。
- 非阻塞IO(non-blocking):
用户程序重复调用系统调用 recvfrom ,等待 return OK信号。 polling轮询
为什么没有Block:用户进程发出read请求后 ,如果kernel中的数据还没有准备好,那么就会立即返回来一个error信号。对于用户角度来讲他并没有等待,而是立即就得到了一个结果。接下来,用户进程就会不断主动询问kernel有没有准备好。
- IO多路复用(IO multiplexing)
常用的方法就是 select 和 epoll 。这种方法也和叫作 事件驱动IO。
这种方式的好处在于:kernel的单个process可以同时处理多个网络连接IO。将轮询的工作统一交给kernel内核监管的函数select/epoll(该函数由用户态 调用,kernel监管)函数。用户可以将多个socket注册到同一个select中,kernel来监管select中注册的IO,一旦发任何一个socket现数据准备好了,就将通知用户process,调用read将数据从kernel拷贝到用户进程。这样用户process就从轮询中解放出来,只需要静静地等待select函数返回即可。当然,这个过程中,用户也是block的,虽然是block的(被select block) 但是什么都不做的block和不断去 “轮询”的block,还是不同的!这样可以减少用户process的负担。
这里就要插入另外一个内容:select、poll、epoll
select主要的缺陷有三个:
1)最大并法数限制:受限于一个进程能够打开的FD(文件描述符)的个数。在linux/posix_types.h头文件有这样的声明:#define __FD_SETSIZE 1024 表示select最多同时监听1024个,因此最大并发数就被限制了。
2)效率问题:select每次都会线性扫描【类似数组来存储】全部的FD集合,这样效率就会呈线形下降。
3)内核/用户空间内存拷贝。
poll 基本等于select 后两点都没有改善
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 就有如下优点:
1) 最大并发数不受限制。上限是:当前系统中打开文件的最大数目。
2)效率高。因为epoll只管当前活跃的socket,epoll维护一颗红黑树。那些注册了的socket或者说是文件描述符,就会高效的在树上插入或者删除。
epoll的高效就在于(引用):“epoll_ctl往里塞入百万个句柄时,epoll_wait仍然可以飞快的返回,并有效的将发生事件的句柄给我们用户。这是由于我们在调用epoll_create时,内核除了帮我们在epoll文件系统里建了个file结点,在内核cache里建了个红黑树用于存储以后epoll_ctl传来的socket外,还会再建立一个list链表,用于存储准备就绪的事件,当epoll_wait调用时,仅仅观察这个list链表里有没有数据即可。有数据就返回,没有数据就sleep,等到timeout时间到后即使链表没数据也返回。所以,epoll_wait非常高效。
而且,通常情况下即使我们要监控百万计的句柄,大多一次也只返回很少量的准备就绪句柄而已,所以,epoll_wait仅需要从内核态copy少量的句柄到用户态而已,如何能不高效?!
那么准备就绪的列表list是怎么维护的呢?当我们执行epoll_ctl的时候,除了把socket放到epoll文件系统里file对应的红黑树上之外,还会在内核的中断处理程序上注册一个回调函数,告诉内核,如果这个句柄的中断到了,就把它放到准备就绪的list中,也就是当socket有数据到达后,内核会把网卡上的数据copy到内核中后就把socket插入到准备列表list中。”
最后,总结一下:
1 当用户调用epoll_create后,内核会在内存cache中创建一个红黑树和就绪列表
2 当用户调用epoll_ctl注册 socket(文件句柄)时,内核会在红黑树上查找是否有该句柄。如果存在,则立即返回。如果没有,那么在红黑树的节点上增加,然后在内核注册回调函数,用于当中断来临的时候,向中断就绪链表中插入数据。
3 当用户调用epoll_wait时,立即返回就绪列表中的数据。
关于epoll的LT(水平触发)和ET(边缘触发)两种工作模式:
LT:只要改句柄上的事件没有处理完,那就此此返回。
ET:只返回一次。
实现的原理在于对list就绪列表的清空机制上。清空后,如果是LT且未处理完,就再放回到就绪列表中。
- 异步IO(Asynchronous IO)
用户发起read之后,服务器端kernel会立刻返回。用户process就会去做其他事情去了。这种情况下,对于服务器来说,接收到每一个IO请求都需要开辟一个线程去专门去处理。
3)用户与内核共享内存,no more 内存拷贝。
######################################################################################
最后说一说,
1)阻塞Block与非阻塞Nonblock的区别:
Block会让用户进程Block住直到请求的IO操作完成;NonBlock会在kernel正在准备数据的时候,立即返回一个结果,知道这个结果是OK就绪信号,那么接下来进程读取数据,done;
2)同步synchronous与异步asynchronous的区别:
首先定义:
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;
上面所讲的block IO 、Non—block IO、IO 多路复用都属于 同步IO。Non-block 与其他方式一样,这里所说的block是当发生IO operation的时候,也就是调用 recv_from系统调用时,会发生block,以完成数据从内核态copy到用户态的过程。以上三种方式都是会block的。而asynchronous IO则不一样,当进程发起IO 操作之后,就直接返回再也不理睬了,直到kernel发送一个信号,告诉进程说IO完成。在这整个过程中,进程完全没有被block。
各个IO Model的比较如图所示:
最后,再举几个不是很恰当的例子来说明这四个IO Model:
有A,B,C,D四个人在钓鱼:
A用的是最老式的鱼竿,所以呢,得一直守着,等到鱼上钩了再拉杆;
B的鱼竿有个功能,能够显示是否有鱼上钩,所以呢,B就和旁边的MM聊天,隔会再看看有没有鱼上钩,有的话就迅速拉杆;
C用的鱼竿和B差不多,但他想了一个好办法,就是同时放好几根鱼竿,然后守在旁边,一旦有显示说鱼上钩了,它就将对应的鱼竿拉起来;
D是个有钱人,干脆雇了一个人帮他钓鱼,一旦那个人把鱼钓上来了,就给D发个短信。