『 Linux 』緩沖區(萬字)

文章目錄

    • 🦦 什么是緩沖區
    • 🦦 格式化輸入/輸出
    • 🦦 刷新策略
      • 🪶 塊緩沖(fully buffered)
      • 🪶 無緩沖(unbuffered)
      • 🪶 行緩沖(line buffered)
    • 🦦 現象解釋
    • 🦦 exit()與_exit()
    • 🦦 進程崩潰或正常結束時對用戶態緩沖區的刷新
    • 🦦 什么是內核態緩沖區
      • 🪶 輸入緩沖區與輸出緩沖區
    • 🦦 簡單實現用戶層IO接口與緩沖區(供參考)
    • 🦦 緩沖區存在的意義


本文演示環境為CentOS 7.6

🦦 什么是緩沖區

請添加圖片描述

緩沖區(Buffer),顧名思義就是一塊可以用于緩存的空間;

也可以說實際上緩沖區是一種臨時存儲區域,一般用于在數據傳輸過程中對數據的緩存;

緩沖區的主要目的是協調數據產生者和消費者之間的速度差異以提高系統的效率和性能;

  • 那么具體什么是緩沖區?

存在幾個例子:

下文中所出現的例子都將使用兩種方式(直接運行 ,重定向至文件當中)以便于區分兩種情況的不同之處;

  • [例1]

    int main() {const char* str1 = "hello fwrite\n";const char* str2 = "hello write\n";// C標準庫接口printf("hello printf\n");fprintf(stdout, "hello fprintf\n");fwrite(str1, strlen(str1), 1, stdout);// 系統調用接口write(1, str2, strlen(str2));return 0;
    }
    

    在這段代碼當中分別用了 C標準庫接口系統調用接口 分別對不同的massage進行打印(不同的massage以便于區分打印的接口);

    • 直接運行

      $ ./mytest
      hello printf
      hello fprintf
      hello fwrite
      hello write
      

      從結果可以看出,當直接運行程序后對應的信息將按照既定的順序分別打印至終端;

    • 重定向至文件

      $ ./mytest > log.txt ; cat log.txt 
      hello write
      hello printf
      hello fprintf
      hello fwrite
      

      從結果看出,雖然程序的結果被打印了出來,但是對應的打印順序發生了變化;

  • [例2]

    int main() {const char* str1 = "hello fwrite\n";const char* str2 = "hello write\n";// 語言接口printf("hello printf\n");fprintf(stdout, "hello fprintf\n");fwrite(str1, strlen(str1), 1, stdout);// 系統調用接口write(1, str2, strlen(str2));fork();return 0;
    }
    

    這段代碼與第一段代碼的差距并不大;

    唯一的區別就是是否有調用fork()的區別;

    • 直接運行

      $ ./mytest 
      hello printf
      hello fprintf
      hello fwrite
      hello write
      

      運行程序后正常打印未出現其他現象;

    • 重定向至文件

      $ >log.txt;./mytest > log.txt ; cat log.txt 
      hello write
      hello printf
      hello fprintf
      hello fwrite
      hello printf
      hello fprintf
      hello fwrite
      

      而在該例子當中運行程序并將其重定向至文件當中將會出現兩個現象;

      分別為 打印順序變化 以及 打印數據量發生變化 ;

  • [例3]

    int main() {const char* str1 = "hello fwrite\n";const char* str2 = "hello write\n";// 語言接口printf("hello printf\n");fprintf(stdout, "hello fprintf\n");fwrite(str1, strlen(str1), 1, stdout);// 系統調用接口write(1, str2, strlen(str2));close(1);return 0;
    }
    

    在這段代碼當中,將fork()替換成了close(1);

    即關閉 文件描述符1;

    • 直接運行

      $ ./mytest 
      hello printf
      hello fprintf
      hello fwrite
      hello write
      

      當直接運行時程序將根據既定打印順序進行打印;

    • 重定向至文件

      $ >log.txt ; ./mytest > log.txt ;cat log.txt 
      hello write
      

      而當重定向至文件后發現真正寫進文件的內容只有 “hello write\n” ;

  • [例4]

    int main() {const char* str1 = "hello fwrite";const char* str2 = "hello write";// 語言接口printf("hello printf");fprintf(stdout, "hello fprintf");fwrite(str1, strlen(str1), 1, stdout);// 系統調用接口write(1, str2, strlen(str2));close(1);return 0;
    }
    

    該例以 例3 為基礎去除了打印時的\n;

    $ ./mytest 
    hello write [USER]$ >log.txt;./mytest >log.txt ;cat log.txt 
    hello write [USER]$ 
    

    從結果看出,對于 例3 而言無論是直接運行還是重定向至文件當中都只會寫入 “hello write” ;

而實際上上文中幾個例子所出現的幾種現象都與緩沖區以及其刷新策略有關;

具體的刷新策略將在下文中進行解釋;

在上文當中出現的幾個例子都存在一個共性,即分別都采用了系統調用接口與C標準接口進行演示;

這可以引入一個猜測:

  • C標準庫當中是否存在緩沖區?

答案是肯定的,在日常中利用一些高級語言在進行開發的過程當中,其對應的將會為用戶提供一個緩沖區;

這個緩沖區一般用于暫時存儲寫入的數據;

C語言 也如此,以printf這些格式化輸出函數為例,在 『 Linux 』基礎IO/文件IO (萬字) 當中提到,在 Linux 當中調用printf函數時實際上是向 文件描述符1 中對應的文件進行寫入操作;

而實際上其在對對應文件進行寫入時并不是直接進行寫入而是需要現將對應的數據寫入至緩沖區當中,而后再根據當時所需的策略將數據逐步寫入至文件當中;

Linux 環境下使用 C語言 等高級語言在進行開發時實際上存在著兩個緩沖區;

這兩個緩沖區為兩個不同層面的緩沖區,分別為:

  • 用戶態緩沖區
  • 內核態緩沖區

本文將著重解釋用戶態緩沖區,不對內核態緩沖區進行細節解釋;


🦦 格式化輸入/輸出

請添加圖片描述

printf()函數,是 C語言 學習當中最早接觸的函數;

其的功能就是將數據打印至顯示屏(終端);

在之前的博客中提到其實際上其底層是調用系統調用接口write()文件描述符1 進行寫入;

  • 存在一行代碼:

    int a = 10;
    printf("hello world %d\n",a);
    

當執行這兩行代碼后對應的終端將會打印出hello world 10;

  • 那么這里打印出的這行內容是什么類型的內容?
  • 對應的%d為什么會被轉化為a?

顯示器(終端)只能顯示文本,故輸出的是字符串類型;

而對應的%d被轉化為a的數據就是因為將對應的數據進行了一些列的格式化;

這也是 格式化輸入/輸出 名稱的由來;

在這兩行代碼中,將會進行以下操作:

  • 解析格式字符串

    printf將首先讀取格式字符串"hello world %d\n",并從左向右依次遍歷,直至遇到第一個 格式指定符 ,在這里為%d;

  • 匹配參數

    當遇到格式指定符后,將會找到對應的數據來替換這個占位符;

  • 格式化輸出

    在該段代碼當中,printf將會把整數10轉化成對應的字符串表示"10"并將其插入到%d的位置;

而在進行完上述的工作之后,printf并不會馬上將數據直接寫入;

而是暫存至其用戶態的緩沖區當中,直至在對應的數據他們將會被寫入內核態緩沖區當中;

  • 那么數據是如何從用戶態緩沖區被刷新至內核態緩沖區當中的?

    glibc為例,對于printf()而言,該函數在調用過程中最后將會調用write()系統調用接口并清空用戶態緩沖區中的數據;

    但通常這是通過一系列的層次和封裝間接完成的;

    在此只需要明白當用戶態緩沖區中的數據需要刷新至內核態緩沖區時最終都需要直接或者間接調用write()函數即可;

當調用write()對文件進行寫入時OS將通過進程的task_struct逐步尋找至對應的文件的內核態緩沖區;

而在內核態緩沖區的刷新一般取決于當前cpu資源的調用;

并將數據寫入至其內核態緩沖區當中;

  • 對于上文所提到的刷新策略又是什么?與數據的刷新有什么關聯?

🦦 刷新策略

請添加圖片描述

在上文當中拋出了一個未解答的問題:

  • 刷新策略是什么?與數據的刷新有什么關聯?

顧名思義,刷新策略是根據不同的情況對由用戶態緩沖區數據刷新至內核態緩沖區的不同方案;

刷新策略主要為三種;

分別為: 無緩沖刷新策略(unbuffered) , 行緩沖刷新策略(line buffered) , 塊緩沖刷新策略(fully buffered) ;

同時在C標準庫當中存在一個函數為setvbuf();

該函數可以修改一個已經打開的文件流對應的刷新策略;

SYNOPSIS#include <stdio.h>int setvbuf(FILE *stream, char *buf, int mode, size_t size);

其中mode為更新后的刷新策略;

              _IONBF unbuffered_IOLBF line buffered_IOFBF fully buffered

同時,用戶態緩沖區當中的刷新策略將根據文件流的指向進行更新;

這里的文件流的指向具體表現在文件描述符當中,這也包括上文舉的幾個例子中所提到的重定向至文件的操作;

重定向具體是底層調用dup()系統調用接口使得文件描述符的指向發生變化;

這也能夠解釋在上文中的[例1]至[例4]中出現的現象;


🪶 塊緩沖(fully buffered)

請添加圖片描述

在塊緩沖(全緩沖)的刷新策略下,數據將會在緩沖區滿時或者是顯示調用fflush()函數時刷新;

標準庫會在緩沖區滿時自動調用write()系統調用接口將緩沖區的數據寫入至內核態緩沖區當中;

一般情況下在對普通文件流的寫入時將使用行緩沖的刷新策略;

存在一段代碼:

#define filename "test.txt"
int main() {FILE* fp = fopen(filename, "w");//不考慮打開失敗fprintf(fp, "fully buffered test\n");sleep(5);fclose(fp);return 0;
}

在該段代碼當中以只寫的方式打開一個名為test.txt的文件;

并調用fprintf()將對應的信息寫入至文件后調用sleep(5)使得進程睡眠5s;

在此處可以使用shell腳本對文件內容進行實時監測;

while :; do cat test.txt ; sleep 1;echo "--------------" ;done

運行shell腳本并運行程序后最終結果為:

--------------
--------------
--------------
--------------
--------------
fully buffered test

從結果可以看出,在運行后的前5s并不會將數據寫入至文件當中;

而是當調用fclose()后對應的將對用戶態緩沖區進行一次刷新,同時在此時數據被寫入至文件當中;


🪶 無緩沖(unbuffered)

請添加圖片描述

在無緩沖模式下,每次 I/O 都會立即調用 write系統調用接口,將數據直接寫入到內核緩沖區;

不會使用使用用戶態緩沖區;

一般情況下, stderr 文件流的數據將采用無緩沖的策略進行刷新;

具體原因是因為防止進程崩潰時對應的錯誤信息仍然被阻塞至用戶態緩沖區當中;

存在一段代碼:

#define filename "test.txt"
int main() {FILE* fp = fopen(filename,"w");//不考慮打開失敗setvbuf(fp, NULL, _IONBF, 0);fprintf(fp,"unbuffered test\n");sleep(50);fclose(fp);return 0;
}

在該程序當中以只寫的方式打開了一個名為test.txt的文件;

并調用setvbuf()將緩沖區的刷新策略更新為無緩沖刷新策略;

再使用fprintf()將數據進行寫入后調用sleep(50)使得進程進入睡眠狀態50s;

在進程未退出時在終端中使用cattest.txt中的內容顯示至終端中;

$ cat test.txt 
unbuffered test

可以發現進程未退出時數據仍被寫入至文件當中;

正常情況下,在對普通文件進行寫入時采用的刷新策略為全緩沖的刷新策略;

而在該例子中調用了setvbuf()后將刷新策略更新為無緩沖,從而使得其能夠直接將數據寫入文件且不需要顯式調用fflush()或是等待用戶態緩沖區被寫滿;


🪶 行緩沖(line buffered)

請添加圖片描述

在行緩沖模式下,數據將會在遇到換行符(\n)或是用戶態緩沖區被寫滿時自動刷新;

此時,標準庫將會直接調用write()系統調用接口,將緩沖區的數據寫到內核態緩沖區當中;

一般情況下, stdout 文件流的數據將采用行緩沖的刷新策略;

存在一段代碼:

#define filename "test.txt"
int main() {FILE* fp = fopen(filename, "w");// 不考慮打開失敗char* massages[] = {"line buffered test 1\n", "line buffered test 2\n","line buffered test 3\n", "line buffered test 4\n","line buffered test 5\n"};setvbuf(fp, NULL, _IOLBF, 0);for (int i = 0; i < sizeof(massages) / sizeof(char*);++i){fprintf(fp,"%s",massages[i]);sleep(1);}fclose(fp);return 0;
}

在這段代碼當中以只寫的方式打開名為test.txt的文件;

并定義了一個char* massages[]數組,數組中的每個元素都存放一個類型為char*的字符串;

由于普通文件的刷新策略為全緩沖;

故在這里調用setvbuf()將對應的刷新策略更新為行緩沖;

在更新刷新策略后用while()循環調用fprintf()依次將對應的數據寫入至文件當中,每次打印時都sleep(1);

此時運行腳本對文件test.txt進行實時監控并運行程序;

$ > test.txt ; ./mytest & while :; do cat test.txt ; sleep 1;echo "--------------" ;done #./mytest & 為隱式執行進程
[1] 9859
line buffered test 1
--------------
line buffered test 1
line buffered test 2
--------------
line buffered test 1
line buffered test 2
line buffered test 3
--------------
line buffered test 1
line buffered test 2
line buffered test 3
line buffered test 4
--------------
line buffered test 1
line buffered test 2
line buffered test 3
line buffered test 4
line buffered test 5
[1]+  Done                    ./mytest

正常來說將數據寫入普通文件當中其刷新策略為全緩沖的刷新策略;

而此時調用了setvbuf()將刷新策略更新為行緩沖;

故對應的數據將會根據順序逐條寫入文件當中;


🦦 現象解釋

請添加圖片描述

在上文當中解釋了緩沖區以及緩沖區的刷新策略;

而在本節中可以對上文中所舉的四個例子進行解釋;

  • [例1]現象解釋

    int main() {const char* str1 = "hello fwrite\n";const char* str2 = "hello write\n";// C標準庫接口printf("hello printf\n");fprintf(stdout, "hello fprintf\n");fwrite(str1, strlen(str1), 1, stdout);// 系統調用接口write(1, str2, strlen(str2));return 0;
    }
    

    在這段代碼當中分別調用了C標準庫接口與系統調用接口write()分別對數據打印;

    其對應的結果為:

    $ ./mytest
    hello printf
    hello fprintf
    hello fwrite
    hello write$ > log.txt ;./mytest > log.txt ; cat log.txt 
    hello write
    hello printf
    hello fprintf
    hello fwrite
    
    • 運行程序

      當運行程序后數據將以既定的順序進行打印;

      原因是無論是printf(),fprintf(stdout),fwrite(stdout)三個調用都是將數據輸出至標準輸出stdout當中;

      而將數據寫入標準輸出stdout時所采用的刷新策略是 行緩沖 ;

      在該例中C標準庫函數所打印的字符串都帶換行符\n;

      而行緩沖的刷新策略為遇到\n時將會將數據由用戶態緩沖區刷新至內核態緩沖區當中;

      而對于系統調用接口write()而言,其屬于系統層的接口函數,本身與用戶層的接口函數將產生一個解耦合的關系;

      在調用write()時其并不會被寫入至用戶態緩沖區當中;

      故當正常運行程序時將以正常的打印順序進行打印;

    • 重定向至文件

      在上文中提到,當發生重定向時對用戶態緩沖區中數據的刷新策略也將跟著更新;

      當運行程序并將結果重定向至文件當中時,對應的 行緩沖 刷新策略將被更新為 塊緩沖 ;

      塊緩沖 刷新條件必須滿足以下其中一點:

      • 用戶態緩沖區被寫滿
      • 顯示調用fflush()對用戶態緩沖區進行刷新
      • 文件流被關閉
      • 進程正常結束并退出

      而在該例子當中并未顯示調用fflush(),同時數據量并未達到緩沖區的最大值;

      故這些數據將一直被保留在用戶態緩沖區當中;

      而對于write()調用而言其數據可以直接被寫入至內核態緩沖區當中并不需要經過用戶態緩沖區;

      故最終寫入至文件時其順序將發生變化;

  • [例2]現象解釋

    int main() {const char* str1 = "hello fwrite\n";const char* str2 = "hello write\n";// 語言接口printf("hello printf\n");fprintf(stdout, "hello fprintf\n");fwrite(str1, strlen(str1), 1, stdout);// 系統調用接口write(1, str2, strlen(str2));fork();return 0;
    }
    

    這段代碼與上段代碼唯一的區別就是是否有調用fork()接口;

    其對應的結果為:

    $ ./mytest 
    hello printf
    hello fprintf
    hello fwrite
    hello write$ >log.txt;./mytest > log.txt ; cat log.txt 
    hello write
    hello printf
    hello fprintf
    hello fwrite
    hello printf
    hello fprintf
    hello fwrite
    
    • 運行程序

      當運行程序后將以既定的順序進行打印;

      其與 [例1] 中的直接運行相同;

      即在行緩沖的刷新策略時每遇到一次\n將會調用write()進行一次刷新,再此不進行贅述;

      而當再調用fork()創建子進程時父子進程未執行完的只有return;

      故沒有任何變化;

    • 重定向至文件

      當重定向至文件時,該刷新策略將被更新為全緩沖 (全緩沖的概念參照上文中對全緩沖的解釋) ;

      write()為系統調用,將直接把數據寫入至內核態緩沖區并進行刷新;

      故在重定向至文件時write()調用的信息將優先進行寫入;

      而由于進程未退出,此時用戶態緩沖區當中仍存在未被刷新至內核緩沖區的數據;

      此時在調用fork()創建子進程時;

      由于子進程是父進程的一個拷貝,其將繼承其父進程的代碼數據;

      而用戶態緩沖區中的數據也屬于代碼數據的一部分,故創建子進程后父子進程在宏觀上仍具有自己的用戶緩沖區,但實際上可能還未給子進程分配這部分內存空間;

      當其中一個進程刷新其用戶態緩沖區時,其動作即為對內存空間進行清空操作;

      而清空操作本質上屬于一個寫入操作;

      故在這里會發生 寫時拷貝 ,最終父子進程都會將自己的用戶態緩沖區刷新至內核態緩沖區當中并寫入文件內;

      故在該例子當中會出現寫入重復的現象;

  • [例3]現象解釋

    int main() {const char* str1 = "hello fwrite\n";const char* str2 = "hello write\n";// 語言接口printf("hello printf\n");fprintf(stdout, "hello fprintf\n");fwrite(str1, strlen(str1), 1, stdout);// 系統調用接口write(1, str2, strlen(str2));close(1);return 0;
    }
    

    這段代碼與 [例1] 唯一的區別就是是否調用close(1)關閉 文件描述符1 ;

    其對應的結果為:

    $ ./mytest 
    hello printf
    hello fprintf
    hello fwrite
    hello write$ >log.txt ; ./mytest > log.txt ;cat log.txt 
    hello write
    
    • 運行程序

      當直接運行程序后其最終結果與上文中其他例子的結果相同;

      其原理也相同;

      即行緩沖策略刷新;

      對應的close(1)并不對其造成影響;

    • 重定向至文件

      當重定向至文件后對應的行緩沖刷新策略將被更新為全緩沖;

      此時只能等待關閉文件流或是程序正常退出時才會對用戶態緩沖區進行刷新;

      write()系統調用接口不經過用戶態緩沖區,故可以直接進行寫入操作;

      在關閉文件流前調用了close(1)關閉了對應的 文件描述符1 ;

      而在重定向過后該 文件描述符1 所對應的文件為重定向后的文件;

      因為 文件描述符 被關閉故無法進行寫入操作;

  • [例4]現象解釋

    int main() {const char* str1 = "hello fwrite";const char* str2 = "hello write";// 語言接口printf("hello printf");fprintf(stdout, "hello fprintf");fwrite(str1, strlen(str1), 1, stdout);// 系統調用接口write(1, str2, strlen(str2));close(1);return 0;
    }
    

    該例與 [例3] 的唯一區別即為少了換行符\n;

    其運行結果為:

    $ ./mytest 
    hello write [USER]$ >log.txt;./mytest >log.txt ;cat log.txt 
    hello write [USER]$ 
    

    可以發現無論是直接運行還是重定向至文件當中其最終結果都相同;

    • 直接運行

      當直接運行時其刷新策略為行緩沖刷新策略;

      而行緩沖刷新策略為在每次遇到換行符\n進行一次刷新;

      而這段打印當中并不存在換行符\n;

      對于write()系統調用而言其并不經過用戶態緩沖區故可以直接打印;

      write()過后調用close(1)后,即使進程正常結束對應的用戶態緩沖區的數據也無法被刷新至內核態并打印;

    • 重定向至文件當中

      重定向至文件當中與直接運行概念完全相同,只不過被重定向至普通文件后刷新策略將更新為全緩沖;

      在此不進行贅述;


🦦 exit()與_exit()

請添加圖片描述

c/C++當中存在著這么一個接口exit();

NAMEexit  - cause normal process termi‐nationSYNOPSIS#include <stdlib.h>void exit(int status);DESCRIPTIONThe exit() function  causes  normal process  termination  and the value of status & 0377is returned to the parent (see wait(2)).

這個接口是一個 C標準庫 提供的接口;

這個接口能夠使得用戶能夠在調用該接口后正常退出進程同時做好結束的清理工作,并返回一個對應的退出碼;

其中int status即為需要返回的退出碼,用戶可以根據該退出碼判斷程序出錯的位置以及原因;

一般情況下exit(0)表示正常退出(其余退出信息參考官方文檔);

  • int main() {printf("hello world");exit(0);return 0;
    }
    

    在這段代碼中調用了printf打印一條信息;

    運行結果如下:

    $ ./mytest 
    hello world $
    

    在上文中了解到當將數據寫入至stdout文件流時采用的刷新策略為行緩沖;

    而行緩沖的刷新條件為 當遇到換行符\n或者是緩沖區被寫滿時將對緩沖區進行刷新 ;

    而在該例子中所打印的信息并不存在換行符,只能等待進程正常結束時退出;

    此時即使調用了exit()后數據也被進行打印,說明該接口會刷新用戶態緩沖區;

而在Linux內核當中同樣存在著一個類似的接口為_exit()/_Exit();

NAME_Exit, _exit  -  terminate  a process
SYNOPSIS#include <stdlib.h>void _Exit(int status);#include <unistd.h>void _exit(int status);
  • _exit() 是 POSIX 標準中定義的系統調用。
  • _Exit() 是由 ISO C99 標準引入的函數。

這是一個系統調用接口,與exit()并不相同;

如果聯系的話可以說exit()封裝了_exit()或是_Exit();

//基于glibc的簡要實現
extern void (*__atexit_funcs[])(void);  // 通過atexit注冊的函數數組
extern int __atexit_count;              // 注冊的函數數量void exit(int status) {// 調用通過atexit注冊的函數for (int i = __atexit_count - 1; i >= 0; --i) {if (__atexit_funcs[i]) {__atexit_funcs[i]();}}// 刷新所有標準I/O緩沖區fflush(NULL);// 關閉所有打開的文件流fcloseall();// 調用系統調用_exit,立即終止程序_exit(status);
}

其主要的功能是終止進程并立即將控制返回給操作系統;

由于該接口屬于系統調用接口,與C標準庫解耦合故其并不會刷新用戶態緩沖區;

  • #define filename "test.txt"
    int main() {printf("hello world");_exit(0);return 0;
    }
    

    對應的運行結果為;

    $ ./mytest 
    $
    

    運行無結果,原因為其并不會刷新用戶態緩沖區;


🦦 進程崩潰或正常結束時對用戶態緩沖區的刷新

請添加圖片描述

在上文中提到,當進程正常退出時將對用戶態緩沖區進行一次刷新;

  • 在進程退出時是如何對用戶態緩沖區進行清理的?

實際上當進程退出時會將所有被打開的文件流中的所有用戶態緩沖區進行一次刷新;

  • 那么當進程return后是如何刷新緩沖區的?

對照這個問題可以利用一個簡單的程序利用gdb進行驗證;

int main() {return 0;
}

在這個程序當中什么都不做,只進行一次return 0返回;

將斷點打至return處并啟動gdb調試后再 逐過程 進行調試;

可以發現當return 0;被執行過后將跳轉至libc-start.c文件中并執行exit();

可以發現,實際上當main()函數結束時,若未顯式調用exit()時編譯器將隱式調用exit()從而達成程序的清理工作;

故對應的用戶態緩沖區也會在此時被刷新;

  • 進程崩潰是否會刷新用戶態緩沖區?

這里存在兩段代碼:

  • int main() {char *ptr = NULL;setvbuf(stdout, NULL, _IOFBF, 0);printf("hello world\n");*ptr = 'a';return 0;
    }
    

    在這段代碼當中定義了一個名為ptrchar*指針,并為其賦值為NULL;

    調用setvbuf()將刷新策略更新為全緩沖,由于數據量較少無法將緩沖區寫滿故只能等待進程結束;

    而此時解引用ptr使其造成一個對空指針的非法解引用;

    最終的結果為:

    $ ./mytest 
    Segmentation fault
    

    程序崩潰進程退出,最終的信息并被打印至終端中;

  • int main() {char *ptr = NULL;setvbuf(stdout, NULL, _IOFBF, 0);printf("hello world\n");assert(ptr);return 0;
    }
    

    在這段代碼當中同樣更新刷新策略為全緩沖,同時也定義了一個*ptr指針并賦值為NULL;

    但不同的是在這段代碼當中調用了assert()宏進行一次斷言;

    其運行結果為:

    $ ./mytest 
    mytest: test.c:140: main: Assertion `ptr' failed.
    hello world
    Aborted
    

    可以觀察到在這里即使進程崩潰也同樣將用戶態緩沖區中的數據進行了刷新;

這里有一個問題:

  • 為什么同樣是使程序崩潰(非正常退出),但其對用戶態緩沖區刷新的結果不同?

本質上當程序崩潰時并不會去刷新用戶態緩沖區;

以第一個例子中的對空指針非法解引用為例;

第一個例子中對空指針非法解引用接而崩潰的原因是,*ptr = 'a'這個操作屬于一個對物理內存的寫入操作;

而用戶無法直接通過對物理內存進行訪問從而進行寫入,只能通過 進程地址空間頁表映射 逐級進行寫入;

MMU 將會通過 頁表 中的權限信息以及其所映射的物理內存進行寫入;

NULL空指針并不存在有效的映射(可能無映射或是映射在非法物理地址當中);

由于是一個危險操作,此時MMU會觸發到一個頁面錯誤;

OS接收到這個異常時將會檢查錯誤的地址,如果地址是無效的,OS會認為這是一個嚴重的錯誤并發送一個信號(例SIGSEGV)最終將進程終止;

這一系列操作通常是在系統層面,由于其與用戶層解耦合,其并不關心用戶層是否存在未結束的代碼數據;

故此時用戶層將毫無預兆的被終止,其對應的用戶態緩沖區也因此無法進行刷新;

  • 為什么assert()宏也是使程序"崩潰"但會刷新其用戶態緩沖區?

首先理解一點,即 assert()宏為C語言提供 ;

既然是C語言提供的,那么其即使最終要使程序"崩潰"也必然需要將未寫入完全的數據進行刷新以保證數據的完整性;

在這里可以使用man手冊對assert()宏進行查詢;

man 3 assert
#---------------------
NAMEassert - abort the program if assertion is falseSYNOPSIS#include <assert.h>void assert(scalar expression);DESCRIPTIONIf  the  macro NDEBUG was defined at the moment <assert.h>was last included, the macro assert() generates  no  code,and  hence  does  nothing  at  all.   Otherwise, the macroassert() prints an error message  to  standard  error  andterminates  the  program by calling abort(3) if expressionis false (i.e., compares equal to zero).

Otherwise, the macro assert() prints an error message to standard error and terminates the program by calling abort(3).可以了解到,當斷言失敗時assert()宏將去調用abort()C標準庫函數接口從而終止進程;

使用man手冊對abort()進行查詢;

man abort
#---------------------
NAMEabort - cause abnormal process terminationSYNOPSIS#include <stdlib.h>void abort(void);DESCRIPTIONThe abort() first unblocks the SIGABRT signal, and then raisesthat signal for the calling  process.   This  results  in  theabnormal  termination of the process unless the SIGABRT signalis  caught  and  the  signal  handler  does  not  return  (seelongjmp(3)).If  the  abort() function causes process termination, all openstreams are closed and flushed.

從對abort()的解釋中可以看出,其將會利用信號從而終止進程;

但在終止進程前其將會關閉所有打開的文件流并在關閉前刷新所有用戶態緩沖區;


🦦 什么是內核態緩沖區

請添加圖片描述

在上文中提到,在這整個體系當中實際上存在兩種緩沖區,分別為內核態緩沖區與用戶態緩沖區;

用戶態緩沖區指高級語言為用戶所提供的一個緩沖區,其將根據不同的場景提供不同的刷新策略;

而內核態緩沖區則是為了使得能夠更好的利用或節省CPU資源而產生的,其對應的刷新策略一般根據當前CPU資源的使用情況而定的;

實際上內核態緩沖區是一個抽象的概念;

『 Linux 』“ 一切皆文件 “中提到,文件系統可以被看做是一個多態的現象;

對應的內核態緩沖區也是如此,可以說其可以屬于文件系統體系之中,也可以看作是一種多態;

而內核態緩沖區并不像用戶態緩沖區,其要比用戶態緩沖區更為復雜;

“一切皆文件” 中提到,以OS的視角觀察來看,文件無非幾種類型,而內核緩沖區已經在OS內核中被定義(描述)好的;

OS將根據這個文件的類型去為其分配相應類型的緩沖區以能夠為其提供對應的I/O需求;

而由用戶態緩沖區刷新到內核緩沖區這里的內核緩沖區將根據數據最終到達的文件類型從而經過不同的緩沖區;


🪶 輸入緩沖區與輸出緩沖區

請添加圖片描述

對于以往的學習而言,可能會出現 輸入緩沖區 , 輸出緩沖區 的概念;

而實際上無論是輸入緩沖區還是輸出緩沖區其都只有一個;

struct _IO_FILE {int _flags; /* 高位字是 _IO_MAGIC; 其余是文件流狀態標志 */#define _IO_file_flags _flags/* 與緩沖區相關的指針,遵循 C++ streambuf 協議 */char* _IO_read_ptr; /* 當前讀取操作的位置指針 */char* _IO_read_end; /* 讀取區域的結束位置指針 */char* _IO_read_base; /* 回退緩沖區加讀取區域的開始位置指針 */char* _IO_write_base; /* 寫入區域的開始位置指針 */char* _IO_write_ptr; /* 當前寫入操作的位置指針 */char* _IO_write_end; /* 寫入區域的結束位置指針 */char* _IO_buf_base; /* 緩沖區的開始位置指針 */char* _IO_buf_end; /* 緩沖區的結束位置指針 *//* 以下字段支持數據回退和撤銷操作 */char *_IO_save_base; /* 備份讀取區域的開始位置指針 */char *_IO_backup_base; /* 備份區域的第一個有效字符的位置指針 */char *_IO_save_end; /* 備份讀取區域的結束位置指針 */struct _IO_marker *_markers; /* 標記鏈表,用于標記流中的位置 */struct _IO_FILE *_chain; /* 指向下一個 FILE 結構體的指針,用于形成鏈表 */int _fileno; /* 文件描述符,對應底層的文件標識符 */#if 0int _blksize; /* 塊大小,不再使用,現在使用 _flags2 */#elseint _flags2; /* 額外的標志位,用于擴展 _flags 的功能 */#endif_IO_off_t _old_offset; /* 文件位置的偏移量,用于文件定位操作 *//* 以下字段暫時使用 */unsigned short _cur_column; /* 當前的列號,用于格式化輸出,+1 表示基于1的計數 */signed char _vtable_offset; /* 虛表偏移量,用于C++的虛函數機制 */char _shortbuf[1]; /* 內部的短緩沖區,用于最小化流的緩沖需求 */_IO_lock_t *_lock; /* 用于多線程同步的鎖 */#ifdef _IO_USE_OLD_IO_FILE/* 此處可能會包含舊版 IO_FILE 結構的額外成員 */#endif
};

該段代碼為FILE結構體的聲明;

其所在位置為/usr/include/stdio.h;

從代碼中可以看到,整個結構體當中對緩沖區的空間只有char* _IO_buf_base;char* _IO_buf_end;

其分別表示緩沖區的開始位置以及結尾位置,此塊區域即為緩沖區的有效范圍;

實際上無論是輸出緩沖區還是輸出緩沖區所指的范圍就是這個范圍;

用戶態緩沖區既可以用作輸出緩沖區也可用作為輸出緩沖區,這次取決于其的I/O形式;


🦦 簡單實現用戶層IO接口與緩沖區(供參考)

請添加圖片描述

  • MyFile.h

    #ifndef __MYSTDIO_H__
    #define __MYSTDIO_H__
    /*此處的#ifndef#define#endif為"包含衛士" 其作用于 #program once 相同用于避免頭文件重復包含所引起的重復編譯問題
    */#include <fcntl.h>
    #include <sys/stat.h>
    #include <sys/types.h>
    #include <unistd.h>
    #include <string.h>
    #include <stdlib.h>
    #include <stdio.h>//定義緩沖區刷新策略
    #define NO_BUFFER 1 //無緩沖
    #define ROW_BUFFER 2 //行緩沖
    #define BLOCK_BUFFER 4  // 塊緩沖(全緩沖)#define CREAT_FILE_MODE 0666
    #define BUFFER_SIZE 1024typedef struct _IO_FILE_{int _fileno;char _buf[BUFFER_SIZE];int _pos;//用于標定文件流的位置 在寫入與讀取時文件流的位置int _flushmode;
    } _FILE;_FILE *_fopen(const char *path, const char *mode);size_t _fwrite(const void*ptr,size_t size,size_t nmemb,_FILE*fp);int _fclose(_FILE *fp);int _fflush(_FILE *fp);size_t _fread(void *ptr, size_t size, size_t nmemb, _FILE *fp);#endif 
    
  • MyFile.c

    #include "MyFile.h"/* "w" "r" "a" 三種打開方式*/
    _FILE *_fopen(const char *path, const char *mode) {int fd = 0;if (strcmp(mode, "w") == 0) {fd = open(path, O_CREAT | O_WRONLY | O_TRUNC, CREAT_FILE_MODE);} else if (strcmp(mode, "r") == 0) {fd = open(path, O_RDONLY);} else if (strcmp(mode, "a") == 0) {fd = open(path, O_CREAT | O_WRONLY | O_APPEND, CREAT_FILE_MODE);} else {return NULL;// 其他選項暫不考慮實現}if (fd == -1) return NULL;_FILE *fp = (_FILE *)malloc(sizeof(_FILE));fp->_fileno = fd;fp->_pos = 0;                // 默認為0;// fp->_flushmode = ROW_BUFFER;  // 默認刷新策略為行緩沖fp->_flushmode = BLOCK_BUFFER;  // 默認刷新策略修改為全緩沖return fp;
    }size_t _fwrite(const void *ptr, size_t size, size_t nmemb, _FILE *fp) {int len = nmemb * size;memcpy(&fp->_buf[fp->_pos], ptr, len);fp->_pos += len;  // 更新文件流所指位置int ret = 0;if (fp->_flushmode & NO_BUFFER) {// 無緩沖 立即刷新ret = write(fp->_fileno, fp->_buf, len);fp->_pos = 0;  // 重置位置} else if (fp->_flushmode & ROW_BUFFER) {// 行緩沖 當遇到\n時進行刷新/*在該接口當中的行緩沖刷新策略并不是做的很好只能刷新一次\n但本次模擬為簡易模擬并不考慮*/int flush_len = 0;for (; flush_len < fp->_pos; ++flush_len) {if (fp->_buf[flush_len] == '\n') break;}flush_len += 1;ret = write(fp->_fileno, fp->_buf, flush_len);fp->_pos = 0;  // 重置位置} else {// 全緩沖(塊緩沖) 當緩沖區滿時進行刷新if (fp->_pos >= BUFFER_SIZE) {ret = write(fp->_fileno, fp->_buf, fp->_pos);fp->_pos = 0;  // 重置位置}}if (ret == -1) {perror("_fwrite\n");return 0;}return len;
    }size_t _fread(void *ptr, size_t size, size_t nmemb, _FILE *fp) {// ptr為用戶自行提供的緩沖區size_t len = size * nmemb;if (len > BUFFER_SIZE) len = BUFFER_SIZE;ssize_t pos = read(fp->_fileno, fp->_buf, len);// pos為實際讀取的數據if (pos == -1) {perror("_fread -- read\n");return 0;}memcpy(ptr, &fp->_buf[fp->_pos], pos);fp->_pos += pos;return pos;
    }int _fclose(_FILE *fp) {// 關閉文件時需要將對應文件流的緩沖區進行刷新_fflush(fp);int ret = close(fp->_fileno);free(fp);if (ret == -1) {// 失敗perror("_fclose\n");return -1;}return 1;
    }int _fflush(_FILE *fp) {ssize_t ret = 0;if (fp->_pos > 0) {ret = write(fp->_fileno, fp->_buf, fp->_pos);fp->_pos = 0;//更新文件流的位置}//判斷調用write是否失敗if(ret == -1){return EOF;}return 0;
    }
具體參照注釋 代碼僅供參考

🦦 緩沖區存在的意義

請添加圖片描述

該博客要講解緩沖區;

  • 緩沖區存在的意義是什么?

其實緩沖區的存在最重要的意義可以從以下幾點進行解釋:

  1. 減少I/O次數

    每當從用戶空間向內核空間進行I/O操作時,都要進行一次上下文的切換;

    這個過程一般涉及到保存當前進程狀態,加載內核狀態等操作,其消耗的資源和時間不容忽視;

    而設置緩沖區過后可以通過在用戶層級積累數據,直至積累到一定量后再統一進行I/O操作;

    減少系統調用的次數,從而減少上下文切換的次數,提高整體效率;

  2. 增加單次I/O數據吞吐量

    I/O操作的數據量增加時,每次I/O操作的時間成本被更多的數據分攤;

    而相對于頻繁的銷量數據I/O操作,批量處理可以提高數據處理的效率吞吐量;

  3. 減少內核緩沖區的頻繁操作

    內核緩沖區的操作同樣是需要消耗資源的;

    頻繁的I/O意味著內核緩沖區需要頻繁的進行讀寫,清空,同步等操作;

    這不僅增加了CPU的氟碳,也可能成為性能的瓶頸;

    而通過減少到內核的I/O次數可以有效降低對內核緩沖區的操作頻率從而提高整體性能;

  4. 減少硬件損耗

    頻繁的I/O操作意味著設備(磁盤,網絡設備等)需要頻繁進行讀寫,可能導致硬件頻繁工作從而降低使用壽命;

    而通過用戶態緩沖區的緩沖可以減少實際的I/O次數從而減少硬件損耗同時延長硬件設備的使用壽命;

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

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

相關文章

list 的實現

目錄 list 結點類 結點類的構造函數 list的尾插尾刪 list的頭插頭刪 迭代器 運算符重載 --運算符重載 和! 運算符重載 * 和 -> 運算符重載 list 的insert list的erase list list實際上是一個帶頭雙向循環鏈表,要實現list,則首先需要實現一個結點類,而一個結點需要…

【代碼隨想錄——回溯算法——四周目】

1.重新安排行程 1.1 我的代碼&#xff0c;超時通不過 var (used []boolpath []stringres []stringisFind bool )func findItinerary(tickets [][]string) []string {sortTickets(tickets)res make([]string, len(tickets)1)path make([]string, 0)used make([]bool,…

JSON Web Token

JWT 什么是JWT JWT&#xff08;JSON Web Token&#xff09;是一種用于在各方之間作為JSON對象安全地傳輸信息的開放標準&#xff08;RFC 7519&#xff09;。該信息經過數字簽名&#xff0c;因此是可驗證和可信的。JWT 可以使用HMAC算法或使用RSA的公鑰/私鑰對進行簽名 JWT的…

微信小程序 vant Picker組件default-index不生效的解決辦法

1、原始的寫法以及問題 <van-popup show"{{ showPopup && cellClick Freq }}" position"bottom" bind:close"onPopupClose"><van-picker value-key"Spec" show-toolbar title"{{cellClick Freq ? showPcCha…

win10鍵盤按亂了,如何恢復?

今天鍵盤被寶寶給按亂了&#xff0c;好不容易給重新調整回來&#xff0c;記錄備忘&#xff1a; 1、win10的asdf和方向鍵互換了&#xff1a; 使用Fnw鍵來回切換&#xff0c;OK&#xff01; 2、鍵盤的win鍵失效&#xff0c;例如&#xff1a;按winD無法顯示桌面。此時&#xf…

Day30

Day30 CSS CSS常用樣式 font-family:“微軟雅黑” -設置字體 font-size: 50px -設置字體大小 font-style : italic-設置字體風格 font-weight:bolder -設置字體粗細 color: white-設置字體顏色 letter-spacing: 20px-設置文本內容的間隔 text-decoration :underline - 設置劃…

電動汽車電子系統架構

電動汽車的普及正在穩步發展&#xff0c;供應鏈的各個環節也在發生變化。它涵蓋了制造電動汽車零件的原材料、化學品、電池和各種組件。與此同時&#xff0c;汽車充電基礎設施也參與其中&#xff0c;它們正經歷一個歷史性的階段&#xff0c;經過徹底的重新設計。它們的電氣化以…

Wpf 使用 Prism 實戰開發Day30

登錄界面設計 一.準備登錄界面圖片素材&#xff08;透明背景圖片&#xff09; 1.把準備好的圖片放在Images 文件夾下面&#xff0c;格式分別是.png和.ico 2.選中 login.png圖片鼠標右鍵&#xff0c;選擇屬性。生成的操作選擇>資源 3.MyTodo 應用程序右鍵&#xff0c;屬性&a…

如何修改開源項目中發現的bug?

如何修改開源項目中發現的bug&#xff1f; 目錄 如何修改開源項目中發現的bug&#xff1f;第一步&#xff1a;找到開源項目并建立分支第二步&#xff1a;克隆分支到本地倉庫第三步&#xff1a;在本地對項目進行修改第四步&#xff1a;依次使用命令行進行操作注意&#xff1a;Gi…

地質災害位移應急監測站

地質災害位移應急監測站是一種專門用于地質災害預警和應急響應的設施&#xff0c;它能夠實時監測和分析山體、建筑物、管道等的位移變化情況。以下是關于地質災害位移應急監測站的詳細介紹&#xff1a; 主要組成部分 傳感器&#xff1a;安裝于需要監測的位置&#xff0c;用于…

RK3588+FPGA+AI高性能邊緣計算盒子,應用于視頻分析、圖像視覺等

搭載RK3588&#xff08;四核 A76四核 A55&#xff09;&#xff0c;CPU主頻高達 2.4GHz &#xff0c;提供1MB L2 Cache 和 3MB L3 &#xff0c;Cache提供更強的 CPU運算能力&#xff0c;具備6T AI算力&#xff0c;可擴展至38T算力。 產品規格 系統主控CPURK3588&#xff0c;四核…

Nginx服務器替換SSL證書記得要重啟

輸入訪問域名&#xff0c;發現https證書過期了&#xff0c;果斷申請好ssl證書&#xff0c;并在Nginx服務器上將原證書替換成新申請的證書。 打開瀏覽器輸入網址確認一看&#xff0c;還是原來的證書并沒有替換成功?感覺不合常理 以下開啟了證書為什么替換不成功的排查 1、清除…

GUI 02:布局管理器相關知識,AWT 的 3 種布局管理器應用,以及嵌套布局的使用

一、前言 記錄時間 [2024-05-31] 前置文章 GUI 01&#xff1a;GUI 編程概述&#xff0c;AWT 相關知識&#xff0c;Frame 窗口&#xff0c;Panel 面板&#xff0c;及監聽事件的應用 本文講述了 GUI 編程種布局管理器的相關知識&#xff0c;以及 AWT 的 3 種布局管理器——流式布…

【FPGA】Verilog語言從零到精通

接觸fpga一段時間&#xff0c;也能寫點跑點吧……試試系統地康康呢~這個需要耐心但是回報巨大的工作。正原子&&小梅哥 15_語法篇&#xff1a;Verilog高級知識點_嗶哩嗶哩_bilibili 1Verilog基礎 Verilog程序框架&#xff1a;模塊的結構 類比&#xff1a;c語言的基礎…

P3881

最小值最大 二分&#xff1a;枚舉兩個牛之間的最小距離&#xff0c;左端點是1&#xff0c;右端點是籬笆總長度。 Check數組&#xff1a; 如果兩頭牛之間距離是Mid不合法&#xff0c;則返回0&#xff08;false&#xff09;&#xff1b; 如果兩頭牛之間距離是Mid合法&#xf…

去噪擴散概率模型在現代技術中的應用:圖像生成、音頻處理到藥物發現

去噪擴散概率模型&#xff08;DDPMs&#xff09;是一種先進的生成模型&#xff0c;它通過模擬數據的噪聲化和去噪過程&#xff0c;展現出多方面的優勢。DDPMs能夠生成高質量的數據樣本&#xff0c;這在圖像合成、音頻生成等領域尤為重要。它們在數據去噪方面表現出色&#xff0…

瑞吉外賣項目學習筆記(二)后臺系統的員工管理業務開發

一、完善登錄功能 1.1 問題分析 1.2 代碼實現 package com.itheima.reggie.filter;//這是一個過濾器類 //登錄檢查過濾器import com.alibaba.fastjson.JSON; import com.itheima.reggie.common.R; import lombok.extern.slf4j.Slf4j; import org.slf4j.Logger; import org.slf…

華為OD機試-最大坐標值

題目描述與示例 題目描述 小明在玩一個游戲&#xff0c;游戲規則如下&#xff1a;在游戲開始前&#xff0c;小明站在坐標軸原點處&#xff08;坐標值為 0&#xff09;給定一組指令和一個幸運數&#xff0c;每個指令都是一個整數&#xff0c;小明按照指定的要求前進或者后退指…

解析Java中1000個常用類:FunctionalInterface類,你學會了嗎?

Java 8 引入了一系列新的特性和改進,其中之一便是函數式編程。函數式接口(Functional Interface)是函數式編程的核心概念之一。本文將深入探討 FunctionalInterface 注解,介紹其用法、重要性,并通過示例展示如何在實際開發中應用函數式接口。 什么是函數式接口? 函數式…

有向圖的拓撲排序

文章目錄 概念及模板例題 雜務 概念及模板 有向圖的拓撲排序是指將有向無環圖中的所有頂點排成一個線性序列&#xff0c;使得圖中任意一對頂點u和v&#xff0c;若邊(u, v)在圖中&#xff0c;則u在該序列中排在v的前面。 例如&#xff0c;假設有n個任務&#xff0c;這些任務需…