6.s081 XV6 底层IO的解析(1)

xv6底层IO的解析——硬件中断篇(1)

xv6的输入篇

xv6的IO这部分的源码可以说是笔者至今为止认为xv6最难的部分
因为这部分涉及到软件和硬件的交互,读者们必须先对前几章的
内容有所了解,才能了解这一章

概述

UART:你可以理解为输入字符和输出字符用的硬件
内存空间中有一部分地址线属于硬件访问的地址线,这部分地址线并不与RAM相连,而是直接连接到硬件的端口上,这样使得CPU能够像访问内存一样访问硬件,这样就能够充分利用地址总线来传输信息。

当我们在控制台输入一个字符的时候,首先会触发一个硬件中断,同时UART会读取你输入的字符,然后传到UART的RHR寄存器——UART硬件内部是一个FIFO形式的队列,而且队头的字符会被放到RHR寄存器上。
硬件中断会传到内核空间,然后沿着trap路径调用UART中断处理程序,然后操作系统的程序会访问内存上的UART位置,然后读取RHR寄存器来获取我们在控制台输入的字符,传入内核的控制台缓冲区,只有在检测到回车键,文件尾或者输入量超出缓冲区长度时,内核才会把缓冲区的数据读入程序中。

UART的寄存器

大致的调用路径就像上图说的那样,接下来我们来详细解析输入流程的源码
基本的trap源码不再做进一步解析,我们直接从环境初始化和UART开始解析

操作系统直接通过内存空间访问UART,而UART中开放了几个寄存器来作为交互的接口

// the UART control registers are memory-mapped
// at address UART0. this macro returns the
// address of one of the registers.
#define Reg(reg) ((volatile unsigned char *)(UART0 + reg))

// the UART control registers.
// some have different meanings for
// read vs write.
// see http://byterunner.com/16550.html
#define RHR 0                 // receive holding register (for input bytes)
#define THR 0                 // transmit holding register (for output bytes)
#define IER 1                 // interrupt enable register
#define IER_RX_ENABLE (1<<0)
#define IER_TX_ENABLE (1<<1)
#define FCR 2                 // FIFO control register
#define FCR_FIFO_ENABLE (1<<0)
#define FCR_FIFO_CLEAR (3<<1) // clear the content of the two FIFOs
#define ISR 2                 // interrupt status register
#define LCR 3                 // line control register
#define LCR_EIGHT_BITS (3<<0)
#define LCR_BAUD_LATCH (1<<7) // special mode to set baud rate
#define LSR 5                 // line status register
#define LSR_RX_READY (1<<0)   // input is waiting to be read from RHR
#define LSR_TX_IDLE (1<<5)    // THR can accept another character to send

#define ReadReg(reg) (*(Reg(reg)))
#define WriteReg(reg, v) (*(Reg(reg)) = (v))

RHR寄存器用于输入,THR寄存器用于输出,THR的解析我们下篇细说
IER是中断开关寄存器,用于控制UART中断传递的开关
IER_RX_ENABLE位 用于控制输入中断
IER_TX_ENBALE位 用于控制输出中断

FCR是FIFO的状态寄存器
FCR_FIFO_ENBALE位 用于控制FIFO读入读出状态
FCR_FIFO_CLEAR位 用于清除输入输出的FIFO的内容

LSR寄存器是行状态寄存器
LSR_RX_READY位 用于表示输入端的读取状态
LSR_TX_IDLE位 用于表示THR寄存器已经可以准备输出

其他寄存器暂时用不上,我们不做介绍

第4行可以获得对应寄存器的内核虚拟地址
26行负责读取对应寄存器的值
27行负责向对应寄存器作写入值
注意第四行的 volatile unsigned char* 这说明读取和写入这些寄存器的长度都是一字节

syscall read()读取控制台输入的解析

接下来我们开始解析read()读取控制台的过程
在用户空间调用read()的时候,系统首先产生trap进入内核
然后进入sys_read() [kernel/sysfile.c]

uint64 sys_read(void)
{
  struct file *f;
  int n;
  uint64 p;

  argaddr(1, &p);
  argint(2, &n);
  if(argfd(0, 0, &f) < 0)
    return -1;
  return fileread(f, p, n);
}

sys_read()的关键函数是fileread(f,p,n) [kernel/file.c]
我们来看看 fileread(f,p,n) 的作用

// Read from file f.
// addr is a user virtual address.
int fileread(struct file *f, uint64 addr, int n)
{
  int r = 0;

  if(f->readable == 0)
    return -1;

  if(f->type == FD_PIPE){
    r = piperead(f->pipe, addr, n);
  } else if(f->type == FD_DEVICE){
    if(f->major < 0 || f->major >= NDEV || !devsw[f->major].read)
      return -1;
    r = devsw[f->major].read(1, addr, n);
  } else if(f->type == FD_INODE){
    ilock(f->ip);
    if((r = readi(f->ip, 1, addr, f->off, n)) > 0)
      f->off += r;
    iunlock(f->ip);
  } else {
    panic("fileread");
  }

  return r;
}

//[kernel/file.h]
struct file {
  enum { FD_NONE, FD_PIPE, FD_INODE, FD_DEVICE } type;
  int ref; // reference count
  char readable;
  char writable;
  struct pipe *pipe; // FD_PIPE
  struct inode *ip;  // FD_INODE and FD_DEVICE
  uint off;          // FD_INODE
  short major;       // FD_DEVICE
};

由于第10行到11行的分支用于处理管道的输入和输出,而第16到20行用于处理文件的输入和输出,因此我们只取12到15行的控制台输出来进行分析

12行显然,用于判定本次读取是由于设备中断而引起的,所以可判定是控制台输入
在解析13行的分支前,可以先看file结构体的描述,第37行中major代表了文件的设备编号
到这里可以回想第一章的内容——文件是对IO的抽象,所以file结构体中包含了管道,程序文件,和设备文件的信息,我们能够利用这些信息来判断这个文件的性质

第13行的前两个分支就是用于判断这个设备文件的所属设备号,然后第三个分支我们来看看devsw结构

// map major device number to device functions.
struct devsw {
  int (*read)(int, uint64, int);
  int (*write)(int, uint64, int);
};

extern struct devsw devsw[];

devsw结构用于保存相对应的结构体的IO函数,devsw数组对应的是各个设备的IO,借助这个语法我们就能够为每个设备接上单独的IO函数,而且为单个设备更换IO手段也非常方便,只需要在运行时更换即可

回到fileread()的解析
14行调用devsw中存储的read函数,读取输入端的内容
然后我们开始讲devsw的read函数,毕竟我们这里只是对read函数做了调用,并没有讲这个东西是哪里来的

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

  uartinit();

  // connect read and write system calls
  // to consoleread and consolewrite.
  devsw[CONSOLE].read = consoleread;
  devsw[CONSOLE].write = consolewrite;
}

consoleinit() [kernel/console.c] 中,可以发现它调用了uart初始化函数以及初始化了devsw[CONSOLE]的IO函数,事实上CONSOLE是一个宏,值为1,对应了uart的设备编号

uartinit()我们不再作深入解析,没什么意义,感兴趣的读者可以自行翻阅xv6源码

到这里我们应该要停下来,做个概念区分:
当我们调用read的时候,控制台不一定已经输入了东西,输入和读取两个过程在内核中是独立的过程

我们继续解析read()的调用路径

fileread() 调用了 consoleread() [kernel/console.c] ,consoleread() 是控制台的输入函数,能够将缓冲区的内容读入用户空间提供的缓冲区中
以下是consoleread()以及consoleintr()的源码

//
// user read()s from the console go here.
// copy (up to) a whole input line to dst.
// user_dist indicates whether dst is a user
// or kernel address.
//
int consoleread(int user_dst, uint64 dst, int n)
{
  uint target;
  int c;
  char cbuf;

  target = n;
  acquire(&cons.lock);
  while(n > 0){
    // wait until interrupt handler has put some
    // input into cons.buffer.
    while(cons.r == cons.w){
      if(killed(myproc())){
        release(&cons.lock);
        return -1;
      }
      sleep(&cons.r, &cons.lock);
    }

    c = cons.buf[cons.r++ % INPUT_BUF_SIZE];

    if(c == C('D')){  // end-of-file
      if(n < target){
        // Save ^D for next time, to make sure
        // caller gets a 0-byte result.
        cons.r--;
      }
      break;
    }

    // copy the input byte to the user-space buffer.
    cbuf = c;
    if(either_copyout(user_dst, dst, &cbuf, 1) == -1)
      break;

    dst++;
    --n;

    if(c == '\n'){
      // a whole line has arrived, return to
      // the user-level read().
      break;
    }
  }
  release(&cons.lock);

  return target - n;
}


struct {
  struct spinlock lock;
  
  // input
#define INPUT_BUF_SIZE 128
  char buf[INPUT_BUF_SIZE];
  uint r;  // Read index
  uint w;  // Write index
  uint e;  // Edit index
} cons;

首先看consoleread(),在这里18行到24行并不好理解,笔者将作详细解析
在解析18行的判断前我们首先要看66行定义的匿名结构体cons,cons存储了控制台的输入缓冲区的相关信息,相关的下标的大致作用在注释上有写,笔者在这里着重对三个变量的作用进行讲解

63行 变量r代表了当前consoleread()实际读入且回传到用户空间的字节数
64行 变量w代表了当前consoleread()实际读入到console的缓冲区cons.buf中的实际字节数
65行 变量e代表了当前consoleintr()读入到cons.buf,但是又并没有完全保存下来的字节数

w和e的区别多少有点抽象,笔者举个简单的例子,就像你在写word的时候,你点开word文件,写了一部分内容,但是没点保存,这时候就相当于你把文件内容写进去了cons.buf,字节数为e,你没点保存的话,在退出的时候就给你清除掉了你写的内容,你再打开的时候还是原来的文件,也就是字节数还是为w

18行里面首先比较 cons.r 和 cons.w,这表明cons.buf没有发生任何变化,然后19行检查当前是不是已经寄了,如果一切正常,那就在21行调用sleep()让程序睡眠,等待中断的到来

然后在这里我们先停止对consoleread()的分析
我们再一次强调这个观点:当我们调用read的时候,控制台不一定已经输入了东西,输入和读取两个过程在内核中是独立的过程

为什么要这样强调这个观点呢,因为在这里,如果我们不输入东西,不触发任何中断,consoleread()就会一直在21行这里睡大觉,就读取不了内容
你每一次成功输入一个字符,其实都触发了一次硬件中断,然后会唤醒consoleread()读取缓冲区

所以我们接下来需要重点来解析UART中断了,因为这个是能够唤醒consoleread()的关键路径

UART中断与consoleintr()

当我们输入字符时,UART硬件会请求riscv触发一次硬件中断,而通过我们之前提到过的trap跳转,在usertrap()中,函数将进入devintr()来判断硬件中断的类型

int devintr()
{
  uint64 scause = r_scause();

  if((scause & 0x8000000000000000L) &&
     (scause & 0xff) == 9){
    // this is a supervisor external interrupt, via PLIC.

    // irq indicates which device interrupted.
    int irq = plic_claim();

    if(irq == UART0_IRQ){
      uartintr();
    } else if(irq == VIRTIO0_IRQ){
      virtio_disk_intr();
    } else if(irq){
      printf("unexpected interrupt irq=%d\n", irq);
    }

    // the PLIC allows each device to raise at most one
    // interrupt at a time; tell the PLIC the device is
    // now allowed to interrupt again.
    if(irq)
      plic_complete(irq);

    return 1;
  } else if(scause == 0x8000000000000001L){
    // software interrupt from a machine-mode timer interrupt,
    // forwarded by timervec in kernelvec.S.

    if(cpuid() == 0){
      clockintr();
    }
    
    // acknowledge the software interrupt by clearing
    // the SSIP bit in sip.
    w_sip(r_sip() & ~2);

    return 2;
  } else {
    return 0;
  }
}

devintr()首先利用scauese检查中断类型,然后判断出是UART发送的中断,于是调用uartintr(),这便是UART中断处理器

// handle a uart interrupt, raised because input has
// arrived, or the uart is ready for more output, or
// both. called from devintr().
void uartintr(void)
{
  // read and process incoming characters.
  while(1){
    int c = uartgetc();
    if(c == -1)
      break;
    consoleintr(c);
  }

  // send buffered characters.
  acquire(&uart_tx_lock);
  uartstart();
  release(&uart_tx_lock);
}

// read one input character from the UART.
// return -1 if none is waiting.
int
uartgetc(void)
{
  if(ReadReg(LSR) & 0x01){
    // input data is ready.
    return ReadReg(RHR);
  } else {
    return -1;
  }
}

在触发中断以后,uartintr()调用uartgetc(),以读取一个字节
uartgetc()则非常直接,它做了两件事,第一是检查LSR寄存器中的LSR_RX_REDAY位(也就是第一位),判断输入是否就绪,然后用ReadReg(RHR)来读取RHR寄存器中的一个字节,接着uartintr()将这个字节传给consoleintr()

第16行uartstart()负责将输出缓冲区中的字符全部发送出去,这样就不需要触发太多次中断了,这个函数的功能我们留到下一篇再详细解析

consoleintr()的主要功能是将UART的RHR中传过来的内容读入cons.buf,并叫醒consoleread()起来干活

#define BACKSPACE 0x100
#define C(x)  ((x)-'@')  // Control-x
//
// the console input interrupt handler.
// uartintr() calls this for input character.
// do erase/kill processing, append to cons.buf,
// wake up consoleread() if a whole line has arrived.
//
void consoleintr(int c)
{
  acquire(&cons.lock);

  switch(c){
  case C('P'):  // Print process list.
    procdump();
    break;
  case C('U'):  // Kill line.
    while(cons.e != cons.w &&
    cons.buf[(cons.e-1) % INPUT_BUF_SIZE] != '\n'){
      cons.e--;
      consputc(BACKSPACE);
    }
    break;
  case C('H'): // Backspace
  case '\x7f': // Delete key
    if(cons.e != cons.w){
      cons.e--;
      consputc(BACKSPACE);
    }
    break;
  default:
    if(c != 0 && cons.e-cons.r < INPUT_BUF_SIZE){
      c = (c == '\r') ? '\n' : c;

      // echo back to the user.
      consputc(c);

      // store for consumption by consoleread().
      cons.buf[cons.e++ % INPUT_BUF_SIZE] = c;

      if(c == '\n' || c == C('D') || cons.e-cons.r == INPUT_BUF_SIZE){
        // wake up consoleread() if a whole line (or end-of-file)
        // has arrived.
        cons.w = cons.e;
        wakeup(&cons.r);
      }
    }
    break;
  }
  
  release(&cons.lock);
}

这段功能没什么太值得讲的,非常简单,看注释就能看懂了
最后便是唤醒consoleread(),然后回到consoleread(),读取缓冲区内容,然后传到用户空间,完成一次输入

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值