您好,登錄后才能下訂單哦!
I/O多路復用是在多線程或多進程編程中常用技術。主要是通過select/epoll/poll三個函數支持的。在此主要對select和epoll函數詳細介紹。
該函數運行進程指示內核等待多個事件中的任何一個發生,并只有一個或多個事件發生或經歷一段指定的時間后才喚醒它。
調用select告知內核對哪些描述符(就讀、寫或異常條件)感興趣以及等待多長時間。我們感興趣的描述符不局限于套接字,任何描述符都可以使用select來測試。
函數原型:
#include<sys/select.h>#include<sys/time.h>int select(int maxfdp1, fd_set *readset, fd_set *writeset, fd_set *exceptset, const struct timeval *timeout); 返回:若有就緒描述符則為其數目,若超時則為0,若出錯則為-1
永遠等待下去:僅在有一個描述符準備好I/O時才返回,將其設為空指針
等待一段固定時間:在有一個描述符準備好I/O時返回,但是不超過由該參數所指向的timeval結構中指定的秒數和微秒數。
根本不等待:檢查描述符后立即返回,這就是輪詢。為此,該參數必須指向一個timeval結構,但是其中的值必須設置為0
最后一個參數timeout,它告知內核等待所指定描述符中的任何一個就緒可花多長時間。該參數有三種可能:
三個參數readset,writeset,exceptset指定我們要讓內核測試讀、寫和異常條件的描述符。
如何給這三個參數的每一個參數指定一個或多個描述符值是一個設計上的問題。select使用描述符集,通常是一個整數數組,其中每個整數中的每一位對應一個描述符。舉例來說,假設使用32位整數,那么該數組的第一個元素對應于描述符0~31,第二個元素對應于描述符32~63,以此類推。所有這些實現細節都與應用程序無關,它們隱藏在名為fd_set的數據類型和以下四個宏中:
void FD_ZERO(fd_set *fdset); //clear all bits in fdsetvoid FD_SET(int fd, fd_set *fdset); //turn on the bit for fd in fdsetvoid FD_CLR(int fd, fd_set *fdset); //turn off the bit for fd in fdsetint FD_ISSET(int fd, fd_set *fdset); //is the bit for fd on in fdset?
我們分配一個fd_set數據類型的描述符集,并用這些宏設置或測試該集合中的每一位,也可以用C語言中的賦值語句把它賦值成另外一個描述符集。
注意:前面所討論的每個描述符占用整數數組中的一位的方法僅僅是select函數的可能實現之一。
maxfdp1參數指定待測試的描述符個數,它的值是待測試的最大描述符加1。描述符0,1,2,...,直到maxfdp1 - 1均被測試。
select函數修改由指針readset,writeset和exceptset所指向的描述符集,因而這三個參數都是值-結果參數。該函數返回后,我們使用FD_ISSET宏來測試fd_set數據類型中的描述符。描述符集內任何與未就緒描述符所對應的位返回時均清成0.為此,每次重新調用select函數時,我們都得再次把所有描述符集內所關心的位均置為1
滿足下列四個條件之一的任何一個時,一個套接字準備好讀:
該套接字接收緩沖區中的數據字節數大于等于套接字接收緩沖區低水位標記的當前大小。對于這樣的套接字執行讀操作不會阻塞并將返回一個大于0的值(也就是返回準備好讀入的數據)。我們使用SO_RECVLOWAT套接字選項設置套接字的低水位標記。對于TCP和UDP套接字而言,其默認值為1
該連接的讀半部關閉(也就是接收了FIN的TCP連接)。對這樣的套接字的讀操作將不阻塞并返回0(也就是返回EOF)
該套接字時一個監聽套接字且已完成的連接數不為0。
其上有一個套接字錯誤待處理。對這樣的套接字的讀操作將不阻塞并返回-1(也就是返回一個錯誤),同時把errno設置為確切的錯誤條件。這些待處理錯誤也可以通過SO_ERROR套接字選項調用getsockopt獲取并清除。
下列四個條件的任何一個滿足時,一個套接字準備好寫:
該套接字發送緩沖區中的可用字節數大于等于套接字發送緩沖區低水位標記的當前大小,并且或該套接字已連接,或者該套接字不需要連接(如UDP套接字)。這意味著如果我們把這樣的套接字設置成非阻塞的,寫操作將不阻塞并返回一個正值(如由傳輸層接收的字節數)。我們使用SO_SNDLOWAT套接字選項來設置該套接字的低水位標記。對于TCP和UDP而言,默認值為2048
該連接的寫半部關閉。對這樣的套接字的寫操作將產生SIGPIPE信號
使用非阻塞式connect套接字已建立連接,或者connect已經已失敗告終
其上有一個套接字錯誤待處理。對這樣的套接字的寫操作將不阻塞并返回-1(也就是返回一個錯誤),同時把errno設置為確切的錯誤條件。這些待處理錯誤也可以通過SO_ERROR套接字選項調用getsockopt獲取并清除。
如果一個套接字存在帶外數據或者仍處于帶外標記,那么它有異常條件待處理。
注意:當某個套接字上發生錯誤時,它將由select標記為既可讀又可寫
接收低水位標記和發送低水位標記的目的在于:允許應用進程控制在select可讀或可寫條件之前有多少數據可讀或有多大空間可用于寫。
任何UDP套接字只要其發送低水位標記小于等于發送緩沖區大小(默認應該總是這種關系)就總是可寫的,這是因為UDP套接字不需要連接。
函數原型:
#include<poll.h>int poll(struct pollfd *fdarray, unsigned long nfds, int timeout); 返回:若有就緒描述符則為數目,若超時則為0,若出錯則為-1
第一個參數是指向一個結構數組第一個元素的指針。每個數組元素都是一個pollfd結構,用于指定測試某個給定描述符fd的條件。
struct pollfd{ int fd; //descriptor to check short event; //events of interest on fd short revents; //events that occurred on fd};
要測試的條件由events成員指定,函數在相應的revents成員中返回該描述符的狀態。(每個描述符都有兩個變量,一個為調用值,另一個為返回結果,從而避免使用值-結果參數。)
poll事件
epoll是Linux特有的I/O復用函數。它在實現和使用上與select、poll有很大的差異。
首先,epoll使用一組函數來完成任務,而不是單個函數。
其次,epoll把用戶關心的文件描述符上的事件放在內核里的一個事件表中,從而無須像select和poll那樣每次調用都要重復傳入文件描述符集或事件集。
但epoll需要使用一個額外的文件描述符,來唯一標識內核中的這個事件表
epoll文件描述符使用如下方式創建:
#include<sys/epoll.h>int epoll_create(int size);
size參數完全不起作用,只是給內核一個提示,告訴它事件表需要多大。該函數返回的文件描述符將用作其他所有epoll系統調用的第一個參數,以指定要訪問的內核事件表。
下面的函數用來操作epoll的內核事件表:
#include<sys/epoll.h>int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); 返回:若成功返回0,失敗返回-1,并置errno
fd參數是要操作的文件描述符,op參數則指定操作類型。操作類型有以下三類:
EPOLL_CTL_ADD, 往事件表中注冊fd上的事件
EPOLL_CTL_MOD, 修改fd上的注冊事件
EPOLL_CTL_DEL, 刪除fd上的注冊事件
event指定事件,它是epoll_event結構指針類型,epoll_event的定義如下:
strcut epoll_event{ __uint32_t events; //epoll事件 epoll_data_t data; //用戶數據};
其中,events成員描述事件類型。epoll支持的事件類型同poll基本相同。表示epoll事件類型的宏在poll對應的宏前加上"E",比如epoll的數據可讀事件是EPOLLIN。
epoll有兩個額外的事件類型——EPOLLET和EPOLLONESHOT。它們對于epoll的高效運作非常關鍵。
data成員用于存儲用戶數據,是一個聯合體:
typedef union epoll_data{ void *ptr; int fd; uint32_t u32; uint64_t u64; }epoll_data_t;
其中4個成員用得最多的是fd,它指定事件所從屬的目標文件描述符。
epoll系列系統調用的主要接口是epoll_wait函數,它在一段超時時間內等待一組文件描述符上的事件,其原型如下:
#include<sys/epoll.h>int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout); 返回:若成功返回就緒的文件描述符個數,失敗時返回-1,并置errnoo
maxevents參數指定最多監聽多少個事件,它必須大于0
event_wait函數如果檢測到事件,就將所有就緒事件從內核事件表(由epfd參數指定)中復制到它的第二個參數events指向的數組中。這個數組只用于輸出epoll_wait檢測到的就緒事件,而不像select和poll的數組參數那樣既用于傳入用戶注冊的事件,又用于輸出內核檢測到的就緒事件。這就極大地提高了應用程序索引就緒文件描述符的效率。
下面代碼給出 poll和epoll在使用上的差別:
//如何索引poll返回的就緒文件描述符int ret = poll(fds, MAX_EVENT_NUMBER, -1);//必須遍歷所有已注冊文件描述符并找到其中的就緒者for(int i = 0; i < MAX_EVENT_NUMBER; ++i){ if(fds[i].revents & POLLIN) //判斷第 i 個文件描述符是否就緒 { int sockfd = fds[i].fd; //處理sockfd } }//如何索引epoll返回的文件描述符int ret = epoll_wait(epollfd, events, MAX_EVENT_NUMBER, -1);//僅遍歷就緒的ret個文件描述符for(int i = 0; i < ret; ++i){ int sockfd = events[i].data.fd; //sockfd肯定就緒,直接處理}
LT和ET模式
LT(Level Trigger,電平觸發)模式:是默認工作模式,在這種模式下的epoll相當于一個效率較高的poll。當epoll_wait檢測到其上有事件發生并將此事件通知應用程序后,應用程序可以不立即處理該事件。這樣,當應用程序下一次調用epoll_wait時,epoll_wait還會再次向應用程序通告此事件。
ET(Edge Trigger,邊沿觸發)模式。對于ET工作模式下的文件描述符,當epoll_wait檢測到其上有事件發生并將此事件通知應用程序后,應用程序必須立即處理該事件,因為后續的epoll_wait調用將不再向應用程序通知這一事件。
ET模式在很大程度上降低了同一個epoll事件被重復觸發的次數。因此效率要比LT模式高。
每個使用ET模式的文件描述符都應該是非阻塞的。如果文件描述符是阻塞的,那么讀或寫操作將會因為沒有后續的時間而一直處于阻塞狀態(饑渴狀態)
EPOLLONESHOT事件
即使使用ET模式,一個socket上的某個事件還是可能被觸發多次。這在并發程序中引起一個問題。比如一個線程(或進程)在讀取完某個socket上的數據后開始處理這些數據,而在數據的處理過程中該socket上又有新數據可讀(EPOLLIN再次被觸發),此時另外一個線程被喚醒來讀取這些新的數據。于是出現了兩個線程同時操作一個socket的場面。這當然不是我們期望的。我們期望的是一個socket連接在任一時刻都只被一個線程處理。
對于注冊了EPOLLONESHOT事件的文件描述符,操作系統最多觸發其上注冊的一個可讀、可寫或異常事件,且只觸發一次,除非我們使用epoll_ctl函數重置該文件描述符上的EPOLLONESHOT事件。這樣,當一個線程在處理某個socket時,其他線程時不可能有機會操作該socket的。但反過來思考,注冊了EPOLLONESHOT事件的socket一旦被某個線程處理完畢,該線程就應該立即重置這個socket上的EPOLLONESHOT事件,以確保這個socket下一次可讀時,其EPOLLIN事件能被觸發,進而讓其他工作線程有機會繼續處理這個socket.
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。