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讀取鍵盤輸入時,開始輸入的字符都存到C標准庫的I/O緩沖區,直到我們遇到換行符(標准輸入和標准輸出都是行緩沖的)時,系統調用read將緩沖區的內容讀到內核的終端輸入隊列;當調用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 無窮遞歸或者定義的極大數組都可能導致操作系統為程序預留的棧空間耗盡 程序崩潰(段錯誤)