应用单例检测器

前言

有时候我们开发一个应用程序,需要让该应用程序保证进程级别的单例,例如需要独占某些资源的情况。

线程互斥锁

无论是 Win32 还是 POSIX,都提供了线程之间的互斥锁(Mutex),可以用其让某些资源在同一时间只能被一个线程独占。

进程互斥锁

为了实现进程级别的单例,我们需要进程级别的互斥锁。

正文

我认为,一个合格的进程互斥锁应该符合如下几点:

  1. 每一把锁都可以有唯一的标识符,并且所有进程都可以通过标识符获取锁。
  2. 同一时间只能有一个进程持有锁,其他进程可以尝试持有同一把锁,并返回结果。
  3. 进程退出时,锁会被自动释放,无论是正常退出还是异常退出。
  4. 锁的完整性和唯一性在正常使用过程中几乎不可能因用户误操作而被破坏(例如内核态的资源)。

以下是一些思路:(注:本文只提供思路,不提供具体代码示例,若要进一步了解,请读者自行查阅相关 API 资料)

Win32 互斥锁

<synchapi.h>
这是 Windows XP 起引入的 Win32 API,动态库是 Kernel32.dll
通过该 API 可以创建进程级别的命名互斥锁,符合 1、2、3、4。

  1. 唯一标识符——互斥锁的名称。所有进程都可以通过 CreateMutex 传参名称试图创建同一把锁。
  2. 通过 CreateMutex 可以尝试创建锁并立即返回结果。
  3. 进程退出时会销毁持有的互斥锁。
  4. 互斥锁是内核态资源,符合。

POSIX 信号量

<semaphore.h>
这是 POSIX API 的一部分,绝大多数现代 POSIX 系统都支持。动态库是 librt
通过该 API 可以创建命名信号量(Semaphore),符合 1、2,不符合 3,不一定符合 4。

  1. 唯一标识符——信号量的名称。所有进程都可以通过 sem_open 传参名称获取同一把锁。
  2. 通过 sem_trywait 可以尝试获取锁并立即返回结果。
  3. 进程退出时会调用 sem_close 而不是 sem_unlink,而 sem_unlink 才能移除命名信号量。命名信号量会一直在系统中保留,直到下一次手动调用 sem_unlink 或者重启系统。
  4. 是否符合本条根据具体实现而定。例如:GNU/Linux 使用共享内存实现,符合。

POSIX 文件锁

<fcntl.h>
这是 POSIX API 的一部分,POSIX 兼容的 C 运行时库基本都支持。
通过该 API 可以创建 POSIX 文件锁,符合 1、2、3,不一定符合 4。
需要注意的是,被 POSIX 文件锁锁住的文件仍可以被修改内容和删除。

  1. 唯一标识符——文件。
  2. 通过 fcntl 可以尝试锁住文件并立即返回结果。
  3. 进程退出时 POSIX 文件锁会被销毁。
  4. 是否符合取决于锁文件是哪个文件,被存放在什么位置,以及文件权限(由开发者来指定)。

FLOCK 文件锁

<sys/file.h>
这是 BSD 风格的文件 API,但一些 *BSD 以外的系统(例如 GNU/Linux)也部分支持。
通过该 API 可以创建 FLOCK 文件锁,符合 1、2、3,不一定符合 4。
需要注意的是,被 FLOCK 文件锁锁住的文件仍可以被修改内容和删除。

  1. 唯一标识符——文件描述符。
  2. 通过 flock 可以尝试锁住文件并立即返回结果。
  3. 进程退出时 FLOCK 文件锁会被销毁。
  4. 是否符合取决于锁文件是哪个文件,被存放在什么位置,以及文件权限(由开发者来指定)。

PIDFile

这是 UNIX 脚本常用的检测手段。
符合 1、2,不一定符合 3、4。

  1. 对特定文件写入进程 PID,作为“锁”。
  2. 通过读取文件和查询文件中的 PID 是否存在于 PID 列表可以尝试获取“锁”并立即返回结果。
  3. 进程异常退出时文件可能未被销毁(取决于开发者是否做了异常退出相关的工作,以及异常退出的实际情况)。
  4. 是否符合取决于锁文件是哪个文件,被存放在什么位置,以及文件权限(由开发者来指定)。

扩展阅读

除此之外,还有使用 DBUS、占用端口、使用 UNIX Domain Socket 和使用共享内存等手段,在此不多赘述,有兴趣可以自行了解。