Advisory Lock

很多时候,在操作系统中,对于特定应用,我们只希望起一个进程实例,但是操作系统并没有限制同一个应用进程的个数,也就是说,在每个应用运行前,我们需要检测进程是否存在。

那么如何检测进程是否已经存在了呢,我们可以在进程启动时,生成一个以 .pid 结尾的文件,并向这个文件上加一个建议性锁( advisory lock ),这样如果有第二个这个的进程起来,它会试着去打开这个 .pid 文件,并试着向这个文件加建议性锁,但由于先起来的进程已经加过锁了,所以第二个进程就没有办法再加锁了,第二个进程知道自己不是第一个起起来的,所以就选择退出。

等等,刚才说的建议性锁又是什么呢,建议性锁就好像人们指定的一系列规则, 例如交通法规建议人们红灯停,绿灯行,这里的红绿灯就是一个建议性锁,但是总有不听话的人不遵守这些规则,也就是说无视建议性锁的存在。建议性锁并不是强制性的,进程可以选择不去遵守它。

上面进程的例子就是这样,第二个进程如果观测到文件已经上锁了,如果它不是恶意进程,那么它会选择尊重规则选择退出。

文件锁

下面我们来实现一个文件锁,文件锁的本质就是文件,所以我们选择对 os.File 做一层封装,然后对它添加额外的行为使其成为一个真正的文件锁。

1
2
3
4
5
6
7
type LockFile struct {
*os.File
}

func NewLockFile(file *os.File) *LockFile {
return &LockFile{file}
}

对文件上锁和解锁的过程其实就是利用系统调用 syscall.Flock,下面是c语言中的系统调用。

1
2
3
4
5
6
7
8
#include <sys/file.h>
#define LOCK_SH 1 /* shared lock */
#define LOCK_EX 2 /* exclusive lock */
#define LOCK_NB 4 /* don't block when locking */
#define LOCK_UN 8 /* unlock */

int
flock(int fd, int operation)

建议性锁分为两大类:共享锁( SH )和专有锁( EX ),这里我们采用专有锁,也就是利用 LOCK_EX 选项,如果文件已经上过锁了,再次尝试对该文件上锁时,操作会被阻塞,为了避免调用此系统调用的进程被阻塞,我们在系统调用时增加一个 LOCK_NB 选项,也就是非阻塞( non blocking )的意思,指定该选项后如果尝试上锁时,文件已经上过锁了,此时则则不会发生阻塞而是返回一个 EWOULDBLOCK 错误,当对文件解锁时,只需要在系统调用时指定 LOCK_UN 选项就可以了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
ErrWouldBlock = errors.New("Resource temporarily unavailable")

func (file *LockFile) Lock() error {
return lockFile(file.Fd())
}

func lockFile(fd uintptr) error {
err := syscall.Flock(int(fd), syscall.LOCK_EX|syscall.LOCK_NB)
if err == syscall.EWOULDBLOCK {
err = ERRWouldBlock
}
return err
}

func (file *LockFile) Unlock() error {
return unlockFile(file.Fd())
}

func unlockFile(fd uintptr) error {
err := syscall.Flock(int(fd), syscall.LOCK_UN)
if err == syscall.EWouldBLOCK {
err = ErrWouldBlock
}
return err
}

OpenLockFile 方法很简单,就是对 os.OpenFile 方法进行了一层封装,将此方法返回的 file 封装为LockFile 返回供接下去使用。

1
2
3
4
5
6
7
func OpenLockFile(name string, perm os.FileMode) (lock *LockFile, err error) {
var file *os.File
if file, err = os.OpenFile(name, os.O_RWWR|os.O_CREATE, perm); err == nil {
lock = &LockFile{file}
}
return
}

进程存在性检测

文件上锁可以用作进程存在性检测,我们在进程创建时生成一个以 .pid 结尾的文件,并向该文件中写入当前进程的进程号,下面的 WritePid 方法就将当前进程的进程号写入文件起始位置,而 ReadPid 则将此文件中存储的 pid 读取出来,注意这里无论是读操作还是写操作都会先将文件的指针定位到最开始的位置(通过 file.Seek(0, os.SEEK_SET) )。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
func (file *LockFile) WritePid() (err error) {
if _, err = file.Seek(0, os.SEEK_SET); err != nil {
return
}
var fileLen int
if fileLen, err = fmt.Fprint(file, os.Getpid()); err != nil {
return
}
if err = file.Truncate(int64(fileLen)); err != nil {
return
}
err = file.Sync()
return
}

func (file *LockFile) ReadPid() (pid int, err error) {
if _, err = file.Seek(0, os.SEEK_SET); err != nil {
return
}
_, err = fmt.Fscan(file, &pid)
return
}

func (file *LockFile) Remove() error {
defer file.Close()

if err := file.Unlock(); err != nil {
return err
}
return os.Remove(file.Name())
}

当我们创建一个 pid 文件时候,我们需要检测其他进程是否已经存在,CreatePidFile 方法会使用 Lock 方法尝试对文件上锁,如果上锁失败,则说明已经有进程对该文件上过锁,此时则应该选择退出,如果上锁成功,则说明自己是第一个进程,并将自己的进程号写入该文件中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func CreatePidFile(name string, perm os.FileMode) (lock *LockFile, err error) {
if lock, err = OpenLockFile(name, perm); err != nil {
return
}
if err = lock.Lock(); err != nil {
lock.Remove()
return
}
if err = lock.WritePid(); err != nil {
lock.Remove()
}
return
}

func ReadPidFile(name string) (pid int, err error) {
var file *os.File
if file, err = os.OpenFile(name, os.O_RDONLY, 0640); err != nil {
return
}
defer file.Close()
lock := &LockFile{file}
pid, err = lock.ReadPid()
return
}
Pieces of Valuable Programming Knowledges