xv6的IO这部分的源码可以说是笔者至今为止认为xv6最难的部分
因为这部分涉及到软件和硬件的交互,读者们必须先对前几章的
内容有所了解,才能了解这一章
UART:你可以理解为输入字符和输出字符用的硬件
内存空间中有一部分地址线属于硬件访问的地址线,这部分地址线并不与RAM相连,而是直接连接到硬件的端口上,这样使得CPU能够像访问内存一样访问硬件,这样就能够充分利用地址总线来传输信息。
当我们在控制台输入一个字符的时候,首先会触发一个硬件中断,同时UART会读取你输入的字符,然后传到UART的RHR寄存器——UART硬件内部是一个FIFO形式的队列,而且队头的字符会被放到RHR寄存器上。
硬件中断会传到内核空间,然后沿着trap路径调用UART中断处理程序,然后操作系统的程序会访问内存上的UART位置,然后读取RHR寄存器来获取我们在控制台输入的字符,传入内核的控制台缓冲区,只有在检测到回车键,文件尾或者输入量超出缓冲区长度时,内核才会把缓冲区的数据读入程序中。
大致的调用路径就像上图说的那样,接下来我们来详细解析输入流程的源码
基本的trap源码不再做进一步解析,我们直接从环境初始化和UART开始解析
操作系统直接通过内存空间访问UART,而UART中开放了几个寄存器来作为交互的接口
// 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)
#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
#define ReadReg(reg) (*(Reg(reg)))
#define WriteReg(reg, v) (*(Reg(reg)) = (v))
RHR寄存器用于输入,THR寄存器用于输出,THR的解析我们下篇细说
IER是中断开关寄存器,用于控制UART中断传递的开关
IER_RX_ENABLE位 用于控制输入中断
IER_TX_ENBALE位 用于控制输出中断
FCR是FIFO的状态寄存器
FCR_FIFO_ENBALE位 用于控制FIFO读入读出状态
FCR_FIFO_CLEAR位 用于清除输入输出的FIFO的内容
LSR寄存器是行状态寄存器
LSR_RX_READY位 用于表示输入端的读取状态
LSR_TX_IDLE位 用于表示THR寄存器已经可以准备输出
其他寄存器暂时用不上,我们不做介绍
第4行可以获得对应寄存器的内核虚拟地址
26行负责读取对应寄存器的值
27行负责向对应寄存器作写入值
注意第四行的 volatile unsigned char* 这说明读取和写入这些寄存器的长度都是一字节
接下来我们开始解析read()读取控制台的过程
在用户空间调用read()的时候,系统首先产生trap进入内核
然后进入sys_read() [kernel/sysfile.c]
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_read()的关键函数是fileread(f,p,n) [kernel/file.c]
我们来看看 fileread(f,p,n) 的作用
// Read from file f.
// addr is a user virtual address.
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;
}
//[kernel/file.h]
struct file {
enum { FD_NONE, FD_PIPE, FD_INODE, FD_DEVICE } type;
int ref; // reference count
char readable;
char writable;
struct pipe *pipe; // FD_PIPE
struct inode *ip; // FD_INODE and FD_DEVICE
uint off; // FD_INODE
short major; // FD_DEVICE
};
由于第10行到11行的分支用于处理管道的输入和输出,而第16到20行用于处理文件的输入和输出,因此我们只取12到15行的控制台输出来进行分析
12行显然,用于判定本次读取是由于设备中断而引起的,所以可判定是控制台输入
在解析13行的分支前,可以先看file结构体的描述,第37行中major代表了文件的设备编号
到这里可以回想第一章的内容——文件是对IO的抽象,所以file结构体中包含了管道,程序文件,和设备文件的信息,我们能够利用这些信息来判断这个文件的性质
第13行的前两个分支就是用于判断这个设备文件的所属设备号,然后第三个分支我们来看看devsw结构
// map major device number to device functions.
struct devsw {
int (*read)(int, uint64, int);
int (*write)(int, uint64, int);
};
extern struct devsw devsw[];
devsw结构用于保存相对应的结构体的IO函数,devsw数组对应的是各个设备的IO,借助这个语法我们就能够为每个设备接上单独的IO函数,而且为单个设备更换IO手段也非常方便,只需要在运行时更换即可
回到fileread()的解析
14行调用devsw中存储的read函数,读取输入端的内容
然后我们开始讲devsw的read函数,毕竟我们这里只是对read函数做了调用,并没有讲这个东西是哪里来的
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;
}
在 consoleinit() [kernel/console.c] 中,可以发现它调用了uart初始化函数以及初始化了devsw[CONSOLE]的IO函数,事实上CONSOLE是一个宏,值为1,对应了uart的设备编号
uartinit()我们不再作深入解析,没什么意义,感兴趣的读者可以自行翻阅xv6源码
到这里我们应该要停下来,做个概念区分:
当我们调用read的时候,控制台不一定已经输入了东西,输入和读取两个过程在内核中是独立的过程
我们继续解析read()的调用路径
fileread() 调用了 consoleread() [kernel/console.c] ,consoleread() 是控制台的输入函数,能够将缓冲区的内容读入用户空间提供的缓冲区中
以下是consoleread()以及consoleintr()的源码
//
// 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;
}
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;
}
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;
首先看consoleread(),在这里18行到24行并不好理解,笔者将作详细解析
在解析18行的判断前我们首先要看66行定义的匿名结构体cons,cons存储了控制台的输入缓冲区的相关信息,相关的下标的大致作用在注释上有写,笔者在这里着重对三个变量的作用进行讲解
63行 变量r代表了当前consoleread()实际读入且回传到用户空间的字节数
64行 变量w代表了当前consoleread()实际读入到console的缓冲区cons.buf中的实际字节数
65行 变量e代表了当前consoleintr()读入到cons.buf,但是又并没有完全保存下来的字节数
w和e的区别多少有点抽象,笔者举个简单的例子,就像你在写word的时候,你点开word文件,写了一部分内容,但是没点保存,这时候就相当于你把文件内容写进去了cons.buf,字节数为e,你没点保存的话,在退出的时候就给你清除掉了你写的内容,你再打开的时候还是原来的文件,也就是字节数还是为w
18行里面首先比较 cons.r 和 cons.w,这表明cons.buf没有发生任何变化,然后19行检查当前是不是已经寄了,如果一切正常,那就在21行调用sleep()让程序睡眠,等待中断的到来
然后在这里我们先停止对consoleread()的分析
我们再一次强调这个观点:当我们调用read的时候,控制台不一定已经输入了东西,输入和读取两个过程在内核中是独立的过程
为什么要这样强调这个观点呢,因为在这里,如果我们不输入东西,不触发任何中断,consoleread()就会一直在21行这里睡大觉,就读取不了内容
你每一次成功输入一个字符,其实都触发了一次硬件中断,然后会唤醒consoleread()读取缓冲区
所以我们接下来需要重点来解析UART中断了,因为这个是能够唤醒consoleread()的关键路径
当我们输入字符时,UART硬件会请求riscv触发一次硬件中断,而通过我们之前提到过的trap跳转,在usertrap()中,函数将进入devintr()来判断硬件中断的类型
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;
}
}
devintr()首先利用scauese检查中断类型,然后判断出是UART发送的中断,于是调用uartintr(),这便是UART中断处理器
// 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);
}
// read one input character from the UART.
// return -1 if none is waiting.
int
uartgetc(void)
{
if(ReadReg(LSR) & 0x01){
// input data is ready.
return ReadReg(RHR);
} else {
return -1;
}
}
在触发中断以后,uartintr()调用uartgetc(),以读取一个字节
uartgetc()则非常直接,它做了两件事,第一是检查LSR寄存器中的LSR_RX_REDAY位(也就是第一位),判断输入是否就绪,然后用ReadReg(RHR)来读取RHR寄存器中的一个字节,接着uartintr()将这个字节传给consoleintr()
第16行uartstart()负责将输出缓冲区中的字符全部发送出去,这样就不需要触发太多次中断了,这个函数的功能我们留到下一篇再详细解析
consoleintr()的主要功能是将UART的RHR中传过来的内容读入cons.buf,并叫醒consoleread()起来干活
#define BACKSPACE 0x100
#define C(x) ((x)-'@') // Control-x
//
// 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);
}
这段功能没什么太值得讲的,非常简单,看注释就能看懂了
最后便是唤醒consoleread(),然后回到consoleread(),读取缓冲区内容,然后传到用户空间,完成一次输入