MIT 6.S081入门lab2 操作系统调用

MIT 6.S081入门lab2 系统调用

一、参考资料阅读与总结

1.xv6 book书籍阅读(操作系统架构)

a. 总览

  • 操作系统的核心: 对多个活动的支持,即多路复用、隔离、交互。
  • xv6前提: 64位操作系统;多核RISC-V:包括RAM、ROM、串口、磁盘;宏内核。

b.抽象系统资源

  • RTOS等实时操作系统: 将系统调用实现为一个库,进程直接和硬件进行交互(合作方案) -> 前提:进程之间相互信任(定时放弃处理器+没有bug)
  • Unix方案(强隔离): 使用固定接口,限制应用与存储的交互,同时将设备抽象为文件路径名称;操作系统作为接口的实现者管理硬件;Ex:CPU的分时复用和多CPU的调度、exec的内存管理的实现(swap分区)。
  • Unix文件描述符(fd): 抽象了底层实现,同时简化了交互方式。

c.用户态,核心态,以及系统调用

  • 强隔离的实现目标: 用户和硬件隔离(寄存器);用户和操作系统隔离(数据结构和代码);用户之间隔离(内存)。
  • 强隔离的硬件基础(CPU): RISC-V为例:
模式 作用
机器模式 CPU配置(start.s)
管理模式 特权指令(中断、页表地址的寄存器操作等)[内核空间]
用户模式 非特权指令[用户空间]
  • 注意: RISC-V提供跳转指令:ecall;同时内核进入点需要由内核控制。

d.内核组织

  • 宏内核与微内核:
    宏内核:整个操作系统在管理模式运行。优点: 便于操作系统内部的交互;缺点: 接口复杂;一个bug就会会导致操作系统崩溃
    微内核:只有必须部分在管理模式运行,采用客户/服务器机制,内核部分只提供低级接口(启动用户程序、IPC、底层硬件设备访问)。优点:防止了系统崩溃; 缺点:沟通复杂。
    image

e.代码(XV6架构篇)

  • 代码位置: kernel/
    使用模块化概念进行分隔,模块接口在def.h定义

    kernel代码文件
    文件 描述
    bio.c 文件系统的磁盘块缓存
    console.c 连接到用户的键盘和屏幕
    entry.S 首次启动指令
    exec.c exec()系统调用
    file.c 文件描述符支持
    fs.c 文件系统
    kalloc.c 物理页面分配器
    kernelvec.S 处理来自内核的陷入指令以及计时器中断
    log.c 文件系统日志记录以及崩溃修复
    main.c 在启动过程中控制其他模块初始化
    pipe.c 管道
    plic.c RISC-V中断控制器
    printf.c 格式化输出到控制台
    proc.c 进程和调度
    sleeplock.c CPU提供的锁机制(睡眠锁)
    spinlock.c CPU为提供的锁机制;程序实现(自旋锁)
    start.c 机器模式启动代码
    string.c 字符串和字节数组库
    swtch.c 线程切换
    syscall.c 调度系统接口函数
    sysfile.c 文件相关的系统调用
    sysproc.c 进程相关的系统调用
    trampoline.S 用于在用户和内核之间切换的汇编代码
    trap.c 对陷入指令和中断进行处理并返回的C代码
    uart.c 串口控制台设备驱动程序
    virtio_disk.c 磁盘设备驱动程序
    vm.c 管理页表和地址空间

f.进程总览

  • 进程本质: 隔离操作的最小单位
  • 进程数据存储实现: 页表提供地址空间(虚拟地址),RISC-V页表将虚拟地址映射为物理地址,其中包括.text段、.data段、栈、堆、切换内核操作所需部分(trampoline、trapframe)。
    注: RISC-V的64位指针中,只有低39位使用,其中低38位为用户的虚拟地址(kernel/riscv.h:348
  • 进程状态: 一个进程的状态有许多元素组成,其被抽象为一个proc结构体(kernel/proc.h:86)。其中重要的有页表、内核栈、运行状态等。执行进程时,会将相应的页表装载到页表寄存器satp中
  • 线程: 针对与每个进程,其都有一个硬件线程(软件线程分时复用);线程被挂起时,其状态被保存在栈上。
    注意: 栈存在两个,用户栈和内核栈,根据执行指令的级别进行应用。
  • 用户<->内核 切换过程:
    ecall(进入内核)-> 提升级别 ->
    pc跳转至内核进入点entry point -> 切换至内核栈 -> 内核执行指令 ->
    切换至用户栈 -> sret(进入用户空间)->
    恢复用户指令

g.代码(启动XV6和第一个进程)

  • xv6启动流程:
    bootloader[程序重定位到0x80000000]
    -> kernel/entry.S[看门狗+栈设置]

    entry.S
    	# qemu -kernel loads the kernel at 0x80000000
    		# and causes each CPU to jump there.
    		# kernel.ld causes the following code to
    		# be placed at 0x80000000.
    .section .text
    _entry:
    	# set up a stack for C.
    		# stack0 is declared in start.c,
    		# with a 4096-byte stack per CPU.
    		# sp = stack0 + (hartid * 4096)
    		la sp, stack0
    		li a0, 1024*4
    	csrr a1, mhartid
    		addi a1, a1, 1
    		mul a0, a0, a1
    		add sp, sp, a0
    	# jump to start() in start.c
    		call start
    spin:
    		j spin
    

    -> kernel/start.c:[修改模式+设置中断/异常管理+设置定时器+禁用MMU](注意: 这里的模式切换是和MMU是直接通过修改寄存器实现的)

    start.c
    #include "types.h"
    #include "param.h"
    #include "memlayout.h"
    #include "riscv.h"
    #include "defs.h"
    
    void main();
    void timerinit();
    
    // entry.S needs one stack per CPU.
    __attribute__ ((aligned (16))) char stack0[4096 * NCPU];
    
    // scratch area for timer interrupt, one per CPU.
    uint64 mscratch0[NCPU * 32];
    
    // assembly code in kernelvec.S for machine-mode timer interrupt.
    extern void timervec();
    
    // entry.S jumps here in machine mode on stack0.
    void
    start()
    {
      // set M Previous Privilege mode to Supervisor, for mret.
      unsigned long x = r_mstatus();
      x &= ~MSTATUS_MPP_MASK;
      x |= MSTATUS_MPP_S;
      w_mstatus(x);
    
      // set M Exception Program Counter to main, for mret.
      // requires gcc -mcmodel=medany
      w_mepc((uint64)main);
    
      // disable paging for now.
      w_satp(0);
    
      // delegate all interrupts and exceptions to supervisor mode.
      w_medeleg(0xffff);
      w_mideleg(0xffff);
      w_sie(r_sie() | SIE_SEIE | SIE_STIE | SIE_SSIE);
    
      // ask for clock interrupts.
      timerinit();
    
      // keep each CPU's hartid in its tp register, for cpuid().
      int id = r_mhartid();
      w_tp(id);
    
      // switch to supervisor mode and jump to main().
      asm volatile("mret");
    }
    
    // set up to receive timer interrupts in machine mode,
    // which arrive at timervec in kernelvec.S,
    // which turns them into software interrupts for
    // devintr() in trap.c.
    void
    timerinit()
    {
      // each CPU has a separate source of timer interrupts.
      int id = r_mhartid();
    
      // ask the CLINT for a timer interrupt.
      int interval = 1000000; // cycles; about 1/10th second in qemu.
      *(uint64*)CLINT_MTIMECMP(id) = *(uint64*)CLINT_MTIME + interval;
    
      // prepare information in scratch[] for timervec.
      // scratch[0..3] : space for timervec to save registers.
      // scratch[4] : address of CLINT MTIMECMP register.
      // scratch[5] : desired interval (in cycles) between timer interrupts.
      uint64 *scratch = &mscratch0[32 * id];
      scratch[4] = CLINT_MTIMECMP(id);
      scratch[5] = interval;
      w_mscratch((uint64)scratch);
    
      // set the machine-mode trap handler.
      w_mtvec((uint64)timervec);
    
      // enable machine-mode interrupts.
      w_mstatus(r_mstatus() | MSTATUS_MIE);
    
      // enable machine-mode timer interrupts.
      w_mie(r_mie() | MIE_MTIE);
    }
    
    

    -> kernel/main.c [初始化设备和子系统] (执行到userinit())

    main.c
    #include "types.h"
    #include "param.h"
    #include "memlayout.h"
    #include "riscv.h"
    #include "defs.h"
    
    volatile static int started = 0;
    
    // start() jumps here in supervisor mode on all CPUs.
    void
    main()
    {
      if(cpuid() == 0){
    	consoleinit();
    	printfinit();
    	printf("\n");
    	printf("xv6 kernel is booting\n");
    	printf("\n");
    	kinit();         // physical page allocator
    	kvminit();       // create kernel page table
    	kvminithart();   // turn on paging
    	procinit();      // process table
    	trapinit();      // trap vectors
    	trapinithart();  // install kernel trap vector
    	plicinit();      // set up interrupt controller
    	plicinithart();  // ask PLIC for device interrupts
    	binit();         // buffer cache
    	iinit();         // inode cache
    	fileinit();      // file table
    	virtio_disk_init(); // emulated hard disk
    	userinit();      // first user process 执行到这里创建第一个进程
    	__sync_synchronize();
    	started = 1;
      } else {
    	while(started == 0)
    	  ;
    	__sync_synchronize();
    	printf("hart %d starting\n", cpuid());
    	kvminithart();    // turn on paging
    	trapinithart();   // install kernel trap vector
    	plicinithart();   // ask PLIC for device interrupts
      }
    
      scheduler();        
    }
    
    

    -> kernel/proc.c:212:第一个进程userinit(分配proc进程结构、初始化用户虚拟内存、申请页表、设置陷阱帧[程序epc]、设置程序栈指针、设置进程名[与用户空间initcode一致]、设置工作目录、设置进程状态、释放进程锁)

    proc.c:userinit
    // a user program that calls exec("/init")
    // od -t xC initcode
    uchar initcode[] = {
      0x17, 0x05, 0x00, 0x00, 0x13, 0x05, 0x45, 0x02,
      0x97, 0x05, 0x00, 0x00, 0x93, 0x85, 0x35, 0x02,
      0x93, 0x08, 0x70, 0x00, 0x73, 0x00, 0x00, 0x00,
      0x93, 0x08, 0x20, 0x00, 0x73, 0x00, 0x00, 0x00,
      0xef, 0xf0, 0x9f, 0xff, 0x2f, 0x69, 0x6e, 0x69,
      0x74, 0x00, 0x00, 0x24, 0x00, 0x00, 0x00, 0x00,
      0x00, 0x00, 0x00, 0x00
    };
    // Set up first user process.
    void
    userinit(void)
    {
      struct proc *p;
    
      p = allocproc();
      initproc = p;
    
      // allocate one user page and copy init's instructions
      // and data into it.
      uvminit(p->pagetable, initcode, sizeof(initcode));
      p->sz = PGSIZE;
    
      // prepare for the very first "return" from kernel to user.
      p->trapframe->epc = 0;      // user program counter
      p->trapframe->sp = PGSIZE;  // user stack pointer
    
      safestrcpy(p->name, "initcode", sizeof(p->name));
      p->cwd = namei("/");
    
      p->state = RUNNABLE;
    
      release(&p->lock);
    }
    

    -> 调度器执行 user/initcode.S 进入用户空间,使用exec进入内核装载init.c ,从init返回用户空间:

    initcode.S
    # Initial process that execs /init.
    # This code runs in user space.
    
    #include "syscall.h"
    
    # exec(init, argv)
    .globl start
    start:
    		la a0, init
    		la a1, argv
    		li a7, SYS_exec
    		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
    

    -> /user/init.c 从内核返回用户空间,创建控制台设备节点并打开文件描述符,使用exec打开shell

    init.c
    // init: The initial user-level program
    
    #include "kernel/types.h"
    #include "kernel/stat.h"
    #include "kernel/spinlock.h"
    #include "kernel/sleeplock.h"
    #include "kernel/fs.h"
    #include "kernel/file.h"
    #include "user/user.h"
    #include "kernel/fcntl.h"
    
    char *argv[] = { "sh", 0 };
    
    int
    main(void)
    {
      int pid, wpid;
    
      if(open("console", O_RDWR) < 0){
    	mknod("console", CONSOLE, 0);
    	open("console", O_RDWR);
      }
      dup(0);  // stdout
      dup(0);  // stderr
    
      for(;;){
    	printf("init: starting sh\n");
    	pid = fork();
    	if(pid < 0){
    	  printf("init: fork failed\n");
    	  exit(1);
    	}
    	if(pid == 0){
    	  exec("sh", argv);
    	  printf("init: exec sh failed\n");
    	  exit(1);
    	}
    
    	for(;;){
    	  // this call to wait() returns if the shell exits,
    	  // or if a parentless process exits.
    	  wpid = wait((int *) 0);
    	  if(wpid == pid){
    		// the shell exited; restart it.
    		break;
    	  } else if(wpid < 0){
    		printf("init: wait returned an error\n");
    		exit(1);
    	  } else {
    		// it was a parentless process; do nothing.
    	  }
    	}
      }
    }
    
    

h.总结、xv6与真实操作系统的差异

  • 真实设备中,同时应用了宏内核和微内核。
  • 现代操作系统支持进单进程多线程,使单一进程可以多核并行,其核心机制有潜在的接口更改、控制线程和进程序的资源共享等

二、涉及函数及其文件:

  • kernel/proc.h: 核心进程管理的重要结构

    kernel/proc.h
    // Saved registers for kernel context switches. 内核上下文切换寄存器
    struct context {
      uint64 ra; //返回地址
      uint64 sp; //堆栈指针
    
      // callee-saved
      uint64 s0;
      uint64 s1;
      uint64 s2;
      uint64 s3;
      uint64 s4;
      uint64 s5;
      uint64 s6;
      uint64 s7;
      uint64 s8;
      uint64 s9;
      uint64 s10;
      uint64 s11;
    };
    
    // Per-CPU state. cpu状态结构体
    struct cpu {
      struct proc *proc;          // The process running on this cpu, or null. 进程
      struct context context;     // swtch() here to enter scheduler(). 上下文切换信息
      int noff;                   // Depth of push_off() nesting.中断嵌套深度
      int intena;                 // Were interrupts enabled before push_off()? 中断使能
    };
    
    extern struct cpu cpus[NCPU]; //SMP数组
    
    // per-process data for the trap handling code in trampoline.S.
    // sits in a page by itself just under the trampoline page in the
    // user page table. not specially mapped in the kernel page table.
    // the sscratch register points here.
    // uservec in trampoline.S saves user registers in the trapframe,
    // then initializes registers from the trapframe's
    // kernel_sp, kernel_hartid, kernel_satp, and jumps to kernel_trap.
    // usertrapret() and userret in trampoline.S set up
    // the trapframe's kernel_*, restore user registers from the
    // trapframe, switch to the user page table, and enter user space.
    // the trapframe includes callee-saved user registers like s0-s11 because the
    // return-to-user path via usertrapret() doesn't return through
    // the entire kernel call stack.
    //进程在用户模式下的所有寄存器状态,用来保存进程上下文
    struct trapframe {
      /*   0 */ uint64 kernel_satp;   // kernel page table
      /*   8 */ uint64 kernel_sp;     // top of process's kernel stack
      /*  16 */ uint64 kernel_trap;   // usertrap()
      /*  24 */ uint64 epc;           // saved user program counter
      /*  32 */ uint64 kernel_hartid; // saved kernel tp
      /*  40 */ uint64 ra;
      /*  48 */ uint64 sp;
      /*  56 */ uint64 gp;
      /*  64 */ uint64 tp;
      /*  72 */ uint64 t0;
      /*  80 */ uint64 t1;
      /*  88 */ uint64 t2;
      /*  96 */ uint64 s0;
      /* 104 */ uint64 s1;
      /* 112 */ uint64 a0;
      /* 120 */ uint64 a1;
      /* 128 */ uint64 a2;
      /* 136 */ uint64 a3;
      /* 144 */ uint64 a4;
      /* 152 */ uint64 a5;
      /* 160 */ uint64 a6;
      /* 168 */ uint64 a7;
      /* 176 */ uint64 s2;
      /* 184 */ uint64 s3;
      /* 192 */ uint64 s4;
      /* 200 */ uint64 s5;
      /* 208 */ uint64 s6;
      /* 216 */ uint64 s7;
      /* 224 */ uint64 s8;
      /* 232 */ uint64 s9;
      /* 240 */ uint64 s10;
      /* 248 */ uint64 s11;
      /* 256 */ uint64 t3;
      /* 264 */ uint64 t4;
      /* 272 */ uint64 t5;
      /* 280 */ uint64 t6;
    };
    
    enum procstate { UNUSED, SLEEPING, RUNNABLE, RUNNING, ZOMBIE }; //进程状态结构
    
    // 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)
    };
    
    
  • kernel/defs.h:s所有的系统调用API和系统结构声明

    defs.h中函数作用表格
    函数名称 函数作用 所在文件
    binit 初始化缓冲区 bio.c
    bread 读取缓冲区 bio.c
    brelse 释放缓冲区 bio.c
    bwrite 写入缓冲区 bio.c
    bpin 固定缓冲区 bio.c
    bunpin 解除固定缓冲区 bio.c
    consoleinit 初始化控制台 console.c
    consoleintr 控制台中断处理 console.c
    consputc 控制台输出字符 console.c
    exec 执行程序 exec.c
    filealloc 分配文件结构 file.c
    fileclose 关闭文件 file.c
    filedup 复制文件描述符 file.c
    fileinit 初始化文件系统 file.c
    fileread 读取文件 file.c
    filestat 获取文件状态 file.c
    filewrite 写入文件 file.c
    fsinit 初始化文件系统 fs.c
    dirlink 创建目录链接 fs.c
    dirlookup 查找目录项 fs.c
    ialloc 分配索引节点 fs.c
    idup 复制索引节点 fs.c
    iinit 初始化索引节点 fs.c
    ilock 锁定索引节点 fs.c
    iput 释放索引节点 fs.c
    iunlock 解锁索引节点 fs.c
    iunlockput 解锁并释放索引节点 fs.c
    iupdate 更新索引节点 fs.c
    namecmp 比较文件名 fs.c
    namei 查找索引节点 fs.c
    nameiparent 查找父索引节点 fs.c
    readi 读取索引节点 fs.c
    stati 获取索引节点状态 fs.c
    writei 写入索引节点 fs.c
    itrunc 截断索引节点 fs.c
    ramdiskinit 初始化RAM磁盘 ramdisk.c
    ramdiskintr RAM磁盘中断处理 ramdisk.c
    ramdiskrw RAM磁盘读写 ramdisk.c
    kalloc 内核内存分配 kalloc.c
    kfree 释放内核内存 kalloc.c
    kinit 初始化内核内存 kalloc.c
    initlog 初始化日志 log.c
    log_write 写入日志 log.c
    begin_op 开始文件系统操作 log.c
    end_op 结束文件系统操作 log.c
    pipealloc 分配管道 pipe.c
    pipeclose 关闭管道 pipe.c
    piperead 读取管道 pipe.c
    pipewrite 写入管道 pipe.c
    printf 格式化输出 printf.c
    panic 内核错误处理 printf.c
    printfinit 初始化格式化输出 printf.c
    cpuid 获取CPU ID proc.c
    exit 进程退出 proc.c
    fork 创建进程 proc.c
    growproc 增加进程大小 proc.c
    proc_pagetable 获取进程页表 proc.c
    proc_freepagetable 释放进程页表 proc.c
    kill 终止进程 proc.c
    mycpu 获取当前CPU proc.c
    getmycpu 获取当前CPU proc.c
    myproc 获取当前进程 proc.c
    procinit 初始化进程系统 proc.c
    scheduler 调度器 proc.c
    sched 进程调度 proc.c
    setproc 设置当前进程 proc.c
    sleep 进程睡眠 proc.c
    userinit 初始化用户进程 proc.c
    wait 等待进程结束 proc.c
    wakeup 唤醒进程 proc.c
    yield 放弃CPU proc.c
    either_copyout 拷贝到用户空间 proc.c
    either_copyin 从用户空间拷贝 proc.c
    procdump 打印进程信息 proc.c
    swtch 上下文切换 swtch.S
    acquire 获取自旋锁 spinlock.c
    holding 检查自旋锁 spinlock.c
    initlock 初始化自旋锁 spinlock.c
    release 释放自旋锁 spinlock.c
    push_off 禁用中断 spinlock.c
    pop_off 恢复中断 spinlock.c
    acquiresleep 获取睡眠锁 sleeplock.c
    releasesleep 释放睡眠锁 sleeplock.c
    holdingsleep 检查睡眠锁 sleeplock.c
    initsleeplock 初始化睡眠锁 sleeplock.c
    memcmp 内存比较 string.c
    memmove 内存移动 string.c
    memset 内存设置 string.c
    safestrcpy 安全字符串拷贝 string.c
    strlen 字符串长度 string.c
    strncmp 字符串比较 string.c
    strncpy 字符串拷贝 string.c
    argint 获取整数参数 syscall.c
    argstr 获取字符串参数 syscall.c
    argaddr 获取地址参数 syscall.c
    fetchstr 获取字符串 syscall.c
    fetchaddr 获取地址 syscall.c
    syscall 系统调用 syscall.c
    trapinit 初始化陷阱 trap.c
    trapinithart 初始化陷阱处理器 trap.c
    usertrapret 用户陷阱返回 trap.c
    uartinit 初始化UART uart.c
    uartintr UART中断 uart.c
    uartputc UART输出字符 uart.c
    uartputc_sync 同步UART输出字符 uart.c
    uartgetc UART获取字符 uart.c
    kvminit 初始化内核虚拟内存 vm.c
    kvminithart 初始化虚拟内存处理器 vm.c
    kvmpa 虚拟内存到物理地址 vm.c
    kvmmap 映射内核虚拟内存 vm.c
    mappages 映射页面 vm.c
    uvmcreate 创建用户虚拟内存 vm.c
    uvminit 初始化用户虚拟内存 vm.c
    uvmalloc 分配用户虚拟内存 vm.c
    uvmdealloc 释放用户虚拟内存 vm.c
    uvmcopy 拷贝用户虚拟内存 vm.c
    uvmfree 释放用户虚拟内存 vm.c
    uvmunmap 取消映射用户虚拟内存 vm.c
    uvmclear 清除用户虚拟内存 vm.c
    walkaddr 遍历地址 vm.c
    copyout 拷贝到用户空间 vm.c
    copyin 从用户空间拷贝 vm.c
    copyinstr 拷贝字符串到用户空间 vm.c
    plicinit 初始化PLIC plic.c
    plicinithart 初始化PLIC处理器 plic.c
    plic_claim 声明PLIC plic.c
    plic_complete 完成PLIC处理 plic.c
    virtio_disk_init 初始化虚拟磁盘 virtio_disk.c
    virtio_disk_rw 虚拟磁盘读写 virtio_disk.c
    virtio_disk_intr 虚拟磁盘中断 virtio_disk.c
    defs.h中结构体作用表格
    结构体名称 结构体作用 所在文件
    struct buf 缓冲区结构,通常用于文件系统的块缓存 buf.h
    struct context 上下文结构,用于保存进程或线程的上下文(如寄存器状态) proc.h, swtch.S
    struct file 文件结构,代表一个打开的文件 file.h
    struct inode 索引节点结构,表示文件系统中的一个文件 file.h
    struct pipe 管道结构,用于进程间通信 pipe.c
    struct proc 进程结构,表示一个进程 proc.h
    struct spinlock 自旋锁结构,用于多线程同步 spinlock.h
    struct sleeplock 睡眠锁结构,另一种同步机制 sleeplock.h
    struct stat 状态结构,存储文件或目录的信息 stat.h
    struct superblock 超级块结构,存储文件系统的全局信息 fs.h
  • kernel/entry.S: 内核启动汇编代码,见上文

  • kernel/main.c: 内核主函数,见上文

  • user/initcode.S: 用户空间第一个进程的初始化,见上文

  • user/init.c: 用户空间的的第一个进程,其创建控制台设备节点并打开文件描述符,同时使用exec打开shell,见上文。

  • 略读:

  • kernel/proc.c: 是进程管理的核心,其中包含的程序如下:

    proc.c中进程管理函数及其作用
    函数名称 函数作用 参数和返回值
    procinit 初始化进程表 无参数,无返回值
    cpuid 返回当前CPU的ID 无参数,返回值:int
    mycpu 返回当前CPU的cpu结构体指针 无参数,返回值:struct cpu*
    myproc 返回当前正在执行的进程的proc结构体指针 无参数,返回值:struct proc*
    allocpid 为新进程分配一个唯一的进程ID 无参数,返回值:int
    allocproc 在进程表中查找未使用的进程项并进行初始化 无参数,返回值:struct proc*
    freeproc 释放一个进程结构和它占用的资源 参数:struct proc *p,无返回值
    proc_pagetable 为给定进程创建一个用户页表 参数:struct proc *p,返回值:pagetable_t
    proc_freepagetable 释放进程的页表和它引用的物理内存 参数:pagetable_t pagetable, uint64 sz,无返回值
    userinit 设置第一个用户进程 无参数,无返回值
    growproc 增加或减少进程的内存大小 参数:int n,返回值:int
    fork 创建一个新进程,复制父进程的状态 无参数,返回值:int
    reparent 将进程的孩子进程重新分配给init进程 参数:struct proc *p,无返回值
    exit 结束当前进程并保留其状态 参数:int status,无返回值
    wait 等待子进程退出并返回其pid 参数:uint64 addr,返回值:int
    scheduler CPU的进程调度器 无参数,无返回值
    sched 切换到调度器 无参数,无返回值
    yield 放弃CPU一个调度轮次 无参数,无返回值
    forkret fork子进程的第一次调度后执行的函数 无参数,无返回值
    sleep 使进程进入睡眠状态 参数:void *chan, struct spinlock *lk,无返回值
    wakeup 唤醒睡眠在chan上的所有进程 参数:void *chan,无返回值
    wakeup1 用于唤醒等待进程的函数 参数:struct proc *p,无返回值
    kill 结束指定pid的进程 参数:int pid,返回值:int
    either_copyout 将数据复制到用户地址或内核地址(取决于usr_dst) 参数:int user_dst, uint64 dst, void *src, uint64 len,返回值:int
    either_copyin 从用户地址或内核地址复制数据(取决于usr_src) 参数:void *dst, int user_src, uint64 src, uint64 len,返回值:int
    procdump 打印进程列表,用于调试 无参数,无返回值
  • kernel/exec.c: 执行程序的核心代码
    exec函数的流程图:
    image

    mermaid源代码
    	graph TD
    		A[开始] --> B[检查文件]
    		B -->|不存在| Z[返回-1]
    		B --> C[锁定inode]
    		C --> D[检查ELF头是否有效]
    		D -->|无效| Z
    		D --> E[创建页表]
    		E --> F[加载程序段到内存]
    		F --> G[解锁inode并结束文件操作]
    		G --> H[准备用户栈]
    		H --> I[推入参数和agrv指针]
    		I --> J[设置trapframe(包括程序入口点和栈指针)]
    		J --> K[切换用户进程镜像(更换页表)]
    		K --> L[返回参数个数(作为main的第一个参数)]
    		L --> M[结束]
    		K --> |无法切换|Z
    		H --> |用户栈申请失败|Z
    		E --> |创建页表失败|Z
    

三、课程视频观看笔记

  • 隔离: 在复用的同时达到强隔离,通过对硬件的抽象实现。 例外:只有基于RTOS的系统是基于库的,即为协作式调度,由于其中的应用程序都为可信的。

    • 进程(fork)是对CPU及其资源的抽象
    • exec是对内存的抽象;
    • 文件是对磁盘块的抽象
    • 硬件基础:用户/内核模式(多权限模式)、虚拟内存/页表(MMU)
  • 注意: 操作系统应该是防御性的,其能够处理恶意程序的注入;同时,应该保证应用程序和内核的强隔离。

  • 内核\用户模式:

    • 内核模式中CPU可以执行特权指令、用户模式只能使用非特权指令;特权指令主要指的是硬件相关的寄存器,如中断、时钟设置等。
    • 虚拟内存(MMU):使用页表,将虚拟地址映射到物理地址,针对每一个进程提供自己的页表。->强页表隔离
  • 系统调用:

    • ecall: RISC-V 指令进入内核,根据系统调用编号进入内核的特定的位置。
      例:fork() ->ecall<sys_fork> -|-> syscall->fork()
    • syscall:进行参数检查
  • 内核理论:
    内核可被视为可信任计算基础(KGB,没有bug)、内核必须将用户程序视作恶意的(安全思维模式)

    • 宏内核:整个操作系统都在内核中(桌面级操作系统)
      优点:OS能够更好的协作,效率更高;
      缺点:bug危险,且代码量庞大
    • 微内核:内核级只有IPC、页表、复用代码等核心代码,大部分操作系统在用户层 (嵌入式操作系统)
      优点:小,更少的bug;
      缺点:系统调用复杂,性能容易受限
  • xv6:
    QEMU模拟RISC-V:读取指令->解码->执行 + 寄存器状态

  • syscall: 调用ecall后进入syscall,syscall根据传入的信息调用内核中相应的函数

    syscall函数
    void
    syscall(void)
    {
      int num;
      struct proc *p = myproc();
    
      num = p->trapframe->a7;
      if(num > 0 && num < NELEM(syscalls) && syscalls[num]) {
    	p->trapframe->a0 = syscalls[num]();
      } else {
    	printf("%d %s: unknown sys call %d\n",
    			p->pid, p->name, num);
    	p->trapframe->a0 = -1;
      }
    }
    

四、完成lab及其代码

主要是跟着操作即可,两个实验的目的都是熟悉系统调用的过程:
系统调用的过程:
user/user.h: 系统调用声明
->user/usys.S: 由usys.pl生成,为从用户台跳至内核态的跳板函数,使用ecall[中断处理]从用户态送入内核态,通过寄存器a7传递参数
->kernel/syscall.c: 到达内核态统一处理函数syscall(),根据表中查找函数并执行
->kernel/sysproc.c:进行具体内核操作,到达sys_()系列函数。
用户态配置修改:

  • user.h下加入相应的系统调用声明

    user.h
    ...省略...
    struct sysinfo; //lab2
    ...省略...
    // system calls
    ...省略...
    int trace(int); //lab1
    int sysinfo(struct sysinfo *); //lab2
    ...省略...
    
  • usys.pl下添加相应参数,完成对跳转代码的生成

    usys.pl
    ...省略...
    entry("trace"); //lab1
    entry("sysinfo"); //lab2
    ...省略...
    

内核态配置修改:

  • syscall.h中加入相应系统调用编号

    syscall.h
    ...省略...
    #define SYS_trace 22 //lab1 
    #define SYS_sysinfo 23 //lab2
    
  • syscall.c中加入相应的内核调用函数,并补充syscalls映射表

    syscall.c
    ...省略...
    extern uint64 sys_trace(void); //lab1
    extern uint64 sys_sysinfo(void); //lab2
    
    static uint64 (*syscalls[])(void) = {
    ...省略...
    [SYS_trace]   sys_trace, //lab1
    [SYS_sysinfo]   sys_sysinfo, //lab2
    };
    ...省略...
    

具体lab1实现(/kernel目录下,即内核态):

  • 修改proc.h中proc结构体定义,添加跟踪系统调用的mask参数:

    proc.h
    // Per-process state
    struct proc {
    ...省略..
      uint64 tracemask;       //tracemask
    };
    
  • proc.c 中在创建新进程时对mask参数初始化、释放进程时清理、创建子进程时候复制

    proc.c
    ...省略...
    static struct proc*
    allocproc(void)//创建新进程
    {
    ...省略...
      // Set up new context to start executing at forkret,
      // which returns to user space.
      memset(&p->context, 0, sizeof(p->context));
      p->context.ra = (uint64)forkret;
      p->context.sp = p->kstack + PGSIZE;
    
      p->tracemask = 0; //初始化mask参数
      return p;
    }
    
    // free a proc structure and the data hanging from it,
    // including user pages.
    // p->lock must be held.
    static void
    freeproc(struct proc *p) //释放进程
    {
    ...省略...  
    p->tracemask = 0;
    ...省略...  
    }
    ...省略...  
    
    // Create a new process, copying the parent.
    // Sets up child kernel stack to return as if from fork() system call.
    int
    fork(void)
    {
      int i, pid;
      struct proc *np;
      struct proc *p = myproc();
    ...省略...  
      pid = np->pid;
    
      np->tracemask = p->tracemask;
    
      np->state = RUNNABLE;
    ...省略...  
    }
    
  • 在sysproc.c中实现相应syscall的代码,其负责将参数注入到proc结构体中:

    sysproc.c
    // sys_trace trace the syscall
    uint64
    sys_trace(void)
    {
      int mask;
    
      if(argint(0, &mask) < 0) //读取trapframe,获得参数。
    	return -1;
      myproc()->tracemask |= mask;
      return 0;
    }
    
  • 由于syscall实现所有内核调用,因此在syscall中根据mask侦测syscall;并加入代号到名字(string)的mapping,便于打印:

    syscall.c
    //内核调用代码与相应名称的mapping
    static char *syscall_name[] = {
    [SYS_fork]    "fork",
    [SYS_exit]    "exit",
    [SYS_wait]    "wait",
    [SYS_pipe]    "pipe",
    [SYS_read]    "read",
    [SYS_kill]    "kill",
    [SYS_exec]    "exec",
    [SYS_fstat]   "fstat",
    [SYS_chdir]   "chdir",
    [SYS_dup]     "dup",
    [SYS_getpid]  "getpid",
    [SYS_sbrk]    "sbrk",
    [SYS_sleep]   "sleep",
    [SYS_uptime]  "uptime",
    [SYS_open]    "open",
    [SYS_write]   "write",
    [SYS_mknod]   "mknod",
    [SYS_unlink]  "unlink",
    [SYS_link]    "link",
    [SYS_mkdir]   "mkdir",
    [SYS_close]   "close",
    [SYS_trace]   "trace",
    };
    
    void
    syscall(void)
    {
      int num;
      struct proc *p = myproc();
    
      num = p->trapframe->a7; //将系统调用从a7寄存器中提出
      if(num > 0 && num < NELEM(syscalls) && syscalls[num]) { //判断是否为合法的系统调用
    	p->trapframe->a0 = syscalls[num](); //返回值存放在a0寄存器中
    	if (p->tracemask & (1 << num)) {
    	  printf("%d: syscall %s -> %d\n", p->pid, syscall_name[num], p->trapframe->a0); //根据mask判定是否需要trace这一调用
    	}
      } else {
    	printf("%d %s: unknown sys call %d\n",
    			p->pid, p->name, num);
    	p->trapframe->a0 = -1;
      }
    }
    

具体lab2实现(/kernel目录下,即内核态):

  • 根据kernel/sysinfo.h结构体,在def.h相应位置声明需要获取内存和进程信息所需要的函数

    def.h
    // kalloc.c
    void*           kalloc(void);
    void            kfree(void *);
    void            kinit(void);
    uint64        kfreemem_bytes(void); //剩余内存获取函数
    
    // proc.c
    ...省略...
    void            procdump(void);
    uint64        nproc(void); //现有进程数量函数
    
  • 在相应的文件中( kalloc.c和proc.c)实现内存获取函数(通过遍历相应链表)和现有进程数量函数(通过遍历相应数组)

    kalloc.c
    struct run {
      struct run *next;
    };
    
    struct {
      struct spinlock lock;
      struct run *freelist;
    } kmem;
    
    ...省略...
    
    // kfreemem_bytes to know how much bytes left in mem
    uint64
    kfreemem_bytes(void)
    {
    	struct run *r;
    	uint64 freemen_bytes = 0;
    	acquire(&kmem.lock); //获取锁
    	r = kmem.freelist;
    	while(r) { //遍历链表
    	  freemen_bytes += PGSIZE; //获取剩余内存字节数
    	  r = r->next;
    	}
    	release(&kmem.lock);//释放锁
    	return freemen_bytes;
    }
    
    proc.c
    struct proc proc[NPROC];
    
    ...省略...
    
    // count how many procs is not in UNUSED state now
    uint64
    nproc(void)
    {
      struct proc *p;
      uint64 proc_count = 0;
    
      for(p = proc; p < &proc[NPROC]; p++) { //遍历数组
    	acquire(&p->lock); //加锁
    	if(p->state != UNUSED) {
    	  proc_count++; //获取使用进程数量
    	}
    	release(&p->lock); //解锁
      }
      return proc_count;
    }
    
  • 参考sys_fstat()(kernel/sysfile.c)和filestat()(kernel/file.c),使用copyout将数据传回用户空间
    int copyout(pagetable_t pagetable, uint64 dstva, char *src, uint64 len)

    sysproc.c
    #include "sysinfo.h"
    
    ...省略...
    
    uint64
    sys_sysinfo(void)
    {
      struct proc *p = myproc(); //获取当前进程结构体
      uint64 addr; //传回用户空间的地址
      struct sysinfo s;
    
      if(argaddr(0, &addr) < 0) //获取相应参数
    	return -1;
    
      s.freemem = kfreemem_bytes();
      s.nproc = nproc(); //调用内核函数填充结构体
    
      if(copyout(p->pagetable, addr, (char *)&s, sizeof(s)) < 0) //将数据回用户空间
    	return -1;
      return 0;
    }
    

参考文献

2020版xv6手册:https://pdos.csail.mit.edu/6.S081/2020/xv6/book-riscv-rev1.pdf
xv6手册与代码笔记:https://zhuanlan.zhihu.com/p/350949057
xv6阅读笔记:https://ghostasky.github.io/2022/07/12/XV6/
xv6手册中文版:http://xv6.dgs.zone/tranlate_books/book-riscv-rev1/c1/s3.html
28天速通MIT 6.S081操作系统公开课:https://zhuanlan.zhihu.com/p/625526955
MIT6.s081操作系统笔记:https://juejin.cn/post/7006016963029762056

posted @ 2024-01-30 10:49  David_Dong  阅读(63)  评论(0编辑  收藏  举报