目录
再识文件属性
查看文件属性的原理
初识inode
//输入指令查看文件属性
ls -li
//输入指令stat 文件名,看到更多信息
需要解释清楚它们,我们就需要了解清楚文件系统。
了解磁盘
在了解文件系统前,我们先了解最常见的存储介质,磁盘。
什么是磁盘
磁盘是计算机的主要存储介质,它可以存储大量二进制数据,并防止断电后数据丢失。早期,计算机中使用软盘,但现在普遍使用硬盘。与之相对应的便是内存,内存有掉电易失性。
磁盘的结构
注意点:
-
磁盘是我们计算机中的唯一的一个机械结构。
-
每个盘面都有一个磁头。
-
磁头和盘面是没有接触的,为了防止抖动。
磁盘的存储结构
我们将盘面进行划分:
注意点:
- 扇区的面积会不同,但都代表512byte空间,通过控制存储信息的密度。
注意点:
-
磁头是共进退的。
CHS寻址
-
磁盘寻址的单位是扇区(512byte)
-
磁头来回摆动用来定位在哪个磁道
-
确定磁道后,盘片旋转时,磁头定位扇区
-
先定位在哪一个磁道(柱面(Cylinder))。
-
再定位盘面(即对应的磁头(Head))。
-
最后定位在哪一个扇区(Sector)。
磁盘中定位任何一个扇区,采用的硬件的基本定位方式:CHS定位法。
磁盘的逻辑结构
磁盘在物理上是圆形的,我们能在逻辑上将其抽象为一个数组。
此时,我们只要知道扇区对应的下标就定位了该扇区。(操作系统中我们称这种地址为LBA地址)
假设:磁盘有4个盘面,每个盘面有10个磁道,每个磁道中划分了100个扇区,此时共有4000个扇区,每一个盘面有1000个扇区。
我们如何定位123号扇区在磁盘中的位置:
-
123/1000 = 0号盘面 H
-
123/100 = 1号磁道 C
-
123%100 = 23号扇区 S
通过这样的计算,我们就拿到了对应的CHS。
使用LBA地址的意义
-
便于进行管理,我们只需要有对应的下标就行了。
-
让操作系统的代码和具体硬件解耦,如果使用CHS,存储就和磁盘绑定在了一起,我们并不想强耦合。
理解文件系统
页框和页帧
虽然磁盘的访问的基本单位是512字节,但其依然很小,操作系统内的文件系统让我们同时对多个扇区进行读取,一般以1KB(两个扇区),2KB(4个扇区),4KB为基本单位。无论一次读取和修改多小的数据都会一次加载4KB空间到内存。这也就是我们所说的局部性原理。
注意点:
-
内存被划分成了4KB大小的空间,即我们所说的页框。
-
磁盘中的文件按照4KB大小划分好的块,即我们所说的页帧。
分治思想管理
我们的磁盘常用的大小是500GB,那么如何管理这500GB的空间呢?
我们要进行分区、分组两个步骤:
此时,我们只需要先将一个组管理好,然后就能通过CV(复制粘贴该组的管理方法到其他组)管理好这个分区了,能管理好当前分区自然也能管理好其他分区,最终我们就管理好了整个磁盘。
Linux ext2文件系统
磁盘文件系统图(内核内存映像肯定有所不同):
硬盘分区被划分为一个个的block。一个block的大小是由格式化的时候确定的,并且不可以更改。例如mke2fs的-b选项可以设定block大小为1024、2048或4096字节。而上图中**启动块(Boot Block)的大小是确定的**。
1. Block Group:ext2文件系统会根据分区的大小划分为数个Block Group。而每个Block Group都有着相同的结构组成.
2. 超级块(Super Block):存放文件系统本身的结构信息。记录的信息主要有:bolck 和 inode的总量,未使用的block和inode的数量,一个block和inode的大小,最近一次挂载的时间,最近一次写入数据的时间,最近一次检验磁盘的时间等其他文件系统的相关信息。Super Block的信息被破坏,可以说整个文件系统结构就被破坏了。
3. GDT,Group Descriptor Table:块组描述符,描述块组属性信息
4. 块位图(Block Bitmap):Block Bitmap中记录着Data Block中哪个数据块已经被占用,哪个数据块没有被占用。
5. inode位图(inode Bitmap):每个bit表示一个inode是否空闲可用。
6. i节点表(inode Table):存放文件属性。 如 文件大小,所有者,最近修改时间等(除了文件名)。
7. 数据区(Data Blocks):存放文件内容。
注意点:
1. inode为了彼此区分,每一个inode都有一个自己的ID。
2. 一个文件一个inode。
3. 文件名并不在inode中存储。
4. 查找一个文件,统一使用的是inode编号(文件名和inode存在映射关系)。
5. Super Block在每个组中都有一份,如果Super Block的信息被破坏可以通过其余的备份来恢复。
接下来回答一些问题:
假设inode的结构如下:
struct inode
{
int id;
mode_t mode;
uid;
size;
//......
int blocks[15];//存储的对应的Block的位置
}
其中有着一个blocks数组用来存储对应的文件内容所在的位置,此时也许你会好奇,就15个位置能存的下吗?答案是能。前13个位置与inode是一一对应,但下标为13和14的位置采用的是二级检索和三级检索。
下标13对应的Block中存放的是其余Block的位置。下标14的同理(再套一层)。
我们可以看到,一个目录是有自己对应的inode的,那么目录文件内容中存储的是什么呢?
答案是文件名和对应文件inode的映射关系。
如何证明?
一个目录内文件是否能删除取决于是否拥有对应的写权限!即我们需要取消对应文件名和inode的映射关系
我们都知道下载文件慢而删除文件快,其原理就在于:
-
当我们下载文件时,首先我们要在inode位图中将对应的indoe由0置1,接着我们需要在数据区不断的申请Block,并将Block位图对应的位置由0置1,还需要在inode中存储其对应的位置。
-
当我们删除文件时,很简单了,直接把inode位图和Block位图中对应的位置由1置0即可。
所以我们删除文件只是改掉了对应位图中的比特位,实际文件数据还是留在磁盘中的。
系统日志中会存储删除掉的文件的inode,我们可以找到该inode,恢复其在inode位图中的位置,再通过inode找到文件对应的Block,恢复Block位图中对应的位置,由此,我们便成功恢复了文件。
所以我们误删文件想恢复时最好别瞎操作,避免对原有的数据进行了覆盖导致无法恢复。
软硬链接
软链接
ln -s 源文件所在路径 建立的链接文件名
在上图中,我们建立了一个test的链接文件lktest,通过其,我们可以直接运行test.
ls -li //查看inode
两者文件的inode不同,lktest是一个全新的文件,那么其内部存储的是什么呢?
我们首先删除了test应用程序,我们发现软链接失效了,当我们再次在当前路径创建相同文件名的文件时,其又恢复了正常。由此观之,我们可以推断,其内部存储的是文件所在的路径。
注意点:
-
软链接文件大小很小,是因为其内部只是存储了文件的路径。
-
我们可以将其看作为Windows的快捷方式
如何解除软链接?
unlink 文件名
硬链接
ln 文件所在路径 链接文件名
我们创建了一个硬链接。
ls -li //查看inode
我们发现二者的inode竟然是相同的。并且其硬链接数变成了2.
删除test后,lktest依然能正常运行,硬连接数变为了1.
我们可以这样理解:
-
test和lktest的链接状态完全相同,他们被称为指向文件的硬链接。内核记录了这个连接数,inode 1054954 的硬连接数为2
-
创建硬链接时,文件名lktest与文件test的inode建立了映射关系。
-
我们在删除文件时干了两件事情:
-
1.在目录中将对应的记录删除。
-
2.将硬链接数-1,如果为0,则将对应的磁盘释放。
-
接着来看一个现象:
为什么创建目录初始的硬链接数是2?
Test目录里的.文件也是硬链接。同理..是上一个目录的硬链接。
Linux系统中,磁盘上的文件和目录被组成一棵目录树,每个节点都是目录或文件。如果我们自己在目录c中建立了目录a的硬链接,就可能会在搜索文件时造成死循环的问题。因此操作系统不让我们进行该操作。
文件的三个时间
通过stat指令查看文件的三个时间:
下面解释一下文件的三个时间:
-
Access 最后访问时间
-
Modify 文件内容最后修改时间
-
Change 属性最后修改时间
注意点:
-
Access时间一般不会立刻更新,因为文件的访问可能十分频繁,操作系统内有自己的更新策略。
-
Modify时间更改时Change时间往往也会一起变化,因为我们更改文件内容时实际上就造成了文件属性的变化,如文件大小等。
动静态库
动静态库的基本原理
-
预处理(进行宏替换):预处理功能主要包括宏定义,文件包含,条件编译,去注释等。生成XXX.i的文件
-
编译(生成汇编):要检查代码的规范性、是否有语法错误等,以确定代码的实际要做的工作,在检查无误后,把代码翻译成汇编语言。生成XXX.s的文件
-
汇编(生成机器可识别代码):汇编阶段是把编译阶段生成的“.s”文件转成目标文件。生成XXX.o文件
-
连接(生成可执行文件或库文件):在成功编译之后,就进入了链接阶段。生成可执行程序。
#include "mul.h"
#include "sub.h"
int main()
{
int a = 5;
int b = 3;
printf("a*b = %d\n",mul(a,b));
printf("a-b = %d\n",sub(a,b));
return 0;
}
动静态库实际上就是把我们编译出的.o文件(方法的实现)进行了打包。再给出对应的.h文件(对应的方法)。
我们把自己的库给别人用的时候,实际上需要给别人两个文件夹:
-
一个文件夹下面放的是一堆头文件的集合。
-
另一个文件夹下面放的是所有的库文件。
mkdir -p mylib/include
mkdir -p mylib/lib
cp -f *.a mylib/lib
cp -f *.h mylib/include
我们创建一个目录文件mylib,再在里面分别创建一个lib目录(放库文件),一个include目录(放头文件)。
我们发布自己的库时只需要发布打包压缩好的mylib就可以了。
制作一个自己的静态库
生成静态库
需要用到命令:
ar -rc libmymath.a mul.o sub.o
-
ar(archive)是gnu归档工具,rc表示(replace and create)
查看静态库中的目录列表
ar -tv libmymath.a
-
t:列出静态库中的文件
-
v:verbose 详细信息
进行打包:
mkdir -p mylib/include
mkdir -p mylib/lib
cp -f *.a mylib/lib
cp -f *.h mylib/include
使用静态库
gcc -o main main.c -I./mylib/include -L./mylib/lib -lmymath
使用自己制作的库,我们编译时需要增加三个选项:
-
-I
:指定头文件所在的路径。 -
-L:指定库文件所在的路径。
-
-l: 指明使用的库的名称。
注意点:
-
编译器并不清楚我们的头文件在哪里,其只会在当前路径和系统路径下寻找,因此我们需要添加路径。
-
库文件同上。
-
库的名称是什么?我们去掉前缀lib和后缀.a或者.so及其后面的版本号,就是库的名称。之所以要指定库的名称,是因为lib目录下可能有大量的库文件。
-
上面的三个选项后可加空格,并不影响。
运行成功
把库文件和头文件拷贝到系统路径下
sudo cp mylib/include/*.h /usr/include
sudo cp mylib/lib/*.a /usr/lib64
我们把库文件和头文件拷贝到系统路径下的过程实际上就是安装了这个库。
我们把库安装好后编译时还是需要指明库的名称,这也是为什么我们使用第三方库时总需要指明库名称的原因。
提示:我们并不推荐将自己写的库放进系统路径,因为我们的库并没有很好的经过验证,会对系统文件造成污染。
一个现象:
我们发现用自己的静态库编译的可执行程序居然显示是动态链接的。为什么?
-
因为形成一个可执行程序,往往不会仅仅依赖一个库,我们的程序当然还依赖了c语言库。
-
gcc默认是动态链接的(建议行为)。
我们可以用Makefile进行打包,方便我们后续的使用
libmymath.a: mul.o sub.o //生成库
ar -rc $@ $^
mul.o: mul.c
gcc -c -o $@ $^
sub.o: sub.c
gcc -c -o $@ $^
.PHONY:output //包装库
output:
mkdir -p mylib/include
mkdir -p mylib/lib
cp -f *.a mylib/lib
cp -f *.h mylib/include
.PHONY:clean //删除
clean:
rm -rf *.o libmymath.a mylib
我们就可以很轻松的进行操作了。
制作一个自己的动态库
生成动态库
生产动态库我们需要进行两步操作:
-
//编译时添加fPIC选项 gcc -fPIC -c -o mul.o mul.c gcc -fPIC -c -o sub.o sub.c
-
gcc -shared -o libmymath.so *.o
注意点:
-
shared: 表示生成共享库格式
-
fPIC:产生位置无关码(position independent code) (让动态库中的指定函数的地址是偏移地址,相对编址方式)
-
库名规则:libxxx.so
libmymath.so: mul.o sub.o //生成库
gcc -shared -o $@ $^
mul.o: mul.c
gcc -fPIC -c -o $@ $^
sub.o: sub.c
gcc -fPIC -c -o $@ $^
.PHONY:output //包装库
output:
mkdir -p mylib/include
mkdir -p mylib/lib
cp -f *.so mylib/lib
cp -f *.h mylib/include
.PHONY:clean //删除
clean:
rm -rf *.o libmymath.so mylib
使用动态库
与静态库时的使用相同
gcc -o main main.c -I./mylib/include -L./mylib/lib -lmymath
但我们发现我们编译出的可执行程序无法运行,为何?
我们使用-I
,-L
,-l
这三个选项都是在编译期间告诉编译器我们使用的头文件和库文件在哪里,但是可执行程序生成后就与编译器没有关系了,操作系统运行可执行程序时可不知道动态库在哪里。
我们通过ldd命令可以查看到:
-
拷贝.so文件到系统共享库路径下, 一般指/usr/lib64。
- 更改 LD_LIBRARY_PATH。
LD_LIBRARY_PATH是环境变量,里面存储了运行动态查找库时所要搜索的路径。
我们使用export指令将我们库的路径导入其中即可
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:所在的路径
但此种方法在重启shell后环境变量会重置而失效(环境变量的配置文件)。
-
配置/etc/ld.so.conf.d/
我们可以看到该目录下都是后缀为.conf的配置文件,这些配置文件当中存放的都是路径。我们只需要用库文件的路径生成.conf的文件,然后拷贝到该目录下即可。拷贝后需要使用ldconfig指令更新配置后才会生效。
-
在当前目录创建软连接
我们可以在当前路径下建立一个与库文件同名的软连接,查找动态库时会默认在当前路径查找。
动静态库的加载
-
静态库(.a):程序在编译链接的时候把库的代码链接到可执行文件中。程序运行的时候将不再需要静态库。
-
动态库(.so):程序在运行的时候才去链接动态库的代码,多个程序共享使用库的代码。
-
一个与动态库链接的可执行文件仅仅包含它用到的函数入口地址的一个表,而不是外部函数所在目标文件的整个机器码。
-
在可执行文件开始运行以前,外部函数的机器码由操作系统从磁盘上的该动态库中复制到内存中,这个过程称为动态链接(dynamic linking)。
-
动态库可以在多个程序间共享,所以动态链接使得可执行文件更小,节省了磁盘空间。操作系统采用虚拟内存机制允许物理内存中的一份动态库被要用到该库的所有进程共用,节省了内存和磁盘空间。