荒野之萍

  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

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)\n",
              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\n",
            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](

posted on 2019-06-09 16:17  荒野之萍  阅读(1272)  评论(0编辑  收藏  举报