Fuchsia X86平台 MMU操作

这段时间在看Fuchsia的代码,发现有很多去自己去做虚拟地址与物理地址映射的代码,一直觉得很奇怪,之前一直觉得MMU做虚拟地址与物理地址的映射,只是构建完成相关的映射表格,在x86上把这个表格指给cr3寄存器,后面开启虚拟地址后,程序访问的都是虚拟地址空间,MMU把虚拟地址转成物理地址,然后进行相关的寻址操作就欧克,至于这个映射表格的建立,我一直以为是一次性的,或者产生缺页的时候由MMU去维护更新就好了,刚好趁着这个机会,边读fuchsia的代码边理解下怎么去维护MMU的虚拟地址与物理地址的映射关系

什么是MMU

为了理解什么是MMU,可能先得捋清楚虚拟内存与物理内存的区别。

物理内存

这个很好理解,比如你买了台电脑,里面装了2跟8G的内存条,那么这个物理内存的大小就是16G,而物理地址空间,则是用来描述物理地址的范围的,这个跟芯片平台有比较大的关系,如果目前在intel X86的芯片上,对物理内存大小的支持分别如下:

  1. 分段方式:支持的 CPU: 8086 以上
    地址长度: 20
    寻址能力: 1M
  2. 分页方式:支持的 CPU: 80386 以上, PSE 需要 Pentium 以上
    地址长度: 32
    寻址能力: 4GB
  3. PAE: Physical Address Extension 物理地址扩展模式:支持的 CPU: Pentium Pro 以上
    地址长度: 36
    寻址能力: 64GB
  4. 长模式 (long-mode, IA-32e 模式):支持的CPU: x86-64 的 CPU
    地址长度: 48
    寻址能力: 256 TB

目前fuchsia只支持intel 64位的CPU,所以不用关注前面三种的物理地址类型,只需要关注x86-64下的长模式就行,在这种模式下采用的是64位物理地址,但是只要低48位是有效地址,高16位是相关的标志位。至于为啥是48位有效地址,这个咱们就不关注了,反正是intel就是这么设计的。

虚拟内存

虚拟内存这个也好理解,意思就是这个内存所指向的数据不是真实可用的嘛,这个地址是需要一系列的转换才能转换成真实可用的数据(物理内存上的数据),这一系列的转换包括逻辑地址经过通过分段机制转成线性地址,线性地址再经过分页机制再转成了物理地址,而这个分页机制则是在我们这篇文章里需要重点探讨的。这里的逻辑地址与线性地址都可以认为是虚拟地址。

虚拟内存的存在的由来是,主要要两个原因:

  1. 因为硬件内存的多样性,比如一台电脑有2G的物理内存也有4G的物理内存还可能有16G的物理内存,不管是操作系统还是应用程序都不希望自己需要根据真实物理内存的大小去做相关的调整,这样估计得烦死,这就是为啥引入虚拟内存,目的就是为了屏蔽掉机器上物理内存的差异,让操作系统或者应用程序都只采用虚拟地址。
  2. 物理内存是需要钱的,你买个8G内存跟16G内存价格是有差异的,但是虚拟内存是不要钱的。又不要钱,又能让操作系统跟应用的开发者不那么头疼,这样的事大家都愿意接受哈。

还有一个关注点就是,比如x86 64位是机器上虚拟地址空间往往也是48位的有效地址,那么可以寻址的空间大小就是256TB,物理地址也是采用48位的有效地址,物理地址的寻址空间大小也是256TB,但是有个问题:谁的机器上会有256TB的内存?目前主流的机器基本上是16G内存。怎么把256TB的虚拟地址映射到16G的真实物理地址上?这也是MMU需要去做的。

MMU

正因为上面所讲的目前计算机体系中有物理内存与虚拟内存的存在,同是这就就引入了一个虚拟地址与物理地址的转换问题。如前面讲到的,经过分段机制之后的线性地址才会经过分页机制去转成最终的物理地址,这就是MMU所干的活:
虚拟地址------》物理地址
分段机制,就不在这里赘述了,重点讲讲分页机制。
简单用文字描述下:

  1. X86上有一系列的页表来完成分页机制。
  2. X86平台上有一个CR3的控制寄存器,这个寄存器里存放的是一个顶级页表的物理地址,这个页表叫做PLM4
  3. 在X86 64位平台上采用了四级页表的方式,分别是PML4表,PDP表(Page directory point),PD表(Page directory),PT表(Page table)
  4. 上面的各个表是一个级联的指向关系:PML4指向PDP,PDP指向PD,PD指向PT,PT指向具体的物理页面。
  5. 48位的虚拟地址是这样去分的,最高的9位标记在PML4表指向哪个PDP,后面9位标记PDP表中指向哪个PD,再后面9位是标记PD表中指向哪个PT,再跟着的9位是标记PT表中指向的是哪个物理页地址,最后12位是物理页内的偏移,注意这里的物理页大小采用的是4K模式。
    用一张Intel官方的图来概括以上内容:
    Fuchsia X86平台 MMU操作_第1张图片

Fuchsia上X86平台的MMU操作

在上面,大致介绍了下虚拟内存与物理内存,已经虚拟内存与物理内存映射的基本概念,接下来我们看看在X86平台上大致是怎样的一种操作方式。

映射表的创建

如上面提到的在X86平台上这个映射表可以直接叫PML4表,另外如我们在上一遍文章Fuchsia X86 kernel启动代码分析中提到的,在Start.S中我们可以看到这样的代码:

  /*
     * Set PGE to enable global kernel pages
     */
    mov   %cr4, %rax
    or    $(X86_CR4_PGE), %rax
    mov   %rax, %cr4
//将pml4放进cr3中,物理内存与虚拟内存映射生效
    /* load the physical pointer to the top level page table */
    mov  $PHYS(pml4), %rax
    mov  %rax, %cr3
————————————————
版权声明:本文为CSDN博主「影子LEON」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/ljp1205/article/details/106231332

至于整个PML4表的创建过程,依然可以参考Fuchsia X86 kernel启动代码分析,在这个里面重点介绍了下,在这个阶段重点做了fuchsia kernel的地址空间的虚拟地址与物理地址映射。

今天我们这里重点介绍下后续这个PML4表格的更新,建立真实可用的映射关系。

映射表的维护

在进入介绍怎么维护更新映射表之前,可能需要先大致捋一下在Fuchsia当中关于内存的一些简单概念:

  1. VmAspace:这个是一整个范围的虚拟地址空间,这个虚拟地址空间有起始地址与范围长度,这个概念是整个虚拟地址的一个子集,比如整个虚拟地址空间是用48位来描述,相当与整个虚拟地址空间是0到256TB的范围,而一个VmAspace则是0到256TB这个范围中的一段。Fuchsia在启动过程中会为Kernel建立一个Kernel 的VmAspace:
void VmAspace::KernelAspaceInitPreHeap() TA_NO_THREAD_SAFETY_ANALYSIS {
  // the singleton kernel address space
  static VmAspace _kernel_aspace(KERNEL_ASPACE_BASE, KERNEL_ASPACE_SIZE, VmAspace::TYPE_KERNEL,
                                 "kernel");

  // the singleton dummy root vmar (used to break a reference cycle in
  // Destroy())
  static VmAddressRegionDummy dummy_vmar;
#if LK_DEBUGLEVEL > 1
  _kernel_aspace.Adopt();
  dummy_vmar.Adopt();
#endif

  dummy_root_vmar = &dummy_vmar;

  static VmAddressRegion _kernel_root_vmar(_kernel_aspace);

  _kernel_aspace.root_vmar_ = fbl::AdoptRef(&_kernel_root_vmar);

  zx_status_t status = _kernel_aspace.Init();
  ASSERT(status == ZX_OK);

  // save a pointer to the singleton kernel address space
  VmAspace::kernel_aspace_ = &_kernel_aspace;
  aspaces.push_front(kernel_aspace_);
}

这个VmAspace的起始地址与大小分别是:

#define KERNEL_ASPACE_BASE 0xffffff8000000000UL  // -512GB
#define KERNEL_ASPACE_SIZE 0x0000008000000000UL

也就是说起点是从512GB的位置开始,大小是64GB

  1. VmAddressRegion :这个是指一段连续的虚拟内存区域,一个VmAspace会有个一个root 的VmAddressRegion,root VmAddressRegion的起始地址与大小与VmAspace的起始地址跟大小一致。然后还会有很多个子的 VmAddressRegion。Kernel VmAspace 中目前有预留一个Kernel VmAddressRegion :
zx_status_t status = aspace->RootVmar()->CreateSubVmar(
      kernel_regions[0].base - aspace->RootVmar()->base(), kernel_region_size, 0,
      VMAR_FLAG_CAN_MAP_SPECIFIC | VMAR_FLAG_SPECIFIC | VMAR_CAN_RWX_FLAGS, "kernel region vmar",
      &kernel_region);
  ASSERT(status == ZX_OK);

  for (const auto& region : kernel_regions) {
    ASSERT(IS_PAGE_ALIGNED(region.base));

    dprintf(INFO,
            "VM: reserving kernel region [%#" PRIxPTR ", %#" PRIxPTR ") flags %#x name '%s'\n",
            region.base, region.base + region.size, region.arch_mmu_flags, region.name);
    status =
        kernel_region->ReserveSpace(region.name, region.base, region.size, region.arch_mmu_flags);
    ASSERT(status == ZX_OK);
  }

在Kernel VmAddressRegion预留了这么些部分的空间地址长度:

const ktl::array _kernel_regions = {
    kernel_region{
        .name = "kernel_code",
        .base = (vaddr_t)__code_start,
        .size = ROUNDUP((uintptr_t)__code_end - (uintptr_t)__code_start, PAGE_SIZE),
        .arch_mmu_flags = ARCH_MMU_FLAG_PERM_READ | ARCH_MMU_FLAG_PERM_EXECUTE,
    },
    kernel_region{
        .name = "kernel_rodata",
        .base = (vaddr_t)__rodata_start,
        .size = ROUNDUP((uintptr_t)__rodata_end - (uintptr_t)__rodata_start, PAGE_SIZE),
        .arch_mmu_flags = ARCH_MMU_FLAG_PERM_READ,
    },
    kernel_region{
        .name = "kernel_data",
        .base = (vaddr_t)__data_start,
        .size = ROUNDUP((uintptr_t)__data_end - (uintptr_t)__data_start, PAGE_SIZE),
        .arch_mmu_flags = ARCH_MMU_FLAG_PERM_READ | ARCH_MMU_FLAG_PERM_WRITE,
    },
    kernel_region{
        .name = "kernel_bss",
        .base = (vaddr_t)__bss_start,
        .size = ROUNDUP((uintptr_t)_end - (uintptr_t)__bss_start, PAGE_SIZE),
        .arch_mmu_flags = ARCH_MMU_FLAG_PERM_READ | ARCH_MMU_FLAG_PERM_WRITE,
    },
};

Kernel VmAspace 与Kernel VmAddressRegion的区别是,Kernel VmAspace描述的是整个kernel内存空间,而Kernel VmAddressRegion描述的是kernel image被装载进内存的空间与大小。

 . = KERNEL_BASE;
    PROVIDE_HIDDEN(__code_start = .);
/kernel/BUILD.gn:95:    "KERNEL_BASE=$kernel_base"
  if (current_cpu == "arm64") {
    kernel_base = "0xffffffff00000000"
  } else if (current_cpu == "x64") {
    kernel_base = "0xffffffff80100000"  # Has KERNEL_LOAD_OFFSET baked into it.
  }

可以看到Kernel VmAddressRegion的base地址是0xffffffff80100000,而Kernel VmAspace的base地址是0xffffffff80000000

  1. VmMapping:前面的VmAspace与VmAddressRegion都是一段段的虚拟内存,还没有跟物理内存挂上钩,而VmMapping则是需要真实去分配物理内存的,也就是说VmMapping是需要分配物理的内存的比较小的虚拟内存颗粒度,当某个VmAddressRegion需要映射到具体的物理内存上的时候,VmAddressRegion会通过构建VmMapping的方式去映射。VmAspace太大了,像Kernel VmAspace就是64G的长度,不可能都去映射到物理内存,只有小点的VmAddressRegion才会去映射
zx_status_t VmAddressRegion::ReserveSpace(const char* name, vaddr_t base, size_t size,
                                          uint arch_mmu_flags) {
  canary_.Assert();
  if (!is_in_range(base, size)) {
    return ZX_ERR_INVALID_ARGS;
  }
  size_t offset = base - base_;
  // We need a zero-length VMO to pass into CreateVmMapping so that a VmMapping would be created.
  // The VmMapping is already mapped to physical pages in start.S.
  // We would never call MapRange on the VmMapping, thus the VMO would never actually allocate any
  // physical pages and we would never modify the PTE except for the permission change bellow
  // caused by Protect.
  fbl::RefPtr<VmObject> vmo;
  zx_status_t status = VmObjectPaged::Create(PMM_ALLOC_FLAG_ANY, 0u, 0, &vmo);
  if (status != ZX_OK) {
    return status;
  }
  vmo->set_name(name, strlen(name));
  // allocate a region and put it in the aspace list
  fbl::RefPtr<VmMapping> r(nullptr);
  // Here we use permissive arch_mmu_flags so that the following Protect call would actually
  // call arch_aspace().Protect to change the mmu_flags in PTE.
  status = CreateVmMapping(
      offset, size, 0, VMAR_FLAG_SPECIFIC, vmo, 0,
      ARCH_MMU_FLAG_PERM_READ | ARCH_MMU_FLAG_PERM_WRITE | ARCH_MMU_FLAG_PERM_EXECUTE, name, &r);
  if (status != ZX_OK) {
    return status;
  }
  return r->Protect(base, size, arch_mmu_flags);
zx_status_t VmAddressRegion::CreateVmMapping(size_t mapping_offset, size_t size, uint8_t align_pow2,
                                             uint32_t vmar_flags, fbl::RefPtr<VmObject> vmo,
                                             uint64_t vmo_offset, uint arch_mmu_flags,
                                             const char* name, fbl::RefPtr<VmMapping>* out) {
  DEBUG_ASSERT(out);
  LTRACEF("%p %#zx %#zx %x\n", this, mapping_offset, size, vmar_flags);

  // Check that only allowed flags have been set
  if (vmar_flags & ~(VMAR_FLAG_SPECIFIC | VMAR_FLAG_SPECIFIC_OVERWRITE | VMAR_CAN_RWX_FLAGS)) {
    return ZX_ERR_INVALID_ARGS;
  }

  // Validate that arch_mmu_flags does not contain any prohibited flags
  if (!is_valid_mapping_flags(arch_mmu_flags)) {
    return ZX_ERR_ACCESS_DENIED;
  }

  // If size overflows, it'll become 0 and get rejected in
  // CreateSubVmarInternal.
  size = ROUNDUP(size, PAGE_SIZE);

  // Make sure that vmo_offset is aligned and that a mapping of this size
  // wouldn't overflow the vmo offset.
  if (!IS_PAGE_ALIGNED(vmo_offset) || vmo_offset + size < vmo_offset) {
    return ZX_ERR_INVALID_ARGS;
  }

  // If we're mapping it with a specific permission, we should allow
  // future Protect() calls on the mapping to keep that permission.
  if (arch_mmu_flags & ARCH_MMU_FLAG_PERM_READ) {
    vmar_flags |= VMAR_FLAG_CAN_MAP_READ;
  }
  if (arch_mmu_flags & ARCH_MMU_FLAG_PERM_WRITE) {
    vmar_flags |= VMAR_FLAG_CAN_MAP_WRITE;
  }
  if (arch_mmu_flags & ARCH_MMU_FLAG_PERM_EXECUTE) {
    vmar_flags |= VMAR_FLAG_CAN_MAP_EXECUTE;
  }

  fbl::RefPtr<VmAddressRegionOrMapping> res;
  zx_status_t status =
      CreateSubVmarInternal(mapping_offset, size, align_pow2, vmar_flags, ktl::move(vmo),
                            vmo_offset, arch_mmu_flags, name, &res);
  if (status != ZX_OK) {
    return status;
  }
  // TODO(teisenbe): optimize this
  *out = res->as_vm_mapping();
  return ZX_OK;
}

4.X86PageTableBase:这个是X86平台上真正操作MMU的部分,里面有一堆的操作MMU 转换表的函数方法。

接下来我们通过追踪一下整个流程看看到底怎么更新MMU的PML4表格的:

前面讲到,当需要把虚拟地址映射到物理地址上的时候,会去创建VmMapping对象,而VmMapping会利用VmObject对象去做真实的物理内存分配。在这里我们不管具体怎么分配的内存,只关心分配到的物理内存如何与虚拟内存建立映射关系。

我们从VmMapping 对象的MapRange函数入手:

zx_status_t VmMapping::MapRange(size_t offset, size_t len, bool commit) {
  canary_.Assert();

  len = ROUNDUP(len, PAGE_SIZE);
  if (len == 0) {
    return ZX_ERR_INVALID_ARGS;
  }

  Guard<Mutex> aspace_guard{aspace_->lock()};
  if (state_ != LifeCycleState::ALIVE) {
    return ZX_ERR_BAD_STATE;
  }

  LTRACEF("region %p, offset %#zx, size %#zx, commit %d\n", this, offset, len, commit);

  DEBUG_ASSERT(object_);
  if (!IS_PAGE_ALIGNED(offset) || !is_in_range(base_ + offset, len)) {
    return ZX_ERR_INVALID_ARGS;
  }

  // precompute the flags we'll pass GetPageLocked
  // if committing, then tell it to soft fault in a page
  uint pf_flags = VMM_PF_FLAG_WRITE;
  if (commit) {
    pf_flags |= VMM_PF_FLAG_SW_FAULT;
  }

  // grab the lock for the vmo
  Guard<Mutex> object_guard{object_->lock()};

  // set the currently faulting flag for any recursive calls the vmo may make back into us.
  DEBUG_ASSERT(!currently_faulting_);
  currently_faulting_ = true;
  auto ac = fbl::MakeAutoCall([&]() { currently_faulting_ = false; });

  // iterate through the range, grabbing a page from the underlying object and
  // mapping it in
  size_t o;
  VmMappingCoalescer coalescer(this, base_ + offset);
  for (o = offset; o < offset + len; o += PAGE_SIZE) {
    uint64_t vmo_offset = object_offset_ + o;

    zx_status_t status;
    paddr_t pa;
    status = object_->GetPageLocked(vmo_offset, pf_flags, nullptr, nullptr, nullptr, &pa);
    if (status != ZX_OK) {
      // no page to map
      if (commit) {
        // fail when we can't commit every requested page
        coalescer.Abort();
        return status;
      }

      // skip ahead
      continue;
    }

    vaddr_t va = base_ + o;
    LTRACEF_LEVEL(2, "mapping pa %#" PRIxPTR " to va %#" PRIxPTR "\n", pa, va);
    status = coalescer.Append(va, pa);
    if (status != ZX_OK) {
      return status;
    }
  }
  return coalescer.Flush();
}

这个函数是去map(也就是去分配)size大小的物理内存。里面关键的几行代码:

VmMappingCoalescer coalescer(this, base_ + offset);
  for (o = offset; o < offset + len; o += PAGE_SIZE) {
    uint64_t vmo_offset = object_offset_ + o;

    zx_status_t status;
    paddr_t pa;
    status = object_->GetPageLocked(vmo_offset, pf_flags, nullptr, nullptr, nullptr, &pa);
    if (status != ZX_OK) {
      // no page to map
      if (commit) {
        // fail when we can't commit every requested page
        coalescer.Abort();
        return status;
      }

      // skip ahead
      continue;
    }

    vaddr_t va = base_ + o;
    LTRACEF_LEVEL(2, "mapping pa %#" PRIxPTR " to va %#" PRIxPTR "\n", pa, va);
    status = coalescer.Append(va, pa);
    if (status != ZX_OK) {
      return status;
    }
status = object_->GetPageLocked(vmo_offset, pf_flags, nullptr, nullptr, nullptr, &pa)

每次通过VmObject对象分配到一个物理页,pa保存的是物理页的地址,也就是物理地址,然后将虚拟地址与对应的物理地址统一放在coalescer中:

status = coalescer.Append(va, pa);

然后,虚拟地址加上一个页面大小,再去分配对应的物理页,直到size大小。这里要注意一下,虚拟地址是肯定连续的,但是物理地址是不一定连续的。

接下来就需要把虚拟内存与对应的物理内存的对应关系加到MMU的映射表中:

zx_status_t VmMappingCoalescer::Flush() {
  if (count_ == 0) {
    return ZX_OK;
  }

  uint flags = mapping_->arch_mmu_flags();
  if (flags & ARCH_MMU_FLAG_PERM_RWX_MASK) {
    size_t mapped;
    zx_status_t ret = mapping_->aspace()->arch_aspace().Map(base_, phys_, count_, flags, &mapped);
    if (ret != ZX_OK) {
      TRACEF("error %d mapping %zu pages starting at va %#" PRIxPTR "\n", ret, count_, base_);
      aborted_ = true;
      return ret;
    }
    DEBUG_ASSERT(mapped == count_);
  }
  base_ += count_ * PAGE_SIZE;
  count_ = 0;
  return ZX_OK;
}
zx_status_t X86ArchVmAspace::Map(vaddr_t vaddr, paddr_t* phys, size_t count, uint mmu_flags,
                                      size_t* mapped) {
  if (!IsValidVaddr(vaddr))
    return ZX_ERR_INVALID_ARGS;

  return pt_->MapPages(vaddr, phys, count, mmu_flags, mapped);
}

然后直接到X86PageTableBase中:

zx_status_t X86PageTableBase::MapPages(vaddr_t vaddr, paddr_t* phys, size_t count, uint mmu_flags,
                                       size_t* mapped) {
  canary_.Assert();

  LTRACEF("aspace %p, vaddr %#" PRIxPTR " count %#zx mmu_flags 0x%x\n", this, vaddr, count,
          mmu_flags);

  if (!check_vaddr(vaddr))
    return ZX_ERR_INVALID_ARGS;
  for (size_t i = 0; i < count; ++i) {
    if (!check_paddr(phys[i]))
      return ZX_ERR_INVALID_ARGS;
  }
  if (count == 0)
    return ZX_OK;

  if (!allowed_flags(mmu_flags))
    return ZX_ERR_INVALID_ARGS;

  PageTableLevel top = top_level();
  ConsistencyManager cm(this);
  {
    Guard<Mutex> a{&lock_};
    DEBUG_ASSERT(virt_);

    // TODO(teisenbe): Improve performance of this function by integrating deeper into
    // the algorithm (e.g. make the cursors aware of the page array).
    size_t idx = 0;
    auto undo = fbl::MakeAutoCall([&]() {
      AssertHeld(lock_);
      if (idx > 0) {
        MappingCursor start = {
            .paddr = 0,
            .vaddr = vaddr,
            .size = idx * PAGE_SIZE,
        };

        MappingCursor result;
        RemoveMapping(virt_, top, start, &result, &cm);
        DEBUG_ASSERT(result.size == 0);
      }
      cm.Finish();
    });

    vaddr_t v = vaddr;
    for (; idx < count; ++idx) {
      MappingCursor start = {
          .paddr = phys[idx],
          .vaddr = v,
          .size = PAGE_SIZE,
      };
      MappingCursor result;
      zx_status_t status = AddMapping(virt_, mmu_flags, top, start, &result, &cm);
      if (status != ZX_OK) {
        dprintf(SPEW, "Add mapping failed with err=%d\n", status);
        return status;
      }
      DEBUG_ASSERT(result.size == 0);

      v += PAGE_SIZE;
    }

    undo.cancel();
    cm.Finish();
  }

  if (mapped) {
    *mapped = count;
  }
  return ZX_OK;
}

这个函数的几个参数分别是
vaddr_t vaddr 虚拟地址
paddr_t* phys 分配到的物理页地址数组。
size_t count 分别到的物理页数目

这个函数主要逻辑是以物理页为单元,为每个页面构建一个MappingCursor对象

      MappingCursor start = {
          .paddr = phys[idx],
          .vaddr = v,
          .size = PAGE_SIZE,
      };

然后为每个物理页的映射关系更新到MMU的映射表当中:

zx_status_t X86PageTableBase::AddMapping(volatile pt_entry_t* table, uint mmu_flags,
                                         PageTableLevel level, const MappingCursor& start_cursor,
                                         MappingCursor* new_cursor, ConsistencyManager* cm) {
  DEBUG_ASSERT(table);
  DEBUG_ASSERT(check_vaddr(start_cursor.vaddr));
  DEBUG_ASSERT(check_paddr(start_cursor.paddr));

  zx_status_t ret = ZX_OK;
  *new_cursor = start_cursor;

  if (level == PT_L) {
    return AddMappingL0(table, mmu_flags, start_cursor, new_cursor, cm);
  }

  auto abort = fbl::MakeAutoCall([&]() {
    AssertHeld(lock_);
    if (level == top_level()) {
      MappingCursor cursor = start_cursor;
      MappingCursor result;
      // new_cursor->size should be how much is left to be mapped still
      cursor.size -= new_cursor->size;
      if (cursor.size > 0) {
        RemoveMapping(table, level, cursor, &result, cm);
        DEBUG_ASSERT(result.size == 0);
      }
    }
  });

  IntermediatePtFlags interm_flags = intermediate_flags();
  PtFlags term_flags = terminal_flags(level, mmu_flags);

  size_t ps = page_size(level);
  bool level_supports_large_pages = supports_page_size(level);
  uint index = vaddr_to_index(level, new_cursor->vaddr);
  for (; index != NO_OF_PT_ENTRIES && new_cursor->size != 0; ++index) {
    volatile pt_entry_t* e = table + index;
    pt_entry_t pt_val = *e;
    // See if there's a large page in our way
    if (IS_PAGE_PRESENT(pt_val) && IS_LARGE_PAGE(pt_val)) {
      return ZX_ERR_ALREADY_EXISTS;
    }

    // Check if this is a candidate for a new large page
    bool level_valigned = page_aligned(level, new_cursor->vaddr);
    bool level_paligned = page_aligned(level, new_cursor->paddr);
    if (level_supports_large_pages && !IS_PAGE_PRESENT(pt_val) && level_valigned &&
        level_paligned && new_cursor->size >= ps) {
      UpdateEntry(cm, level, new_cursor->vaddr, table + index, new_cursor->paddr,
                  term_flags | X86_MMU_PG_PS, false /* was_terminal */);
      new_cursor->paddr += ps;
      new_cursor->vaddr += ps;
      new_cursor->size -= ps;
      DEBUG_ASSERT(new_cursor->size <= start_cursor.size);
    } else {
      // See if we need to create a new table
      if (!IS_PAGE_PRESENT(pt_val)) {
        volatile pt_entry_t* m = AllocatePageTable();
        if (m == nullptr) {
          // The mapping wasn't fully updated, but there is work here
          // that might need to be undone.
          size_t partial_update = fbl::min(ps, new_cursor->size);
          new_cursor->paddr += partial_update;
          new_cursor->vaddr += partial_update;
          new_cursor->size -= partial_update;
          return ZX_ERR_NO_MEMORY;
        }

        LTRACEF_LEVEL(2, "new table %p at level %d\n", m, level);

        UpdateEntry(cm, level, new_cursor->vaddr, e, X86_VIRT_TO_PHYS(m), interm_flags,
                    false /* was_terminal */);
        pt_val = *e;
        pages_++;
      }

      MappingCursor cursor;
      ret = AddMapping(get_next_table_from_entry(pt_val), mmu_flags, lower_level(level),
                       *new_cursor, &cursor, cm);
      *new_cursor = cursor;
      DEBUG_ASSERT(new_cursor->size <= start_cursor.size);
      if (ret != ZX_OK) {
        return ret;
      }
    }
  }
  abort.cancel();
  return ZX_OK;
}
zx_status_t X86PageTableBase::AddMappingL0(volatile pt_entry_t* table, uint mmu_flags,
                                           const MappingCursor& start_cursor,
                                           MappingCursor* new_cursor, ConsistencyManager* cm) {
  DEBUG_ASSERT(IS_PAGE_ALIGNED(start_cursor.size));

  *new_cursor = start_cursor;

  PtFlags term_flags = terminal_flags(PT_L, mmu_flags);

  uint index = vaddr_to_index(PT_L, new_cursor->vaddr);
  for (; index != NO_OF_PT_ENTRIES && new_cursor->size != 0; ++index) {
    volatile pt_entry_t* e = table + index;
    if (IS_PAGE_PRESENT(*e)) {
      return ZX_ERR_ALREADY_EXISTS;
    }

    UpdateEntry(cm, PT_L, new_cursor->vaddr, e, new_cursor->paddr, term_flags,
                false /* was_terminal */);

    new_cursor->paddr += PAGE_SIZE;
    new_cursor->vaddr += PAGE_SIZE;
    new_cursor->size -= PAGE_SIZE;
    DEBUG_ASSERT(new_cursor->size <= start_cursor.size);
  }

  return ZX_OK;
}

上面是两个核心函数。如前面所介绍,MMU的映射表一共有4级,最后一级是叫PT表。

PML4表,PDP表(Page directory point),PD表(Page directory),PT表(Page table)这四级表,每个表都占用了一个物理页面(4K大小),每个页面都存放了512个64位的指针,每个指针都指向的是下一级的页表,PML4表可以指向512个PDP表,每个PDP表中的每个指针可以指向一个PD表,也就是每个PDP表可以指向512个PD表,依次类推。最终PT表的每个64位指针指向的就是真实的物理页面。这个四级映射表的创建是在前面提到的上一篇文章中完成的。

上面的两个函数做的事情就是在找到合适的PT表的合适指针数组,把相应的指针值修改成对应的物理页地址。

简单说起来就是PML4表,PDP表(Page directory point),PD表(Page directory),PT表(Page table)这四个中表项(512个64指针数组中的哪一个成员)是由虚拟地址中有效48位中的从高开始的9位、9位,9位,9位决定的,比如0x000fffffffff000这个地址中,PML4表项是0x1ff,9位全是1,代表的是第511个表项中的指针值指向的PD页表,PD页表项也是0x1ff,代表的是这个表中的第511个表项中的指针值指向的PT页表,PT页表项也是0x1f,代表的是PT页表种第511个表项中指向的物理页地址。这样就完成了一次虚拟地址到物理地址的转换,而上面的两个函数则是建立这样的一个映射关系,可以自己按照前面的介绍去仔细理解逻辑。

好吧,这个就先写到这里了,通过阅读相关代码,发现MMU的映射表是需要OS自己去创建更新的,MMU硬件只在使用的时候会自动按照确定好的映射关系访问相应的物理地址。

你可能感兴趣的:(Fuchsia,Fuchsia,MMU,内存管理)