Linux文件重定向&&文件緩沖區(qū)解讀
一、C文件接口
stdin & stdout & stderr
C默認會打開三個輸入輸出流,分別是stdin, stdout, stderr
仔細觀察發(fā)現(xiàn),這三個流的類型都是FILE*, fopen返回值類型,文件指針
- fwrite向指定文件寫入內容
- fread從指定文件讀取內容
fprintf根據指定的format(格式)發(fā)送信息(參數(shù))到由stream(流)指定的文件,fprintf可以使得信息寫入到指定的文件
調用C文件接口,以w的形式打開,若文件不存在,會在當前目錄下新建文件,當前路徑就是進程的當前路徑cwd,如果改變了進程的cwd就可以在其他目錄下新建文件
w寫入前都會對文件進行清空,a在文件結尾追加寫,兩者都是寫入
C默認打開的三個輸入輸出流不是C語言的特性,而是操作系統(tǒng)的特性,進程會默認打開鍵盤,顯示器,顯示器

二、系統(tǒng)文件I/O
2.1認識系統(tǒng)文件I/O
- 文件其實是在磁盤上的,磁盤是外設,對文件進行訪問,就是對硬件進行訪問
- 任何用戶都不能直接訪問硬件的數(shù)據 ,而必須通過系統(tǒng)調用
- 幾乎所有的庫只要是訪問硬件設備,必須封裝系統(tǒng)調用
- C文件接口就是一種庫函數(shù),是對系統(tǒng)調用的封裝
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ù)選項,用下面的一個或者多個常量進行 “ 或 ” 運算,構成 flags
參數(shù) :
- O_RDONLY: 只讀打開
- O_WRONLY: 只寫打開
- O_RDWR : 讀寫打開
- O_CREAT : 若文件不存在,則創(chuàng)建它,需要使用 mode(例0666) 選項,來指明新文件的訪問權限
- O_APPEND: 追加寫
- O_TRUNC: 每一次寫入都清空文件
返回值:
- 成功:新打開的文件描述符
- 失?。?1
代碼示例:
umask( )可以用來設置掩碼的值




比特方位式的標志位傳遞方式通過位運算來實現(xiàn)



2.3系統(tǒng)調用和庫函數(shù)
上面的 fopen fclose fread fwrite 都是C標準庫當中的函數(shù),我們稱之為庫函數(shù)(libc)
open close read write lseek 都屬于系統(tǒng)提供的接口,稱之為系統(tǒng)調用接口
可以認為,f#系列的函數(shù),都是對系統(tǒng)調用的封裝,方便二次開發(fā)。
2.4open( )的返回值--文件描述符
Linux進程默認情況下會有3個缺省打開的文件描述符,分別是標準輸入0, 標準輸出1, 標準錯誤2
0,1,2對應的物理設備一般是:鍵盤,顯示器,顯示器
linux下文件描述符的分配規(guī)則:從0下標開始,尋找最小沒有被使用過的數(shù)組位置,它的下標就是新文件的文件描述符--結合訪問文件的本質來說明
代碼示例:
- 因為C庫函數(shù)是對系統(tǒng)接口的封裝,系統(tǒng)接口下只認識文件描述符,所以C庫自己提供的FILE結構體中必定也包含著文件描述符,用_fileno記錄


如果關閉了1號文件,printf就無法向1號文件(顯示器)寫入了 ,但可以向3號文件寫入,所以我們打印就只能看到n的值


2.5訪問文件的本質
任何一個被打開的文件在內存中都要被管理起來,操作系統(tǒng)如果管理被打開的文件?----先描述再組織
當我們打開文件時,操作系統(tǒng)在內存中要創(chuàng)建相應的數(shù)據結構來描述目標文件--file結構體(直接或間接包含如下屬性:文件的基本屬性,文件的內核緩沖區(qū)信息,引用計數(shù),struct file*next,在磁盤的什么位置),表示一個已經打開的文件對象而進程執(zhí)行open系統(tǒng)調用,所以必須讓進程和文件關聯(lián)起來,每個進程都有一個指針*files, 指向一張表files_struct,該表最重要的部分就是包涵一個指針數(shù)組,每個元素都是一個指向打開文件的指針!
所以,本質上,文件描述符就是該數(shù)組的下標,只要拿著文件描述符,就可以找到對應的文件

- 當一個進程open()一個文件時,操作系統(tǒng)會在struct_file的指針數(shù)組中從下標為0的地方在開始尋找一個沒有被使用過的數(shù)組位置,填入要打開文件的struct file*,再將數(shù)組下標返回給open( )調用,作為該文件的文件描述符fd
- 當一個進程要向某個文件寫入的時候,操作系統(tǒng)只認識文件描述符,根據文件描述符找到對應的數(shù)組下標,根據數(shù)組下標位置里的內容找到所對應的文件再寫入
- close關閉文件本質上是清空對應fd數(shù)組下標位置的內容,再將該fd內容指向的文件的引用計數(shù)--,引用計數(shù)為0才釋放銷毀相應的struct_ file
三、文件重定向
3.1認識文件重定向
關閉1號文件再打開新文件 ,向1號文件寫入內容


可以看到,原來要向1號文件(顯示屏)打印的信息,被寫入到了新打開的文件,其中,fd=1。這種現(xiàn)象叫做輸出重定向
常見的重定向有:>輸出重定向, >>追加重定向, <輸入重定向
追加重定向


輸入重定向


3.2文件重定向的本質
- 文件重定向的本質:將1號文件描述符在指針數(shù)組中對應位置的內容,用log.txt文件描述符在指針數(shù)組中對應位置的內容進行覆蓋,原本數(shù)組內的指向1號文件的文件指針就被替換成log.txt的文件指針,當我們再向1號文件描述符寫入內容的時候,就是向文件指針指向的log.txt內寫入而不再寫到標準輸出
- dup2系統(tǒng)調用
- 原本向顯示屏打印的內容被寫入到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;
}

- 進程歷史打開的文件以及文件的重定向關系,并不會被程序替換所影響??!進程程序替換之后影響頁表右邊的物理地址所指向的內容,虛擬地址并左邊的部分并不會受到影響
- 程序替換并不會影響文件訪問
3.4stdout和stderr
- stdout和stderr對應的硬件設備都是顯示屏,訪問的都是同一個文件(引用計數(shù))
- 在重定向的時候,默認只對stdout的fd進行重定向
代碼示例:


如果對1號和2號文件都要進行重定向呢?
示例:./mytest 1> log.txt 2>err.txt

示例:./mytest > log.txt 2>&1

3.5如何理解“linux下一切皆文件” --以對外設的IO操作為例
- 不同的外設在進行IO操作時都有自己對應的讀寫方法,放在struct device里
- 這些讀寫方法如何被找到?--由struct operation_func來對讀寫方法進行管理,該結構體里存在指向對應讀寫法的函數(shù)指針
- 如何找到struct operation_func?--由struct file來對struct operation_func進行管理,file結構體存在指向struct operation_func的指針,基于struct file之上的被稱為虛擬文件系統(tǒng)(VFS)--一切皆文件
- 當我們打開一個文件的時候,通過進程的pcb數(shù)據結構找到struct struct_file,操作系統(tǒng)根據文件描述符的分配規(guī)則,在struct struct_file的指針數(shù)組中為該文件分配一個fd;當我們要訪問一個外設的時候,根據該外設文件fd對應的數(shù)組下標內容找到該外設文件的struct file,根據file結構體找到對應的struct operation_func,由于訪問的外設的不同,在struct operation_func中根據函數(shù)指針找到對應的讀寫方法,就可以對外設進行訪問了
四、文件緩沖區(qū)
4.1認識FILE
因為IO相關函數(shù)與系統(tǒng)調用接口對應,并且?guī)旌瘮?shù)封裝系統(tǒng)調用,所以本質上,訪問文件都是通過fd訪問的
所以C庫當中的FILE結構體內部,必定封裝了fd
4.2文件緩沖區(qū)引入
- 對比有無fork( )的代碼

我們發(fā)現(xiàn) printf 和 fwrite (庫函數(shù))都輸出了 2 次,而 write 只輸出了一次(系統(tǒng)調用),為什么呢?肯定和 fork有關!
再來驗證一個現(xiàn)象:
不加'\n'并且在最后close(1)

代碼運行的結果是:只有系統(tǒng)調用接口寫入的內容被打印出來了

加上'\n',結果又不一樣了

4.3文件緩沖區(qū)的原理
C語言會提供一個緩沖區(qū),我們調用C文件接口寫入的數(shù)據會被暫存在這個緩沖區(qū)內,緩沖區(qū)的刷新方式有三種:
- 無緩沖:直接刷新,一般我們使用的fflush( )就是無緩沖的刷新方式
- 行緩沖:遇到'\n'才刷新,一般對應顯示器
- 全緩沖:緩沖區(qū)滿了才刷新,一般對應普通文件的寫入
- 特殊說明:進程結束的時候會自動刷新緩沖區(qū)
在操作系統(tǒng)的內核中也存在一個內核級別的緩沖區(qū),目前認為,只要將數(shù)據刷新到了內核,數(shù)據就可以到硬件了,內核緩沖區(qū)也有自己的刷新方式
為什么要有C層面的緩沖區(qū)?
- 用戶不需要一步一步將數(shù)據寫入到硬件中,而是可以直接調用C庫為我們提供的讀寫方法,將數(shù)據交給庫函數(shù)來處理,解決用戶的效率問題
- 我們真正存到文件里的都是一個個的字符,調用C庫的讀寫方法,可以在放入緩沖區(qū)之前將我們的數(shù)據格式化成字符串,再刷新到內核中進而寫入文件,C層面的緩沖區(qū)可以配合格式化的工作
C為我們提供的緩沖區(qū)在FILE結構體里,F(xiàn)ILE里面有相關緩沖區(qū)的字段和維護信息,F(xiàn)ILE屬于用戶層面,而不屬于操作系統(tǒng)
文件寫入的過程:
- 首先,在文件寫入之前,進程會打開一個文件,通過對各種內核數(shù)據結構的訪問和操作,獲得該文件的文件描述符
- 如果使用系統(tǒng)調用接口來對文件進行寫入,數(shù)據直接通過write和fd寫入對應的內核級別緩沖區(qū),默認最后都會刷新到硬件中
- 如果使用fwrite等庫函數(shù)來對文件進行寫入,首先,在語言層面會malloc出一個FILE結構體,F(xiàn)ILE里面有對應的緩沖區(qū)信息以及文件的fd,然后內容會先被暫存在C層面的緩沖區(qū),如果是無緩沖,數(shù)據直接被刷新到內核中,如果是行緩沖,遇到'\n'就會被刷新到內核中,如果是全緩沖,等緩沖區(qū)滿了就被刷新到內核中
- 由于庫函數(shù)是對系統(tǒng)調用接口的封裝,用戶通過write和fd將數(shù)據刷新到對應的文件的內核緩沖區(qū)內,再由該內核緩沖區(qū)刷新到外設
4.4解釋現(xiàn)象
為什么不加'\n'并且close(1)的時候,使用庫函數(shù)寫入的內容不會被顯示?
不加'\n',調用庫函數(shù)寫入的數(shù)據都會被暫存在C層面的緩沖區(qū)
close(1)后,即使進程退出后緩沖區(qū)會自動刷新,但是此時已經找不到1號文件的fd了,緩沖區(qū)內的數(shù)據也無法被寫入到內核中,最后也不會顯示到顯示器上
加了'\n'即使最后close(1),遇到'\n'緩沖區(qū)就會立馬將數(shù)據刷新到內核中,就會顯示到顯示器上
為什么fork()之后重定向C接口會被調用兩次?
- 重定向后,緩沖區(qū)的刷新方式會從行緩沖變成全緩沖,也就說,數(shù)據要么等到緩沖區(qū)滿了再被刷新,要么等待進程結束后再刷新,所以我們放在緩沖區(qū)中的數(shù)據,就不會被立即刷新,甚至fork之后
- fork( )之后,創(chuàng)建子進程,子進程會繼承父進程的內核數(shù)據結構對象的內容,父子進程在一開始的時候數(shù)據和代碼是共享的,緩沖區(qū)也屬于數(shù)據
- 進程退出后,要對緩沖區(qū)的數(shù)據進行統(tǒng)一刷新,刷新就是對數(shù)據進行訪問寫入,此時父子數(shù)據會發(fā)生寫時拷貝,所以當父進程準備刷新的時候,子進程也就有了同樣的一份數(shù)據,隨即產生兩份數(shù)據
- 由于write沒有所謂的緩沖區(qū),write()寫入的數(shù)據直接在內核中,所以write( )的數(shù)據只有一份
總結
printf fwrite 庫函數(shù)會自帶緩沖區(qū),而 write 系統(tǒng)調用沒有帶緩沖區(qū)。這里所說的緩沖區(qū), 都是用戶級緩沖區(qū)。其實為了提升整機性能,OS也會提供相關內核級緩沖區(qū)
那這個用戶級緩沖區(qū)誰提供呢? printf fwrite 是庫函數(shù), write 是系統(tǒng)調用,庫函數(shù)在系統(tǒng)調用的“上層”, 是對系統(tǒng) 調用的“封裝”,但是 write 沒有緩沖區(qū),而 printf fwrite 有,說明該緩沖區(qū)是二次加上的,由C標準庫提供

以上為個人經驗,希望能給大家一個參考,也希望大家多多支持腳本之家。

