Trap和系统调用
中断和设备驱动
驱动是操作系统用于管理特定设备的代码:它配置设备硬件,通知设备执行操作,处理返回的中断,并且与可能在该设备上进行I/O等待的进程交互。编写驱动代码可能很棘手,因为驱动与它管理的设备是并行执行的,此外,驱动必须理解设备硬件接口,这可能是复杂的并且缺乏文档。
需要被系统注意的设备通常会被配置以生成中断(中断是一种trap),当一个设备发起了一个中断,内核的trap处理代码识别它并调用设备的中断处理器。在xv6中,这个调度发生在devintr
(kernel/trap.c:177)
void
kerneltrap()
{
int which_dev = 0;
uint64 sepc = r_sepc();
uint64 sstatus = r_sstatus();
uint64 scause = r_scause();
// 注意这里!!!!!
if((which_dev = devintr()) == 0){
printf("scause %p\n", scause);
printf("sepc=%p stval=%p\n", r_sepc(), r_stval());
panic("kerneltrap");
}
// ...
}
很多设备驱动都在两个上下文中执行代码:上半部分运行在进程的内核线程中,下半部分在中断时执行。上半部分通过如read
和write
的系统调用被使用,表明我们想要设备执行I/O。这个代码可能请求硬件取执行一个操作(比如请求磁盘去读或写),然后,代码等待操作完成,最终,设备完成了操作,发起一个中断,此时,下半部分,也就是设备的中断处理器,认出是什么操作完成了,如果合适的话,唤醒一个等待的进程,并告诉硬件开始任何在等待中的后续操作。
5.1. 代码:Console输入
Console驱动(console.c)是一个很简单的驱动结构,console驱动通过RISC-V上的UART串口硬件接收人类输入的字符,驱动一次性积累一行输入,处理特殊输入字符(如backspace和control-u),像shell这样的用户进程,使用read
系统调用去从console中获取输入行。当你向在QEMU中的xv6输入时,你的击键通过QEMU模拟的UART硬件传送到xv6中。
驱动对面的UART硬件是一个QEMU模拟的16550芯片,在一个真实的计算机上,一个16550可能管理连接到一个终端或其它计算机的RS232串行链路。当我们运行QEMU时,它连接到你的键盘和显示器。
在软件看来,UART硬件只是一组内存映射的控制寄存器,也就是说,有一些RISC-V硬件连接到UART设备上的物理地址,所以load和store操作实际上是在与设备交互,而不是与RAM交互。UART的内存映射地址起始于0x10000000
,或者说UART0
(kernel/memlayout.h:21)。有一些实用的UART控制寄存器,每一个都是一字节宽,它们与UART0
的偏移量被定义在(kernel/uart.c:22)。举个例子,LSR
寄存器包含了用于表示是否输入字符正在等待被软件读取的位。这些字符(如果有)可以通过读取RHR
寄存器获得。每次一个字符被读取了,UART硬件将它从内部的等待字符FIFO队列中删除,当FIFO是空的时,清除LSR
中的“ready”位。UART的传输硬件与接收硬件很大程度上是独立的,如果软件写入一个字节到THR
中,UART传输这个字节。
xv6的main
调用consoleinit
(kernel/console.c:184)以初始化UART硬件,这个代码配置UART当它接收到每一个字节的输入时,生成一个接收中断,以及每次UART完成发送一个输出字节时,生成一个传输完成中断(kernel/uart.c:53)。
void
consoleinit(void)
{
initlock(&cons.lock, "cons");
uartinit(); // 设置uart芯片
// 将console设备上的read和write调用连接到consoleread和consolewrite函数
// 这就是上面所说的驱动程序的上半部分,包含了两个读写接口,当read和write系统调用
// 发生时,如果fd是一个硬件设备,则会调用这两个读写接口,稍后我们会看到
devsw[CONSOLE].read = consoleread;
devsw[CONSOLE].write = consolewrite;
}
void
uartinit(void)
{
// 先关闭中断
WriteReg(IER, 0x00);
// 打开设置波特率的特殊模式
WriteReg(LCR, LCR_BAUD_LATCH);
// 设置LSB波特率
WriteReg(0, 0x03);
// 设置MSB波特率
WriteReg(1, 0x00);
// 跳出设置波特率模式,设置字长为8比特
WriteReg(LCR, LCR_EIGHT_BITS);
// 重置、开启FIFO
WriteReg(FCR, FCR_FIFO_ENABLE | FCR_FIFO_CLEAR);
// 开启传送(TX)和接收(RX)中断
WriteReg(IER, IER_TX_ENABLE | IER_RX_ENABLE);
initlock(&uart_tx_lock, "uart");
}
xv6的shell通过一个由init.c
打开的文件描述符来读取console(user/init.c:19),read
系统调用通过内核的consoleread
(kernel/console.c:82)来完成工作,consoleread
等待输入到达(通过中断)并被缓冲在cons.buf
中,将输入复制到用户空间,并且(在一整行到达后)返回到用户进程。如果用户尚未输入整行,任何读取进程都将在sleep
调用上等待(kernel/console.c:98)(第七章会解释sleep
的细节)。
int
main(void)
{
// 用户进程初始化时创建console设备
// 现在有了三个文件描述符 stdin=>0 stdout=>1 stderr=>2
int pid, wpid;
if(open("console", O_RDWR) < 0){
mknod("console", CONSOLE, 0);
open("console", O_RDWR);
}
dup(0); // stdout
dup(0); // stderr
// ...
}
// sys_read调用的函数,在kernel/file.c:106
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;
// 调用对应设备驱动程序的read函数,也就是我们在console.c中设置的consoleread
r = devsw[f->major].read(1, addr, n);
// 如果文件类型是inode
} else if(f->type == FD_INODE){
// ...
}
int
consoleread(int user_dst, uint64 dst, int n)
{
uint target;
int c;
char cbuf;
target = n;
acquire(&cons.lock);
// n代表要读的数量,如果还有要读的数据
while(n > 0){
// 等待中断处理器添加一些输入到cons.buffer
// cons是一个环形队列,cons.r是读指针,cons.w是写指针,cons.r==cons.w代表队列中没数据
while(cons.r == cons.w){
if(myproc()->killed){
release(&cons.lock);
return -1;
}
// 没有数据就在cons.r上等待
sleep(&cons.r, &cons.lock);
}
// 有数据了,读取字符c,并向前推进读指针
c = cons.buf[cons.r++ % INPUT_BUF];
// 如果读到的字符是eof (Control-D)
if(c == C('D')){
if(n < target){ // n < target代表已经读取了一些数据,但还没到用户要求的数量
// 回退读指针,确保下次用户调用时能获得一个0字节的结果(我理解是确保用户下次调用时能看见已经eof了)
cons.r--;
}
// 跳出循环
break;
}
// 将输入字节拷贝到用户空间的缓冲区中
cbuf = c;
if(either_copyout(user_dst, dst, &cbuf, 1) == -1)
break;
// 更新必要的变量
dst++;
--n;
// 一行数据收集完了,返回到用户层面的read函数
if(c == '\n'){
break;
}
}
release(&cons.lock);
// 返回本次读取的字符数,target - n
return target - n;
}
译者:嘶,对读取的理解有点加深了,之前在写rust网络编程的时候一直搞不懂为什么读取返回的长度是0时代表已经读到EOF,因为如果没到EOF并且当前数据没有到达时(比如网络连接没断开,只是包还没接收到)时,read的行为是阻塞直到数据到达,而只有在当前已经无法继续读取,上次读取返回时已经触碰文件尾部时才会立即返回一个长度0(没读取时target-n为0)。
当用户输入一个字符,UART通知RISC-V发起一个中断,激活xv6的陷阱处理程序。陷阱处理程序调用devintr
(kernel/trap.c:177),它查看RISC-V的scause
寄存器发现中断是来自外部设备的,然后它要求一个被称作PLIC的硬件单元来告诉它哪一个设备中断了(kernel/trap.c:186),如果是UART,devintr
就会调用uartintr
。
void
usertrap(void)
{
int which_dev = 0;
// ...
// 调用devintr,判断当前trap是否是设备中断,如果是的话,返回中断设备号
} else if((which_dev = devintr()) != 0){
// ok
}
// ...
int
devintr()
{
// 读取scause确定trap是来自外部设备的中断
uint64 scause = r_scause();
if((scause & 0x8000000000000000L) &&
(scause & 0xff) == 9){
// irq代表哪一个设备的中断
int irq = plic_claim();
// 如果是UART0_IRQ,调用uartintr
if(irq == UART0_IRQ){
uartintr();
// 如果是VIRTIO0_IRQ,调用virtio_disk_intr
} else if(irq == VIRTIO0_IRQ){
virtio_disk_intr();
} else if(irq){
printf("unexpected interrupt irq=%d\n", irq);
}
// ...
}
void
uartintr(void)
{
// 不断读取并处理进来的字符
while(1){
// 从uart芯片中读取字符
int c = uartgetc();
if(c == -1)// 如果没有字符可读了
break;
// 将字符交由consoleintr处理
consoleintr(c);
}
// send buffered characters.
acquire(&uart_tx_lock);
uartstart();
release(&uart_tx_lock);
}
int
uartgetc(void)
{
// 如果LSR寄存器的值表明有数据等待读取
if(ReadReg(LSR) & 0x01){
// 从RHR中读取字符
return ReadReg(RHR);
} else {
// 当无字符可读取,返回-1
return -1;
}
}
uartintr
(kernel/uart.c:180)从UART硬件中读取任何等待中的输入字符,并且将它们移交给consoleintr
(kernel/console.c:138)。它不等待字符,因为未来的输入会发起一个新的中断,consoleintr
的工作就是在cons.buf
中积累字符直到完整的一行到达。consoleintr
会特殊对待少量字符,比如backspace。当一个新行到达,consoleintr
唤醒等待中的consoleread
(如果有的话)。
void
consoleintr(int c)
{
acquire(&cons.lock);
switch(c){ // 处理特殊字符
case C('P'): // Ctrl-P,打印进程列表
procdump();
break;
case C('U'): // Ctrl-U,清除该行?
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;
// 正常情况,处理该字符,回显并保存到cons.buf,检测是否已经累积了整行
default:
if(c != 0 && cons.e-cons.r < INPUT_BUF){
c = (c == '\r') ? '\n' : c;
// 通过consputc回显给用户
consputc(c);
// 保存到cons.buf
cons.buf[cons.e++ % INPUT_BUF] = c;
// 如果一行到达,唤醒在cons.r上等待的consoleread
if(c == '\n' || c == C('D') || cons.e == cons.r+INPUT_BUF){
cons.w = cons.e;
wakeup(&cons.r);
}
}
break;
}
release(&cons.lock);
}
一旦被唤醒,consoleread
将会观察cons.buf
中的整行,将它拷贝到用户空间,然后(通过系统调用机制)返回到用户空间。