6.1810: Operating System Engineering 2023 <Lab5: cow: Copy-on-write fork>

news2025/2/4 11:50:57

一、本节任务

二、要点

2.1 设备驱动(device driver)

memory-mapped I/O:设备拥有一个地址范围,软件可以使用 ld/st 指令来访存从而读写设备的寄存器。平台设计者决定设备在物理内存空间中的位置。

内核如何识别设备中断? 

device -> PLIC -> trap -> usertrap()/kerneltrap() -> devintr()

典型的设备驱动结构:上半部/下半部

上半部:执行进程的系统调用,write() 或 read();启动 I/O,可能会等待。

下半部:中断处理,读或写需要与上半部进程交互的设备;将输入放到上半部进程能读取的位置,告诉上半部输入已经读取完毕。

驱动(driver)是在操作系统中管理特定设备的代码:它可以配置设备硬件;告诉设备执行对应的操作;处理中断;并且与正在等待设备I/O的进程进行交互。设备驱动程序可以和它所管理的设备同时执行。

来自设备的中断也是 trap 的一种,内核陷阱处理程序识别设备引发的中断并调用驱动程序的中断处理程序;在xv6中,此调度发生在 devintr(kernel/trap.c:178)中。

许多驱动程序在两种环境中执行代码:一个是上半部(top half),运行在一个进程的内核线程中;和一个在中断时执行的下半部(bottom half)。上半部分是通过系统调用调用的,如 read 和 write 希望设备执行I/O,然后程序会等待操作(如 read 和 write 操作)执行完成;最终设备执行完 I/O 后会发起一个中断,然后执行中断处理程序,中断处理程序可以唤醒等待 I/O 操作的进程,这部分就是中断下半部。

2.2 UART 驱动

xv6 的 main 函数中调用 consoleinit(kernel/console.c)来初始化串口硬件。

当用户键入一个字符时,UART 硬件会引发一个中断,从而激活 xv6 的陷阱处理程序(trap handler)。陷阱处理程序调用 devintr(kernel/trap.c),它通过 RISC-V scause 的寄存器,判断中断来自外部设备。然后它要求 PLIC 告诉它是哪个设备发生了中断(kernel/trap.c:187)。如果是 UART,devintr 就会调用 uartintr。

uartintr 从 UART 硬件中读取任何等待的输入字符,并将它们交给 consoleintr(kernel/console.c:136);consoleintr 的工作是在 cons.buf 中积累输入字符,直到整行到达。当换行到达时,主机会唤醒一个等待的 consoleread(如果有的话)。一旦被唤醒,consoleread 会将 cons.buf 中的一整行复制到用户空间,并(通过系统调用机制)返回到用户空间。

在设备驱动程序中,一般通过 buffer 和 interrupt 来将设备工作和进程进行解耦。

驱动中的并发问题,驱动程序一般会使用锁来保护临界资源:在两个 cpu 核上执行同样的驱动程序的时候,如果不对访问进行约束,可能会导致冲突和死锁的问题。还有一种情况就是,当多个进程并发运行时,发生输入中断的目标进程并不是当前运行进程,所以此时中断只会把输入复制到 buffer 中,然后唤醒目标进程进行后续处理。

2.3 定时器中断(timer interrupt)

RISC-V CPU 要求定时器中断是在机器模式下,而不是监督模式。RISC-V 的机器模式下没有分页,并且其机器模式使用一套完全与监督模式独立的寄存器(如 mstatus、mcause),所以无法在机器模式下运行 xv6 kernel,所以 xv6 使用的一套独立的机制来处理定时器中断。

在 start(kernel/start.c)中,cpu 运行在机器模式,此时会配置定时器中断(配置 CLINT(core local interrupt)硬件)。

有一些机制可以减少中断的发生频率,如把一系列请求放到一个中断中(如 DMA);另外一种方法是完全禁止中断,并且每隔一段时间就去确认一下设备是否有请求需要处理,这种方法被称为 polling,对于中断频率非常高的设备来说,这种方式十分有效,一些驱动动态地在中断和 polling 之间来回切换。

三、Lab: cow: Copy-on-write fork

问题:uservec、kernelvec、timervec 的作用?分别在什么时候执行? 

我们已经知道 stvec 寄存器保存的是中断服务程序的首地址,在 U 模式下,stvec 指向的是 uservec,在 S 模式下,stvec 指向的是 kernelvec,这样做的原因是需要在 uservec 切换页表,并且从用户栈切换到内核栈。

那么 xv6 是如何设置 stvec 的呢?首先在 uservec 例程中除了执行保存上下文、切换页表等操作之外,还会在 usertrap 中将 stvec 指向 kernelvec,这里的切换的目的是当前已经执行到了 S 模式,所有的中断、陷阱等都必须由 kernelvec 负责处理。

当需要返回 usertrap 时,usertrap 会调用 usertrapret,usertrapret 会重新设置 stvec 的值使其指向 uservec,之后跳转到 userret,恢复上下文和切换页表。

xv6 对于时钟中断的处理方式是这样的:在 M-mode 设置好时钟中断的处理函数 timervec,当发生时钟中断时就由 M-mode 的代码读写 mtime 和 mtimecmp 寄存器,然后激活 sip.SSIP 以软件中断的形式通知内核。内核在收到软件中断之后会递增 ticks 变量,并调用 wakeup 函数唤醒沉睡的进程。 内核本身也会收到时钟中断,此时内核会判断当前运行的是不是进程号为 0 的进程,如果不是就会调用 yield() 函数使当前进程放弃 CPU 并调度下一个进程;如果使进程号为 0 的进程,那就不做处理。

3.1 Implement copy-on-write fork(hard)

xv6 中的 fork() 系统调用会将父进程的所有用户空间拷贝到子进程中,对于用户空间比较大的父进程来说,这个拷贝操作十分耗时,并且对于 fork() + exec() 的子进程来说,会用到的父进程的空间很少,大部分拷贝的空间都丢弃了。所以后面有人提出了写时拷贝(COW, copy-on-write),该方法可以推迟拷贝操作。

COW fork() 仅为子进程创建一个页表,该页表指向父进程的物理页面(也可以理解为直接从父进程那儿拷贝来的页表),并且会将父进程和子进程页表中的页表项设置为只读,当父进程或子进程需要对某个页面执行写操作时,CPU 会引发页故障,在内核的页故障处理函数中会给引发该页故障的进程分配一个物理页面,并且将进程需要放问的页面上的内容拷贝到新页面上,然后把对应页表项与新页面关联起来,并且设置页面为可写,当页故障处理函数返回后,该进程即可对页面的拷贝进行写操作了。

写时拷贝同时也增加了释放页面的复杂性, 因为一个页面可能同时关联多个进程,如果要释放页面的话,还需要确认这几个进程与页面的关联是否释放。

而本实验的目标就是要实现写时拷贝。步骤如下:

修改 kernel/kalloc.c,添加 ref_count 数组用来给每个物理页面保存引用该页面的进程数,当引用数为 0 时表示该页面没有任何页面引用,此时在 kfree 中才能释放页面,否则仅将引用数减一;使用 kalloc 函数时,初始化引用数为 1。

/* kernel/kalloc.c */
#define PA2INDEX(pa) ((uint64)pa/PGSIZE)
int ref_count[PHYSTOP/PGSIZE];

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

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

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

  acquire(&kmem.lock);
  int remain_count = --ref_count[PA2INDEX(pa)];
  release(&kmem.lock);

  if(remain_count > 0){
    return;
  }

  // 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 *
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
    ref_count[PA2INDEX(r)] = 1;
  }
  return (void*)r;
}

void ref_count_inc(uint64 pa){
  if(pa > PHYSTOP)
    panic("ref_count_inc");
  acquire(&kmem.lock);
  ++ref_count[PA2INDEX(pa)];
  release(&kmem.lock);
}

修改 uvmcopy() 函数(kernel/vm.c),使得其不会给子进程分配新的物理页面,而是仅将父进程的物理页映射到子进程,其中使用 pte 中的 RSW(reserved for software)两位来分别标志页面是否为 COW 页面(第 9 位),以及页面是否为可写的页面(第 8 位)。

/* kernel/vm.c */
int uvmcopy(pagetable_t old, pagetable_t new, uint64 sz)
{
  pte_t *pte;
  uint64 pa, i;
  uint flags;

  for(i = 0; i < sz; i += PGSIZE){
    if((pte = walk(old, i, 0)) == 0)
      panic("uvmcopy: pte should exist");
    if((*pte & PTE_V) == 0)
      panic("uvmcopy: page not present");
    // clear PTE_W and set RSW means it's a writeable page
    if((*pte & PTE_W) != 0){
      *pte &= (~PTE_W);
      *pte |= (1 << 8);
    }
    // set RSW means it's a COW page
    *pte |= (1 << 9);

    pa = PTE2PA(*pte);
    flags = PTE_FLAGS(*pte);
    if(mappages(new, i, PGSIZE, pa, flags) != 0){
      goto err;
    }
    ref_count_inc(pa);
  }
  return 0;

 err:
  uvmunmap(new, 0, i / PGSIZE, 1);
  return -1;
}

由 riscv 文档可知,引发 page fault 的原因可能有多种,比如因为 load 指令触发的 page fault、因为 store 指令触发的 page fault 又或者是因为 jump 指令触发的 page fault,在引发 page fault 后,cpu 会将出错的地址放到 stval 寄存器中,而发生 page fault 的原因则存放到 scause 寄存器中,scause 的内容如下图所示:

可以看到,12 表示是因为指令执行而引起的 page fault,13 表示是因为 load 而引起的 page fault,15 则是表示因为 store 而引起的中断,而写时拷贝的拷贝操作发生在进程想要写页面的时候,所以在 usertrap 我们只需要判断 scause 是否等于 15 即可。

当 scause == 15 时,即可确定发生了写页面的 page fault,然后判断 RSW 的第二位是否为 1,为 1 则表示此页面为写时拷贝的映射页面,再判断 RSW 的第一位是否为 1,为 1 则代表原来的页面是可写的,此时需要拷贝页面到一个新的物理页面上,然后让进程的页表项指向新的副本页面;若 RSW 的第一位为 0,则代表页面原来是不可写的,此时不拷贝新页面,并且需要 kill 掉该进程,usertrap 代码如下:

/* kernel/trap.c */
void
usertrap(void)
{
  int which_dev = 0;

  if((r_sstatus() & SSTATUS_SPP) != 0)
    panic("usertrap: not from user mode");

  // send interrupts and exceptions to kerneltrap(),
  // since we're now in the kernel.
  w_stvec((uint64)kernelvec);

  struct proc *p = myproc();

  // save user program counter.
  p->trapframe->epc = r_sepc();

  if(r_scause() == 8){
    // system call

    if(killed(p))
      exit(-1);

    // sepc points to the ecall instruction,
    // but we want to return to the next instruction.
    p->trapframe->epc += 4;

    // an interrupt will change sepc, scause, and sstatus,
    // so enable only now that we're done with those registers.
    intr_on();

    syscall();
  } else if(r_scause() == 15){
    if(cow(p->pagetable, r_stval()) != 0){
      setkilled(p);
    }
  } else if((which_dev = devintr()) != 0){
    // ok
  } else {
    printf("usertrap(): unexpected scause %p pid=%d\n", r_scause(), p->pid);
    printf("            sepc=%p stval=%p\n", r_sepc(), r_stval());
    setkilled(p);
  }

  if(killed(p))
    exit(-1);

  // give up the CPU if this is a timer interrupt.
  if(which_dev == 2)
    yield();

  usertrapret();
}

cow() 函数的代码如下:

/* kernel/vm.c */
int cow(pagetable_t pagetable, uint64 va){
  if(va >= MAXVA)
    return -1;
  uint64 pa, pa_new;
  uint flags;
  uint64 va_align = PGROUNDDOWN(va);
  pte_t* pte = walk(pagetable, va_align, 0);

  if((*pte & (1 << 9)) != 0){
    // it's a COW page
    if((*pte & (1 << 8)) != 0){
      // the page is writeable before
      pa = PTE2PA(*pte);
      flags = PTE_FLAGS(*pte);

      if((pa_new = (uint64)kalloc()) == 0)
        return -1;

      memmove((void *)pa_new, (const void *)pa, PGSIZE);
      kfree((void *)pa); // 减少COW页面的reference count
      *pte = PA2PTE(pa_new) | flags | PTE_W;
    } else {
      // the page is not writeable before
      return -1;
    }
  } else {
    printf("not a COW page\n");
  }
  return 0;
}

最后要注意的一个问题就是 copyout 函数,该函数的作用是将数据从内核空间拷贝到用户空间中,并且该函数是在内核中调用,所以不会触发 usertrap,而是 kerneltrap,所以我们要在 copyout 函数中判断我们的目的用户页面是否为 COW 页面,如果是,则需要额外的拷贝操作(写时拷贝):

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

  while(len > 0){
    va0 = PGROUNDDOWN(dstva);
    if(va0 >= MAXVA)
      return -1;
    pte = walk(pagetable, va0, 0);

    if(pte == 0 || (*pte & PTE_V) == 0 || (*pte & PTE_U) == 0)
      return -1;


    if((*pte & PTE_W) == 0 && (*pte & (1 << 9)) != 0){
      // it's a COW page
      if(cow(pagetable, va0) != 0)
        return -1;
    } else if((*pte & PTE_W) == 0){
      return -1;
    }

    pa0 = PTE2PA(*pte);
    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;
}

最后测试全部通过: 

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

K8s出现问题时,如何排查解决!

K8s问题的排查 1. POD启动异常、部分节点无法启动pod2. 审视集群状态3. 追踪事件日志4. 聚焦Pod状态5. 检查网络连通性6. 审视存储配置7. 研究容器日志8. K8S集群网络通信9. 问题&#xff1a;Service 是否通过 DNS 工作&#xff1f;10. 总结1、POD启动异常、部分节点无法启动p…

2024年度AI大模型趋势解读

文章目录 2024年度AI大模型趋势解读写在前面大模型时代大模型未来发展趋势总结 2024年度AI大模型趋势解读 写在前面 大模型指具备超大规模预训练语料、拥有超千亿规模模型参数的深度学习模型。由美国开放人工智能研究中心&#xff08;OpenAI&#xff09;研发、基于大模型的人工…

python 面试题第一弹

1. 如何理解Python中的深浅拷贝 浅拷贝&#xff08;Shallow Copy&#xff09;创建一个新的对象&#xff0c;该对象的内容是原始对象的引用。这意味着新对象与原始对象共享相同的内存地址&#xff0c;因此对于可变对象来说&#xff0c;如果修改了其中一个对象&#xff0c;另一个…

31. Ajax

简介 AJAX 是 Asynchronous JavaScript And XML 的简称。直译为&#xff0c;异步的JS和XML。AJAX的实际意义是&#xff0c;不发生页面跳转、异步载入内容并改写页面内容的技术。AJAX也可以简单的理解为通过JS向服务器发送请求。 AJAX这门技术很早就被发明&#xff0c;但是直到…

Python 新规范 pyproject.toml 完全解析

多谢&#xff1a;thank Python从PEP 518开始引入的使用pyproject.toml管理项目元数据的方案。 该规范目前已经在很多开源项目中得以支持&#xff1a; Django 这个 Python 生态的顶级项目在 5 个月之前开始使用 pyproject.tomlPytest 这个 Python 生态测试框架的领头羊在 4 个…

二、基于图像和结构化数据多模态融合的回归预测网络【框图讲解+源码】

整理读研期间做的项目与日常小实验 本篇未完待续…代码部分整理后补充 0. 背景 实验室有一些材料的SEM&#xff08;扫描电镜&#xff09;图像、也有对应的组分信息&#xff08;结构化数据&#xff0c;包含类别特征和连续的数值特征&#xff09;&#xff0c;以及对应的力学性能…

c++学习笔记(14)-类的对象占用内存

1、类的对象占用内存大小 很多C书籍中都介绍过&#xff0c;一个class对象需要占用多大的内存空间。最权威的结论是&#xff1a; 1. 非静态成员变量总和。 2. 加上编译器为了CPU计算&#xff0c;作出的数据对齐处理。 3. 加上为了支持虚函数&#xff0c;产生的额外负担。 1.1、…

【自然语言处理】扩展命名实体识别器(NER)以使用spaCy标记新实体

自我介绍 做一个简单介绍&#xff0c;酒架年近48 &#xff0c;有20多年IT工作经历&#xff0c;目前在一家500强做企业架构&#xff0e;因为工作需要&#xff0c;另外也因为兴趣涉猎比较广&#xff0c;为了自己学习建立了三个博客&#xff0c;分别是【全球IT瞭望】&#xff0c;【…

用C/C++实现MSML协议栈的详细介绍

一、MSML协议简介 MSML&#xff08;Media Server Markup Language&#xff09;是一种基于XML的标记语言&#xff0c;用于控制媒体服务器。它是媒体服务器控制协议的一种&#xff0c;允许第三方应用与媒体服务器进行交互&#xff0c;实现对媒体流的创建、修改和释放等操作。MSM…

Elasticsearch可视化平台Kibana [ES系列] - 第498篇

历史文章&#xff08;文章累计490&#xff09; 《国内最全的Spring Boot系列之一》 《国内最全的Spring Boot系列之二》 《国内最全的Spring Boot系列之三》 《国内最全的Spring Boot系列之四》 《国内最全的Spring Boot系列之五》 《国内最全的Spring Boot系列之六》 全…

W3 Total Cache Pro v2.6.0 – WordPress 插件

W3 Total Cache Pro v2.6.0&#xff1a;优化WordPress性能的专业解决方案 一、引言 在数字化的世界中&#xff0c;网站性能对于用户体验和搜索引擎排名至关重要。WordPress作为全球最受欢迎的内容管理系统之一&#xff0c;提供了大量的插件来帮助网站所有者优化其性能。其中&a…

数字人平台哪家好!

数字人是指利用人工智能技术&#xff0c;通过计算机生成或模拟人类的外貌、声音、动作和表情等特征&#xff0c;从而创造出具有一定个性和情感的虚拟角色。数字人可以用于于娱乐、教育、营销、服务等多个领域&#xff0c;为用户提供更加丰富和互动的体验。 目前&#xff0c;市场…

『JavaScript』深入理解JavaScript字符串的基础操作与内置方法

&#x1f4e3;读完这篇文章里你能收获到 学习了JavaScript中字符串的基本操作掌握了JavaScript字符串的多种内置方法了解了JavaScript中的模板字符串和插值表达式熟悉了如何在JavaScript中使用正则表达式与字符串方法结合 文章目录 一、基本操作1. 创建字符串2. 字符串长度3. …

Java架构师系统架构设计实践

目录 1 导语2 架构设计实践本章概述3 架构设计要素概述和规划4 架构设计模式5 架构设计输入6 架构设计输出7 架构设计要素总结 想学习架构师构建流程请跳转&#xff1a;Java架构师系统架构设计 1 导语 Java架构师在进行系统架构设计时&#xff0c;需要综合考虑多个方面&#…

洛谷 P2367 语文成绩 刷题笔记

P2367 语文成绩 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn) 差分 令a[i]为b[i]数组的前缀和 a[n]b[1]b[2]b[3].....b[n]; a[n-1]b[1]b[2]b[3].....b[n-1]; 构造差分数组 b[i]a[i]-a[i-1]; 有什么好处 当我们想对a[l]--a[r]范围内所有数据加上一个数x 不必循环 for(i…

C++力扣题目150--逆波兰表达式求值

给你一个字符串数组 tokens &#xff0c;表示一个根据 逆波兰表示法 表示的算术表达式。 请你计算该表达式。返回一个表示表达式值的整数。 注意&#xff1a; 有效的算符为 、-、* 和 / 。每个操作数&#xff08;运算对象&#xff09;都可以是一个整数或者另一个表达式。两个…

前端---html 的基本结构

1. 结构代码 <!DOCTYPE html> <html><head> <meta charset"UTF-8"><title>网页标题</title></head><body>网页显示内容</body> </html>第一行<!DOCTYPE html>是文档声明, 用来指定页…

C# 使用xUnit进行单元测试

目录 写在前面 操作步骤 执行结果 写在前面 xUnit.net 是 NUnit 的作者Jim Newkirk搞的一个新的单元测试框架&#xff0c;目的是解决 NUnit 存在的一些问题和缺点&#xff0c;并在框架中加入一些最佳实践和扩展能力。 主要的改进点如下&#xff1a; 为每个测试方法产生一个…

漏洞复现-泛微OA xmlrpcServlet接口任意文件读取漏洞(附漏洞检测脚本)

免责声明 文章中涉及的漏洞均已修复&#xff0c;敏感信息均已做打码处理&#xff0c;文章仅做经验分享用途&#xff0c;切勿当真&#xff0c;未授权的攻击属于非法行为&#xff01;文章中敏感信息均已做多层打马处理。传播、利用本文章所提供的信息而造成的任何直接或者间接的…

什么是动态IP?静态IP和动态IP有什么区别?

动态IP(Dynamic IP)和静态IP(Static IP)它是指在计算机网络中分配给设备的两种不同类型的IP地址。 动态IP是指每次设备连接到网络时&#xff0c;网络服务提供商(ISP)IP地址的动态分配。当设备重新连接到网络时&#xff0c;它可能会被分配到不同的IP地址。动态IP适用于传统的家…