进程&线程&协程
-
进程
一个正在运行的程序代码,是系统资源分配的最小单位.
进程不仅包含(文本段),还包括进程堆栈段(函数的参数,返回地址,局部变量),数据段(全局变量),还可能包含堆,堆中存放着动态分配的内存.
每个进程都有独立的内存空间,不共享数据,不同进程要通过进程间通信来进行通信.
所以上下文进程的切换开销往往比较大,但是也相对比较稳定安全. -
线程
线程是进程的一个实体,是CPU调度和分派的基本单位,是比进程更小的,能够独立运行的基本单位.线程基本上只拥有最基本的基本资源(程序计数器,寄存器,栈,线程ID)
线程是cpu使用基本单元,是CPU资源调度与分派的基本单元。由线程id,程序计数器,寄存器与栈组成。
不同线程间的代码段,数据段与打开的文件/信号等是共享的。
代码和数据共享的优点是能够允许一个应用程序在同一个地址空间有不同的活动线程
- 协程
协程是一种用户态的轻量级的线程.如golang中的goroutine.协程的调度完全由用户来控制,协程拥有自己的寄存器上下文和堆栈,协程在切换的时候,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文与栈.线程与进程的操作是由程序触发系统接口,最后的执行者是系统,而协程的操作执行者是用户自身程序
什么是进程映像?进程映像与进程关系是什么?
由程序段,相关数据段和PCB三部分组成了进程映像,也叫作进程实体.进程映像是静态的,进程是动态的,进程是进程实体的运行过程.
讲一下进程控制块
进程通知块是进程存在的唯一标识.
系统是通过PCB来对进程进行控制的因为PCB包含以下信息:
- 进程标识符PID
- 进程队列指针,记录下一个PCB的地址,系统的PCB会组织成多个队列,如就绪队列,阻塞队列等
- 程序和数据地址
- 进程优先级
- CPU现场保护区 (程序计数器,状态寄存器,通用寄存器等)
讲一下进程的五种基本状态
- 就绪状态,进程已经获得了除了处理器以外的所有资源,一旦获得处理器资源就可以立即执行
- 执行状态,进程获得必要的资源并且已经在CPU执行
- 阻塞状态,当进程处在阻塞状态时候,把处理器分配给该进程,它也无法进行
- 创建
- 退出
执行状态只能通过就绪状态转换,无法通过阻塞状态直接转换.
为什么要引入线程?为什么要引入进程?
引入进程目的: 使多个程序并发执行,以改善资源利用率以及提高系统的吞吐量
引入线程目的: 为了减少程序并发执行时候的时空开销.具体来讲,进程的并发执行需要操作系统创建,撤销进程以及对进程做切换,在进行这些操作的时候,操作系统需要为进程分配资源和回收资源,为运行进程保存现场信息,这些操作通常需要付出较多的时空开销.
讲一下内核级线程与用户级线程的区别
1.内核级线程:由操作系统来负责线程的创建与撤销,内核维护进程和线程的上下文信息并且完成线程切换的工作.
内核级线程由于IO操作而阻塞的时候,不会影响其他线程运行
时间片分配对象是线程,所以有多个线程的进程将有更多的处理器时间.
2.用户级线程:用户级线程的维护由应用进程来完成.不需要操作系统内核了解用户级线程的存在.
由于操作系统不了解用户线程存在,当一个线程阻塞的时候,整个进程都必须等待
时间片分配对象是进程,所以有多个线程的进程,每个线程执行时间相对减少.
讲一下进程间通信与线程间通信的方式
进程间通信
-
管道
1.半双工(数据单向流动)
2.只能用于有亲缘关系的进程间通信 -
共享内存
映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但是可以供多个进程访问.最快的IPC方式 -
信号量
一个计数器,用来控制多个进程对共享资源的访问 -
消息队列
消息队列是消息组成的链表,存放在内核中并且由消息队列标识符标识. -
套接字
可以用于不同机器间的进程通信
线程间通信
-
临界区:
通过多线程的串行化来访问公共资源或者一段代码,速度快,而且适合控制数据访问 -
互斥量:
采用互斥对象机制,只有拥有互斥对象的线程才有访问公共资源的权限 -
信号量:允许多个线程同一时刻访问同一个资源,一般需要限制同一时刻访问此资源的最大线程数目.
-
事件信号(wait/nofigy)
多线程模型
- 用户层的用户线程
用户线程受内核支持,无须内核管理
- 内核层的内核线程
内核线程由操作系统直接支持与管理
多对一模型
把许多用户级线程映射到一个内核线程。
如果一个线程执行了阻塞系统调用,整个进程会阻塞
也就是说内核任何时候只能有一个线程访问
好处是线程管理在用户空间进行,效率高
一对一模型
一对一模型把每个用户线程映射到一个内核线程
这也是其缺点,创建内核线程开销会很大
多对多模型
多路复用了许多用户线程到相同数量或者更少的内核线程
N个用户线程可以映射到M个内核线程
详细介绍一下golang中的goroutine
- goroutine是一种轻量级的线程
- goroutine之间共享内存
- goroutine的启动代价很小.一般系统级线程都会有一个固定大小的栈,这个栈主要用来保存函数递归调用时候的参数与局部变量
固定了栈的大小导致了两个问题:一是对于很多只需要很小的栈空间的线程来说是一个巨大的浪费,二是对于少数需要巨大栈空间的线程来说又面临栈溢出的风险。
而goroutine会根据需要动态伸缩栈的大小,其会以一个很小的栈启动,所以启动的代价很小.
- goroutine只有在当前Goroutine发生阻塞的时候才会导致调度.
- 同时调度发生在用户态,调度器会根据具体函数只保存必要的寄存器,因此切换代价比系统线程低很多.
讲一下非抢占式,抢占式调度
首先我们来讲一下操作系统的CPU调度:
- 每当cpu空闲的时候,操作系统必须从就绪队列中选择一个进程来执行,进程选择由短期调度程序或者CPU调度程序来执行
非抢占调度:一旦CPU分配给一个进程,这个进程会一直使用该CPU直到进程终止或者切换到等待状态.
抢占调度:操作系统可以把正在运行的进程强行暂停,由调度程序把CPU分配给其他的就绪进程.
用户态与内核态区别
在计算机系统中,运行着系统程序与应用程序两种程序.
-
系统态
运行操作系统内核的程序 -
用户态
应用程序只能在用户态进行,即运行用户程序 -
特权指令 – 在系统态时候运行的指令
1.对内存空间的访问范围基本不受控制,能同时访问用户存储空间与系统存储空间
2.特权指令只允许操作系统使用,不允许应用程序使用
- 非特权指令–在用户态时运行的指令
只能完成一般性的操作和任务,不能对系统中的硬件和软件直接进行访问,其对内存的访问范围也局限于用户空间
用户态切换到内核态的唯一途径:
中断/异常/陷入
内核态切换到用户态的途径:
设置程序状态字
讲一下处理器的三级调度
级别越高,调度的频率越低
1.高级调度(作业调度)
2.中级调度
3.低级调度(进程调度)
作业调度为进程被调用做准备,进程调用使得进程被调用.
作业调度的结果是为作业创建进程,而进程调度的结果是进程被执行.
讲一下CPU调度算法
-
FIFO (先来先服务)
1.调度顺序是任务到达就绪队列的顺序.
2.非抢占式 -
最短作业优先算法(SJF)
1.最短作业(CPU区间长度最小)最先调度
2.非抢占式
3.缺点:对于长作业来说,如果大量的短作业不断进入就绪队列,那么长作业长时间得不到调度就会饥饿
可以保证最小的平均等待时间 -
最短剩余时间优先(SRJF)
1.SJF的可抢占版本,比SJF更具有优势
2.如何知道下一个CPU区间的大小?可以根据以前CPU区间的测量长度的指数平均 -
优先级调度
1.每个任务关联一个优先权,会调度优先权最高的任务,优先权太低的任务会一直就绪,得不到运行,出现"饥饿"现象,所以偶尔要对优先级进行调整.
2.优先调度可以是抢占或者是非抢占的.如果新到达进程的优先级高于当前运行进程的优先级,对于抢占优先级调度算法,其会抢占CPU,对非抢占优先级调度算法,只是把新进程加到就绪队列的头部.
3,问题:无穷阻塞/饥饿,即优先级低的可能一直得不到cpu
解决:老化技术,随着时间优先级提高。
算法核心:如何确定进程优先级?
进程优先级一般分两种:
1,静态优先级
按照进程类型(系统进程优先级高于用户进程)/ 作业资源要求 / 与用户类型和要求来确定
2.动态优先级
根据进程占有CPU长短或者就绪队列中进程等待时间来确定
-
轮转调度算法
1.抢占式调度
2.调度序列是循环序列
3.设置一个时间片,按照时间片来进行轮转调度.如果一个进程的cpu区间时间大于时间片,然后就绪队列又有区间时间小于时间片的进程的时候,正在执行的进程的CPU将会被抢占 -
多级队列调度
根据进程类型不同,把进程分配给不同的就绪队列,每个队列采用不同的调度算法 -
多级反馈队列调度算法
死锁
概念:一组进程中的每一个进程,均无限期等待此组进程中某个其他进程占有的,自己永远得不到的资源.
产生原因:系统资源不足/进程推进顺序不当
处理死锁的四种方法:
1.鸵鸟算法
对死锁视而不见,不理睬死锁
2.预防死锁
破坏产生死锁的四个必要条件
3.避免死锁
在资源动态分配过程中,用某种方法防止系统进入不安全状态,(在资源分配出去之前计算分配之后的系统是否安全)从而避免死锁产生.
4.检测与解除死锁
通过系统检测机构及时检测出死锁的发生,然后采取某种措施解除死锁.
死锁产生的四大条件:
-
互斥条件
至少一个资源只能一次由一个进程使用 -
占有并等待
一个进程必须至少占有一个资源,并且等待另一个资源 -
非抢占
资源不能被抢占,资源只能在进程完成任务之后自动释放 -
循环等待
有一组等待进程,{P0…Pn}
P0等待的资源为P1所占有,P1的资源为P2所占有…Pn的资源为P0锁=所占有
死锁避免算法
银行家算法
当一个进程申请使用资源的时候,银行家算法先试探分配给该进程资源,然后通过安全性算法判断分配后的系统是否处于安全状态.如果不安全,则试探分配作废,让该进程继续等待.
饿死/饥饿/活锁
饥饿: 没有发生死锁,但是但是某些进程可能会长时间等待,当等待时间给进程推进和响应带来明显影响的时候,发生进程饥饿
饿死:当饥饿太久,进程赋予的任务没有任何实际意义,称为饿死
活锁:两个进程之间都拿到了资源,但是互相谦让,不释放,是一种特殊的饥饿
逻辑地址,物理地址与线性地址
逻辑地址:CPU生成的地址,是机器语言指令出现的地址
mov 0x80495b0, %eax
这里的地址是逻辑地址,必须加上DS数据段的基地址才构成线性地址.也就是逻辑地址是当前任务的DS数据段内的偏移.
表述方式:segment:offset 段标识符加上段内相对地址的偏移量
线性地址: CPU所能寻址的空间或者范围
Linux中逻辑地址等于线性地址。为什么这么说呢?因为Linux所有的段(用户代码段、用户数据段、内核代码段、内核数据段)的线性地址都是从 0x00000000 开始,长度4G,这样 线性地址=逻辑地址+ 0x00000000,也就是说逻辑地址等于线性地址了。
物理地址: 用于内存芯片级的单元寻址.加载到内存单元寄存器的地址,即内存单元看到的地址,要经过MMU(CPU的内存管理单元)把虚拟地址转换成物理地址.
分段与分页
- 分段
1.基本原理是把逻辑地址转换为线性地址
1.IA-32首选确定需要访问的段,根据TI位来确定要使用的段寄存器
2.根据段选择符号的TI字段决定是访问GDT还是LDT,它们的首地址通过GTDR和LDTR来获得.
3.将段选择符的index*8,加上首地址,得到段描述符的地址
4.得到段描述符的地址之后,通过描述符的BASE来获得段的首地址
5.段的首地址+逻辑地址的32位偏移地址 = 线性地址
-
为什么要分段?
-
分页
1.基本原理是把线性地址划分成固定长度的单元,称为页.
2.页内部的连续地址映射到连续的物理地址中
3.要给CPU提供当前任务的线性地址到物理地址的转换表 -
为什么要分页?
讲一讲虚拟内存
1.不必要把整个程序一次性装入到内存中,可以按需分页调入
2.进而实现逻辑内存与物理内存的分离
3.允许文件与内存通过共享页来为多个进程所共享
栈与堆的区别
-
堆
分配方式类似于链表,是动态分配的内存块.一般存放对象,需要手动释放内存. -
栈
栈的内存是由系统自动分配,一般存放局部变量,比如对象的地址等值.函数中的局部变量的生命周期在函数调用完成之后就会结束.
转载:https://blog.csdn.net/weixin_38499215/article/details/101392628