1 进程、线程、协程的概念
进程:
静态程序的运行状态就叫进程。是资源分配的基本单位。
线程:
是进程的一个执行单元,是进程内的调度实体。是CPU调度的独立单位。线程也被称为轻量级进程。
协程:
是一种比线程更加轻量级的存在。一个线程也可以拥有多个协程。其执行过程更类似于子线程,或者说不带返回值的函数调用。
2 进程和线程的区别
- 地址空间:
线程共享本进程的地址空间,而进程之间是独立的地址空间。
- 资源:
线程共享本进程的资源如内存、I/O、CPU等,不利于资源的管理和保护,而进程之间的资源是独立的,能很好的进行资源管理和保护。
- 健壮性:
多进程要比多线程健壮,一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都可能死掉。
- 执行过程:
每个独立的进程有一个程序运行的入口、顺序执行序列和程序入口,执行开销大。
但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制,执行开销小。
- 切换时:
进程切换时,消耗的资源大,效率高。所以涉及到频繁的切换时,使用线程要好于进程。同样如果要求同时进行并且又要共享某些变量的并发操作,只能用线程不能用进程。
对于进程,我们需要将状态保存到进程控制块(PCB)中,同样的,线程也有相同作用的线程控制块TCB,但是和进程上下文切换相比,线程在进行上下文切换的时候地址空间保持不变 ,所以不需要切换当前使用的页表。
- 其他:
线程是处理器调度的基本单位,但是进程不是。
3 线程与协程的区别
首先,线程与协程都具有隔离性,拥有自己的上下文。
但是线程在创建的时候需要预先分配MB级别的栈空间,而协程只需要KB。
线程间上下文切换与调度消耗很多资源,例如从用户态切换到内核态,保存当前线程环境,加载目标线程的执行环境,再从内核态切换到用户态,涉及两次上下文切换。而协程是在空户空间下模拟的线程操作。虽然协程在创建的时候也需要动态绑定一个线程,真正执行任务的也是线程。但是协程是在用户级别完成调度切换的,对于内核来说是无感知的,也就是说,协程的调度是基于用户进程空间的,可以避免线程间的切换问题(当然了,如果当前的协程切换是由于时间片到期导致做绑定的线程切换的话,还是会涉及线程的切换)。
线程是抢占式执行,当发生系统调用或者中断的时候,交由OS调度执行;而协程是可以在业务逻辑编写的时候由代码控制主动切换,比如说python 通过yield主动让出cpu所有权,切换到其他协程执行,当然 Go 可以利用 CSP 完成相同功能。
一旦创建完线程,你就无法决定他什么时候获得时间片,什么时候让出时间片了,你把它交给了内核。而协程编写者可以有:可控的切换时机和很小的切换代价。从操作系统有没有调度权上看,协程就是因为不需要进行内核态的切换,所以协程的上下文切换比线程快很多。
4 何时使用多进程,何时使用多线程?
对资源的管理和保护要求高,不限制开销和效率时,使用多进程。
要求效率高,频繁切换时,资源的保护管理要求不是很高时,使用多线程。
5 为什么会有线程?
每个进程都有自己的地址空间,即进程空间,当一个服务器通常需要接收大量不确定数量用户的并发请求,为每一个请求都创建一个进程显然行不通(系统开销大响应用户请求效率低),因此操作系统中线程概念被引进。
6 为什么要有协程
一是节省CPU,避免系统内核级的线程频繁切换,造成的CPU资源浪费。而协程是用户态的线程,用户可以自行控制协程的创建与销毁,极大程度避免了系统级线程上下文切换造成的资源浪费。
二是节约内存,在64位的Linux中,一个线程需要分配8MB栈内存和64MB堆内存,系统内存的制约导致我们无法开启更多线程实现高并发。而在协程编程模式下,可以轻松有十几万协程,这是线程无法比拟的。
三是稳定性,前面提到线程之间通过内存来共享数据,这也导致了一个问题,任何一个线程出错时,进程中的所有线程都会跟着一起崩溃。
四是开发效率,使用协程在开发程序之中,可以很方便的将一些耗时的IO操作异步化,例如写文件、耗时IO请求等。
7 进程有哪几种状态?
我们一般把进程大致分为 5 种状态。
- 创建状态(new) :进程正在被创建,尚未到就绪状态。
- 就绪状态(ready) :进程已处于准备运行状态,即进程获得了除了处理器之外的一切所需资源,一旦得到处理器资源(处理器分配的时间片)即可运行。
- 运行状态(running) :进程正在处理器上运行(单核 CPU 下任意时刻只有一个进程处于运行状态)。
- 阻塞状态(waiting) :又称为等待状态,进程正在等待某一事件而暂停运行如等待某资源为可用或等待 IO 操作完成。即使处理器空闲,该进程也不能运行。
- 结束状态(terminated) :进程正在从系统中消失。可能是进程正常结束或其他原因中断退出运行。
订正:下图中 running 状态被 interrupt 向 ready 状态转换的箭头方向反了。
8 进程间的通信方式
进程间的通信方式有两种基本模型:共享内存和消息传递。
共享内存模型会建立一块供协作进程共享的内存区域,进程通过向此区域读写数据进行通信。
消息传递模型通过协作进程间交换消息来实现通信,通常需要进行系统调用,所以消息传递模型的速度会比共享内存慢。
细分的话,共享内存模型又分:管道,消息队列,共享内存。消息传递模型又分:信号量,信号,socket套接字。
- 管道
- 管道有匿名管道和命名管道。匿名管道只能用于父子进程之间通信,而命名管道可以在任意两个进程间通信。不管是匿名管道还是命名管道,进程写入的数据都是缓存在内核中,另一个进程读取数据时候自然也是从内核中获取,同时通信数据都遵循先进先出原则。
- 消息队列
- 消息队列是保存在内核中的消息链表,在发送数据时,会分成一个一个独立的数据单元,也就是消息体(数据块),消息体是用户自定义的数据类型,消息的发送方和接收方要约定好消息体的数据类型,所以每个消息体都是固定大小的存储块,不像管道是无格式的字节流数据。如果进程从消息队列中读取了消息体,内核就会把这个消息体删除。
- 消息队列生命周期随内核,如果没有释放消息队列或者没有关闭操作系统,消息队列会一直存在,而前面提到的匿名管道的生命周期,是随进程的创建而建立,随进程的结束而销毁。
- 消息队列通信过程中,存在用户态与内核态之间的数据拷贝开销,因为进程写入数据到内核中的消息队列时,会发生从用户态拷贝数据到内核态的过程,同理另一进程读取内核中的消息数据时,会发生从内核态拷贝数据到用户态的过程。
- 共享内存
- 共享内存的机制,就是拿出一块虚拟地址空间来,映射到相同的物理内存中。这样这个进程写入的东西,另外一个进程马上就能看到了,都不需要拷贝来拷贝去,传来传去,大大提高了进程间通信的速度。
- 信号量
- 信号量其实是一个整型的计数器,主要用于实现进程间的互斥与同步,而不是用于缓存进程间通信的数据。
- 信号
- 上面说的进程间通信,都是常规状态下的工作模式。对于异常情况下的工作模式,就需要用「信号」的方式来通知进程。
- 比如说我们常用的
kill
命令,其实就会产生一个信号,告诉内核,我要终止某个进程。
- 套接字
- 前面提到的管道、消息队列、共享内存、信号量和信号都是在同一台主机上进行进程间通信,那要想跨网络与不同主机上的进程之间通信,就需要 Socket 通信了。
9 线程间的同步的方式
那线程间的同步的方式有哪些呢?
线程同步是两个或多个共享关键资源的线程的并发执行。应该同步线程以避免关键的资源使用冲突。
操作系统一般有下面三种线程同步的方式:
- 互斥量(Mutex):采用互斥对象机制,只有拥有互斥对象的线程才有访问公共资源的权限。因为互斥对象只有一个,所以可以保证公共资源不会被多个线程同时访问。
- 信号量(Semphares) :它允许同一时刻多个线程访问同一资源,但是需要控制同一时刻访问此资源的最大线程数量。
- 事件(Event) :Wait/Notify:通过通知操作的方式来保持多线程同步,还可以方便的实现多线程优先级的比较操作。
10 进程的调度算法
你知道操作系统中进程的调度算法有哪些吗?
先到先服务(FCFS)调度算法
- 从就绪队列中选择一个最先进入该队列的进程为之分配资源,使它立即执行并一直执行到完成或发生某事件而被阻塞放弃占用 CPU 时再重新调度。
短作业优先(SJF)的调度算法
- 从就绪队列中选出一个估计运行时间最短的进程为之分配资源,使它立即执行并一直执行到完成或发生某事件而被阻塞放弃占用 CPU 时再重新调度。
时间片轮转调度算法
- 时间片轮转调度是一种最古老,最简单,最公平且使用最广的算法,又称 RR(Round robin)调度。每个进程被分配一个时间段,称作它的时间片,即该进程允许运行的时间。
多级反馈队列调度算法
- 前面介绍的几种进程调度的算法都有一定的局限性。如短进程优先的调度算法,仅照顾了短进程而忽略了长进程 。多级反馈队列调度算法既能使高优先级的作业得到响应又能使短作业(进程)迅速完成。,因而它是目前被公认的一种较好的进程调度算法,UNIX 操作系统采取的便是这种调度算法。
优先级调度
- 为每个流程分配优先级,首先执行具有最高优先级的进程,依此类推。具有相同优先级的进程以 FCFS 方式执行。可以根据内存要求,时间要求或任何其他资源要求来确定优先级。