跳转至

进程

7. 进程

7.1 进程定义 (静态信息)

7.1.1 进程描述

定义 一个具有一定独立功能的程序在一个数据集合上的一次动态执行过程

代码 -> Executable File (包含代码段、数据段) -> 加载后运行

7.1.2 进程组成

  • 程序代码
  • 程序处理的数据
  • 程序计数器中的值,指示下一条要运行的指令
  • 一组通用寄存器的当前值,堆、栈
  • 一组系统资源 (打开的文件等)

总之,进程包含了正在运行的一个程序的所有状态信息

通过多次执行,一个程序可对应多个程序;通过调用关系,一个进程可包括多个程序

程序和进程区别

  • 进程是动态的,程序是静态的;程序是有序代码的集合;进程是程序的执行,有核心态/用户态,例如读写文件等操作都是由操作系统完成,进程发出请求,操作系统代替进程在内核中执行
  • 进程是暂时的,是一个状态变化的过程;程序是永久的,可长久保存
  • 组成不同:进程包括程序、数据和进程控制块(进程状态信息)

7.1.3 进程特点

  • 动态性: 可以动态地创建、结束进程
  • 并发性: 可以被独立调度并占用处理器运行;并发并行,并发->一段时间内多进程执行,并行->一个时刻多进程执行
  • 独立性: 不同进程工作不互相影响
  • 制约性: 资源/进程之间同步而产生制约

描述进程的数据结构: 进程控制块(Process Control Block, PCB),操作系统为每个进程都维护了一个PCB,用来保存和该进程相关的各种状态信息

7.1.4 进程控制结构

PCB

进程存在的唯一标志

  • 进程创建: 生成一个PCB
  • 进程终止: 回收PCB

PCB包含三大信息

  1. 进程标识信息
  2. 处理机(CPU)状态信息保存区: 用户可见寄存器、控制和状态寄存器、栈指针
  3. 进程控制信息,操作系统对进程的管理和控制,比如和其它进程关联的进程队列

PCB组织形式:

  • 链表,统一状态的PCB成一链表 (更常用)
  • 索引表,统一状态进程归入一个index表,多个状态对应不同index表

7.2 进程状态 (动态)

进程生命周期的变化过程

7.2.1 进程生命期管理

进程创建/运行/等待/唤醒/结束

进程创建

  1. 系统初始化时,会创建init进程
  2. 用户请求创建新进场
  3. 正在运行的进程执行了创建进程的系统调用

进程运行

内核选择一个就绪进程,让它占用处理机并执行,这里就有如何选择的问题,即调度算法

进程等待

  1. 请求并等待系统服务
  2. 启动某种操作
  3. 需要的数据没有到达

进程只能自己阻塞自己

进程唤醒

  1. 被阻塞进程的资源得到满足
  2. 等待的时间到达
  3. 将该进程PCB插入到就绪队列

进程只能被其它进程唤醒

进程结束

  1. 正常退出 (自愿的)
  2. 错误退出 (自愿的)
  3. 致命错误 (强制性的)
  4. 被其它进程杀死 (强制性的)

7.2.2 进程状态变化模型

进程三种基本状态

  • 运行
  • 就绪
  • 等待

其它基本状态

  • 创建
  • 结束

7.2.3 进程挂起

进程挂起和阻塞不一样,进程挂起指进程没有占用内存空间

进程可能会把一部分占用的空间放到硬盘上,腾出空间给需要的程序

处于挂起状态的进程映像在磁盘上

  • 阻塞挂起 进程在外存并等待某事件出现
  • 就绪挂起 进程在外存,但只要进入内存便可运行

与挂起相关的状态转换

阻塞

  • 阻塞->阻塞挂起 没有进程处于就绪状态或者就绪进程要求更多内存时,进行这种转换
  • 就绪->就绪挂起 高优先级阻塞进程和低优先级就绪进程时,系统会挂起低优先级就绪进程
  • 运行就绪挂起 对抢先式分时系统,高优先级阻塞挂起进程因事件进入就绪挂起时,系统可能会把运行进程转到就绪挂起

外存状态转换

  • 阻塞挂起->就绪挂起 阻塞挂起进程相关事件出现,系统会将其转为就绪挂起

激活

  • 就绪挂起到就绪 没有就绪进程或者挂起就绪进程优先级高于就绪进程
  • 阻塞挂起到阻塞 一个进程释放足够内存时

操作系统如何管理进程?

状态队列

  • 操作系统维护一组队列,表示系统中所有进程的当前状态
  • 不同状态分别用不同队列来表示
  • 每个进程PCB根据其状态加入对应队列,一个进程的状态变化时,它的PCB从一个状态队列中脱离加入另一个

7.3 线程管理

直到80年代中期,人们又提出了更小的能独立运行的基本单位 —— 线程

7.3.1 为什么用线程

案例 - 编写一个MP3播放软件
单进程
问题: 播放出来的声音不连贯各个函数之间不是并发执行资源使用效率差
main() {
    while (true) {
        read();
        decompress();
        play();
    }
}
多进程read decompress和play写三个进程
问题: 进程间如何通信共享数据维护进程开销大
main() {}
main() {}
main() {}

需要提出一种实体,满足以下特性

  1. 实体之间可以并发地执行
  2. 实体之间共享相同地址空间 (进程之间无法共享相同地址空间)

这种实体就是线程(Thread)

7.3.2 什么是线程

进程当中的一条执行流程 —— 线程

进程负责资源管理,而进程的执行功能交给线程来做

线程控制块 TCB,负责管理和执行流程相关数据

PC - 指向代码
SP
State
Registers

线程 = 进程-共享资源

优点

  • 一个进程中可以存在多个线程
  • 各个线程之间可以并发执行
  • 各个线程之前可以共享地址空间和文件等资源

缺点

  • 一个线程崩溃,会导致其所属进程的所有线程崩溃

浏览器很多采取进程实现(安全性考量,每个进程打开一个网页),高性能计算等采用线程

现在操作系统大多支持多进程、多线程,线程有各自独立的寄存器和堆栈,但共享地址和文件等资源

线程与进程的比较

  1. 进程是资源分配单位,线程是CPU调度单位
  2. 进程拥有一个完整的资源平台,线程只独享必不可少的资源,如寄存器和栈
  3. 线程同样具有就绪、阻塞、还行三种状态,同样有状态间转换关系
  4. 线程能减少并发执行的时间和空间开销
    • 线程创建时间比进程短
    • 线程终止时间比进程短
    • 同一进程内线程切换时间更短
    • 由于统一进程内的线程共享内存和文件资源,线程可以进行不通过内核的通信

7.3.3 线程的实现

线程分用户线程和内核线程

  • 多对一:n个用户线程对应一个内核线程
  • 一对一
  • n对m
用户线程

对操作系统而言看不到TCB,只能看到进程信息PCB

在用户空间实现的线程机制,不依赖操作系统的内核,由一组用户级的线程库函数来完成线程的管理,包括创建、终止、同步、调度

每个进程有自己私有的TCB列表,跟踪记录它的各个线程的状态信息

允许每个进程有自定义的线程调度算法

user-thread

缺点

  • 如果一个线程发起系统调用而阻塞,则整个进程都在等待
  • 一个线程开始运行后,除非它主动交出CPU使用权,其它线程无法运行
  • 由于时间片是分配给进程的,和其它进程比,多线程执行时每个线程得到的时间片会较少,执行较慢
内核线程

操作系统看得见的线程,TCB放在内核里面

线程的创建管理都交给操作调用/内核函数的方式来运行,由内核来完成,因此系统开销较大

某个内核进程发起系统调用被阻塞,不会影响到其它进程的运行

kernel-thread

轻量级进程 (Solaris/Linux)

内核支持的用户线程

一个进程可以有一个或多个轻量级进程,每个量级进程由一个单独的内核线程来支持


7.4 进程上下文切换

7.4.1 上下文切换

停止当前运行进程,调度其它进程

context-switch

操作系统为活跃进程准备了进程控制块PCB

操作系统将PCB放在一个合适的队列里

  • 就绪队列
  • 等待I/O队列
  • 僵尸队列

7.4.2 进程创建

进程创建步骤

  1. 给新进程分配唯一一个进程标识符
  2. 给进程分配空间
  3. 初始化PCB
  4. 设置正确的连接
  5. 创建或扩充其它数据结构

操作系统提供给用户使用的一个系统调用,不同os有不同接口

Window: CreateProcess(filename)
Unix: fork()/exec()
    fork把一个进程复制成两个进程 parent(old PID) child(new PID)
    exec用程序来重写当前进程child进程变为新程序
    int pid = fork();
    if (pid == 0) {  // 子进程在这里继续
        exec("program", argc, argv0, argv1, ...);
    }
    fork返回值:
        子进程返回0子进程可以使用getpid()获取PID
        父进程返回子进程的标识符

fork/exec示例程序

fork时内核会分配一个新的PCB给child,

main {
    int childPID;

    childPID = fork();
    if (childPID == 0) {
        exec_status = exec("calc", argv, argv0...);
        printf("Why would I execute?");
    } else {
        pritnf("Who is your daddy?");
        child_status = wait(pid);
    }
}

fork的复制保持了哪些地方的一致?

int main() {
    pid_t pid;
    int i;
    for (i = 0; i < LOOP; i ++) {
        pid = fork();
        if (pid < 0) exit(-1);
        else if (pid == 0)
            fprintf(stdout, "i=%d, pid=%d, parent pid=%d\n", i, getpid(), getppid());
    }
    wait(NULL);
    exit(0);
}

如果循环三次,创建的进程结果如下,新创建的进程顺序受调度算法控制,并不是严格按照顺序执行,所以进程pid并不是严格增序

[1166]
第一次fork [1166, 1167]
第二次fork [1166, 1167, 1168, 1170]
第三次fork [1166, 1167, 1168, 1170, 1169, 1171, 1172, 1173]

ucore里fork()的实现

  1. 分配新的PCB
  2. 创建kernel stack
  3. 分配内存
  4. 设置进程标识等

空闲进程的创建

当用户进程都执行完后,CPU处于"暂停"状态,此时执行的就是空闲进程

空闲进程 proc_init()

idleproc->分配idleproc需要的资源->初始化idelproc进程控制块->完成idleproc的初始化

fork开销

开销昂贵,99%情况内fork后会调用exec,会有不必要的复制

vfork -> 创建的时候不进行复制,轻量级fork,子进程应该几乎立即调用exec

现在使用copy on write技术,进程在用的时候,根据是否进行写操作来决定是否复制

Linux COW技术

cow即子进程虚拟地址指向父进程物理地址,只有父子进程对这段地址进行修改时,才给子进程分配新的地址空间

而vfork即直接共享父进程虚拟空间,连子进程虚拟地址空间也不创建

7.4.3 加载和执行进程

wait()系统调用用来等待子进程的结束

为什么要让父进程等待,而不是由子进程直接退出

内核里面的PCB很难回收,即使把用户空间子进程的所有资源都释放,内核空间的PCB也无法释放,因此由父进程帮助完成

子进程完成,父进程还没有回收它 -> zombie状态,因此在running和exit间还多了一个zombie状态

子进程结束前父进程已经结束 -> 由init进程来作为其父进程,完成释放