Linux内核段页式内存管理技术

news2024/10/2 14:30:34

一、概述

1.虚拟地址空间

内存是通过指针寻址的,因而CPU的字长决定了CPU所能管理的地址空间的大小,该地址空间就被称为虚拟地址空间,因此32位CPU的虚拟地址空间大小为4G,这和实际的物理内存数量无关。
Linux内核将虚拟地址空间分成了两部分:

  • 一部分是用户进程可用的,这部分地址是地址空间的低地址部分,从0到TASK_SIZE,称为用户空间
  • 一部分是由内核保留使用的,这部分地址是地址空间的高地址部分,从KERNELBASE到结束,称为内核空间

与之相关的一些宏:

  1. KERNELBASE:内核虚拟地址空间的起始地址,一般和PAGE_OFFSET相同,但是也可能不同
  2. PAGE_OFFSET:内核虚拟地址空间中低端内存的起始地址
  3. PHYSICAL_START:内核物理地址的起始地址
  4. MEMORY_START:内核低端内存的物理起始地址

用户进程可用的部分在进程切换时会发生改变,但是由内核保留使用的部分在进程切换时是不变的。在32位系统上,两部分的典型划分比为3:1(该比例可修改),即4G虚拟地址空间中的3G是用户进程可访问的,而另外1G是保留给内核使用的,在这种划分下用户进程可用的虚拟地址空间是0x00000000-0xbfffffff,内核的虚拟地址空间是0xc0000000-0xffffffff。
不同的进程使用不同的用户空间可以使得不同进程的用户空间部分相互隔离,从而保护进程的用户空间部分。
内核空间的保护是通过CPU的特权等级实现的,所有现代CPU都提供了多个特权等级,每个特权等级可以获得的权限是不同的,当CPU处在某个权限等级时就只能执行符合这个等级的权限限制的操作。Linux使用了两个权限等级,分别对应于内核权限和用户权限,并且给属于内核的内存空间添加了权限限制,使得只有处于内核权限等级时CPU才能访问这些内存区域,这就将内核空间也保护了起来。

2.物理地址到虚拟地址的映射

可用的物理内存会被映射到内核虚拟地址空间中。在32位系统中,内核会将一部分物理内存直接映射到内核的虚拟地址空间中,如果访问内存时所使用的虚拟地址与内核虚拟地址起始值的偏移量不超过该部分内存的大小,则该虚拟地址会被直接关联到物理页帧;否则就必须借助”高端内存“来访问,因此也可以看出之所以使用“高端内存”是因为CPU可寻址的虚拟地址可能小于实际的物理内存,因而不得不借助其它机制(“高端内存”)来访问所有的内存。在IA-32系统上,这部分空间大小为896M。
64位系统不使用高端内存,这是因为64位的系统理论上可寻址的地址空间远大于实际的物理内存(至少现在是如此),因而就不必借助“高端内存”了。而对于用户进程来说,由于它的所有内存访问都通过页表进行,不会直接进行,因而对用户进程来说也不存在高端内存之说。
高端内存由32位架构的内核使用,在32位架构的内核中,要使用高端内存必须首先使用kmap将高端内存映射进内核的虚拟地址空间。

3.内存类型

从硬件角度来说存在两种不同类型的机器,分别用不同的方式来管理内存。

  1. UMA(uniform memory access):一致内存访问机器,它将可用内存以连续的方式组织起来。SMP系统中,每个CPU都可以以同样的速度访问内存。
  2. NUMA(non-uniform memory access):非一致内存访问机器总是多处理器机器。系统的各个CPU都有本地内存,可以支持快速访问。系统中的所有处理器都通过总线连接起来,进而可以访问其它CPU的本地内存,但是不如访问本地内存快。

lnux中如果要支持NUMA系统,则需要打开CONFIG_NUMA选项。

 

二、内存组织

linux内核对一致和不一致的内存访问系统使用了同样的数据结构,因此对于不同的内存布局,内存的管理算法几乎没有区别。对于UMA系统,将其看作只有一个NUMA节点的NUMA系统,即将其看成NUMA的特例。这样就将简化了内存管理的其它部分,其它部分都可以认为它们是在处理NUMA系统。

1.基本概念和相关数据结构

linux引入了一个概念称为node,一个node对应一个内存bank,对于UMA系统,只有一个node。其对应的数据结构为“struct pglist_data”。
对于NUMA系统来讲, 整个系统的内存由一个名为node_data 的struct pglist_data(page_data_t) 指针数组来管理。

NUMA系统的内存划分如图所示:

每个node又被分成多个zone,每个zone对应一片内存区域。内核引入了枚举常量 zone_type 来描述zone的类型:

[cpp]  view plain  copy   
<mmzone.h>  
enum zone_type {  
#ifdef CONFIG_ZONE_DMA  
ZONE_DMA,  
#endif  
#ifdef CONFIG_ZONE_DMA32  
ZONE_DMA32,  
#endif  
ZONE_NORMAL,  
#ifdef CONFIG_HIGHMEM  
ZONE_HIGHMEM,  
#endif  
ZONE_MOVABLE,  
MAX_NR_ZONES  
};  

它们之间的用途是不一样的:

  • ZONE_DMA:可用作DMA的内存区域。该类型的内存区域在物理内存的低端,主要是ISA设备只能用低端的地址做DMA操作。
  • ZONE_NORMAL:直接被内核直接映射到自己的虚拟地址空间的地址。
  • ZONE_HIGHMEM:不能被直接映射到内核的虚拟地址空间的地址。
  • ZONE_MOVABLE:伪zone,在防止物理内存碎片机制中使用
  • MAX_NR_ZONES:结束标记

很显然根据内核配置项的不同,zone的类型是有变化的。每个zone都和一个数组关联在一起,该数组用于组织管理属于该zone的物理内存页。
zone用数据结构struct zone来表示。
所有的node都被保存在一个链表中。在使用时,内核总是尝试从与进程所运行的CPU所关联的NUMA节点申请内存。这是就要用到备用列表,每个节点都通过struct zonelist提供了备用列表,该列表包含了其它节点,可用于代替本节点进行内存分配,其顺序代表了分配的优先级,越靠前优先级越高。

资料直通车:最新Linux内核源码资料文档+视频资料icon-default.png?t=N176https://docs.qq.com/doc/DTmFTc29xUGdNSnZ2

内核学习地址:Linux内核源码/内存调优/文件系统/进程管理/设备驱动/网络协议栈icon-default.png?t=N176https://ke.qq.com/course/4032547?flowToken=1040236

2.页(Page)

页概念

内核使用struct page作为基本单位来管理物理内存,在内核看来,所有的RAM都被划分成了固定长度的页帧。每一个页帧包含了一个页,也就是说一个页帧的长度和一个页的长度相同。页帧是主存的一部分,是一个存储区域。页和页帧的区别在于,页是抽象的数据结构,可以存放在任意地方,而页帧是真实的存储区域。
struct page包含了跟踪一个物理页帧当前被用于什么的有信息。比如页面计数,标志等等。

映射页面到zone

内核使用struct page的flags中的字段来保存页所属于的zone以及node。这是通过set_page_zone和set_page_node,这两个函数由函数set_page_links调用。

三、页表

1.页表机制

CPU管理虚拟地址,因而物理地址需要映射到虚拟地址才能给CPU使用。用于将虚拟地址空间映射到物理地址空间的数据结构称为页表。
在使用4k大小页的情况下,4k地址空间需要2的20次方个页表项。即便每个页表项大小为4字节也需要4M内存,而每个进程都需要有自己的页表,这就成了一个极大的内存开销。而且在大多数情况下,虚拟地址空间的大部分区域都是没有被使用的,因而没必要为虚拟地址空间中的每个页都分配管理结构,因而实际中采用的是如下方案:

  • 使用多级页表,每个线性地址被看为形如“页目录表+页目录表+...+页目录表+页表+页内偏移”的形式,每个比特组按照其含义被用于在相应的表中查找数据,最终找到页表。
  • 进程的页表只包含了它所使用的地址空间。进程不使用的地址空间不需要加入进程的页表。
  • 只有在进程实际需要一个页表时才会给该页分配RAM,而不是在一开始就为进程的所有页都分配空间。

页表中包含了关于该页的信息,例如是否存在于主存中,是否是“脏”的,访问所需权限等级,读写标志,cache策略等等。内核的页表保存在全局变量swapper_pg_dir中,应用进程的页表保存在task_struct->mm->pgd中,在应用进程切换时,会切换进程的页表(schedule-->__schedule-->context_switch-->switch_mm-->switch_mmu_context-->local_flush_m)。

linux中采用了4级分页模型。如下:

虽然采用了4级模型,但是:

  • 对于32位且未使能物理地址扩展的系统,使用二级页表。Linux的做法是让页上级目录表和页中间目录表所包含的比特数目为0,让页全局目录表的比特数目包含除了页表和偏移量之外的所有比特,从而取消这两级目录。同时为了让代码可以同时运行在32比特环境和64比特环境,linux保留了这两级目录在指针序列中的位置,做法是将这两级目录所包含的表项数设置为1(这里需要注意的是即便只有一个比特,也可以表示两个项,因此需要此设置)。
  • 对于32且使能了物理地址扩展的系统,使用三级页表。
  • 对于64位系统,取决于硬件对线性地址位的划分。

在linux中,每个进程都有自己的页全局目录表(PGD),以及自己的页表集。当发生进程切换时,linux会完成页表的切换。
使用该方案后,每个虚拟地址都划分为相应的比特分组,其中PGD用于索引每个进程所专有的页全局表,以找到PUD,PUD用于索引进程的页上级目录表,以找到PMD依次类推直到找到PTE。PTE即页表数组,该表的表项包含了指向页帧的指针以及页的访问控制相关的信息,比如权限,是否在主存中,是否包含“脏”数据等等,OFFSET用做表内偏移。
使用该机制后,虚拟地址空间中不存在的内存区域对应的PUD,PMD,PTE将不被创建,这就节省了地址空间。

但是使用该机制后每次寻址都需要多次查表,才能找到对应的物理地址,因而降低了速递,CPU使用高速缓存和TLB来加速寻址过程。在访问内存时,如果虚拟地址对应的TLB存在,也就是TLB 命中了,则直接访问,否则就要使用相关的页表项更新TLB(此时可能需要创建新的页表项)然后再继续进行访问。

下图是一个CPU的虚拟地址到实地址的转换过程:

当被访问的地址不存在对应的TLB表项时,就会产生TLB中断。在TLB中断中,会:

  1. 首先查找访问地址对应的页表,如果找不到对应的页表,就会生成相应的页表项(powerpc通过调用读写异常的处理函数完成该过程)。
  2. 使用PTE的内容更新TLB。

在TLB的内容更新完后,仍可能产生读写异常(也就是通常说的page fault),因为页表项虽然存在,但是其内容可能是非法的(比如页表并不在内存中),。

2.x86架构中的页

地址空间

当使用x86时,必须区分以下三种不同的地址:

  • 逻辑地址:机器语言指令仍用这种地址指定一个操作数的地址或一条指令的地址。这种寻址方式在Intel的分段结构中表现得尤为具体,它使得MS-DOS或Windows程序员把程序分为若干段。每个逻辑地址都由一个段和偏移量组成。
  • 线性地址:线性地址是一个32位的无符号整数,可以表达高达2的32次方(4GB)的地址。通常用16进制表示线性地址,其取值范围为0x00000000~0xffffffff。
  • 物理地址:也就是内存单元的实际地址,用于芯片级内存单元寻址。物理地址也由32位无符号整数表示。

X86中的MMU包含两个部件,一个是分段部件,一个是分页部件,分段部件(段机制)把一个逻辑地址转换为线性地址;接着,分页部件(分页机制)把一个线性地址转换为物理地址。转化过程如图所示:

3.分段

1)分段机制

在x86段机制中,逻辑地址由两部分组成,即段部分(选择符)及偏移部分。
段是形成逻辑地址到线性地址转换的基础。如果我们把段看成一个对象的话,那么对它的描述如下:

  1. 段的基地址(Base Address):在线性地址空间中段的起始地址。
  2. 段的界限(Limit):表示在逻辑地址中,段内可以使用的最大偏移量。
  3. 段的属性(Attribute): 表示段的特性。例如,该段是否可被读出或写入,或者该段是否作为一个程序来执行,以及段的特权级等等。

段的界限定义逻辑地址空间中段的大小。段内在偏移量从0到limit范围内的逻辑地址,对应于从Base到Base+Limit范围内的线性地址。在一个段内,偏移量大于段界限的逻辑地址将没有意义,使用这样的逻辑地址,系统将产生异常。另外,如果要对一个段进行访问,系统会根据段的属性检查访问者是否具有访问权限,如果没有,则产生异常。例如,在80386中,如果要在只读段中进行写入,80386将根据该段的属性检测到这是一种违规操作,则产生异常。
下图表示一个段如何从逻辑地址空间,重新定位到线性地址空间。图的左侧表示逻辑地址空间,定义了A,B及C三个段,段容量分别为LimitA、LimitB及LimitC。图中虚线把逻辑地址空间中的段A、B及C与线性地址空间区域连接起来表示了这种转换。

段的基地址、界限及保护属性存储在段的描述符表中,在虚拟—线性地址转换过程中要对描述符进行访问。段描述符又存储在存储器的段描述符表中,该描述符表是段描述符的一个数组。简单的说段描述符表里存储了段描述符,而段描述符又包含了硬件进行逻辑地址到线性地址转换所需的所有信息。
每个段描述符都定义了线性地址空间中的一段地址,它的属性以及它和逻辑地址空间之间的映射关系,实际上是如何从逻辑地址空间映射到线性地址空间。

2)linux中的段

各种段描述符都存放于段描述符表中,要么在GDT中,要么在LDT中。
描述符表(即段表)定义了386系统的所有段的情况。所有的描述符表本身都占据一个字节为8的倍数的存储器空间,空间大小在8个字节(至少含一个描述符)到64K字节(至多含8K)个描述符之间。

  1. 全局描述符表(GDT):全局描述符表GDT(Global Descriptor Table),包含着系统中所有任务都共用的那些段的描述符。
  2. 局部描述符表(LDT):局部描述符表LDT(local Descriptor Table),包含了与一个给定任务有关的描述符,每个任务各自有一个的LDT。有了LDT,就可以使给定任务的代码、数据与别的任务相隔离。

每一个任务的局部描述符表LDT本身也用一个描述符来表示,称为LDT描述符,它包含了有关局部描述符表的信息,被放在全局描述符表GDT中。
但是linux很少使用分段机制,这是因为,分段和分页都能用于将物理地址划分为小的地址片段的功能,因而它们是相互冗余的。分段可以为不同的进程分配不同的线性地址空间,而分页可以将相同的线性地址空间映射到不同的物理地址空间。linux采用了分页机制,原因是:

  1. 如果所有的进程都使用相同的线性地址空间,内存管理更简单
  2. 很多其他架构的CPU对分段的支持很有限

在linux中,所有运行在用户模式的进程都使用相同的指令和数据段,因此这两个段也被成为用户数据段和用户指令段。类似的,内核使用自己的内核数据段和内核数据段。这几个段分别用宏_ _USER_CS,_ _USER_DS,_ _KERNEL_CS, and_ _KERNEL_DS定义。这些段都从0开始,并且大小都相同,因而linux中,线性地址和逻辑地址是相同的,而且内核和用户进程都可以使用相同的逻辑地址,逻辑地址也就是虚拟地址,这就和其它架构统一起来了。
单处理器系统只有一个GDT,而多处理器系统中每个CPU都有一个GDT,GDT存放在cpu_gdt_table中GDT包含了用户数据段,用户指令段,内核数据段内核指令段以及一些其他段的信息。
绝大多数的linux用户程序并不使用LDT,内核定义了一个缺省的LDT给大多数进程共享。它存放于default_ldt中。如果应用程序需要创建自己的局部描述附表,可以通过modify_ldt系统调用来实现。使用该系统调用创建的LDT需要自己的段。应用程序也可以通过modify_ldt来创建自己的段。

四、内存管理初始化

1.初始化流程

内存初始化关键是page_data_t数据结构以及其下级数据结构(zone,page)的初始化。
宏NODE_DATA用于获取指定节点对应的page_data_t,在多节点系统中,节点数据结构为struct pglist_data *node_data[];该宏获取对应节点所对应的数据结构,如果是单节点系统,节点的数据结构为struct pglist_data contig_page_data;该宏直接返回它。

1.初始化代码流程

系统启动代码中与内存管理相关的初始化代码如图:

其功能分别为:

  • setup_arch:架构相关的初始化,其中包括了内存管理中与架构相关部分的初始化。boot分配器在这个时候被初始化。
  • setup_per_cpu_areas:SMP中,该函数初始化源代码中静态定义的每CPU变量,该类变量对系统中每一个CPU都一个副本。此类变量保存在内核二进制影响的一个独立的段中。
  • build_all_zonelists:建立节点和zone的数据结构
  • mem_init:初始化内存分配器
  • setup_per_cpu_pageset:遍历系统中所有的zone,对于每一个zone为所有的CPU分配pageset(冷热页缓存)并进行初始化,在这个函数被调用之前,只有boot pagesets可用。

2.节点和zone的初始化

build_all_zonelists会遍历系统中所有的节点,并为每个节点的内存域生成数据结构。它最终会使用节点数据结构调用build_zonelists,该函数会在该节点和系统中其它节点的内存之间建立一种距离关系,距离表达的是从其它节点分配的代价,因而距离越大,分配代价也越大;之后的内存分配会依据这种距离进行,优先选择本地的,如果本地的不可用,则按照距离从近到远来分配,直到成功或者所有的都失败。
在一个节点的内存域中:

  1. 高端内存被看做是最廉价的,因为内核不依赖于高端内存,它被耗尽不会对系统有不良影响
  2. DMA看做是最昂贵的,因为它有特殊用途,它用于和外设交互数据
  3. 普通内存介于两者之间,因为内核有些部分是依赖于普通内存的,所以它耗尽对系统会有影响

当分配内存时,假设指定的内存区域的昂贵程度为A,则分配过程为:

  1. 首先尝试从本节点分配,并且是按照昂贵程度递增的顺序从A开始尝试,直到最昂贵的区域
  2. 如果从本节点分配失败,则按照距离关系依次检查其它几点,在检查每个节点时,仍是按照昂贵程度递增的顺序从A开始尝试,直到最昂贵的区域

3.特定于体系结构的设置

1.内核在内存中的布局

在启动装载器将内核复制到内存,并且初始化代码的汇编部分执行完后,内存布局如图所示:

这是一种默认布局,也存在一些例外:

  • PHYSICAL_START可用于配置修改内核在内存中的位置。
  • 内核可以被编译为可重定位二进制程序,此时由启动装载器决定内核的位置。

默认情况下,内核安装在RAM中从物理地址0x00100000开始的地方。也就是第2M开始的那个。没有安装在第1M地址空间开始的地方的原因:

  • 页帧0由BIOS使用,存在上电自检(POST)期间检查到的系统硬件配置。
  • 物理地址从0x000a0000到0x000fffff的范围通常保留给BIOS程序使用
  • 第一个MB内的其它页帧可能由特定计算机模型保留

从_edata到_end之间的初始化数据部分所占用的内存在初始化完成后有些是不再需要的,可以回收利用,可以控制哪些部分可以回收,哪些部分不能回收。
内核占用的内存分为几段,其边界保存在变量中,可以通过System.map查看相关的信息,在系统启动后也可以通过/proc/iomem查看相关的信息。

2.初始化步骤

在start_kernel,在其中会调用setup_arch来进行架构相关的初始化。setup_arch会完成启动分配器的初始化以及各个内存域的初始化(paging_init)。paging_init最终会调用free_area_init_node这是个架构无关的函数,它会完成节点以及zone的数据结构的初始化。

3.分页机制初始化

Linux内核将虚拟地址空间分成了两部分:用户空间和内核空间。用户进程可用的部分在进程切换时会发生改变,但是由内核保留使用的部分在进程切换时是不变的。在32位系统上,两部分的典型划分比为3:1(该比例可修改),即4G虚拟地址空间中的3G是用户进程可访问的,而另外1G是保留给内核使用的。
32位系统中,内核地址空间又被分为几部分,其图示如下:

4.直接映射

其中第一部分用于将一部分物理内存直接映射到内核的虚拟地址空间中,如果访问内存时所使用的虚拟地址与内核虚拟地址起始值的偏移量不超过该部分内存的大小,则该虚拟地址会被直接关联到物理页帧;否则就必须借助”高端内存“来访问,在IA-32系统上,这部分空间大小为896M。
对于直接映射部分的内存,内核提供了两个宏:

  • __pa(vaddr):用于返回与虚拟地址vaddr相对应的物理地址。
  • __va(paddr):用于返回和物理地址paddr相对应的虚拟地址。

剩余部分被内核用作其它用途:

  1. 虚拟地址中连续,但是物理地址不连续的内存区域可以从VMALLO区域分配。该机制通常用于用户进程,内核自己会尽量尝试使用连续的物理地址。当然,当直接映射部分不能满足需求时,内核也会使用该区域。在ppc32中ioremap就使用了该区域。
  2. 持久映射区域用于将高端内存中的非持久页映射到内核中。
  3. 固定映射用于与物理地址空间中的固定页关联的虚拟地址页,但是物理地址页即页帧可以自由选择。

内存的各个区域边界由图中所示的常数定义。high_memory定义了直接映射区域的边界。

系统中定义了与页相关的一些常量:

  • num_physpages:最高可用页帧的页帧号
  • totalram_pages:可用页帧的总数目
  • min_low_pfn:RAM中在内核映像之后的第一个可用的页帧号
  • max_pfn:最后一个可用的页帧号
  • max_low_pfn:被内核直接映射的最后一个页帧的页帧号(低端内存中)
  • totalhigh_pages:没有被内核直接映射的页帧的总数(高端内存中)

在直接映射的内存区域和用于vmalloc的内存区域之间有一个大小为VMALLOC_OFFSET的缺口,它用于对内核进行地址保护,防止内核进行越界访问(越过了直接映射区域)

3.2 vmalloc区

vmalloc区域的起始位置取决于high_memory和VMALLOC_OFFSET。而其结束位置则取决于是否启用了高端内存支持。如果没有启用高端内存支持,就不需要持久映射区域,因为所有内存都可以直接映射。

3.3 持久映射区

持久映射页则开始于PKMAP_BASE,其大小由LAST_PKMAP表示有多少个页。

3.4 固定映射区

固定映射开始于FIXADDR_START结束于FIXADDR_END。这部分区域指向物理内存的随机位置。在该映射中,虚拟地址和物理地址之间的关联是可以自由定义的,但是定义后就不能更改。该区域一直延伸到虚拟地址空间的顶端。

固定映射的优势在于编译时,对该类地址的处理类似于常数,内核一旦启动即为它分配了物理地址。对此类地址的引用比普通指针要快。在上下文切换期间,内核不会将对应于固定地址映射的TLB刷新出去,因此对这类地址的访问总是通过高速缓存。
对于每一个固定地址,都必须创建一个常数并添加到称为fixed_addresses的枚举列表里。内核提供了virt_to_fix和fix_to_virt用于虚拟地址和固定地址常数之间的转换。

set_fixmap用于建立固定地址常量和物理页之间的对应关系。

3.5 冷热页

free_area_init_node最终会调到zone_pcp_init,它会为该zone计算一个batch值。而setup_per_cpu_pageset则会完成冷热缓存的初始化。

4. 启动过程中的内存管理

bootmem分配器用于内核在启动过程中分配和内存。这是一个很简单的最先适配的分配器。它使用位图来管理页面,比特1表示页忙,0表示空闲。需要分配内存时就扫描位图,直到找到第一个能够满足需求的内存区域。

数据结构

内核为每个节点都分配了一个struct bootmem_data结构的实例用来管理该node的内存。

初始化

在不同的架构下初始化的代码不尽相同,但是都是在paging_int中被调用。

分配器接口

alloc_bootmem*用于分配内存free_bootmem*用于释放内存

停用bootmem分配器

当slab系统完成初始化,能够承担内存分配工作时,需要停掉该分配器,这是通过free_all_bootmem(UMA系统)或free_all_bootmem_node(NUMA系统)来完成的

释放初始化数据

内核提供了两个属性__init用于标记初始化函数,__initdata用于标记初始化数据,这意味着这个函数/数据在初始化完成后其内存就不需了,可以进行回收利用。

内核页表的初始化

以powerpc为例,内核页表的初始化由MMU_init来完成,它在start_kernel之前被调用:

MMU_init->mapin_ram->__mapin_ram_chunk->map_page,

map_page的代码如下:

[cpp]  view plain  copy   
int map_page(unsigned long va, phys_addr_t pa, int flags)  
{  
        pmd_t *pd;  
        pte_t *pg;  
        int err = -ENOMEM;  
  
        /* Use upper 10 bits of VA to index the first level map */  
        pd = pmd_offset(pud_offset(pgd_offset_k(va), va), va);  
        /* Use middle 10 bits of VA to index the second-level map */  
        pg = pte_alloc_kernel(pd, va);  
        if (pg != 0) {  
                err = 0;  
                /* The PTE should never be already set nor present in the 
                 * hash table 
                 */  
                BUG_ON((pte_val(*pg) & (_PAGE_PRESENT | _PAGE_HASHPTE)) &&  
                       flags);  
                set_pte_at(&init_mm, va, pg, pfn_pte(pa >> PAGE_SHIFT,  
                                                     __pgprot(flags)));  
        }  
        return err;  
}  

再看下init_mm的相关定义:

[cpp]  view plain  copy   
struct mm_struct init_mm = {   
        .mm_rb          = RB_ROOT,  
        .pgd            = swapper_pg_dir,  
        .mm_users       = ATOMIC_INIT(2),  
        .mm_count       = ATOMIC_INIT(1),  
        .mmap_sem       = __RWSEM_INITIALIZER(init_mm.mmap_sem),  
        .page_table_lock =  __SPIN_LOCK_UNLOCKED(init_mm.page_table_lock),  
        .mmlist         = LIST_HEAD_INIT(init_mm.mmlist),  
        INIT_MM_CONTEXT(init_mm)  
};  

因此可见,kernel的页表是保存在swapper_pg_dir中的。它是init_task的active_mm:

[cpp]  view plain  copy   
#define INIT_TASK(tsk)  \  
{                                                                       \  
        .state          = 0,                                            \  
        .stack          = &init_thread_info,                            \  
        .usage          = ATOMIC_INIT(2),                               \  
        .flags          = PF_KTHREAD,                                   \  
        .prio           = MAX_PRIO-20,                                  \  
        .static_prio    = MAX_PRIO-20,                                  \  
        .normal_prio    = MAX_PRIO-20,                                  \  
        .policy         = SCHED_NORMAL,                                 \  
        .cpus_allowed   = CPU_MASK_ALL,                                 \  
        .nr_cpus_allowed= NR_CPUS,                                      \  
        .mm             = NULL,                                         \  
        .active_mm      = &init_mm,                                     \  
[cpp]  view plain  copy   
truct task_struct init_task = INIT_TASK(init_task);  

init_task是内核代码开始位置被执行的:

[cpp]  view plain  copy   
/* 
 * This is where the main kernel code starts. 
 */  
start_here:  
        /* ptr to current */  
        lis     r2,init_task@h  
        ori     r2,r2,init_task@l  

start_here在start_kernel之前被执行。在start_kernel里rest_init会启动kernel_init来启动一个init进程,init_task并不是init进程,init_task是内核启动主代码所在的上下文,该进程最后停在了cpu_idle中(start_kernel->rest_init->cpu_idle),好吧,它的真面目出来了,它就是创世界的进程,并且最后变成了无所事事的idle了。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/369788.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Calendar计算两个时间之间相差几个月

目录说明说明 计算两个时间之间相差几个月&#xff1a; public int getMonth(String startDt, String endDt) { int month 0;try {SimpleDateFormat sdf new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");Calendar satrt Calendar.getInstance();Calendar end Cal…

有限差分法求解不可压NS方程

网上关于有限差分法解NS方程的程序实现不尽完备&#xff0c;这里是一些补充注解 现有的优秀资料 理论向 【1】如何从物理意义上理解NS方程&#xff1f; - 知乎 【2】NS方程数值解法&#xff1a;投影法的简单应用 - 知乎 【3】[计算流体力学] NS 方程的速度压力法差分格式_…

股票量化交易SQL特征工程入门

虽然现在各种量化教程和自助平台铺天盖地&#xff0c;但是对于新人来说入门最重要的事情就是挖掘特征。 对于传统的学习路径第一步是学习Python或者某一门编程语言&#xff0c;虽说Python入门容易上手快&#xff0c;但是要在实际应用中对股票数据进行分析&#xff0c;并挖掘有…

【数据库】第一章 绪论

第一章 绪论 1.1 数据库系统概述 数据库课程的学习内容 数据库的4个基本概念&#xff1a; 数据&#xff1a;描述事物的符号记录称为数据。 数据的含义成为数据的语义&#xff0c;数据与其语义是不可分割的。 数据库&#xff1a;数据库是长期存储在计算机内、有组织、可共享的…

Netty核心组件ChannelPipeline事件handler源码解析

源码解析目标 当请求进来&#xff0c;ChannelPipeline如何协调内部这些Handler通过源码梳理ChannelPipeline 与ChannelHandlerContext中的read&#xff0c;fireChannelRead等方法的不同 inbound源码解析 在 Netty启动流程源码剖析 文中我们已经知道&#xff0c;启动后&#…

公司项目vue cli2升级到vue cli3

背景&#xff1a;公司项目历时时间较长&#xff0c;通过长时间的迭代&#xff0c;目前项目文件较多&#xff08;src目录下有2217个文件&#xff09;&#xff0c;系统庞大&#xff0c; 之前通过vue cli2脚手架构建的项目框架&#xff0c;在本地开发时已经明显感觉到吃力&#xf…

Win10+vs2019配置与运行RenderMatch(踩坑记录)

Win10vs2019配置与运行RenderMatch RenderMatch旨在解决aerial images 和ground images 匹配问题&#xff0c;其思路可参考原论文 “Leveraging Photogrammetric Mesh Models for Aerial-Ground Feature Point Matching Toward Integrated 3D Reconstruction” 1.源码下载 G…

【2023new】OAK相机如何将Yolov5转换成blob格式?

编辑&#xff1a;OAK中国 首发&#xff1a;oakchina.cn 喜欢的话&#xff0c;请多多&#x1f44d;⭐️✍ 内容可能会不定期更新&#xff0c;官网内容都是最新的&#xff0c;请查看首发地址链接。 ▌前言 Hello&#xff0c;大家好&#xff0c;这里是OAK中国&#xff0c;我是助手…

机械革命极光Pro电脑开启出现英文代码无法启动怎么办?

机械革命极光Pro电脑开启出现英文代码无法启动怎么办&#xff1f;有的小伙伴在使用机械革命极光Pro电脑的时候&#xff0c;正常开启电脑却无法进入到桌面中&#xff0c;而是显示一些英文错误提示。遇到这个问题是我们的系统故障了&#xff0c;可以通过U盘重装系统的方法来进行问…

logback 自定义日志输出到数据库

项目日志格式 Spring Boot 的默认日志输出类似于以下示例&#xff1a; 2021-12-14 22:40:14.159 INFO 20132 --- [ main] com.kuangstudy.SpringbootApplication : Started SpringbootApplication in 2.466 seconds (JVM running for 3.617)输出以下项目&…

SpringBoot 整合 MongoDB 6 以上版本副本集及配置 SSL / TLS 协议

续上一篇 Linux 中使用 docker-compose 部署 MongoDB 6 以上版本副本集及配置 SSL / TLS 协议 前提&#xff1a;此篇文章是对上一篇文章的实战和项目中相关配置的使用&#xff0c;我这边针对 MongoDB 原有基础上做了增强&#xff0c;简化了 MongoDB 配置 SSL / TLS 协议上的支…

Android Studio引入JNI第三方库

一、前言 JNI作为Java与native沟通的桥梁&#xff0c;项目开发中难免要使用到&#xff1b;而我们除了自己开发JNI之外&#xff0c;有时候还要在Android Studio引入别人开源的C第三方库&#xff0c;并在jni层实现第三方库的调用。 二、流程 1.导入头文件和实现文件 将第三方…

Linux内核进程地址空间与进程内存布局

一&#xff0c;进程空间分布概述 对于一个进程&#xff0c;其空间分布如下图所示&#xff1a; 程序段(Text):程序代码在内存中的映射&#xff0c;存放函数体的二进制代码。初始化过的数据(Data):在程序运行初已经对变量进行初始化的数据。未初始化过的数据(BSS):在程序运行初未…

九龙证券|券商春季策略扎堆来袭 风格切换成焦点

2月以来&#xff0c;国泰君安、中信建投、国金证券等10余家券商组织相继发布2023年春季战略。综合来看&#xff0c;组织对A股持达观预期&#xff0c;未来两三个月A股商场或迎来重要切换。风格上&#xff0c;“中心财物&#xff0c;生长接力”或许成为上半年装备主线&#xff0c…

java 系列之Mybatis

java 系列文章 文章目录java 系列文章前言一、Mybatis 入门1.1 认识 框架&#xff08;了解&#xff09;1.2 认识 ORM&#xff08;要知道&#xff09;1.3 认识 Mybatis&#xff08;要知道&#xff09;二、Mybatis 使用2.1 创建maven项目并导入依赖2.2 准备数据库&#xff0c;包和…

释放内存流程

你好&#xff0c;我是安然无虞。 thread cache回收内存 当从 thread cache 中申请的内存对象使用完毕需要还回来的时候, 只需要计算出该内存对象对应 thread cache 中的哪一个自由链表桶, 然后将该内存对象插入进去即可. 不过需要注意的是, 如果不断有内存对象释放回来, 那么…

Java实现根据拼音首字母的排序

1.项目 手机APP端要对企业列表按企业名称首字母(如果企业名是英文的就按)进行分类排序&#xff0c;效果如下&#xff1a; 2.实现过程 2.1 首先引入项目的pinyin4j-2.5.0.jar包。 这个jar的下载地址如下&#xff1a; 链接&#xff1a;https://pan.baidu.com/s/1hkP_gGAYcgzyK_D…

跨链桥:Web3黑客必争之地

跨链桥&#xff0c;区块链的基础设施之一&#xff0c;所实现的功能是允许用户将自己的资产从一条链转移至另外一条链上&#xff0c;是连接不同的区块链的关键桥梁&#xff0c;常使用中心化的方式进行实现。由于跨链桥自身往往存储有用户所质押的巨额资产&#xff0c;是Web3黑客…

KUKA KR C4机器人与S7-1200PLC进行PROFINET通信的具体方法和步骤

KUKA KR C4机器人与S7-1200PLC进行PROFINET通信的具体方法和步骤 首先,从KUKA机器人控制柜中将KOP备选软件包拷贝出来,然后在“WorkVisual Development Environment”安装KUKA备选软件包(版本非常重要,尽量从控制柜中拷贝), 也可以从以下链接中获取: KUKA机器人PROFINET…

php 任务调度

在日常开发中&#xff0c;我们总会遇到一些在某个指定的时刻去执行&#xff0c;或是每隔xx时间执行&#xff0c;或是需要一直在后台监听的任务执行。基于这个需求&#xff0c;对于php我找了一些办法来实现这些功能 1、依赖于laravel的任务调度。 每隔xx时间执行一次命令&#…