首先什么是io復(fù)用呢?
現(xiàn)在web框架沒有不用到io復(fù)用的,這點(diǎn)是肯定的,不然并發(fā)真的很差。
那么io復(fù)用,復(fù)用的是什么呢?復(fù)用的真的不是io管道啥的,也不是io連接啥的,復(fù)用的是io線程。
這其實(shí)是操作系統(tǒng)的設(shè)計(jì)上的io并發(fā)不足的地方,然后自己給慢慢填了。
聽一段歷史:
當(dāng)時(shí)操作系統(tǒng)設(shè)計(jì)的時(shí)候呢? 按道理說是要操作系統(tǒng)去管理io這塊,也就是對(duì)我們屏蔽硬件。
然后我們就調(diào)用系統(tǒng)的接口,讓操作系統(tǒng)去幫我們讀取數(shù)據(jù)啥的,這似乎是非常的nice的時(shí)候。
操作系統(tǒng)就設(shè)計(jì)了兩種讀取方案:
第一種呢,比如自己調(diào)用io系統(tǒng)接口,然后線程陷入等待,當(dāng)有數(shù)據(jù)的時(shí)候呢,操作系統(tǒng)喚醒我們的線程

第二種,就是自己調(diào)用系統(tǒng)接口,然后操作系統(tǒng)告訴我們沒有,這個(gè)時(shí)候我們可以選擇做其他事情,或者等會(huì)再來問。

因?yàn)閷?duì)io是抽象的,這個(gè)時(shí)候呢,如果是磁盤文件系統(tǒng),這還是相當(dāng)nice的,因?yàn)槲覀冇植魂P(guān)心是網(wǎng)絡(luò)io還是文件io,讀取就完事了。
磁盤文件我們一般是對(duì)某個(gè)文件連續(xù)讀對(duì)吧,這樣這兩種方式工作都是很好的。
可能有些人一直認(rèn)為讀磁盤文件就是立即返回的,其實(shí)可以調(diào)用其他方式,當(dāng)磁盤有新數(shù)據(jù)的時(shí)候再返回,是可以的,在操作系統(tǒng)系統(tǒng)會(huì)補(bǔ)全這些機(jī)制的說明,這里不擴(kuò)展。
但是網(wǎng)絡(luò)io有個(gè)什么樣的場(chǎng)景呢?那就是連接數(shù)可能特別多,而且是每個(gè)間斷著讀取。
加入我們采用第一種方式,那么每一個(gè)連接,就需要一個(gè)線程去監(jiān)控,這樣就會(huì)很多線程,這樣的確會(huì)出現(xiàn)線程大爆炸,而且線程調(diào)度是需要開銷的。
那么我們采用第二種方式,第二種方式好像每個(gè)都需要一個(gè)線程,其實(shí)不需要哈。
我們可以排隊(duì)嘛,比如把socket放入到一個(gè)隊(duì)列中,然后不斷地輪訓(xùn),這樣其實(shí)就實(shí)現(xiàn)了io復(fù)用了,這個(gè)時(shí)候就有人問了,io復(fù)用這么簡(jiǎn)單嗎?
是的,這就是io復(fù)用了。呀呀呀,這就實(shí)現(xiàn)了,是的,這就實(shí)現(xiàn)了io線程復(fù)用,就是一個(gè)或者多個(gè)線程實(shí)現(xiàn)io的數(shù)據(jù)接收嘛。
但是性能不太行,不太行的地方在于兩點(diǎn),就是加入有5000個(gè)連接,只有2個(gè)有信息,那么得去輪訓(xùn)一遍,這效率真的很感人。
還要就是不斷地調(diào)用操作系統(tǒng)的陷入這個(gè)開銷也是很大的。
那么這個(gè)時(shí)候操作系統(tǒng)自己就得改進(jìn)了,明明就是操作系統(tǒng)自己知道有沒有消息,為啥不主動(dòng)告訴我呢?
那么操作系統(tǒng)自己做了一些改進(jìn)。
select 函數(shù).
#define __FD_SETSIZE 1024
typedef struct {
unsigned long fds_bits[__FD_SETSIZE / (8 * sizeof(long))];
} __kernel_fd_set;
struct timeval {
time_t tv_sec;
suseconds_t tv_usec;
};
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
通過select函數(shù)可以完成多個(gè)IO事件的監(jiān)聽。
函數(shù)參數(shù):
readfds:內(nèi)核檢測(cè)該集合中的IO是否可讀。如果想讓內(nèi)核幫忙檢測(cè)某個(gè)IO是否可讀,需要手動(dòng)把文件描述符加入該集合。
writefds:內(nèi)核檢測(cè)該集合中的IO是否可寫。同readfds,需要手動(dòng)加入
exceptfds:內(nèi)核檢測(cè)該集合中的IO是否異常。同readfds,需要手動(dòng)加入
nfds:以上三個(gè)集合中最大的文件描述符數(shù)值 + 1,例如集合是{0,1,4},那么 maxfd 就是 5
timeout:用戶線程調(diào)用select的超時(shí)時(shí)長。
設(shè)置成NULL,表示如果沒有 I/O 事件發(fā)生,則 select 一直等待下去。
設(shè)置為非0的值,這個(gè)表示等待固定的一段時(shí)間后從 select 阻塞調(diào)用中返回。
設(shè)置成 0,表示根本不等待,檢測(cè)完畢立即返回。
函數(shù)返回值:
大于0:成功,返回集合中已就緒的IO總個(gè)數(shù)
等于-1:調(diào)用失敗
等于0:沒有就緒的IO
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);
select 缺點(diǎn):
1.fd_set長度限制:由于fd_set本質(zhì)是一個(gè)數(shù)組,同時(shí)操作系統(tǒng)限制了其長度,導(dǎo)致其只能接受文件描述符數(shù)值在1024以內(nèi)的。
2. select函數(shù)的返回值是int,導(dǎo)致每次返回后,用戶得手動(dòng)檢測(cè)集合中哪些值被改為1了(被改為1的表示產(chǎn)生了IO就緒事件)
3. 每次調(diào)用 select,都需要把 fd 集合從用戶態(tài)拷貝到內(nèi)核態(tài),當(dāng)fd很多時(shí),開銷很大。
4. 每次內(nèi)核都是線性掃描整個(gè) fd_set,判斷是否有IO就緒事件,導(dǎo)致隨著監(jiān)控的描述符 fd 數(shù)量增長,其性能會(huì)線性下降
看到select缺點(diǎn)這么多,看著就不怎么好用。
這個(gè)其實(shí)就是批量檢測(cè),然后加了一個(gè)timeout,最后還得自己去輪訓(xùn)一遍,還是有點(diǎn)坑。
這時(shí)候可能我們都會(huì)想,自己設(shè)計(jì)都不會(huì)這么坑。其實(shí)這樣設(shè)計(jì)也是當(dāng)時(shí)的一個(gè)常規(guī)設(shè)計(jì),因?yàn)榧纫H珒?nèi)核的穩(wěn)定,又要維護(hù)陷入函數(shù)的簡(jiǎn)單,后面就能看到數(shù)據(jù)結(jié)構(gòu)之美了。
然后就到了poll了:
和 select 相比,它使用了不同的方式存儲(chǔ)文件描述符,也解決文件描述符的個(gè)數(shù)限制。
struct pollfd {
int fd;
short events;
short revents;
};
int poll(struct pollfd *fds, unsigned long nfds, int timeout);
函數(shù)參數(shù):
fds:struct pollfd類型的數(shù)組, 存儲(chǔ)了待檢測(cè)的文件描述符,struct pollfd有三個(gè)成員:
fd:委托內(nèi)核檢測(cè)的文件描述符
events:委托內(nèi)核檢測(cè)的fd事件(輸入、輸出、錯(cuò)誤),每一個(gè)事件有多個(gè)取值
revents:這是一個(gè)傳出參數(shù),數(shù)據(jù)由內(nèi)核寫入,存儲(chǔ)內(nèi)核檢測(cè)之后的結(jié)果
nfds:描述的是數(shù)組 fds 的大小
timeout: 指定poll函數(shù)的阻塞時(shí)長
-1:一直阻塞,直到檢測(cè)的集合中有就緒的IO事件,然后解除阻塞函數(shù)返回
0:不阻塞,不管檢測(cè)集合中有沒有已就緒的IO事件,函數(shù)馬上返回
大于0:表示 poll 調(diào)用方等待指定的毫秒數(shù)后返回
函數(shù)返回值:
-1:失敗
大于0:表示檢測(cè)的集合中已就緒的文件描述符的總個(gè)數(shù)
在 select 里面,文件描述符的個(gè)數(shù)已經(jīng)隨著 fd_set 的實(shí)現(xiàn)而固定,沒有辦法對(duì)此進(jìn)行配置;而在 poll 函數(shù)里,我們可以自由控制 pollfd 結(jié)構(gòu)的數(shù)組大小,從而突破select中面臨的文件描述符個(gè)數(shù)的限制。
這個(gè)pollfd就設(shè)計(jì)的人性化多了哈,有個(gè)fd然后里面是事件,看起來還是不錯(cuò)的,很面向?qū)ο蟆?/p>
poll 的實(shí)現(xiàn)和 select 非常相似,只是poll 使用 pollfd 結(jié)構(gòu),而 select 使用fd_set 結(jié)構(gòu),poll 解決了文件描述符數(shù)量限制的問題,但是同樣需要從用戶態(tài)拷貝所有的 fd 到內(nèi)核態(tài),
也需要線性遍歷所有的 fd 集合,所以它和 select 并沒有本質(zhì)上的區(qū)別。
所以呢,有人如果系統(tǒng)用poll和epoll比,這兩個(gè)思想就不一樣,下文可見,poll只是在select上進(jìn)行輕微的改進(jìn),和操作系統(tǒng)的溝通真的很感人,但是這個(gè)結(jié)構(gòu)化,還是很舒服的,尤其是寫了很多面向?qū)ο蟠a后
epoll 是 Linux kernel 2.6 之后引入的新 I/O 事件驅(qū)動(dòng)技術(shù),它解決了select、poll在性能上的缺點(diǎn),是目前IO多路復(fù)用的主流解決方案。
epoll 實(shí)現(xiàn):
int epoll_create(int size);
int epoll_create1(int flags);
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);
struct epoll_event {
__uint32_t events;
epoll_data_t data;
};
union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
};
typedef union epoll_data epoll_data_t;
1.epoll_creare、epoll_create1:這兩個(gè)函數(shù)的作用是一樣的,都是創(chuàng)建一個(gè)epoll實(shí)例。
2. epoll_ctl:在創(chuàng)建完 epoll 實(shí)例之后,可以通過調(diào)用 epoll_ctl 往 epoll 實(shí)例增加或刪除需要監(jiān)控的IO事件。
epoll_ctl:在創(chuàng)建完 epoll 實(shí)例之后,可以通過調(diào)用 epoll_ctl 往 epoll 實(shí)例增加或刪除需要監(jiān)控的IO事件。
epfd:調(diào)用 epoll_create 創(chuàng)建的 epoll 獲得的返回值,可以簡(jiǎn)單理解成是 epoll 實(shí)例的唯一標(biāo)識(shí)。
op:表示是增加還是刪除一個(gè)監(jiān)控事件,它有三個(gè)選項(xiàng)可供選擇:
EPOLL_CTL_ADD: 向 epoll 實(shí)例注冊(cè)文件描述符對(duì)應(yīng)的事件;
EPOLL_CTL_DEL:向 epoll 實(shí)例刪除文件描述符對(duì)應(yīng)的事件;
EPOLL_CTL_MOD: 修改文件描述符對(duì)應(yīng)的事件。
fd:需要注冊(cè)的事件的文件描述符。
epoll_event:表示需要注冊(cè)的事件類型,并且可以在這個(gè)結(jié)構(gòu)體里設(shè)置用戶需要的數(shù)據(jù)。
events:表示需要注冊(cè)的事件類型,可選值在下文的 Linux 常見網(wǎng)絡(luò)IO事件定義中列出
data:可以存放用戶自定義的數(shù)據(jù)。
人性化來了,可以自己刪除和添加了,不用一次性給了。
3.epoll_wait:調(diào)用者進(jìn)程調(diào)用該函數(shù)等待I/O 事件就緒。
epfd: epoll 實(shí)例的唯一標(biāo)識(shí)。
epoll_event:相關(guān)事件
maxevents:一個(gè)大于 0 的整數(shù),表示 epoll_wait 可以返回的最大事件值。
timeout:
-1:一直阻塞,直到檢測(cè)的集合中有就緒的IO事件,然后解除阻塞函數(shù)返回
0:不阻塞,不管檢測(cè)集合中有沒有已就緒的IO事件,函數(shù)馬上返回
大于0:表示 epoll 調(diào)用方等待指定的毫秒數(shù)后返回
在內(nèi)核中eventpoll結(jié)構(gòu)如下:
struct eventpoll {
wait_queue_head_t wq;
struct list_head rdllist;
struct rb_root_cached rbr;
}
wq:當(dāng)用戶進(jìn)程執(zhí)行了epoll_wait導(dǎo)致了阻塞后,用戶進(jìn)程就會(huì)存儲(chǔ)到這,等待后續(xù)數(shù)據(jù)準(zhǔn)備完成后喚醒。
rdllist:當(dāng)某個(gè)文件描述符就緒了后,就會(huì)從rbr移動(dòng)到這。其本質(zhì)是一個(gè)鏈表。
rbr:用戶調(diào)用epoll_ctl增加的文件描述都存儲(chǔ)在這,其本質(zhì)是一顆紅黑樹。
先不介紹紅黑樹,到了紅黑樹介紹的時(shí)候再畫圖,不然這個(gè)過程可能比較難理解為啥epoll這么高效,除非利用了紅黑樹,那么看下這個(gè)改變的地方。
- 我們多個(gè)進(jìn)程可以監(jiān)控利用epoll_ctl進(jìn)行監(jiān)控,其實(shí)一般就一個(gè),根據(jù)業(yè)務(wù)也可以多個(gè)
- 當(dāng)觸發(fā)事件的時(shí)候呢,就觸發(fā)的事件再rbr中移除,然后加入到了rdllist中(之所以要移除就說明觸發(fā)了,就不需要再繼續(xù)監(jiān)控該事件了唄)
- 當(dāng)rdllist里面有事件后,那么就會(huì)獲取到wq等待的進(jìn)程,進(jìn)行通知,也就是說epoll_wait就已經(jīng)返回了相關(guān)的epoll_event,不需要再輪訓(xùn)一遍了
相當(dāng)人性化哈,這是我們理解了我們作為用戶和操作系統(tǒng)直接的溝通橋梁塑造好了,所以效率也就高了。
那么問題來了,為啥epoll的效率這么高呢,除了解決和操作系統(tǒng)的溝通問題,還要什么經(jīng)過優(yōu)化呢,后續(xù)關(guān)于紅黑樹的介紹,黑紅樹之所以再這里能發(fā)揮作用就是因?yàn)槠漕l繁的加入和刪除,以及遍歷。
那么請(qǐng)問epoll是阻塞io,還是非阻塞io呢?
那肯定是阻塞io嘛,有個(gè)timeout當(dāng)事件到了就返回了,但是也屬于阻塞。
然后呢,我們?nèi)绻胏語言的時(shí)候,發(fā)現(xiàn)網(wǎng)絡(luò)io和磁盤io其實(shí)是用不同的庫,但是他們也的確可以用的底層函數(shù)read讀取,都抽象成文件了嘛,之所以用不同的庫是因?yàn)檫@兩個(gè)方向針對(duì)的場(chǎng)景的確不同,庫嘛,肯定是幫忙封裝好了的,用好庫比啥都重要。
轉(zhuǎn)自https://www.cnblogs.com/aoximin/p/18347523
該文章在 2025/2/14 11:30:52 編輯過