linuxselect阻塞
⑴ 關於linux下的select/epoll
select這個系統調用的原型如下
第一個參數nfds用來告訴內核 要掃描的socket fd的數量+1 ,select系統調用最大接收的數量是1024,但是如果每次都去掃描1024,實際上的數量並不多,則效率太低,這里可以指定需要掃描的數量。 最大數量為1024,如果需要修改這個數量,則需要重新編譯Linux內核源碼。
第2、3、4個參數分別是readfds、writefds、exceptfds,傳遞的參數應該是fd_set 類型的引用,內核會檢測每個socket的fd, 如果沒有讀事件,就將對應的fd從第二個參數傳入的fd_set中移除,如果沒有寫事件,就將對應的fd從第二個參數的fd_set中移除,如果沒有異常事件,就將對應的fd從第三個參數的fd_set中移除 。這里我們應該 要將實際的readfds、writefds、exceptfds拷貝一份副本傳進去,而不是傳入原引用,因為如果傳遞的是原引用,某些socket可能就已經丟失 。
最後一個參數是等待時間, 傳入0表示非阻塞,傳入>0表示等待一定時間,傳入NULL表示阻塞,直到等到某個socket就緒 。
FD_ZERO()這個函數將fd_set中的所有bit清0,一般用來進行初始化等。
FD_CLR()這個函數用來將bitmap(fd_set )中的某個bit清0,在客戶端異常退出時就會用到這個函數,將fd從fd_set中刪除。
FD_ISSET()用來判斷某個bit是否被置1了,也就是判斷某個fd是否在fd_set中。
FD_SET()這個函數用來將某個fd加入fd_set中,當客戶端新加入連接時就會使用到這個函數。
epoll_create系統調用用來創建epfd,會在開辟一塊內存空間(epoll的結構空間)。size為epoll上能關注的最大描述符數,不夠會進行擴展,size只要>0就行,早期的設計size是固定大小,但是現在size參數沒什麼用,會自動擴展。
返回值是epfd,如果為-1則說明創建epoll對象失敗 。
第一個參數epfd傳入的就是epoll_create返回的epfd。
第二個參數傳入對應操作的宏,包括 增刪改(EPOLL_CTL_ADD、EPOLL_CTL_DEL、EPOLL_CTL_MOD) 。
第三個參數傳入的是 需要增刪改的socket的fd 。
第四個參數傳入的是 需要操作的fd的哪些事件 ,具體的事件可以看後續。
返回值是一個int類型,如果為-1則說明操作失敗 。
第一個參數是epfd,也就是epoll_create的返回值。
第二個參數是一個epoll_event類型的指針,也就是傳入的是一個數組指針。 內核會將就緒的socket的事件拷貝到這個數組中,用戶可以根據這個數組拿到事件和消息等 。
第三個參數是maxevents,傳入的是 第二個參數的數組的容量 。
第四個參數是timeout, 如果設為-1一直阻塞直到有就緒數據為止,如果設為0立即返回,如果>0那麼阻塞一段時間 。
返回值是一個int類型,也就是就緒的socket的事件的數量(內核拷貝給用戶的events的元素的數量),通過這個數量可以進行遍歷處理每個事件 。
一般需要傳入 ev.data.fd 和 ev.events ,也就是fd和需要監控的fd的事件。事件如果需要傳入多個,可以通過按位與來連接,比如需要監控讀寫事件,只需要像如下這樣操作即可: ev.events=EPOLLIN | EPOLLOUT 。
LT(水平觸發), 默認 的工作模式, 事件就緒後用戶可以選擇處理和不處理,如果用戶不處理,內核會對這部分數據進行維護,那麼下次調用epoll_wait()時仍舊會打包出來 。
ET(邊緣觸發),事件就緒之後, 用戶必須進行處理 ,因為內核把事件打包出來之後就把對應的就緒事件給清掉了, 如果不處理那麼就緒事件就沒了 。ET可以減少epoll事件被重復觸發的次數,效率比LT高。
如果需要設置為邊緣觸發只需要設置事件為類似 ev.events=EPOLLIN | EPOLLET 即可 。
select/poll/epoll是nio多路復用技術, 傳統的bio無法實現C10K/C100K ,也就是無法滿足1w/10w的並發量,在這么高的並發量下,在進行上下文切換就很容易將伺服器的負載拉飛。
1.將fd_set從用戶態拷貝到內核態
2.根據fd_set掃描內存中的socket的fd的狀態,時間復雜度為O(n)
3.檢查fd_set,如果有已經就緒的socket,就給對應的socket的fd打標記,那麼就return 就緒socket的數量並喚醒當前線程,如果沒有就緒的socket就繼續阻塞當前線程直到有socket就緒才將當前線程喚醒。
4.如果想要獲取當前已經就緒的socket列表,則還需要進行一次系統調用,使用O(n)的時間去掃描socket的fd列表,將已經打上標記的socket的fd返回。
CPU在同一個時刻只能執行一個程序,通過RR時間片輪轉去切換執行各個程序。沒有被掛起的進程(線程)則在工作隊列中排隊等待CPU的執行,將進程(線程)從工作隊列中移除就是掛起,反映到Java層面的就是線程的阻塞。
什麼是中斷?當我們使用鍵盤、滑鼠等IO設備的時候,會給主板一個電流信號,這個電流信號就給CPU一個中斷信號,CPU執行完當前的指令便會保存現場,然後執行鍵盤/滑鼠等設備的中斷程序,讓中斷程序獲取CPU的使用權,在中斷程序後又將現場恢復,繼續執行之前的進程。
如果第一次沒檢測到就緒的socket,就要將其進程(線程)從工作隊列中移除,並加入到socket的等待隊列中。
socket包含讀緩沖區+寫緩沖區+等待隊列(放線程或eventpoll對象)
當從客戶端往伺服器端發送數據時,使用TCP/IP協議將通過物理鏈路、網線發給伺服器的網卡設備,網卡的DMA設備將接收到的的數據寫入到內存中的一塊區域(網卡緩沖區),然後會給CPU發出一個中斷信號,CPU執行完當前指令則會保存現場,然後網卡的中斷程序就獲得了CPU的使用權,然後CPU便開始執行網卡的中斷程序,將內存中的緩存區中的數據包拿出,判斷埠號便可以判斷它是哪個socket的數據,將數據包寫入對應的socket的讀(輸入)緩沖區,去檢查對應的socket的等待隊列有沒有等待著的進程(線程),如果有就將該線程(進程)從socket的等待隊列中移除,將其加入工作隊列,這時候該進程(線程)就再次擁有了CPU的使用許可權,到這里中斷程序就結束了。
之後這個進程(線程)就執行select函數再次去檢查fd_set就能發現有socket緩沖區中有數據了,就將該socket的fd打標記,這個時候select函數就執行完了,這時候就會給上層返回一個int類型的數值,表示已經就緒的socket的數量或者是發生了錯誤。這個時候就再進行內核態到用戶態的切換,對已經打標記的socket的fd進行處理。
將原本1024bit長度的bitmap(fd_set)換成了數組的方式傳入 ,可以 解決原本1024個不夠用的情況 ,因為傳入的是數組,長度可以不止是1024了,因此socket數量可以更多,在Kernel底層會將數組轉換成鏈表。
在十多年前,linux2.6之前,不支持epoll,當時可能會選擇用Windows/Unix用作伺服器,而不會去選擇Linux,因為select/poll會隨著並發量的上升,性能變得越來越低,每次都得檢查所有的Socket列表。
1.select/poll每次調用都必須根據提供所有的socket集合,然後就 會涉及到將這個集合從用戶空間拷貝到內核空間,在這個過程中很耗費性能 。但是 其實每次的socket集合的變化也許並不大,也許就1-2個socket ,但是它會全部進行拷貝,全部進行遍歷一一判斷是否就緒。
2.select/poll的返回類型是int,只能代表當前的就緒的socket的數量/發生了錯誤, 如果還需要知道是哪些socket就緒了,則還需要再次使用系統調用去檢查哪些socket是就緒的,又是一次O(n)的操作,很耗費性能 。
1.epoll在Kernel內核中存儲了對應的數據結構(eventpoll)。我們可以 使用epoll_create()這個系統調用去創建一個eventpoll對象 ,並返回eventpoll的對象id(epfd),eventpoll對象主要包括三個部分:需要處理的正在監聽的socket_fd列表(紅黑樹結構)、socket就緒列表以及等待隊列(線程)。
2.我們可以使用epoll_ctl()這個系統調用對socket_fd列表進行CRUD操作,因為可能頻繁地進行CRUD,因此 socket_fd使用的是紅黑樹的結構 ,讓其效率能更高。epoll_ctl()傳遞的參數主要是epfd(eventpoll對象id)。
3.epoll_wait()這個系統調用默認會 將當前進程(線程)阻塞,加入到eventpoll對象的等待隊列中,直到socket就緒列表中有socket,才會將該進程(線程)重新加入工作隊列 ,並返回就緒隊列中的socket的數量。
socket包含讀緩沖區、寫緩沖區和等待隊列。當使用epoll_ctl()系統調用將socket新加入socket_fd列表時,就會將eventpoll對象引用加到socket的等待隊列中, 當網卡的中斷程序發現socket的等待隊列中不是一個進程(線程),而是一個eventpoll對象的引用,就將socket引用追加到eventpoll對象的就緒列表的尾部 。而eventpoll對象中的等待隊列存放的就是調用了epoll_wait()的進程(線程),網卡的中斷程序執行會將等待隊列中的進程(線程)重新加入工作隊列,讓其擁有佔用CPU執行的資格。epoll_wait()的返回值是int類型,返回的是就緒的socket的數量/發生錯誤,-1表示發生錯誤。
epoll的參數有傳入一個epoll_event的數組指針(作為輸出參數),在調用epoll_wait()返回的同時,Kernel內核還會將就緒的socket列表添加到epoll_event類型的數組當中。
⑵ Linux內核中select,poll和epoll的區別
在Linux Socket伺服器短編程時,為了處理大量客戶的連接請求,需要使用非阻塞I/O和復用,select、poll
和epoll是Linux API提供的I/O復用方式,自從Linux 2.6中加入了epoll之後,在高性能伺服器領域得到廣泛的
應用,現在比較出名的nginx就是使用epoll來實現I/O復用支持高並發,目前在高並 發的場景下,nginx越來越
收到歡迎。
select:
下面是select的函數介面:
[cpp] view plain
int select (int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
select 函數監視的文件描述符分3類,分別是writefds、readfds、和exceptfds。調用後select函數會阻塞,直
到有描述副就緒(有數據 可讀、可寫、或者有except),或者超時(timeout指定等待時間,如果立即返回設為
null即可),函數返回。當select函數返回後,可以 通過遍歷fdset,來找到就緒的描述符。
select目前幾乎在所有的平台上支持,其良好跨平台支持也是它的一個優點。select的一 個缺點在於單個進程
能夠監視的文件描述符的數量存在最大限制,在Linux上一般為1024,可以通過修改宏定義甚至重新編譯內核的
方式提升這一限制,但 是這樣也會造成效率的降低。
poll:
[cpp] view plain
int poll (struct pollfd *fds, unsigned int nfds, int timeout);
不同與select使用三個點陣圖來表示三個fdset的方式,poll使用一個 pollfd的指針實現。
[cpp] view plain
struct pollfd {
int fd; /* file descriptor */
short events; /* requested events to watch */
short revents; /* returned events witnessed */
};
pollfd結構包含了要監視的event和發生的event,不再使用select「參數-值」傳遞的方式。同時,pollfd並沒有
最大數量限制(但是數量過大後性能也是會下降)。 和select函數一樣,poll返回後,需要輪詢pollfd來獲取
就緒的描述符。
從上面看,select和poll都需要在返回後,通過遍歷文件描述符來獲取已經就緒的socket。事實上,同時連接的
大量客戶端在一時刻可能只有很少的處於就緒狀態,因此隨著監視的描述符數量的增長,其效率也會線性下降。
epoll:
epoll的介面如下:
[cpp] view plain
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *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 events */
epoll_data_t data; /* User data variable */
};
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
主要是epoll_create,epoll_ctl和epoll_wait三個函數。epoll_create函數創建epoll文件描述符,參數size並
'不是限制了epoll所能監聽的描述符最大個數,只是對內核初始分配內部數據結構的一個建議。返回是epoll描
述符。-1表示創建失敗。epoll_ctl 控制對指定描述符fd執行op操作,event是與fd關聯的監聽事件。op操作
有三種:添加EPOLL_CTL_ADD,刪除EPOLL_CTL_DEL,修改EPOLL_CTL_MOD。分別添加、刪除和
修改對fd的監聽事件。epoll_wait 等待epfd上的io事件,最多返回maxevents個事件。
在 select/poll中,進程只有在調用一定的方法後,內核才對所有監視的文件描述符進行掃描,而epoll事先通
過epoll_ctl()來注冊一 個文件描述符,一旦基於某個文件描述符就緒時,內核會採用類似callback的回調機制,
迅速激活這個文件描述符,當進程調用epoll_wait() 時便得到通知。
epoll的優點主要是一下幾個方面:
1. 監視的描述符數量不受限制,它所支持的FD上限是最大可以打開文件的數目,這個數字一般遠大於2048,
舉個例子,在1GB內存的機器上大約是10萬左 右,具體數目可以cat /proc/sys/fs/file-max察看,一般來說這個
數目和系統內存關系很大。select的最大缺點就是進程打開的fd是有數量限制的。這對 於連接數量比較大的
伺服器來說根本不能滿足。雖然也可以選擇多進程的解決方案( Apache就是這樣實現的),不過雖然linux上面
創建進程的代價比較小,但仍舊是不可忽視的,加上進程間數據同步遠比不上線程間同步的高效,所以也 不是
一種完美的方案。
2. IO的效率不會隨著監視fd的數量的增長而下降。epoll不同於select和poll輪詢的方式,而是通過每個fd定義的
回調函數來實現的。只有就緒的fd才會執行回調函數。
3.支持電平觸發和邊沿觸發(只告訴進程哪些文件描述符剛剛變為就緒狀態,它只說一遍,如果我們沒有採取
行動,那麼它將不會再次告知,這種方式稱為邊緣觸發)兩種方式,理論上邊緣觸發的性能要更高一些,但是
代碼實現相當復雜。
4.mmap加速內核與用戶空間的信息傳遞。epoll是通過內核於用戶空間mmap同一塊內存,避免了無畏的內存拷貝。