5.1: Code: Console input

知识点介绍

控制台驱动程序通过连接到 RISC-V 的 UART串口硬件接受由用户键入的字符。该驱动程序一次累积一行输入,处理特殊的输入字符,例如退格键(backspace)和 Control-U。

用户进程(如 shell)通过 read 系统调用从控制台中获取输入行。当您在 QEMU 中向 xv6 输入内容时,您的按键通过 QEMU 的模拟 UART 硬件传递给 xv6。

驱动程序交互的 UART 硬件是 QEMU 模拟的 16550 芯片 [13]。在真实的计算机中,16550 芯片会管理一个 RS232 串行连接,与终端或其他计算机通信。 QEMU 中,16550 模拟器连接到您的键盘和显示器。

UART 硬件对软件的呈现方式是一组内存映射的控制寄存器。这意味着 RISC-V 硬件将某些物理地址连接到 UART 设备,因此对这些地址的加载和存储操作会与设备硬件交互,而不是与 RAM 交互。UART 的内存映射地址从 0x10000000 开始,对应 UART0(定义在 kernel/memlayout.h:21 中)。

// qemu puts UART registers here in physical memory.
#define UART0 0x10000000L
#define UART0_IRQ 10

// virtio mmio interface
#define VIRTIO0 0x10001000
#define VIRTIO0_IRQ 1

UART 有一小部分控制寄存器,每个寄存器的宽度为一个字节,它们相对于 UART0 的偏移量定义在 kernel/uart.c:22 中。

// 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


// qemu puts UART registers here in physical memory.
#define UART0 0x10000000L
#define UART0_IRQ 10

// virtio mmio interface
#define VIRTIO0 0x10001000
#define VIRTIO0_IRQ 1


// 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)))


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

例如,LSR 寄存器包含的位表示是否有输入字符等待软件读取。这些字符(如果有)可以从 RHR 寄存器中读取。每次读取一个字符时,UART 硬件会将其从内部的 FIFO 等待队列中删除,并在 FIFO 为空时清除 LSR 中的“就绪”位。UART 的发送硬件与接收硬件几乎独立;如果软件向 THR 寄存器写入一个字节,UART 会发送该字节。

Xv6 的 main 函数通过调用 consoleinit(kernel/console.c:182)来初始化 UART read/out硬件。

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;
}

这段代码将UART配置为在接收每个输入字节时生成接收中断,以及在每次完成发送一个输出字节时生成发送完成中断(kernel/uart.c:53)。

void
uartinit(void)
{
  // disable interrupts.
  WriteReg(IER, 0x00);

  // special mode to set baud rate.
  WriteReg(LCR, LCR_BAUD_LATCH);

  // LSB for baud rate of 38.4K.
  WriteReg(0, 0x03);

  // MSB for baud rate of 38.4K.
  WriteReg(1, 0x00);

  // leave set-baud mode,
  // and set word length to 8 bits, no parity.
  WriteReg(LCR, LCR_EIGHT_BITS);

  // reset and enable FIFOs.
  WriteReg(FCR, FCR_FIFO_ENABLE | FCR_FIFO_CLEAR);

  // enable transmit and receive interrupts.
  WriteReg(IER, IER_TX_ENABLE | IER_RX_ENABLE);

  initlock(&uart_tx_lock, "uart");
}

Xv6 的 shell 通过 init.c(user/init.c:19)打开的文件描述符从控制台读取输入。

int
main(void)
{
  int pid, wpid;

  if(open("console", O_RDWR) < 0){
    mknod("console", CONSOLE, 0);
    open("console", O_RDWR);
  }

对 read 系统调用的请求会穿过内核到达 consoleread(kernel/console.c:80)。

// 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;
      }
96:      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;
}

consoleread 等待输入通过中断到达并被缓冲到 cons.buf 中,然后将输入复制到用户空间,并在一整行到达后返回用户进程。如果用户尚未键入完整的一行,任何读取的进程将会在调用 sleep(kernel/console.c:96)时进入等待状态(第 7 章详细解释了 sleep 的机制)。

当用户键入一个字符时,UART 硬件会请求 RISC-V 触发一个中断,从而激活 xv6 的中断处理程序。中断处理程序调用 devintr(kernel/trap.c:185:作用参照上一章intro)后者通过检查 RISC-V 的 scause 寄存器发现中断来自外部设备。接着,它请求名为 PLIC [3] 的硬件单元指示具体是哪个设备触发了中断(kernel/trap.c:193;参照上一章intro的devintr)。如果是 UART,devintr 会调用 uartintr。

uartintr(kernel/uart.c:177)从 UART 硬件中读取所有等待的输入字符,并将它们交给 consoleintr(kernel/console.c:136)。

// 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);
}
  • consoleintr

// 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);
}

uartintr 不会等待字符输入,因为未来的输入会触发新的中断。consoleintr 的任务是将输入字符累积到cons.buf 中,直到一整行到达为止。consoleintr 会特别处理退格键和其他几个特殊字符。当收到换行符时,consoleintr会唤醒一个正在等待的 consoleread(如果有)。一旦被唤醒,consoleread会观察到cons.buf中有完整的一行内容,将其复制到用户空间,并通过系统调用机制返回到用户空间。

相关问题

  • uart的寄存器和物理内存地址的映射如何实现,通过寄存器的引脚和总线然后呢 具体步骤如下:

数据传输到寄存器: UART 接收端的数据通过 RXD 引脚接收,存储到数据寄存器(DR)。 处理器通过总线读取 DR 寄存器,获取接收到的数据。 数据从寄存器传输: 处理器通过总线写入数据到 DR 寄存器。 UART 硬件从 DR 寄存器读取数据,并通过 TXD 引脚发送。

  • 总线是计算机中的bus吗,通过总线实现cpu和内存,硬盘以及其他外围设备的相互访问? 是的,总线(Bus)是计算机系统中的一种通信机制,负责在计算机内部不同组件之间传递数据、地址和控制信号。通过总线,CPU、内存、硬盘以及其他外围设备可以实现相互访问和协作。以下是总线的基本概念和作用

总线是计算机中 CPU、内存、硬盘、外设之间通信的关键桥梁。通过数据总线、地址总线和控制总线的协同作用,实现了设备之间的互联和协作。随着计算机体系结构的演化,总线也从单一共享模式发展为分层架构和高速点对点连接,以满足现代系统对性能和扩展性的需求。

基于上述分析,一个现代 CPU(如 Intel 或 AMD 的主流桌面 CPU)通常需要:

内存总线:100~200 个引脚。 PCIe 总线:64~80 个引脚。 外设总线:30~50 个引脚。 电源和地:300~600 个引脚。 其他专用接口:50~100 个引脚。 总计:现代 CPU 的引脚数量通常在 800 到 1500 个 之间(如 LGA1200、AM5 等插槽)。

  • 一般进程与外设交互的过程

  1. 系统调用与进程状态保存 当一个进程发起设备操作请求(如读取硬盘数据)时:

系统调用进入内核模式:

进程通过系统调用(如 read())向内核发起 I/O 请求。 内核记录调用进程的状态(如寄存器、程序计数器、用户态栈等)。 内核注册请求:

内核将设备操作请求发送给设备驱动程序,并在设备队列中挂起该请求。 内核保存了调用进程的上下文信息(如 PCB 中的 I/O 请求描述)。 进程挂起(yield):

发起 I/O 请求的进程被阻塞,进入等待队列,释放 CPU 时间片。 其他进程继续运行。

进程缓冲区的概念 进程缓冲区是指进程在用户空间中用于临时存储数据的内存区域,通常由程序显式分配或操作系统自动分配。这些缓冲区用于以下场景:

与外围设备交互:

存储从硬盘、键盘、网络等设备读取的数据。 准备要发送到设备的数据。 减少频繁的数据交换:

提高 CPU 和 I/O 设备之间的数据传输效率,避免频繁切换内核态和用户态。 支持批处理: 在执行 I/O 操作时,通过缓冲区分批处理数据,减少系统调用的开销。 等待 I/O 的进程状态记录在内核的进程控制块(PCB)中。