nfs 文件句柄_NFS端口上一篇文章中我们以REMOVE请求为例讲解了NFS请求的处理过程,其中提到了文件句柄的概念,NFS需要根据文件句柄查找一个文件,这篇文章中我们就来聊聊文件句柄。在普通的文件系统中,我们用文件索引节点编号(ino)表示一个文件。ino就是一个数字,ino保存在磁盘中,整个文件系统中任何两个文件的ino都不相同,因此给定一个ino,我们就能找到对应的文件。当使用NFS文件系统时就出现问题了,我们无法通
大家好,又见面了,我是你们的朋友全栈君。如果您正在找激活码,请点击查看最新教程,关注关注公众号 “全栈程序员社区” 获取激活教程,可能之前旧版本教程已经失效.最新Idea2022.1教程亲测有效,一键激活。
Jetbrains全系列IDE稳定放心使用
上一篇文章中我们以REMOVE请求为例讲解了NFS请求的处理过程,其中提到了文件句柄的概念,NFS需要根据文件句柄查找一个文件,这篇文章中我们就来聊聊文件句柄。在普通的文件系统中,我们用文件索引节点编号(ino)表示一个文件。ino就是一个数字,ino保存在磁盘中,整个文件系统中任何两个文件的ino都不相同,因此给定一个ino,我们就能找到对应的文件。当使用NFS文件系统时就出现问题了,我们无法通过文件索引编号找到对应的文件。下面的例子中我们将一个文件系统挂载在另一个文件系统之上导出了。
mount /dev/sdb1 /tmp/nfs/root/mount
/tmp/nfs/root 192.168.0.0/16(sec=sys,rw,sync)
/tmp/nfs/root/mount 192.168.0.0/16(nohide,sec=sys,rw,sync)
当客户端执行 mount -t nfs nfs_server:/tmp/nfs/root /tmp/mnt后,客户端挂载了服务器端的两个文件系统/tmp/nfs/root和/tmp/nfs/root/mount。
因此,当NFS客户端给出一个文件索引节点编号时,服务器端无法确定到底是哪个文件系统中的索引编号,也就无法找到对应的文件。为了区分不同的文件系统,NFS用文件句柄标识一个文件,文件句柄中既包含了服务器端文件系统的信息,也包含了文件的信息。服务器端解析客户端传递过来的文件句柄,定位客户端请求的文件。对NFS客户端来说,文件句柄是透明的,客户端不关心文件句柄的构成方式,也不对文件句柄进行解析。只需要将文件句柄传递给服务器端就可以了。服务器端可以向文件句柄中加入任何信息,只要保证能根据文件句柄查找到对应的文件就可以了。
1.文件句柄的数据结构
NFS不同版本对文件句柄的长度进行了不同的限制,NFSv2中文件句柄长度固定为32字节。NFSv3中文件句柄长度可变,但是不能超过64字节。NFSv4中文件句柄长度可变,但是不能超过128字节。Linux中,服务器端一个文件句柄的数据结构如下:
- struct knfsd_fh {
-
- unsigned int fh_size;
-
-
-
- union {
- struct nfs_fhbase_old fh_old;
- __u32 fh_pad[NFS4_FHSIZE/4];
- struct nfs_fhbase_new fh_new;
- } fh_base;
- };
knfsd_fh由两个字段构成:fh_size表示文件句柄的实际长度,fh_base表示文件句柄的内容。文件句柄的构成方式有三种:fh_old、fh_pad、fh_new,这里我们只讲解fh_new的构成方式。在这种方式中,文件句柄用数据结构nfs_fhbase_new表示。
- struct nfs_fhbase_new {
- __u8 fb_version;
- __u8 fb_auth_type;
- __u8 fb_fsid_type;
- __u8 fb_fileid_type;
- __u32 fb_auth[1];
-
-
- };
fb_version表示版本号,固定为1。
fb_auth_type表示文件句柄是否经过了MD5校验,0表示没有经过校验,1表示进行了校验处理。目前的实现方式中固定为0。
fb_fsid_type表示fsid的构成方式,fsid表示一个文件系统。
fb_fileid_type表示fileid的构成方式,fileid表示一个文件。
fb_auth 如果fb_auth_type为1,fb_auth表示文件句柄MD5校验值。如果fb_auth_type为0,则没有这个字段。
fb_fsid 是一个文件系统的标识,这个字段的长度可变,根据fb_fsid_type进行设置。
fb_fileid 是一个文件的标识,这个字段的长度也可变,根据fb_fileid_type进行设置。
fb_fsid_type表示文件系统的标识方式,linux定义了八种方式,这八种方式定义在文件fs/nfsd/nfsfh.h中。
- enum nfsd_fsid {
- FSID_DEV = 0,
- FSID_NUM,
- FSID_MAJOR_MINOR,
- FSID_ENCODE_DEV,
- FSID_UUID4_INUM,
- FSID_UUID8,
- FSID_UUID16,
- FSID_UUID16_INUM,
- };
fb_fsid_type是管理员配置/etc/exports文件时设置的,函数set_version_and_fsid_type()会根据配置情况挑选一种合适的标识方式,我们在后面会讲解这个函数。
fb_fileid_type表示文件的标识方式,Linux定义了很多种
- enum fid_type {
-
-
-
-
- FILEID_ROOT = 0,
-
-
-
-
- FILEID_INO32_GEN = 1,
-
-
-
-
-
- FILEID_INO32_GEN_PARENT = 2,
-
-
-
-
-
- FILEID_BTRFS_WITHOUT_PARENT = 0x4d,
-
-
-
-
-
-
- FILEID_BTRFS_WITH_PARENT = 0x4e,
-
-
-
-
-
-
-
- FILEID_BTRFS_WITH_PARENT_ROOT = 0x4f,
-
-
-
-
-
- FILEID_UDF_WITHOUT_PARENT = 0x51,
-
-
-
-
-
-
- FILEID_UDF_WITH_PARENT = 0x52,
-
-
-
-
-
- FILEID_NILFS_WITHOUT_PARENT = 0x61,
-
-
-
-
-
-
- FILEID_NILFS_WITH_PARENT = 0x62,
- };
注释中已经写得很清楚了,就不翻译了。但是FILEID_INO32_GEN_PARENT需要一点解释,这种方式的fileid长度可能1是2字节,还可能是16字节,如果是16字节,还包括32 bit parent directory generation number。另外需要注意的是,fb_fileid_type不是管理员配置的,而是文件系统的属性,由具体文件系统设置。函数_fh_update()会根据实际情况挑选一种合适的fileid,我们在后面进行讲解。
2.svc_expkey和svc_export
在讲解文件句柄的构造函数之前我们先讲两个数据结构svc_expkey和svc_export。svc_expkey结构的定义如下:
- struct svc_expkey {
- struct cache_head h;
-
- struct auth_domain * ek_client;
- int ek_fsidtype;
- u32 ek_fsid[6];
-
- struct path ek_path;
- };
svc_expkey是一类RPC缓存,这个RPC缓存与rpc.mountd交互的接口是/proc/net/rpc/nfsd.fh,这个缓存的作用是查找指定文件系统的路径,并检查用户对这个文件系统的访问权限。查找条件有三个:
ek_client: 这里保存了用户所属于的认证域,这是通过UNIX认证中的svcauth_unix_set_client()设置的,前面的文章中已经讲过这个函数了。
ek_fsidtype: 这是文件系统中fsid的构成方式
ek_fsid: 这是文件句柄的fsid。
内核将ek_client、ek_fsidtype、ek_fsid写入文件/proc/net/rpc/nfsd.fh/channel中,rpc.mountd中的函数nfsd_fh根据ek_fsidtype和ek_fsid组装出fsid,与导出的每个文件系统进行比较。如果fsid相同,再将ek_client和允许挂载这个文件系统的客户端列表进行比较,如果相同表示ek_client指定的客户端可以访问这个文件系统,就将文件系统的根节点路径写入channel文件中,内核解析channel文件中的数据,更新svc_expkey结构中的ek_path。
svc_export结构的定义如下
- struct svc_export {
- struct cache_head h;
- struct auth_domain * ex_client;
- int ex_flags;
- struct path ex_path;
- uid_t ex_anon_uid;
- gid_t ex_anon_gid;
- int ex_fsid;
- unsigned char * ex_uuid;
- struct nfsd4_fs_locations ex_fslocs;
- int ex_nflavors;
- struct exp_flavor_info ex_flavors[MAX_SECINFO_LIST];
- struct cache_detail *cd;
- };
这也是一类RPC缓存,这个RPC缓存与rpc.mountd交互的接口是/proc/net/rpc/nfsd.export,这个缓存的作用是根据文件系统根节点的路径查询文件系统的详细信息,这些信息就是管理员导出文件系统时设置的一些信息。rpc.mountd中的处理函数是nfsd_export()。nfsd_export()将ex_path指定的路径与每个文件系统根节点的路径进行比较,如果相同就将这个文件系统的信息写入channel文件中,内核解析channel文件中的数据,填充svc_export结构中的各个字段。
3.文件句柄的构造函数
Linux中生成文件句柄的函数是 __be32 fh_compose(struct svc_fh *fhp, struct svc_export *exp, struct dentry *dentry, struct svc_fh *ref_fh)
这个函数的参数说明如下:
fhp是输出参数,生成的文件句柄就保存在这个变量中了
exp是文件所在的文件系统
dentry是文件的目录项结构
ref_fh是创建文件句柄时使用的一个参考值,一般是父目录的文件句柄,这个值可以为NULL.
步骤1.取出文件节点和父节点的信息
- struct inode * inode = dentry->d_inode;
- struct dentry *parent = dentry->d_parent;
- __u32 *datap;
- dev_t ex_dev = exp_sb(exp)->s_dev;
步骤2.设置文件句柄中的fb_version和fb_fsid_type
- set_version_and_fsid_type(fhp, exp, ref_fh);
这个函数定义如下:
fhp是输出参数,将要组装的文件句柄就保存在这里
exp是输入参数,这是文件所在文件系统的数据结构
ref_fh是输入参数,这是一个参考文件句柄,一般是父目录的文件句柄
- static void set_version_and_fsid_type(struct svc_fh *fhp, struct svc_export *exp, struct svc_fh *ref_fh)
- {
- u8 version;
- u8 fsid_type;
- retry:
- version = 1;
-
- if (ref_fh && ref_fh->fh_export == exp) {
- version = ref_fh->fh_handle.fh_version;
- fsid_type = ref_fh->fh_handle.fh_fsid_type;
-
- ref_fh = NULL;
-
- switch (version) {
- case 0xca:
- fsid_type = FSID_DEV;
- break;
- case 1:
- break;
- default:
- goto retry;
- }
-
-
-
-
-
-
-
- if (!fsid_type_ok_for_exp(fsid_type, exp))
- goto retry;
- }
-
- else if (exp->ex_flags & NFSEXP_FSID) {
- fsid_type = FSID_NUM;
- } else if (exp->ex_uuid) {
- if (fhp->fh_maxsize >= 64) {
- if (is_root_export(exp))
- fsid_type = FSID_UUID16;
- else
- fsid_type = FSID_UUID16_INUM;
- } else {
- if (is_root_export(exp))
- fsid_type = FSID_UUID8;
- else
- fsid_type = FSID_UUID4_INUM;
- }
- }
-
- else if (!old_valid_dev(exp_sb(exp)->s_dev))
-
- fsid_type = FSID_ENCODE_DEV;
- else
- fsid_type = FSID_DEV;
- fhp->fh_handle.fh_version = version;
- if (version)
- fhp->fh_handle.fh_fsid_type = fsid_type;
- }
这个函数根据管理员导出文件系统时设置在文件/etc/exports中的信息确定了文件句柄中fsid的类型,这个函数会根据不同设置选择一种合适的fsid类型。
步骤3.设置文件句柄中的fb_fsid
- int len;
- fhp->fh_handle.fh_auth_type = 0;
- datap = fhp->fh_handle.fh_auth+0;
-
- mk_fsid(fhp->fh_handle.fh_fsid_type, datap, ex_dev,
- exp->ex_path.dentry->d_inode->i_ino,
- exp->ex_fsid, exp->ex_uuid);
这里有一些宏定义
- #define fh_version fh_base.fh_new.fb_version
- #define fh_fsid_type fh_base.fh_new.fb_fsid_type
- #define fh_auth_type fh_base.fh_new.fb_auth_type
- #define fh_fileid_type fh_base.fh_new.fb_fileid_type
- #define fh_auth fh_base.fh_new.fb_auth
- #define fh_fsid fh_base.fh_new.fb_auth
Linux中,NFS没有对文件句柄进行MD5认证,因此fh_auth_type固定为0,因此文件句柄中没有fh_auth字段,fh_auth是文件句柄中fh_auth的位置,fh_auth+0就是fh_fsid的位置指针了。文件句柄中的fh_fsid是通过函数mk_fsid()设置的,这是一个简单的函数,根据fh_fsid_type的值进行不同的设置,这个函数的定义如下:
- static inline void mk_fsid(int vers, u32 *fsidv, dev_t dev, ino_t ino,
- u32 fsid, unsigned char *uuid)
- {
- u32 *up;
- switch(vers) {
- case FSID_DEV:
- fsidv[0] = htonl((MAJOR(dev)<<16) |
- MINOR(dev));
- fsidv[1] = ino_t_to_u32(ino);
- break;
- case FSID_NUM:
- fsidv[0] = fsid;
- break;
- case FSID_MAJOR_MINOR:
- fsidv[0] = htonl(MAJOR(dev));
- fsidv[1] = htonl(MINOR(dev));
- fsidv[2] = ino_t_to_u32(ino);
- break;
-
- case FSID_ENCODE_DEV:
- fsidv[0] = new_encode_dev(dev);
- fsidv[1] = ino_t_to_u32(ino);
- break;
-
- case FSID_UUID4_INUM:
-
- up = (u32*)uuid;
- fsidv[0] = ino_t_to_u32(ino);
- fsidv[1] = up[0] ^ up[1] ^ up[2] ^ up[3];
- break;
-
- case FSID_UUID8:
-
- up = (u32*)uuid;
- fsidv[0] = up[0] ^ up[2];
- fsidv[1] = up[1] ^ up[3];
- break;
-
- case FSID_UUID16:
-
- memcpy(fsidv, uuid, 16);
- break;
-
- case FSID_UUID16_INUM:
-
- *(u64*)fsidv = (u64)ino;
- memcpy(fsidv+2, uuid, 16);
- break;
- default: BUG();
- }
- }
步骤4.设置fsid
fsid标识了文件系统中一个具体的文件,设置fsid的程序端如下:
- len = key_len(fhp->fh_handle.fh_fsid_type);
- datap += len/4;
-
- fhp->fh_handle.fh_size = 4 + len;
- if (inode)
- _fh_update(fhp, exp, dentry);
key_len()根据fh_fsid_type计算fsid的长度。fh_fsid_type不同,文件句柄中的fsid构成方式就不同,各种类型的fsid构成方式在函数mk_fsid()中。key_len()就是简单返回了各种fsid的长度,这个函数定义如下:
- static inline int key_len(int type)
- {
- switch(type) {
- case FSID_DEV: return 8;
- case FSID_NUM: return 4;
- case FSID_MAJOR_MINOR: return 12;
- case FSID_ENCODE_DEV: return 8;
- case FSID_UUID4_INUM: return 8;
- case FSID_UUID8: return 8;
- case FSID_UUID16: return 16;
- case FSID_UUID16_INUM: return 24;
- default: return 0;
- }
- }
实际组装文件句柄中fsid字段的函数是_fh_update(),这个函数定义如下:
参数fhp: 输入参数,组装出的文件句柄保存在这里
参数exp:文件所在文件系统的数据结构
参数dentry:文件的目录项结构
- static void _fh_update(struct svc_fh *fhp, struct svc_export *exp,
- struct dentry *dentry)
- {
-
- if (dentry != exp->ex_path.dentry) {
-
- struct fid *fid = (struct fid *)
- (fhp->fh_handle.fh_auth + fhp->fh_handle.fh_size/4 – 1);
-
-
- int maxsize = (fhp->fh_maxsize – fhp->fh_handle.fh_size)/4;
- int subtreecheck = !(exp->ex_flags & NFSEXP_NOSUBTREECHECK);
-
-
-
-
- fhp->fh_handle.fh_fileid_type =
- exportfs_encode_fh(dentry, fid, &maxsize, subtreecheck);
-
- fhp->fh_handle.fh_size += maxsize * 4;
- } else {
- fhp->fh_handle.fh_fileid_type = FILEID_ROOT;
- }
- }
代码中已经对函数流程做了注释,这里解析一下subtree_check。subtree_check是管理员导出文件系统时设置在/etc/exports中的一个配置项。管理员导出的一个NFS文件系统可能不是一个完成的文件系统,而只是文件系统中的一部分。比如根系统(/)安装在磁盘/dev/sda1上,这是一个完成的文件系统,而/tmp/nfs/root只是文件系统中的一个子树,管理员可以只导出这个文件子树。当客户端访问文件前,服务器端需要先检查客户端对文件的访问权限。如果设置了no_subtree_check,则服务器端只检查在导出文件系统中的访问权限就可以了,如果设置了subtree_check,则服务器端还需要检查上层路径的访问权限,也就是说服务器需要检查客户端对/tmp/nfs/、/tmp的访问权限。如果客户端具有/tmp/nfs/root/的访问权限,但是没有上层路径的访问权限,也不能访问/tmp/nfs/root/中的数据。
如果文件节点是导出文件系统的根节点,则fsid类型设置为FILEID_ROOT,fsid字段为空。如果不是根节点,则调用函数exportfs_encode_fh()组装fsid,这个函数位于fs/exportfs/expfs.c中,定义如下:
参数dentry: 文件的目录项结构
参数fid: 这就是文件句柄中fileid的缓存,只不过转换成struct fid结构了
参数max_len: 作为输入参数,这是fileid的最大长度;作为输出参数,这是fielid的实际长度
- int exportfs_encode_fh(struct dentry *dentry, struct fid *fid, int *max_len,
- int connectable)
- {
-
- const struct export_operations *nop = dentry->d_sb->s_export_op;
- int error;
- struct dentry *p = NULL;
-
- struct inode *inode = dentry->d_inode, *parent = NULL;
-
- if (connectable && !S_ISDIR(inode->i_mode)) {
- p = dget_parent(dentry);
-
-
-
-
- parent = p->d_inode;
- }
- if (nop->encode_fh)
- error = nop->encode_fh(inode, fid->raw, max_len, parent);
- else
- error = export_encode_fh(inode, fid, max_len, parent);
- dput(p);
-
- return error;
- }
export_operations是与文件系统相关的一个数据结构,这个数据结构中定义了构造和解析文件句柄中fileid字段的函数,这个数据结构的定义如下:
- struct export_operations {
- int (*encode_fh)(struct inode *inode, __u32 *fh, int *max_len,
- struct inode *parent);
- struct dentry * (*fh_to_dentry)(struct super_block *sb, struct fid *fid,
- int fh_len, int fh_type);
- struct dentry * (*fh_to_parent)(struct super_block *sb, struct fid *fid,
- int fh_len, int fh_type);
- int (*get_name)(struct dentry *parent, char *name,
- struct dentry *child);
- struct dentry * (*get_parent)(struct dentry *child);
- int (*commit_metadata)(struct inode *inode);
- };
EXT2文件系统中,这个数据结构的值如下:
- static const struct export_operations ext2_export_ops = {
- .fh_to_dentry = ext2_fh_to_dentry,
- .fh_to_parent = ext2_fh_to_parent,
- .get_parent = ext2_get_parent,
- };
struct fid结构的定义如下:
- struct fid {
- union {
- struct {
- u32 ino;
- u32 gen;
- u32 parent_ino;
- u32 parent_gen;
- } i32;
- struct {
- u32 block;
- u16 partref;
- u16 parent_partref;
- u32 generation;
- u32 parent_block;
- u32 parent_generation;
- } udf;
- __u32 raw[0];
- };
- };
这个结构中保存的就是文件句柄中的fileid。
exportfs_encode_fh()会检查文件系统是否定义了encode_fh()函数,如果没有定义就调用通用的组装函数export_encode_fh()。EXT2文件系统中没有定义这个函数,因此如果服务器端导出的是EXT2文件系统,则调用export_encode_fh()组装fileid。
- static int export_encode_fh(struct inode *inode, struct fid *fid,
- int *max_len, struct inode *parent)
- {
- int len = *max_len;
- int type = FILEID_INO32_GEN;
-
- if (parent && (len < 4)) {
- *max_len = 4;
- return 255;
- } else if (len < 2) {
- *max_len = 2;
- return 255;
- }
-
- len = 2;
- fid->i32.ino = inode->i_ino;
- fid->i32.gen = inode->i_generation;
- if (parent) {
- fid->i32.parent_ino = parent->i_ino;
- fid->i32.parent_gen = parent->i_generation;
- len = 4;
- type = FILEID_INO32_GEN_PARENT;
- }
- *max_len = len;
- return type;
- }
通过这个函数我们可以看到,如果服务器端设置了subtree_check,则fileid的类型是FILEID_INO32_GEN_PARENT,fileid长度为16字节,包含inode number(4字节)、generation number(4字节)、parent inode number(4字节)、parent generation number(4字节)。如果服务器端设置了no_subtree_check,则fileid的类型是FILEID_INO32_GEN,fileid长度为8字节,包含inode number(4字节)、generation number(4字节)。
最后再说说文件索引节点中的generation number。EXT2文件系统超级块结构ext2_sb_info中存在一个变量s_next_generation,每创建一个新文件,这个变量的值为赋给文件索引节点结构inode中的i_generation,同时s_next_generation加一。i_generation相等于文件的一个验证信息。如果用户删除了EXT2文件系统中一个文件,然后再创建一个同名文件,新创建文件的ino编号很可能和原文件ino编号相同,但是i_generation已经发生了变化。由于文件句柄中包含了i_generation,因此NFS文件系统可以检查出文件是否还是原来的文件,如果不是原来的文件,则NFS返回错误码NFS3ERR_STALE(NFSv3),表示文件句柄已经过期了。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/180695.html原文链接:https://javaforall.cn
【正版授权,激活自己账号】:
Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛
【官方授权 正版激活】:
官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...