Linux基础——Makefile编写优化(一)

简介: 本文主要是为了讲述如何能够编写更专业的Makefile,而不是仅仅通过一项简单的依赖规则生成几个固定目标。(重点参考李云老师编写的《专业嵌入式软件开发——全名走向高质量高效编程》一书,该书是我在图书馆偶然看到,发现原来长期一直使用的Makefile中居然还有这么多需要注意的细节,所以特地整理记录下来。)

本文主要是为了讲述如何能够编写更专业的Makefile,而不是仅仅通过一项简单的依赖规则生成几个固定目标。(重点参考李云老师编写的《专业嵌入式软件开发——全名走向高质量高效编程》一书,该书是我在图书馆偶然看到,发现原来长期一直使用的Makefile中居然还有这么多需要注意的细节,所以特地整理记录下来。)

文章目录

Makefile,开发环境全能管家

基本规则

假目标的用处

运用“变量”提高Makefile可维护性

自动变量

特殊变量

变量的类型与赋值

变量及其值的来源

避免变量被覆盖的方法

借助“模式”精简规则

通过“函数”增强功能

abspath函数

addprefix函数

addsuffix函数

eval函数

filter函数

filter-out函数

notdir函数

patsubst函数

strip函数

wildcard函数

提高编译环境的实用性

让编译环境更加有序

目录的自动创建与删除

通过目录管理文件

提升依赖关系管理

自动生成文件依赖关系

使用依赖关系文件

为依赖关系文件建立依赖关系

打造更专业的编译环境

规划项目目录结构

增进复用性

支持头文件目录的指定

实现库链接

增加一个bar模块

增强可使用性

管理对库的依赖关系

总结


Makefile,开发环境全能管家

Linux 环境下的程序员如果不会使用GNU make来构建和管理自己的工程,应该不能算是一个合格的专业程序员,至少不能称得上是 Unix程序员。在 Linux(unix )环境下使用GNU 的make工具能够比较容易的构建一个属于你自己的工程,整个工程的编译只需要一个命令就可以完成编译、连接以至于最后的执行。不过这需要我们投入一些时间去完成一个或者多个称之为Makefile 文件的编写。

所要完成的Makefile 文件描述了整个工程的编译、连接等规则。其中包括:工程中的哪些源文件需要编译以及如何编译、需要创建哪些库文件以及如何创建这些库文件、如何最后产生我们想要的可执行文件。尽管看起来可能是很复杂的事情,但是为工程编写Makefile 的好处是能够使用一行命令来完成“自动化编译”,一旦提供一个(通常对于一个工程来说会是多个)正确的 Makefile。编译整个工程你所要做的事就是在shell 提示符下输入make命令。整个工程完全自动编译,极大提高了效率。

make是一个命令工具,它解释Makefile 中的指令。在Makefile文件中描述了整个工程所有文件的编译顺序、编译规则。Makefile 有自己的书写格式、关键字、函数。像C 语言有自己的格式、关键字和函数一样。而且在Makefile 中可以使用系统shell所提供的任何命令来完成想要的工作。Makefile在绝大多数的IDE 开发环境中都在使用,已经成为一种工程的编译方法。


基本规则

基本规则

让我们先来粗略地看一看Makefile的规则。


target ... : prerequisites ...
  command
  ...
  ...


以上是Makefile的基本规则,解释如下:

目标:依赖

执行指令 …

target也就是一个目标文件,可以是Object File,也可以是执行文件。还可以是一个标签(Label)。

① prerequisites就是,要生成那个target所需要的文件或是目标。

② command也就是make需要执行的命令。(任意的Shell命令)

这里关于最基本的Makefile就不详细解释了,执行make命令后Makefile会首先生成描述的第一个目标,查看依赖是否成立,如果依赖不存在再去依次生成依赖。。。。最终成功生成第一个目标。


假目标的用处

假目标采用.PHONY关键字来定义,注意它必须是大写字母,通常最基本会将clean修改为假目标,这样避免了当前文件下存在“clean”同名的文件导致make执行出现偏差。如下:


.PHONY: clean
simple: main.o foo.o
  gcc -o simple main.o foo.o
main.o : main.c
  gcc -o main.o -c main.c
foo.o : foo.c
  gcc -o foo.o -c foo.c
clean:
  rm -rf simple main.o foo.o


  • 由于假目标并不会与文件相关联,所以每次构建假目标时它所在的规则中的命令都一定会被执行,也就是上面每次执行“make clean”都会执行文件清除操作。

运用“变量”提高Makefile可维护性

编写专业的Makefile同样离不开运用变量,通过使用变量可以使得Makefile更具有可维护性。如下:


.PHONY:clean
CC = gcc
RM = rm
EXE = simple
OBJS = main.o foo.o
$(EXE) : $(OBJS)
  $(CC) -o $(EXE) $(OBJS)
main.o : main.c
  $(CC) -o main.o -c main.c
foo.o : foo.c
  $(CC) -o foo.o -c foo.c
clean:
  $(RM) -rf $(EXE) $(OBJS)

20200102080545715.png

  • 这里,我们定义了CC,RM,EXE,OBJS四个变量,定义变量时其值可以为空(即无右值),引用变量需要采用“( 变 量 名 ) ” 或 者 “ (变量名)”或者“(变量名)”或者“{变量名}”的形式。
  • 引用变量的好处很明显,比如进入CC变量以后,如果需要修改编译器,只需要修改赋值变量这一处即可。Makefile中变量的数据类型可以理解为C语言中的字符串。

自动变量

在上面的Makefile中,存在目标名和先决条件在规则的命令中重复出现。如果目标名或者先决条件名发生改变,那得在相应的命令中都去修改,这很麻烦,为了省去这种麻烦,我们可以借助以下自动变量。


  • $@:用于表示一个规则中的目标,当一个规则中有多个目标时,指其中任何造成规则命令被运行的目标。
  • $^:表示的是规则中的所有先决条件。
  • $<:表示的是规则中的第一个先决条件。
.PHONY : all
all: first second third
  @echo "\$$@ = $@"
  @echo "$$^ = $^"
  @echo "$$< = $<"
first second third:

2020010208041738.png

除了这三个自动变量外,在Makefile中还可以使用其他的自动变量,后面我们会说到,以上三个自动变量是最常用的。使用上述自动变量来简化之前的Makefile:


.PHONY:clean
CC = gcc
RM = rm
EXE = simple
OBJS = main.o foo.o
$(EXE) : $(OBJS)
  $(CC) -o $@ $^
main.o : main.c
  $(CC) -o $@ -c $^
foo.o : foo.c
  $(CC) -o $@ -c $^
clean:
  $(RM) -rf $(EXE) $(OBJS)


特殊变量

在Makefile中,有两个特殊变量会经常用到:MAKE和MAKECMDGOALS。

MAKE:表示当前处理Makefile的命令名是什么。

MAKECMDGOALS:表示是是当前构建的目标名。

如下:


.PHONY = all clean
all clean:
  echo "MAKE = $(MAKE)"
  echo "MAKECMDGOALS = $(MAKECMDGOALS)"

20200104074240263.png

  • 从结果上看,MAKECMDGOALS变量指的是用户输入的目标,当只允许make命令不带参数时,根据Makefile的语法规则会将Makefile中的第一个目标作为默认目标,即上面的all目标,但是MAKECMDGOALS却仍是空而不是“all”。
  • 另外,从结果中能看到,运行make时可以同时指定多个目标,make在获得了多个目标后,将从左往右依次地构建目标。

变量的类型与赋值

变量的类型有递归扩展变量和简单扩展变量。

最简单的方式是使用“=”进行变量的定义和赋值,这种只用一个“=”符号定义的变量被称为递归扩展变量。如下:


  • 递归扩展变量
.PHONY = all
foo = $(bar)
bar = $(ugh)
ugh = HUH?
all:
  @echo $(foo)

20200104074919719.png

这种递归扩展变量最需要注意的一点就是要防止对变量进行循环扩展,容易造成一个死循环。


  • 简单扩展变量

简单扩展变量是用 “ := ”操作符来定义的。这种变量,make只会对其进行一次展开,如下示例:

.PHONY = all
x = foo
y = $(x) b
x = later
xx := foo
yy := $(xx) b
xx := later
all:
  @echo "x = $(y), xx = $(yy)"

20200104075439247.png

  • 另外,在Makefile中还可以实现条件赋值,“?=”操作符实现,当变量没有被定义时就定义它,并且将右边的值赋值给它;如果变量已经定义了,则不改变其原值。条件赋值操作可以用于为变量赋值默认值。
.PHONY = all
foo = x
foo ?= y
bar ?=y
all:
  @echo "foo=$(foo), bar=$(bar)"

2020010408003151.png

  • 还有一个非常有用的赋值方法是通过“+=”实现追加赋值。
.PHONY:all
objects = main.o foo.o bar.o utils.o
objects += another.o
all:
  @echo $(objects)

20200104080303344.png

变量及其值的来源

从前面的示例可以看出,在Makefile中可以对变量进行定义。此外,还有其他方式使得make获得变量。比如:


  • 对于自动变量,其值是在每一个规则中根据规则的上下文自动获得的。
  • 在运行make时,通过命令参数定义变量。例如下面的Makefile,如果使用“make bar=x”来运行它,得到结果就不一样了。
.PHONY : all
foo = x
foo ?= y
bar ?= y
all:
  @echo "foo = $(foo), bar = $(bar)"


  • 变量还可以来自shell环境,如下:20200119104657226.png

### 高级变量引用功能

如下的Makefile说明了变量引用的一种高级功能,即在赋值的同时完成文件名后缀替换操作。

.PHONY = all
foo = a.c b.c c.c
bar := $(foo:.c=.o)
all:
  @echo "bar = $(bar)"

2020011910515457.png

从截图中的运行结果来看,bar变量中的文件名由.c后缀变成了.o。与使用函数相比,这种方式根据简洁。当然,这种功能也可以采用后面将要介绍的patsubst函数来实现。


避免变量被覆盖的方法


上面介绍了make命令行上定义变量的方式能够使得Makefile文件中定义的变量值被覆盖。但是如果在设计Makefile时不希望发生这种覆盖现象,则需要使用override指令进行预防。具体如下:


.PHONY: all
override foo = x
all:
  @echo "foo = $(foo)"


借助“模式”精简规则


对于之前使用到的Makefile,其中存在多个规则用于构建目标文件。比如,main.o和foo.o,都是采用不同的规则进行描述,如果对于每个目标文件,都得写一个不同的规则来描述,那就变成了很麻烦的事了,虽然也能实现想要的效果。Makefile中的模式就是用来解决这种烦恼的。如下的Makefile就运用到了模式。


.PHONY: clean
CC=gcc
RM=rm
EXE=simple
OBJS=main.o foo.o
$(EXE) : $(OBJS)
  $(CC) -o $@ $^
%.o : %.c
  $(CC) -o $@ -c $^
clean:
  $(RM) -rf $(EXE) $(OBJS)

2020011911052752.png

通过“函数”增强功能

函数是Makefile中的另一个离奇,通过使用函数能显著增强Makefile的功能。对于之前的项目的Makefile,尽管使用了模式规则,但还有一件比较麻烦的事——在Makefile中要指明每一个项目源文件。

如下是采用了wildcard和patsubst函数修改后的Makefile:


.PHONY: clean
CC = gcc
RM = rm
EXE = simple
SRCS = $(wildcard *.c)
OBJS = $(patsubst %.c, %.o, $(SRCS))
$(EXE) : $(OBJS)
  $(CC) -o $@ $^
%.o : %.c
  $(CC) -o $@ -c $^
clean:
  $(RM) -rf $(EXE) $(OBJS)


执行结果如下:

20200119141409236.png

当使用函数编辑Makefile后,在增加需要编译的源文件后不需要再修改Makefile,直接就能对新增的文件进行编译。如下:

20200119141626797.png

下面就来详细介绍一下Makefile中经常用到的几个函数。更多函数的使用方法可以参考官方手册《GNU Make》。


abspath函数

abspath函数被用于将_names中的各路径名转换成绝对路径,并将转换后的结果返回。其形式:


$(abspath _names)

如下示例:


.PHONY: all
ROOT := $(abspath /usr/../lib)
all:
  @echo $(ROOT)

执行结果如下:

20200119142100668.png



相关文章
|
4天前
|
缓存 资源调度 安全
深入探索Linux操作系统的心脏——内核配置与优化####
本文作为一篇技术性深度解析文章,旨在引领读者踏上一场揭秘Linux内核配置与优化的奇妙之旅。不同于传统的摘要概述,本文将以实战为导向,直接跳入核心内容,探讨如何通过精细调整内核参数来提升系统性能、增强安全性及实现资源高效利用。从基础概念到高级技巧,逐步揭示那些隐藏在命令行背后的强大功能,为系统管理员和高级用户打开一扇通往极致性能与定制化体验的大门。 --- ###
19 9
|
1天前
|
算法 Linux 调度
深入理解Linux内核调度器:从基础到优化####
本文旨在通过剖析Linux操作系统的心脏——内核调度器,为读者揭开其高效管理CPU资源的神秘面纱。不同于传统的摘要概述,本文将直接以一段精简代码片段作为引子,展示一个简化版的任务调度逻辑,随后逐步深入,详细探讨Linux内核调度器的工作原理、关键数据结构、调度算法演变以及性能调优策略,旨在为开发者与系统管理员提供一份实用的技术指南。 ####
14 4
|
4天前
|
算法 Unix Linux
深入理解Linux内核调度器:原理与优化
本文探讨了Linux操作系统的心脏——内核调度器(Scheduler)的工作原理,以及如何通过参数调整和代码优化来提高系统性能。不同于常规摘要仅概述内容,本摘要旨在激发读者对Linux内核调度机制深层次运作的兴趣,并简要介绍文章将覆盖的关键话题,如调度算法、实时性增强及节能策略等。
|
6天前
|
机器学习/深度学习 负载均衡 算法
深入探索Linux内核调度机制的优化策略###
本文旨在为读者揭开Linux操作系统中至关重要的一环——CPU调度机制的神秘面纱。通过深入浅出地解析其工作原理,并探讨一系列创新优化策略,本文不仅增强了技术爱好者的理论知识,更为系统管理员和软件开发者提供了实用的性能调优指南,旨在促进系统的高效运行与资源利用最大化。 ###
|
6天前
|
监控 网络协议 算法
Linux内核优化:提升系统性能与稳定性的策略####
本文深入探讨了Linux操作系统内核的优化策略,旨在通过一系列技术手段和最佳实践,显著提升系统的性能、响应速度及稳定性。文章首先概述了Linux内核的核心组件及其在系统中的作用,随后详细阐述了内存管理、进程调度、文件系统优化、网络栈调整及并发控制等关键领域的优化方法。通过实际案例分析,展示了这些优化措施如何有效减少延迟、提高吞吐量,并增强系统的整体健壮性。最终,文章强调了持续监控、定期更新及合理配置对于维持Linux系统长期高效运行的重要性。 ####
|
6天前
|
安全 网络协议 Linux
Linux操作系统的内核升级与优化策略####
【10月更文挑战第29天】 本文深入探讨了Linux操作系统内核升级的重要性,并详细阐述了一系列优化策略,旨在帮助系统管理员和高级用户提升系统的稳定性、安全性和性能。通过实际案例分析,我们展示了如何安全有效地进行内核升级,以及如何利用调优技术充分发挥Linux系统的潜力。 ####
24 1
|
9天前
|
人工智能 算法 大数据
Linux内核中的调度算法演变:从O(1)到CFS的优化之旅###
本文深入探讨了Linux操作系统内核中进程调度算法的发展历程,聚焦于O(1)调度器向完全公平调度器(CFS)的转变。不同于传统摘要对研究背景、方法、结果和结论的概述,本文创新性地采用“技术演进时间线”的形式,简明扼要地勾勒出这一转变背后的关键技术里程碑,旨在为读者提供一个清晰的历史脉络,引领其深入了解Linux调度机制的革新之路。 ###
|
25天前
|
监控 Linux 云计算
Linux操作系统在云计算环境中的实践与优化###
【10月更文挑战第16天】 本文探讨了Linux操作系统在云计算环境中的应用实践,重点分析了其在稳定性、安全性和高效性方面的优势。通过具体案例,阐述了Linux如何支持虚拟化技术、实现资源高效分配以及与其他开源技术的无缝集成。文章还提供了针对Linux系统在云计算中的优化建议,包括内核参数调整、文件系统选择和性能监控工具的应用,旨在帮助读者更好地理解和应用Linux于云计算场景。 ###
31 3
|
1月前
|
Linux C++
Linux c/c++之makefile的基础使用
Linux下C/C++项目中makefile的基本使用,包括基础、进阶和高级用法,以及如何创建和使用makefile来自动化编译过程。
15 0
Linux c/c++之makefile的基础使用
|
2月前
|
存储 监控 安全
探究Linux操作系统的进程管理机制及其优化策略
本文旨在深入探讨Linux操作系统中的进程管理机制,包括进程调度、内存管理以及I/O管理等核心内容。通过对这些关键组件的分析,我们将揭示它们如何共同工作以提供稳定、高效的计算环境,并讨论可能的优化策略。
46 0