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;
}
上面的代码简单解释一下:
- TaskHandler::start() 中会创建一个线程,线程会申请一把互斥锁,并且睡眠 10s,目的是为了在 fork 的时候依然占用这把互斥锁
- TaskHandler::print_str() 会申请这把互斥锁,然后打印字符串
- 程序 main 开始,调用 start() 创建子线程
- 然后 fork() 子进程
- 子进程死循环执行 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 有一致的情况。其他线程不会被复制。