程序的地址空间

研究背景:32位平台

计算机物理内存的大小是固定的,就是计算机主板内存槽上的实际物理内存,cpu可以直接进行寻址,物理内存的容量是固定的,但是寻址的空间取决于cpu地址线的数量。在32位系统上,线性地址空间可达4G(2^32);这4G一般是按照3:1的比例进行分配,用户进程享有3G的空间,而内核独自享有剩下的1G内存

早期的内存分派方式:

最早的时候,计算机还没有虚拟机制,程序指令所访问的内存地址就是物理内存地址,所以就要将所有程序都加载到内存中,但是我们实际的物理内存只有4G。所以就会出现一些问题:

  • 当多个程序需要运行时,必须保证这些程序用到的内存总量要小于计算机实际的物理内存的大小。
  • 内存使用效率低,内存空间不足,就需要将其他程序展示拷贝到硬盘当中,然后将新的程序装入内存。然而由于大量的数据装入装出,内存的使用效率会非常低
  • 进程地址空间不隔离,由于程序是直接访问物理内存的,所以每一个进程都可以修改其他进程的内存数据,设置修改内核地址空间中的数据,所以有些恶意程序可以随意修改别的进程,就会造成一些破坏
  • 程序运行的地址不确定;因为内存地址是随机分配的,所以程序运行的地址也是不正确的

虚拟地址的引入:

因为早期内存的各种缺陷,所以后来引入了虚拟内存,每个进程都可拥有3G的虚拟地址空间,并且用户进程之间的地址是互不可见,互不影响的,也就是说,即使俩个进程对同一块地址进行操作,也不会产生问题。

我们看下面这段代码:

  1 #include<stdio.h>
  2 #include<unistd.h>
  3 #include<stdlib.h>
  4 
  5 int g_val = 0;
  6 
  7 int main()
  8 {
  9     pid_t id = fork();
 10     if(id<0)
 11     {
 12         perror("fork");
 13         return 1;
 14     }
 15     else if(id > 0)
 16     {
 17         printf("father[%d],PPID:%d,%d : %p\n",getpid(),getppid(),g_val,&g_val);
 18     }
 19     else
 20     {
 21         printf("child[%d],PPID:%d,%d : %p\n",getpid(),getppid(),g_val,&g_val);

 22     }
 23     sleep(1);
 24     return 0;
 25 }

运行结果如下:

我们发现父子进程里面的变量的值和地址是完全一样的,这很好理解,因为子进程是按照父进程为模板,子进程并没有对变量进行任何操作,但是我们将代码稍微改动一下:

  1 #include<stdio.h>
  2 #include<unistd.h>
  3 #include<stdlib.h>
  4 
  5 int g_val = 0;
  6 
  7 int main()
  8 {
  9     pid_t id = fork();
 10     if(id<0)
 11     {
 12         perror("fork");
 13         return 1;
 14     }
 15     else if(id > 0)//father
 16     {
 17         printf("father[%d],PPID:%d,%d : %p\n",getpid(),getppid(),g_val,&g_val);
 18     }
 19     else//child
 20     {
            g_val=100;//修改了g_val的值
 21         printf("child[%d],PPID:%d,%d : %p\n",getpid(),getppid(),g_val,&g_val);
 22     }
 23     sleep(1);
 24     return 0;
 25 }

输出结果如下:

 

我们发现,父子进程中的值不一样了,但是他们的地址还是同一块地址,所以得出以下结论:

  • 变量内容不一样。所以父子进程输出的变量绝对不是同一个变量
  • 但是地址是一样的,所以该地址一定不是物理地址
  • 在Linux下,这种地址叫做虚拟地址
  • 我们在c/c++语言所看到的地址,全都是虚拟地址,物理地址用户一概看不到,由OS统一管理
  • OS负责将虚拟地址转化为物理地址

虚拟地址就是,进程创建加载的时候,自身感知获得了一个连续的内存地址空间,然而实际上内核之分配了一个逻辑上的虚拟内存空间,并且对虚拟内存和磁盘通过mmap做映射关系,对虚拟内存和物理内存的映射关系;等到程序真正运行的时候,需要某些数据,并不在虚拟内存中,才会触发缺页异常,进行数据拷贝。

操作系统中管理内存的不同方法:

程序重定位;分段;分页;虚拟内存;按需分页的虚拟内存

分段:

  • 在编写代码的时候,只要指明了所属段,代码段和数据段中出现的所有地址,都是从0开始的,映射关系完全由操作系统维护
  • CPU将内存分割为不同的段,于是指令和数据有效地址并不是真正的物理地址,而是相对于段首地址的偏移地址

解决问题:

  • 因为段寄存器的存在,使得进程的地址空间得以隔离,越界问题很容易被判定出来
  • 实际代码和数据中的地址,都是偏移,所以第一条指令可以从0地址开始,系统会字段进行转化映射,也就解决了程序的地址不确定的问题
  • 可是分段并没有解决性能的问题,在内存空间不足的情况下,依旧要换入换出整个程序或者整个段。无疑要造成内存和硬盘之间拷贝大量数据的情况,进而导致性能问题

所以就有了分页

分页&虚拟地址(结构体mm_struct):

 页表的概念:

  • 页表是一个特殊的数据结构,放在内存空间的页表区
  • 每一个进程都有一个页表,PCB表中有指针指向页表
  • 页表用来存放逻辑地址于物理地址的对应关系,是否映射,是否缓存
  • 页表的每一个表项分为俩部分,第一部分记录此页是否在物理内存上,第二部分记录物理内存页的地址(如果在的话)
  • 当进程访问某个虚拟地址,去查页表的时候如果发现对应的数据不在物理内存中,则发送缺页异常。
  • 缺页异常处理过程:将进程需要的数据从磁盘中拷贝到物理内存里,如果物理内存已经满了,没有空地方了,那就找一个页覆盖,当然如果被覆盖的页曾经被修改过,就需将此页写回到磁盘

页表的状态:

  • 如果页表的有效位置为1,那么就说明虚拟地址存储的内存存储在物理页当中
  • 如果页表的有效位置为0,那么就说明虚拟地址存储的内存没有存储在物理页中,发生了缺页异常,需要处理这个异常

页表的工作原理:

  • 当cpu想访问父进程的g_val的值的时候,先根据其虚拟地址找到虚拟页,根据页表,找出页表中g_val对应的位置,查看该页表的表项是否有效,有效则为1 ,DRMA换命中,根据物理页号找到物理页中的内容,返回
  • 若为无效(0),则参数缺页异常,调用内核缺页异常处理程序,内核会选择一个无理由作为牺牲页,将该页的内容刷新到磁盘空间,然后将g_val映射到该物理页上面,然后页表中的该表项有效位置1,第二位存放对应物理内存页的地址内容
  • 缺页处理完成后,返回中断前的指令,重新指向,此时缓存命中,执行1
  • 将找到的内容映射到告诉缓存当中,CPU从告诉缓存中获取该值,结束

虚拟内存的工作原理:

Linux把虚拟内存划分位区域area的集合,一个area包括连续的多个页

area的数据结果如下:

  • 内核位每个进程维护了一个单独的数据结构task_struct(PCB)
  • 每个task_struct中都有一个指针(struct mm_struct * mm)指针,指向了mm_struct,该结构描述了虚拟内存的运行状态
  • mm_struct的pgd指针指向进程的以及页面的基地址,mmap指针指向vm_erea_struct链表
  • vm_area_sturct描述area的结构,vm_startb表示area的开始,vm_end表示area的结束位置,vm_prot表示area内的读写权限,vm_flags表示area内的页面是进程私有还是共享的,vm_nest指向的是下一个area节点

猜你喜欢

转载自blog.csdn.net/audience_fzn/article/details/81561763