前言

很多时候我们都需要进程单例运行,当再次运行程序时检查到已有程序在运行可以做特别的操作,比如置顶已运行的程序,比如当前程序提示一下就退出。

最简单方案是打开进程创建一个文件,程序结束时删除文件,当第二个程序运行时判断该文件存在则认为已有程序运行。问题是程序异常退出没有删除那个文件就GG了。

还有方案就是进程启动时锁住一个文件,进程退出释放锁,进程异常退出由系统自动释放锁。这个就是完美的方案。

还有一种方案就是判断进程名是否允许,Linux下可以执行【ps auxf | grep xxx】,Windows下可以执行【tasklist | findstr xxx】。问题就是程序文件名被改就GG。

另外还可以监听一个tcp端口,如果第二个进程运行也监听相同端口会报错,因此也能实现进程单例运行,只是需要占用一个端口。在win平台下还可以通过创建一个互斥体实现进程单例。

flock

命令介绍

该工具是用来获取一个文件锁,并执行命令。当文件已经被锁则不会执行命令,使用该命令可有效防止重复执行一些操作。特别是cron脚本,由于脚本执行周期长,下一次定时执行又到了,可以不重复执行。Windows下也是可以安装flock命令的,我是安装【msys2】后默认就有了。下面看下该工具的帮助文档:

flock -h

Usage:
flock [options] <file>|<directory> <command> [<argument>...]
flock [options] <file>|<directory> -c <command>
flock [options] <file descriptor number>

Manage file locks from shell scripts.

Options:
-s, --shared get a shared lock // 获取读锁(共享锁),多个进程可获取并读文件,其他进程获取写锁会返回失败
-x, --exclusive get an exclusive lock (default) // 获取写锁(排它锁),只允许一个进程获取,其他进程获取均返回失败
-u, --unlock remove a lock
-n, --nonblock fail rather than wait // 当获取锁失败时直接返回,不带此选项程序会卡住,直到获取到锁
-w, --timeout <secs> wait for a limited amount of time // 获取锁超时的时间
-E, --conflict-exit-code <number> exit code after conflict or timeout // 当获取锁失败时,返回的错误码.在Linux执行【echo $?】,win执行【echo %errorlevel%】查看
-o, --close close file descriptor before running command
-c, --command <command> run a single command string through the shell // 锁住文件的同时执行一个命令,我一般用来执行一个脚本
--verbose increase verbosity

-h, --help display this help and exit
-V, --version output version information and exit

For more details see flock(1).

flock实例

2135498202010281754372291385525527.gif

代码实现

目录

filelock
    |___filelock.go
    |___filelock_linux.go
    |___filelock_windows.go

filelock.go

package filelock

import (
    "errors"
    "os"
)

var ErrFileLock = errors.New("file is lock")

// 排它锁锁住文件
func Lock(f *os.File) error {
    return lock(f, WriteLock)
}

// 共享锁锁住文件
func RLock(f *os.File) error {
    return lock(f, ReadLock)
}

// 释放文件锁
func Unlock(f *os.File) error {
    return unlock(f)
}

type file struct {
    File *os.File
}

// 打开文件并带上锁
func LockOpenFile(name string, flag int, perm os.FileMode, lt lockType) (*file, error) {
    fr, err := os.OpenFile(name, flag, perm)
    if err != nil {
        return nil, err
    }
    if err = lock(fr, lt); err != nil {
        fr.Close()
        return nil, err
    }
    return &file{File: fr}, nil
}

// 释放锁并关闭文件
func (f *file) Close() error {
    err := unlock(f.File)
    if closeErr := f.File.Close(); err == nil {
        err = closeErr
    }
    return err
}

filelock_linux.go

package filelock

import (
    "os"
    "syscall"
)

type lockType int

const (
    ReadLock  lockType = syscall.LOCK_SH
    WriteLock lockType = syscall.LOCK_EX
)

func lock(f *os.File, lt lockType) error {
    err := syscall.Flock(int(f.Fd()), int(lt)|syscall.LOCK_NB)
    if err != nil {
        if errNo, ok := err.(syscall.Errno); ok && errNo == 0xb {
            return ErrFileLock // 找到文件被锁错误码,返回自定义错误
        }
    }
    return err
}

func unlock(f *os.File) error {
    return syscall.Flock(int(f.Fd()), syscall.LOCK_UN)
}

filelock_windows.go


package filelock

import (
    "os"
    "syscall"
    "unsafe"
)

type lockType uint32

const (
    ReadLock  lockType = 0
    WriteLock lockType = 3 // LOCKFILE_FAIL_IMMEDIATELY | LOCKFILE_EXCLUSIVE_LOCK

    reserved = 0
    allBytes = ^uint32(0)
)

var (
    modKernel32      = syscall.NewLazyDLL("kernel32.dll")
    procLockFileEx   = modKernel32.NewProc("LockFileEx")
    procUnlockFileEx = modKernel32.NewProc("UnlockFileEx")
)

func lock(f *os.File, lt lockType) error {
    ol := new(syscall.Overlapped)
    r1, _, e1 := syscall.Syscall6(procLockFileEx.Addr(), 6, f.Fd(),
        uintptr(lt), uintptr(reserved), uintptr(allBytes),
        uintptr(allBytes), uintptr(unsafe.Pointer(ol)))
    if r1 == 0 {
        if e1 != 0 {
            if e1 == 0x21 { // 找到文件被锁错误码,返回自定义错误
                return ErrFileLock
            }
            return e1
        }
        return syscall.EINVAL
    }
    return nil
}

func unlock(f *os.File) error {
    ol := new(syscall.Overlapped)
    r1, _, e1 := syscall.Syscall6(procUnlockFileEx.Addr(), 5, f.Fd(),
        uintptr(reserved), uintptr(allBytes), uintptr(allBytes),
        uintptr(unsafe.Pointer(ol)), 0)
    if r1 == 0 {
        if e1 != 0 {
            return e1
        }
        return syscall.EINVAL
    }
    return nil
}

获取文件锁

package main
 
import (
    "os"
    "strconv"
    "time"
 
    "filelock"
)
 
func main() {
    fr, err := filelock.LockOpenFile("a.txt", os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666, filelock.WriteLock)
    if err != nil {
        panic(err)
    }
    defer fr.Close()
    fr.File.Write([]byte(strconv.Itoa(os.Getpid())))
    time.Sleep(time.Second * 5)
}

文件锁判断单例

可以根据返回错误值判断文件是否被锁住,如果锁住就认为已有进程在执行。

package main
 
import (
    "fmt"
 
    "github.com/jan-bar/golibs"
)
 
func main() {
    err := golibs.SingletonFile("a.txt")
    if err == golibs.ErrSingleton {
        fmt.Println("已有进程在运行")
    }
}

tcp端口判断单例

如果本机指定端口已经被监听,则证明已有程序在运行。

package main
 
import (
    "fmt"
 
    "github.com/jan-bar/golibs"
)
 
func main() {
    err := golibs.SingletonTcp(8080)
    if err == golibs.ErrSingleton {
        fmt.Println("已有进程在运行")
    }
}

创建互斥体实现单例

原理是使用win32api中的CreateMutexW创建一个互斥体,多个进程都创建同名互斥体时如果已有进程先创建后面的进程会提示已存在。

package main
 
import (
    "fmt"
 
    "github.com/jan-bar/golibs"
)
 
func main() {
    err := golibs.SingletonWin("process")
    fmt.Println(err)
    fmt.Scanln()
}

总结

进程单例运行在很多场景都是有必要的,上面介绍的几种方案,我比较喜欢通过加锁文件来判断进程是否已经被运行过,因为只占用一个文件。很多工具都是通过锁文件,并将自己的pid写入文件,方便其他操作去读取这个进程的pid,而不用通过ps去查询,堪称完美。我只测试了Windows和Linux下的文件锁功能,Windows是使用win32api去实现,Linux是系统自带的接口。