每一個TCP套接口有一個發送緩沖區,可以用SO_SNDBUF套接口選項來改變這個緩沖區的大小。當應用進程調用 write時,內核從應用進程的緩沖區中拷貝所有數據到套接口的發送緩沖區。如果套接口的發送緩沖區容不下應用程序的所有數據(或是應用進程的緩沖區大於 套接口發送緩沖區,或是套接口發送緩沖區還有其他數據),應用進程將被掛起(睡眠)。這里假設套接口是阻塞的,這是通常的缺省設置。內核將不從write 系統調用返回,直到應用進程緩沖區中的所有數據都拷貝到套接口發送緩沖區。因此從寫一個TCP套接口的write調用成功返回僅僅表示我們可以重新使用應 用進程的緩沖區。它並不告訴我們對端的 TCP或應用進程已經接收了數據。
TCP取套接口發送緩沖區的數據並把它發送給對端TCP,其過程基於TCP數據傳輸的所有規則。對端TCP必須確認收到的數據,只有收到對端的ACK,本端TCP才能刪除套接口發送緩沖區中已經確認的數據。TCP必須保留數據拷貝直到對端確認為止。
任何UDP套接字也有緩沖區大小,不過它僅僅是可寫到該套接字的UDP數據報的大小上線,如果一個應用進程寫一個大於套接字發送緩沖區大小的數據報,內核將返回一個EMSGSIZE錯誤,既然UDP是不可靠的,他不必保存應用進程數據的副本因此不需一個真正的緩沖區。(應用進程的數據在沿協議棧向下傳遞時,通常被復制到某種格式的一個內核緩沖區中,然而當數據被發送后,這個副本被數據鏈路層丟棄了)
write()
函數定義:ssize_t write (int fd, const void * buf, size_t count);
函數說明:write()會把參數buf所指的內存寫入count個字節到參數放到所指的文件內。write成功返回,只是buf中的數據被復制到了kernel中的TCP發送緩沖區。至於數據什么時候被發往網絡,什么時候被對方主機接收,什么時候被對方進程讀取,系統調用層面不會給予任何保證和通知。write在什么情況下會阻塞?當kernel的該socket的發送緩沖區已滿時。對於每個socket,擁有自己的send buffer和receive buffer。從Linux 2.6開始,兩個緩沖區大小都由系統來自動調節(autotuning),但一般在default和max之間浮動。已經發送到網絡的數據依然需要暫存在send buffer中,只有收到對方的ack后,kernel才從buffer中清除這一部分數據,為后續發送數據騰出空間。接收端將收到的數據暫存在receive buffer中,自動進行確認。但如果socket所在的進程不及時將數據從receive buffer中取出,最終導致receive buffer填滿,由於TCP的滑動窗口和擁塞控制,接收端會阻止發送端向其發送數據。這些控制皆發生在TCP/IP棧中,對應用程序是透明的,應用程序繼續發送數據,最終導致send buffer填滿,write調用阻塞。
返回值:如果順利write()會返回實際寫入的字節數。當有錯誤發生時則返回-1,錯誤代碼存入errno中。
附加說明:
- write()函數返回值一般無0,只有當如下情況發生時才會返回0:write(fp, p1+len, (strlen(p1)-len)中第三參數為0,此時write()什么也不做,只返回0。
- write()函數從buf寫數據到fd中時,若buf中數據無法一次性讀完,那么第二次讀buf中數據時,其讀位置指針(也就是第二個參數buf)不會自動移動,需要程序員編程控制,而不是簡單的將buf首地址填入第二參數即可。如可按如下格式實現讀位置移動:write(fp, p1+len, (strlen(p1)-len)。 這樣write第二次循環時變會從p1+len處寫數據到fp, 之后的也由此類推,直至(strlen(p1)-len變為0。
- 在write一次可以寫的最大數據范圍內(貌似是BUFSIZ ,8192),第三參數count大小最好為buf中數據的大小,以免出現錯誤。(經過筆者再次試驗,write一次能夠寫入的並不只有8192這么多,筆者嘗試一次寫入81920000,結果也是可以,看來其一次最大寫入數據並不是8192,但內核中確實有BUFSIZ這個參數,具體指什么還有待研究)
#include <string.h> #include <stdio.h> #include <fcntl.h> int main() { char *p1 = "This is a c test code"; volatile int len = 0; int fp = open("/home/test.txt", O_RDWR|O_CREAT); for(;;) { int n; if((n=write(fp, p1+len, (strlen(p1)-len)))== 0) //if((n=write(fp, p1+len, 3)) == 0) { //strlen(p1) = 21 printf("n = %d \n", n); break; } len+=n; } return 0; }
此程序中的字符串"This is a c test code"有21個字符,經筆者親自試驗,若write時每次寫3個字節,雖然可以將p1中數據寫到fp中,但文件test.txt中會帶有很多亂碼。唯一正確的做法還是將第三參數設為(strlen(p1) - len,這樣當write到p1末尾時(strlen(p1) - len將會變為0,此時符合附加說明(1)中所說情況,write返回0, write結束。
寫的本質也不是進行發送操作,而是把用戶態的數據copy 到系統底層去,然后再由系統進行發送操作,send,write返回成功,只表示數據已經copy 到底層緩沖,而不表示數據已經發出,更不能表示對方端口已經接收到數據.
對於write(或者send)而言,
阻塞情況下
阻塞情況下,write會將數據發送完。(不過可能被中斷),在阻塞的情況下,是會一直等待,直到write 完,全部的數據再返回.這點行為上與讀操作有所不同。
原因:
讀,究其原因主要是讀數據的時候我們並不知道對端到底有沒有數據,數據是在什么時候結束發送的,如果一直等待就可能會造成死循環,所以並沒有去進行這方面的處理;
寫,而對於write, 由於需要寫的長度是已知的,所以可以一直再寫,直到寫完.不過問題是write 是可能被打斷嗎,造成write 一次只write 一部分數據, 所以write 的過程還是需要考慮循環write, 只不過多數情況下次write 調用就可能成功.
非阻塞情況下
非阻塞寫的情況下,是采用可以寫多少就寫多少的策略.與讀不一樣的地方在於,有多少讀多少是由網絡發送的那一端是否有數據傳輸到為標准,但是對於可以寫多少是由本地的網絡堵塞情況為標准的,在網絡阻塞嚴重的時候,網絡層沒有足夠的內存來進行寫操作,這時候就會出現寫不成功的情況,阻塞情況下會盡可能(有可能被中斷)等待到數據全部發送完畢, 對於非阻塞的情況就是一次寫多少算多少,沒有中斷的情況下也還是會出現write 到一部分的情況.
read()
函數定義:ssize_t read(int fd, void * buf, size_t count);
函數說明:read()會把參數fd所指的文件傳送count 個字節到buf 指針所指的內存中。
參數 count 是請求讀取的字節數,讀上來的數據保存在緩沖區buf中,同時文件的當前讀寫位置向后移。注意這個讀寫位置和使用C標准I/O庫時的讀寫位置有可能不同,這個讀寫位置是記在內核中的,而使用C標准I/O庫時的讀寫位置是用戶空間I/O緩沖區中的位置。比如用fgetc讀一個字節,
fgetc有可能從內核中預讀1024個字節到I/O緩沖區中,再返回第一個字節,這時該文件在內核中記錄的讀寫位置是1024,而在FILE結構體中記錄的讀寫位置是1。注意返回值類型是ssize_t,表示有符號的size_t,這樣既可以返回正的字節數、0(表示到達文件末尾)也可以返回負值-1(表示出錯) read函數返回時,返回值說明了buf中前多少個字節是剛讀上來的。有些情況下,實際讀到的字節數(返回值)會小於請求讀的字節數count,例如:讀常規文件時,在讀到count個字節之前已到達文件末尾。例如,距文件末尾還有30個字節而請求讀100個字節,則read返回30,下次read將返回0。
從終端設備讀,通常以行為單位,讀到換行符就返回了。
從網絡讀,根據不同的傳輸層協議和內核緩存機制,返回值可能小於請求的字節數
返回值:返回值為實際讀取到的字節數, 如果返回0, 表示已到達文件尾或是無可讀取的數據。若參數count 為0, 則read()不會有作用並返回0。
注意:
read時fd中的數據如果小於要讀取的數據,就會引起阻塞。
對一個管道的read只要管道中有數據,立馬返回,不必等待達到所請求的字節數
read 的原則:
數據在不超過指定的長度的時候有多少讀多少,沒有數據就會一直等待。所以一般情況下::我們讀取數據都需要采用循環讀的方式讀取數據,因為一次read 完畢不能保證讀到我們需要長度的數據,read 完一次需要判斷讀到的數據長度再決定是否還需要再次讀取。
阻塞情況下
在阻塞條件下,read/recv/msgrcv的行為:
- 如果沒有發現數據在網絡緩沖中會一直等待,
- 當發現有數據的時候會把數據讀到用戶指定的緩沖區,但是如果這個時候讀到的數據量比較少,比參數中指定的長度要小,read 並不會一直等待下去,而是立刻返回
非阻塞情況下
在非阻塞的情況下,read 的行為
- 如果發現沒有數據就直接返回,
- 如果發現有數據那么也是采用有多少讀多少的進行處理.
所以read 完一次需要判斷讀到的數據長度再決定是否還需要再次讀取。
對於讀而言:阻塞和非阻塞的區別在於沒有數據到達的時候是否立刻返回.
recv 中有一個MSG_WAITALL 的參數
recv(sockfd, buff, buff_size, MSG_WAITALL),
在正常情況下recv 是會等待直到讀取到buff_size 長度的數據,但是這里的WAITALL 也只是盡量讀全,在有中斷的情況下recv 還是可能會被打斷,造成沒有讀完指定的buff_size的長度。
所以即使是采用recv + WAITALL 參數還是要考慮是否需要循環讀取的問題,在實驗中對於多數情況下recv (使用了MSG_WAITALL)還是可以讀完buff_size,
所以相應的性能會比直接read 進行循環讀要好一些。
注意
使用MSG_WAITALL時,sockfd必須處於阻塞模式下,否則不起作用。
所以MSG_WAITALL不能和MSG_NONBLOCK同時使用。
要注意的是使用MSG_WAITALL的時候,sockfd 必須是處於阻塞模式下,否則WAITALL不能起作用。
非阻塞I/O的概念
當進程調用一個阻塞的系統函數時,該進程被置於睡眠(Sleep)狀態,這時內核調度其它進程運行,直到該進程等待的事件發生了(比如網絡上接收到數據包,或者調用sleep指定的睡眠時間到了)它才有可能繼續運行。與睡眠狀態相對的是運行(Running)狀態,在Linux內核中,處於運行狀態的進程分為兩種情況:
- 正在被調度執行。CPU處於該進程的上下文環境中,程序計數器(eip)里保存着該進程的指令地址,通用寄存器里保存着該進程運算過程的中間結果,正在執行該進程的指令,正在讀寫該進程的地址空間。
- 就緒狀態。該進程不需要等待什么事件發生,隨時都可以執行,但CPU暫時還在執行另一個進程,所以該進程在一個就緒隊列中等待被內核調度。系統中可能同時有多個就緒的進程,那么該調度誰執行呢?內核的調度算法是基於優先級和時間片的,而且會根據每個進程的運行情況動態調整它的優先級和時間片,讓每個進程都能比較公平地得到機會執行,同時要兼顧用戶體驗,不能讓和用戶交互的進程響應太慢。
如果在open一個設備時指定了O_NONBLOCK標志,read/write就不會阻塞。以read為例,如果設備暫時沒有數據可讀就返回-1,同時置errno為EWOULDBLOCK(或者EAGAIN,這兩個宏定義的值相同),表示本來應該阻塞在這里(would block,虛擬語氣),事實上並沒有阻塞而是直接返回錯誤,調用者應該試着再讀一次(again)。這種行為方式稱為輪詢(Poll),調用者只是查詢一下,而不是阻塞在這里死等,這樣可以同時監視多個設備:
while(true) { 非阻塞read(設備1); if(設備1有數據到達) 處理數據; 非阻塞read(設備2); if(設備2有數據到達) 處理數據; ... }
如果read(設備1)是阻塞的,那么只要設備1沒有數據到達就會一直阻塞在設備1的read調用上,即使設備2有數據到達也不能處理,使用非阻塞I/O就可以避免設備2得不到及時處理。
非阻塞I/O有一個缺點,如果所有設備都一直沒有數據到達,調用者需要反復查詢做無用功,如果阻塞在那里,操作系統可以調度別的進程執行,就不會做無用功了。在使用非阻塞I/O時,通常不會在一個while循環中一直不停地查詢(這稱為Tight Loop),而是每延遲等待一會兒來查詢一下,以免做太多無用功,在延遲等待的時候可以調度其它進程執行。
while(true) { 非阻塞read(設備1); if(設備1有數據到達) 處理數據; 非阻塞read(設備2); if(設備2有數據到達) 處理數據; ... sleep(n); }
這樣做的問題是,設備1有數據到達時可能不能及時處理,最長需延遲n秒才能處理,而且反復查詢還是做了很多無用功。而select/poll/epoll 等函數可以阻塞地同時監視多個設備,還可以設定阻塞等待的超時時間,從而圓滿地解決了這個問題。
整個write過程
3.下面是整個write過程
- glibc write是將app_buffer->libc_buffer->page_cache
- write是將app_buffer->page_cache
- mmap可以直接獲取page_cache直寫
- write+O_DIRECT的話將app_buffer寫到io_queue里面
- io_queue一方面將寫鄰近扇區的內容進行merge,另外一方面進行排序確保磁頭和磁 盤旋轉最少。
- io_queue的工作也需要結合IO調度算法。不過這些僅僅對於physical disk有效。
- 對於ssd而言的話,因為完全是隨機寫,基本沒有調度算法。
- driver(filesystem module)通過DMA寫入disk_cache之后(使用fsync就可以強制刷新)到disk上面了。
- 直接操作設備(RAW)方式直接寫disk_cache.
O_DIRECT 和 RAW設備最根本的區別是O_DIRECT是基於文件系統的,也就是在應用層來看,其操作對象是文件句柄,內核和文件層來看,其操作是基於inode和數據塊,這些概念都是和ext2/3的文件系統相關,寫到磁盤上最終是ext3文件。而RAW設備寫是沒有文件系統概念,操作的是扇區號,操作對象是扇區,寫出來的東西不一定是ext3文件(如果按照ext3規則寫就是ext3文件)。一般基於O_DIRECT來設計優化自己的文件模塊,是不滿系統的cache和調度策略,自己在應用層實現這些,來制定自己特有的業務特色文件讀寫。但是寫出來的東西是ext3文件,該磁盤卸下來,mount到其他任何linux系統上,都可以查看。而基於RAW設備的設計系統,一般是不滿現有ext3的諸多缺陷,設計自己的文件系統。自己設計文件布局和索引方式。舉個極端例子:把整個磁盤做一個文件來寫,不要索引。這樣沒有inode限制,沒有文件大小限制,磁盤有多大,文件就能多大。這樣的磁盤卸下來,mount到其他linux系統上,是無法識別其數據的。兩者都要通過驅動層讀寫;在系統引導啟動,還處於實模式的時候,可以通過bios接口讀寫raw設備。
操作系統為了提高文件讀寫效率,在內核層提供了讀寫緩沖區。對於磁盤的寫並不是立刻寫入磁盤, 而是首先寫入頁面緩沖區然后定時刷到硬盤上。但是這種機制降低了文件更新速度,並且如果系統發生故障 的話,那么會造成部分數據丟失。這里的3個sync函數就是為了這個問題的。
- sync.是強制將所有頁面緩沖區都更新到磁盤上。
- fsync.是強制將某個fd涉及到的頁面緩存更新到磁盤上(包括文件屬性等信息).
- fdatasync.是強制將某個fd涉及到的數據頁面緩存更新到磁盤上。