知识点
本章节将介绍make
对规则命令的执行,命令执行过程中的错误处理以及命令包的使用。
make
对规则命令的执行make
的多线程执行make
的错误忽略选项make
的异常结束
本课程项目完成过程中将学习:
- $(SHELL) 执行规则命令
- -j 选项进行多线程执行
- -、-i、-k 参数的作用
- make 异常结束
- define
本章节的源代码位于 /home/shiyanlou/Code/make_example/chapter7/
目录下。
项目结构
项目文件结构:
├── cancel # make 的异常结束处理
│ └── makefile
├── error # make 命令的错误处理
│ ├── iopt.mk
│ ├── kopt.mk
│ └── makefile
├── joption # make 的并行使用
│ └── makefile
├── pack # 命令包使用测试
│ └── makefile
└── shell_vari # 验证`$(SHELL)`环境变量的传递
└── shell.mk
1️⃣ make 对规则命令的执行
SHELL
环境变量的传递
make 使用环境变量 SHELL
指定的 shell (命令解释器) 来处理规则命令行,GNU make 中默认的 shell 是 /bin/sh
,与其它环境变量不同的是,SHELL
变量会由 GNU make 自行定义,而不会使用当前系统的同名变量。这样做的理由是:make 认为系统的 SHELL
变量适用于定义人机交互接口,make 没有交互过程,因此不适用。
shell_vari
目录下的 shell.mk
文件演示了系统环境变量 SHELL
和 make 使用的环境变量的差异, 文件内容如下:
#this is a makefile for $(SHELL) test
.PHONY:all
all:
@echo "\$$SHELL environment is $$SHELL"
@echo "\$$SHELL in makefile is " $(SHELL)
由于符号$
是变量引用的起始字符,因此要使用$
本身这个字符时需要使用$$
进行转义。 all
规则的第一条指令是打印系统环境变量SHELL
,$$
代表$
字符,所以它的执行结果与下面这条命令的执行结果是相同的。
echo "\$SHELL environment is $SHELL"
makefile 中 @echo 与 echo 区别:echo 会在终端中先输出 echo 命令本身,再输出结果,而 @echo 是直接输出结果。详见 Difference between echo and @echo in unix shells
「\」符号是终端下的转义字符,「SHELL会被系统环境变量
SHELL的内容代替,而
SHELL」。all
规则的第二条指令是打印当前 make 使用的 SHELL
变量。
输入下面的命令进入 shell_vari
目录,并查看当前系统 SHELL
变量。
cd shell_vari/;echo $SHELL
Terminal 的输出结果如下图:
执行下面的命令查看,make 在执行 makefile 规则时所使用到的 shell。
make -f shell.mk
Terminal 的输出结果如下图:
从输出结果上看到 make 在执行 makefile 的时候所使用到的 shell 程序是不同于系统默认的 shell 程序。
SHELL
变量传参
接下来测试实验在执行 make 时,传入值为 abc 的 SHELL 变量。
make -f shell.mk SHELL=abc
Terminal 的输出结果如图所示:
可见make
尝试用我们传入的abc
来执行规则结果因为找不到abc
导致执行失败。
这说明
2️⃣ make 的多线程执行
make 也可以使用多线程进行并发执行,使用方法为执行 make 时加入命令行选项 -jN
,其中 N 为一个数字,表示要执行的线程数。而 make 的每个线程会执行一个规则的重建,每条规则只由一个线程执行。当不使用 -j
选项时 make 的执行为单线程编译。
通过 chapter7/joption
文件夹中的 makefile 文件可对 make 的多线程进行验证,makefile 文件的内容如下:
#this is a makefile for -j option
.PHONY:all
all:aim1 aim2 aim3 aim4
@echo "build final finish!"
aim%:
@echo "now build " $@
@sleep 2
@echo "build " $@ " finish!"
从内容上看最终目标 all
有 aim1
到 aim4
四个依赖项,每个依赖项的规则一致,打印信息并睡眠两秒。进入 joption
目录,并执行 make 命令,完成编译需要 8 秒。
运行截图如下:
单线程执行
为了能更直观的看到时间变化过程我们可以修改 makefile 的内容,我们可以为每一个输出过程添加一个时间戳,修改后的内容如下:
#this is a makefile for -j option
.PHONY:all
all:aim1 aim2 aim3 aim4
@echo "build final finish!"
aim%:
@echo "now build $$(date +%T)" $@
@sleep 2
@echo "build " $@ " finish! $$(date +%T)"
先执行 make 命令查看在单线程下编译的情况,Terminal 的输出如下图:
从输出结果可以看出,从开始编译 aim1
到 aim4
编译结束,总共耗时 8 秒。
多线程执行
接下来,通过添加 -j2
选项查看在多线程下的执行情况。
make -j2
Terminal 的输出结果如图:
可以看出 从开始编译 aim1
到 aim4
编译完成总共耗时 4 秒比单线程所消耗的时间减少了一半。
大家可以自己再测试一下三线程和四线程的并行执行过程,并尝试在目标中加入依赖项来限制并行编译的顺序。
然后总结出来的3线程顺序:
#this is a makefile for -j option
.PHONY:all
all:all2 all4 all1 all3
@echo "build final finish!"
all2:aim1
all4:aim2
all1:aim3
all3:aim4
aim%:
@echo "now build $$(date +%T)" $@
@sleep 2
@echo "build " $@ " finish! $$(date +%T)"
总结出来的四线程顺序: 想不出好办法了,用的依赖
#this is a makefile for -j option
.PHONY:all
all:aim1 aim2 aim3 aim4
@echo "build final finish!"
aim4:aim1 aim2 aim3
aim3:aim1 aim2
aim2:aim1
aim%:
@echo "now build $$(date +%T)" $@
@sleep 2
@echo "build " $@ " finish! $$(date +%T)"
3️⃣ make 的错误忽略选项
make
执行过程出错的简单测试
下面我们来看一下 make 执行出错的状况。 在 chapter7/error/
目录下的 makefile
文件对 rm
命令的不同执行状况进行了描述,其内容如下:
#this is a makefile for error handle test
.PHONY:all
all:pre_a pre_b pre_c
$(RM) pre_a
$(RM) pre_b
$(RM) pre_c
$(RM) d
-rm e
rm f
rm g
pre_%:
touch $@
从内容上看前面三条指令是删除生成的文件,后面四条指令则是删除不存在的文件,而在 shell 使用 rm
会直接运行失败。
现在进入到 error
文件夹下并执行此 makefile 并观察执行状况:
cd ../error/;make
Terminal 的输出结果如图:
从输出结果上进行分析 make 在运行规则命令结束后会检测命令执行的返回状态,返回成功则启动另外一个子 shell 来执行下一条命令。在 makefile 执行过程中,先生成 pre_a
、pre_b
、pre_c
三个文件,再使用 rm -f
或者 rm
命令来删除它们,这个过程是没有问题的。
然而第四条指令是删除不存在的文件 d
,由于使用了 -f
参数,因此 shell 也不会返回错误。
第五条指令是删除不存在的文件 e
,由于命令行起始处使用了符号「-」,所以 make 会忽略此命令的执行错误,所以 shell 虽然返回并打印错误,但 make 继续往下执行。
第六条指令是删除不存在的文件 f
,由于只使用了 rm
命令,shell 返回错误,make 收到错误后不再往下执行,因此第七条指令已经没机会执行到。
在某些状况下,用户希望 make 遇上错误可以继续往下执行。在多人维护的庞大工程中,makefile 文件随时可能出现错误,这时用户希望它能继续执行下去方便测试自己的模块,而不是被其他人的错误阻塞住。此时可以使用 -i
选项,-i
选项会让 make 忽略所有的错误。
通过提供的 iopt.mk
文件可以对 -i
选项的用法进行演示,iopt.mk
文件内容如下:
#this is a makefile for error handle test
.PHONY:all
all:
rm a
rm b
rm c
rm d
make 会直接调用 rm
命令删除四个不存在的文件,因此每一条指令都会返回错误。
现在执行 make 命令先并观察不使用 -i
选项时文件执行的结果。
make -f iopt.mk
Terminal 的输出结果如下图:
接下来执行加上 -i
选项再执行 iopt.mk
文件。
make -f iopt.mk -i
Terminal 的输出结果如下图:
可以看到虽然前一条指令执行时发生了错误,但是后面的指令依然得到了执行,并没有就此终止执行。
提供的 kopt.mk
文件可以用来对依赖文件错误的状况进行验证,其内容如下:
#this is a makefile for error handle test
.PHONY:all
all: h i j
@echo "exe OK!"
执行 make -f kopt.mk -i
时 Terminal 的输出结果如下图:
执行 make -f kopt.mk
时 Terminal 的输出结果如下图:
对比两种情况的输出结果,说明在依赖项错误中 -i
选项没有任何作用。
对于这种情况可以使用 -k
选项让其忽略依赖项错误并继续执行,现执行 make 命令。
make -f kopt.mk -k
Terminal 的输出结果如图所示:
-k
选项可以让make
继续检查其它依赖项,但并不会执行终极目标的指令。 若有多个依赖项被修改过后,可以使用此选项测试哪些依赖项的修改有问题。
请谨慎使用-i
和-k
选项,以免产生预期外的错误。
4️⃣ make 的异常结束
make 若收到致命信号被终止时,它会删除此过程中已经重建的目标文件,以免目标文件出现预期外的错误。例如某个目标规则需要对目标文件进行多次处理,处理到一半时 make 被终止,导致目标文件处于异常状态, 此时 make 会删除此文件以免产生难以察觉的问题。
chapter7/cancel/
目录下的 makefile 文件用于对 make 异常结束的状况的验证,内容如下:
#this is a makefile for cancel handle
.PHONY:all clean
all:clean pre_a pre_b pre_c
sleep 1
@echo "exe target all!"
clean:
$(RM) pre_*
pre_%:
@echo "\n"
touch $@
@echo "generate " $@
@ls -l $@
@echo "sleep 5s before finish..."
sleep 5
最终目标 all
依赖于 pre_a
、pre_b
、pre_c
文件,这三个文件在建立过程中会 sleep 五秒钟,方便用户结束 make 命令。
现在进入到 cancel
文件夹中执行 make 命令不作其他任何操作并观察输出结果。
cd ../cancel;make
Terminal 的输出结果如下图:
此时通过 ls
命令查看当前目录下新文件的产生情况。
可见 pre_a
、pre_b
、pre_c
三个文件被被成功的生成。
手动模拟 make 执行过程中异常产生
接下来手动模拟 make 执行过程中异常的产生,在 pre_b
文件被生成的过程中,手动键入 ctrl + c
来终止程序的继续执行。
现在通过 ls
命令查看当目录中文件的情况。
从输出结果可以看出,pre_b
已经被删除,从上一张 make 的执行过程的图片中也可以看出在异常中断的产生过程中 pre_b
文件就被删除了。
5️⃣ 命令包的使用(c函数)
书写makefile
时,可能有多个规则会使用一组相同的命令,就像c
语言要调用函数一样, 我们可以使用define
来完成这项功能。
define
以“define”
开头,以“endef”
结束,作用与c
语言的宏定义类似。
chapter7/pack/
目录下演示了命令包的使用方法,其内容如下:
#this is a makefile for define test
.PHONY:all
define echo-target
@echo "now rebuilding target : " $@
touch $@
endef
all:pre_a pre_b pre_c
@echo "final target finish!"
pre_%:
$(echo-target)
从文件内容上看最终目标 all
的依赖项都会调用同一组命令包。进入 pack
目录并测试执行效果:
cd ../pack;make
Terminal 的输出结果如图所示:
可以看出在 all
的每个依赖的命令部分都执行了 define
定义的命令包。
本章节测试了 make 对规则命令的执行,命令执行过程中的错误处理以及命令包的使用方式。