認識 deb 文件
在 Debian 系列系統(tǒng)中,.deb 包是軟件分發(fā)的標準格式,其本質是一個 ar 歸檔文件,內部包含多個壓縮的 tar 文件和其他元數(shù)據(jù)文件。以下是 .deb 文件的組成結構和提取方法:
1. deb 包的結構解析
一個 .deb 文件由 3 個核心文件組成,按順序排列在 ar 歸檔中,使用編輯器查看一個 deb 包也能看到:
如:man-db 的二進制包,頭部數(shù)據(jù)是:
!<arch>
debian-binary 1582650825 0 0 100644 4
2.0
control.tar.xz 1582650825 0 0 100644 20524
control 數(shù)據(jù)之后就是:
data.tar.xz 1582650825 0 0 100644 1223816
1、debian-binary
作用: 標識 deb 格式版本(純文本文件)。
內容: 通常為 2.0,表示遵循 Debian Package Format 2.0 規(guī)范。
2、control.tar.*
作用: 存儲包的元數(shù)據(jù)和控制腳本(如安裝/卸載前后執(zhí)行的腳本)。
壓縮格式: 可能是 xz (.tar.xz) 或 gzip (.tar.gz),具體取決于打包工具。
內容:
control: 包名、版本、依賴、描述等元信息。
postinst/prerm: 安裝后/卸載前的執(zhí)行腳本。
md5sums: 包內文件的校驗和。
3、data.tar.*
作用: 存儲實際要安裝到系統(tǒng)中的文件(如二進制、配置文件、文檔等)。
壓縮格式: 同樣可能是 xz、gzip 或 bzip2。
內容: 文件路徑與系統(tǒng)安裝路徑一致(如 /usr/bin/, /etc/)
處理 deb 文件
上面已經介紹過 deb 文件的基本組成,它是一個 ar 歸檔文件。當我們使用 dpkg_ar_open 打開 deb 文件,從 deb 讀出來 data.tar.* 等數(shù)據(jù)流。讀取文件內容,判斷是什么類型的壓縮包格式,決定解壓工具用哪個 decompressor。假設 tar.xz 文件,對應的解壓器就是 xz,解壓函數(shù) decompressor_xz。解壓后的 TAR 歸檔文件經過 GZIP 壓縮后的組合格式,需要分為兩層來解析:
外層:XZ 壓縮層
【在 dpkg-deb 子進程 c2 中把它解壓了】
XZ 文件由多個 數(shù)據(jù)塊(Blocks) 組成,每個塊包含以下部分:
| 組成部分 | 描述 |
|---|---|
| 文件頭 | 12 字節(jié)標識,包含魔數(shù) FD 37 7A 58 5A 00(ASCII 為 "FD7zXZ\0") |
| 壓縮數(shù)據(jù)塊 | 使用 LZMA2 壓縮后的數(shù)據(jù),可能分多個塊以提高并行處理效率。 |
| 索引(Index) | 記錄所有壓縮塊的偏移和大小,用于快速定位。 |
| 文件尾 | 包含 CRC-32 校驗和,驗證文件完整性。 |
使用 LZMA2 壓縮算法對原始 TAR 歸檔數(shù)據(jù)進行壓縮。
內層:TAR 歸檔層
【dpkg 父進程拿到這個歸檔數(shù)據(jù),進行tar_extractor 提取】
壓縮層解壓后得到原始的 TAR 歸檔文件,其結構由多個 文件條目(File Entry) 組成,每個條目包含 512 字節(jié)的頭部(Header) 和 文件數(shù)據(jù):
文件頭(Header Block,512 字節(jié)):記錄文件元數(shù)據(jù)(文件名、大小、權限等)。
文件數(shù)據(jù):實際文件內容,按 512 字節(jié)對齊(不足部分填充 \0)。
結束標記:兩個連續(xù)的 512 字節(jié)全 \0 塊,表示 TAR 文件結束。
| 字段名 | 字節(jié)范圍 | 說明 | 示例值(ASCII字符串) |
|---|---|---|---|
| name | 0-99 | 文件名(含路徑) | "dir/file.txt" |
| mode | 100-107 | 文件權限(八進制) | "0644" |
| uid | 108-115 | 用戶 ID(八進制) | "1000" |
| gid | 116-123 | 組 ID(八進制) | "1000" |
| size | 124-135 | 文件大小(字節(jié),八進制) | "12345" → 十進制 5349 |
| mtime | 136-147 | 最后修改時間(八進制,Unix 時間戳) | "1620000000" → 十進制時間 |
| typeflag | 156 | 文件類型標志 | '0'(普通文件) |
| linkname | 157-256 | 鏈接目標路徑(僅符號鏈接) | "/usr/bin/sh" |
| magic | 257-262 | 魔數(shù)(標識格式版本) | "ustar" |
| checksum | 148-155 | Header 校驗和(八進制) | "0000000"(需重新計算驗證) |
#define DEBMAGIC "debian-binary"
#define ADMINMEMBER "control.tar"
#define DATAMEMBER "data.tar"
進程間關系介紹
父進程 dpkg,接受許多 deb 包作為參數(shù),解析參數(shù)并挨個處理 deb 文件。處理時,fork 若干個子進程來一起工作:
第一個子進程 dpkg-deb --control + deb包
這個進程將包的控制腳本等提取到臨時的 ci 目錄,一般是 /var/lib/dpkg/tmp.ci
執(zhí)行的函數(shù)是 extracthalf(debar, dir, DPKG_TAR_EXTRACT, 1);
第二個子進程 dpkg-deb --fsys-tarfile +deb包
這個進程將 deb 包解壓,并將數(shù)據(jù)流信息 data.tar 直接寫到標準輸出中。
執(zhí)行的函數(shù)是 extracthalf(debar, NULL, DPKG_TAR_PASSTHROUGH, 0);
為了實現(xiàn)上面兩個功能,函數(shù) extracthalf 也會根據(jù)情況 fork 兩到三個子進程,使用過濾器模式將解壓提取文件的過程通過管道串起來。
| 管道 | 進程端操作 | 數(shù)據(jù)方向 | 數(shù)據(jù)類型 |
|---|---|---|---|
| p1 | c1: 關閉讀端(p1[0]),寫入 p1[1] c2: 關閉寫端(p1[1]),讀取 p1[0] | c1 → c2 | 原始壓縮數(shù)據(jù) |
| p2 | c2: 關閉讀端(p2[0]),寫入 p2[1] c3: 關閉寫端(p2[1]),讀取 p2[0] | c2 → c3 | 解壓后的 tar 數(shù)據(jù) |
.deb 文件 → c1 (原始數(shù)據(jù)) → p1 → c2 (解壓) → p2 → c3 (tar處理) → 輸出結果
不同解壓參數(shù)時 c2 進程的通信差異:
1、DPKG_TAR_PASSTHROUGH 時,無p2管道,從c1進程讀出來的數(shù)據(jù),經過解壓后,直接寫到標準輸出。
2、DPKG_TAR_EXTRACT 時,創(chuàng)建p2管道,將 c1 進程的數(shù)據(jù),解壓后,傳給管道 p2 的讀端。
提取 control.tar.*
以 control.tar.xz 數(shù)據(jù)流為例,對應的解壓參數(shù)是 DPKG_TAR_EXTRACT,有三個子進程兩條管道:
第一個子進程 c1
執(zhí)行 fd_fd_copy (ar->fd, p1[1], memberlen, &err), 將ar->fd(即.deb文件的內容)通過fd_fd_copy復制到p1的寫端。
第二個子進程 c2
從管道 p1 收到 deb 數(shù)據(jù)流,通過 decompress_filter 解壓,將解壓后的 tar 歸檔流寫入 p2。
第三個子進程 c3
提取 tar 文件,c2 和 c3 實現(xiàn)的類似這樣一條命令 xz -dc archive.tar.xz | tar -x file/to/extract
提取 data.tar.*
對應的解壓參數(shù)是 DPKG_TAR_PASSTHROUGH,有兩個子進程一條管道:
第一個子進程 c1
執(zhí)行 fd_fd_copy (ar->fd, p1[1], memberlen, &err), 將ar->fd(即.deb文件的內容)通過fd_fd_copy復制到p1的寫端。
第二個子進程 c2
從管道 p1 收到 deb 數(shù)據(jù)流,通過 decompress_filter 解壓,將解壓后的 tar 歸檔流寫到標準輸出。
分析 extracthalf 函數(shù),它的第一個參數(shù)是 deb 包,第二個參數(shù)是解壓的目標路徑,第三個是解壓選項,第四個admininfo 。【在解析到 data 這一節(jié)數(shù)據(jù)時,如果這個參數(shù)不為0就跳過 fd_skip】
解壓選項 DPKG_TAR_PASSTHROUGH (0) 表示直接輸出壓縮文件內容,不做任何處理【輸出到管道,被下一個進程處理】
TAR歸檔數(shù)據(jù)流
父進程 dpkg,在 fork 子進程來執(zhí)行對 data.tar.* 提取的時候,創(chuàng)建了管道,并在子進程中將管道的寫端復制到標準輸出。這樣的目的,是子進程中寫到標準輸出的信息都會寫入管道的寫端。上面第二個子進程 c2 將解壓后的 tar 歸檔寫到標準輸出,就這樣傳給了 dpkg 主進程。
m_pipe(p1);
push_cleanup(cu_closepipe, ehflag_bombout, 1, (void *)&p1[0]);
pid = subproc_fork();
if (pid == 0) {
m_dup2(p1[1],1); close(p1[0]); close(p1[1]);
execlp(BACKEND, BACKEND, "--fsys-tarfile", filename, NULL);
……
}
close(p1[1]);
p1[1] = -1;
……
tc.pkg= pkg;
tc.backendpipe= p1[0];
tc.pkgset_getting_in_sync = pkgset_getting_in_sync(pkg);
……
rc = tar_extractor(&tar);
這個操作很重要,接下來的 tar_extractor 就是因為這個操作拿到了數(shù)據(jù)流。tar_extractor 函數(shù)的主要功能是從一個 tar 歸檔文件中提取內容。它通過 tarfileread 函數(shù)從管道讀取 tar 文件數(shù)據(jù)流,從頭部信息解析每個文件的元數(shù)據(jù)(如文件名、文件類型、權限等),接著根據(jù)文件類型執(zhí)行相應的操作(比如提取文件、創(chuàng)建目錄、創(chuàng)建符號鏈接等)。
分析這個函數(shù),多次用到了 ops->mkdir ops->extract_file 等,在 tar_operations 中對應的都是 tarobject。
static const struct tar_operations tf = {
.read = tarfileread,
.extract_file = tarobject,
.link = tarobject,
.symlink = tarobject,
.mkdir = tarobject,
.mknod = tarobject,
};
tarobject 函數(shù)在 archive.c 中定義,,它的主要作用是處理 tar 歸檔文件中的單個對象(如文件、目錄、符號鏈接等)。根據(jù)對象的類型執(zhí)行相應的操作,包括提取文件、創(chuàng)建目錄、處理符號鏈接等。
前面已經介紹過TAR歸檔文件的數(shù)據(jù)組成:
文件頭(Header Block,512 字節(jié)):記錄文件元數(shù)據(jù)(文件名、大小、權限等)。
文件數(shù)據(jù):實際文件內容,按 512 字節(jié)對齊(不足部分填充 \0)。
結束標記:兩個連續(xù)的 512 字節(jié)全 \0 塊,表示 TAR 文件結束。
解析tar包頭部信息
函數(shù) tar_header_decoder 映射 512 字節(jié)的頭部信息,解析 tar_header 并填充 tar_entry 結構。比較重要的的是 linkflag,dpkg 代碼中定義的結構名稱是 linkflag,不過在標準 tar 規(guī)范中叫 typeflag。偏移位置和大小都正確,名字不一樣不影響解析,根據(jù)這個字段的信息可以知道數(shù)據(jù)流中的是普通文件還是目錄、鏈接文件等。
struct tar_header {
char name[100];
char mode[8];
char uid[8];
char gid[8];
char size[12];
char mtime[12];
char checksum[8];
char linkflag; // tar規(guī)范中對應的字段是typeflag,位于第156字節(jié)
char linkname[100];
/* Only valid on ustar and gnu. */
char magic[8]; // 根據(jù)標準 magic 從 257 開始
char user[32];
char group[32];
char devmajor[8];
char devminor[8];
/* Only valid on ustar. */
char prefix[155];
};
提取文件和目錄
從TAR歸檔中解析到文件的類型,執(zhí)行對應的操作。tarobject 在實際提取壓縮文件的時候,先 setupfnamevbs 追加了后綴名 .dpkg-tmp, .dpkg-new 等,比如:
setupvnamevbs 中的 fnamevb.buf, fnametmpvb.buf, fnamenewvb.buf 分別是:
main='/opt/usr/lib/aarch64-linux-gnu/libcurl.so.4'
tmp='/opt/usr/lib/aarch64-linux-gnu/libcurl.so.4.dpkg-tmp'
new='/opt/usr/lib/aarch64-linux-gnu/libcurl.so.4.dpkg-new'
順便思考一下,為什么 control 的解壓參數(shù)是 EXTRACT,而數(shù)據(jù)文件的解壓參數(shù)是 PAATHROUGH? 為什么實際提取的時候,增加了上面這些后綴?
在更新或安裝文件時,文件內容可能分多次寫入磁盤。如果直接覆蓋原文件,中途發(fā)生系統(tǒng)崩潰或進程終止,會導致文件處于 不完整狀態(tài)。通過將新內容先寫入 .dpkg-tmp 或 .dpkg-new 后綴的臨時文件,僅在完全寫入成功 后,通過 rename() 系統(tǒng)調用將臨時文件 原子性重命名 為最終文件名。此操作在文件系統(tǒng)層面是原子的,確保用戶要么看到舊文件,要么看到完整的新文件,永遠不會處于中間狀態(tài)。
通過為文件添加 .dpkg-tmp 和 .dpkg-new 后綴,dpkg 實現(xiàn)了以下核心目標:
原子性:確保文件操作要么完全成功,要么完全失敗。
可靠性:防止因意外中斷導致的數(shù)據(jù)不一致。
可維護性:簡化錯誤恢復和系統(tǒng)監(jiān)控流程。
完成文件落盤操作
在 tarobject 的最后,已經執(zhí)行過 tarobject_extract 了,這時候去做重命名。重命名有兩個時機:
a、一個就在 tarobject的最后,直接 rename(fnamenewvb.buf, fnamevb.buf);
b、另一個是延遲,對于普通文件或者是硬鏈接、軟鏈接文件,都會給 namenode->flags 增加掩碼 FNNF_DEFERRED_RENAME,
tar_deferred_extract 函數(shù)去執(zhí)行 rename(fnamenewvb.buf, fnamevb.buf) 來完成重命名。
只要完成了rename,文件狀態(tài)就加上掩碼 FNNF_PLACED_ON_DISK,表明落盤了。
數(shù)據(jù)庫更新
提取包文件并更新數(shù)據(jù)庫的函數(shù)調用鏈如下:
archivefiles -> process_archive-> modstatdb_note-> modstatdb_note_core-> varbufrecord (&uvb, pkg, &pkg->installed) -> fip->wcall -> varbuf_add_str
Debian 包管理中,在安裝時有兩個目錄,分別是安裝目錄和管理目錄:
安裝目錄 instdir
存放軟件的實際文件:安裝目錄是軟件包內文件(如二進制程序、庫、配置文件等)被解壓并復制到系統(tǒng)中的目標路徑。
管理目錄
記錄軟件包元數(shù)據(jù):存儲 dpkg 管理軟件包所需的狀態(tài)信息,例如:
/var/lib/dpkg:核心數(shù)據(jù)庫,記錄所有已安裝軟件包的狀態(tài)(status 文件),包的本地數(shù)據(jù)庫就是這個 status 文件,讀寫數(shù)據(jù)庫也就是對這個文件的讀寫操作。
包的集合關系
為了能有組織的理解系統(tǒng)中已安裝包的結構關系,需要關注一下兩個數(shù)據(jù)結構。在 lib/dpkg/dpkg.db.h 使用這樣的結構來表示具有同樣包名的可用包與已安裝包:
struct pkgset {
struct pkgset *next;
const char *name;
struct pkginfo pkg;
struct {
struct deppossi *available; // 正在被安裝的包
struct deppossi *installed; // 已經安裝的包
} depended;
int installed_instances;
};
struct pkginfo {
struct pkgset *set;
struct pkginfo *arch_next;
enum pkgwant want;
/** The error flag bitmask. */
enum pkgeflag eflag;
enum pkgstatus status;
enum pkgpriority priority;
const char *otherpriority;
const char *section;
struct dpkg_version configversion;
// pkgbin 是一個描述二進制包信息的數(shù)據(jù)結構,包括:depends、arch、description、maintainer、source、installedsize、version、conffiles 等
struct pkgbin installed;
struct pkgbin available;
struct perpackagestate *clientdata;
struct archivedetails *archives;
struct {
/* ->aw == this */
struct trigaw *head, *tail;
} trigaw; // trigger await 在觸發(fā)器那塊應該會用到
/* ->pend == this, non-NULL for us when Triggers-Pending. */
struct trigaw *othertrigaw_head;
struct trigpend *trigpend_head;
……
};
這兩個都是鏈表結構,pkginfo 鏈表保存同一個包名的不同架構包信息,pkgset 是系統(tǒng)中所有包的集合??梢暬氖疽鈭D如下:

當我們在命令行執(zhí)行 dpkg -L libcurl4 請求輸出這個包安裝到系統(tǒng)中的文件時,首先根據(jù)給定的包名找到 pkgset,再根據(jù)架構從pkgset 中找到pkginfo。
Deb222 風格特征
系統(tǒng)本地數(shù)據(jù)庫文件 /var/lib/dpkg/status 就是這個格式的,讀取數(shù)據(jù)庫信息是采用下面這樣的解析規(guī)則。
(1) 多段落結構
文件由多個 段落(Stanza) 組成,每個段落描述一個軟件包或源碼包。
段落之間通過 空行 分隔。
示例(
Packages文件片段):
Package: curl
Version: 8.5.0
Description: Command-line tool for transferring data with URL syntax
Supports HTTP, HTTPS, FTP, and more.
.
This package provides the curl binary.
Package: libcurl4
Version: 8.5.0
Depends: libc6, libssl3
...
(2) 字段語法規(guī)則
字段名與值:每行一個字段,格式為
Field-Name: Value。多行值:后續(xù)行以 空格開頭(縮進),表示延續(xù)內容。
段落分隔:空行表示當前段落結束,新段落開始。
-
嚴格校驗:
字段名不能以連字符(
-)開頭。字段名后必須緊跟冒號(
:)。值部分不允許空行(
parse_error處理空白行)。
讀寫數(shù)據(jù)庫
當用戶在命令行 dpkg -l 來查詢包的信息,或者 dpkg -i 安裝包之前,或者 apt update 更新可用包信息等,都需要先加載本地數(shù)據(jù)庫,從數(shù)據(jù)庫查詢包的相關信息,或者下載服務器的 Packages.xz 來跟本地數(shù)據(jù)庫作對比,列出可用包信息。
這個本地數(shù)據(jù)庫的構建,就是通過 parsedb_open 讀文件 /var/lib/dpkg/status 來獲取的。
由于 status 文件和 Packages.xz 都是 deb222 風格的,所以創(chuàng)建一個 deb822 解析器上下文 parsedb_state *ps,根據(jù) ps 上下文來加載數(shù)據(jù)庫信息 parsedb_load(ps)
load 分兩種情況,一種是從 FIFO文件讀:封裝 buffer_copy,函數(shù) fd_vbuf_copy 從文件句柄中讀到 buf 中,并進一步將數(shù)據(jù)指針保存到上下文中 ps->dataptr;另一個解析普通文件,將文件映射到內存中來解析。
等到解析完成,將一坨包信息加載到解析器上下文了。都在 ps->dataptr, ps 事關重大,需要格外關注。
解析數(shù)據(jù)庫文件 status
Status 文件是每一個包對應一個段落,每個段落包含若干條字段信息。在 dpkg 中,為了方便讀寫相關字段,定義了該字段信息讀寫數(shù)據(jù)庫的函數(shù),如下:
const struct fieldinfo fieldinfos[]= {
/* name namelen rcall wcall integar */
{ FIELD("Package"), f_name, w_name },
{ FIELD("Essential"), f_boolean, w_booleandefno, PKGIFPOFF(essential) },
{ FIELD("Status"), f_status, w_status },
{ FIELD("Priority"), f_priority, w_priority },
{ FIELD("Section"), f_section, w_section },
{ FIELD("Installed-Size"), f_charfield, w_charfield, PKGIFPOFF(installedsize) },
{ FIELD("Origin"), f_charfield, w_charfield, PKGIFPOFF(origin) },
{ FIELD("Maintainer"), f_charfield, w_charfield, PKGIFPOFF(maintainer) },
……
rcall 負責讀信息【比如命令行查詢版本號】:
lib/dpkg/pkg-hash.c 函數(shù) pkg_hash_find_set從 bins 中找信息,目前bins數(shù)組中存的是 pkgset ,數(shù)組大小接近 Debian suite 的數(shù)目 65521 個。
如果內部數(shù)據(jù)庫已經存在這個包了,find_set 返回已經存在的結果,否則就會創(chuàng)建一個新的pkgset結構體來返回。
wcall 負責寫數(shù)據(jù)庫【比如安裝一個包之后需要更新到status文件中】:
lib/dpkg/dump.c 這個函數(shù)就是追加字符串信息,對于 fip->wcall(vb, pkg, pkgbin, fw_printheader, fip); name字段 是 "Package" 的函數(shù),對照上表,查出來的函數(shù) 就是 w_name,函數(shù)定義如下:
w_name(struct varbuf *vb, const struct pkginfo *pkg, const struct pkgbin *pkgbin, enum fwriteflags flags, const struct fieldinfo *fip)
段落解析 parse_stanza
Status 文件很長,系統(tǒng)中上千個包都寫在這個文件中了。每個包描述信息就是一個段落,解析這個文件按照段落來的。
parse_stanza 有兩個循環(huán),第一個循環(huán)逐段解析,第二個循環(huán)逐字段解析:
1、每個包是一個段
解析到的段通過 fs->valuestart、 fs->valuelen 控制范圍.
每當執(zhí)行 parse_stanza 解析包數(shù)據(jù)前,都會執(zhí)行 pkgset_blank 清空上一次解析的pkginfo,確保每次傳遞的 pkg_obj 是待填充的空包結構
new_pkg = &tmp_set.pkg;
……
pkg_obj.pkg = new_pkg;
pkg_obj.pkgbin = new_pkgbin;
pkgset_blank(&tmp_set);
if (!parse_stanza(ps, &fs, pkg_parse_field, &pkg_obj))
2、每一個段中包含了許多字段
ps的 dataptr 數(shù)據(jù)量太多了,只截取其中一段(一個獨立包)解析的數(shù)據(jù)在 fs 中,通過parse_field(ps, fs, parse_obj); 再去挨個分析字段來填充
每個包信息的 fs 數(shù)據(jù)結構是:
struct field_state {
const char *fieldstart;
const char *valuestart;
struct varbuf value;
int fieldlen;
int valuelen;
int *fieldencountered;
};
經過解析,在逐字段解析函數(shù) pkg_parse_field 中,fs->value 這個 varbuf 存的就是每一個字段對應的值:
parse field, fs->value is xz-utils, len is 8
parse field, fs->value is install ok installed, len is 20
parse field, fs->value is required, len is 8
parse field, fs->value is utils, len is 5
parse field, fs->value is 437, len is 3
parse field, fs->value is Ubuntu Developers <ubuntu-devel-discuss@lists.ubuntu.com>, len is 57
parse field, fs->value is arm64, len is 5
parse field, fs->value is foreign, len is 7
parse field, fs->value is 5.2.4-1kylin2.1, len is 15
parse field, fs->value is lzip (<< 1.8~rc2), xz-lzma, len is 26
parse field, fs->value is lzma, len is 4
parse field, fs->value is libc6 (>= 2.17), liblzma5 (>= 5.2.2), len is 36
parse field, fs->value is lzip (<< 1.8~rc2), len is 17
parse field, fs->value is lzma (<< 9.22-1), xz-lzma, len is 25
parse field, fs->value is XZ-format compression utilities
XZ is the successor to the Lempel-Ziv/Markov-chain Algorithm
compression format, which provides memory-hungry but powerful
compression (often better than bzip2) and fast, easy decompression.
.
This package provides the command line tools for working with XZ
compression, including xz, unxz, xzcat, xzgrep, and so on. They can
also handle the older LZMA format, and if invoked via appropriate
symlinks will emulate the behavior of the commands in the lzma
package.
.
The XZ format is similar to the older LZMA format but includes some
improvements for general use:
.
'file' magic for detecting XZ files;
crc64 data integrity check;
limited random-access reading support;
improved support for multithreading (not used in xz-utils);
support for flushing the encoder., len is 828
有了這個值再去執(zhí)行 fip->rcall(pkg_obj->pkg, pkg_obj->pkgbin, ps, fs->value.buf, fip); rcall 即讀函數(shù),對照上面的每一個字段,這個 rcall 就是:
f_name、f_status、f_priority、f_charfield 等等等,
fieldinfos 中每一個字段,讀著讀著就都保存到 pkg_obj->pkg 和pkg_obj->pkgbin 里面了。
最后再將 pkginfo 掛接到 pkgset 的鏈表上。
dpkg 中系統(tǒng)包的組織結構
結構一:哈希桶 bins,它的哈希算法是 FNV-1a 哈希算法,函數(shù) str_fnv_hash 實現(xiàn)了。在一開始加載數(shù)據(jù)庫時,通過讀數(shù)據(jù)庫文件 /var/lib/dpkg/status,解析 fieldinfos 中每一個包的“Package”字段,根據(jù)包名來構建哈希桶和 pkgset、pkginfo 鏈表。
pkg_hash_find_set (pkgname) 函數(shù)通過哈希算法將包名計算為哈希值,定位哈希桶的位置,并返回 pkgset。如果哈希桶對應位置上沒有pkgset,就創(chuàng)建一個 pkgset 落進去,先占個坑。
對輸入包名的每個字符進行以下操作:
異或操作:將當前哈希值與字符的 ASCII 碼進行異或(h ^= *str++),將字符信息融入哈希值。
乘法混合:將結果與固定質數(shù) FNV_MIXING_PRIME(例如 32 位版本的 0x01000193)相乘(h *= p),增強哈希值的分散性。
結構二:pkgset,每一個二進制包名,對應一個 pkgset結構,其中的 pkg 成員就是這個包名對應的所有架構的包。哈希桶bins的哈希算法根據(jù)二進制包名來計算的,有可能會出現(xiàn)兩個或多個 pkgset 落到同一個哈希桶的情況,比如:
bins[0]: pkgset(vim) → pkgset(gcc) → NULL
├─ pkginfo(vim:amd64) → pkginfo(vim:i386) → NULL
└─ pkginfo(gcc:amd64) → NULL
bins[1]:pkgset(libcurl)->NULL
└─ pkginfo(curl:all) → NULL
結構三:pkginfo,維護了同一個包名在系統(tǒng)上不同架構包的信息,串成一個鏈表。
當 dpkg 開始安裝包時,首先打開數(shù)據(jù)庫,讀取 status 文件,根據(jù)每個段落中的 Package 信息將所有包落入哈希桶bins 中,并且組織 pkgset 和 pkginfo 鏈表。為了方便后續(xù)跟蹤,函數(shù)調用鏈總結如下:
parsedb -> parsedb_parse -> parse_stanza -> pkg_parse_field -> fip->rcall ( 即 f_name) -> pkg_hash_find_set -> bins 與 pkgset
安裝配置新包過程中,需要及時更新包的狀態(tài),比如 installed、halfconfigured、 notinstalled 等各種狀態(tài)信息,都是操作 pkg_set_status 更新 pkginfo 狀態(tài)。包括 pkginfo 的 available 和 installed。
當安裝結束,要寫數(shù)據(jù)庫。會有一個結構的轉換操作,將哈希桶 bins 中的 pkgset 中的每一個 pkginfo,都轉到線性數(shù)據(jù)庫 array 中。然后再遍歷這個 array,將每個包的信息組成字符串寫到數(shù)據(jù)庫文件 status。
哈希桶與數(shù)組的轉換
對每個哈希桶中的 pkgset 鏈表,逐個訪問其 pkginfo 的所有架構實例(通過 arch_next)。當一個 pkgset 的所有實例遍歷完成后,跳轉到下一個 pkgset(通過 pkg->set->next)。目標數(shù)組:pkg_array 結構(包含 pkgs 數(shù)組和 n_pkgs 數(shù)量)。
pkg_array_init_from_hash(&array);
iter = pkg_hash_iter_new();
pkg_hash_iter_next_pkg(struct pkg_hash_iter *iter)
遍歷保存到數(shù)組的轉換過程:
1、從哈希桶 bins 中取出 pkgset 中的 pkginfo 結構,保存到 iter->pkg
2、開始遍歷這個 pkginfo 的各個架構的包
3、如果沒有其他架構了,看看桶里對應的位置還有么有其他 pkgset?有的話繼續(xù)遍歷它的包
4、直到把 bins 中所有的都遍歷完,保存到數(shù)組 pkg_array 中。
為了方便后續(xù)跟蹤,函數(shù)調用鏈如下:
modstatdb_shutdown -> writedb -> writedb_recwritedbords -> pkg_array_init_from_hash -> pkg_hash_iter_next_pkg -> varbufrecord -> fip->wcall -> varbuf_add_str
fip->wcall 就是 fieldinfos 結構中定義的各個字段的寫函數(shù),比如 Package 包名字段,對應函數(shù)就是 w_name。
觸發(fā)器
觸發(fā)器分類
觸發(fā)器分為 interest 和 activate 兩種,前者表示對某些事情感興趣,當事件發(fā)生時自己的觸發(fā)器腳本就會被執(zhí)行到;后者表示主動觸發(fā)一個事件。
比如軟件包 libc-bin,聲明 “interest-await ldconfig”,向 dpkg 注冊對觸發(fā)器事件(ldconfig)的關注。這表示 libc-bin 需要在此類事件發(fā)生時執(zhí)行后續(xù)操作,但不會立即觸發(fā)。后續(xù)操作在 postint 腳本中定義:
if [ "$1" = "triggered" ] || [ "$1" = "configure" ]; then
LDCONFIG_NOTRIGGER=y //防止無限觸發(fā)interest ldconfig 的觸發(fā)器
export LDCONFIG_NOTRIGGER
ldconfig || ldconfig --verbose
exit 0
fi
軟件包 libqt5gui5 通過 activate 主動觸發(fā)一個事件“activate-noawait ldconfig”,通知 dpkg 所有已聲明 interest 的軟件包執(zhí)行相關操作。當libqt5gui5主動觸發(fā)的 ldconfig 執(zhí)行完成后,就會觸發(fā)訂閱器 libc-bin 的 interest,從而再一次執(zhí)行“l(fā)dconfig”,讓libc-bin 作為系統(tǒng)級動態(tài)鏈接器緩存的管理者,完成最終的強制覆蓋更新,確保緩存狀態(tài)與最新庫路徑完全同步。
[圖片上傳失敗...(image-de29bd-1752210510653)]
觸發(fā)器訂閱機制
安裝包在解壓安裝,刪除了舊版本的文件,安裝了新版本的文件之后,就開始考慮新包和舊包的的觸發(fā)器問題了。
trig_parse_ci(pkg_infodb_get_file(pkg, &pkg->installed, TRIGGERSCIFILE), trig_cicb_interest_delete, NULL, pkg, &pkg->installed);
trig_parse_ci(cidir, trig_cicb_interest_add, NULL, pkg, &pkg->available); // cidir 此時追加了 TRIGGERSCIFILE,/var/lib/dpkg/tmp.ci/triggers
上面這兩行代碼的主要工作就是刪除舊版本包的所有 interest 訂閱列表,并增加新包的 interest 訂閱者【如果有的話】,比如 ldconfig 觸發(fā)器的訂閱者 libc-bin,查閱文件 /var/lib/dpkg/triggers/<trigger-name> 可以看到。
trig_parse_ci (const char *file, trig_parse_cicb *interest, trig_parse_cicb *activate, struct pkginfo *pkg, struct pkgbin *pkgbin)
parse_ci_call (const char *file, const char *cmd, trig_parse_cicb *cb,
const char *trig, struct pkginfo *pkg, struct pkgbin *pkgbin,
enum trig_options opts)
trig_parse_ci 負責解析包的 triggers 文件,即解壓 control歸檔時提取出來的 /var/lib/dpkg/tmp.ci/triggers。讀文件內容,提取第一個字段,一般是 interest 或者 activate。根據(jù)這個字段來匹配參數(shù)傳過來 的 interest 和 activate 函數(shù),并調用 parse_ci_call 執(zhí)行它:
如果第一個字段(cmd)為 interest、 interest-await 或者 interest-nowait ,對應執(zhí)行參數(shù)中的 interest 函數(shù)。
如果第一個字段(cmd) 為 activate、 activate-await 或者 activate-nowait,對應執(zhí)行參數(shù)中的 activate 函數(shù)。
訂閱者更新對應的 interest 函數(shù)分別是:trig_cicb_interest_delete、trig_cicb_interest_add。對應核心函數(shù) trk_explicit_interest_change,操作原子文件來更新相關訂閱者。目前整個系統(tǒng)中,對 ldconfig 的訂閱者只有 libc-bin。其他大部分訂閱者關心的都是目錄的變化,這一類基本上都記錄在 /var/lib/dpkg/triggers/File。
觸發(fā)器的鉤子函數(shù)
在 dpkg 的觸發(fā)器機制中,struct trig_hooks 定義了五個關鍵鉤子函數(shù),用于管理觸發(fā)器的生命周期和事件處理邏輯。
static const struct trig_hooks trig_our_hooks = {
.enqueue_deferred = trigproc_enqueue_deferred,
.transitional_activate = trig_transitional_activate,
.namenode_find = th_nn_find,
.namenode_interested = th_nn_interested,
.namenode_name = th_nn_name,
};
unpack 解壓包的時候,執(zhí)行 trigproc_install_hooks 填充觸發(fā)器的鉤子函數(shù)表 。
第一個鉤子函數(shù)
當安裝包時發(fā)現(xiàn) activate 類型的觸發(fā)器時,不會立即執(zhí)行相關操作,而是通過 enqueue_deferred 將事件加入一個內部延遲觸發(fā)器處理隊列 deferred。dpkg 在所有軟件包操作(安裝、升級、卸載等)完成后,再統(tǒng)一處理隊列中的所有觸發(fā)器事件。
觸發(fā)器處理隊列 deferred
在安裝包前會先把它的觸發(fā)器識別出來,通過第一個鉤子函數(shù) enqueue_deferred 將這些觸發(fā)器的新訂閱者加到全局 deferred 隊列中。對于訂閱者,給它維護一個 trigpend_head 鏈表,保存訂閱者所有關注的觸發(fā)器。
dpkg 在子進程 dpkg-deb --control 和 dpkg-deb --fsys-tarfile 之間,也就是剛完成控制腳本提取,還沒開始解壓包文件。先執(zhí)行函數(shù) trig_activate_packageprocessing 來先處理activate類型的觸發(fā)器。
假設正要安裝 libqt5gui5 libqt5core5 libc-bin 這三個包,那么系統(tǒng)中原先裝的這些包的 activate 觸發(fā)器函數(shù)會被執(zhí)行。過程如下:
trig_parse_ci(pkg_infodb_get_file(pkg, &pkg->installed, TRIGGERSCIFILE), NULL, trig_cicb_statuschange_activate, pkg, &pkg->installed);
該函數(shù)首先解析三個安裝包的 triggers 文件,分別是:
libc-bin.triggers : interest-await ldconfig
libqt5gui5.triggers: activate-noawait ldconfig
libqt5core5a.triggers: activate-noawait ldconfig
interest 和 activate 都有,interest 類型的 libc-bin 包,trig_parse_ci 未指定 interest 函數(shù)暫不關注,只關注下面兩個觸發(fā)器。他們都是 activate-noawait, 對應的 activate 函數(shù)是 trig_cicb_statuschange_activate。
activate 觸發(fā)器又分為 explicit 和 file 兩種類型,安裝libqt5gui5的觸發(fā)器是 “l(fā)dconfig”,直接執(zhí)行命令而不是監(jiān)控目錄變化,所以屬于explicit 類型。那么 trig_cicb_statuschange_activate 執(zhí)行 dtki->activate_awaiter 對應為 trk_explicit_activate_awaiter。通過下面這條調用鏈將訂閱者 libc-bin 加入 deferred 隊列中:
trk_explicit_activate_awaiter -> trig_record_activation -> reigh.enqueue_deferred 【即trigproc_enqueue_deferred 鉤子函數(shù)】將 pend 的包加到 deferred 隊列中。
通過下面這條調用鏈維護訂閱者的 trigpend_head 鏈表:
trk_explicit_activate_awaiter -> trig_record_activation -> trig_note_pend -> trig_note_pend_core
如前所說,訂閱者從/var/lib/dpkg/triggers/<trigger-name> 解析得到。比如系統(tǒng)中已經有 libqt5gui5,我再次安裝 libqt5gui5 時,就能看到這幾條日志:
D020000: trigproc_activate_packageprocessing pkg=libqt5gui5:arm64
……
trk_explicit_activate_awaiter: explict is ldconfig, pend pkg is libc-bin
可能會有一個疑問,上面流程中函數(shù)傳遞的 pkginfo 一直是待安裝包,比如 libqt5gui5 等。pend 包 libc-bin 是如何被識別的?
回顧“觸發(fā)器訂閱機制”,提到在解壓函數(shù) process_archives 調用了 trig_parce_ci,用于更新訂閱者。函數(shù)指針分別是 trig_cicb_interest_delete 和 trig_cicb_interest_add。
更新訂閱者-> trig_cicb_interest_delete/add -> trig_cicb_interest_change -> tki->interest_change
因為觸發(fā)器 ldconfig 是 explicit 類型,因此對應的 tki->interest_change 函數(shù)是 trk_explicit_interest_change ,它接著調用 trk_explicit_start 函數(shù)來打開一個文件句柄,文件就是觸發(fā)器的 triggers 文件,對于 ldconfig 來說,文件是 /var/lib/dpkg/triggers/ldconfig。
再回到 trk_explicit_activate_awaiter 看看怎么加 deferred?它的參數(shù)是 libqt5gui5 包對應的 pkginfo 結構,包的觸發(fā)器是 ldconfig。主要功能是:
首先,檢查觸發(fā)器的 triggers 文件是否存在,沒有就算了不做任何操作。
接著,讀文件 /var/lib/dpkg/triggers/ldconfig,解析相關字段,并解析觸發(fā)器類型是否 noawait 的類型的。
通過 trig_record_activation,將pend包加到 deferred 隊列中。同時維護 pend 包的 trigpend_head 鏈表?!総rig_note_pend_core: 觸發(fā)器 ldconfig ,pend 包 libc-bin】
觸發(fā)器循環(huán)檢測與執(zhí)行
系統(tǒng)組件那么多,觸發(fā)器之間的互相依賴不可避免,幾乎所有 lib* 包都會執(zhí)行 ldconfig 觸發(fā)器,甚至于安裝 libc-bin 都會導致訂閱者 man-db 的 interest 觸發(fā)器執(zhí)行。如果存在循環(huán)觸發(fā)器,就可能一直循環(huán)觸發(fā)永無止境,所以 dpkg 專門定義了一些結構來幫助查找循環(huán)觸發(fā)器,以下是幾個關鍵結構 :
struct trigcyclenode {
struct trigcyclenode *next;
struct trigcycleperpkg *pkgs;
struct pkginfo *then_processed;
}; // 遍歷所有數(shù)據(jù)庫中包,用pkgs鏈表來維護其他每個包的的激活觸發(fā)器信息
struct trigcycleperpkg {
struct trigcycleperpkg *next;
struct pkginfo *pkg;
struct trigpend *then_trigs; // 記錄每一個歷史狀態(tài)時對應包的 trigpend_head
}; // 存儲其他存在激活觸發(fā)器的包和觸發(fā)器信息
struct trigpend {
struct trigpend *next;
const char *name;
}; // 觸發(fā)器鏈表
每當完成一個 deb 包的解壓提取,archivefiles 函數(shù)在最后會執(zhí)行 trigproc_run_deferred。該函數(shù)遍歷 deferred 隊列,對每一個包執(zhí)行函數(shù) trigproc。trigproc 函數(shù)用來處理觸發(fā)器,它會檢查包的狀態(tài),處理可能的觸發(fā)器循環(huán)依賴,條件滿足的時候執(zhí)行觸發(fā)器。
archivefiles -> 【解壓完成】trigproc_run_deferred -> trigproc -> check_trigger_cycle -> trigproc_new_cyclenode
trigproc_run_deferred 遍歷 deferred 隊列,彈出隊列成員pkginfo,對這個包執(zhí)行 trigproc函數(shù)。為了檢測循環(huán)依賴,用到了龜兔賽跑算法。
就以上面 deferred 隊列中取出來的 libc-bin 為例,它作為 ldconfig觸發(fā)器的訂閱者被 libqt5gui 激活,trigproc 通過 check_trigger_cycle 來檢查循環(huán)是否存在。
首先 trigproc_new_cyclenode (libc-bin) 遍歷哈希桶中的所有包【注意是所有包】,找到有 trigpend_head 的那些,有則說明這個包有激活的觸發(fā)器處于pending狀態(tài)。假設存在許多這樣情況的包,將它們用鏈表維護起來。
一般情況下,我們用 dpkg -i 會一次安裝許多包,archivefiles 將在所有包的文件都被解壓提取之后,才執(zhí)行 trigproc_run_deferred,遍歷隊列 deferred,對它們執(zhí)行 trigproc【查archivefiles 函數(shù),在 for 循環(huán)遍歷處理 process_archive 之后】。
根據(jù)分析,我們安裝 libc-bin、libqt5gui 會有兩個deferred 成員:被libqt5gui激活的 libc-bin 以及被 libc-bin 激活的 man-db。
1、處理第一個 deferred 成員 libc-bin,遍歷它的 trigpend_head,目前l(fā)ibc-bin也就一個 ldconfig觸發(fā)器。檢查libc-bin 包的依賴情況,沒啥問題再去檢查是否存在循環(huán)觸發(fā)器 check_triggers_cycle(libc-bin)
check_triggers_cycle(libc-bin)第一回合:
將系統(tǒng)中所有存在激活觸發(fā)器的包都收集到 tcn1 節(jié)點的 pkgs 鏈表維護起來(結合trigcyclenode定義)。
將兔指針、龜指針都指向 tcn1 ,并且返回空表明無循環(huán),trigproc 發(fā)現(xiàn)沒有觸發(fā)器循環(huán),直接執(zhí)行 libc-bin的觸發(fā)器。
2、接著處理 deferred 第二個成員 man-db,它的觸發(fā)器是目錄,因為libc-bin 有文件安裝到了 /usr/share/man
check_triggers_cycle(man-db)第二回合:
同樣將這一事件發(fā)生時系統(tǒng)中所有存在激活觸發(fā)器的包都收集到 tcn2 節(jié)點的 pkgs 鏈表維護起來。
將兩次的節(jié)點串成鏈表tcn1, tcn1-> tcn2->next,此時 hare指針移到下一個,指向 tcn2。tortoise 通過一個開關控制,每間隔一次才會使其打開指向下一個。慢一拍。
3、依次類推,如果還有其他 deferred 包,
check_triggers_cycle(man-db)第三回合:
將該時間點時系統(tǒng)中所有存在激活觸發(fā)器的包都收集到 tcn3 節(jié)點的 pkgs 鏈表維護起來。將三次的節(jié)點串成鏈表tcn1, tcn1-> tcn2->tcn3->next,此時 hare指針移到下一個,指向 tcn3, tortoise 開關打開前進指向 tcn2。當 hare 前進到 tcn4 時,tortoise 開關開關還是指向 tcn2。
這里雖然說的是龜兔賽跑算法,但其實實現(xiàn)手段跟常規(guī)的快慢指針相遇不一樣。這里判斷存在循環(huán),用的是一段時間后的范圍覆蓋檢測。因為在許多包的安裝處理過程中,當一個包處理完,那么它的 pending 觸發(fā)器執(zhí)行完成就不再 pending了。比如第一個 libc-bin,判斷沒有循環(huán)直接就執(zhí)行了,它的 trigpend_head 就清空,即:觸發(fā)器的狀態(tài)鏈表是會發(fā)生變化的,總體趨勢是越來越少。
核心判斷函數(shù) tortoise_in_hare,遍歷龜指針 tcn 節(jié)點中的 pkgs 們,對歷史狀態(tài) pending triggers 做判斷。判斷經過若干包的觸發(fā)器處理后,鏈表是否有變化了?上面每一個回合,在每一個時間節(jié)點,我們收集系統(tǒng)中所有存在激活觸發(fā)器的包,都將他們的 trigpend_head 保存到 then_trigs,用它來記錄某一歷史時刻這個包的 pending 的觸發(fā)器名稱。
printf ("比較龜記錄的舊狀態(tài) then_trigs %lx 和當前最新狀態(tài)的 trigpend_head %lx\n", tortoise_pkg->then_trigs, tortoise_pkg->pkg->trigpend_head);
debug(dbg_triggersdetail, "%s pnow=%s tortoise=%s", __func__,
processing_now_name, tortoise_name);
for (tortoise_trig = tortoise_pkg->then_trigs;
tortoise_trig;
tortoise_trig = tortoise_trig->next) {
debug(dbg_triggersdetail,
"%s pnow=%s tortoise=%s tortoisetrig=%s", __func__,
processing_now_name, tortoise_name, tortoise_trig->name);
/* hare 指向當前最新狀態(tài),所以我們直接用實際的數(shù)據(jù)就行 */
for (hare_trig = tortoise_pkg->pkg->trigpend_head;
hare_trig;
hare_trig = hare_trig->next) {
debug(dbg_triggersstupid, "%s pnow=%s tortoise=%s"
" tortoisetrig=%s haretrig=%s", __func__,
processing_now_name, tortoise_name,
tortoise_trig->name, hare_trig->name);
if (strcmp(hare_trig->name, tortoise_trig->name) == 0)
break;
}
if (hare_trig == NULL) {
/* Not found in hare, yay! */
debug(dbg_triggersdetail, "%s pnow=%s tortoise=%s OK",
__func__, processing_now_name, tortoise_name);
return false;
}
}
假如經過一系列處理后,歷史時刻(即龜節(jié)點)的每一個觸發(fā)器,在當前時間點都還存在,一個都沒能被更新處理掉,那就說明可能陷入循環(huán)了,因為后續(xù)操作未引入新包,鏈表狀態(tài)未更新。
一旦檢測到循環(huán)觸發(fā)器,則放棄本次 trigproc 操作,將龜節(jié)點的最早的那個包丟棄,并設置為已安裝未配置的iU狀態(tài),代表一個異常。如果沒有循環(huán),說明包的觸發(fā)器可以執(zhí)行,以 libc-bin 為例,它作為訂閱者并激活,在它的 postinst 腳本定義了 triggered 行為。maintscript_postinst 函數(shù)將執(zhí)行它的 postinst 腳本中的 triggered 相關操作。