四種I/O方式的對比
1. Buffered I/O
read(file, tmp_buf, len);
write(socket, tmp_buf, len);
上下文切換:4次
CPU copy:2次
步驟1:read()系統調用使上下文從用戶態切換到內核態。DMA engine從磁盤中讀取文件內容,然后把數據保存在內核地址空間緩存。
步驟2:CPU從內核緩存復制數據到用戶緩存,read()系統調用返回。Read()返回后,上下文從內核態切換到用戶態。現在,數據儲存在用戶地址空間緩存。
步驟3:write()系統調用使上下文從用戶態切換到內核態。CPU把用戶緩存中的數據復制到內核緩存中。這個內核緩存通常與某個特定的socket關聯。
步驟4:write()系統調用返回,使上下文從內核態切換到用戶態。DMA engine把數據從內核緩存傳遞到protocal engine,這個過程是獨立且異步的。獨立且異步的意思是,write()返回不代表已經將所有數據寫入到protocal engine,甚至不代表數據傳輸已經開始。write()返回僅僅表示Ethernet driver已經接受我們的數據傳輸,這項任務被置入一個隊列。
這種IO被稱為緩存IO(buffered io). 當應用程序訪問某塊數據的時候,操作系統內核會先檢查這塊數據是不是因為前一次對相同文件的訪問而已經被存放在操作系統內核地址空間的緩沖區(頁緩存)內,如果在內核緩沖區中找不到這塊數據,Linux操作系統內核會先將這塊數據從磁盤讀出來放到操作系統內核的緩沖區里去。
2. Direct I/O
你可以對fd進行O_DIRECT的設置
fcntl(file, F_SETFD, O_DIRECT | O_SYNC | oldflags);
read(file, tmp_buf, len);
write(socket, tmp_buf, len);
直接IO會把磁盤上的數據直接復制到用戶地址空間,而不經過內核地址空間。直接IO適合自緩存應用(self-caching applications)。某些應用程序有自己的數據緩存機制,不需要使用操作系統內核緩存,這種應用程序稱為自緩存應用。
內核緩存區對讀寫磁盤數據做了優化,包括按順序預讀取,在成簇磁盤塊上執行IO等等。因此在普通的應用中使用直接IO會降低性能。
一般會在數據庫系統使用直接IO。數據庫系統的高速緩存和IO優化機制均自成一體,無需內核消耗CPU時間和內存去完成相同的任務。
直接IO中read和write的行為必須是同步的,但是O_DIRECT不保證同步,因此O_DIRECT必須與O_SYNC連用來保證同步行為。
請慎用。
3. 內存映射:使用mmap()代替read()
tmp_buf = mmap(file, len);
write(socket, tmp_buf, len);
上下文切換:4次
CPU copy:1次
步驟1:mmap()系統調用使上下文從用戶態切換到內核態。DMA engine從磁盤中讀取文件內容,然后把數據保存在內核地址空間緩存。CPU將該內核緩存區與用戶進程共享。
步驟2:write()系統調用使上下文從用戶態切換到內核態。 CPU把數據從原來的內核緩存復制到另一片與socket關聯的內核緩存區。
步驟3:write()系統調用返回,使上下文從內核態切換到用戶態。DMA engine把數據從內核緩存傳遞到protocal engine。
使用mmap()代替read()可以減少一次CPU copy,但增加了share動作。當復制的數據量很大,一次CPU copy的花費大於share的花費時,使用mmap()代替read()是能優化性能的。但是,mmap()+write()有一個陷阱:You will fall into one of them when you memory map a file and then call write while another process truncates the same file.write()系統調用會被信號SIGBUS中斷,這個信號的默認動作是kill進程然后dump core。作為一個server程序,我們通常不希望被KILL。我們有兩種方法解決這個問題:
第一個方法是修改SIGBUS的信號處理程序,將其改為簡單的return。這樣,SIGBUS信號就不會kill進程,write()會返回被信號中斷前已經成功寫入的字符數,並設置errno。但是當該進程因其他問題收到SIGBUS信號時,卻也簡單的return了,這會掩蓋運行時出現的巨大問題。因此,不推薦使用方法一。
第二個方法是使用文件租借鎖 (windows系統中稱為opportunistic lock)。讓進程a獲得租借鎖,當進程b對正在傳輸的文件進行截斷時,內核會給進程a發送信號,進程a會被中斷,以防止該進程訪問到無效地址並被SIGBUS中斷。進程a的write()會返回中斷前寫入的字符數,並設置errno。以下是租借鎖的示例代碼
/* l_type can be F_RDLCK F_WRLCK */
if(fcntl(fd, F_SETLEASE, l_type)){
perror("kernel lease set type");
return -1;
}
你應該在mapping file前獲得租借鎖,在你完成寫之后釋放租借鎖。
fcntl(fd, F_SETLEASE, F_UNLCK)
4. zero copy : sendfile()
從Linux2.1開始,引入了sendfile()
sendfile(socket, file, len);
上下文切換:2次
CPU copy:1次或0次
sendfile()與前面3節相比,只進行一次系統調用。因此,把用戶態和內核態之間的上下文切換從4次減少到2次。
CPU copy次數取決於硬件是否支持gather operation
如果硬件不支持gather operation:
步驟1:sendfile()系統調用使上下文從用戶態切換到內核態。DMA engine把文件內容復制到內核緩存區。然后CPU把該內核緩存區的數據復制到另一片與socket關聯的內核緩存區。
步驟2:sendfile()系統調用返回,使上下文從內核態切換到用戶態。DMA engine把數據從內核緩存傳遞到protocal engine。
如果調用sendfile()時,文件被截斷。sendfile()會在在訪問到無效地址前返回,以防止被SIGBUS信號中斷。
如果硬件支持gather operation,且Linux2.4以上:
數據不會從kernel buffer復制到sokcet buffer.僅會把包含kernel buffer地址和長度的信息的描述符append到socket buffer.DMA engine會把數據從kernel buffer直接傳到protocol engine.
Sendfile()的一個問題是缺乏標准的實現。
Linux的sendfile()實現提供了file to file和file to socket的接口,但是solaris和HP-UX僅提供了file to socket.
Linux的sendfile()不支持vectored tranfers.
solaris和HP-Ux的sendfile()有額外的參數
Linux的sendfile() notes:
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
out_fd是待寫的fd, in_fd是待讀的fd。
在2.4-2.6.33,out_fd必須是一個socket。2.4以前和2.6.33后,out_fd可以是任何文件。
sendfile最多傳輸(2,147,479,552) bytes
當你使用sendfile()發送文件到TCP socket,但是需要在發送文件內容之前發送一些header,你可以對TCP socket設置TCP_CORK,以減少發送的數據包的數量。
TCP發送數據包的算法
默認:采用nagle算法,當TCP buffer中的數據超過一個MSS時,發送數據,否則等待收到ACK后再發送數據。
對socket設置TCP_NODELAY:禁用nagle算法,TCP buffer接受到數據后馬上發送
對socket設置TCP_CORK:當TCP buffer中的數據超過一個MSS時,發送數據,否則等待時間到達200 毫秒后再發送數據。當這個選項被取消,那么所有被阻塞的數據也將發送出去。
TCP_CORK的用法是在寫數據前設置TCP_CORK,在分別寫完header和body后,取消TCP_CORK。這可以避免header+body長度小於一個MSS導致發送延時的情況
sendfile()如果想要做到zero copy,那么被讀文件的未寫入到被寫文件的部分不能被修改
目前sendfile()由splice()實現的,是對splice()的包裝。
splice() moves data between two file descriptors without copying between kernel address space and user address space.
但是兩個fd中必須有一個是pipe。Stack over flow的Damon不建議使用splice(),因為現在的實現不夠完善
番外小記
O_NONBLOCK對網絡IO有效,對磁盤IO並沒有作用