本文從 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驅動前後端的虛拟裝置建立過程。