目錄
前言:
一、 磁盤
(一)磁盤的物理結構
(二)磁盤的物理存儲結構
1. 數據存儲
2. 存儲結構
二、磁盤的邏輯抽象
三、磁盤信息
(一)具體結構
(二)重新認識目錄?
四、理解文件系統中的增刪查改
五、軟硬鏈接
(一)軟鏈接
?(二)硬鏈接
(三)二者區別
實現原理:?
(四)取消鏈接
(五)ACM時間
六、動靜態庫
(一)什么是庫
1. 庫的作用
(二)制作一個靜態庫
?(三)靜態庫的使用
1. 通過指定路徑使用靜態庫
2.?將頭文件和靜態庫文件安裝至系統目錄中
(四)制作一個動態庫
(五)動態庫的使用
2. 建立軟鏈接
3. 更改配置文件
七、動靜態庫的加載
(一)靜態庫的加載
(二)動態庫的加載
1. 加載過程
2. 動態庫地址的理解
八、動態庫知識補充
總結:
前言:
文件分為 內存文件 和 磁盤文件。磁盤文件,這是一個特殊的存在,因為它不屬于馮諾依曼體系,而是位于專門的存儲設備中,因此 磁盤文件 存在的意義是將文件更好的存儲起來,以便后續對文件進行訪問。在高效存儲 磁盤文件 這件事上,前輩們研究出了十分巧妙的管理手段及操作方法,而這些手段和方法共同構成了我們今天所談的 文件系統。
文件系統是操作系統中負責管理持久數據的子系統,簡單點就是負責把用戶的文件存到磁盤硬件中,因為即使計算機斷電了,磁盤里的數據并不會丟失,所以可以持久化的保存文件。
文件系統的基本數據單位是文件,它的目的是對磁盤上的文件進行組織管理,那組織的方式不同,就會形成不同的文件系統。
一、 磁盤
(一)磁盤的物理結構
現在市面上的磁盤主要分為?機械硬盤?和?固態硬盤,前者讀取速度慢,但便宜、穩定;后者讀取速度快,但價格高昂且數據易損,兩者各有其應用場景,本文主要介紹的是?機械硬盤。磁盤的是計算機中唯一一個機械設備,同時它還是一個外設,其圖片如下:
根據?馮諾依曼體系結構,機械硬盤?在速度上遠遠慢于?CPU
?和?內存?
舉例機械硬盤有多慢
- 假設?
CPU
?運行速度是納秒級,那么內存就是微秒級,而機械硬盤只不是是毫秒級
為何?機械硬盤?如此慢?這與它的結構有很大關系
其主要的核心物理結構有三個:
- 磁盤片:磁盤片是硬盤中承載數據存儲的介制,磁盤是由多個盤片疊加在一起,互相之間由墊圈隔開的。
- 盤面:一片磁盤片是由兩面的,每一面被稱為盤面,磁盤片的兩面都可以存儲數據的。每一個盤面都有對應的磁頭,也就是說一個磁盤片有兩個磁頭的。
- 磁頭:磁頭是向磁盤讀取數據的媒介,其通過磁性原理讀取磁性介質上數據。所以磁頭不與盤面接觸,磁頭懸浮在盤面上面或下面。其中還有重要的一點是:磁頭是共進退的!
- ....
機械設備?控制是需要時間的,因此導致?機械硬盤?讀寫數據速度相對于?
CPU
?和?內存?來說比較慢
(二)磁盤的物理存儲結構
1. 數據存儲
總所周知,數據是以?0
?和?1
?的方式進行存儲的,常見的存儲介質有:強信號與弱信號、高電平與低電平、波峰與波谷、南極與北極?等,而盤面上比較適合的是?南極與北極。
當磁頭移動到指定位置時
- 向磁盤寫入數據:
N->S
- 刪除磁盤中的數據:
S->N
磁盤中讀寫的本質:更改基本元素的南北極、讀取南北極
注意:?磁頭并非與盤面進行直接接觸,而是以?
15
?納米的超低距離進行磁場更改????????這個距離相當于一架波音747距離地面1米進行超低空飛行,所以如果磁頭制作工藝不夠精湛,可能會導致磁頭在寫入/讀取數據時,與盤面發生摩擦(高速旋轉)發熱,從而導致磁場消失,該扇區失效,數據丟失。
????????所以機械硬盤?不能在其運行時隨意移動,因為角度的偏轉也有可能導致發生摩擦,造成數據丟失,更不能用力拍打?機械硬盤
2. 存儲結構
在磁盤的盤面上,磁盤被一個個的同心圓以及射線進行分割,從而出現了:磁道,?扇面,扇區
- 扇區: 被一個個的同心圓以及射線進行分割出的一個個扇形區域。
- 扇面: 兩條相鄰的射線之間夾的所有扇區構成扇面。
- 磁道:盤面上半徑相同的扇區構成一個磁道。
- 柱面:由于現實世界中磁盤的立體結構,所以把空間中所有半徑相同的磁道定義為一個柱面
其中扇區是存儲的基本單元, 每個扇區其大小為:512 byte
或?4kb
?,一般來說都是512 byte
(下面我們討論時也是以512 byte
為準)
由于扇區是最小的存儲單元,所以在硬件的角度:一個文件(內容 + 屬性)無非是占用一個或多個扇區進行數據的存儲。
那么在硬件上磁盤是怎么定位一個扇區的呢? —— 答案是CHS定位法! cylinder柱面 head磁頭 sector扇區
- 磁盤中的磁頭是有編號的,我們首先根據扇區所在的盤面先確定使用幾號磁頭。
- 每個扇區都有自己所在的磁道,根據扇區所在的磁道就可以確定磁頭的偏移位置。
- 每一個扇區在所在的扇面上都已經被編好了號碼,磁頭最后根據扇面所在的號碼確定扇區。
我們既然能夠通過CHS定位一個扇區,那么也能定位多個扇區,從而將文件能夠從硬件的角度進行讀取和寫入!
二、磁盤的邏輯抽象
有了上面的知識我們知道能夠通過CHS去定位一個文件的基本單元,但是操作系統是不是采用這種方式去定位磁盤中的數據呢?答案是不是!?
這主要有以下兩點原因:
- 操作系統是軟件,磁盤是硬件,硬件通過CHS定位扇區,操作系統如果采用和硬件一樣的定位方式就會和硬件之間產生很大的耦合關系,如果我們的硬件變了(例如:機械磁盤變為固態硬盤),那么操作系統也要進行變化,這并不是一個好的情況。
- 扇區的大小是512 byte ,以扇區為單位進行IO時的數據量太小了,在進行大量IO時這會極大的影響到運行的速度。
操作系統實際進行IO時,是以
4kb
為單位的(這個大小是可以調整的)4kb = 512 * 8 byte
?
因此將8個扇區定義為一個塊,操作系統按照一個塊的單位進行IO。
磁盤片的物理結構是一個圓環型結構,假設我們能夠將每一個盤面按照磁道進行拉伸展開(就像使用膠布一樣),那不就變成了一個線性結構了嗎?
展開以后對于每一個磁道里面都有許多扇區,這些扇區組合起來便可以被抽象為一個數組!
但是這個數組太大了,而且每一個單位的數據量也有點太小了,我們還要對其進行抽象,我們將8個扇區組成一個塊,這樣數組的長度就縮短了8倍,經過這一層抽象后,由原來的一個扇區數組變為塊數組。
其中邏輯塊的數組下標被定義為邏輯塊地址 (LBA地址) ,現在 OS 想訪問具體的扇區時,只需通過 起始扇區的地址 + 偏移量 就可以獲取 LBA 地址,然后通過特定手段轉為 CHS 地址,交給外設進行訪問即可 LBA和CHS轉換,這個操作的原理類似于指針的解引用。
于是操作系統通過LBA地址進行訪問存儲的數據,這就是操作系統對磁盤等存儲硬件的邏輯抽象。
因此對于外設中文件的管理,經過 先描述,再組織 后,變成了對數組的管理,這個數據就是 task_struct 中的 struct block。
最后我們就能理解為什么 IO 的基本單位是 4 kb 了,因為直接讀取一個數據塊(4 kb),這樣可以提高 IO 效率(內存對齊)。
三、磁盤信息
(一)具體結構
經過上面的抽象我們操作系統便拿到了一個邏輯塊的大數組,但是這個數組太大了,我們對這個大數組直接管理還是太過于困難了,于是我們操作系統就可以對這個大數組進行分區管理(類似于windows的分盤),當我們管理好了一個分區就可以將一個分區的管理方法復制到其他的分區中,從而實現了全局的數據管理。
但是每個分區的數據還是太大了,操作系統還要對每一個區進行分組,通過分組再次降低管理的難度,其中每個分區其內部的結構如下圖:?
- Boot Block: 里面存放的是與操作系統啟動相關的內容,諸如:分區表,操作系統鏡像的地址等,一般這個區域被放在磁盤的0號磁頭,0號磁道,1號扇區中,如果這個區域的內容受到破壞,那么操作系統將無法啟動!
- 超級塊(Super Block):存放文件系統本身的結構信息。記錄的信息主要有:bolck 和 inode的總量,未使用的bolck 和inode的數量,一個bolck 和inode的大小,最近一次掛載的時間,最近一次寫入數據的時間,最近一次檢驗磁盤的時間等其他文件系統的相關信息。Super Block的信息被破壞,可以說整個文件系統結構就被破壞了。
- GDT,Group Descriptor Table:塊組描述符,描述塊組屬性信息。
- 塊位圖(Block Bitmap):Block Bitmap中記錄著Data Blocks中哪個數據塊已經被占用,哪個數據塊沒有被占用。
- inode位圖(inode Bitmap):每個bit位表示對應的inode是否空閑可用。
- i節點表(inode Table):一般來說,一個文件內部的所有屬性被保存在一個inode節點中(一個inode節點的大小一般是128字節),一個分區會存在大量的文件,所以一個分區中會有大量的inode節點,每一個inode節點都有自己的編號,這個編號也屬于文件屬性。為了更好的管理inode節點,就要有一個專門的區域存放所有的inode節點,這個區域就是inodeTable,其內部可以看成一個數組結構。
- 數據區(Date blocks):里面是大量的數據塊,每一個數據塊都可以用來存放文件內容。
細節注意要點:
- 每一個塊組都有Block Bitmap inode Bitmap inode Table Date blocks,其他的部分每個塊組里面可能沒有。
- Super Block 在每一個塊組里都可能存在,也可能不存在但至少要有一個塊組存在超級塊!而且每一個存在超級塊的塊組里面的超級塊是一樣的,并且所有存在超級塊的塊組其里面的超級塊是聯動更新的。
- 超級塊存在多份的意義是:萬一其中一個超級塊損壞,還有其他超級塊可以使用,并且可以利用其他完好的超級塊去修復已損壞的超級塊。不至于一個超級塊損壞導致整個分區的文件系統直接損壞。
- inode節點中有一個數組,這個數組里面存放了對應文件使用的數據塊的編號。
- inode編號不能跨分區使用,每一個inode編號在一個分區內唯一有效!
- 根據inode可以確定一個分區內的分組
(二)重新認識目錄?
?在Linux
的命令行中,我們可以使用ls -il
命令可以看到文件的inode編號:
其實在Linux中系統對于文件只認識inode編號,并不認識文件名,那你可能會很好奇:我們平時一直使用的都是文件名,沒有使用過inode編號為什么我們還能夠操作文件呢?
這其實和目錄有關!我們的打開任意一個文件都是在一個目錄里面打開的,而且目錄本身也是文件,目錄也有inode編號!其里面也有內容,也需要數據塊,其里面的內容是:該目錄下文件名與該文件的inode映射關系。
因此當我們使用文件名時,目錄會自動幫我們找到對應的inode編號,完成相應的要求。
例如我們在Linux下使用cat xxx.xx 命令,其大致的執行過程是:
- 在目錄下找到log.txt 的inode編號。
- 利用inode編號在inode Table中找到inode。
- 根據inode找到 xxx.xx文件 使用的數據塊的編號。
- 根據數據塊的編號找到數據塊,將內容提取出來并刷新到顯示器上面。
?
四、理解文件系統中的增刪查改
查:見上面的cat xxx.xx 文件
的例子?
刪 :
- 根據當前要刪除的文件名到目錄中找到對應的inode編號。
- 根據inode編號到inode Table中找到inode節點。
- 根據inode節點中的內容找到該文件對應的Block Bitmap,然后將相應的bit進行置0表示內容的刪除。
- 根據inode編號將inode bitmap 對應的bit進行置0表示屬性的刪除。
- 將當前目錄中inode 編號與文件名的映射關系進行刪除。
?
增:(創建一個內容為空的文件)
- 操作系統在inode bitmap 中從低向高依次掃描,將找到的第一個bit為
0
的位置置成1
。 - 然后在inode Table?中的對應位置寫入新的屬性。
- 然后向當前目錄中增加新的inode 編號與文件名的映射關系。?
改:
- 根據當前的文件名到目錄中找到對應的inode 編號。
- 根據inode編號到inode Table中找到inode節點。
- 計算需要的數據塊的個數,在Block bitmap中找到未使用的數據塊,并將相應的bit由
0
置成1
。 - 將分配給文件的數據塊的編號填入inode中。
- 將數據寫入到數據塊中
?
補充細節:
-
如果文件被誤刪了,該怎么辦?數據應該怎么被恢復?(這里我們只討論大致的原理)
答案是:最好什么都不做,因為Block bitmap被置為0
以后,相應的數據塊已經不受保護了,此時再創建新文件就有可能覆蓋原來的文件。 -
數據恢復的原理是:Linux系統為我們提供了一個日志,這個日志里面的數據會根據時間定期刷新,所有被刪除的文件的inode 編號在這里都有記錄,通過被刪除文件的inode 編號,先把inode bitmap相應的位置的bit由
0
置成1
,
然后根據inode 編號到inode Table中找到對應的數據塊編號,然后到Block bitmap中將相應位置的bit由0
置成1
。 -
上面我們說的分區,分組,填寫系統屬性是誰在做,什么時候做的呢?
答案是:是操作系統在做!是在格式化的時候做的!在我們操作系統分區完成以后,為了能讓分區能夠正常使用,需要對分區進行格式化,格式化的本質就是:操作系統向分區內寫入文件系統管理屬性的信息! -
inode里面只是用數組來與數據塊進行單純的一 一映射嗎?
答案是:并不是的,如果一個inode 里面存放數據塊的編號的數組大小是15,如果只是單純的一 一映射 ,那么一個文件只能存儲15 * 4KB = 60 KB
的內容。這顯然是不合理的!
所以inode里面存放數據塊的編號的數組被規定它的前幾個下標是直接索引,中間幾個是二級索引,后面幾個的三級索引, …
直接索引:直接指向數據塊。
二級索引:指向一個數據塊,這個數據塊里面的內容是直接索引。
三級索引:指向一個數據塊,這個數據塊里面的內容是二級索引。二級索引對應的數據量單位:
4KB / 4 * 4KB = 4 MB
。
三級索引對應的數據單位:(4KB / 4 )^2 * 4KB = 4G?
。 -
有沒有一種可能一個分組,數據塊沒用完,inode沒了,或者inode沒用完,數據塊用完了?
答案是:有可能的,如果我們一直創建空文件,就可能導致inode 使用完畢,而數據塊沒有使用完,如果我們的所有內容都放在一個文件中,就可能導致inode 沒有使用完,而數據塊使用完了。
五、軟硬鏈接
(一)軟鏈接
當我們有一個文件在一個很深的目錄時,我們是不方便去使用這個文件的,那有沒有一種方式能夠讓我們能夠輕松的找到并使用這個文件呢,有的,那便是軟鏈接。軟連接非常類似于windows
中的快捷方式。
我們可以在當前的目錄里面建立一個軟連接,其中軟鏈接文件名可以自定義,來讓我們能夠更加方便的去使用mytest 可執行程序
ln -s 文件名 軟鏈接名
可以看到,執行軟鏈接跟執行源可執行程序沒有任何差別:
?(二)硬鏈接
生成硬鏈接文件就更簡單了,對文件 mytest 進行硬連接,生成硬連接文件?my-hard-link?,其中硬鏈接文件名也可以自定義:
ln 文件名 硬連接名 // 不帶參數默認是硬鏈接
可以看到,執行軟鏈接跟執行源可執行程序沒有任何差別:?
?
(三)二者區別
當我們進行創捷一個文件時,在文件權限后面會有一個數字,這個數字就是硬鏈接數。我們查看一下他們的inode編號:
我們可以發現它們的編號并不相同,源程序跟硬鏈接編號一樣,但軟鏈接就不一樣。
區別一:
- 軟鏈接文件的?inode?編號與源文件不同(獨立存在),軟連接文件比源文件小得多,軟連接的內容就是自己所指向的文件的路徑
- 硬鏈接文件與源文件共用一個?inode 編號(對源文件其別名),硬鏈接文件與源文件一樣大,并且硬鏈接文件與源文件的鏈接數變成了?
2
我們再給 mytest 創建一個硬鏈接,并且可以發現源文件的硬鏈接數+1了,同時也再次證實了硬鏈接與源文件?inode?一樣:
實現原理:?
?那為什么源文件跟硬鏈接inode編號一樣并且源文件硬鏈接數會+1捏?與實現原理有關:
當我們創建硬鏈接時,操作系統在當前目錄里面建立新的映射關系,操作系統把原文件的inode編號與硬鏈接建立映射關系,此時一個inode編號就有了兩個文件名,同時在inode節點中會有一個引用計數的變量ref_count,當我們建立一個硬鏈接時,這個引用計數的變量就會自增一下,表示硬鏈接數目加一:
軟鏈接又稱為符號鏈接,它是一個單獨存在的文件,擁有屬于自己的?inode
?屬性及相應的文件內容,不過在軟連接的?Data block
?中存放的是源文件的地址,因此軟連接很小,并且非常依賴于源文件。
我們分別刪除掉源文件,看看軟硬鏈接有什么區別:
?區別二:
- 當我們將源文件刪除后,軟連接失效,因為軟鏈接文件依賴于源文件
- 當我們將源文件刪除后,硬鏈接仍然有效,因為硬鏈接文件是源文件的別名
?原理:
假設只是單純的刪除軟連接文件,那么對源文件的內容沒有絲毫影響,就好比 windows?桌面上的快捷方式,有的人以為將快捷方式(軟鏈接)文件刪除了,就是在 “卸載” 軟件,其實不是,如果想卸載軟件,直接將其源文件相關文件夾全部刪除即可。
當我們刪除一個文件時,目錄會正常幫我們刪除文件名與inode的映射關系,但是操作系統不一定會為我們刪除原文件,操作系統會將該文件對應的inode節點中的引用計算變量也會自減一下,如果減完之后等于0就刪除文件,否則只是修改了引用計數變量。?
這也就解釋了為什么刪除源文件后,硬鏈接文件不受任何影響,僅僅只是 硬鏈接數 -?1
,同理,刪除硬鏈接文件,也不會影響源文件。
為什么新建目錄的硬鏈接數為?2
?
- 因為一個目錄在新建后,其中默認存在兩個隱藏文件:
.
?與?..
- 其中?
.
?表示當前目錄,..
?表示上級目錄
?Linux
?中的目錄結構為多叉樹,即當前節點(目錄)需要與父節點(上級目錄)、子節點(下級目錄)建立鏈接關系,并且還得知道當目錄的地址,否則就會導致切換目錄時出現錯誤。
為了避免因用戶的誤操作而導致的目錄環狀問題,規定用戶不能手動給目錄建立硬鏈接關系,只能由?OS
?自動建立硬鏈接,比如新目錄后,默認與上級目錄和當前目錄建立硬鏈接文件,在當目錄下創建新目錄后,當前目錄的硬鏈接數 +?1:
操作系統拒絕了我們的請求,操作系統不允許給一個目錄建立硬鏈接,因為給目錄建立硬鏈接可能導致環路路徑問題?。
一般來說,一個目錄文件的硬鏈接數?-2?就是該目錄下的目錄個數。?
(四)取消鏈接
取消鏈接的方式有兩種:
- 直接刪除鏈接文件
- 通過?
unlink
?取消鏈接關系
(五)ACM時間
每一個文件都有三個時間:訪問?Access
、修改屬性?Change
、修改內容?Modify
,簡稱為?ACM
?時間
可以通過?stat
?查看指定文件的?ACM
?時間信息
這三個時間的刷新策略如下:
Access
:最近一次查看內容的時間,具體實現取決于系統Change
:最近一次修改屬性的時間Modify
:最近一次修改內容的時間(內容更改后,屬性也會跟著修改)
Access
是高頻操作,如果每次查看都更新的話,會導致?IO
?效率變低,因此?實際變化取決于刷新策略:查看?N
?后次刷新
注意:?修改內容一定會導致屬性時間被修改,但不一定會導致訪問時間被修改,因為可以不打開文件,對文件進行操作,比如直接重定向到文件:echo "...." xxx.xx
六、動靜態庫
(一)什么是庫
簡單來說:庫是一些可重定向的二進制文件,這些文件在鏈接時可以與其他的可重定向的二進制文件一起鏈接形成可執行程序。
一般來說庫被分為靜態庫和動態庫,他們是有不同的后綴來進行區分的。
系統平臺 | 靜態庫 | 動態庫 |
---|---|---|
Windows | .lib | .dll |
Linux | .a | .so |
另外對于C/C++來說其庫的名稱也是有規范要求的,例如在Linux
下:一般要求是 lib +?庫的真實名稱 +(版本號)+ .so /.a + (版本號)
,版本號是可以省略不寫的。
-
比如 libstdc++.so.6 ,去掉前綴跟后綴,最終庫名為?
stdc++
-
libc-2.17.so,去掉前綴跟后綴,最終庫名為 c
有了上面的一點基礎知識以后我們就能夠去見一見庫了,Linux
系統在安裝時已經為我們預裝了C&C++的頭文件和庫文件。
對于C/C++頭文件在Linux
里面一般在/usr/include
目錄下面存放我們的頭文件:
?對于C/C++的庫文件,一般在?/usr/lib64?和?/lib64?里面,/lib64里面給的是root和內核所需so或者a之類的庫文件,而?/usr/lib64?是普通用戶能夠使用的。
1. 庫的作用
提高開發效率
系統已經預裝了?C/C++
?的頭文件和庫文件,頭文件提供說明,庫文件提供方法的實現
- 頭文件提供方法說明,庫提供方法的實現,頭和庫是有對應關系的,是要組合在一起使用的
- 頭文件是在預處理階段就引入的,程序在鏈接時鏈接的本質其實就是鏈接庫!
如果沒有庫文件,那么你在開發時,需要自己手動將?printf
?等高頻函數編寫出來,因此庫文件可以提高我們的開發效率,比如?Python
?中就有很多現成的庫函數可以使用,效率很高。
- 我們在使用像vs2019這樣的編譯器時要下載并安裝開發環境,這其中是在下載什么?安裝編譯器軟件,安裝要開發的語言配套的庫和頭文件。
- 我們在使用編譯器,都會有語法的自動提醒功能,但是都需要先包含頭文件,這時為什么呢?
語法提醒本質是編譯器或者編輯器,它會自動的將用戶輸入的內容,不斷的在被包含的頭文件中進行搜索,自動提醒功能是依賴頭文件而來的!- 我們在寫代碼的時候,我們的環境怎么知道我們的代碼中有哪些地方有語法報錯,哪些地方定義變量有問題?
不要小看編譯器,編譯器有命令行的模式,還有其他自動化的模式,編輯器或集成開發環境可以在后臺不斷的幫我們調用編譯器檢查語法而不生成可執行文件,從而達到語法檢查的效果。
(二)制作一個靜態庫
庫的使用能夠提高我們的開發效率,接下來我們來制作一個庫!
//myadd.h
#pragma once
int myadd(int x, int y);//myadd.c
#include "myadd.h"
int myadd(int x int y)
{return x + y;
}//mysub.h
#pragma once
int mysub(int x, int y);//mysub.c
#include "mysub.h"
int mysub(int x, int y)
{return x - y;
}//main.c
#include <stdio.h>
#include "myadd.h"
#include "mysub.h"int main()
{int a = 10, b = 20;printf("%d + %d = %d\n", a, b, myadd(a, b));printf("%d - %d = %d\n", a, b, mysub(a, b));return 0;
}
我們將庫的頭文件與庫的實現文件放在了mylib 文件夾里面了,將?main.c
?放在了?otherPerson 里面,此時?main.c
?與庫的頭文件以及實現文件不在一起,此時編譯會報錯。
提示我們找不到頭文件,就算我們將頭文件移過去也會有鏈接錯誤。如果我們想讓其他人調用自己程序的一些功能,但是不想把源代碼交給其他人,則可以把自己的程序經過預處理、編譯、匯編,生成?.o?文件,即可重定位目標二進制文件,交給別人使用。
gcc -c myadd.c mysub.c main.c
再把實現功能的源代碼與二進制文件、使用功能的程序分別分離到 mylib 與 otherPerson 目錄中,把?main.c?文件也使用指令?gcc -c?生成?.o??文件,再與其他?.o?文件進行鏈接,最后生成可執行文件:
同樣執行成功。
上面的整個過程就是我們制作靜態庫的基本流程,當然這樣的制作其實還是有缺陷的,當我們的項目文件過于龐大時,我們要給一個.c
文件十幾個這樣的.o
文件,而且文件過于分散了,不利于管理,于是我們就需要將多個這樣的.o
文件打成一個包,我們將這個包直接給別人,別人就能直接使用了。
打包的命令是:
ar -rc [lib庫名.a] [*.o]
- ar 命令用于建立或修改備存文件,或是從備存文件中抽取文件。可集合許多文件,成為單一的備存文件,在備存文件中,所有成員文件皆保有原來的屬性與權限。
- r :如果打包好的 xxx.a 庫中沒有 xxx.o 那么就會把模塊 xxx.o 添加到庫的末尾,如果有的話就會替換之(位置還是原來的位置)。
- c :建立備存文件。
?(三)靜態庫的使用
1. 通過指定路徑使用靜態庫
在我們實際使用庫時,我們一般將頭文件放在一個目錄里面,將庫放到另外一個文件里面,這樣便于我們進行分類管理。我們也按照這種標準化的做法,來整理一下我們的目錄結構。
我們 libmymath.a 靜態庫不是C的標準庫,編譯器不認識第三方庫(需要提供第三方庫的路徑及庫名),所以gcc
不會在進行編譯時去鏈接我們自己寫的靜態庫,所以我們還要給gcc
添加一些參數用來指明我們要鏈接的靜態庫。?
?正確寫法,加上需鏈接的庫:
(其中?-I
?- L
?-l
?,其后面傳遞的內容可以加空格進行分割,也可以不加空格)
-I
: 指明我們要包含的頭文件路徑,此處為 ../include-L
?:指明我們包含的庫的路徑,此處為 ../mylib-l
:指明我們要包含的庫文件名(這里的庫文件名是指真實名稱),庫名稱為 mymath
為什么編譯?
C/C++
?代碼時,不需要指定路徑?
- 因為這些庫都是系統級的,
gcc/g++
?默認找的就是?stdc/stdc++
?庫
2.?將頭文件和靜態庫文件安裝至系統目錄中
除了這種比較麻煩的指定路徑編譯外,我們還可以將頭文件與動態庫文件直接安裝在系統目錄中,直接使用,無需指定路徑(需要指定靜態庫名)
所謂的安裝軟件,就是將自己的文件安裝到系統目錄下
sudo cp ./include/*.h /usr/include/
sudo cp ./mylib/*.a /lib64/
注意:?將自己寫的文件安裝到系統目錄下是一件危險的事(導致系統環境被污染),用完后記得手動刪除?。
總結:第三方庫的使用
- 需要指定的頭文件,和庫文件。
- 如果沒有默認安裝到系統gcc、g++默認的搜索路徑下,用戶必須指明對應的選項,告知編譯器: a.頭文件在哪里 b.庫文件在哪里 c.庫文件具體是誰。
- 將我們下載下來的庫和頭文件,拷貝到系統默認路徑下,在Linux下就是安裝庫! 那么卸載呢?對任何軟件而言,安裝和卸載的本質就是拷貝到系統特定的路徑下!
- 如果我們安裝的庫是第三方的庫,我們要正常使用,即便是已經全部安裝到了系統中,gcc g++必須用 -l 指明具體庫的名稱!
- 無論我們是從網絡中直接下載的庫,還是源代碼(編譯方法)。都會提供一個?make install?安裝的命令,這個命令所做的就是安裝到系統中的工作。我們安裝大部分指令、庫等等都是需要 sudo 提權的。
(四)制作一個動態庫
動態庫:動態庫不同于靜態庫,動態庫中的函數代碼不需要加載到源文件中,而是通過?與位置無關碼?,對指定函數進行鏈接使用。
動態庫的打包也同樣分為兩步:
- 編譯源文件,生成二進制可鏈接文件,此時需要加上?
-fPIC
?與位置無關碼(下文會詳細解釋) - 通過?
gcc/g++
?直接目標程序(此時不需要使用?ar
?歸檔工具)
將源文件編譯為?.o
?二進制文件,此時需要帶上?-fPIC
?與位置無關碼
gcc -c -fPIC *.c
將所有的?.o
?文件打包為動態庫(借助?gcc/g++
)?
gcc -o libmycalc.so *.o -shared
當我們有了動態庫以后,我們是可以刪除可重定向的二進制文件的,但是動態庫不能夠刪除,動態庫刪除的話,依賴此動態庫的程序也將不能夠運行!
(五)動態庫的使用
下面我們嘗試用動態庫去鏈接形成可執行程序:
注意:我們自己寫的庫是屬于第三方庫,我們要編譯時要指明:頭文件路徑,庫文件路徑,庫文件名(真實名稱)。?
發生了錯誤,系統提示我們程序運行時,沒有辦法找到動態庫,這是為什么呢?
這就和動態庫的特性有關了,由于采用動態庫的程序在運行的時候才去鏈接動態庫的代碼,多個程序共享使用庫的代碼,所以運行的程序必須要知道去哪里鏈接我們的庫,即對于動態庫在編譯期間我們要告訴編譯器去哪里鏈接庫進行編譯,在運行期間要告訴操作系統去哪里鏈接庫進行運行。
靜態庫不需要鏈接是因為:靜態庫在編譯鏈接期間將用戶使用的二進制代碼直接拷貝到目標可執行程序中,編譯后的程序是一個完整的程序,不需要再運行時再使用靜態庫了。
操作系統查找動態庫的方法有三種:
- 設置環境變量:LD_LIBRARY_PATH。
- 在系統指定路徑下建立軟鏈接,指向對應的庫。
- 配置文件。
在我們Linux
下有一個環境變量:LD_LIBRARY_PATH
,操作系統會去這個環境變量下的路徑去搜索動態庫,我們可以將我們的第三方庫加入到這個環境變量中,然后我們再運行我們的可執行程序就能成功了。
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/yrj/Linux/file/FileSystem/mylib_static_dynamic/mylib
注意:?更改環境變量只是臨時方案,因為重新登錄后會失效,需重新添加才能成功執行
2. 建立軟鏈接
我們知道Linux
中C/C++的默認庫路徑是/usr/lib64
或/lib64
,這也是系統搜索庫的默認路徑,我們可以將我們的第三方庫在二者之中下面建立一個軟連接(不推薦直接將第三方庫拷貝到默認庫路徑/usr/lib64
或/lib64
下面),這樣我們也能夠正常使用了。
sudo ln -s /home/yrj/Linux/file/FileSystem/mylib_static_dynamic/mylib/libmymath.so /lib64/libmymath.so
通過?ldd
?查看程序鏈接情況:?
因為軟鏈接是一個正常的文件,永遠保存在磁盤上,所以我們退出后再次登錄時,程序依然可以正常運行。
3. 更改配置文件
在我們的Linux
系統中有一個配置文件目錄/etc/ld.so.conf.d
,在這個目錄里面我們可以創建一個文件,文件里面寫上動態庫的路徑,這樣我們系統在搜索動態庫時也會搜索到該路徑。
?現在我們自己在此目錄下?touch?一個配置文件,并在該配置文件內輸入所需庫的路徑:
?更改完配置文件后,需要讓該配置文件生效。采用指令:
ldconfig
???????
注意:?后兩種方法都可以做到永久生效(因為存入了系統目錄中),但在使用完后最好刪除,避免污染系統環境?
七、動靜態庫的加載
(一)靜態庫的加載
在形成可執行程序的鏈接期間,靜態庫中的代碼會被直接拷貝一份進入可執行程序內。所以在程序運行期間靜態庫可以理解為不會被加載,或者說靜態庫和程序一起被加載。
但是由于是靜態庫,當多個進程包含相同的靜態庫時這會導致內存中存在大量的重復代碼,導致內存資源的浪費。
(二)動態庫的加載
1. 加載過程
當使用動態庫編譯好了一個可執行文件后,該可執行文件存儲在磁盤當中,并在運行時加載到內存里。
我們知道,程序被加載到內存后就變成了進程,OS會在內存中創建對應的 task_struct 、 mm_struct 、 頁表 。用戶在執行程序中的代碼時,正常執行。當需要執行動態庫內的代碼時,OS會先在內存中搜尋動態庫是否存在,如果存在,就直接將動態庫通過頁表進行映射到進程的進程地址空間中的共享區中,否則就會將磁盤中的動態庫加載進入內存,然后再通過頁表進行映射,映射到虛擬地址空間的共享區中。這些動作都是由OS自動完成的。
可執行文件在被編譯完成時,就已經具備了對應的虛擬地址。以上動作完成后,再執行動態庫內的代碼,OS會自動識別,并跳轉到虛擬地址的共享區部分,通過頁表的映射關系,執行內存中對應的動態庫代碼,動態庫代碼執行完畢后,再回到虛擬地址的代碼區部分,繼續執行下面的其他代碼。
換句話說,只要把庫加載到內存,映射到進程的地址空間后,進程執行庫中的方法,就依舊還是在自己的地址空間內進行函數跳轉即可。
2. 動態庫地址的理解
在程序編譯鏈接形成可執行程序的時候,可執行程序內部就已經有地址了,地址一共有兩類,分別是絕對編址與相對編址。我們知道被編譯好的程序內部是有地址的!動態庫內部的地址并不是絕對地址,而是偏移量!(相對地址)
動態庫必定面臨一個問題:不同的進程,運行程度不同,需要使用的第三方庫是不同的,這就注定了每一個進程的共享區中的空閑位置是不確定的。如果采用了絕對編址,在一個進程使用了多個庫時就有可能照成地址沖突!因此,動態庫中函數的地址,絕對不能使用絕對編址,動態庫中的所有地址都是偏移量,默認從?0?開始。簡單來說,庫中的函數只需要記錄自己在該庫中的偏移量,即相對地址就可以了。
當一個庫真正的被映射到進程地址空間時,他的起始地址才能真正的確定,并且被OS管理起來。OS本身管理庫,所以OS知道我們調用庫中函數時,使用的是哪一個庫,這個庫的起始地址是什么。當需要執行庫中的函數時,只需要拿到庫的起始地址,加上對應函數在該庫中的偏移量,就能夠調用對應函數了。
借助函數在庫中的相對地址,無論庫被加載到了共享區的哪一個位置,都不影響我們準確的找到對應函數,也不會與其他庫產生沖突了!?所以這種庫被稱為動態庫,動態庫中地址偏移量被稱為與位置無關碼。
八、動態庫知識補充
當同時擁有 靜態庫 和 動態庫 時,默認采用動態鏈接;
如果想要使用靜態鏈接,則需要在編譯時加上?-static?命令選項。?
?如果只有靜態庫,但又不指定靜態鏈接,會發生什么?
會默認使用動態鏈接。而對于其他的,比如C庫等等,依然默認使用動態鏈接。?
可以看看以上三種方式生成的可執行程序大小:
靜態鏈接生成的程序比動態鏈接大得多,并且內含靜態庫的動態鏈接程序,也比純粹的動態鏈接程序大,說明程序不是?非靜即動
,可以同時使用動態庫與靜態庫?。
總結:
關于動靜態庫的優缺點可以看看下面這個表格
區別 | 動態庫 | 靜態庫 |
---|---|---|
調用方式 | 通過函數位置進行調用 | 直接將需要的函數拷貝至程序中 |
依賴性(運行時) | 需要依賴于動態庫 | 可以獨立于靜態庫運行 |
空間占用 | 共享動態庫中的代碼,空間占用少 | 拷貝代碼會占用大量空間 |
加載速度 | 調用函數,加載速度慢 | 直接運行,加載速度快 |