Linux之文件系统,软硬连接和动静态库
一.文件系统
在上篇的学习中我们知道了文件分为被打开的文件和未被打开的文件 ,对于被打开的文件我们学习了在内存中创建strct file来进行管理以及利用文件描述符表来和进程进行关联,那么对于存储在磁盘中未被打开的文件我们又是要如何进行管理的呢?
在了解如何管理之前我们需要思考一下对于磁盘中的文件我们进行管理的主要目的是为什么?存储在磁盘中的文件我们又没有进行使用那么我们管理它的目的是不是就是当我们想要使用它时能够快速的定位到文件 上。
对于如何快速定位到一个文件我们就想到了利用路径 的办法,但是有没有什么新的方法呢?在学习这些知识的同时我们还需要了解磁盘是怎么进行存储的以及操作系统是如何管理磁盘的。
1.1磁盘的存储结构
那么在了解了磁盘的大致存储结构后我们想要在磁盘上进行寻址就可以分为三步
- 选择哪一个盘面---------选择哪一个磁头
- 选择该面的哪一个磁道
- 选择该磁道的哪一个扇区
通过这三步我们就可以在磁盘上进行精准的寻址,而这三步总称为CHS寻址法 。
并且既然我们可以向某个扇区中写入数据那么也可以向任意一个或者任意多个的扇区进行连续写入,同时向随机一个扇区写入也是可以的。
1.2CHS和LBA
在了解了磁盘的物理存储结构后我们就需要站在操作系统的层面上去考虑如何管理磁盘中的存储空间了。对于磁盘中具有多面多个磁道多个扇区的存储形式,我们需要考虑一种方法可以让其对于我们用户来说更加直观以及方便,那么我们是否可以将磁盘的盘片想象成线性空间 呢?
在这种想法下我们是以扇区为最小存储单元 ,但是一个扇区只能够存储512个字节这是否有点过于小了。我们的文件动则几十上百kb最大甚至达到mb,以字节为存储单元确实不太方便操作系统来进行管理但是这种管理方式是可取的,不同的管理方式是操作系统基于不同的选择所创建处理。不同与以扇区为存储单元,操作系统可以基于文件系统来以文件块作为存储单元来进行数据存放。
而一个文件块可以存储4kb 即4096个字节,而这种存储方式就叫做LBA(Logical Block Address) 。但是LBA也是基于CHS来开发出的一种新方法,它的内核也是让操作系统将对于存储设备的管理转换为对于数组的增删查改。
1.3ext2文件系统
现在我们使用笔记本或者电脑时通常会将系统文件存储在C盘中而其他的文件则会根据类型来存储到D盘或者E盘中,但是如果我们研究过自己电脑的配置时我们会发现我们貌似只有一个硬盘那么为什么会有C盘D盘E盘之说呢?
准确来说不应该叫做C盘D盘和E盘,应该叫做C区D区和E区,他们都是从一个硬盘中划分出来的不同区域那么假如我们有500GB的存储空间我们要如何对其进行管理呢?
以这种思想我们就开发出来文件系统这一概念,今天则以讲述ext2 为主。
我们来一个个的讲述每一个块组中所存储的内容
- Date block(数据块) :存放文件内容,用文件块组成的一个大块用来存储文件的内容
- inode table(inode表) :存放文件属性 如 文件大小,所有者,最近修改时间,使用的数据块等。
每个文件都有自己的inode ,而inode是一个结构体其中存储着文件的属性我们可以通过ls -i 来查看每个文件的inode编号。
- inode bitmap(inode位图) :每个bit表示一个inode是否空闲可用。
这是使用了我们在C++中学习的位图的方法,不仅节省空间又可以直观的查看每个inode的情况。 - Block bitmap(块位图):记录着Data Block中哪个数据块已经被占用,哪个数据块没有被占用。
- Group Description(组描述符):描述块组属性信息
- Super Block(超级块) :存放文件系统本身的结构信息。记录的信息主要有:bolck 和 inode的总量,未使用的block和inode的数量,一个block和inode的大小,最近一次挂载的时间,最近一次写入数据的时间,最近一次检验磁盘的时间等其他文件系统的相关信息。Super Block的信息被破坏,可以说整个文件系统结构就被破坏了。
在了解了这些新概念后我们会产生几个问题:
- 如果文件内容过大导致一个块组中的数据块存储不下了会怎么样呢?
在inode中会使用数组的下标和使用的文件块进行映射 。对于文件内容过大的情况下,操作系统会将下标与文件块的映射分为一次映射,二次映射和三次映射甚至更多 ,对于一次映射的文件块其中存储的就是文件的内容,但是二次映射的文件块中存储的就是这个文件的其他文件块的地址,可以让操作系统通过这个文件块访问到其他的文件块。而三次映射就是在二次映射的基础上让第二次映射的文件块中依然存储其他文件块的地址从而实现访问。而利用这种机制我们同样可以将文件的数据存储到其他块组的数据块中从而实现跨组存储和访问。
- 既然inode在不同的分区中具有唯一性,那么如何区分不同inode属于哪个分区哪个组块呢?
在Linux中对于不同分区的区别就是不同文件的路径前缀区别 ,通过路径前缀的不同来判断我们处于哪个分区下。我们可以使用df命令来查看我们当前的分区。
而对于是哪个组块则和磁盘盘片线性看待一样,系统会在每个组块的开头和结尾各自定义一个变量来划分不同inode处于哪些组块。
-
在了解了系统只看inode后我们对于删除文件是否有了新的看法?
在知道了有inode位图和块位图来查看各自的使用情况后我们发现删除一个文件 只需要修改这两个位图的内容即可,即将inode位图中删除文件所对应的inode的bit位置0而块位图也是同样的操作。
-
你说inode和文件名之间是相互映射的,那么怎么证明呢?
这就需要聊到我们的目录,通过ls -i的操作我们发现目录也是一个文件 ,那么目录中存储的是什么内容呢?
答案就是目录中存储的就是它内部直接保存的文件的文件名和inode的映射关系!
这也就解释了为什么我们想要在目录中新建和删除一个文件 时我们需要这个目录的"w"权限 ,因为我们需要将目录中存储的对应文件和inode的映射关系进行新增和删除 。而想要修改一个文件的内容就需要对应的"w"权限。
-
在知道了系统只认inode后系统是如何查找一个文件的呢?
在知道了目录中会存储inode和文件名的映射关系后我们发现怪不得无论是我们使用命令行查找文件还是我们使用进程调用函数来查找文件都需要带上路径 ,当我们使用命令行查找时系统会根据命令行中的路径来一层一层的根据目录中的文件名和inode的映射关系 来查找到最终的文件,而使用进程时我们知道一个环境变量叫做工作目录所以只要知道了路径就可以顺利的查找到了文件。
-
什么是挂载?
在块组中不仅会存储我们自己的文件信息,而且还会存储很多文件管理的数据 这些数据是先一步存储到块组中的。在我们现实生活中我们想要将电脑中的一个分区中自己的数据清空时我们就会进行格式化 ,格式化会将用户自己产生的数据全部清空但是系统自带的数据就不会清空。那么在Linux中格式化一个分区后我们想要再次使用这个分区就需要将这个分区挂载到某个目录下 。
挂载的操作就需要使用mount命令
二.软硬连接
2.1软链接
软链接:也称为符号链接,是Linux系统中的一种特殊类型的文件,它包含对另一个文件或目录的引用路径。
从概念中我们可以知道他是一个文件,并类似于Windows系统中的快捷方式。那么怎么证明呢?
创建一个软连接的格式为:ln -s 被链接的文件名 链接的文件名
2.2硬链接
硬链接:可以将其理解为一个指向原始文件 inode 的指针,系统不会为它分配独立的 inode 和文件数据。原始文件和硬链接文件实际上是同一个文件,只是名字不同。
从概念中我们得知硬链接不是一个独立的文件,它只是一个文件名和inode之间的映射关系而已。证明一下:
硬链接和原文件是同一个文件那么怎么才能算删除了一个文件 呢?
即没有文件名和inode进行映射时也可以说是没有人使用的时候。
那么这种操作是如何实现的?
在inode中会有一个整型变量记录着有多少个文件名和inode编号有映射关系 ,每多一个映射关系就+1,少一个就-1。当变量为零时说明文件被删除 了。大家是不是听这个方法很耳熟,这就是我们之前学习C++时学到过的引用计数。
我们之前提到过目录也是文件那么我们是否可以对目录进行硬链接呢?
那么这是为什么呢?
没有为什么!这就是语法的限制 !但是我们可以发现创建一个空目录时它的硬链接数居然是2!这是因为目录中会有两个隐藏文件!而在学习了硬链接后我们再看这两个隐藏文件我们会有更深的理解了。
三.静态库和动态库
关于动静态库我们在介绍gcc工具的时候讲到过,但是当时我们只了解了动静态库的概念,其内里的知识我们并没有提及到,今天我们就要学习如何在Linux中创建和使用动静态库。
3.1静态库与动态库的概念
- 静态库是指编译链接时,把库文件的代码全部拷贝到可执行文件中,因此生成的文件比较大,但在运行时也就不再需要库文件了。其后缀名一般为".a"
- 动态库与之相反,在编译链接时并没有把库文件的代码加入到可执行文件中,而是在程序执行时由运行时链接文件加载库,这样可以节省系统的开销。动态库一般后缀名为".so" ,如前面所述的 libc.so.6 就是动态库。gcc 在编译时默认使用动态库。而在gcc后第一个选项加上-static则可以指定使用静态库进行编译。
3.2静态库的创建与使用
我们需要头文件来声明函数 ,但是我们不需要.c文件或者说我们不需要给人看的.c文件我们只需要将.c文件经过预处理,编译,汇编后形成的二进制文件即.o文件 即可。
那么四个.c文件全部变为.o文件后呢?还是一个一个的使用吗?
不,我们可以将其打包到一起形成一个库文件 ,而这就是静态库 。
所以静态库的本质就是将库中的源代码经过汇编形成.o文件后再将其打包起来。
那么如何创建一个静态库呢?
我们需要使用ar命令
其格式是:ar -rc lib静态库名字.a .o文件
其中**-rc的意思是replace和create 而lib是静态库的前缀,.a是静态库的后缀。**
在学会如何创建一个静态库后我们就要学习如何使用静态库
- 假设作为一个用户我们当前将源代码 都已经书写完成了,那么要如何才能利用静态库来生成可执行文件呢?
- 我们现在已经有了源文件,那么大家可以想到我们还缺少头文件和静态库 ,所以我们可以将头文件和静态库移到当前目录中来
- 正常在有了头文件和静态库后我们就可以使用编译器来生成可执行文件了
- 奇怪了,为什么提示我们没有定义这四个函数呢?
我可以告诉大家这是链接报错,但是我们明明在静态库中打包了.o文件啊为什么还会出错呢?
这里就需要告诉大家一个概念,由我们自己创建的静态库属于第三方库,编译器gcc默认是不认识第三方库的,所以我们在使用gcc生成可执行文件时我们需要给编译器提供我们需要的库的名称
- 但是为什么现在又提示无法找到静态库呢?我不是都指明静态库的名字了吗?
这是因为对于gcc来说它默认是在linux中/usr/lib64中查找库的,但是我们自己创建的库并不在lib目录下所以我们还需要给gcc一个库文件的搜索路径
- 但是这种做法是否过于粗糙了呢,就这么将头文件和静态库移动到目录中,那么我们是不是可以专门创建一个目录来存放头文件和静态库 呢?这样对于用户来说只有一个源文件和一个目录 即可。
而我们通过ldd命令 就可以查看可执行文件所依赖的库 有哪些
我们不是使用了自己创建的静态库了吗?为什么显示出来的库文件中没有自己创建的库文件呢?
这是因为ldd只能查询到可执行文件所依赖的动态库 ,而且对于静态库来说当可执行文件开始执行的时候静态库的内容就已经全部拷贝到可执行文件中了 当然是查不到的。
同时在我们使用gcc时我们要知道gcc默认是使用动态链接的,但是对于一些库文件如果你只提供了静态库那么只能局部的使用静态链接但是其他的库还是使用动态链接的,除非你在使用gcc时加上-static,这就要求编译器只能使用静态链接了。
3.3动态库的创建与使用
在学习了如何创建和使用静态库后对于动态库的创建我们就非常的得心应手了,直接上代码
那么对于动态库的使用除了有和静态库那样增加各种参数来查找头文件和动态库以外还有什么办法呢?
-
直接安装到系统中
刚刚我们说到gcc编译器会先在系统的库文件中查询库那么我们将头文件和自己创建的库全部安装到gcc的默认查找路径下即可。
-
使用软连接,查找动态库
-
修改环境变量LD_LIBRARY_PATH
在系统中存在一个环境变量LD_LIBRARY_PATH,里面记录了程序运行期间查找动态链接库时,指定除了系统默认路径(/usr/lib)之外的路径 。所以我们可以通过修改其来让系统找到我们想要的动态库
-
直接修改系统关于动态库的配置文件
在系统中会在/etc/ld.so.conf.d配置文件目录下存储系统管理所有动态库加载的配置文件 ,而这些配置文件里只存储了一些路径,所以如果我们想要让系统永久都能找到我们的动态库我们只需要在这个目录下自己新增一个配置文件并且在文件中存储我们动态库的路径 即可。
3.4动态库的加载
对于静态库来说是没有加载 这个过程的,因为在可执行程序运行的时候静态库中的内容就已经全部拷贝到可执行文件 中了而且每个链接了静态库的可执行文件都要存储静态库中的内容 这也就是为什么静态库的缺点 是生成的文件过大。
而对于动态库却有一套自己的加载过程,在了解加载过程的时候我们也会学习到一些新知识。
在Linux 下生成的可执行文件通常被叫做ELF格式 的可执行文件,这些可执行文件的特点是在文件内部不仅会存储自身的内容和属性而且还会存储自己链接了哪些动态库的函数 ,所以也就导致了当可执行文件运行时文件要从磁盘中加载到内存的时候其链接的动态库也需要加载到内存 中。
那么当程序没有被加载的时候在程序内部有地址吗?当变量名,函数名编译成二进制代码时还有对应的名字了吗?
当程序存储在磁盘中时即没有被加载的时候程序内部也是有地址的 而且这些地址还是按照虚拟地址空间的方式来编址 的,而变量名函数名等被编译成二进制代码时就不存在名字的概念 了那时候存储的都是对应的地址 了。
所以对于虚拟地址空间,它不仅是操作系统规定的同时在编译器进行编译的时候也遵守着这个规则,而这就是为了在加载的时候从磁盘文件到内存的时候更快的形成映射关系。
在之前学习虚拟地址空间时我们知道它规定了不同的范围来存储不同的变量信息,而对于虚拟地址空间来说遵守的编址规则 通常是基地址加偏移量 。
而这种编址方法也被叫做绝对编址,当存储的函数变换了位置后我们就需要重新查找它的位置再进行访问。
有绝对编址就有相对编址 ,后者更多用于形成库中函数的地址。两者的差别就像有人问你和你朋友在哪你说你在北纬多少多少度,南纬多少多少度,而你朋友说你在学校大门口朝北走10米。相对编址更多也是以一种偏移量的方式阐述自己的地址。
说回动态库的加载过程,我们说到了动态库也需要被加载到内存中那么对于虚拟地址空间来说动态库是要被加载到哪里呢?
动态库会被加载到共享区中,或者说是被映射到了共享区中。
那么要如何使用相对编址才能让动态库可以在共享区的任意位置加载成功都可以被运行呢?这就和ELF可执行文件中存储的动态库函数的存储方法有关了!
并且动态库和静态库不同,无论有多少可执行文件链接了动态库 在加载的时候我们只需要加载一份动态库 即可。因为在可执行文件中都以及存储了偏移量所以所有的可执行文件只需要利用偏移量来使用动态库。这也就是为什么动态库又被叫做共享库 同时也就是为什么动态库的优点 是节省空间!