5.2: Code: Console output

对控制台的文件描述符进行的写系统调用最终会到达 uartputc(kernel/uart.c:87)。

// add a character to the output buffer and tell the
// UART to start sending if it isn't already.
// blocks if the output buffer is full.
// because it may block, it can't be called
// from interrupts; it's only suitable for use
// by write().
void
uartputc(int c)
{
  acquire(&uart_tx_lock);

  if(panicked){
    for(;;)
      ;
  }
  while(uart_tx_w == uart_tx_r + UART_TX_BUF_SIZE){
    // buffer is full.
    // wait for uartstart() to open up space in the buffer.
    sleep(&uart_tx_r, &uart_tx_lock);
  }
  uart_tx_buf[uart_tx_w % UART_TX_BUF_SIZE] = c;
  uart_tx_w += 1;//将每个字符追加到缓冲区
  uartstart();
  release(&uart_tx_lock);
}


设备驱动程序维护一个输出缓冲区(uart_tx_buf),这样写入进程无需等待 UART 完成发送操作。相反,uartputc 会将每个字符追加到缓冲区中,调用 uartstart 启动设备传输(如果设备尚未开始传输),然后立即返回。

  • 唯一会导致 uartputc 等待的情况是缓冲区已满。

每次 UART 完成发送一个字节时,它会生成一个中断。uartintr 会调用 uartstart,后者检查设备是否确实完成了发送,并将缓冲区中的下一个输出字符交给设备发送。因此,当一个进程向控制台写入多个字节时,通常第一个字节会通过 uartputc 调用 uartstart 被发送,而缓冲区中剩余的字节将通过uartintr 中的 uartstart 调用,在传输完成中断到达时依次发送。

  • 关于uartstart函数


// if the UART is idle, and a character is waiting
// in the transmit buffer, send it.
// caller must hold uart_tx_lock.
// called from both the top- and bottom-half.
void
uartstart()
{
  while(1){
    if(uart_tx_w == uart_tx_r){
      // transmit buffer is empty.
      ReadReg(ISR);
      return;
    }
    
    if((ReadReg(LSR) & LSR_TX_IDLE) == 0){
      // the UART transmit holding register is full,
      // so we cannot give it another byte.
      // it will interrupt when it's ready for a new byte.
      return;
    }
    
    int c = uart_tx_buf[uart_tx_r % UART_TX_BUF_SIZE];
    uart_tx_r += 1;
    
    // maybe uartputc() is waiting for space in the buffer.
    wakeup(&uart_tx_r);
    
    WriteReg(THR, c);
  }
}
  • uartstart是 UART 驱动的核心函数之一,负责从发送缓冲区中提取字符并传递给 UART 硬件进行发送。它既可以由进程调用(上半部 top-half),也可以由中断处理程序调用(下半部 bottom-half)。函数的调用者需要持有发送缓冲区的锁 uart_tx_lock,以确保对缓冲区的访问是线程安全的。

if (uart_tx_w == uart_tx_r) {//
    // transmit buffer is empty.
    ReadReg(ISR);
    return;
}
  • 条件含义:uart_tx_w 是发送缓冲区的写指针,uart_tx_r 是读取指针。如果两者相等,说明缓冲区没有待发送的数据。

  • 操作:调用ReadReg(ISR):读取中断状态寄存器(ISR)以清除潜在的中断状态,确保后续操作的准确性。然后没有数据需要发送,退出函数

if ((ReadReg(LSR) & LSR_TX_IDLE) == 0) {
    // the UART transmit holding register is full,
    // so we cannot give it another byte.
    // it will interrupt when it's ready for a new byte.
    return;
}
  • 条件含义:通过读取 UART 的线路状态寄存器(LSR),检查是否设置了 LSR_TX_IDLE 位(表示 UART 的发送寄存器空闲)。如果未设置,说明 UART 当前忙于发送数据。

  • 操作: 返回:如果 UART 硬件尚未准备好接收新数据,则退出函数。硬件会在准备好时通过中断通知驱动。

int c = uart_tx_buf[uart_tx_r % UART_TX_BUF_SIZE];
uart_tx_r += 1;
  • 从缓冲区读取一个字符

  • 操作: 从发送缓冲区中读取当前字符,位置为 uart_tx_r % UART_TX_BUF_SIZE(取模操作确保缓冲区是环形的)。 将读取指针 uart_tx_r 向前移动一个位置,表示已经消费了一个字符。

wakeup(&uart_tx_r);
  • 唤醒可能等待缓冲区的进程

WriteReg(THR, c);
  • 将字符写入发送寄存器

  • 操作:将刚从缓冲区读取的字符 c 写入 UART 的发送保持寄存器(THR),触发硬件开始发送该字符。

需要注意的一个通用模式是,通过缓冲中断将设备活动与进程活动解耦。 即使没有进程等待读取,控制台驱动程序也能处理输入;随后的读取操作会看到已经累积的输入。同样,进程也可以发送输出,而无需等待设备完成。这种解耦通过允许进程与设备 I/O 并发执行来提高性能,尤其是在设备速度较慢(如 UART)或需要立即处理(如回显键入的字符)时显得尤为重要。

这种思想也被称为 I/O 并发(I/O concurrency)