进程互斥与竞态

缘起

在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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值