• XV6源代码阅读-中断与系统调用


    Exercise1 源代码阅读

    1.启动部分: bootasm.S bootmain.c 和xv6初始化模块:main.c

    • bootasm.S 由16位和32位汇编混合编写成的XV6引导加载器。bootasm.S内的汇编代码会调用bootmain.c中的void bootmain(void);main.c主函数内部初始化各模块;
    • 当x86 PC启动时,它执行的是一个叫BIOS的程序。BIOS存放在非易失存储器中,BIOS的作用是在启动时进行硬件的准备工作,接着把控制权交给操作系统。具体来说,BIOS会把控制权交给从磁盘第0块引导扇区(用于引导的磁盘的第一个512字节的数据区)加载的代码。引导扇区中包含引导加载器——负责内核加载到内存中。BIOS 会把引导扇区加载到内存 0x7c00 处,接着(通过设置寄存器 %ip)跳转至该地址。引导加载器开始执行后,处理器处于模拟Intel 8088处理器的模式下。而接下来的工作就是把处理器设置为现代的操作模式,并从磁盘中把 xv6内核载入到内存中,然后将控制权交给内核。
    # Start the first CPU: switch to 32-bit protected mode, jump into C.
    # The BIOS loads this code from the first sector of the hard disk into
    # memory at physical address 0x7c00 and starts executing in real mode
    # with %cs=0 %ip=7c00.
    
    .code16                       # Assemble for 16-bit mode
    .globl start
    start:
      cli                         # BIOS enabled interrupts; disable
    
      # Zero data segment registers DS, ES, and SS.
      xorw    %ax,%ax             # Set %ax to zero
      movw    %ax,%ds             # -> Data Segment
      movw    %ax,%es             # -> Extra Segment
      movw    %ax,%ss             # -> Stack Segment
    
    

    2.中断与系统调用部分: trap.c trapasm.S vectors.S & vectors.pl syscall.c sysproc.c proc.c 以及相关其他文件代码

    • trap.c 陷入指令c语言处理接口,trapasm.S陷入指令的汇编逻辑;
    • vector.S由vector.pl生成,中断描述符256个;
    • proc.c 内部主要接口:static struct proc * allocproc(void)、void userinit(void)、int growproc(int n)、int fork(void)、void exit(void)、int wait(void)、void scheduler(void)、void yield(void);
    • syscall.c 内部定义了各种类型的系统调用函数,sysproc.c内部是与进程创建、退出等相关的系统调用函数的实现。
    // syscall.h  System call numbers
    ……
    #define SYS_fork    1
    #define SYS_exit    2
    #define SYS_wait    3
    #define SYS_pipe    4
    #define SYS_read    5
    #define SYS_kill    6
    #define SYS_exec    7
    ……
    
    
    // syscall.c 声明系统调用
    ……
    extern int sys_chdir(void);
    extern int sys_close(void);
    extern int sys_dup(void);
    extern int sys_exec(void);
    extern int sys_exit(void);
    extern int sys_fork(void);
    extern int sys_fstat(void);
    extern int sys_getpid(void);
    extern int sys_kill(void);
    extern int sys_link(void);
    extern int sys_mkdir(void);
    extern int sys_mknod(void);
    extern int sys_open(void);
    ……
    
    // sysproc.c 定义前面声明的系统调用接口
    int sys_fork(void)
    {
      return fork();
    }
    
    int sys_exit(void)
    {
      exit();
      return 0;  // not reached
    }
    
    int sys_wait(void)
    {
      return wait();
    }
    
    int sys_kill(void)
    {
      int pid;
    
      if(argint(0, &pid) < 0)
        return -1;
      return kill(pid);
    }
    ……
    

    Exercise2 带着问题阅读

    3.什么是用户态和内核态,两者有何区别? 什么是中断和系统调用,两者有何区别? 计算机在运行时,是如何确定当前处于用户态还是内核态的?

    • 当一个进程在执行用户自己的代码时处于用户运行态(用户态),此时特权级最低,为3级,是普通的用户进程运行的特权级,大部分用户直接面对的程序都是运行在用户态。Ring3状态不能访问Ring0的地址空间,包括代码和数据;当一个进程因为系统调用陷入内核代码中执行时处于内核运行态(内核态),此时特权级最高,为0级。执行的内核代码会使用当前进程的内核栈,每个进程都有自己的内核栈。用户运行一个程序,该程序创建的进程开始时运行自己的代码,处于用户态。如果要执行文件操作、网络数据发送等操作必须通过write、send等系统调用,这些系统调用会调用内核的代码。进程会切换到Ring0,从而进入内核地址空间去执行内核代码来完成相应的操作。内核态的进程执行完后又会切换到Ring3,回到用户态。这样,用户态的程序就不能随意操作内核地址空间,具有一定的安全保护作用。这说的保护模式是指通过内存页表操作等机制,保证进程间的地址空间不会互相冲突,一个进程的操作不会修改另一个进程地址空间中的数据;
    • 系统调用需要借助于中断机制来实现。两者都是从同一个异常处理入口开始,但是系统调用会一开始让CPU进入内核模式且使能中断,然后从系统调用表中取得相应的注册函数调用之;而中断处理则让CPU进入内核模式且disable中断。所以系统调用的真实处理(系统调用表中的注册函数执行)中可以阻塞,而中断处理的上半部不可以。所以在写驱动代码如字符设备驱动,实现读操作时是可以让其sleep的(比如没有数据时候,用户设置读模式是阻塞型的)。另一方面,如果该驱动读操作过于耗时也是不可取的,它在内核态中执行,这个时候只有中断的优先级比它高,其它的高优先级线程将不能得到及时调度执行;
    • 用户态和内核态的特权级不同,因此可以通过特全级判断当前处于用户态还是内核态。

    4.计算机开始运行阶段就有中断吗? XV6 的中断管理是如何初始化的? XV6 是如何实现内核态到用户态的转变的? XV6 中的硬件中断是如何开关的? 实际的计算机里,中断有哪几种?

    • 计算机开始运行阶段就有BIOS支持的中断;
    • 由于xv6在开始运行阶段没有初始化中断处理程序,于是xv6在bootasm.S中用cli命令禁止中断发生。xv6的终端管理初始化各部分通过main.c中的main()函数调用。picinit()和oapicinit()初始化可编程中断控制器,consoleinit()和uartinit()设置了I/O、设备端口的中断。接着,tvinit()调用trap.c中的代码初始化中断描述符表,关联vectors.S中的中断IDT表项,在调度开始前调用idtinit()设置32号时钟中断,最后在scheduler()中调用sti()开中断,完成中断管理初始化;
    • xv6在proc.c中的userinit()函数中,通过设置第一个进程的tf(trap frame)中cs ds es ss处于DPL_USER(用户模式) 完成第一个用户态进程的设置,然后在scheduler中进行初始化该进程页表、切换上下文等操作,最终第一个进程调用trapret,而此时第一个进程构造的tf中保存的寄存器转移到CPU中,设置了 %cs 的低位,使得进程的用户代码运行在 CPL = 3 的情况下,完成内核态到用户态的转变;
    • xv6的硬件中断由picirq.c ioapic.c timer.c中的代码对可编程中断控制器进行设置和管理,比如通过调用ioapicenable控制IOAPIC中断。处理器可以通过设置 eflags 寄存器中的 IF 位来控制自己是否想要收到中断,xv6中通过命令cli关中断,sti开中断;
    • 中断的种类有:程序性中断:程序性质的错误等,如用户态下直接使用特权指令;外中断: 中央处理的外部装置引发,如时钟中断;I/O中断: 输入输出设备正常结束或发生错误时引发,如读取磁盘完成;硬件故障中断: 机器发生故障时引发,如电源故障;访管中断: 对操作系统提出请求时引发,如读写文件。

    5.什么是中断描述符,中断描述符表(IDT)? 在XV6里是用什么数据结构表示的?

    • 中断描述符表的每一项是一个中断描述符,在x86系统中,中断处理程序定义存储在IDT中。XV6的IDT有256个入口点,每个入口点中对应的处理程序不同,在出发trap时,只要找到对应编号的入口,就能得到对应的处理程序;
    • XV6中的数据结构中中断描述符用struct gatedesc表示:
    // trap.c
    # generated by vectors.pl - do not edit
    # handlers
    .globl alltraps
    .globl vector0
    vector0:
      pushl $0
      pushl $0
      jmp alltraps
    .globl vector1
    vector1:
      pushl $0
      pushl $1
      jmp alltraps
    .globl vector2
    ……
    
    • alltraps继续保存处理器的寄存器,设置数据和CPU段,然后压入 %esp,调用trap,到此时已完成用户态到内核态的转变;
    // trapasm.S
      # vectors.S sends all traps here.
    .globl alltraps
    alltraps:
      # Build trap frame.
      pushl %ds
      pushl %es
      pushl %fs
      pushl %gs
      pushal
      
      # Set up data and per-cpu segments. 设置数据和CPU段
      movw $(SEG_KDATA<<3), %ax
      movw %ax, %ds
      movw %ax, %es
      movw $(SEG_KCPU<<3), %ax
      movw %ax, %fs
      movw %ax, %gs
    
      # Call trap(tf), where tf=%esp 压入 %esp
      pushl %esp  # 调用trap
      call trap
      addl $4, %esp
    
    • trap会根据%esp指向对应的tf,首先根据trapno判断该中断是否是系统调用,之后判断硬件中断,由于除零不是以上两种,于是判断为代码错误中断,并且是发生在用户空间的。接着处理程序将该进程标记为killed,并退出,继续下一个进程的调度;
    // trap.c
    //PAGEBREAK: 41
    void trap(struct trapframe *tf)
    {
      if(tf->trapno == T_SYSCALL){ // 判断该中断是否为系统调用
        if(proc->killed)
          exit();
        proc->tf = tf;
        syscall();
        if(proc->killed)
          exit();
        return;
      }
    
      switch(tf->trapno){
      
      ……
      
      // PAGEBREAK: 13  
      // tf->trapno与其他case语句对不上,除零被视为代码错误中断,进入这里杀掉进程
      default: 
        if(proc == 0 || (tf->cs&3) == 0){
          // In kernel, it must be our mistake.
          cprintf("unexpected trap %d from cpu %d eip %x (cr2=0x%x)
    ",
                  tf->trapno, cpu->id, tf->eip, rcr2());
          panic("trap");
        }
        // In user space, assume process misbehaved.  
        cprintf("pid %d %s: trap %d err %d on cpu %d "
                "eip 0x%x addr 0x%x--kill proc
    ",
                proc->pid, proc->name, tf->trapno, tf->err, cpu->id, tf->eip, 
                rcr2());
        proc->killed = 1;
      }
    
      ……
    }
    
    • 涉及到的主要数据结构:中断描述符表IDT(trap.c +12)、(vi x86.h +150)、(vi vector.S)。
    // trap.c
    // Interrupt descriptor table (shared by all CPUs).
    struct gatedesc idt[256];
    extern uint vectors[];  // in vectors.S: array of 256 entry pointers
    ……
    
    // x86.h
    //PAGEBREAK: 36
    // Layout of the trap frame built on the stack by the
    // hardware and by trapasm.S, and passed to trap().
    struct trapframe {
      // registers as pushed by pusha
      uint edi;
      uint esi;
      uint ebp;
      uint oesp;      // useless & ignored
      uint ebx;
      uint edx;
      uint ecx;
      uint eax;
      ……
    };
    
    // vector.S  0~255共256个
    vectors:
      .long vector0
      .long vector1
      .long vector2
      .long vector3
      .long vector4
      .long vector5
      .long vector6
      .long vector7
      .long vector8
      .long vector9
      ……
    

    6.请以系统调用setrlimit(该系统调用的作用是设置资源使用限制)为例,叙述如何在XV6中实现一个系统调用。(提示:需要添加系统调用号,系统调用函数,用户接口等等)。

    • 在syscall.h中添加系统调用号 #define SYS_setrlimit 22;
    // syscall.h
    ……
    #define SYS_mkdir  20
    #define SYS_close  21
    #define  SYS_setrlimit  22 // add by yangyu
    
    • 在syscall.c中添加对应的处理程序的调用接口
    // syscall.c
    ……
    static int (*syscalls[])(void) = {
    ……
    [SYS_mkdir]   sys_mkdir,
    [SYS_close]   sys_close,
    [SYS_setrlimit]   SYS_setrlimit, // add by yangyu
    };
    
    • 在sysproc.c中添加系统调用函数int sys_setrlimit(void),具体实现对于进程资源使用限制的设置;
    // syspro.c
    ……
    int sys_uptime(void)
    {
      uint xticks;
      
      acquire(&tickslock);
      xticks = ticks;
      release(&tickslock);
      return xticks;
    }
    
    // 在这里面写逻辑,限制进程资源的使用
    int sys_setrlimit(void)
    {
        // to do
    }
    
    • 在user.h中声明系统调用接口int setrlimit(int resource, const struct rlimit * rlim);
    // syspro.c
    ……
    // system calls
    int fork(void);
    int exit(void) __attribute__((noreturn));
    …… // 调用该接口陷入内核执行系统调用
    int setrlimit(int resource, const struct rlimit *rlim); 
    
    • 在usys.S添加SYSCALL(setrlimit)。
    // usys.S
    ……
    SYSCALL(sleep)
    SYSCALL(uptime)
    SYSCALL(setrlimit)  // add by yangyu
    

    参考文献

    [1] xv6 idt初始化
    [2] [xv6中文文档](https://th0ar.gitbooks.io/xv6- chinese/content/content/chapter3.html)
    [3] xv6 alltraps
    [4] [xv6 trap/interrupt](

  • 相关阅读:
    为collection view添加一个补充视图(页眉或页脚)
    清除一个View控件上所有的约束
    c++学习笔记---03---从一个小程序说起2
    c++学习笔记---02---从一个小程序说起
    c++学习笔记---01---C++语言与OO思想介绍
    计算机网络(自顶向下)----读书笔记
    Android 开发笔记___基本适配器的使用__BaseAdapter
    Android 开发笔记___时间选择器---timePicker
    Android 开发笔记___DatePicker__日期选择器
    Android 开发笔记___实战项目:购物车
  • 原文地址:https://www.cnblogs.com/icoty23/p/10993829.html
Copyright © 2020-2023  润新知