一、磁盘
基于上篇博客所写到的文件各种操作都是基于被打开文件所进行操作的,那么如果一个文件没有被打开它存在哪里呢?这个答案毫无疑问肯定是存在于磁盘上的。那么,对于一个没有打开的文件(也就是磁盘文件)我们应该如何理解呢?
1. 物理结构
这里我们所要讲的磁盘是机械硬盘、即传统的普通硬盘,主要由:盘片,磁头,盘片转轴及控制电机,磁头控制器,数据转换器,接口,缓存等几个部分组成。
磁头可沿盘片的半径方向运动,加上盘片每分钟几千转的高速旋转,磁头就可以定位在盘片的指定位置上进行数据的读写操作。信息通过离磁性表面很近的磁头,由电磁流来改变极性方式被电磁流写到磁盘上,信息可以通过相反的方式读取。硬盘作为精密设备,尘埃是其大敌,所以进入硬盘的空气必须过滤。
💕这里我们需要注意的是: 一片盘片有两面,两面都能够读写数据,每一面都有一个磁头,磁头是悬浮在盘片上。磁盘的盘面是充满磁的,其上面存在着许多非常细小的 N、S级,磁盘是通过 N、S级来表示 0 和 1。通过充磁和消磁技术,来调转 N、S 级,从而达到 0 和 1 的写入,那么就可以表示数据了。
2. 存储结构
3. 逻辑抽象结构
类比磁带,我们可以将磁盘盘片想象成线性结构,将一摞磁盘沿磁道“拉直”,就抽象成了线性结构。那么整个磁盘可以看做一个sector arr[N]数组,对磁盘数据的管理就变成了对数组的管理。只要知道了扇区的下标,就可以定位扇区。这个下标在操作系统内部称为LBA地址。根据LBA地址可以转化为CHS地址,从而找到对应扇区。
计算机常用的访问方式是:起始地址+偏移量的方式
,所以我们只需要知道数据块的起始地址(第一个扇区的起始地址)+ 4KB(块的类型),所以块的地址,本质就是数组的下标,所以我们可以直接使用下标的方式来定位任意一个块。
磁盘的最小单位是512字节
(扇区的大小)。但是大部分操作系统的文件系统读取数据时会以4KB
为基本单位读取至内存。这个特点也印证了顺序表缓存命中率高的特点。因此,当以4KB为基本单位进行IO时,有时4KB数据并不能完全被利用,但这并不代表着浪费。根据局部性原理,当计算机访问某些数据时,它附近的数据也有非常大的概率被访问到,加载4KB有助于提高IO效率,同时增大缓存命中率。本质上就是一种数据预加载,以空间换时间的做法。因此,操作系统中内存被划分成了一块块4KB大小的空间,每个空间被称为页框
。
操作系统知道任意一个CHS
的地址,就能访问到任意一个扇区,但是OS内部却不是直接访问CHS地址,原因如下:
- OS是软件,CHS是硬件,硬件定位一个地址,但是如果OS直接使用了这个地址,万一硬件变了,OS也要跟着变化,因此操作系统OS要和硬件做好解耦工作。
- 即便是扇区,也仅仅有512个字节,单位IO的基本数据量也是很小的,OS实际进行IO的基本单位是4KB。磁盘是一个块设备,OS需要有一套新的地址,来进行块级别的访问。
二、文件系统
1. 文件系统的结构
磁盘采用的是分治的思想。这里我们做一个假设:将一个500G的磁盘划分成4个100G进行管理,每个125G又可以划分成多个5G的块进行管理。
Super Block:
存放文件系统本身的结构信息。记录的信息主要有:bolck 和 inode的总量,未使用的block和inode的数量,一个block和inode的大小,最近一次挂载的时间,最近一次写入数据的时间,最近一次检验磁盘的时间等其他文件系统的相关信息。在一个分区中超级块的数量不止一个,作用是备份。GDT(Group Descriptor Table):
块组描述表,描述所有块组属性信息;Block Bitmap:
每一个bit 表示datablock用0表示某位没有被使用,用1表示某位数据块已经被使用。inode Bitmap:
每一个bit表示一个inode是否空闲可用,用0表示某位没有被使用,用1表示某位inode已经被使用。inode Table:
保存了分组内部所有的可用(已使用+未使用)的inode。如果inode表中有100个inode,每个inode的大小是128字节或256字节(根据文件系统的不同Inode大小不同),inode表总大小就是100 * 128或100 * 256字节。单个inode: 存放文件中几乎所有的属性,如文件大小,所有者,最近修改时间等,唯独文件名不在inode表中存储。一个文件对应一个inode,inode是固定大小。每个分组中的inode为了区分彼此,它们都有自己的ID。Data blocks:
保存的是分组内部所有文件的数据块、单个Data block:存放文件内容,大小随文件大小变化而变化。
2. 查看文件
想要查看一个文件,我们可以通过inode编号找到该文件的数据块数组blocks[ ],进而找到该文件的数据块,组合成整个文件。
当blocks[ ]指向数据块时,该数据块中保存的是其他的数据块的编号,这些块也可以存储别的数据块编号,因此,blocks[ ]表示的可以是很多个数据块,所以一个文件的数据块可以很多,文件的大小可以很大。
3. 删除文件
删除文件只需要找到 inode 在 inode Bitmap 中的比特位和 Block Bitmap 的比特位,将这两个比特位改成 0,文件就算删除了。恢复文件则与该过程相反,知道删除文件的 inode,再将 inode Bitmap 和 block Bitmap 中对应的比特位改成 1 即可恢复文件。删除文件的 inode 可以通过日志来查到。误删文件后,千万不要新建文件。以防误删文件的 inode 编号和数据块被占用,无法恢复误删文件。
但是我们平常在Linux下查看文件的时候用的是文件名,并没有使用inode来查看文件,其实文件名和inode存在一一映射的关系,目录的数据块放的就是当前目录文件名和inode的映射关系,所以在同一目录下,不可以存在同名的文件,因为文件名就是一个key
值,通过该key值找到对应的inode编号,进而找到该文件!!!所以在同一个目录里创建文件,必须要有该目录的写入权限。因为要将文件名和inode的映射关系写入到目录的数据块中。
三、软硬链接
1. 软链接
ln -s 现有文件 目标文件 #将目标文件与现有文件进行软链接
unlink 文件名 #删除软链接文件
有自己独立的inode的称为软链接,即软链接是独立的文件,独立的文件有独立的inode和对应的文件内容。
因此,软链接生成的文件具有独立的inode,可以被当作独立文件来看待。
💕 软链接的优势: 软链接就相当于Windows系统下的快捷方式,它能够帮助我们快速找到指定路径下的文件。
2. 硬链接
ln 现有文件 目标文件 #将目标文件与现有文件进行硬链接
硬链接的生成的文件没有独立的inode,也就是说,建立硬链接没有创建新的文件,其inode是原来文件的inode。所以建立硬链接并没有创建新的文件。因为没有给硬链接生成的文件分配独立的inode。既然没有创建新文件,因此硬链接生成的文件用的还是源文件的inode和内容。硬链接的本质是让inode中的引用计数count++,所以引用计数也是被称作为硬链接数。这里需要注意的是:当一个文件的硬链接数(引用计数)变成0时,该文件才能算是真正被删除。同一个inode的文件表示的就是同一个文件。
软硬链接的区别:
硬链接
就是相当于给原有文件取别名,硬链接生成的文件和原有文件对应的 inode 是相同的。原有文件删除,并不会影响硬链接生成的文件,只是 inode 中的引用计数减去了 1。但软链接却不同,软链接并不是使用 inode 编号来表示的,而是通过文件名来标识的。软链接
生成的文件有独立的 inode,也就有自己的数据块,其数据块中保存了原有文件的路径。如果删除了原有文件,那么软链接就失效了。但是如果在新建相同名字的文件,软链接会再次生效。但文件的内容肯定是不一样的,这相当于偷梁换柱。
💕 理解创建普通文件和目录文件的默认链接数
创建一个普通文件的默认链接数是1,这是因为一个普通文件,自身就和该普通文件的iode建立了一个人映射关系,所以创建一个普通文件的默认链接数就是1了。
创建一个目录文件的默认链接数是2,这是因为当前路径的目录文件的inode编号和目录文件中的当前目录.
的inode编号是相同的。所以,创建一个目录文件的默认链接数就是2了,如果在当前目录下创建一个新的目录Test,那么Test中的上级目录..
的inode和当前目录是相同的,所以引用计数就变成3了。
Linux 不允许普通用户给目录建立硬链接
。原因是引入了对目录的硬连接就有可能在目录中引入循环,在目录遍历的时候系统就会陷入无限循环当中,这样导致无法定位到访问目录。Linux 的目录结构是一棵以 “/” 为根节点的树,如果允许自定义硬连接,则很有可能会破坏这个结构,甚至形成循环;而一旦形成循环,对于需要遍历目录树的命令,是致命的。所以为了避免对目录树结构的破坏,Linux 不允许用户自定义硬连接在目录上。
但是可以为目录建立软链接,因为软链接生成的文件会有独立的 inode 且该文件的类型为l。注:.
和 ..
是操作系统给目录建立的硬链接。
3. ACM 时间
A(Access):
最后一次访问文件的时间。这个时间并不是访问一次就会更新,而是达到一定的访问次数或者最近两次访问时间的间隔比较长才会更新该时间。因为访问文件的概率比修改文件的概率要大得多,所以如果该时间被频繁更改会降低 Linux 系统的效率。C(Change):
最后一次修改文件属性的时间。一般文件内容被修改,文件属性的修改时间也会更新,因为文件的大小也会方式变化。M(Modify):
最后一次修改文件内容的时间。touch指令
可以将以上三个时间全部更新为当前时间。细节:
所有文件的属性和位图结构需要先被加载到内存中,才能进行更改数据的更改。更改完后,才能将数据写回到磁盘中。
四、动静态库
1. 动静态库的介绍
- 静态库
(.a)
:程序在编译链接的时候把库的代码链接到可执行文件中。程序运行的时候将不再需要静态库- 动态库
(.so)
:程序在运行的时候才去链接动态库的代码,多个程序共享使用库的代码。- 一个与动态库链接的可执行文件仅仅包含它用到的函数入口地址的一个表,而不是外部函数所在目标文件的整个机器码。
- 在可执行文件开始运行以前,外部函数的机器码由操作系统从磁盘上的该动态库中复制到内存中,这个过程称为动态链接(dynamic linking)。
- 动态库可以在多个程序间共享,所以动态链接使得可执行文件更小,节省了磁盘空间。操作系统采用虚拟内存机制允许物理内存中的一份动态库被要用到该库的所有进程共用,节省了内存和磁盘空间。
2. 静态库的制作
在制作静态库之前,我们先来回顾一下以前我们在学习C语言的时候,多文件的分离编译的写法:
//add.h
#pragma once
#include<stdio.h>
extern int Add(int a,int b);
//add.c
#include"add.h"
int Add(int a,int b)
{
return a + b;
}
//sub.h
#pragma once
#include<stdio.h>
extern int Sub(int a,int b);
//sub.c
#include<stdio.h>
int Sub(int a,int b)
{
return a - b;
}
#include"add.h"
#include"sub.h"
int main()
{
int x = 100;
int y = 34;
printf("%d + %d = %d\n", x, y, Add(x, y));
printf("%d - %d = %d\n", x, y, Sub(x, y));
return 0;
}
当我们编译这三个文件时,需要输入指令:
gcc add.c main.c sub.c
现在这种情况还算好,因为需要编译的.c
文件并不是很多,但如果有一天我们需要编译的目标文件非常多的情况下,写起来就会非常的麻烦。而且在传输的过程中很有可能会出现目标文件丢失的情况。那么为了避免这种情况的发生,这里我们可以使用库来将目标文件打包,形成的包就是我们今天所要讲解的动静态库了。下面,我们先来看一下静态库的制作:
ar -rc libSResult.a add.o sub.o
//ar是GNU中的归档工具,archive——归档
//-r 替换
//-c 创建
//lib——库的前缀,.a——静态库的后缀,两者之间的内容就是静态库的名称了
我们可以给对方提供.o
文件(方法的实现),同时还有提供.h
(里面包含着都有哪些方法),此时对方是能用的。为了让用户更好的使用库,我们就有把所有的.o文件打成一个包,给对方提供一个库文件即可!把多个.o合并成一个文件,这个文件就是库,把包方式的不同就分为了动态库和静态库。库的本质就是.o文件的集合。
💕 使用Makefile来生成静态库:
libSResult.a:add_s.o sub_s.o
ar -rc libSResult.a sub_s.o add_s.o
add_s.o:add.c
gcc -c add.c -o add_s.o -std=c99
sub_s.o:sub.c
gcc -c sub.c -o sub_s.o -std=c99
.PHONY:out
out:
mkdir -p out/include
mkdir -p out/lib
cp -rf *.h out/include
cp -rf *.a out/lib
.PHONY:clean
clean:
rm -rf *.o libSResult.a out
将其他的文件清理之后,我们将文件只剩下静态库
和我们要执行的.c文件
。
将静态库拷贝到系统默认的搜索路径,头文件的默认搜索路径是:/usr/include/
,库文件的默认搜索路径是:/lib64/ 或者 /usr/lib64
。
接下来我们就可以编译我们的代码了,但是因为gcc默认链接的静态库是/lib64/libc.a,因此如果我们想要让我们自己的程序链接我们自己写的静态库,需要使用如下的指令:
gcc main.c -l静态库名称
//这里我们需要注意的是,静态库的名称是我们的静态库掉前缀,去掉后面的.a
但是,由于我们自己写的库并没有经过可靠的验证,所以一般不建议将我们的库写入系统的默认路径。因此,最后我们需要将我们的静态库卸载掉。
指定头文件、库的路径和要链接的库
gcc main.c -I ./hello/include/ -L ./hello/lib/ -lhello
//-I 指明头文件所在的路径
//-L 指明库的路径
//-l 指明所要路径的库(因为库路径下可能不止一个库)
3. 动态库的制作
静态库的代码会被拷贝到可执行程序中,动态库在没有被链接前就会被编译好,当使用动态库时,并不是直接将代码拷贝到可执行程序中,而是让可执行程序与动态库产生关联。
💕 生成目标文件
gcc -fPIC -c add.c -o add_d.o -std=c99
gcc -fPIC -c sub.c -o sub_d.o -std=c99
readelf -S 目标文件名 #查查看ELF格式的文件信息
-fPIC
选项的意思是形成与位置无关的目标二进制文件。动态库形成后,可以在内存的任意位置加载。而静态库的代码是拷贝到可执行程序中,可执行程序是有自己的地址空间的,所以静态库的代码需要拷贝到地址空间的特定位置,这就是与位置有关。静态库是按照绝对编址的方式,而动态库是按照相对编址(段地址+偏移量)的方式。
💕 将目标文件打包
打包动态库使用的是gcc + -shared 选项,这里我们需要注意的是,一定要加上 -shared选项。
gcc -shared add_d.o sub_d.o -o libDResult.so
使用 Makefile 来生成动态库:
libDResult.so:add_d.o sub_d.o
gcc -shared -o libDResult.so sub_d.o add_d.o
add_d.o:add.c
gcc -c add.c -o add_d.o -std=c99
sub_d.o:sub.c
gcc -c sub.c -o sub_d.o -std=c99
.PHONY:out
out:
mkdir -p out/include
mkdir -p out/lib
cp -rf *.h out/include
cp -rf *.so out/lib
.PHONY:clean
clean:
rm -rf *.o *.so out
现在我们将我们生成的动态库
和main.c
一起拷贝到一个新的文件夹demo
下:
[cjl@iZ8vb3efb0tbtvz8lz3upyZ demo]$ ls
main.c out
💕 链接动态库
gcc main.c -I out/include/ -L out/lib/ -lDResult
当我们链接动态库并生成可执行程序后,当我们执行这个可执行程序时,发现其报错原因是无法找到对应的动态库。 这是因为我们的库路径只告诉了gcc,而gcc只工作到可执行程序的形成,但是动态库是程序在运行的时候才去链接动态库中的代码的。但是操作系统和shell却不知道库文件的位置,所以我们还需要在程序运行时告诉操作系统动态库的位置。而程序运行时操作系统会去两个地方查找动态库,一个是默认的路径下(lib64),另一个就是环境变量 $LD_LIBRARY_PATH中,所以我们可以将我们的库文件添加到这两个地方。
这里我们需要注意两点:
- 如果
out/lib/
路径下只有静态库时,可以生成对应的可执行程序。这是因为gcc 默认链接的是动态库,而如果只有我们自己制作的静态库,没有自己制作的动态库,那么就只能链接该静态库了。
- 系统的动态库还是动态链接的,只是静态链接自己制作的静态库。如果想让全部库都是静态链接的话,需要加上
-static
选项。-static 的意义就是摒弃优先使用动态库的原则,而是直接使用静态库。
那么面对操作系统找不到动态库的情况,我们应该如何解决呢?下面我们了解一下动态库的加载,这个问题就可以迎刃而解了!
4. 动态库的加载
我们知道,在静态链接中,静态库的代码会直接被拷贝到可执行程序中。因此,当执行可执行程序时,静态库的代码也会被加载到内存中。那么,对于动态库的动态链接又是怎么做的呢?对于动态链接来说,可执行程序和动态库是分批加载的,动态库的代码会被加载到进程地址空间的共享区。
动态库的加载过程:
操作系统将磁盘中的动态库加载到物理内存中,然后通过页表将其映射到该进程的地址空间的共享区中,然后立即确定该动态库在进程地址空间中的地址,即动态库的起始地址,然后继续执行代码。此时操作系统就可以通过库函数中存放的地址。即.o文件在动态库中的偏移量,在加上动态库的起始地址得到.o文件的地址,然后跳转到共享区中执行函数,执行完之后跳转回来继续执行代码段后面的代码。这就是完整的动态库的加载过程。
如果多个进程使用同一个静态库,内存中就会有许多份静态库的代码,这样就会造成内存的浪费。但如果多个进程使用同一个动态库,动态库也只需要加载一次——动态库被加载到物理内存中并通过页表映射到某一个进程(假设是A进程)的共享区后,操作系统会记录该动态库在A进程共享区中的位置,当其他进程也需要执行该动态库的代码时,操作系统会根据记录的地址 + 偏移量通过页表跳转到A进程的共享区中执行该函数,执行完毕后再跳转回当前进程地址空间的代码段处。所以内存中只需要有一份动态库代码就可以了。
那么,对于上面 操作系统找不到动态库的情况 我们应该怎么做呢?
- 将动态库导入环境变量
LD_LIBRARY_PATH
中
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/cjl/Date_4-20/out/lib/
- 新增配置文件
在/etc/ld.so.conf.d/
路径下创建一个.conf
后缀的文件,该文件中保存动态库的路径,最后输入 sudo ldconfig
指令更新配置文件即可。
.conf
后缀文件中需要添加的动态库的路径:
/home/cjl/Date_4-20/out/lib
删除配置文件
- 在/lib64/路径下创建一个与动态库的软链接
💕 库的意义
站在使用库的角度,库的存在,可以大大减少我们开发的周期,提高软件本身的质量 (健壮性等)。同时还有许多好玩的库,比如:ncurses(基于字符的界面库),搜索关键词 Centos 7 yum 安装 ncurses。 boost(C++ 的准标准库)。