一、进程管理
1.1 进程与线程
1.1.1 概述
-
进程
-
进程就是正在执行的程序的实例
-
状态:就绪态,阻塞态,运行态
Linux内核中进程状态由task_struct的state属性描述,主要是以下几种状态
- TASK_RUNNING(运行)
- TASK_INTERRUPTIBLE(可中断),相当于阻塞态
- TASK_UNINTERRUPTIBLE(就绪态)
-
资源:用于存放程序正文、运行时数据的磁盘和内存地址空间(独立的地址空间,包括代码段和数据段),以及在运行时所需要的I/O设备,已打开的文件,信号量等
-
实现:操作系统维护一个进程表,每个进程占用一个进程表项(这些表项也成为进程控制块,PCB),表项中包含PC,堆栈指针,内存分配状态,页表地址,其它调度相关信息等,打开文件列表和调度信息等进程运行必备信息
中断会首先保存相关寄存器的值,这些值就保存在PCB中
-
组成:程序段,数据段,PCB
-
创建:分配新内存空间,将程序装载到分配的内存空间中,生成PCB
Unix中,使用fork(),exec()两个函数完成进程创建,fork()拷贝父进程的task_struct,exec()负责读取可执行文件并将其装入内存;
Unix中复制进程的方式除了fork()之外,还有vfork(),clone()具体区别参考下面:
linux下 fork(),vfork(),clone()的用法及区别 - lonelycatcher - 博客园 (cnblogs.com)
总的来说:
- fork()创造的子进程复制了父亲进程的资源,包括父进程的task_struct,系统堆栈空间和页面表(页面表的复制表示紫禁城会使用不同的地址空间)
- vfork()创建出来的不是真正意义上的进程,与fork()的区别在于没有复制页表,也就没有独立的存储空间
- clone()可以有选择的继承父进程资源,往往创建线程会采用clone()的方式
-
终结:
- 给子进程换个父进程
- 撤销进程描述符
-
-
线程
- 资源:堆栈空间,程序计数器(可以参考JVM)
- 组成:线程控制块,堆栈空间,少量寄存器
是一种轻量级进程,vfork()方式创建出来的就是一个标准的线程;
一般通过clone()方式创建,通过参数决定继承父进程哪些属性;
-
进程线程区别
- 调度:线程是CPU调度的基本单位
- 资源:进程是拥有资源的基本单位,线程仅拥有PC,堆栈空间
1.1.2 进程间通信方式
每个进程都有自己的地址空间,任何一个进程的全局变量其它进程都无法看到,所以进程间的通信需要依赖内核空间,在内核中开辟一块缓冲区用于进程通信,根据对这块空间的不同使用方式可以将进程的通信方式分为一下几种:
- 管道
- 消息对立
- 共享内存
- 信号量
- 套接字(不同主机之间的进程通信)
1.1.3 线程间通信方式
线程通信方式众多,但是总的可以归结为共享内存和消息传递两种方式
Java的内存模型中对于线程通信机制,使用的是共享内存模型
-
共享内存:
通过写/读共享内存中的内容实现线程间隐式通信
常见的共享内存通信方式有
- 管程
- 信号量
- 保护共享资源,限制使用共享资源的进程数
- 互斥锁就是一种简单的二元信号量,只是其限制资源数为1
- 锁
Java中共享内存的通信方式有volatile,synchronized,锁等
-
消息传递:
消息传递方式采取的是线程之间的直接通信,不同的线程之间通过显式的发送消息来通信
常见的消息传递方式有
- 管道:一个进程和另外一个进程一对一传递信息,不能服务于其它进程,且所有信息一次性传输
- 消息队列:多对多
-
两种通信方式的比较
并发模型 | 通信机制 | 同步机制 |
---|---|---|
共享内存 | 线程之间共享程序的公共状态,线程之间通过写-读内存中的公共状态来隐式进行通信。 | 同步是显式进行的。程序员必须显式指定某个方法或某段代码需要在线程之间互斥执行。 |
消息传递(actor) | 线程之间没有公共状态,线程之间必须通过明确的发送消息来显式进行通信。 | 由于消息的发送必须在消息的接收之前,因此同步是隐式进行的。 |
1.1.4 协程
如果说线程是轻量级的进程,那么协程就是轻量级的线程;
一个线程可以拥有多个协程;
协程的切换是在用户态进行(重点),所以才诞生了协程,因为其上下文切换开销很小
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0jJntJXu-1648034331019)(C:\Users\19643\Desktop\notes\img\微信截图_20220306185043.png)]
1.2 调度
调度发生在以下几种情况:
-
当前运行进程退出
-
当前进程阻塞
-
外部I/O中断来到(某些进程正是在等待这个I/O完成而被阻塞,此时应该恢复就绪态,调度算法决定是否让该就绪态进程运行)
抢占式的调度策略下,即便当前线程正在运行,也有可能被剥夺CPU资源,调度其它进程运行
1.2.1 批处理系统中的调度
批处理系统更多的考虑的是吞吐量,周转时间和CPU利用率,针对这种系统有以下几种调度方式:
- 先到先服务
- 利于长作业,不利于短作业
- 利于CPU密集型,不利于I/O密集型
- 短作业优先
- 最短剩余时间
- 高响应比:综合考虑剩余时间和等待时间的调度方式
1.2.2 交互式系统中的调度
交互式系统中强调的是响应速度,其调度方式包括:
-
转轮调度
-
优先级调度(重要)
-
短作业优先
1.3 通信
- 共享空间
- 消息传递
1.4 同步
所有的同步方法都是建立再进程/线程间通信基础之上,同步是控制不同进程/线程之间操作发生相对顺序的机制
同步的机制大致有以下几种方法实现
- 互斥锁
- 信号量
- 条件变量
jdk中实现互斥也是基于以上几点,常见的有:
- Object的wait和notify
- Condition的await和signal + Lock
- CountDownLatch
- CyclicBarrier
-
生产者/消费者示例:
用Java中ReentrantLock和Condition来实现线程之间互斥和P,V(同步)操作
package juc; import org.junit.Test; import java.util.LinkedList; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.ReentrantLock; public class ProducerAndConsumer { private final LinkedList<Message> queue = new LinkedList<Message>(); // 缓冲区 private ReentrantLock lock = new ReentrantLock(); // PV操作中的mutex private Condition notFull = lock.newCondition(); // PV中信号量 private Condition notEmpty = lock.newCondition(); // PV中信号量 public ProducerAndConsumer (){ } class Producer implements Runnable{ public void run() { Integer id = 0; while(true) { // P(mutex) lock.lock(); // while内P(empty)操作 while(queue.size() >= 10) { try { notFull.await(); // 放弃锁(P(empty)失败有放弃锁的动作所以P(mutex)可以在P(empty)之前) } catch (InterruptedException e) { e.printStackTrace(); } } Message message = new Message(id++); queue.addLast(message); message.showEnter(); notEmpty.signalAll(); lock.unlock(); } } } class Consumer implements Runnable{ public void run() { while(true) { // P(mutex) lock.lock(); // while内P(empty)操作 while(queue.size() == 0) { try { notEmpty.await(); // 放弃锁 } catch (InterruptedException e) { e.printStackTrace(); } } queue.removeFirst().showMove(); notFull.signalAll(); lock.unlock(); } } } @Test public void test() throws InterruptedException { ProducerAndConsumer producerAndConsumer = new ProducerAndConsumer(); Thread thread_1 = new Thread(new Producer()); Thread thread_2 = new Thread(new Consumer()); thread_1.start(); thread_2.start(); Thread.sleep(1000); } } class Message { Integer id; public Message() { } public Message(int id){ this.id = id; } public void showEnter() { System.out.println("第 " + id + " 号消息进入"); } public void showMove() { System.out.println("第 " +this.id + " 号消息被移除"); } }
1.5 死锁
四个必要条件:
- 互斥:进程对资源的互斥访问
- 不可剥夺:占有资源的进程除非主动放弃该资源,否则不会被其它进程剥夺
- 请求并保持:处于死锁状态的进程至少拥有一个资源,且在申请至少一个资源
- 循环等待
二、内存管理
内存分配发生在将磁盘存储的程序加载到内存运行,形成进程时,针对分配方式不同分为连续分配和非连续分配方式
2.1 连续分配方式
连续分配方式指的是对单个进程而言,在装入内存时占用连续的空间;
但是空间连续只是相对于单个进程来说,多个进程需要装入内存时,具体为每个进程分配哪一块内存,空闲内存的组织管理方式是可以有变化的,根据空闲内存管理方式可以把连续分配方式分为固定分区分配和动态分区分配
2.1.1 固定分区分配
将内存划分为若干分区,每个进程占用一个分区(无论装入内存之后是否有剩余),单个分区内剩余的空间不填充任何程序,这部分剩余空间称之为内部碎片(内部是相对于分区来说)
固定分区指的是分区大小确定之后不会改变,并不意味着所有分区大小都相同
下图为固定分区分配方式示意图:
2.1.2 动态分区分配(重要)
所有进程按照某个分配算法分配到大小不一的连续内存空间中,动态分配已经没有分区的概念,所有可用内存叫做空闲分区
为了支持动态分区分配需要维护空闲分区的链表,记录每个空闲分区的起始地址(也就相当于记录了大小信息),并根据使用的分区分配算法来为链表排序,利用以上技术完成给请求空间的程序进行分配空间的工作
-
分配算法
- 首次适应
- 最佳适应
- 最坏适应
- 邻近适应
-
弊端:相对于固定分区,产生外部碎片("外部"是相对于进程来说),管理开销增大
-
动态分区分配示意图:
2.2 非连续分配方式(超级重要)
2.2.1 分页管理
2.2.2 分段管理
2.2.3 段页式
2.2.4 虚拟内存技术
虚拟内存技术和传统的分页,分段管理方式不同之处在于程序并不是一次性全部调入内存;
以请求分页管理为例,基于传统的分页管理之上,使用请求分页存储管理,实现虚拟内存,者项技术的必要组件为:
-
新的页表
新增
- 状态位:指示该页是否已经被调入内存,供程序访问时参考
- 修改位(标识该页是否被修改过):因为虚拟内存技术涉及到页面换入换出,换出时如果该页被修改需要写回外存
- 外存地址:换入换出均需要使用
- 访问字段:可以记录本页多久没有被访问,供页面置换算法参考
普通分页管理不需要换入换出页面,所以不需要知道该页是否在内存中(肯定会在),所以不需要状态位,自然也就不需要知道每一页对应的外存地址(只要知道外存存放程序的基地址即可)。至于修改位,普通分页管理下即使有页面修改了结果存储在内存中也不需要立刻写回外存,可以等到对应的内存块要被使用的时候再写回,所以也不需要
-
缺页中断机制
-
新的地址变换机制:基于新的页表结构来实现,具体地址变换过程可以见下图
请求分页过程示意:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UevzDUjK-1648034331023)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20220205173214630.png)]
- 页面置换算法
- 最佳置换
- 先进先出
- 找到在内存最久的页面换出
- 最近最久未使用
- 直接找现有页面中距离上次访问该页面之后经历时间最长的页面替换
- 时钟置换