其中介绍了虚拟内存的机制以及 mmap 系统调用的实现 mmap 允许直接将设备内存映射到用户进程的地址空间中物理内存的管理, 包括缓存的分配及回收, 请页机制, 交换空间等
1)交换模块(swap)
这个模块负责控制内存内容的换入换出, 它通过替换机制, 使得物理内存的页框 (RAM 页) 中保留有效的逻辑页, 即从主存中淘汰最近没被访问的逻辑页, 保存近来访问过的逻辑页该模块实现的源程序分别是:
page_io.c 的主要函数功能是读写交换文件
swap_state.c 的主要函数功能是修改交换高速缓存(swap cache)
swapfile.c 的主要函数功能是完成换入换出系统调用(sys_swapinsys_swapon)
swap.c 的主要函数功能是定义交换使用的数据结构和常量, 如 free_page_lowfree_page_high
kswapd 是一个周期性地处理交换内存和外村的守护进程(内核线程)
2)核心内存管理模块
这个模块负责核心内存管理功能, 即对页的管理, 这些功能将被别的内核子系统 (如文件系统) 所使用该模块实现的源程序分别是:
page_alloc.c 主要函数功能是处理页的释放回收和分配
memory.c 只要包含了请页机制的相关函数
3)结构特定的模块
这个模块负责给各种硬件平台提供通用接口, 这个模块通过执行命令来改变硬件 MMU 的虚拟地址映射, 并在发生页错误时, 提供公用的方法来通知别的内核子系统这个模块是实现虚拟内存的物理基础
首先内存管理程序通过映射机制把用户程序的逻辑地址映射到物理地址, 在用户程序运行时如果发现程序要用的虚地址没有对应的物理内存时, 就发出了请求 也要求 1; 如果有空闲的内存可供分配, 就请求分配内存 2(于是用到了内存的分配二号回收), 并把正在使用的物理页记录在页缓存中 3(使用了缓存机制)如 果没有足够的内存可供分配, 那么就调用交换机制, 腾出一部分内存 4,5. 另外在地址映射中要通过 TLB(转换后备缓冲区)来寻找物理页 8, 交换机制中也要 用到交换缓存 6, 并且把物理页内容交换到交换文件中后也要修改页表来映射文件地址 7.
1.1 内存地址类型和内存保护
1.1.1 地址类型
Linux 是一个使用虚拟内存的系统, 虚拟内存使运行在系统上的程序可以分配到比可用物理内存更多的空间虚拟内存还能在进程地址空间上使用很多技巧, 如映射设备的内存
Linux 系统使用几种类型的地址, 下面列出了 Linux 用到的地址类型概念
1)用户虚拟地址
该地址是用户空间的程序使用的地址根据硬件体系结构的不同, 用户地址可以是 32 位或 64 位, 且每个进程拥有自己独立的虚拟地址空间
2)物理地址
该地址是物理内存的地址, 在处理器和系统内存之间使用物理地址是 32 或者 64 位
3)总线地址
该地址是指总线的寄存器的地址, 在外设总线和内存之间使用
4)内核逻辑地址
内核逻辑地址组成了常规的内核地址空间, 这些地址映射了大部分乃至所有的主内存, 并被视为物理内存使用在大多数的体系结构中, 逻辑地址及其所关联 的物理地址之间的区别, 仅仅在于一个常数的偏移量逻辑地址使用硬件特有的指针大小, 所以, 在配置有大量内存的 32 位系统上, 仅通过逻辑地址可以无法寻址 所有的物理内存在内核中, 逻辑地址通常保存在 unsigned long 或者 void * 这样的变量中由 kmalloc 返回的内存就是逻辑地址
通过宏可将逻辑地址与物理地址相互转换, 通过定义在 < asm/page.h > 中的宏__pa()返回与其关联的物理地址, 也可以使用__va()宏将物理地址映射回逻辑地址, 但只能用于低端内存页
5)内存虚拟地址
由函数 vmalloc 分配的内存是虚拟地址, kmap 函数也返回虚拟地址这种地址却不一定能直接映射到物理内存它需通过一系列处理, 如分配内存地址转换等, 才能与逻辑地址联系起来虚拟地址通常保存在指针变量中
6)低端内存和高端内存
低端内存和高端内存都是指物理内存
低端内存低端内存代表存在于内存空间的逻辑地址的物理内存大家遇到的几乎都是低端内存
高端内存高端内存是那些不存在逻辑地址的内存, 高端内存常常是保留给用户空间的进程页
在 i386 系统上, 低端内存和高端内存的之间的界限通常设置为 1GB 它是内核本身设置的限制, 用于将 32 位地址空间分割为内核空间和用户空间
用户空间可访问 4GB 的虚拟线性内存空间其中, 0 到 3GB 的虚拟内存地址是用户空间, 用户进程可以直接对其进行访问从 3GB 到 4GB 的虚拟内存地址为内核空间, 存放仅用于内核访问的代码和数据, 是所有进程共享的, 用户不能对它进行操作
所有的进程从 3GB 到 4GB 的虚拟空间都是一样的, 有同样的页目录项和同样的页表, 对应的同样的物理内存段这样, 内核态进程就共享代码段和数据段
1.1.2 内存保护
1)不同任务之间的保护
在 80386 上, 通过赋予每个任务不同的虚拟物理地址转换映射, 把每个任何放置在不同的虚拟地址完成在 30386 上, 每个任务都有自己的段表和页表
这种保护操作系统的方法是把操作系统存储在虚拟地址空间的一个公共区域, 然后, 使每个任务按此区域分配一个同样的虚拟地址空间, 并进行同样的虚拟 - 物理地址映射各个任务公用的这部分虚拟地址空间被称为全局地址空间
只有一个任务占有的虚拟地址空间部分, 即不被任何其他任务共享的虚拟地址部分, 称为局部地址空间
在不同的任务中, 对同一虚拟地址的访问, 实际上转换为不同的物理地址这就使得操作系统对每个任务的存储器可以赋予相同的虚拟地址, 仍然保证任务的 隔离另一方面, 对全局地址空间中同一虚拟地址的访问, 在所有任务中都转换为同样的物理地址, 从而支持公共的代码及数据的共享, 例如, 对操作系统的共享
2)同一任务的保护
在一个任务之内, 定义了四种执行特权级别, 用来限制对任务中段进行访问在 0 级是操作系统内核, 处理 I/O, 内存管理及其他关键的操作; 在 1 级是系 统调用处理程序, 用户程序可以通过调用这里的过程执行系统调用, 但是, 只有一些特定的和受保护的过程可以被调用; 2 级是库过程; 最后用户程序运行在级别 3 上, 受到的保护最少
1.2 80386 的段页式管理机制
80386 以两级虚拟 - 物理地址转换, 即使用了分段机制和分页机制来实现两级地址转换的第一级把包含段地址和段内偏移量的虚拟地址, 转换为一个线性地址第二级把线性地址转换为物理地址
Linux 内核中在物理内存中为每个进程维护一个页表, 页表驻留在内存, 不能被交换到磁盘但是, 内核使用一个独立的页表来管理页表映射内核段, 与用户空间每个进程一个页表不同, 该页表属于整个内核, 而且与当前正在运行的进程无关
1.3 进程的内存组织
1.3.1 内存管理的数据结构
内存管理中有三个重要的数据结构 struc vm_area_struct,struct page 和 struct mm_struct, 它们用于表示进程的内存使用, 它们与进程的关系如图 4.10 所示
每个进程都有一个 struct mm_struct 结构体, 用来描述一个进程的虚拟内存在结构体中包含了进程的页表和许多其他的大量信息
vm_area_struct 结构描述进程的一个虚拟内存地址区域一个 VMA(虚拟内存区域)是对页错误处理有同一规则的进程虚拟内存空间的部分, 如共享库运行区域等, 代表进程空间的一块单独连续的地址空间
page 结构用来描述一个物理页, 系统中每个物理页有一个页结构来保护跟踪
1.3.2 VMA 在 / proc 文件系统中的显示
VMA 是 Virtual Memory Aera 的缩写, 即内存虚拟区域在每个进程中, 一般分为这几个虚拟内存区域: 程序的代码区域(即 text 段); 每种类型的数据对应一个区域, 其中包括初 始化数据(在执行之处已经明确赋值的数据); 未初始化的数据(BSS); 程序栈等
vma 在程序运行中, 不断地由申请清除查找分割融合等对 vma 的管理操作, 所有 vma 存在一个双向链表中, 另外还用 AVL 树进行管理, 以加速查找
一个进程的内存区域可以从 / proc/pid/maps 中看到,/proc/*/maps 中的每项都与一个 vm_area_struct 结构成员对应, 可以用一个列表对每个字段进行描述
例如:
每行中的字段说明如下:
start-end 虚拟内存区域的起始和结束地址
perm 虚拟内存区域的读写和执行许可的位掩码最后一个字符 p 表示是私有, s 表示共享
offset 虚拟内存区域在被映射文件中的偏移(以页为单位)
major:minor 主设备号和次设备号
Image 被映射的文件名字
VMA 的应用是在页面错误时产生调页或换页操作的进程的所有 VMA 以一个排序的双向链表来连接, 按虚地址的下降顺序来排列的, 每个 VMA 对应一个相邻的地址空间范围
1.4 虚拟内存管理
1.4.1 大容量对象缓存
内核提供了 kmalloc 和 kfree, 分配真实地址已知的实际物理内存块, 还提供 vmalloc 和 vfree, 用于对内核使用的虚拟内存进行分配和释放由 kmalloc 返回的内存更适合类似设备驱动的程序来使用
当一个结构或表需要建立大容量对象实例时, 常用 vmalloc 函数进行申请
kmalloc 分配的地址范围一般在 3G~high_memory, 所以分配的物理地址与虚拟地址只有一个 PAGE_OFFSET 偏移, 不需要为地址段修改页表而且分配的虚拟逻辑地址和物理地址都是连续的
其中 kmalloc 分配的是物理地址, 而返回的则是虚拟地址(通过一定的偏移量将物理地址转换为虚拟地址)
vmalloc 函数分配的虚拟空间在 3G+high_memory+VMALLOC_OFFSET 以上高端, 由 vmlist 链表管理 3GB 是内核 态赖以访问物理内存的地址, high_memory 是安装在计算机实际可用的物理内存的最高地址 VMALLOC_OFFSET 则为长度为 8MB 的隔离 带, 起着越界保护作用
vmalloc 函数分配的虚拟空间管理结构列出如下:
在 mm/vmalloc.c 中有 vmlist 链表的全局变量申明 struct vm_struct *vmlist, 在 vmlist 链表中, 每个虚拟内存块之间都有个 4KB 大小的隔离带, 用来检测访问指针的越界错误第一个节点的地址就是 VMALLOC_START
函数 vmalloc 的功能是分配与 size 相匹配页面大小的连续虚拟内存, 但对应的物理内存仍需经缺页中断后, 由缺页中断服务程序分配, 分配的物理页是不连续的函数 vmalloc 申请大块缓冲区, 但当前不用的内容不会调用入到物理内存中
函数 vmalloc 列出如下:
- void *vmalloc(unsigned long size)
- {
- return __vmalloc(size,GFP_KERNEL|__GFP_HIGHMEM,PAGE_KERNEL);
- }
函数__vmalloc 分配足够的页数与 size 相配, 把它们映射进连续的内核虚拟空间, 但分配的内存块不一定连续在函数中第一步是在 vmlist 中寻找一个大小合适的虚拟内存块 (get_vm_area(size)) 第二步检查这个虚拟块是否可用 (空闲), 建立页目录, 找到空闲(虚 拟块映射内) 分配给调用进程(get_free_page()); 如果虚拟块是不可用的, 那么必须要释放掉这个虚拟块(vfree)
1.4.2 内存映射
内存映射的介绍
运行可执行文件时, 先被映射到进程的虚拟地址空间中, 形成 vm_area_struct 结构链表, 接着程序的一部分被操作系统装入到物理内存这种将映像链接到进程虚拟地址空间的访问称为内存映射通过内存映射, 文件的内容被直接链接到进程的虚拟地址空间
随着 vm_area_struct 结构的生成, 这些结构所描述的虚拟内存区域上的标准操作函数也由 Linux 初始化在逻辑地址和物理地址之间相互 转换的工作, 是由内核和硬件内存管理单元 (MMU) 共同完成的, MMU 是 CPU 的一个部分内核告诉 MMU 如何为每个进程把逻辑严密映射到某特定物理页 面, 而 MMU 在进程提出内存请求时完成实际的转换工作为了减少开销, 最近被执行的地址转换结果将被存储在 MMU 的转换后备缓存 (TLB) 内除了由于内 核的操作致使 TLB 无效偶尔会通知 CPU 外, Linux 不会明确管理 TLB
内核维护了一个或者更多由 struct page 项构成的数组, 它们跟踪系统上所有的物理内存
一些函数和宏可用来在 struct page 和虚拟地址之间进行转换:
struct page *virt_to_page(void *kaddr): 这个宏接受一个内核逻辑地址, 并返回与其关联的 struct page 指针因为它需要一个逻辑地址, 它对 vmalloc 返回的内存和高端内存无效
void *page_address(struct page *page): 如果这个地址存在, 则返回该页的内核虚拟地址对于高端内存, 仅在该页已经被映射的情况下, 其地址才存在
在移动或修改页表时, 应该持有该 page_table_lock 自旋锁
与内存映射相关的几个文件在 mm 目录下, 其中, mmap.c 文件中主要函数 do_mmap 的功能是: 把文件中的逻辑地址映射成虚存的线性地址, 即把 从文件结构中得到的逻辑地址转换为 vm_area_struct 结构所需的地址 mremap.c 文件中主要函数 sys_mremap 的功能是: 扩张或缩 小现存的虚拟内存空间 filemap.c 文件中的主要函数功能是: 处理内存映射和也高速缓存器, 即把线性地址空间映射到内存且修改页高速缓存这部分含 有从磁盘读写的 I/O 操作
sys_brk 系统调用
sys_brk 提供支持 C 语言的 malloc 和 free 函数低级操作 C 库函数 malloc 通过系统调用 sys_brk 向内核申请了一段虚地址空间 vma 来建立地址映射, vma 全部建立起与内存页的映射, C 库函数 free 也通过这个系统调用告诉内存需要收回这段地址空间, 取消已建立的映射 sys_brk 系统调用可以对用户进程的堆的大小进行操作, 使堆扩展或者缩小
sys_brk 中调用 do_brk 函数, 它是一个简单的 do_mmap 函数, 它仅处理匿名映射, 并将 vma 全部映射到内存页, 而不像 do_mmap 函数是由缺页来引起调页的
mmap 系统调用
一个进程可通过系统调用 mmap(), 将一个已经打开文件的内容映射到它的用户空间它直接调用 do_mmap()函数来完成映射, 在这个函数中, 参数 file 为映射的文件, 参数 addr 为映射的地址, 参数 len 为 VMA 长度, 长度 prot 指定该 vma 段的访问权限, 参数 flag 为 vma 段的属性
1.4.3 虚拟内存的加锁和保护
Linux 可对虚拟内存段中的任一部分加锁或保护对进程的虚拟地址加锁, 其实质就是对 vma 段的 vm_flags 属性或上 VM_LOCKED 在虚存加锁后, 它对应的物理页面驻留内存, 不再被页面置换程序换出除非调用 mlock 的进程终止或者调用 exec 执行其他程序, 这 部分被锁住的源码才被释放通过 fork()调用所创建子进程不能够继承由父进程调用 mlock 锁住的页面加锁操作有四个系统调用函数: mlock munlockmlockall 和 munlockall
1.5 物理内存管理
NUMA(非一致内存访问体系)系统保持了物理上分散而逻辑上同一的内存模式, 是高性能服务器的主流体系结构
1.5.1 伙伴系统
伙伴系统的概述
Linux 内核内存管理的一项重要工作就是如何在频繁申请释放内存的情况下, 避免碎片的产生 Linux 采用伙伴系统解决外部碎片的问题, 采用 slab 解决内部碎片的问题, 在这里我们先讨论外部碎片问题避免外部碎片的方法有两种: 一种是之前介绍过的利用非连续内存的分配; 另外一种则是用一种有效的方法来监视内存, 保证在内 核只要申请一小块内存的情况下, 不会从大块的连续空闲内存中截取一段过来, 从而保证了大块内存的连续性和完整性显然, 前者不能成为解决问题的普遍方法, 一来用来映射非连续内存线性地址空间有限, 二来每次映射都要改写内核的页表, 进而就要刷新 TLB, 这使得分配的速度大打折扣, 这对于要频繁申请内存的内核 显然是无法忍受的因此 Linux 采用后者来解决外部碎片的问题, 也就是著名的伙伴系统
什么是伙伴系统?
伙伴系统的宗旨就是用最小的内存块来满足内核的对于内存的请求在最初, 只有一个块, 也就是整个内存, 假如为 1M 大小, 而允许的最小块为 64K, 那么当我 们申请一块 200K 大小的内存时, 就要先将 1M 的块分裂成两等分, 各为 512K, 这两分之间的关系就称为伙伴, 然后再将第一个 512K 的内存块分裂成两等 分, 各位 256K, 将第一个 256K 的内存块分配给内存, 这样就是一个分配的过程
物理页面的分配和回收
Linux 使用伙伴算法来有效地分配与回收页面块页面分配时, 内存分配器在 free_area 数组中寻找一个与请求大小相同的空闲块分配算法首 先搜寻满足请求大小的页面它从 free_area 数据结构的 list 域着手沿着链来搜索空闲页面如果没有以这样的方式请求大小的空闲页面, 则它搜索两 倍于请求大小的内存块这个过程一直将持续到 free_area 被搜索完或找到满足要求的内存块为止
如果找到的页面大于请求的块, 则把它分成两块: 一个与请求块匹配, 另一个事空闲块每块大小都是 2 的 N 次幂空闲块被链接进相应大小的队列, 而另一个页面块被分配给调用者
页面回收时, 内存分配器将检查是否有相同大小的相邻或者伙伴内存块存在如果有, 则将它们结合起来形成一个大小为原来两倍的新空闲块每次结合完之后, 代码还要检查是否可以继续合并成更大的页面
_get_free_page 函数是申请空闲页面的上层接口 free_pages 函数释放内存页
下面我们结合示意图来了解伙伴系统分配和回收内存块的过程
1 初始化时, 系统拥有 1M 的连续内存, 允许的最小的内存块为 64K, 图中白色的部分为空闲的内存块, 着色的代表分配出去了得内存块
2 程序 A 申请一块大小为 34K 的内存, 对应的 order 为 0, 即 2^0=1 个最小内存块
2.1 系统中不存在 order 0(64K)的内存块, 因此 order 4(1M)的内存块分裂成两个 order 3 的内存块(512K)
2.2 仍然没有 order 0 的内存块, 因此 order 3 的内存块分裂成两个 order 2 的内存块(256K)
2.3 仍然没有 order 0 的内存块, 因此 order 2 的内存块分裂成两个 order 1 的内存块(128K)
2.4 仍然没有 order 0 的内存块, 因此 order 1 的内存块分裂成两个 order 0 的内存块(64K)
2.5 找到了 order 0 的内存块, 将其中的一个分配给程序 A, 现在伙伴系统的内存为一个 order 0 的内存块, 一个 order
1 的内存块, 一个 order 2 的内存块以及一个 order 3 的内存块
3 程序 B 申请一块大小为 66K 的内存, 对应的 order 为 1, 即 2^1=2 个最小内存块, 由于系统中正好存在一个 order 1 的内
存块, 所以直接用来分配
4 程序 C 申请一块大小为 35K 的内存, 对应的 order 为 0, 同样由于系统中正好存在一个 order 0 的内存块, 直接用来分
配
5 程序 D 申请一块大小为 67K 的内存, 对应的 order 为 1
5.1 系统中不存在 order 1 的内存块, 于是将 order 2 的内存块分裂成两块 order 1 的内存块
5.2 找到 order 1 的内存块, 进行分配
6 程序 B 释放了它申请的内存, 即一个 order 1 的内存块
7 程序 D 释放了它申请的内存
7.1 一个 order 1 的内存块回收到内存当中
7.2 由于该内存块的伙伴也是空闲的, 因此两个 order 1 的内存块合并成一个 order 2 的内存块
8 程序 A 释放了它申请的内存, 即一个 order 0 的内存块
9 程序 C 释放了它申请的内存
9.1 一个 order 0 的内存块被释放
9.2 两个 order 0 伙伴块都是空闲的, 进行合并, 生成一个 order 1 的内存块 m
9.3 两个 order 1 伙伴块都是空闲的, 进行合并, 生成一个 order 2 的内存块
9.4 两个 order 2 伙伴块都是空闲的, 进行合并, 生成一个 order 3 的内存块
9.5 两个 order 3 伙伴块都是空闲的, 进行合并, 生成一个 order 4 的内存块
1.5.2 缓存及 slab
缓存的结构
一个对象的所有实例都存在同一缓存区中不同的对象, 放在不同的缓存区中每一缓存区有若干个 slab, 按照满半满空的顺序排列整个内核内存可看做是按照这种缓存区组织的
在内核中缓存的全局变量 malloc_size[]中定义了可分配对象的大小, 每次分配对象时, 只能分配成 cache_sizes 结构描述大小的对象
slab 结构
slab 块是内核内存分配与页面级分配的接口一种对象的所有实例在同一个缓冲区, 每个缓冲区存在若干个 slab 块, 按满半满和空闲的顺序每个 slab 块的大小为页面大小的整数倍, 存有若干个对象
slab 算法基于对象缓存, 就是保留对象初始化状态的不变部分当新建一个对象时, 如果在缓冲区中有空闲的这个对象位置时, 就获得此对象, 不必初始 化当释放对象时, 只是在缓存中将相应位置标为空闲, 而不做析构只是在系统资源不足时, slab 算法才将一部分未使用的缓存空间释放这样减少了大量常 用对象的初始化和析构部分的费时过程
1.5.3 交换空间
内存管理系统需要将暂时不用的内存数据转储到外存中, Linux 采用两种方式保存换出的页面: 一种用整个设备, 如硬盘的一个分区, 称为变换设备; 另 一种用文件系统中固定长度的文件, 称为交换文件它们统称为交换空间这两种交换方式的内部格式是一致的前 4096 字节是一个以字符口串 SWAP- SPACE 结尾的位图位图的每一位对应一个交换空间的页面, 置位表示对应的页面可用于换页操作每 4096 字节之后才是真正存放换出页面的空间这 样, 每个交换空间最多可容纳(4096-10)*8-1=32687 个页面
交换设备比交换文件有效很多在交换设备中, 属于同一页面的数据块总是连续存放的第一个数据块一经确定, 后续的数据块可以按顺序读出或写入而在 交换文件中, 属于同一页面的数据虽然在逻辑上是连续的(在交换文件的位图看来), 但数据块的实际位置可能是零散的, 需要通过交换文件的 inode 检索, 这 决定于拥有交换文件的文件系统
交换文件是在物理磁盘上的, 因此将所有的交换页面按簇为单位存储, 每簇内的页面连续存放
1.5.4 请页机制
页故障错误的产生有三种原因:
程序出现错误, 虚拟内存无效(还没有映射, 即还没有映射到磁盘上),Linux 将向进程发送 SIGSEGV 信号并终止进程的运行;
虚拟内存有效, 但其所有对应的页当前不在物理内存中 (但是存在磁盘中), 即缺页错误, 这时, 操作系统必须从磁盘映像(页未分配或属于共享库) 或交换文件 (此页被换出) 中将其装入物理内存
最后一种情况是, 要访问的虚地址被写访问(对只读区域进行写操作), 即保护错误
对有效的虚拟地址, 即已分配的虚拟地址, 如果缺页错误的话, Linux 必须区分页所在的位置, 即判断页是在交换文件中, 还是在可执行映像中 Linux 通过页表项中的信息区分页面所在的位置如果该页的页表项是无效的, 但非空, 则说明该页处于交换文件中, 操作系统要从交换文件装入页, 如果都不 在, 则把虚拟地址映射到物理内存
1.5.5 守护进程 kswapd
当物理内存出现不足时, Linux 内存管理子系统需要释放部分物理内存页这一任务由内核的交换守护进程 kswapd 完成, 该内核守护进程实际是一 个内核线程, 它在内核初始化时启动, 并周期性地运行它的任务就是保证系统中具有足够的空闲页, 从而使内存管理子系统能够有效运行
1.5.6 内存管理相关的高速缓存
来源: http://www.bubuko.com/infodetail-2497620.html