理想下载站 手游攻略 新游动态 Linux内核虚拟地址空间

Linux内核虚拟地址空间

时间:2024-10-17 19:19:37 来源: 浏览:64

Linux内核高端内存

当内核模块代码或者线程访问内存时,代码中的内存地址都是逻辑地址,与真实的物理内存地址对应,需要进行地址的一一映射。例如逻辑地址0xc0000003对应物理地址0x3,物理地址0xc0000004对应物理地址为0x4,逻辑地址与物理地址的对应关系为

物理地址=逻辑地址0xC0000000

逻辑地址

物理内存地址

0xc0000000

0x0

0xc0000001

0x1

0xc0000002

0x2

0xc0000003

0x3

……

……

0xe0000000

0x20000000

……

……

0xffffffff0xffffffff

0x40000000

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

显然内核地址空间0xc0000000 ~0xffffffff不能用于简单的地址映射。

因此,Linux将物理页分为3个区域:

ZONE_DMA区域专用于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 文件中找到这些符号的线性地址(或虚拟地址)。

启用分页

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。但传输到该条目时使用的指令是“ljmp0x”100000”而不是“ljmpstartup_32”,因此加载到CPU中的寄存器IP的地址是物理地址0x100000而不是虚拟地址0xC0100000。

这样CPU进入startup_32后就会继续在物理地址取指令。只要你不在代码段中引用某个地址,比如对某个地址进行绝对传送或者调用子程序,那么无论CS内容如何,都可以继续这样运行。另外,在进入startup_32之前CPU中断已经关闭。

/* 每个人0-4MB的页表*/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启动的初始阶段,当内核刚刚加载到内存中时,分页功能还没有启用。此时直接访问物理地址(或者线性地址等于物理地址)。但初始化完成后,内核还需要有自己的虚拟地址空间(1G大小)。这个虚拟地址空间的地址映射关系将会作为模板复制到其他进程的内核地址空间中。

临时内核页表仅用于映射物理地址的前8M空间内容。目的是让CPU在实模式(直接访问物理地址)和保护模式(基于虚拟地址映射)切换的过程中能够访问前8M地址。 (假设内核使用的所有内存都可以存放在8M空间中,因为一张页表可以映射4M地址,所以8M空间需要两张页表,也就是需要两个页目录项。这两张页表我们称之为临时页表内核页表pg0 和pg1。

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

.org0x1000ENTRY(swapper_pg_dir).long0x00102007.long0x00103007.fill BOOT_USER_PGD_PTRS-2,4,0/* 默认: 766 个条目*/.long0x00102007.long0x00103007/* 默认: 254 个条目*/.fill BOOT_KERNEL_PGD_PTRS-2,4,0 /** 这里页表仅初始化为8MB - 最终页*表稍后根据内存大小设置。*/.org0x2000ENTRY(pg0).org0x3000ENTRY(pg1)/**empty_zero_page 必须紧跟在该页之后桌子! (*初始化循环计数直到empty_zero_page)*/.org0x4000ENTRY(empty_zero_page)/**初始化页表*/movl $pg0-__PAGE_OFFSET,%edi /*初始化页表*/movl $007,%eax /*'007'并不是说有权利kill,而是PRESENT+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页(即零页),零页存储系统启动参数和命令行参数。

.org0x1000ENTRY(swapper_pg_dir).long0x00102007.long0x00103007.fill BOOT_USER_PGD_PTRS-2,4,0/* 默认: 766 个条目*/.long0x00102007.long0x00103007/* 默认: 254 个条目*/.fill BOOT_KERNEL_PGD_PTRS-2,4,0 /** 启用分页*/3:movl $swapper_pg_dir-__PAGE_OFFSET,%eaxmovl %eax,%cr3 /* 设置页表指针. */movl %cr0,%eaxorl $0x80000000,%eaxmovl %eax,%cr0 /* .并设置分页(PG)位*/jmp 1f /* 刷新预取队列*/1:movl $1f,%eaxjmp *%eax /* 确保eip 已重定位*/1:/* 设置堆栈指针*/lss stack_start,%esp //在堆栈开始处设置CPU 堆栈。这段代码将页目录swapper_pg_dir的物理地址加载到控制寄存器cr3中,并将cr0的最高位设置为1,从而开启分页机制。

不过,启用分页机制并不意味着Linux内核真正进入了保护模式,因为此时指令寄存器EIP中的地址仍然是物理地址,而不是虚拟地址。 “jmp 1f”指令在逻辑上不执行任何操作,但在功能上它会丢弃指令管道的内容(这是英特尔在i386 数据表中推荐的内容),因为它是一个短跳转。但是,EIP仍然是物理地址。以下mov 和jmp 指令将第二个地址(编号为1)加载到EAX 寄存器中并跳转到那里。这两条指令执行过程中,EIP仍然指向物理地址“1MB+某处”。因为编译器将所有符号地址都放在虚拟内存空间中,所以第二个标号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个目录项全部设置为0。因为页表的大小为4KB,每个目录项占用4个字节,即每个页表包含1024个目录项,每个页的大小为也是4KB,所以这768个目录项映射的虚拟空间是76810244K=3G,即swapper_pg_dir表中的前768个目录项映射到用户空间。最后,第768和769个目录项中存储了两个页表pg0和pg1的地址和属性,并将第770到1023共254个目录项设置为0。这256个目录项映射的虚拟地址空间为25610244K=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 是一个全局共享页,始终为零:用于*零映射内存区域等.*/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内核虚拟地址空间的文章了!我一直很困惑这部分内容,现在看起来终于明白了。作者讲解的很清晰,很多关键概念都解释到位了

    有16位网友表示赞同!

拥抱

我理解虚拟内存在提高效率方面很重要!可是,这个虚拟地址空间的划分还是有点绕,希望以后能有更直观的图解来辅助理解。

    有12位网友表示赞同!

如你所愿

这篇文章很有用,帮我巩固了学习Linux内核的基础知识。不过对于一些比较深入的内容,比如不同类型进程的使用方式,我需要再查阅其他资料才能全面了解

    有7位网友表示赞同!

何必锁我心

说的对,理解Linux内核虚拟地址空间的关键是明白它如何管理内存分配和释放!如果没有这个机制,系统将不堪重负。非常棒的博文!

    有9位网友表示赞同!

千城暮雪

感觉这篇文章讲得过于细致了,有些概念虽然重要,但对我来说太基础了。能不能提供一些更实际应用的技术案例?

    有9位网友表示赞同!

颜洛殇

我一直在学习内核驱动开发,理解虚拟地址空间是重中之重!这份文章很有用,让我对这个机制有了更清晰的认识!

    有10位网友表示赞同!

念旧是个瘾。

这篇文章真是太棒了!解释清晰、逻辑严谨,读完之后感觉自己提升了许多。谢谢作者分享!

    有18位网友表示赞同!

ヅ她的身影若隐若现

我目前在从事嵌入式Linux开发,对于虚拟地址空间的研究也十分重要!这个文章帮我理清了很多思路, 希望能再深入一些关于内存管理的策略?

    有13位网友表示赞同!

一尾流莺

感觉这篇文章对内核工程师来说比较有用,对初学者来说可能有些晦涩难懂。能不能提供一些入门方面的建议?

    有15位网友表示赞同!

怀念·最初

虚拟地址空间是一个非常重要的概念,尤其是在多进程系统中!这篇博文帮我更加清楚地理解了它的作用和运作方式。

    有11位网友表示赞同!

有一种中毒叫上瘾成咆哮i

Linux内核虚拟地址空间的管理机制真的非常复杂,我需要花更多时间来研究和消化这份文章的内容。但总体来说,这是一篇很有价值的文章

    有6位网友表示赞同!

◆乱世梦红颜

学习Linux内核确实有很多难点,虚拟地址空间就是其中之一。幸好找到了这篇博文,让我对这个概念有了更清晰的认识!

    有12位网友表示赞同!

在哪跌倒こ就在哪躺下

我觉得这篇文章可以把一些重要的代码片段增加进来,这样能帮助读者更好地理解实际实现过程!

    有15位网友表示赞同!

醉婉笙歌

对于想要深入学习Linux内核的人来说,了解虚拟地址空间绝对是必修课。这篇博文提供了很好的入门指导,值得细细品味

    有7位网友表示赞同!

有些人,只适合好奇~

我一直在关注相关内核的代码实现,想了解虚拟地址空间是如何在实际中应用的?这个文章有没有一些相关源码分析?

    有18位网友表示赞同!

oО清风挽发oО

虽然这篇文章解释了Linux内核虚拟地址空间的基础概念,但我仍然希望能看到更多关于内存管理策略的探讨

    有16位网友表示赞同!

海盟山誓总是赊

对于我这种长期从事嵌入式开发的人来说,这份文章对我帮助不大。 因为我的应用场景与大型服务器系统不同

    有12位网友表示赞同!

标题:Linux内核虚拟地址空间
链接:https://www.ltthb.com/news/xydt/130313.html
版权:文章转载自网络,如有侵权,请联系删除!
资讯推荐
更多
ToonMe怎么取消自动续费?自动续费关闭方法

ToonMe怎么取消自动续费?自动续费关闭方法[多图],ToonMe中的迪士尼滤镜很火爆,有不少小伙伴都喜欢,不过在使用

2024-10-17
航海王热血航线藏宝图位置在哪?全部藏宝图位置坐标大全

航海王热血航线藏宝图位置在哪?全部藏宝图位置坐标大全[多图],航海王热血航线藏宝图在哪里?怎么样才能找到藏

2024-10-17
cf手游云悠悠角色怎么获得?云悠悠什么时候上线

cf手游云悠悠角色怎么获得?云悠悠什么时候上线[多图],cf手游云悠悠角色什么时候出?云悠悠角色获得的方法是什

2024-10-17
英雄联盟联动优衣库活动详情一览:LOL联动优衣库T恤购买地址入口

英雄联盟联动优衣库活动详情一览:LOL联动优衣库T恤购买地址入口[多图],英雄联盟联动优衣库T恤衫什么时候发售

2024-10-17