Redis 源碼簡潔剖析 07 - main 函數啓動

前言

main 函數是 Redis 整個運行程序的入口。源碼主要在 server.c 文件中。

前面 6 篇文章分析了 Redis 的基礎數據結構。

問題

  • Redis server 啓動後具體會做哪些初始化操作?
  • Redis server 初始化時有哪些關鍵配置項?
  • Redis server 如何開始處理客戶端請求?

階段 1:基本初始化

基本的初始化工作,包括設置 server 運行的時區等。

//設置時區
setlocale(LC_COLLATE,"");
tzset();
...
//設置隨機種子
char hashseed[16];
getRandomHexChars(hashseed,sizeof(hashseed));
dictSetHashFunctionSeed((uint8_t*)hashseed);

階段 2:檢查哨兵模式,執行 RDB 或 AOF 檢測

Redis Server 可能以哨兵模式運行。哨兵模式需要額外的參數配置及初始化。

// 判斷 server 是否爲「哨兵模式」
if (server.sentinel_mode) {
    // 初始化哨兵配置
    initSentinelConfig();
    // 初始化哨兵模式
    initSentinel();
}

此外還會檢查是否要執行 RDB 檢測或 AOF 檢查,這對應了實際運行的程序是 redis-check-rdb 或 redis-check-aof。

// 運行的是 redis-check-rdb
if (strstr(argv[0],"redis-check-rdb") != NULL)
    // 檢測 RDB 文件
    redis_check_rdb_main(argc,argv,NULL);
    // 運行的是 redis-check-aof
else if (strstr(argv[0],"redis-check-aof") != NULL)
    // 檢測 AOF 文件
    redis_check_aof_main(argc,argv);

階段 3:運行參數解析

main 函數會對命令行傳入的參數進行解析,並且調用 loadServerConfig 函數,對命令行參數和配置文件中的參數進行合併處理,然後爲 Redis 各功能模塊的關鍵參數設置合適的取值。

int main(int argc, char **argv) {
    …
    //保存命令行參數
    for (j = 0; j < argc; j++) server.exec_argv[j] = zstrdup(argv[j]);
    …
    if (argc >= 2) {
    …
        //對每個運行時參數進行解析
        while(j != argc) {
            …
        }
    …
    loadServerConfig(configfile,options);
}

loadServerConfig 函數是在 config.c 文件中實現的,該函數是以 Redis 配置文件和命令行參數的解析字符串爲參數,將配置文件中的所有配置項讀取出來,形成字符串。

階段 4:初始化 server

調用 initServer 函數,對 server 運行時的各種資源進行初始化工作。這主要包括:

  • server 資源管理所需的數據結構初始化
  • 鍵值對數據庫初始化
  • server 網絡框架初始化

接着會再次判斷是否爲「哨兵模式」:

  • 是哨兵模式,調用 sentinelIsRunning 函數,設置啓動哨兵模式
  • 不是哨兵模式,調用 loadDataFromDisk 函數,從磁盤加載 AOF 或 RDB 文件,恢復之前的數據
// 初始化 server
initServer();
……

if (!server.sentinel_mode) {
    ……
    InitServerLast();
    // 從磁盤加載數據
    loadDataFromDisk();
    ……
} else {
    ……
    sentinelIsRunning();
    ……
}

資源管理

和 server 連接的客戶端、從庫等,Redis 用作緩存時的替換候選集,以及 server 運行時的狀態信息,這些資源的管理信息都會在 initServer 函數中進行初始化。

初始化數據庫

因爲一個 Redis 實例可以同時運行多個數據庫,所以 initServer 函數會使用一個循環,依次爲每個數據庫創建相應的數據結構。

for (j = 0; j < server.dbnum; j++) {
    // 創建全局哈希表
    server.db[j].dict = dictCreate(&dbDictType,NULL);
    // 創建過期 key 的信息表
    server.db[j].expires = dictCreate(&dbExpiresDictType,NULL);
    server.db[j].expires_cursor = 0;
    // 爲被 BLPOP 阻塞的 key 創建信息表
    server.db[j].blocking_keys = dictCreate(&keylistDictType,NULL);
    // 爲將執行 PUSH 的阻塞 key 創建信息表
    server.db[j].ready_keys = dictCreate(&objectKeyPointerValueDictType,NULL);
    // 爲被 MULTI/WATCH 操作監聽的 key 創建信息表
    server.db[j].watched_keys = dictCreate(&keylistDictType,NULL);
    ……
}

創建事件驅動框架

針對每個監聽 IP 上可能發生的客戶端連接,都創建了監聽事件,用來監聽客戶端連接請求。同時,initServer 爲監聽事件設置了相應的處理函數 acceptTcpHandler。

這樣一來,只要有客戶端連接到 server 監聽的 IP 和端口,事件驅動框架就會檢測到有連接事件發生,然後調用 acceptTcpHandler 函數來處理具體的連接。

//創建事件循環框架
server.el = aeCreateEventLoop(server.maxclients+CONFIG_FDSET_INCR);
…
//開始監聽設置的網絡端口
if (server.port != 0 &&
        listenToPort(server.port,server.ipfd,&server.ipfd_count) == C_ERR)
        exit(1);
…
//爲 server 後臺任務創建定時事件
if (aeCreateTimeEvent(server.el, 1, serverCron, NULL, NULL) == AE_ERR) {
        serverPanic("Can't create event loop timers.");
        exit(1);
}
…

階段 5:執行事件驅動框架

高效處理高併發的客戶端連接請求,Redis 採用了事件驅動框架,來併發處理不同客戶端的連接和讀寫請求。main 函數最後會調用 aeMain 函數進入事件驅動框架,循環處理各種觸發的事件。

// 事件驅動框架,循環處理各種觸發的事件
aeMain(server.el);
// 循環結束,刪除 eventLoop
aeDeleteEventLoop(server.el);

aeMain 函數核心調用了 aeProcessEvents 函數。aeProcessEvents 函數的具體源碼將在之後的文章中分析。

void aeMain(aeEventLoop *eventLoop) {
    eventLoop->stop = 0;
    // 循環調用
    while (!eventLoop->stop) {
        // 核心函數,處理事件的邏輯
        aeProcessEvents(eventLoop, AE_ALL_EVENTS|
                                   AE_CALL_BEFORE_SLEEP|
                                   AE_CALL_AFTER_SLEEP);
    }
}

參考鏈接

Redis 源碼簡潔剖析系列

最簡潔的 Redis 源碼剖析系列文章

Java 編程思想-最全思維導圖-GitHub 下載鏈接,需要的小夥伴可以自取~

原創不易,希望大家轉載時請先聯繫我,並標註原文鏈接。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章