一步一步來epoll

一切都從最基本開始。

 

網絡編程中客戶端連服務端的經典代碼:

int sockfd = socket(AF_INET, SOCK_STREAM, 0);

 

struct sockaddr_in svraddr;

svraddr.sin_family              = AF_INET;

svraddr.sin_port                 = htons(iPort);

svraddr.sin_addr.s_addr             = inet_addr(Ip);

 

connect(sockfd, (struct sockaddr*)&svraddr, sizeof(svraddr);

char req[1024]="test";

sendto(acceptfd, req, strlen(req)+1, 0, (struct sockaddr*)&svraddr, sizeof(svraddr));

網絡編程中服務端接收客戶端的經典代碼:

int listenfd = socket(AF_INET, SOCK_STREAM, 0);

 

struct sockaddr_in svraddr;

svraddr.sin_family              = AF_INET;

svraddr.sin_port                 = htons(iPort);

svraddr.sin_addr.s_addr             = INADDR_ANY;

 

bind(listenfd, (struct sockaddr*)&svraddr, sizeof(svraddr)

listen(listenfd, SOMAXCONN);

 

struct sockaddr_in cliaddr;

socklen_t clilen = 0;

 

while(1)

{                

         int acceptfd = accept(listenfd, (struct sockaddr*)&cliaddr, & clilen);

 

         char rec[1024];

         memset(sRec, 0, 1024);

         int byte_read = recv(acceptfd, rec, 1024, 0);

         if (byte_read > 0)

         {

                   sendto(acceptfd, rec, byte_read, 0, (struct sockaddr*)&cliaddr, sizeof(cliaddr));

         }

        

         close(acceptfd);

}      

在單進程單請求的處理中,這幾個函數完美的完成了一次客戶端請求服務端發包的過程。Server端就是一個處理機,當來一個請求時,server捕獲到這個請求,然後處理。

當服務端accept時,會阻塞當前進程,直到客戶端發送請求過來。

 

由於服務端是處理單請求,當多個請求同時到來時,後面的請求會被阻塞住。比如:

服務端S先執行,阻塞於accept

客戶端c1連接S,但先不發送數據,則S會阻塞在recv這個io上面

此時,客戶端c2連接S,即使發送數據過去,S仍然無法響應

c1發送數據給S後,S處理完畢,會acceptc2,處理c2的請求

 

 

這種模式的缺點太明顯了,無法做到處理多個fd。那麼有沒有這麼一種設計,當io請求到來的時候,告訴服務端有請求去處理呢?

這就是i/o多路複用了 

 

 

Select下的多路複用,仍然會阻塞,但是不會像Accept那樣阻塞在i/o上面,而是會阻塞到select這個系統調用上面。

如此這般,當有請求(新客戶端連接請求,已經連接的套接口發送數據請求等)到來時,select會捕捉到這些請求。

Select的基本流程是這樣的:

阻塞於包含一堆fd的集合之上,這些fds可能是套接口,文件等

Fd中有可寫,可讀,或異常,或超時,select返回。

進入邏輯處理階段

Select繼續阻塞,循環處理。

 

經過改造之後的server端代碼

char line[MAXLINE];

socklen_t clilen;

//聲明epoll_event結構體的變量,ev用於註冊事件,數組用於回傳要處理的事件

struct epoll_event ev,events[20];

//生成用於處理acceptepoll專用的文件描述符

epfd=epoll_create(256);

struct sockaddr_in clientaddr;

struct sockaddr_in serveraddr;

listenfd = socket(AF_INET, SOCK_STREAM, 0);

//socket設置爲非阻塞方式

//setnonblocking(listenfd);

//設置與要處理的事件相關的文件描述符

ev.data.fd=listenfd;

//設置要處理的事件類型

ev.events=EPOLLIN|EPOLLET;

//ev.events=EPOLLIN;

//註冊epoll事件

epoll_ctl(epfd,EPOLL_CTL_ADD,listenfd,&ev);

bzero(&serveraddr, sizeof(serveraddr));

serveraddr.sin_family = AF_INET;

serveraddr.sin_addr.s_addr        = inet_addr(Ip);

serveraddr.sin_port=htons(SERV_PORT);

bind(listenfd,(sockaddr *)&serveraddr, sizeof(serveraddr));

listen(listenfd, LISTENQ);

maxi = 0;

for ( ; ; ) {

         //等待epoll事件的發生

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

         //處理所發生的所有事件    

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

         {

                   if(events[i].data.fd==listenfd)

                   {

                            connfd = accept(listenfd,(sockaddr *)&clientaddr, &clilen);

                            //setnonblocking(connfd);

                            //設置用於讀操作的文件描述符

                            ev.data.fd=connfd;

                            //設置用於注測的讀操作事件

                            ev.events=EPOLLIN|EPOLLET;

                            //ev.events=EPOLLIN;

                            //註冊ev

                            epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev);

                   }

                   else if(events[i].events&EPOLLIN)

                   {

                            if ( (sockfd = events[i].data.fd) < 0)

                                     continue;

                            if ( (n = read(sockfd, line, MAXLINE)) < 0) {

                                     if (errno == ECONNRESET) {

                                               close(sockfd);

                                               events[i].data.fd = -1;

                                     }

                            } else if (n == 0) {

                                     close(sockfd);

                                     events[i].data.fd = -1;

                            }

                            line[n] = '/0';

                            cout << "read " << line << endl;

                            //設置用於寫操作的文件描述符

                            ev.data.fd=sockfd;

                            //設置用於注測的寫操作事件

                            ev.events=EPOLLOUT|EPOLLET;

                            //修改sockfd上要處理的事件爲EPOLLOUT

                            epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);

                   }

                   else if(events[i].events&EPOLLOUT)

                   {  

                            sockfd = events[i].data.fd;

                            write(sockfd, line, n);

                            //設置用於讀操作的文件描述符

                            ev.data.fd=sockfd;

                            //設置用於注測的讀操作事件

                            ev.events=EPOLLIN|EPOLLET;

                            //修改sockfd上要處理的事件爲EPOLIN

                            epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);

                   }

         }

}

 

 

        

經過select改造後的server可以同時接受10個請求而不會阻塞,效率提升了很多。

Select返回可能會小於0,這就是異常,其中包括被信號打斷,此時error= EINTR,我們需要特殊處理這種情況。

Select超時情況下會返回爲0.

正常情況下select會返回準備就緒的fd。如果是listen的套接字返回,則程序需要去accept請求,如果是accept的套接字返回,則程序需要去讀或者寫該套接字。

 

Select有個缺點,就是他僅能知道集合中有某個fd觸發,卻無法知道是哪一個,只能挨個去找。

這樣就誕生了epollEpoll會記錄下某個fd的信息,這樣當fd觸發的時候,epoll機制可以很快的找到相應的fd,進行處理。

 

Epoll技術是在linux2.6新加的,在頭文件sys/epoll.h裏面定義了其中的全部數據結構和操作。

主要使用三個接口:

int epoll_create(int size);

創建一個epoll句柄

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

將某個fd操作(op)到該句柄上

int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);

等待該epoll句柄。

 

Epoll設計下server代碼就變成了下面這個樣子了。

int listenfd = socket(AF_INET, SOCK_STREAM, 0);

int epollfd = epoll_create(20);

struct epoll_event ev, events[20];

//setnonblocking(listenfd);

ev.events = EPOLLIN | EPOLLET;

ev.data.fd = listenfd;

epoll_ctl(epollfd, EPOLL_CTL_ADD, listenfd, &ev);

 

struct sockaddr_in svraddr;

svraddr.sin_family = AF_INET;

svraddr.sin_port = htons(atoi(argv[1]));

svraddr.sin_addr.s_addr = INADDR_ANY;

bind(listenfd, (struct sockaddr *)&svraddr, sizeof(svraddr));

listen(listenfd, SOMAXCONN);

int clientfd[10];

memset(clientfd, -1, sizeof(clientfd));

struct sockaddr_in cliaddr;

socklen_t clilen;

char recv[1024];

while(1)

{

         int nfds = epoll_wait(epollfd, events, 20, 500);

         for(int n = 0; n < nfds; ++n)

         {

                   if(events[n].data.fd == listenfd)

                   {

                            int iclifd = accept(listenfd, (struct sockaddr*)&cliaddr, &clilen);

                            for(int i=0; i<10; i++)

                            {

                                     if (clientfd[i] == -1)

                                     {

                                               clientfd[i] = iclifd;

                                               epoll_ctl(epollfd, EPOLL_CTL_ADD, iclifd, &ev);

                                               break;

                                     }

                            }

                   }

                   for(int i=0; i<=10; i++)

                   {

                            if(clientfd[i]>=0 && events[n].data.fd==clientfd[i])

                            {

                                     int n = read(clientfd[i], recv, 1024);

                                     if (n<=0)

                                     {

                                               close(clientfd[i]);

                                               epoll_ctl(epollfd, EPOLL_CTL_DEL, iclifd, &ev);

                                               clientfd[i] = -1;

                                     }

                                     else

                                     {

                                               write(clientfd[i], recv, n);

                                     }

                            }

                   }

         }

}

 

Epoll的重要數據結構:

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 */

}; 

結構體epoll_event 被用於註冊所感興趣的事件和回傳所發生待處理的事件,其中epoll_data 聯合體用來保存觸發事件的某個文件描述符相關的數據。

例如一個client連接到服務器,服務器通過調用accept函數可以得到於這個client對應的socket文件描述符,可以把這文件描述符賦給epoll_datafd字段以便後面的讀寫操作在這個文件描述符上進行。這個可以通過epoll_ctrlEPOLL_CTL_ADD 選項來實現,然後waitepoll句柄,當觸發特定的事件時進行特殊的處理。

epoll_event 結構體的events字段是表示感興趣的事件和被觸發的事件。可能的取值爲:

EPOLLIN :表示對應的文件描述符可以讀;

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

EPOLLPRI:表示對應的文件描述符有緊急的數據可讀

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

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

EPOLLET:表示對應的文件描述符有事件發生;

 

ETLT模式的區別:

LT(水平觸發)模式是缺省的工作模式,同時支持blocknoblock方式,這種模式下面編程會容易一些,因爲只要沒有處理完畢,內核不不斷的通知。LT模式下的epollpoll/select差不多,只是理論速度會快一點而已。

 

ET(邊沿觸發)模式是epoll的高速模式,只支持noblock方式。這種情況下,描述符由未就緒到就緒的變化內核會通知,直到程序做了某些操作使得該描述符不再爲就緒狀態。

比如從一個描述符讀取數據,一次沒有讀完,LT模式下面內核會通知繼續去讀,但在ET模式下需要程序自己去控制,直到讀完位置。一次讀取的內容如果與自己想讀取的數量一致,這個時候緩衝區很有可能有未讀的數據存在。

同樣,往fd寫數據的時候,可能發送端的速度高於接收端的速度,這樣就很容易把緩衝區填滿(返回EAGAIN錯誤),這個時候就需要等一下子,慢慢再傳纔可以了。

 

ET模式下面客戶端關閉某個fd的時候,fd狀態改變,內核會告訴服務端該fd有數據可讀,但這個時候的讀顯然已經違背了服務端的本意,時間上也差遠了。

 

 

多路複用的目的,就是爲了提高服務端處理高併發請求的能力,採用懶人辦法,就是多進程處理,父進程負責接收信號,然後fork子進程處理邏輯,這樣在邏輯梳理上就清晰多了。

每種方法都有自己的利弊,就看我們更看重哪一點了。

 

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