virsh是libvirt的一个命令行工具。相当于libvirt的一个客户端(libvirtd是服务器)。每次执行virsh命令,程序是从virsh.c中的main函数开始执行。
在virsh中有几个比较重要的结构体,其一如下:
struct _vshControl {
const char *name; /* hardcoded name of the binary that cannot
* be changed without recompilation compared
* to program name */
char *connname; /* connection name */
char *progname; /* program name */
vshCmd *cmd; /* the current command */
char *cmdstr; /* string with command */
bool imode; /* interactive mode? */
bool quiet; /* quiet mode */
bool timing; /* print timing info? */
int debug; /* print debug messages? */
char *logfile; /* log file name */
int log_fd; /* log file descriptor */
char *historydir; /* readline history directory name */
char *historyfile; /* readline history file name */
virThread eventLoop;
virMutex lock;
bool eventLoopStarted;
bool quit;
int eventPipe[2]; /* Write-to-self pipe to end waiting for an
* event to occur */
int eventTimerId; /* id of event loop timeout registration */
int keepalive_interval; /* Client keepalive interval */
int keepalive_count; /* Client keepalive count */
# ifndef WIN32
struct termios termattr; /* settings of the tty terminal */
# endif
bool istty; /* is the terminal a tty */
const vshClientHooks *hooks;/* mandatory client specific hooks */
void *privData; /* client specific data */
};
从virsh.c中main函数开始分析
if (!(progname = strrchr(argv[0], '/')))
progname = argv[0];
else
progname++;
ctl->progname = progname;
该部分是获取命令的名字,命令行启动virsh有两种方法,一是直接virsh xxx 二是利用绝对路径启动。对于virsh命令,progname就是“virsh”
if (isatty(STDIN_FILENO)) {
ctl->istty = true;
if (tcgetattr(STDIN_FILENO, &ctl->termattr) < 0)
ctl->istty = false;
}
该部分就是获取标准输入设备的一些信息。
if (virMutexInit(&ctl->lock) < 0) {
vshError(ctl, "%s", _("Failed to initialize mutex"));
return EXIT_FAILURE;
}
初始化锁。
if (virInitialize() < 0) {
vshError(ctl, "%s", _("Failed to initialize libvirt"));
return EXIT_FAILURE;
}
virInitialize函数的实现如下
int
virInitialize(void)
{
if (virOnce(&virGlobalOnce, virGlobalInit) < 0)
return -1;
if (virGlobalError)
return -1;
return 0;
}
在多线程环境里面,virGlobalInit函数只执行一次,该函数对于virsh端主要初始化如下
virErrorInitialize();
virLogSetFromEnv();
remoteRegister();
virErrorInitialize函数用于初始化错误信息变量,该变量是线程特定数据。具体调用方式virThreadLocalInit(&virLastErr, virLastErrFreeData);virLastErr就是一个线程特定数据。
virLogSetFromEnv函数的实现如下
void
virLogSetFromEnv(void)
{
const char *debugEnv;
if (virLogInitialize() < 0)
return;
debugEnv = virGetEnvAllowSUID("LIBVIRT_DEBUG");
if (debugEnv && *debugEnv)
virLogParseDefaultPriority(debugEnv); /*virLogDefaultPriority*/
debugEnv = virGetEnvAllowSUID("LIBVIRT_LOG_FILTERS");
if (debugEnv && *debugEnv)
virLogParseFilters(debugEnv); /*LOGµÈ¼¶*/
debugEnv = virGetEnvAllowSUID("LIBVIRT_LOG_OUTPUTS");
if (debugEnv && *debugEnv)
virLogParseOutputs(debugEnv); /*Êä³öµ½ÏµÍ³ÈÕÖ¾»¹ÊÇ×Ô¼ºµÄ?*/
}
可以看见virLogSetFromEnv函数是用来从系统获取LIBVIRT_DEBUG,LIBVIRT_LOG_FILTERS,LIBVIRT_LOG_OUTPUTS三个配置信息,如果获取到则将配置信息赋值给对应的全局变量。最终LIBVIRT_DEBUG的值会赋值给virLogDefaultPriority。LIBVIRT_LOG_FILTERS的值赋值给virLogFilters。LIBVIRT_LOG_OUTPUTS的值赋值给virLogOutputs。
remoteRegister函数是初始化远程模块。在virsh端没有注册真正的虚拟化模块,知识注册了一个远程模块。
int
remoteRegister(void)
{
if (virRegisterConnectDriver(&connect_driver,
false) < 0)
return -1;
if (virRegisterStateDriver(&state_driver) < 0)
return -1;
return 0;
}
virRegisterConnectDriver函数是将connect_driver变量赋值给virConnectDriverTab数组,该数组存储已经注册的驱动。connect_driver结构中包含了针对remote模块各种操作的接口。connect_driver的初始化如下
static virConnectDriver connect_driver = {
.hypervisorDriver = &hypervisor_driver,
.interfaceDriver = &interface_driver,
.networkDriver = &network_driver,
.nodeDeviceDriver = &node_device_driver,
.nwfilterDriver = &nwfilter_driver,
.secretDriver = &secret_driver,
.storageDriver = &storage_driver,
};
重新回到main函数中,接下来是
if ((defaultConn = virGetEnvBlockSUID("VIRSH_DEFAULT_CONNECT_URI")))
ctl->connname = vshStrdup(ctl, defaultConn);
该部分从系统环境里面获取默认连接的名字。比如qemu://system
if (!vshInit(ctl, cmdGroups, NULL))
exit(EXIT_FAILURE);
该部是初始化操作函数数组。cmdGroups是一个全局的变量,该变量存储了和通过命令行敲入操作匹配的操作函数。该函数里面还调用了vshInitDebug(ctl); vshInitDebug函数是获取日志等级和日志文件。
接下来是virshParseArgv函数,该函数会解析命令行传进来的参数。先解析有没有传入配置项,最后再调用vshCommandStringParse函数或者vshCommandArgvParse函数去解析真正的操作参数。
if (argc == optind) { /*no option , command is onli "virsh"*/
ctl->imode = true;
} else {
/* parse command */
ctl->imode = false;
if (argc - optind == 1) {
vshDebug(ctl, VSH_ERR_INFO, "commands: \"%s\"\n", argv[optind]);
return vshCommandStringParse(ctl, argv[optind]);
} else {
return vshCommandArgvParse(ctl, argc - optind, argv + optind);
}
}
经过解析过会,程序会调用vshCmddefSearch函数从cmdGroups数组中寻找对应的操作函数。然后再获取传进来的操作的参数。并将参数存储在opts链表里,获取到的操作函数则存储在def里。opts和def是在_vshCmd结构体中定义的,如下:
struct _vshCmd {
const vshCmdDef *def; /* command definition */
vshCmdOpt *opts; /* list of command arguments */
vshCmd *next; /* next command */
};
最后将该vshCmd结构体变量存储在ctl->cmd中。
参数解析完毕后回到main中开始执行virshInit函数。
该函数中会执行virEventRegisterDefaultImpl函数,该函数中先执行virEventPollInit来创建一个管道,并将管道的读端加入到eventLoop数组中。读端的事件服务函数仅仅是从管道中读取一字节,然后丢弃。
然后再调用virEventRegisterImpl函数去初始化全局变量addHandleImpl、updateHandleImpl、removeHandleImpl、addTimeoutImpl、updateTimeoutImpl、removeTimeoutImpl。
virEventRegisterDefaultImpl函数执行完后,开始调用如下函数
if (virThreadCreate(&ctl->eventLoop, true, vshEventLoop, ctl) < 0)
return false;
该部分先创造一个线程,该线程会监听eventLoop中的所有描述符。如果有期待时间发生,则调用事件处理函数。
最后调用virshReconnect(ctl)函数去连接到对应的模块。
最后去调用vshCommandRun(ctl, ctl->cmd);
该函数里会调用cmd->def->handler(ctl, cmd),最总就会调用的寻找到的remote端的操作函数。然后解析返回值来判断操作是否正确。