预计阅读本页时间:-
10.6.4 NFS:网络文件系统
网络在Linux中起着重要作用,在UNIX中也是如此——自从网络出现开始(第一个UNIX网络是为了将新的内核从PDP-11/70转移到Interdata 8/32上而建立的)。本节将考察Sun Microsystem的NFS(网络文件系统)。该文件系统应用于所有的现代Linux系统中,其作用是将不同计算机上的不同文件系统连接成一个逻辑整体。当前主流的NFS实现是1994年提出的第3版。NFS第4版在2000年提出,并在前一个NFS体系结构上做了一些增强。NFS有三个方面值得关注:体系结构、协议和实现。我们现在将依次考察这三个方面,首先是简化的NFS第3版,然后简要探讨第4版所做的增强。
1.NFS体系结构
NFS背后的基本思想是允许任意选定的一些客户端和服务器共享一个公共文件系统。在很多情况下,所有的客户端和服务器都在同一个局域网中,但这并不是必需的。如果服务器距离客户端很远,NFS也可以在广域网上运行。简单起见,我们还是说客户端和服务器,就好像它们位于不同的机器上,但实际上,NFS允许一台机器同时既是客户端又是服务器。
广告:个人专属 VPN,独立 IP,无限流量,多机房切换,还可以屏蔽广告和恶意软件,每月最低仅 5 美元
每一个NFS服务器都导出一个或多个目录供远程客户端访问。当一个目录可用时,它的所有子目录也都可用,因而事实上,整个目录树通常作为一个单元导出。服务器导出的目录列表用一个文件来维护,通常是/etc/exports。因此服务器启动后这些目录可以被自动地导出。客户端通过挂载这些导出的目录来访问它们。当一个客户端挂载了一个(远程)目录,该目录就成为客户端目录层次的一部分,如图10-35所示。

在这个例子中,客户端1将服务器1的bin目录挂载到客户端1自己的bin目录。因此它现在可以用/bin/sh引用shell并获得服务器的shell。无磁盘工作站通常只有一个框架文件系统(在RAM中),它从远程服务器中得到所有的文件,就像上例中一样。类似地,客户端1将服务器2中的/projects目录挂载到自己的/usr/ast/work目录,因此它用usr/ast/work/proj1/a就可以访问文件a。最后,客户端2也挂载了projects目录,它可以用/mnt/proj1/a访问文件a。从这里可以看到,由于不同的客户端将文件挂载到各自目录树中不同的位置,同一个文件在不同的客户端有不同的名字。对客户端来说挂载点是完全局部的,服务器不会知道文件在任何一个客户端中的挂载点。
2.NFS协议
由于NFS的目标之一是支持异构系统,客户端和服务器可能在不同硬件上运行不同操作系统,因此对客户端和服务器之间的接口给予明确定义是很关键的。只有这样,才有可能让任何一个新的客户端能够跟现有的服务器一起正确工作,反之亦然。
NFS通过定义两个客户端-服务器协议来实现这一目标。一个协议就是从客户端发送到服务器的一组请求以及从服务器返回给客户端的响应的集合。
第一个NFS协议处理挂载。客户端可以向服务器发送路径名,请求服务器许可将该目录挂载到自己的目录层次的某个地方。由于服务器并不关心目录将被挂载到何处,因此请求消息中并不包含挂载地址。如果路径名是合法的并且该目录已被导出,那么服务器向客户端返回一个文件句柄。这个文件句柄中的域惟一地标识了文件系统类型、磁盘、目录的i节点号以及安全信息等。随后对已挂载目录及其子目录中文件的读写都使用该文件句柄。
Linux启动时会在进入多用户之前运行shell脚本/etc/rc。可以将挂载远程文件系统的命令写入该脚本中,这样就可以在允许用户登录之前自动挂载必要的远程文件系统。此外,大部分Linux版本也支持自动挂载。这个特性允许一组远程目录跟一个本地目录相关联。当客户端启动时,并不挂载这些远程目录(甚至不与它们所在的服务器进行联络)。相反,在第一次打开远程文件时,操作系统向每个服务器发送一条信息。第一个响应的服务器胜出,其目录被挂载。
相对于通过/etc/rc文件进行静态挂载,自动挂载具有两个主要优势。第一,如果/etc/rc中列出的某个NFS服务器出了故障,那么客户端将无法启动,或者至少会带来一些困难、延迟以及很多出错信息。如果用户当前根本就不需要这个服务器,那么刚才的工作就白费了。第二,允许客户端并行地尝试一组服务器,可以实现一定程度的容错性(因为只要其中一个是在运行的就可以了),而且性能也可以得到提高(通过选择第一个响应的服务器——推测该服务器负载最低)。
另一方面,我们默认在自动挂载时所有可选的文件系统都是完全相同的。由于NFS不提供对文件或目录复制的支持,用户需要自己确保所有这些文件系统都是相同的。因此,自动挂载多数情况下被用于包含系统代码的只读文件系统和其他很少改动的文件。
第二个NFS协议是为访问目录和文件设计的。客户端可以通过向服务器发送消息来操作目录和读写文件。客户端也可以访问文件属性,如文件模式、大小、上次修改时间。NFS支持大多数的Linux系统调用,但是也许很让人惊讶的是,open和close不被支持。
对open和close的省略并不是意外事件,而纯粹是有意为之。没有必要在读一个文件之前先打开它,也没有必要在读完后关闭它。读文件时,客户端向服务器发送一个包含文件名的lookup消息,请求查询该文件并返回一个标识该文件的文件句柄(即包含文件系统标识符i节点号以及其他数据)。与open调用不同,lookup操作不向系统内部表中复制任何信息。read调用包含要读取的文件的文件句柄,起始偏移量和需要的字节数。每个这样的消息都是自包含的。这个方案的优势是在两次read调用之间,服务器不需要记住任何关于已打开的连接的信息。因此,如果一个服务器在崩溃之后恢复,所有关于已打开文件的信息都不会丢失,因为这些信息原本就不存在。像这样不维护打开文件的状态信息的服务器称作是无状态的。
不幸的是,NFS方法使得难以实现精确的Linux文件语义。例如,在Linux中一个文件可以被打开并锁定以防止其他进程对其访问。当文件关闭时,锁被释放。在一个像NFS这样的无状态服务器中,锁不能与已打开的文件相关联,这是因为服务器不知道哪些文件是打开的。因此,NFS需要一个独立的,附加的机制来处理加锁。
NFS使用标准UNIX保护机制,为文件属主、组和其他用户使用读、写、执行位(rwx bits)(在第1章中提到过,将在下面详细讨论)。最初,每个请求消息仅仅包含调用者的用户ID和组ID,NFS服务器用它们来验证访问。实际上,它信任客户端,认为客户端不会进行欺骗。若干年来的经验充分表明了这样一个假设。现在,可以使用公钥密码系统建立一个安全密钥,在每次请求和应答中使用它验证客户端和服务器。启用这个选项后,恶意的客户端就不能伪装成另一个客户端了,因为它不知道其他客户端的安全密钥。
3.NFS实现
尽管客户端和服务器代码实现独立于NFS协议,但大多数Linux系统使用一个类似图10-36所示的三层实现。顶层是系统调用层,这一层处理如open、read和close之类的调用。在解析调用和参数检查结束后,调用第二层——虚拟文件系统(VFS)层。

VFS层的任务是维护一个表,每个打开的文件在该表中有一个表项。VFS层为每个打开文件保存一个虚拟i节点(或称为v-node)。v节点用来说明文件是本地文件还是远程文件。对于远程文件,v节点提供足够的信息使客户端能够访问它们。对于本地文件,则记录其所在的文件系统和文件的i节点,这是因为现代Linux系统能支持多文件系统(例如ext2fs、/proc、FAT等)。尽管VFS是为了支持NFS而发明的,但多数现代Linux系统将VFS作为操作系统的一个组成部分,不管有没有使用NFS。
为了理解如何使用v节点,我们来跟踪一组顺序执行的mount,open和read调用。要挂载一个远程文件系统,系统管理员(或/etc/rc)调用mount程序,并指明远程目录、远程目录将被挂载到哪个本地目录,以及其他信息。mount程序解析要被挂载的远程目录并找到该目录所在的NFS服务器,然后与该机器连接,请求远程目录的文件句柄。如果该目录存在并可被远程挂载,服务器就返回一个该目录的文件句柄。最后,mount程序调用mount系统调用,将该句柄传递给内核。
然后内核为该远程目录创建一个v节点,并要求客户端代码(图10-36所示)在其内部表中创建一个r节点(remote i-node)来保存该文件句柄。v节点指向r节点。VFS中的每一个v节点最终要么包含一个指向NFS客户端代码中r节点的指针,要么包含指向一个本地文件系统的i节点的指针(在图10-36中用虚线标出)。因此,我们可以从v节点中判断一个文件或目录是本地的还是远程的。如果是本地的,可以定位相应的文件系统和i节点。如果是远程的,可以找到远程主机和文件句柄。
当客户端打开一个远程文件时,在解析路径名的某个时刻,内核会碰到挂载了远程文件系统的目录。内核看到该目录是远程的,并从该目录的v节点中找到指向r节点的指针,然后要求NFS客户端代码打开文件。NFS客户端代码在与该目录关联的远程服务器上查询路径名中剩余的部分,并返回一个文件句柄。它在自己的表中为该远程文件创建一个r节点并报告给VFS层。VFS层在自己的表中为该文件建立一个指向该r节点的v节点。从这里我们再一次看到,每一个打开的文件或目录有一个v节点,要么指向一个r节点,要么指向一个i节点。
返回给调用者的是远程文件的一个文件描述符。VFS层中的表将该文件描述符映射到v节点。注意,服务器端没有创建任何表项。尽管服务器已经准备好在收到请求时提供文件句柄,但它并不记录哪些文件有文件句柄,哪些文件没有。当一个文件句柄发送过来要求访问文件时,它检查该句柄。如果是有效的句柄,就使用它。如果安全策略被启用,验证包含对RPC头中的认证密钥的检验。
当文件描述符被用于后续的系统调用(例如read)时,VFS层先定位相应的v节点,然后根据它确定文件是本地的还是远程的,同时确定哪个i节点或r节点是描述该文件的。然后向服务器发送一个消息,该消息包含句柄、偏移量(由客户端维持,而不是服务器端)和字节数。出于效率方面的考虑,即使要传输的数据很少,客户端和服务器之间的数据传输也使用大数据块,通常是8192字节。
当请求消息到达服务器,它被送到服务器的VFS层,在那里将判断所请求的文件在哪个本地文件系统中。然后,VFS层调用本地文件系统去读取并返回请求的字节。随后,这些数据被传送给客户端。客户端的VFS层接收到它所请求的这个8KB块之后,又自动发出对下一个块的请求,这样当我们需要下一个块时就可以很快地得到。这个特性称为预读(read ahead),它极大地提高了性能。
客户端向服务器写文件的过程是类似的。文件也是以8KB块为单位传输。如果一个write系统调用提供的数据少于8KB,则数据在客户端本地累积,直到达到8KB时才发送给服务器。当然,当文件关闭时,所有的数据都立即发送给服务器。
另一个用来改善性能的技术是缓存,与在通常的UNIX系统中的用法一样。服务器缓存数据以避免磁盘访问,但这对客户端而言是不可见的。客户端维护两个缓存:一个缓存文件属性(i节点),另一个缓存文件数据。当需要i节点或文件块时,就在缓存中检查有无符合的数据。如果有,就可以避免网络流量了。
客户端缓存对性能提升起到很大帮助的同时,也带来了一些令人讨厌的问题。假设两个客户端都缓存了同一个文件块,并且其中一个客户端修改了它。当另一个客户读该块时,它读到的是旧的数据值。这时缓存是不一致的。
考虑到这个问题可能带来的严重性后果,NFS实现做了一些事情来缓解这一问题。第一,为每个缓存了的块关联一个定时器。当定时器到期时,缓存的项目就被丢弃。通常,数据块的时间是3秒,目录块的时间是30秒。这稍微减少了一些风险。另外,当打开一个有缓存的文件时,会向服务器发送一个消息来找出文件最后修改的时间。如果最后修改时间晚于本地缓存时间,那么旧的副本被丢弃,新副本从服务器取回。最后,每30秒缓存定时器到期一次,缓存中所有的“脏”块(即修改过的块)都发送到服务器。尽管并不完美,但这些修补使得系统在多数实际环境中高度可用。
4.NFS第4版
网络文件系统第4版是为了简化其以前版本的一些操作而设计的。相对于上面描述的第3版NFS,第4版NFS是有状态的文件系统。这样就允许对远程文件调用open操作,因为远程NFS服务器将维护包括文件指针在内的所有文件系统相关的结构。读操作不再需要包含绝对读取范围了,而可以从文件指针上次所在的位置开始增加。这就使消息变短,同时可以在一次网络传输中捆绑多个第3版NFS的操作。