• 进程互斥与竞态


    缘起

    在linux编程中,经常有这样的要求:特定进程(尤其是daemon进程)有且只有一个,即特定资源只能由一进程拥有。问题是:如何保证特定进程间的“互斥”关系(只有一个实例)?当检测到“互斥(锁定)”时,其余进程可直接退出,而无需同步。

    互斥与同步

    互斥:是指某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。但互斥无法限制访问者对资源的访问顺序,即访问是无序的。

    同步:是指在互斥的基础上(大多数情况),通过其它机制实现访问者对资源的有序访问。在大多数情况下,同步已经实现了互斥,特别是所有写入资源的情况必定是互斥的。少数情况是指可以允许多个访问者同时访问资源。

    (以上摘自百度知道)

    Linux提供的同步机制:信号量、文件锁(文件记录锁和文件锁)、互斥量、条件变量。其中后两者需要依赖于共享内存才能用于进程间同步,因此只有文件锁是进程生存期的资源,其他的都属于内核生存期资源。除此之外,信号也可用于进程同步。

    网络端口

    如果进程需要监听特定的端口(如60000),那么在进程起来之后,可直接尝试连接该特定端口,只要能够连上,即可说明该端口已被使用,进程退出。由于listen/connect均是原子操作,故该判断过程不存在竞态。这种方法极其简单且可靠。

    既然端口可用于判断,自然会想利用unix socket来作为替代技术(unix socket远大于65535)。但是由于unix socket将在文件系统上创建一个文件,该文件必须被显式删除,后续的bind方能正常工作,故该方法存在缺陷:没有可靠的办法保证文件必定被删除。(后面分析)

    文件锁

    另一种很常见的方法是:在特定的路径(路径可为配置参数)下创建一个“众所皆知”的文件,并利用独占锁/写锁保证在进程生存期内有且只有一个进程拥有该文件锁。文件锁属于进程生存期资源,不管进程是否正常终止,进程终止后,文件锁一定被释放。

    作为一个加强,可将拥有文件锁的进程PID写入文件,从而在删除锁文件时更“可靠”。问题是:若考虑删除文件,该方案将存在缺陷:删除文件和创建文件是两个系统调用,存在“竞态”。后面将讨论文件删除问题。

    信号量和进程锁(共享内存)

    信号量和进程锁都属于内核生存期资源。若进程异常终止,信号量和进程锁可能处于“不确定状态”,加上进程无法“得知”是否有其他进程使用相同的信号量或进程锁,导致后续进程不能正常工作。不推荐。

    系统调用与竞态

    linux系统编程中,经常会出现“竞态(race condition)”,即多进程的资源获取冲突或者访问时序问题。Linux提供的绝大多数系统调用函数保证函数调用过程是原子的(并非所有的系统调用均是原子的,见附录),即单函数调用在返回或终止之前,该函数的操作是原子的,不受其他系统调用影响。但很多系统调用往往需要配合使用,由多个系统调用组成的调用组合,操作系统是无法保证原子性的!这意味着:2个以上系统调用组合在多进程环境下将出现“竞态”。如何避免竞态是linux系统编程的一个大问题。

    文件操作的竞态分析

    凡涉及多于2个的系统调用,必存在竞态:

    示例1:lseek+read

    off_t orig;

    orig = lseek(fd, 0, SEEK_CUR);    /* Save current offset */

    lseek(fd, offset, SEEK_SET);

    s = read(fd, buf, len);

    lseek(fd, orig, SEEK_SET);        /* Restore original file offset */

    示例2:access+create

    if(access(file, F_OK) !=0){

           int fd = open((char*)arg, O_RDWR|O_CREAT, 0644);

    }

    示例3:删除nfs文件系统的文件夹

    Cloes(fd);

    Remove_Dir(path);

    注:fd指向的文件已经被删除,在fd被close之前,该文件将被重命名为.nfs***的临时文件。

    示例4:unit socket (TLPI 57-3)

        struct sockaddr_un addr;
        int sfd, cfd;
        ssize_t numRead;
        char buf[BUF_SIZE];
        sfd = socket(AF_UNIX, SOCK_STREAM, 0);
        if (sfd == -1)
            errExit("socket");
        /* Construct server socket address, bind socket to it,
           and make this a listening socket */
        if (remove(SV_SOCK_PATH) == -1 && errno != ENOENT)
            errExit("remove-%s", SV_SOCK_PATH);
        memset(&addr, 0, sizeof(struct sockaddr_un));
        addr.sun_family = AF_UNIX;
        strncpy(addr.sun_path, SV_SOCK_PATH, sizeof(addr.sun_path) - 1);
        if (bind(sfd, (struct sockaddr *) &addr, sizeof(struct sockaddr_un)) == -1)
            errExit("bind");
        if (listen(sfd, BACKLOG) == -1)
            errExit("listen");
    

    注:该程序在remove和bind之间存在竞态,即有可能另一程序删除该被刚创建的unix socket文件。对于其他的系统资源,如POSIX信号量,POSIX消息队列,POSIX共享内存,其本质也是文件(通常位于/dev/shm/),且这些文件和普通文件一样可“加锁”!

    文件锁示例

    文件锁机制是一个可靠的进程间同步机制(信号量等机制存在缺陷)。使用该机制并不要求删除“锁文件”,不当的文件删除反而会引入潜在问题。

    “锁文件”删除场景分析:

    1)      创建后立马删除(create + unlink)

    这种做法将导致其他进程“看不到”锁文件,从而创建另一个新文件。

    2)      删除文件时未加锁

    文件锁和文件记录锁若使用不当,锁会因其他操作而释放,从而导致删除文件时,删除进程并未锁定该文件。若此场景出现,则意味着锁文件的“创建+删除”并非原子操作,从而出现竞态。

    3)      程序异常终止

    删除文件这个美好的愿望可能因程序异常终止而无法实现。

    4)      “创建+删除”原子操作且正常执行

    只有在这样的条件下,方能保证完美删除锁文件。(但谁能保证程序永远正确呢?)

    总之,使用锁文件同步进程无需也不应该去删除锁文件。下面的例子是来自TLPI(The Linux Programming Interface) 55-4:

    int createPidFile(const char *progName, const char *pidFile, int flags)
    {
        int fd;
        char buf[BUF_SIZE];
        fd = open(pidFile, O_RDWR | O_CREAT, S_IRUSR | S_IWUSR);
        if (fd == -1)
            errExit("Could not open PID file %s", pidFile);
        if (flags & CPF_CLOEXEC) {
            /* Set the close-on-exec file descriptor flag */
            flags = fcntl(fd, F_GETFD);                     /* Fetch flags */
            if (flags == -1)
                errExit("Could not get flags for PID file %s", pidFile);
            flags |= FD_CLOEXEC;                            /* Turn on FD_CLOEXEC */
            if (fcntl(fd, F_SETFD, flags) == -1)            /* Update flags */
                errExit("Could not set flags for PID file %s", pidFile);
        }
        if (lockRegion(fd, F_WRLCK, SEEK_SET, 0, 0) == -1) {
            if (errno  == EAGAIN || errno == EACCES)
                fatal("PID file '%s' is locked; probably "
                         "'%s' is already running", pidFile, progName);
            else
                errExit("Unable to lock PID file '%s'", pidFile);
        }
        if (ftruncate(fd, 0) == -1)
            errExit("Could not truncate PID file '%s'", pidFile);
        snprintf(buf, BUF_SIZE, "%ld\n", (long) getpid());
        if (write(fd, buf, strlen(buf)) != strlen(buf))
            fatal("Writing to PID file '%s'", pidFile);
        return fd;
    }
    

    几点说明:

    1)      O_CREAT的open方式将保证锁文件被创建或正确打开,即使多个进程同时执行也没有问题。Open是原子的,有且只有一个文件被创建。

    2)      lockRegion采用的是文件记录锁,也可以换成文件锁(flock)。只有fcntl才能用于NFS。

    3)      将进程PID写入锁文件有助于其他程序判断该锁文件是否有效(和文件是否锁定无关),对安全删除锁文件有帮助,比如垃圾清理进程。

    另一种实现:

        int fd = open(lockfile.c_str(), O_RDWR|O_CREAT|O_EXCL, 0644);
        if(fd < 0){
            if(errno == EEXIST){
                fd = open(lockfile.c_str(), O_RDWR);
            }
        }
        if(fd < 0){
            char buf[512] = {0};
            strerror_r(errno, buf, 512);
            exit(-1);
        }
    
        if(writelock(fd) < 0){  // only one process will get the lock.
            char buf[512] = {0};
            strerror_r(errno, buf, 512);
            exit(-1);
        }
    

    几点说明:

    1)      O_CREAT|O_EXCL将保证有且只有一个进程能够创建锁文件。

    2)      通过文件锁保证有且只有一个进程获得文件锁。

    3)      第一种实现更为简单且优雅。

    附录

    不保证原子性的系统调用:

    1) write() -- write N bytes to PIPE,if N > PIPE_BUF, then write is not atomic!

    2) flock() -- lock convert is not guarantee to be atomic. fcntl() guarantee all operators are atomic.

    参考文献

    The Linux Programming Interface

    相关资源

    文件锁与NFS文件锁

    RAII、栈展开和程序终止

    RAII and system resource cleanup

  • 相关阅读:
    MySQL 使用Anemometer基于pt-query-digest将慢查询可视化
    MySQL explain 中key_len的计算
    MySQL explain 详解
    Linux 误删libc.so.6
    Linux crontab 的格式及定义
    Azure容器监控部署(上)
    容器监控部署 -- 整体架构
    Prometheus 介绍
    zabbix自动停用与开启agent
    在jenkins中处理外部命令7z的异常
  • 原文地址:https://www.cnblogs.com/zhenjing/p/process_mutex_race.html
Copyright © 2020-2023  润新知