Redis經典技巧之Makefile檔案詳解

2022-03-10 19:00:46
本篇文章給大家帶來了關於的相關知識,其中主要介紹了關於Redis原始碼編譯中Makefile檔案的相關問題,包括了Makefile檔案、src/Makefile檔案、all目標所依賴的各個子目標的內容等相關內容,希望對大家有幫助。

推薦學習:

1、前言

學習本文需要有Redis原始碼,且最好搭建起相關的編譯環境,這樣才能直觀地看到Makefile檔案的執行過程。這篇文章《C++封裝Redis操作函數》裡有編譯安裝Redis的方法,讀者可以先看一下這篇文章。這裡使用的原始碼版本是redis-6.2.1的。

2、Makefile檔案詳解

原始碼根目錄的Makefile檔案內容如下:

default: all

.DEFAULT:	cd src && $(MAKE) $@install:	cd src && $(MAKE) [email protected]: 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的基礎

  • Makefilefindstring函數的使用格式為$(findstring FIND, IN),表示在IN中查詢FIND,如果查詢到了就返回FIND,找不到就返回空
  • Makefilewords函數表示統計單詞數目,例如$(words, foo bar)的返回值為"2"
  • MakefileMAKECMDGOALS變數表示傳入的引數(全部)
  • MakefileCC預設值是cc
  • Makefile-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 $<

以下是對這部分程式碼的解析:

  • 這部分是通用的根據原始檔生成目標檔案的targetMakefile%表示萬用字元,所以只要符合格式要求的都可以藉助這段程式碼來生成對應的目標檔案
  • .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的值其實就是installLinux下的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-sentinelRedis服務使用的是同一套程式碼

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_NAMEREDIS_BENCHMARK_NAMEREDIS_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目標

所有Makefileclean或者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目標,這個是測試哨兵服務的。這兩個目標分別執行了根目錄的runtestruntest-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 %b\n' $(CCCOLOR)CC$(ENDCOLOR) $(SRCCOLOR)$@$(ENDCOLOR) 1>&2;
QUIET_LINK = @printf '    %b %b\n' $(LINKCOLOR)LINK$(ENDCOLOR) $(BINCOLOR)$@$(ENDCOLOR) 1>&2;
QUIET_INSTALL = @printf '    %b %b\n' $(LINKCOLOR)INSTALL$(ENDCOLOR) $(BINCOLOR)$@$(ENDCOLOR) 1>&2;
endif

所以我們直接去編譯的話很多細節會看不到,可以自己嘗試修改Makefile檔案,在前面這段程式碼之前定義V變數,這樣就可以看到完整的編譯資訊了。修改如下:

V = 'good'

ifndef V
QUIET_CC = @printf '    %b %b\n' $(CCCOLOR)CC$(ENDCOLOR) $(SRCCOLOR)$@$(ENDCOLOR) 1>&2;
QUIET_LINK = @printf '    %b %b\n' $(LINKCOLOR)LINK$(ENDCOLOR) $(BINCOLOR)$@$(ENDCOLOR) 1>&2;
QUIET_INSTALL = @printf '    %b %b\n' $(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經典技巧之Makefile檔案詳解的詳細內容,更多請關注TW511.COM其它相關文章!