目录
- 一.简介磁盘
- 1.磁盘的物理结构:
- 2.磁盘存储方式:
- 3.磁盘的逻辑抽象:
- 二.inode&&文件系统
- 1.inode
- 文件=属性(inode)+内容(data block)
- 为什么删除一个文件相比于写一个文件要快得多?
- 2.硬链接&&软链接
- 3.特殊的隐藏目录`.`和`..`
- 4.acm时间
- 三.动态库和静态库
- 1.生成静态库
- 静态链接:
- 静态链接的优缺点
- 2.生成动态库
- 如何运行动态库?
- 动态链接:
- 动态库的加载过程:
一.简介磁盘
在介绍Linux文件系统的时候先简单的介绍一下企业里比较常用的存储介质——磁盘
1.磁盘的物理结构:
盘片:
一个磁盘由多个盘片叠加而成。盘片的表面涂有磁性物质,这些磁性物质用来记录二进制数据。因为正反两面都可涂上磁性物质,故一个盘片可能会有两个盘面。
磁道、扇区:
每个盘片被划分为一圈一圈的磁道,将磁道划分为若干个小的区段,就是扇区。虽然很小,但实际是一个扇子的形状,故称为扇区(每个扇区的容量默认为512字节),每个磁道又划分为一个一个的扇区(上面用红线描出来的区域)。从图中可以看出,最内侧磁道上的扇区面积较小,而外侧磁道上的扇区面积较大,但是每个扇区一般来讲所存储的容量是相同的,因此内侧的扇区数据密度最大,外侧的扇区数据密度小。
柱面:
所有盘面中相对位置相同的磁道组成柱面,柱面数==磁道数,柱面就是磁道。
磁头:
通过磁性原理读取磁性介质上数据的部件。磁头和盘面没有接触,并且几个磁头来回摆动是共进退的。
2.磁盘存储方式:
磁盘寻址的时候,基本单位不是bit,更不是byte。而是扇区(默认情况下,扇区大小是512byte)。那么在盘面上如何定位一个或多个扇区呢?——CHS定位法
先定位在哪一个磁道(柱面Cylinder),然后再定位在哪一个磁头(Heads)也就是定位盘面,最后定位在哪一个扇区(Sector)。
3.磁盘的逻辑抽象:
一整个磁盘从上到下有很多扇区,不方便管理,我们把整个磁盘从逻辑上看做成一个sector_array[n]的数组这样的线性结构。把对磁盘的管理,转化为对这个数组的管理。那通过这个数组,怎么找到扇区呢?
假如盘面为4,每个盘面的磁道数为10,每个磁道的扇区数为100,每一面就有10*100=1000个扇区。
假如要找123号磁盘的位置,也就是n==123,那么就
先用123/1000=0 那么就在0号盘面,
再用123/100=1 那么就在1号磁道,
最后用123%100=23 也就是23号扇区。
通过类似这样的方式就可以知道盘面、磁道、扇区各是多少。而通过这个数组的下标就可以找到对应的地址,这个地址就叫做LBA地址。
推荐阅读
为什么OS要进行LBA逻辑抽象呢?直接用CHS不行吗?
1.便于管理。很显然用数组下标比三元组信息(CHS)要方便一些。
2. 不想让OS的代码和硬件强耦合。我们知道存储介质不一定只有磁盘,还有ssd、U盘等等,在OS看来都是LBA地址,对于不同的存储介质,只需要将LBA地址转换成对应的这些存储介质对应的存储地址即可,并不会影响OS。
二.inode&&文件系统
磁盘很大那怎么管理呢?磁盘被分为不同的分区,不同的分区又分为不同的分组。通过分治的思想一层一层的往上走,管好每一个分组,将类似的方法运用到每一个分组即可。
虽然对应的磁盘的访问的基本单位是扇区512byte,但是依旧很小!!OS内的文件系统定制的进行多个扇区的读取1KB或者2KB或者4KB为基本单位。你哪怕只想读取/修改1bit,必须将4KB加载到内存,进行读取或者修改,如果必要,再写回磁盘,和局部性原理有关系。
1.inode
为了能解释清楚inode我们先简单了解一下文件系统
拿Linux ext2文件系统为例,上图为磁盘文件系统图(内核内存映像肯定有所不同),磁盘是典型的块设备,硬盘分区被划分为一个个的block。一个block的大小是由格式化的时候确定的,并且不可以更改。例如mke2fs的-b选项可以设定block大小为1024、2048或4096字节。而上图中启动块(Boot Block)的大小是确定的,
- Block Group:ext2文件系统会根据分区的大小划分为多个Block Group。而每个Block Group都由着相同的结构组成。
- 超级块(Super Block):存放整个文件系统本身的结构信息。往往会在不同的组中各存一份,万一一个分组的Super Block被损坏了,把其他组的Super Block复制到过来就可以进行修复。记录的信息主要有:bolck 和 inode的总量,未使用的block和inode的数量,一个block和inode的大小,最近一次挂载的时间,最近一次写入数据的时间,最近一次检验磁盘的时间等其他文件系统的相关信息。若Super Block的信息被破坏,可以说整个文件系统结构就被破坏了。
- GDT,Group Descriptor Table:块组描述符,描述对应分组的属性信息。
- 块位图(Block Bitmap):Block Bitmap中记录着Data Block中哪个数据块已经被占用,哪个数据块没有被占用,被占用了就用1表示,没有被占用就用0来表示。数据块对应的位图结构,位图中的比特位位置和当前data block对应的数据块位置是一一对应关系!
- inode位图(inode Bitmap):每个bit表示一个inode是否空闲可用,被占用了就用1表示,没有被占用就用0来表示。位图中比特位的位置和当前文件对应的inode的位置是一一对应的!
文件=属性(inode)+内容(data block)
- inode(inode Table):存放分组内部所有文件的属性。包括文件大小、所有者、最近修改时间等(但inode中不包含文件名,那哪里存放文件名呢?Linux下一切皆文件,目录也不例外,存放这个文件的目录的data blocks中,才存放着文件名和inode的映射关系)
而且inode为了区分彼此,每一个inode都有自己的一个ID号。 - 数据块(Data blocks):保存的是分组内部所有文件的数据。
为什么删除一个文件相比于写一个文件要快得多?
- 如果我们想创建一个文件。先确认你在哪个路径下,根据路径去找inode的bitmap,先帮你找一个没有被使用的inode,然后把inode属性填进去,然后如果你有内容的话在block bitmap里,帮你把数据给你写进去。比特位置好,数据写好。然后把inode的属性填完,inode的信息直接返回,返回之后,然后再向当前目录下对应数据块里面写inode编号和文件名对应的映射关系就可以了,这就叫做创建文件。
- 如果想删除文件,那么先由路径找到对应inode,然后由inode找到块位图和inode位图把对应的位置由1置0即可。
所以这就是为什么删除一个文件相比于写一个文件要快得多的原因。- 所以说如果数据被误删了,我们最好什么都不做,不进行数据io,数据有被完整恢复的可能性。
2.硬链接&&软链接
真正找到磁盘上文件的并不是文件名,而是通过inode。 其实在linux中可以让多个文件名对应于同一个inode。
下图为软硬链接的操作:
解除链接用nulink def
或者rm都可以。
- abc和def被称为指向文件的硬链接,所谓硬链接就是在指定的路径下,新增文件名和inode编号的映射关系!内核记录了这个链接数,inode编号为926835的硬链接数为2。
让两个inode编号都指向同一个inode:
我们在删除文件时干了两件事情:
- 1.在目录中将对应的记录删除。
- 2.将硬连接数-1,如果硬链接数(count)为0(引用计数),此时文件才会真正的从磁盘被释放。
- 硬链接是通过inode引用另外一个文件,软链接是通过路径引用另外一个文件。
- 软链接相当于windows操作系统下的快捷方式,它内部保存的是被引用文件的文件路径。
- 软硬链接区别:是否具有独立的inode。
软硬链接有什么用?
软链接在动态库中有很大作用,后面会有详细介绍。
3.特殊的隐藏目录.
和..
- 一个普通文件创建之后默认的硬链接数是1;
- 一个目录(假设目录名为empty)创建的默认硬链接数是2,因为除了自己,还有一个
.
表示它当前路径的硬链接。
如果在这个目录empty里再创建一个目录dir,则此时empty硬链接数就变成3了,因为dir在中的..
就代表empty这个目录,所以硬链接数变成3了。
⚠️
.
和..
是操作系统自动创建的目录硬链接,但是Linux是不允许用户自己对目录进行硬链接的创建的。
因为如果给当前目录创建一个硬链接的话,那么在查找的时候会造成死循环,无法停下来。可能把目录树变为一环形图从而通过名字无法定位一个文件。
[root@VM-8-9-centos test]# ll -ai
total 12
927203 drwxr-xr-x 3 root root 4096 Jan 18 19:38 .
393219 dr-xr-x---. 16 root root 4096 Jan 4 18:49 ..
1049182 drwxr-xr-x 3 root root 4096 Jan 18 19:41 empty
926835 -rw-r--r-- 1 root root 0 Jan 18 19:38 test.txt
[root@VM-8-9-centos empty]# ll -ai
total 12
1049182 drwxr-xr-x 3 root root 4096 Jan 18 19:41 .
927203 drwxr-xr-x 3 root root 4096 Jan 18 19:38 ..
1049184 drwxr-xr-x 2 root root 4096 Jan 18 19:41 dir
1049183 -rw-r--r-- 1 root root 0 Jan 18 19:39 test.c
[root@VM-8-9-centos dir]# ll -ai
total 8
1049184 drwxr-xr-x 2 root root 4096 Jan 18 19:41 .
1049182 drwxr-xr-x 3 root root 4096 Jan 18 19:41 ..
4.acm时间
[root@VM-8-9-centos empty]# stat test.c
File: ‘test.c’
Size: 0 Blocks: 0 IO Block: 4096 regular empty file
Device: fd01h/64769d Inode: 1049183 Links: 1
Access: (0644/-rw-r--r--) Uid: ( 0/ root) Gid: ( 0/ root)
Access: 2023-01-18 19:39:47.634929230 +0800
Modify: 2023-01-18 19:39:47.634929230 +0800
Change: 2023-01-18 19:39:47.634929230 +0800
Birth: -
- Access:文件最后访问时间。例如通过
cat
查看文件,这个时间可能不会访问一次就改变一次,我们对一个文件往往会频繁的访问,如果真的实时更新的话,那么效率会有所下降,所以会访问一定次数或者时间之后才会改变这个Access时间。 - Modify:文件内容最近被修改的时间。但一般内容被修改了,文件的大小属性也就改变了,Change也就跟着被修改了。
- Change: 文件的属性最近被修改的时间。
三.动态库和静态库
静态库(libxxxxxx.a):程序在编译链接的时候把库的代码链接到可执行文件中。程序运行的时候将不再需要静态库
动态库(libxxxxxx.so):程序在运行的时候才去链接动态库的代码,多个程序共享使用库的代码。
库文件名称 如:libc.so -> c库,去掉前缀lib,去掉后缀.so,.a
库思想的引入:
如果我们不想直接给对方我们方法的源代码,但是还想让别人去用怎么办呢?下面拿比较简单的加法和减法举一个例子。
测试程序:
/add.h/
#pragma once
int add(int a, int b);
/add.c/
#include "add.h"
int add(int a, int b)
{
return a + b;
}
/sub.h/
#pragma once
int sub(int a, int b);
/sub.c/
#include "sub.h"
int sub(int a, int b)
{
return a - b;
}
///main.c
#include <stdio.h>
#include "add.h"
#include "sub.h"
int main(void)
{
int a = 10;
int b = 20;
printf("add(%d, %d)=%d\n", a, b, add(a, b));
a = 100;
b = 20;
printf("sub(%d,%d)=%d\n", a, b, sub(a, b));
}
把add.c和sub.c通过gcc -c得到add.o和sub.o这两个可重定位目标二进制文件。
[root@localhost linux]# gcc -c add.c -o add.o
[root@localhost linux]# gcc -c sub.c -o sub.o
gcc/g++的使用
接下来没有头文件,编译不通过
[hb@VM-8-9-centos ~]$ ll
total 16
-rw-rw-r-- 1 hb hb 1240 Jan 30 11:25 add.o
-rw-rw-r-- 1 hb hb 221 Jan 30 11:31 main.c
-rw-rw-r-- 1 hb hb 1240 Jan 30 11:25 sub.o
[hb@VM-8-9-centos ~]$ gcc main.c add.o sub.o
main.c:2:17: fatal error: add.h: No such file or directory
#include "add.h"
^
compilation terminated.
再给头文件编译就通过了。
[hb@VM-8-9-centos ~]$ ll
total 36
-rw-rw-r-- 1 hb hb 56 Jan 30 12:07 add.h
-rw-rw-r-- 1 hb hb 1240 Jan 30 11:25 add.o
-rw-rw-r-- 1 hb hb 221 Jan 30 11:31 main.c
-rw-rw-r-- 1 hb hb 56 Jan 30 12:07 sub.h
-rw-rw-r-- 1 hb hb 1240 Jan 30 11:25 sub.o
[hb@VM-8-9-centos ~]$ gcc main.c add.o sub.o
[hb@VM-8-9-centos ~]$ ./a.out
Add(10, 20)=30
Sub(100,20)=80
所以如果我们不想给对方我们的源代码,我们可以给对方提供.o(方法的实现)和.h(都有什么方法)。但是如果方法有很多怎么办,写起来很不方便。我们可以将所有的.o文件打一个包给对方提供一个库文件即可,其实库的本质就是.o文件的集合。根据打包工具和打包方式的不同就将库分为静态库和动态库两类。
1.生成静态库
生成静态库操作:
[root@localhost linux]# ar -rc libmymath.a add.o sub.o
ar是gnu归档工具,ar是archives归档的前两个英文字母,rc表示(replace and create)
查看静态库中的目录列表。
[root@localhost linux]# ar -tv libmymath.a
rw-r--r-- 0/0 1240 Sep 15 16:53 2017 add.o
rw-r--r-- 0/0 1240 Sep 15 16:53 2017 sub.o
t
:列出静态库中的文件
v
:verbose 详细信息
[hb@VM-8-9-centos ~]$ ll
total 28
-rw-rw-r-- 1 hb hb 2688 Jan 30 13:04 libmymath.a
-rw-rw-r-- 1 hb hb 222 Jan 30 17:06 main.c
[hb@VM-8-9-centos ~]$ gcc main.c -I mytest/ -L . -l mymath
选项和路径以及名称之间的空格可有可无
[hb@VM-8-9-centos ~]$ gcc main.c -Imytest/ -L. -lmymath
-I
:头文件路径,后面不需要跟头文件名,指定在哪个目录下即可。因为根据main.c中包含的头文件名会自动匹配对应的头文件。
编译器在搜索头文件时:
一在当前目录下搜索;
二在我们对应的系统默认指定路径上搜索。
-L
:库的路径。
-l
:库的名称,需要注意库名称是去掉前缀lib和.a的部分叫做库名;第三方库编译器不会自动匹配,所以第三方库除了-L
指明库路径之外还需要指明库名称。
测试目标文件生成后,静态库删掉,程序照样可以运行。
我们用gcc和g++的时候却没有向上面一样指明头文件和库,原因是gcc和g++分别是编译c和c++的工具,用他们的标准库的时候会自动帮我们匹配头文件和库。而且gcc默认是动态链接的(建议行为),对于特定的一个库,究竟是动,还是静,取决于你提供的是动态库还是静态库!如果动态库占一部分,静态库占一部分,那么就整体按动态库来看待。
推荐阅读:gcc -I -L -l区别
闲写的太麻烦,也想像gcc编译普通程序那样,不带头文件和库名称有什么方法呢?
可以把头文件和库名称拷贝(拷贝相当于安装)到默认的搜索路径下:
[hb@VM-8-9-centos mytest]$ sudo cp *.h /usr/include/
[hb@VM-8-9-centos ~]$ sudo cp libmymath.a /usr/lib64
但是运行的时候需要带上库名称,还是因为mymath属于第三方库
[hb@VM-8-9-centos ~]$ gcc main.c -l mymath
一般不建议直接把自己写的库安装在系统默认路径下。
静态链接:
静态链接就是在装载之前,就完成所有的符号引用的一种链接方式。静态链接的处理过程分为2个步骤:
1、空间与地址的分配。扫描所有的目标文件,合并相似段,收集当中所有的符号信息。
2、符号解析与重定位。调整代码位置。
静态链接的优缺点
优点: 简单
缺点:
1、浪费内存空间。在多进程的操作系统下,同一时间,内存中可能存在多个相同的公共库函数。
补充:假如一个printf函数,多个程序都调用printf函数,通过静态链接形成的可执行程序运行的时候os会为它会产生PCB,进而和进程地址空间又有关系了,这几个可执行程序的代码区会有相同的部分。
2、只要有一个库模块更新,那么就需要重新编译打包整个代码。
2.生成动态库
shared: 表示生成共享库格式
fPIC:产生与位置无关码(position independent code)
库名规则:libxxx.so
[hb@VM-8-9-centos mytest]$ gcc -fPIC -c sub.c add.c
[hb@VM-8-9-centos mytest]$ gcc -shared -o libmymath.so *.o
[hb@VM-8-9-centos mytest]$ ls
add.c add.h add.o libmymath.so main.c sub.c sub.h sub.o
[hb@VM-8-9-centos ~]$ ll
total 28
-rw-rw-r-- 1 hb hb 56 Jan 30 18:51 add.h
-rw-rw-r-- 1 hb hb 222 Jan 30 17:06 main.c
-rwxrwxr-x 1 hb hb 8432 Jan 30 20:31 myproc
drwxrwxr-x 2 hb hb 4096 Jan 30 18:45 mytest
-rw-rw-r-- 1 hb hb 56 Jan 30 18:51 sub.h
[hb@VM-8-9-centos ~]$ gcc -o myproc main.c -I . -L ./mytest/ -l mymath
[hb@VM-8-9-centos ~]$ ./myproc
./myproc: error while loading shared libraries: libmymath.so: cannot open shared object file: No such file or directory
程序可以编译,但是运行不了,从运行结果可以看出,这里只在编译的时候告诉动态库在哪里,但是运行的时候从默认的搜索路径就找不到动态库了(为什么运行的时候要找动态库呢?因为一开始就说到动态库是程序在运行的时候才去链接动态库的代码,多个程序共享使用库的代码),那么怎么解决?
如何运行动态库?
1、拷贝.so文件 或者 .so文件的软硬链接到系统共享库路径下, 一般指/usr/lib
2、更改环境变量LD_LIBRARY_PATH
LD_LIBRARY_PATH作用:用于指定查找共享库(动态链接库)时除了默认路径(./lib和./usr/lib)之外的其他路径。
[hb@VM-8-9-centos mytest]$ echo $LD_LIBRARY_PATH
:/home/hb/.VimForCpp/vim/bundle/YCM.so/el7.x86_64
//把带有动态库的路径添加到这个环境变量中。
[hb@VM-8-9-centos mytest]$ [hb@VM-8-9-centos mytest]$ export LD_LIBRARY_PATH=:/home/hb/.VimForCpp/vim/bundle/YCM.so/el7.x86_64:/home/hb/mytest
[hb@VM-8-9-centos mytest]$ gcc main.c -lmymath
//现在就可以运行了
[hb@VM-8-9-centos mytest]$ ./a.out
add(10, 20)=30
sub(100, 20)=80
但是这样下次再重新登录Linux的时候上次配置的环境变量就会失效。
3、配置/etc/ld.so.conf.d/,ldconfig更新
随便新建一个配置文件,就拿hb.conf
为例,把将/home/hb/mytest
通过vim写入到hb.conf配置文件里(注意普通用户需要用sudo提权,否则无法写入成功),然后通过ldconfig
更新一下配置(注意普通用户需要用sudo提权),程序就可以运行了,而且下一次重新登录Linux不会失效。
[hb@VM-8-9-centos ld.so.conf.d]$ sudo vim hb.conf
[hb@VM-8-9-centos ld.so.conf.d]$ cat hb.conf
/home/hb/mytest
[hb@VM-8-9-centos ld.so.conf.d]$ sudo ldconfig
4、在把 动态库 或者 动态库的软硬链接 直接复制到当前目录下。
动态链接:
基本思想就是将对符号的重定位推迟到程序运行时才进行。推迟到运行时进行符号的重定位,就能解决静态链接的两个缺点。
对于第一个缺点:在运行时重定位,如果在运行过程中调用了公共库函数或者其他模块的函数,系统只需要在内存中维护一份公共库代码(具体在进程地址空间中的共享区)即可,只要将不同应用程序对公共库函数的调用地址设置成相同即可。
对于第二个缺点:理论上只要将需要替换的模块更新,无需将整个应用程序打包。
动态库的加载过程:
如图,对于动态链接来说,可执行程序中存放的是动态库中某具体 .o 文件的地址,同时,由于组成动态库的可重定向文件是通过fPIC生成的位置无关码,所以这个地址并不是 .o 文件的真正地址,而是该 .o 文件在动态库中的偏移量。
接下来程序运行的时候,操作系统会将磁盘中的可执行程序加载到物理内存中,然后创建进程PCB,开始执行代码,当执行到库函数printf时,操作系统发现该函数链接的是一个动态库的地址,该地址是一个外部地址,操作系统就会暂停程序的运行,开始加载动态库。
然后操作系统会将磁盘中动态库加载到物理内存中,然后通过页表将其映射到该进程的地址空间的共享区中,os根据该动态库在地址空间中的地址(动态库的起始地址),以及库函数中存放的地址( .o文件在动态库中的偏移量),跳转到共享区中执行函数,执行完毕后跳转回来继续执行代码段后面的代码。这就是完整的动态库的加载过程。