进程 | 线程 | 当Linux多线程遭遇Linux多进程
背景
本文并不是介绍Linux多进程多线程编程的科普文,如果希望系统学习Linux编程,可以看[《Unix环境高级编程》第3版]
本文是描述多进程多线程编程中遇到过的一个坑,并从内核角度分析其原理。这里说的多进程多线程并不是单一的多进程或多线程,而是多进程和多线程,往往会在写一个大型应用时才会用到多进程多线程的模型。
这是怎么样的一个坑呢?假设有下面的代码:
童鞋们能分析出来,线程函数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多进程的更多相关文章
- ZT 为什么pthread_cond_t要和pthread_mutex_t同时使用 || pthread/Linux多线程编程
为什么线程同步的时候pthread_cond_t要和pthread_mutex_t同时使用 (2009-10-27 11:07:23) 转载▼ 标签: 杂谈 分类: 计算机 举一个例子(http:// ...
- Linux 多线程 - 线程异步与同步机制
Linux 多线程 - 线程异步与同步机制 I. 同步机制 线程间的同步机制主要包括三个: 互斥锁:以排他的方式,防止共享资源被并发访问:互斥锁为二元变量, 状态为0-开锁.1-上锁;开锁必须由上锁的 ...
- Linux下进程线程,Nignx与php-fpm的进程线程方式
1.进程与线程区别 进程是程序执行时的一个实例,即它是程序已经执行到课中程度的数据结构的汇集.从内核的观点看,进程的目的就是担当分配系统资源(CPU时间.内存等)的基本单位. 线程是进程的一个执行流, ...
- Linux查看进程线程个数
1.根据进程号进行查询: # pstree -p 进程号 # top -Hp 进程号 2.根据进程名字进行查询: # pstree -p `ps -e | grep server | awk '{pr ...
- Linux 进程,线程 -- (未完)
系统调用 Linux 将系内核的功能接口制作成系统调用, Linux 有 200 多个系统调用, 系统调用是操作系统的最小功能单元. 一个操作系统,以及基于操作系统的应用,都不能实现超越系统调用的功能 ...
- Linux多线程实践(1) --线程理论
线程概念 在一个程序里的一个执行路线就叫做线程(thread).更准确的定义是:线程是"一个进程内部的控制序列/指令序列"; 一切进程至少有一个执行线程; 进程 VS. 线程 ...
- Linux内核线程kernel thread详解--Linux进程的管理与调度(十)
内核线程 为什么需要内核线程 Linux内核可以看作一个服务进程(管理软硬件资源,响应用户进程的种种合理以及不合理的请求). 内核需要多个执行流并行,为了防止可能的阻塞,支持多线程是必要的. 内核线程 ...
- Linux的进程线程及调度
本文为宋宝华<Linux的进程.线程以及调度>学习笔记. 1 进程概念 1.1 进程与线程的定义 操作系统中的经典定义: 进程:资源分配单位. 线程:调度单位. 操作系统中用PCB(Pro ...
- [转帖]Linux的进程线程及调度
Linux的进程线程及调度 本文为作者原创,转载请注明出处:https://www.cnblogs.com/leisure_chn/p/10393707.html 本文为宋宝华<Linux的进程 ...
随机推荐
- 如何在阿里云上运行SAP UI5应用
本来Jerry觉得这个知识点太简单了完全不值得写成微信公众号文章,但转念一想,可能网络上有一些刚刚初学UI5的朋友们可能会问到,所以还是写了. 今天一个成都同事问我这个问题,因为SAP WebIDE可 ...
- MYSQL查询练习 1
-- 查询练习 1------------ CREATE TABLE stu ( sid ), sname ), age INT, gender ) ); , 'male'); , 'female') ...
- windows系统编辑过的脚本文件,在linxu上执行报错 /bin/sh^M: bad interpreter: No such file or directory
如题! 现象: 当时的场景是这样的:我在IDEA中编辑了项目中的脚本sh,然后利用maven打成zip包.把zip包上传到linux服务器解压运行. 当在linux服务器上运行该sh脚本文件时,提示错 ...
- Qt 4.8.5 + MinGW32 + Qt creater 安装
Qt 4.8.5 + MinGW32 + Qt creater 安装 下载文件 文件版本 Qt 4.8.5 MinGW 0.4.4 Qt Creator 2.8或2.8.1 gdb-7.4-MinGW ...
- idou老师教你学Istio 07: 如何用istio实现请求超时管理
在前面的文章中,大家都已经熟悉了Istio的故障注入和流量迁移.这两个方面的功能都是Istio流量治理的一部分.今天将继续带大家了解Istio的另一项功能,关于请求超时的管理. 首先我们可以通过一个简 ...
- 说一下 runnable 和 callable 有什么区别?(未完成)
说一下 runnable 和 callable 有什么区别?(未完成)
- ndk学习之c++语言基础复习----面向对象编程
关于面向对象编程对于一个java程序员那是再熟悉不过了,不过对于C++而言相对java还是有很多不同点的,所以全面复习一下. 类 C++ 在 C 语言的基础上增加了面向对象编程,C++ 支持面向对象程 ...
- Pytest命令行执行测试
Pytest命令行执行测试 from collections import namedtuple Task = namedtuple('Task', ['summary','owner','done' ...
- 微信小程序将图片数据流添加到image标签中
原文:https://blog.csdn.net/OliveLao/article/details/78136121 ---------------------------------------- ...
- 如何python循环中删除字典元素
//下面这行就是在循环中遍历删除字典元素的方法! for i in list(dictheme2.keys()): if dictheme2[i]<self.countFortheme: dic ...