欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

高级操作系统——xv6中断调用文档

程序员文章站 2022-06-19 13:25:33
...

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系统的启动运行轨迹如图。系统的启动分为以下几个步骤:

  1. 首先,在bootasm.S中,系统必须初始化CPU的运行状态。具体地说,需要将x86 CPU从启动时默认的Intel 8088 16位实模式切换到80386之后的32位保护模式;然后设置初始的GDT(详细解释参见https://wiki.osdev.org/Global_Descriptor_Table),将虚拟地址直接按值映射到物理地址;最后,调用bootmain.c中的bootmain()函数。
  2. bootmain()函数的主要任务是将内核的ELF文件从硬盘中加载进内存,并将控制权转交给内核程序。具体地说,此函数首先将ELF文件的前4096个字节(也就是第一个内存页)从磁盘里加载进来,然后根据ELF文件头里记录的文件大小和不同的程序头信息,将完整的ELF文件加载到内存中。然后根据ELF文件里记录的入口点,将控制权转交给XV6系统。
  3. entry.S的主要任务是设置页表,让分页硬件能够正常运行,然后跳转到main.c的main()函数处,开始整个操作系统的运行。
  4. main()函数首先初始化了与内存管理、进程管理、中断控制、文件管理相关的各种模块,然后启动第一个叫做initcode的用户进程。至此,整个XV6系统启动完毕。

总的来说为 bootasm.S bootmain() entry.S main()

相关标签: 高级操作系统