溫馨提示×

溫馨提示×

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

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

Java NIO多路復(fù)用的方法以及Linux epoll實現(xiàn)原理詳解

發(fā)布時間:2021-09-13 11:36:09 來源:億速云 閱讀:217 作者:chen 欄目:編程語言

這篇文章主要介紹“Java NIO多路復(fù)用的方法以及Linux epoll實現(xiàn)原理詳解”,在日常操作中,相信很多人在Java NIO多路復(fù)用的方法以及Linux epoll實現(xiàn)原理詳解問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對大家解答”Java NIO多路復(fù)用的方法以及Linux epoll實現(xiàn)原理詳解”的疑惑有所幫助!接下來,請跟著小編一起來學(xué)習(xí)吧!

為什么要 I/O 多路復(fù)用

當(dāng)需要從一個叫 r_fd 的描述符不停地讀取數(shù)據(jù),并把讀到的數(shù)據(jù)寫入一個叫 w_fd 的描述符時,我們可以用循環(huán)使用阻塞 I/O :

while((n = read(r_fd, buf, BUF_SIZE)) > 0)
    if(write(w_fd, buf, n) != n)
        err_sys("write error");

但是,如果要從兩個地方讀取數(shù)據(jù)呢?這時,不能再使用會把程序阻塞住的 read 函數(shù)。因為可能在阻塞地等待 r_fd1 的數(shù)據(jù)時,來不及處理 r_fd2,已經(jīng)到達(dá)的 r_fd2 的數(shù)據(jù)可能會丟失掉。

這個情況下需要使用非阻塞 I/O。

只要做個標(biāo)記,把文件描述符標(biāo)記為非阻塞的,以后再對它使用 read 函數(shù):如果它還沒有數(shù)據(jù)可讀,函數(shù)會立即返回并把 errorno 這個變量的值設(shè)置為 35,于是我們知道它沒有數(shù)據(jù)可讀,然后可以立馬去對其他描述符使用 read;如果它有數(shù)據(jù)可讀,我們就讀取它數(shù)據(jù)。對所有要讀的描述符都調(diào)用了一遍 read 之后,我們可以等一個較長的時間(比如幾秒),然后再從第一個文件描述符開始調(diào)用 read 。這種循環(huán)就叫做輪詢(polling)。

這樣,不會像使用阻塞 I/O 時那樣因為一個描述符 read 長時間處于等待數(shù)據(jù)而使程序阻塞。

輪詢的缺點是浪費太多 CPU 時間。大多數(shù)時候我們沒有數(shù)據(jù)可讀,但是還是用了 read 這個系統(tǒng)調(diào)用,使用系統(tǒng)調(diào)用時會從用戶態(tài)切換到內(nèi)核態(tài)。而大多數(shù)情況下我們調(diào)用 read,然后陷入內(nèi)核態(tài),內(nèi)核發(fā)現(xiàn)這個描述符沒有準(zhǔn)備好,然后切換回用戶態(tài)并且只得到 EAGAIN (errorno 被設(shè)置為 35),做的是無用功。描述符非常多的時候,每次的切換過程就是巨大的浪費。

所以,需要 I/O 多路復(fù)用。I/O 多路復(fù)用通過使用一個系統(tǒng)函數(shù),同時等待多個描述符的可讀、可寫狀態(tài)。

為了達(dá)到這個目的,我們需要做的是:建立一個描述符列表,以及我們分別關(guān)心它們的什么事件(可讀還是可寫還是發(fā)生例外情況);調(diào)用一個系統(tǒng)函數(shù),直到這個描述符列表里有至少一個描述符關(guān)聯(lián)的事件發(fā)生時,這個函數(shù)才會返回。

select, poll, epoll 就是這樣的系統(tǒng)函數(shù)。

select

我們可以在所有 POSIX 兼容的系統(tǒng)里使用 select 函數(shù)來進(jìn)行 I/O 多路復(fù)用。我們需要通過 select 函數(shù)的參數(shù)傳遞給內(nèi)核的信息有:

*   我們關(guān)心哪些描述符
*   我們關(guān)心它們的什么事件
*   我們希望等待多長時間

select 的返回時,內(nèi)核會告訴我們:

*   可讀的描述符的個數(shù)
*   哪些描述符發(fā)生了哪些事件
#include <sys/select.h>
int select(int maxfdp1, fd_set* readfds,
           fd_set* writefds, fd_set* exceptfds,
           struct timeval* timeout);
// 返回值: 已就緒的描述符的個數(shù)。超時時為 0 ,錯誤時為 -1

maxfdp1 意思是 “max file descriptor plus 1” ,就是把你要監(jiān)視的所有文件描述符里最大的那個加上 1 。(它實際上決定了內(nèi)核要遍歷文件描述符的次數(shù),比如你監(jiān)視了文件描述符 5 和 20 并把 maxfdp1 設(shè)置為 21 ,內(nèi)核每次都會從描述符 0 依次檢查到 20。)

中間的三個參數(shù)是你想監(jiān)視的文件描述符的集合。可以把 fd_set 類型視為 1024 位的二進(jìn)制數(shù),這意味著 select 只能監(jiān)視小于 1024 的文件描述符(1024 是由 Linux 的 sys/select.h 里 FD_SETSIZE 宏設(shè)置的值)。在 select 返回后我們通過 FD_ISSET 來判斷代表該位的描述符是否是已準(zhǔn)備好的狀態(tài)。

最后一個參數(shù)是等待超時的時長:到達(dá)這個時長但是沒有任一描述符可用時,函數(shù)會返回 0 。

用一個代碼片段來展示 select 的用法:

    // 這個例子要監(jiān)控文件描述符 3, 4 的可讀狀態(tài),以及 4, 5 的可寫狀態(tài)
    // 初始化兩個 fd_set 以及 timeval
    fd_set read_set, write_set;
    FD_ZERO(read_set);
    FD_ZERO(write_set);
    timeval t;
    t.tv_sec = 5;   // 超時為 5 秒
    t.tv_usec = 0;  // 加 0 微秒
    // 設(shè)置好兩個 fd_set
    int fd1 = 3;
    int fd2 = 4;
    int fd3 = 5;
    int maxfdp1 = 5 + 1;
    FD_SET(fd1, &read_set);
    FD_SET(fd2, &read_set);
    FD_SET(fd2, &write_set);
    FD_SET(fd3, &write_set);
    // 準(zhǔn)備備用的 fd_set
    fd_set r_temp = read_set;
    fd_set w_temp = write_set;
    while(true){
        // 每次都要重新設(shè)置放入 select 的 fd_set
        read_set = r_temp;
        write_set = w_temp;
        // 使用 select
        int n = select(maxfdp1, &read_set, &write_set, NULL, &t);
        // 上面的 select 函數(shù)會一直阻塞,直到
        // 3, 4 可讀以及 4, 5 可寫這四件事中至少一項發(fā)生
        // 或者等待時間到達(dá) 5 秒,返回 0
        for(int i=0; i<maxfdp1 && n>0; i++){
            if(FD_ISSET(i, &read_set)){
                n--;
                if(i==fd1)
                    prinf("描述符 3 可讀");
                if(i==fd2)
                    prinf("描述符 4 可讀");
            }
            if(FD_ISSET(i, &write_set)){
                n--;
                if(i==fd2)
                    prinf("描述符 3 可寫");
                if(i==fd3)
                    prinf("描述符 4 可寫");
            }
        }
        // 上面的 printf 語句換成對應(yīng)的 read 或者 write 函數(shù)就
        // 可以立即讀取或者寫入相應(yīng)的描述符而不用等待
    }

可以看到,select 的缺點有:

  • 默認(rèn)能監(jiān)視的文件描述符不能大于 1024,也代表監(jiān)視的總數(shù)不超過1024。即使你因為需要監(jiān)視的描述符大于 1024 而改動內(nèi)核的 FD_SETSIZE 值,但由于 select 是每次都會線性掃描整個fd_set,集合越大速度越慢,所以性能會比較差。

  • select 函數(shù)返回時只能看見已準(zhǔn)備好的描述符數(shù)量,至于是哪個描述符準(zhǔn)備好了需要循環(huán)用 FD_ISSET 來檢查,當(dāng)未準(zhǔn)備好的描述符很多而準(zhǔn)備好的很少時,效率比較低。

  • select 函數(shù)每次執(zhí)行的時候,都把參數(shù)里傳入的三個 fd_set 從用戶空間復(fù)制到內(nèi)核空間。而每次 fd_set 里要監(jiān)視的描述符變化不大時,全部重新復(fù)制一遍并不劃算。同樣在每次都是未準(zhǔn)備好的描述符很多而準(zhǔn)備好的很少時,調(diào)用 select 會很頻繁,用戶/內(nèi)核間的的數(shù)據(jù)復(fù)制就成了一個大的開銷。

還有一個問題是在代碼的寫法上給我一些困擾的,就是每次調(diào)用 select 前必須重新設(shè)置三個 fd_set。 fd_set 類型只是 1024 位的二進(jìn)制數(shù)(實際上結(jié)構(gòu)體里是幾個 long 變量的數(shù)組;比如 64 位機器上 long 是 64 bit,那么 fd_set 里就是 16 個 long 變量的數(shù)組),由一位的 1 和 0 代表一個文件描述符的狀態(tài),但是其實調(diào)用 select 前后位的 1/0 狀態(tài)意義是不一樣的。

先講一下幾個對 fd_set 操作的函數(shù)的作用:FD_ZERO 把 fd_set 所有位設(shè)置為 0 ;FD_SET 把一個位設(shè)置為 1 ;FD_ISSET 判斷一個位是否為 1 。

調(diào)用 select 前:我們用 FD_ZERO 把 fd_set 先全部初始化,然后用 FD_SET 把我們關(guān)心的代表描述符的位設(shè)置為 1 。我們這時可以用 FD_ISSET 判斷這個位是否被我們設(shè)置,這時的含義是我們想要監(jiān)視的描述符是否被設(shè)置為被監(jiān)視的狀態(tài)。

調(diào)用 select 時:內(nèi)核判斷 fd_set 里的位并把各個 fd_set 里所有值為 1 的位記錄下來,然后把 fd_set 全部設(shè)置成 0 ;一個描述符上有對應(yīng)的事件發(fā)生時,把對應(yīng) fd_set 里代表這個描述符的位設(shè)置為 1 。

在 select 返回之后:我們同樣用 FD_ISSET 判斷各個我們關(guān)心的位是 0 還是 1 ,這時的含義是,這個位是否是發(fā)生了我們關(guān)心的事件

所以,在下一次調(diào)用 select 前,我們不得不把已經(jīng)被內(nèi)核改掉的 fd_set 全部重新設(shè)置一下。

select 在監(jiān)視大量描述符尤其是更多的描述符未準(zhǔn)備好的情況時性能很差?!禪nix 高級編程》里寫,用 select 的程序通常只使用 3 到 10 個描述符。

poll

poll 和 select 是相似的,只是給的接口不同。

#include <poll.h>
int poll(struct pollfd fdarray[], nfds_t nfds, int timeout);
// 返回值: 已就緒的描述符的個數(shù)。超時時為 0 ,錯誤時為 -1

fdarraypollfd 的數(shù)組。pollfd 結(jié)構(gòu)體是這樣的:

struct pollfd {
    int fd;         // 文件描述符
    short events;   // 我期待的事件
    short revents;  // 實際發(fā)生的事件:我期待的事件中發(fā)生的;或者異常情況
};

nfdsfdarray 的長度,也就是 pollfd 的個數(shù)。

timeout 代表等待超時的毫秒數(shù)。

相比 select ,poll 有這些優(yōu)點:由于 poll 在 pollfd 里用 int fd 來表示文件描述符而不像 select 里用的 fd_set 來分別表示描述符,所以沒有必須小于 1024 的限制,也沒有數(shù)量限制;由于 poll 用 events 表示期待的事件,通過修改 revents 來表示發(fā)生的事件,所以不需要像 select 在每次調(diào)用前重新設(shè)置描述符和期待的事件。

除此之外,poll 和 select 幾乎相同。在 poll 返回后,需要遍歷 fdarray 來檢查各個 pollfd 里的 revents 是否發(fā)生了期待的事件;每次調(diào)用 poll 時,把 fdarray 復(fù)制到內(nèi)核空間。在描述符太多而每次準(zhǔn)備好的較少時,poll 有同樣的性能問題。

epoll

epoll 是在 Linux 2.5.44 中首度登場的。不像 select 和 poll ,它提供了三個系統(tǒng)函數(shù)而不是一個。

epoll_create 用來創(chuàng)建一個 epoll 描述符:
#include <sys/epoll.h>
int epoll_create(int size);
// 返回值:epoll 描述符

size 用來告訴內(nèi)核你想監(jiān)視的文件描述符的數(shù)目,但是它并不是限制了能監(jiān)視的描述符的最大個數(shù),而是給內(nèi)核最初分配的空間一個建議。然后系統(tǒng)會在內(nèi)核中分配一個空間來存放事件表,并返回一個 epoll 描述符,用來操作這個事件表。

epoll_ctl 用來增/刪/改內(nèi)核中的事件表:
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
// 返回值:成功時返回 0 ,失敗時返回 -1

epfd 是 epoll 描述符。

op 是操作類型(增加/刪除/修改)。

fd 是希望監(jiān)視的文件描述符。

event 是一個 epoll_event 結(jié)構(gòu)體的指針。epoll_event 的定義是這樣的:

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;        // 用戶數(shù)據(jù)變量
};

這個結(jié)構(gòu)體里,除了期待的事件外,還有一個 data ,是一個 union,它是用來讓我們在得到下面第三個函數(shù)的返回值以后方便的定位文件描述符的。

epoll_wait 用來等待事件
int epoll_wait(int epfd, struct epoll_event *result_events,
              int maxevents, int timeout);
// 返回值:已就緒的描述符個數(shù)。超時時為 0 ,錯誤時為 -1

epfd 是 epoll 描述符。

result_events 是 epoll_event 結(jié)構(gòu)體的指針,它將指向的是所有已經(jīng)準(zhǔn)備好的事件描述符相關(guān)聯(lián)的 epoll_event(在上個步驟里調(diào)用 epoll_ctl 時關(guān)聯(lián)起來的)。下面的例子可以讓你知道這個參數(shù)的意義。

maxevents 是返回的最大事件個數(shù),也就是你能通過 result_events 指針遍歷到的最大的次數(shù)。

timeout 是等待超時的毫秒數(shù)。

用一個代碼片段來展示 epoll 的用法:
   // 這個例子要監(jiān)控文件描述符 3, 4 的可讀狀態(tài),以及 4, 5 的可寫狀態(tài)

/* 通過 epoll_create 創(chuàng)建 epoll 描述符 */
int epfd = epoll_create(4);
int fd1 = 3;
int fd2 = 4;
int fd3 = 5;
/* 通過 epoll_ctl 注冊好四個事件 */
struct epoll_event ev1;
ev1.events = EPOLLIN;      // 期待它的可讀事件發(fā)生
ev1.data   = fd1;          // 我們通常就把 data 設(shè)置為 fd ,方便以后查看
epoll_ctl(epfd, EPOLL_CTL_ADD, fd1, &ev1);  // 添加到事件表
struct epoll_event ev2;
ev2.events = EPOLLIN;
ev2.data   = fd2;
epoll_ctl(epfd, EPOLL_CTL_ADD, fd2, &ev2);
struct epoll_event ev3;
ev3.events = EPOLLOUT;     // 期待它的可寫事件發(fā)生
ev3.data   = fd2;
epoll_ctl(epfd, EPOLL_CTL_ADD, fd2, &ev3);
struct epoll_event ev4;
ev4.events = EPOLLOUT;
ev4.data   = fd3;
epoll_ctl(epfd, EPOLL_CTL_ADD, fd3, &ev4);
/* 通過 epoll_wait 等待事件 */
# DEFINE MAXEVENTS 4
struct epoll_event result_events[MAXEVENTS];
while(true){
    int n = epoll_wait(epfd, &result_events, MAXEVENTS, 5000);
    for(int i=0; i<n; n--){
        // result_events[i] 一定是 ev1 到 ev4 中的一個
        if(result_events[i].events&EPOLLIN)
            printf("描述符 %d 可讀", result_events[i].fd);
        else if(result_events[i].events&EPOLLOUT)
            printf("描述符 %d 可寫", result_events[i].fd)
    }
}

所以 epoll 解決了 poll 和 select 的問題:

  • 只在 epoll_ctl 的時候把數(shù)據(jù)復(fù)制到內(nèi)核空間,這保證了每個描述符和事件一定只會被復(fù)制到內(nèi)核空間一次;每次調(diào)用 epoll_wait 都不會復(fù)制新數(shù)據(jù)到內(nèi)核空間。相比之下,select 每次調(diào)用都會把三個 fd_set 復(fù)制一遍;poll 每次調(diào)用都會把 fdarray 復(fù)制一遍。

  • epoll_wait 返回 n ,那么只需要做 n 次循環(huán),可以保證遍歷的每一次都是有意義的。相比之下,select 需要做至少 n 次至多 maxfdp1 次循環(huán);poll 需要遍歷完 fdarray 即做 nfds 次循環(huán)。

  • 在內(nèi)部實現(xiàn)上,epoll 使用了回調(diào)的方法。調(diào)用 epoll_ctl 時,就是注冊了一個事件:在集合中放入文件描述符以及事件數(shù)據(jù),并且加上一個回調(diào)函數(shù)。一旦文件描述符上的對應(yīng)事件發(fā)生,就會調(diào)用回調(diào)函數(shù),這個函數(shù)會把這個文件描述符加入到就緒隊列上。當(dāng)你調(diào)用 epoll_wait 時,它只是在查看就緒隊列上是否有內(nèi)容,有的話就返回給你的程序。select() poll() epoll_wait() 三個函數(shù)在操作系統(tǒng)看來,都是睡眠一會兒然后判斷一會兒的循環(huán),但是 select 和 poll 在醒著的時候要遍歷整個文件描述符集合,而 epoll_wait 只是看看就緒隊列是否為空而已。這是 epoll 高性能的理由,使得其 I/O 的效率不會像使用輪詢的 select/poll 隨著描述符增加而大大降低。

注 1 :select/poll/epoll_wait 三個函數(shù)的等待超時時間都有一樣的特性:等待時間設(shè)置為 0 時函數(shù)不阻塞而是立即返回,不論是否有文件描述符已準(zhǔn)備好;poll/epoll_wait 中的 timeout 為 -1,select 中的 timeout 為 NULL 時,則無限等待,直到有描述符已準(zhǔn)備好才會返回。

注 2 :有的新手會把文件描述符是否標(biāo)記為阻塞 I/O 等同于 I/O 多路復(fù)用函數(shù)是否阻塞。其實文件描述符是否標(biāo)記為阻塞,決定了你 readwrite 它時如果它未準(zhǔn)備好是阻塞等待,還是立即返回 EAGAIN ;而 I/O 多路復(fù)用函數(shù)除非你把 timeout 設(shè)置為 0 ,否則它總是會阻塞住你的程序。

注 3 :上面的例子只是入門,可能是不準(zhǔn)確或不全面的:一是數(shù)據(jù)要立即處理防止丟失;二是 EPOLLIN/EPOLLOUT 不完全等同于可讀可寫事件,具體要去搜索 poll/epoll 的事件具體有哪些;三是大多數(shù)實際例子里,比如一個 tcp server ,都會在運行中不斷增加/刪除的文件描述符而不是記住固定的 3 4 5 幾個描述符(用這種例子更能看出 epoll 的優(yōu)勢);四是 epoll 的優(yōu)勢更多的體現(xiàn)在處理大量閑連接的情況,如果場景是處理少量短連接,用 select 反而更好,而且用 select 的代碼能運行在所有平臺上。

到此,關(guān)于“Java NIO多路復(fù)用的方法以及Linux epoll實現(xiàn)原理詳解”的學(xué)習(xí)就結(jié)束了,希望能夠解決大家的疑惑。理論與實踐的搭配能更好的幫助大家學(xué)習(xí),快去試試吧!若想繼續(xù)學(xué)習(xí)更多相關(guān)知識,請繼續(xù)關(guān)注億速云網(wǎng)站,小編會繼續(xù)努力為大家?guī)砀鄬嵱玫奈恼拢?/p>

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

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

AI