目录
- 前言
- 1. Makefile 文件的基本构成
- 2. makefile的依赖关系的自动化推导
- 3. make执行过程中的一些现象及其原理
- 3.1 证明该现象原理
- 3.2 关于 stat 时间属性的拓展
前言
身处 linux 平台环境开发中的伙伴们都知道 gcc/g++ 编译器以及编译指令,但是不难想象在以后的生活或者工作中,肯定是有多文件编译的需求,少则数10个,多则上百也不是不可能。
那么我们难道就直接 gcc -o test t1 t2 t3 ..... t99
吗??显然是费力不讨好,毕竟还有一个 rm t1 t2 t3 ...... t99
等着你呢!
所以针对上述场景及其需求,该篇文章主要介绍的是在linux系统中项目自动化构建工具make以及其配置文件Makefile的相关内容。
我们先抛开一切原理及其设计理念,先见一见所谓的make以及Makefile,所谓 “没吃过猪肉,咱也得见一见猪跑吧~~”
# Makefile
test:test.cpp
g++ -o test test.cpp
clean:
rm -f test
有了 Makefile 之后,我们只需要make 即可完成编译生成可执行程序,make clean 即可完成删除可执行程序。
1. Makefile 文件的基本构成
# Makefile
test:test.cpp
g++ -o test test.cpp
clean:
rm -f test
其中的 test:test.cpp 和 clean 我们称之为依赖关系,test 和 clean 下面带的指令我们称为依赖方法
这么一听似乎有点抽象。什么是依赖关系?什么是依赖方法?
打个比方,月底到了,作为大学生的张三生活费以及见底了,于是乎,他需要打电话给他的老爸,而接通电话的那一刻,张三即需要表面身份,即与他电话沟通的人的关系,即所谓的依赖关系。而张三表面完关系之后,需要表面其来电目的,即依赖方法。如果张三不表明依赖关系,他爸凭什么给他生活费?换言之,假设今天张三拨号给中国银行,让中国银行给予其生活费,中国银行会同意吗??道理很简单,因为张三与中国银行之间不存在依赖关系,因此,没有依赖关系的基础上,无法执行依赖方法(也即为张三来电的目的)。同理,假设张三拨号给他老爸,开头一句:”爸!“,然后马上把电话挂了,他爸知道他要干嘛吗??是不是显然不知道!因此,仅有依赖关系也不够,还需要有与该依赖关系对应匹配的依赖方法!
2. makefile的依赖关系的自动化推导
我们把 Makefile 文件改稍微复杂一点,如下:
test:test.o 想要生成 test 可执行程序,需要先有 test.o 文件
g++ test.o -o test
test.o:test.s 想要生成 test.o 文件,需要先有 test.s 文件
g++ -c test.s -o test.o
test.s:test.i 想要生成 test.s 文件,需要先有 test.i 文件
g++ -S test.i -o test.s
test.i:test.cpp 直到最后找到 test.cpp,然后预处理之后生成 test.i 文件
g++ -E test.cpp -o test.i
clean:
rm -f test.i test.s test.o test
我们可以看到,将makefile 改为四步编译之后,执行make依旧可以顺利的进行编译,并且其编译过程是按照四步编译的顺序进行编译的(与Makefile 文件里面的依赖关系与其匹配的依赖方法的位置顺序无关!!!)。这就说明在 make 执行的过程中,make会自动推导 makefile 中的依赖关系。
并且我们这么一看,想要 test
,需要先有 test.o
, 想要 test.o
,需要先有 test.s
, 想要 test.s
,需要先有 test.i
, 想要 test.i,需要先有 test.cpp
,然后 .i
文件有了,返回给 .s
的依赖关系。。。依次进行返回。这个过程不就是类似于递归的过程吗?!而所谓的 test.cpp
就类似于递归出口。 我们都知道,递归就需要借助栈帧或者栈结构来完成。那么我们就将 这种类似于递归过程,栈这样的结构,称之为makefile的依赖关系的自动化推导!
但是,为什么 clean 的时候,我们需要在前面加上一个make,而编译的时候不用呢??
我们修改一下makefile文件:
clean:
rm -f test.i test.s test.o test
test:test.o
g++ test.o -o test
test.o:test.s
g++ -c test.s -o test.o
test.s:test.i
g++ -S test.i -o test.s
test.i:test.cpp
g++ -E test.cpp -o test.i
在我们调换 可执行程序 和 clean
的位置之后,我们发现,make
变成 clean
了!! 而编译我们需要 make + 目标文件名
才能够完成。因此我们不难得出结论:make会自顶向下扫面 makefile 文件,把你要形成的第一个目标文件,当作make的默认动作!而 make + 目标文件即为:指定名称的执行 该依赖关系 与其匹配的 依赖方法
3. make执行过程中的一些现象及其原理
可以看到,当我们make编译完该文件之后,系统就不让我们继续make了!这是为什么呢??
我们知道的是,不管在 windows 还是 linux, 文件 = 文件内容 + 文件属性,并且该等式恒成立。那么我们现在先猜测,可能是因为源文件没有进行任何修改,因此 make 会根据源文件和目标文件的新旧,判定是否需要重新执行依赖关系进行编译!
那么现在的问题是:为什么 make 要这么做??原因其实也很好理解,因为在学习阶段,源代码本身的编译时间几乎忽略不计,但是等到了工作和研发当中,几十万,几百万的源代码,编译时间可是少说一个小时起步的,那么 make 这样做的原因无非就是在不影响程序的前提下,提高效率!
到这里,又有另一个问题产生,make 是如何完成对源文件和目标文件的新旧进行判定的??
在回答这个问题之前,我们要有一个基本的认知:“一定是先有的源文件,才有的可执行程序,而一般而言,源文件的最近修改时间 比 可执行文件要早!!” 。那么这样一来,我们就不难猜测出,该操作只需要 对 可执行程序的最近修改时间 (.out)与 源文件的最近修改时间 (.cpp)进行比较即可判断是否需要对该源文件进行编译。如果 .out < .cpp 那么即说明在可执行程序生成之后,源文件有过变动,因此需要重新编译;反之, .out > .cpp,即说明源文件生成可执行程序之后,并没有过修改动作(不管是文件内容还是文件属性)。
讲到这里,上述的一切也只不过是我们的猜测结论而已!!那么接下来,我们将对上述的猜测进行一定的证明:
3.1 证明该现象原理
[outlier@localhost makefile]$ stat test.cpp
File: ‘test.cpp’
Size: 153 Blocks: 8 IO Block: 4096 regular file
Device: fd00h/64768d Inode: 1351420 Links: 1
Access: (0664/-rw-rw-r--) Uid: ( 1001/ outlier) Gid: ( 1001/ outlier)
Access: 2024-07-06 22:13:05.007175733 +0800
Modify: 2024-07-06 21:55:05.962028145 +0800
Change: 2024-07-06 21:55:05.963028143 +0800
Birth: -
以上是关于 test.cpp 文件的三个时间属性,分别是:
Access(最近访问时间,对文件增删查改都属于访问的范畴)
Modify(文件内容的最近修改时间),
Change(文件属性的最近修改时间)
先是对这三个时间属性进行分析,虽然我们不太能对访问的范畴有一个很好的标准,但是对于文件内容和文件属性的修改可以做一波推论 ==》 Modify 修改,极大可能的会导致 Change 的修改,最常见的就比如文件的大小发生改变。而Change 的修改,不一定会导致 Modify 的修改,比如改变文件的权限属性,那么这种情况下,文件内容是不变的。
接下来我们对文件内容进行修改,并且观察修改前后的 Modify 时间,以此来判断 make 是不是因为最近修改时间作为判断依据。
设 源文件的最近修改时间 = Tcpp,可执行程序的最近修改时间 = Tout
这次的测试证明了我们上述的猜想和推论是正确的,修改了源文件,Tcpp 得到改变之后,Tcpp > Tout,因此 make 会重新执行依赖关系进行编译源文件。
而对文件内容进行修改之后,Change 也进行了更新,说明文件内容的修改,往往极大可能伴随的是,文件属性的修改!一般 Change 也会随之改变。
————————————————————————————————————————————————————
3.2 关于 stat 时间属性的拓展
上面说了, Access 是文件访问的最近时间,可是现在又有一个现象,当我访问了文件,却不见得 Access 得到更新,这是为什么呢??
Access 并没有随着我们的查看而进行改变,其中的原因可能涉及到 IO 方面的设计。
我们应该都需要清楚的是, Access 毫无疑问是三大时间属性当中,修改频率最高的一个,而文件是存储在磁盘当中,修改一次Access ,等价于修改一次文件属性,意味着需要做一次持久化。问题就在于磁盘属于外设,其读写速度远远低于内存,更别提cpu了,因此,高频率的对外设进行读写,是一个非常大的代价,也是效率极低的一个行为,该行为不利于系统整机的效率!
因此,Access 采用了类似“缓存“的更新策略,比如根据 Modify 和 Change 的修改次数 或者 文件被访问到一定次数 之后,才对 Access 进行新的持久化。
但是假如此刻我就是想要在访问文件之后,Access 就立刻、马上得到更新!那有办法吗??
这就需要我们重新来认识一下 touch 这个指令了
touch 文件
a. 文件不存在时,创建文件
b. 文件存在时,更新文件的时间(三个时间属性都会更新)
touch -a
touch -m
touch -c
也可以只根据某个时间属性进行更新
到这里,已经验证了我们上述的所有猜想以及推论,make 是否会重新执行依赖关系进行编译,取决于 可执行程序的最近修改时间 是否早于 或者晚于 源文件的最近修改时间。而 touch 文件
时,文件已经存在,则可以更新文件的时间属性,而一般情况下,不管是 touch -a
还是 touch -m
,Change 也会随着这两个的更新而更新,而 touch
不带选项是更新整个文件的时间
现在,我就是不想要使用 touch, 但是我还想要可以让 依赖关系 总是被执行!还有其它办法吗??
.PHNOY:test
test:test.cpp
g++ -o test test.cpp
clean:
rm -f test
其中的 .PHNOY
后面修饰的文件符号,我们称之为 伪目标
而 .PHNOY:test
代表的就是:test 的依赖关系 与其对应的 依赖方法 总是被执行!
就相当于告诉 make,你别管 Tcpp 和 Tout 的关系了!当用户执行 make 指令的时候,你就执行一遍依赖关系进行编译就行了!
按照 make 原本的规则,这 Tccp < Tout,其依赖关系与其对应的依赖方法是不被执行的啊!但是,我们在 Makefile 添加了 test 这个依赖关系的伪目标,因此 make 在执行的时候就可以忽略时间上的关系。
————————————————————————————————————————————————————
好了,回到我们这篇文章的主题:make
上面讲了那么多样的任性,我们可以那么任性,但是我们一般不那么做,因为出于各种考虑,给 make 加上时间上的限制也不是什么坏事。
所以我们的 makefile 文件一般会把伪目标设置成 clean,因为在以后,clean 的依赖方法可能不仅仅是 rm 这么简单的操作!
test:test.cpp
g++ -o $@ $^
.PHNOY:clean
clean:
rm -f test
其中的 $@
就是目标文件,即冒号以左的部分,$^
就是源文件,寂即冒号以右的部分,这样写的好处是,当依赖关系中的源文件很多的时候,依赖方法就不需要你重新在写一遍了,只需要 $@ $^
即可。
而当我们在执行 make 或者 make clean 的时候,不希望执行的依赖关系对应的依赖方法回显在屏幕上,我们可以这样写
test:test.cpp
@g++ -o $@ $^
.PHNOY:clean
clean:
@rm -f test
OK,关于 make/Makefile 我们就讲这么多,该篇文章可以说是比较完整,系统的讲述了 make 和 Makefile 的方方面面,包括各种想象,以及其原因。
如果感觉该篇文章给你带来了收获,可以 点赞👍 + 收藏⭐️ + 关注➕ 支持一下!
感谢各位观看!