PostgreSQL源码分析
生活随笔
收集整理的這篇文章主要介紹了
PostgreSQL源码分析
小編覺得挺不錯的,現在分享給大家,幫大家做個參考.
PostgreSQL源碼結構
PostgreSQL的使用形態
PostgreSQL采用C/S(客戶機/服務器)模式結構。應用層通過INET或者Unix Socket利用既定的協議與數據庫服務器進行通信。
另外,還有一種‘Standalone Backend’使用的方式, 雖然通過這種方式也可以啟動服務器,但是一般只在數據庫的初始化(PostgreSQL
的cluster的初始化,相當于其他數據庫的instance的初始化)、緊急維護的時候使用,所以簡單來說可以認為PostgreSQL是使用C/S的形
式進行訪問的。
PostgreSQL把客戶端稱為前端(Frontend),把服務器端成為后端(Backend), 后端有復數個進程構成,這個在后面會進行說明。
前端和后端通信的協議在PostgreSQL的官方文檔中的《前端和后端的通信協議》一章中有詳細的說明。簡單來說,大體的工作模式是:
前端向后端發送查詢的SQL文,然后后端通過復數個報文把結果返回給前端。
由于需要進行連接的初始化、錯誤等各種各樣處理,PostgreSQL的協議的處理也是相當復雜,如果要自己從頭實現這些協議的處理的話
,還是相當麻煩的,所以PostgreSQL本身提供了C語言寫的libpq這樣一個協議處理庫,利用這個庫可以比較輕松地和后端進行通信。
PostgreSQL的話除了C以外,還支持Perl和PHP等其他語言,這些語言在內部也調用了libpq.
也有不使用libpq而直接與PostgreSQL通信的庫。比較具有代表性的是Java, PostgreSQL的JDBC驅動是不依賴于libpq直接與PostgreSQL
通信的.
另外后端的話,比較核心的是進行數據庫處理的數據庫引擎(Database Engine)。 數據庫引擎可以對用戶所編寫的函數進行解析和處理
,用戶如果能夠利用好這個功能的話,可以柔軟地擴展PostgreSQL的功能。 比較經常使用的是存儲過程(PostgreSQL中稱為用戶自定義
函數),PostgreSQL支持的用戶定義函數的語言如下:
語言 ? 對應的自定義函數
C ? C函數
SQL ? SQL 函數
類似Oracle的PL/SQL的語言 PL/pgSQL
Perl ? PL/Perl
Python ? ?PL/Python
PostgreSQL的話,用戶可以自定義語言處理引擎。各種服務器腳本語言的解析引擎,以第三方的形式存在,主要的處理語言有Ruby、
Java以及PHP等。
PostgreSQL的結構
這里的話,再詳細看看PostgreSQL的結構。 后端由幾個進程構成。?
Potgres(常駐進程)
管理后端的常駐進程,也稱為’postmaster’。其默認監聽UNIX Domain Socket和TCP/IP(Windows等,一部分的平臺只監聽tcp/ip)的
5432端口,等待來自前端的的連接處理。監聽的端口號可以在PostgreSQL的設置文件postgresql.conf里面可以改。
一旦有前端連接過來,postgres會通過fork(2)生成子進程。沒有Fork(2)的windows平臺的話,則利用createProcess()生成新的進程。
這種情形的話,和fork(2)不同的是,父進程的數據不會被繼承過來,所以需要利用共享內存把父進程的數據繼承過來。
Postgres(子進程)
子進程根據pg_hba.conf定義的安全策略來判斷是否允許進行連接,根據策略,會拒絕某些特定的IP及網絡,或者也可以只允許某些特定
的用戶或者對某些數據庫進行連接。
Postgres會接受前端過來的查詢,然后對數據庫進行檢索,最好把結果返回,有時也會對數據庫進行更新。更新的數據同時還會記錄在
事務日志里面(PostgreSQL稱為WAL日志),這個主要是當停電的時候,服務器當機,重新啟動的時候進行恢復處理的時候使用的。另外
,把日志歸檔保存起來,可在需要進行恢復的時候使用。在PostgreSQL 9.0以后,通過把WAL日志傳送其他的postgreSQL,可以實時得進
行數據庫復制,這就是所謂的‘數據庫復制’功能。
其他的進程
Postgres之外還有一些輔助的進程。這些進程都是由常駐postgres啟動的進程。
Writer process
Writer process在適當的時間點把共享內存上的緩存寫往磁盤。通過這個進程,可以防止在檢查點的時候(checkpoint),大量的往磁盤寫
而導致性能惡化,使得服務器可以保持比較穩定的性能。Background writer起來以后就一直常駐內存,但是并非一直在工作,它會在工
作一段時間后進行休眠,休眠的時間間隔通過postgresql.conf里面的參數bgwriter_delay設置,默認是200微秒。
這個進程的另外一個重要的功能是定期執行檢查點(checkpoint)。
檢查點的時候,會把共享內存上的緩存內容往數據庫文件寫,使得內存和文件的狀態一致。通過這樣,可以在系統崩潰的時候可以縮短
從WAL恢復的時間,另外也可以防止WAL無限的增長。 可以通過postgresql.conf的checkpoint_segments、checkpoint_timeout指定執行
檢查點的時間間隔。
WAL writer process
WAL writer process把共享內存上的WAL緩存在適當的時間點往磁盤寫,通過這樣,可以減輕后端進程在寫自己的WAL緩存時的壓力,提
高性能。另外,非同步提交設為true的時候,可以保證在一定的時間間隔內,把WAL緩存上的內容寫入WAL日志文件。
Archive process
Archive process把WAL日志轉移到歸檔日志里。如果保存了基礎備份以及歸檔日志,即使實在磁盤完全損壞的時候,也可以回復數據庫
到最新的狀態。
stats collector process
統計信息的收集進程。收集好統計表的訪問次數,磁盤的訪問次數等信息。收集到的信息除了能被autovaccum利用,還可以給其他數據
庫管理員作為數據庫管理的參考信息。
Logger process
把postgresql的活動狀態寫到日志信息文件(并非事務日志),在指定的時間間隔里面,對日志文件進行rotate.
Autovacuum啟動進程
autovacuum launcher process是依賴于postmaster間接啟動vacuum進程。而其自身是不直接啟動自動vacuum進程的。通過這樣可以提高
系統的可靠性。
自動vacuum進程
autovacuum worker process進程實際執行vacuum的任務。有時候會同時啟動多個vacuum進程。
wal sender / wal receiver
wal sender 進程和wal receiver進程是實現postgresql復制(streaming replication)的進程。Wal sender進程通過網絡傳送WAL日志,
而其他PostgreSQL實例的wal receiver進程則接收相應的日志。Wal receiver進程的宿主PostgreSQL(也稱為Standby)接受到WAL日志
后,在自身的數據庫上還原,生成一個和發送端的PostgreSQL(也稱為Master)完全一樣的數據庫。
后端的處理流程
下面看看數據庫引擎postgres子進程的處理概要。為了簡單起見下面的說明中,把backend process簡稱為backend。Backend的main函數
是PostgresMain (tcop/postgres.c)。
接收前端發送過來的查詢(SQL文)
SQL文是單純的文字,電腦是認識不了的,所以要轉換成比較容易處理的內部形式構文樹parser tree,這個處理的稱為構文解析。構文解
析的模塊稱為parser.這個階段只能夠使用文字字面上得來的信息,所以只要沒語法錯誤之類的錯誤,即使是select不存在的表也不會報
錯。這個階段的構文樹被稱為raw parse tree. 構文處理的入口在raw_parser (parser/parser.c)。
構文樹解析完以后,會轉換為查詢樹(Query tree)。這個時候,會訪問數據庫,檢查表是否存在,如果存在的話,則把表名轉換為OID。
這個處理稱為分析處理(Analyze), 進行分析處理的模塊是analyzer。 另外,PostgreSQL的代碼里面提到構文樹parser tree的時候,更
多的時候是指查詢樹Query tree。分析處理的模塊的入口在parse_analyze (parser/analyze.c)
PostgreSQL還通過查詢語句的重寫實現視圖(view)和規則(rule), 所以需要的時候,在這個階段會對查詢語句進行重寫。這個處理稱為
重寫(rewrite),重寫的入口在QueryRewrite (rewrite/rewriteHandler.c)。
通過解析查詢樹,可以實際生成計劃樹。生成查詢樹的處理稱為‘執行計劃處理’,最關鍵是要生成估計能在最短的時間內完成的計劃
樹(plan tree)。這個步驟稱為’查詢優化’(不叫query optimize, 而是optimize), 而完成這個處理的模塊稱為查詢優化器(不叫query?
optimizer,而是optimizer, 或者稱為planner)。執行計劃處理的入口在standard_planner (optimizer/plan/planner.c)。
按照執行計劃里面的步驟可以完成查詢要達到的目的。運行執行計劃樹里面步驟的處理稱為執行處理‘execute’, 完成這個處理的模塊
稱為執行器‘Executor’, 執行器的入口地址為,ExecutorRun (executor/execMain.c)
執行結果返回給前端。
返回到步驟一重復執行。
PostgreSQL的源碼
現在基本上理解了PostgreSQL的大體的結構,我們再來看看PostgreSQL代碼的結構。 PostgreSQL初期的時候,大概只有20萬行左右的代
碼,現在已經發展到100萬行了。這個量來說,沒有指導讀起來是極為難理解的,這里把大概的代碼結構說明一下,讓大家對源碼的結構
有個理解。
第一級目錄結構
進入PostgreSQL的源碼目錄后,第一級的結構如下表所示。在這一級里,通過執行如下命令configure;make;make install可以立即進行
簡單的安裝,實際上從PostgreSQL源碼安裝是極為簡單的。
文件目錄 說明
COPYRIGHT 版權信息
GUNMakefile 第一級目錄的 Makefile
GUNMakefile.in Makefile 的雛形
HISTORY ? ? ? ?修改歷史
INSTALL ? ? ? ?安裝方法簡要說明
Makefile Makefile模版
README ? ? ? ?簡單說明
aclocal.m4 config 用的文件的一部分
config/ config 用的文件的目錄
configure configure 文件
configure.in configure 文件的雛形
contrib/ contribution 程序
doc/ ? ? ? ?文檔目錄
src/ ? ? ? ?源代碼目錄
PostgreSQL 的src下面有。
文件目錄 說明
DEVELOPERS ? ? ? ?面向開發人員的注視
Makefile ? ? ? ?Makefile?
Makefile.global make 的設定值(從configure生成的)
Makefile.global.in Configure使用的Makefile.global的雛形
Makefile.port ? ? ? ?平臺相關的make的設定值,實際是一個到makefile/Makefile的連接. (從configure生成的)
Makefile.shlib ? ? ? ?共享庫用的Makefile
backend/ ? ? ? ?后端的源碼目錄
bcc32.mak ? ? ? ?Win32 ポート用の Makefile (Borland C++ 用)
bin/ ? ? ? ? ? ? ? ?psql 等 UNIX命令的代碼
include/ ? ? ? ?頭文件
interfaces/ ? ? ? ?前端相關的庫的代碼
makefiles/ ? ? ? ?平臺相關的make 的設置值
nls-global.mk ? ? ? ?信息目錄用的Makefile文件的規則
pl/ ? ? ? ? ? ? ? ?存儲過程語言的代碼
port/ ? ? ? ? ? ? ? ?平臺移植相關的代碼
template/ ? ? ? ?平臺相關的設置值
test/ ? ? ? ? ? ? ? ?各種測試腳本
timezone/ ? ? ? ?時區相關代碼
tools/ ? ? ? ? ? ? ? ?各自開發工具和文檔
tutorial/ ? ? ? ?教程
win32.mak ? ? ? ?Win32 ポート用の Makefile (Visual C++ 用)?
這里比較核心的是backend,bin,interface這幾個目錄。Backend是對應于后端,bin和interface對應于前端。
bin里面有pgsql,initdb,pg_dump等各種工具的代碼。interface里面有PostgreSQL的C語言的庫libpq,另外可以在C里嵌入SQL的ECPG命令
的相關代碼。
Backend目錄的結構如下:
目錄文件 ? ? ? ?說明
Makefile makefile
access/ 各種存儲訪問方法(在各個子目錄下) common(共同函數)、gin (Generalized Inverted Index通用逆向索引)
?gist (Generalized ?Search Tree通用索引)、 hash (哈希索引)、heap (heap的訪問方法)、
?index (通用索引函數)、 nbtree (Btree函數)、transam (事務處理)
bootstrap/ 數據庫的初始化處理(initdb的時候)
catalog/ 系統目錄
commands/ SELECT/INSERT/UPDATE/DELETE以為的SQL文的處理
executor/ 執行器(訪問的執行)
foreign/ FDW(Foreign Data Wrapper)處理
lib/ ? ? ? ?共同函數
libpq/ ? ? ? ?前端/后端通信處理
main/ ? ? ? ?postgres的主函數
nodes/ ? ? ? ?構文樹節點相關的處理函數
optimizer/ 優化器
parser/ SQL構文解析器
port/ ? ? ? ?平臺相關的代碼
postmaster/ postmaster的主函數 (常駐postgres)
replication/ streaming replication
regex/ ? ? ? ?正則處理
rewrite/ 規則及視圖相關的重寫處理
snowball/ 全文檢索相關(語干處理)
storage/ 共享內存、磁盤上的存儲、緩存等全部一次/二次記錄管理(以下的目錄)buffer/(緩存管理)、 file/(文件)、
freespace/(Fee Space Map管理) ipc/(進程間通信)、large_object /(大對象的訪問函數)、?
lmgr/(鎖管理)、page/(頁面訪問相關函數)、 smgr/(存儲管理器)
tcop/ ? ? ? ?postgres (數據庫引擎的進程)的主要部分
tsearch/ 全文檢索
utils/ ? ? ? ?各種模塊(以下目錄) adt/(嵌入的數據類型)、cache/(緩存管理)、 error/(錯誤處理)、fmgr/(函數管理)、
hash/(hash函數)、 ? ? ? ? init/(數據庫初始化、postgres的初期處理)、 mb/(多字節文字處理)、
misc/(其他)、mmgr/(內存的管理函數)、 resowner/(查詢處理中的數據(buffer pin及表鎖)的管理)、
?sort/(排序處理)、time/(事務的 MVCC 管理)
backend等的代碼的頭文件包含在include里面。其組織雖然與backend的目錄結構類似,但是并非完全相同,基本上來說下一級的子目錄
不再設下一級目錄。例如backend的目錄下面有utils這個目錄,而util下面還有adt這個子目錄,但是include里面省略了這個目錄,變
成了扁平的結構。
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
代碼的閱讀方法
用調試器追蹤代碼
PostgreSQL那樣的龐大系統,用眼睛來追蹤源碼并不容易。這里推薦用gdb這樣的實際調試器來追蹤代碼的執行流程。可能有些人畏懼調
試器,但是如果只是簡單追蹤代碼的執行流的話,還是很簡單的。
但是多少還是要做一些準備的,PostgreSQL在編譯的時候一定要把調試開關打開。通常在編譯的時候configure的時候加上--enable-
debug的選項,然后可能的話可以編輯src/Makefile.global這個文件
CFLAGS = -O2 -Wall -Wmissing-prototypes -Wpointer-arith \
-Wdeclaration-after-statement -Wendif-labels -Wformat-security \
-fno-strict-aliasing -fwrapv
上面的行的"-O2"選項刪除,然后加上"-g"
CFLAGS = -g -Wall -Wmissing-prototypes -Wpointer-arith \
-Wdeclaration-after-statement -Wendif-labels -Wformat-security \
-fno-strict-aliasing -fwrapv
"-O2"是編譯器的優化選項,如果打開了,代碼的執行順序會改變,使得追蹤起代碼來比較困難,所以要去除。當然這樣的話,編譯后的
可執行文件會比較大,而且會比較慢,生產環境不太合適。大家需要理解這個操作僅僅是在學習的時候而設置的。
實際使用gdb試試
下面實際使用gdb來看看比較簡單點的select文。
select 1;
select文執行后,至executor的其中一個函數ExecSelect停止,然后我們調查一下實際調用了那些函數。
首先以PostgreSQL的超級用戶登錄。我的環境是使用t-ishii這個用戶安裝PostgreSQL的,通常一般使用postgres這個用戶,大家在閱讀
的時候替換一下即可。
然后,用psql和數據庫進行連接,連接的狀態可以通過ps命令調查。
$ ps x
3714 ? ? ? ? ?Ss ? ? 0:00 postgres: t-ishii test [local] idle ? ? ? ? ? ? ? ? ??
可以看到上面的進程。這個就是后端的進程。這個是后端的進程,還有其他大量用戶的PostgreSQL的連接也顯示出來,比較難看清楚,
所以還是準備好測試的環境來進行測試比較好。
啟動gdb后,附加到ps里顯示的進程號碼。
$ 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的命令行。在這個狀態下,可以接受gdb的命令,如果輸入b命令的話,在ExecResult可以設置斷點。
(gdb) b ExecResult
Breakpoint 1, ExecResult (node=0xd13eb0) at nodeResult.c:75
(gdb)?
psql啟動以后從終端執行select 1,輸入以后,后端就會執行該命令。這個時候,postgres進程已經暫停,所以psql會動不了。要繼續執
行的話,可在gdb里執行"c"命令。執行了以后,就會在ExecResult 處停止。
Continuing.
Breakpoint 1, ExecResult (node=0xd13eb0) at nodeResult.c:75
75 econtext = node->ps.ps_ExprContext;
(gdb)?
到ExecSelect為止的函數的調用路徑可以用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)?
說明一下看的方法,發起調用的函數在下面,被調用的函數在上面。也就是ExecProcNode調用了ExecResult,ExecutePlan調用了
ExecProcNode,ExecutorRun調用了ExecProcNode,這樣的形式來寫。特別是中間的第7行。
#7 ?0x00000000006cd135 in exec_simple_query?
? ? (query_string=0xcf8420 "select 1;") at postgres.c:1018
這樣可以清楚看到在處理SELECT文。:)仔細看gdb的輸出,可以發現這些細節。
gdb是源碼調試器,所以可以看到和源代碼的對應關系。例如list命令可以看到現在執行的行附近的代碼。
(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命令可以往上面的函數移動。下面用list命令,可以確認實際調用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可以往下面的函數移動。利用up和down的組合,可以調查函數的調用關系。
要退出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就結束了,但是后端進程并不會終止。
使用tag來跳轉到相應的函數定義文件
我們已經使用了gdb來調查postgreSQL的運行,另外用gdb的list來追蹤源碼的話還是相當辛苦的,一般來說用emacs等編輯器一起調查和
瀏覽代碼,可以在邊調試邊查看代碼。
當然,在gdb模式下也可以使用。這個時候,例如如果想看看'exec_simple_query'的定義的話,使用emacs的tags命令可以立刻跳轉到函
數定義的地方。要使用tags的話,需要生產tags文件,PostgreSQL的話,帶有生產tags文件的腳本。
$ cd /usr/local/src/postgresql-9.1.1/src
$ tools/make_etags (使用emacs的場合)
$ tools/make_tags (使用vi的場合)
這樣就可以拉。 然后在emacs中,在exec_simple_query 處執行'ESC-.'(按了ESC鍵后輸入逗號.),即可打開光標所在文字所在的
exec_simple_query函數的定義文件。
總結
要完全理解PostgreSQL的話,通過調查源代碼還是比較有效果的。要理解代碼的話,可以按照目的自己追加必要的功能,改變一些功能
的行為,大家可以最大限度的的享受開源帶來的好處。這次為了讓大家能夠理解PostgreSQL的源代碼,說明了PostgreSQL 9.1的全體結
構,還有說明了代碼樹。然后還使用了調試器來追蹤PostgreSQL的動作。
========
PostgreSQL源碼簡單分析(1)
PostgreSQL源碼簡單分析(by linux_prog@loveopensource.com)? ?
? ? PostgreSQL是一個非常強大的開源數據庫,既然使開源,當然,我們可以去修改他的代碼做任何事情。
最近,忙著設計一個分布式數據庫系統,所以,理所當然,就想到了在postgresql的基礎上直接改。因此,
分析其源代碼就必不可少了。
? ? 簡單講一下分析內容。
? ? 源碼目錄:
? ? $ 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是程序啟動主文件
? ? ? ? ? 主文件沒有作什么重要的事情,主要是作成為daemon等等一些我們并不關心的事情。
? ? ? ? ?
? ? ? ? ? tcop/postgres.c是backend執行入口文件。
? ? ? ? ? 請看第3414行:
? ? ? ? ? ? ? ?case ‘Q’: ? /* simple query */
? ? {
? ? ?const char *query_string;
? ? ?/* Set statement_timestamp() */
? ? ?SetCurrentStatementStartTimestamp();
? ? ?query_string = pq_getmsgstring(&input_message); //拿到通過libpq傳過來的sql語句
? ? ?
? ? ?pq_getmsgend(&input_message);
? ? ?exec_simple_query(query_string); //執行這個sql,并把結果通過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!)
? */
? ?
? ? // 解析這個sql語句到一個語法樹結構中
parsetree_list = pg_parse_query(query_string);
? ?
? ?
? ? 我想做的事情如下:
? ? 在postgresql的基礎上作一個分布式數據庫,但sql parse和backend/frontend的通信都不想自己寫,
? ? 也就是說要使用postgresql的libpq。
? ? 因此做如下實驗:
? ? 任何sql語句進來后,我會在exec_simple_query里面捷獲,如果是一個select語句,
? ? 我會返回一行記錄:列名—name ? 列值– lijianghua
? ?
繼續分析文件: src/access/common/printtup.c
//以下函數使通過libpq發送返回的列的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);
}
//下面這個函數是select返回的數據的值,每一行數據都會調用一下這個函數
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);
}
根據以上分析,我來修改exec_simple_query:
? ?在833行加入如下內容:
? ?//此范例只處理select語句
? ?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, “lijianghua”, 10, false);
? ? ? ? pq_endmessage(&buf);
? ? ? ?
? ? ? ? //此行必須加上,告訴libpq返回結果結束(C代表completed)
? ? ? ? pq_puttextmessage(’C', “select return 1 rows”);
? ? ? ?
? ? ? ? return;
? ?}
??
? ?修改結束,按照正常流程編譯PostgreSQL,并啟動。
? ?測試結果:
[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語句都只返回我們預定義的結果,說明我們當初的想法是可行的(\d其實也是一個select語句)。
========
PostgreSQL存儲引擎源碼分析一
PostgreSQL的存儲系統作為PostgreSQL的最低層,向下通過操作系統系統接口訪問物理數據,向上為存取系統提供由緩沖區頁面及頁面上的接口函數。
存儲系統的總體架構如下圖所示
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 注釋:Lock Manager是鎖管理器,IPC是進程間通信,他們實現了存取層對存儲層的互
斥訪問,操作。
存儲系統各子系統功能如下:
Page Manager:對緩沖區頁面的結構進行定義并提供頁面的相關操作。
Buffer Manager:對共享緩沖區和本地緩沖區進行管理。
Storage Manager:屏蔽不同物理設備接口函數的差異,向Buffer Manager提供統一的接口。
File Manager:一般的操作系統只允許一個進程打開256個文件,而PostgreSQL服務器在工作時需要打開的文件會很多,因此,其使用
File Manager來封裝操作系統文件讀寫的函數。
下面對Page Manager的一段代碼進行分析:Page Manager模塊的功能上面已經講到過,這里便不再贅述,這個模塊主要由三個文件組成
:源碼根目錄下的backend\storage\page路徑下的bufpage.c,itemptr.c,以及根目錄下include\storage路徑的頭文件bufpage.h組成。
頁面Page的結構大致如下:頁面由頁首部,頁面存儲記錄的ID,存儲的記錄以及特殊空間所組成。其中,頁面首部定義在bufpage.h文件
中。如下所示:
typedef struct PageHeaderData
{
?XLogRecPtr pd_lsn; ? /* XLogRecPtr是定義在/include/access/xlogdefs.h中的一個結構體,定義了存取層所用的日志文件 ,期待
其他模塊同學的完善。*/
?uint16 ?pd_tli; ? /* 善未搞明白。。。*/
?uint16 ?pd_flags; ?/* 頁首部標志*/
?LocationIndex pd_lower; ?/* 頁面空閑區域起始偏移量 ,LocationIndex 是無符號16位整型數據,下同*/
?LocationIndex pd_upper; ?/* 頁面空閑區域結束偏移量 */
?LocationIndex pd_special; /* 頁面特殊區域起始偏移量 */
?uint16 ?pd_pagesize_version;/* 頁面大小*/
?TransactionId pd_prune_xid; /* 不重要的XID, 如果為空則為0 */
?ItemIdData pd_linp[1]; ?/* ItemidData是定義在/include/storage/itemid.h中的結構體,主要定義了元組項的底層特征:元組在頁
面上的偏移量,元組項指針的狀態,元組的比特位長度,這里是定義了一個元組項的一個指針,指向頁面不同的元組項(也就是記錄)?
*/
} PageHeaderData;
typedef PageHeaderData *PageHeader;
下面來分析/backend/storage/page/bufpage.c中的PageInit函數(頁面初始化)
void PageInit(Page page,Size pageSize,Size specialSize)
{
PageHeader p = (PageHeader) page;
?specialSize = MAXALIGN(specialSize);//MAXALIGN是常量表達式
?Assert(pageSize == BLCKSZ);//如果頁面大小和磁盤塊大小相等的話,函數終止,頁面初始化失敗
?Assert(pageSize > specialSize + SizeOfPageHeaderData);//SizeofPageHeaderData是定義在bufpage.h中的宏,即offsetof
(PageHeaderData, pd_linp),功能是獲得頁面首部中pd_linp數組的偏移量,如果頁面大小大于特殊空間大小與偏移量之和的話,函數
終止。
MemSet(p, 0, pageSize);//講頁首部初始化,清零。
p->pd_lower = SizeOfPageHeaderData;//初始化頁面空閑區域起始偏移量
?p->pd_upper = pageSize - specialSize;//初始化頁面空閑區域結束偏移量
?p->pd_special = pageSize - specialSize;//初始化特殊區域起始偏移量
?PageSetPageSizeAndVersion(page, pageSize, PG_PAGE_LAYOUT_VERSION);//設置頁面大小以及頁面布局的版本號,這是定義在
bufpage.h下的一個宏原型為:#define PageSetPageSizeAndVersion(page, size, version) \
( \
?AssertMacro(((size) & 0xFF00) == (size)), \
?AssertMacro(((version) & 0x00FF) == (version)), \
?((PageHeader) (page))->pd_pagesize_version = (size) | (version) \
)里面基本上是一些位操作:將頁面大小的后16位置0,版本號的前16為置0.
}
========
?PostgreSQL源碼分析之page
? ? 前面幾篇博客分析了shared buffer,從shared buffer到磁盤文件的映射,到shared buffer的分配和替換,再到如何測量shared?
buffer的性能情況,配置是否合理,基本把shared buffer大概介紹了下,這篇博客主要分析page。
? ?page的源碼落在/src/backend/storage/page,page對于PostgreSQL是個什么概念?page,block,file,這些概念怎么理解?
? ?文件是數據庫的持久化存儲,當然我們已經知道數據庫的relation以文件的形式存在在磁盤上,無論是xxx文件還是xxx_fsm,還是
xxx_vm,這是文件的概念。當relation的xxx的文件特別大,超過1G的時候,同一個relation還會分文件存儲,出現xxx.1,xxx.2這種文
件。Whatever,文件在,PostgreSQL數據庫的信息就在。
? ?所謂block,指的是每次加載進內存的基本單位,如果PostgreSQL需要某個relation的信息,不會是直接relation對應的磁盤文件全
部讀入內存,而是分block載入內存。PostgreSQL有一定的規則知道自己需要的信息或者記錄或者說tuple 落在磁盤文件的那個8K block
上,然后將8K block加載入local buffer,或者shared buffer,總之加載入內存。簡單的說,block是磁盤上文件和內存之間加載/驅逐
的基本單位。
? ?page是個什么概念呢?page大小也是8K,就是上面提到的block,只不過,page仔細的端詳了8KB的內容,分析了信息是如何組織,如
何存放到8KB的block空間之內。注意每條記錄內部的結構不是page關心的事情,他的視角沒有這個細,我們關心的是這條記錄作為一個
整體如何存放到8K的page中去;當然8K的page可能存放多條記錄,如何擺放到8K的page中去;當前page剩余空間還有多少;我有一條需要空
間為size的記錄,page是否有足夠的空間容納之;記錄可能會插入,也可能會刪除,page里面會不會因為刪除動作,頁面內部有很多的洞
,或者頁面碎片化,如何清理碎片,這些都是page要解決的問題。
? ?簡而言之,page,就是管理8K大小的一畝三分地,他要把多條記錄(Tuple)有條不紊地組織在這8K的空間之內。
? ? 一條記錄會插入到8KB的page之中,信息如何組織?自然大多數記錄占用的空間不會超過8KB,以我們前邊提到的friends為例:
? ??
? ? 這個friends的設計不太好,不過我們的重點不在于此,我們關心的是這長度為8192(1個Block或者說1個page)的文件,到底存放
的是啥內容?
? ??
? ?我們看到文件雖然有8K,但是實際上只有最前面的2行32字節,和最后面的64字節中包含信息,因為這個文件對應的就是我們的
friends這張表,而這張表里面有Lee,Bean ,158XXXXX,Nan Jing等信息,當然了這是一條記錄,或者一個Tuple,Tuple內部的組成或
者layout我們不關心,但是這個16385文件作為一定記錄了這些信息。我們用vbindiff查看之:
? ?
? ?
? ?我們看到了,我們的信息Bean,Nan Jing之類的,不管是如何組織的,的確存儲在表friend對應文件16385之中。這條記錄如何放入
8K的空間之內,頭部的一些字符有是干啥的,記錄的信息為何放到了現在的這個位置,這就是page要管的事情,我們下面詳查之。
??
? ? 上圖就是page的結構圖,8K的空間包括一個頭部Page Header,若干個Item,每個Item指向一條記錄(Tuple),有些Page在初始化
的時候,就page的末尾,預留出空間作為Special用,作什么用,我暫時不知,不過沒關系,不影響我們理解Page。當然了,有些Page不
需要Special空間,就沒有預留。
? ?好我們可以分析源碼了。
? ?INIT-page的初始化
? ? 首當其沖的是PageInit函數。我們申請了一個新的干凈的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 */
}
? ? 對于pageSize,默認情況下就是8K即BLCKSZ,而specialSize,某些情況下為0,某些情況下不為0,這都沒關系。
? ? Init做的事情是
? ? 1 給special預留空間 ? ??
? ? specialSize = MAXALIGN(specialSize); ? ? ? ? //4 字節對齊
? ? p->pd_special = pageSize - specialSize;
? ? page header的成員變量pd_special相當于畫了一條線,從pd_special這個位置到page的結尾,都是special的地盤,普通插入Tuple
,都不許進入這個私有地盤。而且這個pd_special一旦初始化之后,這個值就不會動了。
? ? 2 設置pd_lower和pg_upper
? ?當初始化的時候,pd_lower設置為SizeOfPageHeaderData,pd_upper設置為和pd_special一樣。但是注意,這個lower和upper不是固
定的,隨著Tuple的不斷插入,lower變大,而upper不斷變小。當我們每插入一條Tuple,需要在當前的lower位置再分配一個Item,記錄
Tuple的長度,Tuple的起始位置offset,還有flag信息。這個Page Header中的pd_lower就是記錄分配下一個Item的起始位置。所以如果
不斷插入,lower不斷增加,每增加一條Tuple,就要分配一個Item(4個字節)。同樣道理,Tuple的存放位置,根據upper提供的信息,
可以找到將Tuple分配到何處比較合。分配之后,pd_upper就會減少,減少Tuple的長度(對齊也考慮進去)。
? ? 3 設置 page的size 和version
#define PageSetPageSizeAndVersion(page, size, version)?
(?
? ? AssertMacro(((size) & 0xFF00) == (size)),?
? ? AssertMacro(((version) & 0x00FF) == (version)),?
? ? ((PageHeader) (page))->pd_pagesize_version = (size) | (version)?
)
? ?這個不多說,基本就是將版本號和page的長度記錄在16bit的結構里面。
? ?下面我們比較剛初始化和插入一條記錄之后的情形:
? ?
? ? ?
? ?一個記錄對應兩個部分,就頭部附近Item空間和真正記錄信息的Tuple。Item記錄的是Tuple在Page的offset,size等信息。
? ?AddItem-page增加一個記錄
? ?Page是用來存放Tuple的,增加一個Tuple刪除一個Tuple都是Page份內的事情,我們首先看下Page如何增加一個Tuple:
? ?function PageAddItem是完成這件事情。因為這個接口是很通用的接口,要滿足上層的各種需求,所以稍顯復雜,不過整體還好。
OffsetNumber
PageAddItem(Page page,
? ? ? ? ? ? Item item,
? ? ? ? ? ? Size size,
? ? ? ? ? ? OffsetNumber offsetNumber,
? ? ? ? ? ? bool overwrite,
? ? ? ? ? ? bool is_heap)
? ?item是我的當前記錄的指針,size記錄記錄的長度,(item,item+size)這部分地址是Tuple的信息。 Page表示從這個page中查找
空間保存當前的Tuple。這我們很好理解,因為這是基本的要求:在當前頁隨便找個空間保存我的item。咱的要求比較簡單,可是有些客
戶要求可就不簡單了,比如客戶要求,就要將我的記錄拜放到page的第三個item,這就是比較坑爹的客戶了。就像去飯館吃飯,我到了
飯館,喊了一嗓子,小二,給哥隨便找個8人桌,小二很happy,因為我的要求低。也有客官直接喊了一嗓子,小二,我要去三樓最好的
那個雅間,如果有客人,讓他給我騰地方,我們有8個人。得,小二就傻了眼,但是還得辦不是。PageAddItem也是一樣,offsetNumber
這個如參表示,大爺我就要將記錄存放在這個位置。overwrite則這個參數就更拽了,如果有記錄放在我要的位置,讓原來那條記錄給大
爺滾蛋,。如果overwrite =0 表示,大爺要的位置如果有人,原來位置的記錄換個地方,給大爺我騰地方。OK,這幾個參數是干啥的,
我基本交代清楚了
? ?
? ?因為Page Header的長度是固定,而緊跟其后的Item的長度也是固定的,而每增加一個Item,pd_lower就增加一個Item的長度,這樣,
根據pd_lower就可以算出當前的頁面已經有幾個Tuple了。
? ? #define PageGetMaxOffsetNumber(page)?
? ? (((PageHeader) (page))->pd_lower <= SizeOfPageHeaderData ? 0 :?
? ? ?((((PageHeader) (page))->pd_lower - SizeOfPageHeaderData)?
? ? ?/ sizeof(ItemIdData)))
limit = OffsetNumberNext(PageGetMaxOffsetNumber(page));
? ? 這個limit記錄的是當前記錄數+1 ,用這個來判段新來的AddItem請求有沒有指定既有的位置
if (OffsetNumberIsValid(offsetNumber)) //大爺型請求,值定了記錄的存儲位置
{
? ? ? ? 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 ? ? ? ? ? ?//新增加的客戶要求這個位置,需要將原來位于這個位置的記錄遷移到其他位置。
? ? ? ? {
? ? ? ? ? ? if (offsetNumber < limit)
? ? ? ? ? ? ? ? needshuffle = true; ? ? ? ?/* need to move existing linp's */
? ? ? ? }
}
else ? //普通客戶
{
? ??
}
? ? 上面分析了文藝青年式的AddItem,下面我們分析下普通青年的AddItem,普通青年要求低,隨便找個地兒存放當年記錄:
? ? 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 = 當前記錄數 + 1,這個太順理成章了,那個PageHasFreeLinePointers是搞什么飛機?我
們看下:
#define PageHasFreeLinePointers(page)?
? ? (((PageHeader) (page))->pd_flags & PD_HAS_FREE_LINES)
? ? 這個標志是啥意思?看名字的意思是 表征是否有free line。我們會把一些Item狀態置為LP_UNUSED,這時候,Item和它原來的
Tuple就沒有映射關系。這樣原來對應Tuple,就成了垃圾。后面會有會PageRepairFragmentation清理這些空間,但是仍然不會刪除這個
LP_UNUSED狀態的Item,只是打上一個標志,表示存在無主的Item,可以被復用。
? ? if (offsetNumber == limit || needshuffle)
? ? ? ? lower = phdr->pd_lower + sizeof(ItemIdData); //新增一個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;
? ? 因為新增個Tuple,需要alignedSize存儲這記錄的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指示第幾個記錄,offnum是從1開始計數的,查找對應item 是offnum-1.
? ?我們找到Item,就可以找到Tuple對應的offset和size: ? ?
? ? tup = PageGetItemId(page, offnum);
? ? Assert(ItemIdHasStorage(tup));
? ? size = ItemIdGetLength(tup);
? ? offset = ItemIdGetOffset(tup);
??
? ?刪除第二個記錄之后,我們得到的Page布局如下:
??
? ?我們可以看到,至少發生兩次memmove
? ?1 刪除記錄的Item后面的item都要往遷移,防止出現一個空洞
? ? 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,也要移動,否則,會出現Tuple-2對應的空洞。?
? ? addr = (char *) page + phdr->pd_upper;
? ? if (offset > phdr->pd_upper)
? ? ? ? memmove(addr + size, addr, (int) (offset - phdr->pd_upper));
? ? 除了移動內存,item對應的指針也要發生相應的改變:比如洋紅色的兩個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,發生了移位,所以對應Item的lp_off要修改。
? ? ? ? ? ? ? ? ii->lp_off += size;
? ? ? ? }
? ? }
? ? Page還剩多少剩余空間這是很重要的,這決定我們能否插入一條記錄到當前Page。 原理就非常簡單了,pd_upper - pd_lower ,就
是剩余空間,但是,還需要存放Item,所以還需要減Item占據的空間,剩下的才能存放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;
}
? ?文章寫的已經很長了,PageIndexMultiDelete 和 PageRepairFragmentation核心邏輯是類似的,我就不寫這兩個。原來也不難,把
這些碎片化的Tuple排個序,重新連接成一個連續的空間。
========
總結
以上是生活随笔為你收集整理的PostgreSQL源码分析的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 图解VC++ opengl环境配置和几个
- 下一篇: linux cmake编译源码,linu