程序员必备的操作系统知识
进程与线程
- 什么是进程?
- 现代操作系统在运行一个程序时,会为其创建一个进程;例如,启动一个Java程序,操作系 统就会创建一个Java进程。进程是OS(操作系统)资源分配的最小单位。
- 什么是线程?
- 线程是操作系统CPU调度的最小单元,也叫轻量级进程。
- 一个进程可以参加多个线程,这些线程都拥有自己的计数器,堆栈,局部变量等属性。并且能够访问公共内存,CPU在这些线程上进行高速切换,让使用者感觉到这些线程在同 时执行,即并发的概念。
- 线程上下文切换过程
进程与线程的区别
- 一个程序至少有一个进程;一个进程至少有一个线程;
- 线程的划分尺度小于进程,使得多线程程序的并发性高;
- 进程在执行过程中拥有独立的内存单元,而多个线程共享内存;
- 每个独立的线程有一个程序运行的入口,顺序执行序列和程序的出口。但是线程不能够独立运行,必须依赖在应用程序中,由应用程序提供多个线程执行控制。
- 进程的活泼性比线程小;
- 进程仅仅是资源分配的基本单位。而线程是独立调度,分派的基本单位
- 进程的创建,撤销,切换开销远比线程大
什么是临界区、如何解决冲突?
每个进程中访问临界资源的那段程序称为临界区。每次只准许一个进程进入临界区,进入后不允许其他进程进入。如果有若干个进程要求进入空闲的临界区,一次仅允许一个进程进入。任何时候,处于临界区的进程不能多于一个。如果已有进程进入自己的临界区,则其他试图进入临界区的进程必须等待。进入临界区的进程要在有限时间内退出,以便其他进程能及时进入自己临界区。如果不能进入自己的临界区,就应该让出CPU,避免进程出现忙等现象。
进程间通信有哪些方式?它们的区别?
1)管道;
2)命名管道;
3)信号;
4)信号量;
5)消息队列;
6)共享内存
7)套接字;
几种方式的比较:
管道:速度慢、容量有限,半双工通道,只能一端发送,一端接收,不能两端同时发送数据;
命令管道:全双工通道,两端可以同时发送数据。
消息队列:容量收到系统限制,且要注意第一次读的时候,要考虑上一次没有读完数据的问题。
信号量:不能传递复杂信息,只能用来同步,可以代表系统资源。
共享内存:能够很容易控制容量,速度快,但要保持同步,比如一个进程在写的时候,另一个进程要注意读写的问题,相当于线程中的线程安全
套接字:通过网络通信的方法,进行数据交互。Java中体现为分布式系统。
进程的调度算法有哪些?
- 先来先服务FCFS:此算法的原则是按照作业到达后备作业队列(或进程进入就绪队列)的先后次序选择作业(或进程)
- 短作业优先(SJF:Shortest Process First):主要用于作业调度,他从作业后备序列中挑选所需运行时间最短的作业进入主存运行。
- 时间片轮转调度算法:当某个进程的时间片用完是,调度程序便终止该进程的执行,并将它送到就绪队列的末尾,等待下一时间片再执行。然后把处理机分配给就绪队列中新的队首进程,同时也让他执行一个时间片。这样就可以保证队列中的所有进程,在给定的时间内,均能获得一时间片-处理机执行时间。
- 高响应比优先:按照高响应比(以等待时间+要求运行的时间)/要求运行时间 优先的原则,在每个作业选择投入运行时,先计算此时后备队列中每个作业的响应比RP。选择最大的作业投入运行。
- 优先权调度算法:按照进程的优先权大小来调度。
- 非抢占式优先权算法:在这种方式下,系统一旦把处理机分配给就绪队列中优先权最高的进程后,该进程便一直执行下去,直至完成;或因发生某事件使该进程放弃处理机时,系统方可再将处理机重新分配给另一优先权最高的进程。
- 抢占式优先权调度算法:在这种方式下,系统同样是把处理机分配给优先权最高的进程,使之执行。但在其执行期间,只要又出现了另一个其优先权更高的进程,进程调度程序就立即停止当前进程(原优先权最高的进程)的执行,重新将处理机分配给新到的优先权最高的进程。
- 多级反馈队列调度算法(Unix中使用):略;
内存交换
交换(对换)的基本思想是,把处于等待状态(或在 CPU 调度原则下被剥夺运行权利)的程序从内存移到辅存,把内存空间腾出来,这一过程又叫换出;把准备好竞争 CPU 运行的程序从辅存移到内存,这一过程又称为换入。
程序装入和链接
创建进程首先要将程序和数据装入内存。将用户源程序变为可在内存中执行的程序,通常需要以下几个步骤:
- 编译:由编译程序将用户源代码编译成若干个目标模块。
- 链接:由链接程序将编译后形成的一组目标模块,以及所需库函数链接在一起,形成一个完整的装入模块。
- 装入:由装入程序将装入模块装入内存运行。
程序的链接方式
- 静态链接:在程序运行之前,先将各目标模块及它们所需的库函数链接成一个完整的可执行程序,以后不再拆开。
- 装入时动态链接:将用户源程序编译后所得到的一组目标模块,在装入内存时,釆用边装入边链接的链接方式。
- 运行时动态链接:对某些目标模块的链接,是在程序执行中需要该目标模块时,才对它进行的链接。其优点是便于修改和更新,便于实现对目标模块的共享。
冯诺依曼计算机模型详解
- 计算机在运行时,先从内存汇总取出一条指令,通过控制器的译码,按指令的要求,从存储器汇总取出数据进行指定的运算和逻辑操作等加工,然后在按地址将结果送到内存中去。接下来再取出第二条指令,在控制器的指挥下完成规定操作,依次进行下去,直到遇见停止指令。
计算机五大核心组成部分
- 控制器
其功能是对程序规定的控制信息进行解释,控制程序的运行,数据的加载等; - 运算器
其功能室对数据进行各种的算术运算和逻辑运算,即对数据进行加工处理; - 存储器
其功能是存储程序,数据和各种信息,命令等信息; - 输入设备
输入设备和输出设备统称为外部设备。其作用是将信息输入到计算机。常见输入设备有键盘,鼠标器等; - 输出设备
把计算机的中间结果或者最后结果输出出来。常见输出设备:打印机,显示终端CRT等。
下图-冯诺依曼计算机模型图
其应用在现代计算机的硬件结果设计如下:
重点是CPU、内存。
CPU指令结构
CPU内部结构
- 控制单元
- 运算单元
- 数据单元(存储单元)
控制单元
控制单元是整个CPU的指挥控制中心,对协调整个电脑有序工作极为重要。
作用:它根据用户预先编好的程序,依次从存储器中取出各条指令,放在指令寄存器IR中,通过指令译码(分析)确定应该进行什么操作,然后通过 操作控制器OC,按确定的时序,向相应的部件发出微操作控制信号。
运算单元
运算单元是运算器的核心,可以执行算术运算(加减乘除等)和逻辑运算(移位等)。
相对于控制单元而言,运算器接收控制单元的命令而进行动作,即运算单元所进行的全部操作都是由控制单元发出的控制信号来指挥的
存储单元
- 存储单元包括CPU片内缓存Cache,寄存器组,是CPU存放数据的地方,CPU访问寄存器的耗时要比访问内存的耗时短。
- 寄存器是CPU内部的元件,寄存器拥有非常高的读写速度,从而提高了CPU的工作速度。
- 寄存器组可分为专用寄存器和通用寄存器。
CPU缓存架构
现代CPU为了提升执行效率,减少CPU与内存的交互(交互影响CPU效率),一般在CPU上集 成了多级缓存架构,常见的为三级缓存结构
- L1 Cache,分为数据缓存和指令缓存,逻辑核独占
- L2 Cache ,物理核独占
- L3 Cache, 所有物理核共享
缓存的最小的存储区块是由 缓存行(cacheline) 组成的。缓存行的大小通常为64byte;
L2缓存大小为1024KB,缓存行总数为1024 * 1000 / 64 =16000;
CPU读取存储器数据过程
- CPU要取寄存器X的值,只需要一步:直接读取。
- CPU要取L1 cache的某个值,需要1-3步(或者更多):把cache行锁住,把某个数据拿来,解 锁,如果没锁住就慢了。
- CPU要取L2 cache的某个值,先要到L1 cache里取,L1当中不存在,在L2里,L2开始加锁,加锁以后,把L2里的数据复制到L1,再执行读L1的过程,上面的3步,再解锁。
- CPU取L3 cache的也是一样,只不过先由L3复制到L2,从L2复制到L1,从L1到CPU。
- CPU取内存则最复杂:通知内存控制器占用总线带宽,通知内存加锁,发起内存读请求,等待回应,回应数据保存到L3(如果没有就到L2),再从L3/2到L1,再从L1到CPU,之后解除总线锁定。
CPU为何要有高速缓存
现代CPU快速发展,而内存和硬盘的发展速度远不及CPU。而高性能的硬盘和内存昂贵。然而CPU的高度运算需要高速的数据。
为了解决这个问题,CPU中内置了少量的高速缓存以解决I\O速度和CPU运算速度之间的不匹配问题。 在CPU访问存储设备时,无论是存取数据抑或存取指令,都趋于聚集在一片连续的区域中,这就被称为局部性原理
- 时间局部性:如果一个信息项正在被访问,那么近期他可能还会再次被访问。
- 空间局部性:如果一个存储的位置被引用,那么将他的附近位置的数据也会被引用。。
package com.juc;
public class TwoDimensionalArraySum {
private static final int RUNS = 100;
private static final int DIMENSION_1 = 1024 * 1024;
private static final int DIMENSION_2 = 6;
private static long[][] longs;
public static void main(String[] args) {
longs = new long[DIMENSION_1][];
for (int i = 0; i < DIMENSION_1; i++) {
longs[i] = new long[DIMENSION_2];
for (int j = 0; j < DIMENSION_2; j++) {
longs[i][j] = 1L;
}
}
System.out.println("Array初始化完毕....");
long sum = 0L;
long start = System.currentTimeMillis();
for (int r = 0; r < RUNS; r++) {
for (int i = 0; i < DIMENSION_1; i++) {//DIMENSION_1=1024*1024
for (int j=0;j<DIMENSION_2;j++){//6
sum+=longs[i][j];
}
}
}
System.out.println("spend time1:"+(System.currentTimeMillis()- start));
System.out.println("sum1:"+sum);
sum = 0L;
start = System.currentTimeMillis();
for (int r = 0; r < RUNS; r++) {
for (int j=0;j<DIMENSION_2;j++) {//6
for (int i = 0; i < DIMENSION_1; i++){//1024*1024
sum+=longs[i][j];
}
}
}
System.out.println("spend time2:"+(System.currentTimeMillis()- start));
System.out.println("sum2:"+sum);
}
}
带有高速缓存的CPU执行计算的流程
1. 程序以及数据被加载到主内存
2. 指令和数据被加载到CPU的高速缓存 (L3->L2->L1);
3. CPU执行指令,把结果写到高速缓存 (L1->L2->L3);
4. 高速缓存中的数据写回主内存
CPU运行安全等级
CPU有4个运行级别,分别为:
- ring0
- ring1
- ring2
- ring3
Linux与Windows只用到了2个级别:ring0、ring3,操作系统内部内部程序指令通常运行在ring0级别,操作系统以外的第三方程序运行在ring3级别,第三方程序如果要调用操作 系统内部函数功能,由于运行安全级别不够,必须切换CPU运行状态,从ring3切换到ring0, 然后执行系统函数。
下面我大概梳理一下JVM创建线程CPU的工作过程
- step1:CPU从ring3切换ring0创建线程
- step2:创建完毕,CPU从ring0切换回ring3
- step3:线程执行JVM程序
- step4:线程执行完毕,销毁还得切会ring0
操作系统内存管理
执行空间保护
操作系统有用户空间和和内核空间,目的为了程序运行安全隔离和稳定。以 32位操作系统4G大小的内存空间为例
- Linux为内核代码和数据结构预留了几个页框,这些页永远不会被转出到磁盘上。
- 橙色部分为内核空间,用来存放操作系统内核的程序和数据等。
- 绿色部分为用户空间,用来存放用户应用程序和数据等。
用户态与内核态
- 进程与线程只能运行在用户方式(usermode)或内核方式(kernelmode)下。用户程序运行在用户方式下,而系统调用运行在内核方式下。
- 在这两种方式下所用的堆栈不一样:用户方式下用的是 一般的堆栈(用户空间的堆栈),而内核方式下用的是固定大小的堆栈(内核空间的对战,一 般为一个内存页的大小),即每个进程与线程其实有两个堆栈,分别运行与用户态与内核态
- CPU调度的基本单位线程,也划分为:
- 内核线程模型(KLT)
- 用户线程模型(ULT)
内核线程模型
内核线程(KLT):
- 系统内核管理线程,内核保存线程的状态和上下文状态。
- 线程阻塞不会引起进程阻塞。
JAVA使用的是KLT模式,线程管理需要从用户态切换到内核态,每一个Java线程对应操作系统内核的一个线程;因此Java的线程管理效率不如ULT模型,需要状态切换,所以Java线程管理是重量级别的操作
用户线程模型
用户线程(ULT):
- 应用提供创建,同步,调度,管理线程的函数来控制用户线程。
- 不需要用户态到内核态的切换,所以速度比KLT快。
- 内核对ULT无感知,线程阻塞,整个进程阻塞。
因为CPU只保留了用户线程的进程信息,如果用户线程阻塞,CPU不知道进程有哪些线程,不会调度该进程的其他线程到CPU上,因此,用户线程阻塞,整个进程阻塞。
虚拟机指令集架构
虚拟机指令集架构主要分两种:
- 栈指令集架构
- 寄存器指令集架构
寄存器指令集架构
- 指令集架构则完全依赖硬件,可移植性差。
- 性能优秀和执行更高效
- 花费更少的指令去完成一项操作。
- 在大部分情况下,基于寄存器架构的指令集往往都以一地址指令、二地址指令和三 地址指令为主,而基于栈式架构的指令集却是以零地址指令为主
- 三地址指令:op dest, src1, src2 ,将源地址src1,src2进行操作放入目的地址dest;
- 二地址指令:op dest, src,支持二元操作,就只能把其中一个源同时也作为目标。上面的add a, b在执行过后,就会破坏a原有的值,而b的值保持不变。x86系列的处理器就是二地址形式的。
栈指令集架构
-
设计和实现更简单,适用于资源受限的系统
-
避开了寄存器的分配难题:使用零地址指令方式分配
-
指令流中的指令大部分是零地址指令,其执行过程依赖与操作栈,指令集更小,编译器 容易实现;
-
不需要硬件支持,可移植性更好,更好实现跨平台。
-
Java为了实现一处编译,到处运行的想法,使用的是栈指令集架构。
-
iconst_1 就是将int常量1压入操作数栈;不涉及目标对象地址,源对象地址,以及操作地址。
-
iload_3 从局部变量表的3槽中装载int类型值。
中断和轮询
轮询:
它定时对各种设备轮流询问一遍有无处理要求。轮流询问之后,有要求的就加以处理。在处理I/O设备的要求之后,处理机返回继续工作。
轮询效率低、等待时间很长,CPU利用率低;
中断:
容易遗漏一些问题,CPU利用率高。