Linux从0到1——基础IO(下)
- 0. 前言
- 1. 磁盘
- 1.1 物理存储结构
- 1.2 磁盘的逻辑存储结构
- 2. 文件系统
- 2.1 文件系统的管理思想
- 2.2 详解块组
- 2.3 文件名和inode
- 2.4 挂载
- 3. 软硬链接
- 3.1 引入
- 3.2 软链接
- 3.3 硬链接
- 4. 动静态库
- 4.1 静态库
- 4.2 动态库
0. 前言
前面我们所学习的,都是被打开的文件。但是,不是所有的文件都是被打开的,大部分文件是不被打开的(当前不需要被访问),都在磁盘中进行保存。
没有被(进程)打开的文件,也要被管理,对于这部分文件的核心工作,是快速定位它。完成这份工作的是文件系统,这是磁盘级的文件系统(一般谈到文件系统指的就是磁盘级的),它是操作系统中的一个分支。还有内存级别的文件系统,管理打开的文件。
磁盘级文件系统+内存级文件系统=文件系统。
文件的管理工作分为:
- 对打开的文件进行管理;
- 没有打开的文件也要在磁盘中进行管理。
1. 磁盘
1.1 物理存储结构
1. 初识磁盘
- 磁盘主要依靠磁头的高速摆动,和盘片的高速旋转(顺时针或逆时针)来访问磁盘中的任意位置。磁头由马达驱动。
- 磁盘的盘片不同于光盘,磁盘的盘片两面都有数据,且磁盘通常有一落盘片。且光盘是只读的,磁盘是可以被读写的。
- 磁盘内有特定的硬件电路来控制盘片的旋转和马达。
- 磁盘是一个机械设备,其他外设虽然是外设,但是好歹是基于光电信号的。而磁盘既是外设又是机械设备,所以速度很慢。
- 磁头和盘片是不接触的,因为盘片在高速旋转时,一旦和任务物体有接触,就难免会出现划痕,就会损坏数据。所以一个使用机械磁盘的电脑,是不适合搬来搬去的。
- 磁盘必须要做无尘处理,因为高速旋转下,一个灰尘就有可能导致磁头震荡,刮花盘面。一个磁盘如果被拆开,对于普通老板姓来说,基本就报废了。
- 我们现在的笔记本电脑使用的是固态硬盘ssd。
- 服务器中,使用的存储设备是磁盘。主要原因是固态硬盘ssd造价贵,不适合大量使用。
- 磁盘中用S级和N级表示计算机中的0或1。磁盘通过磁头充放电来更改某一个存储单元的01状态。
- 磁盘中,盘片高速旋转会产生大量热量,而过热的环境可以让磁盘消磁。所以机房中必须配备像空调这样的降温设备。
- 对于一个大的互联网公司,一批数据下架时,需要先将磁盘中的数据清空。如果将未清空数据的报废磁盘倒卖给二手贩子,将承担法律责任,因为这其中有数据泄漏的风险。所以磁盘一般都会有一键初始化的功能,将磁盘中的所有存储单元写0或写1。
2. 磁盘的物理存储结构
- 一个盘面可以有很多的同心磁道;
- 一圈磁道可以有很多扇形的扇区;
- 扇区是磁盘的最小存储单元,一般为512字节。所以读取磁盘,一次最少将512字节刷到内存里,即使你只想访问一个bit位。
- 磁盘又被称为块设备。这种支持随机读取,有最小存储单元的设备,称为块设备;
- 我们认为,每一个扇区的存储容量是一样的,即使里面和外面的扇区面积不同。要做到这点,只需要保证各扇区的二进制序列疏密程度不一样即可。
3. 如果我想向一个扇区写入,我们该如何寻址,定位?
- 先选择哪一面——本质是选择磁头(Head);
- 选择该面上的哪一个磁道(Cylinder);
- 选择在该磁道的哪一个扇区(Sector)。
这就是
CHS
定位法。
1.2 磁盘的逻辑存储结构
1. 抽象成一维数组
- 我们可以先将磁盘想象成一个线性空间,对这个线性空间进行区域划分。分为不同的盘面,某一个盘面中不同的磁道,某一圈磁道中不同的扇区。
- 进行了这样的区域划分后,我们惊奇的发现,磁盘被抽象成了一个一维数组。这就是磁盘的逻辑存储结构。
2. 如何通过逻辑结构寻址?
- 假设数组的1到10万为第一面,10万01到20万为第二面,以此类推。1到10000为第一面的第一个磁道,10001到20000为第二个磁道。
- 数组下标为1234的位置,在哪个扇区?
- 1234 / 100000 = 0:在第一面;
- 1234 / 10000 = 0:在第一面的第一个磁道;
- 1234 % 10000 = 1234:在该磁道的1234扇区。
操作系统,可以按照扇区为单位进行存取;也可以基于文件系统,按照文件块为单位进行数据存取。
3. 操作系统基于文件系统读取磁盘
- OS层,依据文件系统,一次访问一个文件块的大小(8个扇区),一般为4KB。
- OS层又抽象出一个新的LBA地址。进行数据存取时,首先要将LBA地址转化为对应的CHS地址,找到对应的扇区。
最终结论:对存储设备的管理,在OS层面,转化为了对数组的增删改查。
2. 文件系统
2.1 文件系统的管理思想
我们平常使用的笔记本电脑,正常情况下,都只有一块固态硬盘。 假设这块固态硬盘的大小为5GB,要想把这5GB的数据一次性管理起来,是非常不方便的。
举个例子,我们国家有这么大的国土面积,如果只设立一个中央政府,一个管理机构,肯定是不够的。所以中央下面还有各种省级政府和市级政府。
文件系统也是一样,它将一块固态硬盘分为了不同的区。 例如我们电脑上的C盘,D盘等各种盘。实际上他们只是一块固态硬盘上的不同分区而已。
这一个分区,又可以细分为一个一个的块组(上图假设C盘100GB,一个块组2GB)。我们要想把这100GB的空间管理好,只需要把这2GB的块组管理好,然后将管理方案迁移给其他块组即可,这是一种分治的思想。
所以要想把这500GB的空间管理好,只需要将这2GB的空间管理好即可。
2.2 详解块组
1. 引入:
- 一个块组中,包含两部分信息:a. 我们的文件信息 b. 很多的文件管理数据
- 文件的信息包括属性和内容,它们都是数据,并且内容和属性是分开存储的。
- 如何理解文件管理数据?举个例子,你去银行存钱,银行肯定不能只存储你的个人信息和钱的信息,银行还要知道自己要怎么管理它们,比如该把你的钱放到哪个银行,这个银行金库中目前有多少钱。这一部分信息就是管理信息。
- 一个块组要想被使用,管理数据一定是最先加载的。
- 就像一个班级中,班长组长这些管理者总是最先确定的。
- 初始化写入块组管理数据的操作,称为格式化。 所以我们常说的格式化一个磁盘,就是将这个磁盘中的管理数据初始化了,变成了一个崭新的磁盘,之前存储在磁盘中的文件数据就全部失效了。
2. 详解inode:
- 使用指令
ll
,可以查看当前目录中的文件信息。添加-i
选项后,会发现最前面多了一列编号,这就是inode
编号。
- 一般情况下,每个文件都要有一个
inode
,并且inode
在整个分区中具有唯一性。在Linux内核中,识别文件只看inode
,不看文件名。
- 所有文件的属性数据大小,一定是相同的。 他们可能有不同的文件大小,不同的权限、ACM时间,但是这些数据的大小都是相同的。
- 这些属性数据是通过
inode
结构体保存的,本文使用的系统中,struct inode
的大小为128字节。 - 每一个块组中,会有一个
inode Table
,它相当于一个struct inode
数组。 - 每一个块组的起始地址会被记录,假设是
start_inode_number
。inode
编号的计算规则是start_inode_number + 偏移量
,这样就可以在一个分区中,唯一定位到某个块组的,inode Table
的某个位置。
3. 详解Data blocks:
inode
记录了文件的属性,那文件的内容放在哪里?就放在Data blocks
中。
strcut inode
中有一个block[N]
数组,会记录该文件使用了哪些数据块,把这些数据块的编号存起来。block
是一个定长数组,这意味着文件的大小肯定有上限。N
一般为15,如果按15个文件块的大小来算的话,只有60KB。60KB并不能满足所有情况,所以:- 0-12号下标:为直接映射,直接保存数据块的下标;
- 13号下标:间接映射,13号下标存储的数据块中,继续存储其他数据块的下标;
- 14号下标:三级映射;
4. 两个位图:
- 上面我们学习的都是文件信息的存储,还有管理信息的存储。
inode Bitmap
:inode
位图,每个bit
表示对应的inode
是否空闲。0表示空闲,1表示被占用;Block Bitmap
:块位图,记录着Data blocks
中哪个数据块被占用,哪个没有。0表示没有被占用,1表示被占用。
如何理解新建一个文件?
- 先搜索
inode
位图,找到数值为0的bit
位,改为1;- 进而找到对应的
inode Table
中的空闲位置,然后将文件属性写入;- 搜索块位图,找到没有被占用的位置,需要几个块就将几个
bit
位改为1;- 进而找到对应的
Data blocks
中的空闲块,写入文件内容。如何理解删除一个文件?
- 首先,我们上层一定知道该文件的
inode
编号,接着只需要由该inode
编号找到其对应的在inode
位图中的位置,将该bit
位改为0;- 然后再根据
struct inode
中保存的文件块信息,找到该文件使用的文件块在块位图中的位置,将对应bit
位改为0即可;删除一个文件时,并不需要清空文件内容和属性。所以我们会发现,删除一个文件比新建一个文件要快。
5. GDT和超级块:
- 这两个块中存储的依然是管理信息。
- GDT(Group Descriptor Table):块组描述符,描述的是一个块组的属性信息,如该块组
inode Table
的使用率,Data blocks
的使用率等。 - 超级块(Super Block):超级块并不是每个块组都有,也并不是只有一个,一般一个分区中,会有3到4个块组有超级块。超级块描述的是整个分区的使用情况,如一共有多少块组,有多少块组被使用了等等。以及最近一次写入时间,文件系统的名称等各种文件系统本身的信息。
为什么超级块要这样设计?不能像其他数据块一样,在每一个块组中都保存一份吗?或者像
Boot Block
一样,在一个分区中只保存一份?
- 因为超级块保存的是整个分区的管理信息,如果它挂掉了,整个分区也就挂掉了。所以文件系统对超级块做了备份,保存了几个副本在其他块组中,方便在常用的超级块挂掉时,做数据恢复;
- 但是如果在每个块组中都备份一份,又太浪费空间了,所以只备份了3到4份。
6. Boot Block:
- 这个数据块和操作系统的启动有关,如果这个块挂掉了,操作系统就起不来了。
2.3 文件名和inode
1. 我们日常操作一个文件,用的都是文件名,那么文件名和inode是什么关系呢?
- 文件名和
inode
之间互为键值,文件名 : inode
,这样的映射关系存储在目录中。
2. 目录的本质:
- Linux中,一切皆文件,目录也是文件,有自己的
inode
。
- 目录内部保存的是文件名和
inode
的映射关系。 - 回顾之前的知识,我们要想在一个目录下,新增,删除,修改一个文件,需要什么权限?需要
w
权限,现在我们就不难理解了,因为上述操作的本质是修改目录文件的内容,新增或删除映射关系。
3. 文件名不属于文件属性:
- Linux中,文件名不属于文件属性,也就是说
struct inode
中,不会存文件名。
4. 如何查找一个文件?
- 我们经常使用的查找方式有:a.绝对路径 b.相对路径;
- 但是系统中只会按照绝对路径的方式查找。
- 举个例子,你在
test
目录下查找test.c
,直接ls test.c
即可,但是test
也是文件啊,它是怎么找到的? - 还记得环境变量
PWD
吗?你进入一个目录,会自动为你保存当前的绝对路径。操作系统会根据这个绝对路径,从根目录开始,一步一步向下查找,找到目标文件。 - 根目录也有
inode
。
- 举个例子,你在
- 每一次都要从根目录开始查找吗?
- 操作系统会将频繁被访问的路径加载进
cache
缓存,提高效率。
- 操作系统会将频繁被访问的路径加载进
2.4 挂载
查找一个文件,第一步根本就不是确定它在哪个目录下,而是确定它在哪个分区!
一个磁盘,被分区格式化之后,Linux中OS要使用这个分区,要使用命令mount
将这个分区进行挂载。例如:mount /dev/sdb1 /mnt
。
可以使用df -h
查看分区的挂载情况:
我使用的是云服务器,配置比较低,只有一个分区vda1
,被挂载到了根目录下。
挂载是运维人员操心的时,我们在这里不做深究。
3. 软硬链接
3.1 引入
1. 观察现象:
- 可以发现,硬链接的
inode
和其链接的文件的inode
一样; - 软连接的
inode
和其链接的文件inode
不一样。
2. 得出结论:
- 软连接是一个独立的文件;
- 硬链接不是一个独立的文件,因为它没有独立的
inode
编号。
3. 思考问题:
- 什么是软链接?什么是硬链接?
- 为什么要有软硬链接?有哪些应用场景?
- 它们的区别是什么?
3.2 软链接
1. 什么是软链接?
- 软链接就像Windows下的桌面快捷方式一样,方便用户快速的定位一个文件。
2. 软链接的应用场景:
- 一个项目中,可执行程序一般在
bin
目录下。如果每次启动项目都要去bin
目录下找可执行程序,未免也太麻烦了。 - 所以最好在外部创建一个软链接指向该可执行程序。
3. 软链接的实现原理:
- 软链接就是一个独立文件,里面的内容是目标文件的路径。
3.3 硬链接
1. 什么是硬链接?
- 硬链接不是一个独立的文件,只是在指定目录内部的一组映射关系:
filenanme : inode
。
inode
内部有引用计数,表明有几个文件名映射了该inode
。表现在用户层就是文件硬链接数。
2. 一个文件什么时候,算是被真正删除?
- 没有文件名和
inode
映射关系时,该文件才算真正删除。
- 上图中,
hello.hard.link
是hello
的一个硬链接。上图一系列操作的本质,相当于对hello
进行了重命名。 - 只有把
hello.hard.link
文件也删除了,inode
编号为922815
的文件才算真正被删除。 - 文件名在同一目录中具有唯一性。
3. 目录和硬链接:
- 用户无法对目录建立硬链接。
- 观察下列目录结构,思考为什么文件的硬链接数为1,而目录的硬链接数不是1?
- 目录中都有一个
.
和..
,它们其实就是两个硬链接,一个映射当前目录的inode
,一个映射上级目录的inode
。这就很好解释了,为什么proc
的硬链接数为2,而bin
的硬链接数为3。
4. 硬链接的应用场景:
- Linux中,用硬链接来构建整个目录结构,方便我们进行路径回退。
4. 动静态库
- 静态库(.a):程序在编译链接的时候把库的代码链接到可执行文件中。程序运行的时候将不再需要静态库。
- 动态库(.so):程序在运行的时候才去链接动态库的代码,多个程序共享使用库的代码。
- 一个与动态库链接的可执行文件仅仅包含它用到的函数入口地址的一个表,而不是外部函数所在目标文件的整个机器码。
- 在可执行文件开始运行以前,外部函数的机器码由操作系统从磁盘上的该动态库中复制到内存中,这个过程称为动态链接(dynamic linking)。
- 动态库可以在多个程序间共享,所以动态链接使得可执行文件更小,节省了磁盘空间。操作系统采用虚拟内存机制允许物理内存中的一份动态库被要用到该库的所有进程共用,节省了内存和磁盘空间。
4.1 静态库
1. 测试代码
Add.h
:
#pragma once
#include <stdio.h>
extern int Add(int, int);
Sub.h
:
#pragma once
#include <stdio.h>
extern int Sub(int, int);
Add.c
:
#include "Add.h"
int Add(int x, int y)
{
return x + y;
}
Sub.c
:
#include "Sub.h"
int Sub(int x, int y)
{
return x - y;
}
main.c
:
#include "Add.h"
#include "Sub.h"
int main()
{
int x = 10;
int y = 20;
printf("%d + %d = %d\n", x, y, Add(x, y));
printf("%d - %d = %d\n", x, y, Sub(x, y));
return 0;
}
Makefile
:- 形成可执行程序,需要先编译形成各种
.o
文件,然后再将这些.o
文件统一链接起来。
- 形成可执行程序,需要先编译形成各种
Test:Add.o Sub.o TestMain.o
gcc -o $@ $^ # 将所有的.o文件都链接起来,形成可执行程序
%.o:%.c # %是通配符
gcc -c $< # $<相当于将.c文件一个一个的拿来
.PHONY:clean
clean:
rm -f *.o Test
- 运行一个形成的可执行程序。
2. 如何形成静态库?库的好处
- 我们可以将形成的
.o
文件打包,形成一个库,用户要想使用这个库,只需要将自己写的.c
源文件也编译形成.o
文件,和库链接即可。 - 这样做有几个好处:
- a. 隐藏了源代码,封装了实现细节,提高了代码的安全性;
- b. 不用再编译源代码了,直接链接即可,节省了时间。
3. 制作静态库的具体做法
- 可以使用指令
ar -rc lib[静态库名字].a [需要打包的.o文件名]
来形成静态库。ar
是gnu
归档工具,rc
表示替换和创建(replace and create)。 - 测试用的
.c
文件和.h
文件如下:
- 编写
Makefile
形成静态库:
static-lib=libmymath.a # 静态库的名字
$(static-lib):Add.o Sub.o
ar -rc $@ $^ # 制作静态库
%.o:%.c # %是通配符
gcc -c $<
.PHONY:clean
clean:
rm -f *.o *.a
- 执行
Makefile
:
- 上面虽然形成了我们的静态库,但是各种
.o/.h
文件混在一起,看起来非常挫。所以我们需要改写一下Makefile
,将这些文件整理一下(发布):
static-lib=libmymath.a # 静态库的名字
$(static-lib):Add.o Sub.o
ar -rc $@ $^ # 制作静态库
%.o:%.c # %是通配符
gcc -c $<
.PHONY:output
output:
mkdir -p mymath_lib/include # 头文件目录
mkdir -p mymath_lib/lib # 库目录
cp -f *.h mymath_lib/include # 拷贝头文件
cp -f *.a mymath_lib/lib # 拷贝库文件
.PHONY:clean
clean:
rm -rf *.o *.a mymath_lib
- 打包库目录,形成压缩包,可以将压缩包挂在网上,供别人下载:
静态库的本质:将库中的源代码直接翻译成.o目标二进制文件,然后打包。
4. 使用静态库
- 从网上下载第三方库(这里就不下载了,直接用我们自己压缩好的做演示),解压缩:
- 用户编译自己的源文件,并指明链接的库:
- 上图中,
gcc
的-I
选项表示新增头文件搜索路径,-l
选项后面跟要链接的库的名字(注意库的名字为去掉前缀lib
,去掉后缀.a
或.so
),-L
选项表示新增库文件搜索路径。
5. 详解gcc链接静态库
gcc
默认是动态链接的,但是如果个别库你只提供.a
,gcc
也会局部性的使用静态库进行静态链接,其他库正常动态链接。- 如果
gcc
后面跟了-static
选项,就只能用静态库链接,没有静态库直接报错。 gcc
默认不认识第三方库,链接第三方库时需要指明头文件和链接库的位置。gcc
会默认在/usr/include
目录下搜索头文件,在/usr/lib64
目录下搜索库文件,这是系统路径。- 我们可以将第三方库的头文件和库文件添加进系统路径下,就是将对应的
.h
文件cp
进/usr/include
目录下,将.a
文件cp
进/usr/lib64
路径下。这样在使用gcc
编译链接时,就不用指明头文件和库文件的搜索路径了。- 上面的操作叫安装第三方库,配置开发环境。但是这种操作会污染系统环境,在这里就不做演示了。
4.2 动态库
1. 制作动态库
- 第一步:
gcc -fPIC -c *.c
,编译所以的.c
文件,注意带选项fPIC
,产生与位置无关码; - 第二步:
gcc -shared -o lib[库名字].so *.o
,生成动态库,-shared
表示生成共享库格式。 - 写成
Makefile
:
dy-lib=libmymath.so # 静态库的名字
$(dy-lib):Add.o Sub.o
gcc -shared -o $@ $^ # 制作动态库,shared生成共享库格式
%.o:%.c # %是通配符
gcc -fPIC -c $< # -fPIT,产生与位置无关码
.PHONY:output
output:
mkdir -p mymath_lib/include # 头文件目录
mkdir -p mymath_lib/lib # 库目录
cp -f *.h mymath_lib/include # 拷贝头文件
cp -f *.so mymath_lib/lib # 拷贝库文件
.PHONY:clean
clean:
rm -rf *.o *.so mymath_lib
2. 发现问题
- 到编译链接这一步为止,基本上和静态库的生成和使用没有区别。但是可执行程序在运行时出现了问题:找不到库文件。
- 这是因为,静态库在链接时,直接就把库中函数的具体实现拷贝到可执行程序中了,所以
a.out
在运行时,不需要再去外部找库文件了。 - 而动态库在链接时,并没有把库中函数的具体实现拷贝到可执行程序中,
a.out
在运行时,还需要去外部找库文件,但是它找不到。
- 这是因为,静态库在链接时,直接就把库中函数的具体实现拷贝到可执行程序中了,所以
ldd
指令可以查找当前可执行程序依赖的动态库有哪些。
3. 使用动态库(解决2中的问题):
- 方法一:直接安装到系统中(这是使用动态库最推荐的方法,没有之一)。
-
方法二:通过软链接,查找动态库。
- 动态链接会在当前路径下搜索库文件,所以可以在可执行程序所在的当前路径下建立软链接。
- 还可以将软链接放在系统路径下,也是可行的。
- 动态链接会在当前路径下搜索库文件,所以可以在可执行程序所在的当前路径下建立软链接。
-
方法三:新增路径到环境变量
LD_LIBRARY_PATH
,让系统找到动态库。- 下面这种操作只是在内存级,再次登入时环境变量会复原。
- 如果想要永久修改环境变量,需要修改配置文件(这一块我们在环境变量处讲过,这里不再赘述了)。
- 方法四:直接更改系统关于动态库的配置文件(先切换成超级用户)。
- 在
/etc/ld.so.conf.d/
目录下新增一个普通文件,里面的内容是需要配置的动态库所在的目录路径。
- 在
同一组库,如果提供了动静态两种库,gcc默认使用动态库。