0X01 初始化串口和Console
在PC上,PC的总线都是并行的,因此串口出现了,负责并行和串行的转换。串口和处理器通过地址总线、数据线以及I/O控制总线相连。除此之外,还有一个中断线,串口收到数据时需要通知CPU,串口通过8259A向CPU发送中断请求。
一般访问外设的内存或者寄存器有2种方式
- 将外设的内存、寄存器映射到CPU的内存地址空间中,CPU访问外设如同访问自己的内存一样。即MMIO(Memory-mapped I/O)
- 使用专用的I/O指令
CPU访问串口使用后者,处理器向串口发送数据的基本步骤如下
-
处理器向地址总线写入串口地址,地址译码电路根据地址的3 ~ 9位,确定CPU意图访问哪个芯片,即片选,拉低对应的片选输出
-
锁存器锁存A0~A2,用来选择目的寄存器
-
处理器将数据送上数据总线
-
处理器拉低WR管脚,通知目标芯片读取数据
对于Guest写串口的操作,KVM需要截取到Guest向串口输出的信息,并且调用虚拟串口设备完成I/O操作。所以,KVM充当的角色之一类似地址译码电路,其需要根据out指令的I/O地址,判断出片选哪个外设。CPU在从Guest模式退 出到Host模式前,会填充VM-EXIT INFORMATION FIELDS字段,即为Host判断是什么原因导致VM exit提供依据。KVM首先从VMCS 中读出VM exit的原因,然后调用对应的处理函数。
KVM将截获的Guest的I/O相关的信息保存在了一个结构体kvm_run实例中,每个VCPU对应一个结构体kvm_run的实例。如果I/O没有在内核空间得到处理,那么还需要切换到用户空间进行模拟。采用内存映射的方式,即在创建VCPU时, 为kvm_run分配了一个页面,用户空间调用mmap函数把这个页面映射到用户空间。实现函数代码为
struct kvm_cpu *kvm_cpu__arch_init(struct kvm *kvm, unsigned long cpu_id) {
struct kvm_cpu *vcpu = malloc(sizeof(struct kvm_cpu));
int mmap_size;
vcpu->kvm = kvm;
if (!vcpu)
return NULL;
vcpu->cpu_id = cpu_id;
vcpu->vcpu_fd = ioctl(vcpu->kvm->vm_fd, KVM_CREATE_VCPU, cpu_id);
if (vcpu->vcpu_fd < 0)
perror("KVM_CREATE_VCPU ioctl");
mmap_size = ioctl(vcpu->kvm->sys_fd, KVM_GET_VCPU_MMAP_SIZE, 0);
if (mmap_size < 0)
perror("KVM_GET_VCPU_MMAP_SIZE ioctl");
vcpu->kvm_run = mmap(NULL, mmap_size, PROT_READ|PROT_WRITE, MAP_SHARED, vcpu->vcpu_fd, 0);
if (vcpu->kvm_run == MAP_FAILED)
perror("unable to mmap vcpu fd");
return vcpu;
}
KVM用户空间实例需要把kvm_run映射到用户空间才可以访问Guest的I/O数据
MMIO
MMIO是PCI规范的一部分,I/O设备被映射到内存地址空间而不是 I/O空间。从处理器的角度来看,I/O映射到内存地址空间后,访问外设与访问内存一样,简化了程序设计。以MMIO方式访问外设时不使用 专用的访问外设的指令(out、outs、in、ins),是一种隐式的I/O访问,但是因为这些映射的地址空间是留给外设的,因此CPU将产生页面 异常,从而触发虚拟机退出,陷入VMM。trivial-kvm中在serial8250__init
函数中进行终端结构题的配置。默认将ttyS0作为首选终端,serial_device结构体如下[2]
struct serial8250_device {
struct device_header dev_hdr;
pthread_mutex_t mutex;
u8 id;
u32 iobase;
u8 irq;
u8 irq_state;
int txcnt;
int rxcnt;
int rxdone;
char txbuf[64];
char rxbuf[64];
u8 dll; // Divisor Latch LOW
u8 dlm;
u8 iir;
u8 ier;
u8 fcr;
u8 lcr; // Line Control Reg
u8 mcr; // Modem Control Reg
u8 lsr; // Line Status Reg
u8 msr; // Modem Status Reg
u8 scr;
};
device_header是所有设备共有的对象,基于红黑树,使得kvmtool能够快速定位到指定设备。同时设置默认devices,具体可以看kvmtool如下代码 https://github.com/christasa/trivial-kvm/blob/main/include/devices.h#L112。之后利用serial8250_mmio对8250的设备模拟来初始化VM的console端。
static void serial8250_mmio(struct kvm_cpu *vcpu, u64 addr, u8 *data, u32 len,
u8 is_write, void *ptr) {
struct serial8250_device *dev = ptr;
if (is_write)
serial8250_out(dev, vcpu, addr - dev->iobase, data);
else
serial8250_in(dev, vcpu, addr - dev->iobase, data);
}
serial8250_mmio()
首先会根据IO的方向选择对应处理函数, 调用时会计算端口的偏移。因此一个设备会映射多个端口, 不同端口具有不同的功能[3]。通过kvm__register_iotrap
函数将device以红黑树的方式写入物理内存。serial8250_out
是将Host物理内存中的内容data写入虚拟设备device的console中。同理serial8250_in
函数将device设备的输入写入Host物理内存中。serial8250_out
的解析参考知乎上对该函数对代码的解释[3]
//offset = 本次Guest访问的IO端口 - 设备的起始IO端口
static bool serial8250_out(struct serial8250_device* dev, struct kvm_cpu* vcpu, u16 offset, void* data)
{
bool ret = true;
char* addr = data;
mutex_lock(&dev->mutex); //独占访问本设备
switch (offset) {
case UART_TX: //操作的是数据传输寄存器
if (dev->lcr & UART_LCR_DLAB) { //如果lcr设置了初始访问标志Divisor Latch Access Bit.
dev->dll = ioport__read8(data); //那么这个数据传输要写入dll, 保存波特率除数的低位
break;
}
/* Loopback mode */
if (dev->mcr & UART_MCR_LOOP) { //如果mcr设置了Loopback标志
if (dev->rxcnt < FIFO_LEN) { //则向rxbuf中压入一个字符
dev->rxbuf[dev->rxcnt++] = *addr;
dev->lsr |= UART_LSR_DR;
}
break;
}
//对于大多数情况
if (dev->txcnt < FIFO_LEN) { //如果缓冲区还有位置
dev->txbuf[dev->txcnt++] = *addr; //则向输出缓冲区压入一个字符
dev->lsr &= ~UART_LSR_TEMT; //清除行状态寄存器lsr的 Transmitter empty标志
if (dev->txcnt == FIFO_LEN / 2) //如果缓冲区使用了一半
dev->lsr &= ~UART_LSR_THRE; //则清除lsr的Transmit-hold-register empty标志
serial8250_flush_tx(vcpu->kvm, dev); //刷新发送缓冲区
} else {
/* Should never happpen */
dev->lsr &= ~(UART_LSR_TEMT | UART_LSR_THRE);
}
break;
...;
}
serial8250_update_irq(vcpu->kvm, dev);
mutex_unlock(&dev->mutex);
return ret;
}
0X02 创建终端terminal
创建终端原理比较简单,即hook原terminal的所有输入,stdin以及stdout保存四个deivces tty数组结构体上,具体见如下代码解释
static int term_fds[4][2]; // 初始化terminal fd
int term_init(struct kvm *kvm)
{
struct termios term;
int i, r;
// 将四个devices的两个fd都设置为STDIN以及stdout
for (i = 0; i < 4; i++)
if (term_fds[i][0] == 0) {
term_fds[i][0] = STDIN_FILENO;
term_fds[i][1] = STDOUT_FILENO;
}
if (!isatty(STDIN_FILENO) || !isatty(STDOUT_FILENO))
return 0;
r = tcgetattr(STDIN_FILENO, &orig_term);
if (r < 0) {
printf("unable to save initial standard input settings\n");
return r;
}
term = orig_term;
term.c_iflag &= ~(ICRNL);
term.c_lflag &= ~(ICANON | ECHO | ISIG);
tcsetattr(STDIN_FILENO, TCSANOW, &term);
/* Use our own blocking thread to read stdin, don't require a tick */
// 使用线程启动terminal使其循环接受stdin以及打印stdout
if(pthread_create(&term_poll_thread, NULL, term_poll_thread_loop, kvm))
perror("Unable to create console input poll thread\n");
signal(SIGTERM, term_sig_cleanup);
atexit(term_cleanup);
return 0;
}
利用线程启动term_poll_thread_loop函数使其循环接受stdin的输入和将结果输入到stdout
void *term_poll_thread_loop(void *param)
{
struct pollfd fds[4];
struct kvm *kvm = (struct kvm *) param;
int i;
kvm__set_thread_name("term-poll");
for (i = 0; i < 4; i++) {
fds[i].fd = term_fds[i][0];
fds[i].events = POLLIN;
fds[i].revents = 0;
}
while (1) {
/* Poll with infinite timeout */
if(poll(fds, 4, -1) < 1)
break;
// 持续接受输入输出
kvm__arch_read_term(kvm);
// 主要有term_getc
}
return NULL;
}
int term_getc(struct kvm *kvm, int term) {
int term_got_escape = 0;
unsigned char c;
if (read_in_full_terminal(term_fds[term][0], &c, 1) < 0)
return -1;
if (term_got_escape) {
term_got_escape = 0;
if (c == 'x') {
if (kvm->cpus[0] && kvm->cpus[0]->thread != 0)
pthread_kill(kvm->cpus[0]->thread, SIGRTMIN);
}
if (c == 0x01)
return c;
}
if (c == 0x01) {
term_got_escape = 1;
return -1;
}
return c;
}
值得注意的是serial8250_update_irq
和serial8250__receive
函数,将stdin的字符送入8520设备。
static void serial8250_update_irq(struct kvm *kvm, struct serial8250_device *dev) {
u8 iir = 0;
// 重置rx寄存器
if (dev->lcr & UART_FCR_CLEAR_RCVR) {
dev->lcr &= ~UART_FCR_CLEAR_RCVR;
dev->rxcnt = dev->rxdone = 0;
dev->lsr &= ~UART_LSR_DR;
}
// 重置tx寄存器
if (dev->lcr & UART_FCR_CLEAR_XMIT) {
dev->lcr &= ~UART_FCR_CLEAR_XMIT;
dev->txcnt = 0;
dev->lsr |= UART_LSR_TEMT | UART_LSR_THRE;
}
if ((dev->ier & UART_IER_RDI) && (dev->lsr & UART_LSR_DR))
iir |= UART_IIR_RDI;
if ((dev->ier & UART_IER_THRI) && (dev->lsr & UART_LSR_TEMT))
iir |= UART_IIR_THRI;
if (!iir) {
dev->iir = UART_IIR_NO_INT;
if (dev->irq_state)
kvm__irq_line(kvm, dev->irq, 0);
} else {
dev->iir = iir;
if (!dev->irq_state)
kvm__irq_line(kvm, dev->irq, 1);
}
dev->irq_state = iir;
if (!(dev->ier & UART_IER_THRI))
serial8250_flush_tx(kvm, dev); // 刷新设备同时使用term_put将stdin输入到8520中
}
之后会在virtio虚拟化中进行irq PCI总线的学习
0XFF Reference
- Inside the Linux Virtualization Principle and Implementation
- https://www.techedge.com.au/tech/8250tec.htm
- https://zhuanlan.zhihu.com/p/583203148