|
第4章 io设备虚拟化
Io设备的虚拟化,不可避免要涉及到设备的概念和隐藏在设备背后的总线。理解了这两个概念,就比较容易理解io设备的虚拟化。
4.1 设备,总线和驱动
设备是计算机系统中一个重要概念。通常的显卡网卡声卡等设备,都是先插入计算机系统的PCI总线插槽(早期还有ISA,MCA总线等。现在PC领域基本PCI总线统一了),然后安装驱动,之后应用程序可以通过文件系统打开和读写设备文件。这个过程可以从三个层面理解:设备本身的特性,总线和操作系统对设备的处理,驱动层次。
以PCI设备为例,一个PCI设备,本身就包含一个配置表。配置表包含设备制造商填充的厂商信息,设备属性等等的通用配置信息。此外,设备厂商还应该提供设备的控制寄存器信息,通过这些控制寄存器,系统可以设置设备的状态,控制设备的运行,或者从设备获得状态信息。另外设备还可能配备了内存(也有的设备可能没有),系统可以读写设备的内存。用图3-1来解释设备的基本信息:
图3-1 设备配置表的信息
如上图,设备本身有一些配置信息。配置信息里面的设备内存基址,指示了设备内存的地址和长度,而设备寄存器基址,则指向了设备的寄存器地址和长度。该设备有两个寄存器,一个输入寄存器,一个输出寄存器。当输入寄存器写入数值后,可以从输出寄存器读到另一个数值。
设备寄存器基址,这个概念有点难理解。实际上,可以看做是一个地址,对这个地址写指令,就可以控制设备。所以,设备寄存器其实就是设备的控制接口。这个接口必须要映射到计算机系统的io空间,这样内核就可以访问设备了。
4.1.1 io端口和io内存
不同的处理器对io访问有不同的处理方式。对X86系统来说,专门提供了特别的指令来访问设备寄存器。所有这些设备寄存器占据了65536个8位的空间。这个空间称为计算机的IO端口空间。
对上文的例子设备来说,需要把设备的寄存器基址纳入到系统的IO端口空间里面,然后驱动就可以通过系统提供的特别指令来访问设备的寄存器。假设设备厂商提供的寄存器基址是0x1c00,长度是8个字节。那么有两种情况,一种是这个0x1c00地址和别的设备没有冲突,可以直接使用,那么操作系统内核就记录设备的寄存器基址为0x1c00,驱动通过X86系统提供的io指令访问0x1c00 io地址,或者叫0x1c00 io端口,就可以设置设备输入寄存器的内容。访问地址0x1c04,就可以读到设备输出寄存器的内容。
另外一种情况是其它设备也使用了0x1c00这个io地址。那么操作系统内核就需要寻找一个合适的寄存器基址,然后更新设备的寄存器基址,并记录到内核的设备信息里面。驱动使用x86的io指令,访问这个更新的地址,就可以设置设备输入寄存器的内容了。
通过设备的io端口访问设备寄存器来控制设备,这就是设备驱动的功能。设备厂商会提供设备寄存器的详细内容,这也是驱动开发者所必须关注的。而发现设备,扫描设备信息,为设备提供合适的io地址空间,这是内核的总线部分要处理的事情。后文将继续分析。
设备的内存处理过程差不多一样。内核同样要读取设备内存基址,然后找到合适的内存空间,把设备的内存映射到内存空间。然后驱动就可以标准的内存接口访问设备的内存了。
4.1.2 总线
设备的配置信息提供了设备寄存器基址和设备内存基址。因此首先要读到这两个寄存器的内容,也就是设备寄存器基址和长度以及设备内存基址和长度,然后操作系统才能安排合适的io端口和io内存。
但是如何去读设备的配置信息?X86系统使用的PCI总线对这个问题的解决方法是:保留了8个字节的io端口地址,就是0xCF8~0xCFF。要访问设备的某个配置信息,先往0xCF8地址写入目标地址信息,然后通过0xCFC地址读数据,就可以获得这个配置信息。这里的写和读,都使用的是x86所特有的io指令。
写入0xCF8的目标地址信息,是一种包括了总线号,设备号,功能号和配置寄存器地址的综合信息。
1) 总线对设备的扫描和管理
计算机系统的设备是如何被发现的?对PCI总线上的设备来说,这是总线扫描所得到的结果(非PCI总线各有各自的扫描方式)。每个PCI设备有一个总线号,一个设备号,一个功能号标识。Pci规范允许一个系统最多拥有256条总线,每条总线最多可以带32个设备,每个设备可以是最多8个功能的多功能板。Pci扫描就是对单条总线的地址范围进行扫描。根据前面关于pci配置信息的知识,如果某个地址存在设备,那么在0xCF8写入目标地址信息,就可以从0xCFC读到设备的信息。包括设备的io端口和io内存,设备的中断号和DMA信息。对每个读到的pci设备,都要为它创建设备对象。
2) 总线对驱动和设备的管理
当设备插入计算机系统时,可以找到自己的驱动开始工作。而升级了设备驱动,也能找到适合的设备,自动产生设备。这些功能其实就是pci总线结构完成的。通过代码分析来了解一下:
代码清单3-1 pci_register_driver
int __pci_register_driver(struct pci_driver *drv, struct module *owner)
{
int error;
/*设置驱动的总线类型和参数*/
/* initialize common driver fields */
drv->driver.name = drv->name;
drv->driver.bus = &pci_bus_type;
drv->driver.owner = owner;
drv->driver.kobj.ktype = &pci_driver_kobj_type;
spin_lock_init(&drv->dynids.lock);
INIT_LIST_HEAD(&drv->dynids.list);
/* register with core */
error = driver_register(&drv->driver);
if (!error)
error = pci_create_newid_file(drv);
return error;
} <object data="data:application/x-silverlight-2," type="application/x-silverlight-2"
注册一个pci驱动,其实就是调用了__pci_register_driver函数。首先设置了驱动的总线类型为pci,然后调用driver_register登记。
代码清单3-2 driver_register
int driver_register(struct device_driver * drv)
{
klist_init(&drv->klist_devices, klist_devices_get, klist_devices_put);
init_completion(&drv->unloaded);
return bus_add_driver(drv);
}<object data="data:application/x-silverlight-2," type="application/x-silverlight-2"
实际是调用了bus_add_driver。bus_add_driver中真正起作用的是driver_attach这个函数。
代码清单3-3 设
void driver_attach(struct device_driver * drv)
{
bus_for_each_dev(drv->bus, NULL, drv, __driver_attach);
} <object data="data:application/x-silverlight-2," type="application/x-silverlight-2"
这里就清楚了,driver_attach实际上把总线上面的所有设备都遍历了一遍,通过__driver_attach函数判断驱动和设备是否匹配。而__driver_attach实际是调用了driver_probe_device来检查设备和驱动的匹配关系。
代码清单3-4 设
int driver_probe_device(struct device_driver * drv, struct device * dev)
{
int ret = 0;
/*总线定义了match函数,通过match函数判断是否匹配*/
if (drv->bus->match && !drv->bus->match(dev, drv))
goto Done;
pr_debug("%s: Matched Device %s with Driver %s\n",
drv->bus->name, dev->bus_id, drv->name);
dev->driver = drv;
/*匹配,调用probe函数*/
if (dev->bus->probe) {
ret = dev->bus->probe(dev);
if (ret) {
dev->driver = NULL;
goto ProbeFailed;
}
} else if (drv->probe) {
ret = drv->probe(dev);
if (ret) {
dev->driver = NULL;
goto ProbeFailed;
}
}
device_bind_driver(dev);
ret = 1;
pr_debug("%s: Bound Device %s to Driver %s\n",
drv->bus->name, dev->bus_id, drv->name);
goto Done;
Done:
return ret;
} <object data="data:application/x-silverlight-2," type="application/x-silverlight-2"
判断驱动和设备是否匹配,首先是通过注册的match函数判断。通常情况,匹配是通过驱动里面包含的id表和扫描设备发现的id比较,如果相同,则说明驱动和设备是适合的。当匹配通过后,调用驱动里面的probe函数。这个函数往往用来继续对设备做进一步的检测工作。在后面块设备的驱动可以看到具体的用法。
4.1.3 设备中断
Cpu虚拟化一节已经讨论了设备中断的处理。
4.2 虚拟化的设备驱动和总线
半虚拟化结构的xen提出了虚拟设备的架构。Xen的虚拟设备架构采用前后端分离的设备驱动结构。虚拟设备驱动包含两个部分:domU中的前段设备驱动(fronted)和dom0中的后端设备驱动。后端设备驱动可以访问真实的硬件设备。
前端设备驱动从Guest OS接收io请求,然后将io请求转发到后端,而后端接收到前端转发的设备请求后,检查请求是否合法,然后通过本地的设备驱动访问真实的硬件设备。Io完成后,后端设备驱动通知前端设备驱动已经准备就绪,然后前端驱动向Guest OS报告io操作完成。
在Xen的半虚拟化架构中,同样需要一种机制来发现设备,连接设备和驱动,自动匹配设备和驱动。而且和linux不同的是,因为前端设备和后端设备是联动的关系,当某一方设备变动的时候,还必须通知另一方设备的变动情况。为了完成这个工作,xen提供了一条虚拟总线xenbus来管理所有的虚拟设备和驱动。Xen系统的所有虚拟设备都要注册到xenbus。Pci总线也是作为一个设备注册到xenbus ,通过注册的pci总线,执行扫描动作可以产生所有的pci设备。而所有的虚拟驱动也都要注册到xenbus,从而可以自动完成虚拟设备和驱动的匹配。
4.2.1 Pci前端注册和扫描
代码清单3-5 pcifront_init
static int __init pcifront_init(void)
{
if (!is_running_on_xen())
return -ENODEV;
return xenbus_register_frontend(&xenbus_pcifront_driver);
}<object data="data:application/x-silverlight-2,"
Pcifront作为一个前端驱动注册到xenbus。注册驱动后,会自动调用驱动的probe函数,完成初始化后,把设备状态改为初始完成状态。通过xenbus,pci后端检测到前端变化,将信息同步后,将状态改为connected。前端此时要调用pcifront_try_connect完成扫描。
代码清单3-6 设
static int pcifront_try_connect(struct pcifront_device *pdev)
{
int err = -EFAULT;
int i, num_roots, len;
char str[64];
unsigned int domain, bus;
spin_lock(&pdev->dev_lock);
/* Only connect once */
if (xenbus_read_driver_state(pdev->xdev->nodename) !=
XenbusStateInitialised)
goto out;
/*检查状态,保证只连接一次*/
err = pcifront_connect(pdev);
if (err) {
xenbus_dev_fatal(pdev->xdev, err,
"Error connecting PCI Frontend");
goto out;
}
err = xenbus_scanf(XBT_NIL, pdev->xdev->otherend,
"root_num", "%d", &num_roots);
if (err == -ENOENT) {
xenbus_dev_error(pdev->xdev, err,
"No PCI Roots found, trying 0000:00");
err = pcifront_scan_root(pdev, 0, 0);
num_roots = 0;
} else if (err != 1) {
if (err == 0)
err = -EINVAL;
xenbus_dev_fatal(pdev->xdev, err,
"Error reading number of PCI roots");
goto out;
}
for (i = 0; i < num_roots; i++) {
len = snprintf(str, sizeof(str), "root-%d", i);
if (unlikely(len >= (sizeof(str) - 1))) {
err = -ENOMEM;
goto out;
}
/*获得pci的域号和总线号*/
err = xenbus_scanf(XBT_NIL, pdev->xdev->otherend, str,
"%x:%x", &domain, &bus);
if (err != 2) {
if (err >= 0)
err = -EINVAL;
xenbus_dev_fatal(pdev->xdev, err,
"Error reading PCI root %d", i);
goto out;
}
/*扫描pci总线*/
err = pcifront_scan_root(pdev, domain, bus);
if (err) {
xenbus_dev_fatal(pdev->xdev, err,
"Error scanning PCI root %04x:%02x",
domain, bus);
goto out;
}
}
err = xenbus_switch_state(pdev->xdev, XenbusStateConnected);
if (err)
goto out;
out:
spin_unlock(&pdev->dev_lock);
return err;
}<object data="data:application/x-silverlight-2," type="application/x-silverlight-2"
获得后端设置的pci域号和pci总线号后,调用pcifront_scan_root来扫描设备。
<object data="data:application/x-silverlight-2,"
代码清单3-7 pcifront_scan_root
int pcifront_scan_root(struct pcifront_device *pdev,
unsigned int domain, unsigned int bus)
{
struct pci_bus *b;
struct pcifront_sd *sd = NULL;
struct pci_bus_entry *bus_entry = NULL;
int err = 0;
bus_entry = kmalloc(sizeof(*bus_entry), GFP_KERNEL);
sd = kmalloc(sizeof(*sd), GFP_KERNEL);
if (!bus_entry || !sd) {
err = -ENOMEM;
goto err_out;
}
pcifront_init_sd(sd, domain, pdev);
b = pci_scan_bus_parented(&pdev->xdev->dev, bus,
&pcifront_bus_ops, sd);
if (!b) {
dev_err(&pdev->xdev->dev,
"Error creating PCI Frontend Bus!\n");
err = -ENOMEM;
goto err_out;
}
bus_entry->bus = b;
list_add(&bus_entry->list, &pdev->root_buses);
/* Claim resources before going "live" with our devices */
pci_walk_bus(b, pcifront_claim_resource, pdev);
pci_bus_add_devices(b);
return 0;
err_out:
kfree(bus_entry);
kfree(sd);
return err;
}<object data="data:application/x-silverlight-2," type="application/x-silverlight-2"
pci_scan_bus_parented就是对pci总线的扫描,扫描之后,产生的pci设备就连接到总线的链表头,从而通过总线可以遍历pci设备。而pci_walk_bus要把设备的资源(io端口,io内存)全都注册到系统内。
4.3 虚拟化的块设备
硬盘等块设备采用了前后端的设备驱动架构。对原生的驱动比较,采用半虚拟化之后的驱动分成了两部分。在domU里面运行的是前端驱动,而dom0里面运行后端驱动。
后端驱动和前端一一对应。当前端驱动状态改变的时候,就会触发事件,通知后端驱动。当后端驱动完成事务处理后,通过改变状态,就可以触发事件,通知前端驱动。所以前端驱动和后端驱动是互相呼应,共同完成io请求。
4.3.1 块设备的前端驱动
前端驱动定义了一个xenbus的驱动类型。如清单所示:
代码清单3-8 blkfront
static struct xenbus_driver blkfront = {
.name = "vbd",
.owner = THIS_MODULE,
.ids = blkfront_ids,
.probe = blkfront_probe,
.remove = blkfront_remove,
.resume = blkfront_resume,
.otherend_changed = backend_changed,
}
根据前面总线的分析,这个驱动要注册到xenbus总线。如果和xenbus扫描出来的设备能匹配,那么要调用驱动提供的probe函数,做进一步的初始化。
代码清单3-9 虚拟驱动例子
static int blkfront_probe(struct xenbus_device *dev,
const struct xenbus_device_id *id)
{
int err, vdevice, i;
struct blkfront_info *info;
/* FIXME: Use dynamic device id if this is not set. */
err = xenbus_scanf(XBT_NIL, dev->nodename,
"virtual-device", "%i", &vdevice);
if (err != 1) {
xenbus_dev_fatal(dev, err, "reading virtual-device");
return err;
}
info = kzalloc(sizeof(*info), GFP_KERNEL);
if (!info) {
xenbus_dev_fatal(dev, -ENOMEM, "allocating info structure");
return -ENOMEM;
}
info->xbdev = dev;
info->vdevice = vdevice;
info->connected = BLKIF_STATE_DISCONNECTED;
INIT_WORK(&info->work, blkif_restart_queue, (void *)info);
for (i = 0; i < BLK_RING_SIZE; i++)
info->shadow.req.id = i+1;
info->shadow[BLK_RING_SIZE-1].req.id = 0x0fffffff;
/* Front end dir is a number, which is used as the id. */
info->handle = simple_strtoul(strrchr(dev->nodename,'/')+1, NULL, 0);
dev->dev.driver_data = info;
/*连接后端*/
err = talk_to_backend(dev, info);
if (err) {
kfree(info);
dev->dev.driver_data = NULL;
return err;
}
return 0;
}<object data="data:application/x-silverlight-2," type="application/x-silverlight-2"
这个函数主要是申请一个blkfront_info类型的数据结构。这个数据结构是个容器,既包括块设备有关的请求队列和gendisk数据结构,也包括和虚拟化有关的ring结构,和grantbable结构。
初始化之后,调用talk_to_backend来和后端设备连接。
代码清单3-10 虚拟驱动例子
static int talk_to_backend(struct xenbus_device *dev,
struct blkfront_info *info)
{
const char *message = NULL;
struct xenbus_transaction xbt;
int err;
/*ring是前端和后端通讯的一种方式*/
/* Create shared ring, alloc event channel. */
err = setup_blkring(dev, info);
if (err)
goto out;
again:
/*启动一个xenbus事务*/
err = xenbus_transaction_start(&xbt);
if (err) {
xenbus_dev_fatal(dev, err, "starting transaction");
goto destroy_blkring;
}
/*输出信息,在后端可以看到输出的信息*/
err = xenbus_printf(xbt, dev->nodename,
"ring-ref","%u", info->ring_ref);
if (err) {
message = "writing ring-ref";
goto abort_transaction;
}
err = xenbus_printf(xbt, dev->nodename, "event-channel", "%u",
irq_to_evtchn_port(info->irq));
if (err) {
message = "writing event-channel";
goto abort_transaction;
}
err = xenbus_printf(xbt, dev->nodename, "protocol", "%s",
XEN_IO_PROTO_ABI_NATIVE);
if (err) {
message = "writing protocol";
goto abort_transaction;
}
err = xenbus_transaction_end(xbt, 0);
if (err) {
if (err == -EAGAIN)
goto again;
xenbus_dev_fatal(dev, err, "completing transaction");
goto destroy_blkring;
}
/*改变前端状态为初始化完成*/
xenbus_switch_state(dev, XenbusStateInitialised);
return 0;
abort_transaction:
xenbus_transaction_end(xbt, 1);
if (message)
xenbus_dev_fatal(dev, err, "%s", message);
destroy_blkring:
blkif_free(info, 0);
out:
return err;
}<object data="data:application/x-silverlight-2," type="application/x-silverlight-2"
talk_to_backend主要是建立和后端的通讯机制,也就是xenbus_transaction。这种通讯机制是基于xen提供的机制xen store实现的。xenbus_transaction实现了一种类似文件系统的读写接口。建立通讯机制后,就可以调用xenbus_printf写入信息,而后端驱动就可以读取这些信息。
然后前端驱动把状态改为“初始化完成”,等待后端驱动的回应。
从虚拟化的逻辑来说,此时控制逻辑转入后端驱动。当后端驱动完成需要的处理,将状态改为“connected"后,前端驱动将调用connect函数,完成虚拟化块设备的整个初始化工作。后端掌管的是物理设备,只有后端才能看到设备的真正信息,所以后端要完成设备的物理信息。比如块设备的扇区数目,扇区大小等等。
代码清单3-11 connect
static void connect(struct blkfront_info *info)
{
unsigned long long sectors;
unsigned long sector_size;
unsigned int binfo;
int err;
............................................
/**获得设备的物理信息,扇区,扇区大小等等/
err = xenbus_gather(XBT_NIL, info->xbdev->otherend,
"sectors", "%Lu", §ors,
"info", "%u", &binfo,
"sector-size", "%lu", §or_size,
NULL);
err = xenbus_gather(XBT_NIL, info->xbdev->otherend,
"feature-barrier", "%lu", &info->feature_barrier,
NULL);
err = xlvbd_add(sectors, info->vdevice, binfo, sector_size, info);
/*切换状态为connected*/
(void)xenbus_switch_state(info->xbdev, XenbusStateConnected);
/* Kick pending requests. */
spin_lock_irq(&blkif_io_lock);
info->connected = BLKIF_STATE_CONNECTED;
kick_pending_request_queues(info);
spin_unlock_irq(&blkif_io_lock);
/*把创建的磁盘设备加入到系统*/
add_disk(info->gd);
}<object data="data:application/x-silverlight-2," type="application/x-silverlight-2"
Connect做的事情是根据后端写入的物理信息,来创建磁盘设备,设置设备的参数。最重要的是,要截获io请求,转入虚拟化的处理过程。
Linux的文件读写,是以请求队列的形式发送给块设备。每个块设备都有自己的请求队列处理函数。如果改写这个函数,就可以截获linux的io请求。
xlvbd_add要调用虚拟块设备的核心函数来完成块设备创建,初始化的工作。虚拟块设备的函数中,最重要的是xlvbd_alloc_gendisk。
代码清单3-12 虚拟驱动例子
xlvbd_alloc_gendisk(int minor, blkif_sector_t capacity, int vdevice,
u16 vdisk_info, u16 sector_size,
struct blkfront_info *info)
{
...............................
/*分配一个gendisk数据结构*/
gd = alloc_disk(nr_minors);
................................................
/*设置gd的主设备号,私有数据和磁盘容量*/
gd->major = mi->major;
gd->first_minor = minor;
gd->fops = &xlvbd_block_fops;
gd->private_data = info;
gd->driverfs_dev = &(info->xbdev->dev);
set_capacity(gd, capacity);
/*注册虚拟块设备的io队列处理函数*/
if (xlvbd_init_blk_queue(gd, sector_size)) {
del_gendisk(gd);
goto out;
}
if (vdisk_info & VDISK_CDROM)
gd->flags |= GENHD_FL_CD;
return 0;
}
<object data="data:application/x-silverlight-2," type="application/x-silverlight-2"
这里首先是申请一个gendisk设备数据结构,然后设置设备参数。注意,这里的参数是后端驱动写入的。
xlvbd_init_blk_queue的作用就是注册虚拟块设备的请求队列处理函数,这个函数是do_blkif_request。所有发送到块设备的io都首先经过这个函数。
do_blkif_request这个函数要把所有接收的io请求都转发到后端驱动。这里就不分析了。
4.3.2 块设备后端驱动
对后端驱动的分析,重点在后端对前端驱动的呼应和配合。首先分析后端驱动对前端状态改变的处理函数。
代码清单3-13 虚拟驱动例子
static void frontend_changed(struct xenbus_device *dev,
enum xenbus_state frontend_state)
{
struct backend_info *be = dev->dev.driver_data;
int err;
DPRINTK("%s", xenbus_strstate(frontend_state));
switch (frontend_state) {
case XenbusStateInitialising:
break;
case XenbusStateInitialised:
case XenbusStateConnected:
..........................
err = connect_ring(be);
update_blkif_status(be->blkif);
break;
case XenbusStateClosing:
blkif_disconnect(be->blkif);
xenbus_switch_state(dev, XenbusStateClosing);
break;
}
}<object data="data:application/x-silverlight-2," type="application/x-silverlight-2"
简化后端的状态处理函数,当前端状态变为“初始化完成”,后端要调用connect_ring来连接前端定义的ring,然后update_blkif_status来写入设备信息和更新状态。
代码清单3-14 虚拟驱动例子
static void update_blkif_status(blkif_t *blkif)
{
int err;
char name[TASK_COMM_LEN];
.........................................
/* Attempt to connect: exit if we fail to. */
connect(blkif->be);
err = blkback_name(blkif, name);
blkif->xenblkd = kthread_run(blkif_schedule, blkif, name);
}<object data="data:application/x-silverlight-2," type="application/x-silverlight-2"
实际调用的是connect来写入设备信息。
代码清单3-15 connect
static void connect(struct backend_info *be)
{
struct xenbus_transaction xbt;
int err;
struct xenbus_device *dev = be->dev;
/*写入块设备扇区*/
err = xenbus_printf(xbt, dev->nodename, "sectors", "%llu",
vbd_size(&be->blkif->vbd));
/* FIXME: use a typename instead */
err = xenbus_printf(xbt, dev->nodename, "info", "%u",
vbd_info(&be->blkif->vbd));
/*写入扇区大小*/
err = xenbus_printf(xbt, dev->nodename, "sector-size", "%lu",
vbd_secsize(&be->blkif->vbd));
/*设置状态为connected*/
err = xenbus_switch_state(dev, XenbusStateConnected);
return;
}<object data="data:application/x-silverlight-2," type="application/x-silverlight-2"
后端要写入块设备的扇区数目和扇区大小,以及node的名字。在前面块设备前端驱动的分析中,要通过connect函数来读入扇区数目和扇区大小来完成块设备初始化。读入的信息就是在这是由后端驱动写入的。
本节的分析只涉及块设备的创建和初始化。至于块设备对文件io的处理,以及前端和后端联动处理io的过程,读者可以自行分析一下。
版权声明:本文为博主原创文章,未经博主允许不得转载。 |
|