5linux操作系统内存管理

71次阅读

共计 7013 个字符,预计需要花费 18 分钟才能阅读完成。

从物理上需要关注物理架构,RAM 的组织管理 / 分配 / 页结构,cache,cpu_cache。内存分配设计到伙伴系统、slab。从逻辑上涉及到虚拟内存,页表,地址空间。二者的使用涉及到缺页,回收,交互,共享内存等内容

物理架构

cpu 之间走 FSB,cpu 与 NB 走 FSB。与 RAM 走 NB。
dma->nb->ram
DRAM,FB-DRAM(RAM 与 nd 多条),一般用于主存,SRAM 用于 cpuchache

=》
NB 的 mc 独立可多条并发

=》

mc 内置于 cpu

对称 SMP,NUMA(非统一资源),NUMA 因子(远端内存代价)

RAM

RAM


RAM 分三个管理区:
ZONE_DMA:可用作 DMA 的内存区域。该类型的内存区域在物理内存的低端,主要是 ISA 设备只能用低端的地址做 DMA 操作。
ZONE_NORMAL:直接被内核直接映射到自己的虚拟地址空间的地址。
ZONE_HIGHMEM:不能被直接映射到内核的虚拟地址空间的地址。
在分配时的管理区分配器:buddy/slab。还有 pre-cpu page。
图中为整体架构,进程映射到 RAM,RAM 的内存结构是 memap,每个页是一个元素,还可能会映射到 swap。三个管理区有自己的管理区分配器。

  • 分配:
    1.alloc_pages 整个页
    2.kmalloc 字节 连续内存虚拟地址,物理地址也连续
    3.vmalloc 虚拟连续,物理不一定,建立页表项需要一个一个的映射
    4.slab 层,防止每个进程自己单独创建 free 链,不能全局判断

    高低度缓冲组。kmem_cache。每个类型比如 task_struct,inode,通用的。3 个链表 empty,full,parti
    kmem_Cache_alloc. 内存紧缺 / 显示撤销才释放

    5. 高端内存(页并不一定永久映射,比如内核 1G 要访问 8G 物理内存,动态映射)

        kmap。可睡眠,永久映射
        kmap_atomic 临时,原子,不睡眠,不阻塞,禁止内核抢占

    6.cpu 上数据,alloc_percpu

page

内存结构最外层是 pglist_data,对于一个 RAM 的就只有一个,SMP 的多个 RAM 是个数组。分为三个区域来管理,每个包含很多页,所有 RAM 对应 mem_map。
zone 中有极值:pages_min, pages_low, pages_high; 来控制 zone 中页面的回收
有等待区队列,一个区一个,页 hash 映射唤醒(每页一个占空间,一个 zone 一个惊群,所以 hash 映射唤醒,当有 hash 冲突时会环境多个进程)
Page 有两个重要字段,address_space 为 cache 的结构。非 cache 的 private

 struct {
    unsigned long private;        
    struct address_space *mapping;
    };


每个物理页一个结构 4G 会占用 20M 被用户空间进程 / 动态分配内核数据 / 静态内核代码 / 页高速缓存等引用

flag 脏 / 锁;count 引用计数,0 空闲;*virtual 虚拟地址

cache

cache 是对外设或者文件或者其他的缓存,是 RAM 的一部分,从分类上大概有 buffer /page/swap/ 硬件,还有 Inode/ 目录等 cache。这些都是在别处有副本的。buffer 是针对块设备,page 基本基于与文件或基于其他 page 管理的缓存,我们用 free 命令可以看到 buff/cache,used。used 指的就是非 cache RAM 的使用量。在 mmap 不同种类时用到的不一样,比如共享匿名映射时,这时是从 cache 中申请内存,在进行匿名私有映射时,并没有占用 cache,文件映射, 则都是将文件映射到 cache 中,然后根据共享还是私有进行不同的操作,私有的会再 used 增加一份;
page cache 本质上都是文件的缓存,大多数可清理 这种时候放 cache 中作为缓存、那为何比如共享内存映射也在呢,因为他本质上也是特殊文件系统 shm 中的一个文件,shm 的安装点在交换区上,类似的还有 tmp.proc 等这种基于内存的文件。对于私有内存映射 非基于文件 的不走 cache。至于回收(无论 cache 与否都一样):页框回收算法必须处理多种属于用户态进程、磁盘高速缓存和内存高速缓存的页,而且必须遵照几条试探法准则只有走缓存的才有 address_space 否则就 private
fd 与磁盘的关联
fd->file->path->dentry->inode->address_space->page->buffer_head-> 磁盘块号

cpucache

缓存线:64 字节长。组关联(外层查找,内层地址置换 mmap)tag-cache set-offset


cpu 都有独立调的 1 级,共享 2 / 3 级等可能有点诧异。l1 一般是虚拟地址,其他是物理地址。sram
主存 -l3-l2-l1-cpu。

数据结构:

性能

1. 组关联,段大小,cache size,数据集大小(对不同 cache 的占用)

2. 顺序访问单线程
数据大小,预取(下面图预取超过缓存,不生效),TLB

3. 随机访问 无预取,大小,TLB
4. 写入 写回 / 写入合并 / 不缓存等策略的影响
5. 多处理器 MESI
6. 多线程
7. 超线程
写入:写回策略,打开 dirty 标记,缓存线丢弃时,通知写回主存,每个写通 / 删除对 FSB 压力大
多处理器一致性:MESI 状态变更,


If a Modified cache line is read from or written to on the local processor, the instruction can use the current cache content and the state does not change. If a second processor wants to read from the cache line the firs processor has to send the content of its cache to the second processor and then it can change the state to Shared.The data sent to the second processor is also received and processed by the memory controller which stores the content in memory. If this did not happen the cache line could not be marked as Shared. If the second processor wants to write to the cache line the first processor sends the cache line content and marks the cache line locally as Invalid. This is the infamous“Request For Ownership”(RFO) operation. Performing this operation in the last level cache, just like the I→M transition is comparatively expensive. For write-through caches we also have to add the time it takes to write the new cache line content to the next higher-level cache or the main memory, further increasing the cost.

numa

互连
内存利用率 -》带条化,自由迁移进程
迁移
一致性

优化

1. 不缓存
2. 充分利用缓存
行预取
64 字节。double 可以 8 个一读
对齐 占尽量少的缓存行,经常用到的组合在一起
l1 级指令。比如 inline 的取舍
TLB 优化。减少页数 / 减少页目录级数
3. 预取
硬件 / 软件 / 单独线程(超线程 /dumber)/directcacheaccess 设置标记让 DMA 可以直接写入 cpucache
4. 多线程优化

组读写变量,局部变量等
原子、原子运算、cas
cpu 亲和性等

工具

https://people.freebsd.org/~l…

buddy

为了便于页面的维护,将多个页面组成内存块
每个大小为 2^n。当没有自己的 size 到大 size 拆分

每个区有自己的 buddy

固定 2^n 这种内存分配会产生碎片化,但是在内存管理这不能随便有权限像其他的一样定期整理,因此采取反碎片化的分配方式,对页面进行分类,https://blog.csdn.net/goodluc…

slab

发现内核对象初始化代价比分配大,对对象进行缓存


一个 cache 管理一组大小固定的内存块(也称为对象实体),每个内存块都可用作一种数据结构。cache 中的内存块来自一到多个 slab。一个 slab 来自物理内存管理器的一到多个物理页
每个缓存结构都包括了两个重要的成员:

struct kmem_list3 **nodelists:kmem_list3 结构中包含了三个链表头,分别对应于完全用尽的 slab 链表,部分用尽的 slab 链,空闲的 slab 链表,其中部分空闲的在最开始
struct array_cache *array[NR_CPUS + MAX_NUMNODES]:array 是一个数组,系统中的每一个 CPU,每一个内存节点都对应该数组中的一个元素。array_cache 结构包含了一些特定于该 CPU/ 节点的管理数据以及一个数组,每个数组元素都指向一个该 CPU/ 节点刚释放的内存对象。该数组有助于提高高速缓存的利用率。
当释放内存对象时,首先将内存对象释放到该数组中对应的元素中
申请内存时,内核假定刚释放的内存对象仍然处于 CPU 高速缓存中,因而会先从该数组的对应数组元素中查找,看是否可以申请。
当特定于 CPU/ 节点的缓存数组是空时,会用 slab 缓存中的空闲对象填充它

CPU 访问内存时使用哪个 cache line 是通过低地址的若干位确定的,比如 cache line 大小为 32,那么是从 bit5 开始的若干位。因此相距很远的内存地址,如果这些位的地址相同,还是会被映射到同一个 cache line。Slab cache 中存放的是相同大小的对象,如果没有着色区,那么同一个 cache 内,不同 slab 中具有相同 slab 内部偏移的对象,其低地址的若干位是相同的,映射到同一个 cache line。访问 cache line 冲突的对象时,就会出现 cache miss,通过着色区使对象的 slab 内偏移各不相同

虚拟内存

为何要有虚拟内存?
进程地址空间不隔离 =》分段
程序运行的地址不确定 =》分段
内存使用效率低 =》分页

地址映射

逻辑地址 + 段基地址(linux 都是 0,)=》线性地址
ds 值为 0x7b, 转换成二进制为 00000000 01111011,TI:0(GDT),GDT:15(用户数据段描述符)。
线性地址 =》物理地址。页表转换

页表

内核临时页表

linux 启动分两个阶段:第一个阶段(汇编)建立分段机制(忽略),建立一个临时页表进入分页机制。第二个阶段(C 语言)初始化系统的各种资源(硬件 / 软件)。
需要映射进页表的内存包括:linux 内核。临时页表(这个不一定需要映射进页表,但是为了方便也这么做了)。128k 的临时内存管理系统(其实就是用的位图管理 1G 的低端内存,2^32/4096/8 = 128K)共占 4M 内核代码 + 其他 映射 8M
linux 内核的临时页表有两层。因为 0xC 和 0x0 都要访问内核代码,外层在 512~1024 之间需要 10 位表示,4K 的页需要偏移位 12 位。剩 10 位足以表示 4M 内存(4M/4K=1024 页)。因此外层两个 -》1024 个 10-10-12 来映射

进程页表 / 内核最终页表


4M 2 级
4G 需要 3 级
其他……

页表优化

随机化安全 VS 顺序性能
优化 TLB 标签附加扩展并唯一标识其涉及页表树的刷入刷出
页面大 TLB 少,装入多。页表树级数少,页面偏移位部分增大,页目录少,内存要连续 / 对其会造成浪费

地址空间

这是一个完成的地址空间到物理页的关系图。右侧为进程地址空间,在 32 位中,上部 3G-4G 位共享的内核空间,下层为用户空间。
最外层结构是 task_struct 中的 mm_struct。里边包含 pgd 和 mmap。pdg 的指针会存在 cr3 中。mmap 由很多 vm_are_struct 组成,每个用户空间分区都是一个 vm,堆申请也是一个 vm。通过页表会定位到 memap 映射的 pagge。

内核地址空间

分为直接映射 / 高端内存
高端内存用于 vmalloc。持久映射区域 / 固定映射区域
虚拟地址中连续,但是物理地址不连续的内存区域可以从 VMALLO 区域分配
持久映射区域用于将高端内存中的非持久页映射到内核中
固定映射用于与物理地址空间中的固定页关联的虚拟地址页,但是物理地址页即页帧可以自由选择

Linux 将内核地址空间划分为三部分 ZONE_DMA、ZONE_NORMAL 和 ZONE_HIGHMEM,高端内存 HIGH_MEM 地址空间范围为 0xF8000000 ~ 0xFFFFFFFF(896MB~1024MB)。那么如内核是如何借助 128MB 高端内存地址空间是如何实现访问可以所有物理内存?
当内核想访问高于 896MB 物理地址内存时,从 0xF8000000 ~ 0xFFFFFFFF 地址空间范围内找一段相应大小空闲的逻辑地址空间,借用一会。借用这段逻辑地址空间,建立映射到想访问的那段物理内存(即填充内核 PTE 页面表),临时用一会,用完后归还。这样别人也可以借用这段地址空间访问其他物理内存,实现了使用有限的地址空间,访问所有所有物理内存。

进程地址空间

在讲进程时已经详细讲解了。这里说下 vm。以红黑树组织

缺页

当用户访问一个页时,MMU 回去查询一个操作系统维护的页表,看看用户访问的页是否存在于内存当中,如果存在,就直接调用;如果没有,就需要开启中断,执行页错误处理程序,在磁盘中找到相应的数据页,并在内存中找到一块空闲,将其载入进来,然后中断返回,执行用户程序,在去访问内存中用户需要的页。

交换

非映射页在磁盘上提供备份(磁盘分为文件 / 交换 / 裸设备)
交换子系统的主要功能总结如下:
1) 在磁盘上建立交换区(swap area),用于存放没有磁盘映像的页。
2) 管理交换区空间。当需求发生时,分配与释放页槽(page slot)。
3) 提供函数用于从 RAM 中把页换出 (swap out) 到交换区或从交换区换入(swap in)到 RAM 中。
4) 利用页表项(现已被换出的换出页页表项)中的换出页标识符跟踪数据在交换区中的位置。

标识一个换出页,在 swap_info 数组中指定交换区的索引和在交换区内指定页槽的索引,
当页被换出时,其标识符就作为页的表项插入页表中


竟然看到有人提到自己老师哈哈。可信老师的课件已经不免费了
https://blog.csdn.net/iostrea…

页回收

回收时机

page_min:如果空闲页数目小于该值,则该 zone 非常缺页,页面回收压力很大。
page_low: 如果空闲页数目小于该值,kswapd 线程将被唤醒,并开始释放回收页面。
page_high: 如果空闲页面的值大于该值,则该 zone 的状态很完美, kswapd 线程将重新休眠。

回收策略

linux 的 LRU active/inactive
会根据不同页情况做不同的策略。比如:
进程映射的页:max_mapped 为 0 换出
锁定:调过
脏页:回写等
……

反向映射

解决如何看一个共享页是否可以被释放?
匿名页:双向链表
文件映射:优先搜索树
子节点的堆索引都不大于相应父节点的堆索引。任意一个节点的左子节点基索引也都不大于右子节点基索引,如果基索引相等,则按照大小索引排序。
基索引(radix index)和堆索引(heap index)两个索引来标识。基索引表示区间的起始点而堆索引表示终点

共享内存

我们在说共享内存的时候。包含 mmap 和 posix 共享内存。要做到共享 mmap 只能在磁盘创建文件,posix 是会在 tmpfs 上,内核持久性,shm_open+mmap。更多参考 https://segmentfault.com/a/11…,https://segmentfault.com/a/11…
进程调用 mmap()时,只是在进程空间内新增了一块相应大小的缓冲区,并设置了相应的访问标识,但并没有建立进程空间到物理页面的映射。因此,第一次访问该空间时,会引发一个缺页异常
对于共享内存映射情况,缺页异常处理程序首先在 swap cache 中寻找目标页(符合 address_space 以及偏移量的物理页),如果找到,则直接返回地址;如果没有找到,则判断该页是否在交换区 (swap area),如果在,则执行一个换入操作;如果上述两种情况都不满足,处理程序将分配新的物理页面,并把它插入到 page cache 中。进程最终将更新进程页表。
注:对于映射普通文件情况(非共享映射),缺页异常处理程序首先会在 page cache 中根据 address_space 以及数据偏移量寻找相应的页面。如果没有找到,则说明文件数据还没有读入内存,处理程序会从磁盘读入相应的页面,并返回相应地址,同时,进程页表也会更新。
5. 所有进程在映射同一个共享内存区域时,情况都一样,在建立线性地址与物理地址之间的映射之后,不论进程各自的返回地址如何,实际访问的必然是同一个共享内存区域对应的物理页面。
注:一个共享内存区域可以看作是特殊文件系统 shm 中的一个文件,shm 的安装点在交换区上

正文完
 0