对于epoll分析,我们借助下面的代码:
package com.haizhang.netty.nio;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Set;
/**
* 此类用于实现IO多路复用,使用单线程模式
* 引入Selector(多路复用选择器)
* 我们创建ServerSocketChannel时,需要往Selector里面注册我们感兴趣的事件。
* 在内核就是传递fd文件描述符,和感兴趣的监听事件给select调用函数,让内核帮我们监听这个文件描述符句柄有没有对应事件发生
*/
public class IOMultiSelectorSingleThread {
private ServerSocketChannel serverSocketChannel;
private Selector selector;
public void initServer(){
try {
//此步会帮我们生成一个文件描述符,比如编号为3, 那么serverSocketChannel这个变量就表示编号为3的fd。
serverSocketChannel = ServerSocketChannel.open();
//绑定server监听端口,为编号为3的文件描述符,bind端口9090
serverSocketChannel.bind(new InetSocketAddress(9090));
//非阻塞
serverSocketChannel.configureBlocking(false);
//打开多路复用选择器
selector = Selector.open();
//注册服务端感兴趣的事件accept,到我们的selector
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
} catch (IOException e) {
e.printStackTrace();
}
}
public void startServer() throws IOException {
//第一步init,打开服务器监听并往selector注册监听事件
initServer();
System.out.println("server start at 9090");
//第二步,循环检查
while(true){
//检查selector是否有事件,0表示阻塞知道有事件触发
while(selector.select(0)>0){
//有事件,拿出所有事件
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while(iterator.hasNext()){
SelectionKey key = iterator.next();
//分析当前事件是否时客户端连接accept事件?
if(key.isAcceptable()){
handleAccept(key);
}else if(key.isReadable()){
handleRead(key);
}
iterator.remove();
}
}
}
}
public void handleAccept(SelectionKey key) throws IOException {
//拿到这个事件实例,对应的channel(这个事件目标发往的channel)
ServerSocketChannel server = (ServerSocketChannel)key.channel();
SocketChannel client = server.accept();
System.out.println("server accept :" + client.getRemoteAddress());
client.configureBlocking(false);
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(1024);
//将这个客户端channel注册到selector。此时selector同时监听了服务器的accept事件,以及客户端的read事件
//注意,这里要将一个bytebuffer传递进去,未来数据存于此,它时和当前的client绑定的,相当于attachment
client.register(selector,SelectionKey.OP_READ,byteBuffer);
}
/**
* 处理客户端的读事件
*/
public void handleRead(SelectionKey key) throws IOException {
SocketChannel channel = (SocketChannel)key.channel();
//拿到前面注册client到selector时绑定的byteBuffer
ByteBuffer buffer = (ByteBuffer)key.attachment();
//让当前应用程序进程去内核中搬运这个客户端发过来的数据,存放到buffer
int read = channel.read(buffer);
//此时表示有数据
if(read > 0 ){
//将消息回写给客户端
buffer.flip();
byte[] msg = new byte[buffer.limit()];
buffer.get(msg);
System.out.println("accept msg : "+ new String(msg));
buffer.clear();
buffer.put("server accept".getBytes());
buffer.flip();
channel.write(buffer);
buffer.clear();
}else if (read == 0 ){
System.out.println("no msg");
}else {
//客户端关闭事件,必须处理,否则会陷入无线死循环,cpu到达100%!
System.out.println("socket close!!!!!!!!!");
// channel.close();
}
}
public static void main(String[] args) {
try {
new IOMultiSelectorSingleThread().startServer();
} catch (IOException e) {
e.printStackTrace();
}
}
}
上面的代码定义了多路复用器Selectors,而jdk1.8默认使用epoll作为selector在内核的实现.
我们可以在linux使用man epoll 查看epoll的系统调用详细讲解。
注意不是man 2 epoll喔!
DESCRIPTION
The epoll API performs a similar task to poll(2): monitoring mul‐
tiple file descriptors to see if I/O is possible on any of them.
The epoll API can be used either as an edge-triggered or a level-
triggered interface and scales well to large numbers of watched
file descriptors. The following system calls are provided to
create and manage an epoll instance:
* epoll_create(2) creates an epoll instance and returns a file
descriptor referring to that instance. (The more recent
epoll_create1(2) extends the functionality of epoll_cre‐
ate(2).)
* Interest in particular file descriptors is then registered via
epoll_ctl(2). The set of file descriptors currently regis‐
tered on an epoll instance is sometimes called an epoll set.
* epoll_wait(2) waits for I/O events, blocking the calling
thread if no events are currently available.
上面就是epoll的一个描述,而epoll里面包含三个具体的系统调用函数epoll_create, epoll_ctl, epoll_wait
要研究它们,我们需要使用man 2 后面介绍 epoll_create 或epoll_ctl,或epoll_wait即可.
- epoll_create
NAME
epoll_create, epoll_create1 - open an epoll file descriptor
SYNOPSIS
#include <sys/epoll.h>
int epoll_create(int size);
int epoll_create1(int flags);
DESCRIPTION
epoll_create() creates an epoll(7) instance. Since Linux 2.6.8, the size argument is ignored, but must be greater than zero; see NOTES
below.
epoll_create() returns a file descriptor referring to the new epoll instance. This file descriptor is used for all the subsequent calls to
the epoll interface. When no longer required, the file descriptor returned by epoll_create() should be closed by using close(2). When all
file descriptors referring to an epoll instance have been closed, the kernel destroys the instance and releases the associated resources for
reuse
从官方文档中可以看出,内核的epoll_create调用接收一个size,指定epoll对应的实例大小,次之需要大于0,而调用epoll_create相当于在内核中开辟了一块存储文件描述符的空间,未来当用户的channel注册到selector时,实际上会将channel对应的文件描述符和监听事件存放到epoll创建的这个空间里. 下图中 7fd 就是这个epoll空间的文件描述符
我们使用strace命令,对开头给出的代码追踪其对应的内核调用,作分析:
strace -ff -o nioout java IOMultiSelectorSingleThread
我们打开对应main线程的内核调用

下面给出代码和对应的内核系统调用函数:

epoll_ctl绑定文件描述符监听事件。
epoll_wait 用于监听等待用户的文件描述符中是否有存在事件的。
如果是select和poll,就不需要在内核中开启额外的空间,维护待监听的文件描述符和对应的监听事件。但是你每次调用select或poll的时候,都需要传递这些你要监听的文件描述符,交给select,内核帮你去挨个遍历这些描述符列表是否有数据准备好。
epoll_wait 会一直等待它监听的文件描述符有事件发生,那么内核是怎么知道哪个文件描述符中有数据的呢?这个过程时如何的?
用户在发消息到我们的服务器时,要将消息经过网络传递到我们的服务器网卡上,在网卡接收到消息后,可以在内存中开辟一块空间来存放数据包,我们称这块空间为DMA。当用户数据到网卡的时候,可能cpu还在忙别的事。此时网卡驱动就会触发一个中断,这个中断就会提示cpu去处理网卡的事情,cpu拿到中断号,去内核中寻找对应的中断事件回调函数,这个回调函数会得到网卡驱动传递过来的用户数据发往的目标socket(对应一个文件描述符),此时这个函数就会将有这个socket对应的文件描述符拷贝到epoll_wait监听的数据结构上(实际上是以红黑树作为这个数据结构)。当应用程序去调用多路复用器的select方法时,就会触发到epoll_wait内核调用,而epoll_wait立刻返回它监听的数据结构中的存在事件的文件描述符给应用程序,然后清空这个数据结构的内容,继续监听。而应用程序调用返回,随后应用程序再去处理对应的socket事件。
中断最重要的就会产生一个事件,这个事件对应的内核就有一个callback函数进行处理,中断会有一个中断号以及中断事件的处理回调函数,在内核中会维护一张中断向量表,包括了每个中断号和对应的中断事件。其中中断号的范围在0到255,而中断对应的事件是内核中先前注册上去的。
上述的过程可以表示如下:
epoll和poll和select区别,对应到java程序
如果你使用epoll这种多路复用器模式话,内核会帮你开辟一块空间,存放你的文件描述符列表,比如当你在java应用程序中,调用channel的register方法,将自己注册到Selector多路复用器时,实际上是直接被缓存到了内核开辟的这块空间里,事件复杂度是O(1),好处就是不需要你每次调Selector的select方法查看是否有channel有事件准备的时候,每次都要传递所有的文件描述符列表到内核这个步骤。
除此之外,在使用selector中的epoll进行select的时候,epoll在内核中还会会有一块内存空间,存放有事件的文件描述符,那么用户程序只要调用Selectors.select方法,对应就会在内核中调用epoll_wait函数,epoll_wait直接检查它监听的这块空间,是否有文件描述符(只要有,就是有事件的),如果有,直接返回给应用程序即可!如果你使用了select和poll这种模式话,多路复用器Selector会在你的程序里开辟一块空间去存储你regist到Selector上的channel对应的文件描述符,而不是存到内核里!所以select和poll每次都要传递应用程序中缓存的这些文件描述符列表,作为内核调用select和poll的参数!

Selectors既可以完成对poll、select的抽象,也可以完成对epoll的抽象,因为它们本质都是多路复用器而已。在java层面,都是把channel对应的文件描述符注册到了多路复用器Selectors去了而已,只不过poll、select将文件描述符维护在应用程序内存中,而epoll维护在内核开辟的内存中。
epoll优缺点总结
epoll在内核中开辟了一块内存空间,充当缓存,存放了应用程序传进来的文件描述符(对应到应用程序注册在Selector多路复用器上的socketChannel和ServerSocketChannel),以及对应的事件,相当于以空间换时间。这样免去了之前的select和poll的内核调用,每次都要求应用程序传递待监听检查的文件描述符的过程。
epoll还有个好处就是,他是基于事件的, epoll中的e 就是event的意思 这可以让cpu忙别的事情,而不用去参与内核的epoll遍历文件描述符检查状态这件事. 而当网卡真正有消息时,会产生中断事件,通知cpu回调对应的内核函数,将fd拷贝到epoll_wait监听的数据结构里. 然后当应用程序实际调用时,cpu只是帮助将这些fd传递给应用程序这件事上. 大大的节省了cpu的时间.
缺点可能就是,epoll只是针对于linux平台的实现,然后epoll牺牲了内核的一块内存空间,用于缓存fd。