C 標準庫IO緩衝區和核心緩衝區的區別

weixin_33912246發表於2018-02-26

1.C標準庫的I/O緩衝區 
        UNIX的傳統 是Everything is a file,鍵盤、顯示器、串列埠、磁碟等裝置在/dev 目錄下都有一個特殊的裝置檔案與之對應,這些裝置檔案也可以像普通檔案(儲存在磁碟上的檔案)一樣開啟、讀、寫和關閉,使用的函式介面是相同的。使用者程式呼叫C標準I/O庫函式讀寫普通檔案或裝置,而這些庫函式要通過系統呼叫把讀寫請求傳給核心 ,最終由核心驅動磁碟或裝置完成I/O操作。C標準庫為每個開啟的檔案分配一個I/O緩衝區以加速讀寫操作,通過檔案的FILE 結構體可以找到這個緩衝區,使用者呼叫讀寫函式大多數時候都在I/O緩衝區中讀寫,只有少數時候需要把讀寫請求傳給核心。以fgetc / fputc 為例,當使用者程式第一次呼叫fgetc 讀一個位元組時,fgetc 函式可能通過系統呼叫 進入核心讀1K位元組到I/O緩衝區中,然後返回I/O緩衝區中的第一個位元組給使用者,把讀寫位置指 向I/O緩衝區中的第二個字元,以後使用者再調fgetc ,就直接從I/O緩衝區中讀取,而不需要進核心 了,當使用者把這1K位元組都讀完之後,再次呼叫fgetc 時,fgetc 函式會再次進入核心讀1K位元組 到I/O緩衝區中。在這個場景中使用者程式、C標準庫和核心之間的關係就像在“Memory Hierarchy”中 CPU、Cache和記憶體之間的關係一樣,C標準庫之所以會從核心預讀一些資料放 在I/O緩衝區中,是希望使用者程式隨後要用到這些資料,C標準庫的I/O緩衝區也在使用者空間,直接 從使用者空間讀取資料比進核心讀資料要快得多。另一方面,使用者程式呼叫fputc 通常只是寫到I/O緩 衝區中,這樣fputc 函式可以很快地返回,如果I/O緩衝區寫滿了,fputc 就通過系統呼叫把I/O緩衝 區中的資料傳給核心,核心最終把資料寫回磁碟或裝置。有時候使用者程式希望把I/O緩衝區中的資料立刻 傳給核心,讓核心寫回裝置或磁碟,這稱為Flush操作,對應的庫函式是fflush,fclose函式在關閉檔案 之前也會做Flush操作。

        我們知道main 函式被啟動程式碼這樣呼叫:exit(main(argc, argv));。

        main 函式return時啟動程式碼會 呼叫exit ,exit 函式首先關閉所有尚未關閉的FILE *指標(關閉之前要做Flush操作),然後通 過_exit 系統呼叫進入核心退出當前程式.

C標準庫的I/O緩衝區有三種型別:全緩衝、行緩衝和無緩衝。當使用者程式呼叫庫函式做寫操作時, 不同型別的緩衝區具有不同特性。 

    全緩衝 

如果緩衝區寫滿了就寫回核心。常規檔案通常是全緩衝的。 

   行緩衝 

如果使用者程式寫的資料中有換行符就把這一行寫回核心,或者如果緩衝區寫滿了就寫回內 核。標準輸入和標準輸出對應終端裝置時通常是行緩衝的。 

     無緩衝 

使用者程式每次調庫函式做寫操作都要通過系統呼叫寫回核心。標準錯誤輸出通常是無緩衝的,這樣使用者程式產生的錯誤資訊可以儘快輸出到裝置。


        除了寫滿緩衝區、寫入換行符之外,行緩衝還有兩種情況會自動做Flush操作。如果: 
使用者程式呼叫庫函式從無緩衝的檔案中讀取 
或者從行緩衝的檔案中讀取,並且這次讀操作會引發系統呼叫從核心讀取資料


       如果使用者程式不想完全依賴於自動的Flush操作,可以調fflush函式手動做Flush操作。 
#include <stdio.h> 
int fflush(FILE *stream); 
返回值:成功返回0,出錯返回EOF並設定errno
fflush函式用於確保資料寫回了核心,以免程式異常終止時丟失資料,如fflush(stdout); 作為一個特例,調 用fflush(NULL)可以對所有開啟檔案的I/O緩衝區做Flush操作。


2. 使用者程式的緩衝區
        在函式棧上分配的如char buf[10];之類的緩衝區, strcpy(buf, str);  str 所指向的字串有可能超過10個字元而導致寫越界,這種寫越界可能當時不出錯, 而在函式返回時出現段錯誤,原因是寫越界覆蓋了儲存在棧幀上的返回地址, 函式返回時跳轉到非法地址,因而出錯。像buf 這種由呼叫者分配並傳給函式讀或寫的一段記憶體通 常稱為緩衝區(Buffer),緩衝區寫越界的錯誤稱為緩衝區溢位(Buffer Overflow)。如果只是出 現段錯誤那還不算嚴重,更嚴重的是緩衝區溢位Bug經常被惡意使用者利用,使函式返回時跳轉到一 個事先設好的地址,執行事先設好的指令,如果設計得巧妙甚至可以啟動一個Shell,然後隨心所欲 執行任何命令,可想而知,如果一個用root 許可權執行的程式存在這樣的Bug,被攻陷了,後果將很 嚴重。


       下圖以fgets / fputs 示意了I/O緩衝區的作用,使用fgets / fputs 函式時在使用者程式中也需要分配緩衝 區(圖中的buf1 和buf2 ),注意區分使用者程式的緩衝區和C標準庫的I/O緩衝區。



3.核心緩衝區
(1)終端緩衝

   終端裝置有輸入和輸出佇列緩衝區,如下圖所示


         以輸入佇列為例,從鍵盤輸入的字元經線路規程過濾後進入輸入佇列,使用者程式以先進先出的順序 從佇列中讀取字元,一般情況下,當輸入佇列滿的時候再輸入字元會丟失,同時系統會響鈴警報。 終端可以配置成回顯(Echo)模式,在這種模式下,輸入佇列中的每個字元既送給使用者程式也送給 輸出佇列,因此我們在命令列鍵入字元時,該字元不僅可以被程式讀取,我們也可以同時在螢幕上 看到該字元的回顯。
        注意上述情況是使用者程式(shell程式也是)呼叫read/write等unbuffer I/O函式的情況,當呼叫printf/scanf (底層實現也是read/write)等C標準I/O庫函式時,當使用者程式呼叫scanf讀取鍵盤輸入時,開始輸入的字元都存到輸入佇列,直到我們遇到換行符(標準輸入和標準輸出都是行緩衝的)時,系統呼叫read將輸入佇列的內容讀到使用者程式的I/O緩衝區; 當呼叫printf 列印一個字串時,如果語句中帶換行符,則立刻將放在I/O緩衝區的字串呼叫write寫到核心的輸出佇列,列印到螢幕上,如果printf語句沒帶換行符,則由上面的討論可知,程式退出時會做fflush操作.
   
 
(2)雖然write 系統呼叫位於C標準庫I/O緩衝區的底 層,被稱為Unbuffered I/O函式,但在write 的底層也可以分配一個核心I/O緩衝區,所以write 也不一定是直接寫到檔案的,也 可能寫到核心I/O緩衝區中,可以使用fsync函式同步至磁碟檔案,至於究竟寫到了檔案中還是核心緩衝區中對於程式來說是沒有差別 的,如果程式A和程式B開啟同一檔案,程式A寫到核心I/O緩衝區中的資料從程式B也能讀到,因為核心空間是程式共享的, 而c標準庫的I/O緩衝區則不具有這一特性,因為程式的使用者空間是完全獨立的.


(3)為了減少讀盤次數,核心快取了目錄的樹狀結構,稱為dentry(directory entry(目錄下項) cache


(4)FIFO和UNIX Domain Socket這兩種IPC機制都是利用檔案系統中的特殊檔案來標識的。FIFO檔案在磁碟上沒有資料塊,僅用來標識核心中的一條通道,各程式可以開啟這個檔案進行read / write ,實際上是在讀寫核心通道(根本原因在於這個file 結構體所指向的read 、write 函式指標和常規檔案不一樣),這樣就實現了程式間通訊。UNIX Domain Socket和FIFO的原理類似,也需 要一個特殊的socket檔案來標識核心中的通道,檔案型別s表示socket,這些檔案在磁碟上也沒有資料塊。UNIX Domain Socket是目前最廣泛使用 的IPC機制.如下圖:



4.stack overflow 無窮遞迴或者定義的極大陣列都可能導致作業系統為程式預留的棧空間耗盡 程式崩潰(段錯誤) 


參考:《linux c 程式設計一站式學習》

相關文章