平时我们都知道地址,是内存单元的编号,指针则是存储变量地址的变量。
那么程序是否会有地址呢?程序是不占用内存的,存储在磁盘中,只有当运行时才会将数据载入内存中。进程的狭义概念是一个正在运行中的程序(进程详解看上一篇博客),因此进程是有地址空间的。
进程虚拟地址空间
假设现在有一个进程,它有一个变量a=100,此时我们创建一个该进程的子进程,子进程的一个特点是代码共享,数据独有。
然后在子进程中修改a的值为1,然后运行
int main()
{
pid_t pid=fork();
int a=100;
if(pid==0)
{
a=1;
printf("a=%d \n",a);
//printf("Pa=%d \n",&a);
}
else
{
printf("a=%d \n",a);
//printf("Pa=%d \n",&a);
}
return 0;
}
运行结果如下,a的值在子进程中确实发生了改变
那么,这两个进程中的a的地址是否相等呢?
两个进程中a的地址是相同的,但是同一块内存空间是不能同时存放两个相同的值的,数据不同,说明这两个进程中的a的地址肯定不是同一块内存空间,那么为什么终端显示上显示这两个a的地址相同呢?
实际上进程访问的地址都是虚拟地址,而我们常说的进程地址空间其实是进程的虚拟地址空间。
如何虚拟一个内存空间?
在LINUX系统下,虚拟地址空间实际上是一个 mm_struct的结构体,是对一块内存空间的描述,通过这个描述向进程虚拟出一个连续的,完整的内存空间。
如下图:
为什么需要虚拟地址?
为了让进程不直接访问物理内存
如过进程直接访问物理内存:
1.进程中的代码数据使用的是连续的地址空间,如果直接使用连续的物理内存会造成内存浪费。
2.直接访问物理内存会因为缺乏内存访问控制而导致进程的不安全
如何通过虚拟内存访问物理内存?
操作系统在为进程创建一个虚拟地址空间的时候,同时也创建了一个页表用于映射虚拟内存与物理内存的关系。
再回到上面的那个问题,在创建子进程的时候,其实就是复制父进程的PCB,同时父进程的虚拟地址空间也会被复制过去,因此在终端上显示的地址值才会是相同的,但实际上在物理内存中,当子进程中的a发生改变的时候,操作系统已经在内存上开辟了一块新的空间,用于这个改变后的a的数据存储,然后页表中关于a在物理内存上的映射也会改变到新开辟的内存空间上。
这种方式也被成为写时拷贝技术
写时拷贝技术:两个进程一开始指向同一块空间,等待发生改变的时候,再给子进程重新开辟空间
目的是提高子进程的创建效率。
使用虚拟地址可以实现数据在内存上的离散式存储,提高内存利用率。
并且可以在页表中实现内存访问控制。
虚拟地址空间:操作系统向进程通过 mm_struct结构体描述的一个虚假的,连续的,完整的地址空间。
三种内存管理方式:
分页式内存管理
分页式内存管理的虚拟地址组成:页号+页内偏移
页号:页表中页表项的编号。
页内偏移:具体一个变量首地址相较于内存页起始位置的偏移量。
页表主要功能:映射虚拟地址与物理地址的关系/提供内存访问控制
访问方式:通过虚拟地址中的页号和页内偏移通过页表找到进程中数据在物理内存上的位置。
页表组成如下:
页号 | 物理块号 |
---|---|
… | … |
2 | 5 |
1 | 6 |
7 |
物理内存块号*物理内存块大小+虚拟地址中偏移量=物理内存地址
假设内存大小为4G,页大小是4096字节,则意味着页号的位是虚拟地址的高二十位,低十二位为页内便宜。
(4G=2^32, 4096=2^12, 4G/4096=2^20)
分页式内存内存管理的优点:将物理内存进行分块管理,通过页表映射虚拟地址与物理内存关系实现数据的离散式存储,提高内存利用率。
分段式内存管理
虚拟地址组成:段号+段内偏移
访问方式:通过段表映射进程数据集在物理内存中的位置
段表
段号 | 物理段起始地址 |
---|---|
… | … |
进程首先通过虚拟地址中的信息找到段号,通过段号在段表中找到在物理内存中的起始位置,然后加上段内偏移,就可以得到进程中某个数据集的物理内存地址。
优点:使程序员对内存的管理更加方便,将内存段分为了代码段,初始化全局段等等段,什么变量就在什么段申请空间。
段页式内存管理
顾名思义,就是将分页式与分段式结合起来的一种内存管理方式。
虚拟地址组成:段号+段内页号+页内偏移
段表:段号 段内页表起始地址
页表:页号 物理块号
访问原理:先将进程数据分成若干段,为每个段进行命名,再将若干段分成若干页
访问方式:进程首先是通过自己的分段找到在段表中相应的段号,然后通过该段表项找到页表始址,通过页表项中的物理块号和页内偏移访问物理内存空间。
以上三种内存管理方式
分页式:提高了内存利用率
分段式:便于管理员/编译器对内存进行管理
段页式:结合了以上两种方式的优点