一些前置知識:
文件 = 屬性 + 內容
文件 分為 打開的文件、未打開的文件
打開的文件:由進程打開,本質是 進程與文件 的關系;維護的文件對象先加載文件屬性,文件內容一般按需加載
未打開的文件:在永久性存儲介質 —— 磁盤上;也需要被管理
C語言文件接口
fopen
打開文件并指定打開方式,返回 FILE* 文件對象指針
r:只讀
w:寫入、但在寫入之前會清空文件;? > 重定向也是使用 w 方法寫入文件
a:追加,在文件結尾寫入; >> 追加方法
fwrite
向已打開的 FILE 文件對象寫入 size 字節的內容
stdin、stdout、stderr
C程序默認啟動時,會打開 3 個標準輸入輸出流,分別是 鍵盤文件 和 2 個顯示器文件
fprintf
向指定文件對象 中寫入
文件相關系統調用接口
未打開的文件存儲在 磁盤上,磁盤是 外設,訪問磁盤上的文件 —— 相當于在 訪問硬件;
幾乎所有的庫,只要是 訪問硬件設備相關,必定封裝了系統調用
因為 操作系統 通過 硬件驅動 管理硬件,并向上層提供 系統調用接口;
open
參數解讀:
pathname:文件路徑
int flags:系統提供了設置好的多個 宏 標志位,可以 通過 位或運算 進行組合,以實現不同的 文件打開方法(bit 位級別的 標志位傳遞方式、有點類似位圖了)
mode:以 8 進制的方式 設置被打開文件的權限,但還要經過 umask 權限掩碼的計算( 最終權限 = 起始權限 & (~umask) )
返回值:一個較小的非負整數,文件描述符 file descriptor
int fd = open("test.txt", O_WRONLY | O_CREAT, 0666);
if( fd < 0 )
{perror("open error");return 1;
}
close
int fd = open("test.txt", O_WRONLY | O_CREAT, 0666);
if( fd < 0 )
{perror("open error");return 1;
}close(fd);
write
int fd = open("logggggg.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
int size_write = write(fd, "aaaaaaa", 3);
close(fd);
默認從文件開始寫,但不會清空原有內容,而是覆蓋
小結
C文件相關庫函數 封裝了系統調用 open、write、close
C庫中使用 自定義類型 FILE?類型的文件對象描述打開的文件,而系統調用中 使用 文件描述符 fd
不論什么語言,底層都是這種 封裝系統調用的 模式,向用戶提供 庫函數,以便二次開發
文件描述符 fd
在 Linux 中,文件描述符 就是一個 小整數
當磁盤文件被加載到內存,內核為該進程創建 PCB,接著創建文件描述符表 struct files_struct,其中有 存放文件對象指針的 指針數組 struct file* fd_array[ ] ,接著打開 3 個默認的標準輸入輸出流 stdin、stdout、stderr,接著創建一個 struct file 自定義數據結構 描述被打開文件的屬性;如果一個進程打開多個文件,那就創建多個文件對象 struct file,用 雙鏈表指針 互相組織起來 —— 對文件的操作就變成了對該文件對象的 增刪改查
通過從進程創建 到 打開文件捋一遍之后,文件描述符 fd 就是 文件描述符表 中 指針數組的 下標。
既然已經有文件描述符表來組織管理 打開的文件對象 struct file,那各個 struct file 還用雙鏈表結構組織是否多此一舉?
非也,考慮了如果進程崩掉了之類的意外情況。
通過 文件描述符表 結構,將 內核中的 進程管理模塊 與 文件管理模塊 解耦。
注意:除了 內核 默認打開的 3 個標準輸入輸出流文件,其余打開文件對象的指針 struct file* 依次按照文件描述符表的 空閑的 元素位置 進行分配。
用 系統調用 操作 fd
例 read:從文件中讀取 count 字節數據
char buff[100];
ssize_t s = read(0, buff, sizeof(buff));
buff[s] = '\0';
printf("echo : %s\n", buff);
從文件描述符為 0 的文件中讀取 sizeof(buff) 大小的數據,并寫入 buff 開始的緩沖區中:
根據運行結果,回車也被寫入,因為此時 bash 內的緩沖區為 行緩沖方式(見后文描述)
小結
為什么默認打開 stdin、stdout、stderr?
因為 操作系統啟動時,鍵盤、顯示器就已經被 操作系統 識別并打開了,在 其他進程被創建時,只需要將這三個 已經被打開的 文件對象struct file 的地址 填進 文件描述符表 中,使用就 ok。
Linux 中的文件相關 系統調用 只認文件描述符,不同于 C 庫中封裝的自定義類型 FILE(但 FILE 中一定封裝了 文件描述符)
stdout 和 stderr 都指向顯示器文件;
一個文件對象 struct file 可以被多個進程使用,例如 3 個默認的 stdin、stdout、stderr 可以被多個進程同時使用;所以 struct file 中會包含一個屬性:引用計數
引用計數:記錄有幾個文件描述符 指向自己;調用 close 會使該文件對象的 引用計數 減 1,并且將 這個進程的 文件描述符表 中的 對應文件描述符下標 的指針數組 的元素 置空,以供其他打開的文件使用; 當被 close 的文件對象的引用計數為 1時,調用 close 會釋放掉這個 文件對象。
重定向
本質:對進程的 文件描述符表 中的地址 進行內核級別的 拷貝。
文件描述符的分配規則:從文件描述符表的 0 下標開始,尋找空閑位置存放 新文件的 文件對象指針。
輸出重定向
示例:將 本來寫入顯示器文件(文件描述符 1)的內容,寫入 文件描述符為 5 的文件
操作系統提供了 系統調用接口,主要介紹 dup2 :完成 拷貝文件描述符下標對應的 文件對象指針 的工作
int fd = open("fdddddddd.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666 );const char* str = "hello world forever!";//dup2(1, fd);dup2(fd, 1);
write(1, str, strlen(str));
追加重定向
與輸出重定向一樣,只是打開文件時 的 flags 參數,再按位或 O_APPEND,就是向文件中追加內容,而不是 清空掉原文件內容 再寫入
int fd = open("fdddddddd.txt", O_WRONLY | O_CREAT | O_APPEND, 0666 );const char* str = "hello world forever!";//dup2(1, fd);dup2(fd, 1);
write(1, str, strlen(str));
輸入重定向
示例:本來要從 stdin 中讀取數據,可以使用 dup2 系統調用接口,拷貝 某個文件描述符對應的 文件對象指針,轉為 從 另一個文件中讀取數據
int fd = open("fdddddddd.txt", O_CREAT | O_APPEND | O_RDONLY, 0666 ); char buff[100];dup2(fd, 0);
ssize_t s = read(0, buff, sizeof(buff));
buff[s] = '\0';printf("%s", buff);
成功將 fddddddd.txt 重定向到 0 號文件描述符,使得本應從 stdin 讀取數據存入 buff,轉為 從 fdddddd.txt 中讀取數據 存入 buff。
注:如果把 stdout、stdin、stderr 給重定向,覆蓋掉了,有辦法找到這三個文件,再重定向回來即可恢復
為什么 stdout、stderr 都指向顯示器,應用場景:
在分離正常日志與錯誤日志時非常有用:
另外,命令行輸出重定向時,默認被重定向的是 1號 文件描述符 stdout,可以省略不寫。
注:程序替換 exce 接口功能,并不影響進程對 文件的訪問。
小結
硬件設備都可以被進程,以 open 打開訪問,因為 Linux 下一切皆文件:
1、內核為 進程創建 PCB —— task_struct,其中包含指針 指向文件描述符表
2、文件描述符表 這個指針數組中 存放著 struct file* —— 文件對象的指針
3、對于所有外設(鍵盤、顯示器、磁盤等硬件)來說,進程在 打開它們并創建對應的 struct file 時,struct file 中有個自定義類型的指針 指向 struct operation_func
4、struct operation_func 中有相應的 讀、寫 的函數指針,不同外設共用相同的?struct operation_func 結構(一般情況下);讀、寫和其他方法的 函數指針,指向相應的外設的 具體底層驅動的 函數方法實現,但這些對上層來說,是不用關心的;相同的函數指針 卻指向 不同的方法實現,就像面向對象中的 多態一樣
5、struct operation_func 結構中 有 讀、寫等方法的函數指針,不同外設的 底層驅動的 方法實現,就會用來 初始化這些 函數指針。
對于 C 庫中的?stdin、stdout、stderr:
內核在創建進程時就已經把 0、1、2 這三個描述符指向同一個終端設備;
C 庫只是隨后把這三個描述符包裝成 FILE* 變量(stdin、stdout、stderr),并加了一層緩沖而已。
在 Linux 中,鍵盤輸入、屏幕輸出都被抽象成同一個“終端設備文件”,這個終端設備文件既提供讀接口(用戶敲的字符)又提供寫接口(送到屏幕的文字),因此 0、1、2 三個文件描述符實際上都指向同一個 inode(同一個 struct file),只是分別用于讀、寫、寫。