libvir服务机制
前言
libvirt服务机制是一个复杂的结构,里面包含了event事件,rpc网络通信,线程池以及相关的job机制,哪一个单拿出来都是一个复杂的模型结构,更何况libvirt服务是这几个机制之间相互协作的复杂结构,因此更难以说明清楚,但是万变不离其宗,本质上还是service创建fd并相互发送消息,所以本篇文章的目标是在代码端说明libvirt服务通信的实现,着重是代码实现。为此我们按模块进行对结构进行拆解,层层递进,以了解基础原件为开始慢慢的再此基础上讲解会用到该基础模块的其他模块,即以逻辑顺序从底层往上层讲解,这样有利于整理的逻辑理解,以及各个模块之间的关系以及相互作用。
1模块讲解
-
- libvirt事件机制
1.1.1 事件操作函数初始化
事件机制是libvirt服务中创建监督事件的一种基础原件,本身并未实现具体的功能,需要结合具体的fd来实现对特定事件的监督,其本质是glib事件循环机制的包装,让事件监督的回调函数更一般化,即为一个通用的中间函数用于调用参数中的回调函数。
为此在libvirt中设计了两类事件处理函数接口。1:普通事件处理函数接口;2:超时事件处理函数接口。都包括了增加,删除,更新相应事件的接口,即总共6个接口。
首先通过virEventGLibRegister->virEventGLibRegister->virEventRegisterImpl初始化全集变量回调函数。
对应的函数virEventGLibHandleAdd、virEventGLibHandleUpdate、virEventGLibHandleRemove、virEventGLibTimeoutAdd、virEventGLibTimeoutUpdate、virEventGLibTimeoutRemove初始化为全局变量addHandleImpl、updateHandleImpl、removeHandleImpl、addTimeoutImpl、updateTimeoutImpl、removeTimeoutImpl。这些全局函数指针变量用于被通用的接口virEventAddHandle、virEventUpdateHandle、virEventRemoveHandle、virEventAddTimeout、virEventUpdateTimeout、virEventRemoveTimeout调用,换言之这些接口调用的时间函数就是初始化的函数。
这是一种初始化参数的方式,只要知道最终调用的是谁就好。接下来就看看这些函数到底是干了什么。
1.1.2 事件操作函数
事件操作函数顾名思义是操作监督操作事件的函数,有添加、删除、更新,本质是glib事件机制的封装。我们着重分析一下添加事件函数,以及添加超时事件函数,其他的就根据glib事件处理机制简略分析。
事件添加:
首先要设置要监督的事件cont,为四类G_IO_IN、G_IO_OUT、G_IO_ERR、G_IO_HUP。很明显也就是四类fd事件用于通信用的。
接着创建一个事件回调函数用的参数data,由前面所说该参数实际就是真正的回调函数所在的对象。data里有watch表示在hanldes中的序列便于查找,fd为事件监督的fd,cb为事件相应时实际调用的回调函数,opaque为cb的参数,ff为释放函数。然后由virEventGLibAddSocketWatch创建一个glib source再赋值到data中。到这里我们可以明显的感知到,data对象包含了所有的创建事件的元素,只要根据watch在全局变量handles中找到data,我们可以对事件做相应的处理。最后把data在handles中的次序watch返回,即把data所在的顺序传递出去,供其他元素查找。
总结一个下添加事件做的几件事:
1:创建事件source,监督fd特定事件;
2:把传入的回调函数,回调函数对应参数,fd,创建的source,监督的事件,创建data在handles中的次序,释放函数,等整合到data中。
3:返回创建data在hanldes中的次序。
事件删除:
事件删除的逻辑为:销毁data中的source,把data从hanldes中删除。
事件更新的逻辑为:事件更新目的是对同一个fd监督其他的事件,为此销毁data中的source,依据新的cout创建新的source赋值到data中。
事件超时机制与事件机制类似,不再累述。
总结:libvirt事件机制本质是glib source机制,为了方便调用使用了类似于父类的方式同一函数添加函数接口根据不同的data调用对应的回调函数。事件机制实际也就是个添加方便的glib事件机制。
-
- libvirt通信创建
libvirt服务涵盖的通信方式有很多,有tcp、udp、ssh、tls、unix等。libvirt要为各种方式创建相应的服务为了后续客户端的连接。虽然有如此多中通信方式在整体看来即是回调函数的不同,为此我们就比较熟悉的tcp为例来分析libvirt服务端创建于client端连接的流程。
-
-
- libvirt tcp服务创建
-
我们都知道服务创建分为两层:一层是socket listen层,一层是socket accept层。同样的libvirt服务创建主要是对connet对应fd设置回调函数与accept对应fd的回调函数。
服务创建:srv
创建服务实际就是创建一个线程池,后面会被用到的函数是:virNetServerHandleJob该函数服务处理的核心函数:
线程池实际就是创建了指定多个线程执行不同的任务:
创建了gain个线程执行virThreadPoolWorker
virThreadHelper函数里主要是设置一些线程属性而已
可以看到每个线程只是while循环然后阻塞等待信号,当信号过来进行唤醒时执行(pool->jobFunc)(job->data, pool->jobOpaque);
而(pool->jobFunc)(job->data, pool->jobOpaque)为virNetServerHandleJob
可以看到核心函数为virNetServerProcessMsg
virNetServerClientSendMessage就不用看了,看看virNetServerProgramDispatchCall吧
这个函数很长但是核心不多:
最主要的目的是找到对应的回调函数
dispatcher = virNetServerProgramGetProc(prog, msg->header.proc);
执行相应的回调函数
rv = (dispatcher->func)(server, client, msg, &rerr, arg, ret);
比如经典的函数migrate都是通过call函数通过客户端往服务端发送信息在virNetServerProgram对象中找到相应的类似于qemuxxx的回调函数然后执行。
TCP服务创建:
函数调用关系:daemonSetupNetworking->virNetServerAddServiceTCP
创建的为virNetServerService类型对象,该对象为virNetServer类型对象的成员变量,且该成员变量是virNetServerService类型的数组,因此srv是一个总的服务描述,而每中类型的服务所创建的src都为其中的一个元素。
该函数为两个模块return 0之前的是重启模块,之后的是新创建服务的模块,区别是重启服务要连接已有的fd,而新创建的只需创建等待连接即可。virNetServerAddServiceActivation函数中包含的就是virNetServerServiceNewTCPs和virNetServerAddService逻辑因此我们以这两个函数为着重介绍对象。
- virNetServerServiceNewTCPs核心目的是创建socket对象sockt,核心函数是socket,bind, virNetSocketNew, virNetSocketNew函数用fd创建cocket对象。
- virNetServerServiceNewSocket函数核心目标是listen fd并把fd添加到事件中监督fd,以及设置listen后的回调函数。核心函数是virNetSocketListen、virNetSocketAddIOCallback
由上面两个函数具体实现可知当sock->fd产生事件的时候最终调用的是svc->dispatchfunc()函数,调用逻辑为:sockt->fd触发回调-> virNetSocketEventHandle->(sockt->func())=virNetServerServiceAccept->(svc->dispatchFunc())同里这些调用的目的还是为了统一化接口因为virNetServerServiceAccept的回调函数调用的dispatchFunc会根据传输类型不同而有不同的回调函数。在virNetServerServiceAccept中
virNetSocketAccept主要目的就是因为listen的fd收到的事件,因此需要accept接收并生成IO fd并用virNetSocketNew创建一个virNetSocket对象。
由virNetSocketAccept创建的virNetSocket来创建一个virNetServerClient对象,该对象是服务与client通信的最终对象。
virNetServerClientNew函数任务就是依据listen的fd clientsock创建一个client对象。
virNetServerAddClient有两个比较重要的函数:virNetServerClientInit、virNetServerClientSetDispatcher
先说virNetServerClientSetDispatcher因为该函数也是一个初始化函数,且会被前面初始化的回调函数调用。
由代码可知函数实际就是初始化了client->dispatchFunc为virNetServerDispatchNewMessage。
再看virNetServerClientInit函数,可以从函数结构上可以看出函数的核心调用为virNetServerClientRegisterEvent即为client设置读写回调函数:
根据事件机制我们知道这里即为把sock加入到事件循环中进行监督,且回调函数为virNetServerClientDispatchEvent:
该函数的核心函数为virNetServerClientDispatchWrite和virNetServerClientDispatchRead,当然如果是virNetServerClientDispatchRead则会执行virNetServerClientDispatchMessage进行处理传过来的信息。
比如client如果往服务端发送各种指令需要执行相应的任务之类的。我们已经知道dispatchOpaque具体函数是什么了。
看到这我们应该已经明白srv中的线程池用来干什么的了。
这里会创建一个job发送给服务其中比较重要的几个元素,client 、msg然后把job发送给服务的线程池。
首先会把virNetServerJob类型的对象赋值给virThreadPoolJob类型的对象job,并把该job添加到线程池中的joblist中,virCondSignal发送信号给线程池。
然后线程里执行相应的disptch函数。
这时候事件监督机制,线程池,网络通信就全部用上了。
1.3客户端创建
客户端是个比较广泛的应用,因为客户不确定以及服务不确定,客户可以时ivirsh指令可以是libvirt服务或者其他服务。服务端可以是libvirt服务也可以是其他服务比如qemu服务等。
创建客户端的开始就是创建连接打开:
该函数完成两个任务,virInitialize初始化和virConnectOpenInternal创建连接;
初始化中比较重要的是对remotedriver的初始化,供后面的客户端调研访问libvirt服务,里面基本包含了所有客户端的操作。
virConnectOpenInternal->remoteConnectOpen->doRemoteOpen->virNetClientNewTCP
服务端与客户端虽然都是创建fd进行监督,不同的就是client创建的网络传输对象就是virNetClient
相应的创建函数:
virNetServerClientNew<->virNetClientNew
创建的对象virNetServerClient<->virNetClient
virNetServerServiceNewTCP<->virNetClientNewTCP
所在文件virnetservice.c<->virnetclient.c
提及这些区别的目的是为了说明,如果见到service的对象基本可以确定是服务端的传输的描述符否则就是client端;
创建连接从connectopen开始
看看client端的创建virNetClient
也就两个函数了:
从代码中可以看出没做啥事,就是创建连接创建一个virNetSocket保存建立连接的fd
这个也没做啥事,就是创建一个loop和context保存在client中
对应的也有fd的回调函数加到事件循环列表中:
熟悉的函数virNetSocketAddIOCallback目的是设置对应fd的回调函数为virNetClientIncomingEvent
显然两个回调函数virNetClientIOHandleOutput,virNetClientIOHandleInput代表着读写操作。通过virNetClientIOHandleOutput->virNetClientIOWriteMessage->virNetSocketWrite->virNetSocketWriteWire->write把数据传到目的端。
来看一个接口怎样实现向服务端发送指令的,还举迁移的例子
virDomainMigrate3->virDomainMigrateVersion3->virDomainMigrateVersion3Full->
domain->conn->driver->domainMigrateBegin3Params ||domain->conn->driver->domainMigratePerform3Params ||domain->conn->driver->domainMigrateConfirm3Params
比如domain->conn->driver->domainMigrateBegin3Params 对应的是remoteDomainMigrateBegin3Params
remote接口都是统一的标准即使用call函数把数据发送到目的端:call发送的参数即为remoteDomainMigrateBegin3Params函数在服务驱动中函数数组所对应的序号。
call->callFull->virNetClientProgramCall->virNetClientSendWithReply->virNetClientSendInternal->virNetClientIO->virNetClientIOEventLoop->virNetClientIOHandleOutput->virNetSocketWrite
1.3 libvirt服务与qemu的连接
libvirt要作为client端与qemu进行通信,并且qemu也需要把虚拟机的状态返回给libvirt让其做出相应处理。
注册:
daemonInitialize:
该函数调用qemuRegister:
最终把qemuHypervisorDriver注册到与qemu进程连接的驱动中
qemuStateInitialize
设置与qemu通信fd
qemuProcessLaunch-> qemuProcessWaitForMonitor()->qemuConnectMonitor
qemuMonitorOpenUnix只是创建一个fd;
qemuMonitorOpenInternal为这个fd设置cb为monitorCallbacks
qemuMonitorOpenInternal->qemuMonitorRegister把fd事件放到事件循环中进行监督。
回调函数为qemuMonitorIO
如果从fd中读出指令信息则qemuMonitorIOProcess->qemuMonitorJSONIOProcess->qemuMonitorJSONIOProcessLine->qemuMonitorJSONIOProcessEvent
如果接收到事件
qemuMonitorJSONIOProcessEvent –》qemuMonitorEmitEvent
找到domainEvent中的回调函数并调用
找到对应的事件调用相应的回调函数进行处理
其中某些monitorCallBackes回调函数会唤醒qemu_driver创建的线程池去完成某些任务:比如
用qemuProcessEventSubmit通过调用virThreadPoolSendJob唤醒driver->workerPool去执行qemuProcessEventHandler即对产生事件的处理