1. 程式人生 > >EPOLL使用的簡單總結

EPOLL使用的簡單總結

EPOLL使用簡單總結

0. 為什麼要用epoll

既然用到epoll,一定對select和poll有一定的瞭解。
Select需要與fd_set結構體配合使用,並在使用者空間維護一個客戶端描述符,且管理控制代碼時有數目的限制。
Poll解決了控制代碼數目的限制(連結串列實現),同時維護一個pollfd結構體的客戶端事件的集合。

這來倆效能侷限點為:
Select和POLL都會遍歷整個集合來確定活躍描述符
與核心互動時會把所有控制代碼拷貝到核心

注意的是:
伺服器效能四大殺手:
1.資料拷貝-> 快取方案
2.環境切換(執行緒切換)->單核單執行緒,多核多執行緒
3.記憶體分配->記憶體池
4.鎖競爭->減少鎖的使用

Poll每次需要從使用者態將所有的控制代碼複製到核心態,如果以萬計的控制代碼會導致每次都要copy幾十幾百KB的記憶體到核心態,非常低效。使用epoll時你只需要呼叫epoll_ctl事先新增到對應紅黑樹,真正用epoll_wait時不用傳遞socket控制代碼給核心,節省了拷貝開銷。

以上此段出自阿里雲《epoll全面講解:從實現到應用》https://www.aliyun.com/jiaocheng/122174.html

Epoll在核心的實現使用了mmap共享記憶體,紅黑樹和鎖,所以在一定條件下提升機器的效能:
大量連結的/不是所有的控制代碼都很活躍 條件下使用epoll

1. 為什麼要使用非阻塞模式

ET模式需要非阻塞。
為此我們需要知道什麼是阻塞模式,非阻塞模式,IO複用模型。
此外,在伺服器程式中發生阻塞一般是讀寫資料和accept等待連結的時候。

以下圖和思想,來源於《Unix網路程式設計 第二版》第一卷 第二部分 第六章 第二節

阻塞模式:
阻塞IO模型
正如原文所說,一開始寫的網路程式設計程式碼都是阻塞模式,直觀一點的意思就是沒有用到select/poll,直接使用socket-> sockaddr_in ->bind->listen->while(1)->accept模型的簡單回射伺服器就是阻塞IO模型應用。此模型的侷限是一個執行緒或者程序只能同時處理一個描述符。

非阻塞模式:
非阻塞IO模型
也就是應用層一直檢查核心是否準備好資料,直到完成。可以做個簡單的實驗,就上面說過的回射伺服器,直接設定成非租塞,accept會一直返回-1。原因是一直在等待連結,當連結到來讀寫完資料,再次瘋狂返回-1。

IO複用模式:
IO複用模型
如圖,IO複用其實就是select/poll/epoll這類的函式,它們們幫我們完成了核心的監控,並可以監控多個,當核心某個IO準備好後通知我們,我們在呼叫。與上面的非阻塞模式配合使用就不會反會-1的錯誤(當資料準備好後再accept,舉例select也就是if (pollfds[0].revents & POLLIN){… accept …})。

我在使用第一次使用epoll時候(就是寫這完文件的前一天)使用的是<非阻塞+IO複用+LT模式>,其實LT模式下非阻塞效能不高,但是好寫。
之後會改ET。
先放個圖。
epoll觸發模式

圖片來源圖片來源CSDN《epoll EPOLLL、EPOLLET模式與阻塞、非阻塞》https://blog.csdn.net/zxm342698145/article/details/80524331

2. epoll使用(c++,面向過程)

先說一下用epoll和不用IO複用網路伺服器程式設計的區別
首先是阻塞的程式設計流程(個人總結不是很嚴謹):
阻塞IO編碼流程

然後就是epoll 的IO複用程式設計模型:
epoll的IO複用模型
程式碼中的體現如下:

int main()
{
	/*Socket(AF_INET, SOCK_STREAM, 0),我這裡設定了非阻塞模式,下面的accept4也是。*/
	int listenfd;
	listenfd = Socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0); // fei zu se IO fu yong
	/*設定伺服器的sockaddr_in結構體,IPv4,當前地址,8000埠*/
	struct sockaddr_in serveraddr;
	bzero(&serveraddr, sizeof(serveraddr));
	serveraddr.sin_family = AF_INET;
	serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
	serveraddr.sin_port = htons(8000);
	/*重連處理*/
	int opt = 1;
	setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
	/*Bind繫結描述符和伺服器結構體*/
	Bind(listenfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr));
	/*Listen監聽描述符*/
	Listen(listenfd, 20);
	/*準備客戶端sockaddr_in結構體,以及accept的返回值*/
	struct sockaddr_in clientaddr;
	socklen_t clientlen;
	int connfd;

	/*準備epoll的epoll_event結構體集,用的是c++的向量,為了方便*/
	typedef std::vector<struct epoll_event> EpollList;     
	/*epoll_create1(EPOLL_CLOEXEC)生成用於處理accept的epoll專用的檔案描述符,建立一個epoll的控制代碼*/
	int epollfd;
	epollfd = epoll_create1(EPOLL_CLOEXEC);
	//Creates a handle to epoll, the size of which tells the kernel how many listeners there are.
	/*設定epoll_event結構體監聽事件,epoll_event結構體的變數,epfd用於註冊事件*/
	struct epoll_event epfd;
	epfd.data.fd = listenfd;
	epfd.events = EPOLLIN/*| EPOLLET */;
	/*epoll_ctl(epollfd, EPOLL_CTL_ADD, listenfd, &epfd);
	epollfd為epoll_create1返回,epfd為epoll_event結構體監聽事件的結構體
	epoll的事件註冊函式,它不同與select()是在監聽事件時(epoll使用epoll_wait監聽)告訴核心要監聽什麼型別的事件,而是在這裡先註冊要監聽的事件型別*/
	epoll_ctl(epollfd, EPOLL_CTL_ADD, listenfd, &epfd);
	/*設定epoll_event結構體集的大小*/
	EpollList events(16);//You can listen for 16 at first

	int nready;//活躍描述符個數

	while(1)
	{	
		/*nready = epoll_wait(epollfd, &*events.begin(), static_cast<int>(events.size()), -1); 
			等待事件的產生,類似於select()呼叫。引數events用來從核心得到事件的集合,maxevents告之核心這個events有多大,
			這個 maxevents的值不能大於建立epoll_create()時的size,引數timeout是超時時間(毫秒,0會立即返回,-1是永久阻塞)。
			該函式返回需要處理的事件數目,如返回0表示已超時。*/
		nready = epoll_wait(epollfd, &*events.begin(), static_cast<int>(events.size()), -1);
		if (nready == -1)//出錯處理
		{
			if(errno == EINTR) 
				continue;
			perror("epoll_wait");
		}
		if(nready == 0) //如果沒有活躍的重來
			continue;

		if ((size_t)nready == events.size())//如果結構體集不夠用了,倍增
		{
			events.resize(events.size() * 2);
		}
		/*遍歷返回的活躍描述符for(int  i=0; i < nready; ++i)*/
		for(int  i=0; i < nready; ++i)
		{
			/*if (events[i].data.fd == listenfd)監聽活躍*/
			if (events[i].data.fd == listenfd)
			{
				/*Accept客戶端結構體和監聽描述符,返回一個客戶描述符,accept4比accept定義一個引數*/
				clientlen = sizeof(clientaddr);
				connfd = Accept4(listenfd, (struct sockaddr*)&clientaddr, &clientlen, 
									SOCK_NONBLOCK | SOCK_CLOEXEC);// fei zu se IO fu yong

				std::cout << connfd << "is come!" << std::endl;
				/*有客戶訪問到來,修改結構體事件,把監聽描述符改為客戶連結描述符,寫入核心*/
				epfd.data.fd = connfd;
				epfd.events = EPOLLIN/* | EPOLLET*/;
				epoll_ctl(epollfd, EPOLL_CTL_ADD, connfd, &epfd);

			}
			/*if (events[i].events & EPOLLIN)客戶描述符活躍,客戶連結描述符有可讀事件*/
			else if (events[i].events & EPOLLIN)
			{
				connfd = events[i].data.fd;//取出連結描述符使用
				if (connfd < 0)
				{
					continue;
				}
				/*用於讀寫的準備*/
				char buf[100];
				bzero(buf, sizeof(buf));
				int n;
				if ((n = read(connfd, buf, 100)) > 0)
				{	
					std::cout << "::" << connfd <<" Date: ["<< buf <<"]" << std::endl;
					write(connfd, buf, n);
				}
				/*關閉描述符,就是客戶斷開連線後處理*/
				else if (n == 0)
				{
					std::cout << connfd << "is go" << std::endl;
					close(connfd);
					epfd = events[i];
					epoll_ctl(epollfd, EPOLL_CTL_DEL, connfd, &epfd);
				}

			}
		}

	}

	return 0;
}

3. epoll介面和結構體

Epoll的標頭檔案

#include <sys/epoll.h>

Epoll的函式介面

	Int epoll_create(int size);

引數size為設定可以連線的多少,老的create函式,例項epoll,現在引數size被忽略,大小取決於核心的處理能力。

Int epoll_create1(int flags);

推薦使用的新版本, flags引數的值為EPOLL_CLOEXEC ;

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

事件註冊函式

  • epfd:epoll_create返回的例項;

  • op:表示動作
    EPOLL_CTL_ADD:註冊新的fd到epfd中;
    EPOLL_CTL_MOD:修改已經註冊的fd的監聽事件;
    EPOLL_CTL_DEL:從epfd中刪除一個fd;

  • fd:監聽的檔案描述符;

  • event:通知核心的結構體下頁說明

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

相當於select函式等待事件產生maxevents:通知核心event的大小;timeout:超時時間,-1為永遠等待;

EPOLL結構體

Typedef union epoll_date{
		void *ptr;
		int fd;
		unit32_t u32;
		unit64_t u64;
}epoll_data_t

聯合體,使用者資料變數,一般使用fd檔案描述符

Struct epoll_event{
	unit32_t events;
	epoll_data_t date;
}

events可以是以下幾個巨集的集合:
**EPOLLIN **:表示對應的檔案描述符可以讀(包括對端SOCKET正常關閉);

EPOLLOUT:表示對應的檔案描述符可以寫;

EPOLLPRI:表示對應的檔案描述符有緊急的資料可讀(這裡應該表示有帶外資料到來);

EPOLLERR:表示對應的檔案描述符發生錯誤;

EPOLLHUP:表示對應的檔案描述符被結束通話;

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

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

參考部落格
博主:lvyilong316
http://blog.chinaunix.net/uid/28541347.html
epoll專欄,共10篇