linux文件锁.docx
《linux文件锁.docx》由会员分享,可在线阅读,更多相关《linux文件锁.docx(19页珍藏版)》请在冰豆网上搜索。
linux文件锁
在多任务操作系统环境中,如果一个进程尝试对正在被其他进程读取的文件进行写操作,可能会导致正在进行读操作的进程读取到一些被破坏或者不完整的数据;如果两个进程并发对同一个文件进行写操作,可能会导致该文件遭到破坏。
因此,为了避免发生这种问题,必须要采用某种机制来解决多个进程并发访问同一个文件时所面临的同步问题,由此而产生了文件加锁方面的技术。
早期的UNIX系统只支持对整个文件进行加锁,因此无法运行数据库之类的程序,因为此类程序需要实现记录级的加锁。
在SystemVRelease3中,通过fcntl提供了记录级的加锁,此后发展成为POSIX标准的一部分。
本文将基于2.6.23版本的内核来探讨Linux中文件锁的相关技术。
Linux中的文件锁
Linux支持的文件锁技术主要包括劝告锁(advisorylock)和强制锁(mandatorylock)这两种。
此外,Linux中还引入了两种强制锁的变种形式:
共享模式强制锁(share-modemandatorylock)和租借锁(lease)。
在Linux中,不论进程是在使用劝告锁还是强制锁,它都可以同时使用共享锁和排他锁(又称为读锁和写锁)。
多个共享锁之间不会相互干扰,多个进程在同一时刻可以对同一个文件加共享锁。
但是,如果一个进程对该文件加了排他锁,那么其他进程则无权再对该文件加共享锁或者排他锁,直到该排他锁被释放。
所以,对于同一个文件来说,它可以同时拥有很多读者,但是在某一特定时刻,它只能拥有一个写者,它们之间的兼容关系如表1所示。
表1.锁间的兼容关系
是否满足请求
当前加上的锁
共享锁
排他锁
无
是
是
共享锁
是
否
排他锁
否
否
劝告锁
劝告锁是一种协同工作的锁。
对于这一种锁来说,内核只提供加锁以及检测文件是否已经加锁的手段,但是内核并不参与锁的控制和协调。
也就是说,如果有进程不遵守“游戏规则”,不检查目标文件是否已经由别的进程加了锁就往其中写入数据,那么内核是不会加以阻拦的。
因此,劝告锁并不能阻止进程对文件的访问,而只能依靠各个进程在访问文件之前检查该文件是否已经被其他进程加锁来实现并发控制。
进程需要事先对锁的状态做一个约定,并根据锁的当前状态和相互关系来确定其他进程是否能对文件执行指定的操作。
从这点上来说,劝告锁的工作方式与使用信号量保护临界区的方式非常类似。
劝告锁可以对文件的任意一个部分进行加锁,也可以对整个文件进行加锁,甚至可以对文件将来增大的部分也进行加锁。
由于进程可以选择对文件的某个部分进行加锁,所以一个进程可以获得关于某个文件不同部分的多个锁。
强制锁
与劝告锁不同,强制锁是一种内核强制采用的文件锁,它是从SystemVRelease3开始引入的。
每当有系统调用open()、read()以及write()发生的时候,内核都要检查并确保这些系统调用不会违反在所访问文件上加的强制锁约束。
也就是说,如果有进程不遵守游戏规则,硬要往加了锁的文件中写入内容,内核就会加以阻拦:
如果一个文件已经被加上了读锁或者共享锁,那么其他进程再对这个文件进行写操作就会被内核阻止;
如果一个文件已经被加上了写锁或者排他锁,那么其他进程再对这个文件进行读取或者写操作就会被内核阻止。
如果其他进程试图访问一个已经加有强制锁的文件,进程行为取决于所执行的操作模式和文件锁的类型,归纳如表2所示:
表2.进行对已加强制锁的文件进行操作时的行为
当前锁类型
阻塞读
阻塞写
非阻塞读
非阻塞写
读锁
正常读取数据
阻塞
正常读取数据
EAGAIN
写锁
阻塞
阻塞
EAGAIN
EAGAIN
需要注意的是,如果要访问的文件的锁类型与要执行的操作存在冲突,那么采用阻塞读/写操作的进程会被阻塞,而采用非阻塞读/写操作的进程则不会阻塞,而是立即返回EAGAIN。
另外,unlink()系统调用并不会受到强制锁的影响,原因在于一个文件可能存在多个硬链接,此时删除文件时并不会修改文件本身的内容,而是只会改变其父目录中dentry的内容。
然而,在有些应用中并不适合使用强制锁,所以索引节点结构中的i_flags字段中定义了一个标志位MS_MANDLOCK用于有选择地允许或者不允许对一个文件使用强制锁。
在super_block结构中,也可以将s_flags这个标志为设置为1或者0,用以表示整个设备上的文件是否允许使用强制锁。
要想对一个文件采用强制锁,必须按照以下步骤执行:
使用-omand选项来挂载文件系统。
这样在执行mount()系统调用时,会传入MS_MANDLOCK标记,从而将super_block结构中的s_flags设置为1,用来表示在这个文件系统上可以采用强制锁。
例如:
#mount-omand/dev/sdb7/mnt
#mount|grepmnt
/dev/sdb7on/mnttypeext3(rw,mand)
1.修改要加强制锁的文件的权限:
设置SGID位,并清除组可执行位。
这种组合通常来说是毫无意义的,系统用来表示该文件被加了强制锁。
例如:
#touch/mnt/testfile
#ls-l/mnt/testfile
-rw-r--r--1rootroot0Jun2214:
43/mnt/testfile
#chmodg+s/mnt/testfile
#chmodg-x/mnt/testfile
#ls-l/mnt/testfile
-rw-r-Sr--1rootroot0Jun2214:
43/mnt/testfile
2.使用fcntl()系统调用对该文件进行加锁或解锁操作。
共享模式锁
Linux中还引入了两种特殊的文件锁:
共享模式强制锁和租借锁。
这两种文件锁可以被看成是强制锁的两种变种形式。
共享模式强制锁可以用于某些私有网络文件系统,如果某个文件被加上了共享模式强制锁,那么其他进程打开该文件的时候不能与该文件的共享模式强制锁所设置的访问模式相冲突。
但是由于可移植性不好,因此并不建议使用这种锁。
租借锁
采用强制锁之后,如果一个进程对某个文件拥有写锁,只要它不释放这个锁,就会导致访问该文件的其他进程全部被阻塞或不断失败重试;即使该进程只拥有读锁,也会造成后续更新该文件的进程的阻塞。
为了解决这个问题,Linux中采用了一种新型的租借锁。
当进程尝试打开一个被租借锁保护的文件时,该进程会被阻塞,同时,在一定时间内拥有该文件租借锁的进程会收到一个信号。
收到信号之后,拥有该文件租借锁的进程会首先更新文件,从而保证了文件内容的一致性,接着,该进程释放这个租借锁。
如果拥有租借锁的进程在一定的时间间隔内没有完成工作,内核就会自动删除这个租借锁或者将该锁进行降级,从而允许被阻塞的进程继续工作。
系统默认的这段间隔时间是45秒钟,定义如下:
137intlease_break_time=45;
这个参数可以通过修改/proc/sys/fs/lease-break-time进行调节(当然,/proc/sys/fs/leases-enable必须为1才行)。
回页首
Linux内核中关于文件锁的实现
在Linux内核中,所有类型的文件锁都是由数据结构file_lock来描述的,file_lock结构是在文件中定义的,内容如下所示:
清单1.file_lock结构
811structfile_lock{
812structfile_lock*fl_next;/*singlylinkedlistforthisinode*/
813structlist_headfl_link;/*doublylinkedlistofalllocks*/
814structlist_headfl_block;/*circularlistofblockedprocesses*/
815fl_owner_tfl_owner;
816unsignedintfl_pid;
817wait_queue_head_tfl_wait;
818structfile*fl_file;
819unsignedcharfl_flags;
820unsignedcharfl_type;
821loff_tfl_start;
822loff_tfl_end;
823
824structfasync_struct*fl_fasync;/*forleasebreaknotifications*/
825unsignedlongfl_break_time;/*fornonblockingleasebreaks*/
826
827structfile_lock_operations*fl_ops;/*Callbacksforfilesystems*/
828structlock_manager_operations*fl_lmops;/*Callbacksforlockmanagers*/
829union{
830structnfs_lock_infonfs_fl;
831structnfs4_lock_infonfs4_fl;
832struct{
833structlist_headlink;/*linkinAFSvnode'spending_lockslist*/
834intstate;/*stateofgrantorerrorif-ve*/
835}afs;
836}fl_u;
837};
表3简单描述了file_lock结构中的各个字段所表示的含义。
表3.file_lock数据结构的字段
类型
字段
字段描述
structfile_lock*
fl_next
与索引节点相关的锁列表中下一个元素
structlist_head
fl_link
指向活跃列表或者被阻塞列表
structlist_head
fl_block
指向锁等待列表
structfiles_struct*
fl_owner
锁拥有者的files_struct
unsignedint
fl_pid
进程拥有者的pid
wait_queue_head_t
fl_wait
被阻塞进程的等待队列
structfile*
fl_file
指向文件对象
unsignedchar
fl_flags
锁标识
unsignedchar
fl_type
锁类型
loff_t
fl_start
被锁区域的开始位移
loff_t
fl_end
被锁区域的结束位移
structfasync_struct*
fl_fasync
用于租借暂停通知
unsignedlong
fl_break_time
租借的剩余时间
structfile_lock_operations*
fl_ops
指向文件锁操作
structlock_manager_operations*
fl_mops
指向锁管理操作
union
fl_u
文件系统特定信息
一个file_lock结构就是一把“锁”,结构中的fl_file就指向目标文件的file结构,而fl_start和fl_end则确定了该文件要加锁的一个区域。
当进程发出系统调用来请求对某个文件加排他锁时,如果这个文件上已经加上了共享锁,那么排他锁请求不能被立即满足,这个进程必须先要被阻塞。
这样,这个进程就被放进了等待队列,file_lock结构中的fl_wait字段就指向这个等待队列。
指向磁盘上相同文件的所有file_lock结构会被链接成一个单链表file_lock_list,索引节点结构中的i_flock字段会指向该单链表结构的首元素,fl_next用于指向该链表中的下一个元素;当前系统中所有被请求,但是未被允许的锁被串成一个链表:
blocked_list。
fl_link字段指向这两个列表其中一个。
对于被阻塞列表(blocked_list)上的每一个锁结构来说,fl_next字段指向与该锁产生冲突的当前正在使用的锁。
所有在等待同一个锁的那些锁会被链接起来,这就需要用到字段fl_block,新来的等待者会被加入到等待列表的尾部。
此外,fl_type表示锁的性质,如读、写。
fl_flags是一些标志位,在linux2.6中,这些标志位的定义如下所示:
清单2.标志位的定义
773#defineFL_POSIX1
774#defineFL_FLOCK2
775#defineFL_ACCESS8/*nottryingtolock,justlooking*/
776#defineFL_EXISTS16/*whenunlocking,testforexistence*/
777#defineFL_LEASE32/*leaseheldonthisfile*/
778#defineFL_CLOSE64/*unlockonclose*/
779#defineFL_SLEEP128/*Ablockinglock*/
FL_POSIX锁是通过系统调用fcntl()创建的;而FL_FLOCK锁是通过系统调用flock()创建的(详细内容请参见后文中的介绍)。
FL_FLOCK锁永远都和一个文件对象相关联,打开这个文件的进程拥有该FL_FLOCK锁。
当一个锁被请求或者允许的时候,内核就会把这个进程在同一个文件上的锁都替换掉。
FL_POSIX锁则一直与一个进程以及一个索引节点相关联。
当进程死亡或者文件描述符被关闭的时候,这个锁会被自动释放。
对于强制锁来说,在Linux中,内核提供了inline函数locks_verify_locked()用于检测目标文件或者目标文件所在的设备是否允许使用强制锁,并且检查该设备是否已经加上了锁,相关函数如下所示:
清单3.与强制锁相关的函数
166#define__IS_FLG(inode,flg)((inode)->i_sb->s_flags&(flg))
173#defineIS_MANDLOCK(inode)__IS_FLG(inode,MS_MANDLOCK)
1047/**
1048*locks_mandatory_locked-Checkforanactivelock
1049*@inode:
thefiletocheck
1050*
1051*Searchestheinode'slistoflockstofindanyPOSIXlockswhichconflict.
1052*Thisfunctioniscalledfromlocks_verify_locked()only.
1053*/
1054intlocks_mandatory_locked(structinode*inode)
1055{
1056fl_owner_towner=current->files;
1057structfile_lock*fl;
1058
1059/*
1060*SearchthelocklistforthisinodeforanyPOSIXlocks.
1061*/
1062lock_kernel();
1063for(fl=inode->i_flock;fl!
=NULL;fl=fl->fl_next){
1064if(!
IS_POSIX(fl))
1065continue;
1066if(fl->fl_owner!
=owner)
1067break;
1068}
1069unlock_kernel();
1070returnfl?
-EAGAIN:
0;
1071}
1368/*
1369*Candidatesformandatorylockinghavethesetgidbitset
1370*butnogroupexecutebit-anotherwisemeaninglesscombination.
1371*/
1372#defineMANDATORY_LOCK(inode)\
1373(IS_MANDLOCK(inode)&&((inode)->i_mode&(S_ISGID|S_IXGRP))==S_ISGID)
1374
1375staticinlineintlocks_verify_locked(structinode*inode)
1376{
1377if(MANDATORY_LOCK(inode))
1378returnlocks_mandatory_locked(inode);
1379return0;
1380}
这里,函数locks_verify_locked()利用宏MANDATORY_LOCK来检测目标文件是否允许加锁,条件包括:
该文件所在的设备的super_block结构中的s_flags必须被置为1,该文件的SGID被置为1而且同组可执行位被清0。
如果允许,则调用函数locks_mandatory_locked(),该函数从索引节点的锁列表中查找是否存在有与其相冲突的锁,即是否已经加上了锁。
回页首
Linux中关于文件锁的系统调用
这里介绍在Linux中与文件锁关系密切的两个系统调用:
flock()和fcntl()。
劝告锁既可以通过系统调用flock()来实现,也可以通过系统调用fcntl()来实现。
flock()系统调用是从BSD中衍生出来的,在传统的类UNIX操作系统中,系统调用flock()只适用于劝告锁。
但是,Linux2.6内核利用系统调用flock()实现了我们前面提到的特殊的强制锁:
共享模式强制锁。
另外,flock()只能实现对整个文件进行加锁,而不能实现记录级的加锁。
系统调用fcntl()符合POSIX标准的文件锁实现,它也是非常强大的文件锁,fcntl()可以实现对纪录进行加锁。
flock()
flock()的函数原型如下所示:
intflock(intfd,intoperation);
其中,参数fd表示文件描述符;参数operation指定要进行的锁操作,该参数的取值有如下几种:
LOCK_SH,LOCK_EX,LOCK_UN和LOCK_MANDphost2008-07-03T00:
00:
00
manpage里面没有提到,其各自的意思如下所示:
∙LOCK_SH:
表示要创建一个共享锁,在任意时间内,一个文件的共享锁可以被多个进程拥有
∙LOCK_EX:
表示创建一个排他锁,在任意时间内,一个文件的排他锁只能被一个进程拥有
∙LOCK_UN:
表示删除该进程创建的锁
∙LOCK_MAND:
它主要是用于共享模式强制锁,它可以与LOCK_READ或者LOCK_WRITE联合起来使用,从而表示是否允许并发的读操作或者并发的写操作(尽管在flock()的手册页中没有介绍LOCK_MAND,但是阅读内核源代码就会发现,这在内核中已经实现了)
通常情况下,如果加锁请求不能被立即满足,那么系统调用flock()会阻塞当前进程。
比如,进程想要请求一个排他锁,但此时,已经由其他进程获取了这个锁,那么该进程将会被阻塞。
如果想要在没有获得这个排他锁的情况下不阻塞该进程,可以将LOCK_NB和LOCK_SH或者LOCK_EX联合使用,那么系统就不会阻塞该进程。
flock()所加的锁会对整个文件起作用。
fcntl()
fcntl()函数的功能很多,可以改变已打开的文件的性质,本文中只是介绍其与获取/设置文件锁有关的功能。
fcntl()的函数原型如下所示:
intfcntl(intfd,intcmd,structflock*lock);
其中,参数fd表示文件描述符;参数cmd指定要进行的锁操作,由于fcntl()函数功能比较多,这里先介绍与文件锁相关的三个取值F_GETLK、F_SETLK以及F_SETLKW。
这三个值均与flock结构有关。
flock结构如下所示:
清单4.flock结构
structflock{
...
shortl_type;/*Typeoflock:
F_RDLCK,
F_WRLCK,F_UNLCK*/
shortl_whence;/*Howtointerpretl_start:
SEEK_SET,SEEK_CUR,SEEK_END*/
off_tl_start;/*Startingoffsetforlock*/
off_tl_len;/*Numberofbytestolock*/
pid_tl_pid;/*PIDofprocessblockingourlock
(F_GETLKonly)*/
...
};
在flock结构中,l_type用来指明创建的是共享锁还是排他锁,其取值有三种:
F_RDLCK(共享锁)、F_WRLCK(排他锁)和F_UNLCK(删除之前建立的锁);l_pid指明了该锁的拥有者;l_whence、l_start和l_end这些字段指明了进程需要对文件的哪个区域进行加锁,这个区域是一个连续的字节集合。
因此,进程可以对同一个文件的不同部分加不同的锁。
l_whence必须是SEEK_SET、SEEK_CUR或SEEK_END这几个值中的一个,它们分别对应着文件头、当前位置和文件尾。
l_whence定义了相对于l_start的偏移量,l_start是从文件开始计算的。
可以执行的操作包括:
∙F_GETLK:
进程可以通过它来获取通过fd打开的那个文件的加锁信息。
执行该操作时,lock指向的结构中就保存了希望对文件加的锁(或者说要查询的锁)。
如果确实存在这样一把锁,它阻止lock指向的flock结构所给出的锁描述符,则把现存的锁的信息写到lock指向的flock结构中,并将该锁拥有者的PID写入l_