iT邦幫忙

2022 iThome 鐵人賽

DAY 22
0

前言

繼續前面 UART TOP 的部分,下面將介紹 UART Bottom 的部分,也就是關於 Interrupt 的處理,並從 Interrupt 的處理中,切入到 UART 是如何讀取鍵盤輸入的,最後討論一個並行時所會發生的問題,當對同一個 buffer 進行讀取與寫入時所會產生出的 生產/消費者問題。

uartgetc() 從 UART 讀取字元

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

在前面我們看到我們在 uartstart() 使用 THR 來接收要輸出的資料,而如果我們要輸入資料,我們在確認 LSR 後得到裝置處於準備狀態後,接著讀取 RHR,RHR 為用於接收輸入的暫存器,而這裡外部輸入的裝置可以能是鍵盤,鍵盤接收了我們的輸入,接著資料會從 UART 進入到 CPU 中,而這過程中鍵盤可能會產生 Interrupt,Interrupt 如前面所提到,會走向 PLIC,接著 PLIC 會決定是否開處理這個 Interrupt,如果有 CPU hart 是處於閒置的,則會處理這個 Interrupt,也就是 PLIC 會將 Interrupt 發送到閒置的 hart,而如果這個 hart 的 sie CSR 的 SEIE, UEIE 等用於允許 External Interrupt 的域被設置,則接下來的 Interrupt 處理流程我們可以試著跟蹤,我們知道處理 Interrupt 這個概念就是 trap,而我們可以看到 trap.cusertrap() 的部分 (因為 Shell 在 user mode 底下執行,因此我們看到 user mode 底下的 trap)

Interrupt Handle

以下為 trap.cusertrap()

void
usertrap(void)
{
  int which_dev = 0;

  if((r_sstatus() & SSTATUS_SPP) != 0)
    panic("usertrap: not from user mode");

  // send interrupts and exceptions to kerneltrap(),
  // since we're now in the kernel.
  w_stvec((uint64)kernelvec);

  struct proc *p = myproc();
  
  // save user program counter.
  p->trapframe->epc = r_sepc();
  
  if(r_scause() == 8){
    // system call

    if(killed(p))
      exit(-1);

    // sepc points to the ecall instruction,
    // but we want to return to the next instruction.
    p->trapframe->epc += 4;

    // an interrupt will change sepc, scause, and sstatus,
    // so enable only now that we're done with those registers.
    intr_on();

    syscall();
  } else if((which_dev = devintr()) != 0){
    // ok
  } else {
    printf("usertrap(): unexpected scause %p pid=%d\n", r_scause(), p->pid);
    printf("            sepc=%p stval=%p\n", r_sepc(), r_stval());
    setkilled(p);
  }

  if(killed(p))
    exit(-1);

  // give up the CPU if this is a timer interrupt.
  if(which_dev == 2)
    yield();

  usertrapret();
}

看到 devintr(),負責處理與 External Interrupt。

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

首先會通過讀取 scause CSR 得知發生 Interrupt 的原因,從這裡去判斷目前的 Interrupt 是否為 External Interrupt。

接著通過 irq (interrupt request) 去判斷該執行哪一個裝置的 ISR (Interrupt Service Routine),通過 plic_claim() 獲得,在plic_claim()中 CPU 會讀取自己的 PLIC Claim 得到要處理的 Interrupt 類型 (通過編號表示),而 UART 的 Interrupt 類型為編號 10 號,因此在執行完 plic_claim() 後會得到 10,放入 irq 中,而通過這個編號來得到裝置對應的 ISR,這裡對應到的 ISR 為 uartintr()

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

這裡討論的情境,也就是在最一開始 Console 上輸出 $,這裡由於我們還沒使用鍵盤等外部設備對 UART 進行輸入,因此我們會獲得 lock 之後進入到 uartstart() 中,而在uartstart()中會將 uart_tx_buf 的內容輸出,而這邊會輸出$後,再輸出一個空格,因此在上面的 Interrupt 結束後,實際上 uart_tx_buf 中還有一個空格,會和$相同的處理流程並且輸出,而到這邊就是全部由 UART TOP 到 Bottom 以及$如何到 Console 上了。

UART 讀取鍵盤

鍵盤為一特殊檔案(設備唯一特殊檔案),因此當我們要從鍵盤設備讀取檔案時,可以想像到我們會使用 read(),接著執行 syscall(),傳入 SYS_read 去執行相關的 System call,我們追蹤 sys_read()

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_write() 時,一樣是檢查完參數後,接著執行 fileread()

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

首先會檢查檔案是否能夠讀取,接著判斷其類別,如同 filewrite() 中做的檢查,我們在 UART TOP 中提到 chkmod() 建立的檔案類別為 FD_DEVICE,而在經過檢查後,會執行到 r = devsw[f->major].read(1, addr, n),通過 f->major 找到對應的設備,也就是 Console,並且找到對應的函式,這邊 read 對應到的就是在開機時,執行完 consoleinit() 後設置的 consoleread()

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

可以看到在這裡也有使用到 buffer,如同 write 中看到,這裡可以看到一個名為 con 的 buffer,我們可以看到他的結構定義於 console.c

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;

我們可以看到與 cons 有關的有以上,一個 buffer,三個指標,以及一個用於保護 buffer 的 lock。

這裡 Shell 會從 cons.buf 讀取資料,而鍵盤提供資料,因此 Shell 變成了消費者,鍵盤為生產者,將資料寫入到 cons.buf

這裡同樣也有 cons.r == cons.w 判斷 buffer 是否為空,也就是鍵盤沒有輸入資料,沒有資料就會進入到 sleep()。而假設某一段時間 user 輸入在鍵盤上輸入的字元,這個字元就會發送到 UART,產生 Interrupt 送到 PLIC,接著 PLIC 將 Interrupt 送到 CPU 進行處理,而這裡 CPU 處理 Interrupt 如同 UART TOP 所看見的,由於為 Externel Interrupt,因此會呼叫 devintr(),發現為 UART 的 Interrupt,接著呼叫 uartgetc(),然後到consoleintr()

這裡字元將一個一個從 con.buf 讀出,接著通過 either_copyout()cbuf 一個一個字元複製到 dst,成功操作之後 n--,表示我們成功讀取了字元,而最終 consoleread() 會回傳我們成功讀取的字元數量。

上方有一段程式碼片段

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

C()為 console 中巨集定義

// Console input and output, to the uart.
// Reads are line at a time.
// Implements special input characters:
//   newline -- end of line
//   control-h -- backspace
//   control-u -- kill line
//   control-d -- end of file
//   control-p -- print process list
//
#define C(x)  ((x)-'@')

如果我們輸入 Ctrl D,會回傳一個空字串 (長度為0),接著繼續執行剛剛沒有完成的工作 (Unix 系統中也是類似的行為),如同上方 if 判斷式所實現。

Interrupt 並行,生產/消費者問題 (producer-costumer)

External Interrupt 為 asynchronous Interrupt,也就是 CPU 和外部設備是並行執行的,例如當 UART 向 Console 發送字元時,CPU 先回到 Shell,而 Shell 可能會執行其他的 System call,向 buffer 寫入其他字元,也就是 buffer 不斷的同時被提取與寫入。生產者把生成的資料放到 buffer 中,消費者把資料從 buffer 中提取,這樣的並行問題稱為 生產/消費者。而背後的問題為以下

  • 生產者不能在 buffer 滿的時候寫入資料 (在上面看到 buffer 滿的時候會執行sleep())
  • 消費者不能在 buffer 為空時提取資料

而由於生產與消費是並行的,也就是對同一共享資料進行存取,因此,我們會需要一些機制來處理這樣的並行問題,並且保證上面所提及的問題不會發生。

而在 UART 中,Driver 的 Top 和 Bottom 是並行的,例如 Shell 在傳送完成 $之後接著傳送空格,這時候會到 UART 的 top 部分,使用uartputc()將空格加入到 buffer (uartputc() 為生產者)。但是在另一個 hart 中,可能會接收來自於 UART 的 Interrupt,接著就必須執行 UART 的 Bottom 去處理 Interrupt。存取相同的 buffer,這裡就會需要使用到 lock 等等機制,我們希望同一時間共享的 buffer 只能夠被一個 hart 所存取。

reference

SiFive FU540-C000 Manual v1p0
xv6-riscv
Operating System Concepts, 9/e
RISC-V xv6 Book
UART: A Hardware Communication Protocol Understanding Universal Asynchronous Receiver/Transmitter


上一篇
Day-20 UART Driver TOP
下一篇
Day-22 Race Condition, Spin Lock
系列文
與作業系統的第一類接觸 : 探索 xv631
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言