PostgreSQL源码分析
生活随笔
收集整理的這篇文章主要介紹了
PostgreSQL源码分析
小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.
PostgreSQL源碼結(jié)構(gòu)
PostgreSQL的使用形態(tài)
PostgreSQL采用C/S(客戶機(jī)/服務(wù)器)模式結(jié)構(gòu)。應(yīng)用層通過INET或者Unix Socket利用既定的協(xié)議與數(shù)據(jù)庫(kù)服務(wù)器進(jìn)行通信。
另外,還有一種‘Standalone Backend’使用的方式, 雖然通過這種方式也可以啟動(dòng)服務(wù)器,但是一般只在數(shù)據(jù)庫(kù)的初始化(PostgreSQL
的cluster的初始化,相當(dāng)于其他數(shù)據(jù)庫(kù)的instance的初始化)、緊急維護(hù)的時(shí)候使用,所以簡(jiǎn)單來說可以認(rèn)為PostgreSQL是使用C/S的形
式進(jìn)行訪問的。
PostgreSQL把客戶端稱為前端(Frontend),把服務(wù)器端成為后端(Backend), 后端有復(fù)數(shù)個(gè)進(jìn)程構(gòu)成,這個(gè)在后面會(huì)進(jìn)行說明。
前端和后端通信的協(xié)議在PostgreSQL的官方文檔中的《前端和后端的通信協(xié)議》一章中有詳細(xì)的說明。簡(jiǎn)單來說,大體的工作模式是:
前端向后端發(fā)送查詢的SQL文,然后后端通過復(fù)數(shù)個(gè)報(bào)文把結(jié)果返回給前端。
由于需要進(jìn)行連接的初始化、錯(cuò)誤等各種各樣處理,PostgreSQL的協(xié)議的處理也是相當(dāng)復(fù)雜,如果要自己從頭實(shí)現(xiàn)這些協(xié)議的處理的話
,還是相當(dāng)麻煩的,所以PostgreSQL本身提供了C語(yǔ)言寫的libpq這樣一個(gè)協(xié)議處理庫(kù),利用這個(gè)庫(kù)可以比較輕松地和后端進(jìn)行通信。
PostgreSQL的話除了C以外,還支持Perl和PHP等其他語(yǔ)言,這些語(yǔ)言在內(nèi)部也調(diào)用了libpq.
也有不使用libpq而直接與PostgreSQL通信的庫(kù)。比較具有代表性的是Java, PostgreSQL的JDBC驅(qū)動(dòng)是不依賴于libpq直接與PostgreSQL
通信的.
另外后端的話,比較核心的是進(jìn)行數(shù)據(jù)庫(kù)處理的數(shù)據(jù)庫(kù)引擎(Database Engine)。 數(shù)據(jù)庫(kù)引擎可以對(duì)用戶所編寫的函數(shù)進(jìn)行解析和處理
,用戶如果能夠利用好這個(gè)功能的話,可以柔軟地?cái)U(kuò)展PostgreSQL的功能。 比較經(jīng)常使用的是存儲(chǔ)過程(PostgreSQL中稱為用戶自定義
函數(shù)),PostgreSQL支持的用戶定義函數(shù)的語(yǔ)言如下:
語(yǔ)言 ? 對(duì)應(yīng)的自定義函數(shù)
C ? C函數(shù)
SQL ? SQL 函數(shù)
類似Oracle的PL/SQL的語(yǔ)言 PL/pgSQL
Perl ? PL/Perl
Python ? ?PL/Python
PostgreSQL的話,用戶可以自定義語(yǔ)言處理引擎。各種服務(wù)器腳本語(yǔ)言的解析引擎,以第三方的形式存在,主要的處理語(yǔ)言有Ruby、
Java以及PHP等。
PostgreSQL的結(jié)構(gòu)
這里的話,再詳細(xì)看看PostgreSQL的結(jié)構(gòu)。 后端由幾個(gè)進(jìn)程構(gòu)成。?
Potgres(常駐進(jìn)程)
管理后端的常駐進(jìn)程,也稱為’postmaster’。其默認(rèn)監(jiān)聽UNIX Domain Socket和TCP/IP(Windows等,一部分的平臺(tái)只監(jiān)聽tcp/ip)的
5432端口,等待來自前端的的連接處理。監(jiān)聽的端口號(hào)可以在PostgreSQL的設(shè)置文件postgresql.conf里面可以改。
一旦有前端連接過來,postgres會(huì)通過fork(2)生成子進(jìn)程。沒有Fork(2)的windows平臺(tái)的話,則利用createProcess()生成新的進(jìn)程。
這種情形的話,和fork(2)不同的是,父進(jìn)程的數(shù)據(jù)不會(huì)被繼承過來,所以需要利用共享內(nèi)存把父進(jìn)程的數(shù)據(jù)繼承過來。
Postgres(子進(jìn)程)
子進(jìn)程根據(jù)pg_hba.conf定義的安全策略來判斷是否允許進(jìn)行連接,根據(jù)策略,會(huì)拒絕某些特定的IP及網(wǎng)絡(luò),或者也可以只允許某些特定
的用戶或者對(duì)某些數(shù)據(jù)庫(kù)進(jìn)行連接。
Postgres會(huì)接受前端過來的查詢,然后對(duì)數(shù)據(jù)庫(kù)進(jìn)行檢索,最好把結(jié)果返回,有時(shí)也會(huì)對(duì)數(shù)據(jù)庫(kù)進(jìn)行更新。更新的數(shù)據(jù)同時(shí)還會(huì)記錄在
事務(wù)日志里面(PostgreSQL稱為WAL日志),這個(gè)主要是當(dāng)停電的時(shí)候,服務(wù)器當(dāng)機(jī),重新啟動(dòng)的時(shí)候進(jìn)行恢復(fù)處理的時(shí)候使用的。另外
,把日志歸檔保存起來,可在需要進(jìn)行恢復(fù)的時(shí)候使用。在PostgreSQL 9.0以后,通過把WAL日志傳送其他的postgreSQL,可以實(shí)時(shí)得進(jìn)
行數(shù)據(jù)庫(kù)復(fù)制,這就是所謂的‘?dāng)?shù)據(jù)庫(kù)復(fù)制’功能。
其他的進(jìn)程
Postgres之外還有一些輔助的進(jìn)程。這些進(jìn)程都是由常駐postgres啟動(dòng)的進(jìn)程。
Writer process
Writer process在適當(dāng)?shù)臅r(shí)間點(diǎn)把共享內(nèi)存上的緩存寫往磁盤。通過這個(gè)進(jìn)程,可以防止在檢查點(diǎn)的時(shí)候(checkpoint),大量的往磁盤寫
而導(dǎo)致性能惡化,使得服務(wù)器可以保持比較穩(wěn)定的性能。Background writer起來以后就一直常駐內(nèi)存,但是并非一直在工作,它會(huì)在工
作一段時(shí)間后進(jìn)行休眠,休眠的時(shí)間間隔通過postgresql.conf里面的參數(shù)bgwriter_delay設(shè)置,默認(rèn)是200微秒。
這個(gè)進(jìn)程的另外一個(gè)重要的功能是定期執(zhí)行檢查點(diǎn)(checkpoint)。
檢查點(diǎn)的時(shí)候,會(huì)把共享內(nèi)存上的緩存內(nèi)容往數(shù)據(jù)庫(kù)文件寫,使得內(nèi)存和文件的狀態(tài)一致。通過這樣,可以在系統(tǒng)崩潰的時(shí)候可以縮短
從WAL恢復(fù)的時(shí)間,另外也可以防止WAL無限的增長(zhǎng)。 可以通過postgresql.conf的checkpoint_segments、checkpoint_timeout指定執(zhí)行
檢查點(diǎn)的時(shí)間間隔。
WAL writer process
WAL writer process把共享內(nèi)存上的WAL緩存在適當(dāng)?shù)臅r(shí)間點(diǎn)往磁盤寫,通過這樣,可以減輕后端進(jìn)程在寫自己的WAL緩存時(shí)的壓力,提
高性能。另外,非同步提交設(shè)為true的時(shí)候,可以保證在一定的時(shí)間間隔內(nèi),把WAL緩存上的內(nèi)容寫入WAL日志文件。
Archive process
Archive process把WAL日志轉(zhuǎn)移到歸檔日志里。如果保存了基礎(chǔ)備份以及歸檔日志,即使實(shí)在磁盤完全損壞的時(shí)候,也可以回復(fù)數(shù)據(jù)庫(kù)
到最新的狀態(tài)。
stats collector process
統(tǒng)計(jì)信息的收集進(jìn)程。收集好統(tǒng)計(jì)表的訪問次數(shù),磁盤的訪問次數(shù)等信息。收集到的信息除了能被autovaccum利用,還可以給其他數(shù)據(jù)
庫(kù)管理員作為數(shù)據(jù)庫(kù)管理的參考信息。
Logger process
把postgresql的活動(dòng)狀態(tài)寫到日志信息文件(并非事務(wù)日志),在指定的時(shí)間間隔里面,對(duì)日志文件進(jìn)行rotate.
Autovacuum啟動(dòng)進(jìn)程
autovacuum launcher process是依賴于postmaster間接啟動(dòng)vacuum進(jìn)程。而其自身是不直接啟動(dòng)自動(dòng)vacuum進(jìn)程的。通過這樣可以提高
系統(tǒng)的可靠性。
自動(dòng)vacuum進(jìn)程
autovacuum worker process進(jìn)程實(shí)際執(zhí)行vacuum的任務(wù)。有時(shí)候會(huì)同時(shí)啟動(dòng)多個(gè)vacuum進(jìn)程。
wal sender / wal receiver
wal sender 進(jìn)程和wal receiver進(jìn)程是實(shí)現(xiàn)postgresql復(fù)制(streaming replication)的進(jìn)程。Wal sender進(jìn)程通過網(wǎng)絡(luò)傳送WAL日志,
而其他PostgreSQL實(shí)例的wal receiver進(jìn)程則接收相應(yīng)的日志。Wal receiver進(jìn)程的宿主PostgreSQL(也稱為Standby)接受到WAL日志
后,在自身的數(shù)據(jù)庫(kù)上還原,生成一個(gè)和發(fā)送端的PostgreSQL(也稱為Master)完全一樣的數(shù)據(jù)庫(kù)。
后端的處理流程
下面看看數(shù)據(jù)庫(kù)引擎postgres子進(jìn)程的處理概要。為了簡(jiǎn)單起見下面的說明中,把backend process簡(jiǎn)稱為backend。Backend的main函數(shù)
是PostgresMain (tcop/postgres.c)。
接收前端發(fā)送過來的查詢(SQL文)
SQL文是單純的文字,電腦是認(rèn)識(shí)不了的,所以要轉(zhuǎn)換成比較容易處理的內(nèi)部形式構(gòu)文樹parser tree,這個(gè)處理的稱為構(gòu)文解析。構(gòu)文解
析的模塊稱為parser.這個(gè)階段只能夠使用文字字面上得來的信息,所以只要沒語(yǔ)法錯(cuò)誤之類的錯(cuò)誤,即使是select不存在的表也不會(huì)報(bào)
錯(cuò)。這個(gè)階段的構(gòu)文樹被稱為raw parse tree. 構(gòu)文處理的入口在raw_parser (parser/parser.c)。
構(gòu)文樹解析完以后,會(huì)轉(zhuǎn)換為查詢樹(Query tree)。這個(gè)時(shí)候,會(huì)訪問數(shù)據(jù)庫(kù),檢查表是否存在,如果存在的話,則把表名轉(zhuǎn)換為OID。
這個(gè)處理稱為分析處理(Analyze), 進(jìn)行分析處理的模塊是analyzer。 另外,PostgreSQL的代碼里面提到構(gòu)文樹parser tree的時(shí)候,更
多的時(shí)候是指查詢樹Query tree。分析處理的模塊的入口在parse_analyze (parser/analyze.c)
PostgreSQL還通過查詢語(yǔ)句的重寫實(shí)現(xiàn)視圖(view)和規(guī)則(rule), 所以需要的時(shí)候,在這個(gè)階段會(huì)對(duì)查詢語(yǔ)句進(jìn)行重寫。這個(gè)處理稱為
重寫(rewrite),重寫的入口在QueryRewrite (rewrite/rewriteHandler.c)。
通過解析查詢樹,可以實(shí)際生成計(jì)劃樹。生成查詢樹的處理稱為‘執(zhí)行計(jì)劃處理’,最關(guān)鍵是要生成估計(jì)能在最短的時(shí)間內(nèi)完成的計(jì)劃
樹(plan tree)。這個(gè)步驟稱為’查詢優(yōu)化’(不叫query optimize, 而是optimize), 而完成這個(gè)處理的模塊稱為查詢優(yōu)化器(不叫query?
optimizer,而是optimizer, 或者稱為planner)。執(zhí)行計(jì)劃處理的入口在standard_planner (optimizer/plan/planner.c)。
按照?qǐng)?zhí)行計(jì)劃里面的步驟可以完成查詢要達(dá)到的目的。運(yùn)行執(zhí)行計(jì)劃樹里面步驟的處理稱為執(zhí)行處理‘execute’, 完成這個(gè)處理的模塊
稱為執(zhí)行器‘Executor’, 執(zhí)行器的入口地址為,ExecutorRun (executor/execMain.c)
執(zhí)行結(jié)果返回給前端。
返回到步驟一重復(fù)執(zhí)行。
PostgreSQL的源碼
現(xiàn)在基本上理解了PostgreSQL的大體的結(jié)構(gòu),我們?cè)賮砜纯碢ostgreSQL代碼的結(jié)構(gòu)。 PostgreSQL初期的時(shí)候,大概只有20萬行左右的代
碼,現(xiàn)在已經(jīng)發(fā)展到100萬行了。這個(gè)量來說,沒有指導(dǎo)讀起來是極為難理解的,這里把大概的代碼結(jié)構(gòu)說明一下,讓大家對(duì)源碼的結(jié)構(gòu)
有個(gè)理解。
第一級(jí)目錄結(jié)構(gòu)
進(jìn)入PostgreSQL的源碼目錄后,第一級(jí)的結(jié)構(gòu)如下表所示。在這一級(jí)里,通過執(zhí)行如下命令configure;make;make install可以立即進(jìn)行
簡(jiǎn)單的安裝,實(shí)際上從PostgreSQL源碼安裝是極為簡(jiǎn)單的。
文件目錄 說明
COPYRIGHT 版權(quán)信息
GUNMakefile 第一級(jí)目錄的 Makefile
GUNMakefile.in Makefile 的雛形
HISTORY ? ? ? ?修改歷史
INSTALL ? ? ? ?安裝方法簡(jiǎn)要說明
Makefile Makefile模版
README ? ? ? ?簡(jiǎn)單說明
aclocal.m4 config 用的文件的一部分
config/ config 用的文件的目錄
configure configure 文件
configure.in configure 文件的雛形
contrib/ contribution 程序
doc/ ? ? ? ?文檔目錄
src/ ? ? ? ?源代碼目錄
PostgreSQL 的src下面有。
文件目錄 說明
DEVELOPERS ? ? ? ?面向開發(fā)人員的注視
Makefile ? ? ? ?Makefile?
Makefile.global make 的設(shè)定值(從configure生成的)
Makefile.global.in Configure使用的Makefile.global的雛形
Makefile.port ? ? ? ?平臺(tái)相關(guān)的make的設(shè)定值,實(shí)際是一個(gè)到makefile/Makefile的連接. (從configure生成的)
Makefile.shlib ? ? ? ?共享庫(kù)用的Makefile
backend/ ? ? ? ?后端的源碼目錄
bcc32.mak ? ? ? ?Win32 ポート用の Makefile (Borland C++ 用)
bin/ ? ? ? ? ? ? ? ?psql 等 UNIX命令的代碼
include/ ? ? ? ?頭文件
interfaces/ ? ? ? ?前端相關(guān)的庫(kù)的代碼
makefiles/ ? ? ? ?平臺(tái)相關(guān)的make 的設(shè)置值
nls-global.mk ? ? ? ?信息目錄用的Makefile文件的規(guī)則
pl/ ? ? ? ? ? ? ? ?存儲(chǔ)過程語(yǔ)言的代碼
port/ ? ? ? ? ? ? ? ?平臺(tái)移植相關(guān)的代碼
template/ ? ? ? ?平臺(tái)相關(guān)的設(shè)置值
test/ ? ? ? ? ? ? ? ?各種測(cè)試腳本
timezone/ ? ? ? ?時(shí)區(qū)相關(guān)代碼
tools/ ? ? ? ? ? ? ? ?各自開發(fā)工具和文檔
tutorial/ ? ? ? ?教程
win32.mak ? ? ? ?Win32 ポート用の Makefile (Visual C++ 用)?
這里比較核心的是backend,bin,interface這幾個(gè)目錄。Backend是對(duì)應(yīng)于后端,bin和interface對(duì)應(yīng)于前端。
bin里面有pgsql,initdb,pg_dump等各種工具的代碼。interface里面有PostgreSQL的C語(yǔ)言的庫(kù)libpq,另外可以在C里嵌入SQL的ECPG命令
的相關(guān)代碼。
Backend目錄的結(jié)構(gòu)如下:
目錄文件 ? ? ? ?說明
Makefile makefile
access/ 各種存儲(chǔ)訪問方法(在各個(gè)子目錄下) common(共同函數(shù))、gin (Generalized Inverted Index通用逆向索引)
?gist (Generalized ?Search Tree通用索引)、 hash (哈希索引)、heap (heap的訪問方法)、
?index (通用索引函數(shù))、 nbtree (Btree函數(shù))、transam (事務(wù)處理)
bootstrap/ 數(shù)據(jù)庫(kù)的初始化處理(initdb的時(shí)候)
catalog/ 系統(tǒng)目錄
commands/ SELECT/INSERT/UPDATE/DELETE以為的SQL文的處理
executor/ 執(zhí)行器(訪問的執(zhí)行)
foreign/ FDW(Foreign Data Wrapper)處理
lib/ ? ? ? ?共同函數(shù)
libpq/ ? ? ? ?前端/后端通信處理
main/ ? ? ? ?postgres的主函數(shù)
nodes/ ? ? ? ?構(gòu)文樹節(jié)點(diǎn)相關(guān)的處理函數(shù)
optimizer/ 優(yōu)化器
parser/ SQL構(gòu)文解析器
port/ ? ? ? ?平臺(tái)相關(guān)的代碼
postmaster/ postmaster的主函數(shù) (常駐postgres)
replication/ streaming replication
regex/ ? ? ? ?正則處理
rewrite/ 規(guī)則及視圖相關(guān)的重寫處理
snowball/ 全文檢索相關(guān)(語(yǔ)干處理)
storage/ 共享內(nèi)存、磁盤上的存儲(chǔ)、緩存等全部一次/二次記錄管理(以下的目錄)buffer/(緩存管理)、 file/(文件)、
freespace/(Fee Space Map管理) ipc/(進(jìn)程間通信)、large_object /(大對(duì)象的訪問函數(shù))、?
lmgr/(鎖管理)、page/(頁(yè)面訪問相關(guān)函數(shù))、 smgr/(存儲(chǔ)管理器)
tcop/ ? ? ? ?postgres (數(shù)據(jù)庫(kù)引擎的進(jìn)程)的主要部分
tsearch/ 全文檢索
utils/ ? ? ? ?各種模塊(以下目錄) adt/(嵌入的數(shù)據(jù)類型)、cache/(緩存管理)、 error/(錯(cuò)誤處理)、fmgr/(函數(shù)管理)、
hash/(hash函數(shù))、 ? ? ? ? init/(數(shù)據(jù)庫(kù)初始化、postgres的初期處理)、 mb/(多字節(jié)文字處理)、
misc/(其他)、mmgr/(內(nèi)存的管理函數(shù))、 resowner/(查詢處理中的數(shù)據(jù)(buffer pin及表鎖)的管理)、
?sort/(排序處理)、time/(事務(wù)的 MVCC 管理)
backend等的代碼的頭文件包含在include里面。其組織雖然與backend的目錄結(jié)構(gòu)類似,但是并非完全相同,基本上來說下一級(jí)的子目錄
不再設(shè)下一級(jí)目錄。例如backend的目錄下面有utils這個(gè)目錄,而util下面還有adt這個(gè)子目錄,但是include里面省略了這個(gè)目錄,變
成了扁平的結(jié)構(gòu)。
access/
bootstrap/
c.h
catalog/
commands/
dynloader.h
executor/
fmgr.h
foreign/
funcapi.h
getaddrinfo.h
getopt_long.h
lib/
libpq/
mb/
miscadmin.h
nodes/
optimizer/
parser/
pg_config.h
pg_config.h.in
pg_config.h.win32
pg_config_manual.h
pg_config_os.h
pg_trace.h
pgstat.h
pgtime.h
port/
port.h
portability/
postgres.h
postgres_ext.h
postgres_fe.h
postmaster/
regex/
rewrite/
rusagestub.h
snowball/
stamp-h
storage/
tcop/
tsearch/
utils/
windowapi.h
代碼的閱讀方法
用調(diào)試器追蹤代碼
PostgreSQL那樣的龐大系統(tǒng),用眼睛來追蹤源碼并不容易。這里推薦用gdb這樣的實(shí)際調(diào)試器來追蹤代碼的執(zhí)行流程。可能有些人畏懼調(diào)
試器,但是如果只是簡(jiǎn)單追蹤代碼的執(zhí)行流的話,還是很簡(jiǎn)單的。
但是多少還是要做一些準(zhǔn)備的,PostgreSQL在編譯的時(shí)候一定要把調(diào)試開關(guān)打開。通常在編譯的時(shí)候configure的時(shí)候加上--enable-
debug的選項(xiàng),然后可能的話可以編輯src/Makefile.global這個(gè)文件
CFLAGS = -O2 -Wall -Wmissing-prototypes -Wpointer-arith \
-Wdeclaration-after-statement -Wendif-labels -Wformat-security \
-fno-strict-aliasing -fwrapv
上面的行的"-O2"選項(xiàng)刪除,然后加上"-g"
CFLAGS = -g -Wall -Wmissing-prototypes -Wpointer-arith \
-Wdeclaration-after-statement -Wendif-labels -Wformat-security \
-fno-strict-aliasing -fwrapv
"-O2"是編譯器的優(yōu)化選項(xiàng),如果打開了,代碼的執(zhí)行順序會(huì)改變,使得追蹤起代碼來比較困難,所以要去除。當(dāng)然這樣的話,編譯后的
可執(zhí)行文件會(huì)比較大,而且會(huì)比較慢,生產(chǎn)環(huán)境不太合適。大家需要理解這個(gè)操作僅僅是在學(xué)習(xí)的時(shí)候而設(shè)置的。
實(shí)際使用gdb試試
下面實(shí)際使用gdb來看看比較簡(jiǎn)單點(diǎn)的select文。
select 1;
select文執(zhí)行后,至executor的其中一個(gè)函數(shù)ExecSelect停止,然后我們調(diào)查一下實(shí)際調(diào)用了那些函數(shù)。
首先以PostgreSQL的超級(jí)用戶登錄。我的環(huán)境是使用t-ishii這個(gè)用戶安裝PostgreSQL的,通常一般使用postgres這個(gè)用戶,大家在閱讀
的時(shí)候替換一下即可。
然后,用psql和數(shù)據(jù)庫(kù)進(jìn)行連接,連接的狀態(tài)可以通過ps命令調(diào)查。
$ ps x
3714 ? ? ? ? ?Ss ? ? 0:00 postgres: t-ishii test [local] idle ? ? ? ? ? ? ? ? ??
可以看到上面的進(jìn)程。這個(gè)就是后端的進(jìn)程。這個(gè)是后端的進(jìn)程,還有其他大量用戶的PostgreSQL的連接也顯示出來,比較難看清楚,
所以還是準(zhǔn)備好測(cè)試的環(huán)境來進(jìn)行測(cè)試比較好。
啟動(dòng)gdb后,附加到ps里顯示的進(jìn)程號(hào)碼。
$ gdb postgres 3714
GNU gdb (GDB) 7.2
Copyright (C) 2010 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later?
<http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law. ?Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-vine-linux".
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>...
Reading symbols from /usr/local/pgsql/bin/postgres...done.
Attaching to program: /usr/local/pgsql/bin/postgres, process 3714
Reading symbols from /lib64/libdl.so.2...done.
Loaded symbols for /lib64/libdl.so.2
Reading symbols from /lib64/libm.so.6...done.
Loaded symbols for /lib64/libm.so.6
Reading symbols from /lib64/libc.so.6...done.
Loaded symbols for /lib64/libc.so.6
Reading symbols from /lib64/ld-linux-x86-64.so.2...done.
Loaded symbols for /lib64/ld-linux-x86-64.so.2
Reading symbols from /lib64/libnss_files.so.2...done.
Loaded symbols for /lib64/libnss_files.so.2
0x00007fad266f82e2 in __libc_recv (fd=<value optimized out>, buf=0xbe9900,
n=8192, flags=<value optimized out>)
? ? at ../sysdeps/unix/sysv/linux/x86_64/recv.c:30
30 ../sysdeps/unix/sysv/linux/x86_64/recv.c:?
in ../sysdeps/unix/sysv/linux/x86_64/recv.c
(gdb)?
(gdb) 是gdb的命令行。在這個(gè)狀態(tài)下,可以接受gdb的命令,如果輸入b命令的話,在ExecResult可以設(shè)置斷點(diǎn)。
(gdb) b ExecResult
Breakpoint 1, ExecResult (node=0xd13eb0) at nodeResult.c:75
(gdb)?
psql啟動(dòng)以后從終端執(zhí)行select 1,輸入以后,后端就會(huì)執(zhí)行該命令。這個(gè)時(shí)候,postgres進(jìn)程已經(jīng)暫停,所以psql會(huì)動(dòng)不了。要繼續(xù)執(zhí)
行的話,可在gdb里執(zhí)行"c"命令。執(zhí)行了以后,就會(huì)在ExecResult 處停止。
Continuing.
Breakpoint 1, ExecResult (node=0xd13eb0) at nodeResult.c:75
75 econtext = node->ps.ps_ExprContext;
(gdb)?
到ExecSelect為止的函數(shù)的調(diào)用路徑可以用bt的命令顯示出來。
(gdb) bt
#0 ?ExecResult (node=0xd13eb0) at nodeResult.c:75
#1 ?0x00000000005b92a4 in ExecProcNode (node=0xd13eb0) at execProcnode.c:367
#2 ?0x00000000005b71bb in ExecutePlan (estate=0xd13da0, planstate=0xd13eb0,
? ? operation=CMD_SELECT, sendTuples=1 '\001', numberTuples=0,?
? ? direction=ForwardScanDirection, dest=0xcf9938) at execMain.c:1439
#3 ?0x00000000005b5835 in standard_ExecutorRun (queryDesc=0xc62820,?
? ? direction=ForwardScanDirection, count=0) at execMain.c:313
#4 ?0x00000000005b5729 in ExecutorRun (queryDesc=0xc62820,?
? ? direction=ForwardScanDirection, count=0) at execMain.c:261
#5 ?0x00000000006d2f79 in PortalRunSelect (portal=0xc60810,?
? ? forward=1 '\001', count=0, dest=0xcf9938) at pquery.c:943
#6 ?0x00000000006d2c4e in PortalRun (portal=0xc60810,?
? ? count=9223372036854775807, isTopLevel=1 '\001', dest=0xcf9938,?
? ? altdest=0xcf9938, completionTag=0x7fffa4b0eeb0 "") at pquery.c:787
#7 ?0x00000000006cd135 in exec_simple_query?
? ? (query_string=0xcf8420 "select 1;") at postgres.c:1018
#8 ?0x00000000006d1144 in PostgresMain (argc=2, argv=0xc42da0,?
? ? username=0xc42c40 "t-ishii") at postgres.c:3926
#9 ?0x0000000000683ced in BackendRun (port=0xc65600) at postmaster.c:3600
#10 0x00000000006833dc in BackendStartup (port=0xc65600) at postmaster.c:3285
#11 0x0000000000680759 in ServerLoop () at postmaster.c:1454
#12 0x000000000067ff4d in PostmasterMain (argc=3, argv=0xc40e00)?
? ?at postmaster.c:1115
#13 0x00000000005f7a39 in main (argc=3, argv=0xc40e00) at main.c:199
(gdb)?
說明一下看的方法,發(fā)起調(diào)用的函數(shù)在下面,被調(diào)用的函數(shù)在上面。也就是ExecProcNode調(diào)用了ExecResult,ExecutePlan調(diào)用了
ExecProcNode,ExecutorRun調(diào)用了ExecProcNode,這樣的形式來寫。特別是中間的第7行。
#7 ?0x00000000006cd135 in exec_simple_query?
? ? (query_string=0xcf8420 "select 1;") at postgres.c:1018
這樣可以清楚看到在處理SELECT文。:)仔細(xì)看gdb的輸出,可以發(fā)現(xiàn)這些細(xì)節(jié)。
gdb是源碼調(diào)試器,所以可以看到和源代碼的對(duì)應(yīng)關(guān)系。例如list命令可以看到現(xiàn)在執(zhí)行的行附近的代碼。
(gdb) list
70 ? ? TupleTableSlot *resultSlot;
71 ? ? PlanState ?*outerPlan;
72 ? ? ExprContext *econtext;
73 ? ? ExprDoneCond isDone;
74?
75 ? ? econtext = node->ps.ps_ExprContext;
76?
77 ? ? /*
78 ? ? ?* check constant qualifications like (2 > 1), if not already done
79 ? ? ?*/
利用up命令可以往上面的函數(shù)移動(dòng)。下面用list命令,可以確認(rèn)實(shí)際調(diào)用ExecSelect 的地方。
(gdb) up
#1 ?0x00000000005b92a4 in ExecProcNode (node=0xd13eb0) at execProcnode.c:367
367 ? ? ? ? ? ? result = ExecResult((ResultState *) node);
(gdb) list
362 ? ? {
363 ? ? ? ? ? ? /*
364 ? ? ? ? ? ? ?* control nodes
365 ? ? ? ? ? ? ?*/
366 ? ? ? ? case T_ResultState:
367 ? ? ? ? ? ? result = ExecResult((ResultState *) node);
368 ? ? ? ? ? ? break;
369?
370 ? ? ? ? case T_ModifyTableState:
371 ? ? ? ? ? ? result = ExecModifyTable((ModifyTableState *) node);
利用down可以往下面的函數(shù)移動(dòng)。利用up和down的組合,可以調(diào)查函數(shù)的調(diào)用關(guān)系。
要退出gdb的話可以用quit。
(gdb) quit
Inferior 1 [process 3714] will be detached.
Quit anyway? (y or n) y
Detaching from program: /usr/local/pgsql/bin/postgres, process 3714
到了這里gdb就結(jié)束了,但是后端進(jìn)程并不會(huì)終止。
使用tag來跳轉(zhuǎn)到相應(yīng)的函數(shù)定義文件
我們已經(jīng)使用了gdb來調(diào)查postgreSQL的運(yùn)行,另外用gdb的list來追蹤源碼的話還是相當(dāng)辛苦的,一般來說用emacs等編輯器一起調(diào)查和
瀏覽代碼,可以在邊調(diào)試邊查看代碼。
當(dāng)然,在gdb模式下也可以使用。這個(gè)時(shí)候,例如如果想看看'exec_simple_query'的定義的話,使用emacs的tags命令可以立刻跳轉(zhuǎn)到函
數(shù)定義的地方。要使用tags的話,需要生產(chǎn)tags文件,PostgreSQL的話,帶有生產(chǎn)tags文件的腳本。
$ cd /usr/local/src/postgresql-9.1.1/src
$ tools/make_etags (使用emacs的場(chǎng)合)
$ tools/make_tags (使用vi的場(chǎng)合)
這樣就可以拉。 然后在emacs中,在exec_simple_query 處執(zhí)行'ESC-.'(按了ESC鍵后輸入逗號(hào).),即可打開光標(biāo)所在文字所在的
exec_simple_query函數(shù)的定義文件。
總結(jié)
要完全理解PostgreSQL的話,通過調(diào)查源代碼還是比較有效果的。要理解代碼的話,可以按照目的自己追加必要的功能,改變一些功能
的行為,大家可以最大限度的的享受開源帶來的好處。這次為了讓大家能夠理解PostgreSQL的源代碼,說明了PostgreSQL 9.1的全體結(jié)
構(gòu),還有說明了代碼樹。然后還使用了調(diào)試器來追蹤PostgreSQL的動(dòng)作。
========
PostgreSQL源碼簡(jiǎn)單分析(1)
PostgreSQL源碼簡(jiǎn)單分析(by linux_prog@loveopensource.com)? ?
? ? PostgreSQL是一個(gè)非常強(qiáng)大的開源數(shù)據(jù)庫(kù),既然使開源,當(dāng)然,我們可以去修改他的代碼做任何事情。
最近,忙著設(shè)計(jì)一個(gè)分布式數(shù)據(jù)庫(kù)系統(tǒng),所以,理所當(dāng)然,就想到了在postgresql的基礎(chǔ)上直接改。因此,
分析其源代碼就必不可少了。
? ? 簡(jiǎn)單講一下分析內(nèi)容。
? ? 源碼目錄:
? ? $ cd postgresql-8.2.4/src/backend/
? ? $ ls
? ? access ? ? catalog ? executor ?libpq ?Makefile ?nodes ? ? ?parser ?port ? ? ?postmaster ?rewrite ?tcop
? ? bootstrap ?commands ?lib ? ? ? main ? nls.mk ? ?optimizer ?po ? ? ?postgres ?regex ? ? ? storage ?utils
? ?
? ? 其中:main/main.c是程序啟動(dòng)主文件
? ? ? ? ? 主文件沒有作什么重要的事情,主要是作成為daemon等等一些我們并不關(guān)心的事情。
? ? ? ? ?
? ? ? ? ? tcop/postgres.c是backend執(zhí)行入口文件。
? ? ? ? ? 請(qǐng)看第3414行:
? ? ? ? ? ? ? ?case ‘Q’: ? /* simple query */
? ? {
? ? ?const char *query_string;
? ? ?/* Set statement_timestamp() */
? ? ?SetCurrentStatementStartTimestamp();
? ? ?query_string = pq_getmsgstring(&input_message); //拿到通過libpq傳過來的sql語(yǔ)句
? ? ?
? ? ?pq_getmsgend(&input_message);
? ? ?exec_simple_query(query_string); //執(zhí)行這個(gè)sql,并把結(jié)果通過libpq返回
? ? ?send_ready_for_query = true;
? ? }
? ? break;
? ??
? ? ?再看看postgres.c的第745行:
static void
exec_simple_query(const char *query_string)
{
CommandDest dest = whereToSendOutput;
MemoryContext oldcontext;
List ? ?*parsetree_list;
ListCell ? *parsetree_item;
bool ?save_log_statement_stats = log_statement_stats;
bool ?was_logged = false;
char ?msec_str[32];
/*
? * Report query to various monitoring facilities.
? */
debug_query_string = query_string;
pgstat_report_activity(query_string);
/*
? * We use save_log_statement_stats so ShowUsage doesn’t report incorrect
? * results because ResetUsage wasn’t called.
? */
if (save_log_statement_stats)
? ResetUsage();
/*
? * Start up a transaction command. All queries generated by the
? * query_string will be in this same command block, *unless* we find a
? * BEGIN/COMMIT/ABORT statement; we have to force a new xact command after
? * one of those, else bad things will happen in xact.c. (Note that this
? * will normally change current memory context.)
? */
start_xact_command();
/*
? * Zap any pre-existing unnamed statement. (While not strictly necessary,
? * it seems best to define simple-Query mode as if it used the unnamed
? * statement and portal; this ensures we recover any storage used by prior
? * unnamed operations.)
? */
unnamed_stmt_pstmt = NULL;
if (unnamed_stmt_context)
{
? DropDependentPortals(unnamed_stmt_context);
? MemoryContextDelete(unnamed_stmt_context);
}
unnamed_stmt_context = NULL;
/*
? * Switch to appropriate context for constructing parsetrees.
? */
oldcontext = MemoryContextSwitchTo(MessageContext);
QueryContext = CurrentMemoryContext;
/*
? * Do basic parsing of the query or queries (this should be safe even if
? * we are in aborted transaction state!)
? */
? ?
? ? // 解析這個(gè)sql語(yǔ)句到一個(gè)語(yǔ)法樹結(jié)構(gòu)中
parsetree_list = pg_parse_query(query_string);
? ?
? ?
? ? 我想做的事情如下:
? ? 在postgresql的基礎(chǔ)上作一個(gè)分布式數(shù)據(jù)庫(kù),但sql parse和backend/frontend的通信都不想自己寫,
? ? 也就是說要使用postgresql的libpq。
? ? 因此做如下實(shí)驗(yàn):
? ? 任何sql語(yǔ)句進(jìn)來后,我會(huì)在exec_simple_query里面捷獲,如果是一個(gè)select語(yǔ)句,
? ? 我會(huì)返回一行記錄:列名—name ? 列值– lijianghua
? ?
繼續(xù)分析文件: src/access/common/printtup.c
//以下函數(shù)使通過libpq發(fā)送返回的列的column 描述信息的
void
SendRowDescriptionMessage(TupleDesc typeinfo, List *targetlist, int16 *formats)
{
Form_pg_attribute *attrs = typeinfo->attrs;
int ? natts = typeinfo->natts;
int ? proto = PG_PROTOCOL_MAJOR(FrontendProtocol);
int ? i;
StringInfoData buf;
ListCell ? *tlist_item = list_head(targetlist);
pq_beginmessage(&buf, ‘T’); /* tuple descriptor message type */
pq_sendint(&buf, natts, 2); /* # of attrs in tuples */
for (i = 0; i < natts; ++i)
{
? Oid ? atttypid = attrs->atttypid;
? int32 ?atttypmod = attrs->atttypmod;
? pq_sendstring(&buf, NameStr(attrs->attname));
? /* column ID info appears in protocol 3.0 and up */
? if (proto >= 3)
? {
? ?/* Do we have a non-resjunk tlist item? */
? ?while (tlist_item &&
? ? ? ?((TargetEntry *) lfirst(tlist_item))->resjunk)
? ? tlist_item = lnext(tlist_item);
? ?if (tlist_item)
? ?{
? ? TargetEntry *tle = (TargetEntry *) lfirst(tlist_item);
? ? pq_sendint(&buf, tle->resorigtbl, 4);
? ? pq_sendint(&buf, tle->resorigcol, 2);
? ? tlist_item = lnext(tlist_item);
? ?}
? ?else
? ?{
? ? /* No info available, so send zeroes */
? ? pq_sendint(&buf, 0, 4);
? ? pq_sendint(&buf, 0, 2);
? ?}
? }
? /* If column is a domain, send the base type and typmod instead */
? atttypid = getBaseTypeAndTypmod(atttypid, &atttypmod);
? pq_sendint(&buf, (int) atttypid, sizeof(atttypid));
? pq_sendint(&buf, attrs->attlen, sizeof(attrs->attlen));
? /* typmod appears in protocol 2.0 and up */
? if (proto >= 2)
? ?pq_sendint(&buf, atttypmod, sizeof(atttypmod));
? /* format info appears in protocol 3.0 and up */
? if (proto >= 3)
? {
? ?if (formats)
? ? pq_sendint(&buf, formats, 2);
? ?else
? ? pq_sendint(&buf, 0, 2);
? }
}
pq_endmessage(&buf);
}
//下面這個(gè)函數(shù)是select返回的數(shù)據(jù)的值,每一行數(shù)據(jù)都會(huì)調(diào)用一下這個(gè)函數(shù)
static void
printtup(TupleTableSlot *slot, DestReceiver *self)
{
TupleDesc typeinfo = slot->tts_tupleDescriptor;
DR_printtup *myState = (DR_printtup *) self;
StringInfoData buf;
int ? natts = typeinfo->natts;
int ? i;
/* Set or update my derived attribute info, if needed */
if (myState->attrinfo != typeinfo || myState->nattrs != natts)
? printtup_prepare_info(myState, typeinfo, natts);
/* Make sure the tuple is fully deconstructed */
slot_getallattrs(slot);
/*
? * Prepare a DataRow message
? */
pq_beginmessage(&buf, ‘D’);
pq_sendint(&buf, natts, 2);
/*
? * send the attributes of this tuple
? */
for (i = 0; i < natts; ++i)
{
? PrinttupAttrInfo *thisState = myState->myinfo + i;
? Datum ?origattr = slot->tts_values,
? ? ?attr;
? if (slot->tts_isnull)
? {
? ?pq_sendint(&buf, -1, 4);
? ?continue;
? }
? /*
? ?* If we have a toasted datum, forcibly detoast it here to avoid
? ?* memory leakage inside the type’s output routine.
? ?*/
? if (thisState->typisvarlena)
? ?attr = PointerGetDatum(PG_DETOAST_DATUM(origattr));
? else
? ?attr = origattr;
? if (thisState->format == 0)
? {
? ?/* Text output */
? ?char ? ?*outputstr;
? ?outputstr = OutputFunctionCall(&thisState->finfo, attr);
? ?pq_sendcountedtext(&buf, outputstr, strlen(outputstr), false);
? ?pfree(outputstr);
? }
? else
? {
? ?/* Binary output */
? ?bytea ? ?*outputbytes;
? ?outputbytes = SendFunctionCall(&thisState->finfo, attr);
? ?pq_sendint(&buf, VARSIZE(outputbytes) - VARHDRSZ, 4);
? ?pq_sendbytes(&buf, VARDATA(outputbytes),
? ? ? ?VARSIZE(outputbytes) - VARHDRSZ);
? ?pfree(outputbytes);
? }
? /* Clean up detoasted copy, if any */
? if (attr != origattr)
? ?pfree(DatumGetPointer(attr));
}
pq_endmessage(&buf);
}
根據(jù)以上分析,我來修改exec_simple_query:
? ?在833行加入如下內(nèi)容:
? ?//此范例只處理select語(yǔ)句
? ?if(parsetree->type == T_SelectStmt)
? ?{
? ? ? ? StringInfoData buf;
? ? ? ? pq_beginmessage(&buf, ‘T’); ?/* tuple descriptor message type */
? ? ? ? pq_sendint(&buf, 1, 2); ? ? ?/* number of columns in tuples */
? ? ? ? pq_sendstring(&buf, “name”); // column名稱
? ? ? ? pq_sendint(&buf, 0, 4); ? ??
? ? ? ? pq_sendint(&buf, 0, 2);
? ? ? ? pq_sendint(&buf, 0, 4);
? ? ? ? pq_sendint(&buf, 2, 2);
? ? ? ? pq_sendint(&buf, 0, 4);
? ? ? ? pq_sendint(&buf, 0, 2);
? ? ? ? pq_endmessage(&buf);
? ? ? ?
? ? ? ? pq_beginmessage(&buf, ‘D’);
? ? ? ? pq_sendint(&buf, 1, 2);
? ? ? ? pq_sendcountedtext(&buf, “l(fā)ijianghua”, 10, false);
? ? ? ? pq_endmessage(&buf);
? ? ? ?
? ? ? ? //此行必須加上,告訴libpq返回結(jié)果結(jié)束(C代表completed)
? ? ? ? pq_puttextmessage(’C', “select return 1 rows”);
? ? ? ?
? ? ? ? return;
? ?}
??
? ?修改結(jié)束,按照正常流程編譯PostgreSQL,并啟動(dòng)。
? ?測(cè)試結(jié)果:
[mypg@webtrends mypg]$ psql
Welcome to psql 8.2.4, the PostgreSQL interactive terminal.
Type: ?\copyright for distribution terms
? ? ? ?\h for help with SQL commands
? ? ? ?\? for help with psql commands
? ? ? ?\g or terminate with semicolon to execute query
? ? ? ?\q to quit
mypg=# \d
List of relations
? ? name ??
————
lijianghua
(1 row)
mypg=# select * from test2;
? ? name ??
————
lijianghua
(1 row)
mypg=# select * from test3;
? ? name ??
————
lijianghua
(1 row)
mypg=# select * from test5;
? ? name ??
————
lijianghua
(1 row)
? ? 可以看到任何select語(yǔ)句都只返回我們預(yù)定義的結(jié)果,說明我們當(dāng)初的想法是可行的(\d其實(shí)也是一個(gè)select語(yǔ)句)。
========
PostgreSQL存儲(chǔ)引擎源碼分析一
PostgreSQL的存儲(chǔ)系統(tǒng)作為PostgreSQL的最低層,向下通過操作系統(tǒng)系統(tǒng)接口訪問物理數(shù)據(jù),向上為存取系統(tǒng)提供由緩沖區(qū)頁(yè)面及頁(yè)面上的接口函數(shù)。
存儲(chǔ)系統(tǒng)的總體架構(gòu)如下圖所示
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 注釋:Lock Manager是鎖管理器,IPC是進(jìn)程間通信,他們實(shí)現(xiàn)了存取層對(duì)存儲(chǔ)層的互
斥訪問,操作。
存儲(chǔ)系統(tǒng)各子系統(tǒng)功能如下:
Page Manager:對(duì)緩沖區(qū)頁(yè)面的結(jié)構(gòu)進(jìn)行定義并提供頁(yè)面的相關(guān)操作。
Buffer Manager:對(duì)共享緩沖區(qū)和本地緩沖區(qū)進(jìn)行管理。
Storage Manager:屏蔽不同物理設(shè)備接口函數(shù)的差異,向Buffer Manager提供統(tǒng)一的接口。
File Manager:一般的操作系統(tǒng)只允許一個(gè)進(jìn)程打開256個(gè)文件,而PostgreSQL服務(wù)器在工作時(shí)需要打開的文件會(huì)很多,因此,其使用
File Manager來封裝操作系統(tǒng)文件讀寫的函數(shù)。
下面對(duì)Page Manager的一段代碼進(jìn)行分析:Page Manager模塊的功能上面已經(jīng)講到過,這里便不再贅述,這個(gè)模塊主要由三個(gè)文件組成
:源碼根目錄下的backend\storage\page路徑下的bufpage.c,itemptr.c,以及根目錄下include\storage路徑的頭文件bufpage.h組成。
頁(yè)面Page的結(jié)構(gòu)大致如下:頁(yè)面由頁(yè)首部,頁(yè)面存儲(chǔ)記錄的ID,存儲(chǔ)的記錄以及特殊空間所組成。其中,頁(yè)面首部定義在bufpage.h文件
中。如下所示:
typedef struct PageHeaderData
{
?XLogRecPtr pd_lsn; ? /* XLogRecPtr是定義在/include/access/xlogdefs.h中的一個(gè)結(jié)構(gòu)體,定義了存取層所用的日志文件 ,期待
其他模塊同學(xué)的完善。*/
?uint16 ?pd_tli; ? /* 善未搞明白。。。*/
?uint16 ?pd_flags; ?/* 頁(yè)首部標(biāo)志*/
?LocationIndex pd_lower; ?/* 頁(yè)面空閑區(qū)域起始偏移量 ,LocationIndex 是無符號(hào)16位整型數(shù)據(jù),下同*/
?LocationIndex pd_upper; ?/* 頁(yè)面空閑區(qū)域結(jié)束偏移量 */
?LocationIndex pd_special; /* 頁(yè)面特殊區(qū)域起始偏移量 */
?uint16 ?pd_pagesize_version;/* 頁(yè)面大小*/
?TransactionId pd_prune_xid; /* 不重要的XID, 如果為空則為0 */
?ItemIdData pd_linp[1]; ?/* ItemidData是定義在/include/storage/itemid.h中的結(jié)構(gòu)體,主要定義了元組項(xiàng)的底層特征:元組在頁(yè)
面上的偏移量,元組項(xiàng)指針的狀態(tài),元組的比特位長(zhǎng)度,這里是定義了一個(gè)元組項(xiàng)的一個(gè)指針,指向頁(yè)面不同的元組項(xiàng)(也就是記錄)?
*/
} PageHeaderData;
typedef PageHeaderData *PageHeader;
下面來分析/backend/storage/page/bufpage.c中的PageInit函數(shù)(頁(yè)面初始化)
void PageInit(Page page,Size pageSize,Size specialSize)
{
PageHeader p = (PageHeader) page;
?specialSize = MAXALIGN(specialSize);//MAXALIGN是常量表達(dá)式
?Assert(pageSize == BLCKSZ);//如果頁(yè)面大小和磁盤塊大小相等的話,函數(shù)終止,頁(yè)面初始化失敗
?Assert(pageSize > specialSize + SizeOfPageHeaderData);//SizeofPageHeaderData是定義在bufpage.h中的宏,即offsetof
(PageHeaderData, pd_linp),功能是獲得頁(yè)面首部中pd_linp數(shù)組的偏移量,如果頁(yè)面大小大于特殊空間大小與偏移量之和的話,函數(shù)
終止。
MemSet(p, 0, pageSize);//講頁(yè)首部初始化,清零。
p->pd_lower = SizeOfPageHeaderData;//初始化頁(yè)面空閑區(qū)域起始偏移量
?p->pd_upper = pageSize - specialSize;//初始化頁(yè)面空閑區(qū)域結(jié)束偏移量
?p->pd_special = pageSize - specialSize;//初始化特殊區(qū)域起始偏移量
?PageSetPageSizeAndVersion(page, pageSize, PG_PAGE_LAYOUT_VERSION);//設(shè)置頁(yè)面大小以及頁(yè)面布局的版本號(hào),這是定義在
bufpage.h下的一個(gè)宏原型為:#define PageSetPageSizeAndVersion(page, size, version) \
( \
?AssertMacro(((size) & 0xFF00) == (size)), \
?AssertMacro(((version) & 0x00FF) == (version)), \
?((PageHeader) (page))->pd_pagesize_version = (size) | (version) \
)里面基本上是一些位操作:將頁(yè)面大小的后16位置0,版本號(hào)的前16為置0.
}
========
?PostgreSQL源碼分析之page
? ? 前面幾篇博客分析了shared buffer,從shared buffer到磁盤文件的映射,到shared buffer的分配和替換,再到如何測(cè)量shared?
buffer的性能情況,配置是否合理,基本把shared buffer大概介紹了下,這篇博客主要分析page。
? ?page的源碼落在/src/backend/storage/page,page對(duì)于PostgreSQL是個(gè)什么概念?page,block,file,這些概念怎么理解?
? ?文件是數(shù)據(jù)庫(kù)的持久化存儲(chǔ),當(dāng)然我們已經(jīng)知道數(shù)據(jù)庫(kù)的relation以文件的形式存在在磁盤上,無論是xxx文件還是xxx_fsm,還是
xxx_vm,這是文件的概念。當(dāng)relation的xxx的文件特別大,超過1G的時(shí)候,同一個(gè)relation還會(huì)分文件存儲(chǔ),出現(xiàn)xxx.1,xxx.2這種文
件。Whatever,文件在,PostgreSQL數(shù)據(jù)庫(kù)的信息就在。
? ?所謂block,指的是每次加載進(jìn)內(nèi)存的基本單位,如果PostgreSQL需要某個(gè)relation的信息,不會(huì)是直接relation對(duì)應(yīng)的磁盤文件全
部讀入內(nèi)存,而是分block載入內(nèi)存。PostgreSQL有一定的規(guī)則知道自己需要的信息或者記錄或者說tuple 落在磁盤文件的那個(gè)8K block
上,然后將8K block加載入local buffer,或者shared buffer,總之加載入內(nèi)存。簡(jiǎn)單的說,block是磁盤上文件和內(nèi)存之間加載/驅(qū)逐
的基本單位。
? ?page是個(gè)什么概念呢?page大小也是8K,就是上面提到的block,只不過,page仔細(xì)的端詳了8KB的內(nèi)容,分析了信息是如何組織,如
何存放到8KB的block空間之內(nèi)。注意每條記錄內(nèi)部的結(jié)構(gòu)不是page關(guān)心的事情,他的視角沒有這個(gè)細(xì),我們關(guān)心的是這條記錄作為一個(gè)
整體如何存放到8K的page中去;當(dāng)然8K的page可能存放多條記錄,如何擺放到8K的page中去;當(dāng)前page剩余空間還有多少;我有一條需要空
間為size的記錄,page是否有足夠的空間容納之;記錄可能會(huì)插入,也可能會(huì)刪除,page里面會(huì)不會(huì)因?yàn)閯h除動(dòng)作,頁(yè)面內(nèi)部有很多的洞
,或者頁(yè)面碎片化,如何清理碎片,這些都是page要解決的問題。
? ?簡(jiǎn)而言之,page,就是管理8K大小的一畝三分地,他要把多條記錄(Tuple)有條不紊地組織在這8K的空間之內(nèi)。
? ? 一條記錄會(huì)插入到8KB的page之中,信息如何組織?自然大多數(shù)記錄占用的空間不會(huì)超過8KB,以我們前邊提到的friends為例:
? ??
? ? 這個(gè)friends的設(shè)計(jì)不太好,不過我們的重點(diǎn)不在于此,我們關(guān)心的是這長(zhǎng)度為8192(1個(gè)Block或者說1個(gè)page)的文件,到底存放
的是啥內(nèi)容?
? ??
? ?我們看到文件雖然有8K,但是實(shí)際上只有最前面的2行32字節(jié),和最后面的64字節(jié)中包含信息,因?yàn)檫@個(gè)文件對(duì)應(yīng)的就是我們的
friends這張表,而這張表里面有Lee,Bean ,158XXXXX,Nan Jing等信息,當(dāng)然了這是一條記錄,或者一個(gè)Tuple,Tuple內(nèi)部的組成或
者layout我們不關(guān)心,但是這個(gè)16385文件作為一定記錄了這些信息。我們用vbindiff查看之:
? ?
? ?
? ?我們看到了,我們的信息Bean,Nan Jing之類的,不管是如何組織的,的確存儲(chǔ)在表friend對(duì)應(yīng)文件16385之中。這條記錄如何放入
8K的空間之內(nèi),頭部的一些字符有是干啥的,記錄的信息為何放到了現(xiàn)在的這個(gè)位置,這就是page要管的事情,我們下面詳查之。
??
? ? 上圖就是page的結(jié)構(gòu)圖,8K的空間包括一個(gè)頭部Page Header,若干個(gè)Item,每個(gè)Item指向一條記錄(Tuple),有些Page在初始化
的時(shí)候,就page的末尾,預(yù)留出空間作為Special用,作什么用,我暫時(shí)不知,不過沒關(guān)系,不影響我們理解Page。當(dāng)然了,有些Page不
需要Special空間,就沒有預(yù)留。
? ?好我們可以分析源碼了。
? ?INIT-page的初始化
? ? 首當(dāng)其沖的是PageInit函數(shù)。我們申請(qǐng)了一個(gè)新的干凈的8K的page,把記錄插入page之前,需要將page初始化,基本就是初始化一
下Page Header。: ?
void
PageInit(Page page, Size pageSize, Size specialSize)
{
? ? PageHeader ? ?p = (PageHeader) page;
? ? specialSize = MAXALIGN(specialSize);
? ? Assert(pageSize == BLCKSZ);
? ? Assert(pageSize > specialSize + SizeOfPageHeaderData);
? ? /* Make sure all fields of page are zero, as well as unused space */
? ? MemSet(p, 0, pageSize);
? ? /* p->pd_flags = 0; ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?done by above MemSet */
? ? p->pd_lower = SizeOfPageHeaderData;
? ? p->pd_upper = pageSize - specialSize;
? ? p->pd_special = pageSize - specialSize;
? ? PageSetPageSizeAndVersion(page, pageSize, PG_PAGE_LAYOUT_VERSION);
? ? /* p->pd_prune_xid = InvalidTransactionId; ? ? ? ?done by above MemSet */
}
? ? 對(duì)于pageSize,默認(rèn)情況下就是8K即BLCKSZ,而specialSize,某些情況下為0,某些情況下不為0,這都沒關(guān)系。
? ? Init做的事情是
? ? 1 給special預(yù)留空間 ? ??
? ? specialSize = MAXALIGN(specialSize); ? ? ? ? //4 字節(jié)對(duì)齊
? ? p->pd_special = pageSize - specialSize;
? ? page header的成員變量pd_special相當(dāng)于畫了一條線,從pd_special這個(gè)位置到page的結(jié)尾,都是special的地盤,普通插入Tuple
,都不許進(jìn)入這個(gè)私有地盤。而且這個(gè)pd_special一旦初始化之后,這個(gè)值就不會(huì)動(dòng)了。
? ? 2 設(shè)置pd_lower和pg_upper
? ?當(dāng)初始化的時(shí)候,pd_lower設(shè)置為SizeOfPageHeaderData,pd_upper設(shè)置為和pd_special一樣。但是注意,這個(gè)lower和upper不是固
定的,隨著Tuple的不斷插入,lower變大,而upper不斷變小。當(dāng)我們每插入一條Tuple,需要在當(dāng)前的lower位置再分配一個(gè)Item,記錄
Tuple的長(zhǎng)度,Tuple的起始位置offset,還有flag信息。這個(gè)Page Header中的pd_lower就是記錄分配下一個(gè)Item的起始位置。所以如果
不斷插入,lower不斷增加,每增加一條Tuple,就要分配一個(gè)Item(4個(gè)字節(jié))。同樣道理,Tuple的存放位置,根據(jù)upper提供的信息,
可以找到將Tuple分配到何處比較合。分配之后,pd_upper就會(huì)減少,減少Tuple的長(zhǎng)度(對(duì)齊也考慮進(jìn)去)。
? ? 3 設(shè)置 page的size 和version
#define PageSetPageSizeAndVersion(page, size, version)?
(?
? ? AssertMacro(((size) & 0xFF00) == (size)),?
? ? AssertMacro(((version) & 0x00FF) == (version)),?
? ? ((PageHeader) (page))->pd_pagesize_version = (size) | (version)?
)
? ?這個(gè)不多說,基本就是將版本號(hào)和page的長(zhǎng)度記錄在16bit的結(jié)構(gòu)里面。
? ?下面我們比較剛初始化和插入一條記錄之后的情形:
? ?
? ? ?
? ?一個(gè)記錄對(duì)應(yīng)兩個(gè)部分,就頭部附近Item空間和真正記錄信息的Tuple。Item記錄的是Tuple在Page的offset,size等信息。
? ?AddItem-page增加一個(gè)記錄
? ?Page是用來存放Tuple的,增加一個(gè)Tuple刪除一個(gè)Tuple都是Page份內(nèi)的事情,我們首先看下Page如何增加一個(gè)Tuple:
? ?function PageAddItem是完成這件事情。因?yàn)檫@個(gè)接口是很通用的接口,要滿足上層的各種需求,所以稍顯復(fù)雜,不過整體還好。
OffsetNumber
PageAddItem(Page page,
? ? ? ? ? ? Item item,
? ? ? ? ? ? Size size,
? ? ? ? ? ? OffsetNumber offsetNumber,
? ? ? ? ? ? bool overwrite,
? ? ? ? ? ? bool is_heap)
? ?item是我的當(dāng)前記錄的指針,size記錄記錄的長(zhǎng)度,(item,item+size)這部分地址是Tuple的信息。 Page表示從這個(gè)page中查找
空間保存當(dāng)前的Tuple。這我們很好理解,因?yàn)檫@是基本的要求:在當(dāng)前頁(yè)隨便找個(gè)空間保存我的item。咱的要求比較簡(jiǎn)單,可是有些客
戶要求可就不簡(jiǎn)單了,比如客戶要求,就要將我的記錄拜放到page的第三個(gè)item,這就是比較坑爹的客戶了。就像去飯館吃飯,我到了
飯館,喊了一嗓子,小二,給哥隨便找個(gè)8人桌,小二很happy,因?yàn)槲业囊蟮汀R灿锌凸僦苯雍傲艘簧ぷ?#xff0c;小二,我要去三樓最好的
那個(gè)雅間,如果有客人,讓他給我騰地方,我們有8個(gè)人。得,小二就傻了眼,但是還得辦不是。PageAddItem也是一樣,offsetNumber
這個(gè)如參表示,大爺我就要將記錄存放在這個(gè)位置。overwrite則這個(gè)參數(shù)就更拽了,如果有記錄放在我要的位置,讓原來那條記錄給大
爺滾蛋,。如果overwrite =0 表示,大爺要的位置如果有人,原來位置的記錄換個(gè)地方,給大爺我騰地方。OK,這幾個(gè)參數(shù)是干啥的,
我基本交代清楚了
? ?
? ?因?yàn)镻age Header的長(zhǎng)度是固定,而緊跟其后的Item的長(zhǎng)度也是固定的,而每增加一個(gè)Item,pd_lower就增加一個(gè)Item的長(zhǎng)度,這樣,
根據(jù)pd_lower就可以算出當(dāng)前的頁(yè)面已經(jīng)有幾個(gè)Tuple了。
? ? #define PageGetMaxOffsetNumber(page)?
? ? (((PageHeader) (page))->pd_lower <= SizeOfPageHeaderData ? 0 :?
? ? ?((((PageHeader) (page))->pd_lower - SizeOfPageHeaderData)?
? ? ?/ sizeof(ItemIdData)))
limit = OffsetNumberNext(PageGetMaxOffsetNumber(page));
? ? 這個(gè)limit記錄的是當(dāng)前記錄數(shù)+1 ,用這個(gè)來判段新來的AddItem請(qǐng)求有沒有指定既有的位置
if (OffsetNumberIsValid(offsetNumber)) //大爺型請(qǐng)求,值定了記錄的存儲(chǔ)位置
{
? ? ? ? if (overwrite) ? //原有的記錄刪除,屬于要求改寫
? ? ? ? {
? ? ? ? ? ? if (offsetNumber < limit)
? ? ? ? ? ? {
? ? ? ? ? ? ? ? itemId = PageGetItemId(phdr, offsetNumber);
? ? ? ? ? ? ? ? if (ItemIdIsUsed(itemId) || ItemIdHasStorage(itemId))
? ? ? ? ? ? ? ? {
? ? ? ? ? ? ? ? ? ? elog(WARNING, "will not overwrite a used ItemId");
? ? ? ? ? ? ? ? ? ? return InvalidOffsetNumber;
? ? ? ? ? ? ? ? }
? ? ? ? ? ? }
? ? ? ? }
? ? ? ? else ? ? ? ? ? ?//新增加的客戶要求這個(gè)位置,需要將原來位于這個(gè)位置的記錄遷移到其他位置。
? ? ? ? {
? ? ? ? ? ? if (offsetNumber < limit)
? ? ? ? ? ? ? ? needshuffle = true; ? ? ? ?/* need to move existing linp's */
? ? ? ? }
}
else ? //普通客戶
{
? ??
}
? ? 上面分析了文藝青年式的AddItem,下面我們分析下普通青年的AddItem,普通青年要求低,隨便找個(gè)地兒存放當(dāng)年記錄:
? ? if (OffsetNumberIsValid(offsetNumber))
? ? {
? ? ? ? ? ...
? ? }
? ? else
? ? {
? ? ? ? /* offsetNumber was not passed in, so find a free slot */
? ? ? ? /* if no free slot, we'll put it at limit (1st open slot) */
? ? ? ? if (PageHasFreeLinePointers(phdr))
? ? ? ? {
? ? ? ? ? ? /*
? ? ? ? ? ? ?* Look for "recyclable" (unused) ItemId. We check for no storage
? ? ? ? ? ? ?* as well, just to be paranoid --- unused items should never have
? ? ? ? ? ? ?* storage.
? ? ? ? ? ? ?*/
? ? ? ? ? ? for (offsetNumber = 1; offsetNumber < limit; offsetNumber++)
? ? ? ? ? ? {
? ? ? ? ? ? ? ? itemId = PageGetItemId(phdr, offsetNumber);
? ? ? ? ? ? ? ? if (!ItemIdIsUsed(itemId) && !ItemIdHasStorage(itemId))
? ? ? ? ? ? ? ? ? ? break;
? ? ? ? ? ? }
? ? ? ? ? ? if (offsetNumber >= limit)
? ? ? ? ? ? {
? ? ? ? ? ? ? ? /* the hint is wrong, so reset it */
? ? ? ? ? ? ? ? PageClearHasFreeLinePointers(phdr);
? ? ? ? ? ? }
? ? ? ? }
? ? ? ? else
? ? ? ? {
? ? ? ? ? ? /* don't bother searching if hint says there's no free slot */
? ? ? ? ? ? offsetNumber = limit;
? ? ? ? }
? ? }
? ? 比較容易想到的是offsetNumber = limit = 當(dāng)前記錄數(shù) + 1,這個(gè)太順理成章了,那個(gè)PageHasFreeLinePointers是搞什么飛機(jī)?我
們看下:
#define PageHasFreeLinePointers(page)?
? ? (((PageHeader) (page))->pd_flags & PD_HAS_FREE_LINES)
? ? 這個(gè)標(biāo)志是啥意思?看名字的意思是 表征是否有free line。我們會(huì)把一些Item狀態(tài)置為L(zhǎng)P_UNUSED,這時(shí)候,Item和它原來的
Tuple就沒有映射關(guān)系。這樣原來對(duì)應(yīng)Tuple,就成了垃圾。后面會(huì)有會(huì)PageRepairFragmentation清理這些空間,但是仍然不會(huì)刪除這個(gè)
LP_UNUSED狀態(tài)的Item,只是打上一個(gè)標(biāo)志,表示存在無主的Item,可以被復(fù)用。
? ? if (offsetNumber == limit || needshuffle)
? ? ? ? lower = phdr->pd_lower + sizeof(ItemIdData); //新增一個(gè)Item
? ? else
? ? ? ? lower = phdr->pd_lower; ? ? ? ? ? ? ? ? ? ? ?
? ? alignedSize = MAXALIGN(size);
? ? upper = (int) phdr->pd_upper - (int) alignedSize;
? ? if (lower > upper)
? ? ? ? return InvalidOffsetNumber;
? ? /*
? ? ?* OK to insert the item. First, shuffle the existing pointers if needed.
? ? ?*/
? ? itemId = PageGetItemId(phdr, offsetNumber);
? ? if (needshuffle)
? ? ? ? memmove(itemId + 1, itemId,
? ? ? ? ? ? ? ? (limit - offsetNumber) * sizeof(ItemIdData));
? ? /* set the item pointer */
? ? ItemIdSetNormal(itemId, upper, size);
? ? /* copy the item's data onto the page */
? ? memcpy((char *) page + upper, item, size);
? ? /* adjust page header */
? ? phdr->pd_lower = (LocationIndex) lower;
? ? phdr->pd_upper = (LocationIndex) upper;
? ? return offsetNumber;
? ? 因?yàn)樾略鰝€(gè)Tuple,需要alignedSize存儲(chǔ)這記錄的Tuple部分,所以pd_upper - alignedSize作為新的pd_upper.
? ? ItemIdSetNormal把Tuple的size,offset信息記錄在Item中:
#define ItemIdSetNormal(itemId, off, len)?
(?
? ? (itemId)->lp_flags = LP_NORMAL,?
? ? (itemId)->lp_off = (off), ? ? //記錄offset, page + off = Tuple的起始位置
? ? (itemId)->lp_len = (len) ? ? ?//記錄Tuple的size 。 (page + off ,page + off + len)記錄的是Tuple的信息
)
? ? PageIndexTupleDelete-page刪除一條記錄 ??
? ? 我們下面講述刪除一條記錄:
void
PageIndexTupleDelete(Page page, OffsetNumber offnum)
? ? offnum指示第幾個(gè)記錄,offnum是從1開始計(jì)數(shù)的,查找對(duì)應(yīng)item 是offnum-1.
? ?我們找到Item,就可以找到Tuple對(duì)應(yīng)的offset和size: ? ?
? ? tup = PageGetItemId(page, offnum);
? ? Assert(ItemIdHasStorage(tup));
? ? size = ItemIdGetLength(tup);
? ? offset = ItemIdGetOffset(tup);
??
? ?刪除第二個(gè)記錄之后,我們得到的Page布局如下:
??
? ?我們可以看到,至少發(fā)生兩次memmove
? ?1 刪除記錄的Item后面的item都要往遷移,防止出現(xiàn)一個(gè)空洞
? ? nbytes = phdr->pd_lower -
? ? ? ? ((char *) &phdr->pd_linp[offidx + 1] - (char *) phdr);
? ? if (nbytes > 0)
? ? ? ? memmove((char *) &(phdr->pd_linp[offidx]),
? ? ? ? ? ? ? ? (char *) &(phdr->pd_linp[offidx + 1]),
? ? ? ? ? ? ? ? nbytes);
? ?2 刪除記錄的Tuple后面的Tuple,也要移動(dòng),否則,會(huì)出現(xiàn)Tuple-2對(duì)應(yīng)的空洞。?
? ? addr = (char *) page + phdr->pd_upper;
? ? if (offset > phdr->pd_upper)
? ? ? ? memmove(addr + size, addr, (int) (offset - phdr->pd_upper));
? ? 除了移動(dòng)內(nèi)存,item對(duì)應(yīng)的指針也要發(fā)生相應(yīng)的改變:比如洋紅色的兩個(gè)item需要修改offset ?
? ? if (!PageIsEmpty(page))
? ? {
? ? ? ? int ? ? ? ? ? ?i;
? ? ? ? nline--; ? ? ? ? ? ? ? ?/* there's one less than when we started */
? ? ? ? for (i = 1; i <= nline; i++)
? ? ? ? {
? ? ? ? ? ? ItemId ? ? ? ?ii = PageGetItemId(phdr, i);
? ? ? ? ? ? Assert(ItemIdHasStorage(ii));
? ? ? ? ? ? if (ItemIdGetOffset(ii) <= offset) ?//在前面Tuple2 前面的Tuple,發(fā)生了移位,所以對(duì)應(yīng)Item的lp_off要修改。
? ? ? ? ? ? ? ? ii->lp_off += size;
? ? ? ? }
? ? }
? ? Page還剩多少剩余空間這是很重要的,這決定我們能否插入一條記錄到當(dāng)前Page。 原理就非常簡(jiǎn)單了,pd_upper - pd_lower ,就
是剩余空間,但是,還需要存放Item,所以還需要減Item占據(jù)的空間,剩下的才能存放Tuple的空間: ??
Size
PageGetFreeSpace(Page page)
{
? ? int ? ? ? ? ? ?space;
? ? /*
? ? ?* Use signed arithmetic here so that we behave sensibly if pd_lower >
? ? ?* pd_upper.
? ? ?*/
? ? space = (int) ((PageHeader) page)->pd_upper -
? ? ? ? (int) ((PageHeader) page)->pd_lower;
? ? if (space < (int) sizeof(ItemIdData))
? ? ? ? return 0;
? ? space -= sizeof(ItemIdData);
? ? return (Size) space;
}
? ?文章寫的已經(jīng)很長(zhǎng)了,PageIndexMultiDelete 和 PageRepairFragmentation核心邏輯是類似的,我就不寫這兩個(gè)。原來也不難,把
這些碎片化的Tuple排個(gè)序,重新連接成一個(gè)連續(xù)的空間。
========
總結(jié)
以上是生活随笔為你收集整理的PostgreSQL源码分析的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 图解VC++ opengl环境配置和几个
- 下一篇: opengl纹理示例