Lec5 设备与驱动


设备

xv6中的设备

这里以PCI总线设备为例,展示映射IO

驱动

驱动是操作系统中管理特定设备的代码,包含如下功能:

  • 初始化硬件寄存器
  • 向硬件发起操作指令
  • 处理硬件产生的中断
  • 唤醒因等待硬件完成指令而处于阻塞状态的进程

驱动程序通常由两部分组成:

  • Top Half:运行在进程上下文中,负责接收内核或用户空间的请求,并向硬件设备发起操作。这部分通常通过系统调用触发,可能会阻塞等待操作完成。
  • Bottom Half:运行在中断上下文中,作为硬件中断的服务程序,负责响应设备中断,处理已完成的操作,并通知等待的进程,必须快速执行不能阻塞。

典型流程:

  1. 用户调用read() → Top Half启动磁盘读取并休眠(此时 CPU 可调度其他任务)
  2. 磁盘就绪后触发中断 → Bottom Half唤醒进程并返回数据

程序通过系统调用向外部设备发出指令,系统调用再通过Top Half向硬件设备发起操作请求,并阻塞自己以等待操作完成;硬件完成操作后会产生中断信号,CPU随即跳转至Bottom Half硬件设备对应的中断处理程序,该程序在确认操作结果后,通过同步机制唤醒之前阻塞在Top Half的进程,使其能够继续执行并处理后续流程(如将数据从内核缓冲区拷贝至用户空间),从而完成整个I/O操作。

这里以 xv6 的 UART 设备驱动程序为例,展示驱动程序的结构

UART

UART(Universal Asynchronous Receiver/Transmitter)是一种异步串行通信硬件

xv6 通过该硬件与外部设备(如键盘、显示器终端)低速交换数据

控制台的输入和输出功能都是通过UART驱动与硬件进行串行数据传输来完成的

UART 驱动 (uart.c)

UART 寄存器

数据寄存器

  • THR:主机的输出暂存在此
  • RHR:主机从这里读取

状态寄存器 LSR

  • LSR_TX_IDLE=1:可以向 THR 输出数据
  • LSR_RX_READY=1:RHR 中有可读取的数据
#define RHR 0                 // receive holding register (for input bytes)
#define THR 0                 // transmit holding register (for output bytes)
#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

UART 读取

如果 LSR 寄存器处于待读取状态,则从 RHR 寄存器中获取数据,是一个瞬时动作,没有任何忙等待/睡眠

int uartgetc(void) {
    if(ReadReg(LSR) & LSR_RX_READY) return ReadReg(RHR); // input data is ready.
    else return -1;
}

UART 写入

UART 设备不是无时无刻都可以接受主机的数据,主机为此要做出妥协:

  • 忙等待UART设备准备接受数据,再接受
  • 开辟缓冲区暂存主机待写入UART的数据,如果UART可接受数据直接写入,否则就一直存在那,等主机可以接受数据时向主机发送中断,在中断处理函数中发送缓冲区中的数据

忙等待方式(uartputc_sync):忙等待 LSR 可写入,后向 THR 写入数据

// alternate version of uartputc() that doesn't use interrupts
// spins waiting for the uart's output register to be empty.
// used by kernel printf() and to echo characters. 
void uartputc_sync(int c) {
        push_off();
        // wait for Transmit Holding Empty to be set in LSR.
        while((ReadReg(LSR) & LSR_TX_IDLE) == 0)
        WriteReg(THR, c);
        pop_off();
}

缓冲区方式(uartputc):尝试获取缓冲区的锁后,如果有空闲位置写入数据,否则睡眠等待直到有空位为止。

将缓冲区中的数据写入寄存器(uartstart):当从缓冲中读取数据时,会唤醒等待缓冲区空位的睡眠线程。

#define UART_TX_BUF_SIZE 32
char uart_tx_buf[UART_TX_BUF_SIZE];
uint64 uart_tx_w; // write next to uart_tx_buf[uart_tx_w % UART_TX_BUF_SIZE]
uint64 uart_tx_r; // read next from uart_tx_buf[uart_tx_r % UART_TX_BUF_SIZE]

void uartputc(int c) {
        acquire(&uart_tx_lock);
        while(1){
            if(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);
            } else {
              uart_tx_buf[uart_tx_w % UART_TX_BUF_SIZE] = c;
              uart_tx_w += 1;
              uartstart();
              release(&uart_tx_lock);
              return;
            }
        }
}

// if the UART is idle, and a character is waiting in the transmit buffer, send it.
// called from both the top- and bottom-half
// caller must hold uart_tx_lock
void uartstart() {
  while(1) {
    // transmit buffer is empty
    if(uart_tx_w == uart_tx_r)   
      return;

     // the UART not ready for receiving data, so we cannot give it another byte
     // it will interrupt when it's ready for a new byte
     // prepare for bottom-up(uartputc)
    if((ReadReg(LSR) & LSR_TX_IDLE) == 0)
      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);
  }
}

UART 中断处理程序

UART 设备在处于准备向主机发送数据,或处于可接收主机输出数据这两个状态时,向主机发出中断

xv6中,UART 的中断处理函数同时执行从寄存器中读取数据发送缓冲区数据两个任务

void uartintr(void) {
        // read and process incoming characters.
        while(1) {
            int c = uartgetc();
            if(c == -1) break;
            consoleintr(c);  // 追加读取的字符至cons.buffer并唤醒可能休眠的consoleread()
        }
      acquire(&uart_tx_lock);
      uartstart();  // send buffered characters
      release(&uart_tx_lock);
}

top-half (interrupt) 和 bottom-half (fn) 均会调用 uartstart

控制台 与 UART

控制台中断(consoleintr):使用consputc打印每个从UART读取到的字符

// the console input interrupt handler, called by uartintr
// 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('U'):  // Kill line.
                while(cons.e != cons.w && cons.buf[(cons.e-1) % INPUT_BUF] != '\n') {
                    cons.e--;
                    consputc(BACKSPACE);
                }
                break;
            case '\x7f':
                if(cons.e != cons.w){
                    cons.e--;
                    consputc(BACKSPACE);
                }
                break;
            default:
                if(c != 0 && cons.e-cons.r < INPUT_BUF){
                    c = (c == '\r') ? '\n' : c;
                    consputc(c); // echo back to the user.
                }
                break;
        }
        release(&cons.lock);
}

向控制台打印字符

  • consputc:少许写入,基于uartputc_syncprintfconsoleintr调用
  • consolewrite:大量写入,基于uartputcwrtie 调用
// send one character to the uart
// called by printf, to echo input characters, not from write()
void consputc(int c) {
 // ...
        uartputc_sync(c);
}

// send many characters to the uart
// user write()s to the console go here
int consolewrite(int user_src, uint64 src, int n) {
        for(i = 0; i < n; i++) {
            if(either_copyin(&c, user_src, src+i, 1) == -1) break;
            uartputc(c);
        }
        return i;
}

文章作者: AthenaCrafter
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 AthenaCrafter !
  目录