对于每一个进程都会对应一个虚拟地址空间,对于32位的操作系统(其指令的位数最大为32位,因此地址码最多32位),虚拟地址空间的大小为2^{32}B即0~4GB的虚拟地址空间,其中内核空间为1GB,64位操作系统同理。
由于每个进程都不能直接访问内核空间,而是通过系统调用间接进入内核,因此,所有的进程都共享内核空间。而每个进程都拥有各自的用户空间,各个进程之间不能相互访问彼此的用户空间。因此,对每一个具体的进程而言,都拥有4GB的虚拟地址空间。
一个程序在经过编译、链接之后形成的地址空间是一个虚拟的地址空间,只有当程序运行的时候才会分配具体的物理空间。由此我们可以得知,程序的虚拟地址相对来时候是固定的,而物理地址则随着每一次程序的运行而有所不同。
对于内核空间而言,它与物理内存之间存在一个简单的线性关系,即存在3GB的偏移量。在Linux内核中,这个偏移量叫做PAGE_OFFSET。如果内核的某个物理地址为x,那么对应的内核虚拟地址就为x+PAGE_OFFSET。
对于用户空间而言,它与物理内存之间的映射远不止这么简单。与内核空间和物理空间的线性映射不同的是,分页机制将虚拟用户空间和物理地址空间分成大小相同的页,然后再通过页表将虚拟页和物理页块映射起来。
程序使用的地址空间要是连续的。
虚拟地址空间
操作系统给进程描述的一个虚假的地址空间,通过struct mm_struct结构体描述的虚拟地址空间。
使用虚拟地址空间的原因
若进程直接使用物理内存,会造成大量的内存碎片,导致资源利用率较低,并且缺乏访问的安全性。
注意:同一变量地址相同,其实是虚拟地址相同,内容不同是被映射到不同的物理地址上了。
虚拟地址空间的作用
1.方面编译器和操作系统安排程序的地址;
2.方便实现各个进程空间之间的隔离,互不干扰,因为每个进程都对应自己的虚拟地址空间;
3.实现虚拟存储,从逻辑上扩大了内存。
管理虚拟地址与物理地址的方法
分页&虚拟地址空间
虚拟地址空间映射到物理内存上的过程
首先我们使用一个虚拟地址向进程提供一个连续的完整的地址使用,通过页表映射到物理内存的各个区域,并且这些物理区域可以不用连续,实现了数据在物理内存上的离散式存储,提高了内存的利用率,并且虚拟地址空间可以对地址访问进行控制,提高访问安全性。
1 保留区(受保护的地址)
保留区即为受保护的地址,大小为128M,位于虚拟地址空间的最低部分,未赋予物理地址(不会与内存地址相对应,因此其不会放任何内容)。任何对它的引用都是非法的,用于捕捉使用空指针和小整型值指针引用内存的异常情况。大多数操作系统中,极小的地址通常都是不允许访问的,如NULL。C语言将无效指针赋值为0也是出于这种考虑,因为0地址上正常情况下不会存放有效的可访问数据。将指针赋值为0,意味着该指针将永远不会被使用,从而不会出现野指针情况。#define NULL 0 与 #define NULL (void*)0 在C语言中是等效的,而在C++中,只能用#define NULL 0,后面 #define NULL (void*)0的使用会出错。
2 代码段
代码段也称正文段或文本段,通常用于存放程序执行代码(即CPU执行的机器指令)。一般C语言执行语句都编译成机器代码保存在代码段。通常代码段是可共享的,因此频繁执行的程序只需要在内存中拥有一份拷贝即可。代码段通常属于只读,以防止其他程序意外地修改其指令(对该段的写操作将导致段错误)。某些架构也允许代码段为可写,即允许修改程序。
3 数据段(.data段)
数据段通常用于存放程序中已初始化的全局变量和静态局部变量。数据段属于静态内存分配(静态存储区),可读可写。由于全局变量未初始化时,其默认值为0,因此值为0的全局变量位于.bbs段(不位于数据段)。对于未初始化的局部变量,其值是不可预测的。注意:在代码段和数据段之间还包括其它段:只读数据段和符号段等。
4 .bbs段
该段用于存放未初始化的全局变量和静态局部变量,包括值为0的全局变量。 数据段和.bbs段又称为全局数据区,前者初始化,后者未初始化。
ELF段包括:代码段、其它段(只读数据段和符号段等)、.data段(数据段)和.bbs段,都属于可执行程序部分。
5 堆空间
new( )和malloc( )函数分配的空间就属于对空间,用于内存空间的分配,其从下往上。 堆用于存放进程运行时动态分配的内存段,可动态扩张或缩减。堆中内容是匿名的,不能按名字直接访问,只能通过指针间接访问。当进程调用malloc© 和new (C++)等函数分配内存时,新分配的内存动态添加到堆上(扩张);当调用free©/delete(C++)等函数释放内存时,被释放的内存从堆中剔除(缩减) 。
6 内存映射段(共享库)
此处,内核将硬盘文件的内容直接映射到内存, 任何应用程序都可通过Linux的mmap()系统调用请求这种映射。内存映射是一种方便高效的文件I/O方式, 因而被用于装载动态共享库。如C标准库函数(fread、fwrite、fopen等)和Linux系统I/O函数,它们都是动态库函数,其中C标准库函数都被封装在了/lib/libc.so库文件中,都是二进制文件。这些动态库函数都是与位置无关的代码,即每次被加载进入内存映射区时的位置都是不一样的,因此使用的是其本身的逻辑地址,经过变换成线性地址(虚拟地址),然后再映射到内存。而静态库不一样,由于静态库被链接到可执行文件中,因此其位于代码段,每次在地址空间中的位置都是固定的。
7 栈空间
用于存放局部变量(非静态局部变量,C语言称为自动变量),分配存储空间时从上往下。栈和堆都是后进先出的数据结构。
8 命令行参数
该段用于存放命令行参数的内容:argc和argv。
9 环境变量
用于存放当前的环境变量,在Linux中用env命令可以查看其值。
代码段(.text段)与只读数据段和符号段(.rodata段),都属于只读部分,在链接的时候这两部分会链接成为一个整体;而.data段和.bbs段属于可读可写RW的部分。这四个部分都是以页(每页4KB)的形式存放在内存中。
进程控制块PCB(又叫进程描述符)放于内核空间。
多个进程在并发执行时,这些进程的用户空间都是彼此独立的,因此各个进程的用户空间在映射为内存空间使都是独立的,互不干扰,这是MMU地址变换必须要能够保证的。例如,各个进程的.text段、只读数据段和符号段、.data段和.bbs段等在用户空间中使用到的其它数据信息,都会与页为基本单位放在内存中,各个进程的映射是独立的。
而对于
内核空间
,由于只有一个操作系统,内核空间主要是 机器指令、操作系统内核的各个模块等,它们是公用的,因此每个进程的映射方式一样。
**ps:**每个进程用到或即将用到的数据才会调入内存,其余都在磁盘上。但是各个进程内核空间的进程控制块(进程描述符)映射的地点是不一样的,也是相互独立的。共用的模块才是一样的。 这些都是MMU的实现机制所决定的。
内核使用mm_struct来描述一个进程的虚拟地址空间:
struct mm_struct{
struct vm_area_struct* mmap; //指向虚拟区域(VMA)链表
rb_root_t mm_rb; //指向red_black树
struct vm_area_struct* mmap_cache; //指向最近找到的虚拟区间
pgd_t* pgd; //指向进程的页目录
atomic_t mm_users; //用户空间中的有多少用户
atomic_t mm_count; //对“struct mm_struct"有多少引用
int map_count; //虚拟区间的个数
struct list_head mmlist; //所有活动(active)mm的链表
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;
};
struct vm_area_struct:用来描述一个虚拟内存区域(VMA)。内核将每个内存区域作为一个单独的内存对象管理,每个内存对象管理,每个内存区域都有一致的属性,比如权限等。
我们程序的代码段、数据段和bss段在内核里都分别有一个struct vm_area_struct结构体来描述:
struct vm_area_struct{
struct mm_struct* vm_mm; //虚拟内存区域所在的地址空间
unsigned long vm_start; //在虚拟内存中的起始地址
unsigned long vm_end; //在虚拟内存中的结束地址
struct vm_area_struct* vm_next;
pgprot_t vm_page_prot; //对这个虚拟内存区域的存取权限
unsigned long vm_flags; //虚拟内存区域的标志
rb_node_t vm_rb; //对这个虚拟内存区域进行操作的函数
struct vm_operations_struct* vm_ops;
struct file* vm_file;
};