架构学习-多任务

架构学习-多任务

Posted by liz on April 13, 2024

架构学习-多任务:进程,线程,协程

多任务

多任务处理:是指计算机同时运行多个程序的能力。比如说,我们在使用电脑的时候,可以边听音乐,边写文档。

从物理层面上看,最早的 CPU 都是单核的,也就是同一时间只能执行一条指令。

单核 CPU 是如何支持多任务处理的呢?方法是把 CPU 切成一段段的时间片,每个时间片只运行某一个软件。多任务的一般方法运行第一个程序的一段代码,保存工作环境;在运行第二个程序的一段代码,保存环境;恢复第一个程序的工作环境,执行第一个程序的下的一段代码…现代的多任务,每个程序的时间分配相对平均。

每个时间片,只运行某一个软件,因为时间片很小,我们会感觉这些软件在同时运行。这种多时间分片实现的多任务系统,我们称之为分时系统。

那么这个任务指的是什么呢?从今天的现实看,任务的抽象并不是唯⼀的。⼤部分操作系统提供了两套:进程和线程。有的 操作系统还会提供第三套叫协程。

来看下这几个的区别

执行体 地址空间 调度方 时间片调度 主动调度
进程 不同的执行体有不同的地址空间 操作系统内核 基于时钟中断 系统调用syscall
线程 不同的执行体共享地址空间 操作系统内核 基于时钟中断 系统调用syscall
协程 不同的执行体共享地址空间 用户态 一般不支持 包装系统调用

这里着重来了解下协程

协程是一种比线程更加轻量的执行体,协程完全有程序控制(在用户态执行),能够带来性能的大幅度提升。

一个操作系统中可以有多个进程;一个进程可以有多个线程;同理,一个线程可以有多个协程。

协程的存在能够高效的利用线程,减少线程的创建。为什么需要减少线程的数量呢,因为线程创建的成本比较高。

线程的时间成本:

1、执行体切换本身的开销,主要是寄存器保存和回复的成本;

2、执行体的调度开销,主要是如何在大量已准备好的执行体中选出谁获得执行权;

3、执行体之间的同步互斥成本。

线程的空间成本:

1、执行体的执行状态;

2、TLS(线程局部存储);

3、执行体的堆栈。

这里首先来看下,什么是寄存器,寄存器是中央处理器用来占存内存指令,数据和地址的电脑存储器。寄存器的存储容量有限,读写速度非常快。在计算机体系结构里,寄存器存储在已知的时间点作为计算的中间结果,通过快速的访问数据来加速计算器程序的访问。

默认情况下,Linux 中线程数量在数 MB 左右,最大的成本是堆栈(虽然堆栈的大小可以设置的,但是处于线程执行的安全考虑,线程的堆栈不能太小),假定一个线程 1MB,那么 1000 个线程的大小就到了 GB 级别了,消耗还是很大的。

协程的存在能够降低执行体的空间和时间成本。

一个完备的协程库可以理解成用户态的操作线程,而协程就是用户态中的线程。

有两个语言实现了完整的协程库,GO 语言和 Erlang 语言。Go 语言里面的用户态”进程”叫 goroutine。有下面的一些设计:

1、堆栈开始很小(只有4k),可以按需增长;

2、干掉了”线程局部存储(TLS)”特性的支持,执行体更加精简;

3、提供了同步,互斥和其他常规执行体的通讯手段,比如 channel;

4、提供了几乎所有重要的系统调用(尤其是IO请求)的包装。

操作系统所有涉及系统调用的方法都在内核空间,包括磁盘读写,内存分配回收,网络接口读写,都是 web 应用巨频繁使用的。

如果是多线程的,线程在进行 I/O 操作时需要从用户态切换到内核态,等待 I/O 的过程,需要进行内核态线程的切换,然后再从内核态切换到用户态,时间和空间的开销成本都很大。

这里提到了内核态和用户态,这里来了解下,为什么会有内核态和用户态,以及为什么会有内核态和用户态的切换过程。

CPU将指令分为特权和非特权两类,以防止危险指令如清内存或设置时钟被滥用导致系统崩溃。只有操作系统级别的程序才能执行特权指令,而普通应用程序只能执行安全的非特权指令。Intel CPU 设有四个特权级别:RING0 到 RING3,其中 RING0 权限最高。

Linux内核为每个用户进程提供了一种机制,使得它们在执行系统调用时能够安全地从用户模式切换到内核模式,并在内核地址空间中运行,仿佛每个进程都有自己的内核副本。

当一个任务(进程)执行系调用而陷入到内核代码中执行时,我们就称代码处于内核运行态(简称为内核态)。此时,程序处于特权级最高的(0级)内核代码中执行。当进程处于内核态的时候,执行的内核代码会使用当前进程的内核栈,每个进程也都有自己的内核栈。

当进程在执行自己的代码的时,则称为用户用户运行态(用户态),即此时处理器在特群最低的(3级)用户代码中运行。当正在执行用户程序突然被中断程序中断时,此时用户程序也会象征性的处于进程的内核态,因为中断进程将使用当前进程的内核栈,这与处于内核态的进程有点类似。

用户态切换到内核态的三种方式

1、系统调用

这是用户态主动切换到内核态的一种方式,用户态通过系统调用申请操作系提供的服务程序完成工作,比如 fork() 实际上是执行了一个创建新进程的系统调用。而系统调用机制的核心还是使用了操作系统给用户特别开放的一个中断实现的,例如Linux的int 80h中断。

2、中断

当外围设备完成用户请求的操作后,会向 CPU 发出相应的中断信号,这时 cpu 会暂停执行下一条即将执行的指令转而去执行与中断信号对应的处理程序,如果先前执行的指令是用户态下面的程序,那么这个过程就发生了从用户态内核态的切换。比如硬盘的读写操作完成,系统会切换到硬盘读写的中断处理程序执行的后续操作。

3、异常

当 cpu 在执行运行在用户态下的程序时,发生了事先不可知的异常,这时会触发由当前运行进程切换到处理此异常的内核相关程序中,也就转换到了内核态,比如缺页中断。

用户态和内核态主要限制不同程序之间的访问能力,防止他们获取别的程序的内存数据,或者外围设备的数据,并发送到网络,CPU 划分出两个权限等级-用户态和内核态。

参考

【多任务处理】https://zh.wikipedia.org/wiki/%E5%A4%9A%E4%BB%BB%E5%8A%A1%E5%A4%84%E7%90%86
【面向对象编程】https://www.liaoxuefeng.com/wiki/1016959663602400/1017495723838528
【许式伟的架构课】https://time.geekbang.org/column/article/89668
【操作系统为什么要分用户态和内核态】https://blog.csdn.net/chen134225/article/details/81783980