RPC状态机执行过程
可能了解这个状态过程对以后工作没用,但还是想了解一下具体执行过程,也算是对自己以前一个工作的交代。
我们用的是SUNRPC,在内核中实现的RPC,代码在net/sunrpc/。
同步RPC和异步RPC
RPC调用有同步和异步两种。
/**
* rpc_call_sync - Perform a synchronous RPC call
* @clnt: pointer to RPC client
* @msg: RPC call parameters
* @flags: RPC call flags
*/
int rpc_call_sync(struct rpc_clnt *clnt, const struct rpc_message *msg, int flags);
/**
* rpc_call_async - Perform an asynchronous RPC call
* @clnt: pointer to RPC client
* @msg: RPC call parameters
* @flags: RPC call flags
* @tk_ops: RPC call ops
* @data: user call data
*/
int
rpc_call_async(struct rpc_clnt *clnt, const struct rpc_message *msg, int flags, const struct rpc_call_ops *tk_ops, void *data);
执行时都会调用rpc_run_task(&task_setup_data);
rpc_run_task
调用rpc_new_task分配一个新的RPC task,再调用rpc_execute执行RPC 任务,执行结果rpc_task返回。
rpc_new_task
/*
* Create a new task for the specified client.
*/
struct rpc_task *rpc_new_task(const struct rpc_task_setup *setup_data)
{
struct rpc_task *task = setup_data->task;
unsigned short flags = 0;
if (task == NULL) {
task = rpc_alloc_task();
if (task == NULL)
goto out;
flags = RPC_TASK_DYNAMIC;
}
rpc_init_task(task, setup_data);
task->tk_flags |= flags;
dprintk("RPC: allocated task %p\n", task);
out:
return task;
}
如果task是空,则调用rpc_alloc_task分配一个task,调用rpc_init_task用struct rpc_task_setup初始化rpc_task,设置task指向的内存空间为全0,会判断task->tk_ops->rpc_call_prepare != NULL,其实是判断task_setup_data->callback_ops回调函数是否有rpc_call_prepare,
const struct rpc_call_ops nfs4_entrycommit_ops = {
.rpc_call_prepare = nfs4_entrycommit_prepare,
.rpc_call_done = nfs4_entrycommit_done,
.rpc_release = nfs4_entrycommit_release,
};
前面在初始化struct rpc_task_setup task_setup_data变量时,有过初始化,rpc_call_prepare非空,task->tk_action = rpc_prepare_task,
/*
* Helper to call task->tk_ops->rpc_call_prepare
*/
void rpc_prepare_task(struct rpc_task *task)
{
task->tk_ops->rpc_call_prepare(task, task->tk_calldata);
}
rpc_prepare_task通过函数指针调用函数nfs4_entrycommit_prepare执行,它会调用rpc_call_start(task),rpc_call_start实际执行task->tk_action = call_start,task->tk_action是函数指针,指向函数call_start,函数名是函数地址,rpc执行过程是状态机,但实际上没有改变状态,是通过回调函数实现的,task->tk_action指向不同的函数,下次就执行不同的函数,另外在struct rpc_task中还有void (tk_callback)(struct rpc_task ),我理解的是异步RPC时回调时使用的,稍后会有验证。
struct rpc_task {
/*
* RPC call state
*/
struct rpc_message tk_msg; /* RPC call info */
/*
* callback to be executed after waking up
* action next procedure for async tasks
* tk_ops caller callbacks
*/
void (*tk_callback)(struct rpc_task *);
void (*tk_action)(struct rpc_task *);
};
rpc_init_task结束后,rpc_new_task也就结束,至此初始化的工作已经完成。
_rpc_execute
rpc_new_task执行结束,开始执行rpc_execute。
rpc_execute调用__rpc_execute开始执行,函数__rpc_execute的注释是这样的,This is the RPC `scheduler’ (or rather, the finite state machine),这才是RPC中传说的有限状态机。
for不停循环,首先判断是否有pending callback,等待回调的函数,void (save_callback)(struct rpc_task ),这是在使用前需要先声明;再判断,这才是有限状态机的下一步。
if (!RPC_IS_QUEUED(task)) {
if (task->tk_action == NULL)
break;
task->tk_action(task);
}
#define RPC_IS_QUEUED(t) test_bit(RPC_TASK_QUEUED, &(t)->tk_runstate)
RPC_IS_QUEUED是判断task的状态,rpc_execute在调用_rpc_execute前会调用rpc_set_active(task),设置rpc_task的状态,设置tk_runstate为RPC_TASK_ACTIVE,一共有3个状态。
#define RPC_TASK_RUNNING 0
#define RPC_TASK_QUEUED 1
#define RPC_TASK_ACTIVE 2
task->tk_action(task),函数指针开始调用call_start,call_start执行会修改task->tk_action,task->tk_action = call_reserve,call_start执行完后,再进入for循环,继续循环,只要task->tk_action非空,就会继续执行,如果是空,break跳出循环。
此处发现一写的很好的博客,参考会给出。
执行过程是:
->call_start
–>call_reserver:调用xprt_reserve,分配一个RPC 请求槽,就是struct rpc_xprt;
—>call_reserveresult
—->call_allocate
—–>call_bind
——>call_connect: 向server端发送前,先连接到server端
还是看图吧,跟着图走一遍流程就清楚了,pnfs的内核代码。
看call_transmit,传输RPC请求,并等待返回。
调用rpc_xdr_encode(task),进行编码,RPC报文分为两部分:RPC报文头和净荷信息。RPC报文头通过rpc_encode_header()组装,净荷信息通过nfs3_xdr_enc_remove3args()组装。它再调用task->tk_msg.rpc_proc->p_encode,调用具体的编码函数进行编码,task->tk_msg是结构体struct rpc_message,task->tk_msg.rpc_proc是在初始化结构体rpc_message时初始化了。
struct rpc_message msg = {
.rpc_proc = &nfs4_procedures[NFSPROC4_CLNT_ENTRYCOMMIT],
};
结构体数组的一个元素,看nfs4_procedures[]这个结构体数组又发现,proc是宏定义,p_encode被初始化为nfs4_xdr_enc_entrycommit,查看该函数的代码,将向server端提交的信息编码。
函数rpc_xdr_encode调用之前初始化的函数nfs4_xdr_enc_entrycommit,其实我是不太能理解这个调用,我理解的函数调用,即使是函数指针调用,也是需要传递参数的,以下的函数调用连传递参数都没有。
SX了,这仅仅是赋值,encode还是指针类型,真正调用时函数指针且有参数传递,在rpcauth_wrap_req中。
encode = task->tk_msg.rpc_proc->p_encode;
task->tk_status = rpcauth_wrap_req(task, encode, req, p,
task->tk_msg.rpc_argp);
encode是函数指针传递,在函数rpcauth_wrap_req,调用 encode(rqstp, data, obj),执行,这个应该涉及函数指针的多种调用方式了。
函数call_transmit,编码结束后,调用xprt_transmit(task),该函数的注释是说发送一个RPC请求给一个端口。
参考[2]博客有如下说:
在实际传输之前要用XDR规范将数据进行编码,这是rpc的约定。看一下xprt_transmit就会发现,底层的rpc使用socket将数据传给服务器的,当然也可以用别的机制,比如任何底层链路协议,只要能进行网络传输的就可以。
我没看出来是socket传输的啊。
另外,请看以下call_status,我的理解是这里状态转变的地方,根据不同的状态采用不同的操作。
client端将RPC请求发送给server端,server端收到请求并处理,将处理结果返回给client端,client端收到后,还会执行到call_status,但是中间是如何执行到call_status的,暂不清楚,call_status会判断task的status,修改task->tk_action=call_decode,再回到_rpc_executefor循环,执行call_decode,调用rpcauth_unwrap_resp,它再调用具体的解码函数nfs4_xdr_dec_entrycommit进行解码。
看到这里,struct rpc_call_ops nfs4_entrycommit_ops,有3个函数,只有1个函数执行,那后两个函数呢?
在call_transmit,执行成功后,task->tk_action = rpc_exit_task,修改task->tk_action,执行rpc_exit_task,通过函数指针task->tk_ops->rpc_call_done调用nfs4_entrycommit_done,entrycommti是完成了,现在要释放占用的资源。
这个时候task->tk_action为空,跳出__rpc_execute的for不停循环,执行rpc_release_task,[rpc_release_task–>rpc_put_task–>rpc_free_task–>rpc_release_calldata],通过函数指针执行前面注册的release函数,nfs4_entrycommit_release。
一般会有3个函数
rpc_call_prepare():发起RPC请求报文前执行的函数,修改task->tk_action的指向;
rpc_call_done():处理完RPC应答报文后执行的函数;
rpc_release()是释放资源的函数,比如释放RPC请求过程中申请的内存,当RPC执行完毕或者失败时都会调用这个函数。
参考:
[1] http://m.blog.csdn.net/blog/ta_nk/7172927
[2] http://blog.csdn.net/dog250/article/details/5303423