MIT 6s081 blog 1.xv6内存管理

xv6内存管理部分

xv6内存布局

内核地址空间

如xv6指导书中图3.3:从0x80000000开始的地址为内核地址空间,CLINT、PLIC、uart0、virtio disk等为I/O设备(内存映射I/O),可以看到xv6虚拟地址到物理地址的映射,大部分是相等的关系。
在这里插入图片描述

在kernel/memlayout.h中对内存分布进行了宏定义

// kernel/memlayout.h
// the kernel expects there to be RAM
// for use by the kernel and user pages
// from physical address 0x80000000 to PHYSTOP.
#define KERNBASE 0x80000000L
#define PHYSTOP (KERNBASE + 128*1024*1024)

// map the trampoline page to the highest address,
// in both user and kernel space.
#define TRAMPOLINE (MAXVA - PGSIZE)

// map kernel stacks beneath the trampoline,
// each surrounded by invalid guard pages.
#define KSTACK(p) (TRAMPOLINE - ((p)+1)* 2*PGSIZE)

进程地址空间

在这里插入图片描述

在kernel/memlayout.h中对内存分布进行了宏定义

// User memory layout.
// Address zero first:
//   text
//   original data and bss
//   fixed-size stack
//   expandable heap
//   ...
//   TRAPFRAME (p->trapframe, used by the trampoline)
//   TRAMPOLINE (the same page as in the kernel)
#define TRAPFRAME (TRAMPOLINE - PGSIZE)

物理内存分配

xv6对于物理内存的管理使用的是空闲链表,以页为单位进行管理,每次分配/释放都是一页。代码如下:

空闲链表

// kernel/kalloc.c

struct run {
  struct run *next;
};

struct {
  struct spinlock lock;
  struct run *freelist;
} kmem;

使用两个结构体维护一个单向链表,kmem作为全局变量,freelist为空闲链表的头节点,每个链表节点为struct run,包含指向下一个链表节点的结构体指针,并为全局变量kmem维护一把锁用于race condition(竞态条件)

申请物理内存

// Allocate one 4096-byte page of physical memory.
// Returns a pointer that the kernel can use.
// Returns 0 if the memory cannot be allocated.
void *
kalloc(void)
{
  struct run *r;

  acquire(&kmem.lock);
  r = kmem.freelist;
  if(r)
    kmem.freelist = r->next;
  release(&kmem.lock);

  if(r)
    memset((char*)r, 5, PGSIZE); // fill with junk
  return (void*)r;
}

首先获取kmem的锁,r = freelist,将freelist移到freelist->next,此时r就是申请到的物理内存。(也就是获取空闲链表的头节点作为申请到的物理内存,然后空闲链表的头节点往后移一位)

释放物理内存

void
kfree(void *pa)
{
  struct run *r;

  if(((uint64)pa % PGSIZE) != 0 || (char*)pa < end || (uint64)pa >= PHYSTOP)
    panic("kfree");

  // Fill with junk to catch dangling refs.
  memset(pa, 1, PGSIZE);

  r = (struct run*)pa;

  acquire(&kmem.lock);
  r->next = kmem.freelist;
  kmem.freelist = r;
  release(&kmem.lock);
}

void
freerange(void *pa_start, void *pa_end)
{
  char *p;
  p = (char*)PGROUNDUP((uint64)pa_start);
  for(; p + PGSIZE <= (char*)pa_end; p += PGSIZE)
    kfree(p);
}

将传入的要释放的物理地址pa强转为链表节点,将该节点的next节点指向空闲链表头结点,然后将该节点作为空闲链表头节点(也就是链表的头插法)

freerange为封装的kfree函数,用于释放pa_start到pa_end的物理空间

内核页表

三级页表遍历过程

xv6使用三级页表来节约因为存储页表而耗费的大量内存页

在这里插入图片描述

物理内存地址是56bit,其中44bit是物理page号(PPN,Physical Page Number),剩下12bit是offset完全继承自虚拟内存地址。

每个页表项:63-54为预留位,53-10为物理页号,9-0为标志位

xv6使用的是三级页表,首先从satp寄存器中获取最高级页目录的地址,从L2(38:30位)得到索引,根据索引在最高级页目录中找到物理页号(也就是中间级页目录的地址),根据L1(29:21位)得到索引,根据索引在中间级页目录中找到物理页号(也就是最低级页目录的地址),根据L0(20:12位)得到索引,根据索引在最低级页目录中找到物理页号,将最低级的物理页号和offset(11:0位)合并得到最终的56位物理地址。(三级页表的查询方式和每个页表项的结构如上图所示)。

初始化

在kernel/vm.c中声明了内核页表,每个pagetable_t(通过kalloc分配一页空间)包含512个页表项(4096 * 8 / 64 = 512)

typedef uint64 pte_t;
typedef uint64 *pagetable_t; // 512 PTEs
pagetable_t kernel_pagetable; // 内核页表

在main.c中调用kvminit函数创建内核页表,kvminit调用了kvmmake函数,在kvmmake函数中为一些启动必备的区域添加映射到内核页表中,如UART0、VIRTIO0、PLIC、etext、TRAMPOLINE等,

// kernel/vm.c
pagetable_t kpgtbl;

kpgtbl = (pagetable_t) kalloc();
memset(kpgtbl, 0, PGSIZE);

// uart registers
kvmmap(kpgtbl, UART0, UART0, PGSIZE, PTE_R | PTE_W);

// virtio mmio disk interface
kvmmap(kpgtbl, VIRTIO0, VIRTIO0, PGSIZE, PTE_R | PTE_W);
...

以及为每个进程的内核栈建立映射(在每一个进程的生命周期中,必然会通过到系统调用陷入内核。在执行系统调用陷入内核之后,这些内核代码所使用的栈并不是原先进程用户空间中的栈,而是一个单独内核空间的栈,这个称作进程内核栈。)

// Allocate a page for each process's kernel stack.
// Map it high in memory, followed by an invalid
// guard page.
void proc_mapstacks(pagetable_t kpgtbl) {
  struct proc *p;
  
  for(p = proc; p < &proc[NPROC]; p++) {
    char *pa = kalloc();
    if(pa == 0)
      panic("kalloc");
    uint64 va = KSTACK((int) (p - proc));
    kvmmap(kpgtbl, va, (uint64)pa, PGSIZE, PTE_R | PTE_W);
  }
}

添加映射

添加映射使用的函数是kvmmap,将添加va->pa、长度为sz、标志位为perm的映射(PTE)

kvmmap->mappages->walk

// kvmmap调用了mappages
void kvmmap(pagetable_t kpgtbl, uint64 va, uint64 pa, uint64 sz, int perm)
{
  if(mappages(kpgtbl, va, sz, pa, perm) != 0)
    panic("kvmmap");
}

// 向pagetable页表中添加长度为sz,va->pa,标志为perm的映射
int mappages(pagetable_t pagetable, uint64 va, uint64 size, uint64 pa, int perm)
{
  uint64 a, last;
  pte_t *pte;

  a = PGROUNDDOWN(va);
  last = PGROUNDDOWN(va + size - 1);
  for(;;){
    if((pte = walk(pagetable, a, 1)) == 0) //  mappages使用walk找到最低一级的pte,alloc = 1
      return -1;
    if(*pte & PTE_V)
      panic("remap");
    *pte = PA2PTE(pa) | perm | PTE_V; // 修改pte
    if(a == last)
      break;
    a += PGSIZE;
    pa += PGSIZE;
  }
  return 0;
}

// 根据va返回最低一级的页表项,alloc选项用于当pte无效时,是否创建pte,本质上是软件模拟硬件MMU的过程
pte_t * walk(pagetable_t pagetable, uint64 va, int alloc)
{
  if(va >= MAXVA)
    panic("walk");

  for(int level = 2; level > 0; level--) {
    pte_t *pte = &pagetable[PX(level, va)];
    if(*pte & PTE_V) {
      pagetable = (pagetable_t)PTE2PA(*pte);
    } else {
      if(!alloc || (pagetable = (pde_t*)kalloc()) == 0)
        return 0;
      memset(pagetable, 0, PGSIZE);
      *pte = PA2PTE(pagetable) | PTE_V;
    }
  }
  return &pagetable[PX(0, va)];
}

设置/切换页表

  • 内核初始化时/进入内核态时切换为内核页表
  • 进入用户态/进程调度后,切换为对应进程的页表

kvminithart函数,这个函数首先设置了SATP寄存器,kernel_pagetable变量来自于kvminit第一行。所以这里实际上是内核告诉MMU来使用刚刚设置好的page table(在此之前,内核在kernel/start.c中使用w_satp(0);来关闭分页机制)。

// Switch h/w page table register to the kernel's page table,
// and enable paging.
void
kvminithart()
{
  w_satp(MAKE_SATP(kernel_pagetable)); // 设置satp寄存器,将对应位进行设置
  sfence_vma(); // 刷新快表
}

进程页表

在进程的结构体中有一个字段为pagetable_t pagetable(User page table),为进程的页表,在用户态时,使用的页表是独立的,符合了操作系统的隔离性,不同进程的虚拟地址所指向的物理地址空间是不同的,进程A无法访问到进程B的内存空间。

// Per-process state
struct proc {
  struct spinlock lock;

  // p->lock must be held when using these:
  enum procstate state;        // Process state
  struct proc *parent;         // Parent process
  void *chan;                  // If non-zero, sleeping on chan
  int killed;                  // If non-zero, have been killed
  int xstate;                  // Exit status to be returned to parent's wait
  int pid;                     // Process ID

  // these are private to the process, so p->lock need not be held.
  uint64 kstack;               // Virtual address of kernel stack
  uint64 sz;                   // Size of process memory (bytes)
  pagetable_t pagetable;       // User page table
  struct trapframe *trapframe; // data page for trampoline.S
  struct context context;      // swtch() here to run process
  struct file *ofile[NOFILE];  // Open files
  struct inode *cwd;           // Current directory
  char name[16];               // Process name (debugging)
};

riscv.h一些声明

这些宏定义在页表操作中被使用到,如PA2PTE将物理地址转换为页表项,PGROUNDUP表示对sz / PGSIZE的结果进行上取整。

#define PGSIZE 4096 // bytes per page
#define PGSHIFT 12  // bits of offset within a page

#define PGROUNDUP(sz)  (((sz)+PGSIZE-1) & ~(PGSIZE-1))
#define PGROUNDDOWN(a) (((a)) & ~(PGSIZE-1))

#define PTE_V (1L << 0) // valid
#define PTE_R (1L << 1)
#define PTE_W (1L << 2)
#define PTE_X (1L << 3)
#define PTE_U (1L << 4) // 1 -> user can access

// shift a physical address to the right place for a PTE.
#define PA2PTE(pa) ((((uint64)pa) >> 12) << 10)

#define PTE2PA(pte) (((pte) >> 10) << 12)

#define PTE_FLAGS(pte) ((pte) & 0x3FF)

// extract the three 9-bit page table indices from a virtual address.
#define PXMASK          0x1FF // 9 bits
#define PXSHIFT(level)  (PGSHIFT+(9*(level)))
#define PX(level, va) ((((uint64) (va)) >> PXSHIFT(level)) & PXMASK)

// one beyond the highest possible virtual address.
// MAXVA is actually one bit less than the max allowed by
// Sv39, to avoid having to sign-extend virtual addresses
// that have the high bit set.
#define MAXVA (1L << (9 + 9 + 9 + 12 - 1))

typedef uint64 pte_t;
typedef uint64 *pagetable_t; // 512 PTEs

vm.c主要函数

ptb:pagetable; va : 虚拟地址; pa:物理地址; pte:页表项

与内核页表相关:

  • kvminit:创建内核页表的一些固定映射

  • kvminithart:使用内核页表

  • kvmmap:向内核页表中添加va->pa,flag为perm|PTE_V的映射

  • kvmpa:根据内核页表将va转换成pa

与进程页表相关:

  • uvmunmap:在ptb中移除va开始的npages个页的叶子级别映射(最低一级页表的映射),并根据do_free参数选择是否释放对应的物理空间(与freewalk配合使用来释放页表)
  • uvmcreate:创建一个空的页表
  • uvminit:用于加载第一个进程的initcode到va = 0处
  • uvmalloc:在进程页表中创建新的PTE和分配对应的物理内存,用于将进程空间从oldsz增大到newsz
  • uvmdealloc:在进程页表中修改进程空间,从oldsz到newsz(用于回收用户页表中的页面)
  • uvmfree:把进程页表空间释放,把最低一级PTE的条目清空,并删除对应的物理页(uvmunmap)。并将最高级和中间级的PTE条目也清空,并清空物理页(freewalk)
  • uvmcopy:将old ptb的sz个页空间拷贝到new ptb(用于fork系统调用,将父进程的页表映射复制到子进程的页表映射,虚拟地址对应的物理地址不同,但内容相同)
  • uvmclear:将用户页表的最低一级PTE条目的PTE_U标志去除,用于exec函数设置守护页

kvm开头和uvm开头的函数都在内核态中运行,使用的都是内核页表,但操作的对象不同

工具方法:

  • walk:根据ptb和va,返回对应的最低一级PTE

  • walkaddr:根据ptb和va,找到对应的pte,并使用PTE2PA,返回pa

  • freewalk:递归释放指定页表的所有项,叶子节点在调用之前必须已经被清空,否则会陷入freewalk leaf

  • mappages:根据给定的ptb, va, pa, size, perm在三级页表中创建映射

  • copyout:将内核空间的n个字节拷贝至用户空间

  • copyin:将用户空间的n个字节拷贝至内核空间

  • copyinstr:将用户空间的max个字符串(必须以‘\0’结尾)拷贝至内核空间

copyin

int copyin(pagetable_t pagetable, char *dst, uint64 srcva, uint64 len)
{
  uint64 n, va0, pa0;
  
  while(len > 0){
    va0 = PGROUNDDOWN(srcva);
    pa0 = walkaddr(pagetable, va0);
    if(pa0 == 0)
      return -1;
    n = PGSIZE - (srcva - va0);
    if(n > len)
      n = len;
    memmove(dst, (void *)(pa0 + (srcva - va0)), n);

    len -= n;
    dst += n;
    srcva = va0 + PGSIZE;
  }
  return 0;
}

从给定进程页表的虚拟地址srcva中拷贝len字节到内核地址dst中,由于此时运行在内核态中,因此char* dst会通过硬件MMU自动翻译成对应的物理地址(使用内核页表),而scrva对应的物理地址要使用walkaddr(软件模拟MMU的方法)从进程页表中找到对应的物理地址进行拷贝

copyout

int copyout(pagetable_t pagetable, uint64 dstva, char *src, uint64 len)
{
  uint64 n, va0, pa0;

  while(len > 0){
    va0 = PGROUNDDOWN(dstva);
    pa0 = walkaddr(pagetable, va0);
    if(pa0 == 0)
      return -1;
    n = PGSIZE - (dstva - va0);
    if(n > len)
      n = len;
    memmove((void *)(pa0 + (dstva - va0)), src, n);

    len -= n;
    src += n;
    dstva = va0 + PGSIZE;
  }
  return 0;
}

与copyin类似,只不过拷贝方向相反。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/322889.html

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

相关文章

国产芯片应用于安防音频接口的选型分析,96KHZ 立体声ADC且高性能

在人工智能兴起之后&#xff0c;安防市场就成为了其全球最大的市场&#xff0c;也是成功落地的最主要场景之一。对于安防应用而言&#xff0c;智慧摄像头、智慧交通、智慧城市等概念的不断涌现&#xff0c;对于芯片产业催生出海量需求。今天&#xff0c;我将为大家梳理GLOBALCH…

牛客周赛 Round 2 解题报告 | 珂学家 | 字符串hash + 打家劫舍型DP + 离线双指针

前言 在黎明到来之前&#xff0c;必须有人稍微照亮黑暗。 整体评价 比赛的时候&#xff0c;A题用了字符串Hash&#xff0c;哭了。B题是经典题&#xff0c;C是模拟题&#xff0c;很怕的. D也是经典题&#xff0c;离散双指针&#xff0c;套路满满。 A. 小红的环形字符串 因为长…

识别卷子怎么弄?分享3款神奇的试卷识别软件!

在当今信息爆炸的时代&#xff0c;随着科技的飞速发展&#xff0c;人工智能已经深入到我们生活的方方面面。其中&#xff0c;试卷识别软件作为教育领域的一大亮点&#xff0c;正逐渐受到越来越多的关注。这类软件不仅能帮助教师减轻批改试卷的负担&#xff0c;还能提高工作效率…

创建YonBIP模块项目

设置UAP HOME&#xff1a; 新建项目&#xff1a;File > New > Other > YonBuilder Premium Development 创建工程和模块成功 新建业务组件currtype 新建业务组件成功

Docker容器(二)安装与初体验wordpress

一、安装 1.1关闭SeLinux SeLinux&#xff08;Security-Enhanced Linux&#xff09;是一种基于Linux内核的安全模块&#xff0c;旨在提供更严格的访问控制和安全策略。它通过强制实施安全策略来限制系统资源的访问&#xff0c;从而保护系统免受恶意软件和未经授权的访问。 在…

探索金融管理硕士项目:中国社科院与美国杜兰大学携手培养未来金融精英

在当今竞争激烈的金融行业中&#xff0c;拥有卓越的专业知识和技能是取得成功的重要因素。而中国社科院与美国杜兰大学金融管理硕士项目&#xff0c;以其严谨的学术要求和实践导向的教学方法&#xff0c;为学生提供了绝佳的学习和成长机会。 中国社科院与美国杜兰大学金融管理硕…

深度探讨 Golang 中并发发送 HTTP 请求的最佳技术

&#x1f482; 个人网站:【 海拥】【神级代码资源网站】【办公神器】&#x1f91f; 基于Web端打造的&#xff1a;&#x1f449;轻量化工具创作平台&#x1f485; 想寻找共同学习交流的小伙伴&#xff0c;请点击【全栈技术交流群】 在 Golang 领域&#xff0c;并发发送 HTTP 请求…

Java学习(十八)--网络编程

介绍 需求 如何准确地定位网络上一台或多台主机&#xff1b; 定位主机上的特定的应用 找到主机后如何可靠高效地进行数据传输 目的 直接或间接地通过网络协议与其它计算机实现数据交换&#xff0c;进行通讯&#xff1b; 网络通信 网络&#xff1a;两台或多台设备通过一…

详解SpringCloud微服务技术栈:Nacos服务搭建及服务分级存储模型

&#x1f468;‍&#x1f393;作者简介&#xff1a;一位大四、研0学生&#xff0c;正在努力准备大四暑假的实习 &#x1f30c;上期文章&#xff1a;详解SpringCloud微服务技术栈&#xff1a;强推&#xff01;源码跟踪分析Ribbon负载均衡原理、Eureka服务部署 &#x1f4da;订阅…

MIT 6s081 lab 1.Xv6 and Unix utilities

Lab1: Xv6 and Unix utilities 作业网址&#xff1a;https://pdos.csail.mit.edu/6.828/2020/labs/util.html Boot xv6(easy) 下载&#xff0c;启动xv6系统 $ git clone git://g.csail.mit.edu/xv6-labs-2020 Cloning into xv6-labs-2020... ... $ cd xv6-labs-2020 $ git …

解决jmeter响应乱码的问题

HTTP请求响应乱码 方法一&#xff1a;添加后置处理器BeanShell PostProcessor&#xff0c;写入【prev.setDataEncoding("utf-8")】 方法二&#xff1a;修改bin目录下的配置文件jmeter.properties&#xff0c;将配置修改为【sampleresult.default.encodingUTF-8】 J…

【LeetCode每日一题】82. 删除排序链表中的重复元素 II

2024-1-15 文章目录 [82. 删除排序链表中的重复元素 II](https://leetcode.cn/problems/remove-duplicates-from-sorted-list-ii/) 82. 删除排序链表中的重复元素 II 思路&#xff1a; 创建一个虚拟节点 dummy 作为头节点的前置节点。使用两个指针 pre 和 cur 分别指向前一个非…

光纤和光缆有何不同之处?

很多人会有这样的疑问&#xff0c;光纤和光缆有何不同之处&#xff1f;主要是因为光纤和光缆这两个名词容易引起混淆。在严格的定义下&#xff0c;光纤和光缆是两种不同的东西&#xff0c;然而在现实生活中&#xff0c;许多人仍然会混淆这两者。为了更好地理解光纤和光缆之间的…

全国博物馆数据, shp+excel数据,数据来源可靠,基于国家文物局发布的《2021年度全国博物馆名录》

数据名称: 全国博物馆数据 数据格式: shpexcel 数据几何类型: 点 数据坐标系: WGS84 数据来源&#xff1a;网络公开数据&#xff0c;数据名录来源于国家文物局发布的《2021年度全国博物馆名录》 数据字段&#xff1a; 序号字段名称字段说明1province省份名称2city城市名…

GoZero微服务个人探索之路(三)Go-Zero官方rpc demo示例探究

官方网址&#xff1a;https://go-zero.dev/docs/tasks/cli/grpc-demo 项目结构 demo包 两个文件均为protoc-gen-go-grpc自动生成构成一个完整的 gRPC 服务的定义和实现 democlient包 demo.go goctl生成的客户端代码 Request 和 Response 别名&#xff1a; 定义了 Request 和…

知识付费saas租户平台:揭秘成功的密码

明理信息科技知识付费saas租户平台 随着互联网的快速发展&#xff0c;人们越来越重视知识的获取和价值的挖掘。在这个信息爆炸的时代&#xff0c;知识付费已经成为了一种新的商业模式&#xff0c;为知识的传播和价值的转化提供了更加高效和便捷的途径。本文将探讨知识付费的发…

UI设计入门:解析用户界面的基础概念

UI设计是什么&#xff1a;用户界面设计的基础 用户的视觉体验。一个好的用户界面需要强大、可靠和良好的使用感。用户界面设计应尽量减少用户与产品互动的能量&#xff0c;使用户更容易实现目标。 以我们最熟悉的应用程序界面为例。通常&#xff0c;手机软件的UI用户界面设计…

LeetCode刷题---基本计算器

解题思路&#xff1a; 根据题意&#xff0c;字符串中包含的运算符只有和- 使用辅助栈的方法来解决该问题 定义结果集res和符号位sign(用于判断对下一数的加减操作),接着对字符串进行遍历。 如果当前字符为数字字符&#xff0c;判断当前字符的下一个字符是否也是数字字符&#x…

样式集-发布,发表页面完整代码及示意图

发表页面完整代码示意图 完整代码 wxml代码&#xff1a; <!--pages/publish/publish.wxml--> <view class"biaoqing" catchtap"showEmoji"><text>&#x1f60a;</text></view> <view class"fabiao" catchtap…