推荐学习: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-initializersoptimization?=-o2opt=$(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.dependif
首先先补充以下几点makefile的基础
makefile的findstring函数的使用格式为$(findstring find, in),表示在in中查找find,如果查找到了就返回find,找不到就返回空makefile的words函数表示统计单词数目,例如$(words, foo bar)的返回值为2makefile的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.hadlist.o: adlist.c adlist.h zmalloc.hae.o: ae.c ae.h monotonic.h fmacros.h anet.h zmalloc.h config.h \ ae_epoll.cae_epoll.o: ae_epoll.c...zipmap.o: zipmap.c zmalloc.h endianconv.h config.hzmalloc.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-settingsendififneq ($(strip $(prev_final_ldflags)), $(strip $(final_ldflags))).make-prerequisites: persist-settingsendif%.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.oredis_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.oredis_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.odep = $(redis_server_obj:%.o=%.d) $(redis_cli_obj:%.o=%.d) $(redis_benchmark_obj:%.o=%.d)-include $(dep)install=installredis_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/localinstall_bin=$(prefix)/bininstall: 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: cleandistclean: 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 vquiet_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 vquiet_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学习教程
以上就是redis经典技巧之makefile文件详解的详细内容。