理解 "文件"
狹義理解
文件在磁盤里
磁盤是永久性存儲介質,因此文件在磁盤上的存儲是永久性的
磁盤是外設(即是輸出設備也是輸入設備)
磁盤上的文件 本質是對文件的所有操作,都是對外設的輸入和輸出 簡稱 IO
廣義理解
Linux 下一切皆文件(鍵盤、顯示器、網卡、磁盤…… 這些都是抽象化的過程)
文件操作的歸類認知
對于 0KB 的空文件是占用磁盤空間的
文件是文件屬性(元數據)和文件內容的集合(文件 = 屬性(元數據)+ 內容)
所有的文件操作本質是文件內容操作和文件屬性操作
系統角度
對文件的操作本質是進程對文件的操作
磁盤的管理者是操作系統
文件的讀寫本質不是通過 C 語言 / C++ 的庫函數來操作的(這些庫函數只是為用戶提供方便),而是通過文件相關的系統調用接口來實現的
C語言的fopen,fwrite,fread是庫函數并不是系統調用,這三個接口都是封裝了操作系統底層的系統調用
回顧C文件接口
打開文件
#include <stdio.h>
int main()
{FILE *fp = fopen("myfile", "w");if(!fp){printf("fopen error!\n");}while(1);fclose(fp);return 0;
}
寫文件
#include <stdio.h>
#include <string.h>
int main()
{FILE *fp = fopen("myfile", "w");if(!fp){printf("fopen error!\n");}const char *msg = "hello bit!\n";int count = 5;while(count--){fwrite(msg, strlen(msg), 1, fp);}fclose(fp);return 0;
}
讀文件
#include <stdio.h>
#include <string.h>
int main()
{FILE *fp = fopen("myfile", "r");if(!fp){printf("fopen error!\n");return 1;}char buf[1024];const char *msg = "hello bit!\n";while(1){//注意返回值和參數,此處有坑,仔細查看man?冊關于該函數的說明 ssize_t s = fread(buf, 1, strlen(msg), fp);if(s > 0){buf[s] = 0;printf("%s", buf);}if(feof(fp)){break;}}fclose(fp);return 0;
}
管理文件
一個進程可以打開多個文件,所以系統中存在大量被打開或正要關閉的文件,所以操作系統需要管理文件,先描述,再組織!!!
操作系統通過一個結構體struct file來管理文件,在文件被打開的時候就會創建一個struct file,其中存放著文件的各種屬性,結構體之間互相鏈接形成鏈表,所以操作系統對文件的管理就轉換成了對鏈表的管理
task_struct中不止存在頁表,還存在著一個文件描述符表struct files_struct,其中存放著一個? struct file數組,數組的每一個的下標都鏈接著一個文件的地址,系統就通過數組來管理每一個進程打開的文件。
相對應一個文件也可以被多個進程打開,那么關閉文件是否會對進程造成影響,畢竟進程是具有獨立性的,而struct file使用和智能指針類似的做法,就是引用計數,只有計數為0的時候才會真正關閉文件
文件的內容會加載到文件緩沖區,文件的屬性會加載到struct file
系統文件 I/O
初識標志位
打開文件的方式不僅僅是 fopen,ifstream 等流式,語言層的方案,其實系統才是打開文件最底層的方案。不過,在學習系統文件 IO 之前,先要了解下如何給函數傳遞標志位,該方法在系統文件 IO 接口中會使用到:
#include <stdio.h>
#define ONE 0001 //0000 0001
#define TWO 0002 //0000 0010
#define THREE 0004 //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);func(THREE);func(ONE | TWO);func(ONE | THREE | TWO);return 0;
}
通過這個代碼能夠了解標志位的作用
狀態表示:每個標志位都能獨立地表示一種狀態。在這個例子中,ONE、TWO 和 THREE 分別代表不同的狀態。
狀態組合:通過按位或運算符 | 可以把多個標志位組合起來,以此表示多種狀態的組合。
狀態檢查:借助按位與運算符 & 能夠檢查某個標志位是否被設置,從而依據不同的狀態執行不同的操作。
系統調用讀文件
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main()
{int fd = open("myfile", O_RDONLY);if(fd < 0){perror("open");return 1;}const char *msg = "hello bit!\n";char buf[1024];while(1){ssize_t s = read(fd, buf, strlen(msg));//類?write if(s > 0){printf("%s", buf);}else{break;}}close(fd);return 0;
}
系統調用寫文件
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main()
{umask(0);int fd = open("myfile", O_WRONLY|O_CREAT, 0644);if(fd < 0){perror("open");return 1;}int count = 5;const char *msg = "hello bit!\n";int len = strlen(msg);while(count--){write(fd, msg, len);//fd: 后?講, msg:緩沖區?地址, len: 本次讀取,期望寫?多少個字節的數據。 返回值:實際寫了多少字節數據 }close(fd);return 0;
}
接口認識
open
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
第一個參數是文件,第二個參數是標志位?,第三個參數表示新創建文件的權限,如果新創建的文件不帶mode會造成新創建的文件權限位亂碼
通過使用標志位用偶有效減少傳參的個數
open標志位介紹
O_RDONLY:以只讀模式打開文件。文件只能被讀取,不能進行寫入操作
O_WRONLY:以只寫模式打開文件。文件只能被寫入,不能進行讀取操作
O_RDWR:以讀寫模式打開文件。文件既可以被讀取,也可以被寫入
O_CREAT:如果文件不存在,則創建該文件。使用此標志位時,需要提供第三個參數 mode 來指定新文件的權限。
O_EXCL:和 O_CREAT 一起使用時,如果文件已經存在,則 open 調用會失敗并返回 -1,同時將 errno 設置為 EEXIST。這可用于確保文件是新創建的。
O_TRUNC:如果文件存在且以可寫模式打開,則將文件長度截斷為零,即清空文件內容
O_APPEND:以追加模式打開文件。每次寫操作都會將數據追加到文件末尾,而不是覆蓋原有內容
O_NONBLOCK:以非阻塞模式打開文件。在進行讀寫操作時,如果沒有數據可讀或無法立即寫入數據,函數不會阻塞,而是立即返回 -1,同時將 errno 設置為 EAGAIN 或 EWOULDBLOCK。這在處理多個文件描述符或需要異步操作時非常有用
而標志位的組合使用|
write
ssize_t write(int fd, const void *buf, size_t count);
第一個參數是文件操作符,第二個參數是寫入文件的內容,第三個參數是需要寫入的字節數
read?
ssize_t read(int fd, void *buf, size_t count);
第一個參數是文件操作符,第二個參數是存放讀到的數據,第三個參數是讀入的字節數
文件操作符
write,read的第一個參數都是fd,fd就是文件操作符
操作系統只認fd,那么fd是什么呢?在文件描述符表中存在struct file數組,fd就是數組的下標。
當我們打開多個文件的時候,查看他們的fd,會發現fd是從3開始的。這是由于數組中默認的0,1,2號下標分別是標準輸入,標準輸出,標準錯誤。
fd的數據是從小到大的,只要前面有多余的位置,系統就會將文件分配到靠前的下標
open的時候就會找到進程的文件描述符表中的數組,找到一個未被使用過的位置,將struct file的地址填入其中,fd就是數組的下標
read就是用fd找到數組的下標,訪問對應文件的地址,從文件緩沖區中將數據拷貝到buffer,文件緩沖區的數據是由磁盤中文件的內容預加載到緩沖區中。
重定向
Linux中存在函數dup2,用于復制文件描述符
int dup2(int oldfd, int newfd);
oldfd:需要被復制的源文件描述符。
newfd:復制到的目標文件描述符。如果newfd已經打開,會先將其關閉。
使用dup2會將newfd位置的文件覆蓋到oldfd上
可以通過dup2來替換標準輸入,標準輸出來實現重定向操作。
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include<unistd.h>
int main()
{int fd = open("test.txt", O_RDWR);dup2(fd,1);close(fd);printf("fd: %d\n", fd);return 0;
}
當我們將文件夾中的test.txt替換掉標準輸出,此時使用printf,?則不會將內容輸出到顯示器上,而是將內容輸出到test.txt中
操作系統只認識fd,printf就是為了往stdout中輸出內容,但是stdout被關閉,此時fd為1的文件是log.txt,printf輸出的內容就會輸出到log.txt中
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include<unistd.h>
int main()
{int fd = open("test.txt", O_RDWR);dup2(fd,0);close(fd);char s[100];scanf("%s",s);printf("%s\n",s);return 0;
}
將test.txt文件替換標準輸入文件,此時scanf一般是從鍵盤上讀取數據,此時就只能從test.txt中讀取數據,將test.txt的數據填寫到s中。
操作系統只認fd,scanf就是從鍵盤中讀取數據,但是標準輸入被關閉,此時fd為0的文件是test.txt,此時scanf就會從?test.txt中讀取數據
通過上面兩端代碼可以大致了解重定向的原理,就是將文件進行替換,從而將一下本該輸出到顯示屏中的數據輸出到其他文件中,將從鍵盤中讀取的數據轉換成從其他文件中讀取數據?
正確的重定向操作方法?
例如存在一個文件:log.txt,需要將原本輸出到顯示屏的數據輸出到log.txt中
ls -l 1 > log.txt
將原本輸出到1號文件的內容輸出到log.txt中?,而1號文件就是stdout
#include<cstdio>
#include<iostream>
using namespace std;
int main()
{cout<<"hellocout"<<endl;cerr<<"hellocerr"<<endl;return 0;
}
當我們使用這段代碼重定向到一個文件時,cerr的內容并不會重定向到文件,而是輸出在顯示屏上,這是由于cerr對應的文件時stderr,它的fd為2,而默認的重定向只有1
所以可以通過將1重定向到log.txt,2追加重定向到log.txt
./a 1 > log.txt 2 >> log.txt
此時cout和cerr的內容就都輸出到log.txt中了?
也可以將標準錯入重定向到標準輸出的目標位置,&1?表示「當前標準輸出的目標位置」
./a 1 > log.txt 2>&1
效果與第一種一樣?
理解“一切皆文件”
首先,在 Windows 中是文件的東西,它們在 Linux 中也是文件;其次一些在 Windows 中不是文件的東西,比如進程、磁盤、顯示器、鍵盤這樣硬件設備也被抽象成了文件,你可以使用訪問文件的方法訪問它們獲得信息;甚至管道,也是文件;將來我們要學習網絡編程中的 socket(套接字)這樣的東西,使用的接口跟文件接口也是一致的。
這樣做最明顯的好處是,開發者僅需要使用一套 API 和開發工具,即可調取 Linux 系統中絕大部分的資源。舉個簡單的例子,Linux 中幾乎所有讀(讀文件,讀系統狀態,讀 PIPE)的操作都可以用 read 函數來進行;幾乎所有更改(更改文件,更改系統參數,寫 PIPE)的操作都可以用 write 函數來進行。
在操作系統中,每個外設都有一套屬于自己的讀寫操作。操作系統中的struct file的內容中存在函數指針,他們會指向對應外設的讀寫操作,而每個函數指針的命名,參數,都一樣
一切皆文件,是進程認為一切皆文件,進程中存儲著文件描述符表,文件描述符表指向struct file,從硬件角度上來看,struct file的函數指針指向硬件的讀寫操作,所以管理好文件也就能管理好硬件,進程無需接觸到硬件,只需接觸到struct file即可
上圖中的外設,每個設備都可以有自己的 read、write,但一定是對應著不同的操作方法!!但通過struct file 下 file_operation 中的各種函數回調,讓我們開發者只用 file 便可調取 Linux 系統中絕大部分的資源!!這便是 “linux 下一切皆文件” 的核心理解。?
緩沖區?
什么是緩沖區
緩沖區就如同日常中的快遞站,可以幫助我們在空閑時間再去拿快遞,而非在快遞送到家門口的時候無論在做任何事情都要回去拿快遞,這樣也嚴重影響了快遞員的效率
緩沖區是內存空間的一部分。也就是說,在內存空間中預留了一定的存儲空間,這些存儲空間用來緩沖輸入或輸出的數據,這部分預留的空間就叫做緩沖區。緩沖區根據其對應的是輸入設備還是輸出設備,分為輸入緩沖區和輸出緩沖區。
可以查看Linux中的FILE結構體
它的指針就指向了緩沖區的位置
為什么要引入緩沖區機制
系統調用也是有成本的,系統調用是需要操作系統進行操作的,而操作系統平時需要進行大量的資源管理,如果沒有緩沖區,當我們進行文件寫入時,操作系統無論做什么事情都需要停下來進行寫入操作
讀寫文件時,如果不會開辟對文件操作的緩沖區,直接通過系統調用對磁盤進行操作 (讀、寫等),那么每次對文件進行一次讀寫操作時,都需要使用讀寫系統調用處理此操作,即需要執行一次系統調用,執行一次系統調用將涉及到 CPU 狀態的切換,即從用戶空間切換到內核空間,實現進程上下文的切換,這將損耗一定的 CPU 時間,頻繁的磁盤訪問對程序的執行效率造成很大的影響。
為了減少使用系統調用的次數,提高效率,我們就可以采用緩沖機制。比如我們從磁盤里取信息,可以在磁盤文件進行操作時,可以一次從文件中讀出大量的數據到緩沖區中,以后對這部分的訪問就不需要再使用系統調用了,等緩沖區的數據取完后再去磁盤中讀取,這樣就可以減少磁盤的讀寫次數,再加上計算機對緩沖區的操作 快于對磁盤的操作,故應用緩沖區可 大提高計算機的運行速度。
又如,我們使用打印機打印文檔,由于打印機的打印速度相對較慢,我們先把文檔輸出到打印機相應的緩沖區,打印機再自行逐步打印,這時我們的 CPU 可以處理別的事情。可以看出,緩沖區就是一塊內存區,它用在輸入輸出設備和 CPU 之間,用來緩存數據。它使得低速的輸入輸出設備和高速的 CPU 能夠協調工作,避免低速的輸入輸出設備占用 CPU,解放出 CPU,使其能夠高效率工作。
緩沖類型
標準 I/O 提供了 3 種類型的緩沖區。
?全緩沖區:這種緩沖方式要求填滿整個緩沖區后才進行 I/O 系統調用操作。對于磁盤文件的操作通常使用全緩沖的方式訪問。
?行緩沖區:在行緩沖情況下,當在輸入和輸出中遇到換行符時,標準 I/O 庫函數將會執行系統調用操作。當所操作的流涉及一個終端時(例如標準輸入和標準輸出),使用行緩沖方式。因為標準 I/O 庫每行的緩沖區長度是固定的,所以只要填滿了緩沖區,即使還沒有遇到換行符,也會執行 I/O 系統調用操作,默認行緩沖區的大小為 1024。
?無緩沖區:無緩沖區是指標準 I/O 庫不對字符進行緩存,直接調用系統調用。標準出錯流 stderr 通常是不帶緩沖區的,這使得出錯信息能夠盡快地顯示出來。
緩沖區的工作
當使用printf/fprintf/fwrite等等的庫函數進行文件操作的時候,數據并非直接寫入到文件內核緩沖區中,c標準庫中存在一個庫緩沖區,使用庫函數進行操作的時候,數據會先占時寫入到庫緩沖區中,直到某種條件的觸發,會將fopen返回值中的fd找出來,通過fd進行系統調用,將庫緩沖區的內容寫到文件內核緩沖區中。
刷新緩沖區的條件
1.強制刷新
2.進程退出
3.刷新條件滿足->(全緩沖,行緩沖,無緩沖)