之前我的一些文章都是在說Postgres的一些查詢相關的代碼。但是對於Postgres服務端是如何啟動,后台進程是如何加載,服務端在哪里以及如何監聽客戶端的連接都沒有一個清晰的邏輯。那么今天我來說說Postgres中的postmaster模塊的代碼,試着解答這些問題。
在正式討論之前,我先說一下,代碼主要涉及的是postgres源碼的src/backend目錄下的main,postmaster以及tcop模塊。
關於postmaster這個命令,熟悉postgres的一定不會陌生。在Linux上它是postgres命令的一個軟連接,而在Windows上,它直接就是postgres命令的別名。因此,話題就轉換為:postgres命令的處理細節。而postgres命令,從官方手冊上我們可以知道,它是啟動后端服務器的命令(當然前提是你要用initdb命令先生成一個database cluster)。無論是是直接使用postgres命令啟動還是用pg_ctl命令,其本質都是調用postgres命令來啟動數據庫的。
下面進入代碼。
1.命令的入口處理
命令的入口在src/backend/main/main.c。這個main()函數所做的工作不多:
做一下基本的初始化(主要是調用MemoryContextInit函數啟動error和memory management子系統,還有其他的locale設置等);
根據命令行的第一個參數分派不同的函數去處理。
我們用postgres命令來啟動一個數據庫的時候,雖然參數很多。最簡單的是對於"–help, --version"這兩個參數的處理。對於這兩個參數我們只需要簡單的返回一下幫助信息即可返回。對於剩下的參數,我們不急着處理,因為我們首先要根據第一個參數確定的是我們希望database工作在何種模式?
要回答這個問題的話,我們看一下src/backend/main/main.c,在main函數里,有以下代碼:
if (argc > 1 && strcmp(argv[1], "--boot") == 0)
AuxiliaryProcessMain(argc, argv); /* does not return */ --->后端子進程,bootstrap
else if (argc > 1 && strcmp(argv[1], "--describe-config") == 0)
GucInfoMain(); /* does not return */
else if (argc > 1 && strcmp(argv[1], "--single") == 0)
PostgresMain(argc, argv,
NULL, /* no dbname */
strdup(get_user_name_or_exit(progname))); /* does not return */ --->backend進程
else
PostmasterMain(argc, argv); /* does not return */ --->后台主進程
我們來一一分析。
首先是bootstrap("--boot"參數指定)模式。
這個模式大家可能比較陌生,在postgres的官方手冊上也沒有說明這個參數,這個參數目前只在postgres程序內部調用。這個模式下,數據庫工作在啟動模式下,此時數據庫還不能接受外部的數據庫連接,大概就是說此時數據庫只是內核啟動了,還沒有對外部提供訪問接口。那么這個模式的用處何在呢?
那就讓我們把目光放遠一點,看看initdb這個命令吧(代碼在src/bin/initdb下)。initdb命令我們很熟悉,這個命令用來初始化一個數據庫集群。而在這個過程中,我們知道會建立template1,template0和postgres這個三個初始的數據庫。那么問題是,我們這三個數據庫是怎么建立的,數據庫的表,視圖索引之類的是怎么創建的?
答案就是在調用工作在"bootstrap"模式下的postgres命令,啟動一個"standalone bootstrap process"。也就是說,以"內核"模式啟動postgres服務器,從而進行這一系列的數據庫操作。證據何在?我們看看initdb.c:
代碼調用棧如下:
main()
->initialize_data_directory()
->bootstrap_template1()
在bootstrap_template1中,有以下代碼:
snprintf(cmd, sizeof(cmd),
"\"%s\" --boot -x1 %s %s %s",
backend_exec,
data_checksums ? "-k" : "",
boot_options, talkargs);
可以說是非常清晰了。
接下來是"--describe-config"參數。
這個參數在官方手冊是有定義說明的,我直接抄下來吧:
這個選項會用制表符分隔的COPY格式導出服務器的內部配置變量、描述以及默認值。設計它的目的是用於管理工具。
所以還是打印信息,只不過是打印數據庫的內部的配置參數信息的,其實還是蠻實用的。
single("--single"參數指定)模式
這個很簡單,選擇數據庫進入單用戶模式。這個模式和正常的啟動的差別是你是以單獨的用戶方式進入數據庫的,不會做任何后台處理,例如自動檢查點。一般是用來debug用。這里也不細說了。
不指定以上參數的話,我們進入Postmater進程。
到這里,我們終於進入正題,進入PostmasterMain()函數,進入正常啟動后端的過程。這個也是我們今天討論的重點了。
2.postmater的處理
postmaster部分的處理主函數是PostmasterMain()。這個函數的處理內容較多,我們按照步驟來解釋:
1.memorycontext的建立與切換(切換到PostmasterContext)
我們知道在main模塊里我們建立了頂層上下文TopMemoryContext和其子上下文ErrorContext。此處調用AllocSetContextCreate()函數建立內存上下文PostmasterContext,並調用函數MemoryContextSwitchTo()將當前上下文切換到PostmasterContext。這樣如果在Postmaster模塊如果出現內存相關的問題,不會影響到其他模塊(這也是內存上下文模塊引入的原因吧)。
2.信號處理函數(singal handler)的設置
作為一個后端主進程,其擁有很多相關的子進程。我們也知道信號(singal)是進程間通信的一種比較方便的方式。這里Postmaster也利用了這一點,注冊了很多信號處理函數來處理信號。有關這部分的信號以及相關的handler我列在下面了:
3.處理GUC和一些命令行參數
這部分就比較常規了。注意這里調用InitializeGUCOptions()函數初始化一些GUC,我們還並不能從config文件中讀取,因為我們還沒有處理命令行參數(命令行參數可以指定一些GUC參數以及config文件)。然后我們用getopt讀取命令行參數,這樣以后,我們就可以讀取config文件進行GUC的設置和驗證參數的合法性了。
4.數據庫集群(database cluster)的鎖定(lock)
調用CreateDataDirLockFile()函數在數據庫集群所在目錄創建數據庫集群的lock文件postmaster.pid。這樣就能保證我們不會對同一個數據庫集群"啟動兩次"。雖然我們也會創建socket lock文件,但是我們還是覺得數據庫集群所在的目錄更加可信和保險一點。
5.共享庫的預加載(process_shared_preload_libraries())
我們喜歡用Postgres的一大原因就是Postgres的豐富的插件,其中很多就是通過共享庫來實現的。這里就是調用process_shared_preload_libraries()函數來導入你在shared_preload_libraries參數中指定的共享庫的。
6.socket的初始化
這里初始化TCP/IP socket和UNIX socket。初始化UNIX socket會在/tmp下創建socket文件。默認情況下,TCP/IP socket是禁用的,我們可以通過修改配置文件來開啟。
7.共享內存和信號量的初始化(reset_shared(PostPortNumber))
這里調用函數reset_shared(PostPortNumber)來處理共享內存和信號量。詳細的說,它調用各模塊的共享內存的使用量估計函數,計算總共所需的共享內存的量,並申請。詳細的我們可以看CreateSharedMemoryAndSemaphores()函數。
8.初始化(並未啟動)數據庫相關后台進程
調用SysLogger_Start()函數啟動syslogger后台子進程;
分別調用pgstat_init()和autovac_init()函數初始化狀態收集子進程(stats collection process)和自動清理子進程(autovacuum process)。
9.讀取客戶端認證的配置文件()
調用load_hba()函數和load_ident()函數讀取客戶端認證文件pg_hba.conf和ident.conf。
10.啟動數據庫(StartupDataBase()函數)
做好了上面這些配置和設置,這里終於可以進行數據庫的啟動操作了。這里我們調用StartupDataBase()函數(其實就是一個宏)來啟動數據庫集群,這里主要發揮作用的是StartupProcessMain(void)函數,這個函數相當於啟動數據庫的Main函數。詳細的調用棧如下,有興趣的讀者可以看看:
PostmasterMain()
->StartupDataBase()
->StartChildProcess()
->AuxiliaryProcessMain()
->StartupProcessMain(void)
->StartupXLOG()
11.服務端主循環(ServerLoop())
既然數據庫終於啟動起來了,我們終於可以接受客戶端發起的連接請求了,這里的ServerLoop()函數就是一個死循環。循環讀取客戶端的請求並進行相關處理。
這里有一點說明,我提個問題,是不是進入ServerLoop()之后,我們就真的可以馬上接受客戶端的連接了呢?或者換句話說,我們到底到什么時候才能接受客戶端連接呢?標志是什么呢?
我們看PostmasterMain()函數里面關於上面的10和11的代碼如下:
StartupPID = StartupDataBase();
Assert(StartupPID != 0);
StartupStatus = STARTUP_RUNNING;
pmState = PM_STARTUP;
/* Some workers may be scheduled to start now */
maybe_start_bgworker();
status = ServerLoop();
關於上面的代碼,我們發現,正是由StartupDataBase()函數啟動了數據庫,然后在ServerLoop()函數里面接受連接。
但是,我們看到在進入ServerLoop()函數之前,pmState的值還是PM_STARTUP,而只有在PM_RUN狀態,數據庫才是真正的啟動起來了。
我們在看下函數reaper(),這個函數是postmaster函數的一個信號處理函數,當它捕獲到Startup進程正常死亡(也就是說,數據庫正常啟動完畢了)后,會設置pmState為PM_RUN。
因此,我們得到結論:在進入ServerLoop()函數后,只有在reaper函數捕獲到Startup的正常死亡並設置pmState為PM_RUN之后,數據庫才能真正的意義上接受連接。
接下來,我們再看ServerLoop()里面到底做了什么。
PostmasterMain()
|->ServerLoop()
|->initMasks()
|->for(;;)
|->select() <--監聽端口
|->ConnCreate() <--創建connection相關的數據結構
|->BackendStartup() <--建立后端進程backend process
|->PostmasterRandom()
|->fork_process()
|->InitPostmasterChild()
|->ClosePostmasterPorts()
|->BackendInitialize()
|->ProcessStartupPacket()
|->BackendRun()
|->PostgresMain()
|->ConnFree() <--釋放connection相關的數據結構
我們再看看關於ServerLoop()中的關於socket和信號處理:
好的,今天就到這里,剩下的會繼續討論Postmaster的其他細節。