飞道的博客

Linux-虚拟地址到物理内存之间的小秘密!!!【图文+代码】

267人阅读  评论(0)

 

前言

一、为什么创建进程?

二、进程虚拟地址空间

1.空间布局图

三.通过虚拟地址找到物理内存地址

  1.数据在物理内存中的存储

  2.如何通过虚拟地址找到物理内存呢?

1.分段式

2.分页式

3.段页式

总结


前言

我们上一篇介绍到进程的概念,运行,管理,以及它的状态。那么我们这一篇就了解一下进程的创建。


提示:以下是本篇文章正文内容,下面案例可供参考

一、为什么创建进程?

 

我们说过:进程就是运行起来的程序,程序运行起来需要被加载到内存中

对操作系统来说:管理程序的运行,就是将程序的运行过程描述起来,然后组织起来进行管理,描述的运行信息对操作系统来说就是运行中的进程。

进程就是pcb,创建一个进程不就相当于是创建一个它的描述,也就是PCB,复制了调用fork的这个进程pcb的信息(内存指针、程序计数器、.上下文数据)

我们用代码看一下


  
  1. #include<stdio.h>
  2. #include<unistd.h>
  3. int main (int argc, char *argv[])
  4. {
  5. pid_ t pid = fork(); //创建子进程
  6. //因为子进程复制了父进程,因此子进程与父进程运行的代码以及运行的位置都是一样的
  7. //从代码运行的角度来看,就都是从fork函数之后开始运行的
  8. //因为父子进程运行的代码数据都一样,因此无法直接分辨,只能通过返回值判断
  9. //对于父进程fork返回值>0;对 于子进程返回值==0
  10. //虽然父子进程代码相同,但是因为返回值不同,因此会各自进入不同的判断执行体
  11. if(pid> 0)
  12. {
  13. //this is parents
  14. printf( "this is parents:%d\n" ,getpid());
  15. }
  16. else if (pid == 0)
  17. {
  18. //this is child
  19. printf( "this is child:%d\n", getpid()); //getpid() 获取调用 的标识符-pid-进程ID[
  20. }
  21. else
  22. {
  23. //error
  24. printf( "error\n");
  25. }
  26. return 0;
  27. }

这个新的进程,运行的代码与调用fork的进程一样, 并且运行位置也相同。

 fork创建子进程之后,父好进程谁先运行,不一定, 大家都是pcb,操作系统调度到谁谁就运行。

为什么要创建子进程的原因:
子进程干的事情与父进程一样,当然使用返回值分流后可以有所不同
有任务了,创建一个子进程,让子进程去完成任务,出问题了崩溃的就是子进程,父进程就不会崩溃了(保护父进程-分摊压力)

创建了子进程,那么就会让子进程有相应的工作,但是由于子进程是拷贝父进程的,看起来并没有什么区别,那么它们是如何访问数据,及内存空间的。这就引入到了下面这个问题。

二、进程虚拟地址空间

1.空间布局图

在32位操作系统下

内核:内核总是驻留在内存中,是操作系统的一部分。内核空间为内核保留,不允许应用程序读写该区域的内容或直接调用内核代码定义的函数。

  1.  函数的返回值和参数。
  2. 临时变量,包括非静态局部变量,以及编译器自动生成的临时变量。
  3. 保存上下文:包括函数调用前后需保持不变的寄存器。

存储映射区域:该区域用于映射可执行文件用到的动态链接库。,若可执行文件依赖共享库,则系统会为这些动态库分配相应空间,并在程序装载时将其载入到该空间。。

堆区堆用于存放进程运行时动态分配的内存段,可动态扩张或缩减。堆中内容是匿名的,不能按名字直接访问,只能通过指针间接访问。当进程调用malloc/new等函数分配内存时,新分配的内存动态添加到堆上(扩张);当调用free/delete等函数释放内存时,被释放的内存从堆中剔除(缩减) 。

BBS段

  1.  未初始化的全局变量和静态局部变量
  2.  初始值为0的全局变量和静态局部变量
  3.  未定义且初值不为0的符号

Data段

 数据段通常用于存放程序中已初始化且初值不为0的全局变量和静态局部变量。数据段属于静态内存分配(静态存储区),可读可写。数据段保存在目标文件中,其内容由程序初始化。

保留区:位于虚拟地址空间的最低部分,未赋予物理地址。任何对它的引用都是非法的,用于捕捉使用空指针和小整型值指针引用内存的异常情况。

 

有了上面的认识,我们在看一下代码:


  
  1. 1 #include <stdio.h>
  2. 2 #include <unistd.h>
  3. 3 int val= 5;
  4. 4 int main()
  5. 5 {
  6. 6 pid_t pid=fork();
  7. 7 if(pid> 0)
  8. 8 {
  9. 9 printf( "this is parent:%d ,val: %d, %p\n",getpid(),val,&val );
  10. 10 }
  11. 11 else if(pid== 0)
  12. 12 {
  13. 13 printf( "this is child%d ,val: %d, %p\n",getpid(),val,&val);
  14. 14 }
  15. 15 else
  16. 16 {
  17. 17 printf( "error\n");
  18. 18 }
  19. 19 return 0;
  20. 20 }
  21. 21

我们可以看到   相同的变量 在父进程和子进程中都一样的,地址也一样。那么他是什么原理呢,我们画图了解一下

我们知道一个内存地址只能指向一个唯一的内存单元一个内存单元只能存储一个数据

我们通过上面可以看到其实进程中所访问的地址都是虚拟地址,都是一个假的地址,并非物理内存地址

我们所说的程序地址空间,实际上也是一个虚拟的地址空间,是操作系统为进程通过一个mm. struct结构体所描述的一个假的地址空间,mm_ struct (task_ size, start_ code, end_ code) --通过大小以及区域的编号描述

 

我们再将上面的代码修改一下


  
  1. 1 #include <stdio.h>
  2. 2 #include <unistd.h>
  3. 3 int val= 5;
  4. 4 int main()
  5. 5 {
  6. 6 pid_t pid=fork();
  7. 7 if(pid> 0)
  8. 8 {
  9. 9 printf( "this is parent:%d ,val: %d, %p\n",getpid(),val,&val);
  10. 10 }
  11. 11 else if(pid== 0)
  12. 12 {
  13. 13 val= 10;
  14. 14 printf( "this is child%d ,val: %d, %p\n",getpid(),val,&val);
  15. 15 }
  16. 16 else
  17. 17 {
  18. 18 printf( "error\n");
  19. 19 }
  20. 20 return 0;
  21. 21 }

我们在看一下结果

我们看到了  数据不同   但是地址相同为什么呢?我们画一幅简图了解一下。

数据不同   但是地址相同的原因:

子进程复制父进程,复制了pcb,页表,虚拟地址空间,所以父子进程除了个别数据(标识符)之外都是一样,并且父子进程的数据指向的是同一块物理内存,所以看起来父进程有什么子进程也有什么。但是进程之间要保持独立性 数据独有 各有个的数据,因此当某一块空间中的数据即将发生改变,则为子进程重新开辟物理内存,将数据拷贝过去。(写时拷贝技术----为 了提高子进程创建效率(毕竟有些数据从来不会改变(比如代码)重新开辟一块内存将数据拷贝过去,没有意义,反而占据了更多的内存) )

我们知道了进程是通过虚拟地址空间访问数据,那么又带来了下面这个问题。

为什么操作系统不让进程直接访问物理内存,而是弄了一个虚拟地址空间,让进程访问虚拟地址呢? ?若进程直接访问物理内存,有哪些不好的?

  1. 程序在编译时,编译器就会给指令和数据进行地址编号;但是如果某个地址内存已经被占用,则这个程序就运行不起来了--编译器的地址管理麻烦(无法动态的获知什么时候那块内存是否被使用,也就无法进行代码以及数据的地址赋值)
  2. 进程直接访问物理内存,如果有一个野指针, 你在操作的时候有可能就把其它进程中的数据改变了(无法进行内存访问控制)
  3. 程序运行加载通常需要使用一块连续的内存空间,对内存的利用率比较低

三.通过虚拟地址找到物理内存地址

  1.数据在物理内存中的存储

1.顺序存储

我们上面这种情况是属于,顺序存储,其必须要使用一块连续的内存,对内存的利用率比较低。

比如上面的,第一种情况,有一个16M的内存,但是里面有一个4M 8M的数据,所以想要在放一个8M的数据,它是放不下的,必须等上面8M的数据释放了之后才能存进来。

                     第二种情况,同样16M的内存,但是里面的数据分别是8M  4M,所以想要在放一个8M的数据,它同样是放不下的,必须等上面的4M的数据释放了之后才能存进来。

 

为了提高效率,我们又有了离散存储

通过虚拟地址空间映射到物理内存上进行数据存储,可以实现数据在物理内存上的离散式存储,提高内存的利用率。

并且每个进程都有自己的虚拟地址空间,因此对于每个进程来说,都会拥有自己的- -块连续的空间使用。

 

2.如何通过虚拟地址找到物理内存呢?

1.分段式

分段式:将虚拟地址的组成分为段号+段内偏移量(比如全局数据段有很多变量,他们的段号都是一样的, 也就意味着物理内存段的起始地址一样,但是每个变量的偏移量不同,)因此,通过段号对应的物理内存段起始地址,以及虚拟地址中的偏移量组成一个完整的物理地址,找到对应的物理内存单元。

分段式优缺点:

对编译器的地址管理比较友好;但是没有解决数据连续存储内存利用率低的问题。因为一个段管理了很多变量数据,这些变量就都是通过同一个起始地址进行偏移的,也就在物理地址中使用了连续的地址空间(分段式管理中,同一个段内地数据都使用了连续的地址空间)

因为分段式还是需要连续的空间,效率不高,所以又提出了分页式。

2.分页式

分页式管理=页号+页内偏移

因为通常物理块比较小,并且不要求同一个进程的多个数据必须在同一个块内,因此分页式实现了数据在物理内存中的离散式存储,提高了内存利用率。

并且页表会在进行内存访问的时候进行内存访问控制(是否有权限)

分页式内存管理的优点:实现数据离散式存储, 提高内存利用率,并且通过页表进行内存访问控制

3.段页式

段页式的内存管理:将内存进行分段,在每个段内采用分页管理。

也就是说:段页式管理=段号+页号+页内偏移

 


总结

以上是所说关于进程创建以及进程如何通过虚拟地址空间访问到物理内存空间的相关内容,感谢您的阅读,如有错误,欢迎指正!


转载:https://blog.csdn.net/qq_45615577/article/details/115678779
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场