Socket編程實踐(11) --epoll原理與封裝

常用模型的特點

    Linux 下設計併發網絡程序,有典型的Apache模型(Process Per Connection,PPC), TPC(Thread Per Connection)模型,以及 select/polL模型和epoll模型。

 

1 、PPC/TPC 模型

    這兩種模型思想類似,就是讓每一個到來的連接一邊自己做事去,別再來煩我.只是 PPC 是爲它開了一個進程,而 TPC 開了一個線程。可是別煩我是有代價的,它要時間和空間啊,連接多了之後,那麼多的進程/線程切換,這開銷就上來了;因此這類模型能接受的最大連接數都不會高,一般在幾百個左右

2 、select 模型

    1) 最大併發數限制,因爲一個進程所打開的 FD (文件描述符)是有限制的,由 FD_SETSIZE 設置,默認值是 1024,因此 Select 模型的最大併發數就被相應限制了。自己改改這個 FD_SETSIZE ?想法雖好,可是先看看下面吧 …

    2) 效率問題, select 每次調用都會線性掃描全部的 FD 集合,這樣效率就會呈現線性下降,把 FD_SETSIZE 改大的後果就是,大家都慢慢來,什麼?都超時了??!!

    3) 內核/用戶空間內存拷貝問題,如何讓內核把 FD 消息通知給用戶空間呢?在這個問題上 select 採取了內存拷貝方法。

3、 poll 模型

    基本上效率和 select 是相同的, select 缺點的 2 和 3 它都沒有改掉。

 

Epoll 的提升

    1. Epoll 沒有最大併發連接的限制,上限是最大可以打開文件的數目,這個數字一般遠大於 2048, 一般來說這個數目和系統內存關係很大 ,具體數目可以 cat /proc/sys/fs/file-max[599534] 察看。

    2. 效率提升, Epoll最大的優點就在於它只管你“活躍”的連接 ,而跟連接總數無關,因此在實際的網絡環境中, Epoll的效率就會遠遠高於 select 和 poll 。

    3. 內存拷貝, Epoll 在這點上使用了“共享內存”,這個內存拷貝也省略了。


epoll的使用

epoll的接口非常簡單,一共就3/4個函數:

  1. int epoll_create(int size);  
  2. int epoll_create1(int flags);  
  3. int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);  
  4. int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);  

   1. 對於epoll_create1 的flag參數: 可以設置爲0 或EPOLL_CLOEXEC,爲0時函數表現與epoll_create一致, EPOLL_CLOEXEC標誌與open 時的O_CLOEXEC 標誌類似,即進程被替換時會關閉打開的文件描述符(需要注意的是,epoll_create與epoll_create1當創建好epoll句柄後,它就是會佔用一個fd值,在linux下如果查看/proc/<pid>/fd/,是能夠看到這個fd的,所以在使用完epoll後,必須調用close()關閉,否則可能導致fd被耗盡)。

   2. 對於epoll_ctl, op參數表示動作,用三個宏來表示:

EPOLL_CTL_ADD

註冊新的fd到epfd中

EPOLL_CTL_DEL

從epfd中刪除一個fd

EPOLL_CTL_MOD

修改已經註冊的fd的監聽事件

   3. 對於epoll_wait:

      events:結構體指針, 一般是一個數組

      maxevents:事件的最大個數, 或者說是數組的大小

      timeout:超時時間, 含義與poll的timeout參數相同,設爲-1表示永不超時;

   4. epoll_event結構體

  1. struct epoll_event  
  2. {  
  3.     uint32_t     events;      /* Epoll events */  
  4.     epoll_data_t data;        /* User data variable */  
  5. };  
  6. typedef union epoll_data  
  7. {  
  8.     void        *ptr;  
  9.     int          fd;  
  10.     uint32_t     u32;  
  11.     uint64_t     u64;  
  12. } epoll_data_t;  

一般data 共同體我們設置其成員fd即可,也就是epoll_ctl 函數的第三個參數。

events集合

EPOLLIN

表示對應的文件描述符可以讀(包括對端SOCKET正常關閉)

EPOLLOUT

表示對應的文件描述符可以寫

EPOLLPRI

表示對應的文件描述符有緊急的數據可讀(這裏應該表示有帶外數據到來)

EPOLLERR

表示對應的文件描述符發生錯誤

EPOLLHUP

表示對應的文件描述符被掛斷

EPOLLET

將EPOLL設爲邊緣觸發(Edge Triggered)模式,這是相對於水平觸發(Level Triggered)來說的

EPOLLONESHOT

只監聽一次事件,當監聽完這次事件之後,如果還需要繼續監聽這個socket的話,需要再次把這個socket加入到EPOLL隊列裏

  1. /**示例: epoll使用示例 
  2.     注:client端與測試端與前同, 而且使用相同的測試端測試select/poll/epoll, 可以發現epoll的效率是非常高的**/  
  3.   
  4. //添加fd到epoll  
  5. void addFd(int epollfd, int fd, const uint32_t &events = EPOLLIN, bool et = false)  
  6. {  
  7.     struct epoll_event event;  
  8.     event.events = events;  
  9.     if (et)  
  10.         event.events |= EPOLLET;  
  11.     event.data.fd = fd;  
  12.     if( epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event) == -1 )  
  13.         err_exit("epoll_ctl_add error");  
  14. }  
  15. //從epoll刪除fd  
  16. void delFd(int epollfd, int fd)  
  17. {  
  18.     struct epoll_event event;  
  19.     event.data.fd = fd;  
  20.     if( epoll_ctl(epollfd, EPOLL_CTL_DEL, fd, &event) == -1 )  
  21.         err_exit("epoll_ctl_del error");  
  22. }  
  23.   
  24. int main()  
  25. {  
  26.     signal(SIGPIPE, sigHandlerForSigPipe);  
  27.     try  
  28.     {  
  29.         TCPServer server(8001);  
  30.         int listenfd = server.getfd();  
  31.         int epollfd = epoll_create1(EPOLL_CLOEXEC);  
  32.         if (epollfd == -1)  
  33.             err_exit("epoll_create1 error");  
  34.         // 將監聽套接字註冊到epoll  
  35.         addFd(epollfd, listenfd, EPOLLIN, true);  
  36.   
  37.         // 用於保存epoll_wait返回事件數組  
  38.         std::vector<struct epoll_event> events(16);  
  39.         char buf[BUFSIZ];  
  40.         int count = 0;  
  41.         while (true)  
  42.         {  
  43.             // 等待epoll返回  
  44.             int nReady = epoll_wait(epollfd, &*events.begin(), (int)events.size(), -1);  
  45.             if (nReady == -1)  
  46.             {  
  47.                 if (errno == EINTR)  
  48.                     continue;  
  49.                 err_exit("epoll_wait error");  
  50.             }  
  51.             if ((size_t)nReady == events.size())  
  52.                 events.resize(events.size()*2);  
  53.   
  54.             for (int i = 0; i < nReady; ++i)  
  55.             {  
  56.                 // 如果是監聽套接字發送了可讀事件  
  57.                 if (events[i].data.fd == listenfd)  
  58.                 {  
  59.                     int connectfd = accept(listenfd, NULL, NULL);  
  60.                     if (connectfd == -1)  
  61.                         err_exit("accept error");  
  62.                     cout << "accept success..." << endl;  
  63.                     cout << "count = " << ++count << endl;  
  64.                     setUnBlock(connectfd, true);  
  65.                     addFd(epollfd, connectfd, EPOLLIN, true);  
  66.                 }  
  67.                 // 如果是已連接套接字發生了可讀事件  
  68.                 else if (events[i].events & EPOLLIN)  
  69.                 {  
  70.                     int connectfd = events[i].data.fd;  
  71.                     if (connectfd < 0)  
  72.                         continue;  
  73.   
  74.                     memset(buf, 0, sizeof(buf));  
  75.                     int ret = readline(connectfd, buf, sizeof(buf)-1);  
  76.                     if (ret == -1)  
  77.                         err_exit("read-line error");  
  78.                     // 如果對端關閉  
  79.                     else if (ret == 0)  
  80.                     {  
  81.                         cerr << "client connect closed..." << endl;  
  82.                         // 將該套接字同epoll中移除  
  83.                         delFd(epollfd, connectfd);  
  84.                         close(connectfd);  
  85.                         continue;  
  86.                     }  
  87.                     cout << buf;  
  88.                     writen(connectfd, buf, strlen(buf));  
  89.                 }  
  90.             }  
  91.         }  
  92.     }  
  93.     catch (const SocketException &e)  
  94.     {  
  95.         cerr << e.what() << endl;  
  96.         err_exit("TCPServer error");  
  97.     }  
  98. }  

小結-epoll與select、poll的區別

1.相比於select與poll, epoll最大的好處在於它不會隨着監聽fd數目的增長而降低效率。

   因爲內核中select/poll的實現是採用輪詢來處理的, 因此他們檢測就緒實踐的算法時間複雜度是O(N), 因此, 需要輪詢的fd數目越多, 自然耗時越多, 他們的性能呈線性甚至指數的方式下降。

   而epoll的實現是基於事件回調的,如果fd有期望的事件發生就通過回調函數將其加入epoll就緒隊列中,也就是說它只關心“活躍”的fd,與fd數目無關 其算法時間複雜度爲O(1)。

2. 內核空間與用戶空間內存拷貝問題,如何讓內核把 fd消息通知給用戶空間呢?在這個問題上select/poll採取了內存拷貝方法。而epoll採用了內核和用戶空間共享內存的方式。

3. epoll不僅會告訴應用程序有I/0 事件到來,還會告訴應用程序相關的信息,這些信息是應用程序填充的,因此根據這些信息應用程序就能直接定位到事件,而不必遍歷整個fd集合。而select/poll模型,當有 I/O 事件到來時, select/poll通知應用程序有事件到達,而應用程序必須輪詢所有的fd集合,測試每個fd是否有事件發生,並處理事件。

4. 當活動連接比較多的時候, epoll_wait的效率就未必比select/poll高了, 因爲這時候對於epoll 來說一直在調用callback 函數, 回調函數被觸發得過於頻繁, 所以epoll_wait適用於連接數量多, 但活動連接少的情況;

 

ET/LT模式

1、EPOLLLT:完全靠Linux-kernel-epoll驅動,應用程序只需要處理從epoll_wait返回的fds, 這些fds我們認爲它們處於就緒狀態。此時epoll可以認爲是更快速的poll。

2、EPOLLET:此模式下,系統僅僅通知應用程序哪些fds變成了就緒狀態,一旦fd變成就緒狀態,epoll將不再關注這個fd的任何狀態信息(從epoll隊列移除), 直到應用程序通過讀寫操作(非阻塞)觸發EAGAIN狀態,epoll認爲這個fd又變爲空閒狀態,那麼epoll又重新關注這個fd的狀態變化(重新加入epoll隊列)。 隨着epoll_wait的返回,隊列中的fds是在減少的,所以在大併發的系統中,EPOLLET更有優勢,但是對程序員的要求也更高,因爲有可能會出現數據讀取不完整的問題,舉例如下:

   假設現在對方發送了2k的數據,而我們先讀取了1k,然後這時調用了epoll_wait,如果是邊沿觸發ET,那麼這個fd變成就緒狀態就會從epoll 隊列移除,則epoll_wait 會一直阻塞,忽略尚未讀取的1k數據; 而如果是水平觸發LT,那麼epoll_wait 還會檢測到可讀事件而返回,我們可以繼續讀取剩下的1k 數據。

   因此總結來說: LT模式可能觸發的次數更多, 一旦觸發的次數多, 也就意味着效率會下降; 但這樣也不能就說LT模式就比ET模式效率更低, 因爲ET的使用對編程人員提出了更高更精細的要求, 一旦編程人員水平達不到(比如本人), 那ET模式還不如LT模式;


Epoll-Class封裝

   在本部分我們實現一個較爲好用實用的Epoll併發類, 由於實現代碼與使用方式較簡單, 因此就不在此贅述了, 下面我還使用了該類實現了一個基於Epoll的echo-server, 以演示該類的用法;

   由於此處僅爲Epoll類庫的第一個版本, 因此錯誤之處必然會存在, 如果讀者在閱讀的過程中發現了該類庫的BUG, 還望這篇博客的讀者朋友不吝賜教; 而作者也會不斷的更新該類庫(主要更新代碼我會發布到此處), 以處理新的業務需求;


Epoll類設計

  1. class Epoll  
  2. {  
  3. public:  
  4.     Epoll(int flags = EPOLL_CLOEXEC, int noFile = 1024);  
  5.     ~Epoll();  
  6.   
  7.     void addfd(int fd, uint32_t events = EPOLLIN, bool ETorNot = false);  
  8.     void modfd(int fd, uint32_t events = EPOLLIN, bool ETorNot = false);  
  9.     void delfd(int fd);  
  10.     int wait(int timeout = -1);  
  11.     int getEventOccurfd(int eventIndex) const;  
  12.     uint32_t getEvents(int eventIndex) const;  
  13.   
  14. public:  
  15.     bool isValid()  
  16.     {  
  17.         if (m_epollfd == -1)  
  18.             return false;  
  19.         return true;  
  20.     }  
  21.     void close()  
  22.     {  
  23.         if (isValid())  
  24.         {  
  25.             :: close(m_epollfd);  
  26.             m_epollfd = -1;  
  27.         }  
  28.     }  
  29.   
  30. private:  
  31.     std::vector<struct epoll_event> events;  
  32.     int m_epollfd;  
  33.     int fdNumber;  
  34.     int nReady;  
  35. private:  
  36.     struct epoll_event event;  
  37. };  

Epoll類實現

  1. /** epoll_create **/  
  2. Epoll::Epoll(int flags, int noFile) : fdNumber(0), nReady(0)  
  3. {  
  4.     struct rlimit rlim;  
  5.     rlim.rlim_cur = rlim.rlim_max = noFile;  
  6.     if ( ::setrlimit(RLIMIT_NOFILE, &rlim) == -1 )  
  7.         throw EpollException("setrlimit error");  
  8.   
  9.     m_epollfd = ::epoll_create1(flags);  
  10.     if (m_epollfd == -1)  
  11.         throw EpollException("epoll_create1 error");  
  12. }  
  13. Epoll::~Epoll()  
  14. {  
  15.     this -> close();  
  16. }  
  1. /** epoll_ctl **/  
  2. void Epoll::addfd(int fd, uint32_t events, bool ETorNot)  
  3. {  
  4.     bzero(&event, sizeof(event));  
  5.     event.events = events;  
  6.     if (ETorNot)  
  7.         event.events |= EPOLLET;  
  8.     event.data.fd = fd;  
  9.     if( ::epoll_ctl(m_epollfd, EPOLL_CTL_ADD, fd, &event) == -1 )  
  10.         throw EpollException("epoll_ctl_add error");  
  11.     ++ fdNumber;  
  12. }  
  13. void Epoll::modfd(int fd, uint32_t events, bool ETorNot)  
  14. {  
  15.     bzero(&event, sizeof(event));  
  16.     event.events = events;  
  17.     if (ETorNot)  
  18.         event.events |= EPOLLET;  
  19.     event.data.fd = fd;  
  20.     if( ::epoll_ctl(m_epollfd, EPOLL_CTL_MOD, fd, &event) == -1 )  
  21.         throw EpollException("epoll_ctl_mod error");  
  22. }  
  23. void Epoll::delfd(int fd)  
  24. {  
  25.     bzero(&event, sizeof(event));  
  26.     event.data.fd = fd;  
  27.     if( ::epoll_ctl(m_epollfd, EPOLL_CTL_DEL, fd, &event) == -1 )  
  28.         throw EpollException("epoll_ctl_del error");  
  29.     -- fdNumber;  
  30. }  
  1. /** epoll_wait **/  
  2. int Epoll::wait(int timeout)  
  3. {  
  4.     events.resize(fdNumber);  
  5.     while (true)  
  6.     {  
  7.         nReady = epoll_wait(m_epollfd, &*events.begin(), fdNumber, timeout);  
  8.         if (nReady == 0)  
  9.             throw EpollException("epoll_wait timeout");  
  10.         else if (nReady == -1)  
  11.         {  
  12.             if (errno == EINTR)  
  13.                 continue;  
  14.             else  throw EpollException("epoll_wait error");  
  15.         }  
  16.         else  
  17.             return nReady;  
  18.     }  
  19.     return -1;  
  20. }  
  21.   
  22. int Epoll::getEventOccurfd(int eventIndex) const  
  23. {  
  24.     if (eventIndex > nReady)  
  25.         throw EpollException("parameter(s) error");  
  26.     return events[eventIndex].data.fd;  
  27. }  
  28. uint32_t Epoll::getEvents(int eventIndex) const  
  29. {  
  30.     if (eventIndex > nReady)  
  31.         throw EpollException("parameter(s) error");  
  32.     return events[eventIndex].events;  
  33. }  

使用Epoll的echoserver(測試)代碼:

  1. int main()  
  2. {  
  3. signal(SIGPIPE, SIG_IGN);  
  4.     /** 
  5.     將下面的這兩個變量設置成爲放在程序的開頭, 
  6.     只是因爲這樣可以使得業務處理部分的代碼顯 
  7.     得簡潔一些,在實際應用(C++)中,沒必要也不 
  8.     推薦這樣使用 
  9.     **/  
  10.     char buf[BUFSIZ];  
  11.     int clientCount = 0;  
  12.     try  
  13.     {  
  14.         TCPServer server(8001);  
  15.         int listenfd = server.getfd();  
  16.         Epoll epoll;  
  17.         // 將監聽套接字註冊到epoll  
  18.         epoll.addfd(server.getfd(), EPOLLIN, true);  
  19.         while (true)  
  20.         {  
  21.             int nReady = epoll.wait();  
  22.             for (int i = 0; i < nReady; ++i)  
  23.                 // 如果是監聽套接字發生了可讀事件  
  24.                 if (epoll.getEventOccurfd(i) == listenfd)  
  25.                 {  
  26.                     int connectfd = accept(listenfd, NULL, NULL);  
  27.                     if (connectfd == -1)  
  28.                         err_exit("accept error");  
  29.                     cout << "accept success..." << endl;  
  30.                     cout << "clientCount = " << ++ clientCount << endl;  
  31.                     setUnBlock(connectfd, true);  
  32.                     epoll.addfd(connectfd, EPOLLIN, true);  
  33.                 }  
  34.                 else if (epoll.getEvents(i) & EPOLLIN)  
  35.                 {  
  36.                     TCPClient *client = new TCPClient(epoll.getEventOccurfd(i));  
  37.                     memset(buf, 0, sizeof(buf));  
  38.                     if (client->read(buf, sizeof(buf)) == 0)  
  39.                     {  
  40.                         cerr << "client connect closed..." << endl;  
  41.                         // 將該套接字從epoll中移除  
  42.                         epoll.delfd(client->getfd());  
  43.                         delete client;  
  44.                         continue;  
  45.                     }  
  46.                     cout << buf;  
  47.                     client->write(buf);  
  48.                 }  
  49.         }  
  50.     }  
  51.     catch (const SocketException &e)  
  52.     {  
  53.         cerr << e.what() << endl;  
  54.         err_exit("TCPServer error");  
  55.     }  
  56.     catch (const EpollException &e)  
  57.     {  
  58.         cerr << e.what() << endl;  
  59.         err_exit("Epoll error");  
  60.     }  
  61. }  

完整源代碼請參照:

http://download.csdn.net/detail/hanqing280441589/8492911


發佈了52 篇原創文章 · 獲贊 10 · 訪問量 9萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章