NFS Linux 网络文件系统入门到实践

NFS:Linux 网络文件系统入门到实践

网络文件系统(NFS)

介绍

NFS的全称是Network FileSystem,即网络文件系统,NFS主要实现的功能是让网络上的不同操作系统之间共享数据。NFS首先在远程服务端(共享数据的操作系统)共享出文件或者目录,然后远端共享出来的文件或者目录就可以通过挂载(mount)的方式挂接到本地的不同操作系统上,最后,本地系统就可以很方便的使用远端提供的文件服务,操作起来像在本地操作一样。从而实现了数据的共享。
在这里插入图片描述

NFS服务器的主要进程

  • rpc.nfsd 进程
    NFS服务的主进程,主要管理客户端是否能够接入NFS服务器以及数据的传输,该进程固定监听TCP/UDP 2049端口号
  • rpc.mountd 进程
    管理和维护NFS文件系统,根据所设定的权限决定是否允许客户端挂载指定的共享目录,该进程监听的端口号默认是不固定的。
  • rpc.lockd进程
    提供文件锁功能,防止多个客户端同时写入一个文件,该进程监听的端口默认是不固定的。
  • rpc.statd进程 RPC 的端口映射器进程,监听UDP111端口。

注意:2049端口UDP和TCP只能选择其中一种协议方式

NFS的特性:

  • NFS的安全性方面
    NFS默认没有加密,且仅依靠IP地址或主机名来决定是否运行客户端挂载指定的共享目录,但是NFS可以通过Kerberos进行认证及加密
  • 共享资源的属主、属组和权限方面:
    NFS服务器和客户端通过UID和GID来识别共享资源的所有者信息,当客户端挂载NFS共享目录时,共享目录中资源的UID和GID将与服务器上面的保持一致;而客户端会将UID和GID映射到客户端上所对应的用户名和组名。
    例如:

服务器上有名为server的用户,其UID为111:客户端上有名为client的用户,其UID同为111。那么,UID为111的共享资源在服务器上的属主是server;在客户端上的属主则是client。这样的话可以继承服务器的gid属组acl与权限的问题。
那么如果UID或GID没有对应的用户或组时,那么用户名或组名将直接以UID或GID表示。例如:客户端上有名为client的用户,其UID为222:而服务端上没有UID为222的用户。那么,UID为222的共享资源在服务器上的属主将直接用222表示:在客户端上的属主则是client,NFS服务器与客户端上共享资源的权限及ACL信息(若支持)将保持一致I

NFS服务端部署

角色 主机名 IP 地址 操作系统版本
NFS-Server centos-manager 10.201.9.134 CentOS Linux 7
client1 node1 10.201.9.133 CentOS Linux 7
client2 node2 10.201.9.140 CentOS Linux 7
  • 检查NFS-Server上是否已经存在NFS服务命令如下:
    rpm -qa | grep nfs
1
2
3
[root@centos-manager ~]# rpm -qa | grep nfs
libnfsidmap-0.25-19.el7.x86_64
nfs-utils-1.3.0-0.68.el7.2.x86_64

如果存在nfs-utils的软件包说明已经安装了nfs服务了,无需再继续安装。没有的话,可以使用yum安装

  • NFS配置文件
    /etc/exports,这是nfs的配置文件,打开之后是空白的,需要自行编辑

    基本语法:
    <共享目录> <客户端1>(选项1,选项2,...) <客户端2>(选项3,...) ...

客户端表达方式示例:

表达式 含义说明
10.201.9.133 单个 IP 地址
10.201.9.0/24 整个子网(CIDR 表示法)
*.example.com 所有 example.com 域中的主机
client1.local 指定主机名(需 DNS 或 /etc/hosts 解析)
* 所有客户端(不推荐,存在安全风险)

常用导出选项:

选项 说明
rw 允许客户端读写(默认是只读 ro
ro 只读访问
sync 同步写入(数据写入磁盘后才返回确认,更安全,默认行为)
async 异步写入(性能更好,但断电可能丢数据)
no_root_squash 允许客户端 root 用户保留 root 权限(高危!慎用)
root_squash 将客户端 root 映射为匿名用户(如 nfsnobody),默认启用
all_squash 所有客户端用户(包括 root)都映射为匿名用户
anonuid=xxx 指定匿名用户的 UID(配合 all_squashroot_squash 使用)
anongid=xxx 指定匿名用户的 GID
no_subtree_check 禁用子树检查(提升性能,适用于整个文件系统被共享的情况)
subtree_check 启用子树检查(默认,但可能影响性能)
no_all_squash 保留共享文件的UID和GID(默认)
  • 配置实例
1
2
3
4
5
[root@centos-manager ~]# cat /etc/exports
# tmp
/tmp *(rw,all_squash,anonuid=65534,anongid=65534,no_subtree_check)
/root 10.201.9.140(rw,no_root_squash) 10.201.9.133(ro,no_root_squash)
/netaccess 10.201.9.140(rw,no_all_squash,async,anonuid=65534,anongid=65534,no_subtree_check)

解释:

/tmp *(rw,all_squash,anonuid=65534,anongid=65534,no_subtree_check) 任何网络中的主机都能挂载并读写该共享(因使用了 *),无论客户端以什么用户(包括 root)挂载并操作 /tmp 共享目录,所有新建/修改的文件在 NFS 服务器上都显示为 属主 UID=65534, GID=65534。

/root 10.201.9.140(rw,no_root_squash) 10.201.9.133(ro,no_root_squash) 将 /root 目录共享,允许 10.201.9.140 读写、10.201.9.133 只读,且两者均保留 root 权限(no_root_squash)

该配置将 /netaccess 目录以异步读写方式共享给 10.201.9.140,且匿名用户被设为 UID/GID 65534。

当前我的系统是没有netaccess目录的,所以需要创建一个

1
2
3
4
5
[root@centos-manager ~]# mkdir /netaccess
# 因为我们在nfs的配置文件中,配置netaccess 目录的指定匿名用户的 UID和GID是65535.所以在创建文件的时候需要把这个目录的归属组和归属主设置为65534
[root@centos-manager ~]# chown 65534:65534 /netaccess/
[root@centos-manager ~]# ll -d /netaccess/
drwxr-xr-x 2 nfsnobody nfsnobody 6 124 13:16 /netaccess/

在 Linux 系统中,UID(用户 ID)65534 通常是一个特殊的保留 UID,用于表示 “nobody” 或 “nfsnobody” 用户,这个用户是 NFS 服务在启用 root_squash 或 all_squash 时,用来映射远程客户端用户的匿名用户。它没有登录 shell,也不属于任何特权组,权限极低,仅用于安全隔离。

  • 启动NFS服务
    命令:systemctl start nfs
    检查端口是否启动,主要检查rpcbing的UDP111端口和nfsd的2049端口
1
2
3
4
5
6
7
root@centos-manager ~]# ss  -luntp | grep 111
udp UNCONN 0 0 *:111 *:* users:(("rpcbind",pid=706,fd=6))
udp UNCONN 0 0 :::111 :::* users:(("rpcbind",pid=706,fd=9))

[root@centos-manager ~]# ss -luntp | grep 2049
tcp LISTEN 0 64 *:2049 *:*
tcp LISTEN 0 64 :::2049 :::*

可以使用exportfs -arv 查看当前服务共享出去的目录

1
2
3
4
5
[root@centos-manager ~]# exportfs  -arv
exporting 10.201.9.140:/netaccess
exporting 10.201.9.140:/root
exporting 10.201.9.133:/root
exporting *:/tmp

客户端挂载

在客户端上可以使用showmount -e nfs服务器IP地址 可以查看nfs服务器输出目录,showmount 命令是依赖于nfs-utils软件包的,如果没有这个命令的话,需要使用yum -y install nfs-utils 进行安装一下客户端上只要安装即可,不需要启动nfs服务

1
2
3
4
5
6
# 可以看到nfs服务器共享出来的可以挂载的目录
[root@node2 ~]# showmount -e 10.201.9.134
Export list for 10.201.9.134:
/tmp *
/netaccess 10.201.9.140
/root 10.201.9.133,10.201.9.140
  • 挂载

挂载tmp目录

1
mount -t nfs4 -o noatime,rsize=1048576,wsize=1048576,proto=tcp 10.201.9.134:/tmp /tmp

noatime 挂载时不更新文件访问时间
rsize 读取块大小为 1 MB
wsize 写入块大小 为1MB

挂载之后使用df -h 命令查看挂载情况

1
2
3
4
5
6
7
8
9
10
[root@node2 ~]# df -h
文件系统 容量 已用 可用 已用% 挂载点
/dev/mapper/centos-root 37G 29G 9.0G 76% /
devtmpfs 1.8G 0 1.8G 0% /dev
tmpfs 1.8G 0 1.8G 0% /dev/shm
tmpfs 1.8G 12M 1.8G 1% /run
tmpfs 1.8G 0 1.8G 0% /sys/fs/cgroup
/dev/sda1 1014M 130M 885M 13% /boot
tmpfs 367M 0 367M 0% /run/user/0
10.201.9.134:/tmp 57G 42G 16G 73% /tmp

可以看到tmp目录已经成功挂载到了nfs主机上的/tmp目录上了

测试
在node2的客户端上挂载的tmp目录创建一个文件,删除一个文件,并在nfs服务端查看

1
2
3
4
5
[root@node2 tmp]# pwd
/tmp
[root@node2 tmp]#
[root@node2 tmp]# touch nfs-140.txt
[root@node2 tmp]# touch nfs-141.txt
1
2
3
4
[root@centos-manager tmp]# ll
总用量 0
-rw-r--r-- 1 nfsnobody nfsnobody 0 124 14:05 nfs-140.txt
-rw-r--r-- 1 nfsnobody nfsnobody 0 124 14:05 nfs-141.txt

可以看到我们在客户端挂载的tmp目录中,创建了两个txt的文件,在nfs服务器中也能正常看到,并且他的归属组和归属主是nfsnobody,这是因为我们在nfs服务器上面挂载的时候配置了all_squash(将客户端 root 映射为匿名用户),并且指定了匿名用户的gid和uid为65534,65534这个id对于的就是nfsnobody用户。

挂载root目录

1
2
3
4
[root@node2 tmp]# mount -t nfs4 -o noatime,rsize=1048576,wsize=1048576,proto=tcp 10.201.9.134:/root /root
[root@node2 tmp]# df -h /root/
文件系统 容量 已用 可用 已用% 挂载点
10.201.9.134:/root 57G 42G 16G 73% /root

测试

1
2
[root@node2 tmp]# cd /root/
[root@node2 ~]# touch up140.test
1
2
[root@centos-manager ~]# ll up140.test
-rw-r--r-- 1 root root 0 124 14:12 up140.test

在客户端挂载的root目录创建一个文件,在nfs服务器同样可以正常看到,只是该文件的归属组和归属主没有映射为匿名用户,而是直接显示的root,因为我们在配置挂载的时候启用了这个参数no_root_squash(允许客户端 root 用户保留 root 权限)

在10.201.9.133也就是node1上挂载测试

1
/root 10.201.9.140(rw,no_root_squash) 10.201.9.133(ro,no_root_squash)

可以看到我们在配置root目录共享的时候,133是只有只读权限的。

1
2
3
4
[root@node1 tmp]# mount -t nfs4 -o noatime,rsize=1048576,wsize=1048576,proto=tcp 10.201.9.134:/root /root
[root@node1 tmp]# df -h /root
文件系统 容量 已用 可用 已用% 挂载点
10.201.9.134:/root 57G 42G 16G 73% /root
1
2
3
4
5
[root@node1 ~]# pwd
/root
[root@node1 ~]# touch up-133.txt
touch: 无法创建"up-133.txt": 只读文件系统
[root@node1 ~]#

因为没有写入权限,所以即使是root权限也是无法写入的。

客户端自动挂载

如果想要是自动挂载的话,这个时候就需要使用到/etc/fastab文件了,在10.201.9.133 也就是node2上自动挂载root目录进行举例

1
2
[root@node1 ~]# vi /etc/fstab
10.201.9.134:/root /root nfs auto,ro,vers=4,hard,intr,tcp,rsize=32768,wsize=32768 0 0

挂载选项详解

选项 说明
auto 允许通过 mount -a 自动挂载(默认行为,通常可省略)
ro 只读挂载(Read-Only),客户端无法修改内容
vers=4 使用 NFSv4 协议
hard 硬挂载:若服务器无响应,客户端会持续重试(保证数据一致性,但可能导致进程卡住)
intr 允许用 Ctrl+C 中断因服务器无响应而挂起的操作(在 hard 模式下有效)
注:Linux 内核 2.6.25+ 后此选项已废弃,但保留兼容性
tcp 使用 TCP 作为传输协议(NFSv4 默认即 TCP,可省略)
rsize=32768 读取缓冲区大小为 32KB(合理值,常见范围 32K–1M)
wsize=32768 写入缓冲区大小为 32KB(同上)

hard + intr(旧内核)曾用于允许中断挂起操作,但 Linux 2.6.25+ 后 intr 已废弃,hard 模式下信号(如 Ctrl+C)默认可中断。

测试
在node2中将root目录进行卸载

1
2
3
4
[root@node1 tmp]# umount  /root/
[root@node1 tmp]# df -h /root
文件系统 容量 已用 可用 已用% 挂载点
/dev/mapper/centos-root 37G 29G 8.7G 77% /

使用mount -a 再继续重新挂载,查看是否能够重新挂载回来,如果可以挂载回来说明自动挂载配置成功

1
2
3
4
5
[root@node1 tmp]# mount -a
# 执行mount -a 之后挂载正常
[root@node1 tmp]# df -h /root
文件系统 容量 已用 可用 已用% 挂载点
10.201.9.134:/root 57G 42G 16G 73% /root

autofs自动挂载服务

autofs 是 Linux 中一个 自动挂载文件系统 的服务,特别适用于按需挂载 NFS、CIFS、本地目录等,在访问时自动挂载,空闲一段时间后自动卸载,避免系统启动时因网络存储不可用而卡住,也节省资源。

autofs自动挂载服务优势

  1. 可以隐藏父目录,提高安全性。
  2. 完美解决由于网络故障而导致服务器宕机的情况。
  3. 高并发状态下可以大量节约网络与服务器资源。

autofs需要从/etc/auto.master文件中读取配置信息。该文件中可以同时指定多个挂载点,由autofs来挂载文件系统

在这里插入图片描述
如图:我需要将NFS的/netaccess目录使用autofs挂载到NFSClient的/web目录中的话,就需要再NFSClient中安装一个autofs的服务,并且编写配置文件

在NFSServer上的/netaccess中创建一个web目录

1
2
3
[root@centos-manager netaccess]# mkdir web
[root@centos-manager netaccess]# cd web/
[root@centos-manager web]# touch 1.txt
  • 查看NFSClinet系统是否已经安装autofs。
1
2
3
# 如果没有输出的话,就是没有安装,需要使用yum安装一下
[root@node1 tmp]# rpm -qa | grep autofs
[root@node1 tmp]# yum -y install autofs
  • autofs配置文件

    主配置文件:/etc/auto.master 定义 autofs 管理的挂载点根目录及其对应的子映射文件或程序
    子映射文件:/etc/auto.(如 /etc/auto.nfs) 定义具体挂载项,即在父目录下创建哪些子目录、挂载什么资源、使用什么选项。
    特殊映射文件:/etc/auto.misc 用途:autofs 默认提供的示例文件,包含常见挂载模板(CD-ROM、软盘等)

  • 修改配置文件
1
2
3
4
5
[root@node1 tmp]# vi  /etc/auto.master
# 这一行是模板文件
/misc /etc/auto.misc
# 添加这一行,对/web目录的挂载定义配置文件
/web /etc/auto.web

将模板文件复制并重命名为自定义的配置文件并进行修改

1
[root@node1 tmp]# cp /etc/auto.misc   /etc/auto.web

在这里插入图片描述

1
2
# 在/etc/auto.web 添加这一行
* -rw,soft 10.201.9.134:/netaccess/&

这里的soft的含义是使用“使用软挂载模式”,当 NFS 服务器无响应(如宕机、网络中断)时,客户端会在重试若干次后直接返回 I/O 错误,应用程序会立即收到错误,而不是无限等待。

  • 启动autofs服务
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 当启动了autofs服务之后,会在/ 目录下自动创建一个web的目录
[root@node1 ~]# systemctl start autofs
[root@node1 ~]# ls /web
[root@node1 ~]# df -h /web
文件系统 容量 已用 可用 已用% 挂载点
/etc/auto.web 0 0 0 - /web
# 使用df -h 查看是没有web目录挂载的。
[root@node1 ~]# df -h
文件系统 容量 已用 可用 已用% 挂载点
/dev/mapper/centos-root 37G 29G 8.7G 77% /
devtmpfs 1.8G 0 1.8G 0% /dev
tmpfs 1.8G 0 1.8G 0% /dev/shm
tmpfs 1.8G 12M 1.8G 1% /run
tmpfs 1.8G 0 1.8G 0% /sys/fs/cgroup
/dev/sda1 1014M 130M 885M 13% /boot
tmpfs 367M 0 367M 0% /run/user/0

因为在编写/etc/auto.web的时候使用了autofs 通配符挂载配置。要触发挂载,需要访问 /web 下的某个具体子目录,但是这个目录必须是要NFSServer共享目录中所存在的,在之前我们已经在netaccess目录中创建了一个web目录了,可以访问web目录触发挂载

  • 测试
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[root@node1 ~]# cd /web/
[root@node1 web]# ls
[root@node1 web]# ls web
1.txt
[root@node1 web]# ls
web
[root@node1 web]# df -h
文件系统 容量 已用 可用 已用% 挂载点
/dev/mapper/centos-root 37G 29G 8.7G 77% /
devtmpfs 1.8G 0 1.8G 0% /dev
tmpfs 1.8G 0 1.8G 0% /dev/shm
tmpfs 1.8G 12M 1.8G 1% /run
tmpfs 1.8G 0 1.8G 0% /sys/fs/cgroup
/dev/sda1 1014M 130M 885M 13% /boot
tmpfs 367M 0 367M 0% /run/user/0
overlay 37G 29G 8.7G 77% /var/lib/docker/overlay2/009d7e68091342e151c35c507dccd08097b6c8264c59ffdf5e9b7713a202c2a5/merged
10.201.9.134:/netaccess/web 57G 42G 16G 73% /web/web

使用 ls /web/web 这会触发 autofs 将 10.201.9.134:/netaccess/web 挂载到 /web/web

设置NFS版本为V4版本

查看当前nfs服务器端支持的nsf版本

1
2
3
4
#+ 表示启用并支持该版本
#- 表示禁用或不支持
[root@centos-manager netaccess]# cat /proc/fs/nfsd/versions
-2 +3 +4 +4.1 +4.2

服务器端

  • 编辑/etc/sysconfig/nfs 配置文件
1
2
3
4
[root@centos-manager netaccess]# vi  /etc/sysconfig/nfs
# 添加以下参数,-N表示禁用版本
RPCNFSDARGS="-N 3 -N 2"
RPCNFSDARGS="-V 4.2"
  • 重启nfs服务
1
[root@centos-manager netaccess]# systemctl reload  nfs

客户端

  • (非必须)编辑/etc/sysconfig/autofs 配置文件
    这是autofs自动挂载服务的配置文件,在配置文件最后一行添加MOUNT_NFS_DEFAULT_PROTOCOL = 4.2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[root@node1 test]# cat /etc/sysconfig/autofs
#
# Init syatem options
#
# If the kernel supports using the autofs miscellanous device
# and you wish to use it you must set this configuration option
# to "yes" otherwise it will not be used.
#
USE_MISC_DEVICE="yes"
#
# Use OPTIONS to add automount(8) command line options that
# will be used when the daemon is started.
#
#OPTIONS=""
#
MOUNT_NFS_DEFAULT_PROTOCOL = 4.2
  • 必须配置
    编辑/etc/auto.web的配置文件,指定版本号
1
* -rw,vers=4.2,soft 10.201.9.134:/netaccess/&
  • 重启autofs 自动挂载服务
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[root@node1 test]# systemctl  restart  autofs
[root@node1 web]# ls web/
1.txt 2 3 4 b
[root@node1 web]# df -h
文件系统 容量 已用 可用 已用% 挂载点
/dev/mapper/centos-root 37G 29G 8.7G 77% /
devtmpfs 1.8G 0 1.8G 0% /dev
tmpfs 1.8G 0 1.8G 0% /dev/shm
tmpfs 1.8G 12M 1.8G 1% /run
tmpfs 1.8G 0 1.8G 0% /sys/fs/cgroup
/dev/sda1 1014M 130M 885M 13% /boot
tmpfs 367M 0 367M 0% /run/user/0
overlay 37G 29G 8.7G 77% /var/lib/docker/overlay2/009d7e68091342e151c35c507dccd08097b6c8264c59ffdf5e9b7713a202c2a5/merged
10.201.9.134:/netaccess/web 57G 42G 16G 73% /web/web
[root@node1 web]# mount | grep /web/web
10.201.9.134:/netaccess/web on /web/web type nfs4 (rw,relatime,vers=4.2,rsize=524288,wsize=524288,namlen=255,soft,proto=tcp,port=0,timeo=600,retrans=2,sec=sys,clientaddr=10.201.9.133,local_lock=none,addr=10.201.9.134)
[root@node1 web]#

使用mount | grep /web/web 可以看到nfs的版本号就变成了v4.2版本
如果要使用mount挂载的时候使用v4.2版本的话,直接在挂载命令中加上vers=4.2即可

1
mount -t nfs4 -o noatime,rsize=1048576,wsize=1048576,vers=4.2,proto=tcp 10.201.9.134:/tmp /tmp

NFS设置固定端口号

之前我们提到过,在NFS中,固定的端口号一共有两个,分别是rpc.nfsd 主进程的2049和rpc.statdRPC 端口映射进程的UDP111端口号,除此以外,还有四个进程是使用的随机端口号
以下是 NFS 相关服务及其默认可能使用随机端口的组件:

服务名称 程序号 (Program Number) 默认是否使用随机端口 主要协议 功能说明
rpc.mountd 100005 ✅ 是 TCP/UDP 处理客户端挂载/卸载请求
rpc.statd (NSM) 100024 ✅ 是 TCP 监控主机状态,用于锁恢复(Network Status Monitor)
lockd (NLM) 100021 ✅ 是 TCP/UDP 实现文件锁机制(Network Lock Manager)
rpc.rquotad 100011 ✅ 是 TCP/UDP 提供磁盘配额查询服务

NFS 本身依赖多个辅助守护进程来提供完整功能,而这些服务默认通常使用随机高端口(由 rpcbind 动态分配),这在防火墙或安全策略严格的环境中会带来问题。因此,固定这些服务的端口是一种常见的做法,便于配置防火墙规则。

如果需要将以上四个进程的端口号固定的话,就可以用通过修改/etc/sysconfig/nfs NFS的配置文件实现

  • 修改配置文件如下:
1
2
3
4
5
6
7
# 将一下的5个配置添加到配置文件的最底部即可
[root@centos-manager ~]# vi /etc/sysconfig/nfs
MOUNTD_PORT=4001 # rpc.mountd 端口号
STATD_PORT=4002 # rpc.statd 端口号
LOCKD_TCPPORT=4003 # lockd TCP 端口号
LOCKD_UDPPORT=4003 # lockd udp 端口号
RQUOTAD_PORT=4004 # rpc.rquotad 端口号
  • 重启服务
    这里需要重启NFS、rpcbind 和rpc-statd服务
1
2
3
[root@centos-manager ~]# systemctl  restart nfs
[root@centos-manager ~]# systemctl restart rpcbind
[root@centos-manager ~]# systemctl restart rpc-statd
  • 检查端口是否监听
1
2
3
root@centos-manager ~]# ss -luntp | grep 4002
[root@centos-manager ~]# ss -luntp | grep 4003
[root@centos-manager ~]# ss -luntp | grep 4001

rpc.rquotad,磁盘配额查询服务因为我在配置中没有开启,所以这里查不到端口号是正常的。

NFS挂载写入失败 注意事项:

误区 事实
“客户端写了 -rw 就能写” ❌ 服务器说了算
“挂载显示 rw 就能写” ❌ 可能因 UID 权限被拒
“导出父目录就能挂子目录” ❌ NFSv4 需显式导出子路径(除非用 fsid=0 伪根)

总结:NFS 的写权限 = 服务端 exports 的 rw + 服务端文件系统对 nfsnobody 可写。


NFS Linux 网络文件系统入门到实践
https://www.situgou.top/2026/01/25/NFS 文件系统配置与使用/
作者
xqj_Blog
发布于
2026年1月25日
许可协议