golang并发编程-01多进程编程-01概述
@
1. 基本概念
1.1 串行程序与并发程序
- 串行程序:特指一个只能被顺序执行的指令列表
- 并发程序:被并发的执行的两个或两个以上的串行程序的统称。
- 多元处理:指计算机中的多个CPU共用一个存储器(即内存),并且在同一时刻可能会有数个串行程序分别运行在不同的CPU之上。
1.2 并发程序与并行程序
- 并行程序:被设计成可以在并行的硬件上执行的并发程序。
- 即
并发程序
包含并行程序
1.3 并发程序与并发系统
- 并发程序:一个内聚的软件单元
- 并发系统:是松耦合的
1.4 并发程序内部的交互
- 并发程序内部的通讯
- 通讯缓存
2. 常用IPC方法
IPC(Interprocess Communication)
2.1 基于通讯的IPC方法
1)以数据传送为手段的IPC方法
- 管道(Pipe):用来传送字节流
- 消息队列(Message Queue) :用来传送结构化的消息对象
2)以共享内存为手段的IPC方法:
- 共享内存区(Shared Memory)
2.2 基于信号的IPC方法
即操作系统的信号(Signal)机制。
它是唯一的一种异步的IPC方。
2.3 基于同步的IPC方法
同步的方法后边有专门的章节说,此处仅介绍一些概念:
- 竞态条件(race condition)
当几个进程同时对同一个资源进行访问的时候,就很可能会造成互相的干扰。这种互相干扰通常被称为竞态条件。 - 原子操作(atomic operation)
我们把执行过程中不能被中断的操作称为原子操作。 - 临界区(critical section)
而把只能被串行化的访问或执行的某个资源或某段代码称为临界区。
原子操作和临界区的区别:
- 原子操作是不能被中断的
- 临界区只要保证一个访问者在临界区中的时候其他访问者不会被放进来就可以了。
- 互斥(mutual exclusion,简称mutex)
保证只有一个进程或线程在临界区之内的这种做法。
3. 进程
程序和进程分别描述了一个程序的静态形式和动态特征
3.1 进程的衍生
- 一个进程可以使用系统调用fork创建若干个新的进程。
前者被称为后者的父进程
后者被称为前者的子进程
- 每个子进程都是源自它的父进程的一个副本
子进程会获得父进程的数据段、堆和栈的副本,并与父进程共享代码段。 - 子进程对属于它的副本的修改对其父进程和兄弟进程(同父进程)都是不可见的
- 每一个进程都有父进程。所有的进程共同组成了一个树状结构
内核启动进程作为进程树的根并负责系统的初始化操作。它是所有进程的祖先。然而,这个进程也是有父进程的——就是它自己。
如果某一个进程先于它的子进程结束,那么这些子进程将会被内核启动进程“收养”,成为它的直接子进程。
- 写时复制(COW)来提高进程创建的效率
全盘复制父进程的数据是相当低效的一种做法。
Linux操作系统内核(以下简称内核)使用写时复制(Copy On Write,常被简称为COW)等技术来提高进程创建的效率。
当然,刚被创建的子进程也可以通过系统调用exec把一个新的程序加载到自己的内存中(而原先在其内存中的数据段、堆、栈以及代码段就会被替换掉)。
在这之后,子进程执行的就会是那个刚刚被加载进来的新程序。
3.2 进程的标识
go查看当前进程的ID
pid := os.Getpid()
ppid := os.Getppid()
3.3 进程的状态
- 可运行状态(TASK_RUNNING,简称为R)
如果一个进程处在该状态,那么说明它将要、立刻或正在CPU上运行。运行的时机是不确定的。这会由进程调度器来决定。
可中断的睡眠状态(TASK_INTERRUPTIBLE,简称为S):当进程正在等待某个事件(比如网络连接或信号灯)的发生时会进入此状态。这样的进程会被放入对应事件的等待队列中。当事件发生时,对应的等待队列中的一个或多个进程就会被唤醒。
- 不可中断的睡眠状态(TASK_UNINTERRUPTIBLE,简称为D)
此种状态与可中断的睡眠状态的唯一区别就是它是不可被打断的。这意味着处在此种状态的进程不会对任何信号作出响应。更确切地讲,发送给处于不可中断状态的进程的信号直到该进程从此状态转出才会被传递过去。进程处于此种状态通常是由于在等待一个特殊的事件。比如在等待同步的I/O操作(磁盘I/O等)的完成。I/O是Input/Output的缩写,在这里可以理解为对输入输出信息的处理。
- 暂停状态或跟踪状态(TASK_STOPPED或TASK_TRACED,简称为T)
向进程发送SIGSTOP信号就会使该进程转入暂停状态,除非该进程正处于不可中断的睡眠状态。向正处于暂停状态的进程发送SIGCONT信号会使该进程转向可运行状态。处于被跟踪状态的进程会暂停并等待跟踪它的进程对它进行操作。例如,我们使用调试工具GDB在某个程序中设置一个断点,而后对应的进程在运行过程中会在断点处停下来并等待被操作。这时,此进程就处于跟踪状态。跟踪状态与暂停状态非常类似。但是,向处于跟踪状态的进程发送SIGCONT信号并不能使它被恢复。只有当调试进程进行了相应的系统调用或者退出之后,它才能够被恢复。
- 僵尸状态(TASK_DEAD-EXIT_ZOMBIE,简称为Z)
处于此状态的进程即将要结束。该进程占用的绝大多数资源也都已经被回收。不过还有一些信息未被删除,比如退出码以及一些统计信息。保留这些信息是考虑到该进程的父进程可能需要它们。由于此时的进程主体已经被删除而只留下了一个空壳,故此状态常被称为僵尸状态。
- 退出状态(TASK_DEAD-EXIT_DEAD,简称为X)
在进程退出的过程中,有可能连退出码和统计信息都不需要被保留。造成这种情况的原因可能是显式地让该进程的父进程忽略掉SIGCHLD信号(当一个进程消亡的时候,内核会给其父进程发送一个SIGCHLD信号以告知此情况),也可能是该进程已经被分离。分离的含义是让子进程和父进程分别独立运行。分离后的子程序将不会再使用和执行与父进程共享的代码段中的指令,而是加载并运行一个全新的程序(我们讲“进程的衍生”的时候提到过)。在这些情况下,该进程在退出的时候就不会转入僵尸状态,而会直接转入退出状态。处于退出状态的进程会立即被干净利落地结束掉。它所占用的系统资源也会被操作系统自动回收。
各状态间的转化:
3.4 进程的空间
- 用户进程生存于用户空间中
它无法直接访问内核空间,不能与硬件进行交互 - 内核生存在内核空间,可以与硬件交互
这两个空间指的都是操作系统在内存上划分出的一个范围。它们共同瓜分了操作系统能够支配的内存区域。
- 内存区域中的每一个单元都是有地址的。这些地址是由指针来标识和定位的。
在32位计算机中可以有效标识2的32次方个内存单元,而在64位计算机中可以有效标识2的64次方个内存单元,
- 内核会为每个用户进程分配的是虚拟内存而不是物理内存
每个用户进程被分配到的虚拟内存总是在用户空间中的,而内核空间被留给内核专用。
每个用户进程都会认为分配给它的虚拟内存就是整个用户空间。即用户的虚拟内存对于前者来说是不可见的
- 内核会把进程的虚拟内存划分为若干页(page)。
- 物理内存单元的划分由CPU负责。一个物理内存单元被称为一个页框(page frame)
图中的进程A和进程B的
- 大多数页都分别与物理内存中的不同页框相对应。
- 但进程A的页7与进程B的页8共享了同一个页框(即最下面的一个页框)。
- 不论进程A还是进程B都有一些页没有与任何一个页框对应。
这也是有可能的。这也许是由于该页没有数据或者数据还不需要被使用,也许是该页已经被换出至磁盘(swap分区)中。
3.5 系统调用
内核会暴露出一些接口,这些接口是用户进程使用内核功能(包括操纵计算机硬件)的唯一手段。
前文已知:用户进程生存在用户空间中且无法直接操纵计算机的硬件,但是在内核空间中的内核却可以做到。
3.6 内核态和用户态
内核态和用户态定义
为了保证操作系统的稳定和安全,内核依据由CPU提供的、可以让进程驻留的特权级别建立了两个特权状态。它们就是内核态和用户态。
内核态和用户态的切换
- 在大部分时间里CPU都处于用户态。这时CPU只能对用户空间进行访问。
CPU在用户态下运行的用户进程是不能与内核接触的。
- 当用户进程发出一个系统调用的时候,内核会把CPU从用户态切换到内核态。而后会让CPU执行对应的内核函数。
CPU在内核态下是有权限访问内核空间的。这就相当于使用户进程通过系统调用使用到了内核提供的功能。
- 当内核函数被执行完毕后,内核会把CPU从内核态切换回用户态,并把执行结果返回给用户进程。