天天看点

xen 添加网卡设备初步分析之 network-attach 流程分析1          流程分析

本文从 xm network-attach 命令着手,逐步分析xen 平台添加网卡的整体流程。需要说明的是,本文档只介绍针对在PV驱动环境下前后端网卡设备的添加流程。对于非PV驱动下的全虚拟化设备,不予关注。

1          流程分析

Xm network-attach 是添加网卡设备的命令,该命令是用python语言编写,相关源文件位于主机/usr/lib64/python2.6/site-packages/xen/目录下。具体在分析流程时,也是逐步在源文件中添加打印信息,以达到一步步揭开该命令的执行流程。

1.1         问题描述

由于相关内容涉及面广、牵扯面多,本文可能无法面面俱到,所以先给出一个中心点,即当时遇到的故障,基于这个问题,在解决此问题的基础上,以求相对全面的分析xen添加网卡设备的流程。

【故障】该故障会发生在迁移、虚拟机内部重启,外部reboot中。假设当前虚拟机有五块网卡,ID 分别是 0,1,2,3,4,如果在迁移前,手动将 ID 为2 (或者0-3)的网卡删除,迁移后再添加网卡就会发生报告“Device xx is already connected”的错误。此后如果继续添加网卡,又会添加成功。

1.2         命令流程

1.2.1          总体入口

命令流程都是通过逐步在python脚本中添加打印得出的。首先看如下代码:

xend/server/XMLRPCServer.py 文件中 XMLRPCServer类中代码:

try:

            while self.running:

                self.server.handle_request()

        finally:

            self.shutdown()

所有 xm 命令的入口都是 while 语句处,至于之前的一些初始化流程,那些过程属于xend的初始化流程,对于xend整体的消息流程框架,本文暂时不深入追究。针对xm network-attach 命令而言,最终执行命令的入口处位于:

xend/XendDomainInfo.py 文件XendDomainInfo类device_create函数中。

给出调用树关系如下:

XendDomainInfo.device_create (self, dev_config)

self._createDevice(dev_type, dev_config_dict) 

self._waitForDevice(dev_type, devid)

下面分别介绍上面几个主要函数。

1.2.2          创建设备

创建设备调用的函数如下:

    def _createDevice(self, deviceClass, devConfig):

        return self.getDeviceController(deviceClass).createDevice(devConfig)

查看xend/XendDomainInfo.py文件,无法简单得出self.getDeviceController(deviceClass)的结构。通过添加打印得出,self.getDeviceController 返回的类是xen.xend.server.netif.NetifController。在 tools/python/xen/xend/server/netif.py文件中,可以看到类class NetifController(DevController)的源代码。

显然,self.getDeviceController(deviceClass).createDevice(devConfig)即调用

NetifController. createDevice(devConfig)。由于NetifController类自己没有createDevice方法,根据面向对象的原则,其调用父类DevController的createDevice方法。

1.2.2.1         获取前后端信息

接着在DevController的createDevice方法中,看到如下代码:

def createDevice(self, config):

        """Trigger the creation of a device with the given configuration.

        @return The ID for the newly created device.

        """

        (devid, back, front) = self.getDeviceDetails(config)

self.getDeviceDetails获取前后端网卡的相关配置信息,这里需要暂时回到NetifController类中,因为DevController中的getDeviceDetails只是一个空接口,具体函数在子类NetifController中。

def getDeviceDetails(self, config):

        """@see DevController.getDeviceDetails"""

        script  = config.get('script', xoptions.get_vif_script())

      ……此处省略

        if not mac:

            raise VmError("MAC address not specified or generated.")

        devid = self.allocateDeviceID() --- 从xenstore获取网卡ID

        saveddevid = config.get('devid') – 从系统中获取网卡ID

        if saveddevid is not None:

            log.info("change devid %d -> int(%s)", devid, str(saveddevid))

            devid=int(saveddevid)

……此处省略

        return (devid, back, front)

对于前文所说的故障而言,这个地方是一个关键点。该函数主要在获取新增网卡设备的配置信息。红色部分的代码尤其关键,devid从 xenstore中获取一个网卡ID, saveddevid从系统配置中获取网卡ID,这个地方可能有点不太清楚,详细说明如下:

如果网卡是新增的,那么saveddevid 是 None;但如果是执行虚拟机内部重启、迁移等过程,可想而知,重启迁移等动作之前的网卡,在重启迁移后,网卡的配置信息是不会丢失的,这说明系统中会保存网卡的配置信息,诸如mac、id等信息,这些信息在重启迁移等动作之后不会丢失。上述变量saveddevid就是从系统中读取这类网卡的配置信息。如果saveddevid 非空,则不使用xenstore自动分配的id,而使用原有的配置ID;相反,如果saveddevid为空,则使用xenstore自动分配的id。到这里,逻辑上是没有任何问题的。

顺便说一下上述函数的返回值,根据打印信息得出:

devid 2 {'mac': '00:16:3e:6d:06:72', 'handle': '2', 'uuid': 'bec8d569-5594-7265-fcc8-9914283d95de', 'script': '/etc/xen/scripts/vif-bridge'} {'mac': '00:16:3e:6d:06:72', 'handle': '2'} 分别对应(devid, back, front)。

下面详细分析下xenstore自动获取网卡配置ID的函数。

1.2.2.1.1        Xenstore自动分配网卡id的机制

主要的函数就是上述代码中的allocateDeviceID()。此时再回到在DevController的allocateDeviceID()方法,因为NetifController类没有该方法,其调用父类DevController中的方法。

def allocateDeviceID(self):

        path = self.frontendMiscPath()

        return complete(path, self._allocateDeviceID)

complete 来自xen.xend.xenstore.xstransact 类,代码如下:

def complete(path, f):

    while True:

        t = xstransact(path)

        try:

            result = f(t)

            if t.commit():

                return result

        except:

            t.abort()

            raise

def _allocateDeviceID(self, t):

        result = t.read("nextDeviceID")

        if result:

            result = int(result)

        else:

            result = 0

        t.write("nextDeviceID", str(result + 1))

        return result

结合上面三段代码看,最后分配的函数实际是在 _allocateDeviceID 函数中。该函数调用t.read从xenstore中读取 nextDeviceID的当前值,该值就是xenstore 自动分配的网卡设备ID。同时,通过t.write将加一后的nextDeviceID值回写到xenstore中。到这里我们基本清楚xenstore网卡ID的分配机制,但对于xenstore的内部分配尚有不明之处,对此,我们继续深入,分析下xenstore里面的分配细节及相关通信机制。

1.2.2.1.2        Xenstore 与ID分配相关的内部通讯

我们从xen.xend.xenstore.xstransact类开始,关注下t.read与t.write的大致机制。由于xenstore本身的复杂性,本文不会对该内容面面俱到。详细请参考《xen虚拟化技术》一书中的第8.5节。

在xenstore.py 文件中查看 xstransact的调用处:

def complete(path, f):

    while True:

        t = xstransact(path) --- 调用 transaction_start

        try:

            result = f(t)  – 调用主体功能,此处是_allocateDeviceID

            if t.commit(): -- 调用 transaction_end

                return result

        except:

            t.abort()

            raise

在每次调用complete时,都会调用xstransact,该类的构造函数如下:

def __init__(self, path = ""):

        self.in_transaction = False # Set this temporarily -- if this

                                    # constructor fails, then we need to

                                    # protect __del__.

        assert path is not None

        self.path = path.rstrip("/")

        self.transaction = xshandle().transaction_start()

        self.in_transaction = True

上述代码中的xshandle()来自xen.xend.xenstore.xsutil,xshandle()函数定义如下:

def xshandle():

    global xs_handle, xs_lock

    if not xs_handle:

        xs_lock.acquire()

        if not xs_handle:

            xs_handle = xen.lowlevel.xs.xs()

        xs_lock.release()

    return xs_handle

根据xshandle的代码结构看出,其中的xs_handle是一个全局变量,非常类似一个“单例模式”,并不是每次都申请都初始化,相反,它初始化一次,却会一直存在。进一步到xen.lowlevel.xs.xs()代码中,此时已经到了python代码与C代码的结合之处,可以看出,这里已经是通过python来调用C的功能,完成xenstore的相关功能。

xenstore本质上是一个文件系统,所有对xenstore的操作与对linux下的文件操作类似,包括读、写、创建目录、设置文件权限等,对应的消息类型如下:(来自xen.lowlevel.xs.xs):

static PyMethodDef xshandle_methods[] = {

    XSPY_METH(read,              METH_VARARGS),

    XSPY_METH(write,             METH_VARARGS),

    XSPY_METH(ls,                METH_VARARGS),

    XSPY_METH(mkdir,             METH_VARARGS),

    XSPY_METH(rm,                METH_VARARGS),

    XSPY_METH(get_permissions,   METH_VARARGS),

    XSPY_METH(set_permissions,   METH_VARARGS),

    XSPY_METH(watch,             METH_VARARGS),

    XSPY_METH(read_watch,        METH_NOARGS),

    XSPY_METH(unwatch,           METH_VARARGS),

    XSPY_METH(transaction_start, METH_NOARGS),

    XSPY_METH(transaction_end,   METH_VARARGS | METH_KEYWORDS),

    XSPY_METH(introduce_domain,  METH_VARARGS),

    XSPY_METH(set_target,        METH_VARARGS),

    XSPY_METH(resume_domain,     METH_VARARGS),

    XSPY_METH(release_domain,    METH_VARARGS),

    XSPY_METH(close,             METH_NOARGS),

    XSPY_METH(get_domain_path,   METH_VARARGS),

    { NULL },

};

xenstore的消息通讯机制依旧是基于环,详细原理见《xen虚拟化技术》。

对于 t.read,t.write,主要功能代码如下:

def _read(self, key):

        path = self.prependPath(key)

        return xshandle().read(self.transaction, path)

    def _write(self, key, data):

        path = self.prependPath(key)

        xshandle().write(self.transaction, path, data)

通过xshandle()的read和write方法,最终调用的还是xen.lowlevel.xs.xs文件中对应的读写方法。通过添加打印,我们得出path的信息:

path /local/domain/55/device-misc/vif/nextDeviceID

这与前文所述,xenstore是一个文件系统吻合起来,path就是读写的路径,通过执行xshandle().read 或者xshandle().write 方法对文件中的路径进行读写,限于篇幅,本文不再继续罗列,读者可以继续往下深入分析,建议阅读《xen虚拟化技术》相关章节。

实际上到这里,还没有真正的创建设备,所以xenstore只是一个中间的交互及存储通信通道,它本身只起到沟通和管理的作用。

这里给出一个结论:此时通过向xenstore中获取nextDeviceID值,或者写入nextDeviceID的值,不会对实际设备造成干扰,因为现在还处于分配设备信息的前期,根本还不存在实际设备。nextDeviceID的值仅仅决定下次新增网卡的ID值,该值对于网卡设备而言,在同一台虚拟机上必须是唯一的。

1.2.2.2         与前后端交互

回到DevController类的createDevice函数中,接下来还是通过xenstore进行交互,向PV驱动的前后端发送消息。

首先获取前后端的消息路径:

(backpath, frontpath) = self.addStoreEntries(config, devid, back,

                                                     front)

通过打印得出:

backpath /local/domain/0/backend/vif/55/2

frontpath /local/domain/55/device/vif/2

在通过xenstore写入前端之前,这里先进行了一次检查:

t = xstransact()

            try:

                if devid in self.deviceIDs(t):

                    if 'dev' in back:

                        dev_str = '%s (%d, %s)' % (back['dev'], devid,

                                                   self.deviceClass)

                    else:

                        dev_str = '%s (%s)' % (devid, self.deviceClass)

                    raise VmError("Device %s is already connected." % dev_str)

此时就与故障中的信息对应起来,故障发生时打印出的错误,就是在这里抛出的。

在新添加网卡时,self.deviceIDs(t)从xenstore中获取了原有网卡的ID值,如果发现目前分配的网卡ID已经存在于xenstore中,则抛出错误。还是假设故障中的场景。综合上面的分析,给出故障的原因:

xenstore 里面有变量/local/domain/domid/device-misc/vif/nextDeviceID,如果网卡原来没有ID,该值决定下次分配网卡的ID。如果网卡原来已经有ID,则使用网卡原来的ID。当每增加一块网卡,nextDeviceID均会加1。迁移,内部重启等动作,虚拟机的网卡ID是不会丢失的,即都会沿用原来的网卡ID。

当前虚拟机有五块网卡,ID 分别是 0,1,2,3,4 此时nextDeviceID = 5,即下次如果新添加网卡,网卡的ID将是5,同时会将nextDeviceID加1。

当虚拟机发生迁移、重启等动作后,nextDeviceID会被置0,网卡设备会重新注册到虚拟机,但是会沿用网卡原来的ID。

如果在迁移前,手动将 ID 为2 (或者0-3)的网卡删除,迁移后再添加网卡就会发生故障。假设迁移前删除ID为2 的网卡,此时剩余网卡ID是 0,1,3,4。迁移后,nextDeviceID=0,此时0,1,3,4的网卡重新注册到虚拟机,0,1,3,4网卡依次注册后,nextDeviceID的值依次是1,2,3,4,即nextDeviceID总是从0开始,每次加1的增长。这个过程不会有问题,问题会发生在下一次添加网卡的过程。这个过程需要明白,0,1,3,4网卡重新注册后,他们的ID是0,1,3,4,而不是0,1,2,3,因为它们原来是有ID的,所以不使用nextDeviceID的当前值。

当用户下次添加新网卡时,由于新网卡显然是不存在ID的,此时nextDeviceID决定网卡的ID,因为当前nextDeviceID=4,所以分配给新网卡的ID是4,此后更新nextDeviceID=5。此时问题来了,因为ID为4的网卡已经在系统中,故发生ID冲突,导致网卡添加失败。这就是迁移后第一次添加网卡失败的原因。

用户第二次添加网卡时,上次已经更新nextDeviceID=5,此时分配给新网卡的ID是5,因为当前nextDeviceID=5,此后更新nextDeviceID=6。由于系统中不存在ID为5的网卡,所以添加成功。这就是第二次添加网卡成功的原因。

对于上述结论,有几点需要说明:

第一:虚拟机内部关闭或者迁移等动作后,nextDeviceID的值会清0,这是通过实验和加打印得出的结论,具体代码点没有深入追究。

第二:虚拟机内部关闭或者迁移等动作后,网卡会依次重新向xenstore中注册,原有网卡的配置信息不会丢失。

第三: 一旦nextDeviceID被分配出去后,如果设备最终没有创建成功,代码中不会对nextDeviceID的值进行回滚处理,目前未发现该机制有问题。

xenstore得知前后端路径后,加上之前获取的网卡的配置信息,将相应的信息写入到前后端中。具体代码不再列出。

1.2.3          等待设备

可以推测,xenstore通过向前后端发送消息后,前端和后端会根据驱动的相关功能,完成具体设备的创建,这部分功能体现在PV驱动里面,xen中所做的就是发送消息给前后端,以及等待他们的是否成功的响应。有关PV驱动前后端实际虚拟设备的创建,将在单独的文档中进行说明。

此时回到xend/XendDomainInfo.py 文件XendDomainInfo类device_create函数的_waitForDevice调用中。由于有了前面的介绍,我们不再走弯路,waitForDevice直接最终位于DevController 类中。

waitForDevice调用waitForBackend函数,waitForBackend函数中执行回调函数hotplugStatusCallback,并设置超时时间,在超时规定的时间内,观察前后端设备是否创建并连接成功,并将最后结果返回,最后根据返回结果做出相应的处理。

【故障解决方案】:本文就不给出具体解决方案。理解该错误的原理后,解决的办法有许多。一个思路是:每次获取自动分别的ID后,可以与旧网卡的ID进行比较,如果前者要小,则继续获取,当然这样也会有其他的缺点,不再进一步描述。

后话:

通过写此文档,我发现对于添加网卡设备而言,xen中所做的事情其实不多,但是并不容易描述,因为这小块功能起点于一个比较庞大的框架之上,牵扯面广。后面真正需要花功夫啃的还是xenstore的整体通信机制,以及PV驱动前后端的虚拟设备创建过程。

继续阅读