1

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

 2 years ago
source link: https://icoty.github.io/2019/06/09/xv6-interrupt-systemcall/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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
  1. 中断与系统调用部分: 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 带着问题阅读

  1. 什么是用户态和内核态,两者有何区别? 什么是中断和系统调用,两者有何区别? 计算机在运行时,是如何确定当前处于用户态还是内核态的?
  • 当一个进程在执行用户自己的代码时处于用户运行态(用户态),此时特权级最低,为3级,是普通的用户进程运行的特权级,大部分用户直接面对的程序都是运行在用户态。Ring3状态不能访问Ring0的地址空间,包括代码和数据;当一个进程因为系统调用陷入内核代码中执行时处于内核运行态(内核态),此时特权级最高,为0级。执行的内核代码会使用当前进程的内核栈,每个进程都有自己的内核栈。用户运行一个程序,该程序创建的进程开始时运行自己的代码,处于用户态。如果要执行文件操作、网络数据发送等操作必须通过write、send等系统调用,这些系统调用会调用内核的代码。进程会切换到Ring0,从而进入内核地址空间去执行内核代码来完成相应的操作。内核态的进程执行完后又会切换到Ring3,回到用户态。这样,用户态的程序就不能随意操作内核地址空间,具有一定的安全保护作用。这说的保护模式是指通过内存页表操作等机制,保证进程间的地址空间不会互相冲突,一个进程的操作不会修改另一个进程地址空间中的数据;
  • 系统调用需要借助于中断机制来实现。两者都是从同一个异常处理入口开始,但是系统调用会一开始让CPU进入内核模式且使能中断,然后从系统调用表中取得相应的注册函数调用之;而中断处理则让CPU进入内核模式且disable中断。所以系统调用的真实处理(系统调用表中的注册函数执行)中可以阻塞,而中断处理的上半部不可以。所以在写驱动代码如字符设备驱动,实现读操作时是可以让其sleep的(比如没有数据时候,用户设置读模式是阻塞型的)。另一方面,如果该驱动读操作过于耗时也是不可取的,它在内核态中执行,这个时候只有中断的优先级比它高,其它的高优先级线程将不能得到及时调度执行;
  • 用户态和内核态的特权级不同,因此可以通过特全级判断当前处于用户态还是内核态。
  1. 计算机开始运行阶段就有中断吗? 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中断: 输入输出设备正常结束或发生错误时引发,如读取磁盘完成;硬件故障中断: 机器发生故障时引发,如电源故障;访管中断: 对操作系统提出请求时引发,如读写文件。
  1. 什么是中断描述符,中断描述符表(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
……
  1. 请以系统调用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中文文档
[3] xv6 alltraps
[4] xv6 trap/interrupt


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK