Redis源碼系列的初衷,是幫助咱們更好地理解Redis,更懂Redis,而怎麼才能懂,光看是不夠的,建議跟着下面的這一篇,把環境搭建起來,後續能夠本身閱讀源碼,或者跟着我這邊一塊兒閱讀。因爲我用c也是好幾年之前了,些許錯誤在所不免,但願讀者能不吝指出。html
曹工說Redis源碼(1)-- redis debug環境搭建,使用clion,達到和調試java同樣的效果java
除了大學那些玩具,一個真正的項目,都是由大量源代碼文件組成一個工程。在Java裏,一個 java 文件要使用其餘 java 文件中的函數、類型、變量等,都須要使用import語句來引入。在c語言裏,也是同樣的,在c語言中,要引入其餘文件的功能,須要使用include語句。node
好比,在redis的主入口,redis.c文件中,就包含了以下一堆語句:linux
#include "redis.h" #include "cluster.h" #include "slowlog.h" #include "bio.h" #include <time.h> #include <signal.h>
其中,以<開頭的,好比<time.h>是標準庫的頭文件,會在系統指定的路徑下查找,可類比爲jdk
官方的class;"bio.h"這種,以""包裹的,則是工程裏自定義的。c++
好比,time.h,我在linux的如下路徑查找到了:redis
[root@mini1 src]# locate time.h /usr/include/time.h
其餘include相關知識,能夠參考:shell
https://www.runoob.com/cprogramming/c-header-files.html數據庫
通常來講,咱們會在.c文件中,去編寫咱們的業務邏輯方法,其中,一些方法,多是隻在本文件內部用到的,相似於java class的private方法;一些方法呢,多是須要在外部的其餘源碼文件中,也須要用到的,這些方法,要怎麼才能讓外部可使用呢?編程
就是經過頭文件機制,能夠理解爲各大高級語言中的接口,在java中,定義一個class,雖然能夠直接把方法設爲public,其餘類能夠直接訪問;可是,在平時的業務開發中,咱們通常並不會直接訪問一個實現類,而是經過它實現的接口去訪問;一個好的實現類,也不該該把沒在接口中定義的方法,設爲public權限。centos
說回頭文件,好比有個源碼文件test.c
以下:
long long ustime(void) { struct timeval tv; long long ust; gettimeofday(&tv, NULL); ust = ((long long)tv.tv_sec)*1000000; ust += tv.tv_usec; return ust; } /* Return the UNIX time in milliseconds */ // 返回毫秒格式的 UNIX 時間 // 1 秒 = 1 000 毫秒 long long mstime(void) { return ustime()/1000; }
這個文件裏,定義了2個方法,但假設咱們只須要對外暴露mstime(void)
方法,那麼,頭文件test.h
應該是下面這樣的:
long long mstime(void);
這樣的話,咱們的另外一個方法,ustime,對外就不可見了。
總之,你們能夠把頭文件理解爲實現類要對外暴露的接口;你們可能以爲個人比喻不恰當,爲啥把c文件,說成實現類,實際上,咱們以前在華爲的時候,確實是用c++的思想,面向對象的思想,來寫c語言的。
我看到網上一篇文章,這裏引用一下(https://zhuanlan.zhihu.com/p/57882822):
反觀Redis,他是純C編碼,可是融入了面向對象的思想。和上述觀點截然相反,可謂是『用C++去設計,用C編碼』。固然本文目的並不是挑起語言之爭,各類語言自有其利弊,開源項目的語言選擇也主要是因爲項目做者的我的經歷和主觀意願。
可是c語言中的頭文件,和java這些語言中的接口,仍是不一樣的;在java中,接口和實現類同樣,最終都是編譯爲獨立的class文件。
在c語言中,在編譯實現類以前,會有一個預處理的過程,預處理的過程,就是把include語句,直接替換爲被include的頭文件的內容,好比,以菜鳥教程中的例子舉例:
header.h char *test (void);
在以下的 program.c
中,須要使用上面的header.h中的test方法,則須要include:
int x; #include "header.h" int main (void) { puts (test ()); }
通過預處理後,(就是進行簡單的replace),效果以下:
int x; char *test (void); int main (void) { puts (test ()); }
咱們可使用以下命令,來演示這個過程:
[root@mini1 test]# gcc -E program.c int x; # 1 "header.h" 1 char *test (void); # 3 "program.c" 2 int main (void) { puts (test ()); }
從上面能夠看到,已經replace進去了;若是咱們include兩次,會怎樣?
[root@mini1 test]# gcc -E program.c int x; # 1 "header.h" 1 char *test (void); # 3 "program.c" 2 # 1 "header.h" 1 char *test (void); # 4 "program.c" 2 int main (void) { puts (test ()); }
能夠發現,這個header的內容,出現了2次,重複了。可是上面這種狀況,並不會報錯,無非是方法被定義了兩次。
你們看頭文件,都會發現以下語句,好比在redis.h中:
#ifndef __REDIS_H #define __REDIS_H #include "fmacros.h" #include "config.h" ... typedef struct redisObject { // 類型 unsigned type:4; // 編碼 unsigned encoding:4; // 對象最後一次被訪問的時間 unsigned lru:REDIS_LRU_BITS; /* lru time (relative to server.lruclock) */ // 引用計數 int refcount; // 指向實際值的指針 void *ptr; } robj; ... #endif
能夠看到,最開始,有一句:
#ifndef __REDIS_H #define __REDIS_H
結尾有一句:
#endif
這個就是爲了解決以下問題:
在頭文件被重複引入時(間接地,或直接地,被include了兩次),若是不加這個,就會致使頭文件裏的內容,被引入兩次;加了這個以後呢,即便被include了兩次,程序在運行時,一開始,發現沒有定義__REDIS_H
這個宏,而後定義它;等到程序遇到第二次include的內容時,發現__REDIS_H
這個宏已經被定義了,就直接跳過了,這樣保證了同一個頭文件,即便被屢次include,也能保證其內容,只被解析一次。
另外,像方法聲明這種,定義屢次可能沒事,可是,若是在頭文件裏,有以下類型定義呢:
typedef char my_char; char *test (void);
若是重複include同一個頭文件的話,就會形成類型重複定義。不過,很奇怪的是,我在centos 7.3.1611上試了,gcc版本:gcc (GCC) 4.8.5 20150623 (Red Hat 4.8.5-16)
,居然沒報錯。看來我以前的c語言知識,也沒學到家。
我在網上暫時也沒找到重複include,具體的害處是啥,網上找到的答案就兩種:
可是,第一個答案,嚴格來講 ,是不存在的,由於公司通常禁止在頭文件中定義變量。
有個知乎問題,你們能夠看看:頭文件被重複包含究竟有哪些危害?
你們能夠自行搜索:華爲技術有限公司c語言編程規範
我這裏僅截取部分:
規則1.6 禁止在頭文件中定義變量。 說明: 在頭文件中定義變量,將會因爲頭文件被其餘.c文件包含而致使變量重複定義。 規則1.7 只能經過包含頭文件的方式使用其餘.c提供的接口,禁止在.c中經過extern的方式使用外部 函數接口、變量。 說明:若a.c使用了b.c定義的foo()函數,則應當在b.h中聲明extern int foo(int input);並在a.c 中經過#include <b.h>來使用foo。禁止經過在a.c中直接寫extern int foo(int input);來使用foo, 後面這種寫法容易在foo改變時可能致使聲明和定義不一致。
這裏的1.7,也是和咱們的理解是一致的,頭文件就是一個實現模塊的對外接口,在裏面通常只能容許放如下內容:
最後這一點,我要補充下。咱們剛纔禁止了,在頭文件中定義變量,因此,咱們的變量,是在c文件中定義。好比,在redis.c中,定義了一個全局變量:
/* Global vars */ struct redisServer server; /* server global state */
這麼一個重要的全局變量,基本維護了redis-server的一個實例的所有狀態值,只在本身redis.c中使用,是不可能的。那要怎麼在其餘文件使用呢,就要在redis.h頭文件中進行以下聲明:
/*----------------------------------------------------------------------------- * Extern declarations *----------------------------------------------------------------------------*/ extern struct redisServer server;
通常使用struct來定義一個結構體,相似高級語言中的class。
好比,redis中的字符串,通常會使用sds這個數據結構來存儲,其結構體定義就像下面這樣:
struct sdshdr { // buf 中已佔用空間的長度 int len; // buf 中剩餘可用空間的長度 int free; // 數據空間 char buf[]; };
另外,c語言中,會大量使用typedef來定義一個類型的別名。
具體能夠參考這個教程看看:
https://www.runoob.com/cprogramming/c-typedef.html
基礎知識:https://www.runoob.com/cprogramming/c-pointers.html
我這裏說下我對指針的理解,指針通常指向一個內存地址,你們能夠先無論這個指針是什麼類型,事實上,當咱們不關心其指向的地址上,是什麼數據類型時,能夠直接定義爲 void * ptr。
這個指針,假設指向A這個地址,當咱們認爲上面存儲的是一個char時,就能夠把這個指針,從void *強轉爲char * 類型,而後對該指針解引用的話,由於char類型只佔用一個字節,因此只須要,從該指針指向的位置開始,取當前這個字節的內容,而後解析爲char,就能獲取到這個地址上的char值。
若是咱們把void * 強轉爲int *的話,對其解引用時,就會取當前指針位置開始的4個字節,由於整數佔4個字節,而後將其轉爲整數。
總的來講,對一個指針解引用時,首先就是看當前指針的數據類型,好比 int *指針,那麼說明指向int,就會取4個字節來進行解引用;若是是指向一個結構體,就會計算結構體佔用的字節數,而後取對應的字節,來解引用爲結構體類型的變量。
這部分,你們能夠看看這塊:
https://www.runoob.com/cprogramming/c-data-types.html
https://www.runoob.com/cprogramming/c-pointer-arithmetic.html
前面說了不少,咱們本講也不夠講徹底部的redis啓動過程了,可能還要兩講的樣子,本講先講解一部分。
啓動入口在:redis.c中的main 方法,若是使用我這邊的代碼來搭建調試環境,能夠直接啓動redis-server。
int main(int argc, char **argv) { struct timeval tv; /** * 1 設置時區 */ setlocale(LC_COLLATE,""); /** *2 */ zmalloc_enable_thread_safeness(); // 3 zmalloc_set_oom_handler(redisOutOfMemoryHandler); // 4 srand(time(NULL)^getpid()); // 5 gettimeofday(&tv,NULL); // 6 dictSetHashFunctionSeed(tv.tv_sec^tv.tv_usec^getpid()); // 檢查服務器是否以 Sentinel 模式啓動 server.sentinel_mode = checkForSentinelMode(argc,argv); // 7 初始化服務器 initServerConfig();
1處,設置時區
2處,設置進行內存分配的線程的數量,這裏會設爲1
3處,設置oom發生時的函數指針,函數指針指向一個函數,相似於java 8中,lambda表達式中,丟一個方法的引用給流;函數指針會在oom時,被回調,整體來講,就相似於java中的模板設計模式或者策略模式。
4處,設置隨機數的種子
5處,獲取當前時間,設置到 tv
這個變量中
注意,這裏把tv的地址傳進去了,這是c語言中典型的用法,相似於java中傳一個對象的引用進去,而後在方法內部,會修改該對象的內部field等
6處,設置hash函數的種子
7處,初始化服務器。
這裏重點說下7處:
void initServerConfig() { int j; // 服務器狀態 // 設置服務器的運行 ID getRandomHexChars(server.runid,REDIS_RUN_ID_SIZE); // 設置默認配置文件路徑 server.configfile = NULL; // 設置默認服務器頻率 server.hz = REDIS_DEFAULT_HZ; // 爲運行 ID 加上結尾字符 server.runid[REDIS_RUN_ID_SIZE] = '\0'; // 設置服務器的運行架構 server.arch_bits = (sizeof(long) == 8) ? 64 : 32; // 設置默認服務器端口號 server.port = REDIS_SERVERPORT; // tcp 全鏈接隊列的長度 server.tcp_backlog = REDIS_TCP_BACKLOG; // 綁定的地址的數量 server.bindaddr_count = 0; // UNIX socket path server.unixsocket = NULL; server.unixsocketperm = REDIS_DEFAULT_UNIX_SOCKET_PERM; // 綁定的 TCP socket file descriptors server.ipfd_count = 0; server.sofd = -1; // redis可以使用的redis db的數量 server.dbnum = REDIS_DEFAULT_DBNUM; // redis 日誌級別 server.verbosity = REDIS_DEFAULT_VERBOSITY; // Client timeout in seconds,客戶端最大空閒時間;超過這個時間的客戶端,會被強制關閉 server.maxidletime = REDIS_MAXIDLETIME; // Set SO_KEEPALIVE if non-zero. 若是設爲非0,則開啓tcp的SO_KEEPALIVE server.tcpkeepalive = REDIS_DEFAULT_TCP_KEEPALIVE; // 打開這個選項,會週期性地清理過時key server.active_expire_enabled = 1; // 客戶端發來的請求中,查詢緩存的最大值;好比一個set命令,value的大小就會和這個緩衝區大小比較, // 若是大了,就根本放不進緩衝區 server.client_max_querybuf_len = REDIS_MAX_QUERYBUF_LEN; // rdb保存參數,好比每60s保存,n個鍵被修改了保存,之類的 server.saveparams = NULL; // 若是爲1,表示服務器正在從磁盤載入數據: We are loading data from disk if true server.loading = 0; // 日誌文件位置 server.logfile = zstrdup(REDIS_DEFAULT_LOGFILE); // 開啓syslog等機制 server.syslog_enabled = REDIS_DEFAULT_SYSLOG_ENABLED; server.syslog_ident = zstrdup(REDIS_DEFAULT_SYSLOG_IDENT); server.syslog_facility = LOG_LOCAL0; // 後臺運行 server.daemonize = REDIS_DEFAULT_DAEMONIZE; // aof狀態 server.aof_state = REDIS_AOF_OFF; // aof的刷磁盤策略,默認每秒刷盤 server.aof_fsync = REDIS_DEFAULT_AOF_FSYNC; // 正在rewrite時,不刷盤 server.aof_no_fsync_on_rewrite = REDIS_DEFAULT_AOF_NO_FSYNC_ON_REWRITE; // Rewrite AOF if % growth is > M and... server.aof_rewrite_perc = REDIS_AOF_REWRITE_PERC; // the AOF file is at least N bytes. aof達到多大時,觸發rewrite server.aof_rewrite_min_size = REDIS_AOF_REWRITE_MIN_SIZE; // 最後一次執行 BGREWRITEAOF 時, AOF 文件的大小 server.aof_rewrite_base_size = 0; // Rewrite once BGSAVE terminates.開啓該選項時,BGSAVE結束時,觸發rewrite server.aof_rewrite_scheduled = 0; // 最近一次aof進行fsync的時間 server.aof_last_fsync = time(NULL); // 最近一次aof重寫,消耗的時間 server.aof_rewrite_time_last = -1; // Current AOF rewrite start time. server.aof_rewrite_time_start = -1; // 最後一次執行 BGREWRITEAOF 的結果 server.aof_lastbgrewrite_status = REDIS_OK; // 記錄 AOF 的 fsync 操做被推遲了多少次 server.aof_delayed_fsync = 0; // File descriptor of currently selected AOF file server.aof_fd = -1; // AOF 的當前目標數據庫 server.aof_selected_db = -1; /* Make sure the first time will not match */ // UNIX time of postponed AOF flush server.aof_flush_postponed_start = 0; // fsync incrementally while rewriting? 重寫過程當中,增量觸發fsync server.aof_rewrite_incremental_fsync = REDIS_DEFAULT_AOF_REWRITE_INCREMENTAL_FSYNC; // pid文件 server.pidfile = zstrdup(REDIS_DEFAULT_PID_FILE); // rdb 文件名 server.rdb_filename = zstrdup(REDIS_DEFAULT_RDB_FILENAME); // aof 文件名 server.aof_filename = zstrdup(REDIS_DEFAULT_AOF_FILENAME); // 是否要密碼 server.requirepass = NULL; // 是否進行rdb壓縮 server.rdb_compression = REDIS_DEFAULT_RDB_COMPRESSION; // rdb checksum server.rdb_checksum = REDIS_DEFAULT_RDB_CHECKSUM; // bgsave失敗,中止寫入 server.stop_writes_on_bgsave_err = REDIS_DEFAULT_STOP_WRITES_ON_BGSAVE_ERROR; // 在執行 serverCron() 時進行漸進式 rehash server.activerehashing = REDIS_DEFAULT_ACTIVE_REHASHING; server.notify_keyspace_events = 0; // 支持的最大客戶端數量 server.maxclients = REDIS_MAX_CLIENTS; // bpop阻塞的客戶端 server.bpop_blocked_clients = 0; // 可使用的最大內存 server.maxmemory = REDIS_DEFAULT_MAXMEMORY; // 內存淘汰策略,也就是key的過時策略 server.maxmemory_policy = REDIS_DEFAULT_MAXMEMORY_POLICY; server.maxmemory_samples = REDIS_DEFAULT_MAXMEMORY_SAMPLES; // hash表的元素小於這個值時,使用ziplist 編碼模式;如下幾個相似 server.hash_max_ziplist_entries = REDIS_HASH_MAX_ZIPLIST_ENTRIES; server.hash_max_ziplist_value = REDIS_HASH_MAX_ZIPLIST_VALUE; server.list_max_ziplist_entries = REDIS_LIST_MAX_ZIPLIST_ENTRIES; server.list_max_ziplist_value = REDIS_LIST_MAX_ZIPLIST_VALUE; server.set_max_intset_entries = REDIS_SET_MAX_INTSET_ENTRIES; server.zset_max_ziplist_entries = REDIS_ZSET_MAX_ZIPLIST_ENTRIES; server.zset_max_ziplist_value = REDIS_ZSET_MAX_ZIPLIST_VALUE; server.hll_sparse_max_bytes = REDIS_DEFAULT_HLL_SPARSE_MAX_BYTES; // 該標識打開時,表示正在關閉服務器 server.shutdown_asap = 0; // 複製相關 server.repl_ping_slave_period = REDIS_REPL_PING_SLAVE_PERIOD; server.repl_timeout = REDIS_REPL_TIMEOUT; server.repl_min_slaves_to_write = REDIS_DEFAULT_MIN_SLAVES_TO_WRITE; server.repl_min_slaves_max_lag = REDIS_DEFAULT_MIN_SLAVES_MAX_LAG; // cluster模式相關 server.cluster_enabled = 0; server.cluster_node_timeout = REDIS_CLUSTER_DEFAULT_NODE_TIMEOUT; server.cluster_migration_barrier = REDIS_CLUSTER_DEFAULT_MIGRATION_BARRIER; server.cluster_configfile = zstrdup(REDIS_DEFAULT_CLUSTER_CONFIG_FILE); // lua腳本 server.lua_caller = NULL; server.lua_time_limit = REDIS_LUA_TIME_LIMIT; server.lua_client = NULL; server.lua_timedout = 0; // server.migrate_cached_sockets = dictCreate(&migrateCacheDictType,NULL); server.loading_process_events_interval_bytes = (1024*1024*2); // 初始化 LRU 時間 server.lruclock = getLRUClock(); // 初始化並設置保存條件 resetServerSaveParams(); // rdb的默認保存策略 appendServerSaveParams(60*60,1); /* save after 1 hour and 1 change */ appendServerSaveParams(300,100); /* save after 5 minutes and 100 changes */ appendServerSaveParams(60,10000); /* save after 1 minute and 10000 changes */ /* Replication related */ // 初始化和複製相關的狀態 server.masterauth = NULL; server.masterhost = NULL; server.masterport = 6379; server.master = NULL; server.cached_master = NULL; server.repl_master_initial_offset = -1; server.repl_state = REDIS_REPL_NONE; server.repl_syncio_timeout = REDIS_REPL_SYNCIO_TIMEOUT; server.repl_serve_stale_data = REDIS_DEFAULT_SLAVE_SERVE_STALE_DATA; server.repl_slave_ro = REDIS_DEFAULT_SLAVE_READ_ONLY; server.repl_down_since = 0; /* Never connected, repl is down since EVER. */ server.repl_disable_tcp_nodelay = REDIS_DEFAULT_REPL_DISABLE_TCP_NODELAY; server.slave_priority = REDIS_DEFAULT_SLAVE_PRIORITY; server.master_repl_offset = 0; /* Replication partial resync backlog */ // 初始化 PSYNC 命令所使用的 backlog server.repl_backlog = NULL; server.repl_backlog_size = REDIS_DEFAULT_REPL_BACKLOG_SIZE; server.repl_backlog_histlen = 0; server.repl_backlog_idx = 0; server.repl_backlog_off = 0; server.repl_backlog_time_limit = REDIS_DEFAULT_REPL_BACKLOG_TIME_LIMIT; server.repl_no_slaves_since = time(NULL); /* Client output buffer limits */ // 設置客戶端的輸出緩衝區限制 for (j = 0; j < REDIS_CLIENT_LIMIT_NUM_CLASSES; j++) server.client_obuf_limits[j] = clientBufferLimitsDefaults[j]; /* Double constants initialization */ // 初始化浮點常量 R_Zero = 0.0; R_PosInf = 1.0/R_Zero; R_NegInf = -1.0/R_Zero; R_Nan = R_Zero/R_Zero; // 初始化命令表,好比get、set、hset等各自的處理函數,放進一個hash表,方便後續處理請求 server.commands = dictCreate(&commandTableDictType,NULL); server.orig_commands = dictCreate(&commandTableDictType,NULL); populateCommandTable(); server.delCommand = lookupCommandByCString("del"); server.multiCommand = lookupCommandByCString("multi"); server.lpushCommand = lookupCommandByCString("lpush"); server.lpopCommand = lookupCommandByCString("lpop"); server.rpopCommand = lookupCommandByCString("rpop"); /* Slow log */ // 初始化慢查詢日誌 server.slowlog_log_slower_than = REDIS_SLOWLOG_LOG_SLOWER_THAN; server.slowlog_max_len = REDIS_SLOWLOG_MAX_LEN; /* Debugging */ // 初始化調試項 server.assert_failed = "<no assertion failed>"; server.assert_file = "<no file>"; server.assert_line = 0; server.bug_report_start = 0; server.watchdog_period = 0; }
以上都加了註釋,咱們能夠先不看:複製、cluster、lua等相關的,先看其餘的。
過久沒碰c了,有些遺忘,不過整體來講,並不難,難的是內存泄露之類,但咱們只是debug學習使用,不用擔憂這些問題。
指針那一塊,須要一點點基礎,你們能夠花點時間學一下。
你們看看有啥問題或者建議,歡迎指出。