大碰撞!当Linux多线程遭遇Linux多进程


作者简介:

    廖威雄,目前就职于珠海全志科技股份有限公司从事linux嵌入式系统(Tina Linux)的开发,主要负责文件系统和存储的开发和维护,兼顾linux测试系统的设计和持续集成的维护。

    拆书帮珠海百岛分舵的组织长老,二级拆书家,热爱学习,热爱分享。

## 背景 

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

(https://book.douban.com/subject/1788421/)**多进程或多线程**,而是**多进程和多线程**,往往会在写一个大型应用时才会用到多进程多线程的模型。

640?wx_fmt=png

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

[GMPY@10:02 share]$./signal-safe 

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

## 线程和fork

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

640?wx_fmt=png

640?wx_fmt=png

执行效果如下:

[GMPY@10:37 share]$./test 

 我们发现,子进程挂了,在打印了```children burn```后,没有了下文,因为在**子进程获取锁的时候,死锁了!**```sub_pthread```线程不是有释放锁么?父进程都能在线程释放后获取到锁,为什么子线程就获取不到锁呢?    子进程通过继承整个地址空间的副本,还从父进程那儿继承了每个互斥量、读写锁和条件变量的状态。    如果父进程包含一个以上的线程,子进程在fork返回以后,如果紧接着不是马上调用exec的话,就需要清理锁状态。    在子进程内部,只存在一个线程,它是由父进程中调用fork的线程的副本构成的。    如果父进程中的线程占有锁,子进程将同样占有这些锁。    问题是子进程并不包含占有锁的线程的副本,所以子进程没有办法知道它占有了哪些锁、需要释放哪些锁。    ......    在多线程的进程中,为了避免不一致状态的问题,POSIX.1声明,在fork返回和子进程调用其中一个exec函数之间,    子进程只能调用异步信号安全的函数。这就限制了在调用exec之前子进程能做什么,但不涉及子进程中锁状态的问题。[可以参考文章](https://www.cnblogs.com/gmpy/p/10265284.html)

640?wx_fmt=png

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

640?wx_fmt=png

 这时候,父进程生娃娃了,这个新生娃娃**集成了父进程的绝大部分资源,包括了锁的状态**,然而,子进程并没有共生出小弟,就是说**子进程并没同时创建出小弟线程**,他就是一个坐拥金山的孤家寡人。```sub_pthread```占用了,```fork```生出来的子进程锁的状态跟父进程一样一样的,锁上了!被人占有了!因此子进程再获取锁就死锁了。```printf```,其内部实现是有获取锁的,因此在fork出来的子进程执行exec之前,**甚至都不能调用printf**

640?wx_fmt=png

 上面的代码主要做了两件事:1. 创建线程,循环printf打印字符'\r'2. 循环创建进程,在子进程中调用printf打印字串```fork```套了一层循环。执行结果怎么样呢?

root@TinaLinux:/mnt/UDISK# demo-c 

 结果在第3次```fork```循环的时候陷入了死锁,子进程不打印不退出,导致父进程```wait```一直阻塞。*上面的结果在全志嵌入式Tina Linux平台验证,比较有意思的是,同样的代码在PC上却很难复现,可能是C库的差异引起的***在fork的子进程到exec之间,只能调用异步信号安全的函数**,这异步信号安全的函数就是认证过不会造成死锁的!

man 7 signal

 检索关键字```Async-signal-safe functions```## 内核原理分析```task_struct```表示一个进程/线程,嗯,换句话说,**不管是进程还是线程,在Linux内核中都是用```task_struct```的结构体表示**[《线程调度为什么比进程调度更少开销?》](https://www.cnblogs.com/gmpy/p/10265284.html),这里不累述。```pthread_create```创建小弟线程时,内核实际上是copy父进程的```task_struct```,创建小弟线程的```task_struct```,且让小弟```task_struct```与父进程```task_struct```共享同一套资源。

640?wx_fmt=png

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

640?wx_fmt=png

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

640?wx_fmt=png

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

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

640?wx_fmt=png

 一句话概述,就是 **通过原子变量标识和记录锁状态**,用户空间也是一样的做法。```fork```流程,我们用这样一张图描述进程/线程与锁的关系:

640?wx_fmt=png

(完)

查看我们精华技术文章请移步:

Linux阅码场原创精华文章汇总

更多精彩,尽在"Linux阅码场",扫描下方二维码关注

640?wx_fmt=png

感谢您的耐心阅读,请随手转发一下或者点个“在看”吧~

发布了124 篇原创文章 · 获赞 334 · 访问量 72万+

猜你喜欢

转载自blog.csdn.net/juS3Ve/article/details/100070007