进程互斥与竞态

标签: 进程 互斥 | 发表时间:2011-07-15 09:33 | 作者:zhenjing MadFrog
出处:http://www.cnblogs.com/

缘起

在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

作者: zhenjing 发表于 2011-07-15 09:33 原文链接

评论: 0 查看评论 发表评论


最新新闻:
· 34%的iPhone 4用户认为4代表4G(2011-07-15 13:37)
· 微软秘密开发社交项目Tulalip 意外泄漏主页面(2011-07-15 13:20)
· 小米公司办公环境探营 小米手机真机曝光(2011-07-15 12:08)
· 微软独角戏:四大智能手机HTML5性能大比拼(2011-07-15 12:07)
· Hotmail新功能“我的朋友被黑了”(2011-07-15 12:06)

编辑推荐:为什么为什么为什么为什么为什么你要做一名程序员?

网站导航:博客园首页  我的园子  新闻  闪存  小组  博问  知识库

相关 [进程 互斥] 推荐:

进程互斥与竞态

- MadFrog - 博客园-首页原创精华区
在linux编程中,经常有这样的要求:特定进程(尤其是daemon进程)有且只有一个,即特定资源只能由一进程拥有. 问题是:如何保证特定进程间的“互斥”关系(只有一个实例). 当检测到“互斥(锁定)”时,其余进程可直接退出,而无需同步. 互斥:是指某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性.

读写锁与互斥锁

- Xin - C++博客-首页原创精华区
长寿梦 2011-08-25 21:55 发表评论.

Redis实现lock互斥访问资源

- - 数据库 - ITeye博客
Redis是当前很流行的一种开源键值数据库. 目前睿思的后台架构在数据库层采用了Redis和MySQL组合的形式,其中Redis主要用来存储状态信息(比如当前种子的peer)和读写频繁的数据. Redis完全运行在内存之上,无lock设计,速度非常快. 通过实测,在睿思服务器上读写速度达到3万次/s.

监控进程

- - 火丁笔记
有时候,进程突然终止服务,可能是没有资源了,也可能是意外,比如说:因为 OOM 被杀;或者由于 BUG 导致崩溃;亦或者误操作等等,此时,我们需要重新启动进程. 实际上,Linux 本身的初始化系统能实现简单的功能,无论是老牌的 SysVinit,还是新潮的  Upstart 或者  Systemd 均可,但它们并不适合处理一些复杂的情况,比如说:CPU 占用超过多少就重启;或者同时管理 100 个 PHP 实现的 Worker 进程等等,如果你有类似的需求,那么可以考虑试试 Monit 和 Supervisor,相信会有不一样的感受.

Linux进程关系

- - 博客园_首页
作者:Vamei 出处:http://www.cnblogs.com/vamei 欢迎转载,也请保留这段声明. Linux的进程相互之间有一定的关系. 比如说,在 Linux进程基础中,我们看到,每个进程都有父进程,而所有的进程以init进程为根,形成一个树状结构. 我们在这里讲解进程组和会话,以便以更加丰富的方式了管理进程.

进程监控脚本

- - CSDN博客架构设计推荐文章
# 如果不存在, 就重启他. 作者:ahyswang 发表于2014-10-11 22:34:45 原文链接. 阅读:111 评论:0 查看评论.

Android 进程间通信

- - SegmentFault 最新的文章
单例居然失效了,一个地方设置值,另个地方居然取不到,这怎么可能. 排查半天,发现这两就不在一个进程里,才恍然大悟……. 按照操作系统中的描述:进程一般指一个执行单元,在 PC 和移动设备上指一个程序或者一个应用. 我们都知道,系统为 APP 每个进程分配的内存是有限的,如果想获取更多内存分配,可以使用多进程,将一些看不见的服务、比较独立而又相当占用内存的功能运行在另外一个进程当中.

进程隐藏与进程保护(SSDT Hook 实现)(一)

- Bloger - 博客园-首页原创精华区
应用层调用 Win32 API 的完整执行流程:. 前面一篇博文呢介绍了代码的注入技术(远程线程实现),博文地址如下:. 虽然代码注入是很老的技术了,但是这种技术也还是比较常见,. 当然也比较好用的,比如在 Spy++ 中就使用了远程线程注入技术,. 同时,如果有兴趣的阅读过 Spy++ 的源码的朋友,当然也可以在其源码中阅读到关于远程线程注入技术了.

进程隐藏与进程保护(SSDT Hook 实现)(二)

- Bloger - 博客园-首页原创精华区
引子 – Demo 实现效果:. SSDT Hook 框架搭建:. 隐藏进程列表和保护进程列表的维护:. 引子 – Demo 实现效果:. 上一篇《进程隐藏与进程保护(SSDT Hook 实现)(一)》呢把 SSDT 说得差不多了,. 不过呢,那也只是些理论的东西,看不到什么实物,估计说来说去把人说晕了后,也没什么感觉,.

Linux中如何杀掉僵尸进程

- Fornote - C++博客-首页原创精华区
  1) 检查当前僵尸进程信息.   执行上面获得的语句即可, 使用信号量9, 僵尸进程数会大大减少..   3) 过一会儿检查当前僵尸进程信息.   发现僵尸进程数减少了一些, 但还有不少啊..   4) 再次获得杀僵尸进程语句.   执行上面获得的语句即可, 这次使用信号量18杀其父进程, 僵尸进程应该会全部消失..