文件锁

整个文档中都会使用到的程序.

/**
 * fcntl [-t TYPE][-w WHENCE][-s START][-l LEN][-c OFF] file.
 * fcntl() 的接口,用于对一个指定的文件添加指定的字节范围锁.
 * 若文件上已经存在一把锁,阻止创建指定的锁,则打印该锁的信息.
 * -t TYPE | --type=TYPE 字节范围锁的类型,TYPE 可取值: wrlck(默认),rdlck.
 * -w WHENCE|--whence=WHENCE 指定了 l_whence 的值,WHENCE 可取值: cur(默认),set,end.
 * -s START | --start=START 指定了 l_start 的值.默认为 0.
 * -l LEN | --len=LEN 指定了 l_len 的值,默认为 0.
 * -c OFF | --cur=OFF 指定了文件读写指针的值,默认为 0,即会在 fcntl() 之前调用 lseek(fd,OFF,SEEK_SET);
 */

字节范围锁

  • 功能: 当一个进程正在读或写文件的一部分时,字节范围锁可以阻止其他进程修改同一文件区.
    将文件视为长度为 L 的字节数组(L 为文件长度),则字节范围锁可以确保在任意时刻,对于字节数组的任意部分[begin,end),只有一个进程可以读写,其中begn,end 为字节数组的索引值.如:

文件锁

l_start,l_whence,l_len 的理解

  • struct flock 中的 l_start,l_whence,l_len 域定义了加锁/解锁区域的范围 [begin,end),其中 begin,end 的值如下:

if(l_whence==SEEK_SET)
    l_whence=0;
else if(l_whence==SEEK_END)
    l_whence=lseek(fd,0,SEEK_END);/* 文件长度 */
else l_whence=lseek(fd,0,SEEK_CUR);/* 当前读写指针 */

begin=l_whence+l_start;
end=begin+l_len;

不同类型锁的兼容性

  • 兼容性与读写锁 pthread_rwlock_t 规则一致:

    • 多个进程在一个给定的字节上可以有一把共享的读锁.但是在一个给定的字节上,只能有一个进程独用的一把写锁.

    • 如果在一个给定的字节上已经有一把或多把读锁,则不能在该字节上再加写锁.如果在一个字节上已经有一把独占性的写锁,则不能再对它加任何读锁/写锁

  • 不过要注意下面两点:

    • 一个给定的字节! 当进程 A 在范围 [33,77) 上拥有一把 F_WRLCK 锁时,进程 B 在 [76,78) 上添加 F_RDLCK 锁,是不被允许的,因为字节 [76,77) 上已经有了一把写锁.

$ ./fcntl --type=wrlck --start=33 --len=44 mycp.cc 
2373: 0x7ffa7c9c3740: 93: 对文件 mycp.cc 加锁成功,锁的描述: 
        l_type: F_WRLCK
        l_start: 33
        l_whence: SEEK_SET
        l_len: 44
        l_pid: 0
$ ./fcntl --start=76 --len=2 mycp.cc 
2387: 0x7f50750b4740: 97: 对文件 mycp.cc 加锁失败,现存锁的描述: 
        l_type: F_WRLCK
        l_start: 33
        l_whence: SEEK_SET
        l_len: 44
        l_pid: 2373

    • 进程对一个给定的字节上已经拥有了一把锁,后来该进程又试图在同一字节再加一把锁,那么新锁将替换老锁!.

int main(int argc,char *argv[]){
	signal(SIGINT,default_sigcatch);

	struct flock filelock;
	filelock.l_type=F_WRLCK;
	filelock.l_start=33;
	filelock.l_whence=SEEK_SET;
	filelock.l_len=77;

	int fd;
	R_1(fd=open(argv[1],O_RDWR|O_CREAT,0666));
	R_1(fcntl(fd,F_SETLK,&filelock));

	Rep("对文件 %s 加锁成功,锁的描述: ",argv[1]);
	print_flock(&filelock);
	pause();

	filelock.l_type=F_RDLCK;
	filelock.l_start=44;
	filelock.l_len=2;
	R_1(fcntl(fd,F_SETLK,&filelock));
	Rep("对文件 %s 加锁成功,锁的描述: ",argv[1]);
	print_flock(&filelock);
	pause();
}
$ ./Debug/Test mycp.cc
2503: 0x7f62bbef5740: 25: 对文件 mycp.cc 加锁成功,锁的描述: 
        l_type: F_WRLCK
        l_start: 33
        l_whence: SEEK_SET
        l_len: 77
        l_pid: 4196928
/**
 * 此时 mycp.cc 上锁的情况:
 * 范围         拥有进程的进程 ID    锁的类型 
 * [33,110)     2503               F_WRLCK
 */
^C
2503: 0x7f62bbef5740: 31: 捕捉到信号: Interrupt
2503: 0x7f62bbef5740: 33: 对文件 mycp.cc 加锁成功,锁的描述: 
        l_type: F_RDLCK
        l_start: 44
        l_whence: SEEK_SET
        l_len: 2
        l_pid: 4196928
/*
 * 此时 mycp.cc 上锁的情况:
 * 范围         拥有进程的进程 ID    锁的类型
 * [33,44)       2503              F_WRLCK
 * [44,46)       2503              F_RDLCK  // 新锁替换老锁.
 * [46,110)      2503              F_WRLCK.   
 * 如下,可以使用 ./fcntl 命令验证:
 */
$ ./fcntl --start=43 --len=33 mycp.cc 
2533: 0x7f57fa676740: 97: 对文件 mycp.cc 加锁失败,现存锁的描述: 
        l_type: F_WRLCK
        l_start: 33
        l_whence: SEEK_SET
        l_len: 11
        l_pid: 2503
$ ./fcntl --start=45 --len=33 mycp.cc 
2534: 0x7f9ce58fc740: 97: 对文件 mycp.cc 加锁失败,现存锁的描述: 
        l_type: F_RDLCK
        l_start: 44
        l_whence: SEEK_SET
        l_len: 2
        l_pid: 2503
$ ./fcntl --start=47 --len=33 mycp.cc 
2537: 0x7f7929a47740: 97: 对文件 mycp.cc 加锁失败,现存锁的描述: 
        l_type: F_WRLCK
        l_start: 46
        l_whence: SEEK_SET
        l_len: 64
        l_pid: 2503

自动组合与拆裂相邻区

  • 若进程在 [a,b),[c,d) 上均有一把写锁,则当该进程再在 [b,c) 上再加一把写锁时,内核会自动将这三把锁组合成一把写锁,范围为 [a,d).

int main(int argc,char *argv[]){
	signal(SIGINT,default_sigcatch);
	int fd;
	int a=33,b=44;
	int c=55,d=77;

	R_1(fd=open(argv[1],O_RDWR|O_CREAT,0666));
	struct flock filelock;
	filelock.l_type=F_WRLCK;
	filelock.l_whence=SEEK_SET;

	/* 在 [a,b) 范围上写锁 */
	filelock.l_start=a;
	filelock.l_len=b-a;
	R_1(fcntl(fd,F_SETLK,&filelock));
	Rep("对文件 %s 范围 [%d,%d) 加写锁成功",argv[1],a,b);

	/* 在 [c,d) 范围上写锁 */
	filelock.l_start=c;
	filelock.l_len=d-c;
	R_1(fcntl(fd,F_SETLK,&filelock));
	Rep("对文件 %s 范围 [%d,%d) 加写锁成功",argv[1],c,d);

	pause();

	/* 在 [b,c) 范围上写锁 */
	filelock.l_start=b;
	filelock.l_len=c-b;
	R_1(fcntl(fd,F_SETLK,&filelock));
	Rep("对文件 %s 范围 [%d,%d) 加写锁成功",argv[1],b,c);
	pause();
}
$ ./Debug/Test mycp.cc
2692: 0x7f3b3dd8b740: 26: 对文件 mycp.cc 范围 [33,44) 加写锁成功
2692: 0x7f3b3dd8b740: 32: 对文件 mycp.cc 范围 [55,77) 加写锁成功
^C 
2774: 0x7f3b3dd8b740: 31: 捕捉到信号: Interrupt
2774: 0x7f3b3dd8b740: 40: 对文件 mycp.cc 范围 [44,55) 加写锁成功

/* 在另一个终端上启动 ./fcntl 验证 */ 
$ ./fcntl --start=34 --len=2 mycp.cc 
2711: 0x7f4858e86740: 97: 对文件 mycp.cc 加锁失败,现存锁的描述:
        l_type: F_WRLCK 
        l_start: 33 /* 写锁范围 [33,44) */
        l_whence: SEEK_SET
        l_len: 11
        l_pid: 2692
$ ./fcntl --start=56 --len=2 mycp.cc 
2712: 0x7f00feaf8740: 97: 对文件 mycp.cc 加锁失败,现存锁的描述: 
        l_type: F_WRLCK
        l_start: 55 /* 写锁范围 [55,77) */
        l_whence: SEEK_SET
        l_len: 22
        l_pid: 2692
$ ./fcntl --start=56 --len=2 mycp.cc 
2713: 0x7f7998a38740: 97: 对文件 mycp.cc 加锁失败,现存锁的描述: 
        l_type: F_WRLCK
        l_start: 33 /* 写锁被合并,此时范围 [33,77) */
        l_whence: SEEK_SET
        l_len: 44
        l_pid: 2692

  •  若进程在 [a,b) 上有一个写锁,则当进程再在 [c,d) 上加一个读锁时,其中 a<c<d<b.则此时 [a,b) 上的写锁会自动拆裂为两把写锁,范围分别是 [a,c) [d,b) (上上面的程序可以验证).

发生死锁

     进程 A              进程 B
 在范围 [0,1) 上写锁     在范围 [1,2) 上写锁
 等待 B 加锁完毕         等待 A 加锁完毕
 在范围 [1,2) 上写锁     在范围 [0,1) 上写锁   /* 内核可以检测出死锁. */

  • 仅当以 F_SETLKW 命令加锁时,才有可能死锁.当内核检测出死锁时,会让其他一个进程 fcntl() 出错返回.而另外一个进程阻塞直至加锁成功或者被信号处理程序中断.

int main( int argc,char *argv[] ){
    int field;
    R_1(field=open(argv[1],O_RDWR|O_CREAT,0666));

    struct flock lock;
    lock.l_type=F_WRLCK;
    lock.l_whence=SEEK_SET;
    lock.l_len=1;

    signal(SIGUSR1,sigcatch);

    /** 阻塞 SIGUSR1 信号 */
    sigset_t sigs;
    sigemptyset(&sigs);
    sigaddset(&sigs,SIGUSR1);
    sigprocmask(SIG_BLOCK,&sigs,0);
    sigfillset(&sigs);
    sigdelset(&sigs,SIGUSR1);

    pid_t child;
    R_1(child=fork());
    if(child==0){
    	/* [0,1) 加写锁. */
        lock.l_start=0;
        R_1(fcntl(field,F_SETLKW,&lock));
        Rep("已经在范围 [0,1) 上加写锁");
        kill(getppid(),SIGUSR1);
        sigsuspend(&sigs);/* 等待父进程加锁完毕 */

        Rep("准备在范围 [1,2) 上加写锁");
        /* [1,2) 加写锁 */
        lock.l_start=1;
        errno=0;
        fcntl(field,F_SETLKW,&lock);/* 这里造成死锁,此时子进程阻塞,直至成功加锁. */
        Rep("%m\n");
    }else{
    	/* [1,2) 加写锁 */
        lock.l_start=1;
        R_1(fcntl(field,F_SETLKW,&lock));
        Rep("已经在范围 [1,2) 上加写锁");
        kill(child,SIGUSR1);
        sigsuspend(&sigs);/* 等待子进程加锁完毕 */

        Rep("准备在范围 [0,1) 上加写锁");
        /* [0,1) 加写锁 */
        lock.l_start=0;
        errno=0;
        fcntl(field,F_SETLKW,&lock);    /* 这里造成死锁,父进程在这里出错返回. */
        Rep("%m\n");
    }
    pause();
    return 0;
}
$ ./Debug/Test mycp.cc  &
[1] 2940
2940: 0x7f7577bf5740: 56: 已经在范围 [1,2) 上加写锁
2941: 0x7f7577bf5740: 42: 已经在范围 [0,1) 上加写锁
2941: 0x7f7577bf5740: 46: 准备在范围 [1,2) 上加写锁
2940: 0x7f7577bf5740: 60: 准备在范围 [0,1) 上加写锁
2940: 0x7f7577bf5740: 65: Resource deadlock avoided /* 父进程检测出死锁,子进程阻塞. */
$ kill -SIGINT 2940               /* 父进程终止,其拥有的文件锁被释放. */
2941: 0x7f7577bf5740: 51: Success /* 所以子进程成功加锁. */

锁的释放

  • 当进程以 F_SETLK (或者 F_SETLKW ) 并且 l_type==F_UNLCK 调用 fcntl() 时会释放指定范围的锁.如:

int main(int argc,char *argv[]){
	signal(SIGINT,default_sigcatch);
	int a=33,b=77,c=44,d=55;
	struct flock lock={F_WRLCK,SEEK_SET,a,b-a};
	int fd;

	R_1(fd=open(argv[1],O_RDWR|O_CREAT,0666));
	R_1(fcntl(fd,F_SETLK,&lock));
	Rep("对文件 %s 范围 [%d,%d) 加锁成功",argv[1],a,b);

	pause();
	lock.l_type=F_UNLCK;
	lock.l_start=c;
	lock.l_len=d-c;
	R_1(fcntl(fd,F_SETLK,&lock));
	Rep("释放文件 %s 范围 [%d,%d) 范围上的锁",argv[1],c,d);
	/* 此时 [a,c),[d,b) 上的写锁被保留 */
	pause();
}

  • 当进程终止时,其拥有的所有文件锁都被释放.

  • 文件描述符 fd 关联到文件 filename.txt,则当 fd 被进程关闭时,进程在 filename.txt 上的锁也都将被释放.

int fd1=open("filename.txt",O_RDWR);
fcntl(fd1,F_SETLK,&lock1);
fcntl(fd1,F_SETLK,&lock2);
fcntl(fd1,F_SETLK,&lock3);
int fd2=dup(fd1);/* 或者 int fd2=open("filename",O_RDWR); */
close(fd2);
/* fd2 关联到文件 filename.txt,当 fd2 被进程关闭时,
 * 进程在 filename.txt 上的锁 lock1,lock2,lock3 也都被释放 
 */

锁的继承

  • 由 fork() 创建的子进程,并不会继承父进程拥有的文件锁.很显然,子进程是一个新的进程嘛.

  • 调用 exec() 后并不会清除原进程拥有的文件锁,新程序可以继承原执行程序的锁.只是当文件描述符设置了 close-on-exec 标志后,则作为 exec() 的一部分关闭该文件描述符时,对相应文件的所有锁也都被释放了.

建议性锁,强制性锁

  • 建议性锁,则其他进程在不拥有锁的前提下,也可以对加锁范围进行读写.

  • 强制性锁,则其他进程在不拥有锁的前提下,对加锁范围进行读写可能会阻塞,或者不允许对加锁范围进行读写.






















































你可能感兴趣的:(文件锁)