IO多路复用的 select、poll、epoll详解

前几篇文章讲述了IO的几种模式及netty的基本概念,netty基于多路复用模型下的reactor模式,对 大量链接、单个处理短且快 的场景很适用 。html

那在往底层思考,linux对于IO又是如何处理的呢react

C10K 问题

http://www.52im.net/thread-566-1-1.htmllinux

最初的服务器都是基于进程/线程模型的,新到来一个TCP链接,就须要分配1个进程(或者线程)。而进程又是操做系统最昂贵的资源,一台机器没法建立不少进程。若是是C10K就要建立1万个进程,那么单机而言操做系统是没法承受的(每每出现效率低下甚至彻底瘫痪)。若是是采用分布式系统,维持1亿用户在线须要10万台服务器,成本巨大。基于上述考虑,如何突破单机性能局限,是高性能网络编程所必需要直面的问题。这些局限和问题最先被Dan Kegel 进行了概括和总结,并首次成系统地分析和提出解决方案,后来这种广泛的网络现象和技术局限都被你们称为 C10K 问题。redis

C10K 问题的最大特色是:设计不够良好的程序,其性能和链接数及机器性能的关系每每是非线性的

举个例子:若是没有考虑过 C10K 问题,一个经典的基于 select 的程序能在旧服务器上很好处理 1000 并发的吞吐量,它在 2 倍性能新服务器上每每处理不了并发 2000 的吞吐量。这是由于在策略不当时,大量操做的消耗和当前链接数 n 成线性相关。会致使单个任务的资源消耗和当前链接数的关系会是 O(n)。而服务程序须要同时对数以万计的socket 进行 I/O 处理,积累下来的资源消耗会至关可观,这显然会致使系统吞吐量不能和机器性能匹配。编程

以上这就是典型的C10K问题在技术层面的表现。C10K问题本质上是操做系统的问题。对于Web1.0/2.0时代的操做系统而言, 传统的同步阻塞I/O模型都是同样的,处理的方式都是requests per second,并发10K和100的区别关键在于CPU。建立的进程线程多了,数据拷贝频繁(缓存I/O、内核将数据拷贝到用户进程空间、阻塞), 进程/线程上下文切换消耗大, 致使操做系统崩溃,这就是C10K问题的本质!
可见,解决C10K问题的关键就是尽量减小这些CPU等核心计算资源消耗,从而榨干单台服务器的性能,突破C10K问题所描述的瓶颈。segmentfault

概念说明

用户空间与内核空间

如今操做系统都是采用虚拟存储器,那么对32位操做系统而言,它的寻址空间(虚拟存储空间)为4G(2的32次方)。操做系统的核心是内核,独立于普通的应用程序,能够访问受保护的内存空间,也有访问底层硬件设备的全部权限。为了保证用户进程不能直接操做内核(kernel),保证内核的安全,操心系统将虚拟空间划分为两部分,一部分为内核空间,一部分为用户空间。针对linux操做系统而言,将最高的1G字节(从虚拟地址0xC0000000到0xFFFFFFFF),供内核使用,称为内核空间,而将较低的3G字节(从虚拟地址0x00000000到0xBFFFFFFF),供各个进程使用,称为用户空间。数组

处在内核空间称为内核态,用户空间称为用户态! 内核的权限远大于用户空间权限,硬件、IO等等系统操做只能经过内核调用!缓存

进程切换

为了控制进程的执行,内核必须有能力挂起正在CPU上运行的进程,并恢复之前挂起的某个进程的执行。这种行为被称为进程切换。安全

进程的阻塞

正在执行的进程,因为期待的某些事件未发生,如请求系统资源失败、等待某种操做的完成、新数据还没有到达或无新工做作等,则由系统自动执行阻塞原语(Block),使本身由运行状态变为阻塞状态。可见,进程的阻塞是进程自身的一种主动行为,也所以只有处于运行态的进程(得到CPU),才可能将其转为阻塞状态。当进程进入阻塞状态,是不占用CPU资源的服务器

文件描述符fd

文件描述符(File descriptor)是计算机科学中的一个术语,是一个用于表述指向文件的引用的抽象化概念。

文件描述符在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者建立一个新文件时,内核向进程返回一个文件描述符。在程序设计中,一些涉及底层的程序编写每每会围绕着文件描述符展开。可是文件描述符这一律念每每只适用于UNIX、Linux这样的操做系统

I/O的socket操做也是一种文件描述符fd

缓存 I/O

缓存 I/O 又被称做标准 I/O,大多数文件系统的默认 I/O 操做都是缓存 I/O。在 Linux 的缓存 I/O 机制中,操做系统会将 I/O 的数据缓存在文件系统的页缓存( page cache )中,也就是说,数据会先被拷贝到操做系统内核的缓冲区中,而后才会从操做系统内核的缓冲区拷贝到应用程序的地址空间。

缓存 I/O 的缺点:数据在传输过程当中须要在应用程序地址空间和内核进行屡次数据拷贝操做,这些数据拷贝操做所带来的 CPU 以及内存开销是很是大的

I/O 多路复用之select、poll、epoll详解

详细部分能够参阅:
http://www.javashuo.com/article/p-sqagmtjn-x.html; 
http://www.javashuo.com/article/p-ocfrejfu-ng.html

select,poll,epoll都是IO多路复用的机制。一个进程能够监视多个描述符,一旦某个描述符就绪(通常是读就绪或者写就绪),可以通知程序进行相应的读写操做

select,poll,epoll本质上都是同步I/O,由于他们都须要在读写事件就绪后本身负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需本身负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。

Netty与redis(单线程的下的I/O多路复用) 使用epoll模式

select/poll的几大缺点

  1. select的本质是采用32个整数的32位,即32*32= 1024来标识,fd值为1-1024。(句柄上限+重复初始化+逐个排查全部文件句柄状态效率不高)
    1. 当fd的值超过1024限制时,就必须修改FD_SETSIZE(管理的句柄上限)的大小,这个时候就能够标识32*max值范围的fd。
    2. 在使用上,由于只有一个字段记录关注和发生事件,每次调用以前要从新初始化 fd_set 结构体。
    3. select的触发方式是水平触发,应用程序若是没有完成对一个已经就绪的文件描述符进行IO操做,那么以后每次select调用仍是会将这些文件描述符通知进程。
  2. poll主要解决 select 的前两个问题,但仍是得逐个排查全部文件句柄状态效率不高:
    1. 经过一个pollfd数组向内核传递须要关注的事件,故没有描述符个数的限制。
    2. pollfd中的events字段和revents分别用于标示关注的事件和发生的事件,故pollfd数组只须要被初始化一次。
  3. select/poll 将这个fd列表维持在用户态, 每次调用时都须要把fd集合从用户态拷贝到内核态, 并在内核中遍历传递进来的全部fd; 返回的的是含有整个句柄的数组,应用程序须要遍历整个数组才能发现哪些句柄发生了事件

epoll

epoll是poll的一种优化,在内核中维持了fd的列表,只遍历发生事件的fd集合。

与poll/select不一样,epoll再也不是一个单独的系统调用,而是由epoll_create/epoll_ctl/epoll_wait三个系统调用组成,epoll在2.6之后的内核才支持。

综合的来讲:

epoll在内核中申请一个简易的文件系统,把原先的select/poll调用分红了3个部分。链接的套接字(socket句柄)是采用红黑树的结构存储在内核cache中的,并给内核中断处理程序注册一个回调函数,告诉内核:若是这个句柄的中断到了,就把它放到准备就绪list链表里。

当有事件准备就绪时,内核在把网卡上的数据copy到内核中后就来把socket插入到准备就绪链表里了(epoll的基础是回调)。当epoll_wait调用时,仅仅观察这个list链表里有没有数据便可;有数据就返回,没有数据就sleep,等到timeout时间到后即便链表没数据也返回。

1)调用epoll_create创建一个epoll对象(在epoll文件系统中为这个句柄对象分配资源, 建立了红黑树和就绪链表)
2)调用epoll_ctl向epoll对象中添加这100万个链接的套接字 (若是增长socket句柄,则检查在红黑树中是否存在,存在当即返回,不存在则添加到树干上,而后向内核注册回调函数,用于当中断事件来临时向准备就绪链表中插入数据)
3)调用epoll_wait收集发生的事件的链接 (马上返回准备就绪链表里的数据)

两种模式LT和ET

ET是边缘触发,LT是水平触发,一个表示只有在变化的边际触发,一个表示在某个阶段都会触发。

当一个socket句柄上有事件时,内核会把该句柄插入上面所说的准备就绪list链表,这时咱们调用epoll_wait,会把准备就绪的socket拷贝到用户态内存,而后清空准备就绪list链表,最后,epoll_wait干了件事,就是检查这些socket,若是不是ET模式(就是LT模式的句柄了),而且这些socket上确实有未处理的事件时,又把该句柄放回到刚刚清空的准备就绪链表了。因此,非ET的句柄,只要它上面还有事件,epoll_wait每次都会返回这个句柄。(从上面这段,能够看出,LT还有个回放的过程,低效了)

场景假设

eg.有100万个客户端同时与一个服务器进程保持着TCP链接。而每一时刻,一般只有几百上千个TCP链接是活跃的(事实上大部分场景都是这种状况)。如何实现这样的高并发?

在select/poll时代,服务器进程每次都把这100万个链接告诉操做系统(从用户态复制句柄数据结构到内核态),让操做系统内核去查询这些套接字上是否有事件发生,轮询完后,再将句柄数据复制到用户态,让服务器应用程序轮询处理已发生的网络事件,这一过程资源消耗较大,所以,select/poll通常只能处理几千的并发链接。若是没有I/O事件产生,咱们的程序就会阻塞在select处。可是依然有个问题,咱们从select那里仅仅知道了,有I/O事件发生了,但却并不知道是那几个流(可能有一个,多个,甚至所有),咱们只能无差异轮询全部流,找出能读出数据,或者写入数据的流,对他们进行操做。处理的流越多,每一次无差异轮询时间就越长!

相关文章
相关标签/搜索