进程、线程及其内存模型
这篇文章是一个大杂烩,内容来自网络和《深入理解操作系统》原书第二版 过后应该回头梳理并做一些深入的分析
进程提供给应用程序关键的抽象:
- 一个独立的逻辑控制流,它提供一个假象,好像我们的程序独占地使用处理器
- 一个私有的地址空间,它提供一个假象,好像我们的程序独占地使用存储器系统
从程序员的角度,我们可以认为进程总是处于下面三种状态之一:
- 运行。进程要么在CPU上执行,要么在等待被执行且最终会被内核调度
- 停止。进程的执行被挂起(suspend),且不会被调度。当收到SIGSTOP、SIGTSTP、SIDTTIN或者SIGTTOU信号时,进程就停止,比呢且保持停止直到它收到一个SIGCONT信号,在这个时候,进程再次开始运行(信号是一种软件中断的形式,将在后文描述)
- 终止。进程永远的停止了。进程会因为三种原因终止
- 收到一个信号,该信号的默认行为是终止进程;
- 从主程序返回;
- 调用exit函数。
创建子进程
- 子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份拷贝,包括文本、数据和bss段、堆以及用户栈。
- 还获得与父进程任何打开文件描述符相同的拷贝。
- 父进程和新创建的子进程之间最大的区别在于它们有不同PID。
- fork函数调用一次,返回两次,父进程中,fork返回子进程的PID,子进程中fork返回0。
进程地址空间
这篇文章总结的非常好,原文转发过来。
我们一般都知道,每个程序都能看到一片完整连续的地址空间,这些空间并没有直接关联到物理内存,而是操作系统提供了内存的一种抽象概念,使得每个进程都有一个连续完整的地址空间,在程序的运行过程,再完成虚拟地址到物理地址的转换。我们同样知道,进程的地址空间是分段的,存在所谓的数据段,代码段,bbs段,堆,栈等等。每个段都有特定的作用,仔细看下面这张图,就对进程地址空间中的划分有了清楚的了解了。
- 从0xc000000000到0xFFFFFFFF共1G的大小是内核地址空间(后面再探讨内核地址空间,先重点关注用户地址空间),余下的低地址3G空间则是用户地址空间。
- Code VMA: 即程序的代码段,CPU执行的机器指令部分。通常,这一段是可以共享的,即多线程共享进程的代码段。并且,此段是只读的,不能修改。
- Data VMA: 即程序的数据段,包含ELF文件在中的data段和bss段。
- 堆和栈: 这两个大家都十分熟悉了,new或者malloc分配的空间在堆上,需要程序猿维护,若没有主动释放堆上的空间,进程运行结束后会被释放。栈上的是函数栈临时的变量,还有程序的局部变量,自动释放。
- 共享库和mmap内容映射区:位于栈和堆之间,例如程序使用的printf,函数共享库printf.o固定在某个物理内存位置上,让许多进程映射共享。mmap是一个系统函数,可以把磁盘文件的一部分直接映射到内存,这样文件中的位置直接就有对应的内存地址。此处参考后面的第三条。
- 命令行参数: 程序的命令行参数
- 环境变量:类似于Linux下的PATH,HOME等环境变量,子进程会继承父进程的环境变量。
一些容易模糊的认知
- 对于32位的机器来说,虚拟的地址空间大小就是4G,可能实际的物理内存大小才1G到2G,意味着程序可以使用比物理内存更大的空间。
- execve函数在当前进程的上下文中加载并运行一个新的程序,它会覆盖当前进程的空间,但并没有创建一个新进程。新的程序仍然拥有相同的PID,并且继承了调用execve函数时已经打开的所有文件的描述符。
- 一个常见的错误是“假设堆存储器初始化为0”。
换页机制
最简单的伪代码,非常清晰,当程序试图访问线性地址空间上的一个地址位置时,发生以下操作:
虚拟存储器的三种重要能力
- 它将驻村看成是一个存储在磁盘上的地址空间的高速缓存,在主存中只保存活动区域,并根据需要在磁盘和主存之间来回传送数据,通过这种方式,它高效地使用了主存。
- 它为每个进程提供了一致的地址空间,简化了存储器管理。
- 它保护了每个进程的地址空间不被其他进程破坏。
程序的执行过程
execve函数在当前的进程中加载并运行包含在可执行文件a.out文件中的程序,用a.out程序有效地替代了当前程序。加载并运行a.out需要以下几个步骤:
- 删除已存在的用户区域。删除当前进程虚拟地址空间用户部分中已存在的区域结构。
映射私有区域。为新程序的文本、数据、bss和栈区创建新的区域结构。所有这些新的区域都是私有的、写时拷贝的。文本和数据区被映射为a.out文件中的文本和数据区。bss区域是请求二进制零的,映射到匿名文件,其大小包含在a.out中。栈和堆区也是请求二进制零的。初始长度为0。初始化的效果如下图所示
映射共享内存。如果a.out程序与共享对象(或目标)链接,比如标准C库libc.so,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享内存区域内。
- 设置程序计数器(PC)。execve做的最后一件事情就是设置当前进程上下文中的程序计数器,使之指向文本区域的入口点。
下一次调度这个进程时,它将从这个入口点开始执行。Linux将根据需要换入代码和数据页面。
多线程
一个进程空间里同时运行多个线程的程序。每个线程有自己的线程上下文,其中包括
- 唯一的线程ID,TID;
- 栈
- 栈指针
- 程序计数器
- 通用目的寄存器
- 条件码
所有运行在一个进程内的线程共享该进程的整个虚拟地址空间,包括:
- 代码
- 数据
- 堆
- 共享库
- 打开的文件
【注意】:上文的表述有一点是不确定的,这就是线程栈:这些栈被保存在虚拟地址空间的栈区域中,并且通常是被相应的线程独立地访问。这里我们说通常而不是总是,是因为一个线程栈不对其他线程设防,我们可以通过一个指向其他线程栈的指针(比如一个全局的指针变量)来读写其他线程栈的任何部分。
线程与进程的不同
- 线程上下文比进程上下文小很多
- 线程没有严格的父子层次关系,意味一个线程可以杀死任何对等线程。
- 每个对等线程都能读写相同的共享数据。
关于他们的更多的比较请见多进程与多线程的深度比较