2015年11月18日星期三

Getting started with KVM source code

In my blogger, the version of Linux Kernel is linux-4.2.5, the qemu version is  qemu-2.4.1.



总的来说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都会使用的头文件
工作在userspace的qemu与内核中kvm通过ioctl系统调用交互,接口就是设备文件/dev/kvm,这个设备文件是不可读/写的,只能使用ioctl接口控制。

<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。与之前的分析相符。

没有评论:

发表评论