我對網(wǎng)絡(luò)IO的理解,你理解了嗎?
Unix/Linux系統(tǒng)下IO主要分為磁盤IO,網(wǎng)絡(luò)IO,我今天主要說一下對網(wǎng)絡(luò)IO的理解,網(wǎng)絡(luò)IO主要是socket套接字的讀(read)、寫(write),socket在Linux系統(tǒng)被抽象為流(stream)。
網(wǎng)絡(luò)IO模型
在Unix/Linux系統(tǒng)下,IO分為兩個不同階段:
- 等待數(shù)據(jù)準備好
- 從內(nèi)核向進程復(fù)制數(shù)據(jù)
阻塞式I/O
阻塞式I/O(blocking I/O)是最簡單的一種,默認情況下,socket 套接字的系統(tǒng)調(diào)用都是阻塞的,我以recv/recvfrom 理解一下網(wǎng)絡(luò)IO的模型。當(dāng)應(yīng)用層的系統(tǒng)調(diào)用recv/recvfrom時,開啟Linux的系統(tǒng)調(diào)用,開始準備數(shù)據(jù),然后將數(shù)據(jù)從內(nèi)核態(tài)復(fù)制到用戶態(tài),然后通知應(yīng)用程序獲取數(shù)據(jù),整個過程都是阻塞的。兩個階段都會被阻塞。

阻塞I/O模型
圖片來源于《Unix網(wǎng)絡(luò)編程卷1》
阻塞I/O下開發(fā)的后臺服務(wù),一般都是通過多進程或者線程取出來請求,但是開辟進程或者線程是非常消耗系統(tǒng)資源的,當(dāng)大量請求時,因為需要開辟更多的進程或者線程有可能將系統(tǒng)資源耗盡,因此這種模式不適合高并發(fā)的系統(tǒng)。
非阻塞式I/O
非阻塞IO(non-blocking I/O)在調(diào)用后,內(nèi)核馬上返回給進程,如果數(shù)據(jù)沒有準備好,就返回一個error ,進程可以先去干其他事情,一會再次調(diào)用,直到數(shù)據(jù)準備好為止,循環(huán)往返的系統(tǒng)調(diào)用的過程稱為輪詢(pool),然后在從內(nèi)核態(tài)將數(shù)據(jù)拷貝到用戶態(tài),但是這個拷貝的過程還是阻塞的。
我還是以recv/recvfrom為例說一下,首選需要將socket套接字設(shè)置成為非阻塞,進程開始調(diào)用recv/recvfrom,如果內(nèi)核沒有準備好數(shù)據(jù)時,立即返回給進程一個error碼(在Linux下是EAGINE的錯誤碼),進程接到error返回后,先去干其他的事情,進入了輪詢,只等到數(shù)據(jù)準備好,然后將數(shù)據(jù)拷貝到用戶態(tài)。
需要通過ioctl 函數(shù)將socket套接字設(shè)置成為非阻塞
- ioctl(fd, FIONBIO, &nb);

非阻塞I/O模型
圖片來源于《Unix網(wǎng)絡(luò)編程卷1》
非阻塞I/O的第一階段不會阻塞,但是第二個階段將數(shù)據(jù)從內(nèi)核態(tài)拷貝到用戶態(tài)時會有阻塞。在開發(fā)后臺服務(wù),由于非阻塞I/O需要通過輪詢的方式去知道是否數(shù)據(jù)準備好,輪詢的方式特別耗CPU的資源。
I/O多路復(fù)用
在Linux下提供一種I/O多路復(fù)用(I/O multiplexing)的機制,這個機制允許同時監(jiān)聽多個socket套接字描述符fd,一旦某個fd就緒(就緒一般是有數(shù)據(jù)可讀或者可寫)時,能夠通知進程進行相應(yīng)的讀寫操作。
在Linux下有三個I/O多路復(fù)用的函數(shù)Select、Poll、Epoll,但是它們都是同步IO,因為它們都需要在數(shù)據(jù)準備好后,讀寫數(shù)據(jù)是阻塞的。

I/O多路復(fù)用模型
圖片來源于《Unix網(wǎng)絡(luò)編程卷1》
I/O多路復(fù)用是Linux處理高并發(fā)的技術(shù),Epoll比Select、Poll性能更優(yōu)越,后面會講到它們的區(qū)別。優(yōu)秀的高并發(fā)服務(wù)例如Nginx、Redis都是采用Epoll+Non-Blocking I/O的模式。
信號驅(qū)動式I/O
信號驅(qū)動式I/O是通過信號的方式通知數(shù)據(jù)準備好,然后再講數(shù)據(jù)拷貝到應(yīng)用層,拷貝階段也是阻塞的。

信號驅(qū)動式I/O
圖片來源于《Unix網(wǎng)絡(luò)編程卷1》
異步I/O
異步I/O(asynchronous I/O或者AIO),數(shù)據(jù)準備通知和數(shù)據(jù)拷貝兩個階段都在內(nèi)核態(tài)完成,兩個階段都不會阻塞,真正的異步I/O。
進程調(diào)用read/readfrom時,內(nèi)核立刻返回,進程不會阻塞,進程可以去干其他的事情,當(dāng)內(nèi)核通知進程數(shù)據(jù)已經(jīng)完成后,進程直接可以處理數(shù)據(jù),不需要再拷貝數(shù)據(jù),因為內(nèi)核已經(jīng)將數(shù)據(jù)從內(nèi)核態(tài)拷貝到用戶態(tài),進程可以直接處理數(shù)據(jù)。

異步I/O模型
圖片來源于《Unix網(wǎng)絡(luò)編程卷1》
Linux對AIO支持不好,因此使用的不是太廣泛。
同步和異步區(qū)別、阻塞和非阻塞的區(qū)別
同步和異步區(qū)別
對于這兩個東西,POSIX其實是有官方定義的。A synchronous I/O operation causes the requesting process to be blocked until that I/O operation completes;An asynchronous I/O operation does not cause the requesting process to be blocked;
一個同步I/O操作會引起請求進程阻塞,只到這個I/O請求結(jié)束。
一個異步I/O操作不會引起請求進程阻塞。
從這個官方定義中,不管是Blocking I/O還是Non-Blocking I/O,其實都是synchronous I/O。因為它們一定都會阻塞在第二階段拷貝數(shù)據(jù)那里。只有異步IO才是異步的。

同步異步對比
圖片來源于知乎
阻塞和非阻塞的區(qū)別
阻塞和非阻塞主要區(qū)別其實是在第一階段等待數(shù)據(jù)的時候。但是在第二階段,阻塞和非阻塞其實是沒有區(qū)別的。程序必須等待內(nèi)核把收到的數(shù)據(jù)復(fù)制到進程緩沖區(qū)來。換句話說,非阻塞也不是真的一點都不”阻塞”,只是在不能立刻得到結(jié)果的時候不會傻乎乎地等在那里而已。
IO多路復(fù)用
Select、Poll、Epoll的區(qū)別
Select、poll、epoll都是I/O多路復(fù)用的機制,I/O多路復(fù)用就是通過一種機制,一個進程可以監(jiān)視多個文件描述符fd,一旦某個描述符就緒(一般是讀就緒或者寫就緒),能夠通知程序進行相應(yīng)的讀寫操作。但select,poll,epoll本質(zhì)上都是同步I/O,因為他們都需要在讀寫事件就緒后自己負責(zé)進行讀寫,也就是說這個讀寫過程是阻塞的,而異步I/O則無需自己負責(zé)進行讀寫,異步I/O的實現(xiàn)會負責(zé)把數(shù)據(jù)從內(nèi)核拷貝到用戶空間。
select
- int select (int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
select 函數(shù)監(jiān)視的文件描述符分3類,分別是writefds、readfds、和exceptfds。調(diào)用后select函數(shù)會阻塞,直到有描述符就緒(有數(shù)據(jù) 可讀、可寫、或者有except),或者超時(timeout指定等待時間,如果立即返回設(shè)為null即可),函數(shù)返回。當(dāng)select函數(shù)返回后,可以 通過遍歷fdset,來找到就緒的描述符。
select支持幾乎所有的平臺,跨平臺是它的優(yōu)點。
select缺點是:1)單個進程支持監(jiān)控的文件描述符數(shù)量有限,Linux下一般是1024,可以修改提升限制,但是會造成效率低下。2)select通過輪詢方式通知消息,效率比較低。
poll
- int poll (struct pollfd *fds, unsigned int nfds, int timeout);
不同于select使用三個位圖來表示三個fdset的方式,poll使用一個pollfd的指針實現(xiàn)。
- struct pollfd {
- int fd; /* file descriptor */
- short events; /* requested events to watch */
- short revents; /* returned events witnessed */
- };
pollfd結(jié)構(gòu)包含了要監(jiān)視的event和發(fā)生的event,不再使用select“參數(shù)-值”傳遞的方式。同時,pollfd并沒有最大數(shù)量限制(但是數(shù)量過大后性能也是會下降)。和select函數(shù)一樣,poll返回后,需要輪詢pollfd來獲取就緒的描述符。
從上面看,select和poll都需要在返回后,通過遍歷文件描述符來獲取已經(jīng)就緒的socket。事實上,同時連接的大量客戶端在一時刻可能只有很少的處于就緒狀態(tài),因此隨著監(jiān)視的描述符數(shù)量的增長,其效率也會線性下降。
epoll
epoll是在2.6內(nèi)核中提出的,是之前的select和poll的增強版本,是Linux特有的。相對于select和poll來說,epoll更加靈活,沒有描述符限制。epoll使用一個文件描述符管理多個描述符,將用戶關(guān)系的文件描述符的事件存放到內(nèi)核的一個事件表中,這樣在用戶空間和內(nèi)核空間的copy只需一次。
- int epoll_create(int size);//創(chuàng)建一個epoll的句柄,size用來告訴內(nèi)核這個監(jiān)聽的數(shù)目一共有多大
- 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);
執(zhí)行epoll_create時,創(chuàng)建了紅黑樹和就緒list鏈表;執(zhí)行epoll_ctl時,如果增加fd,則檢查在紅黑樹中是否存在,存在則立即返回,不存在則添加到紅黑樹中,然后向內(nèi)核注冊回調(diào)函數(shù),用于當(dāng)中斷事件到來時向準備就緒的list鏈表中插入數(shù)據(jù)。執(zhí)行epoll_wait時立即返回準備就緒鏈表里的數(shù)據(jù)即可。
工作模式
1. LT模式
LT(level triggered)是缺省的工作方式,并且同時支持block和no-block socket,在這種做法中,內(nèi)核告訴你一個文件描述符是否就緒了,然后你可以對這個就緒的fd進行IO操作。如果你不做任何操作,內(nèi)核還是會繼續(xù)通知你的。
2. ET模式
ET(edge-triggered)是高速工作方式,只支持no-block socket。在這種模式下,當(dāng)描述符從未就緒變?yōu)榫途w時,內(nèi)核通過epoll告訴你。然后它會假設(shè)你知道文件描述符已經(jīng)就緒,并且不會再為那個文件描述符發(fā)送更多的就緒通知,直到你做了某些操作導(dǎo)致那個文件描述符不再為就緒狀態(tài)了(比如,你在發(fā)送,接收或者接收請求,或者發(fā)送接收的數(shù)據(jù)少于一定量時導(dǎo)致了一個EWOULDBLOCK/EAGAIN 錯誤)。但是請注意,如果一直不對這個fd作IO操作(從而導(dǎo)致它再次變成未就緒),內(nèi)核不會發(fā)送更多的通知(only once),因此必須把緩存區(qū)buff數(shù)據(jù)讀取完畢,不然就可能會丟數(shù)據(jù)。
ET模式在很大程度上減少了epoll事件被重復(fù)觸發(fā)的次數(shù),因此效率要比LT模式高。epoll工作在ET模式的時候,必須使用非阻塞套接口,以避免由于一個文件句柄的阻塞讀/阻塞寫操作把處理多個文件描述符的任務(wù)餓死。
詳細對比

三種I/O多路復(fù)用對比
Nginx中Epoll+非阻塞IO
Nginx高并發(fā)主要是通過Epoll模式+非阻塞I/O
Nginx對I/O多路復(fù)用進行封裝,封裝在結(jié)構(gòu)體struct ngx_event_s,同時將事件封裝在ngx_event_actions_t結(jié)構(gòu)中。
- typedef struct {
- ngx_int_t (*add)(ngx_event_t *ev, ngx_int_t event, ngx_uint_t flags);
- ngx_int_t (*del)(ngx_event_t *ev, ngx_int_t event, ngx_uint_t flags);
- ngx_int_t (*enable)(ngx_event_t *ev, ngx_int_t event, ngx_uint_t flags);
- ngx_int_t (*disable)(ngx_event_t *ev, ngx_int_t event, ngx_uint_t flags);
- ngx_int_t (*add_conn)(ngx_connection_t *c);
- ngx_int_t (*del_conn)(ngx_connection_t *c, ngx_uint_t flags);
- ngx_int_t (*notify)(ngx_event_handler_pt handler);
- ngx_int_t (*process_events)(ngx_cycle_t *cycle, ngx_msec_t timer,
- ngx_uint_t flags);
- ngx_int_t (*init)(ngx_cycle_t *cycle, ngx_msec_t timer);
- void (*done)(ngx_cycle_t *cycle);
- } ngx_event_actions_t;
初始化epoll句柄
- static ngx_int_t
- ngx_epoll_init(ngx_cycle_t *cycle, ngx_msec_t timer)
- {
- ngx_epoll_conf_t *epcf;
- epcf = ngx_event_get_conf(cycle->conf_ctx, ngx_epoll_module);
- if (ep == -1) {
- ep = epoll_create(cycle->connection_n / 2);
- if (ep == -1) {
- ngx_log_error(NGX_LOG_EMERG, cycle->log, ngx_errno,
- "epoll_create() failed");
- return NGX_ERROR;
- }
- ...
- }
- }
將fd設(shè)置為非阻塞
- (ngx_nonblocking(s) == -1) #nginx將fd設(shè)置非阻塞
設(shè)置事件觸發(fā)
- static ngx_int_t
- ngx_epoll_add_event(ngx_event_t *ev, ngx_int_t event, ngx_uint_t flags)
- {
- int op;
- uint32_t events, prev;
- ngx_event_t *e;
- ngx_connection_t *c;
- struct epoll_event ee;
- c = ev->data;
- events = (uint32_t) event;
- if (event == NGX_READ_EVENT) {
- e = c->write;
- prev = EPOLLOUT;
- #if (NGX_READ_EVENT != EPOLLIN|EPOLLRDHUP)
- events = EPOLLIN|EPOLLRDHUP;
- #endif
- } else {
- e = c->read;
- prev = EPOLLIN|EPOLLRDHUP;
- #if (NGX_WRITE_EVENT != EPOLLOUT)
- events = EPOLLOUT;
- #endif
- }
- if (e->active) {
- op = EPOLL_CTL_MOD;
- events |= prev;
- } else {
- op = EPOLL_CTL_ADD;
- }
- #if (NGX_HAVE_EPOLLEXCLUSIVE && NGX_HAVE_EPOLLRDHUP)
- if (flags & NGX_EXCLUSIVE_EVENT) {
- events &= ~EPOLLRDHUP;
- }
- #endif
- ee.events = events | (uint32_t) flags;
- ee.data.ptr = (void *) ((uintptr_t) c | ev->instance);
- ngx_log_debug3(NGX_LOG_DEBUG_EVENT, ev->log, 0,
- "epoll add event: fd:%d op:%d ev:%08XD",
- c->fd, op, ee.events);
- if (epoll_ctl(ep, op, c->fd, &ee) == -1) {
- ngx_log_error(NGX_LOG_ALERT, ev->log, ngx_errno,
- "epoll_ctl(%d, %d) failed", op, c->fd);
- return NGX_ERROR;
- }
- ev->active = 1;
- #if 0
- ev->oneshot = (flags & NGX_ONESHOT_EVENT) ? 1 : 0;
- #endif
- return NGX_OK;
- }
處理就緒的事件
- static ngx_int_t
- ngx_epoll_process_events(ngx_cycle_t *cycle, ngx_msec_t timer, ngx_uint_t flags)
- {
- int events;
- uint32_t revents;
- ngx_int_t instance, i;
- ngx_uint_t level;
- ngx_err_t err;
- ngx_event_t *rev, *wev;
- ngx_queue_t *queue;
- ngx_connection_t *c;
- /* NGX_TIMER_INFINITE == INFTIM */
- ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
- "epoll timer: %M", timer);
- events = epoll_wait(ep, event_list, (int) nevents, timer);
- ...
- }