1.1 远古时代
单道编程:整个系统只有一个用户进程和一个操作系统。
用户程序总是加载到同一个内存地址上运行,不需要地址保护。缺点有三
- 无法运行比实际物理内存大的程序
- 系统只运行一个程序,造成资源浪费
- 无法迁移到其他计算机
多道编程:系统同时运行多个进程。内存管理中出现了,固定分区和动态分区
- 固定分区:在系统编译阶段内存被划分成许多静态区,进程可以装入大于或者等于自身大小的分区。实现简单,操作系统管理开销较小,缺点也很明显:
- 程序大小和分区大小必须匹配
- 活动进程的数目固定
- 地址空间无法增长。
- 动态分区:一整块内存中首先划出一块内存给操作系统本身使用,剩下的内存空间给用户进程使用。进程A运行时先从这一大片内存中切割一块与进程A大小一样的内存给进程A使用。B进程准备运行时,从剩余内存中继续切割与B大小相等的内存块给进程B,一次类推。缺点是:
- 内存碎片
- 为了接菌碎片化的问题,操作系统需要动态的移动进程,使得进程占用的空间都是连续的,并且所有的空间进程也是连续的,整个进程的迁移是一个非常耗时的过程。
不管是动态还是静态,都存在很多问题:
- 进程地址空间保护问题。所有的用户进程都可以访问全部的物理内存,所以恶意程序可以修改其他程序的内存数据。
- 内存使用效率低,如果即将要运行的进程需要的内存空间不足,就需要选择一个进程整体换出,这种机制大致大量的数据需要换出和换入,效率非常底下。
- 程序运行地址重定位问题。
1.2分段机制
把程序所需要的内存空间的虚拟地址映射到某个物理地址空间。
分段机制可以解决地址空间保护问题,进程AB映射到不同的物理地址空间中,他们在物理地址空间是不会有重叠的。
如果一个进程访问了没有映射的虚拟地址空间,或者访问了不属于该进程的虚拟地址空间,cpu会捕捉到这个越界访问,并且拒绝该次访问,cpu会发送一个异常错误给操作系统,由搞作系统去处理这些异常情况。这就是常说的缺页异常
分段机制解决问题的思路:增加一个虚拟内存,进程运行时看到的是虚拟地址,然后需要通过cpu提供的地址映射方法,把虚拟地址转换成实际的物理地址,这样多个进程同时运行时,就可以保证每个进程的虚拟空间是相互隔离的,操作系统只需要维护虚拟地址到物理地址的映射关系即可。
以上:分段机制解决了地址保护问题,但是内存使用效率依然低:
分段机制对虚拟内存到华丽内存的映射以进程为单位,物理内存不足时,换出到磁盘的依然是整个进程,会导致大量的磁盘访问。另外,站在进程的角度看,进程运行时,根据局部性原理,只有一部分数据是一直使用的,若把那些不常用的数据交换出磁盘,就可以节省很带宽,而那些常用的数据驻留在物理内存中也可以得到比较好的性能。
1.3 分页机制
分页机制把进程地址空间分成固定大小的页,进程的虚拟地址空间也按照页来分割,这样常用的页驻留在内存中,不常用的页可以交换到磁盘中,从而节省物理内存。物理页又叫页帧,编号叫页帧号 Page Frame Number PFN。虚拟页:Virtural Page
常用的操作系统默认支持的页面大小是4KB,页表:操作系统通过维护一张表,这张表上记录了每一对页(虚拟页)和框(物理页面)的映射关系。
cpu内部有个专门的硬件单位负责虚拟页面到物理页面的转换,内存管理单元,Memory Managerment Unit MMU,ARM的MMU包括TLB和Table Walk Unit两个部分
- TLB 是一块告诉缓存,用于缓存页表转换的结果,以减少内存访问时间
- 一个完整的页面查询Translation Table Walk,页表查询过程有硬件自动完成,页表的维护需要软件完成,页表查询是一个相对耗时的过程,理想的状态是TLB里有缓存页表转换的相关信息,当TLB未命中时,才会去查询页表,并且读入页表的内容。
1.4 虚拟地址到物理地址的转换。
1. 概念
内核除了管理本身内存外,还必须管理用户空间中进程的内存,这个内存叫进程地址空间。也就是系统中每个用户空间进程所看到的内存。
进程地址空间由进程可寻址的虚拟内存组成。在32位操作系统中,进程的地址空间可以寻址4GB的虚拟内存,这并不代表进程有权访问所有的虚拟地址——Linux将虚拟地址空间分为内核空间和用户空间,03G为用户空间,3G4G是内核空间,只有用户进程进行系统调用(代表用户进程在内核态执行)等时刻可以访问内核空间,而用户空间的进程地址空间则可以被合法的访问,
进程只能访问有效内存区域的内存地址,每个内存区域也具有相关的权限如可读,可写,可执行属性。如果一个进程访问了不在有效范围内的内存区域,或者以不正确的方式访问了内存区域,那么处理器会报告缺页异常。可在Linux内核的缺页异常中处理这些情况,严重的会报告"Segment Fault"段错误终止程序。
内存区域包含集中不同的数据区:
- text段:可执行文件代码的内存映射,只读。
- data段:可执行文件中已初始化全局变量的内存映射,换句话说就是存放程序静态分配的变量和全局变量
- BSS段:包含了程序中未初始化的全局变量,也就是 bss段的零页的内存映射(页面中的信息全部为零);
- heap段:堆是用于存放进程运行中被动态分配的内存段,它的大小不固定,可动态扩张或缩减。当进程调用malloc等函数分配内存时,新分配的内存就被动态添加到堆上(堆被扩张);当利用free等函数释放内存时,被释放的内存从堆中被剔除(堆被缩减);
- stack段:通常是在用户空间的最高地址,从上往下延申,包含了局部变量和函数调用参数。(注意和内核栈区分,进程的内核栈独立存在并由内核维护,主要用于上下文切换)
- mmap段:位于用户进程栈下面,将硬盘文件的内容直接映射到内存,主要用于mmap系统调用,比如映射一个文件的内容到进程地址空间。(实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系。实现这样的映射关系后,进程就可以采用指针的方式读写操作这一段内存,而系统会自动回写脏页面到对应的文件磁盘上,即完成了对文件的操作而不必再调用read,write等系统调用函数。)。
如果两个进程都使用maclloc()函数来分配内存,并且分配的虚拟内存地址都是一样的,是不是说明这两个内存区域重叠??
如果理解进程地址空间的本质,就不难回答这个问题,进程地址空间是每个进程可以寻址的虚拟地址空间,每个进程在运行时都仿佛拥有了整个cpu资源,这就是所谓的"cpu虚拟化",每个进程都有一套页表,这样每个进程地址空间就是相互隔离的,即使进程地址空间的虚拟地址相同,经过两套不同页表的转换之后,会对应不同的物理地址。
2. 内存描述符 mm_struct
内核需要管理每个进程所有的内存区域,以及他们所对应的页表映射,所以必须抽象出要给数据结构,这就是mm_struct数据结构。进程的进程控制块(PCB)数据结构 task_struct中有一个指针指向整个mm_struct内存结构。
struct mm_struct {
//内核管理进程地址空间使用的数据结构是struct vm_area_struct,简称VMA,
//一个进程所有VMA都链接到一个单链表,mmap是这个链表的头
struct vm_area_struct * mmap; /* list of VMAs */
//每个进程都有一个VMA红黑树,VMA红黑树根节点
struct rb_root mm_rb;
//判断虚拟内存是否有足够的空间
unsigned long (*get_unmapped_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);
//执行mmap区域的起始地址
unsigned long mmap_base; /* base of mmap area */
unsigned long task_size; /* size of task vm space */
unsigned long cached_hole_size; /* if non-zero, the largest hole below free_area_cache */
unsigned long free_area_cache; /* first hole of size cached_hole_size or larger */
//指向进程的页面目录PGD Page Global Directory
pgd_t * pgd;
//空间中有多少用户,如果两个线程共享该进程地址空间,值为2
atomic_t mm_users; /* How many users with user space? */
//引用计数;描述有多少指针指向当前的mm_struct
atomic_t mm_count; /* How many references to "struct mm_struct" (users count as 1) */
//虚拟区间的个数
int map_count; /* number of VMAs */
//用来保护进程地址空间VMA的一个读写信号量,防止并发访问
struct rw_semaphore mmap_sem;
spinlock_t page_table_lock; /* Protects page tables and some counters */
//所有的mm_struct数据结构都会链接到一个双向链表,该双向链表头是init_mm的内存描述符,也就是init进程的地址空间
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;
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;
//start_code:代码段的起始地址
//end_code:代码段的结束地址
//start_data:数据段起始地址
//end_data:数据段结束地址
unsigned long start_code, end_code, start_data, end_data;
//start_brk:堆的起始地址
//brk:堆的结束地址
//start_stack:栈的起始地址
unsigned long start_brk, brk, start_stack;
//arg_start,arg_end:参数段的起始和结束地址
//env_start,env_end:环境段的起始和结束地址
unsigned long arg_start, arg_end, env_start, env_end;
..............................................................
};
从进程的角度观察内存管理:
mmap和mm_rb 两个不同的数据结构描述的对象是相同的,地址空间中的全部内存区域,mmap以链表形式存放,mm_rb以红黑树的形式存放。
内核为什么使用两种数据结构组织同一种数据:
此处内核这样的冗余确实派的上用场:
- mmap结构体作为链表,利于简单高效的遍历所有元素
- mm_rb作为红黑树,更适合搜索指定的元素(搜索的时间复杂度 O(log n))
2.1 分配内存描述符
在进程描述符mm_struct结构体中,mm域存放着该进程使用的内存描述符,所以current->mm便指向当前进程的内存描述符,(内核提供了current宏,可以方便的找到当前正在运行的进程的task_struct数据结构)。fork()函数利用copy_mm()复制父进程的内存描述符,也就是current->mm域给其子进程。而子进程中的mm