makefile的原理
问题需求
之前我们讲过C语言代码主要是经过编译和链接两个步骤生成目标文件,但是在编译的时候我们可能需要进行多条指令的输入,要对main函数所在的文件进行汇编,再将我们定义的函数文件进行汇编,分别形成*.o
文件,然后再使用gcc命令将两个汇编文件链接成目标文件,在这个过程中我们就需要手动敲三条指令,这样非常消耗人力,因此我们想到用脚本的方式,将这几条指令封装再一起,只要源文件发生改变就执行脚本,顺序执行这些编译指令,但是如果我们将指令顺序的写好,假如我们的源文件只有部分发生改变,一部分没发生改变,那没发生改变的那部分其实是不用进行编译的,但是我们如果只是将指令简单封装成顺序执行的脚本,那么就无法实现我们说的精准编译,会造成重复编译,从而增加编译所需时长,然而makefile
就可以帮我们解决这个问题
makefile
:增量编译生成代码
只有目标文件不存在或者目标比依赖旧才会执行命令
目标文件:生成的文件
依赖:生成此目标文件需要操作的文件
makefile的实现
(1)名字必须是Makefile / makefile
(2)规则的集合【规则:一个目标文件,0 ~ 多个依赖文件,0 ~ 多个命令,每个命令之前要加一个tab键】
(3)把最终要生成的文件作为第一个规则的目标makefile基本使用
(1)首先我们再文件里面创建两个原文件,并且再main文件里面调用了只在main里面进行声明但是定义再add.c里面的函数add
(2)我们再当前文件下创建一个名为Makefile的文件,并且再里面添加相对应的规则
按照规则要求将生成最终文件main
的文件作为第一个规则,并且在:后面指定由哪些文件来执行指令得到目标文件
第二行先输入tab
在输入要将main.o
文件和add.o
文件生成main
文件的操作指令
下面就是相对应的文件以及相应的操作命令
执行make
命令,编译器便会按照增量编译原理进行编译
假如我们编译之后文件我们都没有改变过也没有使用touch
指令区操作过文件,那么此时执行make
指令就不满足增量编译原理,那么就不会进行编译
如果我们使用例如touch
命令或其他修改文件的命令去操作文件,那么执行make
就会按照增量编译原理对修改的文件进行重新编译
伪目标
(1)目标不存在
(2)执行了命令生成不了目标
如果每次make都一定要执行的指令我们就可以使用伪目标,因为我们在执行makefile指令时,我们首先会检测文件夹中是否存在我们所需要的目标文件,并且会检测目标文件的和其依赖的文件哪个更新,如果我们所需要的文件不存在,或者依赖文件比目标文件新,那么make就会执行相应的指令,因此伪指令就是在其执行指令之后,不会生成对应的目标文件,因此每次执行make都会满足调用伪目标执行的操作
clean指令清除main.o和add.o文件,每次执行
make clean
都会执行clean 因为文件中不存在clean文件,但如果文件中村内在clean文件,我们需要删除此文件才能执行此命令
执行make rebuild
就会先执行clean,在执行main的相关指令
为了让开发者更好的找到Makefile里面的伪目标,我们通常用.PHONY:
来指定伪目标
增加makefile的通用性
我们发现假如我们源文件名更改,其他依赖的文件名发生改变,或者我们想要另一组文件名不同但是编译结构类似的代码使用makefile这时我们的选择就是重新创建makefile文件,但是这也会给我们造成很大的工作量,因此我们想到可以用变量的形式,和函数一样,用变量代替里面具体执行的逻辑,然而变量的改变并不会影响程序运行逻辑,因此我们考虑到用变量代替文件名,以此来提高代码的通用性
更正:以下图面中第三行
$(main)
更改为$(OUT)
变量也分为:
(1)自定义变量变量名:=值
(所有值都是字符串类型) 引用变量用$(变量名)
(2)预定义变量
此时我们发现其编译出来使用的命令全是cc,不是gcc,因此我们需要将cc替换掉
(3)自动变量 同一个变量名 值随着规则变化而变化
在上面修改的Makefile文件中,我们可以发现当我们当我们拷贝Makefile文件到其他程序中时,我们还需要改3行的数据,分别时2,6,8行的数据,这样其实也会给我们带来一定的工作量,因此我们在次发现下面两次指令都是相同的,并且他们的目标文件名都是死第一个命令的依赖文件名,因此我们可以用%来通配main和add,可以修改为一下这样的情况
但是这还不够,这样子还需要我们对Makefile
文件进行修改,我们想让Makefile
文件直接可以不用修改;这就需要依靠wildcard
通配符,其作用时从当前目录的所有文件中,取出符合要求的文件名,这样我们就可以取出当前目录下的所有*.c
的文件,但是我们需要的不是*.c
的文件,我们需要的时*.o
的文件,这就需要我们用patsubst
—pattern substitude对我们的文件名进行文字替换
这样就可以让Makefile
文件自动读取依赖文件,自动生成,而无需修改文件内部的内容
正常执行命令
扩充:假如我们在一个文件夹下,由多个main函数,我们想用Makefile
进行编译,需要各个.c
文件生成自己的可执行文件,这个时候该i如何书写
文件
在Linux系统中万物皆文件
狭义:存储在外部存储介质上的数据集合
广义:传输速度慢,容量大,持久存储
文件类型:
普通文件,目录文件,软链接
字符设备文件 --------鼠标
块设备文件------------磁盘
管道文件----------------通信
socket-------------------网络通信
使用带参数的宏定义封装指定代码
这样的方式能够减少我们的代码长度,能够让我们清晰了解代码结构
源代码:
不同的执行结果,不带参数的会直接报错,带参数的可直接通过
我们可以直接可以将带参数的宏定义放在一个头文件里,这样就可以使用宏定义直接实现上面的功能
要将这个定义为43func.h
的头文件定义在/usr/include
中,这样源文件可以直接调用
以下的代码中我们可以看出我们需要不断的对打开的文件进行检查其是否存在,因此我们可以把重复的代码定义成带参数的宏定义,这里我们使用fopen
的r读模式
使用带参数的宏定义来简化代码,减少代码长度
fopen追加模式
a
append 只写追加 ------->默认从文件结尾写入
a+
读写追加,打开时,处于文件的开始;写入时跳到文件的末尾
fseek()
可以移动流指针移动到流的开始
ftell()
当前流指针指向位置
改变文件属性
改变文件权限
chmod
系统 调用
chmod
它既可以在我们的shell
里面进行修改文件的权限,也可在代码中实现文件权限的修改,但是其修改文件权限是通过数字来指定文件的权限进行修改
前面两次调用目标函数chmod
我们发现都是报错了,第一次是因为我们没有给目标函数指定权限,并且目标函数也不存在,第二次存在目标函数,但是没有给其指定权限,所以也报错了,最后一次我们目标文件存在,并且指令目标文件权限和目标文件,成功将其权限修改
getcwd(系统调用)获取当前目录
getcwd
可以获取到我们在函数栈空间开辟的空间的地址,所以其返回的是一个指针变量,在使用getcwd
创建的buf数组是用来存储当前的目录的,因此如果我们给buf
分配的空间过小就会报错,并且getcwd
在使用的时候还需要给它传递数组的长度,因为数组在作为参数传递的时候其长度信息会丢失,所以需要传递长度,下面的代码就是我们使用getcwd
获取到的当前数组的工作目录
(1)buf
不为空,直接返回buf
我们可以看到返回的指针的地址和main
函数中变量的地址都是一致的,因此getcwd
确实可以返回我们当前空间的工作目录
(2)buf
为空,返回一个堆空间的地址
如果在调用getcwd
的时候buf
为空,那么getcwd
会在堆空间自动创建对应大小的内存空间,并且返回一个指向此内存空间的指针,这样我们的代码量就很少,并且也不用再去考虑buf
空间不够装下文件名的情况,但是这样使用getcwd
我们还必须要手动的释放堆空间,否则就会造成内存泄漏
chdir改变当前工作目录
cwd是进程的属性,我们当前使用chdir打印的工作目录都是源于首先是处于./chdir进程中,然而./chdir
是由shell
进程创建的因此之间是存在是父子关系,我们使用chdir改变的工作目录也只是改变子进程的工作目录,并没有改变shell的工作目录,只能修改子进程rmdir(移除目录) mkdir(创建目录)
我们使用mkdir
时要传递两个参数,第一个就是所要创建的文件名,第二个就是要指定创建文件目录的初始权限
成功创建文件目录,但是我们发现创建出来的目录文件其权限并不是像我们之前设定的777,这其实时因为每个重新创建的文件都会受到
umask
的影响,而改变最终生成文件的权限
rmdir
rmdir必须删除空文件
目录流
文件流中存在指针ptr,其会随着用户的读写操作进行后移,这样用户可以在不了解文件结构的情况下就可以通过简单的遍历访问所有数据
目录存在的目的就是为了能够找到文件
目录在磁盘中以链表的形式进行存储,每个链表节点(目录项,dirent[directory entry])存储了孩子的基本信息
目录流是目录在内存中的缓冲区,并且是一个带有ptr指针的链表
创建目录流
读取目录流
因为上面我们说过目录流其实就是一个携带ptr
自动移动指针的链表,因此我们调用readdir
函数,它会返回当前ptr
指针所指向的目录流元素,其就是一个结构体,并且readdir
会自动在数据段的静态区开辟空间来存放当前ptr
下的数据,因此在主调函数中我们不需要取创建目录流元素的存放空间,只需要定义指向其存放空间的指针即可
其ptr
指针会自动向后移动,因此不需要我们手动移动
销毁目录流
telldir和seekdir
我们在循环输出目录流结束后,如果我们想要输出或者返回到目录流起点,该怎么做呢,此时可以使用
telldir
和seekdir
来实现
telldir
会记录下当前目录流ptr
指向的元素
seekdir
可以将ptr指针指向指定的元素
首先我们构建这样的文件结构
因此dir目录流的链表结构为
.->..->file1->file2->dir2
ptr开始指向.
如果我们在循环的过程中使用telldir记录下file1
元素,最后在用seekdir就可以回溯到file1元素
但是我们看到结果并没有像我们所希望的返回file1
而是返回file1
之后的元素file2
,这是因为我们在使用readir之后ptr
指针就自动指向后面的元素,因此如果我们在执行了readir
之后再调用telldir
就只能获取到下一个元素
rewinddir
rewinddir
的作用是把ptr
回到最开始
stat
stat结构体对象
结构体stat与文件信息的对应
stat配合目录流实现ls -al
stat可以获取文件的信息,因此我们可以结合上面的目录流,实现对文件信息的获取,并且按照一定格式打印出来,这样我们就可以实现ls -l
的命令
我们可以看到我们已经能基本实现展示文件的相关信息,但是这些信息大多都是以数字的形式展示,我们还需要对其进行一定的转换,才能展示出直观的文件信息
(1)通过getpwuid
获取文件拥有者名称
(2)通过getgrgid
获取文件拥有者所在的组
(3)通过ctime
或gmtime
或localtime
将毫秒值转换为日期
ctime
的返回值为固定的格式末尾默认会自动携带一个和换行符号
ctime(&statbuf.st_mtime)
gmtime
格林尼治时间str
localtime
当地时间
经过修改得到的规范时间展示
实现tree命令
tree命令时一个深度优先遍历
使用目录流构建出的tree命令
不带缓冲的文件IO
因为我们操作文件文件的时候是使用库函数来进行操作的,然而库函数也是需要调用内核态的接口来实现的,那么我们最终都要调用内核接口,我们为何不直接就调用内核接口呢。我们直接调用内核接口这样一来,我们就可以不用再创建用户态缓冲区(文件流/FILE),我们可以直接使用内核态的文件对象中的内核文件缓冲区,直接进行读写,这样就相当于直接操作物理存储区域
但是这样操作的难点在于内核态的文件对象不好管理,但是由于这个文件对象是处于内核态,我们又不能将其地址直接传递给所属用户让其来进行操作、管理,这样很容易发生信息被恶意修改的情况,因此我们就使用了文件描述符
解决这一问题,其本质是,内核态再内核区维护一个指针数组,每个指针数组里面存放着对应用户的文件流首地址,而每个用户态拥有指向自己内核态文件流首地址指针数组的下标,用户只需要将自己的下标传入,就可以读取自己的文件流。
文件描述符
(重要!重要!重要!)
非负整数,用它来访问某个具体的文件对象
但是0,1,2这三个下标是不可以使用的,因为系统文件已经使用了这三个下标