目录
1.文件系统概述
1.1磁盘的基本存储结构
1.2磁盘的基本逻辑结构
1.3操作系统中的文件系统
1.4文件系统如何对磁盘进行管理
2.软链接、硬链接
2.1软链接
2.2硬链接
2.3目录的硬链接数
3.静态库和动态库
3.1静态库的制作
3.2静态库的使用
3.3动态库的制作
3.4动态库的使用
3.5动静态库的加载方式
磁盘是一个机械结构构成的外设,所以访问的速度相对于CPU来说会显得非常慢。
一个磁盘由多个盘片(每个盘片对应多个盘面)和多个磁头(每个盘面对应一个磁头)构成(基本结构)。每一个盘面有很多的同心圆,这些圆圈就是磁道。磁盘寻址的基本单位为扇区(512字节),所以磁盘又被称为块设备文件。每一个磁道的某一段对应一个扇区(即使磁道的周长各不相同,但是数据量却一样)。
磁盘的物理存储结构并不是线性的,对其进行管理的成本会增加。为了方便,我们把它逻辑抽象为线性结构。
只要知道了扇区的下标,就可以看做成定位了一个扇区。这样的下标操作系统成为逻辑块地址(LBA地址)。
进行逻辑抽象的原因有两个:
1.便于管理。因为对线性数据结构的管理成本比对非线性数据结构管理的成本低。
2.不想让OS(操作系统)的代码和硬件强耦合。如果没有对磁盘进行逻辑抽象,那么OS的代码可能是只针对磁盘进行管理,但此时将磁盘替换为固态硬盘,那么OS将无法管理固态硬盘。
即使磁盘是以扇区(512字节)为IO的基本单位,但是操作系统仍然觉得这样不合适(512字节对于操作系统来说太小了,而且会增加IO次数)。所以OS内的文件系统定制了一套IO方法:每次进行IO时,一次性读取多个扇区,可能一次性读取两个扇区(1kb),也可能读取4个扇区(2kb),主流的一般都是读取8个扇区(4kb)。这样做的原因是为了提高IO效率和局部性原理。
所以此时得出两个结论:
1.操作系统以4kb为单位管理内存,称为页框。
2.磁盘文件以4kb为基本单位分块(特别是可执行文件),称为页帧。
即使把磁盘逻辑抽象成了线性结构,但是直接进行管理的成本还是太高。所以文件系统使用分治思想对磁盘进行管理,即分区、分组管理。
文件系统对每个分组进行管理。然后将每个分组的管理模式拷贝给其他分组,这样就形成了对分区的管理。最后将每个分区的管理模式拷贝给其他分区,这样就形成对整个磁盘的管理。
在Linux操作系统中,文件=内容+属性,所以Linux在管理文件时,其内容和属性是分批存储的。每一个文件对应一个inode,每一个inode是一个固定大小(128kb或者256kb),每一个inode存储了文件几乎所有的属性(除了文件名),inode为了区分彼此,每一个inode都有一个对应的id([ls -li]查看)。内容是使用data blocks(数据块)存储的,数据块的大小随着应用类型的变化也在变化。
可以看到上图绘制的分组结构,现在来进行解释每一个区域的工作是什么:
在进行文件的查找时,统一使用inode编号(可以跨组访问但不能跨分区,一个分区对应一套文件系统)。每一个inode都有一个data block blocks数组,存储数据块的编号,其默认大小为15。下标为0~12都是一级索引(一个编号对应一个数据块),下标13~14分别是二级索引和三级索引。二、三级索引和一级索引的区别就在于:一级索引的编号对应一个数据块,二级索引的编号对应一个存储其他数据块编号的数据块,三级索引以此类推。
删除文件时,不需要将文件的数据直接清楚掉,而是只要把inode bitmap的比特位由1置0即可(让操作系统认为这个inode无人使用)。所以文件被删除时,其数据还是存在的(是可恢复的),所以误删文件之后,什么也不要做!
为什么我们在自己操作文件时,并未使用inode编号而是直接操作文件名?其原因在于:我们的文件都是保存在目录下面的,目录也是一个文件(所以目录有其对应的inode),其data block存储所有文件的文件名和inode的映射关系,所以我们操作文件时使用的是文件名而不是inode。也就是说,我们只需要知道文件名,操作系统就会帮我们找到其对应的inode(所以同一目录下不能存在同名文件)。
使用如下命令为某一个文件建立软链接:
ln -s 文件名(默认为当前路径) 软链接名称
软链接有一个独立的inode,说明创建软链接即创建了一个新的文件。这个文件保存被链接文件的路径。所以其作用相当于Windows下的快捷方式。
使用如下命令为某一个文件建立硬链接:
ln 文件名(默认为当前路径) 硬链接名称
硬链接没有独立的inode,说明创建硬链接时没有创建新文件。硬链接的本质就是新增一个文件名和inode映射关系。
软硬链接的本质区别就是有无独立的inode。
所以对硬链接的修改,也是对原文件的修改(类似于C++的引用)。
注意使用[ll -i]这个命令时权限与拥有者之间的数字,这个数字便是硬链接数。删除文件时,并不是直接销毁这个文件,而是将其硬连接数减1,当硬链接数为0时,文件才被删除。
让我们创建一个空目录时,其默认硬链接数为2。
因为目录本身是一个文件,占用一个硬链接数;而目录下有一个隐藏的.目录,这个.目录指的是当前目录,所以又占用一个硬链接数。
Linux不允许普通用户为目录建立硬链接,其原因在于:硬链接目录时,需要将被链接目录下的所有数据都建立硬链接关系,所以这是一项相当大的工程,Linux不允许我们这么做。而能够为目录建立软链接,是因为目录本身就是一个文件,软连接只需要保存目录所在的位置就行了。
在C++编程时使用STL就是在使用C++给我们提供的库。如果某一天我们想要给其他人提供我们自己写的库,了解静态库和动态库是非常有必要的。
前面的文章感性介绍了静态链接和动态链接。静态链接直接将方法拷贝至程序中,这样使得访问速度快,但是空间占用率高;动态链接需要编译器主动去程序外部寻找方法,这样使得访问速度慢,但是空间占用率低。
库的思想无非就是将自己的目标文件(如果不想给他人透露我们的具体实现细节)和头文件提供给库的使用者。但是这样的作法是简单、直接并且暴力的。效率更高的做法便是:将我们自己所有的.o文件(目标文件)打包成库文件,配合头文件提供给使用者,而打包工具和打包方式的不同决定了库文件是静态库还是动态库。
我们实现两个头文件:my_add.h和my_sub.h;实现两个源文件:my_add.c和my_sub.c。代码编写完成后,编写makefile将其目标文件和头文件放入一个文件夹当中。注意库文件的昵称格式,一定要有前缀lib和后缀.a(静态库)或.so(动态库)。
生成静态库需要使用如下命名:
ar -rc lib*.a *.o...
//my_add.h
#pragma once
#include
int add(int x,int y);
//my_add.c
#include "my_add.h"
int add(int x,int y)
{
printf("%d + %d = %d\n",x,y,x+y);
return x+y;
}
//my_sub.h
#pragma once
#include
int sub(int x,int y);
//my_sub.c
#include "my_sub.h"
int sub(int x,int y)
{
printf("%d - %d = %d",x,y,x-y);
return x-y;
}
//makefile
libmystatic.a:my_add.o my_sub.o #静态库需要依赖目标文件
ar -rc $@ $^
my_add.o:my_add.c
gcc -c my_add.c -o my_add.o
my_sub.o:my_sub.c
gcc -c my_sub.c -o my_sub.o
.PHONY:output
output:
mkdir -p mystaticlib/include
mkdir -p mystaticlib/lib
cp -f *.h mystaticlib/include
cp -f *.a mystaticlib/lib
.PHONY:clean
clean:
rm -rf *.o libmystatic.a mystaticlib
箭头所指,就是我们制作好的库了。我们可以使用压缩工具将其放在yum上供人下载,也可以直接将此文件夹发给需要使用此库的使用者。
当使用者拿到制作好的库时,可以有两种方法使用。一是将此文件夹里面的头文件拷贝至/usr/include路径下(系统存储头文件的目录),再将此文件夹里面的库文件拷贝至/lib64路径下(系统存储库文件的目录),此过程也称为安装库;二是在编译自己写的程序时指明头文件、库文件的搜索路径。
主要介绍第二种方法,这里站在使用者的角度来编写一份代码:
//main.c
#include "my_add.h"
#include "my_sub.h"
#include
int main()
{
int ret1 = add(3,5);
int ret2 = sub(7,4);
return 0;
}
然后我们使用如下命令进行编译:
gcc main.c -o mytest -I(大写的i) ./mystaticlib/include -L ./mystaticlib/lib -l(小写的L) mystatic
现在解释命令各选项的作用:
那么对于第一种方法,各位朋友可以自己去尝试。只不过在编译的时候需要指定库文件昵称,因为对于gcc/g++这两款编译器来说,只能认识自己的库。
动态库的生成也需要目标文件,但是生成目标文件时需要多加一个选项(产生与位置无关码)。
产生与位置无关码的目标文件的命令:
gcc -fPIC -c *.c -o *.o
生成动态库的命令:
gcc -shared -o lib*.so *.o...
makefile可以这样编写:
//makefile
libmydynamic.so:my_add.o my_sub.o
###生成动态库(使用gcc编译多加一个-shared选项,阻止生成可执行文件)
gcc -shared -o libmydynamic.so my_add.o my_sub.o
###生成目标文件(使用gcc编译多加一个-fPIC选项,生成与位置无关码)
my_add.o:my_add.c
gcc -fPIC -c my_add.c -o my_add.o
my_sub.o:my_sub.c
gcc -fPIC -c my_sub.c -o my_sub.o
.PHONY:output
output:
mkdir -p mydynamiclib/include
mkdir -p mydynamiclib/lib
cp -f *.h mydynamiclib/include
cp -f *.so mydynamiclib/lib
.PHONY:clean
clean:
rm -rf *.o libmydynamic.so mydynamiclib
箭头所指,就是我们制作好的库了。我们可以使用压缩工具将其放在yum上供人下载,也可以直接将此文件夹发给需要使用此库的使用者。
与静态库的使用一样(一样使用main.c文件的代码为例),使用gcc生成可执行文件时操作一样。但是会发现,生成的可执行文件运行不了。其原因在于,使用的是动态库,可执行文件内部就没有动态库的拷贝,所以我们必须让OS和bash知道动态库在哪里。
让OS和bash知道动态库在哪里有四种做法:第一种,将动态库的路径写入LD_LIBRARY_PATH的环境变量中,这种方法的弊端是重启时失效(因为环境变量每次登录时自动配置);第二种,与静态库一样,将制作好的库的头文件、库文件暴力安装至系统目录下;第三种,在/etc/ld.so.conf.d/目录下自定义创建一个后缀为.conf的文件,里面写入动态库的存储路径,然后使用ldconfig命令更新配置;第四种,在系统路径下(/lib64)创建自己制作的动态库的软链接。
第一种方法演示:
第三种方法演示:
第四种方法演示:
静态库直接加载到程序的代码区(直接将静态库拷贝至程序里),加载到内存后形成进程地址空间,当cpu要处理调用静态库实现的方法的指令时,操作系统只能通过地址空间的代码区再通过页表映射到物理内存的代码区。
静态库的加载是一种绝对编址方式。因为程序里面包含有静态库,所以静态库在程序当中一定有绝对的地址。所以静态链接对内存空间占用极高(当有100个不同的程序都使用了同一个静态库并加载到内存中,那么内存当中就会有100份这个静态库)。
动态库的加载比较复杂,它是一种相对编址方式。因为编译生成可执行文件时没有将动态库拷贝至程序里,所以动态库的地址就不确定。那么程序的代码区存储的便是动态库的具体实现方法(函数)相对于动态库起始地址的偏移地址(假设动态库的起始地址为100,函数的地址为120,那么代码区存储的就是20),动态库的起始地址在动态库加载到内存时才能确定(操作系统有办法计算出动态库未加载到内存时函数的偏移地址)。