最近在學(xué)習(xí)Netty,看了好多資料,也看了一部分<<Netty in action>>這本書,發(fā)現(xiàn)完全不能理解它的設(shè)計,它的組件.
聯(lián)想到Netty主要是一個NIO框架,于是覺得是因為對NIO的了解不夠而導(dǎo)致的.然后就查閱NIO的相關(guān)資料,發(fā)現(xiàn)還是不能理解其原理.
不得不說,Google了很多資料,包括英文的和中文的,大多數(shù)都是NIO的具體用法,而對于其核心組件,比如selector,他們的作用,實現(xiàn)原理,卻并沒有說明.看完網(wǎng)上的介紹之后,讓我更加懵懵噠了.
既然查詢不到結(jié)果,就想自己查看源碼了解其原理.于是查看了Oracle JDK1.8.0_91中和NIO相關(guān)的部分的源碼,以及openjdk1.7的部分源碼.因為Oracle JDK1.8.0_91中,對于一些類的實現(xiàn),并沒有給出,只是給出的.class文件.即使我們可以通過反編譯來獲得,但是終究還是太麻煩.所以這部分源碼,就從openjdk1.7來獲得.
Java NIO SelectorProvider
查看Oracle JDK1.8.0_91源碼時,我們可以看到Selector這個組件,是由SelectorProvider創(chuàng)建的.
我們看一下SelectorProvider.provider()方法的具體實現(xiàn):
查看loadProviderFromProperty()方法和loadProviderAsService()方法的源碼:
我們可以看到,SelectorProvider.provider()方法會在System Property中不存在java.nio.channels.spi.SelectorProvider屬性和不能找到SelectorProvider的實現(xiàn)類時,創(chuàng)建一個默認的sun.nio.ch.DefaultSelectorProvider來作為SelectorProvider.
我們從open jdk7中查看sun.nio.ch.DefaultSelectorProvider的源碼:
open jdk7的源碼中,提供了三個版本的sun.nio.ch.DefaultSelectorProvider的實現(xiàn):
我們這里選擇的是solaris版本的.
從sun.nio.ch.DefaultSelectorProvider的源碼中,我們可以看到,如果是linux機器,并且其內(nèi)核版本大于2.6,創(chuàng)建的就是EPollSelectorProvider,否則的話,就創(chuàng)建PollSelectorProvider.
這就是我們今天要介紹的重點-IO多路復(fù)用.
IO多路復(fù)用
IO多路復(fù)用就是我們說的select,poll, epoll,接下來我們會逐個介紹.
Select
基本概念
IO多路復(fù)用是指內(nèi)核一旦發(fā)現(xiàn)進程指定的一個或者多個IO條件準(zhǔn)備讀取程剥,它就通知該進程。IO多路復(fù)用適用如下場合:
當(dāng)客戶處理多個描述字時(一般是交互式輸入和網(wǎng)絡(luò)套接口),必須使用I/O復(fù)用。
當(dāng)一個客戶同時處理多個套接口時,而這種情況是可能的,但很少出現(xiàn)殿遂。
如果一個TCP服務(wù)器既要處理監(jiān)聽套接口诈铛,又要處理已連接套接口,一般也要用到I/O復(fù)用墨礁。
如果一個服務(wù)器即要處理TCP幢竹,又要處理UDP,一般要使用I/O復(fù)用恩静。
如果一個服務(wù)器要處理多個服務(wù)或多個協(xié)議焕毫,一般要使用I/O復(fù)用。
與多進程和多線程技術(shù)相比驶乾,I/O多路復(fù)用技術(shù)的最大優(yōu)勢是系統(tǒng)開銷小邑飒,系統(tǒng)不必創(chuàng)建進程/線程,也不必維護這些進程/線程级乐,從而大大減小了系統(tǒng)的開銷疙咸。
select函數(shù)
該函數(shù)準(zhǔn)許進程指示內(nèi)核等待多個事件中的任何一個發(fā)送,并只在有一個或多個事件發(fā)生或經(jīng)歷一段指定的時間后才喚醒唇牧。函數(shù)原型如下:
**int select(int maxfdp1,fd_set *readset,fd_set *writeset,fd_set exceptset,const struct timeval timeout)
函數(shù)參數(shù)介紹如下:
(1)第一個參數(shù)maxfdp1指定待測試的描述字個數(shù)罕扎,它的值是待測試的最大描述字加1(因此把該參數(shù)命名為maxfdp1)聚唐,描述字0丐重、1、2...maxfdp1-1均將被測試扮惦。因為文件描述符是從0開始的崖蜜。
(2)中間的三個參數(shù)readset豫领、writeset和exceptset指定我們要讓內(nèi)核測試讀舔琅、寫和異常條件的描述字备蚓。如果對某一個的條件不感興趣,就可以把它設(shè)為空指針二跋。struct fd_set可以理解為一個集合扎即,這個集合中存放的是文件描述符,可通過以下四個宏進行設(shè)置:
void FD_ZERO(fd_set *fdset); //清空集合
void FD_SET(int fd, fd_set *fdset); //將一個給定的文件描述符加入集合之中
void FD_CLR(int fd, fd_set *fdset); //將一個給定的文件描述符從集合中刪除
int FD_ISSET(int fd, fd_set *fdset); // 檢查集合中指定的文件描述符是否可以讀寫
(3)timeout告知內(nèi)核等待所指定描述字中的任何一個就緒可花多少時間衫哥。其timeval結(jié)構(gòu)用于指定這段時間的秒數(shù)和微秒數(shù)撤逢。
struct timeval{
long tv_sec; //seconds
long tv_usec; //microseconds
};
這個參數(shù)有三種可能:
(1)永遠等待下去:僅在有一個描述字準(zhǔn)備好I/O時才返回蚊荣。為此互例,把該參數(shù)設(shè)置為空指針NULL媳叨。
(2)等待一段固定時間:在有一個描述字準(zhǔn)備好I/O時返回糊秆,但是不超過由該參數(shù)所指向的timeval結(jié)構(gòu)中指定的秒數(shù)和微秒數(shù)痘番。
(3)根本不等待:檢查描述字后立即返回平痰,這稱為輪詢宗雇。為此赔蒲,該參數(shù)必須指向一個timeval結(jié)構(gòu),而且其中的定時器值必須為0腻扇。
基本原理圖
poll
基本知識
poll的機制與select類似,與select在本質(zhì)上沒有多大差別舶沿,管理多個描述符也是進行輪詢,根據(jù)描述符的狀態(tài)進行處理括荡,但是poll沒有最大文件描述符數(shù)量的限制嫉髓。poll和select同樣存在一個缺點就是邑闲,包含大量文件描述符的數(shù)組被整體復(fù)制于用戶態(tài)和內(nèi)核的地址空間之間苫耸,而不論這些文件描述符是否就緒褪子,它的開銷隨著文件描述符數(shù)量的增加而線性增大。
poll函數(shù)
int poll ( struct pollfd * fds, unsigned int nfds, int timeout);
pollfd結(jié)構(gòu)體定義如下:
struct pollfd {
int fd; /* 文件描述符 */
short events; /* 等待的事件 */
short revents; /* 實際發(fā)生了的事件 */
} ;
每一個pollfd結(jié)構(gòu)體指定了一個被監(jiān)視的文件描述符呀枢,可以傳遞多個結(jié)構(gòu)體硫狞,指示poll()監(jiān)視多個文件描述符晃痴。每個結(jié)構(gòu)體的events域是監(jiān)視該文件描述符的事件掩碼,由用戶來設(shè)置這個域即彪。revents域是文件描述符的操作結(jié)果事件掩碼隶校,內(nèi)核在調(diào)用返回時設(shè)置這個域深胳。events域中請求的任何事件都可能在revents域中返回舞终。合法的事件如下:
POLLIN 有數(shù)據(jù)可讀。
POLLRDNORM 有普通數(shù)據(jù)可讀纷宇。
POLLRDBAND 有優(yōu)先數(shù)據(jù)可讀像捶。
POLLPRI 有緊迫數(shù)據(jù)可讀作岖。
POLLOUT 寫數(shù)據(jù)不會導(dǎo)致阻塞痘儡。
POLLWRNORM 寫普通數(shù)據(jù)不會導(dǎo)致阻塞沉删。
POLLWRBAND 寫優(yōu)先數(shù)據(jù)不會導(dǎo)致阻塞矾瑰。
POLLMSGSIGPOLL 消息可用殴穴。
此外采幌,revents域中還可能返回下列事件:
POLLER 指定的文件描述符發(fā)生錯誤休傍。
POLLHUP 指定的文件描述符掛起事件。
POLLNVAL 指定的文件描述符非法蹲姐。
這些事件在events域中無意義磨取,因為它們在合適的時候總是會從revents中返回。
使用poll()和select()不一樣柴墩,你不需要顯式地請求異常情況報告忙厌。
POLLIN | POLLPRI等價于select()的讀事件,POLLOUT |POLLWRBAND等價于select()的寫事件江咳。POLLIN等價于POLLRDNORM |POLLRDBAND逢净,而POLLOUT則等價于POLLWRNORM。例如,要同時監(jiān)視一個文件描述符是否可讀和可寫汹胃,我們可以設(shè)置 events為POLLIN |POLLOUT婶芭。在poll返回時,我們可以檢查revents中的標(biāo)志,對應(yīng)于文件描述符請求的events結(jié)構(gòu)體。如果POLLIN事件被設(shè)置,則文件描述符可以被讀取而不阻塞。如果POLLOUT被設(shè)置,則文件描述符可以寫入而不導(dǎo)致阻塞。這些標(biāo)志并不是互斥的:它們可能被同時設(shè)置,表示這個文件描述符的讀取和寫入操作都會正常返回而不阻塞。
timeout參數(shù)指定等待的毫秒數(shù)必逆,無論I/O是否準(zhǔn)備好损拢,poll都會返回或舞。timeout指定為負數(shù)值表示無限超時诈豌,使poll()一直掛起直到一個指定事件發(fā)生;timeout為0指示poll調(diào)用立即返回并列出準(zhǔn)備好I/O的文件描述符,但并不等待其它的事件。這種情況下,poll()就像它的名字那樣,一旦選舉出來,立即返回。
成功時,poll()返回結(jié)構(gòu)體中revents域不為0的文件描述符個數(shù);如果在超時前沒有任何事件發(fā)生阐污,poll()返回0隘膘;失敗時钦铁,poll()返回-1黎比,并設(shè)置errno為下列值之一:
EBADF 一個或多個結(jié)構(gòu)體中指定的文件描述符無效不跟。
EFAULTfds 指針指向的地址超出進程的地址空間颓帝。
EINTR 請求的事件之前產(chǎn)生一個信號,調(diào)用可以重新發(fā)起窝革。
EINVALnfds 參數(shù)超出PLIMIT_NOFILE值工猜。
ENOMEM 可用內(nèi)存不足箭昵,無法完成請求鼻忠。
epoll
基本知識
epoll是在2.6內(nèi)核中提出的棘催,是之前的select和poll的增強版本。相對于select和poll來說耳标,epoll更加靈活醇坝,沒有描述符限制。epoll使用一個文件描述符管理多個描述符次坡,將用戶關(guān)系的文件描述符的事件存放到內(nèi)核的一個事件表中呼猪,這樣在用戶空間和內(nèi)核空間的copy只需一次。
epoll接口
epoll操作過程需要三個接口砸琅,分別如下:
#include <sys/epoll.h>
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
(1) int epoll_create(int size);
創(chuàng)建一個epoll的句柄宋距,size用來告訴內(nèi)核這個監(jiān)聽的數(shù)目一共有多大。這個參數(shù)不同于select()中的第一個參數(shù)症脂,給出最大監(jiān)聽的fd+1的值谚赎。需要注意的是淫僻,當(dāng)創(chuàng)建好epoll句柄后,它就是會占用一個fd值壶唤,在linux下如果查看/proc/進程id/fd/雳灵,是能夠看到這個fd的,所以在使用完epoll后闸盔,必須調(diào)用close()關(guān)閉悯辙,否則可能導(dǎo)致fd被耗盡。
(2)int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epoll的事件注冊函數(shù)迎吵,它不同與select()是在監(jiān)聽事件時告訴內(nèi)核要監(jiān)聽什么類型的事件epoll的事件注冊函數(shù)躲撰,它不同與select()是在監(jiān)聽事件時告訴內(nèi)核要監(jiān)聽什么類型的事件,而是在這里先注冊要監(jiān)聽的事件類型击费。第一個參數(shù)是epoll_create()的返回值拢蛋,第二個參數(shù)表示動作,用三個宏來表示:
EPOLL_CTL_ADD:注冊新的fd到epfd中荡灾;
EPOLL_CTL_MOD:修改已經(jīng)注冊的fd的監(jiān)聽事件瓤狐;
EPOLL_CTL_DEL:從epfd中刪除一個fd;
第三個參數(shù)是需要監(jiān)聽的fd批幌,第四個參數(shù)是告訴內(nèi)核需要監(jiān)聽什么事础锐,struct epoll_event結(jié)構(gòu)如下:
struct epoll_event {
__uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
events可以是以下幾個宏的集合:
EPOLLIN :表示對應(yīng)的文件描述符可以讀(包括對端SOCKET正常關(guān)閉);
EPOLLOUT:表示對應(yīng)的文件描述符可以寫荧缘;
EPOLLPRI:表示對應(yīng)的文件描述符有緊急的數(shù)據(jù)可讀(這里應(yīng)該表示有帶外數(shù)據(jù)到來)皆警;
EPOLLERR:表示對應(yīng)的文件描述符發(fā)生錯誤;
EPOLLHUP:表示對應(yīng)的文件描述符被掛斷截粗;
EPOLLET: 將EPOLL設(shè)為邊緣觸發(fā)(Edge Triggered)模式信姓,這是相對于水平觸發(fā)(Level Triggered)來說的。
EPOLLONESHOT:只監(jiān)聽一次事件绸罗,當(dāng)監(jiān)聽完這次事件之后意推,如果還需要繼續(xù)監(jiān)聽這個socket的話,需要再次把這個socket加入到EPOLL隊列里
(3) int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
等待事件的產(chǎn)生珊蟀,類似于select()調(diào)用菊值。參數(shù)events用來從內(nèi)核得到事件的集合,maxevents告之內(nèi)核這個events有多大育灸,這個maxevents的值不能大于創(chuàng)建epoll_create()時的size腻窒,參數(shù)timeout是超時時間(毫秒,0會立即返回磅崭,-1將不確定儿子,也有說法說是永久阻塞)。該函數(shù)返回需要處理的事件數(shù)目砸喻,如返回0表示已超時柔逼。
工作模式
epoll對文件描述符的操作有兩種模式:LT(level trigger)和ET(edge trigger)蒋譬。LT模式是默認模式,LT模式與ET模式的區(qū)別如下:
LT模式:當(dāng)epoll_wait檢測到描述符事件發(fā)生并將此事件通知應(yīng)用程序卒落,應(yīng)用程序可以不立即處理該事件羡铲。下次調(diào)用epoll_wait時蜂桶,會再次響應(yīng)應(yīng)用程序并通知此事件儡毕。
ET模式:當(dāng)epoll_wait檢測到描述符事件發(fā)生并將此事件通知應(yīng)用程序,應(yīng)用程序必須立即處理該事件扑媚。如果不處理腰湾,下次調(diào)用epoll_wait時,不會再次響應(yīng)應(yīng)用程序并通知此事件疆股。
ET模式在很大程度上減少了epoll事件被重復(fù)觸發(fā)的次數(shù)费坊,因此效率要比LT模式高。epoll工作在ET模式的時候旬痹,必須使用非阻塞套接口附井,以避免由于一個文件句柄的阻塞讀/阻塞寫操作把處理多個文件描述符的任務(wù)餓死。
參考資料
Linux IO模式及 select永毅、poll沼死、epoll詳解
IO多路復(fù)用之select總結(jié)
IO多路復(fù)用之poll總結(jié)
IO多路復(fù)用之epoll總結(jié)