一、共识原理:
文件=文件内容+文件属性
磁盘上存储文件=存文件的内容(数据块)+存文件的属性(inode)
Linux的文件在磁盘中存储是将属性和内容分开存储的。
二、硬件简述:
1. 认识硬件
磁盘:唯一的一个机械设备,也是一个外设。磁头一面一个,磁头和盘面不接触。是永久性存储介质,高温会退磁。
内存:掉电易失性存储介质。
磁带:类似于磁盘。
2、磁盘的存储构成
磁盘被访问的最基本单元是扇区(512字节/4KB),我们可以把磁盘看作有无数个扇区构成的存储介质。
要把数据存到磁盘,第一个解决的问题是定位一个扇区,哪一面定位用哪个磁头,哪一个磁道,哪一个扇区。
磁头左右摆动,其实质是定位磁道和柱面的过程(运动越少,效率越高;运动越多,效率越低)。
在软件设计上,设计者一定要有意识的将相关数据放在一起。
磁带延展开,逻辑上是线性的,所以磁盘在逻辑上也是线性的。基于扇区的数组,任意一个扇区都有下标。
3、回归到硬件:不仅仅有CPU有寄存器,其他设备外设也有,磁盘也有。
控制寄存器(rw):控制IO方向。
数据寄存器:存储数据。
地址寄存器:逻辑地址(LBA)。
状态寄存器:表示结果。
三、对硬件进行抽象理解(文件系统ext2):
1、分治: 分区->块组->文件系统。
Data blocks:存文件内容的区域,以块的形式呈现,常见的文件系统块大小是4KB(一般而言,每个块只有自己的数据)。
inode Table:很多inode。
inode:inode结构体,包含单个文件的所有属性(inode number,文件类型,权限,引用计数,拥有者,所属组,ACM时间,int blocks[NUM]等),大小是128字节(一般而言,一个文件,一个inode)(文件文件名不在inode保存)。
注:
①在linux中文件的属性中,不包含文件的名称,只知道文件的编号。查找一个文件仅需找到它的inode编号,从而在它的inode表中找到inode,从而找到其blocks数组,根据数组中记载的块号按顺序读出文件内容。
②block数组规定:分为直接索引和简介间接索引。
直接索引:存储文件内容。
间接索引:不存储文件内容,而是继续存储我们的文件版号,从而创建更大的文件。
Block bitmap比特位的位置和块号映射起来,比特位的内容表示该块是否被使用。
inode bitmap:比特位的位置和inode的编号映射起来,比特位的内容表示inode是否有效。
问:上一个文件的时候用不用把块(文件内容)清空呢?
答:不用。根据文件inode编号,找到inode bitmap(发现位图有效),转而去找inode table,读取其属性获得对应的inode和数据块地址关系,读取所有块号,在bitmap和block map里所有的块号全部清零,其次,根据inode编号找到其bitmap由1置为0。
Group Descriptor Table(不会在每个组都存在):描述整个分组基本的使用情况:一共有多少个组,每个组的大小,每个组的inode数量,每个组的block数量,每个组的起始inode,文件系统的类型与名称等。
Super Block:文件系统的信息:里面包含的是整个分区的基本使用情况。
格式化:每一个分区在被使用前,都必须提前先将部分文件系统的属性信息提前设置进对应的分区中,方便我们后续使用这个分区或者分组。
2、文件理解:
①新建一个文件,系统要做什么?
通过新建路径确定分区,在该分区内分配inode——查询Group Descriptor Table是否还有未使用的inode,然后扫描inode bitmap位图结构并读取,选择最近的未使用的编号,将该比特位由0置1。未来可通过inode编号确定分区。在inode Table内找到inode,将文件属性填入。在Block bitmap确认写入数据量的大小,遍历未使用的块,将对应的块号填充到inode属性中特定的下标里,然后直接跳转到对应的块中,把内容填充进去。
②删除一个文件,系统要做什么?
根据文件所处目录,确定文件所处分区,根据inode范围确定分组,用自己inode编号减去起始inode编号,进而可以在inode bitmap进行索引,根据inode编号在位图中对应的比特位由1置0,根据inode将Block Bitmap对应比特位由1置0。(删除=允许被覆盖)。
③查找一个文件,系统要做什么?同理。
④修改一个文件,系统要做什么?同理。
3、如何理解目录
①Linux系统中,一个文件,一个inode,每一个inode都有自己的inode编号(inode的设置,是以分区为单位的,不能跨分区)
inode表示文件的所有属性,但是文件名并不属于inode内的属性!
②问:可是我怎么知道一个文件的inode编号?使用者从来没关心过inode,用的是文件名。
答:通过目录查找。
③目录也是文件,也有自己的inode,也有自己的内容和属性。目录数据块中存储文件的文件名和对应文件inode的映射关系。
④同一个目录下不能有同名文件,因为key值不能相同。文件名冲突系统就无法找到指定文件了。
⑤目录下,没有w,我们无法创建文件。即便创建了文件,文件名与inode的映射关系也无法写到目录文件的数据块里。
⑥目录下,没有r,我们无法查看文件。无法读取目录所对应数据块的文件名与inode的映射关系无法得到。
⑦目录下,没有x,我们就无法进入这个目录。cd目录名本质上是找到目录的inode,但没有x,无法进入目录,无法更新当前目录环境变量。
⑧问:可是目录是文件,也有inode的编号。怎么找?
答:查任何一个目录使,都要从当前目录开始向上递归找到根目录(此文件文件名确定/),找到该数据块,里面的所有文件子目录数据都可以找到。(绝对路径)
⑨Linux系统中,会把用户最常访问的若干目录路径信息通过dentry缓存。
四、软硬链接:
软链接是一个独立的文件,具有独立的inode
硬链接不是一个独立的文件,因为它没有独立的inode。
1、如何理解硬链接?
所谓建立硬链接,本质其实就是在特定目录的数据块中新增文件名和指向的文件的inode编号的映射关系(类似于取别名)。
任意一个文件,无论是目录,还是普通文件,都有inode。
每一个inode内部,都有一个叫做引用计数的计数器(有多少个文件名指向该文件)(默认引用计数为1,自己指向自己)。
目录里保存的是文件名:inode编号的映射关系(默认引用计数为2,有自己目录的文件名和inode映射关系)(目录内部的.文件是该目录的一个硬链接)。
2、如何理解软链接?
软链接是一个独立的文件,有独立的inode,也有独立的数据块,他的数据块里面保存的是指向文件的路径(类似于快捷方式)。
3、为什么要用软链接?
任意路径软件在系统中能找到的/usr/bin目录下建立软连接,就能不带路径执行。
4、为什么要用硬链接?
通常用来进行路径定位,采用硬链接,可以进行目录间切换。硬链接能够维持Linux整体的目录结构。
注:Linux不允许用户对目录建立硬链接,因为会引发环路问题。(而系统可以,比如.和..)。
五、内存管理简述
1、内存的本质是对数据的临时存取,所以我们在系统层面上把内存看作一个大型的缓冲区。内存中的大部分数据未来都会加载到磁盘里或者释放。物理内存以页框为单位和磁盘以页帧为单位进行数据交互(4KB)。
2、问:为什么要以4KB的方式进行拷贝?
答:
①减少IO的次数/减少访问外设的次数---硬件。
②基于局部性原理的预加载机制---软件。
3、操作系统如何管理内存?
操作系统提供了虚拟地址空间的概念,用户在应用层只能看见虚拟地址,但操作系统也能看到内存的物理地址。操作系统管理内存时,先描述,再组织——用struct page结构体描述page必要的属性信息,存储到struct page mem_array数组中(其中数组的下标称为页号),对内存的管理变成了对数组的管理!
用户要访问一个内存时,只需要先直接找到这个4KB对应的page,就能在系统中找到对应的物理页框。所有申请内存的动作都是在访问内存page数组。
申请内存的本质是检测数组中结构体里flag标志位是否被使用,若未使用则将page的比特位置1即可。
4、伙伴系统算法,slab分派器(了解即可)。
5、文件页缓冲:Linux中我们的每个进程打开的每一个文件都要有自己的inode属性和自己的文件页缓冲区。数据通过基数树/基树(字典树)写入到文件页缓冲区。文件的内容是有偏移量的!可以通过文件内容偏移量按照比特位构成字典序,在字典树中找到相应的文件偏移量与内存中配置对应的映射关系。从而让文件有序地进行刷新。
6、IO子系统:操作系统层面上会提供一个公共的队列IO request queue,所有的上层进程最终将数据通过文件写到内存中,文件中的配置构建成struct request结构体封装到队列里。操作系统所谓的刷新就是将队列里的数据依次提交给磁盘。同时也可以通过IO排序,IO合并优化该过程。
六、动静态库
1. 回顾:站在库的制作者角度。
libXXX.a---静态链接
libYYY.so---动态链接
把我们提供的方法给别人用,可以把源文件直接给他,也可以把我们的源文代码想办法打包成库(库+.h)
静态库原理:将.c文件编译成.o文件,将.o文件打包成.a文件,将main.c文件编译为.o文件,两者一结合为最后的可执行程序。
//mymath.h
#pragma once
#include <stdio.h>
extern int myerrno;
int add(int x, int y);
int sub(int x, int y);
int mul(int x, int y);
int div(int x, int y);
//mymath.c
#include "mymath.h"
int myerrno = 0;
int add(int x, int y)
{
return x + y;
}
int sub(int x, int y)
{
return x - y;
}
int mul(int x, int y)
{
return x* y;
}
int div(int x, int y)
{
if(y == 0)
{
myerrno = 1;
return -1;
}
return x / y;
}
//Makefile
lib=libmymath.a
$(lib):mymath.o
ar -rc $@ $^ //$@ $^表示这些文件的起始和尾
mymath.o:mymath.c
gcc -c $^ //$^:形成同名的.o文件
.PHONY:clean
clean:
rm -rf *.o *.a lib
.PHONY:output
output:
mkdir -p lib/include
mkdir -p lib/mymathlib
cp *.h lib/include
cp *.a lib/mymathlib
[root@hecs-210801 lesson27]# ll
total 12
-rw-r--r-- 1 root root 228 Nov 26 10:19 Makefile
-rw-r--r-- 1 root root 251 Nov 26 10:10 mymath.c
-rw-r--r-- 1 root root 148 Nov 26 10:09 mymath.h
[root@hecs-210801 lesson27]# make
gcc -c mymath.c
ar -rc libmymath.a mymath.o
[root@hecs-210801 lesson27]# make output
mkdir -p lib/include
mkdir -p lib/mymathlib
cp *.h lib/include
cp *.a lib/mymathlib
[root@hecs-210801 lesson27]# ll
total 24
drwxr-xr-x 4 root root 4096 Nov 26 10:27 lib
-rw-r--r-- 1 root root 1880 Nov 26 10:27 libmymath.a
-rw-r--r-- 1 root root 228 Nov 26 10:19 Makefile
-rw-r--r-- 1 root root 251 Nov 26 10:10 mymath.c
-rw-r--r-- 1 root root 148 Nov 26 10:09 mymath.h
-rw-r--r-- 1 root root 1704 Nov 26 10:27 mymath.o
[root@hecs-210801 lesson27]# tree lib
lib
├── include
│?? └── mymath.h
└── mymathlib
└── libmymath.a
2 directories, 2 files
删除lib的指令:
[root@hecs-210801 lesson27]# rm -rf lib
未来我们要把库给别人时仅需提供lib文件夹即可。
在其他目录下使用该库时所包头文件为:
#include “lib/include/mymath.h”
或者让系统在指定目录下找头文件,库文件:
[root@hecs-210801 lesson27]# gcc main.c -I ./lib/include/ -L ./lib/mymathlib/ -lmymath
2、站在库的使用者角度:
①第三方库往后使用的时候,必定要用gcc -l。
②深刻理解errno的本质。
③gcc链接程序时,默认是动态链接的。如果系统中只提供静态链接,gcc则只能对该库进行静态链接。
④如果系统中需要链接多个库,则gcc可以链接多个库。
3、解决下载不到静态库的方法:
①拷贝到系统默认的库路径/lib64/usr/lib64/(库的安装)。
②在系统默认的库路径/lib64/usr/lib64/建立软链接。
4、解决下载不到动态库的方法:
①拷贝到系统默认的库路径/lib64/usr/lib64/(库的安装)。
②在系统默认的库路径/lib64/usr/lib64/建立软链接。
③将自己的库所在的路径,添加到系统的环境变量LD_LIBRARY_PATH中。
④/etc/ld.so.conf.d建立自己的动态库路径的配置文件,然后重新ldconfig即可。
(实际情况,我们用的库都是别人的,成熟的库都采用直接安装到系统的方式。)
5、使用外部库:ncurses--基于终端的图形界面的库。
6、动态库是怎么被加载的。
①动态库在进程运行的时候,是要被加载的(静态库没有),这就是动态库具有可执行权限的原因。
②常见的动态库,被所有相关的可执行程序动态链接,都要使用动态库/共享库。所以,动态库在系统中加载之后,会被所有进程共享。
③动态库被加载的方式为:建立映射,从此往后我们执行的任何代码都是在我们的进程地址空间中进行执行。
④事实上,系统在运行中,一定会存在多个动态库,先描述再组织由OS管理起来。系统中,所有库的加载情况,OS都非常清楚。
7、程序加载前后地址:
①编译好没有加载前的程序是有地址的,其可执行数据区可以被分成很多段。形成可执行程序的编址已经形成了平坦模式(严格遵照地址空间的方式编码),因为编译器也要考虑操作系统。
注:可执行程序的地址为逻辑地址(磁盘中程序形成时所对应的地址,存偏移量)。
②程序加载后的地址进程:当可执行程序加载到内存的时候,每条指令天然的具有物理地址。
③执行程序形成的时候包含入口地址entry(逻辑地址)。
④CPU读取可执行程序时直接将entry加载到内部寄存器(EIP/PC)中,找到正文代码段从而找到页表,此时页表未建立对应的映射,触发缺页中断,于是程序被加载进来,具有了物理地址,从而页表也能填上对应的虚拟地址和物理地址,从而在物理内存中找到对应的指令代码。CPU内读取到的指令内部可能有数据,可能也有地址(虚拟地址)。(CPU读到指令中的地址全是虚拟地址)
8、动静态库的地址:
①问题:动态库在共享区过大,那具体映射到哪里呢?
动态库被加载到固定地址空间中的位置是不可能的。库只需在虚拟内存中任意位置加载,并让让自己内部函数不要采用绝对编址,只表示每个函数在库中的偏移量即可。
fPIC:产生位置无关码(直接用偏移量进行编址)。
②静态库为什么不谈加载,不谈与位置无关?
因为静态库已经拷贝在可执行程序里了,直接按绝对编址编码。
9、虚拟地址最终是怎么转换到物理地址的?
相关内容将会在后续章节讲解。