文章目录

  • Multifd迁移特点
  • Multifd实现原理
  • 数据结构
  • MultiFDInit_t
  • MultiFDPacket_t
  • MultiFDPages_t
  • MultiFDSendParams
  • multifd_send_state
  • 发送原理
  • 迁移线程
  • multifd线程
  • 核心流程
  • 连接建立
  • 迁移准备
  • 迁移迭代
  • Multifd性能测试


Multifd迁移特点

  • 我们知道QEMU的内存迁移分为pre-copy,post-copy,这通过内存拷贝的时间段来区分,当内存拷贝在目的虚机启动之前完成,称为pre-copy,反之称为post-copy。这两种迁移拷贝内存数据都在迁移线程中完成。multifd则不同,它发送内存数据在专门的线程中完成,每个线程都通过socket连接到目的端,建立一个发送通道,所有线程可以并行发送内存数据。
  • multifd迁移与普通迁移的主要区别在于以下两点:
  1. 连接建立方式
    普通迁移可以有两种连接建立方式,一是libvirt服务进程与对端发起连接,然后将fd传递给QEMU,QEMU的迁移线程拿到此fd后,向fd写入数据,发起迁移。二是QEMU负责建立socket连接,libvirt传递给QEMU的是对端访问地址。对于multifd来说,它只能使用第二种方式,QEMU负责建立socket连接,并且除了建立迁移线程的socket连接,还需要为每个发送线程建立socket连接。在建立连接阶段,普通迁移只需要建立主迁移通道(migration channel),使用一个socket连接,multifd则除主迁移通道之外,还建立了多个侧通道(side channel),每个侧通道维护一个socket连接,每个线程维护一个侧通道。侧通道代替主迁移通道发送内存数据,而主通道在multifd中的作用大大弱化,仅仅是向对端发送已拷贝的内存数据。普通迁移在QEMU建立好与目的端的连接以后,就启动迁移线程发起迁移。而multifd在这之前,还需要启动multfd线程,同时初始化一些数据结构,这也是由于连接建立方式不同导致的。
  2. 内存拷贝方式
    普通迁移的内存拷贝只发生在主迁移通道中,所有RAMBlock包含的host内存页都通过此通道发送,发送动作都在迁移线程中完成。multifd迁移的内存拷贝则在侧通道中完成,当迁移线程逐RAMBlock寻找脏页成功后,不会将RAMBlock中包含的内存脏页交给主通道,而是将脏页地址保存起来,交给空闲的multifd线程,由侧通道发送出去,然后主迁移线程继续寻找脏页,成功后交给另一个空闲的multifd线程发送出去。对比来看,普通迁移时迁移线程除了要负责脏页查找,还负责脏页发送,而multifd迁移时将脏页的发送交给了multifd线程,主线程只负责脏页查找。

Multifd实现原理

数据结构

MultiFDInit_t

  • multifd迁移流的开头通过一个初始包标识自己,这个包在每个multfd线程开始发送数据前传输给对端,用于确认迁移方式。
typedef struct {
    uint32_t magic;
    uint32_t version;
    unsigned char uuid[16]; /* QemuUUID */
    uint8_t id;
    uint8_t unused1[7];     /* Reserved for future use */
    uint64_t unused2[4];    /* Reserved for future use */
} __attribute__((packed)) MultiFDInit_t;

MultiFDPacket_t

  • multifd线程一次性会发送多个物理页,最多可以累计至128个(4k页大小),MultiFDPacket_t用于记录multifd线程本次发送物理页的元数据,这些元数据作为数据,在内存页数据发送之前,提前发送到对端。multifd线程会把MultiFDPacket_t结构作为流数据一起发送到对端。
typedef struct {
    uint32_t magic;
    uint32_t version;
    uint32_t flags;
    /* maximum number of allocated pages */
    /* 每个页作为一个iov向量写入侧通道的流中
     * 多个页就有多个iov,multifd累计一定数量的内存页之后一起发送 
     * pages_alloc表示最大可累计的内存页数,QEMU在迁移准备阶段
     * 会分配相应大小的iov数组 */
    uint32_t pages_alloc;
    /* 已经累计的内存页数目 */
    uint32_t pages_used;
    /* size of the next packet that contains pages */
    /* 侧通道的流中既包含元数据又包含数据
     * next_packet_size用于指示侧通道流中
     * 下一次包含packet的位置 */
    uint32_t next_packet_size;
    /* 迁移线程每次交给multifd发送的内存页,都是许多个 
     * QEMU将一次性传输的多个内存页定义成一个packet 
     * multifd每发送一次就记录一下packet的数目
     * packet_num用于记录发送包的数目 */
    uint64_t packet_num;
    uint64_t unused[4];    /* Reserved for future use */
    /* 存放发送的内存页所在的RAMBlock的idstr */
    char ramblock[256];
    uint64_t offset[];
} __attribute__((packed)) MultiFDPacket_t;

MultiFDPages_t

  • MultiFDPages_t用于维护multifd线程一次性要发送的host物理页,multfd线程每次发送的物理页都属于同一个RAMBlock,同一个RAMBlock的物理页累计到128个页再发送(页大小4k),不同的RAMBlock的物理页需要分开发送。
  • 因为每次multifd线程发送的物理页都属于同一个RAMBlock,因此MultiFDPages_t中设计了一个block结构指向发送物理页所属的RAMBlock
  • 如果连续发送同一个RAMBlock的物理页,需要累计128个物理页才能发送,因此发送的iov向量是个数组,初始化的数组大小为128,iov[]数组每个元素指向一个物理页。同理,还需要一个offset来保存iov[]数组中每个对应物理页在RAMBlock中的偏移。iovoffset域设计的目的在此。
typedef struct {
    /* number of used pages */
    /* iov数组中已经存放了物理页地址的元素个数 */
    uint32_t used;
    /* number of allocated pages */
    /* iov数组的大小 */
    uint32_t allocated;
    /* global number of generated multifd packets */
    uint64_t packet_num;
    /* offset of each page */
    /* iov数组每个元素指向的物理页在RAMBlock的偏移 */
    ram_addr_t *offset;
    /* pointer to each page */
    /* 每个元素指向同一个RAMBlock的物理页 */
    struct iovec *iov;
    /* iov数组中指向的所有物理页所属的RAMBlock */
    RAMBlock *block;
} MultiFDPages_t;
  • 从上面三个数据结构可以大致勾勒出侧通道中发送的数据格式,如下图所示:

MultiFDSendParams

  • Multifd的特点是启动多个线程,同时和对端建立socket连接并发送数据,每个线程的socket连接被QEMU封装成一个channel,channel需要发送的内存被维护在MultiFDPages_t中,每个multifd发送数据需要的所有参数被封装成MultiFDSendParams,它的核心数据就是一个channel和要发送的内存页,分别用c和pages表示。
  • MultiFDSendParams有部分信息用于线程同步,sem字段用于通知multfd线程发送数据,mutex字段用于保护MultiFDSendParams,因为迁移线程和multifd线程都要修改这个结构。quit字段用于指示multifd线程是否停止工作
typedef struct {
    /* this fields are not changed once the thread is created */
    /* channel number */
    uint8_t id;
    /* channel thread name */
    /* multifd发送线程名字,格式:multifdsend_%d */
    char *name;
    /* tls hostname */
    char *tls_hostname;
    /* channel thread id */
    /* 指向multifd线程 */
    QemuThread thread;
    /* communication channel */
    /* multifd数据发往的通道 */
    QIOChannel *c;
    /* sem where to wait for more work */
    /* multifd睡眠在此信号量,直到被其它线程唤醒,开始工作,发送数据 */
    QemuSemaphore sem;
    /* this mutex protects the following parameters */
    /* 以下的字段可能被迁移线程和multifd线程同时修改,mutex用于保护以下字段 */
    QemuMutex mutex;
    /* is this channel thread running */
    bool running;
    /* should this thread finish */
    bool quit;
    /* thread has work to do */
    /* 当前multifd线程是否 */
    int pending_job;
    /* array of pages to sent */
    /* 迁移线程准备好pages之后,与multifd线程交换指针
     * multifd指向迁移线程准备好的pages,然后发送内存数据 */
    MultiFDPages_t *pages;
    /* packet allocated len */
    uint32_t packet_len;
    /* pointer to the packet */
    MultiFDPacket_t *packet;
    /* multifd flags for each packet */
    uint32_t flags;
    /* size of the next packet that contains pages */
    uint32_t next_packet_size;
    /* global number of generated multifd packets */
    uint64_t packet_num;
    /* thread local variables */
    /* packets sent through this channel */
    /* 记录通过此channel发送的物理页累积大小 */
    uint64_t num_packets;
    /* pages sent through this channel */
    uint64_t num_pages;
    /* syncs main thread and channels */
    /* 迁移线程迭代发送数据时,每次结束时需要等待multifd线程完成发送 
     * 这时迁移线程睡眠在sem_sync,当multifd线程发送完成后通过
     * sem_sync唤醒迁移线程 */
    QemuSemaphore sem_sync;
    /* used for compression methods */
    void *data;
}  MultiFDSendParams;

multifd_send_state

  • multifd_send_state是一个全局变量,它指向一个全局结构体,主要维护multifd线程的全局信息,包括每个fd线程的参数、迁移线程准备要发送的内存页、fd线程的状态等。
struct {
	/* fd线程发送内存需要的参数数组
	 * multifdsend_0对应params[0] 
	 * multifdsend_1对应params[1],以此类推
	 * */
    MultiFDSendParams *params;
    /* array of pages to sent */
    /* 迁移线程找到脏页后封装成pages,临时存放在该字段 
     * 当需要让fd线程发送这些pages时,让fd线程指向的pages 
     * 与该字段指向的pages交换 */
    MultiFDPages_t *pages;
    /* global number of generated multifd packets */
    uint64_t packet_num;
    /* send channels ready */
    /* 当fd线程中有任意一个准备好之后,会唤醒等待在该信号量 
     * 上的迁移线程,让迁移线程继续工作 */
    QemuSemaphore channels_ready;
    /*
     * Have we already run terminate threads.  There is a race when it
     * happens that we got one error while we are exiting.
     * We will use atomic operations.  Only valid values are 0 and 1.
     */
    int exiting;
    /* multifd ops */
    MultiFDMethods *ops;
} *multifd_send_state;

发送原理

  • 在介绍multifd流程之前,我们先尝试分析multifd迁移的发送原理,如果单独理解发送原理有问题,再跳到后面一节看迁移的核心流程。multifd迁移,顾名思义,多fd迁移,这里的fd就是创建socket连接或者其它连接返回的fd,总之,就是有多个通道可以并发地发送数据。每个通道专门启动了一个线程来负责发送数据。multifd发送示意图如下:
  • 所有multifd线程发送需要的信息被封装成params数组,数组每个元素对应一个multifd线程需要的信息,param的核心数据是pages,即要发送的内存页指针,每个multifd线程都维护了这样一个信息,同时还有一个全局的公共的pages指针,指向迁移线程搜集的需要发送的内存页。
  • 如果上层在内存迁移时指定N个multifd线程用于发送内存数据(libvirt通过–parallel-connections=N指定),那么multifd线程就有N个,指向内存页指针的pages有N+1个(N个属于multifd线程,1个全局的但只有迁移线程会操作)。
  • 如上图所示,当迁移线程进入迭代迁移阶段,每次迭代可以概括成两步,第一步时搜集可以发送的脏页内存,第二部发送脏页内存,迁移线程在第一步中将脏页内存的地址放到全局变量multifd_send_state的pages字段。然后查找空闲的multifd线程,将其pages指针与全局变量multifd_send_state的pages指针交换,这样空闲的multifd线程就获取了需要发送的物理页,然后迁移线程唤醒空闲multifd线程,开始工作。当进入下一次迭代时,迁移线程将新找到的脏页内存地址又放入multifd_send_state的pages字段,查找新的空闲multifd线程,继续发送物理页。
  • 由上面的分析可知,multifd_send_state中的pages字段会依次与空闲multifd线程的pages字段交换内容,不断地在变换。迁移线程负责查找内存脏页,multifd线程负责发送脏页,当所有multifd都忙起来的时候,内存迁移发送数据的能力达到最大值,这时迁移线程如果再找到新的内存脏页,需要等待multifd线程空闲之后才能再次发送数据。

迁移线程

  • 迁移线程迭代查找脏页内存,触发multifd线程发送内存数据的流程如下:
/* 迁移线程入口点 */
migration_thread
	migration_iteration_run
		qemu_savevm_state_iterate
			se->ops->save_live_iterate
/* 迁移迭代的入口点 */
ram_save_iterate
	ram_find_and_save_block
		ram_save_host_page
			ram_save_target_page
				ram_save_multifd_page
					multifd_queue_page
  • multifd_queue_page函数将找到的脏页内存地方存放到multifd_send_state全局变量,分析其具体实现。
/* 入参:
 * f,迁移线程的主通道,在multifd中只用来已传输的数据长度 
 * block,内存脏页所在的RAMBlock,迁移线程是逐block查找的脏页内存 
 * offset,内存脏页通常只占用RAMBlock的一小段4k区间,offset表示内存脏页的起始地址 */
int multifd_queue_page(QEMUFile *f, RAMBlock *block, ram_addr_t offset)
{
	/* 首先获取全局变量multifd_send_state中的pages字段 */
    MultiFDPages_t *pages = multifd_send_state->pages;
	/* pages字段存放的是与multifd线程pages交换的值,或者初始值
	 * 如果block为空,表示之前还没有发送过内存页 */
    if (!pages->block) {
        pages->block = block;
    }
	/* 如果当前迁移线程要发送的内存页与前一次发送的内存页所在 
	 * RAMBlock相同,将内存页地址保存到iov[]的地址中,长度设置为页大小 */
    if (pages->block == block) {
    	/* 保存发送页在RAMBlock中的偏移 */
        pages->offset[pages->used] = offset;
        /* 保存发送页的HVA */
        pages->iov[pages->used].iov_base = block->host + offset;
        /* 设置页大小 */
        pages->iov[pages->used].iov_len = qemu_target_page_size();
        /* 每填充一个页到iov数组,增加一次使用计数 */
        pages->used++;
		/* 如果累计的页没有到最大值,直接返回,这里的最大值为128个物理页,下文有介绍 */
        if (pages->used < pages->allocated) {
            return 1;
        }
    }
	/* 有两种情况可以触发multifd线程发送物理页:
	 * 1. 当前发送的物理页所在RAMBlock与前一次发送的物理页所在RAMBlock不同
	 * 2. 当前发送的物理页个数累计达到了128个 */
    if (multifd_send_pages(f) < 0) {
        return -1;
    }
	/* 进入到这里只有一种情况
	 * 当前物理页所在RAMBlock与前一次发送的物理页所在RAMBlock互不相同 
	 * 需要再次调用multifd_queue_page,将本次要发送的物理页真正地入队 */
    if (pages->block != block) {
        return  multifd_queue_page(f, block, offset);
    }
    return 1;
}
  • multifd_send_pages的主要工作是遍历所有multifd线程,查找空闲的线程,将要发送的内存页地址传递给空闲线程,然后唤醒它,使其工作,流程如下:
static int multifd_send_pages(QEMUFile *f)
{
   	/* 本次查找空闲线程的起始值,每次加1 */
    static int next_channel;
    MultiFDSendParams *p = NULL; /* make happy gcc */
    MultiFDPages_t *pages = multifd_send_state->pages;
    uint64_t transferred;
	/* 首先睡眠在channels_ready信号量上,等待空闲的线程将自己唤醒 */
    qemu_sem_wait(&multifd_send_state->channels_ready);
    /*
     * next_channel can remain from a previous migration that was
     * using more channels, so ensure it doesn't overflow if the
     * limit is lower now.
     */
    next_channel %= migrate_multifd_channels();
    /* 遍历所有multifd线程使用的连接通道,查找空闲的multifd线程 */
    for (i = next_channel;; i = (i + 1) % migrate_multifd_channels()) {
        p = &multifd_send_state->params[i];
        qemu_mutex_lock(&p->mutex);
		/* 如果线程没有任务,说明其空闲,找到目标 */
        if (!p->pending_job) {
            p->pending_job++;
            next_channel = (i + 1) % migrate_multifd_channels();
            break;
        }
        qemu_mutex_unlock(&p->mutex);
    }
    assert(!p->pages->used);
    assert(!p->pages->block);
	/* multifd线程发送的内存页被封装成packet的概念
	 * 每发送一次,增加packet的计数 */
    p->packet_num = multifd_send_state->packet_num++;
    /* 将全局变量中的页指针和找到的multifd空闲线程的内存页指针交换 
     * 通过这种方式,multifd线程得到了要发送的内存页 
     * 而全局变量中的页指针作为了一个中转站
     * 用于存放迁移线程准备好的,需要让multifd线程发送的页指针 */
    multifd_send_state->pages = p->pages;
    p->pages = pages;
    transferred = ((uint64_t) pages->used) * qemu_target_page_size()
                + p->packet_len;
    qemu_file_update_transfer(f, transferred);
    ram_counters.multifd_bytes += transferred;
    ram_counters.transferred += transferred;
    qemu_mutex_unlock(&p->mutex);
    /* 所有准备工作完成之后,唤醒等待在sem上的multifd线程,让其开始工作 */
    qemu_sem_post(&p->sem);
    return 1;
}

multifd线程

  • multifd线程被创建出来之后,它的工作很简单,可以概括为:睡眠、被唤醒、发送数据、继续睡眠。循环如此,直到被通知停止工作。
static void *multifd_send_thread(void *opaque)
{
    MultiFDSendParams *p = opaque;
    Error *local_err = NULL;
    int ret = 0;
    uint32_t flags = 0;
	/* 发送初始化包到对端,表明自己开始进行multifd数据发送 */
    multifd_send_initial_packet(p, &local_err)
    /* initial packet */
    p->num_packets = 1;
	/* 无限循环 */
    while (true) {
    	/* 睡眠在sem信号量上,直到迁移线程准备好要发送的数据,将自己唤醒 */
        qemu_sem_wait(&p->sem);
        /* multifd线程被唤醒,如果被告知结束工作,则退出循环 */
        if (qatomic_read(&multifd_send_state->exiting)) {
            break;
        }
        qemu_mutex_lock(&p->mutex);
		/* 如果迁移线程标记有等待的任务,则进入工作流程 */
        if (p->pending_job) {
            if (used) {
                ret = multifd_send_state->ops->send_prepare(p, used,
                                                            &local_err);
            }
            /* 填充packet的元数据 */
            multifd_send_fill_packet(p);
            p->flags = 0;
            p->num_packets++;
            p->num_pages += used;
            p->pages->used = 0;
            p->pages->block = NULL;
            qemu_mutex_unlock(&p->mutex);
			/* 发送packet的元数据 */
            ret = qio_channel_write_all(p->c, (void *)p->packet,
                                        p->packet_len, &local_err);
            /* 发送内存页 */                          
            if (used) {
                ret = multifd_send_state->ops->send_write(p, used, &local_err);
                if (ret != 0) {
                    break;
                }
            }
            qemu_mutex_lock(&p->mutex);
            p->pending_job--;
            qemu_mutex_unlock(&p->mutex);
			/* 发送结束,将同步等待在sem_sync信号量上的线程唤醒 */
            if (flags & MULTIFD_FLAG_SYNC) {
                qemu_sem_post(&p->sem_sync);
            }
            /* 发送结束,将等待在channels_ready上的线程唤醒
             * 表明自己准备好了,可以进行下一轮的数据传输 */
            qemu_sem_post(&multifd_send_state->channels_ready);
        } 
    }
	......
}

核心流程

连接建立

  • multifd建立了两次连接,第一个是迁移命令下发的初始阶段,建立的主迁移通道,第二个是迁移准备阶段,建立的侧通道。初始阶段建立主迁移通道的代码路径如下:
/* 上层发起内存迁移触发迁移命令 */
qmp_migrate
	socket_start_outgoing_migration
		socket_start_outgoing_migration_internal
			/* 将对端的socket连接地址存放到全局变量outgoing_args中
			 * 建立侧通道连接时可以使用该地址 */
			outgoing_args.saddr = saddr
			/* 创建一个线程异步地创建socket连接,然后将其封装成一个QIOChannelSocket
			 * 它的父类是QIOChannel,QEMU通过操作QIOChannel来向对端发送内存数据 
			 * 该函数首先创建socket连接,同时将该socket_outgoing_migration函数封装成 
			 * 一个任务,当socket连接创建完成后,创建一个线程来执行该任务,因此
			 * socket_outgoing_migration在最后会在一个单独创建的线程中被执行 */
		 	qio_channel_socket_connect_async(sioc,
                                     saddr,
                                     socket_outgoing_migration,
                                     data,
                                     socket_connect_data_free,
                                     NULL);
  				qio_task_run_in_thread(task,
                           qio_channel_socket_connect_worker,
                           addrCopy,
                           (GDestroyNotify)qapi_free_SocketAddress,
                           context)
/* 创建socket连接 */                   
qio_channel_socket_connect_worker
	qio_channel_socket_connect_sync
		socket_connect           
		qio_channel_socket_set_fd
/* 创建线程异步执行qio_task_thread_worker */
qio_channel_socket_connect_async
	qio_task_run_in_thread
		qemu_thread_create(&thread,
                       "io-task-worker",
                       qio_task_thread_worker,
                       task,
                       QEMU_THREAD_DETACHED);    
/* 设置socket连接完成之后需要回调的任务 */                       
qio_task_thread_worker
	/* qio_task_thread_result为socket连接完成后要回调的函数
	 * 传入的task中有要执行的任务 */
    g_source_set_callback(task->thread->completion,
                          qio_task_thread_result, task, NULL)
/* 执行迁移函数socket_outgoing_migration */                          
qio_task_thread_result
	qio_task_complete(task)
		task->func(task, task->opaque)	<=>	socket_outgoing_migration
  • 迁移准备阶段,建立侧通道的代码路径如下:
/* 迁移发起入口点 */
socket_outgoing_migration
	migration_channel_connect
		migrate_fd_connect
			/* 迁移前multifd相关准备 */
			multifd_save_setup
			qemu_thread_create(&s->thread, "live_migration", migration_thread, s,
                       		   QEMU_THREAD_JOINABLE);
/* multifd设置入口 */
multifd_save_setup
	socket_send_channel_create(multifd_new_send_channel_async, p)
		qio_channel_socket_connect_async(sioc, outgoing_args.saddr,
                                     	 f, data, NULL, NULL)
         	qio_task_run_in_thread(task,
                          		   qio_channel_socket_connect_worker,
                           		   addrCopy,
                            	   (GDestroyNotify)qapi_free_SocketAddress,
                           	       context)

迁移准备

  • multifd迁移的准备工作主要在multifd_save_setup函数中完成,它负责初始化每个multifd线程需要的参数,创建multifd线程。如下:
int multifd_save_setup(Error **errp)
{
	/* 默认累计发送内存页的上限,128页,当同一个RAMBlock中累计到128个物理页
	 * multifd线程才开始传输数据,在这之前只是将要发送的内存页的地址存放在iov[]数组中 */
    uint32_t page_count = MULTIFD_PACKET_SIZE / qemu_target_page_size();
	/* 如果没有启用multifd迁移,直接返回 */
    if (!migrate_use_multifd()) {
        return 0;
    }
    s = migrate_get_current();
    /* 获取上层设置的multifd线程数,默认为2 */
    thread_count = migrate_multifd_channels();
    /* 为全局数据结构multifd_send_state分配空间 */
    multifd_send_state = g_malloc0(sizeof(*multifd_send_state));
    /* 为每个multifd线程分配对应的params数据结构 */
    multifd_send_state->params = g_new0(MultiFDSendParams, thread_count);
    /* 初始化全局变量中的pages指针 */
    multifd_send_state->pages = multifd_pages_init(page_count);
    qemu_sem_init(&multifd_send_state->channels_ready, 0);
    qatomic_set(&multifd_send_state->exiting, 0);
    multifd_send_state->ops = multifd_ops[migrate_multifd_compression()];
	/* 针对每个multifd线程对应的params参数 */
    for (i = 0; i < thread_count; i++) {
        MultiFDSendParams *p = &multifd_send_state->params[i];

        qemu_mutex_init(&p->mutex);
        qemu_sem_init(&p->sem, 0);
        qemu_sem_init(&p->sem_sync, 0);
        p->quit = false;
        p->pending_job = 0;
        p->id = i;
        p->pages = multifd_pages_init(page_count);
        p->packet_len = sizeof(MultiFDPacket_t)
                      + sizeof(uint64_t) * page_count;
        p->packet = g_malloc0(p->packet_len);
        p->packet->magic = cpu_to_be32(MULTIFD_MAGIC);
        p->packet->version = cpu_to_be32(MULTIFD_VERSION);
        p->name = g_strdup_printf("multifdsend_%d", i);
        p->tls_hostname = g_strdup(s->hostname);
        /* 创建socket连接,同时创建对应的multifd线程,专门负责传输数据 */
        socket_send_channel_create(multifd_new_send_channel_async, p);
    }
	......
}

迁移迭代

/* 迁移迭代入口点 */
migration_iteration_run
	qemu_savevm_state_iterate
		se->ops->save_live_iterate	<=>	ram_save_iterate
			/* 查找脏页然后异步发送*/
			ram_find_and_save_block
				/* 遍历所有RAMBlock,查找其中可以发送的内存脏页 */
    			do {
    				/* 查找包含脏页的block */
           			find_dirty_block
           			/* 异步发送脏页内容 */
					ram_save_host_page
   			 	} while (!pages && again)
   			 /* 等待multifd线程发送脏页结束,然后进入下一次迭代 */
			multifd_send_sync_main

Multifd性能测试