以前個人一些文章都是在說Postgres的一些查詢相關的代碼。可是對於Postgres服務端是如何啓動,後臺進程是如何加載,服務端在哪裏以及如何監聽客戶端的鏈接都沒有一個清晰的邏輯。那麼今天我來講說Postgres中的postmaster模塊的代碼,試着解答這些問題。數據庫
在正式討論以前,我先說一下,代碼主要涉及的是postgres源碼的src/backend目錄下的main,postmaster以及tcop模塊。bootstrap
關於postmaster這個命令,熟悉postgres的必定不會陌生。在Linux上它是postgres命令的一個軟鏈接,而在Windows上,它直接就是postgres命令的別名。所以,話題就轉換爲:postgres命令的處理細節。而postgres命令,從官方手冊上咱們能夠知道,它是啓動後端服務器的命令(固然前提是你要用initdb命令先生成一個database cluster)。不管是是直接使用postgres命令啓動仍是用pg_ctl命令,其本質都是調用postgres命令來啓動數據庫的。後端
下面進入代碼。服務器
命令的入口在src/backend/main/main.c。這個main()函數所作的工做很少:數據結構
作一下基本的初始化(主要是調用MemoryContextInit函數啓動error和memory management子系統,還有其餘的locale設置等); 根據命令行的第一個參數分派不一樣的函數去處理。
咱們用postgres命令來啓動一個數據庫的時候,雖然參數不少。最簡單的是對於"–help, --version"這兩個參數的處理。對於這兩個參數咱們只須要簡單的返回一下幫助信息便可返回。對於剩下的參數,咱們不急着處理,由於咱們首先要根據第一個參數肯定的是咱們但願database工做在何種模式?dom
要回答這個問題的話,咱們看一下src/backend/main/main.c,在main函數裏,有如下代碼:socket
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 */ --->後臺主進程
咱們來一一分析。ide
首先是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()函數,進入正常啓動後端的過程。這個也是咱們今天討論的重點了。
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的其餘細節。