虚拟内存,是相对于物理内存而言的。虽然也冠以"内存",但它并不是一段存储空间,仅仅是对物理内存的一个抽象,是为了更加有效地管理物理内存而发明出来的一个玩意。
那么,如果没有虚拟内存,会有哪些问题呢?在讨论问题之前要清楚两点:
物理内存基于成本的考虑,一般不会太大
系统可同时运行多个进程
基于以上两点,如果没有虚拟内存,对物理内存直接操作,易产生以下问题:
由于程序是运行在内存中的,当运行的进程过多,会导致一些程序无法正常运行;
由于直接操作物理内存,a 进程可能会写入 b 进程正在用的内存空间,缺乏保护机制;
还有一点,如果在程序中直接操作物理内存,对开发人员也不太友好。
为了解决以上问题,从而提出了虚拟内存机制,作为内存管理的工具,为每个进程提供独立的虚拟地址空间。
进程虚拟内存很多地方都见过,从低地址到高地址分别是代码段、数据段、堆、共享库以及栈等。在进程虚拟内存上面是内核虚拟内存,包括内核中的代码和和数据结构、其他区域则包含每个进程都不相同的数据,如页表、内核在进程的上下文中执行代码时使用的栈,以及记录虚拟地址空间当前组织的各种数据结构等。
对于进程虚拟内存,代码段、数据段、堆、共享库段,以及用户栈也可都称为区域,只是功能不同而已。区域内部是一段连续的存储空间,但区域之间不一定连续存储,可能存在空隙。那么进程的各个区域是怎么联系起来的呢?先看下区域在内核中是如何定义的。
/*
* This struct defines a memory VMM memory area. There is one of these per VM-area/task.
* A VM area is any part of the process virtual memory space that has a special rule for the page-fault handlers.
*/
struct vm_area_struct {
/* The first cache line has the info for VMA tree walking. */
unsigned long vm_start; /* Our start address within vm_mm. */
unsigned long vm_end; /* The first byte after our end address within vm_mm. */
/* linked list of VM areas per task, sorted by address */
struct vm_area_struct *vm_next, *vm_prev;
...
struct mm_struct *vm_mm; /* The address space we belong to. */
pgprot_t vm_page_prot; /* Access permissions of this VMA. */
unsigned long vm_flags; /* Flags, see mm.h. */
...
};
可以看到区域在内核中被定义成 vm_area_struct 结构体,成员指针 *vm_next 指向下一个区域,*vm_prev 指向上一个区域,各区域之间连接成一个双向链表。vm_start 和 vm_end 标记该区域的首地址和尾地址,并且可以通过指针 *vm_mm 指向所属的地址空间。而进程也就是通过这个地址空间与各个区域连接起来的。
struct task_struct {
volatile long state; /* -1 unrunnable, 0 runnable, >0 stopped */
...
struct mm_struct *mm, *active_mm;
...
}
struct mm_struct {
struct vm_area_struct * mmap; /* list of VMAs */
...
unsigned long highest_vm_end; /* highest vma end address */
pgd_t * pgd;
atomic_t mm_users; /* How many users with user space? */
atomic_t mm_count; /* How many references to "struct mm_struct" (users count as 1) */
atomic_long_t nr_ptes; /* Page table pages */
int map_count; /* number of VMAs */
...
}
内核为系统中的每个进程都维护了一个任务结构体 task_struct,该结构体记录了当前进程的pid、状态,以及指向虚拟内存的指针等等。虚拟内存被定义成 mm_struct 结构体,第一个成员便是指向各个区域组成的双向链表的指针 *mmap。大概如下图所示:
回到开始的问题,从上述的各结构体定义可以看到,每个进程都有属于自己的一段地址空间,进程之间互不干涉,从而避免了 a 进程写入 b 进程地址空间的可能;开发者也只需调用 malloc 等方法即可申请内存空间,无需考虑如何与物理内存交互;如果运行进程过多,一定范围内可使用页面置换算法,将不常用的页面置换出去,从而腾出空间。