緩沖區(Buffer)又稱為緩存(Cache),是內存空間的一部分。也就是說,計算機在內存中預留了一定的存儲空間,用來暫時保存輸入或輸出的數據,這部分預留的空間就叫做緩沖區(緩存)。
有時候,從鍵盤輸入的內容,或者將要輸出到顯示器上的內容,會暫時進入緩沖區,待時機成熟,再一股腦將緩沖區中的所有內容“倒出”,我們才能看到變量的值被刷新,或者屏幕產生變化。
有時候,用戶希望得到最及時的反饋,輸入輸出的內容就不能進入緩沖區。
緩沖區是輸入輸出的“名門”所在,只有學習緩沖區,才能解開前幾節遇到的問題。緩沖區是治愈與輸入輸出有關的大部分疑難雜症的良葯,它能使你對輸入輸出的認識上升到一個更高的層次。
為什么要引入緩沖區(緩存)
緩沖區是為了讓低速的輸入輸出設備和高速的用戶程序能夠協調工作,並降低輸入輸出設備的讀寫次數。
用戶程序的執行速度可以看做 CPU 的運行速度,如果沒有各種硬件的阻礙,理論上它們是同步的。
例如,我們都知道硬盤的速度要遠低於 CPU,它們之間有好幾個數量級的差距,當向硬盤寫入數據時,程序需要等待,不能做任何事情,就好像卡頓了一樣,用戶體驗非常差。計算機上絕大多數應用程序都需要和硬件打交道,例如讀寫硬盤、向顯示器輸出、從鍵盤輸入等,如果每個程序都等待硬件,那么整台計算機也將變得卡頓。
但是有了緩沖區,就可以將數據先放入緩沖區中(內存的讀寫速度也遠高於硬盤),然后程序可以繼續往下執行,等所有的數據都准備好了,再將緩沖區中的所有數據一次性地寫入硬盤,這樣程序就減少了等待的次數,變得流暢起來。
緩沖區的另外一個好處是可以減少硬件設備的讀寫次數。其實我們的程序並不能直接讀寫硬件,它必須告訴操作系統,讓操作系統內核(Kernel)去調用驅動程序,只有驅動程序才能真正的操作硬件。
從用戶程序到硬件設備要經過好幾層的轉換,每一層的轉換都有時間和空間的開銷,而且開銷不一定小;一旦用戶程序需要密集的輸入輸出操作,這種開銷將變得非常大,會成為制約程序性能的瓶頸。
這個時候,分配緩沖區就是必不可少的。每次調用讀寫函數,先將數據放入緩沖區,等數據都准備好了再進行真正的讀寫操作,這就大大減少了轉換的次數。實踐證明,合理的緩沖區設置能成倍提高程序性能。
現在你基本明白了吧,緩沖區其實就是一塊內存空間,它用在硬件設備和用戶程序之間,用來緩存數據,目的是讓快速的 CPU 不必等待慢速的輸入輸出設備,同時減少操作硬件的次數。
緩沖區的類型
根據不同的標准,緩沖區可以有不同的分類。
根據緩沖區對應的是輸入設備還是輸出設備,可以分為輸入緩沖區和輸出緩沖區。
根據數據刷新(也可以稱為清空緩沖區,就是將緩沖區中的數據“倒出”)的時機,可以分為全緩沖、行緩沖、不帶緩沖。這種分類才本節要重點講解的內容。
1) 全緩沖
在這種情況下,當緩沖區被填滿以后才進行真正的輸入輸出操作。緩沖區的大小都有限制的,比如 1KB、4MB 等,數據量達到最大值時就清空緩沖區。
在實際開發中,將數據寫入文件后,打開文件並不能立即看到內容,只有清空緩沖區,或者關閉文件,或者關閉程序后,才能在文件中看到內容。這種現象,就是緩沖區在作怪。
2) 行緩沖
在這種情況下,當在輸入或者輸出的過程中遇到換行符時,才執行真正的輸入輸出操作。行緩沖的典型代表就是標准輸入設備(也即鍵盤)和標准輸出設備(也即顯示器)。
① 在講解 printf() 時,我們在 Linux 或者 Mac OS 平台測試了如下的代碼:
- #include<stdio.h>
- #include<unistd.h>
- int main()
- {
- printf("C語言中文網");
- sleep(5); //程序暫停5秒鍾
- printf("http://c.biancheng.net\n");
- return 0;
- }
運行程序后,會發現第一個 printf() 並沒有立即輸出,而是等待 5 秒以后,和第二個 printf() 一起輸出了。
究其原因,就是 printf() 帶有行緩沖區,"C語言中文網"
這幾個字符要先放入緩沖區中,而不是立即顯示到屏幕上。放入緩沖區以后,程序又暫停了 5 秒,然后執行第二個 printf(),又將"http://c.biancheng.net\n"
放入了緩沖區。注意最后的換行符\n
,它會使得緩沖區刷新,將緩沖區中的所有內容都輸出到顯示器上,所以我們才看到兩個 printf() 一起輸出。
如果將第一個 printf() 的最后加上換行符\n
,也就是寫作下面的形式:
printf("C語言中文網\n");
此時情況又不一樣了,第一個 printf() 會先輸出,第二個 printf() 等待 5 秒以后才輸出。這是因為,第一個 printf() 的最后有換行符\n
,它會使得緩沖區刷新,所以立即就輸出了,不用等着第二個 printf()。
② 對於 scanf(),不管用戶輸入多少內容,只要不按下回車鍵,就不進行真正的讀取。這是因為 scanf() 是帶有行緩沖的,用戶輸入的內容會先放入緩沖區,直到用戶按下回車鍵,產生換行符\n
,才會刷新緩沖區,進行真正的讀取。
3) 不帶緩沖
不帶緩沖區,數據就沒有地方緩存,必須立即進行輸入輸出。
getche()、getch() 就不帶緩沖區,輸入一個字符后立即就執行了,根本不用按下回車鍵。
Windows 下的 printf() 也不帶緩沖區,不管最后有沒有換行符\n
,都會立即輸出,所以對於類似的輸出代碼,Windows 和 Linux、Mac OS 會有不同的表現
錯誤信息輸出函數 perror() 也沒有緩沖區。錯誤信息必須刻不容緩、立即、馬上顯示出來,緩沖區將會增加捕獲錯誤的時間,這是毫無理由的。
C語言標准的模棱兩可
C語言標准規定,輸入輸出緩沖區要具有以下特征:
- 當且僅當輸入輸出不涉及交互設備時,它們才可以是全緩沖的。
- 錯誤顯示設備不能帶有緩沖區。
現代計算機已經沒有了專門的錯誤顯示設備,所有的信息都顯示到一個屏幕上,這里的錯誤顯示設備只能是計算機的顯示器。上面提到的 perror() 其實就是向錯誤顯示設備上輸出信息,但是現代計算機已經把顯示器作為了錯誤顯示設備,所以 perror() 也是向顯示器上輸出內容。
所謂交互設備,就是現代計算機上的顯示器和鍵盤。C標准雖然規定它們不能是全緩沖的,但並沒有規定它們到底是行緩沖還是不帶緩沖,這就導致不同的平台有不同的實現。
1) 輸入設備
scanf()、getchar()、gets() 就是從輸入設備(鍵盤)上讀取內容。對於輸入設備,沒有緩沖區將導致非常奇怪的行為,比如,我們本來想輸入一個整數 947,沒有緩沖區的話,輸入 9 就立即讀取了,根本沒有機會輸入 47,所以,沒有輸入緩沖區是不能接受的。Windows、Linux、Mac OS 在實現時都給輸入設備帶上了行緩沖,所以 scanf()、getchar()、gets() 在每個平台下的表現都一致。
但是在某些特殊情況下,我們又希望程序能夠立即響應用戶按鍵,例如在游戲中,用戶按下方向鍵人物要立即轉向,而且越快越好,這肯定就不能帶有緩沖區了。Windows 下特有的 getche() 和 getch() 就是為這種特殊需求而設計的,它們都不帶緩沖區。
2) 輸出設備
printf()、puts()、putchar() 就是向輸出設備(顯示器)上顯示內容。對於輸出設備,有沒有緩沖區其實影響沒有那么大,頂多是晚一會看到內容,不會有功能性的障礙,所以 Windows 和 Linux、Mac OS 采用了不同的方案:
- Windows 平台下,輸出設備是不帶緩沖區的;
- Linux 和 Mac OS 平台下,輸出設備帶有行緩沖區。
在講解 printf() 時,我們在 Windows 平台測試了如下的代碼:
- #include<stdio.h>
- #include<Windows.h>
- int main()
- {
- printf("C語言中文網");
- Sleep(5000); //程序暫停5秒鍾
- printf("http://c.biancheng.net\n");
- return 0;
- }
運行程序后,會發現第一個 printf() 首先輸出(程序運行后立即輸出),等待 5 秒以后,第二個 printf() 才輸出,第一個 printf() 不會等待第二個 printf(),原因就是 Windows 下的輸出設備沒有緩沖區,遇到 printf() 立即就輸出了,不會有延遲。
緩沖區的刷新(清空)
所謂刷新緩沖區,就是將緩沖區中的內容送達到目的地。緩沖區的刷新遵循以下的規則:
- 不管是行緩沖還是全緩沖,緩沖區滿時會自動刷新;
- 行緩沖遇到換行符
\n
時會刷新; - 關閉文件時會刷新緩沖區;
- 程序關閉時一般也會刷新緩沖區,這個是由標准庫來保障的;
總結
緩沖區位於用戶程序和硬件設備之間,用來緩存數據,目的是讓快速的 CPU 不必等待慢速的輸入輸出設備,同時減少操作硬件的次數。對於 IO 密集型的網絡應用程序,比如網站、數據庫、DNS、CDN 等,緩沖區的設計至關重要,它能十倍甚至一百倍得提高程序性能。