你值得擁有的linux下的網絡io 同步/異步/阻塞/非阻塞/BIO/NIO/AIO


Unix/Linux系統下IO主要分爲磁盤IO,網絡IO,我今天主要說一下對網絡IO的理解,網絡IO主要是socket套接字的讀(read)、寫(write),socket在Linux系統被抽象爲流(stream)。
在Unix/Linux系統下,IO分爲兩個不同階段:
等待數據準備好
從內核向進程複製數據

阻塞式I/O

阻塞式I/O(blocking I/O)是最簡單的一種,默認情況下,socket 套接字的系統調用都是阻塞的,我以recv/recvfrom 理解一下網絡IO的模型。當應用層的系統調用recv/recvfrom時,開啓Linux的系統調用,開始準備數據,然後將數據從內核態複製到用戶態,然後通知應用程序獲取數據,整個過程都是阻塞的。兩個階段都會被阻塞。
在這裏插入圖片描述

阻塞I/O下開發的後臺服務,一般都是通過多進程或者線程取出來請求,但是開闢進程或者線程是非常消耗系統資源的,當大量請求時,因爲需要開闢更多的進程或者線程有可能將系統資源耗盡,因此這種模式不適合高併發的系統。

非阻塞式I/O

非阻塞IO(non-blocking I/O)在調用後,內核馬上返回給進程,如果數據沒有準備好,就返回一個error ,進程可以先去幹其他事情,一會再次調用,直到數據準備好爲止,循環往返的系統調用的過程稱爲輪詢(pool),然後在從內核態將數據拷貝到用戶態,但是這個拷貝的過程還是阻塞的。

我還是以recv/recvfrom爲例說一下,首選需要將socket套接字設置成爲非阻塞,進程開始調用recv/recvfrom,如果內核沒有準備好數據時,立即返回給進程一個error碼(在Linux下是EAGINE的錯誤碼),進程接到error返回後,先去幹其他的事情,進入了輪詢,只等到數據準備好,然後將數據拷貝到用戶態。

需要通過ioctl 函數將socket套接字設置成爲非阻塞

ioctl(fd, FIONBIO, &nb);
在這裏插入圖片描述

I/O多路複用模型

I/O多路複用是Linux處理高併發的技術,Epoll比Select、Poll性能更優越,後面會講到它們的區別。優秀的高併發服務例如Nginx、Redis都是採用Epoll+Non-Blocking I/O的模式。

信號驅動式I/O

信號驅動式I/O是通過信號的方式通知數據準備好,然後再講數據拷貝到應用層,拷貝階段也是阻塞的。
在這裏插入圖片描述

異步I/O

異步I/O(asynchronous I/O或者AIO),數據準備通知和數據拷貝兩個階段都在內核態完成,兩個階段都不會阻塞,真正的異步I/O。

進程調用read/readfrom時,內核立刻返回,進程不會阻塞,進程可以去幹其他的事情,當內核通知進程數據已經完成後,進程直接可以處理數據,不需要再拷貝數據,因爲內核已經將數據從內核態拷貝到用戶態,進程可以直接處理數據。
在這裏插入圖片描述
Linux對AIO支持不好,因此使用的不是太廣泛。

同步和異步區別、阻塞和非阻塞的區別

同步和異步區別

對於這兩個東西,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請求結束。

一個異步I/O操作不會引起請求進程阻塞。

從這個官方定義中,不管是Blocking I/O還是Non-Blocking I/O,其實都是synchronous I/O。因爲它們一定都會阻塞在第二階段拷貝數據那裏。只有異步IO纔是異步的。

阻塞和非阻塞的區別

阻塞和非阻塞主要區別其實是在第一階段等待數據的時候。但是在第二階段,阻塞和非阻塞其實是沒有區別的。程序必須等待內核把收到的數據複製到進程緩衝區來。換句話說,非阻塞也不是真的一點都不”阻塞”,只是在不能立刻得到結果的時候不會傻乎乎地等在那裏而已。

IO多路複用

Select、Poll、Epoll的區別

Select、poll、epoll都是I/O多路複用的機制,I/O多路複用就是通過一種機制,一個進程可以監視多個文件描述符fd,一旦某個描述符就緒(一般是讀就緒或者寫就緒),能夠通知程序進行相應的讀寫操作。但select,poll,epoll本質上都是同步I/O,因爲他們都需要在讀寫事件就緒後自己負責進行讀寫,也就是說這個讀寫過程是阻塞的,而異步I/O則無需自己負責進行讀寫,異步I/O的實現會負責把數據從內核拷貝到用戶空間。

select

int select (int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
select 函數監視的文件描述符分3類,分別是writefds、readfds、和exceptfds。調用後select函數會阻塞,直到有描述符就緒(有數據 可讀、可寫、或者有except),或者超時(timeout指定等待時間,如果立即返回設爲null即可),函數返回。當select函數返回後,可以 通過遍歷fdset,來找到就緒的描述符。

select支持幾乎所有的平臺,跨平臺是它的優點。

select缺點是:1)單個進程支持監控的文件描述符數量有限,Linux下一般是1024,可以修改提升限制,但是會造成效率低下。2)select通過輪詢方式通知消息,效率比較低。

pollint poll (struct pollfd *fds, unsigned int nfds, int timeout);

不同於select使用三個位圖來表示三個fdset的方式,poll使用一個pollfd的指針實現。

struct pollfd {
int fd; /* file descriptor /
short events; /
requested events to watch /
short revents; /
returned events witnessed */
};

pollfd結構包含了要監視的event和發生的event,不再使用select“參數-值”傳遞的方式。同時,pollfd並沒有最大數量限制(但是數量過大後性能也是會下降)。和select函數一樣,poll返回後,需要輪詢pollfd來獲取就緒的描述符。

從上面看,select和poll都需要在返回後,通過遍歷文件描述符來獲取已經就緒的socket。事實上,同時連接的大量客戶端在一時刻可能只有很少的處於就緒狀態,因此隨着監視的描述符數量的增長,其效率也會線性下降。

epoll

epoll是在2.6內核中提出的,是之前的select和poll的增強版本,是Linux特有的。相對於select和poll來說,epoll更加靈活,沒有描述符限制。epoll使用一個文件描述符管理多個描述符,將用戶關係的文件描述符的事件存放到內核的一個事件表中,這樣在用戶空間和內核空間的copy只需一次。

int epoll_create(int size);//創建一個epoll的句柄,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);

執行epoll_create時,創建了紅黑樹和就緒list鏈表;執行epoll_ctl時,如果增加fd,則檢查在紅黑樹中是否存在,存在則立即返回,不存在則添加到紅黑樹中,然後向內核註冊回調函數,用於當中斷事件到來時向準備就緒的list鏈表中插入數據。執行epoll_wait時立即返回準備就緒鏈表裏的數據即可。

工作模式
  1. LT模式

LT(level triggered)是缺省的工作方式,並且同時支持block和no-block socket,在這種做法中,內核告訴你一個文件描述符是否就緒了,然後你可以對這個就緒的fd進行IO操作。如果你不做任何操作,內核還是會繼續通知你的。

  1. ET模式

ET(edge-triggered)是高速工作方式,只支持no-block socket。在這種模式下,當描述符從未就緒變爲就緒時,內核通過epoll告訴你。然後它會假設你知道文件描述符已經就緒,並且不會再爲那個文件描述符發送更多的就緒通知,直到你做了某些操作導致那個文件描述符不再爲就緒狀態了(比如,你在發送,接收或者接收請求,或者發送接收的數據少於一定量時導致了一個EWOULDBLOCK/EAGAIN 錯誤)。但是請注意,如果一直不對這個fd作IO操作(從而導致它再次變成未就緒),內核不會發送更多的通知(only once),因此必須把緩存區buff數據讀取完畢,不然就可能會丟數據。

ET模式在很大程度上減少了epoll事件被重複觸發的次數,因此效率要比LT模式高。epoll工作在ET模式的時候,必須使用非阻塞套接口,以避免由於一個文件句柄的阻塞讀/阻塞寫操作把處理多個文件描述符的任務餓死。

詳細對比
在這裏插入圖片描述

Nginx中Epoll+非阻塞IO

Nginx高併發主要是通過Epoll模式+非阻塞I/O

Nginx對I/O多路複用進行封裝,封裝在結構體struct ngx_event_s,同時將事件封裝在ngx_event_actions_t結構中。

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設置爲非阻塞

#(ngx_nonblocking(s) == -1) #nginx將fd設置非阻塞
#設置事件觸發

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);
 ...
}

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