標籤:redis 源碼 伺服器
最近一直在看redis的源碼,準備把對源碼的理解以及閱讀心得記錄下來,避免忘記並可以和大家分享、談論。看代碼的思路很簡單,直接從main函數走起,先看看初始化過程。redis中一個最重要的資料結構是redis_server,會建立一個這個結構的全域變數server,表示當前redis的配置及狀態,初始化的大部分工作就是設定這個結構的屬性。可以把初始化工作主要劃分為4個部分:1)為server設定預設值 2)解析命令列參數 3)解析設定檔 4)初始化server 初始化完成後,就會啟動事件迴圈,以接收、服務要求。下面分步驟解析main函數,不會關注sentinel邏輯。(1)首先是main函數的起始部分,主要進行簡單的初始化工作,包括設定collate,設定隨機數種子,然後調用initServerConfig()為全域變數server設定預設值。
struct timeval tv; /* We need to initialize our libraries, and the server configuration. */#ifdef INIT_SETPROCTITLE_REPLACEMENT spt_init(argc, argv);#endif // <MM> // 使用環境變數初始化 字串比較 // </MM> setlocale(LC_COLLATE,""); zmalloc_enable_thread_safeness(); zmalloc_set_oom_handler(redisOutOfMemoryHandler); // <MM> // 設定random的種子,之後會產生隨機的run id,所有加入進程id因素 // </MM> srand(time(NULL)^getpid()); gettimeofday(&tv,NULL); dictSetHashFunctionSeed(tv.tv_sec^tv.tv_usec^getpid()); server.sentinel_mode = checkForSentinelMode(argc,argv); // <MM> // 為redisServer設定初始預設值 // </MM> initServerConfig(); /* We need to init sentinel right now as parsing the configuration file * in sentinel mode will have the effect of populating the sentinel * data structures with master nodes to monitor. */ if (server.sentinel_mode) { initSentinelConfig(); initSentinel(); }
在initServerConfig函數中,大部分是對server的屬性設定預設值,還有一部分是調用populateCommandTable函數對redis的命令表初始化。全域變數redisCommandTable是redisCommand類型的數組,儲存redis支援的所有命令。server.commands是一個dict,儲存命令名到redisCommand的映射。populateCommandTable函數會遍曆全域redisCommandTable表,把每條命令插入到server.commands中,根據每個命令的屬性設定其flags。redisCommand結構如下:
struct redisCommand { // 命令名稱,在server.commands命令表中,以命令名位key char *name; // 命令處理函數 redisCommandProc *proc; // command的運算元,>0時表示確切的運算元,<0則表示至少有arity個運算元 int arity; // 標記位的字元表示形式,主要用於命令表的初始化 char *sflags; /* Flags as string representation, one char per flag. */ // 屬性標記位,bitwise,指定command的類型 int flags; /* The actual flags, obtained from the ‘sflags‘ field. */ // 下面的4個屬性都是用於從用戶端的一個請求解析出key,比如mset k1, v1, k2, v2 ... /* Use a function to determine keys arguments in a command line. */ redisGetKeysProc *getkeys_proc; /* What keys should be loaded in background when calling this command? */ int firstkey; /* The first argument that‘s a key (0 = no keys) */ int lastkey; /* The last argument that‘s a key */ int keystep; /* The step between first and last key */ // 統計資訊 long long microseconds, calls;};
redisCommand有屬性proc,表示命令處理函數。在處理用戶端請求時,擷取到命令名,從server.commands字典中擷取到redisCommand,然後回調其proc函數。
(2)命令列解析部分,主要是解析出命令列配置選項,以及通過命令列傳入的配置項。然後會解析設定檔,主要是通過兩個函數: loadServerConfig:完成的功能很簡單,就是將檔案內容讀到字串中。並將通過命令列傳入的配置項追加到該字串後。 loadServerConfigFromString:從字串中解析出配置項,並設定server的相關屬性。此步完成後,server中的簡單屬性(整數、字串)基本都設定完成。(3)接下來是初始化部分。設定為守護進程,建立pid檔案,設定進程title以及列印開機記錄。其中最重要的是initServer函數初始化基礎資料結構。在初始化之後,需要從磁碟載入資料,可以是rdb或者aof。
// <MM> // 守護進程 // </MM> if (server.daemonize) daemonize(); initServer(); // <MM> // 建立pid檔案 // </MM> if (server.daemonize) createPidFile(); // <MM> // 設定進程名字 // </MM> redisSetProcTitle(argv[0]); // <MM> // 列印開機記錄 // </MM> redisAsciiArt(); if (!server.sentinel_mode) { /* Things not needed when running in Sentinel mode. */ redisLog(REDIS_WARNING,"Server started, Redis version " REDIS_VERSION); #ifdef __linux__ linuxOvercommitMemoryWarning(); #endif // <MM> // 從磁碟載入資料,rdb或aof // </MM> loadDataFromDisk(); if (server.ipfd_count > 0) redisLog(REDIS_NOTICE,"The server is now ready to accept connections on port %d", server.port); if (server.sofd > 0) redisLog(REDIS_NOTICE,"The server is now ready to accept connections at %s", server.unixsocket); } else { sentinelIsRunning(); } /* Warning the user about suspicious maxmemory setting. */ if (server.maxmemory > 0 && server.maxmemory < 1024*1024) { redisLog(REDIS_WARNING,"WARNING: You specified a maxmemory value that is less than 1MB (current value is %llu bytes). Are you sure this is what you really want?", server.maxmemory); }
initServerConfig中會對server的整型、字串類型的屬性設定,initServer主要對複合資料結構list、dict等初始化,並建立事件迴圈,初始化監聽socket等。下面看下initServer函數:
首先,註冊訊號處理函數。
int j; signal(SIGHUP, SIG_IGN); signal(SIGPIPE, SIG_IGN); setupSignalHandlers();
如果設定開啟syslog,則初始化。
if (server.syslog_enabled) { openlog(server.syslog_ident, LOG_PID | LOG_NDELAY | LOG_NOWAIT, server.syslog_facility); }
初始化用戶端相關的資料結構。redis會將所有串連的用戶端,slave以及monitor的用戶端組織成鏈表,此處主要是建立這些鏈表。
server.current_client = NULL; server.clients = listCreate(); server.clients_to_close = listCreate(); server.slaves = listCreate(); server.monitors = listCreate(); server.slaveseldb = -1; /* Force to emit the first SELECT command. */ server.unblocked_clients = listCreate(); server.ready_keys = listCreate();
redis中會對經常使用的對象,建立常量池,以避免頻繁建立、回收的開銷。這些對象包括經常使用的響應內容以及小於10000的整數,還有表示bulk個數的回應標頭以及bulk長度的回應標頭,這兩個回應標頭只包括長度小於32的。
// <MM> // 建立常量池,避免頻繁建立 // </MM> createSharedObjects();
調整開啟描述符限制,需要調用getrlimit和setrlimit。
// <MM> // 根據maxclients配置,調整開啟檔案描述符限制 // </MM> adjustOpenFilesLimit();
初始化事件迴圈,具體過程在事件迴圈一節中講述。然後根據配置建立db數組。
// <MM> // 初始化event loop,如是epoll實現,會調用epoll_create建立epoll的fd // </MM> server.el = aeCreateEventLoop(server.maxclients+REDIS_EVENTLOOP_FDSET_INCR); server.db = zmalloc(sizeof(redisDb)*server.dbnum);
初始化監聽socket,就是調用socket、bind、listen等,並將socket設定為非阻塞。如果配置了unix domain socket,也會進行相應的初始化。
/* Open the TCP listening socket for the user commands. */ if (server.port != 0 && listenToPort(server.port,server.ipfd,&server.ipfd_count) == REDIS_ERR) exit(1); /* Open the listening Unix domain socket. */ if (server.unixsocket != NULL) { unlink(server.unixsocket); /* don‘t care if this fails */ server.sofd = anetUnixServer(server.neterr,server.unixsocket, server.unixsocketperm, server.tcp_backlog); if (server.sofd == ANET_ERR) { redisLog(REDIS_WARNING, "Opening socket: %s", server.neterr); exit(1); } anetNonBlock(NULL,server.sofd); } /* Abort if there are no listening sockets at all. */ if (server.ipfd_count == 0 && server.sofd < 0) { redisLog(REDIS_WARNING, "Configured to not listen anywhere, exiting."); exit(1); }
接下來的代碼,會初始化server.db數組中的每個db,主要建立相關的dict。然後關於pubsub、rdb、aof、replication相關資料結構的初始化。代碼就不貼了。然後添加定時器事件serverCron,在啟動事件迴圈1ms後執行。redis中的事件迴圈就只有這一個,每0.1s(大約)執行一次,主要處理一些非同步任務,比如清除expired keys,更新統計,check進行rdb、aof子進程的狀態,然後會調用clientCron和databaseCron等。
// <MM> // 註冊serverCrom時間事件,啟動event loop 1ms後執行 // </MM> /* Create the serverCron() time event, that‘s our main way to process * background operations. */ if(aeCreateTimeEvent(server.el, 1, serverCron, NULL, NULL) == AE_ERR) { redisPanic("Can‘t create the serverCron time event."); exit(1); }
將所有監聽socket添加到事件迴圈。可以看到會有acceptTcpHandler處理用戶端串連的建立。
// <MM> // 為所有的監聽socket添加file event,事件處理器是acceptTcpHandler用於處理串連的建立 // </MM> /* Create an event handler for accepting new connections in TCP and Unix * domain sockets. */ for (j = 0; j < server.ipfd_count; j++) { if (aeCreateFileEvent(server.el, server.ipfd[j], AE_READABLE, acceptTcpHandler,NULL) == AE_ERR) { redisPanic( "Unrecoverable error creating server.ipfd file event."); } } if (server.sofd > 0 && aeCreateFileEvent(server.el,server.sofd,AE_READABLE, acceptUnixHandler,NULL) == AE_ERR) redisPanic("Unrecoverable error creating server.sofd file event.");
如果開啟aof,則建立aof檔案。
/* Open the AOF file if needed. */ if (server.aof_state == REDIS_AOF_ON) { server.aof_fd = open(server.aof_filename, O_WRONLY|O_APPEND|O_CREAT,0644); if (server.aof_fd == -1) { redisLog(REDIS_WARNING, "Can‘t open the append-only file: %s", strerror(errno)); exit(1); } }
最後部分,校正並設定maxmemory配置,初始化lua指令碼、slow log以及latency monitor,最後的bioInit初始化非同步io線程。redis是單線程,對於重IO操作,比如aof的fsync的調用會由非同步線程調用,避免阻塞主線程的事件迴圈。以上就是initServer函數。
/* 32 bit instances are limited to 4GB of address space, so if there is * no explicit limit in the user provided configuration we set a limit * at 3 GB using maxmemory with ‘noeviction‘ policy‘. This avoids * useless crashes of the Redis instance for out of memory. */ if (server.arch_bits == 32 && server.maxmemory == 0) { redisLog(REDIS_WARNING,"Warning: 32 bit instance detected but no memory limit set. Setting 3 GB maxmemory limit with ‘noeviction‘ policy now."); server.maxmemory = 3072LL*(1024*1024); /* 3 GB */ server.maxmemory_policy = REDIS_MAXMEMORY_NO_EVICTION; } replicationScriptCacheInit(); scriptingInit(); slowlogInit(); latencyMonitorInit(); // <MM> // 初始化非同步阻塞IO處理線程 // </MM> bioInit();
(4)main函數的最後,是啟動事件迴圈。在事件迴圈的每次迭代,sleep之前會調用beforeSleep函數,進行一些非同步處理。此處首先設定beforeSleep函數,然後啟動aeMain事件迴圈。當從事件迴圈退出後,清理事件迴圈,然後退出。
aeSetBeforeSleepProc(server.el,beforeSleep); // <MM> // 開啟event loop // </MM> aeMain(server.el); aeDeleteEventLoop(server.el); return 0;
以上便是redis的初始化、啟動過程。總體上,比較簡單,相比nginx的設定檔解析,模組初始化,進程初始化要簡單不少,讀起來不會太費勁。下一篇介紹事件迴圈。
redis源碼分析(1)——初始化