0%

进程与线程

阅读更多

1 进程

1.1 进程的定义

进程:每个进程都有独立的代码和数据空间(进程上下文),进程间的切换会有较大的开销,一个进程包含1–n个线程。(进程是资源分配的最⼩小单位)

像hello这样的程序在现代操作系统上运行时,操作系统会提供一种假象,就像系统上只有这个程序在运行,程序看上去独占使用处理器、主存和I/O设备。处理器看上去像不间断地一条接一条地执行程序中的命令,即该程序代码和数据是系统内存中唯一的对象。这些假象是通过进程的概念来实现的,进程是计算机科学中最重要和最成功的的概念之一

进程是操作系统对一个正在运行的程序的一种抽象,在一个系统上可以同时运行多个进程,而每个进程都好像独占地使用硬件。而并发运行,则是说一个进程的指令和另一个进程的指令是交错执行的。在大多数系统中,需要运行的进程是多于可以运行它们的CPU个数的

传统系统在一个时刻只能执行一个程序,而先进的多核处理器同时能够执行多个程序。无论在单核还是多核系统中,一个CPU看上去都像是在并发地执行多个进程,这是通过处理器在进程间切换来实现的。操作系统实现这种交错执行的机制称为上下文切换

操作系统保持跟踪进程运行所需的所有状态信息。这种状态,就是上下文,包括许多信息,比如PC和寄存器文件的当前值,以及主存的内容。当操作系统决定要把控制权从当前进程转移到某个新进程时,就会进行上下文切换,即保存当前进程的上下文、恢复进程的上下文,然后将控制权传递到新进程,新进程就会从它上次停止的地方开始

从一个进程到另一个进程的转换时由操作系统内核(kernel)管理的,内核是操作系统代码常驻主存的部分。当应用程序需要操作系统的某些操作时,比如读文件,它就执行一条特殊的系统调用(system call)指令,将控制权传递给内核,然后内核执行被请求的操作并返回给应用程序

注意,内核不是一个独立的进程,相反,它是系统管理全部进程所用代码和数据结构的集合

fig1

1.2 进程运行时内存结构

fig2

进程运行时内存结构自下而上分别是

  1. 代码段
  2. 数据段
  3. BSS段(图中未标出)
  4. 共享内存
  5. 内核

1.2.1 BSS段与Data段的区别

BSS是Block Started by Symbol的缩写,BSS是Unix链接器产生的未初始化数据段。其他的段分别是包含程序代码的Text段和包含已初始化数据的Data段BSS段的变量只有名称和大小却没有值。此名后来被许多文件格式使用,包括PE。以符号开始的块指的是编译器处理未初始化数据的地方。BSS节不包含任何数据,只是简单的维护开始和结束的地址,以便内存区能在运行时被有效地清零。BSS节在应用程序的二进制映象文件中并不存在。

在采用段式内存管理的架构中(比如intel的80x86系统),BSS段通常是指用来存放程序中未初始化的全局变量的一块内存区域,一般在初始化时BSS段部分将会清零。BSS段属于静态内存分配,即程序一开始就将其清零了。比如,在C语言之类的程序编译完成之后,已初始化的全局变量保存在Data段中,未初始化的全局变量保存在BSS段中。Text和Data段都在可执行文件中(在嵌入式系统里一般是固化在镜像文件中),由系统从可执行文件中加载;而BSS段不在可执行文件中,由系统初始化。

1.3 进程之间通信

每个进程各自有不同的用户地址空间,任何一个进程的全局变量在另一个进程中都是看不到的。所以进程之间如果要交换数据就必须通过内核。

在内核中开辟一块缓冲区,进程1把数据从用户空间拷到内核缓冲区,进程2再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信(IPC,InterProcess Communication)

进程间通信的本质:要让不同的进程看到同一份资源

以下为几种进程间通信方式,IPC(Inter-Process Communication)

  1. 进程通信-管道
  2. 进程通信-消息队列
  3. 进程通信-信号量
  4. 进程通信-信号
  5. 进程通信-共享内存
  6. 进程通信-套接字

2 线程

2.1 定义

线程:同一类线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器(PC),线程切换开销⼩小。(线程是cpu调度的最⼩小单位)

通常我们认为一个进程只有单一的控制流,但在现代系统中,一个进程实际上可以由多个称为线程的执行单元组成,每个线程都运行在进程的上下文中,并共享同样的代码和全局数据

由于网络服务器中对并行处理的需求,线程成为越来越重要的编程模型,因为多线程之间比多进程之间更容易共享数据(进程间也能共享数据),也因为线程一般来说比进程更高效

下图是Java中线程的状态转移图

fig3

2.2 线程私有数据

线程共享的环境包括:进程代码段、进程的公有数据(利用这些共享的数据,线程很容易的实现相互之间的通讯)、进程打开的文件描述符、信号的处理器、进程的当前目录和进程用户ID与进程组ID。进程拥有这许多共性的同时,还拥有自己的个性。有了这些个性,线程才能实现并发性。这些个性包括:

  1. 线程ID:每个线程都有自己的线程ID,这个ID在本进程中是唯一的。进程用此来标识线程
  2. 寄存器组的值:由于线程间是并发运行的,每个线程有自己不同的运行线索,当从一个线程切换到另一个线程上时,必须将原有的线程的寄存器集合的状态保存,以便将来该线程在被重新切换到时能得以恢复
  3. 线程的栈:栈是保证线程独立运行所必须的。线程函数可以调用函数,而被调用函数中又是可以层层嵌套的,所以线程必须拥有自己的函数堆栈,使得函数调用可以正常执行,不受其他线程的影响
  4. 错误返回码:由于同一个进程中有很多个线程在同时运行,可能某个线程进行系统调用后设置了errno值,而在该线程还没有处理这个错误,另外一个线程就在此时被调度器投入运行,这样错误值就有可能被修改。所以,不同的线程应该拥有自己的错误返回码变量
  5. 线程的信号屏蔽码:由于每个线程所感兴趣的信号不同,所以线程的信号屏蔽码应该由线程自己管理。但所有的线程都共享同样的信号处理器
  6. 线程的优先级:由于线程需要像进程那样能够被调度,那么就必须要有可供调度使用的参数,这个参数就是线程的优先级

因此线程共享的内容包括

  1. 代码段
  2. 数据段
  3. 共享内存

3 进程切换与线程切换

进程切换分两步:

  1. 切换页目录以使用新的地址空间
  2. 切换内核栈和硬件上下文

对于Linux来说,线程和进程的最大区别就在于地址空间,对于线程切换,第1步是不需要做的,第2是进程和线程切换都要做的

切换的性能消耗:

  1. 线程上下文切换和进程上下问切换最主要的区别是线程的切换虚拟内存空间依然是相同的,但是进程切换是不同的。这两种上下文切换的处理都是通过操作系统内核来完成的。内核的这种切换过程伴随的最显著的性能损耗是将寄存器中的内容切换出
  2. 另外一个隐藏的损耗是上下文的切换会扰乱处理器的缓存机制。简单的说,一旦去切换上下文,处理器中所有已经缓存的内存地址一瞬间都作废了。还有一个显著的区别是当你改变虚拟内存空间的时候,处理的页表缓冲(processor’s Translation Lookaside Buffer (TLB))或者相当的神马东西会被全部刷新,这将导致内存的访问在一段时间内相当的低效。但是在线程的切换中,不会出现这个问题

4 参考