MIT_6.S081_xv6.Information 4:Interrupt&Device Manage
于2022年3月23日2022年3月23日由Sukuna发布
设备管理
一个设备驱动程序就是操作系统对特定的设备进行管理的程序,这些程序让设备执行操作,并且处理设备引起的中断,并且与因为设备I/O而被阻塞的进程.设备驱动程序往往非常难设计,因为设备和设备驱动程序是一起工作的,而且编写设备驱动程序需要对硬件接口有着深入的了解,这一点往往非常难.
设备会通过引发中断来通知操作系统进行处理,在中断的那一部分我们说过,操作系统通过识别中断来源来判断这是个设备中断,然后调用设备中断处理程序.其中函数会调用devintr
这个函数来获取究竟是什么设备发生了中断.
许多设备中断的程序一般分成两个部分,第一个部分在进程的内核态执行,一般来说用户程序会执行read和write调用以希望从设备中获取一些信息.这一部分的内容可能负责把用户的请求传送给设备,让设备执行用户的请求.第二个部分就是设备中断处理,这一部分一般是设备处理完用户的请求,处理完之后设备会向操作系统发送一个中断请求,这一部分的代码就是用来处理设备的中断请求的,把结果传递给用户程序并唤醒相关的进程.
控制台输入
关于控制台,关于控制台的一些代码存放到了console.c
这个文件中,控制台驱动程序可以接受用户输入的字符,通过UART这个特殊的硬件.控制台驱动程序一次性获得一行输入,用户进程,比如说shell程序会通过read这个系统调用来获得控制台输入.综合起来就是
QEMU模拟的UART硬件->操作系统的内核->用户程序的read系统调用.
在实际的电脑中,16550芯片会管理RS232这个串行链路来连接到其他终端,在QEMU中,这个模拟的芯片连接你的键盘和屏幕.
对于操作系统(软件)来说:我们可以像访问内存一样来访问UART硬件,在之前内存管理的时候我们已经提到了,我们可以通过访问UART0这个地址来像访问内存一样来访问设备.在UART设备中存储了许多寄存器数据,操作系统可以通过UART0地址 偏移来访问寄存器数据.
代码语言:javascript复制#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
首先xv6的S态的main函数会调用consoleinit
函数.这个函数会初始化UART硬件.
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;
}
uartinit()的代码保证了UART在收到每一次键盘输入的时候都会引发中断,然后每一次传输完一整个字符还会送出一个trasmit complete中断.
代码语言:javascript复制WriteReg(IER, IER_TX_ENABLE | IER_RX_ENABLE);
接着UART硬件也会引发一个中断,trap函数会判断这是什么类型中断,发现是设备引起的中断,就转而调用处理设备中断的函数devintr
,接着这个函数通过调用PLIC
判断是什么设备引起的中断,发现是UART设备,转而调用uartintr
.
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=%dn", 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;
}
}
//中断处理分成两个部分,前面部分把存储在UART寄存器的键盘输入发送.
void
uartintr(void)
{
//keyborad->RHR
// 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);
}
接着从uartintr函数中从UART寄存器中获取一个字符,再把字符递交给consoleintr函数.
代码语言:javascript复制int
uartgetc(void)
{
if(ReadReg(LSR) & 0x01){
// input data is ready.
return ReadReg(RHR);
} else {
return -1;
}
}
就是从LSR寄存器判断,然后从RHR寄存器获得数据.
consoleintr
负责把所有UART输入的元素存储起来,存储到cons.buf
这个数组中,然后当输入的是换行,就可以唤醒一个正在运行consoleread
的进程.这个进程会执行consoleread
函数,consoleread
函数会读取缓冲区内的数据,然后返回给用户态.
每一次唤醒,consoleread
就是读取一行的元素,然后把数据传递给用户态.
总结:用户键盘输入->中断一次->UART把中断的输入读取出来送到consoleintr->consointr调用consoleread函数
控制台输出
read()系统调用能获得用户的键盘输入,write()系统调用可以在控制器中进行输出.
UART设备每一次从THR寄存器中输出一字节的数据,它就会产生一个中断,和之前一样,uartintr
会调用uartstart
函数.
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);
}
}
每一次输出一字节的数据都需要看看在缓冲区内有没有其他的数据需要去输出.这个函数就是检查缓冲区内还有没有数据要写,如果要写,就放到THR寄存器中等待去写.
然后机器就会从THR寄存器中读取要输出的内容,输出成功就触发中断,看看还有没有要要写的内容,有的话就接着放入THR寄存器中.
特别地,第一个字节会在uartputc
这个系统调用中进行输出.其他的字节是通过字节
设备驱动的并行性
你会发现,每一次进入consoleread
和consoleintr
都会获取一个锁,这个锁会保证不可能同时有两个进程执行这个函数,当两个进程同时执行consoleread
的时候,有可能会把一整句话分成两部分交付给两个进程,这个是不对的.加上锁可以保证同时只有一个进程进入这个函数
cd abab
process1 :c a
process2 :dabb
还有一个可能就是一个进程在等待consoleread
的结束,另外一个进程正在运行,这个时候执行中断操作可能会把console输入传递给另外一个进程,这个时候我们也需要上一个锁.
Another way in which concurrency requires care in drivers is that one process may be waiting for input from a device, but the interrupt signaling arrival of the input may arrive when a different process (or no process at all) is running. Thus interrupt handlers are not allowed to think about the process or code that they have interrupted. For example, an interrupt handler cannot safely call copyout with the current process’s page table. Interrupt handlers typically do relatively little work (e.g., just copy the input data to a buffer), and wake up top-half code to do the rest.
时钟中断
RISC-V的CPU在一定的时间段就会触发一次时钟中断,RISC-V希望时钟中断能在M态处理而不是在S态处理.xv6选择在一个特殊的方法来处理时钟中断.
在start.c中,我们设置了把所有中断都放在S态进行处理.但是我们在timeinit
函数中创建了一个专属于时钟中断的处理模式.主要有几点:
- 配置了CLINT硬件,这个硬件会在一定间隔时间触发一次中断.
- 配置trapframe,这样可以把通用寄存器的数据放到CLINT寄存器中
- 由于M态的中断只有时钟中断,中断向量配置为timevec.
在M态下的时钟中断处理函数在是timervec
:这个保存了一部分寄存器,然后告诉CLINT硬件什么时候产生下一次时钟中断,然后引发一个S态的软件中断.
在执行用户态或者是内核态的代码的时候都会引发时钟中断,时钟中断尽量不要打扰正在执行关键任务的进程.所以说RISC-V允许引起一个软件中断,这个中断是S态引起的.
当中断被关闭的时候,说明正在执行很关键的任务,代码可以选择拒绝时钟中断的执行,如果没有关中断,这个软件中断就会打断正在执行的代码,执行时钟中断的操作,放弃对CPU的占用.
总结:进入M态处理中断->引发一个S态的中断->如果执行关键任务,先不管中断,如果不执行关键任务,就放弃对于CPU的占用->调度给其他进程.