stack_t
typedef struct sigaltstack {
void *ss_sp; // 栈空间起始地址
int ss_flags;
size_t ss_size; // 栈空间大小
} stack_t;
ucontext_t
ucontext_t
就是用来保存协程切换时当前CPU的寄存器和堆栈指针的。所以,ucontext_t
结构中必须要有存放寄存器的域和存放堆栈指针的域typedef struct ucontext
{
unsigned long int uc_flags;
struct ucontext *uc_link; // 上下文运行终止时要恢复的上下文
stack_t uc_stack; // 上下文使用的栈
mcontext_t uc_mcontext; // 存放硬件上下文(就是寄存器)
__sigset_t uc_sigmask; // 阻塞在线程上下文的信号
struct _libc_fpstate __fpregs_mem;
} ucontext_t;
getcontext
接口协助构造,它获取当前CPU上下文,用户态程序只需要修改其中的eip,让其指向要执行的函数地址,修改堆栈指针,让其指向用户分配好的堆栈,其它东西不用变,就可以定制一个上下文了makecontext
协助用户替换eip。输入参数ucp时要修改的上下文,func是eip要指向的地址,argc是执行func是要传入的参数setcontext
接口用户跳转到ucp上下文,执行任务,但是这个接口有一个问题,就是不能返回,它直接抛弃掉当前CPU上下文跳转到ucp指向的上下文了,没有保存当前上下文到内存,所以这个函数只适合执行一次性的任务,如果当前任务没有执行完成就切换,任务是无法再切换回来的swapcontex
接口,它在跳转到ucp指向的上下文之前,还会将当前上下文保存到oucp指向的内存中,这样就位任务再切换回来提供了可能性getcontext
和swapcontext函数,他们的返回过程都比较特殊,它可能是自身执行完之后的返回(比如第一次执行),也可能时其它函数跳转到这里的,只要其它函数拿到了这个上下文,就可以跳转到这里。#include
#include
#include
int main(int argc, const char *argv[]){
ucontext_t context;
getcontext(&context);
puts("Hello world");
sleep(1);
setcontext(&context);
return 0;
}
这段代码每个1秒中就会打印1次hello world
,无限循环下去
结果如下:
ucontext函数族只是提供了上下文保存和切换的函数接口,这是协程实现的基础。但协程的目的是实现用户态的任务调度,所以还需要一套实现逻辑。协程,可以有不同风格的实现,下面介绍一个比较简单的实现,原创实现见这里
struct schedule {
char stack[STACK_SIZE]; //当前CPU执行任务使用的栈空间
ucontext_t main; //每个任务执行完,或者主动让出后,都返回到这个上下文
int nco; //记录调度器当前管理的协程个数
int cap; //记录调度器当前最多可以管理的协程个数
int running; //记录当前正在运行的协程子在协程数组中的索引
struct coroutine **co; //调度器管理的数组,每个单元就是一个协程
};
struct coroutine {
coroutine_func func; //协程要执行的函数
void *ud; //协程执行函数时传入的参数
ucontext_t ctx; //当协程主动让出后,用于保存当前协程中所在上下文,方便下一次再切换回来时可以继续执行
struct schedule * sch; //协程所在的调度器
ptrdiff_t cap; //当协程主动让出后,记录当前协程堆栈最大容量,超过此需要重新分配内存
ptrdiff_t size; //当协程主动让出后,记录当前协程堆栈大小
int status; //协程状态,可以有4种:ready(待调度),running(正在执行),suspend(挂起)和dead(结束)
char *stack; //当协程主动让出后,保存当前协程的堆栈指针,方便下一次切换回来时重新加载
};
DEFAULT_COROUTINE
个任务的调度器struct schedule *
scheduler_open(void) {
struct schedule *S = malloc(sizeof(*S)); //分配空间
S->nco = 0; //初始化当前管理的任务个数为0
S->cap = DEFAULT_COROUTINE; //调度调度器管理任务的最大值
S->running = -1; //当前没有任务运行
S->co = malloc(sizeof(struct coroutine *) * S->cap); //空间分配
memset(S->co, 0, sizeof(struct coroutine *) * S->cap); //空间初始化
return S;
}
struct coroutine *
_co_new(struct schedule *S , coroutine_func func, void *ud) {
struct coroutine * co = malloc(sizeof(*co));
co->func = func; //协程入口函数
co->ud = ud; //入口函数参数
co->sch = S; //协程所属调度器
co->cap = 0; //协程栈空间的最大值
co->size = 0; //协程栈空间当前值
co->status = COROUTINE_READY; //协程状态,待调度
co->stack = NULL; //协程堆栈空间指针
return co;
}
int
coroutine_new(struct schedule *S, coroutine_func func, void *ud) {
struct coroutine *co = _co_new(S, func , ud); //组装一个协程
if (S->nco >= S->cap) { //分配的协程数超过调度器管理协程的最大值,重新分配2倍的当前协程数,作为最大值
int id = S->cap;
S->co = realloc(S->co, S->cap * 2 * sizeof(struct coroutine *));
memset(S->co + S->cap , 0 , sizeof(struct coroutine *) * S->cap);
S->co[S->cap] = co;
S->cap *= 2;
++S->nco;
return id;
} else { //协程数在最大值范围内,从调度器协程数组中查找空闲的协程,指向新分配的协程,最后返回找到的数组索引
int i;
for (i=0;icap;i++) {
int id = (i+S->nco) % S->cap;
if (S->co[id] == NULL) {
S->co[id] = co;
++S->nco;
return id;
}
}
}
assert(0);
return -1;
}
static void
mainfunc(uint32_t low32, uint32_t hi32) { //每个任务执行前的入口
uintptr_t ptr = (uintptr_t)low32 | ((uintptr_t)hi32 << 32);
struct schedule *S = (struct schedule *)ptr;
int id = S->running; //取出协程在协程数组中的id
struct coroutine *C = S->co[id]; //取出要执行的协程
C->func(S,C->ud); //执行协程任务,过程中可能调用coroutine_yield主动让出,但最后仍然会回到这里
_co_delete(C); //释放执行完成的协程的空间
S->co[id] = NULL; //将协程从协程数组中删掉
--S->nco; //当前正在执行的协程减1
S->running = -1; //但前没有执行的协程
}
void
coroutine_resume(struct schedule * S, int id) { //协程任务发起,参数是调度器和要执行协程任务的id
assert(S->running == -1);
assert(id >=0 && id < S->cap);
struct coroutine *C = S->co[id]; //根据id取出协程
if (C == NULL)
return;
int status = C->status; //取出协程的状态,根据状态判断协程之前没有执行过还是执行到一半让出
switch(status) {
case COROUTINE_READY: //协程之前没有执行过
getcontext(&C->ctx); //取出当前上下文的协程,放到ctx中
C->ctx.uc_stack.ss_sp = S->stack; //给上下文分配堆栈空间
C->ctx.uc_stack.ss_size = STACK_SIZE; //设置堆栈空间的大小
C->ctx.uc_link = &S->main; //设置执行上下文返回后要切换到的下一个上下文,main在后面会被设置成切换协程前的上下文
S->running = id; //设置当前调度器正在执行的协程id
C->status = COROUTINE_RUNNING; //设置协程状态从READY变成RUNNING
uintptr_t ptr = (uintptr_t)S;
makecontext(&C->ctx, (void (*)(void)) mainfunc, 2, (uint32_t)ptr, (uint32_t)(ptr>>32)); //将ctx中的eip替换成mainfunc地址
swapcontext(&S->main, &C->ctx); //开始执行协程,跳转到C->ctx指向的上下文并报错当前上下文到S->main
break;
case COROUTINE_SUSPEND: //协程之前主动让出,处于挂起状态,现在被重新调度了
memcpy(S->stack + STACK_SIZE - C->size, C->stack, C->size); //将挂起前保存的堆栈重新拷贝到调度器的堆栈上
S->running = id; //设置当前调度器运行的协程id
C->status = COROUTINE_RUNNING; //修改协程状态从SUSPEND到RUNNING
swapcontext(&S->main, &C->ctx); //开始执行协程,跳转到C->ctx指向的上下文并报错当前上下文到S->main
break;
default:
assert(0);
}
}
yield
,请求调度器将自身任务挂起,执行其它任务static void
_save_stack(struct coroutine *C, char *top) { //保存任务挂起时当前任务的堆栈
char dummy = 0;
assert(top - &dummy <= STACK_SIZE);
if (C->cap < top - &dummy) { //计算当前运行的协程的堆栈空间大小,如果大于协程的堆栈空间,保存不下,需要重新分配
free(C->stack); //释放原来的空间
C->cap = top-&dummy; //设置新的堆栈空间大小的最大值
C->stack = malloc(C->cap); //分配新的堆栈空间
}
C->size = top - &dummy; //设置堆栈空间大小
//cap是协程执行所有任务中占用的最大堆栈空间的大小,size时上一次挂起时任务的堆栈空间大小
memcpy(C->stack, &dummy, C->size); //将当前调度器运行的任务的堆栈保存到协程的堆栈空间中,下一次运行时再从这儿取
}
void
coroutine_yield(struct schedule * S) { //挂起任务
int id = S->running; //取出当前调度器运行的协程索引,用于取出协程数据结构
assert(id >= 0);
struct coroutine * C = S->co[id];
assert((char *)&C > S->stack);
_save_stack(C,S->stack + STACK_SIZE); //保存当前任务的堆栈到协程的堆栈中
C->status = COROUTINE_SUSPEND; //设置协程的状态为挂起状态
S->running = -1; //设置当前调度器运行的协程索引无效
swapcontext(&C->ctx , &S->main); //将当前上下文保存到协程的上下文数据结构,跳转到调度器的主入口,使调度器可以调度别的协程
}
Coroutine
,保存了协程的基本信息,另一个是CoroutineUContext
,继承自Coroutine
,保存了协程的堆栈和cpu寄存器信息1)协程任务信息,存放任务函数指针,参数
struct Coroutine {
CoroutineEntry *entry; //任务函数指针
void *entry_arg; //任务参数
Coroutine *caller; //协程调用者,当协程让出时,主动跳转到调用者发起调用时保存的上下文
/* Used to catch and abort on illegal co-routine entry.
* Will contain the name of the function that had first
* scheduled the coroutine. */
const char *scheduled;
QSIMPLEQ_ENTRY(Coroutine) co_queue_next; //协程通过这个成员将自己加入到pending等待队列中
/* Coroutines that should be woken up when we yield or terminate.
* Only used when the coroutine is running.
* 当前协程正在运行时,其余协程也想要执行,通过co_queue_next将自身加入到co_queue_wakeup等待队列中
* 当前协程执行完或者主动让出cpu之后,如果co_queue_wakeup等待队列中有成员,就将其取出加到pending队列中,继续运行协程
*/
QSIMPLEQ_HEAD(, Coroutine) co_queue_wakeup;
};
2)协程切换前需要保存的信息
typedef struct {
Coroutine base; //协程
void *stack; //栈空间
size_t stack_size; //保存栈大小
jmp_buf env; //保存cpu寄存器
} CoroutineUContext; //协程上下文
getcontext
类似;sigsetjmp
和setjmp
功能一样,但还提供了保存进程信号集合的功能setcontext
类似,但和setcontext
存在同样的不足,就是没有保存当前上下,跳转到env指向的上下文后不能返回。参数env用于保存当前上下文,参数val传递setjmp的返回值setjmp
保存cpu当前上下文到env中,那么上下文必然保存了程序下一条要指令的地址。longjmp
利用env跳转到setjmp
保存的上下文,那必然从setjmp
处返回。这里存在一个问题,从setjmp
函数返回的,可能是setjmp函数本身,也可能是longjmp
跳转到此的返回。怎么区分是哪种情况呢?约定:setjmp
返回值是0,表示setjmp
函数本身返回,返回值非0,表示longjmp
跳转到此的返回sigsetjmp/siglongjmp
只保存了当前进程栈的信号掩码,setjmp/longjmp
甚至没有保存信号掩码,相对来说性能更好,但仅仅使用setjmp api又无法完成协程的切换,所以qemu协程实现的策略是通过ucontext api
实现协程创建和切换,其它地方都使用setjmp api
以提高性能Coroutine
和CoroutineUContext
数据结构,最后返回Coroutine
/* 调度器主函数,每个协程在第一次被创建时都必须进入,只做一件事:保存当前上下文到self->env结构体中 */
static void coroutine_trampoline(int i0, int i1)
{
union cc_arg arg;
CoroutineUContext *self;
Coroutine *co;
arg.i[0] = i0;
arg.i[1] = i1;
self = arg.p;
co = &self->base;
/* Initialize longjmp environment and switch back the caller */
/* 保存当前上下文到CoroutineUContext的env成员中,下次如果有longjmp使用该env作为参数,就会回到这里*/
if (!sigsetjmp(self->env, 0)) {
/* 取出之前保存的env,跳回到调用者,之前在qemu_coroutine_new函数的sigsetjmp(old_env, 0)中保存了env的上下文
* 所以这里使用siglongjmp会跳回到qemu_coroutine_new
* /
siglongjmp(*(sigjmp_buf *)co->entry_arg, 1);
}
/* 上面sigsetjmp保存上下文到self->env中,在qemu_aio_coroutine_enter或者qemu_coroutine_yield的时候
* 会调用qemu_coroutine_switch,里面会调用siglongjmp(to->env, action)跳转到env保存的上下文
* 如果env保存的时上面的上下文,程序就会跳转到这里进入while循环
* while循环没有终止条件,当它想要跳出循环时,直接跳转到caller->env保存的上下文,然后再页不回来
* 所以,从coroutine_trampoline函数返回的只有第一次创建协程的流程,之后,中途进入coroutine_trampoline的流程
* 不会从coroutine_trampoline返回,而是通过qemu_coroutine_switch跳转
*/
while (1) {
co->entry(co->entry_arg);
qemu_coroutine_switch(co, co->caller, COROUTINE_TERMINATE);
}
}
/*新分配一个协程CoroutineUContext,最后返回它的父对象Coroutine */
Coroutine *qemu_coroutine_new(void)
{
CoroutineUContext *co;
ucontext_t old_uc, uc;
sigjmp_buf old_env;
union cc_arg arg = {0};
/* The ucontext functions preserve signal masks which incurs a
* system call overhead. sigsetjmp(buf, 0)/siglongjmp() does not
* preserve signal masks but only works on the current stack.
* Since we need a way to create and switch to a new stack, use
* the ucontext functions for that but sigsetjmp()/siglongjmp() for
* everything else.
*/
if (getcontext(&uc) == -1) { //获取当前进程上下文,为makcontext替换其中的关键信息,构造ucontext上下文做准备
abort();
}
/* 初始化Coroutine相关信息*/
co = g_malloc0(sizeof(*co)); //分配协程空间
co->stack_size = COROUTINE_STACK_SIZE; //设置协程栈空间大小
co->stack = qemu_alloc_stack(&co->stack_size); //分配协程栈空间
co->base.entry_arg = &old_env; /* stash away our jmp_buf */ //将协程任务函数的参数指向old_env,后面会将上下文保存到old_env中
/* 初始化ucontext_t 结构体 */
uc.uc_link = &old_uc; //初始化协程任务执行完成后要切换的上下文,后面会将协程切换前的上下文保存到此处
uc.uc_stack.ss_sp = co->stack; //堆栈指向之前设置的堆栈
uc.uc_stack.ss_size = co->stack_size;
uc.uc_stack.ss_flags = 0;
arg.p = co;
/* 构造ucontext上下文,设置所有协程的入口函数coroutine_trampoline */
makecontext(&uc, (void (*)(void))coroutine_trampoline, 2, arg.i[0], arg.i[1]);
/* swapcontext() in, siglongjmp() back out */
/* 保存当前上下文到old_env中,因此co->base.entry_arg指向了old_env,后面如果有longjmp(old_env)的类似调用,就会回到这里
* sigsetjmp函数执行完成后会返回0,满足条件进入swapcontext,如果是longjmp跳转到此处,sigsetjmp返回的非0,不会进入
* swapcontext,保证了swapcontext执行1次,不会重复进入。
* coroutine_trampoline函数中会调用siglongjmp(self->env, 0)其实就是调用siglongjmp(old_env, 0)
* 所以,coroutine_trampoline函数会回到这里
**/
if (!sigsetjmp(old_env, 0)) {
swapcontext(&old_uc, &uc);
}
return &co->base;
}
协程创建实现了三个目标,一是分配协程结构体,二是将任务装进协程结构体里面,三是把协程主入口上下文环境放到协程的env中。后面enter协程时,就可以直接调用switch切换到之前保存的主入口上下文,紧接着就可以执行任务函数
CoroutineUContext
结构体的env保存了主入口上下文,通过switch可以回到主入口/*
* 协程切换,跳转到to保存的上下文并将跳转前的上下文保存到from中,更新current执行即将执行的协程to
* to中的caller还保存了指向from的指针,方便在to执行yield时,切换回调用者的上下文
*/
CoroutineAction __attribute__((noinline))
qemu_coroutine_switch(Coroutine *from_, Coroutine *to_,
CoroutineAction action)
{
CoroutineUContext *from = DO_UPCAST(CoroutineUContext, base, from_);
CoroutineUContext *to = DO_UPCAST(CoroutineUContext, base, to_);
int ret;
current = to_; //设置current指向即将执行的协程
ret = sigsetjmp(from->env, 0); //保存当前上下文到caller的env,如果是第一次调用,caller就是leader
if (ret == 0) {
siglongjmp(to->env, action);//切换到qemu_coroutine_new中保存的coroutine_trampoline上下文,进入while循环
}
return ret;
}
void qemu_aio_coroutine_enter(Coroutine *co)
{
QSIMPLEQ_HEAD(, Coroutine) pending = QSIMPLEQ_HEAD_INITIALIZER(pending); //初始化维护等待协程的队列
Coroutine *from = qemu_coroutine_self(); //取出当前运行的协程
QSIMPLEQ_INSERT_TAIL(&pending, co, co_queue_next); //将协程通过其成员co_queue_next放到pending等待队列中
/* Run co and any queued coroutines */
/*
* 遍历等待队列,依次切换到其上每个协程保存的上下文中
*/
while (!QSIMPLEQ_EMPTY(&pending)) {
Coroutine *to = QSIMPLEQ_FIRST(&pending); //取出等待队列中的第一次协程
CoroutineAction ret;
/* Cannot rely on the read barrier for to in aio_co_wake(), as there are
* callers outside of aio_co_wake() */
const char *scheduled = to->scheduled;
QSIMPLEQ_REMOVE_HEAD(&pending, co_queue_next); //等待队列中除当前要执行的协程以外,如没有其它协程了,将等待队列清空
/* if the Coroutine has already been scheduled, entering it again will
* cause us to enter it twice, potentially even after the coroutine has
* been deleted */
if (scheduled) {
fprintf(stderr,
"%s: Co-routine was already scheduled in '%s'\n",
__func__, scheduled);
abort();
}
/* to中的caller指向调用此协程的协程,如果有,表明该协程已经被运行了,不应该再次运行
* 有两种情况caller为空,一是协程还没有开始执行,只是被创建好;二是协程执行过程中主动yield
* 除此之外,不允许协程再次进入
*/
if (to->caller) {
fprintf(stderr, "Co-routine re-entered recursively\n");
abort();
}
/* 设置协程当前协程的调用协程 */
to->caller = from;
/* 协程切换 */
ret = qemu_coroutine_switch(from, to, COROUTINE_ENTER);
/* 协程返回,分两种情况:一是协程协程运行完了,二是协程主动让出 */
/* Queued coroutines are run depth-first; previously pending coroutines
* run after those queued more recently.
* 如果协程执行期间有别的协程请求调度,会将自身加到wakeup队列上。
* 协程返回后需要先检查wakeup队列中是否有元素,如果有,将其加入到pending中,继续执行协程
*/
QSIMPLEQ_PREPEND(&pending, &to->co_queue_wakeup);
switch (ret) {
case COROUTINE_YIELD:
break;
case COROUTINE_TERMINATE:
coroutine_delete(to);
break;
default:
abort();
}
}
}
1)协程进入
qemu_aio_coroutine_enter
qemu_coroutine_switch(from, to, COROUTINE_ENTER)
sigsetjmp(from->env, 0) //保存当前上下文到leader的env中
siglongjmp(to->env, COROUTINE_ENTER) //跳转到协程创建时保存的上下文中
2)协程执行
coroutine_trampoline()
sigsetjmp(self->env, 0) //从longjmp返回,返回值为COROUTINE_ENTER,不为0,因此进入循环
while (1) {
co->entry(co->entry_arg); //执行任务函数
qemu_coroutine_switch(co, co->caller, COROUTINE_TERMINATE)
}
3)协程挂起
co->entry(co->entry_arg)
qemu_coroutine_yield() //任务函数执行过程中挂起协程
qemu_coroutine_switch(self, to, COROUTINE_YIELD) //保存上下文到自身co的env中,切换到调用者即leader的上下文
4)协程终止
qemu_coroutine_switch(co, co->caller, COROUTINE_TERMINATE)
static __thread CoroutineUContext leader; //所有协程的起始调用者
static __thread Coroutine *current; //当前正在执行的协程
/*
* 取出当前正在执行的协程co
* 如果是第一次执行,没有协程正在执行,leader就被设置成当前的协程,用于保存执行协程时,调用者的上下文
* 协程切换时,会调整current的值,指向当前执行的协程
* 如果是其它情况,current指向正在执行的协程
*/
Coroutine *qemu_coroutine_self(void)
{
if (!current) {
current = &leader.base;
}
return current;
}
/* 设置current指向即将执行的协程 */
qemu_coroutine_switch
current = to_;
/*
* 判断协程是否正在执行,可以由其它线程判断
* current不为空并且caller存在
* caller为空只有两种情况:
* 1 协程执行终止了,被删除了,caller为空,表示当前没有协程运行
* 2 协程主动让出了,设置caller为空
*/
int qemu_in_coroutine(void)
{
return current && current->caller;
}
qemu_coroutine_yield
self->caller = NULL
coroutine_delete
self->caller = NULL
qemu_coroutine_enter
就会返回,之后如果要重新执行挂起的协程,需要再次调用qemu_coroutine_enter
继续执行挂起之前的任务。协程yield和terminal的相同点是的都会从qemu_coroutine_enter
返回,不同点是yield返回时保存了上下文到co中,下一次还可以进入,terminal返回已经将co销毁了static void
coroutine_fn yield_5_times(void *opaque)
{
int *done = opaque;
int i;
for (i = 0; i < 5; i++) {
fprintf(stdout, "yield_5_times: ready to yield to test_yield, count %d\n", i);
qemu_coroutine_yield();
fprintf(stdout, "yield_5_times: return frome yield, count %d\n", i);
}
*done = 1;
}
static void
test_yield(void)
{
Coroutine *coroutine;
int done = 0;
int i = -1; /* one extra time to return from coroutine */
coroutine = qemu_coroutine_create(yield_5_times, &done);
while (!done) {
fprintf(stdout, "test_yield: ready to enter yield_5_times, count %d\n", i);
qemu_coroutine_enter(coroutine);
fprintf(stdout, "test_yield: yield_5_times has yield, count %d\n", i);
i++;
}
}
qemu io流程涉及到协程的使用,这一节梳理整个io流程,假设使用qemu-img工具执行dd命令
img_dd
img_open // 获得一个BlockBackend
img_open_opts
blk_new_open
blk_new
bdrv_open
BlockBackend
结构体,用来抽象磁盘文件,另一个是打开文件,创建BlockDriverState
结构,然后将两个通过bdrv_root_attach_child
函数联系起来。BlockBackend
结构体从后端qemu角度看,是一个磁盘文件的操作句柄,其它地方的代码只要拿到这个东西就能操作一个逻辑磁盘,BlockBackend
代表一个逻辑磁盘,它背后的root->bs
真正代表一个真正的虚拟机磁盘。blk_new
没有真正打开文件,bdrv_open
才会打开文件,也就是打开文件描述符。BlockBackend
指向的root
下面有多个bs
,形成一个树状链表并且有父子关系,每个bs
代表一个从虚拟机角度看到的逻辑磁盘,它会有多个backing
,因此一个bs
可能实际由多个文件组成。关于为什么要抽象一个BdrvChild
结构出来,本人不是很理解,在多数场景下这个结构里面除了bs以外,其它结构体都没有用到。猜测这个结构体是设计用来做权限控制的,具体怎么用就不知道了。BlockBackend
,形式类似,blk_pread/blk_pwrite
,我们以读文件举例blk_pread
blk_prw(blk, offset, buf, count, blk_read_entry, 0) // 从这里开始,就要用协程执行函数了,blk_read_entry就是需要协程执行的函数
static int blk_prw(BlockBackend *blk, int64_t offset, uint8_t *buf,
int64_t bytes, CoroutineEntry co_entry,
BdrvRequestFlags flags)
{
QEMUIOVector qiov = QEMU_IOVEC_INIT_BUF(qiov, buf, bytes);
BlkRwCo rwco = { // 封装数据作为参数传递给协程
.blk = blk,
.offset = offset,
.iobuf = &qiov,
.flags = flags,
.ret = NOT_DONE,
};
if (qemu_in_coroutine()) { // 判断当前上下文是不是协程调用进来的,如果是,直接执行io读函数
/* Fast-path if already in coroutine context */
co_entry(&rwco);
} else { // 如果当前不在协程环境,创建一个协程,并发起调用,创建协程参见上面的小节
Coroutine *co = qemu_coroutine_create(co_entry, &rwco);
bdrv_coroutine_enter(blk_bs(blk), co);
BDRV_POLL_WHILE(blk_bs(blk), rwco.ret == NOT_DONE);
}
return rwco.ret;
}
bdrv_coroutine_enter
会跳转到协程创建时注册的上下文,然后执行协程携带的用户函数bdrv_coroutine_enter
aio_co_enter
qemu_aio_coroutine_enter
qemu_coroutine_switch(from, to, COROUTINE_ENTER) // 跳转
sigsetjmp(from->env, 0) // 先保存当前上下文,相当于记录返回地址
siglongjmp(to->env, action) // 再跳转到协程创建时保存的上下文
qemu_coroutine_create
qemu_coroutine_new
makecontext(&uc, (void (*)(void))coroutine_trampoline, 2, arg.i[0], arg.i[1])
coroutine_trampoline
sigsetjmp(self->env, 0) // 第创建协程之后,首次进入协程就从这里返回。由于时从协程跳转,返回值非0,因此执行下面的while循环
while (true) {
co->entry(co->entry_arg); // 终于,这个函数对应的就是前文的blk_read_entry,协程里面开始执行
qemu_coroutine_switch(co, co->caller, COROUTINE_TERMINATE);
}
blk_read_entry
blk_co_preadv
bdrv_co_preadv // 位于block/io.c中
bdrv_co_preadv
bdrv_aligned_preadv
bdrv_driver_preadv
bdrv_driver_preadv
这一步,就要区分具体的qemu磁盘驱动了,驱动的读接口有三种:bdrv_co_preadv,bdrv_aio_preadv,bdrv_co_readv
,优先级依次降低,首选第一个bdrv_co_preadv
,每个类型的磁盘至少实现其中一个接口。对于RBD磁盘文件,它实现了bdrv_aio_preadv接口,对于普通文件系统,实现了bdrv_co_readv。 if (drv->bdrv_aio_preadv) {
BlockAIOCB *acb;
CoroutineIOCompletion co = {
.coroutine = qemu_coroutine_self(),
};
/* 调用rbd驱动实现的接口qemu_rbd_aio_preadv */
acb = drv->bdrv_aio_preadv(bs, offset, bytes, qiov, flags, bdrv_co_io_em_complete, &co);
/* RBD接口时异步io接口,需要注册io完成时的回调 bdrv_co_io_em_complete 函数*/
if (acb == NULL) { // 执行出错,返回EIO错误
return -EIO;
} else {
/* 正常执行,协程跳转到进入协程bdrv_coroutine_enter时的上下文,相当于直接返回了
* qemu_coroutine_yield还保存了当前上下文,相当于返回地址,这样在异步io完成之后,回调函数可以跳转到这里
* /
qemu_coroutine_yield();
/* io完成,由bdrv_co_io_em_complete跳转到这里 */
return co.ret;
}
}
static void bdrv_co_io_em_complete(void *opaque, int ret)
{
CoroutineIOCompletion *co = opaque;
co->ret = ret; // 将异步io接口的返回值给协程
aio_co_wake(co->coroutine); // 跳转到协程yield时候的上下文
}
aio_co_wake
aio_co_enter
qemu_aio_coroutine_enter
qemu_coroutine_switch(from, to, COROUTINE_ENTER)
跳转到bdrv_aio_preadv函数的return co.ret一行,逐级返回,完成整个io流程
bdrv_aio_preadv
/* 调用file文件的 raw_co_preadv 接口*/
drv->bdrv_co_readv(bs, sector_num, nb_sectors, qiov)
raw_co_preadv
raw_co_prw(bs, offset, bytes, qiov, QEMU_AIO_READ)
/* 获取磁盘文件的事件循环上下文,直接利用QEMU的事件循环机制poll异步IO * /
LinuxAioState *aio = aio_get_linux_aio(bdrv_get_aio_context(bs));
laio_co_submit(bs, aio, s->fd, offset, qiov, type) // 提交IO流程
int coroutine_fn laio_co_submit(BlockDriverState *bs, LinuxAioState *s, int fd,
uint64_t offset, QEMUIOVector *qiov, int type)
{
int ret;
struct qemu_laiocb laiocb = {
.co = qemu_coroutine_self(),
.nbytes = qiov->size,
.ctx = s,
.ret = -EINPROGRESS,
.is_read = (type == QEMU_AIO_READ),
.qiov = qiov,
};
ret = laio_do_submit(fd, &laiocb, offset, type); // 提交io流程
if (ret < 0) {
return ret;
}
if (laiocb.ret == -EINPROGRESS) {
qemu_coroutine_yield(); // 跳转到enter协程的地方
}
return laiocb.ret;
}
io_setup,io_set_eventfd,io_submit,io_getevents,io_destroy
raw_open
raw_open_common
aio_setup_linux_aio(bdrv_get_aio_context(bs), errp))
LinuxAioState *aio_setup_linux_aio(AioContext *ctx, Error **errp)
{
if (!ctx->linux_aio) { // LinuxAioState每个磁盘AioContext中包含一个,如果没有就创建
ctx->linux_aio = laio_init(errp); // 初始化LinuxAioState
if (ctx->linux_aio) {
laio_attach_aio_context(ctx->linux_aio, ctx); // 将其关联到AioContext上,下一次提交IO前直接取该磁盘对应的LinuxAioState
}
}
return ctx->linux_aio;
}
laio_init
event_notifier_init(&s->e, false) //创建eventfd,用作异步IO Poll
/* 设置其最大可监听数量,注意,MAX_EVENTS值决定了一次性可并发提交IO的数量 */
io_setup(MAX_EVENTS, &s->ctx)
/* 初始化IO提交队列 */
ioq_init(&s->io_q)
/* 回到raw_open_common 添加libaio的fd到事件循环 */
s->aio_context = new_context;
s->completion_bh = aio_bh_new(new_context, qemu_laio_completion_bh, s);
/* 事件循环监听LinuxAioState的EventNotifier,当其rfd准备好之后,调用注册的回调 qemu_laio_completion_cb */
aio_set_event_notifier(new_context, &s->e, false,
qemu_laio_completion_cb,
qemu_laio_poll_cb);
qemu_laio_completion_cb
qemu_laio_process_completions_and_submit
qemu_laio_process_completions
qemu_laio_process_completion(laiocb)
aio_co_wake(laiocb->co)
aio_co_enter(ctx, co)
qemu_aio_coroutine_enter(ctx, co)
从laio_co_submit的qemu_coroutine_yield()返回,完成整个io流程
laio_do_submit
/* libaio接口,设置内核IO完成后通知用户空间程序的fd */
io_set_eventfd(&laiocb->iocb, event_notifier_get_fd(&s->e))
/* 排队,将libaiocb作为队列一员,提交给磁盘的IO提交队列LinuxAioState->io_q
* 如果IO提交阻塞并且队列长度小于MAX_EVENTS,不要提交IO,累计到MAX_EVENTS在一次性提交
*/
QSIMPLEQ_INSERT_TAIL(&s->io_q.pending, laiocb, next);
s->io_q.in_queue++;
if (!s->io_q.blocked &&
(!s->io_q.plugged ||
s->io_q.in_flight + s->io_q.in_queue >= MAX_EVENTS)) {
ioq_submit(s);
/* 调用libaio接口提交IO */
io_submit(s->ctx, len, iocbs)
}