梅開二度
在 C 語法下就早已知悉基礎 IO ,其實就是耳熟能詳的文件操作,說到文件操作腦子里又是一堆耳熟能詳的函數接口:
以一個簡單的寫入操作為例,運行程序后當前路徑下會生成對應文件,文件當中就是我們寫入的內容:
int main()
{
FILE* fp = fopen("log.txt", "w");
if (fp == NULL){
perror("fopen");
return 1;
}
int count = 5;
while (count){
fputs("hello worldn", fp);
count--;
}
fclose(fp);
return 0;
}
當前路徑
文件操作我們打開文件時,如果 fopen 對象是一個未創建的對象,那么就會自動在當前路徑生成一個該文件,這里就牽涉到一個當前路徑 color{red} {當前路徑}當前路徑的概念。
比如我們在剛剛寫入后的 log.txt 文件進行讀?。?/p>
int main()
{
FILE* fp = fopen("log.txt", "r");
if (fp == NULL){
perror("fopen");
return 1;
}
char buffer[64];
for (int i = 0; i < 5; i++){
fgets(buffer, sizeof(buffer), fp);
printf("%s", buffer);
}
fclose(fp);
return 0;
}
該情況下,我們在總目錄下運行可執行程序 myproc,那么該可執行程序創建的 log.txt 文件會出現在總目錄下:
這是否意味著 “當前路徑” 就是指的 “當前可執行程序所處的路徑”?
我們不妨直接去查看他的路徑對吧,我們用ps -axj | head -1&&ps -axj | grep myproc | grep -v grep可以查看可執行程序的 PID :
然后我們再利用 PID 來查看執行路徑sudo ls /proc/8189 -al,因為我在總目錄 ~ 下,因此這里我使用弄了 sudo 命令進行管理員權限查找:
這里的cwd和exe是軟鏈接,我們下文細談,所以實際上,當前路徑不是指可執行程序所處的路徑,而是指該可執行程序運行成為進程時我們所處的路徑
三大輸入輸出流
我們一直貫徹一個理念就是 Linux 下 一切皆文件,我們肉眼可見的顯示屏輸出的數據,本質是電腦讀取鍵入的字符,電腦從“電腦文件” 讀取字符,電腦再對“顯示器文件”進行輸出
那么問題來了,在我們對這些“文件”進行讀寫之前,為什么我們沒有一個文件打開的操作呢?
要知道打開文件一定是進程運行的時候打開的,而任何進程在運行的時候都會默認打開三個輸入輸出流,即標準輸入流、標準輸出流、標準錯誤流,就是 C 當中的 stdin、stdout、stderr;C++當中的 cin、cout、cerr,其他所有語言都有類似的概念。實際上這種特性并不是某種語言所特有的,而是由操作系統所支持的
其中,標準輸入流對應的設備就是鍵盤,標準輸出流和標準錯誤流對應的設備都是顯示器。查看 man 手冊我們不難發現,stdin、stdout、stderr 這仨 byd 其實就是 FILE* 類型的
extern FILE *stdout;
extern FILE *stderr;
我們之所以可以調用 scanf 、printf 這類的函數向鍵盤顯示器進行輸入輸出操作,其實就是程序運行時,操作系統默認使用 C 的接口將這三個輸入輸出流打開。試想我們使用 fputs 函數時,將其第二個參數設置為 stdout,此時 fputs 函數會不會直接將數據顯示到顯示器上呢?
答案是肯定的,因為此時就是用 fputs 向顯示器文件進行了寫入操作
系統文件 I/O
相比 C,C++ 這些語言的接口,操作系統也有一套文件操作的接口,而且操作系統的接口更加貼近底層,而其他語言的接口本質上也是對操作系統的接口的封裝,我們在 Linux、Windows 平臺下運行 C 代碼時,C 庫函數就是對 Linux、Windows 系統調用接口進行的封裝,這樣做使得語言有了跨平臺性,也方便進行二次開發
open
函數原型:
1.pathname 表示要打開或創建的目標文件。
若pathname以路徑的方式給出,則當需要創建該文件時,就在pathname路徑下進行創建。 若pathname以文件名的方式給出,則當需要創建該文件時,默認在當前路徑下進行創建,注意當前路徑的含義
2.flags 表示打開文件的方式。
flags 的可調用參數有如下這些:
flags 可以同時傳入多個參數選項,這些選項用 “或” 運算符連接。例如以只寫的方式打開文件時,文件不存在就應該自動創建文件,則參數設置如下
我們基于與運算的最根本原因是因為: 這些宏定義選項的共同點就是它們的二進制序列當中有且只有一個比特位是 1 color{red} {這些宏定義選項的共同點就是它們的二進制序列當中有且只有一個比特位是 1}這些宏定義選項的共同點就是它們的二進制序列當中有且只有一個比特位是1,除了 O_RDONLY 序列為全 0,表示他為默認選項,且為 1 的比特位是各不相同的,這樣一來函數內部就可以通過使用與運算來判斷是否設置了某一選項
if (arg2&O_RDONLY){
//設置了O_RDONLY選項
}
if (arg2&O_WRONLY){
//設置了O_WRONLY選項
}
if (arg2&O_RDWR){
//設置了O_RDWR選項
}
if (arg2&O_CREAT){
//設置了O_CREAT選項
}
//...
}
3.mode,表示創建文件的默認權限,在不創建文件時,此選項可以不設置。
我們將mode設置為 0666,則文件創建出來的權限如下,按理說本來應該是 :
但是不要忘了,Linux 系統設有 umask 權限掩碼,文件的真正權限計算方法是:mode &( ~umask),umask 的默認值應該是 0002,所以在我們自己設置的權限下應該減去 umask 得到 0664,即:
當然,如果想繞開 umask ,直接使用我們第一手的設置,那么我們可以直接將 umask 進行置 0 操作
open 返回值
open 的返回值其實是新打開文件的文件描述符 fd,我們這里嘗試一次打開多個文件,然后分別打印它們的文件描述符:
#include
#include
#include
int main()
{
umask(0);
int fd1 = open("log1.txt", O_RDONLY | O_CREAT, 0666);
int fd2 = open("log2.txt", O_RDONLY | O_CREAT, 0666);
int fd3 = open("log3.txt", O_RDONLY | O_CREAT, 0666);
int fd4 = open("log4.txt", O_RDONLY | O_CREAT, 0666);
int fd5 = open("log5.txt", O_RDONLY | O_CREAT, 0666);
printf("fd1:%dn", fd1);
printf("fd2:%dn", fd2);
printf("fd3:%dn", fd3);
printf("fd4:%dn", fd4);
printf("fd5:%dn", fd5);
return 0;
}
我們又知道系統是無法打開一個不存在的文件 fd 會返回 -1,打開成功時如圖所示每個文件的 fd 從 3 開始且都是連續遞增的,那么問題來了:0~2 哪里去了?
所謂的文件描述符本質上是一個指針數組的下標,指針數組當中的每一個指針都指向一個被打開文件的文件信息,通過對應文件的文件描述符就可以找到對應的文件信息
open函數打開文件成功時數組當中的指針個數增加,然后返回該指針在數組中的下標,而當文件打開失敗時直接返回 -1,因此,成功打開多個文件時所獲得的文件描述符就是連續且遞增的
而 Linux 進程默認情況下會有 3 個缺省打開的文件描述符,分別就是標準輸入0、標準輸出1、標準錯誤2,這就是為什么成功打開文件時所得到的文件描述符會從3開始
close
系統接口中使用close函數關閉文件,close函數的函數原型如下:
若關閉文件成功則返回 0,若關閉文件失敗則返回 -1
write
系統接口中使用write函數向文件寫入信息,write函數的函數原型如下:
write函數將 buf 位置開始向后 count 字節的數據寫入文件描述符為 fd 的文件當中;如果數據寫入成功,返回寫入數據的字節個數,如果數據寫入失敗,返回 -1。
read
系統接口中使用read函數從文件讀取信息,read函數的函數原型如下:
read 函數從文件描述符為 fd 的文件讀取 count 字節的數據到 buf 位置當中。如果數據讀取成功,實際讀取數據的字節個數被返回;如果數據讀取失敗,返回 -1
文件描述符fd
我們知道文件只能在進程執行時才能打開,且一個進程可打開多個文件,系統中存在大量的進程,這就表示系統可以在任何時刻存在大量已經打開的文件
Linux 思想面對批量的處理時總會采取 “先描述后組織” 的思想,系統會為大量的文件描述一個 file struct 的結構體,里面存放著這些文件的主要信息,然后將結構體以雙鏈表的形式進行組織,相當于將文件的管理具象成對雙鏈表的增刪查改。
但是在大量進程和大量已打開的文件里,我們要找到每個文件的歸屬進程系統就應該建立對應關系
對應關系
當一個程序運行起來時,操作系統會將該程序的代碼和數據加載到內存,然后為其創建對應的task_struct、mm_struct、頁表等相關的數據結構,并通過頁表建立虛擬內存和物理內存之間的映射關系
首先在 task_struct 里有一個指針,他指向一個名為 file_struct 的結構體,在這個結構體里面又有一個 fd_array 的指針數組,這個數組的下標就是我們所謂的文件描述符。比如進程打開 log.txt 時會先加載進內存形成 struct file ,然后將 struct file 放入一個文件的雙鏈表里,struct file 的首地址再放入鏈表中下標為 3 處的地方,最后返回他的文件描述符即可。
向文件寫入數據時,是先將數據寫入到對應文件的緩沖區當中,然后定期將緩沖區數據刷新,數據才能進入磁盤。
那么為什么進程創建時會默認打開0、1、2 呢?
我們知道操作系統能夠識別硬件,操作系統能管理硬件也意味著鍵盤,顯示器這些東西都有自己對應的 struct_file ,將這 3 個 struct_file 放入雙鏈表,就會對應填入到下標為 0,1,2 的位置,就默認打開了標準輸入流、輸出流、錯誤流。
內存文件
磁盤文件和內存文件之間的關系就像程序和進程的關系一樣,當程序運行起來后便成了進程,而當磁盤文件加載到內存后便成了內存文件。
磁盤文件分為了文件內容和文件屬性兩部分,也將文件屬性叫做元信息,文件加載到內存時,一般先加載文件的屬性信息,當需要對文件內容進行讀取、輸入或輸出等操作時,再加載文件數據
分配規則
我們還是用最開始的代碼做解釋:
#include
#include
#include
int main()
{
umask(0);
int fd1 = open("log1.txt", O_RDONLY | O_CREAT, 0666);
int fd2 = open("log2.txt", O_RDONLY | O_CREAT, 0666);
int fd3 = open("log3.txt", O_RDONLY | O_CREAT, 0666);
int fd4 = open("log4.txt", O_RDONLY | O_CREAT, 0666);
int fd5 = open("log5.txt", O_RDONLY | O_CREAT, 0666);
printf("fd1:%dn", fd1);
printf("fd2:%dn", fd2);
printf("fd3:%dn", fd3);
printf("fd4:%dn", fd4);
printf("fd5:%dn", fd5);
return 0;
}
然而文件描述符是從最小的 0 開始且未被分配的開始分配的,比如我關閉了 0,2 的流,那么新打開 3 個文件就是不是 3,4 5 而是 0,2,3 了
close(2);//關閉描述符為 0,2 的文件
重定向
原理
到這里其實不難理解重定向的原理是修改文件描述符下標對應的 struct file* 內容,比如我們說過的輸出重定向就是將一個本應該輸出到一個文件的數據輸出到另一個文件
比如想讓本應該輸出到顯示器的數據輸出到 log.txt 文件當中,那么可以在打開 log.txt 文件之前將文件描述符為 1 的文件關閉,也就是將“顯示器文件”關閉,這樣一來,當我們后續打開 log.txt 文件時所分配到的文件描述符就是 1
#include
#include
#include
#include
int main()
{
close(1);
int fd = open("log.txt", O_WRONLY | O_CREAT, 0666);//輸出重定向
if (fd < 0){
perror("open");
return 1;
}
printf("hello worldn");
printf("hello worldn");
printf("hello worldn");
printf("hello worldn");
printf("hello worldn");
fflush(stdout);
close(fd);
return 0;
}
這里 printf 是默認向 stdout 輸出數據的,而 stdout 指向的 FILE 結構體中存儲的文件描述符就是1,因此 printf 實際上就是向文件描述符為1的文件輸出數據。C 的數據并不是立馬寫到了內存操作系統里面,而是寫到了緩沖區當中,所以使用 printf 打印完后需要使用 fflush 將緩沖區當中的數據刷新到文件中
可以看出,我執行 file 程序時并沒有任何結果,但是打印 log.txt 時卻得到了我想要的結果,因此就證明了上面的觀點:
但是又有一個問題:標準輸出流和標準錯誤流對應的都是顯示器,它們有什么區別嗎?
答案是有的, 我們以代碼為例:
int main()
{
printf("hello printfn"); //stdout
perror("perror"); //stderr
fprintf(stdout, "stdout:hello fprintfn"); //stdout
fprintf(stderr, "stderr:hello fprintfn"); //stderr
return 0;
}
結果一定會成功的輸出四行內容,然后再對他進行重定向到 log.txt 中:
很明顯這里 log.txt 文件當中只有向標準輸出流輸出的兩行字符串,而向標準錯誤流輸出的兩行數據并沒有重定向到文件當中,而是仍然輸出到了顯示器上。實際上我們使用重定向時,是對輸出流進行了重定向,而對錯誤流無影響 color{red} {實際上我們使用重定向時,是對輸出流進行了重定向,而對錯誤流無影響}實際上我們使用重定向時,是對輸出流進行了重定向,而對錯誤流無影響。
dup2
要完成重定向我們只需對 fd_array 數組當中元素的拷貝即可,Linux 中對于重定向給出了一個接口:==dup2 ==,我們可以使用這個接口完成重定向:
dup2 會將 fd_array[oldfd] 的內容拷貝到 fd_array[newfd] 當中,如果有必要的話我們需要先使用關閉文件描述符為 newfd 的文件,dup2 函數返回值如果調用成功返回 newfd,否則返回 -1。
需要的是:
- 如果 oldfd 不是有效的文件描述符,則 dup2 調用失敗,并且此時文件描述符為 newfd 的文件沒有被關閉
- oldfd 是一個有效的文件描述符,但是 newfd 和 oldfd 具有相同的值,則 dup2 不做任何操作,并返回 newfd
比如:
#include
#include
#include
#include
int main()
{
int fd = open("log.txt", O_WRONLY | O_CREAT, 0666);
if (fd < 0){
perror("open");
return 1;
}
close(1);
dup2(fd, 1);
printf("hello printfn");
fprintf(stdout, "hello fprintfn");
return 0;
}
就像這樣,數據會被傳到 log.txt 里面。
重定向模擬實現
在我們自己實現 shell 的基礎上,是可以自己實現重定向功能的。對于獲取到的命令進行判斷,若命令當中包含重定向符號 >、>> 或是 <,則該命令需要進行處理
設置 type 變量,type 為 0 表示命令當中為輸出重定向,type 為 1 表示追加重定向,type為 2 表示輸入重定向。若 type 值為 0 或者 1,則使用 dup2 接口實現目標文件與標準輸出流的重定向;若 type 值為 2,則使用 dup2 接口實現目標文件與標準輸入流的重定向
#include
#include
#include
#include
#include
#include
#include
#include
#define LEN 1024 //命令最大長度
#define NUM 32 //命令拆分后的最大個數
int main()
{
int type = 0; //0 >, 1 >>, 2 <
char cmd[LEN]; //存儲命令
char* myargv[NUM]; //存儲命令拆分后的結果
char hostname[32]; //主機名
char pwd[128]; //當前目錄
while (1){
//獲取命令提示信息
struct passwd* pass = getpwuid(getuid());
gethostname(hostname, sizeof(hostname)-1);
getcwd(pwd, sizeof(pwd)-1);
int len = strlen(pwd);
char* p = pwd + len - 1;
while (*p != '/'){
p--;
}
p++;
//打印命令提示信息
printf("[%s@%s %s]$ ", pass->pw_name, hostname, p);
//讀取命令
fgets(cmd, LEN, stdin);
cmd[strlen(cmd) - 1] = '?';
//實現重定向功能
char* start = cmd;
while (*start != '?'){
if (*start == '>'){
type = 0; //遇到一個'>',輸出重定向
*start = '?';
start++;
if (*start == '>'){
type = 1; //遇到第二個'>',追加重定向
start++;
}
break;
}
if (*start == '<'){
type = 2; //遇到'<',輸入重定向
*start = '?';
start++;
break;
}
start++;
}
if (*start != '?'){ //start位置不為'?',說明命令包含重定向內容
while (isspace(*start)) //跳過重定向符號后面的空格
start++;
}
else{
start = NULL; //start設置為NULL,標識命令當中不含重定向內容
}
//拆分命令
myargv[0] = strtok(cmd, " ");
int i = 1;
while (myargv[i] = strtok(NULL, " ")){
i++;
}
pid_t id = fork(); //創建子進程執行命令
if (id == 0){
//child
if (start != NULL){
if (type == 0){ //輸出重定向
int fd = open(start, O_WRONLY | O_CREAT | O_TRUNC, 0664); //以寫的方式打開文件(清空原文件內容)
if (fd < 0){
error("open");
exit(2);
}
close(1);
dup2(fd, 1); //重定向
}
else if (type == 1){ //追加重定向
int fd = open(start, O_WRONLY | O_APPEND | O_CREAT, 0664); //以追加的方式打開文件
if (fd < 0){
perror("open");
exit(2);
}
close(1);
dup2(fd, 1); //重定向
}
else{ //輸入重定向
int fd = open(start, O_RDONLY); //以讀的方式打開文件
if (fd < 0){
perror("open");
exit(2);
}
close(0);
dup2(fd, 0); //重定向
}
}
execvp(myargv[0], myargv); //child進行程序替換
exit(1); //替換失敗的退出碼設置為1
}
//shell
int status = 0;
pid_t ret = waitpid(id, &status, 0); //shell等待child退出
if (ret > 0){
printf("exit code:%dn", WEXITSTATUS(status)); //打印child的退出碼
}
}
return 0;
}
效果如圖:
FILE 的文件描述符
訪問文件的本質都是通過文件描述符進行訪問的,而且庫函數又是對系統接口的封裝,所以在庫函數中的 FILE 結構體也必定存在文件描述符 fd
我們在 / u s r / i n c l u d e / s t d i o . h color{red} {/usr/include/stdio.h}/usr/include/stdio.h 頭文件中可以看到下面這句代碼,也就是說 FILE 實際上就是struct _IO_FILE 結構體的一個別名。
接下來轉到 struct _IO_FILE 結構體的定義,其中我們可以看到一個名為_fileno的成員,這個成員實際上就是封裝的文件描述符
int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags
//緩沖區相關 /* The following pointers correspond to the C++ streambuf
protocol. / / Note: Tk uses the _IO_read_ptr and _IO_read_end
fields directly. / char _IO_read_ptr; /* Current read pointer /
char _IO_read_end; /* End of get area. / char _IO_read_base;
/* Start of putback+get area. / char _IO_write_base; /* Start of
put area. / char _IO_write_ptr; /* Current put pointer. / char
_IO_write_end; /* End of put area. / char _IO_buf_base; /* Start of reserve area. / char _IO_buf_end; /* End of reserve area. /
/ The following fields are used to support backing up and undo. */
char _IO_save_base; / Pointer to start of non-current get area. */
char _IO_backup_base; / Pointer to first valid character of backup
area */ char _IO_save_end; / Pointer to end of non-current get
area. */
struct _IO_marker *_markers;
struct _IO_FILE *_chain;
int _fileno; //封裝的文件描述符
#if 0 int _blksize;
#else int _flags2;
#endif _IO_off_t _old_offset; /* This used to be _offset but it’s too small. */
#define __HAVE_COLUMN /* temporary / / 1+column number of pbase(); 0 is unknown. */ unsigned short _cur_column; signed char
_vtable_offset; char _shortbuf[1];
/* char* _save_gptr; char* _save_egptr; */
_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE };
那我們再來聊聊文件函數的底層:
fopen 函數會為用戶在上層調申請 FILE 結構體,返回 FILE* 結構體指針,底層上調用 open 函數獲取文件的 fd,并將 fd 交給 _fileno 填充,這樣就完成了文件的打開操作。其他的比如 fread、fwrite、fputs、fgets ,都會先根據我們傳入的文件指針找到對應的FILE結構體,然后在FILE結構體當中找到文件描述符,最后通過文件描述符對文件進行的一系列操作
我們以三種輸出函數為例:
#include
int main()
{
//c
printf("hello printfn");
fputs("hello fputsn", stdout);
//system
write(1, "hello writen", 12);
fork();
return 0;
}
看到這結果是不是覺得淦!好怪。按照代碼邏輯的話,這里應該只會打印出三個句子對應三個函數,但是為什么這里有兩個函數出現了兩次呢?
不難發現,這里重復的兩個函數都是 C 庫函數,我們就要牽扯到三種緩沖方式了:
無緩沖
行緩沖(對顯示器進行刷新數據)
全緩沖(對磁盤文件寫入數據)
直接執行可執行程序,將數據打印到顯示器時所采用的就是行緩沖,因為代碼當中每句話后面都有 n,所以當我們執行完對應代碼后就立即將數據刷新到了顯示器上
如果將運行結果重定向到 log.txt 文件時,數據的刷新策略就變為了全緩沖,此時使用 printf 和 fputs 打印的數據都打印到了C語言自帶的緩沖區當中,之后 fork 創建子進程時,由于進程間具有獨立性,而之后當父進程或是子進程對要刷新緩沖區內容時,本質就是對父子進程共享的數據進行了修改,此時就需要對數據進行寫時拷貝,至此緩沖區當中的數據就變成了兩份,一份父進程的,一份子進程的,所以重定向到 log.txt 文件當中 printf 和 puts 函數打印的數據就有兩份。但由于 write 是系統接口,我們可以將 write 看作是沒有緩沖區的,因此 write 打印的數據就只打印了一份
這個緩沖區是誰提供的?
他是 C 自帶的,如果說這個緩沖區是操作系統提供的,那么 printf、fputs 和 write 打印的數據重定向到文件后都應該打印兩次
這個緩沖區在哪?
printf 是將數據打印到 stdout 里面,而 stdout 就是一個 FILE* 指針,在 FILE 結構體當中還有一大部分成員是用于記錄緩沖區相關的信息的,我們來看看底層代碼:
/* The following pointers correspond to the C++ streambuf protocol. /
/ Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. /
char _IO_read_ptr; /* Current read pointer /
char _IO_read_end; /* End of get area. /
char _IO_read_base; /* Start of putback+get area. /
char _IO_write_base; /* Start of put area. /
char _IO_write_ptr; /* Current put pointer. /
char _IO_write_end; /* End of put area. /
char _IO_buf_base; /* Start of reserve area. /
char _IO_buf_end; /* End of reserve area. /
/ The following fields are used to support backing up and undo. */
char _IO_save_base; / Pointer to start of non-current get area. */
char _IO_backup_base; / Pointer to first valid character of backup area */
char _IO_save_end; / Pointer to end of non-current get area. */
抗疫知道這里緩沖區是由 C 提供,在 FILE 結構體當中進行維護,FILE 結構體當中不僅保存了對應文件的文件描述符還保存了用戶緩沖區的相關信息
操作系統有緩沖區嗎?
答案是一定的,其實我們數據并不是直接刷新到顯示器和磁盤上的,而是先刷新到操作系統的緩沖區里面,再經過緩沖區加載到顯示器和磁盤上,當然這里我們先不關心操作系統的刷新策略。
因為操作系統是進行軟硬件資源管理的軟件,所以要將數據刷新到具體外設硬件上,就必須要經過操作系統,看一下層狀結構圖也許會更清楚:
inode
磁盤文件由兩部分構成,分別是文件內容和文件屬性。比如文件名、文件大小以及文件創建時間等信息都是文件屬性,文件屬性又被稱為元信息
在命令行當中輸入ls -l,即可顯示當前目錄下各文件的屬性信息,各種文件屬性排列如下:
在 Linux 操作系統中,文件的元信息和內容是分離存儲的,其中保存元信息的結構稱之為 i n o d e color{red} {其中保存元信息的結構稱之為 inode}其中保存元信息的結構稱之為inode,因為系統當中可能存在大量的文件,所以我們需要給每個文件的屬性搞一個唯一的編號,即 inode 號
也就是說,inode 是一個文件的屬性集合,Linux 中幾乎每個文件都有一個 inode,為了區分系統當中大量的 inode,我們為每個 inode 設置了 inode 編號,ls -i 命令即可查看當前目錄下的文件和他的 inode 編號:
無論是文件內容還是文件屬性,他們都是存儲在磁盤里面的
磁盤
盤是一種永久性存儲介質,在計算機中,磁盤幾乎是唯一的機械設備。與磁盤相對應的就是內存,內存是掉電易失存儲介質,目前所有的普通文件都是在磁盤中存儲的,磁盤在馮諾依曼體系結構當中既可以充當輸入設備,又可以充當輸出設備:
尋址方案
對磁盤讀寫時,一般有以下 3 個步驟:
確定讀寫信息的盤面
確定讀寫信息的柱面
確定讀寫信息的扇區
分區與存儲介質
要理解文件系統,我們必須要先將磁盤結構理解為線性的存儲介質,比如說小學英語的磁帶,你扯出磁帶條條時有沒有想過復讀機讀取磁帶的信息,放完重來必要做倒帶的操作才能從頭開始,這就非常貼切線性結構了。
磁盤分區
磁盤也被稱為塊設備,以扇區為單位,一個扇區的大小通常為512字節。如果以大小為 512G 的磁盤為例,該磁盤就可被分為十億多個扇區:
計為了更好的管理磁盤,磁盤進行了分區,原理類似于將整個國家劃分為省市區縣進行管理,使用分區編輯器在磁盤上劃分幾個邏輯部分,盤片一旦劃分成數個分區,不同的目錄與文件就可以存儲進不同的分區,分區越多,文件的性質區分越細,Windows 磁盤就被分為 C 盤和 D 盤
Linux 也是可以查看文件的分區信息:
格式化
磁盤分區完成后就會進行格式化,格式化后每個分區 inode 數就會被確定下來,所以說格式化是對分區進行初始化的一種操作,會導致所有資源被清除,本質上是對分區后各個區域寫入管理信息
其中的管理信息內容是由文件系統決定的,不同的文件系統格式化時管理信息是不同的,常見的文件系統有 EXT2、EXT3、XFS、NTFS 等
EXT2 存儲方案
而對于每一個分區來說,分區的頭部有一個啟動塊(Boot Block),對于該分區的其余區域,EXT2 文件系統會根據分區大小劃分為一個個的塊組(Block Group)
啟動塊的大小是確定的,而塊組的大小是由格式化的時候確定的,并且不可以更改。
其次,每個組塊都有著相同的組成結構,每個組塊都由超級塊(Super Block)、塊組描述符表(Group Descriptor Table)、塊位圖(Block Bitmap)、inode位圖(inode Bitmap)、inode表(inode Table)以及數據表(Data Block)組成:
Super Block: 存放文件系統本身的結構信息。主要有:Data Block和inode的總量、未使用的Data Block和inode的數量、一個Data Block和inode的大小、最近一次掛載時間。Super Block的信息被破壞,可以說整個文件系統結構就被破壞了
Group Descriptor Table: 塊組描述符表,描述該分區當中塊組的屬性信息
Block Bitmap: 塊位圖中記錄著 Data Block 中哪個數據塊已經被占用或沒有被占用
inode Bitmap: inode 位圖中記錄著每個inode 是否空閑可用
inode Table: 文件屬性,即每個文件的inode。
Data Blocks: 文件內容
因為 super block 極為重要,所以一般在其他塊組中會存在冗余,方便損壞后拷貝恢復
此時我們就可以理解文件創建了:
- 先通過遍歷 inode 位圖找到一個空閑的 inode
- 再在 inode 表當中找到對應的 inode,并將文件的屬性信息填充進 inode 結構中。
- 將該文件的文件名和inode指針添加到目錄文件的數據塊中
文件寫入也是同理:
- 通過 inode 編號找到對應的 inode 結構
- 通過 inode 結構找到存儲該文件內容的數據塊,并將數據寫入數據塊
- 若不存在數據塊或申請的數據塊已被寫滿,則通過遍歷塊位圖的方式找到一個空閑的塊號,并在數據區當中找到對應的空閑塊,再將數據寫入數據塊,最后還需要建立數據塊和 inode 結構的對應關系(對應關系是通過數組進行維護的,該數組一般可以存儲 15 個元素,其中前 12 個元素分別對應文件使用的 12 個數據塊,剩余的三個元素分別是一級索引、二級索引和三級索引,當該文件使用數據塊的個數超過12個時,可以用這三個索引進行數據塊擴充)
文件刪除也是同理:
其實刪除并不會真正將文件信息刪除,而只是將其 inode 和數據塊號置為無效,所以刪除文件后短時間內是可以恢復的。
短時間內是個什么意思呢,因為文件對應的 inode 號和數據塊號被置為了無效,后續創建其他文件或是對其他文件進行寫入操作申請 inode 號和數據塊號時,可能會將該無效了的 inode號和數據塊號分配出去,此時刪除文件的數據就會被覆蓋,也就無法恢復文件了
這也就就是了為什么拷貝文件的時候很慢,而刪除文件的時候很快
文件目錄也是同理:
Linux下一切皆文件,目錄當然也會被看作為文件。目錄有自己的屬性信息,他 inode 結構中存儲的是目錄的屬性信息,比如目錄的大小、目錄的擁有者等;目錄的數據塊當中存儲的就是該目錄下的文件名以及對應文件的 inode 指針。
注意: 文件名并沒有存儲在自己的 inode 結構當中,而是存儲在該文件所處目錄文件的文件內容當中。因為系統并不關心文件名,他只關心文件的 inode ,而文件名和 inode 指針存儲在其目錄文件的文件內容當中后,目錄通過文件名和文件的 inode 指針即可將文件名和文件內容及其屬性連接起來
軟鏈接
文件軟鏈接的創建可以通過這個命令:
效果如下:
我們可以通過ls -i可以看到軟鏈接的 inode 與源文件的 inode 是不同的,并且軟鏈接的大小比源文件的大小要小得多!
刪除源文件后軟鏈接文件不能獨立存在,雖然仍保留文件名,但卻不能執行或是查看軟鏈接的內容
硬鏈接
文件硬鏈接的創建可以通過這個命令:
效果如下:
我們可以通過ls -i可以看到硬鏈接的 inode 與源文件的 inode 是相同的,并且硬鏈接文件的大小與源文件的大小也是相同的,特別注意的是,當創建了一個硬鏈接文件后,該硬鏈接文件和源文件的硬鏈接數都變成了 2
所以硬鏈接文件就是源文件的一個別名 color{red} {源文件的一個別名}源文件的一個別名,一個文件有幾個文件名,該文件的硬鏈接數就是幾,這里 inode 為 659031 的文件有 myproc 和 myproc-h 兩個文件名,因此該文件的硬鏈接數為 2
與軟連接不同的是,當硬鏈接的源文件被刪除后,硬鏈接文件仍能正常執行,只是文件的鏈接數減少了一個,但是硬鏈接可以同步修改多個不在或者同在一個目錄下的文件名,其中一個修改后,所有與其有硬鏈接的文件都會一起被修改
-
接口
+關注
關注
33文章
8650瀏覽量
151411 -
Linux
+關注
關注
87文章
11322瀏覽量
209864 -
程序
+關注
關注
117文章
3792瀏覽量
81167 -
文件系統
+關注
關注
0文章
287瀏覽量
19929 -
函數
+關注
關注
3文章
4338瀏覽量
62761
發布評論請先 登錄
相關推薦
評論