2000字范文,分享全网优秀范文,学习好帮手!
2000字范文 > Linux内核虚拟地址空间 -3G的由来。各个进程的虚拟内存4G 内核总在3-4G。内核的虚拟

Linux内核虚拟地址空间 -3G的由来。各个进程的虚拟内存4G 内核总在3-4G。内核的虚拟

时间:2020-10-13 14:30:12

相关推荐

Linux内核虚拟地址空间 -3G的由来。各个进程的虚拟内存4G 内核总在3-4G。内核的虚拟

Linux内核地址空间划分

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

Linux内核高端内存

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

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

假设按照上述简单的地址映射关系,那么内核逻辑地址空间访问为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 的内存。映射关系如下:

当内核访问完 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 之前关闭。

/*pagetablefor0-4MBforeverybody*/externunsignedlongpg0[1024];pte_tpg1[1024];pgd_tswapper_pg_dir[1024];

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

structmm_structinit_mm=INIT_MM(init_mm);#defineINIT_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,这就是初始化的第一阶段。

.org0x1000ENTRY(swapper_pg_dir).long0x0010.long0x00103007.fillBOOT_USER_PGD_PTRS-2,4,0/*default:766entries*/.long0x0010.long0x00103007/*default:254entries*/.fillBOOT_KERNEL_PGD_PTRS-2,4,0/**Thepagetablesareinitializedtoonly8MBhere-thefinalpage*tablesaresetuplaterdependingonmemorysize.*/.org0x2000ENTRY(pg0).org0x3000ENTRY(pg1)/**empty_zero_pagemustimmediatelyfollowthepagetables!(The*initializationloopcountsuntilempty_zero_page)*/.org0x4000ENTRY(empty_zero_page)/**Initializepagetables*/movl$pg0-__PAGE_OFFSET,%edi/*initializepagetables*/movl$007,%eax/*"007"doesn'tmeanwithrighttokill,butPRESENT+RW+USER*/2:stosladd$0x1000,%eaxcmp$empty_zero_page-__PAGE_OFFSET,%edijne2b

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

.org0x1000ENTRY(swapper_pg_dir).long0x0010.long0x00103007.fillBOOT_USER_PGD_PTRS-2,4,0/*default:766entries*/.long0x0010.long0x00103007/*default:254entries*/.fillBOOT_KERNEL_PGD_PTRS-2,4,0/**Enablepaging*/3:movl$swapper_pg_dir-__PAGE_OFFSET,%eaxmovl%eax,%cr3/*setthepagetablepointer..*/movl%cr0,%eaxorl$0x80000000,%eaxmovl%eax,%cr0/*..andsetpaging(PG)bit*/jmp1f/*flushtheprefetch-queue*/1:movl$1f,%eaxjmp*%eax/*makesureeipisrelocated*/1:/*Setupthestackpointer*/lssstack_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 个目录项 0x0010、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_PAGEisaglobalsharedpagethatisalwayszero:used*forzero-mappedmemoryareasetc..*/externunsignedlongempty_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地址 各个进程的虚拟内核共享这个物理内存

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。