您好,登錄后才能下訂單哦!
這篇文章主要介紹“Java NIO多路復用的方法以及Linux epoll實現原理詳解”,在日常操作中,相信很多人在Java NIO多路復用的方法以及Linux epoll實現原理詳解問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對大家解答”Java NIO多路復用的方法以及Linux epoll實現原理詳解”的疑惑有所幫助!接下來,請跟著小編一起來學習吧!
當需要從一個叫
r_fd
的描述符不停地讀取數據,并把讀到的數據寫入一個叫
w_fd
的描述符時,我們可以用循環使用阻塞 I/O :
while((n = read(r_fd, buf, BUF_SIZE)) > 0) if(write(w_fd, buf, n) != n) err_sys("write error");
但是,如果要從兩個地方讀取數據呢?這時,不能再使用會把程序阻塞住的
read
函數。因為可能在阻塞地等待
r_fd1
的數據時,來不及處理
r_fd2
,已經到達的
r_fd2
的數據可能會丟失掉。
這個情況下需要使用非阻塞 I/O。
只要做個標記,把文件描述符標記為非阻塞的,以后再對它使用
read
函數:如果它還沒有數據可讀,函數會立即返回并把 errorno 這個變量的值設置為 35,于是我們知道它沒有數據可讀,然后可以立馬去對其他描述符使用
read
;如果它有數據可讀,我們就讀取它數據。對所有要讀的描述符都調用了一遍
read
之后,我們可以等一個較長的時間(比如幾秒),然后再從第一個文件描述符開始調用
read
。這種循環就叫做輪詢(polling)。
這樣,不會像使用阻塞 I/O 時那樣因為一個描述符
read
長時間處于等待數據而使程序阻塞。
輪詢的缺點是浪費太多 CPU 時間。大多數時候我們沒有數據可讀,但是還是用了
read
這個系統調用,使用系統調用時會從用戶態切換到內核態。而大多數情況下我們調用
read
,然后陷入內核態,內核發現這個描述符沒有準備好,然后切換回用戶態并且只得到 EAGAIN (errorno 被設置為 35),做的是無用功。描述符非常多的時候,每次的切換過程就是巨大的浪費。
所以,需要 I/O 多路復用。I/O 多路復用通過使用一個系統函數,同時等待多個描述符的可讀、可寫狀態。
為了達到這個目的,我們需要做的是:建立一個描述符列表,以及我們分別關心它們的什么事件(可讀還是可寫還是發生例外情況);調用一個系統函數,直到這個描述符列表里有至少一個描述符關聯的事件發生時,這個函數才會返回。
select, poll, epoll 就是這樣的系統函數。
我們可以在所有 POSIX 兼容的系統里使用 select 函數來進行 I/O 多路復用。我們需要通過 select 函數的參數傳遞給內核的信息有:
* 我們關心哪些描述符 * 我們關心它們的什么事件 * 我們希望等待多長時間
select 的返回時,內核會告訴我們:
* 可讀的描述符的個數 * 哪些描述符發生了哪些事件 #include <sys/select.h> int select(int maxfdp1, fd_set* readfds, fd_set* writefds, fd_set* exceptfds, struct timeval* timeout); // 返回值: 已就緒的描述符的個數。超時時為 0 ,錯誤時為 -1
maxfdp1
意思是 “max file descriptor plus 1” ,就是把你要監視的所有文件描述符里最大的那個加上 1 。(它實際上決定了內核要遍歷文件描述符的次數,比如你監視了文件描述符 5 和 20 并把
maxfdp1
設置為 21 ,內核每次都會從描述符 0 依次檢查到 20。)
中間的三個參數是你想監視的文件描述符的集合。可以把 fd_set 類型視為 1024 位的二進制數,這意味著 select 只能監視小于 1024 的文件描述符(1024 是由 Linux 的 sys/select.h 里
FD_SETSIZE
宏設置的值)。在 select 返回后我們通過
FD_ISSET
來判斷代表該位的描述符是否是已準備好的狀態。
最后一個參數是等待超時的時長:到達這個時長但是沒有任一描述符可用時,函數會返回 0 。
用一個代碼片段來展示 select 的用法:
// 這個例子要監控文件描述符 3, 4 的可讀狀態,以及 4, 5 的可寫狀態 // 初始化兩個 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 微秒 // 設置好兩個 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); // 準備備用的 fd_set fd_set r_temp = read_set; fd_set w_temp = write_set; while(true){ // 每次都要重新設置放入 select 的 fd_set read_set = r_temp; write_set = w_temp; // 使用 select int n = select(maxfdp1, &read_set, &write_set, NULL, &t); // 上面的 select 函數會一直阻塞,直到 // 3, 4 可讀以及 4, 5 可寫這四件事中至少一項發生 // 或者等待時間到達 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 語句換成對應的 read 或者 write 函數就 // 可以立即讀取或者寫入相應的描述符而不用等待 }
可以看到,select 的缺點有:
默認能監視的文件描述符不能大于 1024,也代表監視的總數不超過1024。即使你因為需要監視的描述符大于 1024 而改動內核的
FD_SETSIZE
值,但由于 select 是每次都會線性掃描整個fd_set,集合越大速度越慢,所以性能會比較差。
select 函數返回時只能看見已準備好的描述符數量,至于是哪個描述符準備好了需要循環用
FD_ISSET
來檢查,當未準備好的描述符很多而準備好的很少時,效率比較低。
select 函數每次執行的時候,都把參數里傳入的三個 fd_set 從用戶空間復制到內核空間。而每次 fd_set 里要監視的描述符變化不大時,全部重新復制一遍并不劃算。同樣在每次都是未準備好的描述符很多而準備好的很少時,調用 select 會很頻繁,用戶/內核間的的數據復制就成了一個大的開銷。
還有一個問題是在代碼的寫法上給我一些困擾的,就是每次調用 select 前必須重新設置三個 fd_set。 fd_set 類型只是 1024 位的二進制數(實際上結構體里是幾個 long 變量的數組;比如 64 位機器上 long 是 64 bit,那么 fd_set 里就是 16 個 long 變量的數組),由一位的 1 和 0 代表一個文件描述符的狀態,但是其實調用 select 前后位的 1/0 狀態意義是不一樣的。
先講一下幾個對 fd_set 操作的函數的作用:FD_ZERO
把 fd_set 所有位設置為 0 ;FD_SET
把一個位設置為 1 ;FD_ISSET
判斷一個位是否為 1 。
調用 select 前:我們用
FD_ZERO
把 fd_set 先全部初始化,然后用
FD_SET
把我們關心的代表描述符的位設置為 1 。我們這時可以用
FD_ISSET
判斷這個位是否被我們設置,這時的含義是我們想要監視的描述符是否被設置為被監視的狀態。
調用 select 時:內核判斷 fd_set 里的位并把各個 fd_set 里所有值為 1 的位記錄下來,然后把 fd_set 全部設置成 0 ;一個描述符上有對應的事件發生時,把對應 fd_set 里代表這個描述符的位設置為 1 。
在 select 返回之后:我們同樣用
FD_ISSET
判斷各個我們關心的位是 0 還是 1 ,這時的含義是,這個位是否是發生了我們關心的事件。
所以,在下一次調用 select 前,我們不得不把已經被內核改掉的 fd_set 全部重新設置一下。
select 在監視大量描述符尤其是更多的描述符未準備好的情況時性能很差。《Unix 高級編程》里寫,用 select 的程序通常只使用 3 到 10 個描述符。
poll 和 select 是相似的,只是給的接口不同。
#include <poll.h> int poll(struct pollfd fdarray[], nfds_t nfds, int timeout); // 返回值: 已就緒的描述符的個數。超時時為 0 ,錯誤時為 -1
fdarray
是
pollfd
的數組。pollfd
結構體是這樣的:
struct pollfd { int fd; // 文件描述符 short events; // 我期待的事件 short revents; // 實際發生的事件:我期待的事件中發生的;或者異常情況 };
nfds
是
fdarray
的長度,也就是 pollfd 的個數。
timeout
代表等待超時的毫秒數。
相比 select ,poll 有這些優點:由于 poll 在 pollfd 里用
int fd
來表示文件描述符而不像 select 里用的 fd_set 來分別表示描述符,所以沒有必須小于 1024 的限制,也沒有數量限制;由于 poll 用
events
表示期待的事件,通過修改
revents
來表示發生的事件,所以不需要像 select 在每次調用前重新設置描述符和期待的事件。
除此之外,poll 和 select 幾乎相同。在 poll 返回后,需要遍歷
fdarray
來檢查各個
pollfd
里的
revents
是否發生了期待的事件;每次調用 poll 時,把
fdarray
復制到內核空間。在描述符太多而每次準備好的較少時,poll 有同樣的性能問題。
epoll 是在 Linux 2.5.44 中首度登場的。不像 select 和 poll ,它提供了三個系統函數而不是一個。
#include <sys/epoll.h> int epoll_create(int size); // 返回值:epoll 描述符
size
用來告訴內核你想監視的文件描述符的數目,但是它并不是限制了能監視的描述符的最大個數,而是給內核最初分配的空間一個建議。然后系統會在內核中分配一個空間來存放事件表,并返回一個
epoll 描述符,用來操作這個事件表。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); // 返回值:成功時返回 0 ,失敗時返回 -1
epfd
是 epoll 描述符。
op
是操作類型(增加/刪除/修改)。
fd
是希望監視的文件描述符。
event
是一個 epoll_event 結構體的指針。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; // 用戶數據變量 };
這個結構體里,除了期待的事件外,還有一個
data
,是一個 union,它是用來讓我們在得到下面第三個函數的返回值以后方便的定位文件描述符的。
int epoll_wait(int epfd, struct epoll_event *result_events, int maxevents, int timeout); // 返回值:已就緒的描述符個數。超時時為 0 ,錯誤時為 -1
epfd
是 epoll 描述符。
result_events
是 epoll_event 結構體的指針,它將指向的是所有已經準備好的事件描述符相關聯的 epoll_event(在上個步驟里調用 epoll_ctl 時關聯起來的)。下面的例子可以讓你知道這個參數的意義。
maxevents
是返回的最大事件個數,也就是你能通過 result_events 指針遍歷到的最大的次數。
timeout
是等待超時的毫秒數。
用一個代碼片段來展示 epoll 的用法:
// 這個例子要監控文件描述符 3, 4 的可讀狀態,以及 4, 5 的可寫狀態
/* 通過 epoll_create 創建 epoll 描述符 */ int epfd = epoll_create(4); int fd1 = 3; int fd2 = 4; int fd3 = 5; /* 通過 epoll_ctl 注冊好四個事件 */ struct epoll_event ev1; ev1.events = EPOLLIN; // 期待它的可讀事件發生 ev1.data = fd1; // 我們通常就把 data 設置為 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; // 期待它的可寫事件發生 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 的時候把數據復制到內核空間,這保證了每個描述符和事件一定只會被復制到內核空間一次;每次調用 epoll_wait 都不會復制新數據到內核空間。相比之下,select 每次調用都會把三個 fd_set 復制一遍;poll 每次調用都會把
fdarray
復制一遍。
epoll_wait 返回 n ,那么只需要做 n 次循環,可以保證遍歷的每一次都是有意義的。相比之下,select 需要做至少 n 次至多
maxfdp1
次循環;poll 需要遍歷完 fdarray 即做
nfds
次循環。
在內部實現上,epoll 使用了回調的方法。調用 epoll_ctl 時,就是注冊了一個事件:在集合中放入文件描述符以及事件數據,并且加上一個回調函數。一旦文件描述符上的對應事件發生,就會調用回調函數,這個函數會把這個文件描述符加入到就緒隊列上。當你調用 epoll_wait 時,它只是在查看就緒隊列上是否有內容,有的話就返回給你的程序。select()
poll()
epoll_wait()
三個函數在操作系統看來,都是睡眠一會兒然后判斷一會兒的循環,但是 select 和 poll 在醒著的時候要遍歷整個文件描述符集合,而 epoll_wait 只是看看就緒隊列是否為空而已。這是 epoll 高性能的理由,使得其 I/O 的效率不會像使用輪詢的 select/poll 隨著描述符增加而大大降低。
注 1 :select/poll/epoll_wait 三個函數的等待超時時間都有一樣的特性:等待時間設置為 0 時函數不阻塞而是立即返回,不論是否有文件描述符已準備好;poll/epoll_wait 中的 timeout 為 -1,select 中的 timeout 為 NULL 時,則無限等待,直到有描述符已準備好才會返回。
注 2 :有的新手會把文件描述符是否標記為阻塞 I/O 等同于 I/O 多路復用函數是否阻塞。其實文件描述符是否標記為阻塞,決定了你
read
或write
它時如果它未準備好是阻塞等待,還是立即返回 EAGAIN ;而 I/O 多路復用函數除非你把 timeout 設置為 0 ,否則它總是會阻塞住你的程序。注 3 :上面的例子只是入門,可能是不準確或不全面的:一是數據要立即處理防止丟失;二是 EPOLLIN/EPOLLOUT 不完全等同于可讀可寫事件,具體要去搜索 poll/epoll 的事件具體有哪些;三是大多數實際例子里,比如一個 tcp server ,都會在運行中不斷增加/刪除的文件描述符而不是記住固定的 3 4 5 幾個描述符(用這種例子更能看出 epoll 的優勢);四是 epoll 的優勢更多的體現在處理大量閑連接的情況,如果場景是處理少量短連接,用 select 反而更好,而且用 select 的代碼能運行在所有平臺上。
到此,關于“Java NIO多路復用的方法以及Linux epoll實現原理詳解”的學習就結束了,希望能夠解決大家的疑惑。理論與實踐的搭配能更好的幫助大家學習,快去試試吧!若想繼續學習更多相關知識,請繼續關注億速云網站,小編會繼續努力為大家帶來更多實用的文章!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。