NFS协议详解
概述
Sun Microsystems 公司于1984年推出了一个在整个计算机工业中被广泛接受的远程文件存取机制,它被称为 Sun 的网络文件系统(Network File System),或者简称为 NFS。该机制允许在一台计算机上运行一个服务器,使对其上的某些或所有文件都可以进行远程存取,还允许其他计算机上的应用程序对这些文件进行存取。
它使我们能够达到文件的共享。当使用者想用远端档案时只要用 “mount” 就可把 remote 档案系统挂接在自己的档案系统之下,使得远端的文件操作上和本地机器的文件没两样。一个应用程序可以打开 (Open) 一个远程文件以进行存取,可以从这个文件中读取 (Read) 数据,向该文件中写入 (Write) 数据,定位 (Seek) 到文件中的某个指定位置,最后当使用完毕后关闭(Close)该文件。
NFS 与 FTP
NFS协议中客户端可以透明地访问服务器中的文件系统,这不同于提供文件传输的FTP协议。FTP会产生文件一个完整的副本;NFS只访问一个进程引用文件部分,并且一个目的就是使得这种访问透明。这就意味着任何能够访问一个本地文件的客户端程序不需要做任何修改,就应该能够访问一个NFS文件。
NFS 协议简介
NFS 使用 SunRPC 构造的客户端/服务器应用程序,客户端通过向一台 NFS 服务器发送 SunRPC 请求来访问其中的文件。尽管这一工作可以使用一般的用户进程来实现,即 NFS 客户端可以是一个用户进程,对服务器进行显式调用,而服务器也可以是一个用户进程。因为两个理由,NFS 一般不这样实现。首先访问一个 NFS 文件必须对客户端透明,因此 NFS 的客户端调用是由客户端操作系统代表用户进程来完成的;其次,出于效率的考虑,NFS服务器在服务器操作系统中实现。如果NFS服务器是一个用户进程,每个客户端请求和服务器应答(包括读和写的数据)将不得不在内核和用户进程之间进行切换,这个代价太大。第3版的NFS协议在1993年发布,下图所示为一个NFS客户端和一台NFS服务器的典型结构。
- 访问一个本地文件还是一个NFS文件对于客户端来说是透明的,当文件被打开时,由内核决定这一点。文件被打开之后,内核将本地文件的所有引用传递给名为“本地文件访问”的框中,而将一个NFS文件的所有引用传递给名为“NFS客户端”的框中。
- NFS客户端通过其TCP/IP模块向NFS服务器发送RPC请求,NFS主要使用UDP,最新的实现也可以使用TCP。
- NFS服务器在端口2049接收作为UDP数据包的客户端请求,尽管NFS可以被实现为使用端口映射器,允许服务器使用一个临时端口,但是大多数实现都是直接指定UDP端口2049。
- 当NFS服务器收到一个客户端请求时,它将这个请求传递给本地文件访问例程,然后访问服务器主机上的一个本地的磁盘文件。
- NFS服务器需要花一定的时间来处理一个客户端的请求,访问本地文件系统一般也需要一部分时间。在这段时间间隔内,服务器不应该阻止其他客户端请求。为了实现这一功能,大多数的NFS服务器都是多线程的——服务器的内核中实际上有多个NFS服务器在NFS本身的加锁管理程序中运行,具体实现依赖于不同的操作系统。既然大多数UNIX内核不是多线程的,一个共同的技术就是启动一个用户进程(常被称为“nfsd”)的多个实例。这个实例执行一个系统调用,使其作为一个内核进程保留在操作系统的内核中。
- 在客户端主机上,NFS客户端需要花一定的时间来处理一个用户进程的请求。NFS客户端向服务器主机发出一个RPC调用,然后等待服务器的应答。为了给使用NFS的客户端主机上的用户进程提供更多的并发性,在客户端内核中一般运行着多个NFS客户端,同样具体实现也依赖于操作系统。
NFS的工作原理和服务进程的作用
在Linux中,NFS和服务进程是两个不同的概念,但它们确实紧密联系在一起。首先,先介绍NFS的工作原理。
NFS的工作原理
启动NFS文件服务器时,/etc/rc.local会自动启动exportfs程序,指定可以导出的文件或目录,而所能挂载的也只能是其所指定的目录。 NFS是基于XDR/RPC协议的。XDR(eXternal Data Representation,即外部数据表示法)提供一种方法,把数据从一种格式转换成另一种标准数据格式表示法,确保在不同的计算机、操作系统及程序语言中,所有数据代表的意义都是相同的。 RPC(Remote Procedure Call,远程程序调用)请求远程计算机给予服务。客户机通过网络传送RPC到远程计算机,请求服务。 NFS运用RPC传送数据的方法有以下几步: 1) 客户送出信息,请求服务。 2) 客户占位程序把客户送出的参数转换成XDR标准格式,并用系统调用把信息送到网络上。 3) 信息经过网络送达远程主机系统。 4) 远程主机将接受到的信息传给服务器占位程序。 5) 把XDR形式的数据,转换成符合主机端的格式,取出客户发出的服务请求参数,送给服务器。 6) 服务器给客户发送服务的逆向传送过程。
服务进程的作用
服务进程是系统在启动计算机后自动运行的程序,包括对网络的连接、网络协议的加载、图形桌面的显示、文件系统的加载等,linux系统中常见的进程包括以下几种。 1)nfsd 据客户端对文件系统的需求,启动文件系统请求服务进程,响应客户的请求,而一般文件系统请求服务进程的数目是8,这也是在rc.local中写nfsd 8 &的原因。 2)biod 进程是在NFS客户端上用的,用来启动异步块I/O服务进程来建立Buffer Cache,处理在客户机上的读写。(3)mountd 是个RPC服务器。启动rpc.mountd服务进程后,mountd会读取/etc/xtab查看哪一台客户机正在挂载哪一个文件系统,并回应客户机所要挂载的路径。 4)inetd Internet services服务进程 系统启动时,rc.local会启动inetd读取inetd.conf配置文件,读取网络上所有服务器的地址,链接启动inetd.conf中所有的服务器。当客户机请求服务时,inetd就会启动相关的服务进程,如user使用telnet时,inetd启动telnetd配合user telnet的需求,其余像ftp、finger、rlogin等应用程序,inetd也都会启动相对应的服务程序ftpd、fingerd、rloingd等。 5)portmap服务程序 主要功能是将TCP/IP通信协议的端口数字转换成RPC程序数字,因为这样客户端才能进行RPC调用。一般RPC服务器是被inet启动的,所以portmap必须在inetd之前启动,否则无法进行RPC调用。
NFS服务器之RPC
因为NFS支持的功能相当多,而不同的功能都会使用不同的程序来启动。每启动一个功能就会启用一些端口来传输数据,因此NFS的功能所对应的端口才没有固定,而是采用随机取用一些未被使用的小于724的端口来作为传输之用。但如此一来又造成客户端要连接服务器时的困扰,因为客户端要知道服务器端的相关端口才能够联机,此时我们需要远程过程调用(RPC)的服务。RPC最主要的功能就是指定每个NFS功能所对应的端口号,并且回报给客户端,让客户端可以连接到正确的端口上。当服务器在启动NFS时会随机选用数个端口,并主动地向RPC注册。因此RPC可以知道每个端口对应的NFS功能。然后RPC固定使用端口111来监听客户端的请求并回报客户端正确的端口,所以可以让NFS的启动更为容易。注意,启动NFS之前,要先启动RPC;否则NFS会无法向RPC注册。另外,重新启动RPC时原本注册的数据会不见,因此RPC重新启动后它管理的所有程序都需要重新启动以重新向RPC注册。
当客户端有NFS文件要存取请求时,它如何向服务器端要求数据? 1) 客户端会向服务器端的RPC(port 111)发出NFS文件存取功能的询问请求。 2) 服务器端找到对应的已注册的NFS daemon端口后会回报给客户端。 3) 客户端了解正确的端口后,就可以直接与NFS守护进程来联机。
由于NFS的各项功能都必须要向RPC注册,因此RPC才能了解NFS服务的各项功能的port number、PID和NFS在主机所监听的IP等,而客户端才能够通过RPC的询问找到正确对应的端口。即NFS必须要有RPC存在时才能成功地提供服务,因此我们称NFS为RPC Server的一种。事实上,有很多这样的服务器都向RPC注册。例如,NIS(Network Information Service)也是RPC Server的一种。所以如下图所示,不论是客户端还是服务器端,要使用NFS都需要启动RPC。
NFS协议从诞生到现在为止,已经有多个版本,如NFS V2(rfc794)及NFS V3(rfc1813)(最新的版本是V4(rfc307))。最早,SUN公司曾将NFS V2设计为只使用UDP,主要原因是当时机器的内存、网络速度和CPU的影响,不得不选择对机器负担较轻的方式。而到了NFS V3,SUN公司选择了TCP作为默认的传输方式。V3相对V2的主要区别如下: 1) 文件尺寸:V2最大只支持32位的文件大小(4 GB),而V3新增加了支持64位文件大小的技术 2) 文件传输尺寸:V3没有限定传输尺寸,V2最多只能设定为8 KB,可以使用-rsize and -wsize来设定 3) 返回完整的信息:V3增加和完善了返回错误和成功信息,对于服务器的设置和管理能带来很大好处 4) 增加了对TCP传输协议的支持:V2只提供了对UDP的支持,在一些高要求的网络环境中有很大限制;V3增加了对TCP的支持。UDP有着传输速度快且非连接传输的便捷特性,但是在传输上没有TCP稳定。当网络不稳定或者黑客入侵时很容易使NFS的性能大幅度降低,甚至使网络瘫痪。所以对于不同情况,网络要有针对性地选择传输协议。NFS的默认传输协议是UDP,然而RHEL 4.0内核提供了对通过TCP的NFS的支持。要通过TCP来使用NFS,在客户端系统上挂载NFS导出的文件系统时包括一个“-o tcp”选项。使用TCP的优点和缺点如下: 1)被提高了的连接持久性,因此获得的NFS stale file handles消息就会较少。 2)载量较大的网络的性能会有所提高,因为TCP确认每个分组,而UDP只在完成时才确认。 3)TCP具有拥塞控制技术(UDP根本没有),在一个拥塞情况严重的网络上,UDP分组是被首先撤销的类型。使用UDP意味着,如果NFS正在写入数据(单元为8 KB的块),所有这8 KB数据都需要被重新传输。由于TCP的可靠性,8 KB数据中只有一部分需要重新传输。 4)错误检测。当TCP连接中断(由于服务器停止),客户端就会停止发送数据而开始重新连接。UDP是无连接的,使用它的客户端就会继续给网络发送数据直到服务器重新上线为止。 5)TCP的费用在性能方面的提高并不显著。 (5)异步写入特性。 (6)改进了服务器的mount性能。 (7)有更好的I/O写性能。 (8)更强的网络运行效能,使得网络运行更为有效。 (9)更强的灾难恢复功能。
在Linux上,UDP是默认使用的协议。作为服务器别无选择。但作为客户端,可以使用TCP和其他使用TCP的UNIX NFS服务器互联。在局域网中使用UDP较好,因为局域网有比较稳定的网络保证。使用UDP可以带来更好的性能,Linux默认使用V2,但是也可以通过mount option的nfsvers=n选择。NFS使用TCP/IP提供的协议和服务运行于OSI层次模型的应用层,如表所示。
源码分析
挂载目录
1. NFS 目录导出
NFS服务器的设定可以通过 /etc/exports 这个文件进行,设定格式如下:
vim /etc/exports
/tmp *(rw,no_root_squash)
/home/public 192.168.0.*(rw) *(ro)
/home/test 192.168.0.100(rw)
/home/linux *.the9.com(rw,all_squash,anonuid=40,anongid=40)
可以设定的参数主要有以下这些:
rw:可读写的权限;
ro:只读的权限;
no_root_squash:登入到NFS主机的用户如果是root,该用户即拥有root权限;
root_squash:登入NFS主机的用户如果是root,该用户权限将被限定为匿名使用者nobody;
all_squash:不管登陆NFS主机的用户是何权限都会被重新设定为匿名使用者nobody。
anonuid:将登入NFS主机的用户都设定成指定的user id,此ID必须存在于/etc/passwd中。
anongid:同anonuid,但是变成group ID就是了!
sync:资料同步写入存储器中。
async:资料会先暂时存放在内存中,不会直接写入硬盘。
insecure:允许从这台机器过来的非授权访问。
设定好后可以使用以下命令启动NFS:
$ /etc/rc.d/init.d/portmap start (在REDHAT中PORTMAP是默认启动的)
$ /etc/rc.d/init.d/nfs start
Starting NFS services: [ OK ]
Starting NFS quotas: [ OK ]
Starting NFS daemon: [ OK ]
Starting NFS mountd: [ OK ]
# 如果我们在启动了NFS之后又修改了/etc/exports,是不是还要重新启动nfs呢?这个时候我们就可以用exportfs命令来使改动# 立刻生效,该命令格式如下:
# exportfs [-aruv]
# -a :全部mount或者unmount /etc/exports中的内容
# -r :重新mount /etc/exports中分享出来的目录
# -u :umount 目录
# -v :在 export 的时候,将详细的信息输出到屏幕上。
# 全部重新export
$ exportfs -rv
exporting 192.168.0.100:/home/test
exporting 192.168.0.*:/home/public
exporting *.the9.com:/home/linux
exporting *:/home/public
exporting *:/tmp
reexporting 192.168.0.100:/home/test to kernel
# 全部都卸载了
$ exportfs -au
二、NFS客户端的操作: 1、showmout命令对于NFS的操作和查错有很大的帮助,所以我们先来看一下showmount的用法 showmout -a :这个参数是一般在NFS SERVER上使用,是用来显示已经mount上本机nfs目录的cline机器。 -e :显示指定的NFS SERVER上export出来的目录。 例如: showmount -e 192.168.0.30 Export list for localhost: /tmp * /home/linux *.linux.org /home/public (everyone) /home/test 192.168.0.100 2、mount nfs目录的方法: mount -t nfs hostname(orIP):/directory /mount/point 具体例子: Linux: mount -t nfs 192.168.0.1:/tmp /mnt/nfs [root@localhost /]# showmount -e 192.168.0.169 Export list for 192.168.0.169: /home/opt/RHEL4U5 192.168.0.0/255.255.252.0 You have new mail in /var/spool/mail/root
mount -t nfs 192.168.0.169:/home/opt/RHEL4U5 /mnt/soft
FileSystem
nfs 文件系统的源文件在 fs/nfs目录下。
/*
* Register the NFS filesystems
*/
int __init register_nfs_fs(void)
{
int ret;
ret = register_filesystem(&nfs_fs_type);
if (ret < 0)
goto error_0;
ret = register_nfs4_fs();
if (ret < 0)
goto error_1;
ret = nfs_register_sysctl();
if (ret < 0)
goto error_2;
ret = register_shrinker(&acl_shrinker, "nfs-acl");
if (ret < 0)
goto error_3;
#ifdef CONFIG_NFS_V4_2
nfs_ssc_register_ops();
#endif
return 0;
error_3:
nfs_unregister_sysctl();
error_2:
unregister_nfs4_fs();
error_1:
unregister_filesystem(&nfs_fs_type);
error_0:
return ret;
}
/*
* Unregister the NFS filesystems
*/
void __exit unregister_nfs_fs(void)
{
unregister_shrinker(&acl_shrinker);
nfs_unregister_sysctl();
unregister_nfs4_fs();
#ifdef CONFIG_NFS_V4_2
nfs_ssc_unregister_ops();
#endif
unregister_filesystem(&nfs_fs_type);
}
mount
/**
* nfs_mount - Obtain an NFS file handle for the given host and path
* @info: pointer to mount request arguments
* @timeo: deciseconds the mount waits for a response before it retries
* @retrans: number of times the mount retries a request
*
* Uses timeout parameters specified by caller. On successful return, the
* auth_flavs list and auth_flav_len will be populated with the list from the
* server or a faked-up list if the server didn't provide one.
*/
int nfs_mount(struct nfs_mount_request *info, int timeo, int retrans)
{
struct rpc_timeout mnt_timeout;
struct mountres result = {
.fh = info->fh,
.auth_count = info->auth_flav_len,
.auth_flavors = info->auth_flavs,
};
struct rpc_message msg = {
.rpc_argp = info->dirpath,
.rpc_resp = &result,
};
struct rpc_create_args args = {
.net = info->net,
.protocol = info->protocol,
.address = (struct sockaddr *)info->sap,
.addrsize = info->salen,
.timeout = &mnt_timeout,
.servername = info->hostname,
.program = &mnt_program,
.version = info->version,
.authflavor = RPC_AUTH_UNIX,
.cred = current_cred(),
};
struct rpc_clnt *mnt_clnt;
int status;
dprintk("NFS: sending MNT request for %s:%s\n",
(info->hostname ? info->hostname : "server"),
info->dirpath);
if (strlen(info->dirpath) > MNTPATHLEN)
return -ENAMETOOLONG;
if (info->noresvport)
args.flags |= RPC_CLNT_CREATE_NONPRIVPORT;
nfs_init_timeout_values(&mnt_timeout, info->protocol, timeo, retrans);
mnt_clnt = rpc_create(&args);
if (IS_ERR(mnt_clnt))
goto out_clnt_err;
if (info->version == NFS_MNT3_VERSION)
msg.rpc_proc = &mnt_clnt->cl_procinfo[MOUNTPROC3_MNT];
else
msg.rpc_proc = &mnt_clnt->cl_procinfo[MOUNTPROC_MNT];
status = rpc_call_sync(mnt_clnt, &msg, RPC_TASK_SOFT|RPC_TASK_TIMEOUT);
rpc_shutdown_client(mnt_clnt);
if (status < 0)
goto out_call_err;
if (result.errno != 0)
goto out_mnt_err;
dprintk("NFS: MNT request succeeded\n");
status = 0;
/*
* If the server didn't provide a flavor list, allow the
* client to try any flavor.
*/
if (info->version != NFS_MNT3_VERSION || *info->auth_flav_len == 0) {
dprintk("NFS: Faking up auth_flavs list\n");
info->auth_flavs[0] = RPC_AUTH_NULL;
*info->auth_flav_len = 1;
}
out:
return status;
out_clnt_err:
status = PTR_ERR(mnt_clnt);
dprintk("NFS: failed to create MNT RPC client, status=%d\n", status);
goto out;
out_call_err:
dprintk("NFS: MNT request failed, status=%d\n", status);
goto out;
out_mnt_err:
dprintk("NFS: MNT server returned result %d\n", result.errno);
status = result.errno;
goto out;
}
/**
* nfs_umount - Notify a server that we have unmounted this export
* @info: pointer to umount request arguments
*
* MOUNTPROC_UMNT is advisory, so we set a short timeout, and always
* use UDP.
*/
void nfs_umount(const struct nfs_mount_request *info)
{
static const struct rpc_timeout nfs_umnt_timeout = {
.to_initval = 1 * HZ,
.to_maxval = 3 * HZ,
.to_retries = 2,
};
struct rpc_create_args args = {
.net = info->net,
.protocol = IPPROTO_UDP,
.address = (struct sockaddr *)info->sap,
.addrsize = info->salen,
.timeout = &nfs_umnt_timeout,
.servername = info->hostname,
.program = &mnt_program,
.version = info->version,
.authflavor = RPC_AUTH_UNIX,
.flags = RPC_CLNT_CREATE_NOPING,
.cred = current_cred(),
};
struct rpc_message msg = {
.rpc_argp = info->dirpath,
};
struct rpc_clnt *clnt;
int status;
if (strlen(info->dirpath) > MNTPATHLEN)
return;
if (info->noresvport)
args.flags |= RPC_CLNT_CREATE_NONPRIVPORT;
clnt = rpc_create(&args);
if (IS_ERR(clnt))
goto out_clnt_err;
dprintk("NFS: sending UMNT request for %s:%s\n",
(info->hostname ? info->hostname : "server"), info->dirpath);
if (info->version == NFS_MNT3_VERSION)
msg.rpc_proc = &clnt->cl_procinfo[MOUNTPROC3_UMNT];
else
msg.rpc_proc = &clnt->cl_procinfo[MOUNTPROC_UMNT];
status = rpc_call_sync(clnt, &msg, 0);
rpc_shutdown_client(clnt);
if (unlikely(status < 0))
goto out_call_err;
return;
out_clnt_err:
dprintk("NFS: failed to create UMNT RPC client, status=%ld\n",
PTR_ERR(clnt));
return;
out_call_err:
dprintk("NFS: UMNT request failed, status=%d\n", status);
}
Client Side
Client Side 的头文件在 include/linux/ 下面,C 文件在 fs/nfs 下面。
dir.c/file.c/inode.c/symlink.c/unlink.c:与文件操作相关的系统调用 read.c/write.c/flushd.c:文件读写 mount_clnt.c/nfs_root.c:将 NFS 文件系统作为 root 目录的相关实现 proc.c/nfs2xdr.c/nfs3proc.c/nfs3xdr.c:网络数据交换
Server Side
Server Side 的头文件在 include/linux/nfsd 下面,C 文件在 fs/nfsd 下面。
auth.c/lockd.c/export.c/nfsctl.c/nfscache.c/nfsfh.c/stats.c:导出目录的访问管理 nfssvc.c:NFS 服务 deamon 的实现 vfs.c:将 NFS 文件系统的操作转换成具体文件系统的操作 nfsproc.c/nfsxdr.c/nfs3proc.c/nfs3xdr.c:网络数据交换 导出目录的访问管理主要解决网络文件系统实现面临的几个重要问题,包括目录导出服务,外部访问的权限控制,多客户端以及客户端与服务器的文件并发操作等。