基礎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號的指向改變了,就是一個重定向
在以上過程中,每次需要我們手動去關閉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 軟連線名稱