我们先从定义的角度, 来对比这三个概念
进程
进程是操作系统资源分配的基本单位. 分配给进程的资源包括「内存」、「打开的文件」、「设备」等. 这些资源由「进程控制块PCB」记录, PCB中还存储着进程的唯一标识「PID」.
运行中的程序才叫「进程」, 高级语言程序通过编译生成可执行的「二进制文件」, 可执行文件被执行后. 进程进入运行状态, 这时程序才变为进程. 指令被调入内存, 存放在我们刚才说的代码段. CPU不断从代码段中取指令, 译码, 执行指令循环, 程序就运行起来了.
线程
-
线程共享进程的「内存」、「打开文件」,但是线程拥有自己的「寄存器」、「栈」。
-
线程是CPU调度的基本单位。
-
线程分为「用户线程」和「内核线程」,最终要占用CPU执行的,必须是「内核线程」。
-
「线程调度器」是内核里的一个功能模块,负责挑选哪个内核线程上 CPU。
-
用户线程只是一个运行逻辑的抽象,不能直接操作硬件(个人认为,这里的硬件主要指CPU资源)。
-
这样一来,我们所说的「一对一」,「多对一」,「多对多」模型,就好理解了 -- 用户线程需要绑定到内核线程上运行,因为内核线程能占用CPU。Go的协程调度机制GMP模型,就是典型的「多对多」模型。
-
「PCB」中包含多个「TCB」,二者都是操作系统在「内核空间」中维护的一块数据结构。
线程上下文切换
-
线程上下文切换是指CPU 停止执行当前线程,把执行权转交给另一个线程的过程。
-
上下文(Context) :包括 CPU 寄存器的值、程序计数器(PC)、栈指针(SP)、线程状态等信息。
-
我自己的一个理解:
-
用户态 -- 就是用户自己写的代码,代码被翻译成指令,需要CPU执行。
-
内核态 -- 就是OS官方的代码,能正确的操作硬件资源,要让我来写肯定完犊子了。
-
个人的一些理解:
- CPU正在执行用户的代码,一条一条的读指令,突然发现有一个「中断」。
- 「中断」很复杂,用户自己的代码处理不来,就要用OS官方写的代码,于是陷入「内核态」。
- 得知要进行进程切换,内核中负责进程切换的代码先「保存现场」,即保存「上下文」到「TCB」中。
- 内核的「线程调度模块」选择要执行的线程,为线程B。
- 内核的代码,把线程B的「TCB」中的寄存器信息,栈指针,加载到CPU的对应寄存器中。
- CPU的寄存器处于新的正确的位置了,又可以执行用户代码了,于是从内核态切换回用户态。
协程
我个人这样理解,goroutine是协程的一个实现。
goroutine 和 线程 的区别:
- 协程在用户空间模拟了一个类似TCB的结构体,拥有自己的PC和栈指针。
- 协程占用「内存」更小,一个协程初始拥有2KB的栈空间,可以扩容。一个线程拥有MB级别的栈空间。
- 协程的「上下文切换开销」更小,完全由Go runtime管理,不需要陷入内核。
- 协程的「创建与销毁开销」更小,进程创建与销毁也需要陷入内核,协程还是由Go runtime 管理。
type g struct {
stack stack // 栈信息
sched gobuf // 寄存器等上下文
...
}
type gobuf struct {
sp uintptr // 栈指针
pc uintptr // 程序计数器
g guintptr // 关联的 g
}