本文参考《Unix 环境高级编程》,Mac 下实验结果可能会和书上有所不同(因为书上是以 freeBSD 进行实验),希望读者可以在不同的系统下进行实验,如果文章有错误的地方,还请提出,我会及时修正。
一、背景:
试想一下当两个人同时编辑一个文件时,其后果是什么样的呢?在 unix 系统中,文件的状态取决于写该文件的最后一个进程,比如数据库系统,需要保证多个进程写文件,依旧保持正确性。因此 unix 提供了记录锁的机制,作用是当一个进程正在读或者写一个文件时可以阻止另一个进程对同一个区域进行保护避免冲突。
在 go 语言里提供了两个接口来实现记录锁
-
syscall.Flock
:支持整个文件记录锁 -
syscall.FcntlFlock
:支持字节范围记录锁
二、文件记录锁
2.1: 说明
syscall:
Flock(fd int, how int) (err error)
fd
: 为打开的文件描述符how
: 参数可取的值为
how | 作用 |
---|---|
syscall.LOCK_SH | 放置共享锁 |
syscall.LOCK_EX | 放置互斥锁 |
syscall.LOCK_UN | 解锁 |
syscall.LOCK_NB | 非阻塞锁请求 |
2.2: 持有锁规则
在默认情况下如果另一个进程持有了一把锁,那么调用 syscall.Flock
会被阻塞,如果设置为 syscall.LOCK_NB
就不会阻塞而是直接返回error
。
任意数量的进程可以持有文件共享锁,但是同一时刻只能有一个进程持有互斥锁,如果一个进程持有互斥锁,那其他进程的无法持有共享锁,持锁进程可以通过在再次调用 syscall.Flock
并设置对应的 how
值来切换锁的类型,另一个进程持有锁就会被阻塞,可以通过设置 syscall.LOCK_NB
变为非阻塞
锁的转换不是原子性的,在转换的过程中会删除原先的锁,在创建一个新的锁。在这两步之间另一个进程对一个不兼容锁的悬而未决 (pending) 请求可能会得到满足。如果发生了这种情况,那么转换过程会被阻塞,或者在指定了syscall.LOCK_NB
的情况下转换过程会失败并且进程会丢失其原先持有的锁。
2.3: 释放锁
把 how
参数设置为 syscall.LOCK_UN
和关闭文件描述符都可以释放锁,事实上还有更复杂的情况(后面会讲到)。
2.4: syscall.Flock 的限制
- 只能对整个文件进行加锁,锁颗粒度大,影响并发性
- 只能设置建议性锁
- NFS 不识别该锁
三、字节范围记录锁
3.1: 说明
syscall.FcntlFlock
弥补了 syscall.Flock
的不足之处,可以在一个文件的一小部分进行加锁。
syscall:
FcntlFlock(fd uintptr, cmd int, lk *Flock_t) error
fd
: 为打开的文件描述符
cmd
: 支持三种命令
cmd | 说明 |
---|---|
syscall.F_GETLK | 检测是否被上锁,如果该区域被上锁,会重写 lk 的Type 字段为 syscall.F_UNLCK , 其余字段不变,如果获取失败会返回这把锁的相关信息包括持有这把锁的 pid |
syscall.F_SETLK | 设置一把非阻塞锁,当另一个进程持有锁时会返回 |
syscall.F_SETLKW | 设置一把阻塞锁,当另一个进程持有锁时会进入睡眠状态,直到对方释放锁 |
注意:检测锁然后获取 并不是原子性的
lk
:lk 参数是一个结构体
syscall:
type Flock_t struct {
Start int64 // 根据 Whence 决定偏移量,可以为负数
Len int64 // 锁定字节数,0 代表直到 EOF
Pid int32 // 阻止我们取得锁的 pid (F_GETLK only)
Type int16 // 锁类型: F_RDLCK, F_WRLCK, F_UNLCK
Whence int16 // io.SeekStart (起始),io.SeekCurrent(当前), io.SeekEnd(结尾)
}
Type
的字段解释如下:
type | 说明 |
---|---|
syscall.F_RDLCK | 设置读锁 |
syscall.F_WRLCK | 设置写锁 |
syscall.F_UNLCK | 释放一把锁 |
3.2: 持有锁
1、给定范围的字节区域可以拥有多个读锁,但是只能有一个写锁,同样一个区域同一个时刻只能拥有一种锁。
2、如果对给定范围的字节区域进行再次加锁时,如果锁相同那么不会发生任何事,如果是不同的锁,那么会 原子性
的转换为新锁,并且读锁转换为写锁时需要为调用返回一个错误或为阻塞做好准备
3.3: 释放锁:
1、将 Type 字段设置为 syscall.F_UNLCK
,或者关闭文件描述符
2、如果对一个锁区域中间部分进行解锁,那么中间部分的左右部分会变成两个不同的锁区,如果在对中间部分进行加锁(和之前一样的锁)那么有会合并一个大的锁区
3.4: 死锁:
假设有 P1、P2 两个进程
P1 对文件的 0 字节进行加锁
P2 对文件的 1 字节进行加锁
这时 P1 又试图对 1 字节进行加锁或者 P2 又试图对 0 字节进行加锁
这样就会造成死锁,当系统检测到死锁时会选择一个进程接收错误并返回,
四:锁的隐含继承以及释放细节
1、复制出来的文件描述符会引用同一个锁,必须把 所有
的文件描述符关闭,才会释放锁,或者显式的调用 LOCK_UN
进行释放锁
newFd,_ := syscall.Dup(fd) // 复制文件描述符
syscall.Flock(fd, syscall.LOCK_EX)
1:
syscall.Close(fd)
syscall.Close(newFd)
2:
flock(newFd, syscall.LOCK_UN)
2、多次打开文件得到的文件描述符
f1,_ := os.OpenFile("lock.lc", os.O_APPEND|os.O_CREATE, 0666)
f2,_ := os.OpenFile("lock.lc", os.O_APPEND|os.O_CREATE, 0666)
_ = syscall.Flock(int(f1.Fd()), syscall.LOCK_EX)
_ = syscall.Flock(int(f2.Fd()), syscall.LOCK_EX) // 这里会被阻塞,因为 f1 上的锁还没有释放
3、当程序 syscall.Exec 一个进程时,该进程可以继承之前的文件锁,如果是 syscall.ForkExec()出来的进程需要修改文件那么就需要再次获取这把锁。
五、补充
值得注意的这里说的记录锁都是 建议锁
, 因此使用锁的两个进程必定是互相协作的, 如果需要强制性锁需要自行开启操作系统支持
六、实例
实例代码
本文参考《Unix 环境高级编程》,Mac 下实验结果可能会和书上有所不同(因为书上是以 freeBSD 进行实验),希望读者可以在不同的系统下进行实验,如果文章有错误的地方,还请提出,我会及时修正。