【Linux学习笔记】进程fork子进程时,会复制父进程中的线程吗

35 篇文章 1 订阅

fork会复制线程吗
结论:主进程 fork 之后,仅会复制发起调用的线程,不会复制其他线程,如果某个线程占用了某个锁,但是到了子进程,该线程是蒸发掉的,子进程会拷贝这把锁,但是不知道谁能释放,最终死锁。

写一个 demo 验证一下,是否 fork 不会复制子线程,并且有可能造成死锁:

fork demo 验证

// file: fork_copy_thread.cc
// g++ fork_copy_thread.cc  -o fork_copy_thread -std=c++11 -lpthread -ggdb


#include <cstdio>
#include <unistd.h>

#include <iostream>
#include <memory>
#include <string>
#include <thread>
#include <chrono>
#include <mutex>

class Event {
public:
    Event() = default;
    ~Event() = default;
public:
    std::string str_;
};


class TaskHandler {
public:
    TaskHandler() = default;
    ~TaskHandler() = default;

public:
    void start() {
        auto lam = [&]() -> void {
            {
                std::unique_lock<std::mutex> lock(ev_mutex_);
                this->ev_ = std::make_shared<Event>();
                this->ev_->str_ = "hello fork";
                // hold this lock for 10 seconds
                std::this_thread::sleep_for(std::chrono::seconds(10));
            }
            std::cout << "father thread done, exit" << std::endl;
        };

        std::thread th(lam);
        th.detach();
    }

    void print_str() {
        std::unique_lock<std::mutex> lock(ev_mutex_);
        if (!ev_) {
            std::cout << "event  not ready" << std::endl;
            return;
        }
        std::cout << "event:" << ev_->str_ << std::endl;
    }

private:
    std::shared_ptr<Event> ev_ { nullptr };
    std::mutex ev_mutex_;
};

std::shared_ptr<TaskHandler> tsk = nullptr;

int main() {
    tsk = std::make_shared<TaskHandler>();
    tsk->start();
    std::this_thread::sleep_for(std::chrono::seconds(1));

    // for child process
    pid_t pid = fork();
    switch (pid) {
        case -1:
            {
                return -1;
            }
        case 0:
            {
                std::cout << "this is child process" << std::endl;
                while (true) {
                    // will core here, because tsk->ev_ is created in father-thread, not copyed,
                    // so in child process, tsk->ev_ is nullptr
                    tsk->print_str();
                    std::this_thread::sleep_for(std::chrono::seconds(1));
                }
            }
        default:
            {
                // this is father
                break;
            }
    } // end switch

    while (true) {
        std::this_thread::sleep_for(std::chrono::seconds(1));
    }
    return 0;
}

上面的代码简单解释一下:

  1. TaskHandler::start() 中会创建一个线程,线程会申请一把互斥锁,并且睡眠 10s,目的是为了在 fork 的时候依然占用这把互斥锁
  2. TaskHandler::print_str() 会申请这把互斥锁,然后打印字符串
  3. 程序 main 开始,调用 start() 创建子线程
  4. 然后 fork() 子进程
  5. 子进程死循环执行 print_str() 函数打印字符串

运行后与预期不符,子进程并没有死循环打印字符串,死锁了。
然后使用 gdb attach 子进程:

(gdb) bt
#0  0x00007f4154e1a54d in __lll_lock_wait () from /lib64/libpthread.so.0
#1  0x00007f4154e15e9b in _L_lock_883 () from /lib64/libpthread.so.0
#2  0x00007f4154e15d68 in pthread_mutex_lock () from /lib64/libpthread.so.0
#3  0x000000000040128c in __gthread_mutex_lock (__mutex=0x1d2fc48) at /usr/include/c++/4.8.5/x86_64-unknown-linux-gnu/bits/gthr-default.h:748
#4  0x0000000000401730 in std::mutex::lock (this=0x1d2fc48) at /usr/include/c++/4.8.5/mutex:134
#5  0x0000000000401f99 in std::unique_lock<std::mutex>::lock (this=0x7fff168c43a0) at /usr/include/c++/4.8.5/mutex:511
#6  0x0000000000401b13 in std::unique_lock<std::mutex>::unique_lock (this=0x7fff168c43a0, __m=...) at /usr/include/c++/4.8.5/mutex:443
#7  0x0000000000401988 in TaskHandler::print_str (this=0x1d2fc38) at fork_copy_thread.cc:43
#8  0x00000000004013ff in main () at fork_copy_thread.cc:76
(gdb)

可以看到子进程卡在了 print_str() 函数上。
上面的代码,父进程创建线程后,占用了锁,此时 fork 了子进程,子进程拷贝了父进程空间的内存,包括锁,但是没有复制子线程,造成子进程无法获取锁,最终死锁。

fork copy thread?

上面已经验证了死锁的产生原因是由于 fork 时并没有把父进程里的线程复制到子进程,导致子进程无法获取锁。那么简单修改一下上面的代码,来验证一下子进程确实是没有复制父进程的子线程。

// file: fork_copy_thread.cc
// g++ fork_copy_thread.cc  -o fork_copy_thread -std=c++11 -lpthread -ggdb
#include <cstdio>
#include <unistd.h>

#include <iostream>
#include <memory>
#include <string>
#include <thread>
#include <chrono>
#include <mutex>

class Event {
public:
    Event() = default;
    ~Event() = default;
public:
    std::string str_;
};


class TaskHandler {
public:
    TaskHandler() = default;
    ~TaskHandler() = default;

public:
    void start() {
        auto lam = [&]() -> void {
            {
                this->ev_ = std::make_shared<Event>();
                this->ev_->str_ = "hello fork";
                // hold this lock for 10 seconds
                //std::this_thread::sleep_for(std::chrono::seconds(10));
            }
            while (true) {
                std::cout << "this threadid:" << std::this_thread::get_id() << " run" << std::endl;
                std::this_thread::sleep_for(std::chrono::seconds(1));
            }
        };

        std::thread th(lam);
        th.detach();
    }

    void print_str() {
        if (!ev_) {
            std::cout << "event  not ready" << std::endl;
            return;
        }
        std::cout << "event:" << ev_->str_ << std::endl;
    }

private:
    std::shared_ptr<Event> ev_ { nullptr };
    std::mutex ev_mutex_;
};

std::shared_ptr<TaskHandler> tsk = nullptr;

int main() {
    tsk = std::make_shared<TaskHandler>();
    tsk->start();
    std::this_thread::sleep_for(std::chrono::seconds(1));

    // for child process
    pid_t pid = fork();
    switch (pid) {
        case -1:
            {
                return -1;
            }
        case 0:
            {
                std::cout << "this is child process" << std::endl;
                while (true) {
                    std::this_thread::sleep_for(std::chrono::seconds(1));
                }

            }
        default:
            {
                // this is father
                break;
            }
    } // end switch

    while (true) {
        std::this_thread::sleep_for(std::chrono::seconds(1));
    }
    return 0;
}

简单解释一下修改了啥:
父进程启动一个线程,循环打印字符串
父进程 fork,子进程保持睡眠
验证子进程是否有线程打印字符串(如果复制了的话,理应会打印)

./fork_copy_thread 
this threadid:139954726463232 run
this threadid:139954726463232 run
this is child process
this threadid:139954726463232 run
this threadid:139954726463232 run
this threadid:139954726463232 run
this threadid:139954726463232 run

可以看到只有一个线程在打印,也就是父进程创建的那个线程;fork 之后父进程的线程在子进程蒸发了。

多线程程序使用 fork 一定要谨慎,再谨慎,并且也不推荐这样的做法。

fork 到底复制了什么

Copy On Write

Copy On Write(写时复制)技术大大提高了 fork 的性能。fork 之后,内核会把父进程中的所有内存页都设置为 read-only,然后子进程的地址空间指向父进程。如果父进程和子进程都没有涉及到内存的写操作,那么父子进程保持这样的状态,也就是子进程并不会复制父进程的内存空间;如果父进程或者子进程产生了写操作,那么由于内存页被设置为 read-only,所以会触发页异常中断,然后中断程序会把该内存页复制一份,至此父子进程就拥有不同的内存页;而其他没有操作的内存页依然共享。

简单总结下 fork

要理解 fork 的原理,Copy On Write 的原理,重点是理解虚拟内存和物理内存的关系。

fork 之后,子进程会复制父进程的虚拟内存空间,也就是代码段、数据段、堆栈等,虚拟内存空间里表达的就是程序里各个变量的地址,所以子进程里各个变量的地址和父进程里各个变量的地址是一样的。

父子进程只读时,不会发生真实的物理内存拷贝,他们的页映射表内容一致,即同样的虚拟内存地址指向同样的物理内存地址;但当有一方写入数据时,内核会复制要写入的页,此时修改数据的一方的页映射表就发生了变化,即同样的虚拟内存地址指向了不同的物理内存地址,但其他部分还是一样的;

另外,fork 仅会将发起调用的线程复制到子进程中,所以子进程中的线程 ID 与主进程线程 ID 有一致的情况。其他线程不会被复制。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值