Linux内核地址空间划分

通常 32 位 Linux 内核地址空间划分 0~3G 为用户空间,3~4G 为内核空间。64 位内核地址空间划分是不同的。

Linux内核高端内存

当内核模块代码或线程访问内存时,代码中的内存地址都为逻辑地址,而对应到真正的物理内存地址,需要地址一对一的映射,如逻辑地址 0xc0000003 对应的物理地址为 0x3,0xc0000004 对应的物理地址为 0x4,… …,逻辑地址与物理地址对应的关系为

物理地址 = 逻辑地址 – 0xC0000000

逻辑地址 物理内存地址
0xc0000000 0x0
0xc0000001 0x1
0xc0000002 0x2
0xc0000003 0x3
0xe0000000 0x20000000
0xffffffff 0x40000000 

假设按照上述简单的地址映射关系,那么内核逻辑地址空间访问为0xc0000000 ~ 0xffffffff,那么对应的物理内存范围就为0x0 ~ 0x40000000,即只能访问 1G 物理内存。若机器中安装 8G 物理内存,那么内核就只能访问前 1G 物理内存,后面 7G 物理内存将会无法访问,因为内核的地址空间已经全部映射到物理内存地址范围 0x0 ~ 0x40000000。即使安装了 8G 物理内存,那么物理地址为 0x40000001 的内存,内核该怎么去访问呢?代码中必须要有内存逻辑地址的,0xc0000000 ~ 0xffffffff 的地址空间已经被用完了,所以无法访问物理地址 0x40000000 以后的内存。

显然不能将内核地址空间 0xc0000000 ~ 0xfffffff 全部用来简单的地址映射。

因此,Linux 又把物理页面划分为3 个区:

  • 专供 DMA 使用的 ZONE_DMA 区(小于 16MB);

  • 常规的 ZONE_NORMAL 区(大于 16MB 小于 896MB);

  • 内核不能直接映射的区 ZONE_HIGME 区(大于 896MB)。

以上每个区都用 struct zone_struct 结构来表示。

ZONE_HIGHMEM 即为高端内存,这就是内存高端内存概念的由来。

其中把 0~896M 区域为直接映射区,也即是虚拟内存中(3G~3G+896M)区域和物理内存的 0~896M 进行直接映射。由于虚拟内存中内核空间只有1G,因此还剩下的 128M 虚拟内存区域(3G+896M~4G)。

那么如内核是如何借助 128MB 高端内存地址空间是如何实现访问可以所有物理内存?

当内核想访问高于 896MB 物理地址内存时,从 0xF8000000 ~ 0xFFFFFFFF 地址空间范围内找一段相应大小空闲的逻辑地址空间,借用一会。借用这段逻辑地址空间,建立映射到想访问的那段物理内存(即填充内核 PTE 页面表),临时用一会,用完后归还。这样别人也可以借用这段地址空间访问其他物理内存,实现了使用有限的地址空间,访问所有所有物理内存。如下图。

例如内核想访问 2G 开始的一段大小为1MB的物理内存,即物理地址范围为0x80000000 ~ 0x800FFFFF。访问之前先找到一段 1MB 大小的空闲地址空间,假设找到的空闲地址空间为 0xF8700000 ~ 0xF87FFFFF,用这 1MB 的逻辑地址空间映射到物理地址空间 0x80000000 ~ 0x800FFFFF 的内存。映射关系如下:

逻辑地址 物理内存地址
0xF8700000 0x80000000
0xF8700001 0x80000001
0xF8700002 0x80000002
0xF87FFFFF 0x800FFFFF

当内核访问完 0x80000000 ~ 0x800FFFFF 物理内存后,就将 0xF8700000 ~ 0xF87FFFFF内核线性空间释放。这样其他进程或代码也可以使用 0xF8700000 ~ 0xF87FFFFF 这段地址访问其他物理内存。

从上面的描述,我们可以知道高端内存的最基本思想:借一段地址空间,建立临时地址映射,用完后释放,达到这段地址空间可以循环使用,访问所有物理内存。

看到这里,不禁有人会问:万一有内核进程或模块一直占用某段逻辑地址空间不释放,怎么办?若真的出现的这种情况,则内核的高端内存地址空间越来越紧张,若都被占用不释放,则没有建立映射到物理内存都无法访问了。

高端内存分布

在内核的虚拟地址空间的高端内存区中又分为三个区,分别是:非连续内存区、永久内核映射区、固定映射区。

  • 非连续内存区是为系统硬件中断处理和内核模块生产空间一次性准备用的。

  • 永久映射区是给系统底层空间分区和硬件及驱动准备的。

  • 固定映射区是为用户配置和应用软件运行提供可用空间准备的。

在图中,high_memory是高端内存区( ZONE_HIGHMEM )起始地址,VMALLOC 是非连续内存区。

在直接映射的物理页帧末尾与第一个内存区 VMALLOC_START 之间插入了一个 8MB(VMALLOC_OFFSET)的区间,这是一个安全区,目的是为了“捕获”对非连续区的非法访问。出于同样的理由,在其他非连续的内存区之间也插入了 4KB 大小的安全区。每个非连续内存区的大小都是 4096 的倍数。

在内核中,永久内核映射区和固定映射区大小一般都为 4MB,也就是分别用一个页表可以囊括其所包含地址范围,其他都给非连续内存区使用。不过如果物理内存大小小于 896MB 的情况下,内核并不会生成高端内存区,只会有 ZONE_DMA 和 ZONE_NORMAL 两个区。

我们知道,内核可使用的线性地址就只有1G大小( 0xC0000000 ~ 0xFFFFFFFF ),而用于 ZONE_DMA 和 ZONE_NORMAL 这两个区的映射已经花掉了 896MB 的线性地址空间,最后只剩下 128MB 用于映射高端内存,如果内存大于 1G,比如 2G(2048M)的情况下,高端内存区大小就为 1152MB,这个 128MB 大小的线性地址空间是完全不够直接映射高端内存的,所以对于高端内存的处理,linux 并不会直接映射,而是在需要的时候才进行映射,不需要的时候就释放映射,回收线性地址。

在初始化页表时,会对永久内核映射区和固定映射区分别进行初始化,但是都不会对他们进行映射处理,只有在需要使用时才会分配。

以上是虚拟内存中高端内存(3G+896M~4G)的分布情况,那么 ZONE_DMA 和 ZONE_NORMAL (3G~3G+896M)区域内存布局是什么样的呢?

内核启动后内核区域内存布局

一般的,内核启动会被加载到内存的 1MB 开始处,而普通配置的内核大小一般小于3MB,也就是说,内核镜像被加载内存 1MB~4MB 的地方,而为什么0MB~1MB 的内存内核不使用,因为这段内存一般是由 BIOS 使用和做一些硬件映射的。如下图:

  

在里面我们值得注意的就是 _end,它在代码里表明了内核镜像在内存中的结束地址,页表的初始化会先初始化未被内核使用的区域,最后再初始化内核使用的区域。

符号 _text 对应物理地址 0x00100000,表示内核代码的第一个字节的地址。内核代码的结束位置用另一个类似的符号 _etext 表示。内核数据被分为两组:初始化过的数据和未初始化过的数据。初始化过的数据在 _etext 后开始,在 _edata 处结束,紧接着是未初始化过的数据,其结束符号为 _end,这也是整个内核映像的结束符号。

图中出现的符号是由编译程序在编译内核时产生的。你可以在System.map 文件中找到这些符号的线性地址(或叫虚拟地址),System.map 是编译内核以后所创建的。

启用分页机制

当 Linux 启动时,首先运行在实模式下,随后就要转到保护模式下运行。

将 Linux 内核的映像转入内存中,并且做好了一些必要的准备后,CPU 就通过一条转移指令转到映象代码段开头的入口 startup_32, 从那里开始执行。

Linux 内核代码的入口点就是 arch/i386/kernel/head.S 中的 startup_32。(内核版本 2.4.16)。

内核映象的起点时 stext,也就是 _stext, 引导和解压缩以后的整个映象存放在内存中从 0x100000 也即是 1M 开始的区间。CPU 执行内核映象的入口startup_32 就在内核映象开头的地方,因此其物理地址也是 0x100000。

在正常运行时整个内核映象都应该在系统空间中,系统空间的地址映射时线性的、连续的,虚拟地址与物理地址间有个固定的转移,这就是 0xC0000000,也即是 3GB。所以,在连续内核映象时已经在所有的符号地址上加了一个偏移量 0xC0000000,这样 startup_32 虚拟地址就成了 0xC0100000。

在进入 startup_32 时都运行于保护模式下的段式寻址方式。段描述表中与__KERNEL_CS 和 __KERNEL_DS 相对应的描述项所提供的基地址都是0,所以实际产生的就是线性地址。

其中代码段寄存器 CS 已在进入 startup_32 之前设置成 __KERNEL_CS,数据段寄存器则尚未设置成 __KERNEL_DS。不过,虽然代码段寄存器已经设置成 __KERNEL_CS,从而 startup_32 的地址为 0xC0100000。但是在转入这个入口时使用的指令时 “ljmp 0x”100000” 而不是 “ljmp startup_32”,所以装入CPU中寄存器IP的地址是物理地址 0x100000 而不是虚拟地址0xC0100000。

这样 CPU 在进入 startup_32 以后就会继续以物理地址取指令。只要不在代码段中引用某个地址,例如向某个地址作绝对转移或者调用某个子程序,就可以一直这样运行下去,而与 CS 内容无关。另外,CPU 的中断已在进入 startup_32 之前关闭。

/* page table for 0-4MB for everybody */
extern unsigned long pg0[1024];pte_t pg1[1024];pgd_t swapper_pg_dir[1024];

在系统初始化的时候,内核就要创建内核页表 swapper_pg_dir 了。

struct mm_struct init_mm = INIT_MM(init_mm);#define INIT_MM(name) \
{                                \.mm_rb      = RB_ROOT,              \.pgd        = swapper_pg_dir,           \.mm_users   = ATOMIC_INIT(2),           \.mm_count   = ATOMIC_INIT(1),           \.mmap_sem   = __RWSEM_INITIALIZER(name.mmap_sem),   \.page_table_lock =  __SPIN_LOCK_UNLOCKED(name.page_table_lock), \.mmlist     = LIST_HEAD_INIT(name.mmlist),      \.cpu_vm_mask    = CPU_MASK_ALL,             \
}

内核启动过程中,存在一个实模式保护模式的切换过程。在 linux 启动的最初阶段,内核刚刚被装入内存时,分页功能还未启用,此时是直接存取物理地址的(或者说线性地址就等于物理地址)。但初始化完成后,内核也需要有自己的虚拟地址空间(1个G大小),该虚拟地址空间的地址映射关系,会被作为模版拷贝到其他进程的内核地址空间中。

临时内核页表只用来映射物理地址的前 8M 空间内容。目的是允许 CPU 在实模式(直接存取物理地址)和保护模式(根据虚拟地址映射)之间切换的过程中,都能对这前 8M 的地址进行访问。(假如内核使用的全部内存可以存放在 8M 的空间里,因为一个页表可以映射 4M 的地址,所以8M的空间需要两个页表,也就是需要两个页目录项。这两张页表我们称为临时内核页表 pg0 和 pg1。

从 startup_32 开始的汇编代码在 arch/i386/kernel/head.S,这就是初始化的第一阶段。

.org 0x1000
ENTRY(swapper_pg_dir).long 0x00102007.long 0x00103007.fill BOOT_USER_PGD_PTRS-2,4,0/* default: 766 entries */.long 0x00102007.long 0x00103007/* default: 254 entries */.fill BOOT_KERNEL_PGD_PTRS-2,4,0/** The page tables are initialized to only 8MB here - the final page* tables are set up later depending on memory size.*/
.org 0x2000
ENTRY(pg0).org 0x3000
ENTRY(pg1)/** empty_zero_page must immediately follow the page tables ! (The* initialization loop counts until empty_zero_page)*/.org 0x4000
ENTRY(empty_zero_page)/** Initialize page tables*/movl $pg0-__PAGE_OFFSET,%edi /* initialize page tables */movl $007,%eax     /* "007" doesn't mean with right to kill, butPRESENT+RW+USER */
2:    stosladd $0x1000,%eaxcmp $empty_zero_page-__PAGE_OFFSET,%edijne 2b

内核的这段代码执行时,因为页机制还没有启用,还没有进入保护模式,因此指令寄存器 EIP 中的地址还是物理地址,但因为 pg0 中存放的是虚拟地址(gcc 编译内核以后形成的符号地址都是虚拟地址),因此,“$pg0-__PAGE_OFFSET ”获得 pg0 的物理地址(__PAGE_OFFSET 为 0xC0000000,也即是 3GB),可见 pg0 存放在相对于内核代码起点为0x2000 的地方,即物理地址为 0x00102000,而pg1 的物理地址则为0x00103000。Pg0 和 pg1 这个两个页表中的表项则依次被设置为 0x007、0x1007、0x2007 等。其中最低的 3 位均为 1,表示这两个页为用户页,可写,且页的内容在内存中(参见下图)。所映射的物理页的基地址则为 0x0、0x1000、0x2000 等,也就是物理内存中的页面 0、1、2、3 等等,共映射2K 个页面,即 8MB 的存储空间。由此可以看出,Linux 内核对物理内存的最低要求为 8MB。紧接着存放的是 empty_zero_page 页(即零页),零页存放的是系统启动参数和命令行参数。

.org 0x1000
ENTRY(swapper_pg_dir).long 0x00102007.long 0x00103007.fill BOOT_USER_PGD_PTRS-2,4,0/* default: 766 entries */.long 0x00102007.long 0x00103007/* default: 254 entries */.fill BOOT_KERNEL_PGD_PTRS-2,4,0/** Enable paging*/
3:movl $swapper_pg_dir-__PAGE_OFFSET,%eaxmovl %eax,%cr3      /* set the page table pointer.. */movl %cr0,%eaxorl $0x80000000,%eaxmovl %eax,%cr0      /* ..and set paging (PG) bit */jmp 1f          /* flush the prefetch-queue */
1:movl $1f,%eaxjmp *%eax       /* make sure eip is relocated */
1:/* Set up the stack pointer */lss stack_start,%esp   // 将CPU的堆栈设置在 stack-start处

这段代码就是把页目录 swapper_pg_dir 的物理地址装入控制寄存器cr3,并把 cr0 中的最高位置成1,这就开启了分页机制。

但是,启用了分页机制,并不说明Linux 内核真正进入了保护模式,因为此时,指令寄存器 EIP 中的地址还是物理地址,而不是虚地址。“jmp 1f” 指令从逻辑上说不起什么作用,但是,从功能上说它起到丢弃指令流水线中内容的作用(这是 Intel 在 i386 技术资料中所建议的),因为这是一个短跳转,EIP 中还是物理地址。紧接着的 mov 和 jmp 指令把第 2 个标号为 1 的地址装入EAX 寄存器并跳转到那儿。在这两条指令执行的过程中, EIP 还是指向物理地址“1MB+某处”。因为编译程序使所有的符号地址都在虚拟内存空间中,因此,第2 个标号1 的地址就在虚拟内存空间的某处(PAGE_OFFSET+某处),于是,jmp 指令执行以后,EIP 就指向虚拟内核空间的某个地址,这就使 CPU 转入了内核空间,从而完成了从实模式到保护模式的平稳过渡。

然后再看页目录 swapper_pg_dir 中的内容。从前面的讨论我们知道 pg0 和pg1 这两个页表的起始物理地址分别为 0x00102000 和 0x00103000。页目录项的最低12位用来描述页表的属性。因此,在 swapper_pg_dir 中的第0 和第1 个目录项 0x00102007、0x00103007,就表示 pg0 和 pg1 这两个页表是用户页表、可写且页表的内容在内存。

接着,把 swapper_pg_dir 中的第 2~767 共 766 个目录项全部置为0。因为一个页表的大小为 4KB,每个表项占 4 个字节,即每个页表含有 1024 个表项,每个页的大小也为 4KB,因此这 768 个目录项所映射的虚拟空间为768×1024×4K=3G,也就是 swapper_pg_dir 表中的前 768 个目录项映射的是用户空间。最后,在第 768 和 769 个目录项中又存放 pg0 和 pg1 这两个页表的地址和属性,而把第 770~1023 共 254 个目录项置 0。这 256 个目录项所映射的虚拟地址空间为256×1024×4K=1G,也就是 swapper_pg_dir  表中的后 256 个目录项映射的是内核空间。

由此可以看出,在初始的页目录 swapper_pg_dir 中,用户空间和内核空间都只映射了开头的两个目录项,即 8MB 的空间,而且有着相同的映射,如图:

内核开始运行后运行在内核空间,那么,为什么把用户空间的低区(8M)也

进行映射,而且与内核空间低区的映射相同?

简而言之,是为了从实模式到保护模式的平稳过渡。具体地说,当 CPU 进入内核代码的起点 startup_32 后,是以物理地址来取指令的。在这种情况下,如果页目录只映射内核空间,而不映射用户空间的低区,则一旦开启页映射机制以后就不能继续执行了,这是因为,此时 CPU 中的指令寄存器 EIP 仍指向低区,仍会以物理地址取指令,直到以某个符号地址为目标作绝对转移或调用子程序为止。所以,Linux 内核就采取了上述的解决办法。

比如不映射用户空间的低区,内核代码的起点 startup_32 后,是以物理地址来取指令的,比如 eip 里面的地址为 0x0010010,当开启页面映射后,eip 里面的地址就要按照虚拟地址来处理了,这个时候要通过查页表进行把虚拟地址 0x0010010 转换为物理地址,这个时候没有映射用户空间的低区,找不到虚拟地址 0x0010010 到物理地址的映射,这个时候就会出现问题。

在 CPU 转入内核空间以后,应该把用户空间低区的映射清除掉。后面将会看到,页目录 swapper_pg_dir 经扩充后就成为所有内核线程的页目录。在内核线程的正常运行中,处于内核态的 CPU 是不应该通过用户空间的虚拟地址访问内存的。清除了低区的映射以后,如果发生 CPU 在内核中通过用户空间的虚拟地址访问内存,就可以因为产生页面异常而捕获这个错误。

经过这个阶段的初始化,初始化阶段页目录及几个页表在物理空间中的位置如图所示。

/** ZERO_PAGE is a global shared page that is always zero: used* for zero-mapped memory areas etc..*/
extern unsigned long empty_zero_page[1024];

其中 empty_zero_page 中存放的是在操作系统的引导过程中所收集的一些数据,叫做引导参数。因为这个页面开始的内容全为 0,所以叫做“零页”,代码中常常通过宏定义 ZERO_PAGE 来引用这个页面。不过,这个页面要到初始化完成,系统转入正常运行时才会用到。

那 swapper_pg_dir 和 pg0 、pg1 怎么对物理内存进行映射的呢?

从上面的物理内存分布可知,swapper_pg_dir 、pg0 、pg1存在物理内存中,swapper_pg_dir [0] 和 swapper_pg_dir [768] 指向 pg0 所在的物理地址,swapper_pg_dir [1] 和 swapper_pg_dir [769]指向pg1所在的物理地址。而他们每一项对应的映射为 4M。pg0 和 pg1 二者映射物理内存的前 8M 空间。如下图:

比如当访问虚拟内核地址空间 0xC0001002,通过 swapper_pg_dir 进行虚拟地址到物理地址转换时,发现 0xC0001002 处于 swapper_pg_dir [768],而 swapper_pg_dir [768] 指向 pg0 的物理内存地址,然后经过 pg0 找到其对应的物理页框。

关于整个虚拟地址空间和物理空间分布关系如下:

Linux内核虚拟地址空间 - 墨天轮

Linux内核虚拟地址空间,-3G的由来。各个进程的虚拟内存4G,内核总在3-4G。内核的虚拟空间地址-3G,总是指向物理内存的0-1G地址,各个进程的虚拟内核共享这个物理内存相关推荐

  1. 虚拟地址空间【详解】 虚拟地址空间是什么 | 为什么要有虚拟地址空间

    目录 一.什么是虚拟地址空间 / 虚拟地址空间是如何被设计的 1.先看一下linux空间分布 I.示意图: II.验证: 2. 在已知Linux内存分布之后,我们来看一个奇怪的现象 I.代码 : II ...

  2. 面试中常被问到的(14)虚存管理和虚拟地址空间

    虚拟内存技术使得每一个进程在运行过程中,始终都是显式独自占用当前系统内存资源,事实上,所有进程共享同一个物理内存,每一个进程只把自己目前需要的虚拟内存空间映射在物理内存上,此过程内核并不会立即把虚拟内 ...

  3. linux 虚拟地址空间在哪,Linux虚拟地址空间

    Linux虚拟内存管理(glibc) 在使用mysql作为DB开发的兑换券系统中,随着分区表的不断创建,发现mysqld出现了疑似"内存泄露"现象,但通过 valgrind 等工具 ...

  4. 虚拟地址空间以及编译模式

    原文链接 虚拟地址空间以及编译模式 < 上一页虚拟内存到底是什么?为什么我们在C语言中看到的地址是假的? C语言内存对齐,提高寻址效率下一页 > 所谓虚拟地址空间,就是程序可以使用的虚拟地 ...

  5. 虚拟内存,虚拟地址空间,用户空间,内核空间

    虚拟内存,就是用磁盘作为介质,暂时性存储数据,和主存进行换入换出,使程序能够使用更多的内存.虚拟内存是单位是页,固定大小的数据方便数据的交换.如果一个应用程序要访问某段内存,通过MMU得到相应的页号, ...

  6. 一篇长文叙述Linux内核虚拟地址空间的基本概括

    x86-32位虚拟地址空间 就我们所知,Linux内核一般将处理器的虚拟地址空间划分为两个部分.底部比较大的部分用于用户进程,顶部则专用于内核.虽然(在两个用户进程之间的)上下文切换期间会改变下半部分 ...

  7. 【Linux 内核 内存管理】内存映射原理 ① ( 物理地址空间 | 外围设备寄存器 | 外围设备寄存器的物理地址 映射到 虚拟地址空间 )

    文章目录 一.物理地址空间 二.外围设备寄存器 三.外围设备寄存器物理地址 映射到 虚拟地址空间 一.物理地址空间 " 物理地址空间 " 是 CPU 处理器 在 " 总线 ...

  8. linux内核1G虚拟地址空间的映射规则以及什么是高端内存?

    前面我们讲了,在32位linux内核里,内核地址空间是0xc0000000~0xffffffff, 大小1G:内核地址空间是0x00000000~0xbfffffff,大小3G.当内核代码访问内存时, ...

  9. 【Linux 内核 内存管理】虚拟地址空间布局架构 ① ( 虚拟地址空间布局架构 | 用户虚拟地址空间划分 )

    文章目录 一.虚拟地址空间布局架构 二.用户虚拟地址空间划分 一.虚拟地址空间布局架构 在 646464 位的 Linux 操作系统中 , " ARM64 架构 " 并 不支持 6 ...

最新文章

  1. 调查用QQ企业邮箱的smtp需要添加spf1
  2. PHP编译过程中常见错误信息的解决方法
  3. 华为云+AI,视频分析全面进入智能时代
  4. 记一次discuz修改首页图片路径问题
  5. linux挂载盘符扫描,Linux下挂载ISCSI的盘符问题
  6. Linux压缩与解压常用命令
  7. LeetCode 1912. 设计电影租借系统(map+set)
  8. Java之HSF搭建demo
  9. 系统设计挖个坑(未填)
  10. 支付宝否认彭翼捷退出阿里系:在蚂蚁金服出任首席市场官
  11. python中直方图bins是什么意思_Python 中下划线的 5 种含义都是什么?
  12. 寒武纪与华为海思分庭抗礼:中立芯片公司的成人礼
  13. boxplot函数--Matplotlib
  14. 安卓c语言hook,C语言hook技术实现木马功能-盗QQ密码
  15. 英特尔处理器后缀字母的意思
  16. 抖音xlog算法分析
  17. java鼠标左键点击四溅,重生之我是一只鼠
  18. fstream的使用详解
  19. Oracle 12C In-Memory特性研究
  20. cs231n:assignment2——Q1: Fully-connected Neural Network

热门文章

  1. word和pdf等常见文档格式如何转换
  2. 数据分析数据挖掘的步骤
  3. 解决方法dataSource init error java.sql.SQLException: com.mysql.cj.jdbc.Driver
  4. 【Python考试资源】包含重点知识、坑点知识,期末考试看这一份就够了
  5. cmd 下mysql常用的数据库命令
  6. 记:利用函数解决素数问题
  7. 【Linux】权限修改解析
  8. 趣学python编程第六章答案_趣学Python-教孩子学编程--第六章
  9. Java防御路径操作(Path Manipulation) 的正确姿势
  10. windows截图功能