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

news/2024/4/19 11:10:44/文章来源:https://blog.csdn.net/m0_50662680/article/details/129209665

一、概述

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.luyixian.cn/news_show_73918.aspx

如若内容造成侵权/违法违规/事实不符,请联系dt猫网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

五种IO模型以及select多路转接IO模型

目录 一、典型IO模型 1.1 阻塞IO 1.2 非阻塞IO 1.3 信号驱动I0 1.4 IO多路转接 1.5 异步IO 多路转接的作用和意义 二、多路转接IO模型&#xff08;select&#xff09; 2.1 接口 2.2 接口当中的事件集合&#xff1a; fd_set 2.2 select使用事件集合&#xff08;位图&am…

174万亿采购,奔向数字化

采购不单纯发生在外部&#xff0c;更发生在内部&#xff0c;只有两者同时进行&#xff0c;才能完成采购中心从成本到利润中心角色的转变。 作者|斗斗 编辑|皮爷 出品|产业家 数字化&#xff0c;让很多企业业务流程发生了质变。 《2022数字化采购发展报告》显示&#x…

破解票房之谜:为何高票房电影绕不过“猫眼们”?

如此火爆的春节档很多&#xff0c;如此毁誉参半的春节档鲜有。2023开年&#xff0c;集齐张艺谋、沈腾的《满江红》&#xff0c;以及有票房前作打底的《流浪地球2》接连两部春节档电影票房进入前十&#xff0c;为有些颓靡的中国电影市场注入了一针“强心剂”。与票房同样热闹起来…

无重叠区间-力扣435-java贪心策略

一、题目描述给定一个区间的集合 intervals &#xff0c;其中 intervals[i] [starti, endi] 。返回 需要移除区间的最小数量&#xff0c;使剩余区间互不重叠 。示例 1:输入: intervals [[1,2],[2,3],[3,4],[1,3]]输出: 1解释: 移除 [1,3] 后&#xff0c;剩下的区间没有重叠。…

【爬虫】2.2 BeautifulSoup 装载HTML文档

HTML文档结点的查找工具很多&#xff0c;其中 BeautifulSoup 是功能强大且十分流行的查找工具之一。1. BeautifulSoup 的安装安装&#xff1a;pip install bs4导包&#xff1a;from bs4 import BeautifulSoup2. BeautifulSoup 装载HTML文档如果 doc 是一个 HTML 文档&#xff0…

深度学习训练营之yolov5 官方代码调用以及-requirements.txt下载当中遇到的问题

深度学习训练营之yolov5 官方代码调用原文链接内容总结环境介绍前置工作简单介绍yolov5下载源码yolov5的下载遇到问题问题解析问题处理创建虚拟环境下载当中遇到的问题代码运行视频检测参考内容原文链接 &#x1f368; 本文为&#x1f517;365天深度学习训练营 中的学习记录博客…

gazebo仿真环境中添加robotiq 2f 140的gripper_controller控制器

gazebo仿真环境中添加robotiq 2f 140的gripper_controller控制器 搭建环境&#xff1a; ubuntu: 20.04 ros: Nonetic sensor: robotiq_ft300 gripper: robotiq_2f_140_gripper UR: UR3 reasense&#xff1a; D435i 通过下面几篇博客配置好了ur3、力传感器、robotiq夹爪、rea…

18523-47-2,3-Azidopropionic Acid,叠氮基丙酸,可以与炔烃发生点击化学反应

【中文名称】3-叠氮基丙酸【英文名称】 3-Azidopropionic Acid&#xff0c;3-Azidopropionic COOH【结 构 式】【CAS】18523-47-2【分子式】C3H5N3O2【分子量】115.09【纯度标准】95%【包装规格】1g&#xff0c;5g&#xff0c;10g【是否接受定制】可进行定制&#xff0c;定制时…

java原理4:java的io网络模型

文章目录1&#xff1a;基础概念1&#xff1a;同步和异步2&#xff1a;阻塞和非阻塞2.1&#xff1a;阻塞IO2.2&#xff1a;非阻塞io2.3&#xff1a;io复用3&#xff1a;同步/异步和阻塞/非阻塞3.1&#xff1a;同步非阻塞NIO4: redis为什么速度快Java 网络IO模型简介1&#xff1a…

Tapdata 和 Databend 数仓数据同步实战

作者&#xff1a;韩山杰https://github.com/hantmacDatabend Cloud 研发工程师基础架构在云计算时代也发生着翻天地覆的变化&#xff0c;对于业务的支持变成了如何能利用好云资源实现降本增效&#xff0c;同时更好的支撑业务也成为新时代技术人员的挑战。 本篇文章通过&#xf…

删除MySQL表中的重复数据?

前言 一般我们将数据存储在MySQL数据库中&#xff0c;它允许我们存储重复的数据。但是往往重复的数据是作废的、没有用的数据&#xff0c;那么通常我们会使用数据库的唯一索引 unique 键作为限制。问题来了啊&#xff0c;我还没有创建唯一索引捏&#xff0c;数据就重复了&…

jianzhiOffer第二版难重点记录

04. 二维数组中的查找https://leetcode.cn/problems/er-wei-shu-zu-zhong-de-cha-zhao-lcof/ 思路&#xff1a;可以每层用以恶搞二分查找&#xff0c;优化思路&#xff1a;从左下角出发直接用二分。 ​​​​​​07. 重建二叉树https://leetcode.cn/problems/zhong-jian-er-cha…

springboot+vue.js高校大学生选课成绩管理系统javaweb

本课题要求实现一套学生成绩管理系统&#xff0c;系统主要包括管理员&#xff0c;学生和教师三大模块 (a) 管理员&#xff1b;管理员进入系统主要功能包括首页&#xff0c;个人中心&#xff0c;教师管理&#xff0c;学生管理&#xff0c;公告信息管理&#xff0c;课程类型管理&…

Android自定义View实现横向的双水波纹进度条

效果图&#xff1a;网上垂直的水波纹进度条很多&#xff0c;但横向的很少&#xff0c;将垂直的水波纹改为水平的还遇到了些麻烦&#xff0c;现在完善后发布出来&#xff0c;希望遇到的人少躺点坑。思路分析整体效果可分为三个&#xff0c;绘制圆角背景和圆角矩形&#xff0c;绘…

Linux学习(7.5)linux目录配置与重点回顾

鸟哥的 Linux 私房菜 -- Linux 的文件权限与目录配置 (vbird.org) 怎么记啊&#xff0c;直接点进去看吧 目录 Linux目录配置的依据--FHS 绝对路径与相对路径 重点回顾 以下内容转载自鸟哥的Linux私房菜 Linux目录配置的依据--FHS 是希望让使用者可以了解到已安装软件通常…

16、变量、流程控制与游标

文章目录1 变量1.1 系统变量1.1.1 系统变量分类1.1.2 查看系统变量1.2 用户变量1.2.1 用户变量分类1.2.2 会话用户变量1.2.3 局部变量1.2.4 对比会话用户变量与局部变量2 定义条件与处理程序2.1 案例分析2.2 定义条件2.3 定义处理程序2.4 案例解决3 流程控制3.1 分支结构之 IF3…

嵌入式系统硬件设计与实践(学习方法)

【 声明&#xff1a;版权所有&#xff0c;欢迎转载&#xff0c;请勿用于商业用途。 联系信箱&#xff1a;feixiaoxing 163.com】 刚读书的时候&#xff0c;对什么是嵌入式&#xff0c;其实并不太清楚。等到自己知道的时候&#xff0c;已经毕业很多年了。另外对于计算机毕业的学…

Python近红外光谱分析与机器学习、深度学习方法融合实践技术

、 第一n入门基础【理论讲解与案 1、Python环境搭建&#xff08; 下载、安装与版本选择&#xff09;。 2、如何选择Python编辑器&#xff1f;&#xff08;IDLE、Notepad、PyCharm、Jupyter…&#xff09; 3、Python基础&#xff08;数据类型和变量、字符串和编码、list和tu…

每日学术速递2.24

CV - 计算机视觉 | ML - 机器学习 | RL - 强化学习 | NLP 自然语言处理 Subjects: cs.LG 1.BUAA_BIGSCity: Spatial-Temporal Graph Neural Network for Wind Power Forecasting in Baidu KDD CUP 2022 标题&#xff1a;BUAA_BIGSCity&#xff1a;百度KDD CUP 2022风电预测…

新C++(10):Map\Set的封装

"湖人总冠军"一、Map\Set的介绍Set是C标准库中的一种关联容器。所谓关联容器就是通过键&#xff08;key&#xff09;来读取和修改元素。与map关联容器不同&#xff0c;它只是单纯键的集合。取自这里Map是STL 的一个关联容器&#xff0c;它提供一对一&#xff08;其中…