首頁 >資料庫 >Redis >Redis原始碼解析之深入了解Makefile文件

Redis原始碼解析之深入了解Makefile文件

青灯夜游
青灯夜游轉載
2022-01-27 10:26:562211瀏覽

這篇文章帶大家聊聊Redis原始碼編譯,深入詳細解析Makefile文件,希望對大家有幫助!

Redis原始碼解析之深入了解Makefile文件

學習本文需要有Redis原始碼,且最好建置起相關的編譯環境,這樣才能直觀地看到Makefile文件的執行過程。這裡使用的原始碼版本是redis-6.2.1的。 【相關推薦:Redis影片教學

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

src/Makefile檔案詳解

該檔案是真正起編譯作用的文件,內容比較多,比較雜,而且為了相容多種編譯器裡面有不少分支選擇語法,我們這裡只以Linux下的gcc編譯器為例去講解,其餘的沒區別,就是通過判斷語句去改變某些編譯參數而已

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 || true

ifeq (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

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、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)

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)

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

4.2、REDIS_SENTINEL_NAME目标

可以看到REDIS_SENTINEL_NAME目标很简单,只是简单地使用install命令复制了REDIS_SERVER_NAME目标生成的那个文件,即redis-server,从这里可以知道哨兵服务redis-sentinelRedis服务使用的是同一套代码

4.3、REDIS_CHECK_RDB_NAME目标

和前面的如出一辙,也是简单复制了redis-server文件到redis-check-rdb文件去

4.4、REDIS_CHECK_AOF_NAME目标

和前面的如出一辙,也是简单复制了redis-server文件到redis-check-aof文件去

4.5、REDIS_CLI_NAME目标

这个就不是简单复制了,而是使用和REDIS_SERVER_NAME目标相同的方法进行直接编译的,唯一的区别是REDIS_SERVER_NAME链接了…/deps/lua/src/liblua.a,而REDIS_CLI_NAME链接的是…/deps/linenoise/linenoise.o

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

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&#39;s a good idea to run &#39;make test&#39; ;)"
	@echo ""

6、安装和卸载Redis的目标

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)

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)}

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

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)

总结

本文详细地分析了与Redis编译相关的Makefile文件,通过学习Makefile文件里的内容,我们可以更为全面地了解Redis的编译过程,因为Makefile文件中将很多编译命令用@给取消显示了,转而使用它自己特制的编译信息输出给我们看,代码如下:

ifndef V
QUIET_CC = @printf &#39;    %b %b\n&#39; $(CCCOLOR)CC$(ENDCOLOR) $(SRCCOLOR)$@$(ENDCOLOR) 1>&2;
QUIET_LINK = @printf &#39;    %b %b\n&#39; $(LINKCOLOR)LINK$(ENDCOLOR) $(BINCOLOR)$@$(ENDCOLOR) 1>&2;
QUIET_INSTALL = @printf &#39;    %b %b\n&#39; $(LINKCOLOR)INSTALL$(ENDCOLOR) $(BINCOLOR)$@$(ENDCOLOR) 1>&2;
endif

所以我们直接去编译的话很多细节会看不到,可以自己尝试修改Makefile文件,在前面这段代码之前定义V变量,这样就可以看到完整的编译信息了。修改如下:

V = 'good'

ifndef V
QUIET_CC = @printf &#39;    %b %b\n&#39; $(CCCOLOR)CC$(ENDCOLOR) $(SRCCOLOR)$@$(ENDCOLOR) 1>&2;
QUIET_LINK = @printf &#39;    %b %b\n&#39; $(LINKCOLOR)LINK$(ENDCOLOR) $(BINCOLOR)$@$(ENDCOLOR) 1>&2;
QUIET_INSTALL = @printf &#39;    %b %b\n&#39; $(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的代码量

本文转载自:https://blog.csdn.net/weixin_43798887/article/details/117674538

更多编程相关知识,请访问:编程入门!!

以上是Redis原始碼解析之深入了解Makefile文件的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:csdn.net。如有侵權,請聯絡admin@php.cn刪除