进程地址空间和页表

发布时间:2024年01月24日

一、程序&进程&虚拟地址空间

1.1 进程地址空间是什么?

??所谓进程地址空间,本质是描述进程可视范围的大小。更简单点来说,地址空间本质是内核的一个数据结构对象,类似PCB一样,地址空间也是要被操作系统管理的。即先描述,再组织。每一个进程都有一个自己的 task_struct 结构体来描述组织,而它的地址空间我们用 mm_struct 来描述组织。要对地址空间进行各种区域划分,我们需要用 start 和 end 来对线性地址进行标记。要让地址空间被管理起来,则每一个进程的 task_struct 结构体必然能指向 mm_struct,这样才能知道自己的代码、数据等相关信息在哪里,才能管理起来。看下面图示,了解Linux 内核中的地址空间

1.2 C&C++中的地址空间划分

??我们学习C&C++,经常接触到的地址空间并不是内存,准确来说程序&进程&虚拟地址空间。我们可以通过下面代码来验证地址空间的基本排布,同时得到以下结论,堆和栈想向而生,堆向上生长,而栈向下生长(即栈是先使用高地址,而堆是先使用低地址)

# include<stdio.h>                                                                                                         # include<stdlib.h>
# include<stdlib.h>
int g_unval;//未初始化
int g_val = 100;//初始化
int main()
{
    const char* str = "hello bit";
    char* mem = (char*)malloc(20);
    char* mem1 = (char*)malloc(20);
    char* mem2 = (char*)malloc(20);
    int a;int b; int c;
    printf("代码区: %p\n",main);
    printf("字符常量区 : %p\n",str);  
    printf("已初始化全局数据区: %p\n",&g_val);  
    printf("未初始化全局数据区: %p\n",&g_unval);  
    printf("\n");  
    printf("栈地址:%p\n",&a);  
    printf("栈地址:%p\n",&b);  
    printf("栈地址:%p\n",&c);  
    printf("\n");  
    printf("堆地址:%p\n",mem);  
    printf("堆地址:%p\n",mem1);  
    printf("堆地址:%p\n",mem2);  
    return 0;  
  } 

1.3 虚拟地址存在验证

??通过之前fork函数的认识,我们知道,fork创建子进程可以返回两次,并且父子进程读时共享,写时拷贝。那么写实拷贝的时候,同一变量名的两个变量地址是不是应该不同?通过下面的代码验证却会发现,两个变量地址相同!那么这两个变量地址肯定不是所谓的物理地址,即它们是虚拟地址,同时虚拟地址必然和物理地址存在一种映射关系!

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int g_val=100;
int main()                                                      
{                                                             
    pid_t id=fork();                                               
    if(id==0)                                                      
    {                                                              
        int cnt=5;                                                 
        while(1)                                                                                                                    
        {           
            printf("子进程 pid: %d, ppid: %d, g_val: %d,&g_val: %p\n",getpid(),getppid(),g_val,&g_val);    
            sleep(1);                                                                                      
            if(cnt) cnt--;    
            else{             
                g_val=200;    
                printf("子进程改变:g_val 100->200\n");    
                cnt--;                                    
            }             
        }        
    }        
    else    
    {       
        while(1)    
        {           
            printf("父进程 pid: %d, ppid: %d,g_val: %d, &g_val: %p\n",getpid(),getppid(),g_val,&g_val);    
            sleep(1);                                                                                      
        }                
    }            
    return 0;    
} 

二、页表

2.1 页表是什么?

??由上可知虚拟地址是存在的,对于进程地址空间我们需要先描述再组织,因此就多了一个 mm_struct 结构体。而对虚拟地址和物理地址存在的映射关系,我们也需要先描述再组织,在这其中会有一种表结构,即页表。页表的地址保存在CPU的cr3寄存器中,属于物理地址。当进程在CPU中轮转调度时,进程会把cr3寄存器中的页表相关信息带走,CPU下次调度时再恢复过来,这样就知道上次该进程调度时,代码执行的位置。
??每个进程都有自己的页表,父子进程的页表可以完全相同。页表中必然有 key-value 结构来保存虚拟和物理内存的映射关系。这样当fork父子进程数据没有做修改时,映射的的物理地址便是一样的。若父子进程中修改了数据,则进行写实拷贝,修改页表中物理地址的映射值,在物理内存中单独存储一份。但在用户看来,虚拟地址是一样的,这也就解决了上面的 g_val 值不同,而打印出来的地址却相同的疑惑

2.2 页表读写标志位

??一个程序运行有代码区和和字符常量区,它们具有只读属性。进程访问物理内存的怎么知道这个区域是可读还是可写的呢?物理内存是没有权限管理的,没有区分读写的能力(并不能自己独立判断访问是否合法),因此需要通过页表作为桥梁,在页表中设立标志位,而标志位用位图来标明其读写属性。
=

2.3 页表代码和数据加载标志位

??目前,主流的笔记本电脑物理内存大小是8GB,可我们却却能够玩十几个G,甚至几十个G的游戏,这充分证明操作系统可以对大文件进行分批加载!如果一下子把进程所有代码和数据加载到内存,由于进程调度和时间片轮转,后面很大一部分内存并不会被访问,就会造成时间和空间浪费。因此操作系统采取惰性加载的方式(即在进程需要使用某个资源时才将其加载到内存中)。同样在页表中设置一个标志位,用0和1来确定进程的代码和数据是否加载到内存中。
??在这个进程中先把先把页表中的虚拟地址填满,物理地址先填一部分。进程调度时,先找到该虚拟地址对应的标志位,看物理地址是否存在(即代码和数据是否已经加载到内存)。若标志位为1,再看其页表中读写权限访问物理内存。若标志位为0,则说明代码和数据并没有加载到内存里。此时操作系统触发缺页中断,找到该可执行程序在磁盘中的代码和数据,为其在物理内存中开辟一块空间加载进来,并补上在页表中的物理地址。此后,继续执行进程调度访问物理内存。

三、进程地址空间&页表总结

??现在我们对进程有了更加深入的理解,进程 = 内核数数据结构(task_struct&进程地址空间&页表)+ 程序的代码和数据。进程地址空间的存在可以让进程以统一的视角看待内存,虚拟地址空间可以让我们访问内存的时候,增加一个转换的过程,在这个转化的过程中,可以对我们的寻址请求进行审查,所以一旦异常访问,直接拦截,该请求不会到达物理内存,保护物理内存。因为有地址空间和页表的存在,将进程管理模块,和内存管理模块进行解耦合!

文章来源:https://blog.csdn.net/Front123456/article/details/135634816
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。