Linux基础-makefile、Linux系统函数
makefile
- 什么是makefile?
- 项目代码管理工具,用于管理我们程序的源代码。
- 为何需要makefile?
- 比如程序有100个文件,那么编译时:
gcc *c .....
,由于各个文件的目录还不同,那么gcc就很长,很复杂 - 因此makefile就是为了解决这个问题而生
- makefile可以把所有代码编译的命令都写到makefile中
- 只需要执行makefile命令就可以完成编译了
- 比如程序有100个文件,那么编译时:
- makefile这么复杂谁来编写?
- 通常不需要普通程序员编写,通常是项目管理人编写
- 普通程序员只需要编写自己的模块
- makefile的本质
- 就是一个文件,内部写了编译程序的步骤
编写一个最简单的makefile
- makefile的命名规则
- 方式1:
makefile
- 方式2:
Makefile
- 方式1:
- 创建makefaile文件:
vi makefile
- makefile编写规则
- 规则中的三要素
- 目标
- 依赖
- 命令
- 固定格式:
目标:依赖条件 (tab缩进)命令
-
代码举例:
app:add.c main.c gcc add.c main.c -o app
- 编写完后,保存退出vi
- 规则中的三要素
- 执行makefile
-
输入make,就能输出makefile中编写的编译命令,然后执行,并且生成可执行文件app
gcc add.c main.c -o app
-
-
上面的写法有个缺点,每次执行一次make都会把所有的文件都编译一遍,如果我仅仅修改了其中的某一个文件,其他文件都会重新编译,比较浪费,因此需要优化makefile,修改如下
app:add.o main.o gcc add.o main.o -o app main.o:main.c gcc -c main.c add.o:add.c gcc -c add.c
- 上面一共有3条规则
- 第一条规则用于生成终极目标app
- 下面的几个叫做子目标,下面的子目标跟第一个目标的依赖有关系
- 当要生成终极目标app时,他就会查找依赖,发现依赖add.o没有,向下查找对应的规则,直到查找到有一条规则是生成这个依赖的,main.o也一样。
- 子目标就是用来生成第一个目标的依赖条件的。
- 当所有的依赖都找到了,才会执行第一个目标下面的命令
-
再次输入make命令时执行如下:
gcc -c main.c gcc -c add.c gcc add.o -o app
-
如果修改了main.c,然后再执行make
gcc -c main.c gcc add.o -o app
- 说明已经实现了目的
- makefile怎么知道哪些文件被修改了呢?
- 如果没有修改,那么终极目标文件app肯定是时间最晚的;一旦某个.c文件被修改,那么这个文件的时间就大于目标文件app
- 用终极目标的app时间与其他.c文件时间进行比较,来进行判断
- 如下图
- 上面一共有3条规则
makefile中的变量
-
看下面makefile中的内容
app:add.o main.o gcc add.o main.o -o app main.o:main.c gcc -c main.c add.o:add.c gcc -c add.c
- 可以看到第一条规则
add.o main.o
重复了,如果很多文件这里就要写更多重复了 - 第二条、第三条,也非常像似,只是文件的名字不一样
- 那么可不可以简化呢?—-使用变量代替
- 可以看到第一条规则
- makefile中的变量
- makefile中的普通变量
- makefile中的变量是不需要类型的,直接输入一个标识符即可。
- makefile中从变量取值:
$(变量名)
- 多行替换命令:
:3,4s/app/$(target)
- 将第3、4行的app替换成
$(target)
- 将第3、4行的app替换成
-
makefile中的占位符%
app:add.o main.o gcc add.o main.o -o app %.o:%.c gcc -c $< -o $@
%.o:%.c
这个是什么意思呢?- 当生成中级目标时,先查找第一个依赖add.o,发现没有,就下子目标中查找
- 此时找到
%.o:%.c
,就会将里面的%
替换成这个依赖的名字add
- 替换之后就如下:
add.o:add.c
- 其他依赖依次进行。
-
因此即使有n个.c文件,只需要写一个公式即可
%.o:%.c gcc -c $< -o $@
gcc -c $< -o $@
是什么意思呢?
- makefile中的自动变量
$<
:自动变量,代表当前规则中的第一个依赖$@
:自动变量,代表当前规则中的第一个目标$^
:自动变量,代表当前规则中的所有依赖- 这3个变量,只能在规则中的命令中使用
-
比如这个目标:
app:add.o main.o
,对应命令如下gcc -c $^ -o $@
- makefile中自己维护的变量
- 特点:都是大写
- 小写的变量都是用户自定义的,大写的变量是系统维护的。
- 系统维护的变量,有些事有默认值的,有些没有
-
比如:
CC = gcc CPPFLAGS = -I cPPFLAGS:预处理器需要的选项,比如-I CFLAGS:编译的时候使用的参数 -Wall -g -c LDFLAGS:链接库使用的选项 -L -l
- makefile中的普通变量
-
makefile文件简化后如下:
obj=add.o main.o //变量obj target=app //变量target CC=gcc //makefile自己维护的变量 $(target):$(obj) $(CC) $(obj) -o $(target) %.o:%.c $(CC) -c $< -o $@
makefile中的函数
- makefile中提供了很多函数
- 在makefile中所有的函数都是有返回值的。
- 这个变量
obj=add.o main.o
,如果项目中有n个.c,这里就要写n个.o - 函数调用样式:
$(函数名 参数, 参数, ...)
-
这样就可以使用makefile中的函数
target=app //获取指定目录下的所有.c文件 //&(函数名 路径+文件类型) //查找某个目录下面的某种类型的文件 src=$(wildcard ./*.c) //将某个目录下的所有.c,替换成某个 目录下的所有.o obj=$(patsubst ./%.c, ./%.o, $(src)) CC=gcc CPPFLAGS = -I $(target):$(obj) $(CC) $(obj) -o $(target) %.o:%.c $(CC) -c $< -o $@
-
每次修改源文件,再次执行make之前,都要把之前生成的.o与可执行文件app全部删除,才能重新执行make,这个很麻烦,也能通过makefile设置,新增一条规则如下:
//防止当前目录下有个同名文件叫clean,如果没这句,执行make clean就不行了 .PHYONY:clean //删除所有.o文件和最终的可执行目标文件target clean: rm $(obj) $(target) -f
- 保存makefile后直接输入命令
make clean
即可 make 目标
,只执行makefile的对应目标命令。
- 保存makefile后直接输入命令
-
makefile最终内容如下:
#obj=add.o main.o target=app src=$(wildcard ./*.c) obj=$(patsubst ./%.c, ./%.o, $(src)) CC=gcc CPPFLAGS = -I $(target):$(obj) $(CC) $(obj) -o $(target) %.o:%.c $(CC) -c $< -o $@ .PHYONY:clean clean: rm $(obj) $(target) -f
Linux系统函数
- linux中的系统库函数与C语言中的系统库函数相对应
- 调用fopen函数获取一个
FILE *
类型的指针变量,其余的函数都需要传入这个变量,那个这个FILE *
本质是什么呢? - FILE的本质是一个结构体,这个结构体组要内容有三部分
- 文件描述符(整型值):索引到对应的磁盘文件
- 使用fopen打开某个文件,这个文件对应磁盘上的某个位置,就是使用文件描述符找到并访问磁盘上的某个文件
- 文件读写指针位置:读写文件过程中指针的实际位置
- 文件被打开之后肯定有个文件指针,文件指针指向文件起始位置,当写入内容时,文件指针向后移动,写到哪文件指针就指到哪
- 这就是为什么,当写一些内容到文件,但是文件没有保存关闭,然后就直接使用文件指针去读取内容,读取不到值的原因,指针指向最后,当然读取不到
- 必须重置文件指针的位置,重置到开始位置,才能读取到
- I/O缓冲区(内存地址):通过寻址找到对应的内存块
- 在标准C库函数中都提供了I/O缓冲区
- 缓冲区默认大小8KB
- 为什么需要一个I/O缓冲区呢?
- 理论上通过C库函数读一个(fgetc)然后写到磁盘中(fputc),但实际上不是这样
- 实际上是每次读一个字符,然后放到缓冲区,然后再读,直到缓冲区满了之后才会放入到磁盘
- 这样能够减少操作硬盘的次数从而提高效率,内存的效率比硬盘效率高太多。
- 把内存中的数据刷到硬盘上,通常有3种情况
- 用户主动执行刷新缓冲区:
fflush
- 缓冲区已满
- 正常关闭文件
- fclose
- return (main函数)
- exit(main函数)
- 用户主动执行刷新缓冲区:
- 注意:Linux的系统函数是没有这块缓存的。
- linux函数的缓存需要程序员提供
- C库函数是内部封装好的
- 文件描述符(整型值):索引到对应的磁盘文件
虚拟地址空间
- 每一个应用程序执行起来之后,操作系统都会给他分配一个对应的虚拟地址空间
- 这个虚拟地址空间时在虚拟内存中的,虚拟内存空间不是内存而是占用的硬盘空间!!!
- 在32位操作系统中,给程序分配的是0-4G(2的32次方)的虚拟地址空间
- 其中0-3G被称为用户区,专门用于程序员使用的。
- 3-4G为Linux 的内核区,不允许程序员访问操作。
Linux的内核区
- Linux的内核区可以做
- 内存管理
- 进程管理
- 设备驱动管理
- VFS虚拟文件系统
-
上面讲的文件描述符到底是什么呢?
- 文件描述符就位于内核区,内核区有进程管理,进程管理中有个PCB进程控制块,在PCB进程控制块中有个文件描述符表
- 文件描述符表本质是一个数组,数组的大小为0~1023,每个位置代表能够打开一个文件
- 每打开一个文件就会在表中占有一个位置
- 一个进程最多能够打开1024个文件,但是用户能够打开的有1024-3个文件,因为表的前3个(标准输入、标准输出、标出错误)总是处于被打开的状态,被占用
- 没打开一个新文件,则占用一个文件描述符,而且使用的是空间最小的描述符
- 打开A文件,占用3;打开B文件占用4;若此时关闭了A文件,3被空出来了,再打开C文件时,会占用3.
Linux的用户区
- 首先可执行文件a.out被执行,然后操作系统给这个应用程序分配一个虚拟地址空间。
- 用户区主要分为
- 0-4k受保护部分
- ELF段
- 堆空间
- 共享库
- 栈空间
- 命令行参数
- 环境变量
- ELF
- Linux下可执行文件格式叫ELF
-
查看一个可执行程序的文件格式:
file 可执行文件
,比如,输入file app
,输出如下app: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, for GNU/Linux 3.2.0, BuildID[sha1]=48823ee318ab5fec0c08f6b6ea62533f7dc42689, not stripped
- EFL内部都有哪些区域呢?
.text
代码段,二进制机器指令:- 把源代码放到该段
.data
:存放已经初始化的的全局变量.bss
:未初始化的全局变量- 其他段:只读数据段、符号段等
- 可执行程序的执行过程
- CPU在执行程序的时候从main函数开始,即从代码段开始,如果遇到全局变量的就放在
.data
或者.bss
;如果遇到局部变量就放在栈空间;如果遇到了malloc或者new这放到堆空间;如果遇到了调用C标准函数比如fread,这个是动态库函数,那么就会加载相应的动态库到共享区 - 栈空间分配地址从大到小
- 堆空间分配地址从小到大
- 共享区域的加载是无序的,见空插针,所以生成动态库的时候就要生成一个与位置无关的库。静态库就直接放到代码段
.text
- CPU在执行程序的时候从main函数开始,即从代码段开始,如果遇到全局变量的就放在
- 命令行参数主要用于存放main函数的参数
- 环境变量(env),用于存储当前进程的所有环境变量
- cpu为什么要使用虚拟地址空间与物理地址空间映射? 解决了什么样的问题?
- 方便编译器和操作系统安排程序的地址分布
- 程序可以使用一系列相邻的虚拟地址来访问物理内存中不相邻的大内存缓冲区。
- 比如一个进程需要内存中有一块10M的连续空间,但是呢? 物理内存中有大于10m的空间,但是不是连续的。这时如果有了虚拟内存,就可以通过虚拟内存来映射,通过这个映射功能就能够解决这个问题。
- 方便进程之间隔离
- 不同进程使用的虚拟地址彼此隔离。一个进程中的代码无法更改正在由另一进程使用的物理内存。
- 方便OS使用可怜的内存
- 程序可以使用一系列虚拟地址来访问大于可用物理内存的内存缓存区。当物理内存供应量变小时,内存管理器会将物理内存页(通常大小为4k)保存到磁盘文件。数据或代码页会根据需要在物理内存与磁盘之间移动
- 方便编译器和操作系统安排程序的地址分布
- 虚拟内存空间的本质
- 程序启动之后,从硬盘上会有一块虚拟内存分配出来,这个内存叫虚拟地址空间。
- 实际上当执行一个程序之后,内存并没有少4个g。
- 虚拟内存就是有4个g的空间供你操作,实际上这个程序用了多少空间,内存才少多少空间。
- 用通俗的话来说,先用硬盘给你提供一个4G的操作环境,而在操作过程中实际需要的空间,才会占据物理内存。
C库函数与系统函数的关系
-
- 标准C函数printf,怎么就能把字符输出到屏幕上呢?
- prinf不能直接操作硬件
- printf的本质
- printf内部调用标准输出:(stdout):FILE*
- 本质是文件操作
- 文件指针内部有
- FD:文件描述符
- FP_POS:文件指针
- BUFFER:缓冲区
- 其余如上图。