基礎IO
一.理解"文件"
- 文件分類
1.內存級(被打開)文件
2.磁盤級文件
1. 狹義理解
- 文件在磁盤里
- 磁盤是永久性存儲介質,因此文件在磁盤上的存儲是永久性的
- 磁盤是外設 (即是輸出設備也是輸入設備)
- 磁盤上的文件本質是對文件的所有操作,都是對外設的輸入和輸出簡稱IO
2. 廣義理解
- Linux 下一切皆文件(鍵盤、顯示器、網卡、磁盤…… 這些都是抽象化的過程)(后面會講如何去理解)
3. 文件操作的歸類認知
- 對于 0KB 的空文件是占用磁盤空間的
- 文件是文件屬性(元數據)和文件內容的集合(文件 = 屬性(元數據)+ 內容)
- 所有的文件操作本質是文件內容操作和文件屬性操作
4. 系統角度
-
對文件的操作本質是進程對文件的操作
訪問文件,需要先打開文件!
誰打開文件?進程打開的文件!
對文件的操作,本質是進程對文件的操作! -
磁盤的管理者是操作系統
-
文件的讀寫本質不是通過 C 語言 / C++ 的庫函數來操作的(這些庫函數只是為用戶提供方便),而是通過文件相關的系統調用接口來實現的(fopen,fclose…等庫封裝了底層os的文件系統調用!)
二.回顧C文件接口
(見C語言進階文件博客)
C語言基礎:文件操作與數據持久化,-CSDN博客
1. hello.c打開文件
打開的myfile文件在哪個路徑下?
- 在程序的當前路徑下,那系統怎么知道程序的當前路徑在哪里呢?
可以使用 ls /proc/[進程id] -l
命令查看當前正在運行進程的信息:
其中:
- cwd:指向當前進程運行目錄的一個符號鏈接。
- exe:指向啟動當前進程的可執行文件(完整路徑)的符號鏈接。
打開文件,本質是進程打開,所以,進程知道自己在哪里,即便文件不帶路徑,進程也知道。由此OS就能知道要創建的文件放在哪里。
2. hello.c寫文件
3. hello.c讀文件
稍作修改,實現簡單cat命令:
4. 輸出信息到顯示器,你有哪些方法
5. stdin & stdout & stderr
- C默認會打開三個輸入輸出流,分別是stdin, stdout, stderr
- 仔細觀察發現,這三個流的類型都是FILE, fopen返回值類型,文件指針*
6. 打開文件的方式
如上,是我們之前學的文件相關操作。還有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 寫文件
2. hello.c讀文件
4. 接口介紹
open
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
都屬于系統提供的接口,稱之為系統調用接口 - 回憶一下我們講操作系統概念時,畫的一張圖
系統調用接口和庫函數的關系,一目了然。
所以,可以認為, f#
系列的函數,都是對系統調用的封裝,方便二次開發。
6. 文件描述符fd
-
通過對open函數的學習,我們知道了文件描述符就是一個小整數
(1).0 & 1 & 2
-
Linux進程默認情況下會有3個缺省打開的文件描述符,分別是標準輸入0, 標準輸出1, 標準錯誤2.
-
0,1,2對應的物理設備一般是:鍵盤,顯示器,顯示器
所以輸入輸出還可以采用如下方式:
而現在知道,文件描述符就是從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
(2).文件描述符的分配規則
直接看代碼:
輸出發現是fd: 3
關閉0或者2,再看
發現是結果是: fd: 0
或者fd 2
,可見,文件描述符的分配規則:在files_struct數組當中,找到當前沒有被使用的最小的一個下標,作為新的文件描述符。
(3).重定向
- 重定向=打開文件的方式+dup2
那如果關閉1呢?看代碼:
此時,我們發現,本來應該輸出到顯示器上的內容,輸出到了文件myfile
當中,其中,fd=1。這種現象叫做輸出重定向。常見的重定向有: >
, >>
, <
那重定向的本質是什么呢?重定向的本質是更改文件描述符表的指針指向,數組下標不變
重定向完整寫法是:
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
-
可同時重定向多個
./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) 一起重定向到同一個文件。
-
為什么存在一個標準錯誤呢?
答:可以通過重定向能力,把常規消息和錯誤消息(printf/perror和cout/cerr)進行分離!
(4).使用 dup2 系統調用
- 注:進程替換不影響重定向的結果
函數原型如下:進行重定向的系統調用
示例代碼
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
下,以下展示了該結構部分我們關系的內容:
值得關注的是struct file
中的f_op
指針指向了一個file_operations
結構體,這個結構體中的成員除了struct module* owner 其余都是函數指針。該結構和struct file
都在fs.h下。
file_operation
就是把系統調用和驅動程序關聯起來的關鍵數據結構,這個結構的每一個成員都對應著一個系統調用。讀取file_operation
中相應的函數指針,接著把控制權轉交給函數,從而完成了Linux設備驅動程序的工作。
介紹完相關代碼,一張圖總結:
上圖中的外設,每個設備都可以有自己的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
除了上述列舉的默認刷新方式,下列特殊情況也會引發緩沖區的刷新:
- 緩沖區滿時;
- 執行flush語句;
- 強制從文件內核緩沖區刷新到外設方法:
- fsync(同步系統調用)
fsync的作用是把內核緩沖區的數據強制刷新到外設上.(稱為持久化或落盤)
基本用法
#include <unistd.h>int fsync(int fd);
- 參數:
- fd:文件描述符。
- 返回值
- 成功返回 0;
- 失敗返回 -1,并設置 errno。
- sync(命令)
sync命令的作用是強制把內核頁緩存中所有掛起的數據寫入磁盤。
用法
sync
直接運行,不帶參數; 效果是:把所有掛起的文件系統緩沖區寫盤。
刷新示例如下:
我們本來想使用重定向思維,讓本應該打印在顯示器上的內容寫到“log.txt”文件中,但我們發現,程序運行結束后,文件中并沒有被寫入內容:
這是由于我們將1號描述符重定向到磁盤文件后,緩沖區的刷新方式成為了全緩沖。而我們寫入的內容并沒有填滿整個緩沖區,導致并不會將緩沖區的內容刷新到磁盤文件中。怎么辦呢?可以使用fflush強制刷新下緩沖區。注:重定向會改變刷新方式!!!
還有一種解決方法,剛好可以驗證一下stderr是不帶緩沖區的,代碼如下:
這種方式便可以將2號文件描述符重定向至文件,由于stderr沒有緩沖區,“hello world”不用fflash就可以寫入文件:
4. FILE
- 因為IO相關函數與系統調用接口對應,并且庫函數封裝系統調用,所以本質上,訪問文件都是通過fd訪問的。
- 所以C庫當中的FILE結構體內部,必定封裝了fd。
來段代碼在研究一下:
運行出結果:
但如果對進程實現輸出重定向呢?./hello > file
,我們發現結果變成了:
**我們發現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
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;
}