记录上锁(fcntl)
它是唯一一个进程终止时内核自动清理的同步锁。这是一种读写锁的扩展类型,他可用于有亲缘关系或无亲缘关系的进程之间共享某个文件的读或写,被锁住的文件通过文件描述符访问,执行上锁的操作时fcntl,这种类型的锁通常在内核中维护,其属主是由属主的进程ID标识,这就说明了锁可用于不同进程之间上锁,而不是统一进程内的不同线程上锁。
应用程序会指定文件中待上锁或解锁的部分字节范围,这个字节范围会跟同一文件内一个或多个逻辑记录有关。
粒度指的是被锁住的对象的大小,对于posix记录上锁来说,粒度就是单个字节,通常情况下粒度越小,允许同时访问的用户数也就越多,fcntl仅仅是个库函数而不是系统调用,使用fcntl完成lockf的实现
#include<fcntl.h> int fcntl(int fd, int cmd ,.../*struct flock* arg*/); //返回:成功取决与cmd,失败返回-1
返回值,与命令有关。
- 出错,所有命令都返回-1。
- 成功则返回某个其他值。下列三个命令有特定返回值:F_DUPFD,F_GETFD,F_GETFL以及F_GETOWN。第一个返回新的文件描述符,第二个返回相应标志,最后一个返回一个正的进程ID或负的进程组ID。
当fcntl用于管理文件记录锁的操作时,第三个参数指向一个struct flock *lock的结构体
struct flock { short l_type;/*锁的类型F_RDLCK:为了获得一把共享锁文件必须以“读”或“读/写”方式打开。F_WRLCK:为了获得一把写(独占)文件也必须以“读”或“读/写”方式打开。F_UNLCK:它用于把一个锁定的区域解锁*/ short l_whence; /*偏移量的起始位置:SEEK_SET,SEEK_CUR,SEEK_END*/ off_t l_start; /*加锁的起始偏移*/ off_t l_len; /*上锁字节即从偏移开始的连续字节数,o means until end-of-file即从起始偏移到文件偏移的最大值*/ pid_tl_pid; /*锁的属主进程ID,pid returned by F_GETLK */ };
跟lseek一样,起始字节偏移是作为一个相对偏移(l_start成员)伴随解释(l_whence成员)指定的,l_whence成员指定的三个值
- SEEK_SET:l_start相对与文件的开头解释
- SEEK_CUR:l_start相对于文件的当前字节偏移(即当前读写指针位置)解释
- SEEK_END:l_start相对于文件的末尾解释
锁住整个文件:
- 指定l_whence成员为SEEK_SET,l_start为0,l_len为0。(最常用,只需要调用一个函数而不是两个)
- 使用lseek把读写指针定位到文件的头,然后指定l_whence成员为SEEK_CUR,l_start为0,l_len为0
cmd把fcntl分为五种操作:
- 复制复制一个现有的描述符。
- F_DUPFD:与旧的文件描述符共同指向同一个文件。如果对象是文件(file)的话,返回一个新的描述符与arg共享相同的偏移量(offset),新的文件描述符与原来的文件描述符操作符一样的某对象的引用,相同的访问模式(读,写或读/写) ,相同的文件状态标志(如:两个文件描述符共享相同的状态标志);原来的文件描述符与新的文件描述符结合在一起的close-on-exec标志被设置成交叉式访问execve(2)的系统调用。fcnlt(oldfd, F_DUPFD, 0) ==dup2(oldfd, newfd)
- F_DUPFD:与旧的文件描述符共同指向同一个文件。如果对象是文件(file)的话,返回一个新的描述符与arg共享相同的偏移量(offset),新的文件描述符与原来的文件描述符操作符一样的某对象的引用,相同的访问模式(读,写或读/写) ,相同的文件状态标志(如:两个文件描述符共享相同的状态标志);原来的文件描述符与新的文件描述符结合在一起的close-on-exec标志被设置成交叉式访问execve(2)的系统调用。fcnlt(oldfd, F_DUPFD, 0) ==dup2(oldfd, newfd)
- 获得/设置文件描述符标记
- F_GETFD:读取文件描述符close-on-exec标志
- F_SETFD:将文件描述符close-on-exec标志设置为第三个参数arg的最后一位类似FD_CLOEXEC。如果返回值和FD_CLOEXEC进行与运算结果是0的话,文件保持交叉式访问exec(),否则如果通过exec运行的话,文件将被关闭(arg被忽略)。fcntl(fd, F_SETFD, 0);//关闭fd的close-on-exec标志
- 获得/设置文件状态标记
- F_GETFL:获取文件打开方式的标志;当执行F_SETLK时fcntl函数返回一个错误时,导致该错误的某个锁的信息可能由F_GETLK命令返回,从而允许我们确定哪个进程锁着了请求的文件区,及上锁的方式,但是也可返回该文件区已经解锁的信息,因为在F_SETLK和F_GETLK之间该文件可能解锁。
- F_SETFL:设置文件打开方式为arg指定方式;O_DIRECT :最小化或去掉reading和writing的缓存影响.系统将企图避免缓存你的读或写的数据.如果不能够避免缓存,那么它将最小化已经被缓存了的数 据造成的影响.如果这个标志用的不够好,将大大的降低性能;O_ASYNC:当I/O可用的时候,允许SIGIO信号发送到进程组,例如:当有数据可以读的时候
- 获得/设置异步I/O所有权
- F_GETOWN或F_SETOWN:设置获取异步I/O所有权(SIGIO,SIGURG)0;s设置当前进程id时如果参数为负,表示该值的绝对值的一个进程组id;取得当前正在接收SIGIO或者SIGURG信号的进程id或进程组id,进程组id返回成负值(arg被忽略)
- 获得/设置记录锁(此时必须使用第三个参数)
- F_SETLK:获取(l_type为_RDLCK或F_WRLCK)或释放(l_type为F_UNLCK)锁。如果无法将锁授予进程,该函数调用立刻返回EACCESS或EAGAIN而不阻塞
- F_SETLKW:与上一个类似,不过不同的是,当无法将所请求的锁授予调用进程,调用线程将阻塞到该锁能够授予为止,该命令会等待相冲突的锁被释放(最后个字母W为wait意思)
- F_GETLK:检查由arg指向的锁以确定是否由某个已存在的锁会妨碍将新锁授予调用进程,如果没有这样的锁存在,arg指向的flock结构的l_type成员就被设置为F_UNLCK,否则,关于这个已存在的锁的信息将在由arg指向的flock结构中返回也就是该结构的内容由fcntl复写,包括持有该锁的进程ID。如果存在一个或多个锁与希望设置的锁相互冲突,则fcntl返回其中的一个锁的flock结构。在F_GETLK命令后发送F_SETLK不是一个原子操作。但是此命令存在的原因是执行F_SETLK返回错误时导致该错误的信息可有F_GETLK返回。
注意:
- 多进程情况下:每个进程可以在该字节区域上设置不同的读锁。但给定的字节上只能设置一把写锁,并且写锁存在就不能再设其他任何锁,且该写锁只能被一个进程单独使用
- 一个进程可以对文件的某个特定的字节范围多次发送F_SETLK or F_SETLKW,每次是否成功取决于其他进程当时是否锁住该字节范围及锁的类型,与本进程先前是否锁着该字节范围无关,也就是说F_SETLK or F_SETLKW会覆盖先前执行同一字节的两个命令,文件能否读写相应的记录与是否被其他进程锁无关(劝告性上锁)也就是说,一个进程可能访问已经被另一个进程独占的锁住文件的记录,但彼此协作的进程应该不去访问
- 调用进程已有的针对同一字节的锁不会妨碍它获取新锁,因为同一进程内,后执行的锁的命令会覆盖先前的命令。例,对一个进程的同一字节范围先后执行F_SETLK(l_type=F_WRLCK)和F_GETLK(l_type=F_RDLCK),这两个命令间无其他进程的干扰,且他们都执行成功,那么F_GETLK返回l_type成员是F_UNLCK
- 针对同一文件的任意字节,最多有一把类型的锁(读入或写出锁),而且给定的一个字节可以有多个多读出锁但只能有一个写入锁;当一个文件不是打开读的,为其请求读出锁会失败,写入锁同理
- 打开文件的进程来说,文件描述符关闭或进程终止,与文件关联的锁都被删除。锁不能由fork子进程继承
- 记录锁不应与标准I/O一块使用。因为该函数库会执行内部缓冲。当某个文件需要上锁时,为避免占个位问题,应该对它使用read、write。
空锁
设文件描述符状态
在修改文件描述符标志或文件状态标志时先要取得现在的标志值,然后按照希望修改它,最后设置新标志值。不能只是执行F_SETFD或F_SETFL命令,这样会关闭以前设置的标志位。
flags = fcntl(sockfd, F_GETFL, 0); //获取文件的flags值。 fcntl(sockfd, F_SETFL, flags | O_NONBLOCK); //设置成非阻塞模式; flags = fcntl(sockfd,F_GETFL,0); fcntl(sockfd,F_SETFL,flags&~O_NONBLOCK); //设置成阻塞模式;
劝告性上锁
POSIX记录上锁也称劝告性上锁。共含义是内核维护着已由各个进程上锁的所有文件的正确信息,它不能防止一个进程写由另一个进程读锁定的某个文件,也不能防止一个进程读已由另一个进程写锁定的文件。一个进程能够无视劝告性锁而写一个读锁文件,或读一个写锁文件,前提时该进程有读或写该文件的足够权限。
劝告性上锁对于协作进程足够。如网络编程中的协程,这些程序访问如序列号文件的共享资源,而且都在系统管理员的控制下,只要包含该序列号的真正文件不是任何进程都可写,那么在该文件被锁住期间,不理会劝告性锁的进程随意进程无法访问写它。
强制性上锁
另一种类型的记录性上锁。对于强制性上锁,内核检查每个read和write请求,以验证其操作不会干扰由某个进程持有的某个锁,对于通常的时阻塞描述符,与某个强制性锁冲突的read和write将把调用进程投入睡眠,直到该锁释放为止,对于非阻塞描述符,与某个强制锁冲突的read和write将导致他们返回一个EAGAIN。
为对某个特定的文件实行强制锁应满足:
- 组成员执行位必须关掉
- SGID位必须打开
打开某个文件的SUID位而不打开他的用户执行位是没有意义的,同样打开SGID位而不打开组成员的执行位也是没有意义的,因此这种方式的强制性上锁不会影响任何现有的用户软件,强制性上锁不需要新的系统调用。
在支持强制性上锁的系统上,ls输出l或L以指示相应的文件是否强制性上锁。chmod接受l这个指示给某个文件以启用强制性上锁。
- linux 内核会阻塞其他进程的 IO 请求
- 可以通过删除锁文件绕过
与进程关联
- 当一个进程终止时,所建立的所有锁全部被释放
- 关闭一个文件描述符,会释放对该文件的所有锁,包括对其他指向相同文件的文件描述符加的锁
- fork 产生的子进程并不继承父进程所设置的锁
- 在执行
exec
后,新程序可以继承原程序的锁(如果对fd设置了close-on-exec,则exec前会关闭fd,相应文件的锁也会被释放)
与文件描述符关联
- 当一个文件描述符及其所有副本(包括子进程继承的和
dup
的)关闭时,才会释放对其建立的锁 fork
的子进程由于继承了文件描述符,所以也继承了其上的锁- 子进程对继承的文件描述符上的锁进行修改/解锁,会影响到父进程的锁(对于
dup
出的副本同样试用) - 在执行
exec
后,新程序可以继承原程序的锁
文件锁作用
linux有几种技巧可创建文件锁。
- 如果以O_CREAT(文件不存在则创建)和O_EXCL(独占打开)标志调用open,如果文件存在则返回错误。考虑到其它进程的存在,文件是否存在和创建文件应该是原子的。所以可以把这种技巧创建的文件作为锁使用。
- 如果新链接的名字已经存在,那么link将失败,为获取一个锁,首先创建一个唯一的临时文件,其路径名是含有调用进程的id(如果不同进程中的线程以及同一进程内的线程都需要上锁,那么所含的是进程id和线程id的某种组合),然后以建立文件众所周知的路径名调用link函数创建这个临时文件的一个连接,如果创建成功,该临时路径名可以unlink掉。当调用线程使用完该锁时,只需unlink众所周知的路径名就可以解锁;如果link失败返回EEXIST错误,调用线程就需重新尝试。这要求临时文件路径名和锁文件众所周知的路径名在同一个文件系统中,因为硬链接不能跨越文件系统。
- 如果待打开文件已经存在,且打开时指定了O_TRUNC标志,而且调用进程不具备写访问权限,那么open将返回个错误。为获取一个锁,我们在指定O_CREATE|O_WRONLY|O_TRUNC标志并置mode参数为0(即打开的新文件不存在任何权限位)的前提下调用open,如果调用成功我们就持有该锁,以后使用完之后只需unlink路径名,如果open失败调用EACCES错误,调用线程就必须重新尝试,但是这种做法在线程具有超级用户特权下不起作用。
以下是第一中创建办法。
/************************************************************************* > File Name: test.cpp > Author: Chen Tianzeng > Mail: 971859774@qq.com > Created Time: 2019年04月15日 星期一 08时33分04秒 ************************************************************************/ #define LOCKFILE "/home/..." /* * 如果open成功,我们就持有相应的文件锁,函数返回前close原文件,不需要文件 * 描述符因为文件的本身存在代表锁,至于他是否打开无关紧要,如果存在再次尝试 * open */ my_lock(int fd) { int tfd; /* * someone else has the lock,loop around and try again * */ //如果文件不存在就创建他和独占打开它(O_EXCL),检查该文件的存在和创建必须是原子的 while((tfd=open(LOCKFILE,O_RDWR|O_CREATE,O_EXCL,FILE_MODE))<0) { if(errno!=EEXIST) { cerr<<"open error for lock file: "<<strerror(errno)<<endl; exit(-1); } //opend the file,we have the lock close(tfd); } } void mu_unlock(int fd) { unlink(LOCKFILE); }
但存在问题:
- 如果进程持有的锁没有终止就释放它,那么文件名并未删除。解决办法:检查文件最近访问时间,大于某个确定的数量就删除;把进程持有的id写进锁文件,但是进程id在一段时间后会被重用。这种问题对于fcntl不成问题,因为它持有的锁会在进程终止时完全释放
- 如果另外的进程以打开文件,该进程只是无限的open,可以先sleep1s,然后再open,再次尝试open。如果时fcntl指定FSETLKW命令,内核把进程投入睡眠,直到该锁可用,然后唤醒它
- 调用open和unlink删除一个额外的文件涉及系统访问时间,通常比fcntl调用两次(一次获取锁一次释放锁)花去的时间要长