实际上,大部分文件都不是被打开的(当前并不需要访问),都在磁盘中保存。那么对于没有被(进程)打开的文件,要不要管理呢?这部分文件核心工作是什么呢?
答案是肯定要管理的,这部分文件的核心工作就是快速定位文件,所以未来我们访问文件的时候就可以通过路径快速访问文件啦
文件的管理工作:
- 对打开的文件进行管理
- 没有被打开的文件也要在磁盘中进行管理
以上就是文件系统需要做的工作。
1. 磁盘
1.1 磁盘的物理结构
(从网上偷个图来~)
一个盘面可以有很多的同心磁道
一圈磁道可以有很多扇形的扇区
扇区是磁盘的最小存储单元——512B/4KB
我们把磁盘称之为块设备,支持随机读取
如果我们想向一个扇区写入,我们该如何寻址、定位呢?
- 选择某一面——本质就是选择磁头
- 选择该面上的某一个磁道
- 选择在该磁道上的某一个扇区
我们可以向一个扇区写入,就可以向任意一个/多个扇区写入。连续多个扇区式的写入,也就是可以随机式的写入。
总结一下:
- 磁头摆动定位柱面(磁道)
- 盘片旋转定位扇区
1.2 磁盘的逻辑抽象结构
相信我们大部分人都见过磁带吧~
磁盘的数据就存储在一个长条黑带子上,那么如果我们把这条黑带子拉直然后铺在一个平面上呢?同样的,磁盘也可以做出同样的类比,把一条条的磁道拉展平铺在一个平面上,于是乎就可以变成这样了:
(将磁盘盘片想象成线性空间)
由此对磁盘的管理就可以抽象成对数组的管理!!
数组都是有下标的,假定0 ~ 999是第一个盘片,1000 ~ 1999是第二个盘片,以此类推。
在0 ~ 999中第一个盘片中0 ~ 99是第一个磁道,100 ~ 199是第二个磁道,以此类推。
那么我们就可以精确的定位到某个扇区的具体位置了。比如有个文件磁盘号为123,则
- 确定在哪一面:扇区号 / 每一面的大小,如:123 / 1000 = 0,则证明该扇区在第一个盘片中
- 确定柱面/磁道:扇区号 / 每个磁道(柱面)的大小,如:123 / 100 = 1,则证明该扇区在第二个磁道中
- 确定扇区号:扇区号 % 每个磁道(柱面)的大小,如 123 % 100 = 23,则证明该扇区在第24号扇区中
(注:因为按照数组的规则存储,所以说明该磁盘号在何位置时需要+1)说明磁盘号为123的文件存储在第一个盘片的第二个磁道的第24号扇区中
操作系统,可以按照扇区为单位进行存取也可以基于文件系统,按照块为单位进行数据存取。假定一个扇区为512B,那么8个扇区为一个块,一个块的大小就是4KB。
一个块的起始地址叫做LBA,也就是Logic Block Address(逻辑块地址)。
最终结论:
对存储设备的管理,在OS层面上,转换成为了对数组的增删查改!
2. 理解文件系统
2.1 前言
平常我们使用的硬盘小则512GB大则几个TB,如果我需要管理这整个磁盘,对我们属实有点小困难了。所以无论技术上还是应用上,我们都非常的迫切需要,不要将这个磁盘整体来管理。就好比一个学校的每个班级,不可能都由校长来管理,而是需要一个一个的班主任分别管理。
比如,我有一块512GB的硬盘,它又被划分成为了四个128GB的小块,于是乎我们只需将这个128GB的区管理好行啦,这样是不是就容易很多了捏。
但是这128GB依旧还是有点大,于是我们再把这128GB划分成一个个的小组,假定这个组以2GB为单位,那么总共就有64个组。此时,我们只需要管理好第一个组,那么后面的组就按照第一个组照本宣科的来就好了,这样就方便了许多,皆大欢喜!
我们要把512GB空间管理好,只需要把2GB空间管理好即可!
这种思想就是典型的分治思想!
这些组具体又表现为这样:
- 这些块组里会保存我的文件信息,我的文件信息包括内容和属性,这些都是数据,并且内容和属性是分开存储的。
- 与此同时在这些组里会保存很多的文件管理数据,其实这些数据会把相应的块组管理起来,结合块组也要把文件的属性和内容管理起来。
- 在正式使用磁盘之前得先要让管理数据写入到块组当中。这个工作称之为格式化。所以说格式化只是清空你的数据而并不清空管理数据。
2.2 文件系统
使用命令:
ls -li
可以发现,在我们的文件列表前多了一行数字,那么这行数字是什么捏?
- 这些数字是inode编号,一般情况下,一个文件对应一个inode编号,基本上每个文件都要有indode编号。
- 整个分区中inode具有唯一性,在linux内核中,识别文件和文件名无关只和inode有关!
保存文件的属性是通过inode保存的,用struct结构体可以将inode内的字段抽象为
struct inode
{
// 大小、权限、拥有者、所属组、ACM时间、inode编号
int blocks[N]; // 记录该文件所对应的块号
}
在我们现在所学的文件系统中,inode的大小是固定的,为128B
在上面我们画了关于一个组是如何划分的,这里我们再来将这个组详解一下:
Linux ext2文件系统,上图为磁盘文件系统图(内核内存映像肯定有所不同),磁盘是典型的块设备,硬盘分区被划分为一个个的block。一个block的大小是由格式化的时候确定的,并且不可以更改。例如mke2fs的-b选项可以设定block大小为1024、2048或4096字节。而上图中启动块(Boot Block)的大小是确定的。
- Boot Block:启动块,一般情况(在编号为0的盘面、磁道、第一号扇区)只有在第一个分区的最开始有Boot Block,它是负责我们启动的。
- Block Group:ext2文件系统会根据分区的大小划分为数个Block Group。而每个Block Group都有着相同的结构组成。政府管理各区的例子。
- 超级块(Super Block):存放文件系统本身的结构信息。记录的信息主要有:block inode的总量,未使用的block和inode的数量,一个block和inode的大小,最近一次挂载的时间,最近一次写入数据的时间,最近一次检验磁盘的时间等其他文件系统的相关信息。Super Block的信息被破坏,可以说整个文件系统结构就被破坏了。
- GDT,Group Descriptor Table:块组描述符,描述块组属性信息。
- 块位图(Block Bitmap):Block Bitmap中记录着Data Block中哪个数据块已经被占用,哪个数据块没有被占用。
- inode位图(inode Bitmap):比特位的位置表示inode编号,比特位的内容(0/1)表示每个bit表示一个inode是否空闲可用。
- inode节点表(inode Table):存放文件属性,如文件大小,所属者,最近修改时间等。
- Data blocks(数据区):存放文件内容
2.3 文件的新建和删除
新建一个文件的过程:
- 首先查询inode Bitmap,找到一个没有被使用的比特位,由0置为1,记住偏移量。
- 根据刚才的偏移量在inode Table内将文件的属性写入
- 在Block Bitmap中寻找一个没有被使用的比特位,由0置为1,记住偏移量。
- 根据此偏移量在快组中找到相应的块,将文件的内容写入。
- 记录分配情况。内核在inode上的磁盘分布区记录了上述块列表。
- 添加文件名到目录
linux如何在当前的目录中记录这个文件?内核将入口(inode,文件名)添加到目录文件。文件名和inode之间的对应关系将文件名和文件的内容及属性连接起来。
删除一个文件,只需要将该文件在inode Bitmap对应的位置将1置为0即可。
inode如此重要,但是我们用户好像没用从来直接用过inode,一直用的都是文件名:
- 用户只用文件名,内核只用inode编号
- 文件名 == inode编号的映射
目录的内容存什么?
自己目录内部直接保存的文件的文件名和inode的映射关系。
例如:test.c
,inode:123456
所以同一个目录下不允许存在同名文件,inode在一个分区具有唯一性,inode和文件名互为键值。
我们发现,在inode属性中,并没有文件名这一属性,因为Linux中,文件名不属于文件属性。文件名在目录的内容所存储~
2.4 文件的查找
访问一个文件的时候,最开始是查找这个文件在哪一个分区里,因为每个分区里都有属于自己的一套文件系统。如何寻找这个分区?
每一个文件都有路径,可以通过路径的前缀判断出我们的路径在哪一个分区下。
在Linux中,被分区格式化后,要使用这个分区,必须得把这个分区进行挂载mount
挂载就是把内核对应的数据结构和文件系统对应的数据结构用指针关联起来。
2.5 理解软硬链接
硬链接是通过inode引用另外一个文件,软链接是通过名字引用另外一个文件。
软连接的链接方法:
ln -s 文件名 被链接的文件名
硬链接的链接方法:
ln 文件名 被链接的文件名
在分别对软件进行软硬链接后我们发现:
软链接的两个文件inode各不相同,而硬链接的两个文件inode完全相同。
在此可以得到一个结论:
软链接是一个独立的文件,而硬链接不是,因为他没有独立的inode编号。
什么是软链接:
独立文件,拥有独立的inode,其内容是指向目标文件的路径。
什么是硬链接:
不是独立文件,是在指定目录内部的一组映射关系,文件名和inode的映射关系!
当我们删除test.hard文件时,文件的硬链接数也随之而改变:
一个文件什么时候真正被删除?
没有文件名和inode映射了,也就是没有人使用的时候了。我们在删除文件时干了两件事情:
在目录中将对应的记录删除,
将硬连接数-1,如果为0,则将对应的磁盘释放。
在文件系统层面,目标文件怎么知道有没有文件名指向inode了呢?
inode内部有引用计数,表明有几个文件名映射关系。文件名在目录里具有唯一性,文件名就好像一个指针,指向了inode。
新建一个文件夹和文件:
我们发现,新建的文件的硬链接数默认是1,但为什么新建的目录的硬链接数却是2呢?
进入目录并查看目录内的所有文件:
这时,我们又发现,新建目录内的.
文件的inode和目录的inode一摸一样,这就是目录的一个硬链接~
如果我们再在newdir
这个目录中再建立一个目录呢,看看有什么效果:
我们又又发现,newdir
的硬链接数变成了3!!其实不难理解,我们进入到在newdir目录下新建的目录下看一下:
这里..
文件的inode和我们newdir
文件的inode是一样的,所以..
就是表示上一级目录, newdir
目录的硬链接数也随之变为3了。
用户无法对目录建立硬链接!
3. 动态库和静态库
- 静态库(.a):程序在编译链接的时候把库的代码链接到可执行文件中。程序运行的时候将不再需要静态库。
- 动态库(.so):程序在运行的时候才去链接动态库的代码,多个程序共享使用库的代码。
- 一个与动态库链接的可执行文件仅仅包含它用到的函数入口地址的一个表,而不是外部函数所在目标文件的整个机器码。
- 在可执行文件开始运行以前,外部函数的机器码由操作系统从磁盘上的该动态库中复制到内存中,这个过程称为动态链接(dynamic linking)。
- 动态库可以在多个程序间共享,所以动态链接使得可执行文件更小,节省了磁盘空间。操作系统采用虚拟内存机制允许物理内存中的一份动态库被要用到该库的所有进程共用,节省了内存和磁盘空间。
库中是没有main函数的,我们也不能将main函数写入库中
3.1 生成静态库
为了方便理解静态库,我们写一个简易版的计算器。其实包括了这些.c
和.h
文件
在Makefile
中执行以下代码:
static-lib=libmycalculator.a
$(static-lib):Add.o Sub.o Mul.o Div.o
ar -rc $@ $^
%.o:%.c
gcc -c $<
.PHONY:clean
clean:
rm -f *.o *.a
然后就会生成一堆这玩意:
形成的.a
文件就是静态库,本质上就是把这一堆.o
文件打了包。
进入我们的测试文件夹,在此文件夹下创建一个测试所用的TestMain.c
文件,直接运行:
毫无意外的,运行报错!因为库什么的都没有在这个文件夹下(好像那二傻子QAQ)
先把头文件拷贝进来:
头文件都是公开的,要被保护的只有库文件。再次编译:
此时,即使提供了头文件但编译依旧报错,这个报错为链接报错,因为第三方库(你自己写的库),gcc默认是不认识的!
介绍一下一些选项(以下选项后带不带空格都可!):
-L
:指定库的搜索路径-l
:指定库名(去掉lib和.a的真实库名)-I
:指定头文件搜索路径
使用命令进行编译:
gcc TestMain.c -l mycalculator -L.
编译成功,并且成功运行!
但是如果这样把静态库交给使用者那也太矬了,一堆头文件显示在界面上,很难看,于是乎,我们修改一下Makefile
文件:
static-lib=libmycalculator.a
$(static-lib):Add.o Sub.o Mul.o Div.o
ar -rc $@ $^
%.o:%.c
gcc -c $<
.PHONY:output
output:
mkdir -p mycalculator_lib/include
mkdir -p mycalculator_lib/lib
cp -f *.h mycalculator_lib/include
cp -f *.a mycalculator_lib/lib
.PHONY:clean
clean:
rm -rf *.o *.a mycalculator_lib
ar是gnu归档工具,rc表示(replace and create)
静态库,本质就是将库中的源代码直接翻译成为.o
目标二进制文件,然后打包。
执行一下,先make
再make output
,此时我们看到目录里形成了mycalculator_lib
的一个目录:
但是此时如果我再去运行TestMain.c
,它连头文件都找不到了,那该怎么办呢?
- 方法一:很简单,直接在包含头文件的时候包含相对路径即可!
#include "mycalculator_lib/include/Add.h"
#include "mycalculator_lib/include/Sub.h"
#include "mycalculator_lib/include/Mul.h"
#include "mycalculator_lib/include/Div.h"
- 方法二:在编译时带
-I
选项
>gcc TestMain.c -I mycalculator_lib/include
展示一下一套完整的编译:
gcc TestMain.c -I mycalculator_lib/include -l mycalculator -L mycalculator_lib/lib/
此外,如果我们不想这么麻烦,那么直接把相应的头文件和库文件安装到系统里即可。
gcc默认是动态链接的,但个别库如果只提供静态库,gcc只能局部性的把你指定的.a
文件进行静态链接,其他库正常动态链接,如果带-static
选项,就必须链接静态库!
3.2 生成动态库
gcc选项:
-shared
: 表示生成共享库格式-fPIC
:产生位置无关码(position independent code)- 库名规则:libxxx.so
修改一下Makefile
dy-lib=libmycalculator.so
$(dy-lib):Add.o Sub.o Mul.o Div.o
gcc -shared -o $@ $^
%.o:%.c
gcc -fPIC -c $<
.PHONY:output
output:
mkdir -p mycalculator_lib/include
mkdir -p mycalculator_lib/lib
cp -f *.h mycalculator_lib/include
cp -f *.so mycalculator_lib/lib
.PHONY:clean
clean:
rm -f *.o *.so mycalculator_lib
然后就可以开始使用动态库了,在使用上与动态库几乎没有什么区别!
但是此时,运行可执行文件却发生了错误!
指明
-L
只是告诉了编译器我们的库在哪里,我们对应的可执行程序在加载运行时,还要告诉系统我们的动态库在哪里,因为当前的库并没有在系统的默认路径下。
可以看到,可执行程序找不到我们的动态库。那么该怎么解决呢?
- 方法一:直接将我们的库安装到系统里。(使用别人的库最推荐的做法!)
用我现在所使用的这个库举例。
使用命令将头文件安装到系统:
sudo cp mycalculator_lib/include/*.h /usr/include/
查看一波~
安装动态库:
sudo cp mycalculator_lib/lib/libmycalculator.so /lib64/
现在直接编译:gcc TestMain.c
头文件能找到了,但是对应的定义还未找到,也就是库还未被使用,此时指定库名即可gcc TestMain.c -l mycalculator
。
完美生成可执行程序文件并且完美运行!巴适~
- 方法二:建立软链接。
ln -s mycalculator_lib/lib/libmycalculator.so libmycalculator.so
可以很清楚的看到,此时我们的动态库不再是not found了。
同样的我们也可以直接把软链接安装到系统中!
- 方法三:导入
LD_LIBRARY_PATH
环境变量中,让系统找到动态库。
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/minnow/test/mycalculator/test
添加成功!此时系统也知道了我的库在哪个环境变量下。此时可执行程序也可成功运行了!
- 方法四:直接更改系统关于动态库的配置文件:
/etc/ld.so.conf.d/
该配置文件是管理系统相关动态库加载的文件。新建并访问一个配置文件,将动态库所在的路径写入即可:
sudo vim /etc/ld.so.conf.d/my_lib.conf
此时,动态库也能被找到了。
(注:如果动态库还是not found,使用命令:sudo ldconfig
刷新一下配置文件)
同一组库,同时提供动静态两种库,gcc默认使用动态库!
3.3 动态库加载
Linux下的可执行程序是ELF格式的可执行程序。
动态链接的程序,不光光是自己要加载,链接的库也要加载到内存中。
- 程序没有被加载到内存,程序内部有地址吗?有!
- 变量名、函数名等,编译为二进制后,还有这种概念吗?没有!
通过以上两点,我们要知道,在编译的时候如何对代码进行编址的问题。其基本遵循虚拟地址空间的那一套。
虚拟地址空间,不仅仅是操作系统里的概念,编译器在编译的时候,也要按照这样的规则编译,这样才能在加载的时候,进行从磁盘文件到内存的映射。
我们的可执行程序在编译之时,就已经有了虚拟地址,也就是逻辑地址。
整个代码可以理解为基地址(0)+偏移量[0-0xFFFFF]的编址方式。这种起始地址为0的编址方式,我们叫做平坦模式。
平坦模型(Flat Model):
是一种内存管理模型,其中整个4G字节的内存被视为一个连续的大段来处理。在这种模型下,每个段都指向4G字节的内存空间,其基地址为0,段界限为0xFFFFF,段粒度为4K字节。
绝对编制适合平坦模型。
相对编制适合形成库函数中的地址,此时就很好理解编译链接形成动态库时的-fPIC
选项,产生位置无关码了。
库被加载之后,要被映射到指定使用了该库们的进程地址空间中的共享区部分。
我们想做到让库在共享区的任意位置,都可以正确运行。一旦库加载之后,位置就是确定的。
- 当多个可执行程序需要同一个库时,只需将该库映射到共享区即可,整个系统中只会存在一份库,所有代码和可执行程序共用这一份库,所以它被称之为共享库也叫动态库。
- 但是静态库就不一样了,有几个可执行程序就会加载几份库,非常的浪费空间!