通过分析状态机的执行逻辑 ,状态机状态转移和触发嵌套/子状态机的执行过程已经基本清楚,但是一个状态机从创建、启动,到挂起、终止的完整生命周期还需进一步分析。本文涉及多个文件,但主要接口函数包含在src/client/sysint目录下的client-state-machine.h和client-state-machine.c文件中,这些接口函数会调用src/common/id-generator和src/io/job目录下的相关函数。
说明:如下函数名后的括号注明其所在文件,贴出代码为除去gossip日志和次要无关逻辑之后的核心代码,“// ......”表示此处有省略。另外,背景色 相同的行为是配对的,显式标出以方便理解前后对应的流程。
我们知道,状态机的标识是状态机控制块smcb,分配smcb意味着状态机的创建,见函数1。
函数1. PINT_smcb_alloc(src/common/misc/state-machine-fns.c) int PINT_smcb_alloc( struct PINT_smcb **smcb, int op, int frame_size, struct PINT_state_machine_s *(*getmach)(int), int (*term_fn)(struct PINT_smcb *, job_status_s *), job_context_id context_id);
该函数用于分配状态机控制块。第3行参数op是状态机代表的系统操作;第5行参数方程用于将op映射为对应状态机的地址,并依据找到的状态机定义设定smcb->current_state,为执行动作函数和状态转移做好准备;第6行参数是回调函数,在状态机终止时调用;第7行参数是该应用程序的上下文(context),以区别于同时进行的其他应用程序(应用程序指src/apps下的各个程序),详见最后一节“上下文”。
为了更好地说明参数含义,我们以分析过的用户系统接口sys-get-eattr 为例,见函数2。
函数2. PVFS_isys_geteattr_list(src/client/sysint/sys-get-eattr.c) PVFS_error PVFS_isys_geteattr_list( // ...... PVFS_sys_op_id *op_id, void *user_ptr) { int ret = -PVFS_EINVAL; PINT_smcb *smcb = NULL; PINT_client_sm *sm_p = NULL; // ...... PINT_smcb_alloc(&smcb, PVFS_SYS_GETEATTR, sizeof(struct PINT_client_sm), client_op_state_get_machine, client_state_machine_terminate, pint_client_sm_context); // ...... sm_p = PINT_sm_frame(smcb, PINT_FRAME_CURRENT); // ...... sm_p->error_code = 0; // ...... return PINT_client_state_machine_post( smcb, op_id, user_ptr); }
该函数最终被状态机对外接口函数PVFS_sys_geteattr调用。它调用PINT_smcb_alloc时,状态机结束的回调函数设为client_state_machine_terminate,参见函数10 。
第10行参数op赋值为PVFS_SYS_GETEATTR,其定义来自src/client/sysint/client-state-machine.h:enum { // ...... PVFS_SYS_GETEATTR = 13, // ...... PVFS_SYS_READDIRPLUS = 20, PVFS_MGMT_SETPARAM_LIST = 70, // ...... PVFS_MGMT_GET_DIRDATA_HANDLE = 80, PVFS_SERVER_GET_CONFIG = 200, PVFS_CLIENT_JOB_TIMER = 300, PVFS_CLIENT_PERF_COUNT_TIMER = 301, PVFS_DEV_UNEXPECTED = 400 };
op参数赋值给smcb的成员op,用于表达状态机进行的操作,实际上记录了使用哪个状态机。
【注意】另一个参数op_id与之形似但无关,它是在状态机挂起时,在哈希表中注册对应smcb时获得的索引,可视作状态机的一个句柄。
在状态机的执行逻辑 中,我们分析了PINT_state_machine_start函数,而这里主要讨论调用该函数的环节和相关操作。
由上节函数2的第20~21行可知,启动状态机是通过调用PINT_client_state_machine_post函数,即下述函数3。它在第15行调用了PINT_state_machine_start函数。
函数3. PINT_client_state_machine_post(src/client/sysint/client-state-machine.c) PVFS_error PINT_client_state_machine_post( PINT_smcb *smcb, PVFS_sys_op_id *op_id, void *user_ptr /* in */) { PINT_sm_action sm_ret; PVFS_error ret = -PVFS_EINVAL; job_status_s js; int pvfs_sys_op = PINT_smcb_op(smcb); PINT_client_sm *sm_p = PINT_sm_frame(smcb, PINT_FRAME_CURRENT); // ...... memset(&js, 0, sizeof(js)); // ...... gen_mutex_lock(&test_mutex); sm_ret = PINT_state_machine_start(smcb, &js); // ...... if (PINT_smcb_complete(smcb)) { // ...... *op_id = -1; /* free the smcb and any other extra data allocated there */ PINT_sys_release_smcb(smcb); // ...... } else { assert(sm_ret == SM_ACTION_DEFERRED); PINT_id_gen_safe_register(&sm_p->sys_op_id, (void *)smcb); if (op_id) { *op_id = sm_p->sys_op_id; } } gen_mutex_unlock(&test_mutex); return js.error_code; }
除了调用PINT_state_machine_start函数外,它所做的后续工作包括:如果第17行PINT_smcb_complete函数显示状态机已经立即结束了,op_id赋值为-1,否则需将挂起的状态机进行注册 (第28行),注册时获得该状态机的句柄(存储于sm_p->sys_op_id,见第31行),供以后的操作定位状态机。注册函数PINT_id_gen_safe_register只是包裹了src/common/id-generator/id-generator.c中的id_gen_safe_register函数,见函数4.
函数4. id_gen_safe_register(src/common/id-generator/id-generator.c) int id_gen_safe_register( BMI_id_gen_t *new_id, void *item) { id_gen_safe_t *id_elem = NULL; // ...... gen_mutex_lock(&s_id_gen_safe_mutex); id_elem = (id_gen_safe_t *)malloc(sizeof(id_gen_safe_t)); // ...... id_elem->id = ++s_id_gen_safe_tag; // ...... id_elem->item = item; qhash_add(s_id_gen_safe_table, &id_elem->id, &id_elem->hash_link); *new_id = id_elem->id; gen_mutex_unlock(&s_id_gen_safe_mutex); return 0; }
第14行,smcb注册后获得了其在quickhash类型的全局变量s_id_gen_safe_table中的唯一句柄并赋值给new_id传递出去。通过该句柄可获得其对应的状态机控制块。
状态机执行返回SM_ACTION_DEFERRED,表明当前状态机在等待子状态机或其他耗时操作,我们将此状态描述为挂起状态。
还是以用户系统接口sys-get-eattr 为例。上面“创建”一节举出的PVFS_isys_geteattr_list函数是非阻塞的,函数返回并不意味着状态机运行结束。该函数被其阻塞式同名函数(参数不同)调用,见函数5.
函数5. PVFS_sys_geteattr_list(src/client/sysint/sys-get-eattr.c) PVFS_error PVFS_sys_geteattr_list( PVFS_object_ref ref, const PVFS_credentials *credentials, int32_t nkey, PVFS_ds_keyval *key_array, PVFS_sysresp_geteattr *resp_p, PVFS_hint hints) { PVFS_error ret = -PVFS_EINVAL, error = 0; PVFS_sys_op_id op_id; // ...... ret = PVFS_isys_geteattr_list(ref, credentials, nkey, key_array, resp_p, &op_id, hints, NULL); if (ret) { PVFS_perror_gossip("PVFS_isys_geteattr call", ret); error = ret; } else { ret = PVFS_sys_wait(op_id, "geteattr", &error); // ...... } PINT_sys_release(op_id); return error; }
在第12~13行,这个阻塞的PVFS_isys_geteattr_list通过调用非阻塞同名函数获得其op_id。然后在第21行调用PVFS_sys_wait函数等待状态机结束(等待时即阻塞状态)。而PVFS_sys_wait只是包裹了PINT_client_wait_internal函数,如下。
【注意】第24行,状态机完全结束后,调用PINT_sys_release函数取消注册 op_id,并释放对应状态机控制块smcb。
函数6. PINT_client_wait_internal(src/client/sysint/client-state-machine.c) PVFS_error PINT_client_wait_internal( PVFS_sys_op_id op_id, const char *in_op_str, int *out_error, const char *in_class_str) { PVFS_error ret = -PVFS_EINVAL; PINT_smcb *smcb = NULL; PINT_client_sm *sm_p; if (in_op_str && out_error && in_class_str) { smcb = PINT_id_gen_safe_lookup(op_id); assert(smcb); do { ret = PINT_client_state_machine_test(op_id, out_error); } while (!PINT_smcb_complete(smcb) && (ret == 0)); sm_p = PINT_sm_frame(smcb, PINT_FRAME_CURRENT); // ...... } return ret; }
在第12行,先由句柄op_id获得对应的状态机控制块;然后在第16行,该函数不断调用PINT_client_state_machine_test函数检查操作是否结束。PINT_client_state_machine_test函数较为关键,见函数7.
函数7. PINT_client_state_machine_test(src/client/sysint/client-state-machine.c) PVFS_error PINT_client_state_machine_test( PVFS_sys_op_id op_id, int *error_code) { int i = 0, job_count = 0; PVFS_error ret = -PVFS_EINVAL; PINT_smcb *smcb, *tmp_smcb = NULL; PINT_client_sm *sm_p = NULL; job_id_t job_id_array[MAX_RETURNED_JOBS]; job_status_s job_status_array[MAX_RETURNED_JOBS]; void *smcb_p_array[MAX_RETURNED_JOBS] = {NULL}; // ...... gen_mutex_lock(&test_mutex); // ...... smcb = PINT_id_gen_safe_lookup(op_id); // ...... if (PINT_smcb_complete(smcb)) { sm_p = PINT_sm_frame(smcb, PINT_FRAME_CURRENT); *error_code = sm_p->error_code; conditional_remove_sm_if_in_completion_list(smcb); gen_mutex_unlock(&test_mutex); return 0; } ret = job_testcontext(job_id_array, &job_count, /* in/out parameter */ smcb_p_array, job_status_array, 10, pint_client_sm_context); assert(ret > -1); /* do as much as we can on every job that has completed */ for(i = 0; i < job_count; i++) { tmp_smcb = (PINT_smcb *)smcb_p_array[i]; // ...... if (!PINT_smcb_complete(tmp_smcb)) { ret = PINT_state_machine_continue(tmp_smcb, &job_status_array[i]); // ...... } } if (PINT_smcb_complete(smcb)) { sm_p = PINT_sm_frame(smcb, PINT_FRAME_CURRENT); *error_code = sm_p->error_code; conditional_remove_sm_if_in_completion_list(smcb); } gen_mutex_unlock(&test_mutex); return 0; }
第17~24行检查当前状态机是否结束,如果结束就直接进行清理工作,调用conditional_remove_sm_if_in_completion_list函数(第21行)将对应smcb从全局变量s_completion_list中删除 ,该变量收集所有挂起的状态机控制块(【注意】不只限于某个上下文context)。其声明如下:
static PINT_smcb *s_completion_list[MAX_RETURNED_JOBS] = {NULL};
第25~30行,通过函数job_testcontext检查当前上下文 中挂起但可继续的任务(job),调用后参数smcb_p_array指向可继续的状态机控制块的数组,数组大小存在参数job_count中。而后在第39行逐个激活这些状态机,使之继续执行。
job_testcontext主要是包装了对completion_query_context函数的调用,额外功能是设置了一个等待时间上限(如上第29行参数“10”表示超时时限为10秒)。其核心调用completion_query_context函数如下。
函数8. completion_query_contex(src/io/job/job.c)
static int completion_query_context(job_id_t * out_id_array_p, int *inout_count_p, void **returned_user_ptr_array, job_status_s * out_status_array_p, job_context_id context_id) { struct job_desc *query; int incount = *inout_count_p; *inout_count_p = 0; // ...... while (*inout_count_p < incount && (query = job_desc_q_shownext( completion_queue_array[context_id]))) { // ...... fill_status(query, &(returned_user_ptr_array[*inout_count_p]), &(out_status_array_p[*inout_count_p])); // ...... out_id_array_p[*inout_count_p] = query->job_id; job_desc_q_remove(query); (*inout_count_p)++; // ...... } if((*inout_count_p) > 0) { return(1); } else { return (0); } }
其中最关键的数据结构是completion_queue_array,一个由quicklist构成的数组,其声明如下:/* queues of pending jobs */ static job_desc_q_p completion_queue_array[JOB_MAX_CONTEXTS] = {NULL};
该数组的索引即为上下文ID(参见后面“上下文”一节),每个数组元素是quicklist类型的链表,构成完成队列(completion queue),队列由一些挂起但已经可以继续执行的状态机组成。下节函数9提供了completion_queue_array的来源之一,涉及典型的可继续状态机。
状态机终止时除了清理工作,还包括向completion_queue_array中添加元素。
函数9. PINT_state_machine_terminate(src/common/misc/state-machine-fns.c) int PINT_state_machine_terminate(struct PINT_smcb *smcb, job_status_s *r) { struct PINT_frame_s *f; void *my_frame; job_id_t id; /* notify parent */ if (smcb->parent_smcb) { // ...... my_frame = PINT_sm_frame(smcb, PINT_FRAME_CURRENT); /* this will loop from TOS down to the base frame */ /* base frame will not be processed */ qlist_for_each_entry(f, &smcb->parent_smcb->frames, link) { if(my_frame == f->frame) { f->error = r->error_code; break; } } if (--smcb->parent_smcb->children_running <= 0) { /* no more child state machines running, so we can * start up the parent state machine again */ job_null(0, smcb->parent_smcb, 0, r, &id, smcb->context); } return SM_ACTION_DEFERRED; } /* call state machine completion function */ if (smcb->terminate_fn) { (*smcb->terminate_fn)(smcb, r); } return 0; }
当状态机返回SM_ACTION_TERMINATE的时候,调用该函数。如果当前状态机有父状态机(第7行)且父状态机不再有子状态机(第22行),那么就将父状态机控制块作为一个空任务添加到completion_queue_array(第27行job_null函数所做的工作),表明该父状态机可以继续了,这通常发生在最后一个子状态机运行完毕之时。最终,父状态机控制块会被job_testcontext取出来继续执行。
第34行调用了状态机结束时的回调函数,该函数在分配状态机控制块smcb时通过PINT_smcb_alloc设定(参见函数1 )。该回调函数通常设定为client_state_machine_terminate,下面我们关注一下该回调函数所做的清理工作。
函数10. client_state_machine_terminate(src/client/sysint/client-state-machine.c) int client_state_machine_terminate( struct PINT_smcb *smcb, job_status_s *js_p) { int ret; PINT_client_sm *sm_p; sm_p = PINT_sm_frame(smcb, PINT_FRAME_CURRENT); // ...... if (!((PINT_smcb_op(smcb) == PVFS_SYS_IO) && (PINT_smcb_cancelled(smcb)) && (cancelled_io_jobs_are_pending(smcb))) && !PINT_smcb_immediate_completion(smcb)) { // ...... ret = add_sm_to_completion_list(smcb); assert(ret == 0); } else { // ...... } return SM_ACTION_TERMINATE; }
除去取消的IO操作和直接完成的状态机(第8~11行),都会将smcb添加 到全局变量s_completion_list中(第14行add_sm_to_completion_list函数完成此工作),直到彻底完成时再删除(参见函数7 )。
上面有几处提到状态机的上下文context,也是很多函数的参数。上下文一一对应于应用程序,即每个进程开辟一个独有的上下文。因为C语言没有可以保存状态的对象,所以需要在函数调用中层层传递,以保证可以分别系统操作(状态机)的归属。
从实现角度说,任何src/apps/admin文件夹下的应用程序都会调用src/common/misc/pvfs2-util.c文件中的PVFS_util_init_defaults函数,而该函数调用了src/client/sysint/initialize.c中的PVFS_sys_initialize函数,进而调用src/client/sysint/client-state-machine.c中的PINT_client_state_machine_initialize函数。这个函数中就调用了函数11来创建context:
函数11. job_open_context(src/io/job/job.c)
int job_open_context(job_context_id* context_id) { int context_index; /* find an unused context id */ gen_mutex_lock(&completion_mutex); for(context_index=0; context_index<JOB_MAX_CONTEXTS; context_index++) { if(completion_queue_array[context_index] == NULL) { break; } } // ...... /* create a new completion queue for the context */ completion_queue_array[context_index] = job_desc_q_new(); // ...... gen_mutex_unlock(&completion_mutex); *context_id = context_index; return(0); }
该函数通过参数context_id返回新开辟的上下文ID。第6~12行在上下文数目上限内,找到可以存放完成队列的数组位置;第15行在该位置创建新的属于该上下文的完成队列。