本節重點?
- 從狹義與廣義角度理解文件
- 理解文件描述符
- 掌握open,write,read系統調用
- 理解重定向的概念與原理
- 掌握重定向的指令操作
- stdout與stderr的比較
- 為什么存在stderr?
一、理解“文件”
1.1 狹義角度
在狹義層面,Linux文件是磁盤或存儲設備上連續或分散的數據塊集合,具有明確的元數據(如文件名、權限、所有者等),通過文件系統進行管理。其核心特征包括:
1.1.1 數據存儲載體
- 文本文件(如
.txt
、.conf
):存儲人類可讀字符。 - 二進制文件(如
.exe
、.o
):編譯后的程序或庫文件,需特定程序解析。 - 設備文件(如
/dev/sda
、/dev/null
):通過文件接口與硬件或內核交互(如/dev/null
丟棄所有寫入數據)。
1.1.2 元數據
每個文件由inode(索引節點)描述,包含:
- 文件類型(普通文件、目錄、符號鏈接等)
- 權限(
rwx
)與所有者(UID/GID) - 時間戳(創建、修改、訪問時間)
- 實際數據塊的磁盤地址(通過直接/間接指針)。
1.2 廣義角度
在廣義層面,Linux將幾乎所有系統資源抽象為文件,通過統一的文件操作接口(open、write、read等)訪問,形成“一切皆文件”的設計哲學。
1.3 系統角度
用戶對文件的操作本質是進程對文件的操作,文件的管理者是操作系統,對文件的操作是通過文件相關的系統調用接口來實現的。
二、回顧C語言文件接口
https://blog.csdn.net/yue_2899799318/article/details/146305837?fromshare=blogdetail&sharetype=blogdetail&sharerId=146305837&sharerefer=PC&sharesource=yue_2899799318&sharefrom=from_link
三、文件相關系統調用
3.1、open
在Linux系統中系統調用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是文件路徑,flags是打開方式
三參數模式:創建并打開新文件時使用,pathname是文件路徑,flags是打開方式,mode用來設置新文件創建時的權限。
參數解析 :
pathname:
要打開或創建的文件路徑,可以是絕對路徑也可以是相對路徑
flags:
必選標志:(只能選其一)
- O_RDONLY,只讀打開
- O_WRONLY,只寫打開
- O_RDWR,讀寫打開
可選標志:(可組合使用)
O_CREAT
:若文件不存在則創建,需配合mode
參數。O_NOFOLLOW
:不跟隨符號鏈接。O_DIRECTORY
:要求路徑必須是目錄,否則失敗。O_CLOEXEC
:執行exec
時自動關閉文件描述符。O_SYNC
:同步寫入,確保數據寫入物理設備。O_NONBLOCK
:非阻塞模式打開,適用于設備文件或管道。O_APPEND
:追加寫入,每次寫操作從文件末尾開始。O_TRUNC
:若文件存在且以寫模式打開,則將其長度截斷為0。O_EXCL
:與O_CREAT
一起使用時,若文件已存在則返回錯誤,確保原子性創建。
mode:
使用mode參數時說明進程想要創建并打開一個新文件,此時mode表示創建文件時初始化文件權限。具體如下:
注意:mode參數只有O_CREAT參數被指定時有效,用來設置新文件的權限
常用權限宏(定義在<sys/stat.h>中):
S_IRUSR
(用戶讀權限)、S_IWUSR
(用戶寫權限)、S_IXUSR
(用戶執行權限)。S_IRGRP
(組讀權限)、S_IWGRP
(組寫權限)、S_IXGRP
(組執行權限)。S_IROTH
(其他用戶讀權限)、S_IWOTH
(其他用戶寫權限)、S_IXOTH
(其他用戶執行權限)。
實際上由于文件掩碼的存在,文件實際的權限=mode&~umask
返回值:
成功時:返回文件描述符(非負整數)
失敗時:返回-1,并設置全局變量errno指示錯誤類型
代碼演示:
#include<stdio.h>
#include <sys/types.h>
#include <fcntl.h>
#include<string.h>
#include<errno.h>
int main()
{umask(0);int ret=open("./text.txt",O_WRONLY|O_CREAT,0666);if(ret==-1){perror("open fail!\n");printf("%s\n",strerror(errno));return errno;}printf("文件描述符為%d\n",ret);return 0;
}
3.2、write
在Linux系統中,write系統調用用來向文件描述符所指定的文件中寫入數據。
函數原型:
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
參數解析:
fd:文件描述符,通過open等系統調用獲取,標識要讀取的文件,管道,套接字等
buf:用戶空間緩沖區指針,存儲待寫入的數據。
count:請求寫入的字節數
返回值:
成功時:返回實際寫入的字節數,可能會小于count。
失敗時:返回-1,并設置全局變量errno指示錯誤類型。
代碼示例:
向指定文件中寫入字符串并讀取打印
#include<stdio.h>
#include <sys/types.h>
#include<unistd.h>
#include<sys/stat.h>
#include <fcntl.h>
#include<string.h>
#include<errno.h>
int main()
{//讀寫方式打開方便我們將寫入數據后打印出來int ret=open("./text.txt",O_RDWR);if(ret==-1){printf("open fail! %s\n",strerror(errno));return 1;}//打開成功:char buff[]={"jinnzhiqi yuejianhua"};int n=write(ret,buff,sizeof(buff));if(n==-1){printf("write fail! %s\n",strerror(errno));return 2;}printf("寫入數據成功!\n"); lseek(ret,0,SEEK_SET);char buff1[1024];int sz=read(ret,buff1,sizeof(buff1)-1);buff1[sz]='\0';printf("%s\n",buff1);return 0;
}
3.3、read
在Linux系統中,系統調用read表示從文件描述符所指定的文件中讀取數據。
函數原型:
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
參數解析 :
fd:文件描述符,通過open等系統調用獲取,標識要讀取的文件,管道,套接字等。
buf:用戶空間緩沖區指針,用來存儲讀取到的數據。
count:請求讀取的最大字節數。
返回值:
成功時:返回實際讀取到的字節數。
- 若返回值小于cout,說明數據不足read已經讀到文件末尾
- 若返回值等于0,表示已經讀到文件末尾或連接失敗
失敗時:返回-1,并設置全局變量errno指示錯誤類型。?
代碼示例:
從指定文件中讀取字符串:
#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<string.h>
#include<errno.h>
int main()
{int ret=open("./text.txt",O_RDONLY);if(ret==-1){printf("open fail! %s\n",strerror(errno));return 1;}//打開成功:char buff[1024];int n=read(ret,buff,sizeof(buff)-1);if(n==-1){printf("read fail! %s\n",strerror(errno));return 2;}//讀取成功:buff[n]='\0';printf("%s\n",buff);return 0;
}
??
四、文件描述符
在Linux系統中,文件描述符(File Descriptor,簡稱FD)是操作系統內核為每個進程維護的一個非負整數標識符,用于抽象地引用進程已打開的文件、套接字(Socket)、管道(Pipe)、設備文件等I/O資源。它是進程與內核交互時管理I/O操作的核心機制。
4.1 核心概念
文件描述符是一個索引值,指向進程打開文件表(Open File Table)中的條目,而非直接指向文件本身。每個描述符對應一個內核維護的struct file
結構體,記錄文件的元數據(如偏移量、權限、引用計數等)。
4.2 理解文件描述符
與進程管理類似,Linux系統對已經打開的文件也采取“先描述再組織”的管理方法。當用戶(進程)打開磁盤上的文件時,系統在系統層面會創建一個struct file結構體用來描述所打開的文件并存儲相關文件信息。
在系統層面,當有多個文件被打開時,為了更高效地管理各個已打開的文件,系統會將每個struct file結構體用雙鏈表的方式鏈接起來,此時對文件的管理就成了對該雙鏈表的增刪查改。
我們知道,Linux系統天然支持多進程,當多個進程打開多個文件時,一方面系統會給每個打開的文件創建struct file結構體并鏈入到全局鏈表中,另一方面,每個進程PCB中都會管理和維護一張文件描述符表(本質是以struct file* 為元素的指針數組)用來指明當前進程打開了多少個文件。
所以本質上,每個進程都有自己的文件描述符表(指針數組),文件描述符就是數組下標。
4.3 文件描述符的分配機制
4.3.1 分配流程
查找最小可用FD:
當進程調用open等系統調用時,內核會從進程的文件描述符表(File Descriptor Table)中搜索一個最小的未被占用的整數作為新描述符。
初始化描述符條目:
內核將該FD將一個內核維護的文件對象(struct file)進行關聯,記錄文件操作指針、偏移量、權限標志等信息。
4.3.2 關鍵數據結構
進程級文件描述符表:
每個進程都管理或維護一個獨立的FD表,由用戶態的int fd索引到內核態的struct file對象。
系統級打開文件表:
所有進程共享的全局表,存儲struct file的引用計數,inode指針等,避免重復加載文件元數據。
4.4 分配規則的核心邏輯
在Linux系統中文件描述符總是默認從低到高順序分配,也就是說內核默認優先分配最小的可用的FD,例如,到當前進程打開了FD:0、1、2則下一個分配的文件描述符就是3。
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include<unistd.h>int main()
{// 分別打印三個標準流的文件描述符printf("stdin: %d\n", stdin->_fileno);printf("stdout: %d\n", stdout->_fileno);printf("stderr: %d\n", stderr->_fileno);umask(0);int n = open("./text.txt", O_RDONLY |O_CREAT,0666);printf("open: %d\n",n);return 0;
}
?
五、 重定向
5.1 概念
在Linux系統中,文件重定向是用于控制程序輸入/輸出(I/O)流向的核心機制,允許用戶將命令的標準輸入(stdin
)、標準輸出(stdout
)或標準錯誤(stderr
)重新定向到文件、設備或其他進程,而非默認的終端(鍵盤/屏幕)
5.2 重定向的類型與語法
5.2.1 輸出重定向
>:覆蓋目標文件(若文件已存在則清空)
$ echo "Hello" > output.txt # 將"Hello"寫入output.txt(覆蓋原有內容)
>>:追加內容到目標文件
$ echo "World" >> output.txt # 在output.txt末尾追加"World"
5.2.2 輸入重定向
<:從文件讀取并輸入(替代鍵盤輸入)
$ wc -l < input.txt # 統計input.txt的行數(等價于wc -l input.txt)
5.2.3 錯誤重定向
2>:將標準錯誤輸出到文件(覆蓋)
$ ls /nonexistent 2> error.log # 將錯誤信息寫入error.log
2>>:將標準錯誤追加到文件
$ ls /nonexistent1 /nonexistent2 2>> error.log # 追加多個錯誤
5.3?底層原理(dup2系統調用)
5.3.1 dup2
dup2是Linux系統中的一個核心系統調用,用于復制文件描述符。其核心作用是將一個現有的文件描述符(oldfd)復制到指定的目標文件描述符(newfd),使newfd指向與oldfd相同的文件表項。這一機制是文件重定向、進程間通信(如管道)等操作的基礎
函數原型:
#include <unistd.h>
int dup2(int oldfd, int newfd);
?參數解析:
oldfd:需要復制的源文件描述符
newfd:目標文件描述符,若newfd已被占用,dup2會先關閉它
返回值:
成功時:返回newfd
失敗時:返回-1,并設置全局變量errno指明錯誤原因
特殊情況:
- 若newfd與oldfd相同,則dup2會直接返回newfd不會關閉它
- 如果oldfd無效則dup2會直接返回-1,并設置errno為EBADF
代碼演示:
?輸出重定向:
#include<stdio.h>
#include<unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{umask(0);int fd=open("./text.txt",O_CREAT|O_WRONLY,0666);if(fd<0){perror("open fail!\n");return 1;}int newfd=dup2(fd,1);if(newfd<0){perror("dup2 fail!\n");return 2;}printf("hello world!\n");printf("hello world!\n");printf("hello world!\n");printf("hello world!\n");return 0;
}
5.4 stdout與stderr
5.4.1 重定向
標準輸出流(stdout)與標準錯誤流(stderror)都是進程啟動時默認打開的I/O流,屬于Unix/Linux系統的標準文件描述符(0=stdin, 1=stdout, 2=stderr)。
若為顯式重定向,兩者均會輸出到當前終端(如命令行界面)。
#include<iostream>
#include<cstdio>int main()
{ std::cout<<"hello cout"<<std::endl;std::cerr<<"hello error"<<std::endl;fprintf(stderr,"hello error\n");return 0;
}
?
?若進行重定向:
./code 1> text.txt //或者./code > text.txt
此時會發現stdout的內容會寫入文件,而strerr的內容會仍然顯式在終端
?如果想讓stderr的內容也重定向到文件text.txt中可以使用以下指令:
//將stdout重定向到text.txt后再追加stderr中的內容
./code 1> text.txt 2>>text.txt
./code 1> text.txt 2>&1
維度 | 標準輸出(stdout) | 標準錯誤(stderr) |
---|---|---|
設計目的 | 輸出程序的正常結果(如計算結果、用戶提示)。 | 輸出程序的錯誤信息(如語法錯誤、運行時異常)。 |
默認行為 | 與標準輸入(stdin)關聯,通常輸出到終端或文件。 | 與標準輸入/輸出獨立,默認也輸出到終端,但可重定向。 |
緩沖機制 | 通常是行緩沖(遇到換行符或緩沖區滿時刷新)。 | 無緩沖或立即刷新,確保錯誤信息及時顯示。 |
重定向方式 | 使用?> ?或?1> ?重定向到文件(如?command > file )。 | 使用?2> ?或?&> ?重定向到文件(如?command 2> error.log )。 |
文件描述符 | 默認文件描述符為?1。 | 默認文件描述符為?2。 |
典型內容 | 程序運行后的正常輸出(如?echo "Hello" )。 | 程序異常時的警告或錯誤(如?ls /nonexistent )。 |
?5.4.2 為什么要存在stderr?
stderror是工程化設計的必然選擇:
- 錯誤隔離:將異常信息與正常數據分離,提升系統可維護性。
- 實時響應:無緩沖機制確保關鍵錯誤即時暴露。
- 靈活控制:通過重定向和管道實現精細化的輸出管理。
如果沒有stderr導致無論是正常信息還是異常信息都會通過stdout來進行輸出,就會導致嚴重錯誤:
- 用戶可能因錯誤信息被截斷或延遲而困惑,甚至無法感知程序失敗。
- 監控腳本無法區分正常數據與錯誤,導致誤報或漏報
為了區分兩者我們必須花費大量時間來過濾信息,這樣做低效且會增加代碼復雜度。
而通過系統級機制stderr將異常信息與正常數據分離,可以提升系統可維護性,也可以通過重定向和管道實現精細化的輸出管理。