📖 前言:本期介绍文件系统。
目录
- 🕒 1. 磁盘结构
- 🕘 1.1 物理结构
- 🕘 1.2 存储结构
- 🕘 1.3 逻辑结构
- 🕒 2. 文件系统
- 🕘 2.1 对 IO 单位的优化
- 🕘 2.2 磁盘分区与分组
- 🕘 2.3 对分组的具体管理方法
- 🕘 2.4 文件操作
- 🕤 2.4.1 新建文件
- 🕤 2.4.2 获取文件 inode
- 🕤 2.4.3 读取文件属性
- 🕤 2.4.4 读取文件内容
- 🕤 2.4.5 删除文件
- 🕤 2.4.6 恢复文件
- 🕒 3. 软硬链接
- 🕘 3.1 理解硬链接
- 🕘 3.2 理解软链接
- 🕘 3.3 理解 . 和 ..
- 🕒 4. 动静态库
- 🕘 4.1 概念
- 🕘 4.2 静态库的制作与使用
- 🕘 4.3 动态库的制作与使用
- 🕘 4.4 理解动态库加载
🕒 1. 磁盘结构
🕘 1.1 物理结构
磁盘的物理结构如下:磁盘主要由永磁铁、磁头、主轴、盘片以及下面的电路板组成,其中盘片为一摞,每一个盘片都分为上下两面,这两面都可以用来存储数据,且每一面都配有一个磁头。
注意:
1、磁盘中每个盘片的每一面都配有一个磁头,且磁头和盘面是没有接触的,二者的距离非常非常低,而一旦有灰尘等杂质落入到磁盘中就可能会导致磁头撞击灰尘从而刮花盘面,所以磁盘拆开后就会损坏。
2、现在一般个人的笔记本都是使用固态硬盘 SSD,而不再使用磁盘,因为磁盘的磁头与盘面距离非常近,所以为了避免磁盘与盘面接触而刮花盘面导致数据丢失,磁盘不能抖动;但是笔记本通常要进行移动,很可能会发生上述故障;同时,SSD 的读写速度要高于磁盘。
3、但是在企业端,磁盘仍然是存储的主流,因为企业中主机都统一放置在机房中,轻易不会移动;同时,SSD 存在造价贵、读写次数有限等缺点。
4、磁盘是计算机中唯一一个纯机械结构的设备,同时磁盘还是外设,所以磁盘进行数据读写的速度很慢。
🕘 1.2 存储结构
- 磁道:磁盘的表面即盘面由一些磁性物质组成,可以用这些磁性物质来记录二进制数据;同时,盘面被划分为一个个同心圆,这些同心圆被称为 磁道 (一个同心圆就是一个磁道),相邻磁道之间是有间隙的,我们的数据就存在磁道上。
- 扇区:从圆心向外放射,与磁道围成的一小块区域称为扇区,一个磁道会被划分为许多个扇区,每个扇区就是一个 “磁盘块”,这是磁盘寻址的基本单位,即数据进行 IO 的单位,大小一般为 512 byte。(注:每个扇区的大小是固定的,所以从圆心往外,扇区的数据存储密度会随着扇区面积的增大而减小)
- 柱面:磁盘中所有盘面的同一个磁道被称为一个柱面,可以说,柱面和磁道是等价的。
磁盘寻址的过程:
首先定位磁道,即在哪一个柱面 (cylinder);然后再定位盘面,即定位到寻找哪一个盘面的磁头 (head),最后再定位在哪一个扇区 (sector);
上述过程在物理上的表现方式如下:启动主轴后,所有的盘片以同样的方式进行高速旋转,同时所有的磁头也共同从圆心到半径左右摆动,当定位到柱面后,磁头停止摆动,盘片继续旋转,当盘片对应扇区旋转到磁头下方后,对应盘面的磁头向扇区中写入/读取数据。
所以,在磁盘中定位任意一个/多个扇区,采用的基本硬件定位方式是 柱面、磁头、扇区定位,即 CHS 定位法。
🕘 1.3 逻辑结构
磁盘的盘面由一个个磁道构成的,且这些磁道都是同心圆,和磁带卷起来时很相似,我们可以将磁盘结构抽象为线性结构,然后使用数组来存储数据:
如上,我们将整个磁盘从逻辑上看作一个 sector arr[n] – 数组的一个元素代表磁盘中的一个扇区,然后由多个扇区组成一个磁道,由多个磁道组成一个扇面,最后在由多个磁道组成整个磁盘。
自此,我们只需要知道数组中的一个下标就可以定位磁盘中的一个扇区,我们对磁盘的管理也转变为了对数组进行管理;在操作系统内部,我们将这种地址称为 LBA (logical block address) 地址。
LBA 地址转 CHS 定位例子:
假设一个磁盘有两个盘片,每个盘片有两个盘面,每个盘面有10个磁道,每个磁道有100个扇区;现在,某个扇区的LBA地址为1234,求该扇区在磁盘上的具体位置:
解答:
盘面:H = LBA / 一个盘面的大小 = 1234 / (10*100) = 1 号
磁道:C = LBA / 一个柱面的扇区数 = 1234 / 100 = 12 号
扇区:S = LBA % 一个柱面的扇区数 = 1234 % 100 = 34 号
CHS:第12柱面/磁道,第1盘面,第34号扇区
最后,操作系统为什么要对 CHS 进行逻辑抽象呢?有如下两个原因:
- 数组更便于管理;
- 不让操作系统代码与底层硬件强耦合——即使磁盘的存储结构改变,操作系统仍然可以使用 LBA 地址进行定位寻找,只需要改变 LBA 与磁盘扇区的映射关系即可。
🕒 2. 文件系统
🕘 2.1 对 IO 单位的优化
在上面我们提到,磁盘的 IO 单位是一个扇区大小,即 512 byte,但这还是太小了,为了减少 IO 次数,提高 IO 效率,操作系统的文件系统会定制的一次进行多个扇区的读取,如 1KB/2KB/4KB。(文件系统默认采用 4KB 大小为单位 (8个扇区) 进行IO)
即内存被划分为了许多个 4KB 大小的空间 (页框),磁盘中的文件尤其是可执行文件也由多个 4KB 大小的块组成 (页帧)。
注:文件系统以 4KB 作为数据 IO 的单位,那么当读取的数据小于 4KB 时,我们仍然需要读取 4KB 数据,那么就有同学可能担心数据无用的问题,其实,计算机中的局部性原理已经很好的解决了这个问题。
🕘 2.2 磁盘分区与分组
不同的磁盘大小不同,常见磁盘中最少也有500G,但是文件系统的 IO 单位只有 4KB,这就存在磁盘太大不方便管理的问题,所以我们需要对磁盘进行区域划分;但是磁盘分区之后仍然比较大,所以我们需要继续对分区进行分组,如下:
注意:每个分区的第一部分数据是 Boot Block 启动块,后面才是各个分组,它与计算机开机相关,我们不用关心。
现在,我们只需要管理好一个分组,然后管理模式复制到其他分组就可以管理好一个分区;再将一个分区的管理模式复制到其他分区就可以管理好整个磁盘了。其中,操作系统对一个分区的管理就被称为文件系统。如此,便是分而治之。
🕘 2.3 对分组的具体管理方法
下图为磁盘文件系统图(内核内存映像有所不同),磁盘是典型的块设备,硬盘分区被划分为一个个的 block,一个block的大小是由格式化的时候确定的,并且不可以更改。(启动块(Boot Block)的大小是确定的)
Super Block:
超级块,属于整个分区,保存的是整个分区的信息,主要有:bolck 和 inode 的总量,未使用的block和inode的数量,一个block和inode的大小,最近一次挂载的时间,最近一次写入数据的时间,最近一次检验磁盘的时间等其他文件系统的相关信息。Super Block的信息被破坏,可以说整个文件系统结构就被破坏了;
之所以将 super block 放在分组里面,而不是和 boot block 一样放在分区的最前面,是为了数据安全 – 当常用分组里面 super block 的数据损坏之后,我们可以直接从其他分组将正确数据复制过来。
注:super block 不是每个分组里面必备的数据,有些分组里面并没有 super block。
inode Table:
文件的节点表,保存了分组内部所有文件的 inode 块(已用+未用),inode 块的大小是固定的 (128/256 byte),其中包含了一个文件所有的属性信息 – 文件创建时间、文件权限、文件大小、文件类型等等,每个文件都有其对应的 inode。(inode 里面没有文件名)
注:inode 为了区分彼此,每个 inode 都有自己的 ID,可以在 ls 指令中通过 i 选项查看。
inode Bitmap:
inode 位图,里面每个比特位都对应着 inode table 的一个下标,比特位为0表示该小标对应的 inode 未被使用,为1表示该 inode 已被占用。
Data blocks:
数据块,保存了分组内部所有文件的 data block 块,数据块的大小不是固定的,它随着应用类型的变化而变化,其中包含了一个文件的全部/部分内容 (文件内容太多时需要使用多个 data block)。
Block Bitmap:
数据块位图,里面每个比特位都对应着 data blocks 的一个下标,比特位为0表示该小标对应的 data block未被使用,为1表示该 data block 已被占用。
Group Descriptor Table (GDT):
块组描述符表,描述块组属性信息。对应分组的宏观属性信息,比如一共有多少个Inode,一共有多少数据块,Inode备用了多少,数据块备用了多少等等。虽然这些数量可以计算,但是计算会消耗性能,所以GDT存在的意义就是存储这些数量以免计算。
🕘 2.4 文件操作
🕤 2.4.1 新建文件
在了解了一个分组的具体组成之后,如何新建文件也显而易见了 – 在 inode bitmap 里面查找为0的比特位编号,将该比特位置1,然后将文件的所有属性填写到 inode table 对应编号下标的空间中;再在 block bitmap 中查找一个/多个为0的比特位编号,将其置为1,然后将文件的所有内容填写到 data blocks 对应编号下标的空间中;最后再修改 super block、GDT 等位置中的数据。同时,需要将新文件文件名与 inode 的映射关系写入到目录的 data block 中。
🕤 2.4.2 获取文件 inode
在 Linux 中,查找文件统一使用 inode 编号,但是我们平时只适用过文件名,从没有使用过 inode,那么操作系统是如何将文件名与 inode 一一对应的呢?答案是通过目录。目录和普通文件不同,目录的内容是下级目录或者普通文件,所以目录的 data block 里面存储的是当前目录下文件名与 inode 的映射关系。
所以当我们在某一个目录下使用文件名查找文件时,操作系统会读取目录 data block 里面的数据,找到文件名对应的 inode 编号,找不到就提示 文件不存在。而当我们在目录下新建文件/文件夹时,操作系统会向目录 data block 里面写入新文件与 inode 的映射关系。这也是为什么在目录下读取文件信息需要 r 权限,在目录下新建文件需要 w 权限的原因。
🕤 2.4.3 读取文件属性
先通过目录 data block 得到文件的 inode 编号,然后在 inode bitmap 查看对于编号比特位是否为1,检查 inode 有效性,然后从 inode table 中读取对应 inode 中的文件属性。
注:inode 编号可以跨分组,但不可以跨分区,即同一分区内 inode 是统一编号的。
🕤 2.4.4 读取文件内容
读取文件内容比较复杂,首先需要通过 inode 读取文件信息,然后通过 inode 结构体的内容查找 data block 编号,再到 block bitmap 中查找对应比特位是否是否为1,检查有效性,最后在从 data block 中读取文件内容。
struct inode {
int id;
mode_t mode;
int uid;
int gid;
int size;
//...
int blocks[15];
};
注:一般来说,blocks 里面前12个元素存放的都是一个 block 编号,代表 data blocks 里面的一块空间,但是最后三个元素不同,虽然它们存放的也是一个 block 编号,但 data blocks 对应 block 编号中存放的内容却很特殊,blocks[12] 指向的 data block 中存放的是一级索引,即其中存放的也是一个类似于 blocks[15] 的数组,指向多个 data block;blocks[13] 指向的 data block 中存放的是二级索引,即其中存放的内容类似于 blocks[12];以此类推,blocks[14] 里面存放的是三级索引。这样,即使该文件很大,操作系统也能够成功读取文件的内容。
🕤 2.4.5 删除文件
删除文件很简单,只需要将 inode bitmap 和 block bitmap 里面对应比特位置为 0 即可,后面新文件的文件属性和文件内容直接覆盖原来已删除文件的属性和内容。
🕤 2.4.6 恢复文件
在理解了删除文件的原理之后,我们就明白文件删除之后是可以恢复的 – 操作系统包含了文件的日志信息,会将文件名与 inode 的映射关系保存在一个临时的日志文件里,我们通过这个日志文件找到误删文件的 inode,然后将 inode bitmap 里面对应的比特位重新置为1,再通过 inode 结构体中的 blocks 成员找到所有的数据块,再将 block bitmap 中对应比特位置为1即可;
不过这一切的前提是原文件的 inode 没有被新文件使用 – 如果新文件使用了原文件的 inode,那么对应的 inode table 以及 data block 里面的数据都会被覆盖,所以文件误删之后最好的做法就是什么都别做,避免新建文件将原文件的 inode 占用。
🕒 3. 软硬链接
🕘 3.1 理解硬链接
在 Linux 中,我们可以通过 ln 指令来为一个文件创建硬链接,如下:
# 为myfile.txt文件创建硬链接hard_myfile.link
ln myfile.txt hard_myfile.link
如图,我们可以观察到:
- 创建硬链接会改变原文件的硬链接数;
- 硬链接文件与原文件的文件属性完全相同,即硬链接文件与原文件使用同一个 inode。
同时还可以看到,向硬链接文件中写入数据时原文件中也会存在该数据,且删除原文件后硬链接文件除了硬链接数减1以外并不会受影响。
我们在学习文件系统时说过,inode 是一个文件的唯一标识,它里面存放着文件的所有属性,每一个文件都有自己独立的 inode,但是硬链接文件没有,它与原文件使用同一个 inode。
所以,创建硬链接不会创建新文件,硬链接文件仅仅是原文件的一个别名,它使用原文件的 inode 和 data block;而创建硬链接的本质其实仅仅是在指定目录下新增原文件 inode 与硬链接文件名的映射关系,同时将原文件的硬链接数加1。
注:文件 inode 中存在一个类似于 count 的整形变量来记录文件的硬链接数,当我们为文件创建硬链接时 count 加1,删除原文件或者硬链接文件时 count 减1 (C++中称为引用计数,Linux中称为文件的硬链接数),这也就是我们上面观察到文件的硬链接数发生变化的原因;所以,当一个文件的硬链接数变为0时,操作系统才会真正删除该文件,即执行将该文件的 inode bitmap 和 block bitmap 对应比特位置0等操作。
🕘 3.2 理解软链接
在 Linux 中,我们可以通过 ln 指令带上 ‘s’ 选项来为一个文件创建软链接,如下:
# 为myfile.txt文件创建硬链接hard_myfile.link
ln -s myfile.txt soft_myfile.link
可以看到:
- 软链接文件的文件类型为 ‘
l
’ (第一个字母代表文件类型),即链接文件; - 软链接并不会改变原文件的硬链接数;
- 软链接拥有自己独立的 inode,是一个全新的文件,所以软链接文件的文件属性和原文件并不相同。
同时可以看到,和硬链接一样,软链接文件向文件中写入数据时原文件中也会存在该数据,但是当原文件被删除后,再次查看软链接文件报错。
由上面的现象我们可以推断,软链接是通过文件名而不是文件 inode 来链接文件的,因为上面的原文件存在硬链接文件,而硬链接文件与原文件的 inode 是相同的,但是我们删除原文件后软链接直接失效了;同时,我们创建了一个与原文件文件名相同的新文件,尽管该文件与原文件的 inode 并不相同,但软链接仍然重新建立了。(我们也可以使用 unlink 指令来删除链接文件)
所以,创建软链接会创建新文件,软链接文件有自己的独立的 inode,软链接通过文件名的方式来链接文件,所以本质上软链接是将原文件的路径写入到新文件的 data block 中。
注:Linux 中的软链接就相当于 Windows 中的快捷方式,通过该快捷方式我们可以快速方便的对目标文件进行操作。
🕘 3.3 理解 . 和 …
我们上面学习了软硬链接,知道了硬链接相当于文件的别名,其本质是在指定目录下新增原文件 inode 与硬链接文件名的映射关系,软链接相当于快捷方式,本质是将目标文件的路径写入到软链接文件的 data block 中。
软链接的作用:作为快捷方式使用
硬链接的作用:
Linux 中每个目录下都存在两个隐藏目录 .
和 ..
,其中 .
代表当前目录,..
代表上级目录,而它们本质是某一个目录的硬链接,如下:
我们新建一个普通文件,其硬链接数是1,因为普通文件本身就有一个文件名与 inode 相对应;而我们新建一个目录时,目录的硬链接数为2
,这是因为 Linux 中目录下存在隐藏的.
,它是目录的一个硬链接;如果我们在当前目录下再新建一个下级目录,那么当前目录的硬链接数就会变为3
,这是因为下级目录中的..
也是当前目录的一个硬链接。(这也是为什么cd ..
能够回退到上级目录的原因,因为..
是上级目录的一个别名)
注:Linux 不允许用户给目录创建硬链接,只能系统自己创建 (.
和..
),这是因为给目录创建硬链接可能会在目录中引入循环,使得在目录遍历时系统陷入无限循环当中,从而导致无法定位到访问目录 (比如系统从根目录开始查找一个文件,当在查找过程中遇到根目录的硬链接时就会造成环路查找)。
🕒 4. 动静态库
🕘 4.1 概念
- 静态库 (.a):程序在编译链接的时候把库的代码链接 (拷贝) 到可执行文件中,程序运行的时候将不再需要静态库。
- 动态库 (.so):程序在运行的时候才去链接动态库的代码,多个程序共享使用库的代码。
我们以一个简单的加减函数来介绍库:
// add.h
#pragma once
#include <stdio.h>
extern int Add(int a, int b);
// sub.h
#pragma once
#include <stdio.h>
extern int Sub(int a, int b);
// add.c
#include "add.h"
int Add(int a, int b) { return a + b; }
// sub.c
#include "sub.h"
int Sub(int a, int b) { return a - b; }
// main.c
#include "add.h"
#include "sub.h"
int main() {
int a = 20;
int b = 10;
int ret = Add(a, b);
printf("%d + %d = %d\n", a, b, ret);
ret = Sub(a, b);
printf("%d - %d = %d\n", a, b, ret);
return 0;
}
现在,我们分别将它们进行汇编得到可重定向二进制目标文件,最后在同一汇编即可得到可执行程序:
[hins@VM-12-13-centos Lib]$ ll
total 20
-rw-rw-r-- 1 hins hins 67 Jul 13 16:35 add.c
-rw-rw-r-- 1 hins hins 73 Jul 13 16:35 add.h
-rw-rw-r-- 1 hins hins 236 Jul 13 16:37 main.c
-rw-rw-r-- 1 hins hins 67 Jul 13 16:37 sub.c
-rw-rw-r-- 1 hins hins 72 Jul 13 16:37 sub.h
[hins@VM-12-13-centos Lib]$ gcc -c add.c -o add.o
[hins@VM-12-13-centos Lib]$ gcc -c sub.c -o sub.o
[hins@VM-12-13-centos Lib]$ gcc -c main.c -o main.o
[hins@VM-12-13-centos Lib]$ ls
add.c add.h add.o main.c main.o sub.c sub.h sub.o
[hins@VM-12-13-centos Lib]$ gcc add.c sub.c main.c -o mymath
[hins@VM-12-13-centos Lib]$ ls
add.c add.h add.o main.c main.o mymath sub.c sub.h sub.o
[hins@VM-12-13-centos Lib]$ ./mymath
20 + 10 = 30
20 - 10 = 10
上面的过程是一个程序实现的过程,然而对于一个库来说,库里面是不需要存在 main 函数的,因为库仅仅作为某一个功能的实现来供库的使用者调用,所以实际上只有 add.h add.c sub.h sub.c 属于库;同时,库的提供者通常不希望暴露函数的源代码,所以早期给别人提供函数时通常只提供头文件 (有哪些方法) 和 经过汇编后得到的可重定向二进制目标 .o
文件 (方法的实现)。
但是这样存在一个问题 – 如果一个库文件中的方法非常多的话,就需要向库的使用者提供对应个数的 .o 文件,非常麻烦且容易丢失;所以我们就尝试着将所有的 “.o” 文件进行打包,然后给对方提供打包得到的一个库文件,这就是库文件的由来,而根据打包工具和打包方式的不同,又分为动态库和静态库。
所以,库的本质是 .o
文件的集合。
🕘 4.2 静态库的制作与使用
制作静态库就是将多个 .o 文件打包到一个文件中,所以我们可以使用 Linux 中的归档工具 ar
(archive) (rc : replace and create):
[hins@VM-12-13-centos Lib]$ ls
add.c add.h add.o sub.c sub.h sub.o
[hins@VM-12-13-centos Lib]$ ar -rc libmymath.a add.o sub.o
[hins@VM-12-13-centos Lib]$ ls
add.c add.h add.o libmymath.a sub.c sub.h sub.o
[hins@VM-12-13-centos Lib]$ file libmymath.a
libmymath.a: current ar archive
注:我们最好将生成 .o 文件以及归档 .o 文件这些操作都统一到 Makefile 中。
libmymath.a:add.o sub.o
ar -rc $@ $^
add.o:add.c
gcc -c add.c -o add.o
sub.o:sub.c
gcc -c sub.c -o sub.o
.PHONY:clean
clean:
rm -rf *.o libmymath.a
上面我们将所有 .o 文件打包成了库文件,但是别人使用库文件时还需要头文件,所以我们可以做一个发布版本,将头文件也拷贝到对应路径下:
# Makefile
....
.PHONY:output
output:
mkdir -p mylib/include
mkdir -p mylib/lib
cp -f *.a mylib/lib
cp -f *.h mylib/include
....
[hins@VM-12-13-centos Lib]$ make output
mkdir -p mylib/include
mkdir -p mylib/lib
cp -f *.a mylib/lib
cp -f *.h mylib/include
[hins@VM-12-13-centos Lib]$ ls
add.c add.o main.c mylib sub.c sub.o
add.h libmymath.a Makefile mymath sub.h
[hins@VM-12-13-centos Lib]$ tree mylib
mylib
|-- include
| |-- add.h
| `-- sub.h
`-- lib
`-- libmymath.a
现在,我们的软件就已经发布出来了,我们就可以将其打包然后放在网站或者yum的资源中供别人进行下载使用了:
别人使用该静态库时首先需要从 yum 源或者其他地方将软件包下载下来,然后解压即可:
[hins@VM-12-13-centos Lib]$ tar xzf mylib.tgz
然后,我们在进行 gcc 编译汇编时需要通过 I
选项来指定头文件路径、通过 L
选项来指定库所在路径、以及通过 l
选项来指定库名称,最终得到可执行程序:
[hins@VM-12-13-centos Lib]$ gcc -o mymath main.c -I mylib/include -L mylib/lib -l mymath
注意:当我们链接库时,必须指定库的名称,这是因为同一路径下可能同时存在许多库 (头文件不需要指定名称,只需指定路径,因为 main 中指明了我们需要的头文件名称),同时,库需要去掉前缀 lib 和 后缀 .a/.so 才是库真正的名称,这里需要特别注意。
拓展:我们之前连接程序从来没有指明过库名称,这是因为 gcc/g++ 默认帮我们填写了库名称 , gcc/g++ 是 C/C++ 专门的编译器,且我们之前从来没有使用过第三方库,即 C/C++ 自带的库它能够帮我们默认填写。
虽然现在已经成功形成可执行程序并运行,但是这里还存在一个奇怪的地方:mymath 的依赖库中并看不到 libmymath.a,并且 mymath 是动态链接的;
[hins@VM-12-13-centos Link]$ ldd mymath
linux-vdso.so.1 => (0x00007fff6add5000)
libc.so.6 => /lib64/libc.so.6 (0x00007f975fde5000)
/lib64/ld-linux-x86-64.so.2 (0x00007f97601b3000)
[hins@VM-12-13-centos Link]$ file mymath
mymath: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.32, BuildID[sha1]=d007b89f497fc6e8abebf48f8149c373ab671364, not stripped
这是由如下原因造成的:
1、Linux 默认使用动态链接,这是针对动静态库都存在的情况说的,如果只存在静态库,那么 Linux 也只能使用静态链接,同样,如果只存在动态库,即使指明 static 选项也只会使用动态链接;
2、同时,一个可执行程序的形成可能不仅仅只依赖一个库,如果依赖的库中有一部分不只有静态库,有一部分库有动态库,那么形成的可执行程序整体是动态链接的,但其中只有静态库的地方也只能拷贝;
3、这里的现象和第二点一样,mymath 的形成不仅仅依赖一个库 (使用了 C 语言库函数),且Linux存在C语言动态库,所以这里是使用动态链接的,我们自己的库 libmymath.a 以静态的方式进行链接。
最后,除了指定头文件路径和库文件路径的方式,我们也可以直接将头文件和库文件拷贝到系统头文件及库文件路径下 (本质上就是安装),这样下次就可以指定库名称后直接链接了:
🕘 4.3 动态库的制作与使用
动态库的制作和静态库存在很多相似的地方,但也有不同:
1、动态库汇编形成 .o 文件需要指定 fPIC
选项,用于形成位置无关码(即靠相对位置定位);
2、动态库归档不使用 ar 指令,gcc 中指定 shard
选项就可以完成归档工作。
归档后的工作就和静态库一模一样了 – 发布、压缩、下载、指定头文件路径、库文件路径以及库文件名称:
现在一切准备就绪,但是当我们运行程序的时候却发现,程序运行出错了,找不到库文件:
[hins@VM-12-13-centos test]$ ./mymath
./mymath: error while loading shared libraries: libmymath.so: cannot open shared object file: No such file or directory
这是因为我们的库路径只告诉了 gcc,而 gcc 只工作到可执行程序形成,之后就与 gcc 无关了,但是动态库是程序在运行的时候才去链接动态库的代码的,而操作系统和 shell 并不知道库文件的位置,所以我们还需要在程序运行时告诉操作系统动态库的位置,而程序运行时操作系统会去两个地方查找动态库,一个是默认库路径下 (lib64),另一个就是环境变量 $LD_LIBRARY_PATH 中,所以我们可以将我们的库文件添加到这两个地方。
注:使用 export 配置环境变量只在本次登录有效,如果希望其永久有效,我们可以将其写入到配置文件 “/etc/ld.so.conf.d/” 中,即在该目录下新建一个文件,然后将库文件的路径写入其中,最后使用 ldconfig 更新缓存即可。
最后,我们还可以在系统库文件目录下为我们自己的库文件建立一个软链接,这也是永久有效的。
🕘 4.4 理解动态库加载
在学习动态库的加载策略之前,我们要先明白:静态库是不需要加载的,而程序需要加载。当静态库链接的时候,实际上是将代码(例如printf的实现)拷贝进程序中,所以后面程序运行的时候就不再依赖于静态库。
而一旦有很多程序,静态库就会拷贝大量重复的代码分给不同的程序。通过进程地址空间的知识我们知道当静态库拷贝代码给程序时,实际上是把代码拷贝进了代码区。因此在程序运行形成进程地址空间时,静态库中的代码只能被映射到进程地址空间相应的代码区中,未来的这段代码,必须通过相对确定的地址位置进行访问。
现在,我们来正式学习动态库加载,具体分成以下步骤:
1、如图,对于动态链接来说,可执行程序中存放的是动态库中某具体 .o 文件的地址,同时,由于组成动态库的可重定向文件是通过位置无关码 fPIC 生成的,所以这个地址并不是 .o 文件的真正地址,而是该 .o 文件在动态库中的偏移量;
2、然后就是程序运行的过程:操作系统会将磁盘中的可执行程序加载到物理内存中,然后创建 mm_struct,建立页表映射,然后开始执行代码,当执行到库函数时,操作系统发现该函数链接的是一个动态库的地址,且该地址是一个外部地址,操作系统就会暂停程序的运行,开始加载动态库;
3、加载动态库:操作系统会将磁盘中动态库加载到物理内存中,然后通过页表将其映射到该进程的地址空间的共享区中,然后立即确定该动态库在地址空间中的地址,即动态库的起始地址,然后继续执行代码;
4、此时操作系统就可以根据库函数中存放的地址,即 .o 文件在动态库中的偏移量,再加上动态库的起始地址得到 .o 文件的地址,然后跳转到共享区中执行函数,执行完毕后跳转回来继续执行代码段后面的代码。这就是完整的动态库的加载过程。
注:动态库可以避免静态库内存空间浪费的问题,这是由于如果多个进程链接了同一个动态库,动态库也只需要加载一次,动态库被加载到物理内存中并通过页表映射到某一个进程的共享区之后,操作系统会记录该动态库在该进程共享区中的地址,当其他进程也需要执行动态库代码时,操作系统会根据记录的地址加上偏移量通过页表跳转到该进程的共享区中执行函数,执行完毕后再跳回到当前进程地址空间的代码段处。所以从始至终物理内存中都只有一份动态库代码。
因此可以看出,动态库并不像静态库一样,不需要拷贝大量的相同代码,多个程序可以共享一份动态库中的代码。
OK,以上就是本期知识点“文件系统”的知识啦~~ ,感谢友友们的阅读。后续还会继续更新,欢迎持续关注哟📌~
💫如果有错误❌,欢迎批评指正呀👀~让我们一起相互进步🚀
🎉如果觉得收获满满,可以点点赞👍支持一下哟~
❗ 转载请注明出处
作者:HinsCoder
博客链接:🔎 作者博客主页