1. 程式人生 > >阻塞與非阻塞的IO網路讀寫

阻塞與非阻塞的IO網路讀寫

int setnonblocking(int fd) {
int old_option = fcntl(fd, F_GETFL);
int new_option = old_option | O_NONBLOCK;
fcntl(fd, F_SETFL, new_option);
return old_option;
}

為什麼要加上呢。是為了效率。下面詳細說一下阻塞和非阻塞。

基本概念:

阻塞IO: 必須做完IO操作才會返回。

非阻塞IO:操作成功與否,都會返回,需要通過其他方式判斷具體操作是否成功。

阻塞與非阻塞的區別:沒有資料到達的時候,是否立刻返回。

讀(read, recv, msgrcv):

注意,這裡的讀,只是負責把資料從底層系統快取copy到我們指定的位置。實際的資料到達是系統做的。

阻塞情況(read, recv, msgrcv的行為):

  1. 如果沒有資料,會一直等待;

  2. 有資料時候會讀到使用者指定的快取區,但是如果資料量比較少,少於引數指定的大小,read也會立即返回,而不會一直等到資料足夠。

阻塞讀的原則:資料不超過指定長度的時候,有多少讀多少,沒有資料就會一直等待。

所以一般情況下,都需要採用迴圈讀的方式,因為一次read不能保證讀完需要的全部資料。

非阻塞情況(read, recv, msgrcv的行為)

  1. 沒有資料,就立即返回;

  2. 有資料,也是採用有多少讀多少的方式來處理。

所以,read完一次,要判斷讀到的資料長度或者錯誤碼再決定是否再次讀取。注意這裡的EAGAIN錯誤碼是需要繼續讀取,而返回0是對方已關閉連線。
複製程式碼

對非阻塞socket而言,EAGAIN不是一種錯誤。在VxWorks和Windows上,EAGAIN的名字叫做EWOULDBLOCK。錯誤資訊為Resource temporarily unavailable,errno程式碼為11(EAGAIN)。

如果出現EINTR即errno為4,錯誤描述Interrupted system call,操作也應該繼續。

EINTR指操作被中斷喚醒,需要重新讀/寫。
而EAGAGIN不需要重新讀/寫已經操作的資料。

最後,如果recv的返回值為0,那表明連線已經斷開,我們的接收操作也應該結束。

複製程式碼

綜上,對於讀而言,阻塞與非阻塞的區別在於,沒有資料到達的時候是否立刻返回。

而recv函式有一個 MSG_WAITALL的引數。

recv(sockfd, buff, buff_size, MSG_WAITALL);

這個引數意味著recv會爭取等到資料填滿buff_size再返回,但是如果有中斷的情況, recv還是會被大端,造成沒有讀完buff_size的長度。

所以即使採用了recv+MSG_WAITALL的方式,還是要迴圈讀取,當然在大多數情況下是能讀滿的。

注意:MSG_WAITALL只能在阻塞模式下使用,和非阻塞模式不能同時使用。

寫(write/send/msgsnd)的本質也是把使用者態資料copy到系統底層去,然後由系統進行傳送和實際寫操作。只要完成了copy,就意味著寫完成。

阻塞情況(write/send/msgsnd的行為)

與阻塞讀有多少讀多少不同的是,阻塞寫會一直阻塞,直到所有資料都完成,再返回。

這是因為,讀的時候不知道需要讀多少,防止一直等不到足夠的資料;而寫的時候是知道要寫多少資料的。不過也可能被中斷,大多數情況是能夠寫完的。

非阻塞情況(write/send/msgsnd的行為)

非阻塞寫,就是有多少寫多少。能夠寫多少是根據本地網路擁塞情況為標準的,當網路擁塞嚴重的時候,網路層沒有足夠的記憶體來進行寫操作,就會出現寫不完的情況;這時候,阻塞寫除非被中斷,都會等到資料都寫完;而非阻塞寫,就是能寫多少算多少。

IO模式設定方式
Socket:

方法1: 文章開始的方式,對flags加O_NONBLOCK; (注:如果想設定成非阻塞,這樣:flags&~O_NONBLOCK)

方法2: recv, send函式的引數,最後一個引數設定成 MSG_DONTWAIT,如下:

recv(sockfd, buff, buff_size, MSG_DONTWAIT);
send(sockfd, buff, buff_size, MSG_DONTWAIT);

對當次的函式,為非阻塞。

普通檔案:

方法1: open函式的第二個引數加上 O_NONBLOCK,函式說明如下:
複製程式碼

open函式用來開啟或建立一個檔案,若成功返回檔案描述符,否則返回-1。
pathname是要開啟或建立檔案的名字。
oflag引數是下列一個或多個常量執行按位或運算的結果殺
O_RDONLY  只讀開啟
O_WRONLY  只寫開啟
O_RDWR 讀寫開啟

上面三個常量必須指定一個並且只能指定一個,下面一些常量則是可選的:
O_APPEND  將寫入追加到檔案的尾端
O_CREAT 若檔案不存在,則建立它。使用該選項時,需要第三個引數mode,用來指定新檔案的訪問許可權位
O_EXCL 如果同時指定了O_CREAT,而檔案已經存在,則會出錯
O_TRUNC 如果此檔案存在,而且為只寫或讀寫模式成功開啟,則將其長度截短為0
O_NOCTTY 如果pathname指的是終端裝置,則不將該裝置分配作為此程序的控制終端
O_NONBLOCK 如果pathname指的是一個FIFO檔案、塊裝置檔案或字元裝置檔案,則此選項將檔案的本次開啟操作和後續的I/O操作設定為非阻塞模式

複製程式碼

方法2,同socket的方法1,用F_SETFL和flags|O_NONBLOCK.

訊息佇列:

msgsnd和msgrcv的最後一個引數加上 IPC_NOWAIT:
複製程式碼

int msgsnd (int msqid, const void *ptr, size_t length, IPC_NOWAIT) ;

引數flag的值可以指定為IPC_NOWAIT。這類似於檔案IO的非阻塞IO標誌。若訊息佇列已滿,則指定IPC_NOWAIT使得msgsnd立即出錯返回EAGAIN。
如果沒有指定IPC_NOWAIT,則程序阻塞直到下述情況出現為止:①有空間可以容納要傳送的訊息 ②從系統中刪除了此佇列(返回EIDRM“識別符號被刪除”)③捕捉到一個訊號,並從訊號處理程式返回(返回EINTR)

ssize_t msgrcv (int msqid, void* ptr, size_t length, long type, IPC_NOWAIT) ;

引數ptr指定所接收訊息的存放位置。引數length指定了資料部分大小(只想要多長的資料)
引數type指定希望從佇列中讀出什麼樣的訊息。
type == 0 返回佇列中的第一個訊息
type > 0 返回佇列中訊息型別為type的第一個訊息
type < 0 返回佇列中訊息型別值小於或等於type絕對值的訊息,如果這種訊息有若干個。則取型別值最小的訊息。
(如果一個訊息佇列由多個客戶程序和一個伺服器程序使用,那麼type欄位可以用來包含客戶程序的程序ID)

引數flag可以指定為IPC_NOWAIT,使操作不阻塞。

複製程式碼

這類似於檔案IO的非阻塞IO標誌。比如msgsnd,若訊息佇列已滿,則指定IPC_NOWAIT使得msgsnd立即出錯返回EAGAIN。