多线程下的fork问题(模拟与解决)

Posted It‘s so simple

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了多线程下的fork问题(模拟与解决)相关的知识,希望对你有一定的参考价值。

前言

有关进程、多线程、fork的概念,请看我之前写的这两篇文章。
Linux:进程控制(进程创建、进程终止、进程等待、进程程序替换)
Linux:详解多线程(线程概念、线程控制—线程创建、线程终止、线程等待)(一)

1. 浅谈在多线程下的fork的问题

当fork函数创建出一个子进程的时候,子进程会拷贝父进程的进程虚拟地址空间,并且也会从父进程中拷贝一份相应的内存数据到子进程中,这是我们所知道的在单进程的情况下它是这样的,但是如果是在多线程中呢?在多线程代码中,如果在某个工作线程中调用fork函数时,fork函数是怎样运作的?是将该程序的整个工作线程都拷贝一份呢,还是只是拷贝当前创建出他的工作线程的虚拟地址空间?

在 POSIX 标准中,fork 的行为是这样的:复制整个用户空间的数据(通常使用 copy-on-write 的策略,所以可以实现的速度很快)以及所有系统对象, 然后仅复制当前线程到子进程。这里:所有父进程中别的线程,到了子进程中都是突然蒸发掉的。其它线程的突然消失,是一切问题的根源

在大多数操作系统上,为了性能的因素,锁基本上都是实现在用户态的而非内核态(因为在用户态实现最方便,基本上就是通过原子操作),所以调用fork的时候,会复制父进程的所有锁到子进程中。

这就造成了,如果线程A在创建子进程之前就有其他线程对当前全局变量中的锁进行了加锁操作(换句话说就是在创建子进程之前对应的锁就已经被拿到了),当创建出子进程之后,子进程和对应父进程就是两个完全不同的进程(进程独立性),但是子进程却是拷贝于父进程的进程虚拟空间,这就造成了如果父进程中已经拿到了锁,子进程中所对应的那把锁也是被加锁状态,但是子进程目前是不知道这把锁的状态的,因此,一旦在子进程中对这把锁进行加锁操作,就会造成死锁的现象产生。

总结一下,出现死锁的原因就是:

  • 父进程的内存数据会原封不动的拷贝到子进程中
  • 子进程在单线程状态下被生成,仅生成fork函数所在的这个线程。
  • 父进程(在创建子进程之前)中的某个锁已经被lock掉了,并且子进程中对这把锁再次进行lock,就会发生死锁

2. 死锁问题的模拟实现

#include <unistd.h>
#include <sys/wait.h>
#include <pthread.h>
#include <iostream>
using namespace std;


pthread_mutex_t mt;

void* pthreadFork(void * arg)
{
    pthread_detach(pthread_self());
	//等待2秒的原因是为了先让pthreadLock拿到锁
    sleep(2);
    int ret = fork();
    if(ret < 0)
    {
        cout << "fork failed" << endl;
        return 0;
    }
    else if(ret == 0)
    {
        //child
        cout << "It's Child !" << endl;
        while(1)
        {
            pthread_mutex_lock(&mt);
            cout << "It's test pthreadFork_Child " << endl;
            pthread_mutex_unlock(&mt);
        }
    }
    else
    {
        //father
        cout << "It's father!" << endl;
        wait(NULL);
    }

    return NULL;
}

void* pthreadLock(void * arg)
{
    pthread_detach(pthread_self());
    while(1)
    {
	    pthread_mutex_lock(&mt);
	    cout << "It's pthreadLock" << endl;
	    //等待3秒的原因是为了让pthreadFork线程,在已经拿到锁的情况下创建子进程
	    sleep(3);
	    pthread_mutex_unlock(&mt);
	}
    return NULL;
}

int main()
{
    pthread_t pid;

    pthread_mutex_init(&mt,NULL);
    int ret = pthread_create(&pid,NULL,pthreadFork,NULL);
    if(ret < 0)
    {
        cout << "pthread failed" << endl;
        return 0;
    }

    ret = pthread_create(&pid,NULL,pthreadLock,NULL);
    if(ret < 0)
    {
        cout << "pthread failed" << endl;
        return 0;
    }

    while(1)
    {
        sleep(1);
    }

    pthread_mutex_destroy(&mt);
    return 0;
}

结果验证:

使用ps -ef | grep xxx得到当前子进程的pid号,再用gdb attach pid对其进行调试


查看当前锁的状态,发现已经是死锁状态,或者说在刚开始时就拿到了一把废锁(不能进行加锁的锁)。

对12576这个线程进行调试,要对线程进行调试,首先使用gdb对其进程进行调试


这里,我们可以轻而易举的验证出,子进程发生了死锁,进入了阻塞状态,而父进程中的线程是在正常运行的(进一步体现了进程的独立性)。

3. 解决办法

解决①:多线程中在fork出的子进程中立刻调用exec函数(进程程序替换)即可。
有关进程程序替换的概念可以查看我在前言中给的链接。

解决②:使用pthread_atfork函数。

int pthread_atfork(void (*prepare)(void), void (*parent)(void),void (*child)(void));

  • prepare:prepare处理函数由父进程在fork创建子进程前调用,这个函数的任务是获取父进程定义的所有锁。
  • parent:parent处理函数是在fork创建了子进程以后,但在fork返回之前在父进程环境中调用的。它的任务是对prepare获取的所有锁解锁。
  • child:child处理函数在fork返回之前在子进程环境中调用,与parent处理函数一样,它也必须解锁所有prepare中所获取的锁。

代码实现:

#include <unistd.h>
#include <sys/wait.h>
#include <pthread.h>
#include <iostream>
using namespace std;


pthread_mutex_t mt;

void* pthreadFork(void * arg)
{
    pthread_detach(pthread_self());

    sleep(2);
    int ret = fork();
    if(ret < 0)
    {
        cout << "fork failed" << endl;
        return 0;
    }
    else if(ret == 0)
    {
        //child
        cout << "It's Child !" << endl;
        while(1)
        {
            pthread_mutex_lock(&mt);
            cout << "It's test pthreadFork_Child " << endl;
            pthread_mutex_unlock(&mt);
        }
    }
    else
    {
        //father
        cout << "It's father!" << endl;
        wait(NULL);
    }

    return NULL;
}

void* pthreadLock(void * arg)
{
    pthread_detach(pthread_self());
    while(1)
    {
        pthread_mutex_lock(&mt);
        cout << "It's pthreadLock" << endl;
        sleep(3);
        pthread_mutex_unlock(&mt);
        //这里睡眠1秒的原因是能够让prepare函数拿到锁
        sleep(1);
    }
    return NULL;
}

void prepare(void)
{
    pthread_mutex_lock(&mt);
    cout << "prepare: Get mutex success" << endl;
}

void child(void)
{
    pthread_mutex_unlock(&mt);
    cout << "child: release mutex success" << endl;
}
void parent(void)
{
    pthread_mutex_unlock(&mt);
    cout << "parent: release mutex success" << endl;
}

int main()
{
    pthread_t pid;

    int ret = pthread_atfork(prepare,parent,child);

    pthread_mutex_init(&mt,NULL);
    ret = pthread_create(&pid,NULL,pthreadFork,NULL);
    if(ret < 0)
    {
        cout << "pthread failed" << endl;
        return 0;
    }

    ret = pthread_create(&pid,NULL,pthreadLock,NULL);
    if(ret < 0)
    {
        cout << "pthread failed" << endl;
        return 0;
    }

    while(1)
    {
        sleep(1);
    }

    pthread_mutex_destroy(&mt);
    return 0;
}

结果验证:

可以明显的发现子进程可以对这把锁进行相应的操作了,这也就很好的解决此类问题。

以上是关于多线程下的fork问题(模拟与解决)的主要内容,如果未能解决你的问题,请参考以下文章

多线程下的fork问题(模拟与解决)

线程基础:多任务处理(13)——Fork/Join框架(解决排序问题)

基于LINUX下的多线程

期末复习——线程

用Java模拟多线程下的客户端与服务端数据交互

多进程多线程在不同环境下的操作