并发编程--多进程编程

概述

在现代操作系统当中,我们可以很方便的编写出多进程程序。在多进程程序中,如果多个进程之间需要协作完成任务,那么进程间通信的方式就是需要重点考虑的事项之一。这种通信通常被叫做IPC(Inter-Process Communication),不同版本的Unix及其衍生操作系统所支持的IPC方法都不尽相同,下面讨论的IPC时只针对Linux系统。

Linux操作系统可以使用的IPC方法有多种,从处理机制角度看,他们可以分为三类:

  1. 基于通信的IPC方法
  2. 基于信号的IPC方法
  3. 基于同步的IPC方法

基于通信的IPC方法分为:

  • 以数据传输为手段的IPC方法
  1. 管道(pipe),传递字节流
  2. 消息队列(message queue),传递结构化的消息对象
  • 以共享内存为手段的IPC方法
  1. 共享内存区(share memory),他是最快的一种IPC方法

基于信号的IPC方法:

  • 就是我们常常说的操作系统的信号(signal)机制,他是唯一的异步IPC方法

基于同步的IPC方法:

  • 最重要的就是信号量(semaphore)

进程

进程的定义

进程是所有Unix及其衍生操作系统的根本,因为所有的程序都在进程中被执行。通常我们把一个程序的执行称之为一个进程。反过来讲,进程用来描述程序的执行过程。程序和进程分别描述了一个程序的静态形式和动态特征。除此之外。进程是操作系统进行资源分配的基本单位。

进程的衍生

进程使用fork(一个系统调用函数)可以创建若干个新的进程,其中前者称为后者的父进程,后者称为前者的子进程。每个子进程都是源自它父进程的一个副本,它会获得父进程的数据段、堆、栈的副本,并于父进程共享代码段。每一个副本都是独立的,子进程对于它副本的修改对其父进程和兄弟进程都是不可见的,反之亦然。全盘复制父进程的数据是一种相当低效的做法,Linux操作系统内核使用写时复制COW(Copy on wite)等技术来提高创建进程的效率。

Unix操作系统中每一个进程都有父进程,所有的进程共同组成一个树状结构,内核启动程序作为进程树的根,负责系统的初始化操作。如果一个进程先于它的子进程结束,那么这些子进程将会被内核启动程序“收养”,成为它的直接子进程。

进程的标识

为了管理进程,内核必须对每一个进程的属性行为进行详细的记录,包括进程的优先级、状态、虚拟地址范围、各种访问权限等。更具体的说这些信息都被记录在进程描述符中,进程描述符并不是一个简单的符号,而是一个非常复杂的数据结构。保存在进程描述符中的进程ID(常称为PID)是进程在操作系统中唯一标识,进程号为1的进程就是内核启动进程。

进程的状态

在Linux操作系统中,每个进程每时每刻都是友状态的,可能的状态共有如下6种:

  1. 可运行状态(TASK_RUNNING,R),如果一个进程处在此状态,那么说明它立刻要或者正在cpu上运行。不过运行的时机是不确定的,这由进程调度器来确定。
  2. 可中断的睡眠状态(TASK_INTERRUPTIBLE,S),当进程正在等待某个事件(如: 网络连接、信号量)到来时,会进入此状态。这样的进程会被放入对应事件的等待队列中,当事件发生时,等待队列中的进程会被唤醒。
  3. 不可中断的睡眠状态(TASK_UNINTERRUPITEBLE,D),此种状态的进程不会对任何信号做出响应,更确切的说,发送给此状态进程的信号直到它从此状态转出才会被传递过去。处于此状态的进程通常是在等待一个特殊的事件,比如等待同步的I/O操作完成。
  4. 暂停状态或跟踪状态(TASK_STOPPED或TASK_TRACED, T),向进程发送SIGSTOP信号,就会使该进程进入暂停状态,除非该进程正处于不可中断的状态,向正处于暂停状态的进程发送SIGCOUNT信号,会使进程转向可运行状态。处于该状态的进程会暂停,并等待另一个进程(跟踪他的那个进程)对它进行操作。例如我们使用调试工具GDB在某个程序中设置断点,程序运行到断点处会停下来,这时就处于跟踪状态,跟踪状态和暂停状态非常相似,但是,向处于跟踪状态的进程发送SIGCOUNT信号并不能使用使它恢复,只有当调试进程进行了相应的系统调用或者退出后,他才能够恢复。
  5. 僵尸状态(TASK_DEAD-EXIT_ZOMBIE,Z),处于此状态的进程即将结束运行,该进程占用的绝大多数资源也都已经被回收,不过还有一些信息未删除,比如状态码以及一些统计信息。之所以保留这些信息,主要是考虑到该进程的父进程可能需要它们。由于此时的进程主体已经被删除而只留下一个空壳,故此状态称为僵尸状态。
  6. 退出状态(TASK_DEAD_EXIT_DEAD,X),在进程退出的过程中,有可能连退出码和统计信息都不需要保留,,造成这种状态的原因可能是显示地让该进程的父进程忽略掉SIGCHILD信号(当一个进程消亡的时候,内核会给其父进程发送SIGCHID信号告知此情况),也有可能该进程已经被分离(即让子进程和父进程分别独立运行),分离后的子进程将不会再使用和执行与父进程共享的代码段中的指令,而是加载并运行一个全新的程序,。在这些情况下,该进程在退出的时候就不会转入僵尸状态,而是直接转入退出状态。处于退出状态的进程会立即干净利落地结束掉,它占用的系统资源也会被操作系统自动回收。

进程在其生命周期内可能会产生一系列的状态变化。简单的说,进程的状态只会在可运行状态和非可运行状态之间转换,下图展示了一般情况下的进程状态切换。

        并发编程--多进程编程_第1张图片

 进程的空间

用户进程(或者说程序的执行实例)总会生存在用户空间中,他们可以做很多事情,但是却不能与其所在计算机硬件进行交互。内核可以与硬件交互,但是它却生存在内核空间中。用户进程无法直接访问内核空间。用户空间和内核空间都是操作系统在内存上划分的一个范围,他们共同瓜分了操作系统能够支配的内存区域。

内存区域中的每一个单元都是有地址的,用指针来标识和定位,通过指针来寻找内存单元的操作也称为内存寻址。指针是一个二进制正整数,位数由操作系统决定(32,64)。这里所说的地址物理内存中的真实地址,而是虚拟地址,而由虚拟地址来标识的内存区域又称为虚拟地址空间,也称虚拟内存。内核会为每一个用户进程分配虚拟内存,进程的虚拟内存几乎是彼此独立、互不干扰的,这是由于它们基本上被映射到了不同的物理内存上。内核会把进程的虚拟内存划分为若干页,而物理内存单元的划分由cpu负责。一个物理内存单元被称为一个页框,不同进程的大多数页都会与不同的页框对应,当然页框共享也是允许的,这是共享内存区(一种IPC方法)的基础。

系统调用

用户进程无法直接访问内核空间,也无法随意指示内核去做它能做的一些事。但是为了使用户进程能够使用操作系统更底层的功能,内核会暴露一些接口以供他们使用,这些接口是用户进程使用内核功能(包括操纵计算机硬件)的唯一手段,也是用户空间和内核空间的一座桥梁。用户使用这些接口的行为称为系统调用。

说到系统调用,就不得不提及另一对概念--内核态用户态。为了保证操作系统的稳定和安全,内核依据由cpu提供的、可以让进程驻留的特权级别建立了两种状态。当用户进程发送一个系统调用的时候,内核会把cpu从用户态切换到内核态,让cpu执行对应的内核函数,当执行完成后会将cpu状态切换为用户态,并把执行结果返回给用户进程。

进程切换和调度

与其他操作系统一样,Linux操作系统也可以凭借cpu快速的在多个进程间进行切换,这也称为进程间的上下文切换。如此会产生多个进程同时运行的假象,而每个进程都会认为自己独占了cpu,这就是多任务操作系统这个称谓的由来。不过,切换速度如何,在同一时刻正在运行的进程仅会有一个。

进程切换由内核完成。切换cpu正在运行的进程是需要付出代价的。例如,内核此时要换下正在上运行的A进程,运行B进程,在换下A之前要保存A的运行状态,同时将B进程的相关信息恢复到之前B被换下时候的运行状态。为了使各个生存着的进程都有被运行的机会,内核还要考虑下次切换时运行哪个程序、何时进行切换、被换下的进程何时被换上等,解决这类类似问题的方案和任务统称为进程调度。进程切换和进程调度是多个程序并发执行的基础


同步

内核对进程的切换和调度使得多个进程可以有条不紊地并发运行。在很多时候,多个进程之间需要相互配合共同完成一个任务,这就需要IPC地支持。

后续再更...

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

你可能感兴趣的:(并发编程,操作系统)