本節重點
- 初步理解一切皆文件
- 理解文件緩沖區的分類
- 用戶級文件緩沖區與內核級文件緩沖區
- 用戶級文件緩沖區的刷新機制
- 兩級緩沖區的分層協作
一、虛擬文件系統
1.1 理解“一切皆文件”
我們都知道操作系統訪問不同的外部設備(顯示器、磁盤、鍵盤、鼠標、網卡)時都會通過相應的驅動程序,由于各種外設之間的差異在驅動程序中對每個外設的輸入輸出(如獲取設備狀態、屬性)的相關方法的實現都不盡相同:
?我們說操作系統是對軟硬件資源進行管理的軟件,在內核中要對硬件資源進行管理首先需要讓操作系統看到硬件資源,也就是將硬件資源“先描述再組織”:
在操作系統內核中通過類似struct device的結構體來對每種外設進行描述,再通過鏈表的方式將硬件資源管理起來,此時這個數據結構就表示操作系統啟動時默認看到的和打開的外設資源:
當用戶運行自己的代碼與數據時,操作系統就會在內核空間創建進程PCB,其中PCB中的struct files struct指針指向的文件描述符表則記錄了該進程打開的文件的數量。而我們知道文件描述符表本質上是元素為struct file的一維數組,struct file中則詳細記錄了被打開文件的文件緩沖區和元數據,關鍵的是其中還記錄了指向文件操作的各種方法的指針(函數指針),這樣對文件的操作會通過函數指針跳轉到不同的對應外設的驅動層。
這樣即使外設之間存在差異,驅動程序的設計大相徑庭用戶訪問涉及到不同類型外設的文件時也能獲得相似的方法,以為在內核通過函數指針已經幫用戶完成了差異化的方法調用。
二、文件緩沖區
2.1 什么是緩沖區
緩沖區是內存空間的一部分,用來暫時存儲輸入或者輸出的數據內容,這部分預留的空間就叫做緩沖區。緩沖區根據其對接的是輸入設備還是輸出設備分為輸入緩沖區與輸出緩沖區。
2.2 為什么引入緩沖區?
關鍵1:語言級文件操作都會調用系統調用
在介紹操作系統時我們了解到:操作系統為了不直接暴露內核,為上層用戶提供了各類系統調用。在語言層面對文件操作的各類函數接口底層都封裝了系統調用。
例如,以C語言為例fopen,fread,fwrite底層都分別封裝了open,read,write系統調用。
所以本質上我們使用各類編程語言進行文件操作(如I/O操作)都會調用系統調用。
關鍵2:系統調用是有代價的?
在之后的學習中我們會了解到,當程序執行系統調用時,CPU會從用戶態切換到內核態這個過程涉及到保護用戶程序的寄存器狀態,切換頁表,加載內核代碼段等操作。當系統調用完成時,CPU會從內核態返回到用戶態,此時CPU需要恢復用戶程序的寄存器狀態,整個操作會涉及到數百到數千個CPU周期。
關鍵3:緩沖區的引入可以減少系統調用次數
以向文件中寫入數據為例,當我們引入緩沖區的概念后,對文件的輸入操作意味著我們可以逐漸將數據塊輸入到緩沖區中,然后通過適當的緩沖機制調用系統調用將數據塊整體寫入到文件中,大大減少了系統調用的次數,大大提高了輸入效率。
2.3?緩沖區的分類
2.3.1 用戶級(C語言為例)
C標準庫中的I/O函數(printf、fwrite、fgets)均圍繞流的概念設計。每個流(stdout、stderr、stdin、用戶自定義的文件流)都由一個FILE結構體來表示,該結構體包含一個緩沖區以及緩沖策略(行緩沖、全緩沖、無緩沖)。
在C標準庫中對結構體FILE的描述如下:
//FILE本質上是定義的一個宏在/usr/include/stdio.h中typedef struct _IO_FILE FILE;
//在/usr/include/libio.h
struct _IO_FILE
{
int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags//緩沖區相關
/* The following pointers correspond to the C++ streambuf protocol. */
/* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
char* _IO_read_ptr; /* Current read pointer */
char* _IO_read_end; /* End of get area. */
char* _IO_read_base; /* Start of putback+get area. */
char* _IO_write_base; /* Start of put area. */
char* _IO_write_ptr; /* Current put pointer. */
char* _IO_write_end; /* End of put area. */
char* _IO_buf_base; /* Start of reserve area. */
char* _IO_buf_end; /* End of reserve area. *//* The following fields are used to support backing up and undo. */char *_IO_save_base; /* Pointer to start of non-current get area. */char *_IO_backup_base; /* Pointer to first valid character of backup area */char *_IO_save_end; /* Pointer to end of non-current get area. */struct _IO_marker *_markers;struct _IO_FILE *_chain;int _fileno; //封裝的?件描述符#if 0
int _blksize;
#else
int _flags2;
#endif
_IO_off_t _old_offset; /* This used to be _offset but it's too small. */
#define __HAVE_COLUMN /* temporary */
/* 1+column number of pbase(); 0 is unknown. */
unsigned short _cur_column;
signed char _vtable_offset;
char _shortbuf[1];
/* char* _save_gptr; char* _save_egptr; */
_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};
?緩沖機制
在C標準庫(stdio.h)總共定義了三個緩沖機制,每個流(FILE結構體)在其生命周期中通常只配置一個緩沖機制。以下是對三個緩沖機制的介紹:
注意事項:
除了以上默認的刷新方式下列特殊清空也會引發緩沖區的刷新:
- 緩沖區被寫滿
- 顯式刷新(如調用flush)
當緩沖區為行緩沖但是始終沒有遇到換行符(\n)時,當緩沖區滿時會自動提交。?
當涉及磁盤文件操作時默認為全緩沖,當所操作的流涉及一個終端(顯示器)時默認為行緩沖,stderr默認不帶緩沖區即無緩沖。
這里舉一個代碼示例:
//code.c
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
int main()
{const char* s1="hello printf\n";const char* s2="hello fwrite\n";printf("%s",s1);fwrite(s2,1,strlen(s2),stdout);fork();return 0;
}
運行結果:
?首先printf與fwrite將字符串寫入stdout對應的緩沖區中,當涉及到對終端(顯示器)的操作時為行緩沖,所以字符串會依次刷新提交。
此時我們執行以下指令:將程序重定向到一個文本文件(text.txt)中
./code 1> text.txt
?運行結果:
?此時我們發現同一份代碼數據被打印了兩次,原因是當我們進行重定向操作后就成為了用戶對磁盤文件(text.txt)的文件操作,默認緩沖機制變成了全緩沖。
當我們創建子進程之前,父進程的兩個數據(hello printf / hello fwrite)還在緩沖區中并沒有被刷新提交,而我們知道子進程是父進程的副本,當創建子進程時緩沖區中的數據也一并拷貝給了子進程,當程序結束后會自動刷新text.txt文件流的緩沖區,導致數據被打印了兩次。
2.3.2 內核級
在Linux系統中,內核級緩沖區是用于在內核空間和用戶空間之間傳遞數據的關鍵機制。它通常用于提高I/O操作的效率,減少系統調用的次數,并優化數據的傳輸。
內核級緩沖區的類型:
1> 頁緩存
用于緩存文件數據,減少磁盤I/O操作。當文件被讀取時,數據會被緩存在頁緩存中,后續的讀取操作可以直接從緩存中獲取數據,而不需要再次訪問磁盤。
2> 塊設備緩沖區
用于緩存塊設備的數據,如硬盤的塊數據。它與頁緩存類似,但更專注于塊設備的I/O操作。
3> 套接字緩沖區
用于網絡通信,管理網絡數據包的傳輸。每個網絡數據包都會被封裝在sk_buff結構中,以便在內核中進行處理。
與用戶級緩沖區類似,內核級緩沖區也有刷新機制但是在實現方面會復雜很多。以為在內核層面操作系統要考慮的因素會更多,比如刷新操作可能涉及大量的內存操作,不當的刷新策略可能導致系統資源耗盡或內存泄漏,還有在多核或多線程環境下,內核級緩沖區的刷新機制需要處理并發訪問問題。這通常需要引入復雜的同步機制,如自旋鎖或讀寫鎖,以確保數據的一致性和完整性等等
?以下是內核級緩沖區的刷新機制,可以來了解一下:
- 定期刷新:內核會定期將緩沖區中的數據寫入存儲設備。這種刷新通常由內核的守護進程負責,確保數據在一定時間間隔內被寫入磁盤。
- 顯式刷新:應用程序可以通過系統調用(如?
fsync
?或?fdatasync
)顯式請求將緩沖區中的數據刷新到存儲設備。 - 緩沖區滿時刷新:當內核緩沖區達到一定容量時,內核會自動將數據刷新到存儲設備。
- 文件關閉時刷新:當應用程序關閉文件時,內核會自動將與該文件相關的緩沖區數據刷新到存儲設備。
- 內存壓力:當系統內存不足時,內核可能會主動刷新緩沖區以釋放內存。這種機制確保系統在高內存壓力下仍能正常運行。?
2.3 兩級緩沖區的聯系
關鍵詞:分層協作
當應戶程序通過用戶級緩沖區寫入數據時,數據首先存儲在用戶空間的緩沖區中。當緩沖區滿或顯式調用刷新函數時,數據會被復制到內核級緩沖區。內核級緩沖區進一步管理數據的物理寫入操作,確保數據最終被寫入磁盤或發送到網絡設備。
我們可以通過下圖來理解:
這種分層緩沖機制減少了頻繁的系統調用,提高了數據處理的效率。同時,內核級緩沖區還可以利用更高級的優化技術,如延遲寫入和批量處理,進一步提升系統性能。