进程异常退出导致死锁的解决办法

       最近碰到这么一个问题:程序先获得锁,然后进行一些操作,操作完成之后再把锁释放掉,然而在获得锁之后进行的一些操作中可能导致程序异常退出(比如段错误),可以看出还没有来得及把锁释放进程就蹦掉了,从而导致这个锁长期没有被释放,其他想尝试获取锁的进程都会失败。

       这个问题在多进程模型中很容易出现,下面是一个比较简单的多进程模型程序例子:

dead_lock.c:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/wait.h>
#include "mylock.h"

#define MAX_PROCESS     32
#define PROC_NUMBER     5

typedef struct process_map {
    int     index;
    pid_t   pid;
} process_map_t;

pid_t           pid;
int             proc_index;
process_map_t   process_pids[MAX_PROCESS];

int             is_restarting = 0;

char *test_data[PROC_NUMBER];

void work_loop()
{
    char c;

    while (1) {
        //do something
        printf("proc_index: %d, pid: %d, mylock\n", proc_index, pid);
        mylock();

        //获取第一个字符。这里只是模拟,真正的工作要比这个复杂多了
        c = *test_data[proc_index];
        printf("proc_index: %d, pid: %d, c: %c\n", proc_index, pid, c);

        myunlock();
        printf("proc_index: %d, pid: %d, myunlock\n", proc_index, pid);

        sleep(20);
    }
}

int main(int argc, char *argv[])
{
    int proc, i, ret, status;

    memset(process_pids, 0, sizeof(process_pids));

    if (mylock_init() < 0) {
        exit(1);
    }

    for (proc = 0; proc < PROC_NUMBER; proc++) {
        if (proc == 2) {
            //让第3个子进程访问NULL指针而出现段错误
            test_data[proc] = NULL;
        } else {
            test_data[proc] = "xxxxx";
        }
    }

    for (proc = 0; proc < PROC_NUMBER; proc++) {
        ret = fork();
        if (ret == 0) {
            //child process
            break;
        } else if (ret < 0) {
            exit(1);
        }

        process_pids[proc].index = proc;
        process_pids[proc].pid = ret;
    }

    proc_index = proc;

    if (proc == PROC_NUMBER) {
        //parent process
        while (1) {
            for (i = 0; i < PROC_NUMBER; i++) {
                if ((ret = waitpid(process_pids[i].pid, &status, WNOHANG)) == 0) {
                    continue;
                }

                ret = fork();
                if (ret == 0) {
                    proc_index = process_pids[i].index;
                    is_restarting = 1;
                    printf("proc_index: %d, pid: %d, is_restarting\n", proc_index, getpid());
                    goto CHILD_START;
                } else if (ret < 0) {
                    exit(1);
                }

                process_pids[i].pid = ret;
            }

            sleep(5);
        }
    }

CHILD_START:

    pid = getpid();

    work_loop();

    exit(0);
}

mylock.h:

#ifndef __MYLOCK_H__
#define __MYLOCK_H__

int mylock_init(void);
void mylock(void);
void myunlock(void);

#endif

mylock.c:

#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/mman.h>
#include "mylock.h"

static pthread_mutex_t *mptr;

int mylock_init(void)
{
    pthread_mutexattr_t mattr;

    mptr = mmap(0, sizeof(pthread_mutex_t), PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANON, -1, 0);
    if (mptr == MAP_FAILED) {
        return -1;
    }

    pthread_mutexattr_init(&mattr);
    pthread_mutexattr_setpshared(&mattr, PTHREAD_PROCESS_SHARED);
    pthread_mutex_init(mptr, &mattr);

    return 1;
}

void mylock(void)
{
    pthread_mutex_lock(mptr);
}

void myunlock(void)
{
    pthread_mutex_unlock(mptr);
}

以上代码中,main函数主要是初始化test_data数组,故意将下标为2的指针设成NULL,让访问它的进程产生段错误,之后fork出5个子进程,并把进程号和进程索引放到process_ids数组中,然后父进程每隔5秒用waitpid检测子进程有没有退出,退出则重新fork一个子进程来继续工作,而子进程就直接进入work_loop函数做自己的工作,work_loop函数里面处理test_data数组时用了锁(其实test_data不是共享内存数据可以不用锁的,这里只是模拟一下就当test_data是共享数据吧),mylock.c文件里用pthread_mutex_lock和pthread_mutex_unlock来实现一个多进程间的锁,也可以用gcc的原子操作来实现。下面是编译运行结果:


可以看出第2号进程pid为31530已经重启了,由于退出时没有释放掉锁,所以0-4号进程都没办法再次获得锁了,这也是意料之中。

本文就是来解决这个问题的,我想到的解决办法有以下这么几个:

1、进程重启后释放掉重启之前拿到的锁

       因为没有哪个进程去释放锁从而这个锁就被长期锁住了,解决办法之一就是死了的进程在重启之后要自己去把重启之前拿到的锁释放掉,代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/wait.h>
#include "mylock.h"

#define MAX_PROCESS     32
#define PROC_NUMBER     5

typedef struct process_map {
    int     index;
    pid_t   pid;
} process_map_t;

pid_t           pid;
int             proc_index;
process_map_t   process_pids[MAX_PROCESS];

int             is_restarting = 0;

int             *hold_lock_proc = NULL;

char *test_data[PROC_NUMBER];

void work_loop()
{
    char c;

    //如果进程重启,而且是自己进程拿到锁的话就先把锁释放掉
    if (is_restarting == 1 && *hold_lock_proc == proc_index) {
        myunlock();
    }

    is_restarting = 0;

    while (1) {
        //do something
        printf("proc_index: %d, pid: %d, mylock\n", proc_index, pid);
        mylock();

        //保存拿到锁的进程索引
        *hold_lock_proc = proc_index;

        //获取第一个字符
        c = *test_data[proc_index];
        printf("proc_index: %d, pid: %d, c: %c\n", proc_index, pid, c);

        myunlock();
        printf("proc_index: %d, pid: %d, myunlock\n", proc_index, pid);

        sleep(20);
    }
}

int main(int argc, char *argv[])
{
    int proc, i, ret, status;

    memset(process_pids, 0, sizeof(process_pids));

    hold_lock_proc = mmap(0, sizeof(int), PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANON, -1, 0);
    if (hold_lock_proc == MAP_FAILED) {
        exit(1);
    }

    if (mylock_init() < 0) {
        exit(1);
    }

    for (proc = 0; proc < PROC_NUMBER; proc++) {
        if (proc == 2) {
            //让第3个子进程访问NULL指针而出现段错误
            test_data[proc] = NULL;
        } else {
            test_data[proc] = "xxxxx";
        }
    }

    for (proc = 0; proc < PROC_NUMBER; proc++) {
        ret = fork();
        if (ret == 0) {
            //child process
            break;
        } else if (ret < 0) {
            exit(1);
        }

        process_pids[proc].index = proc;
        process_pids[proc].pid = ret;
    }

    proc_index = proc;

    if (proc == PROC_NUMBER) {
        //parent process
        while (1) {
            for (i = 0; i < PROC_NUMBER; i++) {
                if ((ret = waitpid(process_pids[i].pid, &status, WNOHANG)) == 0) {
                    continue;
                }

                ret = fork();
                if (ret == 0) {
                    proc_index = process_pids[i].index;
                    is_restarting = 1;
                    printf("proc_index: %d, pid: %d, is_restarting\n", proc_index, getpid());
                    goto CHILD_START;
                } else if (ret < 0) {
                    exit(1);
                }

                process_pids[i].pid = ret;
            }

            sleep(5);
        }
    }

CHILD_START:

    pid = getpid();

    work_loop();

    exit(0);
}
       每个进程拿到锁之后要用一个变量来保存拿到锁的进程索引号,以便进程重启之后比较是不是自己之前拿到锁没释放就退出了,因为是多个子进程共享的变量,所以main函数开始时得先mmap出一个4字节的int类型共享内存来保存这个进程索引号。在work_loop函数中判断如果是进程重启过而且是自己拿过锁没释放,那就先把锁释放掉。下面是编译运行结果:


可以看出第2号进程重启之后其他进程也能获得锁了,只是第2号进程一直重复产生段错误而重启,这也是意料之中的。

       这种方式在一定程度上解决了锁长期被占从而导致类似死锁的问题,但是在进程退出到重启之后再释放锁期间时间可能比较长,这样其他进程获取锁的时间就有点长了,但总比一直等锁强吧。还有一个缺点是如果程序中用到的锁比较多或者有嵌套锁的话,就比较难管理了,需要用一个列表来保存该进程拿到的锁,而且这个列表也要放到共享内存中,否则进程重启之后无法访问列表也白搭。

2、超时强制释放

       这个方法比较暴力,有可能误伤,要做好评估,评估锁最长会占用多长时间,超过这个时间就可以强制释放锁了,这样锁就得重新实现而不能用上面那种pthread_mutex_lock和pthread_mutex_unlock了,需要自己用共享内存以及gcc的原子操作来实现,以及加入超时机制,这个可以参考nginx的ngx_shmtx.c里面的实现,这里就不展开了。

3、注册SIGSEGV信号处理函数,发现段错误时先把锁释放了再退出

       这个方法比第一种方法稍微好一点,只是锁释放的时机提前了一点而已。看代码吧:

dead_lock_free.c  :

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/wait.h>
#include "mylock.h"

#define MAX_PROCESS     32
#define PROC_NUMBER     5

typedef struct process_map {
    int     index;
    pid_t   pid;
} process_map_t;

pid_t           pid;
int             proc_index;
process_map_t   process_pids[MAX_PROCESS];

int             is_restarting = 0;

int             *hold_lock_proc = NULL;

char *test_data[PROC_NUMBER];

void segment_fault_handler(int sig)
{
    printf("segment_fault_handler\n");

    //如果是自己获取了锁,则先把锁释放了再退出
    if (proc_index == *hold_lock_proc) {
        printf("proc_index: %d, pid: %d, myunlock before exit\n", proc_index, pid);
        myunlock();
    }

    //恢复SIGSEGV信号
    signal(SIGSEGV, SIG_DFL);
}

void work_loop()
{
    char c;

    while (1) {
        //do something
        printf("proc_index: %d, pid: %d, mylock\n", proc_index, pid);
        mylock();

        //保存拿到锁的进程索引
        *hold_lock_proc = proc_index;

        //获取第一个字符
        c = *test_data[proc_index];
        printf("proc_index: %d, pid: %d, c: %c\n", proc_index, pid, c);

        myunlock();
        printf("proc_index: %d, pid: %d, myunlock\n", proc_index, pid);

        sleep(20);
    }
}

int main(int argc, char *argv[])
{
    int proc, i, ret, status;

    memset(process_pids, 0, sizeof(process_pids));

    //申请一个匿名共享内存来保存获得锁的进程索引
    hold_lock_proc = mmap(0, sizeof(int), PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANON, -1, 0);
    if (hold_lock_proc == MAP_FAILED) {
        exit(1);
    }

    //注册SIGSEGV信号
    if (signal(SIGSEGV, segment_fault_handler) == SIG_ERR) {
        perror("signal error: ");
    }

    if (mylock_init() < 0) {
        exit(1);
    }

    for (proc = 0; proc < PROC_NUMBER; proc++) {
        if (proc == 2) {
            //让第3个子进程访问NULL指针而出现段错误
            test_data[proc] = NULL;
        } else {
            test_data[proc] = "xxxxx";
        }
    }

    for (proc = 0; proc < PROC_NUMBER; proc++) {
        ret = fork();
        if (ret == 0) {
            //child process
            break;
        } else if (ret < 0) {
            exit(1);
        }

        process_pids[proc].index = proc;
        process_pids[proc].pid = ret;
    }

    proc_index = proc;

    if (proc == PROC_NUMBER) {
        //parent process
        while (1) {
            for (i = 0; i < PROC_NUMBER; i++) {
                if ((ret = waitpid(process_pids[i].pid, &status, WNOHANG)) == 0) {
                    continue;
                }

                ret = fork();
                if (ret == 0) {
                    proc_index = process_pids[i].index;
                    is_restarting = 1;
                    printf("proc_index: %d, pid: %d, is_restarting\n", proc_index, getpid());
                    goto CHILD_START;
                } else if (ret < 0) {
                    exit(1);
                }

                process_pids[i].pid = ret;
            }

            sleep(5);
        }
    }

CHILD_START:

    pid = getpid();

    work_loop();

    exit(0);
}
代码里面也加注释了,就不细说了。下面是编译运行结果:


可见各个进程都有机会拿到锁,没有发生死锁的情况。

       还有一个终极解决办法是:不要让程序产生段错误,尽量把锁的粒度放到最小,一是减少锁的开销,二是减少发生本文开头所说问题的概率。不要让程序产生段错误这个就得看个人内功和团队的整个能力了,另外也要多做code review。

       如有更好的解决办法,欢迎指正~


  • 4
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
(1)创建生产者和消费者线程 在Windows2000环境下,创建一个控制台进程,在此进程中创建n个线程来模拟生产者或者消费者。这些线程的信息由本程序定义的“测试用例文件”中予以指定。 该文件的格式和含义如下: 3 1 P 3 2 P 4 3 C 4 1 4 P 2 5 C 3 1 2 4 第一行说明程序中设置几个临界区,其余每行分别描述了一个生产者或者消费者线程的信息。每一行的各字段间用Tab键隔开。不管是消费者还是生产者,都有一个对应的线程号,即每一行开始字段那个整数。第二个字段用字母P或者C区分是生产者还是消费者。第三个字段表示在进入相应线程后,在进行生产和消费动作前的休眠时间,以秒计时;这样做的目的是可以通过调整这一列参数,控制开始进行生产和消费动作的时间。如果是代表生产者,则该行只有三个字段。如果代表消费者,则该行后边还有若干字段,代表要求消费的产品所对应的生产者的线程号。所以务必确认这些对应的线程号存在并且该线程代表一个生产者。 (2)生产和消费的规则 在按照上述要求创建线程进行相应的读写操作时,还需要符合以下要求: ①共享缓冲区存在空闲空间时,生产者即可使用共享缓冲区。 ②从上边的测试数据文件例子可以看出,某一生产者生产一个产品后,可能不止一个消费者,或者一个消费者多次地请求消费该产品。此时,只有当所有的消费需求都被满足以后,该产品所在的共享缓冲区才可以被释放,并作为空闲空间允许新的生产者使用。 ③每个消费者线程的各个消费需求之间存在先后顺序。例如上述测试用例文件包含一行信息“5 C 3 l 2 4”,可知这代表一个消费者线程,该线程请求消费1,2,4号生产者线程生产的产品。而这种消费是有严格顺序的,消费1号线程产品的请求得到满足后才能继续往下请求2号生产者线程的产品。 ④要求在每个线程发出读写操作申请、开始读写操作和结束读写操作时分别显示提示信息。 (3)相关基础知识 本实验所使用的生产者和消费者模型具有如下特点: 本实验的多个缓冲区不是环形循环的,也不要求按顺序访问。生产者可以把产品放到目前某一个空缓冲区中。 消费者只消费指定生产者的产品。 在测试用例文件中指定了所有的生产和消费的需求,只有当共享缓冲区的数据满足了所有关于它的消费需求后,此共享缓冲区才可以作为空闲空间允许新的生产者使用。 本实验在为生产者分配缓冲区时各生产者间必须互斥,此后各个生产者的具体生产活动可以并发。而消费者之间只有在对同一产品进行消费时才需要互斥,同时它们在消费过程结束时需要判断该消费对象是否已经消费完毕并清除该产品。 Windows用来实现同步和互斥的实体。在Windows中,常见的同步对象有:信号量(Semaphore)、互斥量(Mutex)、临界段(CriticalSection)等。使用这些对象都分为三个步骤,一是创建或者初始化:接着请求该同步对象,随即进入临界区,这一步对应于互斥量的上锁;最后释放该同步对象,这对应于互斥量的解锁。这些同步对象在一个线程中创建,在其他线程中都可以使用,从而实现同步互斥。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值