高级操作系统——xv6中断调用文档
1. 什么是用户态和内核态?两者有何区别?什么是中断和系统调用?两者有何区别?计算机在运行时,是如何确定当前处于用户态还是内核态的?
用户态:运用非特权指令,在Linux中特权级为3级,Ring3,最低。不能访问内核态的地址空间包括代码和数据
内核态:能够运用特权和非特权指令(除了访管指令),在Linux中特权级为0级,最高。能访问
例子:
如:用户运行一个程序,该程序创建的进程开始时运行自己的代码,处于用户态。如果要执行文件操作、网络数据发送等操作必须通过write、send等系统调用,这些系统调用会调用内核的代码。进程会切换到Ring0,从而进入内核地址空间去执行内核代码来完成相应的操作。内核态的进程执行完后又会切换到Ring3
可以理解为广义上中断包括系统调用,狭义上分为中断和异常,其中异常又可以分为
Trap:访管指令【此为系统调用】 能中断
Fault:page default
Abort:溢出
当然有些时候可以把系统调用从异常中分离开来单独算作一类
其中对
1:系统调用:由于在程序中使用了请求系统服务的系统调用而引发的过程,通常是有意的
而其他两类通常是无意的。
2:其他两类访问中断符号表后即可进行对应的中断处理程序,而系统调用还需要访问系统调用表
用户态和内核态的特权级不同,因此可以通过特全级判断当前处于用户态还是内核态。
cpu只有通过“门结构”才能由低特权级转移到高特权级
2. 计算机开始运行阶段就有中断吗?XV6 的中断管理是如何初始化的?XV6 是如何实现内 核态到用户态的转变的?XV6 中的硬件中断是如何开关的?实际的计算机里,中断有哪几种?
问题一:
有中断
在xv6中中断为cli和sti
其中汇编语言有BIOS自带的cli和sti,另外在x86.h中有
static inline void
cli(void)
{
asm volatile("cli");
}
static inline void
sti(void)
{
asm volatile("sti");
}
可以在汇编层面调用cli和sti,相当于给.c其他文件提供一个封装的接口函数
问题二:
首先中断需要初始化中断描述符表等初始化操作,所以一开始还没有进行需要关闭中断,在bootasm.S:关闭中断
代码为: cli # BIOS enabled interrupts; disable
之后在main,c初始化一系列中断机制
main(void)
{ kvmalloc(); // kernel page table内核页表
mpinit(); // collect info about this machine
lapicinit();
seginit(); // set up segments
cprintf("\ncpu%d: starting xv6\n\n", cpu->id);
picinit(); // interrupt controller 初始化中断控制器
ioapicinit(); // another interrupt controller初始化中断控制器
consoleinit(); // I/O devices & their interruptsIO中断
uartinit(); // serial port设备端口中断
pinit(); // process table进程控制表
tvinit(); // trap vectors初始化中断描述符表
binit(); // buffer cache
fileinit(); // file table
iinit(); // inode cache
ideinit(); // disk在调度开始前调用idtinit()设置32号时钟中断
if(!ismp)
timerinit(); // uniprocessor timer
startothers(); // start other processors
kinit2(P2V(4*1024*1024), P2V(PHYSTOP)); // must come after startothers()
userinit(); // first user process
// Finish setting up this processor in mpmain.
mpmain(); //调用scheduler(); // start running processes
}
重点是tvinit,初始化中断描述符表IDT
之后在mpmain()中调用scheduler()
scheduler()会调用sti();将中断打开
所以过程为
1:Bootasm.S 关闭中断
2: main()初始化一系列中断机制【tvinit初始化中断描述符表】
3:npmain()中调用scheduler(),调用sti()开启中断
问题三:初始化的时候,首先通过
xv6在proc.c中的userinit()函数中,通过设置第一个进程的tf(trap frame)中cs ds es ss处于DPL_USER(用户模式) 完成第一个用户态进程的设置
p->tf->cs = (SEG_UCODE << 3) | DPL_USER;
p->tf->ds = (SEG_UDATA << 3) | DPL_USER;
p->tf->es = p->tf->ds;
p->tf->ss = p->tf->ds;
p->tf->eflags = FL_IF;
p->tf->esp = PGSIZE;
p->tf->eip = 0; // beginning of initcode.S
之后npmain调用scheduler(),调用 switchuvm§;在scheduler中进行初始化该进程页表、切换上下文等操作,最终第一个进程调用trapret,而此时第一个进程构造的tf中保存的寄存器转移到CPU中,设置了 %cs 的低位,使得进程的用户代码运行在 CPL = 3 的情况下,完成内核态到用户态的转变;
问题四:xv6的硬件中断由picirq.c ioapic.c timer.c中的代码对可编程中断控制器进行设置和管理,比如通过调用ioapicenable控制IOAPIC中断。处理器可以通过设置 eflags 寄存器中的 IF 位来控制自己是否想要收到中断,xv6中通过命令cli关中断,sti开中断;【其实不是很理解】
问题五:中断的种类有:程序性中断:程序性质的错误等,如用户态下直接使用特权指令;外中断: *处理的外部装置引发,如时钟中断;I/O中断: 输入输出设备正常结束或发生错误时引发,如读取磁盘完成;硬件故障中断: 机器发生故障时引发,如电源故障;访管中断: 对操作系统提出请求时引发,如读写文件。
3. 什么是中断描述符,中断描述符表?在 XV6 里是用什么数据结构表示的?
函数在中断(或者异常的时候)需要通过中断描述符确定中断入口程序的地址,而中断描述福的记录则为中断描述符表
Xv6:
首先定义结构体门gatedesc
struct gatedesc {//中断描述符的格式,表示每个变量名只占几位
uint off_15_0 : 16; // low 16 bits of offset in segment
uint cs : 16; // code segment selector
uint args : 5; // # args, 0 for interrupt/trap gates
uint rsv1 : 3; // reserved(should be zero I guess)
uint type : 4; // type(STS_{TG,IG32,TG32})
uint s : 1; // must be 0 (system)
uint dpl : 2; // descriptor(meaning new) privilege level
uint p : 1; // Present
uint off_31_16 : 16; // high bits of offset in segment
};
其中: 16为位域符,表示占多少位,如:16表示占16位,uint其实作用不大
struct gatedesc idt[256];表示有256中断描述符
vectors[]可以理解为中断描述符入口地址的程序指针
之后在trap.c中定义了
struct gatedesc idt[256];//中断描述符表
extern uint vectors[]; // in vectors.S: array of 256 entry pointers入口指针
其中
1:tvinit初始化了IDT
2:idtinit(void)相当于将数组地址载入中断向量表寄存器,硬件能够根据中断向量表寄存器准确找出中断处理程序,实质为调用lidt(idt, sizeof(idt));其中的lidt在x86.h中有定义
static inline void
lidt(struct gatedesc *p, int size)
{
volatile ushort pd[3];
pd[0] = size-1;
pd[1] = (uint)p;
pd[2] = (uint)p >> 16;
asm volatile("lidt (%0)" : : "r" (pd));
}
可以理解为ushort容纳不了所有idt的值,所以需要用数组即pd[1],pd2容纳
可以看出来,经过idtinit后,idt转变为了pd,在后面的程序中就不再调用了[我的理解]
而
4. 请以某一个中断(如除零,页错误等)为例,详细描述 XV6 一次中断的处理过程。包括:涉及哪些文件的代码?如何跳转?内核态,用户态如何变化?涉及哪些数据结构等等。
1:首先通过硬件找pd,再找对应的vector【其实有点不明白这个过程】
注意,vector在vector.s里,vector.s并不是直接有的,而是通过vector.pl生成的
print "# generated by vectors.pl - do not edit\n";
print "# handlers\n";
print ".globl alltraps\n";
for(my $i = 0; $i < 256; $i++){
print ".globl vector$i\n";
print "vector$i:\n";
if(!($i == 8 || ($i >= 10 && $i <= 14) || $i == 17)){
print " pushl \$0\n";
}
print " pushl \$$i\n";
print " jmp alltraps\n";
}
print "\n# vector table\n";
print ".data\n";
print ".globl vectors\n";
print "vectors:\n";
for(my $i = 0; $i < 256; $i++){
print " .long vector$i\n";
}
可以看到很print,理解为生成vector.s。
又因为样例中vector0:
#pushl $0
#pushl $0
#jmp alltraps
说明是硬件找到中断描述符对应的vector[i],再调用对应下面的汇编程序。
注意:在xv6中,简化了这个调用,使其中断处理程序全部指向.globl vectors【但是在正常的系统中,中断描述符对应的程序不一样,如果是系统调用,那么就去找系统调用的程序(找系统调用表然后处理云云),如果是除0错误等,则进行相关操作】
Alltraps是trapasm.s定义的一个.globl alltraps
#include "mmu.h"
# vectors.S sends all traps here.
.globl alltraps
alltraps:
pushl %ds
pushl %es
pushl %fs
pushl %gs
pushal
#.global _start #定义 _start 为外部程序可以访问的标签
#jmp alltraps跳转到alltraps
# Set up data and per-cpu segments.
movw $(SEG_KDATA<<3), %ax#define SEG_KDATA 2 // kernel data+stack
movw %ax, %ds#movw ax to ds
movw %ax, %es
movw $(SEG_KCPU<<3), %ax
movw %ax, %fs
movw %ax, %gs
# Call trap(tf), where tf=%esp
pushl %esp
call trap #call:
子程序调用指令,程序运行到此语句时,调用call后的子程序执行jump是跳转到某处开始执行下面的指令,而call是调用过程,系统会先将寄存器的值放入堆栈,等调用返回时再将堆栈里的值放回寄存器
addl $4, %esp
# Return falls through to trapret...
.globl trapret
trapret:
popal
popl %gs
popl %fs
popl %es
popl %ds
addl $0x8, %esp # trapno and errcode
iret
可以很明显的看到实质为压栈(用户态)——移动指针地址(转为内核态)——call trap调用trap.c的trap方法——出栈(内核栈化为用户栈)
而在trap方法中:
trap(struct trapframe *tf)
{
if(tf->trapno == T_SYSCALL){// 判断该中断是否为系统调用
}
switch(tf->trapno){
case T_IRQ0 + IRQ_TIMER:
default:
if(proc == 0 || (tf->cs&3) == 0){//&与操作符00说明在内核态
} }}
很长,但是实在为三个部分。
是否是系统调用
是否是用户自定义的中断
其他
注意:其实这个判断按道理在中断描述符那里就判断了,但是xv6简化了程序使其都走jmp alltraps
将硬件的工作转给了软件的工作
至此就完成,总的来说流程为
1:通过硬件访问pd,找到vector.s中对应的中断处理程序vector[i]
2:执行中断处理程序#jmp alltraps,即执行trapasm.s
3:执行 call trap,即trap.c的trap()函数
5. 请以系统调用 setrlimit (该系统调用的作用是设置资源使用限制)为例,叙述如何在 XV6中实现一个系统调用。(提示:需要添加系统调用号,系统调用函数,用户接口等等)。
1:首先在syscall.h中定义系统调用函数的头文件
#define SYS_fork 1
2:之后在syscall.c中定义extern 函数和*syscalls[])(void增加函数指针
extern int sys_chdir(void);
----------
//对应sysproc.c里面的函数
static int (*syscalls[])(void) = {
[SYS_fork] sys_fork,
-----
};
加入系统调用的函数和数组内容 int (*syscalls[])(void)可以理解成一个函数数组指针,不能有参数(void) [SYS_fork]实质上是宏定义对应[1]
3:之后在sysproc.c中创建函数体,指明要执行的操作
4:最后在user.h中将此函数呈现给用户
思考:那么系统调用的参数哪里来呢
因为int (*syscalls[])(void)是无参数的,所以不能用函数参数进行传递
可以看到argstr argptr argint中找到参数,相当于在系统调用中利用堆栈和存储器的值来作为参数【可以将参数压入堆栈,通过堆栈指针取出】并且我们也可以从 sysproc.c调用argstr argptr argint获得参数来验证我们的想法
引申xv6的初始过程
在源代码中,XV6系统的启动运行轨迹如图。系统的启动分为以下几个步骤:
- 首先,在bootasm.S中,系统必须初始化CPU的运行状态。具体地说,需要将x86 CPU从启动时默认的Intel 8088 16位实模式切换到80386之后的32位保护模式;然后设置初始的GDT(详细解释参见https://wiki.osdev.org/Global_Descriptor_Table),将虚拟地址直接按值映射到物理地址;最后,调用bootmain.c中的bootmain()函数。
- bootmain()函数的主要任务是将内核的ELF文件从硬盘中加载进内存,并将控制权转交给内核程序。具体地说,此函数首先将ELF文件的前4096个字节(也就是第一个内存页)从磁盘里加载进来,然后根据ELF文件头里记录的文件大小和不同的程序头信息,将完整的ELF文件加载到内存中。然后根据ELF文件里记录的入口点,将控制权转交给XV6系统。
- entry.S的主要任务是设置页表,让分页硬件能够正常运行,然后跳转到main.c的main()函数处,开始整个操作系统的运行。
- main()函数首先初始化了与内存管理、进程管理、中断控制、文件管理相关的各种模块,然后启动第一个叫做initcode的用户进程。至此,整个XV6系统启动完毕。
总的来说为 bootasm.S bootmain() entry.S main()
上一篇: Python-judge
下一篇: js html页面原生js横向打印