天天看點

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操作

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 = .);
           
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;
    }
           

每次通過VmObject對象配置設定到一個實體頁,pa儲存的是實體頁的位址,也就是實體位址,然後将虛拟位址與對應的實體位址統一放在coalescer中:

然後,虛拟位址加上一個頁面大小,再去配置設定對應的實體頁,直到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硬體隻在使用的時候會自動按照确定好的映射關系通路相應的實體位址。

繼續閱讀