【操作系统】对内存的管理

操作系统对内存的管理

没有内存抽象的年代

在早些的操作系统中,并没有引入内存抽象的概念。程序直接访问和操作的都是物理内存。比如当执行如下指令时:mov reg1,1000

这条指令会毫无想象力的将物理地址1000中的内容赋值给寄存器。不难想象,这种内存操作方式使得操作系统中存在多进程变得完全不可能,比如MS-DOS,你必须执行完一条指令后才能接着执行下一条。如果是多进程的话,由于直接操作物理内存地址,当一个进程给内存地址1000赋值后,另一个进程也同样给内存地址赋值,那么第二个进程对内存的赋值会覆盖第一个进程所赋的值,这回造成两条进程同时崩溃。

没有内存抽象对于内存的管理通常非常简单,除去操作系统所用的内存之外,全部给用户程序使用。或是在内存中多留一片区域给驱动程序使用

无内存抽象存在的问题

  1. 用户程序可以访问任意内存,容易破坏操作系统,造成崩溃

  2. 同时运行多个程序特别困难

内存抽象:地址空间

  1. 基址寄存器与界限寄存器可以简单的动态重定位,每个内存地址送到内存之前,都会自动加上基址寄存器的内容。

  2. 交换技术把一个进程完全调入内存,使该进程运行一段时间,然后把它存回磁盘。空闲进程主要存在磁盘上,所以当他们不运行时就不会占用内存。

为什么要有地址空间?

首先直接把物理地址暴露给进程会带来严重问题

  1. 如果用户程序可以寻址内存的每个字节,就有很大的可能破坏操作系统,造成系统崩溃

  2. 同时运行多个程序十分困难 地址空间创造了一个新的内存抽象,地址空间是一个进程可用于寻址内存的一套地址的集合。每个进程都有一个自己的地址空间,并且这个地址空间独立于其它进程的地址空间。使用基址寄存器和界限器可以实现。

虚拟内存

虚拟内存是现代操作系统普遍使用的一种技术。前面所讲的抽象满足了多进程的要求,但很多情况下,现有内存无法满足仅仅一个大进程的内存要求(比如很多游戏,都是10G+的级别)。在早期的操作系统曾使用覆盖(overlays)来解决这个问题,将一个程序分为多个块,基本思想是先将块0加入内存,块0执行完后,将块1加入内存。依次往复,这个解决方案最大的问题是需要程序员去程序进行分块,这是一个费时费力让人痛苦不堪的过程。后来这个解决方案的修正版就是虚拟内存。

虚拟内存的基本思想是,每个进程有用独立的逻辑地址空间,内存被分为大小相等的多个块,称为(Page).每个页都是一段连续的地址。对于进程来看,逻辑上貌似有很多内存空间,其中一部分对应物理内存上的一块(称为页框,通常页和页框大小相等),还有一些没加载在内存中的对应在硬盘上。

由上图可以看出,虚拟内存实际上可以比物理内存大。当访问虚拟内存时,会通过MMU(内存管理单元)去匹配对应的物理地址,而如果虚拟内存的页并不存在于物理内存中,会产生缺页中断,从磁盘中取得缺的页放入内存,如果内存已满,还会根据某种算法将磁盘中的页换出。

虚拟内存和物理内存的匹配是通过页表实现,页表存在MMU中,页表中每个项通常为32位,既4byte,除了存储虚拟地址和页框地址之外,还会存储一些标志位,比如是否缺页,是否修改过,写保护等。可以把MMU想象成一个接收虚拟地址项返回物理地址的方法。

因为页表中每个条目是4字节,现在的32位操作系统虚拟地址空间会是2的32次方,即使每页分为4K,也需要2的20次方 * 4字节 = 4M的空间,为每个进程建立一个4M的页表并不明智。因此在页表的概念上进行推广,产生二级页表,二级页表每个对应4M的虚拟地址,而一级页表去索引这些二级页表,因此32位的系统需要1024个二级页表,虽然页表条目没有减少,但内存中可以仅仅存放需要使用的二级页表和一级页表,大大减少了内存的使用。

引入多级页表的原因是避免把全部页表一直存在内存中

虚拟地址和物理地址匹配规则

虚拟页号可用做页表的索引,以找到该虚拟页面对应页表项。由页表项可以找到页框号。然后把页框号拼接到偏移量的高位端,以替换虚拟页号,形成送往内存的物理地址。

页表的目的是把虚拟页面映射为页框,从数学的角度来说,页表是一个函数,它的参数是,虚拟页号,结果是物理页框号。通过这个函数可以把虚拟地址中的虚拟页面域替换为页框域,从而形成物理地址。

页面置换算法

地址映射过程中,若在页面中发现所要访问的页面不再内存中,则产生缺页中断。当发生缺页中断时操作系统必须在内存选择一个页面将其移出内存,以便为即将调入的页面让出空间。如果要换出的页面在内存驻留期间已经被修改过,就必须把它写回磁盘以更新该页面在磁盘的副本;如果该页面没有被修改过,那么它在磁盘上的副本已经是最新的,不需要回写。直接用调入的页面覆盖掉被淘汰的页面就可以了。而用来选择淘汰哪一页的规则叫做页面置换算法。

因为在计算机系统中,读取少量数据硬盘通常需要几毫秒,而内存中仅仅需要几纳秒。一条CPU指令也通常是几纳秒,如果在执行CPU指令时,产生几次缺页中断,那性能可想而知,因此尽量减少从硬盘的读取无疑是大大的提升了性能。而前面知道,物理内存是极其有限的,当虚拟内存所求的页不在物理内存中时,将需要将物理内存中的页替换出去,选择哪些页替换出去就显得尤为重要,如果算法不好将未来需要使用的页替换出去,则以后使用时还需要替换进来,这无疑是降低效率的,让我们来看几种页面替换算法。

最优页面置换算法(Optimal Page Replacement Algorithm)

最饥饿页面置换算法是将未来最久不使用的页替换出去,这听起来很简单,但是无法实现。根据页面被访问前所需要的指令数作为标记,根据指令数的由多到少进行置换,这个方法对评价页面置换算法很有用,但它在实际系统中却不能使用,因为无法真正的实现。这种算法可以作为衡量其它算法的基准。

最近最少使用页面置换算法(Least Recently Used)

通常在前几条指令中使用频繁的页面很可能在后面几条指令中页频繁使用。LRU算法就是在缺页发生时首先置换最长时间未被使用的页面。优秀但是难以实现。

最近未使用页面置换算法(Not Recently Used Replacement Algorithm)

在最近的一个时钟周期内,淘汰一个没有被访问的已修改页面,近似 LRU 算法,NRU 只是更粗略些。

这种算法给每个页一个标志位,R表示最近被访问过,M表示被修改过。定期对R进行清零。这个算法的思路是首先淘汰那些未被访问过R=0的页,其次是被访问过R=1,未被修改过M=0的页,最后是R=1,M=1的页。

先进先出的页面置换算法(First-In First-Out Page Replacement Algorithm)

这种算法的思想是淘汰在内存中最久的页,这种算法的性能接近于随机淘汰。可能抛弃重要的页面,并不好。

第二次机会页面置换算法(Second Chance Page Replacement Algorithm)

这种算法是在FIFO的基础上,为了避免置换出经常使用的页,增加一个标志位R,如果最近使用过将R置1,当页将会淘汰时,如果R为1,则不淘汰页,将R置0.而那些R=0的页将被淘汰时,直接淘汰。这种算法避免了经常被使用的页被淘汰。

时钟替换算法(Clock Page Replacement Algorithm)

虽然改进型FIFO算法避免置换出常用的页,但由于需要经常移动页,效率并不高。因此在改进型FIFO算法的基础上,将队列首位相连形成一个环路,当缺页中断产生时,从当前位置开始找R=0的页,而所经过的R=1的页被置0,并不需要移动页。

下表是上面几种算法的简单比较:

算法描述
最佳置换算法无法实现,最为测试基准使用
最近不常使用算法和LRU性能差不多
先进先出算法有可能会置换出经常使用的页
改进型先进先出算法和先进先出相比有很大提升
最久未使用算法性能非常好,但实现起来比较困难
时钟置换算法非常实用的算法

分页系统中的设计问题

  1. 在任何分页式系统中,都需要考虑两个主要的问题:虚拟地址到到物理地址的映射必须非常快;如果虚拟地址空间很大,页表也会很大。

  2. 局部分分配策略与全局分配策略,怎样在相互竞争的可运行进程之间分配内存.

    • 局部分配为每个进程分配固定的内存片段,即使有大量的空闲页框存在,工作集的增长也会颠簸。

    • 全局分配在进程间动态地分配页框,分配给各个进程的页框数是动态变化的。给每个进程分配一个最小的页框数使无论多么小的进程都可以运行,再需要更大的内存时去公共的内存池里去取。

    • FIFO LRU 既适用于局部算法,也适用于全局算法。WSClock 工作集更适用于局部算法。

  3. 负载控制 , 即使使用了最优的页面置换算法,最理想的全局分配。当进程组合的工作集超出内存容量时,就可能发生颠簸。这时只能根据进程的特性(IO 密集 or CPU 密集)将进程交换到磁盘上。

  4. 页面大小 的确定不存在全局最优的结果,小页面减少页面内内存浪费,但是小页面,意味着更大的页表,更多的计算转换时间。现在一般的页面大小是 4KB 或 8KB

  5. 地址空间太小,所以分离指令空间,数据空间

  6. 共享页面,在数据空间和指令空间分离的基础上很容易实现程序的共享,linux 采取了 copy on write 的方案,也有数据的共享。

  7. 共享库,多个程序并发,如果有一个程序已经装在了共享库,其他程序就没有必要再进行装载,减少内存浪费。而且共享库并不会一次性的装入内存,而是根据需要以页面为单位进行装载的。共享时不使用绝对地址,使用相对偏移量的代码(位置无关代码 position-independent code)

  8. 共享库是内存映射文件的一种特例,核心思想是进程可以发起一个系统调用,将一个文件映射到其虚拟地址空间的一部分,在多数实现中在映射共享的页面时不会实际读入页面的内容,而是在访问时被每次一页的读入,磁盘文件被当作后背存储。当进程退出或显示的接触文件时,所有被改动的页面会被写入到文件中。

  9. 清除策略,发生缺页中断时有大量的空闲页框,此时的分页系统在最佳状态,有一个分页守护(paging daemon)的后台进程,它在多数时候睡眠,但会被定期唤醒,如果空闲页框过少,分页守护进程通过预定的页面置换算法选择页面换出内存。

  10. 虚拟内存接口,在一些高级系统中,程序员可以对内存映射进行控制,允许控制的原因是为了允许两个或多个进程共享一部分内存。页面共享可以用来实现高性能的消息传递系统。

Linux内存管理

内核空间

页(page)是内核的内存管理的基本单位

struct page {
 page_flags_t flags;  页标志符
 atomic_t _count;    页引用计数
 atomic_t _mapcount;     页映射计数
 unsigned long private;    私有数据指针
 struct address_space *mapping;    该页所在地址空间描述结构指针,用于内容为文件的页帧
 pgoff_t index;               该页描述结构在地址空间radix树page_tree中的对象索引号即页号
 struct list_head lru;        最近最久未使用struct slab结构指针链表头变量
 void *virtual;               页虚拟地址
};
  • flags:页标志包含是不是脏的,是否被锁定等等,每一位单独表示一种状态,可同时表示出32种不同状态,定义在<linux/page-flags.h>

  • _count:计数值为-1表示未被使用。

  • virtual:页在虚拟内存中的地址,对于不能永久映射到内核空间的内存(比如高端内存),该值为NULL;需要事必须动态映射这些内存。

尽管处理器的最小可寻址单位通常为字或字节,但内存管理单元(MMU,把虚拟地址转换为物理地址的硬件设备)通常以页为单位处理。内核用struct page结构体表示每个物理页,struct page结构体占40个字节,假定系统物理页大小为4KB,对于4GB物理内存,1M个页面,故所有的页面page结构体共占有内存大小为40MB,相对系统4G,这个代价并不高。

内核把页划分在不同的区(zone)

总共3个区,具体如下:

描述物理内存(MB)
ZONE_DMADMA使用的页<16
ZONE_NORMAL可正常寻址的页16 ~896
ZONE_HIGHMEM动态映射的页>896
  • 执行DMA操作的内存必须从ZONE_DMA区分配

  • 一般内存,既可从ZONE_DMA,也可从ZONE_NORMAL分配,但不能同时从两个区分配;

用户空间

用户空间中进程的内存,往往称为进程地址空间

Linux采用虚拟内存技术。进程的内存空间只是虚拟内存(或者叫作逻辑内存),而程序的运行需要的是实实在在的内存,即物理内存(RAM)。在必要时,操作系统会将程序运行中申请的内存(虚拟内存)映射到RAM,让进程能够使用物理内存。

地址空间

每个进程都有一个32位或64位的地址空间,取决于体系结构。 一个进程的地址空间与另一个进程的地址空间即使有相同的内存地址,也彼此互不相干,对于这种共享地址空间的进程称之为线程。一个进程可寻址4GB的虚拟内存(32位地址空间中),但不是所有虚拟地址都有权访问。对于进程可访问的地址空间称为内存区域。每个内存区域都具有对相关进程的可读、可写、可执行属性等相关权限设置。

内存区域可包含的对象:

  • 代码段(text section): 可执行文件代码

  • 数据段(data section): 可执行文件的已初始化全局变量(静态分配的变量和全局变量)。

  • bss段:程序中未初始化的全局变量,零页映射(页面的信息全部为0值)。

  • 进程用户空间栈的零页映射(进程的内核栈独立存在并由内核维护)

  • 每一个诸如C库或动态连接程序等共享库的代码段、数据段和bss也会被载入进程的地址空间

  • 任何内存映射文件

  • 任何共享内存段

  • 任何匿名的内存映射(比如由malloc()分配的内存)

这些内存区域不能相互覆盖,每一个进程都有不同的内存片段。

内存描述符

内存描述符由mm_struct结构体表示

struct mm_struct {
 struct vm_area_struct *mmap;
 rb_root_t mm_rb;
 ...
 atomic_t mm_users;
 atomic_t mm_count;
​
 struct list_head mmlist;
 ...
};
  • mm_users:代表正在使用该地址的进程数目,当该值为0时mm_count也变为0;

  • mm_count: 代表mm_struct的主引用计数,当该值为0说明没有任何指向该mm_struct结构体的引用,结构体会被撤销。

  • mmap和mm_rb:描述的对象都是相同的

    • mmap以链表形式存放, 利于高效地遍历所有元素

    • mm_rb以红黑树形式存放,适合搜索指定元素

  • mmlist:所有的mm_struct结构体都通过mmlist连接在一个双向链表中,该链表的首元素是init_mm内存描述符,它代表init进程的地址空间。

在进程的进程描述符(<linux/sched.h>中定义的task_struct结构体)中,mm域记录该进程使用的内存描述符。故current->mm代表当前进程的内存描述符。

fork()函数 利用copy_mm函数复制父进程的内存描述符,子进程中的mm_struct结构体通过allcote_mm()从高速缓存中分配得到。通常,每个进程都有唯一的mm_struct结构体,即唯一的进程地址空间。

当子进程与父进程是共享地址空间,可调用clone(),那么不再调用allcote_mm(),而是仅仅是将mm域指向父进程的mm,即 tsk->mm = current->mm。

相反地,撤销内存是exit_mm()函数,该函数会进行常规的撤销工作,更新一些统计量。

内核线程

  • 没有进程地址空间,即内核线程对应的进程描述符中mm=NULL

  • 内核线程直接使用前一个进程的内存描述符,仅仅使用地址空间中和内核内存相关的信息

页表

应用程序操作的对象时映射到物理内存之上的虚拟内存,而处理器直接操作的是物理内存。故应用程序访问一个虚拟地址时,需要将虚拟地址转换为物理地址,然后处理器才能解析地址访问请求,这个转换工作通过查询页表完成。

Linux使用三级页表完成地址转换。

  1. 顶级页表:页全局目录(PGD),指向二级页目录;

  2. 二级页表:中间页目录(PMD),指向PTE中的表项;

  3. 最后一级:页表(PTE),指向物理页面。

多数体系结构,搜索页表工作由硬件完成。每个进程都有自己的页表(线程会共享页表)。为了加快搜索,实现了翻译后缓冲器(TLB),作为将虚拟地址映射到物理地址的硬件缓存。还有写时拷贝方式共享页表,当fork()时,父子进程共享页表,只有当子进程或父进程试图修改特定页表项时,内核才创建该页表项的新拷贝,之后父子进程不再共享该页表项。可见,利用共享页表可以消除fork()操作中页表拷贝所带来的消耗。

进程与内存

所有进程都必须占用一定数量的内存,这些内存用来存放从磁盘载入的程序代码,或存放来自用户输入的数据等。内存可以提前静态分配和统一回收,也可以按需动态分配和回收。

对于普通进程对应的内存空间包含5种不同的数据区:

  • 代码段

  • 数据段

  • BSS段

  • 堆:动态分配的内存段,大小不固定,可动态扩张(malloc等函数分配内存),或动态缩减(free等函数释放);

  • 栈:存放临时创建的局部变量;

进程内存空间

Linux采用虚拟内存管理技术,每个进程都有各自独立的进程地址空间(即4G的线性虚拟空间),无法直接访问物理内存。这样起到保护操作系统,并且让用户程序可使用比实际物理内存更大的地址空间。

  • 4G进程地址空间被划分两部分,内核空间和用户空间。用户空间从0到3G,内核空间从3G到4G;

  • 用户进程通常情况只能访问用户空间的虚拟地址,不能访问内核空间虚拟地址。只有用户进程进行系统调用(代表用户进程在内核态执行)等情况可访问到内核空间;

  • 用户空间对应进程,所以当进程切换,用户空间也会跟着变化;

  • 内核空间是由内核负责映射,不会跟着进程变化;内核空间地址有自己对应的页表,用户进程各自有不同额页表。

内存分配

进程分配内存,陷入内核态分别由brk和mmap完成,但这两种分配还没有分配真正的物理内存,真正分配在后面会讲。

  • brk: 数据段的最高地址指针_edata往高地址推

    • 当malloc需要分配的内存<M_MMAP_THRESHOLD(默认128k)时,采用brk;

    • brk分配的内存需高地址内存全部释放之后才会释放。(由于是通过推动指针方式)

    • 当最高地址空间的空闲内存大于M_TRIM_THRESHOLD时(默认128k),执行内存紧缩操作;

  • do_mmap:在堆栈中间的文件映射区域找空闲的虚拟内存

    • 当malloc需要分配的内存>M_MMAP_THRESHOLD(默认128k)时,采用do_map();

    • mmap分配的内存可以单独释放

物理内存

  • 物理内存只有进程真正去访问虚拟地址,发生缺页中断时,才分配实际的物理页面,建立物理内存和虚拟内存的映射关系。

  • 应用程序操作的是虚拟内存;而处理器直接操作的却是物理内存。当应用程序访问虚拟地址,必须将虚拟地址转化为物理地址,处理器才能解析地址访问请求。

  • 物理内存是通过分页机制实现的

  • 物理页在系统中由也结构struct page描述,所有的page都存储在数组mem_map[]中,可通过该数组找到系统中的每一页。

虚拟内存 转化为 真实物理内存:

  • 虚拟进程空间:通过查询进程页表,获取实际物理内存地址;

  • 虚拟内核空间:通过查询内核页表,获取实际物理内存地址;

  • 物理内存映射区:物理内存映射区与实际物理去偏移量仅PAGE_OFFSET,通过通过virt_to_phys()转化;

虚拟内存与真实物理内存映射关系:

其中物理地址空间中除了896M(ZONE_DMA + ZONE_NORMAL)的区域是绝对的物理连续,其他内存都不是物理内存连续。在虚拟内核地址空间中的安全保护区域的指针都是非法的,用于保证指针非法越界类的操作,vm_struct是连续的虚拟内核空间,对应的物理页面可以不连续,地址范围(3G + 896M + 8M) ~ 4G;另外在虚拟用户空间中 vm_area_struct同样也是一块连续的虚拟进程空间,地址空间范围0~3G。

碎片问题

  • 外部碎片:未被分配的内存,由于太多零碎的不连续小内存,无法满足当前较大内存的申请要求;

    • 原因:频繁的分配与回收物理页导致大量的小块内存夹杂在已分配页面中间;

    • 解决方案:伙伴算法有所改善

  • 内部碎片:已经分配的内存,却不能被利用的内存空间;

    • 缘由:所有内存分配必须起始可被4、8或16(体系结构决定)整除的地址或者MMU分页机制限制;

    • 解决方案:slab分配器有所改善

    • 实例:请求一个11Byte的内存块,系统可能会分配12Byte、16Byte等稍大一些的字节,这些多余空间就产生碎片



作者:Mr槑
链接:https://www.jianshu.com/p/2b11639905ec
来源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值