Redis中如何深入了解Makefile文件

70次阅读
没有评论

共计 10641 个字符,预计需要花费 27 分钟才能阅读完成。

这篇文章的内容主要围绕 Redis 中如何深入了解 Makefile 文件进行讲述,文章内容清晰易懂,条理清晰,非常适合新手学习,值得大家去阅读。感兴趣的朋友可以跟随丸趣 TV 小编一起阅读吧。希望大家通过这篇文章有所收获!

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 的基础

Makefile 的 findstring 函数的使用格式为 $(findstring FIND, IN),表示在 IN 中查找 FIND,如果查找到了就返回 FIND,找不到就返回空

Makefile 的 words 函数表示统计单词数目,例如 $(words, foo bar) 的返回值为 2

Makefile 的 MAKECMDGOALS 变量表示传入的参数(全部)

Makefile 的 CC 默认值是 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 $

以下是对这部分代码的解析:

这部分是通用的根据源文件生成目标文件的 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、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 的值其实就是 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)

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-sentinel 与 Redis 服务使用的是同一套代码

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

6、安装和卸载 Redis 的目标

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)

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 目标

所有 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

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)

总结

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

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

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

V =  good 
ifndef V
QUIET_CC = @printf   %b %b\n  $(CCCOLOR)CC$(ENDCOLOR) $(SRCCOLOR)$@$(ENDCOLOR) 1 
QUIET_LINK = @printf   %b %b\n  $(LINKCOLOR)LINK$(ENDCOLOR) $(BINCOLOR)$@$(ENDCOLOR) 1 
QUIET_INSTALL = @printf   %b %b\n  $(LINKCOLOR)INSTALL$(ENDCOLOR) $(BINCOLOR)$@$(ENDCOLOR) 1 
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 文件”这一问题有一定的了解,快去动手实践吧,如果想了解更多相关知识点,可以关注丸趣 TV 网站!丸趣 TV 小编会继续为大家带来更好的文章!

正文完
 
丸趣
版权声明:本站原创文章,由 丸趣 2023-07-15发表,共计10641字。
转载说明:除特殊说明外本站除技术相关以外文章皆由网络搜集发布,转载请注明出处。
评论(没有评论)