这篇文章讲讲服务器端RPC报文的处理流程。服务器端RPC报文的处理函数是svc_process,这个函数位于net/sunrpc/svc.c中。这个函数需要一个svc_rqst结构的指针作为参数,svc_rqst是与RPC请求相关的一个数据结构,这里包含了接收到的RPC消息,RPC消息的解析结果也放在这个数据结构中,RPC消息的处理结果也放在这个消息中了。这个数据结构的定义在include/linux/sunrpc/svc.h。由于我主要想讲解NFS,所以很多RPC的知识就略过不讲了。
- /*
- * Process the RPC request.
- */
- int
- svc_process(struct svc_rqst *rqstp)
- {
- // 这是一块缓存,服务器从网卡中接收到RPC消息后存放在这里(已经去掉了IP头,TCP或UDP报文头)
- // argv指向了缓存的起始地址,从这里开始就是RPC报文头了。
- struct kvec *argv = &rqstp->rq_arg.head[0];
- // 这是一块缓存,这块缓存用来存放RPC应答消息,现在还没有分配内存。
- struct kvec *resv = &rqstp->rq_res.head[0];
- struct svc_serv *serv = rqstp->rq_server;
- u32 dir;
- /*
- * Setup response xdr_buf.
- * Initially it has just one page
- */
- rqstp->rq_resused = 1;
- // 为RPC应答消息分配内存
- resv->iov_base = page_address(rqstp->rq_respages[0]);
- resv->iov_len = 0;
- rqstp->rq_res.pages = rqstp->rq_respages + 1;
- rqstp->rq_res.len = 0;
- rqstp->rq_res.page_base = 0;
- rqstp->rq_res.page_len = 0;
- rqstp->rq_res.buflen = PAGE_SIZE;
- rqstp->rq_res.tail[0].iov_base = NULL;
- rqstp->rq_res.tail[0].iov_len = 0;
- rqstp->rq_xid = svc_getu32(argv); // 这里开始解析RPC报文了,按照RPC报文格式,这是RPC消息的XID。
- dir = svc_getnl(argv); // 这个函数解析出了RPC保本中的第二个字段(Message Type)
- if (dir != 0) { // RPC协议规定,RPC应答消息中Message Type字段必须为0.如果不是0就不处理了。
- /* direction != CALL */
- svc_printk(rqstp, "bad direction %d, dropping request\n", dir);
- serv->sv_stats->rpcbadfmt++;
- svc_drop(rqstp); // 丢弃这个RPC报文。
- return 0;
- }
- // 现在可以确定这是一个RPC请求报文了,调用svc_process_common()进行处理,这是RPC请求的主处理函数,并且会填充RPC应答报文。
- // 返回值1表示处理过程正常,已经正常填充了RPC应答消息,可以发送给客户端了。
- // 返回值0表示RPC请求报文格式异常,直接丢弃这个报文。
- /* Returns 1 for send, 0 for drop */
- if (svc_process_common(rqstp, argv, resv))
- return svc_send(rqstp); // 这是发送RPC应答消息的报文,不会深入分析这个函数了。
- else {
- svc_drop(rqstp); // 丢弃RPC报文。
- return 0;
- }
- }
svc_process_common是主要的处理函数,这个函数的定义如下:
static int svc_process_common(struct svc_rqst *rqstp, struct kvec *argv, struct kvec *resv)参数rqstp 表示一个RPC请求。
参数argv是一块缓存,这块缓存中保存了接收到的RPC请求报文。
参数resv是一块缓存,这块缓存用来保存组装后的RPC应答报文。
这个函数的处理流程就比较复杂了,基本上包含下面五个处理步骤:
(1)组装RPC报文头基本信息
(2)解析RPC服务信息
(3)对用户身份进行验证
(4)检查服务器端是否可以处理这个RPC请求
(5)处理RPC请求
下面详细讲解各个步骤
(1)组装RPC报文头基本信息
svc_process()中解析了RPC请求报文头的前两个字段(XID和Message Type),这里解析解析请求报文中的第3个字段,组装了应答报文中的前三个字段。处理程序如下:
- svc_putu32(resv, rqstp->rq_xid); // 组装RPC应答报文中的第一个字段XID
- vers = svc_getnl(argv); // 解析RPC请求报文中的第三个字段 RPC Version
- /* First words of reply: */
- svc_putnl(resv, 1); // 组装RPC应答报文中第二个字段Message Type,RPC应答消息中这个字段固定为1.
- // 目前通用的RPC版本是2,服务器端只支持RPC版本2。如果不是版本2,则不处理了。
- if (vers != 2) /* RPC version number */
- goto err_bad_rpc;
- // 组装RPC应答消息的第三个字段 Reply State,这里暂时将这个字段设置为0了,表示正常。
- // 但这只是一个暂时值,如果后面用户验证过程出错了,会修改这个字段。
- // reply_statp就是指向了这个字段的位置,当出错后可以根据reply_statp指针找到这个字段。
- reply_statp = resv->iov_base + resv->iov_len; // 写到这个位置了
- svc_putnl(resv, 0); /* ACCEPT */
(2)解析RPC服务信息
- // 解析RPC请求消息中第4个字段Program,这个字段是RPC服务程序(如NFS服务)的编号
- rqstp->rq_prog = prog = svc_getnl(argv); /* program number */
- // 解析RPC请求消息中第5个字段Version,这个字段是RPC服务程序的版本号
- rqstp->rq_vers = vers = svc_getnl(argv); /* version number */
- // 解析RPC请求消息中第6个字段Procedure,这个字段是RPC服务例程的编号
- rqstp->rq_proc = proc = svc_getnl(argv); /* procedure number */
也很简单,直接从RPC请求消息中提取数据就可以了。
(3)对用户身份进行验证
- auth_res = svc_authenticate(rqstp, &auth_stat);
- /* Also give the program a chance to reject this call: */
- if (auth_res == SVC_OK && progp) {
- auth_stat = rpc_autherr_badcred;
- auth_res = progp->pg_authenticate(rqstp);
- }
这里包含了两个函数svc_authenticate和progp->pg_authenticate。svc_authenticate的作用是解析RPC请求报文中的认证信息, progp->pg_authenticate的作用是根据解析出的信息对用户身份进行验证。这两个函数都和采用的认证方式有关,我们这里只简单介绍,下一篇文章中将以UNIX认证为例详细讲解解析和认证过程。
解析RPC消息中用户信息的函数是svc_authenticate()。rqstp是输入参数,表示一个RPC请求;auth_stat是输出参数,表示解析结果。这个函数的代码如下:
- int
- svc_authenticate(struct svc_rqst *rqstp, __be32 *authp)
- {
- rpc_authflavor_t flavor;
- struct auth_ops *aops;
- *authp = rpc_auth_ok;
- // 前面的函数已经解析出了RPC请求报文中前6个字段,下面该解析第7个字段Credential了。
- // Credential中第一个字段是Flavor,表示认证方式。
- flavor = svc_getnl(&rqstp->rq_arg.head[0]);
- dprintk("svc: svc_authenticate (%d)\n", flavor);
- spin_lock(&authtab_lock);
- // 判断服务器端是否支持这种认证方式
- // authtab[flavor]是这种认证方式的操作函数集合
- if (flavor >= RPC_AUTH_MAXFLAVOR || !(aops = authtab[flavor]) ||
- !try_module_get(aops->owner)) {
- spin_unlock(&authtab_lock);
- *authp = rpc_autherr_badcred; // 不支持这种认证方式,这是错误码
- return SVC_DENIED;
- }
- spin_unlock(&authtab_lock);
- rqstp->rq_authop = aops; // 认证方式的操作函数集合
- // 调用具体认证方式中的accept()函数解析RPC报文中的认证信息。
- // 每种认证方式都有自己的处理函数。
- return aops->accept(rqstp, authp);
- }
这个函数的主要作用是解析RPC请求消息中的Credential字段和Verifier字段,然后填充RPC应答消息中的Verifier字段。这个函数只解析了Credential中的第一个字段,这个字段表示认证类型,然后就调用相应认证方式中的函数进行处理了。目前Linux中支持下列认证方式
- enum rpc_auth_flavors {
- RPC_AUTH_NULL = 0,
- RPC_AUTH_UNIX = 1,
- RPC_AUTH_SHORT = 2,
- RPC_AUTH_DES = 3,
- RPC_AUTH_KRB = 4,
- RPC_AUTH_GSS = 6,
- RPC_AUTH_MAXFLAVOR = 8,
- /* pseudoflavors: */
- RPC_AUTH_GSS_KRB5 = 390003,
- RPC_AUTH_GSS_KRB5I = 390004,
- RPC_AUTH_GSS_KRB5P = 390005,
- RPC_AUTH_GSS_LKEY = 390006,
- RPC_AUTH_GSS_LKEYI = 390007,
- RPC_AUTH_GSS_LKEYP = 390008,
- RPC_AUTH_GSS_SPKM = 390009,
- RPC_AUTH_GSS_SPKMI = 390010,
- RPC_AUTH_GSS_SPKMP = 390011,
- };
RPC_AUTH_MAXFLAVOR表示认证方式种类,下面的认证方式全部属于RPC_AUTH_GSS认证的子类。每种认证方式都需要实现下面的函数
- struct auth_ops {
- char * name;
- struct module *owner;
- int flavour;
- int (*accept)(struct svc_rqst *rq, __be32 *authp);
- int (*release)(struct svc_rqst *rq);
- void (*domain_release)(struct auth_domain *);
- int (*set_client)(struct svc_rqst *rq);
- };
下篇文章中我们会详细介绍UNIX认证的操作过程,这里就不深入讲解了。
(4)检查服务器端是否可以处理这个RPC请求
- progp = serv->sv_program; // 取出这个端口中注册的RPC服务处理程序
- // 每个端口上可以注册多种RPC服务,这些RPC服务的处理程序构成了一个链表,
- // 遍历链表中的每一种处理程序,检查是否支持接收到的RPC请求。
- for (progp = serv->sv_program; progp; progp = progp->pg_next)
- if (prog == progp->pg_prog) // 比较程序编号,如果编号相等就表示找到处理程序了。
- break;
- // progp就是找到的处理程序。如果遍历到链表结尾也没有找到编号相等的处理程序,
- // 则表示服务器不能处理这个请求。这种情况下,由于已经遍历到链表结尾,progp就是NULL。
- if (progp == NULL) // 如果没有处理例程,退出。
- goto err_bad_prog;
- // 对比完RPC程序编号后还需要对比程序版本号,如果版本号不相等也不能处理。
- if (vers >= progp->pg_nvers ||
- !(versp = progp->pg_vers[vers]))
- goto err_bad_vers;
- // versp是RPC处理程序中的一个版本,每个版本的处理程序中包含多个处理例程,
- // procp根据例程编号找到了处理例程,需要检查服务器端是否实现了这个处理例程。
- // procp->pc_func就是这个处理例程的处理函数了,如果服务器端没有实现这个例程,
- // 也直接退出。
- procp = versp->vs_proc + proc; // 取出处理例程
- if (proc >= versp->vs_nproc || !procp->pc_func)
- goto err_bad_proc;
- rqstp->rq_procinfo = procp; // 设置处理程序
- /* Syntactic check complete */
- serv->sv_stats->rpccnt++;
- /* Build the reply header. */
- statp = resv->iov_base +resv->iov_len;
- // 这是RPC应答消息的第5个字段Accept State,先初始化为RPC_SUCCESS。
- // 如果这个RPC的处理过程出错了,会修改这个字段的值,修改为相应的错误码。
- svc_putnl(resv, RPC_SUCCESS); // 认证通过
该解释的内容都写在注释中了,这里说明一下Linux中保存RPC处理程序的数据结构。首先是RPC例程的数据结构。
- struct svc_procedure {
- // 这是RPC请求的处理函数
- svc_procfunc pc_func; /* process the request */
- // 这是RPC请求的解码函数,RPC报文的内容是pc_func的参数,
- // 这个函数负责解析这些内容
- kxdrproc_t pc_decode; /* XDR decode args */
- // 这是RPC请求的编码函数,服务器端需要将pc_func的处理结果封装到
- // RPC应答报文中,这就是封装函数
- kxdrproc_t pc_encode; /* XDR encode result */
- // 这是释放内存的一个函数,因为pc_func可能需要分配额外的内存
- kxdrproc_t pc_release; /* XDR free result */
- // 这是RPC请求报文中数据的长度
- unsigned int pc_argsize; /* argument struct size */
- // 这是RPC应答报文中数据的长度
- unsigned int pc_ressize; /* result struct size */
- // 这是这个例程的调用次数,就是一个统计量
- unsigned int pc_count; /* call count */
- // 这是缓存类型,NFS中某些请求可以缓存处理结果。当再次接收到相同的请求后,
- // 就不处理了,直接将缓存中的数据返回给客户端就可以了。
- unsigned int pc_cachetype; /* cache info (NFS) */
- // 这是调整RPC应答消息缓存的一个数据量
- unsigned int pc_xdrressize; /* maximum size of XDR reply */
- };
下面是RPC版本的数据结构,一个版本中包含多个例程。
- struct svc_version {
- // 版本编号
- u32 vs_vers; /* version number */
- // 这个版本中RPC例程的数量
- u32 vs_nproc; /* number of procedures */
- // 这里包含了各个RPC例程的处理函数,这里不是一个例程,
- // 这个版本中所有例程的处理函数都在这里,各个例程按顺序排列。
- struct svc_procedure * vs_proc; /* per-procedure info */
- // 这也是从组装应答消息相关的缓存的一个长度
- u32 vs_xdrsize; /* xdrsize needed for this version */
- // 如果这个值为1,就说明虽然定义了这个版本的处理例程,但是不对外提供服务
- unsigned int vs_hidden : 1; /* Don‘t register with portmapper.
- * Only used for nfsacl so far. */
- /* Override dispatch function (e.g. when caching replies).
- * A return value of 0 means drop the request.
- * vs_dispatch == NULL means use default dispatcher.
- */
- // 这是RPC请求的处理函数,简单来说就是依次调用svc_procedure中的pc_decode、pc_func、pc_encode函数.
- // NFS中这个函数是nfsd_dispatch().
- int (*vs_dispatch)(struct svc_rqst *, __be32 *);
- };
最后是RPC服务程序的数据结构,每个RPC服务程序包含多个版本。
- struct svc_program {
- // 指向了下一套处理程序,可以将多套处理程序注册在同一个端口上
- struct svc_program * pg_next; /* other programs (same xprt) */
- // RPC程序编号
- u32 pg_prog; /* program number */
- // 这是最低版本
- unsigned int pg_lovers; /* lowest version */
- // 这是最高版本
- unsigned int pg_hivers; /* lowest version */
- // 服务程序中版本的数量
- unsigned int pg_nvers; /* number of versions */
- // 这是各个版本处理程序的指针
- struct svc_version ** pg_vers; /* version array */
- // RPC服务名称
- char * pg_name; /* service name */
- // 属于某个类别,同类别的RPC服务共享相同的认证方式
- char * pg_class; /* class name: services sharing authentication */
- // 这里包含了一些统计信息
- struct svc_stat * pg_stats; /* rpc statistics */
- // 这是RPC处理程序中验证用户信息的函数.
- int (*pg_authenticate)(struct svc_rqst *);
- };
对于NFS服务来说,NFS服务对应的数据结构是svc_program。NFS目前包含3个不同的版本(NFSV2、NFSV3、NFSV4),每个版本对应一个svc_version结构。每个版本中包含多个处理例程,每个处理例程对应一个svc_procedure结构。
还有一点需要注意,svc_program中包含一个函数pg_authenticate,需要注意这个函数和前面提到的认证方式中accept的区别。accept的作用是解析RPC报文中的认证信息,只是解析数据,但是不对用户进行认证。pg_authenticate才是真正的认证函数。
(5)处理RPC请求
- /* Call the function that processes the request. */
- if (!versp->vs_dispatch) {
- /* Decode arguments */
- xdr = procp->pc_decode; // 这是RPC请求的解码函数
- // 开始解码了,解析RPC报文中的数据
- if (xdr && !xdr(rqstp, argv->iov_base, rqstp->rq_argp))
- goto err_garbage;
- // 处理请求
- *statp = procp->pc_func(rqstp, rqstp->rq_argp, rqstp->rq_resp);
- /* Encode reply */
- if (rqstp->rq_dropme) {
- if (procp->pc_release)
- procp->pc_release(rqstp, NULL, rqstp->rq_resp);
- goto dropit;
- }
- // 编码处理结果,将处理结果封装到RPC应答消息中
- if (*statp == rpc_success &&
- (xdr = procp->pc_encode) &&
- !xdr(rqstp, resv->iov_base+resv->iov_len, rqstp->rq_resp)) {
- dprintk("svc: failed to encode reply\n");
- /* serv->sv_stats->rpcsystemerr++; */
- *statp = rpc_system_err;
- }
- } else {
- dprintk("svc: calling dispatcher\n");
- if (!versp->vs_dispatch(rqstp, statp)) { // 这个函数负责处理请求
- /* Release reply info */
- if (procp->pc_release)
- procp->pc_release(rqstp, NULL, rqstp->rq_resp);
- goto dropit;
- }
- }
经过步骤1--步骤4的处理,我们已经解析了RPC请求报头的数据,找到了RPC请求的处理函数,最后一步就是开始处理这个请求了。处理一个RPC请求的函数是svc_version结构中的vs_dispatch函数。如果RPC程序没有定义这个函数,就按照标准的流程进行处理。在标准的流程中,首先调用svc_procedure结构中的pc_decode函数,这个函数的内容是解析RPC报文的净荷,对于NFS服务来说,这个函数的作用就是解析RPC报文中的NFS数据,这些数据就是处理函数的参数。真正的处理函数是svc_procedure结构中的pc_func函数,每个例程都需要定义自己的处理函数。处理完毕后,需要将处理结果封装在RPC应答报文中返回给客户端。比如对于READ操作,我们需要将读取的数据封装在RPC报文中返回,这个封装过程是由svc_procedure结构中的pc_encode函数实现的。
NFS服务定义了自己的vs_dispatch函数,NFSV2、NFSV3、NFSV4使用了同一个vs_dispatch函数,这个函数的定义是nfsd_dispatch,这个函数定义在fs/nfsd/nfssvc.c中,处理流程基本上和上面讲的流程相同,就不讲解了。