xv6: 第二章 操作系统组织

发布时间:2024年01月21日

二、Operating System Organization

计算机中的CPU往往被硬件所包裹,这些硬件通常以I/O接口的形式存在。xv6所基于的硬件是由qemu的“-machine virt”模拟的,包括RAM、ROM(有boot code)、与用户键盘和屏幕的串行连接、存储磁盘。

1. 抽象出物理资源

2. User模式、Supervisor模式、系统调用

RISC-V有三种模式:machine mode、supervisor mode、user mode。machine mode常用于配置计算机,开机后先进入machine mode,然后xv6执行该模式下的一些语句后进入supervisor mode。

在supervisor mode下,CPU可执行特权指令,如开关中断、读写页表基址寄存器等。应用程序只能运行用户指令,且只能运行在用户空间;而supervisor mode下的软件可运行特权指令,且可运行在内核空间。

CPU提供一种特殊指令,可将CPU从user mode转到supervisor mode,并进入内核空间。接着内核检查系统调用的参数,决定该应用程序是否被允许执行其请求的操作,然后拒绝或执行

3. 内核组织

两种内核,monolithic kernel宏内核、microkernel微内核,区别见下表3、4行:
请添加图片描述
从本书角度,不管是宏内核还是微内核,都实现了系统调用、使用页表、处理中断、支持进程、用锁来实现并发控制、实现文件系统等。本书也只关注这些核心理念。

4. 代码

模块间的接口定义在kernel/defs.h

5. 进程

内核实现彼此隔离的进程的机制有:user/supervisor模式标志、地址空间、线程时间片。

xv6为每个进程维护一张页表,将虚拟地址转换成物理地址。

6. 系统调用代码详解

xv6操作系统的初始化进程的汇编代码

# Initial process that execs /init.
# This code runs in user space.

#include "syscall.h"

# exec(init, argv)
.globl start
start:
        la a0, init  // 将程序路径(即程序名)存放在a0寄存器
        la a1, argv  // 将程序参数存放在a1ji存起
        li a7, SYS_exec  //将系统调用号存放在a7寄存器
        ecall  // ecall指令负责陷入内核并执行系统调用

# for(;;) exit();
exit:
        li a7, SYS_exit
        ecall
        jal exit

# char init[] = "/init\0";
init:
  .string "/init\0"

# char *argv[] = { init, 0 };
.p2align 2
argv:
  .long init
  .long 0

xv6内核中的系统调用处理函数(当用户程序执行上面的ecall指令时,会触发中断,内核会跳转到中断处理函数,即下面的syscall函数:

void
syscall(void)
{
  int num;
  struct proc *p = myproc();  // myproc()获取当前进程的PCB

  num = p->trapframe->a7;  // 表示从寄存器a7中得到系统调用号。trapframe是PCB的一个字段,是进程在陷入内核时用于保存CPU寄存器状态的数据结构。该结构体包含了一系列CPU寄存器的值
  if(num > 0 && num < NELEM(syscalls) && syscalls[num]) {  // NELEM(syscalls)表示系统调用数组`syscalls`的长度;`syscalls[num]`判断系统调用号对应的函数是否存在 
    p->trapframe->a0 = syscalls[num]();  // syscalls[num]是一个函数指针,调用该函数,并将返回值存放在陷阱帧的a0寄存器中,最终会在用户空间中返回
  } else {  // 若系统调用号非法,则输出错误信息,并将返回值设为-1
    printf("%d %s: unknown sys call %d\n",
            p->pid, p->name, num);
    p->trapframe->a0 = -1;
  }
}

关于系统调用参数

陷入代码将用户寄存器中的参数保存到进程的trapframe中,以保证内核代码可以找到。

有些系统调用传递指针作为参数,内核需要通过该指针来读写用户空间。这会导致两个问题:

  • 用户程序可能有危害,可能向内核传递一个无效的指针,也可能该指针让内核去访问其他内核空间
  • xv6内核页表映射和用户页表映射并不完全相同,因此内核不能使用普通的指令来读写用户空间

内核需要保证数据能够安全地传向用户地址或从用户地址传进内核。例如像exec的文件系统调用使用fetchstr来从用户空间获取string类型的文件名参数,而从代码可知fetchstr函数调用copyinstr函数来完成一些hark work:

int
fetchstr(uint64 addr, char *buf, int max)
{
  struct proc *p = myproc();
  if(copyinstr(p->pagetable, buf, addr, max) < 0)  // copyinstr将从addr地址开始的字符串复制到buf指向的内核缓冲区
    return -1;
  return strlen(buf);
}
// Copy a null-terminated string from user to kernel.
// Copy bytes to dst from virtual address srcva in a given page table,
// until a '\0', or max.
// Return 0 on success, -1 on error.
int
copyinstr(pagetable_t pagetable, char *dst, uint64 srcva, uint64 max)  // 从给定的虚拟地址srcva开始,沿着页表(由pagetable中的项目指示)映射的物理内存复制字符到dst。
{
  uint64 n, va0, pa0;  // 用于存储字节数、虚拟地址、物理地址
  int got_null = 0;  // 用于标记是否在复制过程中遇到了空字符'\0'

  while(got_null == 0 && max > 0){  // 只要没有遇到空字符且还有剩余复制数量
    va0 = PGROUNDDOWN(srcva);  // 将虚拟地址srcva向下舍入到页的边界,得到va0
    pa0 = walkaddr(pagetable, va0);  // 通过页表pagetable和虚拟地址va0找到对应的物理地址pa0。walkaddr会自动检测虚拟地址是否在该进程的用户地址空间中
    if(pa0 == 0)  // 若找不到对应的物理地址
      return -1;
    n = PGSIZE - (srcva - va0);  // 计算当前页中剩余的字节数
    if(n > max) // 若剩余字节数大于最大复制数量max 
      n = max;

    char *p = (char *) (pa0 + (srcva - va0));  // 从当前页的物理地址开始的字符串
    while(n > 0){  // 逐字节本页复制字符
      if(*p == '\0'){
        *dst = '\0';
        got_null = 1;
        break;
      } else {
        *dst = *p;
      }
      --n;
      --max;
      p++;
      dst++;
    }

    srcva = va0 + PGSIZE;  // 更新srcva,使其指向下一个页的开始地址
  }
  if(got_null){
    return 0;
  } else {
    return -1;
  }
}

7. 安全模型

文章来源:https://blog.csdn.net/qq_43527718/article/details/135669520
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。