[xv6]:控制台中断
控制台Console是与用户进行交互的硬件设备,它接受用户的输入(如键盘输入),将其传递给内核和用户程序,进行相应的处理,然后再输出结果给用户(如输出到屏幕上)
首先,简单地看总体流程:
用户将会通过键盘键入一连串字符,通过连接到RISC-V上的UART串行端口(UART Serial-port)传输,控制台驱动程序将会顺利地接收这些输入
接控制台驱动程序处理其中的一些特殊字符(如BackSpace和Ctrl等),并不断累积这些输入字符,直到达到完整的一行(一般用户键入Enter表示一行的结束)
用户进程,例如shell,就会使用read从控制台中读取这些一行行的输入,然后由shell来具体处理它们
内核可以访问经内存映射的UART控制寄存器。RISC-V硬件将UART设备连接到事先约定好的物理地址上,对这些固定物理地址的读或写指令,相当于直接于硬件设备进行交互,而不是与RAM交互
UART经内存映射到从物理地址0x10000000开始的部分上,它有一小部分控制寄存器,每个1B大小, 当处理设备中断时需要使用到这些寄存器
kernel/uart.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30// the UART control registers are memory-mapped
// at address UART0. this macro returns the
// address of one of the registers.
// 用于寻址, 采用的基址 + 偏移量的方式
#define Reg(reg) ((volatile unsigned char *)(UART0 + reg))
// the UART control registers.
// some have different meanings for
// read vs write.
// see http://byterunner.com/16550.html
#define RHR 0 // receive holding register (for input bytes)
// 保持着UART接受的输入,等待着内核将其内容取走, 与read()相关
#define THR 0 // transmit holding register (for output bytes)
// 保持着内核的输入,等待着UART将其发送, 与write()相关
#define IER 1 // interrupt enable register
// 中断控制寄存器
#define IER_TX_ENABLE (1<<0)
#define IER_RX_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
1. 控制台读取用户输入流程
先给出整个流程,然后给出具体细节描述:
用户使用
read()系统调用从控制台读取输入,控制台等待用户输入, 此时会产生consoleread()进程用户键盘输入一个字符
xv6接收到中断,陷入traptrap handler发现是外部设备中断,设备是UART,调用uartintr()发现
RHR中有字符可读,调用consoleintr()将输入字符缓冲到
cons.buf中,如果读到’\n’或’ctrl+D’,说明用户输入满足一行,就唤醒consoleread()读出一整行的用户输入,拷贝到用户空间中
键入字符后陷入trap
用户键入字符后,UART硬件向RISCV抛出一个终端, 激活usertrap(), usertrap()对中断类型进行检查,调用devintr()判断是否为硬件中断
kernel/trap.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18int
devintr()
{
uint64 scause = r_scause();
if((scause & 0x8000000000000000L) &&
(scause & 0xff) == 9){
// this is a supervisor external interrupt, via PLIC.
// irq indicates which device interrupted.
// 从PLC硬件单元中询问哪个硬件设备中断了
int irq = plic_claim();
if(irq == UART0_IRQ)
// uart设备中断
uartintr();
// 后面的可以忽略...
}
发现为uart中断,调用uartintr()
kernel/uart.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17void
uartintr(void)
{
// read and process incoming characters.
while(1){
// 从RHR控制寄存器中读取一个字符
int c = uartgetc();
if(c == -1)
break;
consoleintr(c);
}
// send buffered characters.
acquire(&uart_tx_lock);
uartstart();
release(&uart_tx_lock);
}uartintr()内部会使用uartgetc()来从RHR控制寄存器中获取用户键入的字符1
2
3
4
5
6
7
8
9
10int
uartgetc(void)
{
if(ReadReg(LSR) & 0x01){
// input data is ready.
return ReadReg(RHR);
} else {
return -1;
}
}此时,已经确定了中断类型,会调用
consoleintr()进行控制台相关处理
调用consoleintr()
关于console, xv6使用了一个cons对象来对其进行维护
kernel/console.c
1
2
3
4
5
6
7
8
9
10struct {
struct spinlock lock;
// input
#define INPUT_BUF 128
char buf[INPUT_BUF]; // console缓冲区
uint r; // Read index
uint w; // Write index
uint e; // Edit index // 这里的索引可能会超过缓冲区大小,后面使用了取余操作
} cons;kernel/console.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52//! @param c 用户键入的字符
void
consoleintr(int c)
{
acquire(&cons.lock);
switch(c){
// ctrl + P, ctrl + U, ctrl + H
case C('P'): // Print process list.
procdump();
break;
case C('U'): // Kill line.
while(cons.e != cons.w &&
cons.buf[(cons.e-1) % INPUT_BUF] != '\n'){
cons.e--;
consputc(BACKSPACE);
}
break;
case C('H'): // Backspace
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;
// echo back to the user.
consputc(c);
// store for consumption by consoleread().
cons.buf[cons.e++ % INPUT_BUF] = c;
if(c == '\n' || c == C('D') || cons.e == cons.r+INPUT_BUF){
// wake up consoleread() if a whole line (or end-of-file)
// has arrived.
// 用户不断键入字符,直到用户输入换行符或者ctrl + d时
// 才会唤醒之前睡在cons.r的consoleread()进程
cons.w = cons.e;
//
wakeup(&cons.r);
}
}
break;
}
release(&cons.lock);
}在这里,默认情况下会对用户键入的字符进行回显,同时将其保存到
cons对象的缓冲区中如果用户输入了一些特殊字符(如ctrl + p等),那么就做特殊处理
当用户键入
\n回车符或者ctrl + d文件结束符时,就立即唤醒之前处于sleep状态并且睡在cons.r的consoleread()进程consoleread()进程在用户发起read()操作时就会产生, 这些进程并没有阻塞,而是使用了
schedule()让出了CPU
consoleread()进程读取控制台缓冲区内容到用户空间
kernel/console.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62/*! @brief 将console缓冲区的内容拷贝到sdt
*! @param user_dst 用于判断dst是用户地址还是内核地址
*/
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.
// 当console的read index与console 的write index相等
// 有两种情况:
// 1. 缓冲区没有字符可读
// 2. 缓冲区已经读完,但是该行仍未结束
while(cons.r == cons.w){
if(myproc()->killed){
release(&cons.lock);
return -1;
}
// 睡眠
// 这里并不会发生阻塞,而是进程的重新调度
sleep(&cons.r, &cons.lock);
}
// 一次性读取一个字符
c = cons.buf[cons.r++ % INPUT_BUF];
if(c == C('D')){ // end-of-file(十进制为4), 当键入ctrl + D时就会触发
if(n < target){
// Save ^D for next time, to make sure
// caller gets a 0-byte result.
// 读取到了文件结束符, 但是没有达到target
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().
// 行尾符, 此时整个read()操作结束
break;
}
}
release(&cons.lock);
return target - n;
}
2. 将用户数据发送到控制台流程
在发送过程中需要使用到的两个比较重要的寄存器:
THR: 如果用户想要将字符发送到控制台,就必须将字符写入到该寄存器当中,随后,会由硬件自动读取该寄存器中的值 如果THR寄存器中的值被硬件读出,就代表该字符真正的被控制台读取了,同时将会自动触发transmit complete 中断, 陷入
trap
LSR: 利用其中的第6个bit可以用来判断THR寄存器内部是否为空
xv6同样也为发送过程维护了一个缓冲区:
kernel/uart.c
1
2
3
4
5
6// the transmit output buffer.
struct spinlock uart_tx_lock;
#define UART_TX_BUF_SIZE 32
char uart_tx_buf[UART_TX_BUF_SIZE];
int uart_tx_w; // write next to uart_tx_buf[uart_tx_w++]
int uart_tx_r; // read next from uart_tx_buf[uar_tx_r++]
简要流程:
用户使用
write()系统调用, 最终将导向到UART驱动程序的top half, 结果为调用uarputc()uartputc()尝试将用户提供的字符写入到uart_tx_buf缓冲区如果缓冲区满的话,将会
sleep否则,调用
uartstart()uartstart()会尝试将缓冲区的字符写入到THR寄存器中如果
THR寄存器此时为空,则表明可以写入否则直接返回
位于
THR寄存器中的值被控制台读出,触发transmit complete中断,陷入trap在
devintr()中发现中断为uart设备中断,调用uartintr()uartintr()中经过判断发现是transmit complete中断,调用uartstart(), 陷入循环
可以看出uartputc()会尝试将用户提供的字符一次性全部写入缓冲区,然后再由
uartstart()一次中断写入一个字符到控制台
在这里就不给出代码分析了,但重要的一点是, 上面的整个过程都是异步的, uartstart()在发现THR寄存器非空会直接返回,
uartputc()发现缓冲区满会进行sleep, 让出CPU
在xv6中还提供了一个uartputc_sync(), 是uartputc()的同步版本,采用了轮询方式(poiling), 用于用户希望快速发出内容的情况
kernel/uart.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20void
uartputc_sync(int c)
{
push_off();
if(panicked){
for(;;)
;
}
// wait for Transmit Holding Empty to be set in LSR.
// 一直阻塞直到THR中的内容发出
// 然后马上将待发送字符写入THR中
while((ReadReg(LSR) & LSR_TX_IDLE) == 0)
;
WriteReg(THR, c);
pop_off();
}这里没有使用到发送缓冲区,而是直接尝试将内容写入到THR中, 如果
THR非空的话就会陷入阻塞
uart, 键盘, 显示器 ?
uart连接了两个键盘以及显示器(Console)两个设备,当通过键盘键入字符时,会将字符发送到uart的相关控制寄存器,然后寄存器的值会被显示器读取显示在屏幕上
3. uart相关并发
在xv6中,设备与CPU(进程)是并发运行的, 一个简单的理解就是UART在向Console发送字符时, Shell可能正同时向Console的缓冲区写入字符, 这被称为producer-consumer并行
通过代码可以看出,UART的输入以及输出缓冲区都是全局的,所有的CPU都会访问相同的输入输出缓冲区,因此在代码中使用了锁来保护共享的数据结构
**Top 与 Bottom **
通常情况下,大多数的设备驱动程序,都可以看成一个分上下部分的结构:顶部top half运行在内核空间中,通常由某一个进程的内核线程来运行,而底部bottom half则在中断产生时执行,大体上就是Interrupt handler
当内核希望与设备进行一些交互时,请求read、write等系统调用,驱动程序的top half就会被调用,top half会根据相应请求,让设备开始执行一些具体的操作(例如从磁盘上读一块);在相关操作完成后,设备就会产生中断,因此驱动程序的bottom half开始执行,它会查看设备完成的是什么工作,在适当的时候唤醒等待该工作的进程,同时让设备开始做新的工作
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!