本篇文章給大家帶來了關于redis的相關知識,其中主要介紹了關于redis源碼編譯中makefile文件的相關問題,包括了makefile文件、src/makefile文件、all目標所依賴的各個子目標的內容等相關內容,希望對大家有幫助。
推薦學習:redis
1、前言
學習本文需要有redis源碼,且最好搭建起相關的編譯環境,這樣才能直觀地看到Makefile文件的執行過程。這篇文章《C++封裝Redis操作函數》里有編譯安裝Redis的方法,讀者可以先看一下這篇文章。這里使用的源碼版本是redis-6.2.1的。
2、Makefile文件詳解
源代碼根目錄的Makefile文件內容如下:
default: all .DEFAULT: cd src && $(MAKE) $@install: cd src && $(MAKE) $@.PHONY: install
從代碼中可以看出以下幾點信息:
- 該文件的第一個目標是default,該目標沒有實際作用,依賴于all目標
- 代碼中并沒有所謂的all目標,所以當我們直接使用make時,首先會調用default目標,然后調用all目標,由于all目標不存在,所以會調用.DEFAULT目標來替代,在Makefile的執行語句中,$@代表的就是目標的意思,$(MAKE)代表的就是make,所以展開之后的代碼如下,讀者可以自行編譯一下,看看第一條輸出語句是否與我們分析的相同
cd src && make all
- install目標和前面的類似,最終也是進去src/目錄,然后調用該目錄下的Makefile文件,區別只在于此時調用的目標變成了install而已,展開后的代碼如下:
cd src && make install
- 當傳入參數是其他是,調用的都會轉到.DEFAULT去,然后去調用子目錄下的Makefile的對應的目標,以clean為例,代碼如下:
cd src && make clean
3、src/Makefile文件詳解
該文件是真正起編譯作用的文件,內容比較多,比較雜,而且為了兼容多種編譯器里面有不少分支選擇語法,我們這里只以Linux下的gcc編譯器為例去講解,其余的沒區別,就是通過判斷語句去改變某些編譯參數而已
3.1、Makefile.dep目標
Makefile在執行對應的目標之前,會先把非目標的指令給執行了,比如變量賦值、Shell語句等等,所以我們會發現,Makefile文件并不會完全按照順序去執行的
相關代碼如下:
NODEPS:=clean distclean# FINAL_CFLAGS里的各個變量原型STD=-pedantic -DREDIS_STATIC=''WARN=-Wall -W -Wno-missing-field-initializers OPTIMIZATION?=-O2 OPT=$(OPTIMIZATION)DEBUG=-g -ggdb#CFLAGS 根據條件選擇的,不重要的參數,忽略#REDIS_CFLAGS 根據條件選擇的,不重要的參數,忽略FINAL_CFLAGS=$(STD) $(WARN) $(OPT) $(DEBUG) $(CFLAGS) $(REDIS_CFLAGS)REDIS_CC=$(QUIET_CC)$(CC) $(FINAL_CFLAGS)all: $(REDIS_SERVER_NAME) $(REDIS_SENTINEL_NAME) $(REDIS_CLI_NAME) $(REDIS_BENCHMARK_NAME) $(REDIS_CHECK_RDB_NAME) $(REDIS_CHECK_AOF_NAME) @echo "" @echo "Hint: It's a good idea to run 'make test' ;)" @echo ""Makefile.dep: -$(REDIS_CC) -MM *.c > Makefile.dep 2> /dev/null || trueifeq (0, $(words $(findstring $(MAKECMDGOALS), $(NODEPS))))-include Makefile.dep endif
首先先補充以下幾點Makefile的基礎
Makefile的findstring函數的使用格式為$(findstring FIND, IN),表示在IN中查找FIND,如果查找到了就返回FIND,找不到就返回空Makefile的words函數表示統計單詞數目,例如$(words, foo bar)的返回值為”2″Makefile的MAKECMDGOALS變量表示傳入的參數(全部)Makefile的CC默認值是ccMakefile的-MM是輸出一個用于make的規則,該規則描述了源文件的依賴關系,但是不包含系統頭文件
則可以總結出以下幾點信息:
- 里面的all目標正是我們前一節說到的那個默認的編譯目標,但是我們可以自己試著去編譯一下,會發現先生成的是Makefile.dep文件,因為他先執行了最下面那個判斷語句,里面調用了Makefile.dep目標
- 由于此時MAKECMDGOALS的值為all,不在NODEPS范圍里,所以上面那個ifeq語句成立,會調用Makefile.dep目標
- REDIS_CC的值由三個變量組成,QUIET_CC是打印調試信息的,讀者可以自己去源碼看相關內容,這部分不重要,我們忽略,CC的值代表的是編譯器,FINAL_CFLAGS里面的值則是編譯的一些參數,這些值在上面的代碼中都已經摘錄出來了
- 綜上所述Makefile.dep目標的作用就是生成當前目錄下所有以.c結尾的文件的依賴關系,并寫入Makefile.dep文件中,編譯之后生成的文件內容如下所示,看起來挺亂,但是里面的內容其實將每個源文件最終生成的目標文件給列出來,并且將它需要的依賴列出來而已
acl.o: acl.c server.h fmacros.h config.h solarisfixes.h rio.h sds.h connection.h atomicvar.h ../deps/lua/src/lua.h ../deps/lua/src/luaconf.h ae.h monotonic.h dict.h mt19937-64.h adlist.h zmalloc.h anet.h ziplist.h intset.h version.h util.h latency.h sparkline.h quicklist.h rax.h redismodule.h zipmap.h sha1.h endianconv.h crc64.h stream.h listpack.h rdb.h sha256.h adlist.o: adlist.c adlist.h zmalloc.h ae.o: ae.c ae.h monotonic.h fmacros.h anet.h zmalloc.h config.h ae_epoll.c ae_epoll.o: ae_epoll.c... zipmap.o: zipmap.c zmalloc.h endianconv.h config.h zmalloc.o: zmalloc.c config.h zmalloc.h atomicvar.h
3.2、通用的生成目標文件的target
代碼如下:
.make-prerequisites: @touch $@ifneq ($(strip $(PREV_FINAL_CFLAGS)), $(strip $(FINAL_CFLAGS))).make-prerequisites: persist-settings endif ifneq ($(strip $(PREV_FINAL_LDFLAGS)), $(strip $(FINAL_LDFLAGS))).make-prerequisites: persist-settings endif %.o: %.c .make-prerequisites $(REDIS_CC) -MMD -o $@ -c $<
以下是對這部分代碼的解析:
- 這部分是通用的根據源文件生成目標文件的target,Makefile中%表示通配符,所以只要符合格式要求的都可以借助這段代碼來生成對應的目標文件
- .make-prerequisites沒啥用忽略,而REDIS_CC的值在上一小節有說明了,是用于編譯文件的指令
- gcc的-MMD參數與前面說的那個-MM是基本一致的,只不過這個會將輸出內容導入到對應的%.d文件中
- Makefile中$@表示目標,$
- 綜上,這個target的作用是依賴于一個源文件,然后根據這個源文件生成對應的目標文件,并且將依賴關系導入到對應的%.d文件中
下面是一個簡單的例子:
# 假設生成的目標文件為acl.o,則代入可得acl.o: acl.c .make-prerequisites $(REDIS_CC) -MMD -o acl.o -c acl.c # 執行完成后在該目錄下會生成一個acl.o文件和acl.d文件
3.3、all目標所依賴的各個子目標的名稱設置
PROG_SUFFIX的值默認為空,可以忽略。這里設置的六個目標名都是會被all這個目標引用的,從名字可以看出這六個目標是對應著Redis不同的功能,依次是服務、哨兵、客戶端、基礎檢測、rdf持久化以及aof持久化。
代碼如下:
REDIS_SERVER_NAME=redis-server$(PROG_SUFFIX) REDIS_SENTINEL_NAME=redis-sentinel$(PROG_SUFFIX) REDIS_CLI_NAME=redis-cli$(PROG_SUFFIX) REDIS_BENCHMARK_NAME=redis-benchmark$(PROG_SUFFIX) REDIS_CHECK_RDB_NAME=redis-check-rdb$(PROG_SUFFIX) REDIS_CHECK_AOF_NAME=redis-check-aof$(PROG_SUFFIX)
3.4、all目標所依賴的各個子目標的內容
- REDIS_LD也是一個編譯指令,和前面那個REDIS_CC有點像,只不過這個指定了另外的一些編譯參數,比如設置了某些依賴的動態庫、靜態庫的路徑,讀者有興趣的話可以去看一下代碼,看看REDIS_LD的詳細內容
- FINAL_LIBS是一系列動態庫鏈接參數,讀者有興趣可以自行去Makefile里面查看該變量的內容,限于篇幅原因這里就不展開講了
- 將QUIET_INSTALL忽略(這個是自定義打印編譯信息的),可以看出REDIS_INSTALL的值其實就是install,Linux下的install命令是用于安裝或升級軟件或備份數據的,這個命令與cp類似,但是install允許你控制目標文件的屬性,這里不作深入分析了,有興趣的讀者可以自行查閱相關的介紹install命令的文章。基本用法為:install src des,表示將src文件復制到des文件去
代碼如下:
REDIS_SERVER_OBJ=adlist.o quicklist.o ae.o anet.o dict.o server.o sds.o zmalloc.o lzf_c.o lzf_d.o pqsort.o zipmap.o sha1.o ziplist.o release.o networking.o util.o object.o db.o replication.o rdb.o t_string.o t_list.o t_set.o t_zset.o t_hash.o config.o aof.o pubsub.o multi.o debug.o sort.o intset.o syncio.o cluster.o crc16.o endianconv.o slowlog.o scripting.o bio.o rio.o rand.o memtest.o crcspeed.o crc64.o bitops.o sentinel.o notify.o setproctitle.o blocked.o hyperloglog.o latency.o sparkline.o redis-check-rdb.o redis-check-aof.o geo.o lazyfree.o module.o evict.o expire.o geohash.o geohash_helper.o childinfo.o defrag.o siphash.o rax.o t_stream.o listpack.o localtime.o lolwut.o lolwut5.o lolwut6.o acl.o gopher.o tracking.o connection.o tls.o sha256.o timeout.o setcpuaffinity.o monotonic.o mt19937-64.o REDIS_CLI_OBJ=anet.o adlist.o dict.o redis-cli.o zmalloc.o release.o ae.o crcspeed.o crc64.o siphash.o crc16.o monotonic.o cli_common.o mt19937-64.o REDIS_BENCHMARK_OBJ=ae.o anet.o redis-benchmark.o adlist.o dict.o zmalloc.o release.o crcspeed.o crc64.o siphash.o crc16.o monotonic.o cli_common.o mt19937-64.o DEP = $(REDIS_SERVER_OBJ:%.o=%.d) $(REDIS_CLI_OBJ:%.o=%.d) $(REDIS_BENCHMARK_OBJ:%.o=%.d)-include $(DEP)INSTALL=install REDIS_INSTALL=$(QUIET_INSTALL)$(INSTALL)# redis-server$(REDIS_SERVER_NAME): $(REDIS_SERVER_OBJ) $(REDIS_LD) -o $@ $^ ../deps/hiredis/libhiredis.a ../deps/lua/src/liblua.a $(FINAL_LIBS)# redis-sentinel$(REDIS_SENTINEL_NAME): $(REDIS_SERVER_NAME) $(REDIS_INSTALL) $(REDIS_SERVER_NAME) $(REDIS_SENTINEL_NAME)# redis-check-rdb$(REDIS_CHECK_RDB_NAME): $(REDIS_SERVER_NAME) $(REDIS_INSTALL) $(REDIS_SERVER_NAME) $(REDIS_CHECK_RDB_NAME)# redis-check-aof$(REDIS_CHECK_AOF_NAME): $(REDIS_SERVER_NAME) $(REDIS_INSTALL) $(REDIS_SERVER_NAME) $(REDIS_CHECK_AOF_NAME)# redis-cli$(REDIS_CLI_NAME): $(REDIS_CLI_OBJ) $(REDIS_LD) -o $@ $^ ../deps/hiredis/libhiredis.a ../deps/linenoise/linenoise.o $(FINAL_LIBS)# redis-benchmark$(REDIS_BENCHMARK_NAME): $(REDIS_BENCHMARK_OBJ) $(REDIS_LD) -o $@ $^ ../deps/hiredis/libhiredis.a ../deps/hdr_histogram/hdr_histogram.o $(FINAL_LIBS)
3.4.1、REDIS_SERVER_NAME目標
該目標依賴于REDIS_SERVER_OBJ,而REDIS_SERVER_OBJ的內容都是一些目標文件(上面代碼有給出),這些目標文件最終都會通過3.2小節介紹的那個target來生成。可以看到REDIS_SERVER_NAME這個target需要使用REDIS_SERVER_OBJ、…/deps/hiredis/libhiredis.a、…/deps/lua/src/liblua.a以及FINAL_LIBS這些來編譯鏈接生成最終的目標文件,即redis-server
3.4.2、REDIS_SENTINEL_NAME目標
可以看到REDIS_SENTINEL_NAME目標很簡單,只是簡單地使用install命令復制了REDIS_SERVER_NAME目標生成的那個文件,即redis-server,從這里可以知道哨兵服務redis-sentinel與Redis服務使用的是同一套代碼
3.4.3、REDIS_CHECK_RDB_NAME目標
和前面的如出一轍,也是簡單復制了redis-server文件到redis-check-rdb文件去
3.4.4、REDIS_CHECK_AOF_NAME目標
和前面的如出一轍,也是簡單復制了redis-server文件到redis-check-aof文件去
3.4.5、REDIS_CLI_NAME目標
這個就不是簡單復制了,而是使用和REDIS_SERVER_NAME目標相同的方法進行直接編譯的,唯一的區別是REDIS_SERVER_NAME鏈接了…/deps/lua/src/liblua.a,而REDIS_CLI_NAME鏈接的是…/deps/linenoise/linenoise.o
3.4.6、REDIS_BENCHMARK_NAME目標
這個也是使用和REDIS_SERVER_NAME目標相同的方法進行直接編譯的,唯一的區別是REDIS_SERVER_NAME鏈接了…/deps/lua/src/liblua.a,而REDIS_BENCHMARK_NAME鏈接的是…/deps/hdr_histogram/hdr_histogram.o
3.5、all目標
經過前面的介紹,all目標的作用也就一目了然了,最終會生成六個可執行文件,以及輸出相應的調試信息
代碼如下:
all: $(REDIS_SERVER_NAME) $(REDIS_SENTINEL_NAME) $(REDIS_CLI_NAME) $(REDIS_BENCHMARK_NAME) $(REDIS_CHECK_RDB_NAME) $(REDIS_CHECK_AOF_NAME) @echo "" @echo "Hint: It's a good idea to run 'make test' ;)" @echo ""
3.6、安裝和卸載Redis的目標
3.6.1、安裝Redis的目標
這里邏輯很簡單,先創建一個用于存放Redis可執行文件的文件夾(默認是/usr/local/bin),然后將REDIS_SERVER_NAME、REDIS_BENCHMARK_NAME、REDIS_CLI_NAME對應的可執行文件復制到/usr/local/bin中去,這里可以看到前面那幾個照葫蘆畫瓢的文件并沒有復制過去,而是直接通過創建軟連接的方式去生成對應的可執行文件(內容相同,復制過去浪費空間)
代碼如下:
PREFIX?=/usr/local INSTALL_BIN=$(PREFIX)/bin install: all @mkdir -p $(INSTALL_BIN) $(REDIS_INSTALL) $(REDIS_SERVER_NAME) $(INSTALL_BIN) $(REDIS_INSTALL) $(REDIS_BENCHMARK_NAME) $(INSTALL_BIN) $(REDIS_INSTALL) $(REDIS_CLI_NAME) $(INSTALL_BIN) @ln -sf $(REDIS_SERVER_NAME) $(INSTALL_BIN)/$(REDIS_CHECK_RDB_NAME) @ln -sf $(REDIS_SERVER_NAME) $(INSTALL_BIN)/$(REDIS_CHECK_AOF_NAME) @ln -sf $(REDIS_SERVER_NAME) $(INSTALL_BIN)/$(REDIS_SENTINEL_NAME)
3.6.2、卸載Redis的目標
這里就是刪除前面復制的那些文件了,比較簡單,就不細講了
代碼如下:
uninstall: rm -f $(INSTALL_BIN)/{$(REDIS_SERVER_NAME),$(REDIS_BENCHMARK_NAME),$(REDIS_CLI_NAME),$(REDIS_CHECK_RDB_NAME),$(REDIS_CHECK_AOF_NAME),$(REDIS_SENTINEL_NAME)}
3.7、clean和distclean目標
所有Makefile的clean或者distclean目標的作用都是大致相同的,就是刪除編譯過程中產生的那些中間文件,以及最終編譯生成的動態庫、靜態庫、可執行文件等等內容,代碼比較簡單,就不作過多的分析了
代碼如下:
clean: rm -rf $(REDIS_SERVER_NAME) $(REDIS_SENTINEL_NAME) $(REDIS_CLI_NAME) $(REDIS_BENCHMARK_NAME) $(REDIS_CHECK_RDB_NAME) $(REDIS_CHECK_AOF_NAME) *.o *.gcda *.gcno *.gcov redis.info lcov-html Makefile.dep dict-benchmark rm -f $(DEP).PHONY: clean distclean: clean -(cd ../deps && $(MAKE) distclean) -(rm -f .make-*).PHONY: distclean
3.8、test目標
執行完Redis編譯之后,會有一段提示文字我們可以運行make test測試功能是否正常,從代碼中我們可以看出其實不止一個test目標,還有另一個test-sentinel目標,這個是測試哨兵服務的。這兩個目標分別運行了根目錄的runtest和runtest-sentinel文件,這兩個是腳本文件,里面會繼續調用其他腳本來完成整個功能的測試,并輸出測試信息到控制臺。具體怎么測試的就不分析了,大家有興趣的可以去看一下。
代碼如下:
test: $(REDIS_SERVER_NAME) $(REDIS_CHECK_AOF_NAME) $(REDIS_CLI_NAME) $(REDIS_BENCHMARK_NAME) @(cd ..; ./runtest)test-sentinel: $(REDIS_SENTINEL_NAME) $(REDIS_CLI_NAME) @(cd ..; ./runtest-sentinel)
4、總結
本文詳細地分析了與Redis編譯相關的Makefile文件,通過學習Makefile文件里的內容,我們可以更為全面地了解Redis的編譯過程,因為Makefile文件中將很多編譯命令用@給取消顯示了,轉而使用它自己特制的編譯信息輸出給我們看,代碼如下:
ifndef V QUIET_CC = @printf ' %b %bn' $(CCCOLOR)CC$(ENDCOLOR) $(SRCCOLOR)$@$(ENDCOLOR) 1>&2; QUIET_LINK = @printf ' %b %bn' $(LINKCOLOR)LINK$(ENDCOLOR) $(BINCOLOR)$@$(ENDCOLOR) 1>&2; QUIET_INSTALL = @printf ' %b %bn' $(LINKCOLOR)INSTALL$(ENDCOLOR) $(BINCOLOR)$@$(ENDCOLOR) 1>&2; endif
所以我們直接去編譯的話很多細節會看不到,可以自己嘗試修改Makefile文件,在前面這段代碼之前定義V變量,這樣就可以看到完整的編譯信息了。修改如下:
V = 'good' ifndef V QUIET_CC = @printf ' %b %bn' $(CCCOLOR)CC$(ENDCOLOR) $(SRCCOLOR)$@$(ENDCOLOR) 1>&2; QUIET_LINK = @printf ' %b %bn' $(LINKCOLOR)LINK$(ENDCOLOR) $(BINCOLOR)$@$(ENDCOLOR) 1>&2; QUIET_INSTALL = @printf ' %b %bn' $(LINKCOLOR)INSTALL$(ENDCOLOR) $(BINCOLOR)$@$(ENDCOLOR) 1>&2; endif
本人之前也寫過Nginx編譯相關的文章,下面總結兩者的幾點區別:
- Nginx使用了大量的Shell相關的技術,而Redis則很少使用這些
- Nginx跨平臺的相關參數是通過配置腳本進行配置的,而Redis則是直接在Makefile文件中將這件事給做了,這兩者沒有什么優劣之分,Nginx主要是為了可擴展性強才使用那么多配置腳本的,而Redis基本不用考慮這些,所以簡單一點實現就行了
- 由于Redis將其一些邏輯都放在了Makefile文件中了,所以看起來Nginx最終生成的Makefile文件要比Redis簡單易懂很多(Nginx復雜邏輯在那些配置腳本里)
- Nginx生成的配置文件足有1000多行,代碼量比Redis的400多行要大很多,因為Nginx把全部依賴的生成方式全部列舉了出來,而Redis借助了Makefile.dep、各種%.d文件來將依賴信息分散到中間文件中去,極大地減少了Makefile的代碼量
推薦學習:redis