PVFS2为每个用户系统接口(src/client/sysint)定义了状态机,而状态机的执行逻辑存在于src/common/misc文件夹下的state-machine.h和state-machine-fns.c两个文件。state-machine.h定义了状态机执行逻辑所需的数据结构,state-machine-fns.c文件定义了功能函数。
- 数据结构
state-machine.h的数据结构各个条目有何含义我们暂不细究,但需首先明确这些数据与函数的作用关系。例如,标志状态机实例的状态机控制块
struct qlist_head frames; /* circular list of frames */
int base_frame; /* index of current base frame */
int frame_count; /* number of frames in list */
/* usage specific routinet to look up SM from OP */
struct PINT_state_machine_s *(*op_get_state_machine)(int);
/* state machine context and control variables */
int op; /* this field externally indicates type of state machine */
PVFS_id_gen_t op_id; /* unique ID for this operation */
struct PINT_smcb *parent_smcb; /* points to parent smcb or NULL */
int op_terminate; /* indicates SM is ready to terminate */
int op_cancelled; /* indicates SM operation was cancelled */
int children_running; /* the number of child SMs running */
int op_completed; /* indicates SM operation was added to completion Q */
/* add a lock here */
job_context_id context; /* job context when waiting for children */
int (*terminate_fn)(struct PINT_smcb *, job_status_s *);
void *user_ptr; /* external user pointer */
int immediate; /* specifies immediate completion of the state machine */
} PINT_smcb;
注意其中的控制变量(control variables
),即第19~22行,表明了当前操作的状态,由某些函数设定后,后续函数读取并根据这些“标志位”采取不同的行为。这是将函数关联起来并控制流程的重要手段。
除此之外,该状态机控制块中还存储状态机所需数据,如状态栈(第5~6行)、帧栈(第8~10行);以及指向相关数据的引用,如父状态机控制块指针(第18行)、任务上下文ID(第24行)等。
其中帧(frame)是个很重要的角色:
帧保存状态机所需的额外数据,通常与状态机实例一一对应,它的行为伴随着状态机的产生和撤销。当一个状态机启动嵌套状态机时,它会在帧栈中压入新帧。帧栈使用quicklist
实现。
state-machine.h中的其他数据结构很多在状态机定义中(参见sys-get-eattr.c
)已经提及,这里就是它们的原型。
- 功能函数
下面重点分析state-machine-fns.c文件,即状态机控制逻辑的操作。阅读函数的顺序和原文件中不一定相同。另外,有关gossip_debug函数及其前后数据准备的部分可以略去,只是日志功能;下面代码都是略过这些之后的核心逻辑。
- 帧栈相关操作
数据结构已在上一部分介绍,有关帧的操作在状态机执行中很普遍,首先集中介绍一下。
该函数通过索引(index)获得状态机控制块*smcb中的指定帧。帧栈是一个双向循环链表,但支持按索引号查找,栈底编号为0,向栈顶依次递增;该编号称作索引的绝对值。【注意】这里的参数index指相对值,是与当前状态机对应帧的偏移量,0表示当前状态机对应帧本身,负数表示本状态机之前压入的帧,正数表示由本状态机压入的帧。
第5行中,base_frame记录了当前状态机对应帧的绝对索引值,与相对值index相加,得到目标帧的绝对索引值。第7行实际上将prev指向了栈底,因为frames是该循环链表的表头。第8~12行就根据绝对索引值target逐步定位到指定帧,最后将该帧的指针返回。
除此之外,还有如下两个函数的定义:
分别是帧的入栈和出栈操作,代码较简单,不再赘述。
- PINT_state_machine_invoke
用于触发动作函数,是推动状态转移的基本操作。
return retval;
}
其实最核心的操作就是第10行,调用当前状态的动作函数。然后根据返回值设定控制变量(第15行),以影响状态机的后续行为。
第27~40行是处理并行调用子状态机的情况,PINT_sm_start_child_frames函数见后。
- PINT_state_machine_next和PINT_state_machine_continue
这两个函数是使状态机运转起来的主要动力。主要逻辑在INT_state_machine_next中:
smcb->current_state = transtbl[i].next_state;
/* To do nested states, we check to see if the next state is
* a nested state machine, and if so we push the return state
* onto a stack */
while (smcb->current_state->flag == SM_JUMP)
{
PINT_push_state(smcb, smcb->current_state);
smcb->current_state =
smcb->current_state->action.nested->first_state;
}/* runs state_action and returns the return code */
ret = PINT_state_machine_invoke(smcb, r);} while (ret == SM_ACTION_COMPLETE || ret == SM_ACTION_TERMINATE);
return ret;
}其中的核心操作主要是三步:
(1) 第一步是第21~25行,顺序查找转移表transtbl,在标志值(在transtbl中用return_value记录,在动作函数中则标记在任务状态r的error_code中)吻合时命中,跳出循环并使用当前转移;
(2) 第二步是第44行,current_state变为转移后的下一个状态;
(3) 第三步是第57行,触发新状态的动作函数。
上述三步在最外层的do-while循环中反复进行,只要动作函数返回正常完成,或者提示终止(进入下一轮循环并在第26行实际终止)。从而将状态机运转起来。
在核心的三步之间,还穿插了一些操作,主要是针对嵌套状态机(nested state machine)。第13~42行的do-while循环处理嵌套状态机返回的情况,该情况由转移的flag标识。此时下一个状态不再从转移中获得,而是从状态栈中弹出,从而恢复到父状态机调用嵌套状态机前的状态。相应地,第49~54行的while循环处理启动嵌套状态机的情况,首先把现有状态压入状态栈保留起来,同时把下一个状态设为嵌套状态机的首状态。
另外,第7~10行保留下来主要是说明控制变量对函数执行的影响,这些控制变量由之前执行的操作设定。
PINT_state_machine_continue是以PINT_state_machine_next函数为基础:
只是在调用PINT_state_machine_next之后,如果返回值为终止,则执行PINT_state_machine_terminate函数(参见)。
- PINT_state_machine_start
这是在状态机控制块smcb准备好后,开启状态机的函数。先对smcb的immediate和base_frame进行设定,然后执行首状态的动作函数,根据返回值判断,如果函数返回正常结束或提示终止则继续下去,如果是推迟(SM_ACTION_DEFERRED)说明有非同步的耗时操作或子状态机运行(如第2个函数PINT_state_machine_invoke的第36~37行),需等待其结束,暂将当前状态机置于挂起状态。
- PINT_sm_start_child_frames
有些状态的动作是启动子状态机,子状态机执行时父状态机的执行流不受影响,类似于中断机制。【注意】区别于嵌套状态机,嵌套状态机会保留父状态机的状态,并在执行完成后恢复,类似一般函数调用;参见第3条PINT_state_machine_next函数的相关代码。
启动子状态机的函数:
*children_started = 0;
my_frame = PINT_sm_frame(smcb, PINT_FRAME_CURRENT);
/* Iterate once up front to determine how many children we are going to
* run. This has to be set before starting any children, otherwise if
* the first one immediately completes it will mistakenly believe it is
* the last one and signal the parent.
*/
qlist_for_each_entry(f, &smcb->frames, link)
{
/* run from TOS until the parent frame */
if(f->frame == my_frame)
{
break;
}
/* increment parent's counter */
smcb->children_running++;
}
/* let the caller know how many children are being started; it won't be
* able to tell from the running_count because they may all immediately
* complete before we leave this function.
*/
*children_started = smcb->children_running;qlist_for_each_entry(f, &smcb->frames, link)
{
/* run from TOS until the parent frame */
if(f->frame == my_frame)
{
break;
}
/* allocate smcb */
PINT_smcb_alloc(&new_sm, smcb->op, 0, NULL,
child_sm_frame_terminate, smcb->context);
/* set parent smcb pointer */
new_sm->parent_smcb = smcb;
/* assign frame */
PINT_sm_push_frame(new_sm, f->task_id, f->frame);
/* locate SM to run */
new_sm->current_state = PINT_sm_task_map(smcb, f->task_id);
/* invoke SM */
retval = PINT_state_machine_start(new_sm, &r);
// ......
}
}第11~32行用于计算子状态机的数目,因为帧和子状态机是一一对应的,而子状态机的帧都压在当前状态机的帧之后,所以由帧栈顶倒数至当前帧即可算出子状态机数目,这是第18~27行循环所做的操作。
第34~53行的循环和上述循环结构相同,相当于对子状态机的帧进行了第42~51行的操作:创建状态机控制块,将子状态机的帧压入新的状态机控制块,找到对应的状态机(第49行函数PINT_sm_task_map,见后),启动新的状态机。
简言之,该函数就是根据已经压入帧栈的子状态机帧分别启动对应的子状态机。那么子状态机的帧从何而来呢?在直接调用该函数的PINT_state_machine_invoke函数中也没有相关操作。这些帧来自于状态机定义的动作函数,这类动作函数对应状态的flag是SM_PJMP(一般为SM_RUN表示执行动作函数,调用嵌套状态机的标记是SM_JUMP),举例如下:
static struct PINT_pjmp_tbl_s ST_setup_getattr_pjtbl[] = {
{ .return_value = 0 ,
.state_machine = &pvfs2_get_attr_work_sm }
};static PINT_sm_action listattr_setup_getattr(
struct PINT_smcb *smcb, job_status_s *js_p)
{
struct PINT_server_op *s_op = PINT_sm_frame(smcb, PINT_FRAME_CURRENT);
struct PINT_server_op *getattr_op;
int ret;
int i;s_op->u.listattr.parallel_sms = 0;
js_p->error_code = 0;for(i=0; i<s_op->req->u.listattr.nhandles; i++)
{
// ......
getattr_op = malloc(sizeof(*getattr_op));
// ......
ret = PINT_sm_push_frame(smcb, 0, getattr_op);
// ......
s_op->u.listattr.parallel_sms++;
}
// ......
if(s_op->u.listattr.parallel_sms > 0)
{
js_p->error_code = 0;
return SM_ACTION_COMPLETE;
}
// ......
}这是src/server/list-attr.c文件,由同目录下list-attr.sm编译而来,与先前分析过的sys-get-eattr.sm
类似。
由第4行可见状态ST_setup_getattr标记为SM_PJMP,在其动作函数listattr_setup_getattr中,第31行将子状态机的帧压入状态机控制块,这一行在for循环中,可压入多个并行子状态的帧。当该动作函数执行完毕后,子状态机的帧均已备好,返回至触发它的PINT_state_machine_invoke函数(参见第2条)后,继而调用PINT_sm_start_child_frames函数,即可根据这些帧启动并行的子状态机。
- PINT_sm_task_map
该函数解决如何通过帧定位到对应子状态机的问题。实际上,启动哪些子状态机是由状态机定义描述的,存储于并行跳转表pjtbl中,如上例的第11~12行;而帧中记录的状态机所需信息包含一个task_id(上例中第31行PINT_sm_push_frame函数的第二个参数就是task_id),
可以通过它查找并行跳转表pjtbl,找到对应状态机。如下该函数的代码,就是通过帧的task_id查找并行跳转表pjtbl定位子状态机的: