Linux 下内存空间分配、物理地址与虚拟地址映射
一、Linux 内核动态内存分配与释放
1.1 kmalloc 函数
Kmalloc 分配的是连续的物理地址空间。如果需要连续的物理页,可以使用此函数,这是内核中内存分配的常用方式,也是大多数情况下应该使用的内存分配方式。
传递给函数的最常用的标志是 GTP_ATOMIC 和 GTP_KERNEL。前面的标志表示进行不睡眠的高优先级分配。在中断处理程序和其他不能睡眠的代码段中需要。后面的标志可以睡眠,在没有持自旋锁的进程上下文中使用。此函数返回内核逻辑地址。
头文件: #include <linux/slab.h>
1.1.1 申请空间
1.1.2 释放内存空间
1.1.3 示例
1.2 vmalloc 函数
分配的空间是线性的,在物理地址上不连续!最多分配 1GB 的空间。
定义文件:\mm\vmalloc.c
头文件:#include <linux/vmalloc.h>
1.2.1 申请空间
参数:
unsigned long size :分配空间的大小
返回值:申请的空间首地址
1.2.2 释放空间
参数:
const void *addr:释放的空间首地址
1.2.3 示例
1.3 区别总结
二、 MMAP 驱动实现
2.1 应用层 mmap 函数介绍
mmap 函数用于将一个文件或者其它对象映射进内存,通过对这段内存的读取和修改,来实现对文件的读取和修改,而不需要再调用 read,write 等操作。
头文件:<sys/mman.h>
函数原型:
映射函数
(1) addr: 指定映射的起始地址,通常设为 NULL,由系统指定。
(2) length:映射到内存的文件长度。
(3) prot:映射的保护方式,可以是:
PROT_EXEC:映射区可被执行
PROT_READ:映射区可被读取
PROT_WRITE:映射区可被写入
PROT_NONE:映射区不能存取
(4) Flags:映射区的特性,可以是:
MAP_SHARED:写入映射区的数据会复制回文件,且允许其他映射该文件的进程共享。
MAP_PRIVATE:对映射区的写入操作会产生一个映射区的复制(copy_on_write),对此区域所做的修改不会写回原文件。
(5) fd:由 open 返回的文件描述符,代表要映射的文件。
(6) offset:以文件开始处的偏移量,必须是分页大小的整数倍,通常为 0,表示从文件头开始映射。
解除映射
功能:取消参数 start 所指向的映射内存,参数 length 表示欲取消的内存大小。
返回值:解除成功返回 0,否则返回-1
2.2 Linux 内核的 mmap 接口
2.2.1 内核描述虚拟内存的结构体
Linux 内核中使用结构体 vm_area_struct 来描述虚拟内存的区域,其中几个主要成员如下:
该结构体定义在<linux/mm_types.h>头文件中。
该结构体的 vm_flags 成员赋值的标志为:VM_IO 和 VM_RESERVED。
其中:VM_IO 表示对设备 IO 空间的映射,M_RESERVED 表示该内存区不能被换出,在设备驱动中虚拟页和物理页的关系应该是长期的,应该保留起来,不能随便被别的虚拟页换出(取消)。
2.2.2 mmap 操作接口
在字符设备的文件操作集合(struct file_operations)中有 mmap 函数的接口。原型如下:
其中第二个参数 struct vm_area_struct *相当于内核找到的,可以拿来用的虚拟内存区间。mmap 内部可以完成页表的建立。
2.2.3 实现 mmap 映射
映射一个设备是指把用户空间的一段地址关联到设备内存上,当程序读写这段用户空间的地址时,它实际上是在访问设备。这里需要做的两个操作:
mmap 设备操作实例如下:
其中的 buf 就是在内核中申请的一段空间。使用 kmalloc 函数实现。
代码如下:
2.2.4 remap_pfn_range 函数
remap_pfn_range 函数用于一次建立所有页表。函数原型如下:
其中 vma 是内核为我们找到的虚拟地址空间,addr 要关联的是虚拟地址,pfn 是要关联的物理地址,size 是关联的长度是多少。
ioremap 与 phys_to_virt、virt_to_phys 的区别:
ioremap 是用来为 IO 内存建立映射的, 它为 IO 内存分配了虚拟地址,这样驱动程序才可以访问这块内存。
phys_to_virt 只是计算出某个已知物理地址所对应的虚拟地址。将内核物理地址转化为虚拟地址。
virt_to_phys :将内核虚拟地址转化为物理地址。
三、 IO 地址空间映射
3.1 ioremap 函数
ioremap 将一个 IO 地址空间映射到内核的虚拟地址空间上去,便于访问。
参数:
头文件: #include <linux/io.h>
功能: 将一个 IO 地址空间映射到内核的虚拟地址空间上去,便于访问;
实现: 对要映射的 IO 地址空间进行判断,低 PCI/ISA 地址不需要重新映射,也不允许用户将 IO 地址空间映射到正在使用的 RAM 中,最后申请一个 vm_area_struct 结构,调用 remap_area_pages 填写页表,若填写过程不成功则释放申请的 vm_area_struct 空间;
ioremap 依靠 __ioremap 实现,它只是在__ioremap 中以第三个参数为 0 调用来实现.
ioremap 是内核提供的用来映射外设寄存器到主存的函数,我们要映射的地址已经从 pci_dev 中读了出来(上一步),这样就水到渠成的成功映射了而不会和其他地址有冲突。映射完了有什么效果呢,我举个例子,比如某个网卡有 100 个寄存器,他们都是连在一块的,位置是固定的,假如每个寄存器占 4 个字节,那么一共 400 个字节的空间被映射到内存成功后,ioaddr 就是这段地址的开头(注意 ioaddr 是虚拟地址,而 mmio_start 是物理地址,它是 BIOS 得到的,肯定是物理地址,而保护模式下 CPU 不认物理地址,只认虚拟地址),ioaddr+0 就是第一个寄存器的地址,ioaddr+4 就是第二个寄存器地址(每个寄存器占 4 个字节),以此类推,我们就能够在内存中访问到所有的寄存器进而操控他们了。
3.2 iounmap 函数
取消 ioremap 映射的空间。
3.3 补充说明
3.4 示例
四、linux 内核 readl()和 writel()函数
writel()往内存映射的 I/O 上写入 32 位数据 (4 字节)。
示例:
五、MMU(内存管理单元)
MMU 是中央处理器(CPU)中用来管理虚拟存储器、物理存储器的控制线路,同时也负责虚拟地址映射为物理地址,以及提供硬件机制的内存访问授权,多用户多进程操作系统。它一个与软件密切相关的硬件部件,也是理解 Linux 等操作系统内核机制的最大障碍之一。不搞清楚 MMU 原理会使编程思想停留在单片机与无 OS 的时代。
5.1 MMU 历史概述
许多年以前,当人们还在使用 DOS 或是更古老的操作系统的时候,计算机的内存还非常小,一般都是以 K 为单位进行计算,相应的,当时的程序规模也不大,所以内存容量虽然小,但还是可以容纳当时的程序。但随着图形界面的兴起还有用户需求的不断增大,应用程序的规模也随之膨胀起来,终于一个难题在程序员的面前,那就是应用程序太大以至于内存容纳不下该程序,通常解决的办法是把程序分割成许多称为覆盖块(overlay)的片段。覆盖块 0 首先运行,结束时他将调用另一个覆盖块。虽然覆盖块的交换是由 OS 完成的,但是必须先由程序员把程序先进行分割,这是一个费时费力的工作,而且相当枯燥。人们必须找到更好的办法从根本上解决这个问题。不久人们找到了一个办法,这就是虚拟存储器(virtual memory)。虚拟存储器的基本思想是程序,数据,堆栈的总的大小可以超过物理存储器的大小,操作系统把当前使用的部分保留在内存中,而把其他未被使用的部分保存在磁盘上。
比如对一个 16MB 的程序和一个内存只有 4MB 的机器,操作系统通过选择,可以决定各个时刻将哪 4M 的内容保留在内存中,并在需要时在内存和磁盘间交换程序片段,这样就可以把这个 16M 的程序运行在一个只具有 4M 内存机器上了。而这个 16M 的程序在运行前不必由程序员进行分割。
5.2 相关概念介绍
——地址范围、虚拟地址映射为物理地址以及分页机制
任何时候,计算机上都存在一个程序能够产生的地址集合,我们称之为地址范围。这个范围的大小由 CPU 的位数决定,例如一个 32 位的 CPU,它的地址范围是 0~0xFFFFFFFF (4G),而对于一个 64 位的 CPU,它的地址范围为 0~0xFFFFFFFFFFFFFFFF (16E)这个范围就是我们的程序能够产生的地址范围,我们把这个地址范围称为虚拟地址空间,该空间中的某一个地址我们称之为虚拟地址。与虚拟地址空间和虚拟地址相对应的则是物理地址空间和物理地址,大多数时候我们的系统所具备的物理地址空间只是虚拟地址空间的一个子集。这里举一个最简单的例子直观地说明这两者,对于一台内存为 256M 的 32bit x86 主机来说,它的虚拟地址空间范围是 0~0xFFFFFFFF(4G),而物理地址空间范围是 0x00000000~0x0FFFFFFF(256M)。
在没有使用虚拟存储器的机器上,地址被直接送到内存总线上,使具有相同地址的物理存储器被读写;而在使用了虚拟存储器的情况下,虚拟地址不是被直接送到内存地址总线上,而是送到存储器管理单元 MMU,把虚拟地址映射为物理地址。
大多数使用虚拟存储器的系统都使用一种称为分页(paging)机制。虚拟地址空间划分成称为页(page)的单位,而相应的物理地址空间也被进行划分,单位是页帧(frame).页和页帧的大小必须相同。
在这个例子中我们有一台可以生成 32 位地址的机器,它的虚拟地址范围从 0~0xFFFFFFFF(4G),而这台机器只有 256M 的物理地址,因此他可以运行 4G 的程序,但该程序不能一次性调入内存运行。这台机器必须有一个达到可以存放 4G 程序的外部存储器(例如磁盘或是 FLASH),以保证程序片段在需要时可以被调用。在这个例子中,页的大小为 4K,页帧大小与页相同——这点是必须保证的,因为内存和外围存储器之间的传输总是以页为单位的。对应 4G 的虚拟地址和 256M 的物理存储器,他们分别包含了 1M 个页和 64K 个页帧。
寻址空间一般指的是 CPU 对于内存寻址的能力。通俗地说,就是能最多用到多少内存的一个问题。数据在存储器(RAM)中存放是有规律的 ,CPU 在运算的时候需要把数据提取出来就需要知道数据在那里 ,这时候就需要挨家挨户的找,这就叫做寻址,但如果地址太多超出了 CPU 的能力范围,CPU 就无法找到数据了。 CPU 最大能查找多大范围的地址叫做寻址能力 ,CPU 的寻址能力以字节为单位 ,如 32 位寻址的 CPU 可以寻址 2 的 32 次方大小的地址也就是 4G,这也是为什么 32 位的 CPU 最大能搭配 4G 内存的原因 ,再多的话 CPU 就找不到了。
5.3 虚拟地址与物理地址介绍
1)虚拟地址/物理地址
如果处理器没有 MMU,CPU 内部执行单元产生的内存地址信号将直接通过地址总线发送到芯片引脚,被内存芯片接收,这就是物理地址(physical address),简称 PA。英文 physical 代表物理的接触,所以 PA 就是与内存芯片 physically connected 的总线上的信号。
如果 MMU 存在且启用,CPU 执行单元产生的地址信号在发送到内存芯片之前将被 MMU 截获,这个地址信号称为虚拟地址(virtual address),简称 VA,MMU 会负责把 VA 翻译成另一个地址,然后发到内存芯片地址引脚上,即 VA 映射成 PA
软件上 MMU 对用户程序不可见,在启用 MMU 的平台上(没有 MMU 不必说,只有物理地址,不存在虚拟地址),用户 C 程序中变量和函数背后的数据/指令地址等都是虚拟地址,这些虚拟内存地址从 CPU 执行单元发出后,都会首先被 MMU 拦截并转换成物理地址,然后再发送给内存。也就是说用户程序运行*pA =100;"这条赋值语句时,假设 debugger 显示指针 pA 的值为 0x30004000(虚拟地址),但此时通过硬件工具(如逻辑分析仪)侦测到的 CPU 与外存芯片间总线信号很可能是另外一个值,如 0x8000(物理地址)。
当然对一般程序员来说,只要上述语句运行后 debugger 显示 0x30004000 位置处的内存值为 100 就行了,根本无需关心 pA 的物理地址是多少。但进行 OS 移植或驱动开发的系统程序员不同,他们必须清楚软件如何在幕后辅助硬件 MMU 完成地址转换。
暂不探讨这种复杂机制的历史原因,很多人学习 MMU 时,都迷失于对一些相关发散问题的无休止探究,我们暂时抽身出来,用一句话做阶段性交待,"所有计算机科学中的问题都能通过增加一个中间转换层来解决"。
2 ) 页/页帧/页表/页表项(PTE)
MMU 是负责把虚拟地址映射为物理地址,但凡"映射"都要解决两个问题:映射的最小单位(粒度)和映射的规则。
MMU 中 VA 到 PA 映射的最小单位称为页(Page),映射的最低粒度是单个虚拟页到物理页,页大小通常是 4K,即一次最少要把 4K 大小的 VA 页块整体映射到 4K 的 PA 页块(从 0 开始 4K 对齐划分页块),页内偏移不变,如 VA 的一页 0x30004000~0x30004fff 被映射到 PA 的一页 0x00008000~0x00008fff,当 CPU 执行单元访问虚拟地址 0x30004008,实际访问的物理地址是 0x00008008(0x30004008 和 0x00008008 分别位于虚实两套地址空间,互不相干,不存在重叠和冲突)。以页为最小单位,就是不能把 VA 中某一页划分成几小块分别映射到不同 PA,也不能把 VA 中属于不同页的碎块映射到 PA 某一页的不同部分,必须页对页整体映射。
页帧(Page Frame)是指物理内存中的一页内存,MMU 虚实地址映射就是寻找物理页帧的过程,对这个概念了解就可以了。
MMU 软件配置的核心是页表(Page Table),它描述 MMU 的映射规则,即虚拟内存哪(几)个页映射到物理内存哪(几)个页帧。页表由一条条代表映射规则的记录组成,每一条称为一个页表条目(Page Table Entry,即 PTE),整个页表保存在片外内存,MMU 通过查找页表确定一个 VA 应该映射到什么 PA,以及是否有权限映射。
但如果 MMU 每次地址转换都到位于外部内存的页表上查找 PTE,转换速度就会大大降低,于是出现了 TLB。
TLB (Translation Lookaside Buffers)即转换快表,又简称快表,可以理解为 MMU 内部专用的存放页表的 cache(快速缓冲贮存区),保存着最近使用的 PTE 乃至全部页表。MMU 接收到虚拟地址后,首先在 TLB 中查找,如果找到该 VA 对应的 PTE 就直接转换,找不到再去外存页表查找,并置换进 TLB。TLB 属于片上 SRAM,访问速度快,通过 TLB 缓存 PTE 可以节省 MMU 访问外存页表的时间,从而加速虚实地址转换。TLB 和 CPU cache 的工作原理一样,只是 TLB 专用于为 MMU 缓存页表。
3) MMU 的内存保护功能
既然所有发往内存的地址信号都要经过 MMU 处理,那让它只单单做地址转换,岂不是浪费了这个特意安插的转换层,显然它有能力对虚地址访问做更多的限定(就像路由器转发网络包的同时还能过滤各种非法访问),比如内存保护。可以在 PTE 条目中预留出几个比特,用于设置访问权限的属性,如禁止访问、可读、可写和可执行等。设好后,CPU 访问一个 VA 时,MMU 找到页表中对应 PTE,把指令的权限需求与该 PTE 中的限定条件做比对,若符合要求就把 VA 转换成 PA,否则不允许访问,并产生异常。
4) 多级页表
虚拟地址由页号和页内偏移组成。前面说过 MMU 映射以页为最小单位,假设页大小为 4K(212),那么无论页表怎样设置,虚拟地址后 12 比特与 MMU 映射后的物理地址后 12 比特总是相同,这不变的比特位就是页内偏移。为什么不变? 比如: 把搭积木想象成一种映射,不管怎么搭,也改变不了每块积木内部的原子排列。所谓以页为最小单位就是保持一部分不变作为最小粒度。
一个 32bits 虚拟地址,可以划分为 220 个内存页,如果都以页为单位和物理页帧随意映射,页表的空间占用就是 220*sizeof(PTE)*进程数(每个进程都要有自己的页表),PTE 一般占 4 字节,即每进程 4M,这对空间占用和 MMU 查询速度都很不利。问题是实际应用中不需要每次都按最小粒度的页来映射,很多时候可以映射更大的内存块。因此最好采用变化的映射粒度,既灵活又可以减小页表空间。具体说可以把 20bits 的页号再划分为几部分
简单说每次 MMU 根据虚拟地址查询页表都是一级级进行,先根据 PGD 的值查询,如果查到 PGD 的匹配,但后续 PMD 和 PTE 没有,就以 2(offset+pte+pmd)=1M 为粒度进行映射,后 20bits 全部是块内偏移,与物理地址相同。
5) 操作系统和 MMU
实际上 MMU 是为满足操作系统越来越复杂的内存管理而产生的。OS 和 MMU 的关系简单说:
a. 系统初始化代码会在内存中生成页表,然后把页表地址设置给 MMU 对应寄存器,使 MMU 知道页表在物理内存中的什么位置,以便在需要时进行查找。之后通过专用指令启动 MMU,以此为分界,之后程序中所有内存地址都变成虚地址,MMU 硬件开始自动完成查表和虚实地址转换。
b. OS 初始化后期,创建第一个用户进程,这个过程中也需要创建页表,把其地址赋给进程结构体中某指针成员变量。即每个进程都要有独立的页表。
c.用户创建新进程时,子进程拷贝一份父进程的页表,之后随着程序运行,页表内容逐渐更新变化
6) 总结
相关概念讲完,VA 到 PA 的映射过程就一目了然:MMU 得到 VA 后先在 TLB 内查找,若没找到匹配的 PTE 条目就到外部页表查询,并置换进 TLB;根据 PTE 条目中对访问权限的限定检查该条 VA 指令是否符合,若不符合则不继续,并抛出 exception 异常;符合后根据 VA 的地址分段查询页表,保持 offset(广义)不变,组合出物理地址,发送出去。
在这个过程中,软件的工作核心就是生成和配置页表。
ARM 系列的 MMU
ARM 出品的 CPU,MMU 作为一个协处理器存在。根据不同的系列有不同搭配。需要查询 DATASHEET 才可知道是否有 MMU。如果有的话,一定是编号为 15 的协处理器。可以提供 32BIT 共 4G 的地址空间。
ARM MMU 提供的分页机制有 1K/4K/64K 3 种模式。ARM-Linux 操作系统上分页使用的是 4K 模式。涉及的寄存器,全部位于协处理器 15。
ARM cpu 地址转换涉及三种地址:虚拟地址(VA,Virtual Address),变换后的虚拟地址(MVA,Modified Virtual Address),物理地址(PA,Physical Address)。没有启动 MMU 时,CPU 核心、cache、MMU、外设等所有部件使用的都是物理地址。启动 MMU 后,CPU 核心对外发出的是虚拟地址 VA,VA 被转换为 MVA 供 cache、MMU 使用,并再次被转换为 PA,最后使用 PA 读取实际设备。
版权声明: 本文为 InfoQ 作者【DS小龙哥】的原创文章。
原文链接:【http://xie.infoq.cn/article/b22d6c5ddefdf41e74b889450】。文章转载请联系作者。
评论