? ? ? ? 本篇博客整理了文件與文件系統、文件與IO的相關知識,借由庫函數、系統調用、硬件之間的交互、操作系統管理文件的手段等,旨在讓讀者更深刻地理解“Linux下一切皆文件”。
【Tips】文件的基本認識?
- 文件 = 內容 + 屬性。文件在創建時就有基本屬性,比如權限,文件名,文件的創建時間等基本信息。
- 文件可分為打開的文件與未被打開的文件,打開的文件由操作系統進行管理,未打開的文件要解決如何找的問題。
- 文件是通過先組織再描述的方式被操作系統間接管理的,具體方式是,通過其共有的屬性被描述為 struct file 結構體對象,這些結構體之間通過鏈表的形式被操作系統組織,進而統一的進行管理。
- 一個進程可能會打開多個文件,多個進程可能會打開同一份文件。以IO的角度來看,顯示器文件,每個進程都要打開這個文件(多對一)。一個進程創建多個文件進行打開(一對多)。因此每個進程都有屬于自己打開的一個或多個文件,因此進程這里就抽象出了 struct files_struct進行管理屬于自己打開的文件。
?
目錄
一、文件相關操作
1.C語言的文件操作接口
1.1-文件的打開和關閉
1.2-文件的讀取和寫入
2.文件相關的系統調用
2.1-open()
2.2-write()
2.3-read()
3.文件描述符
補.Linux下一切皆文件
二、文件重定向
1.重定向的原理
2.重定向相關的系統調用:dup2()
補.stdout 與 stderr 的區別
補.添加重定向至模擬實現的shell
三、文件緩沖區
1.C語言的緩沖區
2.操作系統的緩沖區?
四、文件系統
1.inode
2.磁盤
2.1-磁盤的結構
2.2-分區與格式化
3.EXT2文件系統
4.軟硬鏈接
4.1-軟鏈接
4.2-硬鏈接
4.3-軟鏈接與硬鏈接的區別
補.文件從磁盤加載到內存
五、動靜態庫
1.靜態庫
1.1-打包
1.2-使用
2.動態庫
2.1-打包
2.2-使用
一、文件相關操作
1.C語言的文件操作接口
【補】C語言中涉及文件操作的庫函數
庫函數 功能 fopen 打開文件 fclose 關閉文件 fputc 寫入一個字符 fgetc 讀取一個字符 fputs 寫入一個字符串 fgets 讀取一個字符串 fprintf 格式化寫入數據 fscanf 格式化讀取數據 fwrite 向二進制文件寫入數據 fread 從二進制文件讀取數據 fseek 設置文件指針的位置 ftell 計算當前文件指針相對于起始位置的偏移量 rewind 設置文件指針到文件的起始位置 ferror 判斷文件操作過程中是否發生錯誤 feof 判斷文件指針是否讀取到文件末尾
1.1-文件的打開和關閉
- 打開文件:fopen()
#include <stdio.h>
FILE *fopen( const char *path, const char *mode );
功能:按指定方式打開指定文件
參數:1.path:要打開的文件路徑或當前路徑下的文件名2.mode:打開文件的方式
返回值:打開成功,返回指向該文件信息區的指針(FILE*);打開失敗,返回 NULL
?【補】當前路徑
????????當前路徑,或稱工作路徑,是由進程PCB維護的一個進程屬性。一個可執行程序在被加載到內存成為進程時,它所對應的PCB對象中就維護了一個名為 cwd 的屬性, cwd 就表示這個進程當前的工作路徑。注意!當前路徑具體不是指一個可執行程序所在的路徑,而是指一個可執行程序運行成為進程時,用戶所在的路徑。
【補】打開文件的方式
![]()
- 關閉文件:fclose()
#include <stdio.h>
int fclose( FILE *stream );
功能:關閉一個指定的文件
參數:指向要關閉的文件的指針(FILE*)
返回值:關閉成功,返回 0 ;關閉失敗,返回 EOF
?-------------------------
????????打開和關閉文件的演示:
//file_oc.c #include<stdio.h> int main() {FILE* pf = fopen("log.txt", "r");if (pf == NULL){perror("fopen");return 1;}else{printf("打開成功!\n");}//讀文件 //...//關閉文件fclose(pf);pf = NULL;return 0; }
1.2-文件的讀取和寫入
- 讀取文件內容:fgets()
#include <stdio.h>
char * fgets ( char * str, int num, FILE * stream );
參數:1.str:把讀取到的字符全部拷貝到str所指向的空間2.num:讀取num個的字符(其中會包含一個’\0’,因此實際上只會讀取num-1個字符)3.stream:待讀取的文件,或支持讀取的文件流
返回值:讀取成功會返回str,讀取結束會返回NULL
【ps】fgets是文本行輸入函數,因此當一行沒有讀取完的時候,下一次讀取會從上一次讀取結束的位置繼續讀取當前行的文本。如果待讀取的字符個數num大于當前文本行的字符個數,那此次讀取只會把當前行的所有字符讀取出來,不會去讀取下一行的字符。
?【補】三個標準輸入輸出流
? ? ? ? 程序讀取數據的過程,是用戶會通過敲擊鍵盤的方式將數據寫入到鍵盤文件中,然后程序從鍵盤文件中完成數據的讀取。同理,數據的輸出是將數據寫入到顯示器文件中,然后用戶才能在顯示器上看到相應的數據。
????????C程序在啟動時,默認會打開三個標準流文件:
- stdin:標準輸入流——鍵盤文件
- stdout:標準輸出流——顯示器文件
- stderr:標準錯誤流——顯示器文件
? ? ? ? 于是,向“顯示器文件”寫入數據和從“鍵盤文件”讀取數據的時候,用戶無須先自行進行打開“顯示器文件”和“鍵盤文件”。當C程序運行起來的時候,操作系統就會默認使用C語言的相關接口將這三個輸入輸出流打開,之后才能調用類似于scanf() 和 printf() 之類的輸入輸出函數。
? ? ? ? 而stdin、stdout、stderr這三個流實際上都是FILE*類型的,與用戶在打開某一文件時,獲取到的文件指針是同一個類型。
extern FILE *stdin; extern FILE *stdout; extern FILE *stderr;
-----------------------?
????????讀取文件內容的演示:?
//file_read.c #include <stdio.h> int main() {//打開文件FILE* fp = fopen("log.txt", "r");if (fp == NULL){perror("fopen");return 1;}//讀取文件內容char buffer[64];fgets(buffer, sizeof(buffer), fp);printf("%s", buffer); //關閉文件fclose(fp);return 0; }
- 寫入文件內容:fputs()
#include <stdio.h>
int fputs ( const char * str, FILE * stream );
功能:將一段信息寫入指定的文件
參數:1.str:待寫入的字符串2.stream:待寫入的文件,或支持寫入的流
返回值:寫入成功,返回一個非負數;寫入失敗,返回 EOF
-------------------------
????????寫入文件內容的演示:?
//file_write.c #include <stdio.h> int main() {//打開文件FILE* fp = fopen("log.txt", "a"); //以w方式打開會覆寫文件內容,這里以a(追加)方式打開if (fp == NULL){perror("fopen");return 1;}//寫入文件內容int count = 5;while (count){fputs("hello world\n", fp);count--;}//關閉文件fclose(fp);return 0; }
2.文件相關的系統調用
? ? ? ? 通常來說,文件是保存在磁盤上的,磁盤是外部設備,訪問磁盤其實訪問的是磁盤文件。
????????在計算機層狀結構中,硬件是處于最底層的,操作系統會把這些硬件管理起來。由于操作系統并不相信用戶,因此,操作系統不允許用戶直接訪問硬件,而是向上層的用戶提供了系統調用接口,讓用戶間接去訪問硬件。幾乎所有的庫函數,只要是有關于訪問硬件設備的,它們的底層一定會封裝系統調用,也就是說,C語言里面的 fopen()、fgets()、printf()等,底層都一定封裝了系統調用。
2.1-open()
//open()
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(const char *pathname, int flags, mode_t mode);
功能:打開文件
參數:1. pathname,文件創建的路徑。2. flags,文件打開的方式,具體常見的方式有以下五種:1) O_CREAT,文件沒有就創建。2) O_TRUNC,文件打開即清空。3) O_APPEND,文件以追加形式打開。4) O_WRONLY,文件以只寫方式打開。5) O_RDONLY,文件以只讀方式打開。6) O_RDWR ,文件以讀和寫的方式打開。除此之外,如果想要多種功能可以以 | 進行相連,除此之外若要追加,還得打開寫權限。例如,以只寫方式打開一個尚未創建的文件:O_WRONLY | O_CREAT3.mode,文件的默認打開權限,一般文件設置為0666,目錄設置為0777。新創建文件的默認權限,要考慮權限掩碼,可以配合 umask 系統調用接口來設置自己想要的效果。umask 系統調用產生的效果就只對當前進程創建的文件有關。
返回值:打開成功,返回新打開的文件描述符;打開失敗,返回-1//close()
#include<unistd.h>
int close(int fd);
參數:待關閉文件的文件描述符
返回值:關閉成功,返回0;關閉失敗,返回-1,并設置合適的錯誤碼
【補】 open() 的第二個參數 flags 實際上是一個有32比特位的位圖。
????????若將一個比特位作為一個標志位,則理論上 flags 可以傳遞32種不同的標志位。實際上,傳入flags的每一個選項在系統當中都是以宏的方式進行定義的:
// 在/usr/include/bits/fcntl-linux.h文件中:#define O_RDONLY 00 //O_RDONLY選項的二進制序列為全0,表示O_RDONLY選項為默認選項 #define O_WRONLY 01 #define O_RDWR 02 #define O_CREAT 0100 //...
????????這些宏定義選項的共同點就是,它們的二進制序列當中有且只有一個比特位是1,且為1的比特位是各不相同的,如此,open()內部可以通過&(與)運算來判斷是否設置了某一選項:
int open(arg1, arg2, arg3){//...if (arg2&O_RDONLY){//設置了O_RDONLY選項}if (arg2&O_WRONLY){//設置了O_WRONLY選項}if (arg2&O_RDWR){//設置了O_RDWR選項}if (arg2&O_CREAT){//設置了O_CREAT選項}//...}
? ? ? ? open()的使用示例:
#include<stdio.h> #include<sys/types.h> #include<sys/stat.h> #include<fcntl.h> #include<unistd.h> int main() {umask(0);//設置權限掩碼為0。int fd = open("test.txt",O_CREAT|O_TRUNC|O_WRONLY,0666);//這里是在當前路徑下,以如果沒有就創建,并且清空只寫的方式進行打開。//文件設置的權限為0666(八進制數),切記不能設置為666(十進制數),if(fd < 0){perror("open");return -1;}else{printf("打開成功!\n");}close(fd);//close()是關閉文件的系統調用return 0; }
2.2-write()
#include<unistd.h>
ssize_t write(int fd,const void* buf,size_t count);
功能:向文件寫入內容
參數:1. fd,待寫入文件的文件描述符2. buf,指向待寫入的文件內容3. count,待寫入內容的大小(單位是字節)
返回值:
寫入成功,返回寫入文件的字節數(0表示沒有寫入內容);寫入失敗,返回-1,并設置合適的錯誤碼
? ? ? ? write() 的使用示例:
#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("test.txt",O_CREAT|O_TRUNC|O_WRONLY,0666);if(fd < 0){perror("open");return -1;}const char* message = "hello write\n";write(fd,message,strlen(message));close(fd);return 0; }
2.3-read()
#include<unistd.h>
ssize_t write(int fd,const void* buf,size_t count);
功能:讀取文件內容
參數:1. fd,待讀取文件的文件描述符。2. buf,指向一段空間,該空間用來存儲讀取到的內容3. count,限定參數二指向空間的大小
返回值:讀取成功,返回文件的字節數(0表示沒有內容);讀取失敗,返回-1,并設置合適的錯誤碼
【補】調整文件指針的接口:lseek()
? ? ? ? 文件的讀取是從文件指針所指的位置開始向后讀取的。假設在讀取前,創建一個新的文件并向其中寫入了“ hello write?” ,那么文件指針會指向 “ hello write?”之后的位置,此時進行讀取操作,是無法讀取到任何內容的,而要讀取到文件內容,需將文件指針調整到文件開頭再進行讀取操作。
#include<unistd.h> #include<sys/type.h> off_t lseek(int fd,off_t offset,int whence); 功能:調整文件指針的位置 參數:1. fd,待調整文件的文件描述符2. offset,移動到相對于whence偏移量offset的位置。3. whence, 移動的起點位置,常見的有SEEK_SET(文件開頭),SEEK_CUR(文件的當前位置),SEEK_END(文件末尾位置)。常見用法:1) lseek(fd,0,SEEK_SET); //移動文件指針到開始2) lseek(fd,0,SEEK_CUR); //移動文件指針到當前位置3) lseek(fd,0,SEEK_END); //移動文件指針到結束位置 返回值:調整成功,返回距離文件開頭的字節數;調整失敗,返回-1,并設置合適的錯誤碼。
? ? ? ? read()的使用示例:
#include<stdio.h> #include<sys/types.h> #include<sys/stat.h> #include<fcntl.h> #include<unistd.h> #include<string.h> #include<stdlib.h> #define MAX_SIZE (1024) int main() {int fd = open("test.txt",O_CREAT|O_TRUNC|O_RDWR,0666);if(fd < 0){perror("open");return -1;}//向文件寫入hello write\nconst char* message = "hello write";write(fd,message,strlen(message));lseek(fd,0,SEEK_SET);//移動文件指針到開始//向文件中讀入hello write\nchar buf[MAX_SIZE] = {0};read(fd,buf,strlen(message));printf("read:%s\n",buf);close(fd);return 0; }
3.文件描述符
????????文件是在進程運行時,由進程打開的。一個進程可以打開多個文件,而系統中又存在著大量進程,那么在系統中,任何時刻都可能存在大量已打開的文件,因此,操作系統務必要管理好這些已經打開的文件。
? ? ? ? 具體的方式還是先描述再組織——操作系統會為每個已經打開的文件,創建各自的 struct file 結構體對象,然后將這些結構體對象構建成一個雙向鏈表,將文件的管理轉化成雙向鏈表的增刪查改。而為了區分已經打開的文件是屬于哪個進程的,就還需要建立進程和文件之間的映射關系。
????????那么,進程和文件之間的映射關系是如何建立的呢?
????????當一個進程被創建的時候,操作系統會為其創建相應的進程控制塊(task_struct)、進程地址空間(mm_struct)、頁表等相關的數據結構,并通過頁表建立起虛擬內存和物理內存之間的映射關系。
? ? ? ? 在 task_struct 中有一個指針,指向一個名為 files_struct 的結構體,而在 files_struct 結構體中又有一個名為 fd_array 、類型為 struct file* 的指針數組,這個指針數組其實就叫做文件描述符表,而這個指針數組的下標就叫做文件描述符(因此文件描述符一定大于等于0)。
? ? ? ? 通常來說,文件是保存在磁盤上的。當有一個進程打開了一個文件時,這個文件會從磁盤加載到內存,操作系統會為其創建相應的 struct file*類型的結構體對象,然后將這些結構體對象也構建成一個雙向鏈表,并保存到 fd_array 指針數組下標為3的位置上,最終返回這個下標(該文件的文件描述符)給打開文件的進程。
????????因此,只要有一個文件的文件描述符,就可以找到這個文件的相關信息,進而對這個文件進行文件操作。
【Tips】文件描述符本質是一個負責維護文件信息的指針數組的下標。
【Tips】文件描述符對應的分配規則:從 0 號下標開始,尋找下標最小的、沒有使用過的位置。
????????程序在運行起來的時候,操作系統會默認打開標準輸入流 stdin(下標0)、標準輸出流 stdout(下標1)、標準錯誤流 stderr(下標2),它們也是 struct file*類型的結構體對象,對應著鍵盤文件和顯示器文件。而之后由進程新打開的文件,它們的文件描述符只能從 3 開始,而不能再是0、1、2。
【補】磁盤文件與內存文件
? ? ? ? 文件可由文件的存儲位置分為磁盤文件與內存文件。當文件存儲在磁盤當中時,就稱之為磁盤文件;當磁盤中的文件被加載到內存中后,就稱之為內存文件。
????????磁盤文件和內存文件的關系類似于程序和進程的關系,程序被加載到內存運行起來后便成了進程,而磁盤文件加載到內存后便成了內存文件。
? ? ? ? 日常口頭所說的文件,通常是指磁盤文件。磁盤文件由兩部分構成,分別是文件內容和文件屬性。文件內容就是文件當中存儲的數據,文件屬性就是文件的元信息,例如文件名、文件大小、文件創建時間等。
? ? ? ? 在磁盤文件被加載到內存的過程中,它的文件屬性一般會先被加載,當需要對文件內容進行操作時,再加載文件內容。【補】C語言的 FILE 類型
????????FILE 其實是 C 語言庫中封裝了文件描述符的一個結構體,其中, _fileno 屬性就是文件描述符。
【補】文件關閉與引用計數
????????一個文件可以被多個進程同時打開,例如程序運行時默認打開的 stdout 和 stderr 都會打開顯示器文件。對于一個被多個進程打開的文件,要怎樣合理地關閉它呢?
????????在管理文件的 struct file 對象中有一個 f_count 字段,含義是當前文件的引用計數,能夠記錄當前文件被多少個進程打開。
????????進程要關閉一個文件,是通過調用 close() 來完成的,close() 會將文件在指針數組fd_array 中的相應下標(文件描述符)處置為 NULL,然后操作系統會調整這個文件相應的 struct file 對象中的 f_count 字段。每個打開這個文件的進程進行一次關閉操作,f_count都會自減1,直到?f_count 為0 時,操作系統才將這個文件的 struct file 對象回收。
補.Linux下一切皆文件
????????計算機為用戶提供的服務,都是由進程去完成的,所以,用戶關于文件的操作,也都是由進程去完成的,換句話說,所有對文件的操作都依賴于進程。
????????所有的外設都經過“先描述再組織”的思想被抽象成了文件,每個外設都有自己的讀寫方法。
????????對于不同的外設,它們的讀寫方法一定不同,但用戶在進行文件操作的時候,用戶的行為本身十分統一,來來回回都是在調用open()、close()、read()、write()等系統調用接口。這是因為操作系統為用戶提供了一個接口集合?file_operations 結構體對象,其中封裝的都是函數指針,可以指向不同外設的不同方法,而這種設計思想其實就是多態。
? ? ? ? 總之,所謂的“Linux下一切皆文件”,其實就是操作系統通過封裝的一層文件對象,將進程對各種外設的操作,轉化成了進程對各種文件的操作。對操作系統來說,不論管理的對象如何變化,管理的方法始終還是那不變的六字真言——“先描述再組織”(面向對象)。
二、文件重定向
? ? ? ? 重定向有輸出重定向、輸入重定向、追加重定向。
????????最典型的重定向例子是,“echo + 字符串”默認是將字符串寫入顯示器文件并打印到屏幕上,加入輸出重定向后,就可以將字符串寫入到任一文件中。
????????追加重定向是一種特殊的輸出重定向,它和輸出重定向的區別是,輸出重定向是覆蓋式輸出數據,而追加重定向是追加式輸出數據。
1.重定向的原理
- 輸出重定向的原理
????????輸出重定向就是,將本應該輸出到一個文件的數據,重定向輸出到另一個文件中。
? ? ? ? 為了方便演示輸出重定向的原理,此處引入以下代碼:
//test_p.c
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{//關閉1號文件close(1);//打開一個新文件int fd = open("log.txt", O_WRONLY | O_CREAT, 0666);if (fd < 0){perror("open");return 1;}//打印一段內容printf("hello world\n");fflush(stdout);//關閉剛剛打開的文件 close(fd);return 0;
}
? ? ? ? 按理來說,printf()會將“hello world”打印在屏幕上,為什么在程序運行時,屏幕上并沒有出現任何內容,而新創建的文件 log.txt 中出現了要打印的“hello world”呢?
? ? ? ? 上文已提及,文件描述符的分配規則,即從 fd_array 的 0 號下標開始,尋找最小的、沒有被使用(所存為?NULL,沒有指向任何一個 struct file 對象)的下標位置。
? ? ? ? 以上代碼中,先調用 close() 將 1 號下標對應的顯示器文件關閉,又調用 open() 打開了一個文件 log.txt 。
????????由文件描述符的分配規則,新打開的這個文件的文件描述符應該為?1,換句話說,原本 fd_array 的 1 號下標存的是標準輸出流 stdout /顯示器文件,但現在存的是新打開文件的struct file對象的地址。
????????printf() 的功能是打印內容到顯示器上,具體的實現方式是,將要打印的內容寫入1號文件。原本的1號文件是 stdout ,但此時的1號文件其實是剛剛打開的 log.txt ,所以printf() 會將打印的內容寫入到 log.txt 中,而非顯示器文件。于是在程序運行時,屏幕上并沒有出現任何內容,而 log.txt 中出現了要打印的“hello world”。
- 輸入重定向的原理
????????輸入重定向就是,將本應該從一個文件讀取數據,重定向為從另一個文件讀取數據。
? ? ? ? 為了方便演示輸入重定向的原理,此處引入以下代碼:
//test_s.c
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{close(0);int fd = open("log.txt", O_RDONLY | O_CREAT, 0666);if (fd < 0){perror("open");return 1;}char str[40];while (scanf("%s", str) != EOF){printf("%s\n", str);}close(fd);return 0;
}
? ? ? ? ?按理來說,scanf() 會從鍵盤讀取數據并存入字符數組 str 中,然后 printf() 會將 str 中的數據打印在屏幕上。但在程序運行起來的時候,并沒有等待從鍵盤輸入,而是屏幕上直接出現了“Hello Linux”。這是為什么呢?
????????以上代碼中,先調用 close() 將 0?號下標對應的鍵盤文件關閉,又調用 open() 打開了一個文件 log.txt 。log.txt 中原本寫入有“Hello Linux”。
? ? ? ? 由于關閉了 0 號下標原本的鍵盤文件,又由文件描述符分配規則,因此這個新打開的文件的文件描述符應該為 0,代替鍵盤文件成為新的 0 號文件。
????????scanf()的功能是從鍵盤讀取數據并寫入到一個變量,具體的實現方式是,在 0 號文件中讀取數據并將數據寫入一個變量。原本的 0 號文件是 stdin?,但此時的 0 號文件其實是剛剛打開的 log.txt,所以 scanf() 會從?log.txt 中讀取數據并寫入到 str 中,而非從鍵盤文件讀取。于是,在程序運行起來的時候,并沒有等待從鍵盤輸入,而是屏幕上直接出現了“Hello Linux”。
【Tips】重定向的原理:本質其實是修改了文件描述符表中指針元素的指向。
【ps】盡管 stdout 和 stderr 都對應顯示器文件,但在使用輸出重定向時,只會重定向 1 號文件 stdout,而不會重定向 2 號文件 stderr。
2.重定向相關的系統調用:dup2()
#include<unistd.h>
int dup2(int oldfd,int newfd);
功能:重定向,將oldfd下標指向的文件指針覆蓋newfd下標的文件指針
參數:1.oldfd,是被重定向的文件的文件描述符。2.newfd,是重定向的目標文件的的文件描述符。
返回值:成功,返回newfd;失敗,返回-1,并設置合適的錯誤碼。
【Tips】dup2() 的工作細節
????????dup2() 并不會先將一個文件關閉,再接著打開一個文件,而是將參數 oldfd 對應的 struct file 指針覆蓋了?newfd 對應的原先的struct file 指針,以此完成重定向。
? ? ? ? 假設現要將一個新打開的文件重定向為1號文件——
????????在 dup2() 被調用之前,正常打開了一個文件,且不將顯示器文件關閉,此時顯示器文件的文件描述符就是 1,這個新打開文件的文件描述符就是 3。接下來,在調用 dup2() 的時候,這個新打開文件的文件描述符(3)將會作為參數 oldfd,而顯示器文件的文件描述符(1)將作為參數 newfd。dup2() 的工作,就是將顯示器文件對應的1號下標的 struct file 指針,指向新打開文件的 struct file。
? ? ? ? ?也就是說,dup2() 在調用后,顯示器文件不再是1號文件了,而盡管新打開的文件還是3號文件,但它同時也是1號文件了。
? ? ? ? ?dup2() 的使用示例:
//test_dup2.c #include<stdio.h> #include<unistd.h> #include<sys/types.h> #include<sys/stat.h> #include<fcntl.h> int main() {int fd = open("test.txt",O_TRUNC | O_CREAT | O_RDWR,0666);//重定向dup2(fd,1);//將fd指向的文件指針覆蓋1號下標的文件指針printf("hello world\n");close(fd);return 0; }
補.stdout 與 stderr 的區別
????????標準輸出流 stdout 和標準錯誤流 stderr 都對應顯示器文件,但它們有什么區別呢?
? ? ? ? 為了方便演示,此處引入以下代碼:
#include <stdio.h>
int main()
{printf("hello printf\n"); //stdoutperror("perror"); //stderrfprintf(stdout, "stdout:hello fprintf\n"); //stdoutfprintf(stderr, "stderr:hello fprintf\n"); //stderrreturn 0;
}
????????printf() 的功能是默認使用 stdout 打印,perror() 的功能是默認使用 stderr 打印,fprintf()可以指定一個流來打印。
? ? ? ? 以上代碼的結果,stdout 和 stderr 的信息都成功打印了。
????????stdout 和 stderr 似乎沒有什么區別。
? ? ? ? 但如果將以上代碼的運行結果重定向到一個文件中去,stdout 和 stderr 的區別就很明顯了:
? ? ? ? 由圖易知,stdout 的信息重定向到了 log.txt 中,而 stderr 的信息沒有成功重定向。
【Tips】文件描述符為1的標準輸出流 stdout 默認可以進行重定向,但文件描述符為2的標準錯誤流 stderr 默認不會進行重定向。
【補】指定對 stderr 進行重定向
- 文件描述符 +? >? +? 目標文件名:
- 同時對 stdout 和 stderr 進行重定向:
- 將 stdout 和 stderr 重定向到同一個文件:
補.添加重定向至模擬實現的shell
(模擬實現shell詳見:【Linux系統】進程控制-CSDN博客)
????????重定向的實現步驟:
- 處理輸入的命令(字符串),若包含>、>>、<則分別按不同類型的重定向處理(設置一個type變量,type為0表示命令當中包含輸出重定向,type為1表示命令當中包含追加重定向,type為2表示命令當中包含輸入重定向);
- 打開目標文件(重定向符號后面的字段為重定向的目標文件名,若type值為0則以寫的方式打開目標文件,若type值為1則以追加的方式打開目標文件,若type值為2則以讀的方式打開目標文件);
- 使用 dup2() 完成重定向(若type值為0或1,則使用dup2接口實現目標文件與標準輸出流的重定向,若type值為2,則使用dup2接口實現目標文件與標準輸入流的重定向)。
#include <stdio.h>
#include <fcntl.h>
#include <ctype.h>
#include <pwd.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#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] = '\0';//實現重定向功能char* start = cmd;while (*start != '\0'){if (*start == '>'){type = 0; //遇到一個'>',輸出重定向*start = '\0';start++;if (*start == '>'){type = 1; //遇到第二個'>',追加重定向start++;}break;}if (*start == '<'){type = 2; //遇到'<',輸入重定向*start = '\0';start++;break;}start++;}if (*start != '\0'){ //start位置不為'\0',說明命令包含重定向內容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){//childif (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}//shellint status = 0;pid_t ret = waitpid(id, &status, 0); //shell等待child退出if (ret > 0){printf("exit code:%d\n", WEXITSTATUS(status)); //持續打印child的退出碼}}return 0;
}
三、文件緩沖區
1.C語言的緩沖區
????????FILE是C語言當中與文件相關的一個結構體類型,其中封裝了文件描述符、C語言自帶的緩沖區等相關信息。
//【補】FILE源碼
struct _IO_FILE {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 0int _blksize;
#elseint _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
};
? ? ? ? 為了驗證C語言緩沖區的存在,此處引入以下代碼:
#include <stdio.h>
#include <unistd.h>
int main()
{//庫函數printf("hello printf\n"); //打印結果到顯示器上fputs("hello fputs\n", stdout);//向標準輸入流寫入//系統調用write(1, "hello write\n", 12); //向1號文件寫入fork();return 0;
}
? ? ? ? ?運行由以上代碼生成的程序后,printf()、fputs()、write() 都成功將預期內容輸出到了顯示器上:
????????但如果將程序的結果重定向到一個 test.txt 文件后,文件中的內容與程序運行的結果卻有所不同,write()如預期一樣正常寫入到顯示器文件了一次,而 printf()、fputs() 分別打印了兩次:
????????為什么庫函數打印的結果,重定向到文件后就變成了兩份,而系統接口打印的結果還和原來一樣?
? ? ? ? 要打印數據被寫入的時候,并不是直接存到顯示器文件中的,而是先存在緩沖區中的。
? ? ? ? 在C語言中,要將數據打印到顯示器,一般會以行緩沖的方式(這是因為代碼中有換行符 \n)將數據從緩沖區刷新到顯示器文件中,從而打印在顯示器上。而將數據重定向到一個文件的操作,使數據的去向從顯示器文件變成了磁盤文件,換句話說,數據的刷新策略從行緩沖變為了全緩沖。
????????在以上代碼中,C語言的庫函數 printf() 和 fputs() 所打印的數據都會先存到C語言自帶的緩沖區中。在代碼的下文,又調用了fork()創建了一個子進程,此時由于進程間的獨立性,會發生寫時拷貝(因為父子進程起先是共享一份父進程的數據,之后不論誰要刷新緩沖區,都會涉及數據的修改),于是,緩沖區中的數據就從一份變成了兩份(一份父進程的,一份子進程的)。所以,將程序的結果重定向到 test.txt 文件中后,printf() 和 puts() 就打印了兩份數據。而write() 是系統調用,是直接將數據寫入到顯示器文件的,相當于沒有緩沖區,因此,write() 還是只打印一份數據。
【Tips】C語言緩沖區的刷新方式
- 無緩沖(直接刷新;fflush(stdout))
- 行緩沖(換行就刷新;換行符 \n)——常見于對顯示器進行刷新數據
- 全緩沖(等緩沖區滿了再刷新;輸出重定向>)——常見于對磁盤文件寫入數據
2.操作系統的緩沖區?
????????操作系統也是有緩沖區的。
????????在用戶寫入數據的過程中,這些被用戶正在寫入數據的數據并不是直接存到磁盤或顯示器中的,而先被存到了用戶緩沖區中。而當用戶刷新用戶緩沖區時,其實也不會將用戶緩沖區的數據直接存入磁盤或顯示器中,而是先將數據存到操作系統的緩沖區中,最終由操作系統將這些數據存入磁盤或顯示器。至于是以上面方式刷新緩沖區,操作系統有自己的刷新機制,用戶不必關心。
四、文件系統
1.inode
? ? ? ? 根據文件所在的位置,文件可以被分為磁盤文件和內存文件,日常稱呼的文件都指的是磁盤文件。磁盤文件由兩部分構成,分別是文件內容和文件屬性,文件內容就是文件當中存儲的數據,文件屬性或稱文件的元信息,例如文件名、文件大小、文件創建時間等。
? ? ? ? 指令 ll (ls -l)可以查看當前目錄下文件的各種元信息:
????????在Linux下,文件的屬性和內容雖然都存儲在磁盤中,但它們其實是分離存儲的。用于保存文件各種屬性的集合的結構被稱之為 inode 結構,由于系統當中可能存在大量的文件,因此為了便于管理,每個文件的屬性集合都有一個唯一的 inode號。
? ? ? ? 指令 ls -i,可顯示當前目錄下各文件的 inode 號:
2.磁盤
????????磁盤是一種永久性存儲介質,與之相對應的是內存,內存是一種掉電易失存儲介質。在計算機中,磁盤幾乎是唯一的機械設備,主要用于存儲所有的普通文件;在馮諾依曼體系結構中,磁盤既可以充當輸入設備,又可以充當輸出設備。
? ? ? ? 在當代,文件一般都存儲在硬盤中。普通人的計算機通常搭載的是固態硬盤(SSD),而企業為了降低存儲大型數據的成本,一般使用的是機械硬盤,也就是磁盤。
2.1-磁盤的結構
? ? ? ? 磁盤的結構主要有盤面、磁頭、主軸等。其中,盤面是存儲數據的主力,磁頭是讀取數據的主力。
????????每一個盤面都由多個磁道構成,而一個磁道又有多個扇區構成,所以,磁盤可以看成是由無數個扇區構成的存儲介質。磁盤被訪問的基本單元是扇區,可存儲的大小一般為?512 字節(有的是 4KB)。要修改磁盤中 1 字節的數據,就需要把這 1 字節所在扇區的數據都加載到內存中。要把數據存儲到磁盤,就需要定位一個扇區:
- 先定位一個盤面,其實也就是確定一個磁頭,因為磁頭在盤面上,每一個磁頭都對應一個盤面;
- 接下來在這個盤面上定位一個磁道;
- 最后在這個磁道中定位一個扇區。
????????所有的磁頭都是同步運動的,在某一時刻,從從上向下看去,以磁頭所在點為半徑的不同盤面上的磁道就會形成一個叫做柱面的結構。磁頭的運動由硬件電路進行控制,運動的目的是去定位磁道。盤面旋轉的目的是去定位扇區。
????????磁盤的讀取效率取決于磁頭、盤面的運動速度和運動次數,總得來說,運動越少,效率越高;運動越多,效率越低。因此,在軟件設計上要求設計者一定要有意識的將相關數據放在一起。
【Tips】磁盤的結構
- 盤面:用于存儲文件信息,二進制信號,一般有多個磁盤。
- 磁頭:用于尋找文件,其與磁盤的距離很小,一般有多個磁頭,與磁盤對應。
- 主軸:方便磁頭進行定位。
【Tips】磁盤讀寫的一般流程
- 確定待讀寫信息在磁盤的哪個盤面;
- 確定待讀寫信息在磁盤的哪個磁道;
- 確定待讀寫信息在磁盤的哪個扇區。
【Tips】磁盤的特點
- 磁盤與磁頭一一對應,且不接觸,通過空氣與電進行寫入信息。
- 造價低,壽命長。
- 制作環境要求高。
- 馬達與磁盤轉動的噪音可能較大。
2.2-分區與格式化
? ? ? ? 由于磁盤是由無數個扇區構成的,因此磁盤本身可以被抽象成一個線性的存儲介質,而這個線性的存儲介質可以看作是一個一維數組,每個扇區都可以看作是一維數組中的一個元素,每一個元素都有一個對應的下標來對它進行唯一地標識。
? ? ? ? 如此,就可以方便地從邏輯上對磁盤進行分區,從而更好地管理磁盤。
????????磁盤分區是通過分區編輯器在磁盤上劃分的幾個邏輯部分。每個盤面一旦有了分區,就可以對文件進行更細致的管理,使不同的目錄與文件按其屬性或其他標準存儲進相應的分區。
? ? ? ? 磁盤分區最常見的例子就是,Windows下的C盤、D盤等。
????????當磁盤完成分區后,就可以對磁盤進行格式化(初始化),以便對分區后的各個區域寫入相應的管理信息。
????????寫入的管理信息的具體內容由文件系統來決定,不同的文件系統在格式化時寫入的管理信息是不同的。常見的文件系統有EXT2、EXT3、XFS、NTFS等。
3.EXT2文件系統
? ? ? ? EXT2 是Linux下的一個磁盤文件系統。
? ? ? ? EXT2 的每一個磁盤分區,頭部都會包含一個啟動塊(Boot Block),其余區域是根據分區大小來劃分的一個個的塊組(Block Group)。其中,啟動塊的大小是一定的,而塊組的大小是在格式化時確定的,一旦確定不可以更改。
????????每個組塊都有著相同的組成結構,都由超級塊(Super Block)、塊組描述符表(Group Descriptor Table)、塊位圖(Block Bitmap)、inode位圖(inode Bitmap)、inode表(inode Table)以及數據表(Data Block)組成。
【Tips】組塊的組成結構
- 超級塊(Super Block): 存放文件系統本身的結構信息。記錄的信息主要有:Data Block和inode的總量、未使用的 Data Block 和 inode 的數量、一個 Data Block 和 inode 的大小、最近一次掛載的時間、最近一次寫入數據的時間、最近一次檢驗磁盤的時間等其他文件系統的相關信息。Super Block 的信息被破壞,可以說整個文件系統結構就被破壞了,但其他塊組中可能會存在冗余的 Super Block,當某一塊組的 Super Block 被破壞,還可以通過其他塊組的 Super Block 來恢復。
- 塊組描述符表(Group Descriptor Table): 描述該分區當中塊組的屬性信息。
- 塊位圖(Block Bitmap): 記錄著 Data Block 中哪個數據塊已經被占用,哪個數據塊沒有被占用。
- inode 位圖(inode Bitmap):記錄著每個 inode 號是否空閑可用。
- inode 表(inode Table): 存放每個文件的 inode 結構, inode 結構中是文件的屬性信息。
- 數據表(Data Block): 存放每個文件內容。
【補】文件系統的運作細節
?(1)一個空文件是怎么被創建的?
- 遍歷 inode 位圖,找到一個空閑的 inode號。
- 在 inode 表中找出對應的 inode 結構,然后將新文件的屬性信息寫入這個 inode 結構中。
- 將這個新文件的文件名和 inode 指針添加到它所屬目錄的數據塊中。
- 【ps】每個文件的文件名并沒有存儲在自己的 inode 結構中,而是存儲在它所屬的目錄文件的數據塊中,這是因為操作系統只關注 inode 號而不關注文件名。文件名和文件的inode 指針存儲在它所屬的目錄文件的數據塊中, 只需在目錄下通過文件的 inode 號即可找到文件的文件名、文件屬性和文件內容。
(2)文件是怎么寫入信息的?
- 通過文件的 inode 號,在 inode 表中找到對應的 inode 結構;
- 通過 inode 結構找到存儲這個文件內容的數據塊,并將數據寫入數據塊中;
- 若數據塊還未申請(意味著這是個空文件),或數據塊已被寫滿,則遍歷塊位圖,找出一個空閑的塊號,然后在數據區中找到這個空閑的數據塊,再將數據寫入,最終建立數據塊和 inode 結構的映射。
(3)如何刪除一個文件?
- 將這個文件的 inode 號在 inode 位圖中置為無效;
- 將這個文件已申請的數據塊在塊位圖中置為無效。
(4)為什么一般拷貝文件很慢,但刪除文件很快?
????????拷貝一個文件,通常是要拷貝文件的數據,要先創建一個新文件,再對這個文件通過寫入操作來拷貝源文件的數據。這個過程需要先申請 inode 號和在 inode 結構中文件的屬性信息,再申請數據塊號,然后才能進行文件內容的數據拷貝。
????????刪除一個文件,并不是要將文件真正地從磁盤上抹除,只需將這個文件的 inode 號和數據塊號置為無效即可。
? ? ? ? 相比之下,拷貝文件的步驟更繁瑣,刪除文件的步驟更簡單,所以拷貝文件很慢,刪除文件很快。
(5)目錄
????????目錄也是文件,也有自己的屬性信息和自己的內容。
????????目錄的屬性信息,如目錄的大小、目錄的擁有者等,也存儲在目錄的 inode 結構中;目錄的內容就是目錄下的文件名和它們的 inode 指針,也存儲在目錄的數據塊中。
4.軟硬鏈接
4.1-軟鏈接
????????軟鏈接或稱符號鏈接,類似于 window 下的快捷方式,可以讓用戶快速鏈接到目標文件或目錄。軟鏈接文件可以通過源文件名,找到源文件的數據。
????????軟鏈接文件與源文件的 inode 號是不同的,所擁有的權限也是不相同的。對于源文件來說,軟鏈接文件是一個獨立的文件,擁有自己的 inode 號,只包含了源文件的路徑名,所以,軟鏈接文件要比源文件小得多。當源文件被刪除后,軟鏈接文件不能獨立存在,雖然它的文件名仍會保留,但不能執行或查看軟鏈接的內容了。
? ? ? ? 以下指令可以創建一個軟鏈接文件:
ln -s 源文件名 軟鏈接文件名
4.2-硬鏈接
????????硬鏈接文件就是源文件的一個別名,可以使一個文件名無論在不在同一個目錄下,這個文件都能被修改甚至被同時修改,只要通過其中一個文件名修改了文件,所有與這個文件存在硬鏈接的文件都會被一起修改。
????????通過源文件的 inode 值,產生一個新的文件名而非新的文件,相當于源文件取了一個別名,這個別名文件和源文件都擁有相同的 inode號。
????????一個源文件有多少個相關的文件名,這個源文件的硬鏈接數就是多少。當硬鏈接的源文件被刪除后,硬鏈接文件仍能正常執行和查看,只是這個文件的鏈接數會減?1 。
? ? ? ? 以下指令可以創建一個硬鏈接文件:
ln 源文件名 硬鏈接文件名
【ps】目錄文件不能進行硬鏈接!如果一個目錄可以進行硬鏈接,進入這個目錄時,同時也進入了硬鏈接的目錄,由于目錄是樹形結構,目錄的查找是遞歸操作,因此一旦在這個有硬鏈接的目錄下查找文件,就會陷入遞歸死循環。
【補】為什么新創建的目錄的硬鏈接數是 2??
????????創建一個新的普通文件,它的硬鏈接數一定是 1 ,這很好理解,因為這個新創建的普通文件目前只有一個文件名。但為什么創建一個新的目錄,這個新的目錄的硬鏈接數是 2 呢?
? ? ? ? 這是因為,一個目錄文件在創建后,在這個目錄下會默認創建兩個隱含文件 . 和 ..?,它們分別代表當前目錄和上級目錄。而當前目錄 .?其實就是這個新創建的目錄本身,它們的 inode 號也是相同的,也就是說,存在兩個文件名指向這同一個目錄文件,于是新創建的目錄的硬鏈接數就是 2 了。
【Tips】在一個目錄下,相鄰子目錄數 = 這個目錄的硬鏈接數 - 2 。
4.3-軟鏈接與硬鏈接的區別
- 指令 ln -s 創建軟鏈接,指令 ln 創建硬鏈接。
- 目錄不能創建硬鏈接,且不能跨分區系統創建。
- 軟鏈接支持文件和目錄,且可以跨分區系統創建。
- 硬鏈接文件與源文件的 inode 相同,軟鏈接與源文件的 inode 不同。
- 刪除軟鏈接文件和刪除硬鏈接文件,都對源文件沒有任何影響。
- 刪除源文件,軟鏈接會失效,但硬鏈接沒有影響。
- 刪除源文件和源文件的硬鏈接,這個文件就會被真正刪除。
【補】刪除鏈接的指令:
unlink 鏈接文件名
補.文件從磁盤加載到內存
? ? ? ? 由于磁盤讀取數據的速度較慢,而內存讀取數據的速度要遠快于磁盤,因此訪問文件時一般會先將文件從磁盤加載到內存中。
????????內存與磁盤之間的數據交換,一般默認以 4KB(大小可更改) 為單位進行。其中,內存中一個 4KB 大小的空間叫做頁框,在?4KB 空間中填入的內容叫做頁幀。
【Tips】文件默認以 4KB 為單位,從磁盤加載到內存
- 這樣可以減少 IO (CPU 訪問外設)的次數。一次訪問 4KB 和分四次訪問 1KB 相比,顯然前者效率更高。按前者的方案, CPU 只訪問了一次磁盤,磁頭和盤面只需要進行一次定位就可以讀取出 4KB 的內容;而后者的方案, CPU 要訪問四次磁盤,如果這四次訪問并不連續,那么磁頭和盤面就得分別定位四次,產生大量的機械運動,既影響效率也影響硬件壽命。
- 默認以 4KB 為單位,也是由于局部性原理的預加載機制。即使當前 CPU 只需要訪問100 字節的內容,但磁盤上的數據還是以 4KB 為單位加載到內存。這也可以支持,?CPU 在訪問磁盤中的代碼和數據時,接下來也有較大可能訪問附近頁框的代碼和數據。
- 4KB 是相關科學家經過大量實驗,從而確定的一個較為合理的值。
【Tips】頁框與頁幀
- 頁框(Page Frame):通常指的是在內存管理單元(MMU)中用于存儲頁面表的一組連續條目,包含了用于地址轉換的頁表項,以便將虛擬地址映射到物理地址上的對應頁幀。
- 頁幀(Page Frame):是物理內存(RAM)中的一個固定大小的區域,用于存儲從輔存(如硬盤)中調入的頁面內容,操作系統使用頁幀來管理物理內存,將頁面映射到這些頁幀上。
- 頁框負責虛擬地址與物理地址的映射。,頁幀負責管理從硬盤加載的內容
【Tips】文件頁緩沖區
????????在磁盤上,文件的屬性用 inode 結構來存儲,文件的內容用 block 數據塊來存儲;而在內核中,文件屬性用 struct inode 來存儲,文件的內容文件頁緩沖區來存儲。
????????在每個文件的 struct file 結構中,有一個指向 struct address_space 結構體的指針, 而在 struct address_space 結構體中,有一個 struct radix_tree_root 結構體,它本質上是一個樹狀結構,樹中的每個節點都是 struct radix_tree_node 類型,在該類型中,又有一個名為 slots 的 void * 類型的指針數組,存儲了?struct page 結構體對象的地址。
????????總而言之,在每個文件的 struct file 結構體中都有一個指向某塊物理內存的struct page結構體,而 struct page 結構體中封裝的一塊物理內存,就叫文件頁緩沖區。
五、動靜態庫
? ? ? ? 如果想要別人也使用自己寫的代碼,一般有兩種方案——第一種方案就是,將自己的源代碼直接拷貝一份給別人使用,但拷貝的工作可能很繁瑣,在自己的代碼中也可能有不想讓別人知道細節;第二種方案就是,將自己的的源代碼打包成庫,將打包的庫和相應的頭文件提供給別人使用。而由源代碼打包成的庫,又可以分為靜態庫和動態庫。
(關于動靜態庫的基本介紹和功能,詳見:【Linux入門】基礎開發工具-CSDN博客)
1.靜態庫
1.1-打包
????????為了方便演示,此處以簡單的加法和減法函數為例,并引入下面四個文件:
//add.h #pragma onceextern int my_add(int x, int y);
//add.c #include "add.h"int my_add(int x, int y) {return x + y; }
//sub.h #pragma onceextern int my_sub(int x, int y);
//sub.c #include "sub.h"int my_sub(int x, int y) {return x - y; }
?
- 方案一:使用指令打包
????????step1:先讓所有源文件生成對應的目標文件
? ? ? ? step2:使用以下指令,將所有目標文件打包成文件后綴為 .a 的靜態庫:
ar -rc 靜態庫名 目標文件名1 目標文件名2 ...//【ps】ar命令是gnu的歸檔工具,常用于將目標文件打包為靜態庫 //參數 -r(replace):若靜態庫文件當中的目標文件有更新,則用新的目標文件替換舊的目標文件。 //參數 -c(create):建立靜態庫文件。 //參數 -t:列出靜態庫中的文件。 //參數 -v:顯示文件的詳細信息。
? ? ? ? step3:將源文件相應的頭文件與剛生成的靜態庫組織起來,具體的方式是,將兩者放到同一個目錄下。
- 方案二:通過 make 和 Makefile 一鍵打包?
? ? ? ? 將方案一的指令全部寫到 Makefile 文件中,通過 make 一鍵打包。
? ? ? ? step1:編輯 Makefile 文件。
? ? ? ? step2:通過指令 make 一鍵生成目標文件和相應的靜態庫。
? ? ? ? step3:通過指令 make output 一鍵組織頭文件和靜態庫。
?
1.2-使用
? ? ? ? 由于源文件和庫在進行鏈接時,庫必須是能夠找到的,因此在使用自己打包的庫時,最好指定庫的路徑或將庫拷貝到系統路徑下,以防鏈接失敗。?
? ? ? ? 為了方便演示,此處以使用上文的加法函數和減法函數為例,引入以下代碼:
//main.c #include <stdio.h> #include <add.h> #include <sub.h> int main() {int a = 20;int b = 10;int c = my_add(a, b);int d = my_sub(a, b);printf("%d + %d = %d\n", a, b, c);printf("%d - %d = %d\n", a, b, d);return 0; } //【ps】包含頭文件的方式: // <> :表示到系統路徑下去查找頭文件 // "" :表示在當前源文件的統計目錄下查找頭文件,找到了就用,沒找到就去系統路徑下找
- 方案一:通過 gcc 編譯器的參數指定要使用的庫
????????輸入以下指令,可指定要鏈接的庫:
gcc 源文件名 -I 頭文件的路徑 -L 庫文件的路徑 -l 庫文件路徑下的庫名//參數 -I:指定頭文件搜索路徑。
//參數 -L:指定庫文件搜索路徑。
//參數 -l:指明需要鏈接庫文件路徑下的哪一個庫。注:庫真實的名字為,去掉后綴.a 與前綴 lib之后
- 方案二:將頭文件和庫文件拷貝到系統路徑下?
? ? ? ? 輸入以下指令,可將頭文件和庫文件拷貝到系統路徑下 :
sudo cp 頭文件的路徑/* /usr/include/
sudo cp 庫文件的路徑 /lib64/
?【Tips】庫的安裝,其實就是把頭文件和庫文件拷貝到系統路徑下。
?
2.動態庫
2.1-打包
? ? ? ? 此處仍以上文中演示靜態庫的代碼為例:
//add.h #pragma onceextern int my_add(int x, int y);
//add.c #include "add.h"int my_add(int x, int y) {return x + y; }
//sub.h #pragma onceextern int my_sub(int x, int y);
//sub.c #include "sub.h"int my_sub(int x, int y) {return x - y; }
- 方案一:使用指令打包
? ? ? ? step1:使用 gcc 編譯器的參數 -fPIC ,將所有源文件生成對應的目標文件
????????【ps】參數 -fPIC(position independent code):產生位置無關碼。
- -fPIC作用于編譯階段,告訴編譯器產生與位置無關的代碼,此時產生的代碼中沒有絕對地址,全部都使用相對地址,從而代碼可以被加載器加載到內存的任意位置都可以正確的執行。這正是共享庫所要求的,共享庫被加載時,在內存的位置不是固定的。
- 如果不加-fPIC選項,則加載 .so 動態庫文件的代碼段時,代碼段引用的數據對象需要重定位,重定位會修改代碼段的內容,這就造成每個使用這個.so文件代碼段的進程在內核里都會生成這個 .so 文件代碼段的拷貝,并且每個拷貝都不一樣,取決于這個 .so文件代碼段和數據段內存映射的位置。
- 不加-fPIC編譯出來的 .so 是要在加載時根據加載到的位置再次重定位的,因為它里面的代碼BBS位置無關代碼。如果該.so文件被多個應用程序共同使用,那么它們必須每個程序維護一份.so的代碼副本(因為.so被每個程序加載的位置都不同,顯然這些重定位后的代碼也不同,當然不能共享)。
- 我們總是用 -fPIC 來生成 .so 動態庫文件,但從來不用 -fPIC 來生成 .a 靜態庫文件。但?.so 一樣可以不用-fPIC選項進行編譯,只是這樣的.so必須要在加載到用戶程序的地址空間時重定向所有表目。
? ? ? ? step2:使用 gcc 編譯器的參數 -shared ,將剛生成的所有目標文件打包成以 .so 為后綴的動態庫:
gcc -shared 動態庫文件名 目標文件名1 目標文件名2 ...
? ? ? ? step3:將源文件相應的頭文件與剛生成的動態庫組織起來,具體的方式是,將兩者放到同一個目錄下。
- 方案二:通過 make 和 Makefile 一鍵打包?
? ? ? ? 將方案一的指令全部寫到 Makefile 文件中,通過 make 一鍵打包。
? ? ? ? step1:編輯 Makefile 文件。
? ? ? ? step2:通過指令 make 一鍵生成目標文件和相應的靜態庫。
? ? ? ? step3:通過指令 make output 一鍵組織頭文件和靜態庫。
2.2-使用
? ? ? 此處仍以上文中演示靜態庫的代碼為例:
//main.c #include <stdio.h> #include <add.h> #include <sub.h> int main() {int a = 20;int b = 10;int c = my_add(a, b);int d = my_sub(a, b);printf("%d + %d = %d\n", a, b, c);printf("%d - %d = %d\n", a, b, d);return 0; } //【ps】包含頭文件的方式: // <> :表示到系統路徑下去查找頭文件 // "" :表示在當前源文件的統計目錄下查找頭文件,找到了就用,沒找到就去系統路徑下找
- 方案一:拷貝 .so 動態庫文件到系統共享庫路徑下?
sudo cp 動態庫文件的路徑 /lib64
- 方案二:更改環境變量 LD_LIBRARY_PATH
????????環境變量 LD_LIBRARY_PATH 中有程序鏈接動態庫時所要搜索的路徑,只需將自己的動態庫路徑添加到 LD_LIBRARY_PATH 中,即可鏈接自己的動態庫。
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH: 動態庫文件所在的目錄
- 方案三:配置 /etc/ld.so.conf.d/
????????/etc/ld.so.conf.d/ 路徑下存放的全部都是以 .conf 為后綴的配置文件,這些配置文件中存放的都是路徑,操作系統會自動在 /etc/ld.so.conf.d/ 下找所有配置文件中的路徑,以鏈接程序所需要的庫,所以,只需將自己的動態庫路徑添加 /etc/ld.so.conf.d/ 下,即可鏈接自己的動態庫。
? ? ? ? step1:將動態庫文件所在的目錄路徑,存入一個以 .conf 為后綴的文件中。
? ? ? ? step2:將含有動態庫文件目錄路徑的 .conf 文件拷貝到 /etc/ld.so.conf.d/ 目錄下。
? ? ? ? step3:使用指令 ldconfig 更新配置文件。之后就可以正常鏈接動態庫、正常運行程序了。