[TOC] epoll介紹 epoll的行為與poll(2)相似,監視多個有IO事件的文件描述符。epoll除了提供select/poll那種IO事件的水平觸發(Level Triggered)外,還提供了邊緣觸發(Edge Triggered),這就使得用戶空間程式有可能緩存IO狀態,減少epol ...
目錄
epoll介紹
epoll的行為與poll(2)相似,監視多個有IO事件的文件描述符。epoll除了提供select/poll那種IO事件的水平觸發(Level Triggered)外,還提供了邊緣觸發(Edge Triggered),這就使得用戶空間程式有可能緩存IO狀態,減少epoll_wait/epoll_pwait的調用,提高應用程式效率。
epoll_create(2)
創建一個新的epoll實例,並返回一個引用該實例的文件描述符
epoll_ctl(2)
創建epoll實例後,註冊對感興趣的文件描述符。當前註冊在epoll實例上的文件描述符集被稱為epoll集合。
epoll_wait(2)
等待I/O事件,如果當前沒有事件可用,則阻塞調用線程。
水平觸發
和邊沿觸發
epoll事件分佈介面既可以表現為邊緣觸發(ET),也可以表現為水平觸發(LT)。這兩種機制的區別
可以這樣描述。假設有這種情況發生:
- 表示管道(rfd)的讀側的文件描述符在epoll實例上註冊。
- 管道寫入器在管道的寫入端寫入2 kB的數據。
- 調用epoll_wait(2)將返回rfd作為就緒文件描述符。
- 管道讀取器從rfd讀取1kb的數據。
- epoll_wait(2)調用完成。
如果使用邊緣觸發標誌將rfd文件描述符註冊到epoll介面,那麼第五步的epoll_wait(2)的調用可能會掛起,儘管文件輸入緩衝區仍然有1kb數據可讀;同時,遠程對等端可能正在期望基於它已發送的數據的應答。這樣做的原因是,只有在被監視文件描述符上發生更改時,邊緣觸發模式才交付事件。因此,在步驟5中,調用者可能會以等待那些仍在輸入緩衝區中的數據的狀態下結束。
在上面的例子中,將生成rfd上的一個事件,因為在2中完成了寫入,而在3中使用了該事件。由於在4中完成的讀操作不會消耗整個緩衝區數據,所以在步驟5中完成的對epoll_wait(2)的調用可能會無限期阻塞。
使用EPOLLET標誌的應用程式應該使用非阻塞文件描述符,以避免在處理多個文件描述符時出現有阻塞的讀寫饑餓任務。建議使用epoll作為邊沿觸發(EPOLLET)介面的方式如下:
i、 具有非阻塞文件描述符
ii、只有在read(2)或write(2)返回EAGAIN後才等待事件。
相反,當EPOLLET作為水平觸發介面使用時(預設情況下,沒有指定EPOLLET), epoll只是一個更快的poll(2),並且可以在使用後者的任何地方使用,因為它具有相同的語義。
Epoll的優點:
1、支持一個進程打開大數目的socket描述符(FD)
select能打開的文件描述符有一定的限制,FD_SETSIZE設置,預設值是2048,有兩種解決方法,1、修改它的值,然後重新編譯內核。2、使用多進程加入要併發20w個客戶,那麼就要開100進程;epoll則沒有這個限制,它所支持的FD上限是最大可以打開文件的數目,這個數字一般遠大於2048,舉個例子,在1GB記憶體的機器上大約是2萬左右,具體數目可以cat /proc/sys/fs/file-max察看,一般來說這個數目和系統記憶體關係很大。
2、IO效率不隨FD數目增加而線性下降
select/poll採用輪詢的方式掃描文件描述符,文件描述符數量越多,性能越差;內核 / 用戶空間記憶體拷貝問題,select/poll需要複製大量的句柄數據結構,產生巨大的開銷;select/poll返回的是含有整個句柄的數組,應用程式需要遍歷整個數組才能發現哪些句柄發生了事件,導致效率呈現線性下降。但是epoll不存在這個問題,它只會對"活躍"的socket進行操作---這是因為在內核實現中epoll是根據每個fd上面的callback函數實現的。
3、支持邊緣觸發模式
select/poll的觸發方式是水平觸發,應用程式如果沒有完成對一個已經就緒的文件描述符進行IO操作,那麼之後每次select/poll調用還是會將這些文件描述符通知進程。
4、使用mmap加速內核與用戶空間的消息傳遞。
select/poll和epoll都需要內核把FD消息通知給用戶空間,如何避免不必要的記憶體拷貝很重要,在這點上,select/poll需要複製整個FD數組,產生巨大的開銷;而epoll是通過內核於用戶空間mmap同一塊記憶體實現的。
epoll的系統調用
epoll_create
int epoll_create(int size);
int epoll_create1(int flags);
創建一個epoll的句柄。自從linux2.6.8之後,size參數是被忽略的,更推薦使用epoll_crete1(0)來替代,flags可以設置EPOLL_CLOEXEC標誌
epoll_ctl
#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
該系統調用對文件描述符epfd引用的epoll(7)實例執行控制操作。它請求對目標文件描述符fd執行操作op。
epfd
: epoll_create創建的文件描述符.
op
:參數的有效參數為:
EPOLL_CTL_ADD
在文件描述符epfd引用的epoll實例上註冊目標文件描述符fd。
EPOLL_CTL_MOD
修改已註冊描述符fd關聯的事件。
EPOLL_CTL_DEL
從epfd引用的epoll實例中刪除(取消註冊)目標文件描述符fd。該事件將被忽略,並且可以是NULL
fd
:待監聽的fd
epoll_event
: 描述鏈接到文件描述符fd的對象,它的定義如下
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 */
};
events成員是由以下可用事件類型的零個或多個組合在一起組成的位掩碼:
EPOLLIN :關聯的文件描述符可以讀(包括對端SOCKET正常關閉);
EPOLLOUT:關聯的文件描述符可以寫;
EPOLLPRI:關聯的文件描述符有緊急的數據可讀(這裡應該表示有帶外數據到來);
EPOLLERR:關聯的文件描述符發生錯誤;
EPOLLHUP:關聯的文件描述符被掛斷;
EPOLLRDHUP:流套接字對等關閉連接,或半關閉寫。(當使用邊緣觸發監視時,此標記對於編寫簡單代碼檢測對等端是否關閉特別有用。2.6.17引入)
EPOLLET: 將EPOLL設為邊緣觸發(Edge Triggered)模式,這是相對於水平觸發(Level Triggered)來說的。
EPOLLONESHOT:只監聽一次事件,當監聽完這次事件之後,如果還需要繼續監聽這個fd的話,需要再次把這個fd加入到EPOLL隊列里
它們在內核頭文件里的定義如下:
33
34 enum EPOLL_EVENTS
35 {
36 EPOLLIN = 0x001,
37 #define EPOLLIN EPOLLIN
38 EPOLLPRI = 0x002,
39 #define EPOLLPRI EPOLLPRI
40 EPOLLOUT = 0x004,
41 #define EPOLLOUT EPOLLOUT
42 EPOLLRDNORM = 0x040,
43 #define EPOLLRDNORM EPOLLRDNORM
44 EPOLLRDBAND = 0x080,
45 #define EPOLLRDBAND EPOLLRDBAND
46 EPOLLWRNORM = 0x100,
47 #define EPOLLWRNORM EPOLLWRNORM
48 EPOLLWRBAND = 0x200,
49 #define EPOLLWRBAND EPOLLWRBAND
50 EPOLLMSG = 0x400,
51 #define EPOLLMSG EPOLLMSG
52 EPOLLERR = 0x008,
53 #define EPOLLERR EPOLLERR
54 EPOLLHUP = 0x010,
55 #define EPOLLHUP EPOLLHUP
56 EPOLLRDHUP = 0x2000,
57 #define EPOLLRDHUP EPOLLRDHUP
58 EPOLLEXCLUSIVE = 1u << 28,
59 #define EPOLLEXCLUSIVE EPOLLEXCLUSIVE
60 EPOLLWAKEUP = 1u << 29,
61 #define EPOLLWAKEUP EPOLLWAKEUP
62 EPOLLONESHOT = 1u << 30,
63 #define EPOLLONESHOT EPOLLONESHOT
64 EPOLLET = 1u << 31
65 #define EPOLLET EPOLLET
66 };
67
68
69 /* Valid opcodes ( "op" parameter ) to issue to epoll_ctl(). */
70 #define EPOLL_CTL_ADD 1 /* Add a file descriptor to the interface. */
71 #define EPOLL_CTL_DEL 2 /* Remove a file descriptor from the interface. */
72 #define EPOLL_CTL_MOD 3 /* Change file descriptor epoll_event structure. */
epoll_wait
#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event *events,
int maxevents, int timeout);
int epoll_pwait(int epfd, struct epoll_event *events,
int maxevents, int timeout,
const sigset_t *sigmask);
等待在epoll監控的事件中已經發生的事件。
epfd
: epoll_create() 的返回值.
events
: 分配好的epoll_event結構體數組,epoll將會把發生的事件賦值到events數組中(events不可以是空指針,內核只負責把數據複製到這個events數組中,不會去幫助我們在用戶態中分配記憶體)
maxevents
: maxevents告知內核這個events有多大,這個 maxevents的值大於0(否則Error :Invalid argument)
timeout
: 超時時間(毫秒,0會立即返回,-1將不確定,也有說法說是永久阻塞)。如果函數調用成功,返回對應I/O上已準備好的文件描述符數目,如返回0表示已超時,它會阻塞直到
- 一個文件描述符有事件發生;
- 信號處理器中斷;
- 超時;
epoll示常式序
此程式簡單測試一下三個API,註冊標準輸出的描述符到epoll,監視標準輸出的讀事件,觸發後回顯一遍,quit退出程式.
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <errno.h>
#include <sys/epoll.h>
#include <vector>
typedef std::vector<struct epoll_event> PollFdList;
int main(int argc ,char **argv)
{
int fd;
char buf[1024];
int i,res,real_read, maxfd;
if((fd=open("/dev/stdin",O_RDONLY|O_NONBLOCK)) < 0)
{
fprintf(stderr,"open data1 error:%s",strerror(errno));
return 1;
}
PollFdList m_pollfds;
int epfd = epoll_create1(EPOLL_CLOEXEC);
struct epoll_event ev;
ev.events = EPOLLIN | EPOLLPRI;
ev.data.fd = fd;
epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev);
m_pollfds.resize(1024);
while(1)
{
int ret = epoll_wait(epfd, m_pollfds.data(), m_pollfds.size(), 5000);
if (ret < 0)
{
printf("ePoll error : %s\n",strerror(errno));
return 1;
}
if(ret == 0){
printf("ePoll timeout\n");
continue;
}
for (i = 0; i< 1; i++)
{
if (m_pollfds[i].events & EPOLLIN)
{
memset(buf, 0, 1024);
real_read = read(m_pollfds[i].data.fd, buf, 1024);
if (real_read < 0)
{
if (errno != EAGAIN)
{
printf("read eror : %s\n",strerror(errno));
continue;
}
}
else if (!real_read)
{
close(m_pollfds[i].data.fd);
m_pollfds[i].events = 0;
}
else
{
if (i == 0)
{
buf[real_read] = '\0';
printf("%s", buf);
if ((buf[0] == 'q') || (buf[0] == 'Q'))
{
printf("quit\n");
return 1;
}
}
else
{
buf[real_read] = '\0';
printf("%s", buf);
}
}
}
}
}
exit(0);
}
./test
hello
hello
hello epoll
hello epoll
ePoll timeout
quit
quit
quit