👻内容专栏: Linux操作系统基础
🐨本文概括: 工具使用的背景、理解make/makefile工具、探索工作原理(文件修改时间的对比)、.PHONY伪目标、特性等。
🐼本文作者: 阿四啊
🐸发布时间:2023.9.14
背景
- “
make
” 和 “Makefile
” 是用于自动化构建和编译软件项目的工具和文件。它们通常用于编译源代码并生成可执行文件,库文件或其他项目构建目标。 - 一个工程中的源文件不计数,其按类型、功能、模块分别放在若干个目录中,
makefile
定义了一系列的规则来指定,哪些文件需要先编译,哪些文件需要后编译,哪些文件需要重新编译,甚至于进行更复杂的功能操作。 makefile
带来的好处就是——“自动化编译”,一旦写好,只需要一个make
命令,整个工程完全自动编译,极大的提高了软件开发的效率。make
是一条命令,makefile
是一个文件,两个搭配使用,完成项目自动化构建。
理解
make
📌"make
" 是一个命令行,用于构建项目。它根据一个称为 “Makefile
” 的文本文件中的规则来确定需要构建哪些文件以及如何构建它们。“make” 工具的基本语法如下:
make [目标]
⚠️注:其中 “目标” 是您要构建的项目中的一个特定目标,例如生成可执行文件或库。如果不提供目标,则 “make” 将默认构建 Makefile 中定义的第一个目标。
Makefile
📌Makefile
是一个文本文件,文件名首字母M
一般建议大写,也可以写成makefile
,其中包含构建项目的依赖关系和依赖方法。每个依赖关系都描述了如何生成一个目标文件以及哪些文件或其他目标是其依赖方法。Makefile 使用如下:
- 规则:指定了依赖关系和依赖方法。
-
我们用C语言写一个简单的
code.c
文件:
-
以下是一个简单的示例
Makefile
,用于构建一个名为 “mybin
” 的可执行文件:1 mybin: code.c #指定了依赖关系 2 gcc code.c -o mybin #依赖方法,比如以制表符开头。 3 clean: 4 rm -f mybin
示例中,"
mybin
"是目标,依赖于code.c
,与code.c
构成依赖关系。 依赖方法是构建“mybin
”的命令:gcc code.c -o mybin
(用gcc命令编译code.c文件)。“clean
”是另一个目标,用于删除生成的可执行文件mybin
。 -
运行“
make
”命令,指定要构建的目标。(注:若不指定目标,系统将自动扫描Makefile
中第一个目标)[Asi@localhost ~]$ make gcc code.c -o mybin
运行"
make clean
"命令。[Asi@localhost ~]$ make clean rm -f mybin
工作原理
原理
-
如果我们再次执行几次
make
命令,发现编译不了了,会报出以下错误:[Asi@localhost ~]$ make make: `mybin' is up to date. [Asi@localhost ~]$ make make: `mybin' is up to date. [Asi@localhost ~]$ make make: `mybin' is up to date.
为什么会出现这样的问题呢?对于没有更改的
code.c
文件,系统会进行识别,不会再进行编译链接,到这可能有小伙伴们会问,这么一个小文件,对编译器来说不算什么吧?但是你们想想,若项目中有几百个上千个文件呢,程序翻译的过程的周期是非常长的,所以编译器会进行识别对于没更改过的源文件可以跳过,大大缩短编译的时间。所以make/Makefile
可以提高编译效率! -
编译器是如何知道源文件是否被更改过呢?make是如何工作的?
- 源文件和可执行程序时间的对比,就能体现源文件的新旧(重新编译会重新写入一个二进制可执行文件,其修改时间也会被修改)
- 第一次编译的时候,一定是源文件的修改时间 小于 可执行程序的修改时间,当我们对源文件做任何修改之后,源文件的修改时间就 大于 可执行程序的修改时间,然后进行程序编译之后,那么源文件的修改时间又 小于可执行程序的修改时间。
文件的修改时间(ACM时间)
下面,我们执行stat + [文件名]
命令,可查看一个文件的修改时间。
Access time
(Atime
):这是文件的最后一次访问时间。当文件被读取、浏览或访问时,Atime
时间会被更新。
Modify time
(Mtime
):这是文件的最后一次内容上修改时间。Mtime的更新,很可能会和Ctime或者Atime联动更改。
Change time
(Ctime
):这是文件的最后一次属性上修改时间。Ctime的更新,其他时间基本不会更新。
-
我们尝试给源文件
code.c
修改他的任意属性,比如给所属组添加了执行权限,再次stat命令查看code.c
的时间,发现与原来时间相比,修改文件权限,只改变了Ctime
,其他时间则是保持不变。[Asi@localhost ~]$ ll total 36 -rw-rw-r-- 1 Asi Asi 130 Sep 14 15:29 code.c [Asi@localhost ~]$ chmod g+x code.c
-
下一步,我们试着修改一下
code.c
文件的内容,如下所示
我们可以发现,Mtime
的更新,Atime
和Ctime
发生了联动更改。 -
然后,我们查看一下
code.c
文件,发现它的ACM
时间都没有发生变化,再进行两次查看,时间也照样没有发生变化。[Asi@localhost ~]$ cat code.c #include <stdio.h> int main() { int i; for(i = 0; i <= 10; i++) { printf("num = %d\n",i); } printf("complete!\n"); return 0; } [Asi@localhost ~]$ stat code.c File: ‘code.c’ Size: 156 Blocks: 8 IO Block: 4096 regular file Device: fd01h/64769d Inode: 1063658 Links: 1 Access: (0674/-rw-rwxr--) Uid: ( 1003/ Asi) Gid: ( 1003/ Asi) Access: 2023-09-14 15:39:11.012008120 +0800 Modify: 2023-09-14 15:39:11.010008051 +0800 Change: 2023-09-14 15:39:11.010008051 +0800 Birth: -
🤔💡:那么为什么会这样子呢?
PS:一般而言,一个文件被查看的频率是最高的。而文件是存放到磁盘中的,访问、更改文件本质其实就是访问磁盘,那么每次访问更改access time
就是访问磁盘,这会让Linux系统充满大量的访问磁盘的IO操作,简介地减缓系统效率。所以,一些编译器一般会配置减少文件atime的更新频率,以提高性能。
不修改内容,更新文件的修改时间
我们再次回到前面一个问题,系统究竟是根据三个时间中的哪个时间来进行源文件与可执行程序的对比呢?当然最合理的是Modify time
(Mtime
)。
所以我们这里再次去执行make
命令,编译不了,其根本原因是因为code.c的Mtime
早于mybin的Mtime
!
[Asi@localhost ~]$ make
make: `mybin' is up to date.
[Asi@localhost ~]$ stat code.c
File: ‘code.c’
Size: 156 Blocks: 8 IO Block: 4096 regular file
Device: fd01h/64769d Inode: 1063658 Links: 1
Access: (0674/-rw-rwxr--) Uid: ( 1003/ Asi) Gid: ( 1003/ Asi)
Access: 2023-09-14 15:43:46.586536040 +0800
Modify: 2023-09-14 15:43:46.584535971 +0800
Change: 2023-09-14 15:43:46.584535971 +0800
Birth: -
[Asi@localhost ~]$ stat mybin
File: ‘mybin’
Size: 8496 Blocks: 24 IO Block: 4096 regular file
Device: fd01h/64769d Inode: 1063645 Links: 1
Access: (0775/-rwxrwxr-x) Uid: ( 1003/ Asi) Gid: ( 1003/ Asi)
Access: 2023-09-14 16:09:31.152943398 +0800
Modify: 2023-09-14 16:09:31.152943398 +0800
Change: 2023-09-14 16:09:31.152943398 +0800
Birth: -
所以我们可以通过对Mtime
进行修改以更新文件的时间,那么除了修改源文件的内容,我们可以使用touch
命令,对,你没看错,touch
命令不仅仅可以创建一个新文件,它也可以去更新已存在文件的时间!
touch [文件名]:所有时间都更新
touch -a [文件名]:更新atime
touch -m [文件名]:更新mtime
[Asi@localhost ~]$ make
make: `mybin' is up to date.
[Asi@localhost ~]$ touch -m code.c
[Asi@localhost ~]$ make
gcc code.c -o mybin
[Asi@localhost ~]$ make
make: `mybin' is up to date.
[Asi@localhost ~]$ touch code.c
[Asi@localhost ~]$ make
gcc code.c -o mybin
.PHONY
📌.PHONY
的主要作用是告诉 make 工具,不管是否存在对应的文件,总是执行这些目标中的命令,其依赖方法总是会被执行,不会被任何情况拦截。它用于指定一个伪目标(phony target)。
.PHONY: target_name
- 样例测试:
.PHONY
指定了mybin
为伪目标,所以我们执行make
命令,无论是否存在可执行程序,始终会执行依赖方法相关联的命令。
- 结果:
[Asi@localhost ~]$ make gcc code.c -o mybin [Asi@localhost ~]$ make gcc code.c -o mybin [Asi@localhost ~]$ make gcc code.c -o mybin [Asi@localhost ~]$ make gcc code.c -o mybin
项目清理
- 在开发中,我们的工程会根据情况来清理的,像clean这种,没有被第一个目标文件直接或间接关联,那么它后面所定义的命令将不会被自动执行。
- 不过,我们可以显示用
make
执行。即命令“make clean
”,我们在测试时,以此来清理所有的目标文件,以便重新编译。
特性
用 make/makefile 写出整个程序的翻译过程
“make” 和 “Makefile” 具有依赖性的推导能力的。当运行make
命令,make
会自动检测依赖关系,mybin
根据依赖关系去寻找code.o
,如果code.o
文件不存在,它不会执行依赖方法,回向下寻找code.o
,如果向下没有找到code.o
,编译器可能会报错,可能会隐式形成。那么code.o
找到之后,它会根据依赖关系去寻找code.s
,code.s
继续向下寻找……以此类推。直到找到code.c
,执行依赖方法模块的命令之后形成code.i
,然后在逆向回去扫描,执行其他尚未执行成功的依赖方法。此过程就如同入栈、出栈的过程,所以"make" 和 “Makefile” 就是根据这么一套逻辑执行完依赖关系和依赖方法。
执行结果
[Asi@localhost ~]$ make
gcc code.c -E -o code.i
gcc code.i -S -o code.s
gcc code.s -c -o code.o
gcc code.o -o mybin