【Linux】虚拟地址空间 --- 虚拟地址、空间布局、内存描述符、写时拷贝、页表…



一、虚拟地址空间

1.虚拟地址的引出看不到物理地址只能看看虚拟地址喽

1 #include <stdio.h>
  2 #include <unistd.h>
  3 
  4 
  5 int global_value = 100;
  6 int main()
  7 {
  8     pid_t id = fork();
  9     if(id < 0)
 10     {
 11         printf("fork error\n");
 12         return 1;
 13     }
 14     else if(id == 0)
 15     {
 16         int cnt = 0;
 17         while(1)
 18         {
 19             printf("我是子进程, pid: %d, ppid: %d | global_value: %d, &global_value: %p\n", getpid(), getppid(), global_value, &global_value);
 20             sleep(1);
 21             cnt++;
 22             if(cnt == 10)
 23             {
 24                 global_value = 300;
 25                 printf("子进程已经更改了全局的变量啦..........\n");
 26             }
 27         }
 28     }
 29     else
 30     {
 31         while(1)
 32         {
 33             printf("我是父进程, pid: %d, ppid: %d | global_value: %d, &global_value: %p\n", getpid(), getppid(), global_value, &global_value);
 34             sleep(2);
 35         }
 36     }
 37     sleep(1);
 38     return 0;
 39 }                          

1.
从程序的运行结果可以看出一些端倪就是一个全局变量在地址并未改变的情况下竟然出现了不同的值这说明什么呢首先一个变量肯定是只能有一个值的但是地址只有一个而变量的值却出现了两个那么就必须说明一个结论现在在内存中应该出现了两个变量了因为一个变量是绝对不可能出现两个值的所以我们可以推导出的结论就是内存中现在一定出现了两个全局变量global_value

2.
我们继续推导一个变量在内存中绝对是只能有一个地址的这是铁定的事实也是一定正确的结论那么我们打印出来的地址就肯定不是真实的地址因为真实的地址只能对应真实的一个变量也就是说其实现在的内存中有两个全局变量分别属于子进程和父进程两个进程并且他们都拥有自己的物理内存地址但是他们两个变量共用了一个虚拟地址运行结果打印出来的地址其实就是这个虚拟地址所以之前我们所学习到的C/C++程序中所看到的所有地址实际上都是虚拟的地址而不是真正在内存中操作系统分配给变量的实实在在的物理地址。
真正的物理地址用户一概看不到这些物理地址由OS统一进行管理

3.
虚拟地址还有另外两个名字分别叫做线性地址逻辑地址

在这里插入图片描述

2.虚拟地址空间布局五个段

1.
在内存中是以字节为单位存储信息的每一个字节单元都会有唯一一个内存物理地址又叫实际地址、绝对地址或二进制地址物理地址空间从0开始编号每次按顺序增加1所以虚拟地址空间是呈线性增长的。

2.
在32位操作系统下理论上应该有2^32次方个物理地址也就是4×2的30次方个地址1G=1024MB1MB=1024KB1KB=1024byte1byte=8bit1bit=1二进制位所以在32位操作系统下分配给进程的虚拟地址空间大小为4G。

在这里插入图片描述

3.代码段又称为文本段、程序代码区公共代码区
存放函数体的二进制代码一个C程序由多个函数体构成C程序的执行其实就是函数之间的相互调用换种说法就是存放程序执行的机器指令二进制指令代码段是只读的这样的好处就是可以防止其他进程恶意修改正在运行的进程的二进制指令程序的执行就是从代码段中的main函数开始执行程序运行结束后由操作系统回收此区域。

4.栈段又称为堆栈区
栈是向下增长的与堆段的增长和收缩方向正好相反函数的局部变量、返回值形参等都在栈区上函数调用时开辟的栈帧就是在栈段上。

5.堆段
程序运行时动态申请的内存空间就是在堆段上开辟的由开发人员手动申请手动释放若不手动释放在程序结束之后由操作系统回收例如malloc或new等申请的内存空间就在堆段上堆的内存分配属于动态分配在未显示调用delete或free释放申请的空间时其生命周期为进程的生命周期。

6.未初始化数据段又称为BSS段Block Started by Symbol以符号开头的块
包含所有未初始化的全局变量和局部static变量此段中的所有变量都由零或者空指针初始化该区域大小在编译阶段就已确定运行时的内存分配属于静态内存分配该区域有读写权限所以此段中的变量的值在程序运行期间可以任意改变。

7.已初始化数据段又称为DS段Data Segment数据段
用于存储初始化的全局变量、static变量、对象、也包括字符串、数组等常量但基本类型的常量不包含在其中他们会被编译成指令存放于代码段中段大小在编译时就已确定所以内存的分配也属于静态内存分配。
数据段有只读区域和读写区域分别是.data和.rodata.data段保存的是哪些已经初始化了的全局变量或局部静态变量.rodata段存放的是只读数据一般是程序里面的只读变量如const修饰的变量和字符串常量有的人又把.rodata段称为常量区《程序员的自我修养》书上称之为只读数据段。

8.有些人把已初始化数据段和未初始化数据段合起来叫做静态区或全局区或直接叫数据段这也很好理解因为这两个段里面的变量都是静态或全局变量所以叫做静态区或全局区也理所应当。多提一句static修饰的全局变量会由原来的外部链接属性改为内部链接属性

9.内核空间
非常抱歉我当前的这个水平无法给您讲解透彻内核空间的知识下面的知识内容是我从腾讯云开发社区找到的内容如果您能看懂您可以详细了解一下下面的内容对于我拙劣的编程水平再次深表歉意希望我能快快成长起来吧
在这里插入图片描述
映射段也被称为共享区
在这里插入图片描述
10.程序在未运行之前虚拟地址空间中是没有栈区和堆区的。
在这里插入图片描述

3.感性理解一下虚拟地址空间操作系统画给进程的大饼

1.进程它会认为自己是独占系统资源的事实上并不是

下面讲一个故事来让大家更好的理解究竟什么是虚拟地址空间

在美国有一个名叫peter的大富翁他手上有10亿美金但是呢他没有结婚因为他总觉得总有刁民想害朕他睡觉都需要保镖在门外守护着他防止意外事件发生不结婚的peter就在外面乱搞所以他有3个私生子但是这三个私生子互相之间是不知道对方的存在的他们都以为自己是peter的唯一一个儿子son1是某个工厂的老板son2是某个金融领域公司的CEOson3在MIT就读peter为了让这三个孩子能够认真学习工作就开始画饼了画饼分好饼和坏饼peter分别单独见了这三个孩子画的饼都是一样的说儿子呀你好好干爸就你这么一个儿子等将来爸驾鹤西去的时候我手上的十亿美金就全是你的了petr对三个儿子画了相同的饼每个儿子都会和peter要钱比如大儿子要个几十万美金要给员工发工资二儿子要个几万美金想买块手表三儿子要个几千美金想买双鞋什么的但peter知道这几个儿子是不会直接张口要十亿美金的因为这是peter画给他们各自的饼如果这个饼很容易实现那怎么称之为画大饼呢所以这几个儿子平常就会不定期的问peter要上一些钱但绝不会张口要十亿美金。

2.
故事讲完了peter其实相当于我们的操作系统答应给儿子的十亿美金就是虚拟地址空间儿子就相当于进程平常给到儿子手里的美金才是实际分配给进程的物理内存空间。

3.
peter画饼动动嘴皮子就可以了那操作系统给进程是如何画饼的呢操作系统其实是通过一个叫mm_struct的数据结构来给操作系统画饼的这个数据结构定义出来的对象其实就是操作系统给进程分配的虚拟地址空间每个进程都以为他自己是独占4G的内存空间的但是实际上并不是这样操作系统只会分配给进程他实际需要的空间大小而不是说直接把4G的空间全都给进程这4GB大小只是一个范围而不是空间也就是一个区域性的概念。

4.
虚拟地址空间的本质就是内核的一种数据结构叫做mm_struct结构体这个结构体是描述虚拟地址空间的最核心的结构

4.mm_struct内部结构详谈OS画的大饼

1.
下面就是mm_struct中各个区域的划分每个区域都有自己的区域起始地址和区域结束地址但这些地址都是虚拟地址这都是操作系统画给进程的大饼。

2.
代码区BSS段数据段等都是在编译阶段固定好空间大小的但栈和堆的区域是可以在进程运行的过程当中不断变化的所以这两个区域的空间大小是随时进行调整的区域调整的本质就是修改区域的起始地址和结束地址

3.
定义局部变量调用函数malloc、new开辟空间本质上就是在扩大栈区或堆区
函数调用结束free、delete释放空间本质上就是在缩小栈区或堆区

在这里插入图片描述
4.每个进程都要有自己的虚拟地址空间这个虚拟地址空间是4GB大小如果是64位操作系统自然虚拟地址空间大小就是16GB因为2 ^ 64次方就是2 ^ 32 × 2 ^ 32也就是4GB×4GB=16GB。

下面是task_struct当中的两个指向进程对应的虚拟地址空间的两个指针成员。
task_struct实际上就是通过这两个成员来和mm_struct结构体进行联系每一个进程都会有唯一的mm_struct结构体。

    //关于进程的地址空间指向进程的地址空间。链表和红黑树
    struct mm_struct *mm, *active_mm;

下面是mm_struct结构体内容。




struct mm_struct {

    //指向线性区对象的链表头
    struct vm_area_struct * mmap;       /* list of VMAs */
    //指向线性区对象的红黑树
    struct rb_root mm_rb;
    //指向最近找到的虚拟区间
    struct vm_area_struct * mmap_cache; /* last find_vma result */

    //用来在进程地址空间中搜索有效的进程地址空间的函数
    unsigned long (*get_unmapped_area) (struct file *filp,
                unsigned long addr, unsigned long len,
                unsigned long pgoff, unsigned long flags);

       unsigned long (*get_unmapped_exec_area) (struct file *filp,
                unsigned long addr, unsigned long len,
                unsigned long pgoff, unsigned long flags);

    //释放线性区时调用的方法          
    void (*unmap_area) (struct mm_struct *mm, unsigned long addr);

    //标识第一个分配文件内存映射的线性地址
    unsigned long mmap_base;        /* base of mmap area */


    unsigned long task_size;        /* size of task vm space */
    /*
     * RHEL6 special for bug 790921: this same variable can mean
     * two different things. If sysctl_unmap_area_factor is zero,
     * this means the largest hole below free_area_cache. If the
     * sysctl is set to a positive value, this variable is used
     * to count how much memory has been munmapped from this process
     * since the last time free_area_cache was reset back to mmap_base.
     * This is ugly, but necessary to preserve kABI.
     */
    unsigned long cached_hole_size;

    //内核进程搜索进程地址空间中线性地址的空间空间
    unsigned long free_area_cache;      /* first hole of size cached_hole_size or larger */

    //指向页表的目录
    pgd_t * pgd;

    //共享进程时的个数
    atomic_t mm_users;          /* How many users with user space? */

    //内存描述符的主使用计数器采用引用计数的原理当为0时代表无用户再次使用
    atomic_t mm_count;          /* How many references to "struct mm_struct" (users count as 1) */

    //线性区的个数
    int map_count;              /* number of VMAs */

    struct rw_semaphore mmap_sem;

    //保护任务页表和引用计数的锁
    spinlock_t page_table_lock;     /* Protects page tables and some counters */

    //mm_struct结构第一个成员就是初始化的mm_struct结构
    struct list_head mmlist;        /* List of maybe swapped mm's.  These are globally strung
                         * together off init_mm.mmlist, and are protected
                         * by mmlist_lock
                         */

    /* Special counters, in some configurations protected by the
     * page_table_lock, in other configurations by being atomic.
     */

    mm_counter_t _file_rss;
    mm_counter_t _anon_rss;
    mm_counter_t _swap_usage;

    //进程拥有的最大页表数目
    unsigned long hiwater_rss;  /* High-watermark of RSS usage *///进程线性区的最大页表数目
    unsigned long hiwater_vm;   /* High-water virtual memory usage */

    //进程地址空间的大小锁住无法换页的个数共享文件内存映射的页数可执行内存映射中的页数
    unsigned long total_vm, locked_vm, shared_vm, exec_vm;
    //用户态堆栈的页数
    unsigned long stack_vm, reserved_vm, def_flags, nr_ptes;
    //维护代码段和数据段
    unsigned long start_code, end_code, start_data, end_data;
    //维护堆和栈
    unsigned long start_brk, brk, start_stack;
    //维护命令行参数命令行参数的起始地址和最后地址以及环境变量的起始地址和最后地址
    unsigned long arg_start, arg_end, env_start, env_end;

    unsigned long saved_auxv[AT_VECTOR_SIZE]; /* for /proc/PID/auxv */

    struct linux_binfmt *binfmt;

    cpumask_t cpu_vm_mask;

    /* Architecture-specific MM context */
    mm_context_t context;

    /* Swap token stuff */
    /*
     * Last value of global fault stamp as seen by this process.
     * In other words, this value gives an indication of how long
     * it has been since this task got the token.
     * Look at mm/thrash.c
     */
    unsigned int faultstamp;
    unsigned int token_priority;
    unsigned int last_interval;

    //线性区的默认访问标志
    unsigned long flags; /* Must use atomic bitops to access the bits */

    struct core_state *core_state; /* coredumping support */
#ifdef CONFIG_AIO
    spinlock_t      ioctx_lock;
    struct hlist_head   ioctx_list;
#endif
#ifdef CONFIG_MM_OWNER
    /*
     * "owner" points to a task that is regarded as the canonical
     * user/owner of this mm. All of the following must be true in
     * order for it to be changed:
     *
     * current == mm->owner
     * current->mm != mm
     * new_owner->mm == mm
     * new_owner->alloc_lock is held
     */
    struct task_struct *owner;
#endif

#ifdef CONFIG_PROC_FS
    /* store ref to file /proc/<pid>/exe symlink points to */
    struct file *exe_file;
    unsigned long num_exe_file_vmas;
#endif
#ifdef CONFIG_MMU_NOTIFIER
    struct mmu_notifier_mm *mmu_notifier_mm;
#endif
#ifdef CONFIG_TRANSPARENT_HUGEPAGE
    pgtable_t pmd_huge_pte; /* protected by page_table_lock */
#endif
    /* reserved for Red Hat */
#ifdef __GENKSYMS__
    unsigned long rh_reserved[2];
#else
    /* How many tasks sharing this mm are OOM_DISABLE */
    union {
        unsigned long rh_reserved_aux;
        atomic_t oom_disable_count;
    };

    /* base of lib map area (ASCII armour) */
    unsigned long shlib_base;
#endif
};

在这里插入图片描述

这部分内容转载自csdn博主宇哲_安菲尔德的文章

二、为什么要存在虚拟地址空间页表的引出

1.虚拟存储技术是操作系统管理进步的体现可怜的进程不知道自己已经被画饼了

虚拟内存是计算机系统内存管理的一种技术。它的优点是使得应用程序认为它拥有连续的可用的内存一个连续完整的地址空间。而实际上它通常是被分隔成多个物理内存碎片还有部分暂时存储在外部磁盘存储器上在需要时进行数据交换。

2.进程访问物理空间的方式更为安全页表拦截非法请求

1.
进程在和外部设备IO的过程所占内存大小基本为4KB4096字节的大小也就是4096个连续的内存物理地址这部分区域被称之为页page。

2.
进程无法直接接触物理内存只能通过虚拟地址依靠页表映射物理地址的方式来间接访问物理内存。

在这里插入图片描述

3.如果让进程直接访问物理内存可能出现恶意进程修改物理内存的事件发生并且如果进程越界访问这也可能修改其他的进程对其他进程进行非法操作这是非常不安全的

4.
因为虚拟地址是包含所有的地址的也就是4GB的空间虽然是虚拟的但是进程可以使用呀所以如果进程在虚拟地址中访问了某个本不该属于当前进程的地址接下来在通过页表映射到物理地址的这个阶段中页表就会拦截进程非法访问地址的请求

5.
正是因为页表的存在致使所有的进程访问的虚拟地址都会映射到合法的物理内存上想象一下如果进程直接访问的是物理内存那只要进程中出现野指针或越界访问的问题操作系统很有可能直接就挂了计算机也就跟着崩溃了所以这时候虚拟地址空间和页表也就随之而生了

3.进程之间代码和数据能够解耦保证进程独立性的特征写时拷贝技术

下面的讲解可以更加具体的解惑在文章开头时代码运行结果引出的虚拟地址问题

1.
当进程的任何一方尝试写入某些数据时操作系统会先进行数据的拷贝然后更改页表映射最后再让尝试写入的进程对数据进行修改操作系统这样的技术被称之为写时拷贝。

2.
所以操作系统为了保证进程的独立性通过虚拟地址空间再到页表让不同的进程使用的同一虚拟地址能够映射到不同的物理内存处

在这里插入图片描述

3.
每个进程都有自己独立的内核数据结构也就是独立的PCB独立的虚拟地址空间独立的页表操作系统通过写时拷贝这种技术使得进程之间的数据也达到独立。

4.
进程=内核数据结构+进程对应的代码和数据现在内核数据结构是独立的进程对应的代码和数据起始也是独立的如果数据不被写入进程之间也只是以只读的方式共享数据代码由于不会被修改所以父子进程代码也是以只读的方式共享如果是两个毫不相干的进程那他们的代码其实也是独立的所以进程对应的代码和数据可以看成是独立的当然进程也就完完全全是独立的啦

多提一嘴代码是代码英文数据是数据数字他们组成程序。

5.
写时拷贝是一种可以推迟甚至免除拷贝数据的技术。 内核此时并不复制整个进程地址空间而是让父进程和子进程共享同一个拷贝。 只有在需要写入的时候数据才会被复制从而使各个进程拥有各自的拷贝。 也就是说 资源的复制只有在需要写入的时候才进行 在此之前只是以只读方式共享。 这种技术使地址空间上的页的拷贝被推迟到实际发生写入的时候。

4.使编译器编译后的程序 && 内存中的进程 ==> 均以统一的视角看待程序对应的代码和数据程序和进程两份虚拟地址空间分别在磁盘和内存中进程的虚拟地址空间源于程序的

1.
磁盘上的可执行程序在没有加载到内存的时候其实就有地址了例如各个函数在汇编下的调用跳转都是通过地址来实现的所以不要认为只有操作系统会遵守虚拟地址空间的规则编译器也是要遵守这样的规则的在编译器编译代码的时候就是按照虚拟地址空间的方式来对代码和数据进行编址的

2.
只不过磁盘上可执行程序的虚拟地址空间当中没有栈和堆因为这两个段是在运行的时候才产生的编址的时候也是在32位或64位环境下进行编址的并且这些地址也全部都是虚拟的这些地址都是在程序内部也就是在磁盘上

3.
当程序load到内存里面的时候天然的各个函数或者变量就拥有了物理内存地址因为在编译器编译程序的时候在磁盘里编译器就已经按照虚拟地址空间BSS段、Data段、Text段对程序对应的代码和数据进行编址了所以当程序加载到内存的时候操作系统就会对应的分配给程序物理地址这是一个天然的过程。

4.
程序加载到内存的时候其实就创建进程了与之对应的进程描述符task_struct和内存描述符mm_struct这两个数据结构也就被操作系统创建出来了那么mm_struct中的区域的起始和结束地址哪里来呢操作系统不能胡编乱造一堆地址给进程吧你画饼也得画的像个饼啊好歹听上去得不离谱吧要不然就不是画饼了可以称之为扯淡了就。那么操作系统如何填充mm_struct呢

5.
很简单你之前的程序不是已经按照虚拟地址空间编址了吗而且你的程序现在也已经加载到内存了操作系统自然的就给程序分配了物理地址所以操作系统现在知道程序的虚拟地址和物理地址了并且操作系统也知道程序的Text段、BSS段、Data段的内容所以此时操作系统直接就把程序的虚拟地址搬到mm_struct里面mm_struct中无论是区域的起始和结束地址还是区域里面程序对应的代码和数据都被操作系统填充好了至于栈和堆空间的开辟在CPU读取mm_struct的代码段的时候也就是在进程运行期间由操作系统分配相应的空间就好了。

6.
所以操作系统在程序加载到内存后他首先肯定知道程序的虚拟地址其次他又自然的给程序分配了物理地址在填充mm_struct时用的就是程序的虚拟地址直接拷贝过去就完事了。

在这里插入图片描述

7.
CPU读取的永远都是虚拟地址它压根就无法接触和见到物理地址。
例如平常在debug版本下调试程序的时候程序就是运行起来的此时CPU内部的寄存器读到的就是虚拟地址虚拟地址会通过页表映射从而找到内存上真正的物理地址通过这个真实的物理地址找到函数或者变量的实体从而完成进程的运行

8.
在Visual Studio2022或其他版本编译器中编译程序有32位或者64位的选项那是在干什么呢那其实就是在进行虚拟地址空间编址的选择呢你是选择64比特位的方式进行虚拟地址空间编址呢还是选择32位虚拟地址空间编址一个是4GB一个是16GB但也没啥用因为都是虚拟的仅仅只是空间范围而已编译器和进程一样也被画饼所以CPU在读取加载到内存上的程序的时候读取的就是程序的虚拟地址背后通过操作系统在不断的切换虚拟地址和物理地址以便使得CPU读取的地址始终是虚拟地址

9.
在磁盘上的地址其实有一种更为普遍官方化的叫法叫做逻辑地址
线性地址、虚拟地址、逻辑地址在理解层面其实是一样的但是在不同的场景下他们拥有了不同的名字比如你在学校叫张三公司里叫小张家里叫张张你其实就一个名字但在不同的场景下你有了不同的叫法。

10.
磁盘上程序的编址方式是按照线性编址的由于是虚拟的所以程序认为从0x00000000到0xFFFFFFFF的地址程序他自己都可以使用这样的编址方式是比较新的称之为虚拟地址你也可以将他叫做逻辑地址。
而另一种编址方式不是按照线性编制的方式进行的而是按照起始地址加偏移量的方式进行编址这些地址是实实在在的并不是虚拟的所以它被称之为逻辑地址人家是实实在在的逻辑地址。当然这种编址方式是比较老的上面线性编址的方式是比较新的。

我现在这个水平讲不明白老的那种编址方式等到我变成大佬的时候会详细给大家进行讲解的到时候在重新写一篇博客。

下面放了一张逻辑地址和虚拟地址的区别能看懂的伙伴就看一下吧看不懂的就按照我上面说的简易的方式理解就行。
在这里插入图片描述

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
标签: linux

“【Linux】虚拟地址空间 --- 虚拟地址、空间布局、内存描述符、写时拷贝、页表…” 的相关文章