跟着大彬讀源碼 - Redis 1 - 啓動服務,程序都幹了什麼?

一直很羨慕那些能讀 Redis 源碼的童鞋,也一直想本身解讀一遍,但迫於 C 大魔王的壓力,解讀日期遙遙無期。linux

相信不少小夥伴應該也都對或曾對源碼感興趣,但一來以爲本身不會 C 語言,二來也不知從何入手,結果就和博主同樣,一拖再拖。git

但正所謂,種一棵樹的最好時間是十年前,其次就是如今。若是你真的想了解 Redis 源碼,又有緣看到了這系列博文,何不跟着博主一塊兒解讀 Redis 源碼,作個同行人呢?接下來,就讓咱們一塊兒走入 Redis 的源碼世界吧。github

決定要讀了,下一步就是如何讀。從 github 上克隆下來源碼,一看 src 目錄,望天,104 個文件,我該從哪一個文件開始呢?一個個文件看?不行不行,這樣對我毫無誘惑力,沒有誘惑力,怎麼能打敗遊戲、小說對個人吸引呢?苦苦思考,不得其解。而後忽然想起來 HTTP 協議的那個經典面試題:從瀏覽器輸入網址,到頁面展現,這個過程發生了什麼?面試

把這個面試題換成 Redis:輸入開啓 Redis 服務的命令,回車,到成功啓動 Redis 服務,這個過程發生了什麼?redis

很好,這個問題成功吸引到我了。就讓咱們從源碼中找出這個問題的答案吧。後續的全部文章咱們都嘗試經過提出問題,解答問題的步驟,來深刻了解 Redis。數據庫

要了解 Redis 命令的執行過程,首先要安裝 Redis 服務,搭建 debug 環境。若是咱們能一行行的看到命令在代碼中的執行過程,解讀源碼也就沒任何阻礙了。數組

後續全部文章均基於 redis3.2.13 版本。瀏覽器

1 搭建 debug 環境

一、下載編譯文件
在 linux 上,下載源碼文件,編譯,使用 gdb(cgdb) 進行 debug。bash

# bash
wget https://github.com/antirez/redis/archive/3.2.13.tar.gz
tar -zxvf 3.2.13.tar.gz
mv redis-3.2.13 /opt/
cd redis-3.2.13
make                 # 編譯文件,獲得可執行文件 redis-server、redis-cli 等

二、開啓 debug服務器

# bash
gdb src/redis-server # 在 redis 安裝目錄,進入 gdb 調試環境

按咱們平時調試的習慣,找到一個函數設置斷點,而後一步步運行調試。對於 Redis 也同樣,咱們找到 server.c 文件,服務器運行的 main 函數就在此文件中。咱們對 main 函數設置斷點:

# gdb
(gdb) b main
Breakpoint 1 at 0x42ed05: file server.c, line 3962.

頁面會提示咱們在 server.c 文件的 3962 行設置了斷點,也就是咱們指定的 main 函數的位置。

設置好斷點,下一步就是啓動服務:

// 啓動服務
(gdb) r ./redis.conf
Starting program: /opt/redis-3.2.13/src/redis-server ./redis.conf
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".

Breakpoint 1, main (argc=2, argv=0x7fffffffe5a8) at server.c:3962
3962    int main(int argc, char **argv) {

經過頁面輸出信息,咱們會發現程序已經運行到咱們設置的斷點了。可是咱們看不到運行處的代碼,這可不行,看不到源碼的調試,無法接受使用如下命令」召喚「源碼:

(gdb) layout src

出現下圖所示的界面:

圖 1 - gdb 的 src 和 cmd 並存

到了這一步,咱們已經正式開始踏上 Redis 源碼解讀之路了。

2 初始化服務

繼續往下走,使用 n 命令,執行下一步,而後不斷回車、回車、回車,好像每一行都看不懂什麼意思。無論了,繼續走。咦,好像發現個能看懂的 initServerConfig()。沒看錯的話,這個應該是初始化服務器配置的,讓咱們進到這個函數裏確認下:

(gdb) s

回車,走你。而後咱們就看到了下面這個界面:

圖 2 - 進入初始化服務器配置函數

提示咱們進入了 server.c 1464 行的 initServerConfig 函數中。 n 命令,繼續走。咱們會發如今這個函數裏對服務器的各類基礎參數進行初始化。這裏的參數詳見 server.h/redisServer 結構體。

回到 main 函數後,咱們繼續前進,還會發現一個 initServer() 的函數。這個函數是進行驅動事件的註冊,以及綁定回調函數等。

繼續走,直到執行 aeMain(),以下圖:

圖 3 - Redis 服務已開啓

程序執行到 4133 行時,Redis 服務已成功開啓了。此時服務器處於休眠狀態,並使用 aeMain() 進行事件輪詢,等待監聽事件的發生。

上述整個過程,咱們只是跟着程序的運行,大概看了一遍執行流程。下面,咱們來詳細解讀上面敘述的關鍵步驟:初始化基礎配置初始化服務器數據結構

3 初始化詳細解讀

3.1 初始化基礎配置

初始化服務器的第一步就是建立一個 `redisServer 類型的實例變量 server 做爲服務器的狀態,併爲結構中的各個屬性設置默認值。

void initServerConfig(void) {
    int j;
    // 設置服務器運行 ID
    getRandomHexChars(server.runid,CONFIG_RUN_ID_SIZE);

    // 爲運行 ID 加上結尾字符
    server.runid[CONFIG_RUN_ID_SIZE] = '\0';

    // 設置服務器默認運行架構
    server.arch_bits = (sizeof(long) == 8) ? 64 : 32;

    // 設置服務器默認配置文件路徑
    server.configfile = NULL;

    // 設置服務器默認運行頻率
    server.hz = CONFIG_DEFAULT_HZ;

    // 設置服務器默認端口
    server.port = CONFIG_DEFAULT_SERVER_PORT;
    
    // ...
}

對於 initServerConfig 函數來講,它主要完成如下主要工做:

  • 設置服務器的運行 ID。
  • 設置服務器的默認運行頻率。
  • 設置服務器的默認配置文件路徑。
  • 設置服務器的運行架構。
  • 設置服務器的默認端口號

initServerConfig 函數設置的服務器狀態屬性基本上都是一些整數、浮點數或者字符串屬性。除了命令吧以外,initServerConfig 函數沒有建立服務器狀態的其它數據結構。像數據庫、慢查詢日誌、Lua 環境、共享對象等這些數據結構是在以後的步驟中建立的。

當初始化基礎配置參數後,下一步就要開始載入配置選項

3.2 載入配置選項

在啓動服務器時,用戶能夠經過給定配置參數或者知道配置文件來修改服務器的默認配置。就像咱們能夠在啓動服務時指定端口:

# bash
./src/redis-server --port 7379

經過給定配置參數的方式,修改了服務器的運行端口號。

除了給定配置參數的方式,咱們能夠經過指定配置文件的形式啓動服務:

# bash
./src/redis-server ./redis.conf

經過指定配置文件的形式啓動服務時,咱們實際上就是經過配置文件的形式修改了服務器的數據庫配置。

服務器在用 initServerConfig 函數初始完 server 變量後,就會開始載入用戶給定的配置參數和配置文件,並根據用戶設定的配置,對 server 變量相關屬性進行修改。

關於命令行指定配置、配置文件配置、默認配置,這三種配置中:

  • 若是有指定配置,服務器就是有用戶指定的值來更新對應的屬性。
  • 若是沒有指定值,則沿用 initServerConfig 函數設置的默認值。

3.3 初始化服務器數據結構

在執行 initServerConfig 函數初始化配置時,程序只建立了命令表一個數據結構,而服務器除了命令表還包括其餘數據結構,好比:

  • server.clients 鏈表。這個鏈表記錄了全部與服務器相連的客戶端的狀態結構。鏈表的每一個節點都包含了一個 RedisClient 結構實例。
  • server.db 數組。數組中包含了服務器全部的數據庫。
  • server.pubsub_channels 字典。字典中保存頻道訂閱信息。
  • server.pubsub_patterna 鏈表。鏈表中保存模式訂閱信息。
  • server.lua 屬性。用來執行 Lua 腳本。
  • server.slowlog 屬性。用來保存慢日誌。

上述這些數據結構會在 initServer 函數爲其分配內存,並在有須要時爲這些數據結構設置或關聯初始化值。

之因此在載入用戶配置以後才初始化數據結構,就是由於服務器要先載入用戶的配置選項,才能根據選項正確的對數據結構進行初始化。避免再根據用戶配置修改數據結構相關屬性。

因此,咱們能夠看出,服務器對狀態的初始化分爲兩步進行:

  1. initServerConfig 函數是初始化通常屬性。
  2. initServer 初始化數據結構。

除了初始化數據結構以外,initServer 還進行了一些很是重要的設置操做,包括:

  • 爲服務器設置進程信號處理器。
  • 建立共享對象。這些對象包含 Redis 服務器經常使用到的一些只,好比包含 "OK" 回覆的字符串對象,包含 "ERR" 回覆的字符串對象,包含整數 1 到 10000 的字符串對象等等。服務器正是經過重用這些共享對象來避免反覆建立相同的對象,節約內存。
  • 打開服務器的監聽端口,併爲監聽套接字關聯應答事件處理器,等待服務器正式運行時接受客戶端的鏈接。
  • 爲服務器建立時間事件,等待服務器正是運行時執行 serverCron 函數。
  • 若是開啓了 AOF 持久化功能,打開現有的 AOF 文件。若是 AOF 文件不存在,就建立並打開新的 AOF 文件,爲 AOF 寫入作好準備。
  • 初始化服務器的後臺 IO 模塊,爲 IO 操做作好準備。

initServer 函數執行完畢以後,服務器將用 ASCII 字符在日誌中打印出咱們常見到的 Redis 圖標,以及 Redis 的版本號信息等。

圖 4 - 服務器啓動後打印的 Redis 圖標和版本信息等

4 其它操做

4.1 還原數據庫

在完成了對服務器狀態 server 變量的初始化以後,服務器須要載入 RDB 文件或者 AOF 文件(數據持久化保存文件),並根據文件記錄的內容來還原服務器的數據庫狀態。

還原過程當中,服務器會判斷是否啓用了 AOF 持久化功能:

  • 若是啓用了 AOF 持久化功能,服務器將使用 AOF 文件來還原數據庫狀態。
  • 若是沒有啓用 AOF,服務器使用 RDB 文件來還原數據庫狀態。

當服務器完成數據庫狀態還原工做以後,會在日誌中打印出載入文件和還原數據庫狀態所耗費的時長。

8189:M 31 May 13:12:47.971 * DB loaded from disk: 0.000 seconds

4.2 執行事件循環

在初始化的最後一步,服務器將打印出如下日誌:

8189:M 31 May 13:12:47.971 * The server is now ready to accept connections on port 8379

並開始執行服務器的事件循環。

至此,服務器的初始化工做所有完成。

5 gdb 基礎使用

命令 解釋 示例
gdb file 加載被調試的可執行程序文件 gdb src/redis-server
r Run 的縮寫,運行被調試的程序。 r ./redis.conf
c Continue 的縮寫。繼續執行被調試程序,直至下一個斷點或程序結束 c
b Breakpoint 縮寫。設置斷點。可使用 行號、函數名稱、執行地址等方式指定斷點位置 b main
s/n s 至關於「單步跟蹤並進入」,也就是說進入到執行的函數內部。n 至關於「單步跟蹤」,不進入到執行函數內部 s/n
p 變量名稱 Print 縮寫。顯示指定變量的值。 p server

總結

  1. 搭建環境三步走:下載、編譯、gdb。
  2. 服務啓動包括:初始化基礎配置、數據結構、對外提供服務的準備工做、還原數據庫、執行事件循環等。
  3. gdb 基礎命令:r c b n p。
相關文章
相關標籤/搜索