## 背景

本文并不是介绍Linux多进程多线程编程的科普文,如果希望系统学习Linux编程,可以看[《Unix环境高级编程》第3版]

(https://book.douban.com/subject/1788421/)

本文是描述多进程多线程编程中遇到过的一个坑,并从内核角度分析其原理。这里说的多进程多线程并不是单一的**多进程或多线程**,而是**多进程和多线程**,往往会在写一个大型应用时才会用到多进程多线程的模型。

这是怎么样的一个坑呢?假设有下面的代码:

童鞋们能分析出来,线程函数```sub_pthread```会被执行多少次么?线程函数打印出来的ID是父进程ID呢?还是子进程ID?还是父子进程都有?

答案是,只会执行1次,且是父进程的ID!为什么呢?

[GMPY@10:02 share]$./signal-safe 
ID 6889: in sub_pthread
ID 6889 (father)
ID 6891 (children)

裤子都脱了,你就给我看这个?当然,这个没什么悬念,到目前为止还很简单。精彩的地方正式开始。

## 线程和fork

***在已经创建了多线程的进程中调用fork创建子进程,稍不注意就会陷入死锁的尴尬局面***

以下面的代码做个例子:

执行效果如下:

[GMPY@10:37 share]$./test 
--- sub thread lock ---
children burn
--- sub thread unlock ---
--- father lock ---
--- father unlock ---
--- sub thread lock ---
--- father lock ---
--- sub thread unlock ---
--- father unlock ---
--- sub thread lock ---
--- sub thread unlock ---
--- father lock ---

我们发现,子进程挂了,在打印了```children burn```后,没有了下文,因为在**子进程获取锁的时候,死锁了!**

凭什么啊?```sub_pthread```线程不是有释放锁么?父进程都能在线程释放后获取到锁,为什么子线程就获取不到锁呢?

在《Unix环境高级编程 第3版》的12.9章节中是这么描述的:

子进程通过继承整个地址空间的副本,还从父进程那儿继承了每个互斥量、读写锁和条件变量的状态。
    如果父进程包含一个以上的线程,子进程在fork返回以后,如果紧接着不是马上调用exec的话,就需要清理锁状态。
    在子进程内部,只存在一个线程,它是由父进程中调用fork的线程的副本构成的。
    如果父进程中的线程占有锁,子进程将同样占有这些锁。
    问题是子进程并不包含占有锁的线程的副本,所以子进程没有办法知道它占有了哪些锁、需要释放哪些锁。
    ......
    在多线程的进程中,为了避免不一致状态的问题,POSIX.1声明,在fork返回和子进程调用其中一个exec函数之间,
    子进程只能调用异步信号安全的函数。这就限制了在调用exec之前子进程能做什么,但不涉及子进程中锁状态的问题。

究其原因,就是子进程成孤家寡人了。

每个进程都有一个主线程,这个线程参与到任务调度,而不是进程,[可以参考文章](https://www.cnblogs.com/gmpy/p/10265284.html)。

在上面的例子中,父进程通过```pthread_create```创建出了一个小弟```sub_pthread```,父进程与小弟之间配合默契,你释放锁我就获取,玩得不亦乐乎。

这时候,父进程生娃娃了,这个新生娃娃**集成了父进程的绝大部分资源,包括了锁的状态**,然而,子进程并没有共生出小弟,就是说**子进程并没同时创建出小弟线程**,他就是一个坐拥金山的孤家寡人。

所以,问题就来了。如果在父进程创建子进程的时候,父进程的锁被小弟```sub_pthread```占用了,```fork```生出来的子进程锁的状态跟父进程一样一样的,锁上了!被人占有了!因此子进程再获取锁就死锁了。

或者你会说,我在fork前获取锁,在fork后再释放锁不就好了?是的,能解决这个问题,我们自己创建的锁,所以我们知道有什么锁。

最惨的是什么呢?你根本无法知道你调用的函数是否有锁。例如常用的```printf```,其内部实现是有获取锁的,因此在fork出来的子进程执行exec之前,**甚至都不能调用printf**。

我们看看下面的示例:

上面的代码主要做了两件事:

1. 创建线程,循环printf打印字符'\r'
2. 循环创建进程,在子进程中调用printf打印字串

由于printf的锁不可控,为了加大死锁的概率,为```fork```套了一层循环。执行结果怎么样呢?

root@TinaLinux:/mnt/UDISK# demo-c 
fork
ID 1684: in sub_pthread
ID 1684 (father)
ID 1686 (children)
ID 1686 (children) exit
fork
ID 1684 (father)
ID 1687 (children)
ID 1687 (children) exit
fork
ID 1684 (father)

结果在第3次```fork```循环的时候陷入了死锁,子进程不打印不退出,导致父进程```wait```一直阻塞。

*上面的结果在全志嵌入式Tina Linux平台验证,比较有意思的是,同样的代码在PC上却很难复现,可能是C库的差异引起的*

**在fork的子进程到exec之间,只能调用异步信号安全的函数**,这异步信号安全的函数就是认证过不会造成死锁的!

异步信号安全不再展开讨论,有问题找男人

man 7 signal

检索关键字```Async-signal-safe functions```

## 内核原理分析

我们知道,Linux内核中,用```task_struct```表示一个进程/线程,嗯,换句话说,**不管是进程还是线程,在Linux内核中都是用```task_struct```的结构体表示**。

关于进程与线程的异同,可以看文章[《线程调度为什么比进程调度更少开销?》](https://www.cnblogs.com/gmpy/p/10265284.html),这里不累述。

按这个结论,我们```pthread_create```创建小弟线程时,内核实际上是copy父进程的```task_struct```,创建小弟线程的```task_struct```,且让小弟```task_struct```与父进程```task_struct```共享同一套资源。

如下图

在父进程```pthread_create```之后,父进程和小弟线程组成了我们*概念上的父进程*。什么是概念上的父进程呢?在我们的理解中,创建的线程也是归属于父进程,这是概念上的父进程集合体,然而在Linux中,父进程和线程是独立的个体,他们有自己的调度,有自己的流程,就好像一个屋子下不同的人。

父进程fork过程,发生了什么?

跟进**系统调用fork**的代码:

嗯...只是copy了```task_struct```,怪不得fork之后,子进程没有伴生小弟线程。所以fork之后,如下图:

*(为了方便理解,下图忽略了Linux的写时copy机制)*

Linux如此```fork```,这与锁有什么关系呢?我们看下内核中对互斥锁的定义:

一句话概述,就是 **通过原子变量标识和记录锁状态**,用户空间也是一样的做法。

变量值终究是保存在内存中的,不管是保存在堆还是栈亦或其他,终究是(虚拟)内存中某一个地址存储的值。

结合Linux内核的```fork```流程,我们用这样一张图描述进程/线程与锁的关系:

(完)

大碰撞!当Linux多线程遭遇Linux多进程相关推荐

  1. Linux多线程与Linux多进程混合项目的死锁问题

    目录 背景 线程和fork 内核原理分析 背景 本文并不是介绍Linux多进程多线程编程的科普文,如果希望系统学习Linux编程,可以看<Unix环境高级编程>第3版>. 本文是描述 ...

  2. linux 多线程并行计算,Linux下使用POSIX Thread作多核多线程并行计算

    POSIX线程库根据处理器.操作系统等特性封装了一台线程处理的接口.对于目前基于x86处理器架构的Linux系统来说,它往往会默认地将新创建的一个线程调度到与主线程不同的核中执行,如果这样能更好地平衡 ...

  3. linux 多线程 semaphore ,Linux下多线程编程-Pthread和Semaphore使用.doc

    比锄戴垒丛共麦溺庄哆氏葫季袒飞闲棉铆稼椰悲倘寓矩案铺汞嫡懂伸腑箩五穗颗撩护尚巷苯宅瑚铱焕涅职枝怎摔什街杠写冻泡峡蠢舀以咽铝皇篮糠村墟凤帜攒摧定畜遁陛葛杯复妄婚赣续踌肖祷就抖帘荒徘魂圭焙酸劈待钞林讯啊铂 ...

  4. linux多线程_Java+Linux,深入内核源码讲解多线程之进程

    之前写了两篇文章,都是针对Linux这个系统的,为什么?我为什么这么喜欢写这个系统的知识,可能就是为了今天的内容多线程系列,现在多线程不是一个面试重点 啊,那如果你能深入系统内核回答这个知识点,面试官 ...

  5. linux多线程 消费者,linux c 多线程 生产者-消费者二

    linux c 多线程 生产者--消费者2 实在不好意思,第一个版本有些问题,是局部变量和堆里面变量的区别.今天做了一下修改.代码如下. #ifndef _LIST_H_ #define _LIST_ ...

  6. linux多线程冗余,Linux  下的路径冗余

    首先要确认是否安装了device-mapper-multipathing的RPM包 [root@localhost ~]# rpm -qa |grep device device-mapper-1.0 ...

  7. linux 多线程ppn,Linux下如何查看CPU信息, 包括位数和多核信息

    Linux下如何查看CPU信息, 包括位数和多核信息 # uname -a Linux liuzhouping-laptop 2.6.28-14-generic #46-Ubuntu SMP Wed ...

  8. 深入理解Linux多线程

    深入理解Linux多线程 目录 Linux线程概念 什么是线程 二级页表 线程的优点 线程的缺点 线程异常 线程用途 Linux进程VS线程 Linux线程控制 POSIX线程库 创建线程 线程等待 ...

  9. linux的多任务 多进程,浅谈linux模拟多线程崩溃和多进程崩溃

    结论是: 多线程下如果其中一个线程崩溃了会导致其他线程(整个进程)都崩溃: 多进程下如果其中一个进程崩溃了对其余进程没有影响: 多线程 #include #include #include #incl ...

  10. Linux 之二 Linux 多线程

      最近在整理旧电脑时,发现了一些刚入行时的学习记录,以及最早使用新浪博客 http://blog.sina.com.cn/zcshou 写的一些文章.最近要重拾 Linux,所以把这些 Word 文 ...

最新文章

  1. c++中的public,protected,private
  2. 计算机基础知识整理大全_【干货整理】高中文言文基础知识大全,速度收藏!...
  3. FFT ---- 2021牛客多校第一场 H Hash Function
  4. hive 字段不包含某个字符_hive之面试必问 hive调优
  5. CAD2015软件安装资料及教程
  6. linux下的精确wait
  7. 最大字段和(动态规划,C语言)
  8. MySQL存储过程相互调用
  9. 如何 调系统相机_数码单反相机攻略,了解测光模式的区别
  10. vant部署_记录mpvue+vant-weapp的使用(一):安装配置使用,引入vant-weapp
  11. samba访问其他服务器文件权限设置
  12. 数据库系统概论-第一章绪论
  13. python 镜像_python测开平台使用dockerfile构建镜像
  14. Kafka 多种跨 IDC 灾备方案调研对比
  15. 游戏技巧-《七日杀》存档位置
  16. python电玩城源码_2019最新最全价值2W的微信H5电玩城游戏全套源码+架设教程+配置文档...
  17. Vue3悬浮返回主页按钮设计与实现
  18. ios客户端学习-UIButton
  19. L2-021 点赞狂魔(Python3)
  20. python控制qq语音通话_懒人专用的奇淫技巧,用Python实现炫酷的语音操作电脑!...

热门文章

  1. axis2与cxf区别
  2. 升级Xcode8后需要添加的一些权限
  3. Maven聚合与继承
  4. poj 2485 Highways 超级大水题 kruscal
  5. 总结之:CentOS 6.5 rsync+inotify实现数据实时同步备份
  6. VMware ESXI虚拟机及虚拟系统修改MAC地址的方法
  7. html5-微格式-时间的格式
  8. Have Fun with Numbers及循环链表(约瑟夫问题)
  9. 更新CocoaPods碰到的问题及知识点
  10. 翻译Programming WCF Services第二版