301重定向到新網(wǎng)站廣州網(wǎng)站優(yōu)化方式
通常所謂的IO,其本質(zhì)就是等待通信和進行通信,即IO = 等 + 拷貝。
那么想要做到高效的IO,就要在單位時間內(nèi),減少“等”的比重。
一.五種IO模型
- 阻塞 IO: 在內(nèi)核將數(shù)據(jù)準備好之前, 系統(tǒng)調(diào)用會一直等待. 所有的套接字, 默認都是阻塞方式。阻塞 IO 是最常見的 IO 模型。
- 非阻塞 IO: 如果內(nèi)核還未將數(shù)據(jù)準備好, 系統(tǒng)調(diào)用仍然會直接返回, 并且返回EWOULDBLOCK 錯誤碼. 非阻塞 IO 往往需要程序員循環(huán)的方式反復(fù)嘗試讀寫文件描述符, 這個過程稱為輪詢. 這對 CPU 來說是較大的浪費, 一般只有特定場景下才使用。
- 信號驅(qū)動 IO: 內(nèi)核將數(shù)據(jù)準備好的時候, 使用 SIGIO 信號通知應(yīng)用程序進行 IO 操作.
- IO 多路轉(zhuǎn)接: 和阻塞 IO 類似. 核心在于 IO 多路轉(zhuǎn)接能夠同時等待多個文件描述符的就緒狀態(tài)。
- 異步 IO: 由內(nèi)核在數(shù)據(jù)拷貝完成時, 通知應(yīng)用程序(而信號驅(qū)動是告訴應(yīng)用程序何時可以開始拷貝數(shù)據(jù))。
此處展開分享一下非阻塞IO。?
二.非阻塞IO
系統(tǒng)和網(wǎng)絡(luò)中的文件描述符,其默認情況下都是阻塞IO,下面來看怎么將其設(shè)置為非阻塞IO。
1.fcntl函數(shù)
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );
fcntl 函數(shù)有 5 種功能:
- 復(fù)制一個現(xiàn)有的描述符(cmd=F_DUPFD).
- 獲得/設(shè)置文件描述符標記(cmd=F_GETFD 或 F_SETFD).
- 獲得/設(shè)置文件狀態(tài)標記(cmd=F_GETFL 或 F_SETFL).
- 獲得/設(shè)置異步 I/O 所有權(quán)(cmd=F_GETOWN 或 F_SETOWN).
- 獲得/設(shè)置記錄鎖(cmd=F_GETLK,F_SETLK 或 F_SETLKW).
此處只是用第三種功能, 獲取/設(shè)置文件狀態(tài)標記, 就可以將一個文件描述符設(shè)置為非阻塞。
2.代碼實現(xiàn)非阻塞
void SetNoBlock(int fd)
{int fl = fcntl(fd, F_GETFL);if (fl < 0){perror("fcntl");return;}fcntl(fd, F_SETFL, fl | O_NONBLOCK);
}
使用 F_GETFL 將當前的文件描述符的屬性取出來(這是一個位圖)。
然后再使用 F_SETFL 將文件描述符設(shè)置回去. 設(shè)置回去的同時, 加上一個?O_NONBLOCK 參數(shù),表示為非阻塞。
前邊提到,在非阻塞IO下,如果數(shù)據(jù)沒有就緒,那么IO就會以出錯的形式返回,那么如何區(qū)分到底是數(shù)據(jù)沒有就緒,還是真的出錯了呢???
通過判斷errno錯誤碼,如果錯誤碼為EWOULDBLOCK,表示數(shù)據(jù)沒有就緒,此時可以設(shè)計程序去做其他事,并通過輪詢方式去檢測數(shù)據(jù)是否就緒,反之則為真的出錯,程序退出。
此外,如果進程長期阻塞,可能會收到系統(tǒng)的信號,中斷程序運行,此時返回的錯誤碼為EINTR,所以如果不想程序被系統(tǒng)中斷,就可以通過此錯誤碼在做判斷。
三.多路轉(zhuǎn)接
多路轉(zhuǎn)接,即等待多個fd上的新事件就緒,然后通知程序員,事件已經(jīng)就緒,可以進行IO拷貝了。
1.select
(1)概述
系統(tǒng)提供 select 函數(shù)來實現(xiàn)多路復(fù)用輸入/輸出模型。
- select 系統(tǒng)調(diào)用是用來讓我們的程序監(jiān)視多個文件描述符的狀態(tài)變化的;
- 程序會停在 select 這里等待,直到被監(jiān)視的文件描述符有一個或多個發(fā)生了狀態(tài)改變;
IO = 等 + 拷貝,select負責的就是等待,并且是等待多個新事件的到了。
(2)接口
#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
參數(shù):
- nfds :是需要監(jiān)視的最大的文件描述符值+1;
- rdset,wrset,exset :分別對應(yīng)于需要檢測的可讀文件描述符的集合,可寫文件描述符的集合及異常文件描述符的集合,是輸入輸出型參數(shù)。
- timeout :為結(jié)構(gòu)體?timeval類型,用來設(shè)置 select()的等待時間。
fd_set結(jié)構(gòu):
這個結(jié)構(gòu)就是一個整數(shù)數(shù)組, 更嚴格的說,是一個 "位圖",使用位圖中對應(yīng)的位來表示要監(jiān)視的文件描述符。
- 輸入時,比特位的位置表示文件描述符的編號,比特位的內(nèi)容表示是否關(guān)心該fd事件。
- 輸出時,比特位的位置表示文件描述符的編號,比特位的內(nèi)容表示對應(yīng)的fd事件是否發(fā)生。
下面是OS提供了一組操作 fd_set 的接口, 來比較方便的操作位圖:
void FD_CLR(int fd, fd_set *set); // 用來清除描述詞組 set 中相關(guān) fd 的位
int FD_ISSET(int fd, fd_set *set); // 用來測試描述詞組 set 中相關(guān) fd 的位是否為真
void FD_SET(int fd, fd_set *set); // 用來設(shè)置描述詞組 set 中相關(guān) fd 的位
void FD_ZERO(fd_set *set); // 用來清除描述詞組 set 的全部位
timeval結(jié)構(gòu):
struct timeval
{
? ? ? ? __time_t tv_sec;//秒
? ? ? ? __suseconds_t tv_usec;//微秒
}
timeval 結(jié)構(gòu)用于描述一段時間長度,比如(5,0)則表示在0-5秒內(nèi);如果在這個時間內(nèi),需要監(jiān)視的描述符沒有事件發(fā)生則函數(shù)返回,返回值為 0。
參數(shù) timeout 取值:
- nullptr:則表示 select()沒有 timeout,select 將一直被阻塞,直到某個文件描述符上發(fā)生了事件;
- 0:僅檢測描述符集合的狀態(tài),然后立即返回,并不等待外部事件的發(fā)生,即按照非阻塞輪詢的方式。
- 特定的時間值:如果在指定的時間段里沒有事件發(fā)生,select 將超時返回。
函數(shù)返回值:
- 執(zhí)行成功則返回文件描述詞狀態(tài)已改變的個數(shù)。
- 如果返回 0 代表在描述詞狀態(tài)改變前已超過 timeout 時間,沒有返回。
- 當有錯誤發(fā)生時則返回-1,錯誤原因存于 errno,此時參數(shù) readfds,writefds, exceptfds 和 timeout 的值變成不可預(yù)測。
(3)缺點
每次調(diào)用 select,都需要手動設(shè)置 fd 集合, 從接口使用角度來說也非常不便。
每次調(diào)用 select,都需要把 fd 集合從用戶態(tài)拷貝到內(nèi)核態(tài),這個開銷在 fd 很多時會很大。
同時每次調(diào)用 select 都需要在內(nèi)核遍歷傳遞進來的所有 fd,這個開銷在 fd 很多時也很大。
select 支持的文件描述符數(shù)量太小。
2.poll
(1)概述
poll的作用與select完全相同,也是等待多個fd,等待fd上的新事件就緒,隨后派發(fā)事件,可以理解為是select的優(yōu)化版本。
(2)接口
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
參數(shù):
-
?fds是一個 poll 函數(shù)監(jiān)聽的結(jié)構(gòu)列表. 每一個元素中, 包含了三部分內(nèi)容: 文件描述符, 監(jiān)聽的事件集合, 返回的事件集合。
-
nfds 表示 fds 數(shù)組的長度。
- timeout:以毫秒為單位,設(shè)定的超時時間,設(shè)為0表示非阻塞,-1表示阻塞。
pollfd結(jié)構(gòu)體:
struct pollfd {
int
fd; /* file descriptor */
short events; /* requested events */
short revents; /* returned events */
};
同樣是位圖結(jié)構(gòu),short16位,每一位可代表一個事件,events和revents的取值:?
?這些事件都是宏,且分別表示為不同的二進制位,因此可以自由組合搭配,形成事件集合。
返回值:
- 大于0,表示有幾個fd就緒。
- 等于0,超時。
- 小于0,poll出錯。
(3)優(yōu)點
pollfd 結(jié)構(gòu)包含了要監(jiān)視的 event 和發(fā)生的 event,不再使用 select“參數(shù)-值”傳遞的方式. 接口使用比 select 更方便。
poll 并沒有最大數(shù)量限制 (但是數(shù)量過大后性能也是會下降)。
3.epoll
(1)概述
epoll是除了select和poll之外,公認為 Linux?下性能最好的多路 I/O 就緒通知方法。
(2)接口
使用epoll接口需要包含頭文件?#include<sys/epoll.h>。?
int epoll_create(int size);
創(chuàng)建一個 epoll 的句柄.
- size 參數(shù)可以被忽略。
- 用完之后, 必須調(diào)用 close()關(guān)閉。
返回值epfd供接下來的函數(shù)使用。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epoll 的事件注冊函數(shù). 它不同于 select()是在監(jiān)聽事件時告訴內(nèi)核要監(jiān)聽什么類型的事件, 而是在這里先注冊要監(jiān)聽的事件類型。
epoll_ctl在底層會將用戶讓內(nèi)核關(guān)心的fd及其事件添加進由內(nèi)核構(gòu)成的紅黑樹中進行維護。
- 第一個參數(shù)是 epoll_create()的返回值(epoll 的句柄).
- 第二個參數(shù)表示動作,用三個宏來表示.
- 第三個參數(shù)是需要監(jiān)聽的 fd.
- 第四個參數(shù)是告訴內(nèi)核需要監(jiān)聽什么事.
第二個參數(shù)的取值:
- EPOLL_CTL_ADD:注冊新的 fd 到 epfd 中;
- EPOLL_CTL_MOD:修改已經(jīng)注冊的 fd 的監(jiān)聽事件;
- EPOLL_CTL_DEL:從 epfd 中刪除一個 fd;
struct epoll_event 結(jié)構(gòu)如下:
typedef union epoll_data {
????????void *ptr;
????????int fd;
????????uint32_t u32;
????????uint64_t u64;
} epoll_data_t;????????struct epoll_event {
????????uint32_t events;/* Epoll events */
????????epoll_data_t data;/* User data variable */
};
需要關(guān)注一下epoll_data_t結(jié)構(gòu)體中的fd成員,其要存放事件的fd,當后續(xù)事件就緒時,需要通過該fd來獲取事件。
其中events同樣為位圖結(jié)構(gòu),可以是以下幾個宏的集合:
- EPOLLIN : 表示對應(yīng)的文件描述符可以讀 (包括對端 SOCKET 正常關(guān)閉);
- EPOLLOUT : 表示對應(yīng)的文件描述符可以寫;
- EPOLLPRI : 表示對應(yīng)的文件描述符有緊急的數(shù)據(jù)可讀 (這里應(yīng)該表示有帶外數(shù)據(jù)到來);
- EPOLLERR : 表示對應(yīng)的文件描述符發(fā)生錯誤;
- EPOLLHUP : 表示對應(yīng)的文件描述符被掛斷;
- EPOLLET : 將 EPOLL 設(shè)為邊緣觸發(fā)(Edge Triggered)模式, 這是相對于水平觸發(fā)(Level Triggered)來說的.
- EPOLLONESHOT:只監(jiān)聽一次事件, 當監(jiān)聽完這次事件之后, 如果還需要繼續(xù)監(jiān)聽這個 socket 的話, 需要再次把這個 socket 加入到 EPOLL 隊列里.
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
收集在 epoll 監(jiān)控的事件中已經(jīng)發(fā)送的事件.
epoll_wait會檢測內(nèi)核中構(gòu)成的就緒隊列中是否有事件已經(jīng)就緒,?并將已經(jīng)就緒的事件按照嚴格順序放入我們定義的用戶緩沖區(qū)數(shù)組中。
- 參數(shù) events 是分配好的 epoll_event 結(jié)構(gòu)體數(shù)組. epoll 將會把發(fā)生的事件賦值到 events 數(shù)組中 (events 不可以是空指針,內(nèi)核只負責把數(shù)據(jù)復(fù)制到這個 events 數(shù)組中,不會去幫助我們在用戶態(tài)中分配內(nèi)存).
- maxevents 告訴內(nèi)核這個 events 有多大,這個 maxevents 的值不能大于創(chuàng)建 epoll_create()時的 size.
- 參數(shù) timeout 是超時時間 (毫秒,0 會立即返回,-1 是永久阻塞).
- 如果函數(shù)調(diào)用成功,返回對應(yīng) I/O 上已準備好的文件描述符數(shù)目,如返回 0 表示已超時, 返回小于 0 表示函數(shù)失敗.
對應(yīng)的事件節(jié)點,會同時包含紅黑樹和就緒隊列兩個指針,從而使得該節(jié)點既可以存在于紅黑樹中,也可以存在于就緒隊列中,從而無需新建新節(jié)點來進行轉(zhuǎn)移。?
(3)LT工作模式
LT即水平觸發(fā) Level Triggered 工作模式。
epoll 默認狀態(tài)下就是 LT 工作模式.
當 epoll 檢測到 socket 上事件就緒的時候, 可以不立刻進行處理,或者只處理一部分,當緩沖區(qū)還有事件未處理時,epoll_wait 會不斷地立刻返回并通知 socket 讀事件就緒,直到緩沖區(qū)上所有的數(shù)據(jù)都被處理完, epoll_wait 才不會立刻返回。
支持阻塞讀寫和非阻塞讀寫。
(4)ET工作模式
ET即邊緣觸發(fā) Edge Triggered 工作模式。
在第 1 步將 socket 添加到 epoll 描述符的時候使用?EPOLLET 標志, epoll 將進入 ET 工作模式。
當 epoll 檢測到 socket 上事件就緒時, 必須立刻處理。如果未處理或未一次性處理完,在第二次調(diào)用epoll_wait 的時候, epoll_wait 不會再返回了。
也就是說, ET 模式下, 文件描述符上的事件就緒后, 只有一次處理機會。
ET 的性能比 LT 性能更高( epoll_wait 返回的次數(shù)少了很多). Nginx 默認采用 ET 模式使用 epoll。
只支持非阻塞的讀寫。
LT 是 epoll 的默認行為.
使用 ET 能夠減少 epoll 觸發(fā)的次數(shù). 但是代價就是強逼著程序猿一次響應(yīng)就緒過程中就把所有的數(shù)據(jù)都處理完.
相當于一個文件描述符就緒之后, 不會反復(fù)被提示就緒, 看起來就比 LT 更高效一些. 但是在 LT 情況下如果也能做到每次就緒的文件描述符都立刻處理, 不讓這個就緒被重復(fù)提示的話, 其實性能也是一樣的.
另一方面, ET 的代碼復(fù)雜程度更高。