Ubuntu从17.10开始不再官方支持32位(i386)架构(严格的说是从18.04开始的,因为17.10不支持32位的PC版,但是支持32位的SERVER版,但是偶数稳定版确实是从18.04开始的),只支持64位(amd64)架构,这是因为随着时间的推移,64位系统变得越来越普遍,并且可以更好的利用现代计算机的硬件性能和内存容量,此外,随着软件的发展和更新,许多应用程序都不再提供32位版本,而只提供64位版本,这也促使ubuntu停止更新32位架构。尽管如此,ubuntu18.04仍然提供了32位版本的软件包和库,以便支持老旧系统和应用程序。镜像下载链接:
Index of /ubuntu-releases/18.04.6/ | 清华大学开源软件镜像站 | Tsinghua Open Source Mirror
地址类型
Linux系统有物理地址,虚拟地址,用户虚拟地址,总线地址,内核逻辑地址,内核虚拟地址之分,区别如下:
物理地址,把内存当成一个大的字节数组,物理地址是对数组中的每个字节进行编址得到的结果,物理地址有32位和64位的。
虚拟地址,在CPU使用MMU的体系架构上,CPU发出的地址需要经过MMU翻译后才能变成物理地址,CPU发出的这个未经MMU翻译的地址就是虚拟地址,虚拟地址包括用户虚拟地址,内核虚拟地址和内核逻辑地址。
用户虚拟地址,这是用户空间程序所能看到的地址,用户虚拟地址可以是32位的,也可以是64位的,取决于体系架构,Linux下每个进程都有自己的虚拟地址空间。
总线地址,该地址在外围总线和内存之间使用,通常总线地址与处理器使用的物理地址相同,但也不是必须的,如果体系结构提供了IOMMU单元,则它实现总线第和物理内存之间的重新映射,此时的总线地址就不等于物理地址了。
内核逻辑地址,内核逻辑地址组成了内核的常规地址空间,它是一种虚拟地址,在32位系统中,由于虚拟地址空间有限,内核逻辑地址只映射了部内存,剩下的物理内存作为高端内存映射到内核虚拟地址。在大多数体系结构中,内核逻辑地址和物理地址的区别,仅仅在于他们之间存在一个固定的偏移量。正因为内核逻辑地址和物理地址之间的转换非常简单,内核逻辑地址常常被是为物理地址,但本质上是有区别的。kmalloc和page_address(低端内存,高端内存PAGE一般是临时映射才有内核虚拟地址)返回的就是内核逻辑地址。
内核虚拟地址,内核虚拟地址和内核逻辑地址的相同之处在于,它们都将内核空间的地址映射到物理地址上,内核虚拟地址与物理地址的映射不必是线性的和一对一的,而这恰恰是内核逻辑地址的特点,所有的内核逻辑地址都是内核虚拟地址,但是许多的内核虚拟地址不是逻辑地址。比如,vmalloc分配的是内核虚拟地址,kmap映射高端内存,返回的也是一个内核虚拟地址。内核逻辑地址和物理地址之间可以通过page_to_pfn/pfn_to_page/__va/__pa等宏转化,但是内核虚拟地址不可以,因为它和物理地址没有线性关系,只能反查页表。
虚拟地址空间
内核的架构是由内核自身决定的,和发行版无关,之所以提到UBUNTU是因为后面的实验是在UBUNTU环境下进行的。Linux内核在 32位和64位架构上有着不同的地址空间范围,在32位架构中,地址空间一共32位,从[0x00000000,0xFFFFFFFF],总共是4GB,这个地址空间被分成用户空间和内核空间两个部分,不同的架构划分方式有所不同,通常是按照3G用户1G内核的分法。
相比之下,在64位架构中,虚拟地址空间范围为[0x0000000000000000,0xFFFFFFFFFFFFFFFF],这个范围如此之大,以至于当前没有任何应用需要这么大的空间,所以实际上这个地址空间并不会被占满,而是用户空间和内核空间分别占用一部分,剩下的保留不用。虚拟地址空间的位数和MMU相关,当前主流的4级页表下,虚拟空间是9-9-9-9-12结构,所以一共占用48BIT,当前的主流实现是,64位地址空间,实际使用48位,其中bit47用于区分内核空间还是用户空间,操作系统使用低47访问内核空间或者用户空间,高17位作为模式扩展(全0或者全1,代表用户态或者内和核态),所以4级页表实际用到的地址空间为[0x0000000000000000,0x00007FFFFFFFFFFF](用户态)和[0xFFFF800000000000, 0xFFFFFFFFFFFFFFFF](内核态)。其余地址空间都是保留不用的。如下图所示:
地址映射范围:
同理,五级页表下,地址空间的位数为9-9-9-9-9-12结构,一共57位,BIT56用于区分内核模式还是用户模式,低56位用于模式内部寻址。
用户空间和内核空间之间的空洞是非典型(non-canonical)的用户空间定义。
huge, still almost 64 bits wide hole of non-canonical virtual memory addresses up to the -128 TB starting offset of kernel mappings.
虽然名义上非典型空间为用户区,但是实际上用户态不能用,进程的current->mm->task_size限制了最大可用的用户空间的范围,在进程启动时被初始化为TASK_SIZE:
而TASK_SIZE的的定义取决于也表级数的定义,当四级页表时,TASK_SIZE为1<<47-PAGE_SIZE,恰好为0x7FFFFFFFF000.
关于从BIT63到访问虚拟空间的MSB开始为内核/用户标志扩展的说明,可以参考Documentation/x86/x86_64/mm.rst中的下述描述:
实际运行用户态用例查看
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void)
{
while(1)
{
printf("%s line %d, helloworld.\n", __func__, __LINE__);
sleep(1);
}
return 0;
}
测试发现用户态地址空间确实和上面的分析吻合。
内核地址空间呢?内核地址空间无法通过pmap或者/proc/节点查看,不用急,我们还有/proc/kallsyms节点,它保留了内核中的符号信息,接取一小段分析一下,发现却是落到了上图中得内核地址空间范围内,但是似乎只局限在某个狭窄的区域,并没有从0xFFFF800000000000开始。
实际上,查看内核文档Documentation/x86/x86_64/mm.rst得知,内核代码段仅仅占用了内核空间的一小部分,剩下的部分也都各有所属,被内核的其它机制瓜分了,为了将来拓展内核功能,有些地址空间被设置为保留。
在4.14内核上的描述更简洁清晰一些:
图中可以看到,0xffffffff80000000开始的地址空间确实属于内核代码和数据的地盘,和上面分析/proc/kallsyms结论是吻合的。
物理地址空间
UBUNTU下,可以通过如下几个命令查看物理地址空间分布
- ls -l /sys/firmware/memmap/
- lsmem
- cat /proc/iomem
以lsmem输出为例,我的笔记本主内存有8G,从lsmem输出来看,被分成两段,第一段2.3G,物理地址范围在0x0000000000000000-0x000000008fffffff,另一段是5.8G,范围为0x0000000100000000-0x000000026fffffff。
并且0x000000008fffffff + 1 + (0x000000026fffffff-0x0000000100000000 + 1) = 0x200000000=8G.
完美吻合实际物理内存大小。
PAGE_OFFSET
物理地址与线性地址之间的位移量,在Linux代码中就叫做PAGE_OFFSET, 在32位的内核中,这个值一般是0xc0000000.PAGE_OFFSET 代表的是内核空间和用户空间对虚拟地址空间的划分,不同的体系结构定义和值都不同。比如在32位系统中3G-4G属于内核使用的内存空间,所以 PAGE_OFFSET = 0xC0000000。在X86-64架构下是ffff880000000000。内核程序可以可以访问从PAGE_OFFSET 之后的内存。
可以看到,在开启CONFIG_DYNAMIC_MEMORY_LAYOUT(通过CONFIG_RANDOMIZE_MEMORY控制)的情况下,PAGE_OFFSET的值是动态变化的,取自于page_offset_base变量。
我们可以将一个PAGE的物理地址和内核线性地址的偏移打印出来,如果不出意外,应该和PAGE_OFFSET相等。下图可以看到,计算得到的OFFSET和PAGE_OFFSET他们确实是一致的。
page offset 是线性地址和物理地支之间的转换因子
CONFIG_DYNAMIC_MEMORY_LAYOUT打开情况下:
上图中Physical Memory到Virtual Memory的映射叫做直接映射,它是将物理地址的0地址,直接映射到虚拟内存中的page_offset_base偏移处,通过程序反查内核页表,定位从page_offset_base开始的10个PAGE映射页面可以看出,物理PFN从0递增到9。
也就是说,PFN号为0的物理页面的内核逻辑地址,就是PAGE_OFFSET,有如下等式:
page_address(pfn_to_page(0)) == page_offset_base
但是经过测试,系统中NODE中实际可用的PFN从1号开始,似乎预留了物理页面的前4K。
如果不设置CONFIG_DYNAMIC_MEMORY_LAYOUT,则PAGE_OFFSET为内核设置的固定值:
DUMP系统的PAGE TABLE,起始于0xffff888000000000.
关于几个地址的随机化实现原理,可以参考这篇文章的分析:
Linux内核地址空间随机化ASLR的几种实现方法_papaofdoudou的博客-CSDN博客
struct page 数组起始地址vmemmap
vmemmap就是用来存放稀疏内存的page结构体的数据的虚拟地址空间,其地址空间并不存在于内核逻辑地址空间, 而是内核虚拟地址空间,所以vmemmap地址无法通过virt_to_page/page_to_pfn进行转换。
vmemmap是struct page数组的起始地址,也就是说vmemmap代表的是pfn 0的struct page结构体。并且,通过HACK页表得知,vmemmap的映射为大页映射,也就是PMD 2M映射,如下图所示:
由于地址空间随机化的作用,每次重启系统,vmemmap的地址数值会发生变化,但是规律始终不变。
vmemmap所在的区域属于内核虚拟地址空间,而非内核逻辑地址空间,所以可以在需要的时候再做映射。这个特点非常重要,因为在默认情况下只有物理内存有对应的struct page映射,而对于外设内存,比如显存等等,是没有struct page对应的,所以内核中针对DMA CPU内存和设备内存的映射分成两套API,分别是map_sg和map_resource,设备内存由于没有struct page对应,只能被看成一种资源去做映射。这会在某些情况下带来一些麻烦,典型的是PCIE设备之间P2P访问,内核的P2P DMA实现调用的是map_sg之类的接口,但是map_sg接口需要存储有struct page对应,这种矛盾的解决,就是通过扩展vmemmap数组,将设备内存也纳入到vmemmap数组的管理,为了保证vmemmap开始的内核虚拟地址空间足够映射连同设备资源在内的所有系统物理内存,vmemmap区域的大小是按照MAX_PHYSICAL_BIT的大小确定的,关于这一点的分析,可以参考博客:
PCIE体系结构基础和Linux PCI设备注册过程的实现_linux驱动 pcie设备为何不需要注册-CSDN博客