文章目錄
- 前言
- 一、軟硬鏈接
- 基本認知
- 實現原理
- 應用場景
- 取消鏈接
- ACM時間
- 二、動靜態庫
- 認識庫
- 庫的作用
- 三、制作靜態庫
- 靜態庫的打包
- 靜態庫的使用
- 四、制作動態庫
- 動態區的打包
- 動態庫的鏈接與使用
- 動態庫的鏈接原理
- 總結
前言
??我有款非常喜歡玩的游戲,叫做《饑荒》,現在我下載了這一款游戲,但我不會跑到游戲所在目錄中雙擊 .exe 打開游戲,大多數人都會通過桌面的快捷方式直接打開文件,而這個快捷方式實際就是對 .exe 的 軟鏈接 文件; 當你在游戲中加載地圖、道具等資源時,這些數據是存在 .exe 文件中的嗎? 答案是當然不是,這些資源文件都以 庫 的方式與 .exe 位于同一目錄中,通常為動態庫,在 Windows 中后綴為 dll,那么這些神奇的輔助文件是如何產生的? 其本質又是如何?本篇將帶你一起揭曉。
另外還有一個小要求,請先回顧一下我上篇文章的 inode 這個概念,這在本篇仍很重要
一、軟硬鏈接
基本認知
對文件進行軟鏈接
ln -s file.txt file-soft.link
對文件進行硬鏈接
ln file.txt file-hard.link
源文件,軟鏈接文件,硬鏈接文件如下:
注意: 可以對目錄進行軟鏈接,但不能對目錄進行硬鏈接,具體原因后面再解釋
??那么生成的軟硬鏈接有什么用呢?
??像源文件一樣使用即可,結果一模一樣(因為當前軟硬鏈接的都是同一個源文件)
同時我們發現這兩種鏈接方式其本質上是有很大差別的:
- 軟鏈接文件的 inode 編號與源文件不同(獨立存在),軟鏈接文件比源文件小得多,并且 軟連接文件 -> 源文件
- 硬鏈接文件與源文件共用一個 inode 編號(對源文件其別名),硬鏈接文件與源文件一樣大,并且硬鏈接文件與源文件的鏈接數變成了 2
軟鏈接文件依賴于源文件,而硬鏈接文件是源文件的別名
當我們將源文件刪除后,軟鏈接失效; 硬鏈接仍然有效,不過硬鏈接數變為了 1
??同樣是對源文件進行鏈接,為何兩種鏈接方式差別如此大呢? 這就不得不談一下它們的實現原理了
實現原理
??軟鏈接又稱為符號鏈接,它是一個單獨存在的文件,擁有屬于自己的 inode 屬性及相應的文件內容,不過在軟鏈接的 Data block 中存放的是源文件的地址,因此軟鏈接很小,并且非常依賴于源文件
??在這里以QQ為例,可以看到快捷方式(軟鏈接方式)中存放的是源文件的地址
??因此如果源文件被刪除了,那么在執行軟連接文件時,其中的地址就是一個無效地址(目標文件已丟失),此時就會報錯 No such file or directory
??假設只是單純的刪除軟連接文件,那么對源文件的內容沒有絲毫影響,就好比 桌面上的快捷方式,有的人以為將快捷方式(軟鏈接)文件刪除了,就是在 “卸載” 軟件,其實不是,如果想卸載軟件,直接將其源文件相關文件夾全部刪除即可
有多少人到現在還有誤解呢?
??硬鏈接并非創建一個相同的文件進行鏈接,而是在源文件所目錄下的 (inode編號 & 文件名) 對應表中,新增 inode 編號與 硬鏈接文件名 的映射關系,并將 inode 結構體中的引用計數 +1,表示當前已成功硬鏈接上了一個文件
??當刪除當前 對應文件時,會 先判斷 ref_count 是否為 1,如果是,才會將文件內容及其屬性真正刪除,否則刪除的只是 文件名 與 inode 編號的映射關系inode
??這也就解釋了為什么刪除源文件后,硬鏈接文件不受任何影響,僅僅只是 硬鏈接數 - 1,同理,刪除硬鏈接文件,也不會影響源文件
??為什么新建目錄的硬鏈接數為 2 ?
- 因為一個目錄在新建后,其中默認存在兩個隱藏文件:. 與 …
- 其中 . 表示當前目錄,… 表示上級目錄
??Linux 中的目錄結構為多叉樹,即當前節點(目錄)需要與父節點(上級目錄)、子節點(下級目錄)建立鏈接關系,并且還得知道當前目錄的地址,否則就會導致切換目錄時出現錯誤
??為了避免因用戶的誤操作而導致的目錄環狀問題,規定用戶不能手動給目錄建立硬鏈接關系,只能由 OS 自動建立硬鏈接,比如新目錄后,默認與上級目錄和當前目錄建立硬鏈接文件,在當目錄下創建新目錄后,當前目錄的硬鏈接數 + 1
所以說,將目錄的硬鏈接數 - 2 ,得到的數字就是該目錄下的目錄數
??4 - 2 = 2,所以目錄 gitQuest 下一共有2個目錄,我們來驗證一下:
應用場景
??軟鏈接可以當作快捷方式使用,比如快速運行一個藏的很深的可執行程序
??而硬鏈接一是可以用來當作目錄移動的工具,二是可以用來給重要的源文件起別名并使用,一旦發生刪除等不可逆行為時,可以確保源文件的安全
??注意: 硬鏈接并不是將源文件直接進行備份,而是新建立 inode 編號與硬鏈接文件名的映射關系,同時 struct inode 中的引用計數 ref_count++,只有當 ref_count == 1 時才會真正刪除文件內容及屬性, 否則都只是在取消映射關系和 ref_count–
取消鏈接
取消鏈接的方式有兩種:
- 直接刪除鏈接文件
- 通過 unlink 取消鏈接關系
ACM時間
??每一個文件都有三個時間:訪問 Access、修改屬性 Change、修改內容 Modify,簡稱為時間ACM
??可以通過 查看指定文件的 時間信息 statACM
這三個時間的刷新策略如下:
- Access:最近一次查看內容的時間,具體實現取決于系統
- Change:最近一次修改屬性的時間
- Modify:最近一次修改內容的時間(內容更改后,屬性也會跟著修改)
??Access 是高頻操作,如果每次查看都更新的話,會導致 效率變低,因此 實際變化取決于刷新策略:查看 N 次后刷新IO
??注意: 修改內容一定會導致屬性時間被修改,但不一定會導致訪問時間被修改,因為可以不打開文件,對文件進行操作
二、動靜態庫
認識庫
常見的庫文件:stdio.h stdlib.h string.h等
庫分為 動態庫 和 靜態庫
- Linux 中,.a 后綴為靜態庫,.so 后綴為動態庫
- Windows 中,.lib 后綴為靜態庫,.dll 后綴為動態庫
- 雖然不同環境下的后綴有所不同,但其工作原理是一致的
庫命名
- 比如 libstdc++.so.6
- 去掉前綴跟后綴,最終庫名為 stdc++
??查找當前環境的庫文件:
find /usr/lib64/libc*
??C++ 中具體庫文件可以這樣查看:
find /usr/lib64/libstdc*
??在編寫程序時,一定離不開庫文件,動態庫優勢比靜態庫明顯,因此在編譯代碼時,默認采用動態鏈接的方式,如果想指定為靜態鏈接編譯,只需要在 gcc/g++ 語句后面加上 -static 即可(前提是得有靜態庫)
??關于動靜態庫的優缺點可以看看下面這個表格
區別 | 動態庫 | 靜態庫 |
---|---|---|
調用方式 | 通過函數位置進行調用 | 直接將需要的函數拷貝至程序中 |
依賴性(運行時) | 需要依賴于動態庫 | 可以獨立于靜態庫運行 |
空間占用 | 共享動態庫中的代碼,空間占用少 | 拷貝代碼會占用大量空間 |
加載速度 | 調用函數,加載速度慢 | 直接運行,加載速度快 |
??注意: 靜態庫是將所需要的函數代碼拷貝到源文件中直接使用,而動態庫是通過動態鏈接的方式,進行函數鏈接使用,別急,這個我們后面會再細細講解
庫的作用
- 提高開發效率
- 頭和庫是有對應關系的,需要組合使用
- 頭文件在預處理階段就已經引入了,鏈接的本質就是在鏈接庫
??簡言之,如果沒有庫文件,那么你在開發時,需要自己手動將 等高頻函數編寫出來,因此庫文件可以提高我們的開發效率,比如 中就有很多現成的庫函數可以使用,效率很高,如:printf
我們在IDE環境下編碼的時候,語法提示是如何做到的?
- 安裝開發環境,實際上是在安裝編譯器、開發語言配套的庫和頭文件
- 編譯器的 語法提示功能來源于頭文件(語法提示其實就是搜索)
我們在寫代碼時,開發環境是怎么知道語法錯誤或其他錯誤的?
- 編譯器有命令行模式,還有其他自動化模式,編寫代碼時,不斷進行主動編譯,排查錯誤
三、制作靜態庫
??現在有一些簡單的計算 函數,能滿足整型的 計算
??主函數中將對這些自定義的庫函數進行調用
靜態庫的打包
??一共分為兩步:
- 將源文件進行 預處理 -> 編譯 -> 匯編,生成可鏈接的二進制 .o 文件
- 通過指令將 .o 文件打包為靜態庫
??將文件編譯為 .o 二進制文件
gcc -c add.c sub.c
??將所有的 .o 文件打包為一個靜態庫(庫名自定義),其中的 為庫名mycalc
ar -rc libmycalc.a *.o
ar -tv 靜態庫文件
該指令可以查看打包的庫文件
??獲得靜態庫后,就可以進行使用了
靜態庫的使用
方法一:通過指定路徑使用靜態庫
??如果直接編譯程序,會出現編譯失敗的情況,因為編譯器不認識第三方庫(需要提供第三方庫的路徑及庫名)
第一方庫:語言提供
第二方庫:操作系統提供
第三方庫:other 提供的庫,比如當前我們直接打包的靜態庫
對于自己寫的的第三庫的使用,需要標注三個參數:
- -I 所需頭文件的路徑 需要將所需頭文件的路徑加上,此處為 ./stdc/include
- -L 所需庫文件的路徑 這里加的是庫文件的路徑,也為 ./stdc/lib
- -l 待鏈接靜態庫名 所需要鏈接的靜態庫名字,這里為 libmycalc.a
將選項加上后重新編譯
方塊二:將頭文件和靜態庫文件安裝至系統目錄中
??除了這種比較麻煩的指定路徑編譯外,我們還可以將頭文件與靜態庫文件直接安裝在系統目錄中,直接使用,無需指定路徑(需要指定靜態庫名)
??所謂的安裝軟件,就是將自己的文件安裝到系統目錄下
sudo cp ./stdc/include/*.h /usr/include/
sudo cp ./stdc/lib/*.a /lib64/
??注意: 將自己寫的文件安裝到系統目錄下是一件危險的事(導致系統環境被污染),用完后記得手動刪除
四、制作動態庫
動態區的打包
??動態庫不同于靜態庫,動態庫中的函數代碼不需要加載到源文件中,而是通過 與位置無關碼 ,對指定函數進行鏈接使用
動態庫的打包也同樣分為兩步:
- 編譯源文件,生成二進制可鏈接文件,此時需要加上 -fPIC 與位置無關碼
- 通過 gcc/g++ 直接目標程序(此時不需要使用 ar 歸檔工具)
??將源文件編譯為 .o 二進制文件,此時需要帶上 fPIC 與位置無關碼
??將所有的 .o 文件打包為動態庫(借助 gcc/g++)
gcc -o libmycalc.so *.o -shared
??獲得動態庫后,就可以進行使用了
動態庫的鏈接與使用
??像使用靜態庫一樣使用動態庫(指定路徑及庫名),編譯成功,但運行失敗!
??為什么會出現這種問題? 因為當前只告訴了編譯器動態庫的位置,沒有告訴 OS
通過 查看程序鏈接情況:ldd
運行時, 是如何鏈接動態庫?OS
- 環境變量 LD_LIBRARY_PATH (默認沒有這個環境變量),將第三方動態庫路徑添加至此環境變量中(臨時方案)
- sudo 在 /lib64/ 目錄下建立動態庫的軟鏈接
- 更改配置文件 /etc/ld.so.conf.d 這個目錄中都是各種動態庫配置文件,創建文件 xx.conf 至目錄中(文件中存儲的是第三方動態庫的路徑)ldconfig 令配置文件生效
方法一:通過環境變量解決
??添加動態庫路徑至 環境變量中LD_LIBRARY_PATH
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:路徑
??環境變量 是程序在進行動態庫查找時的默認搜索路徑LD_LIBRARY_PATH
注意: 更改環境變量只是臨時方案,重新登錄后會失效
方法二:將動態庫的軟鏈接文件存入系統目錄中
sudo ln -s 路徑 /lib64/libmycalc.so
??注意: 創建軟連接文件時,需要使用絕對路徑!
方法三:更改配置文件中的信息
echo 路徑 > lhq.conf
sudo mv lhq.conf /etc/ld.so.conf.d/
sudo ldconfig # 手動更新
??注意: 后兩種方法都可以做到永久生效(因為存入了系統目錄中),但在使用完后最好刪除,避免污染系統環境
動態庫的鏈接原理
??程序在鏈接動態庫函數時,是通過 動態庫起始地址 + 所鏈接函數偏移量 的方式進行鏈接訪問的,而這個偏移量就是 fPIC 與位置無關碼
??地址其實就兩種:絕對地址和相對地址,靜態鏈接時,將可鏈接的二進制文件加載至程序中,直接通過 絕對地址 進行鏈接,假設函數被調用了多次,就會導致代碼冗余等問題; 動態鏈接采用 相對地址 的方式進行鏈接,同一個函數的 + 值相同,代碼只需要加載一份,并且可以任意位置進行函數調用(與位置無關)動態庫起始地址所鏈接函數偏移量
??動態庫中所有地址都是偏移量,默認從 開始0
??只有當一個庫被真正映射進地址空間后,它的起始地址才能真正確定
- 鏈接庫中的函數時,通過 動態庫的起始地址 + 函數偏移量 的方式鏈接函數
- 這種方法不論在什么位置,都可以隨便鏈接函數(與位置無關)
- 與位置無關碼:動態庫中地址,是偏移量
總結
最后還有需要總結的一些要點就是:
- 當同時擁有 靜態庫 和 動態庫 時,默認采用動態鏈接
- 可以在編譯的時候最后加上 -static 指定 靜態鏈接
- 只有靜態庫,又不指定靜態鏈接,這個時候是動態鏈接(內含靜態庫)
- 靜態鏈接生成的程序比動態鏈接大得多,并且內含靜態庫的動態鏈接程序,也比純粹的動態鏈接程序大,程序并非非靜即動
??另外,你可不可以利用 Makefile 來簡化步驟?