将磁盘抽象为一个一维数组(LBA寻址)后,需要再理解抽象的分区:
前置知识:
硬件从磁盘读文件,是以1个扇区为基本单位(512字节)交换数据的->磁盘读取基本单位为扇区
操作系统从磁盘读文件,以1到4kb为一个块为基本单位读-(其实内存,文件也被操作系统划分为了块为单位的交换单位)>避免频繁IO,增速
实现:磁盘默认支持LBA/CHS(两者能互转)。块号*块内扇区数+几个多余的扇区就能精确定位磁盘某扇区,找它要数据。--》再次将磁盘抽象为块为单位的数组
一。操作系统对大磁盘的管理
磁盘大容量直接管理麻烦,一个部分崩了影响大->将磁盘拆分多个分区管理 管理一个分区的方法用于多个,管理一个等于管理好多个
分区很大,管理麻烦,一个部分崩了影响大->将分区拆为多个组管理,管理一个组,方法适用于多个组
---->分治思想:开分身管理 独立部分
分治好处:
-
性能优化:不同类型文件放不同地方,集中管理,方便找(对机械硬盘(HDD),将高负载分区(如数据库分区)放在磁盘外圈(转速更快)可提升读写速度(SSD因无机械结构影响较小),且节省资源:不需要频繁刷新的就不频繁刷了
-
数据安全:1.一部分数据丢了或者部分损坏,只需要针对这独立的部分,不需要整体搞 2.重要的放一个分区,提升需要权限,病毒不易入侵
图为磁盘->多分区->多组结构,BootBlock为磁盘第一个扇区(对磁盘是第0号扇区),装了系统启动引导部分,各种表项,mbr等。
磁盘每个组的管理方式:
前提:文件在磁盘的组织方式:内容+属性(inode结构体),其中inode不包含文件名,一般inode占128或256字节。内含blocks数组,根据内数寻找文件对应的块。inode编号并不是随机分配的,其结构隐含了 所属块组 和 组内偏移量 的信息,根据inode编号找到对应Inode位置

组的划分:
1.inodeTable:多个块存储inode
2.DataBlocks:以块为基本单位(多为4kb)存储文件的内容,大部分组的空间分配给它
----->文件属性和内容分开存储
3.InodeBitmap:记录inodeTable使用信息,申请inode创建,看bitmap,空闲的直接置1,在inodeTable某个256字节(inode大小)为基础的位置创建。
提问:系统不是以块或者扇区为单位吗,怎么做到一个位一个位写的:整个块位单位读出写入。
4.BlockBitmap:记录数据块使用信息
5.GDT:块组描述符,管理inode和datablock总共数量记录和已经使用信息,让进来文件判断能不能在这个组存
6.SuperBlock:如图,并且不是每个组一个,多个组几个,用于备份,防止文件系统挂掉
向磁盘申请一个文件写入字符串步骤:
删除一个文件步骤:
inodebitmap:位置置0,blockbitmap:位置置0.
---->文件删了可能恢复,根据删除记录删
格式化本质:
inodebitmap,blockbitmap清零,gdt,superblock初始化。
三.理解文件,目录,几个问题
一.怎么初始化组的大小的
1.文件系统内inode和block数量固定:组内数量同样固定,inode与block以一定比例固定数量,如一个inode10个块
2.一个分区一套inode(一个分区一个文件系统),不能跨文件系统
3.分配inode只要知道起始inode(记录在gdt内),分配block只需要知道起始block
4.分配了组的inode,block数量,inodetable,blockgroup,inodebitmap,blockbitmap,gdt,superblock大小都已经确定,整个组就确定了
二.如何分配一个inode的编号的
去inodebitmap找个空位置,其顺序+其组的inode起始值-->局部到全局
三.如何分配块号的
同上。
四.inode与块关联
inode里记录块的全局编号,这样使文件可以分多组存大文件
五.怎么通过inode编号找inode与其对应块
inode编号去每个组的区间找,属于的区间的起始inode编号找到inodebitmap对应位置,再找到inodetable找inode,再找记录的blocks找块的绝对编号,再找到块。
六,怎么删文件
先inode查找,找到bitmap置0,找到Inode找blocksbitmap置0,更新GDT
七.如何新增
八.inode和block的真实映射
inode里的block数组固定的吗多大?不会不够吗?
----------------------------------------------目录操作-----------------------------------------------------------------------
九.怎么从文件名找inode编号
linux中目录也有inode编号,可见目录也是文件,内部存了文件名与inode编号的映射关系。
引申:目录的rwx
目录没有r权限不能知道其里面有什么文件:因为没有目录的读权限,不能读里面存的文件名等。
没有w权限不能向里面加文件:不能向目录文件内写入映射关系。
没有x权限不能进入目录:本质是打不开目录文件。
十.为什么不用文件名而用inode编号?(同用户进程有uid,pid)
字符比较查找麻烦,效率低!
十一.ll干了什么
十二。为什么任何一个文件都要有路径(同为什么进程要有cwd)
为了做路径解析,任何文件都有文件名,其Inode在上层路径(除了根目录,根目录的文件名与inode映射是写死的)
十三.如何找接近的路径的文件
全部路径再解析太麻烦--->内存级路径缓存
缓存结构:多叉树 struct struct_dentry
符合系统文件结构,下一次访问相关路径只需要先在路径缓存中找,找到部分再去磁盘找剩下的。
(这也是find指令找第一次慢,后来就快了的原因)
相关操作:LRU最近最少访问的路径去除
-----------------------------------------进程文件整体认知----------------------------------------------------------------
十四,
file含path加载dentry加载inode
---------------------------------------------------------------------------------------------------------------------------------
如何确定在哪个分区
挂载的本质:通过修改 dentry
和 vfsmount
的关联关系,实现路径解析的“重定向”
四.软硬链接
一.软链接
本质:一个独立文件,保存对应文件的路径,等同于快捷方式
作用:1.便捷访问(重定位):在/usr/bin路径下建立自己程序的软连接,就可以不限路径访问。或者在浅层路径建立深层路径文件的软链接,在浅层就能访问,不用进深层。
二.硬链接
本质:对应文件在文件夹内对文件名(不同)和inode(同)的新映射
补充:
1.硬链接数:权限后的数字是指Inode与文件名映射数量,只有映射数量归0,系统才去删文件。
2. .和..
.和..其实是对当前路径/上层路径的硬链接(也是为什么空文件夹的数字是2,文件是1),作用是方便定位此层路径的文件,且快速访问简化路径操作(避免拼写长路径)。可以通过目录的数字看内部第一层有多少目录
作用:1..和..简化路径操作
2.方便备份,不需要拷贝
注意:不允许对目录硬链接:避免环状路径(系统的.和..被特殊处理了)
为什么只有硬链接不允许链接目录?
因为软链接在查找时不会混淆,权限第一位是l,硬链接显示就是一个目录,易混淆(权限字段的第一位字符代表文件类型(而非权限))
五.动态库和静态库(续)
1.打包静态库步骤
1.创建.o目标文件
2.用ar -rc lib库名.a ...o ..o
2.安装步骤:
一。将库放到默认目录
1.头文件放到/usr/include目录下,gcc默认从这找
2.把静态库放到/lib64目录下,gcc默认找库路径
3.将自己的文件和使用的头文件方法所在库文件文件名-l链接(因为是第三方库,gcc,g++不会自动找库)
结果:
或者
二。指明目录给gcc找库
三。指明目录给gcc找头文件
-I目录
附加:为什么不同平台需要用不同版本的库?
答:因为不同平台的.o文件不一样(有的程序底层系统调用,如printf等),库又是由.o来的。
1.打包动态库步骤
一。将.c文件以创建与位置无关码的方式编译为.o
二。将.o文件用gcc -shared选项创建.so动态库
2.使用步骤
1.向系统告知.so动态库位置(因为之前g++ -L 是向编译器指定,但动态库的使用是动态加载的,系统不知道,直接和你的程序链接会显示找不到)
1.将.so库拷到/lib64系统默认寻找位置
2.修改系统的环境变量,让系统在指定目录找你的库 LD_LIBRARY_PATH
-
临时变量 → 直接用
export
。 -
永久变量 → 写入配置文件并
source
或重新登录。
3.系统路径建立软链接
为什么是软链接:
-
避免硬链接,因为动态库可能需要更新,软链接更易于维护。
4.
提示:编译时,编译器默认先找动态库,没有再找动态库,要指定可以gcc编译命令中加-static。
ldd 名 可以查可执行程序的动态库
3.为什么要打包使用动态静态库
-
简化依赖管理:将多个目标文件(object files)打包成一个库文件,便于管理和分发
-
减少文件数量:避免项目目录被大量零散的.o文件污染
-
链接效率(静态库):链接器处理一个库文件比处理多个.o文件更高效
-
独立更新(动态库):可以更新动态库而不需要重新编译依赖它的程序
-
减小可执行文件体积(动态库):特别是使用动态库时
六.程序调用动态库从磁盘到虚拟地址空间的加载
问题:
1.1问:动态库和程序的elf是不同的两个部分(虚拟地址不统一),怎么找到库函数的?
1.2问:代码区直接库名:偏移量,库和节都载入内存,将库名替换为物理地址不就能找到了吗,为什么不用?
答:1.代码区载入,是不允许修改的(编译器编译完就加了权限,保护代码区)。2.可以通过在elf添加GOT节,存储库名和偏移量,GOT载入内存,库载入内存时就将GOT条目中库名改为实际物理地址,这样,执行时代码区指令去GOT找指令,就能找到函数实际位置了。---(本质是操作系统给内存指定位置写入库位置,GOT就相当于操作系统与编译器两个模块的约定,编译器负责创建外壳,操作系统负责修改实际内容,不涉及代码区了)
2问.既然动态库给操作系统管理,给多个程序用,怎么判断什么时候移出内存呢?
答:有引用计数,同文件inode与文件名间的引用,引用归零才移除。
加载并使用动态库节与库的交互
(无懒加载时)磁盘先加载程序,读取并选择载入elfheader,headertable,sectionheadertable,根据headertable将程序LOAD段的节载入内存,
根据这些信息,根据节的大小等初始化虚拟地址空间(即在mmstruct内创建并初始化vm_area_struct。start,end等)
然后在页表内创建虚拟地址与物理地址间的映射,载入权限。
然后载入程序所需要的动态库,内存中创建libso结构体,将动态库放入共享库区域。将程序的节内GOT存的动态库名:偏移量改为动态库的实际地址:偏移量。
开始运行,pc载入header内 存的entry虚拟地址,给MMU硬件翻译为物理地址,载入指令执行,一直到载入需要动态库的指令,如GOT虚拟地址:偏移量时,翻译为物理地址去内存找GOT的内容,找到库函数位置,加载运行。
更有逻辑版:
1. 加载ELF文件到内存
-
读取文件头信息
-
内核读取ELF文件的
Header
,确认文件类型(如ET_EXEC/ET_DYN)。 -
解析
Program Header Table
,获取需要加载的段(如LOAD
段)的信息(虚拟地址、文件偏移、权限等)。
-
-
物理内存分配
-
根据
Program Header
的指示,将程序的代码段(.text
)、数据段(.data
等)从磁盘加载到物理内存。
-
2. 构建进程虚拟地址空间
-
初始化内存描述符(mm_struct)
-
为进程创建
mm_struct
结构,管理虚拟内存空间。
-
-
创建虚拟内存区域(VMA)
-
根据每个
LOAD
段的虚拟地址范围(vaddr
)、大小和权限(读/写/执行),在mm_struct
中创建对应的vm_area_struct
结构。 -
示例:代码段VMA(
vm_start=0x400000, vm_end=0x401000, vm_prot=RX
)。
-
-
建立页表映射
-
将VMA的虚拟地址映射到已加载的物理内存页,设置页表项(PTE)的权限(如
PTE_PRESENT | PTE_USER
)。
-
3. 动态链接库处理
-
加载依赖库
-
解析程序的
.dynamic
段,获取依赖的动态库列表(如libc.so
)。 -
通过
dlopen
机制将动态库加载到共享库区域(如0x7ffff7a00000
),并为每个库创建libso
结构体管理其内存和符号表。
-
-
重定位全局偏移表(GOT)
-
修改程序
.got.plt
段中的占位符条目,将动态库名:偏移量
替换为动态库的实际虚拟地址(如0x7ffff7b00000 + 0x1234
)。 -
延迟绑定(Lazy Binding):首次调用库函数时,通过
PLT
触发动态链接器(ld.so
)解析实际地址。
-
4. 启动程序执行
-
设置入口点(Entry Point)
-
将进程的
PC
寄存器设置为ELF头中的e_entry
(如0x400520
),即程序的主函数入口(如_start
)。
-
-
MMU地址翻译
-
CPU访问虚拟地址时,MMU通过页表将其转换为物理地址:
-
例如
PC=0x400520
→ 查页表 → 物理地址0x12345000
。
-
-
-
动态库函数调用
-
当执行到
call printf@plt
时:-
跳转到
.plt
段的桩代码,通过.got.plt
查找printf
的地址。 -
若未绑定,触发动态链接器解析
printf
在libc.so
中的实际地址(如0x7ffff7b01234
)并更新GOT。 -
最终跳转到
0x7ffff7b01234
执行函数。
-
-