iT邦幫忙

2022 iThome 鐵人賽

DAY 21
0

前言

前面我們知道在 xv6 啟動後,Shell 會輸出 $ 到 Console 上,而我們可以追蹤產生 $ 的行為了解 UART 的運作,以及介紹一下 Driver 中,Top 的部分是如何封裝給其他 Process 進行呼叫的。

Console

先看到 init.c 中的 main(),在系統啟動之後會執行的第一個 Process

int
main(void)
{
  int pid, wpid;

  if(open("console", O_RDWR) < 0){
    mknod("console", CONSOLE, 0);
    open("console", O_RDWR);
  }
  dup(0);  // stdout
  dup(0);  // stderr

  for(;;){
    printf("init: starting sh\n");
    pid = fork();
    if(pid < 0){
      printf("init: fork failed\n");
      exit(1);
    }
    if(pid == 0){
      exec("sh", argv);
      printf("init: exec sh failed\n");
      exit(1);
    }

    for(;;){
      // this call to wait() returns if the shell exits,
      // or if a parentless process exits.
      wpid = wait((int *) 0);
      if(wpid == pid){
        // the shell exited; restart it.
        break;
      } else if(wpid < 0){
        printf("init: wait returned an error\n");
        exit(1);
      } else {
        // it was a parentless process; do nothing.
      }
    }
  }
}

可以看到這邊會打開 Console,接著使用 System call mknod() 建立裝置檔案 (裝置為 CONSOLE 類型為 FD_DEVICE)。Console 對應到的 file director 為 0,接著使用 dup() 將 file director 1 和 2 也連接到 Console 上,因此 file director 0, 1, 2 皆連接到 Console 上。

接著使用 fork() 產生出子 process 後子 process 變成 sh。接著我們跟蹤進入 sh.c

int
main(void)
{
  static char buf[100];
  int fd;

  // Ensure that three file descriptors are open.
  while((fd = open("console", O_RDWR)) >= 0){
    if(fd >= 3){
      close(fd);
      break;
    }
  }

  // Read and run input commands.
  while(getcmd(buf, sizeof(buf)) >= 0){
    if(buf[0] == 'c' && buf[1] == 'd' && buf[2] == ' '){
      // Chdir must be called by the parent, not the child.
      buf[strlen(buf)-1] = 0;  // chop \n
      if(chdir(buf+3) < 0)
        fprintf(2, "cannot cd %s\n", buf+3);
      continue;
    }
    if(fork1() == 0)
      runcmd(parsecmd(buf));
    wait(0);
  }
  exit(0);
}

確認三個 file descriptor 皆連接到對應設備後,會執行 getcmd() 無限迴圈,而我們跟蹤進入 getcmd()

int
getcmd(char *buf, int nbuf)
{
  write(2, "$ ", 2);
  memset(buf, 0, nbuf);
  gets(buf, nbuf);
  if(buf[0] == 0) // EOF
    return -1;
  return 0;
}

可以看到這邊朝向 file director 2 指向的裝置寫入 $ 符號,而 file director 2 在 init.c 中的 main() 通過 dup() 連接到了 Console 上,因此 $ 符號被寫入到 Console 上。

這邊可以看到我們通過 write()$ 寫入到 Console 上,對於 sh.c 來說 write() 就是一個 System call,向 file director 2 指向的裝置進行寫入,但其實背後寫入到的設備為 UART,但sh.c並不知道 file director 2 連接到的是什麼裝置,而這也就是 UART TOP 部分所提供的封裝。

而我們可以跟蹤進入 write(),先前我們知道 write() 是將 SYS_WRITE 巨集對應到的數字傳到 syscall() 中,接著呼叫 sys_write(),因此我們可以檢視 sys_write() 檢視其行為 (已經 trap 了,這時候我們在 supervisor mode 底下),位於 sysfile.c 中。

uint64
sys_write(void)
{
  struct file *f;
  int n;
  uint64 p;
  
  argaddr(1, &p);
  argint(2, &n);
  if(argfd(0, 0, &f) < 0)
    return -1;

  return filewrite(f, p, n);
}

首先會對參數進行些檢查,接著執行 filewrite()

int
filewrite(struct file *f, uint64 addr, int n)
{
  int r, ret = 0;

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

  if(f->type == FD_PIPE){
    ret = pipewrite(f->pipe, addr, n);
  } else if(f->type == FD_DEVICE){
    if(f->major < 0 || f->major >= NDEV || !devsw[f->major].write)
      return -1;
    ret = devsw[f->major].write(1, addr, n);
  } else if(f->type == FD_INODE){
    // write a few blocks at a time to avoid exceeding
    // the maximum log transaction size, including
    // i-node, indirect block, allocation blocks,
    // and 2 blocks of slop for non-aligned writes.
    // this really belongs lower down, since writei()
    // might be writing a device like the console.
    int max = ((MAXOPBLOCKS-1-1-2) / 2) * BSIZE;
    int i = 0;
    while(i < n){
      int n1 = n - i;
      if(n1 > max)
        n1 = max;

      begin_op();
      ilock(f->ip);
      if ((r = writei(f->ip, 1, addr + i, f->off, n1)) > 0)
        f->off += r;
      iunlock(f->ip);
      end_op();

      if(r != n1){
        // error from writei
        break;
      }
      i += r;
    }
    ret = (i == n ? n : -1);
  } else {
    panic("filewrite");
  }

  return ret;
}

首先會先判斷這一個裝置是可以寫入的,接著會判斷 file director,而在 file 結構中可以看到列舉了許多種類型,包含 FD_NONE, FD_PIPE, FD_INODE, FD_DEVICE,而 System call 產生的類型為 FD_DEVICE,因此會進入到 ret = devsw[f->major].write(1, addr, n) 的部分,而我們可以看到 devsw 為一個結構

struct devsw {
  int (*read)(int, uint64, int);
  int (*write)(int, uint64, int);
};

devsw 為一結構,兩個成員皆為 function pointer,指向到對應裝置的 read 和 write 部分,這裡通過 devsw[f->major] 得到對應裝置資訊,f->major 意義為 FD_DEVICE 所對應到的裝置,而 FD_DEVICE 對應到的裝置即為 CONSOLE,而 CONSOLE 的 write 和 read 初始化在開機的時候,於 main.cmain() 第一行 consoleinit() 完成了 CONSOLE 的 write 和 read 的 function pointer 設置。

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

因此這邊 ret = devsw[f->major].write(1, addr, n) 對應到的為 consolewrite(),在 filewrite 通過 return ret 進入。而 consolewrite() 位於 console.c 中,這邊可以看到,對於 filewrite 來說,就是通過 file 結構找到裝置,並且呼叫對應裝置的 write 函式,概念上來說,就是 UART TOP 部分提供的 API 封裝,filewrite 使用其 API 進行呼叫。

int
consolewrite(int user_src, uint64 src, int n)
{
  int i;

  for(i = 0; i < n; i++){
    char c;
    if(either_copyin(&c, user_src, src+i, 1) == -1)
      break;
    uartputc(c);
  }

  return i;
}

這裡 consolewrite() 使用 either_copyin 把字元複製到 c 中,接著呼叫 uartputc() 將 c 寫入到 UART 中。而到這裡我們就實際進入到了 UART 內部了。

uartputc() 將字元加入到 uart_tx_buf 接著呼叫 uartstart()

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

可以看到在 uartputc() 中有一個 buffer 為 uart_tx_buf,這是一個用於輸出的 buffer,資料結構上類似於 queue,具有 FIFO 的特性,而這個 buffer 上有兩個指標,分別為 uart_tx_w 和 uart_tx_r 用於判斷 uart_tx_buf 的狀態,是滿的,還是空的。

Buffer status

當 uart_tx_w 和 uart_tx_r 重疊時,表示目前 buffer 為空的

而當我們要寫入時,可以發現有一個對 UART_TX_BUF_SIZE 取餘數的操作,判斷這可能是類似於環狀 queue 的結構

而當 buffer 已經滿了

目前 index r = 5, index w = 13,而可以推出滿的條件為 uart_tx_w == uart_tx_r + UART_TX_BUF_SIZE,如果 buffer 滿了,則會先進入 sleep() 將 CPU 資源給其他 process 使用。

$ 為我們寫入 buffer 的第一個字元,會成功的寫入到 buffer,接著移動寫入指標,接著進入 uartstart() 中。

uartstart() 如果目標處於準備中,則傳送 uart_tx_buf 的內容

void
uartstart()
{
  while(1){
    if(uart_tx_w == uart_tx_r){
      // transmit buffer is empty.
      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);
  }
}

上面是對 UART 的一些暫存器操作,而我們可以看到 UART 的 Register map

<TECHNICAL DATA ON 16550>
可以在 uart.c 中看到相關的暫存器,我們的 UART 為一個代號 16550 的芯片。

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

UART0 位於物理記憶體地址 0x10000000,而這個地址上的固定偏移量會有一些暫存器,諸如 THR, IER 等等,取得這一些暫存器可以通過 UART0 + offset 這種方式取得。

我們會使用兩種狀態去存取這一些暫存器,狀態不同時,暫存器內的值意義也不同,一種狀態是 storing,另外一種為 loading,以圖表示可以看做是以下

  • receive
    • THR (Transmit Hold Register): 接收傳送的資料,像是要傳到 Console 的資料
    • IER (Interrupt Enable Register): 控制 receiver transmitter 的 Interrupt
    • FCR (FIFO Control Register): 用於啟用 FIFO,清除 FIFO 等等
    • LCR (Line Control Register): 設定傳輸速率
  • transmit
    • RHR (Receive Hold Register): 接收輸入,像是來自鍵盤的輸入
    • ISR (Interrupt Status Register): Interrupt 狀態與優先級
    • LSR (Line Status Register) :傳送到 CPU 的資料狀態,像是資料是否為空,是否錯誤校驗等等
      如果我們向 UART0 + 0 的地方 storing 資料,對於 storing 的意義就是 THR,如果是 loading 資料,對於 loading 的意義就是 RHR。

一開始會去檢查 buffer 是否為空,為空表示沒有資料需要傳送,因此直接 return。

接著會去讀取 LSR 暫存器的 LSR_TX_IDLE 域,檢查目前設備是否處於空閒狀態 (IDLE),如果處於閒置 (準備接受資料),則從 uart_tx_buf 中讀取資料,然後將資料寫入到 THR (Transmit Hold Register, use to output),而當資料到達目標設備時,整個 System call 就會結束並返回了。user space 底下的 shell 就可以繼續執行。

上面這一些就是 UART Driver TOP 的部分了,而當 UART 將資料朝向設備送出時,在某個時間點我們會需要處理 UART 所產生的 Interrupt (在 Exception vs Interrupt, overview driver 中 UART 已經可以產生 Interrupt,並且 CPU 需要處理該 Interrupt),而 Interrupt 的處理將在 UART Driver Bottom 中介紹。

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-19 Page Fault Lazy Page Allocation Implementation
下一篇
Day-21 UART Driver Bottom
系列文
與作業系統的第一類接觸 : 探索 xv631
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言