Socket编程实践(11) --epoll原理与封装
常用模型的特點
? ? Linux?下設計并發(fā)網(wǎng)絡程序,有典型的Apache模型(Process?Per?Connection,PPC),?TPC(Thread?Per?Connection)模型,以及?select/polL模型和epoll模型。
?
1?、PPC/TPC?模型
? ??這兩種模型思想類似,就是讓每一個到來的連接一邊自己做事去,別再來煩我(詳見本系列博客).只是?PPC?是為它開了一個進程,而?TPC?開了一個線程。可是別煩我是有代價的,它要時間和空間啊,連接多了之后,那么多的進程/線程切換,這開銷就上來了;因此這類模型能接受的最大連接數(shù)都不會高,一般在幾百個左右。
2?、select?模型
? ??1)?最大并發(fā)數(shù)限制,因為一個進程所打開的?FD?(文件描述符)是有限制的,由?FD_SETSIZE?設置,默認值是?1024,因此?Select?模型的最大并發(fā)數(shù)就被相應限制了。自己改改這個?FD_SETSIZE??想法雖好,可是先看看下面吧?…
? ??2)?效率問題,?select?每次調(diào)用都會線性掃描全部的?FD?集合,這樣效率就會呈現(xiàn)線性下降,把?FD_SETSIZE?改大的后果就是,大家都慢慢來,什么?都超時了??!!
? ??3)?內(nèi)核/用戶空間內(nèi)存拷貝問題,如何讓內(nèi)核把?FD?消息通知給用戶空間呢?在這個問題上?select?采取了內(nèi)存拷貝方法。
3、?poll?模型
? ??基本上效率和?select?是相同的,?select?缺點的?2?和?3?它都沒有改掉。
?
Epoll?的提升
? ??1.?Epoll?沒有最大并發(fā)連接的限制,上限是最大可以打開文件的數(shù)目,這個數(shù)字一般遠大于?2048,?一般來說這個數(shù)目和系統(tǒng)內(nèi)存關系很大?,具體數(shù)目可以?cat?/proc/sys/fs/file-max[599534]?察看。
? ??2.?效率提升,?Epoll最大的優(yōu)點就在于它只管你“活躍”的連接?,而跟連接總數(shù)無關,因此在實際的網(wǎng)絡環(huán)境中,?Epoll的效率就會遠遠高于?select?和?poll?。
? ??3.?內(nèi)存拷貝,?Epoll?在這點上使用了“共享內(nèi)存(詳見本系列其他博客)”,這個內(nèi)存拷貝也省略了。
epoll的使用
epoll的接口非常簡單,一共就3/4個函數(shù):
int epoll_create(int size); int epoll_create1(int flags); 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);? ?1.?對于epoll_create1?的flag參數(shù):?可以設置為0?或EPOLL_CLOEXEC,為0時函數(shù)表現(xiàn)與epoll_create一致,?EPOLL_CLOEXEC標志與open?時的O_CLOEXEC?標志類似,即進程被替換時會關閉打開的文件描述符(需要注意的是,epoll_create與epoll_create1當創(chuàng)建好epoll句柄后,它就是會占用一個fd值,在linux下如果查看/proc/<pid>/fd/,是能夠看到這個fd的,所以在使用完epoll后,必須調(diào)用close()關閉,否則可能導致fd被耗盡)。
? ?2.?對于epoll_ctl,?op參數(shù)表示動作,用三個宏來表示:
EPOLL_CTL_ADD | 注冊新的fd到epfd中 |
EPOLL_CTL_DEL | 從epfd中刪除一個fd |
EPOLL_CTL_MOD | 修改已經(jīng)注冊的fd的監(jiān)聽事件 |
? ?3.?對于epoll_wait:
? ?? ?events:結(jié)構(gòu)體指針,?一般是一個數(shù)組
? ?? ?maxevents:事件的最大個數(shù),?或者說是數(shù)組的大小
? ?? ?timeout:超時時間,?含義與poll的timeout參數(shù)相同,設為-1表示永不超時;
? ?4.?epoll_event結(jié)構(gòu)體
struct epoll_event {uint32_t events; /* Epoll events */epoll_data_t data; /* User data variable */ }; typedef union epoll_data {void *ptr;int fd;uint32_t u32;uint64_t u64; } epoll_data_t;一般data?共同體我們設置其成員fd即可,也就是epoll_ctl?函數(shù)的第三個參數(shù)。
events集合 | |
EPOLLIN | 表示對應的文件描述符可以讀(包括對端SOCKET正常關閉) |
EPOLLOUT | 表示對應的文件描述符可以寫 |
EPOLLPRI | 表示對應的文件描述符有緊急的數(shù)據(jù)可讀(這里應該表示有帶外數(shù)據(jù)到來) |
EPOLLERR | 表示對應的文件描述符發(fā)生錯誤 |
EPOLLHUP | 表示對應的文件描述符被掛斷 |
EPOLLET | 將EPOLL設為邊緣觸發(fā)(Edge?Triggered)模式,這是相對于水平觸發(fā)(Level?Triggered)來說的 |
EPOLLONESHOT | 只監(jiān)聽一次事件,當監(jiān)聽完這次事件之后,如果還需要繼續(xù)監(jiān)聽這個socket的話,需要再次把這個socket加入到EPOLL隊列里 |
小結(jié)-epoll與select、poll的區(qū)別
1.相比于select與poll,?epoll最大的好處在于它不會隨著監(jiān)聽fd數(shù)目的增長而降低效率。
? ?因為內(nèi)核中select/poll的實現(xiàn)是采用輪詢來處理的,?因此他們檢測就緒實踐的算法時間復雜度是O(N),?因此,?需要輪詢的fd數(shù)目越多,?自然耗時越多,?他們的性能呈線性甚至指數(shù)的方式下降。
? ?而epoll的實現(xiàn)是基于事件回調(diào)的,如果fd有期望的事件發(fā)生就通過回調(diào)函數(shù)將其加入epoll就緒隊列中,也就是說它只關心“活躍”的fd,與fd數(shù)目無關?其算法時間復雜度為O(1)。
2.?內(nèi)核空間與用戶空間內(nèi)存拷貝問題,如何讓內(nèi)核把?fd消息通知給用戶空間呢?在這個問題上select/poll采取了內(nèi)存拷貝方法。而epoll采用了內(nèi)核和用戶空間共享內(nèi)存的方式。
3.?epoll不僅會告訴應用程序有I/0?事件到來,還會告訴應用程序相關的信息,這些信息是應用程序填充的,因此根據(jù)這些信息應用程序就能直接定位到事件,而不必遍歷整個fd集合。而select/poll模型,當有?I/O?事件到來時,?select/poll通知應用程序有事件到達,而應用程序必須輪詢所有的fd集合,測試每個fd是否有事件發(fā)生,并處理事件。
4.?當活動連接比較多的時候,?epoll_wait的效率就未必比select/poll高了,?因為這時候?qū)τ趀poll?來說一直在調(diào)用callback?函數(shù),?回調(diào)函數(shù)被觸發(fā)得過于頻繁,?所以epoll_wait適用于連接數(shù)量多,?但活動連接少的情況;
?
ET/LT模式
1、EPOLLLT:完全靠Linux-kernel-epoll驅(qū)動,應用程序只需要處理從epoll_wait返回的fds,?這些fds我們認為它們處于就緒狀態(tài)。此時epoll可以認為是更快速的poll。
2、EPOLLET:此模式下,系統(tǒng)僅僅通知應用程序哪些fds變成了就緒狀態(tài),一旦fd變成就緒狀態(tài),epoll將不再關注這個fd的任何狀態(tài)信息(從epoll隊列移除),?直到應用程序通過讀寫操作(非阻塞)觸發(fā)EAGAIN狀態(tài),epoll認為這個fd又變?yōu)榭臻e狀態(tài),那么epoll又重新關注這個fd的狀態(tài)變化(重新加入epoll隊列)。?隨著epoll_wait的返回,隊列中的fds是在減少的,所以在大并發(fā)的系統(tǒng)中,EPOLLET更有優(yōu)勢,但是對程序員的要求也更高,因為有可能會出現(xiàn)數(shù)據(jù)讀取不完整的問題,舉例如下:
? ?假設現(xiàn)在對方發(fā)送了2k的數(shù)據(jù),而我們先讀取了1k,然后這時調(diào)用了epoll_wait,如果是邊沿觸發(fā)ET,那么這個fd變成就緒狀態(tài)就會從epoll?隊列移除,則epoll_wait?會一直阻塞,忽略尚未讀取的1k數(shù)據(jù);?而如果是水平觸發(fā)LT,那么epoll_wait?還會檢測到可讀事件而返回,我們可以繼續(xù)讀取剩下的1k?數(shù)據(jù)。
? ?因此總結(jié)來說:?LT模式可能觸發(fā)的次數(shù)更多,?一旦觸發(fā)的次數(shù)多,?也就意味著效率會下降;?但這樣也不能就說LT模式就比ET模式效率更低,?因為ET的使用對編程人員提出了更高更精細的要求,?一旦編程人員水平達不到(比如本人),?那ET模式還不如LT模式;
Epoll-Class封裝
? ?在本部分我們實現(xiàn)一個較為好用實用的Epoll并發(fā)類,?由于實現(xiàn)代碼與使用方式較簡單,?因此就不在此贅述了,?下面我還使用了該類實現(xiàn)了一個基于Epoll的echo-server,?以演示該類的用法;
? ?由于此處僅為Epoll類庫的第一個版本,?因此錯誤之處必然會存在,?如果讀者在閱讀的過程中發(fā)現(xiàn)了該類庫的BUG,?還望這篇博客的讀者朋友不吝賜教;?而作者也會不斷的更新該類庫(主要更新代碼我會發(fā)布到此處),?以處理新的業(yè)務需求;
Epoll類設計
class Epoll { public:Epoll(int flags = EPOLL_CLOEXEC, int noFile = 1024);~Epoll();void addfd(int fd, uint32_t events = EPOLLIN, bool ETorNot = false);void modfd(int fd, uint32_t events = EPOLLIN, bool ETorNot = false);void delfd(int fd);int wait(int timeout = -1);int getEventOccurfd(int eventIndex) const;uint32_t getEvents(int eventIndex) const;public:bool isValid(){if (m_epollfd == -1)return false;return true;}void close(){if (isValid()){:: close(m_epollfd);m_epollfd = -1;}}private:std::vector<struct epoll_event> events;int m_epollfd;int fdNumber;int nReady; private:struct epoll_event event; };Epoll類實現(xiàn)
/** epoll_create **/ Epoll::Epoll(int flags, int noFile) : fdNumber(0), nReady(0) {struct rlimit rlim;rlim.rlim_cur = rlim.rlim_max = noFile;if ( ::setrlimit(RLIMIT_NOFILE, &rlim) == -1 )throw EpollException("setrlimit error");m_epollfd = ::epoll_create1(flags);if (m_epollfd == -1)throw EpollException("epoll_create1 error"); } Epoll::~Epoll() {this -> close(); } /** epoll_ctl **/ void Epoll::addfd(int fd, uint32_t events, bool ETorNot) {bzero(&event, sizeof(event));event.events = events;if (ETorNot)event.events |= EPOLLET;event.data.fd = fd;if( ::epoll_ctl(m_epollfd, EPOLL_CTL_ADD, fd, &event) == -1 )throw EpollException("epoll_ctl_add error");++ fdNumber; } void Epoll::modfd(int fd, uint32_t events, bool ETorNot) {bzero(&event, sizeof(event));event.events = events;if (ETorNot)event.events |= EPOLLET;event.data.fd = fd;if( ::epoll_ctl(m_epollfd, EPOLL_CTL_MOD, fd, &event) == -1 )throw EpollException("epoll_ctl_mod error"); } void Epoll::delfd(int fd) {bzero(&event, sizeof(event));event.data.fd = fd;if( ::epoll_ctl(m_epollfd, EPOLL_CTL_DEL, fd, &event) == -1 )throw EpollException("epoll_ctl_del error");-- fdNumber; } /** epoll_wait **/ int Epoll::wait(int timeout) {events.resize(fdNumber);while (true){nReady = epoll_wait(m_epollfd, &*events.begin(), fdNumber, timeout);if (nReady == 0)throw EpollException("epoll_wait timeout");else if (nReady == -1){if (errno == EINTR)continue;else throw EpollException("epoll_wait error");}elsereturn nReady;}return -1; }int Epoll::getEventOccurfd(int eventIndex) const {if (eventIndex > nReady)throw EpollException("parameter(s) error");return events[eventIndex].data.fd; } uint32_t Epoll::getEvents(int eventIndex) const {if (eventIndex > nReady)throw EpollException("parameter(s) error");return events[eventIndex].events; }使用Epoll的echoserver(測試)代碼:
int main() { signal(SIGPIPE, SIG_IGN);/**將下面的這兩個變量設置成為放在程序的開頭,只是因為這樣可以使得業(yè)務處理部分的代碼顯得簡潔一些,在實際應用(C++)中,沒必要也不推薦這樣使用**/char buf[BUFSIZ];int clientCount = 0;try{TCPServer server(8001);int listenfd = server.getfd();Epoll epoll;// 將監(jiān)聽套接字注冊到epollepoll.addfd(server.getfd(), EPOLLIN, true);while (true){int nReady = epoll.wait();for (int i = 0; i < nReady; ++i)// 如果是監(jiān)聽套接字發(fā)生了可讀事件if (epoll.getEventOccurfd(i) == listenfd){int connectfd = accept(listenfd, NULL, NULL);if (connectfd == -1)err_exit("accept error");cout << "accept success..." << endl;cout << "clientCount = " << ++ clientCount << endl;setUnBlock(connectfd, true);epoll.addfd(connectfd, EPOLLIN, true);}else if (epoll.getEvents(i) & EPOLLIN){TCPClient *client = new TCPClient(epoll.getEventOccurfd(i));memset(buf, 0, sizeof(buf));if (client->read(buf, sizeof(buf)) == 0){cerr << "client connect closed..." << endl;// 將該套接字從epoll中移除epoll.delfd(client->getfd());delete client;continue;}cout << buf;client->write(buf);}}}catch (const SocketException &e){cerr << e.what() << endl;err_exit("TCPServer error");}catch (const EpollException &e){cerr << e.what() << endl;err_exit("Epoll error");} }完整源代碼請參照:
http://download.csdn.net/detail/hanqing280441589/8492911
總結(jié)
以上是生活随笔為你收集整理的Socket编程实践(11) --epoll原理与封装的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Oracle 11g 通过创建物化视图实
- 下一篇: Windows 软件授权管理工具检验Wi