【Linux基礎I/O一】文件描述符和重定向
- 1.C語言的文件調用接口
- 2.操作系統的文件調用接口
- 2.1open接口
- 2.2close接口
- 2.3write接口
- 2.4read接口
- 3.文件描述符fd的本質
- 4.標準輸入、輸出、錯誤
- 5.重定向
- 5.1什么是重定向
- 5.2輸入重定向和輸出重定向
- 5.3系統調用的重定向dup2
- 6.緩沖區
1.C語言的文件調用接口
- fopen
FILE *fopen(const char *filename, const char *mode);
filename 參數是一個字符串,指定要打開的文件名或路徑
mode 參數是一個字符串,指定文件的打開模式,常見如下:
“r”:只讀,若文件不存在則報錯
“w”:只寫,若文件不存在則創建,打開時清空文件原有內容
“a”:只寫,若文件不存在則創建,打開時從文件末尾追加
返回值:
如果成功打開文件,則返回指向 FILE 類型結構的指針,該指針用于后續的文件操作。
如果打開失敗,返回 NULL,并且通過檢查 errno 變量可以確定失敗的具體原因
FILE *fp = fopen("temp.txt", "r");
if (fp == NULL) {perror("Error opening file");return 1;
}
- fclose
int fclose(FILE *stream);
stream 是一個指向 FILE 結構的指針,指定要關閉的文件
返回值:
如果成功關閉文件,則返回 0。
如果關閉失敗,則返回 EOF
FILE *fp = fopen("temp.txt", "r");
if (fp == NULL) {perror("opening error");return 1;
}if (fclose(fp) == 0) {printf("close succeed.\n");
} else {perror("closing error");return 1;
}
- fprintf
int fprintf(FILE *stream, const char *format, ...);
stream是一個指向FILE結構的指針,指定要寫入的目標文件
format是一個格式化字符串,類似于printf函數中的格式化字符串,用于指定輸出的格式
返回值:
成功返回寫入的字符數,如果出錯則返回一個負數
默認打開文件的時候,清空文件內容
a打開方式:append追加方式寫入文件,不會清空原文件內容
2.操作系統的文件調用接口
2.1open接口
pathname 是一個字符串,表示要打開的文件路徑
flags 是打開文件的標志位,例如O_RDONLY
(只讀)、O_WRONLY
(只寫)、O_RDWR
(讀寫)、O_CREAT
(若打開的文件不存在,則創建該文件)
mode 是文件的權限,通常與 flags 參數中的某些標志結合使用,用于指定文件的創建模式
返回值:
如果成功,返回一個新的文件描述符,用于后續的文件操作
如果出錯,返回 -1
#include <stdio.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>int main()
{int fd = open("log.txt", O_WRONLY | O_CREAT);return 0;
}
運行./mybin后,當前目錄就出現了文件log.txt了,但是它的權限值是亂碼-r-s–x–T,如果你刪掉該文件后再次運行代碼,你會發現下一次的權限值和上次還不一樣
此時就需要三個參數的open接口:
mode用于控制文件的初始權限,一般系統默認掩碼是0002
#include <stdio.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>int main()
{int fd = open("log.txt", O_WRONLY | O_CREAT, 0666);return 0;
}
open的第三個參數傳入0666,也就是log.txt的初始權限
由于umask是0002,所以最終權限為664也就是rw-rw-r–
2.2close接口
作用:關閉文件
描述符fd對應的文件, 調用成功返回0
2.3write接口
fd:被寫入文件的fd
buf:指向被寫入的字符串
count:寫入字符的個數
#include <stdio.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>int main()
{int fd = open("log.txt", O_WRONLY | O_CREAT, 0666);char* str = "Hello\n Hello\n";write(fd, str, 6);close(fd);return 0;
}
向log.txt寫入了一個字符串str,write的第三個參數為6,只寫入了6個字符Hello\n,輸出結果是Hello
在保留原先log.txt的情況下,更改test.c:
#include <stdio.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>int main()
{int fd = open("log.txt", O_WRONLY | O_CREAT, 0666);char* str = "Hello\n Hello\n";write(fd, str, 6);close(fd);return 0;
}
log.txt的內容變成了123lo,為什么?
以O_WRONLY模式對文件進行寫入時,不會先把文件內容清空,而是直接從頭開始覆蓋
有兩種解決方案,對應兩個open的選項
O_TRUNC
打開時清空文件內容O_APPEND
以追加的形式寫入
2.4read接口
fd:目標文件的文件描述符fd
buf:將文件內容讀取到buf指向的空間中
count:最多讀取count個字節的內容
返回值:
< 0:讀取發送錯誤
= 0:讀取到文件末尾
0:讀取成功,返回讀取到的字符個數
#include <stdio.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <string.h>int main()
{int fd = open("log.txt", O_RDONLY);char buffer[1024];ssize_t ret1 = read(fd, buffer, 1024);printf("ret1 = %ld\n", ret1);printf("%s", buffer);close(fd);return 0;
}
在系統創建一個log.txt文件,內容為字符串hello Linux
一開始用open以只讀RDONLY形式打開文件,通過read(fd, buffer, 1024)
把內容存儲到數組buffer中,并把返回值交給ret1,隨后輸出ret1和buffer
第一次read返回值為12,也就是字符串Hello Linux有12個字符
3.文件描述符fd的本質
文件描述符的本質是一個數組的下標
在Linux系統下,一切皆文件
文件 = 內容 + 屬性
文件操作 = 對內容的操作 或 對屬性的操作
因為馮諾依曼體系結構,CPU只和內存做交互,所以要對文件做修改就需要加載到內存
我們會同時打開多個文件,也會有別人同時打開文件,所以,操作系統需要管理這些打開的文件
管理 = 先描述,再組織
操作系統同樣會像管理進程那樣,也給文件創建對應的結構體,然后文件之間會有一些連接關系
這樣,對文件的管理就變成對某種數據結構的管理
文件操作是用戶讓操作系統進行的,操作系統會為此創建進程
所以文件操作實質是進程和被打開文件的關系
操作系統運行時,會有很多進程在運行
在文件沒有被打開之前,文件是存在磁盤中的
打開文件是進程打開的:進程將文件的數據加載到文件內核級的緩存塊中
而一個進程能打開很多文件,同時,系統當中可以存在許多進程
為了更好的管理文件,所有被打開的文件,操作系統都會對其創建一個結構體struct file
文件 = 屬性 + 內容,而這個struct file結構體對象就是用來描述被打開的文件
被打開的文件結構體對象,被操作系統用雙向鏈表組織起來,成為一個雙向鏈表
于是,操作系統對被打開文件的管理就變成了對一個雙向鏈表的增刪查改,解決了對文件的管理問題
文件和進程之間的關系如何描述和組織?
進程的PCB(task_struct)結構體對象中存在一個指針變量:struct files_struct *file
在Linux 2.6.10內核中,struct files_struct如下:
其最后一個成員fd_array
是一個數組,指向的成員類型為struct file*
,也就是指向struct file的指針
struct files_struct {atomic_t count;spinlock_t file_lock;/* Protects all the below members. Nests inside tsk->alloc_lock */int max_fds;int max_fdset;int next_fd;struct file ** fd;/* current fd array */fd_set *close_on_exec;fd_set *open_fds;fd_set close_on_exec_init;fd_set open_fds_init;struct file * fd_array[NR_OPEN_DEFAULT];
};
操作系統全局管理的struct files_struct
,整個系統中所有被打開的文件都要被這個結構體管理
每個進程自己打開維護的struct files_struct
,分別管理自己打開的文件
這個指針數組內部的一個個指針就對應著一個個被打開的文件的地址
而文件描述符fd就是這個數組的下標
所以,當用戶想要訪問一個進程下的文件時,因為每一個進程的PCB是唯一的,所以文件管理數組也唯一的,只要是進程打開了一個文件,就會把文件的*file
添加進去
只需要返回這個進程PCB結構體中fd_array
數組的下標
就可以找到對應的文件了
下述三個系統調用函數都有一個int fd,當打開一個文件時,
要對這個文件進行寫、讀、操作,都需要傳遞open時返回的文件描述符fd
用fd去找到對應的文件然后執行相關操作
對文件的讀寫等操作的數據改動都是在緩沖區內進行
然后由操作系統刷新到磁盤對應位置
所以,讀取文件,本質上就是從文件的文件內核緩沖區內加載數據到應用層的用戶級緩沖區
4.標準輸入、輸出、錯誤
0:標注輸入 (鍵盤)
1:標準輸出 (顯示器)
2:標準錯誤 (顯示器)
int fd1 = open("text1.txt",O_WRONLY | O_CREAT);
int fd2 = open("text2.txt",O_WRONLY | O_CREAT);
int fd3 = open("text3.txt",O_WRONLY | O_CREAT);
int fd4 = open("text4.txt",O_WRONLY | O_CREAT);printf("fd1 = %d\nfd2 = %d\nfd3 = %d\nfd4 = %d\n",fd1,fd2,fd3,fd4);
確實多出了4個,文件文件描述符從3開始,編號0 1 2的fd去哪里了?
C語言中,會為我們默認打開三個流stdin
標準輸入,stdout
標準輸出,stderr
標準錯誤,0、1、2為編號的fd就已經被占用
C語言中,文件流是以FILE的形式被管理的,毫無疑問FILE是對Linux文件系統的封裝,FILE內部一定存儲了fd,否則無法通過fd來訪問特定的文件,其中FILE的_fileno成員就是fd
#include <stdio.h>int main()
{int fd1 = stdin->_fileno;int fd2 = stdout->_fileno;int fd3 = stderr->_fileno;printf("stdin->_fileno = %d\n", fd1);printf("stdout->_fileno = %d\n", fd2);printf("stderr->_fileno = %d\n", fd3);return 0;
}
LInux下一切皆文件:
在Linux操作系統中的每一個驅動設備都創建了struct file結構體
結構體內部的屬性就是設備的屬性數據和函數指針
所以,盡管每一個設備的操作方法不一樣,但可以把方法的返回值、參數設置成一樣的,然后讓這些函數指針指向底層的硬件設備的操作方法,站在Linxu的角度來看這些硬件,也視為文件
實現這種組織的技術其實就是多態
所以對于硬件這一層,在Linux下叫做vfs,即vitural file system虛擬軟件系統
5.重定向
5.1什么是重定向
文件描述符的分配規則:查看自己的文件描述表,分配最小的沒有被使用的fd
printf和scanf作為C語言的函數接口,同樣無法直接和顯示器和鍵盤作交互,其必須依靠相應的文件,也就是標準輸出和標準輸入
但是依靠的指向并不是FILE*,而是文件描述符,printf依靠1號文件,scanf依靠0號文件
#include <stdio.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>int main()
{close(1);//關掉1號文件(stdout)int fd1 = open("log.txt",O_WRONLY | O_CREAT | O_APPEND,0666);//打開log.txtprintf("Hello Linux\n");//向屏幕打印printf("Hello Linux\n");printf("Hello Linux\n");printf("Hello Linux\n");return 0;
}
可以看到Hello Linux
并沒有如愿打印在屏幕上,因為printf只往1號文件里寫,就算使用fprintf向stdout輸出也不會輸出到屏幕
說明printf函數和stdout只認文件描述符1,不管1此時還是不是
標準輸出
但1號文件被關閉,系統給新文件log.txt分配最小的沒有被使用的fd,也就是1號,所以本來寫入到顯示器里的內容寫入到了log.txt
這就是重定向,重定向的本質:是在內核中改變文件描述符表特定下標的內容
5.2輸入重定向和輸出重定向
>
代表輸出重定向,將輸出從顯示器更改到指定文件
<
代表輸入重定向,將輸入從鍵盤更改到指定文件
測試:
#include <stdio.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>int main()
{printf("printf->stdout\n");fprintf(stdout,"fprintf->stdout\n");fprintf(stderr,"fprintf->stderr\n");return 0;
}
直接運行會向顯示器打印
重定向后標準輸出被重定向到文件中了,但是標準錯誤仍然還是輸出到顯示器中
因為
>
是輸出重定向,只會更改標準輸出stdout的文件描述符,也就是將指向顯示器的文件描述符1更改指向log,txt,但是并不會更改標準錯誤stderr的文件描述符2,所以標準錯誤依然寫入到顯示器上,只有標準輸出寫入到了指定的文件中
>
:將命令的標準輸出重定向到文件,會覆蓋文件內容。
<
:將文件內容作為命令的標準輸入。
>>
:將命令的標準輸出重定向到文件,追加到文件末尾,不會覆蓋文件內容。
5.3系統調用的重定向dup2
操作系統提供一個專門用于文件描述符改向的系統調用就叫做dup2
oldfd:原始文件描述符,表示要復制的文件描述符。
newfd:目標文件描述符,表示將 oldfd復制給的newfd
返回值:
成功時dup2 返回 newfd(復制操作后新的文件描述符)
失敗時返回 -1,并將 errno 設置為相應的錯誤代碼。
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>int main()
{int fd;// 打開文件,如果文件不存在則創建,權限為0644fd = open("output.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);// 將標準輸出重定向到fddup2(fd, STDOUT_FILENO);//STDIN_FILENO和STDOUT_FILENO是C語言標準庫中定義的宏,分別用于表示標準輸入和標準輸出的文件描述符,這兩個宏在<unistd.h>頭文件中定義,通常被賦值為0和1printf("Hello Linux\n");// 關閉原文件描述符close(fd);return 0;
}
6.緩沖區
緩沖區本質上就是一塊內存區域
fwrite
等C語言的文件IO接口,都在底層封裝了系統調用接口write
,用戶調用的所有C語言文件IO接口,都會先寫到緩沖區中,然后等到一定條件,在通過一次系統調用,把之前所有緩沖區的數據寫入到操作系統中,這個一次性寫入過程叫做刷新緩沖區
操作系統也有自己的緩沖區,但是這個是操作系統自己管理的內核緩沖區,其決定了wirte等系統調用接口寫入的數據何時寫入到內存中,但不是該博客討論的范圍,后續討論的緩沖區都是用戶級緩沖區(語言級別的文件緩沖區)
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>int main()
{close(1);int fd = open("myfile.txt", O_WRONLY|O_CREAT, 0666);printf("fd: %d\n", fd);//fflush(stdout);close(fd);exit(0);//中斷程序}
當運行上述代碼時,即使對一個文件寫入了內容,也沒有顯示
使用了fflush函數之后,就顯示了
原因就是struct FILE 結構體內還有一個語言級別的文件緩沖區
我們使用的printf、fprintf函數等寫入的數據都是寫到了語言級別的文件緩沖區,而不是到了內存的緩沖區
所以fflush函數所作的工作其實就是把語言級別的緩沖區刷新到內存中
所以如果在文件關閉之前,沒有進行刷新,那么,就無法把語言級的緩沖區刷新到內存
用戶級緩沖區的刷新策略有以下幾種:
1.無緩沖:不進行緩沖,直接輸出
2.行緩沖:向顯示器寫入時,'\n’會強制刷新緩沖區,也就是一行一行刷新緩沖區
3.全緩沖:向普通文件寫入時,一般緩沖區被寫滿才會刷新
4.程序結束,強制刷新緩沖區
5.用戶調用flush函數,強制刷新緩沖區
緩沖區被struct FILE管理,由于每個文件的FILE是獨立的,因此每個文件的緩沖區也是獨立的
特殊情況:
#include <stdio.h>
#include <unistd.h>
#include <string.h>int main()
{fprintf(stdout,"fprintf->stdout\n");char* temp = "write\n";write(1,temp,strlen(temp));fork();return 0;
}
我們成功將內容輸出到了顯示器
但是,當我們將這些內容重定向到文件中,C語言庫的輸出打印了兩次
為什么write不打印兩次?
當我們重定向時,write是系統調用,直接寫入struct file的緩沖區(文件內核的緩沖區)
為什么fprintf在fork之前就已經完成了卻打印了兩次?
fprintf因為是語言層面的調用,將內容寫入FILE結構體的緩沖區(語言級別的文件緩沖區),原本向顯示器輸出的策略是行刷新,但在重定向到log.txt前還沒有行刷新將其打印,所以fprint->stdout
就被留在了緩沖區
當子進程被創建時會繼承父進程的數據和代碼,stdin的FILE指向的緩沖區也會被繼承
所以當父子進程同時進行至刷新緩沖區時,分別輸出了一句fprint->stdout