目錄
?一、文件描述符
1.文件內核對象?
2.文件描述符分配原則
二、文件重定向
1.重定向的現象
輸出重定向
輸入重定向?
dup2
2.重定向的使用
三、標準輸出和標準錯誤?
繼上篇文章中,我們了解了fd打印的值為文件描述符,那么它還有什么作用呢??
?一、文件描述符
1.文件內核對象?
我們學習了open函數,知道了文件描述符就是一個整數。Linux 進程默認情況下會有?3 個缺省打開的文件描述符,分別是標準輸入 0,?標準輸出 1,?標準錯誤 2。0、1、2 對應的物理設備一般是:鍵盤、顯示器、顯示器。
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);
輸入輸出程序:?
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>int main()
{char buf[1024];// 定義一個1024字節的緩沖區ssize_t s = read(0, buf, sizeof(buf));//從標準輸入(stdin)讀取數據到緩沖區bufif(s > 0)// 檢查是否成功讀取到數據{buf[s] = 0; // 手動添加字符串終止符'\0'write(1, buf, strlen(buf));// 將數據寫入標準輸出write(2, buf, strlen(buf));// 將數據寫入標準錯誤//write() 是直接寫入內核,無需手動刷新緩沖區。}return 0;
}
運行后,輸出一個yes,就會再往顯示屏打印兩個yes?
?
write是如何通過fd找到要打印的文件的呢???
重新理解文件描述符表:
站在用戶的角度,對于文件的操作有這些諸如read、write這些系統調用,也有這些系統調用進行了一定的封裝后誕生的C語言庫函數,這些系統調用的內部會在內存中進行一系列操作,當程序運行的時候,進程里有個叫PCB的結構體,進程的PCB中有這么一塊專門的區域名字叫作fd_array[],它的本質是一個指針數組,它會指向一個一個結構體,而每一個結構體中存儲的是關于這個文件的信息,這個結構體叫做struct file,而前面所說的文件描述符fd,本質上就是這個指針數組的下標。系統默認會打開三個文件:標準輸入、輸出和錯誤。比如你調用read或write的時候,系統會根據fd找到對應的file結構體,然后操作對應文件。不過數據讀寫不是直接和硬盤打交道的——因為CPU只能和內存玩,所以讀寫前得把數據先從硬盤加載到內存里的文件緩沖區里,進程才能從中讀寫。不管讀還是寫,都是在內核緩沖區和用戶空間之間倒騰數據,這也就是為什么系統調用看起來像是在內存里搞事情的原因啦。 結論:在應用層對于數據的讀寫,本質上是將內核緩沖區的數據進行來回拷貝。
2.文件描述符分配原則
我們知道因為系統會默認打開三個文件,分別占用0,1,2
這三個位置,?那么把前面這幾個關掉呢?
關閉0:?
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>int main()
{close(0);int fd = open("log.txt", O_WRONLY|O_CREAT|O_TRUNC, 0666);if(fd < 0){perror("open");return 1;}printf("fd: %d\n", fd);close(fd);return 0;
}
運行結果(關閉2也是一樣的效果):
?
由此可以推測出,文件描述符的分配規則是:尋找最小的,沒有被使用的數據的位置,分配給指定的打開文件?
二、文件重定向
1.重定向的現象
剛剛其實我們已經細微的發現了重定向了,但是現在我們將要學的更加細:
輸出重定向
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>// 重定向
void test()
{close(1);int fd = open("log.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);if(fd < 0){perror("open fail\n");exit(1);}printf("fd-> %d\n", fd);printf("stdout->fd: %d\n", stdout->_fileno);fflush(stdout);close(fd);
}int main()
{test();return 0;
}
?運行結果:
?
運行結果是,在屏幕上沒有任何信息,但在log.txt
中居然出現了輸出信息。?
當程序通過關閉標準輸出(fd=1)并重新打開一個文件(如log.txt)時,由于文件描述符遵循"用完即丟"的復用規則,新文件會搶占原本屬于標準輸出的1號索引位置。此時所有以1為默認目標的輸出操作(包括printf)都會被悄悄重定向——它們不再往顯示器寫數據,而是像對待普通文件一樣向log.txt寫入內容。這個過程中,Linux內核通過VFS將設備抽象成統一的文件對象,使得無論stdout原本指向的是屏幕還是磁盤文件,最終都表現為對特定文件描述符的操作。這種設計巧妙利用了文件描述符的索引特性,實現了I/O重定向的核心邏輯:系統只認數字標號,不關心具體綁定的物理設備,真正實現了"一切皆文件"的統一管理理念。總結:新打開的文件描述符取代了stdout原先的描述符,大家只認fd==1的文件。?
輸入重定向?
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>int main()
{close(0);int fd = open("log.txt", O_RDONLY);if (fd < 0){perror("open");return 1;}printf("fd: %d\n", fd);char buf[64];fgets(buf, sizeof buf, stdin);printf("%s", buf);return 0;
}
?
重定向的本質,就是修改特征文件fd的下標內容。上層的fd不變,變化的是底層fd指向的內容,也就是所謂文件描述符級別的數組內容的拷貝。那這樣的寫法還是太奇怪了,每次都要把一個文件關閉再打開一個新的文件,作為系統理應給操作者提供這樣替換文件描述符的系統調用,事實上也確實提供了這樣的系統調用。?
dup2
SYNOPSIS#include <unistd.h>int dup(int oldfd);int dup2(int oldfd, int newfd);
* If oldfd is not a valid file descriptor, then the call fails, and newfd is not closed.
* If oldfd is a valid file descriptor, and newfd has the same value as oldfd, then dup2() does nothing, and returns newfd.
簡單來說,就是用oldfd
去替換newfd
,保留下來的是oldfd
,那么上面的代碼就可以被改良成這樣:
void redir2()
{int fd = open("log.txt", O_CREAT | O_WRONLY | O_APPEND, 0666);if(fd < 0){perror("open fail\n");exit(1);}dup2(fd, 1);printf("fd-> %d\n", fd);printf("stdout->fd: %d\n", stdout->_fileno);fflush(stdout);close(fd);
}
運行結果:?
?
fd->3,fd也沒有變化,但是就是沒有往顯示屏上打印,依舊是往文件里打印的。也就是說,不需要你去關閉一個文件秒速符,而是用一個文件描述符暫時的代替另外一個文件描述符,這樣不僅更加準確,而且更加明了。
要輸出的文件描述符是 1,而要重定向的目標文件描述符是 fd (echo “hello” > log.txt),dup2 應該怎么傳參 —— dup2(1, fd) || dup2(fd, 1) ??
* If oldfd is not a valid file descriptor, then the call fails, and newfd is not closed.
* If oldfd is a valid file descriptor, and newfd has the same value as oldfd, then dup2() does nothing, and returns newfd.
很明顯依靠函數原型,我們就能認為 dup2(1, fd),因為 1 是先打開的,而 fd 是后打開的.可實際上并不是這樣的,文檔中說 newfd 是 oldfd 的一份拷貝,這里拷貝的是文件描述符對應數組下標的內容,所以數組內容最終應該和 oldfd 一致。換而言之,這里就是想把讓 1 不要指向顯示器了,而指向 log.txt,fd 也指向 log.txt。所以這里的 oldfd 對應 fd,newfd 對應 1,所以應該是 dup2(fd, 1)。
oldfd copy to newfd -> 最后要和誰一樣??oldfd
假設輸出重定向:顯示器(1) -> log.txt(3)。應該是 dup2(1, 3);,還是dup2(3, 1);??
dup2(3, 1);
3 的內容 copy 到 1 里面 -> 最終和 3 一致
2.重定向的使用
?重定向其實并不陌生,在之前的學習中已經用過重定向,只是那是還沒有建立起來一個基礎的概念,先看下面的指令演示:
[root@iZbp1157ft1ib0ydj8jqtzZ test]# echo "hello linux" > log.txt
[root@iZbp1157ft1ib0ydj8jqtzZ test]# cat log.txt
hello linux
這段指令的含義就是,把hello linux重定向到log.txt中,其實這樣的操作符還有下面的這幾種,一一進行介紹
>:這個操作符表示的是輸出重定向,意思就是把內容輸出到某個文件中,有些類似于以w的方式打開一個文件并進行寫入。
>>:這個操作符表示的是追加重定向,意思就是把內容追加輸出到某個文件中,有些類似于append的方式進行寫入。
<:這個操作符表示的是輸入重定向,表示把原來的內容輸入輸入到某個文件中,相當于是替換了標準輸入流的文件。
三、標準輸出和標準錯誤?
前面的知識已經足以理解為什么要有標準輸入和標準輸出,但是還有一個問題有待解決,標準錯誤的意義是什么呢?難道標準輸出的信息還不夠嗎?
答案是確實不夠,在對于大型項目的時候,會有很多的輸出信息,這些輸出信息有些是正常信息,有些是異常信息,而對于開發者來說他們需要的是錯誤信息,因此對于如何獲取錯誤信息就顯得至關重要,于是標準錯誤流信息就誕生了,對于正常來說可能沒有太多的感覺,但是實際上,沒有感覺的原因是因為標準錯誤和標準輸出的文件對象都是顯示器,而實際上這是可以被替換的,基于這個原理可以做出下面的測試代碼:
int main()
{printf("this is normal message\n");perror("this is error message\n");return 0;
}
?運行和操作結果:
[root@iZbp1157ft1ib0ydj8jqtzZ test]# ./test 1>out.txt 2>error.txt
[root@iZbp1157ft1ib0ydj8jqtzZ test]# cat out.txt
this is normal message
[root@iZbp1157ft1ib0ydj8jqtzZ test]# cat error.txt
this is error message
: Success
利用上面的原理可以寫出這樣的測試代碼,把正確信息存儲到一個文件中,把錯誤信息存儲到另外一個文件中,這樣就能知道哪里是錯誤哪里是正確了。
1>&2
意思是把標準輸出重定向到標準錯誤。
2>&1
意思是把標準錯誤輸出重定向到標準輸出。