Linux文件重定向&&文件緩沖區(qū)解讀
一、C文件接口
stdin & stdout & stderr
C默認會打開三個輸入輸出流,分別是stdin, stdout, stderr
仔細觀察發(fā)現(xiàn),這三個流的類型都是FILE*, fopen返回值類型,文件指針
- fwrite向指定文件寫入內(nèi)容
- fread從指定文件讀取內(nèi)容
fprintf根據(jù)指定的format(格式)發(fā)送信息(參數(shù))到由stream(流)指定的文件,fprintf可以使得信息寫入到指定的文件
調(diào)用C文件接口,以w的形式打開,若文件不存在,會在當前目錄下新建文件,當前路徑就是進程的當前路徑cwd,如果改變了進程的cwd就可以在其他目錄下新建文件
w寫入前都會對文件進行清空,a在文件結(jié)尾追加寫,兩者都是寫入
C默認打開的三個輸入輸出流不是C語言的特性,而是操作系統(tǒng)的特性,進程會默認打開鍵盤,顯示器,顯示器
二、系統(tǒng)文件I/O
2.1認識系統(tǒng)文件I/O
- 文件其實是在磁盤上的,磁盤是外設(shè),對文件進行訪問,就是對硬件進行訪問
- 任何用戶都不能直接訪問硬件的數(shù)據(jù) ,而必須通過系統(tǒng)調(diào)用
- 幾乎所有的庫只要是訪問硬件設(shè)備,必須封裝系統(tǒng)調(diào)用
- C文件接口就是一種庫函數(shù),是對系統(tǒng)調(diào)用的封裝
2.2系統(tǒng)文件I/O
open( )
#include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> int open(const char *pathname, int flags); int open(const char *pathname, int flags, mode_t mode);
- pathname: 要打開或創(chuàng)建的目標文件
- flags: 打開文件時,可以傳入多個參數(shù)選項,用下面的一個或者多個常量進行 “ 或 ” 運算,構(gòu)成 flags
參數(shù) :
- O_RDONLY: 只讀打開
- O_WRONLY: 只寫打開
- O_RDWR : 讀寫打開
- O_CREAT : 若文件不存在,則創(chuàng)建它,需要使用 mode(例0666) 選項,來指明新文件的訪問權(quán)限
- O_APPEND: 追加寫
- O_TRUNC: 每一次寫入都清空文件
返回值:
- 成功:新打開的文件描述符
- 失?。?1
代碼示例:
umask( )可以用來設(shè)置掩碼的值
比特方位式的標志位傳遞方式通過位運算來實現(xiàn)
2.3系統(tǒng)調(diào)用和庫函數(shù)
上面的 fopen fclose fread fwrite 都是C標準庫當中的函數(shù),我們稱之為庫函數(shù)(libc)
open close read write lseek 都屬于系統(tǒng)提供的接口,稱之為系統(tǒng)調(diào)用接口
可以認為,f#系列的函數(shù),都是對系統(tǒng)調(diào)用的封裝,方便二次開發(fā)。
2.4open( )的返回值--文件描述符
Linux進程默認情況下會有3個缺省打開的文件描述符,分別是標準輸入0, 標準輸出1, 標準錯誤2
0,1,2對應(yīng)的物理設(shè)備一般是:鍵盤,顯示器,顯示器
linux下文件描述符的分配規(guī)則:從0下標開始,尋找最小沒有被使用過的數(shù)組位置,它的下標就是新文件的文件描述符--結(jié)合訪問文件的本質(zhì)來說明
代碼示例:
- 因為C庫函數(shù)是對系統(tǒng)接口的封裝,系統(tǒng)接口下只認識文件描述符,所以C庫自己提供的FILE結(jié)構(gòu)體中必定也包含著文件描述符,用_fileno記錄
如果關(guān)閉了1號文件,printf就無法向1號文件(顯示器)寫入了 ,但可以向3號文件寫入,所以我們打印就只能看到n的值
2.5訪問文件的本質(zhì)
任何一個被打開的文件在內(nèi)存中都要被管理起來,操作系統(tǒng)如果管理被打開的文件?----先描述再組織
當我們打開文件時,操作系統(tǒng)在內(nèi)存中要創(chuàng)建相應(yīng)的數(shù)據(jù)結(jié)構(gòu)來描述目標文件--file結(jié)構(gòu)體(直接或間接包含如下屬性:文件的基本屬性,文件的內(nèi)核緩沖區(qū)信息,引用計數(shù),struct file*next,在磁盤的什么位置),表示一個已經(jīng)打開的文件對象而進程執(zhí)行open系統(tǒng)調(diào)用,所以必須讓進程和文件關(guān)聯(lián)起來,每個進程都有一個指針*files, 指向一張表files_struct,該表最重要的部分就是包涵一個指針數(shù)組,每個元素都是一個指向打開文件的指針!
所以,本質(zhì)上,文件描述符就是該數(shù)組的下標,只要拿著文件描述符,就可以找到對應(yīng)的文件
- 當一個進程open()一個文件時,操作系統(tǒng)會在struct_file的指針數(shù)組中從下標為0的地方在開始尋找一個沒有被使用過的數(shù)組位置,填入要打開文件的struct file*,再將數(shù)組下標返回給open( )調(diào)用,作為該文件的文件描述符fd
- 當一個進程要向某個文件寫入的時候,操作系統(tǒng)只認識文件描述符,根據(jù)文件描述符找到對應(yīng)的數(shù)組下標,根據(jù)數(shù)組下標位置里的內(nèi)容找到所對應(yīng)的文件再寫入
- close關(guān)閉文件本質(zhì)上是清空對應(yīng)fd數(shù)組下標位置的內(nèi)容,再將該fd內(nèi)容指向的文件的引用計數(shù)--,引用計數(shù)為0才釋放銷毀相應(yīng)的struct_ file
三、文件重定向
3.1認識文件重定向
關(guān)閉1號文件再打開新文件 ,向1號文件寫入內(nèi)容
可以看到,原來要向1號文件(顯示屏)打印的信息,被寫入到了新打開的文件,其中,fd=1。這種現(xiàn)象叫做輸出重定向
常見的重定向有:>輸出重定向, >>追加重定向, <輸入重定向
追加重定向
輸入重定向
3.2文件重定向的本質(zhì)
- 文件重定向的本質(zhì):將1號文件描述符在指針數(shù)組中對應(yīng)位置的內(nèi)容,用log.txt文件描述符在指針數(shù)組中對應(yīng)位置的內(nèi)容進行覆蓋,原本數(shù)組內(nèi)的指向1號文件的文件指針就被替換成log.txt的文件指針,當我們再向1號文件描述符寫入內(nèi)容的時候,就是向文件指針指向的log.txt內(nèi)寫入而不再寫到標準輸出
- dup2系統(tǒng)調(diào)用
- 原本向顯示屏打印的內(nèi)容被寫入到log.txt文件中
3.3在shell中添加重定向功能
#include<stdio.h> #include<stdlib.h> #include<string.h> #include<unistd.h> #include<sys/types.h> #include<sys/wait.h> #include<assert.h> #include<ctype.h> #include<fcntl.h> #define LEFT "[" #define RIGHT "]" #define LABLE "#" #define DELIM " \t" #define LINE_SIZE 1024 #define ARGV_SIZE 32 #define NONE -1 #define IN_RDIR 0 #define OUT_RDIR 1 #define APPEND_RDIR 2 extern char** environ; char commandline[LINE_SIZE]; char* argv[ARGV_SIZE]; char pwd[LINE_SIZE]; char myenv[LINE_SIZE]; int lastcode=0; int quit=0; char *rdirfilename = NULL; int rdir = NONE; const char* getuser() { return getenv("USER"); } const char* gethostname() { return getenv("HOSTNAME"); } void getpwd() { getcwd(pwd,sizeof(pwd)); } void check_redir(char *cmd) { // ls -al -n // ls -al -n >/</>> filename.txt char *pos = cmd; while(*pos) { if(*pos == '>') { if(*(pos+1) == '>'){ *pos++ = '\0'; *pos++ = '\0'; while(isspace(*pos)) pos++; rdirfilename = pos; rdir=APPEND_RDIR; break; } else{ *pos = '\0'; pos++; while(isspace(*pos)) pos++; rdirfilename = pos; rdir=OUT_RDIR; break; } } else if(*pos == '<') { *pos = '\0'; // ls -a -l -n < filename.txt pos++; while(isspace(*pos)) pos++; rdirfilename = pos; rdir=IN_RDIR; break; } else{ //do nothing } pos++; } } void interact(char* cline,int size) { getpwd(); printf(LEFT"%s@%s %s"RIGHT""LABLE" ",getuser(),gethostname(),pwd); char* s=fgets(cline,size,stdin); assert(s); (void)s; cline[strlen(cline)-1]='\0'; //printf("echo : %s",cline); //ls -a -l > myfile.txt check_redir(cline); } int splitstring(char cline[],char* _argv[]) { int i=0; _argv[i++]=strtok(cline,DELIM); while(_argv[i++]=strtok(NULL,DELIM)); return i-1; } void normalexcute(char* _argv[]) { pid_t id=fork(); if(id<0) { perror("fork"); //continue; return ; } else if(id==0) { int fd = 0; // 后面我們做了重定向的工作,后面我們在進行程序替換的時候,難道不影響嗎??? if(rdir == IN_RDIR) { fd = open(rdirfilename, O_RDONLY); dup2(fd, 0); } else if(rdir == OUT_RDIR) { fd = open(rdirfilename, O_CREAT|O_WRONLY|O_TRUNC, 0666); dup2(fd, 1); } else if(rdir == APPEND_RDIR) { fd = open(rdirfilename, O_CREAT|O_WRONLY|O_APPEND, 0666); dup2(fd, 1); } //子進程執(zhí)行指令 //execvpe(argv[0],argv,environ); execvp(argv[0],argv); } else{ int status=0; pid_t rid=waitpid(id,&status,0); if(rid==id) { lastcode=WEXITSTATUS(status); } } } int buildcommand(char* _argv[],int _argc) { if(_argc==2&&strcmp(_argv[0],"cd")==0) { chdir(_argv[1]); getpwd(); sprintf(getenv("PWD"),"%s",pwd); return 1; } else if(_argc==2&&strcmp(_argv[0],"export")==0) { strcpy(myenv,_argv[1]); putenv(myenv); return 1; } else if(_argc==2&&strcmp(_argv[0],"echo")==0) { if(strcmp(_argv[1],"$?")==0) { printf("%d\n",lastcode); lastcode=0; } else if(*_argv[1]=='$') { char* s=getenv(_argv[1]+1); if(s) printf("%s\n",s); } else{ printf("%s\n",_argv[1]); } return 1; } //特殊處理ls if(_argc==2&&strcmp(_argv[0],"ls")==0) { _argv[_argc++]="--color"; _argv[_argc]=NULL; } return 0; } int main() { while(!quit) { //交互問題,獲得命令行參數(shù) interact(commandline,sizeof commandline); //字符串分割,解析命令行參數(shù) int argc = splitstring(commandline,argv); if(argc==0) continue; //指令的判斷 int n=buildcommand(argv,argc); //普通指令的執(zhí)行 if(!n)normalexcute(argv); } return 0; }
- 進程歷史打開的文件以及文件的重定向關(guān)系,并不會被程序替換所影響??!進程程序替換之后影響頁表右邊的物理地址所指向的內(nèi)容,虛擬地址并左邊的部分并不會受到影響
- 程序替換并不會影響文件訪問
3.4stdout和stderr
- stdout和stderr對應(yīng)的硬件設(shè)備都是顯示屏,訪問的都是同一個文件(引用計數(shù))
- 在重定向的時候,默認只對stdout的fd進行重定向
代碼示例:
如果對1號和2號文件都要進行重定向呢?
示例:./mytest 1> log.txt 2>err.txt
示例:./mytest > log.txt 2>&1
3.5如何理解“linux下一切皆文件” --以對外設(shè)的IO操作為例
- 不同的外設(shè)在進行IO操作時都有自己對應(yīng)的讀寫方法,放在struct device里
- 這些讀寫方法如何被找到?--由struct operation_func來對讀寫方法進行管理,該結(jié)構(gòu)體里存在指向?qū)?yīng)讀寫法的函數(shù)指針
- 如何找到struct operation_func?--由struct file來對struct operation_func進行管理,file結(jié)構(gòu)體存在指向struct operation_func的指針,基于struct file之上的被稱為虛擬文件系統(tǒng)(VFS)--一切皆文件
- 當我們打開一個文件的時候,通過進程的pcb數(shù)據(jù)結(jié)構(gòu)找到struct struct_file,操作系統(tǒng)根據(jù)文件描述符的分配規(guī)則,在struct struct_file的指針數(shù)組中為該文件分配一個fd;當我們要訪問一個外設(shè)的時候,根據(jù)該外設(shè)文件fd對應(yīng)的數(shù)組下標內(nèi)容找到該外設(shè)文件的struct file,根據(jù)file結(jié)構(gòu)體找到對應(yīng)的struct operation_func,由于訪問的外設(shè)的不同,在struct operation_func中根據(jù)函數(shù)指針找到對應(yīng)的讀寫方法,就可以對外設(shè)進行訪問了
四、文件緩沖區(qū)
4.1認識FILE
因為IO相關(guān)函數(shù)與系統(tǒng)調(diào)用接口對應(yīng),并且?guī)旌瘮?shù)封裝系統(tǒng)調(diào)用,所以本質(zhì)上,訪問文件都是通過fd訪問的
所以C庫當中的FILE結(jié)構(gòu)體內(nèi)部,必定封裝了fd
4.2文件緩沖區(qū)引入
- 對比有無fork( )的代碼
我們發(fā)現(xiàn) printf 和 fwrite (庫函數(shù))都輸出了 2 次,而 write 只輸出了一次(系統(tǒng)調(diào)用),為什么呢?肯定和 fork有關(guān)!
再來驗證一個現(xiàn)象:
不加'\n'并且在最后close(1)
代碼運行的結(jié)果是:只有系統(tǒng)調(diào)用接口寫入的內(nèi)容被打印出來了
加上'\n',結(jié)果又不一樣了
4.3文件緩沖區(qū)的原理
C語言會提供一個緩沖區(qū),我們調(diào)用C文件接口寫入的數(shù)據(jù)會被暫存在這個緩沖區(qū)內(nèi),緩沖區(qū)的刷新方式有三種:
- 無緩沖:直接刷新,一般我們使用的fflush( )就是無緩沖的刷新方式
- 行緩沖:遇到'\n'才刷新,一般對應(yīng)顯示器
- 全緩沖:緩沖區(qū)滿了才刷新,一般對應(yīng)普通文件的寫入
- 特殊說明:進程結(jié)束的時候會自動刷新緩沖區(qū)
在操作系統(tǒng)的內(nèi)核中也存在一個內(nèi)核級別的緩沖區(qū),目前認為,只要將數(shù)據(jù)刷新到了內(nèi)核,數(shù)據(jù)就可以到硬件了,內(nèi)核緩沖區(qū)也有自己的刷新方式
為什么要有C層面的緩沖區(qū)?
- 用戶不需要一步一步將數(shù)據(jù)寫入到硬件中,而是可以直接調(diào)用C庫為我們提供的讀寫方法,將數(shù)據(jù)交給庫函數(shù)來處理,解決用戶的效率問題
- 我們真正存到文件里的都是一個個的字符,調(diào)用C庫的讀寫方法,可以在放入緩沖區(qū)之前將我們的數(shù)據(jù)格式化成字符串,再刷新到內(nèi)核中進而寫入文件,C層面的緩沖區(qū)可以配合格式化的工作
C為我們提供的緩沖區(qū)在FILE結(jié)構(gòu)體里,F(xiàn)ILE里面有相關(guān)緩沖區(qū)的字段和維護信息,F(xiàn)ILE屬于用戶層面,而不屬于操作系統(tǒng)
文件寫入的過程:
- 首先,在文件寫入之前,進程會打開一個文件,通過對各種內(nèi)核數(shù)據(jù)結(jié)構(gòu)的訪問和操作,獲得該文件的文件描述符
- 如果使用系統(tǒng)調(diào)用接口來對文件進行寫入,數(shù)據(jù)直接通過write和fd寫入對應(yīng)的內(nèi)核級別緩沖區(qū),默認最后都會刷新到硬件中
- 如果使用fwrite等庫函數(shù)來對文件進行寫入,首先,在語言層面會malloc出一個FILE結(jié)構(gòu)體,F(xiàn)ILE里面有對應(yīng)的緩沖區(qū)信息以及文件的fd,然后內(nèi)容會先被暫存在C層面的緩沖區(qū),如果是無緩沖,數(shù)據(jù)直接被刷新到內(nèi)核中,如果是行緩沖,遇到'\n'就會被刷新到內(nèi)核中,如果是全緩沖,等緩沖區(qū)滿了就被刷新到內(nèi)核中
- 由于庫函數(shù)是對系統(tǒng)調(diào)用接口的封裝,用戶通過write和fd將數(shù)據(jù)刷新到對應(yīng)的文件的內(nèi)核緩沖區(qū)內(nèi),再由該內(nèi)核緩沖區(qū)刷新到外設(shè)
4.4解釋現(xiàn)象
為什么不加'\n'并且close(1)的時候,使用庫函數(shù)寫入的內(nèi)容不會被顯示?
不加'\n',調(diào)用庫函數(shù)寫入的數(shù)據(jù)都會被暫存在C層面的緩沖區(qū)
close(1)后,即使進程退出后緩沖區(qū)會自動刷新,但是此時已經(jīng)找不到1號文件的fd了,緩沖區(qū)內(nèi)的數(shù)據(jù)也無法被寫入到內(nèi)核中,最后也不會顯示到顯示器上
加了'\n'即使最后close(1),遇到'\n'緩沖區(qū)就會立馬將數(shù)據(jù)刷新到內(nèi)核中,就會顯示到顯示器上
為什么fork()之后重定向C接口會被調(diào)用兩次?
- 重定向后,緩沖區(qū)的刷新方式會從行緩沖變成全緩沖,也就說,數(shù)據(jù)要么等到緩沖區(qū)滿了再被刷新,要么等待進程結(jié)束后再刷新,所以我們放在緩沖區(qū)中的數(shù)據(jù),就不會被立即刷新,甚至fork之后
- fork( )之后,創(chuàng)建子進程,子進程會繼承父進程的內(nèi)核數(shù)據(jù)結(jié)構(gòu)對象的內(nèi)容,父子進程在一開始的時候數(shù)據(jù)和代碼是共享的,緩沖區(qū)也屬于數(shù)據(jù)
- 進程退出后,要對緩沖區(qū)的數(shù)據(jù)進行統(tǒng)一刷新,刷新就是對數(shù)據(jù)進行訪問寫入,此時父子數(shù)據(jù)會發(fā)生寫時拷貝,所以當父進程準備刷新的時候,子進程也就有了同樣的一份數(shù)據(jù),隨即產(chǎn)生兩份數(shù)據(jù)
- 由于write沒有所謂的緩沖區(qū),write()寫入的數(shù)據(jù)直接在內(nèi)核中,所以write( )的數(shù)據(jù)只有一份
總結(jié)
printf fwrite 庫函數(shù)會自帶緩沖區(qū),而 write 系統(tǒng)調(diào)用沒有帶緩沖區(qū)。這里所說的緩沖區(qū), 都是用戶級緩沖區(qū)。其實為了提升整機性能,OS也會提供相關(guān)內(nèi)核級緩沖區(qū)
那這個用戶級緩沖區(qū)誰提供呢? printf fwrite 是庫函數(shù), write 是系統(tǒng)調(diào)用,庫函數(shù)在系統(tǒng)調(diào)用的“上層”, 是對系統(tǒng) 調(diào)用的“封裝”,但是 write 沒有緩沖區(qū),而 printf fwrite 有,說明該緩沖區(qū)是二次加上的,由C標準庫提供
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
linux服務(wù)器上安裝jdk的兩種方法(yum+下載包)
這篇文章主要給大家介紹了關(guān)于在linux服務(wù)器上安裝jdk的兩種方法,分別是利用yum安裝和從官網(wǎng)下載包安裝,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面來一起看看吧2018-05-05