出处:
https://my.oschina.net/hosee/blog/730598

首先介绍下epoll的基本原理,网上有很多版本,这里选择一个个人觉得相对清晰的讲解(详情见reference):

首先我们来定义流的概念,一个流可以是文件,socket,pipe等等可以进行I/O操作的内核对象。

不管是文件,还是套接字,还是管道,我们都可以把他们看作流。

之后我们来讨论I/O的操作,通过read,我们可以从流中读入数据;通过write,我们可以往流写入数据。现在假定一个情形,我们需要从流中读数据,但是流中还没有数据,(典型的例子为,客户端要从socket读如数据,但是服务器还没有把数据传回来),这时候该怎么办?

阻塞:阻塞是个什么概念呢?比如某个时候你在等快递,但是你不知道快递什么时候过来,而且你没有别的事可以干(或者说接下来的事要等快递来了才能做);那么你可以去睡觉了,因为你知道快递把货送来时一定会给你打个电话(假定一定能叫醒你)。

非阻塞忙轮询:接着上面等快递的例子,如果用忙轮询的方法,那么你需要知道快递员的手机号,然后每分钟给他挂个电话:“你到了没?”

很明显一般人不会用第二种做法,不仅显很无脑,浪费话费不说,还占用了快递员大量的时间。

大部分程序也不会用第二种做法,因为第一种方法经济而简单,经济是指消耗很少的CPU时间,如果线程睡眠了,就掉出了系统的调度队列,暂时不会去瓜分CPU宝贵的时间片了。

为了了解阻塞是如何进行的,我们来讨论缓冲区,以及内核缓冲区,最终把I/O事件解释清楚。缓冲区的引入是为了减少频繁I/O操作而引起频繁的系统调用(你知道它很慢的),当你操作一个流时,更多的是以缓冲区为单位进行操作,这是相对于用户空间而言。对于内核来说,也需要缓冲区。

假设有一个管道,进程A为管道的写入方,B为管道的读出方。

假设一开始内核缓冲区是空的,B作为读出方,被阻塞着。然后首先A往管道写入,这时候内核缓冲区由空的状态变到非空状态,内核就会产生一个事件告诉B该醒来了,这个事件姑且称之为“缓冲区非空”。

但是“缓冲区非空”事件通知B后,B却还没有读出数据;且内核许诺了不能把写入管道中的数据丢掉这个时候,A写入的数据会滞留在内核缓冲区中,如果内核也缓冲区满了,B仍未开始读数据,最终内核缓冲区会被填满,这个时候会产生一个I/O事件,告诉进程A,你该等等(阻塞)了,我们把这个事件定义为“缓冲区满”。

假设后来B终于开始读数据了,于是内核的缓冲区空了出来,这时候内核会告诉A,内核缓冲区有空位了,你可以从长眠中醒来了,继续写数据了,我们把这个事件叫做“缓冲区非满”

也许事件Y1已经通知了A,但是A也没有数据写入了,而B继续读出数据,知道内核缓冲区空了。这个时候内核就告诉B,你需要阻塞了!,我们把这个时间定为“缓冲区空”。

这四个情形涵盖了四个I/O事件,缓冲区满,缓冲区空,缓冲区非空,缓冲区非满(注都是说的内核缓冲区,且这四个术语都是我生造的,仅为解释其原理而造)。这四个I/O事件是进行阻塞同步的根本。(如果不能理解“同步”是什么概念,请学习操作系统的锁,信号量,条件变量等任务同步方面的相关知识)。

然后我们来说说阻塞I/O的缺点。但是阻塞I/O模式下,一个线程只能处理一个流的I/O事件。如果想要同时处理多个流,要么多进程(fork),要么多线程(pthread_create),很不幸这两种方法效率都不高。

于是再来考虑非阻塞忙轮询的I/O方式,我们发现我们可以同时处理多个流了(把一个流从阻塞模式切换到非阻塞模式再此不予讨论):

while true {
for i in stream[]; {
if i has data
read until unavailable
}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

我们只要不停的把所有流从头到尾问一遍,又从头开始。这样就可以处理多个流了,但这样的做法显然不好,因为如果所有的流都没有数据,那么只会白白浪费CPU。这里要补充一点,阻塞模式下,内核对于I/O事件的处理是阻塞或者唤醒,而非阻塞模式下则把I/O事件交给其他对象(后文介绍的select以及epoll)处理甚至直接忽略。

为了避免CPU空转,可以引进了一个代理(一开始有一位叫做select的代理,后来又有一位叫做poll的代理,不过两者的本质是一样的)。这个代理比较厉害,可以同时观察许多流的I/O事件,在空闲的时候,会把当前线程阻塞掉,当有一个或多个流有I/O事件时,就从阻塞态中醒来,于是我们的程序就会轮询一遍所有的流(于是我们可以把“忙”字去掉了)。代码长这样:

while true {
select(streams[])
for i in streams[] {
if i has data
read until unavailable
}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

于是,如果没有I/O事件产生,我们的程序就会阻塞在select处。但是依然有个问题,我们从select那里仅仅知道了,有I/O事件发生了,但却并不知道是那几个流(可能有一个,多个,甚至全部),我们只能无差别轮询所有流,找出能读出数据,或者写入数据的流,对他们进行操作。

但是使用select,我们有O(n)的无差别轮询复杂度,同时处理的流越多,每一次无差别轮询时间就越长。再次

说了这么多,终于能好好解释epoll了

epoll可以理解为event poll,不同于忙轮询和无差别轮询,epoll之会把哪个流发生了怎样的I/O事件通知我们。此时我们对这些流的操作都是有意义的。(复杂度降低到了O(1))

在讨论epoll的实现细节之前,先把epoll的相关操作列出:

epoll_create 创建一个epoll对象,一般epollfd = epoll_create()

epoll_ctl (epoll_add/epoll_del的合体),往epoll对象中增加/删除某一个流的某一个事件

比如

epoll_ctl(epollfd, EPOLL_CTL_ADD, socket, EPOLLIN);//注册缓冲区非空事件,即有数据流入

epoll_ctl(epollfd, EPOLL_CTL_DEL, socket, EPOLLOUT);//注册缓冲区非满事件,即流可以被写入

epoll_wait(epollfd,...)等待直到注册的事件发生
  • 1
  • 2
  • 3
  • 4
  • 5

(注:当对一个非阻塞流的读写发生缓冲区满或缓冲区空,write/read会返回-1,并设置errno=EAGAIN。而epoll只关心缓冲区非满和缓冲区非空事件)。

一个epoll模式的代码大概的样子是:

while true {
active_stream[] = epoll_wait(epollfd)
for i in active_stream[] {
read or write till
}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

限于篇幅,我只说这么多,以揭示原理性的东西,至于epoll的使用细节,请参考man和google,实现细节,请参阅linux kernel source。

上文提到了epoll可以将哪个流发生了什么事件主动通知,而不需要轮询。那么到底是如何实现通知的呢?

这就和操作系统的原理相关,在内核的最底层是中断,类似系统回调的机制。网卡设备对应一个中断号, 当网卡收到网络端的消息的时候会向CPU发起中断请求, 然后CPU处理该请求. 通过驱动程序 进而操作系统得到通知, 系统然后通知epoll, epoll通知用户代码。它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率,因为它会复用文件描述符集合来传递结果而不用迫使开发者每次等待事件之前都必须重新准备要被侦听的文件描述符集合,另一点原因就是获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就行了。

epoll在被内核初始化时(操作系统启动),同时会开辟出epoll自己的内核高速cache区,用于安置每一个我们想监控的socket,这些socket会以红黑树的形式保存在内核cache里,以支持快速的查找、插入、删除。这个内核高速cache区,就是建立连续的物理内存页,然后在之上建立slab层,简单的说,就是物理上分配好你想要的size的内存对象,每次使用时都是使用空闲的已分配好的对象。

所以简单总结下epoll和select的区别:

进程通过将一个或多个fd传递给select或poll系统调用,阻塞在select;这样select/poll可以帮我们侦测许多fd是否就绪;但是select/poll是顺序扫描fd是否就绪,而且支持的fd数量有限。linux还提供了一个epoll系统调用,epoll是基于事件驱动方式,而不是顺序扫描,当有fd就绪时,立即回调函数rollback

这种epoll的方式是不是能联想起Java nio,关于nio请查看[高并发Java 八] NIO和AIO

不过nio中的Selector的取名总让我以为是类似于select/poll的模型,但是你会发现,当有数据被准备好时,调用完select()后,会返回一个SelectionKey,SelectionKey表示在某个selector上的某个Channel的数据已经被准备好了。那到底底层实现是使用select/poll还是epoll呢?

public static SelectorProvider create() {
PrivilegedAction pa = new GetPropertyAction("os.name");
String osname = (String) AccessController.doPrivileged(pa);
if ("SunOS".equals(osname)) {
return new sun.nio.ch.DevPollSelectorProvider();
} // use EPollSelectorProvider for Linux kernels >= 2.6
if ("Linux".equals(osname)) {
pa = new GetPropertyAction("os.version");
String osversion = (String) AccessController.doPrivileged(pa);
String[] vers = osversion.split("\\.", 0);
if (vers.length >= 2) {
try {
int major = Integer.parseInt(vers[0]);
int minor = Integer.parseInt(vers[1]);
if (major > 2 || (major == 2 && minor >= 6)) {
return new sun.nio.ch.EPollSelectorProvider();
}
} catch (NumberFormatException x) {
// format not recognized
}
}
} return new sun.nio.ch.PollSelectorProvider();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

可以看到在Linux下,内核版本大于2.6时使用epoll,小于2.6时使用poll

Reference:

  1. https://www.zhihu.com/question/20122137/answer/14049112

  2. http://my.oschina.net/ielts0909/blog/89849?utm_source=tuicool&utm_medium=referral

  3. http://my.oschina.net/ielts0909/blog/89849?utm_source=tuicool&utm_medium=referral

epoll浅析以及nio中的Selector的更多相关文章

  1. epoll 浅析以及 nio 中的 Selector

    首先介绍下epoll的基本原理,网上有很多版本,这里选择一个个人觉得相对清晰的讲解(详情见reference): 首先我们来定义流的概念,一个流可以是文件,socket,pipe等等可以进行I/O操作 ...

  2. Java I/O(4):AIO和NIO中的Selector

    您好,我是湘王,这是我的博客园,欢迎您来,欢迎您再来- 在Java NIO的三大核心中,除了Channel和Buffer,剩下的就是Selector了.有的地方叫它选择器,也有叫多路复用器的(比如Ne ...

  3. 3、nio中的selector使用

    通过编写一个客户端和服务器端的例子来熟悉selector的使用 服务端逻辑: 1. 绑定一个端口号2. channel注册到selector中3. 用死循环来监听如果有时间发生,遍历selection ...

  4. DK NIO的BUG,例如臭名昭著的epoll bug,它会导致Selector空轮询,最终导致CPU 100%。

    NIO的epoll空轮询bug - Lost blog - 博客园 https://www.cnblogs.com/JAYIT/p/8241634.html NIO的epoll空轮询bug   JDK ...

  5. 两种 NIO 实现:Selector 与 Epoll

    [总结]两种 NIO 实现:Selector 与 Epoll 时间2012-11-17 08:38:42 开源中国新闻原文  http://my.oschina.net/ielts0909/blog/ ...

  6. NIO中Selector分析

        NIO中,使用Selector.select()方法来侦听是否有数据可以读/写,服务端开始执行时,如果没有客户端,这里的语句将进行阻塞,等待下面三个情况出现,才会进行后续的方法之行,这里是重点 ...

  7. 【Java】NIO中Selector的select方法源码分析

    该篇博客的有些内容和在之前介绍过了,在这里再次涉及到的就不详细说了,如果有不理解请看[Java]NIO中Channel的注册源码分析, [Java]NIO中Selector的创建源码分析 Select ...

  8. 浅析Java NIO

    浅析Java NIO 前言   在说NIO之前,先来说说IO的读写原理.我们都知道Java中的IO流可以分为网络IO流和文件IO流,前者在网络中使用,后者在操作文件时使用.但实际上两种流区别并不是太大 ...

  9. 如何解读 Java IO、NIO 中的同步阻塞与同步非阻塞?

    原文链接:如何解读 Java IO.NIO 中的同步阻塞与同步非阻塞? 一.前言 最近刚读完一本书:<Netty.Zookeeper.Redis 并发实战>,个人觉得 Netty 部分是写 ...

随机推荐

  1. 저장소system.runtime.remoting.messaging.callcontext

    https://msdn.microsoft.com/ko-kr/library/system.runtime.remoting.messaging.callcontext(v=vs.110).asp ...

  2. Atitit JAVA p2p设计与总结  JXTA 2

    Atitit JAVA p2p设计与总结  JXTA 2 JXTA 2 是开放源代码 P2P 网络的第二个主要版本,它利用流行的.基于 Java 的参考实现作为构建基础.在设计方面进行了重要的修改,以 ...

  3. iOS开发 -李洪强-清除缓存

    // //  SetViewController.m //  dfhx // //  Created by dfhx_iMac_001 on 16/4/5. //  Copyright © 2016年 ...

  4. html-文本处理集-持续学习更新

    文件处理2:分区分块.有序无序 <!-- 申明HTML5版本 --> <!DOCTYPE html> <html lang="en"> < ...

  5. jquery实现页面的搜索功能

    $(function(){ $("input[type=button]").click(function(){ var txt=$("input[type=text]&q ...

  6. Oracle密码过期,取消密码180天限制

    1.进入sqlplus模式 sqlplus / as sysdba; 2.帐户再改一次密码 alter user 用户名 identified by 原密码; 3.查看用户密码的有效期设置(一般默认的 ...

  7. JdbcTemplate中的exectue和queryForList方法的性能对比

    @Autowired JdbcTemplate jdbcParam; pstm =                 jdbcParam.getDataSource()                ...

  8. python学习笔记3----正则表达式

    正则表达式(RE)是通过re模块来实现的. 字符匹配: --普通字符: *大多数字母和字符一般都会和自身匹配. --元字符:. ^ $ * + ? {}  [] \ | () []: 通常用来指定一个 ...

  9. Unix系统编程(二)open的练习

    在上一篇中学习了一下open函数的基本用法,这里就要练习一下.计算机就是要实践嘛. 用open创建一个文件 #include <sys/types.h> #include <sys/ ...

  10. 【活动】上线了|带你直击react年度盛会

    明后两天,ReactEurope 2016大会在巴黎举行,本次大会演讲主题有: React Native(动画及运行性能优化) Flux-like 数据架构(GraphQL 最佳实践与展望.Redux ...