目录

文件锁是多进程协作的基础设施,常见于单实例守护进程、日志追加互斥、配置热更新加锁、下载临时文件防重等场景。

跨进程协调时,文件锁往往是成本最低、通用性最好的手段之一。

然而它也是容易被”想当然”带偏的部分。

三大操作系统机制各异,连”锁究竟作用于什么对象”这一基本问题,三平台的答案也各不相同。

本文依据官方文档,分别剖析 Linux、Windows、macOS 的锁机制,每种均给出可编译运行的 C++ 与 Go 示例,并标注优缺点、使用注意与常见陷阱。

为什么文件锁是跨平台的重灾区

根本原因可以归结为一条:文件锁从未被任何跨平台标准统一。

POSIX 仅规范了 fcntl 记录锁,flock 源于 BSD 体系,Windows 则是一套独立的 Win32 API。

因此”加锁”这一行为在三平台间,于下列四个维度上均存在差异:

  • 建议性 vs 强制性:建议性锁(advisory)仅约束遵循约定的进程,未持锁的进程仍可直接读写;强制性锁(mandatory)由内核拦截。Linux 默认均为建议性;Windows 的 LockFile 系列对其他句柄/进程强制生效(同进程第二句柄受约束,见后文);macOS 仅支持建议性。
  • 整文件 vs 字节范围:flock 锁整个文件,fcntl 和 LockFileEx 可以只锁某几个字节。
  • 锁的归属:这是容易忽略的一个维度。flock 的锁关联于”打开文件描述(open file description)“;POSIX fcntl 锁关联于”进程 + inode”;Windows 锁关联于”文件句柄(HANDLE)“。归属模型不同,fork、dup 及重复打开文件时的行为便截然不同。
  • fork / dup / close 行为:进程 fork 后,锁随子进程继承还是保留在父进程?关闭一个 fd,锁随之释放还是保持不变?各平台语义不同。

三平台核心机制对照:

平台主力接口锁粒度性质同平台锁机制关系
Linuxflock / fcntl / OFD 锁整文件 / 字节默认建议性flock 与 fcntl 互不感知
WindowsLockFile / LockFileEx字节范围句柄级、对他人强制对其他句柄/进程强制生效
macOSflock / fcntl整文件 / 字节建议性flock 与 fcntl 互相感知

Linux:flock / fcntl / OFD 三种锁机制

Linux 提供三种层面不同的锁,官方文档以 man7.org 的 flock(2)、fcntl(2) 为准。

flock:整文件建议性锁

flock() 自 Linux 2.0 起作为独立系统调用实现(不再由 fcntl 模拟),接口简单:对整个文件加独占锁(LOCK_EX)或共享锁(LOCK_SH),LOCK_NB 表示非阻塞。

C++ 最小示例(只展示原理):

#include <sys/file.h>
#include <fcntl.h>
#include <unistd.h>

int main() {
    int fd = open("/tmp/app.lock", O_RDWR | O_CREAT, 0644);

    flock(fd, LOCK_EX);          // 独占锁,冲突时阻塞等待
    // ... 临界区:同一时刻只有一个进程能进入 ...
    flock(fd, LOCK_UN);          // 解锁
    close(fd);
    return 0;
}

同一逻辑的 Go 写法(仅 Unix 平台):

f, _ := os.OpenFile("/tmp/app.lock", os.O_RDWR|os.O_CREATE, 0o644)

syscall.Flock(int(f.Fd()), syscall.LOCK_EX)  // 独占锁,阻塞等待
// ... 临界区 ...
syscall.Flock(int(f.Fd()), syscall.LOCK_UN)

f.Close()

特性:锁关联于 open file description,dup / fork 得到的 fd 指向同一把锁,经由任一 fd 解锁均会释放。 优势:接口仅一行,整文件互斥场景(如守护进程哨兵锁)十分便捷。 不足:仅支持整文件锁,无法锁定任意区段;且默认建议性,未持锁的进程可直接读写,无法阻止。 注意:fork 后父子共享同一把锁,子进程执行 LOCK_UN 会一并释放父进程的锁,此点易被忽略。此外 flock 不做死锁检测(传统 fcntl 记录锁会做),多进程以不同顺序争抢多把锁时可能静默死锁。

fcntl:字节范围记录锁

fcntl(fd, F_SETLK / F_SETLKW, &flock) 是 POSIX 记录锁,能指定 l_start / l_len 锁任意字节区间,l_type 取 F_RDLCK(共享读)/ F_WRLCK(独占写)/ F_UNLCK。F_SETLKW 的 W 表示冲突时阻塞等待。

C++ 最小示例(锁文件前 512 字节):

#include <fcntl.h>
#include <unistd.h>

int main() {
    int fd = open("/tmp/app.dat", O_RDWR | O_CREAT, 0644);

    struct flock fl = {0};
    fl.l_type = F_WRLCK; fl.l_whence = SEEK_SET; fl.l_start = 0; fl.l_len = 512;
    fcntl(fd, F_SETLKW, &fl);    // 锁 [0,512) 字节,冲突则阻塞等待
    // ... 临界区 ...
    fl.l_type = F_UNLCK; fcntl(fd, F_SETLK, &fl);
    close(fd);
    return 0;
}

Go 等效写法(用标准库 syscall.FcntlFlock):

f, _ := os.OpenFile("/tmp/app.dat", os.O_RDWR|os.O_CREATE, 0o644)

fl := syscall.Flock_t{
    Type:   syscall.F_WRLCK,
    Whence: int16(io.SeekStart),
    Start:  0, Len: 512,
}

syscall.FcntlFlock(uintptr(f.Fd()), syscall.F_SETLKW, &fl)  // 锁 [0,512)
// ... 临界区 ...
fl.Type = syscall.F_UNLCK
syscall.FcntlFlock(uintptr(f.Fd()), syscall.F_SETLK, &fl)
f.Close()

特性:锁与进程关联,可跨进程互斥;支持字节级粒度,适合多进程分别锁定文件不同区段后并发读写。将 l_len 设为 0 可把锁范围延伸至文件末尾(EOF),从而锁定整个文件。 优势:粒度细、可共享读锁、能跨 NFS(取决于配置)。 不足:锁归属进程,隐患尤其突出:同一进程只要 close 与 fcntl 锁关联的同一 open file description 上的任意一个 fd,该 open file description 上的全部 fcntl 锁会立即释放,即便仍有其他 fd 处于打开状态;若两次 open 得到的是不同 open file description,关闭其中一个不影响另一个的锁。此外 fcntl 锁不随 fork 继承。 注意:此”关闭任一 fd 即释放全部锁”的语义源自 System V,Apple 的 man page 亦直言其行为 completely stupid。若调用链中某库函数暗中 open/close 了该文件,锁会无声丢失。

OFD 锁:多线程场景的实现方式

fcntl 锁关联于进程,多线程程序中的多个线程共享同一进程身份,无法借 fcntl 在线程间互斥。Linux 3.15 起引入 OFD 锁(F_OFD_SETLK 系列,glibc 2.20+),其锁改关联于 open file description,与 flock 归属模型一致:各线程独立 open 取得 fd,即可用锁同步;锁亦随 fork 继承,仅在最后一个引用 fd 关闭时释放。代价是内核不为 OFD 锁执行死锁检测(传统进程级 fcntl 锁则会做死锁检测)。

// OFD 锁最小示例(仅 Linux 3.15+ / glibc 2.20+)
struct flock fl = {0};
fl.l_type = F_WRLCK; fl.l_whence = SEEK_SET; fl.l_start = 0; fl.l_len = 0;
// 注意命令是 F_OFD_SETLK / F_OFD_SETLKW,且 l_pid 字段必须置 0
fcntl(fd, F_OFD_SETLKW, &fl);

Go 侧标准库 syscall 没有导出 F_OFD_* 常量,要用需自行定义常量后走 syscall.FcntlFlock,或直接依赖 golang.org/x/sys/unix 提供的 unix.F_OFD_SETLKW。

Linux 的坑:flock 与 fcntl 互不感知

这是 Linux 特有的行为:同一文件上,进程 A 以 flock 持有独占锁,进程 B 仍可用 fcntl 加锁,二者互不感知。man7.org flock(2) 原文:

Since Linux 2.0, flock() is implemented as a system call in its own right rather than being emulated in the GNU C library as a call to fcntl(2). With this implementation, there is no interaction between the types of lock placed by flock() and fcntl(2)…

而在 macOS / BSD 上二者互通(见下文实测)。因此跨平台代码应在同一程序内只采用一种锁接口,避免混用。

Windows:LockFileEx 一家独大

Windows 既无 flock 也无 POSIX fcntl,仅提供 Win32 的 LockFile / LockFileEx(以 Microsoft Learn 文档为准),其锁为字节范围、句柄级;内核会在所有通过常规 HANDLE 的访问上强制实施该锁,因此对其他进程/其他句柄表现为“强制”拒绝,但本质上仍属于由内核强制实施的建议性锁,与 Linux 曾经意义的 mandatory locking 不同。

C++ 最小示例(锁文件前 1 字节):

#include <windows.h>

int main() {
    HANDLE h = CreateFileA(
        "C:\\temp\\app.lock",
        GENERIC_READ | GENERIC_WRITE,
        0, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);

    OVERLAPPED ol = {0};
    LockFileEx(h, LOCKFILE_EXCLUSIVE_LOCK, 0, 1, 0, &ol);  // 锁前 1 字节,阻塞等待
    // ... 临界区 ...
    UnlockFileEx(h, 0, 1, 0, &ol);
    CloseHandle(h);
    return 0;
}

Go 等效写法(用 syscall.NewLazyDLL 直接调 kernel32,不依赖第三方库):

mod := syscall.NewLazyDLL("kernel32.dll")
lockEx := mod.NewProc("LockFileEx")
unlockEx := mod.NewProc("UnlockFileEx")
var ol syscall.Overlapped

// h 来自 syscall.CreateFile(见下文跨平台封装)
// 独占锁前 1 字节
r, _, _ := lockEx.Call(uintptr(h), uintptr(0x2), 0, 1, 0,
    uintptr(unsafe.Pointer(&ol)))
if r == 0 { return syscall.GetLastError() }

// ... 临界区 ...

r, _, _ = unlockEx.Call(uintptr(h), 0, 1, 0, uintptr(unsafe.Pointer(&ol)))
if r == 0 { return syscall.GetLastError() }

特性:锁作用于 HANDLE 指定的字节区间,偏移由 OVERLAPPED 结构给出;LOCKFILE_EXCLUSIVE_LOCK 为独占锁、LOCKFILE_SHARED_LOCK 为共享锁;LOCKFILE_FAIL_IMMEDIATELY 表示非阻塞,否则阻塞等待。 优势:内核在常规 HANDLE 访问上强制实施,独占锁直接拒绝其他进程/其他句柄读写该区间;共享锁拒绝任何方(含持有者自身)写入。支持锁定超出文件末尾的区间,便于预留空间。 不足:没有“建议性”可选模式,该强制语义始终生效,并发只读场景不如共享锁灵活;必须经由 OVERLAPPED 结构,较 flock 繁琐;且强制语义仅对常规 HANDLE 访问生效,内存映射视图不受约束。另有更简单的 LockFile 变体(无 OVERLAPPED、无共享锁选项),但它抢不到锁时立即返回失败、永不阻塞,需要阻塞等待须用 LockFileEx。 注意两个 Windows 特有陷阱:

  1. 同一进程若用第二个句柄再打开同一文件,必须先解锁才能访问该区间(Microsoft Learn 原文:the locking process opens the file a second time, it cannot access the specified region through this second handle until it unlocks)。这与 POSIX 语义截然不同:POSIX 下同一进程对自身永不发生阻塞。
  2. 内存映射文件(memory-mapped file)无视字节锁,锁对 MapViewOfFile 出来的视图读写完全无效,这是官方明确说明的例外。

macOS:源自 BSD 体系,flock 与 fcntl 互通

macOS 源自 BSD,两种机制都在:flock(2) 整文件建议性锁、fcntl(2) 字节范围锁,都只有建议性,没有强制锁,也没有 Linux 的 OFD 锁。

macOS 上 flock 与 fcntl 的调用方式与 Linux 一致(代码见上文 Linux 片段),此处不再重复。open(2) 另提供 BSD 专属的 O_EXLOCK / O_SHLOCK 标志,可在打开文件时原子地取得 flock 语义的独占/共享锁,配合 O_CREAT 时该锁请求不会失败(只要底层文件系统支持锁)。

关键实测:flock 与 fcntl 会互相感知

前文指出 Linux 上二者互不感知,macOS 则相反。以下以探针程序验证:进程 A 以 flock 持锁,进程 B 以 fcntl 抢占同一文件的独占锁,结果 B 直接失败。

探针以 C++ 与 Go 分别实现,逻辑一致:进程 A 以 flock 持锁并 sleep,进程 B 以 fcntl / FcntlFlock 抢占同一文件的独占锁,观察是否冲突。

C++ 探针(进程 A 与进程 B 分别运行):

// 进程 A:持锁 6 秒
int fa = open(path, O_RDWR | O_CREAT, 0644);
flock(fa, LOCK_EX | LOCK_NB);
sleep(6);

// 进程 B(另起进程):用 fcntl 抢独占锁
int fb = open(path, O_RDWR);
struct flock fl = {0}; fl.l_type = F_WRLCK;
fcntl(fb, F_SETLK, &fl);   // macOS 上返回 -1,errno = EAGAIN

Go 探针(等价逻辑):

// 进程 A
fa, _ := os.OpenFile(path, os.O_RDWR|os.O_CREATE, 0o644)
syscall.Flock(int(fa.Fd()), syscall.LOCK_EX|syscall.LOCK_NB)
time.Sleep(6 * time.Second)

// 进程 B(另起进程)
fb, _ := os.OpenFile(path, os.O_RDWR, 0o644)
fl := syscall.Flock_t{Type: syscall.F_WRLCK}
// macOS 上返回 EAGAIN
_ = syscall.FcntlFlock(uintptr(fb.Fd()), syscall.F_SETLK, &fl)

实测输出(C++ 探针,macOS):

[pid 5409] flock LOCK_EX acquired, holding 6s
[pid 5411] fcntl F_WRLCK FAILED: Resource temporarily unavailable  <-- interacts with flock?

结论:在 macOS(BSD 系)上,flock 与 fcntl 共用同一套内核锁空间,一个接口加的锁,另一接口在跨进程视角下能感知到冲突。这是文档化行为而非实现巧合:Apple 的 fcntl(2) man page 注明 “flock(2) and fcntl locks may be safely used concurrently”,其 F_GETLK 说明也明确,由 flock(2) 或 open(2) 的 O_SHLOCK / O_EXLOCK 创建的锁同样能被 fcntl 的 F_GETLK 发现(此时 l_pid 返回 -1)。Linux 将 flock 实现为独立系统调用、与 fcntl 锁互不相干,故行为相反。

特性:仅建议性,整文件或字节范围任选,机制层面与 Linux 相似,但互通性不同。 优势:flock 接口简单;fcntl 粒度细,写法与 Linux 一致,便于跨 Unix 移植。 不足:仅建议性,不阻止未持锁的进程直接访问;无 OFD 锁,多线程场景需以”每线程独立 open + fcntl”自行模拟;无强制锁。 注意:macOS 的 flock 同样关联于 open file description,fork / dup 后子进程解锁会释放父进程的锁,与 Linux 行为一致。

三平台机制对比

维度LinuxWindowsmacOS
主力接口flock / fcntl / OFDLockFile / LockFileExflock / fcntl
锁粒度整文件 / 字节字节范围整文件 / 字节
锁归属open file desc / 进程文件句柄 HANDLEopen file desc / 进程
默认性质建议性内核在常规句柄上强制实施(对他人表现为强制)建议性
flock↔fcntl互不感知无 flock互相感知
同进程第二句柄访问建议性不拦截普通读写强制生效,需先用首句柄解锁建议性不拦截普通读写
内存映射视图忽略字节锁忽略字节锁忽略字节锁
跨 forkflock 共享 / fcntl 不继承锁随句柄存续,子进程继承句柄仍被锁挡flock 共享 / fcntl 不继承
强制锁挂载选项于 v5.14 移除,5.15 起彻底移除相关支持内核在常规句柄上强制实施无

Go 跨平台封装实战

前文给出了各机制的最小片段,以下将其封装为统一的 FileLock 类型,可直接用于工程。

Go 标准库未提供跨平台文件锁,但 syscall.Flock(Unix)与 Windows 的 LockFileEx 均可直接调用。

封装以 build tag 隔离两平台实现,对外仅暴露 Lock / TryLock / Unlock,采用”哨兵锁文件”模式:锁定专用 .lock 文件的第 0 字节,抢到该字节者持锁,从而实现跨进程互斥。

// filelock.go  (跨平台公共 API)
package main

import "os"

type FileLock struct {
	path string
	f    *os.File
}

func NewFileLock(path string) *FileLock { return &FileLock{path: path} }
func (l *FileLock) Lock() error        { return l.lockExclusive(false) }
func (l *FileLock) TryLock() error     { return l.lockExclusive(true) }
func (l *FileLock) Unlock() error      { return l.unlock() }

const lockSentinelLen = 1
// filelock_unix.go  //go:build !windows
package main

import (
	"os"
	"syscall"
)

func (l *FileLock) lockExclusive(nonblock bool) error {
	f, err := os.OpenFile(l.path, os.O_RDWR|os.O_CREATE, 0o644)
	if err != nil { return err }
	l.f = f

	op := syscall.LOCK_EX
	if nonblock { op |= syscall.LOCK_NB }
	if err := syscall.Flock(int(f.Fd()), op); err != nil {
		_ = f.Close(); l.f = nil; return err
	}
	return nil
}

func (l *FileLock) unlock() error {
	if l.f == nil { return nil }

	err := syscall.Flock(int(l.f.Fd()), syscall.LOCK_UN)
	ce := l.f.Close(); l.f = nil
	if err != nil { return err }
	return ce
}
// filelock_windows.go  //go:build windows
package main

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

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

const (
	lockfileExclusiveLock   = 0x00000002
	lockfileFailImmediately = 0x00000001
)

func (l *FileLock) lockExclusive(nonblock bool) error {
	f, err := os.OpenFile(l.path, os.O_RDWR|os.O_CREATE, 0o644)
	if err != nil { return err }
	l.f = f

	var ol syscall.Overlapped
	flags := uint32(lockfileExclusiveLock)
	if nonblock { flags |= lockfileFailImmediately }
	r, _, _ := procLockFileEx.Call(
		uintptr(f.Fd()), uintptr(flags), 0,
		uintptr(lockSentinelLen), 0, uintptr(unsafe.Pointer(&ol)),
	)
	if r == 0 { _ = f.Close(); l.f = nil; return syscall.GetLastError() }
	return nil
}

func (l *FileLock) unlock() error {
	if l.f == nil { return nil }

	var ol syscall.Overlapped
	r, _, _ := procUnlockFileEx.Call(
		uintptr(l.f.Fd()), 0,
		uintptr(lockSentinelLen), 0,
		uintptr(unsafe.Pointer(&ol)),
	)
	ce := l.f.Close(); l.f = nil
	if r == 0 { return syscall.GetLastError() }
	return ce
}

Windows 分支刻意用 syscall.NewLazyDLL 直接调 kernel32.dll,不依赖 golang.org/x/sys/windows,这样在不能联网拉取模块的环境里也能 GOOS=windows go build 交叉编译通过。

运行输出(macOS,go run .):

[pid 85795] Lock() acquired
[pid 85795] TryLock() on already-held lock -> expected failure: resource temporarily unavailable
[pid 85795] Unlock() done
[pid 85795] TryLock() after release -> ok

要点:

  • build tag //go:build !windows 与 //go:build windows 将两套实现完全隔离,互不干扰编译。
  • Unix 侧以 syscall.Flock 一行实现;Windows 侧调用 LockFileEx 必须填写 OVERLAPPED(即便偏移为 0)。
  • 该封装是”建议性 + 哨兵文件”的最小可用版本,生产环境建议改用 gofrs/flock 等成熟库以减少隐患。

C++ 跨平台封装实战

前述片段仅演示单平台调用,此处以 #ifdef _WIN32 将两套实现并入同一类。该 FileLock 类对外接口统一,Unix 走 flock、Windows 走 LockFileEx。

// cross_platform_lock.cpp  (Unix 本机编译运行;Windows 侧仅在该平台编译)
#include <cstdio>
#include <string>

#ifdef _WIN32
#include <windows.h>
class FileLock {
    HANDLE h_ = INVALID_HANDLE_VALUE;
    std::string path_;
public:
    explicit FileLock(const std::string& p) : path_(p) {}

    bool lock() {
        h_ = CreateFileA(path_.c_str(), GENERIC_READ | GENERIC_WRITE, 0,
                         NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
        if (h_ == INVALID_HANDLE_VALUE) return false;
        OVERLAPPED ol = {0};
        return LockFileEx(h_, LOCKFILE_EXCLUSIVE_LOCK, 0, 1, 0, &ol) != 0;
    }

    void unlock() {
        if (h_ != INVALID_HANDLE_VALUE) {
            OVERLAPPED ol = {0};
            UnlockFileEx(h_, 0, 1, 0, &ol);
            CloseHandle(h_); h_ = INVALID_HANDLE_VALUE;
        }
    }
};

#else
#include <fcntl.h>
#include <unistd.h>
#include <sys/file.h>
class FileLock {
    int fd_ = -1;
    std::string path_;
public:
    explicit FileLock(const std::string& p) : path_(p) {}

    bool lock() {
        fd_ = open(path_.c_str(), O_RDWR | O_CREAT, 0644);
        if (fd_ < 0) return false;
        return flock(fd_, LOCK_EX) == 0;
    }

    void unlock() {
        if (fd_ >= 0) { flock(fd_, LOCK_UN); close(fd_); fd_ = -1; }
    }
};

#endif

int main() {
    FileLock lk("/tmp/xlock_demo.lock");
    if (!lk.lock()) { printf("lock failed\n"); return 1; }
    printf("cross-platform lock acquired\n");
    lk.unlock();
    printf("cross-platform lock released\n");
    return 0;
}

运行输出(macOS):

cross-platform lock acquired
cross-platform lock released

要点:#ifdef _WIN32 为编译期切换,两套代码互不参与对方平台的编译;Unix 分支采用 flock 整文件锁,足以覆盖多数”单实例 / 互斥”需求,如需字节粒度再改用 fcntl。

坑位大盘点

  1. 建议性锁仅约束协作方:在 Linux / macOS 上,未调用锁接口的进程仍可直接读写文件,内核不拦截。文件锁不可作为安全边界,它仅是协作约定。
  2. 关掉任一 fd,进程全部 fcntl 锁被释放:POSIX 语义,若库函数暗中 open/close 该文件,锁会无声丢失。
  3. flock 与 fcntl 互通性取决于平台:Linux 互不感知,macOS / BSD 互相感知。同一程序应只采用一种接口。
  4. Windows 同进程第二句柄需先解锁:用另一个 HANDLE 打开同一文件,第一把锁不释放就访问会被拒。
  5. Windows 内存映射视图无视锁:MapViewOfFile 出来的指针读写不受字节锁约束。
  6. fork 语义差异:flock 随 open file description 在父子间共享(子进程解锁会释放父进程锁);fcntl 锁不随 fork 继承。
  7. 网络 / 集群文件系统语义漂移:NFS、SMB 上锁的行为取决于客户端、服务端和挂载选项,跨机器协调别指望文件锁,上 etcd / Redis / ZooKeeper 这类分布式协调服务。
  8. 容器 overlay 文件系统可能不支持:某些 overlay / 网络存储对 flock 或 fcntl 支持不全,容器里跑单实例锁要实测。
  9. 强制锁不可靠:Linux 的 mandatory locking 实现长期存在问题;-o mand 挂载选项于 v5.14 移除,内核 5.15 起彻底不再支持相关代码(4.5 起即为可选配置、多数发行版早已关闭),不可依赖。Windows 的强制语义由内核在常规 HANDLE 访问上实施,并非 Linux 意义的 mandatory locking,且内存映射视图不受约束。
  10. 锁不保证原子写:拿到锁只意味着”别人也在走锁流程时会等你”,它不会自动把你的多次写变成原子操作。需要崩溃安全的更新,请配合 fsync + 临时文件 + rename 的原子替换模式。
  11. fork + exec 会泄漏锁:flock 与 POSIX fcntl 锁在 execve 后仍保留(跨 exec 不释放)。若持锁进程 fork 后未设 FD_CLOEXEC 就 exec 了另一个长驻程序,该程序会一直持有这把锁,导致原进程或其兄弟进程后续加锁失败。守护进程持锁时应确保锁 fd 带 O_CLOEXEC / FD_CLOEXEC。

选型指南

  • 单实例守护进程 / 防重复启动:哨兵锁文件 + flock(Unix)或 LockFileEx(Windows),简单可靠。
  • 同机多进程协作读写同一文件:需要字节粒度用 fcntl;只是互斥 flock 足够。
  • 跨平台 C++ 项目:自封装 #ifdef _WIN32(上文示例),或直接用 boost::interprocess::file_lock,少自己造轮子。
  • 跨平台 Go 项目:自封装 build tag 版本(上文示例),或社区成熟的 gofrs/flock;需要字节范围再考虑 fcntl 直调。
  • 多线程 + 文件锁:Linux 用 OFD 锁;其他平台用”每线程独立 open 拿 fd + 锁”来隔离。
  • 分布式多机:文件锁并不适用,应上真正的分布式锁服务。

总结

Linux 以 flock(整文件、独立系统调用)与 fcntl(字节、进程级)为主,默认建议性,且二者互不感知(另有 Linux 3.15+ 的 OFD 锁);Windows 以 LockFileEx 为主(另有简化版 LockFile),字节范围、句柄级,内核在常规 HANDLE 访问上强制实施,对他人表现为强制拒绝,但同进程第二句柄须先解锁、且内存映射视图无视锁;macOS 源自 BSD,两种锁并存且互相感知,仅支持建议性,无 OFD、无强制锁。

工程实践的三个建议:其一,同一程序内只使用一种锁接口,绝不混用 flock 与 fcntl;其二,始终假定锁为建议性,它仅是协作约定而非安全边界;其三,以 RAII / defer 确保解锁,且不可将文件锁等同于原子写,崩溃安全的更新须配合 fsync + rename。

遵循以上原则,即可规避绝大多数跨平台文件锁引发的数据竞争问题。