繼續前面 UART TOP 的部分,下面將介紹 UART Bottom 的部分,也就是關於 Interrupt 的處理,並從 Interrupt 的處理中,切入到 UART 是如何讀取鍵盤輸入的,最後討論一個並行時所會發生的問題,當對同一個 buffer 進行讀取與寫入時所會產生出的 生產/消費者問題。
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.c
中 usertrap()
的部分 (因為 Shell 在 user mode 底下執行,因此我們看到 user mode 底下的 trap)
以下為 trap.c
中usertrap()
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 上了。
鍵盤為一特殊檔案(設備唯一特殊檔案),因此當我們要從鍵盤設備讀取檔案時,可以想像到我們會使用 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 判斷式所實現。
External Interrupt 為 asynchronous Interrupt,也就是 CPU 和外部設備是並行執行的,例如當 UART 向 Console 發送字元時,CPU 先回到 Shell,而 Shell 可能會執行其他的 System call,向 buffer 寫入其他字元,也就是 buffer 不斷的同時被提取與寫入。生產者把生成的資料放到 buffer 中,消費者把資料從 buffer 中提取,這樣的並行問題稱為 生產/消費者。而背後的問題為以下
sleep()
)而由於生產與消費是並行的,也就是對同一共享資料進行存取,因此,我們會需要一些機制來處理這樣的並行問題,並且保證上面所提及的問題不會發生。
而在 UART 中,Driver 的 Top 和 Bottom 是並行的,例如 Shell 在傳送完成 $
之後接著傳送空格,這時候會到 UART 的 top 部分,使用uartputc()
將空格加入到 buffer (uartputc()
為生產者)。但是在另一個 hart 中,可能會接收來自於 UART 的 Interrupt,接著就必須執行 UART 的 Bottom 去處理 Interrupt。存取相同的 buffer,這裡就會需要使用到 lock 等等機制,我們希望同一時間共享的 buffer 只能夠被一個 hart 所存取。
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