nfs协议,写文件流程分析

原创 吴就业 142 0 2024-06-20

本文为博主原创文章,未经博主允许不得转载。

本文链接:https://wujiuye.com/article/6a882785112e4c4785e90a100a0d4aee

作者:吴就业
链接:https://wujiuye.com/article/6a882785112e4c4785e90a100a0d4aee
来源:吴就业的网络日记
本文为博主原创文章,未经博主允许不得转载。

写文件流程有两种:

  1. 打开一个已存在的文件获得文件句柄,写文件,关闭文件。
  2. 创建一个新文件获得文件句柄,写文件,关闭文件。

打开一个文件写

我们调用系统函数写文件的流程是:OPEN -> WRITE -> CLOSE。但其实nfs协议并没有OPEN方法,也没有CLOSE方法。nfs协议打开文件写的流程是:LOOKUP -> WRITE -> COMMIT。

nfs协议需要通过多次LOOKUP完成OPEN文件操作,OPEN操作是提供文件路径获得文件句柄,而LOOPUP是在一个目录中查询文件(子目录也是文件)的操作,所以需要多次操作,才能获得最终要写的文件的句柄。

举例:假如我们需要写的文件的路径是/data/logs/test.log

那么OPEN需要做的事情:

  1. 调用LOOKUP查询.(当前)目录下的data目录,获取data目录的文件句柄。
  2. 调用LOOKUP查询data目录下的logs目录,获取logs目录的文件句柄。
  3. 调用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的类型是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:

LOOKUP3resfail:

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:

WRITE3res:

成功返回时,WRITE3res.status 为 NFS3_OK,WRITE3res.resok 包含:

WRITE3res.status 为非NFS3_OK,则WRITE3res.resfail 包含以下内容:

弱缓存一致性数据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:

成功返回时,COMMIT3res.status为NFS3_OK,COMMIT3res.resok包含:

失败时,COMMIT3res.status为非NFS3_OK,COMMIT3res.resfail包含:

错误状态取值:

  NFS3ERR_IO
  NFS3ERR_STALE
  NFS3ERR_BADHANDLE
  NFS3ERR_SERVERFAULT

创建一个文件写

我们调用系统函数写文件的流程是:CREATE -> WRITE -> CLOSE,其实CREATE对应也是OPEN,不同的是,如果文件不存在则会创建文件。

在nfs协议中,创建一个文件写的流程是:LOOKUP -> CREATE -> WRITE -> COMMIT。

举例:假如我们需要需要创建的文件的路径是/data/logs/test.log

那么OPEN需要做的事情:

  1. 调用LOOKUP查询.(当前)目录下的data目录,获取data目录的文件句柄。
  2. 调用LOOKUP查询data目录下的logs目录,获取logs目录的文件句柄。
  3. 调用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:

成功返回时,CREATE3res.status为NFS3_OK,CREATE3res.resok中的结果为:

// 关于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包含以下内容:

错误状态取值:

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

#网络

声明:公众号、CSDN、掘金的曾用名:“Java艺术”,因此您可能看到一些早期的文章的图片有“Java艺术”的水印。

文章推荐

解决http event stream一直卡到最后一个消息才一次性返回

我在用http event stream实现类似ChatGPT的打字机效果时遇到了这个问题:浏览器打开debug模式,看http event stream请求,EventStream直到最后一个消息发完才一直显示完所有消息。并不是请求失败,也不是没有响应,只是看起来像是卡住了,一直等到响应的数据全部接收完才一次性的回调onmessage方法。

NFS协议RPC通信数据包解码发现头4个字节不知道是什么?

当我们抓包并写代码解码的过程中发现,我们解码每个TCP数据包,无论是rpc请求还是rpc响应,都是要先跳过前四个字节,才是rpc协议的消息id,这样解码才正确,为什么呢?

nfs协议的rpc通信协议

通过开源项目go-nfs-client理解nfsv3的rpc通信协议,从而知道怎么解析抓取的数据包,获取需要的信息。

tcpdump抓包分析实战-学习网络问题排查必备技能-抓包分析,附多个案例讲解

了解网络协议、学会利用tcpdump抓包,学会利用Wireshark分析数据包,将能帮助我们解决一些仅从客户端日记分析或仅从服务端日记分析无法解决的疑难杂症。本篇结合笔者经历的一些实战案例,带大家掌握网络问题排查必备技能:tcpdump抓包分析。

tcpdump抓包分析实战-客户端接收到网关响应的body是空的

只因请求头deviceId多了一个‘\n’导致,服务端接收到的body是空的。

带宽问题排查实战-记一次线上文件下载慢问题排查

上传的文件是仅办公网络可访问,办法室网络有带宽限制,一个页面加载上百张图片,很容易达到带宽限制,所以出现下载很慢。