XV6源代碼閱讀-中斷與系統調用

Exercise1 源代碼閱讀

1.啓動部分: bootasm.S bootmain.c 和xv6初始化模塊:main.chtml

  • 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 以及相關其餘文件代碼git

  • 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 中的硬件中斷是如何開關的? 實際的計算機裏,中斷有哪幾種?api

  • 計算機開始運行階段就有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中實現一個系統調用。(提示:須要添加系統調用號,系統調用函數,用戶接口等等)。bash

  • 在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中文文檔
[3] xv6 alltraps
[4] [xv6 trap/interrupt](網絡

相關文章
相關標籤/搜索