总的来说KVM提供虚拟化的机制和解决方案,Qemu提供策略。
KVM在内核代码中主要存在如下几个部分:
- KVM基本的操作在virt/kvm文件夹中
- KVM体系结构相关的代码在arch/x86/kvm文件夹中
- KVM基本的头文件在include/linux/kvm*.h和include/linux/asm-generic/kvm*.h中,前者是userspace也会使用的头文件,后者是asm相关的头文件
- KVM体系结构相关的头文件在arch/x86/include/asm/kvm*.h和arch/x86/include/uapi/asm/kvm*.h中,前者是kernel使用的头文件,后者是kernel和userspace都会使用的头文件
<qemu/kvm-all.c>\
static int kvm_init(MachineState *ms)
{
...
s->fd = qemu_open("/dev/kvm", O_RDWR);
...
}
事实上/dev/kvm是一种字符型设备,为了理解KVM代码,首先要了解与设备驱动相关的是三个重要的内核数据结构,分别是file_operations, file和inode。
file_operations结果的作用是连接设备编号和驱动程序操作。file_operations结构定义在<linux/fs.h>中,其中包含一组函数指针。
struct file_operations {
struct module *owner; //指向拥有该结构的模块的指针
loff_t (*llseek) (struct file *, loff_t, int); //用来修改文件的当前读写位置,loff_t是long offset,至少64bits
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *); //从设备读取数据
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *); //向设备发送数据
ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
int (*iterate) (struct file *, struct dir_context *);
unsigned int (*poll) (struct file *, struct poll_table_struct *); //可用于查询某个或多个文件描述符上的读取或写入是否会被阻塞
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
int (*mmap) (struct file *, struct vm_area_struct *); //用于请求将设备内存映射到进程地址空间
int (*mremap)(struct file *, struct vm_area_struct *);
int (*open) (struct inode *, struct file *); //始终是对设备文件执行的第一个操作,并不要求驱动程序一定要声明相应的方法。NULL,打开操作永远成功
int (*flush) (struct file *, fl_owner_t id); //进程关闭设备文件描述符副本时调用,应该执行完设备上尚未完结的操作。
int (*release) (struct inode *, struct file *); // file结构被释放时,调用此操作。
int (*fsync) (struct file *, loff_t, loff_t, int datasync); //用户调用它来刷新待处理的数据
int (*aio_fsync) (struct kiocb *, int datasync); //异步刷新
int (*fasync) (int, struct file *, int);
int (*lock) (struct file *, int, struct file_lock *); //实现文件锁定
ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);//它由内核调用以将数据发送到对应的文件,每次发送一个数据页
unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long); //在进程地址空间找到合适的位置,以便将底层设备中的内存段映射到该位置
int (*check_flags)(int);
int (*flock) (struct file *, int, struct file_lock *);
ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
int (*setlease)(struct file *, long, struct file_lock **, void **);
long (*fallocate)(struct file *file, int mode, loff_t offset,
loff_t len);
void (*show_fdinfo)(struct seq_file *m, struct file *f);
#ifndef CONFIG_MMU
unsigned (*mmap_capabilities)(struct file *);
#endif
};
每个打开的文件(file结构)和一组函数关联(通过包含指向一个file_operations结构的f_op字段)。这些操作实现系统调用,比如read,wirte等。文件是一个对象,操作它的函数是“方法”。按照惯例,file_operations结构或者指向此结构的指针称为fops。注意到在其代码中许多参数包含__user字符串,表明指针是一个用户空间地址,因此不能被直接引用,通常的编译来讲,__user没有任何意义。
接下来看file结构,同样在<linux/fs.h>中有其定义:
struct file {
union {
struct llist_node fu_llist;
struct rcu_head fu_rcuhead;
} f_u;
struct path f_path;
struct inode *f_inode; /* cached value */
const struct file_operations *f_op; //文件相关的操作,方法重载
/*
* Protects f_ep_links, f_flags.
* Must not be taken from IRQ context.
*/
spinlock_t f_lock;
atomic_long_t f_count;
unsigned int f_flags; //文件标志,O_RDONLY,为检查用户请求是否是非阻塞式操作。
fmode_t f_mode; //文件模式,FMODE_READ和FMODE_WRITE位标示文件是否可读写。读写权限检查f_mode
struct mutex f_pos_lock;
loff_t f_pos; //当前的读/写位置
struct fown_struct f_owner;
const struct cred *f_cred;
struct file_ra_state f_ra;
u64 f_version;
#ifdef CONFIG_SECURITY
void *f_security;
#endif
/* needed for tty driver, and maybe others */
void *private_data; //驱动程序可将这个字段用于任何目的或着忽略。是跨系统调用时保存状态信息的非常有用的资源。
#ifdef CONFIG_EPOLL
/* Used by fs/eventpoll.c to link all the hooks to this file */
struct list_head f_ep_links;
struct list_head f_tfile_llink;
#endif /* #ifdef CONFIG_EPOLL */
struct address_space *f_mapping;
} __attribute__((aligned(4))); /* lest something weird decides that 2 is OK */
struct file_handle {
__u32 handle_bytes;
int handle_type;
/* file identifier */
unsigned char f_handle[0];
};
注意:struct file是内核结构。它不会出现在用户空间。与用户空间程序中FILE没有任何关联。file结构代表一个打开的文件,它由内核在open时创建,传递给在该文件上进行操作的所有函数,直到close函数。在文件的所有实例被关闭后,内核会释放此数据结构。在内核源码中,指向struct file的指针通常被成为file或filp(文件指针),习惯上使用filp作为指针。
最后是inode,内核用inode结构在内部表示文件,而file表示打开的文件描述符。对单个文件,可能会有许多个表示打开的文件描述符的file结构,但都指向单个inode结构。
/dev/kvm设备文件操作结构体主要是kvm_chardev_ops、kvm_vm_fops、kvm_vcpu_fops,分别对应字符型设备、VM文件描述符和VCPU文件描述符的三种操作。三种操作的owner都是THIS_MODULE。
kvm_chardev_ops.owner=module;
kvm_vm_fops.owner = module;
kvm_vcpu_fops.owner = module;
static struct file_operations kvm_chardev_ops = {
.unlocked_ioctl = kvm_dev_ioctl,
.compat_ioctl = kvm_dev_ioctl,
.llseek = noop_llseek,
};
可见kvm_chardev_ops为一个标准的file_operations结构体,但是只包含了ioctl函数,read、open、write等常见的系统调用均采用默认实现。因此,就只能在用户态通过ioctl函数进行操作。系统调用ioctl提供了执行设备特定命令的方法,此处的系统调用为kvm_dev_ioctl:
static long kvm_dev_ioctl(struct file *filp, unsigned int ioctl, unsigned long arg)
{
...
case KVM_CREATE_VM
r = kvm_dev_ioctl_create_vm(arg);
...
}
可见kvm_dev_ioctl接口可以创建虚拟机,
static int kvm_dev_ioctl_create_vm(unsigned long type)
{
...
struct kvm *kvm;
kvm = kvm_create_vm(type);
...
r = anon_inode_getfd("kvm-vm", &kvm_vm_fops, kvm, O_RDWR | O_CLOEXEC);
...
}
在调用了kvm_create_vm()之后,创建了一个匿名inode,对应的fop为kvm_vm_fops。
kvm_create_vm最终调用了kvm_arch_alloc_vm():
static struct kvm *kvm_create_vm(unsigned long type)
{
...
struct kvm *kvm = kvm_arch_alloc_vm();
...
}
在kvm_dev_ioctl_create_vm过程中创建的的匿名inode,其实是内核在内部表示文件的结构,它对应的文件操作结构体是kvm_vm_fops:
static struct file_operations kvm_vm_fops = {
.release = kvm_vm_release,
.unlocked_ioctl = kvm_vm_ioctl,
#ifdef CONFIG_KVM_COMPAT
.compat_ioctl = kvm_vm_compat_ioctl,
#endif
.llseek = noop_llseek,
};
声明的系统调用kvm_vm_ioctl。对于虚拟机的每个vCPU,Qemu都会为其创建一个线程,在其中调用kvm_vm_ioctl中的KVM_CREATE_VCPU操作创建VCPU,该操作通过调用kvm_vm_ioctl => kvm_vm_ioctl_create_vcpu => create_vcpu_fd创建一个名为"kvm-vcpu"的匿名inode并返回其描述符。之后对每个vcpu的操作都通过该文件描述 符进行,该匿名inode的kvm_vcpu_fops如下:
static long kvm_vm_ioctl(struct file *filp, unsigned int ioctl, unsigned long arg)
{
...
case KVM_CREATE_VCPU:
r = kvm_vm_ioctl_create_vcpu(kvm, arg);
...
}
// 针对KVM的fd,通过KVM_CREATE_VCPU指令字可以创建KVM的vCPU
static int kvm_vm_ioctl_create_vcpu(struct kvm *kvm, u32 id)
{
...
vcpu = kvm_arch_vcpu_create(kvm, id);
...
/* Now it's all set up, let userspace reach it */
kvm_get_kvm(kvm);
r = create_vcpu_fd(vcpu);
...
}
static int create_vcpu_fd(struct kvm_vcpu *vcpu)
{
return anon_inode_getfd("kvm-vcpu", &kvm_vcpu_fops, vcpu, O_RDWR | O_CLOEXEC);
}
//得到vcpu_fd
对于vcpu的操作主要通过kvm_vcpu_ioctl进行,其最终会调用到kvm_arch_vcpu_ioctl_***函数中。
static struct file_operations kvm_vcpu_fops = {
.release = kvm_vcpu_release,
.unlocked_ioctl = kvm_vcpu_ioctl,
#ifdef CONFIG_KVM_COMPAT
.compat_ioctl = kvm_vcpu_compat_ioctl,
#endif
.mmap = kvm_vcpu_mmap,
.llseek = noop_llseek,
};
static long kvm_vcpu_ioctl(struct file *filp,
unsigned int ioctl, unsigned long arg)
{
....
case KVM_RUN:
...
r = kvm_arch_vcpu_ioctl_run(vcpu, vcpu->run);
...
case KVM_GET_REGS:
...
}
下图是创建的单核虚拟机,可见qemu-system-x86下面有两个子进程,有一个vcpu线程和一个监控线程:
能够看到程序创建的一个名叫"kvm-vm"的anon_inode和一个名叫"kvm-vcpu"的anon_inode。与之前的分析相符。
没有评论:
发表评论