Libvirt结构及部分源代码分析

这篇文章前序部分来自于Libvirt的官方主页的翻译,最近做Libvirt集成方面的工作,奈何源码中的函数太多、各种结构体,确实比较乱,查过来查过去还是官方给的清楚一些,这里就翻译过来,来加强下自己的理解。
首先说下这篇文章不是介绍libvirt的架构的,这网上有很多,而是结合libvirt1.2.2的源码进行的分析。可能看起来略显枯燥,而且需要看的时候结合实际源码走,毕竟文章内容有限,只截取了部分源码。

libvirt中的关键术语

为了避免模糊的概念,这里是Libvirt中使用的几个特别的概念:

Node : 一个node代表实际的一个物理机器
hypervisor : 一个hypervisor指的是位于一个node物理机上的软件层,它可以在一个node上虚拟化一系列的不同配置的虚拟机
domain : 一个domian指的是运行在由hypervisor提供的一个虚拟机上的操作系统实例。

所以Libvirt功能就是:提供一个通用的、稳定的软件层来管理一个node上的domains,可能的话也会提供远程管理(libvirtd存在)

Libvirt中的API实现

Object Exposed 对象暴露

libvirt API 就是被设计用来暴露所有必须的资源来管理对目前主流的操作系统的支持。第一个通过API操作的对象就是virConnectPtr ,这个是用来连接一个hypervisor的对象(结构体指针)。任何使用libvirt的应用程序基本第一步都是通过API来调用virConnectOpen之类的函数。我们会看到这些函数都会有一个name参数,实际上是 connection URI,这个是来选择要打开的hypervisor的。一个URI能够允许远程连接,并 且可以在不同的hypervisors之间选择(意思就是通过不同的URI指定来选择不同的node上的hypervisor)。比如,在同一个节点的Linux系统上可能同时使用KVM 和 LinuxContainers,NULL 参数表示选择默认的hypervisor,但是这并不是明智的选择。

在虚拟机管理中,hypervisor用于提供Domian所需的各种资源,以及管理Domian的状态。为了支持不同的hypervisors,libvirt实现了基于驱动的架构,它允许使用一个通用的API来服务基于不同hypervisor的虚拟机。这也就意味着一些类型的hypervisor独有的功能是没法通过API来实现的。而这部分就会在virConnectPtr->privateData中来单独处理。所以就会有下面的流程:

给定特定hypervisor的URI——>Libvirt找到特定的hypervisor,把API指向到该驱动程序(实际为驱动注册过程)——->通用的API执行时调用选定的驱动。

Test伪驱动分析

我们结合Libvirt源码中的$PROJECT/src/test伪驱动进行分析。如图2:
libvirt-arch
该图表示了libvirt的结构和实际对应的源码的关系。
_virDomain表示指向活跃的或者定义的Domain的指针,其中包含了该Domain所属的Hypervisor的连接virConnectPtr,该变量原型为_virConnect,

这里是$PROJECT/example/hellolibvirt的执行跟踪。我们以./hellolibvirt test:///default执行过程为例。

1、virConnectOpen ——> do_open

uri通过main(argv)传递,为test:///default
conn = virConnectOpen(uri);或者virConnectOpenAuth(const char *name,...),获得对指定hypervisor的连接。(函数在libvirt.c中)
在virConnectOpenAuth调用了do_open()

1
ret = do_open(name, auth, flags);

do_open在libvirt.c中定义,返回virConnectPtr

2、do_open ——>virGetConnect

在do_open中,做了两个重要操作,第一个是ret = virGetConnect()
virGetConnectdatatype.c中实现,主要对datatypes进行初始化,进行空间分配。我们看下datatype.c中的主要代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
VIR_ONCE_GLOBAL_INIT(virDataTypes)

/**
* virGetConnect:
*
* Allocates a new hypervisor connection structure
*
* Returns a new pointer or NULL in case of error.
*/

virConnectPtr
virGetConnect(void)
{

virConnectPtr ret;

if (virDataTypesInitialize() < 0)
return NULL;

if (!(ret = virObjectNew(virConnectClass)))
return NULL;

if (!(ret->closeCallback = virObjectNew(virConnectCloseCallbackDataClass)))
goto error;

if (virMutexInit(&ret->lock) < 0)
goto error;

return ret;

error:
error:
virObjectUnref(ret);
return NULL;
}

virDataTypesInitiialize()这里第一句可能到代码中找不到,这属于宏连接MACROA ## MACROB,满足这个在编译过程中会动态创建。VIR_ONCE_GLOBAL_INIT(virDataTypes)是最最原始的定义。在src/util/virthread.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

# define VIR_ONCE_GLOBAL_INIT(classname) \
static virOnceControl classname ## OnceControl = VIR_ONCE_CONTROL_INITIALIZER; \
static virErrorPtr classname ## OnceError = NULL; \
\
static void classname ## Once(void) \
{ \
if (classname ## OnceInit() < 0) \
classname ## OnceError = virSaveLastError(); \
} \
\
static int classname ## Initialize(void) \
{ \
if (virOnce(&classname ## OnceControl, classname ## Once) < 0) \
return -1; \
\
if (classname ## OnceError) { \
virSetError(classname ## OnceError); \
return -1; \
} \
\
return 0; \
}

#endif

virOnce(classname##OnceControl,classname##Once),同样是组合宏的定义,所以virDataTypesInitialize()就可以找到了。然后在virDataTypesOnceInit中做了最终的初始化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
virDataTypesOnceInit(void)
{
#define DECLARE_CLASS_COMMON(basename, parent) \
if (!(basename ## Class = virClassNew(parent, \
#basename, \
sizeof(basename), \
basename ## Dispose))) \
return -1;
#define DECLARE_CLASS(basename) \
DECLARE_CLASS_COMMON(basename, virClassForObject())
#define DECLARE_CLASS_LOCKABLE(basename) \
DECLARE_CLASS_COMMON(basename, virClassForObjectLockable())

DECLARE_CLASS(virConnect);
DECLARE_CLASS_LOCKABLE(virConnectCloseCallbackData);
DECLARE_CLASS(virDomain);
DECLARE_CLASS(virDomainSnapshot);
DECLARE_CLASS(virInterface);
DECLARE_CLASS(virNetwork);
DECLARE_CLASS(virNodeDevice);
DECLARE_CLASS(virNWFilter);
DECLARE_CLASS(virSecret);
DECLARE_CLASS(virStream);
DECLARE_CLASS(virStorageVol);
DECLARE_CLASS(virStoragePool);

#undef DECLARE_CLASS_COMMON
#undef DECLARE_CLASS_LOCKABLE
#undef DECLARE_CLASS

return 0;
}

这里virClassNew实际是最终操作,就对dateTypes.h中定义了virConnectvirDomian等各类结构体进行了初始化,同时把不同结构体的dispose释放函数的实现赋值给了virClassPtr。

这是我们拿到了virConnectPtr的指针并赋值给ret。

do_open中URI分析

接下来就是do_open的第二个重要操作,将传递进来的URI进行分析,uri是通过virURIParse对传递进来的test:///default解析得到的,其中uri->schemeuri->server会分别赋值,最终结果返回赋值给ret->uri,然后对virDriverTab驱动登记表中的每个驱动进行检查,找到匹配输入地址的正确的驱动,把该驱动赋值给ret->driver。do_open中主要代码:

1
2
ret->driver = virDriverTab[i];
res = virDriverTab[i]->connectOpen(ret, auth, flags);

其他InterfaceDriverstorageDriver处理方式类似。

这样virDriver就随着ret的返回被应用程序得到了,这样我们就通过一系列的空间分配、URI匹配拿到了virConnectPtr,这样之后的操作都可以通过virConnectPtr来操作Domain了,因为virConnectPtr中包含了具体的驱动程序及Domain相关的信息,但是这里我们好像只是的到了virDriver,似乎与实际驱动没什么关联,那么在哪里实现的统一接口与实际驱动的关系呢?

这里需要说明一点:virDriver对于任何hypervisor的驱动程序都是一样都,不一样在于virDriver中的每个操作函数的具体实现是需要每个hypervisor驱动单独的实现.

所以我们接下来看testDriver的实现过程。

testDriver实现

为了能够与上面联系起来,我们先从驱动的注册开始:
驱动的注册函数位域src/libvirt.c中,参数为即将注册的驱动。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int
virRegisterDriver(virDriverPtr driver)
{

VIR_DEBUG("driver=%p name=%s", driver,
driver ? NULLSTR(driver->name) : "(null)");

virCheckNonNullArgReturn(driver, -1);
virDriverCheckTabMaxReturn(virDriverTabCount, -1);

VIR_DEBUG("registering %s as driver %d",
driver->name, virDriverTabCount);

virDriverTab[virDriverTabCount] = driver;
return virDriverTabCount++;
}

注册成功后virDriverTab中会增加一项,同时统计数自增。

下面是testDriver的注册过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int
testRegister(void)
{

if (virRegisterDriver(&testDriver) < 0)
return -1;
if (virRegisterNetworkDriver(&testNetworkDriver) < 0)
return -1;
if (virRegisterInterfaceDriver(&testInterfaceDriver) < 0)
return -1;
if (virRegisterStorageDriver(&testStorageDriver) < 0)
return -1;
if (virRegisterNodeDeviceDriver(&testNodeDeviceDriver) < 0)
return -1;
if (virRegisterSecretDriver(&testSecretDriver) < 0)
return -1;
if (virRegisterNWFilterDriver(&testNWFilterDriver) < 0)
return -1;

return 0;
}

这段代码位于$PROJECT/src/test/test_driver.c中,将testDriver注册到系统中,也就是添加到virDriverTab中(位于src/libvirt.c的全局变量,存放已注册的各类hypervisor驱动)。
我们再看下注册的virDriver driver是什么样的?

1
2
3
4
typedef virDrvOpenStatus
(*virDrvConnectOpen)(virConnectPtr conn,
virConnectAuthPtr auth,
unsigned int flags)
;

src/driver.h中,我们会看到virDriver中的所有变量都是一个个的函数指针定义,没有明确的时候,所以这就对于不同的hypervisor集成扩展提供了灵活的渠道,我们需要做的就是对不同的驱动,针对每个函数指针定义来实现具体过程,最终把每个实现的函数赋值给这些变量。

对于test驱动来说

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
static virDriver testDriver = {
.no = VIR_DRV_TEST,
.name = "Test",
.connectOpen = testConnectOpen, /* 0.1.1 */
.connectClose = testConnectClose, /* 0.1.1 */
.connectGetVersion = testConnectGetVersion, /* 0.1.1 */
.connectGetHostname = testConnectGetHostname, /* 0.6.3 */
.connectGetMaxVcpus = testConnectGetMaxVcpus, /* 0.3.2 */
.nodeGetInfo = testNodeGetInfo, /* 0.1.1 */
.connectGetCapabilities = testConnectGetCapabilities, /* 0.2.1 */
.connectListDomains = testConnectListDomains, /* 0.1.1 */
.connectNumOfDomains = testConnectNumOfDomains, /* 0.1.1 */
.connectListAllDomains = testConnectListAllDomains, /* 0.9.13 */
.domainCreateXML = testDomainCreateXML, /* 0.1.4 */
.domainLookupByID = testDomainLookupByID, /* 0.1.1 */
.domainLookupByUUID = testDomainLookupByUUID, /* 0.1.1 */
.domainLookupByName = testDomainLookupByName, /* 0.1.1 */
.domainSuspend = testDomainSuspend, /* 0.1.1 */
.domainResume = testDomainResume, /* 0.1.1 */
...............................
...............................
}

这里其实就是对virDriver testDriver结构体中的每一个抽象操作函数的一个赋值过程,采用函数指针赋值形式。也就是说如果应用程序选中了testDriver作为使用的hypervisor,那么对于_virDriver中各个结构体内部变量的调用操作最终都会归结到testConnectOpen上来,其他类似。

还是以hellolibvirt.c代码为例,该代码中调用了virConnectGetVersion(conn,&hvVer),获取版本,实际最终操作为testConnectGetVersion的实现,而我们对Domian的各种操作的实现也就是在这些驱动文件的每个函数中。

那么testDriver是什么时候进行注册的呢?
我们再回头看下libvirt.c的代码,在头文件下的第一句就告诉了我们test是在哪里和COMMON API联系起来的

1
2
3
#ifdef WITH_TEST
# include "test/test_driver.h"
#endif

以及libvirt的全局初始化函数virGlobalInit中:

1
2
3
4
# ifdef WITH_TEST
if (testRegister() == -1)
goto error;
#endif

挺麻烦的哈,似乎问题又来了,那virGlobalInit又是谁在什么时候调用的呢?我们看下libvirt.c中的virInitialize函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
* virInitialize:
*
* Initialize the library.
*
* This method is invoked automatically by any of the virConnectOpen() API
* calls, and by virGetVersion(). Since release 1.0.0, there is no need to
* call this method even in a multithreaded application, since
* initialization is performed in a thread safe manner; but applications
* using an older version of the library should manually call this before
* setting up competing threads that attempt virConnectOpen in parallel.
*
* The only other time it would be necessary to call virInitialize is if the
* application did not invoke virConnectOpen as its first API call, such
* as when calling virEventRegisterImpl() before setting up connections,
* or when using virSetErrorFunc() to alter error reporting of the first
* connection attempt.
*
* Returns 0 in case of success, -1 in case of error
*/


int
virInitialize(void)
{

if (virOnce(&virGlobalOnce, virGlobalInit) < 0)
return -1;

if (virGlobalError)
return -1;
return 0;
}

注释里面提到了virInitialize函数在调用任意一个virConnectOpen函数的时候被调用,在1.0.0之后不会直接被应用程序调用,这时候前前后后连接上来吧,我们现在把整个过程走一下:

1、应用程序调用virConnectOpen,传入URI
2、virConnectOpen首先调用virInitialize函数完成驱动注册及其他初始化任务
3、virConnectOpen然后调用do_open,首先完成virConnectPtr的一个变量空间初始化,然后对来获取分析传入的URI并在注册驱动登记表中找到合适的驱动,并赋给virConnectPtr中的virDriver驱动。
4、通过驱动对Domain进行操作。

要点补充

libvirt暴露的函数(COMMON API)分类

在libvirt对外暴露的函数中,主要包含两部分:

1、对libvirt的一些操作,比如获取libvirt的版本virGetVersion,创建与hypervisor连接等函数,这些函数是不需要传入virConnect的。
2、对获取连接后的hypervisor及Domina的操作,这些函数我们会看到基本都有virConnectPtr参数传入或者virDomain参数传入,比如获取使用的黑一片而visor的版本virConnectGetVersion(virConnectPtr conn,unsigned long *hvVer),这些函数内部实际上使通过conn具化的驱动来操作的。

搞两个容易混淆的:
【1】virGetVersion获取libvirt版本virConnectGetVersion获取连接的hypervisor的版本
【2】virConnectOpen获取与hypervisor的连接connectOpen获取与驱动的连接(do_open中均有调用),拿到与hypervisor连接后还需要判断能不能正常初始化驱动,而我们对与不同hypervisor的URI的参数匹配都是在驱动中的connectOpen中分析的。
现在我们可以完善下开始提到的那副图:
libvirt-arch2

virDomain 和 virDomainObj区别

我们先看下virDomainObjList的定义吧

1
2
3
4
5
6
7
struct _virDomainObjList {
virObjectLockable parent;

/* uuid string -> virDomainObj mapping
* for O(1), lockless lookup-by-uuid */

virHashTable *objs;
};

virDomainObjList中的virHashTable 描述了virConnectPtr->UUIDvirDomainObj一一对应关系。而_testConn中的定义中有virDomainObjList类型的成员变量

1
virDomainObjListPtr domains;

从上图可以看到,virDomain中包含了一个UUID,唯一标识了左侧的一个Domain。而每个virDomainPtr中包含的virConnectPtr都是一样的,并且virConnectPtr中又通过privateData存储了每个virDomainObj的信息。

概况的讲,virDomain更多的描述的是每个Domain的连接属性、驱动信息。而virDomainObj更多的使描述每个运行的Domain的状态、配置信息。

virConnectPtr中的privateData

现在我们来说一说privateData这个结构体成员,这个看似平平,却是不可或缺的一部分,它没有指定什么类型,而是以一个空指针的形式存在,这样关于privateData的数据就比较灵活了,它使对libvirt通用函数的一个抽象补充,内容由集成hypervisor驱动的开发者来定。它可以存放驱动需要使用的数据。

1
2
3
4
5
/* Private data pointer which can be used by driver and
* network driver as they wish.
* NB: 'private' is a reserved word in C++.
*/

void * privateData;

test_driver.c的实现为例,_testConn最终就是赋值给了conn->privateData,它就包含了驱动需要使用的各类数据。
_testConn的定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
struct _testConn {
virMutex lock;

char *path;
int nextDomID;
virCapsPtr caps;
virDomainXMLOptionPtr xmlopt;
virNodeInfo nodeInfo;
virDomainObjListPtr domains;
virNetworkObjList networks;
virInterfaceObjList ifaces;
bool transaction_running;
virInterfaceObjList backupIfaces;
virStoragePoolObjList pools;
virNodeDeviceObjList devs;
int numCells;
testCell cells[MAX_CELLS];
size_t numAuths;
testAuthPtr auths;
virObjectEventStatePtr eventState;
};
typedef struct _testConn testConn;
typedef struct _testConn *testConnPtr;

其中包含了驱动的互斥保护锁、驱动的路径、下一个Doamin的ID、位于的node的信息、连接驱动的认证信息、node节点上已有的Domians等信息。

我们看下testOpenDefault函数

1
2
3
4
5
6
7
8
9
...
if (defaultConnections++) {
conn->privateData = &defaultConn;
virMutexUnlock(&defaultLock);
return VIR_DRV_OPEN_SUCCESS;
}
....
conn->privateData = privconn;
...

如果defaultConn操作正常的的话,就直接返回,否则把刚初始化的privConn进行赋值。(两个均为_testConn类型)。

那么驱动使如何使用这些数据呢?我们看下test_driver.c中的testDomainCreateWithFlags函数实现,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
static int testDomainCreateWithFlags(virDomainPtr domain, unsigned int flags) {
testConnPtr privconn = domain->conn->privateData;
virDomainObjPtr privdom;
virObjectEventPtr event = NULL;
int ret = -1;

virCheckFlags(0, -1);

testDriverLock(privconn);
privdom = virDomainObjListFindByName(privconn->domains,
domain->name);
if (privdom == NULL) {
virReportError(VIR_ERR_INVALID_ARG, __FUNCTION__);
goto cleanup;
}
if (virDomainObjGetState(privdom, NULL) != VIR_DOMAIN_SHUTOFF) {
virReportError(VIR_ERR_INTERNAL_ERROR,
_("Domain '%s' is already running"), domain->name);
goto cleanup;
}
if (testDomainStartState(privconn, privdom,
VIR_DOMAIN_RUNNING_BOOTED) < 0)
goto cleanup;
domain->id = privdom->def->id;
event = virDomainEventLifecycleNewFromObj(privdom,
VIR_DOMAIN_EVENT_STARTED,
VIR_DOMAIN_EVENT_STARTED_BOOTED);
ret = 0;
cleanup:
if (privdom)
virObjectUnlock(privdom);
if (event)
testObjectEventQueue(privconn, event);
testDriverUnlock(privconn);
return ret;
}

其中,根据传入的virDomainPtr domain我们可以得到连接指针virConnectPtr,继而拿到privateData,也就是代码中的privconn。在privateData中记录的virDomainObjPtr中来找到对应的virDomainObj,然后在执行创建过程。


到这里为止,我们对整个libvirt应用程序调用的流程应该有个直观的认识吧,虽然前前后后写了差不多三天,修修补补,总算让文章中的东西都有据可循,但是还是写的有些乱,如果有哪里叙述不正确的地方还请谅解。