👍作者主页:进击的1++
🤩 专栏链接:【1++的Linux】
一,磁盘结构
我们前面学习的都是打开的文件,即内存文件。既然有被打开的文件,那么也有没有被打开的文件,其存在哪里呢?—磁盘,即磁盘级文件。
我们的内存是一种掉电易失的存储介质;磁盘则是一种永久性的存储介质—我们的U盘,磁带等也都是永久性的存储介质。并且磁盘是一种外设,而且是我们计算机中的唯一的机械设备,因此其的一个大缺点就是慢!!!
那么磁盘的结构是怎样的呢?
传统的磁盘就是如上这种结构,有一个或多个盘片用于存储数据,多采用铝合金材料。中间有一个主轴,所有盘片都围绕着这个主轴转,每个盘面上都会有一个磁头来进行数据的读写。盘面有正反两面,有多少个盘面就有多少个磁头。
磁头和盘面的距离是无线接近,但又不接触的,为的是防止高速旋转时,划伤盘面,造成数据损失。高速旋转的盘体产生明显的陀螺效应,所以,在硬盘工作时不宜搬动,否则,将增加轴承的工作负荷。为了高速存储和读取信息,硬盘驱动器的磁头质量小,惯性也小,所以,硬盘驱动器的寻道速度明显快于软驱和光驱。磁头停留在启停区,当需要从硬盘读写数据时,磁盘开始旋转。旋转速度达到额定的高速时,磁头就会因盘片旋转产生的气流而抬起,这时磁头才向盘片存放数据的区域移动。该区是没有数据的,适合磁头的软着陆。
磁盘在格式化时被划分成许多同心圆,这些同心圆轨迹叫做磁道(Track)。磁道从外向内从0开始顺序编号。硬盘的每一个盘面有300~1 024个磁道,新式大容量硬盘每面的磁道数更多。信息以脉冲串的形式记录在这些轨迹中,这些同心圆不是连续记录数据,而是被划分成一段段的圆弧,这些圆弧的角速度一样。由于径向长度不一样,所以,线速度也不一样,外圈的线速度较内圈的线速度大,即同样的转速下,外圈在同样时间段里,划过的圆弧长度要比内圈划过的圆弧长度大。每段圆弧叫做一个扇区,扇区从“1”开始编号,每个扇区中的数据作为一个单元同时读出或写入。扇区是磁盘进行读写的最小单位,其大小为512字节 。
二,磁盘的抽象结构
在物理上是如何将数据写入磁盘中的呢?
通过CHS寻址去找到指定扇区,然后进行写入。
那么什么是CHS寻址呢?
CHS寻址模式将硬盘划分为磁头(Heads)、柱面(Cylinder)、扇区(Sector)。先是通过磁头找到在哪一个柱面上,然后找到其在哪一个磁道上,最后找在哪一个扇区上。直到了CHS值我们就能找到任意一个扇区。那么OS也可以通过CHS来管理磁盘吗?答案是不可以!OS是软件,磁盘是硬件,要是通过CHS来管理磁盘,万一磁盘的物理结构变了,那不就OS也得改变。显然是不行的。
因此就有了磁盘的抽象结构。
如上图我们可以想到磁带的读取方法,我们将其展开,便可以得到一个线性的结构,那么我们能不能将磁盘也展开呢?–可以的!我们可以像拆线团一样,按磁道一层一层将磁盘展开,这是一段段的扇区就连到了一块形成了线性结构。然而,扇区又太小,每次读取这么小的单位,效率又太低,因此又将几个扇区合并到一块,形成4KB的逻辑块,OS就可以通过数组来对磁盘进行管理了。
并且要是OS和磁盘使用一样的基本单位,要是磁盘的基本单位变了,那OS是不是也得改?
所以为了软硬之间的解耦,OS每次IO的大小为4KB。
此时我们就初步完成了从物理逻辑到线性逻辑的抽象,由于数组天然的就有下标,那么我们只要知道了数组下表,不就能够找到对应的扇区了吗?通常OS是以4KB的逻辑块来进行IO的,一个逻辑块有八个扇区。计算机的常规访问方式:起始地址+偏移量。因此我们在读取时是第一个扇区的地址+4KB。这是不是和数组一摸一样,因此块的地址就是数组的下标。
因此OS通过数组下标就可以进行LBA寻址,从而找到逻辑块的地址。
但是磁盘只认识CHS地址,因此就又有了LBA----CHS之间的相互转化。
最后对磁盘的管理就变成了对数组的管理,这符合我们前面文章所提到了OS的管理方案:先描述,后组织。
三,文件系统
我们常见的磁盘基本都是成百上千GB大小的,直接管理难度太大。那么要怎么管理呢?分而治之!!!。将磁盘分区,这样我们可以将不同的文件分开,而且还可以提高寻址的速度。
一个区又可以分成若干个组。
如下图:
在 Linux 系统中,最长见的是 ext2 系列的文件系统。我们上述的讲解都是以ext2为例的
原则上,Block 的大小与数量在格式化后就不能够发生改变了,每个 Block
内最多只会存放一个文件的数据(即不会出现两个文件的数据被放入同一个 Block 的情况),如果文件大小超过了一个 Block 的 size,则会占用多个 Block 来存放文件,如果文件小于一个 Block 的 size,则这个 Block 剩余的空间就浪费掉了。sudo dumpe2fs /dev/sda1 | grep "Block size://查看逻辑块的大小
下面是每个分区的结构:
Boot Block:
每个磁盘分区的开头 1024 字节大小都预留为分区的启动扇区,存放引导程序和数据,所以又叫引导块。引导块在第一个 Block,即 Block 0 中存放,但是未必占满这个 Block,原因是 Block 的大小可能大于 1024 字节。
这里是存放开机管理程序的地方,这是个非常重要的设计。因为这样使得我们能够把不同的开机管理程序安装到每个文件系统的最前端,而不用覆盖整颗磁盘唯一的 MBR,这样就能支持多系统启动了。
MBR 共占用了一个扇区,也就是 512 Byte。其中 446 Byte 安装了启动引导程序,其后 64 Byte 描述分区表,最后的 2Byte 是结束标记。我们已经知道,每块硬盘只能划分 4 个主分区,原因就是在 MBR 中描述分区表的空间只有 64Byte。其中每个分区必须占用 16 Byte,那么 64 Byte 就只能划分 4 个主分区。每个分区的 16 字节的规划 .
还有一个问题,BIOS 只能找到 MBR 中的启动引导程序,而找不到在分区的引导扇区中的启动引导程序。那么,要想完成多系统启动,我们的方法是増加启动引导程序的功能,让安装到 MBR 中的启动引导程序(GRUB)可以调用在分区的引导扇区中的其他启动引导程序
每个分区又可以分成多个组。下面是每个组的结构:
super block:
其里面记录了:
block 与 inode 的总量
未使用与已使用的 inode/block 数量
block 与 inode 的大小(block 为 1,2,4K,inode 为 128 Bytes 或 256 Bytes)
filesystem 的挂载时间、最近一次写入数据的时间、最近一次检验磁盘(fsck)的时间等文件系统的相关信息
一个 valid bit 数值,若此文件系统已被挂载,则 valid bit 为 0,若未被挂载,则 valid bit 为 1。
Superblock 的大小为 1024 Bytes,它非常重要,因为分区上重要的信息都在上面。如果Superblock挂掉了,分区上的数据就很难恢复了。可以使用 dumpe2fs 命令查看 分区的Superblock 信息。
**Group Description:**该块组的描述符,描述该组内的属性信息,block的开始与结束位置等。
Block bitmap: 统计该组内的block哪些被使用了,哪些没有被使用。
Inode bitmap: 统计该组内的inode哪些被使用了,哪些没有被使用。
Inode table: 放着一个个inode,inode是一个大小为128kb的空间,里面记录了对应文件的属性以及数据实际放在哪些block中。一个inode对应一个inode编号,一般一个文件对应一个inode编号。
Data block: 这是用来放文件内容的地方。
为什么要有上述这样的分区:目的是为了让一个文件的信息可被追溯,方便管理。
快组分割,并写入相关的管理数据,从而将整个分区都写入文件系统,这其实就是我们的格式化。
若是文件特别大怎么办?
data block不止能存文件的内容,还能够存储其他块的块号。最终就可以得到好多的块。
在Linux中,系统只认识inode编号,文件名是给上层用户用的。
目录的本质也是文件,有其自己的inode编号,那目录的内容是什么呢?存的是该目录下的文件名以及其inode编号,在目录内文件名和inode互为key值。
为什么有时候我们会创建文件失败呢?因为data block的数量是固定的,inode的数量也是固定的。那么就会存在谁先用完,另一个还没有用完的情况。
四,文件的增删查改
查:
我们以log.txt文件为例。
首先OS会根据该文件的目录及其文件名找到对应的inode编号。结合目录的inode编号找到特定分区的特定组,在Inode table中找到该文件的inode进而找到文件所用的数据块。
删:
根据inode找到对应的区组,找到对应的inode,获得其所占用的数据块的编号,再找到blockbitmap,将其对应的位置为0即可,再找到inodebitmap,将其inode对应的位置为0 ,并没有删除其实际的数据内容,那么文件删除后能恢复出来吗?
答案是:可以的!!!只有能够找到其原来的inode,并且原来的数据没有被覆盖就是可以的 。
既然文件的删除只是将其对应的位图置0,
增:
创建文件后系统会给该文件分配一个inode编号,并在inodetable中记录其属性,在inodebitmap与blockbitmap中将对应的位置为1 。
五,软硬链接
如上述图,我们为test2创建了硬链接test2-hard;创建了软连接test2-soft。
我们通过对比软硬链接发现其权限后面的数字也发生了变化。这个数字代表的就是硬链接数。
我们可以通过ls -i来查看inode编号。
我们发现硬链接的inode编号是一样的。根据我们上面所描述,既然inode编号一样,那么其所对应的文件内容也是一样的,并没有创建新的我文件,也就是说这是同一个文件,只不过有两个名字对应同一个inode编号。
所以创建硬链接,实际做的是:在指定目录下建立文件名和指定inode的对应关系而已。
软连接的inode的编号不同,硬链接编号为1 。**因此其是一个独立的文件,**里面放的是指向文件对应的路径。这和我们window下的快捷方式是一样的 。
我们新建一个空文件夹,其本质也是一个文件。我们发现上述file文件夹的硬链接数竟然是2。
我们进入文件夹内,查看其所有内容(包括隐藏的文件)
我们发现其有两文件:. 和 … 。并且 . 的inode的编号和file文件夹的inode编号一样,因此 . 就是file的硬链接。也就是我们的当前目录。我们在运行程序时所写的: ./xxx.exe,就是在当前目录下运行程序。那么还有一个 … 是什么呢?我们退回上级目录。
与上级目录的inode 编号一样,所偶一… 代表的是上级目录的硬链接,因此其硬链接数位3 :文件夹,. ,… 。这样它就有三个硬链接了。我们常用的cd … 返回上级目录就是利用的其硬链接。
我们删除test2-hard,我们发现test2并没有一起消失,二是硬链接数变为了1,因此一个文件的删除,实际上是利用了引用计数,只有当计数为0时才会真正删除这个文件。
六,动静态库
1. 动态库和静态库是什么?
- 静态库(.a):程序在编译链接的时候把库的代码链接到可执行文件中。程序运行的时候将不再需要静态库
- 动态库(.so):程序在运行的时候才去链接动态库的代码,多个程序共享使用库的代码。
- 一个与动态库链接的可执行文件仅仅包含它用到的函数入口地址的一个表,而不是外部函数所在目标文件的整个机器码。
- 在可执行文件开始运行以前,外部函数的机器码由操作系统从磁盘上的该动态库中复制到内存中,这个过程称为动态链接(dynamic linking)动态库可以在多个程序间共享,所以动态链接使得可执行文件更小,节省了磁盘空间。操作系统采用虚拟内存机制允许物理内存中的一份动态库被要用到该库的所有进程共用,节省了内存和磁盘空间。
2. 创建动静态库
创建一个静态库:
我们写一个加法的函数。包括声明和定义。再写一个调用其函数的主函数。
我们将其全部生成.o文件,再进行链接生成可执行文件test。
我们./test 运行,是可以打印出结果的。
以上过程就是将一个外部的函数链接到我们的可执行程序中的过程。那么这就叫做静态库吗?
显然它还不能叫做库。
我们将myadd.o进行打包形成libmy.a,将libmy.a放进libs文件夹中,其.h文件放进include中,最终形成lib-staticd就是我们所创建的静态库 。(其.h文件是供外界使用的,告诉使用者使用方法)
我们在使用语言所提供的静态库时直接包头文件就可以使用,而我们所写的库叫做第三方库,需要给出其搜索路径系统才可以找到。gcc默认搜索头文件的路径为:/usr/include. 默认的搜索库文件的路径为:/lib64 or /usr/lib64 。
我们将库拷到默认路径下就叫做库的安装。
创建动态库
创建过程与静态库类似
加fPIC生成位置无关代码 库名规则 libxxx.so
shared: 表示生成共享库格式。
关于fPIC有下面的相关文章可供参考动态库之fPIC
七,动静态库的使用
我们先来说静态库的使用:
我们再创建一个新的test.文件,包myadd.h头文件后进行编译,
你会发现找不到这个头文件。原因是:头文件的搜索有两种路径。一种是在当前路径下查找头文件。一种是系统头文件路径下查找。所以,我们可以把头文件和库文件拷贝到系统路径下。
但我们发现
仍会报错,这是为什么呢?
虽然我们将自己所写的库拷贝到了默认路径,但是编译器是不认识我们的库的,需要我们指定链接的第三方库的名称(后加-l)
但是我们不建议这样做,因为这样会污染我们的系统的库文件和头文件。
我们可以指定路径;
-I(大写)的意思是:头文件查找路径
-L的意思是:库文件搜索路径
-l(小写)的意思是:在-L指定的路径下你要链接的是哪一个库.
动态库的使用:
第一种方法啊还是将路径拷贝到系统的默认路径下。
我们来看第二种方法:指定路径。
但是我们发现其会报错。
这是为什么呢?因为我们的动态链接是在程序运行起来后才会进行链接加载的。而-I和-L是作用与gcc的,在程序运行前起作用,所以我们运行起来的程序找不到库文件就会报错了。
解决方法:
第一种方法就是导入环境变量:
当程序运行时,会在环境变量中(LD_LIBRARY_PATH)查找自己需要的动态库路径。
ldd命令可以查看一个可执行程序依赖的共享库.
此方法,当我们重启xshell后,我们导入环境变量就会恢复默认值。
因此还有以下这种方法:
Linux是通过 /etc/ld.so.cache 文件搜寻要链接的动态库的。而 /etc/ld.so.cache 是 ldconfig 程序读取 /etc/ld.so.conf 文件生成的。(注意, /etc/ld.so.conf 中并不必包含 /lib 和 /usr/lib,ldconfig程序会自动搜索这两个目录)。
如果我们把 libmax.so 所在的路径添加到 /etc/ld.so.conf 中,再以root权限运行 ldconfig 程序,更新 /etc/ld.so.cache ,test3运行时,就可以找到 libmax.so。
配置文件还没有生效。
运行 ldconfig 程序
还可以通过软连接的方式进行:
动静态库的区别:
静态库:在链接阶段库将会与目标汇编后的目标文件.o一起打包生成可执行文件。成为可执行文件的一部分,后续此库就可以消失了。也就是说在编译的最后一步(链接阶段),如果程序需要使用静态库,在这一步都会一起打包到可执行文件中。
优点:
1、 使可执行文件依赖项少,已经被打包到可执行文件中了
2、 编译阶段完成链接,执行期间代码装载速度快
缺点:
1、使可执行文件变大
2、若作为其他库的依赖库,将会造成多余的副本,因为必须与目标文件打包
3、升级不方便,升级必须重新编译
动态库:而动态库在编译阶段都不会有什么动作,只有在程序运行时才被加载(被加载到堆栈之间的共享区),也就是动态库的链接是发生在程序运行时期的,它和可执行文件是分开的,只是可执行文件在运行的某个时期调用了它。
优点:
1、动态库可以实现进程之间资源共享,有一份就行。
2、升级程序简单,不需要重新编译。
缺点:
1、运行期间在加载,将会减慢代码执行速度。
2、增加程序的依赖项,必须跟着可执行文件一起