linux为什么要采用三级页表?该机制如何工作

linux为什么要采用三级页表?该机制如何工作,第1张

Linux启动并建立一套完整的页表机制要经过以下几个步骤:

1.临时内核页表的初始化(setup_32.s)

2.启动分页机制(head_32.s)

3.建立低端内存和高端内存固定映射区的页表( init_memory_mapping())

4.建立高端内存永久映射区的页表并获取固定映射区的临时映射区页表(paging_init())

具体分析低端内存页表的建立

在setup_arch()中内核通过调用init_memory_mapping()来建立低端内存页表

[cpp] view plaincopy

void __init setup_arch(char **cmdline_p)

...

...

/* max_pfn_mapped is updated here */

max_low_pfn_mapped = init_memory_mapping(0, max_low_pfn<<PAGE_SHIFT)

max_pfn_mapped = max_low_pfn_mapped

...

...

内核将低端内存的起始地址(0),和低端内存的结束地址(max_low_pfn<<PAGE_SHIFT)传递给init_memory_mapping(),下面来看Init_memory_mapping()的具体实现,简单起见,只分析32位系统的情况。

System V init启动过程

概括地讲,Linux/Unix系统一般有两种不同的初始化启动方式.

1) BSD system init

2) System V init

大多数发行套件的Linux使用了与System V init相仿的init也就是Sys V init,它比传统的BSD system init更容易且更加灵活。

System V init的主要思想是定义了不同的"运行级别(runlevel)"。通过配置文件/etc/inittab定义了系统引导时的运行级别, 进入或者切换到一个运行级别时做什么。每个运行级别对应于一个子目录/etc/rc.d/rcX.d。

每个rcX.d目录中都是一些以S或K开头的文件链接。这些链接指向的脚本都 可以接收start和stop参数,S开头的链接会传入start参数,一般是开启一项服务,K会传入stop参数,一般是停止某服务。

以下是一个大致的System V init过程:

(1)init 过程执行的第一个脚本是 /etc/rc.d/rc.sysinit,它主要做在各个运行级别中进行初始化工作,包括: 启动交换分区检查磁盘设置主机名检查并挂载文件系统加载并初始化硬件模块.

(2)执行缺省的运行级别模式。 这一步的内容主要在/etc/inittab中体现, inittab文件会告诉init进程要进入什么运行级别,以及在哪里可以找到该运行级别的配置文件.

(3)执行/etc/rc.d/rc.local脚本文件。 这也是init过程中执行的最后一个脚本文件,所以用户可以在这个文件中添加一些需要在登录之前执行的命令.

(4)执行/bin/login程序

注意:

System V init只是一种模式,每个系统初始化都有差异,但大体上不会相差太多。如busybox执行的第一个启动脚本就是/etc/init.d/rcS,而且不可以改变,与上面讲的不同。

LFS文件系统初始化示例

inittab文件

由下内容可以看出,最先执行的是/etc/rc.d/init.d/rc文件,给这个文件传入的参数是一个数字,rc会由传入的数字合成rcX.d目录的路径,然后执行其中的所有脚本链接。当然这只是一部分功能。

# Begin /etc/inittab

id:3:initdefault:

<em><strong>si::sysinit:/etc/rc.d/init.d/rc sysinit</strong></em>#可以设定初始化脚本

l0:0:wait:/etc/rc.d/init.d/rc 0

l1:S1:wait:/etc/rc.d/init.d/rc 1

l2:2:wait:/etc/rc.d/init.d/rc 2

...

ca:12345:ctrlaltdel:/sbin/shutdown -t1 -a -r now

su:S016:once:/sbin/sulogin

1:2345:respawn:/sbin/agetty tty1 9600

2:2345:respawn:/sbin/agetty tty2 9600

...

# End /etc/inittab

etc目录结构

只是一部分,有删减。

.

├── fstab

├── <em>inittab</em>

├── inputrc

├── profile

├── rc.d

│ ├── init.d

│ │ ├── checkfs

│ │ ├── cleanfs

...

│ │ ├── modules

│ │ ├── mountfs

│ │ ├── mountkernfs

│ │ ├── network

│ │ ├── rc#when boot, run.

│ │ ├── reboot

...

│ ├── rc0.d

│ │ ├── K80network ->../init.d/network

│ │ ├── K90sysklogd ->../init.d/sysklogd

│ │ ├── S60sendsignals ->../init.d/sendsignals

│ │ ├── S70mountfs ->../init.d/mountfs

│ │ ├── S80swap ->../init.d/swap

│ │ ├── S90localnet ->../init.d/localnet

│ │ └── S99halt ->../init.d/halt

│ ├── rc1.d

│ │ ├── K80network ->../init.d/network

│ │ └── K90sysklogd ->../init.d/sysklogd

│ ├── rc2.d

│ │ ├── K80network ->../init.d/network

│ │ └── K90sysklogd ->../init.d/sysklogd

│ ├── rc3.d

│ │ ├── S10sysklogd ->../init.d/sysklogd

│ │ └── S20network ->../init.d/network

│ ├── rc4.d

│ │ ├── S10sysklogd ->../init.d/sysklogd

│ │ └── S20network ->../init.d/network

│ ├── rc5.d

│ │ ├── S10sysklogd ->../init.d/sysklogd

│ │ └── S20network ->../init.d/network

│ ├── rc6.d

│ │ ├── K80network ->../init.d/network

│ │ ├── K90sysklogd ->../init.d/sysklogd

│ │ ├── S60sendsignals ->../init.d/sendsignals

│ │ ├── S70mountfs ->../init.d/mountfs

│ │ ├── S80swap ->../init.d/swap

│ │ ├── S90localnet ->../init.d/localnet

│ │ └── S99reboot ->../init.d/reboot

│ └── rcsysinit.d

│ ├── S00mountkernfs ->../init.d/mountkernfs

│ ├── S02consolelog ->../init.d/consolelog

│ ├── S05modules ->../init.d/modules

...

├── udev

│ ├── rules.d

│ │ └── 55-lfs.rules

│ └── udev.conf

└── vimrc

network脚本

#!/bin/sh

. /etc/sysconfig/rc

. ${rc_functions}

. /etc/sysconfig/network

case "${1}" in

start)

# Start all network interfaces

for file in ${network_devices}/ifconfig.*

do

interface=${file##*/ifconfig.}

# skip if $file is * (because nothing was found)

if [ "${interface}" = "*" ]

then

continue

fi

IN_BOOT=1 ${network_devices}/ifup ${interface}

done

stop)

# Reverse list

FILES=""

for file in ${network_devices}/ifconfig.*

do

FILES="${file} ${FILES}"

done

# Stop all network interfaces

for file in ${FILES}

do

interface=${file##*/ifconfig.}

# skip if $file is * (because nothing was found)

if [ "${interface}" = "*" ]

then

continue

fi

IN_BOOT=1 ${network_devices}/ifdown ${interface}

done

restart)

${0} stop

sleep 1

${0} start

*)

echo "Usage: ${0} {start|stop|restart}"

exit 1

esac

# End /etc/rc.d/init.d/network



页表用来把虚拟页映射到物理页,并且存放页的保护位(即访问权限)。

在Linux4.11版本以前,Linux内核把页表分为4级:

页全局目录表(PGD)、页上层目录(PUD)、页中间目录(PMD)、直接页表(PT)

4.11版本把页表扩展到5级,在页全局目录和页上层目录之间增加了 页四级目录(P4D)

各处处理器架构可以选择使用5级,4级,3级或者2级页表,同一种处理器在页长度不同的情况可能选择不同的页表级数。可以使用配置宏CONFIG_PGTABLE_LEVELS配置页表的级数,一般使用默认值。

如果选择4级页表,那么使用PGD,PUD,PMD,PT;如果使用3级页表,那么使用PGD,PMD,PT;如果选择2级页表,那么使用PGD和PT。 如果不使用页中间目录 ,那么内核模拟页中间目录,调用函数pmd_offset 根据页上层目录表项和虚拟地址获取页中间目录表项时 直接把页上层目录表项指针强制转换成页中间目录表项

每个进程有独立的页表,进程的mm_struct实例的成员pgd指向页全局目录,前面四级页表的表项存放下一级页表的起始地址,直接页表的页表项存放页帧号(PFN)

内核也有一个页表, 0号内核线程的进程描述符init_task的成员active_mm指向内存描述符init_mm,内存描述符init_mm的成员pgd指向内核的页全局目录swapper_pg_dir

ARM64处理器把页表称为转换表,最多4级。ARM64处理器支持三种页长度:4KB,16KB,64KB。页长度和虚拟地址的宽度决定了转换表的级数,在虚拟地址的宽度为48位的条件下,页长度和转换表级数的关系如下所示:

ARM64处理器把表项称为描述符,使用64位的长描述符格式。描述符的0bit指示描述符是不是有效的:0表示无效,1表示有效。第1位指定描述符类型。

在块描述符和页描述符中,内存属性被拆分为一个高属性和一个低属性块。

处理器的MMU负责把虚拟地址转换成物理地址,为了改进虚拟地址到物理地址的转换速度,避免每次转换都需要查询内存中的页表,处理器厂商在管理单元里加了称为TLB的高速缓存,TLB直译为转换后备缓冲区,意译为页表缓存。

页表缓存用来缓存最近使用过的页表项, 有些处理器使用两级页表缓存 第一级TLB分为指令TLB和数据TLB,好处是取指令和取数据可以并行;第二级TLB是统一TLB,即指令和数据共用的TLB

不同处理器架构的TLB表项的格式不同。ARM64处理器的每条TLB表项不仅包含虚拟地址和物理地址,也包含属性:内存类型、缓存策略、访问权限、地址空间标识符(ASID)和虚拟机标识符(VMID)。 地址空间标识符区分不同进程的页表项 虚拟机标识符区分不同虚拟机的页表项

如果内核修改了可能缓存在TLB里面的页表项,那么内核必须负责使旧的TLB表项失效,内核定义了每种处理器架构必须实现的函数。

当TLB没有命中的时候,ARM64处理器的MMU自动遍历内存中的页表,把页表项复制到TLB,不需要软件把页表项写到TLB,所以ARM64架构没有提供写TLB的指令。

为了减少在进程切换时清空页表缓存的需要,ARM64处理器的页表缓存使用非全局位区分内核和进程的页表项(nG位为0表示内核的页表项), 使用地址空间标识符(ASID)区分不同进程的页表项

ARM64处理器的ASID长度是由具体实现定义的,可以选择8位或者16位。寄存器TTBR0_EL1或者TTBR1_EL1都可以用来存放当前进程的ASID,通常使用寄存器TCR_EL1的A1位决定使用哪个寄存器存放当前进程的ASID,通常使用寄存器 TTBR0_EL1 。寄存器TTBR0_EL1的位[63:48]或者[63:56]存放当前进程的ASID,位[47:1]存放当前进程的页全局目录的物理地址。

在SMP系统中,ARM64架构要求ASID在处理器的所有核是唯一的。假设ASID为8位,ASID只有256个值,其中0是保留值,可分配的ASID范围1~255,进程的数量可能超过255,两个进程的ASID可能相同,内核引入ASID版本号解决这个问题。

(1)每个进程有一个64位的软件ASID, 低8位存放硬件ASID,高56位存放ASID版本号

(2) 64位全局变量asid_generation的高56位保存全局ASID版本号

(3) 当进程被调度时,比较进程的ASID版本号和全局版本号 。如果版本号相同,那么直接使用上次分配的ASID,否则需要给进程重新分配硬件ASID。

存在空闲ASID,那么选择一个分配给进程。不存在空闲ASID时,把全局ASID版本号加1,重新从1开始分配硬件ASID,即硬件ASID从255回绕到1。因为刚分配的硬件ASID可能和某个进程的ASID相同,只是ASID版本号不同,页表缓存可能包含了这个进程的页表项,所以必须把所有处理器的页表缓存清空。

引入ASID版本号的好处是:避免每次进程切换都需要清空页表缓存,只需要在硬件ASID回环时把处理器的页表缓存清空

虚拟机里面运行的客户 *** 作系统的虚拟地址转物理地址分两个阶段:

(1) 把虚拟地址转换成中间物理地址,由客户 *** 作系统的内核控制 ,和非虚拟化的转换过程相同。

(2) 把中间物理地址转换成物理地址,由虚拟机监控器控制 ,虚拟机监控器为每个虚拟机维护一个转换表,分配一个虚拟机标识符,寄存器 VTTBR_EL2 存放当前虚拟机的阶段2转换表的物理地址。

每个虚拟机有独立的ASID空间 ,页表缓存使用 虚拟机标识符 区分不同虚拟机的转换表项,避免每次虚拟机切换都要清空页表缓存,在虚拟机标识符回绕时把处理器的页表缓存清空。


欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/yw/6272687.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2023-03-19
下一篇 2023-03-19

发表评论

登录后才能评论

评论列表(0条)

保存