NFS客户端RPC请求封装过程

挂载NFS文件系统后,客户端就可以像访问本地文件一样访问服务器端的文件。NFS客户端根据RFC中的规定将用户操作封装到RPC请求报文中发送给服务器端,服务器端接收到RPC请求后进行处理,将处理结果封装到RPC应答报文中返还给客户端。这篇文章中我们讲讲客户端RPC请求报文的封装过程。LInux中RPC的代码位于net/sunrpc/中,客户端和服务器端的代码都在这个目录中。首先介绍几个数据结构,只说明每个数据结构的含义,不详细介绍数据结构中每个字段的含义。

(1) struct rpc_clnt 这个数据结构表示一个RPC客户端,客户端挂载文件系统时会创建一个nfs_server结构,表示挂载的文件系统,同时需要为这个文件系统创建一个RPC客户端,这个文件系统中所有的RPC请求都通过这个RPC客户端发送,这个客户端保存在nfs_server结构的client字段中。

(2) struct rpc_xprt 这是通过socket建立起来的一个链接,每个rpc_clnt关联一个rpc_xprt,rpc_clnt中所有的报文通过这个链接发送出去。

(3) struct rpc_rqst 这是一个RPC请求的数据结构,包含了一个RPC请求的所有信息。不仅包括RPC报文中的信息,还包括超时重发策略等控制信息,以及归属的rpc_xprt(这个请求从哪个socket链路发送出去)。

(4) struct rpc_task 这是一个RPC任务的数据结构。一个RPC请求的发送过程非常复杂,需要组装RPC报文、创建rpc_rqst、选择合适的rpc_xprt、失败后需要进行处理。因此Linux将一个RPC请求的整个处理过程作为一个RPC任务对待,每个RPC任务用一个rpc_task结构表示。由于一个RPC任务的处理比较复杂,如果处理完一个RPC任务再处理另一个RPC任务不太合适。一个RPC任务的处理过程分成了多个步骤,Linux创建了一个有限状态机,每次只处理RPC任务的一个步骤,这个步骤处理完毕后就调度其他的RPC任务。

现在这个阶段我只想讲解NFS(因为这一部分内容已经很多了),所有关于RPC的代码全部跳过不讲,以后可能会专门开个专题讲解RPC代码,那是以后的事情了。因此这篇文章中就不讲解RPC请求的整个处理流程了,只讲解一个NFS请求是如何封装成RPC报文的。

现将两个数据结构 rpc_procinfo和rpc_message。rpc_procinfo是一个RPC例程的数据结构,包含了RPC例程编号、编码函数和解码函数。

  1. struct rpc_procinfo {
  2. // 这是RPC例程编号
  3. u32 p_proc; /* RPC procedure number */
  4. // 参数编码函数,这个函数负责将RPC请求中的信息封装到RPC报文中
  5. kxdreproc_t p_encode; /* XDR encode function */
  6. // 返回值解码函数,这个函数负责解码RPC应答报文中的信息
  7. kxdrdproc_t p_decode; /* XDR decode function */
  8. // 参数长度,以四字节为单位
  9. unsigned int p_arglen; /* argument hdr length (u32) */
  10. // 返回值长度,以四字节为单位
  11. unsigned int p_replen; /* reply hdr length (u32) */
  12. unsigned int p_count; /* call count */
  13. unsigned int p_timer; /* Which RTT timer to use */
  14. u32 p_statidx; /* Which procedure to account */
  15. // 远端例程名称
  16. const char * p_name; /* name of procedure */
  17. };
struct rpc_procinfo {
    // 这是RPC例程编号
        u32                     p_proc;         /* RPC procedure number */
    // 参数编码函数,这个函数负责将RPC请求中的信息封装到RPC报文中
        kxdreproc_t             p_encode;       /* XDR encode function */
    // 返回值解码函数,这个函数负责解码RPC应答报文中的信息
        kxdrdproc_t             p_decode;       /* XDR decode function */
    // 参数长度,以四字节为单位
        unsigned int            p_arglen;       /* argument hdr length (u32) */
    // 返回值长度,以四字节为单位
        unsigned int            p_replen;       /* reply hdr length (u32) */
        unsigned int            p_count;        /* call count */
        unsigned int            p_timer;        /* Which RTT timer to use */
        u32                     p_statidx;      /* Which procedure to account */
    // 远端例程名称
        const char *            p_name;         /* name of procedure */
};

rpc_message是RPC消息的数据结构,rpc_proc就是上面的rpc_proinfo结构,表示一个RPC例程。rpc_argp是一块缓存,这里保存了RPC请求中的数据,rpc_procinfo结构中的p_encode()函数将rpc_argp中的数据封装到请求报文中传递给服务器端。rpc_procinfo结构中的p_decode()函数解码应答报文中的信息,将解码后的数据填充到rpc_resp中。

  1. struct rpc_message {
  2. // RPC例程
  3. struct rpc_procinfo * rpc_proc; /* Procedure information */
  4. // 参数
  5. void * rpc_argp; /* Arguments */
  6. // 返回值
  7. void * rpc_resp; /* Result */
  8. // 用户信息
  9. struct rpc_cred * rpc_cred; /* Credentials */
  10. };
struct rpc_message {
    // RPC例程
        struct rpc_procinfo *   rpc_proc;       /* Procedure information */
    // 参数
        void *                  rpc_argp;       /* Arguments */
    // 返回值
        void *                  rpc_resp;       /* Result */
    // 用户信息
        struct rpc_cred *       rpc_cred;       /* Credentials */
};

REMOVE例程的数据结构如下:

  1. .p_proc = NFS3PROC_REMOVE,
  2. .p_encode = nfs3_xdr_enc_remove3args,
  3. .p_decode = nfs3_xdr_dec_remove3res,
  4. .p_arglen = NFS3_removeargs_sz,
  5. .p_replen = NFS3_removeres_sz,
        .p_proc      = NFS3PROC_REMOVE,                           
        .p_encode    = nfs3_xdr_enc_remove3args,
        .p_decode    = nfs3_xdr_dec_remove3res,                   
        .p_arglen    = NFS3_removeargs_sz,                        
        .p_replen    = NFS3_removeres_sz,

当客户端删除一个文件时,就会执行函数nfs3_proc_remove(),这个函数的流程如下
参数dir: 这是父目录的文件索引节点结构

参数name: 这是要删除文件的名称

  1. static int nfs3_proc_remove(struct inode *dir, struct qstr *name)
  2. {
  3. // 这是REMOVE请求中的参数.
  4. struct nfs_removeargs arg = {
  5. .fh = NFS_FH(dir), // 父目录的文件句柄
  6. .name = *name, // 要删除文件的名称
  7. };
  8. struct nfs_removeres res; // 这是REMOVE请求的返回信息
  9. // rpc_message是RPC请求相关的数据结构,包含了RPC请求的参数、返回值、用户信息.
  10. struct rpc_message msg = {
  11. .rpc_proc = &nfs3_procedures[NFS3PROC_REMOVE],
  12. .rpc_argp = &arg,
  13. .rpc_resp = &res,
  14. };
  15. int status = -ENOMEM;
  16. dprintk("NFS call remove %s\n", name->name);
  17. // RFC1813要求REMOVE请求返回父目录的属性信息,res.dir_attr用来保存返回的属性信息.
  18. res.dir_attr = nfs_alloc_fattr();
  19. if (res.dir_attr == NULL)
  20. goto out; // 分配内存失败了,退出
  21. // 发起RPC调用
  22. status = rpc_call_sync(NFS_CLIENT(dir), &msg, 0);
  23. // 现在res.dir_attr就保存了从服务器返回的父目录的属性.
  24. nfs_post_op_update_inode(dir, res.dir_attr); // 更新客户端缓存信息
  25. nfs_free_fattr(res.dir_attr); // 释放内存.
  26. out:
  27. dprintk("NFS reply remove: %d\n", status);
  28. return status;
  29. }
static int nfs3_proc_remove(struct inode *dir, struct qstr *name)
{
        // 这是REMOVE请求中的参数.
        struct nfs_removeargs arg = {
                .fh = NFS_FH(dir),      // 父目录的文件句柄
                .name = *name,          // 要删除文件的名称
        };
        struct nfs_removeres res;       // 这是REMOVE请求的返回信息
        // rpc_message是RPC请求相关的数据结构,包含了RPC请求的参数、返回值、用户信息.
        struct rpc_message msg = {
                .rpc_proc = &nfs3_procedures[NFS3PROC_REMOVE],
                .rpc_argp = &arg,
                .rpc_resp = &res,
        };
        int status = -ENOMEM;

        dprintk("NFS call  remove %s\n", name->name);
        // RFC1813要求REMOVE请求返回父目录的属性信息,res.dir_attr用来保存返回的属性信息.
        res.dir_attr = nfs_alloc_fattr();
        if (res.dir_attr == NULL)
                goto out;       // 分配内存失败了,退出

        // 发起RPC调用
        status = rpc_call_sync(NFS_CLIENT(dir), &msg, 0);
        // 现在res.dir_attr就保存了从服务器返回的父目录的属性.
        nfs_post_op_update_inode(dir, res.dir_attr);    // 更新客户端缓存信息
        nfs_free_fattr(res.dir_attr);   // 释放内存.
out:
        dprintk("NFS reply remove: %d\n", status);
        return status;
}

rpc_call_sync()是RPC中的函数,这个函数负责根据REMOVE请求创建一个RPC任务(rpc_task结构),然后调度这个任务,将REMOVE请求发送到服务器端。我们只讲解RPC报文组装过程。RPC报文分为两部分:RPC报文头和净荷信息。RPC报文头通过rpc_encode_header()组装,净荷信息通过nfs3_xdr_enc_remove3args()组装。

  1. static __be32 *rpc_encode_header(struct rpc_task *task)
  2. {
  3. // RPC客户端
  4. struct rpc_clnt *clnt = task->tk_client;
  5. // RPC消息
  6. struct rpc_rqst *req = task->tk_rqstp;
  7. __be32 *p = req->rq_svec[0].iov_base; // 这是实际发送的RPC报文的头部
  8. /* FIXME: check buffer size? */
  9. // 跳过了TCP协议中record fragment的长度
  10. p = xprt_skip_transport_header(task->tk_xprt, p);
  11. *p++ = req->rq_xid; /* XID */ // 这次RPC请求的编号 RPC报文头第1个字段
  12. *p++ = htonl(RPC_CALL); /* CALL */ // 这是一个RPC请求报文 0 RPC报文头第2个字段
  13. *p++ = htonl(RPC_VERSION); /* RPC version */ // 这是RPC版本 2 RPC报文头第3个字段
  14. *p++ = htonl(clnt->cl_prog); /* program number */ // RPC程序号 RPC报文头第4个字段
  15. *p++ = htonl(clnt->cl_vers); /* program version */ // RPC程序的版本号 RPC报文头第5个字段
  16. *p++ = htonl(task->tk_msg.rpc_proc->p_proc); /* procedure */ // RPC例程号 RPC报文头第6个字段
  17. // 编码RPC请求报文中的认证信息
  18. p = rpcauth_marshcred(task, p);
  19. req->rq_slen = xdr_adjust_iovec(&req->rq_svec[0], p); // 这是xdr_buf中数据的总长度
  20. return p;
  21. }
static __be32 *rpc_encode_header(struct rpc_task *task)
{
        // RPC客户端
        struct rpc_clnt *clnt = task->tk_client;
        // RPC消息
        struct rpc_rqst *req = task->tk_rqstp;
        __be32          *p = req->rq_svec[0].iov_base;  // 这是实际发送的RPC报文的头部

        /* FIXME: check buffer size? */
        
       // 跳过了TCP协议中record fragment的长度
        p = xprt_skip_transport_header(task->tk_xprt, p);       
        *p++ = req->rq_xid;             /* XID */       // 这次RPC请求的编号    RPC报文头第1个字段
        *p++ = htonl(RPC_CALL);         /* CALL */      // 这是一个RPC请求报文  0    RPC报文头第2个字段
        *p++ = htonl(RPC_VERSION);      /* RPC version */       // 这是RPC版本 2     RPC报文头第3个字段
        *p++ = htonl(clnt->cl_prog);    /* program number */    // RPC程序号            RPC报文头第4个字段
        *p++ = htonl(clnt->cl_vers);    /* program version */   // RPC程序的版本号     RPC报文头第5个字段
        *p++ = htonl(task->tk_msg.rpc_proc->p_proc);    /* procedure */ // RPC例程号   RPC报文头第6个字段
        // 编码RPC请求报文中的认证信息
        p = rpcauth_marshcred(task, p); 
        req->rq_slen = xdr_adjust_iovec(&req->rq_svec[0], p);   // 这是xdr_buf中数据的总长度
        return p;       
}

task是根据REMOVE信息创建的一个RPC任务,rpc_encode_header()直接编码了RPC报文头前6个字段,RPC包头头中的认证信息通过rpcauth_marshcred()进行封装,这个函数跟认证类型有关,对于UNIX认证来说这个函数是unx_marshal()。

  1. static __be32 *
  2. unx_marshal(struct rpc_task *task, __be32 *p)
  3. {
  4. // 取出RPC客户端的结构
  5. struct rpc_clnt *clnt = task->tk_client;
  6. // 取出认证信息的数据结构
  7. struct unx_cred *cred = container_of(task->tk_rqstp->rq_cred, struct unx_cred, uc_base);
  8. __be32 *base, *hold;
  9. int i;
  10. *p++ = htonl(RPC_AUTH_UNIX); // 编码认证类型,UNIX认证编号是1
  11. base = p++; // base=p; p++ // 为认证长度预留4个字节
  12. *p++ = htonl(jiffies/HZ); // 时间,秒 从这里开始是cred->body 这是一个时间戳
  13. /*
  14. * Copy the UTS nodename captured when the client was created.
  15. */
  16. // 编码RPC客户端的nodename (先是长度,然后是名称,最后按4字节对齐) cl_nodename最长为32字节
  17. p = xdr_encode_array(p, clnt->cl_nodename, clnt->cl_nodelen);
  18. *p++ = htonl((u32) cred->uc_uid); // 用户UID
  19. *p++ = htonl((u32) cred->uc_gid); // 用户GID
  20. hold = p++; // uc_gids的个数
  21. // 编码uc_gids 这是用户所属于的用户组
  22. for (i = 0; i < 16 && cred->uc_gids[i] != (gid_t) NOGROUP; i++)
  23. *p++ = htonl((u32) cred->uc_gids[i]); // 最多只能编码16个用户组
  24. *hold = htonl(p - hold - 1); /* gid array length */ // uc_gids的个数
  25. *base = htonl((p - base - 1) << 2); /* cred length */ // 设置认证信息的长度
  26. *p++ = htonl(RPC_AUTH_NULL); // 0 1 verifier flavor
  27. *p++ = htonl(0); // 0 1 verifier length
  28. return p;
  29. }
static __be32 *
unx_marshal(struct rpc_task *task, __be32 *p)
{
        // 取出RPC客户端的结构
        struct rpc_clnt *clnt = task->tk_client;
        // 取出认证信息的数据结构
        struct unx_cred *cred = container_of(task->tk_rqstp->rq_cred, struct unx_cred, uc_base);
        __be32          *base, *hold;
        int             i;

        *p++ = htonl(RPC_AUTH_UNIX);    // 编码认证类型,UNIX认证编号是1
        base = p++;                     // base=p;  p++         // 为认证长度预留4个字节
        *p++ = htonl(jiffies/HZ);       // 时间,秒   从这里开始是cred->body  这是一个时间戳

        /*
         * Copy the UTS nodename captured when the client was created.
         */
        // 编码RPC客户端的nodename   (先是长度,然后是名称,最后按4字节对齐) cl_nodename最长为32字节
        p = xdr_encode_array(p, clnt->cl_nodename, clnt->cl_nodelen);

        *p++ = htonl((u32) cred->uc_uid);       // 用户UID
        *p++ = htonl((u32) cred->uc_gid);       // 用户GID
        hold = p++;                             // uc_gids的个数
        // 编码uc_gids  这是用户所属于的用户组
        for (i = 0; i < 16 && cred->uc_gids[i] != (gid_t) NOGROUP; i++)
                *p++ = htonl((u32) cred->uc_gids[i]);   // 最多只能编码16个用户组
        *hold = htonl(p - hold - 1);            /* gid array length */  // uc_gids的个数
        *base = htonl((p - base - 1) << 2);     /* cred length */       // 设置认证信息的长度

        *p++ = htonl(RPC_AUTH_NULL);    // 0    1   verifier flavor
        *p++ = htonl(0);                // 0    1   verifier length

        return p;
}

根据RFC1813的规定,REMOVE请求需要编码父目录的文件句柄和要删除文件的名称,这些信息填充到RPC请求的净荷中传输,REMOVE请求的编码函数如下:

  1. static void nfs3_xdr_enc_remove3args(struct rpc_rqst *req,
  2. struct xdr_stream *xdr,
  3. const struct nfs_removeargs *args)
  4. {
  5. // 编码父目录的文件句柄、目标文件的名称
  6. encode_diropargs3(xdr, args->fh, args->name.name, args->name.len);
  7. }
static void nfs3_xdr_enc_remove3args(struct rpc_rqst *req,
                                     struct xdr_stream *xdr,
                                     const struct nfs_removeargs *args)
{
        // 编码父目录的文件句柄、目标文件的名称
        encode_diropargs3(xdr, args->fh, args->name.name, args->name.len);
}

  1. static void encode_diropargs3(struct xdr_stream *xdr, const struct nfs_fh *fh,
  2. const char *name, u32 length)
  3. {
  4. encode_nfs_fh3(xdr, fh); // 编码父目录文件句柄
  5. encode_filename3(xdr, name, length); // 编码要删除文件的名称
  6. }
static void encode_diropargs3(struct xdr_stream *xdr, const struct nfs_fh *fh,
                              const char *name, u32 length)
{
        encode_nfs_fh3(xdr, fh);        // 编码父目录文件句柄
        encode_filename3(xdr, name, length);        // 编码要删除文件的名称
}

  1. static void encode_nfs_fh3(struct xdr_stream *xdr, const struct nfs_fh *fh)
  2. {
  3. __be32 *p;
  4. BUG_ON(fh->size > NFS3_FHSIZE); // 不能超出NFSv3中文件句柄长度限制
  5. p = xdr_reserve_space(xdr, 4 + fh->size); // 为文件句柄预留缓存
  6. // 需要是4字节的整数倍,不足的内容用0填充
  7. xdr_encode_opaque(p, fh->data, fh->size); // 先编码文件句柄长度,再编码文件句柄内容
  8. }
static void encode_nfs_fh3(struct xdr_stream *xdr, const struct nfs_fh *fh)
{
        __be32 *p;

        BUG_ON(fh->size > NFS3_FHSIZE);     // 不能超出NFSv3中文件句柄长度限制
        p = xdr_reserve_space(xdr, 4 + fh->size);       // 为文件句柄预留缓存
        // 需要是4字节的整数倍,不足的内容用0填充
        xdr_encode_opaque(p, fh->data, fh->size);       // 先编码文件句柄长度,再编码文件句柄内容
}

  1. static void encode_filename3(struct xdr_stream *xdr,
  2. const char *name, u32 length)
  3. {
  4. __be32 *p;
  5. // NFS3_MAXNAMLEN是RFC1813规定的文件名称的最大长度
  6. BUG_ON(length > NFS3_MAXNAMLEN);
  7. // length是文件名称的长度,为文件名称预留缓存
  8. p = xdr_reserve_space(xdr, 4 + length);
  9. // 先编码文件名称长度,再编码文件名称.
  10. xdr_encode_opaque(p, name, length);
  11. }
static void encode_filename3(struct xdr_stream *xdr,
                             const char *name, u32 length)
{
        __be32 *p;

        // NFS3_MAXNAMLEN是RFC1813规定的文件名称的最大长度
        BUG_ON(length > NFS3_MAXNAMLEN);
        // length是文件名称的长度,为文件名称预留缓存
        p = xdr_reserve_space(xdr, 4 + length);
        // 先编码文件名称长度,再编码文件名称.
        xdr_encode_opaque(p, name, length);
}

xdr_encode_opaque()是RPC中字符串的编码函数,按照XDR编码规则先编码字符串长度,然后编码字符串内容。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值