接前文postgresql編譯安裝與調試(一),繼續說說postgresql的編譯安裝與調試。
上一篇已經詳細說明了如何在Linux系統上編譯安裝postgresql,這次我們在此基礎上簡單講講如何在linux系統上調試和追蹤代碼。
我記得之前看過一篇關於posgresql的文章,postgresql最早只有20萬左右的代碼量,而如今已經過100萬行了,如此巨大的代碼量,在沒有綱領的前提下簡直是盲人摸象。
為方便調試工作,在進入具體的調試之前,我們先來好好了解下postgresql的代碼體系結構。
- 1.postgresql的目錄結構
首先進入postgresql的第一級目錄
config文件夾主要放的是一些配置文件;contrib文件夾里放的是一些第三方的插件、擴展程序等,常用的有pg_standby、postgres_fdw這些;doc文件夾不用說放的是一些幫助文檔和manuals;最主要的是src目錄,這里放置的是postgresql的源代碼,也是我們調試和跟蹤的主要文件目錄了。然后INSTALL文檔里較為詳細的寫了如何編譯安裝postgresql;configure和Makefile這些是程序編譯時要用的文件了。
進去src目錄,
首先那幾個Makefile文件什么的就不用多介紹了,主要看看這幾個文件夾。
bin/ 放置了postgresql的unix命令,比如psql、initdb這些的源代碼;
backend/ postgresql后端程序的源代碼;
include/ 頭文件;
interfaces/ 前端相關的庫的代碼(包括pgsql的C語言庫libpq);
makefiles/ 平台相關的make的設置文件;
pl/ 存儲過程語言的代碼;
port/ 平台移植相關的代碼;
template/ 平台相關的設置文件;
test/ postgresql自帶的各種測試腳本;
timezone/ 時區相關的代碼文件;
tools/ 各種開發工具和文檔;
tutorial/ 各種相關教程。
可以看出比較核心的是backend、bin、interfaces這三個目錄,其中backend對應后端(服務器端),剩下兩個對應前端(客戶端)。
對於我們的調試工作,大部分關注點集中在后端,即backend目錄,在該目錄下細分了好多目錄:
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 管理)
- 2.利用gdb調試postgresql
首先我們要有gdb這個工具,如果沒有,可以用yum命令自動的去安裝它。
調試postgresql,我們先以最簡單的SQL文作為例子演示如何調試跟蹤代碼。例如:
select 1;
首先,我們利用postgres用戶進入postgresql:
既然要用gdb跟蹤調試程序,我們首先要知道postgresql后端進程的pid,然后才能attach上進行調試(對gdb命令不熟悉的可以先自行百度下)。
要獲取postgresql的pid,我們有兩個辦法。
方法1.使用ps命令查看
[root@promote ~]# ps -ef | grep postgres
我們可以看到
那個[local] idle 提示的那個就是我們要的,可知進程pid為16581;
方法2.直接在進入postgresql后運行下面的查詢語句:
select pg_backend_pid();
也可以得到進程的pid,方便快捷。
得到進程的pid后,我們就可以進入gdb調試了。
另開一個窗口,我們輸入如下命令:
[root@promote ~]# gdb postgres 16581
進入了gdb命令行界面。
在這個狀態下,可以接受gdb命令,這里,我們使用b命令在ExecResult處打上斷點:
這個時候我們再回到postgresql的窗口,執行SQL文:
我們可以看到因為postgres進程已經暫停,SQL會卡在那里動不了,這也是我們的目的,不然怎么一步一步(似魔鬼的步伐)的調試呢?
我們再回到gdb這邊,運行c命令,程序就會繼續執行下去,然后再斷點處(ExecResult)停止。
作為一個好奇寶寶,我們當然會很好奇執行路徑上走過了哪些文件調用了哪些函數(廢話,不然干嘛要調試)?
好的,我們執行gdb的bt命令:
這一大串就是我們夢寐以求的函數調用的堆棧了。這樣從程序開始到ExecResult為止的函數調用都有了。既然說是“堆棧”,我們自然是要反着看的,比如,我們可以看到最早調用的是main函數,它在(at)main.c文件里,在main函數的第228行,調用了PostmasterMain函數,依次類推即可知道函數的調用路徑。
知道了函數的調用路徑,我們可以一步一步地看看這條語句是怎么走的了。以postgresql9.5.4為例(限於篇幅和時間限制,只粗略的講講):
#13 main.c 內:
line99: 函數MemoryContextInit()啟動必須的子系統error和memory管理系統;
line110:函數set_pglocale_pgservice()獲取並設置環境變量;
line146~148: 函數init_locale初始化環境變量;
line219~228:根據輸入參數確定程序走向,這里進入了PostmasterMain(),跳轉至postmaster.c文件。
#12#11#10#9 postmaster.c 內:
該文件中定義了后端的常駐進程"postmaster"所使用的主要函數接口和數據結構定義。postmaster接受前端的請求,建立新的backend進程。
line561~623:讀取上下文信息和配置文件,完成初始化;
line630~812:讀取postmaster的參數;
line930~1000:建立socket通信;
line1100~1159:建立shared memory和semaphores以及堆棧和pipe,初始化子系統(stats collection、autovacuum);
line1296:進入ServerLoop()函數,跳轉至line1604;
line1604:ServerLoop()函數入口。該函數循環監聽端口上的連接請求;
line1673~1699:判斷是否有"合法"的連接請求,fork一個子進程去處理它,進入BackendStartup()函數,跳轉至line3857;
line3857:BackendStartup()函數入口。該函數負責開啟一個新的backend進程;
line3858~3914:做一些初始化准備(數據結構,開啟和關閉一些必要的進程等等);
line3917:進入BackendRun()函數,跳轉至line4179;
line4179:BackendRun()函數入口,該函數運行backend進程,主要干兩件事:1.建立參數列表並初始化2.調用PostgresMain()函數;
line4243:調用PostgresMain()函數,進入postgres.c文件.
#8#7 postgres.c 文件內:
該文件定義了postgres后端的主要模塊,相當於后端的main,並且負責后端進程的調度。
line3572:PostgresMain()函數入口。根據輸入的dbname,username和輸入參數建立一個會話;
line3573~3801:初始化工作。開設初始化環境和默認參數,設置信號處理函數和其他參數,建立內存上下文,設置share buffer等等等等;
line3825:進入POSTGRES的主處理循環,這個if語句主要用於判斷輸入處理是否有異常等;
line3933:進入處理循環中。該循環監聽新的查詢請求並判斷請求的類別;
line4045:判斷查詢請求為simple query,調用exec_simple_query()函數,跳轉至line884;
line884:exec_simple_query()函數入口。該函數做一些初始化工作,建立一個transaction command,做簡單的語法規則判斷,分析重寫,並為該查詢建立查詢計划,並返回查詢結果;
line1104:進入函數PortalRun(),進入pquery.c文件.
#6#5 pquery.c 文件內:
該文件定義了postgres后端查詢語句的代碼。
line706:PortalRun()函數入口。該函數負責運行一個或一組查詢;
line786:進入PortalRunSelect()函數,跳轉至line888;
line888:PortalRunSelect()函數入口。該函數只能執行簡單的SELECT查詢操作;
line942:進入ExecutorRun()函數,進入execMain.c文件.
#4#3#2 execMain.c 文件內:
該文件給出了執行的四個接口函數,分別是ExecutorStart() ExecutorRun() ExecutorFinish() ExecutorEnd()。
line279:ExecutorRun()函數入口。該函數時執行模塊的主要部分,它接受一個查詢描述符並真正的執行一個查詢語句;
line285:進入standard_ExecutorRun()函數。跳轉至line289;
line289:standard_ExecutorRun()函數入口。它執行"標准"的查詢;
line337:進入ExecutePlan()函數,跳轉至line1517;
line1517:ExecutePlan()函數入口。還記得前面exec_simple_query()說的查詢計划么?這里用上了,執行該查詢計划。
line1541:進入查詢計划執行的主循環;
line1549:進入ExecProcNode()函數,進入execProcnode.c文件.
#1 execProcnode.c 文件內:
該文件內提供了執行查詢計划的調度函數,功能分別是:
ExecInitNode():初始化查詢計划的節點以及其子查詢計划;
ExecProcNode():通過執行查詢計划獲得元組;
ExecEndNode():關閉一個查詢節點和它的子查詢計划。
line367:ExecProcNode()函數入口;
line385:進入ExecResult()函數,跳轉至文件nodeResult.c.
#0 nodeResult.c 文件內:
該文件主要為每個查詢計划的節點提供支持。
line67:ExecResult()函數入口,該函數返回查詢計划獲得的元組。
這一段從#13到#0的函數調用簡單分析就到這里,完全是自己的理解,如果有什么不對的地方,歡迎大家批評指正,共同進步。接下來還有函數調用的返回,這里就不細說了,留給自己和大家一起好好琢磨琢磨吧~
不得不說,postgresql的源碼寫的很優雅,注釋也很到位,看起來很少有雲山霧罩的感覺,真乃吾輩楷模。說起閱讀源碼,想推薦一本書,叫《代碼閱讀方法與實踐》,書不太好找,我還是在托師弟在學校的圖書館才找到的。
另外,今天這種代碼閱讀方法仍然有些原始和低效,決定再看看使用Emacs的Tag或者Eclipse來調試一些更難一些的例子,這個例子畢竟比較簡單。這些就留給postgresql編譯安裝與調試(三)來完成吧,感覺這個系列要出好多的樣子呢,哈哈~