Linux文件锁
- 对于有些应用程序,进程有时需要确保只有它自己能够对某一文件进行 I/O 操作,在这段时间内不允许 其它进程对该文件进行 I/O 操作。为了向进程提供这种功能,Linux 系统提供了文件锁机制。
- 譬如进程对文件进行 I/O 操作时,首先对文件进行上锁,将其锁住,然后再进行读写操作;只要进程没有对 文件进行解锁,那么其它的进程将无法对其进行操作;这样就可以保证,文件被锁住期间,只有它(该进程) 可以对其进行读写操作。
文件锁的分类
建议性锁
- 建议性锁本质上是一种协议,程序访问文件之前,先对文件上锁,上锁成功之后再访问文件,这是建议 性锁的一种用法;但是如果你的程序不管三七二十一,在没有对文件上锁的情况下直接访问文件,也是可以 访问的,并非无法访问文件;如果是这样,那么建议性锁就没有起到任何作用,如果要使得建议性锁起作用, 那么大家就要遵守协议,访问文件之前先对文件上锁。这就好比交通信号灯,规定红灯不能通行,绿灯才可 以通行,但如果你非要在红灯的时候通行,谁也拦不住你,那么后果将会导致发生交通事故;所以必须要大 家共同遵守交通规则,交通信号灯才能起到作用。
强制性锁
- 强制性锁比较好理解,它是一种强制性的要求,如果进程对文件上了强制性锁,其它的进程在没有获取 到文件锁的情况下是无法对文件进行访问的。其本质原因在于,强制性锁会让内核检查每一个 I/O 操作(譬 如 read()、write()),验证调用进程是否是该文件锁的拥有者,如果不是将无法访问文件。当一个文件被上 锁进行写入操作的时候,内核将阻止其它进程对其进行读写操作。采取强制性锁对性能的影响很大,每次进 行读写操作都必须检查文件锁。
flock()函数加锁
- 先来学习系统调用 flock(),使用该函数可以对文件加锁或者解锁,但是 flock()函数只能产生建议性锁
|
fd:参数 fd 为文件描述符,指定需要加锁的文件。
operation:参数 operation 指定了操作方式,可以设置为以下值的其中一个:
- LOCK_SH:在 fd 引用的文件上放置一把共享锁。所谓共享,指的便是多个进程可以拥有对同一 个文件的共享锁,该共享锁可被多个进程同时拥有。
- LOCK_EX:在 fd 引用的文件上放置一把排它锁(或叫互斥锁)。所谓互斥,指的便是互斥锁只 能同时被一个进程所拥有。
- LOCK_UN:解除文件锁定状态,解锁、释放锁。
- LOCK_NB:表示以非阻塞方式获取锁。默认情况下,调用 flock()无法获取到文件锁时会阻塞、直 到其它进程释放锁为止,如果不想让程序被阻塞,可以指定 LOCK_NB 标志,如果无法获取到锁 应立刻返回(错误返回,并将 errno 设置为 EWOULDBLOCK),通常与 LOCK_SH 或 LOCK_EX 一起使用,通过位或运算符组合在一起。
注意,虽然一个程序对文件加锁之后,另一个程序企图加锁文件会失败,但是另一个程序同样可以打开并且编辑这个文件。
|
- 另一个试图读写文件的程序
|
- 使用 kill 命令向 testApp1 进程发送编号为 2 的信号,也就是 SIGIO 信号,testApp1 接收到信号之后, 对 infile 文件进行解锁、然后退出;接着再次执行 testApp2 程序,从打印信息可知,这次能够成功对 infile 文件加锁了,读写也是没有问题的。
- 关于 flock()的几条规则
- 同一进程对文件多次加锁不会导致死锁。当进程调用 flock()对文件加锁成功,再次调用 flock()对 文件(同一文件描述符)加锁,这样不会导致死锁,新加的锁会替换旧的锁。譬如调用 flock()对文 件加共享锁,再次调用 flock()对文件加排它锁,最终文件锁会由共享锁替换为排它锁。
- 文件关闭的时候,会自动解锁。进程调用 flock()对文件加锁,如果在未解锁之前将文件关闭,则会 导致文件锁自动解锁,也就是说,文件锁会在相应的文件描述符被关闭之后自动释放。同理,当一 个进程终止时,它所建立的锁将全部释放。
- 一个进程不可以对另一个进程持有的文件锁进行解锁。
- 由 fork()创建的子进程不会继承父进程所创建的锁。这意味着,若一个进程对文件加锁成功,然后 该进程调用 fork()创建了子进程,那么对父进程创建的锁而言,子进程被视为另一个进程,虽然子 进程从父进程继承了其文件描述符,但不能继承文件锁。这个约束是有道理的,因为锁的作用就是 阻止多个进程同时写同一个文件,如果子进程通过 fork()继承了父进程的锁,则 父进程和子进程就 可以同时写同一个文件了。
- 除此之外,当一个文件描述符被复制时(譬如使用 dup()、dup2()或 fcntl()F_DUPFD 操作),这些通过 复制得到的文件描述符和源文件描述符都会引用同一个文件锁,使用这些文件描述符中的任何一个进行解 锁都可以
fcntl()函数加锁
|
与锁相关的 cmd 为 F_SETLK、F_SETLKW、F_GETLK,第三个参数 flockptr 是一个 struct flock 结构体 指针。使用 fcntl()实现文件锁功能与 flock()有两个比较大的区别:
- flock()仅支持对整个文件进行加锁/解锁;而 fcntl()可以对文件的某个区域(某部分内容)进行加锁 /解锁,可以精确到某一个字节数据。
- flock()仅支持建议性锁类型;而 fcntl()可支持建议性锁和强制性锁两种类型。
结构体参数如下
struct flock { |
具体说明
- l_type:所希望的锁类型,可以设置为 F_RDLCK、F_WRLCK 和 F_UNLCK 三种类型之一,F_RDLCK 表示共享性质的读锁,F_WRLCK 表示独占性质的写锁,F_UNLCK 表示解锁一个区域。
- l_whence 和 l_start:这两个变量用于指定要加锁或解锁区域的起始字节偏移量,与 2.7 小节所学 的 lseek()函数中的 offset 和 whence 参数相同,这里不再重述,如果忘记了,可以回到 2.7 小节再 看看。
- l_len:需要加锁或解锁区域的字节长度。
- l_pid:一个 pid,指向一个进程,表示该进程持有的锁能阻塞当前进程,当 cmd=F_GETLK 时有效。
几条规则
- 锁区域可以在当前文件末尾处开始或者越过末尾处开始,但是不能在文件起始位置之前开始。
- 若参数 l_len 设置为 0,表示将锁区域扩大到最大范围,也就是说从锁区域的起始位置开始,到文 件的最大偏移量处(也就是文件末尾)都处于锁区域范围内。而且是动态的,这意味着不管向该文 件追加写了多少数据,它们都处于锁区域范围,起始位置可以是文件的任意位置。
- 如果我们需要对整个文件加锁,可以将 l_whence 和 l_start 设置为指向文件的起始位置,并且指定 参数 l_len 等于 0。
锁的类型
上面我们提到了两种类型的锁,分别为共享性读锁(F_RDLCK)和独占性写锁(F_WRLCK)。基本的 规则与 12.5 小节所介绍的线程同步读写锁很相似,任意多个进程在一个给定的字节上可以有一把共享的读 锁,但是在一个给定的字节上只能有一个进程有一把独占写锁,进一步而言,如果在一个给定的字节上已经 有一把或多把读锁,则不能在该字节上加写锁;如果在一个字节上已经有一把独占性写锁,则不能再对它加 任何锁(包括读锁和写锁)

如果一个进程对文件的某个区域已经上了一把锁,后来该进程又试图在该区域再加一把锁,那么通常新 加的锁将替换旧的锁。譬如,若某一进程在文件的 100
200 字节区间有一把写锁,然后又试图在 100200 字 节区间再加一把读锁,那么该请求将会成功执行,原来的写锁会替换为读锁。当对文件的某一区域加读锁时,调用进程必须对该文件有读权限,譬如 open() 时 flags 参数指定了 O_RDONLY 或 O_RDWR;当对文件的某一区域加写锁时,调用进程必须对该文件有写 权限,譬如 open()时 flags 参数指定了 O_WRONLY 或 O_RDWR。
F_SETLK、F_SETLKW 和 F_GETLK
- F_GETLK:这种用法一般用于测试,测试调用进程对文件加一把由参数 flockptr 指向的 struct flock 对象所描述的锁是否会加锁成功。如果加锁不成功,意味着该文件的这部分区域已经存在一把锁, 并且由另一进程所持有,并且调用进程加的锁与现有锁之间存在排斥关系,现有锁会阻止调用进程 想要加的锁,并且现有锁的信息将会重写参数 flockptr 指向的对象信息。如果不存在这种情况,也 就是说 flockptr 指向的 struct flock 对象所描述的锁会加锁成功,则除了将 struct flock 对象的 l_type 修改为 F_UNLCK 之外,结构体中的其它信息保持不变。
- F_SETLK:对文件添加由 flockptr 指向的 struct flock 对象所描述的锁。譬如试图对文件的某一区 域加读锁(l_type 等于 F_RDLCK)或写锁(l_type 等于 F_WRLCK),如果加锁失败,那么 fcntl() 将立即出错返回,此时将 errno 设置为 EACCES 或 EAGAIN。也可用于清除由 flockptr 指向的 struct flock 对象所描述的锁(l_type 等于 F_UNLCK)。
- F_SETLKW:此命令是 F_SETLK 的阻塞版本(命令名中的 W 表示等待 wait),如果所请求的读 锁或写锁因另一个进程当前已经对所请求区域的某部分进行了加锁,而导致请求失败,那么调用进 程将会进入阻塞状态。只有当请求的锁可用时,进程才会被唤醒。
规则
- 文件关闭的时候,会自动解锁。
- 一个进程不可以对另一个进程持有的文件锁进行解锁。
- 由 fork()创建的子进程不会继承父进程所创建的锁。
|
- 不同区域加锁
|
- 多个进程对同一文件的相同区域都可以加读锁,说明读锁是共享性的。由于程序 是放置在后台运行的,测试完毕之后,可以使用 kill 命令将这些进程杀死,或者直接关闭当前终端,重新启 动新的终端。
- 第一次启动的进程对文件加写锁之后,后面再启动进程对同一文件的相同区域加写 锁发现都会失败,所以由此可知,写锁是独占性的。
锁的规则同上面的flock()函数
强制性锁会直接影响read()和write()函数的操作(失败会报错),在此处略