最近有日子没写博客了,这段时间有点事忙活一阵子,好在已经接近尾声。也该轮到投些时间好好研究下真刀真枪的东西,干些有意义的事。这两天抽时间继 续往下看了看 Linux 内核和 Unix 编程的书,边看边琢磨,想到个关于进程在 fork 子进程或 pthread 出 lwp 时父亲进程的栈段是如何处理的问题,结合 Linux 内核的说明对这个问题有了明确的理解,在此做个笔记。大家也一起研究、分享下~
历史上来说,*nix 里的 C 程序进程由以下几部分组成:
- 正文段。也有叫代码段的。存放着 CPU 执行的机器指令。它一般是共享、只读的。
- 初始化数据段。存放着程序中明确赋初值的变量。
- 非初始化数据段。也叫 bss ,存放着 C 函数之外的变量,内核会初始它为 0 填充。
- 栈段。存放 auto 变量或函数调用传递的信息。包括返回值、实参,调用者上下文(忽然想到,闭包变量会用这个传递么?请教 guru),这些变量会存在一个 stack frame 中。
- 堆段。用于 sbrk (malloc) 等动态内存分配的。
这图说明了这些段的典型的布局。在 x86 CPU 的 Linux 中栈底位于 0xc0000000 开始,该地址以上存储的就是内核代码了(那段线性地址直接映射到物理地址)。当我看到这里的时候,困惑的问题是,在这种段结构中,当我们父进程生成多进程 /线程的子进程时,linux 内核对这个父进程的 stack 段是怎么处理了,来保证每个不同进程/线程中的方法调用时,用 stack 来传递的调用信息不会混乱,怎么保证竞争条件下的正确入/出栈顺序?呵呵,现在看来当时的想法比较可笑了。正确理解如下所述。
*nix 有3种进程创建方式:
- fork。内核为父进程创建副本,即子进程。传统情况下,该子进程将获得父进程完整复制,包括数据段(初始化和bss 段)和堆栈段。但是,由于 fork 之后经常跟着就是 execve 系统调用,因此现在的 *nix 会使用 COW(写时复制) 方式来 fork 子进程,也就是这些区域暂时不复制,并由内核将它们只读,当父/子进程对他们修改时,就将修改的部分保存在本次修改操作的进程地址空间中,通常单位是一 页。内核为了提高性能,如果父子进程在同一颗 CPU 上的话呢(同 CPU 进程队列),会在 TASK_RUNNING 进程链中将子进程放在放在父进程的前边,这样可减少不必要的 COW 开销。此外还有一些其它属性也将由子进程继承,如实际、有效用户/组 ID,会话 ID,工作目录,环境,连接的共享存储段,资源限制等等。
- lwp。轻量进程允许父子进程共享内核的在部分数据,页表(可共享全局数据),文件描述符表等。pthead 就是通过 lwp 实现的。
- vfork。apue2中将 vfork 单提了来,个人觉得实际和 fork 是一要的,只不过是简版的。通过它创建的子进程能够共享父进程内存地址空间,但为了避免父子混乱,子进程暂时阻塞了父进程执行,直到子进程退出父进程再在此基础上继续执行。
实际上看到这里,已经能够很清楚的解释我上面的疑问了,呵呵。fork 方式会独立出父子进程,vfork 会顺序执行。那么 fork/lwp 的具体细节内核是怎么做到的呢,继续挖。
Linux 通过 clone 系统调用来创建 lwp,而 clone、fork、vfork 都是由 do_fork 内核函数来统一处理完成的,而 do_fork 又是由 copy_process 函数来完成功能的。再看一眼上面我最初想到的问题,已经知道 fork 会出来两个独立的用户进程空间,因此父子进程的栈段肯定不重复。但 lwp 怎么保证栈段不重复的呢,就是通过 clone 系统调用的 child_stack、tls 和 flags 参数来控制的,详细说明看文档和内核源码吧。Linux 中子进程的具体创建步骤说明可见《Understanding The Linux Kernel》 3th 的 P123 很详尽有看起来也有力道,呵呵。
现在关于这个 tls 还有没理解的地方,在 GDT 中共有 3 个 TLS ,段选择符 0x33 - 0x43(那也就是说只允许最多 3 个线程局部数据段),这个 clone 的 tls 参数是传递的什么值,是 GDT 中 tls 的段描述符地址么?
研究的还很 top,很多知识点很模糊 ,且待我继续深入~ 期待哪位牛人能够赐教一二。
// 2009.04.21 21:50 添加
作者:lzy.je
出处:http://lzy.javaeye.com
本文版权归作者所有,只允许以摘要和完整全文两种形式转载,不允许对文字进行裁剪。未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。