目录
🏆一、磁盘的浅显认识
磁盘上面有大量的文件,是被静态管理起来的,方便我们随时打开。
1、磁盘的物理结构
磁盘是一个机械结构,并且还是外设,所以磁盘访问会很慢。磁盘是我们计算机中的唯一的一个机械结构。
目前在个人电脑已经很少使用磁盘了,很多笔记本,台式机都使用固态硬盘来存储。
而在企业端,磁盘存储依旧是主流。
2、磁盘的存储结构
那我们要在磁盘上查找文件,究竟是如何查找呢?磁盘寻址的时候,基本单位不是bit,更不是byte。而是划分扇区来查找的,一般一个扇区大小是512byte。
首先,这个像蚊香一样的东西就是我们的磁盘。每一圈都是一个磁道,而磁道被划分为无数的扇区。需要注意的是,虽然磁道的周长不一样,但是存储的数据量是一样的,是为了减少软件编码的难度,每个扇区大小也都是一样的,都是512byte。
确认磁道这一过程并不困难,难的是不仅要确认查找扇区,还要在非常短的时间里读取信息。
光盘高速旋转,有的甚至上网转,对磁头来说要在非常短的时间内就要确定清楚哪一个扇区,并且还要把这个扇区的二进制序列全部读取出来,这是最难的,一旦错过就得再来一圈。而反过来讲,之所以转速不能太快,是因为转速是由磁头定位和读取数据能力决定的。
3、磁盘的逻辑结构
设计为数组结构,也是降低了操作系统和磁盘结构的耦合性,具有普适性,不会因为磁盘的物理结构改变而影响代码的可用性。
每个数组大小都是一个扇区512byte。
所以我们只要知道一个扇区的下标,就算定位了一个扇区!而在操作系统内部,我们称这种地址为LBA地址(逻辑地址)。
我们不妨再来做一个假设数据方便我们定位:
那么,我们现在定位一个磁盘位置就很轻松了:
而实际中我们的磁盘都是一摞放的:
所以磁头数等于面数,而所有的磁头是整体一起移动,共进退的。不是拿着一个磁头在一面上去找,而是拿着一摞磁头在所有的同磁道上去找!
OS是如何进行扇区读取的呢?OS内的文件系统读取是以1KB,2KB,4KB为基本单位!所以哪怕只想读取/修改1bit,必须将4KB load到内存,进行读取或者修改,如果必要,再写回磁盘!
那这样不会造成浪费吗?这是操作系统采取的一种策略(局部性原理),当我们想访问一段数据,有很大机率访问它周围的一些数据,将这些数据load出来会有更高的数据命中。 本质是一种空间换时间的做法。
4、分治思想管理内存
内存被划分成了4KB大小的空间,称为页框;磁盘中的文件尤其是可执行文件,是按照4KB大小划分好的块,被称为页帧。
那么具体拿一块500G的磁盘,那么操作系统如何管理呢?操作系统采用分治的思想,对操作系统来说100G管理好了,那管理100G的方法管理其他100G,是一样的。而这100G可以再划分,比如说20个5G,那管理好其中一个5G空间,其余空间管理复用方法就可以了。
以上的过程称为分区。
上图是磁盘文件系统图,磁盘是典型的块设备,硬盘分区被划分为一个个的block。一个block的大小是由格式化的时候确认的,并且不可更改。我们再来详细介绍图中的名词:
Block Group:文件系统会根据分区的大小划分为数个Block Group。而每个Block Group都有着相同的结构组成,每个Block Group是一个分组。
超级块(Super Block):存放文件系统本身的结构信息。记录的信息主要有:bolck 和 inode的总量,未使用的block和inode的数量,一个block和inode的大小,最近一次挂载的时间,最近一次写入数据的时间,最近一次检验磁盘的时间等其他文件系统的相关信息。Super Block的信息被破坏,可以说整个文件系统结构就被破坏了.
GDT: Group Descriptor Table:块组描述符,描述块组属性信息。
块位图(Block Bitmap):Block Bitmap中记录着Data Block中哪个数据块已经被占用,哪个数据块没有被占用
inode位图(inode Bitmap):每个bit表示一个inode是否空闲可用。
inode表:存放文件属性 如 文件大小,所有者,最近修改时间等
数据区:存放文件内容
因为文件是内容和属性的组合,我们看到Linux的文件属性和文件内容是分批存储的。我们这里需要介绍inode---文件几乎所有的属性但不包括文件名在inode中存储,而文件内容存储在data block中 。每个文件对应一个inode编号,每个inode都有自己的ID。但是我们用户使用的不是inode,而是根据文件名。那么这就是inode bitmap的作用。通过映射由文件名找到对应的inode,再找到文件内容。
inode位图标识哪些inode是空闲的,而每个inode都是一个inode表,其中存储有block地址,通过存储的block地址去查找data blocks,获取信息。如果inode位图表示inode空闲的较少,那么就会考虑添加二级索引,所谓二级索引就是inode中存储指向的block中也存储着block地址,通过这样的方式达到存储更多信息的目的。与此更复杂的是三级索引。
而通过inode编号查找一个文件可以跨组,但是不能跨分区。
🏆二、软硬链接
在介绍软硬链接之前,我们需要再来看一下文件的基本属性。
[root@localhost linux]# ls -l
总用量 12
-rwxr-xr-x. 1 root root 7438 "9月 13 14:56" a.out
-rw-r--r--. 1 root root 654 "9月 13 14:56" test.c
这其中每行包含7列:
权限 硬链接数 文件所有者 组 大小 最后修改时间 文件名
其中硬链接数和我们的硬链接关系很大。
软链接:
ln -s 建立软链接,它的inode和它链接的文件的inode不相同
硬链接:
通过观察,我们发现
同时我们也理解了inode为什么不存储文件名,是为了硬链接服务,方便多个文件名对应于一个inode。
软硬链接的区别:是否具有独立的inode!软链接具有独立的inode,可以被当作独立的文件看待。那么硬链接没有独立的inode,如何理解硬链接呢?
①硬链接
我们看到,建立硬链接,根本没有新增文件,因为没有给硬链接分配独立的inode。既然没有创建文件,那么一定没有自己的属性集合和内容集合,用的一定是别人的inode和内容!
所以,如果我们将myfile.txt删除,那么它的硬链接数应该--。
硬链接的实际应用:
我们创建一个文件,一个目录,文件的硬链接数只有一个,而为什么目录的硬链接数有两个呢?
我们看到empty文件中,有一个.和两个.
一个.表示当前目录,两个.表示上级目录
所以.其实就是empty的一个硬链接,所以empty有两个硬链接数。而..是上级目录的一个硬链接,在上级目录,也有他自己的.再加上他自己的文件名,总共有三个硬链接数!!
而且这里还有一点要注意,那就是Linux不允许普通用户给目录建立硬链接。因为这一行为可能把目录树变为一环形图从而就不可能通过名字定位一个文件。
②软链接
那么软链接有什么用处呢?其实软链接就相当于在Windows下的快捷方式。
软链接的应用场景:
我在/home/Gyh/lesson6目录下创建形成可执行程序myfile。
如果我在其他路径下,想执行这个程序,就必须找到它的路径去执行:
而如果我给它建立软链接,就十分简单了:
🏆三、动静态库
静态库(.a):程序在编译链接的时候把库的代码链接到可执行文件中。程序运行的时候将不再需要静态库。
动态库(.so):程序在运行的时候才去链接动态库的代码,多个程序共享使用库的代码。
如果我们不想给对方我们的源代码,给他们提供我们的.o可重定位目标二进制文件,让他们用他们的代码来进行链接。
1、静态库和静态链接
静态库的名称要去掉前缀lib和后缀.a,中间就是库的名称。
使用ar - rc 方式将.o文件归档成静态库
ar是gnu归档工具,rc表示(replace and create)
归档完之后,我们可以查看静态库中的目录列表。
[root@localhost linux]# ar -tv libmymath.a
rw-r--r-- 0/0 1240 Sep 15 16:53 2017 add.o
rw-r--r-- 0/0 1240 Sep 15 16:53 2017 sub.o
t: 列出静态库中的文件
v: verbose 详细信息
而我们能写c文件,是因为库给我们提供了头文件和库。
所以我们交付库的本质就是库文件(动态库.so文件或静态库.a文件)再加上匹配的头文件都给别人。
完整地对库进行打包发布的过程:
最终我们 就形成了一个mylib的打包文件,它包括打包好的头文件目录:include和打包好的静态库。接下来我们怎么发送呢?就需要将它压缩成一个压缩包。
那我们一般下载软件都是这样的,下载压缩包再解压拿到头文件和库。
那我们在shell上如何编译呢?
-L : 指定库路径
-I : 指定头文件
失败是因为我们必须具体告诉gcc到底要链接/mylib/lib路径下的哪一个库!!而不能只给个目录!因为如果/mylib/lib路径下有好几个库,那么gcc编译器是无法确认到底要链接哪一个库。
所以如果要链接第三方的库,就必须指明库名称。那么问题又来了,我们之前写代码,为什么就不需要指明库名称呢?这是因为gcc、g++会默认为我们添加。
2、动态库和动态链接
shared:表示生成共享库格式
fPIC:产生位置无关码(position independent code)
库名规则:libxxxxx.so
我们生成后,试着链接动态库:
这里我们明明告诉了要链接哪一个库,为什么还会失败呢?
我们看到编译是可以通过的,说明编译没问题,问题出在执行。我们在这里只是告诉了gcc库文件、路径和库名称。而当程序编译完;和gcc就没关系了,我们只告诉了gcc而操作系统没能获知这一信息,所以操作系统也需要知道库在哪里!
而我们这里根据库默认搜索路径就可以提出几种解决方案:
库搜索路径:
1、从左到右搜索-L指定的目录
2、由环境变量指定的目录(LIBRARY_PATH)
3、由系统指定的目录
/usr/lib
/usr/local/lib
那么我们如何告知系统呢?有4种方式:
①添加到默认环境变量
系统在查找路径时,除了去默认库路径下查找,还会到环境变量(LIBRARY_PATH)中查找头文件。
所以我们把我们的动态库添加到默认环境变量中:
但是这种方式只在本次登录有效,我们下次登录时系统会自动将我们添加的环境变量清空。所以这种方式只能临时测试使用。
第二种方式就是将我们的动态库拷贝到
/usr/lib和 /usr/local/lib,拷贝到系统会默认去查找的路径下。
这种方式我们不太建议,会污染我们的标准库。
②创建添加.conf文件
这个方式添加的路径是永久有效的!而我们这些操作需要root权限(sudo提权)。
③建立软链接
建立软链接后,我们搜索动态库默认就能直接在当前目录下搜索到!
我们可以安装一个外部库来玩一下:
最后我们来谈一下动静态库的加载及理解。
上面这个图展示了如何加载动态库的一个过程,当然需要讲解一下过程。
当my.exe需要调用一个libc.so中的函数时,my.exe并不加载这个函数,而是加载进函数在libc.so库中的偏移量。同时libc.so库加载到内存中。动态库加载进来后通过页表映射到共享区。而my.exe存储的是偏移量,内存加载进来my.exe后也是通过页表映射,不过是映射到代码区。然后在调用函数时,因为函数的代码并没有加载进来,所以根据偏移量地址在.so中寻找,根据函数在库中的偏移量和库在共享区的起始地址跳转到共享区访问。至此完成动态库的加载和访问!
这样我们的.so动态库只需拷贝映射一份到共享区,所有需要用到他的都可以根据偏移量去共享区查找。大大节省了空间!