epoll的解釋

 epoll是多路複用IO(I/O Multiplexing)中的一種方式,但是僅用於linux2.6以上內核,在開始討論這個問題之前,先來解釋一下爲什麼需要多路複用IO.


以一個生活中的例子來解釋.

假設你在大學中讀書,要等待一個朋友來訪,而這個朋友只知道你在A號樓,但是不知道你具體住在哪裏,於是你們約好了在A號樓門口見面.

如果你使用的阻塞IO模型來處理這個問題,那麼你就只能一直守候在A號樓門口等待朋友的到來,在這段時間裏你不能做別的事情,不難知道,這種方式的效率是低下的.

現在時代變化了,開始使用多路複用IO模型來處理這個問題.你告訴你的朋友來了A號樓找樓管大媽,讓她告訴你該怎麼走.這裏的樓管大媽扮演的就是多路複用IO的角色.

進一步解釋select和epoll模型的差異.

select版大媽做的是如下的事情:比如同學甲的朋友來了,select版大媽比較笨,她帶着朋友挨個房間進行查詢誰是同學甲,你等的朋友來了,於是在實際的代碼中,select版大媽做的是以下的事情:


int n = select(&readset,NULL,NULL,100);

for (int i = 0; n > 0; ++
i)
{
if (FD_ISSET(fdarray[i], &
readset))
{
do_something(fdarray[i]);
--n;
}
}


epoll版大媽就比較先進了,她記下了同學甲的信息,比如說他的房間號,那麼等同學甲的朋友到來時,只需要告訴該朋友同學甲在哪個房間即可,不用自己親自帶着人滿大樓的找人了.於是epoll版大媽做的事情可以用如下的代碼表示:

n=epoll_wait(epfd,events,20,500);

for(i=0;i<n;++i)


在epoll中,關鍵的數據結構epoll_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_data_t data;
 
};

可以看到,epoll_data是一個union結構體,它就是epoll版大媽用於保存同學信息的結構體,它可以保存很多類型的信息:fd,指針,等等.有了這個結構體,epoll大媽可以不用吹灰之力就可以定位到同學甲.

別小看了這些效率的提高,在一個大規模併發的服務器中,輪詢IO是最耗時間的操作之一.再回到那個例子中,如果每到來一個朋友樓管大媽都要全樓的查詢同學,那麼處理的效率必然就低下了,過不久樓底就有不少的人了.

對比最早給出的阻塞IO的處理模型, 可以看到採用了多路複用IO之後, 程序可以自由的進行自己除了IO操作之外的工作, 只有到IO狀態發生變化的時候由多路複用IO進行通知, 然後再採取相應的操作, 而不用一直阻塞等待IO狀態發生變化了.

從上面的分析也可以看出,epoll比select的提高實際上是一個用空間換時間思想的具體應用.

 

 

 

 

 

 

其實,一切的解釋都是多餘的,按照我目前的瞭解,EPOLL模型似乎只有一種格式,所以大家只要參考我下面的代碼,就能夠對EPOLL有所瞭解了,代碼的解釋都已經在註釋中:

while (TRUE)
{
int nfds = epoll_wait (m_epoll_fd, m_events, MAX_EVENTS, EPOLL_TIME_OUT);//等待EPOLL時間的發生,相當於監聽,至於相關的端口,需要在初始化EPOLL的時候綁定。
if (nfds <= 0)
continue;
m_bOnTimeChecking = FALSE;
G_CurTime = time(NULL);
for (int i=0; i<nfds; i++)
{
try
{
if (m_events[i].data.fd == m_listen_http_fd)//如果新監測到一個HTTP用戶連接到綁定的HTTP端口,建立新的連接。由於我們新採用了SOCKET連接,所以基本沒用。
{
OnAcceptHttpEpoll ();
}
else if (m_events[i].data.fd == m_listen_sock_fd)//如果新監測到一個SOCKET用戶連接到了綁定的SOCKET端口,建立新的連接。
{
OnAcceptSockEpoll ();
}
else if (m_events[i].events & EPOLLIN)//如果是已經連接的用戶,並且收到數據,那麼進行讀入。
{
OnReadEpoll (i);
}

OnWriteEpoll (i);//查看當前的活動連接是否有需要寫出的數據。
}
catch (int)
{
PRINTF ("CATCH捕獲錯誤/n");
continue;
}
}
m_bOnTimeChecking = TRUE;
OnTimer ();//進行一些定時的操作,主要就是刪除一些短線用戶等。
}

 其實EPOLL的精華,按照我目前的理解,也就是上述的幾段短短的代碼,看來時代真的不同了,以前如何接受大量用戶連接的問題,現在卻被如此輕鬆的搞定,真是讓人不得不感嘆。

今天搞了一天的epoll,想做一個高併發的代理程序。剛開始真是鬱悶,一直搞不通,網上也有幾篇介紹epoll的文章。但都不深入,沒有將一些注意的地方講明。以至於走了很多彎路,現將自己的一些理解共享給大家,以少走彎路。

epoll用到的所有函數都是在頭文件sys/epoll.h中聲明,有什麼地方不明白或函數忘記了可以去看一下。
epoll和select相比,最大不同在於:

1epoll返回時已經明確的知道哪個sokcet fd發生了事件,不用再一個個比對。這樣就提高了效率。
2select的FD_SETSIZE是有限止的,而epoll是沒有限止的只與系統資源有關。


1、epoll_create函數
函數聲明:int epoll_create(int size)
該函數生成一個epoll專用的文件描述符。它其實是在內核申請一空間,用來存放你想關注的socket fd上是否發生以及發生了什麼事件。size就是你在這個epoll fd上能關注的最大socket fd數。隨你定好了。只要你有空間。可參見上面與select之不同2.

22、epoll_ctl函數
函數聲明:int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
該函數用於控制某個epoll文件描述符上的事件,可以註冊事件,修改事件,刪除事件。
參數:
epfd:由 epoll_create 生成的epoll專用的文件描述符;
op:要進行的操作例如註冊事件,可能的取值EPOLL_CTL_ADD 註冊、EPOLL_CTL_MOD 修 改、EPOLL_CTL_DEL 刪除

fd:關聯的文件描述符;
event:指向epoll_event的指針;
如果調用成功返回0,不成功返回-1

用到的數據結構
typedef union epoll_data {
void *ptr;
int fd;
__uint32_t u32;
__uint64_t u64;
} epoll_data_t;

struct epoll_event {
__uint32_t events;
epoll_data_t data;
};


如:
struct epoll_event ev;
//設置與要處理的事件相關的文件描述符
ev.data.fd=listenfd;
//設置要處理的事件類型
ev.events=EPOLLIN|EPOLLET;
//註冊epoll事件
epoll_ctl(epfd,EPOLL_CTL_ADD,listenfd,&ev);


常用的事件類型:
EPOLLIN :表示對應的文件描述符可以讀;
EPOLLOUT:表示對應的文件描述符可以寫;
EPOLLPRI:表示對應的文件描述符有緊急的數據可讀
EPOLLERR:表示對應的文件描述符發生錯誤;
EPOLLHUP:表示對應的文件描述符被掛斷;
EPOLLET:表示對應的文件描述符有事件發生;


3、epoll_wait函數
函數聲明:int epoll_wait(int epfd,struct epoll_event * events,int maxevents,int timeout)
該函數用於輪詢I/O事件的發生;
參數:
epfd:由epoll_create 生成的epoll專用的文件描述符;
epoll_event:用於回傳代處理事件的數組;
maxevents:每次能處理的事件數;
timeout:等待I/O事件發生的超時值(單位我也不太清楚);-1相當於阻塞,0相當於非阻塞。一般用-1即可
返回發生事件數。
 

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <errno.h>
  4. #include <string.h>
  5. #include <sys/types.h>
  6. #include <netinet/in.h>
  7. #include <sys/socket.h>
  8. #include <sys/wait.h>
  9. #include <unistd.h>
  10. #include <arpa/inet.h>
  11. #include <openssl/ssl.h>
  12. #include <openssl/err.h>
  13. #include <fcntl.h>
  14. #include <sys/epoll.h>
  15. #include <sys/time.h>
  16. #include <sys/resource.h>
  1. #define MAXBUF 1024
  2. #define MAXEPOLLSIZE 10000
  1. int setnonblocking(int sockfd)
  2. {
  3. if (fcntl(sockfd, F_SETFL, fcntl(sockfd, F_GETFD, 0)|O_NONBLOCK) == -1)
  4. {
  5. return -1;
  6. }
  7. return 0;
  8. }
  1. int handle_message(int new_fd)
  2. {
  3. char buf[MAXBUF + 1];
  4. int len;
  5. bzero(buf, MAXBUF + 1);
  6. len = recv(new_fd, buf, MAXBUF, 0);
  7. if (len > 0)
  8. {
  9. printf
  10. ("%d接收消息成功:'%s',共%d個字節的數據/n",
  11. new_fd, buf, len);
  12. }
  13. else
  14. {
  15. if (len < 0)
  16. printf
  17. ("消息接收失敗!錯誤代碼是%d,錯誤信息是'%s'/n",
  18. errno, strerror(errno));
  19. close(new_fd);
  20. return -1;
  21. }
  22. return len;
  23. }
  24. int main(int argc, char **argv)
  25. {
  26. int listener, new_fd, kdpfd, nfds, n, ret, curfds;
  27. socklen_t len;
  28. struct sockaddr_in my_addr, their_addr;
  29. unsigned int myport, lisnum;
  30. struct epoll_event ev;
  31. struct epoll_event events[MAXEPOLLSIZE];
  32. struct rlimit rt;
  33. myport = 5000;
  34. lisnum = 2;
  35. rt.rlim_max = rt.rlim_cur = MAXEPOLLSIZE;
  36. if (setrlimit(RLIMIT_NOFILE, &rt) == -1)
  37. {
  38. perror("setrlimit");
  39. exit(1);
  40. }
  41. else
  42. {
  43. printf("設置系統資源參數成功!/n");
  44. }
  1. if ((listener = socket(PF_INET, SOCK_STREAM, 0)) == -1)
  2. {
  3. perror("socket");
  4. exit(1);
  5. }
  6. else
  7. {
  8. printf("socket 創建成功!/n");
  9. }
  10. setnonblocking(listener);
  1. bzero(&my_addr, sizeof(my_addr));
  2. my_addr.sin_family = PF_INET;
  3. my_addr.sin_port = htons(myport);
  4. my_addr.sin_addr.s_addr = INADDR_ANY;
  1. if (bind(listener, (struct sockaddr *) &my_addr, sizeof(struct sockaddr)) == -1)
  2. {
  3. perror("bind");
  4. exit(1);
  5. }
  6. else
  7. {
  8. printf("IP 地址和端口綁定成功/n");
  9. }
  10. if (listen(listener, lisnum) == -1)
  11. {
  12. perror("listen");
  13. exit(1);
  14. }
  15. else
  16. {
  17. printf("開啓服務成功!/n");
  18. }
  19. kdpfd = epoll_create(MAXEPOLLSIZE);
  20. len = sizeof(struct sockaddr_in);
  21. ev.events = EPOLLIN | EPOLLET;
  22. ev.data.fd = listener;
  23. if (epoll_ctl(kdpfd, EPOLL_CTL_ADD, listener, &ev) < 0)
  24. {
  25. fprintf(stderr, "epoll set insertion error: fd=%d/n", listener);
  26. return -1;
  27. }
  28. else
  29. {
  30. printf("監聽 socket 加入 epoll 成功!/n");
  31. }
  32. curfds = 1;
  33. while (1)
  34. {
  35. nfds = epoll_wait(kdpfd, events, curfds, -1);
  36. if (nfds == -1)
  37. {
  38. perror("epoll_wait");
  39. break;
  40. }
  41. for (n = 0; n < nfds; ++n)
  42. {
  43. if (events[n].data.fd == listener)
  44. {
  45. new_fd = accept(listener, (struct sockaddr *) &their_addr,&len);
  46. if (new_fd < 0)
  47. {
  48. perror("accept");
  49. continue;
  50. }
  51. else
  52. {
  53. printf("有連接來自於: %d:%d,分配的 socket 爲:%d/n",
  54. inet_ntoa(their_addr.sin_addr), ntohs(their_addr.sin_port), new_fd);
  55. }
  56. setnonblocking(new_fd);
  57. ev.events = EPOLLIN | EPOLLET;
  58. ev.data.fd = new_fd;
  59. if (epoll_ctl(kdpfd, EPOLL_CTL_ADD, new_fd, &ev) < 0)
  60. {
  61. fprintf(stderr, "把 socket '%d' 加入 epoll 失敗!%s/n",
  62. new_fd, strerror(errno));
  63. return -1;
  64. }
  65. curfds++;
  66. }
  67. else
  68. {
  69. ret = handle_message(events[n].data.fd);
  70. if (ret < 1 && errno != 11)
  71. {
  72. epoll_ctl(kdpfd, EPOLL_CTL_DEL, events[n].data.fd,&ev);
  73. curfds--;
  74. }
  75. }
  76. }
  77. }
  78. close(listener);
  79. return 0;
  80. }

epoll_wait運行的原理是 等侍註冊在epfd上的socket fd的事件的發生,如果發生則將發生的sokct fd和事件類型放入到events數組中。 並且將註冊在epfd上的socket fd的事件類型給清空,所以如果下一個循環你還要關注這個socket fd的話,則需要用epoll_ctl(epfd,EPOLL_CTL_MOD,listenfd,&ev)來重新設置socket fd的事件類型。這時不用EPOLL_CTL_ADD,因爲socket fd並未清空,只是事件類型清空。這一步非常重要。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章