5.1: Code: Console input
知识点介绍
控制台驱动程序通过连接到 RISC-V 的 UART串口硬件接受由用户键入的字符。该驱动程序一次累积一行输入,处理特殊的输入字符,例如退格键(backspace)和 Control-U。
用户进程(如 shell)通过 read 系统调用
从控制台中获取输入行。当您在 QEMU 中向 xv6 输入内容时,您的按键通过 QEMU 的模拟 UART 硬件传递给 xv6。
驱动程序交互的 UART 硬件是 QEMU 模拟的 16550 芯片 [13]。在真实的计算机中,16550 芯片会管理一个 RS232 串行连接,与终端或其他计算机通信。在 QEMU 中,16550 模拟器连接到您的键盘和显示器。
UART 硬件对软件的呈现方式是一组内存映射的控制寄存器。这意味着 RISC-V 硬件将某些物理地址连接到 UART 设备,因此对这些地址的加载和存储操作会与设备硬件交互,而不是与 RAM 交互。UART 的内存映射地址从 0x10000000 开始,对应 UART0(定义在 kernel/memlayout.h:21 中)。
// qemu puts UART registers here in physical memory.
#define UART0 0x10000000L
#define UART0_IRQ 10
// virtio mmio interface
#define VIRTIO0 0x10001000
#define VIRTIO0_IRQ 1
UART 有一小部分控制寄存器,每个寄存器的宽度为一个字节,它们相对于 UART0 的偏移量定义在 kernel/uart.c:22 中。
// 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)
#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
// qemu puts UART registers here in physical memory.
#define UART0 0x10000000L
#define UART0_IRQ 10
// virtio mmio interface
#define VIRTIO0 0x10001000
#define VIRTIO0_IRQ 1
// 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)))
#define ReadReg(reg) (*(Reg(reg)))
#define WriteReg(reg, v) (*(Reg(reg)) = (v))
例如,LSR 寄存器包含的位表示是否有输入字符等待软件读取。这些字符(如果有)可以从 RHR 寄存器中读取。每次读取一个字符时,UART 硬件会将其从内部的 FIFO 等待队列中删除,并在 FIFO 为空时清除 LSR 中的“就绪”位。UART 的发送硬件与接收硬件几乎独立;如果软件向 THR 寄存器写入一个字节,UART 会发送该字节。
Xv6 的 main 函数通过调用 consoleinit(kernel/console.c:182)来初始化 UART read/out硬件。
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;
}
这段代码将UART配置为在接收每个输入字节时生成接收中断,以及在每次完成发送一个输出字节时生成发送完成中断(kernel/uart.c:53)。
void
uartinit(void)
{
// disable interrupts.
WriteReg(IER, 0x00);
// special mode to set baud rate.
WriteReg(LCR, LCR_BAUD_LATCH);
// LSB for baud rate of 38.4K.
WriteReg(0, 0x03);
// MSB for baud rate of 38.4K.
WriteReg(1, 0x00);
// leave set-baud mode,
// and set word length to 8 bits, no parity.
WriteReg(LCR, LCR_EIGHT_BITS);
// reset and enable FIFOs.
WriteReg(FCR, FCR_FIFO_ENABLE | FCR_FIFO_CLEAR);
// enable transmit and receive interrupts.
WriteReg(IER, IER_TX_ENABLE | IER_RX_ENABLE);
initlock(&uart_tx_lock, "uart");
}
Xv6 的 shell 通过 init.c(user/init.c:19)打开的文件描述符从控制台读取输入。
int
main(void)
{
int pid, wpid;
if(open("console", O_RDWR) < 0){
mknod("console", CONSOLE, 0);
open("console", O_RDWR);
}
对 read 系统调用的请求会穿过内核到达 consoleread(kernel/console.c:80)。
// user read()s from the console go here.
// copy (up to) a whole input line to dst.
// user_dist indicates whether dst is a user
// or kernel address.
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;
}
96: 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;
}
consoleread 等待输入通过中断到达并被缓冲到 cons.buf 中,然后将输入复制到用户空间,并在一整行到达后返回用户进程。如果用户尚未键入完整的一行,任何读取的进程将会在调用 sleep(kernel/console.c:96)时进入等待状态(第 7 章详细解释了 sleep 的机制)。
当用户键入一个字符时,UART 硬件会请求 RISC-V 触发一个中断,从而激活 xv6 的中断处理程序。中断处理程序调用 devintr(kernel/trap.c:185:作用参照上一章intro)后者通过检查 RISC-V 的 scause 寄存器发现中断来自外部设备。接着,它请求名为 PLIC [3] 的硬件单元指示具体是哪个设备触发了中断(kernel/trap.c:193;参照上一章intro的devintr)。如果是 UART,devintr 会调用 uartintr。
uartintr(kernel/uart.c:177)从 UART 硬件中读取所有等待的输入字符,并将它们交给 consoleintr(kernel/console.c:136)。
// handle a uart interrupt, raised because input has
// arrived, or the uart is ready for more output, or
// both. called from devintr().
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);
}
consoleintr
// the console input interrupt handler.
// uartintr() calls this for input character.
// 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('P'): // Print process list.
procdump();
break;
case C('U'): // Kill line.
while(cons.e != cons.w &&
cons.buf[(cons.e-1) % INPUT_BUF_SIZE] != '\n'){
cons.e--;
consputc(BACKSPACE);
}
break;
case C('H'): // Backspace
case '\x7f': // Delete key
if(cons.e != cons.w){
cons.e--;
consputc(BACKSPACE);
}
break;
default:
if(c != 0 && cons.e-cons.r < INPUT_BUF_SIZE){
c = (c == '\r') ? '\n' : c;
// echo back to the user.
consputc(c);
// store for consumption by consoleread().
cons.buf[cons.e++ % INPUT_BUF_SIZE] = c;
if(c == '\n' || c == C('D') || cons.e-cons.r == INPUT_BUF_SIZE){
// wake up consoleread() if a whole line (or end-of-file)
// has arrived.
cons.w = cons.e;
wakeup(&cons.r);
}
}
break;
}
release(&cons.lock);
}
uartintr 不会等待字符输入,因为未来的输入会触发新的中断。consoleintr 的任务是将输入字符累积到cons.buf 中,直到一整行到达为止。consoleintr 会特别处理退格键和其他几个特殊字符。当收到换行符时,consoleintr会唤醒一个正在等待的 consoleread(如果有)。一旦被唤醒,consoleread会观察到cons.buf中有完整的一行内容,将其复制到用户空间,并通过系统调用机制返回到用户空间。
相关问题
uart的寄存器和物理内存地址的映射如何实现,通过寄存器的引脚和总线然后呢 具体步骤如下:
数据传输到寄存器: UART 接收端的数据通过 RXD 引脚接收,存储到数据寄存器(DR)。 处理器通过总线读取 DR 寄存器,获取接收到的数据。 数据从寄存器传输: 处理器通过总线写入数据到 DR 寄存器。 UART 硬件从 DR 寄存器读取数据,并通过 TXD 引脚发送。
总线是计算机中的bus吗,通过总线实现cpu和内存,硬盘以及其他外围设备的相互访问? 是的,总线(Bus)是计算机系统中的一种通信机制,负责在计算机内部不同组件之间传递数据、地址和控制信号。通过总线,CPU、内存、硬盘以及其他外围设备可以实现相互访问和协作。以下是总线的基本概念和作用
总线是计算机中 CPU、内存、硬盘、外设之间通信的关键桥梁。通过数据总线、地址总线和控制总线的协同作用,实现了设备之间的互联和协作。随着计算机体系结构的演化,总线也从单一共享模式发展为分层架构和高速点对点连接,以满足现代系统对性能和扩展性的需求。
基于上述分析,一个现代 CPU(如 Intel 或 AMD 的主流桌面 CPU)通常需要:
内存总线:100~200 个引脚。 PCIe 总线:64~80 个引脚。 外设总线:30~50 个引脚。 电源和地:300~600 个引脚。 其他专用接口:50~100 个引脚。 总计:现代 CPU 的引脚数量通常在 800 到 1500 个 之间(如 LGA1200、AM5 等插槽)。
一般进程与外设交互的过程
系统调用与进程状态保存 当一个进程发起设备操作请求(如读取硬盘数据)时:
系统调用进入内核模式:
进程通过系统调用(如 read())向内核发起 I/O 请求。 内核记录调用进程的状态(如寄存器、程序计数器、用户态栈等)。 内核注册请求:
内核将设备操作请求发送给设备驱动程序,并在设备队列中挂起该请求。 内核保存了调用进程的上下文信息(如 PCB 中的 I/O 请求描述)。 进程挂起(yield):
发起 I/O 请求的进程被阻塞,进入等待队列,释放 CPU 时间片。 其他进程继续运行。
进程缓冲区的概念 进程缓冲区是指进程在用户空间中用于临时存储数据的内存区域,通常由程序显式分配或操作系统自动分配。这些缓冲区用于以下场景:
与外围设备交互:
存储从硬盘、键盘、网络等设备读取的数据。 准备要发送到设备的数据。 减少频繁的数据交换:
提高 CPU 和 I/O 设备之间的数据传输效率,避免频繁切换内核态和用户态。 支持批处理: 在执行 I/O 操作时,通过缓冲区分批处理数据,减少系统调用的开销。 等待 I/O 的进程状态记录在内核的进程控制块(PCB)中。