一、read/write 函數
read函數從打開的設備或文件中讀取數據。
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
返回值:成功返回讀取的字節數,出錯返回-1並設置errno,如果在調read之前已到達文件末尾,則這次read返回0
參數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,例如:
1、讀常規文件時,在讀到count個字節之前已到達文件末尾。例如,距文件末尾還有30個字節而請求讀100個字節,則read返回30,下次read將返回0。從
2、終端設備讀,通常以行為單位,讀到換行符就返回了。
3、從網絡讀,根據不同的傳輸層協議和內核緩存機制,返回值可能小於請求的字節數。
write函數向打開的設備或文件中寫數據。
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
返回值:成功返回寫入的字節數,出錯返回-1並設置errno
寫常規文件時,write的返回值通常等於請求寫的字節數count,而向終端設備或網絡寫則不一定。
讀常規文件是不會阻塞的,不管讀多少字節,read一定會在有限的時間內返回。從終端設備或網絡讀則不一定,如果從終端輸入的數據沒有換行符,調用read讀終端設備就會阻塞,如果網絡上沒有接收到數據包,調用read從網絡讀就會阻塞,至於會阻塞多長時間也是不確定的,如果一直沒有數據到達就一直阻塞在那裡。同樣,寫常規文件是不會阻塞的,而向終端設備或網絡寫則不一定。
二、(非)阻塞I/O的概念
現在明確一下阻塞(Block)這個概念。當進程調用一個阻塞的系統函數時,該進程被置於睡眠(Sleep)狀態,這時內核調度其它進程運行,直到該進程等待的事件發生了(比如網絡上接收到數據包,或者調用sleep指定的睡眠時間到了)它才有可能繼續運行。與睡眠狀態相對的是運行(Running)狀態,在Linux內核中,處於運行狀態的進程分為兩種情況:
1、正在被調度執行。CPU處於該進程的上下文環境中,程序計數器(eip)裡保存著該進程的指令地址,通用寄存器裡保存著該進程運算過程的中間結果,正在執行該進程的指令,正在讀寫該進程的地址空間。
2、就緒狀態。該進程不需要等待什麼事件發生,隨時都可以執行,但CPU暫時還在執行另一個進程,所以該進程在一個就緒隊列中等待被內核調度。系統中可能同時有多個就緒的進程,那麼該調度誰執行呢?內核的調度算法是基於優先級和時間片的,而且會根據每個進程的運行情況動態調整它的優先級和時間片,讓每個進程都能比較公平地得到機會執行,同時要兼顧用戶體驗,不能讓和用戶交互的進程響應太慢。
如果在open一個設備時指定了O_NONBLOCK標志,read/write就不會阻塞。以read為例,如果設備暫時沒有數據可讀就返回-1,同時置errno為EWOULDBLOCK(或者EAGAIN,這兩個宏定義的值相同),表示本來應該阻塞在這裡(would block,虛擬語氣),事實上並沒有阻塞而是直接返回錯誤,調用者應該試著再讀一次(again)。這種行為方式稱為輪詢(Poll),調用者只是查詢一下,而不是阻塞在這裡死等,這樣可以同時監視多個設備:
while(1)
{
非阻塞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(1)
{
非阻塞read(設備1);
if(設備1有數據到達)
處理數據;
非阻塞read(設備2);
if(設備2有數據到達)
處理數據;
..............................
sleep(n);
}
這樣做的問題是,設備1有數據到達時可能不能及時處理,最長需延遲n秒才能處理,而且反復查詢還是做了很多無用功。而select/poll/epoll 等函數可以阻塞地同時監視多個設備,還可以設定阻塞等待的超時時間,從而圓滿地解決了這個問題。