ljlao ljlao
首页
ljlao总结
  • Java8
  • Java IO
  • Java基础
  • java基础01
  • java基础02
  • java基础03
  • Java虚拟机
  • Java集合
  • Servlet
  • Java高并发
  • 多线程
  • 并发编程面试专栏
  • 数据结构与算法
  • 操作系统
  • 编译原理
  • 计算机网络
  • Shiro
  • MyBatis
  • Spring
  • Spring Boot
  • Spring Cloud
  • RabbitMQ
  • RocketMQ
  • Kafka
  • Elasticsearch
  • Netty
  • Dubbo
  • ZooKeeper
  • 消息队列
  • 分布式
  • 微服务
  • 数据库
  • MySQL
  • Mycat
  • Redis
  • MongoDB
  • Cassandra
  • Hbase
首页
ljlao总结
  • Java8
  • Java IO
  • Java基础
  • java基础01
  • java基础02
  • java基础03
  • Java虚拟机
  • Java集合
  • Servlet
  • Java高并发
  • 多线程
  • 并发编程面试专栏
  • 数据结构与算法
  • 操作系统
  • 编译原理
  • 计算机网络
  • Shiro
  • MyBatis
  • Spring
  • Spring Boot
  • Spring Cloud
  • RabbitMQ
  • RocketMQ
  • Kafka
  • Elasticsearch
  • Netty
  • Dubbo
  • ZooKeeper
  • 消息队列
  • 分布式
  • 微服务
  • 数据库
  • MySQL
  • Mycat
  • Redis
  • MongoDB
  • Cassandra
  • Hbase
  • Java基础

    • java基础01
      • 992.简述 java 内存分配与回收策率以及 Minor GC 和 Major GC-Java面试题
      • 991.什么是类加载器,类加载器有哪些?-Java面试题
      • 990.类加载器双亲委派模型机制?-Java面试题
      • 989.简述 java 类加载机制?-Java面试题
      • 988.java 类加载过程?-Java面试题
      • 987.java 内存模型-Java面试题
      • 986.java 中垃圾收集的方法有哪些?-Java面试题
      • 985.简述 java 垃圾回收机制?-Java面试题
      • 984.如和判断一个对象是否存活?(或者 GC 对象的判定方 法)-Java面试题
      • 983.JVM 内存分哪几个区,每个区的作用是什么?-Java面试题
      • 982.类加载的几个过程:-Java面试题
      • 981.几种常用的内存调试工具:jmap、jstack、jconsole、jhat-Java面试题
      • 980.Minor GC 与 Full GC 分别在什么时候发生?-Java面试题
      • 979.GC 收集器有哪些?CMS 收集器与 G1 收集器的特点。-Java面试题
      • 978.GC 的三种收集方法:标记清除、标记整理、复制算法的原理与特点,分别用 在什么地方,如果让你优化收集方法,有什么思路?-Java面试题
      • 977.SafePoint 是什么-Java面试题
      • 976.GC 的两种判定方法:-Java面试题
      • 975.对象创建方法,对象的内存分配,对象的访问定位。-Java面试题
      • 974.堆里面的分区:Eden,survival (from+ to),老年代,各自的特点。-Java面试题
      • 973.内存模型以及分区,需要详细到每个区放什么。-Java面试题
      • 972. 类加载器双亲委派模型机制?-Java面试题
      • 971.什么是类加载器,类加载器有哪些?-Java面试题
      • 970.Java 中垃圾收集的方法有哪些?-Java面试题
      • 969.JVM 的永久代中会发生垃圾回收么?-Java面试题
      • 968.简述 Java 内存分配与回收策率以及 Minor GC 和 Major GC。-Java面试题
      • 967.在 Java 中,对象什么时候可以被垃圾回收?-Java面试题
      • 966.串行(serial)收集器和吞吐量(throughput)收集器的区别 是什么?-Java面试题
      • 965.什么是分布式垃圾回收(DGC)?它是如何工作的?-Java面试题
      • 964. 如果对象的引用被置为 null,垃圾收集器是否会立即释放对象占 用的内存?-Java面试题
      • 963.finalize() 方法什么时候被调用?析构函数 (finalization) 的 目的是什么?-Java面试题
      • 962.System.gc() 和 Runtime.gc() 会做什么事情?-Java面试题
      • 961.深拷贝和浅拷贝。-Java面试题
      • 960.Java 中会存在内存泄漏吗,请简单描述。-Java面试题
      • 959. 垃圾回收器的基本原理是什么?垃圾回收器可以马上回收内存吗? 有什么办法主动通知虚拟机进行垃圾回收?-Java面试题
      • 958.垃圾回收的优点和原理。并考虑 2 种回收机制。-Java面试题
      • 957.如何判断一个对象是否存活?(或者 GC 对象的判定方法)-Java面试题
      • 956.简述 Java 垃圾回收机制。-Java面试题
      • 955.GC 是什么? 为什么要有 GC?-Java面试题
      • 954.Java 堆的结构是什么样子的?什么是堆中的永久代(Perm Gen space)?-Java面试题
      • 953.Java 内存分配。-Java面试题
      • 952.描述一下 JVM 加载 Class 文件的原理机制?-Java面试题
      • 951.Java 类加载过程?-Java面试题
      • ZooKeeper基本概述-Java面试题
      • 如何高效的学习技术?-Java面试题
      • 使用SpringAPI进行验证-Java面试题
      • Spring中的Null-Safety-Java面试题
      • 一文了解ConfigurationConditon接口-Java面试题
      • SpringAOP扫盲-Java面试题
      • @Bean注解全解析-Java面试题
      • BeanFactory和ApplicationContext的异同-Java面试题
      • 程序员们平时都喜欢逛什么论坛呢?-Java面试题
      • 操作系统之死锁-Java面试题
      • 操作系统网站推荐-Java面试题
      • 操作系统之文件系统-Java面试题
      • 操作系统必知面试题-Java面试题
      • 关于操作系统,你必备的名词-Java面试题
    • java基础02
    • java基础03
    • Java虚拟机
    • Java集合
    • Java高并发
    • Servlet
    • 多线程
    • 并发编程面试专栏
    • 操作系统
    • 数据结构与算法
    • 编译原理
    • 计算机网络
    • 设计模式
    • Java IO
    • Java8
    • Java基础
  • java高级

    • MyBatis
    • 消息队列
    • Kafka
    • RabbitMQ
    • Netty
    • Shiro
    • RocketMQ
    • ZooKeeper
    • Dubbo
    • Spring Boot
    • Elasticsearch
    • Spring Cloud
    • Spring
    • 分布式
    • 微服务
  • sql

    • Cassandra
    • MongoDB
    • Mycat
    • MySQL
    • Redis
    • Hbase
    • 数据库
  • 运维

    • Jenkins
    • Docker
    • Kubernetes
    • Linux
    • Maven
    • Nginx
    • Tomcat
    • 云计算
  • 面试题库
  • Java基础
ljlao
2022-03-09
目录

java基础01

# Java基础

# 992.简述 java 内存分配与回收策率以及 Minor GC 和 Major GC-Java面试题


  1. 对象优先在堆的 Eden 区分配。
  2. 大对象直接进入老年代.
  3. 长期存活的对象将直接进入老年代.
    当 Eden 区没有足够的空间进行分配时,虚拟机会执行一次 Minor GC.Minor Gc 通
    常发生在新生代的 Eden 区,在这个区的对象生存期短,往往发生 Gc 的频率较高,
    回收速度比较快;Full Gc/Major GC 发生在老年代,一般情况下,触发老年代 GC
    的时候不会触发 Minor GC,但是通过配置,可以在 Full GC 之前进行一次 Minor
    GC 这样可以加快老年代的回收速度

# 991.什么是类加载器,类加载器有哪些?-Java面试题


实现通过类的权限定名获取该类的二进制字节流的代码块叫做类加载器。
主要有一下四种类加载器:

  1. 启动类加载器(Bootstrap ClassLoader)用来加载 java 核心类库,无法被 java 程序直接引用。
  2. 扩展类加载器(extensions class loader):它用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。
  3. 系统类加载器(system class loader):它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通过ClassLoader.getSystemClassLoader()来获取它。
  4. 用户自定义类加载器,通过继承 java.lang.ClassLoader 类的方式实现。

# 990.类加载器双亲委派模型机制?-Java面试题


当一个类收到了类加载请求时,不会自己先去加载这个类,而是将其委派给父类,由父类去加载,如果此时父类不能加载,反馈给子类,由子类去完成类的加载。


# 989.简述 java 类加载机制?-Java面试题


虚拟机把描述类的数据从 Class 文件加载到内存,并对数据进行校验,解析和初始化,最终形成可以被虚拟机直接使用的 java 类型。


# 988.java 类加载过程?-Java面试题


java 类加载需要经历一下 7 个过程:
加载
加载时类加载的第一个过程,在这个阶段,将完成一下三件事情:

  1. 通过一个类的全限定名获取该类的二进制流。
  2. 将该二进制流中的静态存储结构转化为方法去运行时数据结构。
  3. 在内存中生成该类的 Class 对象,作为该类的数据访问入口。
    验证
    验证的目的是为了确保 Class 文件的字节流中的信息不回危害到虚拟机.在该阶段主要完成以下四钟验证:
  4. 文件格式验证:验证字节流是否符合 Class 文件的规范,如主次版本号是否在当前虚拟机范围内,常量池中的常量是否有不被支持的类型.
  5. 元数据验证:对字节码描述的信息进行语义分析,如这个类是否有父类,是否集成了不被继承的类等。
  6. 字节码验证:是整个验证过程中最复杂的一个阶段,通过验证数据流和控制流的分析,确定程序语义是否正确,主要针对方法体的验证。如:方法中的类型转换是否正确,跳转指令是否正确等。
  7. 符号引用验证:这个动作在后面的解析过程中发生,主要是为了确保解析动作能正确执行。
    准备
    准备阶段是为类的静态变量分配内存并将其初始化为默认值,这些内存都将在方法区中进行分配。准备阶段不分配类中的实例变量的内存,实例变量将会在对象实例化时随着对象一起分配在 Java 堆中。
    public static int value=123;//在准备阶段 value 初始值为 0 。在初始化阶段才会变为 123 。
    解析
    该阶段主要完成符号引用到直接引用的转换动作。解析动作并不一定在初始化动作完成之前,也有可能在初始化之后。
    初始化
    初始化时类加载的最后一步,前面的类加载过程,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的 Java 程序代码。

# 987.java 内存模型-Java面试题


java 内存模型(JMM)是线程间通信的控制机制.JMM 定义了主内存和线程之间抽象关系。
线程之间的共享变量存储在主内存(main memory)中,每个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读/写共享变量的副本。本地内存是JMM 的一个抽象概念,并不真实存在。它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化。Java 内存模型的抽象示意图如下:
从上图来看,线程 A 与线程 B 之间如要通信的话,必须要经历下面 2 个步骤:

  1. 首先,线程 A 把本地内存 A 中更新过的共享变量刷新到主内存中去。
  2. 然后,线程 B 到主内存中去读取线程 A 之前已更新过的共享变量。

# 986.java 中垃圾收集的方法有哪些?-Java面试题


  1. 标记-清除:
    这是垃圾收集算法中最基础的,根据名字就可以知道,它的思想就是标记哪些要被回收的对象,然后统一回收。这种方法很简单,但是会有两个主要问题:

    1. 效率不高,标记和清除的效率都很低;
      2.会产生大量不连续的内存碎片,导致以后程序在分配较大的对象时,由于没有充足的连续内存而提前触发一次 GC 动作。
  2. 复制算法:
    为了解决效率问题,复制算法将可用内存按容量划分为相等的两部分,然后每次只使用其中的一块,当一块内存用完时,就将还存活的对象复制到第二块内存上,然后一次性清楚完第一块内存,再将第二块上的对象复制到第一块。但是这种方式,内存的代价太高,每次基本上都要浪费一般的内存。
    于是将该算法进行了改进,内存区域不再是按照 1:1 去划分,而是将内存划分为8:1:1 三部分,较大那份内存交 Eden 区,其余是两块较小的内存区叫 Survior 区。
    每次都会优先使用 Eden 区,若 Eden 区满,就将对象复制到第二块内存区上,然后清除 Eden 区,如果此时存活的对象太多,以至于 Survivor 不够时,会将这些对象通过分配担保机制复制到老年代中。(java 堆又分为新生代和老年代)
  3. 标记-整理
    该算法主要是为了解决标记-清除,产生大量内存碎片的问题;当对象存活率较高时,也解决了复制算法的效率问题。它的不同之处就是在清除对象的时候现将可回收对象移动到一端,然后清除掉端边界以外的对象,这样就不会产生内存碎片了。
  4. 分代收集
    现在的虚拟机垃圾收集大多采用这种方式,它根据对象的生存周期,将堆分为新生代和老年代。在新生代中,由于对象生存期短,每次回收都会有大量对象死去,那么这时就采用复制算法。老年代里的对象存活率较高,没有额外的空间进行分配担保,所以可以使用标记-整理 或者 标记-清除。

# 985.简述 java 垃圾回收机制?-Java面试题


在 java 中,程序员是不需要显示的去释放一个对象的内存的,而是由虚拟机自行执行。在JVM 中,有一个垃圾回收线程,它是低优先级的,在正常情况下是不会执行的,只有在虚拟机空闲或者当前堆内存不足时,才会触发执行,扫面那些没有被任何引用的对象,并将它们添加到要回收的集合中,进行回收。


# 984.如和判断一个对象是否存活?(或者 GC 对象的判定方 法)-Java面试题


判断一个对象是否存活有两种方法:

  1. 引用计数法
    所谓引用计数法就是给每一个对象设置一个引用计数器,每当有一个地方引用这个对象时,就将计数器加一,引用失效时,计数器就减一。当一个对象的引用计数器为零时,说明此对象没有被引用,也就是“死对象”,将会被垃圾回收.
    引用计数法有一个缺陷就是无法解决循环引用问题,也就是说当对象 A 引用对象 B,对象B 又引用者对象 A,那么此时 A,B 对象的引用计数器都不为零,也就造成无法完成垃圾回收,所以主流的虚拟机都没有采用这种算法。
    2.可达性算法(引用链法)
    该算法的思想是:从一个被称为 GC Roots 的对象开始向下搜索,如果一个对象到 GCRoots 没有任何引用链相连时,则说明此对象不可用。在 java 中可以作为 GC Roots 的对象有以下几种:

    1. 虚拟机栈中引用的对象
    2. 方法区类静态属性引用的对象
    3. 方法区常量池引用的对象
    4. 本地方法栈 JNI 引用的对象

虽然这些算法可以判定一个对象是否能被回收,但是当满足上述条件时,一个对象比不一定会被回收。当一个对象不可达 GC Root 时,这个对象并不会立马被回收,而是出于一个死缓的阶段,若要被真正的回收需要经历两次标记如果对象在可达性分析中没有与 GC Root 的引用链,那么此时就会被第一次标记并且进行一次筛选,筛选的条件是是否有必要执行 finalize()方法。当对象没有覆盖 finalize()方法或者已被虚拟机调用过,那么就认为是没必要的。
如果该对象有必要执行 finalize()方法,那么这个对象将会放在一个称为 F-Queue 的对队列中,虚拟机会触发一个 Finalize()线程去执行,此线程是低优先级的,并且虚拟机不会承诺一直等待它运行完,这是因为如果 finalize()执行缓慢或者发生了死锁,那么就会造成 FQueue 队列一直等待,造成了内存回收系统的崩溃。GC 对处于 F-Queue 中的对象进行第二次被标记,这时,该对象将被移除”即将回收”集合,等待回收。


# 983.JVM 内存分哪几个区,每个区的作用是什么?-Java面试题


java 虚拟机主要分为以下一个区:
方法区:

  1. 有时候也成为永久代,在该区内很少发生垃圾回收,但是并不代表不发生 GC,在这里进行的 GC 主要是对方法区里的常量池和对类型的卸载
  2. 方法区主要用来存储已被虚拟机加载的类的信息、常量、静态变量和即时编译器编译后的代码等数据。
  3. 该区域是被线程共享的。
  4. 方法区里有一个运行时常量池,用于存放静态编译产生的字面量和符号引用。该常量池具有动态性,也就是说常量并不一定是编译时确定,运行时生成的常量也会存在这个常量池中。
    虚拟机栈:
  5. 虚拟机栈也就是我们平常所称的栈内存,它为 java 方法服务,每个方法在执行的时候都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接和方法出口等信息。
  6. 虚拟机栈是线程私有的,它的生命周期与线程相同。
  7. 局部变量表里存储的是基本数据类型、returnAddress 类型(指向一条字节码指令的地址)和对象引用,这个对象引用有可能是指向对象起始地址的一个指针,也有可能是代表对象的句柄或者与对象相关联的位置。局部变量所需的内存空间在编译器间确定
    4.操作数栈的作用主要用来存储运算结果以及运算的操作数,它不同于局部变量表通过索引来访问,而是压栈和出栈的方式
    5.每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接.动态链接就是将常量池中的符号引用在运行期转化为直接引用。
    本地方法栈
    本地方法栈和虚拟机栈类似,只不过本地方法栈为 Native 方法服务。
    堆
    java 堆是所有线程所共享的一块内存,在虚拟机启动时创建,几乎所有的对象实例都在这里创建,因此该区域经常发生垃圾回收操作。
    程序计数器
    内存空间小,字节码解释器工作时通过改变这个计数值可以选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理和线程恢复等功能都需要依赖这个计数器完成。该内存区域是唯一一个 java 虚拟机规范没有规定任何 OOM 情况的区域。

# 982.类加载的几个过程:-Java面试题


加载、验证、准备、解析、初始化。然后是使用和卸载了通过全限定名来加载生成 class 对象到内存中,然后进行验证这个 class 文件,包括文件格式校验、元数据验证,字节码校验等。准备是对这个对象分配内存。解析是将符号引用转化为直接引用(指针引用),初始化就是开始执行构造器的代码


# 981.几种常用的内存调试工具:jmap、jstack、jconsole、jhat-Java面试题


jstack 可以看当前栈的情况,jmap 查看内存,jhat 进行 dump 堆的信息mat(eclipse 的也要了解一下)


# 980.Minor GC 与 Full GC 分别在什么时候发生?-Java面试题


新生代内存不够用时候发生 MGC 也叫 YGC,JVM 内存不够的时候发生 FGC


# 979.GC 收集器有哪些?CMS 收集器与 G1 收集器的特点。-Java面试题


并行收集器:串行收集器使用一个单独的线程进行收集,GC 时服务有停顿时间串行收集器:次要回收中使用多线程来执行
CMS 收集器是基于“标记—清除”算法实现的,经过多次标记才会被清除G1 从整体来看是基于“标记—整理”算法实现的收集器,从局部(两个 Region 之间)上来看是基于“复制”算法实现的


# 978.GC 的三种收集方法:标记清除、标记整理、复制算法的原理与特点,分别用 在什么地方,如果让你优化收集方法,有什么思路?-Java面试题


先标记,标记完毕之后再清除,效率不高,会产生碎片复制算法:分为 8:1 的 Eden 区和 survivor 区,就是上面谈到的 YGC标记整理:标记完毕之后,让所有存活的对象向一端移动


# 977.SafePoint 是什么-Java面试题


比如 GC 的时候必须要等到 Java 线程都进入到 safepoint 的时候 VMThread 才能开始执行 GC,

  1. 循环的末尾 (防止大循环的时候一直不进入 safepoint,而其他线程在等待它进入safepoint)
  2. 方法返回前
  3. 调用方法的 call 之后
  4. 抛出异常的位置

# 976.GC 的两种判定方法:-Java面试题


引用计数法:指的是如果某个地方引用了这个对象就+1,如果失效了就-1,当为 0 就会回收但是 JVM 没有用这种方式,因为无法判定相互循环引用(A 引用 B,B 引用 A)的情况
引用链法: 通过一种 GC ROOT 的对象(方法区中静态变量引用的对象等-static 变量)来判断,如果有一条链能够到达 GC ROOT 就说明,不能到达 GC ROOT 就说明可以回收


# 975.对象创建方法,对象的内存分配,对象的访问定位。-Java面试题


new 一个对象


# 974.堆里面的分区:Eden,survival (from+ to),老年代,各自的特点。-Java面试题


堆里面分为新生代和老生代(java8 取消了永久代,采用了 Metaspace),新生代包含 Eden+Survivor 区,survivor 区里面分为 from 和 to 区,内存回收时,如果用的是复制算法,从 from 复制到 to,当经过一次或者多次 GC 之后,存活下来的对象会被移动到老年区,当 JVM 内存不够用的时候,会触发 Full GC,清理 JVM 老年区当新生区满了之后会触发 YGC,先把存活的对象放到其中一个 Survice区,然后进行垃圾清理。因为如果仅仅清理需要删除的对象,这样会导致内存碎片,因此一般会把 Eden 进行完全的清理,然后整理内存。那么下次 GC 的时候,就会使用下一个 Survive,这样循环使用。如果有特别大的对象,新生代放不下,就会使用老年代的担保,直接放到老年代里面。因为 JVM 认为,一般大对象的存活时间一般比较久远。


# 973.内存模型以及分区,需要详细到每个区放什么。-Java面试题


JVM 分为堆区和栈区,还有方法区,初始化的对象放在堆里面,引用放在栈里面,class 类信息常量池(static 常量和 static 变量)等放在方法区new:

  1. 方法区:主要是存储类信息,常量池(static 常量和 static 变量),编译后的代码(字节码)等数据
  2. 堆:初始化的对象,成员变量 (那种非 static 的变量),所有的对象实例和数组都要在堆上分配
  3. 栈:栈的结构是栈帧组成的,调用一个方法就压入一帧,帧上面存储局部变量表,操作数栈,方法出口等信息,局部变量表存放的是 8 大基础类型加上一个应用类型,所以还是一个指向地址的指针
  4. 本地方法栈:主要为 Native 方法服务
  5. 程序计数器:记录当前线程执行的行号

# 972. 类加载器双亲委派模型机制?-Java面试题


当一个类收到了类加载请求时,不会自己先去加载这个类,而是将其委派给父类,由父类去加载,如果此时父类不能加载,反馈给子类,由子类去完成类的加载。


# 971.什么是类加载器,类加载器有哪些?-Java面试题


实现通过类的权限定名获取该类的二进制字节流的代码块叫做类加载器。
主要有一下四种类加载器:

  1. 启动类加载器(Bootstrap ClassLoader)用来加载 Java 核心类库,无法被 Java 程序直接引用。
  2. 扩展类加载器(extensions class loader):它用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。
  3. 系统类加载器(system class loader):它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通过ClassLoader.getSystemClassLoader() 来获取它。
  4. 用户自定义类加载器,通过继承 java.lang.ClassLoader 类的方式实现。

# 970.Java 中垃圾收集的方法有哪些?-Java面试题


标记 – 清除:这是垃圾收集算法中最基础的,根据名字就可以知道,它的思想就是标记哪些要被回收的对象,然后统一回收。这种方法很简单,但是会有两个主要问题:

  1. 效率不高,标记和清除的效率都很低;
  2. 会产生大量不连续的内存碎片,导致以后程序在分配较大的对象时,由于没有充足的连续内存而提前触发一次 GC 动作。
    复制算法:为了解决效率问题,复制算法将可用内存按容量划分为相等的两部分,然后每次只使用其中的一块,当一块内存用完时,就将还存活的对象复制到第二块内存上,然后一次性清楚完第一块内存,再将第二块上的对象复制到第一块。但是这种方式,内存的代价太高,每次基本上都要浪费一般的内存。于是将该算法进行了改进,内存区域不再是按照 1:1 去划分,而是将内存划分为 8:1:1 三部分,较大那份内存交 Eden 区,其余是两块较小的内存区叫 Survior 区。每次都会优先使用 Eden 区,若 Eden 区满,就将对象复制到第二块内存区上,然后清除 Eden 区,如果此时存活的对象太多,以至于 Survivor 不够时,会将这些对象通过分配担保机制复制到老年代中。(java 堆又分为新生代和老年代)
    标记 – 整理:该算法主要是为了解决标记 – 清除,产生大量内存碎片的问题;当对象存活率较高时,也解决了复制算法的效率问题。它的不同之处就是在清除对象的时候现将可回收对象移动到一端,然后清除掉端边界以外的对象,这样就不会产生内存碎片了。
    分代收集:现在的虚拟机垃圾收集大多采用这种方式,它根据对象的生存周期,将堆分为新生代和老年代。在新生代中,由于对象生存期短,每次回收都会有大量对象死去,那么这时就采用复制算法。老年代里的对象存活率较高,没有额外的空间进行分配担保。

# 969.JVM 的永久代中会发生垃圾回收么?-Java面试题


垃圾回收不会发生在永久代,如果永久代满了或者是超过了临界值,会触发完全垃圾回收(Full GC)。
注:Java 8 中已经移除了永久代,新加了一个叫做元数据区的native 内存区。


# 968.简述 Java 内存分配与回收策率以及 Minor GC 和 Major GC。-Java面试题


  1. 对象优先在堆的 Eden 区分配
  2. 大对象直接进入老年代
  3. 长期存活的对象将直接进入老年代

当 Eden 区没有足够的空间进行分配时,虚拟机会执行一次Minor GC。Minor GC 通常发生在新生代的 Eden 区,在这个区的对象生存期短,往往发生 Gc 的频率较高,回收速度比较快;Full GC/Major GC 发生在老年代,一般情况下,触发老年代 GC 的时候不会触发 Minor GC,但是通过配置,可以在 Full GC 之前进行一次 Minor GC 这样可以加快老年代的回收速度。


# 967.在 Java 中,对象什么时候可以被垃圾回收?-Java面试题


当对象对当前使用这个对象的应用程序变得不可触及的时候,这个对象就可以被回收了。


# 966.串行(serial)收集器和吞吐量(throughput)收集器的区别 是什么?-Java面试题


吞吐量收集器使用并行版本的新生代垃圾收集器,它用于中等规模和大规模数据的应用程序。 而串行收集器对大多数的小应用(在现代处理器上需要大概 100M 左右的内存)就足够了。


# 965.什么是分布式垃圾回收(DGC)?它是如何工作的?-Java面试题


DGC 叫做分布式垃圾回收。RMI 使用 DGC 来做自动垃圾回收。因为 RMI 包含了跨虚拟机的远程对象的引用,垃圾回收是很困难的。DGC 使用引用计数算法来给远程对象提供自动内存管理。


# 964. 如果对象的引用被置为 null,垃圾收集器是否会立即释放对象占 用的内存?-Java面试题


不会,在下一个垃圾回收周期中,这个对象将是可被回收的。


# 963.finalize() 方法什么时候被调用?析构函数 (finalization) 的 目的是什么?-Java面试题


垃圾回收器(garbage colector)决定回收某对象时,就会运行该对象的 finalize() 方法 但是在 Java 中很不幸,如果内存总是充足的,那么垃圾回收可能永远不会进行,也就是说 filalize() 可能永远不被执行,显然指望它做收尾工作是靠不住的。 那么finalize() 究竟是做什么的呢? 它最主要的用途是回收特殊渠道申请的内存。Java 程序有垃圾回收器,所以一般情况下内存问题不用程序员操心。但有一种 JNI(Java Native Interface)调用non-Java 程序(C 或 C++), finalize() 的工作就是回收这部分的内存。


# 962.System.gc() 和 Runtime.gc() 会做什么事情?-Java面试题


这两个方法用来提示 JVM 要进行垃圾回收。但是,立即开始还是延迟进行垃圾回收是取决于 JVM 的。


# 961.深拷贝和浅拷贝。-Java面试题


简单来讲就是复制、克隆。

Person p=new Person(“张三”);

浅拷贝就是对对象中的数据成员进行简单赋值,如果存在动态成员或者指针就会报错。
深拷贝就是对对象中存在的动态成员或指针重新开辟内存空间。


# 960.Java 中会存在内存泄漏吗,请简单描述。-Java面试题


所谓内存泄露就是指一个不再被程序使用的对象或变量一直被占据在内存中。Java 中有垃圾回收机制,它可以保证一对象不再被引用的时候,即对象变成了孤儿的时候,对象将自动被垃圾回收器从内存中清除掉。由于 Java 使用有向图的方式进行垃圾回收管理,可以消除引用循环的问题,例如有两个对象,相互引用,只要它们和根进程不可达的,那么 GC 也是可以回收它们的,例如下面的代码可以看到这种情况的内存回收:

import java.io.IOException; public class GarbageTest { /** * @param args * @throws IOException */ public static void main(String[] args) throws IOException { // TODO Auto-generated method stub try { gcTest(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } System.out.println("has exited gcTest!"); System.in.read(); System.in.read(); System.out.println("out begin gc!"); for(int i=0;i<100;i++) { System.gc(); System.in.read(); System.in.read(); } } private static void gcTest() throws IOException { System.in.read(); System.in.read(); Person p1 = new Person(); System.in.read(); System.in.read(); Person p2 = new Person(); p1.setMate(p2); p2.setMate(p1); System.out.println("before exit gctest!"); System.in.read(); System.in.read(); System.gc(); System.out.println("exit gctest!"); } private static class Person { byte[] data = new byte[20000000]; Person mate = null; public void setMate(Person other) { mate = other; } } }

Java 中的内存泄露的情况:长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄露,尽管短生命周期对象已经不再需要,但是因为长生命周期对象持有它的引用而导致不能被回收,这就是 Java 中内存泄露的发生场景,通俗地说,就是程序员可能创建了一个对象,以后一直不再使用这个对象,这个对象却一直被引用,即这个对象无用但是却无法被垃圾回收器回收的,这就是 java 中可能出现内存泄露的情况,例如,缓存系统,我们加载了一个对象放在缓存中 (例如放在一个全局 map 对象中),然后一直不再使用它,这个对象一直被缓存引用,但却不再被使用。
检查 Java 中的内存泄露,一定要让程序将各种分支情况都完整执行到程序结束,然后看某个对象是否被使用过,如果没有,则才能判定这个对象属于内存泄露。
如果一个外部类的实例对象的方法返回了一个内部类的实例对象,这个内部类对象被长期引用了,即使那个外部类实例对象不再被使用,但由于内部类持久外部类的实例对象,这个外部类对象将不会被垃圾回收,这也会造成内存泄露。
下面内容来自于网上(主要特点就是清空堆栈中的某个元素,并不是彻底把它从数组中拿掉,而是把存储的总数减少,本人写得可以比这个好,在拿掉某个元素时,顺便也让它从数组中消失,将那个元素所在的位置的值设置为 null 即可):我实在想不到比那个堆栈更经典的例子了,以致于我还要引用别人的例子,下面的例子不是我想到的,是书上看到的,当然如果没有在书上看到,可能过一段时间我自己也想的到,可是那时我说是我自己想到的也没有人相信的。

public class Stack { private Object[] elements=new Object[10]; private int size = 0; public void push(Object e){ ensureCapacity(); elements[size++] = e; } public Object pop(){ if( size == 0) throw new EmptyStackException(); return elements[--size]; } private void ensureCapacity(){ if(elements.length == size){ Object[] oldElements = elements; elements = new Object[2 * elements.length+1]; System.arraycopy(oldElements,0, elements, 0, size); } } }

上面的原理应该很简单,假如堆栈加了 10 个元素,然后全部弹出来,虽然堆栈是空的,没有我们要的东西,但是这是个对象是无法回收的,这个才符合了内存泄露的两个条件:无用,无法回收。
但是就是存在这样的东西也不一定会导致什么样的后果,如果这个堆栈用的比较少,也就浪费了几个 K 内存而已,反正我们的内存都上 G 了,哪里会有什么影响,再说这个东西很快就会被回收的,有什么关系。下面看两个例子。

public class Bad{ public static Stack s=Stack(); static{ s.push(new Object()); s.pop(); //这里有一个对象发生内存泄露 s.push(new Object()); //上面的对象可以被回收了,等于是自 愈了 } }

因为是 static,就一直存在到程序退出,但是我们也可以看到它有自愈功能,就是说如果你的 Stack 最多有 100 个对象,那么最多也就只有 100 个对象无法被回收其实这个应该很容易理解,Stack 内部持有 100 个引用,最坏的情况就是他们都是无用的,因为我们一旦放新的进取,以前的引用自然消失!
内存泄露的另外一种情况:当一个对象被存储进 HashSet 集合中以后,就不能修改这个对象中的那些参与计算哈希值的字段了,否则,对象修改后的哈希值与最初存储进 HashSet 集合中时的哈希值就不同了,在这种情况下,即使在 contains 方法使用该对象的当前引用作为的参数去 HashSet 集合中检索对象,也将返回找不到对象的结果,这也会导致无法从 HashSet 集合中单独删除当前对象,造成内存泄露。


# 959. 垃圾回收器的基本原理是什么?垃圾回收器可以马上回收内存吗? 有什么办法主动通知虚拟机进行垃圾回收?-Java面试题


对于 GC 来说,当程序员创建对象时,GC 就开始监控这个对象的地址、大小以及使用情况。通常,GC 采用有向图的方式记录和管理堆(heap)中的所有对象。通过这种方式确定哪些对象是”可达的”,哪些对象是”不可达的”。当 GC 确定一些对象为“不可达”时,GC 就有责任回收这些内存空间。可以。程序员可以手动执行 System.gc(),通知 GC 运行,但是 Java 语言规范并不保证 GC 一定会执行。


# 958.垃圾回收的优点和原理。并考虑 2 种回收机制。-Java面试题


Java 语言中一个显著的特点就是引入了垃圾回收机制,使 C++ 程序员最头疼的内存管理的问题迎刃而解,它使得 Java 程序员在编写程序的时候不再需要考虑内存管理。由于有个垃圾回收机制,Java 中的对象不再有“作用域”的概念,只有对象的引用才有"作用域"。垃圾回收可以有效的防止内存泄露,有效的使用可以使用的内存。垃圾回收器通常是作为一个单独的低级别的线程运行,不可预知的情况下对内存堆中已经死亡的或者长时间没有使用的对象进行清楚和回收,程序员不能实时的调用垃圾回收器对某个对象或所有对象进行垃圾回收。
回收机制有分代复制垃圾回收和标记垃圾回收,增量垃圾回收。


# 957.如何判断一个对象是否存活?(或者 GC 对象的判定方法)-Java面试题


判断一个对象是否存活有两种方法:

  1. 引用计数法
    所谓引用计数法就是给每一个对象设置一个引用计数器,每当有一个地方引用这个对象时,就将计数器加一,引用失效时,计数器就减一。当一个对象的引用计数器为零时,说明此对象没有被引用,也就是“死对象”,将会被垃圾回收.
    引用计数法有一个缺陷就是无法解决循环引用问题,也就是说当对象 A 引用对象 B,对象 B 又引用者对象 A,那么此时 A、B 对象的引用计数器都不为零,也就造成无法完成垃圾回收,所以主流的虚拟机都没有采用这种算法。
  2. 可达性算法(引用链法)
    该算法的思想是:从一个被称为 GC Roots 的对象开始向下搜索,如果一个对象到 GC Roots 没有任何引用链相连时,则说明此对象不可用。
    在 Java 中可以作为 GC Roots 的对象有以下几种:

    1. 虚拟机栈中引用的对象
    2. 方法区类静态属性引用的对象
    3. 方法区常量池引用的对象
    4. 本地方法栈 JNI 引用的对象
      虽然这些算法可以判定一个对象是否能被回收,但是当满足上述条件时,一个对象比不一定会被回收。当一个对象不可达 GC Root 时,这个对象并不会立马被回收,而是出于一个死缓的阶段,若要被真正的回收需要经历两次标记.
      如果对象在可达性分析中没有与 GC Root 的引用链,那么此时就会被第一次标记并且进行一次筛选,筛选的条件是是否有必要执行finalize() 方法。当对象没有覆盖 finalize() 方法或者已被虚拟机调用过,那么就认为是没必要的。 如果该对象有必要执行finalize() 方法,那么这个对象将会放在一个称为 F-Queue 的对队列中,虚拟机会触发一个 Finalize() 线程去执行,此线程是低优先级的,并且虚拟机不会承诺一直等待它运行完,这是因为如果finalize() 执行缓慢或者发生了死锁,那么就会造成 F-Queue 队列一直等待,造成了内存回收系统的崩溃。GC 对处于 F-Queue 中的对象进行第二次被标记,这时,该对象将被移除” 即将回收”集合,等待回收。

# 956.简述 Java 垃圾回收机制。-Java面试题


在 Java 中,程序员是不需要显示的去释放一个对象的内存的,而是由虚拟机自行执行。在 JVM 中,有一个垃圾回收线程,它是低优先级的,在正常情况下是不会执行的,只有在虚拟机空闲或者当前堆内存不足时,才会触发执行,扫面那些没有被任何引用的对象,并将它们添加到要回收的集合中,进行回收。


# 955.GC 是什么? 为什么要有 GC?-Java面试题


GC 是垃圾收集的意思(GabageCollection),内存处理是编程人员容易出现问题的地方,忘记或者错误的内存回收会导致程序或系统的不稳定甚至崩溃,Java 提供的 GC 功能可以自动监测对象是否超过作用域从而达到自动回收内存的目的,Java 语言没有提供释放已分配内存的显示操作方法。


# 954.Java 堆的结构是什么样子的?什么是堆中的永久代(Perm Gen space)?-Java面试题


JVM 的堆是运行时数据区,所有类的实例和数组都是在堆上分配内存。它在 JVM 启动的时候被创建。对象所占的堆内存是由自动内存管理系统也就是垃圾收集器回收。
堆内存是由存活和死亡的对象组成的。存活的对象是应用可以访问的,不会被垃圾回收。死亡的对象是应用不可访问尚且还没有被垃圾收集器回收掉的对象。一直到垃圾收集器把这些 对象回收掉之前,他们会一直占据堆内存空间。


# 953.Java 内存分配。-Java面试题


  1. 寄存器:我们无法控制。
  2. 静态域:static 定义的静态成员。
  3. 常量池:编译时被确定并保存在 .class 文件中的(final)常量值和一些文本修饰的符号引用(类和接口的全限定名,字段的名称和描述符,方法和名称和描述符)。
  4. 非 RAM 存储:硬盘等永久存储空间。
  5. 堆内存:new 创建的对象和数组,由 Java 虚拟机自动垃圾回收器管理,存取速度慢。
  6. 栈内存:基本类型的变量和对象的引用变量(堆内存空间的访问地址),速度快,可以共享,但是大小与生存期必须确定,缺乏灵活性。

# 952.描述一下 JVM 加载 Class 文件的原理机制?-Java面试题


Java 语言是一种具有动态性的解释型语言,类(Class)只有被加载到 JVM 后才能运行。当运行指定程序时,JVM 会将编译生成的 .class 文件按照需求和一定的规则加载到内存中,并组织成为一个完整的 Java 应用程序。这个加载过程是由类加载器完成,具体来说,就是由 ClassLoader 和它的子类来实现的。类加载器本身也是一个类,其实质是把类文件从硬盘读取到内存中。
类的加载方式分为隐式加载和显示加载。隐式加载指的是程序在使用 new 等方式创建对象时,会隐式地调用类的加载器把对应的类加载到 JVM 中。显示加载指的是通过直接调用 class.forName() 方法来把所需的类加载到 JVM 中。
任何一个工程项目都是由许多类组成的,当程序启动时,只把需要的类加载到 JVM 中,其他类只有被使用到的时候才会被加载,采用这种方法一方面可以加快加载速度,另一方面可以节约程序运行时对内存的开销。此外,在 Java 语言中,每个类或接口都对应一个 .class 文件,这些文件可以被看成是一个个可以被动态加载的单元,因此当只有部分类被修改时,只需要重新编译变化的类即可,而不需要重新编译所有文件,因此加快了编译速度。
在 Java 语言中,类的加载是动态的,它并不会一次性将所有类全部加载后再运行,而是保证程序运行的基础类(例如基类)完全加载到 JVM 中,至于其他类,则在需要的时候才加载。
类加载的主要步骤:

  1. 装载。根据查找路径找到相应的 class 文件,然后导入。
  2. 链接。链接又可分为 3 个小步:
  3. 检查,检查待加载的 class 文件的正确性。
  4. 准备,给类中的静态变量分配存储空间。
  5. 解析,将符号引用转换为直接引用(这一步可选)
  6. 初始化。对静态变量和静态代码块执行初始化工作。

# 951.Java 类加载过程?-Java面试题


Java 类加载需要经历一下 7 个过程:

  1. 加载
    加载是类加载的第一个过程,在这个阶段,将完成一下三件事情:

    1. 通过一个类的全限定名获取该类的二进制流。
    2. 将该二进制流中的静态存储结构转化为方法去运行时数据结构。
    3. 在内存中生成该类的 Class 对象,作为该类的数据访问入口。
  2. 验证
    验证的目的是为了确保 Class 文件的字节流中的信息不回危害到虚拟机.在该阶段主要完成以下四钟验证:

    1. 文件格式验证:验证字节流是否符合 Class 文件的规范,如主次版本号是否在当前虚拟机范围内,常量池中的常量是否有不被支持的类型.
    2. 元数据验证:对字节码描述的信息进行语义分析,如这个类是否有父类,是否集成了不被继承的类等。
    3. 字节码验证:是整个验证过程中最复杂的一个阶段,通过验证数据流和控制流的分析,确定程序语义是否正确,主要针对方法体的验证。如:方法中的类型转换是否正确,跳转指令是否正确等。
    4. 符号引用验证:这个动作在后面的解析过程中发生,主要是为了确保解析动作能正确执行。
  3. 准备
    准备阶段是为类的静态变量分配内存并将其初始化为默认值,这些内存都将在方法区中进行分配。准备阶段不分配类中的实例变量的内存,实例变量将会在对象实例化时随着对象一起分配在 Java 堆中。
    public static int value=123;//在准备阶段 value 初始值为 0 。在初始化阶段才会变为 123 。
  4. 解析
    该阶段主要完成符号引用到直接引用的转换动作。解析动作并不一定在初始化动作完成之前,也有可能在初始化之后。
  5. 初始化
    初始化时类加载的最后一步,前面的类加载过程,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的Java 程序代码。
  6. 使用
  7. 卸载

# ZooKeeper基本概述-Java面试题


什么是ZooKeeper

Apache ZooKeeper 是一个开源的实现高可用的分布式协调服务器。ZooKeeper是一种集中式服务,用于维护配置信息,域名服务,提供分布式同步和集群管理。所有这些服务的种类都被应用在分布式环境中,每一次实施这些都会做很多工作来避免出现bug和竞争条件。

ZooKeeper 设计原则

ZooKeeper 很简单

ZooKeeper 允许分布式进程通过共享的分层命名空间相互协调,ZooKeeper命名空间与文件系统很相似,每个命名空间填充了数据节点的注册信息 – 叫做Znode,这是在 ZooKeeper 中的叫法,Znode 很像我们文件系统中的文件和目录。ZooKeeper 与典型的文件系统不同,ZooKeeper 数据保存在内存中,这意味着 ZooKeeper 可以实现高吞吐量和低时延。

ZooKeeper 可复制

与它协调的分布式进程一样,ZooKeeper本身也可以在称为集群的一组主机上进行复制。

组成 ZooKeeper 服务的单个服务端必须了解彼此。它们维护内存中的状态、持久性的事务日志和快照。只要大多数服务可用,ZooKeeper 服务就可用。

客户端可以连接到单个的服务器。客户端通过连接单个服务器进而维护 TCP 连接,通过连接发送请求,获取响应,获取监听事件以及发送心跳,很像Eureka Server 的功能。如果与单个服务器的连接中断,客户端会自动的连接到ZooKeeper Service 中的其他服务器。

ZooKeeper 有序

ZooKeeper使用时间戳来记录导致状态变更的事务性操作,也就是说,一组事务通过时间戳来保证有序性。基于这一特性。ZooKeeper可以实现更加高级的抽象操作,如同步等。

ZooKeeper 非常快

ZooKeeper包括读写两种操作,基于ZooKeeper的分布式应用,如果是读多写少的应用场景(读写比例大约是10:1),那么读性能更能够体现出高效。

ZooKeeper基本概念

数据模型和分层命名空间

ZooKeeper提供的命名空间非常类似于标准文件系统。名称是由斜杠(/)分隔的路径元素序列。 ZooKeeper名称空间中的每个节点都由路径标识。

ZooKeeper的分层命名空间图

节点和临时节点

与标准的文件系统所不同的是,ZooKeeper命名空间中的每个节点都可以包含与之关联的数据以及子项,这就像一个文件也是目录的文件系统。ZooKeeper被设计用来存储分布式数据:状态信息,配置,定位信息等等。所以每个ZooKeeper 节点能存储的容量非常小,最大容量为 1MB。我们使用术语 Znode 来表明我们正在谈论ZooKeeper数据节点。

Znodes 维护了一个 stat 结构,包括数据变更,ACL更改和时间戳的版本号,用来验证缓存和同步更新。每一次Znode 的数据发生了变化,版本号的数量就会进行增加。每当客户端检索某个znode 数据时,它也会接收该数据的版本。

命名空间下数据存储的Znode 节点都会以原子性的方式读写,也就是保证了原子性。读取所有Znode 相关联的节点数据并通过写的方式替换节点数据。每一个节点都会有一个 访问控制列表(ACL)的限制来判断谁可以进行操作。

ZooKeeper 也有临时节点的概念。只要创建的 Znode 的会话(session)处于活动状态,就会存在这些临时节点。当会话结束,临时节点也就被删除。

选择性更新和watches

ZooKeeper支持watches的概念。客户端可以在 Znode 上设置监听。当 Znode 发生变化时,监听会被触发并移除。触发监听时,客户端会收到一个数据包告知 Znode 发生变更。如果客户端与其中一个 ZooKeeper 服务器之间的连接中断,则客户端将收到本地通知。

集群角色

通常在分布式系统中,构成一个集群中的每一台机器都有一个自己的角色,最典型的集群模式就是 master/slave (主备模式)。在这种模式中,我们把能够处理写操作请求的机器成为 Master ,把所有通过异步复制方式获取最新数据,并提供读请求服务的机器成为 Slave 机器。

而 ZooKeeper 没有采用这种方式,ZooKeeper 引用了 Leader、Follower和 Observer 三个角色。ZooKeeper 集群中的所有机器通过选举的方式选出一个 Leader,Leader 可以为客户端提供读服务和写服务。除了 Leader 外,集群中还包括了 Follower 和 Observer 。Follower 和 Observer 都能够提供读服务,唯一区别在于,Observer 不参与 Leader 的选举过程,也不写操作的"过半写成功"策略,因此 Observer 可以在不影响写性能的情况下提升集群的读性能。

Session

Session 指的是客户端会话,在讲解会话之前先来了解一下客户端连接。客户端连接指的就是客户端和服务器之间的一个 TCP长连接,ZooKeeper 对外的端口是 2181,客户端启动的时候会与服务器建立一个 TCP 连接,从第一次连接建立开始,客户端会话的生命周期也就开始了,通过这个连接,客户端能够通过心跳检测与服务器保证有效的会话,也能够向 ZooKeeper 服务器发送请求并接受响应,同时还能够通过该连接来接收来自服务器的Watch 事件通知。

ZooKeeper 特性

一致性要求

ZooKeeper 非常快并且很简单。但是,由于其开发的目的在于构建更复杂的服务(如同步)的基础,因此它提供了一系列的保证,这些保证是:

  • 顺序一致性:客户端的更新将按顺序应用。
  • 原子性:更新要么成功要么失败,没有其他结果
  • 单一视图:无论客户端连接到哪个服务,所看到的环境都是一样的
  • 可靠性:开始更新后,它将从该时间开始,一直到客户端覆盖更新
  • 及时性: 系统的客户视图保证在特定时间范围内是最新的。

简单的API使用

ZooKeeper 设计之初提供了非常简单的编程接口。作为结果,它支持以下操作:

  • create:在文档目录树中的某一个位置创建节点
  • delete: 删除节点
  • exists: 测试某个位置是否存在节点
  • get data: 从节点中读取数据
  • set data: 向节点中写数据
  • get children: 从节点中检索子节点列表
  • sync: 等待数据传播

实现

ZooKeeper Components 展现了ZooKeeper 服务的高级组件。除请求处理器外,构成 ZooKeeper 服务的每个服务器都复制其自己的每个组件的副本。


图中的 Replicated Database (可复制的数据库)是一个包含了整个数据树的内存数据库。更新将记录到磁盘以获得可恢复性,并且写入在应用到内存数据库之前会得到序列化。

每一个 ZooKeeper 服务器都为客户端服务。客户端只连接到一台服务器用以提交请求。读请求由每个服务器数据库的本地副本提供服务,请求能够改变服务的状态,写请求由"同意协议"进行通过。

作为"同意协议" 的一部分,所有的请求都遵从一个单个的服务,由这个服务来询问除自己之外其他服务是否可以同意写请求,而这个单个的服务被称为Leader。除自己之外其他的服务被称为follower,它们接收来自Leader 的消息并对消息达成一致。消息传底层负责替换失败的 leader 并使 follower 与 leader 进行同步。

ZooKeeper 使用自定义的原子性消息传递协议。因为消息传底层是原子性的,ZooKeeper 能够保证本地副本永远不会产生分析或者冲突。当 leader 接收到写请求时,它会计算系统的状态以确保写请求何时应用,并且开启一个捕获新状态的事务。

使用者

ZooKeeper 的编程接口非常简单,但是通过它,你可以实现更高阶的操作。

性能

ZooKeeper 旨在提供高性能,但是真的是这样吗?ZooKeeper是由雅虎团队开发。当读请求远远高于写请求的时候,它的效率很高,因为写操作涉及同步所有服务器的状态。(读取数量超过写入通常是协调服务的情况。)

ZooKeeper吞吐量作为读写比率变化是在具有双2Ghz Xeon和两个SATA 15K RPM驱动器的服务器上运行的ZooKeeper版本3.2的吞吐量图。一个驱动器用作专用的ZooKeeper日志设备。快照已写入OS驱动器。写请求是1K写入,读取是1K读取。 “服务器”表示ZooKeeper集合的大小,即构成服务的服务器数量。 大约30个其他服务器用于模拟客户端。 ZooKeeper集合的配置使得Leader不允许来自客户端的连接。(此部分来源于翻译结果)

基准也表明它也是可靠的。 存在错误时的可靠性显示了部署如何响应各种故障。 图中标记的事件如下:

  1. follower 的失败和恢复
  2. 失败和恢复不同的 follower
  3. leader 的失败
  4. 两个follower 的失败和恢复
  5. 其他leader 的失败

可靠性

为了在注入故障时显示系统随时间的行为,我们运行了由7台机器组成的ZooKeeper服务。我们运行与以前相同的饱和度基准,但这次我们将写入百分比保持在恒定的30%,这是我们预期工作量的保守比率。(此部分来源于翻译结果)

该图中有一些重要的观察结果。 首先,如果follower 失败并迅速恢复,那么即使失败,ZooKeeper也能够维持高吞吐量。 但也许更重要的是,leader 选举算法允许系统足够快地恢复以防止吞吐量大幅下降。 在我们的观察中,ZooKeeper 选择新 leader 的时间不到200毫秒。 第三,随着follower 的恢复,ZooKeeper能够在开始处理请求后再次提高吞吐量。

文章来源:

https://zookeeper.apache.org/doc/current/zookeeperOver.html

《从Paxos到zookeeper分布式一致性原理与实践》


# 如何高效的学习技术?-Java面试题


学什么

基础与应用

近些年诞生了许多新技术,比如最时髦的 AI(目前还在智障阶段),数学基础是初中就接触过的概率统计。万丈高楼从地起,不要被新工具或者中间件迷住双眼,一味地追新求快。基础知识是所有技术的基石,在未来很长的时间都不会变化,应该花费足够的时间巩固基础。

以数据结构和算法为例,大家阅读一下 Java 的 BitSet 的源码,里面有大量的移位操作,移位运算掌握的好,看这份源码就没问题。Java 同步工具类 AQS用到了双向链表,链表知识不过关,肯定搞不懂它的原理。互联网大厂都喜欢考算法,为了通过面试也要精通算法。

以Java工程师应该掌握的知识为例,按重要程度排出六个梯度:

  • 第一梯度:计算机组成原理、数据结构和算法、网络通信原理、操作系统原理;
  • 第二梯度:Java 基础、JVM 内存模型和 GC 算法、JVM 性能调优、JDK 工具、设计模式;
  • 第三梯度:Spring 系列、Mybatis、Dubbo 等主流框架的运用和原理;
  • 第四梯度:MySQL (含SQL编程)、Redis、RabbitMQ/RocketMQ/Kafka、ZooKeeper 等数据库或者中间件的运用和原理;
  • 第五梯度:CAP 理论、BASE 理论、Paxos 和 Raft 算法等其他分布式理论;
  • 第六梯度:容器化、大数据、AI、区块链等等前沿技术理论;

有同学认为第五梯度应该在移到第一梯度。其实很多小公司的日活犹如古天乐一样平平无奇,离大型分布式架构还远得很。学习框架和中间件的时候,顺手掌握分布式理论,效果更好。

广度与深度

许多公司的招聘 JD 没有设定技术人员年龄门槛,但是会加上一句具备与年龄相当的知识的广度与深度。多广才算广,多深才算深?这是很主观的话题,这里不展开讨论。

如何变得更广更深呢?突破收入上升的瓶颈,发掘自己真正的兴趣。

大多数人只是公司的普通职员,收入上升的瓶颈就是升职加薪。许多 IT 公司会对技术人员有个评级,如果你的评级不高,那就依照晋级章程努力升级。如果你在一个小公司,收入一般,发展前景不明,准备大厂的面试就是最好的学习过程。在这些过程中,你必然学习更多知识,变得更广更深。

个人兴趣是前进的动力之一,许多知名开源项目都源于作者的兴趣。个人兴趣并不局限技术领域,可以是其他学科。我有个朋友喜欢玩山地自行车,还给一些做自行车话题的自媒体投稿。久而久之,居然能够写一手好文章了,我相信他也能写好技术文档。

哲学

哲学不是故作高深的学科,它的现实意义就是解决问题。年轻小伙是怎么泡妞的?三天两头花不断,大庭广众跪求爱。这类套路为什么总是能成功呢?礼物满足女人的物欲,当众求爱满足女人的虚荣心,投其所好。食堂大妈打菜的手越来越抖,辣子鸡丁变成辣子辣丁,为什么呢?食堂要控制成本,直接提价会惹众怒。

科学上的哲学,一般指研究事物发展的规律,归纳终极的解决方案。软件行业充满哲学味道的作品非常多,比如《人月神话》。举个例子,当软件系统遇到性能问题,尝试下面两种哲学思想提升性能

  • 空间换时间:比如引入缓存,消耗额外的存储提高响应速度。
  • 时间换空间:比如大文件的分片处理,分段处理后再汇总结果。

设计稳健高可用的系统,尝试从三个方面考虑问题:

  • 存储:数据会丢失吗,数据一致性怎么解决。
  • 计算:计算怎么扩容,应用允许任意增加节点吗。
  • 传输:网络中断或拥塞怎么办。

从无数的失败或者成功的经验中,总结出高度概括性的方案,让我们下一步做的更好。

英语

英语是极为重要的基础,学好英语与掌握编程语言一样重要。且不说外企对英语的要求,许多知名博客就是把英文翻译成中文,充当知识的搬运工。如果英语足够好,直接阅读一手英语资料,避免他人翻译存在的谬误。

怎么学

知识体系

体系化的知识比零散的更容易记忆和理解,这正如一部好的电视剧,剧情环环相扣才能吸引观众。建议大家使用思维导图罗列知识点,构建体系结构,如下图所示:

图片

克服遗忘

高中是我们知识的巅峰时刻,每周小考每月大考,教辅资料堆成山,地狱式的反复操练强化记忆。复习是对抗遗忘的唯一办法。大脑的遗忘是有规律的,先快后慢。一天后,学到的知识只剩下原来的 25%,甚至更低。随着时间的推移,遗忘的速度减慢,遗忘的数量也就减少。

时间间隔 记忆量
刚看完 100%
20分钟后 60%
1小时后 40%
1天后 30%
2天后 27%

每个人的遗忘程度都不一样,建议第二天复习前一天的内容,七天后复习这段时间的所有内容。

碎片时间

不少朋友利用碎片时间学习,比如在公交上看公众号的推送。其实我们都高估了自己的抗干扰能力,如果处在嘈杂的环境,注意力容易被打断,记忆留存度也很低。碎片时间适合学习简单孤立的知识点,比如链表的定义与实现。

学习复杂的知识,需要大段的连续时间。图书馆是个好地方,安静氛围好。手机放一边,不要理会 QQ 微信,最好阅读纸质书,泡上一整天。有些城市出现了付费自习室,提供格子间、茶水等等,也是非常好的选择。

用起来

技术分享

从下面这张图我们可以看到,教授他人是知识留存率最高的方式。

图片

准备 PPT 和演讲内容,给同事来一场技术分享。不光复习知识,还锻炼口才。曾经有个同事说话又快又急,口头禅也多,比如对吧、是不是,别人经常听不清,但是他本人不以为然。领导让他做了几次技术分享,听众的反应可想而知,他才彻底认清缺点。

坚持写技术博客,别在意你写的东西在网上已经重复千百遍。当自己动手的时候,才会意识到眼高手低。让文章读起来流畅清晰,需要呕心沥血的删改。写作是对大脑的长期考验,想不到肯定写不出,想不清楚肯定写不清楚。

造个轮子

我们经常说不要重复造轮子。为了开发效率,可以不造轮子,但是必须具备造轮子的能力。建议造一个简单的MQ,你能用到通信协议、设计模式、队列等许多知识。在造轮子的过程中,你会频繁的翻阅各种手册或者博客,这就是用输出倒逼输入。

原文链接:https://www.cnblogs.com/xiaoyangjia/p/11535486.html


# 使用SpringAPI进行验证-Java面试题


验证在任何时候都非常关键。考虑将数据验证作为业务逻辑开发有利也有弊,Spring 认为,验证不应该只在Web 端进行处理,在服务端也要进行相应的处理,可以防止脏数据存入数据库中,从而避免为运维同学和测试同学造成更大的困扰,因为数据造成的bug会更加难以发现,而且开发人员关注点也不会放在数据本身的问题上,所以做服务端的验证也是非常有必要的。
考虑到上面这些问题,Spring 提供了两种主要类型的验证:

  • 一个是实现Validator 接口来创建自定义验证器,用于服务端数据校验。
  • 一种是通过Spring 对 Bean Validation 支持实现的。

通过使用 Spring Validator 接口进行验证

Spring 提供 Validator 接口用于验证对象。Validator 接口通过使用 Errors 对象来工作,以便在验证时,验证器可以向 Errors 对象报告验证失败。下面是一个简单的 对象示例

public class Person {
private String name;
private int age;

// get and set...

}

下面一个例子为 Person 对象提供了一种验证方式,通过实现了 org.springframework.validation.Validator 接口 的两个方法:

  • supports(Class): 表示此 Validator 是否能够验证提供的类的实例
  • validate(Object, org.springframework.validation.Errors): 验证给定的对象,如果验证错误,则注册具有给定 Errors 对象。

实现一个 Validator 非常简单,而且Spring 也提供了 ValidationUtils 工具类帮助进行验证。下面是一个验证 Person 对象的例子:

@Component public class PersonValidator implements Validator {

// 此 Validator 只验证 Person 实例
public boolean supports(Class clazz) {
    return Person.class.equals(clazz);
}

public void validate(Object obj, Errors e) {
    ValidationUtils.rejectIfEmpty(e, "name", "name.empty");
}

}

上面代码示例中的静态方法 rejectIfEmpty() 方法用于拒绝name属性,当name 属性是 null 或者是 空串的时候。查看 ValidationUtils 文档关于它能够提供的功能。

然后再来编写配置类 AppConfig:

@Configuration @ComponentScan("com.spring.validation") public class AppConfig {}

使用 @Configuration 注解声明此类为配置类(更多 @Configuration 的用法,请参照 原创 | 我被面试官给虐懵了,竟然是因为我不懂Spring中的@Configuration )

配置@ComponentScan 注解用于自动装配,默认是使用 basePackages 扫描指定包,字符串表示。

然后对上面的程序进行验证

public class SpringValidationApplicationTests {

public static void main(String[] args) {
    AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);

    Person person = new Person();
    person.setAge(18);
    person.setName(null);

    PersonValidator personValidator = applicationContext.getBean("personValidator", PersonValidator.class);
    BeanPropertyBindingResult result = new BeanPropertyBindingResult(person,"cxuan");

    ValidationUtils.invokeValidator(personValidator,person,result);

    List&lt;ObjectError&gt; allErrors = result.getAllErrors();
    allErrors.forEach(e-&gt; System.out.println(e.getCode()));
}

}

因为是基于注解的配置,所以使用 AnnotationConfigApplicationContext上下文启动类,把配置类 AppConfig 当作参数,然后构建一个Person 类,为了测试验证有效性,把 name 设置为 null,然后通过上下问的 getBean 方法获得 personValidator 的实例,通过使用 BeanPropertyBindingResult 把 person 绑定为 cxuan 的名字,然后使用 ValidationUtils 工具类进行验证,最后把验证的结果进行检查。

上面程序经验证后的结果如下:

org.springframework.validation.ValidationUtils – Invoking validator [com.spring.validation.PersonValidator@37918c79]
DEBUG org.springframework.validation.ValidationUtils – Validator found 1 errors
name.empty

使用 Bean Validation 进行验证

从 Spring4 开始,就已经实现对 JSR-349 Bean Validation 的全面支持。Bean Validation API 在 javax.validation.constraints 包中以 Java 注解(例如 @NonNull) 形式定义了一组可用域对象的约束。

通过使用 Bean Validation API ,可以避免耦合到特定的验证服务提供程序。Spring 对 Bean Validation API 提供了无缝支持,主要使用一些注解进行验证,下面一起来看一下

定义对象属性上的验证约束

首先,将验证约束应用于域对象属性。使用maven 配置需要引入对应的依赖

<dependency> <groupId>javax.validation</groupId> <artifactId>validation-api</artifactId> <version>1.1.0.Final</version> </dependency> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-validator</artifactId> <version>5.2.4.Final</version> </dependency>

之后定义了一些实体类,使用 javax.validation.constraints 包中的注释进行标注

public class Singer {

@NotNull
@Size(min = 2,max = 60)
private String firstName;

private String lastName;

@NotNull
private Genre genre;

private Gender gender;

get and set...

}

对于 firstName ,定义了两个约束,第一个约束由 @NotNull 进行控制,它表示该值不能为空。此外,@Size注解控制着 firstName 的长度在 2 – 60 之间。@NotNull 还用于 genre 属性。下面是Genre 和 Gender 的枚举类

public enum Genre {

POP("P"),
JAZZ("J"),
BLUES("B"),
COUNTRY("C");

private String code;

private Genre(String code){
    this.code = code;
}

public String toString(){
    return this.code;
}

}

public enum Gender {

MALE("M"),
FEMALE("F");

private String code;

Gender(String code){
    this.code = code;
}

@Override
public String toString() {
    return this.code;
}

}

Genre 表示歌手所属的音乐类型,而 Gender 与音乐事业不相关,所以可以为空

在 Spring 中配置 Bean Validation 支持

为了在 Spring 的 ApplicationContext 中配置对 Bean Validation API 的支持,可以在Spring 的配置中定义一个 LocalValidatorFactoryBean 的 bean如下

@Configuration @ComponentScan("com.spring.validation") public class ValidationConfig {

@Bean
LocalValidatorFactoryBean validatorFactoryBean(){
    return new LocalValidatorFactoryBean();
}

}

声明一个 LocalValidatorFactoryBean 的 bean 是必须的。默认情况下,Spring 会在类路径下搜索 Hibernate Validator库,验证它是否存在。

下面我们编写一个为 Singer 类提供验证服务的服务类

@Service public class SingerValidationService {

@Autowired
private Validator validator;

public Set&lt;ConstraintViolation&lt;Singer&gt;&gt; validateSinger(Singer singer){
    return validator.validate(singer);
}

}

注入一个 javax.validation.Validator 实例(请注意与 Spring 提供的 Validator 接口不同)。一旦定义了 LocalValidatorFactoryBean ,就可以在应用程序中的任意位置创建 Validator 的句柄。要在 POJO 上进行验证,需要调用 validator.validate 方法,验证结果以 ConstraintViolation<T> 接口的集合形式返回。下面是上面例子程序的验证

public class SpringBeanValidationTest {

public static void main(String[] args) {
    AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(ValidationConfig.class);

    SingerValidationService singerBean = applicationContext.getBean(SingerValidationService.class);

    Singer singer = new Singer();
    singer.setFirstName("c");
    singer.setLastName("xuan");
    singer.setGenre(null);
    singer.setGender(null);

    validateSinger(singer,singerBean);

    applicationContext.close();
}

private static void validateSinger(Singer singer,SingerValidationService singerValidationService){
    Set&lt;ConstraintViolation&lt;Singer&gt;&gt; violationSet = singerValidationService.validateSinger(singer);
    listViolations(violationSet);
}

private static void listViolations(Set&lt;ConstraintViolation&lt;Singer&gt;&gt; violations){
    System.out.println("violations.size() = " + violations.size());
    for(ConstraintViolation&lt;Singer&gt; violation : violations){
        System.out.println("Validation error for property : " + violation.getPropertyPath());
        System.out.println("with value : " + violation.getInvalidValue());
        System.out.println("with error message : " + violation.getMessage());
    }
}

}

上述代码构建了一个 Singer 类进行验证,因为 firstname 属性的要求是长度介于 2 – 60 之间并且不能为null,所以这里只用了一个字符验证,genre 属性不能为null,最核心的验证方法就是 singerValidationService.validateSinger(singer).方法,它会调用

public Set<ConstraintViolation<Singer>> validateSinger(Singer singer){ return validator.validate(singer); }

进行验证,验证的结果返回的是 ConstraintViolation<Singler> 类型,然后把对应的错误信息输出,上面的错误信息是

violations.size() = 2
Validation error for property : firstName
with value : c
with error message : 个数必须在2和60之间
Validation error for property : genre
with value : null
with error message : 不能为null

可以打印出两个错误,并输出错误的属性、值以及错误信息。


# Spring中的Null-Safety-Java面试题


之前一直在某些代码中看到过使用@Nullable 标注过的注释,当时也没有在意到底是什么意思,所以这篇文章来谈谈Spring中关于Null的那些事。

在Java中不允许让你使用类型表示其null的安全性,但Spring Framework 现在在org.sprinngframework.lang包提供以下注释,以便声明API和字段的可空性:

  • @Nullable: 用于指定参数、返回值或者字段可以作为null的注释。
  • @NonNull: 与上述注释相反,表明指定参数、返回值或者字段不允许为null。(不需要@NonNullApi和@NonNullFields适用的参数/返回值和字段)
  • @NonNullApi: 包级别的注释声明非null作为参数和返回值。
  • @NonNullFields:包级别的注释声明字段默认非空

Spring Framework 本身利用了上面这几个注释,但它们也可以运用在任何基于Spring的Java 项目中,以声明空安全api 和 空安全字段。尚未支持泛型和数组元素的可空性,但应也即将发布在后来的版本。Spring Null-Safety出现在Spring5中,让我们更方便的编写空安全的代码,这叫做null-safety,null-safety不是让我们逃脱不安全的代码,而是在编译时产生警告。 此类警告可以在运行时防止灾难性空指针异常(NPE)。

@NonNull

@NonNull注释是null-safety的所有注释中最重要的一个,我们可以使用此注释在期望对象引用的任何地方声明非空约束:字段、方法参数或者方法返回值。

先来看一个例子

public class Student {
private String name;

public String getName() {
    return name;
}

public void setName(String name) {
    if(name != null &amp;&amp; name.isEmpty()){
        name = null;
    }
    this.name = name;
}

}

上述代码对name的校验是有效的,但是存在一个缺陷,如果name被设置为null的话,那么当我们使用name的时候,就会以NullPointerException来结尾。

使用@NonNull

Spring 的null-safety特性能够允许idea或者eclipse报告这个潜在的威胁,例如,如果我们用IDEA对属性加上@NonNull会出现如下的效果。

奇怪,并没有什么变化啊,没看见有潜在的安全提示啊,那是因为你没有在idea进行设置

设置安全检查

如果你也没有提示的话,可以通过如下的方式设置安全检查

如果还不好使的话,那就在右侧 configuration annotations 添加一下 @NonNull和 @Nullable 所在的jar包,如下:

添加上,打上 ✅ 即可看到如下效果。

现在fullName 已经被@NonNull 注释添加编译器检查null值的功能了!

如果你不相信的话,可以把@NonNull 注释去掉,你的鼠标再放在fullName 上,已经没有这句提示了。

@NonNullFields

@NonNull 注解能够帮助你确保null-safety。然而,如果此注释直接装饰所有的字段的话,就会污染整个代码库。

Spring提供了另外一个不允许为null的注解 — @NonNullFields。这个注解适合用在包级别上,通知我们的开发工具注释包中所有的字段,默认的,不允许为null

新建一个Parent类,并在该类所属包下创建一个名为package-info.java的类,创建的不是Java类,而是创建的file,名为package-info.java,如下

package-info.java

@NonNullFields package com.nullsafety.demo.pojo;

import org.springframework.lang.NonNullFields;

新建一个Parent.java 类

public class Parent {

private String son;
private String age;
private String name;

public void setSon(String son) {
    if(son != null &amp;&amp; son.isEmpty()){
        son = null;
    }
    this.son = son;
}

public void setAge(String age) {
    if(age != null &amp;&amp; age.isEmpty()){
        age = null;
    }
    this.age = age;
}

public void setName(String name) {
    if(name != null &amp;&amp; name.isEmpty()){
        name = null;
    }
    this.name = name;
}

}

package-info.java 中的@NonNullFields能够对Parent类中所有的属性起作用,把鼠标放在任意一个属性上,会出现编译期检查的提示

@Nullable

@NonNullFields注释通常比@NonNull更好,因为它有助于减少样板。 但是,有时我们想要从包级别指定的非null约束中免除某些字段,这时候就会使用到@Nullable注解

改造一下Person.java,Person.java 与pack-info.java 处于同一包下

public class Person {

@NonNull
private String fullName;

@Nullable
private String nickName;

public String getNickName() {
    return nickName;
}

public void setNickName(String nickName) {
    if(nickName != null &amp;&amp; nickName.isEmpty()){
        nickName = null;
    }
    this.nickName = nickName;
}

public String getFullName() {
    return fullName;
}

public void setFullName(String fullName) {
    if(fullName != null &amp;&amp; fullName.isEmpty()){
        fullName = null;
    }
    this.fullName = fullName;
}

}

在这种情况下,我们使用@Nullable注释来覆盖字段上@NonNullFields的语义。

@NonNullApi

@NonNullFields注释仅适用于其名称所示的字段。 如果我们想对方法的参数和返回值产生相同的影响,我们需要@NonNullApi。

添加 @NonNullApi和 @NonNullFields 在 configure annotations 中,并选用NonNullApi

与@NonNullFields一样,我们需要在package-info.java 中定义@NonNullApi

package-info.java

@NonNullApi @NonNullFields package com.nullsafety.demo.pojo;

import org.springframework.lang.NonNullApi; import org.springframework.lang.NonNullFields;

加上如下注释后的效果如下: 可以在返回值的时候接受到编译期的提示。

后记:

看完文章,你至少应该了解

  • 四个注解 @NonNull, @Nullable, @NonNullFields, @NonNullApi 四个注解各自的作用范围
  • 如何设置编译期的Null-safety检查


# 一文了解ConfigurationConditon接口-Java面试题


ConfigurationCondition 接口说明

@Conditional 和 Condition

​ 在了解ConfigurationCondition 接口之前,先通过一个示例来了解一下@Conditional 和 Condition。(你也可以通过 https://www.cnblogs.com/cxuanBlog/p/10960575.html 详细了解)

  • 首先新建一个Maven项目(可以使用SpringBoot快速搭建),添加Spring4.0 的pom.xml 依赖
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.5.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.cxuan.configuration</groupId> <artifactId>configuration-condition</artifactId> <version>0.0.1-SNAPSHOT</version> <name>configuration-condition</name> <description>Demo project for Spring Boot</description>
&lt;properties&gt;
    &lt;java.version&gt;1.8&lt;/java.version&gt;
    &lt;spring.version&gt;4.3.13.RELEASE&lt;/spring.version&gt;
&lt;/properties&gt;

&lt;dependencies&gt;
    &lt;dependency&gt;
        &lt;groupId&gt;org.springframework&lt;/groupId&gt;
        &lt;artifactId&gt;spring-context&lt;/artifactId&gt;
        &lt;version&gt;${spring.version}&lt;/version&gt;
    &lt;/dependency&gt;

    &lt;dependency&gt;
        &lt;groupId&gt;org.springframework&lt;/groupId&gt;
        &lt;artifactId&gt;spring-beans&lt;/artifactId&gt;
        &lt;version&gt;${spring.version}&lt;/version&gt;
    &lt;/dependency&gt;

    &lt;dependency&gt;
        &lt;groupId&gt;org.springframework&lt;/groupId&gt;
        &lt;artifactId&gt;spring-core&lt;/artifactId&gt;
        &lt;version&gt;${spring.version}&lt;/version&gt;
    &lt;/dependency&gt;
&lt;/dependencies&gt;

&lt;build&gt;
    &lt;plugins&gt;
        &lt;plugin&gt;
            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
            &lt;artifactId&gt;spring-boot-maven-plugin&lt;/artifactId&gt;
        &lt;/plugin&gt;
    &lt;/plugins&gt;
&lt;/build&gt;

</project>

  • 新建一个IfBeanAExistsCondition 类,该类继承了Condition接口,提供某些注册条件的逻辑
public class IfBeanAExistsCondition implements Condition {

@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
    boolean IfContainsbeanA = context.getBeanFactory().containsBeanDefinition("beanA");
    return IfContainsbeanA;
}

}

Condition是一个接口,里面只有一个方法就是matches,上述表明如果ConditionContext的beanFactory包括名称为beanA的bean就返回true,否则返回false不进行注册。

  • 为了测试Condition是否可用,我们新建一个ConfigurationConditionApplication类,注册两个Bean分别为BeanA和BeanB,BeanB的注册条件是BeanA首先进行注册,采用手动注册和刷新的方式。详见https://www.cnblogs.com/cxuanBlog/p/10958307.html,具体代码如下:
public class ConfigurationConditionApplication {

private static void loadContextAndVerifyBeans(Class...classToRegistry){
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
    context.register(classToRegistry);
    context.refresh();
    System.out.println("Has BeanA? " + context.containsBean("beanA"));
    System.out.println("Has BeanB? " + context.containsBean("beanB"));
}

public static void main(String[] args) {
    loadContextAndVerifyBeans(BeanA.class);
    loadContextAndVerifyBeans(BeanA.class,BeanB.class);
    loadContextAndVerifyBeans(BeanB.class);
    loadContextAndVerifyBeans(BeanB.class,BeanA.class);
}

}

@Configuration() class BeanA{}

@Conditional(IfBeanAExistsCondition.class) @Configuration() class BeanB{}

输出结果:

... Has BeanA? true Has BeanB? false ... Has BeanA? true Has BeanB? true ... Has BeanA? false Has BeanB? false ... Has BeanA? true Has BeanB? false

来解释一下上面的输出结果,第一次只注册了一个BeanA的bean,@Configuration标注的BeanA默认注册的definitionName为beanA,首字母小写。

第二次同时传入了BeanA.class 和 BeanB.class, 由于BeanB的注解上标明@Conditional(IfBeanAExistsCondition.class)表示的是注册BeanA之后才会注册BeanB,所以注册了beanA,因为beanA被注册了,所以同时也就注册了beanB。

第三次只传入了BeanB.class,因为没有注册BeanA和BeanB,所以两次输出都是false。

第四次先传入了BeanB.class,后又传入了BeanA.class,根据加载顺序来看,BeanB.class 首先被加载,然后是BeanA.class 被加载,BeanB被加载的时候BeanA.class 还没有被注入,之后BeanA才会注入,所以输出的结果是true和false。

上述例子可以把BeanA和BeanB类放入ConfigurationConditionApplication中,类似

public class ConfigurationConditionApplication {

@Configuration() static class BeanA{}

@Conditional(IfBeanAExistsCondition.class) @Configuration() static class BeanB{}

}

但是需要把BeanA和BeanB定义为静态类,因为静态类与外部类无关能够独立存在,如果定义为非静态的,启动会报错。

关于ConfigurationConditon

​ ConfigurationCondition接口是Spring4.0提供的注解。位于org.springframework.context.annotation包内,继承于Condition接口。Condition接口和@Configuration以及@Conditional接口为bean的注册提供更细粒度的控制,允许某些Condition在匹配时根据配置阶段进行调整。

public interface ConfigurationCondition extends Condition {

// 评估condition返回的ConfigurationPhase
ConfigurationPhase getConfigurationPhase();

// 可以评估condition的各种配置阶段。
enum ConfigurationPhase {

    // @Condition 应该被评估为正在解析@Configuration类
    // 如果此时条件不匹配,则不会添加@Configuration 类。
    PARSE_CONFIGURATION,

    // 添加常规(非@Configuration)bean时,应评估@Condition。Condition 将不会阻止@Configuration 类
    // 添加。在评估条件时,将解析所有@Configuration
    REGISTER_BEAN
}

}

getConfigurationPhase()方法返回ConfigurationPhase 的枚举。枚举类内定义了两个enum,PARSE_CONFIGURATION 和 REGISTER_BEAN,表示不同的注册阶段。

​ 我们现在对condition实现更细粒度的控制,实现了ConfigurationCondition接口,我们现在需要实现getConfigurationPhase()方法获得condition需要评估的阶段。

  • 新建IfBeanAExistsConfigurationCondition类,实现了ConfigurationCondition接口,分别返回ConfigurationPhase.REGISTER_BEAN 和 ConfigurationPhase.PARSE_CONFIGURATION 阶段。
public class IfBeanAExistsConfigurationCondition implements ConfigurationCondition {

@Override
public ConfigurationPhase getConfigurationPhase() {
    return ConfigurationPhase.REGISTER_BEAN;
}

// @Override // public ConfigurationPhase getConfigurationPhase() { // return ConfigurationPhase.PARSE_CONFIGURATION; // }

@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
    return context.getBeanFactory().containsBeanDefinition("beanA");
}

}

  • 新建SpringConfigurationConditionExample类,与上述测试类基本相同,就是把@Conditional 换为了@Conditional(IfBeanAExistsConfigurationCondition.class)

测试类启动,输出结果

... Has BeanA? true Has BeanB? false ... Has BeanA? true Has BeanB? true ... Has BeanA? false Has BeanB? false ... Has BeanA? true Has BeanB? true

也就是说,如果返回的是PARSE_CONFIGURATION阶段的话,不会阻止@Configuration的标记类的注册顺序,啥意思呢?

第一个结果,只注册了BeanA,因为只有BeanA加载。

第二个结果,注册了BeanA和BeanB,因为BeanA和BeanB都被加载

第三个结果,因为BeanB注册的条件是BeanA注册,因为BeanA没有注册,所以BeanB不会注册

第四个结果,不论BeanA和BeanB的加载顺序如何,都会直接进行注册。

  • 如果把REGISTER_BEAN改为PARSE_CONFIGURATION ,会发现加载顺序第一次一致。


# SpringAOP扫盲-Java面试题


关于AOP

面向切面编程(Aspect-oriented Programming,俗称AOP)提供了一种面向对象编程(Object-oriented Programming,俗称OOP)的补充,面向对象编程最核心的单元是类(class),然而面向切面编程最核心的单元是切面(Aspects)。与面向对象的顺序流程不同,AOP采用的是横向切面的方式,注入与主业务流程无关的功能,例如事务管理和日志管理。

Spring的一个关键组件是AOP框架。 虽然Spring IoC容器不依赖于AOP(意味着你不需要在IOC中依赖AOP),但AOP为Spring IoC提供了非常强大的中间件解决方案。

AOP 是一种编程范式,最早由 AOP 联盟的组织提出的,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。它是 OOP的延续。利用 AOP 可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率

我们之间的开发流程都是使用顺序流程,那么使用 AOP 之后,你就可以横向抽取重复代码,什么叫横向抽取呢?或许下面这幅图你能理解,先来看一下传统的软件开发存在什么样风险。

纵向继承体系:

在改进方案之前,我们或许都遇到过 IDEA 对你输出 Duplicate Code 的时候,这个时候的类的设计是很糟糕的,代码写的也很冗余,基本上 if…else… 完成所有事情,这个时候就需要把相同的代码抽取出来成为公共的方法,降低耦合性。这种提取代码的方式是纵向抽取,纵向抽取的代码之间的关联关系非常密切。
横向抽取也是代码提取的一种方式,不过这种方式不会修改主要业务逻辑代码,只是在此基础上添加一些与主要的业务逻辑无关的功能,AOP 采取横向抽取机制,补充了传统纵向继承体系(OOP)无法解决的重复性 代码优化(性能监视、事务管理、安全检查、缓存),将业务逻辑和系统处理的代码(关闭连接、事务管理、操作日志记录)解耦。

AOP 的概念

在深入学习SpringAOP 之前,让我们先对AOP的几个基本术语有个大致的概念,这些概念不是很容易理解,比较抽象,可以知道有这么几个概念,下面一起来看一下:

  • 切面(Aspect): Aspect 声明类似于 Java 中的类声明,事务管理是AOP一个最典型的应用。在AOP中,切面一般使用 @Aspect 注解来使用,在XML 中,可以使用 <aop:aspect> 来定义一个切面。
  • 连接点(Join Point): 一个在程序执行期间的某一个操作,就像是执行一个方法或者处理一个异常。在Spring AOP中,一个连接点就代表了一个方法的执行。
  • 通知(Advice): 在切面中(类)的某个连接点(方法出)采取的动作,会有四种不同的通知方式: around(环绕通知),before(前置通知),after(后置通知), exception(异常通知),return(返回通知)。许多AOP框架(包括Spring)将建议把通知作为为拦截器,并在连接点周围维护一系列拦截器。
  • 切入点(Pointcut):表示一组连接点,通知与切入点表达式有关,并在切入点匹配的任何连接点处运行(例如执行具有特定名称的方法)。由切入点表达式匹配的连接点的概念是AOP的核心,Spring默认使用AspectJ切入点表达式语言。
  • 介绍(Introduction): introduction可以为原有的对象增加新的属性和方法。例如,你可以使用introduction使bean实现IsModified接口,以简化缓存。
  • 目标对象(Target Object): 由一个或者多个切面代理的对象。也被称为"切面对象"。由于Spring AOP是使用运行时代理实现的,因此该对象始终是代理对象。
  • AOP代理(AOP proxy): 由AOP框架创建的对象,在Spring框架中,AOP代理对象有两种:JDK动态代理和CGLIB代理
  • 织入(Weaving): 是指把增强应用到目标对象来创建新的代理对象的过程,它(例如 AspectJ 编译器)可以在编译时期,加载时期或者运行时期完成。与其他纯Java AOP框架一样,Spring AOP在运行时进行织入。

Spring AOP 中通知的分类

  • 前置通知(Before Advice): 在目标方法被调用前调用通知功能;相关的类org.springframework.aop.MethodBeforeAdvice
  • 后置通知(After Advice): 在目标方法被调用之后调用通知功能;相关的类org.springframework.aop.AfterReturningAdvice
  • 返回通知(After-returning): 在目标方法成功执行之后调用通知功能;
  • 异常通知(After-throwing): 在目标方法抛出异常之后调用通知功能;相关的类org.springframework.aop.ThrowsAdvice
  • 环绕通知(Around): 把整个目标方法包裹起来,在被调用前和调用之后分别调用通知功能相关的类org.aopalliance.intercept.MethodInterceptor

Spring AOP 中织入的三种时期

  • 编译期: 切面在目标类编译时被织入,这种方式需要特殊的编译器。AspectJ 的织入编译器就是以这种方式织入切面的。
  • 类加载期: 切面在目标类加载到 JVM 时被织入,这种方式需要特殊的类加载器( ClassLoader ),它可以在目标类引入应用之前增强目标类的字节码。
  • 运行期: 切面在应用运行的某个时期被织入。一般情况下,在织入切面时,AOP容器会为目标对象动态创建一个代理对象,Spring AOP 采用的就是这种织入方式。

AOP 的两种实现方式

AOP 采用了两种实现方式:静态织入(AspectJ 实现)和动态代理(Spring AOP实现)

AspectJ

AspectJ 是一个采用Java 实现的AOP框架,它能够对代码进行编译(一般在编译期进行),让代码具有AspectJ 的 AOP 功能,AspectJ 是目前实现 AOP 框架中最成熟,功能最丰富的语言。ApectJ 主要采用的是编译期静态织入的方式。在这个期间使用 AspectJ 的 acj 编译器(类似 javac)把 aspect 类编译成 class 字节码后,在 java 目标类编译时织入,即先编译 aspect 类再编译目标类。

Spring AOP 实现

Spring AOP 是通过动态代理技术实现的,而动态代理是基于反射设计的。Spring AOP 采用了两种混合的实现方式:JDK 动态代理和 CGLib 动态代理,分别来理解一下

  • JDK动态代理:Spring AOP的首选方法。 每当目标对象实现一个接口时,就会使用JDK动态代理。目标对象必须实现接口
  • CGLIB代理:如果目标对象没有实现接口,则可以使用CGLIB代理。

Spring 对 AOP的支持

Spring 提供了两种AOP 的实现:基于注解式配置和基于XML配置

@AspectJ 支持

为了在Spring 配置中使用@AspectJ ,你需要启用Spring支持,以根据@AspectJ切面配置Spring AOP,并配置自动代理。自动代理意味着,Spring 会根据自动代理为 Bean 生成代理来拦截方法的调用,并确保根据需要执行拦截。

可以使用XML或Java样式配置启用@AspectJ支持。 在任何一种情况下,都还需要确保AspectJ的aspectjweaver.jar 第三方库位于应用程序的类路径中(版本1.8或更高版本)。

开启@AspectJ 支持

使用@Configuration 支持@AspectJ 的时候,需要添加 @EnableAspectJAutoProxy 注解,就像下面例子展示的这样来开启 AOP代理

@Configuration @EnableAspectJAutoProxy public class AppConfig {}

也可以使用XML配置来开启@AspectJ 支持

<aop:aspectj-autoproxy/>

默认你已经添加了 aop 的schema 空间,如果没有的话,你需要手动添加

<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation=" http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">
&lt;!-- bean definitions here --&gt;

</beans>

声明一个切面

在启用了@AspectJ支持的情况下,在应用程序上下文中定义的任何bean都具有@AspectJ方面的类(具有@Aspect注释),Spring会自动检测并用于配置Spring AOP。

使用XML 配置的方式定义一个切面

<aop:aspect />

使用注解的方式定义一个切面

@Aspect public class MyAspect {}

切面(也就是用@Aspect注解的类)就像其他类一样有属性和方法。它们能够包含切入点,通知和介绍声明。

通过自动扫描检测切面

你可以在Spring XML 配置中将切面类注册为常规的bean,或者通过类路径扫描自动检测它们 – 与任何其他Spring管理的bean相同。然而,只是注解了@Aspect 的类不会被当作bean 进行管理,你还需要在类上面添加 @Component 注解,把它当作一个组件交给 Spring 管理。

定义一个切点

一个切点由两部分组成:包含名称和任何参数以及切入点表达式的签名,该表达式能够确定我们想要执行的方法。在@AspectJ注释风格的AOP中,切入点表达式需要用@Pointcut注解标注(这个表达式作为方法的签名,它的返回值必须是 void)。

@Pointcut("execution(* transfer(..))") // 切入点表达式 private void definePointcut() {}// 方法签名

切入点表达式的编写规则如下:

现在假设我们需要配置的切点仅仅匹配指定的包,就可以使用 within() 限定符来表示,如下表达式所述:

请注意我们使用了 && 操作符把 execution() 和 within() 指示器连接在一起,表示的是 和 的关系,类似的,你还可以使用 || 操作来表示 或 的关系, 使用 ! 表示 非 的关系。

除了within() 表示的限定符外,还有其它的限定符,下面是一个限定符表

AspectJ 描述符 描述
arg() 限制连接点匹配参数为指定类型的执行方法
@args() 限制连接点匹配参数由指定注解标注的执行方法
execution() 用于匹配是连接点的执行方法
this() 限制连接点匹配的AOP代理的bean引用为指定类型的类
target 限制连接点匹配目标对象为指定类型的类
@target() 限制连接点匹配特定的执行对象,这些对象对应的类要具有指定类型的注解
within() 限制连接点匹配指定的类型
@within() 限制连接点匹配指定注解所标注的类型
@annotationn 限定匹配带有指定注解的连接点

使用XML配置来配置切点

<aop:config> <aop:aspect ref = ""> <aop:poincut id = "" exdivssion="execution(** com.cxuan.aop.definePointcut(......))"/> </aop:aspect> </aop:config>

声明一个通知

通知是和切入点表达式相互关联,用于在方法执行之前,之后或者方法前后,方法返回,方法抛出异常时调用通知的方法,切入点表达式可以是对命名切入点的简单引用,也可以是在适当位置声明的切入点表达式。下面以一个例子来演示一下这些通知都是如何定义的:

上面的例子就很清晰了,定义了一个 Audience 切面,并在切面中定义了一个performance() 的切点,下面各自定义了表演之前、表演之后返回、表演失败的时候进行通知,除此之外,你还需要在main 方法中开启 @EnableAspectJAutoProxy 来开启自动代理。

除了使用Java Config 的方式外,你还可以使用基于XML的配置方式

当然,这种切点定义的比较冗余,为了解决这种类似 if...else... 灾难性的业务逻辑,你需要单独定义一个<aop:pointcut>,然后使用 pointcut-ref 属性指向上面那个标签,就像下面这样

环绕通知

在目标方法执行之前和之后都可以执行额外代码的通知。在环绕通知中必须显式的调用目标方法,目标方法才会执行,这个显式调用时通过ProceedingJoinPoint来实现的,可以在环绕通知中接收一个此类型的形参,spring容器会自动将该对象传入,注意这个参数必须处在环绕通知的第一个形参位置。

环绕通知需要返回返回值,否则真正调用者将拿不到返回值,只能得到一个null。下面是环绕通知的一个示例

<aop:around method="around" pointcut-ref="pc1"/> public Object around(ProceedingJoinPoint jp) throws Throwable{ System.out.println("1 -- around before..."); Object obj = jp.proceed(); //--显式的调用目标方法 System.out.println("1 -- around after..."); return obj; }

文章参考:

https://juejin.im/post/5a695b3cf265da3e47449471

《Spring In Action》

https://docs.spring.io/spring/docs/5.1.9.RELEASE/spring-framework-reference/core.html

Spring AOP 五大通知类型https://www.cnblogs.com/chuijingjing/p/9806651.html)


# @Bean注解全解析-Java面试题


随着SpringBoot的流行,基于注解式开发的热潮逐渐覆盖了基于XML纯配置的开发,而作为Spring中最核心的bean当然也能够使用注解的方式进行表示。所以本篇就来详细的讨论一下作为Spring中的Bean到底都有哪些用法。

@Bean 基础声明

Spring的@Bean注解用于告诉方法,产生一个Bean对象,然后这个Bean对象交给Spring管理。产生这个Bean对象的方法Spring只会调用一次,随后这个Spring将会将这个Bean对象放在自己的IOC容器中。

SpringIOC 容器管理一个或者多个bean,这些bean都需要在@Configuration注解下进行创建,在一个方法上使用@Bean注解就表明这个方法需要交给Spring进行管理。

快速搭建一个maven项目并配置好所需要的Spring 依赖

<dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>4.3.13.RELEASE</version> </dependency>

<dependency> <groupId>org.springframework</groupId> <artifactId>spring-beans</artifactId> <version>4.3.13.RELEASE</version> </dependency>

<dependency> <groupId>org.springframework</groupId> <artifactId>spring-core</artifactId> <version>4.3.13.RELEASE</version> </dependency>

<dependency> <groupId>org.springframework</groupId> <artifactId>spring-web</artifactId> <version>4.3.13.RELEASE</version> </dependency>

在src根目录下创建一个AppConfig的配置类,这个配置类也就是管理一个或多个bean 的配置类,并在其内部声明一个myBean的bean,并创建其对应的实体类

@Configuration public class AppConfig {

// 使用@Bean 注解表明myBean需要交给Spring进行管理
// 未指定bean 的名称,默认采用的是 "方法名" + "首字母小写"的配置方式
@Bean
public MyBean myBean(){
    return new MyBean();
}

}

public class MyBean {

public MyBean(){
    System.out.println("MyBean Initializing");
}

}

在对应的test文件夹下创建一个测试类SpringBeanApplicationTests,测试上述代码的正确性

public class SpringBeanApplicationTests {

public static void main(String[] args) {
    ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
    context.getBean("myBean");
}

}

输出 : MyBean Initializing

随着SpringBoot的流行,我们现在更多采用基于注解式的配置从而替换掉了基于XML的配置,所以本篇文章我们主要探讨基于注解的@Bean以及和其他注解的使用。

@Bean 基本构成及其使用

在简单介绍了一下如何声明一个Bean组件,并将其交给Spring进行管理之后,下面我们来介绍一下Spring 的基本构成

@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface Bean {

@AliasFor("name")
String[] value() default {};

@AliasFor("value")
String[] name() default {};

Autowire autowire() default Autowire.NO;

String initMethod() default "";

String destroyMethod() default AbstractBeanDefinition.INFER_METHOD;

}

@Bean不仅可以作用在方法上,也可以作用在注解类型上,在运行时提供注册。

value: name属性的别名,在不需要其他属性时使用,也就是说value 就是默认值

name: 此bean 的名称,或多个名称,主要的bean的名称加别名。如果未指定,则bean的名称是带注解方法的名称。如果指定了,方法的名称就会忽略,如果没有其他属性声明的话,bean的名称和别名可能通过value属性配置

autowire : 此注解的方法表示自动装配的类型,返回一个Autowire类型的枚举,我们来看一下Autowire枚举类型的概念

// 枚举确定自动装配状态:即,bean是否应该使用setter注入由Spring容器自动注入其依赖项。 // 这是Spring DI的核心概念 public enum Autowire {

// 常量,表示根本没有自动装配。 NO(AutowireCapableBeanFactory.AUTOWIRE_NO), // 常量,通过名称进行自动装配 BY_NAME(AutowireCapableBeanFactory.AUTOWIRE_BY_NAME), // 常量,通过类型进行自动装配 BY_TYPE(AutowireCapableBeanFactory.AUTOWIRE_BY_TYPE);

private final int value;
Autowire(int value) {
    this.value = value;
}
public int value() {
    return this.value;
}
public boolean isAutowire() {
    return (this == BY_NAME || this == BY_TYPE);
}

}

autowire的默认值为No,默认表示不通过自动装配。

initMethod: 这个可选择的方法在bean实例化的时候调用,InitializationBean接口允许bean在合适的时机通过设置注解的初始化属性从而调用初始化方法,InitializationBean 接口有一个定义好的初始化方法

void afterPropertiesSet() throws Exception;

Spring不推荐使用InitializationBean 来调用其初始化方法,因为它不必要地将代码耦合到Spring。Spring推荐使用@PostConstruct注解或者为POJO类指定其初始化方法这两种方式来完成初始化。

不推荐使用:

public class InitBean implements InitializingBean {

public void afterPropertiesSet() {}

}

destroyMethod: 方法的可选择名称在调用bean示例在关闭上下文的时候,例如JDBC的close()方法,或者SqlSession的close()方法。DisposableBean 接口的实现允许在bean销毁的时候进行回调调用,DisposableBean 接口之后一个单个的方法

void destroy() throws Exception;

Spring不推荐使用DisposableBean 的方式来初始化其方法,因为它会将不必要的代码耦合到Spring。作为替代性的建议,Spring 推荐使用@divDestory注解或者为@Bean注解提供 destroyMethod 属性,

不推荐使用:

public class DestroyBean {

public void cleanup() {}

}

推荐使用:

public class MyBean {

public MyBean(){
    System.out.println("MyBean Initializing");
}

public void init(){
    System.out.println("Bean 初始化方法被调用");
}

public void destroy(){
    System.out.println("Bean 销毁方法被调用");
}

}

@Configuration public class AppConfig {

// @Bean @Bean(initMethod = "init", destroyMethod = "destroy") public MyBean myBean(){ return new MyBean(); }

}

修改一下测试类,测试其初始化方法和销毁方法在何时会被调用

public class SpringBeanApplicationTests {

public static void main(String[] args) {

    // ------------------------------ 测试一  ------------------------------
    ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);

// context.getBean("myBean");

    // 变体
    context.getBean("myBean");
    ((AnnotationConfigApplicationContext) context).destroy();

// ((AnnotationConfigApplicationContext) context).close(); } }

初始化方法在得到Bean的实例的时候就会被调用,销毁方法在容器销毁或者容器关闭的时候会被调用。

@Bean 注解与其他注解产生的火花

在上面的一个小节中我们了解到了@Bean注解的几个属性,但是对于@Bean注解的功能来讲这有点太看不起bean了,@Bean另外一个重要的功能是能够和其他注解产生化学反应,如果你还不了解这些注解的话,那么请继续往下读,你会有收获的

这一节我们主要探讨@profile,@scope,@lazy,@depends-on @primary等注解

@Profile 注解

@Profile的作用是把一些meta-data进行分类,分成Active和InActive这两种状态,然后你可以选择在active 和在Inactive这两种状态下配置bean,在Inactive状态通常的注解有一个!操作符,通常写为:@Profile("!p"),这里的p是Profile的名字。

三种设置方式:

  • 可以通过ConfigurableEnvironment.setActiveProfiles()以编程的方式激活

  • 可以通过AbstractEnvironment.ACTIVE_PROFILES_PROPERTY_NAME (spring.profiles.active )属性设置为

    JVM属性

  • 作为环境变量,或作为web.xml 应用程序的Servlet 上下文参数。也可以通过@ActiveProfiles 注解在集成测试中以声明方式激活配置文件。

作用域

  • 作为类级别的注释在任意类或者直接与@Component 进行关联,包括@Configuration 类
  • 作为原注解,可以自定义注解
  • 作为方法的注解作用在任何方法

注意:

如果一个配置类使用了Profile 标签或者@Profile 作用在任何类中都必须进行启用才会生效,如果@Profile({"p1","!p2"}) 标识两个属性,那么p1 是启用状态 而p2 是非启用状态的。

现有一个POJO类为Subject学科类,里面有两个属性,一个是like(理科)属性,一个是wenke(文科)属性,分别有两个配置类,一个是AppConfigWithActiveProfile ,一个是AppConfigWithInactiveProfile,当系统环境是 "like"的时候就注册 AppConfigWithActiveProfile ,如果是 "wenke",就注册 AppConfigWithInactiveProfile,来看一下这个需求如何实现

Subject.java

// 学科 public class Subject {

// 理科
private String like;
// 文科
private String wenke;

get and set ...

@Override
public String toString() {
    return "Subject{" +
            "like='" + like + '\'' +
            ", wenke='" + wenke + '\'' +
            '}';
}

}

AppConfigWithActiveProfile.java 注册Profile 为like 的时候

@Profile("like") @Configuration public class AppConfigWithActiveProfile {

@Bean
public Subject subject(){
    Subject subject = new Subject();
    subject.setLike("物理");
    return subject;
}

}

AppConfigWithInactiveProfile.java 注册Profile 为wenke 的时候

@Profile("wenke") @Configuration public class AppConfigWithInactiveProfile {

@Bean
public Subject subject(){
    Subject subject = new Subject();
    subject.setWenke("历史");
    return subject;
}

}

修改一下对应的测试类,设置系统环境,当Profile 为like 和 wenke 的时候分别注册各自对应的属性

// ------------------------------ 测试 profile ------------------------------ AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); // 激活 like 的profile context.getEnvironment().setActiveProfiles("like"); context.register(AppConfigWithActiveProfile.class,AppConfigWithInactiveProfile.class); context.refresh(); Subject subject = (Subject) context.getBean("subject"); System.out.println("subject = " + subject);

把context.getEnvironment().setActiveProfiles("wenke") 设置为wenke,观察其对应的输出内容发生了变化,这就是@Profile的作用,有一层可选择性注册的意味。

@Scope 注解

在Spring中对于bean的默认处理都是单例的,我们通过上下文容器.getBean方法拿到bean容器,并对其进行实例化,这个实例化的过程其实只进行一次,即多次getBean 获取的对象都是同一个对象,也就相当于这个bean的实例在IOC容器中是public的,对于所有的bean请求来讲都可以共享此bean。

那么假如我不想把这个bean被所有的请求共享或者说每次调用我都想让它生成一个bean实例该怎么处理呢?

多例Bean

bean的非单例原型范围会使每次发出对该特定bean的请求时都创建新的bean实例,也就是说,bean被注入另一个bean,或者通过对容器的getBean()方法调用来请求它,可以用如下图来表示:

通过一个示例来说明bean的多个实例

新建一个AppConfigWithAliasAndScope配置类,用来定义多例的bean,

@Configuration public class AppConfigWithAliasAndScope {

/**
 * 为myBean起两个名字,b1 和 b2
 * @Scope 默认为 singleton,但是可以指定其作用域
 * prototype 是多例的,即每一次调用都会生成一个新的实例。
 */
@Bean({"b1","b2"})
@Scope("prototype")
public MyBean myBean(){
    return new MyBean();
}

}

测试一下多例的情况:

// ------------------------------ 测试scope ------------------------------ ApplicationContext context = new AnnotationConfigApplicationContext(AppConfigWithAliasAndScope.class); MyBean myBean = (MyBean) context.getBean("b1"); MyBean myBean2 = (MyBean) context.getBean("b2"); System.out.println(myBean); System.out.println(myBean2);

其他情况

除了多例的情况下,Spring还为我们定义了其他情况:

Scope Descriptionn
singleton 默认单例的bean定义信息,对于每个IOC容器来说都是单例对象
prototype bean对象的定义为任意数量的对象实例
request bean对象的定义为一次HTTP请求的生命周期,也就是说,每个HTTP请求都有自己的bean实例,它是在单个bean定义的后面创建的。仅仅在web-aware的上下文中有效
session bean对象的定义为一次HTTP会话的生命周期。仅仅在web-aware的上下文中有效
application bean对象的定义范围在ServletContext生命周期内。仅仅在web-aware的上下文中有效
websocket bean对象的定义为WebSocket的生命周期内。仅仅在web-aware的上下文中有效

singleton和prototype 一般都用在普通的Java项目中,而request、session、application、websocket都用于web应用中。

request、session、application、websocket的作用范围

你可以体会到 request、session、application、websocket 的作用范围在当你使用web-aware的ApplicationContext应用程序上下文的时候,比如XmlWebApplicationContext的实现类。如果你使用了像是ClassPathXmlApplicationContext的上下文环境时,就会抛出IllegalStateException因为Spring不认识这个作用范围。

@Lazy 注解

@Lazy : 表明一个bean 是否延迟加载,可以作用在方法上,表示这个方法被延迟加载;可以作用在@Component (或者由@Component 作为原注解) 注释的类上,表明这个类中所有的bean 都被延迟加载。如果没有@Lazy注释,或者@Lazy 被设置为false,那么该bean 就会急切渴望被加载;除了上面两种作用域,@Lazy 还可以作用在@Autowired和@Inject注释的属性上,在这种情况下,它将为该字段创建一个惰性代理,作为使用ObjectFactory或Provider的默认方法。下面来演示一下:

@Lazy @Configuration @ComponentScan(basePackages = "com.spring.configuration.pojo") public class AppConfigWithLazy {

@Bean
public MyBean myBean(){
    System.out.println("myBean Initialized");
    return new MyBean();
}

@Bean
public MyBean IfLazyInit(){
    System.out.println("initialized");
    return new MyBean();
}

}

  • 修改测试类
public class SpringConfigurationApplication {

public static void main(String[] args) {

    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfigWithLazy.class);

    // 获取启动过程中的bean 定义的名称
    for(String str : context.getBeanDefinitionNames()){
        System.out.println("str = " + str);
    }
}

}

输出你会发现没有关于bean的定义信息,但是当把@Lazy 注释拿掉,你会发现输出了关于bean的初始化信息

@DependsOn 注解

指当前bean所依赖的bean。任何指定的bean都能保证在此bean创建之前由IOC容器创建。在bean没有通过属性或构造函数参数显式依赖于另一个bean的情况下很少使用,可能直接使用在任何直接或者间接使用 Component 或者Bean 注解表明的类上。来看一下具体的用法

新建三个Bean,分别是FirstBean、SecondBean、ThirdBean三个普通的bean,新建AppConfigWithDependsOn并配置它们之间的依赖关系

public class FirstBean {

@Autowired
private SecondBean secondBean;

@Autowired
private ThirdBean thirdBean;

public FirstBean() {
    System.out.println("FirstBean Initialized via Constuctor");
}

}

public class SecondBean {

public SecondBean() {
    System.out.println("SecondBean Initialized via Constuctor");
}

}

public class ThirdBean {

public ThirdBean() {
    System.out.println("ThirdBean Initialized via Constuctor");
}

}

@Configuration public class AppConfigWithDependsOn {

@Bean("firstBean")
@DependsOn(value = {
        "secondBean",
        "thirdBean"
})
public FirstBean firstBean() {
    return new FirstBean();
}

@Bean("secondBean")
public SecondBean secondBean() {
    return new SecondBean();
}

@Bean("thirdBean")
public ThirdBean thirdBean() {
    return new ThirdBean();
}

}

使用测试类进行测试,如下

// ------------------------------ 测试 DependsOn ------------------------------ AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfigWithDependsOn.class); context.getBean(FirstBean.class); context.close();

输出 :

SecondBean Initialized via Constuctor ThirdBean Initialized via Constuctor FirstBean Initialized via Constuctor

由于firstBean 的创建过程首先需要依赖secondBean 和 thirdBean的创建,所以secondBean 首先被加载其次是thirdBean 最后是firstBean。

如果把@DependsOn 注解加在AppConfigWithDependsOn 类上则它们的初始化顺序就会变为 firstBean、secondBean、thirdBean

@Primary 注解

指示当多个候选者有资格自动装配依赖项时,应优先考虑bean。此注解在语义上就等同于在Spring XML中定义的bean 元素的primary属性。注意: 除非使用component-scanning进行组件扫描,否则在类级别上使用@Primary不会有作用。如果@Primary 注解定义在XML中,那么@Primary 的注解元注解就会忽略,相反使用

@Primary 的两种使用方式

  • 与@Bean 一起使用,定义在方法上,方法级别的注解
  • 与@Component 一起使用,定义在类上,类级别的注解

通过一则示例来演示一下:

新建一个AppConfigWithPrimary类,在方法级别上定义@Primary注解

@Configuration public class AppConfigWithPrimary {

@Bean
public MyBean myBeanOne(){
    return new MyBean();
}

@Bean
@Primary
public MyBean myBeanTwo(){
    return new MyBean();
}

}

上面代码定义了两个bean ,其中myBeanTwo 由@Primary 进行标注,表示它首先会进行注册,使用测试类进行测试

// ------------------------------ 测试 Primary ------------------------------ AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfigWithPrimary.class); MyBean bean = context.getBean(MyBean.class); System.out.println(bean);

你可以尝试放开@Primary ,使用测试类测试的话会发现出现报错信息,因为你尝试获取的是MyBean.class,而我们代码中定义了两个MyBean 的类型,所以需要@Primary 注解表明哪一个bean需要优先被获取。

文章参考:

spring @Profile的运用示例

https://www.javaguides.net/2018/10/spring-dependson-annotation-example.html


# BeanFactory和ApplicationContext的异同-Java面试题


相同:

  • Spring提供了两种不同的IOC 容器,一个是BeanFactory,另外一个是ApplicationContext,它们都是Java interface,ApplicationContext继承于BeanFactory(ApplicationContext继承ListableBeanFactory。
  • 它们都可以用来配置XML属性,也支持属性的自动注入。
  • 而ListableBeanFactory继承BeanFactory),BeanFactory 和 ApplicationContext 都提供了一种方式,使用getBean("bean name")获取bean。

BeanFactory 获取bean注册信息

public class HelloWorldApp{ public static void main(String[] args) { XmlBeanFactory factory = new XmlBeanFactory (new ClassPathResource("beans.xml")); HelloWorld obj = (HelloWorld) factory.getBean("helloWorld"); obj.getMessage(); } }

ApplicationContext 获取bean注册信息

public class HelloWorldApp{ public static void main(String[] args) { ApplicationContext context=new ClassPathXmlApplicationContext("beans.xml"); HelloWorld obj = (HelloWorld) context.getBean("helloWorld"); obj.getMessage(); } }

但是他们在工作和特性上有一些不同:

  • 当你调用getBean()方法时,BeanFactory仅实例化bean,而ApplicationContext 在启动容器的时候实例化单例bean,不会等待调用getBean()方法时再实例化。
  • BeanFactory不支持国际化,即i18n,但ApplicationContext提供了对它的支持。
  • BeanFactory与ApplicationContext之间的另一个区别是能够将事件发布到注册为监听器的bean。
  • BeanFactory 的一个核心实现是XMLBeanFactory 而ApplicationContext 的一个核心实现是ClassPathXmlApplicationContext,Web容器的环境我们使用WebApplicationContext并且增加了getServletContext 方法。
  • 如果使用自动注入并使用BeanFactory,则需要使用API注册AutoWiredBeanPostProcessor,如果使用ApplicationContext,则可以使用XML进行配置。
  • 简而言之,BeanFactory提供基本的IOC和DI功能,而ApplicationContext提供高级功能,BeanFactory可用于测试和非生产使用,但ApplicationContext是功能更丰富的容器实现,应该优于BeanFactory

# 程序员们平时都喜欢逛什么论坛呢?-Java面试题


程序员的工作和日常生活非常的枯燥,这里给大家推荐一些程序员经常使用的网站,也是我经常上的一些网站,我将会从多个角度、多个层面为你整理归纳这些网站和论坛

项目类

项目是一个面试官非常看重的点,也是拓展视野、挖掘轮子一个的地方,下面就为你推荐几个程序员都应该 mark 的项目类网站。

  1. Github 代码托管 https://github.com

作为开源代码库以及版本控制系统,Github 拥有140多万开发者用户。随着越来越多的应用程序转移到了云上,Github 已经成为了管理软件开发以及发现已有代码的首选方法。代码托管必备。

Github 也有非常多好的项目可以推荐,比如我自己的 github (逃了)

https://github.com/crisxuan/bestJavaer

还有其他非常多优秀的 Github ,比如 CS-notes、JavaGuide、mall 商城项目

  1. 码云代码托管平台 Gitee | Software Development and Collaboration Platform

码云托管平台是中国的,之所以国内自己开发了一个码云平台,是因为 github 涉及到 fq,你懂的,而且码云是开源中国的托管平台,会定期评选一些优秀的项目,你不可错过!

  1. Gitlab https://about.gitlab.com/

对于有些人,提到GitHub就会自然的想到Gitlab,Gitlab支持无限的公有项目和私有项目。其首页截图如下

  1. coding.net https://coding.net

谈到 coding.net,首先必须提的是速度快,功能与开源中国相似,同样一个账号最多可以创建 1000 个项目,也支持任务的创建等

博客类

  1. CSDN https://www.csdn.net/

中国专业IT社区CSDN (Chinese Software Developer Network) 创立于1999年,致力于为中国软件开发者提供知识传播、在线学习、职业发展等服务。中国最大、最具专业的博客平台,同时也是质量最高的博客平台

  1. 博客园 http://www.cnblogs.com

这样一个不起眼的地方,却吸引了很多IT技术精英,把这里当作自己的网上家园,每天在这里分享着精彩的原创内容,也许他们看重的不是华丽的外表、诱人的虚名,而是纯净、专注、对技术人员的理解。

  1. 掘金 https://juejin.im/

掘金现在被字节跳动收购了,内容审核变得越来越严格,也涌现了很多大佬,他们写的文章非常帮,强烈推荐大家每天逛一逛,博客内容都是经过层层筛选的,非常值得一看

  1. 思否 https://segmentfault.com/

思否上面有很多大佬,不得不说思否的界面做的非常清新,舒服,这就让人很喜欢这个博客平台,目前看来还是比较小众,但是上面的开发者的互动很多,是一个很温馨的地方。

  1. infoq https://www.infoq.cn/?utm_source=infoq&utm_medium=toutubiaoti

infoq 是最近兴起的一个技术社区,界面非常小清新,让人感觉很舒服,目前入驻的开发者倒不是很多,但是 infoq 依托极客邦的大流量和资源,做起来应该很快的,我入驻了 infoq ,感觉里面还是有很多大佬的,推荐大家经常浏览一波

  1. GitChat https://gitbook.cn

GitChat 是一个付费学习网站,当然也支持免费模式,因为付费所以相对文章质量比较高,但是免费的文章同样也很不错,而且我就在 GitChat 上面写了很多免费文章,不应该是很多,应该是全部免费,我的 gitchat 主页如下 程序员cxuan 的 GitChat

  1. V2EX https://www.v2ex.com

无论你是在大学进行人生最重要阶段的学习,或者是在中国的某座城市工作,或者是在外太空的某个天体如 Sputnik 1 上享受人生,在注册进入 V2EX 之后,你都可以为自己设置一个所在地,从而找到更多和你在同一区域的朋友。

  1. OSCHINA https://www.oschina.net

这个网站和 CSDN 一样是国内最大程序员社区,各种教程、资源、工具、书籍都是可以找到的。在社区里,你可以尽情的写博客,发动态,激烈的讨论问题,只有在这种环境下才能激起你学习的热情。

  1. 简书 https://www.jianshu.com/

简书是一个不仅仅为程序员提供的创作分享社区,上面充满了各种各样精彩的博文,也有很多优秀的博主,但是相比较与 CSDN 个人认为在技术创作方面就略低一筹了。

  1. dev https://dev.to/

dev 社区和国内的掘金社区很相似,技术分类也比较多,各种技术应有尽有,文章质量都很不错。

  1. dreamincode https://www.dreamincode.net

dreamincode 是一个相对小众的技术博客,风格简约,但是内容却不简单。

  1. bytes https://bytes.com

bytes 和 dreamcode 类似,简约但不简单。

  1. hongkiat https://www.hongkiat.com/blog/design-dev

hongkiat是与技术、设计领域相关的站点之一,大家可以在这里分享技术文章。

  1. IBM Developer https://developer.ibm.com

这里面都是一线工程师,技术性很强,每一篇文章都值得细细观看,认真学习。

问答类

  1. 知乎 https://www.zhihu.com/

有问题,上知乎。知乎,可信赖的问答社区,以让每个人高效获得可信赖的解答为使命。

本是一个分享各种人生杂谈,和一些鲜为人知以及各种大牛的免费和付费的知识型网站。由于流量逐步扩大,吸引了大批优秀的程序员在上面分享自己的技术创作,也是一个程序员常去的网站之一,不仅仅是为了学习技术。

  1. 思否问答 https://segmentfault.com

SegmentFault 思否是中国领先的新一代开发者社区和专业的技术媒体。我们为中文开发者提供纯粹、高质的技术交流平台以及最前沿的技术行业动态,帮助更多的开发者获得认知

  1. StackOverFlow https://stackoverflow.com

这是一个由外国人创办的专为程序员提供的国际性问题解答交流社区,正如网站签名:Stack Overflow – Where Developers Learn, Share, & Build Careers。这个网站非常的纯粹,一般人还真不太习惯用这个,没有一手好英语还看不太懂全英文的技术交流与问答。

  1. Reddit https://www.reddit.com

reddit是一个非常个性的社区,你可以在这里讨论编程问题,还可以学习学英语,reddit 还很幽默,有古怪的莫名笑点,评论接楼很有意思。

  1. daniweb https://www.daniweb.com

daniweb也是一个质量比较高的问答平台,有一种像社交平台的感觉。

教程类

  1. 菜鸟教程 https://www.runoob.com

菜鸟教程提供了编程的基础技术教程, 介绍了 HTML、CSS、Javascript、Python、Java、Ruby、C、PHP、MySQL 等各种编程语言的基础知识,是个小白入门,学习语言的好地方。

  1. w3schoool

在W3School,你可以找到你所需要的所有的网站建设教程。 从基础的 HTML 到 CSS,乃至进阶的 XML、SQL、JS、PHP 和 ASP.NET。 从左侧的菜单选择你需要的教程! 和菜鸟教程十分相似的网站。

  1. 易百教程 https://www.yiibai.com

易百网是一个内容全面的教程网站,专注于 VBScript, MATLAB, EJB, IPv6, IPv4, 电子商务, PostgreSQL, SQLite, SDLC, Assembly, 操作系统, JSON, iOS, 设计模式, VB.Net, 计算机基础知识。

  1. Bilibili1 https://www.bilibili.com

期初这个网站是由游戏玩家视频火起来的,尤其LOL骨灰级玩家遍布其中。bilibili 是国内知名的视频弹幕网站,通过动漫打出了名声,最近两年发展势头迅猛,里面有不少有创意的 Up 主,不乏一些有趣的程序员。

  1. 中国大学MOOC网 icourse163.org

中国大学 MOOC(慕课) 是国内优质的中文 MOOC 学习平台,由爱课程网携手网易云课堂打造。平台拥有包括 985 高校在内提供的千余门课程。在这里,每一个有意愿提升自己的人都可以免费获得更优质的高等教育。

  1. 慕课网-程序员的梦工厂 https://www.imooc.com

慕课网(IMOOC)是IT技能学习平台。慕课网(IMOOC)提供了丰富的移动端开发、php开发、web前端、android开发以及 html5 等视频教程资源公开课。

  1. 网易云课堂 https://study.163.com

网易云课堂,是网易公司打造的在线实用技能学习平台,主要为学习者提供海量、优质的课程,用户可以根据自身的学习程度,自主安排学习进度。涵盖实用软件、IT与互联网、外语学习、生活家居、兴趣爱好、职场技能、金融管理、考试认证、中小学、亲子教育等十余大门类。

  1. 实验楼 https://www.lanqiao.cn/courses

实验楼这个网站我直接吹爆,无需配置繁琐的本地环境,随时在线使用。

  1. tutorialspoint https://www.tutorialspoint.com/index.htm

这是一个在线学习的网站,并且免费,里面有各种技术、各个知识点的讲解和demo,灰常全面,这比查找API方便多了,遇到不明白的知识点直接根据索引找就是了,还有各种电子书。

  1. codecademyhttps://www.codecademy.com

学习新语言,敲代码玩就在这里了。这个网站将简化编程学习的过程。比如说网站左边会讲解知识点,右边直接练习。如果出现错误,就会有错误提示,直接给你反馈。所以,使用它不用想太多,直接拼命硬干敲代码入门。

  1. Livecoding.tv https://www.livecoding.tv/accounts/login/

Livecoding.tv 由一群欧美程序爱好者共同发起成立,旨在为全球程序员提供一个实时高效的互动平台。特色是使用了录屏直播技术,用户可以在线观看高手实时编程并且可以向对方提问互动,网站现在已经汇集了一大批程序精英。现在 Livecoding.TV 来到中国,希望更多的中国程序员加入进来一起切磋技术。

  1. Dzone https://dzone.com

Dzone 是一个技术涵盖比较全面的网站,像云平台、数据库、物联网、开发运维、Java 语言等都有。

  1. simpleprogrammer https://simpleprogrammer.com/

simpleprogrammer与其他技术类的社区不太一样,在这里并没有很多技术类的文章,更多的是指导建议性的文章,讲述了一些人生道理,职场规则,编程生涯的一些文章。

  1. SitePoint https://www.sitepoint.com/web

通过 SitePoint 教程,课程和书籍学习 Web 设计和开发-HTML5,CSS3,JavaScript,PHP,移动应用,这是一个偏向前端方向的网站,在这里包含了各种高质量的前端方面的文章,电子书。

  1. YouTobe https://www.youtube.com/

YouTobe 这个网站可算是经典,和国内的哔哩哔哩一样,各类视频汇聚于此,当然各国编程大神也在这啦。

算法类

  1. LeetCode https://leetcode-cn.com

几乎每个算法大牛都知道的神奇网站,这个网站上面有:算法、数据库、Shell、多线程等多种类型供你学习。多数人在上面练习编程算法,尤其是给想进入一线互联网公司的技术人员,提供了一个免费又方便的题库。面试前都会在上面进行长期和充分的刷题,是你的不二选择。

  1. LintCode https://www.lintcode.com

LintCode 领扣上有数量超过 1000 道的算法题目和人工智能题目,通过刷题熟练掌握数据结构和算法。完成各大名企的阶梯训练,为你斩获心仪的 offer 打下坚实的基础

  1. 洛谷 https://www.luogu.com.cn

洛谷创办于 2013 年,致力于为参加 noip、noi、acm 的选手提供清爽、快捷的编程体验。它拥有在线测题系统、强大的社区、在线学习功能,也是一个很好练习刷题的网站啦

  1. Codeforces http://codeforces.com/

Codeforces 是一家为计算机编程爱好者提供的在线评测系统该网站由萨拉托夫国立大学的一个团体创立并负责运营。在编程挑战赛中,选手有 2 个小时的时间去解决 5 道题,通过得分排名,选手可以看到实时的排名,也可以选择查看好友的排名,还可以看到某题有多少人通过等信息。

  1. Topcoder https://www.topcoder.com

Topcoder 据说是世界上规模最大的编程网站,这里面的题型,比赛形式跟 ACM/ICPC 极不相同。该网站把中国纳入其赛区,大家可以上去那里跟来自全世界的程序员(事实上大多数也是大学生)进行更直接的交流,可能也是ACM/ICPC 练兵的好地方吧。

接单类

  1. 程序员客栈 https://www.proginn.com

程序员客栈是领先的程序员自由远程工作平台,未来互联网企业用人方式。提供优秀程序员为您进行网站建设制作、测试运维服务、人工智能 AI、大数据区块链、软件开发等优质服务。

  1. 码市 https://codemart.com/developers

码市是互联网软件外包服务平台,意在连接需求方与广大开发者。让项目的需求方快速的找到合适的开发者,完成项目开发工作。

  1. 猿急送 https://www.yuanjisong.com/job

猿急送为您提供兼职程序员,兼职工程师信息,猿急送是一个高级技术共享平台,是优质的程序员兼职网站,这里汇聚 BAT 等知名互联网公司的技术开发、产品、设计大牛。

  1. 开源众包 https://zb.oschina.net

开源众包–专业的软件众包平台,350万+ 优质开发者为您提供网站、APP、微信/小程序、企业应用等软件开发服务,有效降低企业 IT 软件开发成本、解决技术资源不足等问题。

  1. 实现网 https://shixian.com

这个比较高级,是一些知名公司技术人员兼职的平台。我们可以在线约好去其他公司兼职坐班。实现网为企业提供BAT 等名企背景的、靠谱的开发设计兼职人才和自由职业者,满足企业项目外包、驻场开发、远程兼职、技术咨询等短期人力需求。

  1. 猪八戒 https://shenyang.zbj.com

猪八戒网企业外包服务,中国领先的灵活用工平台,其中服务品类涵盖LOGO设计、UI设计、营销推广、网站建设、装修设计、工业设计、文案策划、知识产权的服务。

  1. 码易众包平台 https://www.mayigeek.com

码易是智网易联旗下 IT 软件服务平台,集软件商城、企业应用、电商软件、crm 软件、商务服务平台于一体的一站式软件外包开发服务平台。

求职类

  1. 牛客网 https://www.nowcoder.com

求职之前,先上牛客,就业找工作一站解决。这个网站不像 csdn 和 OSChina 以技术博客论坛为主了。但是在你需要的时候,却是相当有价值,里面有面试技巧、各种知名的不知名的互联网公司的对应往年校招社招面试题库,刷到你手软,一般应届生用这个比较多。

  1. 拉勾网 https://www.lagou.com

拉勾招聘是专业的互联网求职招聘网站。致力于提供真实可靠的互联网岗位求职招聘找工作信息,拥有海量的互联网人才储备,互联网行业找工作就上拉勾招聘,值得信赖的求职。

  1. Boss直聘 https://www.zhipin.com

BOSS直聘是权威领先的招聘网,开启人才网招聘求职新时代,让求职者与 Boss 直接开聊、加快面试、即时反馈,是一个致力于为招聘者和求职者搭建高效沟通、信息对等的平台

  1. 猎聘 https://www.liepin.com

猎聘,2018年香港上市。作为中国知名中高端人才求职招聘平台,汇聚56万+知名企业、16万+认证猎头,为5700万用户提供高薪工作岗位。总之,猎聘还是非常不错的。


# 操作系统之死锁-Java面试题


  • 操作系统之死锁
    • 前言
    • 资源
      • 资源获取
    • 死锁
      • 资源死锁的条件
      • 死锁模型
    • 鸵鸟算法
    • 死锁检测和恢复
      • 每种类型一个资源的死锁检测方式
      • 从死锁中恢复
        • 通过抢占进行恢复
        • 通过回滚进行恢复
        • 杀死进程恢复
    • 死锁避免
      • 单个资源的银行家算法
    • 破坏死锁
      • 破坏互斥条件
      • 破坏保持等待的条件
      • 破坏不可抢占条件
      • 破坏循环等待条件
    • 其他问题
      • 两阶段加锁
      • 通信死锁
      • 活锁
      • 饥饿
    • 总结
    • 尾声
    • 关于我

前言

计算机系统中有很多独占性的资源,在同一时刻只能每个资源只能由一个进程使用,我们之前经常提到过打印机,这就是一个独占性的资源,同一时刻不能有两个打印机同时输出结果,否则会引起文件系统的瘫痪。所以,操作系统具有授权一个进程单独访问资源的能力。

两个进程独占性的访问某个资源,从而等待另外一个资源的执行结果,会导致两个进程都被阻塞,并且两个进程都不会释放各自的资源,这种情况就是 死锁(deadlock)。

死锁可以发生在任何层面,在不同的机器之间可能会发生死锁,在数据库系统中也会导致死锁,比如进程 A 对记录 R1 加锁,进程 B 对记录 R2 加锁,然后进程 A 和 B 都试图把对象的记录加锁,这种情况下就会产生死锁。

下面我们就来讨论一下什么是死锁、死锁的条件是什么、死锁如何预防、活锁是什么等。

首先你需要先了解一个概念,那就是资源是什么

资源

大部分的死锁都和资源有关,在进程对设备、文件具有独占性(排他性)时会产生死锁。我们把这类需要排他性使用的对象称为资源(resource)。资源主要分为 可抢占资源和不可抢占资源

可抢占资源和不可抢占资源

资源主要有可抢占资源和不可抢占资源。可抢占资源(divemptable resource) 可以从拥有它的进程中抢占而不会造成其他影响,内存就是一种可抢占性资源,任何进程都能够抢先获得内存的使用权。

不可抢占资源(nondivemtable resource) 指的是除非引起错误或者异常,否则进程无法抢占指定资源,这种不可抢占的资源比如有光盘,在进程执行调度的过程中,其他进程是不能得到该资源的。

死锁与不可抢占资源有关,虽然抢占式资源也会造成死锁,不过这种情况的解决办法通常是在进程之间重新分配资源来化解。所以,我们的重点自然就会放在了不可抢占资源上。

下面给出了使用资源所需事件的抽象顺序

如果在请求时资源不存在,请求进程就会强制等待。在某些操作系统中,当请求资源失败时进程会自动阻塞,当自资源可以获取时进程会自动唤醒。在另外一些操作系统中,请求资源失败并显示错误代码,然后等待进程等待一会儿再继续重试。

请求资源失败的进程会陷入一种请求资源、休眠、再请求资源的循环中。此类进程虽然没有阻塞,但是处于从目的和结果考虑,这类进程和阻塞差不多,因为这类进程并没有做任何有用的工作。

请求资源的这个过程是很依赖操作系统的。在一些系统中,一个 request 系统调用用来允许进程访问资源。在一些系统中,操作系统对资源的认知是它是一种特殊文件,在任何同一时刻只能被一个进程打开和占用。资源通过 open 命令进行打开。如果文件已经正在使用,那么这个调用者会阻塞直到当前的占用文件的进程关闭文件为止。

资源获取

对于一些数据库系统中的记录这类资源来说,应该由用户进程来对其进行管理。有一种管理方式是使用信号量(semaphore) 。这些信号量会初始化为 1 。互斥锁也能够起到相同的作用。

这里说一下什么是互斥锁(Mutexes):

在计算机程序中,互斥对象(mutex) 是一个程序对象,它允许多个程序共享同一资源,例如文件访问权限,但并不是同时访问。需要锁定资源的线程都必须在使用资源时将互斥锁与其他线程绑定(进行加锁)。当不再需要数据或线程结束时,互斥锁设置为解锁。

下面是一个伪代码,这部分代码说明了信号量的资源获取、资源释放等操作,如下所示

typedef int semaphore; semaphore aResource;

void processA(void){

down(&aResource); useResource(); up(&aResource);

}

上面显示了一个进程资源获取和释放的过程,但是一般情况下会存在多个资源同时获取锁的情景,这样该如何处理?如下所示

typedef int semaphore; semaphore aResource; semaphore bResource;

void processA(void){

down(&aResource); down(&bResource); useAResource(); useBResource(); up(&aResource); up(&bResource);

}

对于单个进程来说,并不需要加锁,因为不存在和这个进程的竞争条件。所以单进条件下程序能够完好运行。

现在让我们考虑两个进程的情况,A 和 B ,还存在两个资源。如下所示

typedef int semaphore; semaphore aResource; semaphore bResource;

void processA(void){

down(&aResource); down(&bResource); useBothResource(); up(&bResource); up(&aResource);

}

void processB(void){

down(&aResource); down(&bResource); useBothResource(); up(&bResource); up(&aResource);

}

在上述代码中,两个进程以相同的顺序访问资源。在这段代码中,一个进程在另一个进程之前获取资源,如果另外一个进程想在第一个进程释放之前获取资源,那么它会由于资源的加锁而阻塞,直到该资源可用为止。

在下面这段代码中,有一些变化

typedef int semaphore; semaphore aResource; semaphore bResource;

void processA(void){

down(&aResource); down(&bResource); useBothResource(); up(&bResource); up(&aResource);

}

void processB(void){

down(&bResource); // 变化的代码 down(&aResource); // 变化的代码 useBothResource(); up(&aResource); // 变化的代码 up(&bResource); // 变化的代码

}

这种情况就不同了,可能会发生同时获取两个资源并有效地阻塞另一个过程,直到完成为止。也就是说,可能会发生进程 A 获取资源 A 的同时进程 B 获取资源 B 的情况。然后每个进程在尝试获取另一个资源时被阻塞。

在这里我们会发现一个简单的获取资源顺序的问题就会造成死锁,所以死锁是很容易发生的,所以下面我们就对死锁做一个详细的认识和介绍。

死锁

如果要对死锁进行一个定义的话,下面的定义比较贴切

如果一组进程中的每个进程都在等待一个事件,而这个事件只能由该组中的另一个进程触发,这种情况会导致死锁。

简单一点来表述一下,就是每个进程都在等待其他进程释放资源,而其他资源也在等待每个进程释放资源,这样没有进程抢先释放自己的资源,这种情况会产生死锁,所有进程都会无限的等待下去。

换句话说,死锁进程结合中的每个进程都在等待另一个死锁进程已经占有的资源。但是由于所有进程都不能运行,它们之中任何一个资源都无法释放资源,所以没有一个进程可以被唤醒。这种死锁也被称为资源死锁(resource deadlock)。资源死锁是最常见的类型,但不是所有的类型,我们后面会介绍其他类型,我们先来介绍资源死锁

资源死锁的条件

针对我们上面的描述,资源死锁可能出现的情况主要有

  • 互斥条件:每个资源都被分配给了一个进程或者资源是可用的
  • 保持和等待条件:已经获取资源的进程被认为能够获取新的资源
  • 不可抢占条件:分配给一个进程的资源不能强制的从其他进程抢占资源,它只能由占有它的进程显示释放
  • 循环等待:死锁发生时,系统中一定有两个或者两个以上的进程组成一个循环,循环中的每个进程都在等待下一个进程释放的资源。

发生死锁时,上面的情况必须同时会发生。如果其中任意一个条件不会成立,死锁就不会发生。可以通过破坏其中任意一个条件来破坏死锁,下面这些破坏条件就是我们探讨的重点

死锁模型

Holt 在 1972 年提出对死锁进行建模,建模的标准如下:

  • 圆形表示进程
  • 方形表示资源

从资源节点到进程节点表示资源已经被进程占用,如下图所示

在上图中表示当前资源 R 正在被 A 进程所占用

由进程节点到资源节点的有向图表示当前进程正在请求资源,并且该进程已经被阻塞,处于等待这个资源的状态

在上图中,表示的含义是进程 B 正在请求资源 S 。Holt 认为,死锁的描述应该如下

这是一个死锁的过程,进程 C 等待资源 T 的释放,资源 T 却已经被进程 D 占用,进程 D 等待请求占用资源 U ,资源 U 却已经被线程 C 占用,从而形成环。

总结一点:吃着碗里的看着锅里的容易死锁

那么如何避免死锁呢?我们还是通过死锁模型来聊一聊

假设有三个进程 (A、B、C) 和三个资源(R、S、T) 。三个进程对资源的请求和释放序列如下图所示

操作系统可以任意选择一个非阻塞的程序运行,所以它可以决定运行 A 直到 A 完成工作;它可以运行 B 直到 B 完成工作;最后运行 C。

这样的顺序不会导致死锁(因为不存在对资源的竞争),但是这种情况也完全没有并行性。进程除了在请求和释放资源外,还要做计算和输入/输出的工作。当进程按照顺序运行时,在等待一个 I/O 时,另一个进程不能使用 CPU。所以,严格按照串行的顺序执行并不是最优越的。另一方面,如果没有进程在执行任何 I/O 操作,那么最短路径优先作业会优于轮转调度,所以在这种情况下串行可能是最优越的

现在我们假设进程会执行计算和 I/O 操作,所以轮询调度是一种合理的调度算法。资源请求可能会按照下面这个顺序进行

下图是针对上面这六个步骤的资源分配图。

这里需要注意一个问题,为什么从资源出来的有向图指向了进程却表示进程请求资源呢?笔者刚开始看也有这个疑问,但是想了一下这个意思解释为进程占用资源比较合适,而进程的有向图指向资源表示进程被阻塞的意思。

在上面的第四个步骤,进程 A 正在等待资源 S;第五个步骤中,进程 B 在等待资源 T;第六个步骤中,进程 C 在等待资源 R,因此产生了环路并导致了死锁。

然而,操作系统并没有规定一定按照某种特定的顺序来执行这些进程。遇到一个可能会引起死锁的线程后,操作系统可以干脆不批准请求,并把进程挂起一直到安全状态为止。比如上图中,如果操作系统认为有死锁的可能,它可以选择不把资源 S 分配给 B ,这样 B 被挂起。这样的话操作系统会只运行 A 和 C,那么资源的请求和释放就会是下面的步骤

下图是针对上面这六个步骤的资源分配图。

在第六步执行完成后,可以发现并没有产生死锁,此时就可以把资源 S 分配给 B,因为 A 进程已经执行完毕,C 进程已经拿到了它想要的资源。进程 B 可以直接获得资源 S,也可以等待进程 C 释放资源 T 。

有四种处理死锁的策略:

  • 忽略死锁带来的影响(惊呆了)
  • 检测死锁并回复死锁,死锁发生时对其进行检测,一旦发生死锁后,采取行动解决问题
  • 通过仔细分配资源来避免死锁
  • 通过破坏死锁产生的四个条件之一来避免死锁

下面我们分别介绍一下这四种方法

鸵鸟算法

最简单的解决办法就是使用鸵鸟算法(ostrich algorithm),把头埋在沙子里,假装问题根本没有发生。每个人看待这个问题的反应都不同。数学家认为死锁是不可接受的,必须通过有效的策略来防止死锁的产生。工程师想要知道问题发生的频次,系统因为其他原因崩溃的次数和死锁带来的严重后果。如果死锁发生的频次很低,而经常会由于硬件故障、编译器错误等其他操作系统问题导致系统崩溃,那么大多数工程师不会修复死锁。

死锁检测和恢复

第二种技术是死锁的检测和恢复。这种解决方式不会尝试去阻止死锁的出现。相反,这种解决方案会希望死锁尽可能的出现,在监测到死锁出现后,对其进行恢复。下面我们就来探讨一下死锁的检测和恢复的几种方式

每种类型一个资源的死锁检测方式

每种资源类型都有一个资源是什么意思?我们经常提到的打印机就是这样的,资源只有打印机,但是设备都不会超过一个。

可以通过构造一张资源分配表来检测这种错误,比如我们上面提到的

的算法来检测从 P1 到 Pn 这 n 个进程中的死锁。假设资源类型为 m,E1 代表资源类型1,E2 表示资源类型 2 ,Ei 代表资源类型 i (1 <= i <= m)。E 表示的是 现有资源向量(existing resource vector),代表每种已存在的资源总数。

现在我们就需要构造两个数组:C 表示的是当前分配矩阵(current allocation matrix) ,R 表示的是 请求矩阵(request matrix)。Ci 表示的是 Pi 持有每一种类型资源的资源数。所以,Cij 表示 Pi 持有资源 j 的数量。Rij 表示 Pi 所需要获得的资源 j 的数量

一般来说,已分配资源 j 的数量加起来再和所有可供使用的资源数相加 = 该类资源的总数。

死锁的检测就是基于向量的比较。每个进程起初都是没有被标记过的,算法会开始对进程做标记,进程被标记后说明进程被执行了,不会进入死锁,当算法结束时,任何没有被标记过的进程都会被判定为死锁进程。

上面我们探讨了两种检测死锁的方式,那么现在你知道怎么检测后,你何时去做死锁检测呢?一般来说,有两个考量标准:

  • 每当有资源请求时就去检测,这种方式会占用昂贵的 CPU 时间。
  • 每隔 k 分钟检测一次,或者当 CPU 使用率降低到某个标准下去检测。考虑到 CPU 效率的原因,如果死锁进程达到一定数量,就没有多少进程可以运行,所以 CPU 会经常空闲。

从死锁中恢复

上面我们探讨了如何检测进程死锁,我们最终的目的肯定是想让程序能够正常的运行下去,所以针对检测出来的死锁,我们要对其进行恢复,下面我们会探讨几种死锁的恢复方式

通过抢占进行恢复

在某些情况下,可能会临时将某个资源从它的持有者转移到另一个进程。比如在不通知原进程的情况下,将某个资源从进程中强制取走给其他进程使用,使用完后又送回。这种恢复方式一般比较困难而且有些简单粗暴,并不可取。

通过回滚进行恢复

如果系统设计者和机器操作员知道有可能发生死锁,那么就可以定期检查流程。进程的检测点意味着进程的状态可以被写入到文件以便后面进行恢复。检测点不仅包含存储映像(memory image),还包含资源状态(resource state)。一种更有效的解决方式是不要覆盖原有的检测点,而是每出现一个检测点都要把它写入到文件中,这样当进程执行时,就会有一系列的检查点文件被累积起来。

为了进行恢复,要从上一个较早的检查点上开始,这样所需要资源的进程会回滚到上一个时间点,在这个时间点上,死锁进程还没有获取所需要的资源,可以在此时对其进行资源分配。

杀死进程恢复

最简单有效的解决方案是直接杀死一个死锁进程。但是杀死一个进程可能照样行不通,这时候就需要杀死别的资源进行恢复。

另外一种方式是选择一个环外的进程作为牺牲品来释放进程资源。

死锁避免

我们上面讨论的是如何检测出现死锁和如何恢复死锁,下面我们探讨几种规避死锁的方式

单个资源的银行家算法

银行家算法是 Dijkstra 在 1965 年提出的一种调度算法,它本身是一种死锁的调度算法。它的模型是基于一个城镇中的银行家,银行家向城镇中的客户承诺了一定数量的贷款额度。算法要做的就是判断请求是否会进入一种不安全的状态。如果是,就拒绝请求,如果请求后系统是安全的,就接受该请求。

比如下面的例子,银行家一共为所有城镇居民提供了 15 单位个贷款额度,一个单位表示 1k 美元,如下所示

城镇居民都喜欢做生意,所以就会涉及到贷款,每个人能贷款的最大额度不一样,在某一时刻,A/B/C/D 的贷款金额如下

上面每个人的贷款总额加起来是 13,马上接近 15,银行家只能给 A 和 C 进行放贷,可以拖着 B 和 D、所以,可以让 A 和 C 首先完成,释放贷款额度,以此来满足其他居民的贷款。这是一种安全的状态。

如果每个人的请求导致总额会超过甚至接近 15 ,就会处于一种不安全的状态,如下所示

这样,每个人还能贷款至少 2 个单位的额度,如果其中有一个人发起最大额度的贷款请求,就会使系统处于一种死锁状态。

这里注意一点:不安全状态并不一定引起死锁,由于客户不一定需要其最大的贷款额度,但是银行家不敢抱着这种侥幸心理。

银行家算法就是对每个请求进行检查,检查是否请求会引起不安全状态,如果不会引起,那么就接受该请求;如果会引起,那么就推迟该请求。

类似的,还有多个资源的银行家算法,读者可以自行了解。

破坏死锁

死锁本质上是无法避免的,因为它需要获得未知的资源和请求,但是死锁是满足四个条件后才出现的,它们分别是

  • 互斥
  • 保持和等待
  • 不可抢占
  • 循环等待

我们分别对这四个条件进行讨论,按理说破坏其中的任意一个条件就能够破坏死锁

破坏互斥条件

我们首先考虑的就是破坏互斥使用条件。如果资源不被一个进程独占,那么死锁肯定不会产生。如果两个打印机同时使用一个资源会造成混乱,打印机的解决方式是使用 假脱机打印机(spooling printer) ,这项技术可以允许多个进程同时产生输出,在这种模型中,实际请求打印机的唯一进程是打印机守护进程,也称为后台进程。后台进程不会请求其他资源。我们可以消除打印机的死锁。

后台进程通常被编写为能够输出完整的文件后才能打印,假如两个进程都占用了假脱机空间的一半,而这两个进程都没有完成全部的输出,就会导致死锁。

因此,尽量做到尽可能少的进程可以请求资源。

破坏保持等待的条件

第二种方式是如果我们能阻止持有资源的进程请求其他资源,我们就能够消除死锁。一种实现方式是让所有的进程开始执行前请求全部的资源。如果所需的资源可用,进程会完成资源的分配并运行到结束。如果有任何一个资源处于频繁分配的情况,那么没有分配到资源的进程就会等待。

很多进程无法在执行完成前就知道到底需要多少资源,如果知道的话,就可以使用银行家算法;还有一个问题是这样无法合理有效利用资源。

还有一种方式是进程在请求其他资源时,先释放所占用的资源,然后再尝试一次获取全部的资源。

破坏不可抢占条件

破坏不可抢占条件也是可以的。可以通过虚拟化的方式来避免这种情况。

破坏循环等待条件

现在就剩最后一个条件了,循环等待条件可以通过多种方法来破坏。一种方式是制定一个标准,一个进程在任何时候只能使用一种资源。如果需要另外一种资源,必须释放当前资源。对于需要将大文件从磁带复制到打印机的过程,此限制是不可接受的。

另一种方式是将所有的资源统一编号,如下图所示

进程可以在任何时间提出请求,但是所有的请求都必须按照资源的顺序提出。如果按照此分配规则的话,那么资源分配之间不会出现环。

尽管通过这种方式来消除死锁,但是编号的顺序不可能让每个进程都会接受。

其他问题

下面我们来探讨一下其他问题,包括 通信死锁、活锁是什么、饥饿问题和两阶段加锁

两阶段加锁

虽然很多情况下死锁的避免和预防都能处理,但是效果并不好。随着时间的推移,提出了很多优秀的算法用来处理死锁。例如在数据库系统中,一个经常发生的操作是请求锁住一些记录,然后更新所有锁定的记录。当同时有多个进程运行时,就会有死锁的风险。

一种解决方式是使用 两阶段提交(two-phase locking)。顾名思义分为两个阶段,一阶段是进程尝试一次锁定它需要的所有记录。如果成功后,才会开始第二阶段,第二阶段是执行更新并释放锁。第一阶段并不做真正有意义的工作。

如果在第一阶段某个进程所需要的记录已经被加锁,那么该进程会释放所有锁定的记录并重新开始第一阶段。从某种意义上来说,这种方法类似于预先请求所有必需的资源或者是在进行一些不可逆的操作之前请求所有的资源。

不过在一般的应用场景中,两阶段加锁的策略并不通用。如果一个进程缺少资源就会半途中断并重新开始的方式是不可接受的。

通信死锁

我们上面一直讨论的是资源死锁,资源死锁是一种死锁类型,但并不是唯一类型,还有通信死锁,也就是两个或多个进程在发送消息时出现的死锁。进程 A 给进程 B 发了一条消息,然后进程 A 阻塞直到进程 B 返回响应。假设请求消息丢失了,那么进程 A 在一直等着回复,进程 B 也会阻塞等待请求消息到来,这时候就产生死锁。

尽管会产生死锁,但是这并不是一个资源死锁,因为 A 并没有占据 B 的资源。事实上,通信死锁并没有完全可见的资源。根据死锁的定义来说:每个进程因为等待其他进程引起的事件而产生阻塞,这就是一种死锁。相较于最常见的通信死锁,我们把上面这种情况称为通信死锁(communication deadlock)。

通信死锁不能通过调度的方式来避免,但是可以使用通信中一个非常重要的概念来避免:超时(timeout)。在通信过程中,只要一个信息被发出后,发送者就会启动一个定时器,定时器会记录消息的超时时间,如果超时时间到了但是消息还没有返回,就会认为消息已经丢失并重新发送,通过这种方式,可以避免通信死锁。

但是并非所有网络通信发生的死锁都是通信死锁,也存在资源死锁,下面就是一个典型的资源死锁。

当一个数据包从主机进入路由器时,会被放入一个缓冲区,然后再传输到另外一个路由器,再到另一个,以此类推直到目的地。缓冲区都是资源并且数量有限。如下图所示,每个路由器都有 10 个缓冲区(实际上有很多)。

假如路由器 A 的所有数据需要发送到 B ,B 的所有数据包需要发送到 D,然后 D 的所有数据包需要发送到 A 。没有数据包可以移动,因为在另一端没有缓冲区可用,这就是一个典型的资源死锁。

活锁

你会发现一个很有意思的事情,死锁就跟榆木脑袋一样,不会转弯。我看过古代的一则故事:

如果说死锁很痴情的话,那么活锁用一则成语来表示就是 弄巧成拙。

某些情况下,当进程意识到它不能获取所需要的下一个锁时,就会尝试礼貌的释放已经获得的锁,然后等待非常短的时间再次尝试获取。可以想像一下这个场景:当两个人在狭路相逢的时候,都想给对方让路,相同的步调会导致双方都无法前进。

现在假想有一对并行的进程用到了两个资源。它们分别尝试获取另一个锁失败后,两个进程都会释放自己持有的锁,再次进行尝试,这个过程会一直进行重复。很明显,这个过程中没有进程阻塞,但是进程仍然不会向下执行,这种状况我们称之为 活锁(livelock)。

饥饿

与死锁和活锁的一个非常相似的问题是 饥饿(starvvation)。想象一下你什么时候会饿?一段时间不吃东西是不是会饿?对于进程来讲,最重要的就是资源,如果一段时间没有获得资源,那么进程会产生饥饿,这些进程会永远得不到服务。

我们假设打印机的分配方案是每次都会分配给最小文件的进程,那么要打印大文件的进程会永远得不到服务,导致进程饥饿,进程会无限制的推后,虽然它没有阻塞。

总结

死锁是一类通用问题,任何操作系统都会产生死锁。当每一组进程中的每个进程都因等待由该组的其他进程所占有的资源而导致阻塞,死锁就发生了。这种情况会使所有的进程都处于无限等待的状态。

死锁的检测和避免可以通过安全和不安全状态来判断,其中一个检测方式就是银行家算法;当然你也可以使用鸵鸟算法对死锁置之不理,但是你肯定会遭其反噬。

也可以在设计时通过系统结构的角度来避免死锁,这样能够预防死锁;也可以破坏死锁的四个条件来破坏死锁。资源死锁并不是唯一性的死锁,还有通信间死锁,可以设置适当的超时时间来完成。

活锁和死锁的问题有些相似,它们都是一种进程无法继续向下执行的状态。由于进程调度策略导致尝试获取进程的一方永远无法获得资源后,进程会导致饥饿的出现。


# 操作系统网站推荐-Java面试题


  • 操作系统网站推荐
    • studytonight
    • udacity
    • tutorialspoint
    • classcentral
    • nptel
    • codescracker
    • sciencedirect
    • homepage
    • computer.howstuffworks.com
    • tldp.org
    • bilibili

一般很少有人推荐操作系统的网站吧。。。。。。这几个网站来源于我平常的学习总结,也有一些是来源于网上优秀的回答,希望这几个网站能够助力你对操作系统有更深的认识。

studytonight

studytonight 简直太棒了!!! studytonight 会包括 operationg system,但是并不是说 studytonight 就是一个单纯的 OS 学习网站,它是一个基础教程网,它的首页是这样的,不仅限于 os ,还会包括 Java、C、CSS、OS、Computer network 等。

这个页面一看就爱上了,和国内很多页面做的硬风格不同,这样的教程才不那么生硬和让人讨厌。在所有的素材库里面,有各种各样的教程

我们推荐的是操作系统,所以索性就点进去操作系统主页好了,点进去的页面就是这样的

studytonight 是一个对初学者来说学习操作系统的一个很友好的网站。

课程艺术主要分为三部分,由易到难分别是基础篇、中等篇、提高篇,我们点进去基础篇的第一篇


这一篇主要是对操作系统的介绍,操作系统的功能、操作系统任务等

通过简单的描述和图片来说明,让人感觉清晰易懂,很有目的性的学习,而且这些主题也可以作为面试题来考

你还真说对了,studytonight 还真的就有面试题

我们在 Test 页签下面会发现有很多面试题

我们选择 Operating System 进去之后会发现有很多的测试

是一个一体化的学习平台,适用于新手,我大致翻看了一下,里面的东西比较基础,受众广,是大家学习必备的一个网站。

udacity

udacity 是一个视频学习网站,界面看起来很清爽。

下面还有关于课程的介绍,同时你可以开始免费的课程,这个界面就是中文版了,让人眼前一亮

我的课程 就是你学过的所有课程,你可以在所有课程中搜索指定的课程,种类非常多

我们还是以操作系统为例,来看一下具体的课程

左面是具体的课程列表,右面是讲师授课部分,视频非常清晰,而且讲师吐字很清楚,可以锻炼英语。有一些小伙伴们说英文看不懂,对英文网而退却,这不是一个好习惯。英文在编程世界中的重要性不言而喻。所以英文是你一定要学好的一门语言。

讲师讲完课程之后还有具体的练习环节,作为学习巩固,非常不错。

udacity 的课程要比 studytonight 更深一些,操作系统这门课就分成了 22 堂课,每一堂课还有很多小节,真是一个非常好的网站。我现在在写文章,我巴不得不写字了,马上学习!。 还是忍住了,那就写完了马上去看!!!

tutorialspoint

tutorialspoint 这个网站也是一个资源教程网,这个网站的搜索指数非常高,基本上搜任何问题都有 tutorialspoint 的解释。

我们点进去 Library 之后发现,这就是教程宝库啊,罗列的非常清楚

tutorialspoint 也有视频教程,不过大多都收费。

教程非常多,不仅限于编程,也包含 Excel 、SAP 等教程。

tutorialspoint 还有电子书教程,不过大部分也是收费的,应该是 tutorialspoint 自己制作的 PDF,不知道写的怎么样,如果有兴趣的小伙伴可以付费下载,到时候记得给我发一份哦,嘿嘿。

说了这么多,我们还没有看 tutorialspoint 操作系统的主页呢

左边是导航栏,右边是具体的教程,这个样式感觉有些老,不过分类倒是很明确的,里面的内容要比 studytonight 差一些,也是一个入门非常好的教程网

classcentral

classcentral 这个网站有点强啊,里面汇总了各大名校的教程

我们发现了一个中文教程,一看是 南京大学 的教程,南京大学也是非常牛逼的一所学校,小编也有南大的基友在搞影视,其实我是一个被编程耽误的导演,狗头保命。

不扯皮了,来看看南大的课程

这么多牛逼的课程发现了中文大学,也侧面说明了我国大学在国际上的地位。Overview 是 OS 的总体介绍。

我们注册后点击开始课程,就进入讲课页面

课程分为 6 周,以视频的形式展开,我们开始 计算机系统概览。

终于能看到国人讲的操作系统了!!!

右边还可以随时做笔记进行查看,课程还支持 下载 功能,非常不错!

好了我们该退出来了,一会儿又忍不住听课了!

nptel

仿佛见到了一个不得了的网站,这个网站的名字就隐隐的感觉有点牛逼。我们先看牛逼在哪,然后再看为什么牛逼

你能感觉出来牛逼了吗?

这每一个目录都 TM 是一本书啊,虽然页数不多,但这确确实实是宝贝啊。

从操作系统概述到文件系统、进程管理、文件管理、I/O 管理等等等等。这还不是最牛逼的,最牛逼的是右边能免费 下载,你说气人不气人

每一章节都支持下载 PDF 版本。

我们带着好奇心,点开了 About Us。

然后就看到了下面这段话

也就是说,这是一个 印度的国家增强型学习计划,也就是说,印度的各大名校联合推出的免费学习计划,这也能理解为什么印度人在电气、编程、通信方面能够越来越牛逼了。

这个网站你一定要看,我认为可以和黑皮书系列平起平坐了。

codescracker

codescracker 又是一个牛逼的网站,看完这个网站,我才觉得低调才是最牛逼的炫耀。。。

分类都非常全,解释的也很到位,但是 codescracker 并不只是一个 os 网站,它是一门编程语言的学习一站式平台,只不过网站比较简洁

可以看到有各种各样的教程,下面还是有测试类的,基本上企业用的语言都涵盖到了。

你必须要珍藏的一个网站。

sciencedirect

sciencedirect 这个网站真是太牛逼了。百度对 sciencedirect 的解释

不过 sciencedirect 大部分的 PDF 下载是需要收费的,感兴趣的小伙伴们可以购买

homepage

Homepage 是做什么的,一看主页就明白了

这是一个计算机科学和统计学的网站,然后我们点击 search 搜索 os 直接跳转到 os 的网站

然后就呵呵呵呵了,这个网站比较奇葩,它没有后退和前进的按钮,那怎么访问?

我是直接通过 url 访问的,主页就是 https://homepage.cs.uri.edu/faculty/wolfe/book/Readings/Reading01.htm

但是网站太硬核了,很好的一个学习资料哦。

computer.howstuffworks.com

computer.howstuffworks.com 就是一个计算机网站,它涵盖软件和硬件的方方面面。

Computer 知识包括 计算机硬件、网络、计算机软件、计算机秘密等等

都是一些讲的非常好的文章,可以说是计算机的百科全书了。

老样子我们点开 COMPUTER OPERATING SYSTEMS 专题,可以看到都是一些非常好的文章

加入收藏夹!

tldp.org

这个网站又是一个学习操作系统非常牛逼的网站,我都不知道今晚上说了多少次了,哈哈哈哈。

来看看主页是啥样的

这一看就是一个牛逼的网站,不要问我为什么,OS 的页面是这样的

没有任何点缀和装饰,成年人的生活就这么朴实无华。


# 操作系统之文件系统-Java面试题


  • 操作系统之文件系统
    • 文件
      • 文件命名
      • 文件结构
      • 文件类型
      • 文件访问
      • 文件属性
      • 文件操作
    • 目录
      • 一级目录系统
      • 层次目录系统
      • 路径名
      • 目录操作
    • 文件系统的实现
      • 文件系统布局
        • 引导块
        • 超级块
        • 空闲空间块
        • 碎片
        • inode
      • 文件的实现
        • 连续分配
        • 链表分配
        • 使用内存表进行链表分配
        • inode
      • 目录的实现
      • 共享文件
      • 日志结构文件系统
      • 日志文件系统
      • 虚拟文件系统
    • 文件系统的管理和优化
      • 磁盘空间管理
        • 块大小
        • 记录空闲块
      • 磁盘配额
      • 文件系统备份
        • 物理转储和逻辑转储
      • 文件系统的一致性
      • 文件系统性能
        • 高速缓存
        • 块提前读
        • 减少磁盘臂运动
        • 磁盘碎片整理

所有的应用程序都需要存储和检索信息。进程运行时,它能够在自己的存储空间内存储一定量的信息。然而,存储容量受虚拟地址空间大小的限制。对于一些应用程序来说,存储空间的大小是充足的,但是对于其他一些应用程序,比如航空订票系统、银行系统、企业记账系统来说,这些容量又显得太小了。

第二个问题是,当进程终止时信息会丢失。对于一些应用程序(例如数据库),信息会长久保留。在这些进程终止时,相关的信息应该保留下来,是不能丢失的。甚至这些应用程序崩溃后,信息也应该保留下来。

第三个问题是,通常需要很多进程在同一时刻访问这些信息。解决这种问题的方式是把这些信息单独保留在各自的进程中。

因此,对于长久存储的信息我们有三个基本需求:

  • 必须要有可能存储的大量的信息

    • 信息必须能够在进程终止时保留
  • 必须能够使多个进程同时访问有关信息

磁盘(Magnetic disk) 一直是用来长久保存信息的设备。近些年来,固态硬盘逐渐流行起来。

固态硬盘不仅没有易损坏的移动部件,而且能够提供快速的随机访问。相比而言,虽然磁带和光盘也被广泛使用,但是它们的性能相对较差,通常应用于备份。我们会在后面探讨磁盘,现在姑且把磁盘当作一种大小固定块的线性序列好了,并且支持如下操作

  • 读块 k
  • 写块 k

事实上磁盘支持更多的操作,但是只要有了读写操作,原则上就能够解决长期存储的问题。

然而,磁盘还有一些不便于实现的操作,特别是在有很多程序或者多用户使用的大型系统上(如服务器)。在这种情况下,很容易产生一些问题,例如

  • 你如何找到这些信息?

  • 你如何保证一个用户不会读取另外一个用户的数据?

  • 你怎么知道哪些块是空闲的?等等问题

我们可以针对这些问题提出一个新的抽象 – 文件。进程和线程的抽象、地址空间和文件都是操作系统的重要概念。如果你能真正深入了解这三个概念,那么你就走上了成为操作系统专家的道路。

文件(Files)是由进程创建的逻辑信息单元。一个磁盘会包含几千甚至几百万个文件,每个文件是独立于其他文件的。事实上,如果你能把每个文件都看作一个独立的地址空间,那么你就可以真正理解文件的概念了。

进程能够读取已经存在的文件,并在需要时重新创建他们。存储在文件中的信息必须是持久的,这也就是说,不会因为进程的创建和终止而受影响。一个文件只能在当用户明确删除的时候才能消失。尽管读取和写入都是最基本的操作,但还有许多其他操作,我们将在下面介绍其中的一些。

文件由操作系统进行管理,有关文件的构造、命名、访问、使用、保护、实现和管理方式都是操作系统设计的主要内容。从总体上看,操作系统中处理文件的部分称为 文件系统(file system),这就是我们所讨论的。

从用户角度来说,用户通常会关心文件是由什么组成的,如何给文件进行命名,如何保护文件,以及可以对文件进行哪些操作等等。尽管是用链表还是用位图记录内存空闲区并不是用户所关心的主题,而这些对系统设计人员来说至关重要。下面我们就来探讨一下这些主题

文件

文件命名

文件是一种抽象机制,它提供了一种方式用来存储信息以及在后面进行读取。可能任何一种机制最重要的特性就是管理对象的命名方式。在创建一个文件后,它会给文件一个命名。当进程终止时,文件会继续存在,并且其他进程可以使用名称访问该文件。

文件命名规则对于不同的操作系统来说是不一样的,但是所有现代操作系统都允许使用 1 – 8 个字母的字符串作为合法文件名。

某些文件区分大小写字母,而大多数则不区分。UNIX 属于第一类;历史悠久的 MS-DOS 属于第二类(顺便说一句,尽管 MS-DOS 历史悠久,但 MS-DOS 仍在嵌入式系统中非常广泛地使用,因此它绝不是过时的);因此,UNIX 系统会有三种不同的命名文件:maria、Maria、MARIA 。在 MS-DOS ,所有这些命名都属于相同的文件。

这里可能需要在文件系统上预留一个位置。Windows 95 和 Windows 98 都使用了 MS-DOS 文件系统,叫做 FAT-16,因此继承了它的一些特征,例如有关文件名的构造方法。Windows 98 引入了对 FAT-16 的一些扩展,从而导致了 FAT-32 的生成,但是这两者很相似。另外,Windows NT,Windows 2000,Windows XP,Windows Vista,Windows 7 和 Windows 8 都支持 FAT 文件系统,这种文件系统有些过时。然而,这些较新的操作系统还具有更高级的本机文件系统(NTFS),有不同的特性,那就是基于 Unicode 编码的文件名。事实上,Windows 8 还配备了另一种文件系统,简称 ReFS(Resilient File System),但这个文件系统一般应用于 Windows 8 的服务器版本。下面除非我们特殊声明,否则我们在提到 MS-DOS 和 FAT 文件系统的时候,所指的就是 Windows 的 FAT-16 和 FAT-32。这里要说一下,有一种类似 FAT 的新型文件系统,叫做 exFAT。它是微软公司对闪存和大文件系统开发的一种优化的 FAT 32 扩展版本。ExFAT 是现在微软唯一能够满足 OS X读写操作的文件系统。

许多操作系统支持两部分的文件名,它们之间用 . 分隔开,比如文件名 prog.c。原点后面的文件称为 文件扩展名(file extension) ,文件扩展名通常表示文件的一些信息。例如在 MS-DOS 中,文件名是 1 – 8 个字符,加上 1 – 3 个字符的可选扩展名组成。在 UNIX 中,如果有扩展名,那么扩展名的长度将由用户来决定,一个文件甚至可以包括两个或更多的扩展名,例如 homepage.html.zip,html 表示一个 web 网页而 .zip 表示文件homepage.html 已经采用 zip 程序压缩完成。一些常用的文件扩展名以及含义如下图所示

扩展名 含义
bak 备份文件
c c 源程序文件
gif 符合图形交换格式的图像文件
hlp 帮助文件
html WWW 超文本标记语言文档
jpg 符合 JPEG 编码标准的静态图片
mp3 符合 MP3 音频编码格式的音乐文件
mpg 符合 MPEG 编码标准的电影
o 目标文件(编译器输出格式,尚未链接)
pdf pdf 格式的文件
ps PostScript 文件
tex 为 TEX 格式化程序准备的输入文件
txt 文本文件
zip 压缩文件

在 UNIX 系统中,文件扩展名只是一种约定,操作系统并不强制采用。

名为 file.txt 的文件是文本文件,这个文件名更多的是提醒所有者,而不是给计算机传递信息。但是另一方面,C 编译器可能要求它编译的文件以.c 结尾,否则它会拒绝编译。然而,操作系统并不关心这一点。

对于可以处理多种类型的程序,约定就显得及其有用。例如 C 编译器可以编译、链接多种文件,包括 C 文件和汇编语言文件。这时扩展名就很有必要,编译器利用它们区分哪些是 C 文件,哪些是汇编文件,哪些是其他文件。因此,扩展名对于编译器判断哪些是 C 文件,哪些是汇编文件以及哪些是其他文件变得至关重要。

与 UNIX 相反,Windows 就会关注扩展名并对扩展名赋予了新的含义。用户(或进程) 可以在操作系统中注册扩展名,并且规定哪个程序能够拥有扩展名。当用户双击某个文件名时,拥有该文件名的程序就启动并运行文件。例如,双击 file.docx 启动了 Word 程序,并以 file.docx 作为初始文件。

文件结构

文件的构造有多种方式。下图列出了常用的三种构造方式

上图中的 a 是一种无结构的字节序列,操作系统不关心序列的内容是什么,操作系统能看到的就是字节(bytes)。其文件内容的任何含义只在用户程序中进行解释。UNIX 和 Windows 都采用这种办法。

把文件看成字节序列提供了最大的灵活性。用户程序可以向文件中写任何内容,并且可以通过任何方便的形式命名。操作系统不会为为用户写入内容提供帮助,当然也不会干扰阻塞你。对于想做特殊操作的用户来说,后者是十分重要的。所有的 UNIX 版本(包括 Linux 和 OS X)和 Windows 都使用这种文件模型。

图 b 表示在文件结构上的第一部改进。在这个模型中,文件是具有固定长度记录的序列,每个记录都有其内部结构。 把文件作为记录序列的核心思想是:读操作返回一个记录,而写操作重写或者追加一个记录。第三种文件结构如上图 c 所示。在这种组织结构中,文件由一颗记录树构成,记录树的长度不一定相同,每个记录树都在记录中的固定位置包含一个key 字段。这棵树按 key 进行排序,从而可以对特定的 key 进行快速查找。

在记录树的结构中,可以取出下一个记录,但是最关键的还是根据 key 搜索指定的记录。如上图 c 所示,用户可以读出指定的 pony 记录,而不必关心记录在文件中的确切位置。用户也可以在文件中添加新的记录。但是用户不能决定添加到何处位置,添加到何处位置是由操作系统决定的。

文件类型

很多操作系统支持多种文件类型。例如,UNIX(同样包括 OS X)和 Windows 都具有常规的文件和目录。除此之外,UNIX 还具有字符特殊文件(character special file) 和 块特殊文件(block special file)。常规文件(Regular files) 是包含有用户信息的文件。用户一般使用的文件大都是常规文件,常规文件一般包括 可执行文件、文本文件、图像文件,从常规文件读取数据或将数据写入时,内核会根据文件系统的规则执行操作,是写入可能被延迟,记录日志或者接受其他操作。

字符特殊文件和输入/输出有关,用于串行 I/O 类设备,如终端、打印机、网络等。块特殊文件用于磁盘类设备。我们主要讨论的是常规文件。

常规文件一般分为 ASCII 码文件或者二进制文件。ASCII 码文件由文本组成。在一些系统中,每行都会用回车符结束(ASCII码是13,控制字符 CR,转义字符\r。),另外一些则会使用换行符(ASCII码是10,控制字符LF,转义字符\n)。一些系统(比如 Windows)两者都会使用。

ASCII 文件的优点在于显示 和 打印,还可以用任何文本编辑器进行编辑。进一步来说,如果许多应用程序使用 ASCII 码作为输入和输出,那么很容易就能够把多个程序连接起来,一个程序的输出可能是另一个程序的输入,就像管道一样。

其他与 ASCII 不同的是二进制文件。打印出来的二进制文件是无法理解的。下面是一个二进制文件的格式,它取自早期的 UNIX 。尽管从技术上来看这个文件只是字节序列,但是操作系统只有在文件格式正确的情况下才会执行。

这个文件有五个段:文件头、征文、数据、重定位位和符号表。文件头以 魔数(magic number) 为开始,表明这个文件是一个可执行文件(以防止意外执行非此格式的文件)。然后是文件各个部分的大小,开始执行的标志以及一些标志位。程序本身的正文和数据在文件头后面,他们被加载到内存中或者重定位会根据重定位位进行判断。符号表则用于调试。

二进制文件的另外一种形式是存档文件,它由已编译但没有链接的库过程(模块)组合而成。每个文件都以模块头开始,其中记录了名称、创建日期、所有者、保护码和文件大小。和可执行文件一样,模块头也都是二进制数,将它们复制到打印机将会产生乱码。

所有的操作系统必须至少能够识别一种文件类型:它自己的可执行文件。以前的 TOPS-20 系统(用于DECsystem 20)甚至要检查要执行的任何文件的创建时间,为了定位资源文件来检查自动文件创建后是否被修改过。如果被修改过了,那么就会自动编译文件。在 UNIX 中,就是在 shell 中嵌入 make 程序。此时操作系统要求用户必须采用固定的文件扩展名,从而确定哪个源程序生成哪个二进制文件。

什么是 make 程序?在软件发展过程中,make 程序是一个自动编译的工具,它通过读取称为 Makefiles 的文件来自动从源代码构建可执行程序和库,该文件指定了如何导出目标程序。尽管集成开发环境和特定于语言的编译器功能也可以用于管理构建过程,但 Make 仍被广泛使用,尤其是在 Unix 和类似 Unix 的操作系统中使用。

当程序从文件中读写数据时,请求会转到内核处理程序(kernel driver)。如果文件是常规文件,则数据由文件系统驱动程序处理,并且通常存储在磁盘或其他存储介质上的某块区域中,从文件中读取的数据就是之前在该位置写入的数据。

当数据读取或写入到设备文件时,请求会被设备驱动程序处理。每个设备文件都有一个关联的编号,该编号标示要使用的设备驱动程序。设备处理数据的工作是它自己的事儿。

  • 块设备 也叫做块特殊文件,它的行为通常与普通文件相似:它们是字节数组,并且在给定位置读取的值是最后写入该位置的值。来自块设备的数据可以缓存在内存中,并从缓存中读取;写入可以被缓冲。块设备通常是可搜索的,块设备的概念是,相应的硬件可以一次读取或者写入整个块,例如磁盘上的一个扇区
  • 字符设备 也称为字符特殊文件,它的行为类似于管道、串行端口。将字节写入字符设备可能会导致它在屏幕上显示,在串行端口上输出,转换为声音。

目录(Directories) 是管理文件系统结构的系统文件。它是用于在计算机上存储文件的位置。目录位于分层文件系统中,例如 Linux,MS-DOS 和 UNIX。

它显示所有本地和子目录(例如,cdn 目录中的 big 目录)。当前目录是 C 盘驱动器的根目录。之所以称为根目录,是因为该目录下没有任何内容,而其他目录都在该目录下分支。

文件访问

早期的操作系统只有一种访问方式:序列访问(sequential access)。在这些系统中,进程可以按照顺序读取所有的字节或文件中的记录,但是不能跳过并乱序执行它们。顺序访问文件是可以返回到起点的,需要时可以多次读取该文件。当存储介质是磁带而不是磁盘时,顺序访问文件很方便。

在使用磁盘来存储文件时,可以不按照顺序读取文件中的字节或者记录,或者按照关键字而不是位置来访问记录。这种能够以任意次序进行读取的称为随机访问文件(random access file)。许多应用程序都需要这种方式。

随机访问文件对许多应用程序来说都必不可少,例如,数据库系统。如果乘客打电话预定某航班机票,订票程序必须能够直接访问航班记录,而不必先读取其他航班的成千上万条记录。

有两种方法可以指示从何处开始读取文件。第一种方法是直接使用 read 从头开始读取。另一种是用一个特殊的 seek 操作设置当前位置,在 seek 操作后,从这个当前位置顺序地开始读文件。UNIX 和 Windows 使用的是后面一种方式。

文件属性

文件包括文件名和数据。除此之外,所有的操作系统还会保存其他与文件相关的信息,如文件创建的日期和时间、文件大小。我们可以称这些为文件的属性(attributes)。有些人也喜欢把它们称作 元数据(metadata)。文件的属性在不同的系统中差别很大。文件的属性只有两种状态:设置(set) 和 清除(clear)。下面是一些常用的属性

属性 含义
保护 谁可以访问文件、以什么方式存取文件
密码(口令) 访问文件所需要的密码(口令)
创建者 创建文件者的 ID
所有者 当前所有者
只读标志 0 表示读/写,1 表示只读
隐藏标志 0 表示正常,1 表示不再列表中显示
系统标志 0 表示普通文件,1 表示系统文件
存档标志 0 表示已经备份,1 表示需要备份
ASCII / 二进制标志 0 表示 ASCII 文件,1 表示二进制文件
随机访问标志 0 表示只允许顺序访问,1 表示随机访问
临时标志 0 表示正常,1 表示进程退出时删除该文件
加锁标志 0 表示未加锁,1 表示加锁
记录长度 一个记录中的字节数
键的位置 每个记录中的键的偏移量
键的长度 键字段的字节数
创建时间 创建文件的日期和时间
最后一次存取时间 上一次访问文件的日期和时间
最后一次修改时间 上一次修改文件的日期和时间
当前大小 文件的字节数
最大长度 文件可能增长到的字节数

没有一个系统能够同时具有上面所有的属性,但每个属性都在某个系统中采用。

前面四个属性(保护,口令,创建者,所有者)与文件保护有关,它们指出了谁可以访问这个文件,谁不能访问这个文件。

保护(File Protection): 用于保护计算机上有价值数据的方法。文件保护是通过密码保护文件或者仅仅向特定用户或组提供权限来实现。

在一些系统中,用户必须给出口令才能访问文件。标志(flags)是一些位或者短属性能够控制或者允许特定属性。

  • 隐藏文件位(hidden flag)表示该文件不在文件列表中出现。
  • 存档标志位(archive flag)用于记录文件是否备份过,由备份程序清除该标志位;若文件被修改,操作系统则设置该标志位。用这种方法,备份程序可以知道哪些文件需要备份。
  • 临时标志位(temporary flag) 允许文件被标记为是否允许自动删除当进程终止时。

记录长度(record-length)、键的位置(key-position)和键的长度(key-length)等字段只能出现在用关键字查找记录的文件中。它们提供了查找关键字所需要的信息。

不同的时间字段记录了文件的创建时间、最近一次访问时间以及最后一次修改时间,它们的作用不同。例如,目标文件生成后被修改的源文件需要重新编译生成目标文件。这些字段提供了必要的信息。

当前大小字段指出了当前的文件大小,一些旧的大型机操作系统要求在创建文件时指定文件呢最大值,以便让操作系统提前保留最大存储值。但是一些服务器和个人计算机却不用设置此功能。

文件操作

使用文件的目的是用来存储信息并方便以后的检索。对于存储和检索,不同的系统提供了不同的操作。以下是与文件有关的最常用的一些系统调用:

  1. Create,创建不包含任何数据的文件。调用的目的是表示文件即将建立,并对文件设置一些属性。
  2. Delete,当文件不再需要,必须删除它以释放内存空间。为此总会有一个系统调用来删除文件。
  3. Open,在使用文件之前,必须先打开文件。这个调用的目的是允许系统将属性和磁盘地址列表保存到主存中,用来以后的快速访问。
  4. Close,当所有进程完成时,属性和磁盘地址不再需要,因此应关闭文件以释放表空间。很多系统限制进程打开文件的个数,以此达到鼓励用户关闭不再使用的文件。磁盘以块为单位写入,关闭文件时会强制写入最后一块,即使这个块空间内部还不满。
  5. Read,数据从文件中读取。通常情况下,读取的数据来自文件的当前位置。调用者必须指定需要读取多少数据,并且提供存放这些数据的缓冲区。
  6. Write,向文件写数据,写操作一般也是从文件的当前位置开始进行。如果当前位置是文件的末尾,则会直接追加进行写入。如果当前位置在文件中,则现有数据被覆盖,并且永远消失。
  7. append,使用 append 只能向文件末尾添加数据。
  8. seek,对于随机访问的文件,要指定从何处开始获取数据。通常的方法是用 seek 系统调用把当前位置指针指向文件中的特定位置。seek 调用结束后,就可以从指定位置开始读写数据了。
  9. get attributes,进程运行时通常需要读取文件属性。
  10. set attributes,用户可以自己设置一些文件属性,甚至是在文件创建之后,实现该功能的是 set attributes 系统调用。
  11. rename,用户可以自己更改已有文件的名字,rename 系统调用用于这一目的。

目录

文件系统通常提供目录(directories) 或者 文件夹(folders) 用于记录文件的位置,在很多系统中目录本身也是文件,下面我们会讨论关于文件,他们的组织形式、属性和可以对文件进行的操作。

一级目录系统

目录系统最简单的形式是有一个能够包含所有文件的目录。这种目录被称为根目录(root directory),由于根目录的唯一性,所以其名称并不重要。在最早期的个人计算机中,这种系统很常见,部分原因是因为只有一个用户。下面是一个单层目录系统的例子

该目录中有四个文件。这种设计的优点在于简单,并且能够快速定位文件,毕竟只有一个地方可以检索。这种目录组织形式现在一般用于简单的嵌入式设备(如数码相机和某些便携式音乐播放器)上使用。

层次目录系统

对于简单的应用而言,一般都用单层目录方式,但是这种组织形式并不适合于现代计算机,因为现代计算机含有成千上万个文件和文件夹。如果都放在根目录下,查找起来会非常困难。为了解决这一问题,出现了层次目录系统(Hierarchical Directory Systems),也称为目录树。通过这种方式,可以用很多目录把文件进行分组。进而,如果多个用户共享同一个文件服务器,比如公司的网络系统,每个用户可以为自己的目录树拥有自己的私人根目录。这种方式的组织结构如下

根目录含有目录 A、B 和 C ,分别属于不同的用户,其中两个用户个字创建了子目录。用户可以创建任意数量的子目录,现代文件系统都是按照这种方式组织的。

路径名

当目录树组织文件系统时,需要有某种方法指明文件名。常用的方法有两种,第一种方式是每个文件都会用一个绝对路径名(absolute path name),它由根目录到文件的路径组成。举个例子,/usr/ast/mailbox 意味着根目录包含一个子目录usr,usr 下面包含了一个 mailbox。绝对路径名总是以 / 开头,并且是唯一的。在UNIX中,路径的组件由/分隔。在Windows中,分隔符为\。 在 MULTICS 中,它是>。 因此,在这三个系统中,相同的路径名将被编写如下

Windows \usr\ast\mailbox UNIX /usr/ast/mailbox MULTICS >usr>ast>mailbox

不论使用哪种方式,如果路径名的第一个字符是分隔符,那就是绝对路径。

另外一种指定文件名的方法是 相对路径名(relative path name)。它常常和 工作目录(working directory) (也称作 当前目录(current directory))一起使用。用户可以指定一个目录作为当前工作目录。例如,如果当前目录是 /usr/ast,那么绝对路径 /usr/ast/mailbox可以直接使用 mailbox 来引用。也就是说,如果工作目录是 /usr/ast,则 UNIX 命令

cp /usr/ast/mailbox /usr/ast/mailbox.bak

和命令

cp mailbox mailbox.bak

具有相同的含义。相对路径通常情况下更加方便和简洁。而它实现的功能和绝对路径安全相同。

一些程序需要访问某个特定的文件而不必关心当前的工作目录是什么。在这种情况下,应该使用绝对路径名。

支持层次目录结构的大多数操作系统在每个目录中有两个特殊的目录项. 和 ..,长读作 dot 和 dotdot。dot 指的是当前目录,dotdot 指的是其父目录(在根目录中例外,在根目录中指向自己)。可以参考下面的进程树来查看如何使用。

一个进程的工作目录是 /usr/ast,它可采用 .. 沿树向上,例如,可用命令

cp ../lib/dictionary .

把文件 usr/lib/dictionary 复制到自己的目录下,第一个路径告诉系统向上找(到 usr 目录),然后向下到 lib 目录,找到 dictionary 文件

第二个参数 . 指定当前的工作目录,当 cp 命令用目录名作为最后一个参数时,则把全部的文件复制到该目录中。当然,对于上述复制,键入

cp /usr/lib/dictionary .

是更常用的方法。用户这里采用 . 可以避免键入两次 dictionary 。无论如何,键入

cp /usr/lib/dictionary dictionary

也可正常工作,就像键入

cp /usr/lib/dictionary /usr/lib/dictionary

一样。所有这些命令都能够完成同样的工作。

目录操作

不同文件中管理目录的系统调用的差别比管理文件的系统调用差别大。为了了解这些系统调用有哪些以及它们怎样工作,下面给出一个例子(取自 UNIX)。

  1. Create,创建目录,除了目录项 . 和 .. 外,目录内容为空。
  2. Delete,删除目录,只有空目录可以删除。只包含 . 和 .. 的目录被认为是空目录,这两个目录项通常不能删除
  3. opendir,目录内容可被读取。例如,未列出目录中的全部文件,程序必须先打开该目录,然后读其中全部文件的文件名。与打开和读文件相同,在读目录前,必须先打开文件。
  4. closedir,读目录结束后,应该关闭目录用于释放内部表空间。
  5. readdir,系统调用 readdir 返回打开目录的下一个目录项。以前也采用 read 系统调用来读取目录,但是这种方法有一个缺点:程序员必须了解和处理目录的内部结构。相反,不论采用哪一种目录结构,readdir 总是以标准格式返回一个目录项。
  6. rename,在很多方面目录和文件都相似。文件可以更换名称,目录也可以。
  7. link,链接技术允许在多个目录中出现同一个文件。这个系统调用指定一个存在的文件和一个路径名,并建立从该文件到路径所指名字的链接。这样,可以在多个目录中出现同一个文件。有时也被称为硬链接(hard link)。
  8. unlink,删除目录项。如果被解除链接的文件只出现在一个目录中,则将它从文件中删除。如果它出现在多个目录中,则只删除指定路径名的链接,依然保留其他路径名的链接。在 UNIX 中,用于删除文件的系统调用就是 unlink。

文件系统的实现

在对文件有了基本认识之后,现在是时候把目光转移到文件系统的实现上了。之前用户关心的一直都是文件是怎样命名的、可以进行哪些操作、目录树是什么,如何找到正确的文件路径等问题。而设计人员关心的是文件和目录是怎样存储的、磁盘空间是如何管理的、如何使文件系统得以流畅运行的问题,下面我们就来一起讨论一下这些问题。

文件系统布局

文件系统存储在磁盘中。大部分的磁盘能够划分出一到多个分区,叫做磁盘分区(disk partitioning) 或者是磁盘分片(disk slicing)。每个分区都有独立的文件系统,每块分区的文件系统可以不同。磁盘的 0 号分区称为 主引导记录(Master Boot Record, MBR),用来引导(boot) 计算机。在 MBR 的结尾是分区表(partition table)。每个分区表给出每个分区由开始到结束的地址。系统管理员使用一个称为分区编辑器的程序来创建,调整大小,删除和操作分区。这种方式的一个缺点是很难适当调整分区的大小,导致一个分区具有很多可用空间,而另一个分区几乎完全被分配。

MBR 可以用在 DOS 、Microsoft Windows 和 Linux 操作系统中。从 2010 年代中期开始,大多数新计算机都改用 GUID 分区表(GPT)分区方案。

下面是一个用 GParted 进行分区的磁盘,表中的分区都被认为是 活动的(active)。

当计算机开始引 boot 时,BIOS 读入并执行 MBR。

引导块

MBR 做的第一件事就是确定活动分区,读入它的第一个块,称为引导块(boot block) 并执行。引导块中的程序将加载分区中的操作系统。为了一致性,每个分区都会从引导块开始,即使引导块不包含操作系统。引导块占据文件系统的前 4096 个字节,从磁盘上的字节偏移量 0 开始。引导块可用于启动操作系统。

在计算机中,引导就是启动计算机的过程,它可以通过硬件(例如按下电源按钮)或者软件命令的方式来启动。开机后,电脑的 CPU 还不能执行指令,因为此时没有软件在主存中,所以一些软件必须先被加载到内存中,然后才能让 CPU 开始执行。也就是计算机开机后,首先会进行软件的装载过程。

重启电脑的过程称为重新引导(rebooting),从休眠或睡眠状态返回计算机的过程不涉及启动。

除了从引导块开始之外,磁盘分区的布局是随着文件系统的不同而变化的。通常文件系统会包含一些属性,如下

超级块

紧跟在引导块后面的是 超级块(Superblock),超级块 的大小为 4096 字节,从磁盘上的字节偏移 4096 开始。超级块包含文件系统的所有关键参数

  • 文件系统的大小
  • 文件系统中的数据块数
  • 指示文件系统状态的标志
  • 分配组大小

在计算机启动或者文件系统首次使用时,超级块会被读入内存。

空闲空间块

接着是文件系统中空闲块的信息,例如,可以用位图或者指针列表的形式给出。

BitMap 位图或者 Bit vector 位向量

位图或位向量是一系列位或位的集合,其中每个位对应一个磁盘块,该位可以采用两个值:0和1,0表示已分配该块,而1表示一个空闲块。下图中的磁盘上给定的磁盘块实例(分配了绿色块)可以用16位的位图表示为:0000111000000110。

使用链表进行管理

在这种方法中,空闲磁盘块链接在一起,即一个空闲块包含指向下一个空闲块的指针。第一个磁盘块的块号存储在磁盘上的单独位置,也缓存在内存中。

碎片

这里不得不提一个叫做碎片(fragment)的概念,也称为片段。一般零散的单个数据通常称为片段。 磁盘块可以进一步分为固定大小的分配单元,片段只是在驱动器上彼此不相邻的文件片段。如果你不理解这个概念就给你举个例子。比如你用 Windows 电脑创建了一个文件,你会发现这个文件可以存储在任何地方,比如存在桌面上,存在磁盘中的文件夹中或者其他地方。你可以打开文件,编辑文件,删除文件等等。你可能以为这些都在一个地方发生,但是实际上并不是,你的硬盘驱动器可能会将文件中的一部分存储在一个区域内,另一部分存储在另外一个区域,在你打开文件时,硬盘驱动器会迅速的将文件的所有部分汇总在一起,以便其他计算机系统可以使用它。

inode

然后在后面是一个 inode(index node),也称作索引节点。它是一个数组的结构,每个文件有一个 inode,inode 非常重要,它说明了文件的方方面面。每个索引节点都存储对象数据的属性和磁盘块位置

有一种简单的方法可以找到它们 ls -lai 命令。让我们看一下根文件系统:

inode 节点主要包括了以下信息

  • 模式/权限(保护)
  • 所有者 ID
  • 组 ID
  • 文件大小
  • 文件的硬链接数
  • 上次访问时间
  • 最后修改时间
  • inode 上次修改时间

文件分为两部分,索引节点和块。一旦创建后,每种类型的块数是固定的。你不能增加分区上 inode 的数量,也不能增加磁盘块的数量。

紧跟在 inode 后面的是根目录,它存放的是文件系统目录树的根部。最后,磁盘的其他部分存放了其他所有的目录和文件。

文件的实现

最重要的问题是记录各个文件分别用到了哪些磁盘块。不同的系统采用了不同的方法。下面我们会探讨一下这些方式。分配背后的主要思想是有效利用文件空间和快速访问文件 ,主要有三种分配方案

  • 连续分配
  • 链表分配
  • 索引分配

连续分配

最简单的分配方案是把每个文件作为一连串连续数据块存储在磁盘上。因此,在具有 1KB 块的磁盘上,将为 50 KB 文件分配 50 个连续块。

上面展示了 40 个连续的内存块。从最左侧的 0 块开始。初始状态下,还没有装载文件,因此磁盘是空的。接着,从磁盘开始处(块 0 )处开始写入占用 4 块长度的内存 A 。然后是一个占用 6 块长度的内存 B,会直接在 A 的末尾开始写。

注意每个文件都会在新的文件块开始写,所以如果文件 A 只占用了 3 又 1/2 个块,那么最后一个块的部分内存会被浪费。在上面这幅图中,总共展示了 7 个文件,每个文件都会从上个文件的末尾块开始写新的文件块。

连续的磁盘空间分配有两个优点。

  • 第一,连续文件存储实现起来比较简单,只需要记住两个数字就可以:一个是第一个块的文件地址和文件的块数量。给定第一个块的编号,可以通过简单的加法找到任何其他块的编号。

  • 第二点是读取性能比较强,可以通过一次操作从文件中读取整个文件。只需要一次寻找第一个块。后面就不再需要寻道时间和旋转延迟,所以数据会以全带宽进入磁盘。

因此,连续的空间分配具有实现简单、高性能的特点。

不幸的是,连续空间分配也有很明显的不足。随着时间的推移,磁盘会变得很零碎。下图解释了这种现象

这里有两个文件 D 和 F 被删除了。当删除一个文件时,此文件所占用的块也随之释放,就会在磁盘空间中留下一些空闲块。磁盘并不会在这个位置挤压掉空闲块,因为这会复制空闲块之后的所有文件,可能会有上百万的块,这个量级就太大了。

刚开始的时候,这个碎片不是问题,因为每个新文件都会在之前文件的结尾处进行写入。然而,磁盘最终会被填满,因此要么压缩磁盘、要么重新使用空闲块的空间。压缩磁盘的开销太大,因此不可行;后者会维护一个空闲列表,这个是可行的。但是这种情况又存在一个问题,为空闲块匹配合适大小的文件,需要知道该文件的最终大小。

想象一下这种设计的结果会是怎样的。用户启动 word 进程创建文档。应用程序首先会询问最终创建的文档会有多大。这个问题必须回答,否则应用程序就不会继续执行。如果空闲块的大小要比文件的大小小,程序就会终止。因为所使用的磁盘空间已经满了。那么现实生活中,有没有使用连续分配内存的介质出现呢?

CD-ROM 就广泛的使用了连续分配方式。

CD-ROM(Compact Disc Read-Only Memory)即只读光盘,也称作只读存储器。是一种在电脑上使用的光碟。这种光碟只能写入数据一次,信息将永久保存在光碟上,使用时通过光碟驱动器读出信息。

然而 DVD 的情况会更加复杂一些。原则上,一个 90分钟 的电影能够被编码成一个独立的、大约 4.5 GB 的文件。但是文件系统所使用的 UDF(Universal Disk Format) 格式,使用一个 30 位的数来代表文件长度,从而把文件大小限制在 1 GB。所以,DVD 电影一般存储在 3、4个连续的 1 GB 空间内。这些构成单个电影中的文件块称为扩展区(extends)。

就像我们反复提到的,历史总是惊人的相似,许多年前,连续分配由于其简单和高性能被实际使用在磁盘文件系统中。后来由于用户不希望在创建文件时指定文件的大小,于是放弃了这种想法。但是随着 CD-ROM 、DVD、蓝光光盘等光学介质的出现,连续分配又流行起来。从而得出结论,技术永远没有过时性,现在看似很老的技术,在未来某个阶段可能又会流行起来。

链表分配

第二种存储文件的方式是为每个文件构造磁盘块链表,每个文件都是磁盘块的链接列表,就像下面所示

每个块的第一个字作为指向下一块的指针,块的其他部分存放数据。如果上面这张图你看的不是很清楚的话,可以看看整个的链表分配方案

与连续分配方案不同,这一方法可以充分利用每个磁盘块。除了最后一个磁盘块外,不会因为磁盘碎片而浪费存储空间。同样,在目录项中,只要存储了第一个文件块,那么其他文件块也能够被找到。

另一方面,在链表的分配方案中,尽管顺序读取非常方便,但是随机访问却很困难(这也是数组和链表数据结构的一大区别)。

还有一个问题是,由于指针会占用一些字节,每个磁盘块实际存储数据的字节数并不再是 2 的整数次幂。虽然这个问题并不会很严重,但是这种方式降低了程序运行效率。许多程序都是以长度为 2 的整数次幂来读写磁盘,由于每个块的前几个字节被指针所使用,所以要读出一个完成的块大小信息,就需要当前块的信息和下一块的信息拼凑而成,因此就引发了查找和拼接的开销。

使用内存表进行链表分配

由于连续分配和链表分配都有其不可忽视的缺点。所以提出了使用内存中的表来解决分配问题。取出每个磁盘块的指针字,把它们放在内存的一个表中,就可以解决上述链表的两个不足之处。下面是一个例子

上图表示了链表形成的磁盘块的内容。这两个图中都有两个文件,文件 A 依次使用了磁盘块地址 4、7、 2、 10、 12,文件 B 使用了6、3、11 和 14。也就是说,文件 A 从地址 4 处开始,顺着链表走就能找到文件 A 的全部磁盘块。同样,从第 6 块开始,顺着链走到最后,也能够找到文件 B 的全部磁盘块。你会发现,这两个链表都以不属于有效磁盘编号的特殊标记(-1)结束。内存中的这种表格称为 文件分配表(File Application Table,FAT)。

使用这种组织方式,整个块都可以存放数据。进而,随机访问也容易很多。虽然仍要顺着链在内存中查找给定的偏移量,但是整个链都存放在内存中,所以不需要任何磁盘引用。与前面的方法相同,不管文件有多大,在目录项中只需记录一个整数(起始块号),按照它就可以找到文件的全部块。

这种方式存在缺点,那就是必须要把整个链表放在内存中。对于 1TB 的磁盘和 1KB 的大小的块,那么这张表需要有 10 亿项。。。每一项对应于这 10 亿个磁盘块中的一块。每项至少 3 个字节,为了提高查找速度,有时需要 4 个字节。根据系统对空间或时间的优化方案,这张表要占用 3GB 或 2.4GB 的内存。FAT 的管理方式不能较好地扩展并应用于大型磁盘中。而这正是最初 MS-DOS 文件比较实用,并仍被各个 Windows 版本所安全支持。

inode

最后一个记录各个文件分别包含哪些磁盘块的方法是给每个文件赋予一个称为 inode(索引节点) 的数据结构,每个文件都与一个 inode 进行关联,inode 由整数进行标识。

下面是一个简单例子的描述。

给出 inode 的长度,就能够找到文件中的所有块。

相对于在内存中使用表的方式而言,这种机制具有很大的优势。即只有在文件打开时,其 inode 才会在内存中。如果每个 inode 需要 n 个字节,最多 k 个文件同时打开,那么 inode 占有总共打开的文件是 kn 字节。仅需预留这么多空间。

这个数组要比我们上面描述的 FAT(文件分配表) 占用的空间小的多。原因是用于保存所有磁盘块的链接列表的表的大小与磁盘本身成正比。如果磁盘有 n 个块,那么这个表也需要 n 项。随着磁盘空间的变大,那么该表也随之线性增长。相反,inode 需要节点中的数组,其大小和可能需要打开的最大文件个数成正比。它与磁盘是 100GB、4000GB 还是 10000GB 无关。

inode 的一个问题是如果每个节点都会有固定大小的磁盘地址,那么文件增长到所能允许的最大容量外会发生什么?一个解决方案是最后一个磁盘地址不指向数据块,而是指向一个包含额外磁盘块地址的地址,如上图所示。一个更高级的解决方案是:有两个或者更多包含磁盘地址的块,或者指向其他存放地址的磁盘块的磁盘块。Windows 的 NTFS 文件系统采用了相似的方法,所不同的仅仅是大的 inode 也可以表示小的文件。

NTFS 的全称是 New Technology File System,是微软公司开发的专用系统文件,NTFS 取代 FAT(文件分配表) 和 HPFS(高性能文件系统) ,并在此基础上进一步改进。例如增强对元数据的支持,使用更高级的数据结构以提升性能、可靠性和磁盘空间利用率等。

目录的实现

文件只有打开后才能够被读取。在文件打开后,操作系统会使用用户提供的路径名来定位磁盘中的目录。目录项提供了查找文件磁盘块所需要的信息。根据系统的不同,提供的信息也不同,可能提供的信息是整个文件的磁盘地址,或者是第一个块的数量(两个链表方案)或 inode的数量。不过不管用那种情况,目录系统的主要功能就是 将文件的 ASCII 码的名称映射到定位数据所需的信息上。

与此关系密切的问题是属性应该存放在哪里。每个文件系统包含不同的文件属性,例如文件的所有者和创建时间,需要存储的位置。一种显而易见的方法是直接把文件属性存放在目录中。有一些系统恰好是这么做的,如下。

在这种简单的设计中,目录有一个固定大小的目录项列表,每个文件对应一项,其中包含一个固定长度的文件名,文件属性的结构体以及用以说明磁盘块位置的一个或多个磁盘地址。

对于采用 inode 的系统,会把 inode 存储在属性中而不是目录项中。在这种情况下,目录项会更短:仅仅只有文件名称和 inode 数量。这种方式如下所示

到目前为止,我们已经假设文件具有较短的、固定长度的名字。在 MS-DOS 中,具有 1 – 8 个字符的基本名称和 1 – 3 个字符的可拓展名称。在 UNIX 版本 7 中,文件有 1 – 14 个字符,包括任何拓展。然而,几乎所有的现代操作系统都支持可变长度的扩展名。这是如何实现的呢?

最简单的方式是给予文件名一个长度限制,比如 255 个字符,然后使用上图中的设计,并为每个文件名保留 255 个字符空间。这种处理很简单,但是浪费了大量的目录空间,因为只有很少的文件会有那么长的文件名称。所以,需要一种其他的结构来处理。

一种可选择的方式是放弃所有目录项大小相同的想法。在这种方法中,每个目录项都包含一个固定部分,这个固定部分通常以目录项的长度开始,后面是固定格式的数据,通常包括所有者、创建时间、保护信息和其他属性。这个固定长度的头的后面是一个任意长度的实际文件名,如下图所示

上图是 SPARC 机器使用正序放置。

处理机中的一串字符存放的顺序有正序(big-endian) 和逆序(little-endian) 之分。正序存放的就是高字节在前低字节在后,而逆序存放的就是低字节在前高字节在后。

这个例子中,有三个文件,分别是 project-budget、personnel 和 foo。每个文件名以一个特殊字符(通常是 0 )结束,用矩形中的叉进行表示。为了使每个目录项从字的边界开始,每个文件名被填充成整数个字,如下图所示

这个方法的缺点是当文件被移除后,就会留下一块固定长度的空间,而新添加进来的文件大小不一定和空闲空间大小一致。

这个问题与我们上面探讨的连续磁盘文件的问题是一样的,由于整个目录在内存中,所以只有对目录进行紧凑拼接操作才可节省空间。另一个问题是,一个目录项可能会分布在多个页上,在读取文件名时可能发生缺页中断。

处理可变长度文件名字的另外一种方法是,使目录项自身具有固定长度,而将文件名放在目录末尾的堆栈中。如上图所示的这种方式。这种方法的优点是当目录项被移除后,下一个文件将能够正常匹配移除文件的空间。当然,必须要对堆进行管理,因为在处理文件名的时候也会发生缺页异常。

到目前为止的所有设计中,在需要查找文件名时,所有的方案都是线性的从头到尾对目录进行搜索。对于特别长的目录,线性搜索的效率很低。提高文件检索效率的一种方式是在每个目录上使用哈希表(hash table),也叫做散列表。我们假设表的大小为 n,在输入文件名时,文件名被散列在 0 和 n – 1 之间,例如,它被 n 除,并取余数。或者对构成文件名字的字求和或类似某种方法。

无论采用哪种方式,在添加一个文件时都要对与散列值相对 应的散列表进行检查。如果没有使用过,就会将一个指向目录项的指针指向这里。文件目录项紧跟着哈希表后面。如果已经使用过,就会构造一个链表(这种构造方式是不是和 HashMap 使用的数据结构一样?),链表的表头指针存放在表项中,并通过哈希值将所有的表项相连。

查找文件的过程和添加类似,首先对文件名进行哈希处理,在哈希表中查找是否有这个哈希值,如果有的话,就检查这条链上所有的哈希项,查看文件名是否存在。如果哈希不在链上,那么文件就不在目录中。

使用哈希表的优势是查找非常迅速,缺点是管理起来非常复杂。只有在系统中会有成千上万个目录项存在时,才会考虑使用散列表作为解决方案。

另外一种在大量目录中加快查找指令目录的方法是使用缓存,缓存查找的结果。在开始查找之前,会首先检查文件名是否在缓存中。如果在缓存中,那么文件就能立刻定位。当然,只有在较少的文件下进行多次查找,缓存才会发挥最大功效。

共享文件

当多个用户在同一个项目中工作时,他们通常需要共享文件。如果这个共享文件同时出现在多个用户目录下,那么他们协同工作起来就很方便。下面的这张图我们在上面提到过,但是有一个更改的地方,就是 C 的一个文件也出现在了 B 的目录下。

如果按照如上图的这种组织方式而言,那么 B 的目录与该共享文件的联系称为 链接(link)。那么文件系统现在就是一个 有向无环图(Directed Acyclic Graph, 简称 DAG),而不是一棵树了。

在图论中,如果一个有向图从任意顶点出发无法经过若干条边回到该点,则这个图是一个有向无环图,我们不会在此着重探讨关于图论的东西,大家可以自行 google。

将文件系统组织成为有向无环图会使得维护复杂化,但也是必须要付出的代价。

共享文件很方便,但这也会带来一些问题。如果目录中包含磁盘地址,则当链接文件时,必须把 C 目录中的磁盘地址复制到 B 目录中。如果 B 或者 C 随后又向文件中添加内容,则仅在执行追加的用户的目录中显示新写入的数据块。这种变更将会对其他用户不可见,从而破坏了共享的目的。

有两种方案可以解决这种问题。

  • 第一种解决方案,磁盘块不列入目录中,而是会把磁盘块放在与文件本身相关联的小型数据结构中。目录将指向这个小型数据结构。这是 UNIX 中使用的方式(小型数据结构就是 inode)。

  • 在第二种解决方案中,通过让系统建立一个类型为 LINK 的新文件,并把该文件放在 B 的目录下,使得 B 与 C 建立链接。新的文件中只包含了它所链接的文件的路径名。当 B 想要读取文件时,操作系统会检查 B 的目录下存在一个类型为 LINK 的文件,进而找到该链接的文件和路径名,然后再去读文件,这种方式称为 符号链接(symbolic linking)。

上面的每一种方法都有各自的缺点,在第一种方式中,B 链接到共享文件时,inode 记录文件的所有者为 C。建立一个链接并不改变所有关系,如下图所示。

第一开始的情况如图 a 所示,此时 C 的目录的所有者是 C ,当目录 B 链接到共享文件时,并不会改变 C 的所有者关系,只是把计数 + 1,所以此时 系统知道目前有多少个目录指向这个文件。然后 C 尝试删除这个文件,这个时候有个问题,如果 C 把文件移除并清除了 inode 的话,那么 B 会有一个目录项指向无效的节点。如果 inode 以后分配给另一个文件,则 B 的链接指向一个错误的文件。系统通过 inode 可知文件仍在被引用,但是没有办法找到该文件的全部目录项以删除它们。指向目录的指针不能存储在 inode 中,原因是有可能有无数个这样的目录。

所以我们能做的就是删除 C 的目录项,但是将 inode 保留下来,并将计数设置为 1 ,如上图 c 所示。c 表示的是只有 B 有指向该文件的目录项,而该文件的前者是 C 。如果系统进行记账操作的话,那么 C 将继续为该文件付账直到 B 决定删除它,如果是这样的话,只有到计数变为 0 的时刻,才会删除该文件。

对于符号链接,以上问题不会发生,只有真正的文件所有者才有一个指向 inode 的指针。链接到该文件上的用户只有路径名,没有指向 inode 的指针。当文件所有者删除文件时,该文件被销毁。以后若试图通过符号链接访问该文件将会失败,因为系统不能找到该文件。删除符号链接不会影响该文件。

符号链接的问题是需要额外的开销。必须读取包含路径的文件,然后要一个部分接一个部分地扫描路径,直到找到 inode 。这些操作也许需要很多次额外的磁盘访问。此外,每个符号链接都需要额外的 inode ,以及额外的一个磁盘块用于存储路径,虽然如果路径名很短,作为一种优化,系统可以将它存储在 inode 中。符号链接有一个优势,即只要简单地提供一个机器的网络地址以及文件在该机器上驻留的路径,就可以连接全球任何地方机器上的文件。

还有另一个由链接带来的问题,在符号链接和其他方式中都存在。如果允许链接,文件有两个或多个路径。查找一指定目录及其子目录下的全部文件的程序将多次定位到被链接的文件。例如,一个将某一目录及其子目录下的文件转存到磁带上的程序有可能多次复制一个被链接的文件。进而,如果接着把磁带读入另一台机器,除非转出程序具有智能,否则被链接的文件将被两次复制到磁盘上,而不是只是被链接起来。

日志结构文件系统

技术的改变会给当前的文件系统带来压力。这种情况下,CPU 会变得越来越快,磁盘会变得越来越大并且越来越便宜(但不会越来越快)。内存容量也是以指数级增长。但是磁盘的寻道时间(除了固态盘,因为固态盘没有寻道时间)并没有获得提高。

这些因素结合起来意味着许多系统文件中出现性能瓶颈。为此,Berkeley 设计了一种全新的文件系统,试图缓解这个问题,这个文件系统就是 日志结构文件系统(Log-structured File System, LFS)。

日志结构文件系统由 Rosenblum 和 Ousterhout 于90年代初引入,旨在解决以下问题。

  • 不断增长的系统内存

  • 顺序 I/O 性能胜过随机 I/O 性能

  • 现有低效率的文件系统

  • 文件系统不支持 RAID(虚拟化)

另一方面,当时的文件系统不论是 UNIX 还是 FFS,都有大量的随机读写(在 FFS 中创建一个新文件至少需要5次随机写),因此成为整个系统的性能瓶颈。同时因为 Page cache 的存在,作者认为随机读不是主要问题:随着越来越大的内存,大部分的读操作都能被 cache,因此 LFS 主要要解决的是减少对硬盘的随机写操作。

在这种设计中,inode 甚至具有与 UNIX 中相同的结构,但是现在它们分散在整个日志中,而不是位于磁盘上的固定位置。所以,inode 很定位。为了能够找到 inode ,维护了一个由 inode 索引的 inode map(inode 映射)。表项 i 指向磁盘中的第 i 个 inode 。这个映射保存在磁盘中,但是也保存在缓存中,因此,使用最频繁的部分大部分时间都在内存中。

日志结构文件系统主要使用四种数据结构:Inode、Inode Map、Segment、Segment Usage Table。

到目前为止,所有写入最初都缓存在内存中,并且追加在日志末尾,所有缓存的写入都定期在单个段中写入磁盘。所以,现在打开文件也就意味着用映射定位文件的索引节点。一旦 inode 被定位后,磁盘块的地址就能够被找到。所有这些块本身都将位于日志中某处的分段中。

真实情况下的磁盘容量是有限的,所以最终日志会占满整个磁盘空间,这种情况下就会出现没有新的磁盘块被写入到日志中。幸运的是,许多现有段可能具有不再需要的块。例如,如果一个文件被覆盖了,那么它的 inode 将被指向新的块,但是旧的磁盘块仍在先前写入的段中占据着空间。

为了处理这个问题,LFS 有一个清理(clean)线程,它会循环扫描日志并对日志进行压缩。首先,通过查看日志中第一部分的信息来查看其中存在哪些索引节点和文件。它会检查当前 inode 的映射来查看 inode 否在在当前块中,是否仍在被使用。如果不是,该信息将被丢弃。如果仍然在使用,那么 inode 和块就会进入内存等待写回到下一个段中。然后原来的段被标记为空闲,以便日志可以用来存放新的数据。用这种方法,清理线程遍历日志,从后面移走旧的段,然后将有效的数据放入内存等待写到下一个段中。由此一来整个磁盘会形成一个大的环形缓冲区,写线程将新的段写在前面,而清理线程则清理后面的段。

日志文件系统

虽然日志结构系统的设计很优雅,但是由于它们和现有的文件系统不相匹配,因此还没有广泛使用。不过,从日志文件结构系统衍生出来一种新的日志系统,叫做日志文件系统,它会记录系统下一步将要做什么的日志。微软的 NTFS 文件系统、Linux 的 ext3 就使用了此日志。 OS X 将日志系统作为可供选项。为了看清它是如何工作的,我们下面讨论一个例子,比如 移除文件 ,这个操作在 UNIX 中需要三个步骤完成:

  • 在目录中删除文件
  • 释放 inode 到空闲 inode 池
  • 将所有磁盘块归还给空闲磁盘池。

在 Windows 中,也存在类似的步骤。不存在系统崩溃时,这些步骤的执行顺序不会带来问题。但是一旦系统崩溃,就会带来问题。假如在第一步完成后系统崩溃。inode 和文件块将不会被任何文件获得,也不会再分配;它们只存在于废物池中的某个地方,并因此减少了可利用的资源。如果崩溃发生在第二步后,那么只有磁盘块会丢失。日志文件系统保留磁盘写入期间对文件系统所做的更改的日志或日志,该日志可用于快速重建可能由于系统崩溃或断电等事件而发生的损坏。

一般文件系统崩溃后必须运行 fsck(文件系统一致性检查)实用程序。

为了让日志能够正确工作,被写入的日志操作必须是 幂等的(idempotent),它意味着只要有必要,它们就可以重复执行很多次,并不会带来破坏。像操作 更新位表并标记 inode k 或者块 n 是空闲的 可以重复执行任意次。同样地,查找一个目录并且删除所有叫 foobar 的项也是幂等的。相反,把从 inode k 新释放的块加入空闲表的末端不是幂等的,因为它们可能已经被释放并存放在那里了。

为了增加可靠性,一个文件系统可以引入数据库中 原子事务(atomic transaction) 的概念。使用这个概念,一组动作可以被界定在开始事务和结束事务操作之间。这样,文件系统就会知道它必须完成所有的动作,要么就一个不做。

虚拟文件系统

即使在同一台计算机上或者在同一个操作系统下,都会使用很多不同的文件系统。Windows 中的主要文件系统是 NTFS 文件系统,但不是说 Windows 只有 NTFS 操作系统,它还有一些其他的例如旧的 FAT -32 或 FAT -16 驱动器或分区,其中包含仍需要的数据,闪存驱动器,旧的 CD-ROM 或 DVD(每个都有自己的独特文件系统)。Windows 通过指定不同的盘符来处理这些不同的文件系统,比如 C:,D: 等。盘符可以显示存在也可以隐式存在,如果你想找指定位置的文件,那么盘符是显示存在;如果当一个进程打开一个文件时,此时盘符是隐式存在,所以 Windows 知道向哪个文件系统传递请求。

相比之下,UNIX 采用了一种不同的方式,即 UNIX 把多种文件系统整合到一个统一的结构中。一个 Linux 系统可以使用 ext2 作为根文件系统,ext3 分区装载在 /usr 下,另一块采用 Reiser FS 文件系统的硬盘装载到 /home下,以及一个 ISO 9660 的 CD – ROM 临时装载到 /mnt 下。从用户的观点来看,只有一个文件系统层级,但是事实上它们是由多个文件系统组合而成,对于用户和进程是不可见的。

UNIX 操作系统使用一种 虚拟文件系统(Virtual File System, VFS) 来尝试将多种文件系统构成一个有序的结构。关键的思想是抽象出所有文件系统都共有的部分,并将这部分代码放在一层,这一层再调用具体文件系统来管理数据。下面是一个 VFS 的系统结构

还是那句经典的话,在计算机世界中,任何解决不了的问题都可以加个代理来解决。所有和文件相关的系统调用在最初的处理上都指向虚拟文件系统。这些来自用户进程的调用,都是标准的 POSIX 系统调用,比如 open、read、write 和 seek 等。VFS 对用户进程有一个 上层 接口,这个接口就是著名的 POSIX 接口。

VFS 也有一个对于实际文件的 下层 接口,就是上图中标记为 VFS 的接口。这个接口包含许多功能调用,这样 VFS 可以使每一个文件系统完成任务。因此,要创建一个可以与 VFS 一起使用的新文件系统,新文件系统的设计者必须确保它提供了 VFS 要求的功能。一个明显的例子是从磁盘读取特定的块,然后将其放入文件系统的缓冲区高速缓存中,然后返回指向该块的指针的函数。 因此,VFS具有两个不同的接口:上一个到用户进程,下一个到具体文件系统。

当系统启动时,根文件系统在 VFS 中注册。另外,当装载其他文件时,不管在启动时还是在操作过程中,它们也必须在 VFS 中注册。当一个文件系统注册时,根文件系统注册到 VFS。另外,在引导时或操作期间挂载其他文件系统时,它们也必须向 VFS 注册。当文件系统注册时,其基本作用是提供 VFS 所需功能的地址列表、调用向量表、或者 VFS 对象。因此一旦文件系统注册到 VFS,它就知道从哪里开始读取数据块。

装载文件系统后就可以使用它了。比如,如果一个文件系统装载到 /usr 并且一个进程调用它:

open("/usr/include/unistd.h",O_RDONLY)

当解析路径时, VFS 看到新的文件系统被挂载到 /usr,并且通过搜索已经装载文件系统的超级块来确定它的超块。然后它找到它所转载的文件的根目录,在那里查找路径 include/unistd.h。然后 VFS 创建一个 vnode 并调用实际文件系统,以返回所有的在文件 inode 中的信息。这个信息和其他信息一起复制到 vnode (内存中)。而这些其他信息中最重要的是指向包含调用 vnode 操作的函数表的指针,比如 read、write 和 close 等。

当 vnode 被创建后,为了进程调用,VFS 在文件描述符表中创建一个表项,并将它指向新的 vnode,最后,VFS 向调用者返回文件描述符,所以调用者可以用它去 read、write 或者 close 文件。

当进程用文件描述符进行一个读操作时,VFS 通过进程表和文件描述符确定 vnode 的位置,并跟随指针指向函数表,这样就调用了处理 read 函数,运行在实际系统中的代码并得到所请求的块。VFS 不知道请求时来源于本地硬盘、还是来源于网络中的远程文件系统、CD-ROM 、USB 或者其他介质,所有相关的数据结构欧如下图所示

从调用者进程号和文件描述符开始,进而是 vnode,读函数指针,然后是对实际文件系统的访问函数定位。

文件系统的管理和优化

能够使文件系统工作是一回事,能够使文件系统高效、稳定的工作是另一回事,下面我们就来探讨一下文件系统的管理和优化。

磁盘空间管理

文件通常存在磁盘中,所以如何管理磁盘空间是一个操作系统的设计者需要考虑的问题。在文件上进行存有两种策略:分配 n 个字节的连续磁盘空间;或者把文件拆分成多个并不一定连续的块。在存储管理系统中,主要有分段管理和 分页管理 两种方式。

正如我们所看到的,按连续字节序列存储文件有一个明显的问题,当文件扩大时,有可能需要在磁盘上移动文件。内存中分段也有同样的问题。不同的是,相对于把文件从磁盘的一个位置移动到另一个位置,内存中段的移动操作要快很多。因此,几乎所有的文件系统都把文件分割成固定大小的块来存储。

块大小

一旦把文件分为固定大小的块来存储,就会出现问题,块的大小是多少?按照磁盘组织方式,扇区、磁道和柱面显然都可以作为分配单位。在分页系统中,分页大小也是主要因素。

拥有大的块尺寸意味着每个文件,甚至 1 字节文件,都要占用一个柱面空间,也就是说小文件浪费了大量的磁盘空间。另一方面,小块意味着大部分文件将会跨越多个块,因此需要多次搜索和旋转延迟才能读取它们,从而降低了性能。因此,如果分配的块太大会浪费空间;分配的块太小会浪费时间。

记录空闲块

一旦指定了块大小,下一个问题就是怎样跟踪空闲块。有两种方法被广泛采用,如下图所示

第一种方法是采用磁盘块链表,链表的每个块中包含极可能多的空闲磁盘块号。对于 1 KB 的块和 32 位的磁盘块号,空闲表中每个块包含有 255 个空闲的块号。考虑 1 TB 的硬盘,拥有大概十亿个磁盘块。为了存储全部地址块号,如果每块可以保存 255 个块号,则需要将近 400 万个块。通常,空闲块用于保存空闲列表,因此存储基本上是空闲的。

另一种空闲空间管理的技术是位图(bitmap),n 个块的磁盘需要 n 位位图。在位图中,空闲块用 1 表示,已分配的块用 0 表示。对于 1 TB 硬盘的例子,需要 10 亿位表示,即需要大约 130 000 个 1 KB 块存储。很明显,和 32 位链表模型相比,位图需要的空间更少,因为每个块使用 1 位。只有当磁盘快满的时候,链表需要的块才会比位图少。

如果空闲块是长期连续的话,那么空闲列表可以改成记录连续分块而不是单个的块。每个块都会使用 8位、16位、32 位的计数来与每个块相联,来记录连续空闲块的数量。最好的情况是一个空闲块可以用两个数字来表示:第一个空闲块的地址和空闲块的计数。另一方面,如果磁盘严重碎片化,那么跟踪连续分块要比跟踪单个分块运行效率低,因为不仅要存储地址,还要存储数量。

这种情况说明了一个操作系统设计者经常遇到的一个问题。有许多数据结构和算法可以用来解决问题,但是选择一个最好的方案需要数据的支持,而这些数据是设计者无法预先拥有的。只有在系统部署完毕真正使用使用后才会获得。

现在,回到空闲链表的方法,只有一个指针块保存在内存中。创建文件时,所需要的块从指针块中取出。当它用完时,将从磁盘中读取一个新的指针块。类似地,删除文件时,文件的块将被释放并添加到主存中的指针块中。当块被填满时,写回磁盘。

在某些特定的情况下,这个方法导致了不必要的磁盘 IO,如下图所示

上面内存中的指针块仅有两个空闲块,如果释放了一个含有三个磁盘块的文件,那么该指针块就会溢出,必须将其写入磁盘,那么就会产生如下图的这种情况。

如果现在写入含有三个块的文件,已满的指针不得不再次读入,这将会回到上图 a 中的情况。如果有三个块的文件只是作为临时文件被写入,在释放它时,需要进行另一次磁盘写操作以将完整的指针块写回到磁盘。简而言之,当指针块几乎为空时,一系列短暂的临时文件可能会导致大量磁盘 I/O。

避免大部分磁盘 I/O 的另一种方法是拆分完整的指针块。这样,当释放三个块时,变化不再是从 a – b,而是从 a – c,如下图所示

现在,系统可以处理一系列临时文件,而不需要进行任何磁盘 I/O。如果内存中指针块满了,就写入磁盘,半满的指针块从磁盘中读入。这里的思想是:要保持磁盘上的大多数指针块为满的状态(减少磁盘的使用),但是在内存中保留了一个半满的指针块。这样,就可以既处理文件的创建又同时可以处理文件的删除操作,而不会为空闲表进行磁盘 I/O。

对于位图,会在内存中只保留一个块,只有在该块满了或空了的情形下,才到磁盘上取另一个块。通过在位图的单一块上进行所有的分配操作,磁盘块会紧密的聚集在一起,从而减少了磁盘臂的移动。由于位图是一种固定大小的数据结构,所以如果内核是分页的,就可以把位图放在虚拟内存中,在需要时将位图的页面调入。

磁盘配额

为了防止一些用户占用太多的磁盘空间,多用户操作通常提供一种磁盘配额(enforcing disk quotas)的机制。系统管理员为每个用户分配最大的文件和块分配,并且操作系统确保用户不会超过其配额。我们下面会谈到这一机制。

在用户打开一个文件时,操作系统会找到文件属性和磁盘地址,并把它们送入内存中的打开文件表。其中一个属性告诉文件所有者是谁。任何有关文件的增加都会记到所有者的配额中。

第二张表包含了每个用户当前打开文件的配额记录,即使是其他人打开该文件也一样。如上图所示,该表的内容是从被打开文件的所有者的磁盘配额文件中提取出来的。当所有文件关闭时,该记录被写回配额文件。

当在打开文件表中建立一新表项时,会产生一个指向所有者配额记录的指针。每次向文件中添加一个块时,文件所有者所用数据块的总数也随之增加,并会同时增加硬限制和软限制的检查。可以超出软限制,但硬限制不可以超出。当已达到硬限制时,再往文件中添加内容将引发错误。同样,对文件数目也存在类似的检查。

什么是硬限制和软限制?硬限制是软限制的上限。软限制是为会话或进程实际执行的限制。这允许管理员(或用户)将硬限制设置为允许它们希望允许的最大使用上限。然后,其他用户和进程可以根据需要使用软限制将其资源使用量自限制到更低的上限。

当一个用户尝试登陆,系统将检查配额文件以查看用户是否超出了文件数量或磁盘块数量的软限制。如果违反了任一限制,则会显示警告,保存的警告计数减 1,如果警告计数为 0 ,表示用户多次忽略该警告,因而将不允许该用户登录。要想再得到登录的许可,就必须与系统管理员协商。

如果用户在退出系统时消除所超过的部分,他们就可以再一次终端会话期间超过其软限制,但无论什么情况下都不会超过硬限制。

文件系统备份

文件系统的毁坏要比计算机的损坏严重很多。无论是硬件还是软件的故障,只要计算机文件系统被破坏,要恢复起来都是及其困难的,甚至是不可能的。因为文件系统无法抵御破坏,因而我们要在文件系统在被破坏之前做好数据备份,但是备份也不是那么容易,下面我们就来探讨备份的过程。

许多人认为为文件系统做备份是不值得的,并且很浪费时间,直到有一天他们的磁盘坏了,他们才意识到事情的严重性。相对来说,公司在这方面做的就很到位。磁带备份主要要处理好以下两个潜在问题中的一个

  • 从意外的灾难中恢复

这个问题主要是由于外部条件的原因造成的,比如磁盘破裂,水灾火灾等。

  • 从错误的操作中恢复

第二个问题通常是由于用户意外的删除了原本需要还原的文件。这种情况发生的很频繁,使得 Windows 的设计者们针对 删除 命令专门设计了特殊目录,这就是 回收站(recycle bin),也就是说,在删除文件的时候,文件本身并不真正从磁盘上消失,而是被放置到这个特殊目录下,等以后需要的时候可以还原回去。文件备份更主要是指这种情况,能够允许几天之前,几周之前的文件从原来备份的磁盘进行还原。

做文件备份很耗费时间而且也很浪费空间,这会引起下面几个问题。首先,是要备份整个文件还是仅备份一部分呢?一般来说,只是备份特定目录及其下的全部文件,而不是备份整个文件系统。

其次,对上次未修改过的文件再进行备份是一种浪费,因而产生了一种增量转储(incremental dumps) 的思想。最简单的增量转储的形式就是周期性的做全面的备份,而每天只对增量转储完成后发生变化的文件做单个备份。

周期性:比如一周或者一个月

稍微好一点的方式是只备份最近一次转储以来更改过的文件。当然,这种做法极大的缩减了转储时间,但恢复起来却更复杂,因为最近的全面转储先要全部恢复,随后按逆序进行增量转储。为了方便恢复,人们往往使用更复杂的转储模式。

第三,既然待转储的往往是海量数据,那么在将其写入磁带之前对文件进行压缩就很有必要。但是,如果在备份过程中出现了文件损坏的情况,就会导致破坏压缩算法,从而使整个磁带无法读取。所以在备份前是否进行文件压缩需慎重考虑。

第四,对正在使用的文件系统做备份是很难的。如果在转储过程中要添加,删除和修改文件和目录,则转储结果可能不一致。因此,因为转储过程中需要花费数个小时的时间,所以有必要在晚上将系统脱机进行备份,然而这种方式的接受程度并不高。所以,人们修改了转储算法,记下文件系统的瞬时快照,即复制关键的数据结构,然后需要把将来对文件和目录所做的修改复制到块中,而不是到处更新他们。

磁盘转储到备份磁盘上有两种方案:物理转储和逻辑转储。物理转储(physical dump) 是从磁盘的 0 块开始,依次将所有磁盘块按照顺序写入到输出磁盘,并在复制最后一个磁盘时停止。这种程序的万无一失性是其他程序所不具备的。

第二个需要考虑的是坏块的转储。制造大型磁盘而没有瑕疵是不可能的,所以也会存在一些坏块(bad blocks)。有时进行低级格式化后,坏块会被检测出来并进行标记,这种情况的解决办法是用磁盘末尾的一些空闲块所替换。

然而,一些块在格式化后会变坏,在这种情况下操作系统可以检测到它们。通常情况下,它可以通过创建一个由所有坏块组成的文件来解决问题,确保它们不会出现在空闲池中并且永远不会被分配。那么此文件是完全不可读的。如果磁盘控制器将所有的坏块重新映射,物理转储还是能够正常工作的。

Windows 系统有分页文件(paging files) 和 休眠文件(hibernation files) 。它们在文件还原时不发挥作用,同时也不应该在第一时间进行备份。

物理转储和逻辑转储

物理转储的主要优点是简单、极为快速(基本上是以磁盘的速度运行),缺点是全量备份,不能跳过指定目录,也不能增量转储,也不能恢复个人文件的请求。因此句大多数情况下不会使用物理转储,而使用逻辑转储。

逻辑转储(logical dump)从一个或几个指定的目录开始,递归转储自指定日期开始后更改的文件和目录。因此,在逻辑转储中,转储磁盘上有一系列经过仔细识别的目录和文件,这使得根据请求轻松还原特定文件或目录。

既然逻辑转储是最常用的方式,那么下面就让我们研究一下逻辑转储的通用算法。此算法在 UNIX 系统上广为使用,如下图所示

待转储的文件系统,其中方框代表目录,圆圈代表文件。黄色的项目表是自上次转储以来修改过。每个目录和文件都被标上其 inode 号。

此算法会转储位于修改文件或目录路径上的所有目录(也包括未修改的目录),原因有两个。第一是能够在不同电脑的文件系统中恢复转储的文件。通过这种方式,转储和重新存储的程序能够用来在两个电脑之间传输整个文件系统。第二个原因是能够对单个文件进行增量恢复。

逻辑转储算法需要维持一个 inode 为索引的位图(bitmap),每个 inode 包含了几位。随着算法的进行,位图中的这些位会被设置或清除。算法的执行分成四个阶段。第一阶段从起始目录(本例为根目录)开始检查其中所有的目录项。对每一个修改过的文件,该算法将在位图中标记其 inode。算法还会标记并递归检查每一个目录(不管是否修改过)。

在第一阶段结束时,所有修改过的文件和全部目录都在位图中标记了,如下图所示

理论上来说,第二阶段再次递归遍历目录树,并去掉目录树中任何不包含被修改过的文件或目录的标记。本阶段执行的结果如下

注意,inode 编号为 10、11、14、27、29 和 30 的目录已经被去掉了标记,因为它们所包含的内容没有修改。它们也不会转储。相反,inode 编号为 5 和 6 的目录本身尽管没有被修改过也要被转储,因为在新的机器上恢复当日的修改时需要这些信息。为了提高算法效率,可以将这两阶段的目录树遍历合二为一。

现在已经知道了哪些目录和文件必须被转储了,这就是上图 b 中标记的内容,第三阶段算法将以节点号为序,扫描这些 inode 并转储所有标记为需转储的目录,如下图所示

为了进行恢复,每个被转储的目录都用目录的属性(所有者、时间)作为前缀。

最后,在第四阶段,上图中被标记的文件也被转储,同样,由其文件属性作为前缀。至此,转储结束。

从转储磁盘上还原文件系统非常简单。一开始,需要在磁盘上创建空文件系统。然后恢复最近一次的完整转储。由于磁带上最先出现目录,所以首先恢复目录,给出文件系统的框架(skeleton),然后恢复文件系统本身。在完整存储之后是第一次增量存储,然后是第二次重复这一过程,以此类推。

尽管逻辑存储十分简单,但是也会有一些棘手的问题。首先,既然空闲块列表并不是一个文件,那么在所有被转储的文件恢复完毕之后,就需要从零开始重新构造。

另外一个问题是关于链接。如果文件链接了两个或者多个目录,而文件只能还原一次,那么并且所有指向该文件的目录都必须还原。

还有一个问题是,UNIX 文件实际上包含了许多 空洞(holes)。打开文件,写几个字节,然后找到文件中偏移了一定距离的地址,又写入更多的字节,这么做是合法的。但两者之间的这些块并不属于文件本身,从而也不应该在其上进行文件转储和恢复。

最后,无论属于哪一个目录,特殊文件,命名管道以及类似的文件都不应该被转储。

文件系统的一致性

影响可靠性的一个因素是文件系统的一致性。许多文件系统读取磁盘块、修改磁盘块、再把它们写回磁盘。如果系统在所有块写入之前崩溃,文件系统就会处于一种不一致(inconsistent)的状态。如果某些尚未写回的块是索引节点块,目录块或包含空闲列表的块,则此问题是很严重的。

为了处理文件系统一致性问题,大部分计算机都会有应用程序来检查文件系统的一致性。例如,UNIX 有 fsck;Windows 有 sfc,每当引导系统时(尤其是在崩溃后),都可以运行该程序。

可以进行两种一致性检查:块的一致性检查和文件的一致性检查。为了检查块的一致性,应用程序会建立两张表,每个包含一个计数器的块,最初设置为 0 。第一个表中的计数器跟踪该块在文件中出现的次数,第二张表中的计数器记录每个块在空闲列表、空闲位图中出现的频率。

然后检验程序使用原始设备读取所有的 inode,忽略文件的结构,只返回从零开始的所有磁盘块。从 inode 开始,很容易找到文件中的块数量。每当读取一个块时,该块在第一个表中的计数器 + 1,应用程序会检查空闲块或者位图来找到没有使用的块。空闲列表中块的每次出现都会导致其在第二表中的计数器增加。

如果文件系统一致,则每一个块或者在第一个表计数器为 1,或者在第二个表计数器中为 1,如下图所示

但是当系统崩溃后,这两张表可能如下所示

其中,磁盘块 2 没有出现在任何一张表中,这称为 块丢失(missing block)。尽管块丢失不会造成实际的损害,但它的确浪费了磁盘空间,减少了磁盘容量。块丢失的问题很容易解决,文件系统检验程序把他们加到空闲表中即可。

有可能出现的另外一种情况如下所示

其中,块 4 在空闲表中出现了 2 次。这种解决方法也很简单,只要重新建立空闲表即可。

最糟糕的情况是在两个或者多个文件中出现同一个数据块,如下所示

比如上图的磁盘块 5,如果其中一个文件被删除,块 5 会被添加到空闲表中,导致一个块同时处于使用和空闲的两种状态。如果删除这两个文件,那么在空闲表中这个磁盘块会出现两次。

文件系统检验程序采取的处理方法是,先分配一磁盘块,把块 5 中的内容复制到空闲块中,然后把它插入到其中一个文件中。这样文件的内容未改变,虽然这些内容可以肯定是不对的,但至少保证了文件的一致性。这一错误应该报告给用户,由用户检查受检情况。

除了检查每个磁盘块计数的正确性之外,文件系统还会检查目录系统。这时候会用到一张计数器表,但这时是一个文件(而不是一个块)对应于一个计数器。程序从根目录开始检验,沿着目录树向下查找,检查文件系统的每个目录。对每个目录中的文件,使其计数 + 1。

注意,由于存在硬连接,一个文件可能出现在两个或多个目录中。而遇到符号链接是不计数的,不会对目标文件的计数器 + 1。

在检验程序完成后,会得到一张由 inode 索引的表,说明每个文件和目录的包含关系。检验程序会将这些数字与存储在文件 inode 中的链接数目做对比。如果 inode 节点的链接计数大户目录项个数,这时即使所有文件从目录中删除,这个计数仍然不是 0 ,inode 不会被删除。这种错误不严重,却因为存在不属于任何目录的文件而浪费了磁盘空间。

另一种错误则是潜在的风险。如果同一个文件链接两个目录项,但是 inode 链接计数只为 1,如果删除了任何一个目录项,对应 inode 链接计数变为 0。当 inode 计数为 0 时,文件系统标志 inode 为 未使用,并释放全部的块。这会导致其中一个目录指向一未使用的 inode,而很有可能其块马上就被分配给其他文件。

文件系统性能

访问磁盘的效率要比内存满的多,是时候又祭出这张图了

从内存读一个 32 位字大概是 10ns,从硬盘上读的速率大概是 100MB/S,对每个 32 位字来说,效率会慢了四倍,另外,还要加上 5 – 10 ms 的寻道时间等其他损耗,如果只访问一个字,内存要比磁盘快百万数量级。所以磁盘优化是很有必要的,下面我们会讨论几种优化方式

高速缓存

最常用的减少磁盘访问次数的技术是使用 块高速缓存(block cache) 或者 缓冲区高速缓存(buffer cache)。高速缓存指的是一系列的块,它们在逻辑上属于磁盘,但实际上基于性能的考虑被保存在内存中。

管理高速缓存有不同的算法,常用的算法是:检查全部的读请求,查看在高速缓存中是否有所需要的块。如果存在,可执行读操作而无须访问磁盘。如果检查块不再高速缓存中,那么首先把它读入高速缓存,再复制到所需的地方。之后,对同一个块的请求都通过高速缓存来完成。

高速缓存的操作如下图所示

由于在高速缓存中有许多块,所以需要某种方法快速确定所需的块是否存在。常用方法是将设备和磁盘地址进行散列操作,然后,在散列表中查找结果。具有相同散列值的块在一个链表中连接在一起(这个数据结构是不是很像 HashMap?),这样就可以沿着冲突链查找其他块。

如果高速缓存已满,此时需要调入新的块,则要把原来的某一块调出高速缓存,如果要调出的块在上次调入后已经被修改过,则需要把它写回磁盘。这种情况与分页非常相似,所有常用的页面置换算法我们之前已经介绍过,如果有不熟悉的小伙伴可以参考 https://mp.weixin.qq.com/s/5-k2BJDgEp9symxcSwoprw。比如 FIFO 算法、第二次机会算法、LRU 算法、时钟算法、老化算法等。它们都适用于高速缓存。

块提前读

第二个明显提高文件系统的性能是,在需要用到块之前,试图提前将其写入高速缓存,从而提高命中率。许多文件都是顺序读取。如果请求文件系统在某个文件中生成块 k,文件系统执行相关操作并且在完成之后,会检查高速缓存,以便确定块 k + 1 是否已经在高速缓存。如果不在,文件系统会为 k + 1 安排一个预读取,因为文件希望在用到该块的时候能够直接从高速缓存中读取。

当然,块提前读取策略只适用于实际顺序读取的文件。对随机访问的文件,提前读丝毫不起作用。甚至还会造成阻碍。

减少磁盘臂运动

高速缓存和块提前读并不是提高文件系统性能的唯一方法。另一种重要的技术是把有可能顺序访问的块放在一起,当然最好是在同一个柱面上,从而减少磁盘臂的移动次数。当写一个输出文件时,文件系统就必须按照要求一次一次地分配磁盘块。如果用位图来记录空闲块,并且整个位图在内存中,那么选择与前一块最近的空闲块是很容易的。如果用空闲表,并且链表的一部分存在磁盘上,要分配紧邻的空闲块就会困难很多。

不过,即使采用空闲表,也可以使用 块簇 技术。即不用块而用连续块簇来跟踪磁盘存储区。如果一个扇区有 512 个字节,有可能系统采用 1 KB 的块(2 个扇区),但却按每 2 块(4 个扇区)一个单位来分配磁盘存储区。这和 2 KB 的磁盘块并不相同,因为在高速缓存中它仍然使用 1 KB 的块,磁盘与内存数据之间传送也是以 1 KB 进行,但在一个空闲的系统上顺序读取这些文件,寻道的次数可以减少一半,从而使文件系统的性能大大改善。若考虑旋转定位则可以得到这类方法的变体。在分配块时,系统尽量把一个文件中的连续块存放在同一个柱面上。

在使用 inode 或任何类似 inode 的系统中,另一个性能瓶颈是,读取一个很短的文件也需要两次磁盘访问:一次是访问 inode,一次是访问块。通常情况下,inode 的放置如下图所示

其中,全部 inode 放在靠近磁盘开始位置,所以 inode 和它所指向的块之间的平均距离是柱面组的一半,这将会需要较长时间的寻道时间。

一个简单的改进方法是,在磁盘中部而不是开始处存放 inode ,此时,在 inode 和第一个块之间的寻道时间减为原来的一半。另一种做法是:将磁盘分成多个柱面组,每个柱面组有自己的 inode,数据块和空闲表,如上图 b 所示。

当然,只有在磁盘中装有磁盘臂的情况下,讨论寻道时间和旋转时间才是有意义的。现在越来越多的电脑使用 固态硬盘(SSD),对于这些硬盘,由于采用了和闪存同样的制造技术,使得随机访问和顺序访问在传输速度上已经较为相近,传统硬盘的许多问题就消失了。但是也引发了新的问题。

磁盘碎片整理

在初始安装操作系统后,文件就会被不断的创建和清除,于是磁盘会产生很多的碎片,在创建一个文件时,它使用的块会散布在整个磁盘上,降低性能。删除文件后,回收磁盘块,可能会造成空穴。

磁盘性能可以通过如下方式恢复:移动文件使它们相互挨着,并把所有的至少是大部分的空闲空间放在一个或多个大的连续区域内。Windows 有一个程序 defrag 就是做这个事儿的。Windows 用户会经常使用它,SSD 除外。

磁盘碎片整理程序会在让文件系统上很好地运行。Linux 文件系统(特别是 ext2 和 ext3)由于其选择磁盘块的方式,在磁盘碎片整理上一般不会像 Windows 一样困难,因此很少需要手动的磁盘碎片整理。而且,固态硬盘并不受磁盘碎片的影响,事实上,在固态硬盘上做磁盘碎片整理反倒是多此一举,不仅没有提高性能,反而磨损了固态硬盘。所以碎片整理只会缩短固态硬盘的寿命。

相关参考:

https://zhuanlan.zhihu.com/p/41358013

https://www.linuxtoday.com/blog/what-is-an-inode.html

https://www.lifewire.com/what-is-fragmentation-defragmentation-2625884

https://www.geeksforgeeks.org/free-space-management-in-operating-system/

https://sites.ualberta.ca/dept/chemeng/AIX-43/share/man/info/C/a_doc_lib/aixprggd/genprogc/fsyslayout.htm

https://en.wikipedia.org/wiki/Disk_partitioning

https://en.wikipedia.org/wiki/Master_boot_record

https://en.wikipedia.org/wiki/Booting

https://www.computerhope.com/jargon/f/fileprot.htm

https://en.wikipedia.org/wiki/File_attribute

https://en.wikipedia.org/wiki/Make_(software)

https://unix.stackexchange.com/questions/60034/what-are-character-special-and-block-special-files-in-a-unix-system

https://www.computerhope.com/jargon/d/director.htm

https://www.computerhope.com/jargon/r/regular-file.htm

https://baike.baidu.com/item/固态硬盘/453510?fr=aladdin

《现代操作系统》第四版

《Modern Operation System》fourth


# 操作系统必知面试题-Java面试题


  • 操作系统必知面试题
    • 操作系统简介篇
      • 解释一下什么是操作系统
      • 操作系统的主要功能
      • 软件访问硬件的几种方式
      • 解释一下操作系统的主要目的是什么
      • 操作系统的种类有哪些
      • 为什么 Linux 系统下的应用程序不能直接在 Windows 下运行
      • 操作系统结构
        • 单体系统
        • 分层系统
        • 微内核
        • 客户-服务器模式
      • 为什么称为陷入内核
      • 什么是用户态和内核态
      • 用户态和内核态是如何切换的?
      • 什么是内核
      • 什么是实时系统
      • Linux 操作系统的启动过程
    • 进程和线程篇
      • 多处理系统的优势
      • 什么是进程和进程表
      • 什么是线程,线程和进程的区别
      • 什么是上下文切换
      • 使用多线程的好处是什么
      • 进程终止的方式
        • 进程的终止
        • 正常退出
        • 错误退出
        • 严重错误
        • 被其他进程杀死
      • 进程间的通信方式
      • 进程间状态模型
        • 进程的三态模型
        • 进程的五态模型
      • 调度算法都有哪些
        • 批处理中的调度
        • 先来先服务
        • 最短作业优先
        • 最短剩余时间优先
      • 交互式系统中的调度
        • 轮询调度
        • 优先级调度
        • 最短进程优先
        • 彩票调度
        • 公平分享调度
      • 影响调度程序的指标是什么
      • 什么是 RR 调度算法
    • 内存管理篇
      • 什么是按需分页
      • 什么是虚拟内存
      • 虚拟内存的实现方式
      • 内存为什么要分段
      • 物理地址、逻辑地址、有效地址、线性地址、虚拟地址的区别
      • 空闲内存管理的方式
        • 使用位图进行管理
        • 使用空闲链表
      • 页面置换算法都有哪些
    • 文件系统篇
      • 提高文件系统性能的方式
        • 高速缓存
        • 块提前读
        • 减少磁盘臂运动
        • 磁盘碎片整理
      • 磁盘臂调度算法
      • RAID 的不同级别
    • IO 篇
      • 操作系统中的时钟是什么
        • 时钟硬件
      • 设备控制器的主要功能
      • 中断处理过程
      • 什么是设备驱动程序
      • 什么是 DMA
      • 直接内存访问的特点
    • 死锁篇
      • 什么是僵尸进程
      • 死锁产生的原因
      • 死锁产生的必要条件
      • 死锁的恢复方式
        • 通过抢占进行恢复
        • 通过回滚进行恢复
        • 杀死进程恢复
      • 如何破坏死锁
        • 破坏互斥条件
        • 破坏保持等待的条件
        • 破坏不可抢占条件
        • 破坏循环等待条件
      • 死锁类型
        • 两阶段加锁
        • 通信死锁
        • 活锁
        • 饥饿
    • 后记

大家好,我是 cxuan,我之前汇总了一下关于操作系统的面试题,最近又重新翻阅了一下发现不是很全,现在也到了面试季了,所以我又花了一周的时间修订整理了一下这份面试题,这份面试题可以吊打市面上所有的操作系统面试题了,不是我说,是因为我系统查过,如果有不相信的大佬,欢迎狠狠的打我脸。

这份面试题有 43 道题,囊括了校招面试和社招面试,看完这一篇文章,保准你能和面试官侃侃而谈,增加进入大厂的几率!

话不多说,下面我们直接进入面试题。

操作系统简介篇

解释一下什么是操作系统

操作系统是管理硬件和软件的一种应用程序。操作系统是运行在计算机上最重要的一种软件,它管理计算机的资源和进程以及所有的硬件和软件。它为计算机硬件和软件提供了一种中间层,使应用软件和硬件进行分离,让我们无需关注硬件的实现,把关注点更多放在软件应用上。

通常情况下,计算机上会运行着许多应用程序,它们都需要对内存和 CPU 进行交互,操作系统的目的就是为了保证这些访问和交互能够准确无误的进行。

操作系统的主要功能

一般来说,现代操作系统主要提供下面几种功能

  • 进程管理: 进程管理的主要作用就是任务调度,在单核处理器下,操作系统会为每个进程分配一个任务,进程管理的工作十分简单;而在多核处理器下,操作系统除了要为进程分配任务外,还要解决处理器的调度、分配和回收等问题
  • 内存管理:内存管理主要是操作系统负责管理内存的分配、回收,在进程需要时分配内存以及在进程完成时回收内存,协调内存资源,通过合理的页面置换算法进行页面的换入换出
  • 设备管理:根据确定的设备分配原则对设备进行分配,使设备与主机能够并行工作,为用户提供良好的设备使用界面。
  • 文件管理:有效地管理文件的存储空间,合理地组织和管理文件系统,为文件访问和文件保护提供更有效的方法及手段。
  • 提供用户接口:操作系统提供了访问应用程序和硬件的接口,使用户能够通过应用程序发起系统调用从而操纵硬件,实现想要的功能。

软件访问硬件的几种方式

软件访问硬件其实就是一种 IO 操作,软件访问硬件的方式,也就是 I/O 操作的方式有哪些。

硬件在 I/O 上大致分为并行和串行,同时也对应串行接口和并行接口。

随着计算机技术的发展,I/O 控制方式也在不断发展。选择和衡量 I/O 控制方式有如下三条原则

(1) 数据传送速度足够快,能满足用户的需求但又不丢失数据;

(2) 系统开销小,所需的处理控制程序少;

(3) 能充分发挥硬件资源的能力,使 I/O 设备尽可能忙,而 CPU 等待时间尽可能少。

根据以上控制原则,I/O 操作可以分为四类

  • 直接访问:直接访问由用户进程直接控制主存或 CPU 和外围设备之间的信息传送。直接程序控制方式又称为忙/等待方式。
  • 中断驱动:为了减少程序直接控制方式下 CPU 的等待时间以及提高系统的并行程度,系统引入了中断机制。中断机制引入后,外围设备仅当操作正常结束或异常结束时才向 CPU 发出中断请求。在 I/O 设备输入每个数据的过程中,由于无需 CPU 的干预,一定程度上实现了 CPU 与 I/O 设备的并行工作。

上述两种方法的特点都是以 CPU 为中心,数据传送通过一段程序来实现,软件的传送手段限制了数据传送的速度。接下来介绍的这两种 I/O 控制方式采用硬件的方法来显示 I/O 的控制

  • DMA 直接内存访问:为了进一步减少 CPU 对 I/O 操作的干预,防止因并行操作设备过多使 CPU 来不及处理或因速度不匹配而造成的数据丢失现象,引入了 DMA 控制方式。
  • 通道控制方式:通道,独立于 CPU 的专门负责输入输出控制的处理机,它控制设备与内存直接进行数据交换。有自己的通道指令,这些指令由 CPU 启动,并在操作结束时向 CPU 发出中断信号。

解释一下操作系统的主要目的是什么

操作系统是一种软件,它的主要目的有三种

  • 管理计算机资源,这些资源包括 CPU、内存、磁盘驱动器、打印机等。
  • 提供一种图形界面,就像我们前面描述的那样,它提供了用户和计算机之间的桥梁。
  • 为其他软件提供服务,操作系统与软件进行交互,以便为其分配运行所需的任何必要资源。

操作系统的种类有哪些

操作系统通常预装在你购买计算机之前。大部分用户都会使用默认的操作系统,但是你也可以升级甚至更改操作系统。但是一般常见的操作系统只有三种:Windows、macOS 和 Linux。

为什么 Linux 系统下的应用程序不能直接在 Windows 下运行

这是一个老生常谈的问题了,在这里给出具体的回答。

其中一点是因为 Linux 系统和 Windows 系统的格式不同,格式就是协议,就是在固定位置有意义的数据。Linux 下的可执行程序文件格式是 elf,可以使用 readelf 命令查看 elf 文件头。

而 Windows 下的可执行程序是 PE 格式,它是一种可移植的可执行文件。

还有一点是因为 Linux 系统和 Windows 系统的 API 不同,这个 API 指的就是操作系统的 API,Linux 中的 API 被称为系统调用,是通过 int 0x80 这个软中断实现的。而 Windows 中的 API 是放在动态链接库文件中的,也就是 Windows 开发人员所说的 DLL ,这是一个库,里面包含代码和数据。Linux 中的可执行程序获得系统资源的方法和 Windows 不一样,所以显然是不能在 Windows 中运行的。

操作系统结构

单体系统

在大多数系统中,整个系统在内核态以单一程序的方式运行。整个操作系统是以程序集合来编写的,链接在一块形成一个大的二进制可执行程序,这种系统称为单体系统。

在单体系统中构造实际目标程序时,会首先编译所有单个过程(或包含这些过程的文件),然后使用系统链接器将它们全部绑定到一个可执行文件中

在单体系统中,对于每个系统调用都会有一个服务程序来保障和运行。需要一组实用程序来弥补服务程序需要的功能,例如从用户程序中获取数据。可将各种过程划分为一个三层模型

除了在计算机初启动时所装载的核心操作系统外,许多操作系统还支持额外的扩展。比如 I/O 设备驱动和文件系统。这些部件可以按需装载。在 UNIX 中把它们叫做 共享库(shared library),在 Windows 中则被称为 动态链接库(Dynamic Link Library,DLL)。他们的扩展名为 .dll,在 C:\Windows\system32 目录下存在 1000 多个 DLL 文件,所以不要轻易删除 C 盘文件,否则可能就炸了哦。

分层系统

分层系统使用层来分隔不同的功能单元。每一层只与该层的上层和下层通信。每一层都使用下面的层来执行其功能。层之间的通信通过预定义的固定接口通信。

微内核

为了实现高可靠性,将操作系统划分成小的、层级之间能够更好定义的模块是很有必要的,只有一个模块 — 微内核 — 运行在内核态,其余模块可以作为普通用户进程运行。由于把每个设备驱动和文件系统分别作为普通用户进程,这些模块中的错误虽然会使这些模块崩溃,但是不会使整个系统死机。

MINIX 3 是微内核的代表作,它的具体结构如下

在内核的外部,系统的构造有三层,它们都在用户态下运行,最底层是设备驱动器。由于它们都在用户态下运行,所以不能物理的访问 I/O 端口空间,也不能直接发出 I/O 命令。相反,为了能够对 I/O 设备编程,驱动器构建一个结构,指明哪个参数值写到哪个 I/O 端口,并声称一个内核调用,这样就完成了一次调用过程。

客户-服务器模式

微内核思想的策略是把进程划分为两类:服务器,每个服务器用来提供服务;客户端,使用这些服务。这个模式就是所谓的 客户-服务器模式。

客户-服务器模式会有两种载体,一种情况是一台计算机既是客户又是服务器,在这种方式下,操作系统会有某种优化;但是普遍情况下是客户端和服务器在不同的机器上,它们通过局域网或广域网连接。

客户通过发送消息与服务器通信,客户端并不需要知道这些消息是在本地机器上处理,还是通过网络被送到远程机器上处理。对于客户端而言,这两种情形是一样的:都是发送请求并得到回应。

为什么称为陷入内核

如果把软件结构进行分层说明的话,应该是这个样子的,最外层是应用程序,里面是操作系统内核。

应用程序处于特权级 3,操作系统内核处于特权级 0 。如果用户程序想要访问操作系统资源时,会发起系统调用,陷入内核,这样 CPU 就进入了内核态,执行内核代码。至于为什么是陷入,我们看图,内核是一个凹陷的构造,有陷下去的感觉,所以称为陷入。

什么是用户态和内核态

用户态和内核态是操作系统的两种运行状态。

  • 内核态:处于内核态的 CPU 可以访问任意的数据,包括外围设备,比如网卡、硬盘等,处于内核态的 CPU 可以从一个程序切换到另外一个程序,并且占用 CPU 不会发生抢占情况,一般处于特权级 0 的状态我们称之为内核态。

  • 用户态:处于用户态的 CPU 只能受限的访问内存,并且不允许访问外围设备,用户态下的 CPU 不允许独占,也就是说 CPU 能够被其他程序获取。

那么为什么要有用户态和内核态呢?

这个主要是访问能力的限制的考量,计算机中有一些比较危险的操作,比如设置时钟、内存清理,这些都需要在内核态下完成,如果随意进行这些操作,那你的系统得崩溃多少次。

用户态和内核态是如何切换的?

所有的用户进程都是运行在用户态的,但是我们上面也说了,用户程序的访问能力有限,一些比较重要的比如从硬盘读取数据,从键盘获取数据的操作则是内核态才能做的事情,而这些数据却又对用户程序来说非常重要。所以就涉及到两种模式下的转换,即用户态 -> 内核态 -> 用户态,而唯一能够做这些操作的只有 系统调用,而能够执行系统调用的就只有 操作系统。

一般用户态 -> 内核态的转换我们都称之为 trap 进内核,也被称之为 陷阱指令(trap instruction)。

他们的工作流程如下:

  • 首先用户程序会调用 glibc 库,glibc 是一个标准库,同时也是一套核心库,库中定义了很多关键 API。
  • glibc 库知道针对不同体系结构调用系统调用的正确方法,它会根据体系结构应用程序的二进制接口设置用户进程传递的参数,来准备系统调用。
  • 然后,glibc 库调用软件中断指令(SWI) ,这个指令通过更新 CPSR 寄存器将模式改为超级用户模式,然后跳转到地址 0x08 处。
  • 到目前为止,整个过程仍处于用户态下,在执行 SWI 指令后,允许进程执行内核代码,MMU 现在允许内核虚拟内存访问
  • 从地址 0x08 开始,进程执行加载并跳转到中断处理程序,这个程序就是 ARM 中的 vector_swi()。
  • 在 vector_swi() 处,从 SWI 指令中提取系统调用号 SCNO,然后使用 SCNO 作为系统调用表 sys_call_table 的索引,调转到系统调用函数。
  • 执行系统调用完成后,将还原用户模式寄存器,然后再以用户模式执行。

什么是内核

在计算机中,内核是一个计算机程序,它是操作系统的核心,可以控制操作系统中所有的内容。内核通常是在 boot loader 装载程序之前加载的第一个程序。

这里还需要了解一下什么是 boot loader。

boot loader 又被称为引导加载程序,能够将计算机的操作系统放入内存中。在电源通电或者计算机重启时,BIOS 会执行一些初始测试,然后将控制权转移到引导加载程序所在的主引导记录(MBR) 。

什么是实时系统

实时操作系统对时间做出了严格的要求,实时操作系统分为两种:硬实时和软实时

硬实时操作系统规定某个动作必须在规定的时刻内完成或发生,比如汽车生产车间,焊接机器必须在某一时刻内完成焊接,焊接的太早或者太晚都会对汽车造成永久性伤害。

软实时操作系统虽然不希望偶尔违反最终的时限要求,但是仍然可以接受。并且不会引起任何永久性伤害。比如数字音频、多媒体、手机都是属于软实时操作系统。

你可以简单理解硬实时和软实时的两个指标:是否在时刻内必须完成以及是否造成严重损害。

Linux 操作系统的启动过程

当计算机电源通电后,BIOS会进行开机自检(Power-On-Self-Test, POST),对硬件进行检测和初始化。因为操作系统的启动会使用到磁盘、屏幕、键盘、鼠标等设备。下一步,磁盘中的第一个分区,也被称为 MBR(Master Boot Record) 主引导记录,被读入到一个固定的内存区域并执行。这个分区中有一个非常小的,只有 512 字节的程序。程序从磁盘中调入 boot 独立程序,boot 程序将自身复制到高位地址的内存从而为操作系统释放低位地址的内存。

复制完成后,boot 程序读取启动设备的根目录。boot 程序要理解文件系统和目录格式。然后 boot 程序被调入内核,把控制权移交给内核。直到这里,boot 完成了它的工作。系统内核开始运行。

内核启动代码是使用汇编语言完成的,主要包括创建内核堆栈、识别 CPU 类型、计算内存、禁用中断、启动内存管理单元等,然后调用 C 语言的 main 函数执行操作系统部分。

这部分也会做很多事情,首先会分配一个消息缓冲区来存放调试出现的问题,调试信息会写入缓冲区。如果调试出现错误,这些信息可以通过诊断程序调出来。

然后操作系统会进行自动配置,检测设备,加载配置文件,被检测设备如果做出响应,就会被添加到已链接的设备表中,如果没有相应,就归为未连接直接忽略。

配置完所有硬件后,接下来要做的就是仔细手工处理进程0,设置其堆栈,然后运行它,执行初始化、配置时钟、挂载文件系统。创建 init 进程(进程 1 ) 和 守护进程(进程 2)。

init 进程会检测它的标志以确定它是否为单用户还是多用户服务。在前一种情况中,它会调用 fork 函数创建一个 shell 进程,并且等待这个进程结束。后一种情况调用 fork 函数创建一个运行系统初始化的 shell 脚本(即 /etc/rc)的进程,这个进程可以进行文件系统一致性检测、挂载文件系统、开启守护进程等。

然后 /etc/rc 这个进程会从 /etc/ttys 中读取数据,/etc/ttys 列出了所有的终端和属性。对于每一个启用的终端,这个进程调用 fork 函数创建一个自身的副本,进行内部处理并运行一个名为 getty 的程序。

getty 程序会在终端上输入

login:

等待用户输入用户名,在输入用户名后,getty 程序结束,登陆程序 /bin/login 开始运行。login 程序需要输入密码,并与保存在 /etc/passwd 中的密码进行对比,如果输入正确,login 程序以用户 shell 程序替换自身,等待第一个命令。如果不正确,login 程序要求输入另一个用户名。

整个系统启动过程如下

进程和线程篇

多处理系统的优势

随着处理器的不断增加,我们的计算机系统由单机系统变为了多处理系统,多处理系统的吞吐量比较高,多处理系统拥有多个并行的处理器,这些处理器共享时钟、内存、总线、外围设备等。

多处理系统由于可以共享资源,因此可以开源节流,省钱。整个系统的可靠性也随之提高。

什么是进程和进程表

进程就是正在执行程序的实例,比如说 Web 程序就是一个进程,shell 也是一个进程,文章编辑器 typora 也是一个进程。

操作系统负责管理所有正在运行的进程,操作系统会为每个进程分配特定的时间来占用 CPU,操作系统还会为每个进程分配特定的资源。

操作系统为了跟踪每个进程的活动状态,维护了一个进程表。在进程表的内部,列出了每个进程的状态以及每个进程使用的资源等。

什么是线程,线程和进程的区别

这又是一道老生常谈的问题了,从操作系统的角度来回答一下吧。

我们上面说到进程是正在运行的程序的实例,而线程其实就是进程中的单条流向,因为线程具有进程中的某些属性,所以线程又被称为轻量级的进程。浏览器如果是一个进程的话,那么浏览器下面的每个 tab 页可以看作是一个个的线程。

下面是线程和进程持有资源的区别

线程不像进程那样具有很强的独立性,线程之间会共享数据

创建线程的开销要比进程小很多,因为创建线程仅仅需要堆栈指针和程序计数器就可以了,而创建进程需要操作系统分配新的地址空间,数据资源等,这个开销比较大。

什么是上下文切换

对于单核单线程 CPU 而言,在某一时刻只能执行一条 CPU 指令。上下文切换 (Context Switch) 是一种 将 CPU 资源从一个进程分配给另一个进程的机制。从用户角度看,计算机能够并行运行多个进程,这恰恰是操作系统通过快速上下文切换造成的结果。在切换的过程中,操作系统需要先存储当前进程的状态 (包括内存空间的指针,当前执行完的指令等等),再读入下一个进程的状态,然后执行此进程。

使用多线程的好处是什么

多线程是程序员不得不知的基本素养之一,所以,下面我们给出一些多线程编程的好处

  • 能够提高对用户的响应顺序
  • 在流程中的资源共享
  • 比较经济适用
  • 能够对多线程架构有深入的理解

进程终止的方式

进程的终止

进程在创建之后,它就开始运行并做完成任务。然而,没有什么事儿是永不停歇的,包括进程也一样。进程早晚会发生终止,但是通常是由于以下情况触发的

  • 正常退出(自愿的)
  • 错误退出(自愿的)
  • 严重错误(非自愿的)
  • 被其他进程杀死(非自愿的)

正常退出

多数进程是由于完成了工作而终止。当编译器完成了所给定程序的编译之后,编译器会执行一个系统调用告诉操作系统它完成了工作。这个调用在 UNIX 中是 exit ,在 Windows 中是 ExitProcess。面向屏幕中的软件也支持自愿终止操作。字处理软件、Internet 浏览器和类似的程序中总有一个供用户点击的图标或菜单项,用来通知进程删除它锁打开的任何临时文件,然后终止。

错误退出

进程发生终止的第二个原因是发现严重错误,例如,如果用户执行如下命令

cc foo.c

为了能够编译 foo.c 但是该文件不存在,于是编译器就会发出声明并退出。在给出了错误参数时,面向屏幕的交互式进程通常并不会直接退出,因为这从用户的角度来说并不合理,用户需要知道发生了什么并想要进行重试,所以这时候应用程序通常会弹出一个对话框告知用户发生了系统错误,是需要重试还是退出。

严重错误

进程终止的第三个原因是由进程引起的错误,通常是由于程序中的错误所导致的。例如,执行了一条非法指令,引用不存在的内存,或者除数是 0 等。在有些系统比如 UNIX 中,进程可以通知操作系统,它希望自行处理某种类型的错误,在这类错误中,进程会收到信号(中断),而不是在这类错误出现时直接终止进程。

被其他进程杀死

第四个终止进程的原因是,某个进程执行系统调用告诉操作系统杀死某个进程。在 UNIX 中,这个系统调用是 kill。在 Win32 中对应的函数是 TerminateProcess(注意不是系统调用)。

进程间的通信方式

进程间的通信方式比较多,首先你需要理解下面这几个概念

  • 竞态条件:即两个或多个线程同时对一共享数据进行修改,从而影响程序运行的正确性时,这种就被称为竞态条件(race condition)。

  • 临界区:不仅共享资源会造成竞态条件,事实上共享文件、共享内存也会造成竞态条件、那么该如何避免呢?或许一句话可以概括说明:禁止一个或多个进程在同一时刻对共享资源(包括共享内存、共享文件等)进行读写。换句话说,我们需要一种 互斥(mutual exclusion) 条件,这也就是说,如果一个进程在某种方式下使用共享变量和文件的话,除该进程之外的其他进程就禁止做这种事(访问统一资源)。

    一个好的解决方案,应该包含下面四种条件

    1. 任何时候两个进程不能同时处于临界区
    2. 不应对 CPU 的速度和数量做任何假设
    3. 位于临界区外的进程不得阻塞其他进程
    4. 不能使任何进程无限等待进入临界区

  • 忙等互斥:当一个进程在对资源进行修改时,其他进程必须进行等待,进程之间要具有互斥性,我们讨论的解决方案其实都是基于忙等互斥提出的。

进程间的通信用专业一点的术语来表示就是 Inter Process Communication,IPC,它主要有下面 7。种通信方式

  • 消息传递:消息传递是进程间实现通信和同步等待的机制,使用消息传递,进程间的交流不需要共享变量,直接就可以进行通信;消息传递分为发送方和接收方
  • 先进先出队列:先进先出队列指的是两个不相关联进程间的通信,两个进程之间可以彼此相互进程通信,这是一种全双工通信方式
  • 管道:管道用于两个相关进程之间的通信,这是一种半双工的通信方式,如果需要全双工,需要另外一个管道。
  • 直接通信:在这种进程通信的方式中,进程与进程之间只存在一条链接,进程间要明确通信双方的命名。
  • 间接通信:间接通信是通信双方不会直接建立连接,而是找到一个中介者,这个中介者可能是个对象等等,进程可以在其中放置消息,并且可以从中删除消息,以此达到进程间通信的目的。
  • 消息队列:消息队列是内核中存储消息的链表,它由消息队列标识符进行标识,这种方式能够在不同的进程之间提供全双工的通信连接。
  • 共享内存:共享内存是使用所有进程之间的内存来建立连接,这种类型需要同步进程访问来相互保护。

进程间状态模型

进程的三态模型

当一个进程开始运行时,它可能会经历下面这几种状态

图中会涉及三种状态

  1. 运行态:运行态指的就是进程实际占用 CPU 时间片运行时
  2. 就绪态:就绪态指的是可运行,但因为其他进程正在运行而处于就绪状态
  3. 阻塞态:阻塞态又被称为睡眠态,它指的是进程不具备运行条件,正在等待被 CPU 调度。

逻辑上来说,运行态和就绪态是很相似的。这两种情况下都表示进程可运行,但是第二种情况没有获得 CPU 时间分片。第三种状态与前两种状态不同的原因是这个进程不能运行,CPU 空闲时也不能运行。

三种状态会涉及四种状态间的切换,在操作系统发现进程不能继续执行时会发生状态1的轮转,在某些系统中进程执行系统调用,例如 pause,来获取一个阻塞的状态。在其他系统中包括 UNIX,当进程从管道或特殊文件(例如终端)中读取没有可用的输入时,该进程会被自动终止。

转换 2 和转换 3 都是由进程调度程序(操作系统的一部分)引起的,进程本身不知道调度程序的存在。转换 2 的出现说明进程调度器认定当前进程已经运行了足够长的时间,是时候让其他进程运行 CPU 时间片了。当所有其他进程都运行过后,这时候该是让第一个进程重新获得 CPU 时间片的时候了,就会发生转换 3。

程序调度指的是,决定哪个进程优先被运行和运行多久,这是很重要的一点。已经设计出许多算法来尝试平衡系统整体效率与各个流程之间的竞争需求。

当进程等待的一个外部事件发生时(如从外部输入一些数据后),则发生转换 4。如果此时没有其他进程在运行,则立刻触发转换 3,该进程便开始运行,否则该进程会处于就绪阶段,等待 CPU 空闲后再轮到它运行。

进程的五态模型

在三态模型的基础上,增加了两个状态,即 新建 和 终止 状态。

  • 新建态:进程的新建态就是进程刚创建出来的时候

创建进程需要两个步骤:即为新进程分配所需要的资源和空间,设置进程为就绪态,并等待调度执行。

  • 终止态:进程的终止态就是指进程执行完毕,到达结束点,或者因为错误而不得不中止进程。

终止一个进程需要两个步骤:

  1. 先等待操作系统或相关的进程进行善后处理。

  2. 然后回收占用的资源并被系统删除。

调度算法都有哪些

调度算法分为三大类:批处理中的调度、交互系统中的调度、实时系统中的调度

批处理中的调度

先来先服务

很像是先到先得。。。可能最简单的非抢占式调度算法的设计就是 先来先服务(first-come,first-serverd)。使用此算法,将按照请求顺序为进程分配 CPU。最基本的,会有一个就绪进程的等待队列。当第一个任务从外部进入系统时,将会立即启动并允许运行任意长的时间。它不会因为运行时间太长而中断。当其他作业进入时,它们排到就绪队列尾部。当正在运行的进程阻塞,处于等待队列的第一个进程就开始运行。当一个阻塞的进程重新处于就绪态时,它会像一个新到达的任务,会排在队列的末尾,即排在所有进程最后。

这个算法的强大之处在于易于理解和编程,在这个算法中,一个单链表记录了所有就绪进程。要选取一个进程运行,只要从该队列的头部移走一个进程即可;要添加一个新的作业或者阻塞一个进程,只要把这个作业或进程附加在队列的末尾即可。这是很简单的一种实现。

不过,先来先服务也是有缺点的,那就是没有优先级的关系,试想一下,如果有 100 个 I/O 进程正在排队,第 101 个是一个 CPU 密集型进程,那岂不是需要等 100 个 I/O 进程运行完毕才会等到一个 CPU 密集型进程运行,这在实际情况下根本不可能,所以需要优先级或者抢占式进程的出现来优先选择重要的进程运行。

最短作业优先

批处理中,第二种调度算法是 最短作业优先(Shortest Job First),我们假设运行时间已知。例如,一家保险公司,因为每天要做类似的工作,所以人们可以相当精确地预测处理 1000 个索赔的一批作业需要多长时间。当输入队列中有若干个同等重要的作业被启动时,调度程序应使用最短优先作业算法

如上图 a 所示,这里有 4 个作业 A、B、C、D ,运行时间分别为 8、4、4、4 分钟。若按图中的次序运行,则 A 的周转时间为 8 分钟,B 为 12 分钟,C 为 16 分钟,D 为 20 分钟,平均时间内为 14 分钟。

现在考虑使用最短作业优先算法运行 4 个作业,如上图 b 所示,目前的周转时间分别为 4、8、12、20,平均为 11 分钟,可以证明最短作业优先是最优的。考虑有 4 个作业的情况,其运行时间分别为 a、b、c、d。第一个作业在时间 a 结束,第二个在时间 a + b 结束,以此类推。平均周转时间为 (4a + 3b + 2c + d) / 4 。显然 a 对平均值的影响最大,所以 a 应该是最短优先作业,其次是 b,然后是 c ,最后是 d 它就只能影响自己的周转时间了。

需要注意的是,在所有的进程都可以运行的情况下,最短作业优先的算法才是最优的。

最短剩余时间优先

最短作业优先的抢占式版本被称作为 最短剩余时间优先(Shortest Remaining Time Next) 算法。使用这个算法,调度程序总是选择剩余运行时间最短的那个进程运行。当一个新作业到达时,其整个时间同当前进程的剩余时间做比较。如果新的进程比当前运行进程需要更少的时间,当前进程就被挂起,而运行新的进程。这种方式能够使短期作业获得良好的服务。

交互式系统中的调度

交互式系统中在个人计算机、服务器和其他系统中都是很常用的,所以有必要来探讨一下交互式调度

轮询调度

一种最古老、最简单、最公平并且最广泛使用的算法就是 轮询算法(round-robin)。每个进程都会被分配一个时间段,称为时间片(quantum),在这个时间片内允许进程运行。如果时间片结束时进程还在运行的话,则抢占一个 CPU 并将其分配给另一个进程。如果进程在时间片结束前阻塞或结束,则 CPU 立即进行切换。轮询算法比较容易实现。调度程序所做的就是维护一个可运行进程的列表,就像下图中的 a,当一个进程用完时间片后就被移到队列的末尾,就像下图的 b。

优先级调度

事实情况是不是所有的进程都是优先级相等的。例如,在一所大学中的等级制度,首先是院长,然后是教授、秘书、后勤人员,最后是学生。这种将外部情况考虑在内就实现了优先级调度(priority scheduling)

它的基本思想很明确,每个进程都被赋予一个优先级,优先级高的进程优先运行。

但是也不意味着高优先级的进程能够永远一直运行下去,调度程序会在每个时钟中断期间降低当前运行进程的优先级。如果此操作导致其优先级降低到下一个最高进程的优先级以下,则会发生进程切换。或者,可以为每个进程分配允许运行的最大时间间隔。当时间间隔用完后,下一个高优先级的进程会得到运行的机会。

最短进程优先

对于批处理系统而言,由于最短作业优先常常伴随着最短响应时间,一种方式是根据进程过去的行为进行推测,并执行估计运行时间最短的那一个。假设每个终端上每条命令的预估运行时间为 T0,现在假设测量到其下一次运行时间为 T1,可以用两个值的加权来改进估计时间,即aT0+ (1- 1)T1。通过选择 a 的值,可以决定是尽快忘掉老的运行时间,还是在一段长时间内始终记住它们。当 a = 1/2 时,可以得到下面这个序列

可以看到,在三轮过后,T0 在新的估计值中所占比重下降至 1/8。

有时把这种通过当前测量值和先前估计值进行加权平均从而得到下一个估计值的技术称作 老化(aging)。这种方法会使用很多预测值基于当前值的情况。

彩票调度

有一种既可以给出预测结果而又有一种比较简单的实现方式的算法,就是 彩票调度(lottery scheduling)算法。他的基本思想为进程提供各种系统资源的彩票。当做出一个调度决策的时候,就随机抽出一张彩票,拥有彩票的进程将获得资源。比如在 CPU 进行调度时,系统可以每秒持有 50 次抽奖,每个中奖进程会获得额外运行时间的奖励。

可以把彩票理解为 buff,这个 buff 有 15% 的几率能让你产生 速度之靴 的效果。

公平分享调度

如果用户 1 启动了 9 个进程,而用户 2 启动了一个进程,使用轮转或相同优先级调度算法,那么用户 1 将得到 90 % 的 CPU 时间,而用户 2 将之得到 10 % 的 CPU 时间。

为了阻止这种情况的出现,一些系统在调度前会把进程的拥有者考虑在内。在这种模型下,每个用户都会分配一些CPU 时间,而调度程序会选择进程并强制执行。因此如果两个用户每个都会有 50% 的 CPU 时间片保证,那么无论一个用户有多少个进程,都将获得相同的 CPU 份额。

影响调度程序的指标是什么

会有下面几个因素决定调度程序的好坏

  • CPU 使用率:

CPU 正在执行任务(即不处于空闲状态)的时间百分比。

  • 等待时间

这是进程轮流执行的时间,也就是进程切换的时间

  • 吞吐量

单位时间内完成进程的数量

  • 响应时间

这是从提交流程到获得有用输出所经过的时间。

  • 周转时间

从提交流程到完成流程所经过的时间。

什么是 RR 调度算法

RR(round-robin) 调度算法主要针对分时系统,RR 的调度算法会把时间片以相同的部分并循环的分配给每个进程,RR 调度算法没有优先级的概念。这种算法的实现比较简单,而且每个线程都会占有时间片,并不存在线程饥饿的问题。

内存管理篇

什么是按需分页

在操作系统中,进程是以页为单位加载到内存中的,按需分页是一种虚拟内存的管理方式。在使用请求分页的系统中,只有在尝试访问页面所在的磁盘并且该页面尚未在内存中时,也就发生了缺页异常,操作系统才会将磁盘页面复制到内存中。

什么是虚拟内存

虚拟内存是一种内存分配方案,是一项可以用来辅助内存分配的机制。我们知道,应用程序是按页装载进内存中的。但并不是所有的页都会装载到内存中,计算机中的硬件和软件会将数据从 RAM 临时传输到磁盘中来弥补内存的不足。如果没有虚拟内存的话,一旦你将计算机内存填满后,计算机会对你说

呃,不,对不起,您无法再加载任何应用程序,请关闭另一个应用程序以加载新的应用程序。对于虚拟内存,计算机可以执行操作是查看内存中最近未使用过的区域,然后将其复制到硬盘上。虚拟内存通过复制技术实现了 妹子,你快来看哥哥能装这么多程序 的资本。复制是自动进行的,你无法感知到它的存在。

虚拟内存的实现方式

虚拟内存中,允许将一个作业分多次调入内存。釆用连续分配方式时,会使相当一部分内存空间都处于暂时或永久的空闲状态,造成内存资源的严重浪费,而且也无法从逻辑上扩大内存容量。因此,虚拟内存的实需要建立在离散分配的内存管理方式的基础上。虚拟内存的实现有以下三种方式:

  • 请求分页存储管理。
  • 请求分段存储管理。
  • 请求段页式存储管理。

不管哪种方式,都需要有一定的硬件支持。一般需要的支持有以下几个方面:

  • 一定容量的内存和外存。
  • 页表机制(或段表机制),作为主要的数据结构。
  • 中断机构,当用户程序要访问的部分尚未调入内存,则产生中断。
  • 地址变换机构,逻辑地址到物理地址的变换。

内存为什么要分段

内存是随机访问设备,对于内存来说,不需要从头开始查找,只需要直接给出地址即可。内存的分段是从 8086 CPU 开始的,8086 的 CPU 还是 16 位的寄存器宽,16 位的寄存器可以存储的数字范围是 2 的 16 次方,即 64 KB,8086 的 CPU 还没有 虚拟地址,只有物理地址,也就是说,如果两个相同的程序编译出来的地址相同,那么这两个程序是无法同时运行的。为了解决这个问题,操作系统设计人员提出了让 CPU 使用 段基址 + 段内偏移 的方式来访问任意内存。这样的好处是让程序可以 重定位,这也是内存为什么要分段的第一个原因。

那么什么是重定位呢?

简单来说就是将程序中的指令地址改为另一个地址,地址处存储的内容还是原来的。

CPU 采用段基址 + 段内偏移地址的形式访问内存,就需要提供专门的寄存器,这些专门的寄存器就是 CS、DS、ES 等,如果你对寄存器不熟悉,可以看我的这一篇文章。

爱了爱了,这篇寄存器讲的有点意思

也就是说,程序中需要用到哪块内存,就需要先加载合适的段到段基址寄存器中,再给出相对于该段基址的段偏移地址即可。CPU 中的地址加法器会将这两个地址进行合并,从地址总线送入内存。

8086 的 CPU 有 20 根地址总线,最大的寻址能力是 1MB,而段基址所在的寄存器宽度只有 16 位,最大为你 64 KB 的寻址能力,64 KB 显然不能满足 1MB 的最大寻址范围,所以就要把内存分段,每个段的最大寻址能力是 64KB,但是仍旧不能达到最大 1 MB 的寻址能力,所以这时候就需要 偏移地址的辅助,偏移地址也存入寄存器,同样为 64 KB 的寻址能力,这么一看还是不能满足 1MB 的寻址,所以 CPU 的设计者对地址单元动了手脚,将段基址左移 4 位,然后再和 16 位的段内偏移地址相加,就达到了 1MB 的寻址能力。所以内存分段的第二个目的就是能够访问到所有内存。

物理地址、逻辑地址、有效地址、线性地址、虚拟地址的区别

物理地址就是内存中真正的地址,它就相当于是你家的门牌号,你家就肯定有这个门牌号,具有唯一性。不管哪种地址,最终都会映射为物理地址。

在实模式下,段基址 + 段内偏移经过地址加法器的处理,经过地址总线传输,最终也会转换为物理地址。

但是在保护模式下,段基址 + 段内偏移被称为线性地址,不过此时的段基址不能称为真正的地址,而是会被称作为一个选择子的东西,选择子就是个索引,相当于数组的下标,通过这个索引能够在 GDT 中找到相应的段描述符,段描述符记录了段的起始、段的大小等信息,这样便得到了基地址。如果此时没有开启内存分页功能,那么这个线性地址可以直接当做物理地址来使用,直接访问内存。如果开启了分页功能,那么这个线性地址又多了一个名字,这个名字就是虚拟地址。

不论在实模式还是保护模式下,段内偏移地址都叫做有效地址。有效抵制也是逻辑地址。

线性地址可以看作是虚拟地址,虚拟地址不是真正的物理地址,但是虚拟地址会最终被映射为物理地址。下面是虚拟地址 -> 物理地址的映射。

空闲内存管理的方式

操作系统在动态分配内存时(malloc,new),需要对空间内存进行管理。一般采用了两种方式:位图和空闲链表。

使用位图进行管理

使用位图方法时,内存可能被划分为小到几个字或大到几千字节的分配单元。每个分配单元对应于位图中的一位,0 表示空闲, 1 表示占用(或者相反)。一块内存区域和其对应的位图如下

图 a 表示一段有 5 个进程和 3 个空闲区的内存,刻度为内存分配单元,阴影区表示空闲(在位图中用 0 表示);图 b 表示对应的位图;图 c 表示用链表表示同样的信息

分配单元的大小是一个重要的设计因素,分配单位越小,位图越大。然而,即使只有 4 字节的分配单元,32 位的内存也仅仅只需要位图中的 1 位。32n 位的内存需要 n 位的位图,所以1 个位图只占用了 1/32 的内存。如果选择更大的内存单元,位图应该要更小。如果进程的大小不是分配单元的整数倍,那么在最后一个分配单元中会有大量的内存被浪费。

位图提供了一种简单的方法在固定大小的内存中跟踪内存的使用情况,因为位图的大小取决于内存和分配单元的大小。这种方法有一个问题,当决定为把具有 k 个分配单元的进程放入内存时,内容管理器(memory manager) 必须搜索位图,在位图中找出能够运行 k 个连续 0 位的串。在位图中找出制定长度的连续 0 串是一个很耗时的操作,这是位图的缺点。(可以简单理解为在杂乱无章的数组中,找出具有一大长串空闲的数组单元)

使用空闲链表

另一种记录内存使用情况的方法是,维护一个记录已分配内存段和空闲内存段的链表,段会包含进程或者是两个进程的空闲区域。可用上面的图 c 来表示内存的使用情况。链表中的每一项都可以代表一个 空闲区(H) 或者是进程(P)的起始标志,长度和下一个链表项的位置。

在这个例子中,段链表(segment list)是按照地址排序的。这种方式的优点是,当进程终止或被交换时,更新列表很简单。一个终止进程通常有两个邻居(除了内存的顶部和底部外)。相邻的可能是进程也可能是空闲区,它们有四种组合方式。

当按照地址顺序在链表中存放进程和空闲区时,有几种算法可以为创建的进程(或者从磁盘中换入的进程)分配内存。

  • 首次适配算法:在链表中进行搜索,直到找到最初的一个足够大的空闲区,将其分配。除非进程大小和空间区大小恰好相同,否则会将空闲区分为两部分,一部分为进程使用,一部分成为新的空闲区。该方法是速度很快的算法,因为索引链表结点的个数较少。
  • 下次适配算法:工作方式与首次适配算法相同,但每次找到新的空闲区位置后都记录当前位置,下次寻找空闲区从上次结束的地方开始搜索,而不是与首次适配放一样从头开始;
  • 最佳适配算法:搜索整个链表,找出能够容纳进程分配的最小的空闲区。这样存在的问题是,尽管可以保证为进程找到一个最为合适的空闲区进行分配,但大多数情况下,这样的空闲区被分为两部分,一部分用于进程分配,一部分会生成很小的空闲区,而这样的空闲区很难再被进行利用。
  • 最差适配算法:与最佳适配算法相反,每次分配搜索最大的空闲区进行分配,从而可以使得空闲区拆分得到的新的空闲区可以更好的被进行利用。

页面置换算法都有哪些

在地址映射过程中,如果在页面中发现所要访问的页面不在内存中,那么就会产生一条缺页中断。当发生缺页中断时,如果操作系统内存中没有空闲页面,那么操作系统必须在内存选择一个页面将其移出内存,以便为即将调入的页面让出空间。而用来选择淘汰哪一页的规则叫做页面置换算法。

下面我汇总的这些页面置换算法比较齐全,只给出简单介绍,算法具体的实现和原理读者可以自行了解。

  • 最优算法在当前页面中置换最后要访问的页面。不幸的是,没有办法来判定哪个页面是最后一个要访问的,因此实际上该算法不能使用。然而,它可以作为衡量其他算法的标准。
  • NRU 算法根据 R 位和 M 位的状态将页面氛围四类。从编号最小的类别中随机选择一个页面。NRU 算法易于实现,但是性能不是很好。存在更好的算法。
  • FIFO 会跟踪页面加载进入内存中的顺序,并把页面放入一个链表中。有可能删除存在时间最长但是还在使用的页面,因此这个算法也不是一个很好的选择。
  • 第二次机会算法是对 FIFO 的一个修改,它会在删除页面之前检查这个页面是否仍在使用。如果页面正在使用,就会进行保留。这个改进大大提高了性能。
  • 时钟 算法是第二次机会算法的另外一种实现形式,时钟算法和第二次算法的性能差不多,但是会花费更少的时间来执行算法。
  • LRU 算法是一个非常优秀的算法,但是没有特殊的硬件(TLB)很难实现。如果没有硬件,就不能使用 LRU 算法。
  • NFU 算法是一种近似于 LRU 的算法,它的性能不是非常好。
  • 老化 算法是一种更接近 LRU 算法的实现,并且可以更好的实现,因此是一个很好的选择
  • 最后两种算法都使用了工作集算法。工作集算法提供了合理的性能开销,但是它的实现比较复杂。WSClock 是另外一种变体,它不仅能够提供良好的性能,而且可以高效地实现。

最好的算法是老化算法和WSClock算法。他们分别是基于 LRU 和工作集算法。他们都具有良好的性能并且能够被有效的实现。还存在其他一些好的算法,但实际上这两个可能是最重要的。

文件系统篇

提高文件系统性能的方式

访问磁盘的效率要比内存慢很多,是时候又祭出这张图了

所以磁盘优化是很有必要的,下面我们会讨论几种优化方式

高速缓存

最常用的减少磁盘访问次数的技术是使用 块高速缓存(block cache) 或者 缓冲区高速缓存(buffer cache)。高速缓存指的是一系列的块,它们在逻辑上属于磁盘,但实际上基于性能的考虑被保存在内存中。

管理高速缓存有不同的算法,常用的算法是:检查全部的读请求,查看在高速缓存中是否有所需要的块。如果存在,可执行读操作而无须访问磁盘。如果检查块不再高速缓存中,那么首先把它读入高速缓存,再复制到所需的地方。之后,对同一个块的请求都通过高速缓存来完成。

高速缓存的操作如下图所示

由于在高速缓存中有许多块,所以需要某种方法快速确定所需的块是否存在。常用方法是将设备和磁盘地址进行散列操作。然后在散列表中查找结果。具有相同散列值的块在一个链表中连接在一起(这个数据结构是不是很像 HashMap?),这样就可以沿着冲突链查找其他块。

如果高速缓存已满,此时需要调入新的块,则要把原来的某一块调出高速缓存,如果要调出的块在上次调入后已经被修改过,则需要把它写回磁盘。这种情况与分页非常相似。

块提前读

第二个明显提高文件系统的性能是在需要用到块之前试图提前将其写入高速缓存从而提高命中率。许多文件都是顺序读取。如果请求文件系统在某个文件中生成块 k,文件系统执行相关操作并且在完成之后,会检查高速缓存,以便确定块 k + 1 是否已经在高速缓存。如果不在,文件系统会为 k + 1 安排一个预读取,因为文件希望在用到该块的时候能够直接从高速缓存中读取。

当然,块提前读取策略只适用于实际顺序读取的文件。对随机访问的文件,提前读丝毫不起作用。甚至还会造成阻碍。

减少磁盘臂运动

高速缓存和块提前读并不是提高文件系统性能的唯一方法。另一种重要的技术是把有可能顺序访问的块放在一起,当然最好是在同一个柱面上,从而减少磁盘臂的移动次数。当写一个输出文件时,文件系统就必须按照要求一次一次地分配磁盘块。如果用位图来记录空闲块,并且整个位图在内存中,那么选择与前一块最近的空闲块是很容易的。如果用空闲表,并且链表的一部分存在磁盘上,要分配紧邻的空闲块就会困难很多。

不过,即使采用空闲表,也可以使用 块簇 技术。即不用块而用连续块簇来跟踪磁盘存储区。如果一个扇区有 512 个字节,有可能系统采用 1 KB 的块(2 个扇区),但却按每 2 块(4 个扇区)一个单位来分配磁盘存储区。这和 2 KB 的磁盘块并不相同,因为在高速缓存中它仍然使用 1 KB 的块,磁盘与内存数据之间传送也是以 1 KB 进行,但在一个空闲的系统上顺序读取这些文件,寻道的次数可以减少一半,从而使文件系统的性能大大改善。若考虑旋转定位则可以得到这类方法的变体。在分配块时,系统尽量把一个文件中的连续块存放在同一个柱面上。

在使用 inode 或任何类似 inode 的系统中,另一个性能瓶颈是,读取一个很短的文件也需要两次磁盘访问:一次是访问 inode,一次是访问块。通常情况下,inode 的放置如下图所示

其中,全部 inode 放在靠近磁盘开始位置,所以 inode 和它所指向的块之间的平均距离是柱面组的一半,这将会需要较长时间的寻道时间。

一个简单的改进方法是,在磁盘中部而不是开始处存放 inode ,此时,在 inode 和第一个块之间的寻道时间减为原来的一半。另一种做法是:将磁盘分成多个柱面组,每个柱面组有自己的 inode,数据块和空闲表,如上图 b 所示。

当然,只有在磁盘中装有磁盘臂的情况下,讨论寻道时间和旋转时间才是有意义的。现在越来越多的电脑使用 固态硬盘(SSD),对于这些硬盘,由于采用了和闪存同样的制造技术,使得随机访问和顺序访问在传输速度上已经较为相近,传统硬盘的许多问题就消失了。但是也引发了新的问题。

磁盘碎片整理

在初始安装操作系统后,文件就会被不断的创建和清除,于是磁盘会产生很多的碎片,在创建一个文件时,它使用的块会散布在整个磁盘上,降低性能。删除文件后,回收磁盘块,可能会造成空穴。

磁盘性能可以通过如下方式恢复:移动文件使它们相互挨着,并把所有的至少是大部分的空闲空间放在一个或多个大的连续区域内。Windows 有一个程序 defrag 就是做这个事儿的。Windows 用户会经常使用它,SSD 除外。

磁盘碎片整理程序会在让文件系统上很好地运行。Linux 文件系统(特别是 ext2 和 ext3)由于其选择磁盘块的方式,在磁盘碎片整理上一般不会像 Windows 一样困难,因此很少需要手动的磁盘碎片整理。而且,固态硬盘并不受磁盘碎片的影响,事实上,在固态硬盘上做磁盘碎片整理反倒是多此一举,不仅没有提高性能,反而磨损了固态硬盘。所以碎片整理只会缩短固态硬盘的寿命。

磁盘臂调度算法

一般情况下,影响磁盘快读写的时间由下面几个因素决定

  • 寻道时间 – 寻道时间指的就是将磁盘臂移动到需要读取磁盘块上的时间
  • 旋转延迟 – 等待合适的扇区旋转到磁头下所需的时间
  • 实际数据的读取或者写入时间

这三种时间参数也是磁盘寻道的过程。一般情况下,寻道时间对总时间的影响最大,所以,有效的降低寻道时间能够提高磁盘的读取速度。

如果磁盘驱动程序每次接收一个请求并按照接收顺序完成请求,这种处理方式也就是 先来先服务(First-Come, First-served, FCFS) ,这种方式很难优化寻道时间。因为每次都会按照顺序处理,不管顺序如何,有可能这次读完后需要等待一个磁盘旋转一周才能继续读取,而其他柱面能够马上进行读取,这种情况下每次请求也会排队。

通常情况下,磁盘在进行寻道时,其他进程会产生其他的磁盘请求。磁盘驱动程序会维护一张表,表中会记录着柱面号当作索引,每个柱面未完成的请求会形成链表,链表头存放在表的相应表项中。

一种对先来先服务的算法改良的方案是使用 最短路径优先(SSF) 算法,下面描述了这个算法。

假如我们在对磁道 6 号进行寻址时,同时发生了对 11 , 2 , 4, 14, 8, 15, 3 的请求,如果采用先来先服务的原则,如下图所示

我们可以计算一下磁盘臂所跨越的磁盘数量为 5 + 9 + 2 + 10 + 6 + 7 + 12 = 51,相当于是跨越了 51 次盘面,如果使用最短路径优先,我们来计算一下跨越的盘面

跨越的磁盘数量为 4 + 1 + 1 + 4 + 3 + 3 + 1 = 17 ,相比 51 足足省了两倍的时间。

但是,最短路径优先的算法也不是完美无缺的,这种算法照样存在问题,那就是优先级 问题,

这里有一个原型可以参考就是我们日常生活中的电梯,电梯使用一种电梯算法(elevator algorithm) 来进行调度,从而满足协调效率和公平性这两个相互冲突的目标。电梯一般会保持向一个方向移动,直到在那个方向上没有请求为止,然后改变方向。

电梯算法需要维护一个二进制位,也就是当前的方向位:UP(向上)或者是 DOWN(向下)。当一个请求处理完成后,磁盘或电梯的驱动程序会检查该位,如果此位是 UP 位,磁盘臂或者电梯仓移到下一个更高跌未完成的请求。如果高位没有未完成的请求,则取相反方向。当方向位是 DOWN 时,同时存在一个低位的请求,磁盘臂会转向该点。如果不存在的话,那么它只是停止并等待。

我们举个例子来描述一下电梯算法,比如各个柱面得到服务的顺序是 4,7,10,14,9,6,3,1 ,那么它的流程图如下

所以电梯算法需要跨越的盘面数量是 3 + 3 + 4 + 5 + 3 + 3 + 1 = 22

电梯算法通常情况下不如 SSF 算法。

一些磁盘控制器为软件提供了一种检查磁头下方当前扇区号的方法,使用这样的控制器,能够进行另一种优化。如果对一个相同的柱面有两个或者多个请求正等待处理,驱动程序可以发出请求读写下一次要通过磁头的扇区。

这里需要注意一点,当一个柱面有多条磁道时,相继的请求可能针对不同的磁道,这种选择没有代价,因为选择磁头不需要移动磁盘臂也没有旋转延迟。

对于磁盘来说,最影响性能的就是寻道时间和旋转延迟,所以一次只读取一个或两个扇区的效率是非常低的。出于这个原因,许多磁盘控制器总是读出多个扇区并进行高速缓存,即使只请求一个扇区时也是这样。一般情况下读取一个扇区的同时会读取该扇区所在的磁道或者是所有剩余的扇区被读出,读出扇区的数量取决于控制器的高速缓存中有多少可用的空间。

磁盘控制器的高速缓存和操作系统的高速缓存有一些不同,磁盘控制器的高速缓存用于缓存没有实际被请求的块,而操作系统维护的高速缓存由显示地读出的块组成,并且操作系统会认为这些块在近期仍然会频繁使用。

当同一个控制器上有多个驱动器时,操作系统应该为每个驱动器都单独的维护一个未完成的请求表。一旦有某个驱动器闲置时,就应该发出一个寻道请求来将磁盘臂移到下一个被请求的柱面。如果下一个寻道请求到来时恰好没有磁盘臂处于正确的位置,那么驱动程序会在刚刚完成传输的驱动器上发出一个新的寻道命令并等待,等待下一次中断到来时检查哪个驱动器处于闲置状态。

RAID 的不同级别

RAID 称为 磁盘冗余阵列,简称 磁盘阵列。利用虚拟化技术把多个硬盘结合在一起,成为一个或多个磁盘阵列组,目的是提升性能或数据冗余。

RAID 有不同的级别

  • RAID 0 – 无容错的条带化磁盘阵列
  • RAID 1 – 镜像和双工
  • RAID 2 – 内存式纠错码
  • RAID 3 – 比特交错奇偶校验
  • RAID 4 – 块交错奇偶校验
  • RAID 5 – 块交错分布式奇偶校验
  • RAID 6 – P + Q冗余

IO 篇

操作系统中的时钟是什么

时钟(Clocks) 也被称为定时器(timers),时钟/定时器对任何程序系统来说都是必不可少的。时钟负责维护时间、防止一个进程长期占用 CPU 时间等其他功能。时钟软件(clock software) 也是一种设备驱动的方式。下面我们就来对时钟进行介绍,一般都是先讨论硬件再介绍软件,采用由下到上的方式,也是告诉你,底层是最重要的。

时钟硬件

在计算机中有两种类型的时钟,这些时钟与现实生活中使用的时钟完全不一样。

  • 比较简单的一种时钟被连接到 110 V 或 220 V 的电源线上,这样每个电压周期会产生一个中断,大概是 50 – 60 HZ。这些时钟过去一直占据支配地位。
  • 另外的一种时钟由晶体振荡器、计数器和寄存器组成,示意图如下所示

这种时钟称为可编程时钟 ,可编程时钟有两种模式,一种是 一键式(one-shot mode),当时钟启动时,会把存储器中的值复制到计数器中,然后,每次晶体的振荡器的脉冲都会使计数器 -1。当计数器变为 0 时,会产生一个中断,并停止工作,直到软件再一次显示启动。还有一种模式时 方波(square-wave mode) 模式,在这种模式下,当计数器变为 0 并产生中断后,存储寄存器的值会自动复制到计数器中,这种周期性的中断称为一个时钟周期。

设备控制器的主要功能

设备控制器是一个可编址的设备,当它仅控制一个设备时,它只有一个唯一的设备地址;如果设备控制器控制多个可连接设备时,则应含有多个设备地址,并使每一个设备地址对应一个设备。

设备控制器主要分为两种:字符设备和块设备

设备控制器的主要功能有下面这些

  • 接收和识别命令:设备控制器可以接受来自 CPU 的指令,并进行识别。设备控制器内部也会有寄存器,用来存放指令和参数
  • 进行数据交换:CPU、控制器和设备之间会进行数据的交换,CPU 通过总线把指令发送给控制器,或从控制器中并行地读出数据;控制器将数据写入指定设备。
  • 地址识别:每个硬件设备都有自己的地址,设备控制器能够识别这些不同的地址,来达到控制硬件的目的,此外,为使 CPU 能向寄存器中写入或者读取数据,这些寄存器都应具有唯一的地址。
  • 差错检测:设备控制器还具有对设备传递过来的数据进行检测的功能。

中断处理过程

中断处理方案有很多种,下面是 《ARM System Developer’s Guide

Designing and Optimizing System Software》列出来的一些方案

  • 非嵌套的中断处理程序按照顺序处理各个中断,非嵌套的中断处理程序也是最简单的中断处理
  • 嵌套的中断处理程序会处理多个中断而无需分配优先级
  • 可重入的中断处理程序可使用优先级处理多个中断
  • 简单优先级中断处理程序可处理简单的中断
  • 标准优先级中断处理程序比低优先级的中断处理程序在更短的时间能够处理优先级更高的中断
  • 高优先级 中断处理程序在短时间能够处理优先级更高的任务,并直接进入特定的服务例程。
  • 优先级分组中断处理程序能够处理不同优先级的中断任务

下面是一些通用的中断处理程序的步骤,不同的操作系统实现细节不一样

  • 保存所有没有被中断硬件保存的寄存器
  • 为中断服务程序设置上下文环境,可能包括设置 TLB、MMU 和页表,如果不太了解这三个概念,请参考另外一篇文章
  • 为中断服务程序设置栈
  • 对中断控制器作出响应,如果不存在集中的中断控制器,则继续响应中断
  • 把寄存器从保存它的地方拷贝到进程表中
  • 运行中断服务程序,它会从发出中断的设备控制器的寄存器中提取信息
  • 操作系统会选择一个合适的进程来运行。如果中断造成了一些优先级更高的进程变为就绪态,则选择运行这些优先级高的进程
  • 为进程设置 MMU 上下文,可能也会需要 TLB,根据实际情况决定
  • 加载进程的寄存器,包括 PSW 寄存器
  • 开始运行新的进程

上面我们罗列了一些大致的中断步骤,不同性质的操作系统和中断处理程序能够处理的中断步骤和细节也不尽相同,下面是一个嵌套中断的具体运行步骤

什么是设备驱动程序

在计算机中,设备驱动程序是一种计算机程序,它能够控制或者操作连接到计算机的特定设备。驱动程序提供了与硬件进行交互的软件接口,使操作系统和其他计算机程序能够访问特定设备,不用需要了解其硬件的具体构造。

什么是 DMA

DMA 的中文名称是直接内存访问,它意味着 CPU 授予 I/O 模块权限在不涉及 CPU 的情况下读取或写入内存。也就是 DMA 可以不需要 CPU 的参与。这个过程由称为 DMA 控制器(DMAC)的芯片管理。由于 DMA 设备可以直接在内存之间传输数据,而不是使用 CPU 作为中介,因此可以缓解总线上的拥塞。DMA 通过允许 CPU 执行任务,同时 DMA 系统通过系统和内存总线传输数据来提高系统并发性。

直接内存访问的特点

DMA 方式有如下特点:

  • 数据传送以数据块为基本单位

  • 所传送的数据从设备直接送入主存,或者从主存直接输出到设备上

  • 仅在传送一个或多个数据块的开始和结束时才需 CPU 的干预,而整块数据的传送则是在控制器的控制下完成。

DMA 方式和中断驱动控制方式相比,减少了 CPU 对 I/O 操作的干预,进一步提高了 CPU 与 I/O 设备的并行操作程度。

DMA 方式的线路简单、价格低廉,适合高速设备与主存之间的成批数据传送,小型、微型机中的快速设备均采用这种方式,但其功能较差,不能满足复杂的 I/O 要求。

死锁篇

什么是僵尸进程

僵尸进程是已完成且处于终止状态,但在进程表中却仍然存在的进程。僵尸进程通常发生在父子关系的进程中,由于父进程仍需要读取其子进程的退出状态所造成的。

死锁产生的原因

死锁产生的原因大致有两个:资源竞争和程序执行顺序不当

死锁产生的必要条件

资源死锁可能出现的情况主要有

  • 互斥条件:每个资源都被分配给了一个进程或者资源是可用的
  • 保持和等待条件:已经获取资源的进程被认为能够获取新的资源
  • 不可抢占条件:分配给一个进程的资源不能强制的从其他进程抢占资源,它只能由占有它的进程显示释放
  • 循环等待:死锁发生时,系统中一定有两个或者两个以上的进程组成一个循环,循环中的每个进程都在等待下一个进程释放的资源。

死锁的恢复方式

所以针对检测出来的死锁,我们要对其进行恢复,下面我们会探讨几种死锁的恢复方式

通过抢占进行恢复

在某些情况下,可能会临时将某个资源从它的持有者转移到另一个进程。比如在不通知原进程的情况下,将某个资源从进程中强制取走给其他进程使用,使用完后又送回。这种恢复方式一般比较困难而且有些简单粗暴,并不可取。

通过回滚进行恢复

如果系统设计者和机器操作员知道有可能发生死锁,那么就可以定期检查流程。进程的检测点意味着进程的状态可以被写入到文件以便后面进行恢复。检测点不仅包含存储映像(memory image),还包含资源状态(resource state)。一种更有效的解决方式是不要覆盖原有的检测点,而是每出现一个检测点都要把它写入到文件中,这样当进程执行时,就会有一系列的检查点文件被累积起来。

为了进行恢复,要从上一个较早的检查点上开始,这样所需要资源的进程会回滚到上一个时间点,在这个时间点上,死锁进程还没有获取所需要的资源,可以在此时对其进行资源分配。

杀死进程恢复

最简单有效的解决方案是直接杀死一个死锁进程。但是杀死一个进程可能照样行不通,这时候就需要杀死别的资源进行恢复。

另外一种方式是选择一个环外的进程作为牺牲品来释放进程资源。

如何破坏死锁

和死锁产生的必要条件一样,如果要破坏死锁,也是从下面四种方式进行破坏。

破坏互斥条件

我们首先考虑的就是破坏互斥使用条件。如果资源不被一个进程独占,那么死锁肯定不会产生。如果两个打印机同时使用一个资源会造成混乱,打印机的解决方式是使用 假脱机打印机(spooling printer) ,这项技术可以允许多个进程同时产生输出,在这种模型中,实际请求打印机的唯一进程是打印机守护进程,也称为后台进程。后台进程不会请求其他资源。我们可以消除打印机的死锁。

后台进程通常被编写为能够输出完整的文件后才能打印,假如两个进程都占用了假脱机空间的一半,而这两个进程都没有完成全部的输出,就会导致死锁。

因此,尽量做到尽可能少的进程可以请求资源。

破坏保持等待的条件

第二种方式是如果我们能阻止持有资源的进程请求其他资源,我们就能够消除死锁。一种实现方式是让所有的进程开始执行前请求全部的资源。如果所需的资源可用,进程会完成资源的分配并运行到结束。如果有任何一个资源处于频繁分配的情况,那么没有分配到资源的进程就会等待。

很多进程无法在执行完成前就知道到底需要多少资源,如果知道的话,就可以使用银行家算法;还有一个问题是这样无法合理有效利用资源。

还有一种方式是进程在请求其他资源时,先释放所占用的资源,然后再尝试一次获取全部的资源。

破坏不可抢占条件

破坏不可抢占条件也是可以的。可以通过虚拟化的方式来避免这种情况。

破坏循环等待条件

现在就剩最后一个条件了,循环等待条件可以通过多种方法来破坏。一种方式是制定一个标准,一个进程在任何时候只能使用一种资源。如果需要另外一种资源,必须释放当前资源。

另一种方式是将所有的资源统一编号,如下图所示

进程可以在任何时间提出请求,但是所有的请求都必须按照资源的顺序提出。如果按照此分配规则的话,那么资源分配之间不会出现环。

死锁类型

两阶段加锁

虽然很多情况下死锁的避免和预防都能处理,但是效果并不好。随着时间的推移,提出了很多优秀的算法用来处理死锁。例如在数据库系统中,一个经常发生的操作是请求锁住一些记录,然后更新所有锁定的记录。当同时有多个进程运行时,就会有死锁的风险。

一种解决方式是使用 两阶段提交(two-phase locking)。顾名思义分为两个阶段,一阶段是进程尝试一次锁定它需要的所有记录。如果成功后,才会开始第二阶段,第二阶段是执行更新并释放锁。第一阶段并不做真正有意义的工作。

如果在第一阶段某个进程所需要的记录已经被加锁,那么该进程会释放所有锁定的记录并重新开始第一阶段。从某种意义上来说,这种方法类似于预先请求所有必需的资源或者是在进行一些不可逆的操作之前请求所有的资源。

不过在一般的应用场景中,两阶段加锁的策略并不通用。如果一个进程缺少资源就会半途中断并重新开始的方式是不可接受的。

通信死锁

我们上面一直讨论的是资源死锁,资源死锁是一种死锁类型,但并不是唯一类型,还有通信死锁,也就是两个或多个进程在发送消息时出现的死锁。进程 A 给进程 B 发了一条消息,然后进程 A 阻塞直到进程 B 返回响应。假设请求消息丢失了,那么进程 A 在一直等着回复,进程 B 也会阻塞等待请求消息到来,这时候就产生死锁。

尽管会产生死锁,但是这并不是一个资源死锁,因为 A 并没有占据 B 的资源。事实上,通信死锁并没有完全可见的资源。根据死锁的定义来说:每个进程因为等待其他进程引起的事件而产生阻塞,这就是一种死锁。相较于最常见的通信死锁,我们把上面这种情况称为通信死锁(communication deadlock)。

通信死锁不能通过调度的方式来避免,但是可以使用通信中一个非常重要的概念来避免:超时(timeout)。在通信过程中,只要一个信息被发出后,发送者就会启动一个定时器,定时器会记录消息的超时时间,如果超时时间到了但是消息还没有返回,就会认为消息已经丢失并重新发送,通过这种方式,可以避免通信死锁。

但是并非所有网络通信发生的死锁都是通信死锁,也存在资源死锁,下面就是一个典型的资源死锁。

当一个数据包从主机进入路由器时,会被放入一个缓冲区,然后再传输到另外一个路由器,再到另一个,以此类推直到目的地。缓冲区都是资源并且数量有限。如下图所示,每个路由器都有 10 个缓冲区(实际上有很多)。

假如路由器 A 的所有数据需要发送到 B ,B 的所有数据包需要发送到 D,然后 D 的所有数据包需要发送到 A 。没有数据包可以移动,因为在另一端没有缓冲区可用,这就是一个典型的资源死锁。

活锁

某些情况下,当进程意识到它不能获取所需要的下一个锁时,就会尝试礼貌的释放已经获得的锁,然后等待非常短的时间再次尝试获取。可以想像一下这个场景:当两个人在狭路相逢的时候,都想给对方让路,相同的步调会导致双方都无法前进。

现在假想有一对并行的进程用到了两个资源。它们分别尝试获取另一个锁失败后,两个进程都会释放自己持有的锁,再次进行尝试,这个过程会一直进行重复。很明显,这个过程中没有进程阻塞,但是进程仍然不会向下执行,这种状况我们称之为 活锁(livelock)。

饥饿

与死锁和活锁的一个非常相似的问题是 饥饿(starvvation)。想象一下你什么时候会饿?一段时间不吃东西是不是会饿?对于进程来讲,最重要的就是资源,如果一段时间没有获得资源,那么进程会产生饥饿,这些进程会永远得不到服务。

我们假设打印机的分配方案是每次都会分配给最小文件的进程,那么要打印大文件的进程会永远得不到服务,导致进程饥饿,进程会无限制的推后,虽然它没有阻塞。

后记

这篇文章到这里就结束了,后面我会继续写关于计算机网络、计算机基础、Java 相关、Java 架构相关的面试题。

如果这篇文章你觉得还不错的话,还希望可以点赞、在看、转发、留言,欢迎关注一下我的公众号【程序员cxuan】,这个号的干货简直太多了。

最后,你的支持是我继续肝文的动力。希望你能顺利进入大厂,加油!


# 关于操作系统,你必备的名词-Java面试题


  1. 操作系统(Operating System,OS):是管理计算机硬件与软件资源的系统软件,同时也是计算机系统的内核与基石。操作系统需要处理管理与配置内存、决定系统资源供需的优先次序、控制输入与输出设备、操作网络与管理文件系统等基本事务。操作系统也提供一个让用户与系统交互的操作界面。

    Yna28K.png

  2. shell:它是一个程序,可从键盘获取命令并将其提供给操作系统以执行。 在过去,它是类似 Unix 的系统上唯一可用的用户界面。 如今,除了命令行界面(CLI)外,我们还具有图形用户界面(··)。

    YnagC6.png

  3. GUI (Graphical User Interface):是一种用户界面,允许用户通过图形图标和音频指示符与电子设备进行交互。

    YnaRgO.png

  4. 内核模式(kernel mode): 通常也被称为 超级模式(supervisor mode),在内核模式下,正在执行的代码具有对底层硬件的完整且不受限制的访问。 它可以执行任何 CPU 指令并引用任何内存地址。 内核模式通常保留给操作系统的最低级别,最受信任的功能。 内核模式下的崩溃是灾难性的; 他们将停止整个计算机。 超级用户模式是计算机开机时选择的自动模式。

  5. 用户模式(user node):当操作系统运行用户应用程序(例如处理文本编辑器)时,系统处于用户模式。 当应用程序请求操作系统的帮助或发生中断或系统调用时,就会发生从用户模式到内核模式的转换。在用户模式下,模式位设置为1。 从用户模式切换到内核模式时,它从1更改为0。

  6. 计算机架构(computer architecture) : 在计算机工程中,计算机体系结构是描述计算机系统功能,组织和实现的一组规则和方法。它主要包括指令集、内存管理、I/O 和总线结构

YnayU1.png

  1. SATA(Serial ATA):串行 ATA (Serial Advanced Technology Attachment),它是一种电脑总线,负责主板和大容量存储设备(如硬盘及光盘驱动器)之间的数据传输,主要用于个人电脑。

  2. 复用(multiplexing):也称为共享,在操作系统中主要指示了时间和空间的管理。对资源进行复用时,不同的程序或用户轮流使用它。 他们中的第一个开始使用资源,然后再使用另一个,依此类推。

  3. 大型机(mainframes):大型机是一类计算机,通常以其大尺寸,存储量,处理能力和高度的可靠性而著称。它们主要由大型组织用于需要大量数据处理的关键任务应用程序。

    Yna4DH.png

  4. 批处理(batch system): 批处理操作系统的用户不直接与计算机进行交互。 每个用户都在打孔卡等脱机设备上准备工作,并将其提交给计算机操作员。 为了加快处理速度,将具有类似需求的作业一起批处理并成组运行。 程序员将程序留给操作员,然后操作员将具有类似要求的程序分批处理。

  5. OS/360: OS/360,正式称为IBM System / 360操作系统,是由 IBM 为 1964 年发布的其当时新的System/360 大型机开发的已停产的批处理操作系统。

  6. 多处理系统(Computer multitasking):是指计算机同时运行多个程序的能力。多任务的一般方法是运行第一个程序的一段代码,保存工作环境;再运行第二个程序的一段代码,保存环境;……恢复第一个程序的工作环境,执行第一个程序的下一段代码。

  7. 分时系统(Time-sharing):在计算中,分时是通过多程序和多任务同时在许多用户之间共享计算资源的一种系统

  8. 相容分时系统(Compatible Time-Sharing System):最早的分时操作系统,由美国麻省理工学院计算机中心设计与实作。

  9. 云计算(cloud computing):云计算是计算机系统资源(尤其是数据存储和计算能力)的按需可用性,而无需用户直接进行主动管理。这个术语通常用于描述 Internet 上可供许多用户使用的数据中心。 如今占主导地位的大型云通常具有从中央服务器分布在多个位置的功能。 如果与用户的连接相对较近,则可以将其指定为边缘服务器。

    YnaWvD.md.png

  10. UNIX 操作系统:UNIX 操作系统,是一个强大的多用户、多任务操作系统,支持多种处理器架构,按照操作系统的分类,属于分时操作系统。

  11. UNIX System V:是 UNIX 操作系统的一个分支。

  12. BSD(Berkeley Software Distribution):UNIX 的衍生系统。

  13. POSIX:可移植操作系统接口,是 IEEE 为要在各种 UNIX 操作系统上运行软件,而定义API的一系列互相关联的标准的总称。

  14. MINIX:Minix,是一个迷你版本的类 UNIX 操作系统。

  15. Linux:终于到了大名鼎鼎的 Linux 操作系统了,太强大了,不予以解释了,大家都懂。

    YnahKe.md.png

  16. DOS (Disk Operating System):磁盘操作系统(缩写为DOS)是可以使用磁盘存储设备(例如软盘,硬盘驱动器或光盘)的计算机操作系统。

  17. MS-DOS(MicroSoft Disk Operating System) :一个由美国微软公司发展的操作系统,运行在Intel x86个人电脑上。它是DOS操作系统家族中最著名的一个,在Windows 95以前,DOS是IBM PC及兼容机中的最基本配备,而MS-DOS则是个人电脑中最普遍使用的DOS操作系统。

Yna5bd.md.png

  1. MacOS X,怎能少的了苹果操作系统?macOS 是苹果公司推出的基于图形用户界面操作系统,为 Macintosh 的主操作系统

YnaoVA.md.png

  1. Windows NT(Windows New Technology):是美国微软公司 1993 年推出的纯 32 位操作系统核心。

  2. Service Pack(SP):是程序的更新、修复和(或)增强的集合,以一个独立的安装包的形式发布。许多公司,如微软或Autodesk,通常在为某一程序而做的修补程序达到一定数量时,就发布一个Service Pack。

  3. 数字版权管理(DRM):他是工具或技术保护措施(TPM)是一组访问控制技术,用于限制对专有硬件和受版权保护的作品的使用。

  4. x86:x86是一整套指令集体系结构,由 Intel 最初基于 Intel 8086 微处理器及其 8088 变体开发。采用内存分段作为解决方案,用于处理比普通 16 位地址可以覆盖的更多内存。32 位是 x86 默认的位数,除此之外,还有一个 x86-64 位,是x86架构的 64 位拓展,向后兼容于 16 位及 32 位的 x86架构。

  5. FreeBSD:FreeBSD 是一个类 UNIX 的操作系统,也是 FreeBSD 项目的发展成果。

  6. X Window System:X 窗口系统(X11,或简称X)是用于位图显示的窗口系统,在类 UNIX 操作系统上很常见。

Yna64x.md.png

  1. Gnome:GNOME 是一个完全由自由软件组成的桌面环境。它的目标操作系统是Linux,但是大部分的 BSD 系统亦支持 GNOME。

YnaL28.md.png

  1. 网络操作系统(network operating systems):网络操作系统是用于网络设备(如路由器,交换机或防火墙)的专用操作系统。

YnaTUI.md.png

  1. 分布式网络系统(distributed operating systems):分布式操作系统是在独立,网络,通信和物理上独立计算节点的集合上的软件。 它们处理由多个CPU服务的作业。每个单独的节点都拥有全局集合操作系统的特定软件的一部分。

Yna75t.md.png

  1. 程序计数器(Program counter):程序计数器 是一个 CPU 中的寄存器,用于指示计算机在其程序序列中的位置。

  2. 堆栈寄存器(stack pointer): 堆栈寄存器是计算机 CPU 中的寄存器,其目的是跟踪调用堆栈。

  3. 程序状态字(Program Status Word): 它是由操作系统维护的8个字节(或64位)长的数据的集合。它跟踪系统的当前状态。

  4. 流水线(Pipeline): 在计算世界中,管道是一组串联连接的数据处理元素,其中一个元素的输出是下一个元素的输入。 流水线的元素通常以并行或按时间分割的方式执行。 通常在元素之间插入一定数量的缓冲区存储。

YnabPP.md.png

  1. 超标量(superscalar): 超标量 CPU 架构是指在一颗处理器内核中实行了指令级并发的一类并发运算。这种技术能够在相同的CPU主频下实现更高的 CPU 流量。
  2. 系统调用(system call): 指运行在用户空间的程序向操作系统内核请求需要更高权限运行的服务。系统调用提供用户程序与操作系统之间的接口。大多数系统交互式操作需求在内核态运行。如设备 IO 操作或者进程间通信。
  3. 多线程(multithreading):是指从软件或者硬件上实现多个线程并发执行的技术。具有多线程能力的计算机因为有硬件支持而能够在同一时间执行多个线程,进而提升整体处理性能。
  4. CPU 核心(core):它是 CPU 的大脑,它接收指令,并执行计算或运算以满足这些指令。一个 CPU 可以有多个内核。
  5. 图形处理器(Graphics Processing Unit):又称显示核心、视觉处理器、显示芯片或绘图芯片;它是一种专门在个人电脑、工作站、游戏机和一些移动设备(如平板电脑、智能手机等)上运行绘图运算工作的微处理器。

YnaOxS.md.png

  1. 存储体系结构:顶层的存储器速度最高,但是容量最小,成本非常高,层级结构越向下,其访问效率越慢,容量越大,但是造价也就越便宜。

YnavrQ.md.png

  1. 高速缓存行(cache lines):其实就是把高速缓存分割成了固定大小的块,其大小是以突发读或者突发写周期的大小为基础的。
  2. 缓存命中(cache hit):当应用程序或软件请求数据时,会首先发生缓存命中。 首先,中央处理单元(CPU)在其最近的内存位置(通常是主缓存)中查找数据。 如果在缓存中找到请求的数据,则将其视为缓存命中。

Ynaxbj.png

  1. L1 cache:一级缓存是 CPU 芯片中内置的存储库。 L1缓存也称为主缓存,是计算机中最快的内存,并且最接近处理器。

  2. L2 cache: 二级缓存存储库,内置在 CPU 芯片中,包装在同一模块中,或者建在主板上。 L2 高速缓存提供给 L1 高速缓存,后者提供给处理器。 L2 内存比 L1 内存慢。

  3. L2 cache: 三级缓存内置在主板上或CPU模块内的存储库。 L3 高速缓存为 L2 高速缓存提供数据,其内存通常比 L2 内存慢,但比主内存快。 L3 高速缓存提供给 L2 高速缓存,后者又提供给 L1 高速缓存,后者又提供给处理器。

  4. RAM((Random Access Memory):随机存取存储器,也叫主存,是与 CPU 直接交换数据的内部存储器。它可以随时读写,而且速度很快,通常作为操作系统或其他正在运行中的程序的临时数据存储介质。RAM工作时可以随时从任何一个指定的地址写入(存入)或读出(取出)信息。它与 ROM 的最大区别是数据的易失性,即一旦断电所存储的数据将随之丢失。RAM 在计算机和数字系统中用来暂时存储程序、数据和中间结果。

  5. ROM (Read Only Memory):只读存储器是一种半导体存储器,其特性是一旦存储数据就无法改变或删除,且内容不会因为电源关闭而消失。在电子或电脑系统中,通常用以存储不需经常变更的程序或数据。

  6. EEPROM (Electrically Erasable PROM):电可擦除可编程只读存储器,是一种可以通过电子方式多次复写的半导体存储设备。

  7. 闪存(flash memory): 是一种电子式可清除程序化只读存储器的形式,允许在操作中被多次擦或写的存储器。这种科技主要用于一般性数据存储,以及在电脑与其他数字产品间交换传输数据,如储存卡与U盘。

  8. SSD(Solid State Disks):固态硬盘,是一种主要以闪存作为永久性存储器的电脑存储设备。

Yndpan.md.png

  1. 虚拟地址(virtual memory): 虚拟内存是计算机系统内存管理的一种机制。它使得应用程序认为它拥有连续可用的内存(一个连续完整的地址空间),而实际上,它通常是被分隔成多个物理内存碎片,还有部分暂时存储在外部磁盘存储器上,在需要时进行数据交换。与没有使用虚拟内存技术的系统相比,使用这种技术的系统使得大型程序的编写变得更容易,对真正的物理内存(例如RAM)的使用也更有效率。

  2. MMU (Memory Management Unit):内存管理单元,有时称作分页内存管理单元。它是一种负责处理中央处理器(CPU)的内存访问请求的计算机硬件。它的功能包括虚拟地址到物理地址的转换(即虚拟内存管理)、内存保护、中央处理器高速缓存的控制等。

    YndSVs.md.png

  3. context switch:上下文切换,又称环境切换。是一个存储和重建 CPU 状态的机制。要交换 CPU 上的进程时,必需先行存储当前进程的状态,然后再将进程状态读回 CPU 中。

  4. 驱动程序(device driver):设备驱动程序,简称驱动程序(driver),是一个允许高级别电脑软件与硬件交互的程序,这种程序创建了一个硬件与硬件,或硬件与软件沟通的接口,经由主板上的总线或其它沟通子系统与硬件形成连接的机制,这样使得硬件设备上的数据交换成为可能。

    Ynd95q.md.png

  5. 忙等(busy waiting):在软件工程中,忙碌等待也称自旋,是一种以进程反复检查一个条件是否为真的条件,这种机制可能为检查键盘输入或某个锁是否可用。

  6. 中断(Interrupt):通常,在接收到来自外围硬件(相对于中央处理器和内存)的异步信号,或来自软件的同步信号之后,处理器将会进行相应的硬件/软件处理。发出这样的信号称为进行中断请求(interrupt request,IRQ)。硬件中断导致处理器通过一个运行信息切换(context switch)来保存执行状态(以程序计数器和程序状态字等寄存器信息为主);软件中断则通常作为 CPU 指令集中的一个指令,以可编程的方式直接指示这种运行信息切换,并将处理导向一段中断处理代码。中断在计算机多任务处理,尤其是即时系统中尤为有用。

  7. 中断向量(interrupt vector):中断向量位于中断向量表中。中断向量表(IVT)是将中断处理程序列表与中断向量表中的中断请求列表相关联的数据结构。 中断向量表的每个条目(称为中断向量)都是中断处理程序的地址。

    YndPP0.md.png

  8. DMA (Direct Memory Access):直接内存访问,直接内存访问是计算机科学中的一种内存访问技术。它允许某些电脑内部的硬件子系统(电脑外设),可以独立地直接读写系统内存,而不需中央处理器(CPU)介入处理 。

  9. 总线(Bus):总线(Bus)是指计算机组件间规范化的交换数据的方式,即以一种通用的方式为各组件提供数据传送和控制逻辑。

  10. PCIe (Peripheral Component Interconnect Exdivss):官方简称PCIe,是计算机总线的一个重要分支,它沿用现有的PCI编程概念及信号标准,并且构建了更加高速的串行通信系统标准。

  11. DMI (Direct Media Interface):直接媒体接口,是英特尔专用的总线,用于电脑主板上南桥芯片和北桥芯片之间的连接。

  12. USB(Universal Serial Bus):是连接计算机系统与外部设备的一种串口总线标准,也是一种输入输出接口的技术规范,被广泛地应用于个人电脑和移动设备等信息通讯产品,并扩展至摄影器材、数字电视(机顶盒)、游戏机等其它相关领域。

YndiGV.md.png

  1. BIOS(Basic Input Output System):是在通电引导阶段运行硬件初始化,以及为操作系统提供运行时服务的固件。它是开机时运行的第一个软件。

    YndF2T.md.png

  2. 硬实时系统(hard real-time system):硬实时性意味着你必须绝对在每个截止日期前完成任务。 很少有系统有此要求。 例如核系统,一些医疗应用(例如起搏器),大量国防应用,航空电子设备等。

  3. 软实时系统(soft real-time system):软实时系统可能会错过某些截止日期,但是如果错过太多,最终性能将下降。 一个很好的例子是计算机中的声音系统。

  4. 进程(Process):程序本身只是指令、数据及其组织形式的描述,进程才是程序(那些指令和数据)的真正运行实例。若进程有可能与同一个程序相关系,且每个进程皆可以同步(循序)或异步的方式独立运行。

  5. 地址空间(address space):地址空间是内存中可供程序或进程使用的有效地址范围。 也就是说,它是程序或进程可以访问的内存。 存储器可以是物理的也可以是虚拟的,用于执行指令和存储数据。

  6. 进程表(process table):进程表是操作系统维护的数据结构,该表中的每个条目(通常称为上下文块)均包含有关进程的信息,例如进程名称和状态,优先级,寄存器以及它可能正在等待的信号灯。

  7. 命令行界面(command-line interdivter):是在图形用户界面得到普及之前使用最为广泛的用户界面,它通常不支持鼠标,用户通过键盘输入指令,计算机接收到指令后,予以执行。

YndkxU.md.png

  1. 进程间通信(interprocess communication): 指至少两个进程或线程间传送数据或信号的一些技术或方法。
  2. 超级用户(superuser): 也被称为管理员帐户,在计算机操作系统领域中指一种用于进行系统管理的特殊用户,其在系统中的实际名称也因系统而异,如 root、administrator 与supervisor。
  3. 目录(directory): 在计算机或相关设备中,一个目录或文件夹就是一个装有数字文件系统的虚拟容器。在它里面保存着一组文件和其它一些目录。
  4. 路径(path name): 路径是一种电脑文件或目录的名称的通用表现形式,它指向文件系统上的一个唯一位置。
  5. 根目录(root directory):根目录指的就是计算机系统中的顶层目录,比如 Windows 中的 C 盘和 D 盘,Linux 中的 /。
  6. 工作目录(Working directory):它是一个计算机用语。用户在操作系统内所在的目录,用户可在此目录之下,用相对文件名访问文件。
  7. 文件描述符(file descriptor): 文件描述符是计算机科学中的一个术语,是一个用于表述指向文件的引用的抽象化概念。
  8. inode:索引节点的缩写,索引节点是 UNIX 系统中包含的信息,其中包含有关每个文件的详细信息,例如节点,所有者,文件,文件位置等。
  9. 共享库(shared library):共享库是一个包含目标代码的文件,执行过程中多个 a.out 文件可能会同时使用该目标代码。
  10. DLLs (Dynamic-Link Libraries):动态链接库,它是微软公司在操作系统中实现共享函数库概念的一种实现方式。这些库函数的扩展名是 .DLL、.OCX(包含ActiveX控制的库)或者.DRV(旧式的系统驱动程序)。
  11. 客户端(clients):客户端是访问服务器提供的服务的计算机硬件或软件。
  12. 服务端(servers): 在计算中,服务器是为其他程序或设备提供功能的计算机程序或设备,称为服务端
  13. 主从架构(client-server): 主从式架构也称客户端/服务器架构、C/S 架构,是一种网络架构,它把客户端与服务器区分开来。每一个客户端软件的实例都可以向一个服务器或应用程序服务器发出请求。有很多不同类型的服务器,例如文件服务器、游戏服务器等。

  1. 虚拟机(Virtual Machines):在计算机科学中的体系结构里,是指一种特殊的软件,可以在计算机平台和终端用户之间创建一种环境,而终端用户则是基于虚拟机这个软件所创建的环境来操作其它软件。

    YndVr4.png

  2. Java 虚拟机(Jaav virtual Machines):Java虚拟机有自己完善的硬体架构,如处理器、堆栈、寄存器等,还具有相应的指令系统。JVM屏蔽了与具体操作系统平台相关的信息,使得Java程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。

  3. 目标文件(object file):目标文件是包含目标代码的文件,这意味着通常无法直接执行的可重定位格式的机器代码。 目标文件有多种格式,相同的目标代码可以打包在不同的目标文件中。 目标文件也可以像共享库一样工作。

  4. C divprocessor: C 预处理å器是 C 语言、C++ 语言的预处理器。用于在编译器处理程序之前预扫描源代码,完成头文件的包含, 宏扩展, 条件编译, 行控制等操作。

  5. 设备控制器(device controller): 设备控制器是处理 CPU 传入信号和传出信号的系统。设备通过插头和插座连接到计算机,并且插座连接到设备控制器。

  6. ECC(Error-Correcting Code): 指能够实现错误检查和纠正错误技术的内存。

  7. I/O port: 也被称为输入/输出端口,它是由软件用来与计算机上的硬件进行通信的内存地址。

  8. 内存映射I/O(memory mapped I/O,MMIO): 内存映射的 I/O 使用相同的地址空间来寻址内存和 I/O 设备,也就是说,内存映射I/O 设备共享同一内存地址。

  9. 端口映射I/O(Port-mapped I/O ,PMIO):在 PMIO中,内存和I/O设备有各自的地址空间。 端口映射I/O通常使用一种特殊的CPU指令,专门执行I/O操作。

  10. DMA (Direct Memory Access): 直接内存访问,它是计算机系统的一项功能,它允许某些硬件系统能够独立于 CPU 访问内存。如果没有 DMA,当 CPU 执行输入/输出指令时,它通常在读取或写入操作的整个过程中都被完全占用,因此无法执行其他工作。使用 DMA 后,CPU 首先启动传输信号,然后在进行传输时执行其他操作,最后在完成操作后从 DMA 控制器(DMAC)接收中断。完成执行。

YndZqJ.png

  1. 周期窃取(cycle stealing):许多总线能够以两种模式操作:每次一字模式和块模式。一些 DMA 控制器也能够使用这两种方式进行操作。在前一个模式中,DMA 控制器请求传送一个字并得到这个字。如果 CPU 想要使用总线,它必须进行等待。设备可能会偷偷进入并且从 CPU 偷走一个总线周期,从而轻微的延迟 CPU。它类似于直接内存访问(DMA),允许I / O控制器在无需 CPU 干预的情况下读取或写入RAM。

  2. 突发模式(burst mode): 指的是设备在不进行单独事务中重复传输每个数据所需的所有步骤的情况下,重复传输数据的情况。

  3. 中断向量表(interrupt vector table): 用来形成相应的中断服务程序的入口地址或存放中断服务程序的首地址称为中断向量。 中断向量表是中断向量的集合,中断向量是中断处理程序的地址。

  4. 精确中断(divcise interrupt):精确中断是一种能够使机器处于良好状态下的中断,它具有如下特征

  • PC (程序计数器)保存在一个已知的地方
  • PC 所指向的指令之前所有的指令已经完全执行
  • PC 所指向的指令之后所有的指令都没有执行
  • PC 所指向的指令的执行状态是已知的
  1. 非精确中断(imdivcise interrupt):不满足以上要求的中断,指令的执行时序和完成度具有不确定性,而且恢复起来也非常麻烦。
  2. 设备独立性(device independence):我们编写访问任何设备的应用程序,不用事先指定特定的设备。比如你编写了一个能够从设备读入文件的应用程序,那么这个应用程序可以从硬盘、DVD 或者 USB 进行读入,不必再为每个设备定制应用程序。这其实就体现了设备独立性的概念。

  1. UNC(Uniform Naming Convention) :UNC 是统一命名约定或统一命名约定的缩写,是用于命名和访问网络资源(例如网络驱动器,打印机或服务器)的标准。 例如,在 MS-DOS 和 Microsoft Windows 中,用户可以通过键入或映射到类似于以下示例的共享名来访问共享资源。
\\computer\path

然而,在 UNIX 和 Linux 中,你会像如下这么写

//computer/path
  1. 挂载(mounting) :挂载是指操作系统会让存储在硬盘、CD-ROM 等资源设备上的目录和文件,通过文件系统能够让用户访问的过程。
  2. 错误处理(Error handling): 错误处理是指对软件应用程序中存在的错误情况的响应和恢复过程。
  3. 同步阻塞(synchronous): 同步是阻塞式的,CPU 必须等待同步的处理结果。
  4. 异步响应(asynchronous): 异步是由中断驱动的,CPU 不用等待每个操作的处理结果继而执行其他操作
  5. 缓冲区(buffering): 缓冲区是内存的临时存储区域,它的出现是为了加快内存的访问速度而设计的。对于经常访问的数据和指令来说,CPU 应该访问的是缓冲区而非内存
  6. Programmed input–output,PIO:它指的是在 CPU 和外围设备(例如网络适配器或 ATA 存储设备)之间传输数据的一种方法。
  7. 轮询(polling): 轮询是指通过客户端程序主动通过对每个设备进行访问来获得同步状态的过程。

YndnaR.png

  1. 忙等(busy waiting):当一个进程正处在某临界区内,任何试图进入其临界区的进程都必须等待,陷入忙等状态。连续测试一个变量直到某个值出现为止,称为忙等。
  2. 可重入(reentrant): 如果一段程序或者代码在任意时刻被中断后由操作系统调用其他程序或者代码,这段代码调用子程序并能够正确运行,这种现象就称为可重入。也就是说当该子程序正在运行时,执行线程可以再次进入并执行它,仍然获得符合设计时预期的结果。
  3. 主设备编号(major device number)、副设备编号(minor device number) : 所有设备都有一个主,副号码。 主号码是更大,更通用的类别(例如硬盘,输入/输出设备等),而次号码则更具体(即告诉设备连接到哪条总线)。
  4. 多重缓冲区(double buffering): 它指的是使用多个缓冲区来保存数据块,每个缓冲区都保留数据块的一部分,读取的时候通过读取多个缓冲区的数据进而拼凑成一个完整的数据。
  5. 环形缓冲区(circular buffer): 它指的是首尾相连的缓冲区,常用来实现数据缓冲流。

  1. 假脱机(Spooling) :假脱机是多程序的一种特殊形式,目的是在不同设备之间复制数据。 在现代系统中,通常用于计算机应用程序和慢速外围设备(例如打印机)之间的中介。
  2. 守护进程(Daemon): 在计算机中,守护程序是作为后台进程运行的计算机程序,而不是在交互式用户的直接控制下运行的程序。
  3. 逻辑块寻址(logical block addressing, LBA):逻辑块寻址是一种通用方案,用于指定存储在计算机存储设备上的数据块的位置。
  4. RAID:全称是 Redundant Array of Inexpensive Disks ,廉价磁盘或驱动器的冗余阵列,它是一种数据存储虚拟化的技术,将多个物理磁盘驱动器组件组合成一个或多个逻辑单元,以实现数据冗余,改善性能。

  1. MBR(Master Boot Record):主引导记录(MBR)是任何硬盘或软盘的第一扇区中的信息,用于标识操作系统的放置方式和位置,以便可以将其加载到计算机的主存储器或随机存取存储器中。

YndQG6.png

  1. FCFS (First-Come, First-Served): 先进先出的调度算法,也就是说,首先到达 CPU 的进程首先进行服务。
  2. SSF (Shortest Seek First) 最短路径优先算法,这是对先进先出算法的改进,这种算法因为减少了总的磁臂运动,从而缩短了平均响应时间。
  3. 稳定存储(stable storage): 它是计算机存储技术的一种分类,该技术可确保任何给定的写操作都具有原子性。
  4. 时钟(Clocks):也被称为 timers。通常,时钟是指调节所有计算机功能的时序和速度的微芯片。芯片中是一个晶体,当通电时,晶体会以特定的频率振动。 任何一台计算机能够执行的最短时间是一个时钟或时钟芯片的一次振动。
  5. QR Code: 二维码的一种,它的全称是快速响应矩阵图码,能够快速响应。一般应用于手机读码操作,国内火车票上的二维码就是 QR 码

  1. 显卡(Video card),是个人电脑最基本组成部分之一,用途是将计算机系统所需要的显示信息进行转换驱动显示器,并向显示器提供逐行或隔行扫描信号,控制显示器的正确显示,是连接显示器和个人电脑主板的重要组件,是人机对话的重要设备之一。

YndJqH.png

  1. GDI (Graphics Device Interface):图形接口,是微软视窗系统提供的应用程序接口,也是其用来表征图形对象、将图形对象传送给诸如显示器、打印机之类输出设备的核心组件。
  2. 设备上下文(device context):设备上下文是 Windows 数据结构,其中包含有关设备(例如显示器或打印机)的图形属性的信息。 所有绘图调用都是通过设备上下文对象进行的,该对象封装了用于绘制线条,形状和文本的 Windows API。 设备上下文可用于绘制到屏幕,打印机或图元文件。
  3. 位图(bitmap):在计算机中,位图是从某个域(例如,整数范围)到位的映射。也称为位数组或位图索引。
  4. 电阻式触摸屏(Resistive touchscreens):电阻式触摸屏基于施加到屏幕上的压力来工作。 电阻屏由许多层组成。 当按下屏幕时,外部的后面板将被推到下一层,下一层会感觉到施加了压力并记录了输入。 电阻式触摸屏用途广泛,可以用手指,指甲,手写笔或任何其他物体进行操作。

  1. 电容式触摸屏(capacitive touchscreen):电容式触摸屏通过感应物体(通常是指尖上的皮肤)的导电特性来工作。 手机或智能手机上的电容屏通常具有玻璃表面,并且不依赖压力。 当涉及到手势(如滑动和捏合)时,它比电阻式屏幕更具响应性。 电容式触摸屏只能用手指触摸,而不能用普通的手写笔,手套或大多数其他物体来响应。

  1. 死锁(deadlock):死锁常用于并发情况下,死锁 是一种状态,死锁中的每个成员都在等待另一个成员(包括其自身)采取行动。

相信你一定看过这个图

YndtZd.png

  1. 可抢占资源(divemptable resource):可以从拥有它的进程中抢占而并不会产生任何副作用。
  2. 不可抢占资源(nondivemptable resource):与可抢占资源相反,如果资源被抢占后,会导致进程或任务出错。
  3. 系统检查点(system checkpointed):系统检查点是操作系统(OS)的可启动实例。检查点是计算机在特定时间点的快照。
  4. 两阶段加锁(two-phase locking, 2PL):经常用于数据库的并发控制,以保证可串行化

这种方法使用数据库锁在两个阶段:

  • 扩张阶段:不断上锁,没有锁被释放

  • 收缩阶段:锁被陆续释放,没有新的加锁

  1. 活锁(Livelock):活锁类似于死锁,不同之处在于,活锁中仅涉及进程的状态彼此之间不断变化,没有进展。举一个现实世界的例子,当两个人在狭窄的走廊里相遇时,就会发生活锁,每个人都试图通过移动到一边让对方通过而礼貌,但最终却没有任何进展就左右摇摆,因为他们总是同时移动相同的方式。

  2. 饥饿(starvation):在死锁或者活锁的状态中,在任何时刻都可能请求资源,虽然一些调度策略能够决定一些进程在某一时刻获得资源,但是有一些进程永远无法获得资源。永远无法获得资源的进程很容易产生饥饿。

  3. 沙盒(sandboxing):沙盒是一种软件管理策略,可将应用程序与关键系统资源和其他程序隔离。它提供了一层额外的安全保护,可防止恶意软件或有害应用程序对你的系统造成负面影响。

  4. VMM (Virtual Machine Monitor):也被称为 hypervisor,在同一个物理机器上创建出来多态虚拟机器的假象。

YndNdA.png

  1. 虚拟化技术(virtualization): 是一种资源管理技术,将计算机的各种实体资源(CPU、内存、磁盘空间、网络适配器等),进行抽象、转换后呈现出来并可供分割、组合为一个或多个电脑配置环境。

  2. 云(cloud):云是目前虚拟机最重要、最时髦的玩法。

  3. 解释器(interdivter): 解释器是一种程序,能够把编程语言一行一行解释运行。每次运行程序时都要先转成另一种语言再运行,因此解释器的程序运行速度比较缓慢。它不会一次把整个程序翻译出来,而是每翻译一行程序叙述就立刻运行,然后再翻译下一行,再运行,如此不停地进行下去。

  4. 半虚拟化(paravirtualization): 半虚拟化的目的不是呈现出一个和底层硬件一摸一样的虚拟机,而是提供一个软件接口,软件接口与硬件接口相似但又不完全一样。

  5. 全虚拟化(full virtualization):全虚拟化是硬件虚拟化的一种,允许未经修改的客操作系统隔离运行。对于全虚拟化,硬件特征会被映射到虚拟机上,这些特征包括完整的指令集、I/O操作、中断和内存管理等。

  6. 客户操作系统(guest operating system) : 客户操作系统是安装在计算机上操作系统之后的操作系统,客户操作系统既可以是分区系统的一部分,也可以是虚拟机设置的一部分。客户操作系统为设备提供了备用操作系统。

  7. 主机操作系统(host operating system): 主机操作系统是计算机系统的硬盘驱动器上安装的主要操作系统。 在大多数情况下,只有一个主机操作系统。

  8. 应用编程接口(Application Programming Interface,API):应用程序编程接口(API)是软件组件或系统的编程接口,它定义其他组件或系统如何使用它。

  9. 虚拟机接口(Virtual Machine Interface, VMI):它是一个高速接口,同一主机上的虚拟机(VM)可用于相互之间以及主机内核模块之间进行通信。

  10. 输入输出内存管理单元(Input–output memory management unit, I/O MMU):在计算机中,输入输出内存管理单元(IOMMU)是将直接内存访问(DMA)I / O 总线连接到主存的内存管理单元(MMU)。

  11. 设备穿透(device pass through):它允许将物理设备直接分配给特定虚拟机。

  12. 设备隔离(device isolation): 保证设备可以直接访问其分配到的虚拟机的内存而不影响其他虚拟机的完整性。

  13. 基础设施即服务(IAAS (Infrastructure As A Service)):基础架构即服务(IaaS)是一种即时计算基础架构,可通过 Internet 进行配置和管理。 它是四种云服务类型之一,另外还有软件即服务(SaaS),平台即服务(PaaS)和无服务器。

  1. 平台即服务(PAAS (Platform As A Service)):平台即服务(PaaS)或应用程序平台即服务(aPaaS)或基于平台的服务是云计算服务的一种,它提供了一个平台,使客户可以开发,运行和管理应用程序,而无需构建和维护该应用程序。

  1. 软件即服务(SAAS(Software As A Service)): 它是一个提供特定软件服务访问的平台,是一种软件许可和交付模型,在该模型中,软件是基于订阅许可的,并且是集中托管的。

  1. 实时迁移(live migration): 实时迁移是指在不断开客户端或应用程序连接的情况下,在不同的物理机之间移动正在运行的虚拟机或应用程序的过程,一般经常采用的方式是内存预复制迁移
  2. 写入时复制(copy on write):写入时复制是一种计算机程序设计领域的优化策略。其核心思想是,如果有多个调用者(callers)同时请求相同资源(如内存或磁盘上的数据存储),他们会共同获取相同的指针指向相同的资源,直到某个调用者试图修改资源的内容时,系统才会真正复制一份专用副本(private copy)给该调用者,而其他调用者所见到的最初的资源仍然保持不变
  3. 主从模型(master-slave):主/从是一种不对称通信或控制的模型,其中一个设备进程控制一个或多个其他设备或进程并充当其通信中心。 在某些系统中,从一组合格的设备中选择一个主设备,而其他设备则充当从设备的角色。

  1. 分布式系统(distributed system):分布式系统,也称为分布式计算,是一种具有位于不同机器上的多个组件的系统,这些组件可以通信和协调动作,以便对最终用户显示为单个一致的系统。

  2. 局域网(LANs, Local Area Networks):局域网(LAN)是一种计算机网络,可将住宅,学校,实验室,大学校园或办公大楼等有限区域内的计算机互连。

  3. 广域网(WAN,Wide Area Network):又称广域网、外网、公网。是连接不同地区局域网或城域网计算机通信的远程网。通常跨接很大的物理范围,所覆盖的范围从几十公里到几千公里,它能连接多个地区、城市和国家,或横跨几个洲并能提供远距离通信,形成国际性的远程网络。

  4. 以太网(Ethernet):以太网是一种计算机局域网的技术,它规定了包括物理层的连线、电子信号和介质访问层协议的内容。

  5. 桥接器(bridge):当指代计算机时,网桥是连接两个 LAN(局域网)或同一 LAN 的两个网段的设备。与路由器不同,网桥是独立于协议的。他们转发数据包时无需分析和重新路由消息。

  1. 主机(host):在网络硬件中,主机又被称为网络主机,网络主机是连接到计算机网络的计算机或其他设备。主机可以充当服务器,向网络上的用户或其他主机提供信息资源,服务和应用程序。主机被分配至少一个网络地址。
  2. 路由器(router):路由器是在计算机网络之间转发数据包的联网设备。通过互联网发送的数据(例如网页或电子邮件)以数据包的形式出现。
  3. 面向连接的服务(Connection-oriented service):面向连接的服务是一种在数据通信开始之前在通信实体之间建立专用连接的服务。要使用面向连接的服务,用户首先建立一个连接,使用它,然后释放它。TCP 就是一种面向连接的服务,在发送数据包之前需要经过握手操作。
  4. 无连接的服务(Connectionless service):无连接服务是两个节点之间的数据通信,其中发送方在不确保接收方是否可以接收数据的情况下发送数据。此处,每个数据包都具有目标地址,并且与其他数据包无关地独立路由。UDP 就是一种无连接的服务,发送数据包不需要经过握手连接。
  5. 服务质量(quality of service, QoS):服务质量是对服务整体性能的描述或度量,尤其是网络用户看到的性能。
  6. 确认包(acknowledgement packet):在数据网络,电信和计算机总线中,确认(ACK)是作为通信协议一部分在通信过程,计算机或设备之间传递以表示确认或消息接收的信号。
  7. 请求-响应服务(request-reply service):请求-响应是计算机彼此通信的基本方法之一,其中第一台计算机发送对某些数据的请求,第二台计算机对请求进行响应。

YndrQS.png

  1. 协议栈(protocol stack):所有现代网络都使用所谓的协议栈把不同的协议一层一层叠加起来。每一层解决不同的问题。

  1. IP地址:标示互联网上每一台主机有两种方式,一种是 IPv4 ,一种是 IPv6。
  2. 超链接(hyperlink):超链接是可以单击以跳到新文档或当前文档中新部分的单词,短语或图像。 几乎在所有网页中都可以找到超链接,从而允许用户单击页面之间的方式。 文本超链接通常为蓝色并带有下划线。
  3. Web 页面(Web page):网页是一个适用于万维网和网页浏览器的文件。
  4. Web浏览器:Web浏览器(通常称为浏览器)是一种用于访问 Internet 上的信息的软件应用程序。 当用户请求特定网站时,Web 浏览器从 Web 服务器检索必要的内容,然后在用户的设备上显示结果网页。
  5. 漏洞(vulnerability):漏洞是一种系统不安全级别的错误。
  6. 漏洞利用(exploit):漏洞利用是计算机安全术语,指的是利用程序中的某些漏洞,来得到计算机的控制权。
  7. 病毒(virus):计算机病毒是一种计算机程序,在执行时会通过修改其他计算机程序并插入自己的代码来自我复制。复制成功后,可以说受影响的区域已被计算机病毒感染。

YndcZj.png

  1. CIA(Confidentiality,Integrity,Availability):安全系统的三个指标,即机密性、完整性和可用性。
  2. 黑客(cracker):黑客是指经常通过网络闯入他人计算机系统的人。 绕过计算机程序中的密码或许可证; 或以其他方式故意破坏计算机安全性。 黑客可能会出于恶意,出于某些利他目的或原因,或者是因为存在挑战而牟取暴利。 表面上已经进行了一些破解和输入,以指出站点安全系统中的弱点。
  3. 端口扫描(portscan):端口扫描程序是一种旨在探测服务器或主机是否存在开放端口的应用程序。 管理员可以使用这种应用程序来验证其网络的安全策略,攻击者可以使用这种应用程序来识别主机上运行的网络服务并利用漏洞。
  4. 僵尸网络(botnets):僵尸网络是指骇客利用自己编写的分布式拒绝服务攻击程序将数万个沦陷的机器,即骇客常说的傀儡机或肉鸡。
  5. 域(domain):网域名称,简称域名、网域,是由一串用点分隔的字符组成的互联网上某一台计算机或计算机组的名称,用于在数据传输时标识计算机的电子方位。

YndyLQ.png

  1. 盐(solt):在密码学中,盐是随机数据,用作哈希数据,密码或密码的单向函数的附加输入。
  2. 逻辑炸弹(logic bomb) : 是一些嵌入在正常软件中并在特定情况下执行的恶意程式码。这些特定情况包括更改档案、特别的程式输入序列、特定的时间或日期等。恶意程式码可能会将档案删除、使电脑主机当机或是造成其他的损害。
  3. 定时炸弹(time bomb):在计算机软件中,定时炸弹是已编写的计算机程序的一部分,因此它会在达到预定的日期或时间后开始或停止运行。
  4. 登陆欺骗(login spoofing):登录欺骗是用于窃取用户密码的技术。它会向用户显示一个普通的登录提示,提示用户名和密码,这实际上是一个恶意程序,通常在攻击者的控制下称为特洛伊木马。
  5. 后门程序(backdoor):软件后门指绕过软件的安全性控制,从比较隐秘的通道获取对程序或系统访问权的黑客方法。
  6. 防火墙(firewall):防火墙在计算机科学领域中是一个架设在互联网与企业内网之间的信息安全系统,根据企业预定的策略来监控往来的传输。

文章参考:

https://en.wikipedia.org/wiki/Copy-on-write

https://en.wikipedia.org/wiki/Live_migration

https://www.techopedia.com/definition/15763/host-operating-system

https://en.wikibooks.org/wiki/Operating_System_Design/Concurrency/Livelock

https://www.studytonight.com/operating-system/first-come-first-serve

https://blog.csdn.net/liuchuo/article/details/51986201

https://docs.openstack.org/ceilometer/6.1.5/architecture.html

https://www.techopedia.com/definition/16626/error-handling

https://simple.wikipedia.org/wiki/Device_controller

https://blog.csdn.net/zhangjg_blog/article/details/20380971

https://www.techopedia.com/definition/4763/address-space

https://en.wikipedia.org/wiki/Direct_Media_Interface

https://en.wikipedia.org/wiki/Bus_(computing)

https://en.wikipedia.org/wiki/Interrupt_vector_table

https://en.wikipedia.org/wiki/Busy_waiting

https://en.wikipedia.org/wiki/Context_switch

https://en.wikipedia.org/wiki/Read-only_memory

https://www.techopedia.com/definition/6306/cache-hit

https://zhuanlan.zhihu.com/p/37749443

https://en.wikipedia.org/wiki/Pipeline_(computing)

https://en.wikipedia.org/wiki/Stack_register

https://en.wikipedia.org/wiki/Distributed_operating_system

https://en.wikipedia.org/wiki/Time-sharing

https://zh.wikipedia.org/wiki/UNIX

https://zh.wikipedia.org/wiki/UNIX_System_V

https://en.wikipedia.org/wiki/Network_operating_system

https://zh.wikipedia.org/zh/X86-64

https://zh.wikipedia.org/zh/X86

https://en.wikipedia.org/wiki/Cloud_computing

https://www.techopedia.com/definition/24356/mainframe

https://zh.wikipedia.org/wiki/SATA

https://blog.codinghorror.com/understanding-user-and-kernel-mode/

https://en.wikipedia.org/wiki/Protection_ring


上次更新: 2022/03/09, 23:07:05
java基础02

java基础02→

Theme by Vdoing | Copyright © 2019-2025 鄂公网安备42028102000288 鄂ICP备19019767号-2鄂ICP备19019767号-4
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式