一直在纠结这块要不要写出来,毕竟自己没有看懂,后来还是鼓起梁静茹给的勇气,看多少写多少吧。
操作方式
首先先说我们操作内存热扩容的命令操作,采用的是qemu monitor的方式,执行如下两条命令:
两条命令
# 此处一定要加share=on,不然会出现qemu扩的内存和dpdk映射的内存不是同一块
# 导致内存浪费和网络通信的问题
object_add memory-backend-file,id=mem1,size=4G,mem-path=/mnt/huge,share=on
device_add pc-dimm,id=dimm1,memdev=mem1
从文件hmp-commands.hx
中我们可以看一下这两条命令针对的函数调用:
{
.name = "object_add",
.args_type = "object:O",
.params = "[qom-type=]type,id=str[,prop=value][,...]",
.help = "create QOM object",
.mhandler.cmd = hmp_object_add,
.command_completion = object_add_completion,
},
STEXI
@item object_add
@findex object_add
Create QOM object.
ETEXI
{
.name = "device_add",
.args_type = "device:O",
.params = "driver[,prop=value][,...]",
.help = "add device, like -device on the command line",
.mhandler.cmd = hmp_device_add,
.command_completion = device_add_completion,
},
STEXI
@item device_add @var{config}
@findex device_add
Add device.
ETEXI
monitor_init-->monitor_command_cb-->handle_hmp_command
最后会调用到命令行执行的函数,可以看到第一条命令对应的函数hmp_object_add
,第二条命令对应的函数hmp_device_add
,接下来我们针对这两个函数进行分析。
内存扩容
void hmp_object_add(Monitor *mon, const QDict *qdict)
{
/* 解析object的参数 */
QemuOpts *opts = qemu_opts_from_qdict(qemu_find_opts("object"), qdict, &err);
/* 创建一些函数指针,下个函数会用到 */
OptsVisitor *ov = opts_visitor_new(opts);
/* 创建实例,并且申请配置的实例大小 */
Object *obj = user_creatable_add(qdict, opts_get_visitor(ov), &err);
}
Object *user_creatable_add(const QDict *qdict,
Visitor *v, Error **errp)
{
char *type = NULL;
char *id = NULL;
Object *obj = NULL;
Error *local_err = NULL, *end_err = NULL;
/* clone一份 */
QDict *pdict = qdict_clone_shallow(qdict);
/* 调用opts_start_struct创建参数存储 */
visit_start_struct(v, NULL, NULL, 0, &local_err);
/* 删除参数,并且解析参数出来 */
qdict_del(pdict, "qom-type");
visit_type_str(v, "qom-type", &type, &local_err);
/* 同上 */
qdict_del(pdict, "id");
visit_type_str(v, "id", &id, &local_err);
/* 创建目标,并且根据需求做相应的操作,这里我们就是申请内存 */
obj = user_creatable_add_type(type, id, pdict, v, &local_err);
return obj;
}
Object *user_creatable_add_type(const char *type, const char *id,
const QDict *qdict,
Visitor *v, Error **errp)
{
/* 根据type类型获取class */
ObjectClass *klass = object_class_by_name(type);
const QDictEntry *e;
/* 创建目标 */
Object *obj = object_new(type);
/* 调用host_memory_backend_memory_complete */
user_creatable_complete(obj, &local_err);
return obj;
}
static void
host_memory_backend_memory_complete(UserCreatable *uc, Error **errp)
{
HostMemoryBackend *backend = MEMORY_BACKEND(uc);
HostMemoryBackendClass *bc = MEMORY_BACKEND_GET_CLASS(uc);
if (bc->alloc) {
/* 申请memory region,我们是通过调用file_backend_memory_alloc */
bc->alloc(backend, &local_err);
/* 获取memory region的可用地址 */
void *ptr = memory_region_get_ram_ptr(&backend->mr);
/* 获取memory region的大小 */
uint64_t sz = memory_region_size(&backend->mr);
/* 对该段地址进行一些特性的设置,比如可以合并,不允许dump */
if (backend->merge) {
qemu_madvise(ptr, sz, QEMU_MADV_MERGEABLE);
}
if (!backend->dump) {
qemu_madvise(ptr, sz, QEMU_MADV_DONTDUMP);
}
#ifdef CONFIG_NUMA
/* 没细看,我觉得应该是计算有几个node,我们一般是两个 */
unsigned long lastbit = find_last_bit(backend->host_nodes, MAX_NODES);
unsigned long maxnode = (lastbit + 1) % (MAX_NODES + 1);
unsigned flags = MPOL_MF_STRICT | MPOL_MF_MOVE;
/* 设置内存策略,内核尝试移动所有内存范围内的页面,如果无法移动,则失败报错EIO */
mbind(ptr, sz, backend->policy,
maxnode ? backend->host_nodes : NULL, maxnode + 1, flags);
#endif
/* Preallocate memory after the NUMA policy has been instantiated.
* This is necessary to guarantee memory is allocated with
* specified NUMA policy in place.
*/
if (backend->prealloc) {
/* 安装信号处理函数,并且清空内存内容 */
os_mem_prealloc(memory_region_get_fd(&backend->mr), ptr, sz);
}
}
}
内存设备插入
hmp_device_add-->qmp_device_add
void qmp_device_add(QDict *qdict, QObject **ret_data, Error **errp)
{
/* 解析device的参数 */
QemuOpts *opts = qemu_opts_from_qdict(qemu_find_opts("device"), qdict, &local_err);
DeviceState *dev = qdev_device_add(opts, &local_err);
}
DeviceState *qdev_device_add(QemuOpts *opts, Error **errp)
{
BusState *bus = NULL;
/* 查找驱动,并根据驱动找到设备的class */
const char *driver = qemu_opt_get(opts, "driver");
DeviceClass *dc = qdev_get_device_class(&driver, errp);
/* find bus */
const char *path = qemu_opt_get(opts, "bus");
if (path != NULL) {
bus = qbus_find(path, errp);
} else if (dc->bus_type != NULL) {
bus = qbus_find_recursive(sysbus_get_default(), NULL, dc->bus_type);
}
/* create device */
DeviceState *dev = DEVICE(object_new(driver));
if (bus) {
qdev_set_parent_bus(dev, bus);
}
const char *id = qemu_opts_id(opts);
if (id) {
dev->id = id;
}
if (dev->id) {
object_property_add_child(qdev_get_peripheral(), dev->id,
OBJECT(dev), NULL);
} else {
static int anon_count;
gchar *name = g_strdup_printf("device[%d]", anon_count++);
object_property_add_child(qdev_get_peripheral_anon(), name,
OBJECT(dev), NULL);
g_free(name);
}
/* set properties */
qemu_opt_foreach(opts, set_property, dev, &err);
dev->opts = opts;
object_property_set_bool(OBJECT(dev), true, "realized", &err);
return dev;
}
看不下去了,直接写上路径的了。
hmp_device_add-->qmp_device_add-->qdev_device_add-->object_property_set_bool-->object_property_set_qobject-->object_property_set-->property_set_bool-->device_set_realized-->hotplug_handler_plug-->pc_machine_device_plug_cb-->pc_dimm_plug-->pc_dimm_memory_plug-->memory_region_add_subregion-->memory_region_add_subregion_common-->memory_region_update_container_subregions-->memory_region_transaction_commit-->vhost_commit-->vhost_user_set_mem_table
此处函数大概的意思是对DPDK进行一些配置,主要是传递过去作为共享内存的文件描述符fd,并且进行之前qemu扩的内存的mmap,这里需要注意的有两点:
- 文件描述符通过unix socket domain传递的时候可能是在DPDK的进程中重新做了一个fd值映射相同的文件,所以qemu端发送的fd的值和DPDK端收到的值不一样,但是映射的是同一个文件。
- 因为每次扩容,都会将每个region的fd都传一次,所以老的region(非扩容部分)也会传来一个新的fd,这块相当于对映射文件做了多次打开,所以需要注意fd的关闭,保证文件的所有fd都关闭,防止内存泄露,做法就是每次新传来老region的fd,要么关闭新的,要么关闭旧的,保证每个region只打开一个fd。