C標準庫I/O緩衝區和用戶程序的緩衝區

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系統調用進入內核退出當前進程.

  (注:以前我們說main函數是程序的入口點其實不準確,_start纔是真正的入口點,而main函數是被_start調用的。_start首先做一些初始化工作(稱爲啓動例程),然後調用C代碼中提供的main函數。main函數最標準的原型應該是int main(int argc, char *argv[]),也就是說啓動例程會傳兩個參數給main函數,我們到目前爲止都把main函數的原型寫成int main(void),這也是C標準允許的,多傳了參數而不用是沒有問題的,少傳了參數卻用了則會出問題。由於main函數是被啓動例程調用的,所以從main函數return時仍返回到啓動例程中main函數的返回值被啓動例程得到,如果將啓動例程表示成等價的C代碼(實際上啓動例程一般是直接用匯編寫的),則它調用main函數的形式是:exit(main(argc, argv));也就是說,啓動例程得到main函數的返回值後,會立刻用它做參數調用exit函數。exit也是libc中的函數,它首先做一些清理工作,然後調用_exit系統調用終止進程,main函數的返回值最終被傳給_exit系統調用,成爲進程的退出狀態。我們也可以在main函數中直接調用exit函數終止進程而不返回到啓動例程,如 int main(void} {exit(4);}和 int main(void) { return 4;}的效果是一樣的。)

  C標準庫的I/O緩衝區有三種類型:全緩衝、行緩衝和無緩衝。當用戶程序調用庫函數做寫操作時, 不同類型的緩衝區具有不同特性。

  全緩衝:如果緩衝區寫滿了就寫回內核。常規文件通常是全緩衝的。

  行緩衝:如果用戶程序寫的數據中有換行符就把這一行寫回內核,或者如果緩衝區寫滿了就寫回內 核。標準輸入和標準輸出對應終端設備時通常是行緩衝的。

  無緩衝:用戶程序每次調庫函數做寫操作都要通過系統調用寫回內核。標準錯誤輸出通常是無緩衝的,這樣用戶程序產生的錯誤信息可以儘快輸出到設備。

  除了寫滿緩衝區、寫入換行符之外,行緩衝還有兩種情況會自動做Flush操作。如果: 用戶程序調用庫函數從無緩衝的文件中讀取或者從行緩衝的文件中讀取,並且這次讀操作會引發系統調用從內核讀取數據。那麼在讀取之前會自動Flush所有行緩衝。例如:

#include <stdio.h>

#include <unistd.h>

int main()

{

      char buf[20];

      printf("Please input a line: ");

      fgets(buf, 20, stdin);

      return 0;

}

    雖然調用printf並不會把字符串寫到設備,但緊接着調用fgets讀一個行緩衝的文件(標準輸入),在讀取之前會自動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緩衝區。


發佈了28 篇原創文章 · 獲贊 12 · 訪問量 4萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章