Linux文件锁解决多进程并发

1、多进程开发的三大问题

首先小哥介绍一下遇到的三个问题:

1. 日志系统的行交错问题

当多个进程同时向同一日志文件写入数据时,内核的I/O缓冲区可能导致日志行交错。例如:

复制代码
进程A写入:2025-05-01 10:00:01 [A] Start task...
进程B写入:2025-05-01 10:00:02 [B] Error: file not found
进程A写入:2025-05-01 10:00:03 [A] Task completed.

实际日志可能变为:

复制代码
2025-05-01 10:00:01 [A] Start task...2023-10-01 10:00:02 [B] Error: file not found
2025-05-01 10:00:03 [A] Task completed.

格式错乱这样会使得日志可读性极差,无法用于问题排查。

2. 配置文件更新导致损坏

若两个进程同时修改配置文件(如 config.json):

• 进程A写入前半部分内容后,进程B抢占了写入权。

• 进程B覆盖了进程A的部分内容,导致文件内容不完整。

配置文件数据错乱,严重点会导致服务崩溃。

3. 单例进程重复启动

守护进程(如 daemon)通常要求仅允许一个实例运行。若未加锁,当用户误操作重复启动进程,导致资源竞争(如端口占用)。最终导致服务不可用。

2、文件锁来了

文件锁主要是通过内核机制协调多进程对文件的并发访问,解决以下问题:

• 互斥访问:同一时间仅允许一个进程修改文件。

• 数据一致性:确保文件内容的完整性和操作原子性。

• 进程同步:避免资源竞争导致的不可预测行为。

然而linux系统中通常有两类文件锁:

  1. 建议性锁(Advisory Lock):依赖进程主动遵守锁规则(常用)。

  2. 强制性锁(Mandatory Lock):由内核强制限制读写(需特殊配置)。

强制锁相对效率不高,通常用建议锁。主要是使用如下API

flock() :对整个文件加锁(基于文件描述符)。

fcntl():支持对文件的某个区域加锁(更细粒度)。

lockf():基于 fcntl() 的封装,简化文件锁操作。

第一个例子:多进程安全写入日志文件

目标:确保多个进程按顺序写入日志,避免行交错。

代码实现:

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <time.h>

void write_log(const char *message) {
    int fd = open("app.log", O_WRONLY | O_CREAT | O_APPEND, 0644);
    if (fd == -1) {
        perror("open log failed");
        exit(1);
    }

    // 加排他锁(阻塞直到获得锁)
    struct flock lock = {
        .l_type = F_WRLCK,
        .l_whence = SEEK_SET,
        .l_start = 0,
        .l_len = 0// 锁住整个文件
    };
    fcntl(fd, F_SETLKW, &lock);

    // 写入日志(带时间戳)
    time_t now = time(NULL);
    char buffer[256];
    snprintf(buffer, sizeof(buffer), "[%ld] %s\n", now, message);
    write(fd, buffer, strlen(buffer));

    // 释放锁
    lock.l_type = F_UNLCK;
    fcntl(fd, F_SETLK, &lock);

    close(fd);
}

int main() {
    // 模拟多进程写入(编译后启动多个进程测试)
    write_log("Process started");
    sleep(1);  // 模拟业务逻辑耗时
    write_log("Process finished");
    return0;
}

每次写日志前通过 F_WRLCK 获取排他锁,确保原子性, F_SETLKW 会阻塞进程直到锁被释放,其实看起来与线程同步中的读写锁类似。

第二个例子:单例进程锁文件实现

目标:通过锁文件 /var/run/myapp.pid 防止重复启动。

代码实现:

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <errno.h>

void enforce_singleton() {
    constchar *lockfile = "/var/run/myapp.pid";
    int fd = open(lockfile, O_WRONLY | O_CREAT, 0644);
    if (fd == -1) {
        perror("open lockfile failed");
        exit(1);
    }

    // 尝试加建议性锁(非阻塞)
    struct flock lock = {
        .l_type = F_WRLCK,
        .l_whence = SEEK_SET,
        .l_start = 0,
        .l_len = 0
    };
    if (fcntl(fd, F_SETLK, &lock) == -1) {
        if (errno == EAGAIN || errno == EACCES) {
            fprintf(stderr, "Error: Another instance is already running.\n");
            exit(1);
        } else {
            perror("fcntl failed");
            exit(1);
        }
    }

    // 写入当前进程ID
    char pid_str[16];
    snprintf(pid_str, sizeof(pid_str), "%d\n", getpid());
    ftruncate(fd, 0);  // 清空文件内容
    write(fd, pid_str, strlen(pid_str));
}

int main() {
    enforce_singleton();
    printf("Service started. PID: %d\n", getpid());
    while (1) {
        sleep(60);  // 模拟守护进程逻辑
    }
    return0;
}

通过启动时尝试对锁文件加排他锁,若失败则退出,锁文件内容为进程PID,便于问题排查,与设计模式中的单例模式类似。

你还可以通过lslocks命令来查看系统中当前的文件锁,值得注意的是锁的粒度(全局锁 vs 区域锁)和死锁风险。

相关推荐
Graceful_scenery7 小时前
ROS2核心概念之服务
运维·服务器
南棱笑笑生7 小时前
20251213给飞凌OK3588-C开发板适配Rockchip原厂的Buildroot【linux-6.1】系统时适配type-C0
linux·c语言·开发语言·rockchip
长安er7 小时前
LeetCode 83/237/82 链表删除问题-盒子模型
数据结构·算法·leetcode·链表·力扣
小虎牙0077 小时前
RSA 的核心原理
算法
重生之后端学习7 小时前
56. 合并区间
java·数据结构·后端·算法·leetcode·职场和发展
Miqiuha7 小时前
回流用户判定
服务器
历程里程碑8 小时前
C++ 5:模板初阶
c语言·开发语言·数据结构·c++·算法
leoufung8 小时前
LeetCode 74. Search a 2D Matrix
数据结构·算法·leetcode
Kiri霧8 小时前
Go数据类型介绍
java·算法·golang