一直很羨慕那些能讀 Redis 原始碼的童鞋,也一直想自己解讀一遍,但迫於 C 大魔王的壓力,解讀日期遙遙無期。
相信很多小夥伴應該也都對或曾對原始碼感興趣,但一來覺得自己不會 C 語言,二來也不知從何入手,結果就和博主一樣,一拖再拖。
但正所謂,種一棵樹的最好時間是十年前,其次就是現在。如果你真的想了解 Redis 原始碼,又有緣看到了這系列博文,何不跟著博主一起解讀 Redis 原始碼,做個同行人呢?接下來,就讓我們一起走入 Redis 的原始碼世界吧。
決定要讀了,下一步就是如何讀。從 github 上克隆下來原始碼,一看 src 目錄,望天,104 個檔案,我該從哪個檔案開始呢?一個個檔案看?不行不行,這樣對我毫無誘惑力,沒有誘惑力,怎麼能戰勝遊戲、小說對我的吸引呢?苦苦思考,不得其解。然後突然想起來 HTTP 協議的那個經典面試題:從瀏覽器輸入網址,到頁面展示,這個過程發生了什麼?
把這個面試題換成 Redis:輸入開啟 Redis 服務的命令,回車,到成功啟動 Redis 服務,這個過程發生了什麼?
很好,這個問題成功吸引到我了。就讓我們從原始碼中找出這個問題的答案吧。後續的所有文章我們都嘗試通過提出問題,解答問題的步驟,來深入瞭解 Redis。
要了解 Redis 命令的執行過程,首先要安裝 Redis 服務,搭建 debug 環境。如果我們能一行行的看到命令在程式碼中的執行過程,解讀原始碼也就沒任何阻礙了。
後續所有文章均基於 redis3.2.13 版本。
1 搭建 debug 環境
1、下載編譯檔案
在 linux 上,下載原始碼檔案,編譯,使用 gdb(cgdb) 進行 debug。
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 等
2、開啟 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 函式為其分配記憶體,並在有需要時為這些資料結構設定或關聯初始化值。
之所以在載入使用者配置之後才初始化資料結構,就是因為伺服器要先載入使用者的配置選項,才能根據選項正確的對資料結構進行初始化。避免再根據使用者配置修改資料結構相關屬性。
所以,我們可以看出,伺服器對狀態的初始化分為兩步進行:
initServerConfig 函式是初始化一般屬性。
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
總結
搭建環境三步走:下載、編譯、gdb。
服務啟動包括:初始化基礎配置、資料結構、對外提供服務的準備工作、還原資料庫、執行事件迴圈等。
gdb 基礎命令:r c b n p。