转载目的主要了解fork原理,实际fork的使用愈来愈少,缘由也能够经过本文了解。html
实际在当前的多线程状况下,fork已经基本无太多可取之处了。linux
fork的设计之出应该就是为了更方便地使用多进程程序,提升并发性。编程
然而对于多个并发须要共享大量数据时,多线程拥有的内部通讯每每比较高效,而fork只实现了多进程,只能经过相似共享内存方式通讯。安全
在单核时代,你们所编写的程序都是单进程/单线程程序。随着计算机硬件技术的发展,进入了多核时代后,为了下降响应时间,重复充分利用多核cpu的资源,使用多进程编程的手段逐渐被人们接受和掌握。然而由于建立一个进程代价比较大,多线程编程的手段也就逐渐被人们承认和喜好了。多线程
记得在我刚刚学习线程进程的时候就想,为何不多见人把多进程和多线程结合起来使用呢,把两者结合起来不是更好吗?如今想一想当初真是too young too simple,后文就主要讨论一下这个问题。并发
进程的经典定义就是一个执行中的程序的实例。系统中的每一个程序都是运行在某个进程的context中的。context是由程序正确运行所需的状态组成的,这个状态包括存放在存储器中的程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器(PC)、环境变量以及打开的文件描述符的集合。函数
进程主要提供给上层的应用程序两个抽象:高并发
线程,就是运行在进程context中的逻辑流。线程由内核自动调度。每一个线程都有它本身的线程context,包括一个惟一的整数线程ID、栈、栈指针、程序计数器(PC)、通用目的寄存器和条件码。每一个线程和运行在同一进程内的其余线程一块儿共享进程context的剩余部分。这包括整个用户虚拟地址空间,它是由只读文本(代码)、读/写数据、堆以及全部的共享库代码和数据区域组成。线程也一样共享打开文件的集合。性能
即进程是资源管理的最小单位,而线程是程序执行的最小单位。学习
在linux系统中,posix线程能够“看作”为一种轻量级的进程,pthread_create建立线程和fork建立进程都是在内核中调用__clone函数建立的,只不过建立线程或进程的时候选项不一样,好比是否共享虚拟地址空间、文件描述符等。
咱们知道经过fork建立的一个子进程几乎但不彻底与父进程相同。子进程获得与父进程用户级虚拟地址空间相同的(可是独立的)一份拷贝,包括文本、数据和bss段、堆以及用户栈等。子进程还得到与父进程任何打开文件描述符相同的拷贝,这就意味着子进程能够读写父进程中任何打开的文件,父进程和子进程之间最大的区别在于它们有着不一样的PID。
可是有一点须要注意的是,在Linux中,fork的时候只复制当前线程到子进程,在fork(2)-Linux Man Page中有着这样一段相关的描述:
The child process is created with a single thread--the one that called fork(). The entire virtual address space of the parent is replicated in the child, including the states of mutexes, condition variables, and other pthreads objects; the use of pthread_atfork(3) may be helpful for dealing with problems that this can cause.
也就是说除了调用fork的线程外,其余线程在子进程中“蒸发”了。
这就是多线程中fork所带来的一切问题的根源所在了。
互斥锁,就是多线程fork大部分问题的关键部分。
在大多数操做系统上,为了性能的因素,锁基本上都是实如今用户态的而非内核态(由于在用户态实现最方便,基本上就是经过原子操做或者以前文章中提到的memory barrier实现的),因此调用fork的时候,会复制父进程的全部锁到子进程中。
问题就出在这了。从操做系统的角度上看,对于每个锁都有它的持有者,即对它进行lock操做的线程。假设在fork以前,一个线程对某个锁进行的lock操做,即持有了该锁,而后另一个线程调用了fork建立子进程。但是在子进程中持有那个锁的线程却"消失"了,从子进程的角度来看,这个锁被“永久”的上锁了,由于它的持有者“蒸发”了。
那么若是子进程中的任何一个线程对这个已经被持有的锁进行lock操做话,就会发生死锁。
固然了有人会说能够在fork以前,让准备调用fork的线程获取全部的锁,而后再在fork出的子进程的中释放每个锁。先不说现实中的业务逻辑以及其余因素允不容许这样作,这种作法会带来一个问题,那就是隐含了一种上锁的前后顺序,若是次序和平时不一样,就会发生死锁。
若是你说本身必定能够按正确的顺序上锁而不出错的话,还有一个隐含的问题是你所不能控制的,那就是库函数。
由于你不能肯定你所用到的全部库函数都不会使用共享数据,即他们都是彻底线程安全的。有至关一部分线程安全的库函数都是在内部经过持有互斥锁的方式来实现的,好比几乎全部程序都会用到的C/C++标准库函数malloc、printf等等。
好比一个多线程程序在fork以前不免会分配动态内存,这就必然会用到malloc函数;而在fork以后的子进程中也不免要分配动态内存,这也一样要用到malloc,可这倒是不安全的,由于有可能malloc内部的锁已经在fork以前被某一个线程所持有了,而那个线程却在子进程中消失了。
按照上文的分析,彷佛多线程中在fork出的子进程中马上调用exec函数是惟一明智的选择了,其实即便这样作仍是有一点不足。由于子进程会继承父进程中全部已打开的文件描述符,因此在执行exec以前子进程仍然能够读写父进程中的文件,但若是你不但愿子进程能读写父进程里的某个已打开的文件该怎么办?
或许fcntl设置文件属性是一种办法:
1
2
3
4
5
6
|
int
fd = open(
"file"
, O_RDWR | O_CREAT);
if
(fd < 0)
{
perror
(
"open"
);
}
fcntl(fd, F_SETFD, FD_CLOEXEC);
|
可是若是在open打开file文件以后,调用fcntl设置CLOEXEC属性以前有其余线程fork出了子进程了的话,这个子进程仍然是能够读写file文件。若是用锁的话,就又回到了上文所讨论的状况了。
从Linux 2.6.23版本的内核开始,咱们能够在open中设置O_CLOEXEC标志了,至关于“打开文件再设置CLOEXEC”成为了一个原子操做。这样在fork出的子进程执行exec以前就不能读写父进程中已打开的文件了。
若是你不幸真的碰到了一个要解决多线程中fork的问题的时候,能够尝试使用pthread_atfork:
1
|
int
pthread_atfork(
void
(*prepare)(
void
),
void
(*parent)
void
(),
void
(*child)(
void
));
|
由于子进程继承的是父进程的锁的拷贝,全部上述并非解锁了两次,而是各自独自解锁。能够屡次调用pthread_atfork函数从而设置多套fork处理程序,可是使用多个处理程序的时候。处理程序的调用顺序并不相同。parent和child是以它们注册时的顺序调用的,而prepare的调用顺序与注册顺序相反。这样能够容许多个模块注册它们本身的处理程序而且保持锁的层次(相似于多个RAII对象的构造析构层次)。
须要注意的是pthread_atfork只能清理锁,但不能清理条件变量。在有些系统的实现中条件变量不须要清理。可是在有的系统中,条件变量的实现中包含了锁,这种状况就须要清理。可是目前并无清理条件变量的接口和方法。