分类: LINUX
2008-03-17 15:21:00
规则的命令由一些shell命令行组成,它们被一条一条的执行。规则中除了第一条紧跟在依赖列表之后使用分号隔开的命令以外,其它的每一行命令行必须以[Tab]字符开始。多个命令行之间可以有空行和注释行(所谓空行,就是不包含任何字符的一行,如果以[Tab]键开始而其后没有命令的行,此行不是空行是空命令行),在执行规则时空行被忽略。
通常系统中可能存在多个不同的shell。但在make处理Makefile过程时,如果没有明确指定,那么对所有规则中命令行的解析使用“/bin/sh”来完成。
执行过程所使用的shell决定了规则中的命令的语法和处理机制。当使用默认的“/bin/sh”时,命令中出现的字符“#”到行末的内容被认为是注释。当然了“#”可以不在此行的行首,此时“#”之前的内容不会被作为注视处理。
另外在make解析makefile文件时,对待注释也是采用同样的处理方式。我们的shell脚本也一样。
通常,make在执行命令行之前会把要执行的命令行输出到标准输出设备,我们称之为“回显”,就好像我们在shell环境下输入命令执行时一样。
但是,如果规则的命令行以字符“@”开始,则make在执行这个命令时就不会回显这个将要被执行的命令。典型的用法是在使用“echo”命令输出一些信息时。如:
@echo 开始编译XXX模块......
执行时,将会得到“开始编译XXX模块......”这条输出信息。如果在命令行之前没有字符“@”,那么,make的输出将是:
echo编译XXX模块......
编译XXX模块......
另外,如果使用make的命令行参数“-n”或“--just-print”,那么make执行时只显示所要执行的命令,但不会真正的去执行这些命令。只有在这种情况下make才会打印出所有make需要执行的命令,其中也包括了使用“@”字符开始的命令。这个选项对于我们调试Makefile非常有用,使用这个选项我们可以按执行顺序打印出Makefile中所有需要执行的所有命令。
而make参数“-s”或“--silent”则是禁止所有执行命令的显示,就好像所有的命令行均使用“@”开始一样。在Makefile中使用没有依赖的特殊目标“.SILENT”也可以禁止命令的回显,但是它不如使用“@”来的灵活。因此在书写Makefile时,我们推荐使用“@”来控制命令的回显。
规则中,当目标需要被重建时。此规则所定义的命令将会被执行,如果是多行命令,那么每一行命令将在一个独立的子shell进程中被执行(就是说,每一行命令的执行是在一个独立的shell进程中完成)。因此,多行命令之间的执行是相互独立的,相互之间不存在依赖(多条命令行的执行为多个相互独立的进程)。
在Makefile中书写在同一行中的多个命令属于一个完整的shell命令行,书写在独立行的一条命令是一个独立的shell命令行。因此:在一个规则的命令中,命令行“cd”改变目录不会对其后的命令的执行产生影响。就是说其后的命令执行的工作目录不会是之前使用“cd”进入的那个目录。如果要实现这个目的,就不能把“cd”和其后的命令放在两行来书写。而应该把这两条命令写在一行上,用分号分隔。这样它们才是一个完整的shell命令行。如:
foo : bar/lose
cd bar; gobble lose > ../foo
如果希望把一个完整的shell命令行书写在多行上,需要使用反斜杠(\)来对处理多行的命令进行连接,表示他们是一个完整的shell命令行。例如上例我们以也可以这样书写:
foo : bar/lose
cd bar; \
gobble lose > ../foo
make对所有规则命令的解析使用环境变量“SHELL”所指定的那个程序,在GNU make中,默认的程序是“/bin/sh”。
不像其他绝大多数变量,它们的值可以直接从同名的系统环境变量那里获得。make的环境变量“SHELL”没有使用系统环境变量的定义。因为系统环境变量“SHELL”指定那个程序被用来作为用户和系统交互的接口程序,它对于不存在直接交互过程的make显然不合适。在make的环境变量中“SHELL”会被重新赋值;它作为一个变量我们也可以在Makefile中明确地给它赋值(指出解释程序的名字,当明确指定时需要使用完整的路径名。如“/bin/sh”),变量“SHELL”的默认值是“/bin/sh”。
(在MS-DOS下有些不同, MS-DOS不存在SHELL环境变量。这里不对MS-DOS下make进行介绍,有兴趣地可以自行参考info make关于此部分的描述)
GNU make支持同时执行多条命令。通常情况下,同一时刻只有一个命令在执行,下一个命令只有在当前命令执行完成之后才能够开始执行。不过可以通过make的命令行选项“-j”或者“--job”来告诉make在同一时刻可以允许多条命令同时被执行(注意,在MS-DOS中此选项无效,因为它是单任务操作系统)。
如果选项“-j”之后存在一个整数,其含义是告诉make在同一时刻可允许同时执行命令的数目。这个数字被称为“job slots”。当“-j”选项之后没有出现一个数字时,那么同一时刻执行的命令数目没有要求。使用默认的“job slots”,值为1。表示make将串行的执行规则的命令(同一时刻只能有一条命令被执行)。
并行执行命令所带来的问题是显而易见地:
1. 多个同时执的命令的输出信息将同时被输出到终端。当出现错误时很难根据一大堆凌乱的信息来区分是哪条命令执行错误。
2. 在同一时刻可能会存在多个命令执行进程同时读取标准输入,但是对于标准输入设备来说,在同一时刻只能存在一个进程访问它。就是说在某个时间点,make只能保证此刻正在执行的进程中的一个进程读取标准输入流,而其它进程的标准输入流将置无效。因此在一时刻多个执行命令的进程中只能有一个进程获得标准输入,而其它需要读取标准输入流的进程由于输入流无效而导致致命错误(通常此进程会得到操作系统的管道破裂信号而被终止)。
这是因为:执行中的命令在什么时候会读取标准输入流(终端输入或重定向的标准输入)是不可预测的。而得到标准输入的顺序总是按照先来先获得的原则。那个命令首先被执行,那么它就可以首先得到标准输入设备。而其它后续需要获取标准输入设备的命令执行进程,由于不能得到标准输入而产生致命错误。在Makefile规则中如果存在很多命令需要读取标准输入设备,而它们又被允许并行执行时,就会出现这样的错误。
为了解决这个问题。我们可以修改Makefile规则的命令使之在执行过程中避免使用标准输入。当然也可以只存在一个命令在执行时会访问标准输入流的Makefile。
3. 会导致make的递归调用出现问题。
当make在执行命令时,如果某一条命令执行失败(被一个信号中止,或非零退出),且该条命令产生的错误不可忽略,那么其它的用于重建同一目标的命令执行也将会被终止。此种情况下,如果make没有使用“-k”或“--keep-going”选项,make将停止执行而退出。另外:如果make在执行时,由某种原因(包括信号)被中止,此时它的子进程(那些执行规则命令行的shell子进程)正在运行,那么make将等到所有这些子进程结束之后才真正退出。
执行make时,如果系统运行于重负荷状态下,我们需要控制(减轻)系统在执行make时的负荷。可以使用“-l”选项告诉make限制当前运行的任务的数量(make所限制的只是它本身所需要占用的系统负载,而不能通过它去控制其它的任务所占用的系统负载)。“-l”或“--max-load”选项一般后边需要跟一个浮点数。如:
-l 2.5
它的意思是告诉make当系统平均负荷高于2.5时,不再启动任何执行命令的子任务。不带浮点数的“-l”选项用于取消前面通“-l”给定的负荷限制。
更为准确一点就是:每一次,make在启动一项任务之前(当前系统至少存在make的子任务正在运行)。首先make会检查当前系统的负荷;如果当前系统的负荷高于通过“-l”选项指定的值,那么make就不会在其他任务完成之前启动任何任务。缺省情况下没有负荷限制。
通常,规则中的命令在运行结束后,make会检测命令执行的返回状态,如果返回成功,那么就启动另外一个子shell来执行下一条命令。规则中的所有命令执行完成之后,这个规则就执行完成了。如果一个规则中的某一个命令出错(返回非0状态),make就会放弃对当前规则后续命令的执行,也有可能会终止所有规则的执行。
一些情况下,规则中一个命令的执行失败并不代表规则执行的错误。例如我们使用“mkdir”命令来确保存在一个目录。当此目录不存在使我们就建立这个目录,当目录存在时那么“mkdir”就会执行失败。其实我们并不希望mkdir在执行失败后终止规则的执行。为了忽略一些无关命令执行失败的情况,我们可以在命令之前加一个减号“-”(在[Tab]字符之后),来告诉make忽略此命令的执行失败。命令中的“-”号会在shell解析并执行此命令之前被去掉,shell所解释的只是纯粹的命令,“-”字符是由make来处理的。例如对于“clean”目标我们就可以这么写:
clean:
-rm *.o
其含义是:即使执行“rm”删除文件失败,make也继续执行。
在执行make时,如果使用命令行选项“-i”或者“—ignore-errors”, make将忽略所有规则中命令执行的错误。没有依赖的特殊目标“.IGNORE”在Makefile中有同样的效果。但是“.IGNORE”的方式已经很少使用,因为它没有在命令行之前使用“-”的方式灵活。
当使用make的“-i”选项或者使用“-”字符来忽略命令执行的错误时,make始终把命令的执行结果作为成功来对待。但会提示错误信息,同时提示这个错误被忽略。
当不使用这种方式来通知make忽略命令执行的错误时,那么在错误发生时,就意味着定义这个命令规则的目标不能被正确重建,同样,和此目标相关的其它目标也不会被正确重建。由于先决条件不能建立,那么后续的命令将不会被执行。
在发生这样情况时,通常make会立刻退出并返回一个非0状态,表示执行失败。像对待命令执行的错误一样,我们可以使用make的命令行选项“-k”或者“--keep-going”来通知make,在出现错误时不立即退出,而是继续后续命令的执行。直到无法继续执行命令时才异常退出。例如:使用“-k”参数,在重建一个.o文件目标时出现错误,make不会立即退出。虽然make已经知道因为这个错误而无法完成终极目标的重建,但还是继续完成其它后续的依赖文件的重建。直到执行最后链接时才错误退出。
一般make的“-k”参数在实际应用中,主要用途是:当同时修改了工程中的多个文件后,“-k”参数可以帮助我们确认对那些文件的修改是正确的(可以被编译),那些文件的修改是不正确的(不能正确编译)。例如我们修改了工程中的20个源文件,修改完成之后使用带“-k”参数的make,它可以一次性找出修改的20个文件中哪些是不能被编译。
通常情况下,执行失败的命令一旦改变了它所在规则的目标文件,则这个改变了的目标可能就不是一个被正确重建的文件。但是这个文件的时间戳已经被更新过了(这种情况也会发生在使用一个信号来强制中止命令执行的时候)。因此下一次执行make时,由于时间戳更新它将不会被重建,将最终导致终极目标不能被正确重建。为了避免这种错误的出现,应该在一次make执行失败之后使用“make clean”来清除已经重建的所有目标,之后再执行make。我们也可以让make自动完成这个动作,我们只需要在Makefile中定义一个特殊的目标“.DELETE_ON_ERROR”。但是这个做法存在不兼容。推荐的做法是:在make执行失败时,修改错误之后执行make之前,使用“make clean”明确的删除第一次错误重建的所有目标。
本节的最后,需要说明的是:虽然make提供了命令行选项来忽略命令执行的错误。建议对于此选项谨慎使用。因为在一个大型的工程中,可能需要对成千个源文件进行编译。编译过程中的任何一个文件编译的错误都是不能被忽略的,否则可能最后完成的终极目标是一个让你感到非常迷惑的东西,它在运行时可能会产生一些莫名奇妙的现象。这需要我们保证书写的Makefile中规则的命令在执行时不会发生错误。特别需要注意那些有特殊目的的规则中的命令。当所有命令都可以被正确执行时,我们就没有必要为了避免一些讨厌的错误而使用“-i”选项,为了实现同样的目的,我们可以使用其它的一些方式。例如删除命令可以这样写: “$(RM)”或者“rm -f”,创建目录的命令可以这样写:“mkdir –p ”等等。
make在执行命令时如果收到一个致命信号(终止make),那么make将会删除此过程中已经重建的规则的目标文件。其依据是此目标文件的当前时间戳和make开始执行时此文件的时间戳是否相同。
删除这个目标文件的目的是为了确保下一次make时目标文件能够被正确重建。其原因我们上一节已经有所讨论。假设正在编译时键入“Ctrl-c”,此时编译器已经开始写文件“foo.o”,但是“Ctrl-c”产生的信号关闭了编译器。这种情况下文件“foo.o”可能是不完整的,但这个内容不完整的“foo.o”文件的时间戳比源程序‘foo.c’的时间戳新。如果在make收到终止信号后不删除文件“foo.o”而直接退出,那么下次执行make时此文件被认为已是最新的而不会去重建它。最后在链接生成终极目标时由于某一个.o文件的不完整,可能出现一堆令人难以理解的错误信息,或者产生了一个不正确的终极目标。
相反,可以在Makefile中将一个目标文件作为特殊目标“.PRECIOUS”的依赖,来取消make在重建这个目标时,在异常终止的情况下对这个目标文件的删除动作。每一次在make重建一个目标之前,都将首先判断该目标文件是否出现在特殊目标“.PRECIOUS”的依赖列表中,决定在终止信号发生时是否要删除这个目标文件。不删除这种目标文件的原因可能是:1. 目标的重建动作是一个原子的不可被中断的过程;2. 目标文件的存在仅仅为了记录其重建时间(不关心其内容无);3. 这个目标文件必须一直存在来防止其它麻烦。