基礎 IO (Linux學習筆記)

有志者事竟成1019發表於2024-04-20

基礎IO

1.重談檔案

  • 空檔案在磁碟也要佔據空間
  • 檔案 = 內容 + 屬性
  • 檔案操作 = 對檔案內容+對屬性 or 對檔案內容加屬性
  • 標定一個檔案,必須使用檔案路徑加檔名【唯一性】
  • 如果沒有指明對應得檔案路徑,預設是在當前路徑下進行檔案訪問
  • 當寫了一個跟檔案操作有關得程式,編譯後,檔案有沒有被操作呢?沒有,所以本質是程序對檔案的操作
  • 一個檔案要被訪問,肯定要先被開啟

所以

檔案操作=程序+被開啟的檔案 的關係

一個檔案被使用者程序首次開啟即被執行了Open操作,會把檔案的FCB調入記憶體,而不會把檔案內容讀到記憶體中,只有程序希望獲取檔案內容的時候才會讀入檔案內容

2. C檔案介面

C語言,C++,java,python,php,go等語言都有檔案操作介面,函式都不一樣,對應的庫函式都不一樣

檔案在哪裡---->磁碟-,磁碟是硬體---->怎麼訪問磁碟----->OS進行操作----->要訪問磁碟都繞不過OS

----->所以使用OS提供的系統介面來訪問檔案

所以不管語言是什麼底層還是對作業系統的一個系統呼叫介面來進行封裝的,底層只有一個OS

2.1C語言檔案操作

C語言開啟檔案的函式是

fopen(FILE *src,char* used)函式

src是檔案指標,used是要進行的操作方式

其中操作操作方式有

"r"(讀),"w"(寫),"r+"(讀寫,檔案不存在出錯),"w+"(讀寫,檔案不存在建立),"a"(append,加),"a+"()

往檔案裡寫內容是用

fprintf (檔案指標,寫入型別寫入內容);

細節問題:

以w方式開啟檔案,每次呼叫fopen()會把檔案清空,然後再開始讀寫

2.2 系統檔案I/O介面

系統呼叫介面是

#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);

2.2.1 int open(const char *pathname,int flags);


引數:

​ *pathname : 檔案的路徑

​ flags : 標記位


返回值:

​ 失敗返回-1,成功返回檔案描述符(陣列下標)


這裡的flags標記位,利用點陣圖的思想來進行對應不同的操作,然後用宏代表這個資料

例如

#define O_WRONLY (1<<0)		//以只寫方式開啟檔案
#define O_creat (1<<3)		//開啟檔案,如果沒有改檔案,則在當前路徑下建立一個名稱為傳參的檔案
#define O_TRUNE (1<<4)		//每次清空檔案內容
...

判斷哪些位置有1,則執行哪些操作,所以flags傳的是標記位

對於第一個系統呼叫

int open(const char *pathname,int flags);
//eg:
int fd1=open("log.txt",O_WRONLY | O_CREAT);

如果按這種方式來進行檔案訪問,如果沒有改檔案,那麼就建立一個,但是這個函式建立出來的檔案的許可權是亂碼,前提情況是沒有改檔案的時候,才會發生這種情況

所以第一個函式是適合保證有指定的檔案存在是呼叫的

2.2.3 int open(const char *pathname,int flags,mode_t mode);

因為第一個建立的時候沒有許可權,所以這個mode是一個16進位制的預設許可權碼,呼叫的時候傳入的就是要建立檔案的預設許可權碼,然後建立過程需要 &~umask的碼

2.3 open函式返回值

既然檔案操作的本質是 : **程序 **+ 被開啟檔案關係

程序可以開啟多個檔案嗎?,可以------->系統中肯定有著大量的被開啟的檔案------>被開啟的檔案一定要被作業系統管理起來------->如何管理?先描述再組織------->作業系統為了管理被開啟的檔案必定要建立對應的核心資料結構-------> struct file {} ( 包含了檔案的屬性 )

所以有,對於一個程序,在記憶體中,肯定也有其他被開啟的檔案在記憶體中,所以怎麼樣找到屬於自己當前程序的被開啟檔案呢?

① struct file是檔案的結構體,磁碟載入到內容=存中,為每一個檔案建立屬於自己被開啟檔案的結構體struct file ,屬於一個程序的檔案會按屬性用*file利用指標的方式連結起來

②程序都有pcb即task_struct{} ,PCB裡有一個 struct files_struct *files ,這個東西是一個指標,指向一個叫做files_strcut的結構體

files_struct裡包含了一個陣列,這個陣列是 struct file* fd_arrar[] ,這個是一個指標陣列,陣列裡的每個指標即指向了一個①裡每個檔案的結構體,那麼就連起來了,這個陣列的下標就是檔案描述符fd,即open函式的返回值

所以一個透過系統呼叫檔案被開啟,透過磁碟載入到記憶體中,然後建立屬於這個檔案的file結構體,然後再把這個*file指標加入到檔案符描述表中,透過下標來進行系統呼叫,所以檔案描述符本質就是陣列的索引值(下標)

所以C語言的庫函式是透過呼叫系統函式open()

然後open()函式返回fd

然後C函式再去找PCB裡的*file指標

透過fd來訪問對應的被開啟檔案

2.4 fd隱藏的下標

所以被開啟的檔案的fd是從3開始的

3. 重定向

3.1檔案fd的分配規則

已知檔案被程序所開啟,需要找到task_struct{}中的 file_struct *files ,進而找到一個包含*file fd_array[]的結構體,然後根據這個指標陣列找到陣列所對應的下標fd

fd的分配規則:

​ 從下標0開始往後找,找到空位值,填進去指標,指向檔案的file結構體,每個檔案都有一個file結構體

3.2 程序檔案重定向

每個程序一開始都有三個隱藏的被開啟檔案:即

  • stdin 標準輸入
  • stdout 標準輸出
  • stderror 標準錯誤

這三個是每個程序預設有的,所以fd的下標從3開始

這些是系統預設定好的,例如系統只會在1號(stdout)裡列印出來東西,即printf()會往fd為1號的檔案裡打,然後再透過緩衝區列印到顯示器

但是我們如果先把1號的檔案即(stdout)給關閉,然後再建立一個檔案,那麼這個檔案的fd按順序分配就是1了,那麼系統的printf()等輸出則將輸出到這個檔案中了

在這個過程中,我們把fd為1號的指向改變了,就是一個重定向

image-20240306161058390


在以上過程中,每次需要我們手動去關閉1號才能再發生重定向,這種刪除後然後再建立的方式很不友好,有更簡便的如下

有這麼一個函式

int dup2(int **oldfd**,int **newfd** );

能讓我們建立的fd的重定向

eg:

int fd=open(……..);//開啟一個檔案

dup(fd,1);		//將原來1號位置的指向變成了fd指向的檔案

​ 把fd的內容>1的內容 ,1的內容變成fd的

3.3 父子程序之間的重定向

對於一個父程序,開啟子程序後,因為程序擁有獨立性,所以子程序的檔案描述符表不是跟父程序公用的,是跟PCB一樣,按照子程序的模板來建立的

原因 :如果父子程序公用一個檔案描述符表,如果子程序發生了重定向,那麼就會影響父程序,因為程序具有獨立性,所以不能共享一個,但是子程序指向的0 1 2號 fd是指向的同一個

4. 如何理解Linux下一切皆檔案

對於我們用的滑鼠,鍵盤,顯示器,網路卡對應得軟體層是驅動程式

在Linux下,對於所有的檔案,定義了一個strcut file{}的結構體,這個結構體裡有

變數的定義,函式指標的定義,這些函式指標就包括讀函式寫函式,等諸多函式,因為每個檔案的功能不一樣,對應的操作不一樣,以及每個檔案的型別也不一樣,需要的時候就去你對應的驅動等檔案裡找函式的實現,所以這就利用多型的思想

對於Linux系統而言,不管你是什麼檔案,都有著一樣的定義,使用的時候去你檔案裡找對應的實際操作函式,即函式的實現,在strcut file{}中定義了函式定義,所以Linux下一切皆檔案

eg:

struct file{
    int flags;
    int kind;
    char * name;//定義變數
    int (*write)();//寫函式的宣告
    int (*read)();//讀函式的宣告
}

例如:

1.對於一個鍵盤裝置,當作檔案來看,鍵盤只具有讀的操作,不需要寫的操作那麼就把寫函式定義為空的函式,不執行

2.對於一個網路卡,需要讀和寫,所以他的實現則需要寫相應讀函式和寫函式的實現,讓函式指標去指向然後去實現

一切皆檔案

這就多型的思想

5. 緩衝區

5.1 緩衝區的本質

緩衝區的本質就是一段記憶體,緩衝區其實是C語言(或者其他語言來申請的)

當我們向外設進行I/O的時候,向檔案讀取內容或者寫入就是I/O因為外設相對記憶體CPU執行速度較慢,所以便產生了緩衝區的概念,先把輸入/輸出結果放到緩衝區裡,然後再從緩衝區裡讀取,在空閒的時候CPU還能去執行其他的任務

所以緩衝區的出現,大大的節省系統的I/O時間

5.2 緩衝區的重新整理策略

常見的重新整理策略有:

  • 立即重新整理-------無緩衝
  • 行重新整理 --------行緩衝
  • 緩衝區滿-------全緩衝

對於行緩衝是給顯示器用的,因為人的讀取習慣就是按行讀取,所以對於stdout或者顯示器一般採用的是行緩衝

對於全緩衝是給磁碟檔案的,則是對於磁碟讀寫檔案時候的策略

還有兩種重新整理方式

  • 使用者強制重新整理:呼叫fflush()函式
  • 程序退出重新整理緩衝區

5.2 緩衝區的位置

在C語言裡,開啟一個檔案對應的結構體是Struct FILE{},這個FILE裡包含了fd和一個buffer陣列,資料先放入buffer陣列裡,根據不同的重新整理策略在給系統呼叫然後再寫入某些檔案中

所以如果直接用fd進行系統呼叫寫入,不會產生緩衝區,如果使用語言的庫函式呼叫,則會用緩衝區

所以緩衝區不在核心中,而在我們語言結構體裡,隨著結構體的建立,所以buffer的本質就是一段記憶體

stdout,stdin,stderr---->c語言FILE*------>FILE結構體------>fd,.......,一個緩衝區

所以我們可以呼叫C函式fflush() 自主重新整理


6. 檔案系統

6.1 磁碟的物理結構

磁碟是計算機中唯一一個機械結構+外設,所以他的訪問相對較慢一些

個人日常一般用不到磁碟,都被主流的ssd(固態硬碟)所替代了

磁碟現在大部分用於企業級使用者等等

磁碟的組成:由一摞磁片,每個磁片的盤面都有一個磁頭,磁頭往盤面寫內容,磁頭與盤面的距離非常非常近,但是不接觸

寫入磁碟的方法:寫的內容其實就是二進位制,在機械中表達二進位制的方式是帶電和不帶電,所以磁頭可以給磁碟的地方讓他帶電或者不帶即可表達二進位制

6.2 磁碟的儲存結構

6.3 磁碟的邏輯結構

磁碟可以比作一個蚊香或者磁帶,然後我們可以捋直,就是一個線性表的結構了

雖然一個扇區是512位元組,但是依舊很小,OS進行的檔案定製可以同時讀寫多個扇區,1K,2k,3k,4k等

由科學證明得出每次進行4K效率最高

記憶體就被劃分了按4K大的空間-----頁框

可執行程式/檔案就是多個按照4K大小組成的塊-----頁框

那麼怎麼管理整個磁碟呢?

假設一個磁碟空間為500GB,這個空間很大,那麼我們可以紛成多塊,例如5塊

每一塊就是100GB,但是100GB依舊很大,我們可以分為5G每塊,即分成20塊區域

這就是分而治之的思想,我們管好這5GB,其他的區域照這個管理方式去進行即可

對於這5個G,也可以進行區域劃分成多個組

super Block:記錄此 filesystem 的整體資訊,包括inode/block的總量、使用量、剩餘量, 以及檔案系統的格式與相關資訊等;一般有多個,因為需要備份,防止丟失

Group Descriptor Table: 塊組描述表 ----->對應了分組的屬性資訊

Block Bitmap : 記錄使用與未使用的 block 號碼,並在進行檔案新增修改時候對應的修改 block 的使用狀況

inode Bitmap : 記錄使用與未使用的 inode 號碼,並在進行檔案新增修改時候對應的修改 inode 的使用狀況

inode Table (inode表) : 儲存了所有的(使用+未使用)的inode,以及檔案的屬性

Data blocks : 資料塊

(檔案系統分割槽詳解)

檔案=內容+屬性

檔案的內容 在Data blocks裡儲存的

檔案的屬性 在indoe裡儲存的

  • 每個檔案都有一個inode
  • inode為了區分彼此,每個inode有不同的ID
  • 查詢一個檔案的時候統一隻用inode編號查的
  • inode的字首在每個組是不一樣的

那麼怎麼根據inode來查詢對應的檔案呢?

struct inode{
	int ID;
	uid;
    gid;
    size;
    ......
    int bolcks[15];//有一個陣列,這個陣列每個元素指向Data
}

在inode裡有一個陣列,這個陣列每個元素存著對應的Data blocks的塊號,所以按順序往下讀寫

我們可以知道,即使是15個Data block也很小

所以當空間大的時候單純的15個block是不夠的,需要用到多級索引的辦法,在這個陣列從13號位置開始就是指向了一個Data blocks的區域,這個區域裡的內容是一個指向其他Data blocks編號的表,所以可以擴大使用空間,如果空間還不夠那麼就繼續用三級索引,這樣下來,空間其實非常大了

當一個檔案被刪除時候,其實資料沒有被刪除,一個檔案被刪除,那麼他對應的inode bitmap被置為0,inode table變成未使用,其實就是刪除了對映關係,原來的資料塊因為沒有了限制,所以可以被新的檔案當作新的資料塊所使用

對於一個目錄,目錄存放的Data blocks的內容其實就是一個inode和檔名一一對應的對映關係,存放在了目錄下,所以一般查詢檔案是用檔名查詢的,這就是為什麼同一個目錄下不能有相同的檔名

細節問題:其實在電腦開機時候,檔案系統inode的點陣圖等都預載入到記憶體中了,因為刪一個檔案,需要把磁碟對應的inodebitmap置為0,但是我們不能直接與外設打交道,根據馮諾依曼體系,所以都是透過記憶體來打交道的

7. 軟硬連結

軟硬連結的區別:有沒有獨立的 inode

7.1硬連結

  • 建立硬連結:
ln 原始檔名 硬連結檔名

在以上檔案系統中,可以理解:

在磁碟中查詢一個檔案是透過inode的,那麼對於一個目錄下的檔案,這個目錄下存的其實是檔名和inode的對映關係

那麼我們可以建立多個對映關係到同一個inode中,也就是多個對映關係,在這個目錄裡並沒有建立新的檔案以及新的inode,只是在這個目錄的Data block裡建立了一個新的對映關係而已

如上圖就是一個硬連結,兩個檔案的inode是一樣的,其實就是在目錄下建立了一個較hard_link與這個檔案inode的對映關係

  • 硬連結的引用計數 : 可以看到上邊有個數字2,這個就是有多少個檔案連結到了他,就是有多少個對映到了同一個inode,這個引用計數原理跟智慧指標原理類似
  • 當一個檔案被刪除,實際上是當一個inode的引用計數為0時(即不在有對映關係),此時才會在檔案系統中把相應的點陣圖置為0
  • 當一個檔案的硬連結數為0時,這個檔案才被刪除

7.2軟連結

  • 建立軟連結:
ln -s sourceFile LinkName

ln是link的縮寫,-s後邊跟原始檔(sourceFile) ,LinkName代表連結的命名名字

軟連結會產生新的inode即代表他是一個新檔案,這個檔案裡的內容實際上就是軟連結對應的檔名和路徑,就是每次去指定路徑下找到對應的檔名,如果把那個檔案刪掉,那麼軟連結就失效了

  • 斷開軟連結:
unlink 軟連線名稱

相關文章