Linux操作系統之文件(三):緩沖區

前言:

上節課我們講授重定向的概念時,曾提到了一點緩沖區的概念。本文將會為大家更詳細的帶來緩沖區的有關內容:用戶級緩沖區是什么,以及其與內核級緩沖區的關系,最后,我會為大家模擬實現一下stdio.h的關于FILE結構體的有關內容,當然,這個模擬只是從原理上實現。真正的stdio.h肯定會更加復雜。

一、再談緩沖區

我們上篇文章鏈接曾做了一個實驗:

這里我們加了一個fflush之后,就可以在文件中看到我們的輸出結果了。當時我們解釋的是“?printf?的輸出被緩沖在內存中,尚未寫入文件,而程序結束時沒有觸發緩沖區的自動刷新。”?

這句話還是太抽象了,那么更加具體點的解釋是什么呢?

我們曾經說過,內核級緩沖區的出現,是為了提高IO的效率,讓數據積累后再一次性保存在文件中,而不是一次一次的連續IO。

那么我們的C語言,同樣也是為了提高效率,也實現了一個緩沖區,而這個緩沖區被我們稱為用戶級緩沖區。我們調用的printf,fprintf,fputs實際上都是C語言封裝后的函數。這個緩沖區也恰好對應了這些函數。

在stdio.h文件中,FILE實際上是struct _IO_FILE 結構體的重命名。源碼部分如下:
?

?

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
? };

大家可以看到,在?_IO_FILE結構體中,有一段關于緩沖區的代碼,而這個緩沖區,就是我們說得用戶級的緩沖區。

它存在的意義與內核級緩沖區是一模一樣的,調用printf等C語言封裝后的函數,把數據拷貝到用戶級緩沖區里,隨后根據一定的條件,把堆積的數據拷貝到內核級緩沖區。

所以printf等一系列的C語言函數本質上,也是一個拷貝函數。

如果我們的目標文件是顯示器文件,那么這個拷貝條件就是行刷新,當檢測到\n時就會拷貝到內核級緩沖區。如果是普通文件,對應的就是全緩沖,即等待緩沖區寫滿后再刷新。

只要把數據從用戶級緩沖區拷貝到了內核級緩沖區,我們就認為把數據交給了操作系統,這個數據就和用戶無關了。


二、重定向與緩沖區

仍然是這個代碼:

#include <stdio.h>

#include <unistd.h>

#include <fcntl.h>

int main()

{

? ? close(1);

? ? int fd=open("log1.txt",O_WRONLY | O_CREAT | O_TRUNC, 0666);

? ? printf("%d\n",fd);

? ? return 0;

}

如果我們把close(fd)刪除掉,那又是什么情況呢?
?

我們可以看見,重定向后,雖然不能打印到顯示屏,但是文件中是存在打印的的結果的。

為什么把后面的close(fd)去掉后,文件中就存在打印結果了呢?

?先不著急回答,我們再把后面的close換成fclose:

可以看見,fclose也可以達成目標。

所以我們可以告訴大家原因了,當一個進程退出時,?它會自動刷新自己的用戶級緩沖區,但當你調用系統的close把fd關掉后,它想要把數據刷新到操作系統內部都沒有機會了。

而C語言的fflush與close也都只是把用戶級緩沖區的數據刷新到內核級緩沖區,最多也就是fclose會釋放?FILE?結構體,并調用?close(fd)?關閉底層文件描述符。

由于我們重定向了,導致原本打印1到顯示器文件的行緩沖模式變成了全緩沖,導致不會因為‘\n’而刷新數據。

把用戶級緩沖區的數據刷新到內核級我們可以調用fflush,那有沒有什么方式把內核級緩沖區數據刷新到文件里?

有的:

我們可以調用fsync系統調用接口。


總的來說,C語言之所以存在緩沖區,就是為了提高效率,也就是說,C語言從設計上,就十分注重效率。

那C++有沒有自己的用戶級緩沖區呢?

肯定是有的。但是C++的緩沖區效率沒有C語言高,所以才會出現在一些十分注重時間復雜度的算法題上面,出現同樣結構的思維的代碼,出現使用printf可以通過,但是使用cout無法通過的現象。


?

三、子進程與緩沖區

請看下面的代碼:

int main()

{

? ? //C庫函數

? ? pinrtf("hello printf\n");

? ? fprintf(stdout,"hello fprintf\n");

? ? const char*message="hello fwrite\n";

? ? fwrite(message,strlen(message),1,stdout);

? ? //系統調用

? ? const char*w="hello write";

? ? write(1,w,strlen(w));

? ?

? ?

? ? fork();

? ? return 0;

}

它的運行結果是:?

很好,看起來沒有問題,那我們試著重定向一下呢?

?

誒,為什么重定向后,C語言的打印就都執行了兩次,系統調用的打印只執行了一次呢?

這是因為重定向后,用戶級緩沖區的緩沖方式變成全緩沖,父子進程結束后各自fflush了一次到內核級緩沖區?,導致C語言中,拷貝到用戶級緩沖區的數據,各自被刷新到了內核級緩沖區一次,于是當內核級緩沖區的數據刷新到文件中時,就出現了這個現狀。

當我們把\n取消時:

int main()

{

? ? //C庫函數

? ? printf(" hello printf ");

? ? fprintf(stdout," hello fprintf ");

? ? const char*message=" hello fwrite ";

? ? fwrite(message,strlen(message),1,stdout);

? ? //系統調用

? ? const char*w="hello write\n";

? ? write(1,w,strlen(w));

? ?

? ?

? ? fork();

? ? return 0;

}

打印結果如下:
?

這就是因為,在打印到顯示器文件時,緩沖方式為行緩沖,我們沒有遇見\n,同樣導致了重復的刷新。

如果想要達成正確的打印效果,就需要子進程之前使用fflush刷新用戶級緩沖區。


四、模擬實現stdio.h?

相信前面的用例,已經能夠讓大家較為清楚的理解到緩沖區與重定向之間的精密聯系了。

那么現在我就來帶大家簡答的模擬實現一下stdio中的FILE結構體,與fwrite,fclose,fflush,fopen函數吧!!

(注意,本次模擬只是為了讓大家更能理解原理,真正的實現肯定有所不同)

?

我們先不管C標準庫中是什么樣子的,我們只知道,有.h文件,就自然要有.c文件來實現;

首先,我們先根據所知道的知識,把基礎的FILE結構體與這四個函數的聲明寫到.h文件里:

#pragma once#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<fcntl.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<stdlib.h>#define SIZE 1024//定義刷新方式
#define FLUSH_NONE 0
#define FLUSH_LINE 1
#define FLUSH_FULL 2struct _OI_FILE
{int flag;//刷新方式int fileno;//文件描述符char outbuffer[SIZE];//緩沖區int size;int cap;//TODO
};typedef struct _OI_FILE mFILE;mFILE *mfopen(const char *pathname, const char *mode);
int mfflush(mFILE *_stream);
size_t mfwrite(const void *ptr,int size, mFILE *stream);
int mfclose(mFILE *stream);

隨后,我們要在.c文件里實現:
第一個就是fopen函數了,首先,我們要先根據傳入的打開方式,通過open把文件描述符獲取到。而打開方式的判斷,我們選擇用if語句與strcmp相結合。

隨后,我們要為這個文件創建一個FILE結構體對象,并對里面的參數進行初始化:
設置緩沖區的大小,文件標識符,刷新方式,以及容量,這里的容量我們選擇在.h中定義一個SIZE為1024常數,當然,要記得判斷一下是否失敗。

mFILE *mfopen(const char *pathname, const char *mode)
{int fd=-1;// 根據打開模式選擇不同的文件打開方式 if(strcmp(mode,"w")==0)  // 寫入模式:創建/截斷文件 {fd=open(pathname,O_WRONLY | O_CREAT | O_TRUNC, 0666);}else if(strcmp(mode,"r")==0)  // 只讀模式 {fd=open(pathname,O_RDONLY);}else if(strcmp(mode,"a")==0)  // 追加模式 {fd=open(pathname,O_WRONLY | O_CREAT | O_APPEND, 0666);}// 檢查文件是否成功打開 if(fd<0){return NULL;}// 分配mFILE結構體內存 mFILE* mf=malloc(sizeof(mFILE));// 內存分配失敗處理 if(!mf){close(fd);return NULL;}// 初始化結構體字段 mf->flag=FLUSH_LINE;     // 默認行緩沖模式 mf->fileno=fd;           // 設置文件描述符 mf->size=0;              // 初始化緩沖區大小為0 mf->cap=SIZE;            // 設置緩沖區容量 return mf;
}

隨后就是fwrite拷貝函數,他最主要的作用就是通過memcpy函數將數據拷貝到用戶級緩沖區:

我們要注意的是,size參數始終代碼我們的大小,所以outbuffer[size-1]就代表當前數據的最后一個字符。所以我們可以通過這個特性找到memcpy的初始地址,以及判斷是否最后一個字符為\n。根據我們的刷新方式參數以及size的大小是否不為0,來調用我們的mfflush刷新。

而fflush的刷新就更加簡單,我們只需要判斷size大小是否大于0,隨后調用系統接口write,將用戶級緩沖區的內容拷貝到內核級緩沖區就行了。

int mfflush(mFILE *_stream)
{// 檢查緩沖區是否有數據需要刷新 if(_stream->size>0){// 將緩沖區數據寫入文件 write(_stream->fileno,_stream->outbuffer,_stream->size);_stream->size=0;  // 重置緩沖區大小 }return 0;  // 總是返回成功 
}size_t mfwrite(const void *ptr,int size, mFILE *stream)
{// 將數據拷貝到緩沖區 memcpy(stream->outbuffer+stream->size,ptr,size);stream->size+=size; // 更新緩沖區當前大小 // 根據刷新模式決定是否立即刷新 if(stream->flag==FLUSH_LINE && stream->size>0&&stream->outbuffer[stream->size-1]=='\n')  // 行緩沖且遇到換行符 {mfflush(stream);}else if(stream->flag==FLUSH_FULL&&stream->size>=stream->cap)  // 全緩沖且緩沖區滿 {mfflush(stream);}return size;  // 返回成功寫入的字節數
}

最后只剩下一個mfclose。這個函數又該怎么實現呢?

我們需要先明確一下該函數應該完成的任務:

  1. 刷新緩沖區:如果緩沖區還有未寫入的數據(size > 0),調用mfflush寫入內核緩沖區。

  2. 關閉文件描述符:使用close()?關閉 fileno文件描述符的文件。

  3. 釋放內存:釋放 mFILE 結構體占用的內存。

所以具體實現如下:

int mfclose(mFILE *stream) 
{if (stream == NULL) {return -1;  // 錯誤:傳入空指針}// 1. 刷新緩沖區(如果還有未寫入的數據)if (stream->size > 0) {mfflush(stream);}// 2. 關閉文件描述符int ret = close(stream->fileno);if (ret < 0) {// 關閉失敗,但仍然需要釋放內存free(stream);return -1;}// 3. 釋放 mFILE 結構體free(stream);return 0;  // 成功
}

?實現還是比較簡單的,最重要的就是理解這些函數干了什么事情,達成了什么效果,在內部一一通過系統調用或者函數的復用來實現。

另外,請注意我們應該把在.c文件中用到的各種函數、系統調用的相關頭文件,在.h中進行聲明。

最后,我們可以添加一個測試用例:

?

int main()
{mFILE *mf = mfopen("test.txt", "w");if (!mf) {perror("mfopen failed");  // 打印錯誤信息return 1;}size_t written = mfwrite("Hello", 5, mf);  // written = 5mfclose(mf);return 0;
}

?


?

總結:
?

?本文我們繼續詳細談了緩沖區有關的概念,并為大家模擬實現了FILE結構體的有關內容,希望通過這些知識點,能夠幫助你更加了解操作系統中,文件的緩沖區的相關知識。

如果有任何疑問與指正歡迎私信或者評論區留言

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/diannao/89799.shtml
繁體地址,請注明出處:http://hk.pswp.cn/diannao/89799.shtml
英文地址,請注明出處:http://en.pswp.cn/diannao/89799.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

Linux云計算基礎篇(7)

一、< 輸入重定向 wc -l < filelist .txt 統計數據&#xff0c;從file這個文件拿結果。 二、tr 轉換字符命令 $ tr A-Za-z<.bash_profile 將bash_profile文件中的大寫字符全部轉成小寫字符 三、管道符&#xff08;|&#xff09; com…

【學習筆記】Lean4基礎 ing

文章目錄 概述參考文檔運行程序elan 命令行工具lean 命令行工具lake 命令行工具運行單文件程序Hello, world!驗證 Lean4 證明 運行多文件項目 Lean4 基礎語法注釋表達式求值變量和定義定義類型變量 定義函數命名規則命名空間數據類型結構體構造子模式匹配多態List 列表Option 可…

FPGA實現40G網卡NIC,基于PCIE4C+40G/50G Ethernet subsystem架構,提供工程源碼和技術支持

目錄 1、前言工程概述免責聲明 3、相關方案推薦我已有的所有工程源碼總目錄----方便你快速找到自己喜歡的項目我這里已有的以太網方案 4、工程詳細設計方案工程設計原理框圖測試用電腦PClE4CDMA40G/50G Ethernet subsystem工程源碼架構驅動和測試文件 5、Vivado工程詳解1詳解&a…

SAP從入門到放棄系列之流程管理概述

文章目錄前言1.Process Management&#xff08;過程管理&#xff09;2.關鍵術語2.1Control recipe destination2.2 Process instruction characteristic2.3 Process message characteristic2.4 Process instruction category2.5 Process message category2.6 PI sheet3.關鍵配置…

RCLAMP0554S.TCT升特Semtech 5通道TVS二極管,0.5pF+20kV防護,超高速接口!

RCLAMP0554S.TCT&#xff08;Semtech&#xff09;產品解析與推廣文案 一、產品定位 RCLAMP0554S.TCT是Semtech&#xff08;升特半導體&#xff09;推出的5通道超低電容TVS二極管陣列&#xff0c;專為超高速數據接口&#xff08;USB4/雷電4/HDMI 2.1&#xff09;提供靜電放電&a…

【人工智能】DeepSeek的AI實驗室:解鎖大語言模型的未來

《Python OpenCV從菜鳥到高手》帶你進入圖像處理與計算機視覺的大門! 解鎖Python編程的無限可能:《奇妙的Python》帶你漫游代碼世界 DeepSeek作為中國AI領域的先鋒,以其開源大語言模型(LLM)DeepSeek-V3和DeepSeek-R1在全球AI研究中掀起波瀾。本文深入探討DeepSeek AI實驗…

nacos+nginx動態配置大文件上傳限制

前言 今天還要跟大家分享的一個點就是微服務網關gateway用webflux響應式不用servlet后&#xff0c;引發的一個忽略點差點在演示的時候炸鍋&#xff0c;也不多講廢話&#xff0c;說說現象&#xff0c;說說處理就了事。 一、上傳超過20MB的視頻報錯 配置在nacos里&#xff0c;讀…

mr 任務運行及jar

mainclass如下&#xff1a;LoggingDriver

Python 數據分析:numpy,抽提,整數數組索引與基本索引擴展(元組傳參)。聽故事學知識點怎么這么容易?

目錄1 代碼示例2 歡迎糾錯3 論文寫作/Python 學習智能體------以下關于 Markdown 編輯器新的改變功能快捷鍵合理的創建標題&#xff0c;有助于目錄的生成如何改變文本的樣式插入鏈接與圖片如何插入一段漂亮的代碼片生成一個適合你的列表創建一個表格設定內容居中、居左、居右Sm…

ECU開發工具鏈1.10版:更強大的測量、校準與數據分析體驗.

汽車電子開發與測試領域&#xff0c;高效、精準且安全的工具是成功的基石。DiagRA X 作為一款廣受認可的 Windows 平臺綜合解決方案&#xff0c;持續引領行業標準。其最新發布的 1.10 版本帶來了顯著的功能增強與用戶體驗優化&#xff0c;進一步鞏固了其在 ECU 測量、校準、刷寫…

Qt C++串口SerialPort通訊發送指令讀寫NFC M1卡

本示例使用的發卡器&#xff1a;https://item.taobao.com/item.htm?spma21dvs.23580594.0.0.52de2c1bVIuGpf&ftt&id18645495882 一、確定已安裝Qt Serial Port組件 二、在.pro項目文件聲明引用Serialport組件 三、在.h頭文件內引用Serialport組件 四、在.cpp程序中實…

Go 語言開發中用戶密碼加密存儲的最佳實踐

在現代 Web 應用開發中&#xff0c;用戶密碼的安全存儲是系統安全的重要環節。本文將結合 Go 語言和 GORM 框架&#xff0c;詳細介紹用戶密碼加密存儲的完整解決方案&#xff0c;包括數據庫模型設計、加密算法選擇、鹽值加密實現等關鍵技術點。 一、數據庫模型設計與 GORM 實踐…

優化Facebook廣告投放的五大關鍵策略

一、精確篩選目標國家用戶在Audience的locations設置目標國家時&#xff0c;務必勾選"People living in this location"選項。系統默認會選擇"People living in this location or recently in this location"&#xff0c;這會擴大受眾范圍&#xff0c;包含…

Debian-10-standard用`networking`服務的`/etc/network/interfaces`配置文件設置多網卡多IPv6

Debian-10-buster-standard用networking服務的/etc/network/interfaces配置文件設置多網卡多IPv6 Debian-10-buster-standard用networking服務的/etc/network/interfaces配置文件設置多網卡多IPv6 250703_123456 三塊網卡 : enp0s3 , enp0s8 , enp0s9 /etc/network/interfac…

對話式 AI workshop:Voice Agent 全球五城開發實錄

過去幾個月&#xff0c;TEN Framework 團隊與 Agora 和聲網圍繞 “對話式AI”題&#xff0c;踏上了橫跨全球五大城市的精彩旅程——東京、舊金山、巴黎、北京、京都。 五場精心籌備的Workshop 場場爆滿&#xff0c; 匯聚了來自當地及全球的開發者、創業者、產品經理與語音技術愛…

算法學習筆記:6.深度優先搜索算法——從原理到實戰,涵蓋 LeetCode 與考研 408 例題

在計算機科學領域&#xff0c;搜索算法是解決問題的重要工具&#xff0c;其中深度優先搜索&#xff08;Depth-First Search&#xff0c;簡稱 DFS&#xff09;憑借其簡潔高效的特性&#xff0c;在圖論、回溯、拓撲排序等眾多場景中發揮著關鍵作用。無論是 LeetCode 算法題&#…

vue create 和npm init 創建項目對比

以下是關于 vue create 和 npm init 的對比分析&#xff1a; 1. 定位與功能 vue create 定位&#xff1a;Vue 官方提供的腳手架工具&#xff0c;基于 Vue CLI&#xff0c;用于快速創建標準化的 Vue 項目&#xff0c;支持 Vue 2 和 Vue 3。功能&#xff1a;提供交互式配置&…

C++ bitset 模板類

bitset<256> 數據類型詳解 bitset<256> 是 C 標準庫中的一個模板類&#xff0c;用于處理固定大小的位集合&#xff08;Bit Set&#xff09;。它可以高效地操作和存儲二進制位&#xff0c;特別適合需要處理大量布爾標志或簡單計數的場景。 基本定義與特性 1. 模板參…

通信握手言和:PROFINET轉EtherCAT網關讓汽輪機振動數據“破壁”傳輸

某大型電廠的關鍵汽輪機設備采用EtherCAT振動傳感器進行實時監測&#xff0c;但由于工廠PLC振動分析系統基于PROFINET協議&#xff0c;數據無法直接接入&#xff0c;導致振動數據延遲、預警滯后&#xff0c;嚴重影響設備健康管理。傳統的人工巡檢和定期維護難以捕捉早期機械故障…

golang 中當 JSON 數據缺少結構體(struct)中定義的某些字段,會有異常嗎

目錄關鍵影響示例演示潛在問題與解決方案問題 1&#xff1a;邏輯錯誤&#xff08;零值干擾&#xff09;問題 2&#xff1a;忽略可選字段問題 3&#xff1a;第三方庫驗證最佳實踐總結在 Go 語言中&#xff0c;當 JSON 數據缺少結構體&#xff08;struct&#xff09;中定義的某些…