本文主要是介绍Glusterfs之nfs模块源码分析之NFS协议之RPC的实现和NFS协议内容,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
一、网络文件系统概述
Sun Microsystems公司于1984年推出了一个在整个计算机工业中被广泛接受的远程文件存取机制,它被称为Sun的网络文件系统(Network File System),或者简称为NFS。该机制允许在一台计算机上运行一个服务器,使对其上的某些或所有文件都可以进行远程存取,还允许其他计算机上的应用程序对这些文件进行存取。
它使我们能够达到文件的共享。当使用者想用远端档案时只要用"mount"就可把remote档案系统挂接在自己的档案系统之下,使得远端的文件操作上和本地机器的文件没两样。一个应用程序可以打开(Open)一个远程文件以进行存取,可以从这个文件中读取(Read)数据,向该文件中写入(Write)数据,定位(Seek)到文件中的某个指定位置(开始、结尾或者其他地方),最后当使用完毕后关闭(Close)该文件。并且这些操作都是对编程者透明的,操作方法和对本地文件的操作方法完全一样。
二、NFS协议
NFS协议使用NFS,客户端可以透明地访问服务器中的文件系统,这不同于提供文件传输的FTP协议。FTP会产生文件一个完整的副本;NFS只访问一个进程引用文件部分,并且一个目的就是使得这种访问透明。这就意味着任何能够访问一个本地文件的客户端程序不需要做任何修改,就应该能够访问一个NFS文件。NFS是一个使用SunRPC构造的客户端/服务器应用程序,其客户端通过向一台NFS服务器发送RPC请求来访问其中的文件。尽管这一工作可以使用一般的用户进程来实现,即NFS客户端可以是一个用户进程,对服务器进行显式调用,而服务器也可以是一个用户进程。因为两个理由,NFS一般不这样实现。首先访问一个NFS文件必须对客户端透明,因此NFS的客户端调用是由客户端操作系统代表用户进程来完成的;其次,出于效率的考虑,NFS服务器在服务器操作系统中实现。如果NFS服务器是一个用户进程,每个客户端请求和服务器应答(包括读和写的数据)将不得不在内核和用户进程之间进行切换,这个代价太大。第3版的NFS协议在1993年发布,下图所示为一个NFS客户端和一台NFS服务器的典型结构。
图1 NFS客户端和NFS服务器的典型结构
(1)访问一个本地文件还是一个NFS文件对于客户端来说是透明的,当文件被打开时,由内核决定这一点。文件被打开之后,内核将本地文件的所有引用传递给名为“本地文件访问”的框中,而将一个NFS文件的所有引用传递给名为“NFS客户端”的框中。
(2)NFS客户端通过其TCP/IP模块向NFS服务器发送RPC请求,NFS主要使用UDP,最新的实现也可以使用TCP。
(3)NFS服务器在端口2049接收作为UDP数据包的客户端请求,尽管NFS可以被实现为使用端口映射器,允许服务器使用一个临时端口,但是大多数实现都是直接指定UDP端口2049。
(4)当NFS服务器收到一个客户端请求时,它将这个请求传递给本地文件访问例程,然后访问服务器主机上的一个本地的磁盘文件。
(5)NFS服务器需要花一定的时间来处理一个客户端的请求,访问本地文件系统一般也需要一部分时间。在这段时间间隔内,服务器不应该阻止其他客户端请求。为了实现这一功能,大多数的NFS服务器都是多线程的——服务器的内核中实际上有多个NFS服务器在NFS本身的加锁管理程序中运行,具体实现依赖于不同的操作系统。既然大多数UNIX内核不是多线程的,一个共同的技术就是启动一个用户进程(常被称为“nfsd”)的多个实例。这个实例执行一个系统调用,使其作为一个内核进程保留在操作系统的内核中。
(6)在客户端主机上,NFS客户端需要花一定的时间来处理一个用户进程的请求。NFS客户端向服务器主机发出一个RPC调用,然后等待服务器的应答。为了给使用NFS的客户端主机上的用户进程提供更多的并发性,在客户端内核中一般运行着多个NFS客户端,同样具体实现也依赖于操作系统。
三、NFS的工作原理和服务进程的作用
在Linux中,NFS和服务进程是两个不同的概念,但它们确实紧密联系在一起。首先,先介绍NFS的工作原理。
第一节、NFS的工作原理
启动NFS文件服务器时,/etc/rc.local会自动启动exportfs程序,指定可以导出的文件或目录,而所能挂载的也只能是其所指定的目录。
NFS是基于XDR/RPC协议的。XDR(eXternal Data Representation,即外部数据表示法)提供一种方法,把数据从一种格式转换成另一种标准数据格式表示法,确保在不同的计算机、操作系统及程序语言中,所有数据代表的意义都是相同的。
RPC(Remote Procedure Call,远程程序调用)请求远程计算机给予服务。客户机通过网络传送RPC到远程计算机,请求服务。
NFS运用RPC传送数据的方法有以下几步:
(1)客户送出信息,请求服务。
(2)客户占位程序把客户送出的参数转换成XDR标准格式,并用系统调用把信息送到网络上。
(3)信息经过网络送达远程主机系统。
(4)远程主机将接受到的信息传给服务器占位程序。
(5)把XDR形式的数据,转换成符合主机端的格式,取出客户发出的服务请求参数,送给服务器。
(6)服务器给客户发送服务的逆向传送过程。
第二节、服务进程的作用
服务进程是系统在启动计算机后自动运行的程序,包括对网络的连接、网络协议的加载、图形桌面的显示、文件系统的加载等,Linux系统中常见的进程包括以下几种。
(1)nfsd
根据客户端对文件系统的需求,启动文件系统请求服务进程,响应客户的请求,而一般文件系统请求服务进程的数目是8,这也是在rc.local中写nfsd 8 &的原因。
(2)biod
此进程是在NFS客户端上用的,用来启动异步块I/O服务进程来建立Buffer Cache,处理在客户机上的读写。(3)mountd
这是个RPC服务器。启动rpc.mountd服务进程后,mountd会读取/etc/xtab查看哪一台客户机正在挂载哪一个文件系统,并回应客户机所要挂载的路径。
(4)inetd Internet services服务进程
当系统启动时,rc.local会启动inetd读取inetd.conf配置文件,读取网络上所有服务器的地址,链接启动inetd.conf中所有的服务器。当客户机请求服务时,inetd就会启动相关的服务进程,如user使用telnet时,inetd启动telnetd配合user telnet的需求,其余像ftp、finger、rlogin等应用程序,inetd也都会启动相对应的服务程序ftpd、fingerd、rloingd等。
(5)portmap服务程序
主要功能是将TCP/IP通信协议的端口数字转换成RPC程序数字,因为这样客户端才能进行RPC调用。一般RPC服务器是被inet启动的,所以portmap必须在inetd之前启动,否则无法进行RPC调用。
四、NFS服务器之RPC
因为NFS支持的功能相当多,而不同的功能都会使用不同的程序来启动。每启动一个功能就会启用一些端口来传输数据,因此NFS的功能所对应的端口才没有固定,而是采用随机取用一些未被使用的小于724的端口来作为传输之用。但如此一来又造成客户端要连接服务器时的困扰,因为客户端要知道服务器端的相关端口才能够联机,此时我们需要远程过程调用(RPC)的服务。RPC最主要的功能就是指定每个NFS功能所对应的端口号,并且回报给客户端,让客户端可以连接到正确的端口上。当服务器在启动NFS时会随机选用数个端口,并主动地向RPC注册。因此RPC可以知道每个端口对应的NFS功能。然后RPC固定使用端口111来监听客户端的请求并回报客户端正确的端口,所以可以让NFS的启动更为容易。注意,启动NFS之前,要先启动RPC;否则NFS会无法向RPC注册。另外,重新启动RPC时原本注册的数据会不见,因此RPC重新启动后它管理的所有程序都需要重新启动以重新向RPC注册。
当客户端有NFS文件要存取请求时,它如何向服务器端要求数据?
(1)客户端会向服务器端的RPC(port 111)发出NFS文件存取功能的询问请求。
(2)服务器端找到对应的已注册的NFS daemon端口后会回报给客户端。
(3)客户端了解正确的端口后,就可以直接与NFS守护进程来联机。
由于NFS的各项功能都必须要向RPC注册,因此RPC才能了解NFS服务的各项功能的port number、PID和NFS在主机所监听的IP等,而客户端才能够通过RPC的询问找到正确对应的端口。即NFS必须要有RPC存在时才能成功地提供服务,因此我们称NFS为RPC Server的一种。事实上,有很多这样的服务器都向RPC注册。例如,NIS(Network Information Service)也是RPC Server的一种。所以如下图所示,不论是客户端还是服务器端,要使用NFS都需要启动RPC。
NFS协议从诞生到现在为止,已经有多个版本,如NFS V2(rfc794)及NFS V3(rfc1813)(最新的版本是V4(rfc307))。最早,SUN公司曾将NFS V2设计为只使用UDP,主要原因是当时机器的内存、网络速度和CPU的影响,不得不选择对机器负担较轻的方式。而到了NFS V3,SUN公司选择了TCP作为默认的传输方式。V3相对V2的主要区别如下:
(1)文件尺寸:V2最大只支持32位的文件大小(4 GB),而V3新增加了支持64位文件大小的技术
(2)文件传输尺寸:V3没有限定传输尺寸,V2最多只能设定为8 KB,可以使用-rsize and -wsize来设定
(3)返回完整的信息:V3增加和完善了返回错误和成功信息,对于服务器的设置和管理能带来很大好处
(4)增加了对TCP传输协议的支持:V2只提供了对UDP的支持,在一些高要求的网络环境中有很大限制;V3增加了对TCP的支持。UDP有着传输速度快且非连接传输的便捷特性,但是在传输上没有TCP稳定。当网络不稳定或者黑客入侵时很容易使NFS的性能大幅度降低,甚至使网络瘫痪。所以对于不同情况,网络要有针对性地选择传输协议。NFS的默认传输协议是UDP,然而RHEL 4.0内核提供了对通过TCP的NFS的支持。要通过TCP来使用NFS,在客户端系统上挂载NFS导出的文件系统时包括一个“-o tcp”选项。使用TCP的优点和缺点如下:
1)被提高了的连接持久性,因此获得的NFS stale file handles消息就会较少。
2)载量较大的网络的性能会有所提高,因为TCP确认每个分组,而UDP只在完成时才确认。
3)TCP具有拥塞控制技术(UDP根本没有),在一个拥塞情况严重的网络上,UDP分组是被首先撤销的类型。使用UDP意味着,如果NFS正在写入数据(单元为8 KB的块),所有这8 KB数据都需要被重新传输。由于TCP的可靠性,8 KB数据中只有一部分需要重新传输。
4)错误检测。当TCP连接中断(由于服务器停止),客户端就会停止发送数据而开始重新连接。UDP是无连接的,使用它的客户端就会继续给网络发送数据直到服务器重新上线为止。
5)TCP的费用在性能方面的提高并不显著。
(5)异步写入特性。
(6)改进了服务器的mount性能。
(7)有更好的I/O写性能。
(8)更强的网络运行效能,使得网络运行更为有效。
(9)更强的灾难恢复功能。
在Linux上,UDP是默认使用的协议。作为服务器别无选择。但作为客户端,可以使用TCP和其他使用TCP的UNIX NFS服务器互联。在局域网中使用UDP较好,因为局域网有比较稳定的网络保证。使用UDP可以带来更好的性能,Linux默认使用V2,但是也可以通过mount option的nfsvers=n选择。NFS使用TCP/IP提供的协议和服务运行于OSI层次模型的应用层,如表1所示。
表1 OSI层次模型上的NFS
层 数 | 名 称 | 功 能 |
1 | 应用层 | NFS |
2 | 表示层 | XDR |
3 | 会话层 | RPC |
4 | 传输层 | UDP,TCP |
5 | 网络层 | IP |
6 | 数据链路层 | |
7 | 物理层 | Ethernet |
五、Glusterfs实现NFS服务器
第一节、启动过程分析
Glusterfs的nfs服务器启动命令如下:
/usr/local/sbin/glusterfs -f /etc/glusterd/nfs/nfs-server.vol -p /etc/glusterd/nfs/run/nfs.pid
-l /usr/local/var/log/glusterfs/nfs.log
说明:所有列出的代码都把错误处理、参数检查和日志输出去掉了!
上面的命令会启动glusterfsd程序,下面是入口函数main的代码实现:
int main (int argc, char *argv[])
{ glusterfs_ctx_t *ctx = NULL; int ret = -1; ret = glusterfs_globals_init ();//初始化一些全局变量和参数 ctx = glusterfs_ctx_get (); ret = glusterfs_ctx_defaults_init (ctx);//初始化一些glusterfs的上下文默认信息 ret = parse_cmdline (argc, argv, ctx);//解析命令行参数 ret = logging_init (ctx);//初始化日志文件 gf_proc_dump_init();//初始化代表程序的全局锁 ret = create_fuse_mount (ctx);//创建fuse的主(根)xlator:mount/fuse,并且初始化相关值 ret = daemonize (ctx);//设置守护进程运行模式 ret = glusterfs_volumes_init (ctx);//初始化卷服务,创建相应的xlator并且初始化 ret = event_dispatch (ctx->event_pool);//时间分发,将相应的事件交给相应的函数处理
}
整个main做的工作在代码中都有注释了,对于nfs启动比较关心的就是两个函数,一个是命令行参数解析函数parse_cmdline,按照上面给出的命令解析出程序启动的需要的卷文件路径、日志文件路径和存放进程ID的文件。而且程序是以glusterfs模式(有三种模式:(1)glusterfsd;(2)glusterfs;(3)glusterd)运行。
另外一个函数就是初始化具体的卷服务函数glusterfs_volumes_init,根据我们的启动命令是启动nfs类型的服务,每一个卷服务都会用一个xlator表示,代码如下:
int glusterfs_volumes_init (glusterfs_ctx_t *ctx)
{ FILE *fp = NULL; cmd_args_t *cmd_args = NULL; int ret = 0; cmd_args = &ctx->cmd_args; if (cmd_args->sock_file) {//是否设置了sock_file来启动监听服务 ret = glusterfs_listener_init (ctx);//初始化监听服务 } fp = get_volfp (ctx);//得到描述卷的文件指针 ret = glusterfs_process_volfp (ctx, fp);//处理描述卷的文件
}
从启动命令可以看出并没有设置cmd_args->sock_file和cmd_args->volfile_server参数,所以直接进入卷处理函数glusterfs_process_volfp,下面继续看这个函数的实现,如下:
int glusterfs_process_volfp (glusterfs_ctx_t *ctx, FILE *fp)
{ glusterfs_graph_t *graph = NULL; int ret = -1; xlator_t *trav = NULL; graph = glusterfs_graph_construct (fp);//根据卷描述文件构造一个graph for (trav = graph->first; trav; trav = trav->next) { if (strcmp (trav->type, "mount/fuse") == 0) {//卷文件中不能有mount/fuse类型的卷 gf_log ("glusterfsd", GF_LOG_ERROR, "fuse xlator cannot be specified " "in volume file"); goto out; } } ret = glusterfs_graph_prepare (graph, ctx);//准备工作 ret = glusterfs_graph_activate (graph, ctx);//激活这个图结构的卷 gf_log_volume_file (fp);//卷的日志文件 ret = 0;
out: if (fp) fclose (fp); if (ret && !ctx->active) { cleanup_and_exit (0);//如果没有激活就清理掉并且直接退出整个程序 } return ret;
}
继续关注比较重要的,上面代码中最重要的就是激活卷( graph )服务的函数 glusterfs_graph_activate 了,所以继续看这个函数的实现,代码如下:
int glusterfs_graph_activate (glusterfs_graph_t *graph, glusterfs_ctx_t *ctx)
{ int ret = 0; ret = glusterfs_graph_validate_options (graph);//验证所有卷包括子卷的配置选项的正确性 ret = glusterfs_graph_init (graph);//初始化由整个配置文件中的各个卷组成的图结构 ret = glusterfs_graph_unknown_options (graph);//再次验证是否有不知道的参数 list_add (&graph->list, &ctx->graphs);//加入到总的链表中进行统一管理 ctx->active = graph; if (ctx->master)//附加到master(mount/fuse)节点 ret = xlator_notify (ctx->master, GF_EVENT_GRAPH_NEW, graph); ret = glusterfs_graph_parent_up (graph);//设置父节点 return 0;
}
在graph初始化函数中有具体初始化xlator的实现,这个就关系到是怎样连接到nfs服务器,所以继续看这个函数的实现:
int glusterfs_graph_init (glusterfs_graph_t *graph)
{ xlator_t *trav = NULL; int ret = -1; trav = graph->first;//第一个节点,也就是nfs类型的节点 while (trav) { ret = xlator_init (trav);//依次初始化每一个节点(xlator) trav = trav->next;//指向下一个节点 } return 0;
}
继续看初始化节点 xlator 的函数 xlator_init ,代码如下:
int xlator_init (xlator_t *xl)
{ int32_t ret = -1; GF_VALIDATE_OR_GOTO ("xlator", xl, out); if (xl->mem_acct_init) xl->mem_acct_init (xl);//如果这个函数指针不为空就调用 if (!xl->init) {//init函数指针不能为空 } ret = __xlator_init (xl);//继续初始化 xl->init_succeeded = 1; ret = 0;
out: return ret;
}
继续看 __xlator_init (xl);
static int __xlator_init(xlator_t *xl){xlator_t *old_THIS = NULL;int ret = 0;old_THIS = THIS;THIS = xl;ret = xl->init (xl);//调用具体xlator的init函数完成初始化工作THIS = old_THIS;return ret;}
到此为止就可以看到真正调用NFS的init函数了,这个时候才真正开始执行与nfs服务相关功能的代码。当然在结束服务的时候还会执行fini函数。
第二节、NFS协议实现的init函数
先看看这个函数的代码吧,如下:
int init (xlator_t *this) {struct nfs_state *nfs = NULL;int ret = -1;if (!this)return -1;nfs = nfs_init_state (this);//初始化一些nfs的选项参数ret = nfs_add_all_initiators (nfs);//添加所有协议的初始化器ret = nfs_init_subvolumes (nfs, this->children);//初始化nfs的所有子卷ret = nfs_init_versions (nfs, this);//初始化所有nfs协议的版本return ret;}
上面代码可以看出,init函数的主要作用就是初始化nfs协议的所有版本以及其所有的子卷。下面依次分析各个函数实现的功能。
1.nfs_init_state函数
这个函数代码比较多,分步骤解析:
第一步:判断nfs是否存在子卷,如果不存在就退出,因为必须要有子卷才能正常工作,代码如下:
if ((!this->children) || (!this->children->xlator)) {gf_log (GF_NFS, GF_LOG_ERROR, "nfs must have at least one" " child subvolume");return NULL;}
第二步:为 nfs_state 结构体指针分配内存,分配失败就报错并退出程序,分配内存代码如下:
nfs = GF_CALLOC (1, sizeof (*nfs), gf_nfs_mt_nfs_state);
第三步:启动rpc服务:nfs->rpcsvc = nfs_rpcsvc_init (this->ctx, this->options);
第四步:根据参数nfs.mem-factor设置内存池的大小,用于提高访问速度。
这一步首先调用函数xlator_get_volopt_info 得到参数的值,然后转换为无符号整型并赋值给nfs->memfactor,表示内存因子,然后计算整个内存池的大小并新建这样大小的一个内存池,代码如下;
fopspoolsize = nfs->memfactor * GF_NFS_CONCURRENT_OPS_MULT;nfs->foppool = mem_pool_new (struct nfs_fop_local, fopspoolsize);
第五步:安装第四步同样的方式解析参数nfs.dynamic-volumes、nfs.enable-ino32和nfs.port,并且赋值给nfs_state结构体中相应的字段保存。
第六步:将nfs_state保存到xlator的私有数据部分并初始化nfs协议版本的链表。
this->private = (void *)nfs;INIT_LIST_HEAD (&nfs->versions);
经过上面 6 步这个函数就执行完了,如果其中有地方出错都会进行相应的处理,尤其是资源的释放处理尤为重要,保证不会发生内存泄露。其中第三步是比较重要的复杂的,涉及到 rpc 服务的初始化工作,所以需要详细分析,因为后面很多操作都会依赖于 rpc 服务进行通信, nfs_rpcsvc_init 函数定义和实现如下:
rpcsvc_t * nfs_rpcsvc_init (glusterfs_ctx_t *ctx, dict_t *options){rpcsvc_t *svc = NULL;int ret = -1;int poolsize = 0;svc = GF_CALLOC (1, sizeof (*svc), gf_common_mt_rpcsvc_t);//分配内存资源pthread_mutex_init (&svc->rpclock, NULL);//初始化锁INIT_LIST_HEAD (&svc->stages);//初始化rpc服务的阶段处理链表INIT_LIST_HEAD (&svc->authschemes);//初始化可用的身份验证方案链表INIT_LIST_HEAD (&svc->allprograms);//初始化存放所有程序的链表ret = nfs_rpcsvc_init_options (svc, options);//初始化一些选项信息ret = nfs_rpcsvc_auth_init (svc, options);//初始化权限信息poolsize = RPCSVC_POOLCOUNT_MULT * RPCSVC_DEFAULT_MEMFACTOR;//计算内存池大小svc->connpool = mem_pool_new (rpcsvc_conn_t, poolsize);//为连接对象分配内存池空间svc->defaultstage = nfs_rpcsvc_stage_init (svc);//初始化默认阶段执行服务svc->options = options;//赋值选项信息svc->ctx = ctx;//设置属于哪一个glusterfs的上下文return svc;}
这个函数是 rpc 服务的全局初始化函数,这是 rpc 服务的开始阶段( rpc 服务分为多个阶段实现),等待 rpc 程序注册的到来。整个函数其实都是在初始化一个结构体的相关内容,就是 rpcsvc_t 结构体,它的作用就是描述 rpc 服务的状态(包括各个阶段的,对 rpc 各个阶段进行统一管理)。下面看看这个结构体的具体定义:
/* Contains global state required for all the RPC services. */typedef struct rpc_svc_state {/* Contains the list of rpcsvc_stage_t list of (program, version) handlers. other options. *//* At this point, lock is not used to protect anything. Later, it'll be used for protecting stages. */pthread_mutex_t rpclock;/* This is the first stage that is inited, so that any RPC based services that do not need multi-threaded * support can just use the service right away. This is not added to the stages list declared later.* This is also the stage over which all service listeners are run. */rpcsvc_stage_t *defaultstage;/* When we have multi-threaded RPC support, we'll use this to link to the multiple Stages.*/struct list_head stages; /* All stages */unsigned int memfactor;struct list_head authschemes;/* List of the authentication schemes available. */dict_t *options;/* Reference to the options */int allow_insecure;/* Allow insecure ports. */glusterfs_ctx_t *ctx;gf_boolean_t register_portmap;struct list_head allprograms;struct mem_pool *connpool;/* Mempool for incoming connection objects. */} rpcsvc_t;
在nfs_rpcsvc_init函数中,有一个初始化权限信息的函数nfs_rpcsvc_auth_init 和一个初始化rpc执行阶段信息的函数nfs_rpcsvc_stage_init需要重点分析。先分析权限信息的初始化函数nfs_rpcsvc_auth_init,如下:
(1)nfs_rpcsvc_auth_init函数
函数定义和实现如下:
int nfs_rpcsvc_auth_init (rpcsvc_t *svc, dict_t *options){int ret = -1;ret = nfs_rpcsvc_auth_add_initers (svc);//增加auth-null和auth-unix两个关键字代表的初始化函数ret = nfs_rpcsvc_auth_init_auths (svc, options);//开启权限使能相关选项并且执行初始化函数return ret;}
这个函数主要实现增加权限的初始化函数到权限操作链表中,然后通过执行执行初始化函数得到一个描述相关权限信息的结构体,这个结构体包括一些操作函数指针的结构体地址和一些基本信息(如名称)。执行初始化函数并且得到权限描述信息的实现是在如下代码中(在 nfs_rpcsvc_auth_init_auths 中调用的)
int nfs_rpcsvc_auth_init_auth (rpcsvc_t *svc, dict_t *options, struct rpcsvc_auth_list *authitem){.....authitem->auth = authitem->init (svc, options);//执行权限的初始化函数authitem->enable = 1;//权限使能......}
这里执行的初始化函数是在上面初始化的,有两类权限的初始化函数,相关内容定义如下:
1)auth-null
rpcsvc_auth_ops_t nfs_auth_null_ops = {//权限操作相关的处理函数.conn_init = NULL,.request_init = nfs_auth_null_request_init,.authenticate = nfs_auth_null_authenticate};rpcsvc_auth_t nfs_rpcsvc_auth_null = {//权限描述的结构体和默认值.authname = "AUTH_NULL",.authnum = AUTH_NULL,.authops = &nfs_auth_null_ops,.authprivate = NULL};rpcsvc_auth_t * nfs_rpcsvc_auth_null_init (rpcsvc_t *svc, dict_t *options)//初始化函数{return &nfs_rpcsvc_auth_null;//返回权限描述信息结构体}2)auth-unixrpcsvc_auth_ops_t nfs_auth_unix_ops = {//权限操作相关的处理函数.conn_init = NULL,.request_init = nfs_auth_unix_request_init,.authenticate = nfs_auth_unix_authenticate};rpcsvc_auth_t nfs_rpcsvc_auth_unix = {//权限描述的结构体和默认值.authname = "AUTH_UNIX",.authnum = AUTH_UNIX,.authops = &nfs_auth_unix_ops,.authprivate = NULL};rpcsvc_auth_t * nfs_rpcsvc_auth_unix_init (rpcsvc_t *svc, dict_t *options)//初始化函数{return &nfs_rpcsvc_auth_unix;//返回权限描述信息结构体}
(2)nfs_rpcsvc_stage_init函数
首先还是看看这个函数的定义和实现吧:
rpcsvc_stage_t * nfs_rpcsvc_stage_init (rpcsvc_t *svc){rpcsvc_stage_t *stg = NULL;int ret = -1;size_t stacksize = RPCSVC_THREAD_STACK_SIZE;pthread_attr_t stgattr;unsigned int eventpoolsize = 0;stg = GF_CALLOC (1, sizeof(*stg), gf_common_mt_rpcsvc_stage_t);//分配内存资源eventpoolsize = svc->memfactor * RPCSVC_EVENTPOOL_SIZE_MULT;//计算事件内存池大小stg->eventpool = event_pool_new (eventpoolsize);//分配内存资源pthread_attr_init (&stgattr);//初始化线程熟悉值ret = pthread_attr_setstacksize (&stgattr, stacksize);//设置线程的堆栈大小ret = pthread_create (&stg->tid, &stgattr, nfs_rpcsvc_stage_proc, (void *)stg);//创建线程stg->svc = svc;return stg;}
这个函数主要就是启动一个线程然后开始分发事件,事件分发函数会等待某一个事件的发生,发生以后会执行以前已经注册的函数指针,在这里就是注册的是权限操作相关的函数。具体的事件处理和分发过程就不在详细分析了!
2.nfs_add_all_initiators函数
这个函数主要是添加各个版本的nfs协议的初始化。它三次调用函数nfs_add_initer分别来为mnt3、mnt1和nfs3版本的nfs协议(各个版本的协议内容见附件)进行初始化。详细看看nfs_add_initer函数的代码,如下:
int nfs_add_initer (struct list_head *list, nfs_version_initer_t init){struct nfs_initer_list *new = NULL;new = GF_CALLOC (1, sizeof (*new), gf_nfs_mt_nfs_initer_list);//分配内存new->init = init;//赋值初始化函数指针list_add_tail (&new->list, list);//添加到协议版本链表的末尾return 0;}
每个版本的nfs协议都有自己的初始化函数,以便处理那些特殊的协议部分,上面的过程就是将各个版本nfs协议初始化保存到链表中,在使用协议的时候以便调用相应的初始化函数初始化相关协议内容。
(1)mnt3版本协议
mnt3版本的nfs协议的实现函数,代码如下:
rpcsvc_program_t * mnt3svc_init (xlator_t *nfsx){struct mount3_state *mstate = NULL;mstate = mnt3_init_state (nfsx);mnt3prog.private = mstate;return &mnt3prog;}
这个函数代码很简单,只有两个重点内容需要关注,一个结构体和一个函数,结构体就是mount3_state,它的定义如下:/* Describes a program and its version along with the function pointers* required to handle the procedures/actors of each program/version.* Never changed ever by any thread so no need for a lock. */struct rpc_svc_program {struct list_head proglist;char progname[RPCSVC_NAME_MAX];int prognum;int progver;uint16_t progport; /* Registered with portmap */int progaddrfamily; /* AF_INET or AF_INET6 */char *proghost; /* Bind host, can be NULL */rpcsvc_actor_t *actors; /* All procedure handlers */int numactors; /* Num actors in actor array */int proghighvers; /* Highest ver for program supported by the system. */int proglowvers; /* Lowest ver *//* Program specific state handed to actors */void *private;/* An integer that identifies the min auth strength that is required* by this protocol, for eg. MOUNT3 needs AUTH_UNIX at least.* See RFC 1813, Section 5.2.1. */int min_auth;/* The translator in whose context the actor must execute. This is* needed to setup THIS for memory accounting to work correctly. */xlator_t *actorxl;};
这个结构体的定义中注释已经很清晰,就不具体分析了,在看看程序中使用这个结构体的赋值,如下:rpcsvc_program_t mnt3prog = {.progname = "MOUNT3",.prognum = MOUNT_PROGRAM,.progver = MOUNT_V3,.progport = GF_MOUNTV3_PORT,.progaddrfamily = AF_INET,.proghost = NULL,.actors = mnt3svc_actors,.numactors = MOUNT3_PROC_COUNT,};
这个是这个结构体的静态赋值方式,还有动态的赋值方式,就是上面提到的一个函数mnt3_init_state,也是下面将要分析的,这个函数实现代码如下:struct mount3_state * mnt3_init_state (xlator_t *nfsx){struct mount3_state *ms = NULL;int ret = -1;ms = GF_CALLOC (1, sizeof (*ms), gf_nfs_mt_mount3_state);//分配结构体对象内存ms->iobpool = nfsx->ctx->iobuf_pool;//IO缓冲池ms->nfsx = nfsx;//属于哪一个xlatorINIT_LIST_HEAD (&ms->exportlist);//初始化导出列表ret = mnt3_init_options (ms, nfsx->options);//初始化选项信息INIT_LIST_HEAD (&ms->mountlist);//初始化挂载列表LOCK_INIT (&ms->mountlock);//初始化锁return ms;}
上面这个函数最主要的工作还是初始化一些相关的参数和选项,其中主要的的内容还是一个结构体和一个函数,结构体就是mount3_state,它的定义如下struct mount3_state {xlator_t *nfsx;/* The buffers for all network IO are got from this pool. */struct iobuf_pool *iobpool;/* List of exports, can be volumes or directories in those volumes. */struct list_head exportlist;/* List of current mount points over all the exports from this* server. */struct list_head mountlist;/* Used to protect the mountlist. */gf_lock_t mountlock;/* Set to 0 if exporting full volumes is disabled. On by default. */int export_volumes;int export_dirs;};
上面这个结构体基本上描述了mnt3版本的nfs协议的一些状态信息,注释中都有具体的描述了,下面这个函数就是针对这些信息做一些初始化的工作,如下:int mnt3_init_options (struct mount3_state *ms, dict_t *options){xlator_list_t *volentry = NULL;int ret = -1;__mnt3_init_volume_export (ms, options);//根据nfs3.export-volumes配置选项设置导出卷的信息__mnt3_init_dir_export (ms, options);//根据nfs3.export-dirs配置选项设置导出目录的信息volentry = ms->nfsx->children;//初始化xlator的链表while (volentry) {//遍历所有的子xlatorgf_log (GF_MNT, GF_LOG_TRACE, "Initing options for: %s", volentry->xlator->name);ret = __mnt3_init_volume (ms, options, volentry->xlator);//初始化xlator的卷信息volentry = volentry->next;//下一个xlator}return ret;}
上面的代码主要是初始化所有子xlator的卷相关信息,调用函数__mnt3_init_volume实现,代码定义如下(把所有错误处理代码、变量定义和参数检查删掉后的代码):
int __mnt3_init_volume (struct mount3_state *ms, dict_t *opts, xlator_t *xlator){uuid_clear (volumeid);//清除uuid,即设置为0if (gf_nfs_dvm_off (nfs_state (ms->nfsx)))//关闭动态卷goto no_dvm;ret = snprintf (searchstr, 1024, "nfs3.%s.volume-id", xlator->name);//格式化选项key的字符串if (dict_get (opts, searchstr)) {//根据选项key得到选项的值ret = dict_get_str (opts, searchstr, &optstr);//得到字符串形式的值} if (optstr) {//如果不为nullret = uuid_parse (optstr, volumeid);//根据得到的值解析uuid}no_dvm:ret = snprintf (searchstr, 1024, "nfs3.%s.export-dir", xlator->name);//export-dir选项keyif (dict_get (opts, searchstr)) {//同上ret = dict_get_str (opts, searchstr, &optstr);ret = __mnt3_init_volume_direxports (ms, xlator, optstr, volumeid);//初始化卷导出目录}if (ms->export_volumes) {//如果导出卷使能newexp = mnt3_init_export_ent (ms, xlator, NULL, volumeid);//初始化导出环境list_add_tail (&newexp->explist, &ms->exportlist);//添加导出列表到导出列表的末尾}ret = 0;err:return ret;}
由上面代码可知:主要在初始化导出目录和导出环境,具体的实现都是调用相应的函数实现。
总结:这个初始化过程主要是在分配一些资源和建立一些关系,真正处理客户端请求的功能是在很多注册的或关联的函数中,客户端的某一个请求可能就需要调用一个专门的函数来处理。
(2)mnt1版本协议
这个版本的协议实现基本上和mnt3的实现一样,很多函数基本都就是调用mnt3的,不同的就是具体描述相关谢谢的结构体内容不同吧,例如有关信息的客户端请求是执行的处理函数等等。所以不再分析此版本协议初始化。
(3)nfs3版本协议
此版本的nfs协议初始化流程和前面分析的mnt3版本协议基本相同,下面只分析不同的部分,具体流程就不在那么分析了,主要介绍一些重点信息。第一需要介绍的就是nfs3_state结构体,定义如下:
struct nfs3_state {/* The NFS xlator pointer. The NFS xlator can be running* multiple versions of the NFS protocol.*/xlator_t *nfsx;/* The iob pool from which memory allocations are made for receiving* and sending network messages. */struct iobuf_pool *iobpool;/* List of child subvolumes for the NFSv3 protocol.* Right now, is simply referring to the list of children in nfsx above. */xlator_list_t *exportslist;struct list_head exports;/* Mempool for allocations of struct nfs3_local */struct mem_pool *localpool;/* Server start-up timestamp, currently used for write verifier. */uint64_t serverstart;/* NFSv3 Protocol configurables */size_t readsize;size_t writesize;size_t readdirsize;/* Size of the iobufs used, depends on the sizes of the three params above. */size_t iobsize;unsigned int memfactor;struct list_head fdlru;gf_lock_t fdlrulock;int fdcount;};
上面的结构体主要是记录一些nfs3协议运行过程中的状态信息,每一项的意义代码中有详细注释,理解这些信息对后面其他代码的理解是有非常大的好处的。在看看下面这个结构体的初始化默认值:rpcsvc_program_t nfs3prog = {.progname = "NFS3",.prognum = NFS_PROGRAM,.progver = NFS_V3,.progport = GF_NFS3_PORT,.progaddrfamily = AF_INET,.proghost = NULL,.actors = nfs3svc_actors,.numactors = NFS3_PROC_COUNT,/* Requests like FSINFO are sent before an auth scheme* is inited by client. See RFC 2623, Section 2.3.2. */.min_auth = AUTH_NULL,};
在看看里面的nfs3svc_actors这个结构体的值,如下:rpcsvc_actor_t nfs3svc_actors[NFS3_PROC_COUNT] = {{"NULL", NFS3_NULL, nfs3svc_null, NULL, NULL},{"GETATTR", NFS3_GETATTR, nfs3svc_getattr,NULL, NULL},{"SETATTR", NFS3_SETATTR, nfs3svc_setattr,NULL, NULL},{"LOOKUP", NFS3_LOOKUP, nfs3svc_lookup, NULL, NULL},{"ACCESS", NFS3_ACCESS, nfs3svc_access, NULL, NULL},{"READLINK", NFS3_READLINK, nfs3svc_readlink,NULL, NULL},{"READ", NFS3_READ, nfs3svc_read, NULL, NULL},{"WRITE", NFS3_WRITE, nfs3svc_write, nfs3svc_write_vec, nfs3svc_write_vecsizer},{"CREATE", NFS3_CREATE, nfs3svc_create, NULL, NULL},{"MKDIR", NFS3_MKDIR, nfs3svc_mkdir, NULL, NULL},{"SYMLINK", NFS3_SYMLINK, nfs3svc_symlink,NULL, NULL},{"MKNOD", NFS3_MKNOD, nfs3svc_mknod, NULL, NULL},{"REMOVE", NFS3_REMOVE, nfs3svc_remove, NULL, NULL},{"RMDIR", NFS3_RMDIR, nfs3svc_rmdir, NULL, NULL},{"RENAME", NFS3_RENAME, nfs3svc_rename, NULL, NULL},{"LINK", NFS3_LINK, nfs3svc_link, NULL, NULL},{"READDIR", NFS3_READDIR, nfs3svc_readdir,NULL, NULL},{"READDIRPLUS", NFS3_READDIRP, nfs3svc_readdirp,NULL, NULL},{"FSSTAT", NFS3_FSSTAT, nfs3svc_fsstat, NULL, NULL},{"FSINFO", NFS3_FSINFO, nfs3svc_fsinfo, NULL, NULL},{"PATHCONF", NFS3_PATHCONF, nfs3svc_pathconf,NULL, NULL},{"COMMIT", NFS3_COMMIT, nfs3svc_commit, NULL, NULL}};
由上面两个结构体的值可以看出,一个具体版本的nfs协议都有一个对应的结构体描述其基本信息,还有一个结构体存储了消息与函数的对应关系,当接受到什么消息就执行对应的函数,明白了这一点,其实对于各个版本的协议分析都大同小异了,关键就是在各个函数具体的实现了。而开头就介绍的那个结构体存放的都是一些各个版本不同的信息部分,所以会在rpc_svc_program结构体的private保存(void *类型可以保存任何数据类型,也表示是各个版本的nfs协议的私有部分数据)。
3.nfs_init_subvolumes函数
这个函数完成初始化所有子卷的任务,它首先计算需要分配给inode表使用的缓存大小,然后遍历存放子卷的链表,然后依次调用nfs_init_subvolume函数分别初始化每一个子卷,这个函数定义和实现如下:
int nfs_init_subvolume (struct nfs_state *nfs, xlator_t *xl){unsigned int lrusize = 0;int ret = -1;lrusize = nfs->memfactor * GF_NFS_INODE_LRU_MULT;//计算在lru链表中inodes的数量xl->itable = inode_table_new (lrusize, xl);//新建一个inode的表ret = 0;err:return ret;}
这里最重要的是inode_table_t结构体和inode_table_new函数,inode_table_t定义如下:
struct _inode_table {pthread_mutex_t lock;size_t hashsize; /* bucket size of inode hash and dentry hash */char *name; /* name of the inode table, just for gf_log() */inode_t *root; /* root directory inode, with number 1 */xlator_t *xl; /* xlator to be called to do purge */uint32_t lru_limit; /* maximum LRU cache size */struct list_head *inode_hash; /* buckets for inode hash table */struct list_head *name_hash; /* buckets for dentry hash table */struct list_head active; /* list of inodes currently active (in an fop) */uint32_t active_size; /* count of inodes in active list */struct list_head lru; /* list of inodes recently used.lru.next most recent */uint32_t lru_size; /* count of inodes in lru list */struct list_head purge; /* list of inodes to be purged soon */uint32_t purge_size; /* count of inodes in purge list */struct mem_pool *inode_pool; /* memory pool for inodes */struct mem_pool *dentry_pool; /* memory pool for dentrys */struct mem_pool *fd_mem_pool; /* memory pool for fd_t */};
结构体中每一项都有详细的注释了,就不多解析了,下面继续分析inode_table_new函数,由于这个函数代码还是有点点多,所以还是采取分步骤来解析,如下:
第一步:定义一个inode_table_t结构体并且分配内存:
inode_table_t *new = NULL;new = (void *)GF_CALLOC(1, sizeof (*new), gf_common_mt_inode_table_t);
第二步:初始化各个参数;
第三步:初始化各个链表,如下:
INIT_LIST_HEAD (&new->active);//初始化激活链表 INIT_LIST_HEAD (&new->lru);//最近使用链表 INIT_LIST_HEAD (&new->purge);//清楚了的链表
第四步:为inode表设置root的inode节点信息:__inode_table_init_root (new);
第五步:为inode表初始化锁。
上面的第四步是操作inode节点相关的信息,在ext2/3文件系统中也有inode节点,所以具体看看inode节点信息的管理和操作,就从初始化一个inode表的根节点开始,定义如下:
static void __inode_table_init_root (inode_table_t *table){inode_t *root = NULL;//定义inode表的根节点struct iatt iatt = {0, };//inode节点的属性信息root = __inode_create (table);//创建一个inode表的根节点iatt.ia_gfid[15] = 1;//inode节点的属性赋值iatt.ia_ino = 1;iatt.ia_type = IA_IFDIR;table->root = root;//赋值inode表的根节点__inode_link (root, NULL, NULL, &iatt);//inode节点和属性连接起来}
整个inode表的初始化完成根节点创建和属性的赋值,下面主要看看两个结构体定义和两个函数的实现,先看看inode和iatt两个结构体的定义,他们的定义中包含很多重要的信息,他们的定义如下:struct _inode {inode_table_t *table; /* the table this inode belongs to */uuid_t gfid;gf_lock_t lock;uint64_t nlookup;uint32_t ref; /* reference count on this inode */ino_t ino; /* inode number in the storage (persistent) */ia_type_t ia_type; /* what kind of file */struct list_head fd_list; /* list of open files on this inode */struct list_head dentry_list; /* list of directory entries for this inode */struct list_head hash; /* hash table pointers */struct list_head list; /* active/lru/purge */struct _inode_ctx *_ctx; /* replacement for dict_t *(inode->ctx) */};struct iatt {uint64_t ia_ino; /* inode number */uuid_t ia_gfid;uint64_t ia_dev; /* backing device ID */ia_type_t ia_type; /* type of file */ia_prot_t ia_prot; /* protection */uint32_t ia_nlink; /* Link count */uint32_t ia_uid; /* user ID of owner */uint32_t ia_gid; /* group ID of owner */uint64_t ia_rdev; /* device ID (if special file) */uint64_t ia_size; /* file size in bytes */uint32_t ia_blksize; /* blocksize for filesystem I/O */uint64_t ia_blocks; /* number of 512B blocks allocated */uint32_t ia_atime; /* last access time */uint32_t ia_atime_nsec;uint32_t ia_mtime; /* last modification time */uint32_t ia_mtime_nsec;uint32_t ia_ctime; /* last status change time */uint32_t ia_ctime_nsec;};
上面的定义代码中都有很详细的注释了,下面继续看inode节点的创建函数,定义如下static inode_t * __inode_create (inode_table_t *table){inode_t *newi = NULL;newi = mem_get0 (table->inode_pool);//从inode表中的inode内存池中得到一个inode内存newi->table = table;//想创建的inode属于哪一个inode表LOCK_INIT (&newi->lock);//操作inode节点以前初始化锁INIT_LIST_HEAD (&newi->fd_list);//初始化各个链表INIT_LIST_HEAD (&newi->list);INIT_LIST_HEAD (&newi->hash);INIT_LIST_HEAD (&newi->dentry_list);newi->_ctx = GF_CALLOC (1, (sizeof (struct _inode_ctx) *table->xl->graph->xl_count),gf_common_mt_inode_ctx);//为多键值对结构体分配内存if (newi->_ctx == NULL) {LOCK_DESTROY (&newi->lock);//释放锁mem_put (table->inode_pool, newi);//把inode节点放回内存池newi = NULL;goto out;}list_add (&newi->list, &table->lru);//增加链表到最近使用链表table->lru_size++;//最近使用链表的数量加1out:return newi;}
这里面最难懂也最重要的是mem_get0 函数,它的重要就是从inode节点的内存池中获取一个inode节点对象所需要的内存空间,具体的内存池的管理和分配使用到了slab分配器相关的知识。Slab分配器的思想就是把以前已经分配过的对象内存缓存起来,下一次同类的对象来分配对象就直接从缓存中取得,这样省去分配和初始化的时间(因为是同样的内存对象)。除了mem_get0函数其余代码做一些初始化的相关工作,后面有一个分配多键值对的内存结构体需要分配,如果失败就是归还内存池和释放锁占用的资源。这里可以在学习一点知识就是多键值对的结果,定义如下:struct _inode_ctx {union {uint64_t key; xlator_t *xl_key;};union {uint64_t value1; void *ptr1;};union {uint64_t value2; void *ptr2;};};
这个结构体的作用是可以有两种类型的键,也可以有两种类型的值,其中一种可以是任意数据结构,而且这是一种一个键对应两个值的结构,特殊情况特殊的处理,从这里可以学习到,如果以后有一个键关联三个值的时候也可以采取这种方式。虽然这个结构体在这里具体是什么作用还不是很明朗,但是可以肯定的是用处大大的,后面可能会用到。
继续看__inode_link 函数的定义和实现,代码如下:
static inode_t * __inode_link (inode_t *inode, inode_t *parent, const char *name, struct iatt *iatt){dentry_t *dentry = NULL;//目录项和inode相关的变量定义dentry_t *old_dentry = NULL;inode_t *old_inode = NULL;inode_table_t *table = NULL;inode_t *link_inode = NULL;table = inode->table;if (parent) {if (inode->table != parent->table) {//防止不同的inode表连接起来(成为父子关系)GF_ASSERT (!"link attempted b/w inodes of diff table");}}link_inode = inode;if (!__is_inode_hashed (inode)) {//此inode是否有hash链表if (!iatt)//属性值不能为nullreturn NULL;if (uuid_is_null (iatt->ia_gfid))//uuid不能为nullreturn NULL;uuid_copy (inode->gfid, iatt->ia_gfid);//复制uuid到inode节点inode->ino = iatt->ia_ino;//赋值inode节点数量inode->ia_type = iatt->ia_type;//inode节点的类型old_inode = __inode_find (table, inode->gfid);//在inode表里面查找是否存在此inode节点if (old_inode) {link_inode = old_inode;//存在} else {__inode_hash (inode);//不存在进行hash并进入hash链表}}if (parent) {//父节点不为nullold_dentry = __dentry_grep (table, parent, name);//搜索目录项if (!old_dentry || old_dentry->inode != link_inode) {//没有找到目录项或目录项不等于当前目录项dentry = __dentry_create (link_inode, parent, name);//创建一个目录项if (old_inode && __is_dentry_cyclic (dentry)) {//如果inode已经存在并且目录项是循环的__dentry_unset (dentry);//取消设置目录项return NULL;}__dentry_hash (dentry);//hash此目录项if (old_dentry)__dentry_unset (old_dentry);//取消设置老的目录项}}return link_inode;}
这个函数比较复杂,主要涉及到一个目录项的操作,目录项本身有inode节点,也有父节点,还包括很多属于此目录项的inode节点,这里使用的链表进行管理的,还有可能维护一个hash链表。对于目录项的各种具体操作就不在详细分析了。毕竟这次的主要任务是分析nfs协议的实现,所以init函数分析到此结束。
4.nfs_init_versions函数
前面主要完成了nfs协议相关信息的静态内容初始化,这个函数会根据前面的初始化信息执行各个nfs协议版本的初始化函数init,然后会注册监听事件来监听客户端的请求。这个函数的实现如下:
int nfs_init_versions (struct nfs_state *nfs, xlator_t *this){struct nfs_initer_list *version = NULL;//nfs各个版本协议初始化函数列表struct nfs_initer_list *tmp = NULL;rpcsvc_program_t *prog = NULL;//定义个描述rpc服务程序的结构体int ret = -1;struct list_head *versions = NULL;versions = &nfs->versions;//需要遍历的协议链表list_for_each_entry_safe (version, tmp, versions, list) {//变量所有的nfs协议版本prog = version->init (this);//调用协议版本的初始化函数(前面已经分析了具体的初始化过程)prog->actorxl = this;//执行属于哪一个xlatorversion->program = prog;//保存初始化函数返回描述协议的rpc服务程序结构体if (nfs->override_portnum)//是否覆盖端口prog->progport = nfs->override_portnum;//覆盖端口ret = nfs_rpcsvc_program_register (nfs->rpcsvc, *prog);//注册rpc服务监听端口}return ret;}
这个函数的作用主要在初始化由rpc服务相关的内容,某个nfs版本的协议初始化在前面已经分析了,所以这个函数中重点需要分析的内容就是注册rpc服务的函数了,先看看实现,如下:int nfs_rpcsvc_program_register (rpcsvc_t *svc, rpcsvc_program_t program){rpcsvc_program_t *newprog = NULL;rpcsvc_stage_t *selectedstage = NULL;int ret = -1;newprog = GF_CALLOC (1, sizeof(*newprog),gf_common_mt_rpcsvc_program_t);//分配资源memcpy (newprog, &program, sizeof (program));//拷贝INIT_LIST_HEAD (&newprog->proglist);//初始化程序链表list_add_tail (&newprog->proglist, &svc->allprograms);//添加到所有程序链表的末尾selectedstage = nfs_rpcsvc_select_stage (svc);//选择rpc服务阶段程序ret = nfs_rpcsvc_stage_program_register (selectedstage, newprog);//执行rpc阶段程序的注册ret = nfs_rpcsvc_program_register_portmap (svc, newprog);//注册本地端口映射服务return ret;}
真正实现监听功能的是在函数nfs_rpcsvc_stage_program_register中,所以下面继续看这个函数的实现:int nfs_rpcsvc_stage_program_register (rpcsvc_stage_t *stg, rpcsvc_program_t *newprog){rpcsvc_conn_t *newconn = NULL;rpcsvc_t *svc = NULL;svc = nfs_rpcsvc_stage_service (stg);//获得阶段服务程序newconn = nfs_rpcsvc_conn_listen_init (svc, newprog);//创建监听的socket//注册监听事件发生执行的函数if ((nfs_rpcsvc_stage_conn_associate (stg, newconn, nfs_rpcsvc_conn_listening_handler, newconn)) == -1) {}return 0;}
这个函数调用nfs_rpcsvc_conn_listen_init函数创建监听使用的socket并且绑定,开始监听客户端的请求,并且初始化一些链接相关的状态信息。具体实现如下:rpcsvc_conn_t * nfs_rpcsvc_conn_listen_init (rpcsvc_t *svc, rpcsvc_program_t *newprog){rpcsvc_conn_t *conn = NULL;int sock = -1;//创建监听socket对象并且设置相应参数和绑定到对应端口,例如地址重用、设置为非阻塞等sock = nfs_rpcsvc_socket_listen (newprog->progaddrfamily, newprog->proghost, newprog->progport);conn = nfs_rpcsvc_conn_init (svc, sock);//初始化链接的核心,例如分配链接池等资源nfs_rpcsvc_conn_state_init (conn);//初始化rpc为已连接状态return conn;}
在nfs_rpcsvc_stage_program_register中还有一个很重要的函数是nfs_rpcsvc_stage_conn_associate,它关联一些当有链接请求来的时候执行的函数,这里主要是指客户端链接来的时候服务器响应事件时执行的函数。看看是怎么注册和关联的,如下:conn->stage = stg;conn->eventidx = event_register (stg->eventpool, conn->sockfd, handler, data, 1, 0);
终于到达事件处理的核心函数之一了: event_register事件注册函数并且返回注册后id值。这个函数中就一句重点代码event_pool->ops->event_register (event_pool, fd, handler, data, poll_in, poll_out);
由前面初始化过程可知,这里的event_pool->ops的值如下:static struct event_ops event_ops_epoll = {.new = event_pool_new_epoll,.event_register = event_register_epoll,.event_select_on = event_select_on_epoll,.event_unregister = event_unregister_epoll,.event_dispatch = event_dispatch_epoll};
所以这里就是执行event_register_epoll函数,这个函数会在socket描述符上注册一些事件,然后广播一个条件信号,在阻塞的线程就会开始执行并开始调用epoll_wait等待具体的IO事件,当注册的IO事件响应以后会调用响应的函数处理,上面是注册了socket读取事件,也就是如果有客户端的链接请求到来时会执行这里注册的函数,注册的函数定义如下:int nfs_rpcsvc_conn_listening_handler (int fd, int idx, void *data, int poll_in, int poll_out, int poll_err){rpcsvc_conn_t *newconn = NULL;rpcsvc_stage_t *selectedstage = NULL;int ret = -1;rpcsvc_conn_t *conn = NULL;rpcsvc_t *svc = NULL;if (!poll_in)//值处理读取的IO,这里是指客户端发出的链接请求return 0;conn = (rpcsvc_conn_t *)data;//得到传输过来的数据svc = nfs_rpcsvc_conn_rpcsvc (conn);//得到链接阶段的处理程序newconn = nfs_rpcsvc_conn_accept_init (svc, fd);//接收链接请求并且返回一个新的套接字用于通信selectedstage = nfs_rpcsvc_select_stage (svc);//选择一个rpc阶段处理程序(链接阶段)//已经接受连接,需要关联下一个阶段的事件处理程序:指的应该就是数据传输相关,如读写等ret = nfs_rpcsvc_stage_conn_associate (selectedstage, newconn, nfs_rpcsvc_conn_data_handler, newconn);return ret;}
这个函数的功能就是接受客户端的链接并建立新的套接字用于以后单独与客户端通信(传输数据),当然这个新的套接字需要注册相应的读写等epoll事件,注册流程和监听事件完全一样,只是不同的参数(socket和事件类型等)而已。这些事件的处理函数也就是在这里传递函数指针:nfs_rpcsvc_conn_data_handler函数,当有数据传输时就会执行这个函数中的代码,看看它是怎么处理的:int nfs_rpcsvc_conn_data_handler (int fd, int idx, void *data, int poll_in, int poll_out, int poll_err){rpcsvc_conn_t *conn = NULL;int ret = 0;conn = (rpcsvc_conn_t *)data;if (poll_out)ret = nfs_rpcsvc_conn_data_poll_out (conn);//处理可写事件(套接字可写)if (poll_err) {ret = nfs_rpcsvc_conn_data_poll_err (conn);//处理套接字出错事件return 0;}if ((ret != -1) && poll_in) {//如果处理可写事件失败以后就不处理可读事件了ret = 0;ret = nfs_rpcsvc_conn_data_poll_in (conn);//处理可读事件}if (ret == -1)nfs_rpcsvc_conn_data_poll_err (conn);//出错处理return 0;}
这个函数基本上就处理客户端与服务器连接以后的各种可读可写事件,具体的处理在各个函数中,就不在详细分析了,相信到达这里以后就不在有其他的难点了。
到此为止这个nfs协议初始化部分分析完毕!
第三节、fini函数
这个函数和init函数做的基本上是完全相反的工作,主要工作就是卸载掉nfs的各个版本的协议并且释放各种资源,实现如下:
int fini (xlator_t *this){struct nfs_state *nfs = NULL;nfs = (struct nfs_state *)this->private;//从xlator获得私有数据转换为struct nfs_state结构体nfs_deinit_versions (&nfs->versions, this);//卸载协议return 0;}
这个函数代码简单,首先从xlator得到struct nfs_state结构体数据,这是在初始化的时候设置的,然后就调用函数nfs_deinit_versions 来完成协议具体卸载。卸载函数定义如下:int nfs_deinit_versions (struct list_head *versions, xlator_t *this){struct nfs_initer_list *version = NULL;struct nfs_initer_list *tmp = NULL;struct nfs_state *nfs = NULL;nfs = (struct nfs_state *)this->private;list_for_each_entry_safe (version, tmp, versions, list) {//遍历所有版本的协议if (version->program)nfs_rpcsvc_program_unregister (nfs->rpcsvc, *(version->program));//注销rpc服务过程list_del (&version->list);//从版本链表中依次删除GF_FREE (version);//释放内存资源}return 0;}
整个过程都比较简单就不在详细分析卸载过程了。
六、NFS协议之RPC的实现
因为nfs服务器启动时的端口是不确定的,所以nfs服务器将自己的端口注册到rpc服务,客户端通过rpc请求知道nfs服务器的监听端口。下面就分析整个rpc的处理过程。现在假设客户端有一个rpc请求达到服务器端了,通过上面nfs协议初始化的分析知道:所有的数据读写事件都是在函数nfs_rpcsvc_conn_data_handler中处理,因为是客户端发送来的请求数据,所以执行的是epoll_in事件处理相关代码,这些事件的处理都是在函数nfs_rpcsvc_conn_data_poll_in中,这个函数实现如下:
int nfs_rpcsvc_conn_data_poll_in (rpcsvc_conn_t *conn){ssize_t dataread = -1;size_t readsize = 0;char *readaddr = NULL;int ret = -1;readaddr = nfs_rpcsvc_record_read_addr (&conn->rstate);//rpc服务记录开始读取数据的地址readsize = nfs_rpcsvc_record_read_size (&conn->rstate);//rpc服务记录数据需要读取的长度dataread = nfs_rpcsvc_socket_read (conn->sockfd, readaddr, readsize);//从socket中读出记录数据if (dataread > 0)ret = nfs_rpcsvc_record_update_state (conn, dataread);//根据读取的数据处理return ret;}
上面代码首先会根据rpc服务记录中的接收数据类型来判断接收什么数据,主要是分为头部消息和正式的rpc消息,正式的rpc消息的长度是通过头部消息中给出的,所以接收消息的步骤一般是先头部消息,然后正式的rpc调用消息,否则就是视为错误的消息,然后根据消息的长度从socket中读出消息到rpc服务记录的结构体的成员变量中,最后交给函数nfs_rpcsvc_record_update_state处理,它根据读取的数据来处理整个rpc的过程,包括xdr(外部数据表示)和根据消息获取调用的函数并且执行函数,具体实现如下:int nfs_rpcsvc_record_update_state (rpcsvc_conn_t *conn, ssize_t dataread){rpcsvc_record_state_t *rs = NULL;rpcsvc_t *svc = NULL;rs = &conn->rstate;if (nfs_rpcsvc_record_readfraghdr(rs))//根据rpc服务的记录状态是否读取头部消息dataread = nfs_rpcsvc_record_update_fraghdr (rs, dataread);//读取消息头部if (nfs_rpcsvc_record_readfrag(rs)) {//是否读取后面的数据if ((dataread > 0) && (nfs_rpcsvc_record_vectored (rs))) {//是否读取向量片段(dataread = nfs_rpcsvc_handle_vectored_frag (conn, dataread);//处理向量片段数据} else if (dataread > 0) {dataread = nfs_rpcsvc_record_update_frag (rs, dataread);//更新rpc服务记录的片段数据}}if ((nfs_rpcsvc_record_readfraghdr(rs)) && (rs->islastfrag)) {//如果下一条消息是头部消息且是最后一帧nfs_rpcsvc_handle_rpc_call (conn);//处理rpc调用svc = nfs_rpcsvc_conn_rpcsvc (conn);//链接对象引用加1nfs_rpcsvc_record_init (rs, svc->ctx->iobuf_pool);//重新初始化rpc服务记录的状态信息}return 0;}
整个函数首先读取协议信息的头部消息,读取完头部信息以后更新rpc服务记录状态,然后根据更新的状态继续读取头部信息后面的消息,后面的消息分为两种情况来读取,一般第一次来的是一个头部消息,这个消息中记录了下一次需要读取的消息的长度,也就是正式的rpc调用信息的长度。所以当第二次消息响应来的时候就是正式消息,根据不同的消息有不同的处理方式。头部消息处理方式主要是为接收正式的消息做一些初始化和准备工作(例如数据的长度和类型等)。如果头部消息则不会执行处理rpc的调用函数,因为它必须要接收到rpc调用消息以后才能处理。下面继续分析处理rpc调用的函数nfs_rpcsvc_handle_rpc_call,因为它是处理整个rpc调用的核心,它的实现如下:int nfs_rpcsvc_handle_rpc_call (rpcsvc_conn_t *conn){rpcsvc_actor_t *actor = NULL;rpcsvc_request_t *req = NULL;int ret = -1;req = nfs_rpcsvc_request_create (conn);//动态创建一个rpc服务请求对象(结构体)if (!nfs_rpcsvc_request_accepted (req))//是否接受rpc服务请求;actor = nfs_rpcsvc_program_actor (req);//得到rpc服务调用过程的描述对象if ((actor) && (actor->actor)) {THIS = nfs_rpcsvc_request_actorxl (req);//得到请求的xlator链表nfs_rpcsvc_conn_ref (conn);//链接状态对象的引用加1ret = actor->actor (req);//执行函数调用}return ret;}
这个函数首先根据链接状态对象创建一个rpc服务请求的对象,然后根据rpc服务请求对象得到一个rpc服务调用过程的描述对象,最后就根据这个描述对象执行具体的某一个rpc远程调用请求。下面在看看怎样根据连接状态对象创建rpc服务请求对象的,nfs_rpcsvc_request_create函数实现如下:rpcsvc_request_t * nfs_rpcsvc_request_create (rpcsvc_conn_t *conn){char *msgbuf = NULL;struct rpc_msg rpcmsg;struct iovec progmsg; /* RPC Program payload */rpcsvc_request_t *req = NULL;int ret = -1;rpcsvc_program_t *program = NULL;nfs_rpcsvc_alloc_request (conn, req);//从内存池中得到一个权限请求对象并且初始化为0msgbuf = iobuf_ptr (conn->rstate.activeiob);//从激活的IO缓存得到一个用于消息存放的缓存空间//从xdr数据格式转换到rpc数据格式ret = nfs_xdr_to_rpc_call (msgbuf, conn->rstate.recordsize, &rpcmsg,&progmsg, req->cred.authdata, req->verf.authdata);nfs_rpcsvc_request_init (conn, &rpcmsg, progmsg, req);//根据上面转换的消息初始化rpc服务请求对象if (nfs_rpc_call_rpcvers (&rpcmsg) != 2) {//rpc协议版本是否支持;}ret = __nfs_rpcsvc_program_actor (req, &program);//根据程序版本号得到正确的rpc请求描述对象req->program = program;ret = nfs_rpcsvc_authenticate (req);//执行权限验证函数调用验证权限if (ret == RPCSVC_AUTH_REJECT) {//是否被权限拒绝;}return req;}
通过上面的函数调用就得到了一个正确版本的rpc服务远程调用程序的描述对象,后面会根据这个对象得到对应的远程调用函数的描述对象,这个是通过下面这个函数实现的:rpcsvc_actor_t * nfs_rpcsvc_program_actor (rpcsvc_request_t *req){int err = SYSTEM_ERR;rpcsvc_actor_t *actor = NULL;actor = &req->program->actors[req->procnum];//根据函数id得到正确的函数调用对象return actor;}
这里得到的函数调用对象就会返回给调用程序,调用程序就会具体执行远程过程调用了。到此一个完整的rpc调用以及一个nfs服务就完成了,nfs服务器就等待下一个请求,整个过程可谓一波三折,整个过程绕了很大一个圈。下面通过一个图来完整描述整个过程:
附件1 NFS Protocol Family
这篇关于Glusterfs之nfs模块源码分析之NFS协议之RPC的实现和NFS协议内容的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!