添加链接
link之家
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接
相关文章推荐
好帅的咖啡  ·  IIS 使用 office365 SMTP ...·  2 月前    · 
路过的毛衣  ·  如何使用Android ...·  5 月前    · 
空虚的葡萄酒  ·  vuejs3 - TypeScript / ...·  1 年前    · 

准备知识(建议)

  • 熟悉 Java编程语言
  • 熟悉 网络通信协议
  • 熟悉 C语言
  • 熟悉 Linux操作系统
  • 熟悉 Unix环境编程
  • 熟悉 网络抓包拦截分析

NFSV4 文件锁介绍

文件锁是文件系统的最基本特性之一,应用程序借助文件锁可以控制其他应用对文件的并发访问。NFS作为类UNIX系统的标准网络文件系统,在发展过程中逐步的原生支持了文件锁 (从NFSv4开始)

通过命令 man nfs 可以查看官方说明手册内容:

8a4bbeee99848fc140fcacfcad18d54.png

其中比较重要的是第三句: NLM仅支持建议文件锁。要锁定NFS文件,需使用fcntl(2)和F_GETLK和F_SETLK命令。NFS客户端将通过flock(2)获得的文件锁转换为咨询锁。 也就是说应用程序可以通过 fcntl() flock() 系统调用管理NFS文件锁。

下面阿里云文件存储 NAS使用NFSv4挂载时获取文件锁的调用过程图:

e352375c3796d60bc7142fe75210005.png

图片来源: # NFS文件锁一致性设计原理解析

从上图调用栈容易看出来,NFS文件锁实现逻辑基本复用了VFS层设计和数据结构,在通过RPC从Server成功获取文件锁后调用locks_lock_inode_wait()函数将获得文件锁交给到VFS层管理,关于VFS层文件锁设计的相关资料比较多,感兴趣的读者可以自己搜索了解

观察上图的文件锁调用过程,看出最主要的交互在 nfs4_proc_lock() 方法上,通过查看Linux内核源码

https://git.kernel.org/pub/scm/linux/kernel/git/cel/linux.git/tree/fs/nfs/nfs4proc.c

可以看到相关的实现与调用

static int nfs do setlk ( struct ns _ state * state , in t cnd, struct file _ lock * fl , int recovery _ type )
 struct nfs4_ lockdata * data ; struct rpC _ task * task ;
 struct rpc _ message msg ={
. rpc _ proc =&nfs4_ procedures [NFSPROC4_ CLNT _ LOCK ],. rpc _ cred = st ate -> owmer -> so _ cred ,
 rpc _ task _ setup task _ setup _ data =
. rpc _ client = NFS _ CLIENT ( state -> inode ),
. rpc _ message =& msg ,
. callback _ ops =&nfs4_ lock _ ops ,
. workqueue = nfsiod _ workqueue ,
. flags = RPC _ TASK _ ASYNc RPC _ TASK _ CRED _ NOREF ,
 St rUCt 
 int ret :
 struct nfs _ client * client = NFS _ SERVER ( state -> inode )-> nfs _ client : if ( client -> cl _ minorversion )
 task _ setup _ data . flags I = RPC _ TASK _ MoVEABLE ;
 dprintk ("% s : begin ! n ". func __):
 data =nfs4_ alloc _ lockdatafl , nfs _ file _ open _ context ( fl -> fl _ file ),
 fI -> fI _ u . nfs _ fl . one 
 recovery _ type == FS _ LoCK _ NEW ? GFPKENEL : GFP _ NOFS )
 if ( data == NuLL )
 return - ENOMEM ; if ( IS _ SETLKW ( cmd ))
 data -> arg . block =1;
nfs4_ init _ sequence (& data -> arg . seq _ args ,& data -> res . seq _ res ,1, msg . rpc _ argp =& data -> arg 
 msg . rpc _ resp =& data -> res ;
 task _ setup _ data . callback _ data = data ;
 if ( recovery _ type > NFS _L0CK_ NEW ){
 recovery _ type > NFS _L0CK_ NEW );
 if ( recovery _ type == NFS _L0CK_ RECLAIM )
 data -> arg . reclain = NFS _L0CK_ RECLAIM ;
 data -> arg . new _ lock =1:
 task = rpc _ run _ task (& task _ setup _ data ):
 if ( IS _ ERR ( task ))
 return PTR _ ERR ( task ):
 ret = rpc _wa1t_ for _complet1on_ task ( task ):
 if ( ret ==0)
 ret = data -> rpc _ status :
 if ( ret 
nfs4_ handle _ setlk _ error ( data -> server , data -> lsp ,
 data -> arg . new _ lock _ owmer , ret ):
 data -> cancelled = true :
 trace _ntfs4_ set _ lock ( fl , state ,& data -> res . stateid , cmd , ret ):
 rpc _ put _ task ( task ):
 dprintk (“% s : done , ret =% d !\ n ”,__ func __, ret ): return ret ;

除了上面通过linux 内核源码了解nfsv4文件锁的定义存在,还可以查看其 Network File System (NFS) Version 4 Protocol 规范,通过阅读第9章节,也可以知道nfsv4对文件锁的支持说明

bb325a42010f8b4c6d9ea4dd8012251.png

NFSV4文件锁通过在通信协议增加stateid的方式来实现服务器端文件锁功能,在 NFSv3 中,没有 stateid 的概念,因此无法判断发送 READ 或WRITE 操作的客户端的应用程序进程是否也获取了文件上的适当字节范围锁定。因此,没有办法实现强制锁定。使用 stateid 构造,此障碍已被移除。

Java文件锁介绍

Java 提供了文件锁 FileLock 类,利用这个类可以控制不同程序(JVM)对同一文件的并发访问,实现进程间文件同步操作。

FileLock是java 1.4 版本后出现的一个类,它可以通过对一个可写文件(w)加锁,保证同时只有一个进程可以拿到文件的锁,这个进程从而可以对文件做访问;而其它拿不到锁的进程要么选择被挂起等待,要么选择去做一些其它的事情, 这样的机制保证了众进程可以顺序访问该文件。也可以看出,能够利用文件锁的这种性质,在一些场景下,虽然我们不需要操作某个文件, 但也可以通过 FileLock 来进行并发控制,保证进程的顺序执行,避免数据错误。

锁的概念

  • 共享锁: 共享读操作,但只能一个写(读可以同时,但写不能)。共享锁防止其他正在运行的程序获得重复的独占锁,但是允许他们获得重复的共享锁。
  • 独占锁: 只有一个读或一个写(读和写都不能同时)。独占锁防止其他程序获得任何类型的锁。

FileLock 底层实现

我们需要通过调用 FileChannel 类上的 lock() tryLock() 来获得 FileLock 文件锁,

FileChannel.java

这里选用 trylock() ,其实选 lock() 也一样,因为是抽象方法,所以需要到实现类Impl中查看具体逻辑。

FileChannelImpl . class X C FileDispatcherimpl . class FileDispatcl 
 FileDispatcher . dass 
1I SUIe ULIe / v errur uccuis 
@ see 
# lock ()
# lock ( long , long , boolean )
# tryLock ()
这是一个抽象方法
 long position , long size , boolean shared )
 public 
 abstract FileLock tryLock throws IOException ;

FileChannelImpl.java

注意try语句的部分,一般try都是做重要事情的,可以看到正在进行锁lock的操作。

public FileLock tryLock(long position, long size, boolean shared)
throws I0Exception
ensureOpen();
if(shared 86 !readable)
throw new NonReadableChannelException(); if(!shared 8G !writable)
throw new NonWritableChannelException();
FileLockImpl fli = new FileLockImpl(this, position, size, shared); FileLockTable flt = fileLockTable(); flt.add(fli); int result;
int ti = threads.add(); try {
try {
ensure0pen();
// nd is FileDispatcher
result = nd.lock(fd, false, position, size, shared)}catch(IOException e){
flt.remove(fli); throwe;
if(result =FileDispatcher.NO_LOCK){
flt.remove(fli); return null;}
if(result = FileDispatcher.RET_EX_LOCK){    

FileDispatcher.java

lock() 在这里也是一个抽象方法,继续去找它的实现类Impl看看。

abstract int lock(FileDescriptor fd, boolean blocking, long pos, long size,
boolean shared) throws I0Exception;

FileDispatcherImpl.java

终于找到了 lock() 的实现,发现最终调用的是 lock0() ,而一直往下深入发现是一个 native method

int lock(FileDescriptor fd, boolean blocking, long pos, long size,
boolean shared) throws I0Exception
return  locko(fd, blocking, pos, size, shared); 
static  native  int locko(FileDescriptor fd, boolean blocking, long pos,    
long size, boolean shared) throws I0Exception;

FileDispatcherImpl.c

看到 native method ,肯定是用c写的实现逻辑,所以找到 FileDispatcherImpl.c 如下内容,发现其实现中有调用 fcntl() ,nfsv4文件锁的使用也正好需要该函数的调用。

JNIEXPORT jint JNICALL
Java_sun_nio_ch_FileDispatcherImpl lock0((JNIEnv *env, jobject this, jobject fdo,
iboolean block, jlong pos, ilong size, jboolean shared)
jint fd = fdval(env,fdo); jint lockResult = 0; int cmd = 0;
struct flock64 fl;
fl.l_whence = SEEK_SET;
if (size =(jlong)java_lang_Long_MAX_VALUE){
fl.l_len =(off64_t)0;} else {
fl.l_len =(off64_t)size;}
fl.l_start =(off64_t)pos; if(shared =JNI_TRUE){
fl.l_type = F_RDLCK;} else {
fl.l_type = F_WRLCK;}
if(block = JNI_TRUE){
cmd = F_SETLKW64;} else {
cmd = F_SETLK64;}
lockResult =    fcntl(fd,cmd, sfl); 
if (lockResult <o) {
if((cmd=F_SETLK64)sG(errno= EAGAIN Il errno=EACCES))
return sun nio ch FileDispatcherImpl NO LOCK:

NFS文件锁实战演示

前面已经学习过 NFS的安装与配置 ,而且也了解了nfsv4文件锁和java文件锁的,接下来将进行实战演示。

  1. 首先新建一个Java类,该类对文件进行读写锁操作,内容如下:
package com.jpsite.utils;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.file.FileAppender;
import cn.hutool.core.io.file.FileReader;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.net.InetAddress;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.util.Objects;
public class FileUtilTest {
    public static void test1() throws Exception {
        InetAddress ia = InetAddress.getLocalHost();
        String host = ia.getHostName();//获取计算机主机名
        String IP= ia.getHostAddress();//获取计算机IP
        final File file = FileUtil.touch(new File("/data/share/b.txt"));
        FileAppender appender = new FileAppender(file, 1, true);
        appender.append("123"+IP);
        appender.append("abc"+IP);
        appender.append("xyz"+IP);
        appender.flush();
        FileReader fileReader = new FileReader(file);
        String result = fileReader.readString();
        System.out.println(result);
        final FileLock fileLock = getFileLock(file);
        if (Objects.isNull(fileLock)) {
            System.out.println("not get lock");
        } else {
            final String lockType = fileLock.isShared() ? "share lock" : "exclusive lock";
            System.out.println(String.format("getted lock %s", lockType));
            int i = 0;
            while (true) {
                Thread.sleep(1000);
                System.out.println(lockType + "no release");
                if (i == 30) {
                    fileLock.release();
                    if (!fileLock.isValid()) {
                        System.out.println("lock is valid ,return while");
                        break;
    public static FileLock getFileLock(File file) throws IOException {
        final RandomAccessFile rw = new RandomAccessFile(file, "rw");
        final FileChannel channel = rw.getChannel();
        return channel.tryLock();
    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            try {
                test1();
            } catch (Exception e) {
                e.printStackTrace();
        }).start();
复制代码
  1. 把这个类分别上传到NFS Client的 Linux机器,比如centos 1, centos2, cd 到java项目的src路径,分别执行
# 关于命令的说明可以查看我的沸点
javac -classpath /root/.m2/repository/cn/hutool/hutool-all/4.6.17/hutool-all-4.6.17.jar ./com/jpsite/utils/FileUtilTest.java
java -classpath .:/root/.m2/repository/cn/hutool/hutool-all/4.6.17/hutool-all-4.6.17.jar com.jpsite.utils.FileUtilTest
复制代码
  1. 可以看到 centos1 中java代码正常在执行
XYZ127.0.0.1
123127.0.0.1
abc127.0.0.1
xyz127.0.0.1
123127.0.0.1
abc127.0.0.1
xyz127.0.0.1
 getted lock exclusive lock 
 exclusive lockno release 
 exclusive lockno release 
 exclusive lockno release 
 exclusive lockno release 
 exclusive lockno release 
 exclusive lockno release 
 exclusive lockno release 
 exclusive lockno release 
 exclusive lockno release 
 exclusive lockno release 
 exclusive lockno release 
 exclusive lockno release 
 exclusive lockno release 
 exclusive lockno release 
 exclusive lockno release 
 exclusive lockno release 
 exclusive lockno release 

centos2 执行过程中由于获取不到文件锁被退出

123127.0.0.1
abc127.0.
xyz127.0。.
 not get lock .

M1 macbook 笔记本测试问题

刚刚的测试是两台云主机centos机器,现在改为一台centos和一台本地电脑,首先本地电脑执行命令挂载

sudo mount -t nfs -o vers=4.0,proto=tcp,timeo=60 129.xxx.x.139:/data/share /Users/hahas/Documents/data/share
复制代码

测试出现了两种结果:

  • ✅结果一:先执行centos中的java代码,再执行m1 macbook的java代码,结果和两台云主机centos机器测试结果一致。
  • ❌结果二:先执行m1 macbook的java代码,再执行centos中的java代码,发现都能获取到文件锁。

出现结果二问题原因,m1 macbook的nfs client版本过低,为能提供有效支持;或者是因为采用了arm架构内核,导致相关功能失效。

网络协议数据分析

网络抓包的手段和工具很多,本文在centos系统中使用的是内置的 tcpdump ,而macbook则使用 wireshark

通过wireshark可以方便的看到网络协议的相关数据

// 查看发给129.xxx.x.139 nfs server的网络数据
(ip.dst eq 129.xxx.x.139) and nfs 
复制代码

通过抓包发现lock和unlock操作,对比其中的网络数据包,发现其内容和NFSV4文件锁介绍那章相符,所以NFS文件锁的使用,是需要网络协议中stateid等数据字段的支持。

X
(ip.dst eq 12   139) and nfs    X   
No. |Time   Source  Destination |Protocol   |Length Info    
199 11.401756   10.130.1.59 129.    139 NFS 346 V4 Call (Reply In 200)open  OPEN DH:0x33fe6b1a/b.txt    
202 11.417693   10.130.1.59 129.    139 NFS 366 V4 Call (Reply In 203) lock LOCK FH: 0xe13f48a7 Offset: 0 Length: <End of File> 
205 11.599192   10.130.1.59 129.    139 NFS 302 V4 Call (Reply In 206) lookup   LOOKUP DH: 0x33fe6b1a/._.   
208 11.607244   10.130.1.59 129.    139 NFS 278 V4 Call (Reply In 209)getattr   GETATTR FH:0x6d97b374   
211 11.615700   10.130.1.59 129.    139 NFS 342 V4 Call(Reply In 212)open   OPEN DH: 0x33fe6b1a/._· 
214 11.624267   10.130.1.59 129.    139 NFS 302 V4 Call (Reply In 215) close    CLOSE StateID:0x81f2    
217 11.633336   10.130.1.59 129.    .139 NFS    306 V4 Call (Reply In 218) lookup   LO0KUP DH: 0x33fe6b1a/.hidden   
229 12.098835   10.130.1.59 129.    .139 NFS    306 V4 Call (Reply In 230) lookup   LOOKUP DH: 0x33fe6b1a/b.txt RENEW CID:0x47191661c1dcfe0a    
425 22.426617   10.130.1.59 129.    .139 NFS    230 V4 Call (Reply In 426)renew 
679 41.594250   10.130.1.59 129.    .139 NFS    322 V4 Call (Reply In 680)( unlock  LOCKU FH: 0xe13f48a7 Offset: 0 Length: <End of File>    
684 42.007264   10.130.1.59 129.    .139 NFS    302 V4 Call (Reply In 685) close    CLOSE StateID: 0xec11   
1052 67.435838  10.130.1.59 129.2   .139 NFS    230 V4 Call(Reply In 1053)renew RENEW CID: 0x47191661c1dcfe0a   
Network File System,Ops(3): PUTFH, GETATTR, LOCK
[Program Version:4]
[V4 Procedure: COMPOUND(1)]>Tag:lock
minorversion:0
Operations(count:3):PUTFH, GETATTR, LOCK>Opcode:PUTFH(22)>Opcode: GETATTR(9) vOpcode:LOCK(12)
locktype:WRITE_LT(2) reclaim?:No offset:0
length:18446744073709551615 new lock owner?: Yes seqid:0x0000001b StatelD
[StateID Hash: 0xec11] StateID segid: 1
StateID Other: 47191661c1dcfe0a0f000000[StateID Other hash: 0x6c195810] lock seqid: 0x00000000 Owner
clientid: 0x47191661c1dcfe0a>owner:<DATA>
0130    ff ff 00 00 00 01 00 0000 16 00 00 00 01 47 19  。.............G 
0140    16 61 c1 dc fe 0a 0f 00 00 00 00 00 00 00 47 19 ·a·。。。......。.G 
0150    16 61 c1 dc fe 0a 00 00 00 14 00 00 00 00 00 00 ·a..............    
0160    03 af 00 00 00 00 61 16 33 80 00 04 68 f4   ...."a· 3...h·  
Tevt item lteyt! 24 hvtes   Beofle.nofe
679 41.594250   10.130.1.59 129.    .139 NFS    322 v4 call (Reply In 680)  runlock LOCKU FH:   0xe13f48a7 0ffset:  Length: <End of File>   
684 42.007264   10.130.1.59 129.    .139 NFS    302 V4 Call (Reply In 685) close    CLOSE StateID: 0xec11   
1052 67,435838  10,130.1.59 129.    139 NES 230 V4 Call(Reply In 1053)renew RENEW CID: 0x47191661c1dcfe0a   
Frame 679:322 bvtes on wire(2576 bits),322 bytes captured(2576 bits) on interface en0, id 0 EthernetII,Src:Appledc:2e:18(18:3e:ef:dc:2e:18),Dst:70:3a:d7:07:ba:08(70:3ad707:ba08) Internet Protocol Version 4,Src: 10.130.1.59,Dst: 129.204.4.139
TransmissionControlProtocol,SrcPort:49544,Dst Port: 2049,Seq:9753,Ack:14045,Len: 256 Remote Procedure Call, Type:Call XID:0xb335f076
Network File System, 0ps(3): PUTFH, GETATTR, LOCKU
[Program Version: 4]
[V4 Procedure:COMPOUND(1)] Tag:unlock
minorversion:0
Operations(count:3): PUTFH, GETATTR, LOCKU>Opcode:PUTFH(22)>Opcode:GETATTR(9)
Opcode:LOCKU(14)
locktype:WRITE_LT (2) v StateID
[StateID Hash: 0x9d49] StateID segid:1
StateID Other:47191661c1dcfe0a10000000[StateID Other hash: 0x64631fd9] offset:0
length:18446744073709551615 segid:0x00000001
[Main Opcode: LOCKU(14)]

ps:如果有厉害的小伙伴或者黑客,可以不用通过系统底层方法调用,直接采用数据包拦截篡改的方式,使nfs server的文件上锁🔒 👍牛逼🐂plus

NFS client mount 配置说明

local_lock

Linux NFS客户端提供了一种将锁设置为本地的方法。这意味着,应用程序可以锁定文件,但是这种锁只对运行在同一客户端上的其他应用程序提供排除。远程应用程序不受这些锁的影响。

mount -t nfs -o vers=4.1,proto=tcp,local_lock=flock 129.xxx.x.139:/data/share /data/share
复制代码

cto

NFS 实现了一种称为“接近打开一致性”或cto的弱数据一致性。这意味着当一个文件在客户端关闭时,所有与该文件相关的修改数据都将从服务器中刷新。如果您的 NFS 客户端允许这种行为,请确保设置了cto选项。

mount -t nfs -o vers=4.1,proto=tcp,cto 129.xxx.x.139:/data/share /data/share
复制代码

ac / noac

为了提高性能,NFS客户端缓存文件属性。每隔几秒,NFS客户端就会检查服务器版本的每个文件的属性的更新。发生在那些小服务器上的更改在客户端再次检查服务器之前,时间间隔保持未检测。noac选项阻止客户端缓存文件属性,以便应用程序能够更快地检测服务器上的文件更改。除了防止客户机缓存文件属性,noac选项还强制应用程序写保持同步,以便文件的本地更改在服务器上立即可见。通过这种方式,其他客户端在检查文件属性时可以快速检测到最近的写操作。使用noac选项在访问相同文件的NFS客户端之间提供了更强的缓存一致性,但它提取显著的性能损失。因此,我们鼓励明智地使用文件锁定。

DATA AND METADATA COHERENCE 一节Attribute caching详细讨论了这些权衡。

使用noac挂载选项在多个客户端之间实现属性缓存一致性。几乎每个文件系统操作检查文件属性信息。客户端将这些信息缓存一段时间以减少网络和服务器负载。当noac生效时,客户端的文件属性缓存将被禁用,因此每个需要检查文件的属性被强制返回到服务器。这允许客户端非常快速地查看文件的更改许多额外网络操作的成本。注意不要将noac选项与“无数据缓存”混淆。noac挂载选项阻止客户端缓存文件元数据,但仍然存在可能导致客户端和服务器之间的数据缓存不一致的竞争。NFS协议不是设计来支持真正的集群文件系统缓存一致性的,除非有某种类型的应用程序实现。如果客户端之间需要绝对的缓存一致性,应用程序应该使用文件锁定。另外,应用程序也可以使用O_DIRECT标志打开文件,完全禁用数据缓存。

相关参考

NFS文件锁一致性设计原理解析

Linux文件锁实现之flock(2)与fcntl(2)

Using NFSv4 as a shared file system

Network File System (NFS) Version 4 Protocol

NFS读写块大小问题分析

Linux kernel源码

Linux 在线/离线安装 NFS 服务及使用
-m 表示指定文件所在的文件系统或者块设备(处于 mount 状态)。所有访问该文件系统的进程都被列出。如上所示,有两个进程占用了,将其kill掉,再重新取消挂载。
Linux网络服务总结,复习看这个很方便(DHCP、FTP、SSH、DNS、NFS、PXE)
Linux网络服务总结,复习看这个很方便(DHCP、FTP、SSH、DNS、NFS、PXE)