在上一篇博文中提到了五種IO模型,關於這五種IO模型可以參考博文IO模型淺析-阻塞、非阻塞、IO複用、訊號驅動、非同步IO、同步IO,本篇主要介紹IO多路複用的使用和程式設計。
IO多路複用的概念
多路複用是一種機制,可以用來監聽多種描述符,如果其中任意一個描述符處於就緒的狀態,就會返回訊息給對應的程式通知其採取下一步的操作。
IO多路複用的優勢
當程式需要等待多個描述符的時候,通常情況下程式會開啟多個執行緒,每個執行緒等待一個描述符就緒,但是多路複用可以同時監聽多個描述符,程式中無需開啟執行緒,減少系統開銷,在這種情況下多路複用的效能要比使用多執行緒的效能要好很多。
相關API介紹
在linux中,關於多路複用的使用,有三種不同的API,select、poll和epoll
Select介紹
select的使用需要引入sys/select.h標頭檔案,API函式比較簡單,函式原型如下:
int select (int __nfds, fd_set *__restrict __readfds,
fd_set *__restrict __writefds,
fd_set *__restrict __exceptfds,
struct timeval *__restrict __timeout);
fd_set
其中有一個很重要的結構體fd_set,該結構體可以看作是一個描述符的集合,可以將fa_set看作是一個點陣圖,類似於作業系統中的點陣圖,其中每個整數的每一bit代表一個描述符,。
舉個簡單的例子,fd_set中元素的個數為2,初始化都為0,則fd_set中含有兩個整數0,假設一個整數的長度8位(為了好舉例子),則展開fd_set的結構就是 00000000 0000000,如果這個時候新增一個描述符為3,則對應fd_set程式設計 00000000 00001000,可以看到在這種情況下,第一個整數標記描述符0~7,第二個整數標記8~15,依次類推。
fd_set有四個關聯的api
void FD_ZERO(fd_set *fdset) //清空fdset,將所有bit置為0
void FD_SET(int fd, fd_set *fdset) //將fd對應的bit置為1
void FD_CLR(int fd, fd_set *fdset) //將fd對應的bit置為0
void FD_ISSET(int fd, fd_set *fdset) //判斷fd對應的bit是否為1,也就是fd是否就緒
select函式中存在三個fd_set集合,分別代表三種事件,__readfds表示讀描述符集合,__writefds表示讀描述符集合,__exceptfds表示讀描述符集合,當對應的fd_set = NULL時,表示不監聽該類描述符。
__nfds
__nfds是fd_set中最大的描述符+1,當呼叫select的時候,核心態會判斷fd_set中描述符是否就緒,__nfds告訴核心最多判斷到哪一個描述符。
timeval
struct timeval {
long tv_sec; //秒
long tv_usec; //微秒
}
引數__timeout指定select的工作方式:
- __timeout= NULL,表示select永遠等待下去,直到其中至少存在一個描述符就緒
- __timeout結構體中秒或者微妙是一個大於0的整數,表示select等待一段固定的事件,若該短時間內未有描述符就緒則返回
- __timeout= 0,表示不等待,直接返回
函式返回
select函式返回產生事件的描述符的數量,如果為-1表示產生錯誤
值得注意的是,比如使用者態要監聽描述符1和3的讀事件,則將readset對應bit置為1,當呼叫select函式之後,若只有1描述符就緒,則readset對應bit為1,但是描述符3對應的位置為0,這就需要注意,每次呼叫select的時候,都需要重新初始化並賦值readset結構體,將需要監聽的描述符對應的bit置為1,而不能直接使用readset,因為這個時候readset已經被核心改變了。
Poll介紹
select中,每個fd_set結構體最多隻能標識1024個描述符,在poll中去掉了這種限制,使用poll需要引入標頭檔案sys/poll.h,poll呼叫的API如下:
int poll (struct pollfd *__fds, nfds_t __nfds, int __timeout);
pollfd
struct pollfd {
int fd; // poll的檔案描述符
short int events; // poll關心的事件型別
short int revents; // 發生的事件型別
};
Poll使用結構體pollfd來指定一個需要監聽的描述符,結構體中fd為需要監聽的檔案描述符,events為需要監聽的事件型別,而revents為經過poll呼叫之後返回的事件型別,在呼叫poll的時候,一般會傳入一個pollfd的結構體陣列,陣列的元素個數表示監控的描述符個數,所以pollfd相對於select,沒有最大1024個描述符的限制。
事件型別有多種,在bits/poll.h中定義了多種事件型別,主要如下:
#define POLLIN 0x001 // 有資料可讀
#define POLLPRI 0x002 // 有緊迫資料可讀
#define POLLOUT 0x004 // 現在寫資料不會導致阻塞
# define POLLRDNORM 0x040 // 有普通資料可讀
# define POLLRDBAND 0x080 // 有優先資料可讀
# define POLLWRNORM 0x100 // 寫普通資料不會導致阻塞
# define POLLWRBAND 0x200 // 寫優先資料不會導致阻塞
#define POLLERR 0x008 // 發生錯誤
#define POLLHUP 0x010 // 掛起
#define POLLNVAL 0x020 // 無效檔案描述符
當一個檔案描述符要同時監聽讀寫事件時,可以寫成 events = POLLIN | POLLOUT
可以看到,poll中使用結構體儲存一個檔案描述符關心的事件,而在select中,統一使用fd_set,一個fd_set中可以是所有需要監聽讀事件的檔案描述符,也可以是所有需要寫事件的檔案描述符。
相比來說,poll比select更加的靈活,在呼叫poll之後,無需像select一樣需要重新對檔案描述符初始化,因為poll返回的事件寫在了pollfd->revents成員中。
__fds
__fds的作用同select中的__nfds,表示pollfd陣列中最大的下標索引
__timeout
- __timeout = -1:poll阻塞直到有事件產生
- __timeout = -0:poll立刻返回
- __timeout != -1 && __timeout != 0:poll阻塞__timeout對應的時候,如果超過該時間沒有事件產生則返回
函式返回
poll函式返回產生事件的描述符的數量,如果返回0表示超時,如果為-1表示產生錯誤
Epoll介紹
epoll中,使用一個描述符來管理多個檔案描述符,使用epoll需要引入標頭檔案sys/epoll.h,epoll相關的api函式如下:
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);
epoll_event
typedef union epoll_data {
void *ptr; // 可以用改指標指向自定義的引數
int fd; // 可以用改成員指向epoll所監控的檔案描述符
uint32_t u32;
uint64_t u64;
} epoll_data_t;
struct epoll_event {
uint32_t events; // epoll事件
epoll_data_t data; // 使用者資料
} __EPOLL_PACKED;
epoll_event結構體中,首先是一個events的整型變數,類似於pollfd->events,表示要監控的事件,events支援的事件型別在sys/epoll.h的標頭檔案中,跟pollfd中的事件型別基本移植,如下,這裡只寫出一部分:
enum EPOLL_EVENTS {
EPOLLIN = 0x001,
#define EPOLLIN EPOLLIN // 有資料可讀
EPOLLPRI = 0x002,
#define EPOLLPRI EPOLLPRI // 有緊迫資料可讀
EPOLLOUT = 0x004,
#define EPOLLOUT EPOLLOUT // 現在寫資料不會導致阻塞
EPOLLRDNORM = 0x040,
#define EPOLLRDNORM EPOLLRDNORM // 有普通資料可讀
EPOLLRDBAND = 0x080,
#define EPOLLRDBAND EPOLLRDBAND // 有優先資料可讀
EPOLLWRNORM = 0x100,
#define EPOLLWRNORM EPOLLWRNORM // 寫普通資料不會導致阻塞
EPOLLWRBAND = 0x200,
#define EPOLLWRBAND EPOLLWRBAND // 寫優先資料不會導致阻塞
...
EPOLLERR = 0x008,
#define EPOLLERR EPOLLERR // 發生錯誤
EPOLLHUP = 0x010,
#define EPOLLHUP EPOLLHUP // 掛起
EPOLLRDHUP = 0x2000,
...
};
epoll_event中的data指向一個共用體結構,可以用該共用體儲存自定義的引數,或者指向被監控的檔案描述符。
epoll_create
int epoll_create (int __size);
epoll_create函式建立一個epoll例項並返回,該例項可以用於監控__size個檔案描述符
epoll_ctl
int epoll_ctl (int __epfd, int __op, int __fd, struct epoll_event *__event);
該函式用來向epoll中註冊事件函式,其中__epfd為epoll_create返回的epoll例項,__op表示要進行的操作,__fd為要進行監控的檔案描述符,__event要監控的事件。
__op可用的型別定義在sys/epoll.h標頭檔案中,如下:
#define EPOLL_CTL_ADD 1 // 新增檔案描述符
#define EPOLL_CTL_DEL 2 // 刪除檔案描述符
#define EPOLL_CTL_MOD 3 // 修改檔案描述符(指的是epoll_ctl中傳入的__event)
該函式如果呼叫成功返回0,否則返回-1。
epoll_wait
int epoll_wait (int __epfd, struct epoll_event *__events, int __maxevents, int __timeout);
epoll_wait類似與select中的select函式、poll中的poll函式,等待核心返回監聽描述符的事件產生,其中__epfd是epoll_create建立的epoll例項,__events陣列為epoll_wait要返回的已經產生的事件集合,其中第i個元素成員的__events[i]->data->fd表示產生該事件的描述符,__maxevents為希望返回的最大的事件數量(通常為__events的大小),__timeout和poll中的__timeout相同。該函式返回已經就緒的事件的數量,如果為-1表示出錯。
select、poll、epoll比較
select和poll的機制基本相同,只不過poll沒有select最大檔案描述符的限制,在具體使用的時候,有如下缺點:
- 每次呼叫select或者poll,都需要將監聽的fd_set或者pollfd傳送給核心態,如果需要監聽大量的檔案描述符,這樣的效率是很低下的
- 在核心態中,每次需要對傳入的檔案描述符進行輪詢,查詢是否有對應的事件產生。
epoll的高效在於將這些分開,首先epoll不是在每次呼叫epoll_wait的時候,將描述符傳送給核心,而是在epoll_ctl的時候傳送描述符給核心,當呼叫epoll_wait的收,不用每次都接收
不像select和poll使用一個單獨的API函式,在epoll中,使用epoll_create建立一個epoll例項,然後當呼叫epoll_ctl新增監聽描述符的時候,這個時候才將使用者態的描述符傳送到核心態,因為epoll_wait呼叫的頻率肯定要比epoll_create的頻率要高,所以當epoll_wait的時候無需傳送任何描述符到使用者態;
關於第二點,在核心態中,使用一個描述符就緒的連結串列,當描述符就緒的時候,在核心態中會使用回撥函式,該函式會將對應的描述符新增入就緒連結串列中,那麼當epoll_wait呼叫的時候,就不需要遍歷所有的描述符檢視是否有就緒的事件,而是直接檢視連結串列是否為空。
總結
可以使用一個生活中的場景來對三者的區別做個總結,仍然接著筆者的上一篇博文IO模型淺析-阻塞、非阻塞、IO複用、訊號驅動、非同步IO、同步IO中吃飯的例子:
在這個例子中,服務員和餐廳代表核心,客戶“你”就是使用者態程式,可能覺得這個例子寫的不好,在這裡寫下加深記憶。
select和poll:你去餐廳請客吃飯,你是個豪爽的人,點了很多菜,你告訴服務員對應種類的菜有多少上多少,服務員將菜名一一寫在紙上。然後你開始問服務員飯菜有好了麼,服務員看著你的選單一大串,頭皮發麻,於是按著選單的順序去廚房檢視飯菜有沒有好,如果菜沒有好就劃掉選單中對應的菜,終於找出了所有已經燒好的飯菜,服務員把飯菜端給了你。可是這個時候選單上只能看到已經準備好的菜了,沒準備好的菜看不清了,你覺得這個服務員做事很傻逼,沒辦法將就點,誰讓你性格好呢,於是你重新寫了一份選單(可能這個過程中你又想點一些新的菜或者刪除一些菜)。接下來你又去問飯菜好沒好,服務員又開始按照選單的順序去廚房檢視飯菜有沒有好。。。(select和poll的主要區別就在於,select中的選單是有限的,而poll中的選單是無限的,你可以點任意種類的菜)
epoll:你去餐廳請客吃飯,你是個豪爽的人,點了很多菜,你告訴服務員對應種類的菜有多少上多少,服務員將菜名一一錄入到餐廳後臺的選單管理軟體中,廚房的師傅燒好一道菜在管理軟體中標記完成一下,然後在燒好的菜上掛上對應的桌號放在取菜區,這個時候你來問服務員飯菜有準備好的麼,服務員於是查一下管理軟體,有標記欸,於是從取菜區取出對應桌號的飯菜送給你,清空標記。過了段時間,你又想點一道新的菜,於是叫來服務員,服務員在選單軟體中新增一欄。接下來你又去問飯菜好沒好,服務員又開始看選單軟體中是否有標記完成的資訊。。。
另外關於epoll的高效還有很多細節,例如使用mmap將使用者空間和核心空間的地址對映到同一塊實體記憶體地址,使用紅黑樹儲存要監聽的事件等等,具體的細節可以參考博文select、poll、epoll之間的區別總結整理、高併發網路程式設計之epoll詳解、Linux下的I/O複用與epoll詳解、徹底學會使用epoll(一)——ET模式實現分析等幾篇文章。
接下來使用select、poll、epoll實現一個TCP反射程式
參考資料
UNIX網路變成卷1:套接字聯網API
作者:yearsj
轉載請註明出處:https://segmentfault.com/a/11…