makefile教程

分類 devops, make

简介

Linux的make程序用来自动化编译大型源码,很多时候,我们在Linux下编译安装软件,只需要敲一个make就可以全自动完成,非常方便。

make能自动化完成这些工作,是因为项目提供了一个Makefile文件,它负责告诉make,应该如何编译和链接程序。

Makefile相当于Java项目的pom.xml,Node工程的package.json,Rust项目的Cargo.toml,不同之处在于,make虽然最初是针对C语言开发,但它实际上并不限定C语言,而是可以应用到任意项目,甚至不是编程语言。此外,make主要用于Unix/Linux环境的自动化开发,掌握Makefile的写法,可以更好地在Linux环境下做开发,也可以为后续开发Linux内核做好准备。

在本教程中,我们将由浅入深,一步一步学习如何编写Makefile,完全针对零基础小白,只需要提前掌握如何使用Linux命令。


在Linux环境下,当我们输入make命令时,它就在当前目录查找一个名为Makefile的文件,然后,根据这个文件定义的规则,自动化地执行任意命令,包括编译命令。

Makefile这个单词,顾名思义,就是指如何生成文件。

我们举个例子:在当前目录下,有3个文本文件:a.txtb.txtc.txt

现在,我们要合并a.txtb.txt,生成中间文件m.txt,再用中间文件m.txtc.txt合并,生成最终的目标文件x.txt,整个逻辑如下图所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
┌─────┐ ┌─────┐ ┌─────┐
│a.txt│ │b.txt│ │c.txt│
└─────┘ └─────┘ └─────┘
│ │ │
└───┬───┘ │
│ │
▼ │
┌─────┐ │
│m.txt│ │
└─────┘ │
│ │
└─────┬─────┘


┌─────┐
│x.txt│
└─────┘

根据上述逻辑,我们来编写Makefile

规则

Makefile由若干条规则(Rule)构成,每一条规则指出一个目标文件(Target),若干依赖文件(prerequisites),以及生成目标文件的命令。

例如,要生成m.txt,依赖a.txtb.txt,规则如下:

1
2
3
# 目标文件: 依赖文件1 依赖文件2
m.txt: a.txt b.txt
cat a.txt b.txt > m.txt

一条规则的格式为目标文件: 依赖文件1 依赖文件2 ...,紧接着,以Tab开头的是命令,用来生成目标文件。上述规则使用cat命令合并了a.txtb.txt,并写入到m.txt。用什么方式生成目标文件make并不关心,因为命令完全是我们自己写的,可以是编译命令,也可以是cpmv等任何命令。

#开头的是注释,会被make命令忽略。

1
注意:Makefile的规则中,命令必须以Tab开头,不能是空格。

类似的,我们写出生成x.txt的规则如下:

1
2
x.txt: m.txt c.txt
cat m.txt c.txt > x.txt

由于make执行时,默认执行第一条规则,所以,我们把规则x.txt放到前面。完整的Makefile如下:

1
2
3
4
5
x.txt: m.txt c.txt
cat m.txt c.txt > x.txt

m.txt: a.txt b.txt
cat a.txt b.txt > m.txt

在当前目录创建a.txtb.txtc.txt,输入一些内容,执行make

1
2
3
$ make
cat a.txt b.txt > m.txt
cat m.txt c.txt > x.txt

make默认执行第一条规则,也就是创建x.txt,但是由于x.txt依赖的文件m.txt不存在(另一个依赖c.txt已存在),故需要先执行规则m.txt创建出m.txt文件,再执行规则x.txt。执行完成后,当前目录下生成了两个文件m.txtx.txt

可见,Makefile定义了一系列规则,每个规则在满足依赖文件的前提下执行命令,就能创建出一个目标文件,这就是英文Make file的意思。

把默认执行的规则放第一条,其他规则的顺序是无关紧要的,因为make执行时自动判断依赖。

此外,make会打印出执行的每一条命令,便于我们观察执行顺序以便调试。

如果我们再次运行make,输出如下:

1
2
$ make
make: `x.txt' is up to date.

make检测到x.txt已经是最新版本,无需再次执行,因为x.txt的创建时间晚于它依赖的m.txtc.txt的最后修改时间。

1
make使用文件的创建和修改时间来判断是否应该更新一个目标文件。

修改c.txt后,运行make,会触发x.txt的更新:

1
2
$ make
cat m.txt c.txt > x.txt

但并不会触发m.txt的更新,原因是m.txt的依赖a.txtb.txt并未更新,所以,make只会根据Makefile去执行那些必要的规则,并不会把所有规则都无脑执行一遍。

在编译大型程序时,全量编译往往需要几十分钟甚至几个小时。全量编译完成后,如果仅修改了几个文件,再全部重新编译完全没有必要,用Makefile实现增量编译就十分节省时间。

当然,是否能正确地实现增量更新,取决于我们的规则写得对不对,make本身并不会检查规则逻辑是否正确。

伪目标

因为m.txtx.txt都是自动生成的文件,所以,可以安全地删除。

删除时,我们也不希望手动删除,而是编写一个clean规则来删除它们:

1
2
3
clean:
rm -f m.txt
rm -f x.txt

clean规则与我们前面编写的规则有所不同,它没有依赖文件,因此,要执行clean,必须用命令make clean

1
2
3
$ make clean
rm -f m.txt
rm -f x.txt

然而,在执行clean时,我们并没有创建一个名为clean的文件,所以,因为目标文件clean不存在,每次运行make clean,都会执行这个规则的命令。

如果我们手动创建一个clean的文件,这个clean规则就不会执行了!

如果我们希望makeclean不要视为文件,可以添加一个标识:

1
2
3
4
.PHONY: clean
clean:
rm -f m.txt
rm -f x.txt

此时,clean就不被视为一个文件,而是伪目标(Phony Target)。

大型项目通常会提供cleaninstall这些约定俗成的伪目标名称,方便用户快速执行特定任务。

一般来说,并不需要用.PHONY标识clean等约定俗成的伪目标名称,除非有人故意搞破坏,手动创建名字叫clean的文件。

执行多条命令

一个规则可以有多条命令,例如:

1
2
3
4
cd:
pwd
cd ..
pwd

执行cd规则:

1
2
3
4
5
6
$ make cd
pwd
/home/ubuntu/makefile-tutorial/v1
cd ..
pwd
/home/ubuntu/makefile-tutorial/v1

观察输出,发现cd ..命令执行后,并未改变当前目录,两次输出的pwd是一样的,这是因为make针对每条命令,都会创建一个独立的Shell环境,类似cd ..这样的命令,并不会影响当前目录。

解决办法是把多条命令以;分隔,写到一行:

1
2
cd_ok:
pwd; cd ..; pwd;

再执行cd_ok目标就得到了预期结果:

1
2
3
4
$ make cd_ok
pwd; cd ..; pwd
/home/ubuntu/makefile-tutorial/v1
/home/ubuntu/makefile-tutorial

可以使用\把一行语句拆成多行,便于浏览:

1
2
3
4
cd_ok:
pwd; \
cd ..; \
pwd

另一种执行多条命令的语法是用&&,它的好处是当某条命令失败时,后续命令不会继续执行:

1
2
cd_ok:
cd .. && pwd

控制打印

默认情况下,make会打印出它执行的每一条命令。如果我们不想打印某一条命令,可以在命令前加上@,表示不打印命令(但是仍然会执行):

1
2
3
no_output:
@echo 'not display'
echo 'will display'

执行结果如下:

1
2
3
4
$ make no_output
not display
echo 'will display'
will display

注意命令echo 'not display'本身没有打印,但命令仍然会执行,并且执行的结果仍然正常打印。

控制错误

make在执行命令时,会检查每一条命令的返回值,如果返回错误(非0值),就会中断执行。

例如,不使用-f删除一个不存在的文件会报错:

1
2
3
has_error:
rm zzz.txt
echo 'ok'

执行上述目标,输出如下:

1
2
3
4
$ make has_error
rm zzz.txt
rm: zzz.txt: No such file or directory
make: *** [has_error] Error 1

由于命令rm zzz.txt报错,导致后面的命令echo 'ok'并不会执行,make打印出错误,然后退出。

有些时候,我们想忽略错误,继续执行后续命令,可以在需要忽略错误的命令前加上-

1
2
3
ignore_error:
-rm zzz.txt
echo 'ok'

执行上述目标,输出如下:

1
2
3
4
5
6
$ make ignore_error
rm zzz.txt
rm: zzz.txt: No such file or directory
make: [ignore_error] Error 1 (ignored)
echo 'ok'
ok

make检测到rm zzz.txt报错,并打印错误,但显示(ignored),然后继续执行后续命令。

对于执行可能出错,但不影响逻辑的命令,可以用-忽略。

参考源码

可以从GitHub下载源码。

小结

编写Makefile就是编写一系列规则,用来告诉make如何执行这些规则,最终生成我们期望的目标文件。

查看官方手册:

编译C程序

C程序的编译通常分两步:

  1. 将每个.c文件编译为.o文件;
  2. 将所有.o文件链接为最终的可执行文件。

我们假设如下的一个C项目,包含hello.chello.hmain.c

hello.c内容如下:

1
2
3
4
5
6
7
#include <stdio.h>

int hello()
{
printf("hello, world!\n");
return 0;
}

hello.h内容如下:

1
int hello();

main.c内容如下:

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
#include "hello.h"

int main()
{
printf("start...\n");
hello();
printf("exit.\n");
return 0;
}

注意到main.c引用了头文件hello.h。我们很容易梳理出需要生成的文件,逻辑如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
┌───────┐ ┌───────┐ ┌───────┐
│hello.c│ │main.c │ │hello.h│
└───────┘ └───────┘ └───────┘
│ │ │
│ └────┬────┘
│ │
▼ ▼
┌───────┐ ┌───────┐
│hello.o│ │main.o │
└───────┘ └───────┘
│ │
└───────┬──────┘


┌─────────┐
│world.out│
└─────────┘

假定最终生成的可执行文件是world.out,中间步骤还需要生成hello.omain.o两个文件。根据上述依赖关系,我们可以很容易地写出Makefile如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 生成可执行文件:
world.out: hello.o main.o
cc -o world.out hello.o main.o

# 编译 hello.c:
hello.o: hello.c
cc -c hello.c

# 编译 main.c:
main.o: main.c hello.h
cc -c main.c

clean:
rm -f *.o world.out

执行make,输出如下:

1
2
3
4
$ make
cc -c hello.c
cc -c main.c
cc -o world.out hello.o main.o

在当前目录下可以看到hello.omain.o以及最终的可执行程序world.out。执行world.out

1
2
3
4
$ ./world.out 
start...
hello, world!
exit.

与我们预期相符。

修改hello.c,把输出改为"hello, bob!\n",再执行make,观察输出:

1
2
3
$ make
cc -c hello.c
cc -o world.out hello.o main.o

仅重新编译了hello.c,并未编译main.c。由于hello.o已更新,所以,仍然要重新生成world.out。执行world.out

1
2
3
4
$ ./world.out 
start...
hello, bob!
exit.

与我们预期相符。

修改hello.h

1
2
// int 变为 void:
void hello();

以及hello.c,再次执行make

1
2
3
4
$ make
cc -c hello.c
cc -c main.c
cc -o world.out hello.o main.o

会触发main.c的编译,因为main.c依赖hello.h

执行make clean会删除所有的.o文件,以及可执行文件world.out,再次执行make就会强制全量编译:

1
2
3
4
5
$ make clean && make
rm -f *.o world.out
cc -c hello.c
cc -c main.c
cc -o world.out hello.o main.o

这个简单的Makefile使我们能自动化编译C程序,十分方便。

不过,随着越来越多的.c文件被添加进来,如何高效维护Makefile的规则?我们后面继续讲解。

参考源码

可以从GitHub下载源码。

小结

Makefile正确定义规则后,我们就能用make自动化编译C程序。



使用隐式规则

我们仍然以上一节的C项目为例,当我们添加越来越多的.c文件时,就需要编写越来越多的规则来生成.o文件。

实际上,有的同学可能发现了,即使我们把.o的规则删掉,也能正常编译:

1
2
3
4
5
6
# 只保留生成 world.out 的规则:
world.out: hello.o main.o
cc -o world.out hello.o main.o

clean:
rm -f *.o world.out

执行make,输出如下:

1
2
3
4
$ make
cc -c -o hello.o hello.c
cc -c -o main.o main.c
cc -o world.out hello.o main.o

我们没有定义hello.omain.o的规则,为什么make也能正常创建这两个文件?

因为make最初就是为了编译C程序而设计的,为了免去重复创建编译.o文件的规则,make内置了隐式规则(Implicit Rule),即遇到一个xyz.o时,如果没有找到对应的规则,就自动应用一个隐式规则:

1
2
xyz.o: xyz.c
cc -c -o xyz.o xyz.c

make针对C、C++、ASM、Fortran等程序内置了一系列隐式规则,可以参考官方手册查看。

对于C程序来说,使用隐式规则有一个潜在问题,那就是无法跟踪.h文件的修改。如果我们修改了hello.h的定义,由于隐式规则main.o: main.c并不会跟踪hello.h的修改,导致main.c不会被重新编译,这个问题我们放到后面解决。

参考源码

可以从GitHub下载源码。

小结

针对C、C++、ASM、Fortran等程序,make内置了一系列隐式规则,使用隐式规则可减少大量重复的通用编译规则。

查看官方手册:



当我们在Makefile中重复写很多文件名时,一来容易写错,二来如果要改名,要全部替换,费时费力。

编程语言使用变量(Variable)来解决反复引用的问题,类似的,在Makefile中,也可以使用变量来解决重复问题。

以上一节的Makefile为例:

1
2
3
4
5
world.out: hello.o main.o
cc -o world.out hello.o main.o

clean:
rm -f *.o world.out

编译的最终文件world.out重复出现了3次,因此,完全可以定义一个变量来替换它:

1
2
3
4
5
6
7
TARGET = world.out

$(TARGET): hello.o main.o
cc -o $(TARGET) hello.o main.o

clean:
rm -f *.o $(TARGET)

变量定义用变量名 = 值或者变量名 := 值,通常变量名全大写。引用变量用$(变量名),非常简单。

注意到hello.o main.o这个“列表”也重复了,我们也可以用变量来替换:

1
2
3
4
5
6
7
8
OBJS = hello.o main.o
TARGET = world.out

$(TARGET): $(OBJS)
cc -o $(TARGET) $(OBJS)

clean:
rm -f *.o $(TARGET)

如果有一种方式能让make自动生成hello.o main.o这个“列表”,就更好了。注意到每个.o文件是由对应的.c文件编译产生的,因此,可以让make先获取.c文件列表,再替换,得到.o文件列表:

1
2
3
4
5
6
7
8
9
10
# $(wildcard *.c) 列出当前目录下的所有 .c 文件: hello.c main.c
# 用函数 patsubst 进行模式替换得到: hello.o main.o
OBJS = $(patsubst %.c,%.o,$(wildcard *.c))
TARGET = world.out

$(TARGET): $(OBJS)
cc -o $(TARGET) $(OBJS)

clean:
rm -f *.o $(TARGET)

这样,我们每添加一个.c文件,不需要修改Makefile,变量OBJS会自动更新。

思考:为什么我们不能直接定义OBJS = $(wildcard *.o)make列出所有.o文件?

内置变量

我们还可以用变量$(CC)替换命令cc

1
2
$(TARGET): $(OBJS)
$(CC) -o $(TARGET) $(OBJS)

没有定义变量CC也可以引用它,因为它是make的内置变量(Builtin Variables),表示C编译器的名字,默认值是cc,我们也可以修改它,例如使用交叉编译时,指定编译器:

1
2
CC = riscv64-linux-gnu-gcc
...

自动变量

Makefile中,经常可以看到$@$<这样的变量,这种变量称为自动变量(Automatic Variable),它们在一个规则中自动指向某个值。

例如,$@表示目标文件,$^表示所有依赖文件,因此,我们可以这么写:

1
2
world.out: hello.o main.o
cc -o $@ $^

在没有歧义时可以写$@,也可以写$(@),有歧义时必须用括号,例如$(@D)

为了更好地调试,我们还可以把变量打印出来:

1
2
3
4
5
world.out: hello.o main.o
@echo '$$@ = $@' # 变量 $@ 表示target
@echo '$$< = $<' # 变量 $< 表示第一个依赖项
@echo '$$^ = $^' # 变量 $^ 表示所有依赖项
cc -o $@ $^

执行结果输出如下:

1
2
3
4
$@ = world.out
$< = hello.o
$^ = hello.o main.o
cc -o world.out hello.o main.o

参考源码

可以从GitHub下载源码。

小结

使用变量可以让Makefile更加容易维护。

查看官方手册:

使用模式规则

前面我们讲了使用隐式规则可以让make在必要时自动创建.o文件的规则,但make的隐式规则的命令是固定的,对于xyz.o: xyz.c,它实际上是:

1
$(CC) $(CFLAGS) -c -o $@ $<

能修改的只有变量$(CC)$(CFLAGS)。如果要执行多条命令,使用隐式规则就不行了。

这时,我们可以自定义模式规则(Pattern Rules),它允许make匹配模式规则,如果匹配上了,就自动创建一条模式规则。

我们修改上一节的Makefile如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
OBJS = $(patsubst %.c,%.o,$(wildcard *.c))
TARGET = world.out

$(TARGET): $(OBJS)
cc -o $(TARGET) $(OBJS)

# 模式匹配规则:当make需要目标 xyz.o 时,自动生成一条 xyz.o: xyz.c 规则:
%.o: %.c
@echo 'compiling $<...'
cc -c -o $@ $<

clean:
rm -f *.o $(TARGET)

make执行world.out: hello.o main.o时,发现没有hello.o文件,于是需要查找以hello.o为目标的规则,结果匹配到模式规则%.o: %.c,于是make自动根据模式规则为我们动态创建了如下规则:

1
2
3
hello.o: hello.c
@echo 'compiling $<...'
cc -c -o $@ $<

查找main.o也是类似的匹配过程,于是我们执行make,就可以用模式规则完成编译:

1
2
3
4
5
6
$ make
compiling hello.c...
cc -c -o hello.o hello.c
compiling main.c...
cc -c -o main.o main.c
cc -o world.out hello.o main.o

模式规则的命令完全由我们自己定义,因此,它比隐式规则更灵活。

但是,模式规则仍然没有解决修改hello.h头文件不会触发main.c重新编译的问题,这个依赖问题我们继续放到后面解决。

最后注意,模式规则是按需生成,如果我们在当前目录创建一个zzz.o文件,因为make并不会在执行过程中用到它,所以并不会自动生成zzz.o: zzz.c这个规则。

参考源码

可以从GitHub下载源码。

小结

使用模式规则可以灵活地按需动态创建规则,它比隐式规则更灵活。

查看官方手册:



前面我们讲了隐式规则和模式规则,这两种规则都可以解决自动把.c文件编译成.o文件,但都无法解决.c文件依赖.h文件的问题。

因为一个.c文件依赖哪个.h文件必须要分析文件内容才能确定,没有一个简单的文件名映射规则。

但是,要识别出.c文件的头文件依赖,可以用GCC提供的-MM参数:

1
2
$ cc -MM main.c
main.o: main.c hello.h

上述输出告诉我们,编译main.o依赖main.chello.h两个文件。

因此,我们可以利用GCC的这个功能,对每个.c文件都生成一个依赖项,通常我们把它保存到.d文件中,再用include引入到Makefile,就相当于自动化完成了每个.c文件的精准依赖。

我们改写上一节的Makefile如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# 列出所有 .c 文件:
SRCS = $(wildcard *.c)

# 根据SRCS生成 .o 文件列表:
OBJS = $(SRCS:.c=.o)

# 根据SRCS生成 .d 文件列表:
DEPS = $(SRCS:.c=.d)

TARGET = world.out

# 默认目标:
$(TARGET): $(OBJS)
$(CC) -o $@ $^

# xyz.d 的规则由 xyz.c 生成:
%.d: %.c
rm -f $@; \
$(CC) -MM $< >$@.tmp; \
sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' < $@.tmp > $@; \
rm -f $@.tmp

# 模式规则:
%.o: %.c
$(CC) -c -o $@ $<

clean:
rm -rf *.o *.d $(TARGET)

# 引入所有 .d 文件:
include $(DEPS)

变量$(SRCS)通过扫描目录可以确定为hello.c main.c,因此,变量$(OBJS)赋值为hello.o main.o,变量$(DEPS)赋值为hello.d main.d

通过include $(DEPS)我们引入hello.dmain.d文件,但是这两个文件一开始并不存在,不过,make通过模式规则匹配到%.d: %.c,这就给了我们一个机会,在这个模式规则内部,用cc -MM命令外加sed.d文件创建出来。

运行make,首次输出如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ make
Makefile:31: hello.d: No such file or directory
Makefile:31: main.d: No such file or directory
rm -f main.d; \
cc -MM main.c >main.d.tmp; \
sed 's,\(main\)\.o[ :]*,\1.o main.d : ,g' < main.d.tmp > main.d; \
rm -f main.d.tmp
rm -f hello.d; \
cc -MM hello.c >hello.d.tmp; \
sed 's,\(hello\)\.o[ :]*,\1.o hello.d : ,g' < hello.d.tmp > hello.d; \
rm -f hello.d.tmp
cc -c -o hello.o hello.c
cc -c -o main.o main.c
cc -o world.out hello.o main.o

make会提示找不到hello.dmain.d,不过随后自动创建出hello.dmain.dhello.d内容如下:

1
hello.o hello.d : hello.c

上述规则有两个目标文件,实际上相当于如下两条规则:

1
2
hello.o : hello.c
hello.d : hello.c

main.d内容如下:

1
main.o main.d : main.c hello.h

因此,main.o依赖于main.chello.h,这个依赖关系就和我们手动指定的一致。

改动hello.h,再次运行make,可以触发main.c的编译:

1
2
3
4
5
6
7
$ make
rm -f main.d; \
cc -MM main.c >main.d.tmp; \
sed 's,\(main\)\.o[ :]*,\1.o main.d : ,g' < main.d.tmp > main.d; \
rm -f main.d.tmp
cc -c -o main.o main.c
cc -o world.out hello.o main.o

在实际项目中,对每个.c文件都可以生成一个对应的.d文件表示依赖关系,再通过include引入到Makefile,同时又能让make自动更新.d文件,有点蛋生鸡和鸡生蛋的关系,不过,这种机制能正常工作,除了.d文件不存在时会打印错误,有强迫症的同学肯定感觉不满意,这个问题我们后面解决。

参考源码

可以从GitHub下载源码。

小结

利用GCC生成.d文件,再用include引入Makefile,可解决一个.c文件应该如何正确触发编译的问题。

查看官方手册:

完善Makefile

上一节我们解决了自动生成依赖的问题,这一节我们对项目目录进行整理,把所有源码放入src目录,所有编译生成的文件放入build目录:

1
2
3
4
5
6
7
<project>
├── Makefile
├── build
└── src
├── hello.c
├── hello.h
└── main.c

整理Makefile,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
SRC_DIR = ./src
BUILD_DIR = ./build
TARGET = $(BUILD_DIR)/world.out

CC = cc
CFLAGS = -Wall

# ./src/*.c
SRCS = $(shell find $(SRC_DIR) -name '*.c')
# ./src/*.c => ./build/*.o
OBJS = $(patsubst $(SRC_DIR)/%.c,$(BUILD_DIR)/%.o,$(SRCS))
# ./src/*.c => ./build/*.d
DEPS = $(patsubst $(SRC_DIR)/%.c,$(BUILD_DIR)/%.d,$(SRCS))

# 默认目标:
all: $(TARGET)

# build/xyz.d 的规则由 src/xyz.c 生成:
$(BUILD_DIR)/%.d: $(SRC_DIR)/%.c
@mkdir -p $(dir $@); \
rm -f $@; \
$(CC) -MM $< >$@.tmp; \
sed 's,\($*\)\.o[ :]*,$(BUILD_DIR)/\1.o $@ : ,g' < $@.tmp > $@; \
rm -f $@.tmp

# build/xyz.o 的规则由 src/xyz.c 生成:
$(BUILD_DIR)/%.o: $(SRC_DIR)/%.c
@mkdir -p $(dir $@)
$(CC) $(CFLAGS) -c -o $@ $<

# 链接:
$(TARGET): $(OBJS)
@echo "buiding $@..."
@mkdir -p $(dir $@)
$(CC) -o $(TARGET) $(OBJS)

# 清理 build 目录:
clean:
@echo "clean..."
rm -rf $(BUILD_DIR)

# 引入所有 .d 文件:
-include $(DEPS)

这个Makefile定义了源码目录SRC_DIR、生成目录BUILD_DIR,以及其他变量,同时用-include消除了.d文件不存在的错误。执行make,输出如下:

1
2
3
4
5
$ make
cc -Wall -c -o build/hello.o src/hello.c
cc -Wall -c -o build/main.o src/main.c
buiding build/world.out...
cc -o ./build/world.out ./build/hello.o ./build/main.o

可以说基本满足编译需求,收工!

参考源码

可以从GitHub下载源码。

小结

除了基础的用法外,Makefile还支持条件判断,环境变量,嵌套执行,变量展开等各种功能,需要用到时可以查询官方手册



留言與分享

oracelDB基础

分類 database, oracleDB

OracleDB 笔记整理

1. SQL 分类

分类 说明 关键字
DML (Data Manipulation Language) 数据操作语言 SELECT, INSERT, UPDATE, DELETE, MERGE
DDL (Data Definition Language) 数据定义语言 CREATE, ALTER, DROP, RENAME, TRUNCATE, COMMENT
DCL (Data Control Language) 数据控制语言 GRANT, REVOKE
事务 (Transaction) 事务控制 COMMIT, ROLLBACK, SAVEPOINT

2. OracleDB 构成

2.1 基本构成

OracleDB 由 Oracle实例(instance)Database 构成:

  • 实例(Instance) 由:
    • SGA(System Global Area):内存区域
    • Background Process:后台进程
  • Database 由:
    • 控制文件(制御ファイル)
    • REDO 文件(REDO ログ)
    • 数据文件(データファイル)

2.2 进程视角

除了实例中的 Background Process,还包括:

  • 用户进程
  • 服务器进程
  • 监听进程

2.3 工具列表

安装和升级相关工具

工具名 用途
Oracle Universal Installer (OUI) 安装 Oracle 软件
Oracle Database Configuration Assistant (DBCA) 创建数据库
Oracle Database Upgrade Assistant (DBUA) 升级现有数据库到新版本

网络相关工具

工具名 用途
Oracle Net Manager (netmgr) 配置 Oracle 网络
Oracle Net Configuration Assistant (netca) 配置 Oracle 网络

实例和数据库管理工具

工具名 用途
Oracle Enterprise Manager (EM) 管理 Oracle DB
SQL*Plus SQL 命令行工具
SQL Developer 图形化数据库管理工具
Recovery Manager (RMAN) 数据库备份、恢复、复原
Oracle Secure Backup 备份管理
Data Pump 数据库间高速数据传输
SQL*Loader 外部文件数据批量导入

3. 安装

3.1 OUI 功能

  • 显示已安装的 Oracle 软件
  • 安装新软件
  • 删除软件
  • 查看在线帮助
  • 检查安装需求

3.2 系统要求

  • 内存:1GB
  • SWAP:1.5GB
  • 硬盘空间
    • 最小 1GB
    • 一般需要 6.1GB

3.3 创建用户和组

  • 软件所有者:Oracle 用户
  • Oracle Inventory Group:用于管理 Oracle 软件
  • DB 管理组
    • OSDBA:数据库管理员组
    • OSOPER:受限制的数据库管理员组

3.4 环境变量

变量名 说明
ORACLE_BASE Oracle 主目录
ORACLE_HOME Oracle 软件安装位置
ORACLE_SID 系统标识(实例名)
LD_LIBRARY_PATH 共享库路径(如 $ORACLE_HOME/lib

3.5 安装脚本

脚本名 用途
orainstRoot.sh 生成 inventory pointer 文件
root.sh 生成 oratab 并设置环境变量 (oraenvcoraenv),指定 dbstartdbshut 脚本

3.6 创建数据库(DBCA)

3.6.1 指定 Global DB 名

格式:database_name.domain_name

3.6.2 Enterprise Manager 选项

  • Database Express:单数据库管理
  • Cloud Control:集中管理多个数据库(需预先安装 Cloud Control)

3.6.3 存储类型

类型 说明
文件系统 使用操作系统文件
ASM (Automatic Storage Management) 文件存储在 ASM 磁盘组,需额外实例

3.6.4 数据库文件位置

  • 使用模板的文件位置
  • 所有数据库文件共享文件夹
  • Oracle Managed Files:由 Oracle 直接管理文件

3.6.5 模板

模板包含以下信息:

  • 数据库选项
  • 初始化参数
  • 存储属性(数据文件、表空间、控制文件、REDO 日志属性)
模板分类
模板类型 说明
通用事务处理(默认) 适用于 OLTP 场景
数据仓库 (Data Warehouse) 适用于复杂查询和大数据处理
自定义模板 用户自定义配置
模板形式
形式 说明
Sheet Template 包含现有数据库结构和物理文件
Non-Sheet Template 仅包含数据库特性

4. EM Express

4.1 功能

  • 提供数据库管理功能(不包括启动/停止数据库
  • 如需启动/停止数据库,需使用 Oracle Enterprise Manager Cloud Control

4.2 手动配置 EM Express

  1. 启动监听进程
  2. 初始化 DISPATCHERS 参数(设置 PROTOCOL=TCP):
    1
    dispatchers="(PROTOCOL=TCP)(SERVICE=<sid>XDB)"
  3. 设置端口(需 SYSDBA 权限):
    1
    EXEC DBMS_XDB_CONFIG.setHTTPSPORT(5500);

4.3 使用 EM Express

4.3.1 查询 EM Express 端口

1
SELECT DBMS_XDB_CONFIG.getHTTPSPort FROM DUAL;

4.3.2 权限分配

权限 说明
EM_EXPRESS_BASIC 只读模式
EM_EXPRESS_ALL 完全权限

4.4 连接数据库

4.4.1 SQL*Plus 连接方式

  1. 运行 oraenv 设置环境变量:
    1
    . oraenv
  2. 启动 SQL*Plus(/nolog 表示不登录数据库):
    1
    sqlplus /nolog
  3. 连接数据库:
    1
    CONNECT <用户名>/<密码> [AS SYSDBA | AS SYSOPER]
  4. 其他功能:
    • 执行 SQL 脚本:
      1
      @<sql文件名>
    • 执行操作系统命令:
      1
      HOST <命令>  # 例如:HOST ls

4.4.2 SQL Developer 连接方式

  1. 运行 oraenv 设置环境变量:
    1
    . oraenv
  2. 启动 SQL Developer:
    1
    2
    cd $ORACLE_HOME/sqldeveloper
    sh sqldeveloper.sh
  3. 功能:
    • 普通模式:查看、创建、编辑、删除表、视图等对象
    • DBA Navigator:连接 DBA 用户后可启动/停止数据库

5. Oracle 网络构成

5.1 Oracle Net 概述

  • 功能:提供网络服务
  • 安装方式:随 Oracle 数据库软件或客户端一同安装
  • 通信模式
    • 客户端-服务端模式:两端均需安装 Oracle Net
    • 客户端-Web 服务器-DB 服务器模式:客户端和 Web 服务器需安装 Oracle Net

5.2 通信条件

角色 要求
DB 服务端 1. 网络服务器在线
2. 已安装 Oracle DB
3. 支持 TCP/IP 协议
4. 监听进程已启动
客户端 1. 网络服务器在线
2. 已安装 Oracle 客户端
3. 支持 TCP/IP 协议

5.3 监听进程

  • 自动安装:通过 OUI 初始化 DB 时,NetCA 会默认安装监听进程

  • 作用:处理客户端连接请求(连接建立后不再参与通信)

  • 关键命令

    命令 功能
    lsnrctl start <监听进程名> 启动监听进程
    lsnrctl stop <监听进程名> 停止监听进程
    lsnrctl status <监听进程名> 查看状态
    lsnrctl services <监听进程名> 查看支持的服务
    LSNRCTL> set current_listener <名称> 切换监听进程(需交互模式)

5.4 客户端构成

  • 数据库连接示例
    1
    2
    3
    CONNECT hr@(DESCRIPTION = 
    (ADDRESS = (PROTOCOL = TCP)(HOST = proj1-sv)(PORT = 1521))
    (CONNECT_DATA = (SERVICE_NAME = sales.edifist.com)))

5.5 命名方法

类型 存储位置 说明
本地命名 客户端 tnsnames.ora 网络服务名映射存储在本地文件
LDAP 基准 LDAP 服务器 集中管理服务名映射
简易连接命名 直接使用 TCP/IP 连接字符串(如 connect scott@host名/服务名
外部命名 第三方命名服务(如 NIS) 依赖非 Oracle 服务

本地命名配置方法

1
netca  # 启动图形化配置工具

简易连接示例

1
2
sqlplus /nolog
SQL> CONNECT hr/hr@oracle_sv/orcl.edifist.com

6. 数据库服务器架构概要

6.1 核心组件

  • Oracle 实例:内存结构(SGA) + 后台进程
  • Oracle 数据库:物理文件集合

6.2 SGA (System Global Area) 构成

组件 功能
数据库缓存 存储数据块(Buffer Cache)
REDO 日志 Buffer 记录数据变更历史
共享池 缓存 SQL/PLSQL 解析结果、执行计划、数据字典
Large Pool 可选,用于共享服务器模式、并行查询、RMAN 备份等
Java Pool 支持 Java 虚拟机(JVM)
Stream Pool 支持 Oracle Stream 数据复制

6.3 关键后台进程

进程 名称 功能
SMON System Monitor 实例恢复(如崩溃后自动修复)
PMON Process Monitor 清理异常终止的用户进程资源
DBWn Database Writer 将脏缓冲区写入数据文件
CKPT Checkpoint 触发 DBWn 写入,更新控制文件(用于灾难恢复)
LGWR Log Writer 将 REDO 日志缓冲区写入磁盘
ARCn Archiver 归档 REDO 日志
MMON Manageability Monitor 执行 AWR(自动工作负载仓库)相关任务

6.4 用户与服务器进程

  • 用户进程:运行应用程序(如 SQL*Plus)
  • 服务器进程:处理用户提交的 SQL 查询

6.5 PGA (Program Global Area)

  • 特点:非共享内存,仅限单个进程访问
  • 用途:存储 SQL 执行时的排序区、会话信息等

7. 实例启停

7.1 启动方法

  • SQL*Plus(STARTUP 命令)
  • Windows 服务管理器
  • SQL Developer
  • Enterprise Manager Cloud Control

7.2 启动状态迁移

状态 描述
NOMOUNT 读取参数文件,分配 SGA,启动后台进程(控制文件未打开)
MOUNT 打开控制文件(获知数据文件/日志文件路径,但未打开文件)
OPEN 打开所有数据文件和 REDO 日志,数据库可用

7.3 权限要求

权限 能力
SYSDBA 完全控制(包括启停实例、用户授权)
SYSOPER 仅限启停实例(无权管理用户对象)

连接示例

1
2
3
4
CONNECT 用户名/密码 AS {SYSOPER | SYSDBA}
sqlplus /nolog
CONNECT sys AS sysdba
STARTUP

7.4 停止流程

  1. 关闭数据库(OPEN → CLOSED)
    • 执行 Checkpoint,写入数据文件和 REDO 日志
    • 关闭数据文件和 REDO 日志(控制文件仍打开)
  2. 卸载数据库(CLOSED → DISMOUNT)
    • 实例与数据库分离
  3. 停止实例(DISMOUNT → SHUTDOWN)
    • 终止后台进程,释放 SGA 内存

7.5 停止模式对比

行为 NORMAL TRANSACTIONAL IMMEDIATE ABORT
接受新连接 × × × ×
等待当前会话结束 × ×
等待当前事务结束 ×
执行 Checkpoint 后关闭 ×

7.6 初始化参数文件

类型 名称 特点
静态 PFILE(文本文件) 手动编辑,需重启生效
动态 SPFILE(二进制文件) 支持在线修改(ALTER SYSTEM),优先使用

留言與分享

  • 第 1 頁 共 1 頁
作者的圖片

Kein Chan

這是獨立全棧工程師Kein Chan的技術博客
分享一些技術教程,命令備忘(cheat-sheet)等


全棧工程師
資深技術顧問
數據科學家
Hit廣島觀光大使


Tokyo/Macau