基础IO(下)

简介: 文件系统管理不仅涉及打开的文件,未打开的文件也需要管理,核心是快速定位文件以便通过路径访问。操作系统管理磁盘时,通过选择磁头、磁道和扇区进行寻址。磁盘逻辑上被抽象为数组,通过下标定位。文件系统将大磁盘分割管理,如分区、块组,每个文件对应唯一的inode,存储属性和内容。文件创建和删除涉及inode和数据块的分配与回收。文件查找通过路径确定分区,挂载将文件系统与分区关联。软链接通过路径引用,硬链接共享inode。静态库在编译时链接,动态库在运行时加载,动态链接节省资源。文件系统和库管理涉及内存、磁盘和程序执行的复杂交互。

实际上,大部分文件都不是被打开的(当前并不需要访问),都在磁盘中保存。那么对于没有被(进程)打开的文件,要不要管理呢?这部分文件核心工作是什么呢?

答案是肯定要管理的,这部分文件的核心工作就是快速定位文件,所以未来我们访问文件的时候就可以通过路径快速访问文件啦

文件的管理工作:

  1. 对打开的文件进行管理
  2. 没有被打开的文件也要在磁盘中进行管理

以上就是文件系统需要做的工作。

1. 磁盘

1.1 磁盘的物理结构

(从网上偷个图来~)

image-20240607103904376

一个盘面可以有很多的同心磁道

一圈磁道可以有很多扇形的扇区

扇区是磁盘的最小存储单元——512B/4KB

我们把磁盘称之为块设备,支持随机读取

如果我们想向一个扇区写入,我们该如何寻址、定位呢?

  1. 选择某一面——本质就是选择磁头
  2. 选择该面上的某一个磁道
  3. 选择在该磁道上的某一个扇区

我们可以向一个扇区写入,就可以向任意一个/多个扇区写入。连续多个扇区式的写入,也就是可以随机式的写入。

总结一下:

  • 磁头摆动定位柱面(磁道)
  • 盘片旋转定位扇区

1.2 磁盘的逻辑抽象结构

相信我们大部分人都见过磁带吧~

image-20240607104248232

磁盘的数据就存储在一个长条黑带子上,那么如果我们把这条黑带子拉直然后铺在一个平面上呢?同样的,磁盘也可以做出同样的类比,把一条条的磁道拉展平铺在一个平面上,于是乎就可以变成这样了:

(将磁盘盘片想象成线性空间)

image-20240607111122133

由此对磁盘的管理就可以抽象成对数组的管理!!

  • 数组都是有下标的,假定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。

image-20240607110706783

一个块的起始地址叫做LBA,也就是Logic Block Address(逻辑块地址)。

最终结论:

对存储设备的管理,在OS层面上,转换成为了对数组的增删查改!

2. 理解文件系统

2.1 前言

平常我们使用的硬盘小则512GB大则几个TB,如果我需要管理这整个磁盘,对我们属实有点小困难了。所以无论技术上还是应用上,我们都非常的迫切需要,不要将这个磁盘整体来管理。就好比一个学校的每个班级,不可能都由校长来管理,而是需要一个一个的班主任分别管理。

比如,我有一块512GB的硬盘,它又被划分成为了四个128GB的小块,于是乎我们只需将这个128GB的区管理好行啦,这样是不是就容易很多了捏。

但是这128GB依旧还是有点大,于是我们再把这128GB划分成一个个的小组,假定这个组以2GB为单位,那么总共就有64个组。此时,我们只需要管理好第一个组,那么后面的组就按照第一个组照本宣科的来就好了,这样就方便了许多,皆大欢喜!

image-20240607165213377

我们要把512GB空间管理好,只需要把2GB空间管理好即可!

这种思想就是典型的分治思想!

这些组具体又表现为这样:

image-20240609131108962

  • 这些块组里会保存我的文件信息,我的文件信息包括内容和属性,这些都是数据,并且内容和属性是分开存储的。
  • 与此同时在这些组里会保存很多的文件管理数据,其实这些数据会把相应的块组管理起来,结合块组也要把文件的属性和内容管理起来。
  • 在正式使用磁盘之前得先要让管理数据写入到块组当中。这个工作称之为格式化。所以说格式化只是清空你的数据而并不清空管理数据。

2.2 文件系统

使用命令:

ls -li

image-20240609105637697

可以发现,在我们的文件列表前多了一行数字,那么这行数字是什么捏?

  • 这些数字是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 文件的新建和删除

新建一个文件的过程:

  1. 首先查询inode Bitmap,找到一个没有被使用的比特位,由0置为1,记住偏移量。
  2. 根据刚才的偏移量在inode Table内将文件的属性写入
  3. 在Block Bitmap中寻找一个没有被使用的比特位,由0置为1,记住偏移量。
  4. 根据此偏移量在快组中找到相应的块,将文件的内容写入。
  5. 记录分配情况。内核在inode上的磁盘分布区记录了上述块列表。
  6. 添加文件名到目录
    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 文件名 被链接的文件名

image-20240609215229955

在分别对软件进行软硬链接后我们发现:

软链接的两个文件inode各不相同,而硬链接的两个文件inode完全相同。

在此可以得到一个结论:

软链接是一个独立的文件,而硬链接不是,因为他没有独立的inode编号。

什么是软链接:

独立文件,拥有独立的inode,其内容是指向目标文件的路径。

什么是硬链接:

不是独立文件,是在指定目录内部的一组映射关系,文件名和inode的映射关系!

image-20240610103957356

当我们删除test.hard文件时,文件的硬链接数也随之而改变:

image-20240610104410682

一个文件什么时候真正被删除?

没有文件名和inode映射了,也就是没有人使用的时候了。我们在删除文件时干了两件事情:

  1. 在目录中将对应的记录删除,

  2. 将硬连接数-1,如果为0,则将对应的磁盘释放。

在文件系统层面,目标文件怎么知道有没有文件名指向inode了呢?

inode内部有引用计数,表明有几个文件名映射关系。文件名在目录里具有唯一性,文件名就好像一个指针,指向了inode。

新建一个文件夹和文件:

image-20240610105838860

我们发现,新建的文件的硬链接数默认是1,但为什么新建的目录的硬链接数却是2呢?

进入目录并查看目录内的所有文件:

image-20240610105903390

这时,我们又发现,新建目录内的.文件的inode和目录的inode一摸一样,这就是目录的一个硬链接~

如果我们再在newdir这个目录中再建立一个目录呢,看看有什么效果:

image-20240610110514932

我们又又发现,newdir的硬链接数变成了3!!其实不难理解,我们进入到在newdir目录下新建的目录下看一下:

image-20240610110649789

这里..文件的inode和我们newdir文件的inode是一样的,所以..就是表示上一级目录, newdir目录的硬链接数也随之变为3了。

用户无法对目录建立硬链接!

image-20240610114811524

3. 动态库和静态库

  • 静态库(.a):程序在编译链接的时候把库的代码链接到可执行文件中。程序运行的时候将不再需要静态库。
  • 动态库(.so):程序在运行的时候才去链接动态库的代码,多个程序共享使用库的代码。
  • 一个与动态库链接的可执行文件仅仅包含它用到的函数入口地址的一个表,而不是外部函数所在目标文件的整个机器码。
  • 在可执行文件开始运行以前,外部函数的机器码由操作系统从磁盘上的该动态库中复制到内存中,这个过程称为动态链接(dynamic linking)。
  • 动态库可以在多个程序间共享,所以动态链接使得可执行文件更小,节省了磁盘空间。操作系统采用虚拟内存机制允许物理内存中的一份动态库被要用到该库的所有进程共用,节省了内存和磁盘空间。

库中是没有main函数的,我们也不能将main函数写入库中

3.1 生成静态库

为了方便理解静态库,我们写一个简易版的计算器。其实包括了这些.c.h文件

image-20240610174659600

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

然后就会生成一堆这玩意:

image-20240610174501079

形成的.a文件就是静态库,本质上就是把这一堆.o文件打了包。

进入我们的测试文件夹,在此文件夹下创建一个测试所用的TestMain.c文件,直接运行:

image-20240610181022425

毫无意外的,运行报错!因为库什么的都没有在这个文件夹下(好像那二傻子QAQ)

先把头文件拷贝进来:

image-20240610203954141

头文件都是公开的,要被保护的只有库文件。再次编译:

image-20240610181317667

此时,即使提供了头文件但编译依旧报错,这个报错为链接报错,因为第三方库(你自己写的库),gcc默认是不认识的!

介绍一下一些选项(以下选项后带不带空格都可!):

  • -L:指定库的搜索路径
  • -l:指定库名(去掉lib和.a的真实库名)
  • -I:指定头文件搜索路径

使用命令进行编译:

gcc TestMain.c -l mycalculator -L.

image-20240610182047700

编译成功,并且成功运行!

但是如果这样把静态库交给使用者那也太矬了,一堆头文件显示在界面上,很难看,于是乎,我们修改一下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目标二进制文件,然后打包。

执行一下,先makemake output,此时我们看到目录里形成了mycalculator_lib的一个目录:

image-20240610210639580

但是此时如果我再去运行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

然后就可以开始使用动态库了,在使用上与动态库几乎没有什么区别!

但是此时,运行可执行文件却发生了错误!

image-20240611122357184

指明-L只是告诉了编译器我们的库在哪里,我们对应的可执行程序在加载运行时,还要告诉系统我们的动态库在哪里,因为当前的库并没有在系统的默认路径下。

image-20240611145044071

可以看到,可执行程序找不到我们的动态库。那么该怎么解决呢?

  • 方法一:直接将我们的库安装到系统里。(使用别人的库最推荐的做法!)

用我现在所使用的这个库举例。

使用命令将头文件安装到系统:

sudo cp mycalculator_lib/include/*.h /usr/include/

查看一波~

image-20240611150712931

安装动态库:

sudo cp mycalculator_lib/lib/libmycalculator.so /lib64/

image-20240611151013705

现在直接编译:gcc TestMain.c

image-20240611151434856

头文件能找到了,但是对应的定义还未找到,也就是库还未被使用,此时指定库名即可gcc TestMain.c -l mycalculator

image-20240611151604800

完美生成可执行程序文件并且完美运行!巴适~

  • 方法二:建立软链接。

ln -s mycalculator_lib/lib/libmycalculator.so libmycalculator.so

image-20240611152546751

可以很清楚的看到,此时我们的动态库不再是not found了。

同样的我们也可以直接把软链接安装到系统中!

  • 方法三:导入LD_LIBRARY_PATH环境变量中,让系统找到动态库。

export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/minnow/test/mycalculator/test

image-20240611153905610

添加成功!此时系统也知道了我的库在哪个环境变量下。此时可执行程序也可成功运行了!

  • 方法四:直接更改系统关于动态库的配置文件:/etc/ld.so.conf.d/

该配置文件是管理系统相关动态库加载的文件。新建并访问一个配置文件,将动态库所在的路径写入即可:

sudo vim /etc/ld.so.conf.d/my_lib.conf

image-20240611160002144

此时,动态库也能被找到了。

(注:如果动态库还是not found,使用命令:sudo ldconfig刷新一下配置文件)

同一组库,同时提供动静态两种库,gcc默认使用动态库

3.3 动态库加载

Linux下的可执行程序是ELF格式的可执行程序。
动态链接的程序,不光光是自己要加载,链接的库也要加载到内存中。

  1. 程序没有被加载到内存,程序内部有地址吗?
  2. 变量名、函数名等,编译为二进制后,还有这种概念吗?没有

通过以上两点,我们要知道,在编译的时候如何对代码进行编址的问题。其基本遵循虚拟地址空间的那一套。

虚拟地址空间,不仅仅是操作系统里的概念,编译器在编译的时候,也要按照这样的规则编译,这样才能在加载的时候,进行从磁盘文件到内存的映射。

我们的可执行程序在编译之时,就已经有了虚拟地址,也就是逻辑地址

整个代码可以理解为基地址(0)+偏移量[0-0xFFFFF]的编址方式。这种起始地址为0的编址方式,我们叫做平坦模式。

平坦模型(Flat Model):

是一种内存管理模型,其中整个4G字节的内存被视为一个连续的大段来处理。在这种模型下,每个段都指向4G字节的内存空间,其基地址为0,段界限为0xFFFFF,段粒度为4K字节。

绝对编制适合平坦模型

相对编制适合形成库函数中的地址,此时就很好理解编译链接形成动态库时的-fPIC选项,产生位置无关码了。

库被加载之后,要被映射到指定使用了该库们的进程地址空间中的共享区部分。
我们想做到让库在共享区的任意位置,都可以正确运行。一旦库加载之后,位置就是确定的。

  • 当多个可执行程序需要同一个库时,只需将该库映射到共享区即可,整个系统中只会存在一份库,所有代码和可执行程序共用这一份库,所以它被称之为共享库也叫动态库。
  • 但是静态库就不一样了,有几个可执行程序就会加载几份库,非常的浪费空间!
目录
相关文章
|
5月前
|
缓存 Linux API
文件IO和标准IO的区别
文件IO和标准IO的区别
56 2
|
3月前
|
存储 Linux 开发工具
基础IO(上)
本文主要讲述了文件描述符、重定向以及缓冲区的概念和运用。
28 1
基础IO(上)
|
10月前
|
缓存
标准IO和直接IO
标准IO和直接IO
71 0
day26-系统IO(2022.2.23)
day26-系统IO(2022.2.23)
|
缓存 Linux C语言
基础IO+文件(一)
基础IO+文件
77 0
|
编译器 Linux vr&ar
基础IO+文件(三)
基础IO+文件
71 0
|
存储 Linux 块存储
基础IO+文件(二)
基础IO+文件
65 0
|
存储 Linux 文件存储
基础IO详解(一)
基础IO详解
107 0
|
存储 Linux C语言
基础IO详解(二)
基础IO详解
89 0
|
存储 设计模式 缓存