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