Linux 存储管理2——内存管理

1、MMU(内存管理单元)

MMU是怎么将逻辑地址转换成物理地址?

MMU是一种硬件电路,它包含两个部件,一个是分段部件,一个是分页部件,通过分段机制(把一个逻辑地址转换为线性地址,线性地址也是32位,其地址取值范围为0x00000000~0xffffffff)和分页机制(把一个线性地址转换为物理地址),最终将逻辑地址映射为物理地址。如下图:

1.1 分段机制

在操作系统原理关于分段的说明:段的分配时为了更好的满足用户,段的长度不固定,由用户定义,每个段都有自己的地址空间(通过基址包含某物理内存的地址,和长度值来表示段的长度),表示一个地址需要给出段部分(选择符)和偏移部分。如下图,如果没有分页的话,那么图中的线性地址也就是物理地址,这种方式对于段的离散分配就很容易导致碎片问题(进而使用分页机制来提高内存利用率):

<!--[if !vml]--><!--[endif]-->

在Linux中,主要设置了:内核代码段,内核数据段,用户代码段,用户数据段。而且每个段的基地址对应线性地址都是为0,而且都可以使用4G的地址空间,相当与绕过了逻辑地址和线性地址的映射,从而完全利用了分页机制。另外,分段中怎样使用段寄存器可以参考:http://book.51cto.com/art/200812/103305.htm

1.2 分页机制

通过使用分页机制可以很好的提高内存利用率。分页机制把一个线性地址转换为物理地址。Linux下一个页大小为4K,把线性地址(32位)和物理地址(32位,2^32=4G物理内存)都按照4K(2^12)页大小来进行划分,那么,一共有4G/4K=1M的页面,如果只是简单的进行线性地址和物理地址一对一记录映射的话,这样在映射表建立的就会占用比较大的物理内存。这个时候就引入了页表页目录表,地址转换过程见下图:

(1) 通过对32位线性地址划分:第31~22这10位(2^10=1024)定位页目录项,第21~12这10位定位页表项,第11~0这12位(2^12=4K)为页内偏移值。

(2) 对于页目录表,有1024个页目录项,每个页目录项(又含有1024个页表项)指向下一级页表的物理地址(32位=4个字节),那么一共需要1024*4(=4K)字节,即只要分配一页就可以完全存放。

(3) 对于页表,原理和页目录表一样,那么一共需要1024(1024个页目录项)*1024(每个页目录项含有1024个页表项)*4(=1M)字节。

(4) 对于页目录的物理地址,就存放在CR3寄存器中。

所以,这样可以寻址1024*1024*4K=4G的物理内存。另外,在Linux的X86架构引入了3级页表机制(包括了页全局目录、页中间目录和页表)。

2、高端内存的映射方式

高端内存是指物理地址大于 896M 的内存。对于这样的内存,无法在“内核直接映射空间”进行映射。
为什么?
因为“内核直接映射空间”最多只能从 3G 到 4G,只能直接映射 1G 物理内存,对于大于 1G 的物理内存,无能为力。实际上,“内核直接映射空间”也达不到
1G, 还得留点线性空间给“内核动态映射空间” 呢。因此,Linux 规定“内核直接映射空间” 最多映射 896M 物理内存。

对 于高端内存,可以通过 alloc_page() 或者其它函数获得对应的 page,但是要想访问实际物理内存,还得把 page 转为线性地址才行(为什么?想想 MMU 是如何访问物理内存的),也就是说,我们需要为高端内存对应的 page 找一个线性空间,这个过程称为高端内存映射。

高端内存映射有三种方式:

1、映射到“内核动态映射空间”
这种方式很简单,因为通过 vmalloc() ,在”内核动态映射空间“申请内存的时候,就可能从高端内存获得页面(参看 vmalloc 的实现),因此说高端内存有可能映射到”内核动态映射空间“ 中。

2、永久内核映射
如果是通过 alloc_page() 获得了高端内存对应的 page,如何给它找个线性空间?
内核专门为此留出一块线性空间,从 PKMAP_BASE 到 FIXADDR_START ,用于映射高端内存。在 2.4 内核上,这个地址范围是 4G-8M 到 4G-4M 之间。这个空间起叫“内核永久映射空间”或者“永久内核映射空间”
这个空间和其它空间使用同样的页目录表,对于内核来说,就是 swapper_pg_dir,对普通进程来说,通过 CR3 寄存器指向。通常情况下,这个空间是
4M 大小,因此仅仅需要一个页表即可,内核通过来 pkmap_page_table 寻找这个页表。
通过 kmap(), 可以把一个 page 映射到这个空间来
由于这个空间是 4M 大小,最多能同时映射 1024 个 page。因此,对于不使用的的 page,应该及时从这个空间释放掉(也就是解除映射关系),通过
kunmap() ,可以把一个 page 对应的线性地址从这个空间释放出来。

3、临时映射

内核在 FIXADDR_START 到 FIXADDR_TOP 之间保留了一些线性空间用于特殊需求。这个空间称为“固定映射空间”

在这个空间中,有一部分用于高端内存的临时映射。

这块空间具有如下特点:

1、 每个 CPU 占用一块空间

2、 在每个 CPU 占用的那块空间中,又分为多个小空间,每个小空间大小是 1 个 page,每个小空间用于一个目的,这些目的定义在 kmap_types.h 中的 km_type 中。

当要进行一次临时映射的时候,需要指定映射的目的,根据映射目的,可以找到对应的小空间,然后把这个空间的地址作为映射地址。这意味着一次临时映射会导致以前的映射被覆盖。

通过 kmap_atomic() 可实现临时映射。

下图简单简单表达如何对高端内存进行映射

-------------------------

高端内存含义为:线性地址空间 PAGE_OFFSET + 896M 至4G的最后128M线性地址 <==映射==> 896M以上的物理页框,非直接映射。有3种方法:非连续内存区映射,永久内核映射,临时内核映射(固定映射)
   从 PAGE_OFFSET开始的线性地址区域为:
   PAGE_OFFSET(3G)|物理内存映射 --8M-- vmalloc区 --4K-- vmalloc区 --8K-- 永久内核映射--临时内核映射(固定映射)|4G

1. 非连续区映射

1.1 每个非连续内存区都对应一个类型为 vm_struct的描述符,通过next字段,这些描述符被插入到一个vmlist链表中。

1.2 三种非连续区的类型:
   VM_ALLOC   -- 物理内存(调用alloc_page)和线性地址同时申请,物理内存是 __GFP_HIGHMEM类型(分配顺序是HIGH, NORMAL, DMA )(可见vmalloc不仅仅可以映射__GFP_HIGHMEM页框,它的主要目的是为了将零散的,不连续的页框拼凑成连续的内核逻辑地址空间...)
   VM_MAP     -- 仅申请线性区,物理内存另外申请,是VM_ALLOC的简化版
   VM_IOREMAP -- 仅申请线性区,物理内存另外申请(这里的物理内存一般都是高端内存,大于896M的内存)

2. 永久内核映射

2.1 永久内存映射允许建立长期映射。使用主内核页表中swapper_pg_dir的一个专门页表。
    pkmap_page_table: 专门的页表。页表表项数由LAST_PKMAP(512或1024)产生。
    page_address_htable: 存放地址的
    pkmap_count: 包含LAST_PKMAP个计数器的数组。
    PKMAP_BASE: 页表线性地址从PKMAP_BASE开始。

2.2 如果LAST_PKMAP个项都用完,则把当前进程置为 TASK_UNINTERRUPTIBLE,并调用schedule()

3. 临时内存映射

3.1 可以用在中断处理函数和可延迟函数的内部,从不阻塞。因为临时内存映射是固定内存映射的一部分,一个地址固定给一个内核成分使用。

3.2 每个CPU都有自己的一个13个窗口(一个线性地址及页表项)的集合。
enum km_type {
    KM_BOUNCE_READ,
    KM_SKB_SUNRPC_DATA,
    KM_SKB_DATA_SOFTIRQ,
    KM_USER0,
    KM_USER1,
    KM_BIO_SRC_IRQ,
    KM_BIO_DST_IRQ,
    KM_PTE0,
    KM_PTE1,
    KM_IRQ0,
    KM_IRQ1,
    KM_SOFTIRQ0,
    KM_SOFTIRQ1,
    KM_TYPE_NR
};

所有固定映射的固定线性地址
enum fixed_addresses {
    FIX_HOLE,
    FIX_VSYSCALL,
        ....
#ifdef CONFIG_HIGHMEM
    FIX_KMAP_BEGIN,    /* reserved pte's for temporary kernel mappings */
    FIX_KMAP_END = FIX_KMAP_BEGIN+(KM_TYPE_NR*NR_CPUS)-1,
#endif
        .......
    __end_of_permanent_fixed_addresses,
    /* temporary boot-time mappings, used before ioremap() is functional */
#define NR_FIX_BTMAPS    16
    FIX_BTMAP_END = __end_of_permanent_fixed_addresses,
    FIX_BTMAP_BEGIN = FIX_BTMAP_END + NR_FIX_BTMAPS - 1,
    FIX_WP_TEST,
    __end_of_fixed_addresses
};

3.3 注意 fixed_addresses 的地址从上至下是倒着的,FIX_HOLE的地址等于 0xfffff000,是一个洞
#define __fix_to_virt(x)    (FIXADDR_TOP - ((x) << PAGE_SHIFT))
#define __FIXADDR_TOP    0xfffff000

-------------------------

VMALLOC_RESERVE和896M

LINUX 内核虚拟地址空间到物理地址空间一般是固定连续影射的。

假定机器内存为512M,
从3G开始,到3G + 512M 为连续固定影射区。zone_dma, zone_normal为这个区域的。固定影射的VADDR可以直接使用(get a free page, then use pfn_to_virt()等宏定义转换得到vaddr)或用kmalloc等分配. 这样的vaddr的物理页是连续的。得到的地址也一定在固定影射区域内。

如果内存紧张,连续区域无法满足,调用vmalloc分配是必须的,因为它可以将物理不连续的空间组合后分配,所以更能满足分配要求。vmalloc可以映射高端页框,也可以映射底端页框。vmalloc的作用只是为了提供逻辑上连续的地址。。。

但vmalloc分配的vaddr一定不能与固定影射区域的vaddr重合。因为vaddr到物理页的影射同时只能唯一。所以vmalloc得到的 vaddr要在3G + 512m 以上才可以。也就是从VMALLOC_START开始分配。 VMALLOC_START比连续固定影射区大最大vaddr地址还多8-16M(2*VMALLOC_OFFSET)--有个鬼公式在

#define VMALLOC_OFFSET   8*1024
#define VMALLOC_START   (high_memory - 2*VMALLOC_OFFSET) & ~(VMALLOC_OFFSET-1)

high_memory 就是固定影射区域最高处。

空开8-16M做什么? 为了捕获越界的mm_fault.
同样,vmalloc每次得到的VADDR空间中间要留一个PAGE的空(空洞),目的和上面的空开一样。你vmalloc(100)2次,得到的2个地址中间相距8K。
如果连续分配无空洞,那么比如
p1=vmalloc(4096);
p2=vmalloc(4096);
如果p1使用越界到p2中了,也不会mm_falut. 那不容易debug.

下面说明VMALLOC_RESERVE和896M的问题。

上面假设机器物理512M的case. 如果机器有1G物理内存如何是好?那vmalloc()的vaddr是不是要在3G + 1G + 8M 空洞以上分配?超过寻址空间了吗。
这时,4G 下面保留的VMALLOC_RESERVE 128m 就派上用场了。
也就是说如果物理内存超过896M, high_memory也只能在3G + 896地方。可寻址空间最高处要保留VMALLOC_RESREVE 128M给vmalloc用。

所以这128M的VADDR空间是为了vmalloc在物理超过了896M时候使用。如果物理仅仅有512M, 一般使用不到。因为VMALLOC_START很低了。如果vmalloc太多了才会用到。

high_memory在arch/i386/kernel, mm的初始化中设置。根据物理内存大小和VMALLOC_RESERVE得到数值.

所以说那128M的内核线性地址仅仅是为了影射1G以上的物理内存的不对的。如果物理内存2G,1G以下的vmalloc也用那空间影射。总之,内核的高端线性地址是为了访问内核固定映射以外的内存资源

看vmalloc分配的东西可以用

show_vmalloc()
{
struct vm_struct **p, *tmp;

for(p = &vmlist; (tmp = *p); p = &tmp->next) {
   printk("%p %p %d\n", tmp, tmp->addr, tmp->size

}
}

用户空间当然可以使用高端内存,而且是正常的使用,内核在分配那些不经常使用的内存时,都用高端内存空间(如果有),所谓不经常使用是相对来说的,比如内核的一些数据结构就属于经常使用的,而用户的一些数据就属于不经常使用的。

用户在启动一个应用程序时,是需要内存的,而每个应用程序都有3G的线性地址,给这些地址映射页表时就可以直接使用高端内存。

而且还要纠正一点的是:那128M线性地址不仅仅是用在这些地方的,如果你要加载一个设备,而这个设备需要映射其内存到内核中,它也需要使用这段线性地址空间来完成,否则内核就不能访问设备上的内存空间了。

总之,内核的高端线性地址是为了访问内核固定映射以外的内存资源

实际上高端内存是针对内核一段特殊的线性空间提出的概念,和实际的物理内存是两码事。进程在使用内存时,触发缺页异常,具体将哪些物理页映射给用户进程是内核考虑的事情。在用户空间中没有高端内存这个概念

时间: 2024-12-05 02:30:06

Linux 存储管理2——内存管理的相关文章

Linux下C++内存管理的指针传递详解

指针参数是如何传递内存的? 如果函数的参数是一个指针,不要指望用该指针去申请动态内存.示例7-4-1中,Test函数的语句 GetMemory(str, 200)并没有使str获得期望的内存,str依旧是NULL,为什么? void GetMemory(char *p, int num) { p = (char *)malloc(sizeof(char) * num); } void Test(void) { char *str = NULL; GetMemory(str, 100); // s

Linux服务器中内存管理学习笔记

前面说了,Linux MM系统细节非常多,自己在探究的时候,也是尝试尽量抓住主线,这里也只能抽取了一些"主线剧情"介绍,其中还可以扩展出很多细节,看客感兴趣可以自己深究,后续如果兴趣还在,我也还会继续写出来.内核版本如果没有特别说明,就是使用2.6.33版本. 1. 物理内存组织 先声明一下,这里说的Linux都是运行Intel X86架构的.从80386开始,为了更好支持内存管理.虚拟内存技术,x86架构开始支持处理器的分页模式(分页是基于分段).系统将内存分为一个个固定大小的块,称

Linux下进程内存管理之malloc和sbrk

之前自己突发兴趣想写一下malloc函数,顺便了解一下进程的内存管理.在写的过程中发现其实malloc只不过是通过调用Linux下的sbrk函数来实现内存的分配,只是在sbrk之上加了一层对所分配的内存的管理罢了,而sbrk以及brk是实现从虚拟内存到内存的映射的.在实际动手写之前先来了解一下Linux下一个进程的内存空间分配. 进程内存空间分配 Linux下每个进程所分配的虚拟内存空间是3G,但实际使用过程中不可能也没有必要为一个进程分配如此大的空间,毕竟内存是很宝贵的资源.当一个进程执行的时

Linux编程C++内存管理的指针传递详解

指针参数是如何传递内存的? 如果函数的参数是一个指针,不要指望用该指针去申请动态内存.示例7-4-1中,Test函数的语句GetMemory(str, 200)并没有使str获得期望的内存,str依旧是NULL,为什么? 1 2 3 4 5 6 7 8 9 10 void GetMemory(char *p, int num) { p = (char *)malloc(sizeof(char) * num); } void Test(void) { char *str = NULL; GetMe

Linux高端内存管理之永久内核映射

inux高端内存管理之永久内核映射 与直接映射的物理内存末端.高端内存的始端所对应的线性地址存放在high_memory变量中,在x86体系结构上,高于896MB的所有物理内存的范围大都是高端内存,它并不会永久地或自动地映射到内核地址空间,尽管x86处理器能够寻址物理RAM的范围达到4GB(启用PAE可以寻址到64GB).一旦这些页被分配,就必须in射到内核的逻辑地址空间上.在x86上,高端内存中的页被映射到3GB-4GB. 内核可以采用三种不同的机制将页框映射到高端内存:分别叫做永久内核映射.

Linux堆内存管理深入分析(上)

Linux堆内存管理深入分析 (上半部) 作者:走位@阿里聚安全 0 前言 近年来,漏洞挖掘越来越火,各种漏洞挖掘.利用的分析文章层出不穷.从大方向来看,主要有基于栈溢出的漏洞利用和基于堆溢出的漏洞利用两种.国内关于栈溢出的资料相对较多,这里就不累述了,但是关于堆溢出的漏洞利用资料就很少了.鄙人以为主要是堆溢出漏洞的门槛较高,需要先吃透相应操作系统的堆内存管理机制,而这部分内容一直是一个难点.因此本系列文章主要从Linux系统堆内存管理机制出发,逐步介绍诸如基本堆溢出漏洞.基于unlink的堆溢

Linux堆内存管理深入分析(上)

0 前言 近年来,漏洞挖掘越来越火,各种漏洞挖掘.利用的分析文章层出不穷.从大方向来看,主要有基于栈溢出的漏洞利用和基于堆溢出的漏洞利用两种.国内关于栈溢出的资料相对较多,这里就不累述了,但是关于堆溢出的漏洞利用资料就很少了.鄙人以为主要是堆溢出漏洞的门槛较高,需要先吃透相应操作系统的堆内存管理机制,而这部分内容一直是一个难点.因此本系列文章主要从Linux系统堆内存管理机制出发,逐步介绍诸如基本堆溢出漏洞.基于unlink的堆溢出漏洞利用.double free.use-after-free等

Linux内存管理学习笔记--概述

随着要维护的服务器增多,遇到的各种稀奇古怪的问题也会增多,要想彻底解决这些"小"问题往往需要更深的Linux方面的知识.越专业.分工越细的工程师,在这方面的要求也就越高.这次,对MySQL Swap的问题的探索过程,就一不小心掉进了Linux Memory Managemant(Linux MM)的研究中去了,爬了很久才出来,这里做一个系列笔记. 笔记中很多内容都是参考<Understanding the Linux Kernel, 3rd Edition>.Linux S

Linux 内存管理学习笔记

Linux内存采用保护模式的flat模型,即绕过了Intel的段式内存管理,体现在所有进程的段选择子均指向GDT,而GDT中有用的只有四项:  代码如下 复制代码 .quad 0x00cf9a000000ffff /* 0x10 kernel 4GB code at 0x00000000 */ .quad 0x00cf92000000ffff /* 0x18 kernel 4GB data at 0x00000000 */ .quad 0x00cffa000000ffff /* 0x23 use