內核空間和用戶空間:
由於操作系統都包括內核空間和用戶空間(或者說內核態和用戶態),內核空間主要存放的是內核代碼和數據,是供系統進程使用的空間。而用戶空間主要存放的是用戶代碼和數據,是供用戶進程使用的空間。目前Linux系統簡化了分段機制,使得虛擬地址與線性地址總是保持一致,因此,Linux系統的虛擬地址也是0-4G。Linux系統將這4G空間分為了兩個部分:將最高的1G空間(從虛擬地址0xC0000000到0xFFFFFFFF)供內核使用,即為“內核空間”,而將較低的3G空間(從虛擬地址 0x00000000到0xBFFFFFFF)供用戶進程使用,即為“用戶空間”。同時由於每個用戶進程都可以通過系統調用進入到內核空間,因此Linux的內核空間可以認為是被所有用戶進程所共享的,因此對於一個具體用戶進程來說,它可以訪問的虛擬內存地址就是0-4G。另外Linux系統分為了四種特權級:0~3,主要是用來保護資源。0級特權最高,而3級則為最低,系統進程主要運行在0級,用戶進程主要運行在3級。
一般來說,IO操作都分為兩個階段,就拿套接口的輸入操作來說,它的兩個階段主要是:
1)等待網絡數據到來,當分組到來時,將其拷貝到內核空間的臨時緩沖區中
2)將內核空間臨時緩沖區中的數據拷貝到用戶空間緩沖區中
服務器端編程經常需要構造高性能的IO模型,常見的IO模型有四種:
(1)同步阻塞IO(Blocking IO):即傳統的IO模型。
(2)同步非阻塞IO(Non-blocking IO):默認創建的socket都是阻塞的,非阻塞IO要求socket被設置為NONBLOCK。注意這里所說的NIO並非Java的NIO(New IO)庫。
(3)IO多路復用(IO Multiplexing):即經典的Reactor設計模式,有時也稱為異步阻塞IO,Java中的Selector和Linux中的epoll都是這種模型。
(4)異步IO(Asynchronous IO):即經典的Proactor設計模式,也稱為異步非阻塞IO。
同步和異步的概念描述的是用戶線程與內核的交互方式:同步是指用戶線程發起IO請求后需要等待或者輪詢內核IO操作完成后才能繼續執行;而異步是指用戶線程發起IO請求后仍繼續執行,當內核IO操作完成后會通知用戶線程,或者調用用戶線程注冊的回調函數。
阻塞和非阻塞的概念描述的是用戶線程調用內核IO操作的方式:阻塞是指IO操作需要徹底完成后才返回到用戶空間;而非阻塞是指IO操作被調用后立即返回給用戶一個狀態值,無需等到IO操作徹底完成。
對於一個network IO (這里我們以read舉例),它會涉及到兩個系統對象,一個是調用這個IO的process (or thread),另一個就是系統內核(kernel)。當一個read操作發生時,它會經歷兩個階段:
1 等待數據准備 (Waiting for the data to be ready)
2 將數據從內核拷貝到進程中 (Copying the data from the kernel to the process)
記住這兩點很重要,因為這些IO Model的區別就是在兩個階段上各有不同的情況。
阻塞式I/O模型
產生阻塞的原因
linux進程調度算法-時間片調度算法
每個進程占用CPU一個時間片后被掛起
當前運行進程如果需要等待其他系統資源,且不是非阻塞方式運行,將進入等待狀態
設置socket為非阻塞方式
函數fcntl
int flags;
flag=fcntl(sockfd,F_GETFL,0);
fcntl(sockfd,F_SETFL,flag|O_NONBLOCK);
函數ioctl
int on=1;
ioctl(sockfd,FIONBIO,&on);
非阻塞式I/O模型對4種I/O操作返回的錯誤
讀操作
接收緩沖區無數據時返回EWOULDBLOCK
寫操作
發送緩沖區無空間時返回EWOULDBLOCK
空間不夠時部分拷貝,返回實際拷貝字節數
建立連接
啟動3次握手,立刻返回錯誤EINPROGRESS
服務器客戶端在同一主機上connect立即返回成功
接受連接
沒有新連接返回EWOULDBLOCK
阻塞式I/O模型的超時控制
調用alarm函數設置超時
超時到達時產生SIGALARM信號中斷I/O函數阻塞,對於4種產生阻塞的函數均有效
多次調用alarm時,產生的SIGALARM信號無法區分是哪一次超時引發的,無法實現超時控制
示例:alarmio.cpp
當用戶進程調用了select,那么整個進程會被block,而同時,kernel會“監視”所有select負責的socket,當任何一個socket中的數據准備好了,select就會返回。這個時候用戶進程再調用read操作,將數據從kernel拷貝到用戶進程。
這個圖和blocking IO的圖其實並沒有太大的不同,事實上,還更差一些。因為這里需要使用兩個system call (select 和 recvfrom),而blocking IO只調用了一個system call (recvfrom)。但是,用select的優勢在於它可以同時處理多個connection。(多說一句。所以,如果處理的連接數不是很高的話,使用select/epoll的web server不一定比使用multi-threading + blocking IO的web server性能更好,可能延遲還更大。select/epoll的優勢並不是對於單個連接能處理得更快,而是在於能處理更多的連接。)
在IO multiplexing Model中,實際中,對於每一個socket,一般都設置成為non-blocking,但是,如上圖所示,整個用戶的process其實是一直被block的。只不過process是被select這個函數block,而不是被socket IO給block。
注意1:select函數返回結果中如果有文件可讀了,那么進程就可以通過調用accept()或recv()來讓kernel將位於內核中准備到的數據copy到用戶區。
注意2: select的優勢在於可以處理多個連接,不適用於單個連接
阻塞式I/O模型的超時控制
設置socket選項
設置SO_RCVTIMEO和SO_SNDTIMEO選項
設置了這兩個選項之后,所有的讀寫操作可以保證在超時范圍內返回
只需設置一次選項,對以后的讀寫操作均有效
不適用於accept和connect
示例:timeoutio.cpp