xv6操作系统源码阅读之中断和系统调用和驱动

在三种情况下,用户程序需要陷入内核。

  • 系统调用。来自用户程序主动调用请求内核服务。
  • 异常。来自用户程序本身的错误。
  • 中断。来自外部程序的中断信号。

x86硬件机制

在x86中,有四种特权等级,0到3,而操作系统只使用0和3级别,分别代表内核级和用户级。
在x86中中断向量是由IDT(中断描述符表)维护的。在x86中如果要进行系统调用,需要使用int n指令,其中n代表在IDT中的中断向量的索引,int指令主要进行保护现场和跳转到中断向量的操作。

系统调用过程

在main函数中调用了tvinit函数来进行IDT的初始化。

int
main(void)
{
  ...
  tvinit();        // trap vectors
  ...
}

在tvinit中可以看到操作系统设置了256个entry,特别的是设置了专门的系统调用的entry,且其特权级别是USER,其他的所有entry特权级都是内核级,这说明用户程序只有系统调用这一种权限。

void
tvinit(void)
{
  int i;

  for(i = 0; i < 256; i++)
    SETGATE(idt[i], 0, SEG_KCODE<<3, vectors[i], 0);
  SETGATE(idt[T_SYSCALL], 1, SEG_KCODE<<3, vectors[T_SYSCALL], DPL_USER);

  initlock(&tickslock, "time");
}

其中的vector表中的每一个vector定义如下。

vector0:
  pushl $0
  pushl $0
  jmp alltraps
.globl vector1

而alltrap定义如下。这里就是进一步保存现场,最后就形成了一个前述文章中提到的一个trapframe,再接着调用trap过程,而当trap执行完之后,就会接着接着执行trapret过程,这里主要就是恢复现场。

  # vectors.S sends all traps here.
.globl alltraps
alltraps:
  # Build trap frame.
  pushl %ds
  pushl %es
  pushl %fs
  pushl %gs
  pushal
  
  # Set up data segments.
  movw $(SEG_KDATA<<3), %ax
  movw %ax, %ds
  movw %ax, %es

  # Call trap(tf), where tf=%esp
  pushl %esp
  call trap
  addl $4, %esp

接着再看trap函数。可以看到这个函数接收的是一个trapframe,也就是前述过程所保存的现场。接着它会根据其中的trapno来判断trap的类型,时系统调用还是中断,如果是系统调用的话就会调用syscall函数。

void
trap(struct trapframe *tf)
{
  if(tf->trapno == T_SYSCALL){
    if(myproc()->killed)
      exit();
    myproc()->tf = tf;
    syscall();
    if(myproc()->killed)
      exit();
    return;
  }

  switch(tf->trapno){
  case T_IRQ0 + IRQ_TIMER:
    if(cpuid() == 0){
      acquire(&tickslock);
      ticks++;
      wakeup(&ticks);
      release(&tickslock);
    }
    lapiceoi();
    break;
  case T_IRQ0 + IRQ_IDE:
    ideintr();
    lapiceoi();
    break;
  case T_IRQ0 + IRQ_IDE+1:
    // Bochs generates spurious IDE1 interrupts.
    break;
  case T_IRQ0 + IRQ_KBD:
    kbdintr();
    lapiceoi();
    break;
  case T_IRQ0 + IRQ_COM1:
    uartintr();
    lapiceoi();
    break;
  case T_IRQ0 + 7:
  case T_IRQ0 + IRQ_SPURIOUS:
    cprintf("cpu%d: spurious interrupt at %x:%x\n",
            cpuid(), tf->cs, tf->eip);
    lapiceoi();
    break;

  //PAGEBREAK: 13
  default:
    if(myproc() == 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, cpuid(), 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",
            myproc()->pid, myproc()->name, tf->trapno,
            tf->err, cpuid(), tf->eip, rcr2());
    myproc()->killed = 1;
  }

  // Force process exit if it has been killed and is in user space.
  // (If it is still executing in the kernel, let it keep running
  // until it gets to the regular system call return.)
  if(myproc() && myproc()->killed && (tf->cs&3) == DPL_USER)
    exit();

  // Force process to give up CPU on clock tick.
  // If interrupts were on while locks held, would need to check nlock.
  if(myproc() && myproc()->state == RUNNING &&
     tf->trapno == T_IRQ0+IRQ_TIMER)
    yield();

  // Check if the process has been killed since we yielded
  if(myproc() && myproc()->killed && (tf->cs&3) == DPL_USER)
    exit();
}

接着就来到了syscall函数。它主要就是根据系统调用号来找到对应的系统调用过程。

void
syscall(void)
{
  int num;
  struct proc *curproc = myproc();

  num = curproc->tf->eax;
  if(num > 0 && num < NELEM(syscalls) && syscalls[num]) {
    curproc->tf->eax = syscalls[num]();
  } else {
    cprintf("%d %s: unknown sys call %d\n",
            curproc->pid, curproc->name, num);
    curproc->tf->eax = -1;
  }
}

系统调用表定义如下。

static int (*syscalls[])(void) = {
[SYS_fork]    sys_fork,
[SYS_exit]    sys_exit,
[SYS_wait]    sys_wait,
[SYS_pipe]    sys_pipe,
[SYS_read]    sys_read,
[SYS_kill]    sys_kill,
[SYS_exec]    sys_exec,
[SYS_fstat]   sys_fstat,
[SYS_chdir]   sys_chdir,
[SYS_dup]     sys_dup,
[SYS_getpid]  sys_getpid,
[SYS_sbrk]    sys_sbrk,
[SYS_sleep]   sys_sleep,
[SYS_uptime]  sys_uptime,
[SYS_open]    sys_open,
[SYS_write]   sys_write,
[SYS_mknod]   sys_mknod,
[SYS_unlink]  sys_unlink,
[SYS_link]    sys_link,
[SYS_mkdir]   sys_mkdir,
[SYS_close]   sys_close,
};

比如其中的sys_exec,用户进程如果要进行exec的话,必须要通过系统调用的形式,也就是通过系统调用表找到sys_exec,而在sys_exec函数中,实际就是调用了之前分析过的exec函数,而内核中就可以直接调用exec函数。

int
sys_exec(void)
{
  char *path, *argv[MAXARG];
  int i;
  uint uargv, uarg;

  if(argstr(0, &path) < 0 || argint(1, (int*)&uargv) < 0){
    return -1;
  }
  memset(argv, 0, sizeof(argv));
  for(i=0;; i++){
    if(i >= NELEM(argv))
      return -1;
    if(fetchint(uargv+4*i, (int*)&uarg) < 0)
      return -1;
    if(uarg == 0){
      argv[i] = 0;
      break;
    }
    if(fetchstr(uarg, &argv[i]) < 0)
      return -1;
  }
  return exec(path, argv);
}

中断过程

由于xv6构建在多核系统上,所以不使用PIC的方式处理中断。而是使用分离的IOAPIC和local APIC,前者面向设备端,后者面向CPU端。比如在如下代码中xv6通过ioapic将键盘中断导向了CPU0。

void
consoleinit(void)
{
  initlock(&cons.lock, "console");

  devsw[CONSOLE].write = consolewrite;
  devsw[CONSOLE].read = consoleread;
  cons.locking = 1;

  ioapicenable(IRQ_KBD, 0);
}

在main函数中主要有lapicinit来用来初始化local apic,有ioapicinit来初始化io apic,有picinit来禁止pic。比如在lapicinit中初始化定时器中断。这里是硬件上的初始化,即保证CPU能接受到定时器中断信号。

  // The timer repeatedly counts down at bus frequency
  // from lapic[TICR] and then issues an interrupt.
  // If xv6 cared more about precise timekeeping,
  // TICR would be calibrated using an external time source.
  lapicw(TDCR, X1);
  lapicw(TIMER, PERIODIC | (T_IRQ0 + IRQ_TIMER));
  lapicw(TICR, 10000000);

定时器中断的处理和系统调用的处理类似。定时器中断在vector中的号是32,而系统调用是64。而定时器中断的处理过程也就是将tick增加并且调用wakeup函数,而在wakeup函数中可能会发生进程切换。

  case T_IRQ0 + IRQ_TIMER:
    if(cpuid() == 0){
      acquire(&tickslock);
      ticks++;
      wakeup(&ticks);
      release(&tickslock);
    }
    lapiceoi();
    break;

磁盘驱动

磁盘将每512个字节叫做一个扇区(sector)。而xv6中所使用的磁盘读取单位(block)也为一个扇区。xv6使用如下一个结构体来管理一个block。

struct buf {
  int flags;
  uint dev;
  uint blockno;
  struct sleeplock lock;
  uint refcnt;
  struct buf *prev; // LRU cache list
  struct buf *next;
  struct buf *qnext; // disk queue
  uchar data[BSIZE];
};

其中的flags字段包括如下两种值。其中B_VALID表示数据已经从磁盘读进入了内存,而B_DIRTY表示当前内存中的数据需要写入到磁盘上。

#define B_VALID 0x2  // buffer has been read from disk
#define B_DIRTY 0x4  // buffer needs to be written to disk

在main函数中调用了ideinit函数来初始化磁盘驱动。

  ideinit();       // disk 

而在ideinit中,首先调用了ioapicenable函数来开启IRQ_IDE中断。

void
ideinit(void)
{
  int i;

  initlock(&idelock, "ide");
  ioapicenable(IRQ_IDE, ncpu - 1);
  idewait(0);

  // Check if disk 1 is present
  outb(0x1f6, 0xe0 | (1<<4));
  for(i=0; i<1000; i++){
    if(inb(0x1f7) != 0){
      havedisk1 = 1;
      break;
    }
  }

  // Switch back to disk 0.
  outb(0x1f6, 0xe0 | (0<<4));
}

接着调用了idewait函数来等待磁盘进入能够接收CPU命令的状态。而在idewait中通过IO端口0x1f7来读取当前磁盘的状态。idewait函数一直轮询端口的状态值并一直等到IDE_BSY为0且IDE_DRDY为1才返回。而此时表示磁盘已经准备好接收CPU命令开始工作了。

// Wait for IDE disk to become ready.
static int
idewait(int checkerr)
{
  int r;

  while(((r = inb(0x1f7)) & (IDE_BSY|IDE_DRDY)) != IDE_DRDY)
    ;
  if(checkerr && (r & (IDE_DF|IDE_ERR)) != 0)
    return -1;
  return 0;
}

而对上述struct buf进行操作的主要函数就是iderw,这个函数主要的功能是同步struct buf和磁盘之间的数据,比如当B_DIRTY被置位的时候,它会将buf写到磁盘上,而当B_VALID被置位的时候,它就会从磁盘上读取最新的数据来更新struct buf。

//PAGEBREAK!
// Sync buf with disk.
// If B_DIRTY is set, write buf to disk, clear B_DIRTY, set B_VALID.
// Else if B_VALID is not set, read buf from disk, set B_VALID.
void
iderw(struct buf *b)
{
  struct buf **pp;

  if(!holdingsleep(&b->lock))
    panic("iderw: buf not locked");
  if((b->flags & (B_VALID|B_DIRTY)) == B_VALID)
    panic("iderw: nothing to do");
  if(b->dev != 0 && !havedisk1)
    panic("iderw: ide disk 1 not present");

  acquire(&idelock);  //DOC:acquire-lock

  // Append b to idequeue.
  b->qnext = 0;
  for(pp=&idequeue; *pp; pp=&(*pp)->qnext)  //DOC:insert-queue
    ;
  *pp = b;

  // Start disk if necessary.
  if(idequeue == b)
    idestart(b);

  // Wait for request to finish.
  while((b->flags & (B_VALID|B_DIRTY)) != B_VALID){
    sleep(b, &idelock);
  }


  release(&idelock);
}

注意到当对磁盘进行读写的时候,必须要考虑到磁盘和CPU之间的速度差异,如果在读写磁盘的时候一直循环等待显然耗费了大量的CPU资源。所以可以看到iderw对struct buf的读写使用了队列的机制,每次只处理队头的读写请求(这是由于磁盘每次只能处理一个请求,即进行一次读或写),且在读写过程中进入sleep状态,这样其他进程就可以在读写的时间内继续工作。
其中的idestart函数就是通过IO端口向磁盘控制器发出读写请求。

  if(b->flags & B_DIRTY){
    outb(0x1f7, write_cmd);
    outsl(0x1f0, b->data, BSIZE/4);
  } else {
    outb(0x1f7, read_cmd);
  }

最后当磁盘完成读写请求后,会产生一个中断,而处理这个中断的函数就是ideintr函数。当发出的操作是读时,产生的中断信号会告知此时磁盘数据已经ready可以直接读到内存了,而在ideintr中就会进行读取过程。当发出的操作是写时,产生的中断信号就会告知此时数据已经全部写入磁盘了。最后ideintr会唤醒等待的进程并继续发送队列中的下一个磁盘请求。

// Interrupt handler.
void
ideintr(void)
{
  struct buf *b;

  // First queued buffer is the active request.
  acquire(&idelock);

  if((b = idequeue) == 0){
    release(&idelock);
    return;
  }
  idequeue = b->qnext;

  // Read data if needed.
  if(!(b->flags & B_DIRTY) && idewait(1) >= 0)
    insl(0x1f0, b->data, BSIZE/4);

  // Wake process waiting for this buf.
  b->flags |= B_VALID;
  b->flags &= ~B_DIRTY;
  wakeup(b);

  // Start disk on next buf in queue.
  if(idequeue != 0)
    idestart(idequeue);

  release(&idelock);
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值