當(dāng)前位置:首頁 > 公眾號(hào)精選 > 后端技術(shù)指南針
[導(dǎo)讀]1.寫在前面 又到周六了,不過這周有點(diǎn)忙新文章還沒有寫,為了不跳票,就想著把早期還不錯(cuò)的文章,重新排版修改發(fā)一下,因?yàn)楫?dāng)時(shí)讀者很少,現(xiàn)在而言完全可以當(dāng)作一篇新文章( 有種狡辯的意思 )... 今天一起來學(xué)習(xí)一下高并發(fā)實(shí)現(xiàn)的的重要基礎(chǔ): I/O復(fù)用技術(shù) & ep

1.寫在前面

又到周六了,不過這周有點(diǎn)忙新文章還沒有寫,為了不跳票,就想著把早期還不錯(cuò)的文章,重新排版修改發(fā)一下,因?yàn)楫?dāng)時(shí)讀者很少,現(xiàn)在而言完全可以當(dāng)作一篇新文章( 有種狡辯的意思 )...
今天一起來學(xué)習(xí)一下高并發(fā)實(shí)現(xiàn)的的重要基礎(chǔ): I/O復(fù)用技術(shù) & epoll原理 。
通過本文你將了解到以下內(nèi)容:
  • IO復(fù)用的概念
  • epoll出現(xiàn)之前的IO復(fù)用工具
  • epoll三級(jí)火箭
  • epoll底層實(shí)現(xiàn)
  • ET模式&LT模式
  • 一道騰訊面試題
  • epoll驚群?jiǎn)栴}

溫馨提示:技術(shù)文章涉及很多細(xì)節(jié)都會(huì)比較晦澀,反復(fù)琢磨才能掌握。

2.初識(shí)復(fù)用技術(shù)和IO復(fù)用

在了解epoll之前,我們先看下復(fù)用技術(shù)的概念和IO復(fù)用到底在說什么?

2.1 復(fù)用概念和資源特性

2.1.1 復(fù)用的概念

復(fù)用技術(shù) multiplexing 并不是新技術(shù)而是一種設(shè)計(jì)思想,在通信和硬件設(shè)計(jì)中存在頻分復(fù)用、時(shí)分復(fù)用、波分復(fù)用、碼分復(fù)用等,在日常生活中復(fù)用的場(chǎng)景也非常多,因此不要被專業(yè)術(shù)語所迷惑。

從本質(zhì)上來說,復(fù)用就是為了解決有限資源和過多使用者的不平衡問題,從而實(shí)現(xiàn)最大的利用率,處理更多的問題。

2.1.2 資源的可釋放

舉個(gè)例子:
不可釋放場(chǎng)景:ICU 病房的呼吸機(jī)作為有限資源,病人一旦占用且在未脫離危險(xiǎn)之前是無法放棄占用的,因此不可能幾個(gè)情況一樣的病人輪流使用。

可釋放場(chǎng)景:對(duì)于一些其他資源比如醫(yī)護(hù)人員就可以實(shí)現(xiàn)對(duì)多個(gè)病人的同時(shí)監(jiān)護(hù),理論上不存在一個(gè)病人占用醫(yī)護(hù)人員資源不釋放的場(chǎng)景。

所以我們可以想一下,多個(gè) IO 共用的資源(處理線程)是否具備可釋放性?

2.1.3 理解IO復(fù)用

I/O的含義:在計(jì)算機(jī)領(lǐng)域常說的IO包括磁盤 IO 和網(wǎng)絡(luò) IO,我們所說的IO復(fù)用主要是指網(wǎng)絡(luò) IO ,在Linux中一切皆文件,因此網(wǎng)絡(luò)IO也經(jīng)常用文件描述符 FD 來表示。

復(fù)用的含義:那么這些文件描述符 FD 要復(fù)用什么呢?在網(wǎng)絡(luò)場(chǎng)景中復(fù)用的就是任務(wù)處理線程,所以簡(jiǎn)單理解就是多個(gè)IO共用1個(gè)處理線程

IO復(fù)用的可行性:IO請(qǐng)求的基本操作包括read和write,由于網(wǎng)絡(luò)交互的本質(zhì)性,必然存在等待,換言之就是整個(gè)網(wǎng)絡(luò)連接中FD的讀寫是交替出現(xiàn)的,時(shí)而可讀可寫,時(shí)而空閑,所以IO復(fù)用是可用實(shí)現(xiàn)的。

綜上認(rèn)為:IO復(fù)用技術(shù)就是協(xié)調(diào)多個(gè)可釋放資源的FD交替共享任務(wù)處理線程完成通信任務(wù),實(shí)現(xiàn)多個(gè)fd對(duì)應(yīng)1個(gè)任務(wù)處理線程的復(fù)用場(chǎng)景。

現(xiàn)實(shí)生活中IO復(fù)用就像一只邊牧管理幾百只綿羊一樣:

圖片來自網(wǎng)絡(luò):多只綿羊共享1只邊牧的管理

2.1.4 IO復(fù)用的出現(xiàn)背景

業(yè)務(wù)需求是推動(dòng)技術(shù)演進(jìn)的源動(dòng)力。
在網(wǎng)絡(luò)并發(fā)量非常小的原始時(shí)期,即使 per req per process 地處理網(wǎng)絡(luò)請(qǐng)求也可以滿足要求,但是隨著網(wǎng)絡(luò)并發(fā)量的提高,原始方式必將阻礙進(jìn)步,所以就刺激了 IO 復(fù)用機(jī)制的實(shí)現(xiàn)和推廣。
高效 IO 復(fù)用機(jī)制要滿足:協(xié)調(diào)者消耗最少的系統(tǒng)資源、最小化FD的等待時(shí)間、最大化FD的數(shù)量、任務(wù)處理線程最少的空閑、多快好省完成任務(wù)等。

畫外音:上面的一段話可能讀起來有些繞,樸素的說法就是讓任務(wù)處理線程以更小的資源消耗來協(xié)調(diào)更多的網(wǎng)絡(luò)請(qǐng)求連接,IO復(fù)用工具也是逐漸演進(jìn)的,經(jīng)過前后對(duì)比就可以發(fā)現(xiàn)這個(gè)原則一直貫穿其中。

理解了IO復(fù)用技術(shù)的基本概念,我們接著來看Linux系統(tǒng)中先后出現(xiàn)的各種IO復(fù)用工具以及各自的特點(diǎn),加深理解。

3. Linux的IO復(fù)用工具概覽

在 Linux 中先后出現(xiàn)了select、poll、epoll等,F(xiàn)reeBSD的 kqueue也是非常優(yōu)秀的 IO 復(fù)用工具,kqueue 的原理和 epoll 很類似,本文以 Linux 環(huán)境為例,并且不討論過多 select 和 poll 的實(shí)現(xiàn)機(jī)制和細(xì)節(jié)。

3.1 先驅(qū)者select

select 是 2000年左右出現(xiàn)的,對(duì)外的接口定義:
/* According to POSIX.1-2001 */
#include <sys/select.h>

/* According to earlier standards */
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>

int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
void FD_CLR(int fd, fd_set *set);
int FD_ISSET(int fd, fd_set *set);
void FD_SET(int fd, fd_set *set);
void FD_ZERO(fd_set *set);

3.1.1 官方提示

作為第一個(gè)IO復(fù)用系統(tǒng)調(diào)用,select 使用一個(gè)宏定義函數(shù)按照 bitmap原理填充 fd,默認(rèn)大小是 1024個(gè),因此對(duì)于fd的數(shù)值大于 1024都可能出現(xiàn)問題,看下官方預(yù)警:

Macro: int FD_SETSIZE
The value of this macro is the maximum number of file descriptors that a fd_set object can hold information about. On systems with a fixed maximum number, FD_SETSIZE is at least that number. On some systems, including GNU, there is no absolute limit on the number of descriptors open, but this macro still has a constant value which controls the number of bits in an fd_set; if you get a file descriptor with a value as high as FD_SETSIZE, you cannot put that descriptor into an fd_set.

簡(jiǎn)單解釋一下這段話的含義:
當(dāng)fd的絕對(duì)數(shù)值大于1024時(shí)將不可控,因?yàn)榈讓拥奈粩?shù)組的原因,官方不建議超過 1024,但是我們也無法控制 fd的絕對(duì)數(shù)值大小,之前針對(duì)這個(gè)問題做過一些調(diào)研,結(jié)論是系統(tǒng)對(duì)于 fd的分配有自己的策略,會(huì)大概率分配到1024以內(nèi),對(duì)此我并沒有充分理解,只是提及一下這個(gè)坑。

3.1.2 存在的問題和客觀評(píng)價(jià)

由于底層實(shí)現(xiàn)方式的局限性,select 存在一些問題,主要包括:
  • 可協(xié)調(diào)fd數(shù)量和數(shù)值都不超過1024 無法實(shí)現(xiàn)高并發(fā)
  • 使用O(n)復(fù)雜度遍歷fd數(shù)組查看fd的可讀寫性 效率低
  • 涉及大量kernel和用戶態(tài)拷貝 消耗大
  • 每次完成監(jiān)控需要再次重新傳入并且分事件傳入 操作冗余
select 以樸素的方式實(shí)現(xiàn)了IO復(fù)用,將并發(fā)量提高的最大K級(jí),但是對(duì)于完成這個(gè)任務(wù)的代價(jià)和靈活性都有待提高。
無論怎么樣 select作為先驅(qū)對(duì)IO復(fù)用有巨大的推動(dòng),并且指明了后續(xù)的優(yōu)化方向,不要無知地指責(zé) select。

3.2 繼承者epoll

在 epoll出現(xiàn)之前,poll 對(duì) select進(jìn)行了改進(jìn),但是本質(zhì)上并沒有太大變化,因此我們跳過 poll直接看 epoll。
epoll 最初在2.5.44內(nèi)核版本出現(xiàn),后續(xù)在2.6.x版本中對(duì)代碼進(jìn)行了優(yōu)化使其更加簡(jiǎn)潔,先后面對(duì)外界的質(zhì)疑在后續(xù)增加了一些設(shè)置來解決隱藏的問題,所以 epoll也已經(jīng)有十幾年的歷史了。
在《Unix網(wǎng)絡(luò)編程》第三版(2003年)還沒有介紹 epoll,因?yàn)槟莻€(gè)時(shí)代epoll還沒有出現(xiàn),書中只介紹了 select和poll,epoll對(duì)select中存在的問題都逐一解決,epoll的優(yōu)勢(shì)包括:
  • 對(duì)fd數(shù)量沒有限制(當(dāng)然這個(gè)在poll也被解決了)
  • 拋棄了bitmap數(shù)組實(shí)現(xiàn)了新的結(jié)構(gòu)來存儲(chǔ)多種事件類型
  • 無需重復(fù)拷貝fd 隨用隨加 隨棄隨刪
  • 采用事件驅(qū)動(dòng)避免輪詢查看可讀寫事件
epoll的應(yīng)用現(xiàn)狀
epoll出現(xiàn)之后大大提高了并發(fā)量對(duì)于C10K問題輕松應(yīng)對(duì),即使后續(xù)出現(xiàn)了真正的異步IO,也并沒有(暫時(shí)沒有)撼動(dòng)epoll的江湖地位。
因?yàn)閑poll可以解決數(shù)萬數(shù)十萬的并發(fā)量,已經(jīng)可以解決現(xiàn)在大部分的場(chǎng)景了,異步IO固然優(yōu)異,但是編程難度比epoll更大,權(quán)衡之下epoll仍然富有生命力。

4. 初識(shí)epoll

epoll 繼承了select 的風(fēng)格,留給用戶的接口非常簡(jiǎn)潔,可以說是簡(jiǎn)約而不簡(jiǎn)單,我們一起來感受一下。

4.1 epoll的基礎(chǔ)API和數(shù)據(jù)結(jié)構(gòu)

epoll主要包括epoll_data、epoll_event、三個(gè)api,如下所示:

//用戶數(shù)據(jù)載體
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;

//fd裝載入內(nèi)核的載體
struct epoll_event {
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};

//三板斧api
int epoll_create(int size);
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);

4.2 epoll三級(jí)火箭的科班理解

  • epoll_create
    該接口是在內(nèi)核區(qū)創(chuàng)建一個(gè)epoll相關(guān)的一些列結(jié)構(gòu),并且將一個(gè)句柄fd返回給用戶態(tài),后續(xù)的操作都是基于此fd的,參數(shù)size是告訴內(nèi)核這個(gè)結(jié)構(gòu)的元素的大小,類似于stl的vector動(dòng)態(tài)數(shù)組,如果size不合適會(huì)涉及復(fù)制擴(kuò)容,不過貌似4.1.2內(nèi)核之后size已經(jīng)沒有太大用途了;

  • epoll_ctl
    該接口是將fd添加/刪除于epoll_create返回的epfd中,其中epoll_event是用戶態(tài)和內(nèi)核態(tài)交互的結(jié)構(gòu),定義了用戶態(tài)關(guān)心的事件類型和觸發(fā)時(shí)數(shù)據(jù)的載體epoll_data;

  • epoll_wait
    該接口是阻塞等待內(nèi)核返回的可讀寫事件,epfd還是epoll_create的返回值,events是個(gè)結(jié)構(gòu)體數(shù)組指針存儲(chǔ)epoll_event,也就是將內(nèi)核返回的待處理epoll_event結(jié)構(gòu)都存儲(chǔ)下來,maxevents告訴內(nèi)核本次返回的最大fd數(shù)量,這個(gè)和events指向的數(shù)組是相關(guān)的;

其中三個(gè)api中貫穿了一個(gè)數(shù)據(jù)結(jié)構(gòu):epoll_event,它可以說是用戶態(tài)需監(jiān)控fd的代言人,后續(xù)用戶程序?qū)d的操作都是基于此結(jié)構(gòu)的;

4.3 epoll三級(jí)火箭的通俗解釋

可能上面的描述有些抽象,舉個(gè)現(xiàn)實(shí)中的例子,來幫助大家理解:
  • epoll_create場(chǎng)景
    大學(xué)開學(xué)第一周,你作為班長需要幫全班同學(xué)領(lǐng)取相關(guān)物品,你在學(xué)生處告訴工作人員,我是xx學(xué)院xx專業(yè)xx班的班長,這時(shí)工作人員確定你的身份并且給了你憑證,后面辦的事情都需要用到( 也就是調(diào)用epoll_create向內(nèi)核申請(qǐng)了epfd結(jié)構(gòu),內(nèi)核返回了epfd句柄給你使用);
  • epoll_ctl場(chǎng)景
    你拿著憑證在辦事大廳開始辦事,分揀辦公室工作人員說班長你把所有需要辦理事情的同學(xué)的學(xué)生冊(cè)和需要辦理的事情都記錄下來吧,于是班長開始在每個(gè)學(xué)生手冊(cè)單獨(dú)寫對(duì)應(yīng)需要辦的事情:李明需要開實(shí)驗(yàn)室權(quán)限、孫大熊需要辦游泳卡......就這樣班長一股腦寫完并交給了工作人員( 也就是告訴內(nèi)核哪些fd需要做哪些操作);
  • epoll_wait場(chǎng)景
    你拿著憑證在領(lǐng)取辦公室門前等著,這時(shí)候廣播喊xx班長你們班孫大熊的游泳卡辦好了速來領(lǐng)取、李明實(shí)驗(yàn)室權(quán)限卡辦好了速來取....還有同學(xué)的事情沒辦好,所以班長只能繼續(xù)( 也就是調(diào)用epoll_wait等待內(nèi)核反饋的可讀寫事件發(fā)生并處理);

4.4 epoll官方demo

通過man epoll可以看到官方的demo,雖然只有50行,但是干貨滿滿,如下:
#define MAX_EVENTS 10
struct epoll_event ev, events[MAX_EVENTS];
int listen_sock, conn_sock, nfds, epollfd;

/* Set up listening socket, 'listen_sock' (socket(),
bind(), listen()) */

epollfd = epoll_create(10);
if(epollfd == -1) {
perror("epoll_create");
exit(EXIT_FAILURE);
}

ev.events = EPOLLIN;
ev.data.fd = listen_sock;
if(epoll_ctl(epollfd, EPOLL_CTL_ADD, listen_sock, &ev) == -1) {
perror("epoll_ctl: listen_sock");
exit(EXIT_FAILURE);
}

for(;;) {
nfds = epoll_wait(epollfd, events, MAX_EVENTS, -1);
if (nfds == -1) {
perror("epoll_pwait");
exit(EXIT_FAILURE);
}

for (n = 0; n < nfds; ++n) {
if (events[n].data.fd == listen_sock) {
//主監(jiān)聽socket有新連接
conn_sock = accept(listen_sock,
(struct sockaddr *) &local, &addrlen);
if (conn_sock == -1) {
perror("accept");
exit(EXIT_FAILURE);
}
setnonblocking(conn_sock);
ev.events = EPOLLIN | EPOLLET;
ev.data.fd = conn_sock;
if (epoll_ctl(epollfd, EPOLL_CTL_ADD, conn_sock,
&ev) == -1) {
perror("epoll_ctl: conn_sock");
exit(EXIT_FAILURE);
}
} else {
//已建立連接的可讀寫句柄
do_use_fd(events[n].data.fd);
}
}
}
特別注意 : 在epoll_wait時(shí)需要區(qū)分是主監(jiān)聽線程fd的新連接事件還是已連接事件的讀寫請(qǐng)求,進(jìn)而單獨(dú)處理。

5. epoll的底層細(xì)節(jié)

epoll底層實(shí)現(xiàn)最重要的兩個(gè)數(shù)據(jù)結(jié)構(gòu):epitem和eventpoll。
可以簡(jiǎn)單的認(rèn)為epitem是和每個(gè)用戶態(tài)監(jiān)控IO的fd對(duì)應(yīng)的,eventpoll是用戶態(tài)創(chuàng)建的管理所有被監(jiān)控fd的結(jié)構(gòu),我們從局部到整體,從內(nèi)到外看一下epoll相關(guān)的數(shù)據(jù)結(jié)構(gòu)。

5.1 底層數(shù)據(jù)結(jié)構(gòu)

紅黑樹節(jié)點(diǎn)定義:

#ifndef  _LINUX_RBTREE_H
#define _LINUX_RBTREE_H
#include <linux/kernel.h>
#include <linux/stddef.h>
#include <linux/rcupdate.h>

struct rb_node {
unsigned long __rb_parent_color;
struct rb_node *rb_right;
struct rb_node *rb_left;
} __attribute__((aligned(sizeof(long))));
/* The alignment might seem pointless, but allegedly CRIS needs it */
struct rb_root {
struct rb_node *rb_node;
};

epitem定義:

struct epitem {
struct rb_node rbn;
struct list_head rdllink;
struct epitem *next;
struct epoll_filefd ffd;
int nwait;
struct list_head pwqlist;
struct eventpoll *ep;
struct list_head fllink;
struct epoll_event event;
}

eventpoll定義:

struct eventpoll {
spin_lock_t lock;
struct mutex mtx;
wait_queue_head_t wq;
wait_queue_head_t poll_wait;
struct list_head rdllist; //就緒鏈表
struct rb_root rbr; //紅黑樹根節(jié)點(diǎn)
struct epitem *ovflist;
}

5.2 底層調(diào)用過程

epoll_create會(huì)創(chuàng)建一個(gè)類型為struct eventpoll的對(duì)象,并返回一個(gè)與之對(duì)應(yīng)文件描述符,之后應(yīng)用程序在用戶態(tài)使用epoll的時(shí)候都將依靠這個(gè)文件描述符,而在epoll內(nèi)部也是通過該文件描述符進(jìn)一步獲取到eventpoll類型對(duì)象,再進(jìn)行對(duì)應(yīng)的操作,完成了用戶態(tài)和內(nèi)核態(tài)的貫穿。
epoll_ctl底層調(diào)用epoll_insert實(shí)現(xiàn):
  • 創(chuàng)建并初始化一個(gè)strut epitem類型的對(duì)象,完成該對(duì)象和被監(jiān)控事件以及epoll對(duì)象eventpoll的關(guān)聯(lián);

  • 將struct epitem類型的對(duì)象加入到epoll對(duì)象eventpoll的紅黑樹中管理起來;

  • 將struct epitem類型的對(duì)象加入到被監(jiān)控事件對(duì)應(yīng)的目標(biāo)文件的等待列表中,并注冊(cè)事件就緒時(shí)會(huì)調(diào)用的回調(diào)函數(shù),在epoll中該回調(diào)函數(shù)就是ep_poll_callback();

  • ovflist主要是暫態(tài)處理,調(diào)用ep_poll_callback()回調(diào)函數(shù)的時(shí)候發(fā)現(xiàn)eventpoll的ovflist成員不等于EP_UNACTIVE_PTR,說明正在掃描rdllist鏈表,這時(shí)將就緒事件對(duì)應(yīng)的epitem加入到ovflist鏈表暫存起來,等rdllist鏈表掃描完再將ovflist鏈表中的元素移動(dòng)到rdllist鏈表;

如圖展示了紅黑樹、雙鏈表、epitem之間的關(guān)系:

5.3 易混淆的數(shù)據(jù)拷貝

一種廣泛流傳的錯(cuò)誤觀點(diǎn):

epoll_wait返回時(shí),對(duì)于就緒的事件,epoll使用的是共享內(nèi)存的方式,即用戶態(tài)和內(nèi)核態(tài)都指向了就緒鏈表,所以就避免了內(nèi)存拷貝消耗

共享內(nèi)存?不存在的!
關(guān)于epoll_wait使用共享內(nèi)存的方式來加速用戶態(tài)和內(nèi)核態(tài)的數(shù)據(jù)交互,避免內(nèi)存拷貝的觀點(diǎn),并沒有得到2.6內(nèi)核版本代碼的證實(shí),并且關(guān)于這次拷貝的實(shí)現(xiàn)是這樣的:
revents = ep_item_poll(epi, &pt);//獲取就緒事件
if (revents) {
if (__put_user(revents, &uevent->events) ||
__put_user(epi->event.data, &uevent->data)) {
list_add(&epi->rdllink, head);//處理失敗則重新加入鏈表
ep_pm_stay_awake(epi);
return eventcnt ? eventcnt : -EFAULT;
}
eventcnt++;
uevent++;
if (epi->event.events & EPOLLONESHOT)
epi->event.events &= EP_PRIVATE_BITS;//EPOLLONESHOT標(biāo)記的處理
else if (!(epi->event.events & EPOLLET)) {
list_add_tail(&epi->rdllink, &ep->rdllist);//LT模式處理
ep_pm_stay_awake(epi);
}
}

6.LT模式和ET模式

epoll的兩種模式是留給用戶的發(fā)揮空間,也是個(gè)重點(diǎn)問題。

6.1 LT/ET的簡(jiǎn)單理解

默認(rèn)采用LT模式,LT支持阻塞和非阻塞套,ET模式只支持非阻塞套接字,其效率要高于LT模式,并且LT模式更加安全。
LT和ET模式下都可以通過epoll_wait方法來獲取事件,LT模式下將事件拷貝給用戶程序之后,如果沒有被處理或者未處理完,那么在下次調(diào)用時(shí)還會(huì)反饋給用戶程序,可以認(rèn)為數(shù)據(jù)不會(huì)丟失會(huì)反復(fù)提醒;
ET模式下如果沒有被處理或者未處理完,那么下次將不再通知到用戶程序,因此避免了反復(fù)被提醒,卻加強(qiáng)了對(duì)用戶程序讀寫的要求;

6.2 LT/ET的深入理解

上面的解釋在網(wǎng)上隨便找一篇都會(huì)講到,但是LT和ET真正使用起來,還是存在難度的。

6.2.1 LT的讀寫操作

LT對(duì)于read操作比較簡(jiǎn)單,有read事件就讀,讀多讀少都沒有問題,但是write就不那么容易了,一般來說socket在空閑狀態(tài)時(shí)發(fā)送緩沖區(qū)一定是不滿的,假如fd一直在監(jiān)控中,那么會(huì)一直通知寫事件,不勝其煩。
所以必須保證沒有數(shù)據(jù)要發(fā)送的時(shí)候,要把fd的寫事件監(jiān)控從epoll列表中刪除,需要的時(shí)候再加入回去,如此反復(fù)。
天下沒有免費(fèi)的午餐,總是無代價(jià)地提醒是不可能的,對(duì)應(yīng)write的過度提醒,需要使用者隨用隨加,否則將一直被提醒可寫事件。

6.2.2 ET的讀寫操作

fd可讀則返回可讀事件,若開發(fā)者沒有把所有數(shù)據(jù)讀取完畢,epoll不會(huì)再次通知read事件,也就是說如果沒有全部讀取所有數(shù)據(jù),那么導(dǎo)致epoll不會(huì)再通知該socket的read事件,事實(shí)上一直讀完很容易做到。
若發(fā)送緩沖區(qū)未滿,epoll通知write事件,直到開發(fā)者填滿發(fā)送緩沖區(qū),epoll才會(huì)在下次發(fā)送緩沖區(qū)由滿變成未滿時(shí)通知write事件。
ET模式下只有socket的狀態(tài)發(fā)生變化時(shí)才會(huì)通知,也就是讀取緩沖區(qū)由無數(shù)據(jù)到有數(shù)據(jù)時(shí)通知read事件,發(fā)送緩沖區(qū)由滿變成未滿通知write事件。

6.2.3 一道騰訊面試題

仿佛有點(diǎn)蒙圈,那來一道面試題看看:

使用Linux epoll模型的LT水平觸發(fā)模式,當(dāng)socket可寫時(shí),會(huì)不停的觸發(fā)socket可寫的事件,如何處理?

確實(shí)是一道很好的問題??!我們來分析領(lǐng)略一下其中深意。
這道題目對(duì)LT和ET考察比較深入,驗(yàn)證了前文說的LT模式write問題。
普通做法:
當(dāng)需要向socket寫數(shù)據(jù)時(shí),將該socket加入到epoll等待可寫事件。接收到socket可寫事件后,調(diào)用write或send發(fā)送數(shù)據(jù),當(dāng)數(shù)據(jù)全部寫完后, 將socket描述符移出epoll列表,這種做法需要反復(fù)添加和刪除。
改進(jìn)做法:
向socket寫數(shù)據(jù)時(shí)直接調(diào)用send發(fā)送,當(dāng)send返回錯(cuò)誤碼EAGAIN,才將socket加入到epoll,等待可寫事件后再發(fā)送數(shù)據(jù),全部數(shù)據(jù)發(fā)送完畢,再移出epoll模型,改進(jìn)的做法相當(dāng)于認(rèn)為socket在大部分時(shí)候是可寫的,不能寫了再讓epoll幫忙監(jiān)控。
上面兩種做法是對(duì)LT模式下write事件頻繁通知的修復(fù),本質(zhì)上ET模式就可以直接搞定,并不需要用戶層程序的補(bǔ)丁操作。

6.2.4 ET模式的線程饑餓問題

如果某個(gè)socket源源不斷地收到非常多的數(shù)據(jù),在試圖讀取完所有數(shù)據(jù)的過程中,有可能會(huì)造成其他的socket得不到處理,從而造成饑餓問題。
解決辦法:
為每個(gè)已經(jīng)準(zhǔn)備好的描述符維護(hù)一個(gè)隊(duì)列,這樣程序就可以知道哪些描述符已經(jīng)準(zhǔn)備好了但是并沒有被讀取完,然后程序定時(shí)或定量的讀取,如果讀完則移除,直到隊(duì)列為空,這樣就保證了每個(gè)fd都被讀到并且不會(huì)丟失數(shù)據(jù)。
流程如圖:

6.2.5 EPOLLONESHOT設(shè)置

A線程讀完某socket上數(shù)據(jù)后開始處理這些數(shù)據(jù),此時(shí)該socket上又有新數(shù)據(jù)可讀,B線程被喚醒讀新的數(shù)據(jù),造成2個(gè)線程同時(shí)操作一個(gè)socket的局面 ,EPOLLONESHOT保證一個(gè)socket連接在任一時(shí)刻只被一個(gè)線程處理。

6.2.6 LT和ET的選擇

通過前面的對(duì)比可以看到LT模式比較安全并且代碼編寫也更清晰,但是ET模式屬于高速模式,在處理大高并發(fā)場(chǎng)景使用得當(dāng)效果更好,具體選擇什么根據(jù)自己實(shí)際需要和團(tuán)隊(duì)代碼能力來選擇。
在知乎上有關(guān)于ET和LT選擇的對(duì)比,有很多大牛在其中發(fā)表觀點(diǎn),感興趣可以前往查閱。

7.epoll的驚群?jiǎn)栴}

如果你不知道什么是驚群效應(yīng),想象一下:

你在廣場(chǎng)喂鴿子,你只投喂了一份食物,卻引來一群鴿子爭(zhēng)搶,最終還是只有一只鴿子搶到了食物,對(duì)于其他鴿子來說是徒勞的。

這種想象在網(wǎng)絡(luò)編程中同樣存在。
在2.6.18內(nèi)核中accept的驚群?jiǎn)栴}已經(jīng)被解決了,但是在epoll中仍然存在驚群?jiǎn)栴},表現(xiàn)起來就是當(dāng)多個(gè)進(jìn)程/線程調(diào)用epoll_wait時(shí)會(huì)阻塞等待,當(dāng)內(nèi)核觸發(fā)可讀寫事件,所有進(jìn)程/線程都會(huì)進(jìn)行響應(yīng),但是實(shí)際上只有一個(gè)進(jìn)程/線程真實(shí)處理這些事件。
在epoll官方?jīng)]有正式修復(fù)這個(gè)問題之前,Nginx作為知名使用者采用全局鎖來限制每次可監(jiān)聽fd的進(jìn)程數(shù)量,每次只有1個(gè)可監(jiān)聽的進(jìn)程,后來在Linux 3.9內(nèi)核中增加了SO_REUSEPORT選項(xiàng)實(shí)現(xiàn)了內(nèi)核級(jí)的負(fù)載均衡,Nginx1.9.1版本支持了reuseport這個(gè)新特性,從而解決驚群?jiǎn)栴}。
EPOLLEXCLUSIVE是在2016年Linux 4.5內(nèi)核新添加的一個(gè) epoll 的標(biāo)識(shí),Ngnix 在 1.11.3 之后添加了NGX_EXCLUSIVE_EVENT選項(xiàng)對(duì)該特性進(jìn)行支持。EPOLLEXCLUSIVE標(biāo)識(shí)會(huì)保證一個(gè)事件發(fā)生時(shí)候只有一個(gè)線程會(huì)被喚醒,以避免多偵聽下的驚群?jiǎn)栴}。

8.巨人的肩膀

  • http://harlon.org/2018/04/11/networksocket5/
  • https://devarea.com/linux-io-multiplexing-select-vs-poll-vs-epoll/#.Xa0sDqqFOUk
  • https://jvns.ca/blog/2017/06/03/async-io-on-linux--select--poll--and-epoll/
  • https://zhuanlan.zhihu.com/p/78510741
  • http://www.cnhalo.net/2016/07/13/linux-epoll/
  • https://www.ichenfu.com/2017/05/03/proxy-epoll-thundering-herd/
  • https://github.com/torvalds/linux/commit/df0108c5da561c66c333bb46bfe3c1fc65905898
  • https://simpleyyt.com/2017/06/25/how-ngnix-solve-thundering-herd/


免責(zé)聲明:本文內(nèi)容由21ic獲得授權(quán)后發(fā)布,版權(quán)歸原作者所有,本平臺(tái)僅提供信息存儲(chǔ)服務(wù)。文章僅代表作者個(gè)人觀點(diǎn),不代表本平臺(tái)立場(chǎng),如有問題,請(qǐng)聯(lián)系我們,謝謝!

本站聲明: 本文章由作者或相關(guān)機(jī)構(gòu)授權(quán)發(fā)布,目的在于傳遞更多信息,并不代表本站贊同其觀點(diǎn),本站亦不保證或承諾內(nèi)容真實(shí)性等。需要轉(zhuǎn)載請(qǐng)聯(lián)系該專欄作者,如若文章內(nèi)容侵犯您的權(quán)益,請(qǐng)及時(shí)聯(lián)系本站刪除。
換一批
延伸閱讀

9月2日消息,不造車的華為或?qū)⒋呱龈蟮莫?dú)角獸公司,隨著阿維塔和賽力斯的入局,華為引望愈發(fā)顯得引人矚目。

關(guān)鍵字: 阿維塔 塞力斯 華為

倫敦2024年8月29日 /美通社/ -- 英國汽車技術(shù)公司SODA.Auto推出其旗艦產(chǎn)品SODA V,這是全球首款涵蓋汽車工程師從創(chuàng)意到認(rèn)證的所有需求的工具,可用于創(chuàng)建軟件定義汽車。 SODA V工具的開發(fā)耗時(shí)1.5...

關(guān)鍵字: 汽車 人工智能 智能驅(qū)動(dòng) BSP

北京2024年8月28日 /美通社/ -- 越來越多用戶希望企業(yè)業(yè)務(wù)能7×24不間斷運(yùn)行,同時(shí)企業(yè)卻面臨越來越多業(yè)務(wù)中斷的風(fēng)險(xiǎn),如企業(yè)系統(tǒng)復(fù)雜性的增加,頻繁的功能更新和發(fā)布等。如何確保業(yè)務(wù)連續(xù)性,提升韌性,成...

關(guān)鍵字: 亞馬遜 解密 控制平面 BSP

8月30日消息,據(jù)媒體報(bào)道,騰訊和網(wǎng)易近期正在縮減他們對(duì)日本游戲市場(chǎng)的投資。

關(guān)鍵字: 騰訊 編碼器 CPU

8月28日消息,今天上午,2024中國國際大數(shù)據(jù)產(chǎn)業(yè)博覽會(huì)開幕式在貴陽舉行,華為董事、質(zhì)量流程IT總裁陶景文發(fā)表了演講。

關(guān)鍵字: 華為 12nm EDA 半導(dǎo)體

8月28日消息,在2024中國國際大數(shù)據(jù)產(chǎn)業(yè)博覽會(huì)上,華為常務(wù)董事、華為云CEO張平安發(fā)表演講稱,數(shù)字世界的話語權(quán)最終是由生態(tài)的繁榮決定的。

關(guān)鍵字: 華為 12nm 手機(jī) 衛(wèi)星通信

要點(diǎn): 有效應(yīng)對(duì)環(huán)境變化,經(jīng)營業(yè)績穩(wěn)中有升 落實(shí)提質(zhì)增效舉措,毛利潤率延續(xù)升勢(shì) 戰(zhàn)略布局成效顯著,戰(zhàn)新業(yè)務(wù)引領(lǐng)增長 以科技創(chuàng)新為引領(lǐng),提升企業(yè)核心競(jìng)爭(zhēng)力 堅(jiān)持高質(zhì)量發(fā)展策略,塑強(qiáng)核心競(jìng)爭(zhēng)優(yōu)勢(shì)...

關(guān)鍵字: 通信 BSP 電信運(yùn)營商 數(shù)字經(jīng)濟(jì)

北京2024年8月27日 /美通社/ -- 8月21日,由中央廣播電視總臺(tái)與中國電影電視技術(shù)學(xué)會(huì)聯(lián)合牽頭組建的NVI技術(shù)創(chuàng)新聯(lián)盟在BIRTV2024超高清全產(chǎn)業(yè)鏈發(fā)展研討會(huì)上宣布正式成立。 活動(dòng)現(xiàn)場(chǎng) NVI技術(shù)創(chuàng)新聯(lián)...

關(guān)鍵字: VI 傳輸協(xié)議 音頻 BSP

北京2024年8月27日 /美通社/ -- 在8月23日舉辦的2024年長三角生態(tài)綠色一體化發(fā)展示范區(qū)聯(lián)合招商會(huì)上,軟通動(dòng)力信息技術(shù)(集團(tuán))股份有限公司(以下簡(jiǎn)稱"軟通動(dòng)力")與長三角投資(上海)有限...

關(guān)鍵字: BSP 信息技術(shù)
關(guān)閉
關(guān)閉