溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點(diǎn)擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

Java網(wǎng)絡(luò)編程和NIO詳解7:淺談 Linux 中NIO Selector 的實(shí)現(xiàn)原理

發(fā)布時(shí)間:2020-08-10 11:10:21 來源:ITPUB博客 閱讀:176 作者:Java技術(shù)江湖 欄目:編程語言

本文轉(zhuǎn)自互聯(lián)網(wǎng)

本系列文章將整理到我在GitHub上的《Java面試指南》倉庫,更多精彩內(nèi)容請到我的倉庫里查看

https://github.com/h3pl/Java-Tutorial

喜歡的話麻煩點(diǎn)下Star哈

文章將同步到我的個(gè)人博客:

www.how2playlife.com

本文是微信公眾號(hào)【Java技術(shù)江湖】的《不可輕視的Java網(wǎng)絡(luò)編程》其中一篇,本文部分內(nèi)容來源于網(wǎng)絡(luò),為了把本文主題講得清晰透徹,也整合了很多我認(rèn)為不錯(cuò)的技術(shù)博客內(nèi)容,引用其中了一些比較好的博客文章,如有侵權(quán),請聯(lián)系作者。

該系列博文會(huì)告訴你如何從計(jì)算機(jī)網(wǎng)絡(luò)的基礎(chǔ)知識(shí)入手,一步步地學(xué)習(xí)Java網(wǎng)絡(luò)基礎(chǔ),從socket到nio、bio、aio和netty等網(wǎng)絡(luò)編程知識(shí),并且進(jìn)行實(shí)戰(zhàn),網(wǎng)絡(luò)編程是每一個(gè)Java后端工程師必須要學(xué)習(xí)和理解的知識(shí)點(diǎn),進(jìn)一步來說,你還需要掌握Linux中的網(wǎng)絡(luò)編程原理,包括IO模型、網(wǎng)絡(luò)編程框架netty的進(jìn)階原理,才能更完整地了解整個(gè)Java網(wǎng)絡(luò)編程的知識(shí)體系,形成自己的知識(shí)框架。

為了更好地總結(jié)和檢驗(yàn)?zāi)愕膶W(xué)習(xí)成果,本系列文章也會(huì)提供部分知識(shí)點(diǎn)對(duì)應(yīng)的面試題以及參考答案。

如果對(duì)本系列文章有什么建議,或者是有什么疑問的話,也可以關(guān)注公眾號(hào)【Java技術(shù)江湖】聯(lián)系作者,歡迎你參與本系列博文的創(chuàng)作和修訂。

概述

Selector是NIO中實(shí)現(xiàn)I/O多路復(fù)用的關(guān)鍵類。Selector實(shí)現(xiàn)了通過一個(gè)線程管理多個(gè)Channel,從而管理多個(gè)網(wǎng)絡(luò)連接的目的。

Channel代表這一個(gè)網(wǎng)絡(luò)連接通道,我們可以將Channel注冊到Selector中以實(shí)現(xiàn)Selector對(duì)其的管理。一個(gè)Channel可以注冊到多個(gè)不同的Selector中。

當(dāng)Channel注冊到Selector后會(huì)返回一個(gè)SelectionKey對(duì)象,該SelectionKey對(duì)象則代表這這個(gè)Channel和它注冊的Selector間的關(guān)系。并且SelectionKey中維護(hù)著兩個(gè)很重要的屬性:interestOps、readyOps
interestOps是我們希望Selector監(jiān)聽Channel的哪些事件。

我們將我們感興趣的事件設(shè)置到該字段,這樣在selection操作時(shí),當(dāng)發(fā)現(xiàn)該Channel有我們所感興趣的事件發(fā)生時(shí),就會(huì)將我們感興趣的事件再設(shè)置到readyOps中,這樣我們就能得知是哪些事件發(fā)生了以做相應(yīng)處理。

Selector的中的重要屬性

Selector中維護(hù)3個(gè)特別重要的SelectionKey集合,分別是

  • keys:所有注冊到Selector的Channel所表示的SelectionKey都會(huì)存在于該集合中。keys元素的添加會(huì)在Channel注冊到Selector時(shí)發(fā)生。
  • selectedKeys:該集合中的每個(gè)SelectionKey都是其對(duì)應(yīng)的Channel在上一次操作selection期間被檢查到至少有一種SelectionKey中所感興趣的操作已經(jīng)準(zhǔn)備好被處理。該集合是keys的一個(gè)子集。
  • cancelledKeys:執(zhí)行了取消操作的SelectionKey會(huì)被放入到該集合中。該集合是keys的一個(gè)子集。

下面的源碼解析會(huì)說明上面3個(gè)集合的用處

Selector 源碼解析

下面我們通過一段對(duì)Selector的使用流程講解來進(jìn)一步深入其實(shí)現(xiàn)原理。
首先先來段Selector最簡單的使用片段

        ServerSocketChannel serverChannel = ServerSocketChannel.open();
        serverChannel.configureBlocking(false);
        int port = 5566;          
        serverChannel.socket().bind(new InetSocketAddress(port));
        Selector selector = Selector.open();
        serverChannel.register(selector, SelectionKey.OP_ACCEPT);
        while(true){
            int n = selector.select();
            if(n > 0) {
                Iterator<SelectionKey> iter = selector.selectedKeys().iterator();
                while (iter.hasNext()) {
                    SelectionKey selectionKey = iter.next();
                    ......
                    iter.remove();
                }
            }
        }

1、Selector的構(gòu)建

SocketChannel、ServerSocketChannel和Selector的實(shí)例初始化都通過SelectorProvider類實(shí)現(xiàn)。

ServerSocketChannel.open();

    public static ServerSocketChannel open() throws IOException {
        return SelectorProvider.provider().openServerSocketChannel();
    }

SocketChannel.open();

    public static SocketChannel open() throws IOException {
        return SelectorProvider.provider().openSocketChannel();
    }

Selector.open();

    public static Selector open() throws IOException {
        return SelectorProvider.provider().openSelector();
    }

我們來進(jìn)一步的了解下SelectorProvider.provider()

    public static SelectorProvider provider() {
        synchronized (lock) {
            if (provider != null)
                return provider;
            return AccessController.doPrivileged(
                new PrivilegedAction<>() {
                    public SelectorProvider run() {
                            if (loadProviderFromProperty())
                                return provider;
                            if (loadProviderAsService())
                                return provider;
                            provider = sun.nio.ch.DefaultSelectorProvider.create();
                            return provider;
                        }
                    });
        }
    }

① 如果配置了“java.nio.channels.spi.SelectorProvider”屬性,則通過該屬性值load對(duì)應(yīng)的SelectorProvider對(duì)象,如果構(gòu)建失敗則拋異常。
② 如果provider類已經(jīng)安裝在了對(duì)系統(tǒng)類加載程序可見的jar包中,并且該jar包的源碼目錄META-INF/services包含有一個(gè)java.nio.channels.spi.SelectorProvider提供類配置文件,則取文件中第一個(gè)類名進(jìn)行l(wèi)oad以構(gòu)建對(duì)應(yīng)的SelectorProvider對(duì)象,如果構(gòu)建失敗則拋異常。
③ 如果上面兩種情況都不存在,則返回系統(tǒng)默認(rèn)的SelectorProvider,即,sun.nio.ch.DefaultSelectorProvider.create();
④ 隨后在調(diào)用該方法,即SelectorProvider.provider()。則返回第一次調(diào)用的結(jié)果。

不同系統(tǒng)對(duì)應(yīng)著不同的sun.nio.ch.DefaultSelectorProvider

Java網(wǎng)絡(luò)編程和NIO詳解7:淺談 Linux 中NIO Selector 的實(shí)現(xiàn)原理

這里我們看linux下面的sun.nio.ch.DefaultSelectorProvider

public class DefaultSelectorProvider {
    /**
     * Prevent instantiation.
     */
    private DefaultSelectorProvider() { }
    /**
     * Returns the default SelectorProvider.
     */
    public static SelectorProvider create() {
        return new sun.nio.ch.EPollSelectorProvider();
    }
}

可以看見,linux系統(tǒng)下sun.nio.ch.DefaultSelectorProvider.create(); 會(huì)生成一個(gè)sun.nio.ch.EPollSelectorProvider類型的SelectorProvider,這里對(duì)應(yīng)于linux系統(tǒng)的epoll

接下來看下 selector.open():
    /**
     * Opens a selector.
     *
     * <p> The new selector is created by invoking the {@link
     * java.nio.channels.spi.SelectorProvider#openSelector openSelector} method
     * of the system-wide default {@link
     * java.nio.channels.spi.SelectorProvider} object.  </p>
     *
     * @return  A new selector
     *
     * @throws  IOException
     *          If an I/O error occurs
     */
    public static Selector open() throws IOException {
        return SelectorProvider.provider().openSelector();
    }

在得到sun.nio.ch.EPollSelectorProvider后調(diào)用openSelector()方法構(gòu)建Selector,這里會(huì)構(gòu)建一個(gè)EPollSelectorImpl對(duì)象。

EPollSelectorImpl

class EPollSelectorImpl
    extends SelectorImpl
{
    // File descriptors used for interrupt
    protected int fd0;
    protected int fd1;
    // The poll object
    EPollArrayWrapper pollWrapper;
    // Maps from file descriptors to keys
    private Map<Integer,SelectionKeyImpl> fdToKey;
EPollSelectorImpl(SelectorProvider sp) throws IOException {
        super(sp);
        long pipeFds = IOUtil.makePipe(false);
        fd0 = (int) (pipeFds >>> 32);
        fd1 = (int) pipeFds;
        try {
            pollWrapper = new EPollArrayWrapper();
            pollWrapper.initInterrupt(fd0, fd1);
            fdToKey = new HashMap<>();
        } catch (Throwable t) {
            try {
                FileDispatcherImpl.closeIntFD(fd0);
            } catch (IOException ioe0) {
                t.addSuppressed(ioe0);
            }
            try {
                FileDispatcherImpl.closeIntFD(fd1);
            } catch (IOException ioe1) {
                t.addSuppressed(ioe1);
            }
            throw t;
        }
    }

EPollSelectorImpl構(gòu)造函數(shù)完成:
① EPollArrayWrapper的構(gòu)建,EpollArrayWapper將Linux的epoll相關(guān)系統(tǒng)調(diào)用封裝成了native方法供EpollSelectorImpl使用。
② 通過EPollArrayWrapper向epoll注冊中斷事件

    void initInterrupt(int fd0, int fd1) {
        outgoingInterruptFD = fd1;
        incomingInterruptFD = fd0;
        epollCtl(epfd, EPOLL_CTL_ADD, fd0, EPOLLIN);
    }

③ fdToKey:構(gòu)建文件描述符-SelectionKeyImpl映射表,所有注冊到selector的channel對(duì)應(yīng)的SelectionKey和與之對(duì)應(yīng)的文件描述符都會(huì)放入到該映射表中。

EPollArrayWrapper

EPollArrayWrapper完成了對(duì)epoll文件描述符的構(gòu)建,以及對(duì)linux系統(tǒng)的epoll指令操縱的封裝。維護(hù)每次selection操作的結(jié)果,即epoll_wait結(jié)果的epoll_event數(shù)組。
EPollArrayWrapper操縱了一個(gè)linux系統(tǒng)下epoll_event結(jié)構(gòu)的本地?cái)?shù)組。

* typedef union epoll_data {
*     void *ptr;
*     int fd;
*     __uint32_t u32;
*     __uint64_t u64;
*  } epoll_data_t;
*
* struct epoll_event {
*     __uint32_t events;
*     epoll_data_t data;
* };

epoll_event的數(shù)據(jù)成員(epoll_data_t data)包含有與通過epoll_ctl將文件描述符注冊到epoll時(shí)設(shè)置的數(shù)據(jù)相同的數(shù)據(jù)。這里data.fd為我們注冊的文件描述符。這樣我們在處理事件的時(shí)候持有有效的文件描述符了。

EPollArrayWrapper將Linux的epoll相關(guān)系統(tǒng)調(diào)用封裝成了native方法供EpollSelectorImpl使用。

    private native int epollCreate();
    private native void epollCtl(int epfd, int opcode, int fd, int events);
    private native int epollWait(long pollAddress, int numfds, long timeout,
                                 int epfd) throws IOException;

上述三個(gè)native方法就對(duì)應(yīng)Linux下epoll相關(guān)的三個(gè)系統(tǒng)調(diào)用

    // The fd of the epoll driver
    private final int epfd;
     // The epoll_event array for results from epoll_wait
    private final AllocatedNativeObject pollArray;
    // Base address of the epoll_event array
    private final long pollArrayAddress;
    // 用于存儲(chǔ)已經(jīng)注冊的文件描述符和其注冊等待改變的事件的關(guān)聯(lián)關(guān)系。在epoll_wait操作就是要檢測這里文件描述法注冊的事件是否有發(fā)生。
    private final byte[] eventsLow = new byte[MAX_UPDATE_ARRAY_SIZE];
    private final Map<Integer,Byte> eventsHigh = new HashMap<>();
    EPollArrayWrapper() throws IOException {
        // creates the epoll file descriptor
        epfd = epollCreate();
        // the epoll_event array passed to epoll_wait
        int allocationSize = NUM_EPOLLEVENTS * SIZE_EPOLLEVENT;
        pollArray = new AllocatedNativeObject(allocationSize, true);
        pollArrayAddress = pollArray.address();
    }

EPoolArrayWrapper構(gòu)造函數(shù),創(chuàng)建了epoll文件描述符。構(gòu)建了一個(gè)用于存放epoll_wait返回結(jié)果的epoll_event數(shù)組。

ServerSocketChannel的構(gòu)建

ServerSocketChannel.open();

返回ServerSocketChannelImpl對(duì)象,構(gòu)建linux系統(tǒng)下ServerSocket的文件描述符。

    // Our file descriptor
    private final FileDescriptor fd;
    // fd value needed for dev/poll. This value will remain valid
    // even after the value in the file descriptor object has been set to -1
    private int fdVal;
    ServerSocketChannelImpl(SelectorProvider sp) throws IOException {
        super(sp);
        this.fd =  Net.serverSocket(true);
        this.fdVal = IOUtil.fdVal(fd);
        this.state = ST_INUSE;
    }

將ServerSocketChannel注冊到Selector

serverChannel.register(selector, SelectionKey.OP_ACCEPT);

    public final SelectionKey register(Selector sel, int ops,
                                       Object att)
        throws ClosedChannelException
    {
        synchronized (regLock) {
            if (!isOpen())
                throw new ClosedChannelException();
            if ((ops & ~validOps()) != 0)
                throw new IllegalArgumentException();
            if (blocking)
                throw new IllegalBlockingModeException();
            SelectionKey k = findKey(sel);
            if (k != null) {
                k.interestOps(ops);
                k.attach(att);
            }
            if (k == null) {
                // New registration
                synchronized (keyLock) {
                    if (!isOpen())
                        throw new ClosedChannelException();
                    k = ((AbstractSelector)sel).register(this, ops, att);
                    addKey(k);
                }
            }
            return k;
        }
    }
    protected final SelectionKey register(AbstractSelectableChannel ch,
                                          int ops,
                                          Object attachment)
    {
        if (!(ch instanceof SelChImpl))
            throw new IllegalSelectorException();
        SelectionKeyImpl k = new SelectionKeyImpl((SelChImpl)ch, this);
        k.attach(attachment);
        synchronized (publicKeys) {
            implRegister(k);
        }
        k.interestOps(ops);
        return k;
    }

① 構(gòu)建代表channel和selector間關(guān)系的SelectionKey對(duì)象
② implRegister(k)將channel注冊到epoll中
③ k.interestOps(int) 完成下面兩個(gè)操作:
a) 會(huì)將注冊的感興趣的事件和其對(duì)應(yīng)的文件描述存儲(chǔ)到EPollArrayWrapper對(duì)象的eventsLow或eventsHigh中,這是給底層實(shí)現(xiàn)epoll_wait時(shí)使用的。
b) 同時(shí)該操作還會(huì)將設(shè)置SelectionKey的interestOps字段,這是給我們程序員獲取使用的。

EPollSelectorImpl. implRegister

    protected void implRegister(SelectionKeyImpl ski) {
        if (closed)
            throw new ClosedSelectorException();
        SelChImpl ch = ski.channel;
        int fd = Integer.valueOf(ch.getFDVal());
        fdToKey.put(fd, ski);
        pollWrapper.add(fd);
        keys.add(ski);
    }

① 將channel對(duì)應(yīng)的fd(文件描述符)和對(duì)應(yīng)的SelectionKeyImpl放到fdToKey映射表中。
② 將channel對(duì)應(yīng)的fd(文件描述符)添加到EPollArrayWrapper中,并強(qiáng)制初始化fd的事件為0 ( 強(qiáng)制初始更新事件為0,因?yàn)樵撌录赡艽嬖谟谥氨蝗∠^的注冊中。)
③ 將selectionKey放入到keys集合中。

Selection操作

selection操作有3中類型:
① select():該方法會(huì)一直阻塞直到至少一個(gè)channel被選擇(即,該channel注冊的事件發(fā)生了)為止,除非當(dāng)前線程發(fā)生中斷或者selector的wakeup方法被調(diào)用。
② select(long time):該方法和select()類似,該方法也會(huì)導(dǎo)致阻塞直到至少一個(gè)channel被選擇(即,該channel注冊的事件發(fā)生了)為止,除非下面3種情況任意一種發(fā)生:a) 設(shè)置的超時(shí)時(shí)間到達(dá);b) 當(dāng)前線程發(fā)生中斷;c) selector的wakeup方法被調(diào)用
③ selectNow():該方法不會(huì)發(fā)生阻塞,如果沒有一個(gè)channel被選擇也會(huì)立即返回。

我們主要來看看select()的實(shí)現(xiàn) :int n = selector.select();

    public int select() throws IOException {
        return select(0);
    }

最終會(huì)調(diào)用到EPollSelectorImpl的doSelect

    protected int doSelect(long timeout) throws IOException {
        if (closed)
            throw new ClosedSelectorException();
        processDeregisterQueue();
        try {
            begin();
            pollWrapper.poll(timeout);
        } finally {
            end();
        }
        processDeregisterQueue();
        int numKeysUpdated = updateSelectedKeys();
        if (pollWrapper.interrupted()) {
            // Clear the wakeup pipe
            pollWrapper.putEventOps(pollWrapper.interruptedIndex(), 0);
            synchronized (interruptLock) {
                pollWrapper.clearInterrupted();
                IOUtil.drain(fd0);
                interruptTriggered = false;
            }
        }
        return numKeysUpdated;
    }

① 先處理注銷的selectionKey隊(duì)列
② 進(jìn)行底層的epoll_wait操作
③ 再次對(duì)注銷的selectionKey隊(duì)列進(jìn)行處理
④ 更新被選擇的selectionKey

先來看processDeregisterQueue():

    void processDeregisterQueue() throws IOException {
        Set var1 = this.cancelledKeys();
        synchronized(var1) {
            if (!var1.isEmpty()) {
                Iterator var3 = var1.iterator();
                while(var3.hasNext()) {
                    SelectionKeyImpl var4 = (SelectionKeyImpl)var3.next();
                    try {
                        this.implDereg(var4);
                    } catch (SocketException var12) {
                        IOException var6 = new IOException("Error deregistering key");
                        var6.initCause(var12);
                        throw var6;
                    } finally {
                        var3.remove();
                    }
                }
            }
        }
    }

從cancelledKeys集合中依次取出注銷的SelectionKey,執(zhí)行注銷操作,將處理后的SelectionKey從cancelledKeys集合中移除。執(zhí)行processDeregisterQueue()后cancelledKeys集合會(huì)為空。

    protected void implDereg(SelectionKeyImpl ski) throws IOException {
        assert (ski.getIndex() >= 0);
        SelChImpl ch = ski.channel;
        int fd = ch.getFDVal();
        fdToKey.remove(Integer.valueOf(fd));
        pollWrapper.remove(fd);
        ski.setIndex(-1);
        keys.remove(ski);
        selectedKeys.remove(ski);
        deregister((AbstractSelectionKey)ski);
        SelectableChannel selch = ski.channel();
        if (!selch.isOpen() && !selch.isRegistered())
            ((SelChImpl)selch).kill();
    }

注銷會(huì)完成下面的操作:
① 將已經(jīng)注銷的selectionKey從fdToKey( 文件描述與SelectionKeyImpl的映射表 )中移除
② 將selectionKey所代表的channel的文件描述符從EPollArrayWrapper中移除
③ 將selectionKey從keys集合中移除,這樣下次selector.select()就不會(huì)再將該selectionKey注冊到epoll中監(jiān)聽
④ 也會(huì)將selectionKey從對(duì)應(yīng)的channel中注銷
⑤ 最后如果對(duì)應(yīng)的channel已經(jīng)關(guān)閉并且沒有注冊其他的selector了,則將該channel關(guān)閉
完成????的操作后,注銷的SelectionKey就不會(huì)出現(xiàn)先在keys、selectedKeys以及cancelKeys這3個(gè)集合中的任何一個(gè)。

接著我們來看EPollArrayWrapper.poll(timeout):

    int poll(long timeout) throws IOException {
        updateRegistrations();
        updated = epollWait(pollArrayAddress, NUM_EPOLLEVENTS, timeout, epfd);
        for (int i=0; i<updated; i++) {
            if (getDescriptor(i) == incomingInterruptFD) {
                interruptedIndex = i;
                interrupted = true;
                break;
            }
        }
        return updated;
    }

updateRegistrations()方法會(huì)將已經(jīng)注冊到該selector的事件(eventsLow或eventsHigh)通過調(diào)用epollCtl(epfd, opcode, fd, events); 注冊到linux系統(tǒng)中。
這里epollWait就會(huì)調(diào)用linux底層的epoll_wait方法,并返回在epoll_wait期間有事件觸發(fā)的entry的個(gè)數(shù)

再看updateSelectedKeys():

    private int updateSelectedKeys() {
        int entries = pollWrapper.updated;
        int numKeysUpdated = 0;
        for (int i=0; i<entries; i++) {
            int nextFD = pollWrapper.getDescriptor(i);
            SelectionKeyImpl ski = fdToKey.get(Integer.valueOf(nextFD));
            // ski is null in the case of an interrupt
            if (ski != null) {
                int rOps = pollWrapper.getEventOps(i);
                if (selectedKeys.contains(ski)) {
                    if (ski.channel.translateAndSetReadyOps(rOps, ski)) {
                        numKeysUpdated++;
                    }
                } else {
                    ski.channel.translateAndSetReadyOps(rOps, ski);
                    if ((ski.nioReadyOps() & ski.nioInterestOps()) != 0) {
                        selectedKeys.add(ski);
                        numKeysUpdated++;
                    }
                }
            }
        }
        return numKeysUpdated;
    }

該方法會(huì)從通過EPollArrayWrapper pollWrapper 以及 fdToKey( 構(gòu)建文件描述符-SelectorKeyImpl映射表 )來獲取有事件觸發(fā)的SelectionKeyImpl對(duì)象,然后將SelectionKeyImpl放到selectedKey集合( 有事件觸發(fā)的selectionKey集合,可以通過selector.selectedKeys()方法獲得 )中,即selectedKeys。并重新設(shè)置SelectionKeyImpl中相關(guān)的readyOps值。

但是,這里要注意兩點(diǎn):

① 如果SelectionKeyImpl已經(jīng)存在于selectedKeys集合中,并且發(fā)現(xiàn)觸發(fā)的事件已經(jīng)存在于readyOps中了,則不會(huì)使numKeysUpdated++;這樣會(huì)使得我們無法得知該事件的變化。

????這點(diǎn)說明了為什么我們要在每次從selectedKey中獲取到Selectionkey后,將其從selectedKey集合移除,就是為了當(dāng)有事件觸發(fā)使selectionKey能正確到放入selectedKey集合中,并正確的通知給調(diào)用者。

再者,如果不將已經(jīng)處理的SelectionKey從selectedKey集合中移除,那么下次有新事件到來時(shí),在遍歷selectedKey集合時(shí)又會(huì)遍歷到這個(gè)SelectionKey,這個(gè)時(shí)候就很可能出錯(cuò)了。比如,如果沒有在處理完OP_ACCEPT事件后將對(duì)應(yīng)SelectionKey從selectedKey集合移除,那么下次遍歷selectedKey集合時(shí),處理到到該SelectionKey,相應(yīng)的ServerSocketChannel.accept()將返回一個(gè)空(null)的SocketChannel。

② 如果發(fā)現(xiàn)channel所發(fā)生I/O事件不是當(dāng)前SelectionKey所感興趣,則不會(huì)將SelectionKeyImpl放入selectedKeys集合中,也不會(huì)使numKeysUpdated++

epoll原理

select,poll,epoll都是IO多路復(fù)用的機(jī)制。I/O多路復(fù)用就是通過一種機(jī)制,一個(gè)進(jìn)程可以監(jiān)視多個(gè)描述符,一旦某個(gè)描述符就緒(一般是讀就緒或者寫就緒),能夠通知程序進(jìn)行相應(yīng)的讀寫操作。但select,poll,epoll本質(zhì)上都是同步I/O,因?yàn)樗麄兌夹枰谧x寫事件就緒后自己負(fù)責(zé)進(jìn)行讀寫,也就是說這個(gè)讀寫過程是阻塞的,而異步I/O則無需自己負(fù)責(zé)進(jìn)行讀寫,異步I/O的實(shí)現(xiàn)會(huì)負(fù)責(zé)把數(shù)據(jù)從內(nèi)核拷貝到用戶空間。

epoll是Linux下的一種IO多路復(fù)用技術(shù),可以非常高效的處理數(shù)以百萬計(jì)的socket句柄。

在 select/poll中,進(jìn)程只有在調(diào)用一定的方法后,內(nèi)核才對(duì)所有監(jiān)視的文件描述符進(jìn)行掃描,而epoll事先通過epoll_ctl()來注冊一 個(gè)文件描述符,一旦基于某個(gè)文件描述符就緒時(shí),內(nèi)核會(huì)采用類似callback的回調(diào)機(jī)制,迅速激活這個(gè)文件描述符,當(dāng)進(jìn)程調(diào)用epoll_wait() 時(shí)便得到通知。(此處去掉了遍歷文件描述符,而是通過監(jiān)聽回調(diào)的的機(jī)制。這正是epoll的魅力所在。)
如果沒有大量的idle -connection或者dead-connection,epoll的效率并不會(huì)比select/poll高很多,但是當(dāng)遇到大量的idle- connection,就會(huì)發(fā)現(xiàn)epoll的效率大大高于select/poll。

注意:linux下Selector底層是通過epoll來實(shí)現(xiàn)的,當(dāng)創(chuàng)建好epoll句柄后,它就會(huì)占用一個(gè)fd值,在linux下如果查看/proc/進(jìn)程id/fd/,是能夠看到這個(gè)fd的,所以在使用完epoll后,必須調(diào)用close()關(guān)閉,否則可能導(dǎo)致fd被耗盡。

先看看使用c封裝的3個(gè)epoll系統(tǒng)調(diào)用:

  • int epoll_create(int size)
    epoll_create建立一個(gè)epoll對(duì)象。參數(shù)size是內(nèi)核保證能夠正確處理的最大句柄數(shù),多于這個(gè)最大數(shù)時(shí)內(nèi)核可不保證效果。
  • int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
    epoll_ctl可以操作epoll_create創(chuàng)建的epoll,如將socket句柄加入到epoll中讓其監(jiān)控,或把epoll正在監(jiān)控的某個(gè)socket句柄移出epoll。
  • int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout)
    epoll_wait在調(diào)用時(shí),在給定的timeout時(shí)間內(nèi),所監(jiān)控的句柄中有事件發(fā)生時(shí),就返回用戶態(tài)的進(jìn)程。

    大概看看epoll內(nèi)部是怎么實(shí)現(xiàn)的:

  1. epoll初始化時(shí),會(huì)向內(nèi)核注冊一個(gè)文件系統(tǒng),用于存儲(chǔ)被監(jiān)控的句柄文件,調(diào)用epoll_create時(shí),會(huì)在這個(gè)文件系統(tǒng)中創(chuàng)建一個(gè)file節(jié)點(diǎn)。同時(shí)epoll會(huì)開辟自己的內(nèi)核高速緩存區(qū),以紅黑樹的結(jié)構(gòu)保存句柄,以支持快速的查找、插入、刪除。還會(huì)再建立一個(gè)list鏈表,用于存儲(chǔ)準(zhǔn)備就緒的事件。
  2. 當(dāng)執(zhí)行epoll_ctl時(shí),除了把socket句柄放到epoll文件系統(tǒng)里file對(duì)象對(duì)應(yīng)的紅黑樹上之外,還會(huì)給內(nèi)核中斷處理程序注冊一個(gè)回調(diào)函數(shù),告訴內(nèi)核,如果這個(gè)句柄的中斷到了,就把它放到準(zhǔn)備就緒list鏈表里。所以,當(dāng)一個(gè)socket上有數(shù)據(jù)到了,內(nèi)核在把網(wǎng)卡上的數(shù)據(jù)copy到內(nèi)核中后,就把socket插入到就緒鏈表里。
  3. 當(dāng)epoll_wait調(diào)用時(shí),僅僅觀察就緒鏈表里有沒有數(shù)據(jù),如果有數(shù)據(jù)就返回,否則就sleep,超時(shí)時(shí)立刻返回。

    epoll的兩種工作模式:

  • LT:level-trigger,水平觸發(fā)模式,只要某個(gè)socket處于readable/writable狀態(tài),無論什么時(shí)候進(jìn)行epoll_wait都會(huì)返回該socket。
  • ET:edge-trigger,邊緣觸發(fā)模式,只有某個(gè)socket從unreadable變?yōu)閞eadable或從unwritable變?yōu)閣ritable時(shí),epoll_wait才會(huì)返回該socket。

socket讀數(shù)據(jù)

Java網(wǎng)絡(luò)編程和NIO詳解7:淺談 Linux 中NIO Selector 的實(shí)現(xiàn)原理

socket寫數(shù)據(jù)

Java網(wǎng)絡(luò)編程和NIO詳解7:淺談 Linux 中NIO Selector 的實(shí)現(xiàn)原理

最后順便說下在Linux系統(tǒng)中JDK NIO使用的是 LT ,而Netty epoll使用的是 ET。

后記

因?yàn)楸救藢?duì)計(jì)算機(jī)系統(tǒng)組成以及C語言等知識(shí)比較欠缺,因?yàn)槲闹邢嚓P(guān)知識(shí)點(diǎn)的表示也相當(dāng)“膚淺”,如有不對(duì)不妥的地方望讀者指出。同時(shí)我也會(huì)繼續(xù)加強(qiáng)對(duì)該方面知識(shí)點(diǎn)的學(xué)習(xí)~

參考文章

http://www.jianshu.com/p/0d497fe5484a
http://remcarpediem.com/2017/04/02/Netty源碼-三-I-O模型和Java-NIO底層原理/
圣思園netty課程

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI