epoll,求知者離我近點

在這裏插入圖片描述

上網一搜epoll,基本是這樣的結果出來:《多路轉接I/O – epoll模型》,萬變不離這個標題。
但是呢,不變的事物,我們就更應該抓出其中的重點了。
多路、轉接、I/O、模型。
別急,先記住這幾個詞,我比較喜歡你們看我文章的時候帶着問題。

記住我,CSDN搜“看,未來”~~
https://blog.csdn.net/qq_43762191

什麼是epoll?或者說,它和select有什麼判別?

什麼是select

有的朋友可能對select也不是很瞭解啊,我這裏稍微科普一下:網絡連接,服務器也是通過文件描述符來管理這些連接上來的客戶端,既然是供連接的服務器,那就免不了要接收來自客戶端的消息。那麼多臺客戶端(不出意外,本文的客戶端指的是萬級併發),消息那麼的多,要是漏了一條兩條重要消息,那也不要用TCP了,那怎麼辦?

前輩們就是有辦法,輪詢,輪詢每個客戶端文件描述符,查看他們是否帶着消息,如果帶着,那就處理一下;如果沒帶着,那就一邊等着去。這就是select,輪詢,頗有點領導下基層的那種感覺哈。

但是這個select的輪詢吶,會有個問題,明眼人一下就能想到,那即是耗費資源啊,耗費什麼資源,時間吶,慢吶(其實也挺快了,不過相對epoll來說就是慢)。
再認真想一下,還浪費什麼資源,系統資源。有的客戶端吶,佔着那啥玩意兒不幹那啥事兒,這種客戶端吶,還不少。這也怪不得人家,哪兒有客戶端時時刻刻在發消息,要是有,那就要小心是不是惡意攻擊了。那把這麼一堆偶爾動一下的客戶端的文件描述符一直攥手裏,累不累?能一次攥多少個?就像一個老闆,要是一直心繫員工,一直想着下去巡視,那他可以去當車間組長了哈哈哈。

所以,select的默認上限一般是1024(FD_SETSIZE),當然我們可以手動去改,但是人家給個1024自然有人家的道理,改太大的話系統在這一塊的負載就大了。
那句話怎麼說的來着,你每次對系統的索取,其實都早已明碼標價!哈哈哈。。。

所以,我們選用epoll模型。

什麼是epoll

epoll接口是爲解決Linux內核處理大量文件描述符而提出的方案。該接口屬於Linux下多路I/O複用接口中select/poll的增強。其經常應用於Linux下高併發服務型程序,特別是在大量併發連接中只有少部分連接處於活躍下的情況 (通常是這種情況),在該情況下能顯著的提高程序的CPU利用率。

前面說,select就像親自下基層視察的老闆,那麼epoll這個老闆就要顯得精明的多了。他可不親自下基層,他找了個美女祕書,他只要盯着他的祕書看就行了,呸,他只需要聽取他的祕書的彙報就行了。彙報啥呢?基層有任何消息,跟祕書說,祕書彙總之後一次性交給老闆來處理。這樣老闆的時間不就大大的提高了嘛。

如果你學過設計模式,這就是典型的“命令模式”,非常符合“依賴倒置原則”,這是一個非常美妙的模式,這個原則也是我最喜歡的一個原則,將高層實現與低層實現解耦合,從而可以各自開發,只要接口一致便可,這個接口,就是祕書。

扯遠了,如果對“設計模式”有興趣,可以找我的專欄。

好,言歸正傳哈哈哈。

epoll設計思路簡介

  • (1)epoll在Linux內核中構建了一個文件系統,該文件系統採用紅黑樹來構建,紅黑樹在增加和刪除上面的效率極高,因此是epoll高效的原因之一。有興趣可以百度紅黑樹瞭解,但在這裏你只需知道其算法效率超高即可。
  • (2)epoll提供了兩種觸發模式,水平觸發(LT)和邊沿觸發(ET)。當然,涉及到I/O操作也必然會有阻塞和非阻塞兩種方案。目前效率相對較高的是 epoll+ET+非阻塞I/O 模型,在具體情況下應該合理選用當前情形中最優的搭配方案。
  • (3)不過epoll則沒有這個限制,它所支持的FD上限是最大可以打開文件的數目,這個數字一般遠大於1024,舉個例子,在1GB內存的機器上大約是10萬左右,具體數目可以下面語句查看,一般來說這個數目和系統內存關係很大。
系統最大打開文件描述符數
cat /proc/sys/fs/file-max
進程最大打開文件描述符數
ulimit -n

修改這個配置:

sudo vi /etc/security/limits.conf
寫入以下配置,soft軟限制,hard硬限制

*                soft    nofile          65536
*                hard    nofile          100000

那個65536,改一下,不礙事。

邊緣觸發與水平觸發,阻塞I/O與非阻塞I/O

阻塞I/O與非阻塞I/O

爲了方便理解後面的內容,我們先看幾張圖,關於阻塞與非阻塞I/O的。

阻塞式文件I/O

在這裏插入圖片描述

非阻塞式文件I/O

在這裏插入圖片描述

多路複用I/O

在這裏插入圖片描述
好,有了上面這幾張圖墊着,咱來看看邊緣觸發和水平觸發。

ET V/S LT

EPOLL 事件有兩種模型:
Edge Triggered (ET) 邊緣觸發 只有數據到來,才觸發,不管緩存區中是否還有數據。
Level Triggered (LT) 水平觸發 只要有數據都會觸發。

LT(level triggered) 是 缺省 的工作方式 ,並且同時支持 block 和 no-block socket. 在這種做法中,內核告訴你一個文件描述符是否就緒了,然後你可以對這個就緒的 fd 進行 IO 操作。如果你不作任何操作,內核還是會繼續通知你的,所以,這種模式編程出錯誤可能性要小一點。傳統的 select/poll 都是這種模型的代表.

ET(edge-triggered) 是高速工作方式 ,只支持 no-block socket 。在這種模式下,當描述符從未就緒變爲就緒時,內核通過 epoll 告訴你。然後它會假設你知道文件描述符已經就緒,並且不會再爲那個文件描述符發送更多的就緒通知,直到你做了某些操作導致那個文件描述符不再爲就緒狀態了 ( 比如,你在發送,接收或者接收請求,或者發送接收的數據少於一定量時導致了一個 EWOULDBLOCK 錯誤)。但是請注意,如果一直不對這個 fd 作 IO 操作 ( 從而導致它再次變成未就緒 ) ,內核不會發送更多的通知 (only once), 不過在 TCP 協議中, ET 模式的加速效用仍需要更多的 benchmark 確認(這句話不懂的話,可以去翻一下我的《跟我一起重學網絡編程》專欄,那個專欄都是從《卷一》上摘錄的)。

epoll 工作在 ET 模式的時候,必須使用非阻塞套接口,以避免由於一個文件句柄的阻塞讀 / 阻塞寫操作把處理多個文件描述符的任務餓死。最好以下面的方式調用 ET 模式的 epoll 接口,在後面會介紹避免可能的缺陷。

  • 基於非阻塞文件句柄
  • 只有當 read(2) 或者 write(2) 返回 EAGAIN 時才需要掛起,等待。但這並不是說每次 read() 時都需要循環讀,直到讀到產生一個 EAGAIN 才認爲此次事件處理完成,當 read() 返回的讀到的數據長度小於請求的數據長度時,就可以確定此時緩衝中已沒有數據了,也就可以認爲此事讀事件已處理完成。

epoll API

講了這麼多概念的東西,來看看API吧。

頭文件

#include<sys/epoll.h>

創建句柄

int epoll_create( int size);

創建一個epoll句柄,參數size用於告訴內核監聽的文件描述符個數,跟內存大小有關。
返回epoll 文件描述符

epoll控制函數

int epoll_ctl( int epfd, int op, int fd, struct epoll_event *event ); // 成功,返0,失敗返-1

控制某個epoll監控的文件描述符上的事件:註冊,修改,刪除

參數釋義:
epfd:爲epoll的句柄
op:表示動作,用3個宏來表示
··· EPOLL_CTL_ADD(註冊新的 fd 到epfd)
··· EPOLL_CTL_DEL(從 epfd 中刪除一個 fd)
··· EPOLL_CTL_MOD(修改已經註冊的 fd 監聽事件)

event:告訴內核需要監聽的事件

typedef union epoll_data
{
    void* ptr;
    int fd;
    __uint32_t u32;
    __uint64_t u64;
} epoll_data_t;  /* 保存觸發事件的某個文件描述符相關的數據 */
 
struct epoll_event
{
    __uint32_t events;  /* epoll event */
    epoll_data_t data;  /* User data variable */
};
/* epoll_event.events:
  EPOLLIN  表示對應的文件描述符可以讀
  EPOLLOUT 表示對應的文件描述符可以寫
  EPOLLPRI 表示對應的文件描述符有緊急的數據可讀
  EPOLLERR 表示對應的文件描述符發生錯誤
  EPOLLHUP 表示對應的文件描述符被掛斷
  EPOLLET  表示對應的文件描述符有事件發生
*/

epoll消息讀取

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

等待所監控文件描述符上有事件的產生

參數釋義:
events:用來從內核得到事件的集合
maxevent:用於告訴內核這個event有多大,這個maxevent不能大於創建句柄時的size
timeout:超時時間
··· -1:阻塞
··· 0:立即返回
···>0:指定微秒

成功返回有多少個文件描述符準備就緒,時間到返回0,出錯返回-1.

代碼示例

這裏我要聲明一下,如果有疑義,可以在下面評論,因爲我之前初學的時候那篇epoll被人口吐芬芳哈哈哈。

下面這個代碼,可以實現萬級併發。

/*server.c*/

#include<stdio.h>
#include<string.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<sys/epoll.h>
#include<errno.h>

#define MAXLINE 80
#define SERV_PORT 8000
#include OPEN_MAX 1024

int main(void)
{
	struct sockaddr_in servaddr,cliaddr;
	socklen_t cliaddr_len;
	int i,j,maxi,listenfd,connfd,sockfd;
	int nready,efd,res,client[OPEN_MAX];
	ssize_t n;
	char buf[MAXLINE];
	char str[INET_ADDRSTRLEN];
	struct epoll_event tep,ep[OPEN_MAX];
	
	listenfd = socket(AF_INET,SOCK_STREAM,0);
	
	bzero(&servaddr,sizeof(servaddr));
	servaddr.sin_family = AF_INET;
	servaddr.sin_port = htonl(SERV_PORT);
	servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
	bind(listenfd,(struct sockaddr *)&servaddr,sizeof(servaddr));
	listen(listenfd,20);

	maxfd = listenfd;  //初始化
	maxi = -1; //client[]的下標
	for(i = 0 ; i < OPEN_MAX ; i++ )
		client[i] = -1; //用-1初始化client

//套路開始
	efd = epoll_create(OPEN_MAX);  //創建句柄
	if(efd == -1)
		perrno("epoll_create");
	
	tep.events = EPOLLIN;  //設置讀事件
	tep.data.fd = listenfd;  //套接socket文件描述符
	res = epoll_ctl(efd,EPOLL_CTL_ADD,listened,&tep);  //將listenfd加入監聽文件表,監聽listenfd的讀取內容
	if(res == -1)
		perrno("epoll_ctl");
	
	while(1)
	{
		nready = epoll_wait(efd,ep,OPEN_MAX,-1);  //阻塞監聽
		if(nready == -1)
			perrno("epoll_wait error:");
		
		for(i == 0; i<nready; i++)
		{
			if(!ep[i].event & EPOLLIN)
				continue;
			if(ep[i].data.fd == listenfd)
			{
				//開始接收數據了
				printf("Accepting connections···  \n");  //寫完一定要來檢查一下這個換行,一不小心就忘記了
				cliaddr_len = sizeof(cliaddr);  //這得實時更新
				connfd = accept(listenfd,(struct sockaddr *)&cliaddr,&cliaddr_len);  //接收連接

				printf("Read from %s at port %d \n",inet_ntop(AF_INET,&cliaddr.sin_addr,str,sizeof(str)),ntohs(cliaddr.sin_port));
				/*將客戶端的地址讀取到str裏面然後打印*/  /*將端口號轉換成整形數輸出*/

				for(j = 0;j < OPEN_MAX; j++)
				{
					if(client[j] < 0)
					{
						client[j] = connfd;  //保存accept返回的文件描述符到client【】裏
						break;
					}
	
					if( j == OPEN_MAX )
					{
						printf("Too many clients \n",stderr);
						exit(-1);
					}
		
					if(j > maxi)
						maxi = j;
				}
				tep.events = EPOLLIN;
				tep.data.fd = connfd;
				res = epoll_ctl(efd,EPOLL_CTL_ADD,connfd,&tep);  //將connfd加入監聽文件表,監聽connfd的讀取內容
				if(res == -1)
					perrno("epoll_ctl");
				else
				{
					sockfd = ep[i].data.fd;
					n = read(sockfd,buf,MAXLINE);
					if(n == 0)
					{
						for(j = 0 ; j <= maxi ; j++)
						{
							if(client[i] == sockfd)
							{
								client[j] = -1;
								break;
							}
						}
						res = epoll_ctl(efd,EPOLL_CTL_ADD,sockfd,&tep);  //將sockfd加入監聽文件表,監聽sockfd的讀取內容
						if(res == -1)
							perrno("epoll_ctl")
						close(sockfd);
						printf("Client[%d] closed connetion \n",j);
					}
					else
					{
						for(j = 0; j<n; j++)
							buf[j] = toupper(buf[j]);
						write(sockfd,buf,n);
					}
				}
			}	
		}
	}
	close(listenfd);
	close(efd)
	return 0;;		
}

如果有需要,在下面評論討論,我的項目用的是C++,不是C,這套代碼是用作測試端與服務器速度比對的。

番外:高效的併發方式

併發編程的目的是讓程序”同時”執行多個任務。如果程序是計算密集型的,併發編程並沒有什麼優勢,反而由於任務的切換使效率降低。但如果程序是I/O密集型的,那就不同了。

併發模式是指I/O處理單元和多個邏輯單元之間協調完成任務的方法,服務器主要有兩種併發編程模式:半同步/半異步(half-sync/half-async)模式和領導者/追隨者(Leader/Followers)模式。

這裏講一個“半同步/半異步”。

下面的內容需要有一定的基礎了,小白可以收藏一下以後變強了再看。

半同步/半異步模式

在半同步/半異步模式中,同步線程用於處理客戶邏輯,異步線程用於處理I/O事件。異步線程監聽到客戶請求之後就將其封裝成請求對象並插入到請求隊列中。請求隊列將通知某個工作在同步模式的工作線程來讀取並處理該請求對象。

在這裏插入圖片描述

半同步/半反應堆模式(half-sync/half-reactive模式)

半同步/半反應堆模式是半同步/半異步模式的一種變體。
其結構如下圖:
在這裏插入圖片描述
在上圖中,異步線程只有一個,由主線程充當,負責監聽socket上的事件。如果監聽socket上有新的連接請求到來,主線程就接受新的連接socket,然後往epoll內核事件表中註冊該socket上的讀寫事件。如果連接socket上有讀寫事件發生,即有新的客戶請求到來或有數據要發送至客戶端,主線程就將該連接socket插入到請求隊列中,所有工作線程都睡眠在請求隊列上,當有任務到來時,他們通過競爭來獲取任務的接管權。
由於主線程插入請求隊列中的任務是就緒的連接socket,所以該半同步/半反應堆模式所採用的事件處理模式是Reactor模式,即工作線程要自己從socket上讀寫數據。當然,半同步/半反應堆模式也可以用模擬的Proactor事件處理模式,即由主線程來完成數據的讀寫操作,此時主線程將應用程序數據、任務類型等信息封裝爲一個任務對象,然後將其插入到請求隊列。

半同步/半反應堆模式的缺點:
主線程和工作線程共享請求隊列,因而請求隊列是臨界資源,所以對請求隊列操作的時候需要加鎖保護。
每個工作線程在同一時間只能處理一個客戶請求。如果客戶數量增多,則請求隊列中堆積任務太多,客戶端的響應會越來越慢。如果增多工作線程的話,則線程的切花也將消耗大量的CPU時間。

高效的半同步/半異步模式

在半同步/半反應堆模式中,每個工作線程同時只能處理一個客戶請求,如果併發量大的話,客戶端響應會很慢。如果每個工作線程都能同時處理多個客戶鏈接,則就能改善這種情況,所以就有了高效的半同步/半異步模式。
其結構如圖:
在這裏插入圖片描述主線程只管監聽socket,當有新的連接socket到來時,主線程就接受連接並返回新的連接socket給某個工作線程。此後該新連接socket上的任何I/O操作都由被選中的工作線程來處理,直到客戶端關閉連接。當工作線程檢測到有新的連接socket到來時,就把該新的連接socket的讀寫事件註冊到自己的epoll內核事件表中。
主線程和工作線程都維持自己的事件循環,他們各自獨立的監聽不同事件。因此在這種高效的半同步/半異步模式中,每個線程都工作在異步模式中,所以它並非嚴格意義上的半同步/半異步模式。

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