operating system

1. 进程的描述

进程是程序的一次执行过程,那么程序的顺序执行有如下要求

  1. 顺序性

    程序按顺序逐步执行。这和你编程一样,编程就是按照你的思路做一件事,思路乱了事就做不成了。

  2. 封闭性

    独占全机资源,所有资源状态由程序完成。这也是必须的,如果进程 A 读文件读到一半,过一会继续读发现数据状态发生了改变,不是我要的数据,自然就出错了,所以所有状态都要由进程 A 来改变,这就是封闭性。

  3. 可再现性

    程序的执行结果不变,且与计算机的执行速度、方式(持续或走停)无关。

在多道批处理、分时、实时系统等操作系统中,由于存在多个运行的程序,使得程序的执行具有并发性、共享性和异步性等特征。在程序并发的同时,导致程序出现如下特征

  1. 间断性

    多个程序相互制约(如输入程序与计算程序的先后顺序)

  2. 失去封闭性

    多个程序共享资源(资源状态由多个程序改变)

  3. 不可再现性

    由于状态的随意改变,导致有些程序找不到北。

为了使程序在并发、共享和异步的环境下能正常运行,必须专门设置一个控制数据区,为程序保留运行的现场(处理机状态、断点 IP)及控制管理信息(进程标识、处理机状态、进程调度、进程控制等信息),并进行有效的隔离,这样程序又恢复了顺序性、封闭性和可再现性。

1.1 进程的定义与特征

进程是程序的一次运行。

进程是一个程序及其数据在处理机上顺序执行时所发生的活动。

进程是进程实体(包括程序段、数据和 PCB)的运行过程,是系统进行资源分配和调度的一个独立单位(正如人是社会资源分配和调度的独立单位一样)

  1. 进程的特征

    结构性:由程序段、数据段和进程控制块组成。

    动态性:进行可以被动态地创建、执行和撤销。

    并发性:在一个时间段内有多个进程运行。

    独立性:是独立运行和获得资源的基本单位。

    异步性:异步执行。

  2. 进程和程序的区别

    进程是动态的,程序是静态的(是指令的集合)。

    一个程序可以包含多个进程。

    进程可以描述并发活动,程序则并不明显。

    进程执行需要处理机,程序存储需要介质。

    进程有生命周期,程序是永存的。

  3. 进程的结构

    进程由纯代码段、数据和进程控制块组成。

    代码段:描述进程所完成的功能。

    数据集合:程序运行时所需要完成的数据区。

    进程控制块(PCB):既能标识进程的存在,又能刻画出进程瞬间特征的数据结构。

    linux 系统可以通过 cat /proc/pid/maps 查看进程虚存空间布局。

1.2 进程的基本状态

进程有三种基本状态。

  1. 就绪状态

    进程获得了除处理机 CPU 之外的所有资源(多个就绪进程排成就绪队列)。

  2. 执行状态

    进程的程序正在处理机上执行(单 CPU 中只有一个进程处于该状态)。

  3. 阻塞状态

    因发生某事件(如请求 I/O,申请缓存空间等)而暂停执行的状态(也称为睡眠状态或等待状态)。

处于执行状态的进程,可以主动用阻塞原语(block)将其变为阻塞状态。

处于阻塞状态的进程,可以用唤醒(wakeup)原语将其变为就绪状态。

进程状态切换

除了上面三种基本状态,进程还有一些其他状态

1.3 挂起状态

挂起状态是一种静止状态,既不能马上投入运行的状态,包括静止就绪状态(Readys)和静止阻塞状态(Blockeds)。

处于挂起状态的进程可以存放到外存保留,而且可以回收这些挂起状态进程的内存、设备等部分资源。(反正挂着不用)。

设置挂起状态的原因有如下几个

  1. 终端用户的需要:(调试)
  2. 父进程的需求:(子进程同步)
  3. 负荷调节的需要:(减轻负荷)
  4. 操作系统的需要:(检查资源的使用情况)

处于活动状态的进程(Readya,Blockeda)可以用(Suspend)原语将其变为挂起状态(Readys,Blockeds)。

处于挂起状态(静止状态)的进程,可以用激活(Active)原语将其变为活动状态。

进程状态切换

1.4 进程控制块 PCB

由于我们需要让进程在共享世界中还能拥有顺序性、封闭性和可再现性,我们需要用一种数据结构存储进程状态信息,这种结构称为进程控制块(PCB),具体作用如下

  1. 描述进程的变化过程
  2. 记录进程的外部特征
  3. 记录进程与其他进程的联系
  4. 是进程存在的唯一标志

操作系统通过 PCB 控制和管理进程,没有身份证的进程在计算机世界是活不下去的。

PCB 的具体内容有很多,包括

  1. 进程标识符信息

    内部标识符信息:进程唯一序号(进程的身份证号码)

    外部标识符信息:由创建者提供的进程名字(多个进程的名字可以重复)

  2. 处理机状态信息

    如果进程 A 没有运行完,处理机就被进程 B 抢走了,操作系统会在进程切换之前,将进程 A 的处理机状态信息存储到 PCB 中。包括通用寄存器、指令计数器、程序状态字 PSW、用户堆栈指针等信息。

  3. 进程调度信息

    进程的状态(就绪、阻塞、执行)

    进程优先级(优先级高的进程更有机会先用 CPU)

    进程调度所需的其他信息(进程已用 CPU 时间,为调度算法准备的)

    事件(阻塞原因)

  4. 进程控制信息

    程序和数据的地址,以至于下次程序重新拥有 CPU 之后可以找到执行的起点。

    进程同步和通信机制(如消息队列指针等)

    资源清单

    链接指针(与其他 PCB 形成一个队列)

  5. 进程其他信息

    父进程:由系统或用户首先创建的进程。

    子进程:由父进程创建的进程。

    父子(孙)进程之间构成一课树形结构,即进程树。(Linux 系统下用 pstree 查看)

任何子进程只能由父进程创建和撤销,子进程不能对父进程实施控制。父子进程的运行无先后顺序(异步性)。子进程可全部或部分共享父进程拥有的资源。

PCB 的组织方式(数据结构)有三种。

  1. 线性表方式

    将所有 PCB 不分状态,全部组织在一个连续表中。实现简单,但是每次查找都要扫描整个 PCB 表,一般在进程较少的系统中使用。

  2. 链接方式

    把具有相同状态的 PCB,链接成一个队列。有执行队列、就绪队列、阻塞队列和空闲队列。

  3. 索引方式

    所有 PCB 线性存放(不论状态),但是有相应的索引表记录 PCB 的偏移位置。(如就绪索引表、阻塞索引表)

2. 进程控制

进程的控制由原语操作实现。原语是由若干条指令构成,用于完成一定功能的一个过程。

原语操作是一种 “原子操作” ,因此原语操作中的所有动作、要么全做,要么不做。

原语操作是一种不可分割的操作。(类似化学世界中原子不可分割)

原语操作是 OS 内核执行基本操作的主要方法。

2.1 进程的创建

引起进程创建的事件有

  1. 用户登录:用户合法登录后创建 init 进程。
  2. 作业调度:某作业被调度(批处理系统)
  3. 提供服务:用户调用某些系统调用或命令后
  4. 应用请求:由用户程序自己创建进程(贪玩蓝月启动)

进程创建原语的基本步骤是

  1. 申请空白 PCB
  2. 分配资源
  3. 初始化 PCB
  4. 将新进程插入就绪队列。
// 参数依次是 进程名、优先级、所需初始资源,执行程序文件名、参数列表
procedure create (pn, pri, res, fn, args);
begin
    getfreepcb(i);                    // 1. 申请空白 PCB
    if i=NIL then return (NIL);       
    i.id := pn;                       // 2. 初始化 PCB   
	i.priority := pri;                   
	i.resources := res;               // 3. 分配资源  
    memallocate(datasetsize, add);    // 3. 分配内存,add 是首地址
    if add = NIL then                    
    begin
        pcbrelease (i);
        return (NIL);
    end;
    i.dataadd := add;                 // 3. 初始化 PCB
	i.datasize:= datasetsize; 
    datasetinit(i.dataadd, args);     
    filestate(fn, add, size);         // 2. 分配资源 
    if add = NIL then
    begin
       memallocate(size, add);
        if add = NIL then
        begin
            memrelease (i.dataadd, i.datasize);
            pcbrelease(i); return(NIL);
        end
        read(fn, size, add);
    end;
    i.textadd := add;                // 3. 初始化 PCB
	i.textsize : = size;
    i.prog := fn;       
	i.pc := add;
    i. children := 0;  
	i.parent := EXE;                 // EXE 是执行态进程的 PCB 指针
    EXE.children := EXE.children+1;
    i.state := ready; i.queue := RQ;
    insert(RQ, i);                   // 4. 将新进程插入就绪队列
    otherinit;
    return(i);
End;

可以看出,如果 PCB 或资源不足,创建就不会成功。

2.2 进程的终止

  1. 进程正常结束

    通过停止原语实现。

    procedure halt(i);
    begin
        i.state := stop;           // 进程状态变为 stop
        send(i.parent, completed); // 告知父进程,等待回收
    end;
    
  2. 异常结束

    出现某些错误和故障而迫使进程终止。(如越界、非法指令等)。

  3. 外部干预

    用户或系统干预,父进程请求或终止。

进程的终止原语(和停止原语不同,这是强制终止)的大概逻辑是

  1. 读取进程状态,如果是 stop,跳到 2,如果不是 stop,调用 halt 再到 2 。
  2. 将子孙进程终止
  3. 归还资源
  4. 释放 PCB
procedure destory(i);
begin
    if i.state <> stop then
        halt(i);
    while i.children > 0 do       // 2. 将子孙进程终止
    begin
        i.children := i.children  1;
        findchild(i, child);      // 查找子进程
        destory(child);
    end;
    memrelease(i.dataadd, i.datasize)  // 3. 归还资源
    close(i.prog, t);
    if t = true then
        memrelease(i.textadd, i.textsize);
    resrelease(i);
    remove(i.queue, i);         // 4. 释放 PCB ,remove 将进程移出队列
    pcbrelease(i);      
end;

2.3 进程的阻塞

引起进程阻塞的事件有

  1. 请求系统服务(如请求分配 I/O)
  2. 启动某种操作(如启动 I/O)
  3. 新数据尚未到达(如进程通信)
  4. 无新工作可做(系统进程)

进程阻塞原语一般要经过如下几个步骤

  1. 保存现场信息
  2. 进程变为 “Blockeda”
  3. 插入阻塞队列 q
  4. 进程调度
procedure block(q);
begin
    save(EXE);                // 1. 保存现场信息
    EXE.state := Blockeda;  // 2. 进程变为'Blockeda'
    EXE.queue := q;           // 3. 插入阻塞队列 q
    insert(q, EXE);           // insert 将进程插入队列
    EXE := NIL;               // 4. 进程调度
    scheduler;
end;

2.4 进程的唤醒

引起进程唤醒的事件有

  1. 系统请求实现(如获得 I/O 资源)
  2. 某些操作完成(如 I/O 操作完成)
  3. 新数据已经到达(如其他进程已将数据送达)
  4. 又有新工作可做(系统进程)

进程唤醒原语主要有以下步骤

  1. 从队列中移出进程
  2. 读取进程状态,如果为 Blockeda,活动就绪,否则为静止就绪(挂起了)。
procedure wakeup(q);
begin
    outqueue(q, i);             // 1. 从队列移出进程
    if i.state = Blockeda     // 2. 读取进程状态并做相应操作
    then i.state = Readya 
    else  i.state = Readys;
    i.queue := RQ;
    insert(RQ, i);
end;

2.5 进程的挂起

挂起原语 suspend 的操作步骤如下

  1. 读取进程状态,如果是就绪改为静止就绪,否则改为静止阻塞。
  2. 将数据集合复制到外存。
  3. 释放数据段内存。
  4. 判断代码段是否共享,如果不共享释放代码段内存。
procedure suspend(i);
begin
    if i.state = Readya   // 1. 读取并判断相应状态
    then i.state=Readys else i.state=Blockeds; // 活动阻塞 ? 静止阻塞
    swapout(i.dataadd, i.datasize, add)  // 2. 将数据段复制到外存
    i.swapadd := add;
    memrelease(i.dataadd, i.datasize);   // 3. 释放数据段内存
    close(i.prog, t);                    // 4. 判断是否释放程序段内存
    if t = true then memrelease(i.textadd, i.textsize);
end;

2.7 进程的激活

激活原语 active 的操作步骤如下

  1. 为数据段申请内存,失败就退出。
  2. 将数据集调入内存。
  3. 若程序段不是共享的,为程序段申请内存,失败就退出。
  4. 将程序调入内存。
  5. 将进程设置为活动阻塞或活动就绪。
procedure active(i);
begin
    memallocate(i.datasize, add);        // 1. 为数据集申请内存
    if add = NIL then return(false);
    swapin(i.swapadd, i.datasize, add);  // 2. 将数据集调度内存
    i.dataadd := add;
    filestate(i.prog, add, size);        // 3. 为程序段申请内存
    if add = NIL then
    begin
        memallocate(size, add);
        if add = NIL then
        begin
            memrelease(i.dataadd, i.datasize);
            return(false);
        end;
       read(i.prog, size, add);          // 4. 将程序调入内存
    end;
    i.textadd := add;
    if i.state = Readys                // 5. 状态设置
    then i.state := Readya 
    else  i.state := Blockeda
    return(true);
end;

3. 进程同步

多个进程之间有相互合作关系,进程 I 负责读入数据,进程 C 负责处理数据,进程 P 负责写入数据。这就涉及到了进行执行的先后关系。这叫直接相互制约。

多个进程需要用到统一资源,也就是资源共享,如果进程之间没有直接相互制约,那么谁先用也没有多大关系,但是有了相互制约,情况就不一样了。导致使用资源也有了先后顺序,这叫间接相互制约。

进程同步

例子中的 count 就是临界资源(共享资源)

3.1 临界资源

临界资源时一个时刻只能由一个进程使用的资源。包括

  1. 硬件资源:打印机,磁带机等等。
  2. 软件资源:变量、表格、队列等等。

对临界资源的使用采用互斥方式,也就是说一个进程用完之后才轮到下一个进程用。

3.2 临界区

进程中访问临界资源的代码段 。同类临界区则是涉及同一临界资源的不同进程中的临界区。

  1. 进入区

    进入临界区前要通过进入区,进入区检查临界资源使用情况的代码段。如果条件不合适,你就去不了临界区了。只能等,这不就是公交站台吗。

  2. 退出区

    执行完临界区的代码后,进入退出区,退出区是负责恢复临界资源访问标志的代码段。

临界资源使用的同步准则有如下四点(熟记)

  1. 空闲让进:(提高效率)
  2. 忙则等待:(解决互斥)
  3. 有限等待:我的忍耐是有限的(以免死等)
  4. 让权等待:放弃占用 CPU(把机会留给更需要的人)

临界资源的状态判断与设置

因此对临界资源状态(是否正在使用)必须判断与设置同时进行,即判断与状态设置(状态改变)为原子操作,否则就会出现问题。

3.3 信号量

信号量(Semaphore)是一种特殊的变量(S),除初始化外,对信号量变量(S)的操作只能由两个标准的原子操作(不可中断)实现,分别是

wait(S):等待操作(也叫 P 操作)

signal(S):发信号操作(也叫 V 操作)

信号量变量是由一个整型数和一个阻塞等待进程链表构成的记录型数据结构

type semaphore = record
	value: integer;
	L: list of process;
end;

所有对同一信号量进行操作且进入等待状态的进程,都自动进入阻塞等待(让权等待)状态,并将其挂在该信号量阻塞等待队列 L 中。

  1. P 原子操作(wait)

    procedure wait(s);
    var s: semaphore;
    begin
        s.value := s.value  1;
        if s.value < 0 then block(s.L);
    end;
       
    

    小于 0 表示没有资源了,所以要睡眠等待。

  2. V 原子操作(signal)

    procedure signal(s);
    var s: semaphore;
    begin
        s.value := s.value + 1;
        if s.value <= 0 then wakeup(s.L);
    end;
    

    V 操作就是用完资源归还,value 小于等于 0 说明阻塞队列非空,可以叫醒一个进程。

  3. 利用信号量实现进程互斥同步

    例如前面的银行存款问题,count 是共用变量(即共享资源),采用 mutex 互斥信号量实现互斥访问

    var mutex: semaphore := 1;          //定义信号量变量,初值为1
       
    //增加存款进程                //减少存款进程
    procedure P1;               procedure P2;        
    begin                       begin
        wait(mutex);                wait(mutex);
        count += 300;               count -= 200;
        signal(mutex);              signal(mutex);
    end;                        end;
    
  4. 利用信号量实现较复杂的进程同步

    S1 … S7 分别为进程 P1 … P7 的执行部分,它们的执行顺序如下图所示

    信号量例题

    var a, b, c, d, e, f, g, h: semaphore := 0, 0, 0, 0, 0, 0, 0, 0; //初值为0
    P1: begin S1; signal(a); signal(b); end;
    P2: begin wait(a); S2; signal(c); end;
    P3: begin wait(b); S3; signal(d); end;
    P4: begin wait(c); S4; signal(e); signal(f); end;
    P5: begin wait(e); S5; signal(g); end;
    P6: begin wait(f); wait(d); S6; signal(h);
    P7: begin wait(g); wait(h); S7; end;
       
    
  5. 信号量机制的特性

    wait(s) 操作和 signal(s) 操作必须成对出现(可以不在同一个进程中)。

    缺少 wait(s) 不能保证资源互斥使用。

    缺少 signal(s) 将可能使资源永远得不到释放。

    不存在 “忙等” 问题。

  6. and 型信号量集机制

    将进程运行过程中需要的所有临界资源,一次性地全部分配给进程(要么全部分配,要么一个也不分配)。

    进程使用完后再一起释放。

    针对死锁问题。

    P 原子操作(Swait)

    procedure Swait(s1, s2, , sn);
    var s1, s2, , sn: semaphore;
    begin
        if  (s1 >= 1) and  and (sn >= 1) 
        then {for i := 1 to n do  si := si  1; 
                return true;}
        else  {将程序计数器设置为本函数开始处;
                 将进程插入阻塞队列;}
    end;
    

    V 原子操作 (Ssignal)

    procedure Ssignal(s1, s2, , sn);
    var s1, s2, , sn: semaphore;
    begin
        for i := 1 to n do  si := si + 1; 
        将与si有关的进程从阻塞队列中唤醒
    end;
       
    
  7. 一般信号量集机制

    进程运行需要多个临界资源。

    一次需要 N 个同类临界资源。

    大于 t 个同类临界资源才准予分配。

    P 原子操作 (Swait)

    procedure Swait(s1, t1, d1; ; sn, tn, dn);
    var s1, s2, , sn: semaphore;
    begin
        if  (s1 >= t1) and  and (sn >= tn) 
        then  {for i := 1 to n do  si := si  di; 
                  return true; }
        else  {将程序计数器设置为本函数开始处;
                 将进程插入阻塞队列; }
    end;
       
    

    V 原子操作(Ssignal)

    procedure Ssignal (s1, d1; ; sn, dn);
    var s1, s2, , sn: semaphore;
    begin
        for i := 1 to n do  si := si + di; 
        将与si有关的进程从阻塞队列中唤醒
    end;
       
    

    一般信号量集特例

    Swait(s,d,d) :一个信号量,每次同时分配 d 个同类资源。

    Swait(s,1,1) :等效于记录型信号量。也就是互斥信号量。

    Swait(s,1,0) :s=1 运行多个进程进入特定区,s=0阻止任何进程进入特定区。

  8. 管程

    临界区分散在各个进程之中,不便于管理和控制,而且很多临界区的操作是相同的,重复编写,使进程结构不清晰。如果编程出现差错,不便于检查,且会带来严重后果。

    管程定义了一个数据结构和能为并发进程所执行(在该数据结构上)的一组操作,这组操作能同步进程和改变管程中的数据。

    管程实际上是一种能实现进程同步的特殊的子程序(函数、过程、数据)的集合。

    管程由以下部分组成

    1. 名称:该管程的标识。
    2. 共享变量说明:局部于管程的变量说明(包括特殊同步变量)。
    3. 一组过程(函数):对该数据结构进行操作的程序段(含有相当于临界区代码段)。
    4. 初始化:对局部于管程的数据设置初始值。

    进程访问临界资源,必须经过管程,管程每次只允许一个进程进入,即只要某个进程进入了管程,则要么执行完成,要么因故阻塞,然后下一个进程才可以进入管程。

    设置条件变量(相当于互斥信号量)x,当临界资源被占用,执行 x.wait,将进程挂在 x 条件变量的阻塞等待队列中,当临界资源已空闲,执行 x.signal,从 x 条件变量的阻塞等待队列中,唤醒第一个进程。

    monitor

  9. 进程和管程之间的比较

    进程定义私有 PCB,管程定义公共数据结构。 进程顺序执行操作,管程实现同步操作。 进程提高系统并发性,管程解决资源互斥访问。 进程调用管程,管程被进程所调用。 进程之间可以并发,管程不能与调用者并发。 进程具有动态性,管程是资源管理模块。

  10. 管程的优点

    使用临界资源的进程进行调用时非常简单。

    进程结构清晰。

    易于差错

4. 进程同步的经典问题

  1. 生产者-消费者问题
  2. 读者-写者问题
  3. 哲学家进餐问题

5. 进程通信

进程通信时指进程之间的信息交换。

采用信号量机制可以实现进程(低级)通信(如生产者-消费者问题),但其通信效率低,通信对用户不透明(用户必须自己编写访问临界资源的程序或管程,包括数据结构的设置、数据的传送、进程的互斥于同步等),算是进程之间的低级通信。

进程通信有三种常用类型。分别是

  1. 共享存储器系统(无格式)
  2. 消息传递系统(有格式)
  3. 管道通信系统(单向通道)

共享存储器系统:进程间以共享存储器方式进行数据通信。

  1. 基于共享数据结构的通信方式

    由用户程序定义数据结构、申请内存,并同步各进程对该数据结构的访问。(生产者-消费者问题)

  2. 基于共享存储区的通信方式

    系统设置一共享存储区,由系统同步各进程对该共享存储区的访问。用户需要共享存储区时,到系统中去申请,系统分配一部分共享存储分区给用户并返回该分区的名字,相关进程按名共享该分区。

消息传递系统:进程程间的数据交换以消息(message)为单位。操作系统直接提供一组命令(原语)实现通信。

管道:管道是一个打开的共享文件(pipe文件)。管道用于连接一批读进程和一批写进程,实现它们之间的通信。所有相关进程之间的同步都由系统控制,管道采用先进先出(FIFO)操作。发送进程可以源源不断地从管道一端写入数据流;每次写入的信息长度可变;但一个时刻只能有一个发送进程写。接收进程可以从管道另一端读出数据,同样每次读出的信息长度可变;但一个时刻只能有一个接收进程读。

5.1 消息传递通信实现方法

  1. 直接通信方式

    发生进程直接将消息发送给接收进程。

    接收进程从发送进程得到消息。

    主要采用两种命令(原语)

    1. 发送消息原语 Send(Receiver, message)
    2. 接收消息原语 Receive(Sender, message)
  2. 间接通信方式

    需要某种共享数据结构的实体作为中介。发送进程将发送给目标进程的消息暂存于该中介中,接收进程从中介中取出发送给自己的消息。

    中介一般称为信箱(mailbox),因此,间接消息传递通信也称信箱通信。

    主要采用四种命令(原语)

    1. 信箱创建命令 Create(mailbox)
    2. 信箱撤销命令 Delete(mailbox)
    3. 消息发送命令 Send(mailbox,message)
    4. 消息接收命令 Receive(mailbox, message)

    信箱也有好几种类型,如下

    1. 私用信箱

      由用户进程创建,其它进程只能向该信箱发送消息。

    2. 公用信箱

      由操作系统创建,并提供给系统中的所有核准进程使用。

    3. 共享信箱

      由某进程创建,并指明共享者名字;所有共享者和创建者进程都可以取走自己的消息。

    通信可以实现一对一,一对多,多对一,多对多。

  3. 进程同步方式

    发送进程阻塞,接收进程阻塞[无缓冲]。

    发送进程不阻塞,接收进程阻塞[如服务器]

    发送进程不阻塞,接收进程不阻塞[相当于生产者消费者问题]

5.2 消息缓冲队列通信机制

发送进程直接将消息发送给接收进程,并将它挂在接收进程的消息缓冲队列上,接收进程从消息缓冲队列中取得消息

6. 线程

在传统OS中,进程既是CPU调度和分配的基本单位,同时,进程又是拥有资源的独立实体。在处理(创建、撤消、调度)进程时,所费开销较大。

在OS中引入进程,主要是为了提高计算机系统的并发执行能力,将进程的两个属性分开,即让进程仅成为拥有资源的单位,而让线程成为调度和执行的基本单位,是为了进一步提高并发能力,并有利于多处理机系统的调度。

6.1 线程与进程的关系

  1. 进程是线程的载体。
  2. 一个进程可包含多个线程。
  3. 一个线程是进程的一个执行流。
  4. 一个进程中至少包含一个线程,称主线程。