epoll底层原理总结

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了epoll底层原理总结相关的知识,希望对你有一定的参考价值。

参考技术A 1  epoll一种网络模式,采用的是 IO多路复用技术(就是可以监控多个文件描述符),相比较于select 和poll是非常快的;

首先看这三个函数:

1 int epoll_create(int size);

2 int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

3 int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);

函数用法不在这里讲解;

首先epoll_create创建一个epoll文件描述符,底层同时创建一个 红黑树 ,和一个 就绪链表 ;红黑树存储所监控的文件描述符的节点数据,就绪链表存储就绪的文件描述符的节点数据;epoll_ctl将会添加新的描述符,首先判断是红黑树上是否有此文件描述符节点,如果有,则立即返回。如果没有, 则在树干上插入新的节点,并且告知 内核注册回调函数 。当接收到某个文件描述符过来数据时,那么内核将该节点插入到就绪链表里面。epoll_wait将会接收到消息,并且将数据拷贝到用户空间,清空链表。对于LT模式epoll_wait清空就绪链表之后会检查该文件描述符是哪一种模式, 如果为LT模式,且必须该节点确实有事件未处理,那么就会把该节点重新放入到刚刚删除掉的且刚准备好的就绪链表,epoll_wait马上返回。 ET 模式不会检查,只会调用一次

每个epollfd在内核中有一个对应的eventpoll结构对象.其中关键的成员是一个readylist(eventpoll:rdllist)

和一棵红黑树(eventpoll:rbr).

一个fd被添加到epoll中之后(EPOLL_ADD),内核会为它生成一个对应的epitem结构对象.epitem被添加到

eventpoll的红黑树中.红黑树的作用是使用者调用EPOLL_MOD的时候可以快速找到fd对应的epitem。

调用epoll_wait的时候,将readylist中的epitem出列,将触发的事件拷贝到用户空间.之后判断epitem是否需

要重新添加回readylist.

epitem重新添加到readylist必须满足下列条件:

1) epitem上有用户关注的事件触发.

2) epitem被设置为水平触发模式(如果一个epitem被设置为边界触发则这个epitem不会被重新添加到readylist

中,在什么时候重新添加到readylist请继续往下看).

注意,如果epitem被设置为EPOLLONESHOT模式,则当这个epitem上的事件拷贝到用户空间之后,会将

这个epitem上的关注事件清空(只是关注事件被清空,并没有从epoll中删除,要删除必须对那个描述符调用

EPOLL_DEL),也就是说即使这个epitem上有触发事件,但是因为没有用户关注的事件所以不会被重新添加到

readylist中.

epitem被添加到readylist中的各种情况(当一个epitem被添加到readylist如果有线程阻塞在epoll_wait中,那

个线程会被唤醒):

1)对一个fd调用EPOLL_ADD,如果这个fd上有用户关注的激活事件,则这个fd会被添加到readylist.

2)对一个fd调用EPOLL_MOD改变关注的事件,如果新增加了一个关注事件且对应的fd上有相应的事件激活,

则这个fd会被添加到readylist.

3)当一个fd上有事件触发时(例如一个socket上有外来的数据)会调用ep_poll_callback(见eventpoll::ep_ptable_queue_proc),

如果触发的事件是用户关注的事件,则这个fd会被添加到readylist中.

了解了epoll的执行过程之后,可以回答一个在使用边界触发时常见的疑问.在一个fd被设置为边界触发的情况下,

调用read/write,如何正确的判断那个fd已经没有数据可读/不再可写.epoll文档中的建议是直到触发EAGAIN

错误.而实际上只要你请求字节数小于read/write的返回值就可以确定那个fd上已经没有数据可读/不再可写.

最后用一个epollfd监听另一个epollfd也是合法的,epoll通过调用eventpoll::ep_eventpoll_poll来判断一个

epollfd上是否有触发的事件(只能是读事件).

NIO多路复用底层原理(SelectPollEPoll)

一、NIO概述

1. BIO

        BIO,即Blockig IO,阻塞IO,一个线程对应一个连接,如果你的服务器有很多用户,每个用户都需要与你的服务器建立一个连接,那么你有多少用户,你的服务器就得创建多少个线程,显然是不显示的,而且每个线程是阻塞的,只要你连接上,我当前的线程就会等着客户端发送数据,然后处理,如果客户端没断开连接,也没发送数据,那服务端的线程就会一直等待,资源被浪费。其模型如下

2. NIO

        NIO一种说法叫 New IO,另一种说法叫Non Blockig IO即非阻塞IO,它是JDK1.4引入的,与BIO不同的是,它是一个线程可以顾及多个连接,在这里引入了IO多路复用模型,多个连接注册到同一个多路复用器,然后多路复用器轮询各个连接,有收发数据的就去处理,其模型如下

二、文件描述符

        本篇文章的NIO是基于Linux的,尽可能浅显易懂,但核心的文件描述符关键词不能忽略,首先给大家介绍一下什么是文件描述符:
        文件描述符是计算机科学中的一个术语,是一个指向文件的引用的抽象化概念。它往往只适用于Unix、Linux这种系统。直白地说,在Linux系统下,一切皆文件,我们不管有什么操作,都离不开对文件的读写,而文件描述符实际上就是一个索引值,它会指向一个文件,这个文件维护了进程对文件操作的记录,说白了,看着这个描述符指向的文件,我就知道我下一步要读写哪个文件了。

三、Select、Poll、EPoll

1. Select

        服务器端的一个线程处理客户端的多个连接请求,解决了BIO一个线程对应一个连接请求的问题,其底层依赖于操作系统的内核,这里的核心为多路复用器,这个多路复用器会管理所有客户端的连接,一旦有数据收发事件触发,便会去轮询所有连接,执行相应业务逻辑,那么,我们想一下,如果你客户端有100个连接,实际有收发数据的只有5个,那么轮询100个,是不是有大量无效循环呢?当然这是JDK1.4刚出BIO时的多路复用器的实现方式,采用的select 模型,而且,当时限制客户端只能有1024个连接,即多路复用器管理的集合最大为1024,那么我有超过1024个用户来连接你的服务端,却发现连不上?你认为合理吗?如果你的是游戏服务器,你认为还会有人玩吗?
        实际上,在操作系统底层,调用了int select(int maxfdp1,fd_set *readset,fd_set *writeset,fd_set *exceptset,const struct timeval *timeout)函数, 这是由操作系统提供的,其中的fd_set为文件描述符的集合,而与服务端建立了连接后都会有一个文件描述符存放在这个集合,这个函数会遍历这个集合,以处理有读写需求的连接

2. Poll模型

        后来又对多路复用器做了优化,底层采用poll 模型,去掉了1024的限制,理论上来说不限个数,但其余的实现方式一样,那么又有新的问题出现,如果我客户端有一百万连接,那么只有1000个连接有收发,那么我却要遍历100万个,而如果有收发数据的恰好在集合最末尾,也就是集合遍历到最后才会执行,那么对于客户端来讲意味着什么呢?
Poll模型与Select模型没什么大区别,本质问题没有解决,还是会有很多无效循环,在量大的情况下,非常影响性能

3. EPoll模型

        在JDK1.5,对NIO又进行了一次优化,底层采用了EPoll模型,上图即为采用EPoll 实现的NIO模型,当我们创建多路复用器时,会调用操作系统提供的函数epoll_create,并生成一个epfd,即epoll文件描述符,这个描述符指向的文件所存储的是注册到的多路复用器的事件的文件描述符
        其执行原理: EPoll是基于事件驱动模型,就跟Java的swt一样,只不过在这里的事件并不是人为触发的,而是由操作系统感知的,比如,当前你的服务器有一个新的连接请求发过来,先按照类型将其注册到多路复用器上,然后内核感知到这个请求,采用中断将其文件描述符就绪事件队列当中,后续epoll_wait会处理,比如连接好以后便通过epoll_ctl注册该连接的读事件,当该通道有读请求时,依旧由操作系统内核感知并中断,把它的文件描述符赋复制到epfd中,然后epoll_wait处理后续。
实际上,EPoll模型的底层核心函数有三个:

  1. int epoll_create(int size)

该函数生成一个epoll专用的文件描述符。它其实是在内核申请一空间,用来存放你想关注的socketChannel fd上是否发生以及发生了什么事件。size就是你在这个epoll fd上能关注的最大socketChannel fd数。随你定好了。只要你有空间。

  1. int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)

该函数用于控制某个epoll文件描述符上的事件,可以注册事件,修改事件,删除事件。
参数:
epfd: 由 epoll_create生成的epoll专用的文件描述符;
op: 要进行的操作例如注册事件,可能的取值EPOLL_CTL_ADD 注册、EPOLL_CTL_MOD
修改、EPOLL_CTL_DEL 删除
fd: 关联的文件描述符; event:指向epoll_event的指针;如果调用成功返回0,不成功返回-1

  1. int epoll_wait(int epfd,struct epoll_event * events,int maxevents,int timeout)

该函数用于轮询I/O事件的发生;
参数:
epfd: 由epoll_create 生成的epoll专用的文件描述符;
epoll_event: 就绪事件列表,此列表由操作系统提供 maxevents: 每次能处理的事件数;
timeout: 等待I/O事件发生的超时值;-1相当于阻塞,0相当于非阻塞。一般用-1即可 返回发生事件数。

        epoll_wait运行的原理是: 等待注册在epfd上的socketChannel fd的事件的发生,如果发生则将发生的socketChannel fd和事件类型放入到events(图中就绪事件列表)数组中。并且将注册在epfd上的socketChannel fd的事件类型给清空(这里是指多路复用器的另外一个集合),所以如果下一个循环你还要关注这个socketChannel fd的话,则需要用epoll_ctl(epfd,EPOLL_CTL_MOD,listenfd,&ev)来重新设置socketChannel fd的事件类型。这时不用EPOLL_CTL_ADD,因为socketChannel fd并未清空,只是事件类型清空。这一步非常重要。

四、总结

        在Linux下,NIO底层就是用的EPoll模型,而selector的底层,是由Linux用C语言在系统级别帮我们创建了一个数据结构体,这个结构体就是EPoll实例,我们可以把它理解为Java对象,可以存取数据。

1. 大致过程如下:

        1.进程先调用epoll的create,返回一个epoll的fd; epoll通过mmap开辟一块共享空间,增删改由内核完成,查询内核和用户进程都可以 这块共享空间中有一个红黑树和一个链表
        2.进程调用epoll的ctl add/delete sfd,把新来的链接放入红黑树中,
                2.1进程调用wait(),等待事件(事件驱动)
        3.当红黑树中的fd有数据到了,就把它放入一个链表中并维护该数据可写还是可读,wait返回;
        4.上层用户空间(通过epoll)从链表中取出fd,然后调用read/write读写数据. 所以epoll也是NIO,不是AIO

NIO编写服务端实现代码

public static void main(String[] args) throws IOException {
    // 1. 获取连接通道
    ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
    // 2. 切换成非阻塞模式
    serverSocketChannel.configureBlocking(false);
    // 3. 绑定链接, 是端口
    serverSocketChannel.bind(new InetSocketAddress(8888));
    // 4. 获取选择器
    Selector selector = Selector.open();
    // 5. 将连接通道注册到选择器, 并注册为接受连接事件
    serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
    while (true) {
        // 6. select()为获取选择器中的就绪事件, 例如连接事件, 如果有连接请求
        // 则该事件就是就绪的, 如果一个事件都没有, 那么将阻塞在该行代码
        selector.select();
        // 7.获取所有就绪事件, 然后轮询处理
        Set<SelectionKey> selectionKeys = selector.selectedKeys();
        Iterator<SelectionKey> iterator = selectionKeys.iterator();
        // 8.轮询处理就绪事件
        while (iterator.hasNext()) {
            SelectionKey next = iterator.next();
            // 按照不同的监听事件处理做相应的处理
            if (next.isAcceptable()) {
                // 8.1.1获取到当前事件的连接
                ServerSocketChannel server = (ServerSocketChannel) next.channel();
                // 8.1.2接受当前客户端的数据传输通道
                SocketChannel socketChannel = server.accept();
                // 8.1.3设置非阻塞
                if (socketChannel != null) {
                    socketChannel.configureBlocking(false);
                    // 8.1.4将当前数据传输通道注册到选择器, 并注册为读事件
                    socketChannel.register(selector, SelectionKey.OP_READ);
                    System.out.println("客户端连接成功了...");}
            } else if (next.isReadable()) {
                // 8.2.1获取到当前事件的连接
                SocketChannel socketChannel = (SocketChannel) next.channel();
                ByteBuffer buffer = ByteBuffer.allocate(128);
                int len = socketChannel.read(buffer);
                if (len > 0) {
                    System.out.println(new String(buffer.array()));
                } else if (len == -1) {
                    System.out.println("客户端断开连接...");
                    socketChannel.close();
                }}}}
}

以上是关于epoll底层原理总结的主要内容,如果未能解决你的问题,请参考以下文章

NIO多路复用底层原理(SelectPollEPoll)

NIO多路复用底层原理(SelectPollEPoll)

epoll深入网卡底层的讲解

ElasticSearch搜索底层基础原理总结

周瑜总结分享:分布式架构设计之Zookeeper底层原理详解

HashMap底层实现原理剖析