【Linux】深入理解程序地址空间

程序:只是一段代码,保存在文件中。

编译器在编译程序生成可执行文件时,会对每一条指令和数据,进行地址排号。

程序运行时,就会将指令和数据放到指定的内存当中去。而程序只有在运行的时候才会占据内存,因此程序地址空间又被叫做进程地址空间。
【Linux】深入理解程序地址空间_第1张图片

内存空间是这样的。
若运行中的程序直接访问物理地址,会怎么样呢?

  1. 可能导致程序无法运行起来。 程序在编译时,会给变量数据进行地址排号,但若是某个地址被占用了,就会使程序无法运行起来。(编译器无法动态的获取哪一块内存是否被使用)
  2. 野指针问题。 若进程直接访问物理地址,野指针可能更改其它进程的数据。
  3. 内存使用率低。 程序运行需要一块连续的地址空间,会一定程度上造成空间的浪费。

所以OS中设置了虚拟内存,通过虚拟地址空间映射到物理内存上。而使用C语言/C++时,变量或函数的地址,都是虚拟空间地址,物理内存地址用户一概看不到,由OS统一管理。而OS负责将虚拟地址映射到对应的物理地址

每运行一段程序,就会开辟连续的地址空间,若是每个程序占据的空间比较大,很多程序共同运行,就会导致有的程序在内存中无法运行。而连续开辟的内存地址空间的空间使用率是很低的。

进程使用了虚拟内存之后,每个进程都拥有自己的虚拟地址空间,都会有一块连续的空间使用。

看一下这段代码:

   #include 
   #include 
   #include 
  
   int global_val = 200;
   
   int main()
   {
       pid_t pid = fork();//创建子进程
      if(pid < 0)
      {
          printf("fork error\n");
          return 0;
      }
      else if(pid == 0)
      {
          printf("child:%d  %p\n",global_val,&global_val);
      }
      else
      {
          printf("parent:%d   %p\n",global_val,&global_val);
      }
      return 0;                                                                                                                                                                             
   }

其输出为:
【Linux】深入理解程序地址空间_第2张图片
发现子进程中和父进程使用的是同样的变量和地址。

对代码进行一点小更改:

   #include 
   #include 
   #include 
  
   int global_val = 200;
   
   int main()
   {
       pid_t pid = fork();//创建子进程
      if(pid < 0)
      {
          printf("fork error\n");
          return 0;
      }
      else if(pid == 0)
      {
          global_val = 100;
          printf("child:%d  %p\n",global_val,&global_val);
      }
      else
      {
          sleep(3);
          printf("parent:%d   %p\n",global_val,&global_val);
      }
      return 0;                                                                                                                                                                             
   }

其输出为:
【Linux】深入理解程序地址空间_第3张图片
可以看到子进程的变量改变了,而父进程的变量是没有改变的。

为什么子进程变量改变了,而父进程的变量没有改变?
子进程是父进程的一份拷贝,子进程拷贝了父进程所有的信息。在子进程中数据未发生改变的时候,子进程使用父进程的所有信息。

在第一份代码中,子进程中变量没有改变,父进程中变量没有改变。所以第一份代码中,地址相等,变量也相等。

第二份代码中,子进程中变量发生了改变,父进程中变量没有发生改变。相同的虚拟地址映射到了不同的物理地址。所以第二份代码,地址相同,变量不同。

这里的相同是指:子进程拷贝了父进程所有的信息,进程地址空间、PCB…

【Linux】深入理解程序地址空间_第4张图片
子进程数据发生更改,进行了拷贝了。

第二份代码中,这里涉及到了写时拷贝技术:Linux中fork()使用写时拷贝实现。写时拷贝是一种推迟或者免除拷贝的技术。OS并不复制整个进程地址空间,而是子进程父进程共享一个地址,当有数据写入,发生改变时,数据才会被复制,使每个进程都有了自己的拷贝。资源的复制只有在写入的时候才进行。 而在此之前,子进程只是可读共享的,这样就保证了父子进程的代码共享,数据独立。

写时拷贝技术带来的好处:

  1. 提高子进程创建效率。
  2. 节省资源。

那么为什么OS要使用虚拟地址空间?或者说虚拟地址空间带了什么好处?

  1. 提高物理内存的使用率。
  2. 保证进程之间的独立性

虚拟地址是如何映射到物理地址的?

操作系统中内存管理方式:

  1. 分段式:段号+段内偏移【Linux】深入理解程序地址空间_第5张图片

段表:操作系统记录内存分了多少块。
通过段号寻找对应的物理内存起始地址,再加上段内偏移量,就找到了物理地址。

  1. 分页式:页号+页内偏移,这里画的比较简单【Linux】深入理解程序地址空间_第6张图片
    在这里有所不同的时候,我们需要知道一个页面的大小。一般一个页面的大小为4K。
    则在32位OS中,内存为4G,则占有4* 1024* 1024* 1024/4* 1024个页号,即页表项。
    共计有2^20个页表项/页号。将内存分为很多个细小的块。

通过找到对应的页号,其物理地址和页内偏移就可找到变量的物理地址。

  1. 段页式:内存通过分段式进行管理,而每个段内使用分页式。
    首先取得段号,在段表中进行查找;在段表中,存放着对应段号的页表起始地址,再通过段内页表起始地址找到页表。

当前计算机使用的段页式管理

虚拟页会缓存在物理内存中。如图:
【Linux】深入理解程序地址空间_第7张图片
虚拟内存可缓存到页表中:页命中,VP2就会缓存在内存中。
缓存不命中:缺页 ,VP3不会命中,发生缺页中断。那么OS就会从磁盘复制VP3到内存中PP3,再更新PTE3,随后返回。
VP3:虚拟内存3.
PP3:物理内存3
PTE3:页表条目3。0:发生中断,1:可以缓存。
【Linux】深入理解程序地址空间_第8张图片
经过缺页中断之后:缺页处理程序会选择一个作为牺牲页,并从磁盘上VP3的副本取代它,

MMU利用页表来实现虚拟地址空间到物理地址空空间的映射:
【Linux】深入理解程序地址空间_第9张图片

那么该选择牺牲页呢?
采用内存置换算法

  1. OPT:最佳置换算法,所被置换出的页面是以后永远不会再使用的或者是最长时间内不会再使用。这个算法只是理论上的算法。
  2. FIFO:先进先出算法。会导致缺页率升高。
  3. LRU:最久未使用算法,将最久未使用的页面置换出来。 (一般使用这个算法)
  4. LFU:最不常用算法,一段时间内使用的次数最少,在将来使用的可能性也很低。

你可能感兴趣的:(linux学习,操作系统,linux,程序地址空间,写时拷贝技术)