Linux:6_基礎IO

基礎IO

一.理解"文件"

  • 文件分類
    1.內存級(被打開)文件
    2.磁盤級文件

1. 狹義理解

  • 文件在磁盤里
  • 磁盤是永久性存儲介質,因此文件在磁盤上的存儲是永久性的
  • 磁盤是外設 (即是輸出設備也是輸入設備)
  • 磁盤上的文件本質是對文件的所有操作,都是對外設的輸入和輸出簡稱IO

2. 廣義理解

  • Linux 下一切皆文件(鍵盤、顯示器、網卡、磁盤…… 這些都是抽象化的過程)(后面會講如何去理解)

3. 文件操作的歸類認知

  • 對于 0KB 的空文件是占用磁盤空間的
  • 文件是文件屬性(元數據)和文件內容的集合(文件 = 屬性(元數據)+ 內容)
  • 所有的文件操作本質是文件內容操作和文件屬性操作

4. 系統角度

  • 對文件的操作本質是進程對文件的操作

    訪問文件,需要先打開文件!
    誰打開文件?進程打開的文件!
    對文件的操作,本質是進程對文件的操作!

  • 磁盤的管理者是操作系統

  • 文件的讀寫本質不是通過 C 語言 / C++ 的庫函數來操作的(這些庫函數只是為用戶提供方便),而是通過文件相關的系統調用接口來實現的(fopen,fclose…等庫封裝了底層os的文件系統調用!)

二.回顧C文件接口

(見C語言進階文件博客)

C語言基礎:文件操作與數據持久化,-CSDN博客

1. hello.c打開文件

image-20250904165150227

打開的myfile文件在哪個路徑下?

  • 在程序的當前路徑下,那系統怎么知道程序的當前路徑在哪里呢?

可以使用 ls /proc/[進程id] -l 命令查看當前正在運行進程的信息:

image-20250904165225209

其中:

  • cwd:指向當前進程運行目錄的一個符號鏈接。
  • exe:指向啟動當前進程的可執行文件(完整路徑)的符號鏈接。

打開文件,本質是進程打開,所以,進程知道自己在哪里,即便文件不帶路徑,進程也知道。由此OS就能知道要創建的文件放在哪里。

2. hello.c寫文件

image-20250904165344098

3. hello.c讀文件

image-20250904165520705

稍作修改,實現簡單cat命令:

image-20250904165646087

4. 輸出信息到顯示器,你有哪些方法

image-20250904165718999

5. stdin & stdout & stderr

  • C默認會打開三個輸入輸出流,分別是stdin, stdout, stderr
  • 仔細觀察發現,這三個流的類型都是FILE, fopen返回值類型,文件指針*

image-20250905011019324

6. 打開文件的方式

image-20250905011126848

如上,是我們之前學的文件相關操作。還有fseek ftell rewind 的函數,在C部分已經有所涉獵,請同學們自行復習。

補充:往文件里寫字符串不要加0

三.系統文件I/O

打開文件的方式不僅僅是fopen,ifstream等流式,語言層的方案,其實系統才是打開文件最底層的方案。不過,在學習系統文件IO之前,先要了解下如何給函數傳遞標志位,該方法在系統文件IO接口中會使用到:

1. 一種傳遞標志位的方法(本質就是位圖)

#include "stdio.h"#define ONE_FLAG (1<<0) //00000000 0000 00000000 00000000 00000001
#define TWO_FLAG (1<<1) //00000000 0000 00000000 00000000 00000010
#define THREE_FLAG (1<<2) //00000000 0000 00000000 00000000 00000100
#define FOUR_FLAG (1<<3) //00000000 0000 00000000 00000000 00001000void Print(int flags)
{if(flags & ONE_FLAG){printf("One\n");}if(flags & TWO_FLAG){printf("Two\n");}if(flags & THREE_FLAG){printf("Three\n");}if(flags & FOUR_FLAG){printf("Four\n");}
}int main()
{Print(ONE_FLAG);printf("\n");Print(ONE_FLAG | TWO_FLAG);printf("\n");Print(ONE_FLAG | TWO_FLAG | THREE_FLAG);printf("\n");Print(ONE_FLAG | TWO_FLAG | THREE_FLAG | FOUR_FLAG);printf("\n");Print(ONE_FLAG | FOUR_FLAG);printf("\n");return 0;
}
//結果
OneOne
TwoOne
Two
ThreeOne
Two
Three
FourOne
Four

操作文件,除了上小節的C接口(當然,C++也有接口,其他語言也有),我們還可以采用系統接口來進行文件訪問, 先來直接以系統代碼的形式,實現和上面一模一樣的代碼:

2. hello.c 寫文件

image-20250905011606426

2. hello.c讀文件

image-20250905011726790

4. 接口介紹

open

image-20250905012210052

mode_t通常被定義為一個無符號整數,在大多數Linux平臺上typedef unsigned int mode_t;

open 函數具體使用哪個,和具體應用場景相關,如目標文件不存在,需要open創建,則第三個參數表示創建文件的默認權限,否則,使用兩個參數的open。

write read close lseek ,類比C文件相關接口。

(0).補充
①.ssize_t

ssize_t 是一個有符號整數類型,

typedef long ssize_t; 		//大多數操作系統中

%zd 是專門用來打印 ssize_t 的格式化符號。

②.off_t
typedef long off_t;        // 在 32 位系統上
typedef long long off_t;   // 在部分 64 位系統上
// 打印常用
off_t size;
printf("%lld\n", (long long)size);
(1).read

read的作用是讀文件

#include <unistd.h>ssize_t read(int fd, void *buf, size_t count);
  • 參數
    • fd:文件描述符。
    • buf:讀到的數據存放到這個緩沖區。
    • count:希望讀取的字節數。
  • 返回值
    • 成功:實際讀取的字節數(遇到文件結尾時返回0)。
    • 失敗:-1,并設置 errno。
(2).write

write的作用是寫文件

#include <unistd.h>ssize_t write(int fd, const void *buf, size_t count);
  • 參數
    • fd:文件描述符。
    • buf:要寫的數據。
    • count:寫多少字節。
  • 返回值
    • 成功:實際寫入的字節數。
    • 失敗: -1,并設置 errno。
(3).close

close的作用是關閉一個已經打開的文件描述符;自動刷新緩沖區

函數原型

#include <unistd.h>int close(int fd);
  • 參數 fd:文件描述符。
  • 返回值
    • 成功 :0
    • 失敗: -1,并設置 errno。
(4).lseek

lseek的作用是改變文件讀寫光標位置

#include <unistd.h>
#include <sys/types.h>off_t lseek(int fd, off_t offset, int whence);
  • 參數
    • fd:文件描述符。
    • offset:偏移量,可以是正數、負數。
    • whence:參照點:
      • SEEK_SET:從文件開頭偏移 。
      • SEEK_CUR:從當前位置偏移 。
      • SEEK_END:從文件末尾偏移 。
  • 返回值
    • 成功:新的光標位置(以字節為單位)
    • 失敗:返回 -1。

5. open函數返回值

在認識返回值之前,先來認識一下兩個概念: 系統調用庫函數

  • 上面的fopen fclose fread fwrite 都是C標準庫當中的函數,我們稱之為庫函數(libc)。
  • open close read write lseek 都屬于系統提供的接口,稱之為系統調用接口
  • 回憶一下我們講操作系統概念時,畫的一張圖

image-20250905013143095

系統調用接口和庫函數的關系,一目了然。

所以,可以認為, f# 系列的函數,都是對系統調用的封裝,方便二次開發。

6. 文件描述符fd

  • 通過對open函數的學習,我們知道了文件描述符就是一個小整數

    image-20250908093347249

(1).0 & 1 & 2
  • Linux進程默認情況下會有3個缺省打開的文件描述符,分別是標準輸入0, 標準輸出1, 標準錯誤2.

  • 0,1,2對應的物理設備一般是:鍵盤,顯示器,顯示器

    所以輸入輸出還可以采用如下方式:

image-20250905013333162

image-20250908104803520

而現在知道,文件描述符就是從0開始的小整數。當我們打開文件時,操作系統在內存中要創建相應的數據結構來描述目標文件。于是就有了file結構體。表示一個已經打開的文件對象。而進程執行open系統調用,所以必須讓進程和文件關聯起來。每個進程都有一個指針*files, 指向一張表files_struct,該表最重要的部分就是包含一個指針數組,每個元素都是一個指向打開文件的指針!所以,本質上,文件
描述符就是該數組的下標。所以,只要拿著文件描述符,就可以找到對應的文件。

對于以上原理結論我們可通過內核源碼驗證:

首先要找到task_struct結構體在內核中為位置,地址為:/usr/src/kernels/3.10.0-1160.71.1.el7.x86_64/include/linux/sched.h(3.10.0-1160.71.1.el7.x86_64是內核版本,可使用uname -a自行查看服務器配置, 因為這個文件夾只有一個,所以也不用刻意去分辨,內核版本其實也隨意)

  • 要查看內容可直接用vscode在windows下打開內核源代碼

  • 相關結構體所在位置

    • struct task_struct/usr/src/kernels/3.10.0-1160.71.1.el7.x86_64/include/linux/sched.h

    • struct files_struct/usr/src/kernels/3.10.0-1160.71.1.el7.x86_64/include/linux/fdtable.h

    • struct file/usr/src/kernels/3.10.0-1160.71.1.el7.x86_64/include/linux/fs.h

image-20250908105602560

(2).文件描述符的分配規則

直接看代碼:

image-20250908105641029

輸出發現是fd: 3

關閉0或者2,再看

image-20250908105712451

發現是結果是: fd: 0 或者fd 2 ,可見,文件描述符的分配規則:在files_struct數組當中,找到當前沒有被使用的最小的一個下標,作為新的文件描述符。

(3).重定向
  • 重定向=打開文件的方式+dup2

那如果關閉1呢?看代碼:

image-20250908105956412

此時,我們發現,本來應該輸出到顯示器上的內容,輸出到了文件myfile 當中,其中,fd=1。這種現象叫做輸出重定向。常見的重定向有: > , >> , <

那重定向的本質是什么呢?重定向的本質是更改文件描述符表的指針指向,數組下標不變

image-20250908110221589

重定向完整寫法是:

command fd>filename/&fd fd>filename/fd ... #(可同時重定向多個)
即:command 文件描述符>文件名/&文件描述符 文件描述符>文件名/文件描述符 ... #(可同時重定向多個)

例:

//stream.cc
int main()
{//向標準輸出打印,stdout,cout->1std::cout<<"hello cout"<<std::endl;    printf("hello printf\n");//向標準錯誤進行打印,stderr, cerr->2, 顯示器std::cerr << "hello cerr" <<std::endl;fprintf(stderr, "hello stderr\n");return 0;
}
czj@iv-ye46gvrx8gcva4hc07x0:linux$ ./stream 1>log.normal 2>log.error
#運行stream, 屏幕無打印, 標準輸出打印到了log.normal, 標準錯誤打印到了log.error.

注:

  1. 不寫文件描述符,則默認</>/>>左邊文件描述符是1

  2. 可同時重定向多個

    ./stream 1>log.txt 2>log.txt
    

    可以將1和2重定向到同一文件中,但是文件只會有標準錯誤打印的內容,因為2重定向的時候清空了文件;

    czj@iv-ye46gvrx8gcva4hc07x0:lesson20$ cat log.txt 
    hello cerr
    hello stderr
    

解決辦法:

#法一:后面的改成追加重定向
./stream 1>log.txt 2>>log.txt
#法二:fd>&fd(我們一般多用法二)
./stream 1>log.txt 2>&1
#法三:&>filename
./stream &>log.txt #完全等價法二
  • &后面跟文件描述符,區分文件夾名;

    n>&m = dup2(m, n)

  • &> 是 Bash 的一種簡寫語法,表示把 標準輸出 (fd=1) 和 標準錯誤 (fd=2) 一起重定向到同一個文件。

  1. 為什么存在一個標準錯誤呢?

    答:可以通過重定向能力,把常規消息和錯誤消息(printf/perror和cout/cerr)進行分離!

(4).使用 dup2 系統調用
  • 注:進程替換不影響重定向的結果

函數原型如下:進行重定向的系統調用

image-20250908111341680

示例代碼

image-20250908111723471

printf是C庫當中的IO函數,一般往 stdout 中輸出,但是stdout底層訪問文件的時候,找的還是fd:1,但此時,fd:1下標所表示內容,已經變成了./log的地址,不再是顯示器文件的地址,所以,輸出的任何消息都會往文件中寫入,進而完成輸出重定向。追加和輸入重定向同理。

(5).在minishell中添加重定向功能
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <ctype.h>
#include <unistd.h>
#include <cstring>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unordered_map>#define COMMAND_SIZE 1024
#define FORMAT "[%s@%s %s]# "// 下面是shell定義的全局變量
// 1.命令行參數表
#define MAXARGC 128
char* g_argv[MAXARGC];
int g_argc = 0;// 2.環境變量表
#define MAX_ENVS 100
char* g_env[MAX_ENVS];
int g_envs = 0;// 3.別名映射表
std::unordered_map<std::string,std::string> g_alias; //提一下,不寫了.// 4.關于重定向,我們關心的內容
#define NONE_REDIR 0
#define INPUT_REDIR 1
#define OUTPUT_REDIR 2
#define APPEND_REDIR 3int redir = NONE_REDIR;
std::string filename;// for test
char cwd[1024];
char cwdenv[1024];// last exit code
int lastcode=0;const char* GetUserName()
{const char* name = getenv("USER");return name==NULL?"None":name;
}const char* GetHostName()
{//const char* hostname = getenv("HOSTNAME");static char hostname[64];int i = gethostname(hostname,64);return i==-1?"None":hostname;
}const char* GetPwd()
{//const char* pwd = getenv("PWD");const char* pwd = getcwd(cwd,sizeof(cwd));if(pwd != NULL){// 法1:遍歷g_env更新環境變量if (getenv("PWD") == NULL){snprintf(cwdenv, sizeof(cwdenv), "PWD=%s", cwd);putenv(cwdenv);g_env[g_envs++] = cwdenv;g_env[g_envs] = NULL;}else{for (int i = 0; i < g_envs; i++){if (strncmp(g_env[i], "PWD=", 4) == 0){snprintf(g_env[i], strlen(g_env[i]) + 1, "PWD=%s", cwd);putenv(g_env[i]);break;}}}//法2:用cwdenv臨時變量// snprintf(cwdenv,sizeof(cwdenv),"PWD=%s",cwd);// putenv(cwdenv);}return pwd==NULL?"None":pwd;
}const char* GetHome()
{const char* home = getenv("HOME");return home == NULL ? "" : home;
}void InitEnv()
{extern char **environ;memset(g_env,0,sizeof(g_env));g_envs = 0;//本來要從配置文件來//1.獲取環境變量for(int i = 0;environ[i];i++){// 1.1申請空間g_env[i] = (char*)malloc(strlen(environ[i])+1);strcpy(g_env[i],environ[i]);g_envs++;}g_env[g_envs++] = (char*)"HAHA=for_test"; //測試標識g_env[g_envs] = NULL;//2.導成環境變量for(int i = 0; g_env[i];i++){putenv(g_env[i]);}environ = g_env;
}//command
bool Cd()
{if (g_argc == 1){std::string home = GetHome();if (home.empty())return true;chdir(home.c_str());}else{std::string where = g_argv[1];// cd - / cd ~if (where == "-"){// Todu}else if (where == "~"){// Todu}else{chdir(where.c_str());}}return true;
}void Echo()
{if (g_argc == 2){// echo "hello world"// echo $?// echo $PATHstd::string option = g_argv[1];if (option == "$?"){std::cout << lastcode << std::endl;lastcode = 0;}else if (option[0] == '$'){std::string env_name = option.substr(1);const char *env_value = getenv(env_name.c_str());if (env_value){std::cout << env_value << std::endl;}}else{std::cout << option << std::endl;}}
}//切割得到當前目錄名
std::string DirName(const char* pwd)
{
#define SLASH "/" //注意:局部定義define外部也可以用std::string dir = pwd;if(dir==SLASH)return SLASH;auto pos = dir.rfind(SLASH);if(pos == std::string::npos)return "BUG?";return dir.substr(pos+1);
}void MakeCommandLine(char cmd_prompt[],int size)
{snprintf(cmd_prompt,size,FORMAT,GetUserName(),GetHostName(),DirName(GetPwd()).c_str());//snprintf(cmd_prompt,size,FORMAT,GetUserName(),GetHostName(),GetPwd());
}void PrintCommandPrompt()
{char prompt[COMMAND_SIZE];MakeCommandLine(prompt,sizeof(prompt));printf("%s",prompt);fflush(stdout);
}bool GetCommandLine(char *out,int size)
{char *c = fgets(out,size,stdin);         if(c==NULL)return false;out[strlen(out)-1]=0;//清理\nif(strlen(out)==0)return false;return true;
}bool CommandParse(char* commandline)
{
#define SEP " "//命令行分析"ls -a -l" -> "ls" "-a" "-l"g_argc = 0;g_argv[g_argc++] = strtok(commandline,SEP);while(g_argv[g_argc++] = strtok(nullptr,SEP));g_argc--;return g_argc > 0 ? true : false;
}void PrintArgv()
{for(int i = 0; g_argv[i];i++){printf("g_argv[%d]->%s\n",i,g_argv[i]);}printf("argc:%d\n",g_argc);
}//檢查是否是內鍵命令(有些內鍵命令磁盤上也有一份,是為了處理shell本身,shell腳本能用)
bool CheckAndExecBuiltin()
{//內鍵命令重定向,采用打開一個臨時文件,用0/1/2覆蓋臨時文件,再dup2,完事再從臨時文件換回來std::string cmd = g_argv[0];if(cmd=="cd"){Cd();return true;}else if(cmd=="echo"){Echo();return true;}else if(cmd=="export"){//...}else if(cmd=="alias"){//...}return false;
}int Execute()
{pid_t id = fork();if(id == 0){//子進程檢查重定向情況int fd = -1;if(redir == INPUT_REDIR){fd = open(filename.c_str(), O_RDONLY);if(fd<0) exit(1);dup2(fd,0);close(fd);}else if(redir == OUTPUT_REDIR){umask(0);fd = open(filename.c_str(), O_CREAT | O_WRONLY | O_TRUNC,0666);if(fd<0) exit(2);dup2(fd,1);close(fd);}else if(redir == APPEND_REDIR){umask(0);fd = open(filename.c_str(), O_CREAT | O_WRONLY | O_APPEND,0666);if(fd<0) exit(2);dup2(fd,1);close(fd);}else{}//child//進程替換不影響重定向的結果execvp(g_argv[0],g_argv);exit(1);}int status = 0;//fatherpid_t rid = waitpid(id,&status,0);if(rid>0){lastcode = WEXITSTATUS(status);}(void)rid; //用一下rid省的編譯器告警return 0;
}void clearup()
{for(int i = 0; i < g_envs; i++){free(g_env[i]);}
}void TrimSpace(char cmd[],int& end)
{while(isspace(cmd[end])){end++;}
}void RedirChech(char cmd[])
{//清空上條命令殘留數據redir = NONE_REDIR;filename.clear();int start = 0;int end = strlen(cmd)-1;while(end > start){if(cmd[end] == '<'){cmd[end++] = 0;TrimSpace(cmd,end);redir = INPUT_REDIR;filename = &cmd[end];//或filename = cmd+endbreak;}else if(cmd[end] == '>'){if(cmd[end-1] == '>'){// >>cmd[end-1] = 0;cmd[end++] = 0;TrimSpace(cmd,end);redir = APPEND_REDIR;filename = &cmd[end];//或filename = cmd+endbreak;}else{// >cmd[end++] = 0;TrimSpace(cmd,end);redir = OUTPUT_REDIR;filename = &cmd[end];//或filename = cmd+endbreak;}}else{end--;}}
}int main()
{// shell啟動的時侯,從系統中獲取環境變量// 真實的shell從配置文件中讀,但是配置文件是shell腳本,目前看不懂// 我們的環境變量信息直接從父shell統一來InitEnv();while(true){//1.輸出命令行提示符PrintCommandPrompt();//2.獲取用戶輸入的命令char commandline[COMMAND_SIZE];if(!GetCommandLine(commandline,sizeof(commandline)))continue;//3.重定向分析"ls -a -l > file.txt" -> "ls -a -l" "file.txt" -> 判斷重定向方式RedirChech(commandline);// printf("redir:%d,filename:%s\n",redir,filename.c_str());//4.命令行分析"ls -a -l" -> "ls" "-a" "-l"if(!CommandParse(commandline))continue; //PrintArgv();//可補:檢查別名,替換命令//5.檢查并處理內鍵命令if(CheckAndExecBuiltin())continue;//6.執行命令Execute();} //釋放堆空間clearup();return 0;
}

四.理解“一切皆文件”

首先,在windows中是文件的東西,它們在linux中也是文件;其次一些在windows中不是文件的東西,比如進程、磁盤、顯示器、鍵盤這樣硬件設備也被抽象成了文件,你可以使用訪問文件的方法訪問它們獲得信息;甚至管道,也是文件;將來我們要學習網絡編程中的socket(套接字)這樣的東西,使用的接口跟文件接口也是一致的。

這樣做最明顯的好處是,開發者僅需要使用一套 API 和開發工具,即可調取 Linux 系統中絕大部分的資源。舉個簡單的例子,Linux 中幾乎所有讀(讀文件,讀系統狀態,讀PIPE)的操作都可以用read 函數來進行;幾乎所有更改(更改文件,更改系統參數,寫 PIPE)的操作都可以用 write 函數來進行。

之前我們講過,當打開一個文件時,操作系統為了管理所打開的文件,都會為這個文件創建一個file結構體,該結構體定義在/usr/src/kernels/3.10.0-1160.71.1.el7.x86_64/include/linux/fs.h 下,以下展示了該結構部分我們關系的內容:

image-20250908143413114

值得關注的是struct file 中的f_op 指針指向了一個file_operations 結構體,這個結構體中的成員除了struct module* owner 其余都是函數指針。該結構和struct file 都在fs.h下。

image-20250908143605967

file_operation 就是把系統調用和驅動程序關聯起來的關鍵數據結構,這個結構的每一個成員都對應著一個系統調用。讀取file_operation 中相應的函數指針,接著把控制權轉交給函數,從而完成了Linux設備驅動程序的工作。

介紹完相關代碼,一張圖總結:

image-20250908143806110

上圖中的外設,每個設備都可以有自己的read、write,但一定是對應著不同的操作方法!!但通過struct file 下file_operation 中的各種函數回調,讓我們開發者只用file便可調取 Linux 系統中絕大部分的資源!!這便是“linux下一切皆文件”的核心理解。

五.緩沖區

1. 什么是緩沖區(內存的一段空間)

緩沖區是內存空間的一部分。也就是說,在內存空間中預留了一定的存儲空間,這些存儲空間用來緩沖輸入或輸出的數據,這部分預留的空間就叫做緩沖區。緩沖區根據其對應的是輸入設備還是輸出設備,分為輸入緩沖區和輸出緩沖區。

2. 為什么要引入緩沖區機制(提高效率:提高使用者的效率)

讀寫文件時,如果不會開辟對文件操作的緩沖區,直接通過系統調用對磁盤進行操作(讀、寫等),那么每次對文件進行一次讀寫操作時,都需要使用讀寫系統調用來處理此操作,即需要執行一次系統調用,執行一次系統調用將涉及到CPU狀態的切換,即從用戶空間切換到內核空間,實現進程上下文的切換,這將損耗一定的CPU時間,頻繁的磁盤訪問對程序的執行效率造成很大的影響。系統調用是有成本的!

為了減少使用系統調用的次數,提高效率,我們就可以采用緩沖機制。比如我們從磁盤里取信息,可以在磁盤文件進行操作時,可以一次從文件中讀出大量的數據到緩沖區中,以后對這部分的訪問就不需要再使用系統調用了,等緩沖區的數據取完后再去磁盤中讀取,這樣就可以減少磁盤的讀寫次數,再加上計算機對緩沖區的操作大 快于對磁盤的操作,故應用緩沖區可大 提高計算機的運行速度。

又比如,我們使用打印機打印文檔,由于打印機的打印速度相對較慢,我們先把文檔輸出到打印機相應的緩沖區,打印機再自行逐步打印,這時我們的CPU可以處理別的事情。可以看出,緩沖區就是一塊內存區,它用在輸入輸出設備和CPU之間,用來緩存數據。它使得低速的輸入輸出設備和高速的CPU能夠協調工作,避免低速的輸入輸出設備占用CPU,解放出CPU,使其能夠高效率工作。

3. 緩沖類型

分類:
1.用戶級緩沖區(庫緩沖區)
2.內核級緩沖區

C標準庫中有文件緩沖區(在FILE中),struct file中也有文件內核緩沖區;
當用戶滿足以下三個
1.強制刷新;
2.刷新條件滿足;
3.進程退出;
任意一條時,將C標準庫文件緩沖區數據,根據文件描述符刷新(采取fd+系統調用,比如write)到操作系統文件內核緩沖區里,即拷貝交給操作系統.

注:

  • 1.操作系統會參考下面這幾種刷新方式,怎么刷新到外設我們不關心。我們認為只要把數據交給OS,就相當于交給了硬件!
  • 2.數據交給系統,交給硬件–本質全是拷貝!
    計算機數據流動的本質:一切皆拷貝!!

標準I/O提供了3種類型的緩沖區。

  • 全緩沖區:這種緩沖方式要求填滿整個緩沖區后才進行I/O系統調用操作。對于磁盤文件的操作通常使用全緩沖的方式訪問。
  • 行緩沖區:在行緩沖情況下,當在輸入和輸出中遇到換行符時,標準I/O庫函數將會執行系統調用操作。當所操作的流涉及一個終端時(例如標準輸入和標準輸出),使用行緩沖方式。因為標準I/O庫每行的緩沖區長度是固定的,所以只要填滿了緩沖區,即使還沒有遇到換行符,也會執行I/O系統調用操作,默認行緩沖區的大小為1024。大多顯示器用,
  • 無緩沖區:無緩沖區是指標準I/O庫不對字符進行緩存,直接調用系統調用。標準出錯流stderr通常是不帶緩沖區的,這使得出錯信息能夠盡快地顯示出來。立即刷新–無緩沖–寫透模式WT

除了上述列舉的默認刷新方式,下列特殊情況也會引發緩沖區的刷新:

  1. 緩沖區滿時;
  2. 執行flush語句;

image-20250909081741707

  • 強制從文件內核緩沖區刷新到外設方法:
  1. fsync(同步系統調用)

fsync的作用是把內核緩沖區的數據強制刷新到外設上.(稱為持久化或落盤)

基本用法

#include <unistd.h>int fsync(int fd);
  • 參數
    • fd:文件描述符。
  • 返回值
    • 成功返回 0;
    • 失敗返回 -1,并設置 errno。

  1. sync(命令)

sync命令的作用是強制把內核頁緩存中所有掛起的數據寫入磁盤。

用法

sync

直接運行,不帶參數; 效果是:把所有掛起的文件系統緩沖區寫盤。


刷新示例如下:

image-20250909082350752

我們本來想使用重定向思維,讓本應該打印在顯示器上的內容寫到“log.txt”文件中,但我們發現,程序運行結束后,文件中并沒有被寫入內容:

image-20250909082456589

這是由于我們將1號描述符重定向到磁盤文件后,緩沖區的刷新方式成為了全緩沖。而我們寫入的內容并沒有填滿整個緩沖區,導致并不會將緩沖區的內容刷新到磁盤文件中。怎么辦呢?可以使用fflush強制刷新下緩沖區。注:重定向會改變刷新方式!!!

image-20250909082524620

還有一種解決方法,剛好可以驗證一下stderr是不帶緩沖區的,代碼如下:

image-20250909082746732

這種方式便可以將2號文件描述符重定向至文件,由于stderr沒有緩沖區,“hello world”不用fflash就可以寫入文件:

image-20250909082843390

4. FILE

  • 因為IO相關函數與系統調用接口對應,并且庫函數封裝系統調用,所以本質上,訪問文件都是通過fd訪問的。
  • 所以C庫當中的FILE結構體內部,必定封裝了fd。

來段代碼在研究一下:

image-20250909083437514

運行出結果:

image-20250909083451567

但如果對進程實現輸出重定向呢?./hello > file,我們發現結果變成了:

image-20250909083527619

**我們發現printf 和fwrite(庫函數)都輸出了2次,而 write 只輸出了一次(系統調用)。**為什么呢?肯定和fork有關!

  • 一般C庫函數寫入文件時是全緩沖的,而寫入顯示器是行緩沖。
  • printf fwrite 庫函數+會自帶緩沖區(進度條例子就可以說明),當發生重定向到普通文件時,數據的緩沖方式由行緩沖變成了全緩沖。
  • 而我們放在緩沖區中的數據,就不會被立即刷新,甚至fork之后
  • 但是進程退出之后,會統一刷新,寫入文件當中。
  • 但是fork的時候,父子數據會發生寫時拷貝,所以當你父進程準備刷新的時候,子進程也就有了同樣的一份數據,隨即產生兩份數據。
  • write 沒有變化,說明沒有所謂的緩沖。

綜上: printf fwrite 庫函數會自帶緩沖區,而write系統調用沒有帶緩沖區。另外,我們這里所說的緩沖區,都是用戶級緩沖區。其實為了提升整機性能,OS也會提供相關內核級緩沖區,不過不再我們討論范圍之內。

那這個緩沖區誰提供呢? printf fwrite 是庫函數, write 是系統調用,庫函數在系統調用的“上層”, 是對系統調用的“封裝”,但是write 沒有緩沖區,而 printf fwrite 有,足以說明,該緩沖區是二次加上的,又因為是C,所以由C標準庫提供。

如果有興趣,可以看看FILE結構體:(glibc 的 FILE 結構體實現)用戶層緩沖區
typedef struct _IO_FILE FILE;/usr/include/stdio.h

image-20250909084419772

5. 簡單設計一下libc庫

mystdio.h

#pragma once#include <stdio.h>#define MAX 1024
#define NONE_FLUSH (1<<0)
#define LINE_FLUSH (1<<1)
#define FULL_FLUSH (1<<2)typedef struct IO_FILE
{int fileno;int flag;char outbuffer[MAX];int bufferlen;int flush_method;
}MyFile;MyFile* MyFopen(const char *path, const char *mode);void MyFclose(MyFile *);int MyFwrite(MyFile *, void *str, int len);void MyFFlush(MyFile *);

mystdio.c

#include "mystdio.h"
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>//static修飾的是函數,讓他只在本文件中使用
static MyFile* BuyFile(int fd, int flag)
{MyFile* f = (MyFile*)malloc(sizeof(MyFile));if(f==NULL)return NULL;f->bufferlen = 0;f->fileno = fd;f->flag = flag;f->flush_method = LINE_FLUSH;memset(f->outbuffer,0,sizeof(f->outbuffer));return f;
}MyFile* MyFopen(const char *path, const char *mode)
{int flag = -1;int fd = 0;if(strcmp(mode,"w") == 0){flag = O_CREAT | O_WRONLY | O_TRUNC;fd = open(path, flag, 0666);}if(strcmp(mode,"a") == 0){flag = O_CREAT | O_WRONLY | O_APPEND;fd = open(path, flag, 0666);}if(strcmp(mode,"r") == 0){flag = O_RDONLY;fd = open(path, flag);}else{//TODO        }if(fd < 0)return NULL;return BuyFile(fd,flag);
}void MyFclose(MyFile *file)
{if(file->fileno < 0)return;MyFFlush(file);close(file->fileno);free(file);
}int MyFwrite(MyFile *file, void *str, int len)
{//1. 拷貝memcpy(file->outbuffer+file->bufferlen,str,len);file->bufferlen+=len;//2. 嘗試判斷是否滿足刷新條件if((file->flush_method & LINE_FLUSH)&&(file->outbuffer[file->bufferlen-1]=='\n'))MyFFlush(file);return 0;
}void MyFFlush(MyFile *file)
{if(file->bufferlen <= 0)return;// 把數據從用戶拷貝到內核文件緩沖區中int n = write(file->fileno, file->outbuffer, file->bufferlen);(void)n;//內核文件緩沖區強制刷新到外設fsync(file->fileno);file->bufferlen = 0;
}

usercode.c

#include "mystdio.h"
#include <string.h>
#include <unistd.h>int main()
{MyFile* filep = MyFopen("./log.txt","a");if(!filep){printf("fopen error\n");return 1;}int cnt = 10;while(cnt--){char* msg = (char*)"hello myfile!";MyFwrite(filep,msg,strlen(msg));MyFFlush(filep);printf("buffer:%s\n",filep->outbuffer);sleep(1);}MyFclose(filep);return 0;
}

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

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

相關文章

Coze源碼分析-資源庫-刪除插件-前端源碼-核心邏輯

刪除插件邏輯 1. 刪除操作入口組件 刪除插件操作主要通過 usePluginConfig hook 中的 renderActions 方法實現&#xff0c;該方法返回 TableAction 組件來處理表格行的操作。 文件位置&#xff1a;frontend/packages/studio/workspace/entry-base/src/pages/library/hooks/u…

第一代:嵌入式本地狀態(Flink 1.x)

最初的架構將狀態以 JVM Heap 對象的形式存儲在 TaskManager 的內存中。對于小規模數據集&#xff0c;這種方式效果良好&#xff0c;但隨著狀態大小的增長超出內存&#xff0c;將所有狀態保存在內存中變得成本高昂且不穩定。 為了解決狀態規模增長的問題&#xff0c;引入了一種…

跨境金融數據對接實踐:印度NSE/BSE股票行情API集成指南

跨境金融數據對接實踐&#xff1a;印度NSE/BSE股票行情API集成指南 關鍵詞&#xff1a;印度股票數據對接 NSE實時行情 BSE證券接口 金融API開發 Python請求示例一、印度股市數據源技術解析&#xff08;核心價值&#xff09; 印度兩大交易所數據獲取難點&#xff1a; 時區差異&a…

AFSim2.9.0學習筆記 —— 1、AFSim及完整工具介紹(文末附:完整afsim2.9.0源碼、編譯好的完整工具包、中文教材等)

&#x1f514; AFSim2.9.0 相關技術、疑難雜癥文章合集&#xff08;掌握后可自封大俠 ?_?&#xff09;&#xff08;記得收藏&#xff0c;持續更新中…&#xff09; AFSim介紹 AFSim&#xff08;Advanced Framework for Simulation Integration & Modeling【高級仿真集成與…

ArcGIS學習-18 實戰-降雨量空間分布插值分析

設置環境加載要素投影查看要素&#xff0c;發現均不是投影數據&#xff0c;但都是地理坐標都是WGS1984使用工具進行批量投影然后新建空地圖&#xff0c;重新加載確認圖層的投影與柵格數據一致插值樣條法得到反距離權重法插值得到克里金法插值得到

HarmonyOS應用開發:深入理解聲明式UI與彈窗交互的最佳實踐

HarmonyOS應用開發&#xff1a;深入理解聲明式UI與彈窗交互的最佳實踐 引言 隨著HarmonyOS 4.0的發布及后續版本的演進&#xff0c;華為的分布式操作系統已經進入了全新的發展階段。基于API 12及以上的開發環境為開發者提供了更強大、更高效的開發工具和框架。在HarmonyOS應用…

探索Java并發編程--從基礎到高級實踐技巧

Thread&#xff08;線程&#xff09;線程 程序執行的最小單位&#xff08;一個進程至少有一個線程&#xff09;。線程內有自己的執行棧、程序計數器&#xff08;PC&#xff09;&#xff0c;但與同進程內其他線程共享堆內存與進程資源 在java中&#xff0c;線程由java.lang.Thr…

Go語言實戰案例-開發一個Markdown轉HTML工具

這個小工具可以把 .md 文件轉換為 .html 文件&#xff0c;非常適合寫筆記、博客或者快速預覽 Markdown 內容。&#x1f4cc; 案例目標? 讀取一個 Markdown 文件? 使用開源庫將 Markdown 轉換為 HTML? 將 HTML 輸出到新文件中&#x1f4e6; 所需庫我們用 goldmark 這個 Markd…

基于51單片機的太陽能鋰電池充電路燈

基于51單片機的太陽能鋰電池充電路燈系統設計 1 系統功能介紹 本設計以 STC89C52單片機 為核心&#xff0c;構建了一個能夠利用太陽能為鋰電池充電并智能控制LED路燈的系統。系統結合了 光照檢測電路、LED燈電路、按鍵檢測電路、太陽能充電電路 等模塊&#xff0c;實現了節能、…

PAT 1178 File Path

這一題的大意是給出了一個windows的文件夾目錄&#xff0c;讓我們按照所屬的目錄關系&#xff0c;來找相應的目錄是否存在&#xff0c;如果存在&#xff0c;就輸出找到該文件的路徑&#xff0c;如果不存在輸出error 我的思路是用合適的樹形結構保存下來目錄的所屬關系&#xff…

云原生部署_k8s入門

K8S官網文檔&#xff1a;&#xfeff;https://kubernetes.io/zh/docs/home/Kubernetes是什么Kubernetes 是用于自動部署、擴縮和管理容器化應用程序的開源系統。 Kubernetes 源自 &#xff0c;Google 15 年生產環境的運維經驗同時凝聚了社區的最佳創意和實踐。簡稱K8s.Kubernet…

實戰項目-----Python+OpenCV 實現對視頻的椒鹽噪聲注入與實時平滑還原”

實戰項目實現以下功能&#xff1a;功能 1&#xff1a;為視頻每一幀添加椒鹽噪聲作用&#xff1a;模擬真實環境中圖像傳輸或采集時可能出現的噪聲。實現方式&#xff1a;讀取視頻的每一幀。隨機選擇 10000 個像素點&#xff0c;將其設置為黑色&#xff08;0&#xff09;或白色&a…

Day42 PHP(mysql注入、跨庫讀取)

一、sql注入基本原理&#xff1a;沒有對用戶輸入的數據進行限制&#xff0c;導致數據庫語句可以做什么&#xff0c;用戶就可以做什么。取決于不同數據庫的不同查詢語言&#xff0c;所以為什么有mysql注入/orcale注入等等。步驟&#xff1a;【access】表名&#xff08;字典爆破來…

機器人控制器開發(部署——軟件打包備份更新)

文章總覽 為什么做備份更新 為機器人控制器設計一套打包備份更新機制&#xff0c;為控制器的批量生產和產品與項目落地做準備。 當某個模塊出現bug需要升級時&#xff0c;用戶可以快速獲取正確的bak包并導入到控制器中重啟生效。 如果沒有做好軟件的備份更新機制&#xff0c…

LaTeX TeX Live 安裝與 CTAN 國內鏡像配置(Windows / macOS / Linux 全流程)

這是一份面向國內環境的 LaTeX 從零到可編譯 指南&#xff1a;覆蓋 TeX Live / MacTeX 安裝、PATH 配置、CTAN 國內鏡像&#xff08;清華/北外/上交/中科大等&#xff09;一鍵切換與回滾、常見坑位&#xff08;權限、鏡像路徑、版本切換&#xff09;、以及 XeLaTeX/latexmk 的實…

WhoisXML API再次榮登2025年美國Inc. 5000快速成長企業榜單

WhoisXML API非常自豪地宣布&#xff0c;我們再次榮登美國權威榜單——2025年Inc.5000全美成長最快的私營企業之一。今年&#xff0c;公司在地區排名中位列第119名&#xff0c;在全美總體排名中位列第4,271名。Inc. 5000榜單要求參評企業必須保持獨立運營&#xff0c;并在2021至…

Elasticsearch面試精講 Day 9:復合查詢與過濾器優化

【Elasticsearch面試精講 Day 9】復合查詢與過濾器優化 在Elasticsearch的搜索體系中&#xff0c;復合查詢&#xff08;Compound Queries&#xff09;與過濾器&#xff08;Filters&#xff09;優化是構建高效、精準搜索邏輯的核心能力。作為“Elasticsearch面試精講”系列的第…

Android使用ReactiveNetwork監聽網絡連通性

引入庫 implementation com.github.pwittchen:reactivenetwork-rx2:3.0.8監聽網絡連接變更ReactiveNetwork.observeNetworkConnectivity(context).subscribeOn(Schedulers.io())// ... // anything else what you can do with RxJava.observeOn(Schedulers.computation()).subs…

基于阿里云部署 RustDesk 自托管服務器

基于阿里云部署 RustDesk 自托管服務器一、背景與需求場景二、什么是 RustDesk&#xff1f;為什么選擇自托管&#xff1f;2.1 RustDesk 是什么&#xff1f;2.2 為什么選擇自托管&#xff1f;三、環境準備與架構說明四、操作步驟4.1 在阿里云上安裝 RustDesk 服務端4.1.1 下載并…

細說分布式ID

針對高并發寫&#xff0c;分布式ID是其業務基礎&#xff0c;本文從一個面試題細細展開。面試官&#xff1a;1.對于Mysql的InnoDB引擎下&#xff0c;自增ID和UUID作為主鍵各自有什么優劣&#xff0c;對于一張表的主鍵你建議使用哪種ID&#xff1f;2.除了UUID是否還了解其他類型的…