網絡通信基礎重難點解析 12 :Linux epoll 模型

Linux epoll 模型

綜合 selectpoll 的一些優缺點,Linux 從內核 2.6 版本開始引入了更高效的 epoll 模型,本節我們來詳細介紹 epoll 模型。

要想使用 epoll 模型,必須先需要創建一個 epollfd,這需要使用 epoll_create 函數去創建:

#include <sys/epoll.h>

int epoll_create(int size);

參數 size 從 Linux 2.6.8 以後就不再使用,但是必須設置一個大於 0 的值。epoll_create 函數調用成功返回一個非負值的 epollfd,調用失敗返回 -1。

有了 epollfd 之後,我們需要將我們需要檢測事件的其他 fd 綁定到這個 epollfd 上,或者修改一個已經綁定上去的 fd 的事件類型,或者在不需要時將 fd 從 epollfd 上解綁,這都可以使用 epoll_ctl 函數:

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

參數說明:

  • 參數 epfd 即上文提到的 epollfd;

  • 參數 op,操作類型,取值有 EPOLL_CTL_ADDEPOLL_CTL_MODEPOLL_CTL_DEL,分別表示向 epollfd 上添加、修改和移除一個其他 fd,當取值是 EPOLL_CTL_DEL,第四個參數 event 忽略不計,可以設置爲 NULL;

  • 參數 fd,即需要被操作的 fd;

  • 參數 event,這是一個 epoll_event 結構體的地址,epoll_event 結構體定義如下:

    struct epoll_event
    {
        uint32_t     events;      /* 需要檢測的 fd 事件,取值與 poll 函數一樣 */
        epoll_data_t data;        /* 用戶自定義數據 */
    };
    

    epoll_event 結構體的 data 字段的類型是 epoll_data_t,我們可以利用這個字段設置一個自己的自定義數據,它本質上是一個 Union 對象,在 64 位操作系統中其大小是 8 字節,其定義如下:

    typedef union epoll_data
    {
        void*		 ptr;
        int          fd;
        uint32_t     u32;
        uint64_t     u64;
    } epoll_data_t;
    
  • 函數返回值epoll_ctl 調用成功返回 0,調用失敗返回 -1,你可以通過 errno 錯誤碼獲取具體的錯誤原因。

創建了 epollfd,設置好某個 fd 上需要檢測事件並將該 fd 綁定到 epollfd 上去後,我們就可以調用 epoll_wait 檢測事件了,epoll_wait 函數簽名如下:

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

參數的形式和 poll 函數很類似,參數 events 是一個 epoll_event 結構數組的首地址,這是一個輸出參數,函數調用成功後,events 中存放的是與就緒事件相關 epoll_event 結構體數組;參數 maxevents 是數組元素的個數;timeout 是超時時間,單位是毫秒,如果設置爲 0,epoll_wait 會立即返回。

epoll_wait 調用成功會返回有事件的 fd 數目;如果返回 0 表示超時;調用失敗返回 -1。

epoll_wait 使用示例如下:

while (true)
{
	epoll_event epoll_events[1024];
	int n = epoll_wait(epollfd, epoll_events, 1024, 1000);
	if (n < 0)
	{
		//被信號中斷
		if (errno == EINTR)
			continue;
		
		//出錯,退出
		break;
	}
	else if (n == 0)
	{
		//超時,繼續
		continue;
	}

	for (size_t i = 0; i < n; ++i)
	{
		// 處理可讀事件
		if (epoll_events[i].events & POLLIN)
		{
		}
		// 處理可寫事件
		else if (epoll_events[i].events & POLLOUT)
		{
		}
		//處理出錯事件
		else if (epoll_events[i].events & POLLERR)
		{
		}
	}
}

epoll_wait 與 poll 的區別

通過前面介紹 pollepoll_wait 函數的介紹,我們可以發現:

epoll_wait 函數調用完之後,我們可以直接在 event 參數中拿到所有有事件就緒的 fd,直接處理即可(event 參數僅僅是個出參);而 poll 函數的事件集合調用前後數量都未改變,只不過調用前我們通過 pollfd 結構體的 events 字段設置待檢測事件,調用後我們需要通過 pollfd 結構體的 revents 字段去檢測就緒的事件( 參數 fds 既是入參也是出參)。

舉個生活中的例子,某人不斷給你一些蘋果,這些蘋果有生有熟,調用 epoll_wait 相當於:

1. 你把蘋果挨個投入到 epoll 機器中(調用 epoll_ctl);
2. 調用 epoll_wait 加工,你直接通過另外一個袋子就能拿到所有熟蘋果。

調用 poll 相當於:

1. 把收到的蘋果裝入一個袋子裏面然後調用 poll 加工;
2. 調用結束後,拿到原來的袋子,袋子中還是原來那麼多蘋果,只不過熟蘋果被貼上了標籤紙,你還是需要挨個去查看標籤紙挑選熟蘋果。 

當然,這並不意味着,poll 函數的效率不如 epoll_wait,一般在 fd 數量比較多,但某段時間內,就緒事件 fd 數量較少的情況下,epoll_wait 纔會體現出它的優勢,也就是說 socket 連接數量較大時而活躍連接較少時 epoll 模型更高效。

LT 模式和 ET 模式

與 poll 的事件宏相比,epoll 新增了一個事件宏 EPOLLET,這就是所謂的邊緣觸發模式Edge Trigger,ET),而默認的模式我們稱爲 水平觸發模式Level Trigger,LT)。這兩種模式的區別在於:

  • 對於水平觸發模式,一個事件只要有,就會一直觸發;
  • 對於邊緣觸發模式,只有一個事件從無到有才會觸發。

這兩個詞彙來自電學術語,你可以將 fd 上有數據認爲是高電平,沒有數據認爲是低電平,將 fd 可寫認爲是高電平,fd 不可寫認爲是低電平。那麼水平模式的觸發條件是狀態處於高電平,而邊緣模式是狀態改爲高電平,即:

水平模式的觸發條件

1. 低電平 => 高電平
2. 高電平 => 高電平

邊緣模式的觸發條件

1. 低電平 => 高電平

說的有點抽象,以 socket 的讀事件爲例,對於水平模式,只要 socket 上有未讀完的數據,就會一直產生 POLLIN 事件;而對於邊緣模式,socket 上第一次有數據會觸發一次,後續 socket 上存在數據也不會再觸發,除非把數據讀完後,再次產生數據纔會繼續觸發。對於 socket 寫事件,如果 socket 的 TCP 窗口一直不飽和,會一直觸發 POLLOUT 事件;而對於邊緣模式,只會觸發一次,除非 TCP 窗口由不飽和變成飽和再一次變成不飽和,纔會再次觸發 POLLOUT 事件。

socket 可讀事件水平模式觸發條件:

1. socket上無數據 => socket上有數據
2. socket上有數據 => socket上有數據

socket 可讀事件邊緣模式觸發條件:

1. socket上無數據 => socket上有數據

socket 可寫事件水平模式觸發條件:

1. socket可寫   => socket可寫
2. socket不可寫 => socket可寫

socket 可寫事件邊緣模式觸發條件:

1. socket不可寫 => socket可寫

也就是說,如果對於一個非阻塞 socket,如果使用 epoll 邊緣模式去檢測數據是否可讀,觸發可讀事件以後,一定要一次性把 socket 上的數據收取乾淨才行,也就是一定要循環調用 recv 函數直到 recv 出錯,錯誤碼是EWOULDBLOCKEAGAIN 一樣);如果使用水平模式,則不用,你可以根據業務一次性收取固定的字節數,或者收完爲止。邊緣模式下收取數據的代碼示例如下:

bool TcpSession::RecvEtMode()
{
   //每次只收取256個字節
   char buff[256];
   while (true)
   {       
       int nRecv = ::recv(clientfd_, buff, 256, 0);
       if (nRecv == -1)
       {
           if (errno == EWOULDBLOCK)
               return true;
           else if (errno == EINTR)
               continue;

           return false;
       }
       //對端關閉了socket
       else if (nRecv == 0)
           return false;

       inputBuffer_.add(buff, (size_t)nRecv);
   }

   return true;
}

最後,我們來看一個 epoll 模型的完整例子:

/**
 * 演示 epoll 通信模型,epoll_server.cpp
 * zhangyl 2019.03.16
 */
#include <sys/types.h> 
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/epoll.h>
#include <poll.h>
#include <iostream>
#include <string.h>
#include <vector>
#include <errno.h>

int main(int argc, char* argv[])
{
    //創建一個偵聽socket
    int listenfd = socket(AF_INET, SOCK_STREAM, 0);
    if (listenfd == -1)
    {
        std::cout << "create listen socket error." << std::endl;
        return -1;
    }
	
	//將偵聽socket設置爲非阻塞的
	int oldSocketFlag = fcntl(listenfd, F_GETFL, 0);
	int newSocketFlag = oldSocketFlag | O_NONBLOCK;
	if (fcntl(listenfd, F_SETFL,  newSocketFlag) == -1)
	{
		close(listenfd);
		std::cout << "set listenfd to nonblock error." << std::endl;
		return -1;
	}

    //初始化服務器地址
    struct sockaddr_in bindaddr;
    bindaddr.sin_family = AF_INET;
    bindaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    bindaddr.sin_port = htons(3000);
    if (bind(listenfd, (struct sockaddr *)&bindaddr, sizeof(bindaddr)) == -1)
    {
        std::cout << "bind listen socket error." << std::endl;
		close(listenfd);
        return -1;
    }

	//啓動偵聽
    if (listen(listenfd, SOMAXCONN) == -1)
    {
        std::cout << "listen error." << std::endl;
		close(listenfd);
        return -1;
    }
	
	//複用地址和端口號
	int on = 1;
	setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, (char *)&on, sizeof(on));
	setsockopt(listenfd, SOL_SOCKET, SO_REUSEPORT, (char *)&on, sizeof(on));
	
	//創建epollfd
	int epollfd = epoll_create(1);
    if (epollfd == -1)
    {
        std::cout << "create epollfd error." << std::endl;
		close(listenfd);
        return -1;
    }
	
	epoll_event listen_fd_event;
	listen_fd_event.events = POLLIN;
	listen_fd_event.data.fd = listenfd;
	
	//將偵聽socket綁定到epollfd上去
	if(epoll_ctl(epollfd, EPOLL_CTL_ADD, listenfd, &listen_fd_event) == -1)
	{
		std::cout << "epoll_ctl error." << std::endl;
		close(listenfd);
        return -1;
	}
		
	int n;
	while (true)
	{		
		epoll_event epoll_events[1024];
		n = epoll_wait(epollfd, epoll_events, 1024, 1000);
		if (n < 0)
		{
			//被信號中斷
			if (errno == EINTR)
				continue;
			
			//出錯,退出
			break;
		}
		else if (n == 0)
		{
			//超時,繼續
			continue;
		}
		
		for (size_t i = 0; i < n; ++i)
		{
			// 事件可讀
			if (epoll_events[i].events & POLLIN)
			{
				if (epoll_events[i].data.fd == listenfd)
				{
					//偵聽socket,接受新連接
					struct sockaddr_in clientaddr;
					socklen_t clientaddrlen = sizeof(clientaddr);
					//接受客戶端連接, 並加入到fds集合中
					int clientfd = accept(listenfd, (struct sockaddr *)&clientaddr, &clientaddrlen);
					if (clientfd != -1)
					{
						//將客戶端socket設置爲非阻塞的
						int oldSocketFlag = fcntl(clientfd, F_GETFL, 0);
						int newSocketFlag = oldSocketFlag | O_NONBLOCK;
						if (fcntl(clientfd, F_SETFL,  newSocketFlag) == -1)
						{
							close(clientfd);
							std::cout << "set clientfd to nonblock error." << std::endl;						
						}
						else
						{
							epoll_event client_fd_event;
							client_fd_event.events = POLLIN;
							client_fd_event.data.fd = clientfd;							
							if(epoll_ctl(epollfd, EPOLL_CTL_ADD, clientfd, &client_fd_event) != -1)
							{
								std::cout << "new client accepted, clientfd: " << clientfd << std::endl;
							}
							else
							{
								std::cout << "add client fd to epollfd error." << std::endl;
								close(clientfd);							
							}
						}		
					}
				}
				else 
				{
					//普通clientfd,收取數據
					char buf[64] = { 0 };
					int m = recv(epoll_events[i].data.fd, buf, 64, 0);
					if (m == 0)
					{
						//對端關閉了連接,從epollfd上移除clientfd
						if(epoll_ctl(epollfd, EPOLL_CTL_DEL, epoll_events[i].data.fd, NULL) != -1)
						{
							std::cout << "client disconnected, clientfd: " << epoll_events[i].data.fd << std::endl;
						}
						close(epoll_events[i].data.fd);
					}
					else if (m < 0)
					{											
						//出錯,從epollfd上移除clientfd
						if (errno != EWOULDBLOCK && errno != EINTR)
						{
							if(epoll_ctl(epollfd, EPOLL_CTL_DEL, epoll_events[i].data.fd, NULL) != -1)
							{
								std::cout << "client disconnected, clientfd: " << epoll_events[i].data.fd << std::endl;
							}
							close(epoll_events[i].data.fd);
						}
					}
					else
					{
						//正常收到數據
						std::cout << "recv from client: " << buf << ", clientfd: " << epoll_events[i].data.fd << std::endl;
					}
				}
			}
			else if (epoll_events[i].events & POLLERR)
			{
				//TODO: 暫且不處理
			}
			
		}// end  outer-for-loop
	}// end  while-loop
	  
	  
	//關閉偵聽socket
	//(理論上應該關閉包括所有clientfd在內的fd,但這裏只是爲了演示問題,就不寫額外的代碼來處理啦)
	close(listenfd);			

    return 0;
}

編譯上述程序生成 epoll_server 並啓動,然後使用 nc 命令啓動三個客戶端給服務器發數據效果如下圖所示:
在這裏插入圖片描述


本文首發於『easyserverdev』公衆號,歡迎關注,轉載請保留版權信息。

歡迎加入高性能服務器開發 QQ 羣一起交流: 578019391
微信掃碼關注

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