精华内容
下载资源
问答
  • NFS协议解析

    千次阅读 2019-04-22 16:22:29
    关于NFS协议A1。NFS版本2和3之间的主要区别是什么?答:从系统的角度来看,主要区别在于:版本2客户端只能访问文件的最低2GB(带符号的32位偏移)。版本3客户端支持更大的文件(最多64位偏移)。最大文件大小取决...

    有助于了解NFS的文档,摘自http://nfs.sourceforge.net/,用浏览器的翻译插件翻译,可能某些翻译不准确。
    A.关于NFS协议
    A1。NFS版本2和3之间的主要区别是什么?
    答:从系统的角度来看,主要区别在于:

    版本2客户端只能访问文件的最低2GB(带符号的32位偏移)。版本3客户端支持更大的文件(最多64位偏移)。最大文件大小取决于NFS服务器的本地文件系统。


    NFS版本2将线上NFS读取或写入操作的最大大小限制为8KB(8192字节)。基于UDP的NFS版本3理论上支持高达56KB(UDP数据报的最大大小为64KB,因此具有NFS,RPC和UDP标头的空间,是NFS上UDP的最大线上NFS读取或写入大小约为60KB)。对于基于TCP的NFS版本3,限制取决于实现。大多数实现不支持超过32KB。


    NFS版本3引入了弱缓存一致性的概念。弱缓存一致性可帮助NFS版本3客户端更快地检测由其他客户端修改的文件的更改。这是通过在服务器对读或写操作的回复中返回额外的属性信息来完成的。客户端可以使用此信息来确定其数据和属性缓存是否过时。


    版本2客户端自己解释文件的模式位以确定用户是否可以访问文件。版本3客户端可以使用新操作(称为ACCESS)来请求服务器决定访问权限。这允许不支持访问控制列表的客户端与服务器正确交互。


    NFS版本2要求服务器必须将写入操作中的所有数据保存到磁盘,然后才能回复写入操作已完成的客户端。这可能很昂贵,因为它会将写入请求分成小块(8KB或更少),每个块必须在写入下一个块之前写入磁盘。当磁盘可以同时写入大量数据时,磁盘效果最佳。


    NFS版本3引入了“安全异步写入”的概念。版本3客户端可以指定允许服务器在将请求的数据保存到磁盘之前进行回复,从而允许服务器将小型NFS写入操作收集到单个高效的磁盘写入操作中。版本3客户端还可以指定在服务器回复之前必须将数据 写入磁盘,就像版本2写入一样。客户端通过将每个写入操作的参数中的stable_how字段设置为 UNSTABLE以请求安全的异步写入来指定写入类型,并将FILE_SYNC设置为NFS版本2样式写入。

    服务器通过在对每个NFS写操作的响应中设置相应的字段来指示是否永久存储所请求的数据。服务器可以使用UNSTABLE应答或FILE_SYNC应答响应不稳定写入请求,具体取决于所请求的数据是否仍驻留在永久存储器上。符合NFS协议的服务器必须仅使用FILE_SYNC回复来响应FILE_SYNC请求。

    客户端使用称为COMMIT的版本3中提供的新操作确保使用安全异步写入写入的数据已写入永久存储器。在将请求中指定的所有数据写入永久存储器之前,服务器不会向COMMIT操作发送响应。NFS版本3客户端必须保护使用安全异步写入但尚未提交的缓冲数据。如果服务器在客户端发送适当的COMMIT之前重新启动,则服务器可以以强制客户端重新发送原始写入操作的方式回复最终的COMMIT请求。在关闭(2) 或fsync(2)期间刷新对服务器的安全异步写入时,版本3客户端使用COMMIT操作 系统调用,或遇到内存压力时。

    有关NFS版本3协议的更多信息,请阅读 RFC 1813。
    A2。我可以跨TCP / IP传输协议运行NFS吗?
    答:客户端对NFS over TCP的支持已集成到所有2.4及更高版本的内核中。
    TCP的服务器支持出现在2.4.19及更高版本的2.4内核中,以及2.6及更高版本的内核中。并非所有基于2.4的发行版都支持Linux NFS服务器中的NFS over TCP。
    A3。是否还有其他版本的NFS正在开发中?
    答:是的。NFS版本4正在互联网工程任务组(IETF)的监督下开发 。IETF主持 了几个 描述NFS Version 4工作组迄今所做工作的文档。一些商业供应商已经发布了支持新版NFS的NFS客户端和服务器。
    在Andy Adamson的指导下,密歇根大学信息技术集成中心正在开发Linux版本的NFS 。此版本现在可在Linux 2.6内核中使用。虽然这是NFS版本4客户端和服务器的参考实现,但是作为IETF标准过程的一部分需要两个这样的实现之一,它仍然缺少一些功能。这些功能目前正在开发中,很快就会出现。有关更多信息,请访问CITI UM的 NFSv4项目网站。
    A4。如何防止使用NFS版本2或其他NFS版本?
    答:协议版本是在安装时确定的,可以通过指定服务器支持的NFS协议版本或传输协议版本来修改。例如,客户端挂载命令
    mount -o vers = 3 foo:// bar将在授予挂载请求时请求服务器使用NFS版本3(请注意,“vers”和“nfsvers”在mount命令中具有相同的含义;字符串“vers”与Solaris和其他供应商的NFS实现兼容)。如果您希望在所有情况下都禁止使用NFS版本2,则必须在服务器上重新启动rpc.mountd,并使用选项 “-N 1 -N 2”。执行此操作的最佳方法是通过修改NFS启动脚本选项,然后关闭并重新启动NFS作为整体来修改服务器上的nfs rpc.mountd配置:
    cd /etc/rc.d/init.d
    修改nfs脚本中的RPCMOUNTDOPTS以包含 “-N 1 -N 2”
    使用“./nfs restart”重新启动nfs(您必须具有root访问权限)
    现在,在重新启动rpc.mountd后尝试使用NFS版本2(现在无法识别)nfs挂载文件系统时,您将收到以下错误:
    mount:RPC:无法接收; errno =拒绝连接
    当您卸载任何nfs挂载的文件系统时,无论何时挂载,您都会收到以下(非致命)警告:
    错误的UMNT RPC:RPC:程序/版本不匹配; 低版本= 3,高版本= 3
    A5。我可以在Linux上使用NFS的Kerberos身份验证吗?
    A. Sun定义了一个名为RPCSEC GSSAPI的新接口,该接口创建了对基于RPC的NFS等协议使用身份验证插件的能力。这是为NFS提供Kerberos身份验证支持的标准方法。
    使用RPCSEC支持NFS安全机制GSSAPI目前正在Linux中开发,基于2.6内核中已有的工作。完成后,RPCSEC GSSAPI将与所有版本的NFS协议一起使用。除了三种Kerberos安全性(身份验证,完整性检查和完全隐私)之外,RPCSEC GSSAPI最终还将支持其他安全风格,如SPKM3,并将与其他实现完全兼容,例如Solaris中的实现。
    除了对RPCSEC GSSAPI的内核支持之外,还需要以各种用户级更改的形式提供额外支持(例如,mount命令和一对rpcgss守护程序)。目前,只有Fedora Core 2在其内核中启用了RPCSEC GSSAPI,并将用户级支持集成到其标准发行版中。我们希望,随着这项工作的成熟,它将被所有基于2.6的发行版所采用。
    目前Fedora Core 2仅支持在NFS版本4中使用Kerberos 5身份验证。由于存在漏洞和缺少功能,目前支持使用Kerberos的Linux NFS仅适用于早期采用者,而不适用于生产用途。
    有关RPCSEC GSS的更多信息,请阅读 RFC 2203。有关RPCSEC GSSAPI的Linux实现的信息,请访问 此处。
    A6。NFS协议版本4中的主要新功能是什么?
    答:以下是新功能的简短摘要。有关这些功能的完整讨论,请参阅 NFSv4工作组提供的 文档。

    NFS版本2和3是无状态协议,但NFS版本4引入了状态。NFS Version 4客户端使用state来通知NFS Version 4服务器其在文件上的意图:锁定,读取,写入等。NFS版本4服务器可以向客户端返回有关其他客户端对文件有何意图的信息,以允许客户端通过委派更积极地缓存文件数据。为了保持状态一致,NFS版本4协议内置了更复杂的客户端和服务器重启恢复机制。


    NFS版本4引入了对字节范围锁定和共享保留的支持。NFS版本4中的锁定是基于租约的,因此NFS版本4客户端必须与NFS版本4服务器保持联系以继续扩展其打开和锁定租约。


    NFS版本4引入了文件委派。NFS版本4服务器可以允许NFS版本4客户端访问和修改其自己的缓存中的文件,而不向服务器发送任何网络请求,直到服务器通过回调指示另一个客户端希望访问文件。在没有其他客户端希望同时访问一组文件的情况下,这会显着减少NFS版本4客户端和服务器之间的通信量。


    NFS版本4使用复合RPC。NFS版本4客户端可以将多个传统NFS操作(例如,LOOKUP,OPEN和READ)组合到单个RPC请求中,以在一次网络往返中执行复杂操作。


    NFS版本4指定了许多复杂的安全机制,并要求所有符合要求的客户端实现它们。除了传统的AUTH_SYS安全性之外,这些机制还包括Kerberos 5和SPKM3。提供了一个新的API,以便将来轻松添加新的安全机制。


    NFS版本4标准化了Posix和Windows环境中ACL的使用和解释。它还支持命名属性。用户和组信息以字符串的形式存储,而不是数值。ACL,用户名,组名和命名属性以UTF-8编码存储。


    NFS版本4将不同的NFS协议(stat,NLM,mount,ACL和NFS)组合到单个协议规范中,以便更好地与网络防火墙兼容。


    NFS版本4引入了对文件迁移和复制的协议支持。


    NFS版本4需要支持RPC over streaming网络传输协议,例如TCP。虽然许多NFS版本4客户端继续通过数据报支持RPC,但是这种支持可能会逐步淘汰,以支持更可靠的流传输协议。

    有关NFS版本4协议的更多信息,请阅读 RFC 3530。
    A7。我听说NFS版本4不能与早期版本的NFS互操作。什么是真正的交易?
    答: 与仅NFS版本3客户端无法与仅NFS版本2服务器通信的方式相同,仅NFS版本4客户端或服务器无法与仅支持早期版本NFS的客户端和服务器通信。NFS版本4在RPC标头中使用不同的版本号来区分新协议版本。因此,仅支持NFS版本4的客户端无法与仅支持版本2和3的服务器通信。通过实现可以使用所有三种协议版本进行通信的客户端和服务器实现真正的互操作性:NFS版本2,3和4。
    早期版本的Linux NFS Version 4原型使用两个独立的客户端:支持NFS版本2和3的原始客户端,以及仅支持NFS版本4的新的单独客户端。由于各种原因,这阻止了安装NFS版本4服务器的能力在安装NFS版本2和3服务器的同时。这是一个实现选择,而不是协议限制。现在不再是这样了:Linux 2.5 NFS客户端和Linux NFS客户端的所有未来版本都可以无缝地支持所有三个版本,并且可以同时挂载导出版本2,版本3和版本4的服务器。
    目标是NFS版本4将与版本2和3共存,其方式与NFS版本3与NFS版本2共存的方式大致相同。升级应该几乎是透明的。
    当客户端上运行的应用程序使用NFS版本4的某些新功能(如强制锁定,共享预留和委派)时,会出现一些小的互操作性问题。这些功能有助于使NFS版本4与CIFS等传统Windows文件系统更兼容。制作可以同时通过CIFS和NFS导出文件系统的文件服务器的Network Appliance发布了描述其中一些问题的论文。看到:
    Multiprotcol数据访问:NFS,CIFS和HTTP [TR 3014]
    SecureShare:保证多协议文件锁定[TR 3024]
    A8。什么是接近开放的缓存一致性?
    A. 完全不同的NFS客户端之间的高速缓存一致性实现起来非常昂贵,因此NFS可以满足大多数日常类型的文件共享要求。日常文件共享通常是完全顺序的:第一个客户端A打开文件,向其写入内容,然后关闭它; 然后客户端B打开相同的文件,并读取更改。
    因此,当应用程序打开存储在NFS中的文件时,NFS客户端会检查它是否仍然存在于服务器上,并通过发送GETATTR或ACCESS操作被允许进入开启者。当应用程序关闭文件时,NFS客户端会将所有挂起的更改写回文件,以便下一个开启者可以查看更改。这也使NFS客户端有机会通过close()的返回代码向应用程序报告任何服务器写入错误。此行为称为接近打开的高速缓存一致性。
    Linux通过比较文件关闭后完成的GETATTR操作的结果与下次打开文件时完成的GETATTR操作的结果,实现了接近开放的缓存一致性。如果结果相同,客户端将假定其数据缓存仍然有效; 否则,清除缓存。
    在2.4.20中,Linux NFS客户端引入了接近开放的缓存一致性。如果由于某种原因您拥有依赖于旧行为的应用程序,则可以使用“nocto”挂载选项禁用“近距离打开”支持。
    客户端的数据缓存仍有机会包含陈旧数据。NFS版本3协议引入了“弱缓存一致性”(也称为WCC),它提供了一种在操作之前和之后检查文件属性的方法,以允许客户端识别其他客户端可能已经进行的更改。不幸的是,当客户端使用许多同时更新同一文件的并发操作时,无法判断是客户端的更新还是其他客户端的更新更改了文件。
    因此,某些版本的Linux 2.6 NFS客户端完全放弃WCC检查,只需信任自己的数据缓存。在这些版本上,如果打开文件进行写入,客户端可以维护一个充满陈旧文件数据的缓存。在这种情况下,使用文件锁定是确保所有客户端都能看到文件数据的最新版本的最佳方法。
    系统管理员可以尝试使用“noac”挂载选项来实现多个客户端之间的属性缓存一致性。几乎每个客户端操作都会检查文件属性信息。通常,客户端会将此信息缓存一段时间,以减少网络和服务器负载。当“noac”生效时,客户端的文件属性缓存被禁用,因此每个需要检查文件属性的操作都会被强制返回服务器。这允许客户端以非常快的速度查看文件的更改,但代价是许多额外的网络操作。
    注意不要将“noac”与“无数据缓存”混淆。“noac”挂载选项将使文件属性与服务器保持同步,但仍存在可能导致客户端和服务器之间数据不一致的争用。如果您需要客户端之间的绝对缓存一致性,应用程序可以使用文件锁定,其中客户端在文件被锁定时清除文件数据,并在解锁文件之前将更改刷新回服务器; 或应用程序可以使用O_DIRECT标志打开其文件以完全禁用数据缓存。
    为了更好地理解NFS缓存设计中所面临的妥协,请参阅Callaghan的“ NFS Illustrated”。
    A9。为什么在多个客户端上使用O_APPEND打开文件会导致文件损坏?
    答 :NFS协议不支持原子追加写入,因此对于任何平台,追加写入在NFS 上永远不是原子的。
    大多数NFS客户端(包括比2.4.20更新的内核中的Linux NFS客户端)支持“接近开放”的高速缓存一致性,这提供了良好的性能并满足大多数应用程序的共享需求。这种高速缓存一致性不会在多个客户端之间提供文件大小属性的严格一致性,这对于确保追加写入始终放在文件末尾是必要的。
    阅读所有关于NFS高速缓存一致性模型这里。
    或者,NFS协议可以包括特定的原子追加写入操作,但是今天的协议版本没有。NFS协议的设计者认为原子附加写入很少使用,因此他们从未添加该功能。即使有这样的功能,保持文件大小属性是最新的将是具有挑战性的。
    A10。当我的申请因ESTALE错误而失败时,这意味着什么?
    A. NFS协议不按名称或路径引用文件和目录; 它使用一个称为文件句柄的不透明二进制值。在NFSv3中,此文件句柄最长可达64个字节; NFSv4允许它们更大。文件的文件句柄由NFS服务器分配,并且在该服务器的生命周期内应该是唯一的。客户端通过执行LOOKUP操作或使用READDIRPLUS操作的部分结果来发现文件的文件句柄的值。在安装NFS文件系统时,通常会执行一个特殊的过程来确定文件系统根目录的文件句柄。
    ESTALE是文件句柄无效时服务器报告的错误。以下是文件句柄无效的一些常见原因:
    1.该文件驻留在无法访问的导出中。 它可能是未导出的,导出的访问列表可能已更改,或者服务器可能已启动但只是不导出其共享。
    2.文件句柄指的是已删除的文件。 在服务器上删除文件后,客户端在尝试使用从先前的LOOKUP缓存的文件句柄访问该文件之前不会发现。在另一个客户端上使用时,使用rsync或mv替换文件是导致ESTALE错误的常见方案。
    3.该文件已重命名为另一个目录,并且在Linux NFS服务器导出的共享上启用了子树检查。 有关Linux NFS服务器上子树检查的更多详细信息,请参阅问题C7。
    4.保存导出文件的分区的设备ID已更改。 文件句柄通常包含全部或部分物理设备ID,并且该ID可能会在重新启动,与RAID相关的更改或服务器上的硬件热交换事件后发生更改。在Linux上使用“fsid”导出选项将强制导出的分区的fsid保持不变。有关更多详细信息,请参见“exports”手册页。
    5.导出的文件系统不支持永久的inode编号。 出于这个原因,通过NFS导出FAT文件系统是有问题的。通过仅导出具有良好NFS支持的本地文件系统可以避免此问题。有关更多信息,请参阅问题C6。
    客户端可以在路径名解析期间遇到ESTALE错误时恢复,但在READ或WRITE操作期间不会恢复。NFS客户端通过在读取或写入请求期间替换文件时立即通知应用程序来防止数据损坏。毕竟,如果应用程序写入错误文件或从错误文件读取,通常会造成灾难性后果。
    因此,通常,要从ESTALE错误中恢复,应用程序必须关闭发生错误的文件或目录,然后重新打开它,以便NFS客户端可以再次解析路径名并检索新文件句柄。
    即使在路径名解析期间,较旧的Linux NFS客户端也无法从ESTALE错误中恢复。在2.6.12内核及更高版本中,当遇到ESTALE以正确恢复时,Linux VFS层可以重新获得路径名解析。

    B.表现
    B1。一般来说,我该怎么做才能提高NFS性能?
    A.查看 NFS Howto doc 的 性能部分,然后查看几个方面:
    服务器上的磁盘IO速度有多快?这将对版本2和版本3的整体NFS性能产生重大影响。
    您的应用程序是否使用O_SYNC选项打开其文件?这将迫使NFS版本3的行为与(同步)NFS版本2完全相同。
    UDP需要IP片段重组。如果您在netstat -s的输出中看到碎片错误,则可能需要增加套接字缓冲区的大小。
    你有足够的NFS守护进程吗?查看/ proc / net / rpc / nfsd的内容,尤其是以“th”开头的行。该行的第一个数字是启动并等待NFS请求的NFS服务器线程的总数。第二个数字表示在任何时候所有线程是否一次运行。剩余的数字是线程计数时间直方图。有关根据此直方图中的数据调整服务器的详细信息,请参阅NFS操作方法。
    您的NIC和交换机/集线器/路由器是否可以自动协商到10baseT或半双工?半双工将为您提供更多的网络冲突,这是UDP中NFS性能最糟糕的事情。
    你在运行ext3还是ReiserFS?您可能会将日志放在单独的磁盘上或NVRAM上。截至2002年1月,ext3允许这样做,而Reiser有一个补丁可用。
    B2。一切似乎都很慢,我认为默认的rsize和wsize设置为1024 - 发生了什么?
    答:通常,Linux NFS客户端使用预读和延迟写入来隐藏NFS读写操作的延迟。但是,客户端每页只能缓存一个读取或写入请求。因此,如果读取或写入整个页面需要多个线上读取或写入操作(如果rsize或wsize为1024,则肯定会这样做),这些操作中的每一个必须在下一个操作发布之前完成。对于小型NFS版本3写入操作,写入必须是FILE_SYNC,因为客户端必须在发出下一个写入之前完全完成每个写入。
    请注意,对于支持较大页面的硬件,此限制尤为重要。例如,许多分销商提供了一个为Itanium处理器构建的Linux内核,它使用16KB页面,而不是通常在32位x86系统上找到的4KB页面。在这样的系统上,如果wsize小于16KB,则客户端始终按顺序发送写操作(如果它们出现在同一页中)。
    最后,请注意,在2.4内核中应用与TCP over TCP实现相关的所有修补程序时,Linux服务器允许的最大传输大小(NFSSVC_MAXBLKSIZE)设置为32KB。最新的2.4内核集成了TCP支持,允许传输大小高达32KB。
    B3。为什么我不能在我的客户端上安装超过255个NFS文件系统?为什么有时甚至不到255?
    A.在Linux上,为每个挂载的文件系统分配一个主编号,指示它是什么文件系统类型(例如,ext3,nfs,isofs); 和次要编号,使其在相同类型的文件系统中是唯一的。在2.6之前的内核中,Linux主要和次要数字只有8位,因此它们可以在数字范围内从0到255.由于次要数字只有8位,因此系统只能安装255个相同类型的文件系统。因此,系统最多可以安装255个NFS文件系统,另外255个ext3文件系统,255个以上的iosfs文件系统,等等。2.6之后的内核具有20位宽的次要数字,这减轻了这种限制。
    但是,对于Linux NFS客户端,问题有点糟糕,因为它是一个匿名文件系统。基于磁盘的本地文件系统具有与之关联的块设备,但匿名文件系统则没有。 例如,/ proc是一个匿名文件系统,其他网络文件系统也是如此。所有匿名文件系统共享相同的主要编号,因此在单个主机上最多只能安装255个匿名文件系统。
    通常,在任何给定的客户端上,您不需要超过十或二十个NFS挂载。但是,在某些大型企业中,您的工作和用户可能分布在数百个NFS文件服务器上。要解决可以在单个主机上安装的NFS文件系统数量的限制,我们建议您为Linux设置并运行一个automounter守护程序。自动挂载程序根据需要查找并装入文件系统,并卸载它发现的任何非活动状态。您可以在此处找到有关Linux自动安装程序的更多信息 。
    您还可能会对系统上的特权网络端口数量设置限制。NFS客户端使用唯一的套接字,每个NFS安装点都有自己的端口号。使用自动挂载程序可以通过自动卸载未使用的文件系统来帮助解决有限数量的可用端口,从而释放其网络端口。Linux NFS客户端中的NFS版本4支持每个客户端 - 服务器对使用一个套接字,这也有助于增加客户端上允许的NFS挂载点数。
    B4。为什么NFS Version 2看起来比版本3快得多?
    答:这里实际上有两个问题,还有一个功能。一,背景; NFS版本2协议规范 要求服务器在将响应发送到客户端之前将每个写入记录到永久存储。这使得服务器和客户端重启恢复非常简单,并且可以很好地保证发送到服务器的数据是永久存储的。Linux服务器(尽管不是Solaris参考实现)允许通过在/ etc / exports中设置per-export选项来放宽此要求 。此导出选项的名称是“[a] sync”(请注意,还有一个同名的客户端安装选项,但它具有不同的功能,并且不会破坏NFS协议合规性)。
    设置为“sync”时,Linux服务器行为严格符合NFS协议。这是大多数其他服务器实现中的默认行为。设置为“async”时,Linux服务器会在将数据或元数据修改操作刷新到永久存储之前回复NFS客户端,从而提高性能,但会破坏有关服务器重新启动恢复的所有保证。

    第一个问题:
    在nfs-utils-1.0.1之前,Linux NFS服务器上此导出选项的默认值为“async”。如果系统管理员未在/ etc / exports中指定“sync”或“async” ,则 exportfs默认使用“async”。这允许服务器在将请求的数据写入服务器磁盘之前回复版本2写入操作和元数据更新操作(例如CREATE或MKDIR),从而大大提高写入操作的性能以及引入无法检测的数据的可能性腐败。从版本1.0.1开始的nfs-utils的发布使用默认值“sync”,这使得Linux服务器能够正确地符合NFS协议规范。


    第二个问题:
    在Linux 2.2中支持NFS版本3的NFS服务器不支持“异步”导出选项。因此,默认情况下,在运行带有旧版本nfs-utils软件包的Linux 2.2的系统上,NFS版本2写入速度快且不安全,但版本3写入和提交操作是安全的,虽然速度较慢,因为它们始终遵循客户端的请求对于UNSTABLE或FILE_SYNC(参见问题A1)。


    功能:
    当您使用exportfs命令及其详细选项集时,它会显示对每个导出的文件系统有效的各种导出选项。如果设置了“async”导出选项,它将显示在选项列表中,但如果请求“sync”,它将不会出现在exportfs参数列表中。这反映了“sync”在其他平台中作为默认值的常见用法,但可能有些令人困惑。

    B5。为什么默认的NFS Version 2性能似乎与2.4内核中的NFS Version 3性能相当?
    答:有关导出选项如何影响Linux NFS服务器写入行为的背景信息,请参阅B4。
    从Linux 2.4开始,NFS版本3服务器识别“异步”导出选项。设置此选项后,服务器会在将数据写入永久存储器之前回复客户端。服务器还向客户端发送FILE_SYNC响应,指示客户端不需要保留缓冲数据或发送后续COMMIT操作。如果服务器在实际将数据写入稳定存储之前崩溃,则会将客户端暴露给与NFS版本2(使用“async”)相同的不可检测的损坏。(有关此行为及其后果的进一步讨论,请参阅问题B6。)请注意,即使客户端发送版本3 COMMIT操作,如果已使用“async”选项导出文件系统,服务器也会立即回复。
    相反,当在Linux 2.4服务器上使用“sync”导出选项时,版本2和版本3写入都符合NFS协议规范的要求。在这种情况下,NFS版本3具有优于NFS版本2的性能优势,同时在服务器崩溃期间保持数据弹性。
    请注意,“[a] sync”也会影响服务器上的某些元数据操作。
    B6。为什么“异步”导出选项不安全,这真的是一个严重的问题?
    答:最大的问题不仅在于它不安全,而且可能无法检测到腐败。
    在NFS版本2的Linux实现中,当“async”导出选项生效时,Linux NFS服务器可能会在将所有NFS写入请求发布到磁盘之前崩溃。但是,版本2客户端始终假定数据永久写入稳定存储,并且丢弃包含写入数据的缓冲区是安全的。
    服务器崩溃后,版本2客户端无法知道未写入的数据丢失; 这就是为什么版本2写入在服务器回复之前应该是永久性的。即使客户端仍然在其缓存中具有已修改的数据,服务器上的数据也不再与客户端上缓存的数据匹配(因为在服务器崩溃之前部分或全部写入未完成)。这可能会导致应用程序根据客户端缓存的数据而不是服务器上的数据做出未来的决策,从而进一步破坏文件。
    对于NFS版本3的Linux实现,不再需要使用“async”导出选项来允许更快的写入。NFS版本3明确允许服务器在受控情况下将数据写入磁盘之前进行回复。它允许客户端和服务器就写入数据的处置进行通信,以便在服务器重新引导时,第3版客户端可以检测到重新引导并重新发送数据。
    总之,请确保Linux NFS服务器上的所有导出都通过显式设置或通过将nfs-utils软件包升级到1.0.1版或更高版本来使用“sync”选项。如果需要快速写入,请确保使用NFS版本3装入客户端。您还可以通过向导出添加“wdelay”选项来提高写入性能。
    B7。我在某些客户端基准测试中实现了非常快的速度,但是当我的客户端负载很重时,它会大大减慢速度。为什么会这样?
    答:Linux客户端限制每个挂载点的挂起读取或写入操作的总数。这可以防止客户端在网络或服务器运行缓慢时使用缓存的读取或写入请求耗尽其内存。硬限制是每个安装点256个未完成的读取或写入操作。达到该限制时,客户端不会发出新的读取或写入操作,直到至少一个未完成的读取或写入操作完成,从而序列化该挂载点上的所有读取和写入,直到负载减少。
    减轻这种影响的两种方法是:
    1.
    在客户端的挂载点上增加rsize和wsize。这会增加在任何给定时间可能涉及未完成读取或写入的数据量。
    2.
    3.
    在客户端上多次挂载相同的服务器分区,并在挂载点之间分布应用程序。
    4.
    2.6和更高版本的内核中已删除此限制。
    B8。当我挂载Linux NFS服务器时,为什么我的客户端不允许我使用大于8KB的rsize或wsize?
    答: NFS版本2支持最多8KB的读写操作。NFS版本3允许更大的读写(参见问题 A1)。对于NFS版本2或3,早于2.4.20的库存2.4内核不支持大于8192字节的读取或写入操作。服务器端TCP支持(在2.4.20中作为实验性编译时选项引入)增加了服务器的最大值通过增加NFSSVC_MAXBLKSIZE的值来将I / O大小增加到32KB (参见问题B2)。
    当客户端安装文件服务器时,文件服务器会在单个操作中通告它可以读取或写入的最大字节数。客户端始终使用服务器的最大值和mount命令中客户端指定的rsize和wsize值指定的值中较小的值。
    使用UDP时,较大的rsize和wsize值可能会抑制性能。UDP数据报必须分成适合您网络的最大传输单元的片段。丢失任何这些片段需要重传整个数据报。如果您的网络拥挤,这可能会对客户端性能产生特别不利的影响。TCP在恢复一个或两个丢失的段并管理网络拥塞方面要好得多,因此在使用NFS over TCP时,更大的I / O操作通常可以更有效地提高性能。
    B9。我使用“sync”或“noac”挂载选项。我增加了我的wsize,但写入吞吐量低于我的预期。为什么是这样?
    答: 通常,NFS客户端会延迟发送应用程序写入请求,从而允许应用程序处理与NFS写入操作重叠。NFS客户端仅在应用程序关闭或刷新文件时使应用程序等待写入完成。但是,当客户端同步发送写入操作时,客户端会使应用程序等待每个写入操作在服务器上完成。这导致性能低得多。
    Linux NFS客户端在许多情况下使用同步写入,其中一些是显而易见的,其中一些是您可能不期望的。应用程序通过使用O_SYNC或O_DSYNC标志打开文件,为单个文件启用同步写入。系统管理员通过使用“sync”选项挂载该文件系统,为本地文件系统中的所有文件启用同步写入。“noac”挂载选项还可以启用同步写入。如果没有,则在客户端延迟写入时,在其他客户端上运行的应用程序将很难检索文件修改。
    目前,Linux NFS客户端有一个限制,阻止它安全地生成大型同步写入。客户端将大写请求分解为不大于单个页面的线上写入操作,以保证写入请求以字节顺序到达服务器的磁盘(某些应用程序依赖于此行为)。即使您将wsize设置为大于页面,客户端也会将任何应用程序写入请求分解为页面大小的NFS写入操作以满足此保证。
    此外,如果服务器的页面大小大于客户端的页面大小,则当客户端以小块写入时,服务器将被强制执行其他工作。NFS客户端通常将读取和写入对齐到它们自己的页面大小,如果它使用较大的页面,则可能在服务器上未对齐。根据服务器操作系统和文件系统,这可能会导致许多性能限制问题。
    B10。有时我的服务器变慢或变得反应迟钝,然后恢复生机。我正在使用NFS over UDP,我注意到网络上有很多IP碎片。有什么我能做的吗?
    A. 必须将大于IP最大传输单元(MTU)的UDP数据报分成足够小以便传输的部分。例如,如果您的网络MTU为1524字节,则Linux IP层必须将大于1524字节的UDP数据报分成不同的数据包,所有数据包都必须小于MTU。这些分离的数据包称为片段。
    Linux IP层在分解UDP数据报时传输每个片段,在每个片段中编码足够的信息,以便接收端可以将各个片段重新组合成原始UDP数据报。如果发生阻止客户端继续分段数据包的事情(例如,超出IP层中的输出套接字缓冲区空间),则IP层停止发送片段。在这种情况下,接收端有一组不完整的片段,并且在一定时间窗口之后,如果片段没有收到足够的片段来组装完整的数据报,它将丢弃片段。发生这种情况时,UDP数据报将丢失。客户端在一定时间间隔后没有收到服务器的回复时检测到此丢失,并通过重新传输数据报进行恢复。
    在大量写入负载下,Linux NFS客户端可以生成许多大型UDP数据报。这可以快速耗尽客户端上的输出套接字缓冲区空间。如果在短时间内多次发生这种情况,客户端会向服务器发送大量片段,但几乎从不会向服务器获取整个数据报的片段。这将填充服务器的IP重组队列,导致它无法通过UDP访问,直到它从队列中排出无用的片段。
    请注意,在读取负载较重的服务器上可能会发生同样的情况。如果服务器的输出套接字缓冲区太小,则大的读取将导致它们在IP碎片期间溢出。然后,客户端的IP重组队列会填充无用的片段,并且很少有UDP流量可以到达客户端。
    以下是此问题的一些症状:
    您使用带有大型wsize的NFS(相对于网络的MTU),并且您的应用程序工作负载是写入密集型的,或者使用带有读取密集型应用程序的大型rsize。
    您可能会在服务器或客户端上看到许多碎片错误(netstat -s将讲述故事)。
    您的服务器可能会定期变得非常慢或无法访问。
    增加服务器上的线程数对性能没有影响。
    一个或少数客户端似乎使服务器无法使用。
    客户端和服务器之间的网络路径可能具有带有小端口缓冲区的路由器或交换机,或者路径可能包含以不同速度(100Mb / s和GbE)运行的链路。
    解决方法是使Linux的IP分段逻辑继续分段数据报,即使输出套接字缓冲区空间超过其限制。此修复程序出现在比2.4.20更新的内核中。您可以通过以下几种方法之一解决此问题:
    1.使用NFS over TCP。TCP不使用碎片,因此它不会遇到此问题。对于仅支持NFS over UDP的旧Linux NFS客户端和服务器,可能无法使用TCP。
    2.如果无法使用NFS over TCP,请将客户端升级到2.4.20或更高版本。
    3.如果无法升级客户端,请增加客户端套接字缓冲区的默认大小(参见下文)。2.4.20及更高版本的内核会自动为NFS客户端的套接字缓冲区执行此操作。有关 更多信息,请参见 NFS方法的 第5.3节。
    4.如果你的rsize或wsize非常大,请减少它。这将减少客户端和服务器的输出套接字缓冲区的负载。
    5.通过确保您的GbE链路使用全流控制,交换机和路由器端口使用足够的缓冲区大小以及所有链路正在协商其最快设置来减少网络拥塞。
    B11。为什么我的服务器在使用Linux客户端时会看到如此多的ACCESS调用?
    A. 默认NFS服务器行为是为了防止客户端计算机上的root用户具有对导出文件的特权访问权限。服务器通过将“root”用户映射到服务器端的某些非特权用户(通常是用户“nobody”)来完成此操作。这被称为root压扁。大多数服务器(包括Linux NFS服务器)都提供导出选项以禁用此行为,并允许所选客户端上的root用户享有导出文件系统的完全root权限。
    不幸的是,NFS客户端无法确定服务器是否正在压缩root。因此,当客户端以root用户身份运行应用程序时,Linux客户端将使用NFS版本3 ACCESS操作。如果应用程序作为普通用户运行,则客户端使用它自己的身份验证检查,并且不愿意联系服务器。
    Linux NFS客户端应该缓存这些ACCESS操作的结果。事实上,在新的2.6.x内核中,它执行此操作并将ACCESS检查扩展到所有用户,以允许在服务器上进行通用的uid / gid映射。这还可以为服务器的本地文件系统中的访问控制列表提供适当的支持。在2.6之前的内核中,库存NFS客户端不会缓存ACCESS操作的结果。

    C.常见的导出配置错误
    C1。如何在服务器上跟踪导出的文件系统和客户端挂载点?
    A. / etc / exports包含有关如何正常导出文件系统的信息。这只能由exportfs读取。
    / var / lib / nfs / etab包含有关当前应将哪些文件系统导出给谁的信息。
    / var / lib / nfs / rmtab包含目前某些客户端实际安装的文件系统的列表。
    / proc / fs / nfs / exports包含有关当前导出到实际客户端(个人,而不是子网或其他)的文件系统的信息。
    / var / lib / nfs / xtab与/ proc / fs / nfs / exports的信息相同, 但由nfs-utils维护,而不是由内核直接维护。它仅在未安装/ proc时使用。
    C2。我是否可以修改导出权限而无需重新安装客户端以使其生效?
    答:是的。最安全的做法是编辑/ etc / exports 并运行“ exportfs -r ”。
    请注意,当挂载请求到达时,mountd check ... / etab以查看是否允许该主机访问。如果是,则将一个条目放在 ... / rmtab中并导出文件系统,从而在/ proc / fs / nfs / exports中创建一个条目 。
    当您运行“ exportfs -io <options> host:/ dir 然后更改../etab中的条目,或者添加一个新条目。如果它是子网/通配符/网络组条目,则../中的每一行 检查rmtab是否匹配。当找到匹配项时,将为内核提供(或更改)特定于主机的条目。当您运行“ exportfs -a”时,它确保/ etc / exports中的所有条目正确地反映在../etab中.etab中的任何额外条目都是单独的。一旦确定了etab的正确内容,就会检查rmtab以创建etab中任何新条目的特定主机条目列表。特定条目被提供给内核。
    当您运行“ exportfs -r ”时,它会忽略../etab的先前内容,并将etab初始化为/ etc / exportfs的内容。然后它检查rmtab并对/ proc / fs / nfs / export 进行必要的更改。
    C3。我的出口似乎是每个人都可读的 - 或者/ etc / exports没有给出预期的权限
    A. / etc / exports对空格非常敏感 - 因此以下语句不一样,因为选项“hostname”和左括号之间的空格:

    / export / dir hostname(rw,no_root_squash) 
    / export / dir主机名(rw,no_root_squash)
    第一个将授予对/ export / dir的主机名读写访问权限, 而不会压缩root权限。第二个将使用root squash 授予主机名读写权限,并且它将授予其他所有人读写权限,而不会压缩root权限。
    C4。我相信Linux NFS服务器不会导出fat32分区。那是对的吗?
    答:FAT文件系统可以从早期的2.4内核开始导出,但如果广泛使用,可能会导致悲伤。首先,只有那些由导出的文件系统支持的操作才会受到尊重。这些文件系统不支持“chown”,“link”和“symlink”等操作,并且会失败。读/写/创建等,应该没问题,只要文件保持相对不变。
    最严重的问题是FAT文件系统布局不包含足够的信息来创建NFS创建持久文件句柄所需的持久身份。例如,如果您获取一个文件,将其重命名为另一个目录,将其删除并向其写入新数据,则文件系统中没有任何内容可用于显示生成的文件在任何意义上都是“与原始文件相同,并且在给定有关原始文件的任何详细信息的情况下无法找到新文件。因此,Linux NFS服务器无法保证一旦打开文件,如果以上面给出的方式修改文件,则可以继续访问该文件。然后,NFS可能无法正确定位或识别文件,因此可能会返回ESTALE错误。
    C5。有时我的客户端在尝试挂载文件系统时会收到“权限被拒绝”错误,即使它在几小时前管理它而不更改服务器上的配置。
    答:您的服务器的/ etc / exports可能配置错误。如果导出文件包含域名和IP地址,则在装入时可能会导致随机客户端行为,尤其是当您的客户端具有向DNS注册的多个IP地址时。
    如果导出目录及其祖先之一,并且两者都驻留在服务器上的同一物理文件系统上,则在安装时可能会导致随机客户端行为。
    C6。我可以使用Linux NFS服务器导出哪些本地文件系统?
    答:我们希望以下本地文件系统能够正常运行,因为它们经常被测试:ext2,ext3,jfs,reiserfs,xfs。
    这些本地文件系统可能有用或可能有一些小问题:iso9660,ntfs,reiser4,udf。在NFS邮件列表上询问 详细信息。
    任何基于FAT或无法提供永久性inode编号的文件系统都会遇到NFS版本2和3的问题(参见问题C4)。
    已知不能与Linux NFS服务器一起使用的本地文件系统是:procfs,sysfs,tmpfs(和friends)。
    C7。为什么要禁用NFS服务器导出的子树检查?
    A.当NFS服务器导出本地文件系统的子目录但未导出其余部分时,NFS服务器必须检查每个NFS请求是否针对驻留在导出区域中的文件。此检查称为子树检查。
    要执行此检查,服务器包含有关分发给NFS客户端的NFS文件句柄中每个文件的父目录的信息。例如,如果将文件重命名为其他目录,则会更改文件句柄,即使文件本身仍是同一文件。这会破坏NFS协议合规性,通常会导致客户端出现错误行为,例如ESTALE错误,对重命名或删除的文件的不当访问,损坏的硬链接等等。
    许多人认为,子树检查会比保存更麻烦,在大多数情况下应该避免。该subtree_check只有当你想防止文件处理者获取的是介于服务器的本地文件系统中导出的部分以外的文件猜测***选项是必需的。如果您需要确定没有人可以访问本地文件系统导出部分之外的文件,请在服务器上设置分区,以便只导出整个文件系统。

    D.常见错误消息
    D1。我一直在NFS服务器上获得权限失败消息。这些是什么?
    A.您提到的消息采用以下格式:

    1月7日09:15:29服务器内核:fh_verify:邮件/来宾权限失败,acc = 4,错误= 13 
    Jan 7 09:23:51服务器内核:fh_verify:ekonomi / test permission failure,acc = 4,error = 13

    当您对没有写访问权限的文件尝试进行NFS setattr操作时,会发生这种情况。这些消息是无害的。
    D2。什么是“愚蠢的重命名”?为什么这些.nfsXXXXX文件会一直显示?
    答: Unix应用程序经常打开一个临时文件然后取消链接。他们这样做是为了使文件在文件系统名称空间中看不到任何其他应用程序,以便系统在应用程序退出时自动清理(删除)文件。这被称为“在最后关闭时删除”,并且是Unix应用程序中的传统。
    由于NFS协议的设计,无法从名称空间中删除文件,但仍可由应用程序使用。因此,NFS客户端必须使用协议中已存在的内容来模拟它。如果取消链接打开的文件,NFS客户端会将其重命名为看起来像“.nfsXXXXX”的特殊名称。这在文件保持使用时“隐藏”该文件。这被称为“愚蠢的重命名”。请注意,NFS服务器与此行为无关。
    在客户端上的所有应用程序关闭了傻瓜重命名的文件后,客户端会通过删除服务器上的文件自动完成取消链接。通常这是有效的,但如果客户端在删除文件之前崩溃,它将保留.nfsXXXXX文件。如果您确定使用这些文件的应用程序不再运行,则可以安全地手动删除这些文件。
    NFS版本4协议是有状态的,实际上可以支持delete-on-last-close。不幸的是,没有一种简单的方法可以做到这一点,并保持与版本2和3访问器向后兼容。
    D3。这是什么意思:svc:unknown program 100227(me 100003)
    答:它指的是NFS客户端的安装请求,它支持Solaris NFS_ACL边带协议。主线内核中的Linux NFS服务器不支持此协议,但许多发行版包括在其NFS实现中提供NFS_ACL支持的修补程序。可以安全地忽略该消息。
    D4。我经常在日志中看到这个:
    kernel:nfs:server server.domain.name没有响应,仍在尝试
    kernel:nfs:task 10754无法获取请求槽
    kernel:nfs:server server.domain.name好的
    答 :“无法获取请求槽”消息意味着客户端RPC代码已检测到大量超时(可能是由于网络拥塞,可能是由于服务器过载),并且限制了并发数量未尽的请求,试图减轻负担。一些可能的原因:
    网络拥塞
    服务器重载
    由坏NIC或驱动程序丢弃的数据包(输入或输出)....
    D5。我刚刚升级到最新的nfs-utils,现在NLM锁定不再适用于驻留在我的NFS服务器上的文件。这是怎么回事?
    A.上有permisions 的/ var / lib中/ NFS /平方米和 /var/lib/nfs/sm.bak 必须解决的文件。rpc.statd运行的任何人必须拥有所有权并且rw访问这些目录。两者的权限应设置为700。此外,etab,rmtab和xtab都必须存在并且可以由root写入。
    D6。我已经安装了“intr”选项,但是当我的服务器不可用时,进程仍然无法启动。如何杀死进程以便我可以卸载它们?
    答 :确实,即使使用“intr”挂载选项,也不会总是成功杀死挂在NFS上的任务。在这些实例中,任务通常在内核中等待另一个进程持有的某个信号量。由于信号不能中断信号量,因此信号对挂起任务没有影响。
    已经有一些建议的解决方案,但都没有实施。一种是设置一个特殊的信号量类,它们可以用'SIGKILL'填充,但是最早在2.7内核之前无法替换VFS和VM层中的相关信号量。正在考虑的另一个解决方案是当用户请求卸载时使rpciod唤醒所有等待请求,允许它们以错误退出。
    在实现这些之前,您可以通过终止在给定文件系统中等待I / O完成的所有进程来解决此问题:
    使用'lsof'或其他方法来识别等待目标文件系统中文件的进程,
    然后杀死-9所有进程
    杀死-9 rpciod。
    另一种不太理想的解决方法是使用“软”安装座。这将导致进程在一段时间后停止重试I / O. 最终,进程将变为unstuck,您的文件系统可以卸载。但是,软质安装并不是完全安全的。有关使用“软”安装的风险的描述,请参阅问题E4。
    D7。锁定恢复怎么对我不起作用?
    A. 当客户端重新启动时,它应该通知它先前安装的任何服务器以释放所有已锁定的锁。它通过在系统启动期间调用rpc.statd来完成此操作。
    有几个常见问题可能阻止rpc.statd 工作。首先,确保您的客户端启用了相应的启动脚本(Red Hat发行版上的/etc/rc.d/init.d/nfslock)。接下来,确保当rpc.statd启动时,网络已经可用于它(例如,某些DHCP配置的主机可能存在此问题)。
    确保客户端的节点名(uname -n)与客户端上gethostbyname(3)返回的名称相同。这些可能因您的nsswitch配置,/ etc / hosts的内容而有所不同,因为您的客户端是通过DHCP配置的,或者是因为DNS配置错误。内核中的锁定进程使用客户端的节点名来在发送锁定请求时识别其锁定。Rpc.statd在发送恢复通知时必须发送相同的字符串,否则服务器无法将通知与其可能仍为客户端保留的任何锁定相匹配。
    还建议NFS客户端的节点名称是完全限定的域名,而不仅仅是主机名。如果具有相同主机名的其他域中的另一个客户端与您的服务器联系,则两个客户端上的完全限定的节点名将允许服务器区分在每个客户端上设置的锁。
    在客户端和服务器之间遍历防火墙时,如果需要锁定恢复工作,则必须允许双向RPC流量,因为NLM是基于回调的。可能阻止服务器调用客户端的两个重要问题是:
    阻塞锁(F_SETLKW)将受到阻碍,因为客户端希望服务器的lockd守护进程在释放任何冲突锁并授予锁后立即将其调回。
    服务器重启恢复将被破坏,因为服务器的rpc.statd 守护程序将无法通知客户端其锁已丢失且需要恢复。
    D8。当我的应用程序使用内存映射的NFS文件时,它会中断。为什么?
    答: 通常这是因为应用程序开发人员依赖某些本地文件系统行为来保证数据的一致性,而不是仔细阅读mmap手册页以了解所有文件系统实现所需的行为。一些例子:
    尽管munmap(2)的某些实现恰好将脏页写入本地文件系统,但munmap(2)的NFS版本却没有。一则msync(2)呼叫总是需要保证脏映射的数据被写入到永久存储。Linux NFS客户端对munmap(2)的处理的细微分支 是不考虑将munmap(2)作为近距离打开高速缓存一致性的近距离操作 。
    MS_SYNC和MS_ASYNC标志 之间的区别也很重要。 MS_ASYNC将迫使脏页映射到永久存储最终。只有MS_SYNC保证在 msync(2)返回应用程序之前写入页面。因此,应用程序应使用msync(MS_SYNC)序列化对映射文件的数据写入。
    最后,当通过close(2)关闭文件描述符时,Linux NFS客户端可能无法刷新脏映射页面。通常在关闭处理期间 ,客户端可以刷新映射的页面以及由write(2)调用弄脏的页面,但是不保证这种行为。许多应用程序将打开文件,映射它,然后关闭它并继续使用地图。上述行为是尝试优化此用例的性能。
    D9。当我更新NFS导出上的共享可执行文件时,在我的客户端上运行的程序都是段错误的。怎么会?
    答: 如果只是通过旧版本复制新的可执行文件或库,则通过更改客户端上保持打开的文件,违反了NFS缓存一致性规则(此处所述)。
    复制可执行文件会创建一个窗口,在此窗口期间,NFS客户端的缓存可能包含旧版本的部分内容和新版本的部分内容,所有这些内容都组合在同一文件中。更新NFS共享上的可执行文件和共享库的正确方法是使用带有'-b'选项的安装程序。这将重命名正在使用的可执行文件的版本,然后创建一个全新的文件以包含新版本的可执行文件。
    D10。我正在尝试使用flock() / BSD锁来锁定多个客户端上使用的文件,但文件已损坏。怎么会?
    A. flock() / BSD锁仅在2.6.12之前的Linux NFS客户端上本地执行。使用fcntl() / POSIX锁定以确保其他客户端可以看到文件锁定。
    以下是一些序列化对NFS文件的访问的方法。
    使用fcntl() / POSIX锁定API。 这种类型的锁定通过NLM协议或通过NFSv4在多个客户端之间提供字节范围锁定。
    使用单独的锁文件,并创建它的硬链接。 请参阅creat(2) 手册页的O_EXCL部分中的说明。
    值得注意的是,直到早期的2.6内核,O_EXCL创建在Linux NFS客户端上并不是原子的。除非运行的是比2.6.5更新的内核,否则不要在多个NFS客户端之间使用O_EXCL创建并期望原子行为。
    这是一个已知的问题,Perl 默认使用flock() / BSD锁定。这可能会破坏从其他操作系统(例如Solaris)移植的程序,这些操作系统希望flock/ BSD锁像POSIX锁一样工作。
    在Linux上,使用文件锁定而不是硬链接具有使用服务器检查客户端缓存的额外好处。获取文件锁定后,客户端将刷新该文件的页面缓存,以便任何后续读取从服务器获取新数据。释放文件锁定后,在释放锁定之前,将对该客户端上的文件的任何更改刷新回服务器,以便等待锁定该文件的其他客户端可以看到更改。
    2.6.12中的NFS客户端通过根据POSIX字节范围锁模拟BSD样式的锁,为NFS文件上的flock() / BSD锁提供支持。使用相同仿真机制或使用fcntl() / POSIX锁定的其他NFS客户端 将看到Linux NFS客户端看到的相同锁。
    在本地Linux文件系统上,POSIX锁和BSD锁彼此不可见。因此,由于这种模拟,在Linux NFS服务器上运行的应用程序仍然会看到NFS客户端锁定的文件被锁定为 fcntl() / POSIX锁,无论客户端上的应用程序是使用BSD样式还是POSIX-风格锁。如果服务器应用程序使用 flock()BSD锁,则不会看到NFS客户端使用的锁。
    D11。为什么不“ mount -oremount,tcp ”将安装了UDP的NFS安装文件系统转换为使用TCP挂载的文件系统?
    A. 在mount命令上的“重新安装”选项只对一般的安装选项,如RO / RW,同步,等等(见的人安装了通用的完整列表mount命令选项)。无法使用“mount -oremount”样式的mount命令更改nfs手册页中列出的NFS特定的挂载选项。您必须卸载文件系统并使用新选项再次安装它才能修改特定于NFS的设置。
    请注意, 无论内核中的实际安装设置是否已更改,mount命令都可以更新/ etc / mtab的内容。因此,当您尝试使用特定于NFS的挂载选项安装-oremount时,后续挂载命令可能会报告该设置有效。这只是因为mount命令正在读取/ etc / mtab。在的/ proc /坐骑文件反映了内核使用真实安装选项。
    D12。我没有使用“intr”挂载(默认为“nointr”),当我的服务器不可用时,某些进程无法启动。我能做什么?
    A. 使用umount命令的“-f”标志强制卸载。当umount命令尝试联系服务器时会有短暂的暂停,然后所有对服务器的未完成请求都将失败,从而使进程可以运行。
    收到I / O错误后的某些程序只会尝试更多的I / O,使它们再次无法振荡。出于这个原因,首先尝试先杀死卡住的挂载上的所有进程,然后运行“umount -f”。当I / O请求失败时,进程将变为可充电状态,将看到信号,并将死亡。有时它可以在“杀死进程”的几个阶段然后“umount -f”循环,直到文件系统被卸载,但它通常有效。
    如果所有其他方法都失败,您仍然可以使用“umount -l”命令卸载进程挂起的分区。这会导致卡住的挂载与客户端上的文件系统名称空间层次结构分离,因此其他进程将不再可见。您可以将该挂载点替换为另一个挂载到同一服务器再次可用时,或者如果远程数据已移动则替换到其他服务器。但请注意,旧的挂载点将继续消耗客户端内存,直到卡住的进程全部死亡。

    E.将Linux NFS与备用平台配合使用
    E1。我使用的是Tru64 Unix 4.x或SunOS 4.1.x客户端。NFS文件锁定似乎不起作用,除非我授予所有用户对该文件的权限。
    答 :NFS版本2和3的默认规范允许任何用户锁定文件,无论该用户是否有权访问该文件。Linux NFS服务器的编写者将此行为视为不安全,并选择仅允许有权访问文件的用户锁定它。但是,较旧的SunOS和Tru64客户端以及某些HP / UX客户端通过使用守护程序的凭据发出所有NFS文件锁定请求来利用NFS规范。这意味着如果守护程序无法访问文件,服务器将拒绝锁定它们。
    导出选项no_auth_nlm旨在缓解此问题。将其设置在您要导出到这些客户端的任何共享上。这将禁用对文件锁定请求的授权检查。
    E2。我没有使用Redhat或VALinux发行版,因此rpm中的nfs-utils启动脚本被破坏了。我该怎么办?
    答:您应该注释掉/etc/rc.d/init.d/nfs中的以下行 :
    。/etc/rc.d/init.d/functions
    E3。我正在使用Irix客户端,我在Linux服务器上看到了文件列表和cwd的一系列问题。服务器正在运行NFS版本3.这是一个Linux错误吗?
    A. IRIX不正确地处理Linux 2.4.x中的NFS服务器使用的小于32字节的文件句柄。SGI在2001年发布的IRIX 6.5.13中解决了这个问题。
    解决此问题的方法是使用NFS版本2.在IRIX客户端上,在挂载选项中使用vers = 2。
    E4。从Solaris NFS客户端安装Linux NFS服务器时,为什么会出现NFS超时?
    A.你得到NFS超时,因为使用的是软坐骑。通常,挂载很难,这需要客户端继续尝试永久地到达服务器。一个软mount允许客户端在一段时间后停止尝试操作。如果在数据或元数据传输期间发生静态数据损坏,则软超时可能会导致静默数据损坏,因此在客户端响应性比数据完整性更重要的情况下,您应该只使用软安装。如果您需要在不可靠的链路(如DSL)上使用软安装,请尝试使用TCP,这是Solaris默认使用的。这将有助于管理短暂网络中断的影响。如果无法使用TCP,则应通过在mount命令选项中指定长重传超时值和相对大量的重试(即timeo = 30,retrans = 10)来降低使用UDP软安装的风险。
    请注意,NFS over UDP现在在最新的2.4和2.6内核中使用重新传输超时估计算法,这意味着timeo = mount选项在防止软超时导致数据损坏方面效果较差。

    转载于:https://blog.51cto.com/13835328/2382772

    展开全文
  • 六、NFS协议之RPC的实现因为nfs服务器启动时的端口是不确定的,所以nfs服务器将自己的端口注册到rpc服务,客户端通过rpc请求知道nfs服务器的监听端口。下面就分析整个rpc的处理过程。现在假设客户端有一个rpc请求...
     
    

    一、网络文件系统概述

     

    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协议的。XDReXternal Data Representation,即外部数据表示法)提供一种方法,把数据从一种格式转换成另一种标准数据格式表示法,确保在不同的计算机、操作系统及程序语言中,所有数据代表的意义都是相同的。
    RPCRemote Procedure Call,远程程序调用)请求远程计算机给予服务。客户机通过网络传送RPC到远程计算机,请求服务。
    NFS运用RPC传送数据的方法有以下几步:
    1)客户送出信息,请求服务。
    2)客户占位程序把客户送出的参数转换成XDR标准格式,并用系统调用把信息送到网络上。
    3)信息经过网络送达远程主机系统。
    4)远程主机将接受到的信息传给服务器占位程序。
    5)把XDR形式的数据,转换成符合主机端的格式,取出客户发出的服务请求参数,送给服务器。
    6)服务器给客户发送服务的逆向传送过程。

    第二节、服务进程的作用

    服务进程是系统在启动计算机后自动运行的程序,包括对网络的连接、网络协议的加载、图形桌面的显示、文件系统的加载等,Linux系统中常见的进程包括以下几种。
    (1nfsd
    根据客户端对文件系统的需求,启动文件系统请求服务进程,响应客户的请求,而一般文件系统请求服务进程的数目是8,这也是在rc.local中写nfsd 8 &的原因。
    (2biod
    此进程是在NFS客户端上用的,用来启动异步块I/O服务进程来建立Buffer Cache,处理在客户机上的读写。(3mountd
    这是个RPC服务器。启动rpc.mountd服务进程后,mountd会读取/etc/xtab查看哪一台客户机正在挂载哪一个文件系统,并回应客户机所要挂载的路径。
    (4inetd Internet services服务进程
    当系统启动时,rc.local会启动inetd读取inetd.conf配置文件,读取网络上所有服务器的地址,链接启动inetd.conf中所有的服务器。当客户机请求服务时,inetd就会启动相关的服务进程,如user使用telnet时,inetd启动telnetd配合user telnet的需求,其余像ftpfingerrlogin等应用程序,inetd也都会启动相对应的服务程序ftpdfingerdrloingd等。
    (5portmap服务程序
    主要功能是将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)客户端会向服务器端的RPCport 111)发出NFS文件存取功能的询问请求。
    2)服务器端找到对应的已注册的NFS daemon端口后会回报给客户端。
    3)客户端了解正确的端口后,就可以直接与NFS守护进程来联机。
    由于NFS的各项功能都必须要向RPC注册,因此RPC才能了解NFS服务的各项功能的port numberPIDNFS在主机所监听的IP等,而客户端才能够通过RPC的询问找到正确对应的端口。即NFS必须要有RPC存在时才能成功地提供服务,因此我们称NFSRPC Server的一种。事实上,有很多这样的服务器都向RPC注册。例如,NISNetwork Information Service)也是RPC Server的一种。所以如下图所示,不论是客户端还是服务器端,要使用NFS都需要启动RPC

     


     

    2 NFS RPC 服务及操作系统的相关性

     

     

    NFS协议从诞生到现在为止,已经有多个版本,如NFS V2rfc794)及NFS V3rfc1813)(最新的版本是V4rfc307))。最早,SUN公司曾将NFS V2设计为只使用UDP,主要原因是当时机器的内存、网络速度和CPU的影响,不得不选择对机器负担较轻的方式。而到了NFS V3SUN公司选择了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内核提供了对通过TCPNFS的支持。要通过TCP来使用NFS,在客户端系统上挂载NFS导出的文件系统时包括一个“-o tcp”选项。使用TCP的优点和缺点如下:
    1)被提高了的连接持久性,因此获得的NFS stale file handles消息就会较少。
    2)载量较大的网络的性能会有所提高,因为TCP确认每个分组,而UDP只在完成时才确认。
    3TCP具有拥塞控制技术(UDP根本没有),在一个拥塞情况严重的网络上,UDP分组是被首先撤销的类型。使用UDP意味着,如果NFS正在写入数据(单元为8 KB的块),所有这8 KB数据都需要被重新传输。由于TCP的可靠性,8 KB数据中只有一部分需要重新传输。
    4)错误检测。当TCP连接中断(由于服务器停止),客户端就会停止发送数据而开始重新连接。UDP是无连接的,使用它的客户端就会继续给网络发送数据直到服务器重新上线为止。
    5TCP的费用在性能方面的提高并不显著。
    5)异步写入特性。
    6)改进了服务器的mount性能。
    7)有更好的I/O写性能。
    8)更强的网络运行效能,使得网络运行更为有效。
    9)更强的灾难恢复功能。

    Linux上,UDP是默认使用的协议。作为服务器别无选择。但作为客户端,可以使用TCP和其他使用TCPUNIX NFS服务器互联。在局域网中使用UDP较好,因为局域网有比较稳定的网络保证。使用UDP可以带来更好的性能,Linux默认使用V2,但是也可以通过mount optionnfsvers=n选择。NFS使用TCP/IP提供的协议和服务运行于OSI层次模型的应用层,如表1所示。


    1  OSI层次模型上的NFS

     

    层    数

    名    称

    功    能

    1

    应用层

    NFS

    2

    表示层

    XDR

    3

    会话层

    RPC

    4

    传输层

    UDPTCP

    5

    网络层

    IP

    6

    数据链路层

     

    7

    物理层

    Ethernet

    五、Glusterfs实现NFS服务器

    第一节、启动过程分析

    Glusterfsnfs服务器启动命令如下:

     /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模式(有三种模式:(1glusterfsd;(2glusterfs;(3glusterd)运行。

    另外一个函数就是初始化具体的卷服务函数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_filecmd_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;
    
    }

    到此为止就可以看到真正调用NFSinit函数了,这个时候才真正开始执行与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-volumesnfs.enable-ino32nfs.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;//权限使能
    
    ......
    
    }

    这里执行的初始化函数是在上面初始化的,有两类权限的初始化函数,相关内容定义如下:

    1auth-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-unix
    
    rpcsvc_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分别来为mnt3mnt1nfs3版本的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;//属于哪一个xlator
    
      INIT_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) {//遍历所有的子xlator
    
         gf_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,即设置为0
    
            if (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) {//如果不为null
    
                    ret = uuid_parse (optstr, volumeid);//根据得到的值解析uuid
    
            }
    
    no_dvm:
    
            ret = snprintf (searchstr, 1024, "nfs3.%s.export-dir", xlator->name);//export-dir选项key
    
            if (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表设置rootinode节点信息:
    __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表的初始化完成根节点创建和属性的赋值,下面主要看看两个结构体定义和两个函数的实现,先看看inodeiatt两个结构体的定义,他们的定义中包含很多重要的信息,他们的定义如下:
    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++;//最近使用链表的数量加1
    
    out:
    
     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)//属性值不能为null
    
            return NULL;
    
     if (uuid_is_null (iatt->ia_gfid))//uuid不能为null
    
               return 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) {//父节点不为null
    
        old_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;//执行属于哪一个xlator
    
           version->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);//链接对象引用加1
    
          nfs_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);//链接状态对象的引用加1
    
           ret = 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);//从内存池中得到一个权限请求对象并且初始化为0
    
    msgbuf = 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

     

     

    NFS Protocol Family

    The NFS protocol suite includes the following protocols:

    MNTV1

    Mount protocol version 1, for NFS version 2

    Mntv3

    Mount protocol version 3, for NFS version 3

    NFS2

    Sun Network File system version 2

    NFS3

    Sun Network File system version 3

    NFSv4

    Sun Network File system version 4

    NLMv4

    Network Lock Manager version 4

    NSMv1

    Network Status Monitor protocol

    MNTV1ftp://ftp.rfc-editor.org/in-notes/rfc1094.txt.
        The Mount protocol version 1 for NFS version 2 (MNTv1) is separate from, but related to, the NFS protocol. It provides operating system specific services to get the NFS off the ground -- looks up server path names, validates user identity, and checks access permissions. Clients use the Mount protocol to get the first file handle, which allows them entry into a remote filesystem.
    The Mount protocol is kept separate from the NFS protocol to make it easy to plug in new access checking and validation methods without changing the NFS server protocol.
        Notice that the protocol definition implies stateful servers because the server maintains a list of client's mount requests. The Mount list information is not critical for the correct functioning of either the client or the server. It is intended for advisory use only, for example, to warn possible clients when a server is going down.
        Version one of the Mount protocol is used with version two of the NFS protocol. The only information communicated between these two protocols is the "fhandle" structure. The header structure is as follows:

    8

    7

    6

    5

    4

    3

    2

    1

    Octets

    Directory Path Length

    1

    2

    3

    4

    Directory Path Name

    5-N

    Directory Path LengthThe directory path length.
    Directory Path NameThe directory path name. 

    Mntv3ftp://ftp.rfc-editor.org/in-notes/rfc1813.txt.
        The supporting Mount protocol version 3 for NFS version 3 protocol performs the operating system-specific functions that allow clients to attach remote directory trees to a point within the local file system. The Mount process also allows the server to grant remote access privileges to a restricted set of clients via export control.
        The Lock Manager provides support for file locking when used in the NFS environment. The Network Lock Manager (NLM) protocol isolates the inherently stateful aspects of file locking into a separate protocol. A complete description of the above protocols and their implementation is to be found in [X/OpenNFS].
        The normative text is the description of the RPC procedures and arguments and results, which defines the over-the-wire protocol, and the semantics of those procedures. The material describing implementation practice aids the understanding of the protocol specification and describes some possible implementation issues and solutions. It is not possible to describe all implementations and the UNIX operating system implementation of the NFS version 3 protocol is most often used to provide examples. The structure of the protocol is as follows.

    8

    7

    6

    5

    4

    3

    2

    1

    Octets

    Directory Path Length

    1

    2

    3

    4

    Directory Path Name

    5-N

    Directory path lengthThe directory path length.
    Directory Path NameThe directory path name

    NFS2ftp://ftp.rfc-editor.org/in-notes/rfc1094.txt.
        The Sun Network File system (NFS version 2) protocol provides transparent remote access to shared files across networks. The NFS protocol is designed to be portable across different machines, operating systems, network architectures, and transport protocols. This portability is achieved through the use of Remote Procedure Call (RPC) primitives built on top of an eXternal Data Representation (XDR). Implementations already exist for a variety of machines, from personal computers to supercomputers.
        The supporting Mount protocol allows the server to hand out remote access privileges to a restricted set of clients. It performs the operating system-specific functions that allow, for example, to attach remote directory trees to some local file systems. The protocol header is as follows:

    8

    7

    6

    5

    4

    3

    2

    1

    Octets

    File info/Directory info

    1

    .

    .

    .

    .

    .

    N

    File info/Directory infoThe File info or directory info.

    NFS3ftp://ftp.rfc-editor.org/in-notes/rfc1813.txt.
        Version 3 of the NFS protocol addresses new requirements, for instance; the need to support larger files and file systems has prompted extensions to allow 64 bit file sizes and offsets. The revision enhances security by adding support for an access check to be done on the server. Performance modifications are of three types:

    1 The number of over-the-wire packets for a given set of file operations is reduced by returning file attributes on every operation, thus decreasing the number of calls to get modified attributes.

    2 The write throughput bottleneck caused by the synchronous definition of write in the NFS version 2 protocol has been addressed by adding support so that the NFS server can do unsafe writes. Unsafe writes are writes which have not been committed to stable storage before the operation returns.

    3 Limitations on transfer sizes have been relaxed.

        The ability to support multiple versions of a protocol in RPC will allow implementors of the NFS version 3 protocol to define clients and servers that provide backward compatibility with the existing installed base of NFS version 2 protocol implementations.
        The extensions described here represent an evolution of the existing NFS protocol and most of the design features of the NFS protocol previsouly persist. The protocol header structure is as follows:

    8

    7

    6

    5

    4

    3

    2

    1

    Octets

    Object info/ File info/ Directory info Length

    1

    2

    3

    4

    Object info/ File info/ Directory info Name

    5-N

    Object info/ File info/ Directory info LengthThe information length in octets
    Object info/ File info/ Directory info NameThe information value (string).

    NFSv4ftp://ftp.rfc-editor.org/in-notes/rfc3010.txt
        NFS (Network File System) version 4 is a distributed file system protocol based on NFS protocol versions 2 [RFC1094] and 3 [RFC1813]. Unlike earlier versions, the NFS version 4 protocol supports traditional file access while integrating support for file locking and the mount protocol. In addition, support for strong security (and its negotiation), compound operations, client caching, and internationalization have been added. Attention has also been applied to making NFS version 4 operate well in an Internet environment.
        The goals of the NFS version 4 revision are as follows:

    · Improved access and good performance on the Internet.

    · Strong security with negotiation built into the protocol.

    · Good cross-platform interoperability.

    · Designed for protocol extensions.

        The general file system model used for the NFS version 4 protocol is the same as previous versions. The server file system is hierarchical with the regular files contained within being treated as opaque byte streams. In a slight departure, file and directory names are encoded with UTF-8 to deal with the basics of internationalization.
        A separate protocol to provide for the initial mapping between path name and filehandle is no longer required. Instead of using the older MOUNT protocol for this mapping, theserver provides a ROOT filehandle that represents the logical root or top of the file system tree provided by the server. 
        The protocol header is as follows:

    8

    7

    6

    5

    4

    3

    2

    1

    Octets

    Tag Length

    1-4 

    Tag (depends on Tag length)

    5-N

    Minor Version

    N+1-N+4

    Operation Argument

    N+5-N+8

    Tag LengthThe length in bytes of the tag
    TagDefined by the implementor
    Minor VersionEach minor version number will correspond to an RFC. Minor version zero corresponds to NFSv4
    Operation ArgumentOperation to be executed by the protocol
    Operaton Argument ValuesThe operation arg value, can be one of the following:

    Value

    Name

    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38

    ACCESS
    CLOSE
    COMMIT 
    CREATE 
    DELEGPURGE 
    DELEGRETURN 
    GETATTR 
    GETFH 
    LINK 
    LOCK 
    LOCKT 
    LOCKU 
    LOOKUP 
    LOOKUPP 
    NVERIFY 
    OPEN 
    OPENATTR 
    OPEN_CONFIRM 
    OPEN_DOWNGRADE 
    PUTFH 
    PUTPUBFH 
    PUTROOTFH 
    READ 
    READDIR 
    READLINK 
    REMOVE 
    RENAME 
    RENEW 
    RESTOREFH 
    SAVEFH 
    SECINFO 
    SETATTR 
    SETCLIENTID 
    SETCLIENTID_CONFIRM 
    VERIFY 
    WRITE

    NLMv4ftp://ftp.rfc-editor.org/in-notes/rfc1813.txt.
        Since the NFS versions 2 and 3 are stateless, an additional Network Lock Manager (NLM) protocol is required to support locking of NFS-mounted files. As a result of the changes in version 3 of the NFS protocol version 4 of the NLM protocol is required.
        In this version 4, almost all the names in the NLM version 4 protocol have been changed to include a version number. The procedures in the NLM version 4 protocol are semantically the same as those in the NLM version 3 protocol. The only semantic difference is the addition of a NULL procedure that can be used to test for server responsiveness.
    The structure of the NLMv4 heading is as follows:

    8

    7

    6

    5

    4

    3

    2

    1

    Octet

    Cookie Length

    1

    2

    3

    4

    Cookie

    5-N

    Cookie LengthThe cookie length.
    CookieThe cookie string itself. 

    NSMv1http://www.opengroup.org/onlinepubs/009629799/chap11.htm.
        The Network Status Monitor (NSM) protocol is related to, but separate from, the Network Lock Manager (NLM) protocol.The NLM uses the NSM (Network Status Monitor Protocol V1) to enable it to recover from crashes of either the client or server host. To do this, the NSM and NLM protocols on both the client and server hosts must cooperate. 
        The NSM is a service that provides applications with information on the status of network hosts. Each NSM keeps track of its own "state" and notifies any interested party of a change in this state to any other NSM upon request. The state is merely a number which increases monotonically each time the state of the host changes; an even number indicates the host is down, while an odd number indicates the host is up. 
        Applications register the network hosts they are interested in with the local NSM. If one of these hosts crashes, the NSM on the crashed host, after a reboot, will notify the NSM on the local host that the state changed. The local NSM can then, in turn, notify the interested application of this state change. 
        The NSM is used heavily by the Network Lock Manager (NLM). The local NLM registers with the local NSM all server hosts on which the NLM has currently active locks. In parallel, the NLM on the remote (server) host registers all of its client hosts with its local NSM. If the server host crashes and reboots, the server NSM will inform the NSM on the client hosts of this event. The local NLM can then take steps to re-establish the locks when the server is rebooted. Low-end systems that do not run an NSM, due to memory or speed constraints, are restricted to using non-monitored locks.
    The structure of the protocol is as follows:

    8

    7

    6

    5

    4

    3

    2

    1

    Octet

    Name Length

    1

    2

    3

    4

    Mon Name /Host Name

    5-N

    Name Length The mon name or host name length.
    Mon Name   The name of the host to be monitored by the NSM.
    Host Name   The host name.


    
    
    
    
    展开全文
  • NFS 协议

    2008-10-23 10:09:00
     网络文件系统是FreeBSD支持的文件系统中的一种,也被称为NFS. NFS允许一个系统在网络上与它人共享目录和文件。通过使用NFS,用户和程序可以象访问本地文件一样访问远端系统上的文件。 以下是NFS最显而易见的好处...
    NFS是Net File System的简写,即网络文件系统. 

    网络文件系统是FreeBSD支持的文件系统中的一种,也被称为NFS. NFS允许一个系统在网络上与它人共享目录和文件。通过使用NFS,用户和程序可以象访问本地文件一样访问远端系统上的文件。 

    以下是NFS最显而易见的好处: 

    1.本地工作站使用更少的磁盘空间,因为通常的数据可以存放在一台机器上而且可以通过网络访问到。 

    2.用户不必在每个网络上机器里头都有一个home目录。Home目录 可以被放在NFS服务器上并且在网络上处处可用。 

    3.诸如软驱,CDROM,和 Zip® 之类的存储设备可以在网络上面被别的机器使用。这可以减少整个网络上的可移动介质设备的数量。 

    NFS至少有两个主要部分:一台服务器和一台(或者更多)客户机。客户机远程访问存放在服务器上的数据。为了正常工作,一些进程需要被配置并运行。 

    NFS 有很多实际应用。下面是比较常见的一些: 

    1.多个机器共享一台CDROM或者其他设备。这对于在多台机器中安装软件来说更加便宜跟方便。 

    2.在大型网络中,配置一台中心 NFS 服务器用来放置所有用户的home目录可能会带来便利。这些目录能被输出到网络以便用户不管在哪台工作站上登录,总能得到相同的home目录。 

    3.几台机器可以有通用的/usr/ports/distfiles 目录。这样的话,当您需要在几台机器上安装port时,您可以无需在每台设备上下载而快速访问源码。 

    注:NFS 由Sun microsystems 公司开发。是一种网络操作系统 
    是使用底层传输层协议TCP/IP的应用层协议
    展开全文
  • Sun Microsystems公司于1984年推出了一个在整个计算机工业中被广泛接受的远程文件存取机制,它被称为Sun的网络文件系统(Network File System),或者简称为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协议的。XDReXternal Data Representation,即外部数据表示法)提供一种方法,把数据从一种格式转换成另一种标准数据格式表示法,确保在不同的计算机、操作系统及程序语言中,所有数据代表的意义都是相同的。
    RPCRemote Procedure Call,远程程序调用)请求远程计算机给予服务。客户机通过网络传送RPC到远程计算机,请求服务。
    NFS运用RPC传送数据的方法有以下几步:
    1)客户送出信息,请求服务。
    2)客户占位程序把客户送出的参数转换成XDR标准格式,并用系统调用把信息送到网络上。
    3)信息经过网络送达远程主机系统。
    4)远程主机将接受到的信息传给服务器占位程序。
    5)把XDR形式的数据,转换成符合主机端的格式,取出客户发出的服务请求参数,送给服务器。
    6)服务器给客户发送服务的逆向传送过程。

    第二节、服务进程的作用

    服务进程是系统在启动计算机后自动运行的程序,包括对网络的连接、网络协议的加载、图形桌面的显示、文件系统的加载等,Linux系统中常见的进程包括以下几种。
    (1nfsd
    根据客户端对文件系统的需求,启动文件系统请求服务进程,响应客户的请求,而一般文件系统请求服务进程的数目是8,这也是在rc.local中写nfsd 8 &的原因。
    (2biod
    此进程是在NFS客户端上用的,用来启动异步块I/O服务进程来建立Buffer Cache,处理在客户机上的读写。(3mountd
    这是个RPC服务器。启动rpc.mountd服务进程后,mountd会读取/etc/xtab查看哪一台客户机正在挂载哪一个文件系统,并回应客户机所要挂载的路径。
    (4inetd Internet services服务进程
    当系统启动时,rc.local会启动inetd读取inetd.conf配置文件,读取网络上所有服务器的地址,链接启动inetd.conf中所有的服务器。当客户机请求服务时,inetd就会启动相关的服务进程,如user使用telnet时,inetd启动telnetd配合user telnet的需求,其余像ftpfingerrlogin等应用程序,inetd也都会启动相对应的服务程序ftpdfingerdrloingd等。
    (5portmap服务程序
    主要功能是将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)客户端会向服务器端的RPCport 111)发出NFS文件存取功能的询问请求。
    2)服务器端找到对应的已注册的NFS daemon端口后会回报给客户端。
    3)客户端了解正确的端口后,就可以直接与NFS守护进程来联机。
    由于NFS的各项功能都必须要向RPC注册,因此RPC才能了解NFS服务的各项功能的port numberPIDNFS在主机所监听的IP等,而客户端才能够通过RPC的询问找到正确对应的端口。即NFS必须要有RPC存在时才能成功地提供服务,因此我们称NFSRPC Server的一种。事实上,有很多这样的服务器都向RPC注册。例如,NISNetwork Information Service)也是RPC Server的一种。所以如下图所示,不论是客户端还是服务器端,要使用NFS都需要启动RPC

     


     

    2 NFS RPC 服务及操作系统的相关性

     

     

    NFS协议从诞生到现在为止,已经有多个版本,如NFS V2rfc794)及NFS V3rfc1813)(最新的版本是V4rfc307))。最早,SUN公司曾将NFS V2设计为只使用UDP,主要原因是当时机器的内存、网络速度和CPU的影响,不得不选择对机器负担较轻的方式。而到了NFS V3SUN公司选择了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内核提供了对通过TCPNFS的支持。要通过TCP来使用NFS,在客户端系统上挂载NFS导出的文件系统时包括一个“-o tcp”选项。使用TCP的优点和缺点如下:
    1)被提高了的连接持久性,因此获得的NFS stale file handles消息就会较少。
    2)载量较大的网络的性能会有所提高,因为TCP确认每个分组,而UDP只在完成时才确认。
    3TCP具有拥塞控制技术(UDP根本没有),在一个拥塞情况严重的网络上,UDP分组是被首先撤销的类型。使用UDP意味着,如果NFS正在写入数据(单元为8 KB的块),所有这8 KB数据都需要被重新传输。由于TCP的可靠性,8 KB数据中只有一部分需要重新传输。
    4)错误检测。当TCP连接中断(由于服务器停止),客户端就会停止发送数据而开始重新连接。UDP是无连接的,使用它的客户端就会继续给网络发送数据直到服务器重新上线为止。
    5TCP的费用在性能方面的提高并不显著。
    5)异步写入特性。
    6)改进了服务器的mount性能。
    7)有更好的I/O写性能。
    8)更强的网络运行效能,使得网络运行更为有效。
    9)更强的灾难恢复功能。

    Linux上,UDP是默认使用的协议。作为服务器别无选择。但作为客户端,可以使用TCP和其他使用TCPUNIX NFS服务器互联。在局域网中使用UDP较好,因为局域网有比较稳定的网络保证。使用UDP可以带来更好的性能,Linux默认使用V2,但是也可以通过mount optionnfsvers=n选择。NFS使用TCP/IP提供的协议和服务运行于OSI层次模型的应用层,如表1所示。


    1  OSI层次模型上的NFS

     

    层    数

    名    称

    功    能

    1

    应用层

    NFS

    2

    表示层

    XDR

    3

    会话层

    RPC

    4

    传输层

    UDPTCP

    5

    网络层

    IP

    6

    数据链路层

     

    7

    物理层

    Ethernet

    五、Glusterfs实现NFS服务器

    第一节、启动过程分析

    Glusterfsnfs服务器启动命令如下:

     /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模式(有三种模式:(1glusterfsd;(2glusterfs;(3glusterd)运行。

    另外一个函数就是初始化具体的卷服务函数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_filecmd_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;
    
    }

    到此为止就可以看到真正调用NFSinit函数了,这个时候才真正开始执行与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-volumesnfs.enable-ino32nfs.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;//权限使能
    
    ......
    
    }

    这里执行的初始化函数是在上面初始化的,有两类权限的初始化函数,相关内容定义如下:

    1auth-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-unix
    
    rpcsvc_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分别来为mnt3mnt1nfs3版本的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;//属于哪一个xlator
    
      INIT_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) {//遍历所有的子xlator
    
         gf_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,即设置为0
    
            if (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) {//如果不为null
    
                    ret = uuid_parse (optstr, volumeid);//根据得到的值解析uuid
    
            }
    
    no_dvm:
    
            ret = snprintf (searchstr, 1024, "nfs3.%s.export-dir", xlator->name);//export-dir选项key
    
            if (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表设置rootinode节点信息:
    __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表的初始化完成根节点创建和属性的赋值,下面主要看看两个结构体定义和两个函数的实现,先看看inodeiatt两个结构体的定义,他们的定义中包含很多重要的信息,他们的定义如下:
    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++;//最近使用链表的数量加1
    
    out:
    
     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)//属性值不能为null
    
            return NULL;
    
     if (uuid_is_null (iatt->ia_gfid))//uuid不能为null
    
               return 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) {//父节点不为null
    
        old_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;//执行属于哪一个xlator
    
           version->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);//链接对象引用加1
    
          nfs_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);//链接状态对象的引用加1
    
           ret = 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);//从内存池中得到一个权限请求对象并且初始化为0
    
    msgbuf = 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

     

     

    NFS Protocol Family

    The NFS protocol suite includes the following protocols:

    MNTV1

    Mount protocol version 1, for NFS version 2

    Mntv3

    Mount protocol version 3, for NFS version 3

    NFS2

    Sun Network File system version 2

    NFS3

    Sun Network File system version 3

    NFSv4

    Sun Network File system version 4

    NLMv4

    Network Lock Manager version 4

    NSMv1

    Network Status Monitor protocol

    MNTV1ftp://ftp.rfc-editor.org/in-notes/rfc1094.txt.
        The Mount protocol version 1 for NFS version 2 (MNTv1) is separate from, but related to, the NFS protocol. It provides operating system specific services to get the NFS off the ground -- looks up server path names, validates user identity, and checks access permissions. Clients use the Mount protocol to get the first file handle, which allows them entry into a remote filesystem.
    The Mount protocol is kept separate from the NFS protocol to make it easy to plug in new access checking and validation methods without changing the NFS server protocol.
        Notice that the protocol definition implies stateful servers because the server maintains a list of client's mount requests. The Mount list information is not critical for the correct functioning of either the client or the server. It is intended for advisory use only, for example, to warn possible clients when a server is going down.
        Version one of the Mount protocol is used with version two of the NFS protocol. The only information communicated between these two protocols is the "fhandle" structure. The header structure is as follows:

    8

    7

    6

    5

    4

    3

    2

    1

    Octets

    Directory Path Length

    1

    2

    3

    4

    Directory Path Name

    5-N

    Directory Path LengthThe directory path length.
    Directory Path NameThe directory path name. 

    Mntv3ftp://ftp.rfc-editor.org/in-notes/rfc1813.txt.
        The supporting Mount protocol version 3 for NFS version 3 protocol performs the operating system-specific functions that allow clients to attach remote directory trees to a point within the local file system. The Mount process also allows the server to grant remote access privileges to a restricted set of clients via export control.
        The Lock Manager provides support for file locking when used in the NFS environment. The Network Lock Manager (NLM) protocol isolates the inherently stateful aspects of file locking into a separate protocol. A complete description of the above protocols and their implementation is to be found in [X/OpenNFS].
        The normative text is the description of the RPC procedures and arguments and results, which defines the over-the-wire protocol, and the semantics of those procedures. The material describing implementation practice aids the understanding of the protocol specification and describes some possible implementation issues and solutions. It is not possible to describe all implementations and the UNIX operating system implementation of the NFS version 3 protocol is most often used to provide examples. The structure of the protocol is as follows.

    8

    7

    6

    5

    4

    3

    2

    1

    Octets

    Directory Path Length

    1

    2

    3

    4

    Directory Path Name

    5-N

    Directory path lengthThe directory path length.
    Directory Path NameThe directory path name

    NFS2ftp://ftp.rfc-editor.org/in-notes/rfc1094.txt.
        The Sun Network File system (NFS version 2) protocol provides transparent remote access to shared files across networks. The NFS protocol is designed to be portable across different machines, operating systems, network architectures, and transport protocols. This portability is achieved through the use of Remote Procedure Call (RPC) primitives built on top of an eXternal Data Representation (XDR). Implementations already exist for a variety of machines, from personal computers to supercomputers.
        The supporting Mount protocol allows the server to hand out remote access privileges to a restricted set of clients. It performs the operating system-specific functions that allow, for example, to attach remote directory trees to some local file systems. The protocol header is as follows:

    8

    7

    6

    5

    4

    3

    2

    1

    Octets

    File info/Directory info

    1

    .

    .

    .

    .

    .

    N

    File info/Directory infoThe File info or directory info.

    NFS3ftp://ftp.rfc-editor.org/in-notes/rfc1813.txt.
        Version 3 of the NFS protocol addresses new requirements, for instance; the need to support larger files and file systems has prompted extensions to allow 64 bit file sizes and offsets. The revision enhances security by adding support for an access check to be done on the server. Performance modifications are of three types:

    1 The number of over-the-wire packets for a given set of file operations is reduced by returning file attributes on every operation, thus decreasing the number of calls to get modified attributes.

    2 The write throughput bottleneck caused by the synchronous definition of write in the NFS version 2 protocol has been addressed by adding support so that the NFS server can do unsafe writes. Unsafe writes are writes which have not been committed to stable storage before the operation returns.

    3 Limitations on transfer sizes have been relaxed.

        The ability to support multiple versions of a protocol in RPC will allow implementors of the NFS version 3 protocol to define clients and servers that provide backward compatibility with the existing installed base of NFS version 2 protocol implementations.
        The extensions described here represent an evolution of the existing NFS protocol and most of the design features of the NFS protocol previsouly persist. The protocol header structure is as follows:

    8

    7

    6

    5

    4

    3

    2

    1

    Octets

    Object info/ File info/ Directory info Length

    1

    2

    3

    4

    Object info/ File info/ Directory info Name

    5-N

    Object info/ File info/ Directory info LengthThe information length in octets
    Object info/ File info/ Directory info NameThe information value (string).

    NFSv4ftp://ftp.rfc-editor.org/in-notes/rfc3010.txt
        NFS (Network File System) version 4 is a distributed file system protocol based on NFS protocol versions 2 [RFC1094] and 3 [RFC1813]. Unlike earlier versions, the NFS version 4 protocol supports traditional file access while integrating support for file locking and the mount protocol. In addition, support for strong security (and its negotiation), compound operations, client caching, and internationalization have been added. Attention has also been applied to making NFS version 4 operate well in an Internet environment.
        The goals of the NFS version 4 revision are as follows:

    · Improved access and good performance on the Internet.

    · Strong security with negotiation built into the protocol.

    · Good cross-platform interoperability.

    · Designed for protocol extensions.

        The general file system model used for the NFS version 4 protocol is the same as previous versions. The server file system is hierarchical with the regular files contained within being treated as opaque byte streams. In a slight departure, file and directory names are encoded with UTF-8 to deal with the basics of internationalization.
        A separate protocol to provide for the initial mapping between path name and filehandle is no longer required. Instead of using the older MOUNT protocol for this mapping, theserver provides a ROOT filehandle that represents the logical root or top of the file system tree provided by the server. 
        The protocol header is as follows:

    8

    7

    6

    5

    4

    3

    2

    1

    Octets

    Tag Length

    1-4 

    Tag (depends on Tag length)

    5-N

    Minor Version

    N+1-N+4

    Operation Argument

    N+5-N+8

    Tag LengthThe length in bytes of the tag
    TagDefined by the implementor
    Minor VersionEach minor version number will correspond to an RFC. Minor version zero corresponds to NFSv4
    Operation ArgumentOperation to be executed by the protocol
    Operaton Argument ValuesThe operation arg value, can be one of the following:

    Value

    Name

    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38

    ACCESS
    CLOSE
    COMMIT 
    CREATE 
    DELEGPURGE 
    DELEGRETURN 
    GETATTR 
    GETFH 
    LINK 
    LOCK 
    LOCKT 
    LOCKU 
    LOOKUP 
    LOOKUPP 
    NVERIFY 
    OPEN 
    OPENATTR 
    OPEN_CONFIRM 
    OPEN_DOWNGRADE 
    PUTFH 
    PUTPUBFH 
    PUTROOTFH 
    READ 
    READDIR 
    READLINK 
    REMOVE 
    RENAME 
    RENEW 
    RESTOREFH 
    SAVEFH 
    SECINFO 
    SETATTR 
    SETCLIENTID 
    SETCLIENTID_CONFIRM 
    VERIFY 
    WRITE

    NLMv4ftp://ftp.rfc-editor.org/in-notes/rfc1813.txt.
        Since the NFS versions 2 and 3 are stateless, an additional Network Lock Manager (NLM) protocol is required to support locking of NFS-mounted files. As a result of the changes in version 3 of the NFS protocol version 4 of the NLM protocol is required.
        In this version 4, almost all the names in the NLM version 4 protocol have been changed to include a version number. The procedures in the NLM version 4 protocol are semantically the same as those in the NLM version 3 protocol. The only semantic difference is the addition of a NULL procedure that can be used to test for server responsiveness.
    The structure of the NLMv4 heading is as follows:

    8

    7

    6

    5

    4

    3

    2

    1

    Octet

    Cookie Length

    1

    2

    3

    4

    Cookie

    5-N

    Cookie LengthThe cookie length.
    CookieThe cookie string itself. 

    NSMv1http://www.opengroup.org/onlinepubs/009629799/chap11.htm.
        The Network Status Monitor (NSM) protocol is related to, but separate from, the Network Lock Manager (NLM) protocol.The NLM uses the NSM (Network Status Monitor Protocol V1) to enable it to recover from crashes of either the client or server host. To do this, the NSM and NLM protocols on both the client and server hosts must cooperate. 
        The NSM is a service that provides applications with information on the status of network hosts. Each NSM keeps track of its own "state" and notifies any interested party of a change in this state to any other NSM upon request. The state is merely a number which increases monotonically each time the state of the host changes; an even number indicates the host is down, while an odd number indicates the host is up. 
        Applications register the network hosts they are interested in with the local NSM. If one of these hosts crashes, the NSM on the crashed host, after a reboot, will notify the NSM on the local host that the state changed. The local NSM can then, in turn, notify the interested application of this state change. 
        The NSM is used heavily by the Network Lock Manager (NLM). The local NLM registers with the local NSM all server hosts on which the NLM has currently active locks. In parallel, the NLM on the remote (server) host registers all of its client hosts with its local NSM. If the server host crashes and reboots, the server NSM will inform the NSM on the client hosts of this event. The local NLM can then take steps to re-establish the locks when the server is rebooted. Low-end systems that do not run an NSM, due to memory or speed constraints, are restricted to using non-monitored locks.
    The structure of the protocol is as follows:

    8

    7

    6

    5

    4

    3

    2

    1

    Octet

    Name Length

    1

    2

    3

    4

    Mon Name /Host Name

    5-N

    Name Length The mon name or host name length.
    Mon Name   The name of the host to be monitored by the NSM.
    Host Name   The host name.


    展开全文
  • 什么是NFS协议

    千次阅读 2009-12-03 20:23:00
    网络文件系统是FreeBSD支持的文件系统中的一种,也被称为NFS. NFS允许一个系统在网络上与它人共享目录和文件。通过使用NFS,用户和程序可以象访问本地文件一样访问远端系统上的文件。 以下是NFS最显而易见的好处: ...
  • 那么这个NFS协议是怎么实现的呢?  网络文件系统实现的核心是使用了RPC(Remote Procedure Call Protocol),也就是使用了远程过程调用协议。在说明NFS协议之前必须先搞清楚RPC。 传统模式下都是本地编写程序,编译...
  • NFS

    2019-11-11 20:07:55
    NFS(Network FIle System,网络文件系统)是一种分布式文件系统,允许网络中不同操作系统的计算机间共享文件,其协议基于TCP/IP协议层,可以将远端的计算机磁盘挂载至本地目录,读写文件与本地磁盘一样操作 ...
  • Sun Microsystems公司于1984年推出了一个在整个计算机工业中被广泛接受的远程文件存取机制,它被称为Sun的网络文件系统(Network File System),或者简称为NFS。该机制允许在一台计算机上运行一个服务器,使
  • 协议 物理 通过媒介传输比特,确定机械及电气规范(比特Bit) 各种传输媒体(光线、网线),各类DTE和DCE之间通讯的物理设备(如:计算机、HUB),各类插槽、插座。 RJ45、CLOCK、IEEE802.3 数据链路 将...
  • TCP/IP层次模型共分为四:应用、传输、网络、数据链 路。 TCP/IP网络协议 TCP/IP(Transmission Control Protocol/Internet Protocol,传输控制协议/ 网间网协议)是目前世界上应用最为广泛的协议,它的流行...
  • 学习运维——网络文件协议-CIFS与NFS

    千次阅读 2017-03-20 15:52:43
    网络文件协议 前言: 当今最主要的两大网络文件系统是Sun提出的NFS(Network File System)以及由微软、EMC和NetApp提出的CIFS(Common Internet File System),前者主要用于各种Unix平台,后者则主要用于...
  • TCP/IP层次模型共分为四:应用、传输、网络、数据链  路。  TCP/IP网络协议  TCP/IP(Transmission Control Protocol/Internet Protocol,传输控制协议/  网间网协议)是目前...
  •  NFS协议,客户端可以透明地访问服务器中的文件系统。 NFS只访问一个进程引用文件部分 ,并且一个目的就是使得这种访问透明。这就意味着任何能够访问一个本地文件的客户端程序不需要做任何修改,就应该能够访问一个...
  • NFS服务

    2019-05-26 15:55:20
    NFS概念 网络文件系统(NFS)是Unix系统和...NFS协议由多个版本:Linux支持版本4,版本3和版本2,而大多数系统管理员熟悉的是NFSv3.默认情况下,该协议并不安全,但是更新的版本(如NFSv4)提供了对更安全的身分验...
  • 7是指OSI七层协议模型,主要是:应用(Application)、表示(Presentation)、会话(Session)、传输(Transport)、网络(Network)、数据链路(Data Link)、物理(Physical)。  OSI是Open ...
  • Atitit 远程存储与协议 mtp ptp rndis midi nfs smb webdav ftp Atitit mtp ptp rndis midi协议的不同区别 1. PTP: 图片传输协议的 缩写,全称为:picture transfer protocol; 1 2. Atitit 分布式文件...
  • NFS笔记

    2020-03-21 17:21:10
    NFS详解什么是NFS服务器工作原理VFS简单介绍vfs工作逻辑NFS辅助功能 什么是NFS服务器 NFS(Network File System)即网络文件系统,它允许网络中的计算机之间共享资源。在NFS的应用中,本地NFS的客户端应用可以透明地...
  • 应用常用协议

    万次阅读 2016-07-29 20:44:35
    应用常用协议