目錄
1、理解“文件”
1.1 狹義理解
1.2 廣義理解
1.3 文件操作的歸類認知
1.4?系統角度
2、回顧C文件接口
2.1 文件的打開與關閉
2.2 文件的讀寫函數
2.3?stdin & stdout & stderr
3、系統文件I/O
3.1 一種傳標志位的方式
3.2 文件的系統調用接口
3.2.1 open()
3.2.2 read()?& write()?& close()
3.3 庫函數和系統調用
3.4?文件描述符fd
3.4.1 0 & 1 & 2
3.4.2 文件描述符的分配規則
3.4.3 重定向
3.4.4 重定向系統調用dup2()
4、理解一切皆文件
5、緩沖區
5.1 緩沖區的定義
5.2 緩沖區的作用
5.3 緩沖區的機制
現象1:
現象2:
1、理解“文件”
1.1 狹義理解
- 文件在磁盤里。
- 磁盤是永久性存儲介質,因此文件在磁盤上永久性存儲。
- 磁盤是外設(即是輸出設備也是輸入設備)。
- 對磁盤文件的所有操作(如讀取、寫入)本質上都是對外設的輸入/輸出,簡稱I/O(Input/Output)。
1.2 廣義理解
- Linux中,一切皆文件(鍵盤、顯示器、網卡、磁盤……這些都是抽象化的過程)(后面會深入理解)。
1.3 文件操作的歸類認知
- 文件 = 屬性(元數據)+ 內容。
- 對于0KB的空文件是占用磁盤空間的,有文件屬性。
- 所有的文件操作本質是文件內容操作和文件屬性操作。
1.4?系統角度
- 對文件的操作本質是進程對文件的操作。
- 磁盤的管理者是操作系統。
- 文件的讀寫本質不是通過C語言/C++的庫函數來操作的(這些庫函數只是為用戶提供方便),而是通過文件相關的系統調用接口來實現的。
文件分為“內存級(被打開)”文件,“磁盤級(未打開)”文件。
本節主講“內存級(被打開)”文件。
2、回顧C文件接口
2.1 文件的打開與關閉
FILE *fopen(const char *path, const char *mode);
mode | 含義 | 文件不存在時 | 文件存在時 | 寫入方式 |
---|---|---|---|---|
"r" | 只讀 | 返回?NULL | 正常打開 | 不可寫入 |
"r+" | 讀寫 | 返回?NULL | 正常打開 | 從當前位置覆蓋 |
"w" | 只寫(新建) | 新建文件 | 清空原內容 | 從頭寫入 |
"w+" | 讀寫(新建) | 新建文件 | 清空原內容 | 從頭寫入 |
"a" | 追加(只寫) | 新建文件 | 保留內容,追加到末尾 | 只能末尾追加 |
"a+" | 追加(讀寫) | 新建文件 | 保留內容,可讀/追加寫入 | 可讀,但寫入僅限末尾 |
int fclose(FILE *fp);
注意:
ls /proc/[ 進程 id] -l 命令,查看當前正在運行進程的信息。
- cwd:指向進程的當前工作目錄,創建文件和打開文件的默認路徑。
- exe:指向啟動當前進程的可執行文件的路徑。
2.2 文件的讀寫函數
函數名 | 功能描述 | 適用流類型 | 參數說明 | 返回值 | 備注 |
---|---|---|---|---|---|
fgetc | 從流中讀取單個字符 | 所有輸入流 (如 stdin 、文件) | FILE *stream (文件指針) | 讀取的字符(int )失敗返回 EOF | 通常用于逐字符處理 |
fputc | 向流寫入單個字符 | 所有輸出流 (如 stdout 、文件) | int char (字符)FILE *stream | 寫入的字符(int )失敗返回 EOF | |
fgets | 從流中讀取一行文本 | 所有輸入流 | char *str (緩沖區)int n (最大長度)FILE *stream | 成功返回str 失敗返回 NULL | 保留換行符\n |
fputs | 向流寫入一行文本 | 所有輸出流 | const char *str (字符串)FILE *stream | 成功返回非負值 失敗返回 EOF | 不自動添加換行符 |
fscanf | 格式化輸入(類似scanf ) | 所有輸入流 | FILE *stream const char *format (格式字符串)... (變量地址) | 成功匹配的參數數量 失敗返回 EOF | 需注意緩沖區溢出風險 |
fprintf | 格式化輸出(類似printf ) | 所有輸出流 | FILE *stream const char *format ... (變量值) | 成功返回寫入字符數 失敗返回負值 | |
fread | 二進制輸入(塊讀取) | 文件流 | void *ptr (緩沖區)size_t size (每塊大小)size_t nmemb (塊數)FILE *stream | 實際讀取的塊數 | 用于結構體等二進制數據 |
fwrite | 二進制輸出(塊寫入) | 文件流 | const void *ptr (數據地址)size_t size size_t nmemb FILE *stream | 實際寫入的塊數 |
注意:
寫字符串,不用寫\0,因為這是C語言的規定,不是文件的規定,寫進去會亂碼。?
2.3?stdin & stdout & stderr
C程序啟動,默認打開三個輸入輸出流,分別是stdin,stdout,stderr。
#include <stdio.h>extern FILE *stdin; // 標準輸入,鍵盤文件
extern FILE *stdout; // 標準輸出,顯示器文件
extern FILE *stderr; // 標準錯誤,顯示器文件
3、系統文件I/O
3.1 一種傳標志位的方式
使用位圖,用比特位作為標志位。
#include <stdio.h>#define ONE (1 << 0) // 0000 0001 (二進制)
#define TWO (1 << 1) // 0000 0010 (二進制)
#define THREE (1 << 2) // 0000 0100 (二進制)void func(int flags) {if (flags & ONE) printf("flags has ONE! ");if (flags & TWO) printf("flags has TWO! ");if (flags & THREE) printf("flags has THREE! ");printf("\n");
}int main() {func(ONE); // 輸出: flags has ONE!func(THREE); // 輸出: flags has THREE!func(ONE | TWO); // 輸出: flags has ONE! flags has TWO!func(ONE | TWO | THREE); // 輸出: flags has ONE! flags has TWO! flags has THREE!return 0;
}
3.2 文件的系統調用接口
man 2 系統調用,有具體說明。
3.2.1 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:打開文件時的選項標志,可以使用以下常量通過"或"運算(|)組合:
必須指定且只能指定一個的選項:
-
O_RDONLY:只讀打開。
-
O_WRONLY:只寫打開。
-
O_RDWR:讀寫打開。
可選標志:
-
O_CREAT:若文件不存在則創建它(需要mode參數,設置新文件的訪問權限)。
-
O_APPEND:追加寫模式。
-
O_TRUNC:如果文件已存在且為普通文件,打開時會將其長度截斷為0,邏輯上的清空(類似與vector的size)
return value:
-
成功:返回新打開的文件描述符fd(非負整數)
-
失敗:返回-1。
注意:
那么C語言的fopen的flag就是:
“r” = O_RDONLY;
“w” = O_CREAT | O_WRONLY | O_TRUNC;
“a” = O_CREAT | O_WRONLY | O_APPEND;
“r+” = O_RDWR;
“w+” =?O_CREAT | O_RDWR?| O_TRUNC;
“a+” = O_CREAT | O_RDWR?| O_APPEND。
3.2.2 read()?& write()?& close()
類比C文件相關接口。
#include <unistd.h>ssize_t read(int fd, void *buf, size_t count);
fd:文件描述符
buf:存儲讀取數據的緩沖區
count:請求讀取的字節數
#include <unistd.h>ssize_t write(int fd, const void *buf, size_t count);
fd:文件描述符
buf:包含待寫入數據的緩沖區
count:請求寫入的字節數
#include <unistd.h>int close(int fd);
fd:要關閉的文件描述符
注意:
read()和write()的buf都是void*,不關心數據格式,以二進制流輸入輸出。
那么為什么語言層,有字符流的輸入輸出?
- 首先,底層都是二進制流的輸入輸出。
- 字符按ASCII輸入(讀出),按ASCII輸出(寫入)。對于字符設備,字符通過ASCII轉化成二進制寫到里面,然后通過ASCII解釋,以字符的形式顯示。
字符流的輸入輸出,是因為,我們輸入輸出的是字符串。
3.3 庫函數和系統調用
類型 | 示例函數 | 所屬層級 | 特點 |
---|---|---|---|
庫函數 | fopen ,?fclose ,?fread ,?fwrite | C標準庫(libc) | 1. 提供更高級的抽象 2. 帶緩沖區 3. 可移植性更好 4. 最終會調用系統調用 |
系統調用 | open ,?close ,?read ,?write ,?lseek | 操作系統接口 | 1. 直接與內核交互 2. 無緩沖區 3. 效率更高但更底層 4. 與具體操作系統相關 |
3.4?文件描述符fd
3.4.1 0 & 1 & 2
Linux 進程默認情況下會有 3 個缺省打開的文件描述符,分別是
標準輸入 0,標準輸出 1,標準錯誤 2
0,1,2 對應的物理設備一般是:鍵盤,顯示器,顯示器
所以輸入輸出還可以采用如下方式:
0,1,2是自動打開的
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h> // 添加read/write所需的頭文件int main()
{char buf[1024];// 從標準輸入(文件描述符0)讀取數據ssize_t s = read(0, buf, sizeof(buf) - 1); // 保留1字節給結尾的\0if(s > 0) {buf[s] = '\0'; // 添加字符串結束符// 將輸入內容同時輸出到標準輸出(1)和標準錯誤(2)write(1, buf, s);write(2, buf, s);}return 0;
}
而現在知道,文件描述符就是從 0 開始的小整數。當我們打開文件時,操作系統在內存中要創建相應的數據結構來描述目標文件。于是就有了 file 結構體,表示一個已經打開的文件對象。而進程執行 open 系統調用,所以必須讓進程和文件關聯起來。每個進程都有一個指針 * files,指向一張表 files_struct,該表最重要的部分就是包含一個指針數組,每個元素都是一個指向打開文件的指針!所以,本質上,文件描述符就是該數組的下標。所以,只要拿著文件描述符,就可以找到對應的文件。
注意:
C語言的stdin(fd = 0),stdout(fd = 1),stderr(fd = 2),是一個FILE結構體的指針,FILE結構體里面封裝了文件描述符fd,其他語言也一樣。
3.4.2 文件描述符的分配規則
直接看代碼:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h> // 添加 close 函數所需的頭文件int main()
{int fd = open("myfile", O_RDONLY);if (fd < 0) {perror("open");return 1;}printf("fd: %d\n", fd);close(fd); // 正確的關閉位置return 0;
}
輸出:?fd: 3
關閉 fd = 0 或者 fd = 2,再看
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h> // 添加 close() 所需的頭文件int main()
{close(0); // 關閉標準輸入(文件描述符 0)// close(2); // 注釋掉的關閉標準錯誤(文件描述符 2)int fd = open("myfile", O_RDONLY);if (fd < 0) {perror("open");return 1;}printf("fd: %d\n", fd);close(fd); // 關閉文件描述符return 0;
}
?輸出:?fd: 0或?fd: 2
結論:
在 Linux 系統中,文件描述符的分配原則:最小的,沒有被使用的下標,作為fd,給新打開的文件。
3.4.3 重定向
那如果關閉 fd = 1 呢?看代碼:
#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", O_CREAT | O_WRONLY | O_TRUNC, 0644);if(fd < 0) {perror("open");return 1;}printf("fd: %d\n", fd);fflush(stdout);close(fd);exit(0);
}
因為語言層只認stdout中的fd = 1,此時下標為1的指針指向myfile,所以
本來應該輸出到顯示器上的內容,輸出到了myfile文件中。
這種現象叫做輸出重定向。
常見的重定向有: >,>>,<。
輸出重定向的本質:
注意:
cat log.txt > myfile,實際上是cat log.txt 1>myfile,只重定向了標準輸出,
cat log.txt 1>myfile 2>&1,重定向了標準輸出和標準錯誤。
3.4.4 重定向系統調用dup2()
#include <unistd.h>int dup2(int oldfd, int newfd);
oldfd的指針 覆蓋 ?newfd的指針 。
如:dup2(fd,0),實現輸入重定向,dup2(fd,1),實現輸出重定向。
所以,重定向 = 文件打開方式 + dup2()。
4、理解一切皆文件
首先,在 Windows 中是文件的東西,它們在 Linux 中也是文件;其次一些在 Windows 中不是文件的東西,比如進程、磁盤、顯示器、鍵盤這樣的硬件設備也被抽象成了文件,你可以使用訪問文件的方法訪問它們獲得信息;甚至管道,也是文件;將來我們要學習網絡編程中的 socket(套接字)這樣的東西,使用的接口跟文件接口也是一致的。
這樣做最明顯的好處是,開發者僅需要使用一套 API ,即可調取 Linux 系統中絕大部分的資源。舉個簡單的例子,Linux 中幾乎所有讀(讀文件,讀系統狀態,讀 PIPE)的操作都可以用 read 函數來進行;幾乎所有更改(更改文件,更改系統參數,寫 PIPE)的操作都可以用 write 函數來進行。
上圖中的外設,每個設備都可以有自己的 read、write,但一定是對應著不同的操作方法!!但通過 struct file 下的 struct file_operations 中的各種函數回調,讓我們開發者只用 file 便可調取 Linux 系統中絕大部分的資源!!這便是 "Linux 下一切皆文件" 的核心理解。
封裝+多態的體現。?
5、緩沖區
5.1 緩沖區的定義
臨時存儲數據的內存區域。
5.2 緩沖區的作用
提高使用者的效率。
5.3 緩沖區的機制
- 用戶級語言層緩沖區,避免頻繁調用系統調用(成本高),提高C語言接口的效率。
- 文件內核緩沖區,提高系統調用的效率。
- 可以通過fsync(),將文件內核緩沖區的數據刷新到硬件。
- 一般認為數據交給OS,就相當于交給硬件。
基于上面的機制,可以理解下面的現象:
現象1:
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>int main() {// 關閉標準輸出(文件描述符1)close(1);int fd = open("log.txt", O_CREAT | O_WRONLY | O_TRUNC, 0664);if (fd < 0) {perror("open");return 1;}printf("hello world: %d\n", fd); // 注意:這里打印的fd值應該是1close(fd);return 0;
}
這個時候,對于普通文件,應該是滿了刷新,可是沒滿,也沒有強制刷新,然后關閉了fd,在程序退出時,刷新,但fd已經關閉了,刷新不了,所以log.txt中不會有數據。
可以使用 fflush()?強制刷新下緩沖區。
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>int main() {// 關閉標準輸出(文件描述符1)close(1);int fd = open("log.txt", O_CREAT | O_WRONLY | O_TRUNC, 0664);if (fd < 0) {perror("open");return 1;}printf("hello world: %d\n", fd); // 注意:這里打印的fd值應該是1fflush(stdout); // 強制刷新close(fd);return 0;
}
注意:stderr是不帶緩沖區,即立即刷新。
現象2:
#include <stdio.h>
#include <string.h>
#include <unistd.h> // 添加 write() 和 fork() 所需的頭文件int main() {const char *msg0 = "hello printf\n";const char *msg1 = "hello fwrite\n";const char *msg2 = "hello write\n";printf("%s", msg0);fwrite(msg1, 1, strlen(msg1), stdout);write(1, msg2, strlen(msg2));fork();return 0;
}
結果:
hello printf
hello fwrite
hello write
顯示器,行刷新;
系統調用write(),直接寫入內核。
但是重定向一下 ./hello > file,結果:
hello write
hello printf
hello fwrite
hello printf
hello fwrite
系統調用write(),直接寫入內核;
重定向,改變了刷新方式,普通文件,滿了刷新,可是沒慢,也沒有強制刷新,程序退出時,刷新,父子進程各刷新一份。