了解epoll工作原理前,通过简单案例先了解下IO模型的发展历程
一、IO发展历程
BLOCKING-IO (阻塞IO模型)
嘿小黑去火车站春运买票,排队三天三夜买到一张票
成本:车站滞留三天,其他事一件没做
NON-BLOCKING IO(同步非阻塞IO)
嘿小黑去火车站春运买票,每隔12小时去火车站问有没有票,三天后买到一张票
成本:往返6次,路上消耗时间,但其他时间在做别的事
IO MULTIPLEXING(多路复用IO模型)
阶段1:select/poll
嘿小黑去火车站春运买票,委托黄牛,每隔6小时电话黄牛问有没有票,黄牛三天后买到一张票,嘿小黑去火车站交钱买票
成本:往返2次,路上消耗时间,黄牛手续费,打电话12次
阶段2:epoll
嘿小黑去火车站春运买票,黄牛买到票后通知嘿小黑去买,然后嘿小黑去火车站交钱买票
成本:往返2次,路上消耗时间,黄牛手续费,无需打电话
SIGNAL DRIVEN IO(信号驱动IO模型)
嘿小黑去火车站春运买票,给售票员留了电话,有票后电话通知嘿小黑,然后嘿小黑去火车站交钱买票
成本:往返2次,路上消耗时间,无需手续费,无需打电话
ASYNCHRONOUS IO(异步IO模型)
嘿小黑去火车站春运买票,给售票员留了电话,有票后电话通知嘿小黑并邮寄车票上门
成本:往返1次,路上消耗时间,无需手续费,无需打电话
了解完这个后,我们再开始看Linux的运作及发展历程
对于一次IO访问 ,会经历以下两个阶段:
1、准备数据
2、将数据从内核缓冲区拷贝到进程地址空间
因为存在这两个阶段,所以Linux产生下面三种模型
二、LINUX中 IO模型发展历程
1、阻塞IO模型
当用户进程调用了recvfrom等阻塞方法时,内核进入IO的第1个阶段:准备数据(内核需要等待足够的数据再拷贝)这个过程需要等待,用户进程会被阻塞,等内核将数据准备好,然后拷贝到用户地址空间,内核返回结果,用户进程才从阻塞态进入就绪态
Linux中默认情况下所有的socket都是阻塞的
2、非阻塞IO模型
当用户进程发出read操作时,如果kernel中的数据还没有准备好,那么它并不会block用户进程,而是立刻返回一个error。
用户进程判断结果是一个error时,它就知道数据还没有准备好,于是它可以再次发送read操作
一旦kernel中的数据准备好了,并且又再次收到了用户进程的system call,那么它马上就将数据拷贝到了用户内存,然后返回
非阻塞IO模式下用户进程需要不断地询问内核的数据准备好了没有
3、IO多路复用模型
通过一种机制,一个进程可以监视多个文件描述符(套接字描述符)一旦某个文件描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作(这样就不需要每个用户进程不断的询问内核数据准备好了没)
常用的IO多路复用方式有select、poll和epoll
首先要理解,Linux为啥会有这个模型,sellect/poll的价值是什么?
如果一个I/O流进来,我们就开启一个进程处理这个I/O流
那么假设现在有一百万个I/O流进来,那我们就需要开启一百万个进程一一对应处理这些I/O流
思考一下,一百万个进程,你的CPU占有率以及内存会多高(一个进程成本1MB),这个实现方式极其的不合理
所以人们提出了I/O多路复用这个模型,一个线程,通过记录I/O流的状态来同时管理多个I/O,可以提高服务器的吞吐能力
所以poll/sellect的存在价值就是解决了将fd请求从用户态拷贝到内核态,sellect存储请求的结构是fd_set,有最大文件描述符数量的限制(1024个)
poll存储请求的结构poll_fd结构,解决了最大文件描述符数量的限制,别的相似
为啥会优化成epoll?
上面select/poll 模型会带来一个就是 fd的相关数据会在用户态、内核态来回拷贝,这样也是一笔不小的开销
epoll只关心“活跃”链接,无需遍历全部描述符的集合,能够处理大量的链接请求(系统可打开的文件数目)
在用户态、内核态中引入了共享空间(mmap-->红黑树+链表)的概念
简单总结下IO多路复用模型下sellect、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内部定义的等待队列)。这也能节省不少的开销。
后面视情况再出一篇 epoll源码相关文章~
OVER~