前段时间听了boss栋的nio的讲座,有很多没有理解的地方,最近特意又研究了一下,写一下心得。
计算机中io分为网络io以及磁盘io,我们这里主要说的就是网络io。
http://blog.csdn.net/zhxue123/article/details/22285957中提到:
一般来说,服务器端的I/O主要有两种情况:一是来自网络的I/O;二是对文件(设备)的I/O。Windows的异步I/O模型能很好的适用于这两种情况。而Linux针对前者提供了epoll模型,针对后者提供了AIO模型(关于是否把两者统一起来争论了很久)。
epoll跟AIO的区别?
http://www.cnblogs.com/Anker/p/3265058.html 中介绍poll流程时,提到:
__pollwait的主要工作就是把current(当前进程)挂到设备的等待队列中,不同的设备有不同的等待队列,对于tcp_poll来说,其等待队列是sk->sk_sleep(注意把进程挂到等待队列中并不代表进程已经睡眠了)。在设备收到一条消息(网络设备)或填写完文件数据(磁盘设备)后,会唤醒设备等待队列上睡眠的进程,这时current便被唤醒了。
网络io与磁盘io又有共同点?
以上是两个疑问,搜了以下AIO,原来是异步io
Linux 2.6内核包含对一套新的异步IO,即AIO 的支持为用户空间提供统一的异步I/O 接口。目前好像只支持本地IO。有时间再学习。
io模型分为五种,阻塞io,非阻塞io,io复用,信号驱动io,异步io
而nio就是实现的io复用模型
操作系统实现IO复用模型,一般通过3个系统调用poll,select和epoll,其中poll和select实现相似,在这里统一讲解。epoll实现略有差别,我们单独讲解,在目前的JDK实现中,优先选择epoll实现。
select/poll实现原理图
Select/poll在系统调用时一般需要3个参数:
待检查的文件描述符
待检查的文件描述符对应的事件
超时时间
操作系统会逐个检查待检查的文件(Sock是Socket在内核中的数据结构),是否有事件就绪。每个文件也有一个poll方法,主要是检查自己的某个事件是否准备就绪。我们也用一个简化的模型来分析,每个文件有一个事件和状态的对应关系,当事件就绪就设置此状态。同时每个事件还有一个等待队列。
当事件状态是就绪状态,返回就绪。
否则创建一个内部对象放入等待队列,这个内部对象有一个回调方法,用来唤醒当前进程。
如果有事件就绪返回就绪文件的个数。
如果所有的文件都没有事件就绪,则阻塞进程,阻塞时间为timeout。
在阻塞期间,如果设备通过中断促使某些事件变为就绪,则对事件对应的等待列表进行回调唤醒阻塞的进程。
epoll原理图
由于poll和select系统调用会把所有的文件当作参数传递给操作系统,每次调用会产生大量用户空间到内核空间(内核空间到用户空间)的拷贝。而且由于每次对所有的文件进行遍历,如果存在大量不活跃的文件导致性能的急剧下降。所以提出了一个新的系统调用epoll。
它通过3个系统调用来完成:
poll_create:会创建一个文件,其实是创建了在内核的数据结构,在Linux中一切设备都是文件,epoll可以理解为一个虚拟设备。
poll_ctl:向epoll中加入/删除/修改待检测的文件描述符。当文件描述符加入待检测列表时,epoll会向此文件注册回调函数,在文件某事件就绪时会回调此函数。函数的默认实现是把此文件加入事件就绪列表中。
poll_wait:检查是否有就绪的文件描述符。直接检查就绪列表,如果有数据就返回就绪个数,否则返回空。
由于epoll是通过事件通知的方式获取就绪列表的,所以它只关注活跃的文件,这样对于大量不活跃文件的监控有更好的效率,但由于它实现的复杂性,当所检测的文件大多是活跃的情况,它的性能有可能低于select或poll。
个人理解,当文件活跃时,会调用回调函数,如果绝大部分都活跃,意味着都要调用回调函数,这个时候跟poll中轮询文件效率差不多甚至还要差
具体介绍,还可以参考 http://www.cnblogs.com/Anker/p/3265058.html
其中介绍poll流程时,有一段话
遍历所有fd,调用其对应的poll方法(对于socket,这个poll方法是sock_poll,sock_poll根据情况会调用到tcp_poll,udp_poll或者datagram_poll)
由poll的原理图,可以看出,每一个Socket对应一个Sock(Sock是Socket在内核中的数据结构),我理解为poll epoll中常被提到的文件描述符,poll遍历每个fd时,需要调用每个fd对应的poll方法(每个文件也有一个poll方法),主要是检查自己的某个事件是否准备就绪。
总结一下select/poll与epoll
(1)select,poll实现需要自己不断轮询所有fd集合,直到设备就绪,期间可能要睡眠和唤醒多次交替。而epoll其实也需要调用epoll_wait不断轮询就绪链表,期间也可能多次睡眠和唤醒交替,但是它是设备就绪时,调用回调函数,把就绪fd放入就绪链表中,并唤醒在epoll_wait中进入睡眠的进程。虽然都要睡眠和交替,但是select和poll在“醒着”的时候要遍历整个fd集合,而epoll在“醒着”的时候只要判断一下就绪链表是否为空就行了,这节省了大量的CPU时间。这就是回调机制带来的性能提升。
(2)select,poll每次调用都要把fd集合从用户态往内核态拷贝一次,并且要把current往设备等待队列中挂一次,而epoll只要一次拷贝,而且把current往等待队列上挂也只挂一次(在epoll_wait的开始,注意这里的等待队列并不是设备等待队列,只是一个epoll内部定义的等待队列)。这也能节省不少的开销。
另外,知乎上的讨论帖,对于拓宽考虑nio问题的视野很有帮助 如 http://www.zhihu.com/question/32163005中提到以下一些观点
1、
第一种方法就是最传统的多进程并发模型 (每进来一个新的I/O流会分配一个新的进程管理。)
第二种方法就是I/O多路复用 (单个线程,通过记录跟踪每个I/O流(sock)的状态,来同时管理多个I/O流 。)
2、
于是5年以后, 在2002, 大神 Davide Libenzi 实现了epoll.
epoll 可以说是I/O 多路复用最新的一个实现,epoll 修复了poll 和select绝大部分问题, 比如:
- epoll 现在是线程安全的。
- epoll 现在不仅告诉你sock组里面数据,还会告诉你具体哪个sock有数据,你不用自己去找了。
io多路复用避免阻塞在io上,原本为多进程或多线程来接收多个连接的消息变为单进程或单线程保存多个socket的状态后轮询处理
实现io多路复用需要函数来支持,就是你说的linux下的select/poll,epoll以及win下 iocp 和 BSD 的 kqueue
如果楼主看不懂它们的区别,建议查查资料或者看看apue或unp的相关章节弥补一下不理解的部分。
简单说epoll和select/poll最大区别是
1.epoll内部使用了mmap共享了用户和内核的部分空间,避免了数据的来回拷贝
2.epoll基于事件驱动,epoll_ctl注册事件并注册callback回调函数,epoll_wait只返回发生的事件避免了像select和poll对事件的整个轮寻操作。
nginx中使用了epoll,是基于事件驱动模型的,由一个或多个事件收集器来收集或者分发事件,epoll就属于事件驱动模型的事件收集器,将注册过的事件中发生的事件收集起来,master进程负责管理worker进程
最近又做了一点研究,现在流程大体了解了