多线程程序中fork导致的一些问题
最近项目中,在使用多线程和多进程时,遇到了些问题。
问题描述:在多线程程序中fork出一个新进程,发现新的进程无法正常工作。
解决办法:将开线程的代码放在fork以后。也就是放在新的子进程中进行创建。
产生原因:在使用fork时会将原来进程中的所有内存数据复制一份保存在子进程中。但是在拷贝的时候,但是线程是无法被拷贝的。如果在原来线程中加了锁,在使用的时候会造成死锁。以下是具体的例子(转发):
在多线程程序里,在”自身以外的线程存在的状态”下一使用fork的话,就可能引起各种各样的问题.比较典型的例子就是,fork出来的子进程可能会死锁.请不要,在不能把握问题的原委的情况下就在多线程程序里fork子进程.
能引起什么问题呢?
那看看实例吧.一执行下面的代码,在子进程的执行开始处调用doit()时,发生死锁的机率会很高.
void* doit(void*) {
static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_lock(&mutex);
struct timespec ts = {10, 0}; nanosleep(&ts, 0); // 10秒寝る
// 睡10秒
pthread_mutex_unlock(&mutex);
return 0;
}
int main(void) {
pthread_t t;
pthread_create(&t, 0, doit, 0); // 做成并启动子线程
if (fork() == 0) {
//子进程
//在子进程被创建的瞬间,父的子进程在执行nanosleep的场合比较多
doit(0);
return 0;
}
pthread_join(t, 0); //
// 等待子线程结束
}
以下是说明死锁的理由:
一般的,fork做如下事情
1. 父进程的内存数据会原封不动的拷贝到子进程中
2. 子进程在单线程状态下被生成
在内存区域里,静态变量mutex的内存会被拷贝到子进程里.而且,父进程里即使存在多个线程,但它们也不会被继承到子进程里.
fork的这两个特征就是造成死锁的原因.
译者注: 死锁原因的详细解释 ---
1. 线程里的doit()先执行.
2. doit执行的时候会给互斥体变量mutex加锁.
3. mutex变量的内容会原样拷贝到fork出来的子进程中(在此之前,mutex变量的内容已经被线程改写成锁定状态).
4.子进程再次调用doit的时候,在锁定互斥体mutex的时候会发现它已经被加锁,所以就一直等待,直到拥有该互斥体的进程释放它(实际上没有人拥有这个mutex锁).
5.线程的doit执行完成之前会把自己的mutex释放,但这是的mutex和子进程里的mutex已经是两份内存.所以即使释放了mutex锁也不会对子进程里的mutex造成什么影响.
例如,请试着考虑下面那样的执行流程,就明白为什么在上面多线程程序里不经意地使用fork就造成死锁了*3.
1. 在fork前的父进程中,启动了线程1和2
2. 线程1调用doit函数
3. doit函数锁定自己的mutex
4. 线程1执行nanosleep函数睡10秒
5. 在这儿程序处理切换到线程2
6. 线程2调用fork函数
7. 生成子进程
8. 这时,子进程的doit函数用的mutex处于”锁定状态”,而且,解除锁定的线程在子进程里不存在
9. 子进程的处理开始
10. 子进程调用doit函数
11. 子进程再次锁定已经是被锁定状态的mutex,然后就造成死锁
像这里的doit函数那样的,在多线程里因为fork而引起问题的函数,我们把它叫
做”fork-unsafe函数”.反之,不能引起问题的函数叫做”fork-safe函数”.虽然在一些商用的UNIX里,源于OS提供的函数(系统调
用),在文档里有fork-safety的记载,但是在
Linux(glibc)里当然!不会被记载.即使在POSIX里也没有特别的规定,所以那些函数是fork-safe的,几乎不能判别.不明白的话,作
为unsafe考虑的话会比较好一点吧.(2004/9/12追记)Wolfram
Gloger说过,调用异步信号安全函数是规格标准,所以试着调查了一下,在pthread_atforkの这个地方里有” In the
meantime*5, only a short list of async-signal-safe library routines are
promised to be available.”这样的话.好像就是这样.
随便说一下,malloc函数就是一个维持自身固有mutex的典型例子,通常情况下它是fork-unsafe的.依赖于malloc函数的函数有很多,例如printf函数等,也是变成fork-unsafe的.
直到目前为止,已经写上了thread+fork是危险的,但是有一个特例需要告诉大家.”fork后马上调用exec的场合,是作为一个特列不会产生问题的”. 什么原因呢..?exec函数*6一被调用,进程的”内存数据”就被临时重置成非常漂亮的状态.因此,即使在多线程状态的进程里,fork后不马上调用一切危险的函数,只是调用exec函数的话,子进程将不会产生任何的误动作.但是,请注意这里使用的”马上”这个词.即使exec前仅仅只是调用一回printf(“I’m
child process”),也会有死锁的危险.
译者注:exec函数里指明的命令一被执行,该命令的内存映像就会覆盖父进程的内存空间.所以,父进程里的任何数据将不复存在.
本blog的理解:查看前面进程创建中,子进程在创建后,是写时复制的,也就是子进程刚创建时,与父进程一样的副本,当exce后,那么老的地址空间被丢弃,而被新的exec的命令的内存的印像覆盖了进程的内存空间,所以锁的状态无关紧要了。
如何规避灾难呢?
为了在多线程的程序中安全的使用fork,而规避死锁问题的方法有吗?试着考虑几个.
规避方法1:做fork的时候,在它之前让其他的线程完全终止.
在fork之前,让其他的线程完全终止的话,则不会引起问题.但这仅仅是可能的情况.还有,因为一些原因而其他线程不能结束就执行了fork的时候,就会是产生出一些解析困难的不具合的问题.
规避方法2:fork后在子进程中马上调用exec函数
(2004/9/11 追记一些忘了写的东西)
不用使用规避方法1的时候,在fork后不调用任何函数(printf等)就马上调用execl等,exec系列的函数.如果在程序里不使用”没有exec就fork”的话,这应该就是实际的规避方法吧.
译者注:笔者的意思可能是把原本子进程应该做的事情写成一个单独的程序,编译成可执行程序后由exec函数来调用.
规避方法3:”其他线程”中,不做fork-unsafe的处理
除了调用fork的线程,其他的所有线程不要做
fork-unsafe的处理.为了提高数值计算的速度而使用线程的场合*7,这可能是fork-
safe的处理,但是在一般的应用程序里则不是这样的.即使仅仅是把握了那些函数是fork-safe的,做起来还不是很容易的.fork-safe函
数,必须是异步信号安全函数,而他们都是能数的过来的.因此,malloc/new,printf这些函数是不能使用的.
规避方法4:使用pthread_atfork函数,在即将fork之前调用事先准备的回调函数.apue中详细介绍了它
使用pthread_atfork函数,在即将
fork之前调用事先准备的回调函数,在这个回调函数内,协商清除进程的内存数据.但是关于OS提供的函数
(例:malloc),在回调函数里没有清除它的方法.因为malloc里使用的数据结构在外部是看不见的.因此,pthread_atfork函数几乎
是没有什么实用价值的.
规避方法5:在多线程程序里,不使用fork
就是不使用fork的方法.即用pthread_create来代替fork.这跟规避策2一样都是比较实际的方法,值得推荐.
*1:生成子进程的系统调用
*2:全局变量和函数内的静态变量
*3:如果使用Linux的话,查看pthread_atfork函数的man手册比较好.关于这些流程都有一些解释.
*4:Solaris和HP-UX等
*5:从fork后到exec执行的这段时间
*6:≒execve系统调用
*7:仅仅做四则演算的话就是fork-safe的
总结:在程序正常运行时出现不能正常工作,而在调试时又能正常工作。则可以考虑死锁的情况!
多线程程序中fork导致的一些问题的更多相关文章
- zz剖析为什么在多核多线程程序中要慎用volatile关键字?
[摘要]编译器保证volatile自己的读写有序,但由于optimization和多线程可以和非volatile读写interleave,也就是不原子,也就是没有用.C++11 supposed会支持 ...
- CountDownLatch在多线程程序中的应用
一.CountDownLatch介绍 CountDownLatch是JDK1.5之后引入的,存在于java.util.concurrent包下,能够使一个线程等待其他线程完成动作后再执行.构造方法: ...
- Linux -- 在多线程程序中避免False Sharing
1.什么是false sharing 在对称多处理器(SMP)系统中,每个处理器均有属于自己的本地高速缓存区. 如图,CPU0和CPU1有各自的本地高速缓存区(cache).线程0和线程1会用到不同的 ...
- 多线程程序调用fork的现象
- 多线程中fork与mutex
在多线程程序中fork出一个新进程,发现新的进程无法正常工作.因为:在使用fork时会将原来进程中的所有内存数据复制一份保存在子进程中.但是在拷贝的时候,但是线程是无法被拷贝的.如果在原来线程中加了锁 ...
- gdb常用命令及使用gdb调试多进程多线程程序
一.常用普通调试命令 1.简单介绍GDB 介绍: gdb是Linux环境下的代码调试⼯具.使⽤:需要在源代码⽣成的时候加上 -g 选项.开始使⽤: gdb binFile退出: ctrl + d 或 ...
- Linux下多线程编程中信号量介绍及简单使用
在Linux中有两种方法用于处理线程同步:信号量和互斥量. 线程的信号量是一种特殊的变量,它可以被增加或减少,但对其的关键访问被保证是原子操作.如果一个程序中有多个线程试图改变一个信号量的值,系统将保 ...
- 《Java大学教程》—第22章 多线程程序
22.2 进程(process):P551时间切片(time-slicing):处理器只是完成了一个任务的一部分工作,然后完成下一个任务的一部分工作,因为处理吕每次完成工作的时间都非常短,因此看起来这 ...
- C# 多线程程序隐患
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.T ...
随机推荐
- Qt调用外部程序QProcess通信
mainwindow.cpp文件: -------------------------------- #include "mainwindow.h" #include " ...
- PHP APC缓存配置、使用详解
一.APC缓存简介 APC,全称是Alternative PHP Cache,官方翻译叫”可选PHP缓存”.它为我们提供了缓存和优化PHP的中间代码的框架. APC的缓存分两部分:系统缓存和用户数据缓 ...
- oracle 联表更新
依 a 表 cate_pub_id 为依据 更新 v 表的 cate_pub_id update td_tobrel_cate_pub_attrval v set v.CATE_PUB_ID=(se ...
- Tomcat学习笔记 - 错误日志 - Tomcat访问Manager apps出现401 Unauthorized错误
原因是配置文件中未指定管理员身份. 打开tomcat>conf>tomcat-user.xml文件,添加如下代码: <role rolename="admin-gui&qu ...
- php5.5新特性之yield理解
今天,在阅读别人代码时,其中出现了一个陌生的关键字yield,想一探究竟,于是找到:http://php.net/manual/zh/language.generators.overview.php ...
- sql 添加约束
在表中添加约束,基本常用的有两种类型,一个是创建表时同时添加约束,另一个是创建好表通过修改表添加约束,在这里是创建表时同时添加约束,但是有两种不同的用写法. 在这里列举出一些创建约束的形式,共参考(均 ...
- PHP中output control
Output Control 函数可以让你自由控制脚本中数据的输出.它非常地有用,特别是对于:当你想在数据已经输出后,再输出文件头的情况.输出控制函数不对使用 header() 或 setcookie ...
- Mysql基本类型(五种年日期时间类型)——mysql之二
转自:<MySQL技术内幕:时间和日期数据类型> http://tech.it168.com/a2012/0904/1393/000001393605_all.shtml
- Dropbox 用什么语言开发的?(Python在各个平台都是全能的,特别是有PyObjC真没想到)
Dropbox 绝大部分是用 Python 开发的.用到 Python 的地方有:服务器后台.客户端.Dropbox 网页版前段.API 后台.数据分析. 在服务器端.桌面版客户端使用的是 Pytho ...
- 懒猫们终究要付出代码(本领是一生的),鲸鱼们的短视(逐小利而暴死)——这么说我应该只去互联网公司:IM,云存储,邮箱(别的一概不考虑)
摘自周鸿伟的书,好像: