linux 的世界里,最好用的調(diào)試工具不是 gdb,而是日志和 printf。日志在一個(gè)軟件系統(tǒng)中是非常常見(jiàn)的,一個(gè)關(guān)鍵的作用即定位錯(cuò)誤,當(dāng)系統(tǒng)出問(wèn)題首先想到就是日志,查看日志能快速定位問(wèn)題。Redis 中的日志模塊較為簡(jiǎn)單。我們?cè)?Redis 源碼中,到處都可以見(jiàn)到 redisLog()。
通常,日志會(huì)分為幾個(gè)級(jí)別。在 Redis 中 5 個(gè)日志級(jí)別,在 redis.h 文件中有定義:
/* Log levels */
#define REDIS_DEBUG 0 // 調(diào)試級(jí)別,這一級(jí)別產(chǎn)生最多的日志信息
#define REDIS_VERBOSE 1
#define REDIS_NOTICE 2
#define REDIS_WARNING 3
#define REDIS_LOG_RAW (1<<10) /* Modifier to log without timestamp */
#define REDIS_DEFAULT_VERBOSITY REDIS_NOTICE
服務(wù)器的配置結(jié)構(gòu)體中,struct redisServer.verbosity 是用來(lái)設(shè)定日志級(jí)別的,譬如將日志級(jí)別設(shè)定為REDIS_NOTICE 后,代碼中 REDIS_VERBOSE 和REDIS_DEBUG 級(jí)別的日志都不會(huì)被打印。
日志級(jí)別值越是低,日志級(jí)別越高,產(chǎn)生了日志也就越多,開(kāi)發(fā)人員在產(chǎn)品上線之前會(huì)將日志級(jí)別調(diào)至最低,方便發(fā)現(xiàn)定位或發(fā)現(xiàn)潛在的問(wèn)題。而上線之后,可以將志級(jí)別降低,減少調(diào)試日志。如果日志級(jí)別過(guò)高,則日志量大,可能會(huì)對(duì)線上的服務(wù)產(chǎn)生影響,因?yàn)閷?xiě)日志就是寫(xiě)文件操作,系統(tǒng)調(diào)用是要消耗時(shí)間的。
日志是想要記錄某一個(gè)時(shí)間點(diǎn),在哪里發(fā)送了什么事情,以方便出現(xiàn)問(wèn)題的時(shí)候,恢復(fù)現(xiàn)場(chǎng),快速定位問(wèn)題所在。“某一時(shí)間點(diǎn)”即添加時(shí)間戳;“在哪里”即程序執(zhí)行的位置,對(duì)應(yīng)的是源碼的文件,行號(hào)函數(shù)等;“發(fā)生了什么事情”即記錄一些關(guān)鍵數(shù)據(jù)。
// redis 日志函數(shù),會(huì)將給定的數(shù)據(jù)寫(xiě)入日志文件,和常用的printf 函數(shù)用法差不多
void redisLog(int level, const char *fmt, ...) {
va_list ap;
char msg[REDIS_MAX_LOGMSG_LEN];
// 如果日志級(jí)別小于預(yù)設(shè)的日志級(jí)別,直接返回
if ((level&0xff) < server.verbosity) return;
va_start(ap, fmt);
vsnprintf(msg, sizeof(msg), fmt, ap);
va_end(ap);
// redisLogRaw() 函數(shù)將給定的信息,在增加時(shí)間戳和進(jìn)程id 后寫(xiě)入日志文件
redisLogRaw(level,msg);
}
為什么需要斷言???????> TODO 當(dāng)你認(rèn)為某些事情在正常情況下不可能出現(xiàn),應(yīng)盡可能結(jié)束任務(wù),而不是捕捉錯(cuò)誤,嘗試挽救。同樣在西加加里,使用 try…catch() 會(huì)讓程序的邏輯變亂,甚至讓程序的行為變得不可預(yù)測(cè),大膽的使用斷言吧。
Redis 中不僅僅實(shí)現(xiàn)了斷言,且在斷言失敗的時(shí)候會(huì)打印一些關(guān)鍵的信息。
在 Redis.h 中定義了兩個(gè)斷言相關(guān)的宏:
#define redisAssertWithInfo(_c,_o,_e) \
((_e)?(void)0 : \
(_redisAssertWithInfo(_c,_o,#_e,__FILE__,__LINE__),_exit(1)))
#define redisAssert(_e) \
((_e)?(void)0 : \
(_redisAssert(#_e,__FILE__,__LINE__),_exit(1)))
如果斷言為真,執(zhí)行一個(gè)空操作;斷言為假,會(huì)打印關(guān)鍵的信息。
_redisAssert() 函數(shù)會(huì)記錄斷言發(fā)生的錯(cuò)誤信息,文件名和行號(hào)
void _redisAssert(char *estr, char *file, int line) {
// 向日志文件中寫(xiě)入BUG 頭部
bugReportStart();
// 將文件名,行號(hào),錯(cuò)誤信息寫(xiě)入日志
redisLog(REDIS_WARNING,"=== ASSERTION FAILED ===");
redisLog(REDIS_WARNING,"==> %s:%d '%s' is not true",file,line,estr);
// 如果需要,可以記錄錯(cuò)誤信息,文件名和行號(hào),以便在進(jìn)程崩潰后調(diào)試(gdb core?)
#ifdef HAVE_BACKTRACE
server.assert_failed = estr;
server.assert_file = file;
server.assert_line = line;
redisLog(REDIS_WARNING,"(forcing SIGSEGV to print the bug report.)");
#endif
// 強(qiáng)制segmentation fault。無(wú)效的內(nèi)存訪問(wèn),可以產(chǎn)生SIGSEGV,如此會(huì)
// 產(chǎn)生coredump 文件以供進(jìn)程崩潰后調(diào)試使用
*((char*)-1) = 'x';
}
這有個(gè)小有意思的語(yǔ)句:*((char*)-1) = ’x’;(char *)-1表示指向地址值為 -1 的指針,它所指向的內(nèi)存肯定是非法的,對(duì)非法內(nèi)存的操作會(huì)觸發(fā) SIGSEGV 信號(hào),進(jìn)程結(jié)束后會(huì)產(chǎn)生 coredump 文件,方便調(diào)試使用。使用gdb、可執(zhí)行文件和 coredump 文件能快速定位問(wèn)題所在,即使進(jìn)程已經(jīng)崩潰了。
_redisAssertWithInfo() 函數(shù)會(huì)打印 Redis 服務(wù)器當(dāng)前服務(wù)的客戶端和某個(gè)關(guān)鍵 Redis 對(duì)象的信息,具體請(qǐng)參看源碼,在這不展開(kāi)了。