You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在macOS/Linux中用命名对象限制特定路径的应用实例数?

基于应用路径限制实例数的跨平台实现方案

针对「同路径仅允许一个实例,不同路径允许多实例」的需求,同时解决崩溃后残留资源的问题,推荐以下两种可靠方案:

方案一:文件独占锁(fcntl)

这是跨macOS/Linux最稳妥的方案,进程崩溃后系统会自动释放锁,不会留下残留资源。

实现思路

  1. 基于应用Bundle路径生成唯一的锁文件路径(比如用路径的SHA256哈希作为文件名,避免路径字符冲突和长度问题)。
  2. 打开锁文件(不存在则创建),通过fcntl设置独占写锁:
    • 锁设置成功:当前是首个实例,持有锁直到进程退出。
    • 设置失败且错误码为EAGAIN/EACCES:已有同路径实例在运行。

代码示例(C)

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <openssl/sha.h> // 需要链接libcrypto

// 生成路径的SHA256哈希作为锁文件名
void get_lock_filename(const char* bundle_path, char* out_filename, size_t out_size) {
    unsigned char hash[SHA256_DIGEST_LENGTH];
    SHA256((const unsigned char*)bundle_path, strlen(bundle_path), hash);
    
    // 转成十六进制字符串
    for (int i = 0; i < SHA256_DIGEST_LENGTH; i++) {
        snprintf(out_filename + i*2, out_size - i*2, "%02x", hash[i]);
    }
    // 拼接临时目录路径
    char tmp_path[PATH_MAX];
    snprintf(tmp_path, sizeof(tmp_path), "/tmp/app_lock_%s", out_filename);
    strncpy(out_filename, tmp_path, out_size);
    out_filename[out_size-1] = '\0';
}

bool IsAnotherInstanceRunning(const char* bundle_path) {
    char lock_file[PATH_MAX];
    get_lock_filename(bundle_path, lock_file, sizeof(lock_file));
    
    // 打开锁文件,不存在则创建,权限0644
    int fd = open(lock_file, O_CREAT | O_RDWR, 0644);
    if (fd == -1) {
        perror("open lock file failed");
        return true; // 异常情况默认视为有实例
    }
    
    struct flock lock = {0};
    lock.l_type = F_WRLCK;    // 独占写锁
    lock.l_whence = SEEK_SET; // 从文件开头
    lock.l_start = 0;
    lock.l_len = 0;           // 锁定整个文件
    
    // 设置非阻塞锁
    int ret = fcntl(fd, F_SETLK, &lock);
    if (ret == -1) {
        if (errno == EAGAIN || errno == EACCES) {
            close(fd);
            return true; // 锁已被持有,存在其他实例
        } else {
            perror("fcntl failed");
            close(fd);
            return true;
        }
    }
    
    // 保持文件描述符打开,进程退出时自动关闭释放锁
    static int g_lock_fd = -1;
    g_lock_fd = fd;
    
    return false;
}

注意事项

  • 需要链接OpenSSL库(macOS下编译加-lcrypto参数),如果不想依赖第三方库,可以替换成MD5哈希,或者沿用路径字符替换逻辑(把斜杠换成下划线后拼接临时目录)。
  • 锁文件会留在/tmp目录,但不会影响功能,下次启动时旧文件的锁已失效,不影响新实例加锁。

方案二:Unix域套接字(AF_UNIX)

利用Unix域套接字的绑定特性,崩溃后残留的套接字文件可通过连接检测清理。

实现思路

  1. 基于应用路径生成唯一的套接字路径(建议用哈希缩短路径,避免长度限制)。
  2. 创建套接字并尝试绑定:
    • 绑定成功:当前是首个实例,保持套接字监听。
    • 绑定失败且错误码为EADDRINUSE:尝试连接该套接字,连接成功说明有实例运行;连接失败则删除残留套接字后重新绑定。

代码示例(C)

#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
#include <errno.h>
#include <openssl/sha.h>

void get_socket_path(const char* bundle_path, char* out_path, size_t out_size) {
    unsigned char hash[SHA256_DIGEST_LENGTH];
    SHA256((const unsigned char*)bundle_path, strlen(bundle_path), hash);
    
    char hash_str[SHA256_DIGEST_LENGTH*2 + 1];
    for (int i = 0; i < SHA256_DIGEST_LENGTH; i++) {
        snprintf(hash_str + i*2, sizeof(hash_str) - i*2, "%02x", hash[i]);
    }
    
    snprintf(out_path, out_size, "/tmp/app_sock_%s", hash_str);
    out_path[out_size-1] = '\0';
}

bool IsAnotherInstanceRunning(const char* bundle_path) {
    char sock_path[PATH_MAX];
    get_socket_path(bundle_path, sock_path, sizeof(sock_path));
    
    int sock_fd = socket(AF_UNIX, SOCK_STREAM, 0);
    if (sock_fd == -1) {
        perror("socket create failed");
        return true;
    }
    
    struct sockaddr_un addr = {0};
    addr.sun_family = AF_UNIX;
    strncpy(addr.sun_path, sock_path, sizeof(addr.sun_path)-1);
    
    // 尝试绑定套接字
    int ret = bind(sock_fd, (struct sockaddr*)&addr, sizeof(addr));
    if (ret == 0) {
        // 绑定成功,开始监听(仅占位,无需处理连接)
        listen(sock_fd, 1);
        static int g_sock_fd = -1;
        g_sock_fd = sock_fd;
        return false;
    } else if (errno == EADDRINUSE) {
        // 套接字已存在,尝试连接检测是否有进程使用
        int test_fd = socket(AF_UNIX, SOCK_STREAM, 0);
        ret = connect(test_fd, (struct sockaddr*)&addr, sizeof(addr));
        close(test_fd);
        
        if (ret == 0) {
            // 连接成功,存在运行的实例
            close(sock_fd);
            return true;
        } else {
            // 连接失败,说明套接字是残留的,删除后重新绑定
            unlink(sock_path);
            ret = bind(sock_fd, (struct sockaddr*)&addr, sizeof(addr));
            if (ret == 0) {
                listen(sock_fd, 1);
                static int g_sock_fd = -1;
                g_sock_fd = sock_fd;
                return false;
            } else {
                perror("rebind failed");
                close(sock_fd);
                return true;
            }
        }
    } else {
        perror("bind failed");
        close(sock_fd);
        return true;
    }
}

注意事项

  • macOS下Unix域套接字路径长度有限制(sun_path最多104字符),必须用哈希缩短路径。
  • 进程正常退出时可调用unlink(sock_path)清理套接字文件,崩溃时系统不会自动清理,需通过连接检测处理残留。

为什么不推荐命名信号量

命名信号量是系统级持久化资源,进程崩溃后如果未调用sem_unlink,信号量会一直留存,导致后续同路径应用无法启动,必须手动清理(如ipcrm命令),完全不符合需求的可靠性要求。

内容的提问来源于stack exchange,提问作者c00000fd

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.09 04:40:23