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