6.S081——仮想メモリ部——xv6ソースコード完全解析シリーズ(2)

0.簡単に話す

このシリーズのブログの前回の記事は、ここをクリックしてください
。前回のブログでは、主に vm.c ファイル内の 3 つのグローバル変数と 6 つの関数を分析して、xv6 カーネル コードの仮想メモリ部分を詳細に分析しました。前回のブログのペースに沿って次に、次のソースコードを分析して読みます。まず読むべきすべてのソースコードをリストします

1.kernel/memorylayout.h
2.kernel/vm.c(434行) <-----------(このブログで読むコード)
3.kernel/kalloc.c
4.kernel /exec.c
5.kernel/riscv.h

前回は vm.c のソース コードの一部を読みましたが、今回はそれを読みます。こちらのラベルも前回の記事を踏襲しています。

1.kenrel/vm.c

1.8 walkaddr関数

walkaddr関数はウォーク機能のカプセル化層、特にユーザーページテーブルを見つけるために使用されます特定の仮想アドレス va に対応する物理アドレスしたがって、この関数については 2 つの点に注意してください:

1. ユーザー ページ テーブルを検索するためにのみ使用されます
2. walk 関数のような最終層の PTE だけでなく、物理アドレスを返します

。以下のコメント付きのコード:)

// Look up a virtual address, return the physical address,
// or 0 if not mapped.
// Can only be used to look up user pages.
uint64
walkaddr(pagetable_t pagetable, uint64 va)
{
    
    
  pte_t *pte;
  uint64 pa;
  
  // 如果虚拟地址大于最大虚拟地址,返回0
  // 物理地址为0的地方是未被使用的地址空间
  // Question: 为什么不像walk函数一样直接陷入panic?
  if(va >= MAXVA)
    return 0;
  
  // 调用walk函数,直接在用户页表中找到最低一级的PTE
  pte = walk(pagetable, va, 0);
  // 如果此PTE不存在,或者无效,或者用户无权访问
  // 都统统返回0(为什么不陷入panic?)
  if(pte == 0)
    return 0;
  if((*pte & PTE_V) == 0)
    return 0;
  if((*pte & PTE_U) == 0)
    return 0;
  
  // 从PTE中截取下来物理地址页号字段,直接返回
  pa = PTE2PA(*pte);
  return pa;
}

最後に、この関数を呼び出しているのが誰であるかを確認しようとしたところ、この3つの機能

1.コピーイン
2.コピーアウト
3.コピーインストラ

これらは vm.c のハイライトです。カーネル モードとユーザー モード間のデータのコピーを担当します。はい。これら 3 つの機能とその仕組みを詳しく見ていきます。walkaddr 関数を使用してユーザー ページ テーブルにアドレス変換を実装するの。

1.9 フリーウォーク機能

もう 1 つの walk 関数...この関数の機能は次のとおりです。ページ テーブルはマルチレベル構造であるため、この関数の実装では再帰が使用されるため、ページ テーブル ページのメモリを再利用するために特に使用されます。、ソース コードの英語のコメントに記載されているように、この関数を呼び出すときは、それが保証される必要があります。リーフレベルのページテーブルマッピング関係はすべて解放され、解放されます (これは後で uvmunmap 関数を担当します)。ページテーブルページを再利用するために特別に使用されます

uvmunmap関数とfreewalk関数の組み合わせを実現しましたページテーブルページと物理ページの完全リリース


ここに画像の説明を挿入
以下は概略図です。注釈付きのソースコード

// Recursively free page-table pages.
// All leaf mappings must already have been removed.
// 译:递归地释放页表页,所有的叶级别页表映射关系必须已经被解除
void
freewalk(pagetable_t pagetable)
{
    
    
  // there are 2^9 = 512 PTEs in a page table.
  // 每一个页表都正好有512个页表项PTE,所以要遍历它们并尝试逐个释放
  for(int i = 0; i < 512; i++){
    
    
    // 取得对应的PTE
    pte_t pte = pagetable[i];
    // 注意,这里通过标志位的设置来判断是否到达了叶级页表
    // 如果有效位为1,且读位、写位、可执行位都是0
    // 说明这是一个高级别(非叶级)页表项,且此项未被释放,应该去递归地释放
    if((pte & PTE_V) && (pte & (PTE_R|PTE_W|PTE_X)) == 0){
    
    
      // this PTE points to a lower-level page table.
      uint64 child = PTE2PA(pte);
      // 去递归地释放下一级页表
      freewalk((pagetable_t)child);
      // 释放完毕之后,将原有的PTE全部清空,表示已经完全释放
      pagetable[i] = 0;
    // 如果有效位为1,且读位、写位、可执行位有一位为1
    // 表示这是一个叶级PTE,且未经释放,这不符合本函数调用条件,会陷入一个panic
    } else if(pte & PTE_V){
    
    
      panic("freewalk: leaf");
    }
    // 这里隐藏了一个逻辑,即if(pte & PTE_V == 0)
    // 这说明当前PTE已经被释放,不用再次释放了,直接遍历下一个PTE
  }
  // 最后释放页表本身占用的内存,回收,回到上一层递归
  kfree((void*)pagetable);
}

分割線を引いて、次に見てみましょうユーザーアドレス空間の操作に使用される関数、これらの関数はすべて uvm で始まります。最初から一つずつ始めてみましょう!

1.10 uvmcreate関数

この関数の機能は非常に単純で、ユーザープロセス用です。ページテーブルページを割り当て、このページへのポインタを返します。

// create an empty user page table.
// returns 0 if out of memory.
// 译:创建一个空的用户页表,当内存耗尽时返回空指针
pagetable_t
uvmcreate()
{
    
    
  pagetable_t pagetable;
  // 分配一个内存页
  pagetable = (pagetable_t) kalloc();
  if(pagetable == 0)
    return 0;
  // 将此页表的每一个PTE完全清空
  memset(pagetable, 0, PGSIZE);
  return pagetable;
}

1.10 uvminit関数

この関数の機能は、initcode をロードすることです。ユーザーページテーブルのアドレス0上記の initcode は、最初のプロセスを開始するために必要なコードです。この関数の機能は、initcode をユーザー アドレス空間にマップすることです。まず、xv6 でのユーザー アドレス空間の設計を示します。仮想アドレス0からプロセスのコード セグメントが最初に保存されます。オペレーティング システムによって開始された最初のプロセスの場合、この場所はinitcodeコードが配置されています
ここに画像の説明を挿入

// Load the user initcode into address 0 of pagetable,
// for the very first process.
// sz must be less than a page.
// 译:将用户的initcode加载到页表的0地址
// 仅为第一个进程而服务
// 代码的尺寸必须小于一个页(4096 bytes)
void
uvminit(pagetable_t pagetable, uchar *src, uint sz)
{
    
    
  // mem虽然是一个指针,但是因为内核地址空间中虚拟地址和物理地址
  // 在RAM上是直接映射的,所以它其实也就等于物理地址
  char *mem;
  
  // 如果要求分配的大小大于一个页面,则陷入panic
  if(sz >= PGSIZE)
    panic("inituvm: more than a page");
  // 分配一页物理内存作为initcode的存放处,memset用来将当前页清空
  mem = kalloc();
  memset(mem, 0, PGSIZE);
  
  // 在页表中加入一条虚拟地址0 <-> mem的映射,相当于将initcode成功映射到了虚拟地址0
  mappages(pagetable, 0, PGSIZE, (uint64)mem, PTE_W|PTE_R|PTE_X|PTE_U);
  // 将initcode的代码一个字节一个字节地搬运到mem地址
  memmove(mem, src, sz);
}

この機能ではmemmove関数が呼び出されますkernel/string.c で定義されており、実装される関数は次のとおりです。src アドレスから dst アドレスに n バイトをコピーします、そして戻ります宛先アドレスへのポインタよく読んでみると、このコードは文字列をコピーする一連のプロセスを簡潔かつエレガントに実装していて、非常に素晴らしいコードであることがわかりました。

void*
memmove(void *dst, const void *src, uint n)
{
    
    
 // 声明两个活动的指针,用来实时更新拷贝的过程
  const char *s;
  char *d;
  
  // 如果n等于0,说明不需要拷贝字符,直接返回dst
  if(n == 0)
    return dst;
  
  s = src;
  d = dst;
  // case1:src与dst字符串部分重叠时,倒序对字节进行复制,这样可以避免覆盖问题
  if(s < d && s + n > d){
    
    
    // 调整指针到字符串尾部,准备开始倒序复制
    s += n;	
    d += n;
    while(n-- > 0)
      *--d = *--s;
  } else
    // 否则直接正序复制即可
    while(n-- > 0)
      *d++ = *s++;

  return dst;
}

最も重要なことは、分類して議論すること、ただ分析することですSrc アドレスと Dst アドレスの重複に関する潜在的な問題, 重なっている場合は逆コピー(終点から始点へコピー)、それ以外の場合は順コピー(始点から終点へコピー)という、非常に厳密かつ独創的なmemmove関数の実装です。
ここに画像の説明を挿入

1.11 uvmunmap 関数

この関数が行うことは、ユーザープロセスページテーブルの指定範囲のマッピング関係を解除します、仮想アドレス va から始まる npages ページを解放しますが、注意してくださいva はページ位置に揃える必要があります(ここでは、メモリ管理コードで、受信メモリ アドレスの位置合わせが必要なコードと、そうでないコードがあるのはなぜですか?という質問についての拡張的な考え方を示します。)

実際には、uvmunmap関数とfreewalk関数を組み合わせて使用​​します、以前にフリーウォーク機能を見たときのことを今でも覚えています。ページテーブルページの解放を担当しますの場合、ここの uvmunmap はリーフレベルのページテーブル内の PTE レコードを解放する役割を果たします。マッピング関係特に、 do_free フラグが設定されている場合、この関数も同時に、割り当てられた物理ページも回収されます。

// Remove npages of mappings starting from va. va must be
// page-aligned. The mappings must exist.
// Optionally free the physical memory.
// 译:从虚拟地址va开始移除npages个页面的映射关系
// va必须是页对齐的,映射必须存在
// 释放物理内存是可选的
void
uvmunmap(pagetable_t pagetable, uint64 va, uint64 npages, int do_free)
{
    
    
  uint64 a;
  pte_t *pte;
  
  // va不是页对齐的,陷入panic  
  if((va % PGSIZE) != 0)
    panic("uvmunmap: not aligned");
   // 通过遍历释放npages * PGSIZE大小的内存
  for(a = va; a < va + npages*PGSIZE; a += PGSIZE){
    
    
    // 如果虚拟地址在索引过程中对应的中间页表页不存在,陷入panic
    // 回顾一下,walk函数返回0,只有一种情况,那就是某一级页表页在查询时发现不存在
    if((pte = walk(pagetable, a, 0)) == 0)
      panic("uvmunmap: walk");
    
    // 查找成功,但发现此PTE不存在,陷入panic
    if((*pte & PTE_V) == 0)
      panic("uvmunmap: not mapped");
    
    // 查找成功,但发现此PTE除了valid位有效外,其他位均为0
    // 这暗示这个PTE原本不应该出现在叶级页表(奇怪的错误),陷入panic
    if(PTE_FLAGS(*pte) == PTE_V)
      panic("uvmunmap: not a leaf");
    
    // 否则这是一个合法的,应该被释放的PTE
    // 如果do_free被置位,那么还要释放掉PTE对应的物理内存
    if(do_free){
    
    
      uint64 pa = PTE2PA(*pte);
      kfree((void*)pa);
    }
    // 最后将PTE本身全部清空,成功解除了映射关系
    *pte = 0;
  }
}

このコードではマクロ PTE_FLAGS が使用されており、これは次の目的で直接使用されます。PTE のすべてのフラグを抽出します、次のように定義されます。

// 提取出PTE的所有标记位
// 0x3ff相当于保留低10位
#define PTE_FLAGS(pte) ((pte) & 0x3FF)

1.12 uvmdealloc 関数

この機能は次の目的で使用されますユーザーページテーブル内のページをリサイクルする、ユーザープロセスに割り当てられたスペースのサイズをから変更します。oldsz を newsz に変更、そして新しいアドレス空間のサイズを返します。、oldsz が newsz より必ずしも大きいわけではない、つまりこの関数は注目に値します。必ずしもユーザーのアドレス空間の縮小につながるわけではありません

実際、この関数は、冗長プロセス ヒープ メモリ領域を再利用するために使用される sbrk システム コールに直接関連しています。詳細については、growproc 関数 (kernel/proc.c) を参照してください。

// Deallocate user pages to bring the process size from oldsz to
// newsz.  oldsz and newsz need not be page-aligned, nor does newsz
// need to be less than oldsz.  oldsz can be larger than the actual
// process size.  Returns the new process size.
// 译:回收用户页,使得进程的内存大小从oldsz变为newsz。oldsz和newsz不一定要是
// 页对齐的,newsz也不一定要大于oldsz。oldsz可以比当前实际所占用的内存大小更大。
// 函数返回进程新占用的内存大小
uint64
uvmdealloc(pagetable_t pagetable, uint64 oldsz, uint64 newsz)
{
    
    
  // 如果新的内存大小比原先内存还要大,那么什么也不用做,直接返回oldsz即可
  if(newsz >= oldsz)
    return oldsz;
  
  // 如果newsz经过圆整后占据的页面数小于oldsz
  // PGROUNDUP宏定义的讲解见上一篇博客
  if(PGROUNDUP(newsz) < PGROUNDUP(oldsz)){
    
    
    // 计算出来要释放的页面数量
    int npages = (PGROUNDUP(oldsz) - PGROUNDUP(newsz)) / PGSIZE;
    // 调用uvmunmap,清空叶级页表的PTE并释放物理内存
    // 因为我们使用了PGROUNDUP来取整页面数量,所以这里可以保证va是页对齐的
    // 因为用户地址空间是从地址0开始紧密排布的, 所以PGROUNDUP(newsz)对应着新内存大小的结束位置
    // 注意do_free置为1,表示一并回收物理内存
    uvmunmap(pagetable, PGROUNDUP(newsz), npages, 1);
  }

  return newsz;
}

1.13 uvmalloc関数

uvmdealloc関数がありますプロセスメモリを再利用する、それに応じて、ユーザープロセス用の関数が存在します。カーネルに追加のメモリを要求する、これは uvmalloc 関数の役割であり、uvmdealloc 関数と同じです。対応姉妹関数ソース コードの実装を直接見てみましょう。

// Allocate PTEs and physical memory to grow process from oldsz to
// newsz, which need not be page aligned.  Returns new size or 0 on error.
// 译:分配PTE和物理内存来将分配给用户的内存大小从oldsz提升到newsz
// oldsz和newsz不必是页对齐的
// 成功时返回新的内存大小,出错时返回0
uint64
uvmalloc(pagetable_t pagetable, uint64 oldsz, uint64 newsz)
{
    
    
  
  char *mem;
  uint64 a;
  
  // 如果新的内存大小更小,不用分配,直接返回旧内存大小
  if(newsz < oldsz)
    return oldsz;
  
  // 计算原先内存大小需要至少多少页,因为进程地址空间紧密排列
  // 所以这里oldsz指向的其实是原先已经使用内存的下一页,崭新的一页
  oldsz = PGROUNDUP(oldsz);
  // 开始进行新内存的分配
  for(a = oldsz; a < newsz; a += PGSIZE){
    
    
    // 获取一页新的内存
    mem = kalloc();
    // 如果mem为空指针,表示内存耗尽
    // 释放之前分配的所有内存,返回0表示出错
    if(mem == 0){
    
    
      uvmdealloc(pagetable, a, oldsz);
      return 0;
    }
    // 如果分配成功,则将新分配的页面全部清空
    memset(mem, 0, PGSIZE);
	// 并在当前页表项中建立起来到新分配页表的映射
	// mappages函数的讲解见完全解析系列博客(1)
    if(mappages(pagetable, a, PGSIZE, (uint64)mem, PTE_W|PTE_X|PTE_R|PTE_U) != 0){
    
    
      // 如果mappages函数调用返回值不为0,表明在调用walk函数时索引到的PTE无效
      // 释放之前分配的所有内存,返回0表示出错
      kfree(mem);
      uvmdealloc(pagetable, a, oldsz);
      return 0;
    }
  }
  // 如果成功跳出循环,表示执行成功,返回新的内存空间大小
  return newsz;
}

1.14 uvmcopy関数

uvmcopy 関数の目的は次のとおりです。フォークシステムコールサービスの場合、それは親プロセスのアドレス空間全体になりますすべて子プロセスにコピー、これには以下が含まれますページテーブル自体そしてページテーブルが指す物理メモリ内のデータ
ここに画像の説明を挿入

完全なソースコードは次のとおりです。

// Given a parent process's page table, copy
// its memory into a child's page table.
// Copies both the page table and the
// physical memory.
// returns 0 on success, -1 on failure.
// frees any allocated pages on failure.
// 译:给定一个父进程页表,将其内存拷贝到子进程页表中
// 同时拷贝页表和对应的物理内存
// 返回0表示成功,-1表示失败
// 失败时会释放所有已经分配的内存
int
uvmcopy(pagetable_t old, pagetable_t new, uint64 sz)
{
    
    
  pte_t *pte;
  uint64 pa, i;
  uint flags;
  char *mem;
  
  // sz指要复制的地址空间大小,被调用时传入p->sz,表示整个地址空间
  // 对整个地址空间逐页复制
  for(i = 0; i < sz; i += PGSIZE){
    
    
    // 如果寻找过程中发现有PTE不存在,陷入panic
    if((pte = walk(old, i, 0)) == 0)
      panic("uvmcopy: pte should exist");
    // PTE存在但是对应页未被使用,陷入panic
    // 再次强调,用户空间的内存使用是严格紧密的,中间不会有未使用的页存在
    // 自下而上:text、data、guard page、stack、heap
    if((*pte & PTE_V) == 0)
      panic("uvmcopy: page not present");
    
    // 获得对应的物理地址和标志位
    pa = PTE2PA(*pte);
    flags = PTE_FLAGS(*pte);
    // 如果没有成功分配到物理内存,转移到错误处理程序
    if((mem = kalloc()) == 0)
      goto err;
    // 将父进程对应的整个页面复制到新分配的页面中
    memmove(mem, (char*)pa, PGSIZE);
    // 在新的页表中建立映射关系
    if(mappages(new, i, PGSIZE, (uint64)mem, flags) != 0){
    
    
      // 如果映射不成功,则释放掉分配的内存,并转入错误处理程序
      kfree(mem);
      goto err;
    }
  }
  // 成功时返回0
  return 0;
 // 错误处理程序,解除所有已经分配的映射关系,并释放对应的物理内存,返回-1
 err:
  uvmunmap(new, 0, i / PGSIZE, 1);
  return -1;
}

1.15 uvmclear関数

uvmclear 関数は、PTE をクリアするために特別に使用されます。ユーザーのアクセス権、ガードページを設定するための exec 関数用。実装は次のように非常に簡単です。

// mark a PTE invalid for user access.
// used by exec for the user stack guard page.
// 译:将一个PTE标记为用户不可访问的
// 用在exec函数中来进行用户栈守护页的设置
void
uvmclear(pagetable_t pagetable, uint64 va)
{
    
    
  pte_t *pte;
  // 使用walk函数找到对应的PTE
  pte = walk(pagetable, va, 0);
  // 如果找不到va对应的PTE就陷入panic
  if(pte == 0)
    panic("uvmclear");
  // 否则将对应PTE的User位清空
  *pte &= ~PTE_U;
}

1.16 uvmfree関数

uvmunmap が次の目的で使用されるという前に述べたことを思い出してください。リーフレベルのページテーブルのマッピング関係をキャンセルします,フリーウォークページテーブルページを解放するために使用されます、この 2 つを組み合わせてメモリ領域を完全に解放できますか。uvmfree 関数は、この 2 つの単純な組み合わせとカプセル化であり、次の目的で使用されます。ユーザーのアドレス空間を完全に解放します

// Free user memory pages,
// then free page-table pages.
// 译:释放用户内存页
// 然后释放页表页
void
uvmfree(pagetable_t pagetable, uint64 sz)
{
    
    
  // 如果用户内存空间大小大于0,首先调用uvmunmap完全释放所有的叶级页表映射关系和物理页
  if(sz > 0)
    uvmunmap(pagetable, 0, PGROUNDUP(sz)/PGSIZE, 1);
  // 然后再释放页表页
  freewalk(pagetable);
}

この時点では、完全に使用します。ユーザーモードを操作するすべての機能(uvm*)を徹底的に研究、安堵のため息をつきます。次に、copyin、copyout、copyinstr の 3 つの関数が残ります。これら 3 つの関数は専用です。カーネルモードとユーザーモード間のデータ転送

この記事は短くないので、次の記事に書きましょう...

ここをクリックすると次のブログにジャンプします

おすすめ

転載: blog.csdn.net/zzy980511/article/details/129912497