网络文件系统协议定义(RFC1094)
本文是对RFC1094的翻译,它是NFS的第2版,之所以翻译这个版本是因为它比较简单。后面会陆续完成第3版和第4版的翻译,并且完成Linux操作系统下NFS文件系统和其服务端的代码解析。作者翻译本文章出于如下几个目的:
1)练习英语,提升自身英语水平;2)进一步熟悉NFS协议;3)传播存储相关知识。
另外,协议本身是纯文本格式,没有配图,因此理解起来可能有一定的难度,因此译者在翻译的时候加入了一些译者注和插图,以方便大家理解。不妥之处还请大家多批评指正。
预警:文章比较长
本备忘录的状态
本RFC描述了一个在太阳微系统公司(译者注:这里是指SUN,当前已经被Oracle收购)和其它正在使用的协议。新版本的协议正在开发中,但其他人仍然可以从当前协议的描述中及设计问题的讨论中受益。本备忘录的分发不受任何限制。
1. INTRODUCTION(引言)
Sun网络文件系统(NFS)协议提供了透明的通过网络远程访问共享文件的方式。NFS协议被设计于可以很轻松的跨不同的机器、操作系统、网络架构和传输协议。这种可移植性是通过使用建立在外部数据表示(XDR)之上的远程过程调用(RPC)原语实现的。实现已经存在于从个人计算机到超级计算机的各种机器上。
支持的mount协议允许服务器向受限制的一组客户端分配远程访问权限。它执行特定于操作系统的功能,例如,允许将远程目录树附加到某个本地文件系统。
image译者注:
NFS实现了一个跨越网络的文件访问功能,如图1可以简要说明其原理。其整个架构为Client-Server架构,客户端和服务端通过RPC协议进行通信,RPC协议可以简单的理解为一个基于TCP的应用层协议,它简化命令和数据的传输。
NFS最大的特点是将服务端的文件系统目录树映射到客户端,而在客户端访问该目录树与访问本地文件系统没有任何差别,客户端并不知道这个文件系统目录树是本地的还是远在另外一台服务器。
<figcaption style="margin-top: 0.66667em; padding: 0px 1em; font-size: 0.9em; line-height: 1.5; text-align: center; color: rgb(153, 153, 153);">图1 客户端服务端示意</figcaption>
1.1 远程过程调用
Sun的远程过程调用规范为远程服务提供了一个面向过程的接口。每台服务器都提供一个“程序”,这是一组过程。NFS就是这样一个程序。主机地址、程序号和过程号的组合指定一个远程过程。NFS的目标是不需要低级别的任何特定可靠性级别,因此它可能用于许多底层传输协议,甚至其他远程过程调用实现。为了便于讨论,本文的其余部分将假定NFS是在Sun RPC之上实现的,如RFC 1057《RPC:远程过程调用协议规范》中所述。
image译者注:
从字面意义上可以看出,RPC是一个跨越不同计算节点的过程调用,可以理解为跨越不同计算机的函数调用。也就是说通过RPC,你可以在一台计算机上调用另外一台计算机上进程中的函数实现。如图2是一个简答的调用示意图,客户端发起RPC调用,经过网络传输给服务端,服务端进行处理后将结果返回给客户端。虽然经过了复杂的网络传输等过程,但在客户端看来,就是进行了一次函数调用。
<figcaption style="margin-top: 0.66667em; padding: 0px 1em; font-size: 0.9em; line-height: 1.5; text-align: center; color: rgb(153, 153, 153);">图2 RPC调用过程示意</figcaption>
图2 RPC调用过程示意
1.2 外部数据表示
外部数据表示(XDR)标准提供了一种在网络上表示一组数据类型的通用方法。NFS协议规范是使用RPC数据描述语言编写的。有关更多信息,请参阅RFC 1014,《XDR:外部数据表示标准》。虽然存在自动的rpc/xdr编译器来生成服务器和客户机的“存根”,但是NFS不需要它们的使用。可以使用提供等效功能的任何软件,如果编码完全相同,则可以与其他NFS实现进行互操作。
1.3 无状态服务
NFS协议旨在尽可能无状态。也就是说,为了正常工作,服务器不需要维护任何客户机的协议状态信息。无状态服务器在发生故障时比有状态服务器有明显的优势。
对于无状态服务器,客户机只需要重试一个请求,直到服务器响应;它甚至不需要知道服务器已经崩溃,或者网络暂时中断。另一方面,有状态服务器的客户机需要检测服务器故障并在服务器恢复时重建服务器状态,或者导致客户机操作失败。
这听起来可能不是一个重要的问题,但它以一些意想不到的方式影响了协议。我们认为,在协议中,能够编写不需要特殊崩溃恢复的非常简单的服务器可能需要一些额外的复杂性。请注意,即使使用了所谓的“可靠”传输协议(如TCP),客户端仍必须能够通过在连接超时时重新打开连接来处理服务中断。因此,无状态协议实际上可以简化实现。
另一方面,NFS处理一些对象,比如文件和目录,这些对象本身就具有状态——如果一个文件不保持其内容完整,它会有什么好处?目标是不在协议本身中引入任何额外的状态。文件或记录锁定以及远程执行等固有的状态操作是作为单独的服务实现的,在本文档中没有描述。
简化恢复的基本方法是使操作尽可能“等量”(以便可以重复)。此版本协议中的某些操作没有达到此目标;幸运的是,大多数操作(如读和写)都是等幂的。此外,大多数服务器故障发生在操作之间,而不是在接收操作和响应之间。最后,尽管实际的服务器故障可能很少,但在复杂的网络中,任何网络、路由器或网桥的故障都可能与服务器故障不可区分。
NFS协议的定义
服务器会随着时间的推移而改变,它们使用的协议也会随之改变。rpc为每个rpc请求提供一个版本号。此RFC描述了第二版的NFS协议。即使在第二个版本中,也有一些过时的过程和参数,将在以后的版本中删除。目前正在为第三版NFS协议准备一个RFC。
2.1 文件系统模型
NFS假定文件系统是基于目录进行分层的,目录为除底层文件之外的所有内容。目录(文件、目录、设备等)中的每个条目都有一个字符串名称。不同的操作系统可能对树的深度或使用的名称有限制,并且使用不同的语法来表示“路径名”,这是名称中所有“组件”(目录和文件名)的串联。“文件系统”是具有指定“根”的单个服务器(通常是单个磁盘或物理分区)上的树。一些操作系统提供了一个“挂载”操作,使所有文件系统显示为一棵树,而其他操作系统则维护一个文件系统的“森林”。文件是未解释字节的非结构化流。NFS的版本3使用了稍微更通用的文件系统模型。
NFS一次查找路径名的一个组件。这可能并不明显,为什么它不只是获取整个路径名,沿着目录移动,并在完成后返回一个文件句柄。有几个好理由不这么做。首先,路径名需要在目录组件之间使用分隔符,不同的操作系统使用不同的分隔符。我们可以定义一个网络标准的路径名表示,但是每个路径名都必须在每一端进行解析和转换。其他问题在第3节,NFS实现问题中讨论。
虽然文件和目录在许多方面都是相似的对象,但读取目录和文件时使用了不同的过程。这为表示目录提供了一种网络标准格式。可以使用与上面相同的参数来对每个调用只返回一个目录条目的过程进行证明。问题在于效率。目录可以包含多个条目,远程调用返回每个条目的速度太慢。
2.2 服务例程
协议定义通过一组例程进行定义,这些例程的参数和结果是使用RPC语言定义的(使用程序、版本和过程声明扩展的XDR语言)。对每个例程功能的简要描述应提供足够的信息,以便于实施。第2.3节更详细地描述了基本数据类型。
假定NFS协议中的所有过程都是同步的。当一个过程返回到客户机时,客户机可以假定操作已经完成,并且与请求相关联的任何数据现在都在稳定存储中。例如,客户机写请求可能会导致服务器更新数据块、文件系统信息块(如间接块)和文件属性信息(大小和修改时间)。当写操作返回到客户机时,它可以假定写操作是安全的,即使在服务器崩溃的情况下,它也可以丢弃所写的数据。这是服务器无状态的一个非常重要的部分。如果服务器等待从远程请求中刷新数据,客户机必须保存这些请求,以便在服务器崩溃时重新发送它们。
/* Remote file service routines */
program NFS_PROGRAM {
version NFS_VERSION {
void
NFSPROC_NULL(void) = 0;
attrstat
NFSPROC_GETATTR(fhandle) = 1;
attrstat
NFSPROC_SETATTR(sattrargs) = 2;
void
NFSPROC_ROOT(void) = 3;
diropres
NFSPROC_LOOKUP(diropargs) = 4;
readlinkres
NFSPROC_READLINK(fhandle) = 5;
readres
NFSPROC_READ(readargs) = 6;
void
NFSPROC_WRITECACHE(void) = 7;
attrstat
NFSPROC_WRITE(writeargs) = 8;
diropres
NFSPROC_CREATE(createargs) = 9;
stat
NFSPROC_REMOVE(diropargs) = 10;
stat
NFSPROC_RENAME(renameargs) = 11;
stat
NFSPROC_LINK(linkargs) = 12;
stat
NFSPROC_SYMLINK(symlinkargs) = 13;
diropres
NFSPROC_MKDIR(createargs) = 14;
stat
NFSPROC_RMDIR(diropargs) = 15;
readdirres
NFSPROC_READDIR(readdirargs) = 16;
statfsres
NFSPROC_STATFS(fhandle) = 17;
} = 2;
} = 100003;
2.2.1 什么都不做
void
NFSPROC_NULL(void) = 0;
这个例程什么都不做,它在所有的RPC服务中都可用,以允许服务响应测试和计时。
2.2.2 获取文件属性
attrstat
NFSPROC_GETATTR (fhandle) = 1;
如果响应状态为NFS_OK,这时响应的属性中包含通过输入fhandle所指定的文件的属性信息。
2.2.3 设置文件属性
struct sattrargs {
fhandle file;sattr attributes;
};
attrstat
NFSPROC_SETATTR (sattrargs) = 2;
参数"attributes" 包含着或者为-1,或者为“file”文件属性新值的域。如果其回复的状态为 NFS_OK,这时应答的属性中具有了"SETATTR"操作完成后文件的属性。
注意:在下一个版本的协议中,使用-1表示"attributes"中一个未使用的域发生了变化。
2.2.4 获取文件系统的根
void
NFSPROC_ROOT(void) = 3;
过时的。由于查找一个文件系统的根文件句柄需要在客户端和服务端移动路径名称,因此本例程不在使用。要正确执行此操作,我们必须定义路径名的网络标准表示。我们用MNTPROC_MNT 例程代替了查找根文件句柄的功能。(细节请参考附录A,“挂载协议的定义”)
2.2.5 查找文件名
diropres
NFSPROC_LOOKUP(diropargs) = 4;
如果回复的状态为NFS_OK,这时应答的"file"和"attributes"分别是参数中给定目录"dir"的文件句柄和文件“name”的属性。
2.2.6 从符号链接读取数据
union readlinkres switch (stat status) {
case NFS_OK:path data;default:void;
};
readlinkres
NFSPROC_READLINK(fhandle) = 5;
如果回复的状态为NFS_OK,这时响应中的“data”是通过fhandle参数指定的文件的数据。
注意: 由于NFS仅仅在客户端解析路径名,符号链接的路径名在不同的客户端意义是不同的(或者是无意义的)。
2.2.7 从文件读
struct readargs {
fhandle file;unsigned offset;unsigned count;unsigned totalcount;
};
union readres switch (stat status) {
case NFS_OK:fattr attributes;nfsdata data;default:void;
};
readres
NFSPROC_READ(readargs) = 6;
从给定的“file”文件返回“count”大小的数据给“data”,数据以文件的“offset”字节文件偏移为起始位置。文件的第一个字节在0偏移的位置。当读完成后,文件属性通过“attributes”返回。
注意:参数 "totalcount"没有用,协议的下一个版本将被移除。
2.2.8 写缓存
void
NFSPROC_WRITECACHE(void) = 7;
下一版本将被采用。
2.2.9 写文件
struct writeargs {
fhandle file;unsigned beginoffset;unsigned offset;unsigned totalcount;nfsdata data;
};
attrstat
NFSPROC_WRITE(writeargs) = 8;
从文件“file”开始偏移“offset”的位置开始写数据“data”。文件的第一个字节在偏移为0的位置。如果回复的“status”是NFS_OK,这时写完后“attributes”中包含文件的属性。写操作是原子操作。本客户端"WRITE"写的数据不会和其它客户端的"WRITE"操作冲突。
注意:参数 "beginoffset"和"totalcount"将在下一个版本被移除。
2.2.10 创建文件
struct createargs {
diropargs where;
sattr attributes;
};
diropres
NFSPROC_CREATE(createargs) = 9;
在给定的文件夹“dir”中创建一个名为“name”的文件。新文件的初始属性由“attributes”确定。回复“status”如果是NFS_OK则表示文件创建成功,并通过“file”和“attributes”返回文件句柄和属性。其它任何“status”意味着操作失败,没有文件创建成功。
注意: 该例程应该创建一个互斥创建旗标,意味着“仅仅不在该文件时创建”。
2.2.11 移除文件
stat
NFSPROC_REMOVE(diropargs) = 10;
从给定的目录“dir”中移除文件“name”。回复NFS_OK 表示文件被移除。
注意:可能是非幂等操作
2.2.12 重命名文件
struct renameargs {
diropargs from;diropargs to;
};
stat
NFSPROC_RENAME(renameargs) = 11;
给定目录"from.dir"中的文件 "from.name"命名为"to.dir"中的文件"to.name"。如果回复NFS_OK表示命名成功。RENAME 操作是原子操作,在执行过程中不能被打断。
注意:可能是非幂等操作
2.2.13 创建文件链接
例程12, 版本 2.
struct linkargs {
fhandle from;diropargs to;
};
stat
NFSPROC_LINK(linkargs) = 12;
在给定的“to.dir”目录中创建一个名为“to.name”的文件。该文件是一个当前已经存在文件“from”的硬链接。如果返回值是NFS_OK,链接将被创建。其它任何返回值将表示有错误,链接不会被创建。
一个硬链接应该具有改变任何被链接文件都应该反映在两个文件中的特性。当一个文件做了硬链接的时候,该文件的“nlink”属性应该具有一个值,而且值应该比做链接之前大。
注意:可能是非幂等操作
2.2.14 创建符号链接
struct symlinkargs {
diropargs from;path to;sattr attributes;
};
stat
NFSPROC_SYMLINK(symlinkargs) = 13;
在给定的目录"from.dir"中创建类型为NFLNK的文件"from.name"。新文件包含路径名"to",并且具有通过"attibutes"指定的初始属性。如果返回值是NFS_OK,链接将被创建成功。任何其它返回值表示创建错误,链接不被创建。
符号链接是另外一个文件的指针。给定的名称"to"不会被服务端解析,仅仅存储在新建的文件中。当客户端引用一个符号链接文件时,符号链接的内容通常被透明地重新解释为要替换的路径名。READLINK操作向客户端返回解析的数据。
注意: 在UNIX系统上属性从来不会被使用,这是因为符号链接的模式总是0777.
2.2.15 创建目录
diropres
NFSPROC_MKDIR (createargs) = 14;
在给定的目录"where.dir"中新建一个目录"where.name"。通过给定的"attributes"设置目录的初始属性。返回NFS_OK表示新目录创建成功,此时返回的"file"和"attributes"为文件句柄和属性。其它任何返回表示操作失败,并且没有目录创建。
注意:可能是非幂等操作
2.2.16 移除文件夹
stat
NFSPROC_RMDIR(diropargs) = 15;
更定文件夹“dir”中的名为“name”的空文件夹被移除。如果回复是NFS_OK,则文件夹被移除。
注意: 可能是费米等操作
2.2.17 从文件夹中读
struct readdirargs {
fhandle dir;nfscookie cookie;unsigned count;
};
struct entry {
unsigned fileid;filename name;nfscookie cookie;entry *nextentry;
};
union readdirres switch (stat status) {
case NFS_OK:
struct {
entry *entries;bool eof;
} readdirok;
default:void;
};
readdirres
NFSPROC_READDIR (readdirargs) = 16;
从给定的文件夹“dir”中返回一定数量的目录项,数量有参数“count”决定。如果返回值中“status”的值为NFS_OK,这时将有多个“entry”紧跟其后。每个“entry”包含一个“fileid”,其由一个文件系统中标识一个文件的唯一数字、文件的“name”和一个目录中指向下一个项的隐藏指针构成。
cookie 用于下一次READDIR调用中,用于在本目录中指定开始查找的位置。特定的0 cookie用于对目录进行从头查找。
fileid 域应该与文件属性中的“fileid”有相同的值。
eof 是一个旗标,如果目录中没有更多项目,则返回TRUE。
2.2.18 获取文件属性
union statfsres (stat status) {
case NFS_OK:
struct {
unsigned tsize;unsigned bsize;unsigned blocks;unsigned bfree;unsigned bavail;
} info;
default:
void;
};
statfsres
NFSPROC_STATFS(fhandle) = 17;
如果返回的"status"是NFS_OK,回复"info"包含由输入fhandle指定文件的文件系统的属性 。属性域包含如下值:
tsize 服务器端以字节为单位的最有传输大小。这个是READ和WRITE请求中服务器端期望的数据部分的字节数量。
bsize 文件系统以字节为单位的块大小。
blocks 文件系统上"bsiez"块的总数量。
bfree 文件系统上"bsize"块的剩余数量。
bavail "bsize"块非特权用户的可用数量。
注意: 如果文件系统具有变化大小的块,本调用工作可能会有问题。
2.3 基本数据类型
如下用在其它数据结构中的XDR定义是基本的数据结构和类型。
2.3.1. stat
enum stat {
NFS_OK = 0,
NFSERR_PERM=1,
NFSERR_NOENT=2,
NFSERR_IO=5,
NFSERR_NXIO=6,
NFSERR_ACCES=13,
NFSERR_EXIST=17,
NFSERR_NODEV=19,
NFSERR_NOTDIR=20,
NFSERR_ISDIR=21,
NFSERR_FBIG=27,
NFSERR_NOSPC=28,
NFSERR_ROFS=30,
NFSERR_NAMETOOLONG=63,
NFSERR_NOTEMPTY=66,
NFSERR_DQUOT=69,
NFSERR_STALE=70,
NFSERR_WFLUSH=99
};
“stat”类型作为每次例程调用结果返回。如果值为NFS_OK则说明该次调用成功完成,而且结果是可用的。其它值则说明在服务端例程处理过程中发生了某种错误。错误码继承了UNIX错误码。
NFSERR_PERM
非所有者。调用者没有正确的所有权去执行请求。
NFSERR_NOENT
没有该文件或者目录,指定的文件或者目录不存在。
NFSERR_IO
当执行操作的时候发生了某种硬件错误,比如磁盘错误。
NFSERR_NXIO
没有该设备或者地址。
NFSERR_ACCES
权限被拒绝。调用者对执行的请求操作没有正确的权限。
NFSERR_EXIST
文件存在。指定的文件以及存在。
NFSERR_NODEV
无此设备。
NFSERR_NOTDIR
无此目录。调用者在执行一个目录操作是指定了一个不存在的目录。
NFSERR_ISDIR
是目录。调用者在执行一个非目录操作的时候指定了一个目录。
NFSERR_FBIG
文件太大。操作致使文件大小超过了服务端的限制。
NFSERR_NOSPC
设备无空间。操作致使服务端文件系统达到其最大限制。
NFSERR_ROFS
只读文件系统。企图在一个只读的文件系统执行写操作。
NFSERR_NAMETOOLONG
文件名太长。在一个操作中的文件名太长。
NFSERR_NOTEMPTY
目录非空。试图删除一个非空目录。
NFSERR_DQUOT
磁盘配额已超。在服务器上的客户端配额已超。
NFSERR_STALE
参数中的"fhandle"不可用。文件句柄所关联的文件已经不存在或者已经被撤销。
NFSERR_WFLUSH
请求"WRITECACHE"中涉及的服务端的写缓存需要刷写到磁盘。
2.3.2. ftype
enum ftype {
NFNON = 0,
NFREG = 1,
NFDIR = 2,
NFBLK = 3,
NFCHR = 4,
NFLNK = 5
};
枚举"ftype"给出了文件的类型。NFNON表示为非文件,NFREG表示普通文件,NFDIR表示目录,NFBLK表示块设备,NFCHK表示字符设备,NFLNK表示符号链接。
2.3.3. fhandle
typedef opaque fhandle[FHSIZE];
"fhandle"是在服务器端与客户端传递的文件句柄。所有文件操作通过文件句柄来关联一个文件或者目录。文件句柄包含这能够区分一个独立文件的关键信息。
2.3.4. timeval
struct timeval {
unsigned int seconds;
unsigned int useconds;
};
结构体"timeval"包含着格林威治时间自1970年1月1日零时起的秒和毫秒的数字。用于传递时间和日期信息。
2.3.5. fattr
struct fattr {
ftype type;
unsigned int mode;
unsigned int nlink;
unsigned int uid;
unsigned int gid;
unsigned int size;
unsigned int blocksize;
unsigned int rdev;
unsigned int blocks;
unsigned int fsid;
unsigned int fileid;
timeval atime;
timeval mtime;
timeval ctime;
};
结构体"fattr" 包含一个文件的属性。
“type”是文件的类型;
“nlink”是该文件硬链接的数量;
“uid”是拥有该文件的用户的识别数字;
“gid”该文件所属组的组识别数字;
“size”是文件以字节为单位的大小;
“blocksize”是文件块以字节为单位的大小;
“rdev”文件的设备号;
“blocks”是文件在磁盘上占用的块数;
“fsid”是包含该文件的文件系统的唯一标识;
“fileid”是文件在文件系统中的唯一标识;
“atime”最后一次访问时间,包括写或者读;
“mtime”最后一次更改时间(写);
“ctime”是文件状态发生变化的时间,如果写文件,文件大小发生变化时也会影响该值。
“mode”是通过一系列位标识的访问模式。需要注意的是文件类型在模式位和文件type中都有定义。这个确实是一个Bug,在以后的版本中将会合并。下面以八进制数字给出每一位的含义。
0040000 代表文件夹; "type" 域应该是NFDIR。
0020000 是一个字符相关的文件; "type" 域应该是 NFCHR。
0060000 是一个块相关的文件; "type" 域应该是 NFBLK.
0100000 是一个常规文件; "type" 域应该是 NFREG.
0120000 是一个符号链接文件; "type"域应该是 NFLNK.
0140000 是一个命名套接字; "type"域应该是 NFNON.
0004000 执行的时候设置用户ID.
0002000 执行的时候设置组ID.
0001000 保存交换文本,甚至在用完之后.
0000400 允许所有者读.
0000200 允许所有者写.
0000100 允许所有者执行或者搜索.
0000040 允许组读.
0000020 允许组写.
0000010 允许组执行或者搜索.
0000004 允许其他用户读.
0000002 允许其他用户读.
0000001 允许其他用户执行或者搜索.
注意: 这些位与使用UNIX中的stat(2)系统调用返回的结果一致。文件类型在模式位和文件type中都有定义。在以后的版本中将会合并。
属性结构体的“rdev”域是操作系统定义的设备表示,在下一个版本的协议中将被移除。
2.3.6. sattr
struct sattr {
unsigned int mode;
unsigned int uid;
unsigned int gid;
unsigned int size;
timeval atime;
timeval mtime;
};
结构体"sattr" 包含可以被客户端设置的文件属性。各个域的含义与上述“fattr”中一致。如果“size”为零,则表示文件应该被截断。如果为-1,则表示应该被忽略。
2.3.7. filename
typedef string filename<MAXNAMLEN>;
"filename"类型用于传输文件名或者路径名组件。
2.3.8. path
typedef string path<MAXPATHLEN>;
类型“path”是一个路径名。服务端认为其是一个没有内部结构的字符串,但客户端认为其实文件系统树种的一个节点的名称。
2.3.9. attrstat
union attrstat switch (stat status) {
case NFS_OK:
fattr attributes;
default:
void;
};
结构体 "attrstat" 是普通过程调用的返回结果。其包含一个“status”,如果调用成功,它还会包含被操作文件的属性信息。
2.3.10. diropargs
struct diropargs {
fhandle dir;
filename name;
};
结构体"diropargs"用于目录操作。"fhandle" "dir" 是要查找文件“name”的目录。目录操作是对目录内部的操作。
2.3.11. diropres
union diropres switch (stat status) {
case NFS_OK:
struct {
fhandle file;
fattr attributes;
} diropok;
default:
void;
};
目录操作的结果通过"diropres"结构体返回。如果调用成功,与该文件关联的文件句柄“file”和“attibutes”将被返回,同时返回“status”。
3 NFS实现规则
NFS协议设计于可以运行不同的操作系统共享文件。但是,由于其在UNIX环境中设计,很多操作与UNIX文件系统的操作非常相像。本节将讨论一些特定的实现细节和语义问题。
3.1. 服务器/客户端的关系
NFS协议设计认为服务器越简单,越通用越好。如果客户端想实现复杂的文件系统语义的情况下,有时服务器端的简化可能会是问题。
例如,有些操作系统允许删除一些正在打开状态的文件。一个进程可以打开一个文件并删除一个文件,并且该文件处于已经被其它进程打开的状态。只要进程保持该文件的打开状态,一个文件能够一直被读写,即使这个文件在文件系统中已经不存在了。对于一个无状态的服务端,实现这些语义是不可能的。客户端可以通过一些技巧,例如在删除的时候重命名,然后在关闭的时候删除等。我们相信服务端为客户端实现大多数文件系统语义提供了足够的功能。
译者注: 在Linux操作系统下是允许一个文件被多个进程同时打开的,如果其中一个进程删除了该文件,而另外一个进程仍然可以进行该文件的读写。其实现的基本原理就是通过“孤儿”文件实现,也就是在后台将文件移动到一个隐藏的位置。但对于Windows操作系统则有互斥的操作,也就是在一个文件正在被读写的过程中,其它进程是不可以删除该文件的。
每一个NFS客户端都潜在的是一个服务端,并且远程文件系统和本地文件系统可以被任意混用。这将导致一些有趣的问题,当一个客户端沿着一个远程文件系统的目录树遍历时,如果达到该服务器的另外一个远程文件系统。允许服务器执行第二次远程装载需要进行循环检测、服务器查找和用户重新验证。然后,我们决定不允许客户端跨越服务器的挂载点。当一个客户端在一个目录中做LOOKUP 操作,而服务端又在该目录挂载了一个文件系统,服务端看到的是底层目录而非挂载目录。
例如,如果服务端有一个文件系统名为“/usr”,并且在其中又将另外一个文件系统挂载在“/usr/src”。如果客户端挂载“/usr”,它将不会看到“/usr/src”的挂载版本。一个客户端可以做与服务端的装载点匹配的远程挂载,以维护与服务端一致的视图。本例中,客户端也可以将“/usr/src”挂载到“/usr”中,即使它们来自同一个服务器。
3.2. 路径名的解释
通常在客户机上解析路径名的规则有一些复杂之处。例如,符号链接可以在不同的客户机上有不同的解释。非UNIX实现的另一个常见问题是路径名“.”的特殊解释,即给定目录的父目录。协议的下一个修订版使用显式标志来指示父级。
3.3. 权限问题
严格来说,NFS协议没有定义服务器使用的权限检查。但是,预料之中的是服务器将使用AUTH_UNIX风格的身份验证作为其保护机制的基础,执行正常的操作系统权限检查。服务端在每次调用中获取客户端的有效“uid”、有效“gid”和组,并使用它们检查权限。这种方法有各种各样的问题,可以用有趣的方式解决。
使用“uid”和“gid”意味着客户机和服务器共享相同的“uid”列表。每个服务器和客户机对必须具有从用户到“uid”和从组到“gid”的相同映射。由于每个客户机也可以是服务器,这往往意味着整个网络共享相同的“uid/gid”空间。AUTH_DES(以及下一版本的nfs协议)使用字符串名称而不是数字,但仍有一些复杂的问题需要解决。
另一个问题是由于通常是有状态的开放操作。大多数操作系统在打开时检查权限,然后在每次读写请求时检查文件是否打开。对于无状态服务器,服务器不知道文件已打开,必须对每个读写调用执行权限检查。在本地文件系统上,用户可以打开一个文件,然后更改权限,这样就不允许任何人接触它,但仍然可以写入该文件,因为它是打开的。相反,在远程文件系统上,写入将失败。为了解决这个问题,服务器的权限检查算法应该允许文件的所有者访问它,而不管权限设置如何。
类似的问题与通过网络从文件调入有关。操作系统通常在打开文件进行请求分页之前检查执行权限,然后从打开的文件中读取块。该文件可能没有读取权限,但打开后并不重要。NFS服务器无法区分正常文件读取和请求页读取之间的区别。要使此工作正常,如果调用中给定的“uid”对文件具有“执行”或“读取”权限,则服务器允许读取文件。
在大多数操作系统中,一个特定的用户(在Unix上,用户ID为零)可以访问所有文件,不管它们拥有什么权限和所有权。服务器上可能不允许此“超级用户”权限,因为任何可以在其工作站上成为超级用户的人都可以访问所有远程文件。在进行访问检查之前,Unix服务器默认将用户ID 0映射到-2。除了不能避免超级用户访问的nfs根文件系统外,这是可以工作的。
3.4. RPC 信息
认证
NFS服务使用AUTH_UNIX, AUTH_DES或AUTH_SHORT风格的认证,除了在NULL例程中。这时AUTH_NONE是被允许的。
传输协议
NFS通常在UDP协议上。
端口
当前NFS协议使用UDP的2049端口,这个并不是官方认可的端口,因此后续版本的协议将采用端口映射机制。
3.5. XDR结构体的大小
下面是协议中的各种XDR结构体的大小,以10进制字节为单位。
/*
- READ或 WRITE请求的最大字节数
/
const MAXDATA = 8192;
/ 路径名参数的最大字节数 /
const MAXPATHLEN = 1024;
/ 文件名参数的最大字节数 /
const MAXNAMLEN = 255;
/ 通过 READDIR传递的非透明 "cookie"的最大字节数 /
const COOKIESIZE = 4;
/ 非透明文件句柄的最大字节数 */
const FHSIZE = 32;
3.6. 设置RPC参数
各种文件系统参数和选项应该在挂载的时候设置。挂载协议在下面的附录中描述。例如,“软”挂载和“硬”挂载通常都提供。软挂载文件系统在RPC操作失败时返回错误(在给定数量的可选重新传输之后),而硬挂载文件系统将永远继续重新传输。最大传输大小取决于实现。为了在本地网络上进行有效的操作,通常使用8192字节的数据。这可能导致较低级别的碎片(例如在IP级别)。由于某些网络接口可能不允许这样的数据包在低速网络或主机上运行,或通过网关运行,512或1024字节的传输大小通常会提供更好的结果。
客户机和服务器可能需要保存最近操作的缓存,以帮助避免非等幂操作的问题。例如,如果传输协议丢弃删除文件操作的响应,则在重新传输时,服务器可能返回错误代码NFSERR_NOENT,而不是NFS_OK。但是,如果服务器保留上次请求的操作及其结果,它可以返回正确的成功代码。当然,服务器可能在重新传输之间崩溃并重新启动,但是一个小的缓存(甚至一个条目)可以解决大多数问题。
译者注: 对于删除操作,如果服务端已经完成并回复,但客户端接收响应超时,再次发起删除请求。此时服务端已经没有要删除的文件,因此会返回NFSERR_NOENT。但这种结果在用户层面是不应该的,因为用户时期望删除一个存在的文件,而不关心内部的容错。
索引 A. 挂载协议的定义
A.1. 简介
挂载协议独立于NFS协议,但与之相关。它提供了操作系统特定的服务来让NFS运行起来——查找服务器路径名、验证用户身份和检查访问权限。客户机使用挂载协议获取第一个文件句柄,这允许他们进入远程文件系统。
挂载协议与nfs协议分开,以便在不更改nfs服务器协议的情况下插入新的访问检查和验证方法。
注意,协议定义意味着有状态的服务器,因为服务器维护客户机的装载请求列表。装载列表信息对于客户机或服务器的正确运行不是至关重要的。它仅用于建议用途,例如,在服务器停机时警告可能的客户机。
挂载协议的版本1与nfs协议的版本2一起使用。这两个协议之间唯一通信的信息是“fhandle”结构。
A.2. RPC 信息
认证
挂载服务仅仅使用AUTH_UNIX和 AUTH_NONE风格的认证。
传输协议
挂载服务支持UPD和TCP协议。
端口
请参考服务器端口映射,在RFC 1057中有详细的描述, "RPC: Remote
Procedure Call Protocol Specification"。
A.3. XDR结构的大小
下面是协议中的各种XDR结构体的大小,以10进制字节为单位。
/* The maximum number of bytes in a pathname argument. /
const MNTPATHLEN = 1024;
/ The maximum number of bytes in a name argument. /
const MNTNAMLEN = 255;
/ The size in bytes of the opaque file handle. */
const FHSIZE = 32;
A.4. 基本数据类型
本节描述挂载协议中使用的数据类型。在大多数情况下,这些数据类型与NFS协议类似。
A.4.1. fhandle
typedef opaque fhandle[FHSIZE];
"fhandle"是在服务器端与客户端传递的文件句柄。所有文件操作通过文件句柄来关联一个文件或者目录。文件句柄包含这能够区分一个独立文件的关键信息。
这个定义与NFS第二版本中的XDR“fhandle”定义一致,具体请参考"基本数据类型"下的"2.3.3. fhandle"。
A.4.2. fhstatus
union fhstatus switch (unsigned status) {
case 0:
fhandle directory;
default:
void;
}
“fhstatus”是一个枚举类型。如果返回的“status”是0, 则表示调用成功完成,同时会返回一个 文件句柄"directory"。一个非0值表示某种错误。这种情况下,status是一个UNIX错误码。
A.4.3. dirpath
typedef string dirpath<MNTPATHLEN>;
"dirpath" 类型是一个目录路径名。
A.4.4. name
typedef string name<MNTNAMLEN>;
"name"是一个用于做各种名称的任意字符串。
A.5. Server Procedures
如下定义了挂载服务使用的各种RPC例程。
/*
- Protocol description for the mount program
/
program MOUNTPROG {
/- Version 1 of the mount protocol used with
- version 2 of the NFS protocol.
*/
version MOUNTVERS {
void
MOUNTPROC_NULL(void) = 0;
fhstatus
MOUNTPROC_MNT(dirpath) = 1;
mountlist
MOUNTPROC_DUMP(void) = 2;
void
MOUNTPROC_UMNT(dirpath) = 3;
void
MOUNTPROC_UMNTALL(void) = 4;
exportlist
MOUNTPROC_EXPORT(void) = 5;
} = 1;
} = 100005;
A.5.1. 不做任何事情
void
MNTPROC_NULL(void) = 0;
该例程不做任何事情。在任何RPC服务中都可以使用,用于测试和计时。
A.5.2. 添加挂载项
fhstatus
MNTPROC_MNT(dirpath) = 1;
如果返回的“status”为0,则 "directory"中包含目录“dirname”的文件句柄。该句柄也可能用在NFS协议中。该例程将在客户端的"dirname"上添加一个新项。
A.5.3. 返回挂载项
struct *mountlist {
name hostname;
dirpath directory;
mountlist nextentry;
};
mountlist
MNTPROC_DUMP(void) = 2;
返回远程已挂载文件系统的列表。"mountlist"包含"hostname"和 "directory"对。
A.5.4. 移除挂载点
void
MNTPROC_UMNT(dirpath) = 3;
移除"dirpath"指定的所有挂载项。
A.5.5. 移除所有挂载项
void
MNTPROC_UMNTALL(void) = 4;
移除本客户端的所有挂载项。
A.5.6. 返回导出列表
struct *groups {
name grname;
groups grnext;
};
struct *exportlist {
dirpath filesys;
groups groups;
exportlist next;
};
exportlist
MNTPROC_EXPORT(void) = 5;
返回一定数量的导出列表项。每一项包含文件系统名称和允许导入的组列表。文件系统名称在“filesys”中,组名称在“groups”列表中。
注意: 导出列表应该包含文件系统状态的更多信息,例如只读旗标等。
PDF版下载地址: https://u19702000.ctfile.com/fs/19702000-375873053