操作系统 Chap.3 进程 / 线程
1. 进程(Process)
1.1 概念
Process is a program in execution.
进程是一个正在执行的程序
一个进程通常包括多个部分:
- 程序代码(Program code / Text section)
- 当前活动(Current activity)
- 程序计数器(Program counter)
- 处理器寄存器(Processor registers)
- 栈(Stack): 用于存储临时信息, 如函数参数 / 返回地址 / 局部变量等
- 数据区(Data section): 用于存储全局变量
- 堆(Heap): 用于内存的动态分配
Program is passive entity stored on disk, while process is active.
程序是存储在硬盘中的被动实体, 而进程是活跃的程序. 这意味着当一个程序的执行文件加载如内存后, 程序就成为了一个进程.
1.2 进程状态(Process state)
一个进程被执行的完整周期中, 存在以下五个状态:
- 创建(New): 进程刚刚被创建
- 就绪(Ready): 进程已经准备好运行, 即进程已经被分配到除CPU以外的全部资源时
- 执行(Running): 进程已获得CPU, 程序正在执行
- 阻塞(Block / Waiting): 正在执行的进程由于某事件而无法继续运行的状态
- 终止(Terminated): 进程结束运行
1.3 调用栈(Call stack)
调用栈 是一种栈数据结构, 它负责存储活动函数的某些信息.
调用栈由 栈帧(stack frames / activation records) 构成, 每个函数的调用都对应着一个单独的栈帧, 里面包含:
- 传递的参数
- 返回地址
- 当前的程序计数器值
- 局部变量
调用栈在新的调用出现时, 会增长; 相应的, 在调用结束后, 会缩小.
1.4 调用约定(Calling Convention)
调用约定 规定了一个函数如何调用其它的函数, 以及这个调用如何返回. 包括:
- 参数是怎么传递的(如通过寄存器 / 通过调用栈)
- 参数顺序
- 谁来负责恢复调用之前的环境
- 返回值是怎样从被调用的函数传递给调用者的
1.5 执行上下文(Execution context / Processor state) / PCB
执行上下文 指的是CPU寄存器在任意时间点的内容.
常见的数据有:
- 程序计数器(Program counter): 指向当前程序运行位置
- 调用栈指针(Call stack pointer): 指向当前调用栈的顶端
- 分配给进程的内存的地址
- …
明确 进程状态(Process state) / 处理器状态(Processor state) 的区别
执行上下文很重要, 存哪?
进程控制块PCB(Process Control Block) 是 内核内存 中负责存储执行上下文的数据结构.
一个PCB中通常存储了:
- 执行上下文(Execution context)
- 进程ID(Process ID)
- 进程控制信息(Process control information)
1.6 进程控制(Process Scheduling) / 进程切换(Process Switch)
上面说了一堆, 主要是引出操作系统 进程控制 这一概念
我们之前提过, 进程控制是为了最大化CPU资源的利用率, 由于我们已经通过PCB将一个进程结构化了, 因此可以通过结构体数组来管理进程的执行, 即 控制队列
- Job queue: 包含系统中全部的进程
- Ready queue: 包含当前处于就绪状态, 等待执行的进程
- Device queue: 包含当前所有需要等待I/O设备的进程
进程在不同状态间转换, 体现在控制队列中, 即 进程在不同队列之间迁移.
我们上文中提及到进程的 阻塞态 , 当进程进入该状态, 操作系统需要暂存其状态并切换至其他的进程, 这即所谓 进程切换 .
当需要进程切换时, OS要进行如下操作:
- 将进程当前的上下文存储到它的PCB中
- 从处于就绪态的进程中选择一个进程
- 从被选择的进程的PCB中恢复其上下文
事实上, 进程切换广泛存在于操作系统的运行过程中: 我们刚刚提及到的进程进入阻塞态只是进程切换的一种情况.
进程阻塞 / 进程结束运行 / CPU时间片被用尽 时, 均会导致进程切换.
1.7 进程的创建(Process Creation) 与终止(Termination)
从用户视角而言, 要创建一个进程只需要调用OS提供的系统函数即可:
#include <unistd.h>
int pid = fork();
if(pid<0){
//error: No process created
}else if(pid>0){
//Parent process
}else{
//pid == 0
//Child process
}
在UNIX系统中, 进程通过 Process Identifier(pid) 进行管理, 通过一棵进程树来进行管理.
对于这一对父子进程, 子进程是由父进程创建出来的, 那自然子进程运行完毕后也要由父进程进行处理. 通常, 会有如下语句:
int pid = fork();
if(pid<0){
//error: No process created
}else if(pid>0){
...
wait(NULL); //Parent will wait the child to complete
}else{
...
exit(0); //Child will exit and finish its work
}
当然, 在某些情况下, 子进程未能正常退出, 或者父进程异常终止时, 则父进程会强制结束其下属全部子进程, 这是通过:
abort();
这一系统调用实现的.
1.8 僵尸进程(Zombie Process)
If no parent waiting (did not invoke wait()) process is zombie
当一个进程退出后, 它会进入被称为 僵尸进程 的状态. 当其父进程调用wait()后, 该僵尸进程才会被清理.
Init Process(pid = 1)会定期收集未能及时清理的僵尸进程.
init进程是一个特殊的进程, 是Linux系统中所有进程的起点。它是系统引导过程中由内核启动的第一个用户级进程。init进程的PID始终为1, 它是所有其他进程的祖先进程.
2. 线程(Thread)
我们上面提及的进程都是 单线程的 , 这意味着一个线程只能同时执行单个任务.
人们认为线程这种划分仍然不够细致, 因此进一步提出了 多线程(Multithread) 的概念.
多线程是很有好处的:
- 持续响应(Responsiveness): 一个进程的部分线程被阻塞时, 其余线程还能够继续响应指示. 这对于用户界面的设计尤为重要.
- 资源共享(Resource Sharing): 不同的线程共享一个进程的资源, 这相比于不同进程之间的信息传递要快很多
- 经济(Economy): 线程的创建要比进程创建快很多
2.1 用户线程(User Thread) / 内核线程(Kernel Thread)
当前的线程主要分两类:
- 用户线程(User Thread): 在内核之上运行, 由用户层线程库提供支持
- 内核线程(Kernel Thread): 由操作系统直接提供支持
在Windows以及Linux中, 用户线程与内核线程是一对一(One-to-one)的. 这意味着每个用户线程都映射到一个内核线程
除一对一之外, 多对一(Many-to-one)也是一种设计模型, 但由于其多对一的设计方式, 一个线程出现问题会导致整个映射都崩溃掉, 这有违多线程的初衷, 因此采用该种方式的操作系统较少.
除上, 还有多对多(Many-to-many) / 一对多与多对多混用 等线程设计模型.
当前, 总共有三个广为使用的多线程库:
- POSIX Pthreads
- Windows threads
- Java threads
至此, 我们总结一下进程与线程的知识与关联:
- 进程: 一个被执行的程序实例
- 不同的进程有不同的内存地址空间
- 创建进程需要很高的资源占用
- 一个进程最少是单线程的
- 线程: 是进程下的一个实体, 专用于代码执行
- 同进程下不同的线程共享着很多资源, 如内存地址空间 / 已经打开的文件等.
- 创建线程耗费的资源显著少于创建进程
- 对应的, 廉价的共享资源方式代表着一个线程的错误可能引起其余线程的崩溃
至此, 进程与线程我们基本梳理完毕.
这篇博文就到这里~