整个文档中都会使用到的程序.
/** * 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 为字节数组的索引值.如:
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() 的一部分关闭该文件描述符时,对相应文件的所有锁也都被释放了.
建议性锁,则其他进程在不拥有锁的前提下,也可以对加锁范围进行读写.
强制性锁,则其他进程在不拥有锁的前提下,对加锁范围进行读写可能会阻塞,或者不允许对加锁范围进行读写.