文件鎖

整個文檔中都會使用到的程序.shell

/**
 * 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 的值以下:ide

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 規則一致:spa

    • 多個進程在一個給定的字節上能夠有一把共享的讀鎖.可是在一個給定的字節上,只能有一個進程獨用的一把寫鎖.3d

    • 若是在一個給定的字節上已經有一把或多把讀鎖,則不能在該字節上再加寫鎖.若是在一個字節上已經有一把獨佔性的寫鎖,則不能再對它加任何讀鎖/寫鎖指針

  • 不過要注意下面兩點:code

    • 一個給定的字節! 當進程 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() 的一部分關閉該文件描述符時,對相應文件的全部鎖也都被釋放了.

建議性鎖,強制性鎖

  • 建議性鎖,則其餘進程在不擁有鎖的前提下,也能夠對加鎖範圍進行讀寫.

  • 強制性鎖,則其餘進程在不擁有鎖的前提下,對加鎖範圍進行讀寫可能會阻塞,或者不容許對加鎖範圍進行讀寫.

相關文章
相關標籤/搜索