写文件流程有两种:
- 打开一个已存在的文件获得文件句柄,写文件,关闭文件。
- 创建一个新文件获得文件句柄,写文件,关闭文件。
打开一个文件写
我们调用系统函数写文件的流程是:OPEN -> WRITE -> CLOSE。但其实nfs协议并没有OPEN方法,也没有CLOSE方法。nfs协议打开文件写的流程是:LOOKUP -> WRITE -> COMMIT。
nfs协议需要通过多次LOOKUP完成OPEN文件操作,OPEN操作是提供文件路径获得文件句柄,而LOOPUP是在一个目录中查询文件(子目录也是文件)的操作,所以需要多次操作,才能获得最终要写的文件的句柄。
举例:假如我们需要写的文件的路径是/data/logs/test.log
。
那么OPEN需要做的事情:
- 调用LOOKUP查询
.
(当前)目录下的data
目录,获取data
目录的文件句柄。 - 调用LOOKUP查询
data
目录下的logs
目录,获取logs
目录的文件句柄。 - 调用LOOKUP查询
logs
目录下的test.log
文件的文件句柄。
然后我们就可以调用WRITE方法往文件写数据了。
写完数据后,CLOSE需要做的事情就是调用一下COMMIT。
如果客户端使用 stable 参数设置为 UNSTABLE 将数据写入服务器,并且回复产生了 DATA_SYNC 或 UNSTABLE 的提交响应,客户端将在将来的某个时间跟进 COMMIT 操作,以将未完成的异步数据和元数据与服务器的稳定存储同步,除非客户端出错。由于客户端崩溃或其他错误,可能无法收到后续的 COMMIT。
在nfs协议中,文件句柄并不是一个具体的结构体,对客户端来说是不透明的,客户端只需要知道文件句柄是一个不定长度的byte数组就可以了。判断两个文件句柄指向的是不是同一个文件,只需要判断byte数组的长度和每个数组元素是否都相等。
表示文件句柄的结构体是nfs_fh3:
struct nfs_fh3 {
byte[] data;
};
一些基本数据类型定义:
typedef char *filename3;
typedef uint64 fileid3;
typedef uint32 uid3;
typedef uint32 gid3;
typedef uint64 size3;
typedef uint64 offset3;
typedef uint32 mode3;
typedef uint32 count3;
typedef char writeverf3[NFS3_WRITEVERFSIZE]; // NFS3_WRITEVERFSIZE = 8
文件系统对象属性结构体:
enum ftype3 {
NF3REG = 1, // 表示普通文件
NF3DIR = 2, // 表示目录
NF3BLK = 3, // 表示块设备文件
NF3CHR = 4, // 表示字符设备文件
NF3LNK = 5, // 表示符号链接
NF3SOCK = 6, // 表示套接字
NF3FIFO = 7 // 表示命名管道
};
// 包含两个无符号32位整数(specdata1和specdata2)。
// 这两个整数的解释取决于文件系统对象的类型。
// 对于块设备(NF3BLK)或字符设备(NF3CHR)文件,specdata1和specdata2分别表示主设备号和次设备号。(这显然是一个UNIX特定的解释。)
// 对于其他所有文件类型,这两个元素应设置为0,或者其值应由客户端和服务器协商确定。
struct specdata3 {
uint32 specdata1;
uint32 specdata2;
};
// 自1970年1月1日格林威治标准时间午夜以来的秒数和纳秒数。
struct nfstime3 {
uint32 seconds;
uint32 nseconds;
};
// 文件系统对象属性结构体
struct fattr3 {
ftype3 type; // 表示文件的类型
mode3 mode; // 表示保护模式位
uint32 nlink; // 表示文件的软链接数,即相同文件的不同名称数。
uid3 uid; // 表示文件所有者的用户ID。
gid3 gid; // 表示文件所属组的组ID。
size3 size; // 表示文件的字节大小。
size3 used; // 表示文件实际使用的磁盘空间的字节数(可能小于大小,因为文件可能有空洞,或者由于碎片化而变大)。
specdata3 rdev; // 描述了设备文件
uint64 fsid; // 文件系统的标识符。
fileid3 fileid; // 在其文件系统内唯一标识文件的编号(在UNIX系统中,通常是inode号)。
nfstime3 atime; // 表示文件数据最后一次被访问的时间。
nfstime3 mtime; // 表示文件数据最后一次被修改的时间。 写入文件会改变mtime和ctime。
nfstime3 ctime; // 表示文件属性最后一次被更改的时间。 写入文件会改变mtime和ctime。
};
其它一些基础结构体:
struct pre_op_attr {
bool_t attributes_follow;
union {
wcc_attr attributes; // 当attributes_follow为true时有这个字段
} pre_op_attr_u;
};
struct post_op_attr {
bool_t attributes_follow;
union {
fattr3 attributes; // 当attributes_follow为true时有这个字段
} post_op_attr_u;
};
LOOKUP操作
LOOKUP3res NFSPROC3_LOOKUP(LOOKUP3args) = 3;
struct LOOKUP3args {
diropargs3 what;
};
struct LOOKUP3resok {
nfs_fh3 object;
post_op_attr obj_attributes;
post_op_attr dir_attributes;
};
struct LOOKUP3resfail {
post_op_attr dir_attributes;
};
union LOOKUP3res switch (nfsstat3 status) {
case NFS3_OK:
LOOKUP3resok resok;
default:
LOOKUP3resfail resfail;
};
LOOKUP是在目录中搜索给定名称的文件,并返回相应文件的文件句柄。请求参数为LOOKUP3args,相应参数为LOOKUP3res。
LOOKUP3args:
- what:要查找的对象:
- dir:要搜索的目录的文件句柄。
- name:要搜索的文件名。
what的类型是diropargs3:
struct diropargs3 {
nfs_fh3 dir;
filename3 name;
};
diropargs3 结构在目录操作中使用,例如在目录中查找文件。文件句柄dir是目录的文件句柄,文件name则是要进行操作的文件名。
LOOKUP3res是一个联合结构体,当成功时返回的是LOOKUP3resok,失败时返回的是LOOKUP3resfail。
nfsstat3:枚举值,取值:
NFS3ERR_IO
NFS3ERR_NOENT
NFS3ERR_ACCES
NFS3ERR_NOTDIR
NFS3ERR_NAMETOOLONG
NFS3ERR_STALE
NFS3ERR_BADHANDLE
NFS3ERR_SERVERFAULT
LOOKUP3resok:
- object:查询的文件的文件句柄。
- obj_attributes:查询的文件的属性。
- dir_attributes:目录(what.dir)的属性。
LOOKUP3resfail:
- dir_attributes:目录(what.dir)的属性。
WRITE操作
WRITE3res NFSPROC3_WRITE(WRITE3args) = 7;
enum stable_how {
UNSTABLE = 0,
DATA_SYNC = 1,
FILE_SYNC = 2
};
struct WRITE3args {
nfs_fh3 file;
offset3 offset;
count3 count;
stable_how stable;
byte[] data
};
struct WRITE3resok {
wcc_data file_wcc;
count3 count;
stable_how committed;
writeverf3 verf;
};
struct WRITE3resfail {
wcc_data file_wcc;
};
union WRITE3res switch (nfsstat3 status) {
case NFS3_OK:
WRITE3resok resok;
default:
WRITE3resfail resfail;
};
WRITE过程用于向文件写入数据。请求参数为WRITE3args,响应参数为WRITE3res。
WRITE3args:
- file:要写入数据的文件的文件句柄。
- offset:写入偏移量,用于指定在文件的哪个位置开始写入,偏移量为0表示从文件开头开始写入数据。
- count:要写入的数据字节数。如果 count 为 0,则 WRITE 将成功并返回 0 的计数,除非由于权限检查而出现错误。数据的大小必须小于或等于 FSINFO 回复结构中的 wtmax 字段的值,该结构用于包含文件系统中的文件。如果大于该值,服务器可能只会写入 wtmax 字节,导致写入不完整。
- data:要写入文件的数据。
- stable:
- 如果stable为FILE_SYNC,则服务器在返回结果之前必须将写入的数据以及所有文件系统元数据提交到稳定存储。
- 如果 stable 为 DATA_SYNC,则服务器必须将所有数据提交到稳定存储,并提交足够的元数据以检索数据后才返回。
- 如果 stable 为 UNSTABLE,则服务器可以在返回客户端的回复之前将数据和元数据的任何部分提交到稳定存储,包括全部或不提交。无法保证任何未提交的数据是否会随后提交到稳定存储。
WRITE3res:
- status:状态码
成功返回时,WRITE3res.status 为 NFS3_OK,WRITE3res.resok 包含:
- file_wcc:文件的弱缓存一致性数据。
- count:写入文件的数据字节数。服务器可能写入的字节数少于请求的字节数。
- committed:服务器应通过 committed 返回数据和元数据的提交级别的指示。如果服务器将所有数据和元数据都提交到稳定存储,则 committed 应设置为 FILE_SYNC。如果提交级别至少与 DATA_SYNC 一样强,则 committed 应设置为 DATA_SYNC。否则,committed 必须返回为 UNSTABLE。如果 stable 为 FILE_SYNC,则 committed 也必须为 FILE_SYNC:其他任何值都构成协议违规。如果 stable 为 DATA_SYNC,则 committed 可以是 FILE_SYNC 或 DATA_SYNC:其他任何值都构成协议违规。如果 stable 为 UNSTABLE,则 committed 可以是 FILE_SYNC、DATA_SYNC 或 UNSTABLE。
- verf:这是客户端可以使用的 cookie,用于确定服务器在 WRITE 调用和随后对 WRITE 或 COMMIT 的调用之间是否更改了状态。
WRITE3res.status 为非NFS3_OK,则WRITE3res.resfail 包含以下内容:
- file_wcc:文件的弱缓存一致性数据。即使写入失败,也会返回完整的 wcc_data。
弱缓存一致性数据wcc_data结构体:
struct wcc_data {
pre_op_attr before;
post_op_attr after;
};
wcc_data结构体包含了操作前的对象属性的关键字段以及操作后的对象属性。
COMMIT操作
nfs没有close方法,close实际就是调用一下commit方法, 将服务器上的缓存数据提交到稳定存储。
COMMIT过程在操作和语义上类似于POSIX fsync系统调用,它将文件的状态与磁盘同步,即刷新文件的数据和元数据到磁盘。COMMIT为客户端执行相同的操作,将服务器上的任何未同步数据和元数据刷新到指定文件的服务器磁盘上。与fsync类似,可能存在一些修改的数据或没有修改的数据需要同步。数据可能已经通过服务器的正常周期性缓冲区同步活动进行了同步。COMMIT与fsync的不同之处在于,客户端可以刷新文件的一部分范围。
COMMIT3res NFSPROC3_COMMIT(COMMIT3args) = 21;
struct COMMIT3args {
nfs_fh3 file;
offset3 offset;
count3 count;
};
struct COMMIT3resok {
wcc_data file_wcc;
writeverf3 verf;
};
struct COMMIT3resfail {
wcc_data file_wcc;
};
union COMMIT3res switch (nfsstat3 status) {
case NFS3_OK:
COMMIT3resok resok;
default:
COMMIT3resfail resfail;
};
如果WRITE操作指定stable为UNSTABLE,那么COMMIT操作就是用于将WRITE写入的数据强制刷新到稳定存储。
请求参数为COMMIT3args,响应参数为COMMIT3res。
COMMIT3args:
- file:要刷新(提交)数据的文件的文件句柄。这必须标识类型为NF3REG的文件系统对象。
- offset:刷新开始的文件内位置。偏移量为0表示从文件开头开始刷新数据。
- count:要刷新的数据字节数。如果count为0,则从偏移量到文件末尾进行刷新。
成功返回时,COMMIT3res.status为NFS3_OK,COMMIT3res.resok包含:
- file_wcc:文件的弱缓存一致性数据。
- verf:这是客户端可以使用的cookie,用于确定服务器在WRITE调用和后续的COMMIT调用之间是否重新启动。
失败时,COMMIT3res.status为非NFS3_OK,COMMIT3res.resfail包含:
- file_wcc:文件的弱缓存一致性数据。即使COMMIT失败,仍返回完整的wcc_data。
错误状态取值:
NFS3ERR_IO
NFS3ERR_STALE
NFS3ERR_BADHANDLE
NFS3ERR_SERVERFAULT
创建一个文件写
我们调用系统函数写文件的流程是:CREATE -> WRITE -> CLOSE,其实CREATE对应也是OPEN,不同的是,如果文件不存在则会创建文件。
在nfs协议中,创建一个文件写的流程是:LOOKUP -> CREATE -> WRITE -> COMMIT。
举例:假如我们需要需要创建的文件的路径是/data/logs/test.log
。
那么OPEN需要做的事情:
- 调用LOOKUP查询
.
(当前)目录下的data
目录,获取data
目录的文件句柄。 - 调用LOOKUP查询
data
目录下的logs
目录,获取logs
目录的文件句柄。 - 调用CREATE在
/logs
目录下创建test.log
文件,获取test.log
文件的文件句柄。
后续写操作和Close与前面相同。
CREATE操作
CREATE操作用于创建一个普通文件。(不支持创建设备文件、FIFO文件)
CREATE3res NFSPROC3_CREATE(CREATE3args) = 8;
enum createmode3 {
UNCHECKED = 0,
GUARDED = 1,
EXCLUSIVE = 2
};
union createhow3 switch (createmode3 mode) {
case UNCHECKED:
case GUARDED:
sattr3 obj_attributes;
case EXCLUSIVE:
createverf3 verf;
};
struct CREATE3args {
diropargs3 where;
createhow3 how;
};
struct CREATE3resok {
post_op_fh3 obj;
post_op_attr obj_attributes;
wcc_data dir_wcc;
};
struct CREATE3resfail {
wcc_data dir_wcc;
};
union CREATE3res switch (nfsstat3 status) {
case NFS3_OK:
CREATE3resok resok;
default:
CREATE3resfail resfail;
};
请求参数为CREATE3args,响应参数为CREATE3res。
CREATE3args:
where:要创建的文件的位置:
- dir:文件所在的目录的文件句柄。
- name:创建的文件的名称。
how:创建方式,有三种定义的创建方式:
- mode:UNCHECKED、GUARDED和EXCLUSIVE中的一种。UNCHECKED表示应该在不检查同一目录中是否存在重复文件的情况下创建文件。在这种情况下,how.obj_attributes是一个描述文件的初始属性的sattr3。GUARDED指定服务器在执行创建操作之前应检查是否存在重复文件,并且如果存在重复文件,则应拒绝请求并返回NFS3ERR_EXIST。如果文件不存在,则按照UNCHECKED的描述执行请求。EXCLUSIVE指定服务器应遵循独占创建语义,使用验证器(verifier)确保目标的独占创建。在这种情况下,不能提供任何属性,因为服务器可能使用目标文件的元数据来存储createverf3验证器。
- obj_attributes:UNCHECKED和GUARDED下,obj_attributes是一个描述文件的初始属性的sattr3。
- verf:EXCLUSIVE下,包含一个可以合理地期望是唯一的验证器。
成功返回时,CREATE3res.status为NFS3_OK,CREATE3res.resok中的结果为:
- obj:新创建的普通文件的文件句柄。(类型为post_op_fh3)
- obj_attributes:刚刚创建的普通文件的属性。
- dir_wcc:where.dir目录的弱缓存一致性数据。
// 关于post_op_fh3结构体
union post_op_fh3 switch (bool handle_follows) {
case TRUE:
nfs_fh3 handle;
case FALSE:
void;
};
// handle_follows是一个布尔值,用于指示是否存在文件句柄。
// 当handle_follows为true时,存在nfs_fh3字段,且字段值为文件句柄对象。否则表示没有文件句柄。
否则,CREATE3res.status为错误状态码,CREATE3res.resfail包含以下内容:
- dir_wcc:where.dir目录的弱缓存一致性数据。即使CREATE失败,也会返回完整的wcc_data。
错误状态取值:
NFS3ERR_IO
NFS3ERR_ACCES
NFS3ERR_EXIST
NFS3ERR_NOTDIR
NFS3ERR_NOSPC
NFS3ERR_ROFS
NFS3ERR_NAMETOOLONG
NFS3ERR_DQUOT
NFS3ERR_STALE
NFS3ERR_BADHANDLE
NFS3ERR_NOTSUPP
NFS3ERR_SERVERFAULT
关于sattr3结构体:
struct set_mode3 {
bool_t set_it;
union {
mode3 mode;
} set_mode3_u;
};
struct set_uid3 {
bool_t set_it;
union {
uid3 uid;
} set_uid3_u;
};
struct set_gid3 {
bool_t set_it;
union {
gid3 gid;
} set_gid3_u;
};
struct set_size3 {
bool_t set_it;
union {
size3 size;
} set_size3_u;
};
struct set_atime {
time_how set_it;
union {
nfstime3 atime;
} set_atime_u;
};
struct set_mtime {
time_how set_it;
union {
nfstime3 mtime;
} set_mtime_u;
};
struct sattr3 {
set_mode3 mode; // 设置文件的访问权限模式。
set_uid3 uid; // 设置文件的用户ID。
set_gid3 gid; // 设置文件的组ID。
set_size3 size; // 设置文件的大小。
set_atime atime; // 设置文件的访问时间。
set_mtime mtime; // 设置文件的修改时间。
};
sattr3结构体包含可以从客户端设置的文件属性。这些字段与fattr3结构体中的同名字段相同。在NFS版本3协议中,可设置的属性由一个包含一组带有鉴别联合体的结构体描述。每个联合体指示相应的属性是否要更新,如果是,则指定如何更新。
有两种形式的鉴别联合体。在设置mode、uid、gid或size时,鉴别联合体基于一个布尔值set_it进行切换;如果set_it为TRUE,则编码相应类型的值。
在设置atime或mtime时,联合体基于一个枚举类型set_it进行切换。如果set_it的值为DONT_CHANGE,则相应的属性保持不变。如果set_it的值为SET_TO_SERVER_TIME,则服务器将相应的属性设置为其本地时间;客户端不提供数据。最后,如果set_it的值为SET_TO_CLIENT_TIME,则属性将设置为客户端在nfstime3结构中传递的时间。
参考文献:https://www.ietf.org/rfc/rfc1813.txt (使用AI翻译帮助理解)
项目代码:https://github.com/unfs3/unfs3/blob/master/nfs.h
nfsv3协议go语言client实现:https://github.com/vmware/go-nfs-client