Redis原始碼漂流記(二)-搭建Redis除錯環境

從此啟程發表於2022-05-07

Redis原始碼漂流記(二)-搭建Redis除錯環境

一、目標

  • 搭建Redis除錯環境
  • 簡要理解Redis命令運轉流程

二、前提

1、有一些c知識簡單基礎(變數命名、常用資料型別、指標等)

可以參考這篇簡單入門C語言入門教程 , 或者B站搜尋C語言相關教材(播放量最高的幾個均可)。

/*引入標頭檔案,類似java和go中的import包,C#中的using名稱空間*/
#include<stdio.h>                 
int main(void)                  /*一個簡單的C程式*/
{
    int number;            /*定義個名字叫做number的變數*/
    number=2022;                /*給number賦一個值*/
    printf("This year is %d\n",number); /*呼叫printf()函式*/
    int intsize = sizeof(int);
    /*輸出:int sizeof is 4 bytes*/
    printf("int sizeof is %d bytes\n",intsize);
    return 0;
}

/*Redis State of an event based program */
typedef struct aeEventLoop {
/* highest file descriptor currently registered */
    int maxfd;  
     /* max number of file descriptors tracked */
    int setsize;
    long long timeEventNextId;
    aeFileEvent *events; /* Registered events */
    aeFiredEvent *fired; /* Fired events */
    aeTimeEvent *timeEventHead;
    int stop;
    /* This is used for polling API specific data */
    void *apidata; 
    aeBeforeSleepProc *beforesleep;
    aeBeforeSleepProc *aftersleep;
    int flags;
} aeEventLoop;

基本資料型別

2、瞭解Redis的基本使用

如 set/get等即可。

3、 本地搭建過Ubautu虛擬機器或者直接有伺服器。

考慮到redis一般安裝到linux環境中,所以採取Ubantu進行除錯。

windows下需要下載redis-windows版本的原始碼,IDE採用使用Clion搭建redis debug環境Using GCC with MinGW

三、搭建IDE環境

1、安裝vscode

2、安裝vscode c++擴充套件

  • C/C++:提供C/C++支援
  • Code Runner:提供編譯後程式的執行環境
  • C/C++ Snippets:提供一些常用的C/C++片段,如for(;;){},安裝後寫程式碼 方便(tip.如果想要新增自己寫的程式碼段可以點左下角齒輪->使用者程式碼片段)
  • EPITECH C/C++ Headers :為C/C++檔案新增頭部(包括作者、建立和修改日期等),併為.h標頭檔案新增防重複的巨集
  • Include Autocomplete: 標頭檔案自動補全

3、安裝 gcc/gdb

先嚐試下面命令安裝

sudo apt-get update
sudo apt-get install build-essential gdb

如果不行就嘗試下面的。

sudo apt-get install aptitude
sudo aptitude install gcc g++

如果存在依賴性報錯安裝失敗,對建議的方案,第一個no,第二個yes. 檢測是否安裝成功

gcc -v
g++ -v
gdb -v
make -v

四、檢測c檔案執行和除錯

建立一個目錄,用於存放演示檔案

mkdir MyCode/src
cd MyCode/src

建立hello.c檔案

#include <stdio.h>
int main()
{
    puts("HelloC");
    return 0;
}

執行:按CodeRunner快捷鍵【Ctrl+Alt+N】執行程式碼:

[Running] cd "/home/fcw/MyCode/src/main/" && gcc hello.c -o hello && "/home/fcw/MyCode/src/main/"hello
HelloC

除錯: Run-->Start Debugging或 F5除錯

選擇環境 選擇配置

五、下載和編譯Redis

1.下載redis原始碼

// 建立redis目錄
mkdir MyCode/redis
cd MyCode/redis
// 下載redis
wget http://download.redis.io/releases/redis-6.2.7.tar.gz

// 解壓
tar xzf redis-6.2.7.tar.gz
cd redis-6.2.7/

2、編譯Redis

  • 編輯Makefile (這裡也可以開啟vsCode編輯複製 )
vim src/Makefile
  • 更新 makefile 下面對應的編譯項內容,修改該項的主要目的是為了防止編譯優化.

O0 -->> O1 -->> O2 -->> O3 (少優化->多優化), -O0表⽰沒有優化,-O1為預設值,-O3優化級別最⾼

# --------------------
# OPTIMIZATION?=-O2
OPTIMIZATION?=-O0
# REDIS_LD=$(QUIET_LINK)$(CC) $(FINAL_LDFLAGS)
REDIS_LD=$(QUIET_LINK)$(CC) $(FINAL_LDFLAGS) $(OPTIMIZATION)
# --------------------

更新前: 更新後

  • 編譯redis
make clean; make

Makefile和Make簡要說明:

需要執行/除錯多檔案時,Makefile可以設定你想要的編譯規則,你想要編譯哪些檔案,哪些檔案不需要編譯等等都可以體現在Makefile中,而且支援多執行緒併發操作,可以減少編譯的時間。

make是用來執行Makefile的,make根據Makefile中寫的內容進行編譯和連結,make更像是一個批處理的工具,可以批處理原始檔,只要執行一條make命令,就可以實現自動編譯。當我們編譯整個專案工程的時候,make只會編譯我們修改過的檔案,沒有修改過的就不用重新編譯,使用make+Makefile極大的提高了我們的工作效率。

cmake可以生成Makefile檔案,支援生成不同平臺的Makefile。cmake根據一個叫CMakeLists.txt(手寫)的檔案生成Makefile。

3、配置launch.json

launch.json

隨便選中一個c檔案。點除錯(F5),會提示新增配置。

{
    "configurations": [
        {
            "name""(gdb) 啟動",
            "type""cppdbg",
            "request""launch",
            "program""${workspaceFolder}/src/redis-server",
            "args": [ "${workspaceFolder}/redis.conf"],
            "stopAtEntry"false,
            "cwd""${fileDirname}",
            "environment": [],
            "externalConsole"false,
            "MIMode""gdb",
            "setupCommands": [
                {
                    "description""為 gdb 啟用整齊列印",
                    "text""-enable-pretty-printing",
                    "ignoreFailures"true
                },
                {
                    "description":  "將反彙編風格設定為 Intel",
                    "text""-gdb-set disassembly-flavor intel",
                    "ignoreFailures"true
                }
            ]
        }      
    ]
}

六、除錯Redis原始碼-初探

通過Readme.md可以看到相關檔案的簡介,例server.c檔案

server.c
---

This is the entry point of the Redis server, where the `main()` function
is defined. The following are the most important steps in order to startup
the Redis server.

* `initServerConfig()` sets up the default values of the `server` structure.
* `initServer()` allocates the data structures needed to operate, setup the listening socket, and so forth.
* `aeMain()` starts the event loop which listens for new connections.

There are two special functions called periodically by the event loop:

1. `serverCron()` is called periodically (according to `server.hz` frequency), and performs tasks that must be performed from time to time, like checking for timed out clients.
2. `beforeSleep()` is called every time the event loop fired, Redis served a few requests, and is returning back into the event loop.

Inside server.c you can find code that handles other vital things of the Redis server:

* `call()` is used in order to call a given command in the context of a given client.
* `activeExpireCycle()` handles eviction of keys with a time to live set via the `EXPIRE` command.
* `performEvictions()` is called when a new write command should be performed but Redis is out of memory according to the `maxmemory` directive.
* The global variable `redisCommandTable` defines all the Redis commands, specifying the name of the command, the function implementing the command, the number of arguments required, and other properties of each command.

找到 server.c, main 這是總入口

int main(int argc, char **argv)
aeMain(server.el);//這裡面是一個事件迴圈監聽
aeDeleteEventLoop(server.el);

這裡接收tcp或socket連線,然後將event及handler放入事件池/bus中。

/* Create an event handler for accepting new connections in TCP or TLS domain sockets.
 * This works atomically for all socket fds */

int createSocketAcceptHandler(socketFds *sfd, aeFileProc *accept_handler) {
    int j;

    for (j = 0; j < sfd->count; j++) {
        if (aeCreateFileEvent(server.el, sfd->fd[j], AE_READABLE, accept_handler,NULL) == AE_ERR) {
            /* Rollback */
            for (j = j-1; j >= 0; j--) aeDeleteFileEvent(server.el, sfd->fd[j], AE_READABLE);
            return C_ERR;
        }
    }
    return C_OK;
}

另起一個終端,執行redis-cli,會連結到redis-server,從而除錯redis相關原始碼。

cd MyCode/redis/redis-6.2.7/
./src/redis-cli

找到ae.c,這裡是一個while迴圈監控命令,用於監聽新函式的事件迴圈處理(server.cmain函式會呼叫這裡)。

void aeMain(aeEventLoop *eventLoop) {
    eventLoop->stop = 0;
    while (!eventLoop->stop) {
        aeProcessEvents(eventLoop, AE_ALL_EVENTS|
                                   AE_CALL_BEFORE_SLEEP|
                                   AE_CALL_AFTER_SLEEP);
    }
}
int aeProcessEvents(aeEventLoop *eventLoop, int flags)

找到connection.c

static void connSocketEventHandler(struct aeEventLoop *el, int fd, void *clientData, int mask)

找到connhelper.c

static inline int callHandler(connection *conn, ConnectionCallbackFunc handler) {
    connIncrRefs(conn);
    if (handler) handler(conn);
    connDecrRefs(conn);
    if (conn->flags & CONN_FLAG_CLOSE_SCHEDULED) {
        if (!connHasRefs(conn)) connClose(conn);
        return 0;
    }
    return 1;

找到networking.cprocessInputBufferprocessCommandAndResetClient 處打斷點

int processCommandAndResetClient(client *c) {
    int deadclient = 0;
    client *old_client = server.current_client;
    server.current_client = c;
    if (processCommand(c) == C_OK) {
        commandProcessed(c);
    }

找到server.c,在 processCommandcall等處打斷點

處理命令的總入口。

int processCommand(client *c) 

經過了moduleCallCommandFilters、檢查是否是quitlookupCommandauthRequiredACLCheckAllPermcluster_enabledserver.maxmemorywriteCommandsDeniedByDiskErrorrejectCommandblockClient等一系列安全檢查邏輯後,來到了執行命令的地方

  c->cmd = c->lastcmd = lookupCommand(c->argv[0]->ptr);
  ...
  ...
    /* Exec the command */
    if (c->flags & CLIENT_MULTI &&
        c->cmd->proc != execCommand && c->cmd->proc != discardCommand &&
        c->cmd->proc != multiCommand && c->cmd->proc != watchCommand &&
        c->cmd->proc != resetCommand)
    {
        queueMultiCommand(c);
        addReply(c,shared.queued);
    } else {
        call(c,CMD_CALL_FULL);
        c->woff = server.master_repl_offset;
        if (listLength(server.ready_keys))
            handleClientsBlockedOnKeys();
    }
void call(client *c, int flags) 
...
server.in_nested_call++
;
c->cmd->proc(c);
server.in_nested_call--;

找到t_string.c

setCommand

處理命令的堆疊資訊如下:

此後將結果寫回到客戶端

int writeToClient(client *c, int handler_installed) {
    /* Update total number of writes on server */
    atomicIncr(server.stat_total_writes_processed, 1);

返回結果的堆疊資訊如下:

從上面的除錯以及堆疊資訊可以看出,處理結果和將結果寫回到客戶端是在兩個事件中處理的。

總結redis服務端整個程式流程圖如下:

七、環境問題解決

  • gcc : 依賴: gcc-7(>= 7.3.0-12~) 但是它將不會被安裝
sudo apt-get install gcc
出現如下錯誤:
正在讀取軟體包列表… 完成
正在分析軟體包的依賴關係樹
正在讀取狀態資訊… 完成
有一些軟體包無法被安裝。如果您用的是 unstable 發行版,這也許是
因為系統無法達到您要求的狀態造成的。該版本中可能會有一些您需要的軟體
包尚未被建立或是它們已被從新到(Incoming)目錄移出。
下列資訊可能會對解決問題有所幫助:
下列軟體包有未滿足的依賴關係:
gcc : 依賴: gcc-7(>= 7.3.0-12~) 但是它將不會被安裝
E: 無法修正錯誤,因為您要求某些軟體包保持現狀,就是它們破壞了軟體包間的依賴關係。

使用aptitude包依賴管理工具代替apt來處理,aptitude軟體包管理工具在解決依賴性問題上更有優勢,具體使用方法如下:
sudo apt-get install aptitude
sudo aptitude install gcc g++
終端中輸入後會提示aptitude給出的解決方案,可以選擇no,
會繼續提供下一個解決方案,但前面的方案會是忽略掉依賴衝突,所以想要徹底解決的話可以跳過前面的幾種方案,然後再yes解決。(個人第一次No,第二次Yes)
https://blog.csdn.net/zhutingting0428/article/details/51120949
  • linux ubuntu gcc編譯 fatal error: bits/libc-header-start.h 錯誤解決
apt-get install gcc-multilib 

其實主要是gcc安裝環境沒有安裝完善Multilib,顧名思義,就是多重的。 用它完全可以替代原來單一的lib。 這樣就既能產生32位的程式碼,又能生成64位的。比如:64bit機器,同時可以產生32和64兩種格式,

八、引用資料

這篇文章整理了一個從服務端和客戶端兩個維度流程圖片

九、轉載請註明出處

  • https://www.cnblogs.com/fancunwei
  • ”威行雲棧“公眾號(微訊號:fundeway)

相關文章