【Linux的文件篇章 - 磁盘文件】

发布于:2024-05-03 ⋅ 阅读:(27) ⋅ 点赞:(0)

Linux之进程程序替换理解

前言:
前篇开始进行了解学习Linux的文件I/O等相关知识内容,接下来学习关于Linux的磁盘文件的基本知识,深入地了解这个强大的开源操作系统。
/知识点汇总/

1、磁盘文件

存在非常多的文件,被打开的文件只是少量的;没有被打开的文件,在哪里放着呢? — 磁盘 --》磁盘文件
其次要打开一个文件就得先找到这个文件,要先在磁盘中找到该文件 --》文件路径+文件名

1.1、文件如何存取的问题

1.物理磁盘

百度查看
盘片:可读可写可擦除,一片两面均可写。无尘。
磁头:一面一个磁头。寻址。不与盘片接触,悬浮在盘片上。
马达:转动
所以本质上属于是机械设备。存储容量大,稳定,性价比高。
随着时代的发展和机械磁盘存在的问题,易刮花等问题,更新迭代SSD。

2.分类:

1.桌面级磁盘(民用)
2.企业级磁盘(企业)

1.2、了解磁盘的存储结构

磁盘属于机械设备。是磁头和盘片,基于马达相对悬浮转动的。
优点容量大,性价比高,稳定。
缺点就是慢,盘片易受损。

计算机只认识二进制,0/1,在磁盘的盘片上,物理上是由一圈一圈的磁道组成,可理解为由无数个磁铁组成,比如N极代表1,S极表示0,最后通过转动磁头调整正负极,0101…

磁铁磁性能的特点就是满足悬浮,使得磁头不接触盘片。磁性也具备长久性,所以磁盘上的数据具备长久保存。

扇区:

一个个磁盘又被划分为一个个扇区,而不是以byte/bite为单位的。磁盘读写的基本单位是扇区。通常为512字节,也有4KB的…
一片 = n个磁道
一个磁道 = m个扇区
其中,尽管你的数据小于512字节,在磁盘中也是固定以512字节进行读写数据的。
又由于磁盘不是一片,而是一摞的盘片,所以由一摞盘片的扇区就构成了柱面。

1.3、如何找到指定位置的扇区?

a、找到并确认该面的磁头 — Header
b、找到指定的磁道/柱面 ---- Cylinder
c、找到指定的扇区 --------- Sector

所以想要找到任意一个位置的扇区,那么就需要3个参数:CHS地址法,对三个参数编址

1.Header – 磁头
2.Cylinder – 柱面
3.Sector – 扇区

磁盘中盘片为什么要高速旋转?磁头要左右摆动?

盘片的转动是找到该磁盘上的对应扇区,磁头的左右摆动是找到数据对应的那圈磁道。
即:定位扇区,定位磁道

从磁盘的角度分析:

文件 = 内容+ 属性 == 数据 == 都是二进制数据
文件存储到磁盘,即:文件其实就是在磁盘中占有几个扇区的问题。
但是全靠硬件肯定是不行的,需要结构操作系统。先描述,再组织。

1.4、对磁盘的存储进行逻辑抽象(了解OS对磁盘这样的硬件设备的抽象与管理)

为什么要抽象?

OS直接使用CHS寻址法,直接对实际物理地址更改的话,导致耦合度太高,工作量大,不易于变通。主要需要方便管理。

磁带 – 抽象理解磁带,把磁带拉直不就是由圆形变成了线性结构了吗?

抽象成线性结构。线性结构的每一个单元就是一个扇区。类似于数组的一个元素空间.
结果磁盘就被抽象成数组。 – sector disk_array[N]; – 数组的下标就对应的地址(编址) —》inode算法转换为 -->CHS对应的地址,就找到了实际的物理地址。维护就只需要维护虚拟的sector结构体

那么怎么通过算法转换呢?

比如:1000扇区,10个磁道 == 100个扇区/磁道
inode / 1000 == H —确定那一面
inode % 1000 == temp[N] ,N = 0~999
temp / 100 == C
temp % 100 == S
比如,当inode = 500带入上面的算法逻辑求得:
500/1000 = 0 – H
500%1000 = 500
500/100 = 5 – C
500%100 = 0 – S

抽象理解:

文件 = 许多sector的数组的下标

对于512个字节单元的扇区大小,合适吗?

一般而言,操作系统与磁盘的交互的数据,基本单位是4KB,即:8*sector
所以规定:4KB = 8个连续的扇区 --》一个数据块的大小
文件 = 很多块构成
对块号的维护管理,对于OS而言就变成了,读取数据以块为单位了。
只要知道第一个块的块号,其他的块号都知道了,一个起始地址和磁盘的总大小,有多少块,每一块的块号,如何转换到对应的多个CHS地址,就全清楚了。
LBA — 逻辑区块地址(logical Block Address,LBA)
所以抽象到最后,对磁盘的管理就变成了对数组的管理,LBA block[N];

但是这个数组是很大的,如何处理呢?

拆分数组,分组管理,且每组的策略相同 — 分区
只需要各组的起始地址和结束LBA下标。
windows中的C盘和D盘…就是分区管理。管理好一个区就能管好所有区,管理区的策略相同。
那么最后抽象成管理好磁盘,就需要把某一个分区管理好即可。

2、磁盘级文件系统

2.1、Linux文件系统特定

文件内容 和 文件属性 分开存储
文件 = 内容 + 属性(也是数据)
文件在磁盘存储的本质是:文件的内容 + 文件的属性数据

2.2、文件的6个概念以及inode

1.data block数据区:存放文件的内容,分为一个个4KB的数据块,只存储文件的内容
2.块位图:记录数据块中哪一个数据块已经被占用,哪一个数据块未被占用,
即:比特位的位置,表示块号,比特位的内容,表示该块是否被占用。
3.i节点表(inode表):存放文件的属性,如文件的大小,所有者,最近修改时间等。
Linux中文件的属性是一个大小固定的集合体。
struct inode
{
int size;
mode_t mode;
int creater;
int time;

int inode_number;
int datablocks[N];
}
一个文件一个inode属性集合(128字节) ---- 注意struct inode并不包含文件名
在内核层面上,每一个文件都要有一个inode number,我们通过inode号来标识一个文件。

查看文件的inode号命令:ll -li

我们能通过inode_number找到对应的文件属性信息,那么怎么找到文件的内容,以及在数据块的哪一个数据块呢?

在struct inode中还有一个成员int datablocks[N];,它指定了该文件占用了哪些数据块。–》数据块号–》内容
所以我们在上层要找到一个文件,只需要找到inode文件属性,通过文件属性和块的映射关系,就能找到数据+属性了。

4.inode位图:每个bit位表示一个inode是否空闲可用。
比特位的位置表示第几个inode,比特位的内容,表示是否被占用。
5.GDT:块组描述符,描述块组属性信息,即,用于管理块组的。
6.SuperBlock:超级块,存放文件系统本身的结构信息。整个分区等总的信息
但是根据结构图发现,超级块并没有单独独立出来,囊括后面的管理,而是处于结构体内;
说明超级块并不是每个分组都有的,而是特有的。而且不一定只有一个。可能在多个组存在。
在每一个分区内部分组,然后写入文件系统的管理数据 —》格式化 --》 在磁盘中写入文件系统

2.3、平时我们一直用的文件名,怎么拿到的inoode呢?

谈谈目录:
目录 = 文件属性 + 文件内容(目录内容放文件名和inode编号的映射关系)

所以这里就能更好的解释以下问题:

1.一个目录下不能建立同名文件。 – 因为文件名和inode互为键值,具备唯一性。
2.查找文件的顺序:文件名 --》映射找到 inode编号 --》根据所在的分区,确定范围的所在组 --》在所在组理,再找到inode map(位图) 和 inode lable(属性) --》 再找到属性里的data 和 数据块
3.再次理解目录的权限:
a.如果目录没有r权限,就查看不了目录的内容:本质是,是否允许我们读取目录的内容,即:是否允许读取到,文件名和inode的映射关系。
b.如果目录没有w权限,对于新建文件,是一定要对当前所出目录内容中写入,该文件的文件名和inode的映射关系的。所以没有w就写入创建不了新文件。
4.如何理解一个文件的增删查改呢?
基本都是根据文件名找到映射的inode编号,再根据inode找到分区分组的位图以及lable,最后找到data块,进行增、查,改。
而删比较特殊,只需要把bit map把占用1改为0为占用即可。即:删除并不用删除文件的属性+文件的内容,只需要将对应的位图由1置0,即可。
5.那么一个文件被误删除了,可以恢复吗?
可以,通过保留的层层记录(日志),重新置1,但是一旦删除后,所在的数据块就属于无效了,可以其他文件被覆盖了。
所以前提是,删除后的操作不会覆盖,被误删除的区域。
6.找到一个指定的文件 --》 找到所在的目录 --》打开 --》根据文件名:inode -->目标文件的inode
而目录也是文件,那么就会找目录名,目录名又要找层层路径目录名,最终找到了/根目录
而根目录是规定好的文件名和inode.这个过程称为逆向路径解析 – 由OS自己完成。
也就再次验证了,在Linux中定位一个文件,在任何时候,都要有路径的原因。
这样的工作显得繁琐,所以就有了linux会缓存路径。
7.我们现在知道了能够通过inode层层找到对应的位图和属性以及数据,那么这么多的分区,且inode是分区访问的,
可是inode在不同的分区里,是有可能相同的,那么怎么认定要找的文件,到底是属于哪一个分区的呢?

云服务器的分区:ls /dev/vda*
通过挂载的方式,挂载到指定得目录上。

2.4、虚拟分区

dd命令:实现一个文件得读入,写到另一个文件里。
dd if=/dev/zero/ of=disk.img bs=1M count=10
表示if这个/dev/zero/文件存在,则从/dev/zero/读入得数据,of=写到of=disk.img文件中,一个单元是1M,写10个,也就是10M的大小。
ls -l 查看到属于字符文件
du -h disk.img 查看文件的大小
mkf 查看可格式化的分区
mkfs.ext4 disk.img 指定格式化分区
mkdir mymnt 创建一个待挂载的目录
mount disk.img mymnt(sudo)
执行挂载到mymnt目录
最后能成功的在虚拟分区里创建一个文件。

说明:在访问一个分区之前,需要写入文件系统(也就是格式化) --》那么就得挂载到指定的目录下 --》所以进入该目录 --》 就能在指定的分区里进行文件的操作。
通过这样的方式,就能实现,

一个文件其实在访问之前,都是先有的目录,(至少从根目录开始的)只要有目录:
就能确定在哪个分区。
所以目录就能确定在哪个分区。

目录/路径通常是由谁提供的呢?

OS,用户,进程 --》 cwd
由内核文件系统·提前写入并组织好的,然后也是有我们自己提供的(打开的文件的相对路径/绝对路径)。

Linux内核在被使用的时候,一定存在大量的解析完毕的路径,要不要对访问的路径做管理呢?

比如路径缓存等。
路径解析的信息,一个文件一个dentry
struct dentry
{
struct dentry* next;
listnode list;
struct file*;
struct munt;
struct lru;

}
答:需要 --》 “先描述,再组织”

系统提供相对路径和绝对路径就是为了让用户方便使用。提高效率

3、inode 和软硬链接

3.1、先见一见软硬件链接

链接命令:ln
ln -s target_file.txt file_soft.link(后者链接前者)
ln -s(soft) – 软链接
ll 查看到生成为链接文件
file_soft.txt -> target_file.txt
ln -s target2_file.txt file_hard.link
ln 不加s – 硬链接
file_hard.link

图片:

3.2、软硬件链接的特征是什么?

根据现象:

1.软连接是一个独立的文件,因为有独立的inode number
软链接的内容:目标文件所对应的路径字符串,类似于Windows中的快捷方式。
所以删除快捷方式并不能删除目标文件,两者是各自独立的。也能解释,通过快捷方式能够找到目标文件的所在目录/路径。
2.硬链接不是一个独立的文件,因为inode numner用的目标文件的inode

软链接是“快捷方式”,而硬链接是个独立的文件,怎么理解硬链接呢?

1.硬链接就是一个文件名和inode的映射关系,建立硬链接,就是在指定目录下,添加一个新的文件名和inode number的映射关系。
2.建立硬链接就导致了两个索引,与之前结构中提到的引用计数属性就为2了。
3.硬链接就是一个文件名和inode的映射关系。删除源文件,对硬链接文件操作照样可以追溯到底层的数据块和属性,进行相应的操作。等同于重命名。

属性中有一列硬链接数,软链接2,硬链接1

文件的磁盘级引用计数:表示有多少个文件名字符串通过inode_number指向我的inode。

3.3、什么是软硬件链接?有什么用(应用场景)?

1.就是当作快捷方式使用(软连接)

mkdir -p bin/a/b/c
bin/a/b/c log conf
.link

2.硬链接就是一个文件名和inode的映射关系。

删除源文件,对硬链接文件操作照样可以追溯到底层的数据块和属性,进行相应的操作。等同于重命名(备份)。
dir
.① … other
. …②
比如:dir的inode = 12345
那么①.也是指向的inode = 12345,
同理②…也指向的inode = 12345,
所以能过实现返回上级目录,并创建当前目录就自动保存的硬链接。
这样该inode的引用计数为1+1+1 = 3
结论:
任何一个目录,刚开始新建的时候,引用计数一定是2;
目录内部,新建一个目录会让A目录的引用计数+1,所以一个A目录有几个引用计数,那么该目录下就有几个子目录。

硬链接的作用是:构建Linux的路径结构,让我们可以使用…来进行路径定位。

注意的是,Linux系统中,不允许给目录建立硬链接。(避免形成路径的环绕/死循环)

3.硬件链接还有一个作用:作为文件的备份(比较常用的场景就是云盘)

小结:
定位一个文件,只有两个方式:
1.通过路径(软连接)
2.直接找到目标文件的inode(硬链接),最终还是通过的inode_number
3.和…文件是固定的,所有系统指令在设定的时候,几乎都能知道.和…是什么的。

复盘整理思路

文件分为打开的文件和未打开的文件。
1.打开的文件 – 内核,内存
2.没有打开的文件 – 磁盘,文件系统有关。

4、动态库和静态库

4.1、我们用过那些库?

C/C++标准库(所有库函数都有具体的实现)

#include <stdio.h>
#include <string.h>


int main()
{
  char buffer[1024];
  strcpy(buffer,"hello bit\n");
  printf("%s\n",buffer);

  return 0;
}

查看可执行程序所依赖的库
libc.so.6 --> lib64/libc.so.6 — C库
ls /lib64/libc.so.6 --> /lib64/libc.so.6 -> libc-2.17.so --发现是库的软连接

4.2、动静态库

后缀:
Linux中.so(动态库) .a(静态库)
windows中.dll(动态库).lib(静态库)

动静态库怎么办/怎么用?
思考;
a、怎么做库?(开发者角度)
b、怎么使用库?(用户角度)

测试代码: stdio.c/stdio.h/mymath.c/mymath.h
stdio.c

#include "mystdio.h"
myFILE* my_fopen(const char* path, const char* flag)
{
  int flag1 = 0;
  int iscreate = 0;
  mode_t mode = 0666;
  if(strcmp(flag ,"r") == 0)
  {
    flag1 = O_RDONLY;
  }
  else if(strcmp(flag,"w") == 0)
  {
    flag1 = (O_WRONLY | O_CREAT | O_TRUNC);
    iscreate = 1;
  }
  else if(strcmp(flag, "a") == 0)
  {
    flag1 = (O_WRONLY | O_CREAT | O_APPEND);
    iscreate = 1;
  }
  else 
  {}
  int fd = 0;
  if(iscreate)
    fd = open(path, flag1, mode);
  else 
    fd = open(path, flag1);

  if(fd < 0) return NULL;
  myFILE* fp = (myFILE*)malloc(sizeof(myFILE));
  if(!fp) return NULL;
  fp->fileno = fd;
  fp->flags = FLUSH_LINE;
  fp->cap = LINE_SIZE;
  fp->pos = 0;
  return fp;
}
void my_fflush(myFILE* fp)
{
  write(fp->fileno, fp->cache, fp->pos);
  fp->pos = 0;
}
ssize_t my_fwrite(myFILE* fp,const char* data, int len)
{
  //write(fp->fileno, data, len);
  //写入操作本质就是拷贝,如果条件允许,就刷新,否则不刷新
  memcpy(fp->cache + fp->pos, data, len);
  fp->pos += len;
  if((fp->flags&FLUSH_LINE) && fp->cache[fp->pos-1] == '\n')
  {
    //write(fp->fileno,fp->cache, fp->pos);
    //fp->pos = 0;
    my_fflush(fp);
  }
  return len;
}
void my_fclose(myFILE* fp)
{
  //先刷新,再关闭
  my_fflush(fp);
  
  close(fp->fileno);
  free(fp);
}

stdio.h

#pragma once 

#include <sys/types.h> 
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>

#define LINE_SIZE 1024
//刷新方式 --- 主要写的FLUSH_LINE行刷新
#define FLUSH_NOW 1
#define FLUSH_LINE 2
#define FLUSH_FULL 4

struct _myFILE
{
  unsigned int flags;
  int fileno;

  //定义缓冲区
  char cache[LINE_SIZE];
  int cap; //容量
  int pos;//下次写入的位置
};
typedef struct _myFILE myFILE;
myFILE* my_fopen(const char* path, const char* flag);
void my_fflush(myFILE* fp);
ssize_t my_fwrite(myFILE* fp,const char* data, int len);
void my_fclose(myFILE* fp);

mymath.c

#include "mymath.h"

int Add(int x,int y)
{
  return x + y;
}

int Sub(int x,int y)
{
  return x - y;
}

mymath.h

#pragma once 

//#include <stdio.h>

int Add(int x,int y);

int Sub(int x,int y);

根据现象可知:

头文件相当于一个使用手册,提供函数声明,告诉用户如何使用;
.o文件提供实现功能,我们只需要补上一个main函数,调用头文件提供的方法,然后和.o进行链接,就能实现可执行程序了。
但是呢,这样当工程量巨大时,涉及多个.o文件就会容易造成丢失,打包也不好用;
那么能够解决此类问题的最好办法就是建立静态库,封装起来直接使用。

4.3、静态库相关指令

生成静态库:
ar -rc libmyexe.a *.o
ar是gnu归档工具,rc表示(replace和create)
打包所有.o文件封装成库文件。
查看静态库中的目录列表:
ar -tv libmymath.a
t:列出静态库中的文件
v:详细信息

4.4、动态库相关指令

gcc -fpIC -c mymath.c
-fpIC:产生位置无关码。

由于动态库相较于其他库,属于超高频的使用,所以gcc编译命令就已经集成了打包动态库的功能。

即:gcc -shared *.o -o libmyc.so(后缀.so)
-shared:表示共享
比如:
cp libmyc.so mylib
gcc main.c -I mylib/include/ -L mylib/lib -lmyc
ldd a.out
发现:
libmyc.so --> not found
此时能够生成可执行却编译不了,说明只是把动静态库的路径告诉了gcc/g++编译器,而并没有告诉操作系统

结论:

动态库问题,因为动态库要在程序运行时,要找到动态库加载并运行才行。
静态库,没有这个问题,因为静态库在编译器期间,就已经把代码拷贝到了我们的可执行程序内部了,所以就与是否需要加载库无关了。

4.5、那么怎么解决动态库的这个问题呢?

1.方法一:

对于操作系统,它只会默认去固定的lib64目录下查找,所以运行动态库,需要提前拷贝自定义的动态库到lib64目录下。
即:cp mylib/lib/libmyc.so /lib64(但是加入lib64可能会有一些影响,比如影响健壮性,不建议)

2.方法二:

通过软连接的方式,安全的重命名一份到lib64目录下
即:sudo ln -s /home/class11/project/lesson23/roommate/mylib/lib/mylibmy.so /lib64/libmyc.so
删除链接:
sudo unlink /lib64/libmyc.so

3.方法三:

环境变量–》LD_LIBRARY_PATH(加载库路径)
操作环境变量要注意规范操作:
LD_LIBA+RARY_PATH=$LD_LIBRARY_PATH:/home/class11/project/lesson23/roommate/mylib/lib/mylibmy.so
冒号后添加进环境变量.
不过此时的环境变量处于内存级的缓存中,断点和重启环境变量就会恢复到默认的配置文件的数据了。

4.方法四:

基于方法3修改换环境变量的.bashrc配置文件,使其永久有效。

5.方法五:

ls /tec/ld.so.conf.d
存放许多内核的配置文件,所以可以把用户自定义的配置文件
touch bit_111.conf
vim bit_111.conf -->填写绝对路径
添加配置文件并使配置文件生效–》ldconfig

4.6、动静态库到底是什么?

静态库:

所谓的库文件,本质就是将.o文件打包(指令:ar -rc)

动态库:

所谓的库文件,本质就是将.o文件打包(指令:ar -fpIC)(gcc/g++ -shard)

静态库VS动态库

默认链接的是动态库,如果没有使用-static,并且只能提供.a,就只能静态链接当前的.a库,
其他库需要正常记载动态链接。

-static的意义是什么呢?

必须强制性的,将我们的程序进行静态链接,这就要求我们链接的任何库都必须提供对应的静态库版本。

为什么要有动静态库?

提高开发效率

4.7、动态库的编译和使用特点

gcc/g+++ & 环境变量

库的安装:

cp mylib/include/.h /usr/include/
cp mylib/lib/
.a /lib64
但是不建议把自己的库存放进去

库的卸载:

rm /usr/include/mystdio.h
rm /usr/include/mymath.h
rm /lib64/libmyc.a

库名的规则:

C/C++的库,gcc/g++默认是认识C/C++库,libmyc.a --> 用户写的(第三方提供的) --》gcc/g++ 不认识 --》添加 -l 使其认识
gcc main.c -llibmyc.a(gcc/g++只认识库真实的名字 – myc,去掉前缀lib和后缀.a)
gcc main.c -lmyc 以-l链接myc就能执行了。

4.8、动态库的加载问题

gcc mian.c -I ./mylib/include/ -L ./mylib/lib -lmyc
-I:表示指定用户自定义头文件路径
-L;表示指定用户自定义库文件路径

第三方库:

ncurses库 – 窗口级管理的工具库
sudo yum list | grep ncurses
yum -y install ncurses
注意使用第三方库:去掉前后缀,导入第三方库
ls /lib64/libncurse.so --(去掉前后缀ncurse)

注意使用第三方库:去掉前后缀,才导入第三方库

ls /lib64/libncurse.so --(去掉前后缀ncurse)

动态库加载(库加载不考虑静态库,静态库编译时已经加载) – 可执行程序和地址空间(动态库使用的整体轮廓)
动态库在加载之后,要映射到当前进程的堆栈之间的共享区。
所以动态库也就共享库

我们的可执行程序,编译成功,没有加载运行成功,二进制代码中此时有地址吗?

测试代码:

#include <stdio.h>

int sum(int top)
{
  int i = 1;
  int ret = 0;
  for(i = 1;i<=top;i++)
  {
    ret += i;
  }
  return ret;
}
int main()
{
  int top = 100;
  int result = sum(top);

  printf("reulut : %d\n",result);
  return 0;
}

objdump -S code
转反汇编后,可查看到,文件在二进制阶段就已经包含了地址。
一旦文件被翻译成二进制文件后,一些变量/函数都被翻译成了对应的地址。

ELF格式的可执行程序:

二进制是有自己的固定格式的,elf可执行程序的头部 + 可执行程序的属性

可执行程序被编译之后,会变成很多行汇编语句,每一条汇编语句都有它的地址。
如何编址的呢?(比如以0000…全0,一直编址到1111…全1的范围)

编址的种类:

1.绝对编址 — 平坦模式
2.相对编址
虚拟地址 == 逻辑地址

ELF+加载器:各个区域的起始地址和结束地址,main函数的入口地址。
进程 = 内核数据结构 + 代码和数据

先创建内核数据结构,还是先把代码和数据加载到内存?

比如高考入学,现有你的成绩入榜的信息,才能办理入学。
所以先有内核数据结构,再加载代码和数据加载到内存。

5、探究地址空间

5.1、地址初始值从哪里来?

mm_struct 是结构体对象吗?有成员变量吗?
如果有成员变量,那么code_start,code_end,global_start…
那么初始值从哪里来的呢?

答:不是通过操作系统,因为还没从磁盘加载到内存,操作系统不会去管理;
上面说到,ELF+加载器,在程序编译时就已经,编址加上虚拟地址空间等;
而编址的用到的各个模式就会得到:各个区域的起始地址和结束地址,main函数的入口地址
从而拿取到这些结构体成员参数。所以是从可执行程序来的。
那么也验证了虚拟地址空间概念,不是OS独有的,而是,OS、编译器和加载器共同所需的。

5.2、虚拟地址和物理地址的范围映射(PC指针)

反汇编的每一条语句对应一个虚拟地址,虚拟地址通过页表对应一个实际的物理地址,CPU中存在一个指针寄存器pc指针。
pc指针保存当前执行语句的下一条指令的地址。
即:pc指针指向哪里,就去执行哪里。

所以我们既然能够通过加载器得到各个语句的物理地址,包括main函数的入口地址。
将pc指针指向main的地址即可(指向的虚拟地址)。
再结合页表,实现虚拟地址和物理地址的范围映射。

所谓的寻址就是CPU通过pc指针和页表,再到虚拟地址空间,再到物理地址,形成闭环。
以上就是程序的加载,接着谈库的加载。
1.进程创建阶段,初始化地址空间,让CPU知道main函数入口地址
2.加载后,每一行代码和数据,就都有了物理地址,自己的虚拟地址自己也知道(%p…),再然后就能构建映射了。

库被映射到虚拟地址空间的什么位置,重要吗?不重要。

只需要正文代码的首地址加偏移量就能找到在虚拟地址空间中库的地址。

加粗样式结论:

库函数调用,本质就是通过地址空间的反复跳转+偏移找到的。

比如刚开始的一个程序调用了库函数,并告诉我们同时加载了库,接着又来一个程序,调用同样的库函数,那么它怎么知道所需要的库是否被加载了呢?

即:那么我们使用库函数时,我们怎么知道它到底是否被加载呢?
根据现象:一个进程可以同时使用多个库,既然有多个库,那么肯定有对库管理的数据结构。“先描述,再组织”。
所以加载时,查以下库的结构体就能知道需要的库是否被加载。
相反思考静态库,随同程序一同被加载到内存的,所以它的映射关系的位置就不能是随意的。


网站公告

今日签到

点亮在社区的每一天
去签到