在上一篇博文中提到了五種IO模型,關於這五種IO模型能夠參考博文IO模型淺析-阻塞、非阻塞、IO複用、信號驅動、異步IO、同步IO,本篇主要介紹IO多路複用的使用和編程。html
多路複用是一種機制,能夠用來監聽多種描述符,若是其中任意一個描述符處於就緒的狀態,就會返回消息給對應的進程通知其採起下一步的操做。linux
當進程須要等待多個描述符的時候,一般狀況下進程會開啓多個線程,每一個線程等待一個描述符就緒,可是多路複用能夠同時監聽多個描述符,進程中無需開啓線程,減小系統開銷,在這種狀況下多路複用的性能要比使用多線程的性能要好不少。編程
在linux中,關於多路複用的使用,有三種不一樣的API,select、poll和epollsegmentfault
select的使用須要引入sys/select.h頭文件,API函數比較簡單,函數原型以下:api
int select (int __nfds, fd_set *__restrict __readfds, fd_set *__restrict __writefds, fd_set *__restrict __exceptfds, struct timeval *__restrict __timeout);
其中有一個很重要的結構體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是fd_set中最大的描述符+1,當調用select的時候,內核態會判斷fd_set中描述符是否就緒,__nfds告訴內核最多判斷到哪個描述符。併發
struct timeval { long tv_sec; //秒 long tv_usec; //微秒 }
參數__timeout指定select的工做方式:異步
select函數返回產生事件的描述符的數量,若是爲-1表示產生錯誤
值得注意的是,好比用戶態要監聽描述符1和3的讀事件,則將readset對應bit置爲1,當調用select函數以後,若只有1描述符就緒,則readset對應bit爲1,可是描述符3對應的位置爲0,這就須要注意,每次調用select的時候,都須要從新初始化並賦值readset結構體,將須要監聽的描述符對應的bit置爲1,而不能直接使用readset,由於這個時候readset已經被內核改變了。
select中,每一個fd_set結構體最多隻能標識1024個描述符,在poll中去掉了這種限制,使用poll須要引入頭文件sys/poll.h,poll調用的API以下:
int poll (struct pollfd *__fds, nfds_t __nfds, int __timeout);
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的做用同select中的__nfds,表示pollfd數組中最大的下標索引
poll函數返回產生事件的描述符的數量,若是返回0表示超時,若是爲-1表示產生錯誤
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);
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指向一個共用體結構,能夠用該共用體保存自定義的參數,或者指向被監控的文件描述符。
int epoll_create (int __size);
epoll_create函數建立一個epoll實例並返回,該實例能夠用於監控__size個文件描述符
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。
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的機制基本相同,只不過poll沒有select最大文件描述符的限制,在具體使用的時候,有以下缺點:
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...