diff --git a/2022/12/21/JavaWeb/index.html b/2022/12/21/JavaWeb/index.html index 2928b5d6..a3e43bf4 100644 --- a/2022/12/21/JavaWeb/index.html +++ b/2022/12/21/JavaWeb/index.html @@ -2171,7 +2171,7 @@

- + diff --git a/2023/01/10/xv6$chap1/index.html b/2023/01/10/xv6$chap1/index.html index 31fc15b0..d3151365 100644 --- a/2023/01/10/xv6$chap1/index.html +++ b/2023/01/10/xv6$chap1/index.html @@ -567,7 +567,7 @@

- + diff --git a/2023/01/10/xv6$chap2/index.html b/2023/01/10/xv6$chap2/index.html index 3ffc33a1..81fc0a12 100644 --- a/2023/01/10/xv6$chap2/index.html +++ b/2023/01/10/xv6$chap2/index.html @@ -568,7 +568,7 @@

- + diff --git a/2023/01/10/xv6$chap3/index.html b/2023/01/10/xv6$chap3/index.html index c035d8af..eef72739 100644 --- a/2023/01/10/xv6$chap3/index.html +++ b/2023/01/10/xv6$chap3/index.html @@ -640,10 +640,10 @@
- + - + diff --git a/2023/01/10/xv6$chap4/index.html b/2023/01/10/xv6$chap4/index.html index 5cf5c5e2..414ceb10 100644 --- a/2023/01/10/xv6$chap4/index.html +++ b/2023/01/10/xv6$chap4/index.html @@ -693,10 +693,10 @@
- + - + diff --git a/2023/01/10/xv6$chap5/index.html b/2023/01/10/xv6$chap5/index.html index fc54344a..9923934e 100644 --- a/2023/01/10/xv6$chap5/index.html +++ b/2023/01/10/xv6$chap5/index.html @@ -431,7 +431,7 @@

代码
- + diff --git a/2023/01/10/xv6$chap6/index.html b/2023/01/10/xv6$chap6/index.html index d33e5c75..adf51f17 100644 --- a/2023/01/10/xv6$chap6/index.html +++ b/2023/01/10/xv6$chap6/index.html @@ -543,7 +543,7 @@
- +
diff --git a/2023/01/10/xv6$chap7/index.html b/2023/01/10/xv6$chap7/index.html index 00beb7c2..ffa5e80c 100644 --- a/2023/01/10/xv6$chap7/index.html +++ b/2023/01/10/xv6$chap7/index.html @@ -527,10 +527,10 @@

- + - + diff --git a/2023/01/10/xv6$chap8/index.html b/2023/01/10/xv6$chap8/index.html index 37220808..bbebfad2 100644 --- a/2023/01/10/xv6$chap8/index.html +++ b/2023/01/10/xv6$chap8/index.html @@ -941,10 +941,10 @@

- + - + diff --git a/2023/01/10/xv6$chap9/index.html b/2023/01/10/xv6$chap9/index.html index 60f503e4..649b1ebf 100644 --- a/2023/01/10/xv6$chap9/index.html +++ b/2023/01/10/xv6$chap9/index.html @@ -520,10 +520,10 @@

- + - + diff --git a/2023/01/10/xv6/index.html b/2023/01/10/xv6/index.html index 691179ec..06cfe2b8 100644 --- a/2023/01/10/xv6/index.html +++ b/2023/01/10/xv6/index.html @@ -257,10 +257,10 @@

- + - +
diff --git a/2023/02/25/cs144$else/index.html b/2023/02/25/cs144$else/index.html index 45583843..0ba6b125 100644 --- a/2023/02/25/cs144$else/index.html +++ b/2023/02/25/cs144$else/index.html @@ -366,10 +366,10 @@

其他
- + - +
diff --git a/2023/02/25/cs144$lab0/index.html b/2023/02/25/cs144$lab0/index.html index a7ebeb5a..d4a0fa10 100644 --- a/2023/02/25/cs144$lab0/index.html +++ b/2023/02/25/cs144$lab0/index.html @@ -308,10 +308,10 @@

- + - + diff --git a/2023/02/25/cs144$lab1/index.html b/2023/02/25/cs144$lab1/index.html index a7957b96..a5b5e17d 100644 --- a/2023/02/25/cs144$lab1/index.html +++ b/2023/02/25/cs144$lab1/index.html @@ -398,10 +398,10 @@

- + - + diff --git a/2023/02/25/cs144$lab2/index.html b/2023/02/25/cs144$lab2/index.html index 4e0b351c..ad1b62e5 100644 --- a/2023/02/25/cs144$lab2/index.html +++ b/2023/02/25/cs144$lab2/index.html @@ -391,10 +391,10 @@

- + - + diff --git a/2023/02/25/cs144$lab3/index.html b/2023/02/25/cs144$lab3/index.html index 7b11591f..e3c86250 100644 --- a/2023/02/25/cs144$lab3/index.html +++ b/2023/02/25/cs144$lab3/index.html @@ -349,10 +349,10 @@

- + - + diff --git a/2023/02/25/cs144$lab4/index.html b/2023/02/25/cs144$lab4/index.html index 88ec21ec..327c339f 100644 --- a/2023/02/25/cs144$lab4/index.html +++ b/2023/02/25/cs144$lab4/index.html @@ -432,10 +432,10 @@

- + - + diff --git a/2023/02/25/cs144$lab5/index.html b/2023/02/25/cs144$lab5/index.html index 92e55f77..858d0c3a 100644 --- a/2023/02/25/cs144$lab5/index.html +++ b/2023/02/25/cs144$lab5/index.html @@ -386,7 +386,7 @@

- + diff --git a/2023/02/25/cs144$lab6/index.html b/2023/02/25/cs144$lab6/index.html index 5e988403..27cca3b6 100644 --- a/2023/02/25/cs144$lab6/index.html +++ b/2023/02/25/cs144$lab6/index.html @@ -263,10 +263,10 @@

- + - + diff --git a/2023/02/25/cs144/index.html b/2023/02/25/cs144/index.html index 926935c8..0359e3a4 100644 --- a/2023/02/25/cs144/index.html +++ b/2023/02/25/cs144/index.html @@ -287,7 +287,7 @@

- + diff --git "a/2023/03/10/\345\257\271moore\345\236\213\345\222\214mealy\345\236\213\347\212\266\346\200\201\346\234\272\347\232\204\347\220\206\350\247\243/index.html" "b/2023/03/10/\345\257\271moore\345\236\213\345\222\214mealy\345\236\213\347\212\266\346\200\201\346\234\272\347\232\204\347\220\206\350\247\243/index.html" index fc1051d2..70003902 100644 --- "a/2023/03/10/\345\257\271moore\345\236\213\345\222\214mealy\345\236\213\347\212\266\346\200\201\346\234\272\347\232\204\347\220\206\350\247\243/index.html" +++ "b/2023/03/10/\345\257\271moore\345\236\213\345\222\214mealy\345\236\213\347\212\266\346\200\201\346\234\272\347\232\204\347\220\206\350\247\243/index.html" @@ -218,10 +218,10 @@

状态机

- + - +
diff --git a/2023/03/13/cmu15445$lab0/index.html b/2023/03/13/cmu15445$lab0/index.html index 18da2601..806a340b 100644 --- a/2023/03/13/cmu15445$lab0/index.html +++ b/2023/03/13/cmu15445$lab0/index.html @@ -459,10 +459,10 @@

- + - + diff --git a/2023/03/13/cmu15445$lab1/index.html b/2023/03/13/cmu15445$lab1/index.html index 8b690c1f..8c999e82 100644 --- a/2023/03/13/cmu15445$lab1/index.html +++ b/2023/03/13/cmu15445$lab1/index.html @@ -481,10 +481,10 @@

- + - + diff --git a/2023/03/13/cmu15445$lab2/index.html b/2023/03/13/cmu15445$lab2/index.html index 5ed9d86f..e0bfd13c 100644 --- a/2023/03/13/cmu15445$lab2/index.html +++ b/2023/03/13/cmu15445$lab2/index.html @@ -449,10 +449,10 @@

- + - + diff --git a/2023/03/13/cmu15445$lab3/index.html b/2023/03/13/cmu15445$lab3/index.html index 71f2bb83..60e1adf0 100644 --- a/2023/03/13/cmu15445$lab3/index.html +++ b/2023/03/13/cmu15445$lab3/index.html @@ -680,7 +680,7 @@

- + diff --git a/2023/03/13/cmu15445/index.html b/2023/03/13/cmu15445/index.html index 3253be67..1f963035 100644 --- a/2023/03/13/cmu15445/index.html +++ b/2023/03/13/cmu15445/index.html @@ -229,7 +229,7 @@

CMU15445

实验官网

代码

-

Project0 C++ Primer

Project1 Buffer Pool

Project2 B+Tree Index

Project3 Query Execution

+

Project0 C++ Primer

Project1 Buffer Pool

Project2 B+Tree Index

Project3 Query Execution

@@ -252,7 +252,7 @@

- + diff --git "a/2023/06/17/\345\257\271GRUB\345\222\214initramfs\347\232\204\345\260\217\346\216\242\347\251\266/index.html" "b/2023/06/17/\345\257\271GRUB\345\222\214initramfs\347\232\204\345\260\217\346\216\242\347\251\266/index.html" index 43d3c0e5..44ba8db9 100644 --- "a/2023/06/17/\345\257\271GRUB\345\222\214initramfs\347\232\204\345\260\217\346\216\242\347\251\266/index.html" +++ "b/2023/06/17/\345\257\271GRUB\345\222\214initramfs\347\232\204\345\260\217\346\216\242\347\251\266/index.html" @@ -353,7 +353,7 @@

课程学习 - +

diff --git a/archives/2023/02/index.html b/archives/2023/02/index.html index 8e9ee099..905a1a71 100644 --- a/archives/2023/02/index.html +++ b/archives/2023/02/index.html @@ -223,9 +223,12 @@

2023

- +
+ cs144 + 二月 25, 2023 +
@@ -234,12 +237,9 @@

2023

+ -
- cs144 - 二月 25, 2023 -
diff --git a/archives/2023/03/index.html b/archives/2023/03/index.html index 5d422670..2d1a1268 100644 --- a/archives/2023/03/index.html +++ b/archives/2023/03/index.html @@ -168,9 +168,12 @@

2023

- +
+ CMU15445 + 三月 13, 2023 +
@@ -179,12 +182,9 @@

2023

+ -
- CMU15445 - 三月 13, 2023 -
diff --git a/archives/2023/page/2/index.html b/archives/2023/page/2/index.html index fac6a829..b70051d3 100644 --- a/archives/2023/page/2/index.html +++ b/archives/2023/page/2/index.html @@ -257,9 +257,12 @@

2023

- +
+ CMU15445 + 三月 13, 2023 +
diff --git a/archives/2023/page/3/index.html b/archives/2023/page/3/index.html index e885a8fb..b1ddc3d4 100644 --- a/archives/2023/page/3/index.html +++ b/archives/2023/page/3/index.html @@ -146,12 +146,9 @@

2023

+ -
- CMU15445 - 三月 13, 2023 -
diff --git a/archives/2023/page/4/index.html b/archives/2023/page/4/index.html index 4fdc05ca..ce06d61b 100644 --- a/archives/2023/page/4/index.html +++ b/archives/2023/page/4/index.html @@ -146,9 +146,12 @@

2023

- +
+ cs144 + 二月 25, 2023 +
@@ -157,12 +160,9 @@

2023

+ -
- cs144 - 二月 25, 2023 -
diff --git a/archives/page/3/index.html b/archives/page/3/index.html index de7af8e2..aec52180 100644 --- a/archives/page/3/index.html +++ b/archives/page/3/index.html @@ -146,9 +146,12 @@

2023

- +
+ CMU15445 + 三月 13, 2023 +
@@ -157,12 +160,9 @@

2023

+ -
- CMU15445 - 三月 13, 2023 -
diff --git a/archives/page/4/index.html b/archives/page/4/index.html index 0cadc39d..877ddc4b 100644 --- a/archives/page/4/index.html +++ b/archives/page/4/index.html @@ -157,9 +157,12 @@

2023

- +
+ cs144 + 二月 25, 2023 +
@@ -168,12 +171,9 @@

2023

+ -
- cs144 - 二月 25, 2023 -
diff --git a/search.xml b/search.xml index 8553441e..7958534a 100644 --- a/search.xml +++ b/search.xml @@ -73,6322 +73,5846 @@ - 计算机体系结构 - /2024/01/04/arch/ - 01 基本知识
    -
  1. SISD、SIMD、MIMD、向量处理器的基本概念

    -

    向量处理器意思是一条指令可以同时处理多个数据元素(SIMD)(就类似于这几个数据元素组成了一个向量);多发射处理器可以同一时间并行多条指令。

    -
  2. -
  3. 发射与流出

    -

    在计算机体系结构中,”发射”和”流出”是与指令执行有关的两个重要概念,它们描述了处理器在执行指令时的不同阶段和行为。

    + JavaWeb + /2022/12/21/JavaWeb/ + 第一部分 Java基础

    JUnit单元测试

    JUnit是白盒测试。

    +

    简要使用步骤

    定义测试类

    包含各种测试用例。

    +

    一般放在包名xxx.xxx.xx.test里,类名为“被测试类名Test”。

    +

    定义测试方法

    测试方法可以独立运行。

    +

    方法名一般为“test测试的方法”,void,空参。

    +

    给方法加@Test标签

    加入JUnit依赖包

    具体细节

    断言

    Assert.assertEquals(3,result);
    + +

    @Before @After

    @Before在所有测试方法执行前自动执行,常用于资源申请。

    +

    @After在所有测试方法执行完后自动执行,常用于释放资源。

    +

    反射

    反射是框架设计的灵魂。

    +

    Java对象创建的三个阶段

    image-20221205194807395

    +

    类加载器把硬盘中的字节流文件装载进内存,并且翻译封装为Class类对象。通过Class类对象才能创建Person对象。

    +

    而这也就是说,如果我们有了Class对象,我们就可以创建该类对象。

    +

    获取Class对象

    有三种方式。

    +

    Class.forName(“类的全名”)

    将字节码文件加载进内存,返回class对象。多用于配置文件【将类名定义在配置文件】

    +

    注意:类的全名指的是包.类,包含包名。

    +

    类型.class

    通过类名的属性class获取。多用于参数传递。

    +

    对象.getClass()

    getClass()是Object类的方法。多用于对象的获取字节码的方式。

    +
    //第一种方式
    Class class1 = Class.forName("Student");
    System.out.println(class1);
    //第二种方式
    Class class2 = Student.class;
    System.out.println(class2);
    //第三种方式
    Student stu = new Student();
    Class class3 = stu.getClass();
    System.out.println(class3);

    System.out.println((class1==class2)+" "+(class2==class3));

    /*输出
    class Student
    class Student
    class Student
    true true*/
    + +

    同一个字节码文件(*.class)在一次程序运行过程中只会被加载一次,不管是以哪种方式得到的Class对象,都是同一个。

    +

    使用Class对象

    可以通过class对象得到其字段、构造方法、方法等。

    +
    class Student{
    public String name;
    int birthday;
    protected int money;
    private double weight;

    private Student() {

    }
    public Student(String name, int birthday, int money, double weight) {
    this.name = name;
    this.birthday = birthday;
    this.money = money;
    this.weight = weight;
    }

    @Override
    public String toString() {
    return "Student{" +
    "name='" + name + '\'' +
    ", birthday=" + birthday +
    ", money=" + money +
    ", weight=" + weight +
    '}';
    }

    public static void haha(){
    System.out.println("haha");
    }
    public void hello(){
    System.out.println("hello");
    }
    public void hello(String name){
    System.out.println("hello,"+name+"!");
    }
    private void giveMoney(){
    System.out.println("my money is yours...");
    money = 0;
    }
    }
    + +
    Student stu = new Student("张三",321,1000,57.7);
    Class stuC = stu.getClass();
    + +

    字段

    获取字段

    常用方法:

    +
    //获取所有公有字段
    Field[] fs = stuC.getFields();
    //获取某个公有字段
    Filed f = stuC.getField("name");
    //获取所有字段
    Field[] fs = stuC.getDeclaredFields();
    //获取某个字段
    Filed f = stuC.getDeclaredField("name");
    /*输出
    public java.lang.String Student.name

    public java.lang.String Student.name

    public java.lang.String Student.name
    int Student.birthday
    protected int Student.money
    private double Student.weight*/
    + +
    使用字段
    Field f = stuC.getDeclaredField("money");
    //由于money protected,故应该先设置其为可访问,否则会抛出异常。
    //这是暴力反射,不推荐。
    f.setAccessible(true);
    //获取成员变量的值
    //注意此处是要用field.get(该类型对象)的。想想也有道理,Filed字段是属于Class对象的,因而你想获取某个对象的值当然得传入该对象。
    System.out.println(f.get(stu));
    //设置对象的值
    f.set(stu,0);
    System.out.println(stu);
    /* Student{name='张三', birthday=321, money=0, weight=57.7} */
    + +

    构造方法

    获取方法跟上面格式差不多。

    +
    //获取构造方法
    //获取无参私有构造方法
    Constructor cons = stuC.getDeclaredConstructor();
    cons.setAccessible(true);
    System.out.println(cons.newInstance());
    //获取有参构造方法
    Constructor cons2 = stuC.getConstructor(String.class,int.class,int.class,double.class);
    System.out.println(cons2.newInstance("李四",102,100,90.9));
    /*输出
    Student{name='null', birthday=0, money=0, weight=0.0}
    Student{name='李四', birthday=102, money=100, weight=90.9}*/
    + +

    如果想要获取公有的无参构造器,还可以使用Class类提供的更简单的方法,不用先创造构造器:

    +
    System.out.println(stuC.newInstance());
    + +

    方法

    //获取方法
    //可以获取静态方法
    Method m = stuC.getMethod("haha");
    System.out.println(m);
    //获取带参方法,自动根据参数推断
    Method m2 = stuC.getMethod("hello");
    Method m3 = stuC.getMethod("hello",String.class);
    //调用方法
    m2.invoke(stu);
    m3.invoke(stu,"琳琳");
    //获取私有方法并调用
    Method m4 = stuC.getDeclaredMethod("giveMoney");
    m4.setAccessible(true);
    m4.invoke(stu);
    /*输出
    public static void Student.haha()
    hello
    hello,琳琳!
    my money is yours...*/
    + +

    使用反射的案例

    image-20221205205122805

    +
    public class ReflectTest {
    public static void main(String[] args) throws Exception {
    /* 加载配置文件 */
    Properties pro = new Properties();
    //获取字节码文件加载器
    ClassLoader cl = ReflectTest.class.getClassLoader();
    pro.load(cl.getResourceAsStream("pro.properties"));

    /* 获取配置文件中的数据并执行 */
    Class cla = Class.forName(pro.getProperty("className"));
    cla.getMethod(pro.getProperty("methodName")).invoke(cla.newInstance());
    }
    }
    + + + +

    注解

    image-20221205211932790

    +

    image-20221205212028989

    +

    ①和③都是jdk预定义的。自定义主要是②。

    +

    生成doc文档

    javadoc XXX.java
    + +

    会自动根据里面的注解生成文档

    +

    JDK预定义注解

    image-20221205212443918

    +

    自定义注解

    注解类的本质

    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.SOURCE)
    public @interface Override {
    }
    + +

    本质上

    +
    public @interface Override{}
    + +

    等价于

    +
    public interface Override extends java.lang.annotation.Annotation{}
    + +

    注解的属性

    注解的属性就是接口中的成员方法。要求无参,且返回类型有固定取值:

    +

    image-20221205213135522

    +
    public @interface MyAnno {
    String name() default "haha";
    }
    + +
    @MyAnno(name = "haha")
    public class Student{}
    + +

    元注解

    描述注解的注解

    +

    image-20221205213324052

    +

    RetentionPolicy的三个取值:SOURCE、CLASS、RUNTIME,正对应着java对象的三个阶段。

    +

    SOURCE:不保留到字节码文件,会被编译器扔掉

    +

    CLASS:保留到字节码文件

    +

    RUNTIME:被读到

    +

    自定义的注解一般都取RUNTIME

    +

    在程序中获取注解属性

    相当于用注解替换配置文件

    +
    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface Pro {
    String className();
    String methodName();
    }
    //保留在runtime应该是因为运行时要动态获取值。我试了一下换成CLASS或者SOURCE,会有NullPointerException
    + +
    @Pro(className = "Student",methodName = "hello")
    public class ReflectTest {
    public static void main(String[] args) throws Exception{
    Pro pro = ReflectTest.class.getAnnotation(Pro.class);

    Class cla = Class.forName(pro.className());
    cla.getMethod(pro.methodName()).invoke(cla.newInstance());
    }
    }
    + +

    class.getAnnotation(Pro.class);这句话实质上是创建了一个实例,继承了Pro接口,重载了里面的抽象方法。

    +

    使用案例:测试框架

    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface Check {
    }
    + +

    然后在要测试的每个方法上面加上此标签。

    +

    image-20221205215017285

    +

    然后编写test方法:

    +
    public class TestCheck {
    public static void main(String[] args) throws IOException {
    //创建对象
    Calculator c = new Calculator();
    //获取所有方法
    Method[] methods = c.getClass().getMethods();

    //写入文件
    int number = 0;//异常次数
    BufferedWriter bf = new BufferedWriter(new FileWriter("bug.txt"));

    //检查每个方法是否有注解。有的话则执行。
    for (Method m : methods){
    if (m.isAnnotationPresent(Check.class)){
    try {
    m.invoke(c);
    } catch (Exception e) {
    //记录文件信息
    number++;
    bf.write(m.getName()+"出异常了。");
    bf.write("\n");
    bf.write(e.getCause().getClass().getSimpleName()+" "+e.getCause().getMessage());
    bf.write("\n");
    bf.write("-------------");
    bf.write("\n");
    }
    }
    }
    bf.write("共出现"+number+"次异常");
    bf.flush();
    bf.close();
    }
    }
    /*
    haha出异常了。
    ArithmeticException / by zero
    -------------
    共出现1次异常
    */
    + +

    image-20221205220053258

    +

    第二部分 数据库

    Mysql

    登录方式

    mysql -h[IP地址] -u[用户名] -p
    + +

    文件结构

    本地的一个文件夹就代表一个数据库,文件夹里的一个文件代表一张表。

    +

    image-20221205221041508

    +

    image-20221205221055114

    +

    SQL语法

    image-20221205221216882

    +

    SQL有四种语句类型

    +

    image-20221205221246672

    +

    DDL 操作数据库、表

    操纵数据库

    create datebase 数据库名称;
    create datebase if not exists 数据库名称;
    create datebase if not exists 数据库名称 character set gbk;
    + +
    drop database 数据库名称;
    drop database if exists 数据库名称;
    + +
    alter database 数据库名称 charactor set 修改后新值;
    + +
    show databases;# 查询所有数据库名称
    show create database 数据库名称;# 显示指定数据库创建时的指令内容
    + +
    使用
    select database();# 查询正在使用的数据库名称
    use 数据库名称;
    + +

    操纵表

    create table students(
    name varchar(20),
    age int,
    score double(3,1),
    birthday date,
    insert_time timestamp
    );# 创建表
    create table students2 like students;# 复制表
    + +

    *注:

      -
    1. 发射(Issue):
        -
      • “发射”指的是将指令从指令流中发送到处理器的执行部件或执行单元,以进行实际的执行。
      • -
      • 发射阶段通常是在取指令和解码指令之后,将指令发送到执行单元的过程。
      • -
      • 多发射处理器意味着多条指令可以同时进入执行阶段,通过并行执行提高处理器的性能
      • -
      -
    2. -
    3. 流出(Out-of-Order Execution):
        -
      • “流出”是指处理器在执行过程中允许指令乱序执行,即不按照它们在程序中的原始顺序执行。在乱序执行的情况下,处理器会通过重新排序指令来填充执行单元的空闲周期,以提高整体性能。
      • -
      • 多流出处理器采用乱序执行的方式,允许在执行单元空闲时执行无关的指令,以最大程度地利用执行单元的并行性。
      • -
      +
    4. mysql的数据类型表

      +

      image-20221205222127740

      +

      其中:

      +

      ① double(3,1)表示XXX.X,最大值为99.9.

      +

      ② 关于三个时间类型

      +

      image-20221205222307284

      +

      所以timestamp常用作插入时间。

      +

      ③ varchar(20)表示二十个字符长的字符串。

      +

      注意,是“二十个字符”而不是“二十个字节”。如果使用的字符集每个字符占3个字节,则varchar(20)占60个字节。

      +

      ④ BLOB、CLOB、二进制这些用于存储大数,不常用

    -

    这两个概念都涉及到提高指令级并行性,但它们描述了处理器在执行阶段的不同方面。发射强调在同一时钟周期内同时发送多条指令,而流出强调在执行过程中的乱序执行策略。

    -
  4. -
  5. tensor 张量

    -

    sparse tensor 稀疏张量

    -
  6. -
  7. 异构计算

    -

    指的是在同一系统中集成多种不同体系结构或架构的处理器和计算设备,以便更有效地处理各种类型的任务。这包括集成不同类型的中央处理单元(CPU)、图形处理单元(GPU)、加速器、协处理器等。异构计算的目标是充分发挥各种处理器的优势,以提高整体系统性能和能效。

    -

    其关键概念有协处理器等等等。

    +
    drop table 表名;
    + +
    # 修改表名
    alter table students rename to new_students;
    # 修改表的字符集
    alter table students character set 字符集名称;
    # 修改表的列名/类型
    alter table students change name new_name varchar(20);# 新列名 新数据类型
    alter table students modify name varchar(15);# 新数据类型
    # 添加一列
    alter table students add ID double(10);
    # 删除一列
    alter table students drop ID;
    + +
    show tables;# 查询数据库中所有表的名字
    desc 表名;# 查询某个表的结构
    + + + +

    DML 增删改表中数据

    insert into students(name,age,score,birthday) values('张三',15,99.9,"2022-12-5");
    insert into students values("张三",15,99.9,"2022-12-5",NULL);
    + +

    如果不加条件,会把表中所有数据删除

    +
    delete from students where name="张三";
    truncate table students;# 删除表,然后创建一张一模一样的新表
    + +

    image-20221205224600869

    +

    如果不加条件,会把表中所有记录全部修改

    +
    update students set name="1", age=10 where name="张三";
    + + + +

    DQL 查询表中记录

    语法

    image-20221205224802814

    +

    基础查询

    select # 多字段查询
    name,
    age
    from
    students;

    select distinct # 去重
    address
    from
    students;

    # 有NULL参与的计算结果都为NULL
    select name,math,english,math+english from students;
    # ifnull函数不会修改原表中的数据
    select name,math,english,IFNULL(math,0)+IFNULL(english,0) from students;

    select
    name,
    math,
    english,
    IFNULL(math,0)+IFNULL(english,0) total_score # 起别名
    from
    students;
    + +

    条件查询

    运算符
      +
    1. 基本运算符

      +

      <、>、=、<=、>=、<>(不等于,也可以用!=)

    2. -
    -

    02 现代处理器体系结构

    img

    -

    img

    -

    例题

    题型1 生成指令序列,分析时间

    1

    img

    -

    注意几点:

    -
      -
    1. 变量需要通过LD指令载入到寄存器
    2. -
    -

    2

    img

    -

    img

    -

    注意,它的意思是LD、SD、DADDIU都只占1个时钟周期,ADD占2个

    -

    img

    -

    感觉这么个例题下来,我就懂了循环展开的作用了

    -

    题型2 换名/消除WAR WAW

    1

    img

    -

    2

    img

    -

    题型3 记分牌

    img

    -

    这里的结构相关值得注意

    -

    做这种题的套路是,需要明确它要求的时刻时的情况,并且依照以下规则判断即可:

    -
      -
    1. 指令状态表

      -
        -
      1. 流出

        -

        无结构冲突、无WAW冲突

        -

        如① 当MULT准备写回时,此时前两条L必定流出,然后后面的SUB、DIV、ADD都没有结构冲突和WAW冲突,所以全部流出。只不过ADD和DIV会卡在读操作数阶段

        -

        ② 由①可知全部流出

        +
      2. 逻辑运算符

        +

        AND、OR

      3. -
      4. 读操作数

        -

        操作数可用时完成该阶段

        -

        如① 此时前三条必定完成。并且SUB也完成了,所以ADD也完成了读数阶段。只有DIV还在等待mul的结果

        -

        ② 此时大伙差不多都结了,没什么好说的

        +
      5. BETWEEN AND

      6. -
      7. 执行

        -

        纯纯的算术

        -

        如① 除了除法别的都完了,没什么好说的

        -

        ② 全部都结了

        +
      8. IN后跟集合

        +

        image-20221205230824086

      9. -
      10. 写结果

        -

        不存在WAR则写入

        -

        如① 前两个肯定完成了,然后SUB也结了,ADD存在WAR,所以最后是ADD和MUL没完成。

        -

        ② 除了DIV全部结了

        +
      11. IS、IS NOT

        +

        image-20221205230902110

      12. +
      13. LIKE 模糊查询

        +

        类似正则使用占位符匹配

        +

        image-20221205231037021

        +
        select * from students where name like "马%";
      +

      各种函数一样的东西

      排序函数
      order by 排序字段1 排序方式1,排序字段2 排序方式2;
      + +
        +
      1. 默认升序。

      2. -
      3. 功能部件状态表

        -

        记住这些字母的含义即可:

        -
          -
        • Busy:yes/no
        • -
        • Op:操作编码
        • -
        • Fi:目的寄存器编号
        • -
        • Fj,Fk:源寄存器编号
        • -
        • Qj,Qk:正在计算Fj和Fk的功能部件
        • -
        • Rj,Rk:Fj和Fk是否就绪且还没被取走
        • -
        +
      4. ASC、DESC

      5. -
      6. 寄存器状态表

        -

        每个寄存器有一项,用于指出哪个功能部件将把结果写入

        +
      7. 多关键字排序

        +

        image-20221205231606255

        +

        第二条件仅当第一条件一样才使用。

      -

      img

      -

      img

      -

      题型4 Tomasulo算法

      3段流水

      -
        -
      1. 流出

        +
        聚合函数

        将一列数据作为整体,纵向计算

        +

        注意,聚合函数的计算会排除NULL值。如果不想让空置排除,可以尝试该方法:

        +
        select count(ifnull(math,0)) from students;
        +
          -
        1. 没有结构冲突就流出,填进保留站

          -

          一般有ADD1,ADD2,ADD3(加减),MUL1,MUL2(乘除),LD1,LD2(SL)

          +
        2. count 计算个数

          +
          select count(name) from students;# 有多少条记录
          + +

          一般如果要看有多少记录,可以用count(主键),因为主键不为空。

        3. -
        4. 具体填什么看操作数有没有就绪

          +
        5. max、min

        6. -
        -

        保留站有以下字段:

        -
          -
        • Op:操作

          -
        • -
        • Qj,Qk:操作数保留站号

          -
        • -
        • Vj,Vk:源操作数值

          -

          load的Vk保存偏移量

          -
        • -
        • Busy

          -
        • -
        • A:存放立即数字段 or 有效地址,仅用于load和store缓冲器

          -
        • -
        • Qi:寄存器状态表

          -

          存放要写入它的保留站ID

          -
        • -
        -
      2. -
      3. 执行

        -

        两个操作数就绪后就执行

        +
      4. sum 求和

      5. -
      6. 写结果

        -

        计算完毕后由CDB传送

        +
      7. avg 平均值

      -

      例题

      img

      -

      img

      -

      这里不知道为什么LD2没有跟LD1同时完成?限制了一个时钟周期只流出一条指令吗

      -

      img

      -

      这里可以注意其特点是结果一经算出全部写回

      -

      img

      -

      img

      -

      img

      -

      img

      -

      通过换名避免了WAR,而不是像记分牌那样通过等待

      -

      img

      -

      题型5 Tomasulo+前瞻执行

      4段流水

      -
        -
      1. 流出

        -
          -
        1. 保留站&ROB都有空闲才流出

          -

          一般有ADD1,ADD2,ADD3(加减),MUL1,MUL2(乘除),LD1,LD2(SL)

          -
        2. -
        3. 具体填什么看操作数有没有就绪

          -
        4. +
          分组查询

          分组之后查询的字段只能是两种:① 分组字段 ② 聚合函数。因为分组了之后再查具有个人特色的东西就没意义了。【高版本的mysql如果查询别的字段会报错】

          +
          select sex,avg(math) from students group by sex;
          + +

          image-20221207212103151

          +
          对分组结果进行条件限制

          还可以在分组前对条件限定,使用WHERE

          +
          select sex,avg(math) from students where math >= 70 group by sex;
          + +

          或者在分组后限定,使用HAVING

          +
          select sex,avg(math),count(id) from students group by sex having count(id)>2;
          # 或
          select sex,avg(math),count(id) total from students group by sex having total>2;
          + +
          WHERE和HAVING的区别
            +
          1. WHERE在分组前限定,不满足where则不参与分组;HAVING在分组后限定,不满足having则不会被查询出来。
          2. +
          3. WHERE条件里不能有聚合函数,HAVING可以。
          -

          保留站有以下字段:

          -
            -
          • Op:操作

            +
            分页查询

            image-20221207213958848

            +

            这种就是分页查询。

            +
            limit 开始的索引,每页查询的条数;
            + +

            limit只能在mysql使用。

            +

            DCL 管理用户,授权操作

            管理用户

            查询用户

            image-20221219223932789

            +

            用户表存放地点↑

            +
            USE mysql;
            SELECT * FROM USER;
            + +
            创建用户

            注意,以下出现的”用户名”@”主机名” IDENTIFIED BY “密码”,不能在@两侧加空格,否则报错。

            +
            CREATE USER "用户名"@"主机名" IDENTIFIED BY "密码";
            + +
            删除用户
            DROP USER "用户名"@"主机名";
            + +
            修改密码
            -- 使用mysql自带的密码加密函数PASSWORD
            -- 1
            UPDATE USER SET PASSWORD = PASSWORD("新密码") WHERE USER = "用户名";
            -- 2
            SET PASSWORD FOR "用户名"@"主机名" = PASSWORD("新密码");
            + +

            image-20221219225036633

            +

            授权操作

            查询权限
            SHOW GRANTS FOR "root"@"%";
            + +
            授予权限
            grant 权限列表 on 数据库名.表名 to '用户名'@'主机名';
            + +

            image-20221219225715075

            +
            撤销权限
            revoke 权限列表 on 数据库名.表名 from '用户名'@'主机名';
            + +

            约束

            非空约束

            添加非空约束

            CREATE TABLE stu(
            id INT,
            name VARCHAR(20) NOT NULL
            );
            ALTER TABLE stu MODIFY name VARCHAR(20) NOT NULL;
            + +

            删除非空约束

            如果要去掉该约束,可以这么做:

            +
            ALTER TABLE stu MODIFY name VARCHAR(20);
            + +

            由于我们没写“NOT NULL ”,所以非空约束就被去掉了。感觉这点的解释挺有意思的。

            +

            唯一约束

            添加唯一约束

            某列值不能重复

            +
            CREATE TABLE stu(
            id INT,
            phone_number VARCHAR(20) UNIQUE
            );
            ALTER TABLE stu MODIFY phone_number VARCHAR(20) UNIQUE;
            + +

            但是注意,唯一约束允许多个NULL存在

            +

            删除唯一约束

            唯一约束的删除方法跟前面的非空约束就完全不一样了。

            +
            ALTER TABLE stu DROP INDEX phone_number;
            + +
            +

            创建唯一约束时会自动创建唯一索引,需要删除索引

            +
            +

            主键约束

            一张表只能有一个主键。主键非空且唯一。

            +

            添加主键约束

            CREATE TABLE stu(
            id INT PRIMARY KEY,
            name VARCHAR(20)
            );
            ALTER TABLE stu MODIFY id INT PRIMARY KEY;
            + +

            删除主键约束

            ALTER TABLE stu DROP PRIMARY KEY;
            + +

            自动增长

            这东西一般都跟主键结合使用。

            +

            若某一列是数值类型,可以使用auto_increment关键字来完成值的自动增长。

            +
            CREATE TABLE stu(
            id INT PRIMARY KEY AUTO_INCREMENT,
            NAME VARCHAR(20)
            );
            INSERT INTO stu VALUES(NULL,'111');# 设NULL或自己指派都行。
            + +

            自动增长的数据只跟上一个记录有关系。

            +

            外键约束

            引言

            image-20221207222232057

            +

            表中dep_name和dep_location有数据冗余,修改或者插入都不方便,不符合数据库设计准则,所以需要创造两张表。

            +

            image-20221207222418827

            +

            image-20221207222439652

            +

            但要是你想裁员了,直接在第二个表删研发部是没用的,第一个表数据还在,还得麻烦地一个个删。这时候外键就起作用了。

            +

            添加外键约束

            外键只能关联唯一约束或者主键约束的列。一般外键都是去关联主表的主键。

            +

            image-20221207223758950

            +
            CREATE TABLE employee(
            id INT PRIMARY KEY AUTO_INCREMENT,
            NAME VARCHAR(20),
            age INT,
            dep_id INT, -- 外键列
            CONSTRAINT emp_dept_fk FOREIGN KEY (dep_id) REFERENCES department(id) -- 外键声明
            );

            ALTER TABLE employee ADD CONSTRAINT emp_dept_fk FOREIGN KEY (dep_id) REFERENCES department(id) -- 外键声明
            + +

            此时不能删除department的行【要是该行在employee表没出现过的话就可以删掉】,也不能在employee添加一个不存在的外键值。

            +

            删除外键约束

            ALTER TABLE employee DROP FOREIGN KEY emp_dept_fk;
            + +

            外键级联

            如果你想修改外表主键值,就需要用到级联更新。

            +
            ALTER TABLE employee ADD CONSTRAINT emp_dept_fk FOREIGN KEY (dep_id) REFERENCES department(id) ON UPDATE CASCADE ;-- 外键声明+级联更新声明
            + +

            如果你想达到删除一个主键值就能删除表中的所有与该主键值关联的数据,就需要用到级联删除。

            +
            ALTER TABLE employee ADD CONSTRAINT emp_dept_fk FOREIGN KEY (dep_id) REFERENCES department(id) ON DELETE CASCADE ;-- 外键声明+级联删除声明
            + +

            级联使用应该要谨慎。一是它不大安全,二是它涉及多表操作,效率低下

            +

            多表关系与范式

            多表关系

            image-20221219161901631

            +

            image-20221219161847190

            +

            image-20221219161853190

            +

            范式

            image-20221219162311785

            +

            image-20221219162646556

            +

            1NF

            image-20221219162034996

            +

            image-20221219162047446

            +

            2NF

            1NF中的主属性为学号和课程名称。可以看到,分数完全依赖于码,但是姓名、系名、系主任都只是部分依赖于码,这不符合2NF的条件。因而,我们就可以选择拆分表,把完全依赖的部分和部分依赖的部分分开:

            +

            由于分数->(学号,课程名称),因而可以把学号、课程名称、分数放在一张表

            +

            由于姓名、系名、系主任 ->(学号),因而可以把学号、姓名、系名、系主任放在一张表

            +

            如下图所示。这样就消除了部分依赖。

            +

            图片2

            +

            3NF

            2NF中选课表的主属性为学号和课程名称,学生表的主属性为学号。可以看到,学生表中,存在着系主任->系名->学号这样的传递依赖,不符合3NF的规定。因而,我们需要对学生表进行进一步的拆分。

            +

            我们为了破坏系主任->系名->学号这个传递链,可以拆分成系主任->系名和系名->学号两个传递关系。

            +

            因而,可以把学生表拆分为如下图两张表:

            +

            image-20221219162231385

            +

            多表查询

            内连接查询

            隐式内连接

            使用where条件

            +
            -- 查询所有员工信息和对应的部门信息
            SELECT * FROM emp,dept WHERE emp.`dept_id` = dept.`id`;
            -- 进行去重就成为了自然连接:
            SELECT emp.*,dept.`NAME` FROM emp,dept WHERE emp.`dept_id` = dept.`id`;
            + +
            +

            此在书中称为“等值连接和非等值连接”。

            +
            +

            显式内连接

            语法: select 字段列表 from 表名1 [inner] join 表名2 on 条件

            +
            SELECT * FROM emp INNER JOIN dept ON emp.`dept_id` = dept.`id`;	
            + +

            外连接查询

            +

            关于外连接和内连接的区别,以及左外连接与右外连接的区别:

            +

            image-20221219155724391

            +

            ![屏幕截图 2022-12-19 155750](./JavaWeb/屏幕截图 2022-12-19 155750.png)

            +

            image-20221219155846157

            +
            +

            左外连接

            语法:select 字段列表 from 表1 left [outer] join 表2 on 条件;

            +
            SELECT 	t1.*,t2.`name` FROM emp t1 LEFT JOIN dept t2 ON t1.`dept_id` = t2.`id`;
            + +

            右外连接

            语法:select 字段列表 from 表1 right [outer] join 表2 on 条件;

            +
            SELECT 	* FROM dept t2 RIGHT JOIN emp t1 ON t1.`dept_id` = t2.`id`;
            + +

            子查询

            查询嵌套

            +
            +

            子查询中不允许使用ORDER BY

            +

            在实际运用中,内连接比子查询的效率更高

            +
            +

            不相关子查询

            +

            image-20221219160800762

            +
            +
            子查询结果单行单列

            可用于WHERE条件

            +
            -- 查询工资最高的员工信息
            SELECT * FROM emp WHERE emp.salary = (SELECT MAX(salary) FROM emp);
            + +
            子查询结果多行单列

            可以作为条件用IN关键字

            +
            -- 查询'财务部'和'市场部'所有的员工信息
            SELECT * FROM emp WHERE dept_id IN (SELECT id FROM dept WHERE name = "财务部" OR name = "市场部");
            + +
            子查询结果多行多列

            可以当做一个新表,可以转化为普通内连接

            +
            -- 查询员工入职日期是2011-11-11日之后的员工信息和部门信息
            -- 嵌套查询
            SELECT *
            FROM dept, (
            SELECT *
            FROM emp
            WHERE emp.join_data > '2011-11-11'
            )
            WHERE dept.id = emp.dept_id;
            -- 内连接
            SELECT * FROM emp,dept WHERE emp.join_data > '2011-11-11' AND emp.dep_id = dept.id;
            + +

            相关子查询

            +

            image-20221219160653496

            +

            子查询内部使用了父查询的东西

            +

            image-20221219161626542

            +

            image-20221219161637827

            +
            +

            事务

            基本介绍

            概念

            一个包含多个步骤的业务操作被事务管理,操作要么同时成功,要么同时失败。【有种原子操作的感觉?】

            +

            image-20221219214157761

            +

            当操作失败时,会回滚到执行前的状态。

            +

            事务操作

            事实上就是类似有个缓冲区,得到commit指令就把缓冲区内容更新,得到rollback指令就把缓冲区内容丢弃。

            +
            -- 开启事务
            START TRANSACTION;

            -- 操作序列:转账500元
            UPDATE usr SET money = money - 500 WHERE uname = "Mary";
            UPDATE usr SET money = money + 500 WHERE uname = "Lily";

            -- 提交
            COMMIT;
            + +
            -- 开启事务
            START TRANSACTION;

            -- 操作序列:转账500元
            UPDATE usr SET money = money - 500 WHERE uname = "Mary";
            出错了
            UPDATE usr SET money = money + 500 WHERE uname = "Lily";

            -- 出错则回滚
            ROLLBACK;
            + +
            开启事务
            START TRANSACTION;
            + +
            回滚事务
            ROLLBACK;
            + +
            提交事务
            COMMIT;
            + +

            一条DML(增删改表中数据)语句默认会自动提交。但如果手动开启了事务,那么事务内保护的原子序列就需要手动提交。

            +

            如果想将默认提交给kill了,也即不论是否开启事务都得手动提交,那么就需要用如下语句:

            +
            SET @@autocommit = 0;
            + +

            事务的四大特征

            原子性

            持久性

            一旦事务提交/回滚,会持久性更新数据库表。

            +

            隔离性

            多个事务之间应该相互独立。为了保障这一点,需要设置事务的隔离级别。

            +

            一致性

            事务操作前后数据总量不变。

            +

            事务的隔离级别

            概念:多个事务之间隔离的,相互独立的。但是如果多个事务操作同一批数据,则会引发一些问题,设置不同的隔离级别就可以解决这些问题。【有点并发的感觉】

            +

            image-20221219221141605

            +

            隔离级别越高,安全性越高,效率越来越差。

            +

            mysql默认的是3,oracle默认的是2.

            +

            可以设置隔离级别。

            +
            set global transaction isolation level  "级别字符串";
            + +
            +

            通过之后老师说的内容,感觉有了点个人的感悟:

            +

            级别1寻找数据可能优先从缓冲区找;级别2相当于不能读到缓冲区内容;级别3可能相当于在开启事务前对表做了个快照?级别4应该就是直接上了把互斥锁,同一时刻只能一个事务读写。

            +
            +

            JDBC

            概念

            Java Database Connectivity Java语言操作数据库

            +

            image-20221220141025613

            +

            image-20221220141214259

            +

            快速入门

              +
            1. 导入驱动jar包

              +

              ① 新建libs目录

              +

              ② 把jar包复制到libs目录下

              +

              ③ 右键libs目录 add as library

            2. -
            3. Qj,Qk:操作数保留站号

              +
            4. 注册驱动

            5. -
            6. Vj,Vk:源操作数值

              -

              load的Vk保存偏移量

              +
            7. 获取数据库连接对象 Connection

            8. -
            9. Busy

              +
            10. 定义sql语句

            11. -
            12. A:存放立即数字段 or 有效地址,仅用于load和store缓冲器

              +
            13. 获取执行sql语句的对象 Statement

            14. -
            15. Qi:寄存器状态表

              -

              存放要写入它的保留站ID

              +
            16. 执行sql,接收返回的结果

            17. -
          +
        5. 处理结果

        6. -
        7. 执行

          -

          两个操作数就绪后就执行

          +
        8. 释放资源

        9. -
        10. 写结果

          -
            -
          1. 写入ROB,CDB传送ROB编号到保留站
          2. -
          3. 释放产生该结果的保留站
          -

          ROB字段:

          -
            -
          • 指令类型

            -
          • -
          • 目标地址

            -

            目标寄存器/存储器单元地址

            -
          • -
          • 数据值字段

            -

            前瞻结果

            -
          • -
          • 就绪字段

            -

            结果是否就绪

            -
          • -
          -
        11. -
        12. 指令确认

          -

          分支结果出来后确认

          +
          public class JdbcDemo1 {
          public static void main(String[] args) throws ClassNotFoundException, SQLException {
          //1 注册驱动
          Class.forName("com.mysql.jdbc.Driver");
          //2 获取数据库连接对象
          Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/helloworld","root","root");
          //3 定义sql语句
          String sql = "update usr set money = 5000 where id = 1";
          //4 获取执行对象 Statement
          Statement stmt = conn.createStatement();
          //5 执行sql
          int count = stmt.executeUpdate(sql);
          //6 处理结果
          System.out.println(count);
          //7 释放资源
          stmt.close();
          conn.close();
          }
          }
          + +

          优化版【增加try-catch-finally】:

          +
          public class JdbcDemo1 {
          public static void main(String[] args) {
          //1 注册驱动
          //提升作用域,放在try外面
          Connection conn = null;
          Statement stmt = null;
          ResultSet resultSet = null;
          try {
          Class.forName("com.mysql.jdbc.Driver");
          //2 获取数据库连接对象
          conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/helloworld","root","root");
          //3 定义sql语句
          String sql = "select * from usr";
          //4 获取执行对象 Statement
          stmt = conn.createStatement();
          //5 执行sql
          resultSet = stmt.executeQuery(sql);
          //6 处理结果
          if (resultSet == null)
          System.out.println("修改失败");
          else {
          while(resultSet.next()){
          System.out.println(resultSet.getInt(1)+" "
          +resultSet.getString(2)+" "
          +resultSet.getInt(3));
          }
          }

          } catch (ClassNotFoundException e) {
          throw new RuntimeException(e);
          } catch (SQLException e) {
          throw new RuntimeException(e);
          } finally {
          if (resultSet != null){
          try {
          resultSet.close();
          } catch (SQLException e) {
          throw new RuntimeException(e);
          }
          }
          //为了避免空指针异常
          if (stmt != null) {
          try {
          stmt.close();
          } catch (SQLException e) {
          throw new RuntimeException(e);
          }
          }
          if (conn != null){
          try {
          conn.close();
          } catch (SQLException e) {
          throw new RuntimeException(e);
          }
          }
          }
          }
          }
          + +

          详解各个类

          DriverManager

          注册驱动

          目的是告诉程序该使用哪一个数据库驱动jar包

          +

          在快速入门中,我们使用这一行来注册驱动:

          +
          Class.forName("com.mysql.jdbc.Driver");
          + +

          表面上看跟DriverManager类可以说是毫无关系。

          +

          但其实,类加载器加载类的时候,其实是会自动执行类中的静态代码块的。Driver类中有一段静态代码块如下:

          +
          static {
          try {
          DriverManager.registerDriver(new Driver());
          } catch (SQLException var1) {
          throw new RuntimeException("Can't register driver!");
          }
          }
          + +

          可见,注册驱动其实主要任务是由DriverManager类干的。这个静态块仅仅用于简化代码书写。

          +
          +

          注意:mysql5之后的版本,这一步注册驱动可以省略。

          +

          image-20221220151045275

          +

          配置文件里自动帮你注册了。我想原理应该是让本文件的类自动加载。

          +
          +
          获取数据库连接
          Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/helloworld","root","root");
          + +

          url的语法:”jdbc:mysql://IP地址:端口号/数据库名称”

          +

          image-20221220151425953

          +

          Connection

          数据库连接对象。

          +
          获取Statement对象
          Statement createStatement() throws SQLException;
          PreparedStatement prepareStatement(String sql) throws SQLException;
          + +
          管理事务
          开启事务
          void setAutoCommit(boolean autoCommit) throws SQLException;
          + +

          设置参数为false即开启事务。也即关闭自动提交。

          +
          提交事务
          void commit() throws SQLException;
          + +
          回滚事务
          void rollback() throws SQLException;
          + +

          Statement

          +

          The object used for executing a static SQL statement and returning the results it produces.执行静态sql

          +
          +
          执行sql
          //执行任意语句
          boolean execute(String sql) throws SQLException;
          /*
          执行DML(增删改表中数据)和DDL(表和库)语句
          返回值:影响到的行数。
          */
          int executeUpdate(String sql) throws SQLException;
          //执行DQL(查询表记录)语句
          ResultSet executeQuery(String sql) throws SQLException;
          + +

          ResultSet

          封装查询结果集。

          +

          具体取数方法就是类似迭代器原理。next移动迭代器指针,getXxx()方法,Xxx是数据类型,得到该行表记录中对应列对应数据类型的值。可以传入列数或者列名。

          +
          boolean next() throws SQLException;
          String getString(int columnIndex) throws SQLException;
          boolean getBoolean(int columnIndex) throws SQLException;
          //...
          long getLong(int columnIndex) throws SQLException;
          //...
          /*@param:
          columnLabel – the label for the column specified with the SQL AS clause.
          If the SQL AS clause was not specified, then the label is the name of the column
          */
          String getString(String columnLabel) throws SQLException;
          + +

          使用实例:

          +

          image-20221220164414363

          +
          public static Collection<Client> query(){
          ArrayList<Client> clients = new ArrayList<>();
          Connection conn = null;
          Statement stmt = null;
          ResultSet resultSet = null;
          try {
          Class.forName("com.mysql.jdbc.Driver");
          conn = DriverManager.getConnection("jdbc:mysql:///helloworld","root","root");
          stmt = conn.createStatement();
          resultSet = stmt.executeQuery("select * from usr");
          if (resultSet == null)
          System.out.println("查询失败");
          else
          while(resultSet.next())
          clients.add(new Client(resultSet.getInt(1),
          resultSet.getString(2),
          resultSet.getInt(3)));
          } catch (ClassNotFoundException e) {
          throw new RuntimeException(e);
          } catch (SQLException e) {
          throw new RuntimeException(e);
          } finally {
          if (resultSet != null){
          try {
          resultSet.close();
          } catch (SQLException e) {
          throw new RuntimeException(e);
          }
          }
          //为了避免空指针异常
          if (stmt != null) {
          try {
          stmt.close();
          } catch (SQLException e) {
          throw new RuntimeException(e);
          }
          }
          if (conn != null){
          try {
          conn.close();
          } catch (SQLException e) {
          throw new RuntimeException(e);
          }
          }
          }
          return clients;
          }
          + +

          注意:

            -
          1. 猜测对 写入寄存器/存储器,释放ROB
          2. -
          3. 猜测错 从另一条路径开始重新执行,清空ROB
          4. -
          +
        13. 这东西也得Close

        14. +
        15. 如果想做“查询到了结果则返回true”这样的操作,不应该使用这样的代码:

          +
          if (resultSet != null)  return true;
          else return false;
          + +

          而应该这样:

          +
          return resultSet.next();
        -

        例题

        img

        -

        img

        -

        img

        -

        img

        -

        img

        -

        img

        -

        题型6 超标量实现

        例题

        img

        -

        img

        -

        img

        -

        img

        -

        注意,SD指令的0和R1有了就开始执行,不必等到F4有了再执行。。。

        -

        img

        -

        img

        -

        img

        -

        img

        -

        img

        -

        题型7 循环展开

        具体步骤:

        +

        PreparedStatement

        简介
        //An object that represents a precompiled SQL statement.
        //表示预编译的sql语句的对象
        public interface PreparedStatement extends Statement
        + +

        Statement的子类。可以用来解决sql注入问题。

        +

        它是预编译的sql语句,也即sql语句中的参数使用“?”占位符,需要传入参数。

        +
        使用步骤

        如下面的验证密码程序。键盘输入账号密码,从数据库查询该用户是否存在。

        +
        public class Login {
        public static void main(String[] args) throws IOException {
        //输入账号密码
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        System.out.println("Please enter the user name.");
        String uname = br.readLine();
        System.out.println("Please enter the password.");
        String password = br.readLine();
        //验证账号密码
        System.out.println(checkPassword(uname,password));
        }

        public static boolean checkPassword(String uname,String password){
        if (uname == null | password == null) return false;

        Connection conn = null;
        PreparedStatement stmt = null;
        ResultSet resultSet = null;
        try {
        conn = JDBCUtils.getConnection();
        stmt = conn.prepareStatement(
        "select * from user where name = ? and password = ?");
        //这跟resultset的获取表值的那个是一样的,都是需要指定列数和要设定的值。
        //并且下标都是以1开始。
        stmt.setString(1,uname);
        stmt.setString(2,password);
        resultSet = stmt.executeQuery();
        // stmt = conn.createStatement();
        // resultSet = stmt.executeQuery("select * from user where name = '"
        // +uname+"' and password = '"+password+"'");
        return resultSet.next();
        } catch (SQLException e) {
        throw new RuntimeException(e);
        } finally {
        JDBCUtils.close(conn,stmt,resultSet);
        }
        }
        }
        + +
        用PreparedStatement替代Statement

        它更安全且效率更高。

        +

        JDBC工具类

        书写

        public class JDBCUtils {
        //获取连接时不想传参,且需要保证通用性,使用配置文件
        //配置文件只需读取一次,可以用静态代码块完成
        private static Properties pro;
        private static String url;
        private static String driver;
        private static String user;
        private static String password;
        static{
        pro = new Properties();
        try {
        /*
        ClassLoader类可以获取src路径下的文件。使用ClassLoader获取文件时,只用传入相对于src的相对路径就行
        此处如果使用FileReader,需要以下写法:
        pro.load(new FileReader(
        JDBCUtils.class.getClassLoader()
        .getResource("jdbc.properties")
        .getPath())
        );
        */
        pro.load(JDBCUtils.class.getClassLoader().getResourceAsStream("./jdbc.properties"));
        url = pro.getProperty("url");
        user = pro.getProperty("user");
        password = pro.getProperty("password");
        driver = pro.getProperty("driver");
        //在静态块里注册驱动
        Class.forName(driver);
        } catch (IOException e) {
        throw new RuntimeException(e);
        } catch (ClassNotFoundException e) {
        throw new RuntimeException(e);
        }
        }
        public static Connection getConnection() {
        try {
        return DriverManager.getConnection(url,user,password);
        } catch (SQLException e) {
        throw new RuntimeException(e);
        }
        }

        //重载机制
        public static void close(Connection conn){
        close(conn,null,null);
        }

        public static void close(Connection conn, Statement stmt){
        close(conn,stmt,null);
        }

        public static void close(Connection conn, Statement stmt, ResultSet resultSet){
        if (resultSet != null){
        try {
        resultSet.close();
        } catch (SQLException e) {
        throw new RuntimeException(e);
        }
        }
        if (stmt != null){
        try {
        stmt.close();
        } catch (SQLException e) {
        throw new RuntimeException(e);
        }
        }
        if (conn !=null){
        try {
        conn.close();
        } catch (SQLException e) {
        throw new RuntimeException(e);
        }
        }
        }
        }
        + +

        使用

        public static Collection<Client> query(){
        ArrayList<Client> clients = new ArrayList<>();
        Statement stmt = null;
        Connection conn = null;
        ResultSet resultSet = null;
        try {
        conn = JDBCUtils.getConnection();
        stmt = conn.createStatement();
        resultSet = stmt.executeQuery("select * from usr");
        if (resultSet == null)
        System.out.println("查询失败");
        else
        while(resultSet.next())
        clients.add(new Client(resultSet.getInt(1),resultSet.getString(2),resultSet.getInt(3)));
        } catch (SQLException e) {
        throw new RuntimeException(e);
        } finally {
        JDBCUtils.close(conn,stmt,resultSet);
        }
        return clients;
        }
        + +

        JDBC控制事务

        使用Connection对象的管理事务的方法。

        +

        image-20221220214249168

        +

        image-20221220224401302

        +
        public class Account {
        public static void main(String[] args) {
        Connection conn = null;
        PreparedStatement stmt = null;
        PreparedStatement stmt2 = null;
        try {
        conn = JDBCUtils.getConnection();
        conn.setAutoCommit(false);
        stmt = conn.prepareStatement(
        "update usr set money = money - 500 where id = 1");
        stmt.executeUpdate();
        // int i = 3/0;
        stmt2 = conn.prepareStatement(
        "update usr set money = money + 500 where id = 2");
        stmt2.executeUpdate();
        conn.commit();
        } catch (Exception e) {
        try {
        conn.rollback();
        } catch (SQLException ex) {
        throw new RuntimeException(ex);
        }
        throw new RuntimeException(e);
        } finally {
        JDBCUtils.close(conn,stmt);
        }
        }
        }
        + +

        数据库连接池

        其实就是上面的JDBC中的Connection的对象池。

        +

        image-20221222212904151

        +

        C3P0

        基本使用

        非常简单,就是改一下Connection的获取,写一下xml就行。

        +
        设置配置文件

        固定放在src目录下。名字必须为c3p0-config.xml或者c3p0.properties

        +
        <c3p0-config>
        <!-- 使用默认的配置读取连接池对象 -->
        <default-config>
        <!-- 连接参数 -->
        <property name="driverClass">com.mysql.jdbc.Driver</property>
        <property name="jdbcUrl">jdbc:mysql://localhost:3306/helloworld</property>
        <property name="user">root</property>
        <property name="password">root</property>

        <!-- 连接池参数 -->
        <property name="initialPoolSize">5</property>
        <property name="maxPoolSize">10</property>
        <!-- 如果超过此超时时间,就说明数据库连接失败 -->
        <property name="checkoutTimeout">3000</property>
        </default-config>

        <named-config name="otherc3p0">
        <!-- 连接参数 -->
        <property name="driverClass">com.mysql.jdbc.Driver</property>
        <property name="jdbcUrl">jdbc:mysql://localhost:3306/day25</property>
        <property name="user">root</property>
        <property name="password">root</property>

        <!-- 连接池参数 -->
        <property name="initialPoolSize">5</property>
        <property name="maxPoolSize">8</property>
        <property name="checkoutTimeout">1000</property>
        </named-config>
        </c3p0-config>
        + +

        可以注意到,xml文件里面可以保存多套配置,比如上面的示例代码就保存了两套配置,default-config和name=”otherc3p0”的config。

        +

        ComboPooledDataSource有一个含参构造器:

        +
        public ComboPooledDataSource(String configName) {
        super(configName);
        }
        + +

        就可以传入config的名称指定要用的配置信息。

        +
        使用
        DataSource cpds = new ComboPooledDataSource();
        Connection conn = null;
        try{
        conn = cpds.getConnection();
        //正常使用......
        } catch(Exception e){

        } finally{
        if (conn != null){
        try {
        //正常使用关闭方法
        conn.close();
        } catch (SQLException e) {
        throw new RuntimeException(e);
        }
        }
        }
        + +

        Druid

        基本使用

        设置配置文件

        Druid的配置文件可以放在任意路径下,随便取名字。因为到时候需要指定配置文件。使用的是Properties文件。

        +
        driverClassName=com.mysql.jdbc.Driver
        url=jdbc:mysql://localhost:3306/helloworld
        username=root
        password=root
        initialSize=5
        maxActive=10
        maxWait=3000
        + +
        使用
        //导入配置文件
        Properties pro = new Properties();
        pro.load(Main.class.getClassLoader().getResourceAsStream("./druid.properties"));
        //使用工厂方法获取连接池对象
        DataSource cpds = DruidDataSourceFactory.createDataSource(pro);
        Connection conn = null;
        try{
        conn = cpds.getConnection();
        //正常使用......
        } catch(Exception e){

        } finally{
        if (conn != null){
        try {
        //正常使用关闭方法
        conn.close();
        } catch (SQLException e) {
        throw new RuntimeException(e);
        }
        }
        }
        + +

        定义工具类

        一般使用的时候还是会自定义一个工具类的

        +
        import com.alibaba.druid.pool.DruidDataSourceFactory;

        import javax.sql.DataSource;
        import java.sql.Connection;
        import java.sql.ResultSet;
        import java.sql.SQLException;
        import java.sql.Statement;
        import java.util.Properties;

        public class JDBCUtils {
        private static DataSource ds;
        static{
        try {
        Properties pro = new Properties();
        pro.load(JDBCUtils.class.getClassLoader().getResourceAsStream("./druid.properties"));
        ds = DruidDataSourceFactory.createDataSource(pro);
        } catch (Exception e) {
        throw new RuntimeException(e);
        }
        }

        public static Connection getConnection() throws SQLException {
        return ds.getConnection();
        }

        public static DataSource getDataSource(){
        return ds;
        }

        //重载机制
        public static void close(Connection conn){
        close(conn,null,null);
        }

        public static void close(Connection conn, Statement stmt){
        close(conn,stmt,null);
        }

        public static void close(Connection conn, Statement stmt, ResultSet resultSet){
        if (resultSet != null){
        try {
        resultSet.close();
        } catch (SQLException e) {
        throw new RuntimeException(e);
        }
        }
        if (stmt != null){
        try {
        stmt.close();
        } catch (SQLException e) {
        throw new RuntimeException(e);
        }
        }
        if (conn !=null){
        try {
        conn.close();
        } catch (SQLException e) {
        throw new RuntimeException(e);
        }
        }
        }
        }
        + +

        使用同上的JDBCUtils

        +

        Spring JDBC

        Spring框架对JDBC的简单封装,提供JDBCTemplate对象。

        +

        使用方法

        带参(PreparedStatement)

        jdbcTemplate.update("update usr set money = ? where uname = ?",10,"Mary");
        + +

        DML

        update
        public static void main(String[] args) throws Exception {
        JdbcTemplate jdbcTemplate = new JdbcTemplate(JDBCUtils.getDataSource());

        //增
        int count = jdbcTemplate.update("insert into usr values (null,'Jack',3000),(null,'LiMing',500000)");
        System.out.println(count);
        //删
        int count2 = jdbcTemplate.update("delete from usr where uname = 'Jack'");
        System.out.println(count2);
        //改
        int count3 = jdbcTemplate.update("update usr set money = ? where uname = ?",10,"Mary");
        System.out.println(count3);
        }
        /*输出结果:
        2
        1
        1
        */
        + +

        DQL

        提供了三种方法。

        +
        queryForMap

        将得到的结果(只能是一行)封装为一个Map<String,Object>,其中key为列名,value为该行该列的值。

        +

        如果得到的结果不为1行(=0 or >1),会抛出异常。

        +
            JdbcTemplate jdbcTemplate = new JdbcTemplate(JDBCUtils.getDataSource());

        Map<String,Object> m = jdbcTemplate.queryForMap("select * from usr where id = 2");
        System.out.println(m);
        //输出:{id=2, uname=Lily, money=2000}
        + +
        queryForList

        将得到的结果封装为List<Map<String,Object>>,其中一个Map为一行,多个Map表示多行,存储在List中。

        +
            List<Map<String, Object>> res = jdbcTemplate.queryForList("select * from usr");
        for (Map<String,Object> m : res){
        System.out.println(m.hashCode());
        System.out.println(m.keySet().toString());
        System.out.println(m.values().toString());
        }
        /*输出结果:
        213151839
        [id, uname, money]
        [1, Mary, 10]
        213143443
        [id, uname, money]
        [2, Lily, 2000]
        -2022948335
        [id, uname, money]
        [4, LiMing, 500000]
        */
        + +
        query

        可以把查询回的结果封装为自己想要的对象而不是Map。如示例就封装为了Client对象。

        +
        原始一点的

        可以看到,里面的包装内容还是得自己写,有点麻烦。

        +
                JdbcTemplate jdbcTemplate = new JdbcTemplate(JDBCUtils.getDataSource());

        List<Client> clients = jdbcTemplate.query(
        "select * from usr where money > 1000",
        new RowMapper<Client>() {
        @Override
        public Client mapRow(ResultSet resultSet, int i) throws SQLException {
        return new Client(
        resultSet.getInt(1),
        resultSet.getString(2),
        resultSet.getInt(3));
        }
        });
        System.out.println(clients.toString());
        /*输出结果
        [Client{id=2, name='Lily', money=2000}, Client{id=4, name='LiMing', money=500000}, Client{id=6, name='LiMing', money=500000}]
        */
        + +
        常用的

        使用包装好的BeanPropertyRowMapper类。

        +
                JdbcTemplate jdbcTemplate = new JdbcTemplate(JDBCUtils.getDataSource());

        List<Client> clients = jdbcTemplate.query("select * from usr where money > 1000",
        new BeanPropertyRowMapper<Client>(Client.class));
        System.out.println(clients.toString());
        /*输出结果:
        [Client{id=2, uname='Lily', money=2000}, Client{id=4, uname='LiMing', money=500000}, Client{id=6, uname='LiMing', money=500000}]
        */
        + +

        注意:

          -
        1. 依题意展开
        2. -
        3. 去除多余的BNE、合并所有DADDUI
        4. -
        5. 寄存器换名消除名相关
        6. -
        7. 重排序消除数据相关
        8. +
        9. 要求包装的class,比如说Client,必须要有public的无参构造器
        10. +
        11. Java的那个被包装类的字段最好使用基本数据类型,而使用引用类型,如Integer,Double等等等。因为如果使用基本数据类型,当表中数据为null时会报错。
        12. +
        13. 要求被包装的class的字段名称一定要与数据库的一模一样,大小写可以不一样。
        14. +
        15. 要求被包装的class的字段一定要是可以修改的。也就是说,要么public,要么提供set方法。
        -

        1

        img

        -

        img

        -

        img

        -

        img

        -

        img

        -

        img

        -

        img

        -

        img

        -

        img

        -

        2

        img

        -

        img

        -

        img

        -

        题型8 VIEW技术

        例题

        img

        -

        img

        -

        img

        -

        看起来大概可能就有点类似树的概念,什么都不依赖的就放前面,然后依赖1层的依赖2层的之类的

        -

        img

        -

        题型9 软流水

        例题

        img

        -

        img

        -

        img

        -

        img

        -

        指令级并行

        概念

          -
        1. 开发指令级并行ILP的方法

          +
          queryForObject

          返回查到的某个东西。可以用于聚合函数的查询。

          +
          int money = jdbcTemplate.queryForObject("select money from usr where uname = 'Mary'",Integer.class);
          System.out.println(money);
          + +

          第三部分 Web概述和静态网页技术

          Web概述

            +
          • JavaWeb:

            +
              +
            • 使用Java语言开发基于互联网的项目
            • +
            +
          • +
          • 软件架构:

              -
            1. 基于硬件的动态开发
            2. -
            3. 基于软件的静态开发
            4. +
            5. C/S: Client/Server 客户端/服务器端
                +
              • 在用户本地有一个客户端程序,在远程有一个服务器端程序
              • +
              • 如:QQ,迅雷…
              • +
              • 优点:
                  +
                1. 用户体验好
              • -
              • 流水线CPI

                -

                实际CPI = 理想CPI + 停顿(结构/数据/控制冲突引起)

                -
              • -
              • 理想CPI是衡量流水线最高性能

                -
              • -
              • IPC:每个时钟周期完成的指令数

                -

                CPI:每个指令所需时钟周期数

                +
              • 缺点:
                  +
                1. 开发、安装,部署,维护 麻烦
                2. +
              • -
              • 基本程序块:一串没有分支和跳转转入点的指令块

                +
            6. +
            7. B/S: Browser/Server 浏览器/服务器端
                +
              • 只需要一个浏览器,用户通过不同的网址(URL),客户访问不同的服务器端程序
              • +
              • 优点:
                  +
                1. 开发、安装,部署,维护 简单
                -

                解决冲突的方法之一是序列调度,不过对于跨块的调度(也即jump指令)会有影响

                -

                相关与并行

                相关:两条指令之间存在某种依赖关系

                -

                只能部分(完全不难)在流水线中重叠执行

                -

                类型:数据相关(真数据相关)、名相关、控制相关

                -

                约定先执行i再执行j

                -
                  -
                1. 数据相关

                  -
                    -
                  1. 定义:j使用i的结果,也即先写后读

                  2. -
                  3. 具有传递性

                    +
                  4. 缺点:
                      +
                    1. 如果应用过大,用户的体验可能会受到影响
                    2. +
                    3. 对硬件要求过高
                    4. +
                  5. -
                  6. 反映数据流动关系,即如何从生产者流动到消费者

                    +
            8. -
            9. 数据相关不能并行,需要插入暂停解决冲突

              +
          • -
          • 解决方法

            +
          • B/S架构详解

            +
              +
            • 资源分类:

                -
              1. 保持相关但避免冲突

                -

                调度

                +
              2. 静态资源:
                  +
                • 使用静态网页开发技术发布的资源。
                • +
                • 特点:
                    +
                  • 所有用户访问,得到的结果是一样的。
                  • +
                  • 如:文本,图片,音频、视频, HTML,CSS,JavaScript
                  • +
                  • 如果用户请求的是静态资源,那么服务器会直接将静态资源发送给浏览器。浏览器中内置了静态资源的解析引擎,可以展示静态资源
                  • +
                • -
                • 变换代码消除相关关系

                  +
              3. -
              +
            • 动态资源:
                +
              • 使用动态网页及时发布的资源。
              • +
              • 特点:
                  +
                • 所有用户访问,得到的结果可能不一样。
                • +
                • 如:jsp/servlet,php,asp…
                • +
                • 如果用户请求的是动态资源,那么服务器会执行动态资源,转换为静态资源,再发送给浏览器
                • +
              • -
              • 检测方法

                -

                流经寄存器时直观;流经存储器复杂

                +
      2. -
      3. 名相关

        -

        img

        -
          -
        1. 分类

          -
            -
          1. 反相关:先读后写,也即i读的名与j写的名相同
          2. -
          3. 输出相关:i和j写的名相同
          4. -
          -
        2. -
        3. 解决方法:换名技术,可以编译器静态实现 or 硬件动态实现

          -

          img

          -
        4. -
        5. 相关问题

          -

          寄存器换名可以消除WAR和WAW冲突

          -
            -
          1. WAR(反相关)
          2. -
          3. WAW(输出相关)
          4. -
          -
        6. -
        +
      4. 我们要学习动态资源,必须先学习静态资源!

      5. -
      6. 数据冲突

        -

        注意这里的命名,是按照正确顺序命名的。比如说RAW(read after write),写后读,正确次序就是i写入然后j再读,所以叫写后读。

        -
          -
        1. RAW(数据相关)

          -

          也即i写j读

          +
        2. 静态资源:

          +
            +
          • HTML:用于搭建基础网页,展示页面的内容
          • +
          • CSS:用于美化页面,布局页面
          • +
          • JavaScript:控制页面的元素,让页面有一些动态的效果
          • +
        3. -
        4. WAW(输出相关)

          -

          也即i写j写

          -

          流水线发生条件:流水线不止一个段可以写操作、指令被重新排序

          -

          5段流水线不会发生,因为只会在WB阶段写寄存器

          +
        5. -
        6. WAR(反相关)

          -

          也即i读j写

          -

          流水线发生条件:有些指令写操作提前有些读操作滞后、指令被重新排序

          + +

          静态网页概述

          练习:用纯HTML写旅游网站首页

          代码

          <!DOCTYPE html>
          <html lang="ch">
          <head>
          <meta charset="UTF-8">
          <title>旅游网站</title>
          </head>
          <body>
          <table>
          <tr align="center">
          <td><img src="./image/top_banner.jpg" alt="亲子周边旅游节" width="100%"></td>
          </tr>
          <tr>
          <table>
          <tr align="center">
          <td width="25%"><img src="./image/logo.jpg" alt="logo" width="100%"></td>
          <td width="50%"><img src="./image/search.png" alt="search" width="100%"></td>
          <td width="25%"><img src="./image/hotel_tel.png" alt="hotel" width="100%"></td>
          </tr>
          </table>
          </tr>
          <tr>
          <table width="100%" bgcolor="orange" cellspacing="0" cellpadding="0">
          <tr align="center" height = "45">
          <td>首页</td>
          <td>门票</td>
          <td>酒店</td>
          <td>香港车票</td>
          <td>出境游</td>
          <td>国内游</td>
          <td>港澳游</td>
          <td>抱团定制</td>
          <td>全球自由行</td>
          <td>收藏排行榜</td>
          </tr>
          </table>
          </tr>
          <tr>
          <img src="./image/banner_3.jpg" alt="亲子周边旅游节" width="100%">
          </tr>
          <tr>
          <table>
          <tr>
          <td align="right" width="20%"><img src="./image/icon_5.jpg" alt="亲子周边旅游节" width="100%"></td>
          <td width="80%">黑马精选</td>
          </tr>
          </table>
          <hr color="orange">
          </tr>
          <tr>
          <table>
          <tr>
          <td>
          <div>
          <img src="./image/jiangxuan_1.jpg" alt="" width="100%">
          <br>上海飞三亚五天4晚自由行(春节销售+亲子+蜜月+自由行)<br>
          <font size = 3 color="#8b0000">¥899</font>
          </div>
          </td>
          <td>
          <div>
          <img src="./image/jiangxuan_1.jpg" alt="" width="100%">
          <br>上海飞三亚五天4晚自由行(春节销售+亲子+蜜月+自由行)<br>
          <font size = 3 color="#8b0000">¥899</font>
          </div>
          </td>
          <td>
          <div>
          <img src="./image/jiangxuan_1.jpg" alt="" width="100%">
          <br>上海飞三亚五天4晚自由行(春节销售+亲子+蜜月+自由行)<br>
          <font size = 3 color="#8b0000">¥899</font>
          </div>
          </td>
          </tr>
          </table>
          </tr>
          <tr>
          <table>
          <tr>
          <td align="right" width="20%"><img src="./image/icon_6.jpg" alt="亲子周边旅游节" width="100%"></td>
          <td width="80%">国内游</td>
          </tr>
          </table>
          <hr color="orange">
          </tr>
          <tr>
          <table align="center" width="95%">
          <tr>
          <td rowspan="2" width = "25%">
          <img src="./image/guonei_1.jpg" alt="亲子周边旅游节">
          </td>
          <td>
          <div>
          <img src="./image/jiangxuan_2.jpg" alt="" width="100%">
          <br>上海飞三亚五天4晚自由行(春节销售+亲子+蜜月+自由行)<br>
          <font size = 3 color="#8b0000">¥899</font>
          </div>
          </td>
          <td>
          <div>
          <img src="./image/jiangxuan_2.jpg" alt="" width="100%">
          <br>上海飞三亚五天4晚自由行(春节销售+亲子+蜜月+自由行)<br>
          <font size = 3 color="#8b0000">¥899</font>
          </div>
          </td>
          <td>
          <div>
          <img src="./image/jiangxuan_2.jpg" alt="" width="100%">
          <br>上海飞三亚五天4晚自由行(春节销售+亲子+蜜月+自由行)<br>
          <font size = 3 color="#8b0000">¥899</font>
          </div>
          </td>
          </tr>
          <tr>
          <td>
          <div>
          <img src="./image/jiangxuan_2.jpg" alt="" width="100%">
          <br>上海飞三亚五天4晚自由行(春节销售+亲子+蜜月+自由行)<br>
          <font size = 3 color="#8b0000">¥899</font>
          </div>
          </td>
          <td>
          <div>
          <img src="./image/jiangxuan_2.jpg" alt="" width="100%">
          <br>上海飞三亚五天4晚自由行(春节销售+亲子+蜜月+自由行)<br>
          <font size = 3 color="#8b0000">¥899</font>
          </div>
          </td>
          <td>
          <div>
          <img src="./image/jiangxuan_2.jpg" alt="" width="100%">
          <br>上海飞三亚五天4晚自由行(春节销售+亲子+蜜月+自由行)<br>
          <font size = 3 color="#8b0000">¥899</font>
          </div>
          </td>
          </tr>
          </table>
          </tr>
          <tr>
          <table>
          <tr>
          <td align="right" width="20%"><img src="./image/icon_7.jpg" alt="亲子周边旅游节" width="100%"></td>
          <td width="80%">境外游</td>
          </tr>
          </table>
          <hr color="orange">
          </tr>
          <tr>
          <table align="center" width="95%">
          <tr>
          <td rowspan="2" width = "25%">
          <img src="./image/jiangwai_1.jpg" alt="亲子周边旅游节">
          </td>
          <td>
          <div>
          <img src="./image/jiangxuan_2.jpg" alt="" width="100%">
          <br>上海飞三亚五天4晚自由行(春节销售+亲子+蜜月+自由行)<br>
          <font size = 3 color="#8b0000">¥899</font>
          </div>
          </td>
          <td>
          <div>
          <img src="./image/jiangxuan_2.jpg" alt="" width="100%">
          <br>上海飞三亚五天4晚自由行(春节销售+亲子+蜜月+自由行)<br>
          <font size = 3 color="#8b0000">¥899</font>
          </div>
          </td>
          <td>
          <div>
          <img src="./image/jiangxuan_2.jpg" alt="" width="100%">
          <br>上海飞三亚五天4晚自由行(春节销售+亲子+蜜月+自由行)<br>
          <font size = 3 color="#8b0000">¥899</font>
          </div>
          </td>
          </tr>
          <tr>
          <td>
          <div>
          <img src="./image/jiangxuan_2.jpg" alt="" width="100%">
          <br>上海飞三亚五天4晚自由行(春节销售+亲子+蜜月+自由行)<br>
          <font size = 3 color="#8b0000">¥899</font>
          </div>
          </td>
          <td>
          <div>
          <img src="./image/jiangxuan_2.jpg" alt="" width="100%">
          <br>上海飞三亚五天4晚自由行(春节销售+亲子+蜜月+自由行)<br>
          <font size = 3 color="#8b0000">¥899</font>
          </div>
          </td>
          <td>
          <div>
          <img src="./image/jiangxuan_2.jpg" alt="" width="100%">
          <br>上海飞三亚五天4晚自由行(春节销售+亲子+蜜月+自由行)<br>
          <font size = 3 color="#8b0000">¥899</font>
          </div>
          </td>
          </tr>
          </table>
          </tr>
          <tr>
          <img src="./image/footer_service.png" alt="" width="100%">
          </tr>
          <tr>
          <table bgcolor="orange" width="100%" height = 75>
          <tr align="center">
          <td>
          <font color="gray" size = 2>江苏传智播客教育科技股份有限公司 版权所有Copyright 2006-2018&copy;, All Rights Reserved 苏ICP备16007882</font>
          </td>
          </tr>
          </table>
          </tr>
          </table>
          </body>
          </html>
          + +

          注意点

            +
          1. 布局

            +

            页面布局使用table标签。这点让我感觉非常新奇。

            +

            而且表格布局可以嵌套,也即每一行可以是一个新的表格。

          2. +
          3. 图片适应屏幕宽度

            +

            只需在img标签加个width=”100%”的属性即可。如:

            +
            <img src="./image/top_banner.jpg" width="100%">
          +

          表单

          注意

            +
          1. 表项中的数据要被提交的话,必须指定其名称

            +

            image-20221223161847622

            +

            也即一定要有属性name。

          2. -
          3. 控制相关

            -

            由分支指令引起

            +
          4. 关于from的属性

            +

            image-20221223162035569

          5. -
          -

          调度

          img

          -

          img

          -

          动态调度

          基本思想

          img

          -

          img

          -

          img

          -

          img

          -

          这里可能意思是引出了多流出,所以会导致DIV和ADD同时流出,从而发生WAW。同理,可能的阻塞也会导致WAR。

          -

          img

          -

          记分牌动态调度算法

            -
          1. 基本思想

            -

            在没有结构冲突时,尽可能早地执行没有数据冲突的指令,实现每个时钟周期执行一条指令

            +
          2. 一般都这么写

            +

            image-20221223165046277

          3. -
          4. 基本结构

            -

            三张表:指令执行状态、功能部件状态、寄存器状态及数据相关关系

            -
              -
            1. 指令状态表

              -

              记录正在执行的各条指令的状态

              +
            +

            练习

            image-20221223171603366

            +
            <!DOCTYPE html>
            <html lang="ch">
            <head>
            <meta charset="UTF-8">
            <title>注册界面</title>
            </head>
            <body>
            <form action="#">
            <table border="1" cellpadding="1" cellspacing="1">
            <tr>
            <td>
            <label for="username">用户名:</label>
            </td>
            <td>
            <input type="text" id="username" name="username" placeholder="请输入用户名">
            </td>
            </tr>
            <tr>
            <td>
            <label for="password">密码:</label>
            </td>
            <td>
            <input type="password" id="password" name="password" placeholder="请输入密码">
            </td>
            </tr>
            <tr>
            <td>
            <label for="email">邮箱:</label>
            </td>
            <td>
            <input type="email" id="email" name="email" placeholder="请输入邮箱">
            </td>
            </tr>
            <tr>
            <td>
            <label for="name">姓名:</label>
            </td>
            <td>
            <input type="text" id="name" name="name" placeholder="请输入姓名">
            </td>
            </tr>
            <tr>
            <td>
            <label for="phone_number">手机号:</label>
            </td>
            <td>
            <input type="text" id="phone_number" name="phone_number" placeholder="请输入手机号">
            </td>
            </tr>
            <tr>
            <td>
            性别:
            </td>
            <td>
            <input type="radio" name="gender" value="1">
            <input type="radio" name="gender" value="2">
            </td>
            </tr>
            <tr>
            <td>
            <label for="birthday">出生日期:</label>
            </td>
            <td>
            <input type="date" name="birthday" id="birthday">
            </td>
            </tr>
            <tr>
            <td>
            <label for="certification">验证码:</label>
            </td>
            <td>
            <input type="text" name="certification" id="certification">
            <img src="./image/verify_code.jpg" >
            </td>
            </tr>
            <tr align="center">
            <td colspan="2">
            <input type="image" src="./image/regbtn.jpg">
            </td>
            </tr>
            </table>
            </form>

            </body>
            </html>
            + +

            CSS

            盒子模型

            image-20221223204733649

            +

            练习:注册页面

            image-20221223223128861

            +
            <!DOCTYPE html>
            <html lang="ch">
            <head>
            <meta charset="UTF-8">
            <title>注册界面</title>
            <link rel="stylesheet" href="./2.css">
            </head>
            <body>
            <div id="log_in_box">
            <div id="log_in_text">
            <div id="log_in_text1">
            新用户注册
            </div>
            <div id="log_in_text2">
            USER REGISTER
            </div>
            </div>

            <div id="log_in_text3">
            已有账号?<font color = red>立即登录</font>
            </div>
            <div id="log_in_table">
            <form>
            <table>
            <tr>
            <td class="td_left"><label for="username">用户名</label></td>
            <td class="td_right"><input type="text" name="username" id="username" placeholder="请输入用户名"></td>
            </tr>

            <tr>
            <td class="td_left"><label for="password">密码</label></td>
            <td class="td_right"><input type="password" name="password" id="password" placeholder="请输入密码"></td>
            </tr>

            <tr>
            <td class="td_left"><label for="email">Email</label></td>
            <td class="td_right"><input type="email" name="email" id="email" placeholder="请输入邮箱"></td>
            </tr>

            <tr>
            <td class="td_left"><label for="name">姓名</label></td>
            <td class="td_right"><input type="text" name="name" id="name" placeholder="请输入姓名"></td>
            </tr>

            <tr>
            <td class="td_left"><label for="tel">手机号</label></td>
            <td class="td_right"><input type="text" name="tel" id="tel" placeholder="请输入手机号"></td>
            </tr>

            <tr>
            <td class="td_left"><label>性别</label></td>
            <td class="td_right">
            <input type="radio" name="gender" value="male"> <span class="choice"></span>
            <input type="radio" name="gender" value="female"> <span class="choice"></span>
            </td>
            </tr>

            <tr>
            <td class="td_left"><label for="birthday">出生日期</label></td>
            <td class="td_right"><input type="date" name="birthday" id="birthday" placeholder="请输入出生日期"></td>
            </tr>

            <tr>
            <td class="td_left"><label for="checkcode" >验证码</label></td>
            <td class="td_right"><input type="text" name="checkcode" id="checkcode" placeholder="请输入验证码">
            <img id="img_check" src="img/verify_code.jpg">
            </td>
            </tr>


            <tr>
            <td colspan="2" align="center"><input type="submit" value="注册" id="submit"></td>
            </tr>
            </table>
            </form>
            </div>
            </div>
            </body>
            </html>
            + +
            *{
            margin: 0px;
            padding: 0px;
            /*防止大小因padding变化*/
            box-sizing: border-box;
            }
            body{
            z-index: 0;
            background-image: url("./img/login_bg.png");
            }
            #log_in_box {
            border: 9px solid darkgray;
            z-index: 100;
            width: 987px;
            height: 590px;
            /*让div水平居中*/
            margin: auto;
            margin-top: 20px;
            padding: 15px;
            background: white;

            }

            #log_in_table {
            margin-top: 96px;
            margin-left: 300px;
            }

            #log_in_text3{
            font-size: 12px;
            float: right;
            }

            #log_in_text > div:first-child{
            margin-right: 0px;
            width: 150px;
            color: orange;
            font-size: 25px;
            }
            #log_in_text > div:last-child{
            margin-right: 0px;
            width: 200px;
            color: darkgray;
            font-size: 17px;
            font-family: "Arial Black";
            }

            #log_in_text{
            margin: 0px;
            display:inline;
            float: left;
            }

            /*此处一定得是选择input,不能是选择.td_right,因为这样才能覆盖掉input原有的那个丑边框*/
            input {
            padding: 5px;
            align-self: center;
            border: 1px solid lightgrey;
            height: 35px;
            border-radius: 6px;
            margin: 3px;
            margin-left: 15px;
            /*解决了radio的选框和文本不对齐。*/
            vertical-align: middle;
            }

            .td_left{
            color: slategrey;
            text-align: right;
            }

            .choice{
            font-size: 15px;
            }

            #checkcode{
            width: 100px;
            font-size: 15px;
            }

            #img_check{
            vertical-align:middle;
            }

            #submit{
            margin-top: 15px;
            margin-left: 15px;
            margin-right: 155px;
            color: transparent;
            width: 100px;
            background-image: url("./img/regbtn.jpg");
            }
            + +

            JavaScript

            对象

            function
            1. 创建:
            +   1. var fun = new Function(形式参数列表,方法体);  //忘掉吧
            +   2. function 方法名称(形式参数列表){
            +          方法体
            +      }
            +
            +   3. var 方法名 = function(形式参数列表){
            +           方法体
            +      }
            +2. 方法:
            +
            +3. 属性:
            +   length:代表形参的个数
            +4. 特点:
            +   1. 方法定义是,形参的类型不用写,返回值类型也不写。
            +   2. 方法是一个对象,如果定义名称相同的方法,会覆盖
            +   3. 在JS中,方法的调用只与方法的名称有关,和参数列表无关
            +   4. 在方法声明中有一个隐藏的内置对象(数组),**arguments**,封装所有的实际参数
            +5. 调用:
            +   方法名称(实际参数列表);
            +
            +
            /**
            * 求任意个数的和
            */
            function add (){
            var sum = 0;
            for (var i = 0; i < arguments.length; i++) {
            sum += arguments[i];
            }
            return sum;
            }

            var sum = add(1,2,3,4);
            alert(sum);
            + +
            Global
              +
            1. 特点:全局对象,这个Global中封装的方法不需要对象就可以直接调用。 方法名();

            2. -
            3. 功能部件状态表

              -

              记录各个功能部件状态,每项有以下字段:

              +
            4. 方法:
              encodeURI():url编码
              decodeURI():url解码

              +

              encodeURIComponent():url编码,编码的字符更多
              decodeURIComponent():url解码

              +

              parseInt():将字符串转为数字

                -
              • Busy:yes/no
              • -
              • Op:操作编码
              • -
              • Fi:目的寄存器编号
              • -
              • Fj,Fk:源寄存器编号
              • -
              • Qj,Qk:Fj和Fk的功能部件
              • -
              • Rj,Rk:Fj和Fk是否就绪且还没被取走
              • +
              • 逐一判断每一个字符是否是数字,直到不是数字为止,将前边数字部分转为number
                isNaN():判断一个值是否是NaN
                  +
                • NaN六亲不认,连自己都不认。NaN参与的==比较全部问false
              • -
              • 结果寄存器状态表

                -

                每个寄存器有一项,用于指出哪个功能部件将把结果写入

                -

                大概是这样的结果:n(寄存器数量) X m(功能部件数量) 的值为0 or 1的矩阵

                -
              • +
              +

              eval():讲 JavaScript 字符串,并把它作为脚本代码来执行。

              +
              var str = "http://www.baidu.com?wd=传智播客";
              var encode = encodeURI(str);
              document.write(encode +"<br>");//%E4%BC%A0%E6%99%BA%E6%92%AD%E5%AE%A2
              var s = decodeURI(encode);
              document.write(s +"<br>");//传智播客


              var str1 = "http://www.baidu.com?wd=传智播客";
              var encode1 = encodeURIComponent(str1);
              document.write(encode1 +"<br>");//%E4%BC%A0%E6%99%BA%E6%92%AD%E5%AE%A2
              var s1 = decodeURIComponent(encode);
              document.write(s1 +"<br>");//传智播客

              var jscode = "alert(123)";
              eval(jscode);
            +

            DOM

            image-20221224162341993

            +

            image-20221224162540306

            +
            document
            获取元素对象
            getElementById();
            getElementsByTagName();//通过标签名
            getElementsByClassName();
            getElementsByName();//注意是数组
            + +
            创建DOM对象
            createAttribute(name);
            createComment();
            createElement();
            createTextNode();
            + +
            Element
            removeAttribute();
            setAttribute(属性名,属性值);
            + +
            Node

            说了树结构后,这个就好理解多了。

            +

            image-20221224164239507

            +
            练习:动态表格
            <!DOCTYPE html>
            <html lang="ch">
            <head>
            <meta charset="UTF-8">
            <title>动态表格</title>
            <style>
            #input{
            width: 60%;
            margin: auto;
            margin-top: 50px;
            }
            table{
            margin: auto;
            margin-top: 100px;
            width: 70%;
            text-align: center;
            }
            </style>
            </head>
            <body>
            <div id="input">
            <input type="text" placeholder="请输入编号" id="id">
            <input type="text" placeholder="请输入姓名" id="name">
            <input type="text" placeholder="请输入性别" id="gender">
            <input type="button" id="add_but" value="添加">
            </div>
            <table border="1px solid black" id="table">
            <tr>
            <title>学生信息表</title>
            <td>编号</td>
            <td>姓名</td>
            <td>性别</td>
            <td>操作</td>
            </tr>
            </table>
            <script>
            //获取表对象
            let table = document.getElementById("table");

            //最后那列删除要用很多次,所以这里只写一份原始的,之后要用再copy del_col对象即可。
            //但是要注意,js的深拷贝 object.cloneNode(true)是不会拷贝事件绑定的
            //所以事件绑定不得不放在下面的函数里做了
            let del_col = document.createElement("td");
            let del_col_a = document.createElement("a");
            del_col_a.href="javascript:void(0);";
            del_col_a.innerHTML="删除";
            del_col.appendChild(del_col_a);

            //删除列
            function del_row(obj){
            let target = obj.parentNode.parentNode;
            table.removeChild(target);
            }
            //添加行
            function add_row(id,name,gender){
            if (id=="" || name=="" || gender=="") return;
            let row = document.createElement("tr");
            let col1 = document.createElement("td");
            let col2 = document.createElement("td");
            let col3 = document.createElement("td");
            col1.innerHTML = id;
            col2.innerHTML = name;
            col3.innerHTML = gender;

            //为“删除”绑定事件
            let col4 = del_col.cloneNode(true);
            col4.lastChild.onclick = function(){
            //通过this定位。此时this指代a标签。
            //如果在del_row内把obj换成this反倒是不行的,因为那时候的this会指代的是window
            del_row(this);
            };

            row.appendChild(col1);
            row.appendChild(col2);
            row.appendChild(col3);
            row.appendChild(col4);

            table.appendChild(row);
            }

            //为“添加”按钮绑定事件
            document.getElementById("add_but").onclick = function(){
            add_row(document.getElementById("id").value,
            document.getElementById("name").value,
            document.getElementById("gender").value);
            };
            </script>
            </body>
            </html>
            + +

            老师标答值得学习借鉴的点:

            +
            //使用innerHTML添加
            document.getElementById("btn_add").onclick = function() {
            //2.获取文本框的内容
            var id = document.getElementById("id").value;
            var name = document.getElementById("name").value;
            var gender = document.getElementById("gender").value;

            //获取table
            var table = document.getElementsByTagName("table")[0];

            //追加一行
            table.innerHTML += "<tr>\n" +
            " <td>"+id+"</td>\n" +
            " <td>"+name+"</td>\n" +
            " <td>"+gender+"</td>\n" +
            " <td><a href=\"javascript:void(0);\" onclick=\"delTr(this);\" >删除</a></td>\n" +
            " </tr>";
            }
            + +
            事件
            练习:全选/全不选/反选+行变色

            image-20221225192124809

            +
            <!DOCTYPE html>
            <html lang="ch">
            <head>
            <meta charset="UTF-8">
            <title>动态表格</title>
            <style>
            #input{
            width: 60%;
            margin: auto;
            margin-top: 50px;
            }
            table{
            margin: auto;
            margin-top: 100px;
            width: 70%;
            text-align: center;
            }
            </style>
            </head>
            <body>
            <div id="input">
            <input type="text" placeholder="请输入编号" id="id">
            <input type="text" placeholder="请输入姓名" id="name">
            <input type="text" placeholder="请输入性别" id="gender">
            <input type="button" id="add_but" value="添加">
            </div>
            <table border="1px solid black" id="table">
            <tr>
            <td>选择</td>
            <td>编号</td>
            <td>姓名</td>
            <td>性别</td>
            <td>操作</td>
            </tr>
            <tr>
            <td><input type="checkbox" class="box"></td>
            <td>1</td>
            <td>Lily</td>
            <td>female</td>
            <td><a href="#" >删除</a></td>
            </tr>
            <tr>
            <td><input type="checkbox" class="box"></td>
            <td>2</td>
            <td>Jack</td>
            <td>male</td>
            <td><a href="#" >删除</a></td>
            </tr>
            <tr>
            <td><input type="checkbox" class="box"></td>
            <td>3</td>
            <td>Peterson</td>
            <td>male</td>
            <td><a href="#" >删除</a></td>
            </tr>
            <tr>
            <td><input type="checkbox" class="box"></td>
            <td>4</td>
            <td>Mary</td>
            <td>female</td>
            <td><a href="#" >删除</a></td>
            </tr>
            </table>
            <div style="text-align: center;margin-top: 20px">
            <input type="button" id="select_all" value="全选">
            <input type="button" id="select_none" value="全不选">
            <input type="button" id="select_aside" value="反选">
            </div>
            <script>
            window.onload = function (){
            //行变色
            let rows = document.getElementsByTagName("tr");
            for (let i = 0; i < rows.length; i++) {
            rows[i].onmouseover = function (){
            rows[i].setAttribute("style","background: pink");
            };
            rows[i].onmouseout = function (){
            rows[i].removeAttribute("style");
            }
            }

            //三个按钮
            let select_all = document.getElementById("select_all");
            let select_none = document.getElementById("select_none");
            let select_aside = document.getElementById("select_aside");
            let boxes = document.getElementsByClassName("box");

            select_all.onclick = function(){
            for (let i = 0; i < boxes.length; i++) {
            boxes[i].checked = 1;
            }
            };
            select_none.onclick = function(){
            for (let i = 0; i < boxes.length; i++) {
            boxes[i].checked = 0;
            }
            };
            select_aside.onclick = function(){
            for (let i = 0; i < boxes.length; i++) {
            boxes[i].checked = !(boxes[i].checked);
            }
            };
            };
            </script>
            </body>
            </html>
            + +
            练习:表单校验
            <!DOCTYPE html>
            <html lang="ch">
            <head>
            <meta charset="UTF-8">
            <title>注册界面</title>
            <link rel="stylesheet" href="./2.css">
            </head>
            <body>
            <div id="log_in_box">
            <div id="log_in_text">
            <div id="log_in_text1">
            新用户注册
            </div>
            <div id="log_in_text2">
            USER REGISTER
            </div>
            </div>

            <div id="log_in_text3">
            已有账号?<font color = red>立即登录</font>
            </div>
            <div id="log_in_table">
            <form id="form">
            <table>
            <tr>
            <td class="td_left"><label for="username">用户名</label></td>
            <td class="td_right"><input type="text" name="username" id="username" placeholder="请输入用户名"></td>
            <td></td>
            </tr>

            <tr>
            <td class="td_left"><label for="password">密码</label></td>
            <td class="td_right"><input type="password" name="password" id="password" placeholder="请输入密码"></td>
            <td></td>
            </tr>

            <tr>
            <td class="td_left"><label for="email">Email</label></td>
            <td class="td_right"><input type="email" name="email" id="email" placeholder="请输入邮箱"></td>
            </tr>

            <tr>
            <td class="td_left"><label for="name">姓名</label></td>
            <td class="td_right"><input type="text" name="name" id="name" placeholder="请输入姓名"></td>
            </tr>

            <tr>
            <td class="td_left"><label for="tel">手机号</label></td>
            <td class="td_right"><input type="text" name="tel" id="tel" placeholder="请输入手机号"></td>
            </tr>

            <tr>
            <td class="td_left"><label>性别</label></td>
            <td class="td_right">
            <input type="radio" name="gender" value="male"> <span class="choice"></span>
            <input type="radio" name="gender" value="female"> <span class="choice"></span>
            </td>
            </tr>

            <tr>
            <td class="td_left"><label for="birthday">出生日期</label></td>
            <td class="td_right"><input type="date" name="birthday" id="birthday" placeholder="请输入出生日期"></td>
            </tr>

            <tr>
            <td class="td_left"><label for="checkcode" >验证码</label></td>
            <td class="td_right"><input type="text" name="checkcode" id="checkcode" placeholder="请输入验证码">
            <img id="img_check" src="img/verify_code.jpg">
            </td>
            </tr>


            <tr>
            <td colspan="2" align="center"><input type="submit" value="注册" id="submit"></td>
            </tr>
            </table>
            </form>
            </div>
            </div>

            <script>
            //在开始加载的时候绑定事件的好习惯
            window.onload = function(){
            document.getElementById("form").onsubmit = function (){
            return checkUsername()&&checkPassword();
            };
            document.getElementById("username").onblur = checkUsername;
            document.getElementById("password").onblur = checkPassword;
            }
            let username = document.getElementById("username");
            let password = document.getElementById("password");

            function checkUsername(){
            let reg_username = /^\w{6,12}$/;
            let flag = reg_username.test(username.value);
            if (flag){
            username.parentNode.parentNode.lastElementChild.innerHTML = "<img src=\"./img/gou.png\" alt=\"\">";
            }
            else{
            username.parentNode.parentNode.lastElementChild.innerHTML = "<font color=\"red\">格式错误!</font>";
            }
            return flag;
            }

            function checkPassword(){
            let reg_username = /^\w{6,12}$/;
            let flag = reg_username.test(password.value);
            if (flag){
            password.parentNode.parentNode.lastElementChild.innerHTML = "<img src=\"./img/gou.png\" alt=\"\">";
            }
            else{
            password.parentNode.parentNode.lastElementChild.innerHTML = "<font color=\"red\">格式错误!</font>";
            }
            return flag;
            }

            </script>
            </body>
            </html>
            + +

            BOM

            浏览器对象模型,将浏览器各个组成部分封装成对象。

            +

            image-20221224152804747

            +

            Window对象包含DOM对象。

            +

            组成:Window、Navigator、Screen、History、Location

            +
            Window

            不需要创建,直接用window.使用,也可以直接用方法名。比如alert

            +
            方法
              +
            1. 与弹出有关的方法

              +

              alert:弹出警告框; confirm:确认取消对话框。确定返回true;prompt:输入框。参数为输入提示,返回值为输入值。

            2. -
            3. 执行流程

              -

              每条指令的执行过程分为4段(只考虑浮点计算)

              -
                -
              1. 流出

                -

                如果①所需功能部件空闲(结构冲突) ②其他正在执行指令目的寄存器与当前不同(WAW冲突),则流出

                +
              2. 与开关有关的方法

                +

                close:关闭调用的window对象的浏览器窗口;open:打开新窗口,可传入URL,返回新的window对象

              3. -
              4. 读操作数

                -

                记分牌监测操作数可用性,可用时通知功能部件从寄存器中读出源操作数开始执行(RAW冲突)

                +
              5. 定时器

                +
                //只执行一次
                setTimeout();
                clearTimeout();
                //间隔执行多次
                setInterval();
                clearInterval();
                + +
                //一次性定时器
                //setTimeout("fun();",2000);
                var id = setTimeout(fun,2000);
                //取消
                clearTimeout(id);
                function fun(){
                alert('boom~~');
                }

                //循环定时器
                var id = setInterval(fun,2000);
                clearInterval(id);
              6. +
              +
              属性
                +
              1. 获取其他BOM对象

                +

                history、location、navigator、screen

              2. -
              3. 写结果

                -

                记分牌监测是否完成执行,若不存在or已消失WAR,则写入;存在,等待

                +
              4. 获取DOM对象

                +

                document

              +
              练习:轮播图
              <!DOCTYPE html>
              <html lang="ch">
              <head>
              <meta charset="UTF-8">
              <title>轮播图</title>
              </head>
              <body>
              <img src="./img/banner_1.jpg" width="100%" id="picture">
              <script>
              let pictures = ["./img/banner_1.jpg","./img/banner_2.jpg","./img/banner_3.jpg"];
              let i = 0;
              let img = document.getElementById("picture");
              function change_picture(){
              img.src = pictures[(++i)%3];
              }
              setInterval(change_picture,2000);
              </script>
              </body>
              </html>
              + +
              Location
                +
              1. 刷新

                +

                location.reload方法

              2. -
              3. 性能分析

                -

                img

                +
              4. 设置或返回完整的url

                +

                location.href属性

              -

              Tomasulo算法

                -
              1. 核心思想

                -

                记录和检测指令相关,操作数一旦就绪立刻执行,把发生RAW的可能减到最小;

                -

                通过寄存器换名消除WAR和WAW(上面的记分牌是通过等待)

                -

                img

                -
              2. -
              3. 基本结构

                -
                  -
                1. 保留站

                  -

                  每个保留一条已经流出并且等待到本功能部件执行的指令的相关信息。包括操作数、操作码以及各种元数据。

                  -

                  img

                  -

                  img

                  -

                  故而,需要有以下字段:

                  +
                  练习:自动返回首页
                  <!DOCTYPE html>
                  <html lang="ch">
                  <head>
                  <meta charset="UTF-8">
                  <title>自动跳转</title>
                  </head>
                  <body>
                  <div style="width: 235px; height: 100px; margin: auto">
                  <span style="color: red" id="second">5</span>秒后,自动跳转首页
                  </div>
                  <script>
                  let num = 5;
                  let second = document.getElementById("second");
                  function dao_ji_shi(){
                  if (num == 0){
                  location.href = "https://www.baidu.com/";
                  clearInterval(id);
                  }
                  second.innerHTML = (num--);
                  }
                  let id = setInterval(dao_ji_shi,1000);
                  </script>
                  </body>
                  </html>
                  + +

                  Bootstrap

                  web前端框架

                  +

                  image-20221225160246028

                  +

                  快速入门

                  image-20221225161243229

                  +

                  基本模板:

                  +
                  <!DOCTYPE html>
                  <html lang="zh-CN">
                  <head>
                  <meta charset="utf-8">
                  <meta http-equiv="X-UA-Compatible" content="IE=edge">
                  <meta name="viewport" content="width=device-width, initial-scale=1">
                  <!-- 上述3个meta标签*必须*放在最前面,任何其他内容都*必须*跟随其后! -->
                  <title>Bootstrap 101 Template</title>

                  <link href="./css/bootstrap.min.css" rel="stylesheet">
                  </head>
                  <body>
                  <h1>你好,世界!</h1>

                  <script src="./jquery.min.js"></script>
                  <script src="js/bootstrap.min.js"></script>
                  </body>
                  </html>
                  + +

                  响应式布局

                  实现依赖于栅格系统。

                  +

                  栅格系统

                  将一行平均分成12个格子,可以指定元素占几个格子

                  +

                  可以感受到,其实跟我们之前那个纯纯HTML做页面的思想是差不多的,都是把整个页面看做一个表,表有很多行,每行有不同的格子。

                  +
                  基本原理
                    +
                  1. 定义容器。相当于之前的table
                  2. +
                    -
                  • Op:操作

                    -
                  • -
                  • Qj,Qk:操作数保留站号

                    -
                  • -
                  • Vj,Vk:源操作数值

                    -

                    load的Vk保存偏移量

                    -
                  • -
                  • Busy

                    -
                  • -
                  • A:存放立即数字段 or 有效地址,仅用于load和store缓冲器

                    -
                  • -
                  • Qi:寄存器状态表

                    -

                    存放把结果写入该寄存器的保留站ID

                    -
                  • -
                  -
                2. -
                3. 公共数据总线CDB

                  -

                  用于发送各个功能部件的计算结果。如果具有多个执行部件且采用多流出流水线,则需要采用多条CDB。

                  -
                4. -
                5. load缓冲器和store缓冲器

                  +
                6. 容器分类

                    -
                  1. load缓冲器
                      -
                    1. 存放用于计算有效地址的分量
                    2. -
                    3. 记录正在进行的load访存
                    4. -
                    5. 保存buffer等待CDB传输
                    6. +
                    7. container:两边留白
                    8. +
                    9. container-fluid:每一种设备都是100%宽度
                  2. -
                  3. store缓冲器
                      -
                    1. 存放用于计算有效地址的分量
                    2. -
                    3. 记录正在进行的store访存,如目标地址以及是否已有数据
                    4. -
                    5. 保存buffer等待CDB传输
                    6. + +
                        +
                      1. 定义行。相当于之前的tr 样式:row
                      2. +
                      3. 定义元素。指定该元素在不同的设备上,所占的格子数目。样式:col-设备代号-格子数目
                      - +
                        +
                      • 设备代号:

                        +
                          +
                        1. xs:超小屏幕 手机 (<768px):col-xs-12
                        2. +
                        3. sm:小屏幕 平板 (≥768px)
                        4. +
                        5. md:中等屏幕 桌面显示器 (≥992px)
                        6. +
                        7. lg:大屏幕 大桌面显示器 (≥1200px)
                      • -
                      • 浮点寄存器FP

                        -

                        img

                        +
                      +
                      使用方法
                      <!DOCTYPE html>
                      <html lang="zh-CN">
                      <head>
                      <meta charset="utf-8">
                      <meta http-equiv="X-UA-Compatible" content="IE=edge">
                      <meta name="viewport" content="width=device-width, initial-scale=1">
                      <!-- 上述3个meta标签*必须*放在最前面,任何其他内容都*必须*跟随其后! -->
                      <title>Bootstrap HelloWorld</title>

                      <!-- Bootstrap -->
                      <link href="css/bootstrap.min.css" rel="stylesheet">


                      <!-- jQuery (Bootstrap 的所有 JavaScript 插件都依赖 jQuery,所以必须放在前边) -->
                      <script src="js/jquery-3.2.1.min.js"></script>
                      <!-- 加载 Bootstrap 的所有 JavaScript 插件。你也可以根据需要只加载单个插件。 -->
                      <script src="js/bootstrap.min.js"></script>
                      <style>
                      .inner{
                      border:1px solid red;
                      }

                      </style>
                      </head>
                      <body>
                      <!--1.定义容器-->
                      <div class="container-fluid">
                      <!--2.定义行-->
                      <div class="row">
                      <!--3.定义元素
                      在大显示器一行12个格子
                      在pad上一行6个格子
                      -->
                      <div class="col-lg-1 col-sm-2 inner">栅格</div>
                      <div class="col-lg-1 col-sm-2 inner">栅格</div>
                      <div class="col-lg-1 col-sm-2 inner">栅格</div>
                      <div class="col-lg-1 col-sm-2 inner">栅格</div>
                      <div class="col-lg-1 col-sm-2 inner">栅格</div>
                      <div class="col-lg-1 col-sm-2 inner">栅格</div>
                      <!--超出12个格子的部分自动换行-->
                      <div class="col-lg-1 col-sm-2 inner">栅格</div>
                      <div class="col-lg-1 col-sm-2 inner">栅格</div>
                      <div class="col-lg-1 col-sm-2 inner">栅格</div>
                      <div class="col-lg-1 col-sm-2 inner">栅格</div>
                      <div class="col-lg-1 col-sm-2 inner">栅格</div>
                      <div class="col-lg-1 col-sm-2 inner">栅格</div>
                      </div>

                      </div>

                      </body>
                      </html>
                      + +

                      样式

                      看文档。

                      +

                      练习:用bootstrap优化旅游网站首页

                      代码

                      HTML
                      <!DOCTYPE html>
                      <html lang="zh-CN">
                      <head>
                      <meta charset="utf-8">
                      <meta http-equiv="X-UA-Compatible" content="IE=edge">
                      <meta name="viewport" content="width=device-width, initial-scale=1">
                      <!-- 上述3个meta标签*必须*放在最前面,任何其他内容都*必须*跟随其后! -->
                      <title>Bootstrap 101 Template</title>

                      <link href="./css/bootstrap.min.css" rel="stylesheet">
                      <link rel="stylesheet" href="./3.css">
                      </head>
                      <body>
                      <div class="container-fluid">
                      <div class="row">
                      <img src="./image/top_banner.jpg" class="img-responsive" id="top">
                      </div>

                      <div class="row" id="top_2">
                      <div class="col-md-3">
                      <img src="./image/logo.jpg" class="img-responsive" id="logo">
                      </div>
                      <div class="col-md-6" id="search">
                      <input type="text" placeholder="Search" id="search_text">
                      <div id="search_but">
                      <a href="#">搜索</a>
                      </div>
                      </div>
                      <div class="col-md-3">
                      <img src="./image/hotel_tel.png" alt="hotel">
                      </div>
                      </div>

                      <div class="row">
                      <nav class="navbar navbar-default">
                      <div class="container-fluid">
                      <!-- Brand and toggle get grouped for better mobile display -->
                      <div class="navbar-header">
                      <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1" aria-expanded="false">
                      <span class="sr-only">Toggle navigation</span>
                      <span class="icon-bar"></span>
                      <span class="icon-bar"></span>
                      <span class="icon-bar"></span>
                      </button>
                      <a class="navbar-brand" href="#">传智播客</a>
                      </div>

                      <!-- Collect the nav links, forms, and other content for toggling -->
                      <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
                      <ul class="nav navbar-nav">
                      <li><a href="#">Android <span class="sr-only">(current)</span></a></li>
                      <li><a href="#">Android</a></li>
                      <li><a href="#">Android</a></li>
                      <li><a href="#">Android</a></li>
                      <li><a href="#">Android</a></li>
                      <li><a href="#">Android</a></li>
                      <li><a href="#">Android</a></li>
                      </ul>
                      </div><!-- /.navbar-collapse -->
                      </div><!-- /.container-fluid -->
                      </nav>
                      </div>

                      <!-- 轮播图-->
                      <div class="row">
                      <div id="carousel-example-generic" class="carousel slide" data-ride="carousel">
                      <!-- Indicators -->
                      <ol class="carousel-indicators">
                      <li data-target="#carousel-example-generic" data-slide-to="0" class="active"></li>
                      <li data-target="#carousel-example-generic" data-slide-to="1"></li>
                      <li data-target="#carousel-example-generic" data-slide-to="2"></li>
                      </ol>

                      <!-- Wrapper for slides -->
                      <div class="carousel-inner" role="listbox">
                      <!-- 注意只能有其中一张的class是active的-->
                      <div class="item active">
                      <img src="./image/banner_1.jpg" alt="...">
                      <div class="carousel-caption">
                      </div>
                      </div>
                      <div class="item">
                      <img src="./image/banner_2.jpg" alt="...">
                      <div class="carousel-caption">
                      </div>
                      </div>
                      <div class="item">
                      <img src="./image/banner_3.jpg" alt="...">
                      <div class="carousel-caption">
                      </div>
                      </div>
                      </div>

                      <!-- Controls -->
                      <a class="left carousel-control" href="#carousel-example-generic" role="button" data-slide="prev">
                      <span class="glyphicon glyphicon-chevron-left" aria-hidden="true"></span>
                      <span class="sr-only">Previous</span>
                      </a>
                      <a class="right carousel-control" href="#carousel-example-generic" role="button" data-slide="next">
                      <span class="glyphicon glyphicon-chevron-right" aria-hidden="true"></span>
                      <span class="sr-only">Next</span>
                      </a>
                      </div>
                      </div>

                      <div class="container">
                      <div class="row">
                      <img src="./image/icon_5.jpg" alt="亲子周边旅游节">
                      <span class="text1">黑马精选</span>
                      <hr>
                      </div>
                      <div class="row block">
                      <div class="xuanchuan col-md-3">
                      <img src="./image/jiangxuan_1.jpg" alt="" >
                      <br>上海飞三亚五天4晚自由行(春节销售+亲子+蜜月+自由行)<br>
                      <font size = 3 color="#8b0000">¥899</font>
                      </div>
                      <div class="xuanchuan col-md-3">
                      <img src="./image/jiangxuan_1.jpg" alt="" >
                      <br>上海飞三亚五天4晚自由行(春节销售+亲子+蜜月+自由行)<br>
                      <font size = 3 color="#8b0000">¥899</font>
                      </div>
                      <div class="xuanchuan col-md-3">
                      <img src="./image/jiangxuan_1.jpg" alt="">
                      <br>上海飞三亚五天4晚自由行(春节销售+亲子+蜜月+自由行)<br>
                      <font size = 3 color="#8b0000">¥899</font>
                      </div>
                      <div class="xuanchuan col-md-3">
                      <img src="./image/jiangxuan_1.jpg" alt="">
                      <br>上海飞三亚五天4晚自由行(春节销售+亲子+蜜月+自由行)<br>
                      <font size = 3 color="#8b0000">¥899</font>
                      </div>
                      </div>


                      <div class="row">
                      <img src="./image/icon_6.jpg" alt="亲子周边旅游节">
                      <span class="text1">国内游</span>
                      <hr>
                      </div>
                      <div class="row">
                      <div class="col-md-4">
                      <img src="./image/guonei_1.jpg" alt="亲子周边旅游节">
                      </div>
                      <div class="col-md-8">
                      <div class="xuanchuan col-md-4">
                      <img src="./image/jiangxuan_2.jpg" alt="" >
                      <br>上海飞三亚五天4晚自由行(春节销售+亲子+蜜月+自由行)<br>
                      <font size = 3 color="#8b0000">¥899</font>
                      </div>
                      <div class="xuanchuan col-md-4">
                      <img src="./image/jiangxuan_1.jpg" alt="" >
                      <br>上海飞三亚五天4晚自由行(春节销售+亲子+蜜月+自由行)<br>
                      <font size = 3 color="#8b0000">¥899</font>
                      </div>
                      <div class="xuanchuan col-md-4">
                      <img src="./image/jiangxuan_1.jpg" alt="" >
                      <br>上海飞三亚五天4晚自由行(春节销售+亲子+蜜月+自由行)<br>
                      <font size = 3 color="#8b0000">¥899</font>
                      </div>
                      <div class="xuanchuan col-md-4">
                      <img src="./image/jiangxuan_1.jpg" alt="" >
                      <br>上海飞三亚五天4晚自由行(春节销售+亲子+蜜月+自由行)<br>
                      <font size = 3 color="#8b0000">¥899</font>
                      </div>
                      <div class="xuanchuan col-md-4">
                      <img src="./image/jiangxuan_1.jpg" alt="">
                      <br>上海飞三亚五天4晚自由行(春节销售+亲子+蜜月+自由行)<br>
                      <font size = 3 color="#8b0000">¥899</font>
                      </div>
                      <div class="xuanchuan col-md-4">
                      <img src="./image/jiangxuan_1.jpg" alt="">
                      <br>上海飞三亚五天4晚自由行(春节销售+亲子+蜜月+自由行)<br>
                      <font size = 3 color="#8b0000">¥899</font>
                      </div>
                      </div>
                      </div>
                      </div>
                      <div class="row">
                      <img src="./image/footer_service.png" alt="" class="img-responsive">
                      <div id="foot_text" class="col-md-12">
                      <font color="gray" size = 2>江苏传智播客教育科技股份有限公司 版权所有Copyright 2006-2018&copy;, All Rights Reserved 苏ICP备16007882</font>
                      </div>
                      </div>
                      </div>

                      <script src="./jquery-3.2.1.min.js"></script>
                      <script src="js/bootstrap.min.js"></script>
                      </body>
                      </html>
                      + +
                      CSS
                      *{
                      margin: 0px;
                      padding: 0px;
                      }

                      #search{
                      text-align: center;
                      }

                      #top_2{
                      margin-top: 10px;
                      }

                      #search_text{
                      border: 3px solid orange;
                      width: 400px;
                      height: 50px;
                      margin-right: 0px;
                      text-align: left;
                      padding: 15px;
                      }

                      #search_but{
                      margin: 0px;
                      width: 90px;
                      height: 50px;
                      display: inline-block;
                      background: orange;
                      box-sizing: border-box;
                      vertical-align: top;
                      padding: 13px;
                      }

                      #search_but a{
                      color: white;
                      }

                      .text1{
                      font-size: 15px;
                      }

                      hr{
                      margin: 3px;
                      border: 1px solid orange;
                      background: orange;
                      }

                      .xuanchuan{
                      padding: 6px;
                      margin: auto;
                      height: 244px;
                      border: 1px solid dimgray;
                      text-align: center;
                      }

                      .xuanchuan img{
                      width: 90%;
                      }

                      .block{
                      text-align: center;
                      }

                      #foot_text{
                      height: 60px;
                      width: 100%;
                      background: orange;
                      text-align: center;
                      padding: 15px;
                      }

                      .row{
                      margin-top: 10px;
                      margin-bottom: 10px;
                      }
                      + +

                      注意点

                      这东西写了我还挺久的。。。不过收获也挺多。

                      +
                        +
                      1. text-align

                        +

                        是一个css属性,我觉得挺好用的(。我不知道它精确是什么意思,但我发现它好像有种能让该元素下的子元素水平居中的效果。

                      2. -
                      3. 指令队列

                        -

                        FIFO

                        +
                      4. 关于“容器”的理解

                        +

                        上面说过,Bootstrap有个容器的概念,跟我们上面纯HTML的表格概念其实是很类似的。

                        +

                        HTML的容器是表格标签,Bootsrap的容器是container-fluid和container类的标签。

                        +

                        与HTML的表格相同,“容器”也是可以嵌套的。这点在本案例体现为一下两点:

                        +

                        ① container中可以嵌套container-fluid。

                        +

                        ​ 案例中,页首-轮播图和页尾这两段是两边不留白的,轮播图-页尾这段是两边留白的。所以,我们就可以让整体为一个container容器,中间一段再用container-fluid容器包装起来。也即:

                        +
                        <body>
                        <div class="container-fluid">
                        <div class="row"></div>
                        <div class="container"></div>
                        <div class="row"></div>
                        </div>
                        </body>
                        + +

                        注意,此处不要作死为了优雅统一性这样写:

                        +
                        <div class="row container"></div>
                        + +

                        也即多加一个row类。要不row的属性会覆盖掉container的。

                        +

                        ② 对于“col-md-4”这些的理解

                        +

                        image-20221225183910393

                        +

                        在做这样的包含row-span元素的行时,之前的解决方案是采用表格嵌套。同样的,这里也可以采用容器嵌套。而此时,列的书写方式就比较特殊了。

                        +
                        <div class="row">
                        <div class="col-md-4">
                        row-span的图片
                        </div>

                        <div class="col-md-8">
                        <div class="col-md-4"></div>
                        <div class="col-md-4"></div>
                        <div class="col-md-4"></div>
                        <div class="col-md-4"></div>
                        <div class="col-md-4"></div>
                        <div class="col-md-4"></div>
                        </div>
                        </div>
                        + +

                        其实是非常直观的,相信以后你看到这段应该也能理解(。提示一点,栅格系统其实好像是相对于父类的。也就是说,不是“把整个页面分成12个格子”,而是,“把父类占有的空间分成12个格子”。

                      5. -
                      6. 运算部件

                        -

                        浮点加法器、浮点乘法器

                        +
                      7. 关于hr标签

                        +

                        使用css改颜色时应该写background: orange;而不是color: orange;

                      +

                      XML

                      xml叫做可扩展标签语言。它的全部标签都是自定义的。

                      +

                      image-20221225220356932

                      +

                      快速入门

                        +
                      • 基本语法:
                          1. xml文档的后缀名 .xml
                        +  2. xml第一行必须定义为文档声明
                        +  3. xml文档中有且仅有一个根标签
                        +  4. 属性值必须使用引号(单双都可)引起来
                        +  5. 标签必须正确关闭
                        +  6. xml标签名称区分大小写
                        +
                      • -
                      • 寄存器换名实现

                        -

                        当指令流出,如果操作数缺失,则将指令数据换名为保留站编号

                        +
                      +
                      <?xml version="1.0" encoding="utf-8" ?>

                      <users>
                      <user id="1">
                      <name>zhangsan</name>
                      <age>23</age>
                      <gender>male</gender>
                      </user>
                      <user id="2">
                      <name>zhangsan</name>
                      <age>23</age>
                      <gender>male</gender>
                      </user>
                      <user id="3">
                      <name>zhangsan</name>
                      <age>23</age>
                      <gender>male</gender>
                      </user>
                      </users>
                      + +

                      细说

                      语法

                      文档声明

                      常见属性:version[必须]、encoding、standalone[取值为yes和no,yes为依赖其他文件]

                      +
                      属性

                      id属性值唯一

                      +
                      文本

                      image-20221225222029698

                      +

                      这种要转义来转义去的显然很麻烦。所以就需要用到CDATA区。

                      +

                      CDATA区的文本会被原样展示。

                      +
                      <code>

                      <![CDATA[
                      if(1 == 1 && 2 == 2){}
                      ]]>

                      </code>
                      + +

                      约束

                      只能写约束文件内的标签

                      +
                      dtd
                      文档
                      //students标签里可以包含若干个student标签
                      <!ELEMENT students (student*) >
                      //student标签必须按顺序出现name,age,sex标签
                      <!ELEMENT student (name,age,sex)>
                      //name、age、sex都为字符串类型
                      <!ELEMENT name (#PCDATA)>
                      <!ELEMENT age (#PCDATA)>
                      <!ELEMENT sex (#PCDATA)>
                      //student标签有一个属性叫number,类型为ID,并且必须要有。类型为ID表示该属性值唯一。
                      <!ATTLIST student number ID #REQUIRED>
                      + +
                      引入方式
                      //外部引入
                      <!DOCTYPE 根标签名 SYSTEM "dtd文件的位置">
                      <!DOCTYPE 根标签名 PUBLIC "dtd文件名字" "dtd文件的位置URL">
                      + +

                      或者可以直接在xml内部写:

                      +
                      <?xml version="1.0" encoding="UTF-8" ?>
                      <!DOCTYPE students SYSTEM "student.dtd">

                      <!DOCTYPE students [

                      <!ELEMENT students (student+) >
                      <!ELEMENT student (name,age,sex)>
                      <!ELEMENT name (#PCDATA)>
                      <!ELEMENT age (#PCDATA)>
                      <!ELEMENT sex (#PCDATA)>
                      <!ATTLIST student number ID #REQUIRED>


                      ]>
                      <students>

                      <student number="s001">
                      <name>zhangsan</name>
                      <age>abc</age>
                      <sex>hehe</sex>
                      </student>

                      <student number="s002">
                      <name>lisi</name>
                      <age>24</age>
                      <sex>female</sex>
                      </student>

                      </students>
                      + +
                      使用
                      <student number="s001">
                      <name>zhangsan</name>
                      <age>abc</age>
                      <sex>hehe</sex>
                      </student>

                      <student number="s002">
                      <name>lisi</name>
                      <age>24</age>
                      <sex>female</sex>
                      </student>
                      + +
                      schema
                      文档
                      <?xml version="1.0"?>
                      <xsd:schema xmlns="http://www.itcast.cn/xml"
                      xmlns:xsd="http://www.w3.org/2001/XMLSchema"
                      targetNamespace="http://www.itcast.cn/xml" elementFormDefault="qualified">

                      <xsd:element name="students" type="studentsType"/>
                      //定义类型
                      <xsd:complexType name="studentsType">
                      //类型里面有有序序列
                      <xsd:sequence>
                      <xsd:element name="student" type="studentType" minOccurs="0" maxOccurs="unbounded"/>
                      </xsd:sequence>
                      </xsd:complexType>

                      <xsd:complexType name="studentType">
                      <xsd:sequence>
                      <xsd:element name="name" type="xsd:string"/>
                      <xsd:element name="age" type="ageType" />
                      <xsd:element name="sex" type="sexType" />
                      </xsd:sequence>
                      <xsd:attribute name="number" type="numberType" use="required"/>
                      </xsd:complexType>

                      <xsd:simpleType name="sexType">
                      <xsd:restriction base="xsd:string">
                      //枚举类型
                      <xsd:enumeration value="male"/>
                      <xsd:enumeration value="female"/>
                      </xsd:restriction>
                      </xsd:simpleType>

                      <xsd:simpleType name="ageType">
                      <xsd:restriction base="xsd:integer">
                      <xsd:minInclusive value="0"/>
                      <xsd:maxInclusive value="256"/>
                      </xsd:restriction>
                      </xsd:simpleType>

                      <xsd:simpleType name="numberType">
                      <xsd:restriction base="xsd:string">
                      //正则匹配
                      <xsd:pattern value="heima_\d{4}"/>
                      </xsd:restriction>
                      </xsd:simpleType>
                      </xsd:schema>
                      + +
                      引入方式
                      <?xml version="1.0" encoding="UTF-8" ?>
                      <!--
                      1.填写xml文档的根元素
                      2.引入xsi前缀. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                      3.引入xsd文件命名空间. xsi:schemaLocation="http://www.itcast.cn/xml student.xsd"
                      4.为每一个xsd约束声明一个前缀,作为标识 xmlns="http://www.itcast.cn/xml"


                      -->
                      <students xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                      xmlns="http://www.itcast.cn/xml"
                      xsi:schemaLocation="http://www.itcast.cn/xml student.xsd"
                      >
                      <student number="heima_0001">
                      <name>tom</name>
                      <age>18</age>
                      <sex>male</sex>
                      </student>

                      </students>
                      + +

                      它这意识就是,每个schema文件都要起一个别名,比如xsi:schemaLocation="http://www.itcast.cn/xml student.xsd"这行代码实际上就是把student.xsd的别名起为了http://www.itcast.cn/xml

                      +

                      为什么要起名呢?这就类似于命名空间这种东西,你要用到一个标签需要指明这个标签从哪来的,比如std::vectorClass.toString这种。这种命名空间在xml里叫前缀。所以,实际上完整写法应该是<http://www.itcast.cn/xml:students>

                      +

                      但这样显然太麻烦了,别名一般都是这种网址,写起来太长了。所以我们选择给别名起别名,设置方法为xmlns:a="http://www.itcast.cn/xml",这样一来,以后就不用写<http://www.itcast.cn/xml:students>,只用写<a:students>了。

                      +

                      但是如果每个都写一个前缀还是有点难顶。所以就引入了一个空前缀。这样写<students>这样没有前缀的标签,就相当于从空前缀那个命名空间里拿出来的了。当然如果有多个命名空间,还是得区分一下的。

                      +

                      解析

                      image-20221225234106078

                      +

                      解析方式有两种方法。

                      +
                      解析方法
                      DOM

                      将标记语言文档一次性加载进内存,形成DOM树

                      +

                      操作方便,可以进行CRUD所有操作;但占内存

                      +
                      SAX

                      逐行读取,基于事件驱动

                      +

                      不占内存;但只能读取

                      +
                      解析工具

                      image-20221225234649287

                      +

                      主要学习Jsoup。

                      +
                      快速入门

                      image-20221225234813135

                      +

                      跟前面html的DOM是差不多的。

                      +
                      public class JsoupDemo {
                      public static void main(String[] args) throws IOException {
                      //获取Document对象,根据xml文档
                      //解析xml文档(加载进内存且获取dom树)
                      Document doc = Jsoup.parse(new File(JsoupDemo.class.getClassLoader()
                      .getResource("student.xml").getPath()),"utf-8");
                      //获取元素对象
                      //Elements extends ArrayList<Element>
                      Elements ele = doc.getElementsByTag("name");
                      //获取元素里的数据
                      System.out.println(ele.get(0).text());
                      }
                      }
                      + +
                      细说
                        +
                      1. Jsoup:工具类,可以解析html或xml文档,返回Document

                        +
                          +
                        • parse:解析html或xml文档,返回Document
                            +
                          • parse(File in, String charsetName):解析xml或html文件的。
                          • +
                          • parse(String html):解析xml或html字符串
                          • +
                          • parse(URL url, int timeoutMillis):通过网络路径获取指定的html或xml的文档对象
                          • +
                        • -
                        • 特点

                          -
                            -
                          1. 冲突检测与指令执行是分布的

                            -

                            通过保留站和CDB实现

                            -

                            计算结果通过CDB直接从产生它的保留站传送到所有需要它的功能部件,无需经过寄存器

                            +
                      2. -
                      3. 消除了WAW和WAR

                        +
                      4. Document:文档对象。代表内存中的dom树

                        +
                          +
                        • 获取Element对象
                            +
                          • getElementById(String id):根据id属性值获取唯一的element对象
                          • +
                          • getElementsByTag(String tagName):根据标签名称获取元素对象集合
                          • +
                          • getElementsByAttribute(String key):根据属性名称获取元素对象集合
                          • +
                          • getElementsByAttributeValue(String key, String value):根据对应的属性名和属性值获取元素对象集合
                          • +
                        • -
                      + -
                    7. 执行步骤

                      -

                      3段流水

                      -
                        -
                      1. 流出

                        -

                        如果操作要求的保留站空闲(结构冲突),则送到保留站r。如果操作数已就绪,填入;否则,填入产生该操作数的保留站ID(寄存器换名,消除WAW、WAR)。

                        +
                      2. Elements:元素Element对象的集合。可以当做 ArrayList来使用

                      3. -
                      4. 执行

                        -

                        两个操作数就绪后,就可以用保留站对应功能部件执行

                        -

                        img

                        +
                      5. Element:元素对象

                        +
                          +
                        1. 获取子元素对象
                        2. +
                        +

                        这一点很好理解。因为Document和Element对象的获取元素方法都继承自Node结点,本意就是获取子元素对象。只不过Document是根节点,所以就变成了获取所有元素对象。

                        +
                          +
                        • getElementById(String id):根据id属性值获取唯一的element对象
                        • +
                        • getElementsByTag(String tagName):根据标签名称获取元素对象集合
                        • +
                        • getElementsByAttribute(String key):根据属性名称获取元素对象集合
                        • +
                        • getElementsByAttributeValue(String key, String value):根据对应的属性名和属性值获取元素对象集合
                        • +
                        +
                          +
                        1. 获取属性值
                        2. +
                        +
                          +
                        • String attr(String key):根据属性名称获取属性值
                        • +
                        +
                          +
                        1. 获取文本内容
                        2. +
                        +
                          +
                        • String text():获取字标签的所有纯文本内容
                        • +
                        • String html():获取标签体的所有内容(包括字标签的字符串内容)
                        • +
                      6. -
                      7. 写结果

                        -

                        计算完毕后由CDB传送

                        +
                      8. Node:节点对象

                        +
                          +
                        • 是Document和Element的父类
                        • +
                      -
                    8. +
                      快速查找
                        +
                      1. 使用选择器selector

                        +

                        其实语法格式跟css的那个选择器差不多。

                        +
                        /**
                        *选择器查询
                        */
                        public class JsoupDemo5 {
                        public static void main(String[] args) throws IOException {
                        //1.获取student.xml的path
                        String path = JsoupDemo5.class.getClassLoader().getResource("student.xml").getPath();
                        //2.获取Document对象
                        Document document = Jsoup.parse(new File(path), "utf-8");

                        //3.查询name标签
                        /*
                        div{

                        }
                        */
                        Elements elements = document.select("name");
                        System.out.println(elements);
                        System.out.println("=----------------");
                        //4.查询id值为itcast的元素
                        Elements elements1 = document.select("#itcast");
                        System.out.println(elements1);
                        System.out.println("----------------");
                        //5.获取student标签并且number属性值为heima_0001的age子标签
                        //5.1.获取student标签并且number属性值为heima_0001
                        Elements elements2 = document.select("student[number=\"heima_0001\"]");
                        System.out.println(elements2);
                        System.out.println("----------------");

                        //5.2获取student标签并且number属性值为heima_0001的age子标签
                        Elements elements3 = document.select("student[number=\"heima_0001\"] > age");
                        System.out.println(elements3);

                        }

                        }
                      2. +
                      3. 使用XPath

                        +

                        XPath:xml路径语言。

                        +

                        XPath API文档

                        +
                        /**
                        *XPath查询
                        */
                        public class JsoupDemo6 {
                        public static void main(String[] args) throws IOException, XpathSyntaxErrorException {
                        //1.获取student.xml的path
                        String path = JsoupDemo6.class.getClassLoader().getResource("student.xml").getPath();
                        //2.获取Document对象
                        Document document = Jsoup.parse(new File(path), "utf-8");

                        //3.根据document对象,创建JXDocument对象
                        JXDocument jxDocument = new JXDocument(document);

                        //4.结合xpath语法查询
                        //4.1查询所有student标签
                        List<JXNode> jxNodes = jxDocument.selN("//student");
                        for (JXNode jxNode : jxNodes) {
                        System.out.println(jxNode);
                        }

                        System.out.println("--------------------");

                        //4.2查询所有student标签下的name标签
                        List<JXNode> jxNodes2 = jxDocument.selN("//student/name");
                        for (JXNode jxNode : jxNodes2) {
                        System.out.println(jxNode);
                        }

                        System.out.println("--------------------");

                        //4.3查询student标签下带有id属性的name标签
                        List<JXNode> jxNodes3 = jxDocument.selN("//student/name[@id]");
                        for (JXNode jxNode : jxNodes3) {
                        System.out.println(jxNode);
                        }
                        System.out.println("--------------------");
                        //4.4查询student标签下带有id属性的name标签 并且id属性值为itcast

                        List<JXNode> jxNodes4 = jxDocument.selN("//student/name[@id='itcast']");
                        for (JXNode jxNode : jxNodes4) {
                        System.out.println(jxNode);
                        }
                        }

                        }
                      -

                      基于硬件的前瞻执行

                      img

                      -

                      img

                      -

                      img

                      -

                      img

                      -

                      img

                      -

                      img

                      -

                      多指令流出

                      img

                      -

                      img

                      -

                      img

                      -

                      img

                      -

                      img

                      -

                      超标量实现

                      img

                      +

                      第四部分 JavaWeb核心

                      Tomcat

                      概述

                      概述

                      image-20221226154531990

                      +

                      Tomcat是Java相关的web服务器软件。

                      +

                      tomcat目录结构

                      image-20221226155107723

                      +

                      image-20221226155143920启动

                      +

                      启动时出现的问题

                      省流:看系统环境变量有没有CATALINA_HOME这一项,并且看这个CATALINA_HOME的值是否与当前版本安装路径相符合。

                      +

                      我电脑上本来也有了一个tomcat,只不过跟老师版本不一样。我把这两个都安到同一个目录了。然后我启动了老师版本,却发现输入localhost:8080没有任何响应。我首先去看了一下tomcat的config下的server.xml,发现端口号确实是8080没问题。然后试图访问localhost,发现没有响应,故推测是此处发生了问题。因而我上网按照该教程做了一遍:

                      +

                      127.0.0.1 拒绝了我们的连接请求–访问本地IP时显示拒绝访问

                      +

                      我重启电脑后,再次启动老师版本,发现还是不行。这时我开始怀疑是否我的tomcat没有正常启动,或者是否是因为8080这个端口号冲突了。所以我又找了一下如何查看端口号占用情况:

                      +

                      如何查看端口号是否被占用

                      +

                      netstat -a命令即可。我便发现,在我开着tomcat的情况下,8080这个端口没有被使用。说明好像启动不大正常。于是我打开了另一篇回答:

                      +

                      tomcat 启动了,为什么没打开 8080 端口?

                      +

                      按照它说的去查看日志文件。发现老师版本的tomcat下的log目录为空。我就去我本安装的版本下的log目录去看了,惊奇地发现,原来我在使用老师版本的tomcat时,tomcat用的是老版本的log目录。也就是说,很有可能config目录也是用的老版本的。我去查看老版本的config,发现端口是8888。于是我把老师版本的tomcat卸载了,去访问localhost:8888,成功力。

                      +

                      我探寻了以下原因,发现tomcat的startup里面如此写道:

                      +
                      if not "%CATALINA_HOME%" == "" goto gotHome
                      :gotHome
                      if exist "%CATALINA_HOME%\bin\catalina.bat" goto okHome
                      :okHome
                      rem ....
                      call "%EXECUTABLE%" start %CMD_LINE_ARGS%
                      :end
                      + +

                      这一段大概是在找到tomcat这个软件的位置。如果我们在环境变量里面设置了CATALINA_HOME,那么就会直接把软件位置定位到CATALINA_HOME的值的地方,随后之后的逻辑都在那边执行。

                      +

                      我发现我确实设置了这个CATALINA_HOME,并且:

                      +

                      image-20221226170930763

                      +

                      它的值是我电脑原本有的老版本的目录!

                      +

                      故而,这也就说明了为什么老师的版本不去用自己的log,不去用自己的config,而用的是我电脑上的老版本的log,config了。。。

                      +

                      image-20221226172653916

                      +

                      配置

                        +
                      • 部署项目的方式:

                          -
                        1. 假设每个时钟周期流出两条,1整数型指令+1浮点型指令。

                          -

                          整数型:load、store、分支

                          -

                          浮点型:可能各种运算吧

                          +
                        2. 直接将项目放到webapps目录下即可。 * /hello:项目的访问路径–>虚拟目录 * 简化部署:将项目打成一个war包,再将war包放置到webapps目录下。

                          +
                            +
                          • war包会自动解压缩
                          • +
                        3. -
                        4. 假设所有浮点指令都是加法,执行时间3个时钟周期,且图中整数总在浮点前

                          +
                        5. 配置conf/server.xml文件
                          <Host>标签体中配置
                          <Context docBase="C:\aWorkSpace\Projects\Java\JavaWeb" path="/web" />

                        -

                        img

                        -

                        img

                        -

                        img

                        -

                        没懂,难道单发射流水线就不会吗。。。

                        -

                        img

                        -

                        基于静态调度

                        img

                        -

                        基于动态调度

                        img

                        -

                        img

                        -

                        img

                        -

                        VLIW技术

                        img

                        -

                        img

                        -

                        img

                        -

                        img

                        -

                        基本指令调度和循环展开

                        img

                        -

                        img

                        -

                        指令调度

                        img

                        -

                        img

                        -

                        img

                        -

                        循环展开

                        img

                        -

                        img

                        -

                        软流水

                        img

                        -

                        img

                        -

                        03 AI处理器

                        并行体系结构

                        分类

                        SISD

                        img

                        -

                        SIMD

                        img

                        -

                        img

                        -

                        MIMD

                        img

                        -

                        向量体系结构

                        img

                        -

                        img

                        -

                        img

                        -

                        img

                        -

                        img

                        -

                        img

                        -

                        img

                        -

                        后面不知道为什么写着写着开始英文了。。。算了,看起来都不重要。

                        -

                        GPU

                        概念

                        img

                        -

                        img

                        -

                        GPU体系结构

                        img

                        -

                        img

                        -

                        GPU计算

                        img

                        -

                        CUDA编程

                        img

                        -

                        img

                        -

                        img

                        -

                        img

                        -

                        GPU中的线程是执行计算任务的最小单位,可以看作是一系列指令的执行者。每个线程都有自己的程序计数器(PC)、寄存器集和局部内存。这些线程以并行的方式执行相同的指令,但可以有不同的输入数据,从而在数据并行的模式下执行计算。

                        -

                        img

                        -

                        img

                        -

                        下面两个标题反了额

                        -

                        img

                        -

                        img

                        -

                        img

                        -

                        img

                        -

                        感觉能明白其划分一组组线程的意义了,就是方便管理,一个warp执行相同的指令代码,所以要求同时调度同时执行

                        -

                        img

                        -

                        img

                        -

                        img

                        -

                        img

                        -

                        img

                        -

                        img

                        -

                        img

                        -

                        例题

                        img

                        -

                        img

                        -

                        img

                        -

                        img

                        -

                        img

                        -

                        img

                        -

                        访存优化

                        img

                        -

                        img

                        -

                        img

                        -

                        img

                        -

                        img

                        -

                        img

                        -

                        img

                        -

                        img

                        -

                        真没懂。。。。

                        -

                        img

                        -

                        img

                        -

                        img

                        -

                        真没看懂

                        -

                        GPU控制流和指令优化

                        img

                        -

                        AI开发

                        DL框架

                        img

                        -

                        img

                        -

                        PyTorch

                        img

                        -

                        img

                        -

                        img

                        -

                        算子开发

                        img

                        -

                        img

                        -

                        TODO接下来有兴趣看吧

                        -

                        模型开发

                        img

                        -

                        04 自动驾驶体系结构

                        img

                        -

                        img

                        -

                        img

                        -

                        img

                        -]]> - - - Project0 C++ Primer - /2023/03/13/cmu15445$lab0/ - Project0 C++ Primer

                        相比于fall2022(Trie),spring2023(COW-Trie)的难度更大,算法更复杂【毕竟是要实现一个cow的数据结构】,我认为两个都很有意义,故而两个都做了。

                        -

                        其中在Trie中,由于我是第一次接触cpp,所以遇到了很多麻烦。好在经过18h+的cpp拷打后,cow-trie虽然难度大,语法也更复杂一些,但我还是很快(话虽如此也花了7、8小时23333)就完美pass了。不过效率可能还是不大高,毕竟我不熟悉cpp,很多地方可能都直接拷贝了emm希望后续学习可以加把劲。

                        -

                        Trie

                        -

                        In this project, you will implement a key-value store backed by a concurrent trie. 实现并发安全的trie

                        -

                        To simplify the explaination, we will assume tha the keys are all non-empty variable-length strings but in practice they can be any arbitrary type. key为非空变长字符串

                        -

                        The key-value store you will implement can store string keys mapped to values of any type. key必须是字符串类型,但value可以是任意类型

                        -

                        The value of a key is stored in the node representing the last character of that key.

                        -

                        image-20230312150245669

                        -
                        -

                        心得

                        感想

                        本次实验完成时间总计18h+。是的,lab0就做了这么久【难绷】

                        -

                        其实光就实验内容来看,无非就是实现trie树,算法上没有很难,最难的应该是Remove函数的编写,因为它是个递归。

                        -

                        但正如本次实验的主题C++ Primer所揭示的那样,本次实验的真正难点在于C++……而在接触本实验之前,我对c++一无所知

                        -

                        除了这个萌新debuf之外,我还不小心犯了另一件非常sb的乌龙,加上对cpp实在是太小白了,再加上这几天破事又贼多,更是让我心态大崩,差点一蹶不振不想写了(。

                        -

                        因而,整个实验在我看来十分痛苦。coding阶段,就是 语法错误-看了半天报错信息才发现哪错了-改错误-改得不对-再改-再改-再改……这样的痛苦过程在循环往复;运行阶段,就是看着stack trace发呆、用gdb调来调去还不知道为什么错了这样的痛苦过程在循环往复。好在,我还是坚持下来了,虽然内心还是很浮躁很浮躁(

                        -

                        不过总而言之,我认为这次实验给我收获挺大的。它帮助我熟悉了C++,但我认为更重要的,是它帮我矫正了心态。做这个实验之前,我内心是很浮躁的(那会破事太多了),而且因为它是lab0所以有点轻敌(对不起。。),因而我所采取的策略是“错误驱动”,也即哪里报错就百度下怎么改就行。这样的心态就导致我的debug过程极度痛苦,因为完全看不懂报错信息,压根不知道错在哪里,百度也百度不出来。于是我被迫修改了战略,去看了我一直不想看的书,学了我一直很害怕的cpp,用了我一直很抗拒的gdb调试,才发现其实都没有我想象的这么恐怖。这期间、这几天的种种心路历程,我认为是十分可贵的。

                        -

                        错误集锦

                        sb错误

                        我下载下来starter code的时候,发现找不到它要我们实现的p0_trie.h,只有这几个:

                        -

                        image-20230318154940622

                        -

                        我便觉得可能是实验代码改版了。但是我并没有多想,我觉得可能只是代码模板改版了但实验内容不变QAQ【为什么会这么觉得呢?因为我看到指导书的url为fall2022便以为这是最新版指导书,没有想到春季学期也可以开课,还有个spring2023呃呃】而且代码看起来也确实是要我们实现Trie树【虽然跟指导书说得不大一样】。故而,我就这么直接开干了。

                        -

                        写完了Tire树的逻辑【这部分确实挺简单的】之后,我就开始了漫长的痛苦且折磨的原地兜圈之旅。由于真正的spring2023的代码模板是实现COW-Trie,故而代码模板中很多地方都使用了const关键字,包括树结点以及树的children_成员。

                        -
                        // in class Trie
                        std::shared_ptr<const TrieNode> root_{nullptr};
                        template <class T> auto Get(std::string_view key) const -> const T *;
                        template <class T> auto Put(std::string_view key, T value) const -> Trie;
                        auto Remove(std::string_view key) const -> Trie;
                        // in class TrieNode
                        std::map<char, std::shared_ptr<const TrieNode>> children_;
                        - -

                        如上是spring2023的代码模板。

                        -

                        如果使用其给我们提供的COW-Tire接口来实现Trie树,就会产生巨大的矛盾。你无法在root_的孩子中插入或者删除一个树节点,因为root_指向一个const对象,其children_域也是const的。同样的,你也无法对root_的孩子的孩子集合做增删结点操作,因为它也是const的。

                        -

                        由于对C++不熟悉,通过满屏幕的报错从而搞清楚上面那些东西这件事,就花费了我很多很多时间。

                        -
                        error: no matching function for call to 
                        ‘std::map<char, std::shared_ptr<const bustub::TrieNode> >
                        ::insert(std::pair<char, std::shared_ptr<const bustub::TrieNode> >) const
                        - -

                        比如说这个错误我就看了半天完全不知道啥意思(

                        -

                        好在明白上面这点后,我很快就发现了spring2023的存在,然后切到了fall2022的正确分支【乐】

                        -

                        经过了此乌龙后,我深刻地意识到了我对C++一窍不通的程度(,比如说上面的这些const,还有比如说&是什么东西&&又是什么东西,shared_ptr又是什么东西等等等,我都不懂。故而,我压制了内心的浮躁,去简单看了一下书,了解了new的作用、左值引用右值引用、move、智能指针这几个地方,然后再去重新开始写本实验,最终果然好了不少。

                        -
                        错误使用unique_ptr::get

                        Trie::GetValue中,我本来是这么写的:

                        -
                        	std::unique_ptr<TrieNode>* t = &root_;
                        // ...
                        std::unique_ptr<TrieNodeWithValue<T>> tmp(dynamic_cast<TrieNodeWithValue<T> *>(t->get()));
                        // ...
                        }
                        - -

                        这就会导致,tmp和(*t)会指向同一块内存区域,并且它们都是unique_ptr。随后,代码块遇到}结束,tmp的析构函数被调用,那块内存区域被free,但(*t)依然指向那块内存区域,随后在释放整个Trie树时这块区域就会被再次释放,然后寄(

                        -
                        共享unique_ptr

                        有一个方法可以在不剥夺某个unique_ptr的所有权的同时,又能用另一个变量来操作该指针所指向的对象。这个方法就是——使用指向unique_ptr的指针(。

                        -

                        也即比如:std::unique_ptr<TrieNode> *

                        -
                        代码规范

                        本次实验还格外要求了代码规范问题。

                        -
                        $ make format
                        $ make check-lint
                        $ make check-clang-tidy-p0
                        +
                        然后之后访问时输入`localhost/web/JavaWeb.html`即可
                         
                        -
                        自测

                        我暂时没进行gradescope的自测,原因是它上面报了个跟我没啥关系的错,我不知道怎么改呃呃。

                        -

                        image-20230318165521340

                        -
                        In file included from /autograder/bustub/src/common/bustub_instance.cpp:17:
                        /autograder/bustub/src/include/common/bustub_instance.h:30:10: fatal error: 'libfort/lib/fort.hpp' file not found
                        #include "libfort/lib/fort.hpp"
                        +* docBase:项目存放的路径 +* path:虚拟目录 +
                        +
                          +
                        1. 在conf\Catalina\localhost创建任意名称的xml文件。在文件中编写
                          <Context docBase="C:\aWorkSpace\Projects\Java\JavaWeb" />
                            +
                          • 虚拟目录:xml文件的名称
                          • +
                          +
                        2. +
                        +
                        注意,该方法是热部署的。也就是说,可以不关闭服务器的情况下,去增删xml文件,会马上变化,而不是像上面两种方式一样重启生效。
                        +
                        +
                      • +
                      +

                      动态项目目录结构

                      项目都存放在webapp里。打开webapp中的任一个。

                      +

                      image-20221226221811747

                      +

                      WEB-INF下是动态资源,也就是Java控制的一些文件【大概这个意思】。有这个文件夹的项目是动态项目。

                      +

                      WEB-INF以外的都是静态资源。

                      +

                      image-20221226221933346

                      +

                      tomcat集成到IDEA

                      使用maven创建Web项目
                      更换maven镜像源

                      idea中Maven镜像源详细配置步骤(对所有项目)

                      +
                      创造项目

                      image-20221226235520519

                      +

                      然后等着它开始下载就行了。

                      +

                      最后的目录结构:

                      +

                      image-20221226235607604

                      +

                      如果java或者resources目录没有,自己建就行。

                      +
                      加入tomcat

                      1.

                      +

                      TOMCAT -> IDEA

                      +

                      2.

                      +

                      还有另一种更便捷的方式,就是直接添加maven的tomcat插件。在pom.xml文件里加入此段:

                      +
                      <build>
                      <plugins>
                      <plugin>
                      <groupId>org.apache.tomcat.maven</groupId>
                      <artifactId>tomcat7-maven-plugin</artifactId>
                      <version>2.2</version>
                      </plugin>
                      </plugins>
                      </build>
                      -

                      都指向说找不到这个fort。但我真的不知道它为啥找不到,因为我看CMakeLists.txt中已经加了third_party/这个include目录了,并且这个东西的路径也确实是third_party/libfort/lib/for.hpp

                      -

                      我还在CMackLists.txtsrc/CMackLists.txttools/shell/CMackLists.txt里面都加了include(${PROJECT_SOURCE_DIR}/third_party/libfort/lib/fort.hpp),但是依然报了这样的错:

                      -

                      image-20230318173941576

                      -

                      image-20230318174102171

                      -

                      它这为啥找不到我是真的很不理解。

                      -

                      所以真的很奇怪。暂且先放着吧,之后有精力研究下这些编译链接过程。

                      -

                      COW-Trie

                      -

                      CMU 15445 Project 0 (Spring 2023) 学习记录 参考了task2和一个bug

                      +

                      即可,可用alt+insert自动补全。

                      +

                      这里我出现了一个飘红报错问题,用这个可以解决:

                      +

                      maven学习 & Plugin ‘org.apache.tomcat.maven:tomcat7-maven-plugin:2.2’ not found报错解决【问题及解决过程记录】

                      +

                      然后,右键项目就可以run了:

                      +

                      image-20221227003805955

                      +

                      如果没有此选项,就去下载maven helper插件。

                      +
                      修改tomcat配置参数
                      图形化界面

                      run-edit configuration-tomcat

                      +
                      配置文件

                      image-20221230221339275

                      +

                      启动服务器时控制台前几句输出有一句这样的。对应目录下的就可以找到tomcat配置文件。

                      +

                      Servlet

                      server applet运行在服务器端的小程序

                      +

                      servlet是java编写的服务器端的程序,运行在web服务器中。作用:接收用户端发来的请求,调用其他java程序来处理请求,将处理结果返回到服务器中

                      +

                      image-20221227154053743

                      +

                      servlet是接口,定义了Java类被tomcat执行、被浏览器访问的规则。

                      +

                      image-20221227154222612

                      +

                      快速入门

                      image-20221227154841102

                      +

                      这里的配置用的是注解,具体原理在第一部分的JavaSE基础里有详细描述了。

                      +
                      +

                      使用maven创建web项目见上面的tomcat-tomcat集成到IDEA-使用maven创建web项目

                      -

                      先放个通关截图~

                      -

                      image-20230322235159843

                      -

                      总体用时(coding+debug+note)10h+

                      -

                      本次实验是在它给的接口的基础上,实现一株并发安全的cow的trie树,还有一个小小的实现upperlower函数的实验用来熟悉我们之后要写的db的东西。算法难度还是有一些的,我的coding和debug时间估摸着可能有46开。

                      -

                      总体来说整个实验还是非常有价值的,相比往年难度和意义都更上了一层。感谢实验设计者让我做到设计得这么好的实验~

                      -

                      Task1 cow-trie

                      -

                      In this task, you will need to modify trie.h and trie.cpp to implement a copy-on-write trie.

                      -

                      下面举例说明

                      -

                      Consider inserting ("ad", 2) in the above example. We create a new Node2 by reusing two of the child nodes from the original tree, and creating a new value node 2. (See figure below)

                      -

                      image-20230323000513767

                      -

                      If we then insert ("b", 3), we will create a new root, a new node and reuse the previous nodes. In this way, we can get the content of the trie before and after each insertion operation. As long as we have the root object (Trie class), we can access the data inside the trie at that time. (See figure below)

                      -

                      image-20230323000601882

                      -

                      One more example: if we then insert ("a", "abc") and remove ("ab", 1), we can get the below trie. Note that parent nodes can have values, and you will need to purge all unnecessary nodes after removal.

                      -

                      image-20230323000658620

                      -

                      To create a new node, you should use the Clone function on the TrieNode class. To reuse an existing node in the new trie, you can copy std::shared_ptr<TrieNode>: copying a shared pointer doesn’t copy the underlying data.

                      -

                      You should not manually allocate memory by using new and delete in this project. std::shared_ptr will deallocate the object when no one has a reference to the underlying object.

                      +
                      +

                      如果已经导入依赖坐标却还未生效,就点击右侧侧边栏的maven刷新。

                      +

                      Maven导入依赖后还不出现Servlet的问题

                      -

                      感想

                      task1的目标就是实现我们的cow-trie的主体,先不要求并发。

                      -

                      虽说算法上比较复杂,但是由于它图解以及代码中的注释解说都已经说得很详细了,再加上之前已经写过了trie树有一个大体框架,因而具体coding的时候思路还是比较清晰的。

                      -

                      我认为具体的难点还是在于cpp上。下面列出了几个比较有价值的错误和相关debug过程,其中const转移显示保存is_value_node_是我认为两个比较难的点。

                      -

                      错误集锦

                      const转移

                      trie.h中:

                      -
                      class Trie {
                      private:
                      // The root of the trie.
                      std::shared_ptr<const TrieNode> root_{nullptr};
                      // Create a new trie with the given root.
                      explicit Trie(std::shared_ptr<const TrieNode> root) : root_(std::move(root)) {}

                      public:
                      // Create an empty trie.
                      Trie() = default;

                      // Get the value associated with the given key.
                      // 1. If the key is not in the trie, return nullptr.
                      // 2. If the key is in the trie but the type is mismatched, return nullptr.
                      // 3. Otherwise, return the value.
                      template <class T>
                      auto Get(std::string_view key) const -> const T *;

                      // Put a new key-value pair into the trie. If the key already exists, overwrite the value.
                      // Returns the new trie.
                      template <class T>
                      auto Put(std::string_view key, T value) const -> Trie;

                      // Remove the key from the trie. If the key does not exist, return the original trie.
                      // Otherwise, returns the new trie.
                      auto Remove(std::string_view key) const -> Trie;
                      };
                      - -

                      可以看到,为了呼应我们的cow-trie,在语法上强制性要求不能“directly modify”,它将root_children_->second同时设置为了一个指向对象为const的指针。而这意味着什么呢?意味着我们不能修改root_的内容,也不能修改root_->children_->second的内容,同样的孩子的孩子也不行。这就需要我们在Put方法中遍历trie时,对遍历路径上的每个结点都需要copy一次,故而我们的代码具体是如下实现的:

                      -

                      首先,利用TrieNode::Clone()方法来创造一个非const指针的新root:

                      -
                      // in trie.h  TrieNode{}
                      virtual auto Clone() const -> std::unique_ptr<TrieNode> { return std::make_unique<TrieNode>(children_); }
                      - -
                      // 创造新的根节点,并且为非const类型
                      std::shared_ptr<TrieNode> root = std::shared_ptr<TrieNode>(root_->Clone());
                      // 使用t指针来遍历trie树
                      std::shared_ptr<TrieNode> t = root;
                      +

                      原理

                      执行原理

                        +
                      • 执行原理:
                          +
                        1. 当服务器接受到客户端浏览器的请求后,会解析请求URL路径,获取访问的Servlet的资源路径
                        2. +
                        3. 查找web.xml文件,是否有对应的标签体内容。
                        4. +
                        5. 如果有,则在找到对应的全类名【注意:在下面,url-pattern都使用注解配置方法了,所以这两步应该是不用了,应该会变成这样:① 逐个遍历注册的servlet实现类,查看其注解属性是否为对应的url-pattern。② 如果有,则找到类名,步骤继续】
                        6. +
                        7. tomcat会将字节码文件加载进内存,并且创建其对象
                        8. +
                        9. 调用其方法
                        10. +
                        +
                      • +
                      +

                      生命周期

                      image-20221227161516964

                      +

                      并发安全

                      Servlet的init方法只执行一次,一种Servlet在内存中只存在一个对象,Servlet是单例的。因而,当多线程同时访问同一个Servlet对象时,就会产生线程安全问题。所以有需要的话,就要采取手段保障Servlet类的线程安全性。

                      +

                      体系结构

                      为了简化开发,我们可以用提供的servlet的实现类。

                      +

                      image-20221227163713317

                      +

                      GenericServlet

                      除了service方法之外的方法,差不多都只做了空实现。所以只需写service方法即可。

                      +
                      @WebServlet("/demo2")
                      public class Servletdemo2 extends GenericServlet {
                      public void service(ServletRequest servletRequest, ServletResponse servletResponse){
                      }
                      }
                      -

                      再然后,每次迭代的时候在遍历路径上创造新的结点,结点类型非const;再利用shared_ptr的共享复制( t = tmp;),就能使得当前的t指针一直保持非const状态。

                      -
                      for (uint64_t i = 0; i < key.length(); i++) {
                      auto it = t->children_.find(key.at(i));
                      if (it == t->children_.end()) {
                      if (i != key.length() - 1) {
                      std::shared_ptr<TrieNode> tmp(new TrieNode());
                      // ...
                      t = tmp;
                      } else {
                      std::shared_ptr<TrieNodeWithValue<T>> tmp(new TrieNodeWithValue(std::make_shared<T>(std::move(value))));
                      // ...
                      t = tmp;
                      break;
                      }
                      } else {
                      if (i == key.length() - 1) {
                      std::shared_ptr<TrieNodeWithValue<T>> node =
                      std::make_shared<TrieNodeWithValue<T>>(it->second->children_, std::make_shared<T>(std::move(value)));
                      // ...
                      t = node;
                      break;
                      }
                      std::shared_ptr<TrieNode> node = std::shared_ptr<TrieNode>(it->second->Clone());
                      // ...
                      t = node;
                      }
                      }
                      +

                      HttpServlet

                      使用

                      比如httpservlet,就只用重写里面的doGet和doPost两个方法就行。

                      +
                      @WebServlet("/demo2")
                      public class Servletdemo2 extends HttpServlet {
                      @Override
                      protected void doGet(HttpServletRequest req, HttpServletResponse resp){
                      System.out.println("get!!!");
                      }

                      @Override
                      protected void doPost(HttpServletRequest req, HttpServletResponse resp){
                      System.out.println("post!!!");
                      }
                      }

                      -
                      -

                      注:我本来的写法是这样的:

                      -
                      for (uint64_t i = 0; i < key.length(); i++) {
                      auto it = t->children_.find(key.at(i));
                      if (it == t->children_.end()) {
                      if (i != key.length() - 1) {
                      std::shared_ptr<TrieNode> tmp(new TrieNode());
                      // ...
                      } else {
                      std::shared_ptr<TrieNodeWithValue<T>> tmp(new TrieNodeWithValue(std::make_shared<T>(std::move(value))));
                      // ...
                      break;
                      }
                      } else {
                      if (i == key.length() - 1) {
                      std::shared_ptr<TrieNodeWithValue<T>> node =
                      std::make_shared<TrieNodeWithValue<T>>(it->second->children_, std::make_shared<T>(std::move(value)));
                      // ...
                      break;
                      }
                      std::shared_ptr<TrieNode> node = std::shared_ptr<TrieNode>(it->second->Clone());
                      // ...
                      }
                      it = t->children_.find(key.at(i));
                      t = it->second;
                      }
                      +

                      这两个方法的区别就是,当使用get方式提交表单,就会执行第一个方法;使用post则会执行第二个方法。

                      +

                      比方说post时:

                      +

                      网页代码如下(放在webapp目录下)

                      +
                      <!DOCTYPE html>
                      <html lang="en">
                      <head>
                      <meta charset="UTF-8">
                      <title>Title</title>
                      </head>
                      <body>
                      Hello,World!
                      <!-- action内写Servlet的资源路径 -->
                      <form action="/webdemo4_war/demo2" method="post">
                      name: <input type="text" name="username" id="username" placeholder="请输入用户名">
                      <input type="submit" value="submit">
                      </form>
                      </body>
                      </html>
                      -

                      也就是为了省事,将t指针的转移集中放在了循环体最后进行。但这样是不行的。

                      -

                      cpp中,可以将非const对象自然转移为const对象,比如代码中就将非const的新结点放进了children_中;但是不允许将const对象自然转移为非const对象,比如代码中的t = it->second;。因而,我们对t指针的转移不能在新结点放入其children_之后。

                      -
                      +

                      servlet代码同上。

                      +

                      最终在网页中点击提交

                      +

                      image-20221230172048250

                      +

                      会跳转到\demo页面【也即servlet的访问路径】,并且在console打印“post!!!”

                      -

                      注2:在这里,我本来还多用了一个prev指针,因为在coding的时候用的是上面的本来的写法,误以为t指针只能是const,所以还得有父节点才能再把t指针复制一遍。但其实并非如此,而且就算如此prev指针也还是跟t指针一样的const的。。。不过还好编译前发现了上面那点改过来了,要不然就得面对编译大报错2333

                      +

                      为啥会这样呢?

                      +

                      之前在讲表单的时候说过,form的action属性代表着提交时这个表单会提交给谁,值为一个URL。所以,这里action的值设置为Servlet的路径,意思就是把表单数据发送给了Servelet,由于使用的是post方式,因此触发了Servlet的doPost方法。Servlet对得到的数据进行各种处理,并且通过req和resp进行交互。

                      -
                      make_shared

                      make_shared作用也类似于new,会在堆上开辟空间用以存放共享指针的base对象。这也让我想起来我在做上面那个实验时一个地方改成make_shared就对了,估计是犯了用栈中对象创建共享指针的错误。

                      -

                      官方鼓励用make_shared函数来创建对象,而不要手动去new。这一是因为,new出来的类型是原始指针,make_shared可以防止我们去使用原始指针创建多个引用计数体系;二是因为,make_shared可以防止内存碎片化。

                      +

                      为什么此处写的是“\demo”这样的路径?

                      +

                      事实上这是一个相对路径。

                      +

                      image-20221230215927404

                      +

                      部署的根路径可以在 run-edit configuration-tomcat-deployment中找到。

                      -
                      一个奇妙的报错

                      在写这样的shared_ptr的共享转移时:

                      -
                      std::shared_ptr<TrieNode> tmp = make_shared<TrieNode>();
                      // ...
                      t = tmp;
                      - -

                      会在t=tmp这里报错不能把int类型的tmp复制给t。我看了半天很奇怪哪来的int类型,查了半天怎么共享shared_ptr,最后才发现是因为这里:

                      -
                      std::make_shared<TrieNode>()
                      - -

                      漏了个std::呃呃。

                      -
                      显式保存is_value_node_

                      trie.cpp RemoveHelper()中:

                      -
                      if (i != key.length() - 1) {
                      // 注意此处需要保留原来的is_value_node_,之后再赋值回去!!!
                      bool tmp_val = node->second->is_value_node_;
                      std::shared_ptr<TrieNode> tmp = std::shared_ptr<TrieNode>(node->second->Clone());
                      tmp->is_value_node_ = tmp_val;

                      root->children_.erase(key.at(i));
                      root->children_.insert(std::make_pair(key.at(i), tmp));
                      flag = RemoveHelper(tmp, key, i + 1);
                      }
                      - -

                      否则会:

                      -

                      image-20230323114435061

                      -

                      查看trie_test.cpp的代码:

                      -
                      TEST(TrieTest, BasicRemoveTest2) {
                      auto trie = Trie();
                      // Put something
                      trie = trie.Put<uint32_t>("test", 2333);
                      ASSERT_EQ(*trie.Get<uint32_t>("test"), 2333);
                      trie = trie.Put<uint32_t>("te", 23);
                      ASSERT_EQ(*trie.Get<uint32_t>("te"), 23);
                      trie = trie.Put<uint32_t>("tes", 233);
                      ASSERT_EQ(*trie.Get<uint32_t>("tes"), 233);

                      // Delete something
                      trie = trie.Remove("te");
                      trie = trie.Remove("tes");
                      trie = trie.Remove("test");

                      ASSERT_EQ(trie.Get<uint32_t>("te"), nullptr);
                      ASSERT_EQ(trie.Get<uint32_t>("tes"), nullptr);
                      ASSERT_EQ(trie.Get<uint32_t>("test"), nullptr);
                      }
                      +
                      深层一些的问题
                      分成get和post

                      之所以这两种方法需要分别处理,是因为在Servlet的service方法中,其实是要对req对象进行参数分解,这两种方法分解方式不一样。

                      +

                      按照以往,我们需要这样写

                      +
                       public void service(ServletRequest servletRequest, ServletResponse servletResponse) {
                      String method = ((HttpServletRequest)servletRequest).getMethod();
                      if("GET".equals(method)){
                      //执行get的逻辑
                      }
                      else if ("POST".equals(method)){
                      //执行post的逻辑
                      }
                      }
                      -

                      它是在ASSERT_EQ(trie.Get<uint32_t>("te"), nullptr);这句报错的。这确实很奇怪,因为“te”已经被remove了。这是为什么呢?

                      -

                      经过gdb调试,trie的Remove和Put功能都确实很正常,但是我发现了一个诡异的现象。

                      -

                      在经过trie = trie.Remove("te");这句话后,trie的状态是t-e-(s)-(t)【括号表示为有值结点,类型为TireNodeWithValue】,符合预期。但是,经过紧随其后的trie = trie.Remove("tes");之后,trie的状态却变成了t-(e)-s-(t)。

                      -

                      image-20230323115902073

                      -

                      这实在是很诡异,为什么经过了一次Remove之后,trie = trie.Remove("te");这句话的效果就被重置了?

                      -

                      我想了挺久,最终认为这是构造方法的问题。

                      -

                      再次看一遍我们的Remove的代码:

                      -
                      if (i != key.length() - 1) {
                      std::shared_ptr<TrieNode> tmp = std::shared_ptr<TrieNode>(node->second->Clone());

                      root->children_.erase(key.at(i));
                      root->children_.insert(std::make_pair(key.at(i), tmp));
                      flag = RemoveHelper(tmp, key, i + 1);
                      } else {
                      if (node->second->is_value_node_) {
                      if (!node->second->children_.empty()) {
                      std::shared_ptr<TrieNode> tmp = std::shared_ptr<TrieNode>(node->second->Clone());
                      tmp->is_value_node_ = false;
                      root->children_.erase(key.at(i));
                      root->children_.insert(std::make_pair(key.at(i), tmp));
                      } else {
                      root->children_.erase(key.at(i));
                      }
                      return true;
                      }
                      return false;
                      }
                      +

                      就类似于可以这么写:

                      +
                      public void service(ServletRequest servletRequest, ServletResponse servletResponse){
                      String method = ((HttpServletRequest)servletRequest).getMethod();
                      if("GET".equals(method)){
                      doGet(servletRequest,servletResponse);
                      }
                      else if ("POST".equals(method)){
                      doPost(servletRequest,servletResponse);
                      }
                      }
                      -

                      以及TrieNodeWithValue::Clone()

                      -
                      auto Clone() const -> std::unique_ptr<TrieNode> override {
                      return std::make_unique<TrieNodeWithValue<T>>(children_, value_);
                      }
                      +

                      于是最后就融合入httpservlet了。

                      +

                      url-pattern配置

                        +
                      1. 一个Servlet可以定义多个访问路径 : @WebServlet({“/d4”,”/dd4”,”/ddd4”})

                        +
                      2. +
                      3. 路径定义规则:

                        +
                          +
                        1. /xxx:路径匹配如/demo、/*【第一个优先级大于第二个】
                        2. +
                        3. /xxx/xxx:多层路径,目录结构
                        4. +
                        5. *.do:扩展名匹配不能在前面加’/‘。也即:
                          @WebServlet("*.do")
                          -

                          以及Clone()方法调用的TrieNodeWithValue的构造方法:

                          -
                          explicit TrieNodeWithValue(std::shared_ptr<T> value) : value_(std::move(value)) { this->is_value_node_ = true; }
                          - -

                          可以发现,在多态作用下,e结点始终是一个TrieNodeWithValue的结点。

                          -

                          在我们去除tes这个key时,会到这个分支:

                          -
                          if (i != key.length() - 1) {
                          std::shared_ptr<TrieNode> tmp = std::shared_ptr<TrieNode>(node->second->Clone());
                          - -

                          Clone()中会调用node->second,也即e结点的构造方法,然后将e结点的is_value_node_设置为true,从而导致Get中无法通过这句代码返回nullptr。

                          -
                          if (!(t->is_value_node_)) {
                          return nullptr;
                          }
                          - -

                          因而,为了解决这个问题,我们就需要暂存is_value_node_,并在之后恢复它。

                          -

                          Task2 concurrency

                          -

                          In this task, you will need to modify trie_store.h and trie_store.cpp.需要实现并发安全版本。

                          -

                          For the original Trie class, everytime we modify the trie, we need to get the new root to access the new content. But for the concurrent key-value store, the put and delete methods do not have a return value. This requires you to use concurrency primitives to synchronize reads and writes so that no data is lost through the process.在并发安全版本中,PutGet不会返回trie,而是应该修改包装类的base trie。

                          -

                          Your concurrent key-value store should concurrently serve multiple readers and a single writer. That is to say, when someone is modifying the trie, reads can still be performed on the old root. When someone is reading, writes can still be performed without waiting for reads.同一时刻可以有一个writer和多个reader。

                          -

                          Also, if we get a reference to a value from the trie, we should be able to access it no matter how we modify the trie. The Get function from Trie only returns a pointer. If the trie node storing this value has been removed, the pointer will be dangling. Therefore, in TrieStore, we return a ValueGuard which stores both a reference to the value and the TrieNode corresponding to the root of the trie structure, so that the value can be accessed as we store the ValueGuard.为我们提供了 ValueGuard用以确保return值长时间有效。

                          -

                          To achieve this, we have provided you with the pseudo code for TrieStore::Get in trie_store.cpp. Please read it carefully and think of how to implement TrieStore::Put and TrieStore::Remove.我们在Get方法中给出了详细的步骤引导。你需要依据它来对PutGet进行修改。

                          -
                          -

                          感想

                          task2的内容是实现cow-trie并发安全版本的包装类TrieStore

                          -

                          相比于fall2022的并发内容,由于加上了cow的特性,本次实验更加复杂。我写了三版都没写对,看到别人的才豁然开朗(很遗憾没有自己再多想会儿……)接下来就从我的错误版本开始,逐步过渡到正确版本吧。

                          -

                          错误集锦

                          版本1

                          Get的实现很简单,按他说的一步步做就行,在这边不做赘述。PutRemove思路差不多,在此只放Put的代码。

                          -

                          image-20230321185244067

                          -

                          这样看起来很合理:同一时刻似乎确实只有一个writer对root_进行修改,也似乎确实同时可以有别的线程获取root_lock_对其进行读取。但其实,前者是错误的。

                          -

                          假如说进程A和进程B都在Put逻辑中。进程A执行到了root_ = new_trie这句话,然后进程B进入到root_.Put中。

                          -

                          root_ = new_trie使用了运算符=的默认实现,进行浅拷贝,故而会修改root_->root_root_.Put中会对root_->root_进行移动。

                          -

                          进程B在Put中执行std::move(root_)之后,进程A又让root_->root_变成了别的值(trie浅拷贝),导致原来的root_的引用计数变为0,自动释放(因为是智能指针shared_ptr),进程B在Put中再次访问就会寄。

                          -
                          -

                          注,此处是因为智能指针引用计数为零才释放的,cpp没有垃圾回收机制。

                          -
                          -
                          版本2
                          void TrieStore::Put(std::string_view key, T value) {
                          // You will need to ensure there is only one writer at a time. Think of how you can achieve this.
                          // The logic should be somehow similar to `TrieStore::Get`.
                          root_lock_.lock();
                          Trie tmp = root_;
                          root_lock_.unlock();

                          write_lock_.lock();
                          Trie new_trie = tmp.Put(key, std::move(value));
                          write_lock_.unlock();

                          root_lock_.lock();
                          root_ = new_trie;
                          root_lock_.unlock();
                          }
                          - -

                          版本1错误后,我发现我并没有按它强调的“somehow similar to Get”那样,模仿Get中的写法来做。于是我就修改了下,版本2诞生了。

                          -

                          但是这样的话,依然不能解决版本1中的问题。所以我又搞了个版本3.

                          -
                          版本3
                          void TrieStore::Put(std::string_view key, T value) {
                          write_lock_.lock();
                          Trie tmp = root_;
                          Trie new_trie = tmp.Put(key, std::move(value));
                          root_ = new_trie;
                          write_lock_.unlock();
                          }
                          - -

                          这样就能通过所有测试了。

                          -

                          但这样做虽然能解决多个writer的争夺问题,但不能解决一个writer和一个reader的争夺问题:因为两者都争夺同一个root_变量,但只有reader争夺root_lock_,这显然很不安全。因而,终极版本应该是这样:

                          -
                          正确版本
                          template <class T>
                          void TrieStore::Put(std::string_view key, T value) {
                          write_lock_.lock();
                          root_lock_.lock();
                          Trie tmp = root_;
                          root_lock_.unlock();

                          Trie new_trie = tmp.Put(key, std::move(value));

                          root_lock_.lock();
                          root_ = new_trie;
                          root_lock_.unlock();
                          write_lock_.unlock();
                          }
                          - - - -

                          可以看到整个思维过程是线性的,逐步改进下来,正确答案其实很容易想到。只可惜我太浮躁了,没有静下心来好好想,在版本3之后就去看了眼别人怎么写的(罪过)没有独立思考,算是一个小遗憾。

                          -

                          Task3 debugging

                          感想

                          一个考查我们debug入门技巧的小任务,简单,但我觉得形势很新颖。

                          -

                          debug过程

                          随便贴点debug过程的截图。

                          -

                          image-20230322221526353

                          -

                          image-20230322221634506

                          -

                          image-20230322221716462

                          -

                          小问题

                          gdb:Attempt to take address of value not located in memory.

                          任务中,需要获取root_的孙子。所以我就这么写了个gdb指令:p root_->children_.find('9')->second,然后就爆出了标题这个错误。

                          -

                          百度了下看到了这个:

                          -
                          -

                          gdb调试时好用的命令

                          -

                          image-20230323142137068

                          -
                          -

                          也许是因为我们通过.访问了children_的成员find吧(

                          -

                          总之,我最后是在trie_debug_test添加了这几行代码解决的:

                          -
                          // Put a breakpoint here.

                          // (1) How many children nodes are there on the root?
                          // Replace `CASE_1_YOUR_ANSWER` in `trie_answer.h` with the correct answer.
                          if (CASE_1_YOUR_ANSWER != Case1CorrectAnswer()) {
                          ASSERT_TRUE(false);
                          }
                          auto it = trie.root_->children_.find('9');
                          // (2) How many children nodes are there on the node of prefix `9`?
                          // Replace `CASE_2_YOUR_ANSWER` in `trie_answer.h` with the correct answer.
                          if (CASE_2_YOUR_ANSWER != Case2CorrectAnswer()) {
                          ASSERT_TRUE(false);
                          }
                          auto val = trie.Get<uint32_t>("93");
                          std::cout << val << it->first << std::endl;
                          // (3) What's the value for `93`?
                          // Replace `CASE_3_YOUR_ANSWER` in `trie_answer.h` with the correct answer.
                          if (CASE_3_YOUR_ANSWER != Case3CorrectAnswer()) {
                          ASSERT_TRUE(false);
                          }
                          - -

                          也即添加了it和val,以及防止unused报错的cout语句。gdb调试时打印it和val就行。

                          -
                          答案对但是过不了评测
                          -

                          来自CMU 15445 Project 0 (Spring 2023) 学习记录

                          -

                          在我本地的环境上,调试三问的答案分别是8 1 42,但该答案无法通过 Grade 平台的评测。发现在 Discord 上有人提出了同样的问题,助教 Alex Chi 给出了解答:

                          -
                          -

                          Alex Chi — 2023/02/15 23:29
                          It is possible that your environment produces different random numbers than the grading environment. In case your environment is producing different set of random numbers than our grader, replace your TrieDebugger test with:

                          -
                          -
                          auto trie = Trie();
                          trie = trie.Put<uint32_t>("65", 25);
                          trie = trie.Put<uint32_t>("61", 65);
                          trie = trie.Put<uint32_t>("82", 84);
                          trie = trie.Put<uint32_t>("2", 42);
                          trie = trie.Put<uint32_t>("16", 67);
                          trie = trie.Put<uint32_t>("94", 53);
                          trie = trie.Put<uint32_t>("20", 35);
                          trie = trie.Put<uint32_t>("3", 57);
                          trie = trie.Put<uint32_t>("93", 30);
                          trie = trie.Put<uint32_t>("75", 29);
                          -
                          -

                          难绷,我反复确认了好几遍(。主要还是太相信cmu的权威了,觉得这实验都发布了好几个月了应该不会有错,就没想到是这个问题。我觉得最好还是把这个问题反应在指导书上吧。

                          -

                          Task4 SQL String Functions

                          -

                          Now it is time to dive into BusTub itself!

                          -

                          You will need to implement upper and lower SQL functions.

                          -

                          This can be done in 2 steps:

                          -
                            -
                          1. implement the function logic in string_expression.h.
                          2. -
                          3. register the function in BusTub, so that the SQL framework can call your function when the user executes a SQL, in plan_func_call.cpp.
                          4. +url访问填写http://localhost/webdemo4_war/*.do
                          -

                          To test your implementation, you can use bustub-shell:

                          -
                          cd build
                          make -j`nproc` shell
                          ./bin/bustub-shell
                          bustub> select upper('AbCd'), lower('AbCd');
                          ABCD abcd
                          -
                          -

                          感想

                          说实话乍一看我还没看懂(。它放在这个位置,我还以为跟上面实现的cow-trie有什么关系,并且误以为这个upper和lower是什么上层接口底层接口的意思,跟它大眼瞪小眼了半天。直到看到了下面的案例,才发现跟trie似乎没有任何关系23333

                          -

                          本次实验内容其实就是实现sql的转换大小写的函数。知道了要做什么之后,任务就很简单了,按着它提示一步步做就行。

                          -

                          不过此task重点其实也是在稍微了解下我们接下来要打交道的sql框架的代码。比如说,此次我们的实现涉及到的,居然是一个差不多是工厂模式(其实更像策略模式?)的一部分:

                          -

                          外界传入想调用的函数名,通过GetFuncCallFromFactory获取对应的处理对象

                          -

                          image-20230322154915206

                          -

                          得到处理对象后调用其Compute方法就行

                          -

                          image-20230322154849449

                          -

                          第一次如此鲜明地看到一个设计模式在cpp的应用,真是让我非常震撼。

                          -

                          代码规范

                          依旧是这三件套:

                          -
                          make format
                          make check-lint
                          make check-clang-tidy-p0
                          - -

                          错误集锦

                          关于sanitizer

                          执行了该命令:cmake -DCMAKE_BUILD_TYPE=Debug -DBUSTUB_SANITIZER= ..之后,执行make报错missing argument to '-fsanitize='

                          -

                          发生这个的原因是cmake的命令中将BUSTUB_SANITIZER设置成了空。解决方法就是将其设置为别的值就好了,具体想设置成什么值可以参见:关于GCC/LLVM编译器中的sanitize选项用处用法详解 我这里姑且随便设置了个leak

                          -]]> - - - Project1 Buffer Pool - /2023/03/13/cmu15445$lab1/ - Project1 Buffer Pool

                          先放个通关记录~

                          -

                          image-20230330235300907

                          -
                          -

                          特别鸣谢:

                          -

                          某不愿透露姓名的友人hhj

                          -

                          大佬的实验过程

                          -

                          大佬的性能优化

                          -
                          -
                          -

                          During the semester, you will build a disk-oriented storage manager for the BusTub DBMS.

                          -

                          注:DBMS(Database Management System),比如说Oracle数据库

                          -

                          The first programming project is to implement a buffer pool in your storage manager.The buffer pool is responsible for moving physical pages back and forth from main memory to disk.也就是负责物理页从磁盘的读写

                          -

                          It allows a DBMS to support databases that are larger than the amount of memory available to the system. 是的,这其实就跟内存换入换出差不多。我们现在是不是要在用户态实现这个功能?我记得xv6似乎是没有这个机制的。有点小期待呀。不过这部分感觉说不定和xv6的磁盘管理(使用双向链表管理buffer cache),及其的改进版本(lab:locking 使用哈希+双向链表)比较类似。

                          -
                          -

                          注:xv6确实没有内存换入换出机制,其是固定大小的内存空间。但xv6的文件系统有采用LRU算法的buffer cache(怪不得有什么数据库型的文件系统,这两个确实有点像)。

                          -
                          -

                          The buffer pool’s operations are transparent to other parts in the system. For example, the system asks the buffer pool for a page using its unique identifier (page_id_t) and it does not know whether that page is already in memory or whether the system has to retrieve it from disk.

                          -

                          Your implementation will need to be thread-safe.

                          -
                          -

                          总结

                          由于这几天时间比较零散+事情比较多,因此完成的总时间数不一定值得参考:26h(乐)

                          -

                          本次实验要说简单也还算简单。大概就是实现一个database与磁盘交换页的buffer pool,机制类似于内存换入换出。而实现这个buffer pool,首先得实现换入换出算法,也即task1的LRU-K算法。再然后就是在我们的LRU-K算法的基础上,实现真正的buffer pool(真正指:真正地存储以及读写磁盘、向外暴露接口),也即BufferPoolManager。最后,我们会实现类似于lock_guard这样结构的PageGuard,用于自动释放页引用和读写锁。最后的最后,我们会对实现的buffer pool进行性能优化,优化方向包括细粒度化锁以实现并行IO、针对特定应用场景调整LRU-K策略等。

                          -

                          这四者都是相互联系相互递进的,我认为每一个task都设计得非常不错,写完了之后对它所涉及的知识点都有了更深刻的理解。我认为其中最优美的一点就是LRU-K算法与buffer pool的解耦,这个设计让我十分地赞叹。

                          -

                          最后,再对我的完成情况进行一个评价。本次实验确实内容不是很难【除了性能调优部分,这个我是真不懂QAQ】,毕竟它指导书以及代码注释都给了详细的步骤参考,我之所以做了那么久一是因为我有不好的习惯,就是没认真看指导书和提示就开始按着自己的理解写,然后写完就直接开始debug开始交了;二是因为这几天学业的破事太多、竞赛也逐步开始了,因而战线拉得太长,总耗时就太多了。

                          -

                          因而,吸取经验,我之后coding完了之后,再照着指导书仔仔细细地过一遍自己的代码。同时,15445这个实验我也决定先暂时搁置,毕竟接下来这两个月应该会在竞赛和学业两头转,实在不能抽出很大段时间继续写了。

                          -

                          就酱。

                          -

                          Task1 LRU-K

                          -

                          This component is responsible for tracking page usage in the buffer pool.

                          -

                          The LRU-K algorithm evicts a frame whose backward k-distance is maximum of all frames in the replacer. LRU-K 算法驱逐一个帧,其backward k-distance是替换器中所有帧的最大值。

                          -

                          Backward k-distance is computed as the difference in time between current timestamp and the timestamp of kth previous access. backward k-distance=现在的时间戳 - 之前第k次访问时的时间戳

                          -

                          A frame with fewer than k historical accesses is given +inf as its backward k-distance. 不足k次访问的帧的backward k-distance应该设置为inf(对应上图左边那个访问记录队列吧)

                          -

                          **When multiple frames have +inf backward k-distance, the replacer evicts the frame with the earliest overall timestamp (i.e., the frame whose least-recent recorded access is the overall least recent access, overall, out of all frames).**如果有多个inf的结点,按照LRU规则淘汰(也即上图左边那个历史记录队列采取LRU规则)

                          -

                          The maximum size for the LRUKReplacer is the same as the size of the buffer pool since it contains placeholders for all of the frames in the BufferPoolManager. However, at any given moment, not all the frames in the replacer are considered to be evictable. The size of LRUKReplacer is represented by the number of evictable frames. The LRUKReplacer is initialized to have no frames in it. Then, only when a frame is marked as evictable, replacer’s size will increase. size为可驱逐的frame数而非所有frame数。

                          +
                        6. +
                        +

                        service参数

                        image-20230102005833327

                        +

                        http协议

                        概述

                        概念:Hyper Text Transfer Protocol 超文本传输协议

                        +
                          +
                        • 传输协议:定义了客户端和服务器端通信时发送数据的格式

                          +
                        • +
                        • 特点:

                          +
                            +
                          1. 基于TCP/IP的高级协议需要先经历三次握手,可靠传输
                          2. +
                          3. 默认端口号:80
                            +

                            如果说域名是ip地址的简化表示,ip地址又表示着一台主机,那么使用http协议访问一个网址,相当于访问一台主机,并且端口号为80.

                            -

                            正确思路

                            本次实验要我们实现的是一个LRU-K算法的页面置换器。

                            -

                            LRU-K算法是对LRU算法和LFU算法的折中优化,平衡了LFU和LRU的性能和开销的同时,也解决了缓存污染问题,其核心思想是将“最近使用过1次”的判断标准扩展为“最近使用过K次”。具体来说,它维护了一个backward k-distance,其计算方法:

                            +
                          4. +
                          5. 基于请求/响应模型的:一次请求对应一次响应
                          6. +
                          7. 无状态的:每次请求之间相互独立,不能交互数据
                          8. +
                          +

                          历史版本:

                          +
                            +
                          • 1.0:每一次请求响应都会建立新的连接
                          • +
                          • 1.1:复用连接
                          • +
                          +
                        • +
                        +
                        报文格式
                        请求

                        客户端发送给服务器端的消息

                        +

                        数据格式:

                          -
                        1. 如果已经被访问过k次: backward k-distance = current_timestamp_ - 倒数第k次访问的时间戳
                        2. -
                        3. 如果还没被访问过k次: backward k-distance = +inf
                        4. +
                        5. 请求行
                          请求方式 请求url 请求协议/版本
                          GET /login.html HTTP/1.1

                          +
                            +
                          • 请求方式:
                              +
                            • HTTP协议有7中请求方式,常用的有2种
                                +
                              • GET:
                                  +
                                1. 请求参数在请求行中【在url后】
                                2. +
                                3. 请求的url长度有限制的
                                4. +
                                5. 不太安全
                                -

                                页面驱逐规则:

                                +
                              • +
                              • POST:
                                  +
                                1. 请求参数在请求体中
                                2. +
                                3. 请求的url长度没有限制的
                                4. +
                                5. 相对安全
                                6. +
                                +
                              • +
                              +
                            • +
                            +
                          • +
                          +
                        6. +
                        7. 请求头:客户端浏览器告诉服务器一些信息
                          请求头名称: 请求头值

                          +
                            +
                          • 常见的请求头:

                              -
                            1. 驱逐 backward k-distance 最大的页。

                              -

                              也即情况2总是优先会比情况1被驱逐;每次优先驱逐previous k次访问最早的页面。

                              +
                            2. User-Agent:浏览器告诉服务器,我访问你使用的浏览器版本信息 * 可以在服务器端获取该头的信息,解决浏览器的兼容性问题

                            3. -
                            4. 当有多个页值为+inf,则采取FIFO规则进行驱逐。

                              +
                            5. Accept:可以支持的响应格式

                              +
                            6. +
                            7. Accept-language:可以支持的语言环境

                              +
                            8. +
                            9. Referer:http://localhost/login.html * 告诉服务器,我(当前请求)从哪里来?

                              +
                                +
                              • 作用:
                              • +
                              +
                                +
                              1. 防盗链:image-20230101235437317如果ref头非合法就不播放
                              2. +
                              3. 统计工作:看从哪个网站来的人数多
                              4. +
                              +
                            10. +
                            11. Connection:连接是否活着

                            -

                            故而,在具体实现中,为了便于管理,我将此拆分为两个队列:

                            -
                            -

                            思路来自:LRU . LFU 和 LRU-K 的解释与区别

                            -

                            image-20230323205851168

                            +
                          • +
                          +
                        8. +
                        9. 请求空行
                          空行,就是用于分割POST请求的请求头,和请求体的。

                          +
                        10. +
                        11. 请求体(正文):

                          +
                            +
                          • 封装POST请求消息的请求参数的
                          • +
                          +
                        12. +
                        +

                        字符串格式:

                        +
                        //请求行
                        POST /login.html HTTP/1.1
                        //请求头
                        Host: localhost
                        User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:60.0) Gecko/20100101 Firefox/60.0
                        Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
                        Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
                        Accept-Encoding: gzip, deflate
                        Referer: http://localhost/login.html
                        Connection: keep-alive
                        Upgrade-Insecure-Requests: 1
                        //请求空行

                        //请求体
                        username=zhangsan
                        + + + +
                        响应

                        响应消息:服务器端发送给客户端的数据

                        +

                        数据格式:

                          -
                        1. 数据第一次被访问,加入到访问历史列表;

                          +
                        2. 响应行

                          +
                            +
                          1. 组成:协议/版本 响应状态码 状态码描述 HTTP/1.1 200 OK

                          2. -
                          3. 如果数据在访问历史列表里后没有达到K次访问,则按照一定规则(FIFO,LRU)淘汰;

                            +
                          4. 响应状态码:服务器告诉客户端浏览器本次请求和响应的一个状态, 状态码都是3位数字. 分类:

                            +
                              +
                            1. 1xx:服务器接收客户端消息,但没有接收完成,等待一段时间后,发送1xx多状态码,询问是否还要继续发

                            2. -
                            3. 当访问历史队列中的数据访问次数达到K次后,将数据索引从历史队列删除,将数据移到缓存队列中,并缓存此数据,缓存队列重新按照时间排序;

                              +
                            4. 2xx:成功。代表:200

                            5. -
                            6. 缓存数据队列中被再次访问后,重新排序;

                              +
                            7. 3xx:重定向。代表:302(重定向),304(访问缓存)

                              +

                              image-20230103151112791

                              +

                              需要自动重定向到另一个C去

                              +

                              image-20230103151237984

                              +

                              发现资源未变化且本地有缓存

                            8. -
                            9. 需要淘汰数据时,淘汰缓存队列中排在末尾的数据,即:淘汰“倒数第K次访问离现在最久”的数据。

                              +
                            10. 4xx:由客户端造成的错误

                              +

                              代表:

                              +
                                +
                              1. 404(请求路径没有对应的资源,可能路径输错了)

                              2. +
                              3. 405:请求方式没有对应的doXxx方法

                                +

                                当我们在Servlet中未重写doXXX方法,就默认不能用此方法进行访问。因为doXXX方法的默认实现为:

                                +
                                String protocol = req.getProtocol();
                                String msg = lStrings.getString("http.method_get_not_supported");
                                if (protocol.endsWith("1.1")) {
                                resp.sendError(405, msg);
                                } else {
                                resp.sendError(400, msg);
                                }
                              -
                      -

                      每个页面结构持有一个时间戳队列即可:

                      -

                      image-20231128152139485

                      -

                      感想

                      刚coding完

                      task1的内容就是实现对一堆frame_id的LRU-K算法管理,挺简单的(也可能是测试用例少我错误没排查出来2333)

                      -

                      我并没有用默认给的模板的unorder_map,也没有用默认给的模板思路(但原理以及最终效果是差不多的,就是没用它的方法),而是选择类似像上面这张图一样,分成两个队列实现,一个队列visit_record_存储那些访问次数<k的数据,另一个队列cache_data_存储那些访问次数>=k的顺序,每次优先淘汰visit_record_中的数据,两个队列都采用LRU的方式管理。与此同时,我觉得LRU管理时间戳只用记录最新访问的就行,所以将历史访问时间戳队列改成了只有一个变量。

                      -

                      终于通过online-test

                      -

                      参考:

                      -

                      FIFO和LRU这里面的实例非常直观地说明了两种算法的差异,可以跟着手推感受一下

                      -

                      pro1这个用的是我上面的那个想法,是错的。但是评论很值得参考:

                      -

                      image-20230329230045194

                      -

                      pro1这个评论的“偷测试用例”xswl,虽然这次没用,但以后说不定能用上:

                      -

                      image-20230329230139300

                      -
                      -
                      正确思路

                      ……简单个屁!!

                      -

                      算法上,上面错误的算法确实很简单;而正确的算法也确实很简单。那么难的是什么呢?我觉得难的还是搞清楚它要我们实现的究竟是上面东西。

                      -

                      结合指导书这段话:

                      -
                      -

                      The LRU-K algorithm evicts a frame whose backward k-distance is maximum of all frames in the replacer. 每次驱逐 backward k-distance最大的

                      -

                      那么 backward k-distance是什么?

                      -

                      Backward k-distance is computed as the difference in time between current timestamp and the timestamp of kth previous access. backward k-distance = current_timestamp_ - 倒数第k次访问的时间戳

                      -

                      A frame with fewer than k historical accesses is given +inf as its backward k-distance. 没有达到k次访问的, backward k-distance为+inf。也就是说,每次优先从历史访问队列清除元素。

                      -

                      【**When multiple frames have +inf backward k-distance, the replacer evicts the frame with the earliest overall timestamp (i.e., the frame whose least-recent recorded access is the overall least recent access, overall, out of all frames).**】当历史访问队列有多个元素,就驱逐英文描述那样的frame。

                      -
                      -

                      我们可以发现,它这个对于两个队列的LRU,并非我们原来算法那样,对于每个frame,针对其最新的访问时间戳,也即history_.back(),进行LRU淘汰;而是,针对其倒数第k新的访问记录,也即history_.front() && history_.size()<=k,进行LRU淘汰。

                      -

                      其中,由于历史访问队列的记录少于k个,因而其事实上从k-distance算法退化为了FIFO算法。【感受一下这一点的优美:FIFO实际上是k-distance的特例】

                      -

                      我们上面的算法比较的是history_.back(),所以可以省略时间戳队列为一个变量,然后将两个队列使用FILO的形式组织起来。正确算法就不能这么简单了,要按front排序的话,实现开销可能更大,所以下面就采用了map形式来实现logn的查找。

                      -
                      关于LRU的翻译

                      这里一个点我其实还是很疑惑的,完全想不通。

                      -

                      就是,对缓存队列实现k-distance算法没毛病,这段话已经写得很清楚了。

                      -
                      -

                      Backward k-distance is computed as the difference in time between current timestamp and the timestamp of kth previous access. backward k-distance = current_timestamp_ - 倒数第k次访问的时间戳

                      -
                      -

                      但是,为什么历史访问队列要用FIFO呢?是我英语不好吗,这段话不是实现纯正LRU的意思吗:

                      -
                      -

                      【**When multiple frames have +inf backward k-distance, the replacer evicts the frame with the earliest overall timestamp (i.e., the frame whose least-recent recorded access is the overall least recent access, overall, out of all frames).**】当历史访问队列有多个元素,就驱逐英文描述那样的frame。

                      -
                      -

                      我翻译一下:

                      -

                      当多个frame有+inf这个 backward k-distance的时候,replacer需要驱逐拥有全部(overall)frame中最早的timestamp的frame。(也就是说,frame,它的最近访问记录是所有frame里面最早的)

                      -

                      这样确实看起来就是要用LRU。

                      -

                      但其实,是我英语不好。咨询了场外热心人士hhj之后,我才修订出了如下版本:

                      -

                      当多个frame有+inf这个 backward k-distance的时候,replacer需要驱逐拥有全部(overall)frame中最早的timestamp的frame。(也就是说选择一个frame,这个frame的最不近的访问记录,是所有frame中最近最少访问的)【也即这个frame的history的front是所有frame中最早的,也即使用FIFO算法】

                      -

                      可见,正确解法确实是没问题的,就是理解上很困难。要是可以配个实例就好了QAQ

                      -

                      所以说,所谓LRU(Least Recently Used)的直译还是最不近使用,也即最近最少使用。里面这个least不是用来修饰recent表示recent程度深的,相反它表示的是recent的程度浅。英语不好的惨痛教训啊。

                      -

                      image-20230330160207757

                      -

                      最后一下子交了这么多次才过。绷不住了。

                      -

                      Task2 BufferPoolManager

                      -

                      The BufferPoolManager is responsible for fetching database pages from the DiskManager and storing them in memory.从DiskManager中取出页,然后存入内存。

                      -

                      也就是说,我们的Buffer Pool是磁盘到内存的映射,我们在Task1实现了内存部分的管理数据结构?

                      -

                      The BufferPoolManager can also write dirty pages out to disk when it is either explicitly instructed to do so or when it needs to evict a page to make space for a new page.也要负责dirty页的写回

                      -

                      You will also not need to implement the code that actually reads and writes data to disk (this is called the DiskManager in our implementation). We will provide that functionality. DiskManager已给出

                      -

                      All in-memory pages in the system are represented by Page objects. Each Page object contains a block of memory that the DiskManager will use as a location to copy the contents of a physical page that it reads from disk. Page 是可复用的内存页容器

                      -

                      The Page object’s identifer (page_id) keeps track of what physical page it contains; if a Page object does not contain a physical page, then its page_id must be set to INVALID_PAGE_ID.

                      -

                      也就是说,page_id表示的是实际的物理页号;frame_id表示的是你的Page容器的序号,同时也是LRU的对象。你需要一个类似<fid, pid>这样的map来记录这二者的映射。具体是通过:

                      -
                      /** Array of buffer pool pages. */
                      Page *pages_; // <fid, pid>
                      /** Page table for keeping track of buffer pool pages. */
                      std::unordered_map<page_id_t, frame_id_t> page_table_; // <pid, fid>
                      - -

                      Each Page object also maintains a counter for the number of threads that have “pinned” that page. Your BufferPoolManager is not allowed to free a Page that is pinned. 有引用计数机制

                      -

                      Each Page object also keeps track of whether it is dirty or not. It is your job to record whether a page was modified before it is unpinned. Your BufferPoolManager must write the contents of a dirty Page back to disk before that object can be reused.需要track dirty,并且这是你要干的;要写回,这也是你要干的

                      -

                      Your BufferPoolManager implementation will use the LRUKReplacer class that you created in the previous steps of this assignment. The LRUKReplacer will keep track of when Page objects are accessed so that it can decide which one to evict when it must free a frame to make room for copying a new physical page from disk. When mapping page_id to frame_id in the BufferPoolManager, again be warned that STL containers are not thread-safe.

                      -
                      -

                      感想

                      刚coding完

                      task2说得比较复杂,实现的函数较多,实际coding细节也比较繁琐,但debug倒是很轻松。

                      -

                      主要内容就是实现BufferPoolManager,在task1实现的LRU-K算法的基础上,写具体的内存换入换出的接口逻辑。

                      -

                      再次回顾我们整个project1的目的:实现一个从磁盘到内存的buffer。task1只是实现了一个内存页换入换出的LRU-K算法部分,task2则基于算法部分,实现了具体与上层交互的像样的逻辑。

                      -

                      我认为这其中一个亮点就是,它非常完美地将LRU-K算法和具体的上层逻辑进行了解耦。LRU-K只需关注如何将这一堆freme_id组织起来组织好,而无需关心具体内存页存放在哪,以及对应frame淘汰之后内存页又何去何从,因为这些逻辑都会由上层实现;而上层逻辑也无需关心具体的淘汰页算法【LRU-K/LRU/LFU,只需替换replacer_就可以替换换入换出策略】,而只需打好evictable标记,并且在调用evict方法之后做好后处理(如内存释放等等等)即可。

                      -

                      这其中有一个小细节也值得借鉴,即从page_id_frame_id_的转化。frame_id_有界,比较方便LRU-K算法实现,并且进行了LRU-K算法的容量控制,同时由于算法和上层逻辑的容量相同,故而也是pages_的索引号;而page_id_不能有界,因为实际上访问到的物理页不可能只共享pool_size_个序列号。故而在这样解耦实现的基础上,二者缺一不可。

                      -

                      还有frame_id_的复用,它是采用了类似我们日常生活中取号那样,要用号时从队列头取,不用号时塞回队列尾就行,这种方式我觉得还挺有意思。

                      -

                      其他部分虽然步骤繁杂,但理解难度不高,而且它提示得也很保姆了,所以不多bb。

                      -

                      通过online-test

                      确实算简单了,我主要倒在没有认真看它的需求,这应该是语文问题(绷

                      -

                      一个是FetchPage这里:

                      -

                      image-20230330162812680

                      -

                      如果所求物理页存在于buffer pool,直接返回+record access即可,不用再写回+读入。因为它的提示这边:

                      -

                      image-20230330162941774

                      -

                      这个是句号。也就是说后面那些写回啊read啊,是没找到时才做的,不是并列关系。

                      -

                      这也很合理,毕竟你找到所需页就说明不用从磁盘读入,也即找到所需页=直接返回即可。

                      -

                      另一个是UnpinPage这里:

                      -

                      image-20230330163031739

                      -

                      不应该写is_dirty_ = is_dirty,因为它的提示这边:

                      -

                      image-20230330163058921

                      -

                      可见参数is_dirty为true是需要设置为dirty,为false的话没有别的意义,保持原来值就行。

                      -

                      还有一个就是,在Page类中声明了friend:

                      -

                      image-20230330163337929

                      -

                      故而BufferPoolManager可以直接访问Page的私有成员变量,而无需手动为Page添加Getter/Setter方法。

                      -

                      Task3 Page Guard

                      这是要写我们在上面用的那个PageGuard?这让我想起了Lab0的ValueGuard

                      -
                      template <class T>
                      class ValueGuard {
                      public:
                      ValueGuard(Trie root, const T &value) : root_(std::move(root)), value_(value) {}
                      auto operator*() const -> const T & { return value_; }

                      private:
                      Trie root_;
                      const T &value_;
                      };
                      + +
                    9. 5xx:服务器端错误。

                      +

                      代表:500(服务器内部出现Exception)

                      +
                      int i = 3/0;
                    10. +
                    +
                  4. +
                  +
                7. +
                8. 响应头:

                  +
                    +
                  1. 格式: [头名称 : 值]

                    +
                  2. +
                  3. 常见的响应头:

                    +
                      +
                    1. Content-Type:服务器告诉客户端本次响应体 数据格式以及编码格式

                      +

                      浏览器依照编码格式来对该页面进行解码。

                      +
                    2. +
                    3. Content-disposition:服务器告诉客户端以什么格式打开响应体数据

                      +
                        +
                      • 值:
                          +
                        • in-line:默认值,在当前页面内打开
                        • +
                        • attachment;filename=xxx:以附件形式打开响应体。也即点击超链接后开始文件下载
                        • +
                        +
                      • +
                      +
                    4. +
                    +
                  4. +
                  +
                9. +
                10. 响应空行

                  +
                11. +
                12. 响应体:传输的数据

                  +
                13. +
                +

                字符串格式:

                +
                //响应行
                HTTP/1.1 200 OK
                //响应头
                Content-Type: text/html;charset=UTF-8
                Content-Length: 101
                Date: Wed, 06 Jun 2018 07:08:42 GMT
                //响应空行

                //响应体
                <html>
                <head>
                <title>$Title$</title>
                </head>
                <body>
                hello , response
                </body>
                </html>
                -

                不过其实这两个是不一样的。本次要实现的Page Guard的语义更类似lock_guard

                -
                -

                我们需要手动调用UnpinPage,但这中就跟new/delete、malloc/free一样都要靠人脑来记住,不大安全。

                -

                You will implement BasicPageGuard which store the pointers to BufferPoolManager and Page objects. A page guard ensures that UnpinPage is called on the corresponding Page object as soon as it goes out of scope. 【也许这需要在析构函数中实现?】Note that it should still expose a method for a programmer to manually unpin the page.仍然需要提供UnPin方法。

                -

                As BasicPageGuard hides the underlying Page pointer, it can also provide read-only/write data APIs that provide compile-time checks to ensure that the is_dirty flag is set correctly for each use case.这个思想很值得学习。

                -

                In the future projects, multiple threads will be reading and writing from the same pages, thus reader-writer latches are required to ensure the correctness of the data. Note that in the Page class, there are relevant latching methods for this purpose. Similar to unpinning of a page, a programmer can forget to unlatch a page after use. To mitigate the problem, you will implement ReadPageGuard and WritePageGuard which automatically unlatch the pages as soon as they go out of scope.

                -
                -

                感想

                怎么说,其实只用仔细看相关文档和它的要求就不难,但你懂的我的尿性就是不细看文档,所以这里我也用gdb调了蛮久才过的。正确思路没什么好说的,直接记录下我觉得比较有意义的错误吧。

                -

                错误集锦

                析构函数的调用

                image-20230330233252372

                -

                在这个用例中,退出“}”会调用两次析构函数。

                -
                奇怪的死锁
                debug过程

                我在coding的过程中,遇到了一个很神奇的死锁现象。

                -

                在这里page->WLatch();这句会死锁,而且还是在第一次调用FetchWritePage()时死锁的:

                -
                WritePageGuard(BufferPoolManager *bpm, Page *page) : guard_(bpm, page) {
                page->WLatch();
                }
                -

                但是添加了一句page->WUnlatch();

                -
                WritePageGuard(BufferPoolManager *bpm, Page *page) : guard_(bpm, page) {
                page->WUnlatch();
                page->WLatch();
                }
                -

                它就不会死锁了。

                -

                这很奇怪,到底是发生了什么?我用GDB调了半天,在RWLatch.WLock()处打了断点,也没发现在这之前有调用过lock()。于是我就去看了下std::shared_mutex的官方文档(当然,这中间想了很久也不知道怎么办):

                -

                image-20230331222601644

                -

                我就怀疑是不是我哪里写错了,所以就干了这种undefined的事,然后就导致死锁了。于是我写了个测试程序:

                -

                image-20230330195418370

                -

                发现,当在调用WLock(也即std::shared_mutex::lock())之前,如果多调了一次XUnlock(也即std::shared_mutex::unlock()或者std::shared_mutex::unlock_shared()),就会卡住。

                -

                这说明确实发生了不匹配问题。于是我就在Page中添加了两个成员变量用来记录上锁和解锁的次数,并且在gurad test中打印了出来,结果发现:

                -

                image-20230330233018049

                -

                确实发生了不匹配问题,是在这里:

                -

                image-20230330233252372

                -

                之后用gdb调下就发现错误了,不赘述了。

                -
                另外的想法

                在出现死锁问题时,我是想着,会不会是测试程序中,对同一页获取了一次ReadGuardPage对象之后,再对同一页获取Read/WriteGuardPage导致的呢?于是我就开始思考如何防范这个流程,最后写下了这样的代码:

                -
                auto BufferPoolManager::FetchPageRead(page_id_t page_id) -> ReadPageGuard {
                Page *page = FetchPage(page_id);
                bool should_release = true;
                if (!page->rwlatch_.try_lock_shared()) {
                // 说明此时已有read/write锁
                should_release = false;
                }
                return {this, pagei, should_release};
                }

                auto BufferPoolManager::FetchPageWrite(page_id_t page_id) -> WritePageGuard {
                Page *page = FetchPage(page_id);
                bool should_release = true;
                if (!page->rwlatch_.try_lock()) {
                // 获取write锁失败,可能原因:该进程持有write锁、别的进程有read锁、该进程持有read锁
                if (page->rwlatch_.try_lock_shared()) {
                // 成功read,说明是别的进程有read锁
                page->rwlatch_.unlock_shared();
                // 等待
                page->rwlatch_.lock();
                } else {
                // 说明当前进程有read/write锁
                should_release = false;
                }
                }
                return {this, pagei, should_release};
                }
                +

                Request

                继承体系结构

                ServletRequest(I) - HttpServletRequest(I) - RequestFacade(C)[tomcat创建]

                +
                功能
                获取请求行
                  +
                1. 获取请求方式 POST

                  +
                  String getMethod()
                2. +
                3. 获取虚拟目录 /webdemo

                  +
                  String getContextPath()
                4. +
                5. 获取Servlet路径 /demo1

                  +
                  String getServletPath()
                6. +
                7. 获取get方式请求参数 name=zhangsan

                  +

                  &分割每个键值对

                  +
                  String getQueryString()
                8. +
                9. 获取请求URI和URL

                  +
                  //  /webdemo/demo1
                  String getRequestURI();

                  // http://localhost/webdemo/demo1
                  StringBuffer getRequestURL();
                  -

                  但很遗憾的是,我发现是无法区分当前进程持有write还是read锁的。也许有别的办法但我没想起来。

                  -

                  总之,我认为这段代码还是很有参考价值的,姑且放着先。

                  -

                  Task4 性能调优

                  -

                  参考:

                  -

                  CMU 15-445 2023 P1 优化攻略 [rank#3] 写得非常细致,思路很清晰

                  -

                  CMU 15-445 Project 1 (Spring 2023) 优化记录

                  -
                  -

                  我的实现有一些并发小问题,详见lab2的并发部分~

                  +

                  URL:统一资源定位符 : http://localhost/day14/demo1 中华人民共和国
                  URI:统一资源标识符 : /day14/demo1 共和国

                  +

                  URI的代表范围更大

                  -

                  lru-k的算法优化是自己想的,并行IO的优化思路全部来自 CMU 15-445 Project 1 (Spring 2023) 优化记录,我只是把这位大佬的思路自己实现了一遍。感觉还是太菜了,面对这种实际场景毫无还手之力一点思路没有QAQ但正是如此,这个细粒度化锁的小task才值得学习。

                  -

                  放上优化前后性能对比:

                  -

                  image-20230331000020775

                  -

                  image-20230404140838247

                  -

                  Better replacer algorithm

                  -

                  In the leaderboard test, we will have multiple threads accessing the pages on the disk. There are two types of threads running in the benchmark:在具体的benchtest中,可以分为两类线程。

                  -
                    -
                  1. Scan threads. Each scan thread will update all pages on the disk sequentially. There will be 8 scan threads.
                  2. -
                  3. Get threads. Each get thread will randomly select a page for access using the zipfian distribution. There will be 8 get threads.
                  4. + +
                  5. 获取协议及版本 HTTP/1.1

                    +
                    String getProtocol()
                  6. +
                  7. 获取访问的客户机的IP地址

                    +
                    String getRemoteAddr()
                  -

                  Given that get workload is skewed(有偏向性的)(i.e., some pages are more frequently accessed than others), you can design your LRU-k replacer to take page access type into consideration, so as to reduce page miss.

                  -
                  -

                  解决方法

                  我们可以回想起当初选择LRU-K而不选择LRU算法的原因:缓存污染。

                  -
                  -

                  LRU 一种缓存淘汰算法

                  -

                  缓存污染:

                  -

                  LRU因为只需要一次访问就能成为最新鲜的数据,当出现很多偶发数据时,这些偶发的数据也会被当作最新鲜的,从而成为缓存。但其实这些偶发数据以后并不会是被经常访问的。

                  -
                  -

                  而在这里也是同理。我们的benchtest中,scan线程是顺序地访问磁盘上所有页,而get线程是遵从zip分布地访问,显然get线程的access记录比scan线程的有价值的多,并且scan线程的数据是很容易污染get线程的。

                  -

                  所以,我的解决方法是,如果某个页被第一次访问,且该访问方式为SCAN,则RecordAccess进入历史访问队列;如果某个页不是被第一次访问,且访问方式为SCAN,则不做任何处理。不用修改UnpinPage的处理方式。

                  -

                  Parallel I/O operations

                  -

                  Instead of holding a global lock when accessing the disk manager【不要在访问disk_manager_的时候使用bpm的全局锁latch_】, you can issue multiple requests to the disk manager at the same time. This optimization will be very useful in modern storage devices, where concurrent access to the disk can make better use of the disk bandwidth.

                  -
                  -

                  解决方法

                  详细的解决方法大佬这边已经说得很清楚了,接下来我就对其总体的做法进行一点总结,加上一些个人理解。

                  -

                  我刚看到这个需求的时候是这么做的:

                  -
                  if (pages_[fid].IsDirty()) {
                  latch_.unlock();
                  disk_manager_->WritePage(pages_[fid].GetPageId(), pages_[fid].GetData());
                  latch_.lock();
                  }
                  +
                  获取请求头
                    +
                  1. 通过请求头的名称获取请求头的值

                    +
                    String getHeader(String name)
                  2. +
                  3. 获取所有的请求头名称

                    +
                    Enumeration<String> getHeaderNames()
                    -

                    也即在原来代码的基础上做简单的改动,每次执行到涉及磁盘读写的地方,就暂时地开一下锁。但其实这样是不行的,当多个线程访问bpm,线程A在这里开锁执行Write,线程B正好得到锁,然后对pages_[fid]执行比如说ResetMemory操作,这样就寄了。

                    -

                    所以,在磁盘读写的时候,我们仍然需要使用锁保护,只不过我们需要选择粒度更细的锁。这时我们就可以想到在page_guard里常用的page自带的锁。在这里用page锁,既能够锁保护,又符合语义,看起来非常完美:

                    -
                    pages_[fid].WLatch();
                    latch_.unlock();
                    if (pages_[fid].IsDirty()) {
                    disk_manager_->WritePage(pages_[fid].GetPageId(), pages_[fid].GetData());
                    }
                    latch_.lock();
                    pages_[fid].WUnlatch();
                    +

                    返回的是一个迭代器

                    +
                  4. +
                  +
                  public class Servletdemo2 extends GenericServlet {

                  public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
                  HttpServletRequest req = (HttpServletRequest) servletRequest;
                  Enumeration<String> enumerator = req.getHeaderNames();
                  while(enumerator.hasMoreElements()){
                  String name = enumerator.nextElement();
                  System.out.println(name);
                  //System.out.println(name+"-----"+req.getHeader(name));
                  }
                  }
                  }

                  输出结果:
                  host
                  connection
                  sec-ch-ua
                  sec-ch-ua-mobile
                  sec-ch-ua-platform
                  upgrade-insecure-requests
                  user-agent
                  accept
                  purpose
                  sec-fetch-site
                  sec-fetch-mode
                  sec-fetch-user
                  sec-fetch-dest
                  accept-encoding
                  accept-language
                  cookie
                  -

                  但由于我们在returnpage_guard的时候会获取锁,因而在这样的情况下,会发生死锁:

                  -
                  auto reader_guard_1 = bpm->FetchPageRead(page_id_temp);
                  auto reader_guard_2 = bpm->FetchPageRead(page_id_temp);
                  +

                  这些请求头名称正是上面的键值对里的键。

                  +
                  获取请求体

                  request将请求体中的数据封装成了流。如果数据是字符,那就是字符流;是视频这种的字节,那就是字节流。

                  +
                  * 步骤:
                  +    1. 获取流对象
                  +  *  BufferedReader getReader():获取字符输入流,只能操作字符数据
                  +  *  ServletInputStream getInputStream():获取字节输入流,可以操作所有类型数据
                  +    2. 操作流获取数据
                  +
                  +
                  @WebServlet("/demo2")
                  public class Servletdemo2 extends GenericServlet {

                  public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
                  HttpServletRequest req = (HttpServletRequest) servletRequest;
                  BufferedReader bfr = req.getReader();
                  String line;
                  while((line = bfr.readLine())!=null){
                  System.out.print(line);
                  }
                  }
                  }
                  +

                  请求体中键值对会在一行里,用&分割

                  -

                  在这里我们首先获取reader_guard_1 ,持有了该 page 的读锁,并允许其他线程读;但在获取reader_guard_2时,FetchPage会在释放 bpm 写锁前,请求该 page 的写锁;但由于reader_guard_1已经申请了该 page 的读锁,就会造成死锁,与预期结果不符。

                  -
                  -

                  因而,我们就可以选择在bpm内部,单独为pages_数组的每一页都维护一个锁,在每个对page页属性进行读写的地方进行锁定:

                  -
                  std::shared_mutex latch_;
                  std::vector<std::mutex> pages_latch_;
                  - -

                  然后对代码进行重排序,尽量分离bpm内部成员和page内部成员属性的修改:(以FetchPage为例)

                  -
                  auto BufferPoolManager::FetchPage(page_id_t page_id, [[maybe_unused]] AccessType access_type) -> Page * {
                  ...
                  if (free_list_.empty()) {
                  frame_id_t fid;
                  if (!replacer_->Evict(&fid)) { ... }
                  // 这些地方不涉及对page的读写,只涉及对bpm内部成员的读写
                  page_table_.erase(page_table_.find(pages_[fid].GetPageId()));
                  page_table_.insert(std::make_pair(page_id, fid));

                  replacer_->RecordAccess(fid, access_type);
                  replacer_->SetEvictable(fid, false);

                  // 两个锁的交接点
                  pages_latch_[fid].lock();
                  latch_.unlock();

                  if (pages_[fid].IsDirty()) {
                  disk_manager_->WritePage(pages_[fid].GetPageId(), pages_[fid].GetData());
                  }

                  Page *res = &(pages_[fid]);
                  res->page_id_ = page_id;
                  res->ResetMemory();
                  disk_manager_->ReadPage(page_id, res->GetData());
                  res->is_dirty_ = false;

                  res->pin_count_ = 1;

                  pages_latch_[fid].unlock();
                  return res;
                  }
                  // 这些地方不涉及对page的读写,只涉及对bpm内部成员的读写
                  frame_id_t fid = free_list_.front();
                  free_list_.pop_front();
                  page_table_.insert(std::make_pair(page_id, fid));

                  replacer_->RecordAccess(fid, access_type);
                  replacer_->SetEvictable(fid, false);

                  // 两个锁的交接点
                  pages_latch_[fid].lock();
                  latch_.unlock();

                  Page *res = &(pages_[fid]);
                  res->page_id_ = page_id;
                  res->ResetMemory();
                  disk_manager_->ReadPage(page_id, res->GetData());
                  res->is_dirty_ = false;

                  res->pin_count_ = 1;
                  pages_latch_[fid].unlock();

                  return res;
                  }
                  +

                  获取时中文乱码

                  +
                    +
                  • get方式:tomcat 8 已经将get方式乱码问题解决了

                    +
                  • +
                  • post方式:会乱码

                    +
                                       * 解决:在获取参数前,设置流的编码:
                     
                    -

                    其他地方也是一样。就不多赘述了。

                    -
                    一个小地方

                    当外界需要对页进行读写时,需要使用page自带的锁;而当bpm内部需要对页进行读写时,则使用的是bpm内部自带的页锁。

                    -

                    这句话说完,相信危险性已经显而易见了:我们使用了两把不同的锁维护了同一个变量!而且可能会有两个线程分别持有这两个锁,对这个变量并发更新!

                    -

                    但其实,在当前这个场景,这么做是没问题的。

                    -

                    外界实质上只能对page的data字段进行读写。因而,有上述危险的,实质上就只有bpm中会对data字段进行改变的地方,也即bpm::NewPage()bpm::FetchPage()bpm::DeletePage()这三个地方。

                    -

                    而在前两个地方,我们会使用到的page都是闲置/已经被释放的页,因而外界不可能,也即不可能有别的线程,会持有page的锁并且对其修改;同样的,在第三个地方,我们会使用的page也是pincount==0的页,仅有当前线程在对其进行读写。

                    -

                    因而,综上所述,这样做是并发安全的。

                    -]]> - - - Project2 B+Tree - /2023/03/13/cmu15445$lab2/ - -

                    参考

                    -

                    CMU 15-445 Project 2 (Spring 2023) | 关于 B+Tree 的十个问题

                    -

                    对crabbing lock、乐观锁做了详尽解释

                    - -

                    Project2 B+Tree

                    -

                    In this programming project you will implement a B+Tree index in your database system.

                    -

                    Your implementation will support thread-safe search, insertion, deletion (including splitting and merging nodes包括分裂和合并结点), and an iterator to support in-order leaf scans.

                    +
                    request.setCharacterEncoding("utf-8");
                    +
                    +
                  • +
                  -

                  它这里的B+树(以及wiki里的)跟王道考研讲得不大一样。王道考研的B+每个结点一个关键字对应一个child,但是这里的是B树的形式。

                  -

                  undefined

                  -

                  image-20230417181239196

                  -

                  Task1 B+Tree Pages

                  -

                  You must implement three Page classes to store the data of your B+Tree:

                  +
                  获取请求参数通用的方法(通用指对get和post通用)

                  这里的请求参数应该是指上面Post的请求体、Get的请求行里的参数,请求头里的参数是获取不到的。

                    -
                  1. B+Tree Page BPlusTreePage

                    -

                    下面那两个的基类

                    -
                  2. -
                  3. B+Tree Internal Page

                    -

                    An Internal Page stores m ordered keys and m+1 child pointers (as page_ids) to other B+Tree Pages.These keys and pointers are internally represented as an array of key/page_id pairs.

                    -

                    Because the number of pointers does not equal the number of keys, the first key is set to be invalid, and lookups should always start with the second key.

                    -

                    At any time, each internal page should be at least half full.【min_size<= <=max_size】

                    -

                    During deletion, two half-full pages can be merged, or keys and pointers can be redistributed to avoid merging. During insertion, one full page can be split into two, or keys and pointers can be redistributed to avoid splitting.

                    +
                  4. 根据参数名称获取参数值

                    +
                    String getParameter(String name)
                    + +

                    如 username=zs&password=123,getParameter(“username”)会得到zs。

                  5. -
                  6. B+Tree Leaf Page

                    -

                    The Leaf Page stores m ordered keys and their m corresponding values. In your implementation, the value should always be the 64-bit record_id for where the actual tuples are stored; see the RID class, in src/include/common/rid.h.

                    -

                    *Note:* Even though Leaf Pages and Internal Pages contain the same type of key, they may have different value types. Thus, the max_size can be different.

                    +
                  7. 根据参数名称获取参数值的数组

                    +
                    String[] getParameterValues(String name)
                    + +

                    如 hobby=xx&hobby=game,会得到{xx,game}

                  8. +
                  9. 获取所有请求的参数名称

                    +
                    Enumeration<String> getParameterNames()
                  10. +
                  11. 取所有参数的map集合

                    +
                    Map<String,String[]> getParameterMap()
                  -
                  -

                  大概就是有一个基类结点,它有两个子类,一个表示b+树的leaf node,另一个表示b+树的internal node,每个结点都占据一个内存页。

                  -

                  也就是说,一个内存页中存储着一个结点类对象。每次我们都是读取一页到内存中,然后将它类型转换为TreeNodePage*,就可以访问其里面的存储数据的数组array_了。体会一下这个思想。

                  -

                  值得一提的是,LeafPage的成员变量中有个这样的成员:

                  -
                  private:
                  // Flexible array member for page data.
                  MappingType array_[0];
                  +
                  请求转发

                  在服务器内部资源跳转

                  +

                  image-20230102195615676

                  +

                  AServlet做了一部分事情,把剩余的事情交给BServlet去做

                  +

                  步骤:

                  +
                    +
                  1. 通过request对象获取请求转发器对象

                    +
                    RequestDispatcher getRequestDispatcher(String path)
                  2. +
                  3. 使用RequestDispatcher对象来进行转发

                    +
                    requestDispatcher.forward(ServletRequest request, ServletResponse response) 
                  4. +
                  +
                  @WebServlet("/demo2")
                  public class Servletdemo2 extends GenericServlet {

                  public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
                  System.out.println("I am "+Servletdemo2.class.getName());
                  //进行转发
                  servletRequest.getRequestDispatcher("/demo3")
                  .forward(servletRequest,servletResponse);
                  }
                  }

                  @WebServlet("/demo3")
                  public class ServletDemo3 extends GenericServlet {

                  public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
                  System.out.println("I am "+ServletDemo3.class.getName());
                  }
                  }
                  -

                  它就是柔性数组成员。

                  +

                  特点:

                  +
                    +
                  1. 浏览器地址栏路径不变
                  2. +
                  3. 只能在服务器内部跳转,只能转发到服务器内部的资源中
                  4. +
                  5. 转发是一次请求,多个资源使用的是同一次请求
                  6. +
                  +
                  共享数据

                  接力工作的两个Servlet可以通过request对象进行数据通信。

                  +
                  * 方法:
                  +
                  +
                    +
                  1. 存储键值对

                    +
                    void setAttribute(String name,Object obj)
                  2. +
                  3. 获取值

                    +
                    Object getAttitude(String name)
                  4. +
                  5. 移除键值对

                    +
                    void removeAttribute(String name)
                  6. +
                  +
                  获取ServletContext
                  ServletContext getServletContext()
                  + +
                  练习:结合数据库与Servlet进行用户登录
                  +

                  要求:

                  +

                  1.编写login.html登录页面
                  username & password 两个输入框
                  2.使用Druid数据库连接池技术,操作mysql,day14数据库中user表
                  3.使用JdbcTemplate技术封装JDBC
                  4.登录成功跳转到SuccessServlet展示:登录成功!用户名,欢迎您
                  5.登录失败跳转到FailServlet展示:登录失败,用户名或密码错误

                  +
                  +
                  文件结构

                  ![屏幕截图 2023-01-02 235207](./JavaWeb/屏幕截图 2023-01-02 235207.png)

                  -

                  在C++中,Flexible Array Member(柔性数组成员)是一种用于定义具有可变大小的结构的技术。它通常用于在结构的末尾声明一个数组,该数组的大小是动态确定的,这允许你在使用该结构时更灵活地处理变长数据。

                  -

                  在你提供的代码片段中,MappingType array_[0]; 是一个柔性数组成员的例子。这里 array_ 后面有 [0],这并不表示它们的大小是固定的0。相反,它们的大小是在运行时动态确定的,而 [0] 的写法是一种历史上的技巧,用于告诉编译器这是柔性数组成员。

                  -

                  例如,如果有一个结构定义如下:

                  -
                  struct MyStruct {
                  // 其他成员...
                  MappingType array_[0];
                  };
                  +

                  错误历程

                  +
                    +
                  1. lib目录位置错误

                    +

                    NoClassDefFoundError解决方案一开始lib目录没放进web-inf,通过此文章得知错误为包未引入,再由下面这篇文章得知lib目录放置错误

                    +

                    JDBC Template报错:java.lang.ClassNotFoundException: org.springframework.jdbc.core.RowMapper

                    +
                  2. +
                  3. druid.properties文件位置错误

                    +

                    报错

                    +

                    java.lang.NullPointerException at java.util.Properties$LineReader.readLine(Properties.java:434)

                    +

                    ,报错位置在pro.load(JDBCUtils.class.getClassLoader().getResourceAsStream("druid.properties"));

                    +

                    由文章

                    +

                    关于java.lang.NullPointerException at java.util.Properties$LineReader.readLine(Properties.java:434)问题

                    +

                    回忆到,由于是使用类加载器获取文件流,故而要求druid.properties文件应该放在resource文件下。对于以前的项目,resource文件都默认是src文件夹。

                    +

                    但是这次放在src目录下还是不行。定睛一看它web项目文件结构中有一个硕大的resources……放在下面果然就好了。

                    +
                  4. +
                  +
                  +
                  druid.properties
                  driverClassName=com.mysql.jdbc.Driver
                  url=jdbc:mysql://localhost:3306/helloworld
                  username=root
                  password=root
                  initialSize=5
                  maxActive=10
                  maxWait=3000
                  -

                  你可以根据需要为 array_ 分配任意数量的内存,例如:

                  -
                  int arraySize = 10;  // 你想要的数组大小
                  MyStruct* myObject = static_cast<MyStruct*>(operator new(sizeof(MyStruct) + arraySize * sizeof(MappingType)));

                  // 在这里你可以使用 myObject,并通过 myObject->array_ 访问柔性数组

                  // 记得在使用完毕后释放内存
                  operator delete(myObject);
                  +
                  html界面
                  <!DOCTYPE html>
                  <html lang="en">
                  <head>
                  <meta charset="UTF-8">
                  <title>Title</title>
                  </head>
                  <body>
                  <!-- action内写Servlet的资源路径 -->
                  <form action="/webdemo4_war/check" method="post">
                  name: <input type="text" name="username" id="username" placeholder="请输入用户名">
                  password: <input type="password" name="password" id="password" placeholder="请输入密码">
                  <input type="submit" value="submit">
                  </form>
                  </body>
                  </html>
                  -

                  在这个例子中,array_ 可以用于存储可变大小的数据,而结构体 MyStruct 的大小将动态地调整为 sizeof(MyStruct) + arraySize * sizeof(MappingType)。这样的设计通常在需要处理变长数据块的场景中比较有用。请注意,在C++17之后,你也可以使用 std::byte 类型来定义柔性数组成员。

                  - -

                  拥有柔性数组成员的实例需要动态分配内存(或者像接下来的把一块内存空间interpret一下),柔性数组成员会占用其他成员没有占用的剩下的空间,也即:

                  -
                  +--------------------------+
                  | Other Members of MyClass |
                  | ... |
                  +--------------------------+
                  | array_ (flexible) |
                  | |
                  | |
                  +--------------------------+
                  +
                  Servlet
                  @WebServlet(value = "/fail")
                  public class FailServlet extends HttpServlet {
                  @Override
                  protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
                  doPost(req,resp);
                  }

                  @Override
                  protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
                  // 设置字符集,防止中文乱码
                  resp.setContentType("text/html;charset=utf-8");
                  resp.getWriter().write("登录失败,用户名或密码错误");
                  }
                  }
                  +
                  @WebServlet(value = "/success")
                  public class SuccessServlet extends HttpServlet {
                  @Override
                  protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
                  doPost(req,resp);
                  }

                  @Override
                  protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
                  resp.setContentType("text/html;charset=utf-8");
                  resp.getWriter().write("登录成功!"+req.getAttribute("uname")+",欢迎您");
                  }
                  }
                  +
                  @WebServlet(value = "/check")
                  public class CheckServlet extends HttpServlet {
                  @Override
                  protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
                  doPost(req,resp);
                  }

                  @Override
                  protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
                  req.setCharacterEncoding("utf-8");

                  //使用BeanUtils把Map转化为对象
                  User tmp = new User();
                  try {
                  BeanUtils.populate(tmp,req.getParameterMap());
                  } catch (IllegalAccessException e) {
                  throw new RuntimeException(e);
                  } catch (InvocationTargetException e) {
                  throw new RuntimeException(e);
                  }

                  User res = UserDao.login(tmp);
                  if (res == null)
                  req.getRequestDispatcher("/fail").forward(req,resp);
                  else{
                  req.setAttribute("uname",res.getUname());
                  req.getRequestDispatcher("/success").forward(req,resp);
                  }

                  }
                  }
                  -

                  Task2a Insertion and Search + Task3 Iterator

                  -

                  The index should support only unique keys; if you try to reinsert an existing key into the index, it should not perform the insertion, and should return false. key必须unique

                  -

                  B+Tree pages should be split (or keys should be redistributed) if an insertion would violate the B+Tree’s invariants. 插入时需要分裂

                  -

                  If an insertion changes the page ID of the root, you must update the root_page_id in the B+Tree index’s header page. You can do this by accessing the header_page_id_ page, which is given to you in the constructor. Then, by using reinterpret_cast, you can interpret this page as a BPlusTreeHeaderPage (from src/include/storage/page/b_plus_tree_header_page.h) and update the root page ID from there. You also must implement GetRootPageId, which currently returns 0 by default.对root_page_id的一切访问,都需要通过header_page_id_。如果插入后改变了root的page ID,需要更新root_page_id

                  -

                  We recommend that you use the page guard classes from Project 1 to help prevent synchronization problems. For this checkpoint, we recommend that you use FetchPageBasic (defined in src/include/storage/page/) when you access a page. 在当前task中,我们推荐你使用pro1实现的page guard,比如说这里如果要访问一页,就需要用 FetchPageBasic

                  -

                  You may optionally use the Context class (defined in src/include/storage/index/b_plus_tree.h) to track the pages that you’ve read or written (via the read_set_ and write_set_ fields) or to store other metadata that you need to pass into other functions recursively.你可以随意使用和修改 Context class,它大概就是一个存储共享信息的对象。

                  -

                  If you are using the Context class, here are some tips:如果你要用,要注意以下几点:

                  -
                    -
                  • You might only need to use write_set_ when inserting or deleting. 当你在为B+树插入/删除结点时,需要用到write_set_。【为什么?这个set存储的是修改路径上的结点吗?然后如果要分裂/合并结点,只需什么while(pop且需要分裂/合并){分裂/合并}??所以说这里的deque是栈结构?】

                    -

                    也就是说,其实我们就可以不用递归了,而是将上下文存储在context->write_set_这个栈里面就行了?大概是这个意思吧

                    -

                    It is possible that you don’t need to use read_set_, depending on your implementation.

                    -

                    read可以用递归(比较简单)也可以不用,所以说具体看实现。

                    -
                  • -
                  • You might want to store the root page id in the context and acquire write guard of header page when modifying the B+Tree.你需要将root page id存储在context,并且在修改b+树(插入、删除)时获取header page的WritePageGurad。

                    -
                  • -
                  • To find a parent to the current node, look at the back of write_set_. It should contain all nodes along the access path.如果想要寻找当前node的父亲,可以看看write_set_.back,它包含了访问路径上所有结点的引用【所以确实是当成栈来用了】

                    -
                  • -
                  • You should use BUSTUB_ASSERT to help you find inconsistent data in your implementation. 需要使用 BUSTUB_ASSERT

                    -

                    For example, if you want to split a node (except root), you might want to ensure there is still at least one node in the write_set_. If you need to split root, you might want to check if header_page_ is std::nullopt.

                    -

                    如果你想要分割一个根节点以外的node,那你必须保证write_set_中至少有一个结点;如果你想要分割根节点,那你必须保证header_page_非空。

                    -
                  • -
                  • To unlock the header page, simply set header_page_ to std::nullopt. To unlock other pages, pop from the write_set_ and drop.如果你想要不锁住header page,那就置其为空指针;如果想释放别的页,那就将它从 write_set_ pop出来就行。【这是因为我们要用到的page类型都是page guard,可以析构时unpin吗?】

                    -
                  • -
                  -
                  -

                  感想

                  由于各种原因,lab2的战线还是拉得太长了。四月份完成了代码初版,中间修了几个bug勉强通过了insertion test,然后一直到十一月底的现在才再次捡起来。不得不说,回看当初的代码,还是能够很清晰地感受到自己这半年多来的成长的,令人感慨。

                  -

                  我先是花了一天的时间重构了下以前写的所有代码,然后再花了两天时间修bug终于通过了insertion test和sequence scale test,并且将b+树的代码修到了我满意的地步(指不像以前那样一坨重复代码和中文注释。。。)。

                  -

                  思路

                  这里简要介绍下B+树的插入实现及我觉得实现中需要注意的几个要点吧。

                  -

                  B+树的插入流程大概是这样的:

                  +
                  +

                  关于BeanUtils

                  +

                  BeanUtils工具类,简化数据封装, 用于封装JavaBean的

                    -
                  1. 查找到key要插入的叶子结点(途中需要维护write_set,也即查找路径)

                    +
                  2. JavaBean:标准的Java类

                    +
                     1. 要求:
                    +     1. 类必须被public修饰
                    +     2. 必须提供空参的构造器
                    +     3. 成员变量必须使用private修饰
                    +     4. 提供公共setter和getter方法
                    + 2. 功能:封装数据
                    +
                  3. -
                  4. 判断结点是否满

                    -
                      -
                    1. 未满,直接插入即可。(我采取插入排序的方法)

                      +
                    2. 概念:

                      +

                      ​ 成员变量:
                      ​ 属性:setter和getter方法截取后的产物

                      +
                                 例如:getUsername() --> Username--> username
                      +
                    3. -
                    4. 已满,需要对结点进行分裂。

                      -

                      推举出中间结点tmp_key,它和新结点page_id接下来将插入到父节点中。

                      +
                    5. 方法:

                      +
                      1. setProperty()
                      +1. getProperty()
                      +1. populate(Object obj , Map map):
                      +
                      +

                      ​ 将map集合的键值对信息,封装到对应的JavaBean对象中

                    -
                  5. -
                  6. 持续进行分裂:

                    -

                    需要注意具体的分裂方法,我认为其中internal page size == 3的情况尤为棘手。在具体实现中,我是这样分裂的:

                    +
                  +
                  JDBCUtils

                  原封不动地照搬了:第二部分-数据库连接池-Druid-定义工具类 部分的代码。

                  +
                  UserDao
                  public class UserDao {
                  private static final JdbcTemplate jdbcTemplate = new JdbcTemplate(JDBCUtils.getDataSource());
                  public static User login(User user){
                  List<User> users = jdbcTemplate.query("select * from usr where uname = ? and pass = ?",
                  new BeanPropertyRowMapper<User>(User.class),
                  user.getUname(),user.getPass());
                  if (users.size() == 0)
                  return null;
                  else
                  return users.get(0);
                  }
                  }
                  + +

                  Response

                  功能

                  设置响应消息。

                  +
                  设置响应行

                  设置状态码

                  +
                  setStatus(int sc);
                  + +
                  设置响应头
                  setHeader(String name, String value) 
                  + +
                  设置响应体

                  以流的方式传输数据。

                  +

                  使用步骤:

                    -
                  1. 推举出将要被插入到父节点的tmp_key

                    -

                    该推举出的key将不会出现在分裂后的新旧结点中,而是会被加入到父节点中。默认为(m + 1) / 2【m为max size】。

                    -

                    但是要尤其注意size为3的case,此时tmp_key为array_[2],很有可能右边结点为空。所以我们需要做点特殊处理:

                    +
                  2. 获取输出流

                      -
                    1. 当要插入到该节点的insert_key > array_[(m + 1) / 2]时,我们推举(m + 1) / 2这个结点。
                    2. -
                    3. insert_key < array_[m / 2],我们转而推举m / 2(此时为array_[1])。
                    4. -
                    5. insert_key < array_[(m + 1) / 2]insert_key > array_[m / 2]时,我们应该对此做出特殊处理,推举insert_key。在此为了代码实现方便,我们还需要调换insert_key和tmp_key的地位
                    6. +
                    7. 字节输出流

                      +
                      ServletOutputStream getOutputStream()
                    8. +
                    9. 字符输出流

                      +
                      PrintWriter getWriter()
                    -
                    special = false;
                    middle = (m + 1) / 2;
                    tmp_key = root->KeyAt(middle);
                    insert_small_than_tmp_key = (comparator_(insert_key, tmp_key) < 0);
                    if (insert_small_than_tmp_key) {
                    middle = m / 2;
                    tmp_key = root->KeyAt(middle);
                    if (comparator_(insert_key, tmp_key) >= 0) {
                    special = true;
                    swap = insert_key;
                    insert_key = tmp_key;
                    tmp_key = swap;
                    }
                    }
                  3. -
                  4. 分裂旧结点

                    -

                    被推举出的tmp_key的value及其右部元素会变成新结点,左部依然留在旧结点,tmp_key会到父节点中去。也即如下图所示:

                    -

                    ![未命名文件 (1)](./cmu15445/未命名文件 (1).png)

                    -

                    依然是注意上面那个case3特殊情况,需要交换insert key和middle key:

                    -
                    if (!special)
                    new_page->SetValueAt(0, root->ValueAt(middle));
                    else {
                    new_page->SetValueAt(0, insert_val);
                    insert_val = root->ValueAt(middle);
                    }
                  5. -
                  6. 持续进行推举和分裂,直到父节点不用分裂

                    -

                    此时直接将insert key和insert value插入排序到父节点即可。

                  7. -
                  +
                10. 使用输出流,将数据输出到客户端浏览器

                -

                然后是Iterator的话,我感觉这也是设计得很不错,让我们亲手写了下c++的重载运算符,也是让我学到了很多c++知识。。。

                -

                遇到的问题

                感觉问题其实不多,主要还是debug有点痛苦花了很长时间()

                -

                cmake报错

                切换内核前后报错。

                -

                Check for working C compiler: /usr/bin/cc - broken

                -

                感觉可能是内核切来切去,导致cmake cache发生了点小问题?总之我最后在5.11内核把build文件删了,重新执行cmake -DCMAKE_CXX_COMPILER=$(which g++) -DCMAKE_C_COMPILER=$(which gcc) ..就ok了。

                -

                page guard

                用错了

                image-20230505002652748

                -

                我发现在这里创建的root最后好像会被释放掉?

                -

                比如我看到新root的page为6,连接也做得好好的,最后出了函数就寄了:

                -

                image-20230505002731312

                -

                还有一个是发现新的leaf page好像不大对,其类型甚至是internal呃呃,我调下看看

                -

                尼玛,绷不住了是这里:

                -

                image-20230505011731797

                -

                原来写的

                -

                image-20230505011744924

                -

                改了之后test2马上ok,乐

                -
                作用域

                还弄了个commit修:

                -

                image-20231130222702358

                -

                一点c++引用震撼

                auto INDEXITERATOR_TYPE::operator*() -> const MappingType &
                +
                案例
                重定向

                资源跳转的一种方式。

                +

                image-20230103153445565

                +
                @WebServlet("/demo1")
                public class ServletDemo extends HttpServlet {
                @Override
                protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
                System.out.println("I am demo1 "+req.hashCode());
                /* 重定向 */
                //设置状态码
                resp.setStatus(302);
                //要填的是完整资源路径。
                resp.setHeader("location","/practice_war/demo2");
                }

                @Override
                protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
                doGet(req,resp);
                }
                }
                -

                这个函数卡了我还挺久。。。里面逻辑很简单,不过难就难在怎么构造出一个const MappingType &

                -

                如果这样:

                -
                INDEX_TEMPLATE_ARGUMENTS
                auto INDEXITERATOR_TYPE::operator*() -> const MappingType & {
                auto page = guard_.As<LeafPage>();
                return std::pair<KeyType, ValueType>(page->KeyAt(cnt_), page->ValueAt(cnt_));
                // or use make_pair. the same result
                }
                +
                @WebServlet("/demo2")
                public class ServletDemo2 extends HttpServlet {
                @Override
                protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
                System.out.println("I am demo2 "+req.hashCode());
                }

                @Override
                protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
                doGet(req,resp);
                }
                }
                -

                会说你临时对象不能作为引用。如果这样:

                -
                INDEX_TEMPLATE_ARGUMENTS
                auto INDEXITERATOR_TYPE::operator*() -> const MappingType & {
                auto page = guard_.As<LeafPage>();
                auto res = new MappingType(std::pair<KeyType, ValueType>(page->KeyAt(cnt_), page->ValueAt(cnt_)));
                return *res;
                }
                +
                输出:
                I am demo1 1675674230
                I am demo2 1675674230
                -

                又会找不到机会delete导致内存泄漏。冥思苦想了半天不知道该怎么办,最后从网上看了别人怎么写的:

                -
                INDEX_TEMPLATE_ARGUMENTS
                auto INDEXITERATOR_TYPE::operator*() -> const MappingType & {
                auto page = guard_.As<LeafPage>();
                return page->PairAt(cnt_);
                }

                INDEX_TEMPLATE_ARGUMENTS
                auto B_PLUS_TREE_LEAF_PAGE_TYPE::PairAt(int index) const -> const MappingType & {
                return array_[index];
                }
                +

                重定向的这几行代码其实是可以简化的:

                +
                /* 重定向 */
                //设置状态码
                resp.setStatus(302);
                //要填的是完整资源路径。
                resp.setHeader("location","/practice_war/demo2");
                -

                我服了。

                -

                不过可能有更好的解决方法?可惜我c++水平不大够,所以暂时想不出来了。

                -

                Task4 Remove

                感想

                由于有了insert的沉淀,remove的实现便相较不大困难了,写完代码到通过内置的delete测试只花了一天的时间。

                -

                思路

                  -
                1. 找到需要操作的叶结点路径

                  -
                2. -
                3. 判断叶子结点属于以下四种策略中的哪一种,执行对应策略(优先级从高到低):

                  -
                    -
                  1. 直接删除

                    -

                    当删除后叶结点元素数仍在合法范围,并且路径上父节点没有target key,直接删除然后返回即可。

                    -
                  2. -
                  3. 更新父节点路径

                    -

                    当删除后叶结点元素数仍在合法范围,并且路径上父节点target key,直接删除然后向上回溯更新父节点即可。

                    -
                  4. -
                  5. 窃取兄弟元素

                    -
                    If do a steal, we should update related key in the parent, and update up till reaching the root.

                    /*
                    For that steal is more simple, we first check whether it can do a steal first.
                    We steal the node whose size is biggest between the next and the prev node.
                    If the prev size is bigger, we only update self key in parent.
                    If the next size is bigger, we update both self key and next key n parent.
                    After that, we trace back and update all the parent nodes which contains the
                    target key.
                    */
                    +

                    可以简化为:

                    +
                    resp.sendRedirect("/practice_war/demo2");
                    -

                    当删除后叶结点元素数过少,并且左右兄弟元素充足,则从左右兄弟窃取一个。优先窃取元素最多者。

                    +
                    +

                    关于req对象不一样,但hashcode值相同的解释:

                    +

                    hashcode很大程度与对象内存空间相关,与对象的具体内容没什么关系。两个对象拥有相同的hashcode有可能只是因为存储的内存空间位置大小都相同导致的。所以是因为两次的req对象都占用了同一个内存空间【JVM调度问题】,所以才让hashcode值相同。这两个对象实质上是不一样的。

                    +
                    +

                    重定向的特点(与请求转发完全相反):

                      -
                    1. 窃取左兄弟

                      -

                      窃取左兄弟的最大元素

                      -

                      需递归更新自身父节点路径上的对应值。

                      -
                    2. -
                    3. 窃取右兄弟

                      -

                      窃取右兄弟的最小元素

                      -

                      需要递归更新自身和右兄弟父节点路径上的对应值。

                      -
                    4. -
                    -

                    之后返回即可。

                    -
                  6. -
                  7. 合并

                    -
                    /*
                    Need to merge with one of the node. It is more simple to try to merge the left node
                    first. So the strategy:
                    1. Pick the prev node to merge. (If leaf is most left, pick next node)
                    2. Update delete-key. (for prev, it's leaf[0]; for next, it's right key, and need
                    to update self)
                    3. Go up till reaching root. Do:
                    1. delete delete-key.
                    2. pick merging or stealing like above.
                    1. if merge, update delete-key, go up;
                    2. if steal, break to do update and has no need to go up.
                    4. Remember to deal with edge case: root.
                    */
                    - -

                    当删除后叶结点元素数过少,并且左右兄弟元素也都是最小值,那么需要与左右兄弟之一进行合并。优先合并左兄弟。合并都为大->小,也即target->左兄弟 或者 右兄弟->target。

                    -

                    需要递归删除父节点路径上的merge from元素。

                    -
                  8. +
                  9. 浏览器地址栏路径改变
                  10. +
                  11. 可以访问其他站点的资源
                  12. +
                  13. 使用多次请求,不能使用request对象共享数据
                  +

                  路径写法:

                  +
                    +
                  1. 相对路径:通过相对路径不可以确定唯一资源

                    +
                      +
                    • 规则:找到当前资源和目标资源之间的相对位置关系
                    • +
                  2. -
                  3. 可以看到,1/2/3三种情况都可以实现简单地直接返回。4稍显复杂,由于递归删除,所以需要对每一个父节点都再次进行上面几种策略的判断,直到遇到情况123返回为止。

                    +
                  4. 绝对路径:通过绝对路径可以确定唯一资源

                    +
                  -

                  遇到的问题

                  一个比较sb的小bug……

                  -

                  image-20231204163052528

                  -

                  Task5 Concurrency

                  感想

                  这位可更是重量级,足足花了我三天的时间……不过感觉第一次处理这么一个复杂的并发情景,花的时间还是值得的。

                  -

                  最后的结果虽然很一般(指排行榜倒数水平。。。),但至少还是过了。就先这样吧。

                  -

                  image-20231204170030245

                  -

                  思路

                  我实现了crabbing lock+optimal lock。对于Insert和Remove,都是先在一开始获取header page和路径上父节点的读锁,然后在之后有可能向上更新时(比如说Insert的需要分裂、Remove的Update和Merge两种情况),丢弃所有读锁,然后获取header page和路径上父节点的写锁。

                  -

                  不过感觉我这个思路还是略有粗糙,因为相当一部分时间都得占用header page的写锁。但是我思考了一下细粒度方案,发现还是有点难实现。比如说,对于insert,细粒度化的方式也许就是一直持有header page的读锁,一直到需要分裂根节点时,才释放读锁获取写锁。但这样一来就会暴露一个危险的空窗期(而且感觉这个空窗期还不小),当你真的拿到写锁,这树的结构可能已经变得不知道什么样了。在这种情况下,你就需要再做一次回溯工作,也即获取从新root结点到旧root结点的路径,递归插入insert key和insert value,最后安全分裂根节点(因为此时已经安全持有了header page写锁)。感觉思路也是比较易懂,但是实现上还是太麻烦了,所以先暂且搁置吧。

                  -

                  遇到的问题

                  这种感觉大多还是在面向测试用例见招拆招……所以其实感觉没什么好说的。

                  -

                  bpm遗留

                  这个并发问题是这样的,我原来是先evict,然后再写回被替换的页面,写回过程中磁盘没加bpm锁。这就会出现这样一个情况:

                  -

                  一个page被进程A evict,进程A还没执行写回的时候这个page又被进程B捡回来了,因为还没写入所以磁盘空空如也。这时候pages_latch_这个细粒度锁不能防范这种情况,是因为此时这个page对应的container不是同一个,所以fid不同,细粒度锁不同导致寄。

                  -

                  解决方法是要么写的时候持有bpm锁,但是这太太慢了。另一个就是干脆直接在unpin的时候不带bpm锁顺便写回了。也即把写回从evict后移到unpin中立即写回:

                  -
                  if (pages_[fid].GetPinCount() == 0 && pages_[fid].IsDirty()) {
                  pages_[fid].is_dirty_ = false;
                  disk_manager_->WritePage(pages_[fid].GetPageId(), pages_[fid].GetData());
                  }
                  - -

                  火焰图性能分析

                  -

                  FlameGraph

                  -

                  参考博客

                  -
                  -

                  看起来感觉大多性能损耗还是在bpm上,特别是LRU-K。也许是我的全局锁太暴力了。

                  -

                  out

                  -]]> - - - CMU15445 - /2023/03/13/cmu15445/ - -

                  实验官网

                  -

                  代码

                  - -

                  Project0 C++ Primer

                  Project1 Buffer Pool

                  Project2 B+Tree Index

                  Project3 Query Execution

                  ]]>
                  - - labs - -
                  - - Project3 Query Execution - /2023/03/13/cmu15445$lab3/ - Project3 Query Execution

                  TODO,注意一下为什么每个executor的child executor的&&和&的差别

                  -
                  -

                  In this project, you will implement the components that allow BusTub to execute queries. You will create the operator executors that execute SQL queries and implement optimizer rules to transform query plans.

                  -

                  实现SQL查询的执行,并且实现语句优化。

                  -
                  -
                  -

                  In this project, you will add new operator executors and query optimizations to BusTub.

                  -

                  BusTub uses the iterator (i.e., Volcano) query processing model, in which every executor implements a Next function to get the next tuple result.

                  -

                  When the DBMS invokes an executor’s Next function, the executor returns either:

                  -

                  (1) a single tuple

                  -

                  ​ In BusTub’s implementation of the iterator model, 除了元组外还会返回record identifier (RID)

                  -

                  (2) an indicator that there are no more tuples.

                  -

                  With this approach, each executor implements a loop that continues calling Next on its children to retrieve tuples and process them one by one.

                  -
                  -

                  Background

                  Bustub Framewor

                  image-20231227153858926

                  -

                  AST

                  介绍完了bustub的框架之后,它对通过语法树进行查询优化进行了详细的样例介绍。

                  -

                  首先温习一下什么是语法树(abstract syntax tree, AST ):

                  -

                  SQL语句

                  -
                  Select `title`
                  From Books, Borrowers, Loans
                  Where Books.LC_NO = Loans.LC_NO and Borrowers.CARD_NO = Loans.CARD_NO and DATE <= 1/1/78
                  - -

                  其语法树表示+优化结果如下图所示:

                  -

                  image-20231227155236633

                  -

                  算法如下,其关键思路就是选择投影尽早做,能移多下去就移多下去

                  -

                  image-20231227155806019

                  -

                  而这里15445介绍的也是这样的语法树优化算法。

                  -

                  首先记录一下它这几个专有名词对应的操作:

                  -
                  -
                    -
                  1. Projection:投影
                  2. -
                  3. Filter:选择
                  4. -
                  5. MockScan:对一个表进行的扫描操作
                  6. -
                  7. Aggregation:聚合函数
                  8. -
                  9. NestedLoopJoin:嵌套循环连接
                  10. -
                  -
                  -

                  再结合它给的几个语法树的例子:

                  -
                  SELECT * FROM __mock_table_1;

                  === PLANNER ===
                  Projection { exprs=[#0.0, #0.1] } | (__mock_table_1.colA:INTEGER, __mock_table_1.colB:INTEGER)
                  MockScan { table=__mock_table_1 } | (__mock_table_1.colA:INTEGER, __mock_table_1.colB:INTEGER)
                  === OPTIMIZER ===
                  MockScan { table=__mock_table_1 } | (__mock_table_1.colA:INTEGER, __mock_table_1.colB:INTEGER)
                  - -
                  SELECT colA, MAX(colB) FROM
                  (SELECT * FROM __mock_table_1, __mock_table_3 WHERE colA = colE) GROUP BY colA;

                  === OPTIMIZER ===
                  Agg { types=[max], aggregates=[#0.1], group_by=[#0.0] }
                  NestedLoopJoin { type=Inner, predicate=(#0.0=#1.0) }
                  MockScan { table=__mock_table_1 }
                  MockScan { table=__mock_table_3 }
                  - -

                  image-20231227160450894

                  -
                  SELECT * FROM __mock_table_1 WHERE colA > 1;

                  === OPTIMIZER ===
                  Filter { predicate=(#0.0>1) } | (__mock_table_1.colA:INTEGER, __mock_table_1.colB:INTEGER)
                  MockScan { table=__mock_table_1 } | (__mock_table_1.colA:INTEGER, __mock_table_1.colB:INTEGER)
                  - -
                  values (1, 2, 'a'), (3, 4, 'b');

                  === PLANNER ===
                  Projection { exprs=[#0.0, #0.1, #0.2] } | (__values#0.0:INTEGER, __values#0.1:INTEGER, __values#0.2:VARCHAR)
                  Values { rows=2 } | (__values#0.0:INTEGER, __values#0.1:INTEGER, __values#0.2:VARCHAR)
                  === OPTIMIZER ===
                  Values { rows=2 } | (__values#0.0:INTEGER, __values#0.1:INTEGER, __values#0.2:VARCHAR)
                  +
                4. 规则:判断定义的路径是给谁用的?判断请求将来从哪儿发出

                  +
                    +
                  • 客户端浏览器使用:需要加虚拟目录(项目的访问路径)

                    +

                    比如说在页面中弄了个a标签,将来是要给客户端点的,那么这个a标签的href就要用绝对路径。

                    +

                    再比如说重定向:

                    +
                    //要填的是完整资源路径。
                    resp.setHeader("location","/practice_war/demo2");
                    -

                    可以看到,它大概是用缩进来表示了AST的父子关系。

                    -

                    我们课上学习的语法树中每个table标志对应着一个MockScan;笛卡尔积+选择操作可以表示为一个NestedLoopJoin。

                    -

                    对于这些输出的意义,指导书也给了详细的解释:

                    -

                    ColumnValueExpression

                    -

                    也即类似exprs=[#0.0, #0.1]#0意为第一个子节点(不是第一个表的意思。。)

                    -

                    Volcano Model

                    introduction

                    -

                    火山模型和优化(向量化执行、编译执行) 这篇文章写得很详细,下文也摘抄自该博客

                    -

                    数据库内核通过 code-gen 提升性能的探索

                    -
                    -

                    火山模型又称 Volcano Model 或者 Pipeline Model(或者迭代器模型)。该计算模型将关系代数中每一种操作抽象为一个 Operator,将整个 SQL 构建成一个 Operator 树,从根节点到叶子结点自上而下地递归调用 next() 函数。

                    -

                    一般Operator的next() 接口实现分为三步:

                    +

                    这个路径将来是给客户端将来要使用的路径,是客户端路径,所以要加虚拟目录。

                      -
                    • 调用子节点Operator的next() 接口获取一行数据(tuple);
                    • -
                    • 对tuple进行Operator特定的处理(如filter 或project 等);
                    • -
                    • 返回处理后的tuple。
                    • +
                    • 建议虚拟目录动态获取:request.getContextPath()
                    • +
                    • ,
                      ,重定向…
                    -

                    因此,查询执行时会由查询树自顶向下的调用next() 接口,数据则自底向上的被拉取处理。火山模型的这种处理方式也称为拉取执行模型(Pull Based)。

                    -

                    大多数关系型数据库都是使用迭代模型的,如 SQLite、MongoDB、Impala、DB2、SQLServer、Greenplum、PostgreSQL、Oracle、MySQL 等。

                    -

                    火山模型的优点是,处理逻辑清晰,简单,每个Operator 只要关心自己的处理逻辑即可,耦合性低。但是缺点也非常明显:

                    +
                  • +
                  • 服务器使用:不需要加虚拟目录

                    +

                    比如说之前的请求转发

                      -
                    • 每处理一行需要调用多次next() 函数,而next()为虚函数,开销大。

                      -

                      编译器无法对虚函数进行inline优化,同时也带来分支预测的开销,且很容易预测失败,导致CPU流水线执行混乱。

                      +
                    • 转发路径
                    • +
                  • -
                  • 数据以行为单位进行处理,不利于CPU cache 发挥作用。

                    +
                5. -

                  pipeline breaker

                  火山模型显而易见是以从上到下一个流水线形式执行的,它的最理想情况是每个流水线节点所需的这个tuple都存储在寄存器中。然而,有一些操作,如聚合函数等等,需要对整个表进行操作才能获取到当前所需tuple,而整个表显然最多只能读入到内存中,这样的操作就被称为pipeline breaker

                  -

                  下面的实现中的aggregation、sort、hash join的build阶段都是pipeline breaker,这些复杂的操作阶段都需要在init()函数中进行。

                  -

                  Summary

                  TODO,从宏观整个架构简介

                  -

                  ADDITIONAL INFORMATION

                  System Catalog

                  -

                  The entirety of the catalog implementation is in src/include/catalog/catalog.h. You should pay particular attention to the member functions Catalog::GetTable() and Catalog::GetIndex(). You will use these functions in the implementation of your executors to query the catalog for tables and indexes.

                  -
                  -

                  它意思大概是说在实现executor时可能需要用到catelog里这两个函数。

                  -

                  GetTable返回一个TableInfo

                  -
                  struct TableInfo {
                  /** The table schema */
                  Schema schema_;
                  /** The table name */
                  const std::string name_;
                  /** An owning pointer to the table heap */
                  std::unique_ptr<TableHeap> table_;
                  /** The table OID */
                  const table_oid_t oid_;
                  };
                  + +
                +
                服务器输出字符数据到浏览器
                @WebServlet("/responseDemo4")
                public class ResponseDemo4 extends HttpServlet {
                protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

                //获取流对象之前,设置流的默认编码:ISO-8859-1 设置为:GBK
                response.setCharacterEncoding("utf-8");

                //告诉浏览器,服务器发送的消息体数据的编码。建议浏览器使用该编码解码
                response.setHeader("content-type","text/html;charset=utf-8");

                /* 也有设置编码的简单形式
                //简单的形式,设置编码
                response.setContentType("text/html;charset=utf-8");
                */

                //1.获取字符输出流
                PrintWriter pw = response.getWriter();
                //2.输出数据
                //pw.write("<h1>hello response</h1>");
                pw.write("你好啊啊啊 response");
                }

                protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
                this.doPost(request,response);
                }
                }
                +
                服务器输出字节数据到浏览器
                @WebServlet("/responseDemo5")
                public class ResponseDemo5 extends HttpServlet {
                protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
                //依然要保证编码一致
                response.setContentType("text/html;charset=utf-8");

                //1.获取字节输出流
                ServletOutputStream sos = response.getOutputStream();
                //2.输出数据
                sos.write("你好".getBytes("utf-8"));
                }

                protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
                this.doPost(request,response);
                }
                }
                +
                验证码
                @WebServlet("/demo1")
                public class ServletDemo extends HttpServlet {
                @Override
                protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
                //验证码图片大小
                final int width = 100;
                final int height = 50;

                /* 绘制验证码 */
                BufferedImage image = new BufferedImage(width,height,BufferedImage.TYPE_INT_RGB);
                Graphics pen = image.getGraphics();
                //绘制背景
                pen.setColor(new Color((int)(Math.random()*255),(int)(Math.random()*255),(int)(Math.random()*255)));
                pen.fillRect(0,0,width,height);
                //绘制边框
                pen.setColor(new Color((int)(Math.random()*255),(int)(Math.random()*255),(int)(Math.random()*255)));
                pen.drawRect(0,0,width-1,height-1);
                //随机填充字母数字
                String source = "QWERTYUIOPASDFGHJKLZXCVBNMqwertyuiopasdfghjklzxcvbnm1234567890";
                for (int i = 1; i <= 4; i++){
                int index = (int)(Math.random()*source.length());
                pen.drawString(source.substring(index,index+1),20*i,27);
                }
                //画干扰色线
                pen.setColor(new Color((int)(Math.random()*255),(int)(Math.random()*255),(int)(Math.random()*255)));
                for (int i = 0; i < 5; i++){
                pen.drawLine((int)(Math.random()*width),(int)(Math.random()*height),(int)(Math.random()*width),(int)(Math.random()*height));
                }

                //将图片输出
                ImageIO.write(image,"jpg",resp.getOutputStream());
                }

                @Override
                protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
                doGet(req,resp);
                }
                }
                -
                -

                For the table modification executors (InsertExecutor, UpdateExecutor, and DeleteExecutor) you must modify all indexes for the table targeted by the operation. You may find the Catalog::GetTableIndexes() function useful for querying all of the indexes defined for a particular table. Once you have the IndexInfo instance for each of the table’s indexes, you can invoke index modification operations on the underlying index structure.

                -

                In this project, we use your implementation of B+ Tree Index from Project 2 as the underlying data structure for all index operations. Therefore, successful completion of this project relies on a working implementation of the B+ Tree Index.

                -
                -

                话说index是那个索引吗,就是每张表有几个建立在某个属性的索引,也即一张表可以有n棵b+树

                -

                GetIndex返回一个IndexInfo

                -
                struct IndexInfo {
                /** The schema for the index key */
                Schema key_schema_;
                /** The name of the index */
                std::string name_;
                /** An owning pointer to the index */
                std::unique_ptr<Index> index_;
                /** The unique OID for the index */
                index_oid_t index_oid_;
                /** The name of the table on which the index is created */
                std::string table_name_;
                /** The size of the index key, in bytes */
                const size_t key_size_;
                };
                +
                <!DOCTYPE html>
                <html lang="en">
                <head>
                <meta charset="UTF-8">
                <title>Title</title>
                </head>
                <body>
                <img id="img" src="/practice_war/demo1"/>
                <a href="" id = "a">看不清?换一张</a>
                <script>
                window.onload = function (){
                let img = document.getElementById("img");
                let a = document.getElementById("a");
                img.onclick = function (){
                //加时间戳作为请求参数,为了防止浏览器不更换图片缓存
                img.src = "/practice_war/demo1?"+new Date().getTime();
                }
                a.onclick = img.onclick;
                }
                </script>
                </body>
                </html>
                -

                Optimizer Rule Implementation Guide

                -

                The BusTub optimizer is a rule-based optimizer. Most optimizer rules construct optimized plans in a bottom-up way(自底向上). Because the query plan has this tree structure, before applying the optimizer rules to the current plan node, you want to first recursively apply the rules to its children.

                -

                At each plan node, you should determine if the source plan structure matches the one you are trying to optimize, and then check the attributes in that plan to see if it can be optimized into the target optimized plan structure.

                -

                In the public BusTub repository, we already provide the implementation of several optimizer rules. Please take a look at them as reference.

                -
                -

                Task1 Access Method Executors

                -

                In the background section above, we saw that the BusTub can already retrieve data from mock tables in SELECT queries.

                -

                This is implemented without real tables by using a MockScan executor to always generate the same tuples using a predefined algorithm.

                -

                This is why you cannot update these tables.

                -
                -

                也就是说意思是目前的mockscan executor不是真的查表,而是返回固定的元组。

                -

                看了一遍代码,感觉大概明白了。我们可以来看一下迭代器的Next函数:

                -
                auto MockScanExecutor::Next(Tuple *tuple, RID *rid) -> bool {
                if (cursor_ == size_) {
                // Scan complete
                return EXECUTOR_EXHAUSTED;
                }
                if (shuffled_idx_.empty()) {
                *tuple = func_(cursor_);
                } else {
                *tuple = func_(shuffled_idx_[cursor_]);
                }
                ++cursor_;
                *rid = MakeDummyRID();
                return EXECUTOR_ACTIVE;
                }
                +

                ServletContext对象

                概念

                代表整个web应用,可以和servlet容器(服务器)通信

                +

                获取

                通过request对象获取
                ServletContext getServletContext()
                -

                其核心就是调用func_来获取表的元组。

                -

                也就是说是这样的,每个MockScanExecutor用来执行一个plan,那么也就对应着某一个table。通过执行某一个table特定的迭代function,就可以返回元组。

                -

                这个迭代function比如说对于表tas_2023是这样的:

                -
                if (table == "__mock_table_tas_2023") {
                return [plan](size_t cursor) {
                std::vector<Value> values{};
                values.push_back(ValueFactory::GetVarcharValue(ta_list_2023[cursor]));
                values.push_back(ValueFactory::GetVarcharValue(ta_oh_2023[cursor]));
                return Tuple{values, &plan->OutputSchema()};
                };
                }
                +
                通过HttpServlet获取
                this.getContext();
                -

                也即MockScanExecutor负责对表指针的管理,function负责实际对表的物理访问。这样就成功解耦了。

                -
                -

                In this task, you will implement executors that read from and write to tables in the storage system.

                -
                  -
                • src/execution/seq_scan_executor.cpp
                • -
                • src/execution/insert_executor.cpp
                • -
                • src/execution/update_executor.cpp
                • -
                • src/execution/delete_executor.cpp
                • -
                • src/execution/index_scan_executor.cpp
                • -
                -
                -

                而我们本次实验就是需要实现这么一大堆的executor。看来又是个体力活了。

                -

                seq_scan

                一些想法

                c++知识

                image-20240115121419677

                -

                可以看到,前缀++重载的运算符方法和后缀++是不一样的。

                -
                -

                这里我理解得还是肤浅了…… 根据 这篇文章++i 的内部类定义为 T& T:: operator++();,而 i++ 的内部类定义为 T T:: operator++(int);[1]前置操作返回引用,后置操作返回值。后置操作的 int 参数是一个虚拟参数,用于区分运算符 ++ 的前置和后置。理论上,i++ 会产生临时对象,实践中,编译器会对内置类型进行优化;而对于自定义类型(如这里的 Iterator),++i 的性能通常优于 i++

                -
                -
                MockScan

                值得一提的是它跟MockScan的关系。MockScan是一种模拟操作,所以各种表都是硬编码在它的mock_scan.h里的;而SeqScan就是真正的遍历操作了,它需要获取tuple就需要通过各种复杂的物理操作和封装一步步读取了。

                -
                physical layer

                通过实现SeqScan,我们可以初步窥探整个bustub物理层面交互的架构。

                -

                跟之前project中的索引entry一样,实际的数据tuple也保存在page中,其对应类为TablePage。并且是堆文件组织结构:

                -

                image-20240115114448798

                -
                -

                TablePage的结构值得一提。

                -

                在它的成员定义中,我们可以看到其中有两个柔性数组成员(Flexible array member):

                -
                char page_start_[0];
                TupleInfo tuple_info_[0];
                +

                功能

                获取MIME类型

                MIME是在互联网通信过程中定义的一种文件数据类型

                +
                        * 格式: 大类型/小类型   text/html        image/jpeg
                +
                +
                /*
                @param: 文件的后缀扩展名,如.txt
                */
                String getMimeType(String file);
                -

                之前的Project2,我们只接触过一个的case,这里的两个感觉其实也同理可得,相当于page_start_tuple_info_都指向最末尾空闲空间的开始。

                -

                TablePage的实际存储结构如下:

                -
                ->  increase											   increase  <-
                | ------------------------------- | ********************************* |
                ↑ ↑
                page_start & tuple_info +TUPLE_INFO_SIZE*sizeof(TupleInfo)
                +

                image-20230107010006874

                +

                mime映射存在了服务器的xml文件中。

                +

                使用案例:

                +
                System.out.println(this.getServletContext().getMimeType("a.txt"));
                -

                也即tuple info存储在前半部分,tuple data存储在后半部分,并且二者增长方式相反。

                -
                -

                而多页TablePage就构成了一个TableHeap,也即其物理存储空间。每次创建表时,我们就会分配对应的heap空间和相关meta data。TableHeap对外提供了增删改查元组的方法,也提供了一个迭代器实现TableIterator,用于遍历里面的元素。

                -

                而由于元组tuple存储在磁盘中,所以我们需要在读取它的值的时候先进行反序列化DeserializeFrom,这个过程需要用到表的类型信息和offset信息之类的,所以Tuple::GetValue需要传入schema参数。

                -

                实现

                它基本原理也就是顺序遍历整张表,没什么好说的。

                -

                在本次的sequence scan实现中,我们就需要首先获取表对应的iterator:

                -
                // 巨长一串
                table_iterator_ = std::make_unique<TableIterator>(exec_ctx_->GetCatalog()->GetTable(plan_->GetTableOid())->table_->MakeIterator());
                -

                然后通过这个iterator不断迭代获取元素即可。

                -

                有一点要注意的,应该是对删除元组的处理,毕竟sequence scan算是是实现其他二级操作的基石了,所以我们必须在这里处理删除元组。具体逻辑如下:

                -
                do {
                if (table_iterator_->IsEnd()) {
                return EXECUTOR_EXHAUSTED;
                }

                get tuple;
                ++ iterator;

                } while(tuple_meta.is_deleted_);
                +
                共享数据

                ServletContext是一个域对象,可以用来共享数据。

                +

                ServletContext代表着服务器,因而它的生命周期跟随服务器关闭而灭亡。ServletContext可以共享所有请求的数据。也就是说,任何一次请求,任何用户,看到的ServletContext域都是同一个。

                +

                这样大的效果也使得我们需要更加谨慎地使用它。一旦数据存入ServletContext域,就只会在服务器关闭后才会消亡,很耗内存。

                +
                获取文件的真实(服务器)路径
                String getRealPath();
                +

                经测试发现,这东西只是起了一个字符串拼接的作用,是不会帮忙检查文件是否存在的。

                +

                学到这我顺便看了看文件放在不同的地方最后应该如何访问:

                +

                image-20230107012903495

                +

                这是最终部署项目文件夹的结构:

                +

                image-20230107013010276

                +

                可以看到只有bcd被保留了。它们的目录要这样获取:

                +
                    @Override
                protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
                ServletContext context = this.getServletContext();

                System.out.println(context.getRealPath("/WEB-INF/classes/b.txt"));
                System.out.println(context.getRealPath("/c.txt"));
                System.out.println(context.getRealPath("/WEB-INF/d.txt"));
                }
                /*输出结果:
                D:\aWorkStorage\etc\apache-tomcat-8.5.83\webapps\practice_war\WEB-INF\classes\b.txt
                D:\aWorkStorage\etc\apache-tomcat-8.5.83\webapps\practice_war\c.txt
                D:\aWorkStorage\etc\apache-tomcat-8.5.83\webapps\practice_war\WEB-INF\d.txt
                是我的电脑里tomcat的目录
                */
                -

                insert

                一些想法

                recursive execute

                对于SQL的嵌套子查询,bustub采用的是递归实现。具体来说,以insertion为例:

                -

                外界调用情况如下所示。

                -
                // Execute a query plan.
                auto Execute(...) -> bool {
                // Construct the executor for the abstract plan node
                auto executor = ExecutorFactory::CreateExecutor(exec_ctx, plan);
                executor->Init();
                PollExecutor(executor.get(), plan, result_set);
                PerformChecks(exec_ctx);
                }
                +

                案例:文件下载

                要求
                  +
                • 文件下载需求:
                    +
                  1. 页面显示超链接
                  2. +
                  3. 点击超链接后弹出下载提示框
                  4. +
                  5. 完成图片文件下载,那种会存到你电脑download目录下,而不是直接加载出来的
                  6. +
                  +
                • +
                +

                image-20230201170701909

                +

                用户点击下载->请求发送给某个servlet,servlet修改response->tomcat响应用户,传递的图片资源按照response的方法打开

                +
                代码

                说实话看了感觉有点难以下手,主要还是完全不知道html和servlet怎么交互造成的,看了老师讲解才有点恍然大悟。

                +

                我们可以把a标签以重定向的角度去看。它会新建一个request,然后发送到它的href中的那个url。在此处我们将url设置为/practice_war/download?filename=1.jpg,也即要以GET的方式发送给download,请求体为filename=1.jpg。然后servlet执行结束后,就会将信息存储在resp中返回给tomcat,由tomcat发送给用户。

                +
                html
                <body>
                <a href="/practice_war/download?filename=1.jpg" id = "a">点击下载图片</a>
                </body>
                -

                CreateExecutor是一个递归函数,递归创建每个子查询的实例,把对应的executor返回给父查询

                -
                auto ExecutorFactory::CreateExecutor(...)
                -> std::unique_ptr<AbstractExecutor> {

                switch (plan->GetType()) {

                case PlanType::SeqScan: {
                return std::make_unique<SeqScanExecutor>(exec_ctx, dynamic_cast<const SeqScanPlanNode *>(plan.get()));
                }

                case PlanType::Insert: {
                auto insert_plan = dynamic_cast<const InsertPlanNode *>(plan.get());
                // 递归创建每个子查询的实例
                auto child_executor = ExecutorFactory::CreateExecutor(exec_ctx, insert_plan->GetChildPlan());
                // 把对应的executor返回给父查询
                return std::make_unique<InsertExecutor>(exec_ctx, insert_plan, std::move(child_executor));
                }
                }
                }
                +
                servlet

                思路:

                +

                获取要下载的资源,并且将其输入到resp的stream中。

                +

                有一点需要非常注意:

                +
                resp.setContentType(this.getServletContext().getMimeType(path));
                resp.setHeader("content-disposition","attachment;filename="+name);
                -

                然后我们再在父查询的Init中调用子查询的Init和Next等方法

                -
                void InsertExecutor::Init() {
                child_executor_->Init();
                ...
                }
                - -

                如此,就能递归实现嵌套子查询。

                -

                实现

                -

                The InsertExecutor inserts tuples into a table and updates any affected indexes.

                -

                The planner will ensure that the values have the same schema as the table. The executor will produce a single tuple of integer type as the output, indicating how many rows have been inserted into the table.

                -
                -

                这里将Insert语句插入的值视为一个匿名子表,对其初始化后使用它的迭代器进行元素访问即可。

                -

                update

                一些想法

                expression

                bustub将一切表达式抽象为了这么几个类:

                -
                AbstractExpression // 基类
                ConstantValueExpression // 常量值表达式
                ColumnValueExpression // 列值表达式,访问某一列的值
                ArithmeticExpression // 算术表达式,树递归结构,子节点是值or算术表达式
                ComparisonExpression // 比较表达式,表示两个表达式
                LogicExpression // 逻辑表达式
                StringExpression // 字符串表达式,包括原字符串or upper之类的
                - -

                而从UpdatePlanNode中,我们可以获取到update字句的所有表达式:

                -
                /** The new expression at each column */
                std::vector<AbstractExpressionRef> target_expressions_;
                - -

                比如此处:

                -
                bustub> explain (o,s) update test_1 set colB = 15445;
                === OPTIMIZER ===
                // 可以注意这边target_exprs的值
                Update { table_oid=20, target_exprs=[#0.0, 15445, #0.2, #0.3] } | (__bustub_internal.update_rows:INTEGER)
                SeqScan { table=test_1 } | (test_1.colA:INTEGER, test_1.colB:INTEGER, test_1.colC:INTEGER, test_1.colD:INTEGER)
                - -

                然后我们分别计算每个expression的值,就可以获取更新之后的元组:

                -
                   // insert again
                std::vector<Value> insert_values;
                for (auto exp : plan_->target_expressions_) {
                // tuple为旧值元组
                insert_values.push_back(exp->Evaluate(&tuple, table_info->schema_));
                }
                // 注意table_info应为要插入的表的info,此处易写为update plan子表的info
                table_heap->InsertTuple(TupleMeta(), Tuple(insert_values, &(table_info->schema_)));
                - - - -
                lazy delete

                删除元组的实现似乎只是简单地标记is_delete_为true就好了。但是我在实际的代码实现(InsertTuple)中似乎并没有看到重组删除空间or覆盖删除空间,每次插入页满只是简单地再申请新的一页,不会再回头。也许是为了简化起见暂不实现这个吧。

                -

                不过改进方法也很简单,对每个表进行固定分配页(或者说提供一个数据量达到百分之几的时候扩容的机制),然后页面间组织成环形链表,这样就能充分覆盖删除空间,同时也兼顾一定性能了。

                -

                实现

                update的实现也不会很难,只需先删除原来的元组,再加个新元组即可。

                -

                delete

                delete的实现完全照搬update就行,没什么好说的。

                -

                index_scan

                -

                The IndexScanExecutor iterates over an index to retrieve RIDs for tuples. The operator then uses these RIDs to retrieve their tuples in the corresponding table. It then emits these tuples one at a time.

                -

                You can test your index scan executor by SELECT FROM <table> ORDER BY <index column>. We will explain why ORDER BY can be transformed into IndexScan in Task #3. 哦吼,也就是说order-by会被翻译为index scan?那order-by的关键字如果不存在索引会怎么样,现建吗

                -

                BusTub only supports indexes with a single, unique integer column. Our test cases will not contain duplicate keys. The type of the index object in the plan will always be BPlusTreeIndexForTwoIntegerColumn in this project. You can safely cast it and store it in the executor object:

                -
                using BPlusTreeIndexForTwoIntegerColumn = BPlusTreeIndex<IntegerKeyType, IntegerValueType, IntegerComparatorType>;

                tree_ = dynamic_cast<BPlusTreeIndexForTwoIntegerColumn *>(index_info_->index_.get())
                - -

                但我看测试里怎么好像有两个键的index?

                -

                You can then construct an index iterator from the index object, scan through all the keys and tuple IDs, lookup the tuple from the table heap, and emit all tuples in order.

                -

                是的,project2的b+树实现确实只存了rid,然后我们通过rid就能知道实际的物理位置了

                -
                -

                一些想法

                索引实现

                通过b+树组织索引结构,索引结点中存的是RID,RID可以用来指示tuple的物理位置,于是我们通过RID就可以获取到tuple,从而减少了磁盘IO。RID结构如下:

                -
                // RID: Record Identifier
                // 高32位是pgid,低32位是slot num
                class RID {
                public:
                explicit RID(int64_t rid) : page_id_(static_cast<page_id_t>(rid >> 32)), slot_num_(static_cast<uint32_t>(rid)) {}

                inline auto Get() const -> int64_t { return (static_cast<int64_t>(page_id_)) << 32 | slot_num_; }

                private:
                page_id_t page_id_{INVALID_PAGE_ID};
                uint32_t slot_num_{0}; // logical offset from 0, 1...
                };
                - -

                并且,bustub保证了对于有索引的表,是不会有重复元组的,故而b+树实际上应该是一个稠密索引。

                -

                (毕竟这个情况似乎有点复杂……物理存储上应该是按插入顺序顺序存储的,故而重复元组可能不放在一起,而我们实现的b+树又不支持重复结点,所以就会g。如果想要支持重复元组,可能就需要从两个改变思路入手,要么是修改b+树支持重复索引结点,此时b+树依然为稠密索引;要么是修改为链式存储结构以支持重复元组放在一起,此时b+树为稀疏索引。)

                -
                c++知识

                非常非常崩溃,怎么保存索引尝试了很久都没做到:

                -
                // 这样不行……
                std::unique_ptr<BPlusTreeIndexIteratorForTwoIntegerColumn> iterator_;
                iterator_ = std::make_unique<BPlusTreeIndexIteratorForTwoIntegerColumn>(std::move(tree_->GetBeginIterator()));

                // 这样也不行……
                BPlusTreeIndexIteratorForTwoIntegerColumn iterator_;
                iterator_ = std::move(tree_->GetBeginIterator());
                +

                必须要在把资源输入到resp的stream前设置好,精确来说是调用sos.write前设置好,不然无法起作用。

                +

                猜测是因为可能resp会根据disposition方式的不同而自动决策write的方式。

                +
                @Override
                protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
                //获取要下载的资源名称
                String name = req.getParameter("filename");

                //获取路径
                String path = this.getServletContext().getRealPath("/img/"+name);
                //使用字节流
                FileInputStream fis = new FileInputStream(path);
                //输出数据

                resp.setContentType(this.getServletContext().getMimeType(path));
                resp.setHeader("content-disposition",
                "attachment;filename="+
                // 为了防止中文乱码,需要针对不同的浏览器来进行编码
                DownLoadUtils.getFileName(req.getHeader("user-agent"),name));

                //获取字节输出流
                ServletOutputStream sos = resp.getOutputStream();
                byte[] buff = new byte[1024];
                int len = 0;
                while((len = fis.read(buff))!=-1){
                sos.write(buff,0,len);
                }
                //释放资源
                fis.close();

                // resp.setContentType(this.getServletContext().getMimeType(path));
                // resp.setHeader("content-disposition","attachment;filename="+name);
                }
                -

                没办法,最终只能保存tree,iterator在next里动态获取了,我真是服了。等之后看完c++primer或者c++水平有所提升了再来解决这个问题吧。

                -

                实现

                难绷,本来以为这个index-scan应该是最简单的,毕竟只用调用现成索引接口,没想到居然写了最久,可能足足两三个小时。。。

                -

                首先的一个大难点就是如何保存迭代器了。在之前的seq-scan的时候,使用的是unique ptr,然而这里却不行会报一堆奇奇怪怪的错误(具体见一些想法-c++知识)。最后只能换一个思路,不保存迭代器而是保存next_key_了。然而又由于之前b+树的实现bug问题,导致对end iterator解引用是合法的,所以会产生各种奇奇怪怪的错误。解决了这个之后,之前写的insert、update、delete的更新索引部分又出了问题,rid和insert_key弄错了,诸如此类。

                -

                总之,解决了这一大堆小问题之后,才总算通过了index-scan的测试,真是令人南蚌。具体改了什么bug可以详情见b8d3ba546cfdea6fc576ad8d668322c87f6386c1这个commit。

                -

                同时,也跟上面的sequence scan一样,都需要对标识为deleted的元组进行跳过处理。

                -
                -

                这里我也是没想太多……事实上,index scan无需实时检测is_deleted字段并做处理,因为索引是会随着修改实时更新的,被删除的tuple不会在索引中。

                -
                -

                Task2 Aggregation & Join Executors

                -

                In this task you will add an aggregation executor, several join executors, and enable the optimizer to select between a nested loop join and hash join when planning a query.

                -

                You will complete your implementation in the following files:

                +

                会话

                会话:一次会话中包含多次请求和响应。

                  -
                • src/execution/aggregation_executor.cpp
                • -
                • src/execution/nested_loop_join_executor.cpp
                • -
                • src/execution/hash_join_executor.cpp
                • -
                • src/optimizer/nlj_as_hash_join.cpp
                • +
                • 一次会话:浏览器第一次给服务器资源发送请求,会话建立,直到有一方断开为止

                  +
                • +
                • 功能:请求之间本来是相互独立的。将多次请求组织在一次会话中,就可以让请求之间进行数据的共享。

                  +
                • +
                • 方式:

                  +
                    +
                  • 客户端会话技术 Cookie

                    +

                    把数据存进客户端

                    +
                  • +
                  • 服务器端会话技术 Session

                    +

                    把数据存进服务器端

                    +
                  -
                -

                aggregation

                -

                The AggregationPlanNode is used to support queries like the following:

                -
                EXPLAIN SELECT colA, MIN(colB) FROM __mock_table_1 GROUP BY colA;
                EXPLAIN SELECT COUNT(colA), mi(colB) FROM __mock_table_1;
                EXPLAIN SELECT colA, MIN(colB) FROM __mock_table_1 GROUP BY colA HAVING MAX(colB) > 10;
                EXPLAIN SELECT DISTINCT colA, colB FROM __mock_table_1;
                - -

                也即聚合函数和DISTINCT、GROUP这种。

                -
                -

                此处注意DINSTINCT也是通过aggregation实现的:

                -
                EXPLAIN SELECT DISTINCT colA, colB FROM __mock_table_1;
                === OPTIMIZER ===
                Agg { types=[], aggregates=[], group_by=[#0.0, #0.1] } | (__mock_table_1.colA:INTEGER, __mock_table_1.colB:INTEGER)
                MockScan { table=__mock_table_1 } | (__mock_table_1.colA:INTEGER, __mock_table_1.colB:INTEGER)
                +
              4. + +

                概念

                客户端会话技术,将数据保存到客户端

                +

                快速入门

                  +
                • 使用步骤:

                  +
                    +
                  1. 创建Cookie对象,绑定数据【为了从服务器端发送cookie给客户端】
                      +
                    • new Cookie(String name, String value)
                    • +
                    • 可以看到,Cookie其实就是一种name-value这样的键值对对象
                    • +
                    +
                  2. +
                  3. 发送Cookie对象【因为要发送给客户端,所以应该在response里存】
                      +
                    • response.addCookie(Cookie cookie)
                    • +
                    +
                  4. +
                  5. 获取Cookie,拿到数据【因为是来自客户端,所以要从request里要】
                      +
                    • Cookie[] request.getCookies()
                    • +
                    +
                  6. +
                  +
                • +
                • 代码

                  +
                  @WebServlet("/demo")
                  public class ServletDemo extends HttpServlet {
                  @Override
                  protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
                  response.addCookie(new Cookie("password","abc123"));
                  }

                  @Override
                  protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
                  this.doGet(request, response);
                  }
                  }
                  -
                  -

                  The aggregation executor computes an aggregation function for each group of input. 作用于每一组

                  -

                  It has exactly one child. The output schema consists of the group-by columns followed by the aggregation columns.

                  -
                  EXPLAIN SELECT colA, MIN(colB) FROM __mock_table_1 GROUP BY colA;
                  === OPTIMIZER ===
                  // types标志聚合的种类,aggregates标识聚合的目标,group_by单独用于表示是否有group
                  Agg { types=[min], aggregates=[#0.1], group_by=[#0.0] } | (__mock_table_1.colA:INTEGER, <unnamed>:INTEGER)
                  MockScan { table=__mock_table_1 } | (__mock_table_1.colA:INTEGER, __mock_table_1.colB:INTEGER)


                  EXPLAIN SELECT COUNT(colA), min(colB) FROM __mock_table_1
                  === OPTIMIZER === // 如果没有group,则其字段为空
                  Agg { types=[count, min], aggregates=[#0.0, #0.1], group_by=[] } | (<unnamed>:INTEGER, <unnamed>:INTEGER)
                  MockScan { table=__mock_table_1 } | (__mock_table_1.colA:INTEGER, __mock_table_1.colB:INTEGER)
                  +
                  @WebServlet("/demo2")
                  public class ServletDemo2 extends HttpServlet {
                  @Override
                  protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
                  System.out.println(request.getCookies());
                  }

                  @Override
                  protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
                  this.doGet(request, response);
                  }
                  }
                • +
                • 得到效果

                  +

                  运行服务器,首先访问/demo,然后在同一个浏览器再次访问/demo2,就可以在控制台看到输出。

                  +

                  这个过程发生了什么呢?

                  +

                  首先,访问/demo就相当于建立了会话。/demo的Servlet获取到请求之后,在response中将cookie填入。

                  +

                  保持浏览器窗口不变,会话也不变。

                  +

                  再次访问/demo2,cookie信息自动保存在request对象中。/demo2的Servlet获取到请求之后,在控制台中打印输出了cookie。

                  +
                • +
                +

                细节学习

                一次发送多个cookie

                你看它那个API叫add,就知道数据结构差不多是个list,所以多次add就行。

                +
                保存时间

                默认情况下,浏览器关闭则cookie就马上被销毁。

                +

                如果需要持久化存储:

                +
                cookie.setMaxAge(int seconds)
                -

                As discussed in class, a common strategy for implementing aggregation is to use a hash table, with the group-by columns as the key.

                -

                In this project, you may assume that the aggregation hash table fits in memory. This means that you do not need to implement a multi-stage, partition-based strategy, and the hash table does not need to be backed by buffer pool pages.

                -

                也就是说这里采取的是基于hashtable的实现而非基于归并排序的,并且为了简单起见将hash table保存在内存中,所以无需进行多趟划分扫描。

                -

                We provide a SimpleAggregationHashTable data structure that exposes an in-memory hash table (std::unordered_map) but with an interface designed for computing aggregations. This class also exposes an SimpleAggregationHashTable::Iterator type that can be used to iterate through the hash table. You will need to complete the CombineAggregateValues function for this class.

                - -
                -

                Note: The aggregation executor itself won’t need to handle the HAVING predicate. The planner will plan aggregations with a HAVING clause as an AggregationPlanNode followed by a FilterPlanNode.

                -
                -
                -

                Hint: In the context of a query plan, aggregations are pipeline breakers. This may influence the way that you use the AggregationExecutor::Init() and AggregationExecutor::Next() functions in your implementation. Carefully decide whether the build phase of the aggregation should be performed in AggregationExecutor::Init() or AggregationExecutor::Next().

                -
                -

                一些想法

                countstar

                值得注意的是,这里的实现将COUNT(*)COUNT(colum)区分开了:

                -
                enum class AggregationType { CountStarAggregate, CountAggregate };
                +

                参数:

                +
                  +
                1. 正数:将Cookie数据写到硬盘的文件中。持久化存储。并指定cookie存活时间,时间到后,cookie文件自动失效
                2. +
                3. 负数:默认值
                4. +
                5. 零:删除cookie信息
                6. +
                +
                中文问题

                在tomcat 8 之前 cookie中不能直接存储中文数据,需要将中文数据转码——一般采用URL编码(%E3)

                +

                在tomcat 8 之后,cookie支持中文数据。

                +
                获取范围
                  +
                1. 假设在一个tomcat服务器中,部署了多个web项目,那么在这些web项目中cookie能不能共享?

                  +
                    +
                  • 默认情况下cookie不能共享

                    +
                  • +
                  • 共享方法:

                    +

                    setPath(String path):设置cookie的获取范围。默认情况下,设置当前的虚拟目录

                    +

                    如果要共享,则可以将path设置为”/“

                    +
                  • +
                  +
                2. +
                3. 不同的tomcat服务器间cookie共享问题?

                  +

                  比如说:

                  +image-20230221225514567 -

                  因为这两者似乎语义上是有区别的,大概体现为以下几点:

                  +
                    +
                  • setDomain(String path):如果设置一级域名相同,那么多个服务器之间cookie可以共享

                    +

                    setDomain(".baidu.com"),那么tieba.baidu.com和news.baidu.com中cookie可以共享)

                    +
                  • +
                  +
                4. +
                +

                作用和特点

                特点:

                  -
                1. 当没有结果时,CountStar返回0,Count返回integer_null
                2. -
                3. CountStar只记录行数,不管值是否为空;Count只记录所要求的列非空的那些行数
                4. +
                5. cookie存储数据在客户端浏览器

                  +

                  因而它相对不安全

                  +
                6. +
                7. 浏览器对于单个cookie 的大小有限制(4kb) 以及 对同一个域名下的总cookie数量也有限制(20个)

                  +
                -
                hash aggregation

                关于hashtable实现聚合的相关原理及相关示例,具体可见 这篇文章。感觉这系列文章都写得挺好的,如对TiDB有兴趣可以细看。

                -
                -

                在 SQL 中,聚合操作对一组值执行计算,并返回单个值。TiDB 实现了 2 种聚合算法:Hash Aggregation 和 Stream Aggregation

                -

                在 Hash Aggregate 的计算过程中,我们需要维护一个 Hash 表,Hash 表的键为聚合计算的 Group-By值为聚合函数的中间结果 sumcount

                -

                计算过程中,只需要根据每行输入数据计算出键,在 Hash 表中找到对应值进行更新即可。输入数据输入完后,扫描 Hash 表并计算,便可以得到最终结果

                -
                -

                故而思路也是很清晰了。我们在aggregation的实现中要做的,就是把child executor逐行喂给hashtable,最后再遍历hashtable得到结果即可。故而,我们重点需要实现hashtable的InsertCombine函数和hashtable的iterator。

                -

                实现

                理解了hash-aggregation的算法原理后,代码逻辑方面就不算难了,其余最主要的难点应该是空值的处理。

                -

                总结一下,bustub对空值的处理大概有以下几个要点:

                +

                作用:

                  -
                1. 聚合函数对空值处理

                  -

                  COUNT(*):计入空值

                  -

                  COUNT/MAX/MIN/SUM(v1):跳过空值

                  +
                2. cookie一般用于存出少量的不太敏感的数据

                3. -
                4. 空值自身运算性质

                  -

                  任意运算若有一个操作数为空,那么结果也为空。

                  -

                  故而,当没有使用group by关键字的时候(也即hashtable的key为空),此时不能天真地传入一个空的AggregationKey,而应该给它随便塞某个值。不然的话,hashtable内部的比较函数在处理空值的时候恒返回false,会导致检索失败。

                  +
                5. 在不登录的情况下,完成服务器对客户端的身份识别

                  +

                  比如说,以不登录情况下对某个网页进行属性设置,你下次打开的时候属性设置依然在,这是因为你的属性设置的cookie在设置后被存入到你的电脑中,下次访问该网页发出请求,服务器端就能根据请求中cookie里的属性设置信息来做出响应了。

                6. -
                7. 空表情况处理

                  -

                  当表为空的时候,要求:

                  -
                  select COUNT(*), MAX(v1), COUNT(v1) from table_;
                  # 0 integer_null integer_null
                  select COUNT(*), MAX(v1), COUNT(v1) from table_ group by v2;
                  # no-output
                  - -

                  这个操作我着实不懂为什么。。。所以我最终代码只能面向测试用例:

                  -
                  if (!has_next && plan_->GetGroupBys().empty()) {
                  // 当表为空并且不使用聚合函数时,输出一个默认情况对
                  AggregateKey agg_key;
                  agg_key.group_bys_.push_back(Value(TypeId::INTEGER, 1));
                  aht_->InsertCombine(agg_key, MakeAggregateValue(nullptr));
                  }
                -

                NestedLoopJoin

                -

                The DBMS will use NestedLoopJoinPlanNode for all join operations, by default.

                -

                You will need to implement an inner join and left join for the NestedLoopJoinExecutor using the simple nested loop join algorithm from class.

                -

                The output schema of this operator is all columns from the left table followed by all columns from the right table.

                -

                For each tuple in the outer table, consider each tuple in the inner table and emit an output tuple if the join predicate is satisfied.

                -
                -

                也即嵌套循环实现的join,与在课上学的sort merge join一样,都是古法join实现。

                -

                nested join的实现相比之前的思路确实会复杂一些。我们需要学习如何迭代地调用Next来实现一次嵌套循环。思路大概是这样:

                -
                Init():
                Init left, right
                Move left to get current_left_tuple_
                Next():
                while (1):
                if (Move(right)) :;
                else:
                Move left
                Init right
                continue;
                if (checkPredict):
                break;
                +

                案例:记住上一次访问时间

                需求:
                1. 访问一个Servlet,如果是第一次访问,则提示:您好,欢迎您首次访问。
                2. 如果不是第一次访问,则提示:欢迎回来,您上次访问时间为:显示时间字符串

                +
                public class ServletDemo extends HttpServlet {
                @Override
                protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
                //获取流对象之前,设置流的默认编码:ISO-8859-1 设置为:GBK
                response.setCharacterEncoding("utf-8");

                //告诉浏览器,服务器发送的消息体数据的编码。建议浏览器使用该编码解码
                response.setHeader("content-type","text/html;charset=utf-8");
                if(request.getCookies() != null)
                for(Cookie c : request.getCookies()){
                if(c.getName().equals("isfirst")){
                response.getWriter()
                .write("<h1>欢迎回来,您上次访问的时间为<h1>"+c.getValue());
                break;
                }
                }
                else
                response.getWriter().write("<h1>你好!欢迎你!<h1>");

                SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd-HH:mm:ss");
                Date date1 = new Date();
                String currentTime = dateFormat.format(date1);

                response.addCookie(new Cookie("isfirst",currentTime));
                }

                @Override
                protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
                this.doGet(request, response);
                }
                }
                -

                然而其中有这几个细节需要进行处理:

                +

                Session

                概念

                服务器端会话技术,在一次会话的多次请求间共享数据,将数据保存在服务器端的对象中。HttpSession

                +

                应用场合

                比如说购物网站的购物车这种,就会存在session。想想也是(

                  -
                1. 左连接的实现

                  -

                  需要增加逻辑:当right遍历完之后,current_left_tuple_仍未被组装进结果过,此时需要帮其拼接上空right tuple。

                  +
                2. session用于存储一次会话的多次请求的数据,存在服务器端

                  +

                  比如说,当我们做重定向的时候,就可以选择用session共享数据(会话域)而非使用ServletContext(此范围过大)

                3. -
                4. 空表情况

                  -

                  这个分支中:

                  -
                  else:
                  Move left
                  Init right
                  continue;
                  - -

                  不能这样:

                  -
                  else:
                  Move left
                  Init right
                  Move right
                  - -

                  这是为了防止空表情况,使得Move right一直返回false,导致之后checkPredict报空指针异常。

                  +
                5. session可以存储任意类型,任意大小的数据

                6. -
                7. 测试要求left->Next()调用次数与right->Init()调用次数相同。

                  -
                  -

                  这是为了强制让NestedLoopJoin的实现不是Pipeline Break,从而导致它性能垃圾了

                  -
                  +
                8. session与Cookie的区别:

                  +
                    +
                  1. session存储数据在服务器端,Cookie在客户端
                  2. +
                  3. session没有数据大小限制,Cookie有
                  4. +
                  5. session数据安全,Cookie相对于不安全
                  6. +
                  +
                9. +
                +

                快速入门

                  +
                1. 获取HttpSession对象:
                  HttpSession session = request.getSession();

                  +
                2. +
                3. 使用HttpSession对象:

                  +
                    Object getAttribute(String name)  
                  void setAttribute(String name, Object value)
                  void removeAttribute(String name)

                  #### 原理

                  ![image-20230223102722575](./JavaWeb/image-20230223102722575.png)

                  实现依赖于Cookie

                  #### 细节

                  前面说到,当客户端和服务器端有任何一端关闭之后,会话结束,在这种情况下,session在客户端和服务器端的保留情况不同。

                  1. 当客户端关闭后,服务器不关闭,两次获取session是否为同一个?
                  * 默认情况下。不是。
                  * 如果需要相同,则可以创建Cookie,键为JSESSIONID,设置最大存活时间,让cookie持久化保存。
                  ```java
                  Cookie c = new Cookie("JSESSIONID",session.getId());
                  c.setMaxAge(60*60);
                  response.addCookie(c);
                4. +
                5. 客户端不关闭,服务器关闭后,两次获取的session是同一个吗?

                -

                HashJoin

                -

                The DBMS can use HashJoinPlanNode if a query contains a join with a conjunction of equi-conditions between two columns (equi-conditions are seperated by AND).

                -

                也就是说,当连接条件为一/多个列相等时,就可以用hash join。可以看到这是类似等值连接。

                -
                EXPLAIN SELECT * FROM __mock_table_1, __mock_table_3 WHERE colA = colE;
                EXPLAIN SELECT * FROM __mock_table_1 INNER JOIN __mock_table_3 ON colA = colE;
                EXPLAIN SELECT * FROM __mock_table_1 LEFT OUTER JOIN __mock_table_3 ON colA = colE;
                EXPLAIN SELECT * FROM test_1 t1, test_2 t2 WHERE t1.colA = t2.colA AND t1.colB = t2.colC;
                EXPLAIN SELECT * FROM test_1 t1 INNER JOIN test_2 t2 on t1.colA = t2.colA AND t2.colC = t1.colB;
                EXPLAIN SELECT * FROM test_1 t1 LEFT OUTER JOIN test_2 t2 on t2.colA = t1.colA AND t2.colC = t1.colB;
                - -

                You will need to implement the inner join and left join for HashJoinExecutor using the hash join algorithm from class.

                -

                The output schema of this operator is all columns from the left table followed by all columns from the right table.

                -

                As with aggregation, you may assume that the hash table used by the join fits entirely in memory.

                -

                Hint: Your implementation should correctly handle the case where multiple tuples have hash collisions (on either side of the join). 必须正确处理哈希冲突的情况

                -

                Hint: You will want to make use of the join key accessors functions GetLeftJoinKey() and GetRightJoinKey() in the HashJoinPlanNode to construct the join keys for the left and right sides of the join, respectively.

                -

                Hint: You will need a way to hash a tuple with multiple attributes in order to construct a unique key. As a starting point, take a look at how the SimpleAggregationHashTable in the AggregationExecutor implements this functionality. 可以参考 SimpleAggregationHashTable的实现

                -

                Hint: As with aggregation, the build side of a hash join is a pipeline breaker. You should again consider whether the build phase of the hash join should be performed in HashJoinExecutor::Init() or HashJoinExecutor::Next().

                -
                -

                具体什么是hash join,可以参考 这篇文章

                -

                img

                -

                其大概思路也很简单,hash table就是一个map<key, vector<value>>这样的数据结构,然后将两个输入的关系选举出一个小表作为Build(建立hash table),另一个作为Probe(扫描,并根据hash table->second进行迭代组合)。它其实就是一个精确了范围的nested loop join的变种,将nested里层的针对整个关系的大循环缩小为针对hash table一个bucket的小循环。

                -

                具体到这里,思路可以是这样的。首先为了简单起见,我们就不进行选举小表的判断了,固定将right child作为Build,left child作为Probe。建表的话,我们就简单粗暴地遍历right table,然后以right_key_expressions_为keyTuple为value直接建表(反正也是in-memory即可。。。)。然后之后,就仿照之前思路即可。

                -

                Optimizing NestedLoopJoin to HashJoin

                -

                Hash joins usually yield better performance than nested loop joins. You should modify the optimizer to transform a NestedLoopJoinPlanNode into a HashJoinPlanNode when it is possible to use a hash join.

                -

                Specifically, the hash join algorithm can be used when a join predicate is a conjunction of equi-conditions between two columns. For the purpose of this project, handling a single equi-condition, and also two equi-conditions connected by AND, will earn full credit.

                -

                Consider the following example:

                -
                bustub> EXPLAIN (o) SELECT * FROM test_1 t1, test_2 t2 WHERE t1.colA = t2.colA AND t1.colB = t2.colC;
                - -

                Without applying the NLJAsHashJoin optimizer rule, the plan may look like:

                -
                NestedLoopJoin { type=Inner, predicate=((#0.0=#1.0)and(#0.1=#1.2)) } 
                SeqScan { table=test_1 }
                SeqScan { table=test_2 }
                - -

                After applying the NLJAsHashJoin optimizer rule, the left and right join key expressions will be extracted from the single join predicate in the NestedLoopJoinPlanNode. The resulting plan will look like:

                -
                HashJoin { type=Inner, left_key=[#0.0, #0.1], right_key=[#0.0, #0.2] } 
                SeqScan { table=test_1 }
                SeqScan { table=test_2 }
                - -

                Note: Please check the Optimizer Rule Implementation Guide section for details on implementing an optimizer rule.

                -

                Hint: Make sure to check which table the column belongs to for each side of the equi-condition. It is possible that the column from outer table is on the right side of the equi-condition. You may find ColumnValueExpression::GetTupleIdx helpful.

                -

                Hint: The order to apply optimizer rules matters. For example, you want to optimize NestedLoopJoin into HashJoin after filters and NestedLoopJoin have merged. 这个感觉可能意思就是说优化规则的优先级之类的吧,这里用了个例子说hash join的优先级一般得在filter和nested loop join合并了之后。

                -

                Hint: At this point, you should pass SQLLogicTests - #14 to #15.

                -
                -

                一些想法

                bustub optimizer
                -

                The BusTub optimizer is a rule-based optimizer. Most optimizer rules construct optimized plans in a bottom-up way(自底向上). Because the query plan has this tree structure, before applying the optimizer rules to the current plan node, you want to first recursively apply the rules to its children. 基于规则的优化器,规则都是对语法树自底向上实施。感觉跟课内学的差不多。

                -

                In the public BusTub repository, we already provide the implementation of several optimizer rules. Please take a look at them as reference.

                -
                -

                在课程中学到的语法优化,应该也是基于规则的优化,具体见下图及之后列出的无穷无尽个定理:

                -

                image

                -

                image-20231227155806019

                -

                (本图新增了一条规则:选择+嵌套笛卡尔积=嵌套连接)

                -

                查看目录src/optimizer/,我们可以看到:

                -
                $ tree ../src/optimizer/
                ../src/optimizer/
                ├── eliminate_true_filter.cpp # 消除恒真选择
                ├── merge_filter_nlj.cpp # 合并选择和嵌套连接
                ├── merge_filter_scan.cpp # 合并选择和scan
                ├── merge_projection.cpp # 合并多个投影
                ├── nlj_as_hash_join.cpp # 嵌套连接->hash连接
                ├── nlj_as_index_join.cpp # 嵌套连接->index连接
                ├── optimizer.cpp
                ├── optimizer_custom_rules.cpp
                ├── optimizer_internal.cpp
                ├── order_by_index_scan.cpp
                └── sort_limit_as_topn.cpp # 针对 top-N queries 进行优化
                - -

                在本小节任务中,我们需要做的,就是参照其他的规则来实现nlj_as_hash_join。但在此之前,我们不妨先研究一下它语法优化的总体架构。

                -
                auto Optimizer::OptimizeCustom(const AbstractPlanNodeRef &plan) -> AbstractPlanNodeRef {
                auto p = plan;
                p = OptimizeMergeProjection(p); // 首先合并影响相同的投影
                p = OptimizeMergeFilterNLJ(p); // 然后合并选择和嵌套连接
                p = OptimizeNLJAsHashJoin(p); // 然后把嵌套连接改为hash join
                p = OptimizeOrderByAsIndexScan(p); // 根据索引进行查找
                p = OptimizeSortLimitAsTopN(p); // 针对 top-N queries 进行优化
                return p;
                }
                - -

                可以看到,它的实际原理很简单,就是按照这样的优先级顺序对语法树运用规则进行优化。

                -
                merge filter nlj

                OptimizeMergeFilterNLJ为例,我们可以研究一下它的整体架构:

                -
                auto Optimizer::OptimizeMergeFilterNLJ(const AbstractPlanNodeRef &plan) -> AbstractPlanNodeRef {
                // 首先自底向上地对其所有子节点进行优化,采用DFS
                std::vector<AbstractPlanNodeRef> children;
                for (const auto &child : plan->GetChildren()) {
                children.emplace_back(OptimizeMergeFilterNLJ(child));
                }

                auto optimized_plan = plan->CloneWithChildren(std::move(children));
                // 仅当当前结点为filter,并且其唯一子节点为nlj时,才进行重写优化
                if (optimized_plan->GetType() == PlanType::Filter) {
                const auto &filter_plan = dynamic_cast<const FilterPlanNode &>(*optimized_plan);
                const auto &child_plan = optimized_plan->children_[0]; // Has exactly one child
                if (child_plan->GetType() == PlanType::NestedLoopJoin) {
                const auto &nlj_plan = dynamic_cast<const NestedLoopJoinPlanNode &>(*child_plan);
                // 这里可能简单起见,仅当nlj为纯纯的笛卡尔积时,才会进行合并
                // 所以看起来就无法处理多个连续的选择的情况,或许在planner阶段规避了这种情况?
                if (IsPredicateTrue(nlj_plan.Predicate())) {
                // 将该filter+nlj结点重写为一个新的连接结点
                return std::make_shared<NestedLoopJoinPlanNode>(
                filter_plan.output_schema_, nlj_plan.GetLeftPlan(), nlj_plan.GetRightPlan(),
                RewriteExpressionForJoin(filter_plan.GetPredicate(),
                nlj_plan.GetLeftPlan()->OutputSchema().GetColumnCount(), nlj_plan.GetRightPlan()->OutputSchema().GetColumnCount()), nlj_plan.GetJoinType());
                }
                }
                }
                return optimized_plan;
                }
                - -

                可见,对语法树运用该merge filter nlj规则是采用自底向上的顺序,并且仅合并那些filter-笛卡尔积的结点。那么接下来,我们可以具体关注RewriteExpressionForJoin的实现。

                -

                首先,我们需要明确bustub中对expression的抽象。以#0.0=#1.0为例,expression的结构树如下所示:

                -

                image-20240120104722170

                -

                每个叶子结点都是一个基本的expression类型,如column value、constant value等等等,整个子树构成一个其他expression类型,如comparation expr、arithmetic expr等等等。

                -

                在未优化前,我们是先做笛卡尔积,再做选择。故而,假设t1有2列,t2有2列,选择条件为t1.col1 = t2.col4,在未优化前,filter结点的expr将为:#0.0=#0.3(两表经过笛卡尔积合在一起了)。故而,在RewriteExpressionForJoin函数中,我们需要根据t1表和t2表分别的列数,将#0.0=#0.3这样的表达式转化为#0.0=#1.1这样的表达式(其实也就是只用处理所有类型为colum expr的叶结点即可)。而由于expression是递归结构,所以我们需要先针对其所有子节点进行处理。故而,RewriteExpressionForJoin的实现如下:

                -
                auto Optimizer::RewriteExpressionForJoin(const AbstractExpressionRef &expr, size_t left_column_cnt, size_t right_column_cnt) -> AbstractExpressionRef {
                // 首先自底向上地对其所有子节点进行优化,采用DFS
                std::vector<AbstractExpressionRef> children;
                for (const auto &child : expr->GetChildren()) {
                children.emplace_back(RewriteExpressionForJoin(child, left_column_cnt, right_column_cnt));
                }
                // 仅对那些类型为column expr的叶子结点进行处理
                if (const auto *column_value_expr = dynamic_cast<const ColumnValueExpression *>(expr.get()); column_value_expr != nullptr) {
                // #0.1, "0"为tuple_idx,"1"为col_idx
                // 此时tuple_idx一定是0,因为filter结点只有一个子节点
                BUSTUB_ENSURE(column_value_expr->GetTupleIdx() == 0, "tuple_idx cannot be value other than 0 before this stage.")
                auto col_idx = column_value_expr->GetColIdx();
                if (col_idx < left_column_cnt) {
                return std::make_shared<ColumnValueExpression>(0, col_idx, column_value_expr->GetReturnType()); // 替换为#0.X
                }
                if (col_idx >= left_column_cnt && col_idx < left_column_cnt + right_column_cnt) {
                return std::make_shared<ColumnValueExpression>(1, col_idx - left_column_cnt, column_value_expr->GetReturnType()); // 替换为#1.X
                }
                throw bustub::Exception("col_idx not in range");
                }

                // xiunian: do nothing if the filter contains no column value expression
                return expr->CloneWithChildren(children);
                }
                - -

                实现

                -

                Specifically, the hash join algorithm can be used when a join predicate is a conjunction of equi-conditions between two columns. For the purpose of this project, handling a single equi-condition, and also two equi-conditions connected by AND, will earn full credit.

                -
                -

                看完了merge filter nlj的实现之后,本次任务的实现就变得不那么困难了。

                -

                当一个nlj的predicate条件是一堆使用AND连接的“=”expr,我们就可以将该nlj转化为hash join。而OptimizeNLJAsHashJoin作用于OptimizeMergeFilterNLJ之后,故而,我们可以直接对所有的nlj结点进行判定重写。

                -

                具体来说,我们可以首先实现一个函数CheckIfEquiConjunction,给定expr结构树输入,判断其是否只由AND、”=”、”column expr”构成。这个过程还需要做一件事,就是分离出hash join所需要的key expression,如nlj的连接条件为#0.1=#1.2 AND #1.1=#0.2,则最后形成的hash join为left_key_expr=[#0.1, #0.2], right_key_expr=[#1.2, #1.1]

                -

                然后,在OptimizeNLJAsHashJoin函数主体中,我们只需遍历语法树的所有结点,然后对其进行判定,符合条件则将其转化为hash join即可。

                -

                Task #3 - Sort + Limit Executors and Top-N Optimization

                -

                You will finally implement a few more common executors, completing your implementation in the following files:

                  -
                • src/execution/sort_executor.cpp
                • -
                • src/execution/limit_executor.cpp
                • -
                • src/execution/topn_executor.cpp
                • -
                • src/optimizer/sort_limit_as_topn.cpp
                • +
                • 不是同一个,但是要确保数据不丢失。tomcat自动(IDEA不会活化)完成以下工作
                    +
                  • session的钝化:(序列化)
                        * 在服务器正常关闭之前,将session对象序列化到硬盘上
                    +
                    +
                  -

                  You must implement the IndexScanExecutor in Task #1 before starting this task. If there is an index over a table, the query processing layer will automatically pick it for sorting. In other cases, you will need a special sort executor to do this.

                  -

                  For all order by clauses, we assume every sort key will only appear once. You do not need to worry about ties in sorting.

                  -
                -

                Sort

                -

                If a query’s ORDER BY attributes don’t match the keys of an index, BusTub will produce a SortPlanNode for queries such as:

                -
                EXPLAIN SELECT * FROM __mock_table_1 ORDER BY colA ASC, colB DESC;
                - -

                如果要求排序的key不是index key,就会用到这个sort executor。

                -

                This plan node has the same output scheme as its input schema. You can extract sort keys from order_bys, and then use std::sort with a custom comparator to sort the child node’s tuples. You may assume that all entries in a table will fit entirely in memory.

                -

                If the query does not include a sort direction (i.e., ASC, DESC), then the sort mode will be default (which is ASC).

                -
                -

                一些想法

                comparator实现
                -

                我有一个类Tuple,另一个类Executor。我想实现一个Tuple的比较函数,但需要用到类Executor的成员变量,那么我该怎么写一个可以用于std::sort的cmp函数

                -
                -

                最终给出的提示是这样的,实现一个函数对象

                -
                struct CompareTuplesByOrder {
                Schema schema_;
                // add any new member

                CompareTuplesByOrder(Schema schema, const std::vector<std::pair<OrderByType, AbstractExpressionRef>>& order_by) : schema_(schema) { }

                // override the "()" operator
                bool operator()(const Tuple &t1, const Tuple &t2) const {
                // do any logic
                }
                };

                // use in sort
                std::sort(tuples_.begin(), tuples_.end(), CompareTuplesByOrder(GetOutputSchema(), plan_->GetOrderBy()));
                + + +
                        * 具体是会放在这里:
                 
                -

                可以看到,其本质是通过重载”()”运算符来实现的,感觉是一个很有意思的trick。

                -

                实现

                它提示的实现思路很简单,就是大概从sort plan node获取所有key,然后用std:sort即可,默认升序,并且所有entry都是in-memory的。

                -

                有一点值得注意的是,在sql语言中,排序是可以指定多个关键词+不同顺序(关键词出现顺序表明排序优先级)的,如order by col1 ASC, col3 DESC。所以我们需要在comparator实现中按照优先级(也即order_by_数组顺序)一步步比较。

                -

                比较难的地方大概还是c++知识,也即如何为std:sort实现一个较为复杂的comparator。具体操作可见一些想法-comparator实现

                -

                Limit

                -

                The LimitPlanNode specifies the number of tuples that query will generate. Consider the following example:

                -
                EXPLAIN SELECT * FROM __mock_table_1 LIMIT 10;
                + ![image-20230223104447097](./JavaWeb/image-20230223104447097.png) + +* session的活化:(反序列化 + * 在服务器启动后,将session文件转化为内存中的session对象即可。 -

                The LimitExecutor constrains the number of output tuples from its child executor. If the number of tuples produced by its child executor is less than the limit specified in the plan node, this executor has no effect and yields all of the tuples that it receives.

                -

                This plan node has the same output scheme as its input schema. You do not need to support offsets.

                +我想,cookie应该在这点上不会像session这么做,因为cookie本质上是保存在客户端的数据,按理来说服务器端把cookie发出去之后就可以销毁了,在服务器序列化一点意义都没有。 +
                +
                  +
                1. 销毁时间

                  +
                    +
                  1. 服务器关闭

                    +
                  2. +
                  3. session对象调用invalidate() 。

                    +
                  4. +
                  5. session默认失效时间 30分钟
                    选择性配置修改

                    +

                    可以在每个项目的子配置文件(如下图)或者总的项目的父配置文件apache-tomcat-8.5.83\conf\web.xml中配置

                    +

                    image-20230223105053170

                    +
                    <session-config>
                    <session-timeout>30</session-timeout>
                    </session-config>
                  6. +
                  +
                2. +
                +

                案例

                +

                需求:

                +
                  +
                1. 访问带有验证码的登录页面login.jsp
                2. +
                3. 用户输入用户名,密码以及验证码。
                    +
                  • 如果用户名和密码输入有误,跳转登录页面,提示:用户名或密码错误
                  • +
                  • 如果验证码输入有误,跳转登录页面,提示:验证码错误
                  • +
                  • 如果全部输入正确,则跳转到主页success.jsp,显示:用户名,欢迎您
                  • +
                  +
                4. +
                -

                挺简单的,就是限制输出的数量,没什么好说的。

                -

                Top-N Optimization Rule

                -

                Finally, you should modify BusTub’s optimizer to efficiently support top-N queries. (These were called top-K queries in class.) Consider the following:

                -
                EXPLAIN SELECT * FROM __mock_table_1 ORDER BY colA LIMIT 10;
                +
                初见思路

                我们可以在服务器端使用session存储password和username的map,存储验证码图片编号和图片的map,然后用cookie携带验证码图片编号,在req中存储用户名和密码。

                +
                正确思路

                感觉我上面的思路是没有充分利用到session的性质,仅仅把它作为在服务器端存储数据的工具,

                +

                “在服务器端存储password和username的map,存储验证码图片编号和图片的map,然后用cookie携带验证码图片编号,在req中存储用户名和密码。”

                +

                这样也依然成立,跟session没半毛钱关系。我们可以这样使用session:

                +
                  +
                1. 在服务器端存储password和username的map,存储验证码图片编号和图片的map
                2. +
                3. 当会话建立,由于没有cookie,故而session第一次创建。我们在session内写入验证码对应的编号,把图片通过response发送给客户端。
                4. +
                5. 会话端输入图片验证码后,按下submit按键,验证码存入request域,向服务器端发送请求
                6. +
                7. 服务器端Servlet从请求中get到验证码,然后在session中get到当前验证码的图片编号,向一开始存储的map查询数据,这样就能验证验证码是否正确了
                8. +
                +

                那么在这里为什么不用Cookie而使用session呢?大概是因为cookie不安全罢(慌乱)

                +
                代码
                jsp
                <html lang="en">
                <head>
                <meta charset="UTF-8">
                <title>Title</title>
                </head>
                <body>
                <form action="/practice_war/loginServlet" method="post">
                name: <input type="text" name="username" id="username" placeholder="请输入用户名">
                password: <input type="password" name="password" id="password" placeholder="请输入密码">
                verifycode:<input type="text" name="verifycode" id="verifycode" placeholder="请输入验证码">

                <img id="img" src="/practice_war/check"/>

                <input type="submit" value="submit">
                </form>
                <script>
                window.onload = function(){
                document.getElementById("img").onclick = function(){
                this.src = "/practice_war/check?"+new Date().getTime();
                }
                }

                </script>
                </body>
                </html>
                -

                By default, BusTub will plan this query as a SortPlanNode followed by a LimitPlanNode. This is inefficient because a heap can be used to keep track of the smallest 10 elements far more efficiently than sorting the entire table.

                -

                Implement the TopNExecutor and modify the optimizer to use it for queries containing ORDER BY and LIMIT clauses.

                -

                An example of the optimized plan of this query:

                -
                TopN { n=10, order_bys=[(Default, #0.0)]} | (__mock_table_1.colA:INTEGER, __mock_table_1.colB:INTEGER)
                MockScan { table=__mock_table_1 } | (__mock_table_1.colA:INTEGER, __mock_table_1.colB:INTEGER)
                +
                checkcode
                @WebServlet("/check")
                public class ServletDemo extends HttpServlet {
                @Override
                protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
                //验证码图片大小
                final int width = 100;
                final int height = 50;

                /* 绘制验证码 */
                BufferedImage image = new BufferedImage(width,height,BufferedImage.TYPE_INT_RGB);
                Graphics pen = image.getGraphics();
                //绘制背景
                pen.setColor(new Color((int)(Math.random()*255),(int)(Math.random()*255),(int)(Math.random()*255)));
                pen.fillRect(0,0,width,height);
                //绘制边框
                pen.setColor(new Color((int)(Math.random()*255),(int)(Math.random()*255),(int)(Math.random()*255)));
                pen.drawRect(0,0,width-1,height-1);
                //随机填充字母数字
                String source = "QWERTYUIOPASDFGHJKLZXCVBNMqwertyuiopasdfghjklzxcvbnm1234567890";

                StringBuilder verifyAnswer = new StringBuilder();

                for (int i = 1; i <= 4; i++){
                int index = (int)(Math.random()*source.length());
                verifyAnswer = verifyAnswer.append(source.charAt(index));
                pen.drawString(source.substring(index,index+1),20*i,27);
                }
                //画干扰色线
                pen.setColor(new Color((int)(Math.random()*255),(int)(Math.random()*255),(int)(Math.random()*255)));
                for (int i = 0; i < 5; i++){
                pen.drawLine((int)(Math.random()*width),(int)(Math.random()*height),(int)(Math.random()*width),(int)(Math.random()*height));
                }

                request.getSession().setAttribute("verifycode",verifyAnswer.toString());
                System.out.println("verify:"+verifyAnswer.toString());
                //将图片输出
                ImageIO.write(image,"jpg",response.getOutputStream());
                }

                @Override
                protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
                this.doGet(request, response);
                }
                }
                -

                Hint: See OptimizeSortLimitAsTopN for more information, and check the Optimizer Rule Implementation Guide for details on implementing an optimizer rule.

                -

                Note: At this point, your implementation should pass SQLLogicTests #16 to #19. Integration-test-2 requires you to use release mode to run.

                -
                -

                感觉标准做法应该是使用快速排序的partion思想。但是这里的话,我打算使用另一个实现更简单的思路,也即维护一个元素个数为limit_的有序set,每次插入元素同其最小值比较即可,这样的时间复杂度为O(nlogk)。当k不会太大的时候,我觉得这样应该还是会比快排要快些的。

                -

                小插曲

                本来写到这准备开开心心提交了,突然发现自己的版本似乎跟仓库最新不大一样。rebase了感觉一下午(最后甚至还找了个以前写的小bug……),最后才终于提交完获得了full score……

                -

                image-20240121200829707

                -

                bustub仓库中的每个课程版本都是有这样的小tag了,一开始没发现直接大力出奇迹rebase最新,结果整了半天人麻了。。。

                -

                image-20240121200726718

                -

                Leaderboard Task

                TODO

                -]]> - - - Java并发编程实战 - /2022/11/06/Java%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B%E5%AE%9E%E6%88%98/ - -

                idea 替换注释正则表达式/\*{1,2}[\s\S]*?\*/

                - -

                第一章 简介

                线程的作用

                +
                login
                @WebServlet("/loginServlet")
                public class loginServlet extends HttpServlet {
                @Override
                protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
                HttpSession session = request.getSession();
                String verifycode = request.getParameter("verifycode");
                System.out.println(flag);

                String ans = session.getAttribute("verifycode");
                if (ans == null||!ans.equals(verifycode)){
                session.removeAttribute("verifycode");
                // 重定向到错误界面
                request.getRequestDispatcher("/fail_code").forward(request,response);
                return;
                }
                session.removeAttribute("verifycode");

                // 进行密码账号匹配处理
                String username = request.getParameter("username");
                String password = request.getParameter("password");
                System.out.println(username+" "+password);
                if(UserDao.login(new User(username,password))){
                // 成功界面
                request.setAttribute("uname",username);
                request.getRequestDispatcher("/success").forward(request,response);
                }else{
                // 失败界面
                request.getRequestDispatcher("/fail").forward(request,response);
                }
                }

                @Override
                protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
                this.doGet(request, response);
                }
                }
                -

                这一段写得很好,非常易懂地概括了什么是“多线程把异步转化为同步”:把异步中的不同操作分解为一个个独立的同类型操作,然后只需实现这些相较简单的同类型操作,再异步地把它们调度起来就行。线程正是把复杂的异步工作流分解成了一组简单的同步工作流

                -

                线程无处不在

                如果一个模块在代码中引入了并发性,那么它所有的代码路径【调用链】都得是并发的。

                - +

                老师的写法是将错误信息直接写在原登录界面,和我的略有不同:

                +
                // in loginServlet
                if (!session.getAttribute("verifycode").equals(verifycode)){
                request.setAttribute("message","checkcode_fail");
                request.getRequestDispatcher("/login.jsp").forward(request,response);
                return;
                }
                -

                最后一句话很关键,“把线程安全性封装在共享对象内部”

                - +
                // in login.jsp
                <%
                String message = (String) request.getAttribute("message");
                if(message != null){
                if(message.equals("checkcode_fail")){
                out.write("验证码错误!");
                }else if(message.equals("pass_fail")){
                out.write("用户名或密码错误!");
                }
                }
                %>
                - +

                以及success.jsp

                +

                image-20230302233110661

                +
                成功/两个失败

                仅以成功为例

                +
                @WebServlet(value = "/success")
                public class SuccessServlet extends HttpServlet {
                @Override
                protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
                doPost(req,resp);
                }

                @Override
                protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
                resp.setContentType("text/html;charset=utf-8");
                resp.getWriter().write("登录成功!"+req.getAttribute("uname")+",欢迎您");
                }
                }
                - +

                JSP

                +

                现在都用 Thymeleaf ,更符合 MVC 的执行过程,也没有 JSP 这种耦合杂乱的页面代码,但是模板引擎的思路大致相同,还是可以看一看的

                +
                +

                改动之后无需重启服务器,刷新界面即可。

                +
                +

                关于热更新的机制可以看看这篇文章,水平有限还看不懂就先放在这了:

                +

                JSP热部署的实现原理[通俗易懂]

                +
                +

                概念

                JSP(Java Server Pages) Java服务器端页面,用于简化书写

                +

                可以理解为:一个特殊的页面,其中既可以定义html标签,又可以定义java代码

                +

                比如说,上一个案例的Servlet代码就可以直接写入到JSP中,而且response和request这些对象可以直接用

                +
                <%@ page import="java.text.SimpleDateFormat" %>
                <%@ page import="java.util.Date" %>
                <html>
                <body>
                <h2>Hello World!</h2>
                <%
                //获取流对象之前,设置流的默认编码:ISO-8859-1 设置为:GBK
                response.setCharacterEncoding("utf-8");

                //告诉浏览器,服务器发送的消息体数据的编码。建议浏览器使用该编码解码
                response.setHeader("content-type","text/html;charset=utf-8");
                if(request.getCookies() != null)
                for(Cookie c : request.getCookies()){
                if(c.getName().equals("isfirst")){
                //response.getWriter().write("<h1>欢迎回来,您上次访问的时间为<h1>"+c.getValue());
                response.getWriter().write("<h1>Welcome!The last time you visit is <h1>"+c.getValue());
                //System.out.println("欢迎回来,您上次访问的时间为"+c.getValue());
                break;
                }
                }
                else
                response.getWriter().write("<h1>Hello!Welcome!<h1>");

                SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd-HH:mm:ss");
                Date date1 = new Date();
                String currentTime = dateFormat.format(date1);

                //response.getWriter().write("<h1>你好!欢迎你!<h1>");
                //System.out.println("你好!欢迎你!");
                response.addCookie(new Cookie("isfirst",currentTime));
                %>
                </body>
                </html>
                - +

                最终效果:

                +image-20230222230929893 -

                这个不同于上面的方法:将共享对象包装为线程安全的。它是要求了这些共享对象仅能在事件线程中运行,这样来保证线程安全性。

                -

                第二章 线程安全性

                **线程安全的核心就是对状态的访问和操作进行管理**,特别是对那些共享(shared)的、可变(mutable)的状态。关于本句话,其中几点将在下面一一细说:

                +

                原理

                JSP本质上是Servlet

                +

                image-20230222233533332

                +

                JSP的脚本

                JSP定义Java代码的方式

                  -
                1. 状态

                  -

                  状态是指存储在状态变量里的数据,如成员变量、静态域等等等。对象的状态还可能包括其他依赖对象的域,如HashMap的状态包括Map.Entry的状态。

                  +
                2. <% 代码 %>

                  +

                  定义的java代码,在service方法中。service方法中可以定义什么,该脚本中就可以定义什么。

                  +

                  也即最后会构成Servlet体

                3. -
                4. 共享和可变

                  -

                  共享意味着变量可以由多个线程同时访问,可变意味着变量的值在生命周期可发生变化

                  +
                5. <%! 代码 %>

                  +

                  定义的java代码,在jsp转换后的java类的成员位置。可以是成员变量,或者是成员方法。

                  +

                  注:最好不要在Servlet中定义成员变量,否则容易引发线程安全问题。

                6. -
                7. 是否需要线程安全

                  -

                  取决于它是否被多个线程访问。比如说,如果一个局部变量仅在某个函数体中同时只被一个线程访问,那么它就不需要线程安全,不需要同步机制。

                  +
                8. <%= 代码 %>

                  +

                  定义的java代码,会输出到页面上。输出语句中可以定义什么,该脚本中就可以定义什么。

                  +

                  image-20230223000057595

                  +

                  比如说可以用来输出某个变量的值。注意这东西由于本质上是写在Servlet的service方法中的,因而当成员变量和service方法的局部变量重名,会依据就近原则优先使用局部变量的值。

                - - - +

                指令

                也就是jsp开头那些东西,比如说这个:

                +
                <%@ page contentType="text/html;charset=UTF-8" language="java" %>
                -

                什么是线程安全

                概念

                +

                用来配置jsp的资源页面信息

                +
                  +
                • 分类:
                    +
                  1. page : 配置JSP页面的
                      +
                    • contentType:等同于response.setContentType()
                      contentType="text/html;charset=UTF-8"
                      +
                        +
                      1. 设置响应体的mime类型以及字符集
                      2. +
                      3. 设置当前jsp页面的编码(只能是高级的IDE才能生效,如果使用低级工具,则需要设置pageEncoding属性设置当前页面的字符集)
                      4. +
                      +
                    • +
                    • import:导包
                    • +
                    • errorPage:当前页面发生异常后,会自动跳转到指定的错误页面
                    • +
                    • isErrorPage:标识当前页面是否是错误页面。
                        +
                      • true:是,可以使用内置对象exception【用来获取异常名称/信息等】
                      • +
                      • false:否。默认值。不可以使用内置对象exception
                      • +
                      +
                    • +
                    +
                  2. +
                  3. include : 页面包含的。导入页面的资源文件,可以引入其它的jsp或者html,引入之后就会展示同样的内容。
                      +
                    • <%@include file=”top.jsp”%>
                    • +
                    +
                  4. +
                  5. taglib导入资源。用来导入标签库
                        * <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
                    +        * prefix:前缀,自定义的。之后就可以用`<c:XXX>`了。相当于什么std::。
                    +
                    +
                  6. +
                  +
                • +
                +

                中文乱码

                但是注意一点

                +
                //获取流对象之前,设置流的默认编码:ISO-8859-1 设置为:GBK
                response.setCharacterEncoding("utf-8");

                //告诉浏览器,服务器发送的消息体数据的编码。建议浏览器使用该编码解码
                response.setHeader("content-type","text/html;charset=utf-8");
                - - - - -

                注意,线程安全不会违背不变性和后验条件,这句话在后面会用到。

                -

                无状态对象一定是线程安全的

                在此举例一个无状态线程:

                - - -
                @ThreadSafe
                public class StatelessFactorize implements Servlet{
                public void service(ServletRequest req,ServletResponse resp){
                BigInteger i = extractFromRequest(req);
                BigInteger[] factors = factor(i);
                encodeIntoResponse(resp,factors);
                }
                }
                - - - - - -

                无状态对象一定是线程安全的

                -

                原子性

                引例

                我们可以在无状态对象的基础上为它增加一个域:

                - - -

                这是线程不安全的,因为++count包含了三个动作:读取—修改—写入

                -
                mov reg,count
                add reg,1
                mov count,reg
                - -

                它并不具有原子性。

                -

                在并发编程中,这种由于时序原因产生错误的情况叫做“竞态条件”。

                -

                竞态条件

                - -

                竞态条件有两种常见的类型。两种竞态条件的本质其实都是“基于对象之前的状态来定义对象状态的转换”。对于读取-修改-写入,是先copy原值,然后对原值+1,再写回,这是基于对象之前的状态来定义对象状态的转换;对于先检查后执行,很显然就是判断原值然后再转换到下一个状态,这就不必说了。

                -

                读取-修改-写入

                如上引例

                -

                先检查后执行

                实例:懒加载,延迟初始化中的竞态条件

                -
                public class LazyInitRace {
                private ExpensiveObject instance = null;

                public ExpensiveObject getInstance() {
                if (instance == null)
                instance = new ExpensiveObject();
                return instance;
                }
                }
                - - -

                竞态条件与数据竞争差别

                - - - -

                这书里讲得云里雾里的,百度了一下:

                - - -

                比如说书给例子,线程向共享对象读写数据,线程是操作对象A,共享对象是被操作对象B。则:

                -

                竞态条件:在乎的是被线程操控的共享对象的结果是否正确

                -

                数据竞争:在乎的是操作共享对象后,线程的结果是否正确。

                - - -

                确实,书里对数据竞争强调的是一个读一个写,对竞态条件更像是两个同时写

                -

                复合操作

                - - - - - -

                我们可以用一个线程安全类来解决前面的Count请求的需求:

                -
                @ThreadSafe
                public class CountingFactorizer implements Servlet{
                private final AtomicLong count = new AtomicLong(0);

                public long getCount(){ return count.get(); }

                public void service(SevletRequest req,ServletResponse resp){
                BigInteger i = extractFromRequest(req);
                BigInteger[] factors = factor(i);
                count.incrementAndGet();
                encodeIntoResponse(resp,factors);
                }
                }
                - - - - - - - -

                加锁机制

                线程安全分析法与为什么要加锁

                上面说到,当对象内仅有一个状态时,可以通过使用线程安全类来保障原子性。但当对象里存在多个状态时,就必须用锁来进行线程同步,而非简单地用多个线程安全类。

                -

                还是以上面的实例来解释。

                - - -
                public class UnsafeCachingFactorizer implements Servlet{
                private final AtomicReference<BigInteger> lastNumber = new AtomicReference<>();
                private final AtomicReference<BigInteger[]> lastFactors = new AtomicReference<>();

                public void service(ServletRequest req,ServletResponse resp){
                BigInteger i = extractFromRequest(req);
                /*
                此处产生了竞态条件。
                如果一个变量在此之后,return之前修改了lastFactors,就会寄
                */
                if (i.equals(lastNumber.get())) encodeIntoResponse(resp,lastFactors.get());
                else{
                BigInteger[] factors = factor(i);
                //本该需要瞬间一起完成的两个动作之间有时间间隔,不具原子性
                lastNumber.set(i);
                lastFactors.set(factors);
                encodeIntoResponse(resp,factors);
                }
                }
                }
                - - - - - -

                这段论述非常精彩,昭示了两个道理:1.分析线程安全性的时候,可以从“不变性条件不被破坏”开始考虑,首先考虑不变性条件应该是什么。2.在不变性条件涉及的多个变量彼此不独立,因而这些变量需要同时同步更新,上面那个例子就是因为不变性约束条件中的两个不独立变量没有同时同步更新。

                - - - - -

                确实,非常重要的一点就是在两个需要连续同时修改的变量之间有了并行的时间间隔,导致此期间并行的线程的不变性被破坏

                -

                内置锁

                - - - -

                同步代码块包含两部分,锁的引用和保护的代码段。关键字synchronized修饰的方法就是一段同步代码段,其锁对象为当前实例【非静态方法】或者是当前class的实例【静态方法】。

                -
                -

                这个具体的“锁”是什么以前是真不知道。已知的是所有Object都有wait和什么什么notify方法。不过想想也确实。所有线程争抢着访问一个对象的某个同步方法段,这不正跟所有线程争抢着一个锁是差不多意思的吗?“锁”的定义其实是很宽泛的

                -
                - - -

                java的内置锁并非无饥饿的。当线程B永远不释放锁,A会一直等待下去。

                - - -

                我们可以用synchronized来解决上面的计数器问题,即直接给service方法设为synchronized。当然这种方法性能很糟糕,因为它极大降低了并发度。

                -

                重入

                - -

                其中关于粒度的理解:

                -

                不是“每一次调用获取一次锁,该锁属于该此调用”,而是“每个线程调用时获取一次锁,该锁属于该线程”

                - - - - - - -
                public class Widget {
                public synchronized void doSomething(){

                }
                }

                class LoggingWidget extends Widget{
                @Override
                public synchronized void doSomething() {
                System.out.println(toString()+":calling doSomething.");
                super.doSomething();
                }
                }
                - -

                比如上述代码,创建了一个LoggingWidget实例,然后调用该实例的dosmething方法,就会获取到该实例的锁。如果不允许重入,那么在做super.doSomething时,该实例的锁【注意,是同一个实例】已经被占用还未释放,因此产生死锁。有重入就可以避免此问题

                -

                用锁来保护状态

                - - - -

                但这很考验人的记性。一旦你在某个地方忘了同步了就会寄。

                - - - - -

                活跃性与性能

                上面那个直接对service方法进行synchronized的改善方法粒度太粗了,可以试试如下方法:

                -
                @ThreadSafe
                public class CachedFactorizer implements Servlet{
                @GuardedBy ("this") private BigInteger lastNumber;
                @GuardedBy ("this") private BigInteger[] lastFactors;
                @GuardedBy ("this") private long hits;
                @GuardedBy ("this") private long cacheHits;

                public synchronized long getHits(){return hits;}
                public synchronized double getCacheHitRatio(){
                return (double)cacheHits/(double) hits;
                }

                public void service(ServletRequest req,ServletResponse resp){
                BigInteger i = extractFromRequest(req);
                BigInteger[] factors = null;
                synchronized (this){
                ++hits;
                if (i.equals(lastNumber)){
                ++cacheHits;
                factors = lastFactors.clone();
                }
                }
                //局部变量无需同步保护
                if (factors == null){
                factors = factor(i);
                synchronized (this){
                lastNumber = i;
                lastFactors = factors.clone();
                }
                }
                encodeIntoResponse(resp,factors);
                }
                }
                - - - -

                毕竟因数分解的时候无需同步保护,因为这时候参与运算的都是局部变量。

                - - - - -

                第三章 安全地共享对象

                上一章讲述了,线程安全的本质就是对共享和可变状态进行管理,以及介绍了用锁来保护状态。

                -

                本章将引入同步除原子性外的另一特性——可见性,然后再介绍如何构建线程安全类,并且安全地发布和共享对象。

                -

                关键词:可见性 Volatile 线程封闭 不可变对象

                - - - - - - -

                可见性

                引例——可见性的定义

                public class Main {
                private static boolean ready;
                private static int number;

                private static class ReaderThread extends Thread{
                public void run(){
                while(!ready){
                Thread.yield();
                }
                System.out.println(number);
                }
                }

                public static void main(String[] args){
                new ReaderThread().start();
                number=42;
                ready=true;
                }
                }
                - - - - - -
                -

                关于此程序显示出的对于内存可见性的理解,可以看这篇文章:

                -

                多线程(六):并发编程的三大特性之可见性

                - - -

                其实原因非常显而易见:主线程改了之后不会立刻把变量刷新到主存【可能默认是在ret时刷新,或者定时刷新,前者会导致相互等待的死锁,后者也会产生性能问题】,导致主线程的那个修改的flag变量对t1线程是**不可见**的,因此t1会继续循环等待。

                -
                -

                失效数据

                - - - - - - - -

                最低安全性

                - -

                注意,最低安全性不适用于非volatile类型的64位数据。

                - - - - - - -

                加锁与可见性

                - - - -

                要实现这种操作,我们可以设想一下关于内存可见性这一块内置锁的实现原理:lock时绑定指定变量,unlock时再刷新这个/些绑定变量的内存

                - - -

                所以说得有锁,并且锁还得是对的。

                -

                看着看着有种always语句块的感觉了2333

                - - - - -

                Volatile关键字

                Volatile保证内存可见性

                到其他线程。【我想大概就是一改变了,就马上刷新内存中的旧值,然后也许通过什么嗅探检测到值变化,通知所有线程改变自己持有的旧值。】 - - - -

                注意不放在寄存器里或者线程的私有栈里

                - - - +

                这样做在Servlet不会导致中文乱码,但JSP不行,这个大概是因为两者原理不一样。

                +

                Servlet的中文乱码:

                +image-20230222231756277 +

                JSP的:

                +

                image-20230222231820358

                +

                Servlet乱码是因为客户端和response请求体编码不一致,JSP乱码与JSP的原理有关,是只跟服务器端有关

                - +

                编译jsp有以下几个步骤:
                (1)把jsp转化为java源码。pageEncoding=xxx指定以xxx编码格式读取jsp文件,因此,jsp文件的编码格式应与pageEncoding值一致。
                (2)把java源码编译为字节码,即.class文件。转化后的java源码为utf-8编码格式,字节码也为utf-8编码,我们无需干预。
                (3)执行.class文件。在此过程,需向浏览器发送中文字符,contentType=xxx指定了jsp以xxx编码显示字符。也就是在浏览器中查看页面编码,其值为contentType指定的编码。

                +

                因此,在1、3环节,**只要指定一致的编码格式(jsp文件编码格式=pageEncoding=contentType)**,即可保证jsp页面不出现乱码。
                举例:jsp文件以utf-8格式编写,那么pageEncoding=utf-8, contentType=utf-8,就保证了jsp页面不出现乱码。
                ————————————————
                版权声明:本文为CSDN博主「liuhaidl」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
                原文链接:https://blog.csdn.net/liuhaidl/article/details/84012372

                -

                Volatile不保证原子性

                - -
                -

                volatile为什么不能保证原子性?

                -

                但这个有争议:

                - +

                指定方法是在JSP开头添加:

                +
                <%@ page pageEncoding="UTF-8"%>
                +

                内置对象

                在jsp页面中不需要获取和创建,可以直接使用的对象。

                +

                jsp一共有9个内置对象。

                +
                  +
                1. request HttpServletRequest 一次请求访问的多个资源(转发)

                  +
                2. +
                3. response HttpServletResponse 响应对象

                  +
                4. +
                5. out: JspWriter 字符输出流对象。可以将数据输出到页面上。和response.getWriter()类似

                    -
                  • 在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里
                  • +
                  • response.getWriter()和out.write()的区别: 在tomcat服务器真正给客户端做出响应之前,会先找response缓冲区数据,再找out缓冲区数据。因而response.getWriter()数据输出永远在out.write()之前 所以说,用out更好,因为它跟随你布局变化,你out写在哪,这句话最终就会输出在哪
                  -

                  也就是说,

                  -

                  如果线程B在+1前知道数据无效了,就会重新载入数据然后+1然后载入内存,结果正确;

                  -

                  如果线程B在+1后才知道数据无效,虽然会重新载入数据,数据为A修改后的新数据,但是此时指令无法回退,因而只能继续执行下一条指令:写回内存,B写回内存的是A修改后的新数据,因而结果错误。

                  -
                  大致过程:
                  a:mov reg 1
                  b:mov reg 1
                  a:add reg 1
                  b:add reg 1
                  a:mov reg mem
                  然后b线程得到通知,重新载入数据:mov reg mem
                  但是指令无法回退:mov reg mem
                  因而结果是A修改后的值被写入了两遍。
                  - -

                  所以其实volatile仅确保单次读写的瞬时线程安全

                  - - -

                  以下是别人的理解扩展:

                  -

                  对volatile不具有原子性的理解

                  -

                  volatile 无法保证原子性一个简单示例的疑问

                  -

                  Java并发编程:volatile关键字解析

                  -
                -

                Volatile的使用方法

                - - - -

                下面给出一个volatile的典型用法:检查某个状态标记以判断是否退出循环。【也就是上文那个例子】

                -
                volatile boolean asleep;
                //...
                while(!asleep) countingSheep();
                - - - -

                发布与逸出

                发布与逸出的概念

                通俗地解释发布和逸出

                - - - - - - - - - -

                这个“逸出作用域”的表述非常不错。

                - - - - -

                什么时候会发生发布和逸出

                外部方法

                当把一个对象传递给某个外部方法,就相当于发布了这个对象

                -

                外部方法:

                -
                发布内部的类实例

                “this escape”

                - - -
                public class ThisEscape{
                public final int id;
                public final String name;
                public ThisEscape(EventSource source){
                id = 1;
                //发布
                source.registerListener(
                //内部类
                new EventListener(){
                public void onEvent(Event e){
                //doSomething(e);
                }
                }
                );
                name = "escape";
                }
                }
                - -
                -

                java this 逸出_Java并发编程——this引用逸出(“this” Escape)

                -

                并发编程实践中,this引用逃逸(“this”escape)是指对象还没有构造完成,它的this引用就被发布出去了。

                -

                ThisEscape在构造函数中引入了一个内部类EventListener,而内部类会自动的持有其外部类(这里是ThisEscape)的this引用。source.registerListener会将内部类发布出去,从而ThisEscape.this引用也随着内部类被发布了出去。但此时ThisEscape对象还没有构造完成 —— id已被赋值为1,但name还没被赋值,仍然为null。这样一来,就有些线程持有不完整实例,不确定性太大了

                -
                - - -

                也就是说,如果是单线程情况下,这样做是没问题的,毕竟最后都会构造完整。但多线程情况下,这俩有时间间隔,因此会产生问题,并且不能靠简单地把这句发布对象的语句放在构造函数最后一行。

                - - -

                这段话非常值得注意

                -

                所以说上面那个例子的正确代码:

                - - -
                public class SafeListener {
                private final EventListener listener;

                private SafeListener(){
                listener = new EventListener() {
                public void onEvent(Event e){
                //doSomething(e);
                }
                };
                }

                public static SafeListener newInstance(EventSource source){
                SafeListener safe = new SafeListener();
                source.registerListener(safe.listener);
                return safe;
                }
                }
                - - - -

                线程封闭

                线程封闭是什么

                - - - -

                线程封闭一般有三种方法,这三种方法的规范性是逐级递增的。

                -

                Ad-hoc线程封闭

                - -
                -

                这里,书写得非常地抽象。通过查阅资料可得解释得更通俗的:

                -

                Ad-hoc线程封闭

                -

                Example of ad hoc thread confinement in Java

                -

                总之其实精华就这一句话:

                -

                并且都是人为约束,并且一般可能会用volatile来控制单线程写这种情况下的同步。

                -
                // Don't modify this from any other thread than Thread X.
                // So use it read-only for those other threads.
                private volatile int someNumber;
                -
                -

                栈封闭

                - -

                也就是我们前面说的,局部变量只能在该线程内访问,除非逸出了,否则是非常安全的。

                - - - - -

                对于基本类型

                - -意思就是说,java没有指针,获取不了这些不是对象的基本类型的引用,因而这些基本类型不可能通过调用外部方法之类的逸出【调用外部方法仅仅是取得它们的一份copy而非本身】,所以这些基本类型的局部变量始终封闭在线程内。 - -

                对于引用类型

                - -

                因而需要格外注意逸出问题

                -

                下面给出对基本类型和引用类型栈封闭的实例:

                -
                public int loadTheArk(Collection<Animal> candidates){
                //引用类型
                SortedSet<Animal> animals;
                //基本类型
                int numPairs = 0;
                Animal candidate = null;

                //需要详细写好注释↓
                //animals被封闭在方法中,不要使它们逸出!
                animals = new TreeSet<Animal>(new SpeciesGenderComparator());
                animals.addAll(candidates);
                for (Animal a : animals){
                if (candidate == null || !candidate.isPotentialMate(a))
                candidate = a;
                else{
                ark.load(new AnimalPair(candidate,a));
                ++numPairs;
                candidate = null;
                new ThreadLocal<char[]>(){

                };
                }
                }
                return numPairs;
                }
                - - - -

                ThreadLocal类

                -

                史上最全ThreadLocal 详解(一)

                -
                -

                简介和应用实例

                上面介绍了使用局部变量来实现线程封闭的方法,也就是栈封闭。它只要合理地控制在调用方法时不发生逸出,就可以实现线程安全。

                -

                当有多个线程都需要同一类对象【比如Connection对象、ThreadID】,并且要求每个线程内的该对象是不一样的,并且该对象需要在多个方法中访问,栈封闭的方法就显得有些麻烦和不够优雅:需要在每个线程内都创建一个不同的对象实例,并且在调用方法的时候,都把该对象实例作为参数传进去。

                -

                这时候就需要ThreadLocal类了。

                -

                ThreadLocal类会给每个线程分配一个对象,并且仅需使用get方法,就能自动地把线程中的对象给弄出来。并且这些分配的对象对于各个线程来说都是隔离,相互不可见的,因此实现了线程封闭,具有安全性。

                - - -

                以ThreadID为例:

                -

                For example, the class below generates unique identifiers local to each thread. A thread’s id is assigned the first time it invokes ThreadId.get() and remains unchanged on subsequent calls.下面代码保证每个线程首次调用ThreadId.get方法后可以分配到一个不重ID,并且ID一旦确定,之后再调用get方法得到的ID是不会改变的。它这相当于维护了一个共有的计数器局部变量。

                -
                import java.util.concurrent.atomic.AtomicInteger;
                public class ThreadId {
                // Atomic integer containing the next thread ID to be assigned
                private static final AtomicInteger nextId = new AtomicInteger(0);

                // Thread local variable containing each thread's ID
                private static final ThreadLocal<Integer> threadId =
                new ThreadLocal<Integer>() {
                @Override protected Integer initialValue() {
                return nextId.getAndIncrement();
                }
                };

                // Returns the current thread's unique ID, assigning it if necessary
                public static int get() {
                return threadId.get();
                }
                }
                - -

                对于每个想获取自身ThreadID的线程,在所有想用到ID的方法中,只需:

                -
                void method(){
                AtomicInteger myID = ThreadID.get();
                //do something
                }
                - -

                而不用:

                -
                AtomicInteger myID = getID();
                void method(AtomicInteger myID){
                //do something
                }
                - -

                这样大大简化了实现。

                -

                再比如:

                - - -
                private static final String DB_URL = "";
                private static ThreadLocal<Connection> connectionHolder
                = new ThreadLocal<Connection>(){
                public Connection initialValue(){
                try {
                return DriverManager.getConnection(DB_URL);
                } catch (SQLException e) {
                throw new RuntimeException(e);
                }
                }
                };
                public static Connection getConnection(){
                return connectionHolder.get();
                }
                - - - -

                关于这个的大概代码猜想:

                -

                这样一来,在一个线程中使用toString,就仅需造一个buf【这个是ThreadLocal封闭】,而不用每次调用都造一个【这个是栈封闭】了

                -
                private static final ThreadLocal<char[]> buf
                = new ThreadLocal<char[]>(){
                @Override
                public char[] initialValue(){
                return new char[12];
                }
                };

                @Override
                public static String toString(int i) {
                //...
                //使用buf
                char[] buffer = buf.get();
                //...
                }
                - - - -

                底层实现

                大致结构

                我们可以初步猜想,ThreadLocal大概是通过一个map实现的,里面存储着<Thread,value>这样的键值对,每次就能通过Thread来取出对应的value了。Java低版本确实是这么做的。但Java的高版本对此进行了优化。

                - - -

                从本来的:ThreadLocalMap<Thread, value> ∈ ThreadLocal

                -

                变成了: ThreadLocalMap<ThreadLocal, value> ∈ Thread

                -

                并且其中的ThreadLocal这个key是以弱引用【WeakReference】的方式实现的。

                -
                -

                ThreadLocal探究

                -

                这样的结构演进有什么好处
                在旧版的ThreadLocal中,所有线程都将本地变量存在同一个ThreadLocalMap中,当并发量比较高的时候,ThreadLocalMap中的数据量会很大,而新版的ThreadLocalMap是属于线程的,也就是每个线程都操作属于自己的ThreadLocalMap,那么map中存储的变量就只有自己所存入的,数据量大大减少。

                -

                还有一个好处,旧版的ThreadLocalMap属于ThreadLocal,当Thread实例被销毁时ThreadLocalMap里该线程的数据不会被同时销毁【这也许就带来了危险性】,而新版ThreadLocalMap属于线程,线程被销毁时,ThreadLocalMap也随之销毁

                -
                -
                源码阅读
                -

                This class provides thread-local variables.线程局部变量,也就是我们说的线程封闭手法。

                -

                These variables differ from their normal counterparts in that each thread that accesses one (via its get or set method) has its own, independently initialized copy of the variable. 每个线程都有它自己的、独立初始化的该变量的副本。

                -

                ThreadLocal instances are typically private static fields in classes that wish to associate state with a thread (e.g., a user ID or Transaction ID).它一般用于私有静态字段,whose 状态和线程关系密切。

                -

                Each thread holds an implicit reference to its copy of a thread-local variable as long as the thread is alive and the ThreadLocal instance is accessible.

                -

                After a thread goes away, all of its copies of thread-local instances are subject to garbage collection (unless other references to these copies exist).最后会被垃圾回收

                -
                -
                public class ThreadLocal<T> {

                private final int threadLocalHashCode = nextHashCode();


                private static AtomicInteger nextHashCode =
                new AtomicInteger();


                private static final int HASH_INCREMENT = 0x61c88647;


                private static int nextHashCode() {
                return nextHashCode.getAndAdd(HASH_INCREMENT);
                }

                /*
                它这个初始化方法非常聪明且独特。
                一般使用它的时候是直接new然后重载一个匿名内部类的,
                于是就直接在建立匿名内部类时override此方法,在里面构造初始化的对象,
                且该方法仅在get调用的时候才会顺带调用
                有一种lazy的思想在里面。
                Normally, this method is invoked at most once per thread.
                */
                protected T initialValue() {
                return null;
                }

                //Creates a thread local variable.
                //The initial value of the variable is determined by Supplier的get方法.
                public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
                return new SuppliedThreadLocal<>(supplier);
                }

                public ThreadLocal() {
                }


                public T get() {
                //也就是说,每个线程都有个ThreadLocal的map成员变量
                //里面装的是<ThreadLocal变量,该变量在该线程的值>这样的键值对
                Thread t = Thread.currentThread();
                //得到线程里存储的ThreadLocalMap
                ThreadLocalMap map = getMap(t);
                if (map != null) {
                //<ThreadLocal, value>
                ThreadLocalMap.Entry e = map.getEntry(this);
                if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
                }
                }
                //map==null【还没有线程局部变量】或者e==null【还没有该线程局部变量】
                return setInitialValue();
                }

                boolean isPresent() {
                Thread t = Thread.currentThread();
                ThreadLocalMap map = getMap(t);
                return map != null && map.getEntry(this) != null;
                }

                private T setInitialValue() {
                //获取初始化值
                T value = initialValue();
                Thread t = Thread.currentThread();
                ThreadLocalMap map = getMap(t);
                if (map != null) {
                map.set(this, value);
                } else {
                //传入空map的第一个结点
                createMap(t, value);
                }
                if (this instanceof TerminatingThreadLocal) {
                TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this);
                }
                return value;
                }

                public void set(T value) {
                Thread t = Thread.currentThread();
                ThreadLocalMap map = getMap(t);
                if (map != null) {
                map.set(this, value);
                } else {
                createMap(t, value);
                }
                }

                public void remove() {
                ThreadLocalMap m = getMap(Thread.currentThread());
                if (m != null) {
                m.remove(this);
                }
                }

                ThreadLocalMap getMap(Thread t) {
                return t.threadLocals;
                }

                void createMap(Thread t, T firstValue) {
                t.threadLocals = new ThreadLocalMap(this, firstValue);
                }

                static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
                return new ThreadLocalMap(parentMap);
                }

                T childValue(T parentValue) {
                throw new UnsupportedOperationException();
                }

                static final class SuppliedThreadLocal<T> extends ThreadLocal<T> {

                private final Supplier<? extends T> supplier;

                SuppliedThreadLocal(Supplier<? extends T> supplier) {
                this.supplier = Objects.requireNonNull(supplier);
                }

                @Override
                protected T initialValue() {
                return supplier.get();
                }
                }

                /*
                ThreadLocalMap is a customized hash map suitable only for maintaining thread local values.
                To help deal with very large and long-lived usages, the hash table entries use WeakReferences for keys.key【ThreadLocal】是弱引用的
                */
                static class ThreadLocalMap {

                /*
                Note that null keys (i.e. entry.get() == null) mean that the key is no longer referenced, so the entry can be expunged from table. Such entries are referred to as "stale entries" in the code that follows.
                意思就是说陈旧条目【stale entry】指的是key为空的
                */
                static class Entry extends WeakReference<ThreadLocal<?>> {
                //The value associated with this ThreadLocal.
                Object value;

                Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
                }
                }

                //The initial capacity -- MUST be a power of two.
                private static final int INITIAL_CAPACITY = 16;

                private Entry[] table;

                private int size = 0;

                //The next size value at which to resize.
                private int threshold; // Default to 0

                //Set the resize threshold to maintain at worst a 2/3 load factor.
                //默认情况下,装载因子为2/3
                private void setThreshold(int len) {
                threshold = len * 2 / 3;
                }

                //使i增加,并且让增加后的结果模len。也就是(++i)%len。
                private static int nextIndex(int i, int len) {
                return ((i + 1 < len) ? i + 1 : 0);
                }

                //也就是(--i)%len。
                private static int prevIndex(int i, int len) {
                return ((i - 1 >= 0) ? i - 1 : len - 1);
                }

                //Construct a new map initially containing (firstKey, firstValue).
                //ThreadLocalMaps are constructed lazily,
                //so we only create one when we have at least one entry to put in it.
                ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
                table = new Entry[INITIAL_CAPACITY];
                //依旧是HashMap里面经典的掩码操作,key的hashcode作为entry在table里的序号
                //与hashmap的差别就在于,hashmap的桶table一个里面可以存放多个结点,
                //但这里的ThreadLocal的hash显然是不冲突的,因而只能存放一个结点
                int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
                table[i] = new Entry(firstKey, firstValue);
                size = 1;
                setThreshold(INITIAL_CAPACITY);
                }

                private ThreadLocalMap(ThreadLocalMap parentMap) {
                Entry[] parentTable = parentMap.table;
                int len = parentTable.length;
                setThreshold(len);
                table = new Entry[len];

                for (int j = 0; j < len; j++) {
                Entry e = parentTable[j];
                //有对应结点
                if (e != null) {
                @SuppressWarnings("unchecked")
                ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
                if (key != null) {
                Object value = key.childValue(e.value);
                Entry c = new Entry(key, value);
                int h = key.threadLocalHashCode & (len - 1);
                while (table[h] != null)
                h = nextIndex(h, len);
                table[h] = c;
                size++;
                }
                }
                }
                }

                private Entry getEntry(ThreadLocal<?> key) {
                int i = key.threadLocalHashCode & (table.length - 1);
                Entry e = table[i];
                if (e != null && e.get() == key)
                return e;
                else
                return getEntryAfterMiss(key, i, e);
                }

                private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
                Entry[] tab = table;
                int len = tab.length;

                while (e != null) {
                ThreadLocal<?> k = e.get();
                if (k == key)
                return e;
                if (k == null)
                expungeStaleEntry(i);
                else
                i = nextIndex(i, len);
                e = tab[i];
                }
                return null;
                }

                private void set(ThreadLocal<?> key, Object value) {

                // We don't use a fast path as with get() because it is at
                // least as common to use set() to create new entries as
                // it is to replace existing ones, in which case, a fast
                // path would fail more often than not.

                Entry[] tab = table;
                int len = tab.length;
                int i = key.threadLocalHashCode & (len-1);

                for (Entry e = tab[i];
                e != null;
                e = tab[i = nextIndex(i, len)]) {
                ThreadLocal<?> k = e.get();

                if (k == key) {
                e.value = value;
                return;
                }

                if (k == null) {
                replaceStaleEntry(key, value, i);
                return;
                }
                }

                tab[i] = new Entry(key, value);
                int sz = ++size;
                if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
                }


                private void remove(ThreadLocal<?> key) {
                Entry[] tab = table;
                int len = tab.length;
                int i = key.threadLocalHashCode & (len-1);
                for (Entry e = tab[i];
                e != null;
                e = tab[i = nextIndex(i, len)]) {
                if (e.get() == key) {
                e.clear();
                expungeStaleEntry(i);
                return;
                }
                }
                }


                private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                int staleSlot) {
                Entry[] tab = table;
                int len = tab.length;
                Entry e;

                // Back up to check for prior stale entry in current run.
                // We clean out whole runs at a time to avoid continual
                // incremental rehashing due to garbage collector freeing
                // up refs in bunches (i.e., whenever the collector runs).
                int slotToExpunge = staleSlot;
                for (int i = prevIndex(staleSlot, len);
                (e = tab[i]) != null;
                i = prevIndex(i, len))
                if (e.get() == null)
                slotToExpunge = i;

                // Find either the key or trailing null slot of run, whichever
                // occurs first
                for (int i = nextIndex(staleSlot, len);
                (e = tab[i]) != null;
                i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();

                // If we find key, then we need to swap it
                // with the stale entry to maintain hash table order.
                // The newly stale slot, or any other stale slot
                // encountered above it, can then be sent to expungeStaleEntry
                // to remove or rehash all of the other entries in run.
                if (k == key) {
                e.value = value;

                tab[i] = tab[staleSlot];
                tab[staleSlot] = e;

                // Start expunge at preceding stale entry if it exists
                if (slotToExpunge == staleSlot)
                slotToExpunge = i;
                cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
                return;
                }

                // If we didn't find stale entry on backward scan, the
                // first stale entry seen while scanning for key is the
                // first still present in the run.
                if (k == null && slotToExpunge == staleSlot)
                slotToExpunge = i;
                }

                // If key not found, put new entry in stale slot
                tab[staleSlot].value = null;
                tab[staleSlot] = new Entry(key, value);

                // If there are any other stale entries in run, expunge them
                if (slotToExpunge != staleSlot)
                cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
                }


                private int expungeStaleEntry(int staleSlot) {
                Entry[] tab = table;
                int len = tab.length;

                // expunge entry at staleSlot
                tab[staleSlot].value = null;
                tab[staleSlot] = null;
                size--;

                // Rehash until we encounter null
                Entry e;
                int i;
                for (i = nextIndex(staleSlot, len);
                (e = tab[i]) != null;
                i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();
                if (k == null) {
                e.value = null;
                tab[i] = null;
                size--;
                } else {
                int h = k.threadLocalHashCode & (len - 1);
                if (h != i) {
                tab[i] = null;

                // Unlike Knuth 6.4 Algorithm R, we must scan until
                // null because multiple entries could have been stale.
                while (tab[h] != null)
                h = nextIndex(h, len);
                tab[h] = e;
                }
                }
                }
                return i;
                }


                private boolean cleanSomeSlots(int i, int n) {
                boolean removed = false;
                Entry[] tab = table;
                int len = tab.length;
                do {
                i = nextIndex(i, len);
                Entry e = tab[i];
                if (e != null && e.get() == null) {
                n = len;
                removed = true;
                i = expungeStaleEntry(i);
                }
                } while ( (n >>>= 1) != 0);
                return removed;
                }


                private void rehash() {
                expungeStaleEntries();

                // Use lower threshold for doubling to avoid hysteresis
                if (size >= threshold - threshold / 4)
                resize();
                }


                private void resize() {
                Entry[] oldTab = table;
                int oldLen = oldTab.length;
                int newLen = oldLen * 2;
                Entry[] newTab = new Entry[newLen];
                int count = 0;

                for (int j = 0; j < oldLen; ++j) {
                Entry e = oldTab[j];
                if (e != null) {
                ThreadLocal<?> k = e.get();
                if (k == null) {
                e.value = null; // Help the GC
                } else {
                int h = k.threadLocalHashCode & (newLen - 1);
                while (newTab[h] != null)
                h = nextIndex(h, newLen);
                newTab[h] = e;
                count++;
                }
                }
                }

                setThreshold(newLen);
                size = count;
                table = newTab;
                }


                private void expungeStaleEntries() {
                Entry[] tab = table;
                int len = tab.length;
                for (int j = 0; j < len; j++) {
                Entry e = tab[j];
                if (e != null && e.get() == null)
                expungeStaleEntry(j);
                }
                }
                }
                }
                - -

                注意点:

                + +
              5. pageContext PageContext 当前页面共享数据,还可以获取其他八个内置对象

                +
              6. +
              7. session HttpSession 一次会话的多个请求间

                +
              8. +
              9. application ServletContext 所有用户间共享数据

                +
              10. +
              11. page Object 当前页面(Servlet)的对象,相当于this

                +
              12. +
              13. config ServletConfig Servlet的配置对象

                +
              14. +
              15. exception Throwable 异常对象。只在page指令的isErrorPage为true的情况下才能使用此对象。

                +
              16. +
              +

              其中,

              +

              image-20230307144539506

              +

              这四个为用来共享数据的域对象

              +

              演变:MVC开发模式

              jsp的演变

              image-20230307145442383

              +

              MVC模式

              将程序分成三个部分,分别是M-V-C。

                -
              1. 哈希方法和解决哈希冲突
                - -

                存在哈希冲突的话,大概是采用的线性探测方法。

                +
              2. M:Model,模型。JavaBean
                  +
                • 完成具体的业务操作,如:查询数据库,封装对象
                • +
              3. -
              4. 解决内存泄漏

                关于其remove方法:

                -
                public void remove() {
                ThreadLocalMap m = getMap(Thread.currentThread());
                if (m != null)
                m.remove(this);
                }
                //m.remove
                private void remove(ThreadLocal<?> key) {
                Entry[] tab = table;
                int len = tab.length;
                int i = key.threadLocalHashCode & (len-1);
                //线性探测
                for (Entry e = tab[i];
                e != null;
                e = tab[i = nextIndex(i, len)]) {
                if (e.get() == key) {
                e.clear();
                expungeStaleEntry(i);
                return;
                }
                }
                }
                - -

                两篇文章都有解释

                -
                -

                remove方法,直接将ThrealLocal 对应的值从当前相差Thread中的ThreadLocalMap中删除。为什么要删除,这涉及到内存泄露的问题。

                -

                实际上 ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,弱引用的特点是,如果这个对象只存在弱引用,那么在下一次垃圾回收的时候必然会被清理掉。

                -

                所以如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候会被清理掉的,这样一来 ThreadLocalMap中使用这个 ThreadLocal 的 key 也会被清理掉。但是,value 是强引用,不会被清理,这样一来就会出现 key 为 null 的 value。

                -

                ThreadLocal其实是与线程绑定的一个变量,如此就会出现一个问题:如果没有将ThreadLocal内的变量删除(remove)或替换,它的生命周期将会与线程共存。通常线程池中对线程管理都是采用线程复用的方法,在线程池中线程很难结束甚至于永远不会结束,这将意味着线程持续的时间将不可预测,甚至与JVM的生命周期一致。举个例字,如果ThreadLocal中直接或间接包装了集合类或复杂对象,每次在同一个ThreadLocal中取出对象后,再对内容做操作,那么内部的集合类和复杂对象所占用的空间可能会开始持续膨胀。
                ————————————————
                版权声明:本文为CSDN博主「倔强的不服」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
                原文链接:https://blog.csdn.net/u010445301/article/details/111322569

                -
                -
                -

                ThreadLocal内存泄漏问题的解析。
                前面我们说到它虽然线程安全,但是它存在一个问题那就是内存泄漏。

                -

                首先我们要明白为什么会内存泄漏,前面也说了ThreaLocal是一个弱引用,什么是弱引用就是当它为null时候,就会被垃圾回收机制给带走,重点就是,如果我们的ThreadLocal突然为null,然后就被回收了,但此时我们的ThreadLocalMap它的生命周期是和Thread相同的,简单理解就是,裤子没了,兜还在,兜里面还有我们的数据,这就造成了内存泄漏。

                -

                如何解决那:我们必须在使用完ThreadLocal后,执行remove()方法,避免内存溢出。
                ————————————————
                版权声明:本文为CSDN博主「某刘姓男子i的码农客栈」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
                原文链接:https://blog.csdn.net/qq_20783497/article/details/107980858

                -
                +
              5. V:View,视图。JSP
                  +
                • 展示数据
                • +
                +
              6. +
              7. C:Controller,控制器。Servlet
                  +
                • 获取用户的输入
                • +
                • 调用模型
                • +
                • 将数据交给视图进行展示【域对象共享数据】
                • +
              -

              不变性

              不可变对象

              不可变对象的线程安全性

              满足同步需求的另一种方案就是使用不可变对象

              - - -

              这个思路非常地简单粗暴:什么东西影响了,就直接让它消失。非常有意思2333

              -

              如果某个对象在创建后不能被修改,那么它就叫不可变对象。线程安全性是不可变对象的固有属性之一

              - - -

              比如说final域只能在声明的成员域或者构造函数中初始化,两者本质上都是在构造函数中初始化的。

              -

              并且不可变对象也更加安全。

              - - - - -

              不可变对象与final域

              不可变性不等于将对象中的所有域都设置为final域,因为final类型的域可以是对可变对象的引用。【这就类似C语言中const指针】当且仅当满足下列条件,对象才是不可变的:

              - - -
              -

              对于这里注释提到的String类,它讲得有些让人迷惑。因而我查阅资料得到解说如下:

              -

              String中hashCode方法的线程安全

              -
              class String{
                //默认值是0
                int hash;

                public int hashCode() {
              //将成员变量hash缓存到局部变量
              int h = hash;
                   //这里使用的是局部变量,因此没有多线程修改的风险
              if (h == 0 && value.length > 0) {
              char val[] = value;
              //求hashcode过程使用局部h变量防止产生静态条件
              for (int i = 0; i < value.length; i++) {
              h = 31 * h + val[i];
              }

              //把求出的hashcode缓存到局部变量,原子操作
              //这里不需要考虑线程可见性的问题,
              //如果其它线程未能及时看到最新修改,重新计算hash值代价也不大
              hash = h;
              }
              return h;
              }
              }
              - -

              再回去看书中注释的描述:

              - - -

              这个的意思就是说,对每个线程来说,同一个字符串hashcode值都是一样的【每次计算都得到相同的结果】,所以就不会产生多个线程计算出不同值的情况,导致不同步的发生。

              - - -

              意思是说之所以hashcode值一样,是因为这个hashcode计算是基于不可变对象的:

              -
              private final char value[];
              - -

              并且重复计算性能代价可能远没有加锁的消耗来得大,因而这里仅使用了栈封闭来保证一定程度上的线程同步。

              +

              image-20230307150845273

              +

              服务器将接收的请求给控制器处理,控制器控制model完成必要的运算,model把算出的东西返回给控制器,控制器再把数据交给视图展示,数据最终就回到了浏览器客户端。

              +

              这就算是一个微型CPU了吧,控制器就是CU,模型就是ALU,也许客户端和视图什么的可以视为IO接口。

              +
                +
              • 优缺点:

                +
                  +
                1. 优点:

                  +
                    +
                  1. 耦合性低,方便维护,可以利于分工协作
                  2. +
                  3. 重用性高
                  4. +
                  +
                2. +
                3. 缺点:

                  +
                    +
                  1. 使得项目架构变得复杂,对开发人员要求高
                  2. +
                  +
                4. +
                +
              • +
              +

              那么,我们可以知道,jsp就只需负责数据的展示了。那怎么展示数据呢?这就需要用到jsp的几个技术了:

              +

              EL表达式

              +

              注意,servlet3.0以来默认关闭el表达式解析,需要手动在page上加属性打开,详见 jsp文件中的el表达式失效问题解决

              -

              可变对象基础上构建不可变类

              - -
              public final class ThreeStooge {
              private final Set<String> stooges = new HashSet<>();

              public ThreeStooge(){
              stooges.add("Moe");
              stooges.add("Larry");
              stooges.add("Curly");
              }

              public boolean isStooge(String name){
              return stooges.contains(name);
              }
              }
              - - - -

              也就是说,实现的核心是保证可变对象不变即可。

              - - - - -

              Final域

              - -

              final不仅保证了引用对象的不可变,还保证了不可变对象初始化过程中的线程安全性

              - - -

              所以说还是尽量多用final

              -

              Volatile与不可变对象提供弱原子性

              也就是这个图片中所说的:

              - - -

              使用volatile变量来发布不可变对象,不仅可以更新保存在不可变对象中的程序状态,还可以为一组操作提供弱原子性。

              - - -

              前面的代码为:

              -
              public class UnsafeCachingFactorizer implements Servlet{
              private final AtomicReference<BigInteger> lastNumber = new AtomicReference<>();
              private final AtomicReference<BigInteger[]> lastFactors = new AtomicReference<>();

              public void service(ServletRequest req,ServletResponse resp){
              BigInteger i = extractFromRequest(req);
              /*竞态条件*/
              if (i.equals(lastNumber.get())) encodeIntoResponse(resp,lastFactors.get());
              else{
              BigInteger[] factors = factor(i);
              //时间间隔
              lastNumber.set(i);
              lastFactors.set(factors);
              encodeIntoResponse(resp,factors);
              }
              }
              }
              - -

              如今,利用volatile和不可变类的相互配合,我们修改如下:

              -
              public class VolatileCachedFactorizer implements Servlet{
              //从使用两个分别原子的变量,变为使用一个volatile修饰的不可变类
              private volatile OneValueCache cache = new OneValueCache(null,null);

              public void service(ServletRequest req,ServletResponse resp){
              BigInteger i = extractFromRequest(req);
              BigInteger[] factors = cache.getFactors(i);
              if (factors == null){
              factors = factor(i);
              //直接new一个新容器,利用了final域在初始化过程中的线程安全,因而保证了原子性
              //同时也用了volatile快刷新的性质,保证了可见性,当一个线程设置为新的,其他会立即看到
              //妙啊,这样就非常完美地达成了线程安全性
              cache = new OneValueCache(i,factors);
              }
              encodeIntoResponse(resp,factors);
              }
              }

              public class OneValueCache {
              private final BigInteger lastNumber;
              private final BigInteger[] lastFactors;

              public OneValueCache(BigInteger lastNumber, BigInteger[] lastFactors) {
              this.lastNumber = lastNumber;
              //传递副本,保证不可变
              this.lastFactors = Arrays.copyOf(lastFactors,lastFactors.length);
              }

              public BigInteger[] getFactors(BigInteger i){
              if (lastNumber == null || !lastNumber.equals(i)){
              return null;
              }
              else{
              //传递副本,保证不可变
              return Arrays.copyOf(lastFactors,lastFactors.length);
              }
              }
              }
              - - - -

              每当需要对一组相关数据以原子方式执行某个操作时,就可以考虑创建一个不可变的类来包含这些数据,其本质就是利用不可变性消除了访问和更新多个变量的竞态条件。

              - - -

              因为依然满足该程序的不变性原理:factor数组中各个数字的乘积=lastNumber,也就是说容器对象的两个值都是正确对应的,因而容器对象处于一致的状态。又或者是因为volatile及时刷新,因此确保了各个线程的内存可见性。

              - - - - -

              安全发布

              - -

              现在,我们要来讲讲如何安全地对对象进行发布。

              -

              一个不正确程序案例

              public Holder holder;

              public void initialize(){ holder = new Holder(); }

              //以下是Holder类定义
              public class Holder{
              private int n;

              public Holder(int n){ this.n = n; }

              public void assertSanity(){
              if( n != n )
              throw new AssertionError("This statement is false.");
              }
              }
              - -

              Holder类本身是没有问题的,这段代码出问题的原因是holder没有被正确地发布。

              -

              关于holder为什么没有被正确地发布:

              -

              参考文章

              【并发编程】安全发布对象与防止对象逸出(原因与防护方法)

              -

              分析过程

              由参考文章1:

              - - -

              可知,new一个对象并非原子操作,并且很有可能先得到内存引用才初始化对象。

              -

              因而,在上面那段不安全代码的语境下可分析:

              -
              Holder的错误发布有三点如下:

              首先明确,引用,和引用的对象的状态,这两个是两个需要独立考虑的方面。前者是一个指针值,后者是指针所指的数据。下面的点1仅考虑引用的更新,点2考虑了引用对象的状态更新。

              -

              \1. 发布对象的那个线程给holder初始化之后,holder这个引用没有及时刷新到内存,因而对其他线程不可见,其他线程读到的holder引用是旧的。

              -

              \2. 又或者,发布了holder还没初始化完毕的时候,别的进程读取到未完成初始化的holder这个引用,但这个引用指向的状态却是旧的,因为它还没完成初始化,其状态值为旧值或者默认值。【发生了上面new一个对象的指令重排】

              -

              \3. 如果在assert方法中两次读取n发生了上面第二条,就可能会导致前后的n不唯一,抛出异常。

              -

              由书中表述,如果将Holder转化为不可变类,那么该发布是安全的。

              - - -

              至于为什么,可见下个标题。

              -
              -

              此处插入思考:是否可以将public Holder holder修改为public final Holder holder,或者volatile修饰,来解决上述问题呢?

              -

              java多线程关键字final和static详解

              -

              通过看该文章得知:

              - - -

              volatile和final都会禁止字段引用的对象在构造对象过程中发生指令重排,别的线程得到引用的时候构造已经完成,而不会先得到引用再完成构造,并且两个标志都可以保证可见性。

              -

              不过继续读下去,书中给出了答案:我说的这个方法也是可行的。

              - - -

              我的疑问就是第二点和第三点。

              -
              -

              不可变对象的初始化安全性

              - - - - - -

              安全发布的常用模式

              - -

              分别解说

              -

              静态初始化对象引用

              - -

              volatile、final以及AtomicReferance保护引用

              详见上面那个不正确案例最后的思考

              -

              由锁保护的区域

              这个区域除了是通过程序构造的,也可以是使用Java自带的线程安全类库

              - - - - -

              事实不可变对象

              安全发布可以保证发布时的线程安全所以说你如果承诺发布后可以一直保证不可变,那就一直都是线程安全的。

              - - - - - - - - -

              对象的可变性与正确发布

              - - - -

              安全地共享对象

              - - - -

              第四章 对象的组合

              - -

              也就是说上面都是在讲怎么让一个对象的共享变得安全,下面我们讲怎么依据设计模式,让一个类更容易成为线程安全的

              -

              如何设计线程安全的类

              - - - - - - - - - - - -

              收集同步需求

              本质上是找不变性条件和后验条件

              要保证不变性条件始终成立,确保后验条件符合预期。

              - - -

              讲了什么是不变性条件和后验条件:

              - - - - -

              无效的状态转换只能出现在原子序列中

              - - - - -

              依赖状态的操作

              - -

              也就是说先验条件和状态域相关。

              - - - - -

              状态的所有权

              - - - -

              666666

              - - - - -

              实例封闭

              什么是实例封闭

              - - - -

              所以需要上一章学的安全发布。

              - - -
              //通过封闭机制保证线程安全
              @ThreadSafe
              public class PersonSet {
              //不安全
              //封闭在实例内部
              private final Set<Person> mySet = new HashSet<>();

              //对所有代码路径加锁访问
              public synchronized void addPerson(Person p) {
              mySet.add(p);
              }
              public synchronized boolean containsPerson(Person p){
              return mySet.contains(p);
              }
              }
              - - - - - -

              阅读源码可知:

              -
              static class SynchronizedCollection<E> implements Collection<E>, Serializable {
              private static final long serialVersionUID = 3053995032091335093L;

              final Collection<E> c; // Backing Collection
              final Object mutex; // Object on which to synchronize

              SynchronizedCollection(Collection<E> c) {
              this.c = Objects.requireNonNull(c);
              mutex = this;
              }

              SynchronizedCollection(Collection<E> c, Object mutex) {
              this.c = Objects.requireNonNull(c);
              this.mutex = Objects.requireNonNull(mutex);
              }

              public int size() {
              synchronized (mutex) {return c.size();}
              }
              public boolean isEmpty() {
              synchronized (mutex) {return c.isEmpty();}
              }
              public boolean contains(Object o) {
              synchronized (mutex) {return c.contains(o);}
              }
              public Object[] toArray() {
              synchronized (mutex) {return c.toArray();}
              }
              public <T> T[] toArray(T[] a) {
              synchronized (mutex) {return c.toArray(a);}
              }

              //注意此处没用同步
              public Iterator<E> iterator() {
              return c.iterator(); // Must be manually synched by user!
              }

              public boolean add(E e) {
              synchronized (mutex) {return c.add(e);}
              }
              public boolean remove(Object o) {
              synchronized (mutex) {return c.remove(o);}
              }

              public boolean containsAll(Collection<?> coll) {
              synchronized (mutex) {return c.containsAll(coll);}
              }
              public boolean addAll(Collection<? extends E> coll) {
              synchronized (mutex) {return c.addAll(coll);}
              }
              public boolean removeAll(Collection<?> coll) {
              synchronized (mutex) {return c.removeAll(coll);}
              }
              public boolean retainAll(Collection<?> coll) {
              synchronized (mutex) {return c.retainAll(coll);}
              }
              public void clear() {
              synchronized (mutex) {c.clear();}
              }
              public String toString() {
              synchronized (mutex) {return c.toString();}
              }
              // Override default methods in Collection
              @Override
              public void forEach(Consumer<? super E> consumer) {
              synchronized (mutex) {c.forEach(consumer);}
              }
              @Override
              public boolean removeIf(Predicate<? super E> filter) {
              synchronized (mutex) {return c.removeIf(filter);}
              }
              @Override
              public Spliterator<E> spliterator() {
              return c.spliterator(); // Must be manually synched by user!
              }
              @Override
              public Stream<E> stream() {
              return c.stream(); // Must be manually synched by user!
              }
              @Override
              public Stream<E> parallelStream() {
              return c.parallelStream(); // Must be manually synched by user!
              }
              private void writeObject(ObjectOutputStream s) throws IOException {
              synchronized (mutex) {s.defaultWriteObject();}
              }
              }
              - -

              就是把原来的collection给实例封闭了,之后的访问都用了同步锁。

              -

              Java监视器模式

              使用内置锁

              - -

              直白点来说,就是把所有要访问自己状态的地方/方法通通synchronized。

              - - -
              //监视器模式
              @ThreadSafe
              public final class Counter {
              @GuardedBy("this") private long value = 0;

              public synchronized long getValue(){
              return value;
              }

              public synchronized long increment(){
              if (value == Long.MAX_VALUE){
              throw new IllegalStateException();
              }
              return ++value;
              }
              }
              - -

              这样虽然简单,但缺点就是很粗暴:同步的粒度太粗了。

              -

              使用私有锁

              也跟内置锁道理差不多

              - - -

              也就是说私有锁可以让外面的世界也参与到同步中来,但内置锁不大行。

              -

              示例:车辆追踪

              public class MutablePoint {
              public int x,y;

              public MutablePoint() {
              x=0;y=0;
              }
              public MutablePoint(MutablePoint p){
              //深拷贝
              this.x=p.x;
              this.y=p.y;
              }
              }
              - -
              //基于监视器模式
              public class MonitorVehicleTracker {
              private final Map<String, MutablePoint> locations;

              public MonitorVehicleTracker(
              Map<String,MutablePoint> locations
              ){
              this.locations = deepCopy(locations);
              }

              public synchronized Map<String,MutablePoint> getLocations(){
              return deepCopy(locations);
              }
              public synchronized MutablePoint getLocation(String id){
              MutablePoint loc = locations.get(id);
              //返回copy对象,深拷贝
              return loc == null?null : new MutablePoint(loc);
              }
              public synchronized void setLocation(String id,int x,int y){
              MutablePoint loc = locations.get(id);
              if (loc == null) throw new IllegalArgumentException();
              loc.x = x;
              loc.y = y;
              }

              //为什么这方法不用锁?是因为调用它的地方都锁着
              private static Map<String,MutablePoint> deepCopy(Map<String,MutablePoint> m){
              Map<String,MutablePoint> res = new HashMap<>();
              for (Map.Entry en : m.entrySet()){
              //此处通过MutablePoint的构造函数重新拷贝了一个Point
              //如果简单地使用HashMap的构造函数new HashMap(m)的拷贝来创建一个新的map是不行的
              //因为这样只会拷贝Point对象的指针值,依然是浅拷贝
              res.put((String) en.getKey(),new MutablePoint((MutablePoint) en.getValue()));
              }
              return Collections.unmodifiableMap(res);
              }
              }
              - - - - - -

              将线程安全性委托给独立的状态变量

              - -

              定义

              - - - -

              意思就是保证一个类里面仅有一个状态,只要该状态是线程安全的,那么该类也就是线程安全的

              -

              示例

              //线程安全
              public class Point {
              public final int x,y;

              public Point(int x, int y) {
              this.x = x;
              this.y = y;
              }
              }
              - -
              //将线程安全委托给ConcurrentMap
              public class DelegatingVehicleTracker {
              //用了两个
              private final ConcurrentMap<String,Point> locations;
              private final Map<String,Point> unmodifiableMap;

              public DelegatingVehicleTracker(Map<String,Point> ps) {
              locations = new ConcurrentHashMap<>(ps);
              unmodifiableMap = Collections.unmodifiableMap(locations);
              }

              public Map<String,Point> getLocations(){
              //unmodifiableMap baked by locations,所以locations变化也会反映到unmodifiableMap上
              //目的只是为了提供外界无法修改的视图
              //不得不说真是妙啊
              return unmodifiableMap;
              }
              public Point getLocation(String id){
              return locations.get(id);
              }

              public void setLocation(String id,int x,int y){
              if (locations.replace(id,new Point(x,y)) == null){
              throw new IllegalArgumentException();
              }
              }
              }
              - - - - - -
              public Map<String,Point> getLocations(){
              return Collections.unmodifiableMap(new HashMap<>(locations));
              }
              - - - -

              委托给多个状态变量

              - -

              也就是说这些对象彼此不会构成不变性条件。

              -
              //将线程安全性委托给多个状态变量
              public class VisualComponent {
              private final List<KeyListener> keyListeners = new CopyOnWriteArrayList<>();
              private final List<MouseListener> mouseListeners = new CopyOnWriteArrayList<>();

              public void addKeyListener(KeyListener listener){
              keyListeners.add(listener);
              }

              public void addMouseListener(MouseListener listener){
              mouseListeners.add(listener);
              }

              public void removeKeyListener(KeyListener listener){
              keyListeners.remove(listener);
              }

              public void remiveMouseListener(MouseListener listener){
              mouseListeners.remove(listener);
              }
              }
              - - - -

              而且键盘监听和鼠标监听彼此独立。

              -

              不独立多个状态变量不能委托

              public class NumberRange {
              //不变性条件: lower<=upper
              private final AtomicInteger lower = new AtomicInteger(0);
              private final AtomicInteger upper = new AtomicInteger(0);

              public void setLower(int i){
              //先检查后执行
              if (i>upper.get()){
              throw new IllegalArgumentException();
              }
              lower.set(i);
              }
              public void setUpper(int i){
              if (i<lower.get()){
              throw new IllegalArgumentException();
              }
              upper.set(i);
              }
              public boolean isInRange(int i){
              return (i>=lower.get() && i<=upper.get());
              }
              }
              - - - -

              根本原因就是因为不独立。

              - - - - -

              发布被委托的状态变量

              什么时候可以发布

              - - - -

              示例

              - -
              package sit;
              @ThreadSafe
              public class SafePoint {
              //注意此处x和y没有用任何同步修饰词修饰
              private int x,y;
              private SafePoint(int[] a){
              this(a[0],a[1]);
              }

              //此处为什么不直接用this(p.x,p.y)呢?
              //是因为x和y本身并没有任何线程安全的防护手段,这样做的话会发生竞态条件。况且x和y也被实例封闭了
              //私有构造函数捕获模式
              public SafePoint(SafePoint p){
              this(p.get());
              }

              public SafePoint(int x,int y){
              this.x=x;
              this.y=y;
              }

              //x、y都放入数组,保证x和y的同时读写,nb
              public synchronized int[] get(){
              return new int[] {x,y};
              }

              public synchronized void set(int x,int y){
              this.x=x;
              this.y=y;
              }
              }
              - -
              //跟上面那个委托没什么差,区别只在于上面的那个SafePoint类,既是线程安全的,又是可修改的
              public class PublishingVehicleTracker {
              private final Map<String,SafePoint> locations;
              private final Map<String,SafePoint> unmodifiableMap;

              public PublishingVehicleTracker(Map<String,SafePoint> locations){
              this.locations = new ConcurrentHashMap<>(locations);
              this.unmodifiableMap = Collections.unmodifiableMap(this.locations);
              }

              public Map<String,SafePoint> getLocations(){
              return unmodifiableMap;
              }

              public SafePoint getLocation(String id){
              return locations.get(id);
              }

              public void setLocations(String id,int x,int y){
              if (!locations.containsKey(id))
              throw new IllegalArgumentException();
              locations.get(id).set(x,y);
              }
              }
              - - - - - -

              这仅仅是一个委托发布的实例。

              -

              在现有的线程安全类中添加功能

              引论

              - -

              比如说想给vector添加一个“put-if-absent”

              - - -

              可以用子类扩展法,也可以直接加源代码。后者有时候源代码不可访问,前者的父类很多域可能不对子类开发,并且非常脆弱。因而下面介绍几种比较好的机制。

              -

              客户端加锁机制

              定义和实例

              - -
              @NotThreadSafe
              public class NotThreadSafeListHelper<E> {
              public List<E> list = Collections.synchronizedList(new ArrayList<>());

              public synchronized boolean putIfAbsent(E x){
              boolean absent = !list.contains(x);
              if (absent)
              list.add(x);
              return absent;
              }
              }
              - - - -

              我曹,66666666

              -

              也就是说,这里加的是ListHelper的锁,只能让别的线程不能通过putIfAbsent方法同时修改list,但别的线程完全可以直接获取list再修改。

              - - -

              客户端指的是我们的ListHelper。我们正是不知道list这个对象使用的是哪一个锁才发愣的。

              -

              所以我们使用ArrayList自身的锁,也就是list自己的内置,来加锁。

              -
              //使用客户端加锁实现
              @ThreadSafe
              public class ListHelper<E> {
              public List<E> list = Collections.synchronizedList(new ArrayList<>());

              public boolean putIfAbsent(E x){
              synchronized(list) {
              boolean absent = !list.contains(x);
              if (absent)
              list.add(x);
              return absent;
              }
              }
              }
              - -

              评价

              - - - -

              它非常依赖于其他类的客户端加锁机制。

              - - -

              确实,毕竟你锁被外界拿去用了。

              -

              组合

              - -
              public class ImprovedList<T> implements List<T> {
              //实例封闭
              private final List<T> list;

              public ImprovedList(List<T> list){this.list=list;}

              public synchronized boolean putIfAbsent(T x){
              boolean contains = list.contains(x);
              if (contains)
              list.add(x);
              return !contains;
              }

              public synchronized void clear(){list.clear();}
              //... 按照类似的方式委托List接口其他未实现的方法
              }
              - - - -

              是的,跟synchronizedList非常像

              - - -

              这也就是用的java的监视器模式了

              -

              第五章 基础构建模块

              - -

              保证独立即可委托,从而构建一个线程安全类

              - - -

              同步容器类

              - -

              差不多都是使用的监视器模式。

              -

              同步容器类:Vector、Hashtable、Collections.synchronizedXxx

              -

              同步容器类的问题

              - -

              也就是需要避免两个原子操作之间的非线程安全的时间间隔。

              -
                 public static Object getLast(Vector list){
              //复合操作
              //先检查后执行
              int lastIndex = list.size()-1;
              return list.get(lastIndex);
              }

              public static void deleteLast(Vector list){
              int lastIndex = list.size()-1;
              list.remove(lastIndex);
              }
              - - - -

              它这段话说得非常好。由于Vector这个类本身是线程安全的,因而它可以保证外部任何操作都不会导致该对象因为并发而被破坏。但是,我们用不加锁的复合操作虽然不会破坏Vector,但可能导致不能出现我们想要的结果。

              -

              所以我们必须用锁机制来对此复合操作进行保护:

              -
              public static Object getLast(Vector list){
              synchronized (list) {
              //复合操作
              //先检查后执行
              int lastIndex = list.size()-1;
              return list.get(lastIndex);
              }
              }

              public static void deleteLast(Vector list){
              synchronized (list){
              int lastIndex = list.size()-1;
              list.remove(lastIndex);
              }
              }
              - -

              除此之外,迭代也是一种经典的复合操作。我们可以通过下面这种粗粒度加锁来避免:

              -
              public static void travel(Vector list){
              synchronized (list) {
              for (int i=0;i<list.size();i++){
              //do something
              }
              }
              }
              - - - -

              迭代器与ConcurrentModificationException

              - -

              所以才会引入fail-fast机制。

              - - -
              -

              foreach语法糖内部是通过Iterator来实现的。

              -

              Java 的 foreach 本质

              -
              public void testIterableForEach() {
              List<String> list = new ArrayList<>();
              for (String str : list) {
              System.out.println(str);
              }
              }
              //反编译后:
              public void testIterableForEach() {
              List<String> list = new ArrayList<>();
              Iterator i = list.iterator();
              while(i.hashNext()){
              String str = (String)i.next();
              System.out.println(str);
              }
              }
              -
              - - - - -

              可见同步容器类还是有很多局限性的。

              -

              隐藏迭代器

              有时候,迭代会隐藏起来。要一个个揪出需要加锁的地方是非常麻烦的。

              -
              //隐藏在字符串连接中的迭代操作
              @NotThreadSafe
              public class HiddenIterator {
              private final Set<Integer> set = new HashSet<>();

              public synchronized void add(Integer i){ set.add(i); }
              public synchronized void remove(Integer i){ set.remove(i); }

              public void addTenThings(){
              Random r = new Random();
              for (int i=0; i<10; i++) {
              add(r.nextInt());
              }
              //隐式迭代
              System.out.println("DEBUG: Added ten elements to "+ set);
              }
              }

              - - - - - - - - - -

              并发容器类

              同步容器类的加锁太粗粒度了,导致并发性弱。因而引入并发容器类来解决问题。

              - - -

              ConcurrentHashMap —— HashMap

              -

              CopyOnWriteArrayList —— List

              - - - - -

              BlockingQueue

              - - -

              ConcurrentSkipListMap —— TreeMap

              -

              ConcurrentSkipListSet —— TreeSet

              -

              ConcurrentHashMap

              - -

              使用分段锁来细粒度加锁。

              - - -
              -

              关于ConcurrentHashMap的分段锁:ConcurrentHashMap

              -

              JDK1.7中,ConcurrentHashMap 类所采用的正是分段锁的思想,将 HashMap 进行切割,把 HashMap 中的哈希数组切分成小数组,每个小数组有 n 个 HashEntry 组成,其中小数组继承自ReentrantLock(可重入锁),这个小数组名叫Segment

              -

              JDK1.8 中 ConcurrentHashMap 类取消了 Segment 分段锁,采用 CAS + synchronized 来保证并发安全,数据结构跟 jdk1.8 中 HashMap 结构类似,都是数组 + 链表(当链表长度大于 8 时,链表结构转为红黑二叉树)结构。

              -

              ConcurrentHashMap 中 synchronized 只锁定当前链表或红黑二叉树的首节点,只要节点 hash 不冲突,就不会产生并发,相比 JDK1.7 的 ConcurrentHashMap 效率又提升了 N 倍!

              -
              -
              -

              关于ConcurrentHashMap的弱一致性:ConcurrentHashMap的弱一致性

              -

              get方法是弱一致的,是什么含义?可能你期望往ConcurrentHashMap底层数据结构中加入一个元素后,立马能对get可见,但ConcurrentHashMap并不能如你所愿。换句话说,put操作将一个元素加入到底层数据结构后,get可能在某段时间内还看不到这个元素,若不考虑内存模型,单从代码逻辑上来看,却是应该可以看得到的。

              -
              - - -

              精确值—>估计值

              - - - - -
              -

              关于AQS框架:重大发现,AQS加锁机制竟然跟Synchronized有惊人的相似

              -

              在并发多线程的情况下,为了保证数据安全性,一般我们会对数据进行加锁,通常使用Synchronized或者ReentrantLock同步锁。Synchronized是基于JVM实现,而ReentrantLock是基于Java代码层面实现的,底层是继承的AQS

              -

              AQS全称**AbstractQueuedSynchronizer**,即抽象队列同步器,是一种用来构建锁和同步器的框架。

              -

              我们常见的并发锁ReentrantLockCountDownLatchSemaphoreCyclicBarrier都是基于AQS实现的,所以说不懂AQS实现原理的,就不能说了解Java锁。

              -
              -
              -

              并发容器类不能实现独占访问:

              -

              类似ConcurrentHashMap的并发容器不能采用客户端加锁机制,因为并发容器没有采用synchronized内置锁而大多基于AQS框架(不是独占式的锁),所以使用客户端加锁机制来扩展并发容器的方法是不能实现的。

              -

              所以说不能客户端加锁不是不提倡,而是真的不行【】

              -
              -

              所以最好还是用并发容器类替代同步容器类

              -

              对部分复合操作的支持

              - -

              客户端加锁不能使用,就只能用它提供的东西了。

              -

              CopyOnWriteArrayList

              Copy-On-Write意为“写入时复制”,仅当要修改的时候,才会重新创建一次副本,实现可变性。犹记得第一次接触到这个思想是在操作系统的fork()创建子进程的原理那个地方,那可真是有些惊为天人23333

              - - - - -

              也就是说,COWAL内部维护的base数组是事实不可变的,因而访问它的时候不需要同步。但是,我们事实上需要一个可变的并发容器,那该怎么办呢?解决方法就是每次要修改的时候,直接把base数组换成一个新的数组,就像之前某个例子一样,这样就能实现可变性了。

              -

              与此同时,这样的方法也能保证多线程访问时的内存可见性。

              -

              由COWAL的底层代码:

              -
              //base数组,volatile保证引用一变就可以刷新
              private transient volatile Object[] array;

              final Object[] getArray() {
              return array;
              }

              final void setArray(Object[] a) {
              array = a;
              }

              public boolean add(E e) {
              //获取锁
              final ReentrantLock lock = this.lock;
              lock.lock();
              try {
              //getArray:直接返回base数组的引用
              Object[] elements = getArray();
              int len = elements.length;
              //创建新数组再修改
              Object[] newElements = Arrays.copyOf(elements, len + 1);
              newElements[len] = e;
              //直接改变base数组的引用
              setArray(newElements);
              return true;
              } finally {
              lock.unlock();
              }
              }
              - -

              可知,它保证可见性,是直接修改引用的,并且注意,对原数组的拷贝是浅拷贝的。这样一来,就既不会改变原数组的东西,也能保证可见性的更新迅速了。我的评价是牛逼爆了。

              - - - - -

              阻塞队列和生产者—消费者模式

              基本介绍

              - -

              简直就是为了生产者消费者而生的

              - - - - -

              这两段话说得非常本质,需要有个缓冲队列本质上就是因为处理数据速率的不同,生产者消费者也起到了解耦作用

              - - -

              所以说用有界队列还是更好

              -

              BlockingQueue有多种实现。

              -

              ArrayBlockingQueue和LinkedBlockingQueue是FIFO队列,PriorityBlockingQueue是优先队列,最后还有一个特殊的SynhronousQueue。

              - - - - - - -

              实例:桌面搜索

              - -
              //生产者
              public class FileCrawler implements Runnable {
              private final BlockingDeque<File> fileQueue;
              private final FileFilter fileFilter;
              private final File root;

              public FileCrawler(BlockingDeque<File> fileQueue, FileFilter fileFilter, File root) {
              this.fileQueue = fileQueue;
              this.fileFilter = fileFilter;
              this.root = root;
              }

              @Override
              public void run() {
              try{
              crawl(root);
              }catch (InterruptedException e){
              //中断处理
              Thread.currentThread().interrupt();
              }
              }

              private void crawl(File root) throws InterruptedException{
              File[] entries = root.listFiles(fileFilter);
              if (entries!=null){
              for (File entry:entries){
              //递归打印目录
              if (entry.isDirectory()) crawl(entry);
              else if (!alreadyIndexed(entry)) fileQueue.put(entry);
              }
              }
              }
              }

              //消费者
              public class Indexer implements Runnable{
              private final BlockingDeque<File> queue;

              public Indexer(BlockingDeque<File> q){
              this.queue=q;
              }

              @Override
              public void run() {
              try{
              while(true){
              indexFile(queue.take());
              }
              }catch(InterruptedException e){
              Thread.currentThread().interrupt();
              }
              }
              }
              - - - - - -
              //启动生产者-消费者程序
              public static void startIndexing(File[] roots){
              BlockingDeque<File> queue = new LinkedBlockingDeque<>(BOUND);
              FileFilter filter = new FileFilter() {
              @Override
              public boolean accept(File pathname) {
              return true;
              }
              };

              for (File root : roots){
              new Thread(new FileCrawler(queue,filter,root)).start();
              }
              for (int i=0; i<N_CONSUMERS;i++){
              new Thread(new Indexer((queue))).start();
              }
              }
              - - - -

              串行线程封闭

              - -

              安全发布

              - - - - -

              6666666

              -

              之所以叫“串行”,想必是因为这个过程:发布-转接-放弃访问权是一个串行过程。

              - - - - -

              总而言之,串行线程封闭的具体做法就是,一个线程将一个安全发布的对象的所有权完全转移给另一个线程,保证之后自己不会再使用。这样一来,该对象就相当于被另一个线程封闭了。而如何保证“自己以后不再使用”呢?最简单的方法就是安全发布完这个东西后直接把这个东西给踢出去

              -

              阻塞队列是自动会把这个东西安全发布然后就踢出去的,所以说阻塞队列简化了这个工作。

              - - - - -

              双端队列与工作密取

              - - - - - -

              阻塞方法与中断方法

              阻塞方法

              - -

              当某方法抛出InterruptedException时,表明该方法为阻塞方法,也即这个方法会在执行过程中由于各种原因而被阻塞。如果这个方法被中断,它将会努力提前结束阻塞状态。

              -

              中断方法

              - - - -

              处理InterruptedException的两种选择

              - -

              传递InterruptedException

              - -

              恢复中断

              - -
              -

              此处关于为什么如果代码是Runnable的一部分就不能抛出异常:

              -

              是因为java的异常继承体系。

              -

              在重写的run方法中,我们只能够进行异常的捕获而不能够抛出异常,原因是因为在父类Runnable接口中,run方法没有抛出异常,则实现Runnable的子类就无法抛出异常

              -

              所以实际上是语法层面上不允许。

              -
              - - - - - - -

              同步工具类

              - -

              实现同步的方法:使用同步容器类/并发容器类、使用锁、使用同步工具类

              - - - - -

              闭锁 CountDownLatch

              作用

              - - - - - -

              CountDownLatch

              - -
              //使用CountDownLatch来进行计时测试
              public class TestHarness {
              public long timeTasks(int nThreads,final Runnable task)
              throws InterruptedException{
              //初始化计数器为1
              final CountDownLatch startGate = new CountDownLatch(1);
              final CountDownLatch endGate = new CountDownLatch(nThreads);

              for (int i = 0; i < nThreads; i++){
              Thread t = new Thread(){
              @Override
              public void run() {
              try {
              startGate.await();
              try {
              task.run();
              } finally {
              endGate.countDown();
              }
              } catch (InterruptedException e) {
              throw new RuntimeException(e);
              }
              }
              };
              t.start();
              }
              long start = System.nanoTime();
              //所有线程刷拉拉往下走
              startGate.countDown();
              //等待所有线程结束
              endGate.await();
              long end = System.nanoTime();
              return end-start;
              }

              }
              - - - - - -

              是的,这样测试出来的时间应该更加平均,性能更加准确。

              -

              FutureTask

              - -
              -

              java的future机制原理

              -

              关于Future:

              - - -

              其中get方法是阻塞的。

              -

              获取异步任务执行完后的结果。

              -

              关于FutureTask:

              -

              FutureTask既包含了Future的语义,又包含了Runnable的语义。

              -

              它其实内部封装了一个Runnable Task。调用FutureTask的run,其实本质上就是调用Task的run,只不过要多一些检查和存储结果之类的手续。

              -

              所以说它其实就是通过内部封装一个线程,然后就能获取这个线程运行的状态和运行的结果等等等,这样来实现Future语义的。

              -
              -
              -

              关于Callable

              -

              Runnable里面的run方法是不能传参,也没有返回值的。Callable相当于有返回值的Runnable,也即书中说的“有生成结果的Runnable”。

              -
              - - - - -
              public class Preloader {
              private final FutureTask<ProductInfo> future =
              new FutureTask<ProductInfo>(new Callable<ProductInfo>() {
              @Override
              public ProductInfo call() throws Exception {
              return loadProductInfo();
              }
              });
              private final Thread thread = new Thread(future);

              public void start(){thread.start();}

              public ProductInfo get()
              throws DataLoadException,InterruptedException{
              try {
              //阻塞
              return future.get();
              } catch (ExecutionException e) {
              Throwable cause = e.getCause();
              if (cause instanceof DataLoadException)
              throw (DataLoadException) cause;
              else
              throw launderThrowable(cause);
              }
              }

              public static RuntimeException launderThrowable(Throwable t){
              if (t instanceof RuntimeException)
              return (RuntimeException) t;
              else if (t instanceof Error) {
              throw (Error) t;
              }
              else
              throw new IllegalStateException("Not checked.",t);
              }
              }
              - - - -

              也就是调用start线程启动后,可以就去做别的事情,回来就可以拿到结果了,通过这样实现异步调用。

              - - - - -

              这里其实是在讲异常的事了。可以给我们一个启发式思路:

              -

              Callable抛出的Exception这种抽象的异常集合该如何分解处理:首先分解出受检查的异常【也就是说我们调用该方法就知道该方法可能会抛出的异常】,然后针对其他未检查异常,再进行处理。此例中是把这些未检查异常分成了Error和RuntimeException。

              -

              信号量 Semaphore

              - -
              -

              注意

              - -
              -

              这种情况下,其实信号量跟BlockingQueue语义十分近似:

              - - -

              信号量还可以用来将非阻塞容器包装为有界阻塞容器

              - - - - -
              //使用信号量为容器设置边界,有界+阻塞
              public class BoundedHashSet <T>{
              //baked collection,同步容器
              private final Set<T> set;
              //信号量
              private final Semaphore sem;

              public BoundedHashSet(int bound){
              //同步容器类
              this.set = Collections.synchronizedSet(new HashSet<>());
              //初始化许可数
              sem = new Semaphore(bound);
              }

              //阻塞方法
              public boolean add(T e) throws InterruptedException{
              //获取许可
              sem.acquire();
              boolean wasAdded = false;
              try {
              //由于是同步容器类,故而不用使用锁来保护状态
              wasAdded=set.add(e);
              return wasAdded;
              } finally {
              //try捕获异常/正常return后,finally语句都会执行。
              if (!wasAdded)
              sem.release();
              }
              }

              public boolean remove(Object o){
              boolean wasRemoved = set.remove(o);
              if (wasRemoved)
              sem.release();
              return wasRemoved;
              }
              }
              - -
              -

              注:try语句块正常return后,finally语句依然会执行:

              -
              public class Main{
              public static void main(String[] args) {
              System.out.println(haha());
              }

              public static int haha(){
              int number = 10;
              try{
              System.out.println("main!");
              return number++;
              }finally {
              System.out.println("come!"+number);
              }
              }
              }
              /*输出结果
              main!
              come!11
              10*/
              -
              -

              栅栏 Barrier

              - -

              闭锁是某个事件发生后所有线程才能继续执行;栅栏是所有线程都在同样位置等待才能继续执行。

              -

              CyclicBarrier

              - - - -

              一个线程寄了,其他所有等待线程都会死。

              -

              栅栏我觉得一个很重要的点就是保证并发安全。

              - - -

              常常出现那种需要等待所有线程都完成某一步操作才能进行下一步操作的情况,所以栅栏不得不说非常实用。

              -
              public class CellularAutomata {
              //细胞板
              private final Board mainBoard;
              //栅栏
              private final CyclicBarrier barrier;
              //计算线程
              private final Worker[] workers;

              public CellularAutomata(Board board){
              this.mainBoard = board;
              //所有可调度的CPU都被拉过来了
              int count = Runtime.getRuntime().availableProcessors();
              //当所有线程到达栅栏后,马上执行该run方法:提交计算得出的新值
              this.barrier = new CyclicBarrier(count,
              new Runnable() {
              @Override
              public void run() {
              mainBoard.commitNewValues();
              }
              });
              //创建工作线程池
              this.workers = new Worker[count];
              for (int i = 0; i < count; i++)
              //分治,把大的细胞板划分成多个小细胞板处理
              workers[i] = new Worker(mainBoard,getSubBoard(count,i));
              }

              private class Worker implements Runnable{

              private final Board board;

              public Worker(Board board){this.board = board;}

              @Override
              public void run() {
              while (!board.hasConverged()){
              for (int x = 0; x < board.getMaxX(); x++)
              for (int y = 0; y < board.getMaxY(); y++)
              //为二维细胞板上每个点计算新值
              board.setNewValue(x,y,computeValue(x,y));
              try {
              //计算完之后等待其它线程也计算完
              barrier.await();
              } catch (InterruptedException e) {
              return;
              } catch (BrokenBarrierException e) {
              return;
              }
              }
              }
              }

              public void start(){
              for (int i = 0; i < workers.length; i++)
              new Thread(workers[i]).start();
              mainBoard.waitForConvergence();
              }
              }
              - -
              -

              注:

              -

              的目的

              - -
              -

              Exchanger

              - -

              这个“写满”大概对应着栅栏思想里的“全部到达”

              - - - - -

              示例:构建高效且可伸缩的结果缓存

              - - - -
              public interface Computable <A,V>{
              V compute(A arg) throws InterruptedException;
              }

              public class ExpensiveFunction implements Computable<String, BigInteger>{
              @Override
              public BigInteger compute(String arg) throws InterruptedException {
              //经过长时间的计算后
              return new BigInteger(arg);
              }
              }
              - - - -

              用内置锁对方法进行上锁

              //感受一下cache也是Computable的这个多态运用的巧妙性
              public class Memoizer1<A,V> implements Computable<A,V> {
              private final Map<A,V> cache = new HashMap<>();
              private final Computable<A,V> c;

              public Memoizer1(Computable<A,V> c){
              this.c=c;
              }

              //对整个方法体进行上锁
              @Override
              public synchronized V compute(A arg) throws InterruptedException {
              V result = cache.get(arg);
              if (result == null){
              result = c.compute(arg);
              cache.put(arg,result);
              }
              return result;
              }
              }
              - -

              保证了线程安全,但是可伸缩性极差,而且很有可能变成普通的串行排队计算。

              - - - - -

              使用并发容器类

              public class Memoizer2 <A,V> implements Computable<A,V>{
              //并发map
              private final Map<A,V> cache = new ConcurrentHashMap<>();
              private final Computable<A,V> c;

              public Memoizer2(Computable<A,V> c){this.c = c;}
              @Override
              public V compute(A arg) throws InterruptedException {
              V result = cache.get(arg);
              if (result == null){
              result = c.compute(arg);
              cache.put(arg,result);
              }
              return result;
              }
              }
              - -

              确实加锁的粒度小了【没有把高耗时的compute过程锁上】,但会带来某个值重复计算的问题。

              - - -

              而且这不仅仅是性能损耗问题,还有可能会变成安全隐患。

              - - - - -

              使用FutureTask

              - -
              public class Memoizer3<A,V> implements Computable<A,V> {
              private final Map<A, Future<V>> cache
              = new ConcurrentHashMap<>();
              private final Computable<A,V> c;

              public Memoizer3(Computable<A, V> c) {
              this.c = c;
              }
              @Override
              public V compute(A arg) throws InterruptedException {
              Future<V> f = cache.get(arg);
              if (f == null){
              Callable<V> eval = new Callable<V>() {
              @Override
              public V call() throws Exception {
              return c.compute(arg);
              }
              };
              FutureTask<V> ft = new FutureTask<>(eval);
              f = ft;
              cache.put(arg,ft);
              //没有启动新线程,直接润
              ft.run();
              }
              try {
              return f.get();
              } catch (ExecutionException e) {
              throw new RuntimeException(e);
              }
              }
              }
              - - - -

              这个“判断是否开始”与“判断是否完成”的表述非常有意思。此时cache变成了arg和一个异步任务得到的未来结果的映射,而非arg和结果的映射。从此处也可好好理解感受一下“Future”的语义(一个异步执行的结果)。

              -

              只是它依然没有解决上面所说的问题,还是可能会有两个线程计算同一个值,虽然概率小得多,主要原因是因为它使用了非原子的“先检查后执行”

              - - -

              因而,我们可以通过map提供的putifabsent同步方法来解决这个问题。

              -

              使用FutureTask和putIfAbsent

              public class Memoizer<A,V> implements Computable<A,V> {
              private final Map<A, Future<V>> cache
              = new ConcurrentHashMap<>();
              private final Computable<A,V> c;

              public Memoizer(Computable<A, V> c) {
              this.c = c;
              }
              @Override
              public V compute(A arg) throws InterruptedException {
              Future<V> f = cache.get(arg);
              //一重保险,筛选线程,防止ft对象重复声明销毁
              if (f == null){
              Callable<V> eval = new Callable<V>() {
              @Override
              public V call() throws Exception {
              return c.compute(arg);
              }
              };
              FutureTask<V> ft = new FutureTask<>(eval);
              f = cache.putIfAbsent(arg,ft);
              //二重保险,保障仅一个线程能进入
              //只有那个成功把ft放进map的线程才能进入。
              if (f == null){
              f = ft;
              ft.run();
              }
              }
              try {
              return f.get();
              } catch (ExecutionException e) {
              throw new RuntimeException(e);
              }
              }
              }
              - -

              虽然这个最终版本看起来很完美,但实际上,它还会带来其他的性能问题。

              - - - - - - -

              运用最终方案建立cache

              public class Factorizer implements Servlet{
              private final Computable<BigInteger,BigInteger[]> c =
              new Computable<BigInteger, BigInteger[]>() {
              @Override
              public BigInteger[] compute(BigInteger arg) throws InterruptedException {
              return factor(arg);
              }
              };
              private final Computable<BigInteger,BigInteger[]> cache =
              new Memoizer<>(c);
              public void service(ServletRequest req,ServletResponse resp){
              BigInteger i = extractFromRequest(req);
              try {
              encodeIntoResponse(resp,cache.compute(i));
              } catch (InterruptedException e) {
              encodeError(resp,"factorization interrupted.");
              }
              }
              }
              - - - - - -

              第六章 任务执行

              - - - -

              在线程中执行任务

              - - - - - -

              也就是说,一个请求视为一个任务。这样做是非常reasonable的,因为每个请求间都是独立的。

              -

              串行地执行任务

              调度任务最简单粗暴的就是直接让任务串行执行。

              -
              //串行的web服务器
              public class SingleThreadWebServer {
              public static void main(String[] args) throws IOException {
              ServerSocket socket = new ServerSocket(80);
              while(true){
              Socket connection = socket.accept();
              handleRequest(connection);
              }
              }
              }
              - - - -

              这里有一个点非常棒:网络也是IO,也会造成阻塞。

              -

              为每一个任务都创建一个线程

              public class SingleThreadWebServer {
              public static void main(String[] args) throws IOException {
              ServerSocket socket = new ServerSocket(80);
              while(true){
              Socket connection = socket.accept();
              new Thread(new Runnable() {
              @Override
              public void run() {
              handleRequest(connection);
              }
              }).start();
              }
              }
              }
              - - - -

              但是其实这种方式是不好的,因为它无限制地创建线程,这听起来就很容易寄,要知道,高并发的服务器可能会一次解决几千万个请求【当然不知道有没有那么多hhh】,每个都创建一个线程的话,很容易爆内存,而且还会有很大的性能开销。

              - - - - - - -

              而且这样的话,要是高并发情况下,服务器会马上崩溃,做不到我们之前说的自我调节功能。

              -

              所以,我们应该为系统能创建的线程数做一个限制。如此,便引出了Executor框架。

              -

              Executor框架

              - -
              /*
              An object that executes submitted Runnable tasks.
              An Executor is normally used instead of explicitly creating threads.
              此接口提供了一种将任务的提交与每个任务将如何运行的机制解耦的方法,包括线程使用、调度等的详细信息。
              The Executor implementations provided in this package implement ExecutorService, which is a more extensive interface. The ThreadPoolExecutor class provides an extensible thread pool implementation. The Executors class provides convenient factory methods for these Executors.
              */
              public interface Executor {
              void execute(Runnable command);
              }
              - - - - - - - - - -

              示例:基于Executor线程池的Web服务器

              public class TaskExecutionWebServer {
              private static final int NTHREADS = 100;
              //工厂方法创建线程池
              private static final Executor exec
              = Executors.newFixedThreadPool(NTHREADS);

              public static void main(String[] args) throws IOException {
              ServerSocket socket = new ServerSocket(80);
              while (true){
              final Socket connection = socket.accept();
              //每次提交一个任务,自然会在某个时刻调度执行
              exec.execute(new Runnable() {
              @Override
              public void run() {
              handleRequest(connection);
              }
              });
              }
              }
              }
              - - - - - -

              思考问题

              Executor到底什么原理

              这里感觉绕来绕去的,execute到底是会创建一个线程,还是不会创建一个线程?到底是在调用时就创建线程执行任务,还是会在将来的某一个时刻调度执行任务?它这里说得云里雾里的,我来锐评一下我的看法。

              -

              首先,我认为,它给我们的ExecutorService类的execute应该都仅仅是提交任务,放进任务队列,之所以什么时候执行得看调度情况。【Form java ThreadPoolExecutor.execute: Executes the given task sometime in the future. 】

              -

              而下面那两个类应该都是对execute进行了简单的重写,因而此处execute跟java包里的ExecuteService没有任何关系,调用execute仅相当于调用一个普通的方法。

              -
              Executor到底有什么用
              -

              参考视频:java线程池其实不难,只要搞清楚来龙去脉

              -
              -
              解耦

              Executor作为一个接口,其核心思想便是“解耦”

              -

              如果没有Executor的话,我们要创建并运行一个任务,一般都得这样用:new Thread(....).start(),或者是比如说串行的new Runnable(...).run()。也就是我们将任务的创建和任务的执行都混在一起了。而假定说,如果以后要改变该线程池的执行方式,比如说从单任务单线程的并行改成全部任务都串行或者反之,那么就需要每个地方都改掉。但如果使用Executor框架将任务创建和任务具体执行解耦开来,那么我们就仅需修改任务具体执行了。

              -
              Java的线程管理框架

              JUC(java.util.concurrent)其实就只是分为三个部分。

              - - -
              ThreadPoolExecutor
                -
              1. ExecutorService

                -

                ThreadPoolExecutor继承了该接口。

                -

                是Executor接口的加强版,包含了更多方法,具体为:

                -

                ① 自身生命周期的管理 shutdown、isshutdown等等

                -

                ② 对异步任务的支持 返回Future的submit方法

                -

                ③ 对批处理任务的支持 invokeall

                -
              2. -
              3. 内部原理

                -

                当空闲的线程足够多,直接执行;当线程不够多,进入阻塞队列;当阻塞队列满,使用拒绝策略。

                -

                内部的线程池分为救急线程和核心线程。核心线程一直存在,当阻塞队列和核心线程都不够用,就会新开几个救急线程。

                -
              4. -
              -

              执行策略

              - - - - - -

              线程池

              - - - - - -

              说得非常全面

              - - - - -

              66666

              - - - - -

              Executor的生命周期

              - -

              我们结束executor,可以采取或温和或粗暴的方法:可以让它不接受新的,慢慢执行完全部再结束;也可以让它直接全部结束,管它有没有执行完或者有没有还没被执行,就跟断电一样。

              - - - - - - -
              -

              此处疑问:不应该先shutdown再awaitTermination吗?我百度了,也都是说先shutdown。毕竟awaitTermination方法是阻塞的。

              -
              - - -
              //支持关闭操作的Web服务器
              public class LifecycleWebServer {
              private final ExecutorService exec = Executors.newFixedThreadPool(100);

              public void start() throws IOException {
              ServerSocket socket = new ServerSocket(80);
              //服务器没被关闭就一直接受请求
              while (!exec.isShutdown()){
              try {
              final Socket conn = socket.accept();
              exec.execute(new Runnable() {
              @Override
              public void run() {
              handleRequest(conn);
              }
              });
              } catch (RejectedExecutionException e) {
              if (!exec.isShutdown())
              //异常地拒绝了
              log("task submission rejected.",e);
              }
              }
              }

              public void stop(){exec.shutdown();}

              void handleRequest(Socket connection){
              Request req = readRequest(connection);
              //是否是代表关闭的特定HTTP请求
              if (isShutdownRequest(req))
              stop();
              else
              dispatchRequest(req);
              }
              }
              - - - -

              延迟任务与周期任务

              - -

              Timer类的缺陷

              单线程带来的精确性问题
              - -
              线程泄漏
              - - - - - -
              public class OutOfTime {
              public static void main(String[] args) {
              try {
              Timer timer = new Timer();
              timer.schedule(new ThrowTask(),1);
              Thread.sleep(1000);
              timer.schedule(new ThrowTask(),1);
              Thread.sleep(5000);
              } catch (InterruptedException e) {
              throw new RuntimeException(e);
              }
              }

              static class ThrowTask extends TimerTask{

              @Override
              public void run() {
              throw new RuntimeException();
              }
              }
              }
              - - - -

              找出可利用的并行性

              - -

              所以并发编程最难的其实还是建模,如何从串行中挖掘出并行性。

              - - -

              串行的页面渲染器

              - -

              这个把文字的render和图片的render都归结进图像缓存的统一化思想很有意思。

              - - -
              //串行地渲染页面元素
              public class SingleThreadRenderer {
              void renderPage(CharSequence source){
              renderText(source);
              List<ImageData> imageData = new ArrayList<>();
              for (ImageInfo imageInfo : scanForImageInfo(source)){
              imageData.add(imageInfo.downloadImage());
              }
              for (ImageData data : imageData){
              renderImage(data);
              }
              }
              }
              - -

              显而易见,图像的IO需要耗费大量时间,这段时间内CPU都处于空闲状态,可以说利用率非常低下。

              -

              携带结果的Callable与Future

              Callable

              - - - -

              意思就是Callable比Runnable有时候更灵活,因为Callable可以抛出异常,也可以有返回值。

              -

              Future

              - - - -

              这个Future的说法很棒,只能说比起前面那个含糊的“表示一个异步执行的结果”,这个“任务的生命周期”方法更加醍醐灌顶。

              -
              public interface Future<V> {
              boolean cancel(boolean mayInterruptIfRunning);
              boolean isCancelled();
              boolean isDone();
              V get() throws InterruptedException, ExecutionException;
              V get(long timeout, TimeUnit unit)
              throws InterruptedException, ExecutionException, TimeoutException;
              }
              - -
              @FunctionalInterface
              public interface Callable<V> {
              V call() throws Exception;
              - -

              其中,get的方法取决于任务的状态

              - - - - -

              可以利用返回的Future实例来对任务线程进行管理。

              - - - - -

              Future实现并行渲染

              将要求分解为两个任务:渲染文本和渲染图像。

              - - -
              //使用Future等待图像下载
              public class FutureRenderer {
              private final ExecutorService executor = Executors.newFixedThreadPool(80);

              void renderPage(CharSequence source){
              final List<ImageInfo> imageInfos = scanForImageInfo(source);
              //单独开启下载图像的任务
              Future<List<ImageData>> future = executor.submit(new Callable<List<ImageData>>() {
              @Override
              public List<ImageData> call() throws Exception {
              List<ImageData> result = new ArrayList<>();
              for (ImageInfo imageInfo : imageInfos)
              result.add(imageInfo.downloadImage(source));
              return result;
              }
              });
              //在本线程中执行文字的渲染任务
              renderText(source);

              try {
              //阻塞方法
              List<ImageData> imageData = future.get();
              for (ImageData data : imageData)
              renderImage(data);
              } catch (InterruptedException e) {
              //重新设置线程的中断状态
              Thread.currentThread().interrupt();
              //不需要结果了,因而取消任务
              future.cancel(true);
              } catch (ExecutionException e) {
              throw launderThrowable(e.getCause());
              }
              }
              }
              - - - -

              在异构任务并行化中存在的局限

              - - - - - -

              所以难点还是分解同构任务。

              -

              CompletionServiceExecutor和BlockingQueue

              - -

              CompletionService的思想其实和这个差不多。它主要就是多包装一层,数据结构的管理不用你写,更加方便。

              - - - - - - -

              也就是说ExecutorCompletionService将CompletionService的计算部分交给了传进来的线程池Executor,然后自己管理一个阻塞队列,类似生产者-消费者模式,把线程池里出来的结果放进去。

              -

              使用CompletionService实现页面渲染器

              - -
              public class Renderer {
              //为什么这里的线程池要变成包内外界给的呢?
              private final ExecutorService executor;

              Renderer(ExecutorService executor){this.executor = executor;}

              void renderPage(CharSequence source){
              List<ImageInfo> info = scanForImageInfo(source);
              //传入委托计算的线程池
              CompletionService<ImageData> completionService
              = new ExecutorCompletionService<>(executor);
              //提交任务
              for (final ImageInfo imageInfo : info)
              completionService.submit(new Callable<ImageData>() {
              @Override
              public ImageData call() throws Exception {
              return imageInfo.downloadImage();
              }
              });

              renderText(source);

              try {
              for (int t = 0, n = info.size(); t < n; t++){
              //得到下载结果
              Future<ImageData> f = completionService.take();
              ImageData imageData = f.get();
              renderImage(imageData);
              }
              } catch (InterruptedException e) {
              Thread.currentThread().interrupt();
              } catch (ExecutionException e) {
              throw launderThrowable(e.getCause());
              }
              }
              }
              - - - -

              疑问

              我这里写了一个自己用list来保存Future结果的。不知道为什么这个不行,有待说明。

              -
              public class MyRenderer {
              private final ExecutorService executor = Executors.newFixedThreadPool(30);

              void renderPage(CharSequence source){
              List<Future> res = new ArrayList<>();
              List<ImageInfo> info = scanForImageInfo(source);
              //提交任务
              for (final ImageInfo imageInfo : info){
              res.add(executor.submit(new Callable<Object>() {
              @Override
              public Object call() throws Exception {
              return imageInfo.downloadImage();
              }
              }));
              }

              renderText(source);

              try {
              for (int t = 0, n = info.size(); t < n; t++){
              Future<ImageData> f = res.get(t);
              ImageData imageData = f.get();
              renderImage(imageData);
              }
              } catch (InterruptedException e) {
              Thread.currentThread().interrupt();
              } catch (ExecutionException e) {
              throw launderThrowable(e.getCause());
              }
              }
              }
              - - - -

              为任务设置时限

              - - - - - -
              Page renderPageWithAd() throws InterruptedException{
              long endNanos = System.nanoTime()+TIME_BUDGET;
              //提交下载广告的任务
              Future<Ad> f = exec.submit(new FetchAdTask());
              //在等待广告的同时显示页面
              Page page = renderPageBody();
              Ad ad;

              try {
              //相当于timeleft=TIME_BUDGET-经过的时间,只不过进行了运算的化简
              long timeLeft = endNanos- System.nanoTime();
              ad=f.get(timeLeft, TimeUnit.NANOSECONDS);
              } catch (ExecutionException e) {
              ad = DEFAULT_AD;
              } catch (TimeoutException e) {
              //超时取消
              ad = DEFAULT_AD;
              f.cancel(true);
              }

              page.setAd(ad);
              return page;
              }
              - - - - - -
                 /**
              * Attempts to cancel execution of this task. This attempt will
              * fail if the task has already completed, has already been cancelled,
              * or could not be cancelled for some other reason.
              If successful,
              * and this task has not started when {@code cancel} is called,
              * this task should never run.

              If the task has already started,
              * then the mayInterruptIfRunning parameter determines
              * whether the thread executing this task should be interrupted in
              * an attempt to stop the task.
              */
              /*
              也就是说,如果mayInterruptIfRunning==false,就需要等该任务完成;如果==true,就直接中断
              */
              boolean cancel(boolean mayInterruptIfRunning);
              - - - -

              示例:旅行预定门户网站

              - - - - - -

              也就是说,跟前面的CompletionService的优化目的是一致的,都是为了方便管理这一组future,这也跟我上面写的那个list管理版本是一样的。只不过区别在于,CompletionService还可以共用任务池,因而功能更强。invokeAll用法更简便。

              -
              private class QuoteTask implements Callable<TravelQuote>{
              private final TravelCompany company;
              private final TravelInfo travelInfo;
              //...

              public TravelQuote call() throws Exception{
              //solicit:征求、招揽 quote:报价
              return company.solicitQuote(travelInfo);
              }
              }
              //得到排序的报价表
              public List<TravelQuote> getRankedTravleQuotes(
              ThravelInfo travelInfo, Set<TravelCompany> companies, Comparator<TravelQuote> ranking, long time, TimeUnit unit
              )throws InterruptedException{
              List<QuoteTask> tasks = new ArrayList<>();
              for (TravelCompany company:companies){
              tasks.add(new QuoteTask(company,travelInfo));
              }

              //使用invokeAll,一键定时任务,非常方便
              List<Future<TravelQuote>> futures =
              exec.invokeAll(tasks,time,unit);

              List<TravelQuote> quotes =
              new ArrayList<>(tasks.size());
              Iterator<QuoteTask> taskIterator = tasks.iterator();

              for (Future<TravelQuote> f : futures){
              QuoteTask task = taskIterator.next();
              try {
              //只需调用get就行,不用传时间参数
              quotes.add(f.get());
              } catch (ExecutionException e) {
              quotes.add(task.getFailureQuote(e.getCause()));
              } catch (CancellationException e){
              quotes.add(task.getTimeoutQuote(e));
              }
              }

              //排序
              Collections.sort(quotes,ranking);
              return quotes;

              }
              - - - -

              小结

              - - - - - -

              第七章 取消与关闭

              - -

              中断是个重要概念,也算是老朋友了

              - - - - -

              任务取消

              取消的原因

              - - - - - -

              使用volatile标志取消

              使用方法

              Java并没有提供取消某个线程的安全抢占方法,仅有约定俗成的协作机制。

              -

              比如说,可以设置一个volatile类型的取消标志,并且让线程定期查看该标志。【这是volatile的经典用途】

              -
              public class PrimGenerator implements Runnable{
              private final List<BigInteger> prims
              = new ArrayList<>();
              //使用volatile域保护取消状态
              private volatile boolean cancelled;

              @Override
              public void run() {
              BigInteger p = BigInteger.ONE;
              //任务执行时定期检查取消状态
              while (!cancelled){
              p=p.nextProbablePrime();
              //这里用同步可能是因为下面的getPrim方法使用了同步
              synchronized (this){
              prims.add(p);
              }
              }
              }

              public void cancel(){cancelled = true;}

              public synchronized List<BigInteger> get(){
              return new ArrayList<>(prims);
              }
              }
              - -

              使用实例:

              -
              PrimGenerator generator = new PrimGenerator();
              //使用Executor代替Thread
              ExecutorService exec = Executors.newFixedThreadPool(1);
              exec.execute(generator);

              try {
              Thread.sleep(1000);
              } catch (InterruptedException e) {
              throw new RuntimeException(e);
              } finally {
              generator.cancel();
              }

              System.out.println(generator.get());
              exec.shutdown();
              - - - - - - - -

              缺陷

              - -

              比如下面程序:

              -
              public class BrokenPrimeProducer extends Thread{
              private final BlockingQueue<BigInteger> queue;
              private volatile boolean cancelled = false;

              BrokenPrimeProducer(BlockingQueue<BigInteger> queue){
              this.queue=queue;
              }

              public void run(){
              try {
              BigInteger p = BigInteger.ONE;
              while (! cancelled){
              //如果一直阻塞在这,便永远不会检查cancelled标志
              queue.put(p=p.nextProbablePrime());
              }
              } catch (InterruptedException e) {
              throw new RuntimeException(e);
              }
              }
              public void cancel(){cancelled = true;}
              }
              - - - -

              所以解决方法其实很简单,只要让阻塞状态下我们还能知道要取消任务就行。这靠我们在表层写代码是做不到的,需要用到Java提供的另一种协作机制:线程中断。

              -

              中断

              是什么

              - - - -

              所以说中断其实就是为取消而量身定做的。

              - - -
              public class Thread{
              public void interrupt();
              public boolean isInterrupted();
              public static boolean interrupted();
              }
              - - - - - -

              所以作为上层开发者,我们仅需捕获中断异常即可。

              - - -

              也就是说,打断阻塞状态下的线程会清空中断状态,打断正常状态的线程会保持中断状态。而正常状态的线程如果不对中断状态处理,就会一直保持中断状态然后继续运行,也就是屏蔽中断状态。

              - - -

              具体检查方法还是定期看标记。在看到标记后可以做善后工作再决定停不停。

              - - -

              程序清单5-10:

              - - -

              恢复中断状态的示例:

              - - -

              捕获睡眠时的中断异常,然后重新设置打断标志为true,进入下一次循环时再对标记进行处理。

              - - - - -

              程序示例

              //注意此处继承自Thread
              public class PrimeProducer extends Thread{
              private final BlockingQueue<BigInteger> queue;

              PrimeProducer(BlockingQueue<BigInteger> queue){
              this.queue = queue;
              }

              @Override
              public void run() {
              try {
              BigInteger p = BigInteger.ONE;
              //put本就可以检测中断,为啥还要外层包装一层检测的while呢?书中说是为了提高相应度。
              while(!Thread.currentThread().isInterrupted()){
              queue.put(p = p.nextProbablePrime());
              }
              } catch (InterruptedException e) {
              //此时catch完之后自动退出
              /* 允许线程退出 */
              }
              }

              public void cancel(){interrupt();}

              //这个是我为了方便调试自己加的方法
              public synchronized void get(){
              for(BigInteger i : queue){
              System.out.println(i.toString());
              }
              }
              }
              - -

              测试主函数部分如下:

              -
              PrimeProducer generator = new PrimeProducer(new ArrayBlockingQueue<>(10));
              generator.start();

              try {
              Thread.sleep(1000);
              } catch (InterruptedException e) {
              throw new RuntimeException(e);
              } finally {
              generator.cancel();
              }

              generator.get();
              - - - -

              一个有待解决的疑问

              此处编写主函数运行时,不小心产生了一个错误:Why does the ThreadpoolExecutor code never stop running?

              -
              public static void main(String[] args) {
              PrimeProducer generator = new PrimeProducer(new ArrayBlockingQueue<>(10));
              ExecutorService exec = Executors.newFixedThreadPool(1);
              exec.execute(generator);

              try {
              Thread.sleep(1000);
              } catch (InterruptedException e) {
              throw new RuntimeException(e);
              } finally {
              generator.cancel();
              }

              //generator.get();
              exec.shutdown();
              }
              - -

              这段代码跑起来的最终结果就是进程永远无法终止。至于为什么:

              -

              PrimeProducer类继承自Thread,而execute的参数是一个Runnable。也就是说,Executor会把传进来的这个Thread当成一个Runnable,然后再把它包装成一个新的Thread。所以你的generator里的cancel方法:

              -
              public void cancel(){interrupt();}
              - -

              调用的就不是本线程的中断方法,而是一个全新的毫无关系的线程的中断方法了。

              -

              所以其实应该这么写:

              -
              public static void main(String[] args) {
              PrimeProducer generator = new PrimeProducer(new ArrayBlockingQueue<>(10));
              generator.start();

              try {
              Thread.sleep(1000);
              } catch (InterruptedException e) {
              throw new RuntimeException(e);
              } finally {
              generator.cancel();
              }

              generator.get();
              }
              - -

              但我还是有个奇思妙想。可不可以沿用一开始那个错误的主方法版本,然后修改PrimeProducer类为:

              -
              //此处修改为Runnable
              public class PrimeProducer implements Runnable {
              //......

              //此处修改
              public void cancel(){Thread.currentThread().interrupt();}

              //......
              }
              - -

              结果还是跑不起来,不知道为什么,有待解答。

              -

              中断策略

              - - - - - -

              意思就是,单个的任务是非线程所有者,因为它们是被分配到线程池所有的线程执行的。所以它们不能直接对中断进行处理,需要把中断异常抛给那个目前还不知道是谁的所有者线程,让调用者决定自己该怎么做。

              - - - - - - - - - - - - -

              以下的地方一个字也看不懂,写自己的思考也没什么意义。就附上正确代码模板吧。

              -
              //通过future定时取消任务
              private static final ScheduledExecutorService taskExec =
              Executors.newScheduledThreadPool(10);

              public static void timeRun(Runnable r,
              long timeout, TimeUnit unit)
              throws InterruptedException {
              Future<?> task = taskExec.submit(r);
              try {
              task.get(timeout,unit);
              } catch (ExecutionException e) {
              throw launderThrowable(e.getCause());
              } catch (TimeoutException e) {
              //接下来任务将被取消
              } finally {
              task.cancel(true);
              }
              }
              - - - - - -]]> - - books - - - - 编译原理 - /2023/11/18/compilation_principle/ - 第一章 绪论

              概述

              picture

              -

              可重定位的代码通过linker和loader重定位这部分内容就是在之前那本书学过的。

              -

              picture

              -

              从中,我们也可以看到有语法分析、中间代码的影子。

              -

              picture

              -

              词法分析相当于通过DFA NFA捉出各类符号,形成简单的符号表和token list;语法分析相当于对token list组词成句,判断该句子是否符合语言规则;语义分析相当于对词句进行类型判断和中间代码的生成,获得基本语义。

              -

              编译程序总体结构

              picture

              -

              picture

              -

              语法制导翻译:语义分析和中间代码生成集成到语法分析中

              -

              词法分析

              将结果转化为token的形式。

              -

              picture

              -

              picture

              -

              语法分析

              从token list中识别出各个短语,并且构造语法分析树。

              -

              picture

              -

              picture

              -

              相当于是通过文法来进行归约(自底向上的语法分析),从而判断给定句子是否合法。

              -

              语义分析

              picture

              -
                -
              1. 收集标识符的属性信息,并将其存入符号表
              2. -
              -

              picture

              -

              种属就是比如是函数还是数组之类的。

              -

              picture

              -
                -
              1. 语义检查
              2. -
              -

              picture

              -
                -
              1. 静态绑定

                -

                包括绑定代码相对地址(子程序)、数据相对地址(变量)

                -
              2. -
              -

              中间代码生成

              picture

              -

              picture

              -

              波兰也就是前序遍历二叉树(中左右),逆波兰也就是后序遍历二叉树(左右中)

              -

              picture

              -

              代码优化

              picture

              -
                -
              1. 无关机器

                -

                picture

                -
              2. -
              3. 有关机器

                -

                picture

                -
              4. -
              -

              目标代码生成

              picture

              -

              表格管理

              这也挺好理解,相当于管理符号表吧。

              -

              picture

              -

              错误处理

              picture

              -

              编译程序的组织

              了解了编译程序的基本结构,那么我们就可以想想该怎么实现这个编译器了。

              -

              最直观的想法是,我们有几个步骤就对代码进行多少次扫描:

              -
                -
              1. 首先扫一次,进行词法分析,将所有标识符写入到符号表中,同时进行语法分析,看看有没有错,如果出错了就转到错误处理,没有的话就进行语义分析;(三合一)
              2. -
              3. 然后再针对得出来的语义分析树进行中间代码生成;
              4. -
              5. 再对得出来的中间代码进行代码优化,最后对优化出来的代码进行翻译处理。(二合一)
              6. -
              -

              picture

              -

              picture

              -

              picture

              -

              实现编译器

              picture

              -

              T形图

              picture

              -

              自展

              picture

              -

              也就是说:

              -
                -
              1. P0是汇编语言,可以用来编译C语言子集;(P0:汇编语言,C子集→汇编)
              2. -
              3. P1是机器语言,可以用来把汇编语言翻译为机器语言;(P1:机器语言,汇编→机器)
              4. -
              5. 所以我们就得到了P2,也即一个可以用来编译C语言子集的机器语言程序;(P2:机器语言,C子集→汇编)
              6. -
              7. 然后我们就可以用C语言子集来写C语言编译程序P3,再用P2翻译P3,就可以得到工具P4。(P4:汇编语言,C→汇编)
              8. -
              -

              image-20230912153726618

              -

              帅的。

              -

              移植

              picture

              -

              picture

              -

              本机编译器的利用

              picture

              -

              编译程序的自动生成

              这大概是描述了我们到时候会怎么实现这两个阶段代码。

              -

              不过确实,词法分析可以看作是正则匹配,语法分析可以看作是产生式。

              -

              picture

              -

              picture

              -

              第二章 文法等概念

              image-20231111160656018

              -

              基本概念

                -
              1. 字母表

                -

                picture

                -

                picture

                -

                picture

                -

                picture

                -
              2. -
              3. -

                克林闭包中的每一个元素都称为是字母表Σ上的一个串

                -

                picture

                -

                picture

                -

                picture

                -
              4. -
              -

              文法

              picture

              -

              如果文法用于描述单词,基本符号就是字母;用于描述句子,基本符号就是单词

              -
                -
              1. 文法的形式化定义

                -

                picture

                -

                picture

                -

                由于可以从它们推出其他语法成分,故而称之为非终结符

                -

                picture

                -

                picture

                -

                还真是最大的语法成分

                -
              2. -
              3. 产生式

                -

                picture

                -
              4. -
              5. 符号约定

                -

                picture

                -

                picture

                -

                picture

                -

                文法符号串应该就是指既包含终结符也包含非终结符的,也可能是空串的串。

                -

                注意终结符号串也包括空串。

                -
              6. -
              -

              语言

              picture

              -

              这部分就是要讲怎么看一个串是否满足文法规则,那么我们就需要先从什么样的串是满足文法规则的串开始说起,也即引入“语言”的概念。

              -
                -
              1. 推导与归约

                -

                picture

                -

                然后也分为最左推导和最右推导,对应最右归约和最左归约。

                -

                picture

                -

                故而,如果从开始符号可以推导(派生)出该句子,或者从该句子可以归约到开始符号,那么该句子就是该语言的句子。

                -
              2. -
              3. 句子与句型

                -

                picture

                -

                句型就是可以有非终结符,句子就是只能有终结符

                -
              4. -
              5. 语言

                -

                picture

                -

                文法解决了无穷语言的有穷表示问题。

                -

                picture

                -

                picture

                -

                emm,就是好像没有∩运算

                -

                picture

                -

                有正则那味了

                -
              6. -
              -

              乔姆斯基文法体系

              picture

              -

              picture

              -
                -
              1. 0型

                -

                picture

                -
              2. -
              3. 1型

                -

                picture

                -

                之所以是上下文有关,是因为只有A的上下文为a1和a2时才能替换为β【666666,第一次懂】

                -

                CSG不包含空产生式。

                -
              4. -
              5. 2型

                -

                picture

                -

                左部只能是一个非终结符。

                -
              6. -
              7. 3型

                -

                picture

                -

                产生式右部最多只有一个非终结符,且要在同一侧

                -

                picture

                -

                看起来还能转(是的,自动机教的已经全忘了())

                -
              8. -
              -

              CFG

              正则文法用于判定大多数标识,但是无法判断句子构造

              -
                -
              1. 分析树
              2. -
              -

              picture

              -

              picture

              -

              也就是说,每个句型都有自己对应的分析树。那么接下来就介绍什么是句型的短语

              -

              picture

              -

              意思就是直接短语是高度为2的子树的边缘,直接短语一定是某个产生式的右部,但是产生式右部不一定是给定句型的直接短语(因为有可能给定句型的推导用不到那个产生式)

              -
                -
              1. 二义性文法
              2. -
              -

              picture

              -

              通过自定义规则消除歧义

              -

              picture

              -

              第三章 词法分析

              正则语言

              正则表达式

              picture

              -

              picture

              -

              picture

              -

              picture

              -

              最后两条值得注意

              -

              picture

              -

              正则定义

              picture

              -

              picture

              -

              picture

              -

              有穷自动机

              概述

              picture

              -

              picture

              -

              picture

              -

              picture

              -

              所以真正的终止是输入带到末尾并且指向终态

              -

              分类

              DFA

              picture

              -

              NFA

              picture

              -

              NFA与DFA转化

              picture

              -

              picture

              -

              e-NFA

              picture

              -

              e-NFA与NFA转化

              picture

              -

              词法分析相关

              识别单词的DFA

              数字

              picture

              -

              picture

              -

              66666,还能这么捏起来

              -

              picture

              -

              注释

              picture

              -

              识别token

              picture

              -

              关键字是在识别完标识符之后进行查表识别的

              -

              scanner的错误处理

              说实话没太看懂

              -

              picture

              -

              picture

              -

              picture

              -

              第四章 语法分析

              根据给定文法,识别各类短语,构造分析树。所以关键就是怎么构建分析树

              -

              自顶向下LL(1)

              概念

              可以看做是推导(派生)的过程。
              如果同一非终结符的各个产生式的可选集互不相交,就可以进行确定的自顶向下分析:

              -

              picture

              -

              这两个分析也是我们的分析方法需要解决的。

              -

              picture

              -

              picture

              -

              也就是说,在自顶向下分析时,采用的是最左推导;在自底向上分析时,最左归约和最右推导才是正道!

              -

              通用算法

              例子

              picture

              -

              大概流程应该是,有产生式就展开,然后当产生式右部有多个候选式的时候再根据输入决定。

              -

              递归下降分析

              picture

              -

              如果有多个以输入终结符打头的右部候选,那就需要逐一尝试错了再回溯,因而效率较低。

              -

              预测分析

              picture

              -

              66666,这其实就可以类似于动态规划了吧

              -

              【感觉这里也能窥见一些算法设计的思想。

              -

              仔细想想,我们在引入动态规划时,也是这个说辞:对于一些回溯问题,回溯效率太低,所以我们就可以提前通过动态规划的思想构造一个状态转移表,到时候只需从零开始按照表进行状态转移即可。

              -

              仔细想想,这不就是这里这个预测分析提出的思想吗!真的牛逼,6666

              -

              我记得KMP算法一开始也是这个思想,感觉十分神奇】

              -

              文法转换

              什么情况需要改造

              picture

              -

              picture

              -

              消除左递归

              直接左递归

              picture

              -

              这个左递归及其消除方法解释得很形象

              -

              picture

              -
              间接左递归

              picture

              -

              先转化为直接左递归

              -

              消除回溯

              picture

              -

              666666这个解读可以,感觉这个就跟:

              -

              image-20231111224823978

              -

              这个“向前看”有异曲同工之妙了。

              -

              LL(1)文法

              LL(1)文法才能使用预测分析技术。判断是否是LL文法就得看具有相同左部的产生式的select集是否相交

              -

              S_文法

              picture

              -

              S文法不包含空产生式

              -

              q_文法

              picture

              -

              也就是说,B的Follow集为{b,c},只有当输入符号为b/c时才能使用空产生式

              -

              picture

              -

              first集和follow集不交。

              -

              这下总算知道这两个是什么玩意了。也就是这样:

              -
                -
              1. 输入符号与B的First集元素匹配

                -

                直接用那个产生式

                -
              2. -
              3. 否则,看输入符号是否与Follow集元素匹配

                -
                  -
                1. -

                  若B无空产生式,报错;否则,使用B的空产生式(相当于消了一个符号但不变输入带指针)

                  -
                2. -
                3. -

                  报错

                  -
                4. -
                -
              4. -
              -

              picture

              -

              这个感觉跟first集有点像,相当于是右部只能以终结符开始的形式,所以下面的LL文法会增强定义。

              -

              当该非终结符对应的所有SELECT集不相交,就可以进行确定的自顶向下语法分析。这个思想也将贯穿下面的LL文法

              -

              picture

              -

              LL(1)文法

              picture

              -

              picture

              -

              最后,如果同一非终结符的各个产生式的可选集互不相交,就可以进行确定的自顶向下分析:

              -

              picture

              -

              picture

              -

              总结

              这几个推理下来,真是让人感觉酣畅淋漓!

              -

              确定的自顶向下分析的核心就是,给定一个当前所处的非终结符和一个输入字符[E, a],我们可以唯一确定一个产生式P用于构建语法分析树。

              -

              picture

              -

              也即,同一个非终结符的所有产生式的SELECT集必须是不交的【才能确保选择产生式的唯一性】。因而,问题就转化为了如何让SELECT集不交

              -

              我们需要对空产生式和正常产生式的SELECT集计算做一个分类讨论。

              -
                -
              1. 空产生式

                -

                由于可以推导出空,相当于把该符号啥了去读下一个符号,因此我们的问题就转化为输入字符a是否能够跟该符号后面紧跟着的字符相匹配。而紧跟着的字符集我们将其成为FOLLOW集,如果a在follow集中,那么就可以接受,否则不行。

                -

                对于LL(1)文法,相当于是进一步处理了简介推出空的串:

                -

                ​ 由于α串->*空,则α串必定仅由非终结符构成。那么它能推导出的所有可能即为SELECT集。故而为First(α)∪Follow(α)

                -
              2. -
              3. 非空产生式

                -

                很简单,就是其First集。

                -
              4. -
              -

              故而,只需要让这些计算出来的First集合不交,就能进行确定的自顶向下语法分析,构造确定的语法分析树。不得不说真的牛逼。

              -

              感觉其“预测分析”的“预测”主要体现在对空产生式的处理上。

              -

              总算懂了为什么LL(1)能够解决这个回溯效率太低的问题了,太牛逼。不过问题是怎么转化为LL(1)呢()上面的消除回溯和左递归只是一部分而已吧。

              -

              预测分析法

              picture

              -

              这个消除二义性是啥玩意?二轮的时候看看PPT怎么讲的

              -

              递归的预测分析

              picture

              -

              picture

              -

              66666,它这个计算follow集的方法就很直观

              -

              declistn有个空产生式,那么我们看得看②,而②的declistn排在最后,也就是说declistn的follow集就是其左部declist的follow集【6666】,所以我们看①,可以发现declist后面为:。

              -

              picture

              -

              如果是终结符,就直接==比较;非终结符,就把token传入到其对应的过程。

              -

              非递归的预测分析

              picture

              -

              66666

              -

              感觉从中又能窥见动态规划的同样思想了。下推自动机其实感觉就像是递归思想(或者说顺序模拟递归,因为它甚至有一个栈,出栈相当于达成条件递归return),动态规划的话可能有点像是把每个不同状态以及不同状态时的栈顶元素整成一个2x2的表,所以感觉思想类似。

              -

              picture

              -

              注意,是栈顶跟输入一样都是非终结符才会移动指针和出栈

              -

              值得注意的是,输出的产生式序列就对应了一个最左推导。

              -

              picture

              -

              picture

              -

              错误处理

              picture

              -

              picture

              -

              picture

              -

              其实也挺有道理,栈顶是非终结符,但是输入是它的follow集,那我们自然而然可以想到把这b赶跑,看看下面有没有真的它的follow集在嗷嗷待哺。

              -

              自底向上语法分析

              概述

              正确识别句柄是一个关键问题。

              -

              句柄:当前句型的最左直接短语。【最左、子树高度为2】

              -

              自底向上

              picture

              -

              picture

              -

              每次句柄形成就将它归约,因而保证一直是最左归约(recall that,句柄一定是某个产生式的右部,并且每次最左句柄一旦形成就归约)

              -

              picture

              -

              正如上面的LL分析,每次推导要选择哪个产生式是一个问题;这里的LR分析,每次归约要选择哪个产生式,也即正确识别句柄,也是一个关键问题。

              -

              所以,我们应该把句柄定义为当前句型的最左直接短语。

              -

              如下图所示,左下角是当前句型(画红线部分)的语法分析树,红字为在栈中的部分,蓝字为输入符号串剩余部分。当前句型的直接短语(相当于根节点的高度为二的子树,或者说子树前两层)有两个,一个是以<IDS>为根节点的<IDS> , iB,另一个是<T>为根节点的real

              -

              picture

              -

              而LR分析技术的核心就是正确地识别了句柄

              -

              LR文法

              picture

              -

              也就是说LR技术就是用来识别句柄的,识别完了句柄就可以构建类似自顶向下的预测分析那样的自动机表来进行转移。

              -

              picture

              -
                -
              1. 移进状态

                -

                ·后为终结符

                -
              2. -
              3. 待约状态

                -

                ·后为非终结符

                -
              4. -
              5. 归约状态

                -

                ·后为空

                -
              6. -
              -

              picture

              -

              picture

              -

              以前感觉一直很难理解GOTO表的作用,现在感觉稍微明白了点了,你想想,归约之后的那个结果是不是有可能是另一个产生式的右部成分之一,也即一个新的句柄?并且这个也是由你栈顶刚归约好的那个左部和下面的输入符号决定的。那么你自然而然需要切换一下当前状态,以便之后遇到那个产生式的时候能发现到了。

              -

              那么,剩下的问题就是如何构造LR分析表了:

              -

              picture

              -

              算符分析

              picture

              -

              也就是它会整一个终结符之间的优先级关系。。。

              -

              picture

              -

              picture

              -

              也就是说:

              -
                -
              1. a=b

                -

                相邻

                -
              2. -
              3. a<b

                -

                也即在A->aB时,b在FIRSTOP(B)中(理解一下,这个First指在前面。。。)

                -
              4. -
              5. a>b

                -

                也即在A->Bb时,a在LASTOP(B)中(理解一下,这个LAST指在后面。。。)

                -
              6. -
              -

              picture

              -

              picture

              -

              我服了

              -

              picture

              -

              picture

              -

              好像#这个固定都是,横的为左,竖的为右

              -

              picture

              -

              根据优先关系来判断移入和归约

              -

              picture

              -

              LR分析

              LR(0)

              每个分析方法其实都对应着一种构造LR分析表的方法。
              LR(0)通过构造规范LR0项集族,从而构造LR分析表,从而构造LR0 DFA来最终进行语法分析。

              -

              每一个项目都对应着句柄识别的一个状态。

              -

              picture

              -

              picture

              -

              picture

              -

              picture

              -

              而肯定不可能整那么多个状态,所以我们需要进行状态合并。(这样也就很容易理解LR的状态族构建了。)

              -

              picture

              -

              它这里也很直观解释了为什么点遇到非终结符就需要加入其对应的所有产生式,因为在等待该非终结符就相当于在等待它的对应产生式的第一个字母。

              -

              picture

              -

              picture

              -

              上面这东西就是这个所谓的规范LR(0)项集族了。

              -

              picture

              -

              picture

              -

              但是会产生移进归约冲突:

              -

              picture

              -

              picture

              -

              还有归约归约冲突:

              -

              picture

              -

              所以我们就把没有冲突的叫LR(0)文法。

              -

              image-20231112165527201

              -

              感觉上述两个问题都是因为有公共前缀【包括空产生式勉强也能算是这个情况】,导致信息不足无法判断应该怎么做,多读入一个字符(也即LR(1))应该可以有效解决该问题。

              -

              SLR分析

              其实本质还是识别句柄问题,也即此时是归约还是移入,得看是不是句柄。故而LR0信息已经不能帮我们识别句柄了。

              -

              picture

              -

              Follow集可以帮助我们判断。由该状态I2可知,输入一个*应该跳转到I7。如果在I2把T归约为一个E,由Follow集可知E后面不可能有一个*,也就说明在这里进行归约是错误的,应该进行移入。

              -

              这种依靠Follow集和下一个符号判断的思想,就会运用在SLR分析中。

              -

              picture

              -

              picture

              -

              picture

              -

              但值得注意的是SLR分析的条件还是相对更严苛,它要求移进项目和归约项目的Follow集不相交,所以它也会产生像下图这样的冲突:

              -

              picture

              -

              LR(1)

              picture

              -

              SLR将子集扩大到了全集,显然进行了概念扩大。

              -

              含义为只有当下一个输入符号是XX时,才能运用这个产生式归约。这个XX是产生式左部非终结符的Follow子集。

              -

              picture

              -

              这玩意只有归约时会用到,这个很显然,毕竟前面提到的LR0的问题就是归约冲突。

              -

              picture

              -

              对了,值得注意的是这个FIRST(βa),它表示的并不是FIRST(a)∪FIRST(β),里面的βa应该取连接意,也即,当β为非空时这玩意等于FIRST(β),当β空时这玩意等于FIRST(a)

              -

              picture

              -

              刚刚老师对着这个状态转移图进行了一番强大的看图写话操作,我感觉还是十分地牛逼。她从这个图触发,讲述了状态I2为什么不能对R->L进行归约。

              -

              假如我们进行了归约,那么我们就需要弹出状态I2回到I0,压入符号R,I0遇到符号R进入了I3,I3继续归约回到I0,I0遇到符号S到状态I1,但1是接收状态,下一个符号是=不是$,所以错了。

              -

              picture

              -

              picture

              -

              比如说I8和I10就是同心的。左边的那个实际上是LR0项目集,所以这里的心指的是LR0。

              -

              picture

              -

              LALR分析

              然而,LR(1)会导致状态急剧膨胀,影响效率,所以又提出了个LALR分析。

              -

              picture

              -

              picture

              -

              跟前面的SLR对比可以发现,相当于它就是多了个逗号后面的条件。但是这是可以瞎合的吗?不会出啥问题不。。。

              -

              picture

              -

              好吧问题这就来了,LALR可能会产生归约归约冲突。但值得注意的是,它不可能出现归约移入冲突,因为LR1没有这个东西,而LALR只是修改右边的符号,所以也不会有这个。

              -

              picture

              -

              因为LALR实际上是合并了展望符集合,这东西与移进没有关系,所以只会影响归约,不会影响移进。

              -

              picture

              -

              LALR可能会产生归约归约冲突。但值得注意的是,它不可能出现归约移入冲突,因为LR1没有这个东西,而LALR只是修改右边的符号,所以也不会有这个。

              -

              它有可能做多余的归约动作,从而推迟错误的发现

              -

              形式上与LR1相同;大小上与LR0/SLR相当;分析能力介于SLR和LR1之间;展望集仍为Follow集的子集。

              -

              总结

              感觉一路看下来,思路还是很流畅的。LR0会产生归约移进冲突和归约归约冲突,所以我们在归约时根据下一个符号是在移进符号还是在Follow集中来判断是要归约还是要移进。但是SLR条件严苛,对于那些移进符号集和Follow集有交的不适用,并且这种情况其实很普遍。加之,出于这个motivation:其实不应该用整个Follow集判断,而是应该用其真子集,所以我们开发出来个LR1文法。然后LR1文法虽然效果好但是状态太多了,所以我们再次折中一下,造出来个效果没有那么好但是状态少的LALR文法。

              -

              二义性文法的LR

              picture

              -

              所以我们可以用LR对二义性文法进行分析

              -

              我们可以通过自定义规则来消除二义性文法的归约移入冲突

              -

              picture

              -

              对于状态7,此时输入+ or *会面临归约移入冲突。由于有E->E+E归约式子,可以知道此时栈中为E+E。当输入*,由于*运算优先级更高,所以我们在此时进行移入动作转移到I5;当输入+,由于同运算先执行左结合,所以我们此时可以安全归约。

              -

              对于状态8,由于*运算比+优先级高,且左结合,所以始终进行归约。

              -

              picture

              -

              picture

              -

              picture

              -

              picture

              -

              错误检测

              picture

              -

              picture

              -

              它这个意思大概就是,符号栈和状态栈都一直pop,直到pop到一个状态,GOTO[符号栈顶,状态栈顶]有值【注意,始终保持符号栈元素+1 == 状态栈元素数+1】。然后,一直不断丢弃输入符号,直到输入符号在A的Follow集中。此时,就将GOTO值压入栈中继续分析。

              -

              【这其实也很有道理。如果输入符号在A的Follow集,说明A之后很有可能可以消耗这个输入符号。】

              -

              picture

              -

              picture

              -

              第五章 语义分析

              注意:

              -
                -
              1. 语义翻译包含语义分析和中间代码生成
              2. -
              3. 这笔包含了语法分析、语义分析、中间代码生成
              4. -
              -

              思想:

              -
                -
              1. 通过为文法符号设置语义属性,来表达语义信息
              2. -
              3. 通过与产生式(语法规则)相关联的语义规则来计算符号的语义属性值
              4. -
              -

              也可能是先入为主吧,感觉用实验的方法来理解语义分析比较便利。语义分析相当于定义一连串事件,附加在每个产生式上。当该产生式进行归约的时候,就执行对应的语义事件。而由于执行语义分析时需要的符号在语法分析栈中,所以我们也同样需要维护一个语义分析栈,在移进时也需要进栈。

              -

              SDD/SDT概念

              语义分析一般与语法分析一同实现,这一技术成为语法制导翻译。

              -

              picture

              -

              picture

              -

              picture

              -

              SDD

              picture

              -

              可以回忆一下实验,相当于对每个产生式进行一个switch-case,然后依照产生式的类别和代码规则进行出栈入栈来计算属性值。

              -

              SDT

              picture

              -

              picture

              -

              SDD

              picture

              -

              概念

              一个很简单区分综合属性和继承属性的方法,就是如果定义的是产生式左部的属性,那就是综合属性;右部,那就是继承属性

              -

              综合属性

              picture

              -

              picture

              -

              继承属性

              picture

              -

              picture

              -

              这个东西就是我们实验里写的,副作用也是更新符号表。

              -

              属性文法

              没有副作用的SDD称为属性文法。

              -

              求值顺序

              picture

              -

              而感觉语法分析这个过程的产生式归约顺序就能一定程度上表示了这个求值顺序

              -

              picture

              -
                -
              1. 继承属性放在结点左边,综合属性放在结点右边
              2. -
              3. 如果属性值A依赖于属性值B,那么就有一条从B到A的箭头【B决定A】
              4. -
              5. 对于副作用,我们将其看作一个虚综合属性【注意是综合的,虽然它看起来既由兄弟结点决定也由子节点决定】
              6. -
              7. 可行的求值序列就是拓扑排序
              8. -
              -

              picture

              -

              蛤?这不是你自己规则设计有问题吗,关我屁事

              -

              picture

              -

              其实我还是不大理解,因为这个规则不是user定义的吗?所以产生环不也是它的事,难道说自顶向下或者自底向上分析还能优化SDD定义??

              -

              感觉它意思应该是这样的,有一个方法能绝对不产生循环依赖环,也即将自底向上/自顶向下语法分析与语义分析结合的这个方法。这个方法就是它说的真子集。

              -

              所以我们接下来要研究的就是什么样的语义分析可以用自顶向下or自底向上语法分析一起制导。

              -

              S-SDD

              picture

              -

              那确实,你自底向上想要计算继承属性好像也不大可能

              -

              L-SDD

              picture

              -

              picture

              -

              对应了自顶向下的最左推导顺序

              -

              S-SDD包含于L-SDD

              -

              picture

              -

              SDT

              picture

              -

              S-SDD -> SDT

              picture

              -

              picture

              -

              当归约发生时执行对应的语义动作

              -

              picture

              -

              还需要加个属性栈

              -

              picture

              -

              所以S-SDD+自底向上其实很简单,因为只需在归约的时候进行语义分析,在移进的时候push进属性栈就行了。

              -

              picture

              -

              具体的S-SDD结合语法分析的分析过程可以看视频

              -

              这个例子还算简单的,毕竟只是综合属性的计算而已,只需要加个属性栈,保存值就行了。

              -

              picture

              -

              我们可以来关注一下这个SDT的设计,也很简单。可以产生式和语义规则分离看待,这也给我们以后设计提供一定的启发。

              -

              L-SDD -> SDT

              picture

              -

              picture

              -

              picture

              -

              非递归的预测分析

              picture

              -

              picture

              -

              这个是自顶向下的语法分析,本来只用一个栈就行了,现在需要进行扩展。T的综合属性存放在它的右边,继承属性存放在它的平行位置。

              -

              当属性值还没计算完时,不能出栈;当综合记录出栈时,它要将属性值借由语义动作复制给特定属性。

              -

              picture

              -

              然后语义动作也得一起进栈。

              -

              image-20231117015114181

              -

              digit是终结符,只有词法分析器提供值

              -

              此时,digit跟一个语义动作关联,所以我们需要把它的值复制给它关联的这个语义动作{a6},然后才能出栈。

              -image-20231117015317921 - -
              -

              关联的另一个实例:

              -

              image-20231117015508123

              -

              此时由于T’.inh还要被a3用到,所以我们就得在T’出栈前把它的这个inh值复制给a3。

              -
              -

              当遇到语义动作之后,就执行动作,并且出栈语义动作。

              -

              picture

              -

              它这意思应该是遇到每个产生式的每个符号要执行什么动作都是确定的,所以代码实现是可能的。

              -

              可以看到:

              -
                -
              1. 语义动作代码就是执行
              2. -
              3. 综合属性代码就是赋给关联语义动作
              4. -
              5. 非终结符就是选一个它作为左部的产生式,然后看看要不要用到它自身的属性对右部子属性进行复制(体现了继承属性)
              6. -
              -

              递归的预测分析

              picture

              -

              666666666

              -

              感觉这个值得深思,但反正现在的我思不出啥了。。。

              -

              picture

              -

              picture

              -

              LR分析

              picture

              -

              picture

              -

              相当于把L-SDD转化为了个S-SDD。具体是这样,把原式子右边的变量替换为marker的继承属性,结果替换为marker的综合属性。那么新符号继承属性怎么算啊。。。不用担心,因为观察可知要使用的这两个非终结符一定已经在栈中了。

              -

              具体分析也看视频就好了。

              -

              第六章 中间代码生成

              中间代码的形式

              picture

              -

              逆波兰(后缀)

              picture

              -

              三地址码

              picture

              -

              picture

              -

              picture

              -

              picture

              -

              false list就是if失败后的那个goto序号,true list是成功的那个goto序号,s.nextline是整个if的下一条指令

              -

              picture

              -

              四元式

              picture

              -

              picture

              -

              picture

              -

              增量生成

              -

              DAG图

              picture

              -

              picture

              -

              声明语句

              类型表达式

              picture

              -

              一般声明

              非嵌套

              picture

              -

              picture

              -

              picture

              -

              picture

              -

              嵌套

              picture

              -

              picture

              -

              它这个相当于是把符号表和offset都整成了一个栈,毕竟确实过程调用就是得用栈结构的

              -

              picture

              -

              picture

              -

              记录

              picture

              -

              picture

              -

              之后用到该记录类型,就指向记录符号表即可。

              -

              picture

              -

              简单赋值语句

              定义

              这个就不用填符号表了,所以helper function都是用来产生中间代码的

              -

              picture

              -

              addr属性需要从符号表中获取

              -

              picture

              -

              临时变量处理

              picture

              -

              数组元素寻址

              picture

              -

              picture

              -

              picture

              -

              picture

              -

              picture

              -

              picture

              -

              picture

              -

              类型检查

              规则

              看个乐吧

              -

              picture

              -

              类型转换

              picture

              -

              picture

              -

              在语义动作中实现

              -

              控制流语句

              简单控制流

              picture

              -

              picture

              -

              反正意思就是用S.next这个继承属性来表示S.code执行完后的下一个三地址码地址。

              -

              picture

              -

              if-then

              picture

              -

              if-then-else

              picture

              -

              while-do

              picture

              -

              ;

              其实不大懂这什么玩意

              -

              picture

              -

              picture

              -

              picture

              -

              抽象

              -

              picture

              -

              picture

              -

              picture

              -

              布尔表达式

              布尔表达式翻译

              基本

              picture

              -

              picture

              -
              数值表示

              picture

              -

              picture

              -

              picture

              -
              控制流表示

              picture

              -

              picture

              -

              picture

              -

              picture

              -

              混合模式布尔表达式

              picture

              -

              picture

              -

              picture

              -

              回填

              基本

              picture

              -

              picture

              -

              picture

              -

              这两个都是综合属性

              -

              相当于是一个waiting list

              -
              布尔表达式的回填

              picture

              -

              可以理解为,B这个表达式可以分为两种情况,两种情况有一个为真B就为真。那么,B的真回填list相当于也被分为了两种情况,所以要求B的就是把它们合起来。

              -

              picture

              -

              picture

              -

              picture

              -

              picture

              -

              picture

              -

              原来回填是这个意思

              -
              控制流结构的回填

              nextline是一个综合属性

              -
              if-then

              picture

              -
              if-then-else

              picture

              -
              while-do

              picture

              -
              sequence

              picture

              -
              for

              picture

              -

              picture

              -
              repeat

              picture

              -
              switch-case

              TODO 这笔之后再看。。。。

              -

              picture

              -

              picture

              -

              picture

              -

              过程调用

              picture

              -

              picture

              -

              picture

              -

              输入输出语句

              TODO

              -

              picture

              -

              picture

              -

              题型1 四元序列

              picture

              -

              第七章 运行存储分配

              概念

              存储组织

              活动记录

              picture

              -

              picture

              -

              picture

              -

              picture

              -

              静态/动态链

              picture

              -

              静态链也被称作访问链,用于访问存放于其他活动记录中的非局部数据。

              -

              动态链也被称作控制链,用于指向调用者的活动记录。

              -

              picture

              -

              picture

              -

              内存对齐

              picture

              -

              picture

              -

              作用域

              picture

              -

              picture

              -

              传参方式

              传值

              picture

              -

              传地址

              picture

              -

              传值结果

              picture

              -

              反正意思就是既要得到原来的A,又要修改A

              -

              传名

              picture

              -

              picture

              -

              静态存储分配

              picture

              -

              picture

              -

              顺序分配法

              picture

              -

              层次分配法

              picture

              -

              栈式存储分配

              概念

              picture

              -

              picture

              -

              也就是说左边及其所有子树全调完了,才能调下一个兄弟的。

              -

              picture

              -

              picture

              -

              image-20231114154150835

              -

              左边这几点设计规则都十分reasonable,很值得注意。

              -

              不过我其实挺好奇,参数存在那么后面该咋访问。。。。看xv6,似乎是fp指向前面,sp才指向local,也即用了两个栈指针。

              -

              这个控制链也是约定俗成的,具体可以想起来xv6也是类似结构:

              -

              picture

              -

              当函数返回的时候,就会进行恢复现场,从而出栈一直到ra,很合理。

              -

              调用/返回序列

              是什么

              picture

              -

              调用序列应该就是设置参数、填写栈帧一类,返回序列就是恢复现场

              -

              picture

              -

              picture

              -

              picture

              -

              picture

              -

              picture

              -

              生成代码

              picture

              -
              调用序列

              传变量、改变meta data、改变top和sp指针

              -

              picture

              -

              picture

              -
              返回序列

              picture

              -

              变长数据

              picture

              -

              这段解释了下为什么不用堆,说得很好

              -

              picture

              -

              缺点

              picture

              -

              第二点,比如malloc后不free

              -

              栈中非局部数据的访问

              picture

              -

              有过程嵌套

              picture

              -

              静态作用域

              访问链

              picture

              -

              picture

              -

              picture

              -

              picture

              -
              建立访问链

              picture

              -

              picture

              -

              picture

              -
              过程参数的访问链

              picture

              -

              picture

              -

              Display表

              通俗解释

              每一个嵌套深度的分配一个Display位

              -

              S嵌套深度1,所以占据d[1];Y和X嵌套深度2,所以占据d[2];Z嵌套深度3,所以占据d[3]。

              -

              然后,一开始遇到个S,d1指向S;然后调用Y,d2指向Y;然后Y中调用X,就修改d2指向X;然后调用Z,就修改d3指向Z。

              -

              总之显示栈就是这个变换指针的过程。

              -

              至于控制栈,要打印这里面的display表,就是看层数。如果d1那就打印当前层,d2就打印的12层,d3就123层【不是纯显示栈,是它自己内部的未变换指针的结果】

              -

              picture

              -

              picture

              -

              picture

              -

              结果:SXZ

              -
              定义

              picture

              -

              picture

              -

              picture

              -
              访问流程

              picture

              -

              picture

              -

              picture

              -
              生成代码

              picture

              -

              picture

              -

              picture

              -

              picture

              -

              动态作用域

              静态作用域是空间上就近原则,动态是时间上。

              -

              picture

              -

              picture

              -

              无过程嵌套

              picture

              -

              picture

              -

              也就是说这时候非局部的一定是全局变量或者静态的局部变量。

              -

              堆管理

              picture

              -

              内存管理器

              局部性

              picture

              -

              堆分配算法

              人工回收请求

              符号表

              如题

              picture

              -

              picture

              -

              如果是支持过程声明嵌套,顺着符号表就可以找到其父过程/子过程的数据。

              -

              符号表也可以用于构造访问链,因为过程名也是一种符号。

              -

              picture

              -

              符号表的建立

              picture

              -

              第九章 代码生成

              概述

              picture

              -

              目标代码形式

              picture

              -

              指令选择

              picture

              -

              寄存器分配

              picture

              -

              计算顺序选择

              picture

              -

              不讨论这个

              -

              目标语言

              定义

              picture

              -

              指令开销

              picture

              -

              picture

              -

              picture

              -

              picture

              -

              picture

              -

              运行时刻地址

              简单的代码生成器

              后续引用信息

              picture

              -

              picture

              -

              寄存器与地址描述符

              picture

              -

              代码生成算法

              picture

              -

              picture

              -

              picture

              -

              picture

              -

              picture

              -

              窥孔优化

              picture

              -

              冗余指令消除

              picture

              -

              不可达代码消除

              picture

              -

              强度削弱

              picture

              -

              特殊机器指令使用

              picture

              -

              寄存器分配指派

              picture

              -

              全局寄存器分配

              picture

              -

              引用计数

              picture

              -

              picture

              -

              picture

              -

              picture

              -

              所以这东西是用来决策寄存器分配的

              -

              外层循环的寄存器指派

              picture

              -

              picture

              -

              反正类似保护现场恢复现场

              -

              拓展阅读

              AC自动机

              在思考自动机和动态规划的关系时,胡乱搜索看到了AC自动机,于是来了解了一下。

              -
              -

              算法学习笔记(89): AC自动机 - Pecco的文章 - 知乎

              -
              -
              -

              考虑一个问题:给出若干个模式串,如何构建一个DFA,接受所有以任一模式串结尾(称为与该模式串匹配)的文本串?

              -

              可以先思考一个更简单的问题:如何构建接受所有模式串的DFA?很明显,**字典树**就可以看做符合要求的自动机。例如,有模式串"abab""abc""bca""cc" ,我们把它们插入字典树,可以得到:

              -

              picture

              -

              为了使它不仅接受模式串,还接受以模式串结尾的文本串,一个看起来挺正确的改动是,使每个状态接受所有原先不能接受的字符,转移到初始状态(即根节点)。

              -

              picture

              -

              但是如果我们尝试"abca",我们会发现我们的自动机并不能接受它。稍加观察发现,我们在状态5接受a应该跳到状态8才对,而不是初始状态。某种意义上来说,状态7是状态5退而求其次的选择,因为状态7在trie上对应的字符串"bc"是状态5对应的字符串"abc"后缀。既然状态5原本不能接受"a",我们完全可以退而求其次看看状态7是否可以接受。这看起来很像KMP算法,确实,AC自动机常常被人称作trie上KMP。

              -

              所以我们给每个状态分配一条fail边,它连向的是该状态对应字符串在trie上存在的最长真后缀所对应的状态。我们令所有状态p接受原来不能接受的字符c,转移到 next(fail(p),c) ,特别地,根节点转移到自己。为什么不需要像KMP算法一样,用一个循环不断进行退而求其次的选择呢?因为如果我们用BFS的方式进行上面的重构,我们可以保证 fail(p) 在p重构前已经重构完成了,类似于动态规划

              -

              picture

              -

              这样建fail边和重构完成后得到的自动机称为AC自动机(Aho-Corasick Automation)。

              -

              我们发现fail边也形成一棵树,所以其实AC自动机包含两棵树:trie树fail树。一个重要的性质是,如果当前状态 p 在某个终止状态 s 的fail树的子树上,那么当前文本串就与 s 所对应模式串匹配

              -
              -

              也就是说它的解决方法是加fall边(蓝色)和加新边(红色),

              -]]>
              -
              - - JavaWeb - /2022/12/21/JavaWeb/ - 第一部分 Java基础

              JUnit单元测试

              JUnit是白盒测试。

              -

              简要使用步骤

              定义测试类

              包含各种测试用例。

              -

              一般放在包名xxx.xxx.xx.test里,类名为“被测试类名Test”。

              -

              定义测试方法

              测试方法可以独立运行。

              -

              方法名一般为“test测试的方法”,void,空参。

              -

              给方法加@Test标签

              加入JUnit依赖包

              具体细节

              断言

              Assert.assertEquals(3,result);
              - -

              @Before @After

              @Before在所有测试方法执行前自动执行,常用于资源申请。

              -

              @After在所有测试方法执行完后自动执行,常用于释放资源。

              -

              反射

              反射是框架设计的灵魂。

              -

              Java对象创建的三个阶段

              image-20221205194807395

              -

              类加载器把硬盘中的字节流文件装载进内存,并且翻译封装为Class类对象。通过Class类对象才能创建Person对象。

              -

              而这也就是说,如果我们有了Class对象,我们就可以创建该类对象。

              -

              获取Class对象

              有三种方式。

              -

              Class.forName(“类的全名”)

              将字节码文件加载进内存,返回class对象。多用于配置文件【将类名定义在配置文件】

              -

              注意:类的全名指的是包.类,包含包名。

              -

              类型.class

              通过类名的属性class获取。多用于参数传递。

              -

              对象.getClass()

              getClass()是Object类的方法。多用于对象的获取字节码的方式。

              -
              //第一种方式
              Class class1 = Class.forName("Student");
              System.out.println(class1);
              //第二种方式
              Class class2 = Student.class;
              System.out.println(class2);
              //第三种方式
              Student stu = new Student();
              Class class3 = stu.getClass();
              System.out.println(class3);

              System.out.println((class1==class2)+" "+(class2==class3));

              /*输出
              class Student
              class Student
              class Student
              true true*/
              - -

              同一个字节码文件(*.class)在一次程序运行过程中只会被加载一次,不管是以哪种方式得到的Class对象,都是同一个。

              -

              使用Class对象

              可以通过class对象得到其字段、构造方法、方法等。

              -
              class Student{
              public String name;
              int birthday;
              protected int money;
              private double weight;

              private Student() {

              }
              public Student(String name, int birthday, int money, double weight) {
              this.name = name;
              this.birthday = birthday;
              this.money = money;
              this.weight = weight;
              }

              @Override
              public String toString() {
              return "Student{" +
              "name='" + name + '\'' +
              ", birthday=" + birthday +
              ", money=" + money +
              ", weight=" + weight +
              '}';
              }

              public static void haha(){
              System.out.println("haha");
              }
              public void hello(){
              System.out.println("hello");
              }
              public void hello(String name){
              System.out.println("hello,"+name+"!");
              }
              private void giveMoney(){
              System.out.println("my money is yours...");
              money = 0;
              }
              }
              - -
              Student stu = new Student("张三",321,1000,57.7);
              Class stuC = stu.getClass();
              - -

              字段

              获取字段

              常用方法:

              -
              //获取所有公有字段
              Field[] fs = stuC.getFields();
              //获取某个公有字段
              Filed f = stuC.getField("name");
              //获取所有字段
              Field[] fs = stuC.getDeclaredFields();
              //获取某个字段
              Filed f = stuC.getDeclaredField("name");
              /*输出
              public java.lang.String Student.name

              public java.lang.String Student.name

              public java.lang.String Student.name
              int Student.birthday
              protected int Student.money
              private double Student.weight*/
              - -
              使用字段
              Field f = stuC.getDeclaredField("money");
              //由于money protected,故应该先设置其为可访问,否则会抛出异常。
              //这是暴力反射,不推荐。
              f.setAccessible(true);
              //获取成员变量的值
              //注意此处是要用field.get(该类型对象)的。想想也有道理,Filed字段是属于Class对象的,因而你想获取某个对象的值当然得传入该对象。
              System.out.println(f.get(stu));
              //设置对象的值
              f.set(stu,0);
              System.out.println(stu);
              /* Student{name='张三', birthday=321, money=0, weight=57.7} */
              - -

              构造方法

              获取方法跟上面格式差不多。

              -
              //获取构造方法
              //获取无参私有构造方法
              Constructor cons = stuC.getDeclaredConstructor();
              cons.setAccessible(true);
              System.out.println(cons.newInstance());
              //获取有参构造方法
              Constructor cons2 = stuC.getConstructor(String.class,int.class,int.class,double.class);
              System.out.println(cons2.newInstance("李四",102,100,90.9));
              /*输出
              Student{name='null', birthday=0, money=0, weight=0.0}
              Student{name='李四', birthday=102, money=100, weight=90.9}*/
              - -

              如果想要获取公有的无参构造器,还可以使用Class类提供的更简单的方法,不用先创造构造器:

              -
              System.out.println(stuC.newInstance());
              - -

              方法

              //获取方法
              //可以获取静态方法
              Method m = stuC.getMethod("haha");
              System.out.println(m);
              //获取带参方法,自动根据参数推断
              Method m2 = stuC.getMethod("hello");
              Method m3 = stuC.getMethod("hello",String.class);
              //调用方法
              m2.invoke(stu);
              m3.invoke(stu,"琳琳");
              //获取私有方法并调用
              Method m4 = stuC.getDeclaredMethod("giveMoney");
              m4.setAccessible(true);
              m4.invoke(stu);
              /*输出
              public static void Student.haha()
              hello
              hello,琳琳!
              my money is yours...*/
              - -

              使用反射的案例

              image-20221205205122805

              -
              public class ReflectTest {
              public static void main(String[] args) throws Exception {
              /* 加载配置文件 */
              Properties pro = new Properties();
              //获取字节码文件加载器
              ClassLoader cl = ReflectTest.class.getClassLoader();
              pro.load(cl.getResourceAsStream("pro.properties"));

              /* 获取配置文件中的数据并执行 */
              Class cla = Class.forName(pro.getProperty("className"));
              cla.getMethod(pro.getProperty("methodName")).invoke(cla.newInstance());
              }
              }
              - - - -

              注解

              image-20221205211932790

              -

              image-20221205212028989

              -

              ①和③都是jdk预定义的。自定义主要是②。

              -

              生成doc文档

              javadoc XXX.java
              - -

              会自动根据里面的注解生成文档

              -

              JDK预定义注解

              image-20221205212443918

              -

              自定义注解

              注解类的本质

              @Target(ElementType.METHOD)
              @Retention(RetentionPolicy.SOURCE)
              public @interface Override {
              }
              - -

              本质上

              -
              public @interface Override{}
              - -

              等价于

              -
              public interface Override extends java.lang.annotation.Annotation{}
              - -

              注解的属性

              注解的属性就是接口中的成员方法。要求无参,且返回类型有固定取值:

              -

              image-20221205213135522

              -
              public @interface MyAnno {
              String name() default "haha";
              }
              - -
              @MyAnno(name = "haha")
              public class Student{}
              - -

              元注解

              描述注解的注解

              -

              image-20221205213324052

              -

              RetentionPolicy的三个取值:SOURCE、CLASS、RUNTIME,正对应着java对象的三个阶段。

              -

              SOURCE:不保留到字节码文件,会被编译器扔掉

              -

              CLASS:保留到字节码文件

              -

              RUNTIME:被读到

              -

              自定义的注解一般都取RUNTIME

              -

              在程序中获取注解属性

              相当于用注解替换配置文件

              -
              @Target(ElementType.TYPE)
              @Retention(RetentionPolicy.RUNTIME)
              public @interface Pro {
              String className();
              String methodName();
              }
              //保留在runtime应该是因为运行时要动态获取值。我试了一下换成CLASS或者SOURCE,会有NullPointerException
              - -
              @Pro(className = "Student",methodName = "hello")
              public class ReflectTest {
              public static void main(String[] args) throws Exception{
              Pro pro = ReflectTest.class.getAnnotation(Pro.class);

              Class cla = Class.forName(pro.className());
              cla.getMethod(pro.methodName()).invoke(cla.newInstance());
              }
              }
              - -

              class.getAnnotation(Pro.class);这句话实质上是创建了一个实例,继承了Pro接口,重载了里面的抽象方法。

              -

              使用案例:测试框架

              @Target(ElementType.METHOD)
              @Retention(RetentionPolicy.RUNTIME)
              public @interface Check {
              }
              - -

              然后在要测试的每个方法上面加上此标签。

              -

              image-20221205215017285

              -

              然后编写test方法:

              -
              public class TestCheck {
              public static void main(String[] args) throws IOException {
              //创建对象
              Calculator c = new Calculator();
              //获取所有方法
              Method[] methods = c.getClass().getMethods();

              //写入文件
              int number = 0;//异常次数
              BufferedWriter bf = new BufferedWriter(new FileWriter("bug.txt"));

              //检查每个方法是否有注解。有的话则执行。
              for (Method m : methods){
              if (m.isAnnotationPresent(Check.class)){
              try {
              m.invoke(c);
              } catch (Exception e) {
              //记录文件信息
              number++;
              bf.write(m.getName()+"出异常了。");
              bf.write("\n");
              bf.write(e.getCause().getClass().getSimpleName()+" "+e.getCause().getMessage());
              bf.write("\n");
              bf.write("-------------");
              bf.write("\n");
              }
              }
              }
              bf.write("共出现"+number+"次异常");
              bf.flush();
              bf.close();
              }
              }
              /*
              haha出异常了。
              ArithmeticException / by zero
              -------------
              共出现1次异常
              */
              - -

              image-20221205220053258

              -

              第二部分 数据库

              Mysql

              登录方式

              mysql -h[IP地址] -u[用户名] -p
              - -

              文件结构

              本地的一个文件夹就代表一个数据库,文件夹里的一个文件代表一张表。

              -

              image-20221205221041508

              -

              image-20221205221055114

              -

              SQL语法

              image-20221205221216882

              -

              SQL有四种语句类型

              -

              image-20221205221246672

              -

              DDL 操作数据库、表

              操纵数据库

              create datebase 数据库名称;
              create datebase if not exists 数据库名称;
              create datebase if not exists 数据库名称 character set gbk;
              - -
              drop database 数据库名称;
              drop database if exists 数据库名称;
              - -
              alter database 数据库名称 charactor set 修改后新值;
              - -
              show databases;# 查询所有数据库名称
              show create database 数据库名称;# 显示指定数据库创建时的指令内容
              - -
              使用
              select database();# 查询正在使用的数据库名称
              use 数据库名称;
              - -

              操纵表

              create table students(
              name varchar(20),
              age int,
              score double(3,1),
              birthday date,
              insert_time timestamp
              );# 创建表
              create table students2 like students;# 复制表
              - -

              *注:

              -
                -
              1. mysql的数据类型表

                -

                image-20221205222127740

                -

                其中:

                -

                ① double(3,1)表示XXX.X,最大值为99.9.

                -

                ② 关于三个时间类型

                -

                image-20221205222307284

                -

                所以timestamp常用作插入时间。

                -

                ③ varchar(20)表示二十个字符长的字符串。

                -

                注意,是“二十个字符”而不是“二十个字节”。如果使用的字符集每个字符占3个字节,则varchar(20)占60个字节。

                -

                ④ BLOB、CLOB、二进制这些用于存储大数,不常用

                -
              2. -
              -
              drop table 表名;
              - -
              # 修改表名
              alter table students rename to new_students;
              # 修改表的字符集
              alter table students character set 字符集名称;
              # 修改表的列名/类型
              alter table students change name new_name varchar(20);# 新列名 新数据类型
              alter table students modify name varchar(15);# 新数据类型
              # 添加一列
              alter table students add ID double(10);
              # 删除一列
              alter table students drop ID;
              - -
              show tables;# 查询数据库中所有表的名字
              desc 表名;# 查询某个表的结构
              - - - -

              DML 增删改表中数据

              insert into students(name,age,score,birthday) values('张三',15,99.9,"2022-12-5");
              insert into students values("张三",15,99.9,"2022-12-5",NULL);
              - -

              如果不加条件,会把表中所有数据删除

              -
              delete from students where name="张三";
              truncate table students;# 删除表,然后创建一张一模一样的新表
              - -

              image-20221205224600869

              -

              如果不加条件,会把表中所有记录全部修改

              -
              update students set name="1", age=10 where name="张三";
              - - - -

              DQL 查询表中记录

              语法

              image-20221205224802814

              -

              基础查询

              select # 多字段查询
              name,
              age
              from
              students;

              select distinct # 去重
              address
              from
              students;

              # 有NULL参与的计算结果都为NULL
              select name,math,english,math+english from students;
              # ifnull函数不会修改原表中的数据
              select name,math,english,IFNULL(math,0)+IFNULL(english,0) from students;

              select
              name,
              math,
              english,
              IFNULL(math,0)+IFNULL(english,0) total_score # 起别名
              from
              students;
              - -

              条件查询

              运算符
                -
              1. 基本运算符

                -

                <、>、=、<=、>=、<>(不等于,也可以用!=)

                -
              2. -
              3. 逻辑运算符

                -

                AND、OR

                -
              4. -
              5. BETWEEN AND

                -
              6. -
              7. IN后跟集合

                -

                image-20221205230824086

                -
              8. -
              9. IS、IS NOT

                -

                image-20221205230902110

                -
              10. -
              11. LIKE 模糊查询

                -

                类似正则使用占位符匹配

                -

                image-20221205231037021

                -
                select * from students where name like "马%";
              12. -
              -

              各种函数一样的东西

              排序函数
              order by 排序字段1 排序方式1,排序字段2 排序方式2;
              - -
                -
              1. 默认升序。

                -
              2. -
              3. ASC、DESC

                -
              4. -
              5. 多关键字排序

                -

                image-20221205231606255

                -

                第二条件仅当第一条件一样才使用。

                -
              6. -
              -
              聚合函数

              将一列数据作为整体,纵向计算

              -

              注意,聚合函数的计算会排除NULL值。如果不想让空置排除,可以尝试该方法:

              -
              select count(ifnull(math,0)) from students;
              - -
                -
              1. count 计算个数

                -
                select count(name) from students;# 有多少条记录
                - -

                一般如果要看有多少记录,可以用count(主键),因为主键不为空。

                -
              2. -
              3. max、min

                -
              4. -
              5. sum 求和

                -
              6. -
              7. avg 平均值

                -
              8. -
              -
              分组查询

              分组之后查询的字段只能是两种:① 分组字段 ② 聚合函数。因为分组了之后再查具有个人特色的东西就没意义了。【高版本的mysql如果查询别的字段会报错】

              -
              select sex,avg(math) from students group by sex;
              - -

              image-20221207212103151

              -
              对分组结果进行条件限制

              还可以在分组前对条件限定,使用WHERE

              -
              select sex,avg(math) from students where math >= 70 group by sex;
              - -

              或者在分组后限定,使用HAVING

              -
              select sex,avg(math),count(id) from students group by sex having count(id)>2;
              # 或
              select sex,avg(math),count(id) total from students group by sex having total>2;
              - -
              WHERE和HAVING的区别
                -
              1. WHERE在分组前限定,不满足where则不参与分组;HAVING在分组后限定,不满足having则不会被查询出来。
              2. -
              3. WHERE条件里不能有聚合函数,HAVING可以。
              4. -
              -
              分页查询

              image-20221207213958848

              -

              这种就是分页查询。

              -
              limit 开始的索引,每页查询的条数;
              - -

              limit只能在mysql使用。

              -

              DCL 管理用户,授权操作

              管理用户

              查询用户

              image-20221219223932789

              -

              用户表存放地点↑

              -
              USE mysql;
              SELECT * FROM USER;
              - -
              创建用户

              注意,以下出现的”用户名”@”主机名” IDENTIFIED BY “密码”,不能在@两侧加空格,否则报错。

              -
              CREATE USER "用户名"@"主机名" IDENTIFIED BY "密码";
              - -
              删除用户
              DROP USER "用户名"@"主机名";
              - -
              修改密码
              -- 使用mysql自带的密码加密函数PASSWORD
              -- 1
              UPDATE USER SET PASSWORD = PASSWORD("新密码") WHERE USER = "用户名";
              -- 2
              SET PASSWORD FOR "用户名"@"主机名" = PASSWORD("新密码");
              - -

              image-20221219225036633

              -

              授权操作

              查询权限
              SHOW GRANTS FOR "root"@"%";
              - -
              授予权限
              grant 权限列表 on 数据库名.表名 to '用户名'@'主机名';
              - -

              image-20221219225715075

              -
              撤销权限
              revoke 权限列表 on 数据库名.表名 from '用户名'@'主机名';
              - -

              约束

              非空约束

              添加非空约束

              CREATE TABLE stu(
              id INT,
              name VARCHAR(20) NOT NULL
              );
              ALTER TABLE stu MODIFY name VARCHAR(20) NOT NULL;
              - -

              删除非空约束

              如果要去掉该约束,可以这么做:

              -
              ALTER TABLE stu MODIFY name VARCHAR(20);
              - -

              由于我们没写“NOT NULL ”,所以非空约束就被去掉了。感觉这点的解释挺有意思的。

              -

              唯一约束

              添加唯一约束

              某列值不能重复

              -
              CREATE TABLE stu(
              id INT,
              phone_number VARCHAR(20) UNIQUE
              );
              ALTER TABLE stu MODIFY phone_number VARCHAR(20) UNIQUE;
              - -

              但是注意,唯一约束允许多个NULL存在

              -

              删除唯一约束

              唯一约束的删除方法跟前面的非空约束就完全不一样了。

              -
              ALTER TABLE stu DROP INDEX phone_number;
              - -
              -

              创建唯一约束时会自动创建唯一索引,需要删除索引

              -
              -

              主键约束

              一张表只能有一个主键。主键非空且唯一。

              -

              添加主键约束

              CREATE TABLE stu(
              id INT PRIMARY KEY,
              name VARCHAR(20)
              );
              ALTER TABLE stu MODIFY id INT PRIMARY KEY;
              - -

              删除主键约束

              ALTER TABLE stu DROP PRIMARY KEY;
              - -

              自动增长

              这东西一般都跟主键结合使用。

              -

              若某一列是数值类型,可以使用auto_increment关键字来完成值的自动增长。

              -
              CREATE TABLE stu(
              id INT PRIMARY KEY AUTO_INCREMENT,
              NAME VARCHAR(20)
              );
              INSERT INTO stu VALUES(NULL,'111');# 设NULL或自己指派都行。
              - -

              自动增长的数据只跟上一个记录有关系。

              -

              外键约束

              引言

              image-20221207222232057

              -

              表中dep_name和dep_location有数据冗余,修改或者插入都不方便,不符合数据库设计准则,所以需要创造两张表。

              -

              image-20221207222418827

              -

              image-20221207222439652

              -

              但要是你想裁员了,直接在第二个表删研发部是没用的,第一个表数据还在,还得麻烦地一个个删。这时候外键就起作用了。

              -

              添加外键约束

              外键只能关联唯一约束或者主键约束的列。一般外键都是去关联主表的主键。

              -

              image-20221207223758950

              -
              CREATE TABLE employee(
              id INT PRIMARY KEY AUTO_INCREMENT,
              NAME VARCHAR(20),
              age INT,
              dep_id INT, -- 外键列
              CONSTRAINT emp_dept_fk FOREIGN KEY (dep_id) REFERENCES department(id) -- 外键声明
              );

              ALTER TABLE employee ADD CONSTRAINT emp_dept_fk FOREIGN KEY (dep_id) REFERENCES department(id) -- 外键声明
              - -

              此时不能删除department的行【要是该行在employee表没出现过的话就可以删掉】,也不能在employee添加一个不存在的外键值。

              -

              删除外键约束

              ALTER TABLE employee DROP FOREIGN KEY emp_dept_fk;
              - -

              外键级联

              如果你想修改外表主键值,就需要用到级联更新。

              -
              ALTER TABLE employee ADD CONSTRAINT emp_dept_fk FOREIGN KEY (dep_id) REFERENCES department(id) ON UPDATE CASCADE ;-- 外键声明+级联更新声明
              - -

              如果你想达到删除一个主键值就能删除表中的所有与该主键值关联的数据,就需要用到级联删除。

              -
              ALTER TABLE employee ADD CONSTRAINT emp_dept_fk FOREIGN KEY (dep_id) REFERENCES department(id) ON DELETE CASCADE ;-- 外键声明+级联删除声明
              - -

              级联使用应该要谨慎。一是它不大安全,二是它涉及多表操作,效率低下

              -

              多表关系与范式

              多表关系

              image-20221219161901631

              -

              image-20221219161847190

              -

              image-20221219161853190

              -

              范式

              image-20221219162311785

              -

              image-20221219162646556

              -

              1NF

              image-20221219162034996

              -

              image-20221219162047446

              -

              2NF

              1NF中的主属性为学号和课程名称。可以看到,分数完全依赖于码,但是姓名、系名、系主任都只是部分依赖于码,这不符合2NF的条件。因而,我们就可以选择拆分表,把完全依赖的部分和部分依赖的部分分开:

              -

              由于分数->(学号,课程名称),因而可以把学号、课程名称、分数放在一张表

              -

              由于姓名、系名、系主任 ->(学号),因而可以把学号、姓名、系名、系主任放在一张表

              -

              如下图所示。这样就消除了部分依赖。

              -

              图片2

              -

              3NF

              2NF中选课表的主属性为学号和课程名称,学生表的主属性为学号。可以看到,学生表中,存在着系主任->系名->学号这样的传递依赖,不符合3NF的规定。因而,我们需要对学生表进行进一步的拆分。

              -

              我们为了破坏系主任->系名->学号这个传递链,可以拆分成系主任->系名和系名->学号两个传递关系。

              -

              因而,可以把学生表拆分为如下图两张表:

              -

              image-20221219162231385

              -

              多表查询

              内连接查询

              隐式内连接

              使用where条件

              -
              -- 查询所有员工信息和对应的部门信息
              SELECT * FROM emp,dept WHERE emp.`dept_id` = dept.`id`;
              -- 进行去重就成为了自然连接:
              SELECT emp.*,dept.`NAME` FROM emp,dept WHERE emp.`dept_id` = dept.`id`;
              - -
              -

              此在书中称为“等值连接和非等值连接”。

              -
              -

              显式内连接

              语法: select 字段列表 from 表名1 [inner] join 表名2 on 条件

              -
              SELECT * FROM emp INNER JOIN dept ON emp.`dept_id` = dept.`id`;	
              - -

              外连接查询

              -

              关于外连接和内连接的区别,以及左外连接与右外连接的区别:

              -

              image-20221219155724391

              -

              ![屏幕截图 2022-12-19 155750](./JavaWeb/屏幕截图 2022-12-19 155750.png)

              -

              image-20221219155846157

              -
              -

              左外连接

              语法:select 字段列表 from 表1 left [outer] join 表2 on 条件;

              -
              SELECT 	t1.*,t2.`name` FROM emp t1 LEFT JOIN dept t2 ON t1.`dept_id` = t2.`id`;
              - -

              右外连接

              语法:select 字段列表 from 表1 right [outer] join 表2 on 条件;

              -
              SELECT 	* FROM dept t2 RIGHT JOIN emp t1 ON t1.`dept_id` = t2.`id`;
              - -

              子查询

              查询嵌套

              -
              -

              子查询中不允许使用ORDER BY

              -

              在实际运用中,内连接比子查询的效率更高

              -
              -

              不相关子查询

              -

              image-20221219160800762

              -
              -
              子查询结果单行单列

              可用于WHERE条件

              -
              -- 查询工资最高的员工信息
              SELECT * FROM emp WHERE emp.salary = (SELECT MAX(salary) FROM emp);
              - -
              子查询结果多行单列

              可以作为条件用IN关键字

              -
              -- 查询'财务部'和'市场部'所有的员工信息
              SELECT * FROM emp WHERE dept_id IN (SELECT id FROM dept WHERE name = "财务部" OR name = "市场部");
              - -
              子查询结果多行多列

              可以当做一个新表,可以转化为普通内连接

              -
              -- 查询员工入职日期是2011-11-11日之后的员工信息和部门信息
              -- 嵌套查询
              SELECT *
              FROM dept, (
              SELECT *
              FROM emp
              WHERE emp.join_data > '2011-11-11'
              )
              WHERE dept.id = emp.dept_id;
              -- 内连接
              SELECT * FROM emp,dept WHERE emp.join_data > '2011-11-11' AND emp.dep_id = dept.id;
              - -

              相关子查询

              -

              image-20221219160653496

              -

              子查询内部使用了父查询的东西

              -

              image-20221219161626542

              -

              image-20221219161637827

              -
              -

              事务

              基本介绍

              概念

              一个包含多个步骤的业务操作被事务管理,操作要么同时成功,要么同时失败。【有种原子操作的感觉?】

              -

              image-20221219214157761

              -

              当操作失败时,会回滚到执行前的状态。

              -

              事务操作

              事实上就是类似有个缓冲区,得到commit指令就把缓冲区内容更新,得到rollback指令就把缓冲区内容丢弃。

              -
              -- 开启事务
              START TRANSACTION;

              -- 操作序列:转账500元
              UPDATE usr SET money = money - 500 WHERE uname = "Mary";
              UPDATE usr SET money = money + 500 WHERE uname = "Lily";

              -- 提交
              COMMIT;
              - -
              -- 开启事务
              START TRANSACTION;

              -- 操作序列:转账500元
              UPDATE usr SET money = money - 500 WHERE uname = "Mary";
              出错了
              UPDATE usr SET money = money + 500 WHERE uname = "Lily";

              -- 出错则回滚
              ROLLBACK;
              - -
              开启事务
              START TRANSACTION;
              - -
              回滚事务
              ROLLBACK;
              - -
              提交事务
              COMMIT;
              - -

              一条DML(增删改表中数据)语句默认会自动提交。但如果手动开启了事务,那么事务内保护的原子序列就需要手动提交。

              -

              如果想将默认提交给kill了,也即不论是否开启事务都得手动提交,那么就需要用如下语句:

              -
              SET @@autocommit = 0;
              - -

              事务的四大特征

              原子性

              持久性

              一旦事务提交/回滚,会持久性更新数据库表。

              -

              隔离性

              多个事务之间应该相互独立。为了保障这一点,需要设置事务的隔离级别。

              -

              一致性

              事务操作前后数据总量不变。

              -

              事务的隔离级别

              概念:多个事务之间隔离的,相互独立的。但是如果多个事务操作同一批数据,则会引发一些问题,设置不同的隔离级别就可以解决这些问题。【有点并发的感觉】

              -

              image-20221219221141605

              -

              隔离级别越高,安全性越高,效率越来越差。

              -

              mysql默认的是3,oracle默认的是2.

              -

              可以设置隔离级别。

              -
              set global transaction isolation level  "级别字符串";
              - -
              -

              通过之后老师说的内容,感觉有了点个人的感悟:

              -

              级别1寻找数据可能优先从缓冲区找;级别2相当于不能读到缓冲区内容;级别3可能相当于在开启事务前对表做了个快照?级别4应该就是直接上了把互斥锁,同一时刻只能一个事务读写。

              -
              -

              JDBC

              概念

              Java Database Connectivity Java语言操作数据库

              -

              image-20221220141025613

              -

              image-20221220141214259

              -

              快速入门

                -
              1. 导入驱动jar包

                -

                ① 新建libs目录

                -

                ② 把jar包复制到libs目录下

                -

                ③ 右键libs目录 add as library

                -
              2. -
              3. 注册驱动

                -
              4. -
              5. 获取数据库连接对象 Connection

                -
              6. -
              7. 定义sql语句

                -
              8. -
              9. 获取执行sql语句的对象 Statement

                -
              10. -
              11. 执行sql,接收返回的结果

                -
              12. -
              13. 处理结果

                -
              14. -
              15. 释放资源

                -
              16. -
              -
              public class JdbcDemo1 {
              public static void main(String[] args) throws ClassNotFoundException, SQLException {
              //1 注册驱动
              Class.forName("com.mysql.jdbc.Driver");
              //2 获取数据库连接对象
              Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/helloworld","root","root");
              //3 定义sql语句
              String sql = "update usr set money = 5000 where id = 1";
              //4 获取执行对象 Statement
              Statement stmt = conn.createStatement();
              //5 执行sql
              int count = stmt.executeUpdate(sql);
              //6 处理结果
              System.out.println(count);
              //7 释放资源
              stmt.close();
              conn.close();
              }
              }
              - -

              优化版【增加try-catch-finally】:

              -
              public class JdbcDemo1 {
              public static void main(String[] args) {
              //1 注册驱动
              //提升作用域,放在try外面
              Connection conn = null;
              Statement stmt = null;
              ResultSet resultSet = null;
              try {
              Class.forName("com.mysql.jdbc.Driver");
              //2 获取数据库连接对象
              conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/helloworld","root","root");
              //3 定义sql语句
              String sql = "select * from usr";
              //4 获取执行对象 Statement
              stmt = conn.createStatement();
              //5 执行sql
              resultSet = stmt.executeQuery(sql);
              //6 处理结果
              if (resultSet == null)
              System.out.println("修改失败");
              else {
              while(resultSet.next()){
              System.out.println(resultSet.getInt(1)+" "
              +resultSet.getString(2)+" "
              +resultSet.getInt(3));
              }
              }

              } catch (ClassNotFoundException e) {
              throw new RuntimeException(e);
              } catch (SQLException e) {
              throw new RuntimeException(e);
              } finally {
              if (resultSet != null){
              try {
              resultSet.close();
              } catch (SQLException e) {
              throw new RuntimeException(e);
              }
              }
              //为了避免空指针异常
              if (stmt != null) {
              try {
              stmt.close();
              } catch (SQLException e) {
              throw new RuntimeException(e);
              }
              }
              if (conn != null){
              try {
              conn.close();
              } catch (SQLException e) {
              throw new RuntimeException(e);
              }
              }
              }
              }
              }
              - -

              详解各个类

              DriverManager

              注册驱动

              目的是告诉程序该使用哪一个数据库驱动jar包

              -

              在快速入门中,我们使用这一行来注册驱动:

              -
              Class.forName("com.mysql.jdbc.Driver");
              - -

              表面上看跟DriverManager类可以说是毫无关系。

              -

              但其实,类加载器加载类的时候,其实是会自动执行类中的静态代码块的。Driver类中有一段静态代码块如下:

              -
              static {
              try {
              DriverManager.registerDriver(new Driver());
              } catch (SQLException var1) {
              throw new RuntimeException("Can't register driver!");
              }
              }
              - -

              可见,注册驱动其实主要任务是由DriverManager类干的。这个静态块仅仅用于简化代码书写。

              -
              -

              注意:mysql5之后的版本,这一步注册驱动可以省略。

              -

              image-20221220151045275

              -

              配置文件里自动帮你注册了。我想原理应该是让本文件的类自动加载。

              -
              -
              获取数据库连接
              Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/helloworld","root","root");
              - -

              url的语法:”jdbc:mysql://IP地址:端口号/数据库名称”

              -

              image-20221220151425953

              -

              Connection

              数据库连接对象。

              -
              获取Statement对象
              Statement createStatement() throws SQLException;
              PreparedStatement prepareStatement(String sql) throws SQLException;
              - -
              管理事务
              开启事务
              void setAutoCommit(boolean autoCommit) throws SQLException;
              - -

              设置参数为false即开启事务。也即关闭自动提交。

              -
              提交事务
              void commit() throws SQLException;
              - -
              回滚事务
              void rollback() throws SQLException;
              - -

              Statement

              -

              The object used for executing a static SQL statement and returning the results it produces.执行静态sql

              -
              -
              执行sql
              //执行任意语句
              boolean execute(String sql) throws SQLException;
              /*
              执行DML(增删改表中数据)和DDL(表和库)语句
              返回值:影响到的行数。
              */
              int executeUpdate(String sql) throws SQLException;
              //执行DQL(查询表记录)语句
              ResultSet executeQuery(String sql) throws SQLException;
              - -

              ResultSet

              封装查询结果集。

              -

              具体取数方法就是类似迭代器原理。next移动迭代器指针,getXxx()方法,Xxx是数据类型,得到该行表记录中对应列对应数据类型的值。可以传入列数或者列名。

              -
              boolean next() throws SQLException;
              String getString(int columnIndex) throws SQLException;
              boolean getBoolean(int columnIndex) throws SQLException;
              //...
              long getLong(int columnIndex) throws SQLException;
              //...
              /*@param:
              columnLabel – the label for the column specified with the SQL AS clause.
              If the SQL AS clause was not specified, then the label is the name of the column
              */
              String getString(String columnLabel) throws SQLException;
              - -

              使用实例:

              -

              image-20221220164414363

              -
              public static Collection<Client> query(){
              ArrayList<Client> clients = new ArrayList<>();
              Connection conn = null;
              Statement stmt = null;
              ResultSet resultSet = null;
              try {
              Class.forName("com.mysql.jdbc.Driver");
              conn = DriverManager.getConnection("jdbc:mysql:///helloworld","root","root");
              stmt = conn.createStatement();
              resultSet = stmt.executeQuery("select * from usr");
              if (resultSet == null)
              System.out.println("查询失败");
              else
              while(resultSet.next())
              clients.add(new Client(resultSet.getInt(1),
              resultSet.getString(2),
              resultSet.getInt(3)));
              } catch (ClassNotFoundException e) {
              throw new RuntimeException(e);
              } catch (SQLException e) {
              throw new RuntimeException(e);
              } finally {
              if (resultSet != null){
              try {
              resultSet.close();
              } catch (SQLException e) {
              throw new RuntimeException(e);
              }
              }
              //为了避免空指针异常
              if (stmt != null) {
              try {
              stmt.close();
              } catch (SQLException e) {
              throw new RuntimeException(e);
              }
              }
              if (conn != null){
              try {
              conn.close();
              } catch (SQLException e) {
              throw new RuntimeException(e);
              }
              }
              }
              return clients;
              }
              - -

              注意:

              -
                -
              1. 这东西也得Close

                -
              2. -
              3. 如果想做“查询到了结果则返回true”这样的操作,不应该使用这样的代码:

                -
                if (resultSet != null)  return true;
                else return false;
                - -

                而应该这样:

                -
                return resultSet.next();
              4. -
              -

              PreparedStatement

              简介
              //An object that represents a precompiled SQL statement.
              //表示预编译的sql语句的对象
              public interface PreparedStatement extends Statement
              - -

              Statement的子类。可以用来解决sql注入问题。

              -

              它是预编译的sql语句,也即sql语句中的参数使用“?”占位符,需要传入参数。

              -
              使用步骤

              如下面的验证密码程序。键盘输入账号密码,从数据库查询该用户是否存在。

              -
              public class Login {
              public static void main(String[] args) throws IOException {
              //输入账号密码
              BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
              System.out.println("Please enter the user name.");
              String uname = br.readLine();
              System.out.println("Please enter the password.");
              String password = br.readLine();
              //验证账号密码
              System.out.println(checkPassword(uname,password));
              }

              public static boolean checkPassword(String uname,String password){
              if (uname == null | password == null) return false;

              Connection conn = null;
              PreparedStatement stmt = null;
              ResultSet resultSet = null;
              try {
              conn = JDBCUtils.getConnection();
              stmt = conn.prepareStatement(
              "select * from user where name = ? and password = ?");
              //这跟resultset的获取表值的那个是一样的,都是需要指定列数和要设定的值。
              //并且下标都是以1开始。
              stmt.setString(1,uname);
              stmt.setString(2,password);
              resultSet = stmt.executeQuery();
              // stmt = conn.createStatement();
              // resultSet = stmt.executeQuery("select * from user where name = '"
              // +uname+"' and password = '"+password+"'");
              return resultSet.next();
              } catch (SQLException e) {
              throw new RuntimeException(e);
              } finally {
              JDBCUtils.close(conn,stmt,resultSet);
              }
              }
              }
              - -
              用PreparedStatement替代Statement

              它更安全且效率更高。

              -

              JDBC工具类

              书写

              public class JDBCUtils {
              //获取连接时不想传参,且需要保证通用性,使用配置文件
              //配置文件只需读取一次,可以用静态代码块完成
              private static Properties pro;
              private static String url;
              private static String driver;
              private static String user;
              private static String password;
              static{
              pro = new Properties();
              try {
              /*
              ClassLoader类可以获取src路径下的文件。使用ClassLoader获取文件时,只用传入相对于src的相对路径就行
              此处如果使用FileReader,需要以下写法:
              pro.load(new FileReader(
              JDBCUtils.class.getClassLoader()
              .getResource("jdbc.properties")
              .getPath())
              );
              */
              pro.load(JDBCUtils.class.getClassLoader().getResourceAsStream("./jdbc.properties"));
              url = pro.getProperty("url");
              user = pro.getProperty("user");
              password = pro.getProperty("password");
              driver = pro.getProperty("driver");
              //在静态块里注册驱动
              Class.forName(driver);
              } catch (IOException e) {
              throw new RuntimeException(e);
              } catch (ClassNotFoundException e) {
              throw new RuntimeException(e);
              }
              }
              public static Connection getConnection() {
              try {
              return DriverManager.getConnection(url,user,password);
              } catch (SQLException e) {
              throw new RuntimeException(e);
              }
              }

              //重载机制
              public static void close(Connection conn){
              close(conn,null,null);
              }

              public static void close(Connection conn, Statement stmt){
              close(conn,stmt,null);
              }

              public static void close(Connection conn, Statement stmt, ResultSet resultSet){
              if (resultSet != null){
              try {
              resultSet.close();
              } catch (SQLException e) {
              throw new RuntimeException(e);
              }
              }
              if (stmt != null){
              try {
              stmt.close();
              } catch (SQLException e) {
              throw new RuntimeException(e);
              }
              }
              if (conn !=null){
              try {
              conn.close();
              } catch (SQLException e) {
              throw new RuntimeException(e);
              }
              }
              }
              }
              - -

              使用

              public static Collection<Client> query(){
              ArrayList<Client> clients = new ArrayList<>();
              Statement stmt = null;
              Connection conn = null;
              ResultSet resultSet = null;
              try {
              conn = JDBCUtils.getConnection();
              stmt = conn.createStatement();
              resultSet = stmt.executeQuery("select * from usr");
              if (resultSet == null)
              System.out.println("查询失败");
              else
              while(resultSet.next())
              clients.add(new Client(resultSet.getInt(1),resultSet.getString(2),resultSet.getInt(3)));
              } catch (SQLException e) {
              throw new RuntimeException(e);
              } finally {
              JDBCUtils.close(conn,stmt,resultSet);
              }
              return clients;
              }
              - -

              JDBC控制事务

              使用Connection对象的管理事务的方法。

              -

              image-20221220214249168

              -

              image-20221220224401302

              -
              public class Account {
              public static void main(String[] args) {
              Connection conn = null;
              PreparedStatement stmt = null;
              PreparedStatement stmt2 = null;
              try {
              conn = JDBCUtils.getConnection();
              conn.setAutoCommit(false);
              stmt = conn.prepareStatement(
              "update usr set money = money - 500 where id = 1");
              stmt.executeUpdate();
              // int i = 3/0;
              stmt2 = conn.prepareStatement(
              "update usr set money = money + 500 where id = 2");
              stmt2.executeUpdate();
              conn.commit();
              } catch (Exception e) {
              try {
              conn.rollback();
              } catch (SQLException ex) {
              throw new RuntimeException(ex);
              }
              throw new RuntimeException(e);
              } finally {
              JDBCUtils.close(conn,stmt);
              }
              }
              }
              - -

              数据库连接池

              其实就是上面的JDBC中的Connection的对象池。

              -

              image-20221222212904151

              -

              C3P0

              基本使用

              非常简单,就是改一下Connection的获取,写一下xml就行。

              -
              设置配置文件

              固定放在src目录下。名字必须为c3p0-config.xml或者c3p0.properties

              -
              <c3p0-config>
              <!-- 使用默认的配置读取连接池对象 -->
              <default-config>
              <!-- 连接参数 -->
              <property name="driverClass">com.mysql.jdbc.Driver</property>
              <property name="jdbcUrl">jdbc:mysql://localhost:3306/helloworld</property>
              <property name="user">root</property>
              <property name="password">root</property>

              <!-- 连接池参数 -->
              <property name="initialPoolSize">5</property>
              <property name="maxPoolSize">10</property>
              <!-- 如果超过此超时时间,就说明数据库连接失败 -->
              <property name="checkoutTimeout">3000</property>
              </default-config>

              <named-config name="otherc3p0">
              <!-- 连接参数 -->
              <property name="driverClass">com.mysql.jdbc.Driver</property>
              <property name="jdbcUrl">jdbc:mysql://localhost:3306/day25</property>
              <property name="user">root</property>
              <property name="password">root</property>

              <!-- 连接池参数 -->
              <property name="initialPoolSize">5</property>
              <property name="maxPoolSize">8</property>
              <property name="checkoutTimeout">1000</property>
              </named-config>
              </c3p0-config>
              - -

              可以注意到,xml文件里面可以保存多套配置,比如上面的示例代码就保存了两套配置,default-config和name=”otherc3p0”的config。

              -

              ComboPooledDataSource有一个含参构造器:

              -
              public ComboPooledDataSource(String configName) {
              super(configName);
              }
              - -

              就可以传入config的名称指定要用的配置信息。

              -
              使用
              DataSource cpds = new ComboPooledDataSource();
              Connection conn = null;
              try{
              conn = cpds.getConnection();
              //正常使用......
              } catch(Exception e){

              } finally{
              if (conn != null){
              try {
              //正常使用关闭方法
              conn.close();
              } catch (SQLException e) {
              throw new RuntimeException(e);
              }
              }
              }
              - -

              Druid

              基本使用

              设置配置文件

              Druid的配置文件可以放在任意路径下,随便取名字。因为到时候需要指定配置文件。使用的是Properties文件。

              -
              driverClassName=com.mysql.jdbc.Driver
              url=jdbc:mysql://localhost:3306/helloworld
              username=root
              password=root
              initialSize=5
              maxActive=10
              maxWait=3000
              - -
              使用
              //导入配置文件
              Properties pro = new Properties();
              pro.load(Main.class.getClassLoader().getResourceAsStream("./druid.properties"));
              //使用工厂方法获取连接池对象
              DataSource cpds = DruidDataSourceFactory.createDataSource(pro);
              Connection conn = null;
              try{
              conn = cpds.getConnection();
              //正常使用......
              } catch(Exception e){

              } finally{
              if (conn != null){
              try {
              //正常使用关闭方法
              conn.close();
              } catch (SQLException e) {
              throw new RuntimeException(e);
              }
              }
              }
              - -

              定义工具类

              一般使用的时候还是会自定义一个工具类的

              -
              import com.alibaba.druid.pool.DruidDataSourceFactory;

              import javax.sql.DataSource;
              import java.sql.Connection;
              import java.sql.ResultSet;
              import java.sql.SQLException;
              import java.sql.Statement;
              import java.util.Properties;

              public class JDBCUtils {
              private static DataSource ds;
              static{
              try {
              Properties pro = new Properties();
              pro.load(JDBCUtils.class.getClassLoader().getResourceAsStream("./druid.properties"));
              ds = DruidDataSourceFactory.createDataSource(pro);
              } catch (Exception e) {
              throw new RuntimeException(e);
              }
              }

              public static Connection getConnection() throws SQLException {
              return ds.getConnection();
              }

              public static DataSource getDataSource(){
              return ds;
              }

              //重载机制
              public static void close(Connection conn){
              close(conn,null,null);
              }

              public static void close(Connection conn, Statement stmt){
              close(conn,stmt,null);
              }

              public static void close(Connection conn, Statement stmt, ResultSet resultSet){
              if (resultSet != null){
              try {
              resultSet.close();
              } catch (SQLException e) {
              throw new RuntimeException(e);
              }
              }
              if (stmt != null){
              try {
              stmt.close();
              } catch (SQLException e) {
              throw new RuntimeException(e);
              }
              }
              if (conn !=null){
              try {
              conn.close();
              } catch (SQLException e) {
              throw new RuntimeException(e);
              }
              }
              }
              }
              - -

              使用同上的JDBCUtils

              -

              Spring JDBC

              Spring框架对JDBC的简单封装,提供JDBCTemplate对象。

              -

              使用方法

              带参(PreparedStatement)

              jdbcTemplate.update("update usr set money = ? where uname = ?",10,"Mary");
              - -

              DML

              update
              public static void main(String[] args) throws Exception {
              JdbcTemplate jdbcTemplate = new JdbcTemplate(JDBCUtils.getDataSource());

              //增
              int count = jdbcTemplate.update("insert into usr values (null,'Jack',3000),(null,'LiMing',500000)");
              System.out.println(count);
              //删
              int count2 = jdbcTemplate.update("delete from usr where uname = 'Jack'");
              System.out.println(count2);
              //改
              int count3 = jdbcTemplate.update("update usr set money = ? where uname = ?",10,"Mary");
              System.out.println(count3);
              }
              /*输出结果:
              2
              1
              1
              */
              - -

              DQL

              提供了三种方法。

              -
              queryForMap

              将得到的结果(只能是一行)封装为一个Map<String,Object>,其中key为列名,value为该行该列的值。

              -

              如果得到的结果不为1行(=0 or >1),会抛出异常。

              -
                  JdbcTemplate jdbcTemplate = new JdbcTemplate(JDBCUtils.getDataSource());

              Map<String,Object> m = jdbcTemplate.queryForMap("select * from usr where id = 2");
              System.out.println(m);
              //输出:{id=2, uname=Lily, money=2000}
              - -
              queryForList

              将得到的结果封装为List<Map<String,Object>>,其中一个Map为一行,多个Map表示多行,存储在List中。

              -
                  List<Map<String, Object>> res = jdbcTemplate.queryForList("select * from usr");
              for (Map<String,Object> m : res){
              System.out.println(m.hashCode());
              System.out.println(m.keySet().toString());
              System.out.println(m.values().toString());
              }
              /*输出结果:
              213151839
              [id, uname, money]
              [1, Mary, 10]
              213143443
              [id, uname, money]
              [2, Lily, 2000]
              -2022948335
              [id, uname, money]
              [4, LiMing, 500000]
              */
              - -
              query

              可以把查询回的结果封装为自己想要的对象而不是Map。如示例就封装为了Client对象。

              -
              原始一点的

              可以看到,里面的包装内容还是得自己写,有点麻烦。

              -
                      JdbcTemplate jdbcTemplate = new JdbcTemplate(JDBCUtils.getDataSource());

              List<Client> clients = jdbcTemplate.query(
              "select * from usr where money > 1000",
              new RowMapper<Client>() {
              @Override
              public Client mapRow(ResultSet resultSet, int i) throws SQLException {
              return new Client(
              resultSet.getInt(1),
              resultSet.getString(2),
              resultSet.getInt(3));
              }
              });
              System.out.println(clients.toString());
              /*输出结果
              [Client{id=2, name='Lily', money=2000}, Client{id=4, name='LiMing', money=500000}, Client{id=6, name='LiMing', money=500000}]
              */
              - -
              常用的

              使用包装好的BeanPropertyRowMapper类。

              -
                      JdbcTemplate jdbcTemplate = new JdbcTemplate(JDBCUtils.getDataSource());

              List<Client> clients = jdbcTemplate.query("select * from usr where money > 1000",
              new BeanPropertyRowMapper<Client>(Client.class));
              System.out.println(clients.toString());
              /*输出结果:
              [Client{id=2, uname='Lily', money=2000}, Client{id=4, uname='LiMing', money=500000}, Client{id=6, uname='LiMing', money=500000}]
              */
              - -

              注意:

              -
                -
              1. 要求包装的class,比如说Client,必须要有public的无参构造器
              2. -
              3. Java的那个被包装类的字段最好使用基本数据类型,而使用引用类型,如Integer,Double等等等。因为如果使用基本数据类型,当表中数据为null时会报错。
              4. -
              5. 要求被包装的class的字段名称一定要与数据库的一模一样,大小写可以不一样。
              6. -
              7. 要求被包装的class的字段一定要是可以修改的。也就是说,要么public,要么提供set方法。
              8. -
              -
              queryForObject

              返回查到的某个东西。可以用于聚合函数的查询。

              -
              int money = jdbcTemplate.queryForObject("select money from usr where uname = 'Mary'",Integer.class);
              System.out.println(money);
              - -

              第三部分 Web概述和静态网页技术

              Web概述

                -
              • JavaWeb:

                -
                  -
                • 使用Java语言开发基于互联网的项目
                • -
                -
              • -
              • 软件架构:

                -
                  -
                1. C/S: Client/Server 客户端/服务器端
                    -
                  • 在用户本地有一个客户端程序,在远程有一个服务器端程序
                  • -
                  • 如:QQ,迅雷…
                  • -
                  • 优点:
                      -
                    1. 用户体验好
                    2. -
                    -
                  • -
                  • 缺点:
                      -
                    1. 开发、安装,部署,维护 麻烦
                    2. -
                    -
                  • -
                  -
                2. -
                3. B/S: Browser/Server 浏览器/服务器端
                    -
                  • 只需要一个浏览器,用户通过不同的网址(URL),客户访问不同的服务器端程序
                  • -
                  • 优点:
                      -
                    1. 开发、安装,部署,维护 简单
                    2. -
                    -
                  • -
                  • 缺点:
                      -
                    1. 如果应用过大,用户的体验可能会受到影响
                    2. -
                    3. 对硬件要求过高
                    4. -
                    -
                  • -
                  -
                4. -
                -
              • -
              • B/S架构详解

                -
                  -
                • 资源分类:

                  -
                    -
                  1. 静态资源:
                      -
                    • 使用静态网页开发技术发布的资源。
                    • -
                    • 特点:
                        -
                      • 所有用户访问,得到的结果是一样的。
                      • -
                      • 如:文本,图片,音频、视频, HTML,CSS,JavaScript
                      • -
                      • 如果用户请求的是静态资源,那么服务器会直接将静态资源发送给浏览器。浏览器中内置了静态资源的解析引擎,可以展示静态资源
                      • -
                      -
                    • -
                    -
                  2. -
                  3. 动态资源:
                      -
                    • 使用动态网页及时发布的资源。
                    • -
                    • 特点:
                        -
                      • 所有用户访问,得到的结果可能不一样。
                      • -
                      • 如:jsp/servlet,php,asp…
                      • -
                      • 如果用户请求的是动态资源,那么服务器会执行动态资源,转换为静态资源,再发送给浏览器
                      • -
                      -
                    • -
                    -
                  4. -
                  -
                • -
                • 我们要学习动态资源,必须先学习静态资源!

                  -
                • -
                • 静态资源:

                  -
                    -
                  • HTML:用于搭建基础网页,展示页面的内容
                  • -
                  • CSS:用于美化页面,布局页面
                  • -
                  • JavaScript:控制页面的元素,让页面有一些动态的效果
                  • -
                  -
                • -
                -
              • -
              -

              静态网页概述

              练习:用纯HTML写旅游网站首页

              代码

              <!DOCTYPE html>
              <html lang="ch">
              <head>
              <meta charset="UTF-8">
              <title>旅游网站</title>
              </head>
              <body>
              <table>
              <tr align="center">
              <td><img src="./image/top_banner.jpg" alt="亲子周边旅游节" width="100%"></td>
              </tr>
              <tr>
              <table>
              <tr align="center">
              <td width="25%"><img src="./image/logo.jpg" alt="logo" width="100%"></td>
              <td width="50%"><img src="./image/search.png" alt="search" width="100%"></td>
              <td width="25%"><img src="./image/hotel_tel.png" alt="hotel" width="100%"></td>
              </tr>
              </table>
              </tr>
              <tr>
              <table width="100%" bgcolor="orange" cellspacing="0" cellpadding="0">
              <tr align="center" height = "45">
              <td>首页</td>
              <td>门票</td>
              <td>酒店</td>
              <td>香港车票</td>
              <td>出境游</td>
              <td>国内游</td>
              <td>港澳游</td>
              <td>抱团定制</td>
              <td>全球自由行</td>
              <td>收藏排行榜</td>
              </tr>
              </table>
              </tr>
              <tr>
              <img src="./image/banner_3.jpg" alt="亲子周边旅游节" width="100%">
              </tr>
              <tr>
              <table>
              <tr>
              <td align="right" width="20%"><img src="./image/icon_5.jpg" alt="亲子周边旅游节" width="100%"></td>
              <td width="80%">黑马精选</td>
              </tr>
              </table>
              <hr color="orange">
              </tr>
              <tr>
              <table>
              <tr>
              <td>
              <div>
              <img src="./image/jiangxuan_1.jpg" alt="" width="100%">
              <br>上海飞三亚五天4晚自由行(春节销售+亲子+蜜月+自由行)<br>
              <font size = 3 color="#8b0000">¥899</font>
              </div>
              </td>
              <td>
              <div>
              <img src="./image/jiangxuan_1.jpg" alt="" width="100%">
              <br>上海飞三亚五天4晚自由行(春节销售+亲子+蜜月+自由行)<br>
              <font size = 3 color="#8b0000">¥899</font>
              </div>
              </td>
              <td>
              <div>
              <img src="./image/jiangxuan_1.jpg" alt="" width="100%">
              <br>上海飞三亚五天4晚自由行(春节销售+亲子+蜜月+自由行)<br>
              <font size = 3 color="#8b0000">¥899</font>
              </div>
              </td>
              </tr>
              </table>
              </tr>
              <tr>
              <table>
              <tr>
              <td align="right" width="20%"><img src="./image/icon_6.jpg" alt="亲子周边旅游节" width="100%"></td>
              <td width="80%">国内游</td>
              </tr>
              </table>
              <hr color="orange">
              </tr>
              <tr>
              <table align="center" width="95%">
              <tr>
              <td rowspan="2" width = "25%">
              <img src="./image/guonei_1.jpg" alt="亲子周边旅游节">
              </td>
              <td>
              <div>
              <img src="./image/jiangxuan_2.jpg" alt="" width="100%">
              <br>上海飞三亚五天4晚自由行(春节销售+亲子+蜜月+自由行)<br>
              <font size = 3 color="#8b0000">¥899</font>
              </div>
              </td>
              <td>
              <div>
              <img src="./image/jiangxuan_2.jpg" alt="" width="100%">
              <br>上海飞三亚五天4晚自由行(春节销售+亲子+蜜月+自由行)<br>
              <font size = 3 color="#8b0000">¥899</font>
              </div>
              </td>
              <td>
              <div>
              <img src="./image/jiangxuan_2.jpg" alt="" width="100%">
              <br>上海飞三亚五天4晚自由行(春节销售+亲子+蜜月+自由行)<br>
              <font size = 3 color="#8b0000">¥899</font>
              </div>
              </td>
              </tr>
              <tr>
              <td>
              <div>
              <img src="./image/jiangxuan_2.jpg" alt="" width="100%">
              <br>上海飞三亚五天4晚自由行(春节销售+亲子+蜜月+自由行)<br>
              <font size = 3 color="#8b0000">¥899</font>
              </div>
              </td>
              <td>
              <div>
              <img src="./image/jiangxuan_2.jpg" alt="" width="100%">
              <br>上海飞三亚五天4晚自由行(春节销售+亲子+蜜月+自由行)<br>
              <font size = 3 color="#8b0000">¥899</font>
              </div>
              </td>
              <td>
              <div>
              <img src="./image/jiangxuan_2.jpg" alt="" width="100%">
              <br>上海飞三亚五天4晚自由行(春节销售+亲子+蜜月+自由行)<br>
              <font size = 3 color="#8b0000">¥899</font>
              </div>
              </td>
              </tr>
              </table>
              </tr>
              <tr>
              <table>
              <tr>
              <td align="right" width="20%"><img src="./image/icon_7.jpg" alt="亲子周边旅游节" width="100%"></td>
              <td width="80%">境外游</td>
              </tr>
              </table>
              <hr color="orange">
              </tr>
              <tr>
              <table align="center" width="95%">
              <tr>
              <td rowspan="2" width = "25%">
              <img src="./image/jiangwai_1.jpg" alt="亲子周边旅游节">
              </td>
              <td>
              <div>
              <img src="./image/jiangxuan_2.jpg" alt="" width="100%">
              <br>上海飞三亚五天4晚自由行(春节销售+亲子+蜜月+自由行)<br>
              <font size = 3 color="#8b0000">¥899</font>
              </div>
              </td>
              <td>
              <div>
              <img src="./image/jiangxuan_2.jpg" alt="" width="100%">
              <br>上海飞三亚五天4晚自由行(春节销售+亲子+蜜月+自由行)<br>
              <font size = 3 color="#8b0000">¥899</font>
              </div>
              </td>
              <td>
              <div>
              <img src="./image/jiangxuan_2.jpg" alt="" width="100%">
              <br>上海飞三亚五天4晚自由行(春节销售+亲子+蜜月+自由行)<br>
              <font size = 3 color="#8b0000">¥899</font>
              </div>
              </td>
              </tr>
              <tr>
              <td>
              <div>
              <img src="./image/jiangxuan_2.jpg" alt="" width="100%">
              <br>上海飞三亚五天4晚自由行(春节销售+亲子+蜜月+自由行)<br>
              <font size = 3 color="#8b0000">¥899</font>
              </div>
              </td>
              <td>
              <div>
              <img src="./image/jiangxuan_2.jpg" alt="" width="100%">
              <br>上海飞三亚五天4晚自由行(春节销售+亲子+蜜月+自由行)<br>
              <font size = 3 color="#8b0000">¥899</font>
              </div>
              </td>
              <td>
              <div>
              <img src="./image/jiangxuan_2.jpg" alt="" width="100%">
              <br>上海飞三亚五天4晚自由行(春节销售+亲子+蜜月+自由行)<br>
              <font size = 3 color="#8b0000">¥899</font>
              </div>
              </td>
              </tr>
              </table>
              </tr>
              <tr>
              <img src="./image/footer_service.png" alt="" width="100%">
              </tr>
              <tr>
              <table bgcolor="orange" width="100%" height = 75>
              <tr align="center">
              <td>
              <font color="gray" size = 2>江苏传智播客教育科技股份有限公司 版权所有Copyright 2006-2018&copy;, All Rights Reserved 苏ICP备16007882</font>
              </td>
              </tr>
              </table>
              </tr>
              </table>
              </body>
              </html>
              - -

              注意点

                -
              1. 布局

                -

                页面布局使用table标签。这点让我感觉非常新奇。

                -

                而且表格布局可以嵌套,也即每一行可以是一个新的表格。

                -
              2. -
              3. 图片适应屏幕宽度

                -

                只需在img标签加个width=”100%”的属性即可。如:

                -
                <img src="./image/top_banner.jpg" width="100%">
              4. -
              -

              表单

              注意

                -
              1. 表项中的数据要被提交的话,必须指定其名称

                -

                image-20221223161847622

                -

                也即一定要有属性name。

                -
              2. -
              3. 关于from的属性

                -

                image-20221223162035569

                -
              4. -
              5. 一般都这么写

                -

                image-20221223165046277

                -
              6. -
              -

              练习

              image-20221223171603366

              -
              <!DOCTYPE html>
              <html lang="ch">
              <head>
              <meta charset="UTF-8">
              <title>注册界面</title>
              </head>
              <body>
              <form action="#">
              <table border="1" cellpadding="1" cellspacing="1">
              <tr>
              <td>
              <label for="username">用户名:</label>
              </td>
              <td>
              <input type="text" id="username" name="username" placeholder="请输入用户名">
              </td>
              </tr>
              <tr>
              <td>
              <label for="password">密码:</label>
              </td>
              <td>
              <input type="password" id="password" name="password" placeholder="请输入密码">
              </td>
              </tr>
              <tr>
              <td>
              <label for="email">邮箱:</label>
              </td>
              <td>
              <input type="email" id="email" name="email" placeholder="请输入邮箱">
              </td>
              </tr>
              <tr>
              <td>
              <label for="name">姓名:</label>
              </td>
              <td>
              <input type="text" id="name" name="name" placeholder="请输入姓名">
              </td>
              </tr>
              <tr>
              <td>
              <label for="phone_number">手机号:</label>
              </td>
              <td>
              <input type="text" id="phone_number" name="phone_number" placeholder="请输入手机号">
              </td>
              </tr>
              <tr>
              <td>
              性别:
              </td>
              <td>
              <input type="radio" name="gender" value="1">
              <input type="radio" name="gender" value="2">
              </td>
              </tr>
              <tr>
              <td>
              <label for="birthday">出生日期:</label>
              </td>
              <td>
              <input type="date" name="birthday" id="birthday">
              </td>
              </tr>
              <tr>
              <td>
              <label for="certification">验证码:</label>
              </td>
              <td>
              <input type="text" name="certification" id="certification">
              <img src="./image/verify_code.jpg" >
              </td>
              </tr>
              <tr align="center">
              <td colspan="2">
              <input type="image" src="./image/regbtn.jpg">
              </td>
              </tr>
              </table>
              </form>

              </body>
              </html>
              - -

              CSS

              盒子模型

              image-20221223204733649

              -

              练习:注册页面

              image-20221223223128861

              -
              <!DOCTYPE html>
              <html lang="ch">
              <head>
              <meta charset="UTF-8">
              <title>注册界面</title>
              <link rel="stylesheet" href="./2.css">
              </head>
              <body>
              <div id="log_in_box">
              <div id="log_in_text">
              <div id="log_in_text1">
              新用户注册
              </div>
              <div id="log_in_text2">
              USER REGISTER
              </div>
              </div>

              <div id="log_in_text3">
              已有账号?<font color = red>立即登录</font>
              </div>
              <div id="log_in_table">
              <form>
              <table>
              <tr>
              <td class="td_left"><label for="username">用户名</label></td>
              <td class="td_right"><input type="text" name="username" id="username" placeholder="请输入用户名"></td>
              </tr>

              <tr>
              <td class="td_left"><label for="password">密码</label></td>
              <td class="td_right"><input type="password" name="password" id="password" placeholder="请输入密码"></td>
              </tr>

              <tr>
              <td class="td_left"><label for="email">Email</label></td>
              <td class="td_right"><input type="email" name="email" id="email" placeholder="请输入邮箱"></td>
              </tr>

              <tr>
              <td class="td_left"><label for="name">姓名</label></td>
              <td class="td_right"><input type="text" name="name" id="name" placeholder="请输入姓名"></td>
              </tr>

              <tr>
              <td class="td_left"><label for="tel">手机号</label></td>
              <td class="td_right"><input type="text" name="tel" id="tel" placeholder="请输入手机号"></td>
              </tr>

              <tr>
              <td class="td_left"><label>性别</label></td>
              <td class="td_right">
              <input type="radio" name="gender" value="male"> <span class="choice"></span>
              <input type="radio" name="gender" value="female"> <span class="choice"></span>
              </td>
              </tr>

              <tr>
              <td class="td_left"><label for="birthday">出生日期</label></td>
              <td class="td_right"><input type="date" name="birthday" id="birthday" placeholder="请输入出生日期"></td>
              </tr>

              <tr>
              <td class="td_left"><label for="checkcode" >验证码</label></td>
              <td class="td_right"><input type="text" name="checkcode" id="checkcode" placeholder="请输入验证码">
              <img id="img_check" src="img/verify_code.jpg">
              </td>
              </tr>


              <tr>
              <td colspan="2" align="center"><input type="submit" value="注册" id="submit"></td>
              </tr>
              </table>
              </form>
              </div>
              </div>
              </body>
              </html>
              - -
              *{
              margin: 0px;
              padding: 0px;
              /*防止大小因padding变化*/
              box-sizing: border-box;
              }
              body{
              z-index: 0;
              background-image: url("./img/login_bg.png");
              }
              #log_in_box {
              border: 9px solid darkgray;
              z-index: 100;
              width: 987px;
              height: 590px;
              /*让div水平居中*/
              margin: auto;
              margin-top: 20px;
              padding: 15px;
              background: white;

              }

              #log_in_table {
              margin-top: 96px;
              margin-left: 300px;
              }

              #log_in_text3{
              font-size: 12px;
              float: right;
              }

              #log_in_text > div:first-child{
              margin-right: 0px;
              width: 150px;
              color: orange;
              font-size: 25px;
              }
              #log_in_text > div:last-child{
              margin-right: 0px;
              width: 200px;
              color: darkgray;
              font-size: 17px;
              font-family: "Arial Black";
              }

              #log_in_text{
              margin: 0px;
              display:inline;
              float: left;
              }

              /*此处一定得是选择input,不能是选择.td_right,因为这样才能覆盖掉input原有的那个丑边框*/
              input {
              padding: 5px;
              align-self: center;
              border: 1px solid lightgrey;
              height: 35px;
              border-radius: 6px;
              margin: 3px;
              margin-left: 15px;
              /*解决了radio的选框和文本不对齐。*/
              vertical-align: middle;
              }

              .td_left{
              color: slategrey;
              text-align: right;
              }

              .choice{
              font-size: 15px;
              }

              #checkcode{
              width: 100px;
              font-size: 15px;
              }

              #img_check{
              vertical-align:middle;
              }

              #submit{
              margin-top: 15px;
              margin-left: 15px;
              margin-right: 155px;
              color: transparent;
              width: 100px;
              background-image: url("./img/regbtn.jpg");
              }
              - -

              JavaScript

              对象

              function
              1. 创建:
              -   1. var fun = new Function(形式参数列表,方法体);  //忘掉吧
              -   2. function 方法名称(形式参数列表){
              -          方法体
              -      }
              -
              -   3. var 方法名 = function(形式参数列表){
              -           方法体
              -      }
              -2. 方法:
              -
              -3. 属性:
              -   length:代表形参的个数
              -4. 特点:
              -   1. 方法定义是,形参的类型不用写,返回值类型也不写。
              -   2. 方法是一个对象,如果定义名称相同的方法,会覆盖
              -   3. 在JS中,方法的调用只与方法的名称有关,和参数列表无关
              -   4. 在方法声明中有一个隐藏的内置对象(数组),**arguments**,封装所有的实际参数
              -5. 调用:
              -   方法名称(实际参数列表);
              -
              -
              /**
              * 求任意个数的和
              */
              function add (){
              var sum = 0;
              for (var i = 0; i < arguments.length; i++) {
              sum += arguments[i];
              }
              return sum;
              }

              var sum = add(1,2,3,4);
              alert(sum);
              - -
              Global
                -
              1. 特点:全局对象,这个Global中封装的方法不需要对象就可以直接调用。 方法名();

                -
              2. -
              3. 方法:
                encodeURI():url编码
                decodeURI():url解码

                -

                encodeURIComponent():url编码,编码的字符更多
                decodeURIComponent():url解码

                -

                parseInt():将字符串转为数字

                -
                  -
                • 逐一判断每一个字符是否是数字,直到不是数字为止,将前边数字部分转为number
                  isNaN():判断一个值是否是NaN
                    -
                  • NaN六亲不认,连自己都不认。NaN参与的==比较全部问false
                  • -
                  -
                • -
                -

                eval():讲 JavaScript 字符串,并把它作为脚本代码来执行。

                -
                var str = "http://www.baidu.com?wd=传智播客";
                var encode = encodeURI(str);
                document.write(encode +"<br>");//%E4%BC%A0%E6%99%BA%E6%92%AD%E5%AE%A2
                var s = decodeURI(encode);
                document.write(s +"<br>");//传智播客


                var str1 = "http://www.baidu.com?wd=传智播客";
                var encode1 = encodeURIComponent(str1);
                document.write(encode1 +"<br>");//%E4%BC%A0%E6%99%BA%E6%92%AD%E5%AE%A2
                var s1 = decodeURIComponent(encode);
                document.write(s1 +"<br>");//传智播客

                var jscode = "alert(123)";
                eval(jscode);
              4. -
              -

              DOM

              image-20221224162341993

              -

              image-20221224162540306

              -
              document
              获取元素对象
              getElementById();
              getElementsByTagName();//通过标签名
              getElementsByClassName();
              getElementsByName();//注意是数组
              - -
              创建DOM对象
              createAttribute(name);
              createComment();
              createElement();
              createTextNode();
              - -
              Element
              removeAttribute();
              setAttribute(属性名,属性值);
              - -
              Node

              说了树结构后,这个就好理解多了。

              -

              image-20221224164239507

              -
              练习:动态表格
              <!DOCTYPE html>
              <html lang="ch">
              <head>
              <meta charset="UTF-8">
              <title>动态表格</title>
              <style>
              #input{
              width: 60%;
              margin: auto;
              margin-top: 50px;
              }
              table{
              margin: auto;
              margin-top: 100px;
              width: 70%;
              text-align: center;
              }
              </style>
              </head>
              <body>
              <div id="input">
              <input type="text" placeholder="请输入编号" id="id">
              <input type="text" placeholder="请输入姓名" id="name">
              <input type="text" placeholder="请输入性别" id="gender">
              <input type="button" id="add_but" value="添加">
              </div>
              <table border="1px solid black" id="table">
              <tr>
              <title>学生信息表</title>
              <td>编号</td>
              <td>姓名</td>
              <td>性别</td>
              <td>操作</td>
              </tr>
              </table>
              <script>
              //获取表对象
              let table = document.getElementById("table");

              //最后那列删除要用很多次,所以这里只写一份原始的,之后要用再copy del_col对象即可。
              //但是要注意,js的深拷贝 object.cloneNode(true)是不会拷贝事件绑定的
              //所以事件绑定不得不放在下面的函数里做了
              let del_col = document.createElement("td");
              let del_col_a = document.createElement("a");
              del_col_a.href="javascript:void(0);";
              del_col_a.innerHTML="删除";
              del_col.appendChild(del_col_a);

              //删除列
              function del_row(obj){
              let target = obj.parentNode.parentNode;
              table.removeChild(target);
              }
              //添加行
              function add_row(id,name,gender){
              if (id=="" || name=="" || gender=="") return;
              let row = document.createElement("tr");
              let col1 = document.createElement("td");
              let col2 = document.createElement("td");
              let col3 = document.createElement("td");
              col1.innerHTML = id;
              col2.innerHTML = name;
              col3.innerHTML = gender;

              //为“删除”绑定事件
              let col4 = del_col.cloneNode(true);
              col4.lastChild.onclick = function(){
              //通过this定位。此时this指代a标签。
              //如果在del_row内把obj换成this反倒是不行的,因为那时候的this会指代的是window
              del_row(this);
              };

              row.appendChild(col1);
              row.appendChild(col2);
              row.appendChild(col3);
              row.appendChild(col4);

              table.appendChild(row);
              }

              //为“添加”按钮绑定事件
              document.getElementById("add_but").onclick = function(){
              add_row(document.getElementById("id").value,
              document.getElementById("name").value,
              document.getElementById("gender").value);
              };
              </script>
              </body>
              </html>
              - -

              老师标答值得学习借鉴的点:

              -
              //使用innerHTML添加
              document.getElementById("btn_add").onclick = function() {
              //2.获取文本框的内容
              var id = document.getElementById("id").value;
              var name = document.getElementById("name").value;
              var gender = document.getElementById("gender").value;

              //获取table
              var table = document.getElementsByTagName("table")[0];

              //追加一行
              table.innerHTML += "<tr>\n" +
              " <td>"+id+"</td>\n" +
              " <td>"+name+"</td>\n" +
              " <td>"+gender+"</td>\n" +
              " <td><a href=\"javascript:void(0);\" onclick=\"delTr(this);\" >删除</a></td>\n" +
              " </tr>";
              }
              - -
              事件
              练习:全选/全不选/反选+行变色

              image-20221225192124809

              -
              <!DOCTYPE html>
              <html lang="ch">
              <head>
              <meta charset="UTF-8">
              <title>动态表格</title>
              <style>
              #input{
              width: 60%;
              margin: auto;
              margin-top: 50px;
              }
              table{
              margin: auto;
              margin-top: 100px;
              width: 70%;
              text-align: center;
              }
              </style>
              </head>
              <body>
              <div id="input">
              <input type="text" placeholder="请输入编号" id="id">
              <input type="text" placeholder="请输入姓名" id="name">
              <input type="text" placeholder="请输入性别" id="gender">
              <input type="button" id="add_but" value="添加">
              </div>
              <table border="1px solid black" id="table">
              <tr>
              <td>选择</td>
              <td>编号</td>
              <td>姓名</td>
              <td>性别</td>
              <td>操作</td>
              </tr>
              <tr>
              <td><input type="checkbox" class="box"></td>
              <td>1</td>
              <td>Lily</td>
              <td>female</td>
              <td><a href="#" >删除</a></td>
              </tr>
              <tr>
              <td><input type="checkbox" class="box"></td>
              <td>2</td>
              <td>Jack</td>
              <td>male</td>
              <td><a href="#" >删除</a></td>
              </tr>
              <tr>
              <td><input type="checkbox" class="box"></td>
              <td>3</td>
              <td>Peterson</td>
              <td>male</td>
              <td><a href="#" >删除</a></td>
              </tr>
              <tr>
              <td><input type="checkbox" class="box"></td>
              <td>4</td>
              <td>Mary</td>
              <td>female</td>
              <td><a href="#" >删除</a></td>
              </tr>
              </table>
              <div style="text-align: center;margin-top: 20px">
              <input type="button" id="select_all" value="全选">
              <input type="button" id="select_none" value="全不选">
              <input type="button" id="select_aside" value="反选">
              </div>
              <script>
              window.onload = function (){
              //行变色
              let rows = document.getElementsByTagName("tr");
              for (let i = 0; i < rows.length; i++) {
              rows[i].onmouseover = function (){
              rows[i].setAttribute("style","background: pink");
              };
              rows[i].onmouseout = function (){
              rows[i].removeAttribute("style");
              }
              }

              //三个按钮
              let select_all = document.getElementById("select_all");
              let select_none = document.getElementById("select_none");
              let select_aside = document.getElementById("select_aside");
              let boxes = document.getElementsByClassName("box");

              select_all.onclick = function(){
              for (let i = 0; i < boxes.length; i++) {
              boxes[i].checked = 1;
              }
              };
              select_none.onclick = function(){
              for (let i = 0; i < boxes.length; i++) {
              boxes[i].checked = 0;
              }
              };
              select_aside.onclick = function(){
              for (let i = 0; i < boxes.length; i++) {
              boxes[i].checked = !(boxes[i].checked);
              }
              };
              };
              </script>
              </body>
              </html>
              - -
              练习:表单校验
              <!DOCTYPE html>
              <html lang="ch">
              <head>
              <meta charset="UTF-8">
              <title>注册界面</title>
              <link rel="stylesheet" href="./2.css">
              </head>
              <body>
              <div id="log_in_box">
              <div id="log_in_text">
              <div id="log_in_text1">
              新用户注册
              </div>
              <div id="log_in_text2">
              USER REGISTER
              </div>
              </div>

              <div id="log_in_text3">
              已有账号?<font color = red>立即登录</font>
              </div>
              <div id="log_in_table">
              <form id="form">
              <table>
              <tr>
              <td class="td_left"><label for="username">用户名</label></td>
              <td class="td_right"><input type="text" name="username" id="username" placeholder="请输入用户名"></td>
              <td></td>
              </tr>

              <tr>
              <td class="td_left"><label for="password">密码</label></td>
              <td class="td_right"><input type="password" name="password" id="password" placeholder="请输入密码"></td>
              <td></td>
              </tr>

              <tr>
              <td class="td_left"><label for="email">Email</label></td>
              <td class="td_right"><input type="email" name="email" id="email" placeholder="请输入邮箱"></td>
              </tr>

              <tr>
              <td class="td_left"><label for="name">姓名</label></td>
              <td class="td_right"><input type="text" name="name" id="name" placeholder="请输入姓名"></td>
              </tr>

              <tr>
              <td class="td_left"><label for="tel">手机号</label></td>
              <td class="td_right"><input type="text" name="tel" id="tel" placeholder="请输入手机号"></td>
              </tr>

              <tr>
              <td class="td_left"><label>性别</label></td>
              <td class="td_right">
              <input type="radio" name="gender" value="male"> <span class="choice"></span>
              <input type="radio" name="gender" value="female"> <span class="choice"></span>
              </td>
              </tr>

              <tr>
              <td class="td_left"><label for="birthday">出生日期</label></td>
              <td class="td_right"><input type="date" name="birthday" id="birthday" placeholder="请输入出生日期"></td>
              </tr>

              <tr>
              <td class="td_left"><label for="checkcode" >验证码</label></td>
              <td class="td_right"><input type="text" name="checkcode" id="checkcode" placeholder="请输入验证码">
              <img id="img_check" src="img/verify_code.jpg">
              </td>
              </tr>


              <tr>
              <td colspan="2" align="center"><input type="submit" value="注册" id="submit"></td>
              </tr>
              </table>
              </form>
              </div>
              </div>

              <script>
              //在开始加载的时候绑定事件的好习惯
              window.onload = function(){
              document.getElementById("form").onsubmit = function (){
              return checkUsername()&&checkPassword();
              };
              document.getElementById("username").onblur = checkUsername;
              document.getElementById("password").onblur = checkPassword;
              }
              let username = document.getElementById("username");
              let password = document.getElementById("password");

              function checkUsername(){
              let reg_username = /^\w{6,12}$/;
              let flag = reg_username.test(username.value);
              if (flag){
              username.parentNode.parentNode.lastElementChild.innerHTML = "<img src=\"./img/gou.png\" alt=\"\">";
              }
              else{
              username.parentNode.parentNode.lastElementChild.innerHTML = "<font color=\"red\">格式错误!</font>";
              }
              return flag;
              }

              function checkPassword(){
              let reg_username = /^\w{6,12}$/;
              let flag = reg_username.test(password.value);
              if (flag){
              password.parentNode.parentNode.lastElementChild.innerHTML = "<img src=\"./img/gou.png\" alt=\"\">";
              }
              else{
              password.parentNode.parentNode.lastElementChild.innerHTML = "<font color=\"red\">格式错误!</font>";
              }
              return flag;
              }

              </script>
              </body>
              </html>
              - -

              BOM

              浏览器对象模型,将浏览器各个组成部分封装成对象。

              -

              image-20221224152804747

              -

              Window对象包含DOM对象。

              -

              组成:Window、Navigator、Screen、History、Location

              -
              Window

              不需要创建,直接用window.使用,也可以直接用方法名。比如alert

              -
              方法
                -
              1. 与弹出有关的方法

                -

                alert:弹出警告框; confirm:确认取消对话框。确定返回true;prompt:输入框。参数为输入提示,返回值为输入值。

                -
              2. -
              3. 与开关有关的方法

                -

                close:关闭调用的window对象的浏览器窗口;open:打开新窗口,可传入URL,返回新的window对象

                -
              4. -
              5. 定时器

                -
                //只执行一次
                setTimeout();
                clearTimeout();
                //间隔执行多次
                setInterval();
                clearInterval();
                - -
                //一次性定时器
                //setTimeout("fun();",2000);
                var id = setTimeout(fun,2000);
                //取消
                clearTimeout(id);
                function fun(){
                alert('boom~~');
                }

                //循环定时器
                var id = setInterval(fun,2000);
                clearInterval(id);
              6. -
              -
              属性
                -
              1. 获取其他BOM对象

                -

                history、location、navigator、screen

                -
              2. -
              3. 获取DOM对象

                -

                document

                -
              4. -
              -
              练习:轮播图
              <!DOCTYPE html>
              <html lang="ch">
              <head>
              <meta charset="UTF-8">
              <title>轮播图</title>
              </head>
              <body>
              <img src="./img/banner_1.jpg" width="100%" id="picture">
              <script>
              let pictures = ["./img/banner_1.jpg","./img/banner_2.jpg","./img/banner_3.jpg"];
              let i = 0;
              let img = document.getElementById("picture");
              function change_picture(){
              img.src = pictures[(++i)%3];
              }
              setInterval(change_picture,2000);
              </script>
              </body>
              </html>
              - -
              Location
                -
              1. 刷新

                -

                location.reload方法

                -
              2. -
              3. 设置或返回完整的url

                -

                location.href属性

                -
              4. -
              -
              练习:自动返回首页
              <!DOCTYPE html>
              <html lang="ch">
              <head>
              <meta charset="UTF-8">
              <title>自动跳转</title>
              </head>
              <body>
              <div style="width: 235px; height: 100px; margin: auto">
              <span style="color: red" id="second">5</span>秒后,自动跳转首页
              </div>
              <script>
              let num = 5;
              let second = document.getElementById("second");
              function dao_ji_shi(){
              if (num == 0){
              location.href = "https://www.baidu.com/";
              clearInterval(id);
              }
              second.innerHTML = (num--);
              }
              let id = setInterval(dao_ji_shi,1000);
              </script>
              </body>
              </html>
              - -

              Bootstrap

              web前端框架

              -

              image-20221225160246028

              -

              快速入门

              image-20221225161243229

              -

              基本模板:

              -
              <!DOCTYPE html>
              <html lang="zh-CN">
              <head>
              <meta charset="utf-8">
              <meta http-equiv="X-UA-Compatible" content="IE=edge">
              <meta name="viewport" content="width=device-width, initial-scale=1">
              <!-- 上述3个meta标签*必须*放在最前面,任何其他内容都*必须*跟随其后! -->
              <title>Bootstrap 101 Template</title>

              <link href="./css/bootstrap.min.css" rel="stylesheet">
              </head>
              <body>
              <h1>你好,世界!</h1>

              <script src="./jquery.min.js"></script>
              <script src="js/bootstrap.min.js"></script>
              </body>
              </html>
              - -

              响应式布局

              实现依赖于栅格系统。

              -

              栅格系统

              将一行平均分成12个格子,可以指定元素占几个格子

              -

              可以感受到,其实跟我们之前那个纯纯HTML做页面的思想是差不多的,都是把整个页面看做一个表,表有很多行,每行有不同的格子。

              -
              基本原理
                -
              1. 定义容器。相当于之前的table
              2. -
              -
                -
              • 容器分类

                -
                  -
                1. container:两边留白
                2. -
                3. container-fluid:每一种设备都是100%宽度
                4. -
                -
              • -
              -
                -
              1. 定义行。相当于之前的tr 样式:row
              2. -
              3. 定义元素。指定该元素在不同的设备上,所占的格子数目。样式:col-设备代号-格子数目
              4. -
              -
                -
              • 设备代号:

                -
                  -
                1. xs:超小屏幕 手机 (<768px):col-xs-12
                2. -
                3. sm:小屏幕 平板 (≥768px)
                4. -
                5. md:中等屏幕 桌面显示器 (≥992px)
                6. -
                7. lg:大屏幕 大桌面显示器 (≥1200px)
                8. -
                -
              • -
              -
              使用方法
              <!DOCTYPE html>
              <html lang="zh-CN">
              <head>
              <meta charset="utf-8">
              <meta http-equiv="X-UA-Compatible" content="IE=edge">
              <meta name="viewport" content="width=device-width, initial-scale=1">
              <!-- 上述3个meta标签*必须*放在最前面,任何其他内容都*必须*跟随其后! -->
              <title>Bootstrap HelloWorld</title>

              <!-- Bootstrap -->
              <link href="css/bootstrap.min.css" rel="stylesheet">


              <!-- jQuery (Bootstrap 的所有 JavaScript 插件都依赖 jQuery,所以必须放在前边) -->
              <script src="js/jquery-3.2.1.min.js"></script>
              <!-- 加载 Bootstrap 的所有 JavaScript 插件。你也可以根据需要只加载单个插件。 -->
              <script src="js/bootstrap.min.js"></script>
              <style>
              .inner{
              border:1px solid red;
              }

              </style>
              </head>
              <body>
              <!--1.定义容器-->
              <div class="container-fluid">
              <!--2.定义行-->
              <div class="row">
              <!--3.定义元素
              在大显示器一行12个格子
              在pad上一行6个格子
              -->
              <div class="col-lg-1 col-sm-2 inner">栅格</div>
              <div class="col-lg-1 col-sm-2 inner">栅格</div>
              <div class="col-lg-1 col-sm-2 inner">栅格</div>
              <div class="col-lg-1 col-sm-2 inner">栅格</div>
              <div class="col-lg-1 col-sm-2 inner">栅格</div>
              <div class="col-lg-1 col-sm-2 inner">栅格</div>
              <!--超出12个格子的部分自动换行-->
              <div class="col-lg-1 col-sm-2 inner">栅格</div>
              <div class="col-lg-1 col-sm-2 inner">栅格</div>
              <div class="col-lg-1 col-sm-2 inner">栅格</div>
              <div class="col-lg-1 col-sm-2 inner">栅格</div>
              <div class="col-lg-1 col-sm-2 inner">栅格</div>
              <div class="col-lg-1 col-sm-2 inner">栅格</div>
              </div>

              </div>

              </body>
              </html>
              - -

              样式

              看文档。

              -

              练习:用bootstrap优化旅游网站首页

              代码

              HTML
              <!DOCTYPE html>
              <html lang="zh-CN">
              <head>
              <meta charset="utf-8">
              <meta http-equiv="X-UA-Compatible" content="IE=edge">
              <meta name="viewport" content="width=device-width, initial-scale=1">
              <!-- 上述3个meta标签*必须*放在最前面,任何其他内容都*必须*跟随其后! -->
              <title>Bootstrap 101 Template</title>

              <link href="./css/bootstrap.min.css" rel="stylesheet">
              <link rel="stylesheet" href="./3.css">
              </head>
              <body>
              <div class="container-fluid">
              <div class="row">
              <img src="./image/top_banner.jpg" class="img-responsive" id="top">
              </div>

              <div class="row" id="top_2">
              <div class="col-md-3">
              <img src="./image/logo.jpg" class="img-responsive" id="logo">
              </div>
              <div class="col-md-6" id="search">
              <input type="text" placeholder="Search" id="search_text">
              <div id="search_but">
              <a href="#">搜索</a>
              </div>
              </div>
              <div class="col-md-3">
              <img src="./image/hotel_tel.png" alt="hotel">
              </div>
              </div>

              <div class="row">
              <nav class="navbar navbar-default">
              <div class="container-fluid">
              <!-- Brand and toggle get grouped for better mobile display -->
              <div class="navbar-header">
              <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1" aria-expanded="false">
              <span class="sr-only">Toggle navigation</span>
              <span class="icon-bar"></span>
              <span class="icon-bar"></span>
              <span class="icon-bar"></span>
              </button>
              <a class="navbar-brand" href="#">传智播客</a>
              </div>

              <!-- Collect the nav links, forms, and other content for toggling -->
              <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
              <ul class="nav navbar-nav">
              <li><a href="#">Android <span class="sr-only">(current)</span></a></li>
              <li><a href="#">Android</a></li>
              <li><a href="#">Android</a></li>
              <li><a href="#">Android</a></li>
              <li><a href="#">Android</a></li>
              <li><a href="#">Android</a></li>
              <li><a href="#">Android</a></li>
              </ul>
              </div><!-- /.navbar-collapse -->
              </div><!-- /.container-fluid -->
              </nav>
              </div>

              <!-- 轮播图-->
              <div class="row">
              <div id="carousel-example-generic" class="carousel slide" data-ride="carousel">
              <!-- Indicators -->
              <ol class="carousel-indicators">
              <li data-target="#carousel-example-generic" data-slide-to="0" class="active"></li>
              <li data-target="#carousel-example-generic" data-slide-to="1"></li>
              <li data-target="#carousel-example-generic" data-slide-to="2"></li>
              </ol>

              <!-- Wrapper for slides -->
              <div class="carousel-inner" role="listbox">
              <!-- 注意只能有其中一张的class是active的-->
              <div class="item active">
              <img src="./image/banner_1.jpg" alt="...">
              <div class="carousel-caption">
              </div>
              </div>
              <div class="item">
              <img src="./image/banner_2.jpg" alt="...">
              <div class="carousel-caption">
              </div>
              </div>
              <div class="item">
              <img src="./image/banner_3.jpg" alt="...">
              <div class="carousel-caption">
              </div>
              </div>
              </div>

              <!-- Controls -->
              <a class="left carousel-control" href="#carousel-example-generic" role="button" data-slide="prev">
              <span class="glyphicon glyphicon-chevron-left" aria-hidden="true"></span>
              <span class="sr-only">Previous</span>
              </a>
              <a class="right carousel-control" href="#carousel-example-generic" role="button" data-slide="next">
              <span class="glyphicon glyphicon-chevron-right" aria-hidden="true"></span>
              <span class="sr-only">Next</span>
              </a>
              </div>
              </div>

              <div class="container">
              <div class="row">
              <img src="./image/icon_5.jpg" alt="亲子周边旅游节">
              <span class="text1">黑马精选</span>
              <hr>
              </div>
              <div class="row block">
              <div class="xuanchuan col-md-3">
              <img src="./image/jiangxuan_1.jpg" alt="" >
              <br>上海飞三亚五天4晚自由行(春节销售+亲子+蜜月+自由行)<br>
              <font size = 3 color="#8b0000">¥899</font>
              </div>
              <div class="xuanchuan col-md-3">
              <img src="./image/jiangxuan_1.jpg" alt="" >
              <br>上海飞三亚五天4晚自由行(春节销售+亲子+蜜月+自由行)<br>
              <font size = 3 color="#8b0000">¥899</font>
              </div>
              <div class="xuanchuan col-md-3">
              <img src="./image/jiangxuan_1.jpg" alt="">
              <br>上海飞三亚五天4晚自由行(春节销售+亲子+蜜月+自由行)<br>
              <font size = 3 color="#8b0000">¥899</font>
              </div>
              <div class="xuanchuan col-md-3">
              <img src="./image/jiangxuan_1.jpg" alt="">
              <br>上海飞三亚五天4晚自由行(春节销售+亲子+蜜月+自由行)<br>
              <font size = 3 color="#8b0000">¥899</font>
              </div>
              </div>


              <div class="row">
              <img src="./image/icon_6.jpg" alt="亲子周边旅游节">
              <span class="text1">国内游</span>
              <hr>
              </div>
              <div class="row">
              <div class="col-md-4">
              <img src="./image/guonei_1.jpg" alt="亲子周边旅游节">
              </div>
              <div class="col-md-8">
              <div class="xuanchuan col-md-4">
              <img src="./image/jiangxuan_2.jpg" alt="" >
              <br>上海飞三亚五天4晚自由行(春节销售+亲子+蜜月+自由行)<br>
              <font size = 3 color="#8b0000">¥899</font>
              </div>
              <div class="xuanchuan col-md-4">
              <img src="./image/jiangxuan_1.jpg" alt="" >
              <br>上海飞三亚五天4晚自由行(春节销售+亲子+蜜月+自由行)<br>
              <font size = 3 color="#8b0000">¥899</font>
              </div>
              <div class="xuanchuan col-md-4">
              <img src="./image/jiangxuan_1.jpg" alt="" >
              <br>上海飞三亚五天4晚自由行(春节销售+亲子+蜜月+自由行)<br>
              <font size = 3 color="#8b0000">¥899</font>
              </div>
              <div class="xuanchuan col-md-4">
              <img src="./image/jiangxuan_1.jpg" alt="" >
              <br>上海飞三亚五天4晚自由行(春节销售+亲子+蜜月+自由行)<br>
              <font size = 3 color="#8b0000">¥899</font>
              </div>
              <div class="xuanchuan col-md-4">
              <img src="./image/jiangxuan_1.jpg" alt="">
              <br>上海飞三亚五天4晚自由行(春节销售+亲子+蜜月+自由行)<br>
              <font size = 3 color="#8b0000">¥899</font>
              </div>
              <div class="xuanchuan col-md-4">
              <img src="./image/jiangxuan_1.jpg" alt="">
              <br>上海飞三亚五天4晚自由行(春节销售+亲子+蜜月+自由行)<br>
              <font size = 3 color="#8b0000">¥899</font>
              </div>
              </div>
              </div>
              </div>
              <div class="row">
              <img src="./image/footer_service.png" alt="" class="img-responsive">
              <div id="foot_text" class="col-md-12">
              <font color="gray" size = 2>江苏传智播客教育科技股份有限公司 版权所有Copyright 2006-2018&copy;, All Rights Reserved 苏ICP备16007882</font>
              </div>
              </div>
              </div>

              <script src="./jquery-3.2.1.min.js"></script>
              <script src="js/bootstrap.min.js"></script>
              </body>
              </html>
              - -
              CSS
              *{
              margin: 0px;
              padding: 0px;
              }

              #search{
              text-align: center;
              }

              #top_2{
              margin-top: 10px;
              }

              #search_text{
              border: 3px solid orange;
              width: 400px;
              height: 50px;
              margin-right: 0px;
              text-align: left;
              padding: 15px;
              }

              #search_but{
              margin: 0px;
              width: 90px;
              height: 50px;
              display: inline-block;
              background: orange;
              box-sizing: border-box;
              vertical-align: top;
              padding: 13px;
              }

              #search_but a{
              color: white;
              }

              .text1{
              font-size: 15px;
              }

              hr{
              margin: 3px;
              border: 1px solid orange;
              background: orange;
              }

              .xuanchuan{
              padding: 6px;
              margin: auto;
              height: 244px;
              border: 1px solid dimgray;
              text-align: center;
              }

              .xuanchuan img{
              width: 90%;
              }

              .block{
              text-align: center;
              }

              #foot_text{
              height: 60px;
              width: 100%;
              background: orange;
              text-align: center;
              padding: 15px;
              }

              .row{
              margin-top: 10px;
              margin-bottom: 10px;
              }
              - -

              注意点

              这东西写了我还挺久的。。。不过收获也挺多。

              -
                -
              1. text-align

                -

                是一个css属性,我觉得挺好用的(。我不知道它精确是什么意思,但我发现它好像有种能让该元素下的子元素水平居中的效果。

                -
              2. -
              3. 关于“容器”的理解

                -

                上面说过,Bootstrap有个容器的概念,跟我们上面纯HTML的表格概念其实是很类似的。

                -

                HTML的容器是表格标签,Bootsrap的容器是container-fluid和container类的标签。

                -

                与HTML的表格相同,“容器”也是可以嵌套的。这点在本案例体现为一下两点:

                -

                ① container中可以嵌套container-fluid。

                -

                ​ 案例中,页首-轮播图和页尾这两段是两边不留白的,轮播图-页尾这段是两边留白的。所以,我们就可以让整体为一个container容器,中间一段再用container-fluid容器包装起来。也即:

                -
                <body>
                <div class="container-fluid">
                <div class="row"></div>
                <div class="container"></div>
                <div class="row"></div>
                </div>
                </body>
                - -

                注意,此处不要作死为了优雅统一性这样写:

                -
                <div class="row container"></div>
                - -

                也即多加一个row类。要不row的属性会覆盖掉container的。

                -

                ② 对于“col-md-4”这些的理解

                -

                image-20221225183910393

                -

                在做这样的包含row-span元素的行时,之前的解决方案是采用表格嵌套。同样的,这里也可以采用容器嵌套。而此时,列的书写方式就比较特殊了。

                -
                <div class="row">
                <div class="col-md-4">
                row-span的图片
                </div>

                <div class="col-md-8">
                <div class="col-md-4"></div>
                <div class="col-md-4"></div>
                <div class="col-md-4"></div>
                <div class="col-md-4"></div>
                <div class="col-md-4"></div>
                <div class="col-md-4"></div>
                </div>
                </div>
                - -

                其实是非常直观的,相信以后你看到这段应该也能理解(。提示一点,栅格系统其实好像是相对于父类的。也就是说,不是“把整个页面分成12个格子”,而是,“把父类占有的空间分成12个格子”。

                -
              4. -
              5. 关于hr标签

                -

                使用css改颜色时应该写background: orange;而不是color: orange;

                -
              6. -
              -

              XML

              xml叫做可扩展标签语言。它的全部标签都是自定义的。

              -

              image-20221225220356932

              -

              快速入门

                -
              • 基本语法:
                  1. xml文档的后缀名 .xml
                -  2. xml第一行必须定义为文档声明
                -  3. xml文档中有且仅有一个根标签
                -  4. 属性值必须使用引号(单双都可)引起来
                -  5. 标签必须正确关闭
                -  6. xml标签名称区分大小写
                -
                -
              • -
              -
              <?xml version="1.0" encoding="utf-8" ?>

              <users>
              <user id="1">
              <name>zhangsan</name>
              <age>23</age>
              <gender>male</gender>
              </user>
              <user id="2">
              <name>zhangsan</name>
              <age>23</age>
              <gender>male</gender>
              </user>
              <user id="3">
              <name>zhangsan</name>
              <age>23</age>
              <gender>male</gender>
              </user>
              </users>
              - -

              细说

              语法

              文档声明

              常见属性:version[必须]、encoding、standalone[取值为yes和no,yes为依赖其他文件]

              -
              属性

              id属性值唯一

              -
              文本

              image-20221225222029698

              -

              这种要转义来转义去的显然很麻烦。所以就需要用到CDATA区。

              -

              CDATA区的文本会被原样展示。

              -
              <code>

              <![CDATA[
              if(1 == 1 && 2 == 2){}
              ]]>

              </code>
              - -

              约束

              只能写约束文件内的标签

              -
              dtd
              文档
              //students标签里可以包含若干个student标签
              <!ELEMENT students (student*) >
              //student标签必须按顺序出现name,age,sex标签
              <!ELEMENT student (name,age,sex)>
              //name、age、sex都为字符串类型
              <!ELEMENT name (#PCDATA)>
              <!ELEMENT age (#PCDATA)>
              <!ELEMENT sex (#PCDATA)>
              //student标签有一个属性叫number,类型为ID,并且必须要有。类型为ID表示该属性值唯一。
              <!ATTLIST student number ID #REQUIRED>
              - -
              引入方式
              //外部引入
              <!DOCTYPE 根标签名 SYSTEM "dtd文件的位置">
              <!DOCTYPE 根标签名 PUBLIC "dtd文件名字" "dtd文件的位置URL">
              - -

              或者可以直接在xml内部写:

              -
              <?xml version="1.0" encoding="UTF-8" ?>
              <!DOCTYPE students SYSTEM "student.dtd">

              <!DOCTYPE students [

              <!ELEMENT students (student+) >
              <!ELEMENT student (name,age,sex)>
              <!ELEMENT name (#PCDATA)>
              <!ELEMENT age (#PCDATA)>
              <!ELEMENT sex (#PCDATA)>
              <!ATTLIST student number ID #REQUIRED>


              ]>
              <students>

              <student number="s001">
              <name>zhangsan</name>
              <age>abc</age>
              <sex>hehe</sex>
              </student>

              <student number="s002">
              <name>lisi</name>
              <age>24</age>
              <sex>female</sex>
              </student>

              </students>
              - -
              使用
              <student number="s001">
              <name>zhangsan</name>
              <age>abc</age>
              <sex>hehe</sex>
              </student>

              <student number="s002">
              <name>lisi</name>
              <age>24</age>
              <sex>female</sex>
              </student>
              - -
              schema
              文档
              <?xml version="1.0"?>
              <xsd:schema xmlns="http://www.itcast.cn/xml"
              xmlns:xsd="http://www.w3.org/2001/XMLSchema"
              targetNamespace="http://www.itcast.cn/xml" elementFormDefault="qualified">

              <xsd:element name="students" type="studentsType"/>
              //定义类型
              <xsd:complexType name="studentsType">
              //类型里面有有序序列
              <xsd:sequence>
              <xsd:element name="student" type="studentType" minOccurs="0" maxOccurs="unbounded"/>
              </xsd:sequence>
              </xsd:complexType>

              <xsd:complexType name="studentType">
              <xsd:sequence>
              <xsd:element name="name" type="xsd:string"/>
              <xsd:element name="age" type="ageType" />
              <xsd:element name="sex" type="sexType" />
              </xsd:sequence>
              <xsd:attribute name="number" type="numberType" use="required"/>
              </xsd:complexType>

              <xsd:simpleType name="sexType">
              <xsd:restriction base="xsd:string">
              //枚举类型
              <xsd:enumeration value="male"/>
              <xsd:enumeration value="female"/>
              </xsd:restriction>
              </xsd:simpleType>

              <xsd:simpleType name="ageType">
              <xsd:restriction base="xsd:integer">
              <xsd:minInclusive value="0"/>
              <xsd:maxInclusive value="256"/>
              </xsd:restriction>
              </xsd:simpleType>

              <xsd:simpleType name="numberType">
              <xsd:restriction base="xsd:string">
              //正则匹配
              <xsd:pattern value="heima_\d{4}"/>
              </xsd:restriction>
              </xsd:simpleType>
              </xsd:schema>
              - -
              引入方式
              <?xml version="1.0" encoding="UTF-8" ?>
              <!--
              1.填写xml文档的根元素
              2.引入xsi前缀. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
              3.引入xsd文件命名空间. xsi:schemaLocation="http://www.itcast.cn/xml student.xsd"
              4.为每一个xsd约束声明一个前缀,作为标识 xmlns="http://www.itcast.cn/xml"


              -->
              <students xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
              xmlns="http://www.itcast.cn/xml"
              xsi:schemaLocation="http://www.itcast.cn/xml student.xsd"
              >
              <student number="heima_0001">
              <name>tom</name>
              <age>18</age>
              <sex>male</sex>
              </student>

              </students>
              - -

              它这意识就是,每个schema文件都要起一个别名,比如xsi:schemaLocation="http://www.itcast.cn/xml student.xsd"这行代码实际上就是把student.xsd的别名起为了http://www.itcast.cn/xml

              -

              为什么要起名呢?这就类似于命名空间这种东西,你要用到一个标签需要指明这个标签从哪来的,比如std::vectorClass.toString这种。这种命名空间在xml里叫前缀。所以,实际上完整写法应该是<http://www.itcast.cn/xml:students>

              -

              但这样显然太麻烦了,别名一般都是这种网址,写起来太长了。所以我们选择给别名起别名,设置方法为xmlns:a="http://www.itcast.cn/xml",这样一来,以后就不用写<http://www.itcast.cn/xml:students>,只用写<a:students>了。

              -

              但是如果每个都写一个前缀还是有点难顶。所以就引入了一个空前缀。这样写<students>这样没有前缀的标签,就相当于从空前缀那个命名空间里拿出来的了。当然如果有多个命名空间,还是得区分一下的。

              -

              解析

              image-20221225234106078

              -

              解析方式有两种方法。

              -
              解析方法
              DOM

              将标记语言文档一次性加载进内存,形成DOM树

              -

              操作方便,可以进行CRUD所有操作;但占内存

              -
              SAX

              逐行读取,基于事件驱动

              -

              不占内存;但只能读取

              -
              解析工具

              image-20221225234649287

              -

              主要学习Jsoup。

              -
              快速入门

              image-20221225234813135

              -

              跟前面html的DOM是差不多的。

              -
              public class JsoupDemo {
              public static void main(String[] args) throws IOException {
              //获取Document对象,根据xml文档
              //解析xml文档(加载进内存且获取dom树)
              Document doc = Jsoup.parse(new File(JsoupDemo.class.getClassLoader()
              .getResource("student.xml").getPath()),"utf-8");
              //获取元素对象
              //Elements extends ArrayList<Element>
              Elements ele = doc.getElementsByTag("name");
              //获取元素里的数据
              System.out.println(ele.get(0).text());
              }
              }
              - -
              细说
                -
              1. Jsoup:工具类,可以解析html或xml文档,返回Document

                -
                  -
                • parse:解析html或xml文档,返回Document
                    -
                  • parse(File in, String charsetName):解析xml或html文件的。
                  • -
                  • parse(String html):解析xml或html字符串
                  • -
                  • parse(URL url, int timeoutMillis):通过网络路径获取指定的html或xml的文档对象
                  • -
                  -
                • -
                -
              2. -
              3. Document:文档对象。代表内存中的dom树

                -
                  -
                • 获取Element对象
                    -
                  • getElementById(String id):根据id属性值获取唯一的element对象
                  • -
                  • getElementsByTag(String tagName):根据标签名称获取元素对象集合
                  • -
                  • getElementsByAttribute(String key):根据属性名称获取元素对象集合
                  • -
                  • getElementsByAttributeValue(String key, String value):根据对应的属性名和属性值获取元素对象集合
                  • -
                  -
                • -
                -
              4. -
              5. Elements:元素Element对象的集合。可以当做 ArrayList来使用

                -
              6. -
              7. Element:元素对象

                -
                  -
                1. 获取子元素对象
                2. -
                -

                这一点很好理解。因为Document和Element对象的获取元素方法都继承自Node结点,本意就是获取子元素对象。只不过Document是根节点,所以就变成了获取所有元素对象。

                -
                  -
                • getElementById(String id):根据id属性值获取唯一的element对象
                • -
                • getElementsByTag(String tagName):根据标签名称获取元素对象集合
                • -
                • getElementsByAttribute(String key):根据属性名称获取元素对象集合
                • -
                • getElementsByAttributeValue(String key, String value):根据对应的属性名和属性值获取元素对象集合
                • -
                -
                  -
                1. 获取属性值
                2. -
                -
                  -
                • String attr(String key):根据属性名称获取属性值
                • -
                -
                  -
                1. 获取文本内容
                2. -
                -
                  -
                • String text():获取字标签的所有纯文本内容
                • -
                • String html():获取标签体的所有内容(包括字标签的字符串内容)
                • -
                -
              8. -
              9. Node:节点对象

                -
                  -
                • 是Document和Element的父类
                • -
                -
              10. -
              -
              快速查找
                -
              1. 使用选择器selector

                -

                其实语法格式跟css的那个选择器差不多。

                -
                /**
                *选择器查询
                */
                public class JsoupDemo5 {
                public static void main(String[] args) throws IOException {
                //1.获取student.xml的path
                String path = JsoupDemo5.class.getClassLoader().getResource("student.xml").getPath();
                //2.获取Document对象
                Document document = Jsoup.parse(new File(path), "utf-8");

                //3.查询name标签
                /*
                div{

                }
                */
                Elements elements = document.select("name");
                System.out.println(elements);
                System.out.println("=----------------");
                //4.查询id值为itcast的元素
                Elements elements1 = document.select("#itcast");
                System.out.println(elements1);
                System.out.println("----------------");
                //5.获取student标签并且number属性值为heima_0001的age子标签
                //5.1.获取student标签并且number属性值为heima_0001
                Elements elements2 = document.select("student[number=\"heima_0001\"]");
                System.out.println(elements2);
                System.out.println("----------------");

                //5.2获取student标签并且number属性值为heima_0001的age子标签
                Elements elements3 = document.select("student[number=\"heima_0001\"] > age");
                System.out.println(elements3);

                }

                }
              2. -
              3. 使用XPath

                -

                XPath:xml路径语言。

                -

                XPath API文档

                -
                /**
                *XPath查询
                */
                public class JsoupDemo6 {
                public static void main(String[] args) throws IOException, XpathSyntaxErrorException {
                //1.获取student.xml的path
                String path = JsoupDemo6.class.getClassLoader().getResource("student.xml").getPath();
                //2.获取Document对象
                Document document = Jsoup.parse(new File(path), "utf-8");

                //3.根据document对象,创建JXDocument对象
                JXDocument jxDocument = new JXDocument(document);

                //4.结合xpath语法查询
                //4.1查询所有student标签
                List<JXNode> jxNodes = jxDocument.selN("//student");
                for (JXNode jxNode : jxNodes) {
                System.out.println(jxNode);
                }

                System.out.println("--------------------");

                //4.2查询所有student标签下的name标签
                List<JXNode> jxNodes2 = jxDocument.selN("//student/name");
                for (JXNode jxNode : jxNodes2) {
                System.out.println(jxNode);
                }

                System.out.println("--------------------");

                //4.3查询student标签下带有id属性的name标签
                List<JXNode> jxNodes3 = jxDocument.selN("//student/name[@id]");
                for (JXNode jxNode : jxNodes3) {
                System.out.println(jxNode);
                }
                System.out.println("--------------------");
                //4.4查询student标签下带有id属性的name标签 并且id属性值为itcast

                List<JXNode> jxNodes4 = jxDocument.selN("//student/name[@id='itcast']");
                for (JXNode jxNode : jxNodes4) {
                System.out.println(jxNode);
                }
                }

                }
              4. -
              -

              第四部分 JavaWeb核心

              Tomcat

              概述

              概述

              image-20221226154531990

              -

              Tomcat是Java相关的web服务器软件。

              -

              tomcat目录结构

              image-20221226155107723

              -

              image-20221226155143920启动

              -

              启动时出现的问题

              省流:看系统环境变量有没有CATALINA_HOME这一项,并且看这个CATALINA_HOME的值是否与当前版本安装路径相符合。

              -

              我电脑上本来也有了一个tomcat,只不过跟老师版本不一样。我把这两个都安到同一个目录了。然后我启动了老师版本,却发现输入localhost:8080没有任何响应。我首先去看了一下tomcat的config下的server.xml,发现端口号确实是8080没问题。然后试图访问localhost,发现没有响应,故推测是此处发生了问题。因而我上网按照该教程做了一遍:

              -

              127.0.0.1 拒绝了我们的连接请求–访问本地IP时显示拒绝访问

              -

              我重启电脑后,再次启动老师版本,发现还是不行。这时我开始怀疑是否我的tomcat没有正常启动,或者是否是因为8080这个端口号冲突了。所以我又找了一下如何查看端口号占用情况:

              -

              如何查看端口号是否被占用

              -

              netstat -a命令即可。我便发现,在我开着tomcat的情况下,8080这个端口没有被使用。说明好像启动不大正常。于是我打开了另一篇回答:

              -

              tomcat 启动了,为什么没打开 8080 端口?

              -

              按照它说的去查看日志文件。发现老师版本的tomcat下的log目录为空。我就去我本安装的版本下的log目录去看了,惊奇地发现,原来我在使用老师版本的tomcat时,tomcat用的是老版本的log目录。也就是说,很有可能config目录也是用的老版本的。我去查看老版本的config,发现端口是8888。于是我把老师版本的tomcat卸载了,去访问localhost:8888,成功力。

              -

              我探寻了以下原因,发现tomcat的startup里面如此写道:

              -
              if not "%CATALINA_HOME%" == "" goto gotHome
              :gotHome
              if exist "%CATALINA_HOME%\bin\catalina.bat" goto okHome
              :okHome
              rem ....
              call "%EXECUTABLE%" start %CMD_LINE_ARGS%
              :end
              - -

              这一段大概是在找到tomcat这个软件的位置。如果我们在环境变量里面设置了CATALINA_HOME,那么就会直接把软件位置定位到CATALINA_HOME的值的地方,随后之后的逻辑都在那边执行。

              -

              我发现我确实设置了这个CATALINA_HOME,并且:

              -

              image-20221226170930763

              -

              它的值是我电脑原本有的老版本的目录!

              -

              故而,这也就说明了为什么老师的版本不去用自己的log,不去用自己的config,而用的是我电脑上的老版本的log,config了。。。

              -

              image-20221226172653916

              -

              配置

                -
              • 部署项目的方式:

                -
                  -
                1. 直接将项目放到webapps目录下即可。 * /hello:项目的访问路径–>虚拟目录 * 简化部署:将项目打成一个war包,再将war包放置到webapps目录下。

                  -
                    -
                  • war包会自动解压缩
                  • -
                  -
                2. -
                3. 配置conf/server.xml文件
                  <Host>标签体中配置
                  <Context docBase="C:\aWorkSpace\Projects\Java\JavaWeb" path="/web" />

                  -
                4. -
                -
                然后之后访问时输入`localhost/web/JavaWeb.html`即可
                -
                -* docBase:项目存放的路径
                -* path:虚拟目录
                -
                -
                  -
                1. 在conf\Catalina\localhost创建任意名称的xml文件。在文件中编写
                  <Context docBase="C:\aWorkSpace\Projects\Java\JavaWeb" />
                    -
                  • 虚拟目录:xml文件的名称
                  • -
                  -
                2. -
                -
                注意,该方法是热部署的。也就是说,可以不关闭服务器的情况下,去增删xml文件,会马上变化,而不是像上面两种方式一样重启生效。
                -
                -
              • -
              -

              动态项目目录结构

              项目都存放在webapp里。打开webapp中的任一个。

              -

              image-20221226221811747

              -

              WEB-INF下是动态资源,也就是Java控制的一些文件【大概这个意思】。有这个文件夹的项目是动态项目。

              -

              WEB-INF以外的都是静态资源。

              -

              image-20221226221933346

              -

              tomcat集成到IDEA

              使用maven创建Web项目
              更换maven镜像源

              idea中Maven镜像源详细配置步骤(对所有项目)

              -
              创造项目

              image-20221226235520519

              -

              然后等着它开始下载就行了。

              -

              最后的目录结构:

              -

              image-20221226235607604

              -

              如果java或者resources目录没有,自己建就行。

              -
              加入tomcat

              1.

              -

              TOMCAT -> IDEA

              -

              2.

              -

              还有另一种更便捷的方式,就是直接添加maven的tomcat插件。在pom.xml文件里加入此段:

              -
              <build>
              <plugins>
              <plugin>
              <groupId>org.apache.tomcat.maven</groupId>
              <artifactId>tomcat7-maven-plugin</artifactId>
              <version>2.2</version>
              </plugin>
              </plugins>
              </build>
              - -

              即可,可用alt+insert自动补全。

              -

              这里我出现了一个飘红报错问题,用这个可以解决:

              -

              maven学习 & Plugin ‘org.apache.tomcat.maven:tomcat7-maven-plugin:2.2’ not found报错解决【问题及解决过程记录】

              -

              然后,右键项目就可以run了:

              -

              image-20221227003805955

              -

              如果没有此选项,就去下载maven helper插件。

              -
              修改tomcat配置参数
              图形化界面

              run-edit configuration-tomcat

              -
              配置文件

              image-20221230221339275

              -

              启动服务器时控制台前几句输出有一句这样的。对应目录下的就可以找到tomcat配置文件。

              -

              Servlet

              server applet运行在服务器端的小程序

              -

              servlet是java编写的服务器端的程序,运行在web服务器中。作用:接收用户端发来的请求,调用其他java程序来处理请求,将处理结果返回到服务器中

              -

              image-20221227154053743

              -

              servlet是接口,定义了Java类被tomcat执行、被浏览器访问的规则。

              -

              image-20221227154222612

              -

              快速入门

              image-20221227154841102

              -

              这里的配置用的是注解,具体原理在第一部分的JavaSE基础里有详细描述了。

              -
              -

              使用maven创建web项目见上面的tomcat-tomcat集成到IDEA-使用maven创建web项目

              -
              -
              -

              如果已经导入依赖坐标却还未生效,就点击右侧侧边栏的maven刷新。

              -

              Maven导入依赖后还不出现Servlet的问题

              -
              -

              原理

              执行原理

                -
              • 执行原理:
                  -
                1. 当服务器接受到客户端浏览器的请求后,会解析请求URL路径,获取访问的Servlet的资源路径
                2. -
                3. 查找web.xml文件,是否有对应的标签体内容。
                4. -
                5. 如果有,则在找到对应的全类名【注意:在下面,url-pattern都使用注解配置方法了,所以这两步应该是不用了,应该会变成这样:① 逐个遍历注册的servlet实现类,查看其注解属性是否为对应的url-pattern。② 如果有,则找到类名,步骤继续】
                6. -
                7. tomcat会将字节码文件加载进内存,并且创建其对象
                8. -
                9. 调用其方法
                10. -
                -
              • -
              -

              生命周期

              image-20221227161516964

              -

              并发安全

              Servlet的init方法只执行一次,一种Servlet在内存中只存在一个对象,Servlet是单例的。因而,当多线程同时访问同一个Servlet对象时,就会产生线程安全问题。所以有需要的话,就要采取手段保障Servlet类的线程安全性。

              -

              体系结构

              为了简化开发,我们可以用提供的servlet的实现类。

              -

              image-20221227163713317

              -

              GenericServlet

              除了service方法之外的方法,差不多都只做了空实现。所以只需写service方法即可。

              -
              @WebServlet("/demo2")
              public class Servletdemo2 extends GenericServlet {
              public void service(ServletRequest servletRequest, ServletResponse servletResponse){
              }
              }
              - -

              HttpServlet

              使用

              比如httpservlet,就只用重写里面的doGet和doPost两个方法就行。

              -
              @WebServlet("/demo2")
              public class Servletdemo2 extends HttpServlet {
              @Override
              protected void doGet(HttpServletRequest req, HttpServletResponse resp){
              System.out.println("get!!!");
              }

              @Override
              protected void doPost(HttpServletRequest req, HttpServletResponse resp){
              System.out.println("post!!!");
              }
              }

              - -

              这两个方法的区别就是,当使用get方式提交表单,就会执行第一个方法;使用post则会执行第二个方法。

              -

              比方说post时:

              -

              网页代码如下(放在webapp目录下)

              -
              <!DOCTYPE html>
              <html lang="en">
              <head>
              <meta charset="UTF-8">
              <title>Title</title>
              </head>
              <body>
              Hello,World!
              <!-- action内写Servlet的资源路径 -->
              <form action="/webdemo4_war/demo2" method="post">
              name: <input type="text" name="username" id="username" placeholder="请输入用户名">
              <input type="submit" value="submit">
              </form>
              </body>
              </html>
              - -

              servlet代码同上。

              -

              最终在网页中点击提交

              -

              image-20221230172048250

              -

              会跳转到\demo页面【也即servlet的访问路径】,并且在console打印“post!!!”

              -
              -

              为啥会这样呢?

              -

              之前在讲表单的时候说过,form的action属性代表着提交时这个表单会提交给谁,值为一个URL。所以,这里action的值设置为Servlet的路径,意思就是把表单数据发送给了Servelet,由于使用的是post方式,因此触发了Servlet的doPost方法。Servlet对得到的数据进行各种处理,并且通过req和resp进行交互。

              -
              -
              -

              为什么此处写的是“\demo”这样的路径?

              -

              事实上这是一个相对路径。

              -

              image-20221230215927404

              -

              部署的根路径可以在 run-edit configuration-tomcat-deployment中找到。

              -
              -
              深层一些的问题
              分成get和post

              之所以这两种方法需要分别处理,是因为在Servlet的service方法中,其实是要对req对象进行参数分解,这两种方法分解方式不一样。

              -

              按照以往,我们需要这样写

              -
               public void service(ServletRequest servletRequest, ServletResponse servletResponse) {
              String method = ((HttpServletRequest)servletRequest).getMethod();
              if("GET".equals(method)){
              //执行get的逻辑
              }
              else if ("POST".equals(method)){
              //执行post的逻辑
              }
              }
              - -

              就类似于可以这么写:

              -
              public void service(ServletRequest servletRequest, ServletResponse servletResponse){
              String method = ((HttpServletRequest)servletRequest).getMethod();
              if("GET".equals(method)){
              doGet(servletRequest,servletResponse);
              }
              else if ("POST".equals(method)){
              doPost(servletRequest,servletResponse);
              }
              }
              - -

              于是最后就融合入httpservlet了。

              -

              url-pattern配置

                -
              1. 一个Servlet可以定义多个访问路径 : @WebServlet({“/d4”,”/dd4”,”/ddd4”})

                -
              2. -
              3. 路径定义规则:

                -
                  -
                1. /xxx:路径匹配如/demo、/*【第一个优先级大于第二个】
                2. -
                3. /xxx/xxx:多层路径,目录结构
                4. -
                5. *.do:扩展名匹配不能在前面加’/‘。也即:
                  @WebServlet("*.do")
                  - -url访问填写http://localhost/webdemo4_war/*.do
                6. -
                -
              4. -
              -

              service参数

              image-20230102005833327

              -

              http协议

              概述

              概念:Hyper Text Transfer Protocol 超文本传输协议

              -
                -
              • 传输协议:定义了客户端和服务器端通信时发送数据的格式

                -
              • -
              • 特点:

                -
                  -
                1. 基于TCP/IP的高级协议需要先经历三次握手,可靠传输
                2. -
                3. 默认端口号:80
                  -

                  如果说域名是ip地址的简化表示,ip地址又表示着一台主机,那么使用http协议访问一个网址,相当于访问一台主机,并且端口号为80.

                  -
                  -
                4. -
                5. 基于请求/响应模型的:一次请求对应一次响应
                6. -
                7. 无状态的:每次请求之间相互独立,不能交互数据
                8. -
                -

                历史版本:

                -
                  -
                • 1.0:每一次请求响应都会建立新的连接
                • -
                • 1.1:复用连接
                • -
                -
              • -
              -
              报文格式
              请求

              客户端发送给服务器端的消息

              -

              数据格式:

              -
                -
              1. 请求行
                请求方式 请求url 请求协议/版本
                GET /login.html HTTP/1.1

                -
                  -
                • 请求方式:
                    -
                  • HTTP协议有7中请求方式,常用的有2种
                      -
                    • GET:
                        -
                      1. 请求参数在请求行中【在url后】
                      2. -
                      3. 请求的url长度有限制的
                      4. -
                      5. 不太安全
                      6. -
                      -
                    • -
                    • POST:
                        -
                      1. 请求参数在请求体中
                      2. -
                      3. 请求的url长度没有限制的
                      4. -
                      5. 相对安全
                      6. -
                      -
                    • -
                    -
                  • -
                  -
                • -
                -
              2. -
              3. 请求头:客户端浏览器告诉服务器一些信息
                请求头名称: 请求头值

                -
                  -
                • 常见的请求头:

                  -
                    -
                  1. User-Agent:浏览器告诉服务器,我访问你使用的浏览器版本信息 * 可以在服务器端获取该头的信息,解决浏览器的兼容性问题

                    -
                  2. -
                  3. Accept:可以支持的响应格式

                    -
                  4. -
                  5. Accept-language:可以支持的语言环境

                    -
                  6. -
                  7. Referer:http://localhost/login.html * 告诉服务器,我(当前请求)从哪里来?

                    -
                      -
                    • 作用:
                    • -
                    -
                      -
                    1. 防盗链:image-20230101235437317如果ref头非合法就不播放
                    2. -
                    3. 统计工作:看从哪个网站来的人数多
                    4. -
                    -
                  8. -
                  9. Connection:连接是否活着

                    -
                  10. -
                  -
                • -
                -
              4. -
              5. 请求空行
                空行,就是用于分割POST请求的请求头,和请求体的。

                -
              6. -
              7. 请求体(正文):

                -
                  -
                • 封装POST请求消息的请求参数的
                • -
                -
              8. -
              -

              字符串格式:

              -
              //请求行
              POST /login.html HTTP/1.1
              //请求头
              Host: localhost
              User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:60.0) Gecko/20100101 Firefox/60.0
              Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
              Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
              Accept-Encoding: gzip, deflate
              Referer: http://localhost/login.html
              Connection: keep-alive
              Upgrade-Insecure-Requests: 1
              //请求空行

              //请求体
              username=zhangsan
              - - - -
              响应

              响应消息:服务器端发送给客户端的数据

              -

              数据格式:

              -
                -
              1. 响应行

                -
                  -
                1. 组成:协议/版本 响应状态码 状态码描述 HTTP/1.1 200 OK

                  -
                2. -
                3. 响应状态码:服务器告诉客户端浏览器本次请求和响应的一个状态, 状态码都是3位数字. 分类:

                  -
                    -
                  1. 1xx:服务器接收客户端消息,但没有接收完成,等待一段时间后,发送1xx多状态码,询问是否还要继续发

                    -
                  2. -
                  3. 2xx:成功。代表:200

                    -
                  4. -
                  5. 3xx:重定向。代表:302(重定向),304(访问缓存)

                    -

                    image-20230103151112791

                    -

                    需要自动重定向到另一个C去

                    -

                    image-20230103151237984

                    -

                    发现资源未变化且本地有缓存

                    -
                  6. -
                  7. 4xx:由客户端造成的错误

                    -

                    代表:

                    -
                      -
                    1. 404(请求路径没有对应的资源,可能路径输错了)

                      -
                    2. -
                    3. 405:请求方式没有对应的doXxx方法

                      -

                      当我们在Servlet中未重写doXXX方法,就默认不能用此方法进行访问。因为doXXX方法的默认实现为:

                      -
                      String protocol = req.getProtocol();
                      String msg = lStrings.getString("http.method_get_not_supported");
                      if (protocol.endsWith("1.1")) {
                      resp.sendError(405, msg);
                      } else {
                      resp.sendError(400, msg);
                      }
                    4. -
                    -
                  8. -
                  9. 5xx:服务器端错误。

                    -

                    代表:500(服务器内部出现Exception)

                    -
                    int i = 3/0;
                  10. -
                  -
                4. -
                -
              2. -
              3. 响应头:

                -
                  -
                1. 格式: [头名称 : 值]

                  -
                2. -
                3. 常见的响应头:

                  -
                    -
                  1. Content-Type:服务器告诉客户端本次响应体 数据格式以及编码格式

                    -

                    浏览器依照编码格式来对该页面进行解码。

                    -
                  2. -
                  3. Content-disposition:服务器告诉客户端以什么格式打开响应体数据

                    -
                      -
                    • 值:
                        -
                      • in-line:默认值,在当前页面内打开
                      • -
                      • attachment;filename=xxx:以附件形式打开响应体。也即点击超链接后开始文件下载
                      • -
                      -
                    • -
                    -
                  4. -
                  -
                4. -
                -
              4. -
              5. 响应空行

                -
              6. -
              7. 响应体:传输的数据

                -
              8. -
              -

              字符串格式:

              -
              //响应行
              HTTP/1.1 200 OK
              //响应头
              Content-Type: text/html;charset=UTF-8
              Content-Length: 101
              Date: Wed, 06 Jun 2018 07:08:42 GMT
              //响应空行

              //响应体
              <html>
              <head>
              <title>$Title$</title>
              </head>
              <body>
              hello , response
              </body>
              </html>
              - - - -

              Request

              继承体系结构

              ServletRequest(I) - HttpServletRequest(I) - RequestFacade(C)[tomcat创建]

              -
              功能
              获取请求行
                -
              1. 获取请求方式 POST

                -
                String getMethod()
              2. -
              3. 获取虚拟目录 /webdemo

                -
                String getContextPath()
              4. -
              5. 获取Servlet路径 /demo1

                -
                String getServletPath()
              6. -
              7. 获取get方式请求参数 name=zhangsan

                -

                &分割每个键值对

                -
                String getQueryString()
              8. -
              9. 获取请求URI和URL

                -
                //  /webdemo/demo1
                String getRequestURI();

                // http://localhost/webdemo/demo1
                StringBuffer getRequestURL();
                - -
                -

                URL:统一资源定位符 : http://localhost/day14/demo1 中华人民共和国
                URI:统一资源标识符 : /day14/demo1 共和国

                -

                URI的代表范围更大

                -
                -
              10. -
              11. 获取协议及版本 HTTP/1.1

                -
                String getProtocol()
              12. -
              13. 获取访问的客户机的IP地址

                -
                String getRemoteAddr()
              14. -
              -
              获取请求头
                -
              1. 通过请求头的名称获取请求头的值

                -
                String getHeader(String name)
              2. -
              3. 获取所有的请求头名称

                -
                Enumeration<String> getHeaderNames()
                - -

                返回的是一个迭代器

                -
              4. -
              -
              public class Servletdemo2 extends GenericServlet {

              public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
              HttpServletRequest req = (HttpServletRequest) servletRequest;
              Enumeration<String> enumerator = req.getHeaderNames();
              while(enumerator.hasMoreElements()){
              String name = enumerator.nextElement();
              System.out.println(name);
              //System.out.println(name+"-----"+req.getHeader(name));
              }
              }
              }

              输出结果:
              host
              connection
              sec-ch-ua
              sec-ch-ua-mobile
              sec-ch-ua-platform
              upgrade-insecure-requests
              user-agent
              accept
              purpose
              sec-fetch-site
              sec-fetch-mode
              sec-fetch-user
              sec-fetch-dest
              accept-encoding
              accept-language
              cookie
              - -

              这些请求头名称正是上面的键值对里的键。

              -
              获取请求体

              request将请求体中的数据封装成了流。如果数据是字符,那就是字符流;是视频这种的字节,那就是字节流。

              -
              * 步骤:
              -    1. 获取流对象
              -  *  BufferedReader getReader():获取字符输入流,只能操作字符数据
              -  *  ServletInputStream getInputStream():获取字节输入流,可以操作所有类型数据
              -    2. 操作流获取数据
              -
              -
              @WebServlet("/demo2")
              public class Servletdemo2 extends GenericServlet {

              public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
              HttpServletRequest req = (HttpServletRequest) servletRequest;
              BufferedReader bfr = req.getReader();
              String line;
              while((line = bfr.readLine())!=null){
              System.out.print(line);
              }
              }
              }
              - -

              请求体中键值对会在一行里,用&分割

              -
              -

              获取时中文乱码

              -
                -
              • get方式:tomcat 8 已经将get方式乱码问题解决了

                -
              • -
              • post方式:会乱码

                -
                                   * 解决:在获取参数前,设置流的编码:
                -
                -    
                request.setCharacterEncoding("utf-8");
                -
                -
              • -
              -
              -
              获取请求参数通用的方法(通用指对get和post通用)

              这里的请求参数应该是指上面Post的请求体、Get的请求行里的参数,请求头里的参数是获取不到的。

              -
                -
              1. 根据参数名称获取参数值

                -
                String getParameter(String name)
                - -

                如 username=zs&password=123,getParameter(“username”)会得到zs。

                -
              2. -
              3. 根据参数名称获取参数值的数组

                -
                String[] getParameterValues(String name)
                - -

                如 hobby=xx&hobby=game,会得到{xx,game}

                -
              4. -
              5. 获取所有请求的参数名称

                -
                Enumeration<String> getParameterNames()
              6. -
              7. 取所有参数的map集合

                -
                Map<String,String[]> getParameterMap()
              8. -
              -
              请求转发

              在服务器内部资源跳转

              -

              image-20230102195615676

              -

              AServlet做了一部分事情,把剩余的事情交给BServlet去做

              -

              步骤:

              -
                -
              1. 通过request对象获取请求转发器对象

                -
                RequestDispatcher getRequestDispatcher(String path)
              2. -
              3. 使用RequestDispatcher对象来进行转发

                -
                requestDispatcher.forward(ServletRequest request, ServletResponse response) 
              4. -
              -
              @WebServlet("/demo2")
              public class Servletdemo2 extends GenericServlet {

              public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
              System.out.println("I am "+Servletdemo2.class.getName());
              //进行转发
              servletRequest.getRequestDispatcher("/demo3")
              .forward(servletRequest,servletResponse);
              }
              }

              @WebServlet("/demo3")
              public class ServletDemo3 extends GenericServlet {

              public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
              System.out.println("I am "+ServletDemo3.class.getName());
              }
              }
              - -

              特点:

              -
                -
              1. 浏览器地址栏路径不变
              2. -
              3. 只能在服务器内部跳转,只能转发到服务器内部的资源中
              4. -
              5. 转发是一次请求,多个资源使用的是同一次请求
              6. -
              -
              共享数据

              接力工作的两个Servlet可以通过request对象进行数据通信。

              -
              * 方法:
              -
              -
                -
              1. 存储键值对

                -
                void setAttribute(String name,Object obj)
              2. -
              3. 获取值

                -
                Object getAttitude(String name)
              4. -
              5. 移除键值对

                -
                void removeAttribute(String name)
              6. -
              -
              获取ServletContext
              ServletContext getServletContext()
              - -
              练习:结合数据库与Servlet进行用户登录
              -

              要求:

              -

              1.编写login.html登录页面
              username & password 两个输入框
              2.使用Druid数据库连接池技术,操作mysql,day14数据库中user表
              3.使用JdbcTemplate技术封装JDBC
              4.登录成功跳转到SuccessServlet展示:登录成功!用户名,欢迎您
              5.登录失败跳转到FailServlet展示:登录失败,用户名或密码错误

              -
              -
              文件结构

              ![屏幕截图 2023-01-02 235207](./JavaWeb/屏幕截图 2023-01-02 235207.png)

              -
              -

              错误历程

              -
                -
              1. lib目录位置错误

                -

                NoClassDefFoundError解决方案一开始lib目录没放进web-inf,通过此文章得知错误为包未引入,再由下面这篇文章得知lib目录放置错误

                -

                JDBC Template报错:java.lang.ClassNotFoundException: org.springframework.jdbc.core.RowMapper

                -
              2. -
              3. druid.properties文件位置错误

                -

                报错

                -

                java.lang.NullPointerException at java.util.Properties$LineReader.readLine(Properties.java:434)

                -

                ,报错位置在pro.load(JDBCUtils.class.getClassLoader().getResourceAsStream("druid.properties"));

                -

                由文章

                -

                关于java.lang.NullPointerException at java.util.Properties$LineReader.readLine(Properties.java:434)问题

                -

                回忆到,由于是使用类加载器获取文件流,故而要求druid.properties文件应该放在resource文件下。对于以前的项目,resource文件都默认是src文件夹。

                -

                但是这次放在src目录下还是不行。定睛一看它web项目文件结构中有一个硕大的resources……放在下面果然就好了。

                -
              4. -
              -
              -
              druid.properties
              driverClassName=com.mysql.jdbc.Driver
              url=jdbc:mysql://localhost:3306/helloworld
              username=root
              password=root
              initialSize=5
              maxActive=10
              maxWait=3000
              - -
              html界面
              <!DOCTYPE html>
              <html lang="en">
              <head>
              <meta charset="UTF-8">
              <title>Title</title>
              </head>
              <body>
              <!-- action内写Servlet的资源路径 -->
              <form action="/webdemo4_war/check" method="post">
              name: <input type="text" name="username" id="username" placeholder="请输入用户名">
              password: <input type="password" name="password" id="password" placeholder="请输入密码">
              <input type="submit" value="submit">
              </form>
              </body>
              </html>
              - -
              Servlet
              @WebServlet(value = "/fail")
              public class FailServlet extends HttpServlet {
              @Override
              protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
              doPost(req,resp);
              }

              @Override
              protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
              // 设置字符集,防止中文乱码
              resp.setContentType("text/html;charset=utf-8");
              resp.getWriter().write("登录失败,用户名或密码错误");
              }
              }
              - -
              @WebServlet(value = "/success")
              public class SuccessServlet extends HttpServlet {
              @Override
              protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
              doPost(req,resp);
              }

              @Override
              protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
              resp.setContentType("text/html;charset=utf-8");
              resp.getWriter().write("登录成功!"+req.getAttribute("uname")+",欢迎您");
              }
              }
              - -
              @WebServlet(value = "/check")
              public class CheckServlet extends HttpServlet {
              @Override
              protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
              doPost(req,resp);
              }

              @Override
              protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
              req.setCharacterEncoding("utf-8");

              //使用BeanUtils把Map转化为对象
              User tmp = new User();
              try {
              BeanUtils.populate(tmp,req.getParameterMap());
              } catch (IllegalAccessException e) {
              throw new RuntimeException(e);
              } catch (InvocationTargetException e) {
              throw new RuntimeException(e);
              }

              User res = UserDao.login(tmp);
              if (res == null)
              req.getRequestDispatcher("/fail").forward(req,resp);
              else{
              req.setAttribute("uname",res.getUname());
              req.getRequestDispatcher("/success").forward(req,resp);
              }

              }
              }
              - -
              -

              关于BeanUtils

              -

              BeanUtils工具类,简化数据封装, 用于封装JavaBean的

              -
                -
              1. JavaBean:标准的Java类

                -
                 1. 要求:
                -     1. 类必须被public修饰
                -     2. 必须提供空参的构造器
                -     3. 成员变量必须使用private修饰
                -     4. 提供公共setter和getter方法
                - 2. 功能:封装数据
                -
                -
              2. -
              3. 概念:

                -

                ​ 成员变量:
                ​ 属性:setter和getter方法截取后的产物

                -
                           例如:getUsername() --> Username--> username
                -
                -
              4. -
              5. 方法:

                -
                1. setProperty()
                -1. getProperty()
                -1. populate(Object obj , Map map):
                -
                -

                ​ 将map集合的键值对信息,封装到对应的JavaBean对象中

                -
              6. -
              -
              -
              JDBCUtils

              原封不动地照搬了:第二部分-数据库连接池-Druid-定义工具类 部分的代码。

              -
              UserDao
              public class UserDao {
              private static final JdbcTemplate jdbcTemplate = new JdbcTemplate(JDBCUtils.getDataSource());
              public static User login(User user){
              List<User> users = jdbcTemplate.query("select * from usr where uname = ? and pass = ?",
              new BeanPropertyRowMapper<User>(User.class),
              user.getUname(),user.getPass());
              if (users.size() == 0)
              return null;
              else
              return users.get(0);
              }
              }
              - -

              Response

              功能

              设置响应消息。

              -
              设置响应行

              设置状态码

              -
              setStatus(int sc);
              - -
              设置响应头
              setHeader(String name, String value) 
              - -
              设置响应体

              以流的方式传输数据。

              -

              使用步骤:

              -
                -
              1. 获取输出流

                -
                  -
                1. 字节输出流

                  -
                  ServletOutputStream getOutputStream()
                2. -
                3. 字符输出流

                  -
                  PrintWriter getWriter()
                4. -
                -
              2. -
              3. 使用输出流,将数据输出到客户端浏览器

                -
              4. -
              -
              案例
              重定向

              资源跳转的一种方式。

              -

              image-20230103153445565

              -
              @WebServlet("/demo1")
              public class ServletDemo extends HttpServlet {
              @Override
              protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
              System.out.println("I am demo1 "+req.hashCode());
              /* 重定向 */
              //设置状态码
              resp.setStatus(302);
              //要填的是完整资源路径。
              resp.setHeader("location","/practice_war/demo2");
              }

              @Override
              protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
              doGet(req,resp);
              }
              }
              - -
              @WebServlet("/demo2")
              public class ServletDemo2 extends HttpServlet {
              @Override
              protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
              System.out.println("I am demo2 "+req.hashCode());
              }

              @Override
              protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
              doGet(req,resp);
              }
              }
              - -
              输出:
              I am demo1 1675674230
              I am demo2 1675674230
              - -

              重定向的这几行代码其实是可以简化的:

              -
              /* 重定向 */
              //设置状态码
              resp.setStatus(302);
              //要填的是完整资源路径。
              resp.setHeader("location","/practice_war/demo2");
              - -

              可以简化为:

              -
              resp.sendRedirect("/practice_war/demo2");
              - -
              -

              关于req对象不一样,但hashcode值相同的解释:

              -

              hashcode很大程度与对象内存空间相关,与对象的具体内容没什么关系。两个对象拥有相同的hashcode有可能只是因为存储的内存空间位置大小都相同导致的。所以是因为两次的req对象都占用了同一个内存空间【JVM调度问题】,所以才让hashcode值相同。这两个对象实质上是不一样的。

              -
              -

              重定向的特点(与请求转发完全相反):

              -
                -
              1. 浏览器地址栏路径改变
              2. -
              3. 可以访问其他站点的资源
              4. -
              5. 使用多次请求,不能使用request对象共享数据
              6. -
              -

              路径写法:

              -
                -
              1. 相对路径:通过相对路径不可以确定唯一资源

                -
                  -
                • 规则:找到当前资源和目标资源之间的相对位置关系
                • -
                -
              2. -
              3. 绝对路径:通过绝对路径可以确定唯一资源

                -
                  -
                • 如:http://localhost/day15/responseDemo2 /day15/responseDemo2

                  -
                  <form action="/webdemo4_war/check" method="post">
                • -
                • 以/开头的路径

                  -
                • -
                • 规则:判断定义的路径是给谁用的?判断请求将来从哪儿发出

                  -
                    -
                  • 客户端浏览器使用:需要加虚拟目录(项目的访问路径)

                    -

                    比如说在页面中弄了个a标签,将来是要给客户端点的,那么这个a标签的href就要用绝对路径。

                    -

                    再比如说重定向:

                    -
                    //要填的是完整资源路径。
                    resp.setHeader("location","/practice_war/demo2");
                    - -

                    这个路径将来是给客户端将来要使用的路径,是客户端路径,所以要加虚拟目录。

                    - -
                  • -
                  • 服务器使用:不需要加虚拟目录

                    -

                    比如说之前的请求转发

                    -
                      -
                    • 转发路径
                    • -
                    -
                  • -
                  -
                • -
                -
              4. -
              -
              服务器输出字符数据到浏览器
              @WebServlet("/responseDemo4")
              public class ResponseDemo4 extends HttpServlet {
              protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

              //获取流对象之前,设置流的默认编码:ISO-8859-1 设置为:GBK
              response.setCharacterEncoding("utf-8");

              //告诉浏览器,服务器发送的消息体数据的编码。建议浏览器使用该编码解码
              response.setHeader("content-type","text/html;charset=utf-8");

              /* 也有设置编码的简单形式
              //简单的形式,设置编码
              response.setContentType("text/html;charset=utf-8");
              */

              //1.获取字符输出流
              PrintWriter pw = response.getWriter();
              //2.输出数据
              //pw.write("<h1>hello response</h1>");
              pw.write("你好啊啊啊 response");
              }

              protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
              this.doPost(request,response);
              }
              }
              - -
              服务器输出字节数据到浏览器
              @WebServlet("/responseDemo5")
              public class ResponseDemo5 extends HttpServlet {
              protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
              //依然要保证编码一致
              response.setContentType("text/html;charset=utf-8");

              //1.获取字节输出流
              ServletOutputStream sos = response.getOutputStream();
              //2.输出数据
              sos.write("你好".getBytes("utf-8"));
              }

              protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
              this.doPost(request,response);
              }
              }
              - -
              验证码
              @WebServlet("/demo1")
              public class ServletDemo extends HttpServlet {
              @Override
              protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
              //验证码图片大小
              final int width = 100;
              final int height = 50;

              /* 绘制验证码 */
              BufferedImage image = new BufferedImage(width,height,BufferedImage.TYPE_INT_RGB);
              Graphics pen = image.getGraphics();
              //绘制背景
              pen.setColor(new Color((int)(Math.random()*255),(int)(Math.random()*255),(int)(Math.random()*255)));
              pen.fillRect(0,0,width,height);
              //绘制边框
              pen.setColor(new Color((int)(Math.random()*255),(int)(Math.random()*255),(int)(Math.random()*255)));
              pen.drawRect(0,0,width-1,height-1);
              //随机填充字母数字
              String source = "QWERTYUIOPASDFGHJKLZXCVBNMqwertyuiopasdfghjklzxcvbnm1234567890";
              for (int i = 1; i <= 4; i++){
              int index = (int)(Math.random()*source.length());
              pen.drawString(source.substring(index,index+1),20*i,27);
              }
              //画干扰色线
              pen.setColor(new Color((int)(Math.random()*255),(int)(Math.random()*255),(int)(Math.random()*255)));
              for (int i = 0; i < 5; i++){
              pen.drawLine((int)(Math.random()*width),(int)(Math.random()*height),(int)(Math.random()*width),(int)(Math.random()*height));
              }

              //将图片输出
              ImageIO.write(image,"jpg",resp.getOutputStream());
              }

              @Override
              protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
              doGet(req,resp);
              }
              }
              - -
              <!DOCTYPE html>
              <html lang="en">
              <head>
              <meta charset="UTF-8">
              <title>Title</title>
              </head>
              <body>
              <img id="img" src="/practice_war/demo1"/>
              <a href="" id = "a">看不清?换一张</a>
              <script>
              window.onload = function (){
              let img = document.getElementById("img");
              let a = document.getElementById("a");
              img.onclick = function (){
              //加时间戳作为请求参数,为了防止浏览器不更换图片缓存
              img.src = "/practice_war/demo1?"+new Date().getTime();
              }
              a.onclick = img.onclick;
              }
              </script>
              </body>
              </html>
              - -

              ServletContext对象

              概念

              代表整个web应用,可以和servlet容器(服务器)通信

              -

              获取

              通过request对象获取
              ServletContext getServletContext()
              - -
              通过HttpServlet获取
              this.getContext();
              - -

              功能

              获取MIME类型

              MIME是在互联网通信过程中定义的一种文件数据类型

              -
                      * 格式: 大类型/小类型   text/html        image/jpeg
              -
              -
              /*
              @param: 文件的后缀扩展名,如.txt
              */
              String getMimeType(String file);
              - -

              image-20230107010006874

              -

              mime映射存在了服务器的xml文件中。

              -

              使用案例:

              -
              System.out.println(this.getServletContext().getMimeType("a.txt"));
              - - - -
              共享数据

              ServletContext是一个域对象,可以用来共享数据。

              -

              ServletContext代表着服务器,因而它的生命周期跟随服务器关闭而灭亡。ServletContext可以共享所有请求的数据。也就是说,任何一次请求,任何用户,看到的ServletContext域都是同一个。

              -

              这样大的效果也使得我们需要更加谨慎地使用它。一旦数据存入ServletContext域,就只会在服务器关闭后才会消亡,很耗内存。

              -
              获取文件的真实(服务器)路径
              String getRealPath();
              - -

              经测试发现,这东西只是起了一个字符串拼接的作用,是不会帮忙检查文件是否存在的。

              -

              学到这我顺便看了看文件放在不同的地方最后应该如何访问:

              -

              image-20230107012903495

              -

              这是最终部署项目文件夹的结构:

              -

              image-20230107013010276

              -

              可以看到只有bcd被保留了。它们的目录要这样获取:

              -
                  @Override
              protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
              ServletContext context = this.getServletContext();

              System.out.println(context.getRealPath("/WEB-INF/classes/b.txt"));
              System.out.println(context.getRealPath("/c.txt"));
              System.out.println(context.getRealPath("/WEB-INF/d.txt"));
              }
              /*输出结果:
              D:\aWorkStorage\etc\apache-tomcat-8.5.83\webapps\practice_war\WEB-INF\classes\b.txt
              D:\aWorkStorage\etc\apache-tomcat-8.5.83\webapps\practice_war\c.txt
              D:\aWorkStorage\etc\apache-tomcat-8.5.83\webapps\practice_war\WEB-INF\d.txt
              是我的电脑里tomcat的目录
              */
              - -

              案例:文件下载

              要求
                -
              • 文件下载需求:
                  -
                1. 页面显示超链接
                2. -
                3. 点击超链接后弹出下载提示框
                4. -
                5. 完成图片文件下载,那种会存到你电脑download目录下,而不是直接加载出来的
                6. -
                -
              • -
              -

              image-20230201170701909

              -

              用户点击下载->请求发送给某个servlet,servlet修改response->tomcat响应用户,传递的图片资源按照response的方法打开

              -
              代码

              说实话看了感觉有点难以下手,主要还是完全不知道html和servlet怎么交互造成的,看了老师讲解才有点恍然大悟。

              -

              我们可以把a标签以重定向的角度去看。它会新建一个request,然后发送到它的href中的那个url。在此处我们将url设置为/practice_war/download?filename=1.jpg,也即要以GET的方式发送给download,请求体为filename=1.jpg。然后servlet执行结束后,就会将信息存储在resp中返回给tomcat,由tomcat发送给用户。

              -
              html
              <body>
              <a href="/practice_war/download?filename=1.jpg" id = "a">点击下载图片</a>
              </body>
              - -
              servlet

              思路:

              -

              获取要下载的资源,并且将其输入到resp的stream中。

              -

              有一点需要非常注意:

              -
              resp.setContentType(this.getServletContext().getMimeType(path));
              resp.setHeader("content-disposition","attachment;filename="+name);
              - -

              必须要在把资源输入到resp的stream前设置好,精确来说是调用sos.write前设置好,不然无法起作用。

              -

              猜测是因为可能resp会根据disposition方式的不同而自动决策write的方式。

              -
              @Override
              protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
              //获取要下载的资源名称
              String name = req.getParameter("filename");

              //获取路径
              String path = this.getServletContext().getRealPath("/img/"+name);
              //使用字节流
              FileInputStream fis = new FileInputStream(path);
              //输出数据

              resp.setContentType(this.getServletContext().getMimeType(path));
              resp.setHeader("content-disposition",
              "attachment;filename="+
              // 为了防止中文乱码,需要针对不同的浏览器来进行编码
              DownLoadUtils.getFileName(req.getHeader("user-agent"),name));

              //获取字节输出流
              ServletOutputStream sos = resp.getOutputStream();
              byte[] buff = new byte[1024];
              int len = 0;
              while((len = fis.read(buff))!=-1){
              sos.write(buff,0,len);
              }
              //释放资源
              fis.close();

              // resp.setContentType(this.getServletContext().getMimeType(path));
              // resp.setHeader("content-disposition","attachment;filename="+name);
              }
              - -

              会话

              会话:一次会话中包含多次请求和响应。

              -
                -
              • 一次会话:浏览器第一次给服务器资源发送请求,会话建立,直到有一方断开为止

                -
              • -
              • 功能:请求之间本来是相互独立的。将多次请求组织在一次会话中,就可以让请求之间进行数据的共享。

                -
              • -
              • 方式:

                -
                  -
                • 客户端会话技术 Cookie

                  -

                  把数据存进客户端

                  -
                • -
                • 服务器端会话技术 Session

                  -

                  把数据存进服务器端

                  -
                • -
                -
              • -
              -

              概念

              客户端会话技术,将数据保存到客户端

              -

              快速入门

                -
              • 使用步骤:

                -
                  -
                1. 创建Cookie对象,绑定数据【为了从服务器端发送cookie给客户端】
                    -
                  • new Cookie(String name, String value)
                  • -
                  • 可以看到,Cookie其实就是一种name-value这样的键值对对象
                  • -
                  -
                2. -
                3. 发送Cookie对象【因为要发送给客户端,所以应该在response里存】
                    -
                  • response.addCookie(Cookie cookie)
                  • -
                  -
                4. -
                5. 获取Cookie,拿到数据【因为是来自客户端,所以要从request里要】
                    -
                  • Cookie[] request.getCookies()
                  • -
                  -
                6. -
                -
              • -
              • 代码

                -
                @WebServlet("/demo")
                public class ServletDemo extends HttpServlet {
                @Override
                protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
                response.addCookie(new Cookie("password","abc123"));
                }

                @Override
                protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
                this.doGet(request, response);
                }
                }
                - -
                @WebServlet("/demo2")
                public class ServletDemo2 extends HttpServlet {
                @Override
                protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
                System.out.println(request.getCookies());
                }

                @Override
                protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
                this.doGet(request, response);
                }
                }
              • -
              • 得到效果

                -

                运行服务器,首先访问/demo,然后在同一个浏览器再次访问/demo2,就可以在控制台看到输出。

                -

                这个过程发生了什么呢?

                -

                首先,访问/demo就相当于建立了会话。/demo的Servlet获取到请求之后,在response中将cookie填入。

                -

                保持浏览器窗口不变,会话也不变。

                -

                再次访问/demo2,cookie信息自动保存在request对象中。/demo2的Servlet获取到请求之后,在控制台中打印输出了cookie。

                -
              • -
              -

              细节学习

              一次发送多个cookie

              你看它那个API叫add,就知道数据结构差不多是个list,所以多次add就行。

              -
              保存时间

              默认情况下,浏览器关闭则cookie就马上被销毁。

              -

              如果需要持久化存储:

              -
              cookie.setMaxAge(int seconds)
              - -

              参数:

              -
                -
              1. 正数:将Cookie数据写到硬盘的文件中。持久化存储。并指定cookie存活时间,时间到后,cookie文件自动失效
              2. -
              3. 负数:默认值
              4. -
              5. 零:删除cookie信息
              6. -
              -
              中文问题

              在tomcat 8 之前 cookie中不能直接存储中文数据,需要将中文数据转码——一般采用URL编码(%E3)

              -

              在tomcat 8 之后,cookie支持中文数据。

              -
              获取范围
                -
              1. 假设在一个tomcat服务器中,部署了多个web项目,那么在这些web项目中cookie能不能共享?

                -
                  -
                • 默认情况下cookie不能共享

                  -
                • -
                • 共享方法:

                  -

                  setPath(String path):设置cookie的获取范围。默认情况下,设置当前的虚拟目录

                  -

                  如果要共享,则可以将path设置为”/“

                  -
                • -
                -
              2. -
              3. 不同的tomcat服务器间cookie共享问题?

                -

                比如说:

                -image-20230221225514567 - -
                  -
                • setDomain(String path):如果设置一级域名相同,那么多个服务器之间cookie可以共享

                  -

                  setDomain(".baidu.com"),那么tieba.baidu.com和news.baidu.com中cookie可以共享)

                  -
                • -
                -
              4. -
              -

              作用和特点

              特点:

              -
                -
              1. cookie存储数据在客户端浏览器

                -

                因而它相对不安全

                -
              2. -
              3. 浏览器对于单个cookie 的大小有限制(4kb) 以及 对同一个域名下的总cookie数量也有限制(20个)

                -
              4. -
              -

              作用:

              -
                -
              1. cookie一般用于存出少量的不太敏感的数据

                -
              2. -
              3. 在不登录的情况下,完成服务器对客户端的身份识别

                -

                比如说,以不登录情况下对某个网页进行属性设置,你下次打开的时候属性设置依然在,这是因为你的属性设置的cookie在设置后被存入到你的电脑中,下次访问该网页发出请求,服务器端就能根据请求中cookie里的属性设置信息来做出响应了。

                -
              4. -
              -

              案例:记住上一次访问时间

              需求:
              1. 访问一个Servlet,如果是第一次访问,则提示:您好,欢迎您首次访问。
              2. 如果不是第一次访问,则提示:欢迎回来,您上次访问时间为:显示时间字符串

              -
              public class ServletDemo extends HttpServlet {
              @Override
              protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
              //获取流对象之前,设置流的默认编码:ISO-8859-1 设置为:GBK
              response.setCharacterEncoding("utf-8");

              //告诉浏览器,服务器发送的消息体数据的编码。建议浏览器使用该编码解码
              response.setHeader("content-type","text/html;charset=utf-8");
              if(request.getCookies() != null)
              for(Cookie c : request.getCookies()){
              if(c.getName().equals("isfirst")){
              response.getWriter()
              .write("<h1>欢迎回来,您上次访问的时间为<h1>"+c.getValue());
              break;
              }
              }
              else
              response.getWriter().write("<h1>你好!欢迎你!<h1>");

              SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd-HH:mm:ss");
              Date date1 = new Date();
              String currentTime = dateFormat.format(date1);

              response.addCookie(new Cookie("isfirst",currentTime));
              }

              @Override
              protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
              this.doGet(request, response);
              }
              }
              - -

              Session

              概念

              服务器端会话技术,在一次会话的多次请求间共享数据,将数据保存在服务器端的对象中。HttpSession

              -

              应用场合

              比如说购物网站的购物车这种,就会存在session。想想也是(

              -
                -
              1. session用于存储一次会话的多次请求的数据,存在服务器端

                -

                比如说,当我们做重定向的时候,就可以选择用session共享数据(会话域)而非使用ServletContext(此范围过大)

                -
              2. -
              3. session可以存储任意类型,任意大小的数据

                -
              4. -
              5. session与Cookie的区别:

                -
                  -
                1. session存储数据在服务器端,Cookie在客户端
                2. -
                3. session没有数据大小限制,Cookie有
                4. -
                5. session数据安全,Cookie相对于不安全
                6. -
                -
              6. -
              -

              快速入门

                -
              1. 获取HttpSession对象:
                HttpSession session = request.getSession();

                -
              2. -
              3. 使用HttpSession对象:

                -
                  Object getAttribute(String name)  
                void setAttribute(String name, Object value)
                void removeAttribute(String name)

                #### 原理

                ![image-20230223102722575](./JavaWeb/image-20230223102722575.png)

                实现依赖于Cookie

                #### 细节

                前面说到,当客户端和服务器端有任何一端关闭之后,会话结束,在这种情况下,session在客户端和服务器端的保留情况不同。

                1. 当客户端关闭后,服务器不关闭,两次获取session是否为同一个?
                * 默认情况下。不是。
                * 如果需要相同,则可以创建Cookie,键为JSESSIONID,设置最大存活时间,让cookie持久化保存。
                ```java
                Cookie c = new Cookie("JSESSIONID",session.getId());
                c.setMaxAge(60*60);
                response.addCookie(c);
              4. -
              5. 客户端不关闭,服务器关闭后,两次获取的session是同一个吗?

                -
              6. -
              -
                -
              • 不是同一个,但是要确保数据不丢失。tomcat自动(IDEA不会活化)完成以下工作
                  -
                • session的钝化:(序列化)
                      * 在服务器正常关闭之前,将session对象序列化到硬盘上
                  -
                  -
                • -
                -
              • -
              -
                      * 具体是会放在这里:
              -
              -      ![image-20230223104447097](./JavaWeb/image-20230223104447097.png)
              -    
              -* session的活化:(反序列化
              -    * 在服务器启动后,将session文件转化为内存中的session对象即可。
              -
              -我想,cookie应该在这点上不会像session这么做,因为cookie本质上是保存在客户端的数据,按理来说服务器端把cookie发出去之后就可以销毁了,在服务器序列化一点意义都没有。
              -
              -
                -
              1. 销毁时间

                -
                  -
                1. 服务器关闭

                  -
                2. -
                3. session对象调用invalidate() 。

                  -
                4. -
                5. session默认失效时间 30分钟
                  选择性配置修改

                  -

                  可以在每个项目的子配置文件(如下图)或者总的项目的父配置文件apache-tomcat-8.5.83\conf\web.xml中配置

                  -

                  image-20230223105053170

                  -
                  <session-config>
                  <session-timeout>30</session-timeout>
                  </session-config>
                6. -
                -
              2. -
              -

              案例

              -

              需求:

              -
                -
              1. 访问带有验证码的登录页面login.jsp
              2. -
              3. 用户输入用户名,密码以及验证码。
                  -
                • 如果用户名和密码输入有误,跳转登录页面,提示:用户名或密码错误
                • -
                • 如果验证码输入有误,跳转登录页面,提示:验证码错误
                • -
                • 如果全部输入正确,则跳转到主页success.jsp,显示:用户名,欢迎您
                • -
                -
              4. -
              -
              -
              初见思路

              我们可以在服务器端使用session存储password和username的map,存储验证码图片编号和图片的map,然后用cookie携带验证码图片编号,在req中存储用户名和密码。

              -
              正确思路

              感觉我上面的思路是没有充分利用到session的性质,仅仅把它作为在服务器端存储数据的工具,

              -

              “在服务器端存储password和username的map,存储验证码图片编号和图片的map,然后用cookie携带验证码图片编号,在req中存储用户名和密码。”

              -

              这样也依然成立,跟session没半毛钱关系。我们可以这样使用session:

              -
                -
              1. 在服务器端存储password和username的map,存储验证码图片编号和图片的map
              2. -
              3. 当会话建立,由于没有cookie,故而session第一次创建。我们在session内写入验证码对应的编号,把图片通过response发送给客户端。
              4. -
              5. 会话端输入图片验证码后,按下submit按键,验证码存入request域,向服务器端发送请求
              6. -
              7. 服务器端Servlet从请求中get到验证码,然后在session中get到当前验证码的图片编号,向一开始存储的map查询数据,这样就能验证验证码是否正确了
              8. -
              -

              那么在这里为什么不用Cookie而使用session呢?大概是因为cookie不安全罢(慌乱)

              -
              代码
              jsp
              <html lang="en">
              <head>
              <meta charset="UTF-8">
              <title>Title</title>
              </head>
              <body>
              <form action="/practice_war/loginServlet" method="post">
              name: <input type="text" name="username" id="username" placeholder="请输入用户名">
              password: <input type="password" name="password" id="password" placeholder="请输入密码">
              verifycode:<input type="text" name="verifycode" id="verifycode" placeholder="请输入验证码">

              <img id="img" src="/practice_war/check"/>

              <input type="submit" value="submit">
              </form>
              <script>
              window.onload = function(){
              document.getElementById("img").onclick = function(){
              this.src = "/practice_war/check?"+new Date().getTime();
              }
              }

              </script>
              </body>
              </html>
              - -
              checkcode
              @WebServlet("/check")
              public class ServletDemo extends HttpServlet {
              @Override
              protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
              //验证码图片大小
              final int width = 100;
              final int height = 50;

              /* 绘制验证码 */
              BufferedImage image = new BufferedImage(width,height,BufferedImage.TYPE_INT_RGB);
              Graphics pen = image.getGraphics();
              //绘制背景
              pen.setColor(new Color((int)(Math.random()*255),(int)(Math.random()*255),(int)(Math.random()*255)));
              pen.fillRect(0,0,width,height);
              //绘制边框
              pen.setColor(new Color((int)(Math.random()*255),(int)(Math.random()*255),(int)(Math.random()*255)));
              pen.drawRect(0,0,width-1,height-1);
              //随机填充字母数字
              String source = "QWERTYUIOPASDFGHJKLZXCVBNMqwertyuiopasdfghjklzxcvbnm1234567890";

              StringBuilder verifyAnswer = new StringBuilder();

              for (int i = 1; i <= 4; i++){
              int index = (int)(Math.random()*source.length());
              verifyAnswer = verifyAnswer.append(source.charAt(index));
              pen.drawString(source.substring(index,index+1),20*i,27);
              }
              //画干扰色线
              pen.setColor(new Color((int)(Math.random()*255),(int)(Math.random()*255),(int)(Math.random()*255)));
              for (int i = 0; i < 5; i++){
              pen.drawLine((int)(Math.random()*width),(int)(Math.random()*height),(int)(Math.random()*width),(int)(Math.random()*height));
              }

              request.getSession().setAttribute("verifycode",verifyAnswer.toString());
              System.out.println("verify:"+verifyAnswer.toString());
              //将图片输出
              ImageIO.write(image,"jpg",response.getOutputStream());
              }

              @Override
              protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
              this.doGet(request, response);
              }
              }
              - -
              login
              @WebServlet("/loginServlet")
              public class loginServlet extends HttpServlet {
              @Override
              protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
              HttpSession session = request.getSession();
              String verifycode = request.getParameter("verifycode");
              System.out.println(flag);

              String ans = session.getAttribute("verifycode");
              if (ans == null||!ans.equals(verifycode)){
              session.removeAttribute("verifycode");
              // 重定向到错误界面
              request.getRequestDispatcher("/fail_code").forward(request,response);
              return;
              }
              session.removeAttribute("verifycode");

              // 进行密码账号匹配处理
              String username = request.getParameter("username");
              String password = request.getParameter("password");
              System.out.println(username+" "+password);
              if(UserDao.login(new User(username,password))){
              // 成功界面
              request.setAttribute("uname",username);
              request.getRequestDispatcher("/success").forward(request,response);
              }else{
              // 失败界面
              request.getRequestDispatcher("/fail").forward(request,response);
              }
              }

              @Override
              protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
              this.doGet(request, response);
              }
              }
              - -

              老师的写法是将错误信息直接写在原登录界面,和我的略有不同:

              -
              // in loginServlet
              if (!session.getAttribute("verifycode").equals(verifycode)){
              request.setAttribute("message","checkcode_fail");
              request.getRequestDispatcher("/login.jsp").forward(request,response);
              return;
              }
              - -
              // in login.jsp
              <%
              String message = (String) request.getAttribute("message");
              if(message != null){
              if(message.equals("checkcode_fail")){
              out.write("验证码错误!");
              }else if(message.equals("pass_fail")){
              out.write("用户名或密码错误!");
              }
              }
              %>
              - -

              以及success.jsp

              -

              image-20230302233110661

              -
              成功/两个失败

              仅以成功为例

              -
              @WebServlet(value = "/success")
              public class SuccessServlet extends HttpServlet {
              @Override
              protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
              doPost(req,resp);
              }

              @Override
              protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
              resp.setContentType("text/html;charset=utf-8");
              resp.getWriter().write("登录成功!"+req.getAttribute("uname")+",欢迎您");
              }
              }
              - -

              JSP

              -

              现在都用 Thymeleaf ,更符合 MVC 的执行过程,也没有 JSP 这种耦合杂乱的页面代码,但是模板引擎的思路大致相同,还是可以看一看的

              -
              -

              改动之后无需重启服务器,刷新界面即可。

              -
              -

              关于热更新的机制可以看看这篇文章,水平有限还看不懂就先放在这了:

              -

              JSP热部署的实现原理[通俗易懂]

              -
              -

              概念

              JSP(Java Server Pages) Java服务器端页面,用于简化书写

              -

              可以理解为:一个特殊的页面,其中既可以定义html标签,又可以定义java代码

              -

              比如说,上一个案例的Servlet代码就可以直接写入到JSP中,而且response和request这些对象可以直接用

              -
              <%@ page import="java.text.SimpleDateFormat" %>
              <%@ page import="java.util.Date" %>
              <html>
              <body>
              <h2>Hello World!</h2>
              <%
              //获取流对象之前,设置流的默认编码:ISO-8859-1 设置为:GBK
              response.setCharacterEncoding("utf-8");

              //告诉浏览器,服务器发送的消息体数据的编码。建议浏览器使用该编码解码
              response.setHeader("content-type","text/html;charset=utf-8");
              if(request.getCookies() != null)
              for(Cookie c : request.getCookies()){
              if(c.getName().equals("isfirst")){
              //response.getWriter().write("<h1>欢迎回来,您上次访问的时间为<h1>"+c.getValue());
              response.getWriter().write("<h1>Welcome!The last time you visit is <h1>"+c.getValue());
              //System.out.println("欢迎回来,您上次访问的时间为"+c.getValue());
              break;
              }
              }
              else
              response.getWriter().write("<h1>Hello!Welcome!<h1>");

              SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd-HH:mm:ss");
              Date date1 = new Date();
              String currentTime = dateFormat.format(date1);

              //response.getWriter().write("<h1>你好!欢迎你!<h1>");
              //System.out.println("你好!欢迎你!");
              response.addCookie(new Cookie("isfirst",currentTime));
              %>
              </body>
              </html>
              - -

              最终效果:

              -image-20230222230929893 - -

              原理

              JSP本质上是Servlet

              -

              image-20230222233533332

              -

              JSP的脚本

              JSP定义Java代码的方式

              -
                -
              1. <% 代码 %>

                -

                定义的java代码,在service方法中。service方法中可以定义什么,该脚本中就可以定义什么。

                -

                也即最后会构成Servlet体

                -
              2. -
              3. <%! 代码 %>

                -

                定义的java代码,在jsp转换后的java类的成员位置。可以是成员变量,或者是成员方法。

                -

                注:最好不要在Servlet中定义成员变量,否则容易引发线程安全问题。

                -
              4. -
              5. <%= 代码 %>

                -

                定义的java代码,会输出到页面上。输出语句中可以定义什么,该脚本中就可以定义什么。

                -

                image-20230223000057595

                -

                比如说可以用来输出某个变量的值。注意这东西由于本质上是写在Servlet的service方法中的,因而当成员变量和service方法的局部变量重名,会依据就近原则优先使用局部变量的值。

                -
              6. -
              -

              指令

              也就是jsp开头那些东西,比如说这个:

              -
              <%@ page contentType="text/html;charset=UTF-8" language="java" %>
              - -

              用来配置jsp的资源页面信息

              -
                -
              • 分类:
                  -
                1. page : 配置JSP页面的
                    -
                  • contentType:等同于response.setContentType()
                    contentType="text/html;charset=UTF-8"
                    -
                      -
                    1. 设置响应体的mime类型以及字符集
                    2. -
                    3. 设置当前jsp页面的编码(只能是高级的IDE才能生效,如果使用低级工具,则需要设置pageEncoding属性设置当前页面的字符集)
                    4. -
                    -
                  • -
                  • import:导包
                  • -
                  • errorPage:当前页面发生异常后,会自动跳转到指定的错误页面
                  • -
                  • isErrorPage:标识当前页面是否是错误页面。
                      -
                    • true:是,可以使用内置对象exception【用来获取异常名称/信息等】
                    • -
                    • false:否。默认值。不可以使用内置对象exception
                    • -
                    -
                  • -
                  -
                2. -
                3. include : 页面包含的。导入页面的资源文件,可以引入其它的jsp或者html,引入之后就会展示同样的内容。
                    -
                  • <%@include file=”top.jsp”%>
                  • -
                  -
                4. -
                5. taglib导入资源。用来导入标签库
                      * <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
                  -        * prefix:前缀,自定义的。之后就可以用`<c:XXX>`了。相当于什么std::。
                  -
                  -
                6. -
                -
              • -
              -

              中文乱码

              但是注意一点

              -
              //获取流对象之前,设置流的默认编码:ISO-8859-1 设置为:GBK
              response.setCharacterEncoding("utf-8");

              //告诉浏览器,服务器发送的消息体数据的编码。建议浏览器使用该编码解码
              response.setHeader("content-type","text/html;charset=utf-8");
              - -

              这样做在Servlet不会导致中文乱码,但JSP不行,这个大概是因为两者原理不一样。

              -

              Servlet的中文乱码:

              -image-20230222231756277 - -

              JSP的:

              -

              image-20230222231820358

              -

              Servlet乱码是因为客户端和response请求体编码不一致,JSP乱码与JSP的原理有关,是只跟服务器端有关

              -
              -

              编译jsp有以下几个步骤:
              (1)把jsp转化为java源码。pageEncoding=xxx指定以xxx编码格式读取jsp文件,因此,jsp文件的编码格式应与pageEncoding值一致。
              (2)把java源码编译为字节码,即.class文件。转化后的java源码为utf-8编码格式,字节码也为utf-8编码,我们无需干预。
              (3)执行.class文件。在此过程,需向浏览器发送中文字符,contentType=xxx指定了jsp以xxx编码显示字符。也就是在浏览器中查看页面编码,其值为contentType指定的编码。

              -

              因此,在1、3环节,**只要指定一致的编码格式(jsp文件编码格式=pageEncoding=contentType)**,即可保证jsp页面不出现乱码。
              举例:jsp文件以utf-8格式编写,那么pageEncoding=utf-8, contentType=utf-8,就保证了jsp页面不出现乱码。
              ————————————————
              版权声明:本文为CSDN博主「liuhaidl」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
              原文链接:https://blog.csdn.net/liuhaidl/article/details/84012372

              -
              -

              指定方法是在JSP开头添加:

              -
              <%@ page pageEncoding="UTF-8"%>
              - -

              内置对象

              在jsp页面中不需要获取和创建,可以直接使用的对象。

              -

              jsp一共有9个内置对象。

              -
                -
              1. request HttpServletRequest 一次请求访问的多个资源(转发)

                -
              2. -
              3. response HttpServletResponse 响应对象

                -
              4. -
              5. out: JspWriter 字符输出流对象。可以将数据输出到页面上。和response.getWriter()类似

                -
                  -
                • response.getWriter()和out.write()的区别: 在tomcat服务器真正给客户端做出响应之前,会先找response缓冲区数据,再找out缓冲区数据。因而response.getWriter()数据输出永远在out.write()之前 所以说,用out更好,因为它跟随你布局变化,你out写在哪,这句话最终就会输出在哪
                • -
                -
              6. -
              7. pageContext PageContext 当前页面共享数据,还可以获取其他八个内置对象

                -
              8. -
              9. session HttpSession 一次会话的多个请求间

                -
              10. -
              11. application ServletContext 所有用户间共享数据

                -
              12. -
              13. page Object 当前页面(Servlet)的对象,相当于this

                -
              14. -
              15. config ServletConfig Servlet的配置对象

                -
              16. -
              17. exception Throwable 异常对象。只在page指令的isErrorPage为true的情况下才能使用此对象。

                -
              18. -
              -

              其中,

              -

              image-20230307144539506

              -

              这四个为用来共享数据的域对象

              -

              演变:MVC开发模式

              jsp的演变

              image-20230307145442383

              -

              MVC模式

              将程序分成三个部分,分别是M-V-C。

              -
                -
              1. M:Model,模型。JavaBean
                  -
                • 完成具体的业务操作,如:查询数据库,封装对象
                • -
                -
              2. -
              3. V:View,视图。JSP
                  -
                • 展示数据
                • -
                -
              4. -
              5. C:Controller,控制器。Servlet
                  -
                • 获取用户的输入
                • -
                • 调用模型
                • -
                • 将数据交给视图进行展示【域对象共享数据】
                • -
                -
              6. -
              -

              image-20230307150845273

              -

              服务器将接收的请求给控制器处理,控制器控制model完成必要的运算,model把算出的东西返回给控制器,控制器再把数据交给视图展示,数据最终就回到了浏览器客户端。

              -

              这就算是一个微型CPU了吧,控制器就是CU,模型就是ALU,也许客户端和视图什么的可以视为IO接口。

              -
                -
              • 优缺点:

                -
                  -
                1. 优点:

                  -
                    -
                  1. 耦合性低,方便维护,可以利于分工协作
                  2. -
                  3. 重用性高
                  4. -
                  -
                2. -
                3. 缺点:

                  -
                    -
                  1. 使得项目架构变得复杂,对开发人员要求高
                  2. -
                  -
                4. -
                -
              • -
              -

              那么,我们可以知道,jsp就只需负责数据的展示了。那怎么展示数据呢?这就需要用到jsp的几个技术了:

              -

              EL表达式

              -

              注意,servlet3.0以来默认关闭el表达式解析,需要手动在page上加属性打开,详见 jsp文件中的el表达式失效问题解决

              -
              -

              Expression language,替换和简化jsp上java代码的书写

              -

              语法:${表达式}

              -

              jsp会执行里面的表达式,然后把结果输出。

              -

              image-20230307151706211

              -

              加反斜杠可忽略。

              -

              使用场景:

              -
                -
              1. 运算

                -
                      1. 算数运算符: + - *  / %
                -      2. 比较运算符: > < >= <= == !=
                -      3. 逻辑运算符: && || !
                -      4. 空运算符: empty
                -   * 功能:用于判断字符串、集合、数组对象是否为null**或者**长度是否为0
                -   * `${empty 变量名}`: 判断字符串、集合、数组对象是否为null或者长度为0
                -   * `${not empty 变量名}`: 表示判断字符串、集合、数组对象是否不为null 并且 长度>0
                -
                -
              2. -
              3. 获取值

                -
                  -
                1. el表达式只能从域对象中获取值

                  -

                  image-20230307144539506

                  -
                2. -
                3. 语法:

                  -
                    -
                  1. ${域名称.键名}:从指定域中获取指定键的值
                  2. -
                  -
                    -
                  • 域名称:
                      -
                    1. pageScope –> pageContext
                    2. -
                    3. requestScope –> request
                    4. -
                    5. sessionScope –> session
                    6. -
                    7. applicationScope –> application(ServletContext)
                    8. -
                    -
                  • -
                  • 举例:在request域中存储了name=张三,获取:${requestScope.name}
                  • -
                  -
                    -
                  1. ${键名}:表示依次从最小的域中查找是否有该键对应的值,直到找到为止。
                  2. -
                  -
                4. -
                5. 案例

                  -

                  这样一来,访问/demo就能转发到index.jsp,显示出属性值

                  -
                    -
                  1. Servlet

                    -
                    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
                    request.setAttribute("name","xiunian");
                    request.getRequestDispatcher("/index.jsp").forward(request,response);
                    }
                  2. -
                  3. index.jsp

                    -
                    <%@ page pageEncoding="UTF-8" isELIgnored="false" %>
                    <html>
                    <body>
                    <h2>Hello World!</h2>
                    ${requestScope.name}
                    </body>
                    </html>
                  4. -
                  -
                6. -
                7. 获取非字符串类型的值

                  -
                    -
                  1. 对象

                    -
                  2. -
                  3. 集合(List、Map等)

                    -
                  4. -
                  -
                8. -
                9. -
                -
              4. -
              -]]>
              - - Java - -
              - - 密码学基础 - /2023/11/26/cryptography/ - -

              学习目的:顺利过考试,以及获取基本的密码学知识,数学原理不重要

              -
              -

              第一章 概述

              image

              -

              image

              -

              image

              -

              image

              -

              image

              -

              image

              -

              image

              -

              第二章 传统密码技术

              概念

              image

              -

              image

              -

              分类

              置换密码

              image

              -

              列置换密码

              加密

              image

              -
              解密

              image

              -
              例子

              image

              -

              image

              -

              周期置换密码

              image

              -

              image

              -

              代换密码

              image

              -

              单表代换

              image

              -

              image

              -

              多表代换

              image

              -

              image

              -

              image

              -

              image

              -

              传统密码体制分析

              频率(单表代换)

              image

              -

              重合指数(多表代换)

              image

              -

              明文-密文对(hill密码)

              image

              -

              第三章 分组密码-DES

              概述

              image

              -
                -
              1. 分组密码一般指对称分组密码
              2. -
              -

              image

              -
                -
              1. 明文经编码表示后变成二进制序列
              2. -
              3. 二进制序列固定长度分组
              4. -
              5. 每组在密钥控制下转为密文分组
              6. -
              7. 本质上是明文到密文的一一映射
              8. -
              9. 一般明文长度=密文长度,密钥长度不一定
              10. -
              -

              image

              -

              image

              -

              设计思想

              image

              -

              image

              -

              image

              -

              image

              -

              image

              -

              基本特点

              image

              -

              子密钥生成算法

              image

              -

              轮函数

              image

              -

              迭代轮数

              image

              -

              DES算法

              概述

              image

              -

              image

              -

              加密流程

              总体流程

              image

              -

              image

              -

              image

              -

              选择扩展置换E

              image

              -

              子密钥生成

              image

              -

              压缩替代S-盒

              image

              -

              image

              -

              image

              -

              置换p-盒

              image

              -

              解密流程

              image

              -

              image

              -

              安全性分析

              image

              -

              互补性

              image

              -

              image

              -

              弱密钥

              image

              -

              image

              -

              差分分析

              只有理论上意义

              -

              image

              -

              线性分析

              实际上不可行

              -

              image

              -

              密钥搜索

              image

              -

              image

              -

              多重DES

              image

              -

              image

              -

              二重

              image

              -

              3DES

              你也是过渡阶段?

              -

              image

              -

              第四章 有限域

              数学基础

              image

              -

              逆元:

              -

              image-20231119235319197

              -

              比如说在G(7)中,2的逆元为4。

              -

              也即,任意整数a,则存在x,a / 2 == a * 4 (mod 7),4为2模7的乘法逆元,记为 2(-1)(mod 7) = 4。

              -

              image

              -

              求逆元的方法是求b^(m-2) mod m。如2^(5) mod 7 = 4。

              -

              群环域

              image

              -

              image

              -

              image

              -

              确实封闭且结合且单位元且逆元

              -

              循环群

              image

              -

              image

              -

              image

              -

              确实是环

              -

              image

              -

              image

              -

              有限域GF(p)

              有限域就是阶为素数幂的域?

              -

              image

              -

              image

              -

              image

              -

              image-20231119233220659

              -

              多项式运算

              image

              -

              普通多项式运算

              image

              -

              image

              -

              image

              -

              image

              -

              系数模p运算的多项式运算

              image

              -

              确实,毕竟系数本身就是域了,除了没定义逆元外别的都满足。

              -

              image

              -

              image

              -

              有限域GF(2^n)

              image

              -

              image

              -

              image

              -

              第五章 高级加密标准-AES

              概述

              简介

              image

              -

              image

              -

              Nr=Nk的幂数x2

              -

              简化版AES

              image

              -

              image

              -

              具体算法详见PPT。

              -

              基本结构

              image

              -

              总体流程

              image

              -

              加密流程

              整体流程

              image

              -

              image

              -

              image

              -

              状态矩阵

              image

              -

              字节代替

              image

              -

              行移位

              image

              -

              列混淆

              image

              -

              image

              -

              可以关注下是怎么通过C矩阵求出这个固定多项式的:

              -

              image

              -

              轮密钥加

              image

              -

              密钥扩展

              image

              -

              image

              -

              感觉也是类似对明文做的操作

              -

              安全评估

              image

              -

              image

              -

              image

              -

              image

              -

              SM4

              image

              -

              image

              -

              第六章 分组密码的工作模式

              image

              -

              image

              -

              电码本ECB

              image

              -

              image

              -

              image

              -

              密码分组链接CBC

              image

              -

              image

              -

              密码反馈CFB

              image

              -

              image

              -

              输出反馈OFB

              image

              -

              image

              -

              计数器Counter

              image

              -

              image

              -

              image

              -

              总结

              image

              -

              第七章 序列密码

              概述

              序列密码的密钥序列是随机的。

              -

              image

              -

              image

              -

              image

              -

              image

              -

              image

              -

              总体流程

              image

              -

              image

              -

              image

              -

              密钥产生器KG

              总体构成

              image

              -

              image

              -

              线性反馈移位寄存器理论

              image

              -

              反馈移位寄存器

              image

              -

              image

              -

              线性反馈移位寄存器

              image

              -

              image

              -

              确实,感觉相比上面的这笔就是换了个反馈函数,就达到了2^n-1的周期

              -

              m序列

              特性

              image

              -

              image

              -
              生成

              image

              -

              image

              -
              分析

              image

              -
              破译

              image

              -

              image

              -

              image

              -

              image

              -

              常见序列生成算法

              Geffe序列生成器

              image

              -

              Pless生成器

              image

              -

              image

              -

              A5算法

              image

              -

              image

              -

              ZUC算法

              image

              -

              image

              -

              image

              -

              image

              -

              image

              -

              image

              -

              image

              -

              RC4

              简介

              image

              -

              image

              -

              image

              -

              image

              -

              流程

              数据表S的初始状态

              image

              -

              数据表S的初始置换

              image

              -

              密钥流的生成

              image

              -

              第八章 数论基础

              整除性和带余除法,最大公因子

              image

              -

              image

              -

              素数和模运算

              image

              -

              image

              -

              也就是说求最大公因子实际上可以只求共有素数因子

              -

              image

              -

              image

              -

              image

              -

              image

              -

              欧几里得算法和扩展欧几里得算法

              欧几里得算法

              image

              -

              image

              -

              image

              -

              扩展欧几里得

              image

              -

              image

              -

              image

              -

              image

              -

              image

              -

              费马小定理和欧拉定理

              费马小定理

              image

              -

              image

              -

              欧拉定理

              image

              -

              image

              -

              素性检测

              miller-rabin

              image

              -

              image

              -

              image

              -

              中国剩余定理

              image

              -

              image

              -

              image

              -

              image

              -

              image

              -

              离散对数

              image

              -

              image

              -

              image

              -

              image

              -

              image

              -

              第九章 公钥加密体系-RSA

              image

              -

              image

              -

              概述

              image

              -

              image

              -

              image

              -

              RSA

              组成

              image

              -

              image

              -

              image

              -

              安全性

              image

              -

              image

              -

              image

              -

              image

              -

              应用

              image

              -

              Rabin加密

              image

              -

              image

              -

              MH背包密码

              image

              -

              简介

              image

              -

              流程

              image

              -

              image

              -

              例子

              image

              -

              image

              -

              安全性分析

              image

              -

              image

              -

              EIGamal加密

              image

              -

              image

              -

              image

              -

              image

              -

              椭圆曲线密码体制

              image

              -

              数学理论

              image

              -

              image

              -

              image

              -

              image

              -

              image

              -

              image

              -

              image

              -

              image

              -

              密码体制

              image

              -

              image

              -

              image

              -

              image

              -

              IBE算法

              image

              -

              image

              -

              image

              -

              image

              -

              image

              -

              看起来意思就是公钥完全明文,用的是用户的身份ID;私钥用户自己存着。

              -

              image

              -

              image

              -

              image

              -

              后量子密码

              概述

              image

              -

              image

              -

              image

              -

              image

              -

              NTRU

              流程

              image

              -

              image

              -

              image

              -

              image

              -

              举例

              image

              -

              image

              -

              安全性

              image

              -

              第十一章 哈希函数

              概述

              image

              -

              image

              -

              image

              -

              image

              -

              这个角度很有意思,确实是名字一样原理相近,但是目的完全不一样:

              -

              image

              -

              image

              -

              常见哈希函数

              SHA

              image

              -

              image

              -

              SM3

              image

              -

              image

              -

              安全性

              image

              -

              image

              -

              暴力攻击

              image

              -

              生日攻击

              image

              -

              image

              -

              攻击过程

              image

              -

              image

              -

              应用

              image

              -

              身份认证

              image

              -

              image

              -

              image

              -

              数字签名

              image

              -

              也就是中途会哈希两次吼。

              -

              第十二章 消息认证码 (MAC)

              概述

              基本思想

              image

              -

              image

              -

              一样的话就是说明消息没被篡改

              -

              image

              -

              要求

              image

              -

              基于哈希函数的MAC

              image

              -

              直观构造

              image

              -

              image

              -

              image

              -

              image

              -

              HMAC

              image

              -

              image

              -

              image

              -

              基于分组密码的MAC

              image

              -

              数据认证算法DAA

              image

              -

              image

              -

              CMAC

              image

              -

              认证加密

              概述

              image

              -

              image

              -

              CCM

              image

              -

              局限性

              image

              -

              第十三章 数字签名PKI

              概述

              简介

              image

              -

              image

              -

              对比

              image

              -

              image

              -

              安全性

              image

              -

              实现

              image

              -

              image

              -

              image

              -

              image

              -

              常见实现

              都包含签名算法、验证算法、正确性证明、举例,详细看PPT吧。

              -

              基于RSA

              image

              -

              基于离散对数

              image

              -

              ELGamal

              Schnorr

              DSA

              盲签名

              image

              -

              image

              -

              image

              -

              群(组)签名

              image

              -

              第十四章 密码协议

              概述

              image

              -

              image

              -

              分割和选择协议

              image

              -

              掷硬币协议

              image

              -

              单向函数

              image

              -

              模p指数运算

              image

              -

              零知识证明

              image

              -

              image

              -

              image

              -

              比特承诺

              image

              -

              image

              -

              image

              -

              安全多方计算

              这个有点复杂,可以看看PPT。

              -

              第十五章 密钥管理

              概述

              image

              -

              image

              -

              密钥分配

              image

              -

              image

              -

              无中心

              image

              -

              中心模式

              image

              -

              基于公钥密钥

              image

              -

              密钥协商

              image

              -

              Diffie-Hellman密钥交换方案

              image

              -

              image

              -

              PKI

              image

              -

              image

              -

              image

              -

              image

              -

              image

              -

              image

              -

              image

              -

              image

              -

              image

              -

              SSL

              概述

              image

              -

              image

              -

              底层协议

              image

              -

              image

              -

              image

              -

              上层协议

              警告协议

              image

              -

              握手协议/密码变化协议

              image

              -

              密钥交换(四握手)

              image

              -

              image

              -

              image

              -

              image

              -

              image

              +

              Expression language,替换和简化jsp上java代码的书写

              +

              语法:${表达式}

              +

              jsp会执行里面的表达式,然后把结果输出。

              +

              image-20230307151706211

              +

              加反斜杠可忽略。

              +

              使用场景:

              +
                +
              1. 运算

                +
                      1. 算数运算符: + - *  / %
                +      2. 比较运算符: > < >= <= == !=
                +      3. 逻辑运算符: && || !
                +      4. 空运算符: empty
                +   * 功能:用于判断字符串、集合、数组对象是否为null**或者**长度是否为0
                +   * `${empty 变量名}`: 判断字符串、集合、数组对象是否为null或者长度为0
                +   * `${not empty 变量名}`: 表示判断字符串、集合、数组对象是否不为null 并且 长度>0
                +
                +
              2. +
              3. 获取值

                +
                  +
                1. el表达式只能从域对象中获取值

                  +

                  image-20230307144539506

                  +
                2. +
                3. 语法:

                  +
                    +
                  1. ${域名称.键名}:从指定域中获取指定键的值
                  2. +
                  +
                    +
                  • 域名称:
                      +
                    1. pageScope –> pageContext
                    2. +
                    3. requestScope –> request
                    4. +
                    5. sessionScope –> session
                    6. +
                    7. applicationScope –> application(ServletContext)
                    8. +
                    +
                  • +
                  • 举例:在request域中存储了name=张三,获取:${requestScope.name}
                  • +
                  +
                    +
                  1. ${键名}:表示依次从最小的域中查找是否有该键对应的值,直到找到为止。
                  2. +
                  +
                4. +
                5. 案例

                  +

                  这样一来,访问/demo就能转发到index.jsp,显示出属性值

                  +
                    +
                  1. Servlet

                    +
                    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
                    request.setAttribute("name","xiunian");
                    request.getRequestDispatcher("/index.jsp").forward(request,response);
                    }
                  2. +
                  3. index.jsp

                    +
                    <%@ page pageEncoding="UTF-8" isELIgnored="false" %>
                    <html>
                    <body>
                    <h2>Hello World!</h2>
                    ${requestScope.name}
                    </body>
                    </html>
                  4. +
                  +
                6. +
                7. 获取非字符串类型的值

                  +
                    +
                  1. 对象

                    +
                  2. +
                  3. 集合(List、Map等)

                    +
                  4. +
                  +
                8. +
                9. +
                +
              4. +
              +]]> + + Java + + + + 计算机体系结构 + /2024/01/04/arch/ + 01 基本知识
                +
              1. SISD、SIMD、MIMD、向量处理器的基本概念

                +

                向量处理器意思是一条指令可以同时处理多个数据元素(SIMD)(就类似于这几个数据元素组成了一个向量);多发射处理器可以同一时间并行多条指令。

                +
              2. +
              3. 发射与流出

                +

                在计算机体系结构中,”发射”和”流出”是与指令执行有关的两个重要概念,它们描述了处理器在执行指令时的不同阶段和行为。

                +
                  +
                1. 发射(Issue):
                    +
                  • “发射”指的是将指令从指令流中发送到处理器的执行部件或执行单元,以进行实际的执行。
                  • +
                  • 发射阶段通常是在取指令和解码指令之后,将指令发送到执行单元的过程。
                  • +
                  • 多发射处理器意味着多条指令可以同时进入执行阶段,通过并行执行提高处理器的性能
                  • +
                  +
                2. +
                3. 流出(Out-of-Order Execution):
                    +
                  • “流出”是指处理器在执行过程中允许指令乱序执行,即不按照它们在程序中的原始顺序执行。在乱序执行的情况下,处理器会通过重新排序指令来填充执行单元的空闲周期,以提高整体性能。
                  • +
                  • 多流出处理器采用乱序执行的方式,允许在执行单元空闲时执行无关的指令,以最大程度地利用执行单元的并行性。
                  • +
                  +
                4. +
                +

                这两个概念都涉及到提高指令级并行性,但它们描述了处理器在执行阶段的不同方面。发射强调在同一时钟周期内同时发送多条指令,而流出强调在执行过程中的乱序执行策略。

                +
              4. +
              5. tensor 张量

                +

                sparse tensor 稀疏张量

                +
              6. +
              7. 异构计算

                +

                指的是在同一系统中集成多种不同体系结构或架构的处理器和计算设备,以便更有效地处理各种类型的任务。这包括集成不同类型的中央处理单元(CPU)、图形处理单元(GPU)、加速器、协处理器等。异构计算的目标是充分发挥各种处理器的优势,以提高整体系统性能和能效。

                +

                其关键概念有协处理器等等等。

                +
              8. +
              +

              02 现代处理器体系结构

              img

              +

              img

              +

              例题

              题型1 生成指令序列,分析时间

              1

              img

              +

              注意几点:

              +
                +
              1. 变量需要通过LD指令载入到寄存器
              2. +
              +

              2

              img

              +

              img

              +

              注意,它的意思是LD、SD、DADDIU都只占1个时钟周期,ADD占2个

              +

              img

              +

              感觉这么个例题下来,我就懂了循环展开的作用了

              +

              题型2 换名/消除WAR WAW

              1

              img

              +

              2

              img

              +

              题型3 记分牌

              img

              +

              这里的结构相关值得注意

              +

              做这种题的套路是,需要明确它要求的时刻时的情况,并且依照以下规则判断即可:

              +
                +
              1. 指令状态表

                +
                  +
                1. 流出

                  +

                  无结构冲突、无WAW冲突

                  +

                  如① 当MULT准备写回时,此时前两条L必定流出,然后后面的SUB、DIV、ADD都没有结构冲突和WAW冲突,所以全部流出。只不过ADD和DIV会卡在读操作数阶段

                  +

                  ② 由①可知全部流出

                  +
                2. +
                3. 读操作数

                  +

                  操作数可用时完成该阶段

                  +

                  如① 此时前三条必定完成。并且SUB也完成了,所以ADD也完成了读数阶段。只有DIV还在等待mul的结果

                  +

                  ② 此时大伙差不多都结了,没什么好说的

                  +
                4. +
                5. 执行

                  +

                  纯纯的算术

                  +

                  如① 除了除法别的都完了,没什么好说的

                  +

                  ② 全部都结了

                  +
                6. +
                7. 写结果

                  +

                  不存在WAR则写入

                  +

                  如① 前两个肯定完成了,然后SUB也结了,ADD存在WAR,所以最后是ADD和MUL没完成。

                  +

                  ② 除了DIV全部结了

                  +
                8. +
                +
              2. +
              3. 功能部件状态表

                +

                记住这些字母的含义即可:

                +
                  +
                • Busy:yes/no
                • +
                • Op:操作编码
                • +
                • Fi:目的寄存器编号
                • +
                • Fj,Fk:源寄存器编号
                • +
                • Qj,Qk:正在计算Fj和Fk的功能部件
                • +
                • Rj,Rk:Fj和Fk是否就绪且还没被取走
                • +
                +
              4. +
              5. 寄存器状态表

                +

                每个寄存器有一项,用于指出哪个功能部件将把结果写入

                +
              6. +
              +

              img

              +

              img

              +

              题型4 Tomasulo算法

              3段流水

              +
                +
              1. 流出

                +
                  +
                1. 没有结构冲突就流出,填进保留站

                  +

                  一般有ADD1,ADD2,ADD3(加减),MUL1,MUL2(乘除),LD1,LD2(SL)

                  +
                2. +
                3. 具体填什么看操作数有没有就绪

                  +
                4. +
                +

                保留站有以下字段:

                +
                  +
                • Op:操作

                  +
                • +
                • Qj,Qk:操作数保留站号

                  +
                • +
                • Vj,Vk:源操作数值

                  +

                  load的Vk保存偏移量

                  +
                • +
                • Busy

                  +
                • +
                • A:存放立即数字段 or 有效地址,仅用于load和store缓冲器

                  +
                • +
                • Qi:寄存器状态表

                  +

                  存放要写入它的保留站ID

                  +
                • +
                +
              2. +
              3. 执行

                +

                两个操作数就绪后就执行

                +
              4. +
              5. 写结果

                +

                计算完毕后由CDB传送

                +
              6. +
              +

              例题

              img

              +

              img

              +

              这里不知道为什么LD2没有跟LD1同时完成?限制了一个时钟周期只流出一条指令吗

              +

              img

              +

              这里可以注意其特点是结果一经算出全部写回

              +

              img

              +

              img

              +

              img

              +

              img

              +

              通过换名避免了WAR,而不是像记分牌那样通过等待

              +

              img

              +

              题型5 Tomasulo+前瞻执行

              4段流水

              +
                +
              1. 流出

                +
                  +
                1. 保留站&ROB都有空闲才流出

                  +

                  一般有ADD1,ADD2,ADD3(加减),MUL1,MUL2(乘除),LD1,LD2(SL)

                  +
                2. +
                3. 具体填什么看操作数有没有就绪

                  +
                4. +
                +

                保留站有以下字段:

                +
                  +
                • Op:操作

                  +
                • +
                • Qj,Qk:操作数保留站号

                  +
                • +
                • Vj,Vk:源操作数值

                  +

                  load的Vk保存偏移量

                  +
                • +
                • Busy

                  +
                • +
                • A:存放立即数字段 or 有效地址,仅用于load和store缓冲器

                  +
                • +
                • Qi:寄存器状态表

                  +

                  存放要写入它的保留站ID

                  +
                • +
                +
              2. +
              3. 执行

                +

                两个操作数就绪后就执行

                +
              4. +
              5. 写结果

                +
                  +
                1. 写入ROB,CDB传送ROB编号到保留站
                2. +
                3. 释放产生该结果的保留站
                4. +
                +

                ROB字段:

                +
                  +
                • 指令类型

                  +
                • +
                • 目标地址

                  +

                  目标寄存器/存储器单元地址

                  +
                • +
                • 数据值字段

                  +

                  前瞻结果

                  +
                • +
                • 就绪字段

                  +

                  结果是否就绪

                  +
                • +
                +
              6. +
              7. 指令确认

                +

                分支结果出来后确认

                +
                  +
                1. 猜测对 写入寄存器/存储器,释放ROB
                2. +
                3. 猜测错 从另一条路径开始重新执行,清空ROB
                4. +
                +
              8. +
              +

              例题

              img

              +

              img

              +

              img

              +

              img

              +

              img

              +

              img

              +

              题型6 超标量实现

              例题

              img

              +

              img

              +

              img

              +

              img

              +

              注意,SD指令的0和R1有了就开始执行,不必等到F4有了再执行。。。

              +

              img

              +

              img

              +

              img

              +

              img

              +

              img

              +

              题型7 循环展开

              具体步骤:

              +
                +
              1. 依题意展开
              2. +
              3. 去除多余的BNE、合并所有DADDUI
              4. +
              5. 寄存器换名消除名相关
              6. +
              7. 重排序消除数据相关
              8. +
              +

              1

              img

              +

              img

              +

              img

              +

              img

              +

              img

              +

              img

              +

              img

              +

              img

              +

              img

              +

              2

              img

              +

              img

              +

              img

              +

              题型8 VIEW技术

              例题

              img

              +

              img

              +

              img

              +

              看起来大概可能就有点类似树的概念,什么都不依赖的就放前面,然后依赖1层的依赖2层的之类的

              +

              img

              +

              题型9 软流水

              例题

              img

              +

              img

              +

              img

              +

              img

              +

              指令级并行

              概念

                +
              1. 开发指令级并行ILP的方法

                +
                  +
                1. 基于硬件的动态开发
                2. +
                3. 基于软件的静态开发
                4. +
                +
              2. +
              3. 流水线CPI

                +

                实际CPI = 理想CPI + 停顿(结构/数据/控制冲突引起)

                +
              4. +
              5. 理想CPI是衡量流水线最高性能

                +
              6. +
              7. IPC:每个时钟周期完成的指令数

                +

                CPI:每个指令所需时钟周期数

                +
              8. +
              9. 基本程序块:一串没有分支和跳转转入点的指令块

                +
              10. +
              +

              解决冲突的方法之一是序列调度,不过对于跨块的调度(也即jump指令)会有影响

              +

              相关与并行

              相关:两条指令之间存在某种依赖关系

              +

              只能部分(完全不难)在流水线中重叠执行

              +

              类型:数据相关(真数据相关)、名相关、控制相关

              +

              约定先执行i再执行j

              +
                +
              1. 数据相关

                +
                  +
                1. 定义:j使用i的结果,也即先写后读

                  +
                2. +
                3. 具有传递性

                  +
                4. +
                5. 反映数据流动关系,即如何从生产者流动到消费者

                  +
                6. +
                7. 数据相关不能并行,需要插入暂停解决冲突

                  +
                8. +
                9. 解决方法

                  +
                    +
                  1. 保持相关但避免冲突

                    +

                    调度

                    +
                  2. +
                  3. 变换代码消除相关关系

                    +
                  4. +
                  +
                10. +
                11. 检测方法

                  +

                  流经寄存器时直观;流经存储器复杂

                  +
                12. +
                +
              2. +
              3. 名相关

                +

                img

                +
                  +
                1. 分类

                  +
                    +
                  1. 反相关:先读后写,也即i读的名与j写的名相同
                  2. +
                  3. 输出相关:i和j写的名相同
                  4. +
                  +
                2. +
                3. 解决方法:换名技术,可以编译器静态实现 or 硬件动态实现

                  +

                  img

                  +
                4. +
                5. 相关问题

                  +

                  寄存器换名可以消除WAR和WAW冲突

                  +
                    +
                  1. WAR(反相关)
                  2. +
                  3. WAW(输出相关)
                  4. +
                  +
                6. +
                +
              4. +
              5. 数据冲突

                +

                注意这里的命名,是按照正确顺序命名的。比如说RAW(read after write),写后读,正确次序就是i写入然后j再读,所以叫写后读。

                +
                  +
                1. RAW(数据相关)

                  +

                  也即i写j读

                  +
                2. +
                3. WAW(输出相关)

                  +

                  也即i写j写

                  +

                  流水线发生条件:流水线不止一个段可以写操作、指令被重新排序

                  +

                  5段流水线不会发生,因为只会在WB阶段写寄存器

                  +
                4. +
                5. WAR(反相关)

                  +

                  也即i读j写

                  +

                  流水线发生条件:有些指令写操作提前有些读操作滞后、指令被重新排序

                  +
                6. +
                +
              6. +
              7. 控制相关

                +

                由分支指令引起

                +
              8. +
              +

              调度

              img

              +

              img

              +

              动态调度

              基本思想

              img

              +

              img

              +

              img

              +

              img

              +

              这里可能意思是引出了多流出,所以会导致DIV和ADD同时流出,从而发生WAW。同理,可能的阻塞也会导致WAR。

              +

              img

              +

              记分牌动态调度算法

                +
              1. 基本思想

                +

                在没有结构冲突时,尽可能早地执行没有数据冲突的指令,实现每个时钟周期执行一条指令

                +
              2. +
              3. 基本结构

                +

                三张表:指令执行状态、功能部件状态、寄存器状态及数据相关关系

                +
                  +
                1. 指令状态表

                  +

                  记录正在执行的各条指令的状态

                  +
                2. +
                3. 功能部件状态表

                  +

                  记录各个功能部件状态,每项有以下字段:

                  +
                    +
                  • Busy:yes/no
                  • +
                  • Op:操作编码
                  • +
                  • Fi:目的寄存器编号
                  • +
                  • Fj,Fk:源寄存器编号
                  • +
                  • Qj,Qk:Fj和Fk的功能部件
                  • +
                  • Rj,Rk:Fj和Fk是否就绪且还没被取走
                  • +
                  +
                4. +
                5. 结果寄存器状态表

                  +

                  每个寄存器有一项,用于指出哪个功能部件将把结果写入

                  +

                  大概是这样的结果:n(寄存器数量) X m(功能部件数量) 的值为0 or 1的矩阵

                  +
                6. +
                +
              4. +
              5. 执行流程

                +

                每条指令的执行过程分为4段(只考虑浮点计算)

                +
                  +
                1. 流出

                  +

                  如果①所需功能部件空闲(结构冲突) ②其他正在执行指令目的寄存器与当前不同(WAW冲突),则流出

                  +
                2. +
                3. 读操作数

                  +

                  记分牌监测操作数可用性,可用时通知功能部件从寄存器中读出源操作数开始执行(RAW冲突)

                  +
                4. +
                5. 写结果

                  +

                  记分牌监测是否完成执行,若不存在or已消失WAR,则写入;存在,等待

                  +
                6. +
                +
              6. +
              7. 性能分析

                +

                img

                +
              8. +
              +

              Tomasulo算法

                +
              1. 核心思想

                +

                记录和检测指令相关,操作数一旦就绪立刻执行,把发生RAW的可能减到最小;

                +

                通过寄存器换名消除WAR和WAW(上面的记分牌是通过等待)

                +

                img

                +
              2. +
              3. 基本结构

                +
                  +
                1. 保留站

                  +

                  每个保留一条已经流出并且等待到本功能部件执行的指令的相关信息。包括操作数、操作码以及各种元数据。

                  +

                  img

                  +

                  img

                  +

                  故而,需要有以下字段:

                  +
                    +
                  • Op:操作

                    +
                  • +
                  • Qj,Qk:操作数保留站号

                    +
                  • +
                  • Vj,Vk:源操作数值

                    +

                    load的Vk保存偏移量

                    +
                  • +
                  • Busy

                    +
                  • +
                  • A:存放立即数字段 or 有效地址,仅用于load和store缓冲器

                    +
                  • +
                  • Qi:寄存器状态表

                    +

                    存放把结果写入该寄存器的保留站ID

                    +
                  • +
                  +
                2. +
                3. 公共数据总线CDB

                  +

                  用于发送各个功能部件的计算结果。如果具有多个执行部件且采用多流出流水线,则需要采用多条CDB。

                  +
                4. +
                5. load缓冲器和store缓冲器

                  +
                    +
                  1. load缓冲器
                      +
                    1. 存放用于计算有效地址的分量
                    2. +
                    3. 记录正在进行的load访存
                    4. +
                    5. 保存buffer等待CDB传输
                    6. +
                    +
                  2. +
                  3. store缓冲器
                      +
                    1. 存放用于计算有效地址的分量
                    2. +
                    3. 记录正在进行的store访存,如目标地址以及是否已有数据
                    4. +
                    5. 保存buffer等待CDB传输
                    6. +
                    +
                  4. +
                  +
                6. +
                7. 浮点寄存器FP

                  +

                  img

                  +
                8. +
                9. 指令队列

                  +

                  FIFO

                  +
                10. +
                11. 运算部件

                  +

                  浮点加法器、浮点乘法器

                  +
                12. +
                +
              4. +
              5. 寄存器换名实现

                +

                当指令流出,如果操作数缺失,则将指令数据换名为保留站编号

                +
              6. +
              7. 特点

                +
                  +
                1. 冲突检测与指令执行是分布的

                  +

                  通过保留站和CDB实现

                  +

                  计算结果通过CDB直接从产生它的保留站传送到所有需要它的功能部件,无需经过寄存器

                  +
                2. +
                3. 消除了WAW和WAR

                  +
                4. +
                +
              8. +
              9. 执行步骤

                +

                3段流水

                +
                  +
                1. 流出

                  +

                  如果操作要求的保留站空闲(结构冲突),则送到保留站r。如果操作数已就绪,填入;否则,填入产生该操作数的保留站ID(寄存器换名,消除WAW、WAR)。

                  +
                2. +
                3. 执行

                  +

                  两个操作数就绪后,就可以用保留站对应功能部件执行

                  +

                  img

                  +
                4. +
                5. 写结果

                  +

                  计算完毕后由CDB传送

                  +
                6. +
                +
              10. +
              +

              基于硬件的前瞻执行

              img

              +

              img

              +

              img

              +

              img

              +

              img

              +

              img

              +

              多指令流出

              img

              +

              img

              +

              img

              +

              img

              +

              img

              +

              超标量实现

              img

              +
                +
              1. 假设每个时钟周期流出两条,1整数型指令+1浮点型指令。

                +

                整数型:load、store、分支

                +

                浮点型:可能各种运算吧

                +
              2. +
              3. 假设所有浮点指令都是加法,执行时间3个时钟周期,且图中整数总在浮点前

                +
              4. +
              +

              img

              +

              img

              +

              img

              +

              没懂,难道单发射流水线就不会吗。。。

              +

              img

              +

              基于静态调度

              img

              +

              基于动态调度

              img

              +

              img

              +

              img

              +

              VLIW技术

              img

              +

              img

              +

              img

              +

              img

              +

              基本指令调度和循环展开

              img

              +

              img

              +

              指令调度

              img

              +

              img

              +

              img

              +

              循环展开

              img

              +

              img

              +

              软流水

              img

              +

              img

              +

              03 AI处理器

              并行体系结构

              分类

              SISD

              img

              +

              SIMD

              img

              +

              img

              +

              MIMD

              img

              +

              向量体系结构

              img

              +

              img

              +

              img

              +

              img

              +

              img

              +

              img

              +

              img

              +

              后面不知道为什么写着写着开始英文了。。。算了,看起来都不重要。

              +

              GPU

              概念

              img

              +

              img

              +

              GPU体系结构

              img

              +

              img

              +

              GPU计算

              img

              +

              CUDA编程

              img

              +

              img

              +

              img

              +

              img

              +

              GPU中的线程是执行计算任务的最小单位,可以看作是一系列指令的执行者。每个线程都有自己的程序计数器(PC)、寄存器集和局部内存。这些线程以并行的方式执行相同的指令,但可以有不同的输入数据,从而在数据并行的模式下执行计算。

              +

              img

              +

              img

              +

              下面两个标题反了额

              +

              img

              +

              img

              +

              img

              +

              img

              +

              感觉能明白其划分一组组线程的意义了,就是方便管理,一个warp执行相同的指令代码,所以要求同时调度同时执行

              +

              img

              +

              img

              +

              img

              +

              img

              +

              img

              +

              img

              +

              img

              +

              例题

              img

              +

              img

              +

              img

              +

              img

              +

              img

              +

              img

              +

              访存优化

              img

              +

              img

              +

              img

              +

              img

              +

              img

              +

              img

              +

              img

              +

              img

              +

              真没懂。。。。

              +

              img

              +

              img

              +

              img

              +

              真没看懂

              +

              GPU控制流和指令优化

              img

              +

              AI开发

              DL框架

              img

              +

              img

              +

              PyTorch

              img

              +

              img

              +

              img

              +

              算子开发

              img

              +

              img

              +

              TODO接下来有兴趣看吧

              +

              模型开发

              img

              +

              04 自动驾驶体系结构

              img

              +

              img

              +

              img

              +

              img

              +]]>
              +
              + + Java并发编程实战 + /2022/11/06/Java%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B%E5%AE%9E%E6%88%98/ + +

              idea 替换注释正则表达式/\*{1,2}[\s\S]*?\*/

              + +

              第一章 简介

              线程的作用

              + +

              这一段写得很好,非常易懂地概括了什么是“多线程把异步转化为同步”:把异步中的不同操作分解为一个个独立的同类型操作,然后只需实现这些相较简单的同类型操作,再异步地把它们调度起来就行。线程正是把复杂的异步工作流分解成了一组简单的同步工作流

              +

              线程无处不在

              如果一个模块在代码中引入了并发性,那么它所有的代码路径【调用链】都得是并发的。

              + + +

              最后一句话很关键,“把线程安全性封装在共享对象内部”

              + + + + + + + + +

              这个不同于上面的方法:将共享对象包装为线程安全的。它是要求了这些共享对象仅能在事件线程中运行,这样来保证线程安全性。

              +

              第二章 线程安全性

              **线程安全的核心就是对状态的访问和操作进行管理**,特别是对那些共享(shared)的、可变(mutable)的状态。关于本句话,其中几点将在下面一一细说:

              +
                +
              1. 状态

                +

                状态是指存储在状态变量里的数据,如成员变量、静态域等等等。对象的状态还可能包括其他依赖对象的域,如HashMap的状态包括Map.Entry的状态。

                +
              2. +
              3. 共享和可变

                +

                共享意味着变量可以由多个线程同时访问,可变意味着变量的值在生命周期可发生变化

                +
              4. +
              5. 是否需要线程安全

                +

                取决于它是否被多个线程访问。比如说,如果一个局部变量仅在某个函数体中同时只被一个线程访问,那么它就不需要线程安全,不需要同步机制。

                +
              6. +
              + + + + +

              什么是线程安全

              概念

              + + + + + +

              注意,线程安全不会违背不变性和后验条件,这句话在后面会用到。

              +

              无状态对象一定是线程安全的

              在此举例一个无状态线程:

              + + +
              @ThreadSafe
              public class StatelessFactorize implements Servlet{
              public void service(ServletRequest req,ServletResponse resp){
              BigInteger i = extractFromRequest(req);
              BigInteger[] factors = factor(i);
              encodeIntoResponse(resp,factors);
              }
              }
              + + + + + +

              无状态对象一定是线程安全的

              +

              原子性

              引例

              我们可以在无状态对象的基础上为它增加一个域:

              + + +

              这是线程不安全的,因为++count包含了三个动作:读取—修改—写入

              +
              mov reg,count
              add reg,1
              mov count,reg
              + +

              它并不具有原子性。

              +

              在并发编程中,这种由于时序原因产生错误的情况叫做“竞态条件”。

              +

              竞态条件

              + +

              竞态条件有两种常见的类型。两种竞态条件的本质其实都是“基于对象之前的状态来定义对象状态的转换”。对于读取-修改-写入,是先copy原值,然后对原值+1,再写回,这是基于对象之前的状态来定义对象状态的转换;对于先检查后执行,很显然就是判断原值然后再转换到下一个状态,这就不必说了。

              +

              读取-修改-写入

              如上引例

              +

              先检查后执行

              实例:懒加载,延迟初始化中的竞态条件

              +
              public class LazyInitRace {
              private ExpensiveObject instance = null;

              public ExpensiveObject getInstance() {
              if (instance == null)
              instance = new ExpensiveObject();
              return instance;
              }
              }
              + + +

              竞态条件与数据竞争差别

              + + + +

              这书里讲得云里雾里的,百度了一下:

              + + +

              比如说书给例子,线程向共享对象读写数据,线程是操作对象A,共享对象是被操作对象B。则:

              +

              竞态条件:在乎的是被线程操控的共享对象的结果是否正确

              +

              数据竞争:在乎的是操作共享对象后,线程的结果是否正确。

              + + +

              确实,书里对数据竞争强调的是一个读一个写,对竞态条件更像是两个同时写

              +

              复合操作

              + + + + + +

              我们可以用一个线程安全类来解决前面的Count请求的需求:

              +
              @ThreadSafe
              public class CountingFactorizer implements Servlet{
              private final AtomicLong count = new AtomicLong(0);

              public long getCount(){ return count.get(); }

              public void service(SevletRequest req,ServletResponse resp){
              BigInteger i = extractFromRequest(req);
              BigInteger[] factors = factor(i);
              count.incrementAndGet();
              encodeIntoResponse(resp,factors);
              }
              }
              + + + + + + + +

              加锁机制

              线程安全分析法与为什么要加锁

              上面说到,当对象内仅有一个状态时,可以通过使用线程安全类来保障原子性。但当对象里存在多个状态时,就必须用锁来进行线程同步,而非简单地用多个线程安全类。

              +

              还是以上面的实例来解释。

              + + +
              public class UnsafeCachingFactorizer implements Servlet{
              private final AtomicReference<BigInteger> lastNumber = new AtomicReference<>();
              private final AtomicReference<BigInteger[]> lastFactors = new AtomicReference<>();

              public void service(ServletRequest req,ServletResponse resp){
              BigInteger i = extractFromRequest(req);
              /*
              此处产生了竞态条件。
              如果一个变量在此之后,return之前修改了lastFactors,就会寄
              */
              if (i.equals(lastNumber.get())) encodeIntoResponse(resp,lastFactors.get());
              else{
              BigInteger[] factors = factor(i);
              //本该需要瞬间一起完成的两个动作之间有时间间隔,不具原子性
              lastNumber.set(i);
              lastFactors.set(factors);
              encodeIntoResponse(resp,factors);
              }
              }
              }
              + + + + + +

              这段论述非常精彩,昭示了两个道理:1.分析线程安全性的时候,可以从“不变性条件不被破坏”开始考虑,首先考虑不变性条件应该是什么。2.在不变性条件涉及的多个变量彼此不独立,因而这些变量需要同时同步更新,上面那个例子就是因为不变性约束条件中的两个不独立变量没有同时同步更新。

              + + + + +

              确实,非常重要的一点就是在两个需要连续同时修改的变量之间有了并行的时间间隔,导致此期间并行的线程的不变性被破坏

              +

              内置锁

              + + + +

              同步代码块包含两部分,锁的引用和保护的代码段。关键字synchronized修饰的方法就是一段同步代码段,其锁对象为当前实例【非静态方法】或者是当前class的实例【静态方法】。

              +
              +

              这个具体的“锁”是什么以前是真不知道。已知的是所有Object都有wait和什么什么notify方法。不过想想也确实。所有线程争抢着访问一个对象的某个同步方法段,这不正跟所有线程争抢着一个锁是差不多意思的吗?“锁”的定义其实是很宽泛的

              +
              + + +

              java的内置锁并非无饥饿的。当线程B永远不释放锁,A会一直等待下去。

              + + +

              我们可以用synchronized来解决上面的计数器问题,即直接给service方法设为synchronized。当然这种方法性能很糟糕,因为它极大降低了并发度。

              +

              重入

              + +

              其中关于粒度的理解:

              +

              不是“每一次调用获取一次锁,该锁属于该此调用”,而是“每个线程调用时获取一次锁,该锁属于该线程”

              + + + + + + +
              public class Widget {
              public synchronized void doSomething(){

              }
              }

              class LoggingWidget extends Widget{
              @Override
              public synchronized void doSomething() {
              System.out.println(toString()+":calling doSomething.");
              super.doSomething();
              }
              }
              + +

              比如上述代码,创建了一个LoggingWidget实例,然后调用该实例的dosmething方法,就会获取到该实例的锁。如果不允许重入,那么在做super.doSomething时,该实例的锁【注意,是同一个实例】已经被占用还未释放,因此产生死锁。有重入就可以避免此问题

              +

              用锁来保护状态

              + + + +

              但这很考验人的记性。一旦你在某个地方忘了同步了就会寄。

              + + + + +

              活跃性与性能

              上面那个直接对service方法进行synchronized的改善方法粒度太粗了,可以试试如下方法:

              +
              @ThreadSafe
              public class CachedFactorizer implements Servlet{
              @GuardedBy ("this") private BigInteger lastNumber;
              @GuardedBy ("this") private BigInteger[] lastFactors;
              @GuardedBy ("this") private long hits;
              @GuardedBy ("this") private long cacheHits;

              public synchronized long getHits(){return hits;}
              public synchronized double getCacheHitRatio(){
              return (double)cacheHits/(double) hits;
              }

              public void service(ServletRequest req,ServletResponse resp){
              BigInteger i = extractFromRequest(req);
              BigInteger[] factors = null;
              synchronized (this){
              ++hits;
              if (i.equals(lastNumber)){
              ++cacheHits;
              factors = lastFactors.clone();
              }
              }
              //局部变量无需同步保护
              if (factors == null){
              factors = factor(i);
              synchronized (this){
              lastNumber = i;
              lastFactors = factors.clone();
              }
              }
              encodeIntoResponse(resp,factors);
              }
              }
              + + + +

              毕竟因数分解的时候无需同步保护,因为这时候参与运算的都是局部变量。

              + + + + +

              第三章 安全地共享对象

              上一章讲述了,线程安全的本质就是对共享和可变状态进行管理,以及介绍了用锁来保护状态。

              +

              本章将引入同步除原子性外的另一特性——可见性,然后再介绍如何构建线程安全类,并且安全地发布和共享对象。

              +

              关键词:可见性 Volatile 线程封闭 不可变对象

              + + + + + + +

              可见性

              引例——可见性的定义

              public class Main {
              private static boolean ready;
              private static int number;

              private static class ReaderThread extends Thread{
              public void run(){
              while(!ready){
              Thread.yield();
              }
              System.out.println(number);
              }
              }

              public static void main(String[] args){
              new ReaderThread().start();
              number=42;
              ready=true;
              }
              }
              + + + + + +
              +

              关于此程序显示出的对于内存可见性的理解,可以看这篇文章:

              +

              多线程(六):并发编程的三大特性之可见性

              + + +

              其实原因非常显而易见:主线程改了之后不会立刻把变量刷新到主存【可能默认是在ret时刷新,或者定时刷新,前者会导致相互等待的死锁,后者也会产生性能问题】,导致主线程的那个修改的flag变量对t1线程是**不可见**的,因此t1会继续循环等待。

              +
              +

              失效数据

              + + + + + + + +

              最低安全性

              + +

              注意,最低安全性不适用于非volatile类型的64位数据。

              + + + + + + +

              加锁与可见性

              + + + +

              要实现这种操作,我们可以设想一下关于内存可见性这一块内置锁的实现原理:lock时绑定指定变量,unlock时再刷新这个/些绑定变量的内存

              + + +

              所以说得有锁,并且锁还得是对的。

              +

              看着看着有种always语句块的感觉了2333

              + + + + +

              Volatile关键字

              Volatile保证内存可见性

              到其他线程。【我想大概就是一改变了,就马上刷新内存中的旧值,然后也许通过什么嗅探检测到值变化,通知所有线程改变自己持有的旧值。】 + + + +

              注意不放在寄存器里或者线程的私有栈里

              + + + + +
              + +
              +

              Volatile不保证原子性

              + +
              +

              volatile为什么不能保证原子性?

              +

              但这个有争议:

              + + +
                +
              • 在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里
              • +
              +

              也就是说,

              +

              如果线程B在+1前知道数据无效了,就会重新载入数据然后+1然后载入内存,结果正确;

              +

              如果线程B在+1后才知道数据无效,虽然会重新载入数据,数据为A修改后的新数据,但是此时指令无法回退,因而只能继续执行下一条指令:写回内存,B写回内存的是A修改后的新数据,因而结果错误。

              +
              大致过程:
              a:mov reg 1
              b:mov reg 1
              a:add reg 1
              b:add reg 1
              a:mov reg mem
              然后b线程得到通知,重新载入数据:mov reg mem
              但是指令无法回退:mov reg mem
              因而结果是A修改后的值被写入了两遍。
              + +

              所以其实volatile仅确保单次读写的瞬时线程安全

              + + +

              以下是别人的理解扩展:

              +

              对volatile不具有原子性的理解

              +

              volatile 无法保证原子性一个简单示例的疑问

              +

              Java并发编程:volatile关键字解析

              +
              +

              Volatile的使用方法

              + + + +

              下面给出一个volatile的典型用法:检查某个状态标记以判断是否退出循环。【也就是上文那个例子】

              +
              volatile boolean asleep;
              //...
              while(!asleep) countingSheep();
              + + + +

              发布与逸出

              发布与逸出的概念

              通俗地解释发布和逸出

              + + + + + + + + + +

              这个“逸出作用域”的表述非常不错。

              + + + + +

              什么时候会发生发布和逸出

              外部方法

              当把一个对象传递给某个外部方法,就相当于发布了这个对象

              +

              外部方法:

              +
              发布内部的类实例

              “this escape”

              + + +
              public class ThisEscape{
              public final int id;
              public final String name;
              public ThisEscape(EventSource source){
              id = 1;
              //发布
              source.registerListener(
              //内部类
              new EventListener(){
              public void onEvent(Event e){
              //doSomething(e);
              }
              }
              );
              name = "escape";
              }
              }
              + +
              +

              java this 逸出_Java并发编程——this引用逸出(“this” Escape)

              +

              并发编程实践中,this引用逃逸(“this”escape)是指对象还没有构造完成,它的this引用就被发布出去了。

              +

              ThisEscape在构造函数中引入了一个内部类EventListener,而内部类会自动的持有其外部类(这里是ThisEscape)的this引用。source.registerListener会将内部类发布出去,从而ThisEscape.this引用也随着内部类被发布了出去。但此时ThisEscape对象还没有构造完成 —— id已被赋值为1,但name还没被赋值,仍然为null。这样一来,就有些线程持有不完整实例,不确定性太大了

              +
              + + +

              也就是说,如果是单线程情况下,这样做是没问题的,毕竟最后都会构造完整。但多线程情况下,这俩有时间间隔,因此会产生问题,并且不能靠简单地把这句发布对象的语句放在构造函数最后一行。

              + + +

              这段话非常值得注意

              +

              所以说上面那个例子的正确代码:

              + + +
              public class SafeListener {
              private final EventListener listener;

              private SafeListener(){
              listener = new EventListener() {
              public void onEvent(Event e){
              //doSomething(e);
              }
              };
              }

              public static SafeListener newInstance(EventSource source){
              SafeListener safe = new SafeListener();
              source.registerListener(safe.listener);
              return safe;
              }
              }
              + + + +

              线程封闭

              线程封闭是什么

              + + + +

              线程封闭一般有三种方法,这三种方法的规范性是逐级递增的。

              +

              Ad-hoc线程封闭

              + +
              +

              这里,书写得非常地抽象。通过查阅资料可得解释得更通俗的:

              +

              Ad-hoc线程封闭

              +

              Example of ad hoc thread confinement in Java

              +

              总之其实精华就这一句话:

              +

              并且都是人为约束,并且一般可能会用volatile来控制单线程写这种情况下的同步。

              +
              // Don't modify this from any other thread than Thread X.
              // So use it read-only for those other threads.
              private volatile int someNumber;
              +
              +

              栈封闭

              + +

              也就是我们前面说的,局部变量只能在该线程内访问,除非逸出了,否则是非常安全的。

              + + + + +

              对于基本类型

              + +意思就是说,java没有指针,获取不了这些不是对象的基本类型的引用,因而这些基本类型不可能通过调用外部方法之类的逸出【调用外部方法仅仅是取得它们的一份copy而非本身】,所以这些基本类型的局部变量始终封闭在线程内。 + +

              对于引用类型

              + +

              因而需要格外注意逸出问题

              +

              下面给出对基本类型和引用类型栈封闭的实例:

              +
              public int loadTheArk(Collection<Animal> candidates){
              //引用类型
              SortedSet<Animal> animals;
              //基本类型
              int numPairs = 0;
              Animal candidate = null;

              //需要详细写好注释↓
              //animals被封闭在方法中,不要使它们逸出!
              animals = new TreeSet<Animal>(new SpeciesGenderComparator());
              animals.addAll(candidates);
              for (Animal a : animals){
              if (candidate == null || !candidate.isPotentialMate(a))
              candidate = a;
              else{
              ark.load(new AnimalPair(candidate,a));
              ++numPairs;
              candidate = null;
              new ThreadLocal<char[]>(){

              };
              }
              }
              return numPairs;
              }
              + + + +

              ThreadLocal类

              +

              史上最全ThreadLocal 详解(一)

              +
              +

              简介和应用实例

              上面介绍了使用局部变量来实现线程封闭的方法,也就是栈封闭。它只要合理地控制在调用方法时不发生逸出,就可以实现线程安全。

              +

              当有多个线程都需要同一类对象【比如Connection对象、ThreadID】,并且要求每个线程内的该对象是不一样的,并且该对象需要在多个方法中访问,栈封闭的方法就显得有些麻烦和不够优雅:需要在每个线程内都创建一个不同的对象实例,并且在调用方法的时候,都把该对象实例作为参数传进去。

              +

              这时候就需要ThreadLocal类了。

              +

              ThreadLocal类会给每个线程分配一个对象,并且仅需使用get方法,就能自动地把线程中的对象给弄出来。并且这些分配的对象对于各个线程来说都是隔离,相互不可见的,因此实现了线程封闭,具有安全性。

              + + +

              以ThreadID为例:

              +

              For example, the class below generates unique identifiers local to each thread. A thread’s id is assigned the first time it invokes ThreadId.get() and remains unchanged on subsequent calls.下面代码保证每个线程首次调用ThreadId.get方法后可以分配到一个不重ID,并且ID一旦确定,之后再调用get方法得到的ID是不会改变的。它这相当于维护了一个共有的计数器局部变量。

              +
              import java.util.concurrent.atomic.AtomicInteger;
              public class ThreadId {
              // Atomic integer containing the next thread ID to be assigned
              private static final AtomicInteger nextId = new AtomicInteger(0);

              // Thread local variable containing each thread's ID
              private static final ThreadLocal<Integer> threadId =
              new ThreadLocal<Integer>() {
              @Override protected Integer initialValue() {
              return nextId.getAndIncrement();
              }
              };

              // Returns the current thread's unique ID, assigning it if necessary
              public static int get() {
              return threadId.get();
              }
              }
              + +

              对于每个想获取自身ThreadID的线程,在所有想用到ID的方法中,只需:

              +
              void method(){
              AtomicInteger myID = ThreadID.get();
              //do something
              }
              + +

              而不用:

              +
              AtomicInteger myID = getID();
              void method(AtomicInteger myID){
              //do something
              }
              + +

              这样大大简化了实现。

              +

              再比如:

              + + +
              private static final String DB_URL = "";
              private static ThreadLocal<Connection> connectionHolder
              = new ThreadLocal<Connection>(){
              public Connection initialValue(){
              try {
              return DriverManager.getConnection(DB_URL);
              } catch (SQLException e) {
              throw new RuntimeException(e);
              }
              }
              };
              public static Connection getConnection(){
              return connectionHolder.get();
              }
              + + + +

              关于这个的大概代码猜想:

              +

              这样一来,在一个线程中使用toString,就仅需造一个buf【这个是ThreadLocal封闭】,而不用每次调用都造一个【这个是栈封闭】了

              +
              private static final ThreadLocal<char[]> buf
              = new ThreadLocal<char[]>(){
              @Override
              public char[] initialValue(){
              return new char[12];
              }
              };

              @Override
              public static String toString(int i) {
              //...
              //使用buf
              char[] buffer = buf.get();
              //...
              }
              + + + +

              底层实现

              大致结构

              我们可以初步猜想,ThreadLocal大概是通过一个map实现的,里面存储着<Thread,value>这样的键值对,每次就能通过Thread来取出对应的value了。Java低版本确实是这么做的。但Java的高版本对此进行了优化。

              + + +

              从本来的:ThreadLocalMap<Thread, value> ∈ ThreadLocal

              +

              变成了: ThreadLocalMap<ThreadLocal, value> ∈ Thread

              +

              并且其中的ThreadLocal这个key是以弱引用【WeakReference】的方式实现的。

              +
              +

              ThreadLocal探究

              +

              这样的结构演进有什么好处
              在旧版的ThreadLocal中,所有线程都将本地变量存在同一个ThreadLocalMap中,当并发量比较高的时候,ThreadLocalMap中的数据量会很大,而新版的ThreadLocalMap是属于线程的,也就是每个线程都操作属于自己的ThreadLocalMap,那么map中存储的变量就只有自己所存入的,数据量大大减少。

              +

              还有一个好处,旧版的ThreadLocalMap属于ThreadLocal,当Thread实例被销毁时ThreadLocalMap里该线程的数据不会被同时销毁【这也许就带来了危险性】,而新版ThreadLocalMap属于线程,线程被销毁时,ThreadLocalMap也随之销毁

              +
              +
              源码阅读
              +

              This class provides thread-local variables.线程局部变量,也就是我们说的线程封闭手法。

              +

              These variables differ from their normal counterparts in that each thread that accesses one (via its get or set method) has its own, independently initialized copy of the variable. 每个线程都有它自己的、独立初始化的该变量的副本。

              +

              ThreadLocal instances are typically private static fields in classes that wish to associate state with a thread (e.g., a user ID or Transaction ID).它一般用于私有静态字段,whose 状态和线程关系密切。

              +

              Each thread holds an implicit reference to its copy of a thread-local variable as long as the thread is alive and the ThreadLocal instance is accessible.

              +

              After a thread goes away, all of its copies of thread-local instances are subject to garbage collection (unless other references to these copies exist).最后会被垃圾回收

              +
              +
              public class ThreadLocal<T> {

              private final int threadLocalHashCode = nextHashCode();


              private static AtomicInteger nextHashCode =
              new AtomicInteger();


              private static final int HASH_INCREMENT = 0x61c88647;


              private static int nextHashCode() {
              return nextHashCode.getAndAdd(HASH_INCREMENT);
              }

              /*
              它这个初始化方法非常聪明且独特。
              一般使用它的时候是直接new然后重载一个匿名内部类的,
              于是就直接在建立匿名内部类时override此方法,在里面构造初始化的对象,
              且该方法仅在get调用的时候才会顺带调用
              有一种lazy的思想在里面。
              Normally, this method is invoked at most once per thread.
              */
              protected T initialValue() {
              return null;
              }

              //Creates a thread local variable.
              //The initial value of the variable is determined by Supplier的get方法.
              public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
              return new SuppliedThreadLocal<>(supplier);
              }

              public ThreadLocal() {
              }


              public T get() {
              //也就是说,每个线程都有个ThreadLocal的map成员变量
              //里面装的是<ThreadLocal变量,该变量在该线程的值>这样的键值对
              Thread t = Thread.currentThread();
              //得到线程里存储的ThreadLocalMap
              ThreadLocalMap map = getMap(t);
              if (map != null) {
              //<ThreadLocal, value>
              ThreadLocalMap.Entry e = map.getEntry(this);
              if (e != null) {
              @SuppressWarnings("unchecked")
              T result = (T)e.value;
              return result;
              }
              }
              //map==null【还没有线程局部变量】或者e==null【还没有该线程局部变量】
              return setInitialValue();
              }

              boolean isPresent() {
              Thread t = Thread.currentThread();
              ThreadLocalMap map = getMap(t);
              return map != null && map.getEntry(this) != null;
              }

              private T setInitialValue() {
              //获取初始化值
              T value = initialValue();
              Thread t = Thread.currentThread();
              ThreadLocalMap map = getMap(t);
              if (map != null) {
              map.set(this, value);
              } else {
              //传入空map的第一个结点
              createMap(t, value);
              }
              if (this instanceof TerminatingThreadLocal) {
              TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this);
              }
              return value;
              }

              public void set(T value) {
              Thread t = Thread.currentThread();
              ThreadLocalMap map = getMap(t);
              if (map != null) {
              map.set(this, value);
              } else {
              createMap(t, value);
              }
              }

              public void remove() {
              ThreadLocalMap m = getMap(Thread.currentThread());
              if (m != null) {
              m.remove(this);
              }
              }

              ThreadLocalMap getMap(Thread t) {
              return t.threadLocals;
              }

              void createMap(Thread t, T firstValue) {
              t.threadLocals = new ThreadLocalMap(this, firstValue);
              }

              static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
              return new ThreadLocalMap(parentMap);
              }

              T childValue(T parentValue) {
              throw new UnsupportedOperationException();
              }

              static final class SuppliedThreadLocal<T> extends ThreadLocal<T> {

              private final Supplier<? extends T> supplier;

              SuppliedThreadLocal(Supplier<? extends T> supplier) {
              this.supplier = Objects.requireNonNull(supplier);
              }

              @Override
              protected T initialValue() {
              return supplier.get();
              }
              }

              /*
              ThreadLocalMap is a customized hash map suitable only for maintaining thread local values.
              To help deal with very large and long-lived usages, the hash table entries use WeakReferences for keys.key【ThreadLocal】是弱引用的
              */
              static class ThreadLocalMap {

              /*
              Note that null keys (i.e. entry.get() == null) mean that the key is no longer referenced, so the entry can be expunged from table. Such entries are referred to as "stale entries" in the code that follows.
              意思就是说陈旧条目【stale entry】指的是key为空的
              */
              static class Entry extends WeakReference<ThreadLocal<?>> {
              //The value associated with this ThreadLocal.
              Object value;

              Entry(ThreadLocal<?> k, Object v) {
              super(k);
              value = v;
              }
              }

              //The initial capacity -- MUST be a power of two.
              private static final int INITIAL_CAPACITY = 16;

              private Entry[] table;

              private int size = 0;

              //The next size value at which to resize.
              private int threshold; // Default to 0

              //Set the resize threshold to maintain at worst a 2/3 load factor.
              //默认情况下,装载因子为2/3
              private void setThreshold(int len) {
              threshold = len * 2 / 3;
              }

              //使i增加,并且让增加后的结果模len。也就是(++i)%len。
              private static int nextIndex(int i, int len) {
              return ((i + 1 < len) ? i + 1 : 0);
              }

              //也就是(--i)%len。
              private static int prevIndex(int i, int len) {
              return ((i - 1 >= 0) ? i - 1 : len - 1);
              }

              //Construct a new map initially containing (firstKey, firstValue).
              //ThreadLocalMaps are constructed lazily,
              //so we only create one when we have at least one entry to put in it.
              ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
              table = new Entry[INITIAL_CAPACITY];
              //依旧是HashMap里面经典的掩码操作,key的hashcode作为entry在table里的序号
              //与hashmap的差别就在于,hashmap的桶table一个里面可以存放多个结点,
              //但这里的ThreadLocal的hash显然是不冲突的,因而只能存放一个结点
              int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
              table[i] = new Entry(firstKey, firstValue);
              size = 1;
              setThreshold(INITIAL_CAPACITY);
              }

              private ThreadLocalMap(ThreadLocalMap parentMap) {
              Entry[] parentTable = parentMap.table;
              int len = parentTable.length;
              setThreshold(len);
              table = new Entry[len];

              for (int j = 0; j < len; j++) {
              Entry e = parentTable[j];
              //有对应结点
              if (e != null) {
              @SuppressWarnings("unchecked")
              ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
              if (key != null) {
              Object value = key.childValue(e.value);
              Entry c = new Entry(key, value);
              int h = key.threadLocalHashCode & (len - 1);
              while (table[h] != null)
              h = nextIndex(h, len);
              table[h] = c;
              size++;
              }
              }
              }
              }

              private Entry getEntry(ThreadLocal<?> key) {
              int i = key.threadLocalHashCode & (table.length - 1);
              Entry e = table[i];
              if (e != null && e.get() == key)
              return e;
              else
              return getEntryAfterMiss(key, i, e);
              }

              private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
              Entry[] tab = table;
              int len = tab.length;

              while (e != null) {
              ThreadLocal<?> k = e.get();
              if (k == key)
              return e;
              if (k == null)
              expungeStaleEntry(i);
              else
              i = nextIndex(i, len);
              e = tab[i];
              }
              return null;
              }

              private void set(ThreadLocal<?> key, Object value) {

              // We don't use a fast path as with get() because it is at
              // least as common to use set() to create new entries as
              // it is to replace existing ones, in which case, a fast
              // path would fail more often than not.

              Entry[] tab = table;
              int len = tab.length;
              int i = key.threadLocalHashCode & (len-1);

              for (Entry e = tab[i];
              e != null;
              e = tab[i = nextIndex(i, len)]) {
              ThreadLocal<?> k = e.get();

              if (k == key) {
              e.value = value;
              return;
              }

              if (k == null) {
              replaceStaleEntry(key, value, i);
              return;
              }
              }

              tab[i] = new Entry(key, value);
              int sz = ++size;
              if (!cleanSomeSlots(i, sz) && sz >= threshold)
              rehash();
              }


              private void remove(ThreadLocal<?> key) {
              Entry[] tab = table;
              int len = tab.length;
              int i = key.threadLocalHashCode & (len-1);
              for (Entry e = tab[i];
              e != null;
              e = tab[i = nextIndex(i, len)]) {
              if (e.get() == key) {
              e.clear();
              expungeStaleEntry(i);
              return;
              }
              }
              }


              private void replaceStaleEntry(ThreadLocal<?> key, Object value,
              int staleSlot) {
              Entry[] tab = table;
              int len = tab.length;
              Entry e;

              // Back up to check for prior stale entry in current run.
              // We clean out whole runs at a time to avoid continual
              // incremental rehashing due to garbage collector freeing
              // up refs in bunches (i.e., whenever the collector runs).
              int slotToExpunge = staleSlot;
              for (int i = prevIndex(staleSlot, len);
              (e = tab[i]) != null;
              i = prevIndex(i, len))
              if (e.get() == null)
              slotToExpunge = i;

              // Find either the key or trailing null slot of run, whichever
              // occurs first
              for (int i = nextIndex(staleSlot, len);
              (e = tab[i]) != null;
              i = nextIndex(i, len)) {
              ThreadLocal<?> k = e.get();

              // If we find key, then we need to swap it
              // with the stale entry to maintain hash table order.
              // The newly stale slot, or any other stale slot
              // encountered above it, can then be sent to expungeStaleEntry
              // to remove or rehash all of the other entries in run.
              if (k == key) {
              e.value = value;

              tab[i] = tab[staleSlot];
              tab[staleSlot] = e;

              // Start expunge at preceding stale entry if it exists
              if (slotToExpunge == staleSlot)
              slotToExpunge = i;
              cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
              return;
              }

              // If we didn't find stale entry on backward scan, the
              // first stale entry seen while scanning for key is the
              // first still present in the run.
              if (k == null && slotToExpunge == staleSlot)
              slotToExpunge = i;
              }

              // If key not found, put new entry in stale slot
              tab[staleSlot].value = null;
              tab[staleSlot] = new Entry(key, value);

              // If there are any other stale entries in run, expunge them
              if (slotToExpunge != staleSlot)
              cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
              }


              private int expungeStaleEntry(int staleSlot) {
              Entry[] tab = table;
              int len = tab.length;

              // expunge entry at staleSlot
              tab[staleSlot].value = null;
              tab[staleSlot] = null;
              size--;

              // Rehash until we encounter null
              Entry e;
              int i;
              for (i = nextIndex(staleSlot, len);
              (e = tab[i]) != null;
              i = nextIndex(i, len)) {
              ThreadLocal<?> k = e.get();
              if (k == null) {
              e.value = null;
              tab[i] = null;
              size--;
              } else {
              int h = k.threadLocalHashCode & (len - 1);
              if (h != i) {
              tab[i] = null;

              // Unlike Knuth 6.4 Algorithm R, we must scan until
              // null because multiple entries could have been stale.
              while (tab[h] != null)
              h = nextIndex(h, len);
              tab[h] = e;
              }
              }
              }
              return i;
              }


              private boolean cleanSomeSlots(int i, int n) {
              boolean removed = false;
              Entry[] tab = table;
              int len = tab.length;
              do {
              i = nextIndex(i, len);
              Entry e = tab[i];
              if (e != null && e.get() == null) {
              n = len;
              removed = true;
              i = expungeStaleEntry(i);
              }
              } while ( (n >>>= 1) != 0);
              return removed;
              }


              private void rehash() {
              expungeStaleEntries();

              // Use lower threshold for doubling to avoid hysteresis
              if (size >= threshold - threshold / 4)
              resize();
              }


              private void resize() {
              Entry[] oldTab = table;
              int oldLen = oldTab.length;
              int newLen = oldLen * 2;
              Entry[] newTab = new Entry[newLen];
              int count = 0;

              for (int j = 0; j < oldLen; ++j) {
              Entry e = oldTab[j];
              if (e != null) {
              ThreadLocal<?> k = e.get();
              if (k == null) {
              e.value = null; // Help the GC
              } else {
              int h = k.threadLocalHashCode & (newLen - 1);
              while (newTab[h] != null)
              h = nextIndex(h, newLen);
              newTab[h] = e;
              count++;
              }
              }
              }

              setThreshold(newLen);
              size = count;
              table = newTab;
              }


              private void expungeStaleEntries() {
              Entry[] tab = table;
              int len = tab.length;
              for (int j = 0; j < len; j++) {
              Entry e = tab[j];
              if (e != null && e.get() == null)
              expungeStaleEntry(j);
              }
              }
              }
              }
              + +

              注意点:

              +
                +
              1. 哈希方法和解决哈希冲突
                + +

                存在哈希冲突的话,大概是采用的线性探测方法。

                +
              2. +
              3. 解决内存泄漏

                关于其remove方法:

                +
                public void remove() {
                ThreadLocalMap m = getMap(Thread.currentThread());
                if (m != null)
                m.remove(this);
                }
                //m.remove
                private void remove(ThreadLocal<?> key) {
                Entry[] tab = table;
                int len = tab.length;
                int i = key.threadLocalHashCode & (len-1);
                //线性探测
                for (Entry e = tab[i];
                e != null;
                e = tab[i = nextIndex(i, len)]) {
                if (e.get() == key) {
                e.clear();
                expungeStaleEntry(i);
                return;
                }
                }
                }
                + +

                两篇文章都有解释

                +
                +

                remove方法,直接将ThrealLocal 对应的值从当前相差Thread中的ThreadLocalMap中删除。为什么要删除,这涉及到内存泄露的问题。

                +

                实际上 ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,弱引用的特点是,如果这个对象只存在弱引用,那么在下一次垃圾回收的时候必然会被清理掉。

                +

                所以如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候会被清理掉的,这样一来 ThreadLocalMap中使用这个 ThreadLocal 的 key 也会被清理掉。但是,value 是强引用,不会被清理,这样一来就会出现 key 为 null 的 value。

                +

                ThreadLocal其实是与线程绑定的一个变量,如此就会出现一个问题:如果没有将ThreadLocal内的变量删除(remove)或替换,它的生命周期将会与线程共存。通常线程池中对线程管理都是采用线程复用的方法,在线程池中线程很难结束甚至于永远不会结束,这将意味着线程持续的时间将不可预测,甚至与JVM的生命周期一致。举个例字,如果ThreadLocal中直接或间接包装了集合类或复杂对象,每次在同一个ThreadLocal中取出对象后,再对内容做操作,那么内部的集合类和复杂对象所占用的空间可能会开始持续膨胀。
                ————————————————
                版权声明:本文为CSDN博主「倔强的不服」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
                原文链接:https://blog.csdn.net/u010445301/article/details/111322569

                +
                +
                +

                ThreadLocal内存泄漏问题的解析。
                前面我们说到它虽然线程安全,但是它存在一个问题那就是内存泄漏。

                +

                首先我们要明白为什么会内存泄漏,前面也说了ThreaLocal是一个弱引用,什么是弱引用就是当它为null时候,就会被垃圾回收机制给带走,重点就是,如果我们的ThreadLocal突然为null,然后就被回收了,但此时我们的ThreadLocalMap它的生命周期是和Thread相同的,简单理解就是,裤子没了,兜还在,兜里面还有我们的数据,这就造成了内存泄漏。

                +

                如何解决那:我们必须在使用完ThreadLocal后,执行remove()方法,避免内存溢出。
                ————————————————
                版权声明:本文为CSDN博主「某刘姓男子i的码农客栈」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
                原文链接:https://blog.csdn.net/qq_20783497/article/details/107980858

                +
                +
              4. +
              +

              不变性

              不可变对象

              不可变对象的线程安全性

              满足同步需求的另一种方案就是使用不可变对象

              + + +

              这个思路非常地简单粗暴:什么东西影响了,就直接让它消失。非常有意思2333

              +

              如果某个对象在创建后不能被修改,那么它就叫不可变对象。线程安全性是不可变对象的固有属性之一

              + + +

              比如说final域只能在声明的成员域或者构造函数中初始化,两者本质上都是在构造函数中初始化的。

              +

              并且不可变对象也更加安全。

              + + + + +

              不可变对象与final域

              不可变性不等于将对象中的所有域都设置为final域,因为final类型的域可以是对可变对象的引用。【这就类似C语言中const指针】当且仅当满足下列条件,对象才是不可变的:

              + + +
              +

              对于这里注释提到的String类,它讲得有些让人迷惑。因而我查阅资料得到解说如下:

              +

              String中hashCode方法的线程安全

              +
              class String{
                //默认值是0
                int hash;

                public int hashCode() {
              //将成员变量hash缓存到局部变量
              int h = hash;
                   //这里使用的是局部变量,因此没有多线程修改的风险
              if (h == 0 && value.length > 0) {
              char val[] = value;
              //求hashcode过程使用局部h变量防止产生静态条件
              for (int i = 0; i < value.length; i++) {
              h = 31 * h + val[i];
              }

              //把求出的hashcode缓存到局部变量,原子操作
              //这里不需要考虑线程可见性的问题,
              //如果其它线程未能及时看到最新修改,重新计算hash值代价也不大
              hash = h;
              }
              return h;
              }
              }
              + +

              再回去看书中注释的描述:

              + + +

              这个的意思就是说,对每个线程来说,同一个字符串hashcode值都是一样的【每次计算都得到相同的结果】,所以就不会产生多个线程计算出不同值的情况,导致不同步的发生。

              + + +

              意思是说之所以hashcode值一样,是因为这个hashcode计算是基于不可变对象的:

              +
              private final char value[];
              + +

              并且重复计算性能代价可能远没有加锁的消耗来得大,因而这里仅使用了栈封闭来保证一定程度上的线程同步。

              +
              +

              可变对象基础上构建不可变类

              + +
              public final class ThreeStooge {
              private final Set<String> stooges = new HashSet<>();

              public ThreeStooge(){
              stooges.add("Moe");
              stooges.add("Larry");
              stooges.add("Curly");
              }

              public boolean isStooge(String name){
              return stooges.contains(name);
              }
              }
              + + + +

              也就是说,实现的核心是保证可变对象不变即可。

              + + + + +

              Final域

              + +

              final不仅保证了引用对象的不可变,还保证了不可变对象初始化过程中的线程安全性

              + + +

              所以说还是尽量多用final

              +

              Volatile与不可变对象提供弱原子性

              也就是这个图片中所说的:

              + + +

              使用volatile变量来发布不可变对象,不仅可以更新保存在不可变对象中的程序状态,还可以为一组操作提供弱原子性。

              + + +

              前面的代码为:

              +
              public class UnsafeCachingFactorizer implements Servlet{
              private final AtomicReference<BigInteger> lastNumber = new AtomicReference<>();
              private final AtomicReference<BigInteger[]> lastFactors = new AtomicReference<>();

              public void service(ServletRequest req,ServletResponse resp){
              BigInteger i = extractFromRequest(req);
              /*竞态条件*/
              if (i.equals(lastNumber.get())) encodeIntoResponse(resp,lastFactors.get());
              else{
              BigInteger[] factors = factor(i);
              //时间间隔
              lastNumber.set(i);
              lastFactors.set(factors);
              encodeIntoResponse(resp,factors);
              }
              }
              }
              + +

              如今,利用volatile和不可变类的相互配合,我们修改如下:

              +
              public class VolatileCachedFactorizer implements Servlet{
              //从使用两个分别原子的变量,变为使用一个volatile修饰的不可变类
              private volatile OneValueCache cache = new OneValueCache(null,null);

              public void service(ServletRequest req,ServletResponse resp){
              BigInteger i = extractFromRequest(req);
              BigInteger[] factors = cache.getFactors(i);
              if (factors == null){
              factors = factor(i);
              //直接new一个新容器,利用了final域在初始化过程中的线程安全,因而保证了原子性
              //同时也用了volatile快刷新的性质,保证了可见性,当一个线程设置为新的,其他会立即看到
              //妙啊,这样就非常完美地达成了线程安全性
              cache = new OneValueCache(i,factors);
              }
              encodeIntoResponse(resp,factors);
              }
              }

              public class OneValueCache {
              private final BigInteger lastNumber;
              private final BigInteger[] lastFactors;

              public OneValueCache(BigInteger lastNumber, BigInteger[] lastFactors) {
              this.lastNumber = lastNumber;
              //传递副本,保证不可变
              this.lastFactors = Arrays.copyOf(lastFactors,lastFactors.length);
              }

              public BigInteger[] getFactors(BigInteger i){
              if (lastNumber == null || !lastNumber.equals(i)){
              return null;
              }
              else{
              //传递副本,保证不可变
              return Arrays.copyOf(lastFactors,lastFactors.length);
              }
              }
              }
              + + + +

              每当需要对一组相关数据以原子方式执行某个操作时,就可以考虑创建一个不可变的类来包含这些数据,其本质就是利用不可变性消除了访问和更新多个变量的竞态条件。

              + + +

              因为依然满足该程序的不变性原理:factor数组中各个数字的乘积=lastNumber,也就是说容器对象的两个值都是正确对应的,因而容器对象处于一致的状态。又或者是因为volatile及时刷新,因此确保了各个线程的内存可见性。

              + + + + +

              安全发布

              + +

              现在,我们要来讲讲如何安全地对对象进行发布。

              +

              一个不正确程序案例

              public Holder holder;

              public void initialize(){ holder = new Holder(); }

              //以下是Holder类定义
              public class Holder{
              private int n;

              public Holder(int n){ this.n = n; }

              public void assertSanity(){
              if( n != n )
              throw new AssertionError("This statement is false.");
              }
              }
              + +

              Holder类本身是没有问题的,这段代码出问题的原因是holder没有被正确地发布。

              +

              关于holder为什么没有被正确地发布:

              +

              参考文章

              【并发编程】安全发布对象与防止对象逸出(原因与防护方法)

              +

              分析过程

              由参考文章1:

              + + +

              可知,new一个对象并非原子操作,并且很有可能先得到内存引用才初始化对象。

              +

              因而,在上面那段不安全代码的语境下可分析:

              +
              Holder的错误发布有三点如下:

              首先明确,引用,和引用的对象的状态,这两个是两个需要独立考虑的方面。前者是一个指针值,后者是指针所指的数据。下面的点1仅考虑引用的更新,点2考虑了引用对象的状态更新。

              +

              \1. 发布对象的那个线程给holder初始化之后,holder这个引用没有及时刷新到内存,因而对其他线程不可见,其他线程读到的holder引用是旧的。

              +

              \2. 又或者,发布了holder还没初始化完毕的时候,别的进程读取到未完成初始化的holder这个引用,但这个引用指向的状态却是旧的,因为它还没完成初始化,其状态值为旧值或者默认值。【发生了上面new一个对象的指令重排】

              +

              \3. 如果在assert方法中两次读取n发生了上面第二条,就可能会导致前后的n不唯一,抛出异常。

              +

              由书中表述,如果将Holder转化为不可变类,那么该发布是安全的。

              + + +

              至于为什么,可见下个标题。

              +
              +

              此处插入思考:是否可以将public Holder holder修改为public final Holder holder,或者volatile修饰,来解决上述问题呢?

              +

              java多线程关键字final和static详解

              +

              通过看该文章得知:

              + + +

              volatile和final都会禁止字段引用的对象在构造对象过程中发生指令重排,别的线程得到引用的时候构造已经完成,而不会先得到引用再完成构造,并且两个标志都可以保证可见性。

              +

              不过继续读下去,书中给出了答案:我说的这个方法也是可行的。

              + + +

              我的疑问就是第二点和第三点。

              +
              +

              不可变对象的初始化安全性

              + + + + + +

              安全发布的常用模式

              + +

              分别解说

              +

              静态初始化对象引用

              + +

              volatile、final以及AtomicReferance保护引用

              详见上面那个不正确案例最后的思考

              +

              由锁保护的区域

              这个区域除了是通过程序构造的,也可以是使用Java自带的线程安全类库

              + + + + +

              事实不可变对象

              安全发布可以保证发布时的线程安全所以说你如果承诺发布后可以一直保证不可变,那就一直都是线程安全的。

              + + + + + + + + +

              对象的可变性与正确发布

              + + + +

              安全地共享对象

              + + + +

              第四章 对象的组合

              + +

              也就是说上面都是在讲怎么让一个对象的共享变得安全,下面我们讲怎么依据设计模式,让一个类更容易成为线程安全的

              +

              如何设计线程安全的类

              + + + + + + + + + + + +

              收集同步需求

              本质上是找不变性条件和后验条件

              要保证不变性条件始终成立,确保后验条件符合预期。

              + + +

              讲了什么是不变性条件和后验条件:

              + + + + +

              无效的状态转换只能出现在原子序列中

              + + + + +

              依赖状态的操作

              + +

              也就是说先验条件和状态域相关。

              + + + + +

              状态的所有权

              + + + +

              666666

              + + + + +

              实例封闭

              什么是实例封闭

              + + + +

              所以需要上一章学的安全发布。

              + + +
              //通过封闭机制保证线程安全
              @ThreadSafe
              public class PersonSet {
              //不安全
              //封闭在实例内部
              private final Set<Person> mySet = new HashSet<>();

              //对所有代码路径加锁访问
              public synchronized void addPerson(Person p) {
              mySet.add(p);
              }
              public synchronized boolean containsPerson(Person p){
              return mySet.contains(p);
              }
              }
              + + + + + +

              阅读源码可知:

              +
              static class SynchronizedCollection<E> implements Collection<E>, Serializable {
              private static final long serialVersionUID = 3053995032091335093L;

              final Collection<E> c; // Backing Collection
              final Object mutex; // Object on which to synchronize

              SynchronizedCollection(Collection<E> c) {
              this.c = Objects.requireNonNull(c);
              mutex = this;
              }

              SynchronizedCollection(Collection<E> c, Object mutex) {
              this.c = Objects.requireNonNull(c);
              this.mutex = Objects.requireNonNull(mutex);
              }

              public int size() {
              synchronized (mutex) {return c.size();}
              }
              public boolean isEmpty() {
              synchronized (mutex) {return c.isEmpty();}
              }
              public boolean contains(Object o) {
              synchronized (mutex) {return c.contains(o);}
              }
              public Object[] toArray() {
              synchronized (mutex) {return c.toArray();}
              }
              public <T> T[] toArray(T[] a) {
              synchronized (mutex) {return c.toArray(a);}
              }

              //注意此处没用同步
              public Iterator<E> iterator() {
              return c.iterator(); // Must be manually synched by user!
              }

              public boolean add(E e) {
              synchronized (mutex) {return c.add(e);}
              }
              public boolean remove(Object o) {
              synchronized (mutex) {return c.remove(o);}
              }

              public boolean containsAll(Collection<?> coll) {
              synchronized (mutex) {return c.containsAll(coll);}
              }
              public boolean addAll(Collection<? extends E> coll) {
              synchronized (mutex) {return c.addAll(coll);}
              }
              public boolean removeAll(Collection<?> coll) {
              synchronized (mutex) {return c.removeAll(coll);}
              }
              public boolean retainAll(Collection<?> coll) {
              synchronized (mutex) {return c.retainAll(coll);}
              }
              public void clear() {
              synchronized (mutex) {c.clear();}
              }
              public String toString() {
              synchronized (mutex) {return c.toString();}
              }
              // Override default methods in Collection
              @Override
              public void forEach(Consumer<? super E> consumer) {
              synchronized (mutex) {c.forEach(consumer);}
              }
              @Override
              public boolean removeIf(Predicate<? super E> filter) {
              synchronized (mutex) {return c.removeIf(filter);}
              }
              @Override
              public Spliterator<E> spliterator() {
              return c.spliterator(); // Must be manually synched by user!
              }
              @Override
              public Stream<E> stream() {
              return c.stream(); // Must be manually synched by user!
              }
              @Override
              public Stream<E> parallelStream() {
              return c.parallelStream(); // Must be manually synched by user!
              }
              private void writeObject(ObjectOutputStream s) throws IOException {
              synchronized (mutex) {s.defaultWriteObject();}
              }
              }
              + +

              就是把原来的collection给实例封闭了,之后的访问都用了同步锁。

              +

              Java监视器模式

              使用内置锁

              + +

              直白点来说,就是把所有要访问自己状态的地方/方法通通synchronized。

              + + +
              //监视器模式
              @ThreadSafe
              public final class Counter {
              @GuardedBy("this") private long value = 0;

              public synchronized long getValue(){
              return value;
              }

              public synchronized long increment(){
              if (value == Long.MAX_VALUE){
              throw new IllegalStateException();
              }
              return ++value;
              }
              }
              + +

              这样虽然简单,但缺点就是很粗暴:同步的粒度太粗了。

              +

              使用私有锁

              也跟内置锁道理差不多

              + + +

              也就是说私有锁可以让外面的世界也参与到同步中来,但内置锁不大行。

              +

              示例:车辆追踪

              public class MutablePoint {
              public int x,y;

              public MutablePoint() {
              x=0;y=0;
              }
              public MutablePoint(MutablePoint p){
              //深拷贝
              this.x=p.x;
              this.y=p.y;
              }
              }
              + +
              //基于监视器模式
              public class MonitorVehicleTracker {
              private final Map<String, MutablePoint> locations;

              public MonitorVehicleTracker(
              Map<String,MutablePoint> locations
              ){
              this.locations = deepCopy(locations);
              }

              public synchronized Map<String,MutablePoint> getLocations(){
              return deepCopy(locations);
              }
              public synchronized MutablePoint getLocation(String id){
              MutablePoint loc = locations.get(id);
              //返回copy对象,深拷贝
              return loc == null?null : new MutablePoint(loc);
              }
              public synchronized void setLocation(String id,int x,int y){
              MutablePoint loc = locations.get(id);
              if (loc == null) throw new IllegalArgumentException();
              loc.x = x;
              loc.y = y;
              }

              //为什么这方法不用锁?是因为调用它的地方都锁着
              private static Map<String,MutablePoint> deepCopy(Map<String,MutablePoint> m){
              Map<String,MutablePoint> res = new HashMap<>();
              for (Map.Entry en : m.entrySet()){
              //此处通过MutablePoint的构造函数重新拷贝了一个Point
              //如果简单地使用HashMap的构造函数new HashMap(m)的拷贝来创建一个新的map是不行的
              //因为这样只会拷贝Point对象的指针值,依然是浅拷贝
              res.put((String) en.getKey(),new MutablePoint((MutablePoint) en.getValue()));
              }
              return Collections.unmodifiableMap(res);
              }
              }
              + + + + + +

              将线程安全性委托给独立的状态变量

              + +

              定义

              + + + +

              意思就是保证一个类里面仅有一个状态,只要该状态是线程安全的,那么该类也就是线程安全的

              +

              示例

              //线程安全
              public class Point {
              public final int x,y;

              public Point(int x, int y) {
              this.x = x;
              this.y = y;
              }
              }
              + +
              //将线程安全委托给ConcurrentMap
              public class DelegatingVehicleTracker {
              //用了两个
              private final ConcurrentMap<String,Point> locations;
              private final Map<String,Point> unmodifiableMap;

              public DelegatingVehicleTracker(Map<String,Point> ps) {
              locations = new ConcurrentHashMap<>(ps);
              unmodifiableMap = Collections.unmodifiableMap(locations);
              }

              public Map<String,Point> getLocations(){
              //unmodifiableMap baked by locations,所以locations变化也会反映到unmodifiableMap上
              //目的只是为了提供外界无法修改的视图
              //不得不说真是妙啊
              return unmodifiableMap;
              }
              public Point getLocation(String id){
              return locations.get(id);
              }

              public void setLocation(String id,int x,int y){
              if (locations.replace(id,new Point(x,y)) == null){
              throw new IllegalArgumentException();
              }
              }
              }
              + + + + + +
              public Map<String,Point> getLocations(){
              return Collections.unmodifiableMap(new HashMap<>(locations));
              }
              + + + +

              委托给多个状态变量

              + +

              也就是说这些对象彼此不会构成不变性条件。

              +
              //将线程安全性委托给多个状态变量
              public class VisualComponent {
              private final List<KeyListener> keyListeners = new CopyOnWriteArrayList<>();
              private final List<MouseListener> mouseListeners = new CopyOnWriteArrayList<>();

              public void addKeyListener(KeyListener listener){
              keyListeners.add(listener);
              }

              public void addMouseListener(MouseListener listener){
              mouseListeners.add(listener);
              }

              public void removeKeyListener(KeyListener listener){
              keyListeners.remove(listener);
              }

              public void remiveMouseListener(MouseListener listener){
              mouseListeners.remove(listener);
              }
              }
              + + + +

              而且键盘监听和鼠标监听彼此独立。

              +

              不独立多个状态变量不能委托

              public class NumberRange {
              //不变性条件: lower<=upper
              private final AtomicInteger lower = new AtomicInteger(0);
              private final AtomicInteger upper = new AtomicInteger(0);

              public void setLower(int i){
              //先检查后执行
              if (i>upper.get()){
              throw new IllegalArgumentException();
              }
              lower.set(i);
              }
              public void setUpper(int i){
              if (i<lower.get()){
              throw new IllegalArgumentException();
              }
              upper.set(i);
              }
              public boolean isInRange(int i){
              return (i>=lower.get() && i<=upper.get());
              }
              }
              + + + +

              根本原因就是因为不独立。

              + + + + +

              发布被委托的状态变量

              什么时候可以发布

              + + + +

              示例

              + +
              package sit;
              @ThreadSafe
              public class SafePoint {
              //注意此处x和y没有用任何同步修饰词修饰
              private int x,y;
              private SafePoint(int[] a){
              this(a[0],a[1]);
              }

              //此处为什么不直接用this(p.x,p.y)呢?
              //是因为x和y本身并没有任何线程安全的防护手段,这样做的话会发生竞态条件。况且x和y也被实例封闭了
              //私有构造函数捕获模式
              public SafePoint(SafePoint p){
              this(p.get());
              }

              public SafePoint(int x,int y){
              this.x=x;
              this.y=y;
              }

              //x、y都放入数组,保证x和y的同时读写,nb
              public synchronized int[] get(){
              return new int[] {x,y};
              }

              public synchronized void set(int x,int y){
              this.x=x;
              this.y=y;
              }
              }
              + +
              //跟上面那个委托没什么差,区别只在于上面的那个SafePoint类,既是线程安全的,又是可修改的
              public class PublishingVehicleTracker {
              private final Map<String,SafePoint> locations;
              private final Map<String,SafePoint> unmodifiableMap;

              public PublishingVehicleTracker(Map<String,SafePoint> locations){
              this.locations = new ConcurrentHashMap<>(locations);
              this.unmodifiableMap = Collections.unmodifiableMap(this.locations);
              }

              public Map<String,SafePoint> getLocations(){
              return unmodifiableMap;
              }

              public SafePoint getLocation(String id){
              return locations.get(id);
              }

              public void setLocations(String id,int x,int y){
              if (!locations.containsKey(id))
              throw new IllegalArgumentException();
              locations.get(id).set(x,y);
              }
              }
              + + + + + +

              这仅仅是一个委托发布的实例。

              +

              在现有的线程安全类中添加功能

              引论

              + +

              比如说想给vector添加一个“put-if-absent”

              + + +

              可以用子类扩展法,也可以直接加源代码。后者有时候源代码不可访问,前者的父类很多域可能不对子类开发,并且非常脆弱。因而下面介绍几种比较好的机制。

              +

              客户端加锁机制

              定义和实例

              + +
              @NotThreadSafe
              public class NotThreadSafeListHelper<E> {
              public List<E> list = Collections.synchronizedList(new ArrayList<>());

              public synchronized boolean putIfAbsent(E x){
              boolean absent = !list.contains(x);
              if (absent)
              list.add(x);
              return absent;
              }
              }
              + + + +

              我曹,66666666

              +

              也就是说,这里加的是ListHelper的锁,只能让别的线程不能通过putIfAbsent方法同时修改list,但别的线程完全可以直接获取list再修改。

              + + +

              客户端指的是我们的ListHelper。我们正是不知道list这个对象使用的是哪一个锁才发愣的。

              +

              所以我们使用ArrayList自身的锁,也就是list自己的内置,来加锁。

              +
              //使用客户端加锁实现
              @ThreadSafe
              public class ListHelper<E> {
              public List<E> list = Collections.synchronizedList(new ArrayList<>());

              public boolean putIfAbsent(E x){
              synchronized(list) {
              boolean absent = !list.contains(x);
              if (absent)
              list.add(x);
              return absent;
              }
              }
              }
              + +

              评价

              + + + +

              它非常依赖于其他类的客户端加锁机制。

              + + +

              确实,毕竟你锁被外界拿去用了。

              +

              组合

              + +
              public class ImprovedList<T> implements List<T> {
              //实例封闭
              private final List<T> list;

              public ImprovedList(List<T> list){this.list=list;}

              public synchronized boolean putIfAbsent(T x){
              boolean contains = list.contains(x);
              if (contains)
              list.add(x);
              return !contains;
              }

              public synchronized void clear(){list.clear();}
              //... 按照类似的方式委托List接口其他未实现的方法
              }
              + + + +

              是的,跟synchronizedList非常像

              + + +

              这也就是用的java的监视器模式了

              +

              第五章 基础构建模块

              + +

              保证独立即可委托,从而构建一个线程安全类

              + + +

              同步容器类

              + +

              差不多都是使用的监视器模式。

              +

              同步容器类:Vector、Hashtable、Collections.synchronizedXxx

              +

              同步容器类的问题

              + +

              也就是需要避免两个原子操作之间的非线程安全的时间间隔。

              +
                 public static Object getLast(Vector list){
              //复合操作
              //先检查后执行
              int lastIndex = list.size()-1;
              return list.get(lastIndex);
              }

              public static void deleteLast(Vector list){
              int lastIndex = list.size()-1;
              list.remove(lastIndex);
              }
              + + + +

              它这段话说得非常好。由于Vector这个类本身是线程安全的,因而它可以保证外部任何操作都不会导致该对象因为并发而被破坏。但是,我们用不加锁的复合操作虽然不会破坏Vector,但可能导致不能出现我们想要的结果。

              +

              所以我们必须用锁机制来对此复合操作进行保护:

              +
              public static Object getLast(Vector list){
              synchronized (list) {
              //复合操作
              //先检查后执行
              int lastIndex = list.size()-1;
              return list.get(lastIndex);
              }
              }

              public static void deleteLast(Vector list){
              synchronized (list){
              int lastIndex = list.size()-1;
              list.remove(lastIndex);
              }
              }
              + +

              除此之外,迭代也是一种经典的复合操作。我们可以通过下面这种粗粒度加锁来避免:

              +
              public static void travel(Vector list){
              synchronized (list) {
              for (int i=0;i<list.size();i++){
              //do something
              }
              }
              }
              + + + +

              迭代器与ConcurrentModificationException

              + +

              所以才会引入fail-fast机制。

              + + +
              +

              foreach语法糖内部是通过Iterator来实现的。

              +

              Java 的 foreach 本质

              +
              public void testIterableForEach() {
              List<String> list = new ArrayList<>();
              for (String str : list) {
              System.out.println(str);
              }
              }
              //反编译后:
              public void testIterableForEach() {
              List<String> list = new ArrayList<>();
              Iterator i = list.iterator();
              while(i.hashNext()){
              String str = (String)i.next();
              System.out.println(str);
              }
              }
              +
              + + + + +

              可见同步容器类还是有很多局限性的。

              +

              隐藏迭代器

              有时候,迭代会隐藏起来。要一个个揪出需要加锁的地方是非常麻烦的。

              +
              //隐藏在字符串连接中的迭代操作
              @NotThreadSafe
              public class HiddenIterator {
              private final Set<Integer> set = new HashSet<>();

              public synchronized void add(Integer i){ set.add(i); }
              public synchronized void remove(Integer i){ set.remove(i); }

              public void addTenThings(){
              Random r = new Random();
              for (int i=0; i<10; i++) {
              add(r.nextInt());
              }
              //隐式迭代
              System.out.println("DEBUG: Added ten elements to "+ set);
              }
              }

              + + + + + + + + + +

              并发容器类

              同步容器类的加锁太粗粒度了,导致并发性弱。因而引入并发容器类来解决问题。

              + + +

              ConcurrentHashMap —— HashMap

              +

              CopyOnWriteArrayList —— List

              + + + + +

              BlockingQueue

              + + +

              ConcurrentSkipListMap —— TreeMap

              +

              ConcurrentSkipListSet —— TreeSet

              +

              ConcurrentHashMap

              + +

              使用分段锁来细粒度加锁。

              + + +
              +

              关于ConcurrentHashMap的分段锁:ConcurrentHashMap

              +

              JDK1.7中,ConcurrentHashMap 类所采用的正是分段锁的思想,将 HashMap 进行切割,把 HashMap 中的哈希数组切分成小数组,每个小数组有 n 个 HashEntry 组成,其中小数组继承自ReentrantLock(可重入锁),这个小数组名叫Segment

              +

              JDK1.8 中 ConcurrentHashMap 类取消了 Segment 分段锁,采用 CAS + synchronized 来保证并发安全,数据结构跟 jdk1.8 中 HashMap 结构类似,都是数组 + 链表(当链表长度大于 8 时,链表结构转为红黑二叉树)结构。

              +

              ConcurrentHashMap 中 synchronized 只锁定当前链表或红黑二叉树的首节点,只要节点 hash 不冲突,就不会产生并发,相比 JDK1.7 的 ConcurrentHashMap 效率又提升了 N 倍!

              +
              +
              +

              关于ConcurrentHashMap的弱一致性:ConcurrentHashMap的弱一致性

              +

              get方法是弱一致的,是什么含义?可能你期望往ConcurrentHashMap底层数据结构中加入一个元素后,立马能对get可见,但ConcurrentHashMap并不能如你所愿。换句话说,put操作将一个元素加入到底层数据结构后,get可能在某段时间内还看不到这个元素,若不考虑内存模型,单从代码逻辑上来看,却是应该可以看得到的。

              +
              + + +

              精确值—>估计值

              + + + + +
              +

              关于AQS框架:重大发现,AQS加锁机制竟然跟Synchronized有惊人的相似

              +

              在并发多线程的情况下,为了保证数据安全性,一般我们会对数据进行加锁,通常使用Synchronized或者ReentrantLock同步锁。Synchronized是基于JVM实现,而ReentrantLock是基于Java代码层面实现的,底层是继承的AQS

              +

              AQS全称**AbstractQueuedSynchronizer**,即抽象队列同步器,是一种用来构建锁和同步器的框架。

              +

              我们常见的并发锁ReentrantLockCountDownLatchSemaphoreCyclicBarrier都是基于AQS实现的,所以说不懂AQS实现原理的,就不能说了解Java锁。

              +
              +
              +

              并发容器类不能实现独占访问:

              +

              类似ConcurrentHashMap的并发容器不能采用客户端加锁机制,因为并发容器没有采用synchronized内置锁而大多基于AQS框架(不是独占式的锁),所以使用客户端加锁机制来扩展并发容器的方法是不能实现的。

              +

              所以说不能客户端加锁不是不提倡,而是真的不行【】

              +
              +

              所以最好还是用并发容器类替代同步容器类

              +

              对部分复合操作的支持

              + +

              客户端加锁不能使用,就只能用它提供的东西了。

              +

              CopyOnWriteArrayList

              Copy-On-Write意为“写入时复制”,仅当要修改的时候,才会重新创建一次副本,实现可变性。犹记得第一次接触到这个思想是在操作系统的fork()创建子进程的原理那个地方,那可真是有些惊为天人23333

              + + + + +

              也就是说,COWAL内部维护的base数组是事实不可变的,因而访问它的时候不需要同步。但是,我们事实上需要一个可变的并发容器,那该怎么办呢?解决方法就是每次要修改的时候,直接把base数组换成一个新的数组,就像之前某个例子一样,这样就能实现可变性了。

              +

              与此同时,这样的方法也能保证多线程访问时的内存可见性。

              +

              由COWAL的底层代码:

              +
              //base数组,volatile保证引用一变就可以刷新
              private transient volatile Object[] array;

              final Object[] getArray() {
              return array;
              }

              final void setArray(Object[] a) {
              array = a;
              }

              public boolean add(E e) {
              //获取锁
              final ReentrantLock lock = this.lock;
              lock.lock();
              try {
              //getArray:直接返回base数组的引用
              Object[] elements = getArray();
              int len = elements.length;
              //创建新数组再修改
              Object[] newElements = Arrays.copyOf(elements, len + 1);
              newElements[len] = e;
              //直接改变base数组的引用
              setArray(newElements);
              return true;
              } finally {
              lock.unlock();
              }
              }
              + +

              可知,它保证可见性,是直接修改引用的,并且注意,对原数组的拷贝是浅拷贝的。这样一来,就既不会改变原数组的东西,也能保证可见性的更新迅速了。我的评价是牛逼爆了。

              + + + + +

              阻塞队列和生产者—消费者模式

              基本介绍

              + +

              简直就是为了生产者消费者而生的

              + + + + +

              这两段话说得非常本质,需要有个缓冲队列本质上就是因为处理数据速率的不同,生产者消费者也起到了解耦作用

              + + +

              所以说用有界队列还是更好

              +

              BlockingQueue有多种实现。

              +

              ArrayBlockingQueue和LinkedBlockingQueue是FIFO队列,PriorityBlockingQueue是优先队列,最后还有一个特殊的SynhronousQueue。

              + + + + + + +

              实例:桌面搜索

              + +
              //生产者
              public class FileCrawler implements Runnable {
              private final BlockingDeque<File> fileQueue;
              private final FileFilter fileFilter;
              private final File root;

              public FileCrawler(BlockingDeque<File> fileQueue, FileFilter fileFilter, File root) {
              this.fileQueue = fileQueue;
              this.fileFilter = fileFilter;
              this.root = root;
              }

              @Override
              public void run() {
              try{
              crawl(root);
              }catch (InterruptedException e){
              //中断处理
              Thread.currentThread().interrupt();
              }
              }

              private void crawl(File root) throws InterruptedException{
              File[] entries = root.listFiles(fileFilter);
              if (entries!=null){
              for (File entry:entries){
              //递归打印目录
              if (entry.isDirectory()) crawl(entry);
              else if (!alreadyIndexed(entry)) fileQueue.put(entry);
              }
              }
              }
              }

              //消费者
              public class Indexer implements Runnable{
              private final BlockingDeque<File> queue;

              public Indexer(BlockingDeque<File> q){
              this.queue=q;
              }

              @Override
              public void run() {
              try{
              while(true){
              indexFile(queue.take());
              }
              }catch(InterruptedException e){
              Thread.currentThread().interrupt();
              }
              }
              }
              + + + + + +
              //启动生产者-消费者程序
              public static void startIndexing(File[] roots){
              BlockingDeque<File> queue = new LinkedBlockingDeque<>(BOUND);
              FileFilter filter = new FileFilter() {
              @Override
              public boolean accept(File pathname) {
              return true;
              }
              };

              for (File root : roots){
              new Thread(new FileCrawler(queue,filter,root)).start();
              }
              for (int i=0; i<N_CONSUMERS;i++){
              new Thread(new Indexer((queue))).start();
              }
              }
              + + + +

              串行线程封闭

              + +

              安全发布

              + + + + +

              6666666

              +

              之所以叫“串行”,想必是因为这个过程:发布-转接-放弃访问权是一个串行过程。

              + + + + +

              总而言之,串行线程封闭的具体做法就是,一个线程将一个安全发布的对象的所有权完全转移给另一个线程,保证之后自己不会再使用。这样一来,该对象就相当于被另一个线程封闭了。而如何保证“自己以后不再使用”呢?最简单的方法就是安全发布完这个东西后直接把这个东西给踢出去

              +

              阻塞队列是自动会把这个东西安全发布然后就踢出去的,所以说阻塞队列简化了这个工作。

              + + + + +

              双端队列与工作密取

              + + + + + +

              阻塞方法与中断方法

              阻塞方法

              + +

              当某方法抛出InterruptedException时,表明该方法为阻塞方法,也即这个方法会在执行过程中由于各种原因而被阻塞。如果这个方法被中断,它将会努力提前结束阻塞状态。

              +

              中断方法

              + + + +

              处理InterruptedException的两种选择

              + +

              传递InterruptedException

              + +

              恢复中断

              + +
              +

              此处关于为什么如果代码是Runnable的一部分就不能抛出异常:

              +

              是因为java的异常继承体系。

              +

              在重写的run方法中,我们只能够进行异常的捕获而不能够抛出异常,原因是因为在父类Runnable接口中,run方法没有抛出异常,则实现Runnable的子类就无法抛出异常

              +

              所以实际上是语法层面上不允许。

              +
              + + + + + + +

              同步工具类

              + +

              实现同步的方法:使用同步容器类/并发容器类、使用锁、使用同步工具类

              + + + + +

              闭锁 CountDownLatch

              作用

              + + + + + +

              CountDownLatch

              + +
              //使用CountDownLatch来进行计时测试
              public class TestHarness {
              public long timeTasks(int nThreads,final Runnable task)
              throws InterruptedException{
              //初始化计数器为1
              final CountDownLatch startGate = new CountDownLatch(1);
              final CountDownLatch endGate = new CountDownLatch(nThreads);

              for (int i = 0; i < nThreads; i++){
              Thread t = new Thread(){
              @Override
              public void run() {
              try {
              startGate.await();
              try {
              task.run();
              } finally {
              endGate.countDown();
              }
              } catch (InterruptedException e) {
              throw new RuntimeException(e);
              }
              }
              };
              t.start();
              }
              long start = System.nanoTime();
              //所有线程刷拉拉往下走
              startGate.countDown();
              //等待所有线程结束
              endGate.await();
              long end = System.nanoTime();
              return end-start;
              }

              }
              + + + + + +

              是的,这样测试出来的时间应该更加平均,性能更加准确。

              +

              FutureTask

              + +
              +

              java的future机制原理

              +

              关于Future:

              + + +

              其中get方法是阻塞的。

              +

              获取异步任务执行完后的结果。

              +

              关于FutureTask:

              +

              FutureTask既包含了Future的语义,又包含了Runnable的语义。

              +

              它其实内部封装了一个Runnable Task。调用FutureTask的run,其实本质上就是调用Task的run,只不过要多一些检查和存储结果之类的手续。

              +

              所以说它其实就是通过内部封装一个线程,然后就能获取这个线程运行的状态和运行的结果等等等,这样来实现Future语义的。

              +
              +
              +

              关于Callable

              +

              Runnable里面的run方法是不能传参,也没有返回值的。Callable相当于有返回值的Runnable,也即书中说的“有生成结果的Runnable”。

              +
              + + + + +
              public class Preloader {
              private final FutureTask<ProductInfo> future =
              new FutureTask<ProductInfo>(new Callable<ProductInfo>() {
              @Override
              public ProductInfo call() throws Exception {
              return loadProductInfo();
              }
              });
              private final Thread thread = new Thread(future);

              public void start(){thread.start();}

              public ProductInfo get()
              throws DataLoadException,InterruptedException{
              try {
              //阻塞
              return future.get();
              } catch (ExecutionException e) {
              Throwable cause = e.getCause();
              if (cause instanceof DataLoadException)
              throw (DataLoadException) cause;
              else
              throw launderThrowable(cause);
              }
              }

              public static RuntimeException launderThrowable(Throwable t){
              if (t instanceof RuntimeException)
              return (RuntimeException) t;
              else if (t instanceof Error) {
              throw (Error) t;
              }
              else
              throw new IllegalStateException("Not checked.",t);
              }
              }
              + + + +

              也就是调用start线程启动后,可以就去做别的事情,回来就可以拿到结果了,通过这样实现异步调用。

              + + + + +

              这里其实是在讲异常的事了。可以给我们一个启发式思路:

              +

              Callable抛出的Exception这种抽象的异常集合该如何分解处理:首先分解出受检查的异常【也就是说我们调用该方法就知道该方法可能会抛出的异常】,然后针对其他未检查异常,再进行处理。此例中是把这些未检查异常分成了Error和RuntimeException。

              +

              信号量 Semaphore

              + +
              +

              注意

              + +
              +

              这种情况下,其实信号量跟BlockingQueue语义十分近似:

              + + +

              信号量还可以用来将非阻塞容器包装为有界阻塞容器

              + + + + +
              //使用信号量为容器设置边界,有界+阻塞
              public class BoundedHashSet <T>{
              //baked collection,同步容器
              private final Set<T> set;
              //信号量
              private final Semaphore sem;

              public BoundedHashSet(int bound){
              //同步容器类
              this.set = Collections.synchronizedSet(new HashSet<>());
              //初始化许可数
              sem = new Semaphore(bound);
              }

              //阻塞方法
              public boolean add(T e) throws InterruptedException{
              //获取许可
              sem.acquire();
              boolean wasAdded = false;
              try {
              //由于是同步容器类,故而不用使用锁来保护状态
              wasAdded=set.add(e);
              return wasAdded;
              } finally {
              //try捕获异常/正常return后,finally语句都会执行。
              if (!wasAdded)
              sem.release();
              }
              }

              public boolean remove(Object o){
              boolean wasRemoved = set.remove(o);
              if (wasRemoved)
              sem.release();
              return wasRemoved;
              }
              }
              + +
              +

              注:try语句块正常return后,finally语句依然会执行:

              +
              public class Main{
              public static void main(String[] args) {
              System.out.println(haha());
              }

              public static int haha(){
              int number = 10;
              try{
              System.out.println("main!");
              return number++;
              }finally {
              System.out.println("come!"+number);
              }
              }
              }
              /*输出结果
              main!
              come!11
              10*/
              +
              +

              栅栏 Barrier

              + +

              闭锁是某个事件发生后所有线程才能继续执行;栅栏是所有线程都在同样位置等待才能继续执行。

              +

              CyclicBarrier

              + + + +

              一个线程寄了,其他所有等待线程都会死。

              +

              栅栏我觉得一个很重要的点就是保证并发安全。

              + + +

              常常出现那种需要等待所有线程都完成某一步操作才能进行下一步操作的情况,所以栅栏不得不说非常实用。

              +
              public class CellularAutomata {
              //细胞板
              private final Board mainBoard;
              //栅栏
              private final CyclicBarrier barrier;
              //计算线程
              private final Worker[] workers;

              public CellularAutomata(Board board){
              this.mainBoard = board;
              //所有可调度的CPU都被拉过来了
              int count = Runtime.getRuntime().availableProcessors();
              //当所有线程到达栅栏后,马上执行该run方法:提交计算得出的新值
              this.barrier = new CyclicBarrier(count,
              new Runnable() {
              @Override
              public void run() {
              mainBoard.commitNewValues();
              }
              });
              //创建工作线程池
              this.workers = new Worker[count];
              for (int i = 0; i < count; i++)
              //分治,把大的细胞板划分成多个小细胞板处理
              workers[i] = new Worker(mainBoard,getSubBoard(count,i));
              }

              private class Worker implements Runnable{

              private final Board board;

              public Worker(Board board){this.board = board;}

              @Override
              public void run() {
              while (!board.hasConverged()){
              for (int x = 0; x < board.getMaxX(); x++)
              for (int y = 0; y < board.getMaxY(); y++)
              //为二维细胞板上每个点计算新值
              board.setNewValue(x,y,computeValue(x,y));
              try {
              //计算完之后等待其它线程也计算完
              barrier.await();
              } catch (InterruptedException e) {
              return;
              } catch (BrokenBarrierException e) {
              return;
              }
              }
              }
              }

              public void start(){
              for (int i = 0; i < workers.length; i++)
              new Thread(workers[i]).start();
              mainBoard.waitForConvergence();
              }
              }
              + +
              +

              注:

              +

              的目的

              + +
              +

              Exchanger

              + +

              这个“写满”大概对应着栅栏思想里的“全部到达”

              + + + + +

              示例:构建高效且可伸缩的结果缓存

              + + + +
              public interface Computable <A,V>{
              V compute(A arg) throws InterruptedException;
              }

              public class ExpensiveFunction implements Computable<String, BigInteger>{
              @Override
              public BigInteger compute(String arg) throws InterruptedException {
              //经过长时间的计算后
              return new BigInteger(arg);
              }
              }
              + + + +

              用内置锁对方法进行上锁

              //感受一下cache也是Computable的这个多态运用的巧妙性
              public class Memoizer1<A,V> implements Computable<A,V> {
              private final Map<A,V> cache = new HashMap<>();
              private final Computable<A,V> c;

              public Memoizer1(Computable<A,V> c){
              this.c=c;
              }

              //对整个方法体进行上锁
              @Override
              public synchronized V compute(A arg) throws InterruptedException {
              V result = cache.get(arg);
              if (result == null){
              result = c.compute(arg);
              cache.put(arg,result);
              }
              return result;
              }
              }
              + +

              保证了线程安全,但是可伸缩性极差,而且很有可能变成普通的串行排队计算。

              + + + + +

              使用并发容器类

              public class Memoizer2 <A,V> implements Computable<A,V>{
              //并发map
              private final Map<A,V> cache = new ConcurrentHashMap<>();
              private final Computable<A,V> c;

              public Memoizer2(Computable<A,V> c){this.c = c;}
              @Override
              public V compute(A arg) throws InterruptedException {
              V result = cache.get(arg);
              if (result == null){
              result = c.compute(arg);
              cache.put(arg,result);
              }
              return result;
              }
              }
              + +

              确实加锁的粒度小了【没有把高耗时的compute过程锁上】,但会带来某个值重复计算的问题。

              + + +

              而且这不仅仅是性能损耗问题,还有可能会变成安全隐患。

              + + + + +

              使用FutureTask

              + +
              public class Memoizer3<A,V> implements Computable<A,V> {
              private final Map<A, Future<V>> cache
              = new ConcurrentHashMap<>();
              private final Computable<A,V> c;

              public Memoizer3(Computable<A, V> c) {
              this.c = c;
              }
              @Override
              public V compute(A arg) throws InterruptedException {
              Future<V> f = cache.get(arg);
              if (f == null){
              Callable<V> eval = new Callable<V>() {
              @Override
              public V call() throws Exception {
              return c.compute(arg);
              }
              };
              FutureTask<V> ft = new FutureTask<>(eval);
              f = ft;
              cache.put(arg,ft);
              //没有启动新线程,直接润
              ft.run();
              }
              try {
              return f.get();
              } catch (ExecutionException e) {
              throw new RuntimeException(e);
              }
              }
              }
              + + + +

              这个“判断是否开始”与“判断是否完成”的表述非常有意思。此时cache变成了arg和一个异步任务得到的未来结果的映射,而非arg和结果的映射。从此处也可好好理解感受一下“Future”的语义(一个异步执行的结果)。

              +

              只是它依然没有解决上面所说的问题,还是可能会有两个线程计算同一个值,虽然概率小得多,主要原因是因为它使用了非原子的“先检查后执行”

              + + +

              因而,我们可以通过map提供的putifabsent同步方法来解决这个问题。

              +

              使用FutureTask和putIfAbsent

              public class Memoizer<A,V> implements Computable<A,V> {
              private final Map<A, Future<V>> cache
              = new ConcurrentHashMap<>();
              private final Computable<A,V> c;

              public Memoizer(Computable<A, V> c) {
              this.c = c;
              }
              @Override
              public V compute(A arg) throws InterruptedException {
              Future<V> f = cache.get(arg);
              //一重保险,筛选线程,防止ft对象重复声明销毁
              if (f == null){
              Callable<V> eval = new Callable<V>() {
              @Override
              public V call() throws Exception {
              return c.compute(arg);
              }
              };
              FutureTask<V> ft = new FutureTask<>(eval);
              f = cache.putIfAbsent(arg,ft);
              //二重保险,保障仅一个线程能进入
              //只有那个成功把ft放进map的线程才能进入。
              if (f == null){
              f = ft;
              ft.run();
              }
              }
              try {
              return f.get();
              } catch (ExecutionException e) {
              throw new RuntimeException(e);
              }
              }
              }
              + +

              虽然这个最终版本看起来很完美,但实际上,它还会带来其他的性能问题。

              + + + + + + +

              运用最终方案建立cache

              public class Factorizer implements Servlet{
              private final Computable<BigInteger,BigInteger[]> c =
              new Computable<BigInteger, BigInteger[]>() {
              @Override
              public BigInteger[] compute(BigInteger arg) throws InterruptedException {
              return factor(arg);
              }
              };
              private final Computable<BigInteger,BigInteger[]> cache =
              new Memoizer<>(c);
              public void service(ServletRequest req,ServletResponse resp){
              BigInteger i = extractFromRequest(req);
              try {
              encodeIntoResponse(resp,cache.compute(i));
              } catch (InterruptedException e) {
              encodeError(resp,"factorization interrupted.");
              }
              }
              }
              + + + + + +

              第六章 任务执行

              + + + +

              在线程中执行任务

              + + + + + +

              也就是说,一个请求视为一个任务。这样做是非常reasonable的,因为每个请求间都是独立的。

              +

              串行地执行任务

              调度任务最简单粗暴的就是直接让任务串行执行。

              +
              //串行的web服务器
              public class SingleThreadWebServer {
              public static void main(String[] args) throws IOException {
              ServerSocket socket = new ServerSocket(80);
              while(true){
              Socket connection = socket.accept();
              handleRequest(connection);
              }
              }
              }
              + + + +

              这里有一个点非常棒:网络也是IO,也会造成阻塞。

              +

              为每一个任务都创建一个线程

              public class SingleThreadWebServer {
              public static void main(String[] args) throws IOException {
              ServerSocket socket = new ServerSocket(80);
              while(true){
              Socket connection = socket.accept();
              new Thread(new Runnable() {
              @Override
              public void run() {
              handleRequest(connection);
              }
              }).start();
              }
              }
              }
              + + + +

              但是其实这种方式是不好的,因为它无限制地创建线程,这听起来就很容易寄,要知道,高并发的服务器可能会一次解决几千万个请求【当然不知道有没有那么多hhh】,每个都创建一个线程的话,很容易爆内存,而且还会有很大的性能开销。

              + + + + + + +

              而且这样的话,要是高并发情况下,服务器会马上崩溃,做不到我们之前说的自我调节功能。

              +

              所以,我们应该为系统能创建的线程数做一个限制。如此,便引出了Executor框架。

              +

              Executor框架

              + +
              /*
              An object that executes submitted Runnable tasks.
              An Executor is normally used instead of explicitly creating threads.
              此接口提供了一种将任务的提交与每个任务将如何运行的机制解耦的方法,包括线程使用、调度等的详细信息。
              The Executor implementations provided in this package implement ExecutorService, which is a more extensive interface. The ThreadPoolExecutor class provides an extensible thread pool implementation. The Executors class provides convenient factory methods for these Executors.
              */
              public interface Executor {
              void execute(Runnable command);
              }
              + + + + + + + + + +

              示例:基于Executor线程池的Web服务器

              public class TaskExecutionWebServer {
              private static final int NTHREADS = 100;
              //工厂方法创建线程池
              private static final Executor exec
              = Executors.newFixedThreadPool(NTHREADS);

              public static void main(String[] args) throws IOException {
              ServerSocket socket = new ServerSocket(80);
              while (true){
              final Socket connection = socket.accept();
              //每次提交一个任务,自然会在某个时刻调度执行
              exec.execute(new Runnable() {
              @Override
              public void run() {
              handleRequest(connection);
              }
              });
              }
              }
              }
              + + + + + +

              思考问题

              Executor到底什么原理

              这里感觉绕来绕去的,execute到底是会创建一个线程,还是不会创建一个线程?到底是在调用时就创建线程执行任务,还是会在将来的某一个时刻调度执行任务?它这里说得云里雾里的,我来锐评一下我的看法。

              +

              首先,我认为,它给我们的ExecutorService类的execute应该都仅仅是提交任务,放进任务队列,之所以什么时候执行得看调度情况。【Form java ThreadPoolExecutor.execute: Executes the given task sometime in the future. 】

              +

              而下面那两个类应该都是对execute进行了简单的重写,因而此处execute跟java包里的ExecuteService没有任何关系,调用execute仅相当于调用一个普通的方法。

              +
              Executor到底有什么用
              +

              参考视频:java线程池其实不难,只要搞清楚来龙去脉

              +
              +
              解耦

              Executor作为一个接口,其核心思想便是“解耦”

              +

              如果没有Executor的话,我们要创建并运行一个任务,一般都得这样用:new Thread(....).start(),或者是比如说串行的new Runnable(...).run()。也就是我们将任务的创建和任务的执行都混在一起了。而假定说,如果以后要改变该线程池的执行方式,比如说从单任务单线程的并行改成全部任务都串行或者反之,那么就需要每个地方都改掉。但如果使用Executor框架将任务创建和任务具体执行解耦开来,那么我们就仅需修改任务具体执行了。

              +
              Java的线程管理框架

              JUC(java.util.concurrent)其实就只是分为三个部分。

              + + +
              ThreadPoolExecutor
                +
              1. ExecutorService

                +

                ThreadPoolExecutor继承了该接口。

                +

                是Executor接口的加强版,包含了更多方法,具体为:

                +

                ① 自身生命周期的管理 shutdown、isshutdown等等

                +

                ② 对异步任务的支持 返回Future的submit方法

                +

                ③ 对批处理任务的支持 invokeall

                +
              2. +
              3. 内部原理

                +

                当空闲的线程足够多,直接执行;当线程不够多,进入阻塞队列;当阻塞队列满,使用拒绝策略。

                +

                内部的线程池分为救急线程和核心线程。核心线程一直存在,当阻塞队列和核心线程都不够用,就会新开几个救急线程。

                +
              4. +
              +

              执行策略

              + + + + + +

              线程池

              + + + + + +

              说得非常全面

              + + + + +

              66666

              + + + + +

              Executor的生命周期

              + +

              我们结束executor,可以采取或温和或粗暴的方法:可以让它不接受新的,慢慢执行完全部再结束;也可以让它直接全部结束,管它有没有执行完或者有没有还没被执行,就跟断电一样。

              + + + + + + +
              +

              此处疑问:不应该先shutdown再awaitTermination吗?我百度了,也都是说先shutdown。毕竟awaitTermination方法是阻塞的。

              +
              + + +
              //支持关闭操作的Web服务器
              public class LifecycleWebServer {
              private final ExecutorService exec = Executors.newFixedThreadPool(100);

              public void start() throws IOException {
              ServerSocket socket = new ServerSocket(80);
              //服务器没被关闭就一直接受请求
              while (!exec.isShutdown()){
              try {
              final Socket conn = socket.accept();
              exec.execute(new Runnable() {
              @Override
              public void run() {
              handleRequest(conn);
              }
              });
              } catch (RejectedExecutionException e) {
              if (!exec.isShutdown())
              //异常地拒绝了
              log("task submission rejected.",e);
              }
              }
              }

              public void stop(){exec.shutdown();}

              void handleRequest(Socket connection){
              Request req = readRequest(connection);
              //是否是代表关闭的特定HTTP请求
              if (isShutdownRequest(req))
              stop();
              else
              dispatchRequest(req);
              }
              }
              + + + +

              延迟任务与周期任务

              + +

              Timer类的缺陷

              单线程带来的精确性问题
              + +
              线程泄漏
              + + + + + +
              public class OutOfTime {
              public static void main(String[] args) {
              try {
              Timer timer = new Timer();
              timer.schedule(new ThrowTask(),1);
              Thread.sleep(1000);
              timer.schedule(new ThrowTask(),1);
              Thread.sleep(5000);
              } catch (InterruptedException e) {
              throw new RuntimeException(e);
              }
              }

              static class ThrowTask extends TimerTask{

              @Override
              public void run() {
              throw new RuntimeException();
              }
              }
              }
              + + + +

              找出可利用的并行性

              + +

              所以并发编程最难的其实还是建模,如何从串行中挖掘出并行性。

              + + +

              串行的页面渲染器

              + +

              这个把文字的render和图片的render都归结进图像缓存的统一化思想很有意思。

              + + +
              //串行地渲染页面元素
              public class SingleThreadRenderer {
              void renderPage(CharSequence source){
              renderText(source);
              List<ImageData> imageData = new ArrayList<>();
              for (ImageInfo imageInfo : scanForImageInfo(source)){
              imageData.add(imageInfo.downloadImage());
              }
              for (ImageData data : imageData){
              renderImage(data);
              }
              }
              }
              + +

              显而易见,图像的IO需要耗费大量时间,这段时间内CPU都处于空闲状态,可以说利用率非常低下。

              +

              携带结果的Callable与Future

              Callable

              + + + +

              意思就是Callable比Runnable有时候更灵活,因为Callable可以抛出异常,也可以有返回值。

              +

              Future

              + + + +

              这个Future的说法很棒,只能说比起前面那个含糊的“表示一个异步执行的结果”,这个“任务的生命周期”方法更加醍醐灌顶。

              +
              public interface Future<V> {
              boolean cancel(boolean mayInterruptIfRunning);
              boolean isCancelled();
              boolean isDone();
              V get() throws InterruptedException, ExecutionException;
              V get(long timeout, TimeUnit unit)
              throws InterruptedException, ExecutionException, TimeoutException;
              }
              + +
              @FunctionalInterface
              public interface Callable<V> {
              V call() throws Exception;
              + +

              其中,get的方法取决于任务的状态

              + + + + +

              可以利用返回的Future实例来对任务线程进行管理。

              + + + + +

              Future实现并行渲染

              将要求分解为两个任务:渲染文本和渲染图像。

              + + +
              //使用Future等待图像下载
              public class FutureRenderer {
              private final ExecutorService executor = Executors.newFixedThreadPool(80);

              void renderPage(CharSequence source){
              final List<ImageInfo> imageInfos = scanForImageInfo(source);
              //单独开启下载图像的任务
              Future<List<ImageData>> future = executor.submit(new Callable<List<ImageData>>() {
              @Override
              public List<ImageData> call() throws Exception {
              List<ImageData> result = new ArrayList<>();
              for (ImageInfo imageInfo : imageInfos)
              result.add(imageInfo.downloadImage(source));
              return result;
              }
              });
              //在本线程中执行文字的渲染任务
              renderText(source);

              try {
              //阻塞方法
              List<ImageData> imageData = future.get();
              for (ImageData data : imageData)
              renderImage(data);
              } catch (InterruptedException e) {
              //重新设置线程的中断状态
              Thread.currentThread().interrupt();
              //不需要结果了,因而取消任务
              future.cancel(true);
              } catch (ExecutionException e) {
              throw launderThrowable(e.getCause());
              }
              }
              }
              + + + +

              在异构任务并行化中存在的局限

              + + + + + +

              所以难点还是分解同构任务。

              +

              CompletionServiceExecutor和BlockingQueue

              + +

              CompletionService的思想其实和这个差不多。它主要就是多包装一层,数据结构的管理不用你写,更加方便。

              + + + + + + +

              也就是说ExecutorCompletionService将CompletionService的计算部分交给了传进来的线程池Executor,然后自己管理一个阻塞队列,类似生产者-消费者模式,把线程池里出来的结果放进去。

              +

              使用CompletionService实现页面渲染器

              + +
              public class Renderer {
              //为什么这里的线程池要变成包内外界给的呢?
              private final ExecutorService executor;

              Renderer(ExecutorService executor){this.executor = executor;}

              void renderPage(CharSequence source){
              List<ImageInfo> info = scanForImageInfo(source);
              //传入委托计算的线程池
              CompletionService<ImageData> completionService
              = new ExecutorCompletionService<>(executor);
              //提交任务
              for (final ImageInfo imageInfo : info)
              completionService.submit(new Callable<ImageData>() {
              @Override
              public ImageData call() throws Exception {
              return imageInfo.downloadImage();
              }
              });

              renderText(source);

              try {
              for (int t = 0, n = info.size(); t < n; t++){
              //得到下载结果
              Future<ImageData> f = completionService.take();
              ImageData imageData = f.get();
              renderImage(imageData);
              }
              } catch (InterruptedException e) {
              Thread.currentThread().interrupt();
              } catch (ExecutionException e) {
              throw launderThrowable(e.getCause());
              }
              }
              }
              + + + +

              疑问

              我这里写了一个自己用list来保存Future结果的。不知道为什么这个不行,有待说明。

              +
              public class MyRenderer {
              private final ExecutorService executor = Executors.newFixedThreadPool(30);

              void renderPage(CharSequence source){
              List<Future> res = new ArrayList<>();
              List<ImageInfo> info = scanForImageInfo(source);
              //提交任务
              for (final ImageInfo imageInfo : info){
              res.add(executor.submit(new Callable<Object>() {
              @Override
              public Object call() throws Exception {
              return imageInfo.downloadImage();
              }
              }));
              }

              renderText(source);

              try {
              for (int t = 0, n = info.size(); t < n; t++){
              Future<ImageData> f = res.get(t);
              ImageData imageData = f.get();
              renderImage(imageData);
              }
              } catch (InterruptedException e) {
              Thread.currentThread().interrupt();
              } catch (ExecutionException e) {
              throw launderThrowable(e.getCause());
              }
              }
              }
              + + + +

              为任务设置时限

              + + + + + +
              Page renderPageWithAd() throws InterruptedException{
              long endNanos = System.nanoTime()+TIME_BUDGET;
              //提交下载广告的任务
              Future<Ad> f = exec.submit(new FetchAdTask());
              //在等待广告的同时显示页面
              Page page = renderPageBody();
              Ad ad;

              try {
              //相当于timeleft=TIME_BUDGET-经过的时间,只不过进行了运算的化简
              long timeLeft = endNanos- System.nanoTime();
              ad=f.get(timeLeft, TimeUnit.NANOSECONDS);
              } catch (ExecutionException e) {
              ad = DEFAULT_AD;
              } catch (TimeoutException e) {
              //超时取消
              ad = DEFAULT_AD;
              f.cancel(true);
              }

              page.setAd(ad);
              return page;
              }
              + + + + + +
                 /**
              * Attempts to cancel execution of this task. This attempt will
              * fail if the task has already completed, has already been cancelled,
              * or could not be cancelled for some other reason.
              If successful,
              * and this task has not started when {@code cancel} is called,
              * this task should never run.

              If the task has already started,
              * then the mayInterruptIfRunning parameter determines
              * whether the thread executing this task should be interrupted in
              * an attempt to stop the task.
              */
              /*
              也就是说,如果mayInterruptIfRunning==false,就需要等该任务完成;如果==true,就直接中断
              */
              boolean cancel(boolean mayInterruptIfRunning);
              + + + +

              示例:旅行预定门户网站

              + + + + + +

              也就是说,跟前面的CompletionService的优化目的是一致的,都是为了方便管理这一组future,这也跟我上面写的那个list管理版本是一样的。只不过区别在于,CompletionService还可以共用任务池,因而功能更强。invokeAll用法更简便。

              +
              private class QuoteTask implements Callable<TravelQuote>{
              private final TravelCompany company;
              private final TravelInfo travelInfo;
              //...

              public TravelQuote call() throws Exception{
              //solicit:征求、招揽 quote:报价
              return company.solicitQuote(travelInfo);
              }
              }
              //得到排序的报价表
              public List<TravelQuote> getRankedTravleQuotes(
              ThravelInfo travelInfo, Set<TravelCompany> companies, Comparator<TravelQuote> ranking, long time, TimeUnit unit
              )throws InterruptedException{
              List<QuoteTask> tasks = new ArrayList<>();
              for (TravelCompany company:companies){
              tasks.add(new QuoteTask(company,travelInfo));
              }

              //使用invokeAll,一键定时任务,非常方便
              List<Future<TravelQuote>> futures =
              exec.invokeAll(tasks,time,unit);

              List<TravelQuote> quotes =
              new ArrayList<>(tasks.size());
              Iterator<QuoteTask> taskIterator = tasks.iterator();

              for (Future<TravelQuote> f : futures){
              QuoteTask task = taskIterator.next();
              try {
              //只需调用get就行,不用传时间参数
              quotes.add(f.get());
              } catch (ExecutionException e) {
              quotes.add(task.getFailureQuote(e.getCause()));
              } catch (CancellationException e){
              quotes.add(task.getTimeoutQuote(e));
              }
              }

              //排序
              Collections.sort(quotes,ranking);
              return quotes;

              }
              + + + +

              小结

              + + + + + +

              第七章 取消与关闭

              + +

              中断是个重要概念,也算是老朋友了

              + + + + +

              任务取消

              取消的原因

              + + + + + +

              使用volatile标志取消

              使用方法

              Java并没有提供取消某个线程的安全抢占方法,仅有约定俗成的协作机制。

              +

              比如说,可以设置一个volatile类型的取消标志,并且让线程定期查看该标志。【这是volatile的经典用途】

              +
              public class PrimGenerator implements Runnable{
              private final List<BigInteger> prims
              = new ArrayList<>();
              //使用volatile域保护取消状态
              private volatile boolean cancelled;

              @Override
              public void run() {
              BigInteger p = BigInteger.ONE;
              //任务执行时定期检查取消状态
              while (!cancelled){
              p=p.nextProbablePrime();
              //这里用同步可能是因为下面的getPrim方法使用了同步
              synchronized (this){
              prims.add(p);
              }
              }
              }

              public void cancel(){cancelled = true;}

              public synchronized List<BigInteger> get(){
              return new ArrayList<>(prims);
              }
              }
              + +

              使用实例:

              +
              PrimGenerator generator = new PrimGenerator();
              //使用Executor代替Thread
              ExecutorService exec = Executors.newFixedThreadPool(1);
              exec.execute(generator);

              try {
              Thread.sleep(1000);
              } catch (InterruptedException e) {
              throw new RuntimeException(e);
              } finally {
              generator.cancel();
              }

              System.out.println(generator.get());
              exec.shutdown();
              + + + + + + + +

              缺陷

              + +

              比如下面程序:

              +
              public class BrokenPrimeProducer extends Thread{
              private final BlockingQueue<BigInteger> queue;
              private volatile boolean cancelled = false;

              BrokenPrimeProducer(BlockingQueue<BigInteger> queue){
              this.queue=queue;
              }

              public void run(){
              try {
              BigInteger p = BigInteger.ONE;
              while (! cancelled){
              //如果一直阻塞在这,便永远不会检查cancelled标志
              queue.put(p=p.nextProbablePrime());
              }
              } catch (InterruptedException e) {
              throw new RuntimeException(e);
              }
              }
              public void cancel(){cancelled = true;}
              }
              + + + +

              所以解决方法其实很简单,只要让阻塞状态下我们还能知道要取消任务就行。这靠我们在表层写代码是做不到的,需要用到Java提供的另一种协作机制:线程中断。

              +

              中断

              是什么

              + + + +

              所以说中断其实就是为取消而量身定做的。

              + + +
              public class Thread{
              public void interrupt();
              public boolean isInterrupted();
              public static boolean interrupted();
              }
              + + + + + +

              所以作为上层开发者,我们仅需捕获中断异常即可。

              + + +

              也就是说,打断阻塞状态下的线程会清空中断状态,打断正常状态的线程会保持中断状态。而正常状态的线程如果不对中断状态处理,就会一直保持中断状态然后继续运行,也就是屏蔽中断状态。

              + + +

              具体检查方法还是定期看标记。在看到标记后可以做善后工作再决定停不停。

              + + +

              程序清单5-10:

              + + +

              恢复中断状态的示例:

              + + +

              捕获睡眠时的中断异常,然后重新设置打断标志为true,进入下一次循环时再对标记进行处理。

              + + + + +

              程序示例

              //注意此处继承自Thread
              public class PrimeProducer extends Thread{
              private final BlockingQueue<BigInteger> queue;

              PrimeProducer(BlockingQueue<BigInteger> queue){
              this.queue = queue;
              }

              @Override
              public void run() {
              try {
              BigInteger p = BigInteger.ONE;
              //put本就可以检测中断,为啥还要外层包装一层检测的while呢?书中说是为了提高相应度。
              while(!Thread.currentThread().isInterrupted()){
              queue.put(p = p.nextProbablePrime());
              }
              } catch (InterruptedException e) {
              //此时catch完之后自动退出
              /* 允许线程退出 */
              }
              }

              public void cancel(){interrupt();}

              //这个是我为了方便调试自己加的方法
              public synchronized void get(){
              for(BigInteger i : queue){
              System.out.println(i.toString());
              }
              }
              }
              + +

              测试主函数部分如下:

              +
              PrimeProducer generator = new PrimeProducer(new ArrayBlockingQueue<>(10));
              generator.start();

              try {
              Thread.sleep(1000);
              } catch (InterruptedException e) {
              throw new RuntimeException(e);
              } finally {
              generator.cancel();
              }

              generator.get();
              + + + +

              一个有待解决的疑问

              此处编写主函数运行时,不小心产生了一个错误:Why does the ThreadpoolExecutor code never stop running?

              +
              public static void main(String[] args) {
              PrimeProducer generator = new PrimeProducer(new ArrayBlockingQueue<>(10));
              ExecutorService exec = Executors.newFixedThreadPool(1);
              exec.execute(generator);

              try {
              Thread.sleep(1000);
              } catch (InterruptedException e) {
              throw new RuntimeException(e);
              } finally {
              generator.cancel();
              }

              //generator.get();
              exec.shutdown();
              }
              + +

              这段代码跑起来的最终结果就是进程永远无法终止。至于为什么:

              +

              PrimeProducer类继承自Thread,而execute的参数是一个Runnable。也就是说,Executor会把传进来的这个Thread当成一个Runnable,然后再把它包装成一个新的Thread。所以你的generator里的cancel方法:

              +
              public void cancel(){interrupt();}
              + +

              调用的就不是本线程的中断方法,而是一个全新的毫无关系的线程的中断方法了。

              +

              所以其实应该这么写:

              +
              public static void main(String[] args) {
              PrimeProducer generator = new PrimeProducer(new ArrayBlockingQueue<>(10));
              generator.start();

              try {
              Thread.sleep(1000);
              } catch (InterruptedException e) {
              throw new RuntimeException(e);
              } finally {
              generator.cancel();
              }

              generator.get();
              }
              + +

              但我还是有个奇思妙想。可不可以沿用一开始那个错误的主方法版本,然后修改PrimeProducer类为:

              +
              //此处修改为Runnable
              public class PrimeProducer implements Runnable {
              //......

              //此处修改
              public void cancel(){Thread.currentThread().interrupt();}

              //......
              }
              + +

              结果还是跑不起来,不知道为什么,有待解答。

              +

              中断策略

              + + + + + +

              意思就是,单个的任务是非线程所有者,因为它们是被分配到线程池所有的线程执行的。所以它们不能直接对中断进行处理,需要把中断异常抛给那个目前还不知道是谁的所有者线程,让调用者决定自己该怎么做。

              + + + + + + + + + + + + +

              以下的地方一个字也看不懂,写自己的思考也没什么意义。就附上正确代码模板吧。

              +
              //通过future定时取消任务
              private static final ScheduledExecutorService taskExec =
              Executors.newScheduledThreadPool(10);

              public static void timeRun(Runnable r,
              long timeout, TimeUnit unit)
              throws InterruptedException {
              Future<?> task = taskExec.submit(r);
              try {
              task.get(timeout,unit);
              } catch (ExecutionException e) {
              throw launderThrowable(e.getCause());
              } catch (TimeoutException e) {
              //接下来任务将被取消
              } finally {
              task.cancel(true);
              }
              }
              + + + + + +]]>
              + + books + +
              + + Project1 Buffer Pool + /2023/03/13/cmu15445$lab1/ + Project1 Buffer Pool

              先放个通关记录~

              +

              image-20230330235300907

              +
              +

              特别鸣谢:

              +

              某不愿透露姓名的友人hhj

              +

              大佬的实验过程

              +

              大佬的性能优化

              +
              +
              +

              During the semester, you will build a disk-oriented storage manager for the BusTub DBMS.

              +

              注:DBMS(Database Management System),比如说Oracle数据库

              +

              The first programming project is to implement a buffer pool in your storage manager.The buffer pool is responsible for moving physical pages back and forth from main memory to disk.也就是负责物理页从磁盘的读写

              +

              It allows a DBMS to support databases that are larger than the amount of memory available to the system. 是的,这其实就跟内存换入换出差不多。我们现在是不是要在用户态实现这个功能?我记得xv6似乎是没有这个机制的。有点小期待呀。不过这部分感觉说不定和xv6的磁盘管理(使用双向链表管理buffer cache),及其的改进版本(lab:locking 使用哈希+双向链表)比较类似。

              +
              +

              注:xv6确实没有内存换入换出机制,其是固定大小的内存空间。但xv6的文件系统有采用LRU算法的buffer cache(怪不得有什么数据库型的文件系统,这两个确实有点像)。

              +
              +

              The buffer pool’s operations are transparent to other parts in the system. For example, the system asks the buffer pool for a page using its unique identifier (page_id_t) and it does not know whether that page is already in memory or whether the system has to retrieve it from disk.

              +

              Your implementation will need to be thread-safe.

              +
              +

              总结

              由于这几天时间比较零散+事情比较多,因此完成的总时间数不一定值得参考:26h(乐)

              +

              本次实验要说简单也还算简单。大概就是实现一个database与磁盘交换页的buffer pool,机制类似于内存换入换出。而实现这个buffer pool,首先得实现换入换出算法,也即task1的LRU-K算法。再然后就是在我们的LRU-K算法的基础上,实现真正的buffer pool(真正指:真正地存储以及读写磁盘、向外暴露接口),也即BufferPoolManager。最后,我们会实现类似于lock_guard这样结构的PageGuard,用于自动释放页引用和读写锁。最后的最后,我们会对实现的buffer pool进行性能优化,优化方向包括细粒度化锁以实现并行IO、针对特定应用场景调整LRU-K策略等。

              +

              这四者都是相互联系相互递进的,我认为每一个task都设计得非常不错,写完了之后对它所涉及的知识点都有了更深刻的理解。我认为其中最优美的一点就是LRU-K算法与buffer pool的解耦,这个设计让我十分地赞叹。

              +

              最后,再对我的完成情况进行一个评价。本次实验确实内容不是很难【除了性能调优部分,这个我是真不懂QAQ】,毕竟它指导书以及代码注释都给了详细的步骤参考,我之所以做了那么久一是因为我有不好的习惯,就是没认真看指导书和提示就开始按着自己的理解写,然后写完就直接开始debug开始交了;二是因为这几天学业的破事太多、竞赛也逐步开始了,因而战线拉得太长,总耗时就太多了。

              +

              因而,吸取经验,我之后coding完了之后,再照着指导书仔仔细细地过一遍自己的代码。同时,15445这个实验我也决定先暂时搁置,毕竟接下来这两个月应该会在竞赛和学业两头转,实在不能抽出很大段时间继续写了。

              +

              就酱。

              +

              Task1 LRU-K

              +

              This component is responsible for tracking page usage in the buffer pool.

              +

              The LRU-K algorithm evicts a frame whose backward k-distance is maximum of all frames in the replacer. LRU-K 算法驱逐一个帧,其backward k-distance是替换器中所有帧的最大值。

              +

              Backward k-distance is computed as the difference in time between current timestamp and the timestamp of kth previous access. backward k-distance=现在的时间戳 - 之前第k次访问时的时间戳

              +

              A frame with fewer than k historical accesses is given +inf as its backward k-distance. 不足k次访问的帧的backward k-distance应该设置为inf(对应上图左边那个访问记录队列吧)

              +

              **When multiple frames have +inf backward k-distance, the replacer evicts the frame with the earliest overall timestamp (i.e., the frame whose least-recent recorded access is the overall least recent access, overall, out of all frames).**如果有多个inf的结点,按照LRU规则淘汰(也即上图左边那个历史记录队列采取LRU规则)

              +

              The maximum size for the LRUKReplacer is the same as the size of the buffer pool since it contains placeholders for all of the frames in the BufferPoolManager. However, at any given moment, not all the frames in the replacer are considered to be evictable. The size of LRUKReplacer is represented by the number of evictable frames. The LRUKReplacer is initialized to have no frames in it. Then, only when a frame is marked as evictable, replacer’s size will increase. size为可驱逐的frame数而非所有frame数。

              +
              +

              正确思路

              本次实验要我们实现的是一个LRU-K算法的页面置换器。

              +

              LRU-K算法是对LRU算法和LFU算法的折中优化,平衡了LFU和LRU的性能和开销的同时,也解决了缓存污染问题,其核心思想是将“最近使用过1次”的判断标准扩展为“最近使用过K次”。具体来说,它维护了一个backward k-distance,其计算方法:

              +
                +
              1. 如果已经被访问过k次: backward k-distance = current_timestamp_ - 倒数第k次访问的时间戳
              2. +
              3. 如果还没被访问过k次: backward k-distance = +inf
              4. +
              +

              页面驱逐规则:

              +
                +
              1. 驱逐 backward k-distance 最大的页。

                +

                也即情况2总是优先会比情况1被驱逐;每次优先驱逐previous k次访问最早的页面。

                +
              2. +
              3. 当有多个页值为+inf,则采取FIFO规则进行驱逐。

                +
              4. +
              +

              故而,在具体实现中,为了便于管理,我将此拆分为两个队列:

              +
              +

              思路来自:LRU . LFU 和 LRU-K 的解释与区别

              +

              image-20230323205851168

              +
                +
              1. 数据第一次被访问,加入到访问历史列表;

                +
              2. +
              3. 如果数据在访问历史列表里后没有达到K次访问,则按照一定规则(FIFO,LRU)淘汰;

                +
              4. +
              5. 当访问历史队列中的数据访问次数达到K次后,将数据索引从历史队列删除,将数据移到缓存队列中,并缓存此数据,缓存队列重新按照时间排序;

                +
              6. +
              7. 缓存数据队列中被再次访问后,重新排序;

                +
              8. +
              9. 需要淘汰数据时,淘汰缓存队列中排在末尾的数据,即:淘汰“倒数第K次访问离现在最久”的数据。

                +
              10. +
              +
              +

              每个页面结构持有一个时间戳队列即可:

              +

              image-20231128152139485

              +

              感想

              刚coding完

              task1的内容就是实现对一堆frame_id的LRU-K算法管理,挺简单的(也可能是测试用例少我错误没排查出来2333)

              +

              我并没有用默认给的模板的unorder_map,也没有用默认给的模板思路(但原理以及最终效果是差不多的,就是没用它的方法),而是选择类似像上面这张图一样,分成两个队列实现,一个队列visit_record_存储那些访问次数<k的数据,另一个队列cache_data_存储那些访问次数>=k的顺序,每次优先淘汰visit_record_中的数据,两个队列都采用LRU的方式管理。与此同时,我觉得LRU管理时间戳只用记录最新访问的就行,所以将历史访问时间戳队列改成了只有一个变量。

              +

              终于通过online-test

              +

              参考:

              +

              FIFO和LRU这里面的实例非常直观地说明了两种算法的差异,可以跟着手推感受一下

              +

              pro1这个用的是我上面的那个想法,是错的。但是评论很值得参考:

              +

              image-20230329230045194

              +

              pro1这个评论的“偷测试用例”xswl,虽然这次没用,但以后说不定能用上:

              +

              image-20230329230139300

              +
              +
              正确思路

              ……简单个屁!!

              +

              算法上,上面错误的算法确实很简单;而正确的算法也确实很简单。那么难的是什么呢?我觉得难的还是搞清楚它要我们实现的究竟是上面东西。

              +

              结合指导书这段话:

              +
              +

              The LRU-K algorithm evicts a frame whose backward k-distance is maximum of all frames in the replacer. 每次驱逐 backward k-distance最大的

              +

              那么 backward k-distance是什么?

              +

              Backward k-distance is computed as the difference in time between current timestamp and the timestamp of kth previous access. backward k-distance = current_timestamp_ - 倒数第k次访问的时间戳

              +

              A frame with fewer than k historical accesses is given +inf as its backward k-distance. 没有达到k次访问的, backward k-distance为+inf。也就是说,每次优先从历史访问队列清除元素。

              +

              【**When multiple frames have +inf backward k-distance, the replacer evicts the frame with the earliest overall timestamp (i.e., the frame whose least-recent recorded access is the overall least recent access, overall, out of all frames).**】当历史访问队列有多个元素,就驱逐英文描述那样的frame。

              +
              +

              我们可以发现,它这个对于两个队列的LRU,并非我们原来算法那样,对于每个frame,针对其最新的访问时间戳,也即history_.back(),进行LRU淘汰;而是,针对其倒数第k新的访问记录,也即history_.front() && history_.size()<=k,进行LRU淘汰。

              +

              其中,由于历史访问队列的记录少于k个,因而其事实上从k-distance算法退化为了FIFO算法。【感受一下这一点的优美:FIFO实际上是k-distance的特例】

              +

              我们上面的算法比较的是history_.back(),所以可以省略时间戳队列为一个变量,然后将两个队列使用FILO的形式组织起来。正确算法就不能这么简单了,要按front排序的话,实现开销可能更大,所以下面就采用了map形式来实现logn的查找。

              +
              关于LRU的翻译

              这里一个点我其实还是很疑惑的,完全想不通。

              +

              就是,对缓存队列实现k-distance算法没毛病,这段话已经写得很清楚了。

              +
              +

              Backward k-distance is computed as the difference in time between current timestamp and the timestamp of kth previous access. backward k-distance = current_timestamp_ - 倒数第k次访问的时间戳

              +
              +

              但是,为什么历史访问队列要用FIFO呢?是我英语不好吗,这段话不是实现纯正LRU的意思吗:

              +
              +

              【**When multiple frames have +inf backward k-distance, the replacer evicts the frame with the earliest overall timestamp (i.e., the frame whose least-recent recorded access is the overall least recent access, overall, out of all frames).**】当历史访问队列有多个元素,就驱逐英文描述那样的frame。

              +
              +

              我翻译一下:

              +

              当多个frame有+inf这个 backward k-distance的时候,replacer需要驱逐拥有全部(overall)frame中最早的timestamp的frame。(也就是说,frame,它的最近访问记录是所有frame里面最早的)

              +

              这样确实看起来就是要用LRU。

              +

              但其实,是我英语不好。咨询了场外热心人士hhj之后,我才修订出了如下版本:

              +

              当多个frame有+inf这个 backward k-distance的时候,replacer需要驱逐拥有全部(overall)frame中最早的timestamp的frame。(也就是说选择一个frame,这个frame的最不近的访问记录,是所有frame中最近最少访问的)【也即这个frame的history的front是所有frame中最早的,也即使用FIFO算法】

              +

              可见,正确解法确实是没问题的,就是理解上很困难。要是可以配个实例就好了QAQ

              +

              所以说,所谓LRU(Least Recently Used)的直译还是最不近使用,也即最近最少使用。里面这个least不是用来修饰recent表示recent程度深的,相反它表示的是recent的程度浅。英语不好的惨痛教训啊。

              +

              image-20230330160207757

              +

              最后一下子交了这么多次才过。绷不住了。

              +

              Task2 BufferPoolManager

              +

              The BufferPoolManager is responsible for fetching database pages from the DiskManager and storing them in memory.从DiskManager中取出页,然后存入内存。

              +

              也就是说,我们的Buffer Pool是磁盘到内存的映射,我们在Task1实现了内存部分的管理数据结构?

              +

              The BufferPoolManager can also write dirty pages out to disk when it is either explicitly instructed to do so or when it needs to evict a page to make space for a new page.也要负责dirty页的写回

              +

              You will also not need to implement the code that actually reads and writes data to disk (this is called the DiskManager in our implementation). We will provide that functionality. DiskManager已给出

              +

              All in-memory pages in the system are represented by Page objects. Each Page object contains a block of memory that the DiskManager will use as a location to copy the contents of a physical page that it reads from disk. Page 是可复用的内存页容器

              +

              The Page object’s identifer (page_id) keeps track of what physical page it contains; if a Page object does not contain a physical page, then its page_id must be set to INVALID_PAGE_ID.

              +

              也就是说,page_id表示的是实际的物理页号;frame_id表示的是你的Page容器的序号,同时也是LRU的对象。你需要一个类似<fid, pid>这样的map来记录这二者的映射。具体是通过:

              +
              /** Array of buffer pool pages. */
              Page *pages_; // <fid, pid>
              /** Page table for keeping track of buffer pool pages. */
              std::unordered_map<page_id_t, frame_id_t> page_table_; // <pid, fid>
              + +

              Each Page object also maintains a counter for the number of threads that have “pinned” that page. Your BufferPoolManager is not allowed to free a Page that is pinned. 有引用计数机制

              +

              Each Page object also keeps track of whether it is dirty or not. It is your job to record whether a page was modified before it is unpinned. Your BufferPoolManager must write the contents of a dirty Page back to disk before that object can be reused.需要track dirty,并且这是你要干的;要写回,这也是你要干的

              +

              Your BufferPoolManager implementation will use the LRUKReplacer class that you created in the previous steps of this assignment. The LRUKReplacer will keep track of when Page objects are accessed so that it can decide which one to evict when it must free a frame to make room for copying a new physical page from disk. When mapping page_id to frame_id in the BufferPoolManager, again be warned that STL containers are not thread-safe.

              +
              +

              感想

              刚coding完

              task2说得比较复杂,实现的函数较多,实际coding细节也比较繁琐,但debug倒是很轻松。

              +

              主要内容就是实现BufferPoolManager,在task1实现的LRU-K算法的基础上,写具体的内存换入换出的接口逻辑。

              +

              再次回顾我们整个project1的目的:实现一个从磁盘到内存的buffer。task1只是实现了一个内存页换入换出的LRU-K算法部分,task2则基于算法部分,实现了具体与上层交互的像样的逻辑。

              +

              我认为这其中一个亮点就是,它非常完美地将LRU-K算法和具体的上层逻辑进行了解耦。LRU-K只需关注如何将这一堆freme_id组织起来组织好,而无需关心具体内存页存放在哪,以及对应frame淘汰之后内存页又何去何从,因为这些逻辑都会由上层实现;而上层逻辑也无需关心具体的淘汰页算法【LRU-K/LRU/LFU,只需替换replacer_就可以替换换入换出策略】,而只需打好evictable标记,并且在调用evict方法之后做好后处理(如内存释放等等等)即可。

              +

              这其中有一个小细节也值得借鉴,即从page_id_frame_id_的转化。frame_id_有界,比较方便LRU-K算法实现,并且进行了LRU-K算法的容量控制,同时由于算法和上层逻辑的容量相同,故而也是pages_的索引号;而page_id_不能有界,因为实际上访问到的物理页不可能只共享pool_size_个序列号。故而在这样解耦实现的基础上,二者缺一不可。

              +

              还有frame_id_的复用,它是采用了类似我们日常生活中取号那样,要用号时从队列头取,不用号时塞回队列尾就行,这种方式我觉得还挺有意思。

              +

              其他部分虽然步骤繁杂,但理解难度不高,而且它提示得也很保姆了,所以不多bb。

              +

              通过online-test

              确实算简单了,我主要倒在没有认真看它的需求,这应该是语文问题(绷

              +

              一个是FetchPage这里:

              +

              image-20230330162812680

              +

              如果所求物理页存在于buffer pool,直接返回+record access即可,不用再写回+读入。因为它的提示这边:

              +

              image-20230330162941774

              +

              这个是句号。也就是说后面那些写回啊read啊,是没找到时才做的,不是并列关系。

              +

              这也很合理,毕竟你找到所需页就说明不用从磁盘读入,也即找到所需页=直接返回即可。

              +

              另一个是UnpinPage这里:

              +

              image-20230330163031739

              +

              不应该写is_dirty_ = is_dirty,因为它的提示这边:

              +

              image-20230330163058921

              +

              可见参数is_dirty为true是需要设置为dirty,为false的话没有别的意义,保持原来值就行。

              +

              还有一个就是,在Page类中声明了friend:

              +

              image-20230330163337929

              +

              故而BufferPoolManager可以直接访问Page的私有成员变量,而无需手动为Page添加Getter/Setter方法。

              +

              Task3 Page Guard

              这是要写我们在上面用的那个PageGuard?这让我想起了Lab0的ValueGuard

              +
              template <class T>
              class ValueGuard {
              public:
              ValueGuard(Trie root, const T &value) : root_(std::move(root)), value_(value) {}
              auto operator*() const -> const T & { return value_; }

              private:
              Trie root_;
              const T &value_;
              };
              + +

              不过其实这两个是不一样的。本次要实现的Page Guard的语义更类似lock_guard

              +
              +

              我们需要手动调用UnpinPage,但这中就跟new/delete、malloc/free一样都要靠人脑来记住,不大安全。

              +

              You will implement BasicPageGuard which store the pointers to BufferPoolManager and Page objects. A page guard ensures that UnpinPage is called on the corresponding Page object as soon as it goes out of scope. 【也许这需要在析构函数中实现?】Note that it should still expose a method for a programmer to manually unpin the page.仍然需要提供UnPin方法。

              +

              As BasicPageGuard hides the underlying Page pointer, it can also provide read-only/write data APIs that provide compile-time checks to ensure that the is_dirty flag is set correctly for each use case.这个思想很值得学习。

              +

              In the future projects, multiple threads will be reading and writing from the same pages, thus reader-writer latches are required to ensure the correctness of the data. Note that in the Page class, there are relevant latching methods for this purpose. Similar to unpinning of a page, a programmer can forget to unlatch a page after use. To mitigate the problem, you will implement ReadPageGuard and WritePageGuard which automatically unlatch the pages as soon as they go out of scope.

              +
              +

              感想

              怎么说,其实只用仔细看相关文档和它的要求就不难,但你懂的我的尿性就是不细看文档,所以这里我也用gdb调了蛮久才过的。正确思路没什么好说的,直接记录下我觉得比较有意义的错误吧。

              +

              错误集锦

              析构函数的调用

              image-20230330233252372

              +

              在这个用例中,退出“}”会调用两次析构函数。

              +
              奇怪的死锁
              debug过程

              我在coding的过程中,遇到了一个很神奇的死锁现象。

              +

              在这里page->WLatch();这句会死锁,而且还是在第一次调用FetchWritePage()时死锁的:

              +
              WritePageGuard(BufferPoolManager *bpm, Page *page) : guard_(bpm, page) {
              page->WLatch();
              }
              + +

              但是添加了一句page->WUnlatch();

              +
              WritePageGuard(BufferPoolManager *bpm, Page *page) : guard_(bpm, page) {
              page->WUnlatch();
              page->WLatch();
              }
              + +

              它就不会死锁了。

              +

              这很奇怪,到底是发生了什么?我用GDB调了半天,在RWLatch.WLock()处打了断点,也没发现在这之前有调用过lock()。于是我就去看了下std::shared_mutex的官方文档(当然,这中间想了很久也不知道怎么办):

              +

              image-20230331222601644

              +

              我就怀疑是不是我哪里写错了,所以就干了这种undefined的事,然后就导致死锁了。于是我写了个测试程序:

              +

              image-20230330195418370

              +

              发现,当在调用WLock(也即std::shared_mutex::lock())之前,如果多调了一次XUnlock(也即std::shared_mutex::unlock()或者std::shared_mutex::unlock_shared()),就会卡住。

              +

              这说明确实发生了不匹配问题。于是我就在Page中添加了两个成员变量用来记录上锁和解锁的次数,并且在gurad test中打印了出来,结果发现:

              +

              image-20230330233018049

              +

              确实发生了不匹配问题,是在这里:

              +

              image-20230330233252372

              +

              之后用gdb调下就发现错误了,不赘述了。

              +
              另外的想法

              在出现死锁问题时,我是想着,会不会是测试程序中,对同一页获取了一次ReadGuardPage对象之后,再对同一页获取Read/WriteGuardPage导致的呢?于是我就开始思考如何防范这个流程,最后写下了这样的代码:

              +
              auto BufferPoolManager::FetchPageRead(page_id_t page_id) -> ReadPageGuard {
              Page *page = FetchPage(page_id);
              bool should_release = true;
              if (!page->rwlatch_.try_lock_shared()) {
              // 说明此时已有read/write锁
              should_release = false;
              }
              return {this, pagei, should_release};
              }

              auto BufferPoolManager::FetchPageWrite(page_id_t page_id) -> WritePageGuard {
              Page *page = FetchPage(page_id);
              bool should_release = true;
              if (!page->rwlatch_.try_lock()) {
              // 获取write锁失败,可能原因:该进程持有write锁、别的进程有read锁、该进程持有read锁
              if (page->rwlatch_.try_lock_shared()) {
              // 成功read,说明是别的进程有read锁
              page->rwlatch_.unlock_shared();
              // 等待
              page->rwlatch_.lock();
              } else {
              // 说明当前进程有read/write锁
              should_release = false;
              }
              }
              return {this, pagei, should_release};
              }
              + +

              但很遗憾的是,我发现是无法区分当前进程持有write还是read锁的。也许有别的办法但我没想起来。

              +

              总之,我认为这段代码还是很有参考价值的,姑且放着先。

              +

              Task4 性能调优

              +

              参考:

              +

              CMU 15-445 2023 P1 优化攻略 [rank#3] 写得非常细致,思路很清晰

              +

              CMU 15-445 Project 1 (Spring 2023) 优化记录

              +
              +
              +

              我的实现有一些并发小问题,详见lab2的并发部分~

              +
              +

              lru-k的算法优化是自己想的,并行IO的优化思路全部来自 CMU 15-445 Project 1 (Spring 2023) 优化记录,我只是把这位大佬的思路自己实现了一遍。感觉还是太菜了,面对这种实际场景毫无还手之力一点思路没有QAQ但正是如此,这个细粒度化锁的小task才值得学习。

              +

              放上优化前后性能对比:

              +

              image-20230331000020775

              +

              image-20230404140838247

              +

              Better replacer algorithm

              +

              In the leaderboard test, we will have multiple threads accessing the pages on the disk. There are two types of threads running in the benchmark:在具体的benchtest中,可以分为两类线程。

              +
                +
              1. Scan threads. Each scan thread will update all pages on the disk sequentially. There will be 8 scan threads.
              2. +
              3. Get threads. Each get thread will randomly select a page for access using the zipfian distribution. There will be 8 get threads.
              4. +
              +

              Given that get workload is skewed(有偏向性的)(i.e., some pages are more frequently accessed than others), you can design your LRU-k replacer to take page access type into consideration, so as to reduce page miss.

              +
              +

              解决方法

              我们可以回想起当初选择LRU-K而不选择LRU算法的原因:缓存污染。

              +
              +

              LRU 一种缓存淘汰算法

              +

              缓存污染:

              +

              LRU因为只需要一次访问就能成为最新鲜的数据,当出现很多偶发数据时,这些偶发的数据也会被当作最新鲜的,从而成为缓存。但其实这些偶发数据以后并不会是被经常访问的。

              +
              +

              而在这里也是同理。我们的benchtest中,scan线程是顺序地访问磁盘上所有页,而get线程是遵从zip分布地访问,显然get线程的access记录比scan线程的有价值的多,并且scan线程的数据是很容易污染get线程的。

              +

              所以,我的解决方法是,如果某个页被第一次访问,且该访问方式为SCAN,则RecordAccess进入历史访问队列;如果某个页不是被第一次访问,且访问方式为SCAN,则不做任何处理。不用修改UnpinPage的处理方式。

              +

              Parallel I/O operations

              +

              Instead of holding a global lock when accessing the disk manager【不要在访问disk_manager_的时候使用bpm的全局锁latch_】, you can issue multiple requests to the disk manager at the same time. This optimization will be very useful in modern storage devices, where concurrent access to the disk can make better use of the disk bandwidth.

              +
              +

              解决方法

              详细的解决方法大佬这边已经说得很清楚了,接下来我就对其总体的做法进行一点总结,加上一些个人理解。

              +

              我刚看到这个需求的时候是这么做的:

              +
              if (pages_[fid].IsDirty()) {
              latch_.unlock();
              disk_manager_->WritePage(pages_[fid].GetPageId(), pages_[fid].GetData());
              latch_.lock();
              }
              + +

              也即在原来代码的基础上做简单的改动,每次执行到涉及磁盘读写的地方,就暂时地开一下锁。但其实这样是不行的,当多个线程访问bpm,线程A在这里开锁执行Write,线程B正好得到锁,然后对pages_[fid]执行比如说ResetMemory操作,这样就寄了。

              +

              所以,在磁盘读写的时候,我们仍然需要使用锁保护,只不过我们需要选择粒度更细的锁。这时我们就可以想到在page_guard里常用的page自带的锁。在这里用page锁,既能够锁保护,又符合语义,看起来非常完美:

              +
              pages_[fid].WLatch();
              latch_.unlock();
              if (pages_[fid].IsDirty()) {
              disk_manager_->WritePage(pages_[fid].GetPageId(), pages_[fid].GetData());
              }
              latch_.lock();
              pages_[fid].WUnlatch();
              + +

              但由于我们在returnpage_guard的时候会获取锁,因而在这样的情况下,会发生死锁:

              +
              auto reader_guard_1 = bpm->FetchPageRead(page_id_temp);
              auto reader_guard_2 = bpm->FetchPageRead(page_id_temp);
              + +
              +

              在这里我们首先获取reader_guard_1 ,持有了该 page 的读锁,并允许其他线程读;但在获取reader_guard_2时,FetchPage会在释放 bpm 写锁前,请求该 page 的写锁;但由于reader_guard_1已经申请了该 page 的读锁,就会造成死锁,与预期结果不符。

              +
              +

              因而,我们就可以选择在bpm内部,单独为pages_数组的每一页都维护一个锁,在每个对page页属性进行读写的地方进行锁定:

              +
              std::shared_mutex latch_;
              std::vector<std::mutex> pages_latch_;
              + +

              然后对代码进行重排序,尽量分离bpm内部成员和page内部成员属性的修改:(以FetchPage为例)

              +
              auto BufferPoolManager::FetchPage(page_id_t page_id, [[maybe_unused]] AccessType access_type) -> Page * {
              ...
              if (free_list_.empty()) {
              frame_id_t fid;
              if (!replacer_->Evict(&fid)) { ... }
              // 这些地方不涉及对page的读写,只涉及对bpm内部成员的读写
              page_table_.erase(page_table_.find(pages_[fid].GetPageId()));
              page_table_.insert(std::make_pair(page_id, fid));

              replacer_->RecordAccess(fid, access_type);
              replacer_->SetEvictable(fid, false);

              // 两个锁的交接点
              pages_latch_[fid].lock();
              latch_.unlock();

              if (pages_[fid].IsDirty()) {
              disk_manager_->WritePage(pages_[fid].GetPageId(), pages_[fid].GetData());
              }

              Page *res = &(pages_[fid]);
              res->page_id_ = page_id;
              res->ResetMemory();
              disk_manager_->ReadPage(page_id, res->GetData());
              res->is_dirty_ = false;

              res->pin_count_ = 1;

              pages_latch_[fid].unlock();
              return res;
              }
              // 这些地方不涉及对page的读写,只涉及对bpm内部成员的读写
              frame_id_t fid = free_list_.front();
              free_list_.pop_front();
              page_table_.insert(std::make_pair(page_id, fid));

              replacer_->RecordAccess(fid, access_type);
              replacer_->SetEvictable(fid, false);

              // 两个锁的交接点
              pages_latch_[fid].lock();
              latch_.unlock();

              Page *res = &(pages_[fid]);
              res->page_id_ = page_id;
              res->ResetMemory();
              disk_manager_->ReadPage(page_id, res->GetData());
              res->is_dirty_ = false;

              res->pin_count_ = 1;
              pages_latch_[fid].unlock();

              return res;
              }
              + +

              其他地方也是一样。就不多赘述了。

              +
              一个小地方

              当外界需要对页进行读写时,需要使用page自带的锁;而当bpm内部需要对页进行读写时,则使用的是bpm内部自带的页锁。

              +

              这句话说完,相信危险性已经显而易见了:我们使用了两把不同的锁维护了同一个变量!而且可能会有两个线程分别持有这两个锁,对这个变量并发更新!

              +

              但其实,在当前这个场景,这么做是没问题的。

              +

              外界实质上只能对page的data字段进行读写。因而,有上述危险的,实质上就只有bpm中会对data字段进行改变的地方,也即bpm::NewPage()bpm::FetchPage()bpm::DeletePage()这三个地方。

              +

              而在前两个地方,我们会使用到的page都是闲置/已经被释放的页,因而外界不可能,也即不可能有别的线程,会持有page的锁并且对其修改;同样的,在第三个地方,我们会使用的page也是pincount==0的页,仅有当前线程在对其进行读写。

              +

              因而,综上所述,这样做是并发安全的。

              +]]>
              +
              + + Project0 C++ Primer + /2023/03/13/cmu15445$lab0/ + Project0 C++ Primer

              相比于fall2022(Trie),spring2023(COW-Trie)的难度更大,算法更复杂【毕竟是要实现一个cow的数据结构】,我认为两个都很有意义,故而两个都做了。

              +

              其中在Trie中,由于我是第一次接触cpp,所以遇到了很多麻烦。好在经过18h+的cpp拷打后,cow-trie虽然难度大,语法也更复杂一些,但我还是很快(话虽如此也花了7、8小时23333)就完美pass了。不过效率可能还是不大高,毕竟我不熟悉cpp,很多地方可能都直接拷贝了emm希望后续学习可以加把劲。

              +

              Trie

              +

              In this project, you will implement a key-value store backed by a concurrent trie. 实现并发安全的trie

              +

              To simplify the explaination, we will assume tha the keys are all non-empty variable-length strings but in practice they can be any arbitrary type. key为非空变长字符串

              +

              The key-value store you will implement can store string keys mapped to values of any type. key必须是字符串类型,但value可以是任意类型

              +

              The value of a key is stored in the node representing the last character of that key.

              +

              image-20230312150245669

              +
              +

              心得

              感想

              本次实验完成时间总计18h+。是的,lab0就做了这么久【难绷】

              +

              其实光就实验内容来看,无非就是实现trie树,算法上没有很难,最难的应该是Remove函数的编写,因为它是个递归。

              +

              但正如本次实验的主题C++ Primer所揭示的那样,本次实验的真正难点在于C++……而在接触本实验之前,我对c++一无所知

              +

              除了这个萌新debuf之外,我还不小心犯了另一件非常sb的乌龙,加上对cpp实在是太小白了,再加上这几天破事又贼多,更是让我心态大崩,差点一蹶不振不想写了(。

              +

              因而,整个实验在我看来十分痛苦。coding阶段,就是 语法错误-看了半天报错信息才发现哪错了-改错误-改得不对-再改-再改-再改……这样的痛苦过程在循环往复;运行阶段,就是看着stack trace发呆、用gdb调来调去还不知道为什么错了这样的痛苦过程在循环往复。好在,我还是坚持下来了,虽然内心还是很浮躁很浮躁(

              +

              不过总而言之,我认为这次实验给我收获挺大的。它帮助我熟悉了C++,但我认为更重要的,是它帮我矫正了心态。做这个实验之前,我内心是很浮躁的(那会破事太多了),而且因为它是lab0所以有点轻敌(对不起。。),因而我所采取的策略是“错误驱动”,也即哪里报错就百度下怎么改就行。这样的心态就导致我的debug过程极度痛苦,因为完全看不懂报错信息,压根不知道错在哪里,百度也百度不出来。于是我被迫修改了战略,去看了我一直不想看的书,学了我一直很害怕的cpp,用了我一直很抗拒的gdb调试,才发现其实都没有我想象的这么恐怖。这期间、这几天的种种心路历程,我认为是十分可贵的。

              +

              错误集锦

              sb错误

              我下载下来starter code的时候,发现找不到它要我们实现的p0_trie.h,只有这几个:

              +

              image-20230318154940622

              +

              我便觉得可能是实验代码改版了。但是我并没有多想,我觉得可能只是代码模板改版了但实验内容不变QAQ【为什么会这么觉得呢?因为我看到指导书的url为fall2022便以为这是最新版指导书,没有想到春季学期也可以开课,还有个spring2023呃呃】而且代码看起来也确实是要我们实现Trie树【虽然跟指导书说得不大一样】。故而,我就这么直接开干了。

              +

              写完了Tire树的逻辑【这部分确实挺简单的】之后,我就开始了漫长的痛苦且折磨的原地兜圈之旅。由于真正的spring2023的代码模板是实现COW-Trie,故而代码模板中很多地方都使用了const关键字,包括树结点以及树的children_成员。

              +
              // in class Trie
              std::shared_ptr<const TrieNode> root_{nullptr};
              template <class T> auto Get(std::string_view key) const -> const T *;
              template <class T> auto Put(std::string_view key, T value) const -> Trie;
              auto Remove(std::string_view key) const -> Trie;
              // in class TrieNode
              std::map<char, std::shared_ptr<const TrieNode>> children_;
              + +

              如上是spring2023的代码模板。

              +

              如果使用其给我们提供的COW-Tire接口来实现Trie树,就会产生巨大的矛盾。你无法在root_的孩子中插入或者删除一个树节点,因为root_指向一个const对象,其children_域也是const的。同样的,你也无法对root_的孩子的孩子集合做增删结点操作,因为它也是const的。

              +

              由于对C++不熟悉,通过满屏幕的报错从而搞清楚上面那些东西这件事,就花费了我很多很多时间。

              +
              error: no matching function for call to 
              ‘std::map<char, std::shared_ptr<const bustub::TrieNode> >
              ::insert(std::pair<char, std::shared_ptr<const bustub::TrieNode> >) const
              + +

              比如说这个错误我就看了半天完全不知道啥意思(

              +

              好在明白上面这点后,我很快就发现了spring2023的存在,然后切到了fall2022的正确分支【乐】

              +

              经过了此乌龙后,我深刻地意识到了我对C++一窍不通的程度(,比如说上面的这些const,还有比如说&是什么东西&&又是什么东西,shared_ptr又是什么东西等等等,我都不懂。故而,我压制了内心的浮躁,去简单看了一下书,了解了new的作用、左值引用右值引用、move、智能指针这几个地方,然后再去重新开始写本实验,最终果然好了不少。

              +
              错误使用unique_ptr::get

              Trie::GetValue中,我本来是这么写的:

              +
              	std::unique_ptr<TrieNode>* t = &root_;
              // ...
              std::unique_ptr<TrieNodeWithValue<T>> tmp(dynamic_cast<TrieNodeWithValue<T> *>(t->get()));
              // ...
              }
              + +

              这就会导致,tmp和(*t)会指向同一块内存区域,并且它们都是unique_ptr。随后,代码块遇到}结束,tmp的析构函数被调用,那块内存区域被free,但(*t)依然指向那块内存区域,随后在释放整个Trie树时这块区域就会被再次释放,然后寄(

              +
              共享unique_ptr

              有一个方法可以在不剥夺某个unique_ptr的所有权的同时,又能用另一个变量来操作该指针所指向的对象。这个方法就是——使用指向unique_ptr的指针(。

              +

              也即比如:std::unique_ptr<TrieNode> *

              +
              代码规范

              本次实验还格外要求了代码规范问题。

              +
              $ make format
              $ make check-lint
              $ make check-clang-tidy-p0
              + +
              自测

              我暂时没进行gradescope的自测,原因是它上面报了个跟我没啥关系的错,我不知道怎么改呃呃。

              +

              image-20230318165521340

              +
              In file included from /autograder/bustub/src/common/bustub_instance.cpp:17:
              /autograder/bustub/src/include/common/bustub_instance.h:30:10: fatal error: 'libfort/lib/fort.hpp' file not found
              #include "libfort/lib/fort.hpp"
              + +

              都指向说找不到这个fort。但我真的不知道它为啥找不到,因为我看CMakeLists.txt中已经加了third_party/这个include目录了,并且这个东西的路径也确实是third_party/libfort/lib/for.hpp

              +

              我还在CMackLists.txtsrc/CMackLists.txttools/shell/CMackLists.txt里面都加了include(${PROJECT_SOURCE_DIR}/third_party/libfort/lib/fort.hpp),但是依然报了这样的错:

              +

              image-20230318173941576

              +

              image-20230318174102171

              +

              它这为啥找不到我是真的很不理解。

              +

              所以真的很奇怪。暂且先放着吧,之后有精力研究下这些编译链接过程。

              +

              COW-Trie

              +

              CMU 15445 Project 0 (Spring 2023) 学习记录 参考了task2和一个bug

              +
              +

              先放个通关截图~

              +

              image-20230322235159843

              +

              总体用时(coding+debug+note)10h+

              +

              本次实验是在它给的接口的基础上,实现一株并发安全的cow的trie树,还有一个小小的实现upperlower函数的实验用来熟悉我们之后要写的db的东西。算法难度还是有一些的,我的coding和debug时间估摸着可能有46开。

              +

              总体来说整个实验还是非常有价值的,相比往年难度和意义都更上了一层。感谢实验设计者让我做到设计得这么好的实验~

              +

              Task1 cow-trie

              +

              In this task, you will need to modify trie.h and trie.cpp to implement a copy-on-write trie.

              +

              下面举例说明

              +

              Consider inserting ("ad", 2) in the above example. We create a new Node2 by reusing two of the child nodes from the original tree, and creating a new value node 2. (See figure below)

              +

              image-20230323000513767

              +

              If we then insert ("b", 3), we will create a new root, a new node and reuse the previous nodes. In this way, we can get the content of the trie before and after each insertion operation. As long as we have the root object (Trie class), we can access the data inside the trie at that time. (See figure below)

              +

              image-20230323000601882

              +

              One more example: if we then insert ("a", "abc") and remove ("ab", 1), we can get the below trie. Note that parent nodes can have values, and you will need to purge all unnecessary nodes after removal.

              +

              image-20230323000658620

              +

              To create a new node, you should use the Clone function on the TrieNode class. To reuse an existing node in the new trie, you can copy std::shared_ptr<TrieNode>: copying a shared pointer doesn’t copy the underlying data.

              +

              You should not manually allocate memory by using new and delete in this project. std::shared_ptr will deallocate the object when no one has a reference to the underlying object.

              +
              +

              感想

              task1的目标就是实现我们的cow-trie的主体,先不要求并发。

              +

              虽说算法上比较复杂,但是由于它图解以及代码中的注释解说都已经说得很详细了,再加上之前已经写过了trie树有一个大体框架,因而具体coding的时候思路还是比较清晰的。

              +

              我认为具体的难点还是在于cpp上。下面列出了几个比较有价值的错误和相关debug过程,其中const转移显示保存is_value_node_是我认为两个比较难的点。

              +

              错误集锦

              const转移

              trie.h中:

              +
              class Trie {
              private:
              // The root of the trie.
              std::shared_ptr<const TrieNode> root_{nullptr};
              // Create a new trie with the given root.
              explicit Trie(std::shared_ptr<const TrieNode> root) : root_(std::move(root)) {}

              public:
              // Create an empty trie.
              Trie() = default;

              // Get the value associated with the given key.
              // 1. If the key is not in the trie, return nullptr.
              // 2. If the key is in the trie but the type is mismatched, return nullptr.
              // 3. Otherwise, return the value.
              template <class T>
              auto Get(std::string_view key) const -> const T *;

              // Put a new key-value pair into the trie. If the key already exists, overwrite the value.
              // Returns the new trie.
              template <class T>
              auto Put(std::string_view key, T value) const -> Trie;

              // Remove the key from the trie. If the key does not exist, return the original trie.
              // Otherwise, returns the new trie.
              auto Remove(std::string_view key) const -> Trie;
              };
              + +

              可以看到,为了呼应我们的cow-trie,在语法上强制性要求不能“directly modify”,它将root_children_->second同时设置为了一个指向对象为const的指针。而这意味着什么呢?意味着我们不能修改root_的内容,也不能修改root_->children_->second的内容,同样的孩子的孩子也不行。这就需要我们在Put方法中遍历trie时,对遍历路径上的每个结点都需要copy一次,故而我们的代码具体是如下实现的:

              +

              首先,利用TrieNode::Clone()方法来创造一个非const指针的新root:

              +
              // in trie.h  TrieNode{}
              virtual auto Clone() const -> std::unique_ptr<TrieNode> { return std::make_unique<TrieNode>(children_); }
              + +
              // 创造新的根节点,并且为非const类型
              std::shared_ptr<TrieNode> root = std::shared_ptr<TrieNode>(root_->Clone());
              // 使用t指针来遍历trie树
              std::shared_ptr<TrieNode> t = root;
              + +

              再然后,每次迭代的时候在遍历路径上创造新的结点,结点类型非const;再利用shared_ptr的共享复制( t = tmp;),就能使得当前的t指针一直保持非const状态。

              +
              for (uint64_t i = 0; i < key.length(); i++) {
              auto it = t->children_.find(key.at(i));
              if (it == t->children_.end()) {
              if (i != key.length() - 1) {
              std::shared_ptr<TrieNode> tmp(new TrieNode());
              // ...
              t = tmp;
              } else {
              std::shared_ptr<TrieNodeWithValue<T>> tmp(new TrieNodeWithValue(std::make_shared<T>(std::move(value))));
              // ...
              t = tmp;
              break;
              }
              } else {
              if (i == key.length() - 1) {
              std::shared_ptr<TrieNodeWithValue<T>> node =
              std::make_shared<TrieNodeWithValue<T>>(it->second->children_, std::make_shared<T>(std::move(value)));
              // ...
              t = node;
              break;
              }
              std::shared_ptr<TrieNode> node = std::shared_ptr<TrieNode>(it->second->Clone());
              // ...
              t = node;
              }
              }
              + +
              +

              注:我本来的写法是这样的:

              +
              for (uint64_t i = 0; i < key.length(); i++) {
              auto it = t->children_.find(key.at(i));
              if (it == t->children_.end()) {
              if (i != key.length() - 1) {
              std::shared_ptr<TrieNode> tmp(new TrieNode());
              // ...
              } else {
              std::shared_ptr<TrieNodeWithValue<T>> tmp(new TrieNodeWithValue(std::make_shared<T>(std::move(value))));
              // ...
              break;
              }
              } else {
              if (i == key.length() - 1) {
              std::shared_ptr<TrieNodeWithValue<T>> node =
              std::make_shared<TrieNodeWithValue<T>>(it->second->children_, std::make_shared<T>(std::move(value)));
              // ...
              break;
              }
              std::shared_ptr<TrieNode> node = std::shared_ptr<TrieNode>(it->second->Clone());
              // ...
              }
              it = t->children_.find(key.at(i));
              t = it->second;
              }
              + +

              也就是为了省事,将t指针的转移集中放在了循环体最后进行。但这样是不行的。

              +

              cpp中,可以将非const对象自然转移为const对象,比如代码中就将非const的新结点放进了children_中;但是不允许将const对象自然转移为非const对象,比如代码中的t = it->second;。因而,我们对t指针的转移不能在新结点放入其children_之后。

              +
              +
              +

              注2:在这里,我本来还多用了一个prev指针,因为在coding的时候用的是上面的本来的写法,误以为t指针只能是const,所以还得有父节点才能再把t指针复制一遍。但其实并非如此,而且就算如此prev指针也还是跟t指针一样的const的。。。不过还好编译前发现了上面那点改过来了,要不然就得面对编译大报错2333

              +
              +
              make_shared

              make_shared作用也类似于new,会在堆上开辟空间用以存放共享指针的base对象。这也让我想起来我在做上面那个实验时一个地方改成make_shared就对了,估计是犯了用栈中对象创建共享指针的错误。

              +
              +

              官方鼓励用make_shared函数来创建对象,而不要手动去new。这一是因为,new出来的类型是原始指针,make_shared可以防止我们去使用原始指针创建多个引用计数体系;二是因为,make_shared可以防止内存碎片化。

              +
              +
              一个奇妙的报错

              在写这样的shared_ptr的共享转移时:

              +
              std::shared_ptr<TrieNode> tmp = make_shared<TrieNode>();
              // ...
              t = tmp;
              + +

              会在t=tmp这里报错不能把int类型的tmp复制给t。我看了半天很奇怪哪来的int类型,查了半天怎么共享shared_ptr,最后才发现是因为这里:

              +
              std::make_shared<TrieNode>()
              + +

              漏了个std::呃呃。

              +
              显式保存is_value_node_

              trie.cpp RemoveHelper()中:

              +
              if (i != key.length() - 1) {
              // 注意此处需要保留原来的is_value_node_,之后再赋值回去!!!
              bool tmp_val = node->second->is_value_node_;
              std::shared_ptr<TrieNode> tmp = std::shared_ptr<TrieNode>(node->second->Clone());
              tmp->is_value_node_ = tmp_val;

              root->children_.erase(key.at(i));
              root->children_.insert(std::make_pair(key.at(i), tmp));
              flag = RemoveHelper(tmp, key, i + 1);
              }
              + +

              否则会:

              +

              image-20230323114435061

              +

              查看trie_test.cpp的代码:

              +
              TEST(TrieTest, BasicRemoveTest2) {
              auto trie = Trie();
              // Put something
              trie = trie.Put<uint32_t>("test", 2333);
              ASSERT_EQ(*trie.Get<uint32_t>("test"), 2333);
              trie = trie.Put<uint32_t>("te", 23);
              ASSERT_EQ(*trie.Get<uint32_t>("te"), 23);
              trie = trie.Put<uint32_t>("tes", 233);
              ASSERT_EQ(*trie.Get<uint32_t>("tes"), 233);

              // Delete something
              trie = trie.Remove("te");
              trie = trie.Remove("tes");
              trie = trie.Remove("test");

              ASSERT_EQ(trie.Get<uint32_t>("te"), nullptr);
              ASSERT_EQ(trie.Get<uint32_t>("tes"), nullptr);
              ASSERT_EQ(trie.Get<uint32_t>("test"), nullptr);
              }
              + +

              它是在ASSERT_EQ(trie.Get<uint32_t>("te"), nullptr);这句报错的。这确实很奇怪,因为“te”已经被remove了。这是为什么呢?

              +

              经过gdb调试,trie的Remove和Put功能都确实很正常,但是我发现了一个诡异的现象。

              +

              在经过trie = trie.Remove("te");这句话后,trie的状态是t-e-(s)-(t)【括号表示为有值结点,类型为TireNodeWithValue】,符合预期。但是,经过紧随其后的trie = trie.Remove("tes");之后,trie的状态却变成了t-(e)-s-(t)。

              +

              image-20230323115902073

              +

              这实在是很诡异,为什么经过了一次Remove之后,trie = trie.Remove("te");这句话的效果就被重置了?

              +

              我想了挺久,最终认为这是构造方法的问题。

              +

              再次看一遍我们的Remove的代码:

              +
              if (i != key.length() - 1) {
              std::shared_ptr<TrieNode> tmp = std::shared_ptr<TrieNode>(node->second->Clone());

              root->children_.erase(key.at(i));
              root->children_.insert(std::make_pair(key.at(i), tmp));
              flag = RemoveHelper(tmp, key, i + 1);
              } else {
              if (node->second->is_value_node_) {
              if (!node->second->children_.empty()) {
              std::shared_ptr<TrieNode> tmp = std::shared_ptr<TrieNode>(node->second->Clone());
              tmp->is_value_node_ = false;
              root->children_.erase(key.at(i));
              root->children_.insert(std::make_pair(key.at(i), tmp));
              } else {
              root->children_.erase(key.at(i));
              }
              return true;
              }
              return false;
              }
              + +

              以及TrieNodeWithValue::Clone()

              +
              auto Clone() const -> std::unique_ptr<TrieNode> override {
              return std::make_unique<TrieNodeWithValue<T>>(children_, value_);
              }
              + +

              以及Clone()方法调用的TrieNodeWithValue的构造方法:

              +
              explicit TrieNodeWithValue(std::shared_ptr<T> value) : value_(std::move(value)) { this->is_value_node_ = true; }
              + +

              可以发现,在多态作用下,e结点始终是一个TrieNodeWithValue的结点。

              +

              在我们去除tes这个key时,会到这个分支:

              +
              if (i != key.length() - 1) {
              std::shared_ptr<TrieNode> tmp = std::shared_ptr<TrieNode>(node->second->Clone());
              + +

              Clone()中会调用node->second,也即e结点的构造方法,然后将e结点的is_value_node_设置为true,从而导致Get中无法通过这句代码返回nullptr。

              +
              if (!(t->is_value_node_)) {
              return nullptr;
              }
              + +

              因而,为了解决这个问题,我们就需要暂存is_value_node_,并在之后恢复它。

              +

              Task2 concurrency

              +

              In this task, you will need to modify trie_store.h and trie_store.cpp.需要实现并发安全版本。

              +

              For the original Trie class, everytime we modify the trie, we need to get the new root to access the new content. But for the concurrent key-value store, the put and delete methods do not have a return value. This requires you to use concurrency primitives to synchronize reads and writes so that no data is lost through the process.在并发安全版本中,PutGet不会返回trie,而是应该修改包装类的base trie。

              +

              Your concurrent key-value store should concurrently serve multiple readers and a single writer. That is to say, when someone is modifying the trie, reads can still be performed on the old root. When someone is reading, writes can still be performed without waiting for reads.同一时刻可以有一个writer和多个reader。

              +

              Also, if we get a reference to a value from the trie, we should be able to access it no matter how we modify the trie. The Get function from Trie only returns a pointer. If the trie node storing this value has been removed, the pointer will be dangling. Therefore, in TrieStore, we return a ValueGuard which stores both a reference to the value and the TrieNode corresponding to the root of the trie structure, so that the value can be accessed as we store the ValueGuard.为我们提供了 ValueGuard用以确保return值长时间有效。

              +

              To achieve this, we have provided you with the pseudo code for TrieStore::Get in trie_store.cpp. Please read it carefully and think of how to implement TrieStore::Put and TrieStore::Remove.我们在Get方法中给出了详细的步骤引导。你需要依据它来对PutGet进行修改。

              +
              +

              感想

              task2的内容是实现cow-trie并发安全版本的包装类TrieStore

              +

              相比于fall2022的并发内容,由于加上了cow的特性,本次实验更加复杂。我写了三版都没写对,看到别人的才豁然开朗(很遗憾没有自己再多想会儿……)接下来就从我的错误版本开始,逐步过渡到正确版本吧。

              +

              错误集锦

              版本1

              Get的实现很简单,按他说的一步步做就行,在这边不做赘述。PutRemove思路差不多,在此只放Put的代码。

              +

              image-20230321185244067

              +

              这样看起来很合理:同一时刻似乎确实只有一个writer对root_进行修改,也似乎确实同时可以有别的线程获取root_lock_对其进行读取。但其实,前者是错误的。

              +

              假如说进程A和进程B都在Put逻辑中。进程A执行到了root_ = new_trie这句话,然后进程B进入到root_.Put中。

              +

              root_ = new_trie使用了运算符=的默认实现,进行浅拷贝,故而会修改root_->root_root_.Put中会对root_->root_进行移动。

              +

              进程B在Put中执行std::move(root_)之后,进程A又让root_->root_变成了别的值(trie浅拷贝),导致原来的root_的引用计数变为0,自动释放(因为是智能指针shared_ptr),进程B在Put中再次访问就会寄。

              +
              +

              注,此处是因为智能指针引用计数为零才释放的,cpp没有垃圾回收机制。

              +
              +
              版本2
              void TrieStore::Put(std::string_view key, T value) {
              // You will need to ensure there is only one writer at a time. Think of how you can achieve this.
              // The logic should be somehow similar to `TrieStore::Get`.
              root_lock_.lock();
              Trie tmp = root_;
              root_lock_.unlock();

              write_lock_.lock();
              Trie new_trie = tmp.Put(key, std::move(value));
              write_lock_.unlock();

              root_lock_.lock();
              root_ = new_trie;
              root_lock_.unlock();
              }
              + +

              版本1错误后,我发现我并没有按它强调的“somehow similar to Get”那样,模仿Get中的写法来做。于是我就修改了下,版本2诞生了。

              +

              但是这样的话,依然不能解决版本1中的问题。所以我又搞了个版本3.

              +
              版本3
              void TrieStore::Put(std::string_view key, T value) {
              write_lock_.lock();
              Trie tmp = root_;
              Trie new_trie = tmp.Put(key, std::move(value));
              root_ = new_trie;
              write_lock_.unlock();
              }
              + +

              这样就能通过所有测试了。

              +

              但这样做虽然能解决多个writer的争夺问题,但不能解决一个writer和一个reader的争夺问题:因为两者都争夺同一个root_变量,但只有reader争夺root_lock_,这显然很不安全。因而,终极版本应该是这样:

              +
              正确版本
              template <class T>
              void TrieStore::Put(std::string_view key, T value) {
              write_lock_.lock();
              root_lock_.lock();
              Trie tmp = root_;
              root_lock_.unlock();

              Trie new_trie = tmp.Put(key, std::move(value));

              root_lock_.lock();
              root_ = new_trie;
              root_lock_.unlock();
              write_lock_.unlock();
              }
              + + + +

              可以看到整个思维过程是线性的,逐步改进下来,正确答案其实很容易想到。只可惜我太浮躁了,没有静下心来好好想,在版本3之后就去看了眼别人怎么写的(罪过)没有独立思考,算是一个小遗憾。

              +

              Task3 debugging

              感想

              一个考查我们debug入门技巧的小任务,简单,但我觉得形势很新颖。

              +

              debug过程

              随便贴点debug过程的截图。

              +

              image-20230322221526353

              +

              image-20230322221634506

              +

              image-20230322221716462

              +

              小问题

              gdb:Attempt to take address of value not located in memory.

              任务中,需要获取root_的孙子。所以我就这么写了个gdb指令:p root_->children_.find('9')->second,然后就爆出了标题这个错误。

              +

              百度了下看到了这个:

              +
              +

              gdb调试时好用的命令

              +

              image-20230323142137068

              +
              +

              也许是因为我们通过.访问了children_的成员find吧(

              +

              总之,我最后是在trie_debug_test添加了这几行代码解决的:

              +
              // Put a breakpoint here.

              // (1) How many children nodes are there on the root?
              // Replace `CASE_1_YOUR_ANSWER` in `trie_answer.h` with the correct answer.
              if (CASE_1_YOUR_ANSWER != Case1CorrectAnswer()) {
              ASSERT_TRUE(false);
              }
              auto it = trie.root_->children_.find('9');
              // (2) How many children nodes are there on the node of prefix `9`?
              // Replace `CASE_2_YOUR_ANSWER` in `trie_answer.h` with the correct answer.
              if (CASE_2_YOUR_ANSWER != Case2CorrectAnswer()) {
              ASSERT_TRUE(false);
              }
              auto val = trie.Get<uint32_t>("93");
              std::cout << val << it->first << std::endl;
              // (3) What's the value for `93`?
              // Replace `CASE_3_YOUR_ANSWER` in `trie_answer.h` with the correct answer.
              if (CASE_3_YOUR_ANSWER != Case3CorrectAnswer()) {
              ASSERT_TRUE(false);
              }
              + +

              也即添加了it和val,以及防止unused报错的cout语句。gdb调试时打印it和val就行。

              +
              答案对但是过不了评测
              +

              来自CMU 15445 Project 0 (Spring 2023) 学习记录

              +

              在我本地的环境上,调试三问的答案分别是8 1 42,但该答案无法通过 Grade 平台的评测。发现在 Discord 上有人提出了同样的问题,助教 Alex Chi 给出了解答:

              +
              +

              Alex Chi — 2023/02/15 23:29
              It is possible that your environment produces different random numbers than the grading environment. In case your environment is producing different set of random numbers than our grader, replace your TrieDebugger test with:

              +
              +
              auto trie = Trie();
              trie = trie.Put<uint32_t>("65", 25);
              trie = trie.Put<uint32_t>("61", 65);
              trie = trie.Put<uint32_t>("82", 84);
              trie = trie.Put<uint32_t>("2", 42);
              trie = trie.Put<uint32_t>("16", 67);
              trie = trie.Put<uint32_t>("94", 53);
              trie = trie.Put<uint32_t>("20", 35);
              trie = trie.Put<uint32_t>("3", 57);
              trie = trie.Put<uint32_t>("93", 30);
              trie = trie.Put<uint32_t>("75", 29);
              +
              +

              难绷,我反复确认了好几遍(。主要还是太相信cmu的权威了,觉得这实验都发布了好几个月了应该不会有错,就没想到是这个问题。我觉得最好还是把这个问题反应在指导书上吧。

              +

              Task4 SQL String Functions

              +

              Now it is time to dive into BusTub itself!

              +

              You will need to implement upper and lower SQL functions.

              +

              This can be done in 2 steps:

              +
                +
              1. implement the function logic in string_expression.h.
              2. +
              3. register the function in BusTub, so that the SQL framework can call your function when the user executes a SQL, in plan_func_call.cpp.
              4. +
              +

              To test your implementation, you can use bustub-shell:

              +
              cd build
              make -j`nproc` shell
              ./bin/bustub-shell
              bustub> select upper('AbCd'), lower('AbCd');
              ABCD abcd
              +
              +

              感想

              说实话乍一看我还没看懂(。它放在这个位置,我还以为跟上面实现的cow-trie有什么关系,并且误以为这个upper和lower是什么上层接口底层接口的意思,跟它大眼瞪小眼了半天。直到看到了下面的案例,才发现跟trie似乎没有任何关系23333

              +

              本次实验内容其实就是实现sql的转换大小写的函数。知道了要做什么之后,任务就很简单了,按着它提示一步步做就行。

              +

              不过此task重点其实也是在稍微了解下我们接下来要打交道的sql框架的代码。比如说,此次我们的实现涉及到的,居然是一个差不多是工厂模式(其实更像策略模式?)的一部分:

              +

              外界传入想调用的函数名,通过GetFuncCallFromFactory获取对应的处理对象

              +

              image-20230322154915206

              +

              得到处理对象后调用其Compute方法就行

              +

              image-20230322154849449

              +

              第一次如此鲜明地看到一个设计模式在cpp的应用,真是让我非常震撼。

              +

              代码规范

              依旧是这三件套:

              +
              make format
              make check-lint
              make check-clang-tidy-p0
              + +

              错误集锦

              关于sanitizer

              执行了该命令:cmake -DCMAKE_BUILD_TYPE=Debug -DBUSTUB_SANITIZER= ..之后,执行make报错missing argument to '-fsanitize='

              +

              发生这个的原因是cmake的命令中将BUSTUB_SANITIZER设置成了空。解决方法就是将其设置为别的值就好了,具体想设置成什么值可以参见:关于GCC/LLVM编译器中的sanitize选项用处用法详解 我这里姑且随便设置了个leak

              +]]>
              +
              + + CMU15445 + /2023/03/13/cmu15445/ + +

              实验官网

              +

              代码

              + +

              Project0 C++ Primer

              Project1 Buffer Pool

              Project2 B+Tree Index

              Project3 Query Execution

              ]]>
              + + labs + +
              + + Project3 Query Execution + /2023/03/13/cmu15445$lab3/ + Project3 Query Execution

              TODO,注意一下为什么每个executor的child executor的&&和&的差别

              +
              +

              In this project, you will implement the components that allow BusTub to execute queries. You will create the operator executors that execute SQL queries and implement optimizer rules to transform query plans.

              +

              实现SQL查询的执行,并且实现语句优化。

              +
              +
              +

              In this project, you will add new operator executors and query optimizations to BusTub.

              +

              BusTub uses the iterator (i.e., Volcano) query processing model, in which every executor implements a Next function to get the next tuple result.

              +

              When the DBMS invokes an executor’s Next function, the executor returns either:

              +

              (1) a single tuple

              +

              ​ In BusTub’s implementation of the iterator model, 除了元组外还会返回record identifier (RID)

              +

              (2) an indicator that there are no more tuples.

              +

              With this approach, each executor implements a loop that continues calling Next on its children to retrieve tuples and process them one by one.

              +
              +

              Background

              Bustub Framewor

              image-20231227153858926

              +

              AST

              介绍完了bustub的框架之后,它对通过语法树进行查询优化进行了详细的样例介绍。

              +

              首先温习一下什么是语法树(abstract syntax tree, AST ):

              +

              SQL语句

              +
              Select `title`
              From Books, Borrowers, Loans
              Where Books.LC_NO = Loans.LC_NO and Borrowers.CARD_NO = Loans.CARD_NO and DATE <= 1/1/78
              + +

              其语法树表示+优化结果如下图所示:

              +

              image-20231227155236633

              +

              算法如下,其关键思路就是选择投影尽早做,能移多下去就移多下去

              +

              image-20231227155806019

              +

              而这里15445介绍的也是这样的语法树优化算法。

              +

              首先记录一下它这几个专有名词对应的操作:

              +
              +
                +
              1. Projection:投影
              2. +
              3. Filter:选择
              4. +
              5. MockScan:对一个表进行的扫描操作
              6. +
              7. Aggregation:聚合函数
              8. +
              9. NestedLoopJoin:嵌套循环连接
              10. +
              +
              +

              再结合它给的几个语法树的例子:

              +
              SELECT * FROM __mock_table_1;

              === PLANNER ===
              Projection { exprs=[#0.0, #0.1] } | (__mock_table_1.colA:INTEGER, __mock_table_1.colB:INTEGER)
              MockScan { table=__mock_table_1 } | (__mock_table_1.colA:INTEGER, __mock_table_1.colB:INTEGER)
              === OPTIMIZER ===
              MockScan { table=__mock_table_1 } | (__mock_table_1.colA:INTEGER, __mock_table_1.colB:INTEGER)
              + +
              SELECT colA, MAX(colB) FROM
              (SELECT * FROM __mock_table_1, __mock_table_3 WHERE colA = colE) GROUP BY colA;

              === OPTIMIZER ===
              Agg { types=[max], aggregates=[#0.1], group_by=[#0.0] }
              NestedLoopJoin { type=Inner, predicate=(#0.0=#1.0) }
              MockScan { table=__mock_table_1 }
              MockScan { table=__mock_table_3 }
              + +

              image-20231227160450894

              +
              SELECT * FROM __mock_table_1 WHERE colA > 1;

              === OPTIMIZER ===
              Filter { predicate=(#0.0>1) } | (__mock_table_1.colA:INTEGER, __mock_table_1.colB:INTEGER)
              MockScan { table=__mock_table_1 } | (__mock_table_1.colA:INTEGER, __mock_table_1.colB:INTEGER)
              + +
              values (1, 2, 'a'), (3, 4, 'b');

              === PLANNER ===
              Projection { exprs=[#0.0, #0.1, #0.2] } | (__values#0.0:INTEGER, __values#0.1:INTEGER, __values#0.2:VARCHAR)
              Values { rows=2 } | (__values#0.0:INTEGER, __values#0.1:INTEGER, __values#0.2:VARCHAR)
              === OPTIMIZER ===
              Values { rows=2 } | (__values#0.0:INTEGER, __values#0.1:INTEGER, __values#0.2:VARCHAR)
              + +

              可以看到,它大概是用缩进来表示了AST的父子关系。

              +

              我们课上学习的语法树中每个table标志对应着一个MockScan;笛卡尔积+选择操作可以表示为一个NestedLoopJoin。

              +

              对于这些输出的意义,指导书也给了详细的解释:

              +

              ColumnValueExpression

              +

              也即类似exprs=[#0.0, #0.1]#0意为第一个子节点(不是第一个表的意思。。)

              +

              Volcano Model

              introduction

              +

              火山模型和优化(向量化执行、编译执行) 这篇文章写得很详细,下文也摘抄自该博客

              +

              数据库内核通过 code-gen 提升性能的探索

              +
              +

              火山模型又称 Volcano Model 或者 Pipeline Model(或者迭代器模型)。该计算模型将关系代数中每一种操作抽象为一个 Operator,将整个 SQL 构建成一个 Operator 树,从根节点到叶子结点自上而下地递归调用 next() 函数。

              +

              一般Operator的next() 接口实现分为三步:

              +
                +
              • 调用子节点Operator的next() 接口获取一行数据(tuple);
              • +
              • 对tuple进行Operator特定的处理(如filter 或project 等);
              • +
              • 返回处理后的tuple。
              • +
              +

              因此,查询执行时会由查询树自顶向下的调用next() 接口,数据则自底向上的被拉取处理。火山模型的这种处理方式也称为拉取执行模型(Pull Based)。

              +

              大多数关系型数据库都是使用迭代模型的,如 SQLite、MongoDB、Impala、DB2、SQLServer、Greenplum、PostgreSQL、Oracle、MySQL 等。

              +

              火山模型的优点是,处理逻辑清晰,简单,每个Operator 只要关心自己的处理逻辑即可,耦合性低。但是缺点也非常明显:

              +
                +
              • 每处理一行需要调用多次next() 函数,而next()为虚函数,开销大。

                +

                编译器无法对虚函数进行inline优化,同时也带来分支预测的开销,且很容易预测失败,导致CPU流水线执行混乱。

                +
              • +
              • 数据以行为单位进行处理,不利于CPU cache 发挥作用。

                +
              • +
              +

              pipeline breaker

              火山模型显而易见是以从上到下一个流水线形式执行的,它的最理想情况是每个流水线节点所需的这个tuple都存储在寄存器中。然而,有一些操作,如聚合函数等等,需要对整个表进行操作才能获取到当前所需tuple,而整个表显然最多只能读入到内存中,这样的操作就被称为pipeline breaker

              +

              下面的实现中的aggregation、sort、hash join的build阶段都是pipeline breaker,这些复杂的操作阶段都需要在init()函数中进行。

              +

              Summary

              TODO,从宏观整个架构简介

              +

              ADDITIONAL INFORMATION

              System Catalog

              +

              The entirety of the catalog implementation is in src/include/catalog/catalog.h. You should pay particular attention to the member functions Catalog::GetTable() and Catalog::GetIndex(). You will use these functions in the implementation of your executors to query the catalog for tables and indexes.

              +
              +

              它意思大概是说在实现executor时可能需要用到catelog里这两个函数。

              +

              GetTable返回一个TableInfo

              +
              struct TableInfo {
              /** The table schema */
              Schema schema_;
              /** The table name */
              const std::string name_;
              /** An owning pointer to the table heap */
              std::unique_ptr<TableHeap> table_;
              /** The table OID */
              const table_oid_t oid_;
              };
              + + + +
              +

              For the table modification executors (InsertExecutor, UpdateExecutor, and DeleteExecutor) you must modify all indexes for the table targeted by the operation. You may find the Catalog::GetTableIndexes() function useful for querying all of the indexes defined for a particular table. Once you have the IndexInfo instance for each of the table’s indexes, you can invoke index modification operations on the underlying index structure.

              +

              In this project, we use your implementation of B+ Tree Index from Project 2 as the underlying data structure for all index operations. Therefore, successful completion of this project relies on a working implementation of the B+ Tree Index.

              +
              +

              话说index是那个索引吗,就是每张表有几个建立在某个属性的索引,也即一张表可以有n棵b+树

              +

              GetIndex返回一个IndexInfo

              +
              struct IndexInfo {
              /** The schema for the index key */
              Schema key_schema_;
              /** The name of the index */
              std::string name_;
              /** An owning pointer to the index */
              std::unique_ptr<Index> index_;
              /** The unique OID for the index */
              index_oid_t index_oid_;
              /** The name of the table on which the index is created */
              std::string table_name_;
              /** The size of the index key, in bytes */
              const size_t key_size_;
              };
              + +

              Optimizer Rule Implementation Guide

              +

              The BusTub optimizer is a rule-based optimizer. Most optimizer rules construct optimized plans in a bottom-up way(自底向上). Because the query plan has this tree structure, before applying the optimizer rules to the current plan node, you want to first recursively apply the rules to its children.

              +

              At each plan node, you should determine if the source plan structure matches the one you are trying to optimize, and then check the attributes in that plan to see if it can be optimized into the target optimized plan structure.

              +

              In the public BusTub repository, we already provide the implementation of several optimizer rules. Please take a look at them as reference.

              +
              +

              Task1 Access Method Executors

              +

              In the background section above, we saw that the BusTub can already retrieve data from mock tables in SELECT queries.

              +

              This is implemented without real tables by using a MockScan executor to always generate the same tuples using a predefined algorithm.

              +

              This is why you cannot update these tables.

              +
              +

              也就是说意思是目前的mockscan executor不是真的查表,而是返回固定的元组。

              +

              看了一遍代码,感觉大概明白了。我们可以来看一下迭代器的Next函数:

              +
              auto MockScanExecutor::Next(Tuple *tuple, RID *rid) -> bool {
              if (cursor_ == size_) {
              // Scan complete
              return EXECUTOR_EXHAUSTED;
              }
              if (shuffled_idx_.empty()) {
              *tuple = func_(cursor_);
              } else {
              *tuple = func_(shuffled_idx_[cursor_]);
              }
              ++cursor_;
              *rid = MakeDummyRID();
              return EXECUTOR_ACTIVE;
              }
              + +

              其核心就是调用func_来获取表的元组。

              +

              也就是说是这样的,每个MockScanExecutor用来执行一个plan,那么也就对应着某一个table。通过执行某一个table特定的迭代function,就可以返回元组。

              +

              这个迭代function比如说对于表tas_2023是这样的:

              +
              if (table == "__mock_table_tas_2023") {
              return [plan](size_t cursor) {
              std::vector<Value> values{};
              values.push_back(ValueFactory::GetVarcharValue(ta_list_2023[cursor]));
              values.push_back(ValueFactory::GetVarcharValue(ta_oh_2023[cursor]));
              return Tuple{values, &plan->OutputSchema()};
              };
              }
              + +

              也即MockScanExecutor负责对表指针的管理,function负责实际对表的物理访问。这样就成功解耦了。

              +
              +

              In this task, you will implement executors that read from and write to tables in the storage system.

              +
                +
              • src/execution/seq_scan_executor.cpp
              • +
              • src/execution/insert_executor.cpp
              • +
              • src/execution/update_executor.cpp
              • +
              • src/execution/delete_executor.cpp
              • +
              • src/execution/index_scan_executor.cpp
              • +
              +
              +

              而我们本次实验就是需要实现这么一大堆的executor。看来又是个体力活了。

              +

              seq_scan

              一些想法

              c++知识

              image-20240115121419677

              +

              可以看到,前缀++重载的运算符方法和后缀++是不一样的。

              +
              +

              这里我理解得还是肤浅了…… 根据 这篇文章++i 的内部类定义为 T& T:: operator++();,而 i++ 的内部类定义为 T T:: operator++(int);[1]前置操作返回引用,后置操作返回值。后置操作的 int 参数是一个虚拟参数,用于区分运算符 ++ 的前置和后置。理论上,i++ 会产生临时对象,实践中,编译器会对内置类型进行优化;而对于自定义类型(如这里的 Iterator),++i 的性能通常优于 i++

              +
              +
              MockScan

              值得一提的是它跟MockScan的关系。MockScan是一种模拟操作,所以各种表都是硬编码在它的mock_scan.h里的;而SeqScan就是真正的遍历操作了,它需要获取tuple就需要通过各种复杂的物理操作和封装一步步读取了。

              +
              physical layer

              通过实现SeqScan,我们可以初步窥探整个bustub物理层面交互的架构。

              +

              跟之前project中的索引entry一样,实际的数据tuple也保存在page中,其对应类为TablePage。并且是堆文件组织结构:

              +

              image-20240115114448798

              +
              +

              TablePage的结构值得一提。

              +

              在它的成员定义中,我们可以看到其中有两个柔性数组成员(Flexible array member):

              +
              char page_start_[0];
              TupleInfo tuple_info_[0];
              + +

              之前的Project2,我们只接触过一个的case,这里的两个感觉其实也同理可得,相当于page_start_tuple_info_都指向最末尾空闲空间的开始。

              +

              TablePage的实际存储结构如下:

              +
              ->  increase											   increase  <-
              | ------------------------------- | ********************************* |
              ↑ ↑
              page_start & tuple_info +TUPLE_INFO_SIZE*sizeof(TupleInfo)
              + +

              也即tuple info存储在前半部分,tuple data存储在后半部分,并且二者增长方式相反。

              +
              +

              而多页TablePage就构成了一个TableHeap,也即其物理存储空间。每次创建表时,我们就会分配对应的heap空间和相关meta data。TableHeap对外提供了增删改查元组的方法,也提供了一个迭代器实现TableIterator,用于遍历里面的元素。

              +

              而由于元组tuple存储在磁盘中,所以我们需要在读取它的值的时候先进行反序列化DeserializeFrom,这个过程需要用到表的类型信息和offset信息之类的,所以Tuple::GetValue需要传入schema参数。

              +

              实现

              它基本原理也就是顺序遍历整张表,没什么好说的。

              +

              在本次的sequence scan实现中,我们就需要首先获取表对应的iterator:

              +
              // 巨长一串
              table_iterator_ = std::make_unique<TableIterator>(exec_ctx_->GetCatalog()->GetTable(plan_->GetTableOid())->table_->MakeIterator());
              + +

              然后通过这个iterator不断迭代获取元素即可。

              +

              有一点要注意的,应该是对删除元组的处理,毕竟sequence scan算是是实现其他二级操作的基石了,所以我们必须在这里处理删除元组。具体逻辑如下:

              +
              do {
              if (table_iterator_->IsEnd()) {
              return EXECUTOR_EXHAUSTED;
              }

              get tuple;
              ++ iterator;

              } while(tuple_meta.is_deleted_);
              + + + +

              insert

              一些想法

              recursive execute

              对于SQL的嵌套子查询,bustub采用的是递归实现。具体来说,以insertion为例:

              +

              外界调用情况如下所示。

              +
              // Execute a query plan.
              auto Execute(...) -> bool {
              // Construct the executor for the abstract plan node
              auto executor = ExecutorFactory::CreateExecutor(exec_ctx, plan);
              executor->Init();
              PollExecutor(executor.get(), plan, result_set);
              PerformChecks(exec_ctx);
              }
              + +

              CreateExecutor是一个递归函数,递归创建每个子查询的实例,把对应的executor返回给父查询

              +
              auto ExecutorFactory::CreateExecutor(...)
              -> std::unique_ptr<AbstractExecutor> {

              switch (plan->GetType()) {

              case PlanType::SeqScan: {
              return std::make_unique<SeqScanExecutor>(exec_ctx, dynamic_cast<const SeqScanPlanNode *>(plan.get()));
              }

              case PlanType::Insert: {
              auto insert_plan = dynamic_cast<const InsertPlanNode *>(plan.get());
              // 递归创建每个子查询的实例
              auto child_executor = ExecutorFactory::CreateExecutor(exec_ctx, insert_plan->GetChildPlan());
              // 把对应的executor返回给父查询
              return std::make_unique<InsertExecutor>(exec_ctx, insert_plan, std::move(child_executor));
              }
              }
              }
              + +

              然后我们再在父查询的Init中调用子查询的Init和Next等方法

              +
              void InsertExecutor::Init() {
              child_executor_->Init();
              ...
              }
              + +

              如此,就能递归实现嵌套子查询。

              +

              实现

              +

              The InsertExecutor inserts tuples into a table and updates any affected indexes.

              +

              The planner will ensure that the values have the same schema as the table. The executor will produce a single tuple of integer type as the output, indicating how many rows have been inserted into the table.

              +
              +

              这里将Insert语句插入的值视为一个匿名子表,对其初始化后使用它的迭代器进行元素访问即可。

              +

              update

              一些想法

              expression

              bustub将一切表达式抽象为了这么几个类:

              +
              AbstractExpression // 基类
              ConstantValueExpression // 常量值表达式
              ColumnValueExpression // 列值表达式,访问某一列的值
              ArithmeticExpression // 算术表达式,树递归结构,子节点是值or算术表达式
              ComparisonExpression // 比较表达式,表示两个表达式
              LogicExpression // 逻辑表达式
              StringExpression // 字符串表达式,包括原字符串or upper之类的
              + +

              而从UpdatePlanNode中,我们可以获取到update字句的所有表达式:

              +
              /** The new expression at each column */
              std::vector<AbstractExpressionRef> target_expressions_;
              + +

              比如此处:

              +
              bustub> explain (o,s) update test_1 set colB = 15445;
              === OPTIMIZER ===
              // 可以注意这边target_exprs的值
              Update { table_oid=20, target_exprs=[#0.0, 15445, #0.2, #0.3] } | (__bustub_internal.update_rows:INTEGER)
              SeqScan { table=test_1 } | (test_1.colA:INTEGER, test_1.colB:INTEGER, test_1.colC:INTEGER, test_1.colD:INTEGER)
              + +

              然后我们分别计算每个expression的值,就可以获取更新之后的元组:

              +
                 // insert again
              std::vector<Value> insert_values;
              for (auto exp : plan_->target_expressions_) {
              // tuple为旧值元组
              insert_values.push_back(exp->Evaluate(&tuple, table_info->schema_));
              }
              // 注意table_info应为要插入的表的info,此处易写为update plan子表的info
              table_heap->InsertTuple(TupleMeta(), Tuple(insert_values, &(table_info->schema_)));
              + + + +
              lazy delete

              删除元组的实现似乎只是简单地标记is_delete_为true就好了。但是我在实际的代码实现(InsertTuple)中似乎并没有看到重组删除空间or覆盖删除空间,每次插入页满只是简单地再申请新的一页,不会再回头。也许是为了简化起见暂不实现这个吧。

              +

              不过改进方法也很简单,对每个表进行固定分配页(或者说提供一个数据量达到百分之几的时候扩容的机制),然后页面间组织成环形链表,这样就能充分覆盖删除空间,同时也兼顾一定性能了。

              +

              实现

              update的实现也不会很难,只需先删除原来的元组,再加个新元组即可。

              +

              delete

              delete的实现完全照搬update就行,没什么好说的。

              +

              index_scan

              +

              The IndexScanExecutor iterates over an index to retrieve RIDs for tuples. The operator then uses these RIDs to retrieve their tuples in the corresponding table. It then emits these tuples one at a time.

              +

              You can test your index scan executor by SELECT FROM <table> ORDER BY <index column>. We will explain why ORDER BY can be transformed into IndexScan in Task #3. 哦吼,也就是说order-by会被翻译为index scan?那order-by的关键字如果不存在索引会怎么样,现建吗

              +

              BusTub only supports indexes with a single, unique integer column. Our test cases will not contain duplicate keys. The type of the index object in the plan will always be BPlusTreeIndexForTwoIntegerColumn in this project. You can safely cast it and store it in the executor object:

              +
              using BPlusTreeIndexForTwoIntegerColumn = BPlusTreeIndex<IntegerKeyType, IntegerValueType, IntegerComparatorType>;

              tree_ = dynamic_cast<BPlusTreeIndexForTwoIntegerColumn *>(index_info_->index_.get())
              + +

              但我看测试里怎么好像有两个键的index?

              +

              You can then construct an index iterator from the index object, scan through all the keys and tuple IDs, lookup the tuple from the table heap, and emit all tuples in order.

              +

              是的,project2的b+树实现确实只存了rid,然后我们通过rid就能知道实际的物理位置了

              +
              +

              一些想法

              索引实现

              通过b+树组织索引结构,索引结点中存的是RID,RID可以用来指示tuple的物理位置,于是我们通过RID就可以获取到tuple,从而减少了磁盘IO。RID结构如下:

              +
              // RID: Record Identifier
              // 高32位是pgid,低32位是slot num
              class RID {
              public:
              explicit RID(int64_t rid) : page_id_(static_cast<page_id_t>(rid >> 32)), slot_num_(static_cast<uint32_t>(rid)) {}

              inline auto Get() const -> int64_t { return (static_cast<int64_t>(page_id_)) << 32 | slot_num_; }

              private:
              page_id_t page_id_{INVALID_PAGE_ID};
              uint32_t slot_num_{0}; // logical offset from 0, 1...
              };
              + +

              并且,bustub保证了对于有索引的表,是不会有重复元组的,故而b+树实际上应该是一个稠密索引。

              +

              (毕竟这个情况似乎有点复杂……物理存储上应该是按插入顺序顺序存储的,故而重复元组可能不放在一起,而我们实现的b+树又不支持重复结点,所以就会g。如果想要支持重复元组,可能就需要从两个改变思路入手,要么是修改b+树支持重复索引结点,此时b+树依然为稠密索引;要么是修改为链式存储结构以支持重复元组放在一起,此时b+树为稀疏索引。)

              +
              c++知识

              非常非常崩溃,怎么保存索引尝试了很久都没做到:

              +
              // 这样不行……
              std::unique_ptr<BPlusTreeIndexIteratorForTwoIntegerColumn> iterator_;
              iterator_ = std::make_unique<BPlusTreeIndexIteratorForTwoIntegerColumn>(std::move(tree_->GetBeginIterator()));

              // 这样也不行……
              BPlusTreeIndexIteratorForTwoIntegerColumn iterator_;
              iterator_ = std::move(tree_->GetBeginIterator());
              + +

              没办法,最终只能保存tree,iterator在next里动态获取了,我真是服了。等之后看完c++primer或者c++水平有所提升了再来解决这个问题吧。

              +

              实现

              难绷,本来以为这个index-scan应该是最简单的,毕竟只用调用现成索引接口,没想到居然写了最久,可能足足两三个小时。。。

              +

              首先的一个大难点就是如何保存迭代器了。在之前的seq-scan的时候,使用的是unique ptr,然而这里却不行会报一堆奇奇怪怪的错误(具体见一些想法-c++知识)。最后只能换一个思路,不保存迭代器而是保存next_key_了。然而又由于之前b+树的实现bug问题,导致对end iterator解引用是合法的,所以会产生各种奇奇怪怪的错误。解决了这个之后,之前写的insert、update、delete的更新索引部分又出了问题,rid和insert_key弄错了,诸如此类。

              +

              总之,解决了这一大堆小问题之后,才总算通过了index-scan的测试,真是令人南蚌。具体改了什么bug可以详情见b8d3ba546cfdea6fc576ad8d668322c87f6386c1这个commit。

              +

              同时,也跟上面的sequence scan一样,都需要对标识为deleted的元组进行跳过处理。

              +
              +

              这里我也是没想太多……事实上,index scan无需实时检测is_deleted字段并做处理,因为索引是会随着修改实时更新的,被删除的tuple不会在索引中。

              +
              +

              Task2 Aggregation & Join Executors

              +

              In this task you will add an aggregation executor, several join executors, and enable the optimizer to select between a nested loop join and hash join when planning a query.

              +

              You will complete your implementation in the following files:

              +
                +
              • src/execution/aggregation_executor.cpp
              • +
              • src/execution/nested_loop_join_executor.cpp
              • +
              • src/execution/hash_join_executor.cpp
              • +
              • src/optimizer/nlj_as_hash_join.cpp
              • +
              +
              +

              aggregation

              +

              The AggregationPlanNode is used to support queries like the following:

              +
              EXPLAIN SELECT colA, MIN(colB) FROM __mock_table_1 GROUP BY colA;
              EXPLAIN SELECT COUNT(colA), mi(colB) FROM __mock_table_1;
              EXPLAIN SELECT colA, MIN(colB) FROM __mock_table_1 GROUP BY colA HAVING MAX(colB) > 10;
              EXPLAIN SELECT DISTINCT colA, colB FROM __mock_table_1;
              + +

              也即聚合函数和DISTINCT、GROUP这种。

              +
              +

              此处注意DINSTINCT也是通过aggregation实现的:

              +
              EXPLAIN SELECT DISTINCT colA, colB FROM __mock_table_1;
              === OPTIMIZER ===
              Agg { types=[], aggregates=[], group_by=[#0.0, #0.1] } | (__mock_table_1.colA:INTEGER, __mock_table_1.colB:INTEGER)
              MockScan { table=__mock_table_1 } | (__mock_table_1.colA:INTEGER, __mock_table_1.colB:INTEGER)
              + +
              +

              The aggregation executor computes an aggregation function for each group of input. 作用于每一组

              +

              It has exactly one child. The output schema consists of the group-by columns followed by the aggregation columns.

              +
              EXPLAIN SELECT colA, MIN(colB) FROM __mock_table_1 GROUP BY colA;
              === OPTIMIZER ===
              // types标志聚合的种类,aggregates标识聚合的目标,group_by单独用于表示是否有group
              Agg { types=[min], aggregates=[#0.1], group_by=[#0.0] } | (__mock_table_1.colA:INTEGER, <unnamed>:INTEGER)
              MockScan { table=__mock_table_1 } | (__mock_table_1.colA:INTEGER, __mock_table_1.colB:INTEGER)


              EXPLAIN SELECT COUNT(colA), min(colB) FROM __mock_table_1
              === OPTIMIZER === // 如果没有group,则其字段为空
              Agg { types=[count, min], aggregates=[#0.0, #0.1], group_by=[] } | (<unnamed>:INTEGER, <unnamed>:INTEGER)
              MockScan { table=__mock_table_1 } | (__mock_table_1.colA:INTEGER, __mock_table_1.colB:INTEGER)
              + +

              As discussed in class, a common strategy for implementing aggregation is to use a hash table, with the group-by columns as the key.

              +

              In this project, you may assume that the aggregation hash table fits in memory. This means that you do not need to implement a multi-stage, partition-based strategy, and the hash table does not need to be backed by buffer pool pages.

              +

              也就是说这里采取的是基于hashtable的实现而非基于归并排序的,并且为了简单起见将hash table保存在内存中,所以无需进行多趟划分扫描。

              +

              We provide a SimpleAggregationHashTable data structure that exposes an in-memory hash table (std::unordered_map) but with an interface designed for computing aggregations. This class also exposes an SimpleAggregationHashTable::Iterator type that can be used to iterate through the hash table. You will need to complete the CombineAggregateValues function for this class.

              +
              +
              +

              Note: The aggregation executor itself won’t need to handle the HAVING predicate. The planner will plan aggregations with a HAVING clause as an AggregationPlanNode followed by a FilterPlanNode.

              +
              +
              +

              Hint: In the context of a query plan, aggregations are pipeline breakers. This may influence the way that you use the AggregationExecutor::Init() and AggregationExecutor::Next() functions in your implementation. Carefully decide whether the build phase of the aggregation should be performed in AggregationExecutor::Init() or AggregationExecutor::Next().

              +
              +

              一些想法

              countstar

              值得注意的是,这里的实现将COUNT(*)COUNT(colum)区分开了:

              +
              enum class AggregationType { CountStarAggregate, CountAggregate };
              + +

              因为这两者似乎语义上是有区别的,大概体现为以下几点:

              +
                +
              1. 当没有结果时,CountStar返回0,Count返回integer_null
              2. +
              3. CountStar只记录行数,不管值是否为空;Count只记录所要求的列非空的那些行数
              4. +
              +
              hash aggregation

              关于hashtable实现聚合的相关原理及相关示例,具体可见 这篇文章。感觉这系列文章都写得挺好的,如对TiDB有兴趣可以细看。

              +
              +

              在 SQL 中,聚合操作对一组值执行计算,并返回单个值。TiDB 实现了 2 种聚合算法:Hash Aggregation 和 Stream Aggregation

              +

              在 Hash Aggregate 的计算过程中,我们需要维护一个 Hash 表,Hash 表的键为聚合计算的 Group-By值为聚合函数的中间结果 sumcount

              +

              计算过程中,只需要根据每行输入数据计算出键,在 Hash 表中找到对应值进行更新即可。输入数据输入完后,扫描 Hash 表并计算,便可以得到最终结果

              +
              +

              故而思路也是很清晰了。我们在aggregation的实现中要做的,就是把child executor逐行喂给hashtable,最后再遍历hashtable得到结果即可。故而,我们重点需要实现hashtable的InsertCombine函数和hashtable的iterator。

              +

              实现

              理解了hash-aggregation的算法原理后,代码逻辑方面就不算难了,其余最主要的难点应该是空值的处理。

              +

              总结一下,bustub对空值的处理大概有以下几个要点:

              +
                +
              1. 聚合函数对空值处理

                +

                COUNT(*):计入空值

                +

                COUNT/MAX/MIN/SUM(v1):跳过空值

                +
              2. +
              3. 空值自身运算性质

                +

                任意运算若有一个操作数为空,那么结果也为空。

                +

                故而,当没有使用group by关键字的时候(也即hashtable的key为空),此时不能天真地传入一个空的AggregationKey,而应该给它随便塞某个值。不然的话,hashtable内部的比较函数在处理空值的时候恒返回false,会导致检索失败。

                +
              4. +
              5. 空表情况处理

                +

                当表为空的时候,要求:

                +
                select COUNT(*), MAX(v1), COUNT(v1) from table_;
                # 0 integer_null integer_null
                select COUNT(*), MAX(v1), COUNT(v1) from table_ group by v2;
                # no-output
                + +

                这个操作我着实不懂为什么。。。所以我最终代码只能面向测试用例:

                +
                if (!has_next && plan_->GetGroupBys().empty()) {
                // 当表为空并且不使用聚合函数时,输出一个默认情况对
                AggregateKey agg_key;
                agg_key.group_bys_.push_back(Value(TypeId::INTEGER, 1));
                aht_->InsertCombine(agg_key, MakeAggregateValue(nullptr));
                }
              6. +
              +

              NestedLoopJoin

              +

              The DBMS will use NestedLoopJoinPlanNode for all join operations, by default.

              +

              You will need to implement an inner join and left join for the NestedLoopJoinExecutor using the simple nested loop join algorithm from class.

              +

              The output schema of this operator is all columns from the left table followed by all columns from the right table.

              +

              For each tuple in the outer table, consider each tuple in the inner table and emit an output tuple if the join predicate is satisfied.

              +
              +

              也即嵌套循环实现的join,与在课上学的sort merge join一样,都是古法join实现。

              +

              nested join的实现相比之前的思路确实会复杂一些。我们需要学习如何迭代地调用Next来实现一次嵌套循环。思路大概是这样:

              +
              Init():
              Init left, right
              Move left to get current_left_tuple_
              Next():
              while (1):
              if (Move(right)) :;
              else:
              Move left
              Init right
              continue;
              if (checkPredict):
              break;
              + +

              然而其中有这几个细节需要进行处理:

              +
                +
              1. 左连接的实现

                +

                需要增加逻辑:当right遍历完之后,current_left_tuple_仍未被组装进结果过,此时需要帮其拼接上空right tuple。

                +
              2. +
              3. 空表情况

                +

                这个分支中:

                +
                else:
                Move left
                Init right
                continue;
                + +

                不能这样:

                +
                else:
                Move left
                Init right
                Move right
                + +

                这是为了防止空表情况,使得Move right一直返回false,导致之后checkPredict报空指针异常。

                +
              4. +
              5. 测试要求left->Next()调用次数与right->Init()调用次数相同。

                +
                +

                这是为了强制让NestedLoopJoin的实现不是Pipeline Break,从而导致它性能垃圾了

                +
                +
              6. +
              +

              HashJoin

              +

              The DBMS can use HashJoinPlanNode if a query contains a join with a conjunction of equi-conditions between two columns (equi-conditions are seperated by AND).

              +

              也就是说,当连接条件为一/多个列相等时,就可以用hash join。可以看到这是类似等值连接。

              +
              EXPLAIN SELECT * FROM __mock_table_1, __mock_table_3 WHERE colA = colE;
              EXPLAIN SELECT * FROM __mock_table_1 INNER JOIN __mock_table_3 ON colA = colE;
              EXPLAIN SELECT * FROM __mock_table_1 LEFT OUTER JOIN __mock_table_3 ON colA = colE;
              EXPLAIN SELECT * FROM test_1 t1, test_2 t2 WHERE t1.colA = t2.colA AND t1.colB = t2.colC;
              EXPLAIN SELECT * FROM test_1 t1 INNER JOIN test_2 t2 on t1.colA = t2.colA AND t2.colC = t1.colB;
              EXPLAIN SELECT * FROM test_1 t1 LEFT OUTER JOIN test_2 t2 on t2.colA = t1.colA AND t2.colC = t1.colB;
              + +

              You will need to implement the inner join and left join for HashJoinExecutor using the hash join algorithm from class.

              +

              The output schema of this operator is all columns from the left table followed by all columns from the right table.

              +

              As with aggregation, you may assume that the hash table used by the join fits entirely in memory.

              +

              Hint: Your implementation should correctly handle the case where multiple tuples have hash collisions (on either side of the join). 必须正确处理哈希冲突的情况

              +

              Hint: You will want to make use of the join key accessors functions GetLeftJoinKey() and GetRightJoinKey() in the HashJoinPlanNode to construct the join keys for the left and right sides of the join, respectively.

              +

              Hint: You will need a way to hash a tuple with multiple attributes in order to construct a unique key. As a starting point, take a look at how the SimpleAggregationHashTable in the AggregationExecutor implements this functionality. 可以参考 SimpleAggregationHashTable的实现

              +

              Hint: As with aggregation, the build side of a hash join is a pipeline breaker. You should again consider whether the build phase of the hash join should be performed in HashJoinExecutor::Init() or HashJoinExecutor::Next().

              +
              +

              具体什么是hash join,可以参考 这篇文章

              +

              img

              +

              其大概思路也很简单,hash table就是一个map<key, vector<value>>这样的数据结构,然后将两个输入的关系选举出一个小表作为Build(建立hash table),另一个作为Probe(扫描,并根据hash table->second进行迭代组合)。它其实就是一个精确了范围的nested loop join的变种,将nested里层的针对整个关系的大循环缩小为针对hash table一个bucket的小循环。

              +

              具体到这里,思路可以是这样的。首先为了简单起见,我们就不进行选举小表的判断了,固定将right child作为Build,left child作为Probe。建表的话,我们就简单粗暴地遍历right table,然后以right_key_expressions_为keyTuple为value直接建表(反正也是in-memory即可。。。)。然后之后,就仿照之前思路即可。

              +

              Optimizing NestedLoopJoin to HashJoin

              +

              Hash joins usually yield better performance than nested loop joins. You should modify the optimizer to transform a NestedLoopJoinPlanNode into a HashJoinPlanNode when it is possible to use a hash join.

              +

              Specifically, the hash join algorithm can be used when a join predicate is a conjunction of equi-conditions between two columns. For the purpose of this project, handling a single equi-condition, and also two equi-conditions connected by AND, will earn full credit.

              +

              Consider the following example:

              +
              bustub> EXPLAIN (o) SELECT * FROM test_1 t1, test_2 t2 WHERE t1.colA = t2.colA AND t1.colB = t2.colC;
              + +

              Without applying the NLJAsHashJoin optimizer rule, the plan may look like:

              +
              NestedLoopJoin { type=Inner, predicate=((#0.0=#1.0)and(#0.1=#1.2)) } 
              SeqScan { table=test_1 }
              SeqScan { table=test_2 }
              + +

              After applying the NLJAsHashJoin optimizer rule, the left and right join key expressions will be extracted from the single join predicate in the NestedLoopJoinPlanNode. The resulting plan will look like:

              +
              HashJoin { type=Inner, left_key=[#0.0, #0.1], right_key=[#0.0, #0.2] } 
              SeqScan { table=test_1 }
              SeqScan { table=test_2 }
              + +

              Note: Please check the Optimizer Rule Implementation Guide section for details on implementing an optimizer rule.

              +

              Hint: Make sure to check which table the column belongs to for each side of the equi-condition. It is possible that the column from outer table is on the right side of the equi-condition. You may find ColumnValueExpression::GetTupleIdx helpful.

              +

              Hint: The order to apply optimizer rules matters. For example, you want to optimize NestedLoopJoin into HashJoin after filters and NestedLoopJoin have merged. 这个感觉可能意思就是说优化规则的优先级之类的吧,这里用了个例子说hash join的优先级一般得在filter和nested loop join合并了之后。

              +

              Hint: At this point, you should pass SQLLogicTests - #14 to #15.

              +
              +

              一些想法

              bustub optimizer
              +

              The BusTub optimizer is a rule-based optimizer. Most optimizer rules construct optimized plans in a bottom-up way(自底向上). Because the query plan has this tree structure, before applying the optimizer rules to the current plan node, you want to first recursively apply the rules to its children. 基于规则的优化器,规则都是对语法树自底向上实施。感觉跟课内学的差不多。

              +

              In the public BusTub repository, we already provide the implementation of several optimizer rules. Please take a look at them as reference.

              +
              +

              在课程中学到的语法优化,应该也是基于规则的优化,具体见下图及之后列出的无穷无尽个定理:

              +

              image

              +

              image-20231227155806019

              +

              (本图新增了一条规则:选择+嵌套笛卡尔积=嵌套连接)

              +

              查看目录src/optimizer/,我们可以看到:

              +
              $ tree ../src/optimizer/
              ../src/optimizer/
              ├── eliminate_true_filter.cpp # 消除恒真选择
              ├── merge_filter_nlj.cpp # 合并选择和嵌套连接
              ├── merge_filter_scan.cpp # 合并选择和scan
              ├── merge_projection.cpp # 合并多个投影
              ├── nlj_as_hash_join.cpp # 嵌套连接->hash连接
              ├── nlj_as_index_join.cpp # 嵌套连接->index连接
              ├── optimizer.cpp
              ├── optimizer_custom_rules.cpp
              ├── optimizer_internal.cpp
              ├── order_by_index_scan.cpp
              └── sort_limit_as_topn.cpp # 针对 top-N queries 进行优化
              + +

              在本小节任务中,我们需要做的,就是参照其他的规则来实现nlj_as_hash_join。但在此之前,我们不妨先研究一下它语法优化的总体架构。

              +
              auto Optimizer::OptimizeCustom(const AbstractPlanNodeRef &plan) -> AbstractPlanNodeRef {
              auto p = plan;
              p = OptimizeMergeProjection(p); // 首先合并影响相同的投影
              p = OptimizeMergeFilterNLJ(p); // 然后合并选择和嵌套连接
              p = OptimizeNLJAsHashJoin(p); // 然后把嵌套连接改为hash join
              p = OptimizeOrderByAsIndexScan(p); // 根据索引进行查找
              p = OptimizeSortLimitAsTopN(p); // 针对 top-N queries 进行优化
              return p;
              }
              + +

              可以看到,它的实际原理很简单,就是按照这样的优先级顺序对语法树运用规则进行优化。

              +
              merge filter nlj

              OptimizeMergeFilterNLJ为例,我们可以研究一下它的整体架构:

              +
              auto Optimizer::OptimizeMergeFilterNLJ(const AbstractPlanNodeRef &plan) -> AbstractPlanNodeRef {
              // 首先自底向上地对其所有子节点进行优化,采用DFS
              std::vector<AbstractPlanNodeRef> children;
              for (const auto &child : plan->GetChildren()) {
              children.emplace_back(OptimizeMergeFilterNLJ(child));
              }

              auto optimized_plan = plan->CloneWithChildren(std::move(children));
              // 仅当当前结点为filter,并且其唯一子节点为nlj时,才进行重写优化
              if (optimized_plan->GetType() == PlanType::Filter) {
              const auto &filter_plan = dynamic_cast<const FilterPlanNode &>(*optimized_plan);
              const auto &child_plan = optimized_plan->children_[0]; // Has exactly one child
              if (child_plan->GetType() == PlanType::NestedLoopJoin) {
              const auto &nlj_plan = dynamic_cast<const NestedLoopJoinPlanNode &>(*child_plan);
              // 这里可能简单起见,仅当nlj为纯纯的笛卡尔积时,才会进行合并
              // 所以看起来就无法处理多个连续的选择的情况,或许在planner阶段规避了这种情况?
              if (IsPredicateTrue(nlj_plan.Predicate())) {
              // 将该filter+nlj结点重写为一个新的连接结点
              return std::make_shared<NestedLoopJoinPlanNode>(
              filter_plan.output_schema_, nlj_plan.GetLeftPlan(), nlj_plan.GetRightPlan(),
              RewriteExpressionForJoin(filter_plan.GetPredicate(),
              nlj_plan.GetLeftPlan()->OutputSchema().GetColumnCount(), nlj_plan.GetRightPlan()->OutputSchema().GetColumnCount()), nlj_plan.GetJoinType());
              }
              }
              }
              return optimized_plan;
              }
              + +

              可见,对语法树运用该merge filter nlj规则是采用自底向上的顺序,并且仅合并那些filter-笛卡尔积的结点。那么接下来,我们可以具体关注RewriteExpressionForJoin的实现。

              +

              首先,我们需要明确bustub中对expression的抽象。以#0.0=#1.0为例,expression的结构树如下所示:

              +

              image-20240120104722170

              +

              每个叶子结点都是一个基本的expression类型,如column value、constant value等等等,整个子树构成一个其他expression类型,如comparation expr、arithmetic expr等等等。

              +

              在未优化前,我们是先做笛卡尔积,再做选择。故而,假设t1有2列,t2有2列,选择条件为t1.col1 = t2.col4,在未优化前,filter结点的expr将为:#0.0=#0.3(两表经过笛卡尔积合在一起了)。故而,在RewriteExpressionForJoin函数中,我们需要根据t1表和t2表分别的列数,将#0.0=#0.3这样的表达式转化为#0.0=#1.1这样的表达式(其实也就是只用处理所有类型为colum expr的叶结点即可)。而由于expression是递归结构,所以我们需要先针对其所有子节点进行处理。故而,RewriteExpressionForJoin的实现如下:

              +
              auto Optimizer::RewriteExpressionForJoin(const AbstractExpressionRef &expr, size_t left_column_cnt, size_t right_column_cnt) -> AbstractExpressionRef {
              // 首先自底向上地对其所有子节点进行优化,采用DFS
              std::vector<AbstractExpressionRef> children;
              for (const auto &child : expr->GetChildren()) {
              children.emplace_back(RewriteExpressionForJoin(child, left_column_cnt, right_column_cnt));
              }
              // 仅对那些类型为column expr的叶子结点进行处理
              if (const auto *column_value_expr = dynamic_cast<const ColumnValueExpression *>(expr.get()); column_value_expr != nullptr) {
              // #0.1, "0"为tuple_idx,"1"为col_idx
              // 此时tuple_idx一定是0,因为filter结点只有一个子节点
              BUSTUB_ENSURE(column_value_expr->GetTupleIdx() == 0, "tuple_idx cannot be value other than 0 before this stage.")
              auto col_idx = column_value_expr->GetColIdx();
              if (col_idx < left_column_cnt) {
              return std::make_shared<ColumnValueExpression>(0, col_idx, column_value_expr->GetReturnType()); // 替换为#0.X
              }
              if (col_idx >= left_column_cnt && col_idx < left_column_cnt + right_column_cnt) {
              return std::make_shared<ColumnValueExpression>(1, col_idx - left_column_cnt, column_value_expr->GetReturnType()); // 替换为#1.X
              }
              throw bustub::Exception("col_idx not in range");
              }

              // xiunian: do nothing if the filter contains no column value expression
              return expr->CloneWithChildren(children);
              }
              + +

              实现

              +

              Specifically, the hash join algorithm can be used when a join predicate is a conjunction of equi-conditions between two columns. For the purpose of this project, handling a single equi-condition, and also two equi-conditions connected by AND, will earn full credit.

              +
              +

              看完了merge filter nlj的实现之后,本次任务的实现就变得不那么困难了。

              +

              当一个nlj的predicate条件是一堆使用AND连接的“=”expr,我们就可以将该nlj转化为hash join。而OptimizeNLJAsHashJoin作用于OptimizeMergeFilterNLJ之后,故而,我们可以直接对所有的nlj结点进行判定重写。

              +

              具体来说,我们可以首先实现一个函数CheckIfEquiConjunction,给定expr结构树输入,判断其是否只由AND、”=”、”column expr”构成。这个过程还需要做一件事,就是分离出hash join所需要的key expression,如nlj的连接条件为#0.1=#1.2 AND #1.1=#0.2,则最后形成的hash join为left_key_expr=[#0.1, #0.2], right_key_expr=[#1.2, #1.1]

              +

              然后,在OptimizeNLJAsHashJoin函数主体中,我们只需遍历语法树的所有结点,然后对其进行判定,符合条件则将其转化为hash join即可。

              +

              Task #3 - Sort + Limit Executors and Top-N Optimization

              +

              You will finally implement a few more common executors, completing your implementation in the following files:

              +
                +
              • src/execution/sort_executor.cpp
              • +
              • src/execution/limit_executor.cpp
              • +
              • src/execution/topn_executor.cpp
              • +
              • src/optimizer/sort_limit_as_topn.cpp
              • +
              +

              You must implement the IndexScanExecutor in Task #1 before starting this task. If there is an index over a table, the query processing layer will automatically pick it for sorting. In other cases, you will need a special sort executor to do this.

              +

              For all order by clauses, we assume every sort key will only appear once. You do not need to worry about ties in sorting.

              +
              +

              Sort

              +

              If a query’s ORDER BY attributes don’t match the keys of an index, BusTub will produce a SortPlanNode for queries such as:

              +
              EXPLAIN SELECT * FROM __mock_table_1 ORDER BY colA ASC, colB DESC;
              + +

              如果要求排序的key不是index key,就会用到这个sort executor。

              +

              This plan node has the same output scheme as its input schema. You can extract sort keys from order_bys, and then use std::sort with a custom comparator to sort the child node’s tuples. You may assume that all entries in a table will fit entirely in memory.

              +

              If the query does not include a sort direction (i.e., ASC, DESC), then the sort mode will be default (which is ASC).

              +
              +

              一些想法

              comparator实现
              +

              我有一个类Tuple,另一个类Executor。我想实现一个Tuple的比较函数,但需要用到类Executor的成员变量,那么我该怎么写一个可以用于std::sort的cmp函数

              +
              +

              最终给出的提示是这样的,实现一个函数对象

              +
              struct CompareTuplesByOrder {
              Schema schema_;
              // add any new member

              CompareTuplesByOrder(Schema schema, const std::vector<std::pair<OrderByType, AbstractExpressionRef>>& order_by) : schema_(schema) { }

              // override the "()" operator
              bool operator()(const Tuple &t1, const Tuple &t2) const {
              // do any logic
              }
              };

              // use in sort
              std::sort(tuples_.begin(), tuples_.end(), CompareTuplesByOrder(GetOutputSchema(), plan_->GetOrderBy()));
              + +

              可以看到,其本质是通过重载”()”运算符来实现的,感觉是一个很有意思的trick。

              +

              实现

              它提示的实现思路很简单,就是大概从sort plan node获取所有key,然后用std:sort即可,默认升序,并且所有entry都是in-memory的。

              +

              有一点值得注意的是,在sql语言中,排序是可以指定多个关键词+不同顺序(关键词出现顺序表明排序优先级)的,如order by col1 ASC, col3 DESC。所以我们需要在comparator实现中按照优先级(也即order_by_数组顺序)一步步比较。

              +

              比较难的地方大概还是c++知识,也即如何为std:sort实现一个较为复杂的comparator。具体操作可见一些想法-comparator实现

              +

              Limit

              +

              The LimitPlanNode specifies the number of tuples that query will generate. Consider the following example:

              +
              EXPLAIN SELECT * FROM __mock_table_1 LIMIT 10;
              + +

              The LimitExecutor constrains the number of output tuples from its child executor. If the number of tuples produced by its child executor is less than the limit specified in the plan node, this executor has no effect and yields all of the tuples that it receives.

              +

              This plan node has the same output scheme as its input schema. You do not need to support offsets.

              +
              +

              挺简单的,就是限制输出的数量,没什么好说的。

              +

              Top-N Optimization Rule

              +

              Finally, you should modify BusTub’s optimizer to efficiently support top-N queries. (These were called top-K queries in class.) Consider the following:

              +
              EXPLAIN SELECT * FROM __mock_table_1 ORDER BY colA LIMIT 10;
              + +

              By default, BusTub will plan this query as a SortPlanNode followed by a LimitPlanNode. This is inefficient because a heap can be used to keep track of the smallest 10 elements far more efficiently than sorting the entire table.

              +

              Implement the TopNExecutor and modify the optimizer to use it for queries containing ORDER BY and LIMIT clauses.

              +

              An example of the optimized plan of this query:

              +
              TopN { n=10, order_bys=[(Default, #0.0)]} | (__mock_table_1.colA:INTEGER, __mock_table_1.colB:INTEGER)
              MockScan { table=__mock_table_1 } | (__mock_table_1.colA:INTEGER, __mock_table_1.colB:INTEGER)
              + +

              Hint: See OptimizeSortLimitAsTopN for more information, and check the Optimizer Rule Implementation Guide for details on implementing an optimizer rule.

              +

              Note: At this point, your implementation should pass SQLLogicTests #16 to #19. Integration-test-2 requires you to use release mode to run.

              +
              +

              感觉标准做法应该是使用快速排序的partion思想。但是这里的话,我打算使用另一个实现更简单的思路,也即维护一个元素个数为limit_的有序set,每次插入元素同其最小值比较即可,这样的时间复杂度为O(nlogk)。当k不会太大的时候,我觉得这样应该还是会比快排要快些的。

              +

              小插曲

              本来写到这准备开开心心提交了,突然发现自己的版本似乎跟仓库最新不大一样。rebase了感觉一下午(最后甚至还找了个以前写的小bug……),最后才终于提交完获得了full score……

              +

              image-20240121200829707

              +

              bustub仓库中的每个课程版本都是有这样的小tag了,一开始没发现直接大力出奇迹rebase最新,结果整了半天人麻了。。。

              +

              image-20240121200726718

              +

              Leaderboard Task

              TODO

              +]]>
              +
              + + Project2 B+Tree + /2023/03/13/cmu15445$lab2/ + +

              参考

              +

              CMU 15-445 Project 2 (Spring 2023) | 关于 B+Tree 的十个问题

              +

              对crabbing lock、乐观锁做了详尽解释

              + +

              Project2 B+Tree

              +

              In this programming project you will implement a B+Tree index in your database system.

              +

              Your implementation will support thread-safe search, insertion, deletion (including splitting and merging nodes包括分裂和合并结点), and an iterator to support in-order leaf scans.

              +
              +

              它这里的B+树(以及wiki里的)跟王道考研讲得不大一样。王道考研的B+每个结点一个关键字对应一个child,但是这里的是B树的形式。

              +

              undefined

              +

              image-20230417181239196

              +

              Task1 B+Tree Pages

              +

              You must implement three Page classes to store the data of your B+Tree:

              +
                +
              1. B+Tree Page BPlusTreePage

                +

                下面那两个的基类

                +
              2. +
              3. B+Tree Internal Page

                +

                An Internal Page stores m ordered keys and m+1 child pointers (as page_ids) to other B+Tree Pages.These keys and pointers are internally represented as an array of key/page_id pairs.

                +

                Because the number of pointers does not equal the number of keys, the first key is set to be invalid, and lookups should always start with the second key.

                +

                At any time, each internal page should be at least half full.【min_size<= <=max_size】

                +

                During deletion, two half-full pages can be merged, or keys and pointers can be redistributed to avoid merging. During insertion, one full page can be split into two, or keys and pointers can be redistributed to avoid splitting.

                +
              4. +
              5. B+Tree Leaf Page

                +

                The Leaf Page stores m ordered keys and their m corresponding values. In your implementation, the value should always be the 64-bit record_id for where the actual tuples are stored; see the RID class, in src/include/common/rid.h.

                +

                *Note:* Even though Leaf Pages and Internal Pages contain the same type of key, they may have different value types. Thus, the max_size can be different.

                +
              6. +
              +
              +

              大概就是有一个基类结点,它有两个子类,一个表示b+树的leaf node,另一个表示b+树的internal node,每个结点都占据一个内存页。

              +

              也就是说,一个内存页中存储着一个结点类对象。每次我们都是读取一页到内存中,然后将它类型转换为TreeNodePage*,就可以访问其里面的存储数据的数组array_了。体会一下这个思想。

              +

              值得一提的是,LeafPage的成员变量中有个这样的成员:

              +
              private:
              // Flexible array member for page data.
              MappingType array_[0];
              + +

              它就是柔性数组成员。

              +
              +

              在C++中,Flexible Array Member(柔性数组成员)是一种用于定义具有可变大小的结构的技术。它通常用于在结构的末尾声明一个数组,该数组的大小是动态确定的,这允许你在使用该结构时更灵活地处理变长数据。

              +

              在你提供的代码片段中,MappingType array_[0]; 是一个柔性数组成员的例子。这里 array_ 后面有 [0],这并不表示它们的大小是固定的0。相反,它们的大小是在运行时动态确定的,而 [0] 的写法是一种历史上的技巧,用于告诉编译器这是柔性数组成员。

              +

              例如,如果有一个结构定义如下:

              +
              struct MyStruct {
              // 其他成员...
              MappingType array_[0];
              };
              + +

              你可以根据需要为 array_ 分配任意数量的内存,例如:

              +
              int arraySize = 10;  // 你想要的数组大小
              MyStruct* myObject = static_cast<MyStruct*>(operator new(sizeof(MyStruct) + arraySize * sizeof(MappingType)));

              // 在这里你可以使用 myObject,并通过 myObject->array_ 访问柔性数组

              // 记得在使用完毕后释放内存
              operator delete(myObject);
              + +

              在这个例子中,array_ 可以用于存储可变大小的数据,而结构体 MyStruct 的大小将动态地调整为 sizeof(MyStruct) + arraySize * sizeof(MappingType)。这样的设计通常在需要处理变长数据块的场景中比较有用。请注意,在C++17之后,你也可以使用 std::byte 类型来定义柔性数组成员。

              +
              +

              拥有柔性数组成员的实例需要动态分配内存(或者像接下来的把一块内存空间interpret一下),柔性数组成员会占用其他成员没有占用的剩下的空间,也即:

              +
              +--------------------------+
              | Other Members of MyClass |
              | ... |
              +--------------------------+
              | array_ (flexible) |
              | |
              | |
              +--------------------------+
              + + + +

              Task2a Insertion and Search + Task3 Iterator

              +

              The index should support only unique keys; if you try to reinsert an existing key into the index, it should not perform the insertion, and should return false. key必须unique

              +

              B+Tree pages should be split (or keys should be redistributed) if an insertion would violate the B+Tree’s invariants. 插入时需要分裂

              +

              If an insertion changes the page ID of the root, you must update the root_page_id in the B+Tree index’s header page. You can do this by accessing the header_page_id_ page, which is given to you in the constructor. Then, by using reinterpret_cast, you can interpret this page as a BPlusTreeHeaderPage (from src/include/storage/page/b_plus_tree_header_page.h) and update the root page ID from there. You also must implement GetRootPageId, which currently returns 0 by default.对root_page_id的一切访问,都需要通过header_page_id_。如果插入后改变了root的page ID,需要更新root_page_id

              +

              We recommend that you use the page guard classes from Project 1 to help prevent synchronization problems. For this checkpoint, we recommend that you use FetchPageBasic (defined in src/include/storage/page/) when you access a page. 在当前task中,我们推荐你使用pro1实现的page guard,比如说这里如果要访问一页,就需要用 FetchPageBasic

              +

              You may optionally use the Context class (defined in src/include/storage/index/b_plus_tree.h) to track the pages that you’ve read or written (via the read_set_ and write_set_ fields) or to store other metadata that you need to pass into other functions recursively.你可以随意使用和修改 Context class,它大概就是一个存储共享信息的对象。

              +

              If you are using the Context class, here are some tips:如果你要用,要注意以下几点:

              +
                +
              • You might only need to use write_set_ when inserting or deleting. 当你在为B+树插入/删除结点时,需要用到write_set_。【为什么?这个set存储的是修改路径上的结点吗?然后如果要分裂/合并结点,只需什么while(pop且需要分裂/合并){分裂/合并}??所以说这里的deque是栈结构?】

                +

                也就是说,其实我们就可以不用递归了,而是将上下文存储在context->write_set_这个栈里面就行了?大概是这个意思吧

                +

                It is possible that you don’t need to use read_set_, depending on your implementation.

                +

                read可以用递归(比较简单)也可以不用,所以说具体看实现。

                +
              • +
              • You might want to store the root page id in the context and acquire write guard of header page when modifying the B+Tree.你需要将root page id存储在context,并且在修改b+树(插入、删除)时获取header page的WritePageGurad。

                +
              • +
              • To find a parent to the current node, look at the back of write_set_. It should contain all nodes along the access path.如果想要寻找当前node的父亲,可以看看write_set_.back,它包含了访问路径上所有结点的引用【所以确实是当成栈来用了】

                +
              • +
              • You should use BUSTUB_ASSERT to help you find inconsistent data in your implementation. 需要使用 BUSTUB_ASSERT

                +

                For example, if you want to split a node (except root), you might want to ensure there is still at least one node in the write_set_. If you need to split root, you might want to check if header_page_ is std::nullopt.

                +

                如果你想要分割一个根节点以外的node,那你必须保证write_set_中至少有一个结点;如果你想要分割根节点,那你必须保证header_page_非空。

                +
              • +
              • To unlock the header page, simply set header_page_ to std::nullopt. To unlock other pages, pop from the write_set_ and drop.如果你想要不锁住header page,那就置其为空指针;如果想释放别的页,那就将它从 write_set_ pop出来就行。【这是因为我们要用到的page类型都是page guard,可以析构时unpin吗?】

                +
              • +
              +
              +

              感想

              由于各种原因,lab2的战线还是拉得太长了。四月份完成了代码初版,中间修了几个bug勉强通过了insertion test,然后一直到十一月底的现在才再次捡起来。不得不说,回看当初的代码,还是能够很清晰地感受到自己这半年多来的成长的,令人感慨。

              +

              我先是花了一天的时间重构了下以前写的所有代码,然后再花了两天时间修bug终于通过了insertion test和sequence scale test,并且将b+树的代码修到了我满意的地步(指不像以前那样一坨重复代码和中文注释。。。)。

              +

              思路

              这里简要介绍下B+树的插入实现及我觉得实现中需要注意的几个要点吧。

              +

              B+树的插入流程大概是这样的:

              +
                +
              1. 查找到key要插入的叶子结点(途中需要维护write_set,也即查找路径)

                +
              2. +
              3. 判断结点是否满

                +
                  +
                1. 未满,直接插入即可。(我采取插入排序的方法)

                  +
                2. +
                3. 已满,需要对结点进行分裂。

                  +

                  推举出中间结点tmp_key,它和新结点page_id接下来将插入到父节点中。

                  +
                4. +
                +
              4. +
              5. 持续进行分裂:

                +

                需要注意具体的分裂方法,我认为其中internal page size == 3的情况尤为棘手。在具体实现中,我是这样分裂的:

                +
                  +
                1. 推举出将要被插入到父节点的tmp_key

                  +

                  该推举出的key将不会出现在分裂后的新旧结点中,而是会被加入到父节点中。默认为(m + 1) / 2【m为max size】。

                  +

                  但是要尤其注意size为3的case,此时tmp_key为array_[2],很有可能右边结点为空。所以我们需要做点特殊处理:

                  +
                    +
                  1. 当要插入到该节点的insert_key > array_[(m + 1) / 2]时,我们推举(m + 1) / 2这个结点。
                  2. +
                  3. insert_key < array_[m / 2],我们转而推举m / 2(此时为array_[1])。
                  4. +
                  5. insert_key < array_[(m + 1) / 2]insert_key > array_[m / 2]时,我们应该对此做出特殊处理,推举insert_key。在此为了代码实现方便,我们还需要调换insert_key和tmp_key的地位
                  6. +
                  +
                  special = false;
                  middle = (m + 1) / 2;
                  tmp_key = root->KeyAt(middle);
                  insert_small_than_tmp_key = (comparator_(insert_key, tmp_key) < 0);
                  if (insert_small_than_tmp_key) {
                  middle = m / 2;
                  tmp_key = root->KeyAt(middle);
                  if (comparator_(insert_key, tmp_key) >= 0) {
                  special = true;
                  swap = insert_key;
                  insert_key = tmp_key;
                  tmp_key = swap;
                  }
                  }
                2. +
                3. 分裂旧结点

                  +

                  被推举出的tmp_key的value及其右部元素会变成新结点,左部依然留在旧结点,tmp_key会到父节点中去。也即如下图所示:

                  +

                  ![未命名文件 (1)](./cmu15445/未命名文件 (1).png)

                  +

                  依然是注意上面那个case3特殊情况,需要交换insert key和middle key:

                  +
                  if (!special)
                  new_page->SetValueAt(0, root->ValueAt(middle));
                  else {
                  new_page->SetValueAt(0, insert_val);
                  insert_val = root->ValueAt(middle);
                  }
                4. +
                5. 持续进行推举和分裂,直到父节点不用分裂

                  +

                  此时直接将insert key和insert value插入排序到父节点即可。

                  +
                6. +
                +
              6. +
              +

              然后是Iterator的话,我感觉这也是设计得很不错,让我们亲手写了下c++的重载运算符,也是让我学到了很多c++知识。。。

              +

              遇到的问题

              感觉问题其实不多,主要还是debug有点痛苦花了很长时间()

              +

              cmake报错

              切换内核前后报错。

              +

              Check for working C compiler: /usr/bin/cc - broken

              +

              感觉可能是内核切来切去,导致cmake cache发生了点小问题?总之我最后在5.11内核把build文件删了,重新执行cmake -DCMAKE_CXX_COMPILER=$(which g++) -DCMAKE_C_COMPILER=$(which gcc) ..就ok了。

              +

              page guard

              用错了

              image-20230505002652748

              +

              我发现在这里创建的root最后好像会被释放掉?

              +

              比如我看到新root的page为6,连接也做得好好的,最后出了函数就寄了:

              +

              image-20230505002731312

              +

              还有一个是发现新的leaf page好像不大对,其类型甚至是internal呃呃,我调下看看

              +

              尼玛,绷不住了是这里:

              +

              image-20230505011731797

              +

              原来写的

              +

              image-20230505011744924

              +

              改了之后test2马上ok,乐

              +
              作用域

              还弄了个commit修:

              +

              image-20231130222702358

              +

              一点c++引用震撼

              auto INDEXITERATOR_TYPE::operator*() -> const MappingType &
              + +

              这个函数卡了我还挺久。。。里面逻辑很简单,不过难就难在怎么构造出一个const MappingType &

              +

              如果这样:

              +
              INDEX_TEMPLATE_ARGUMENTS
              auto INDEXITERATOR_TYPE::operator*() -> const MappingType & {
              auto page = guard_.As<LeafPage>();
              return std::pair<KeyType, ValueType>(page->KeyAt(cnt_), page->ValueAt(cnt_));
              // or use make_pair. the same result
              }
              + +

              会说你临时对象不能作为引用。如果这样:

              +
              INDEX_TEMPLATE_ARGUMENTS
              auto INDEXITERATOR_TYPE::operator*() -> const MappingType & {
              auto page = guard_.As<LeafPage>();
              auto res = new MappingType(std::pair<KeyType, ValueType>(page->KeyAt(cnt_), page->ValueAt(cnt_)));
              return *res;
              }
              + +

              又会找不到机会delete导致内存泄漏。冥思苦想了半天不知道该怎么办,最后从网上看了别人怎么写的:

              +
              INDEX_TEMPLATE_ARGUMENTS
              auto INDEXITERATOR_TYPE::operator*() -> const MappingType & {
              auto page = guard_.As<LeafPage>();
              return page->PairAt(cnt_);
              }

              INDEX_TEMPLATE_ARGUMENTS
              auto B_PLUS_TREE_LEAF_PAGE_TYPE::PairAt(int index) const -> const MappingType & {
              return array_[index];
              }
              + +

              我服了。

              +

              不过可能有更好的解决方法?可惜我c++水平不大够,所以暂时想不出来了。

              +

              Task4 Remove

              感想

              由于有了insert的沉淀,remove的实现便相较不大困难了,写完代码到通过内置的delete测试只花了一天的时间。

              +

              思路

                +
              1. 找到需要操作的叶结点路径

                +
              2. +
              3. 判断叶子结点属于以下四种策略中的哪一种,执行对应策略(优先级从高到低):

                +
                  +
                1. 直接删除

                  +

                  当删除后叶结点元素数仍在合法范围,并且路径上父节点没有target key,直接删除然后返回即可。

                  +
                2. +
                3. 更新父节点路径

                  +

                  当删除后叶结点元素数仍在合法范围,并且路径上父节点target key,直接删除然后向上回溯更新父节点即可。

                  +
                4. +
                5. 窃取兄弟元素

                  +
                  If do a steal, we should update related key in the parent, and update up till reaching the root.

                  /*
                  For that steal is more simple, we first check whether it can do a steal first.
                  We steal the node whose size is biggest between the next and the prev node.
                  If the prev size is bigger, we only update self key in parent.
                  If the next size is bigger, we update both self key and next key n parent.
                  After that, we trace back and update all the parent nodes which contains the
                  target key.
                  */
                  + +

                  当删除后叶结点元素数过少,并且左右兄弟元素充足,则从左右兄弟窃取一个。优先窃取元素最多者。

                  +
                    +
                  1. 窃取左兄弟

                    +

                    窃取左兄弟的最大元素

                    +

                    需递归更新自身父节点路径上的对应值。

                    +
                  2. +
                  3. 窃取右兄弟

                    +

                    窃取右兄弟的最小元素

                    +

                    需要递归更新自身和右兄弟父节点路径上的对应值。

                    +
                  4. +
                  +

                  之后返回即可。

                  +
                6. +
                7. 合并

                  +
                  /*
                  Need to merge with one of the node. It is more simple to try to merge the left node
                  first. So the strategy:
                  1. Pick the prev node to merge. (If leaf is most left, pick next node)
                  2. Update delete-key. (for prev, it's leaf[0]; for next, it's right key, and need
                  to update self)
                  3. Go up till reaching root. Do:
                  1. delete delete-key.
                  2. pick merging or stealing like above.
                  1. if merge, update delete-key, go up;
                  2. if steal, break to do update and has no need to go up.
                  4. Remember to deal with edge case: root.
                  */
                  + +

                  当删除后叶结点元素数过少,并且左右兄弟元素也都是最小值,那么需要与左右兄弟之一进行合并。优先合并左兄弟。合并都为大->小,也即target->左兄弟 或者 右兄弟->target。

                  +

                  需要递归删除父节点路径上的merge from元素。

                  +
                8. +
                +
              4. +
              5. 可以看到,1/2/3三种情况都可以实现简单地直接返回。4稍显复杂,由于递归删除,所以需要对每一个父节点都再次进行上面几种策略的判断,直到遇到情况123返回为止。

                +
              6. +
              +

              遇到的问题

              一个比较sb的小bug……

              +

              image-20231204163052528

              +

              Task5 Concurrency

              感想

              这位可更是重量级,足足花了我三天的时间……不过感觉第一次处理这么一个复杂的并发情景,花的时间还是值得的。

              +

              最后的结果虽然很一般(指排行榜倒数水平。。。),但至少还是过了。就先这样吧。

              +

              image-20231204170030245

              +

              思路

              我实现了crabbing lock+optimal lock。对于Insert和Remove,都是先在一开始获取header page和路径上父节点的读锁,然后在之后有可能向上更新时(比如说Insert的需要分裂、Remove的Update和Merge两种情况),丢弃所有读锁,然后获取header page和路径上父节点的写锁。

              +

              不过感觉我这个思路还是略有粗糙,因为相当一部分时间都得占用header page的写锁。但是我思考了一下细粒度方案,发现还是有点难实现。比如说,对于insert,细粒度化的方式也许就是一直持有header page的读锁,一直到需要分裂根节点时,才释放读锁获取写锁。但这样一来就会暴露一个危险的空窗期(而且感觉这个空窗期还不小),当你真的拿到写锁,这树的结构可能已经变得不知道什么样了。在这种情况下,你就需要再做一次回溯工作,也即获取从新root结点到旧root结点的路径,递归插入insert key和insert value,最后安全分裂根节点(因为此时已经安全持有了header page写锁)。感觉思路也是比较易懂,但是实现上还是太麻烦了,所以先暂且搁置吧。

              +

              遇到的问题

              这种感觉大多还是在面向测试用例见招拆招……所以其实感觉没什么好说的。

              +

              bpm遗留

              这个并发问题是这样的,我原来是先evict,然后再写回被替换的页面,写回过程中磁盘没加bpm锁。这就会出现这样一个情况:

              +

              一个page被进程A evict,进程A还没执行写回的时候这个page又被进程B捡回来了,因为还没写入所以磁盘空空如也。这时候pages_latch_这个细粒度锁不能防范这种情况,是因为此时这个page对应的container不是同一个,所以fid不同,细粒度锁不同导致寄。

              +

              解决方法是要么写的时候持有bpm锁,但是这太太慢了。另一个就是干脆直接在unpin的时候不带bpm锁顺便写回了。也即把写回从evict后移到unpin中立即写回:

              +
              if (pages_[fid].GetPinCount() == 0 && pages_[fid].IsDirty()) {
              pages_[fid].is_dirty_ = false;
              disk_manager_->WritePage(pages_[fid].GetPageId(), pages_[fid].GetData());
              }
              + +

              火焰图性能分析

              +

              FlameGraph

              +

              参考博客

              +
              +

              看起来感觉大多性能损耗还是在bpm上,特别是LRU-K。也许是我的全局锁太暴力了。

              +

              out

              ]]>
              - Lab0 - /2023/02/25/cs144$lab0/ - Lab0
              -

              本次实验一直在强调的一点就是,TCP的功能是将底层的零散数据包,拼接成一个reliable in-order的byte stream。这个对我来说非常“振聋发聩”(夸张了233),以前只是背诵地知道TCP的可靠性,这次我算是第一次知道了所谓“可靠”究竟可靠在哪:一是保证了序列有序性,二是保证了数据不丢失(从软件层面)。

              -

              还有一个就是大致了解了cs144的主题:实现TCP协议。也就是说,运输层下面的那些层是不用管的吗?不过这样也挺恰好,我正好在学校的实验做过对下面这些层的实现了,就差一个TCP23333这样一来,我的协议栈就可以完整了。

              -
              + 编译原理 + /2023/11/18/compilation_principle/ + 第一章 绪论

              概述

              picture

              +

              可重定位的代码通过linker和loader重定位这部分内容就是在之前那本书学过的。

              +

              picture

              +

              从中,我们也可以看到有语法分析、中间代码的影子。

              +

              picture

              +

              词法分析相当于通过DFA NFA捉出各类符号,形成简单的符号表和token list;语法分析相当于对token list组词成句,判断该句子是否符合语言规则;语义分析相当于对词句进行类型判断和中间代码的生成,获得基本语义。

              +

              编译程序总体结构

              picture

              +

              picture

              +

              语法制导翻译:语义分析和中间代码生成集成到语法分析中

              +

              词法分析

              将结果转化为token的形式。

              +

              picture

              +

              picture

              +

              语法分析

              从token list中识别出各个短语,并且构造语法分析树。

              +

              picture

              +

              picture

              +

              相当于是通过文法来进行归约(自底向上的语法分析),从而判断给定句子是否合法。

              +

              语义分析

              picture

              +
                +
              1. 收集标识符的属性信息,并将其存入符号表
              2. +
              +

              picture

              +

              种属就是比如是函数还是数组之类的。

              +

              picture

              +
                +
              1. 语义检查
              2. +
              +

              picture

              +
                +
              1. 静态绑定

                +

                包括绑定代码相对地址(子程序)、数据相对地址(变量)

                +
              2. +
              +

              中间代码生成

              picture

              +

              picture

              +

              波兰也就是前序遍历二叉树(中左右),逆波兰也就是后序遍历二叉树(左右中)

              +

              picture

              +

              代码优化

              picture

              +
                +
              1. 无关机器

                +

                picture

                +
              2. +
              3. 有关机器

                +

                picture

                +
              4. +
              +

              目标代码生成

              picture

              +

              表格管理

              这也挺好理解,相当于管理符号表吧。

              +

              picture

              +

              错误处理

              picture

              +

              编译程序的组织

              了解了编译程序的基本结构,那么我们就可以想想该怎么实现这个编译器了。

              +

              最直观的想法是,我们有几个步骤就对代码进行多少次扫描:

              +
                +
              1. 首先扫一次,进行词法分析,将所有标识符写入到符号表中,同时进行语法分析,看看有没有错,如果出错了就转到错误处理,没有的话就进行语义分析;(三合一)
              2. +
              3. 然后再针对得出来的语义分析树进行中间代码生成;
              4. +
              5. 再对得出来的中间代码进行代码优化,最后对优化出来的代码进行翻译处理。(二合一)
              6. +
              +

              picture

              +

              picture

              +

              picture

              +

              实现编译器

              picture

              +

              T形图

              picture

              +

              自展

              picture

              +

              也就是说:

              +
                +
              1. P0是汇编语言,可以用来编译C语言子集;(P0:汇编语言,C子集→汇编)
              2. +
              3. P1是机器语言,可以用来把汇编语言翻译为机器语言;(P1:机器语言,汇编→机器)
              4. +
              5. 所以我们就得到了P2,也即一个可以用来编译C语言子集的机器语言程序;(P2:机器语言,C子集→汇编)
              6. +
              7. 然后我们就可以用C语言子集来写C语言编译程序P3,再用P2翻译P3,就可以得到工具P4。(P4:汇编语言,C→汇编)
              8. +
              +

              image-20230912153726618

              +

              帅的。

              +

              移植

              picture

              +

              picture

              +

              本机编译器的利用

              picture

              +

              编译程序的自动生成

              这大概是描述了我们到时候会怎么实现这两个阶段代码。

              +

              不过确实,词法分析可以看作是正则匹配,语法分析可以看作是产生式。

              +

              picture

              +

              picture

              +

              第二章 文法等概念

              image-20231111160656018

              +

              基本概念

                +
              1. 字母表

                +

                picture

                +

                picture

                +

                picture

                +

                picture

                +
              2. +
              3. +

                克林闭包中的每一个元素都称为是字母表Σ上的一个串

                +

                picture

                +

                picture

                +

                picture

                +
              4. +
              +

              文法

              picture

              +

              如果文法用于描述单词,基本符号就是字母;用于描述句子,基本符号就是单词

              +
                +
              1. 文法的形式化定义

                +

                picture

                +

                picture

                +

                由于可以从它们推出其他语法成分,故而称之为非终结符

                +

                picture

                +

                picture

                +

                还真是最大的语法成分

                +
              2. +
              3. 产生式

                +

                picture

                +
              4. +
              5. 符号约定

                +

                picture

                +

                picture

                +

                picture

                +

                文法符号串应该就是指既包含终结符也包含非终结符的,也可能是空串的串。

                +

                注意终结符号串也包括空串。

                +
              6. +
              +

              语言

              picture

              +

              这部分就是要讲怎么看一个串是否满足文法规则,那么我们就需要先从什么样的串是满足文法规则的串开始说起,也即引入“语言”的概念。

              +
                +
              1. 推导与归约

                +

                picture

                +

                然后也分为最左推导和最右推导,对应最右归约和最左归约。

                +

                picture

                +

                故而,如果从开始符号可以推导(派生)出该句子,或者从该句子可以归约到开始符号,那么该句子就是该语言的句子。

                +
              2. +
              3. 句子与句型

                +

                picture

                +

                句型就是可以有非终结符,句子就是只能有终结符

                +
              4. +
              5. 语言

                +

                picture

                +

                文法解决了无穷语言的有穷表示问题。

                +

                picture

                +

                picture

                +

                emm,就是好像没有∩运算

                +

                picture

                +

                有正则那味了

                +
              6. +
              +

              乔姆斯基文法体系

              picture

              +

              picture

              +
                +
              1. 0型

                +

                picture

                +
              2. +
              3. 1型

                +

                picture

                +

                之所以是上下文有关,是因为只有A的上下文为a1和a2时才能替换为β【666666,第一次懂】

                +

                CSG不包含空产生式。

                +
              4. +
              5. 2型

                +

                picture

                +

                左部只能是一个非终结符。

                +
              6. +
              7. 3型

                +

                picture

                +

                产生式右部最多只有一个非终结符,且要在同一侧

                +

                picture

                +

                看起来还能转(是的,自动机教的已经全忘了())

                +
              8. +
              +

              CFG

              正则文法用于判定大多数标识,但是无法判断句子构造

              +
                +
              1. 分析树
              2. +
              +

              picture

              +

              picture

              +

              也就是说,每个句型都有自己对应的分析树。那么接下来就介绍什么是句型的短语

              +

              picture

              +

              意思就是直接短语是高度为2的子树的边缘,直接短语一定是某个产生式的右部,但是产生式右部不一定是给定句型的直接短语(因为有可能给定句型的推导用不到那个产生式)

              +
                +
              1. 二义性文法
              2. +
              +

              picture

              +

              通过自定义规则消除歧义

              +

              picture

              +

              第三章 词法分析

              正则语言

              正则表达式

              picture

              +

              picture

              +

              picture

              +

              picture

              +

              最后两条值得注意

              +

              picture

              +

              正则定义

              picture

              +

              picture

              +

              picture

              +

              有穷自动机

              概述

              picture

              +

              picture

              +

              picture

              +

              picture

              +

              所以真正的终止是输入带到末尾并且指向终态

              +

              分类

              DFA

              picture

              +

              NFA

              picture

              +

              NFA与DFA转化

              picture

              +

              picture

              +

              e-NFA

              picture

              +

              e-NFA与NFA转化

              picture

              +

              词法分析相关

              识别单词的DFA

              数字

              picture

              +

              picture

              +

              66666,还能这么捏起来

              +

              picture

              +

              注释

              picture

              +

              识别token

              picture

              +

              关键字是在识别完标识符之后进行查表识别的

              +

              scanner的错误处理

              说实话没太看懂

              +

              picture

              +

              picture

              +

              picture

              +

              第四章 语法分析

              根据给定文法,识别各类短语,构造分析树。所以关键就是怎么构建分析树

              +

              自顶向下LL(1)

              概念

              可以看做是推导(派生)的过程。
              如果同一非终结符的各个产生式的可选集互不相交,就可以进行确定的自顶向下分析:

              +

              picture

              +

              这两个分析也是我们的分析方法需要解决的。

              +

              picture

              +

              picture

              +

              也就是说,在自顶向下分析时,采用的是最左推导;在自底向上分析时,最左归约和最右推导才是正道!

              +

              通用算法

              例子

              picture

              +

              大概流程应该是,有产生式就展开,然后当产生式右部有多个候选式的时候再根据输入决定。

              +

              递归下降分析

              picture

              +

              如果有多个以输入终结符打头的右部候选,那就需要逐一尝试错了再回溯,因而效率较低。

              +

              预测分析

              picture

              +

              66666,这其实就可以类似于动态规划了吧

              +

              【感觉这里也能窥见一些算法设计的思想。

              +

              仔细想想,我们在引入动态规划时,也是这个说辞:对于一些回溯问题,回溯效率太低,所以我们就可以提前通过动态规划的思想构造一个状态转移表,到时候只需从零开始按照表进行状态转移即可。

              +

              仔细想想,这不就是这里这个预测分析提出的思想吗!真的牛逼,6666

              +

              我记得KMP算法一开始也是这个思想,感觉十分神奇】

              +

              文法转换

              什么情况需要改造

              picture

              +

              picture

              +

              消除左递归

              直接左递归

              picture

              +

              这个左递归及其消除方法解释得很形象

              +

              picture

              +
              间接左递归

              picture

              +

              先转化为直接左递归

              +

              消除回溯

              picture

              +

              666666这个解读可以,感觉这个就跟:

              +

              image-20231111224823978

              +

              这个“向前看”有异曲同工之妙了。

              +

              LL(1)文法

              LL(1)文法才能使用预测分析技术。判断是否是LL文法就得看具有相同左部的产生式的select集是否相交

              +

              S_文法

              picture

              +

              S文法不包含空产生式

              +

              q_文法

              picture

              +

              也就是说,B的Follow集为{b,c},只有当输入符号为b/c时才能使用空产生式

              +

              picture

              +

              first集和follow集不交。

              +

              这下总算知道这两个是什么玩意了。也就是这样:

              +
                +
              1. 输入符号与B的First集元素匹配

                +

                直接用那个产生式

                +
              2. +
              3. 否则,看输入符号是否与Follow集元素匹配

                +
                  +
                1. +

                  若B无空产生式,报错;否则,使用B的空产生式(相当于消了一个符号但不变输入带指针)

                  +
                2. +
                3. +

                  报错

                  +
                4. +
                +
              4. +
              +

              picture

              +

              这个感觉跟first集有点像,相当于是右部只能以终结符开始的形式,所以下面的LL文法会增强定义。

              +

              当该非终结符对应的所有SELECT集不相交,就可以进行确定的自顶向下语法分析。这个思想也将贯穿下面的LL文法

              +

              picture

              +

              LL(1)文法

              picture

              +

              picture

              +

              最后,如果同一非终结符的各个产生式的可选集互不相交,就可以进行确定的自顶向下分析:

              +

              picture

              +

              picture

              +

              总结

              这几个推理下来,真是让人感觉酣畅淋漓!

              +

              确定的自顶向下分析的核心就是,给定一个当前所处的非终结符和一个输入字符[E, a],我们可以唯一确定一个产生式P用于构建语法分析树。

              +

              picture

              +

              也即,同一个非终结符的所有产生式的SELECT集必须是不交的【才能确保选择产生式的唯一性】。因而,问题就转化为了如何让SELECT集不交

              +

              我们需要对空产生式和正常产生式的SELECT集计算做一个分类讨论。

              +
                +
              1. 空产生式

                +

                由于可以推导出空,相当于把该符号啥了去读下一个符号,因此我们的问题就转化为输入字符a是否能够跟该符号后面紧跟着的字符相匹配。而紧跟着的字符集我们将其成为FOLLOW集,如果a在follow集中,那么就可以接受,否则不行。

                +

                对于LL(1)文法,相当于是进一步处理了简介推出空的串:

                +

                ​ 由于α串->*空,则α串必定仅由非终结符构成。那么它能推导出的所有可能即为SELECT集。故而为First(α)∪Follow(α)

                +
              2. +
              3. 非空产生式

                +

                很简单,就是其First集。

                +
              4. +
              +

              故而,只需要让这些计算出来的First集合不交,就能进行确定的自顶向下语法分析,构造确定的语法分析树。不得不说真的牛逼。

              +

              感觉其“预测分析”的“预测”主要体现在对空产生式的处理上。

              +

              总算懂了为什么LL(1)能够解决这个回溯效率太低的问题了,太牛逼。不过问题是怎么转化为LL(1)呢()上面的消除回溯和左递归只是一部分而已吧。

              +

              预测分析法

              picture

              +

              这个消除二义性是啥玩意?二轮的时候看看PPT怎么讲的

              +

              递归的预测分析

              picture

              +

              picture

              +

              66666,它这个计算follow集的方法就很直观

              +

              declistn有个空产生式,那么我们看得看②,而②的declistn排在最后,也就是说declistn的follow集就是其左部declist的follow集【6666】,所以我们看①,可以发现declist后面为:。

              +

              picture

              +

              如果是终结符,就直接==比较;非终结符,就把token传入到其对应的过程。

              +

              非递归的预测分析

              picture

              +

              66666

              +

              感觉从中又能窥见动态规划的同样思想了。下推自动机其实感觉就像是递归思想(或者说顺序模拟递归,因为它甚至有一个栈,出栈相当于达成条件递归return),动态规划的话可能有点像是把每个不同状态以及不同状态时的栈顶元素整成一个2x2的表,所以感觉思想类似。

              +

              picture

              +

              注意,是栈顶跟输入一样都是非终结符才会移动指针和出栈

              +

              值得注意的是,输出的产生式序列就对应了一个最左推导。

              +

              picture

              +

              picture

              +

              错误处理

              picture

              +

              picture

              +

              picture

              +

              其实也挺有道理,栈顶是非终结符,但是输入是它的follow集,那我们自然而然可以想到把这b赶跑,看看下面有没有真的它的follow集在嗷嗷待哺。

              +

              自底向上语法分析

              概述

              正确识别句柄是一个关键问题。

              +

              句柄:当前句型的最左直接短语。【最左、子树高度为2】

              +

              自底向上

              picture

              +

              picture

              +

              每次句柄形成就将它归约,因而保证一直是最左归约(recall that,句柄一定是某个产生式的右部,并且每次最左句柄一旦形成就归约)

              +

              picture

              +

              正如上面的LL分析,每次推导要选择哪个产生式是一个问题;这里的LR分析,每次归约要选择哪个产生式,也即正确识别句柄,也是一个关键问题。

              +

              所以,我们应该把句柄定义为当前句型的最左直接短语。

              +

              如下图所示,左下角是当前句型(画红线部分)的语法分析树,红字为在栈中的部分,蓝字为输入符号串剩余部分。当前句型的直接短语(相当于根节点的高度为二的子树,或者说子树前两层)有两个,一个是以<IDS>为根节点的<IDS> , iB,另一个是<T>为根节点的real

              +

              picture

              +

              而LR分析技术的核心就是正确地识别了句柄

              +

              LR文法

              picture

              +

              也就是说LR技术就是用来识别句柄的,识别完了句柄就可以构建类似自顶向下的预测分析那样的自动机表来进行转移。

              +

              picture

              +
                +
              1. 移进状态

                +

                ·后为终结符

                +
              2. +
              3. 待约状态

                +

                ·后为非终结符

                +
              4. +
              5. 归约状态

                +

                ·后为空

                +
              6. +
              +

              picture

              +

              picture

              +

              以前感觉一直很难理解GOTO表的作用,现在感觉稍微明白了点了,你想想,归约之后的那个结果是不是有可能是另一个产生式的右部成分之一,也即一个新的句柄?并且这个也是由你栈顶刚归约好的那个左部和下面的输入符号决定的。那么你自然而然需要切换一下当前状态,以便之后遇到那个产生式的时候能发现到了。

              +

              那么,剩下的问题就是如何构造LR分析表了:

              +

              picture

              +

              算符分析

              picture

              +

              也就是它会整一个终结符之间的优先级关系。。。

              +

              picture

              +

              picture

              +

              也就是说:

              +
                +
              1. a=b

                +

                相邻

                +
              2. +
              3. a<b

                +

                也即在A->aB时,b在FIRSTOP(B)中(理解一下,这个First指在前面。。。)

                +
              4. +
              5. a>b

                +

                也即在A->Bb时,a在LASTOP(B)中(理解一下,这个LAST指在后面。。。)

                +
              6. +
              +

              picture

              +

              picture

              +

              我服了

              +

              picture

              +

              picture

              +

              好像#这个固定都是,横的为左,竖的为右

              +

              picture

              +

              根据优先关系来判断移入和归约

              +

              picture

              +

              LR分析

              LR(0)

              每个分析方法其实都对应着一种构造LR分析表的方法。
              LR(0)通过构造规范LR0项集族,从而构造LR分析表,从而构造LR0 DFA来最终进行语法分析。

              +

              每一个项目都对应着句柄识别的一个状态。

              +

              picture

              +

              picture

              +

              picture

              +

              picture

              +

              而肯定不可能整那么多个状态,所以我们需要进行状态合并。(这样也就很容易理解LR的状态族构建了。)

              +

              picture

              +

              它这里也很直观解释了为什么点遇到非终结符就需要加入其对应的所有产生式,因为在等待该非终结符就相当于在等待它的对应产生式的第一个字母。

              +

              picture

              +

              picture

              +

              上面这东西就是这个所谓的规范LR(0)项集族了。

              +

              picture

              +

              picture

              +

              但是会产生移进归约冲突:

              +

              picture

              +

              picture

              +

              还有归约归约冲突:

              +

              picture

              +

              所以我们就把没有冲突的叫LR(0)文法。

              +

              image-20231112165527201

              +

              感觉上述两个问题都是因为有公共前缀【包括空产生式勉强也能算是这个情况】,导致信息不足无法判断应该怎么做,多读入一个字符(也即LR(1))应该可以有效解决该问题。

              +

              SLR分析

              其实本质还是识别句柄问题,也即此时是归约还是移入,得看是不是句柄。故而LR0信息已经不能帮我们识别句柄了。

              +

              picture

              +

              Follow集可以帮助我们判断。由该状态I2可知,输入一个*应该跳转到I7。如果在I2把T归约为一个E,由Follow集可知E后面不可能有一个*,也就说明在这里进行归约是错误的,应该进行移入。

              +

              这种依靠Follow集和下一个符号判断的思想,就会运用在SLR分析中。

              +

              picture

              +

              picture

              +

              picture

              +

              但值得注意的是SLR分析的条件还是相对更严苛,它要求移进项目和归约项目的Follow集不相交,所以它也会产生像下图这样的冲突:

              +

              picture

              +

              LR(1)

              picture

              +

              SLR将子集扩大到了全集,显然进行了概念扩大。

              +

              含义为只有当下一个输入符号是XX时,才能运用这个产生式归约。这个XX是产生式左部非终结符的Follow子集。

              +

              picture

              +

              这玩意只有归约时会用到,这个很显然,毕竟前面提到的LR0的问题就是归约冲突。

              +

              picture

              +

              对了,值得注意的是这个FIRST(βa),它表示的并不是FIRST(a)∪FIRST(β),里面的βa应该取连接意,也即,当β为非空时这玩意等于FIRST(β),当β空时这玩意等于FIRST(a)

              +

              picture

              +

              刚刚老师对着这个状态转移图进行了一番强大的看图写话操作,我感觉还是十分地牛逼。她从这个图触发,讲述了状态I2为什么不能对R->L进行归约。

              +

              假如我们进行了归约,那么我们就需要弹出状态I2回到I0,压入符号R,I0遇到符号R进入了I3,I3继续归约回到I0,I0遇到符号S到状态I1,但1是接收状态,下一个符号是=不是$,所以错了。

              +

              picture

              +

              picture

              +

              比如说I8和I10就是同心的。左边的那个实际上是LR0项目集,所以这里的心指的是LR0。

              +

              picture

              +

              LALR分析

              然而,LR(1)会导致状态急剧膨胀,影响效率,所以又提出了个LALR分析。

              +

              picture

              +

              picture

              +

              跟前面的SLR对比可以发现,相当于它就是多了个逗号后面的条件。但是这是可以瞎合的吗?不会出啥问题不。。。

              +

              picture

              +

              好吧问题这就来了,LALR可能会产生归约归约冲突。但值得注意的是,它不可能出现归约移入冲突,因为LR1没有这个东西,而LALR只是修改右边的符号,所以也不会有这个。

              +

              picture

              +

              因为LALR实际上是合并了展望符集合,这东西与移进没有关系,所以只会影响归约,不会影响移进。

              +

              picture

              +

              LALR可能会产生归约归约冲突。但值得注意的是,它不可能出现归约移入冲突,因为LR1没有这个东西,而LALR只是修改右边的符号,所以也不会有这个。

              +

              它有可能做多余的归约动作,从而推迟错误的发现

              +

              形式上与LR1相同;大小上与LR0/SLR相当;分析能力介于SLR和LR1之间;展望集仍为Follow集的子集。

              +

              总结

              感觉一路看下来,思路还是很流畅的。LR0会产生归约移进冲突和归约归约冲突,所以我们在归约时根据下一个符号是在移进符号还是在Follow集中来判断是要归约还是要移进。但是SLR条件严苛,对于那些移进符号集和Follow集有交的不适用,并且这种情况其实很普遍。加之,出于这个motivation:其实不应该用整个Follow集判断,而是应该用其真子集,所以我们开发出来个LR1文法。然后LR1文法虽然效果好但是状态太多了,所以我们再次折中一下,造出来个效果没有那么好但是状态少的LALR文法。

              +

              二义性文法的LR

              picture

              +

              所以我们可以用LR对二义性文法进行分析

              +

              我们可以通过自定义规则来消除二义性文法的归约移入冲突

              +

              picture

              +

              对于状态7,此时输入+ or *会面临归约移入冲突。由于有E->E+E归约式子,可以知道此时栈中为E+E。当输入*,由于*运算优先级更高,所以我们在此时进行移入动作转移到I5;当输入+,由于同运算先执行左结合,所以我们此时可以安全归约。

              +

              对于状态8,由于*运算比+优先级高,且左结合,所以始终进行归约。

              +

              picture

              +

              picture

              +

              picture

              +

              picture

              +

              错误检测

              picture

              +

              picture

              +

              它这个意思大概就是,符号栈和状态栈都一直pop,直到pop到一个状态,GOTO[符号栈顶,状态栈顶]有值【注意,始终保持符号栈元素+1 == 状态栈元素数+1】。然后,一直不断丢弃输入符号,直到输入符号在A的Follow集中。此时,就将GOTO值压入栈中继续分析。

              +

              【这其实也很有道理。如果输入符号在A的Follow集,说明A之后很有可能可以消耗这个输入符号。】

              +

              picture

              +

              picture

              +

              第五章 语义分析

              注意:

              +
                +
              1. 语义翻译包含语义分析和中间代码生成
              2. +
              3. 这笔包含了语法分析、语义分析、中间代码生成
              4. +
              +

              思想:

              +
                +
              1. 通过为文法符号设置语义属性,来表达语义信息
              2. +
              3. 通过与产生式(语法规则)相关联的语义规则来计算符号的语义属性值
              4. +
              +

              也可能是先入为主吧,感觉用实验的方法来理解语义分析比较便利。语义分析相当于定义一连串事件,附加在每个产生式上。当该产生式进行归约的时候,就执行对应的语义事件。而由于执行语义分析时需要的符号在语法分析栈中,所以我们也同样需要维护一个语义分析栈,在移进时也需要进栈。

              +

              SDD/SDT概念

              语义分析一般与语法分析一同实现,这一技术成为语法制导翻译。

              +

              picture

              +

              picture

              +

              picture

              +

              SDD

              picture

              +

              可以回忆一下实验,相当于对每个产生式进行一个switch-case,然后依照产生式的类别和代码规则进行出栈入栈来计算属性值。

              +

              SDT

              picture

              +

              picture

              +

              SDD

              picture

              +

              概念

              一个很简单区分综合属性和继承属性的方法,就是如果定义的是产生式左部的属性,那就是综合属性;右部,那就是继承属性

              +

              综合属性

              picture

              +

              picture

              +

              继承属性

              picture

              +

              picture

              +

              这个东西就是我们实验里写的,副作用也是更新符号表。

              +

              属性文法

              没有副作用的SDD称为属性文法。

              +

              求值顺序

              picture

              +

              而感觉语法分析这个过程的产生式归约顺序就能一定程度上表示了这个求值顺序

              +

              picture

              +
                +
              1. 继承属性放在结点左边,综合属性放在结点右边
              2. +
              3. 如果属性值A依赖于属性值B,那么就有一条从B到A的箭头【B决定A】
              4. +
              5. 对于副作用,我们将其看作一个虚综合属性【注意是综合的,虽然它看起来既由兄弟结点决定也由子节点决定】
              6. +
              7. 可行的求值序列就是拓扑排序
              8. +
              +

              picture

              +

              蛤?这不是你自己规则设计有问题吗,关我屁事

              +

              picture

              +

              其实我还是不大理解,因为这个规则不是user定义的吗?所以产生环不也是它的事,难道说自顶向下或者自底向上分析还能优化SDD定义??

              +

              感觉它意思应该是这样的,有一个方法能绝对不产生循环依赖环,也即将自底向上/自顶向下语法分析与语义分析结合的这个方法。这个方法就是它说的真子集。

              +

              所以我们接下来要研究的就是什么样的语义分析可以用自顶向下or自底向上语法分析一起制导。

              +

              S-SDD

              picture

              +

              那确实,你自底向上想要计算继承属性好像也不大可能

              +

              L-SDD

              picture

              +

              picture

              +

              对应了自顶向下的最左推导顺序

              +

              S-SDD包含于L-SDD

              +

              picture

              +

              SDT

              picture

              +

              S-SDD -> SDT

              picture

              +

              picture

              +

              当归约发生时执行对应的语义动作

              +

              picture

              +

              还需要加个属性栈

              +

              picture

              +

              所以S-SDD+自底向上其实很简单,因为只需在归约的时候进行语义分析,在移进的时候push进属性栈就行了。

              +

              picture

              +

              具体的S-SDD结合语法分析的分析过程可以看视频

              +

              这个例子还算简单的,毕竟只是综合属性的计算而已,只需要加个属性栈,保存值就行了。

              +

              picture

              +

              我们可以来关注一下这个SDT的设计,也很简单。可以产生式和语义规则分离看待,这也给我们以后设计提供一定的启发。

              +

              L-SDD -> SDT

              picture

              +

              picture

              +

              picture

              +

              非递归的预测分析

              picture

              +

              picture

              +

              这个是自顶向下的语法分析,本来只用一个栈就行了,现在需要进行扩展。T的综合属性存放在它的右边,继承属性存放在它的平行位置。

              +

              当属性值还没计算完时,不能出栈;当综合记录出栈时,它要将属性值借由语义动作复制给特定属性。

              +

              picture

              +

              然后语义动作也得一起进栈。

              +

              image-20231117015114181

              +

              digit是终结符,只有词法分析器提供值

              +

              此时,digit跟一个语义动作关联,所以我们需要把它的值复制给它关联的这个语义动作{a6},然后才能出栈。

              +image-20231117015317921 +
              -

              本次实验与TCP的关系:

              -

              在我们的webget实现中,正是由于TCP的可靠传输,才能使我们的http request正确地被服务器接收,才能使服务器的response正确地被我们接收打印。

              -

              而在ByteStream中,我们也做了跟TCP类似的工作:接收substring,并且将它们拼接为in-order的byte stream【由于在内存中/单线程,所以这个工作看起来非常简单】:

              -
              while(is_input_end == false&&pointer<length){
              if(buffer.size() == capacity) break;
              buffer.push_back(data[pointer]);
              pointer++;
              }
              +

              关联的另一个实例:

              +

              image-20231117015508123

              +

              此时由于T’.inh还要被a3用到,所以我们就得在T’出栈前把它的这个inh值复制给a3。

              -

              Fetch a Web page

              主要是介绍了telnet指令

              -

              屏幕截图 2023-02-23 194758

              -

              Send yourself an email

              用的是telnet带smtp参

              -

              Listening and connecting

              上面的telnet是一个client program。接下来我们要把自己放在server的位置上。

              -

              用的是netcat指令。

              -

              image-20230223202202509

              -

              Use socket to write webget

              这个确实不难,就是这个地方有点坑:

              +

              当遇到语义动作之后,就执行动作,并且出栈语义动作。

              +

              picture

              +

              它这意思应该是遇到每个产生式的每个符号要执行什么动作都是确定的,所以代码实现是可能的。

              +

              可以看到:

              +
                +
              1. 语义动作代码就是执行
              2. +
              3. 综合属性代码就是赋给关联语义动作
              4. +
              5. 非终结符就是选一个它作为左部的产生式,然后看看要不要用到它自身的属性对右部子属性进行复制(体现了继承属性)
              6. +
              +

              递归的预测分析

              picture

              +

              666666666

              +

              感觉这个值得深思,但反正现在的我思不出啥了。。。

              +

              picture

              +

              picture

              +

              LR分析

              picture

              +

              picture

              +

              相当于把L-SDD转化为了个S-SDD。具体是这样,把原式子右边的变量替换为marker的继承属性,结果替换为marker的综合属性。那么新符号继承属性怎么算啊。。。不用担心,因为观察可知要使用的这两个非终结符一定已经在栈中了。

              +

              具体分析也看视频就好了。

              +

              第六章 中间代码生成

              中间代码的形式

              picture

              +

              逆波兰(后缀)

              picture

              +

              三地址码

              picture

              +

              picture

              +

              picture

              +

              picture

              +

              false list就是if失败后的那个goto序号,true list是成功的那个goto序号,s.nextline是整个if的下一条指令

              +

              picture

              +

              四元式

              picture

              +

              picture

              +

              picture

              +

              增量生成

              +

              DAG图

              picture

              +

              picture

              +

              声明语句

              类型表达式

              picture

              +

              一般声明

              非嵌套

              picture

              +

              picture

              +

              picture

              +

              picture

              +

              嵌套

              picture

              +

              picture

              +

              它这个相当于是把符号表和offset都整成了一个栈,毕竟确实过程调用就是得用栈结构的

              +

              picture

              +

              picture

              +

              记录

              picture

              +

              picture

              +

              之后用到该记录类型,就指向记录符号表即可。

              +

              picture

              +

              简单赋值语句

              定义

              这个就不用填符号表了,所以helper function都是用来产生中间代码的

              +

              picture

              +

              addr属性需要从符号表中获取

              +

              picture

              +

              临时变量处理

              picture

              +

              数组元素寻址

              picture

              +

              picture

              +

              picture

              +

              picture

              +

              picture

              +

              picture

              +

              picture

              +

              类型检查

              规则

              看个乐吧

              +

              picture

              +

              类型转换

              picture

              +

              picture

              +

              在语义动作中实现

              +

              控制流语句

              简单控制流

              picture

              +

              picture

              +

              反正意思就是用S.next这个继承属性来表示S.code执行完后的下一个三地址码地址。

              +

              picture

              +

              if-then

              picture

              +

              if-then-else

              picture

              +

              while-do

              picture

              +

              ;

              其实不大懂这什么玩意

              +

              picture

              +

              picture

              +

              picture

              +

              抽象

              +

              picture

              +

              picture

              +

              picture

              +

              布尔表达式

              布尔表达式翻译

              基本

              picture

              +

              picture

              +
              数值表示

              picture

              +

              picture

              +

              picture

              +
              控制流表示

              picture

              +

              picture

              +

              picture

              +

              picture

              +

              混合模式布尔表达式

              picture

              +

              picture

              +

              picture

              +

              回填

              基本

              picture

              +

              picture

              +

              picture

              +

              这两个都是综合属性

              +

              相当于是一个waiting list

              +
              布尔表达式的回填

              picture

              +

              可以理解为,B这个表达式可以分为两种情况,两种情况有一个为真B就为真。那么,B的真回填list相当于也被分为了两种情况,所以要求B的就是把它们合起来。

              +

              picture

              +

              picture

              +

              picture

              +

              picture

              +

              picture

              +

              原来回填是这个意思

              +
              控制流结构的回填

              nextline是一个综合属性

              +
              if-then

              picture

              +
              if-then-else

              picture

              +
              while-do

              picture

              +
              sequence

              picture

              +
              for

              picture

              +

              picture

              +
              repeat

              picture

              +
              switch-case

              TODO 这笔之后再看。。。。

              +

              picture

              +

              picture

              +

              picture

              +

              过程调用

              picture

              +

              picture

              +

              picture

              +

              输入输出语句

              TODO

              +

              picture

              +

              picture

              +

              题型1 四元序列

              picture

              +

              第七章 运行存储分配

              概念

              存储组织

              活动记录

              picture

              +

              picture

              +

              picture

              +

              picture

              +

              静态/动态链

              picture

              +

              静态链也被称作访问链,用于访问存放于其他活动记录中的非局部数据。

              +

              动态链也被称作控制链,用于指向调用者的活动记录。

              +

              picture

              +

              picture

              +

              内存对齐

              picture

              +

              picture

              +

              作用域

              picture

              +

              picture

              +

              传参方式

              传值

              picture

              +

              传地址

              picture

              +

              传值结果

              picture

              +

              反正意思就是既要得到原来的A,又要修改A

              +

              传名

              picture

              +

              picture

              +

              静态存储分配

              picture

              +

              picture

              +

              顺序分配法

              picture

              +

              层次分配法

              picture

              +

              栈式存储分配

              概念

              picture

              +

              picture

              +

              也就是说左边及其所有子树全调完了,才能调下一个兄弟的。

              +

              picture

              +

              picture

              +

              image-20231114154150835

              +

              左边这几点设计规则都十分reasonable,很值得注意。

              +

              不过我其实挺好奇,参数存在那么后面该咋访问。。。。看xv6,似乎是fp指向前面,sp才指向local,也即用了两个栈指针。

              +

              这个控制链也是约定俗成的,具体可以想起来xv6也是类似结构:

              +

              picture

              +

              当函数返回的时候,就会进行恢复现场,从而出栈一直到ra,很合理。

              +

              调用/返回序列

              是什么

              picture

              +

              调用序列应该就是设置参数、填写栈帧一类,返回序列就是恢复现场

              +

              picture

              +

              picture

              +

              picture

              +

              picture

              +

              picture

              +

              生成代码

              picture

              +
              调用序列

              传变量、改变meta data、改变top和sp指针

              +

              picture

              +

              picture

              +
              返回序列

              picture

              +

              变长数据

              picture

              +

              这段解释了下为什么不用堆,说得很好

              +

              picture

              +

              缺点

              picture

              +

              第二点,比如malloc后不free

              +

              栈中非局部数据的访问

              picture

              +

              有过程嵌套

              picture

              +

              静态作用域

              访问链

              picture

              +

              picture

              +

              picture

              +

              picture

              +
              建立访问链

              picture

              +

              picture

              +

              picture

              +
              过程参数的访问链

              picture

              +

              picture

              +

              Display表

              通俗解释

              每一个嵌套深度的分配一个Display位

              +

              S嵌套深度1,所以占据d[1];Y和X嵌套深度2,所以占据d[2];Z嵌套深度3,所以占据d[3]。

              +

              然后,一开始遇到个S,d1指向S;然后调用Y,d2指向Y;然后Y中调用X,就修改d2指向X;然后调用Z,就修改d3指向Z。

              +

              总之显示栈就是这个变换指针的过程。

              +

              至于控制栈,要打印这里面的display表,就是看层数。如果d1那就打印当前层,d2就打印的12层,d3就123层【不是纯显示栈,是它自己内部的未变换指针的结果】

              +

              picture

              +

              picture

              +

              picture

              +

              结果:SXZ

              +
              定义

              picture

              +

              picture

              +

              picture

              +
              访问流程

              picture

              +

              picture

              +

              picture

              +
              生成代码

              picture

              +

              picture

              +

              picture

              +

              picture

              +

              动态作用域

              静态作用域是空间上就近原则,动态是时间上。

              +

              picture

              +

              picture

              +

              无过程嵌套

              picture

              +

              picture

              +

              也就是说这时候非局部的一定是全局变量或者静态的局部变量。

              +

              堆管理

              picture

              +

              内存管理器

              局部性

              picture

              +

              堆分配算法

              人工回收请求

              符号表

              如题

              picture

              +

              picture

              +

              如果是支持过程声明嵌套,顺着符号表就可以找到其父过程/子过程的数据。

              +

              符号表也可以用于构造访问链,因为过程名也是一种符号。

              +

              picture

              +

              符号表的建立

              picture

              +

              第九章 代码生成

              概述

              picture

              +

              目标代码形式

              picture

              +

              指令选择

              picture

              +

              寄存器分配

              picture

              +

              计算顺序选择

              picture

              +

              不讨论这个

              +

              目标语言

              定义

              picture

              +

              指令开销

              picture

              +

              picture

              +

              picture

              +

              picture

              +

              picture

              +

              运行时刻地址

              简单的代码生成器

              后续引用信息

              picture

              +

              picture

              +

              寄存器与地址描述符

              picture

              +

              代码生成算法

              picture

              +

              picture

              +

              picture

              +

              picture

              +

              picture

              +

              窥孔优化

              picture

              +

              冗余指令消除

              picture

              +

              不可达代码消除

              picture

              +

              强度削弱

              picture

              +

              特殊机器指令使用

              picture

              +

              寄存器分配指派

              picture

              +

              全局寄存器分配

              picture

              +

              引用计数

              picture

              +

              picture

              +

              picture

              +

              picture

              +

              所以这东西是用来决策寄存器分配的

              +

              外层循环的寄存器指派

              picture

              +

              picture

              +

              反正类似保护现场恢复现场

              +

              拓展阅读

              AC自动机

              在思考自动机和动态规划的关系时,胡乱搜索看到了AC自动机,于是来了解了一下。

              -

              Please note that in HTTP, each line must be ended with “\r\n” (it’s not sufficient to use just “\n” or endl).

              +

              算法学习笔记(89): AC自动机 - Pecco的文章 - 知乎

              -

              导致我跟400 Bad Request大眼瞪小眼了好久。。。

              -
              void get_URL(const string &host, const string &path) {
              TCPSocket sock;
              string tmp;
              // sock.set_blocking(true);// 默认情况下即为true
              sock.connect(Address(host,"http"));
              sock.write("GET " + path + " HTTP/1.1\r\nHost: " +
              host + "\r\nConnection: close\r\n\r\n");

              while((tmp = sock.read(1)) != ""){
              cout << tmp;
              }
              /*
              上面那个写法不大规范,更规范的写法:
              while(!sock.eof()){
              cout << sock.read(1);
              }
              */
              sock.close();
              }
              - -

              还有一点值得注意的是,当我这样时:

              -
              TCPSocket sock;
              sock.set_blocking(false);
              sock.connect(Address(host,"http"));
              - -

              会报错Operation now in progress

              -

              关于socket通信中在connect()遇到的Operation now in progress错误

              -

              遇到此错误是因为将在connect()函数之前将套接字socket设为了非阻塞模式。改为在connect()函数之后设置即可。

              -
              -

              我觉得这个实验设计得挺好的,写的时候感觉很有意思。我推荐看下 https://github.com/shootfirst/CS144/blob/main/lab-0/apps/webget.cc 里的注释,写得很好很规范,让我明白了很多本来没搞懂的地方,比如说shutdown的用法。

              -

              An in-memory reliable byte stream

              -

              实现一个ByteStream类,可以通过readwrite对其两端进行读写。是单线程程序,因而无需考虑阻塞。

              +

              考虑一个问题:给出若干个模式串,如何构建一个DFA,接受所有以任一模式串结尾(称为与该模式串匹配)的文本串?

              +

              可以先思考一个更简单的问题:如何构建接受所有模式串的DFA?很明显,**字典树**就可以看做符合要求的自动机。例如,有模式串"abab""abc""bca""cc" ,我们把它们插入字典树,可以得到:

              +

              picture

              +

              为了使它不仅接受模式串,还接受以模式串结尾的文本串,一个看起来挺正确的改动是,使每个状态接受所有原先不能接受的字符,转移到初始状态(即根节点)。

              +

              picture

              +

              但是如果我们尝试"abca",我们会发现我们的自动机并不能接受它。稍加观察发现,我们在状态5接受a应该跳到状态8才对,而不是初始状态。某种意义上来说,状态7是状态5退而求其次的选择,因为状态7在trie上对应的字符串"bc"是状态5对应的字符串"abc"后缀。既然状态5原本不能接受"a",我们完全可以退而求其次看看状态7是否可以接受。这看起来很像KMP算法,确实,AC自动机常常被人称作trie上KMP。

              +

              所以我们给每个状态分配一条fail边,它连向的是该状态对应字符串在trie上存在的最长真后缀所对应的状态。我们令所有状态p接受原来不能接受的字符c,转移到 next(fail(p),c) ,特别地,根节点转移到自己。为什么不需要像KMP算法一样,用一个循环不断进行退而求其次的选择呢?因为如果我们用BFS的方式进行上面的重构,我们可以保证 fail(p) 在p重构前已经重构完成了,类似于动态规划

              +

              picture

              +

              这样建fail边和重构完成后得到的自动机称为AC自动机(Aho-Corasick Automation)。

              +

              我们发现fail边也形成一棵树,所以其实AC自动机包含两棵树:trie树fail树。一个重要的性质是,如果当前状态 p 在某个终止状态 s 的fail树的子树上,那么当前文本串就与 s 所对应模式串匹配

              -

              感想

              这东西其实是很简单的,但是我还是花了一定的时间,主要原因有两点,一是我不懂c++,所以一些地方错得我很懵逼,二是因为我是sb。

              -

              下面就记录下三个我印象比较深刻的错误吧。

              -
              错误1 member initialization list

              构造函数我一开始是这么写的:

              -

              image-20230224113108208

              -

              结果爆出了这样的错:

              -

              image-20230224112056879

              -

              搜了半天也没看懂怎么回事,去求助了下某场外c艹选手,才知道了还有成员变量初始化列表这玩意,这个东西似乎比较高效安全。

              -

              于是我改成了这么写:

              -

              image-20230224113333962

              -

              它告诉我buffer也得初始化。于是我又这么写:

              -

              image-20230224113358856

              -

              又是奇奇怪怪的错误,说明vector不能这么初始化。

              -

              场外c艹选手看到了这个:

              -

              image-20230224113456432

              -

              所以说vector应该这样初始化:

              -

              image-20230224113549970

              -
              错误2 使用了vector作为buffer的载体

              应该使用的是可以从front删除数据的数据结构,比如说deque。【vector也行,但是效率较低】

              -

              具体为什么,可以以数据流为cat为例。执行peek(2)时,使用vector得到的是at,使用deque得到的是ca。

              -
              错误3 错误地阻塞

              一开始在write方法,我是这么写的:

              -
              int length = data.length();
              while(is_input_end == false&&pointer<length){
              while(buffer.size() == capacity);
              buffer.push_back(data[pointer]);
              pointer++;
              total_write ++;
              }
              - -

              结果就是测试用例Timeout。我找了很久都不知道错在了哪,最后求助了场外观众【罪过……这次实验太不独立了】,学着他把length改成了这样:

              -
              int length = min(data.length(),capacity-buffer.size());
              - -

              发现成了。

              -

              我去看了看testbench,猜测应该是因为阻塞了,我还以为是deque自身会阻塞【是的,我完全没注意到自己顺手把阻塞写了下去】,查了半天发现不会,最后才发现是自己不小心搞错了呃呃…………

              -

              代码

              头文件声明

              class ByteStream {
              private:
              // Your code here -- add private members as necessary.

              // Hint: This doesn't need to be a sophisticated data structure at
              // all, but if any of your tests are taking longer than a second,
              // that's a sign that you probably want to keep exploring
              // different approaches.

              size_t total_write;
              size_t total_read;
              bool is_input_end;
              const size_t capacity;
              deque<char> buffer;
              - -

              具体实现

              ByteStream::ByteStream(const size_t cap) : total_write(0),total_read(0),is_input_end(false),capacity(cap),buffer(){ }

              //! Write a string of bytes into the stream. Write as many
              //! as will fit, and return how many were written.
              //! \returns the number of bytes accepted into the stream
              size_t ByteStream::write(const string &data) {
              if(is_input_end == true) is_input_end = false;
              int pointer = 0;
              int length = data.length();
              while(is_input_end == false&&pointer<length){
              if(buffer.size() == capacity) break;
              buffer.push_back(data[pointer]);
              pointer++;
              }
              total_write+=pointer;
              return pointer;
              }
              //! Peek at next "len" bytes of the stream
              //! \param[in] len bytes will be copied from the output side of the buffer
              string ByteStream::peek_output(const size_t len) const {
              string res;
              size_t i = 0;
              for (auto it = buffer.begin(); it != buffer.end(); it++) {
              if (i >= len)
              break;
              i++;
              res.push_back(*it);
              }
              return res;
              }

              //! Remove bytes from the buffer
              //! \param[in] len bytes will be removed from the output side of the buffer
              void ByteStream::pop_output(const size_t len) {
              size_t i;
              for (i = 0; i < len; i++) {
              if (buffer.empty())
              break;
              buffer.pop_front();
              }
              total_read+=i;
              }

              //! Read (i.e., copy and then pop) the next "len" bytes of the stream
              //! \param[in] len bytes will be popped and returned
              //! \returns a string
              std::string ByteStream::read(const size_t len) {
              string res = peek_output(len);
              pop_output(len);
              return res;
              }

              void ByteStream::end_input() {is_input_end = true;}

              bool ByteStream::input_ended() const { return is_input_end; }

              size_t ByteStream::buffer_size() const { return buffer.size(); }

              bool ByteStream::buffer_empty() const { return buffer.empty(); }

              bool ByteStream::eof() const { return is_input_end && buffer.empty(); }

              size_t ByteStream::bytes_written() const { return total_write; }

              size_t ByteStream::bytes_read() const { return total_read; }

              size_t ByteStream::remaining_capacity() const { return capacity - buffer.size(); }
              +

              也就是说它的解决方法是加fall边(蓝色)和加新边(红色),

              ]]>
              @@ -7207,6 +6731,413 @@ url访问填写http://localhost/webdemo4_war/*.do
            4. 连接方式

              image-20230619204520342

              image-20230619204537086

              与程序中断比较

              image-20230619204641555

              +]]> + + + 密码学基础 + /2023/11/26/cryptography/ + +

              学习目的:顺利过考试,以及获取基本的密码学知识,数学原理不重要

              + +

              第一章 概述

              image

              +

              image

              +

              image

              +

              image

              +

              image

              +

              image

              +

              image

              +

              第二章 传统密码技术

              概念

              image

              +

              image

              +

              分类

              置换密码

              image

              +

              列置换密码

              加密

              image

              +
              解密

              image

              +
              例子

              image

              +

              image

              +

              周期置换密码

              image

              +

              image

              +

              代换密码

              image

              +

              单表代换

              image

              +

              image

              +

              多表代换

              image

              +

              image

              +

              image

              +

              image

              +

              传统密码体制分析

              频率(单表代换)

              image

              +

              重合指数(多表代换)

              image

              +

              明文-密文对(hill密码)

              image

              +

              第三章 分组密码-DES

              概述

              image

              +
                +
              1. 分组密码一般指对称分组密码
              2. +
              +

              image

              +
                +
              1. 明文经编码表示后变成二进制序列
              2. +
              3. 二进制序列固定长度分组
              4. +
              5. 每组在密钥控制下转为密文分组
              6. +
              7. 本质上是明文到密文的一一映射
              8. +
              9. 一般明文长度=密文长度,密钥长度不一定
              10. +
              +

              image

              +

              image

              +

              设计思想

              image

              +

              image

              +

              image

              +

              image

              +

              image

              +

              基本特点

              image

              +

              子密钥生成算法

              image

              +

              轮函数

              image

              +

              迭代轮数

              image

              +

              DES算法

              概述

              image

              +

              image

              +

              加密流程

              总体流程

              image

              +

              image

              +

              image

              +

              选择扩展置换E

              image

              +

              子密钥生成

              image

              +

              压缩替代S-盒

              image

              +

              image

              +

              image

              +

              置换p-盒

              image

              +

              解密流程

              image

              +

              image

              +

              安全性分析

              image

              +

              互补性

              image

              +

              image

              +

              弱密钥

              image

              +

              image

              +

              差分分析

              只有理论上意义

              +

              image

              +

              线性分析

              实际上不可行

              +

              image

              +

              密钥搜索

              image

              +

              image

              +

              多重DES

              image

              +

              image

              +

              二重

              image

              +

              3DES

              你也是过渡阶段?

              +

              image

              +

              第四章 有限域

              数学基础

              image

              +

              逆元:

              +

              image-20231119235319197

              +

              比如说在G(7)中,2的逆元为4。

              +

              也即,任意整数a,则存在x,a / 2 == a * 4 (mod 7),4为2模7的乘法逆元,记为 2(-1)(mod 7) = 4。

              +

              image

              +

              求逆元的方法是求b^(m-2) mod m。如2^(5) mod 7 = 4。

              +

              群环域

              image

              +

              image

              +

              image

              +

              确实封闭且结合且单位元且逆元

              +

              循环群

              image

              +

              image

              +

              image

              +

              确实是环

              +

              image

              +

              image

              +

              有限域GF(p)

              有限域就是阶为素数幂的域?

              +

              image

              +

              image

              +

              image

              +

              image-20231119233220659

              +

              多项式运算

              image

              +

              普通多项式运算

              image

              +

              image

              +

              image

              +

              image

              +

              系数模p运算的多项式运算

              image

              +

              确实,毕竟系数本身就是域了,除了没定义逆元外别的都满足。

              +

              image

              +

              image

              +

              有限域GF(2^n)

              image

              +

              image

              +

              image

              +

              第五章 高级加密标准-AES

              概述

              简介

              image

              +

              image

              +

              Nr=Nk的幂数x2

              +

              简化版AES

              image

              +

              image

              +

              具体算法详见PPT。

              +

              基本结构

              image

              +

              总体流程

              image

              +

              加密流程

              整体流程

              image

              +

              image

              +

              image

              +

              状态矩阵

              image

              +

              字节代替

              image

              +

              行移位

              image

              +

              列混淆

              image

              +

              image

              +

              可以关注下是怎么通过C矩阵求出这个固定多项式的:

              +

              image

              +

              轮密钥加

              image

              +

              密钥扩展

              image

              +

              image

              +

              感觉也是类似对明文做的操作

              +

              安全评估

              image

              +

              image

              +

              image

              +

              image

              +

              SM4

              image

              +

              image

              +

              第六章 分组密码的工作模式

              image

              +

              image

              +

              电码本ECB

              image

              +

              image

              +

              image

              +

              密码分组链接CBC

              image

              +

              image

              +

              密码反馈CFB

              image

              +

              image

              +

              输出反馈OFB

              image

              +

              image

              +

              计数器Counter

              image

              +

              image

              +

              image

              +

              总结

              image

              +

              第七章 序列密码

              概述

              序列密码的密钥序列是随机的。

              +

              image

              +

              image

              +

              image

              +

              image

              +

              image

              +

              总体流程

              image

              +

              image

              +

              image

              +

              密钥产生器KG

              总体构成

              image

              +

              image

              +

              线性反馈移位寄存器理论

              image

              +

              反馈移位寄存器

              image

              +

              image

              +

              线性反馈移位寄存器

              image

              +

              image

              +

              确实,感觉相比上面的这笔就是换了个反馈函数,就达到了2^n-1的周期

              +

              m序列

              特性

              image

              +

              image

              +
              生成

              image

              +

              image

              +
              分析

              image

              +
              破译

              image

              +

              image

              +

              image

              +

              image

              +

              常见序列生成算法

              Geffe序列生成器

              image

              +

              Pless生成器

              image

              +

              image

              +

              A5算法

              image

              +

              image

              +

              ZUC算法

              image

              +

              image

              +

              image

              +

              image

              +

              image

              +

              image

              +

              image

              +

              RC4

              简介

              image

              +

              image

              +

              image

              +

              image

              +

              流程

              数据表S的初始状态

              image

              +

              数据表S的初始置换

              image

              +

              密钥流的生成

              image

              +

              第八章 数论基础

              整除性和带余除法,最大公因子

              image

              +

              image

              +

              素数和模运算

              image

              +

              image

              +

              也就是说求最大公因子实际上可以只求共有素数因子

              +

              image

              +

              image

              +

              image

              +

              image

              +

              欧几里得算法和扩展欧几里得算法

              欧几里得算法

              image

              +

              image

              +

              image

              +

              扩展欧几里得

              image

              +

              image

              +

              image

              +

              image

              +

              image

              +

              费马小定理和欧拉定理

              费马小定理

              image

              +

              image

              +

              欧拉定理

              image

              +

              image

              +

              素性检测

              miller-rabin

              image

              +

              image

              +

              image

              +

              中国剩余定理

              image

              +

              image

              +

              image

              +

              image

              +

              image

              +

              离散对数

              image

              +

              image

              +

              image

              +

              image

              +

              image

              +

              第九章 公钥加密体系-RSA

              image

              +

              image

              +

              概述

              image

              +

              image

              +

              image

              +

              RSA

              组成

              image

              +

              image

              +

              image

              +

              安全性

              image

              +

              image

              +

              image

              +

              image

              +

              应用

              image

              +

              Rabin加密

              image

              +

              image

              +

              MH背包密码

              image

              +

              简介

              image

              +

              流程

              image

              +

              image

              +

              例子

              image

              +

              image

              +

              安全性分析

              image

              +

              image

              +

              EIGamal加密

              image

              +

              image

              +

              image

              +

              image

              +

              椭圆曲线密码体制

              image

              +

              数学理论

              image

              +

              image

              +

              image

              +

              image

              +

              image

              +

              image

              +

              image

              +

              image

              +

              密码体制

              image

              +

              image

              +

              image

              +

              image

              +

              IBE算法

              image

              +

              image

              +

              image

              +

              image

              +

              image

              +

              看起来意思就是公钥完全明文,用的是用户的身份ID;私钥用户自己存着。

              +

              image

              +

              image

              +

              image

              +

              后量子密码

              概述

              image

              +

              image

              +

              image

              +

              image

              +

              NTRU

              流程

              image

              +

              image

              +

              image

              +

              image

              +

              举例

              image

              +

              image

              +

              安全性

              image

              +

              第十一章 哈希函数

              概述

              image

              +

              image

              +

              image

              +

              image

              +

              这个角度很有意思,确实是名字一样原理相近,但是目的完全不一样:

              +

              image

              +

              image

              +

              常见哈希函数

              SHA

              image

              +

              image

              +

              SM3

              image

              +

              image

              +

              安全性

              image

              +

              image

              +

              暴力攻击

              image

              +

              生日攻击

              image

              +

              image

              +

              攻击过程

              image

              +

              image

              +

              应用

              image

              +

              身份认证

              image

              +

              image

              +

              image

              +

              数字签名

              image

              +

              也就是中途会哈希两次吼。

              +

              第十二章 消息认证码 (MAC)

              概述

              基本思想

              image

              +

              image

              +

              一样的话就是说明消息没被篡改

              +

              image

              +

              要求

              image

              +

              基于哈希函数的MAC

              image

              +

              直观构造

              image

              +

              image

              +

              image

              +

              image

              +

              HMAC

              image

              +

              image

              +

              image

              +

              基于分组密码的MAC

              image

              +

              数据认证算法DAA

              image

              +

              image

              +

              CMAC

              image

              +

              认证加密

              概述

              image

              +

              image

              +

              CCM

              image

              +

              局限性

              image

              +

              第十三章 数字签名PKI

              概述

              简介

              image

              +

              image

              +

              对比

              image

              +

              image

              +

              安全性

              image

              +

              实现

              image

              +

              image

              +

              image

              +

              image

              +

              常见实现

              都包含签名算法、验证算法、正确性证明、举例,详细看PPT吧。

              +

              基于RSA

              image

              +

              基于离散对数

              image

              +

              ELGamal

              Schnorr

              DSA

              盲签名

              image

              +

              image

              +

              image

              +

              群(组)签名

              image

              +

              第十四章 密码协议

              概述

              image

              +

              image

              +

              分割和选择协议

              image

              +

              掷硬币协议

              image

              +

              单向函数

              image

              +

              模p指数运算

              image

              +

              零知识证明

              image

              +

              image

              +

              image

              +

              比特承诺

              image

              +

              image

              +

              image

              +

              安全多方计算

              这个有点复杂,可以看看PPT。

              +

              第十五章 密钥管理

              概述

              image

              +

              image

              +

              密钥分配

              image

              +

              image

              +

              无中心

              image

              +

              中心模式

              image

              +

              基于公钥密钥

              image

              +

              密钥协商

              image

              +

              Diffie-Hellman密钥交换方案

              image

              +

              image

              +

              PKI

              image

              +

              image

              +

              image

              +

              image

              +

              image

              +

              image

              +

              image

              +

              image

              +

              image

              +

              SSL

              概述

              image

              +

              image

              +

              底层协议

              image

              +

              image

              +

              image

              +

              上层协议

              警告协议

              image

              +

              握手协议/密码变化协议

              image

              +

              密钥交换(四握手)

              image

              +

              image

              +

              image

              +

              image

              +

              image

              ]]>
              @@ -7255,237 +7186,85 @@ url访问填写http://localhost/webdemo4_war/*.do

              具体实现

              构造器的参数

              参考文章

              也是系统调用socket的参数,了解一下知识多多益善。

                -
              1. domain

                -

                在本次实验中只会取值前两个,即本地通信和IPv4网络通信

                -

                image-20230309232045195

                -
              2. -
              3. type

                -

                好像比如说取SOCK_DGRAM就是UDP,取SOCK_STREAM就是TCP。

                -
              4. -
              -
              代码
              /* Socket */
              /* 构造器 */
              // default constructor for socket of (subclassed) domain and type
              Socket::Socket(const int domain, const int type) : FileDescriptor(SystemCall("socket", socket(domain, type, 0))) {}
              // construct from file descriptor
              Socket::Socket(FileDescriptor &&fd, const int domain, const int type) : FileDescriptor(move(fd)) { ... }

              // get the local or peer address the socket is connected to
              // 此为private函数,应该是用于方便下面那两个函数的,虽然我觉得这个设计意图没什么必要()
              Address Socket::get_address(const string &name_of_function,const function<int(int, sockaddr *, socklen_t *)> &function) const {
              Address::Raw address;
              socklen_t size = sizeof(address);
              SystemCall(name_of_function, function(fd_num(), address, &size));
              return {address, size};
              }
              Address Socket::local_address() const { return get_address("getsockname", getsockname); }
              Address Socket::peer_address() const { return get_address("getpeername", getpeername); }

              /*
              这两个函数是用于把socket连到CS的
              将socket的一端连上本机,就需要调用bind;连上别的什么东西就要用connect
              */
              // bind socket to a specified local address (usually to listen/accept)
              // address is a local Address to bind
              void Socket::bind(const Address &address) { SystemCall("bind", ::bind(fd_num(), address, address.size())); }
              // connect socket to a specified peer address
              // address is the peer's Address
              void Socket::connect(const Address &address) { SystemCall("connect", ::connect(fd_num(), address, address.size())); }

              // shut down a socket in the specified way
              // how can be `SHUT_RD`, `SHUT_WR`, or `SHUT_RDWR`
              void Socket::shutdown(const int how) {
              SystemCall("shutdown", ::shutdown(fd_num(), how));
              switch (how) {
              case SHUT_RD:
              register_read();
              break;
              // ...
              }
              }

              // set socket option,传入协议层以及要设置非选项的键和值
              template <typename option_type>
              void Socket::setsockopt(const int level, const int option, const option_type &option_value) {
              SystemCall("setsockopt", ::setsockopt(fd_num(), level, option, &option_value, sizeof(option_value)));
              }

              // allow local address to be reused sooner, at the cost of some robustness
              // 以鲁棒性为代价,让local address可复用
              // Using `SO_REUSEADDR` may reduce the robustness of your application
              void Socket::set_reuseaddr() { setsockopt(SOL_SOCKET, SO_REUSEADDR, int(true)); }

              /* UDPSocket */
              // 从socket中接收数据并放进datagram中
              // If mtu is too small to hold the received datagram, this method throws a runtime_error
              void UDPSocket::recv(received_datagram &datagram, const size_t mtu) {
              // receive source address and payload
              // ...
              const ssize_t recv_len = SystemCall(
              "recvfrom",
              ::recvfrom(
              fd_num(), datagram.payload.data(), datagram.payload.size(), MSG_TRUNC, datagram_source_address, &fromlen));
              // ...
              }
              UDPSocket::received_datagram UDPSocket::recv(const size_t mtu) {
              received_datagram ret{{nullptr, 0}, ""};
              recv(ret, mtu);
              return ret;
              }

              // 向socket发送数据
              void sendmsg_helper(const int fd_num,
              const sockaddr *destination_address,
              const socklen_t destination_address_len,
              const BufferViewList &payload) {
              // ...
              const ssize_t bytes_sent = SystemCall("sendmsg", ::sendmsg(fd_num, &message, 0));
              // ...
              }
              void UDPSocket::sendto(const Address &destination, const BufferViewList &payload) {
              sendmsg_helper(fd_num(), destination, destination.size(), payload);
              register_write();
              }
              void UDPSocket::send(const BufferViewList &payload) {
              sendmsg_helper(fd_num(), nullptr, 0, payload);
              register_write();
              }
              // ...
              - -

              * TCPSpongeSocket

              上面那俩类其实就是两个包装类,用来将系统调用包装为c++类,看起来很抽象很迷惑。但到这就不一样了!我们开始用上我们之前写的TCP协议的代码了!

              -

              除了跟fd以及socket一致的readwrite以及close之外,TCPSocket最独特的功能,应该就是TCP连接的建立与释放了,其状态转移等逻辑已由我们在Lab0-4实现,此socket类仅实现事件的监听TCP协议对象生命周期的管理

              -

              双线程

              在详细说明其两个功能——事件监听和生命周期管理——之前,不妨先了解下其总体的架构。

              -

              TCPSpongeSocket需要双线程实现。其中一个线程用来招待其owner:它会执行向owner public的connect、read、write等服务。另一个线程用来运行TCPConnection:它会时刻调用connection的tick方法,并且进行事件监听。

              -
              //! \class TCPSpongeSocket
              //! This class involves the simultaneous operation of two threads.
              //!
              //! One, the "owner" or foreground thread, interacts with this class in much the
              //! same way as one would interact with a TCPSocket: it connects or listens, writes to
              //! and reads from a reliable data stream, etc. Only the owner thread calls public
              //! methods of this class.
              //!
              //! The other, the "TCPConnection" thread, takes care of the back-end tasks that the kernel would
              //! perform for a TCPSocket: reading and parsing datagrams from the wire, filtering out
              //! segments unrelated to the connection, etc.
              - -

              事件监听

              完成事件监听的核心部分是方法_tcp_loop以及_initialize_TCP中对_eventloop的初始化,还有eventloop的实现。

              -

              看下来其实理解难度不大(虽然细节很多并且我懒得研究了),但我认为很值得学习。

              -
              _initialize_TCP

              主要功能是添加我们想监听的事件,有四个,分别是从app得到数据、有要向app发送的数据、从底层协议得到数据、有要向底层协议发送的数据。具体的话,代码和注释都写得很详细就不说了。

              -

              可以看到,TCP与协议栈交互【包括收发数据报】,是通过AdaptT _datagram_adapter;实现的;TCP与上层APP交互【包括传送数据】,是通过LocalStreamSocket _thread_data;实现的。

              -
              template <typename AdaptT>
              void TCPSpongeSocket<AdaptT>::_initialize_TCP(const TCPConfig &config) {
              _tcp.emplace(config);
              // Set up the event loop

              // There are four possible events to handle:需要监听以下四种事件
              //
              // 1) Incoming datagram received (needs to be given to
              // TCPConnection::segment_received method)得到底层协议栈送过来的data
              //
              // 2) Outbound bytes received from local application via a write()
              // call (needs to be read from the local stream socket and
              // given to TCPConnection::data_written method)得到上层app送过来的data
              //
              // 3) Incoming bytes reassembled by the TCPConnection
              // (needs to be read from the inbound_stream and written
              // to the local stream socket back to the application)TCP协议需要向app写入data
              //
              // 4) Outbound segment generated by TCP (needs to be
              // given to underlying datagram socket)TCP需要向外界发送data

              // rule 1: read from filtered packet stream and dump into TCPConnection得到外界data
              _eventloop.add_rule(_datagram_adapter,
              Direction::In,
              [&] {
              auto seg = _datagram_adapter.read();
              if (seg) {
              _tcp->segment_received(move(seg.value()));
              }
              if (_thread_data.eof() and _tcp.value().bytes_in_flight() == 0 and not _fully_acked) { _fully_acked = true; }
              },
              [&] { return _tcp->active(); });

              // rule 2: read from pipe into outbound buffer得到app data
              _eventloop.add_rule(
              // LocalStreamSocket _thread_data;
              // 看来用户是通过socket写入的数据
              _thread_data,
              Direction::In,
              [&] {
              const auto data = _thread_data.read(_tcp->remaining_outbound_capacity());
              const auto len = data.size();
              const auto amount_written = _tcp->write(move(data));
              if (amount_written != len) {
              throw runtime_error("TCPConnection::write() accepted less than advertised length");
              }
              if (_thread_data.eof()) {
              _tcp->end_input_stream();
              _outbound_shutdown = true;
              }
              },
              [&] { return (_tcp->active()) and (not _outbound_shutdown) and (_tcp->remaining_outbound_capacity() > 0); },
              [&] {
              _tcp->end_input_stream();
              _outbound_shutdown = true;
              });

              // rule 3: read from inbound buffer into pipe向app写入data
              _eventloop.add_rule(
              _thread_data,
              Direction::Out,
              [&] {
              ByteStream &inbound = _tcp->inbound_stream();
              // Write from the inbound_stream into the pipe
              const size_t amount_to_write = min(size_t(65536), inbound.buffer_size());
              const std::string buffer = inbound.peek_output(amount_to_write);
              // 通过向socket写实现
              const auto bytes_written = _thread_data.write(move(buffer), false);
              inbound.pop_output(bytes_written);

              if (inbound.eof() or inbound.error()) {
              _thread_data.shutdown(SHUT_WR);
              _inbound_shutdown = true;
              }
              },
              [&] {
              return (not _tcp->inbound_stream().buffer_empty()) or
              ((_tcp->inbound_stream().eof() or _tcp->inbound_stream().error()) and not _inbound_shutdown);
              });

              // rule 4: read outbound segments from TCPConnection and send as datagrams向外界写data
              _eventloop.add_rule(_datagram_adapter,
              Direction::Out,
              [&] {
              while (not _tcp->segments_out().empty()) {
              // 通过对adapter写实现
              _datagram_adapter.write(_tcp->segments_out().front());
              _tcp->segments_out().pop();
              }
              },
              [&] { return not _tcp->segments_out().empty(); });
              }
              - -
              _tcp_loop

              可以看到,_tcp_loop的功能就是,在condition为真的时候,一是监听我们之前塞进_event_loop的所有事件,二是调用TCPConnectiontick方法来管理时间。

              -
              // condition is a function returning true if loop should continue
              // Process events while specified condition is true
              // 周期性调用事件condition以达到监听等待事件的效果,管理TCP的tick
              template <typename AdaptT>
              void TCPSpongeSocket<AdaptT>::_tcp_loop(const function<bool()> &condition) {
              auto base_time = timestamp_ms();
              // 当条件一直为真时,监听event
              while (condition()) {
              // 持续监听eventloop中的各种event
              auto ret = _eventloop.wait_next_event(TCP_TICK_MS);
              // 条件为退出/丢弃
              if (ret == EventLoop::Result::Exit or _abort) {
              break;
              }
              // 如果tcp还存活,则调用其tick方法
              if (_tcp.value().active()) {
              const auto next_time = timestamp_ms();
              _tcp.value().tick(next_time - base_time);
              _datagram_adapter.tick(next_time - base_time);
              base_time = next_time;
              }
              }
              }
              - -
              eventloop

              eventloop具体是通过Linux提供的poll机制来进行事件监听的。

              -
              -

              Linux poll机制

              -

              怎么说,又一次感受到了“网络就是IO”这个抽象的牛逼之处。操作系统的poll机制和poll函数本质上是针对IO读写来设计的,而正因为网络的本质是IO,正因为网络收发数据包、与上层app交互本质还是IO(因为通过文件描述符),才能在这里采用这种方式进行文件读写。

              -

              我的评价是佩服到五体投地好吧

              -

              image-20230310185319115

              -

              poll函数就是IO等待的一种实现机制。

              -
              int poll(struct pollfd *fds, nfds_t nfds, int timeout);
              - -

              事件类型events可以为下列值:

              -
              POLLIN:有数据可读
              POLLRDNORM:有普通数据可读,等效于POLLIN
              POLLRDBAND:有优先数据可读
              POLLPRI:有紧迫数据可读
              POLLOUT:写数据不会导致阻塞
              POLLWRNORM:写普通数据不会导致阻塞
              POLLWRBAND:写优先数据不会导致阻塞
              POLLMSG:SIGPOLL消息可用
              POLLER:指定的文件描述符发生错误
              POLLHUP:指定的文件描述符挂起事件
              POLLNVAL:无效的请求,打不开指定的文件描述符
              -
              -

              我们在前面的eventloop的rule初始化中:

              -
              _eventloop.add_rule(_datagram_adapter,
              Direction::In,
              [&] { ... });
              - -

              这个的意思是针对_datagram_adapter这个文件的Direction::In这个事件发生时,就会执行[&]中的事件。那么Direction::In是什么?

              -
              enum class Direction : short {
              In = POLLIN, //!< Callback will be triggered when Rule::fd is readable.
              Out = POLLOUT //!< Callback will be triggered when Rule::fd is writable.
              };
              - -

              可见,eventloop具体是通过os提供的IO事件机制来进行监听的。

              -

              具体的监听以及执行逻辑由wait_next_event来实现。它主要干的就是,清理掉那些我们不感兴趣的或者已经似了(比如说对应的fd已经close之类的)的事件,然后找到那些触发到了的active的事件并且调用它们的caller。

              -

              具体代码还是有些微复杂的,有兴趣可以去看看,这里就不放了。

              -

              生命周期的管理

              核心部分为方法connectlisten_and_accept以及_tcp_main

              -
              connect

              由客户端调用。

              -
              // Client调用
              // 未收到外界连接时,owner进程会阻塞
              template <typename AdaptT>
              void TCPSpongeSocket<AdaptT>::connect(const TCPConfig &c_tcp, const FdAdapterConfig &c_ad) {
              // 初始化tcp的事件监听
              _initialize_TCP(c_tcp);
              // 初始化adapater
              _datagram_adapter.config_mut() = c_ad;

              cerr << "DEBUG: Connecting to " << c_ad.destination.to_string() << "...\n";
              // 我们实现的:发送SYN报文
              _tcp->connect();

              // 统一的状态管理
              const TCPState expected_state = TCPState::State::SYN_SENT;
              // 等待直到条件为假,也即脱离SYN-SENT转移到ESTABLISHED
              _tcp_loop([&] { return _tcp->state() == TCPState::State::SYN_SENT; });
              cerr << "Successfully connected to " << c_ad.destination.to_string() << ".\n";

              // 建立连接后开启connection进程, 执行_tcp_main,继续监听event直到死亡
              _tcp_thread = thread(&TCPSpongeSocket::_tcp_main, this);
              }
              - -
              _tcp_main

              负责establish状态的监听以及之后关闭TCP连接的擦屁股工作

              -
              template <typename AdaptT>
              void TCPSpongeSocket<AdaptT>::_tcp_main() {
              try {
              if (not _tcp.has_value()) {
              throw runtime_error("no TCP");
              }
              // 持续监听直到死亡
              _tcp_loop([] { return true; });
              shutdown(SHUT_RDWR);
              if (not _tcp.value().active()) {
              cerr << "DEBUG: TCP connection finished "
              << (_tcp.value().state() == TCPState::State::RESET ? "uncleanly" : "cleanly.\n");
              }
              _tcp.reset();
              } catch (const exception &e) {
              cerr << "Exception in TCPConnection runner thread: " << e.what() << "\n";
              throw e;
              }
              }
              - -
              listen_and_accept

              由服务器端调用。

              -
              // Server调用
              // 未收到外界连接时,owner进程会阻塞
              template <typename AdaptT>
              void TCPSpongeSocket<AdaptT>::listen_and_accept(const TCPConfig &c_tcp, const FdAdapterConfig &c_ad) {
              _initialize_TCP(c_tcp);
              _datagram_adapter.config_mut() = c_ad;

              _datagram_adapter.set_listening(true);

              cerr << "DEBUG: Listening for incoming connection...\n";
              // 等待直到ESTABLISHED。注意下这里的状态条件
              // 其中各种收发报文的事件由tcp_loop中的event做
              _tcp_loop([&] {
              const auto s = _tcp->state();
              return (s == TCPState::State::LISTEN or s == TCPState::State::SYN_RCVD or s == TCPState::State::SYN_SENT);
              });
              cerr << "New connection from " << _datagram_adapter.config().destination.to_string() << ".\n";

              // 开启connection进程
              _tcp_thread = thread(&TCPSpongeSocket::_tcp_main, this);
              }
              - -

              CS144TCPSocket 和 FullStackSocket

              主菜(上面那个)已经说完了,这两个就是简单的包装类,没什么好说的,大概就做了点传参工作,主要差异还是adapter。

              -

              Adapter实现

              在我们的TCPSpongeSocket实现中,我们引入了“adapter”的概念。

              -
                protected:
              //! Adapter to underlying datagram socket (e.g., UDP or IP)
              AdaptT _datagram_adapter;

              using TCPOverUDPSpongeSocket = TCPSpongeSocket<TCPOverUDPSocketAdapter>;
              using TCPOverIPv4SpongeSocket = TCPSpongeSocket<TCPOverIPv4OverTunFdAdapter>;
              using TCPOverIPv4OverEthernetSpongeSocket = TCPSpongeSocket<TCPOverIPv4OverEthernetAdapter>;

              using LossyTCPOverUDPSpongeSocket = TCPSpongeSocket<LossyTCPOverUDPSocketAdapter>;
              using LossyTCPOverIPv4SpongeSocket = TCPSpongeSocket<LossyTCPOverIPv4OverTunFdAdapter>;
              - -

              它很完美地以策略模式的形式,凝结出了我们本次实验所需的各种协议栈的共同代码,放进了TCPSpongeSocket,而将涉及到协议栈差异的部分用adapter完成。

              -

              TCPSpongeSocket中,adapter主要完成了如下操作:

              -
                -
              1. adapter的tick函数

                -
                // in tcp_loop
                _tcp.value().tick(next_time - base_time);
                _datagram_adapter.tick(next_time - base_time);
              2. -
              3. 作为订阅事件的IO流

                -
                _eventloop.add_rule(_datagram_adapter,
                Direction::In,
                [&] {
                // ...
              4. -
              5. TCP层通过对其读写来获取TCP segment

                -
                auto seg = _datagram_adapter.read();
                _datagram_adapter.write(_tcp->segments_out().front());
              6. -
              7. 记录各类参数

                -
                datagram_adapter.config().destination.to_string()
              8. -
              -

              Inheritance graph

              -

              具体实现说实话没什么好说的,确实无非也就是上面那几个方法,然后在里面包装下和操作系统提供的tun和tap的接口交互罢了,代码也比较简单,此处就不说了。

              -

              apps

              除了对协议栈的实现之外,在app文件夹下还有许多对我们实现的协议栈的应用实例。我认为了解下应用实例也是很重要的。

              -

              bidirectional_stream_copy

              其作用就是建立stdin/stdout与socket的关联。它从stdin读输入,作为上层app的输入写入socket;从socket读输出,传给上层app,也即stdout输出。它的具体实现在stdin/stdout之间隔了两条bytestream,分别是_inbound_outbound

              -

              由于stdin、stdout、socket本质上都是fd,所以我们依然可以采用跟上面一样的事件驱动方式。我们只需在socket有输出时马上读给inbound bytestream,在inbound bytestream有输入时马上读给stdout,在stdin有输入时马上写入outbound bytestream,在outbound bytestream有输入时马上读给socket。遵守这4条rule就行了。

              -

              因而,具体实现就是TCPSpongeSocket::_initialize_TCPTCPSpongeSocket::_tcp_loop的结合体,订阅事件+循环等待。由于跟前面类似,在此就不放代码了。

              -

              其他

              其他都太复杂了,感觉我水平一般还不大能理解,也懒得看了【草】总之先咕咕咕

              -]]> -
              - - Lab2 TCPReceiver - /2023/02/25/cs144$lab2/ - Lab2 TCPReceiver

              前置学习

              Overview

              承上启下

              在前两个实验中,我们可以说只是做了点算法上的抽象工作,跟TCP协议还是没什么显著的关系的。但来到了本次实验,一切就都不一样了。

              -

              image-20230226194708398

              -

              依然还是这张图。相信此时做过前两个实验之后,看到这张图就会有了不一样的发现。

              -

              我们对TCP协议的实现是由内向外的,先实现里面再实现最外层。前两节实验,我们由内而外实现了ByteStreamStreamReassembler;在这次实验中,我们会实现更外层一点的TCPReceiver

              -

              根据我们前两次实验内容,我们可以知道,TCPReceiver的功能之一就是,将数据包TCPSegment拆分成一个个data,并且通过seq生成出这些dataindex,然后传递给StreamReassembler

              -

              由外而内

              TCPConnection

              在说明TCPReceiver的其他功能前,不妨先从外面的TCPConnection说起,由外而内回忆一下整个TCP协议过程。

              -

              image-20230226200935395

              -

              image-20230226202631406

              -

              这期间最关键需要理解的,是SYN FIN ACK ack seq这些东西究竟是什么东西。

              -
              对象说明
              seq

              seq用来标识字节流中某个字节的序号,在TCP报文中,它表示的是该报文携带的数据的第一个字节的序号。

              -

              与我们在Lab1实现的StreamReassembler的参数index相比,它有三方面不同:

              -
                -
              1. seq为32位,index为64位

                -

                当一个字节流的数据超过2^32字节(实际上比这少就会环绕)时,seq就会产生环绕。如,当前seq为0xFFFFFFFF,则下一个seq就是0x00000000。

                -
              2. -
              3. seq不从0开始,index从0开始

                -

                为了确保传输过程中的安全性,一个字节流的起始seq不为0,而是一个随机数,称其为ISN

                -
              4. -
              5. seq有不携带数据的两个逻辑报文SYN和FIN,index没有

                -
              6. -
              -
              SYN

              SYN是TCP“三握手”中服务器端接收到的来自客户端的第一个报文。它是TCP报文中的一个标识位:

              -

              image-20230227135255525

              -

              它用以标识数据传输的开始,并且携带seq最初随机的序号ISN。

              -
              -

              除了确保收到所有字节的数据外,TCP必须确保也能收到流的开始和结束

              -

              这个说得非常好,完美解释了为什么需要占据一个seqno

              -
              -
              ACK

              ACK也是一个标识位,它代表当前报文是一个确认收到的报文ACK,也即报文中的ackno值有效

              -
              ack

              ack表示当前endpoint【包括客户端和服务器端】希望接收到的下一个数据流的起始字节的seq。

              -
              -

              关于seq和ack,听起来还是有点抽象,不如以连接释放图中ack和seq的值变化为例来说明。

              -

              image-20230226202631406

              -

              为什么一开始seq=u,ack=v,但下一个就是seq=v,ack=u+1?

              -

              这是因为,ack和seq的语义对于客户端和服务器端都是不变的。ack为已经收到的数据的seq+1表示第一个应该接收的值,seq为已经发送的数据的seq+1表示已经发送的值。并且还需要意识到,图中其实有两个数据流(一个是C→S,另一个是S→C),也即有两套seq和ack。

              -
                -
              • ack

                -

                服务器从客户端接收信号。ack表示服务器希望接收到的下一个序列号,也即为它从客户端收到的数据的seq+1。

                -

                对于此情况,虽然终止报文不携带数据,但其依然占据一个序列号seq。

                -

                因而服务器的ack=u+1.

                -
              • -
              • seq

                -

                服务器向客户端发送数据。ack表示客户端希望接收到的下一个序号,因而服务器端就应该发送ack这个序号的数据,也即v。

                -
              • -
              -
              -
              FIN

              FIN也是一个标识位,标识着数据传输的结束

              -
              接收报文类型

              因而,从图中可以看出,TCP连接中大概会收到以下几类报文:

              -
                -
              1. 特殊报文

                -
                  -
                1. SYN = 1

                  -
                    -
                  1. C的连接请求 携带了ISN

                    -
                  2. -
                  3. S的连接请求确认,ACK = 1,携带了S的ISN

                    -
                  4. -
                  -
                2. -
                3. ACK = 1

                  -

                  额我觉得这是TCPSender管的。这大概是Connection知道了之后通知下TCPSender吧,应该跟我们这次实验没关系

                  -
                4. -
                5. FIN = 1

                  -
                6. -
                -
              2. -
              3. 普通的数据

                -
              4. -
              -
              TCPReceiver的作用

              我们的TCPReceiver需要负责TCP协议中部分关键对象的管理。我们需要生成ackno以及拥塞窗口大小;我们需要接收SYN和FIN等信号;我们需要对seq进行处理,将其变为StreamReassembler所想要的index。

              -

              总结TCPReceiver的作用

                -
              1. 处理数据

                -

                把Internet过来的一个个TCP报文变成一个个小data,小data再由整流器整流为完整的data,外界再通过socket从ByteStream读取完整的data。

                -
              2. -
              3. 反馈信息

                -

                向发送方反馈自己当前的一些状态信息,如拥塞窗口的大小以及ack等。

                -
                  -
                1. ackno

                  -

                  本质上是“index of the first unassembled byte”

                  -
                2. -
                3. window size

                  -

                  本质上是“the distance between the first unassembled index and the first unacceptable index”

                  +
                4. domain

                  +

                  在本次实验中只会取值前两个,即本地通信和IPv4网络通信

                  +

                  image-20230309232045195

                5. -
                -

                也即,ackno为拥塞窗口的左端点,ackno+window_size为拥塞窗口的右端点

                +
              4. type

                +

                好像比如说取SOCK_DGRAM就是UDP,取SOCK_STREAM就是TCP。

              -

              64-bit indexes ←→ 32-bit seqnos

              从Overview中可以看出来,至关重要的一点就是,将环绕的32bit的seq转化为我们在StreamReassembler中使用的index。

              -

              我们不妨再引入一个中间变量abstract seqno。则seqnoabstract seqnostream index三者关系如下图:

              -

              image-20230227141242426

              -

              显然从seqno转化为abstract seqno更加复杂。因而,我们要做的第一个实验部分就是实现这个转化。

              -

              我们需要实现类WrappingInt32。它的wrap函数将64位的abstract seqno转化为32位的seqno,它的unwrap将32位的seqno转化为64位的abstract seqno

              -

              感想

              64-bit indexes ←→ 32-bit seqnos

              这个实验完美地触及到了我的雷点:对这种环绕来环绕去的东西非常头疼……因而昨天晚上做的时候晕晕乎乎的什么也思考不了,今天过来边画了下图才知道要怎么做。

              -

              wrap很简单我就不说了。对于unwrap,我的做法是,先让checkpoint和n-isn都处在同一个区间(红圈)内【也即都让它们对2^32取余】,再通过几个东西之间的关系来确定最终的res是否需要+-HEAD_ONE:

              -

              【蓝线表示n-isn,橙线表示红圈区间的中点】

              -

              image-20230227133550293

              -

              具体的就不多说了。直接看下面的代码,多画画图就能明白了。

              -

              TCPReceiver

              image-20230227231044428

              -

              心得

              我一开始头晕晕地去写,对很多地方产生了疑问,激情地写下了一些消极的话语。刚刚出去吹了会儿风回来,bug全都改对了,于是狂喜着把消极的话语全部删掉了()

              -

              怎么说呢,我的错误发生是因为我没有意识到sponge的TCP也许算是一个“简化版”。

              -

              在学习本章内容之前,我特地先去回顾了下TCP协议的全过程,并且所有的SYN,FIN等等等概念都是按照网上的概念来的。因而我在面对自己的错误时真的是一脸懵逼……好在,吹完风之后我还是及时醒悟了。

              -

              思路还是很简单的,细节也不像Lab1那样那么多那么破防,就是一些奇奇怪怪的恶心小毛病太多了,导致我出错频频,并且都是些很sb的问题,让人直接心态爆炸。

              -

              先不吐槽了,接下来就来讲讲总体的思路,以及我产生疑惑的一些地方吧。

              -

              思路

              基本流程

              得益于Lab1那个复杂算法的健壮性和多功能性,我们对TCPReceiver的实现就可以变得更加简洁。我们不再需要关心报文是否能够被成功接收、报文是否重叠等等等。我们仅需对SYN和FIN这样的报文做特殊的参数处理,将seqno转化为index,然后直接传入我们的StreamReassembler中就行了。

              -

              也即,基本流程为:

              -
                -
              1. 如果收到SYN报文,则对一些参数进行初始化,并且标记数据传输开始信号syn为true
              2. -
              3. 如果syn为true,则计算index后传入整流器
              4. -
              5. 判断是否需要加上FIN报文的比特位
              6. -
              -
              一些细节
              SYN和FIN各占一个seqno
              // SYN
              if(!syn&&header.syn){ // is the first packet
              // ...
              isn = header.seqno;
              seqno = seqno + 1; // plus one to skip the SYN byte
              // ...
              }
              // FIN
              if(header.fin) fin = true; // 这个一定要写在上面那个if的后面
              // ...
              if(_reassembler.empty() && fin){
              ack += 1;
              }
              +
              代码
              /* Socket */
              /* 构造器 */
              // default constructor for socket of (subclassed) domain and type
              Socket::Socket(const int domain, const int type) : FileDescriptor(SystemCall("socket", socket(domain, type, 0))) {}
              // construct from file descriptor
              Socket::Socket(FileDescriptor &&fd, const int domain, const int type) : FileDescriptor(move(fd)) { ... }

              // get the local or peer address the socket is connected to
              // 此为private函数,应该是用于方便下面那两个函数的,虽然我觉得这个设计意图没什么必要()
              Address Socket::get_address(const string &name_of_function,const function<int(int, sockaddr *, socklen_t *)> &function) const {
              Address::Raw address;
              socklen_t size = sizeof(address);
              SystemCall(name_of_function, function(fd_num(), address, &size));
              return {address, size};
              }
              Address Socket::local_address() const { return get_address("getsockname", getsockname); }
              Address Socket::peer_address() const { return get_address("getpeername", getpeername); }

              /*
              这两个函数是用于把socket连到CS的
              将socket的一端连上本机,就需要调用bind;连上别的什么东西就要用connect
              */
              // bind socket to a specified local address (usually to listen/accept)
              // address is a local Address to bind
              void Socket::bind(const Address &address) { SystemCall("bind", ::bind(fd_num(), address, address.size())); }
              // connect socket to a specified peer address
              // address is the peer's Address
              void Socket::connect(const Address &address) { SystemCall("connect", ::connect(fd_num(), address, address.size())); }

              // shut down a socket in the specified way
              // how can be `SHUT_RD`, `SHUT_WR`, or `SHUT_RDWR`
              void Socket::shutdown(const int how) {
              SystemCall("shutdown", ::shutdown(fd_num(), how));
              switch (how) {
              case SHUT_RD:
              register_read();
              break;
              // ...
              }
              }

              // set socket option,传入协议层以及要设置非选项的键和值
              template <typename option_type>
              void Socket::setsockopt(const int level, const int option, const option_type &option_value) {
              SystemCall("setsockopt", ::setsockopt(fd_num(), level, option, &option_value, sizeof(option_value)));
              }

              // allow local address to be reused sooner, at the cost of some robustness
              // 以鲁棒性为代价,让local address可复用
              // Using `SO_REUSEADDR` may reduce the robustness of your application
              void Socket::set_reuseaddr() { setsockopt(SOL_SOCKET, SO_REUSEADDR, int(true)); }

              /* UDPSocket */
              // 从socket中接收数据并放进datagram中
              // If mtu is too small to hold the received datagram, this method throws a runtime_error
              void UDPSocket::recv(received_datagram &datagram, const size_t mtu) {
              // receive source address and payload
              // ...
              const ssize_t recv_len = SystemCall(
              "recvfrom",
              ::recvfrom(
              fd_num(), datagram.payload.data(), datagram.payload.size(), MSG_TRUNC, datagram_source_address, &fromlen));
              // ...
              }
              UDPSocket::received_datagram UDPSocket::recv(const size_t mtu) {
              received_datagram ret{{nullptr, 0}, ""};
              recv(ret, mtu);
              return ret;
              }

              // 向socket发送数据
              void sendmsg_helper(const int fd_num,
              const sockaddr *destination_address,
              const socklen_t destination_address_len,
              const BufferViewList &payload) {
              // ...
              const ssize_t bytes_sent = SystemCall("sendmsg", ::sendmsg(fd_num, &message, 0));
              // ...
              }
              void UDPSocket::sendto(const Address &destination, const BufferViewList &payload) {
              sendmsg_helper(fd_num(), destination, destination.size(), payload);
              register_write();
              }
              void UDPSocket::send(const BufferViewList &payload) {
              sendmsg_helper(fd_num(), nullptr, 0, payload);
              register_write();
              }
              // ...
              -

              SYN很直观,没什么好说的。

              -

              FIN比较烧。之所以不是这么写:

              -
              if(header.fin){
              ack += 1;
              }
              +

              * TCPSpongeSocket

              上面那俩类其实就是两个包装类,用来将系统调用包装为c++类,看起来很抽象很迷惑。但到这就不一样了!我们开始用上我们之前写的TCP协议的代码了!

              +

              除了跟fd以及socket一致的readwrite以及close之外,TCPSocket最独特的功能,应该就是TCP连接的建立与释放了,其状态转移等逻辑已由我们在Lab0-4实现,此socket类仅实现事件的监听TCP协议对象生命周期的管理

              +

              双线程

              在详细说明其两个功能——事件监听和生命周期管理——之前,不妨先了解下其总体的架构。

              +

              TCPSpongeSocket需要双线程实现。其中一个线程用来招待其owner:它会执行向owner public的connect、read、write等服务。另一个线程用来运行TCPConnection:它会时刻调用connection的tick方法,并且进行事件监听。

              +
              //! \class TCPSpongeSocket
              //! This class involves the simultaneous operation of two threads.
              //!
              //! One, the "owner" or foreground thread, interacts with this class in much the
              //! same way as one would interact with a TCPSocket: it connects or listens, writes to
              //! and reads from a reliable data stream, etc. Only the owner thread calls public
              //! methods of this class.
              //!
              //! The other, the "TCPConnection" thread, takes care of the back-end tasks that the kernel would
              //! perform for a TCPSocket: reading and parsing datagrams from the wire, filtering out
              //! segments unrelated to the connection, etc.
              -

              也即一发现FIN报文到了就++,是因为可能会发生这种情况:

              -

              image-20230227224055002

              -

              也即FIN报文虽然到了,但是中间有一段数据还没到,ack应该等于中间那段数据的开头,你这时候想要跳过FIN而把ack+1那肯定是不对的。

              -

              也因而,我们需要记录fin是否有过,并且仅当:

              -
              bool StreamReassembler::empty() const { return buffer.empty()&&is_eof; }
              +

              事件监听

              完成事件监听的核心部分是方法_tcp_loop以及_initialize_TCP中对_eventloop的初始化,还有eventloop的实现。

              +

              看下来其实理解难度不大(虽然细节很多并且我懒得研究了),但我认为很值得学习。

              +
              _initialize_TCP

              主要功能是添加我们想监听的事件,有四个,分别是从app得到数据、有要向app发送的数据、从底层协议得到数据、有要向底层协议发送的数据。具体的话,代码和注释都写得很详细就不说了。

              +

              可以看到,TCP与协议栈交互【包括收发数据报】,是通过AdaptT _datagram_adapter;实现的;TCP与上层APP交互【包括传送数据】,是通过LocalStreamSocket _thread_data;实现的。

              +
              template <typename AdaptT>
              void TCPSpongeSocket<AdaptT>::_initialize_TCP(const TCPConfig &config) {
              _tcp.emplace(config);
              // Set up the event loop

              // There are four possible events to handle:需要监听以下四种事件
              //
              // 1) Incoming datagram received (needs to be given to
              // TCPConnection::segment_received method)得到底层协议栈送过来的data
              //
              // 2) Outbound bytes received from local application via a write()
              // call (needs to be read from the local stream socket and
              // given to TCPConnection::data_written method)得到上层app送过来的data
              //
              // 3) Incoming bytes reassembled by the TCPConnection
              // (needs to be read from the inbound_stream and written
              // to the local stream socket back to the application)TCP协议需要向app写入data
              //
              // 4) Outbound segment generated by TCP (needs to be
              // given to underlying datagram socket)TCP需要向外界发送data

              // rule 1: read from filtered packet stream and dump into TCPConnection得到外界data
              _eventloop.add_rule(_datagram_adapter,
              Direction::In,
              [&] {
              auto seg = _datagram_adapter.read();
              if (seg) {
              _tcp->segment_received(move(seg.value()));
              }
              if (_thread_data.eof() and _tcp.value().bytes_in_flight() == 0 and not _fully_acked) { _fully_acked = true; }
              },
              [&] { return _tcp->active(); });

              // rule 2: read from pipe into outbound buffer得到app data
              _eventloop.add_rule(
              // LocalStreamSocket _thread_data;
              // 看来用户是通过socket写入的数据
              _thread_data,
              Direction::In,
              [&] {
              const auto data = _thread_data.read(_tcp->remaining_outbound_capacity());
              const auto len = data.size();
              const auto amount_written = _tcp->write(move(data));
              if (amount_written != len) {
              throw runtime_error("TCPConnection::write() accepted less than advertised length");
              }
              if (_thread_data.eof()) {
              _tcp->end_input_stream();
              _outbound_shutdown = true;
              }
              },
              [&] { return (_tcp->active()) and (not _outbound_shutdown) and (_tcp->remaining_outbound_capacity() > 0); },
              [&] {
              _tcp->end_input_stream();
              _outbound_shutdown = true;
              });

              // rule 3: read from inbound buffer into pipe向app写入data
              _eventloop.add_rule(
              _thread_data,
              Direction::Out,
              [&] {
              ByteStream &inbound = _tcp->inbound_stream();
              // Write from the inbound_stream into the pipe
              const size_t amount_to_write = min(size_t(65536), inbound.buffer_size());
              const std::string buffer = inbound.peek_output(amount_to_write);
              // 通过向socket写实现
              const auto bytes_written = _thread_data.write(move(buffer), false);
              inbound.pop_output(bytes_written);

              if (inbound.eof() or inbound.error()) {
              _thread_data.shutdown(SHUT_WR);
              _inbound_shutdown = true;
              }
              },
              [&] {
              return (not _tcp->inbound_stream().buffer_empty()) or
              ((_tcp->inbound_stream().eof() or _tcp->inbound_stream().error()) and not _inbound_shutdown);
              });

              // rule 4: read outbound segments from TCPConnection and send as datagrams向外界写data
              _eventloop.add_rule(_datagram_adapter,
              Direction::Out,
              [&] {
              while (not _tcp->segments_out().empty()) {
              // 通过对adapter写实现
              _datagram_adapter.write(_tcp->segments_out().front());
              _tcp->segments_out().pop();
              }
              },
              [&] { return not _tcp->segments_out().empty(); });
              }
              -

              成立时,才能表示数据传输真正结束,让ack++。

              -
              以abstract seqno的形式保存ackno

              说实话我一开始ackno的数据结构是WrappingInt32。为了这么搞,我还得特地维护一个checkpoint变量用来做unwrap的参数,然后ackno也不能用_reassembler.get_left_bound()来获取,总之就搞得非常非常麻烦。这时候我不小心【是故意的还是不小心的?】看到了感恩的代码,对其用abstract seqno保存ackno这个想法大为赞叹,于是就果断地沿用了()果然设计思想方面我还是有很大不足啊。

              -

              疑惑

              关于特殊报文

              我一开始被这个图以及百度得到的结果受影响:

              -

              image-20230227224429692

              -

              image-20230227224449780

              -

              认为SYN报文不能携带数据【同理FIN也是】,因而在最初实现的时候看到test case人都麻透了开始怀疑人生……

              -

              不过这也怪我没有意识到实验和业界可能是不一样的,但指导书也没说SYN和FIN到底会不会携带数据……emm,我感觉这一点做得不够详细,也许可以改进一下。

              -
              关于window size的定义

              我现在还是搞不懂这东西究竟是什么玩意……

              -

              指导书上是这么说的:

              +
              _tcp_loop

              可以看到,_tcp_loop的功能就是,在condition为真的时候,一是监听我们之前塞进_event_loop的所有事件,二是调用TCPConnectiontick方法来管理时间。

              +
              // condition is a function returning true if loop should continue
              // Process events while specified condition is true
              // 周期性调用事件condition以达到监听等待事件的效果,管理TCP的tick
              template <typename AdaptT>
              void TCPSpongeSocket<AdaptT>::_tcp_loop(const function<bool()> &condition) {
              auto base_time = timestamp_ms();
              // 当条件一直为真时,监听event
              while (condition()) {
              // 持续监听eventloop中的各种event
              auto ret = _eventloop.wait_next_event(TCP_TICK_MS);
              // 条件为退出/丢弃
              if (ret == EventLoop::Result::Exit or _abort) {
              break;
              }
              // 如果tcp还存活,则调用其tick方法
              if (_tcp.value().active()) {
              const auto next_time = timestamp_ms();
              _tcp.value().tick(next_time - base_time);
              _datagram_adapter.tick(next_time - base_time);
              base_time = next_time;
              }
              }
              }
              + +
              eventloop

              eventloop具体是通过Linux提供的poll机制来进行事件监听的。

              -

              the distance between the “first unassembled” index and the “first unacceptable” index.

              -

              This is called the “window size”.

              +

              Linux poll机制

              +

              怎么说,又一次感受到了“网络就是IO”这个抽象的牛逼之处。操作系统的poll机制和poll函数本质上是针对IO读写来设计的,而正因为网络的本质是IO,正因为网络收发数据包、与上层app交互本质还是IO(因为通过文件描述符),才能在这里采用这种方式进行文件读写。

              +

              我的评价是佩服到五体投地好吧

              +

              image-20230310185319115

              +

              poll函数就是IO等待的一种实现机制。

              +
              int poll(struct pollfd *fds, nfds_t nfds, int timeout);
              + +

              事件类型events可以为下列值:

              +
              POLLIN:有数据可读
              POLLRDNORM:有普通数据可读,等效于POLLIN
              POLLRDBAND:有优先数据可读
              POLLPRI:有紧迫数据可读
              POLLOUT:写数据不会导致阻塞
              POLLWRNORM:写普通数据不会导致阻塞
              POLLWRBAND:写优先数据不会导致阻塞
              POLLMSG:SIGPOLL消息可用
              POLLER:指定的文件描述符发生错误
              POLLHUP:指定的文件描述符挂起事件
              POLLNVAL:无效的请求,打不开指定的文件描述符
              -

              所谓的“first unassembled”正是ackno。而,我正是理解错了所谓“first unacceptable” 的意思,才导致我想了好久好久都没想出来,最后看了答案被薄纱到现在。

              -

              看到这个“first unacceptable” ,我的第一反应就是,维护一个变量right_bound,当packet过来的时候,如果packet的index范围(seqno + data.length())比right_bound大就更新。我认为这才叫做“first unacceptable”。但其实!我会这么想是因为我英语不好……

              -

              “first unacceptable” ,unacceptable,意为无法接受的,也就是说,它跟容量有关。第一个无法接受的,就是第一个超出容量的。而结合我们上面的那张图:

              -

              image-20230225232723083

              -

              可以看出,事实上window size就是黑框部分,也即紫框部分减去绿色部分,也即ByteStreamremaining_capacity()……

              -

              而我以为它是还未收到的的意思,故而才理解成了上面那样。

              -

              看来英语不好也是原罪23333

              -

              代码

              64-bit indexes ←→ 32-bit seqnos

              //! Transform an "absolute" 64-bit sequence number (zero-indexed) into a WrappingInt32
              //! \param n The input absolute 64-bit sequence number
              //! \param isn The initial sequence number
              WrappingInt32 wrap(uint64_t n, WrappingInt32 isn) {
              uint32_t tmp = (n & TAIL_MASK);
              return isn + tmp;
              }

              //! Transform a WrappingInt32 into an "absolute" 64-bit sequence number (zero-indexed)
              //! \param n The relative sequence number
              //! \param isn The initial sequence number
              //! \param checkpoint A recent absolute 64-bit sequence number
              //! \returns the 64-bit sequence number that wraps to `n` and is closest to `checkpoint`
              //!
              //! \note Each of the two streams of the TCP connection has its own ISN. One stream
              //! runs from the local TCPSender to the remote TCPReceiver and has one ISN,
              //! and the other stream runs from the remote TCPSender to the local TCPReceiver and
              //! has a different ISN.
              uint64_t unwrap(WrappingInt32 n, WrappingInt32 isn, uint64_t checkpoint) {
              uint32_t tmp_n = n.raw_value() - isn.raw_value();
              uint64_t res = (checkpoint & HEAD_MASK);
              uint32_t tmp_cp = (checkpoint & TAIL_MASK);

              res |= tmp_n;
              if(tmp_cp < FLAG){
              if(tmp_n > tmp_cp + FLAG){
              if(res >= HEAD_ONE) res -= HEAD_ONE;
              }
              }else if(tmp_cp > FLAG){
              if(tmp_n < tmp_cp - FLAG) res += HEAD_ONE;
              }
              return res;
              }
              +

              我们在前面的eventloop的rule初始化中:

              +
              _eventloop.add_rule(_datagram_adapter,
              Direction::In,
              [&] { ... });
              -

              TCPReceiver

              头文件

              class TCPReceiver {
              StreamReassembler _reassembler;

              size_t _capacity;
              uint64_t ack = 0;
              WrappingInt32 isn = WrappingInt32(0);

              bool syn = false;
              bool fin = false;
              // ...
              +

              这个的意思是针对_datagram_adapter这个文件的Direction::In这个事件发生时,就会执行[&]中的事件。那么Direction::In是什么?

              +
              enum class Direction : short {
              In = POLLIN, //!< Callback will be triggered when Rule::fd is readable.
              Out = POLLOUT //!< Callback will be triggered when Rule::fd is writable.
              };
              -

              具体实现

              void TCPReceiver::segment_received(const TCPSegment &seg) {
              TCPHeader header = seg.header();
              WrappingInt32 seqno = header.seqno;
              string data = seg.payload().copy();
              size_t index = 0; // the param of the reassembler

              // LISTENING -> SYN_SENT
              if(!syn&&header.syn){ // is the first packet
              _reassembler.set_is_eof();// reset the eof flag
              fin = false;// reset the fin flag
              isn = header.seqno;
              seqno = seqno + 1; // plus one to skip the SYN byte
              syn = true;// mark the start of the byte stream
              }

              // must keep after the last if branch to avoid the case "flag = SF"
              // FIN_RECEIVED
              if(header.fin) fin = true;
              if(syn){
              uint64_t abs_seqno = unwrap(seqno,isn,ack);
              index = abs_seqno - 1;
              if (abs_seqno != 0)// write into the assembler
              _reassembler.push_substring(data,index,header.fin);
              ack = _reassembler.get_left_bound() + 1;

              if (_reassembler.stream_out().input_ended() && fin)
              ack += 1;// plus one to skip the FIN byte
              }
              }
              optional<WrappingInt32> TCPReceiver::ackno() const {
              if(syn) return wrap(ack,isn);
              else return {};// empty
              }

              size_t TCPReceiver::window_size() const {
              return stream_out().remaining_capacity();
              }
              +

              可见,eventloop具体是通过os提供的IO事件机制来进行监听的。

              +

              具体的监听以及执行逻辑由wait_next_event来实现。它主要干的就是,清理掉那些我们不感兴趣的或者已经似了(比如说对应的fd已经close之类的)的事件,然后找到那些触发到了的active的事件并且调用它们的caller。

              +

              具体代码还是有些微复杂的,有兴趣可以去看看,这里就不放了。

              +

              生命周期的管理

              核心部分为方法connectlisten_and_accept以及_tcp_main

              +
              connect

              由客户端调用。

              +
              // Client调用
              // 未收到外界连接时,owner进程会阻塞
              template <typename AdaptT>
              void TCPSpongeSocket<AdaptT>::connect(const TCPConfig &c_tcp, const FdAdapterConfig &c_ad) {
              // 初始化tcp的事件监听
              _initialize_TCP(c_tcp);
              // 初始化adapater
              _datagram_adapter.config_mut() = c_ad;

              cerr << "DEBUG: Connecting to " << c_ad.destination.to_string() << "...\n";
              // 我们实现的:发送SYN报文
              _tcp->connect();

              // 统一的状态管理
              const TCPState expected_state = TCPState::State::SYN_SENT;
              // 等待直到条件为假,也即脱离SYN-SENT转移到ESTABLISHED
              _tcp_loop([&] { return _tcp->state() == TCPState::State::SYN_SENT; });
              cerr << "Successfully connected to " << c_ad.destination.to_string() << ".\n";

              // 建立连接后开启connection进程, 执行_tcp_main,继续监听event直到死亡
              _tcp_thread = thread(&TCPSpongeSocket::_tcp_main, this);
              }
              + +
              _tcp_main

              负责establish状态的监听以及之后关闭TCP连接的擦屁股工作

              +
              template <typename AdaptT>
              void TCPSpongeSocket<AdaptT>::_tcp_main() {
              try {
              if (not _tcp.has_value()) {
              throw runtime_error("no TCP");
              }
              // 持续监听直到死亡
              _tcp_loop([] { return true; });
              shutdown(SHUT_RDWR);
              if (not _tcp.value().active()) {
              cerr << "DEBUG: TCP connection finished "
              << (_tcp.value().state() == TCPState::State::RESET ? "uncleanly" : "cleanly.\n");
              }
              _tcp.reset();
              } catch (const exception &e) {
              cerr << "Exception in TCPConnection runner thread: " << e.what() << "\n";
              throw e;
              }
              }
              + +
              listen_and_accept

              由服务器端调用。

              +
              // Server调用
              // 未收到外界连接时,owner进程会阻塞
              template <typename AdaptT>
              void TCPSpongeSocket<AdaptT>::listen_and_accept(const TCPConfig &c_tcp, const FdAdapterConfig &c_ad) {
              _initialize_TCP(c_tcp);
              _datagram_adapter.config_mut() = c_ad;

              _datagram_adapter.set_listening(true);

              cerr << "DEBUG: Listening for incoming connection...\n";
              // 等待直到ESTABLISHED。注意下这里的状态条件
              // 其中各种收发报文的事件由tcp_loop中的event做
              _tcp_loop([&] {
              const auto s = _tcp->state();
              return (s == TCPState::State::LISTEN or s == TCPState::State::SYN_RCVD or s == TCPState::State::SYN_SENT);
              });
              cerr << "New connection from " << _datagram_adapter.config().destination.to_string() << ".\n";

              // 开启connection进程
              _tcp_thread = thread(&TCPSpongeSocket::_tcp_main, this);
              }
              + +

              CS144TCPSocket 和 FullStackSocket

              主菜(上面那个)已经说完了,这两个就是简单的包装类,没什么好说的,大概就做了点传参工作,主要差异还是adapter。

              +

              Adapter实现

              在我们的TCPSpongeSocket实现中,我们引入了“adapter”的概念。

              +
                protected:
              //! Adapter to underlying datagram socket (e.g., UDP or IP)
              AdaptT _datagram_adapter;

              using TCPOverUDPSpongeSocket = TCPSpongeSocket<TCPOverUDPSocketAdapter>;
              using TCPOverIPv4SpongeSocket = TCPSpongeSocket<TCPOverIPv4OverTunFdAdapter>;
              using TCPOverIPv4OverEthernetSpongeSocket = TCPSpongeSocket<TCPOverIPv4OverEthernetAdapter>;

              using LossyTCPOverUDPSpongeSocket = TCPSpongeSocket<LossyTCPOverUDPSocketAdapter>;
              using LossyTCPOverIPv4SpongeSocket = TCPSpongeSocket<LossyTCPOverIPv4OverTunFdAdapter>;
              + +

              它很完美地以策略模式的形式,凝结出了我们本次实验所需的各种协议栈的共同代码,放进了TCPSpongeSocket,而将涉及到协议栈差异的部分用adapter完成。

              +

              TCPSpongeSocket中,adapter主要完成了如下操作:

              +
                +
              1. adapter的tick函数

                +
                // in tcp_loop
                _tcp.value().tick(next_time - base_time);
                _datagram_adapter.tick(next_time - base_time);
              2. +
              3. 作为订阅事件的IO流

                +
                _eventloop.add_rule(_datagram_adapter,
                Direction::In,
                [&] {
                // ...
              4. +
              5. TCP层通过对其读写来获取TCP segment

                +
                auto seg = _datagram_adapter.read();
                _datagram_adapter.write(_tcp->segments_out().front());
              6. +
              7. 记录各类参数

                +
                datagram_adapter.config().destination.to_string()
              8. +
              +

              Inheritance graph

              +

              具体实现说实话没什么好说的,确实无非也就是上面那几个方法,然后在里面包装下和操作系统提供的tun和tap的接口交互罢了,代码也比较简单,此处就不说了。

              +

              apps

              除了对协议栈的实现之外,在app文件夹下还有许多对我们实现的协议栈的应用实例。我认为了解下应用实例也是很重要的。

              +

              bidirectional_stream_copy

              其作用就是建立stdin/stdout与socket的关联。它从stdin读输入,作为上层app的输入写入socket;从socket读输出,传给上层app,也即stdout输出。它的具体实现在stdin/stdout之间隔了两条bytestream,分别是_inbound_outbound

              +

              由于stdin、stdout、socket本质上都是fd,所以我们依然可以采用跟上面一样的事件驱动方式。我们只需在socket有输出时马上读给inbound bytestream,在inbound bytestream有输入时马上读给stdout,在stdin有输入时马上写入outbound bytestream,在outbound bytestream有输入时马上读给socket。遵守这4条rule就行了。

              +

              因而,具体实现就是TCPSpongeSocket::_initialize_TCPTCPSpongeSocket::_tcp_loop的结合体,订阅事件+循环等待。由于跟前面类似,在此就不放代码了。

              +

              其他

              其他都太复杂了,感觉我水平一般还不大能理解,也懒得看了【草】总之先咕咕咕

              ]]>
              @@ -7648,114 +7427,225 @@ url访问填写http://localhost/webdemo4_war/*.do。 ]]> - Lab3 TCPSender - /2023/02/25/cs144$lab3/ - Lab3 TCPSender

              前置知识

              在TCP协议中,TCPSender负责对ack进行处理,将字节流封装为TCP报文,根据拥塞窗口的大小传输数据,以及管理超时重传。

              -

              我们的TCPSender需要做的是:

              + Lab0 + /2023/02/25/cs144$lab0/ + Lab0
              +

              本次实验一直在强调的一点就是,TCP的功能是将底层的零散数据包,拼接成一个reliable in-order的byte stream。这个对我来说非常“振聋发聩”(夸张了233),以前只是背诵地知道TCP的可靠性,这次我算是第一次知道了所谓“可靠”究竟可靠在哪:一是保证了序列有序性,二是保证了数据不丢失(从软件层面)。

              +

              还有一个就是大致了解了cs144的主题:实现TCP协议。也就是说,运输层下面的那些层是不用管的吗?不过这样也挺恰好,我正好在学校的实验做过对下面这些层的实现了,就差一个TCP23333这样一来,我的协议栈就可以完整了。

              +
              +
              +

              本次实验与TCP的关系:

              +

              在我们的webget实现中,正是由于TCP的可靠传输,才能使我们的http request正确地被服务器接收,才能使服务器的response正确地被我们接收打印。

              +

              而在ByteStream中,我们也做了跟TCP类似的工作:接收substring,并且将它们拼接为in-order的byte stream【由于在内存中/单线程,所以这个工作看起来非常简单】:

              +
              while(is_input_end == false&&pointer<length){
              if(buffer.size() == capacity) break;
              buffer.push_back(data[pointer]);
              pointer++;
              }
              +
              +

              Fetch a Web page

              主要是介绍了telnet指令

              +

              屏幕截图 2023-02-23 194758

              +

              Send yourself an email

              用的是telnet带smtp参

              +

              Listening and connecting

              上面的telnet是一个client program。接下来我们要把自己放在server的位置上。

              +

              用的是netcat指令。

              +

              image-20230223202202509

              +

              Use socket to write webget

              这个确实不难,就是这个地方有点坑:

              +
              +

              Please note that in HTTP, each line must be ended with “\r\n” (it’s not sufficient to use just “\n” or endl).

              +
              +

              导致我跟400 Bad Request大眼瞪小眼了好久。。。

              +
              void get_URL(const string &host, const string &path) {
              TCPSocket sock;
              string tmp;
              // sock.set_blocking(true);// 默认情况下即为true
              sock.connect(Address(host,"http"));
              sock.write("GET " + path + " HTTP/1.1\r\nHost: " +
              host + "\r\nConnection: close\r\n\r\n");

              while((tmp = sock.read(1)) != ""){
              cout << tmp;
              }
              /*
              上面那个写法不大规范,更规范的写法:
              while(!sock.eof()){
              cout << sock.read(1);
              }
              */
              sock.close();
              }
              + +

              还有一点值得注意的是,当我这样时:

              +
              TCPSocket sock;
              sock.set_blocking(false);
              sock.connect(Address(host,"http"));
              + +

              会报错Operation now in progress

              +
              +

              关于socket通信中在connect()遇到的Operation now in progress错误

              +

              遇到此错误是因为将在connect()函数之前将套接字socket设为了非阻塞模式。改为在connect()函数之后设置即可。

              +
              +

              我觉得这个实验设计得挺好的,写的时候感觉很有意思。我推荐看下 https://github.com/shootfirst/CS144/blob/main/lab-0/apps/webget.cc 里的注释,写得很好很规范,让我明白了很多本来没搞懂的地方,比如说shutdown的用法。

              +

              An in-memory reliable byte stream

              +

              实现一个ByteStream类,可以通过readwrite对其两端进行读写。是单线程程序,因而无需考虑阻塞。

              +
              +

              感想

              这东西其实是很简单的,但是我还是花了一定的时间,主要原因有两点,一是我不懂c++,所以一些地方错得我很懵逼,二是因为我是sb。

              +

              下面就记录下三个我印象比较深刻的错误吧。

              +
              错误1 member initialization list

              构造函数我一开始是这么写的:

              +

              image-20230224113108208

              +

              结果爆出了这样的错:

              +

              image-20230224112056879

              +

              搜了半天也没看懂怎么回事,去求助了下某场外c艹选手,才知道了还有成员变量初始化列表这玩意,这个东西似乎比较高效安全。

              +

              于是我改成了这么写:

              +

              image-20230224113333962

              +

              它告诉我buffer也得初始化。于是我又这么写:

              +

              image-20230224113358856

              +

              又是奇奇怪怪的错误,说明vector不能这么初始化。

              +

              场外c艹选手看到了这个:

              +

              image-20230224113456432

              +

              所以说vector应该这样初始化:

              +

              image-20230224113549970

              +
              错误2 使用了vector作为buffer的载体

              应该使用的是可以从front删除数据的数据结构,比如说deque。【vector也行,但是效率较低】

              +

              具体为什么,可以以数据流为cat为例。执行peek(2)时,使用vector得到的是at,使用deque得到的是ca。

              +
              错误3 错误地阻塞

              一开始在write方法,我是这么写的:

              +
              int length = data.length();
              while(is_input_end == false&&pointer<length){
              while(buffer.size() == capacity);
              buffer.push_back(data[pointer]);
              pointer++;
              total_write ++;
              }
              + +

              结果就是测试用例Timeout。我找了很久都不知道错在了哪,最后求助了场外观众【罪过……这次实验太不独立了】,学着他把length改成了这样:

              +
              int length = min(data.length(),capacity-buffer.size());
              + +

              发现成了。

              +

              我去看了看testbench,猜测应该是因为阻塞了,我还以为是deque自身会阻塞【是的,我完全没注意到自己顺手把阻塞写了下去】,查了半天发现不会,最后才发现是自己不小心搞错了呃呃…………

              +

              代码

              头文件声明

              class ByteStream {
              private:
              // Your code here -- add private members as necessary.

              // Hint: This doesn't need to be a sophisticated data structure at
              // all, but if any of your tests are taking longer than a second,
              // that's a sign that you probably want to keep exploring
              // different approaches.

              size_t total_write;
              size_t total_read;
              bool is_input_end;
              const size_t capacity;
              deque<char> buffer;
              + +

              具体实现

              ByteStream::ByteStream(const size_t cap) : total_write(0),total_read(0),is_input_end(false),capacity(cap),buffer(){ }

              //! Write a string of bytes into the stream. Write as many
              //! as will fit, and return how many were written.
              //! \returns the number of bytes accepted into the stream
              size_t ByteStream::write(const string &data) {
              if(is_input_end == true) is_input_end = false;
              int pointer = 0;
              int length = data.length();
              while(is_input_end == false&&pointer<length){
              if(buffer.size() == capacity) break;
              buffer.push_back(data[pointer]);
              pointer++;
              }
              total_write+=pointer;
              return pointer;
              }
              //! Peek at next "len" bytes of the stream
              //! \param[in] len bytes will be copied from the output side of the buffer
              string ByteStream::peek_output(const size_t len) const {
              string res;
              size_t i = 0;
              for (auto it = buffer.begin(); it != buffer.end(); it++) {
              if (i >= len)
              break;
              i++;
              res.push_back(*it);
              }
              return res;
              }

              //! Remove bytes from the buffer
              //! \param[in] len bytes will be removed from the output side of the buffer
              void ByteStream::pop_output(const size_t len) {
              size_t i;
              for (i = 0; i < len; i++) {
              if (buffer.empty())
              break;
              buffer.pop_front();
              }
              total_read+=i;
              }

              //! Read (i.e., copy and then pop) the next "len" bytes of the stream
              //! \param[in] len bytes will be popped and returned
              //! \returns a string
              std::string ByteStream::read(const size_t len) {
              string res = peek_output(len);
              pop_output(len);
              return res;
              }

              void ByteStream::end_input() {is_input_end = true;}

              bool ByteStream::input_ended() const { return is_input_end; }

              size_t ByteStream::buffer_size() const { return buffer.size(); }

              bool ByteStream::buffer_empty() const { return buffer.empty(); }

              bool ByteStream::eof() const { return is_input_end && buffer.empty(); }

              size_t ByteStream::bytes_written() const { return total_write; }

              size_t ByteStream::bytes_read() const { return total_read; }

              size_t ByteStream::remaining_capacity() const { return capacity - buffer.size(); }
              +]]>
              +
              + + Lab2 TCPReceiver + /2023/02/25/cs144$lab2/ + Lab2 TCPReceiver

              前置学习

              Overview

              承上启下

              在前两个实验中,我们可以说只是做了点算法上的抽象工作,跟TCP协议还是没什么显著的关系的。但来到了本次实验,一切就都不一样了。

              +

              image-20230226194708398

              +

              依然还是这张图。相信此时做过前两个实验之后,看到这张图就会有了不一样的发现。

              +

              我们对TCP协议的实现是由内向外的,先实现里面再实现最外层。前两节实验,我们由内而外实现了ByteStreamStreamReassembler;在这次实验中,我们会实现更外层一点的TCPReceiver

              +

              根据我们前两次实验内容,我们可以知道,TCPReceiver的功能之一就是,将数据包TCPSegment拆分成一个个data,并且通过seq生成出这些dataindex,然后传递给StreamReassembler

              +

              由外而内

              TCPConnection

              在说明TCPReceiver的其他功能前,不妨先从外面的TCPConnection说起,由外而内回忆一下整个TCP协议过程。

              +

              image-20230226200935395

              +

              image-20230226202631406

              +

              这期间最关键需要理解的,是SYN FIN ACK ack seq这些东西究竟是什么东西。

              +
              对象说明
              seq

              seq用来标识字节流中某个字节的序号,在TCP报文中,它表示的是该报文携带的数据的第一个字节的序号。

              +

              与我们在Lab1实现的StreamReassembler的参数index相比,它有三方面不同:

                -
              1. 维护拥塞窗口

                -

                image-20230228105405827

                -

                我们需要通过ackno和window_size两个参数维护拥塞窗口的大小

                -
              2. -
              3. 填充拥塞窗口

                -

                必须as possible。除非拥塞窗口满或者ByteStream空才不填。

                -

                对于从ByteStream读出的数据,我们需要把其封装为一个TCPSegment再向_segment_out输出

                +
              4. seq为32位,index为64位

                +

                当一个字节流的数据超过2^32字节(实际上比这少就会环绕)时,seq就会产生环绕。如,当前seq为0xFFFFFFFF,则下一个seq就是0x00000000。

              5. -
              6. 记录哪一部分ack了,哪一部分没有ack

                -

                我们需要在发送segment的同时暂存segment,当且仅当接收到ack,并且ack为segment.seqno+length的时候才能将其释放。

                +
              7. seq不从0开始,index从0开始

                +

                为了确保传输过程中的安全性,一个字节流的起始seq不为0,而是一个随机数,称其为ISN

              8. -
              9. 管理超时重传

                -

                当对方超过一段时间还没有收到数据时,需要进行超时重传

                -

                以segment为单位,一个segment重传具有原子性。

                -

                在sender和暂存segment的数据结构中保存时钟滴答

                +
              10. seq有不携带数据的两个逻辑报文SYN和FIN,index没有

              -

              特别的,指导书上有一段话表述得很有意思:

              -

              image-20230228110046088

              -

              这体现了TCPReceiverTCPSender之间的对偶关系,这种细节性的设计理念值得学习。

              -

              感想

              写完TCPSender后我还是觉得有些迷茫……就跟TCPReceiver一样。说不出来具体是哪里不清楚,但总感觉隐隐约约有些怪怪的?总感觉相互之间接口有点混乱,对它们之间是怎么交互的一概不知。我想这是由于我们是自底向上实现TCP协议所带来的问题。希望这种感觉在实现完TCPConnection之后可以好转吧。

              -

              TCPReceiver的主要任务是把segment拼接成字节流,以及维护即将要告知TCPSender的ackno和拥塞窗口大小。而TCPSender的作用就是把字节流切成segment,并且根据ackno和拥塞窗口大小,进行数据的填充以及超时重传的管理。可以看到,它们是对偶的关系。

              -

              初见思路

              看完指导书以及各种接口定义可以得知,我们需要:

              -
                -
              1. 增加成员变量

                -
                  -
                1. window_size 拥塞窗口的大小

                  -
                2. -
                3. ackono 记录当前收到的最大ackno

                  -
                4. -
                5. ticks 记录sender从出生到现在的时钟滴答

                  +
                  SYN

                  SYN是TCP“三握手”中服务器端接收到的来自客户端的第一个报文。它是TCP报文中的一个标识位:

                  +

                  image-20230227135255525

                  +

                  它用以标识数据传输的开始,并且携带seq最初随机的序号ISN。

                  +
                  +

                  除了确保收到所有字节的数据外,TCP必须确保也能收到流的开始和结束

                  +

                  这个说得非常好,完美解释了为什么需要占据一个seqno

                  +
                  +
                  ACK

                  ACK也是一个标识位,它代表当前报文是一个确认收到的报文ACK,也即报文中的ackno值有效

                  +
                  ack

                  ack表示当前endpoint【包括客户端和服务器端】希望接收到的下一个数据流的起始字节的seq。

                  +
                  +

                  关于seq和ack,听起来还是有点抽象,不如以连接释放图中ack和seq的值变化为例来说明。

                  +

                  image-20230226202631406

                  +

                  为什么一开始seq=u,ack=v,但下一个就是seq=v,ack=u+1?

                  +

                  这是因为,ack和seq的语义对于客户端和服务器端都是不变的。ack为已经收到的数据的seq+1表示第一个应该接收的值,seq为已经发送的数据的seq+1表示已经发送的值。并且还需要意识到,图中其实有两个数据流(一个是C→S,另一个是S→C),也即有两套seq和ack。

                  +
                    +
                  • ack

                    +

                    服务器从客户端接收信号。ack表示服务器希望接收到的下一个序列号,也即为它从客户端收到的数据的seq+1。

                    +

                    对于此情况,虽然终止报文不携带数据,但其依然占据一个序列号seq。

                    +

                    因而服务器的ack=u+1.

                  • -
                  • tmp_size 记录tmp_segments 中的数据字节数(注意算上SYN和FIN)

                    +
                  • seq

                    +

                    服务器向客户端发送数据。ack表示客户端希望接收到的下一个序号,因而服务器端就应该发送ack这个序号的数据,也即v。

                  • -
                  • tmp_segments 暂存segment,等待收到ack

                    -

                    数据结构:

                    -

                    list,自定义struct,结构体内有

                    -
                      -
                    • TCPSegment
                    • -
                    • seqno 记录该segment的起始数据的seq
                    • -
                    • data_size 记录该segment携带数据的长度
                    +
                  +
                  FIN

                  FIN也是一个标识位,标识着数据传输的结束

                  +
                  接收报文类型

                  因而,从图中可以看出,TCP连接中大概会收到以下几类报文:

                  +
                    +
                  1. 特殊报文

                    +
                      +
                    1. SYN = 1

                      +
                        +
                      1. C的连接请求 携带了ISN

                      2. -
                      3. cons_retran 记录连续的超时重传次数

                        +
                      4. S的连接请求确认,ACK = 1,携带了S的ISN

                      5. -
                      6. syn 标记当前是否为第一个segment

                        +
                    2. -
                    3. fin

                      +
                    4. ACK = 1

                      +

                      额我觉得这是TCPSender管的。这大概是Connection知道了之后通知下TCPSender吧,应该跟我们这次实验没关系

                    5. -
                    6. rto 记录当前的RTO

                      +
                    7. FIN = 1

                    8. -
                    9. timer_start 记录timer是否等待中

                      +
                  2. -
                  3. timer_ticks 记录timer开启时的时间

                    +
                  4. 普通的数据

                  +
                  TCPReceiver的作用

                  我们的TCPReceiver需要负责TCP协议中部分关键对象的管理。我们需要生成ackno以及拥塞窗口大小;我们需要接收SYN和FIN等信号;我们需要对seq进行处理,将其变为StreamReassembler所想要的index。

                  +

                  总结TCPReceiver的作用

                    +
                  1. 处理数据

                    +

                    把Internet过来的一个个TCP报文变成一个个小data,小data再由整流器整流为完整的data,外界再通过socket从ByteStream读取完整的data。

                  2. -
                  3. 实现一个定时函数

                    -

                    第一次从bytestream取出数据包装为segment的时候(也即发送SYN报文)开启它,当所有data都收到ack的时候(也即FIN报文也被成功ACK)关闭它

                    -

                    应该在ticks中被调用

                    -
                    -

                    Every time a segment containing data (nonzero length in sequence space) is sent (whether it’s the first time or a retransmission), if the timer is not running, start it running so that it will expire after RTO milliseconds.

                    -
                    -

                    当timer触发时,我们需要重传tmp_segments 队列头。

                    -

                    如果空间足够,直接重传就行了,然后double RTO,然后用RTO reset timer,然后再次启动timer。

                    -

                    如果空间不足够,只做上面那个的后两步,也即reset timer,然后再次启动timer。

                    -
                  4. -
                  5. ack_received

                    +
                  6. 反馈信息

                    +

                    向发送方反馈自己当前的一些状态信息,如拥塞窗口的大小以及ack等。

                      -
                    1. 更新window_size和ackno

                      -
                    2. -
                    3. 重置超时重传

                      -

                      如果接收到的ackno比以前的大,则重置RTO,重启timer(如果tmp_segments不为空),重置cons_retran

                      -
                    4. -
                    5. 从tmp_segments中删除元素

                      +
                    6. ackno

                      +

                      本质上是“index of the first unassembled byte”

                    7. -
                    8. 调用fill_window

                      +
                    9. window size

                      +

                      本质上是“the distance between the first unassembled index and the first unacceptable index”

                    -
                  7. -
                  8. fill_window

                    -

                    如果window_size - tmp_size <= 0 或者 byte stream空,则什么也不做

                    -

                    否则根据syn和fin标记创建一个new segment,然后写入out stream

                    -
                    -

                    no bigger than the value given by TCPConfig::MAX PAYLOAD SIZE (1452 bytes)

                    -
                    -
                    -

                    If the receiver has announced a window size of zero, the fifill window method should act like the window size is one.

                    -
                    +

                    也即,ackno为拥塞窗口的左端点,ackno+window_size为拥塞窗口的右端点

                  -

                  细节补充

                  实现起来虽然很复杂,但思路确实很简单,正确思路和初见思路差不多,指导书写得很好很详细【以至于一开始我被指导书这么多内容给吓到了】。在这里只记录点实现过程中遇到的一些小错误以及我各个部分的实现细节补充。

                  -

                  timer实现

                  指导书的建议是实现一个类,但是我太懒了()而且确实这个timer的状态也很少,因而我就直接把它写在sender里面了。

                  -

                  SYN报文是否可以带数据

                  此实验未涉及这个。本次全部的测试用例都是SYN报文不携带数据的情况。【因为发出syn报文之后才将window_size设置为非0情况】

                  -

                  如果需要SYN报文不携带数据,可以在fill_window中把这句话:

                  -
                  if (!(_stream.buffer_empty() || remaining == 0)) {
                  +

                  64-bit indexes ←→ 32-bit seqnos

                  从Overview中可以看出来,至关重要的一点就是,将环绕的32bit的seq转化为我们在StreamReassembler中使用的index。

                  +

                  我们不妨再引入一个中间变量abstract seqno。则seqnoabstract seqnostream index三者关系如下图:

                  +

                  image-20230227141242426

                  +

                  显然从seqno转化为abstract seqno更加复杂。因而,我们要做的第一个实验部分就是实现这个转化。

                  +

                  我们需要实现类WrappingInt32。它的wrap函数将64位的abstract seqno转化为32位的seqno,它的unwrap将32位的seqno转化为64位的abstract seqno

                  +

                  感想

                  64-bit indexes ←→ 32-bit seqnos

                  这个实验完美地触及到了我的雷点:对这种环绕来环绕去的东西非常头疼……因而昨天晚上做的时候晕晕乎乎的什么也思考不了,今天过来边画了下图才知道要怎么做。

                  +

                  wrap很简单我就不说了。对于unwrap,我的做法是,先让checkpoint和n-isn都处在同一个区间(红圈)内【也即都让它们对2^32取余】,再通过几个东西之间的关系来确定最终的res是否需要+-HEAD_ONE:

                  +

                  【蓝线表示n-isn,橙线表示红圈区间的中点】

                  +

                  image-20230227133550293

                  +

                  具体的就不多说了。直接看下面的代码,多画画图就能明白了。

                  +

                  TCPReceiver

                  image-20230227231044428

                  +

                  心得

                  我一开始头晕晕地去写,对很多地方产生了疑问,激情地写下了一些消极的话语。刚刚出去吹了会儿风回来,bug全都改对了,于是狂喜着把消极的话语全部删掉了()

                  +

                  怎么说呢,我的错误发生是因为我没有意识到sponge的TCP也许算是一个“简化版”。

                  +

                  在学习本章内容之前,我特地先去回顾了下TCP协议的全过程,并且所有的SYN,FIN等等等概念都是按照网上的概念来的。因而我在面对自己的错误时真的是一脸懵逼……好在,吹完风之后我还是及时醒悟了。

                  +

                  思路还是很简单的,细节也不像Lab1那样那么多那么破防,就是一些奇奇怪怪的恶心小毛病太多了,导致我出错频频,并且都是些很sb的问题,让人直接心态爆炸。

                  +

                  先不吐槽了,接下来就来讲讲总体的思路,以及我产生疑惑的一些地方吧。

                  +

                  思路

                  基本流程

                  得益于Lab1那个复杂算法的健壮性和多功能性,我们对TCPReceiver的实现就可以变得更加简洁。我们不再需要关心报文是否能够被成功接收、报文是否重叠等等等。我们仅需对SYN和FIN这样的报文做特殊的参数处理,将seqno转化为index,然后直接传入我们的StreamReassembler中就行了。

                  +

                  也即,基本流程为:

                  +
                    +
                  1. 如果收到SYN报文,则对一些参数进行初始化,并且标记数据传输开始信号syn为true
                  2. +
                  3. 如果syn为true,则计算index后传入整流器
                  4. +
                  5. 判断是否需要加上FIN报文的比特位
                  6. +
                  +
                  一些细节
                  SYN和FIN各占一个seqno
                  // SYN
                  if(!syn&&header.syn){ // is the first packet
                  // ...
                  isn = header.seqno;
                  seqno = seqno + 1; // plus one to skip the SYN byte
                  // ...
                  }
                  // FIN
                  if(header.fin) fin = true; // 这个一定要写在上面那个if的后面
                  // ...
                  if(_reassembler.empty() && fin){
                  ack += 1;
                  }
                  -

                  修改为这句话:

                  -
                  if (!segment.header().syn&&!(_stream.buffer_empty() || remaining == 0)) {
                  +

                  SYN很直观,没什么好说的。

                  +

                  FIN比较烧。之所以不是这么写:

                  +
                  if(header.fin){
                  ack += 1;
                  }
                  -

                  代码

                  头文件

                  class TCPSender {
                  private:
                  // our initial sequence number, the number for our SYN.
                  WrappingInt32 _isn;

                  // outbound queue of segments that the TCPSender wants sent
                  std::queue<TCPSegment> _segments_out{};

                  // retransmission timer for the connection
                  unsigned int _initial_retransmission_timeout;// 初始的超时重传时间

                  // outgoing stream of bytes that have not yet been sent
                  ByteStream _stream;

                  // the (absolute) sequence number for the next byte to be sent
                  uint64_t _next_seqno{0};

                  struct OutSegment { // outstanding segment的包装类
                  TCPSegment segment;
                  uint64_t seqno;
                  size_t data_size;
                  };
                  std::list<OutSegment> tmp_segments{};// 内部存储结构
                  size_t tmp_size = 0;// 存储结构中含有的segment的总字节数

                  // 注意此处一定要初始化为1
                  size_t window_size = 1;// 拥塞窗口大小
                  uint64_t ackno = 0;// 最大的ackno
                  size_t ticks = 0;// 从出生到当前经过的时间

                  unsigned int cons_retran = 0; // 超时重传连续次数
                  unsigned int rto;// 当前超时重传时间
                  bool timer_start = false;// 超时重传timer是否开启
                  unsigned int timer_ticks = 0;// timer开启时的时间

                  bool syn = false;// 是否发送了SYN报文
                  bool fin = false;// 是否发送了FIN报文
                  public:
                  void send_empty_rst_segment();
                  void send_empty_ack_segment(WrappingInt32 t_ackno);
                  bool fully_acked() const { return _next_seqno == ackno; }
                  +

                  也即一发现FIN报文到了就++,是因为可能会发生这种情况:

                  +

                  image-20230227224055002

                  +

                  也即FIN报文虽然到了,但是中间有一段数据还没到,ack应该等于中间那段数据的开头,你这时候想要跳过FIN而把ack+1那肯定是不对的。

                  +

                  也因而,我们需要记录fin是否有过,并且仅当:

                  +
                  bool StreamReassembler::empty() const { return buffer.empty()&&is_eof; }
                  -

                  具体实现

                  TCPSender::TCPSender(const size_t capacity, const uint16_t retx_timeout, const std::optional<WrappingInt32> fixed_isn)
                  : _isn(fixed_isn.value_or(WrappingInt32{random_device()()}))
                  , _initial_retransmission_timeout{retx_timeout}
                  , _stream(capacity)
                  , rto{retx_timeout} {}

                  uint64_t TCPSender::bytes_in_flight() const { return tmp_size; }

                  // 尽可能地创造segment并且填充到segment output中
                  void TCPSender::fill_window() {
                  // should act like the window size is one
                  size_t t_win_size = window_size == 0 ? 1 : window_size;
                  size_t remaining = t_win_size - tmp_size;
                  // 防止数值溢出的情况
                  if (t_win_size < tmp_size)
                  remaining = 0;

                  // fill as possible
                  while (remaining > 0) {
                  // create and fill in a segment
                  TCPSegment segment = TCPSegment();
                  // 如果处于CLOSED状态
                  if (!syn) {
                  // 转移到SYN_SENT状态
                  // first segment
                  segment.header().syn = true;
                  segment.header().seqno = _isn;
                  remaining -= 1;
                  syn = true;
                  // should start the timer here
                  rto = _initial_retransmission_timeout;
                  timer_start = true;
                  timer_ticks = ticks;
                  }
                  // fill in the payload
                  if (!segment.header().syn && !(_stream.buffer_empty() || remaining == 0)) {
                  string data = _stream.read(min(remaining, TCPConfig::MAX_PAYLOAD_SIZE));
                  remaining -= data.length();
                  Buffer buf = Buffer(move(data));
                  segment.payload() = buf;
                  }

                  // 转移到FIN_SENT状态
                  if (_stream.eof() && !fin && remaining > 0) {
                  // last segment
                  segment.header().fin = true;
                  fin = true;
                  remaining -= 1;
                  }

                  // segment为空(不为SYN、FIN,也不携带任何数据)
                  if (segment.length_in_sequence_space() == 0)
                  break;

                  segment.header().seqno = wrap(_next_seqno, _isn);
                  _next_seqno += segment.length_in_sequence_space();
                  // push into the outstanding segments
                  tmp_segments.push_back(
                  {segment, unwrap(segment.header().seqno, _isn, _next_seqno), segment.length_in_sequence_space()});
                  tmp_size += segment.length_in_sequence_space();
                  // push into the segment out queue
                  _segments_out.push(segment);
                  }
                  }

                  void TCPSender::ack_received(const WrappingInt32 ack, const uint16_t wind_size) {
                  window_size = wind_size;
                  uint64_t a_ack = unwrap(ack, _isn, ackno);
                  if (a_ack > _next_seqno)
                  return; // impossible ack is ignored
                  if (a_ack > ackno) {
                  // reset the retransmission
                  rto = _initial_retransmission_timeout;
                  timer_ticks = ticks;
                  cons_retran = 0;
                  // erase elements from the tmp_segments
                  for (auto it = tmp_segments.begin(); it != tmp_segments.end();) {
                  if (a_ack >= it->seqno + it->data_size) {
                  tmp_size -= (it->segment).length_in_sequence_space();
                  // 如果FIN报文被成功接收,就关闭timer
                  // FIN_ACKED
                  if (it->segment.header().fin)
                  timer_start = false;
                  it = tmp_segments.erase(it);
                  } else
                  it++;
                  }
                  }
                  ackno = a_ack;
                  fill_window();
                  }

                  void TCPSender::tick(const size_t ms_since_last_tick) {
                  if (ticks > ticks + ms_since_last_tick) {
                  // 进行简单的溢出处理,还是有可能溢出
                  ticks -= timer_ticks;
                  timer_ticks = 0;
                  }
                  ticks += ms_since_last_tick;

                  if (timer_start && ticks > timer_ticks && ticks - timer_ticks >= rto) {
                  if (!tmp_segments.empty()) {
                  // resend
                  _segments_out.push(tmp_segments.front().segment);
                  if (window_size != 0) {
                  cons_retran++;
                  rto *= 2;
                  }
                  }
                  timer_ticks = ticks;
                  }
                  }

                  unsigned int TCPSender::consecutive_retransmissions() const { return cons_retran; }

                  /* 在TCPConnection中被使用的辅助方法们 */
                  void TCPSender::send_empty_segment() {
                  TCPSegment segment = TCPSegment();
                  segment.header().seqno = wrap(_next_seqno, _isn);
                  _segments_out.push(segment);
                  }
                  void TCPSender::send_empty_ack_segment(WrappingInt32 t_ackno) {
                  TCPSegment segment = TCPSegment();
                  segment.header().seqno = wrap(_next_seqno, _isn);
                  segment.header().ack = true;
                  segment.header().ackno = t_ackno;
                  _segments_out.push(segment);
                  }
                  void TCPSender::send_empty_rst_segment() {
                  TCPSegment segment = TCPSegment();
                  segment.header().seqno = wrap(_next_seqno, _isn);
                  segment.header().rst = true;
                  _segments_out.push(segment);
                  }
                  ]]> +

                  成立时,才能表示数据传输真正结束,让ack++。

                  +
                  以abstract seqno的形式保存ackno

                  说实话我一开始ackno的数据结构是WrappingInt32。为了这么搞,我还得特地维护一个checkpoint变量用来做unwrap的参数,然后ackno也不能用_reassembler.get_left_bound()来获取,总之就搞得非常非常麻烦。这时候我不小心【是故意的还是不小心的?】看到了感恩的代码,对其用abstract seqno保存ackno这个想法大为赞叹,于是就果断地沿用了()果然设计思想方面我还是有很大不足啊。

                  +

                  疑惑

                  关于特殊报文

                  我一开始被这个图以及百度得到的结果受影响:

                  +

                  image-20230227224429692

                  +

                  image-20230227224449780

                  +

                  认为SYN报文不能携带数据【同理FIN也是】,因而在最初实现的时候看到test case人都麻透了开始怀疑人生……

                  +

                  不过这也怪我没有意识到实验和业界可能是不一样的,但指导书也没说SYN和FIN到底会不会携带数据……emm,我感觉这一点做得不够详细,也许可以改进一下。

                  +
                  关于window size的定义

                  我现在还是搞不懂这东西究竟是什么玩意……

                  +

                  指导书上是这么说的:

                  +
                  +

                  the distance between the “first unassembled” index and the “first unacceptable” index.

                  +

                  This is called the “window size”.

                  +
                  +

                  所谓的“first unassembled”正是ackno。而,我正是理解错了所谓“first unacceptable” 的意思,才导致我想了好久好久都没想出来,最后看了答案被薄纱到现在。

                  +

                  看到这个“first unacceptable” ,我的第一反应就是,维护一个变量right_bound,当packet过来的时候,如果packet的index范围(seqno + data.length())比right_bound大就更新。我认为这才叫做“first unacceptable”。但其实!我会这么想是因为我英语不好……

                  +

                  “first unacceptable” ,unacceptable,意为无法接受的,也就是说,它跟容量有关。第一个无法接受的,就是第一个超出容量的。而结合我们上面的那张图:

                  +

                  image-20230225232723083

                  +

                  可以看出,事实上window size就是黑框部分,也即紫框部分减去绿色部分,也即ByteStreamremaining_capacity()……

                  +

                  而我以为它是还未收到的的意思,故而才理解成了上面那样。

                  +

                  看来英语不好也是原罪23333

                  +

                  代码

                  64-bit indexes ←→ 32-bit seqnos

                  //! Transform an "absolute" 64-bit sequence number (zero-indexed) into a WrappingInt32
                  //! \param n The input absolute 64-bit sequence number
                  //! \param isn The initial sequence number
                  WrappingInt32 wrap(uint64_t n, WrappingInt32 isn) {
                  uint32_t tmp = (n & TAIL_MASK);
                  return isn + tmp;
                  }

                  //! Transform a WrappingInt32 into an "absolute" 64-bit sequence number (zero-indexed)
                  //! \param n The relative sequence number
                  //! \param isn The initial sequence number
                  //! \param checkpoint A recent absolute 64-bit sequence number
                  //! \returns the 64-bit sequence number that wraps to `n` and is closest to `checkpoint`
                  //!
                  //! \note Each of the two streams of the TCP connection has its own ISN. One stream
                  //! runs from the local TCPSender to the remote TCPReceiver and has one ISN,
                  //! and the other stream runs from the remote TCPSender to the local TCPReceiver and
                  //! has a different ISN.
                  uint64_t unwrap(WrappingInt32 n, WrappingInt32 isn, uint64_t checkpoint) {
                  uint32_t tmp_n = n.raw_value() - isn.raw_value();
                  uint64_t res = (checkpoint & HEAD_MASK);
                  uint32_t tmp_cp = (checkpoint & TAIL_MASK);

                  res |= tmp_n;
                  if(tmp_cp < FLAG){
                  if(tmp_n > tmp_cp + FLAG){
                  if(res >= HEAD_ONE) res -= HEAD_ONE;
                  }
                  }else if(tmp_cp > FLAG){
                  if(tmp_n < tmp_cp - FLAG) res += HEAD_ONE;
                  }
                  return res;
                  }
                  + +

                  TCPReceiver

                  头文件

                  class TCPReceiver {
                  StreamReassembler _reassembler;

                  size_t _capacity;
                  uint64_t ack = 0;
                  WrappingInt32 isn = WrappingInt32(0);

                  bool syn = false;
                  bool fin = false;
                  // ...
                  + +

                  具体实现

                  void TCPReceiver::segment_received(const TCPSegment &seg) {
                  TCPHeader header = seg.header();
                  WrappingInt32 seqno = header.seqno;
                  string data = seg.payload().copy();
                  size_t index = 0; // the param of the reassembler

                  // LISTENING -> SYN_SENT
                  if(!syn&&header.syn){ // is the first packet
                  _reassembler.set_is_eof();// reset the eof flag
                  fin = false;// reset the fin flag
                  isn = header.seqno;
                  seqno = seqno + 1; // plus one to skip the SYN byte
                  syn = true;// mark the start of the byte stream
                  }

                  // must keep after the last if branch to avoid the case "flag = SF"
                  // FIN_RECEIVED
                  if(header.fin) fin = true;
                  if(syn){
                  uint64_t abs_seqno = unwrap(seqno,isn,ack);
                  index = abs_seqno - 1;
                  if (abs_seqno != 0)// write into the assembler
                  _reassembler.push_substring(data,index,header.fin);
                  ack = _reassembler.get_left_bound() + 1;

                  if (_reassembler.stream_out().input_ended() && fin)
                  ack += 1;// plus one to skip the FIN byte
                  }
                  }
                  optional<WrappingInt32> TCPReceiver::ackno() const {
                  if(syn) return wrap(ack,isn);
                  else return {};// empty
                  }

                  size_t TCPReceiver::window_size() const {
                  return stream_out().remaining_capacity();
                  }
                  +]]> Lab4 TCPConnection @@ -7951,28 +7841,114 @@ url访问填写http://localhost/webdemo4_war/*.do
                6. ]]> - Lab6 Router - /2023/02/25/cs144$lab6/ - Lab6 Router

                  心得

                  要做什么

                  本次实验要实现的是IP层的路由工作,但是只用实现对路由表进行操作的部分,比如说增加表项以及查询路由表等,其他的什么RIP、OSPF都不用我们实现,所以这样一来其实就简单非常多了()

                  -

                  有一点需要注意的是,它一直在强调一个“最长前缀匹配”。也就是:

                  -

                  image-20230309142032359

                  -

                  image-20230309141949757

                  -

                  还有一点需要注意的是路由的结构:

                  -

                  image-20230308142934287

                  -

                  实际上就是路由表+一堆网络接口,这些端口都是network interface。

                  + Lab3 TCPSender + /2023/02/25/cs144$lab3/ + Lab3 TCPSender

                  前置知识

                  在TCP协议中,TCPSender负责对ack进行处理,将字节流封装为TCP报文,根据拥塞窗口的大小传输数据,以及管理超时重传。

                  +

                  我们的TCPSender需要做的是:

                  +
                    +
                  1. 维护拥塞窗口

                    +

                    image-20230228105405827

                    +

                    我们需要通过ackno和window_size两个参数维护拥塞窗口的大小

                    +
                  2. +
                  3. 填充拥塞窗口

                    +

                    必须as possible。除非拥塞窗口满或者ByteStream空才不填。

                    +

                    对于从ByteStream读出的数据,我们需要把其封装为一个TCPSegment再向_segment_out输出

                    +
                  4. +
                  5. 记录哪一部分ack了,哪一部分没有ack

                    +

                    我们需要在发送segment的同时暂存segment,当且仅当接收到ack,并且ack为segment.seqno+length的时候才能将其释放。

                    +
                  6. +
                  7. 管理超时重传

                    +

                    当对方超过一段时间还没有收到数据时,需要进行超时重传

                    +

                    以segment为单位,一个segment重传具有原子性。

                    +

                    在sender和暂存segment的数据结构中保存时钟滴答

                    +
                  8. +
                  +

                  特别的,指导书上有一段话表述得很有意思:

                  +

                  image-20230228110046088

                  +

                  这体现了TCPReceiverTCPSender之间的对偶关系,这种细节性的设计理念值得学习。

                  +

                  感想

                  写完TCPSender后我还是觉得有些迷茫……就跟TCPReceiver一样。说不出来具体是哪里不清楚,但总感觉隐隐约约有些怪怪的?总感觉相互之间接口有点混乱,对它们之间是怎么交互的一概不知。我想这是由于我们是自底向上实现TCP协议所带来的问题。希望这种感觉在实现完TCPConnection之后可以好转吧。

                  +

                  TCPReceiver的主要任务是把segment拼接成字节流,以及维护即将要告知TCPSender的ackno和拥塞窗口大小。而TCPSender的作用就是把字节流切成segment,并且根据ackno和拥塞窗口大小,进行数据的填充以及超时重传的管理。可以看到,它们是对偶的关系。

                  +

                  初见思路

                  看完指导书以及各种接口定义可以得知,我们需要:

                  +
                    +
                  1. 增加成员变量

                    +
                      +
                    1. window_size 拥塞窗口的大小

                      +
                    2. +
                    3. ackono 记录当前收到的最大ackno

                      +
                    4. +
                    5. ticks 记录sender从出生到现在的时钟滴答

                      +
                    6. +
                    7. tmp_size 记录tmp_segments 中的数据字节数(注意算上SYN和FIN)

                      +
                    8. +
                    9. tmp_segments 暂存segment,等待收到ack

                      +

                      数据结构:

                      +

                      list,自定义struct,结构体内有

                      +
                        +
                      • TCPSegment
                      • +
                      • seqno 记录该segment的起始数据的seq
                      • +
                      • data_size 记录该segment携带数据的长度
                      • +
                      +
                    10. +
                    11. cons_retran 记录连续的超时重传次数

                      +
                    12. +
                    13. syn 标记当前是否为第一个segment

                      +
                    14. +
                    15. fin

                      +
                    16. +
                    17. rto 记录当前的RTO

                      +
                    18. +
                    19. timer_start 记录timer是否等待中

                      +
                    20. +
                    21. timer_ticks 记录timer开启时的时间

                      +
                    22. +
                    +
                  2. +
                  3. 实现一个定时函数

                    +

                    第一次从bytestream取出数据包装为segment的时候(也即发送SYN报文)开启它,当所有data都收到ack的时候(也即FIN报文也被成功ACK)关闭它

                    +

                    应该在ticks中被调用

                    -

                    路由器可分为两部分,一部分控制路由协议,包括完善路由表之类的;另一部分负责数据转发。

                    -

                    负责接收数据的端口既可能收到数据,也可能收到路由信息报文。收到前者,则需要查询转发表然后进行路由转发;收到后者,就需要将其交付给路由选择处理机进行处理。

                    -

                    它有一个地方说得很有意思:路由表需要对网络拓扑最优化,转发表需要使查找过程最优化

                    -

                    也就是说,路由表只是key为目的IP地址,value为下一跳IP地址的一个普通map,可以是unordered_map,因为无需对它进行查找操作;转发表的内容可能跟路由表差不多,但是由于它要被进行频繁的查找工作,因而其数据结构需要对查找的消耗较低。

                    -

                    不过在我们这边,一般不区分路由表和转发表的概念。

                    +

                    Every time a segment containing data (nonzero length in sequence space) is sent (whether it’s the first time or a retransmission), if the timer is not running, start it running so that it will expire after RTO milliseconds.

                    -

                    感想

                    说实话思路很直观很简单,懒得说了,直接看代码吧【开摆】

                    -

                    我唯一卡得比较久的有两个地方,一个是一开始数据结构选用的是set,图它的天然排序,针对prefix_length排序来优化查找,但是没有意识到,对于自定义比较运算符的结构体,set也是会自动去重的()而不同路由项的prefix_length显然可以重复。因而这样是达咩的,最后不得已选用了一个普通的list。

                    -

                    另一个是子网掩码计算问题,刚开始一个小地方想错了。这个没什么好说的,纯纯脑子一抽。

                    -

                    代码

                    头文件

                    // ...
                    class Router {
                    struct route_node {
                    uint32_t route_prefix = 0;
                    uint8_t prefix_length = 0;
                    std::optional<Address> next_hop{};
                    size_t interface_num = 0;
                    // 降序
                    bool operator<(const route_node &b) const { return prefix_length > b.prefix_length; }
                    };

                    std::list<route_node> route_table{};
                    // ...
                    +

                    当timer触发时,我们需要重传tmp_segments 队列头。

                    +

                    如果空间足够,直接重传就行了,然后double RTO,然后用RTO reset timer,然后再次启动timer。

                    +

                    如果空间不足够,只做上面那个的后两步,也即reset timer,然后再次启动timer。

                    +
                  4. +
                  5. ack_received

                    +
                      +
                    1. 更新window_size和ackno

                      +
                    2. +
                    3. 重置超时重传

                      +

                      如果接收到的ackno比以前的大,则重置RTO,重启timer(如果tmp_segments不为空),重置cons_retran

                      +
                    4. +
                    5. 从tmp_segments中删除元素

                      +
                    6. +
                    7. 调用fill_window

                      +
                    8. +
                    +
                  6. +
                  7. fill_window

                    +

                    如果window_size - tmp_size <= 0 或者 byte stream空,则什么也不做

                    +

                    否则根据syn和fin标记创建一个new segment,然后写入out stream

                    +
                    +

                    no bigger than the value given by TCPConfig::MAX PAYLOAD SIZE (1452 bytes)

                    +
                    +
                    +

                    If the receiver has announced a window size of zero, the fifill window method should act like the window size is one.

                    +
                    +
                  8. +
                  +

                  细节补充

                  实现起来虽然很复杂,但思路确实很简单,正确思路和初见思路差不多,指导书写得很好很详细【以至于一开始我被指导书这么多内容给吓到了】。在这里只记录点实现过程中遇到的一些小错误以及我各个部分的实现细节补充。

                  +

                  timer实现

                  指导书的建议是实现一个类,但是我太懒了()而且确实这个timer的状态也很少,因而我就直接把它写在sender里面了。

                  +

                  SYN报文是否可以带数据

                  此实验未涉及这个。本次全部的测试用例都是SYN报文不携带数据的情况。【因为发出syn报文之后才将window_size设置为非0情况】

                  +

                  如果需要SYN报文不携带数据,可以在fill_window中把这句话:

                  +
                  if (!(_stream.buffer_empty() || remaining == 0)) {
                  -

                  具体实现

                  void Router::add_route(const uint32_t route_prefix,
                  const uint8_t prefix_length,
                  const optional<Address> next_hop,
                  const size_t interface_num) {
                  cerr << "DEBUG: adding route " << Address::from_ipv4_numeric(route_prefix).ip() << "/" << int(prefix_length)
                  << " => " << (next_hop.has_value() ? next_hop->ip() : "(direct)") << " on interface " << interface_num << "\n";
                  // 添加
                  route_node node;
                  node.route_prefix = route_prefix;
                  node.prefix_length = prefix_length;
                  node.next_hop = next_hop;
                  node.interface_num = interface_num;
                  route_table.push_back(node);
                  route_table.sort();
                  }

                  //! \param[in] dgram The datagram to be routed
                  void Router::route_one_datagram(InternetDatagram &dgram) {
                  // 减少TTL
                  if (dgram.header().ttl <= 1)
                  return; // drop
                  dgram.header().ttl -= 1;

                  const uint32_t target_ip = dgram.header().dst;
                  for (auto it = route_table.begin(); it != route_table.end(); it++) {
                  uint32_t mask = 0;
                  mask = (((~mask) >> (32-it->prefix_length)) << (32-it->prefix_length));
                  if (it->prefix_length == 0 || ((it->route_prefix & mask) == (target_ip & mask))){
                  // 发送报文
                  if (it->next_hop.has_value())
                  interface(it->interface_num).send_datagram(dgram, it->next_hop.value());
                  else
                  interface(it->interface_num).send_datagram(dgram,
                  Address::from_ipv4_numeric(dgram.header().dst));
                  return; // 一定是最长前缀
                  }
                  }
                  }

                  void Router::route() {
                  // Go through all the interfaces, and route every incoming datagram to its proper outgoing interface.
                  for (auto &interface : _interfaces) {
                  auto &queue = interface.datagrams_out();
                  while (not queue.empty()) {
                  route_one_datagram(queue.front());
                  queue.pop();
                  }
                  }
                  }
                  ]]>
                  +

                  修改为这句话:

                  +
                  if (!segment.header().syn&&!(_stream.buffer_empty() || remaining == 0)) {
                  + +

                  代码

                  头文件

                  class TCPSender {
                  private:
                  // our initial sequence number, the number for our SYN.
                  WrappingInt32 _isn;

                  // outbound queue of segments that the TCPSender wants sent
                  std::queue<TCPSegment> _segments_out{};

                  // retransmission timer for the connection
                  unsigned int _initial_retransmission_timeout;// 初始的超时重传时间

                  // outgoing stream of bytes that have not yet been sent
                  ByteStream _stream;

                  // the (absolute) sequence number for the next byte to be sent
                  uint64_t _next_seqno{0};

                  struct OutSegment { // outstanding segment的包装类
                  TCPSegment segment;
                  uint64_t seqno;
                  size_t data_size;
                  };
                  std::list<OutSegment> tmp_segments{};// 内部存储结构
                  size_t tmp_size = 0;// 存储结构中含有的segment的总字节数

                  // 注意此处一定要初始化为1
                  size_t window_size = 1;// 拥塞窗口大小
                  uint64_t ackno = 0;// 最大的ackno
                  size_t ticks = 0;// 从出生到当前经过的时间

                  unsigned int cons_retran = 0; // 超时重传连续次数
                  unsigned int rto;// 当前超时重传时间
                  bool timer_start = false;// 超时重传timer是否开启
                  unsigned int timer_ticks = 0;// timer开启时的时间

                  bool syn = false;// 是否发送了SYN报文
                  bool fin = false;// 是否发送了FIN报文
                  public:
                  void send_empty_rst_segment();
                  void send_empty_ack_segment(WrappingInt32 t_ackno);
                  bool fully_acked() const { return _next_seqno == ackno; }
                  + +

                  具体实现

                  TCPSender::TCPSender(const size_t capacity, const uint16_t retx_timeout, const std::optional<WrappingInt32> fixed_isn)
                  : _isn(fixed_isn.value_or(WrappingInt32{random_device()()}))
                  , _initial_retransmission_timeout{retx_timeout}
                  , _stream(capacity)
                  , rto{retx_timeout} {}

                  uint64_t TCPSender::bytes_in_flight() const { return tmp_size; }

                  // 尽可能地创造segment并且填充到segment output中
                  void TCPSender::fill_window() {
                  // should act like the window size is one
                  size_t t_win_size = window_size == 0 ? 1 : window_size;
                  size_t remaining = t_win_size - tmp_size;
                  // 防止数值溢出的情况
                  if (t_win_size < tmp_size)
                  remaining = 0;

                  // fill as possible
                  while (remaining > 0) {
                  // create and fill in a segment
                  TCPSegment segment = TCPSegment();
                  // 如果处于CLOSED状态
                  if (!syn) {
                  // 转移到SYN_SENT状态
                  // first segment
                  segment.header().syn = true;
                  segment.header().seqno = _isn;
                  remaining -= 1;
                  syn = true;
                  // should start the timer here
                  rto = _initial_retransmission_timeout;
                  timer_start = true;
                  timer_ticks = ticks;
                  }
                  // fill in the payload
                  if (!segment.header().syn && !(_stream.buffer_empty() || remaining == 0)) {
                  string data = _stream.read(min(remaining, TCPConfig::MAX_PAYLOAD_SIZE));
                  remaining -= data.length();
                  Buffer buf = Buffer(move(data));
                  segment.payload() = buf;
                  }

                  // 转移到FIN_SENT状态
                  if (_stream.eof() && !fin && remaining > 0) {
                  // last segment
                  segment.header().fin = true;
                  fin = true;
                  remaining -= 1;
                  }

                  // segment为空(不为SYN、FIN,也不携带任何数据)
                  if (segment.length_in_sequence_space() == 0)
                  break;

                  segment.header().seqno = wrap(_next_seqno, _isn);
                  _next_seqno += segment.length_in_sequence_space();
                  // push into the outstanding segments
                  tmp_segments.push_back(
                  {segment, unwrap(segment.header().seqno, _isn, _next_seqno), segment.length_in_sequence_space()});
                  tmp_size += segment.length_in_sequence_space();
                  // push into the segment out queue
                  _segments_out.push(segment);
                  }
                  }

                  void TCPSender::ack_received(const WrappingInt32 ack, const uint16_t wind_size) {
                  window_size = wind_size;
                  uint64_t a_ack = unwrap(ack, _isn, ackno);
                  if (a_ack > _next_seqno)
                  return; // impossible ack is ignored
                  if (a_ack > ackno) {
                  // reset the retransmission
                  rto = _initial_retransmission_timeout;
                  timer_ticks = ticks;
                  cons_retran = 0;
                  // erase elements from the tmp_segments
                  for (auto it = tmp_segments.begin(); it != tmp_segments.end();) {
                  if (a_ack >= it->seqno + it->data_size) {
                  tmp_size -= (it->segment).length_in_sequence_space();
                  // 如果FIN报文被成功接收,就关闭timer
                  // FIN_ACKED
                  if (it->segment.header().fin)
                  timer_start = false;
                  it = tmp_segments.erase(it);
                  } else
                  it++;
                  }
                  }
                  ackno = a_ack;
                  fill_window();
                  }

                  void TCPSender::tick(const size_t ms_since_last_tick) {
                  if (ticks > ticks + ms_since_last_tick) {
                  // 进行简单的溢出处理,还是有可能溢出
                  ticks -= timer_ticks;
                  timer_ticks = 0;
                  }
                  ticks += ms_since_last_tick;

                  if (timer_start && ticks > timer_ticks && ticks - timer_ticks >= rto) {
                  if (!tmp_segments.empty()) {
                  // resend
                  _segments_out.push(tmp_segments.front().segment);
                  if (window_size != 0) {
                  cons_retran++;
                  rto *= 2;
                  }
                  }
                  timer_ticks = ticks;
                  }
                  }

                  unsigned int TCPSender::consecutive_retransmissions() const { return cons_retran; }

                  /* 在TCPConnection中被使用的辅助方法们 */
                  void TCPSender::send_empty_segment() {
                  TCPSegment segment = TCPSegment();
                  segment.header().seqno = wrap(_next_seqno, _isn);
                  _segments_out.push(segment);
                  }
                  void TCPSender::send_empty_ack_segment(WrappingInt32 t_ackno) {
                  TCPSegment segment = TCPSegment();
                  segment.header().seqno = wrap(_next_seqno, _isn);
                  segment.header().ack = true;
                  segment.header().ackno = t_ackno;
                  _segments_out.push(segment);
                  }
                  void TCPSender::send_empty_rst_segment() {
                  TCPSegment segment = TCPSegment();
                  segment.header().seqno = wrap(_next_seqno, _isn);
                  segment.header().rst = true;
                  _segments_out.push(segment);
                  }
                  ]]>
                  Lab5 NetworkInterface @@ -8970,79 +8946,28 @@ url访问填写http://localhost/webdemo4_war/*.do。 ]]> - git使用记录 - /2023/10/07/git/ - -

                  记录一些git的原理学习,以及工作学习中遇到的一些git的操作问题。

                  + Lab6 Router + /2023/02/25/cs144$lab6/ + Lab6 Router

                  心得

                  要做什么

                  本次实验要实现的是IP层的路由工作,但是只用实现对路由表进行操作的部分,比如说增加表项以及查询路由表等,其他的什么RIP、OSPF都不用我们实现,所以这样一来其实就简单非常多了()

                  +

                  有一点需要注意的是,它一直在强调一个“最长前缀匹配”。也就是:

                  +

                  image-20230309142032359

                  +

                  image-20230309141949757

                  +

                  还有一点需要注意的是路由的结构:

                  +

                  image-20230308142934287

                  +

                  实际上就是路由表+一堆网络接口,这些端口都是network interface。

                  +
                  +

                  路由器可分为两部分,一部分控制路由协议,包括完善路由表之类的;另一部分负责数据转发。

                  +

                  负责接收数据的端口既可能收到数据,也可能收到路由信息报文。收到前者,则需要查询转发表然后进行路由转发;收到后者,就需要将其交付给路由选择处理机进行处理。

                  +

                  它有一个地方说得很有意思:路由表需要对网络拓扑最优化,转发表需要使查找过程最优化

                  +

                  也就是说,路由表只是key为目的IP地址,value为下一跳IP地址的一个普通map,可以是unordered_map,因为无需对它进行查找操作;转发表的内容可能跟路由表差不多,但是由于它要被进行频繁的查找工作,因而其数据结构需要对查找的消耗较低。

                  +

                  不过在我们这边,一般不区分路由表和转发表的概念。

                  -

                  操作

                  pull request

                    -
                  1. 第一次提pr

                    -
                      -
                    1. fork原仓库
                    2. -
                    3. 本地clone,两个remote,fork和origin
                    4. -
                    5. checkout -b new-branch
                    6. -
                    7. 修改,add,commit,push
                    8. -
                    9. 在github提pr
                    10. -
                    -
                  2. -
                  3. 修改提过的pr

                    -
                      -
                    1. 本地仓库与远程同步

                      -

                      直接修改,然后push到fork的对应分支就行,会自动更新。

                      -
                    2. -
                    3. 本地仓库与远程不同步

                      -

                      以下操作都在new-branch分支上

                      -
                        -
                      1. git fetch origin
                      2. -
                      3. git rebase origin/master
                      4. -
                      5. 如果有冲突则解决,然后git rebase --continue继续rebase
                      6. -
                      7. push fork
                      8. -
                      -
                    4. -
                    -
                  4. -
                  -

                  rebase

                  修改 git 的历史 commit,你能想到几种方案? 详细介绍了rebase基本用法

                  -

                  object损坏

                  image-20231030100954710

                  -

                  如图,我也不知道为什么突然就寄了。。。

                  -

                  总之进行了这些操作,虽然不知道是哪个起作用了,但总算好了:

                  -

                  https://blog.csdn.net/xiaoqixiaoguai/article/details/128591332

                  -

                  首先删除空白对象

                  -
                  cd .git
                  find . -type f -empty -delete -print
                  - -

                  然后更新ref到某个版本号

                  -
                  cd ..
                  tail -n 2 .git/logs/refs/heads/master
                  git show xxxx(版本号)
                  git update-ref HEAD xxxx(版本号)
                  git fsck
                  - -

                  如果还不能用,继续:

                  -
                  rm .git/index
                  git reset
                  git fsck
                  - -

                  我到这里之后显示:

                  -

                  image-20231030101123181

                  -

                  继续执行:

                  -
                    -
                  1. 修复 refs/remotes/origin/master:

                    -
                    bashCopy codegit update-ref -d refs/remotes/origin/master
                    git fetch origin
                    - -

                    这将删除损坏的 origin/master 引用,然后从远程仓库重新获取。

                    -
                  2. -
                  3. 修复 dangling blob:

                    -

                    如果 git fsck 显示了 dangling blob,你可以尝试删除这些对象:

                    -
                    bashCopy codegit reflog expire --expire=now --all
                    git gc --prune=now
                    +

                    感想

                    说实话思路很直观很简单,懒得说了,直接看代码吧【开摆】

                    +

                    我唯一卡得比较久的有两个地方,一个是一开始数据结构选用的是set,图它的天然排序,针对prefix_length排序来优化查找,但是没有意识到,对于自定义比较运算符的结构体,set也是会自动去重的()而不同路由项的prefix_length显然可以重复。因而这样是达咩的,最后不得已选用了一个普通的list。

                    +

                    另一个是子网掩码计算问题,刚开始一个小地方想错了。这个没什么好说的,纯纯脑子一抽。

                    +

                    代码

                    头文件

                    // ...
                    class Router {
                    struct route_node {
                    uint32_t route_prefix = 0;
                    uint8_t prefix_length = 0;
                    std::optional<Address> next_hop{};
                    size_t interface_num = 0;
                    // 降序
                    bool operator<(const route_node &b) const { return prefix_length > b.prefix_length; }
                    };

                    std::list<route_node> route_table{};
                    // ...
                    -

                    这将清理无用的 dangling 对象。

                    -
                  4. -
                  -

                  成功。

                  -

                  原理

                    -
                  1. merge与rebase的差异

                    -

                    merge:

                    -

                    merge

                    -

                    rebase:

                    -

                    rebase

                    -
                  2. -
                  3. -
                  -]]>
                  +

                  具体实现

                  void Router::add_route(const uint32_t route_prefix,
                  const uint8_t prefix_length,
                  const optional<Address> next_hop,
                  const size_t interface_num) {
                  cerr << "DEBUG: adding route " << Address::from_ipv4_numeric(route_prefix).ip() << "/" << int(prefix_length)
                  << " => " << (next_hop.has_value() ? next_hop->ip() : "(direct)") << " on interface " << interface_num << "\n";
                  // 添加
                  route_node node;
                  node.route_prefix = route_prefix;
                  node.prefix_length = prefix_length;
                  node.next_hop = next_hop;
                  node.interface_num = interface_num;
                  route_table.push_back(node);
                  route_table.sort();
                  }

                  //! \param[in] dgram The datagram to be routed
                  void Router::route_one_datagram(InternetDatagram &dgram) {
                  // 减少TTL
                  if (dgram.header().ttl <= 1)
                  return; // drop
                  dgram.header().ttl -= 1;

                  const uint32_t target_ip = dgram.header().dst;
                  for (auto it = route_table.begin(); it != route_table.end(); it++) {
                  uint32_t mask = 0;
                  mask = (((~mask) >> (32-it->prefix_length)) << (32-it->prefix_length));
                  if (it->prefix_length == 0 || ((it->route_prefix & mask) == (target_ip & mask))){
                  // 发送报文
                  if (it->next_hop.has_value())
                  interface(it->interface_num).send_datagram(dgram, it->next_hop.value());
                  else
                  interface(it->interface_num).send_datagram(dgram,
                  Address::from_ipv4_numeric(dgram.header().dst));
                  return; // 一定是最长前缀
                  }
                  }
                  }

                  void Router::route() {
                  // Go through all the interfaces, and route every incoming datagram to its proper outgoing interface.
                  for (auto &interface : _interfaces) {
                  auto &queue = interface.datagrams_out();
                  while (not queue.empty()) {
                  route_one_datagram(queue.front());
                  queue.pop();
                  }
                  }
                  }
                  ]]>
                  驱动开发小记 @@ -9407,64 +9332,79 @@ url访问填写http://localhost/webdemo4_war/*.do。 ]]> - 开源的第一个月 - /2023/10/19/open-source-9.19-10.19/ + git使用记录 + /2023/10/07/git/ -

                  其实是9.10就开始了,但9.19才办理入职所以少算了几天xxxx不然这不显得我菜hh

                  - -

                  引言

                  也许有许多programer与我一样,在初次接触到“开源”这个概念时,便对其产生了无限的向往。千万人通过共同的事业联结在一起,为了行业的进步不求回报地压缩自己的时间,贡献出自己的一份力,这是何等的浪漫。再加上听了无数遍的Linux发展历程故事,对“开源”我是愈发地憧憬。

                  -

                  然而,由于学习cs的时间尚不足,也不知道该从何入手去参与社区贡献,尽管心中怀有对参与这份事业的渴望,我还是会“望而却步”。转折点在今年参加竞赛时,在使用某个开源项目时因为遇到了一点问题而去提了个issue。虽然这个问题本质很傻,但在与开发者你来我往的交流中,我深切地感受到自己仿佛离憧憬更近了一步。因而,在今年竞赛结束后的九月,我毫不犹豫地向PLCT Lab提交了简历,试图追逐那个长存我心的幻影。

                  -

                  而现在,距离第一次提issue已有半年,距离考核通过也已有一个半月之久,我想也是时候好好总结一下我这一个月以来的心路历程了。

                  -

                  我的第一个月

                  -

                  从9.10过审核开始写了很久的日报,当然除了实习还包括别的内容:

                  -

                  image-20231019155950614

                  +

                  记录一些git的原理学习,以及工作学习中遇到的一些git的操作问题。

                  -

                  这一个月以来,我学到了很多东西,学习周期大致可分为三个阶段:初步了解rtt和配置环境、设备树学习以及最后的gpio driver开发。

                  -

                  rtt

                  由于时间有限,所以仅对rtt的标准版本做了一个比较基本的了解(也就是说没有太涉及到源码部分,只能说是对文档中心的那些对外开发用接口有一定的了解)。rtt是一个微内核的RTOS,这与以前所接触的Linux和xv6都不同。由于RTOS的特性,它的许多设计都十分精简,相比于Linux可谓”麻雀虽小五脏俱全“。

                  -

                  对rtt的基本介绍,详情可见其文档中心。我印象最深(也是开发过程中接触得最多的)的几个点有:

                  +

                  操作

                  pull request

                    +
                  1. 第一次提pr

                      -
                    1. 接口设计

                      -

                      与Linux一样,rtt也采用了精简的接口设计。

                      +
                    2. fork原仓库
                    3. +
                    4. 本地clone,两个remote,fork和origin
                    5. +
                    6. checkout -b new-branch
                    7. +
                    8. 修改,add,commit,push
                    9. +
                    10. 在github提pr
                    11. +
                  2. -
                  3. 自动初始化机制

                    -

                    帅的一匹,具体详见

                    +
                  4. 修改提过的pr

                    +
                      +
                    1. 本地仓库与远程同步

                      +

                      直接修改,然后push到fork的对应分支就行,会自动更新。

                    2. +
                    3. 本地仓库与远程不同步

                      +

                      以下操作都在new-branch分支上

                      +
                        +
                      1. git fetch origin
                      2. +
                      3. git rebase origin/master
                      4. +
                      5. 如果有冲突则解决,然后git rebase --continue继续rebase
                      6. +
                      7. push fork
                      8. +
                      +
                    4. +
                    +
                  5. +
                  +

                  rebase

                  修改 git 的历史 commit,你能想到几种方案? 详细介绍了rebase基本用法

                  +

                  object损坏

                  image-20231030100954710

                  +

                  如图,我也不知道为什么突然就寄了。。。

                  +

                  总之进行了这些操作,虽然不知道是哪个起作用了,但总算好了:

                  +

                  https://blog.csdn.net/xiaoqixiaoguai/article/details/128591332

                  +

                  首先删除空白对象

                  +
                  cd .git
                  find . -type f -empty -delete -print
                  + +

                  然后更新ref到某个版本号

                  +
                  cd ..
                  tail -n 2 .git/logs/refs/heads/master
                  git show xxxx(版本号)
                  git update-ref HEAD xxxx(版本号)
                  git fsck
                  + +

                  如果还不能用,继续:

                  +
                  rm .git/index
                  git reset
                  git fsck
                  + +

                  我到这里之后显示:

                  +

                  image-20231030101123181

                  +

                  继续执行:

                  +
                    +
                  1. 修复 refs/remotes/origin/master:

                    +
                    bashCopy codegit update-ref -d refs/remotes/origin/master
                    git fetch origin
                    + +

                    这将删除损坏的 origin/master 引用,然后从远程仓库重新获取。

                    +
                  2. +
                  3. 修复 dangling blob:

                    +

                    如果 git fsck 显示了 dangling blob,你可以尝试删除这些对象:

                    +
                    bashCopy codegit reflog expire --expire=now --all
                    git gc --prune=now
                    + +

                    这将清理无用的 dangling 对象。

                    +
                  4. +
                  +

                  成功。

                  +

                  原理

                    +
                  1. merge与rebase的差异

                    +

                    merge:

                    +

                    merge

                    +

                    rebase:

                    +

                    rebase

                    +
                  2. +
                  -

                  其余的只能说不甚了解,还有待挖掘。

                  -

                  配置环境

                  由于确实对这种东西毫无所知,所以配环境这个过程也是比较漫长,而且很折磨很痛苦(。

                  -

                  硬件方面,一开始拿到IO-board连这是啥都不知道,还以为这就是开发板研究了半天怎么上电和把内核烧进去(((。然后东西也是买得缺斤少两,比如拿到开发板不知道还要有TF卡,上网搜图研究了半天才意识到;再比如也没有USB-TTL,又是一通淘宝购物。这些各种各样的小白问题导致配环境的周期十分漫长。然后还有一些很傻的错误,再次也不好意思多说了,详情可见rtt硬件环境搭建

                  -

                  软件方面倒是没什么问题,之前也早就跟编译内核用的menuconfig打过很多交道,磁盘分区之类的东西之前也简单使过几次,只不过经过这次后也算是使得更加熟练了。

                  -

                  设备树学习

                  -

                  TODO,这部分还是比较多好写的,虽然还是有点模糊(待我之后有时间整理下放个链接

                  -
                  -

                  设备树还是比较复杂,而且因为本人的不审慎,导致对其理解出了偏差,还麻烦了社区看我的代码(悲)只能说个人出道的开源社区还是需要对自己的所有笔代码负责。

                  -

                  gpio driver

                  -

                  pr:

                  -

                  image-20231021105306299

                  -

                  涉及到的各种硬件手册:

                  -

                  image-20231019165451893

                  -
                  -

                  这也是我最后这一周在做的工作,虽说只有短短一周,但是每天都研究这个花了我不少时间和精力()目前算是写完了它的所有功能(大概),并且已经能把LED闪烁和中断绑定函数润起来了,提的pr在这里

                  -

                  以前对驱动的理解,还停留在手把手教你做事的xv6的netlab。也因而,这次可以算是以完全一无所知的状态接下了这个任务。

                  -

                  不过,好在有以前那个短暂的lab经验,我还是稳扎稳打地定下了具体的学习步骤:调研(包括获取各种data book、schematic、rtt官方文档、Linux和rtt相关代码),然后就是学习。

                  -

                  好在有设备树的研究积淀,我也算是比较快地掌握了milkv上gpio的分布、型号及其地址空间,从而顺藤摸瓜找到了对应gpio型号的Linux驱动代码参考和硬件手册,算是免去了不少麻烦。

                  -

                  image-20231019165206369

                  -

                  然后,我观察rtt的gpio驱动们,也找到了对应的pin.md文档,了解了下大致的代码框架思路:

                  -

                  image-20231019165313914

                  -

                  于是接下来的工作也可以比较独立地划分为两部分,一个是数据读写的实现(通过LED闪烁程序测试),另一个是rtt特有的中断回调函数的支持(通过中断程序测试),可以专注对这两个方面开发了。

                  -

                  其中,数据读写的实现需要对寄存器和引脚号等有所了解。寄存器相对比较简单,只需阅读dwapb的data book即可;而引脚号到gpio的转换则花了我不少时间,做了许多猜想并且进行验证,最后误打误撞地“猜”中了正确思路,通过了LED测试。

                  -

                  image-20231019165706806

                  -

                  image-20231019165719966

                  -

                  image-20231019165738144

                  -

                  不过引脚号我现在还是不大懂,总之先参照别的bsp写法自己编了个,等着代码review看下吧。

                  -

                  前期调研一直到LED亮起来花了我整整五六天()相比于此的困难,中断倒显得简单了许多,毕竟它属于是偏软件相关的。调了一天,也从Linux和其他bsp那边抄了些代码,最终在凌晨两点半成功完成了功能测试()今天又花了一个下午整理了下代码和写日记,最终总算是把这个作业交上去了。

                  -

                  整个过程光是写看起来还是比较轻松,但是由于初次开发摸索,每个小跨越都得花费我不少时间去调研搜索,经常是在长达几个小时的不知所措后才短暂地获得了一些光明,我甚至多次想过要不要去辞职了(((。总之,最后还是坚持了下来。看到蓝色的LED在夜晚的T5中闪烁,我还是十分激动的,眼泪都爆出来了()

                  -

                  哎,坚持难能可贵,很高兴我最终做到了,虽然尚有测试上的不完善,以及还在等待review。

                  -

                  稍作总结

                  总之,这一个月来我学到了许多,同时也对我以前未曾涉足的空白领域做了许多探索,包括对设备树、对嵌入式开发、对Linux设备驱动等的学习,总体来说还是十分甚至九分地开心。希望下个月能再接再厉。

                  ]]>
                  - - intern -
                  rtt硬件环境搭建 @@ -9504,6 +9444,66 @@ url访问填写http://localhost/webdemo4_war/*.do
                ]]> + + 开源的第一个月 + /2023/10/19/open-source-9.19-10.19/ + +

                其实是9.10就开始了,但9.19才办理入职所以少算了几天xxxx不然这不显得我菜hh

                + +

                引言

                也许有许多programer与我一样,在初次接触到“开源”这个概念时,便对其产生了无限的向往。千万人通过共同的事业联结在一起,为了行业的进步不求回报地压缩自己的时间,贡献出自己的一份力,这是何等的浪漫。再加上听了无数遍的Linux发展历程故事,对“开源”我是愈发地憧憬。

                +

                然而,由于学习cs的时间尚不足,也不知道该从何入手去参与社区贡献,尽管心中怀有对参与这份事业的渴望,我还是会“望而却步”。转折点在今年参加竞赛时,在使用某个开源项目时因为遇到了一点问题而去提了个issue。虽然这个问题本质很傻,但在与开发者你来我往的交流中,我深切地感受到自己仿佛离憧憬更近了一步。因而,在今年竞赛结束后的九月,我毫不犹豫地向PLCT Lab提交了简历,试图追逐那个长存我心的幻影。

                +

                而现在,距离第一次提issue已有半年,距离考核通过也已有一个半月之久,我想也是时候好好总结一下我这一个月以来的心路历程了。

                +

                我的第一个月

                +

                从9.10过审核开始写了很久的日报,当然除了实习还包括别的内容:

                +

                image-20231019155950614

                +
                +

                这一个月以来,我学到了很多东西,学习周期大致可分为三个阶段:初步了解rtt和配置环境、设备树学习以及最后的gpio driver开发。

                +

                rtt

                由于时间有限,所以仅对rtt的标准版本做了一个比较基本的了解(也就是说没有太涉及到源码部分,只能说是对文档中心的那些对外开发用接口有一定的了解)。rtt是一个微内核的RTOS,这与以前所接触的Linux和xv6都不同。由于RTOS的特性,它的许多设计都十分精简,相比于Linux可谓”麻雀虽小五脏俱全“。

                +

                对rtt的基本介绍,详情可见其文档中心。我印象最深(也是开发过程中接触得最多的)的几个点有:

                +
                  +
                1. 接口设计

                  +

                  与Linux一样,rtt也采用了精简的接口设计。

                  +
                2. +
                3. 自动初始化机制

                  +

                  帅的一匹,具体详见

                  +
                4. +
                +

                其余的只能说不甚了解,还有待挖掘。

                +

                配置环境

                由于确实对这种东西毫无所知,所以配环境这个过程也是比较漫长,而且很折磨很痛苦(。

                +

                硬件方面,一开始拿到IO-board连这是啥都不知道,还以为这就是开发板研究了半天怎么上电和把内核烧进去(((。然后东西也是买得缺斤少两,比如拿到开发板不知道还要有TF卡,上网搜图研究了半天才意识到;再比如也没有USB-TTL,又是一通淘宝购物。这些各种各样的小白问题导致配环境的周期十分漫长。然后还有一些很傻的错误,再次也不好意思多说了,详情可见rtt硬件环境搭建

                +

                软件方面倒是没什么问题,之前也早就跟编译内核用的menuconfig打过很多交道,磁盘分区之类的东西之前也简单使过几次,只不过经过这次后也算是使得更加熟练了。

                +

                设备树学习

                +

                TODO,这部分还是比较多好写的,虽然还是有点模糊(待我之后有时间整理下放个链接

                +
                +

                设备树还是比较复杂,而且因为本人的不审慎,导致对其理解出了偏差,还麻烦了社区看我的代码(悲)只能说个人出道的开源社区还是需要对自己的所有笔代码负责。

                +

                gpio driver

                +

                pr:

                +

                image-20231021105306299

                +

                涉及到的各种硬件手册:

                +

                image-20231019165451893

                +
                +

                这也是我最后这一周在做的工作,虽说只有短短一周,但是每天都研究这个花了我不少时间和精力()目前算是写完了它的所有功能(大概),并且已经能把LED闪烁和中断绑定函数润起来了,提的pr在这里

                +

                以前对驱动的理解,还停留在手把手教你做事的xv6的netlab。也因而,这次可以算是以完全一无所知的状态接下了这个任务。

                +

                不过,好在有以前那个短暂的lab经验,我还是稳扎稳打地定下了具体的学习步骤:调研(包括获取各种data book、schematic、rtt官方文档、Linux和rtt相关代码),然后就是学习。

                +

                好在有设备树的研究积淀,我也算是比较快地掌握了milkv上gpio的分布、型号及其地址空间,从而顺藤摸瓜找到了对应gpio型号的Linux驱动代码参考和硬件手册,算是免去了不少麻烦。

                +

                image-20231019165206369

                +

                然后,我观察rtt的gpio驱动们,也找到了对应的pin.md文档,了解了下大致的代码框架思路:

                +

                image-20231019165313914

                +

                于是接下来的工作也可以比较独立地划分为两部分,一个是数据读写的实现(通过LED闪烁程序测试),另一个是rtt特有的中断回调函数的支持(通过中断程序测试),可以专注对这两个方面开发了。

                +

                其中,数据读写的实现需要对寄存器和引脚号等有所了解。寄存器相对比较简单,只需阅读dwapb的data book即可;而引脚号到gpio的转换则花了我不少时间,做了许多猜想并且进行验证,最后误打误撞地“猜”中了正确思路,通过了LED测试。

                +

                image-20231019165706806

                +

                image-20231019165719966

                +

                image-20231019165738144

                +

                不过引脚号我现在还是不大懂,总之先参照别的bsp写法自己编了个,等着代码review看下吧。

                +

                前期调研一直到LED亮起来花了我整整五六天()相比于此的困难,中断倒显得简单了许多,毕竟它属于是偏软件相关的。调了一天,也从Linux和其他bsp那边抄了些代码,最终在凌晨两点半成功完成了功能测试()今天又花了一个下午整理了下代码和写日记,最终总算是把这个作业交上去了。

                +

                整个过程光是写看起来还是比较轻松,但是由于初次开发摸索,每个小跨越都得花费我不少时间去调研搜索,经常是在长达几个小时的不知所措后才短暂地获得了一些光明,我甚至多次想过要不要去辞职了(((。总之,最后还是坚持了下来。看到蓝色的LED在夜晚的T5中闪烁,我还是十分激动的,眼泪都爆出来了()

                +

                哎,坚持难能可贵,很高兴我最终做到了,虽然尚有测试上的不完善,以及还在等待review。

                +

                稍作总结

                总之,这一个月来我学到了许多,同时也对我以前未曾涉足的空白领域做了许多探索,包括对设备树、对嵌入式开发、对Linux设备驱动等的学习,总体来说还是十分甚至九分地开心。希望下个月能再接再厉。

                +]]>
                + + intern + +
                Operating system interface /2023/01/10/xv6$chap1/ @@ -10159,858 +10159,858 @@ url访问填写http://localhost/webdemo4_war/*.do
              2. ]]> - Page tables - /2023/01/10/xv6$chap3/ - Page tables

                Paging hardware

                为什么需要页表

                将主存储器以及各种外设接口卡里面内置的存储器连接起来,就形成了内存地址空间。内存地址空间中的地址是真实的物理地址。RISC-V架构的指令使用的地址是虚拟地址。为了通过指令中的虚拟地址访问到真实的物理内存,需要进行从虚拟地址到物理地址的转换。从虚拟地址到物理地址的转换,就需要通过页表来实现。

                -

                页表如何运作

                在RISC-V指令集中,当我们需要开启页表服务时,我们需要将我们预先配置好的页表首地址放入 satp 寄存器中。从此之后, 计算机硬件 将把访存的地址 均视为虚拟地址 ,都需要通过硬件查询页表,将其 翻译成为物理地址 ,然后将其作为地址发送给内存进行访存。

                -

                xv6采用的指令集标准为RISC-V标准,其中页表的标准为SV39标准,也就是虚拟地址最多为39位。

                -

                虚实地址翻译流程:

                + Traps and system calls + /2023/01/10/xv6$chap4/ + Traps and system calls

                traps=系统调用+异常+中断。本章着重讲traps概述以及traps中的系统调用。

                +

                对trap的处理包含四个部分:硬件处理、中断向量、trap handler、对应的处理函数

                +

                RISC-V trap machinery

                +

                image-20230618162437636

                +

                image-20230618162500928

                +

                在RISC-V中,异常通常是由于程序执行过程中的错误或非预期事件而引起的,包括故障(faults)、陷阱(traps)和中止(aborts)。中断(interrupts)则是由外部事件触发的,例如定时器到期、外部设备请求等。中断是异步事件,与当前正在执行的指令无关,因此会在任何时候发生。

                +

                xv6是基于RISC-V架构的。因此,发生异常的时候,就会跳转到统一的kernel trap,然后再在里面通过读取scause来进行相应处理。

                +

                发生中断的处理方式就和x86差不多了,都是通过中断向量实现的。

                +
                +

                control register

                risc-v为trap提供了一组寄存器:

                  -
                1. 获得一个虚拟地址。根页表基地址已经被装填至寄存器 satp 中。
                2. -
                3. 通过 satp 找到根页表的物理页帧号,转成物理地址(Offset为0),通过虚拟地址的L2索引,找到对应的页表项。
                4. -
                5. 通过页表项可以找到找到 次页表 的物理页帧号,转成物理地址(Offset为0),通过虚拟地址的L1索引,找到对应的页表项。
                6. -
                7. 通过页表项可以找到找到 叶子页表 的物理页帧号,转成物理地址(Offset为0),通过虚拟地址的L0索引,找到对应的页表项。
                8. -
                9. 通过页表项可以找到找到 物理地址 的物理页帧号,通过虚拟地址的Offset,转成物理地址(Offset和虚拟地址Offset相同)。
                10. +
                11. stvec

                  +

                  trap handler的入口地址

                  +
                12. +
                13. sepc

                  +

                  原程序PC

                  +
                14. +
                15. scause

                  +

                  中断号

                  +
                16. +
                17. sscratch

                  +

                  TRAPFRAME地址

                  +
                18. +
                19. sstatus

                  +

                  是否允许中断,以及中断来自内核态还是用户态

                  +
                -

                页表组成

                页表项

                页表由页表项PTE(Page Table Entries)构成,每个页表项由44位的PPN(Physical Page Number)和一些参数flag组成。

                -

                image-20230109153937459

                -

                Each PTE contains flflag bits that tell the paging hardware how the associated virtual address is allowed to be used. PTE_V indicates whether the PTE is present: if it is not set, a reference to the page causes an exception (i.e. is not allowed). PTE_R controls whether instructions are allowed to read to the page. PTE_W controls whether instructions are allowed to write to the page. PTE_X controls whether the CPU may interpret the content of the page as instructions and execute them. PTE_U controls whether instructions in user mode are allowed to access the page; if PTE_U is not set, the PTE can be used only in supervisor mode.

                -

                这个表项的几个参数定义在kernel/riscv.h中的341行左右。

                +

                The above registers relate to traps handled in supervisor mode, and they cannot be read or written in user mode.

                +

                There is an equivalent set of control registers for traps handled in machine mode; xv6 uses them only for the special case of timer interrupts.

                -

                虚拟地址有64bit,其中25bits未使用,39bits包含了27位的PTE索引号以及12位的offset。

                -

                物理地址有56位,由PPN和offset拼接组成。

                -

                单页表和多级页表

                以单页表为例,物理地址形成过程如下图所示。

                -

                image

                -

                每个页表项PTE索引着一页。因而,每一页的大小为2^12=4096B。单页表中PTE的索引号有2^27个,因而单页表中表项有134217728个,即可以代表134217728页。页表实际上也是以页的形式存储的。因而单页表需要的存储空间为(2^27x7)/2^12=2^15x7=229376页。

                -

                RISC-V架构中真实情况是会有三级页表。三级页表结构相比于单级页表结构,会占据更多的物理存储空间

                -

                image-20230109151346780

                -

                每个页表项PTE索引着一页,这一页可能代表着另一个页表,也可能代表着内存中需要的指令和数据。因而,每一页的大小为2^12=4096B。三页表中,一级页表中PTE的索引号有512个,可以代表的物理内存页数有512x515x512=2^27页,即可以代表134217728页。页表实际上也是以页的形式存储的,一个页表有2^9x7个字节,可以存储在1页中。因而三页表需要的存储空间为1+2^9+2^18 = 262657页。

                -

                三级页表结构相比于单级页表结构,可以节省更多内存空间

                +

                每个CPU都有自己的一套这样的控制寄存器。

                +

                硬件处理步骤

                时钟中断、device interrupt以及关中断的情况下,不会做以下步骤。

                -

                参考:页表是啥以及为啥多级页表能够节省空间

                +

                \1. If the trap is a device interrupt, and the sstatus SIE bit is clear, don’t do any of the following.

                +

                \2. Disable interrupts by clearing SIE.关中断

                +

                \3. Copy the pc to sepc.保存PC

                +

                \4. Save the current mode (user or supervisor) in the SPP bit in sstatus.保存mode

                +

                \5. Set scause to reflflect the trap’s cause.保存中断号

                +

                \6. Set the mode to supervisor.切换到内核态

                +

                \7. Copy stvec to the pc.将trap handler写入pc,开始执行trap handler【uservec or kernelvec?】

                -

                考虑到这样一个进程:

                -

                watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2Z1eXVhbmRl,size_16,color_FFFFFF,t_70

                -

                进程使用页表时,需要将整个页表读入内存。

                -

                如果使用单级页表,尽管一个进程仅使用到页表中的某两项,也需要把整个页表都读入内存,光是页表就占据了2^15x7x4k/2^20 约为1G的内存空间。

                -

                如果使用三级页表,一个进程需要用到某两页。假设这两页存储在不同的二级页表中,则只需要读入1+2+2=5页 约为20K的内存空间。

                -

                两者相对比,显然用三级页表比单级页表顶多了。三级页表相较于一级页表,多用了13%的物理空间,却可以节省99.998%的空间。

                -

                页表使用

                每个进程会保留自己的一份用户级别的页表地址。当轮到自己使用CPU时,会将CPU的satp寄存器更换为自己的页表地址。

                -

                Kernel address space

                介绍了xv6中内核的页表结构。

                +

                切换到内核页表、切换内核栈、保存寄存器现场这些工作交给操作系统完成。

                +

                Traps from user space

                从用户态来的trap会经历怎么样的过程?

                +

                前面说到,下面需要进行页表的切换,页表的切换必然是接下来要做的指令的某个环节。那么为了让页表切换之后,CPU还知道要从哪里取指执行,就要让某段物理内存在内核空间和用户空间的虚拟地址一样。这样,不论页表是用户的还是内核的,都可以通过同样的虚拟地址访问到该段存放指令的物理内存从而继续执行。

                +

                这段虚拟地址就是trampoline。它在内核页表和用户页表都位于MAXVA的位置。

                -

                这里为了方便,就把三级页表省略了,只留下va和pa的对比

                +

                我感觉这段大概可以这么理解:

                +

                通过查看代码,可知trampoline段实际上存储的是trampoline.S中的数据,也即uservec和userret的汇编代码,也即执行切换页表我们实际上就是在执行trampoline里的代码。trampoline的存在,就可以使得每个页表的这部分都是这两个的代码,这样一来切换页表也就不影响指令流的执行。

                -

                每个进程都有一个用户级别的页表。xv6给内核提供了一个单独的内核地址空间的页表。其层级映射关系如下:

                p3

                -

                在kernel/memlayout.h中正记录了这些参数:

                -
                // Physical memory layout

                // qemu -machine virt is set up like this,
                // based on qemu's hw/riscv/virt.c:
                //
                // 00001000 -- boot ROM, provided by qemu
                // 02000000 -- CLINT
                // 0C000000 -- PLIC
                // 10000000 -- uart0
                // 10001000 -- virtio disk
                // 80000000 -- boot ROM jumps here in machine mode
                // -kernel loads the kernel here
                // unused RAM after 80000000.

                // the kernel uses physical memory thus:
                // 80000000 -- entry.S, then kernel text and data
                // end -- start of kernel page allocation area
                // PHYSTOP -- end RAM used by the kernel

                // qemu puts UART registers here in physical memory.
                #define UART0 0x10000000L
                #define UART0_IRQ 10

                // virtio mmio interface
                #define VIRTIO0 0x10001000
                #define VIRTIO0_IRQ 1

                // core local interruptor (CLINT), which contains the timer.
                // ...
                +

                stevc存储的正是trampoline段中的uservec。

                +

                uservec

                sscratch里面存的是trapframe的值。

                +

                trapframe存在于用户空间中,并且每个进程的trapframe所处位置固定是在trampoline下方。

                +

                image-20230111203357767

                +

                首先将寄存器的值都存入trapframe中;然后,再从trapframe中读取内核栈指针、当前CPUid,下一步要跳转的usertrap的地址,以及内核页表。最后,uservec切换到内核页表,并且jmp到usertrap。

                +
                #in kernel/trampoline.S
                .section trampsec
                .globl trampoline
                trampoline:
                .align 4
                .globl uservec
                uservec:
                #
                # trap.c sets stvec to point here, so
                # traps from user space start here,
                # in supervisor mode, but with a
                # user page table.
                #
                # sscratch points to where the process's p->trapframe is
                # mapped into user space, at TRAPFRAME.
                #

                # swap a0 and sscratch
                # so that a0 is TRAPFRAME
                csrrw a0, sscratch, a0

                # save the user registers in TRAPFRAME
                sd ra, 40(a0)
                sd sp, 48(a0)
                # ...
                sd t6, 280(a0)

                # save the user a0 in p->trapframe->a0
                csrr t0, sscratch
                sd t0, 112(a0)

                # restore kernel stack pointer from p->trapframe->kernel_sp
                # 完成了内核栈的切换
                ld sp, 8(a0)

                # make tp hold the current hartid, from p->trapframe->kernel_hartid
                ld tp, 32(a0)

                # load the address of usertrap(), p->trapframe->kernel_trap
                ld t0, 16(a0)

                # restore kernel page table from p->trapframe->kernel_satp
                ld t1, 0(a0)
                # 这里完成了页表的切换
                csrw satp, t1
                sfence.vma zero, zero

                # a0 is no longer valid, since the kernel page
                # table does not specially map p->tf.

                # jump to usertrap(), which does not return
                jr t0
                -

                由图可知,一直从0x0到0x86400000,都是采取的直接映射的方式,虚拟地址=物理地址,这段是内核使用的空间。在0x0-0x800000000阶段,物理地址代表着各种IO设备的存储器。

                -

                但是注意,在0x86400000(PHYSTOP)以上的地址都不是直接映射,这些非直接映射的层级包含两类:

                -
                  -
                1. trampoline

                  -
                  -

                  It is mapped at the top of the virtual address space; user page tables have this same mapping.

                  -
                  -

                  它有一点很特殊的是,它实际对应的物理内存是0x80000000开始的一段。也就是说,0x80000000开始的这段内存,既被直接映射了,也被trampoline通过虚拟地址映射了。它被映射了两次。

                  -
                2. -
                3. 内核栈

                  -

                  Each process has its own kernel stack, which is mapped high so that below it xv6 can leave an unmapped guard page. The guard page’s PTE is invalid (i.e., PTE_V is not set), so that if the kernel overflflows a kernel stack, it will likely cause an exception and the kernel will panic.

                  -

                  guard page可以用来防止内核栈溢出。

                  +

                  注:trampoline和trapframe有一些相通点。

                  +

                  trampoline为了保障某段物理内存的虚拟地址在内核栈和用户栈中不变,做出的努力是,在内核栈和用户栈都分配同一位置的PTE。

                  +

                  trapframe用于保护现场、用户态向内核态传递参数等等,做出的努力是,在用户栈分配同一位置的PTE,在内核态的局部变量中保存了自己的物理地址。

                  +

                  这两个说实话有点容易混起来,因为我想了半天trampoline可不可以用类似trapframe一样的方法,结论是不行。因为你trampoline的作用是维持指令序列依然不变,不会突然没掉;而trapframe段是用来存储数据而非执行的,对其的控制也是需要指令的。如果trampoline使用第二种方法,指令流就会断掉,更别说别的了。

                  -
                4. -
                -

                内核使用PTE_R和PTE_X权限映射trampoline和kernel text。这表明这份内存段可以读,可以被当做指令块执行,但不能写。其他的块都是可读可写的,除了guard page被设置为不可访问。

                -

                Code: creating an address space

                vm.c

                操作地址空间和页表部分的代码都在kernel/vm.c中。代表页表的数据结构是pagetable_t

                -

                vm.c的主要函数有walk、mappages等。walk用来在三级页表中找到某个虚拟地址表项,或者创建一个新的表项。mappages用来新建一个表项,主要用到了walk函数。

                -

                vm.c中,以kvm开头的代表操纵内核页表,以uvm开头的代表操纵进程里的用户页表。

                -

                以初始化为例介绍各个函数

                创建页表

                一开始操作系统初始化时,会调用vm.c中的kvminit来创建内核页表。主要就是在以内核地址空间的页表结构在填写页表。

                -
                void
                kvminit(void)
                {
                kernel_pagetable = kvmmake();
                }
                // Make a direct-map page table for the kernel.
                pagetable_t
                kvmmake(void)
                {
                //内核页表
                pagetable_t kpgtbl;
                //申请新的一页
                kpgtbl = (pagetable_t) kalloc();
                memset(kpgtbl, 0, PGSIZE);

                //给内核页表初始化表项,结构详见上面的内核地址空间部分
                // uart registers
                kvmmap(kpgtbl, UART0, UART0, PGSIZE, PTE_R | PTE_W);

                // virtio mmio disk interface
                kvmmap(kpgtbl, VIRTIO0, VIRTIO0, PGSIZE, PTE_R | PTE_W);

                // PLIC
                kvmmap(kpgtbl, PLIC, PLIC, 0x400000, PTE_R | PTE_W);

                // map kernel text executable and read-only.
                kvmmap(kpgtbl, KERNBASE, KERNBASE, (uint64)etext-KERNBASE, PTE_R | PTE_X);

                // map kernel data and the physical RAM we'll make use of.
                kvmmap(kpgtbl, (uint64)etext, (uint64)etext, PHYSTOP-(uint64)etext, PTE_R | PTE_W);

                // map the trampoline for trap entry/exit to
                // the highest virtual address in the kernel.
                kvmmap(kpgtbl, TRAMPOLINE, (uint64)trampoline, PGSIZE, PTE_R | PTE_X);

                // allocate and map a kernel stack for each process.
                proc_mapstacks(kpgtbl);

                return kpgtbl;
                }
                +

                usertrap

                作用是得到trap发生的原因,并且执行对应的处理程序,然后返回结果。

                +
                // handle an interrupt, exception, or system call from user space.
                // called from trampoline.S
                void
                usertrap(void)
                {
                int which_dev = 0;

                if((r_sstatus() & SSTATUS_SPP) != 0)
                panic("usertrap: not from user mode");

                // send interrupts and exceptions to kerneltrap(),
                // since we're now in the kernel.
                //首先把trap handler切换到kernel的,这样一来如果在kernel中发生trap就会由kernel的handler处理
                w_stvec((uint64)kernelvec);

                struct proc *p = myproc();

                //在当前进程中再次保存用户程序的原PC,防止之后sepc被覆盖
                // save user program counter.
                p->trapframe->epc = r_sepc();

                //根据cause号不同处理
                if(r_scause() == 8){
                // system call

                if(p->killed)
                exit(-1);

                // sepc points to the ecall instruction,
                // but we want to return to the next instruction.
                p->trapframe->epc += 4;

                // an interrupt will change sstatus &c registers,
                // so don't enable until done with those registers.
                //注意,在此处开启了中断
                intr_on();

                //调用syscall处理
                syscall();
                } else if((which_dev = devintr()) != 0){
                // ok
                } else {
                printf("usertrap(): unexpected scause %p pid=%d\n", r_scause(), p->pid);
                printf(" sepc=%p stval=%p\n", r_sepc(), r_stval());
                p->killed = 1;
                }

                if(p->killed)
                exit(-1);

                // give up the CPU if this is a timer interrupt.
                if(which_dev == 2)
                yield();

                usertrapret();
                }
                -

                其中,kvmmap用来在内核页表中添加一个新的表项。其函数形式为

                -
                // add a mapping to the kernel page table.
                // only used when booting.
                // does not flush TLB or enable paging.
                void
                kvmmap(pagetable_t kpgtbl, uint64 va, uint64 pa, uint64 sz, int perm)
                {
                if(mappages(kpgtbl, va, sz, pa, perm) != 0)
                panic("kvmmap");
                }
                +

                执行对应的处理函数

                比如说system call会修改trapframe中的a0为返回的结果,会获取trapframe中的各个参数。这个“保护现场“感觉是非常微妙的,它兼顾了保护现场和传递参数两个作用

                +

                usertrapret

                回到用户态。之前陷入内核态对stvec、satp、sp、hartid、trap handler都做了适应内核态的改变,因而这里就需要改回原来适应用户态的样子,然后返回用户态。

                +
                // return to user space
                void
                usertrapret(void)
                {
                struct proc *p = myproc();

                // 关中断
                // we're about to switch the destination of traps from
                // kerneltrap() to usertrap(), so turn off interrupts until
                // we're back in user space, where usertrap() is correct.
                intr_off();

                // 重置trapframe
                // send syscalls, interrupts, and exceptions to trampoline.S
                w_stvec(TRAMPOLINE + (uservec - trampoline));
                // set up trapframe values that uservec will need when
                // the process next re-enters the kernel.
                p->trapframe->kernel_satp = r_satp(); // kernel page table
                p->trapframe->kernel_sp = p->kstack + PGSIZE; // process's kernel stack
                p->trapframe->kernel_trap = (uint64)usertrap;
                p->trapframe->kernel_hartid = r_tp(); // hartid for cpuid()

                // set up the registers that trampoline.S's sret will use
                // to get to user space.

                // set S Previous Privilege mode to User.
                unsigned long x = r_sstatus();
                x &= ~SSTATUS_SPP; // clear SPP to 0 for user mode
                x |= SSTATUS_SPIE; // enable interrupts in user mode
                w_sstatus(x);

                // set S Exception Program Counter to the saved user pc.
                w_sepc(p->trapframe->epc);

                // tell trampoline.S the user page table to switch to.
                uint64 satp = MAKE_SATP(p->pagetable);

                // jump to trampoline.S at the top of memory, which
                // switches to the user page table, restores user registers,
                // and switches to user mode with sret.
                uint64 fn = TRAMPOLINE + (userret - trampoline);
                ((void (*)(uint64,uint64))fn)(TRAPFRAME, satp);
                }
                -

                实现主要逻辑的是mappages函数

                -
                // Create PTEs for virtual addresses starting at va that refer to
                // physical addresses starting at pa. va and size might not
                // be page-aligned. Returns 0 on success, -1 if walk() couldn't
                // allocate a needed page-table page.
                int
                mappages(pagetable_t pagetable, uint64 va, uint64 size, uint64 pa, int perm)
                {
                uint64 a, last;
                pte_t *pte;

                if(size == 0)
                panic("mappages: size");

                a = PGROUNDDOWN(va);
                last = PGROUNDDOWN(va + size - 1);
                for(;;){
                //walk函数通过虚拟地址新建一个第三级页表的表项并返回其指针,之后只需要填这个表项即可
                if((pte = walk(pagetable, a, 1)) == 0)
                return -1;
                //如果pte存在并且标记为已使用,说明该虚拟地址映射已经存在
                if(*pte & PTE_V)
                panic("mappages: remap");
                //填写表项:物理地址 flags
                *pte = PA2PTE(pa) | perm | PTE_V;
                if(a == last)
                break;
                //每两个表项间隔PGSIZE个字节
                a += PGSIZE;
                pa += PGSIZE;
                }
                return 0;
                }
                +

                userret

                .globl userret
                userret:
                # userret(TRAPFRAME, pagetable)
                # switch from kernel to user.
                # usertrapret() calls here.
                # a0: TRAPFRAME, in user page table.
                # a1: user page table, for satp.

                # 切换为用户页表
                # switch to the user page table.
                csrw satp, a1
                sfence.vma zero, zero

                # put the saved user a0 in sscratch, so we
                # can swap it with our a0 (TRAPFRAME) in the last step.
                ld t0, 112(a0)
                csrw sscratch, t0

                # restore all but a0 from TRAPFRAME
                ld ra, 40(a0)
                ld sp, 48(a0)
                # ...
                ld t6, 280(a0)

                # restore user a0, and save TRAPFRAME in sscratch
                csrrw a0, sscratch, a0

                # return to user mode and user pc.
                # usertrapret() set up sstatus and sepc.
                sret
                -

                通过虚拟地址获取表项主要是通过walk实现的

                -
                // Return the address of the PTE in page table pagetable
                // that corresponds to virtual address va. If alloc!=0,
                // create any required page-table pages.
                //
                // The risc-v Sv39 scheme has three levels of page-table
                // pages. A page-table page contains 512 64-bit PTEs.
                // A 64-bit virtual address is split into five fields:
                // 39..63 -- must be zero.
                // 30..38 -- 9 bits of level-2 index.
                // 21..29 -- 9 bits of level-1 index.
                // 12..20 -- 9 bits of level-0 index.
                // 0..11 -- 12 bits of byte offset within the page.
                // 虚拟地址的格式:UNUSED 页表索引 offset,其中页表索引在三级页表中被划分为了三个,分别是
                // level0-level2,分别代表了第三级、第二级、第一级页表的索引【具体可见页表组成中的图】
                // walk的目的就是要在这三级页表中找到虚拟地址对应的页表项。当alloc!=0时,则要求找不到就新建一个
                pte_t *
                walk(pagetable_t pagetable, uint64 va, int alloc)
                {
                if(va >= MAXVA)
                panic("walk");

                for(int level = 2; level > 0; level--) {
                pte_t *pte = &pagetable[PX(level, va)];
                if(*pte & PTE_V) {
                // 取出PTE中表示下一级页表地址的字节
                pagetable = (pagetable_t)PTE2PA(*pte);
                } else {
                // 页表不存在的情况,要么返回0,要么新建一页
                if(!alloc || (pagetable = (pde_t*)kalloc()) == 0)
                return 0;
                memset(pagetable, 0, PGSIZE);
                *pte = PA2PTE(pagetable) | PTE_V;
                }
                }
                // 最终返回第三级页表的对应表项
                return &pagetable[PX(0, va)];
                }
                +

                Code: calling system calls

                +

                Chapter 2 ended with initcode.S invoking the exec system call (user/initcode.S:11). Let’s look at how the user call makes its way to the exec system call’s implementation in the kernel.要讲如何从用户态找到exec的代码了。

                +
                +

                Code: system call arguments

                讲的是系统调用时,是如何把用户态传递的地址转化为内核态地址的。

                +

                这个部分可以看看hit实验的实验3 6.7,讲得很详细,而且流程是差不多的。linux0.11的get_fs_byte()就相当于xv6的copyinstr

                +

                Traps from kernel space

                kernelvec

                不同于用户态还得先潜入内核再潜出内核,内核的trap可简单多了,省去了切来切去各种东西的步骤,只需当做一个普通的函数调用就行。

                +
                # in kernel/kernelvec.S
                .globl kerneltrap
                .globl kernelvec
                .align 4
                kernelvec:
                // make room to save registers.
                addi sp, sp, -256

                // save the registers.
                sd ra, 0(sp)
                sd sp, 8(sp)
                # ...
                sd t6, 240(sp)

                // call the C trap handler in trap.c
                call kerneltrap

                // restore registers.
                ld ra, 0(sp)
                ld sp, 8(sp)
                # ...
                ld t5, 232(sp)
                ld t6, 240(sp)

                addi sp, sp, 256

                // return to whatever we were doing in the kernel.
                sret
                -
                装上页表

                使用的是kvminithart函数。它将内核页表的root page table的物理地址写入了satp寄存器。从这个函数之后,就开启了内存映射

                -
                // Switch h/w page table register to the kernel's page table,
                // and enable paging.
                void
                kvminithart()
                {
                // wait for any previous writes to the page table memory to finish.
                sfence_vma();

                w_satp(MAKE_SATP(kernel_pagetable));

                // flush stale entries from the TLB.
                sfence_vma();
                }
                +

                kerneltrap

                +

                kerneltrap is prepared for two types of traps: device interrrupts and exceptions.

                +

                It calls devintr (kernel/trap.c:177) to check for and handle the former. If the trap isn’t a device interrupt, it must be an exception, and that is always a fatal error if it occurs in the xv6 kernel; the kernel calls panic and stops executing.

                +

                If kerneltrap was called due to a timer interrupt, and a process’s kernel thread is running (rather than a scheduler thread), kerneltrap calls yield to give other threads a chance to run.

                +
                +
                // in kernel/trap.c
                // interrupts and exceptions from kernel code go here via kernelvec,
                // on whatever the current kernel stack is.
                void
                kerneltrap()
                {
                int which_dev = 0;
                uint64 sepc = r_sepc();
                uint64 sstatus = r_sstatus();
                uint64 scause = r_scause();

                if((sstatus & SSTATUS_SPP) == 0)
                panic("kerneltrap: not from supervisor mode");
                if(intr_get() != 0)
                panic("kerneltrap: interrupts enabled");

                // 在此处的devintr对不同的设备进行不同的处理方式
                if((which_dev = devintr()) == 0){
                printf("scause %p\n", scause);
                printf("sepc=%p stval=%p\n", r_sepc(), r_stval());
                panic("kerneltrap");
                }

                // give up the CPU if this is a timer interrupt.
                if(which_dev == 2 && myproc() != 0 && myproc()->state == RUNNING)
                yield();

                // the yield() may have caused some traps to occur,
                // so restore trap registers for use by kernelvec.S's sepc instruction.
                w_sepc(sepc);
                w_sstatus(sstatus);
                }
                -

                其中sfence_vma()的用途是强制更新TLB的旧页表,类似于Java volatile的作用。

                -
                疑问

                附上书里的详细解释:

                -

                image-20230109222917346

                -

                TLB与页表类似于cache与主存的关系。TLB保存了页表的一部分。

                -
                我的错误想法

                我怎么感觉怪怪的啊?因为TLB既然是高速缓存,那么读写页表也应该优先从TLB读写【注:应该就是从这里开始错的hhh写应该是直接写入页表】。所以说,会陈旧的应该是主存中的页表,而不是TLB中的页表。但是,书里是说,改完页表必须通知TLB更改。也就是说,读写页表不是从TLB读写的,那该是从哪里?是TLB以外的free memory吗?

                -

                不过,要是从多CPU的角度思考,说不定他这个意思是某个CPU的TLB变了,需要通知其他所有CPU的TLB也变。虽然不同CPU当前执行的进程是不一样的,使用的页表项不一样,切换进程的时候也会把用户地址空间的页表项flush掉。但是内核地址空间的页表项一般是不会随着进程切换而flush掉的。所以内核页表修改就需要手动多CPU同步。

                -

                我认为多CPU角度考虑更加合理,因为它最后说了,xv6会在内核页表init后flush,以及在从内核态切换回用户态的时候flush。这两个(好像)都影响内核页表比较多,所以就需要手动flush一下。

                -
                解答

                之后学了缺页异常后,可以发现这里其实是没问题的。

                -

                计算机体系结构 – 虚拟内存

                -

                v2-e15454bf032baa4dc088b6e41ed4f4a4_1440w

                -

                页表的管理(创建、更新、删除等)是由操作系统负责的。地址转换时,页表检索是由硬件内存管理单元(Memory Management Unit, MMU)负责的。MMU通常由两部分构成:表查找单元(Table Walk Unit, TWU)和转换旁路缓冲(Translation Lookaside Buffer, TLB)[2]。TWU负责链式的访问PDE、PTE,完成上述的查表过程。

                -

                应用多级页表之后,想要完成一次地址转换,需要访问多级目录和页表,这么多次的内存访问会严重降低性能。

                -

                为了优化地址转换速度,人们在MMU中增加了一块高速cache,专门用来缓存虚拟地址到物理地址的映射,这块cache就是TLB[7][8]。MMU在做地址转换的时候,会先检索TLB,如果命中则直接返回对应的物理地址,如果不命中则会调用TWU查找页表。

                -

                TLB中缓存的是虚拟地址到物理地址映射。然而,多级页表的查找是一个链式的过程,对于在虚拟地址空间中连续的两个页,它们的各级目录项可能都是一样的,只有最后一级页号不一样。查找完第一个虚拟页之后,我们可以将相同的前级目录项都缓存起来。查找第二个虚拟页时,可以直接使用缓存好的前几级目录项,节省查找时间。这种缓存叫做Page Structure Cache[9]

                -

                而当TLB和MMU中都没有该物理页,就会发生缺页异常。但是操作系统仅会对页表更新,而不会被TLB更新。故而,TBL中数据可能陈旧,需要手动flush。

                -

                Physical memory allocation

                在内核运行的时候,需要申请很多空间用来存放各种数据。

                -
                -

                The kernel must allocate and free physical memory at run-time for page tables, user memory, kernel stacks, and pipe buffers.

                +

                Page-fault exceptions

                似乎xv6是没有这个缺页exception的。这里主要讲解了三个可以利用缺页中断实现的优化:COW fork、lazy allocation、paging from disk。还提及了automatically extending stacks 以及memory-mapped fifiles。

                +

                Lab:Trap

                +

                This lab explores how system calls are implemented using traps. You will first do a warm-up exercises with stacks and then you will implement an example of user-level trap handling.

                -

                用的是这段空闲内存:

                -

                image-20230109225700837

                -
                -

                It keeps track of which pages are free by threading a linked list through the pages themselves.

                +

                RISC-V assembly

                题目和答案

                +

                参考:

                +

                Lab4: traps

                +

                There is a file user/call.c in your xv6 repo. make fs.img compiles it and also produces a readable assembly version of the program in user/call.asm.

                +

                Read the code in call.asm for the functions g, f, and main. Here are some questions that you should answer:

                +
                +
                  +
                1. a2

                  +
                2. +
                3. 被inline掉了

                  +
                4. +
                5. 0x64A

                  +

                  image-20230111224927837

                  +

                  auipc的作用是把立即数左移12位,低12位补0,和pc相加赋给指定寄存器。这里立即数是0,指定寄存器是ra,即ra=pc=0x30=48。jalr作用是跳转到立即数+指定寄存器处并且把ra的值置为下一条指令。因此jalr会跳转1562+48=1594=0x64A处,观察汇编代码可知确实在000000000000064a处。

                  +
                6. +
                7. 0x38

                  +
                8. +
                9. +

                  Run the following code.

                  +
                  unsigned int i = 0x00646c72;
                  printf("H%x Wo%s", 57616, &i);
                  + +

                  What is the output? Here’s an ASCII table that maps bytes to characters.

                  +

                  The output depends on that fact that the RISC-V is little-endian. If the RISC-V were instead big-endian what would you set i to in order to yield the same output? Would you need to change 57616 to a different value?

                  +

                  format,png

                  +
                  +
                10. +
                11. 取决于寄存器a2(第3个参数)的值。

                  +
                12. +
                +

                Backtrace

                +

                For debugging it is often useful to have a backtrace: a list of the function calls on the stack above the point at which the error occurred.

                +

                image-20230111232323444

                +

                The compiler puts in each stack frame a frame pointer that holds the address of the caller’s frame pointer. Your backtrace should use these frame pointers to walk up the stack and print the saved return address in each stack frame.

                -

                kalloc.c中就是这么实现的。

                -

                Code: Physical memory allocator

                内核运行时申请释放空闲物理空间是通过kernel/kalloc.c完成的。它为内核栈、用户进程、页表和管道buffer服务。

                -

                kalloc.c用来在运行时申请分配新的一页,上面的vm.c正是用了kalloc申请一页,要么作为页表,要么作为存储数据的第三级页表指向的物理内存。

                +

                Some hints:

                +
                  +
                • Add the prototype for backtrace to kernel/defs.h so that you can invoke backtrace in sys_sleep.

                  +
                • +
                • The GCC compiler stores the frame pointer of the currently executing function in the register s0. Add the following function to kernel/riscv.h:

                  +
                  static inline uint64
                  r_fp()
                  {
                  uint64 x;
                  asm volatile("mv %0, s0" : "=r" (x) );
                  return x;
                  }
                  + +

                  and call this function in backtrace to read the current frame pointer. This function uses in-line assembly to read s0.

                  +
                • +
                • These lecture notes have a picture of the layout of stack frames. Note that the return address lives at a fixed offset (-8) from the frame pointer of a stackframe, and that the saved frame pointer lives at fixed offset (-16) from the frame pointer.

                  +
                • +
                • Xv6 allocates one page for each stack in the xv6 kernel at PAGE-aligned address. You can compute the top and bottom address of the stack page by using PGROUNDDOWN(fp) and PGROUNDUP(fp) (see kernel/riscv.h. These number are helpful for backtrace to terminate its loop.

                  +
                • +
                -

                最后应该会在空闲内存内形成这样的结构:

                -

                内存分成一页一页的,每页内存中的前几个字节存储着其对应队列中下一块内存的物理地址。不一定是从小地址到大地址顺序连接。

                +

                感想

                我超,这题真的是那怎能只叫一个拷打……

                +
                存储在s0中的栈帧指针

                这个应该是risc-v的约定成俗的特性。我搜了一下risc-v的栈帧指针保存在哪个寄存器,看到了这样一篇文章:

                -

                It store each free page’s run structure in the free page itself, since there’s nothing else stored there.

                +

                risc-v 栈分析

                +

                image-20230112012358601

                -
                // Physical memory allocator, for user processes,
                // kernel stacks, page-table pages,
                // and pipe buffers. Allocates whole 4096-byte pages.

                #include "types.h"
                #include "param.h"
                #include "memlayout.h"
                #include "spinlock.h"
                #include "riscv.h"
                #include "defs.h"

                // 释放在这范围内的物理内存空间
                void freerange(void *pa_start, void *pa_end);

                // 也就是上面说的free memory的起始位置
                extern char end[]; // first address after kernel.
                // defined by kernel.ld.

                // run代表的是一页内存
                struct run {
                struct run *next;
                };

                // 代表了整个内核空闲的物理空间
                struct {
                struct spinlock lock;
                struct run *freelist;
                } kmem;

                void
                kinit()
                {
                initlock(&kmem.lock, "kmem");
                // init的时候先清空空闲空间,建立空闲页队列
                freerange(end, (void*)PHYSTOP);
                }

                void
                freerange(void *pa_start, void *pa_end)
                {
                char *p;
                // PGROUNDUP和PGROUNDDOWN是用于将地址四舍五入到PGSIZE
                p = (char*)PGROUNDUP((uint64)pa_start);
                for(; p + PGSIZE <= (char*)pa_end; p += PGSIZE)
                kfree(p);
                }

                // Free the page of physical memory pointed at by pa,
                // which normally should have been returned by a
                // call to kalloc(). (The exception is when
                // initializing the allocator; see kinit above.)
                void
                kfree(void *pa)
                {
                struct run *r;

                // pa得是整数页,并且得在内核物理内存范围之间
                if(((uint64)pa % PGSIZE) != 0 || (char*)pa < end || (uint64)pa >= PHYSTOP)
                panic("kfree");

                // Fill with junk to catch dangling refs.
                memset(pa, 1, PGSIZE);

                // 之后将在pa对应的那一页的前几个字节写入next字段
                r = (struct run*)pa;

                // 这意思就是在空闲内存的链表队列中新增一块
                acquire(&kmem.lock);
                r->next = kmem.freelist;
                kmem.freelist = r;
                release(&kmem.lock);
                }

                // Allocate one 4096-byte page of physical memory.
                // Returns a pointer that the kernel can use.
                // Returns 0 if the memory cannot be allocated.
                void *
                kalloc(void)
                {
                struct run *r;

                acquire(&kmem.lock);
                r = kmem.freelist;
                if(r)
                kmem.freelist = r->next;
                release(&kmem.lock);

                if(r)
                memset((char*)r, 5, PGSIZE); // fill with junk
                return (void*)r;
                }
                +

                这个信息没有放在题干提示,是在考察信息检索能力吗(

                +
                栈的结构与栈帧的理解

                image-20230112010749756

                +

                这是来自hint的栈结构。整个栈存储在一页中,由高地址向低地址增长。栈帧代表了一次函数调用,其中会存储如函数名、函数参数、局部变量等等信息。有几次函数调用就有几个栈帧,栈由栈帧组成。

                +

                s0中存储的栈帧指针fp指向的是栈帧的最高地址,如图fp所示。

                +
                +

                我理解错了栈帧的定义,都怪我基础不大牢固也不认真思考【悲】我一开始以为stack frame指的是一个栈,也即一页空间【我知道栈帧这个中文名词,但遇到英语就短路了】。老师画的这个图也被我理解为多个栈,也即多页拼在一起,要打印的Return Address处于页的最顶部。我就在这个思路上一去不复返了,压根没有意识到一个进程只有一个栈【大悲】然后顺带脑补把r_fp()也曲解了,以为它的意思是读取当前栈【非常自然地认为有很多个栈←】的下一个栈的最低地址【因为栈换掉了,所以s0也会变成父亲的栈的地址】。于是就写出了这样的代码:

                +
                void
                backtrace(){
                printf("backtrace:\n");
                uint64 kstack = PGROUNDUP((uint64)(myproc()->kstack)+1);
                uint64 nstack = 0;
                while((nstack=(uint64)r_fp())!=0){
                printf("%p\n",*((uint64*)(kstack-8)));
                kstack = nstack;
                }
                }
                -

                Process address space

                当用户进程叫xv6分配内存时,xv6会用kalloc去取,然后登记在页表上。

                -
                -

                The stack is a single page, and is shown with the initial contents as created by exec. Strings containing the command-line arguments, as well as an array of pointers to them, are at the very top of the stack. Just under that are values that allow a program to start at main as if the function main(argc, argv) had just been called.

                +

                结果最后死循环了。去看了别人的代码发现他们写的结构就跟我完全不一样。琢磨着画着图,最后找了stack frame的定义,才恍然大悟(

                -

                image-20230109234930690

                -

                Code: sbrk

                -

                Sbrk is the system call for a process to shrink or grow its memory. The system call is implemented by the function growproc (kernel/proc.c:239).

                +
                思路形成

                我们只需遍历栈中所有栈帧,打印每个栈帧的Return Address部分就行。通过r_fp()获取第一个栈帧的位置,其他栈帧的位置由Prev.Frame获取。循环的界限是PGROUNDUP(r_fp()),因为栈只有一页的空间。

                +

                代码

                void
                backtrace(){
                printf("backtrace:\n");
                uint64 stack = r_fp();
                uint64 nstack = 0;
                uint64 top = PGROUNDUP(stack);
                while(stack!=top){
                nstack=*((uint64*)(stack-16));
                printf("%p\n",*((uint64*)(stack-8)));
                stack = nstack;
                }
                }
                + +

                Alarm

                +

                In this exercise you’ll add a feature to xv6 that periodically alerts a process as it uses CPU time. This might be useful for compute-bound processes that want to limit how much CPU time they chew up, or for processes that want to compute but also want to take some periodic action.

                +

                More generally, you’ll be implementing a primitive form of user-level interrupt/fault handlers; you could use something similar to handle page faults in the application, for example.

                +

                You should add a new sigalarm(interval, handler) system call. If an application calls sigalarm(n, fn), then after every n “ticks” of CPU time that the program consumes, the kernel should cause application function fn to be called. When fn returns, the application should resume where it left off.

                -
                // Grow or shrink user memory by n bytes.注意单位是bytes,grow n+,shrink n-
                // Return 0 on success, -1 on failure.
                // 主要逻辑还是通过vm.c实现
                int
                growproc(int n)
                {
                uint64 sz;//size
                struct proc *p = myproc();

                sz = p->sz;
                if(n > 0){
                if((sz = uvmalloc(p->pagetable, sz, sz + n, PTE_W)) == 0) {
                return -1;
                }
                } else if(n < 0){
                sz = uvmdealloc(p->pagetable, sz, sz + n);
                }
                p->sz = sz;
                return 0;
                }
                +

                感觉从alarm中可以窥见信号的实现思路:

                +

                image-20231218164244526

                +

                而alarm的机理感觉也有点类似。用户先通过sigalarm注册定时函数,内核在时钟中断的时候对该标记位进行检查,然后去do_signal回到用户态执行用户的signal handler,再通过sigreturn回到用户模式。sigalarm相当于一个信号注册函数,sigreturn也就是上图的sigreturn。

                +

                分析

                初见思路

                思路:sigalarm需要在用户程序在用户态运行的情况下,监测到用户程序已经运行了n个时间片,然后发出中断请求。我们会新设置一个中断类型alarm。kerneltrap接收到sigalarm的中断请求,检测到中断类型为alarm,就会在处理的时候调用fn。fn调用完就自然而然利用中断恢复到原来的现场了。所以要做的可以分为两部分。但问题是,如何让sigalarm在用户程序运行的同时监测n个时间片呢?难道得fork一个新的进程吗?然后父进程返回,子进程执行类似sleep里面那样的监测,直到时间片到了,就发送一个中断请求,让父进程停止,执行完fn回来之后就exit。

                +
                正确思路

                可以看到,初见思路很多地方跟最后是不一样的。其中错得最离谱的,也是比较隐坑很容易因为想不明白就寄了的,是handler是个用户态的函数(。你不可能在内核态中调用fn,然后fn执行完后再自然而然地通过中断机制返回,因为你想要执行fn就必须进入用户态。这一点是需要一开始明确的。

                +

                明确了这一点后,让人更加不知道该怎么办了。那就一步步跟着指导书的脚步来思考吧。

                +
                part 1

                首先,明确你需要实现什么。你需要实现两个系统调用,一个是sigalarm,一个是sigreturn。结合提示,可知实验设计者给我们的思路是,通过sigalarm设置定时函数,通过sigalarm(0,0)取消定时函数。每次时钟中断检测当前定时时间是否达到,若已达到,则跳到定时函数执行。定时函数执行完后,需要借助sigreturn,才能正确返回时钟中断前的程序点。

                +
                part 2

                这又可拆解为几个要点:

                +
                  +
                1. 如何实现“定时”?
                2. +
                3. 时钟中断在内核态的usertrap被检测。怎么从usertrap出来跳到定时函数而非原程序执行点?
                4. +
                5. 执行完定时函数后,怎么样才能回到原程序执行点?
                6. +
                +
                part 3

                一个个来说,首先是如何实现定时。这个很简单。参照sys_sleep的代码:

                +
                uint64
                sys_sleep(void)
                {
                int n;
                uint ticks0;

                backtrace();
                if(argint(0, &n) < 0)
                return -1;
                acquire(&tickslock);
                ticks0 = ticks;
                while(ticks - ticks0 < n){
                if(myproc()->killed){
                release(&tickslock);
                return -1;
                }
                sleep(&ticks, &tickslock);
                }
                release(&tickslock);

                return 0;
                }
                -
                // Allocate PTEs and physical memory to grow process from oldsz to
                // newsz, which need not be page aligned.不需要页对齐 Returns new size or 0 on error.
                uint64
                uvmalloc(pagetable_t pagetable, uint64 oldsz, uint64 newsz, int xperm)
                {
                char *mem;
                uint64 a;

                if(newsz < oldsz)
                return oldsz;

                // oldsz向上取整
                oldsz = PGROUNDUP(oldsz);
                // 每页alloc
                for(a = oldsz; a < newsz; a += PGSIZE){
                mem = kalloc();
                if(mem == 0){
                // 说明失败,恢复到原状
                // 这里不用像下面一样kfree是因为这里压根没有alloc成功
                uvmdealloc(pagetable, a, oldsz);
                return 0;
                }
                // 除去junk data
                memset(mem, 0, PGSIZE);
                // 放入页表
                if(mappages(pagetable, a, PGSIZE, (uint64)mem, PTE_R|PTE_U|xperm) != 0){
                // 不成功
                // dealloc原理是顺着页表一个个free的。由于mem此处没有成功放入页表,所以就得单独free掉
                kfree(mem);
                uvmdealloc(pagetable, a, oldsz);
                return 0;
                }
                }
                return newsz;
                }
                +

                可知,我们可以用ticks表示当前系统滴答数。这样,我们就可以在proc域里维护一个变量lasttick,记录上一次执行handler时的滴答数。每次在时钟中断时检测,所以需要写在kernel/trap.c中的usertrap中。

                +
                part 4

                然后,是怎么从usertrap出来跳到指定程序结束点。在书中,我们知道,sepc寄存器保存了中断前原程序的下一个执行点,sepc的备份存储在了proc域中的栈帧中。当中断返回时(在usertrapret中),会从栈帧中的epc字段读取sepc的备份赋值给sepc,再由sret帮助我们跳转到原程序点。因而,如果想要改变跳转点,我们只需要修改p->trapframe->epc就行。

                +
                part 5

                最后,是如何从periodic回到原程序执行点。

                +

                image-20230113002057893

                +

                这是每次进行时钟中断时的栈情况和执行代码链:t1->trampoline->usertrap->handler。

                +

                再然后,handler调用了sigreturn,用户栈中就会产生sigreturn的栈帧:

                +

                image-20230113002434093

                +

                此时,如果sigreturn执行完,就会在这样的情况下执行handler的ret指令:

                +

                image-20230113002542335

                +

                ret指令会把栈帧弄走,也就是说会直接回到某个错误的地方去。这显然不大合适。所以,我们要做的,就是在sigreturn之后,不执行handler的ret指令,也不执行sigreturn的ret指令,而是直接恢复到时钟中断前的上下文。时钟中断前的上下文,会因在handler中调用sigreturn系统调用,而被覆盖,因而,我们就需要记录时钟中断前的上下文,也即在proc域中保存trapframe的一份拷贝savedtrap,每次时钟中断都更新一次savedtrap,然后在sigreturn调用的时候将proc原本的trapframe替换为savedtrap即可。这样一来,就完成了这道题。

                +
                感想

                这题目确实最终代码看起来完全不难,但是非常地拷打。。。。我前前后后修修补补差不多一共花了五个小时之久。

                +

                计时怎么计,以及使用trapframe->epc来跳转这两点还是很容易想到的【话虽如此,其实也很曲折】。主要难点还是在怎么恢复现场。怎么说呢,我花了这么久做实验,但是实际笔记却写不出个鬼来,足以看出其复杂程度。

                +

                我主要还是思维固化了点,一直在想,怎么确保它正确返回现场。我一开始以为proc域保存一个寄存器状态,且只用在一开始设置定时函数也即sigalarm的时候保存一次就行了,并且认为其是epc。然而实际操作后发现usertrap崩了,并且epc中存储的并不是程序被时钟中断的地方,而是各种神奇的地方,具体我也忘了,反正不能行。我印象最深刻的是有一次停在了usys.S中的sigreturn的最后一个ret处。我就在想,也许是栈出了问题。于是我就想着直接在sigreturn的时候把epc指向栈帧中的return address,直接回到原执行段。我百度了一下,确实有这么个寄存器ra,存储着return address。于是我就把proc域的状态换成了ra,依然仅保存一次,最后发现还是不行,程序在test0之后就异常终止了,main也回不去,十分古怪,十分匪夷所思。我实在没忍住,百度了一下大家怎么做的,发现大家压根没有我这样的二选一的烦恼,是直接保存整个栈帧。而且也不是仅保存一次,而是每次时钟中断触发都保存一次。我觉得十分奇怪震惊,但此时已是差不多晚饭时间,我就先去吃了个饭()

                +

                回来之后,我细细画了图【向正确思路part5中的那样】,发现我原来那个只保存两者之一,且都只保存一次的方式,确实完全不能行。但是,我发现两个一起保存,并且每次时钟中断保存的方法,似乎能行,而且,比保存一整个栈帧要聪明得多。于是我就去试了,发现还是不行。我再细想了一遍,发现,如果想回去原程序的现场,除了ra和epc,还有一个很重要的东西需要保存,那就是——用户栈指针sp!

                +

                也就是说,只需保存ra、epc、sp,就可以保证回到正确的时钟中断前的位置

                +

                image-20230113005100895

                +

                此为handler中sigreturn执行完要返回时的状态。

                +

                当处在handler中时,sp的值为sigreturn处的栈帧。执行系统调用时,proc域中的上下文被覆盖,也即时钟中断前的上下文被覆盖。如果此时不对栈帧中的sp进行恢复,仅恢复ra和epc,在从sigreturn返回到epc对应处也即t1,t1执行ret想回到main的时候,就会回不去,而是回到了sigreturn要回的位置,也即handler的位置,然后不知不觉就寄了。所以,就需要防止sp被覆盖。因而,再保存一个状态sp,就可以保障回到正确的地方了。测试出来,kernel确实不再会panic了。

                +

                但是由于运行时很多除这三个以外的寄存器都被改过了,回是回得去,接下来干的活就不一定对了。因此为了保险以及通用性以及便利性来看,还是像别人那样直接保存栈帧比较ok。

                +

                还有一件事,就是上述错误中经常会出现的一个输出结果:

                +
                usertrap:unexpected cause scause = 0x0c
                -

                Code:exec

                -

                Exec is the system call that creates the user part of an address space. It initializes the user part of an address space from a fifile stored in the fifile system.

                -

                exec是创建地址空间的用户部分的系统调用。它使用一个存储在文件系统中的文件初始化地址空间的用户部分。

                +

                我留意了一下是什么意思。网上搜索得,scause=12,说明这是一个instruction page fault,而这个缺页错误说明了什么?:

                +

                image-20230113012355740

                +

                这样,一切都明朗了。出现了scause=0x0c的意思就是说pc里的值不恰当,也就是说上面错误的方法都会跳转到错误的地方去。

                +

                Lab:xv6 lazy page allocation

                +

                参考:https://blog.csdn.net/m0_53157173/article/details/131349366

                -
                int
                exec(char *path, char **argv)
                {
                char *s, *last;
                int i, off;
                uint64 argc, sz = 0, sp, ustack[MAXARG], stackbase;
                struct elfhdr elf;
                struct inode *ip;
                struct proghdr ph;
                pagetable_t pagetable = 0, oldpagetable;
                struct proc *p = myproc();

                //开始打开文件的意思吧(
                begin_op();

                //ip是一个inode
                //打开路径为path的文件
                if((ip = namei(path)) == 0){
                end_op();
                return -1;
                }
                //暂时锁住文件,别人不许动
                ilock(ip);

                //之后应该就是把文件读入内存吧
                // Check ELF header
                if(readi(ip, 0, (uint64)&elf, 0, sizeof(elf)) != sizeof(elf))
                goto bad;

                if(elf.magic != ELF_MAGIC)
                goto bad;

                //分配新页表
                if((pagetable = proc_pagetable(p)) == 0)
                goto bad;

                //elfhd应该指的是可执行文件头
                // Load program into memory.
                for(i=0, off=elf.phoff; i<elf.phnum; i++, off+=sizeof(ph)){
                if(readi(ip, 0, (uint64)&ph, off, sizeof(ph)) != sizeof(ph))
                goto bad;
                if(ph.type != ELF_PROG_LOAD)
                continue;
                if(ph.memsz < ph.filesz)
                goto bad;
                if(ph.vaddr + ph.memsz < ph.vaddr)
                goto bad;
                if(ph.vaddr % PGSIZE != 0)
                goto bad;
                //总之顺利读到了
                uint64 sz1;
                //读到了就给它分配新空间并且填入页表
                if((sz1 = uvmalloc(pagetable, sz, ph.vaddr + ph.memsz, flags2perm(ph.flags))) == 0)
                goto bad;
                sz = sz1;
                if(loadseg(pagetable, ph.vaddr, ip, ph.off, ph.filesz) < 0)
                goto bad;
                }
                //在这里解锁
                iunlockput(ip);
                end_op();
                ip = 0;

                p = myproc();
                uint64 oldsz = p->sz;

                //读完文件,开始造一个新的用户栈【fork之后用户栈是不会清空的】
                // Allocate two pages at the next page boundary.
                // Make the first inaccessible as a stack guard.
                // Use the second as the user stack.
                sz = PGROUNDUP(sz);
                uint64 sz1;
                if((sz1 = uvmalloc(pagetable, sz, sz + 2*PGSIZE, PTE_W)) == 0)
                goto bad;
                sz = sz1;
                // mark a PTE invalid for user access.造guard page
                uvmclear(pagetable, sz-2*PGSIZE);
                // sp为栈顶
                sp = sz;
                // 应该指的是栈尾
                stackbase = sp - PGSIZE;

                // 开始往栈中填入执行参数
                // Push argument strings, prepare rest of stack in ustack.
                for(argc = 0; argv[argc]; argc++) {
                if(argc >= MAXARG)
                goto bad;
                sp -= strlen(argv[argc]) + 1;
                sp -= sp % 16; // riscv sp must be 16-byte aligned
                if(sp < stackbase)
                goto bad;
                //argv来自用户空间,所以需要使用copyout
                if(copyout(pagetable, sp, argv[argc], strlen(argv[argc]) + 1) < 0)
                goto bad;
                //这什么东西
                //exec一次将参数中的一个字符串复制到栈顶,并在ustack中记录指向它们的指针
                ustack[argc] = sp;
                }
                //放置空指针
                ustack[argc] = 0;

                // push the array of argv[] pointers.
                sp -= (argc+1) * sizeof(uint64);
                sp -= sp % 16;
                if(sp < stackbase)
                goto bad;
                if(copyout(pagetable, sp, (char *)ustack, (argc+1)*sizeof(uint64)) < 0)
                goto bad;

                // arguments to user main(argc, argv)
                // argc is returned via the system call return
                // value, which goes in a0.
                p->trapframe->a1 = sp;

                // Save program name for debugging.
                for(last=s=path; *s; s++)
                if(*s == '/')
                last = s+1;
                safestrcpy(p->name, last, sizeof(p->name));

                //只有成功了才会来到这,才会覆盖掉旧的内存镜像
                // Commit to the user image.
                oldpagetable = p->pagetable;
                p->pagetable = pagetable;
                p->sz = sz;
                p->trapframe->epc = elf.entry; // initial program counter = main
                p->trapframe->sp = sp; // initial stack pointer
                proc_freepagetable(oldpagetable, oldsz);

                return argc; // this ends up in a0, the first argument to main(argc, argv)

                bad:
                //释放新镜像,不改变旧镜像
                if(pagetable)
                proc_freepagetable(pagetable, sz);
                if(ip){
                iunlockput(ip);
                end_op();
                }
                return -1;
                }
                - -

                Real world

                image-20230110010651653

                -

                xv6内核缺少一个类似malloc可以为小对象提供内存的分配器,这使得内核无法使用需要动态分配的复杂数据结构。【确实,感觉一分配就是一页(】

                -

                内存分配是一个长期的热门话题,基本问题是有效使用有限的内存并为将来的未知请求做好准备。今天,人们更关心速度而不是空间效率。此外,一个更复杂的内核可能会分配许多不同大小的小块,而不是(如xv6中)只有4096字节的块;一个真正的内核分配器需要处理小分配和大分配。

                -

                Lab:Pagetable

                -

                In this lab you will explore page tables and modify them to to speed up certain system calls and to detect which pages have been accessed.

                +
                +

                来自书本:

                +

                Another widely-used feature is called lazy allocation, which has two parts:

                +
                  +
                1. First, when an application calls sbrk, the kernel grows the address space, but marks the new addresses as not valid in the page table.
                2. +
                3. Second, on a page fault on one of those new addresses, the kernel allocates physical memory and maps it into the page table.
                4. +
                +

                The kernel allocates memory only when the application actually uses it.

                -

                不过遗憾的是usertests还有好几个没通过,具体都标注了。

                -

                Speed up system calls

                -

                When each process is created, map one read-only page at USYSCALL (a VA defined in memlayout.h). At the start of this page, store a struct usyscall (also defined in memlayout.h), and initialize it to store the PID of the current process. For this lab, ugetpid() has been provided on the userspace side and will automatically use the USYSCALL mapping. You will receive full credit for this part of the lab if the ugetpid test case passes when running pgtbltest.

                -

                参考文章:MIT 6.S081 2021: Lab page tables

                +

                Eliminate allocation from sbrk()

                +

                Your first task is to delete page allocation from the sbrk(n).

                +

                The sbrk(n) system call grows the process’s memory size by n bytes, and then returns the start of the newly allocated region (i.e., the old size). Your new sbrk(n) should just increment the process’s size (myproc()->sz) by n and return the old size. It should not allocate memory – so you should delete the call to growproc() (but you still need to increase the process’s size!).

                -

                感想

                乌龙

                这里好像是因为实验改版了,我下的是2020年的实验包,在memlayout压根找不到USYSCALL和struct usyscall这俩东西。最后翻了下网上的总算找到了。

                -

                我一开始没找到,还以为USYSCALL以及usyscall这两个都得自己写在memlayout里面,想了很久都没想出来USYSCALL的值应该设置为多少。我认为只需满足两个条件即可:1.所处内存段应该是free memory那段,也即自kernel结束(PHYSTOP)到MAXVA这一大块。2.得确保能被用户和内核都能访问到。

                -

                前者意为虚拟地址在MAXVA和PHYSTOP之间,后者意为那段内存应该标记为PTE_U。这个范围是很宽泛的,我实在不知道要分配这期间的哪块内存,感觉也不大可能是真的自由度那么大。所以我就偷偷看了hints【悲】,想看它对这个USYSCALL应该写什么值有没有建议。结果发现这东西是实验给我们定的。遂去网上找到了它给的真正的USYSCALL值。

                -
                #define USYSCALL (TRAPFRAME - PGSIZE)

                struct usyscall{
                int pid;
                };
                +
                uint64
                sys_sbrk(void)
                {
                int addr;
                int n;

                if(argint(0, &n) < 0)
                return -1;
                int sz = myproc()->sz;
                addr = sz;
                myproc()->sz = sz + n;
                //if(growproc(n) < 0)
                // return -1;
                return addr;
                }
                -

                用户的ugetpid只找到了一个截图:

                -

                v2-0c2603da4c8102e46ae390a0d0b1191d_1440w

                -

                恕我愚钝实在不知道该把这段代码放在哪orz于是接下来写的东西就没有自测。

                -
                panic:freewalk leaf

                一开始写好代码准备启动xv6的时候爆出了这么一个panic,搜了一下得到如下解答:

                -
                -

                来源:MIT-6.S081-2020实验(xv6-riscv64)十:mmap

                -

                这时运行会发现freewalk函数panic:freewalk: leaf,这是因为freewalk希望所有虚拟地址已经被解绑并释放对应的物理空间了,该函数只负责释放页表。

                +

                Lazy allocation

                +

                Modify the code in trap.c to respond to a page fault from user space by mapping a newly-allocated page of physical memory at the faulting address, and then returning back to user space to let the process continue executing.

                +

                You should add your code just before the printf call that produced the “usertrap(): …” message. Modify whatever other xv6 kernel code you need to in order to get echo hi to work.

                -

                让我得知freewalk在vm.c下面【吐槽,我一开始还以为是自由自在地走(,看到这个才反应过来是free walk,跟页表有关的】。结合freewalk的代码

                -

                image-20230110225359361

                -

                可以知道,造成这个panic的原因是需要手动释放页表项。而在这里

                -
                // in proc.c  freeproc()
                if(p->usyscall)
                kfree((void*)p->usyscall);
                p->usyscall = 0;
                +

                感想

                思路

                首先,要知道缺页中断的scause为13或15.【论我怎么知道的:被以前的实验逼出来的hhh】然后,要写在if条件的第二个分支。在该分支内,我们需要先获取出问题的地方的虚拟地址的值,然后申请新的一页,再map到当前页表中。

                +
                一个难以察觉的错误
                描述

                思路是很简单的,就是有小细节需要格外注意。

                +

                trap.c在mappages时,一定不能直接传入va,必须传入PGROUNDDOWN(va)。如果直接传入va,会爆出如下错误:

                +

                image-20230116154004538

                +

                但是,查看mappages的代码:

                +
                int
                mappages(pagetable_t pagetable, uint64 va, uint64 size, uint64 pa, int perm)
                {
                uint64 a, last;
                pte_t *pte;

                a = PGROUNDDOWN(va);
                last = PGROUNDDOWN(va + size - 1);
                for(;;){
                if((pte = walk(pagetable, a, 1)) == 0)
                return -1;
                if(*pte & PTE_V)
                panic("remap");
                *pte = PA2PTE(pa) | perm | PTE_V;
                if(a == last)
                break;
                a += PGSIZE;
                pa += PGSIZE;
                }
                return 0;
                }
                -

                仅仅是释放掉了对应的物理页,页表项并没有被释放

                -

                对比了一下别人写的,才发现原来这里也需要修改:

                -
                // Free a process's page table, and free the
                // physical memory it refers to.
                void
                proc_freepagetable(pagetable_t pagetable, uint64 sz)
                {
                uvmunmap(pagetable, TRAMPOLINE, 1, 0);
                uvmunmap(pagetable, TRAPFRAME, 1, 0);
                //添加此句
                uvmunmap(pagetable, USYSCALL, 1, 0);
                uvmfree(pagetable, sz);
                }
                +

                我们可以看到,它在里面已经对va进行了处理了,使它变成了page-align的a变量。那么为什么,我们还要在外面再对va处理一次呢?

                +

                其实问题不是出在mappages中的a变量上,而是出现在mappages中的last变量上。比如,令va=PGSIZE+200,则a=PGSIZE,last=2*PGSIZE。这样一来,在下面的循环中,除了添加了刚刚申请的那页的映射以外,我们还多添加了新的一页,其物理地址为mem+PGSIZE。

                +

                这十分地危险!假设你要申请的va为proc->size的最后一页,那么,经过本次缺页中断之后,你事实上申请了两页,两页的地址分别为va和va+PGSIZE。而va+PGSIZE大于proc->size。也就是说,地址溢出了!

                +

                这会导致页表释放的时候出问题。以下是页表释放的路径。

                +
                // in proc.c
                void
                proc_freepagetable(pagetable_t pagetable, uint64 sz)
                {
                uvmunmap(pagetable, TRAMPOLINE, 1, 0);
                uvmunmap(pagetable, TRAPFRAME, 1, 0);
                uvmfree(pagetable, sz);
                }

                // in vm.c
                void
                uvmfree(pagetable_t pagetable, uint64 sz)
                {
                if(sz > 0)
                uvmunmap(pagetable, 0, PGROUNDUP(sz)/PGSIZE, 1);
                freewalk(pagetable);
                }

                // in vm.c
                void
                uvmunmap(pagetable_t pagetable, uint64 va, uint64 npages, int do_free)
                {
                uint64 a;
                pte_t *pte;

                if((va % PGSIZE) != 0)
                panic("uvmunmap: not aligned");

                for(a = va; a < va + npages*PGSIZE; a += PGSIZE){
                if((pte = walk(pagetable, a, 0)) == 0){
                printf("uvmunmap walk: %p\n",a);
                continue;
                }
                if((*pte & PTE_V) == 0){
                printf("uvmunmap: %p\n",a);
                continue;
                }
                printf("uvmunmap OK: %p\n",a);
                if(PTE_FLAGS(*pte) == PTE_V)
                panic("uvmunmap: not a leaf");
                if(do_free){
                uint64 pa = PTE2PA(*pte);
                kfree((void*)pa);
                }
                *pte = 0;
                }
                }
                -

                这样一来,问题就解决了。

                -
                总结

                因而,可以看到,如果进程想使用页的话,需要经历以下四步:

                -
                  -
                1. 通过kalloc获取物理页地址(可以通过该地址对页进行读写),并且记录在进程proc结构中(否则之后就获取不了了)
                2. -
                3. 建立mappages映射
                4. -
                5. 释放物理页
                6. -
                7. 释放PTE映射
                8. -
                -

                可见12和34都是分别一一对应的。

                -

                代码

                // Look in the process table for an UNUSED proc.
                // If found, initialize state required to run in the kernel,
                // and return with p->lock held.
                // If there are no free procs, or a memory allocation fails, return 0.
                static struct proc*
                allocproc(void)
                {
                struct proc *p;

                //有线程池那味了
                for(p = proc; p < &proc[NPROC]; p++) {
                acquire(&p->lock);
                if(p->state == UNUSED) {
                goto found;
                } else {
                release(&p->lock);
                }
                }
                return 0;

                found:
                p->pid = allocpid();

                // Allocate a trapframe page.
                if((p->trapframe = (struct trapframe *)kalloc()) == 0){
                release(&p->lock);
                return 0;
                }
                // Allocate a usyscall page.
                if((p->usyscall = (struct usyscall *)kalloc()) == 0){
                release(&p->lock);
                return 0;
                }
                //在USYSCALL写入usyscall结构体
                p->usyscall->pid = p->pid;

                // An empty user page table.
                p->pagetable = proc_pagetable(p);
                if(p->pagetable == 0){
                freeproc(p);
                release(&p->lock);
                return 0;
                }

                // Set up new context to start executing at forkret,
                // which returns to user space.
                memset(&p->context, 0, sizeof(p->context));
                p->context.ra = (uint64)forkret;
                p->context.sp = p->kstack + PGSIZE;

                return p;
                }

                // free a proc structure and the data hanging from it,
                // including user pages.
                // p->lock must be held.
                static void
                freeproc(struct proc *p)
                {
                if(p->trapframe)
                kfree((void*)p->trapframe);
                p->trapframe = 0;
                if(p->pagetable)
                proc_freepagetable(p->pagetable, p->sz);
                p->pagetable = 0;
                if(p->usyscall)
                kfree((void*)p->usyscall);
                p->usyscall = 0;
                p->sz = 0;
                p->pid = 0;
                p->parent = 0;
                p->name[0] = 0;
                p->chan = 0;
                p->killed = 0;
                p->xstate = 0;
                p->state = UNUSED;
                }

                // Create a user page table for a given process,
                // with no user memory, but with trampoline pages.
                pagetable_t
                proc_pagetable(struct proc *p)
                {
                pagetable_t pagetable;

                // An empty page table.
                pagetable = uvmcreate();
                if(pagetable == 0)
                return 0;

                // map the trampoline code (for system call return)
                // at the highest user virtual address.
                // only the supervisor uses it, on the way
                // to/from user space, so not PTE_U.
                if(mappages(pagetable, TRAMPOLINE, PGSIZE,
                (uint64)trampoline, PTE_R | PTE_X) < 0){
                uvmfree(pagetable, 0);
                return 0;
                }

                // map the trapframe just below TRAMPOLINE, for trampoline.S.
                if(mappages(pagetable, TRAPFRAME, PGSIZE,
                (uint64)(p->trapframe), PTE_R | PTE_W) < 0){
                uvmunmap(pagetable, TRAMPOLINE, 1, 0);
                uvmfree(pagetable, 0);
                return 0;
                }

                // 映射USYSCALL
                if(mappages(pagetable, USYSCALL, PGSIZE,
                (uint64)(p->usyscall), PTE_R|PTE_U) < 0){
                uvmunmap(pagetable, TRAMPOLINE, 1, 0);
                uvmunmap(pagetable, TRAPFRAME, 1, 0);
                uvmfree(pagetable, 0);
                return 0;
                }
                return pagetable;
                }

                // Free a process's page table, and free the
                // physical memory it refers to.
                void
                proc_freepagetable(pagetable_t pagetable, uint64 sz)
                {
                uvmunmap(pagetable, TRAMPOLINE, 1, 0);
                uvmunmap(pagetable, TRAPFRAME, 1, 0);
                uvmunmap(pagetable, USYSCALL, 1, 0);
                uvmfree(pagetable, sz);
                }
                +

                freewalk要求在uvmunmap中已经释放完所有的叶子结点。而由于uvmunmap中释放结点的va是从0递增到proc->size的,也因而,前面的那个大于proc->size的那页虽然还在页表中存在,但是不会被uvmunmap释放!这也就导致,接下来调用freewalk的时候,会发现该页的叶子结点仍然存在,从而导致freewalk: leaf

                +

                可以结合uvmunmap和trap.c中的调试语句看下图的过程,可以看到非常清晰明了,0x14000这一页并没有在uvmunmap中释放!

                +

                trap.c中的调试语句:

                +
                printf("trap: %p,+PGSIZE = %p\n",PGROUNDDOWN(va),PGROUNDDOWN(va)+PGSIZE);
                if(mappages(p->pagetable,va, PGSIZE, (uint64)mem, PTE_W|PTE_X|PTE_R|PTE_U) != 0){
                kfree(mem);
                }
                -

                问答题

                -

                Which other xv6 system call(s) could be made faster using this shared page? Explain how.

                -
                -

                我觉得如果能在fork的父子进程用shared page共享页表应该会节省很多时间和空间,用个读时写。其他的倒是想不到了。不过这题会不会问的是那些在内核态和用户态穿梭频繁的system call呢?这个的话我就想不出来了。

                -
                -

                write a function that prints the contents of a page table.

                -

                Define a function called vmprint().

                -

                It should take a pagetable_t argument, and print that pagetable in the format described below.

                -

                Insert if(p->pid==1) vmprint(p->pagetable) in exec.c just before the return argc, to print the first process’s page table.

                -

                image-20230110231020570

                -

                The first line displays the argument to vmprint. After that there is a line for each PTE, including PTEs that refer to page-table pages deeper in the tree. Each PTE line is indented by a number of " .." that indicates its depth in the tree.

                -

                Each PTE line shows the PTE index in its page-table page, the pte bits, and the physical address extracted from the PTE. Don’t print PTEs that are not valid.

                -

                In the above example, the top-level page-table page has mappings for entries 0 and 255. The next level down for entry 0 has only index 0 mapped, and the bottom-level for that index 0 has entries 0, 1, and 2 mapped.

                -
                -

                感想

                image-20230111000329475

                -

                很可惜,我在上面检索freewalk leaf到底是什么东西的时候,不小心看到了这题需要去参照freewalk这个提示【悲】其实我觉得这点还是需要绕点弯才能想到的,可能直接想到有点难【谁知道呢,世界线已经变动了】。

                -

                它这个打印页表其实最主要是考查如何遍历页表,这让人想起了walk这样的东西。但是walk是根据虚拟地址一级级找PTE的,中间很多地方会被跳过。有没有一个过程会在做事的时候遍历整个页表呢?答案是,这个过程就是释放页表的过程。释放页表才会一个个地看是否需要释放。释放页表的函数是freewalk,因而这道题参考freewalk的代码即可。

                -

                我觉得从“遍历页表”联想到“释放页表”这点是很巧的。不过也不会很突兀,毕竟学数据结构时就知道释放就需要遍历,逆向思维有点难但问题不大。

                -

                其他的就都挺简单的,不多赘述。

                -

                代码

                记得在defs.h中添加声明

                -
                //在vm.c下
                void
                vmprint_helper(pagetable_t pagetable,int level)
                {
                // there are 2^9 = 512 PTEs in a page table.
                for(int i = 0; i < 512; i++){
                pte_t pte = pagetable[i];
                if(pte & PTE_V){
                for(int j=0;j<level;j++){
                printf(" ..");
                }
                printf("%d: pte %p pa %p\n",i,(uint64)pte,(uint64)(PTE2PA(pte)));
                if((pte & (PTE_R|PTE_W|PTE_X)) == 0){
                // this PTE points to a lower-level page table.
                uint64 child = PTE2PA(pte);
                vmprint_helper((pagetable_t)child,level+1);
                }
                }
                }
                }

                // 打印页表
                void
                vmprint(pagetable_t pagetable)
                {
                // typedef uint64 *pagetable_t;所以pagetable可以以%p形式打印
                printf("page table %p\n",(uint64)pagetable);
                vmprint_helper(pagetable,1);
                }
                +

                图:

                +

                image-20230116165910356

                +
                debug过程

                看到freewalk: leaf这一错误,很容易联想到跟页表的释放有关。并且加上PGGROUNDDOWN就没问题,不加上才有问题,也很容易联想到跟mappages中多申请的那一页有关。但是具体是什么关系,这一点想要想到对我来说还是非常曲折的。

                +

                我一开始,以为是因为多申请的那一页(下面简称为B页好了)很有可能是其他进程在使用的,然后其他进程在echo进程释放页表前释放了页表,从而导致B页已经free了,这样一来uvmunmap说不定就能监测到对应物理页已经free,然后爆出panic。我一开始认为uvmunmap的这句话是用来监测物理页是否free的:

                +
                if((*pte & PTE_V) == 0){
                continue;
                //panic("uvmunmap: not mapped");
                }
                -

                问答题

                -

                Explain the output of vmprint in terms of Fig 3-4 from the text.

                -

                What does page 0 contain?

                -

                What is in page 2? When running in user mode, could the process read/write the memory mapped by page 1?

                -

                What does the third to last page contain?

                -
                -

                从上面操作系统的启动来看,进程1应该是在main.c中的userinit()中创建的进程,也是shell的父进程。【确实,经实践可得shell的pid为2】

                -

                可以来看一下userint的代码:

                -
                void
                userinit(void)
                {
                struct proc *p;

                p = allocproc();
                initproc = p;

                // 申请一页,将initcode的指令和数据放进去
                // allocate one user page and copy initcode's instructions
                // and data into it.
                /*
                uvminit的注释:
                // Load the user initcode into address 0 of pagetable,
                // for the very first process.
                // sz must be less than a page.
                */
                uvminit(p->pagetable, initcode, sizeof(initcode));
                p->sz = PGSIZE;

                //为内核态到用户态的转变做准备
                // prepare for the very first "return" from kernel to user.
                /*
                Trap Frame是指中断、自陷、异常进入内核后,在堆栈上形成的一种数据结构
                */
                p->trapframe->epc = 0; // user program counter
                p->trapframe->sp = PGSIZE; // user stack pointer

                // 修改进程名
                safestrcpy(p->name, "initcode", sizeof(p->name));
                p->cwd = namei("/");

                //这个也许是为了能被优先调度
                p->state = RUNNABLE;

                release(&p->lock);
                }
                +

                然后顺理成章地,这边条件==0成立,然后continue,然后直到freewalk才发现该pte未释放。

                +

                但是,我仔细过脑子想了想,发现,就算物理页已经free了,但是,*pte依然存在,PTE_V也依然为1,这个条件是不成立的。也就是说,B页不会continue,而是会继续下面的正常释放的流程。也就是说,B页是可以正常释放的,我们的“B页已经free导致uvmunmap释放失败”的推论是错误的。

                +

                但究竟是为什么呢?肯定跟B页有关系,但是又不是这种关系,这让我十分地苦恼且烦躁,于是我就去打了会儿游戏。边玩的时候突然注意到一件非常可疑的事情。

                +

                image-20230116154004538

                +

                这是发生错误时退出的截图。有一个点引起了我的注意,就是echo hi并没有打印hi在console上。也就是说,这个panic是在echo执行前产生的!那么这个执行前是在哪呢?答案就是在exec中!

                +
                // in exec.c
                // Commit to the user image.
                oldpagetable = p->pagetable;
                p->pagetable = pagetable;
                p->sz = sz;
                p->trapframe->epc = elf.entry; // initial program counter = main
                p->trapframe->sp = sp; // initial stack pointer
                // 这里!!!!!
                proc_freepagetable(oldpagetable, oldsz);
                -

                可见,page0是initcode的代码和数据,page1和page2用作了进程的栈,其中page1应该是guard page,page2是stack。

                -

                不过这里从exec的角度解释其实更通用

                -
                int
                exec(char *path, char **argv)
                {
                //分配新页表
                if((pagetable = proc_pagetable(p)) == 0)
                goto bad;

                //elfhd应该指的是可执行文件头
                // Load program into memory.
                for(i=0, off=elf.phoff; i<elf.phnum; i++, off+=sizeof(ph)){
                //...
                //总之顺利读到了
                uint64 sz1;
                //读到了就给它分配新空间并且填入页表
                if((sz1 = uvmalloc(pagetable, sz, ph.vaddr + ph.memsz, flags2perm(ph.flags))) == 0)
                goto bad;
                sz = sz1;
                }

                //读完文件,开始造一个新的用户栈【fork之后用户栈是不会清空的】
                sz = PGROUNDUP(sz);
                uint64 sz1;
                if((sz1 = uvmalloc(pagetable, sz, sz + 2*PGSIZE, PTE_W)) == 0)
                goto bad;
                sz = sz1;
                // mark a PTE invalid for user access.造guard page
                uvmclear(pagetable, sz-2*PGSIZE);
                // sp为栈顶
                sp = sz;
                // 应该指的是栈尾
                stackbase = sp - PGSIZE;
                //...
                }
                +

                可以看到,这里调用了proc_freetable,从而跟freewalk有了联系。

                +

                但是,如果我们还坚持是因为B页错的,就需要找到一个可能会产生B页的地方,也就是验证shell准备执行echo命令,fork出一个子进程之后,又在exec free页表前,已经调用过sbrk函数,并且已经触发过缺页中断。这个验证其实很简单,只需要找sbrk在哪被调用过,哪边使用过heap内存【也即哪边涉及了指针赋值】就行了。

                +

                通过全局搜索,可知sbrk在user/umalloc.c下的malloc()被使用过,而在user/sh.c中,fork子进程之后:

                +
                if(fork1() == 0)
                runcmd(parsecmd(buf));
                -

                page0就填程序。这里重点说明一下为什么page1和page2分别是guard page和stack。

                -

                按照它的那个算术关系,stack和guard page的虚拟内存位置关系应该是这样的:

                -

                image-20230111004330079

                -

                那为什么最后在页表中,变成了page1是gurad page,page2是stack这样上下颠倒了呢?看vm.c中的uvmalloc就能明白。

                -

                image-20230111004500827

                -

                在253行设置了新映射。可以看到,这里设置映射的顺序是sz->sz+PGSIZE,也即先设置guard page的映射,再设置stack的映射。所以,这两位才会上下颠倒了。

                -

                Detecting which pages have been accessed

                -

                Some garbage collectors (a form of automatic memory management) can benefit from information about which pages have been accessed (read or write). In this part of the lab, you will add a new feature to xv6 that detects and reports this information to userspace by inspecting the access bits in the RISC-V page table. The RISC-V hardware page walker marks these bits in the PTE whenever it resolves a TLB miss.

                -
                -
                -

                Your job is to implement pgaccess(), a system call that reports which pages have been accessed.

                -

                The system call takes three arguments. First, it takes the starting virtual address of the first user page to check. Second, it takes the number of pages to check. Finally, it takes a user address to a buffer to store the results into a bitmask (a datastructure that uses one bit per page and where the first page corresponds to the least significant bit).

                -

                You will receive full credit for this part of the lab if the pgaccess test case passes when running pgtbltest.

                -
                -

                感想

                实验内容:

                -

                实现void pgaccess(uint64 sva,int pgnum,int* bitmask);,一个系统调用。在这里面,我们要做的是,访问从svasva+pgnum*PGSIZE这一范围内的虚拟地址对应的PTE,然后查看PTE的标记项是否有PTE_A。有的话则在bitmask对应位标记为1.

                -

                应该注意的点:

                -

                1.需要进行内核态到用户态的参数传递 2.需要进行系统调用的必要步骤 3.PTE_A需要自己定义

                -

                以上是初见。做完了发现,确实就是那么简单,我主要时间花费在下的实验版本不对,折腾来折腾去了可能有一个小时,最后还是选择了直接把测试函数搬过来手工调用。已经换到正确的年份版本了【泪目】

                -

                有一点我忽视了,看了提示才知道:

                -
                -

                Be sure to clear PTE_A after checking if it is set. Otherwise, it won’t be possible to determine if the page was accessed since the last time pgaccess() was called (i.e., the bit will be set forever).

                +

                这其中的parsecmd()中malloc被使用过,并且发生了指针赋值!!也就是说,“是因为B页错的”这个结论是对的。

                +

                虽然这一段debug没有改变我们要证明“是B页在释放内存中出错的”的这个目的,但是确实带给了我很多这种执行时申请内存的知识,并且也让我突然想起了可以用printf debug。于是,我就去做了上面那个在trap.c中和uvmunmap中printf的调试语句,最终成功发现了结论。

                +

                实在是太艰苦了()这告诉我们以后千万千万要注意,是否需要用到PGGROUNDDOWN。

                +
                一个漏掉未考虑的细节

                摘自https://blog.csdn.net/m0_53157173/article/details/131349366

                +

                image-20231209011856305

                +

                image-20231209011932500

                +

                不过我以前好像是有考虑到这个的,但是我是这么做的:

                +

                image-20231209015238219

                +

                也就是相当于把它在addr parse的那段代码移进了walkaddr中。但是这样是不行的,查找可知argaddr的应用范围可比walkaddr广得多……

                +

                而为什么我下面的COW也是修改了walkaddr,而非修改argaddr,就可以达到同样的效果呢?这是因为cow只需对在内核中写用户页这种情况进行特殊处理,而这只有一个情况,也即只在copyout中发生。因而,我们只需修改walkaddr,就可以完全防范该情况了。

                +

                Lazytests and Usertests

                +

                We’ve supplied you with lazytests, an xv6 user program that tests some specific situations that may stress your lazy memory allocator. Modify your kernel code so that all of both lazytests and usertests pass.

                -

                也就是说每次检查到一个,就需要手动清除掉PTE_A标记。

                -

                还有一点以前一直没注意到的,头文件的引用需要注意次序。比如说要是把spinlock.h放在proc.h后面,就会寄得很彻底。

                -

                代码

                那些系统调用的登记步骤就先省略了。

                -
                // kernel/sysproc.c
                uint64
                sys_pgaccess(void)
                {
                uint64 sva;
                int pgnum;
                uint64 bitmask;

                if(argaddr(0,&sva) < 0 || argint(1, &pgnum) < 0 || argaddr(2, &bitmask) < 0)
                return -1;
                return pgaccess((void*)sva,pgnum,(void*)bitmask);
                }
                +

                感想

                一个绷不住的错误

                其实很简单,按照提示一步步做就行了。为什么我做得那么久那么崩溃呢?知道原因后我都笑嘻了。

                +

                在第一步修改sys_sbrk()的时候,我一下子没多想,使用了一句int sz = myproc()->sz,其实本来应该使用uint64的,使用int会溢出。这个伏笔就一直隐含到这里,然后大坑了我一笔。

                +

                一开始是发现lazytests的第二个,也就是oom过不去。我想了很久,也去网上找了别人的代码一步步对比下来看了,没有发现特别大的问题。于是我就在walk和sys_sbrk分别留下了调试信息:

                +
                // in walk()
                if(va >= MAXVA){
                printf("walk:va=%p,p->sz=%p,MAXVA=%p,pgva=%p\n",va,myproc()->sz,MAXVA,PGROUNDDOWN(va));
                panic("walk");
                }
                -
                // kernel/pgaccess.c
                #include "types.h"
                #include "param.h"
                #include "memlayout.h"
                #include "riscv.h"
                #include "spinlock.h"
                #include "defs.h"
                #include "proc.h"
                int
                pgaccess(void* sva,int pgnum,void* bitmask){
                if(pgnum > 32){
                printf("pgaccess: range too big.\n");
                exit(1);
                }
                int kmask = 0;
                struct proc* p = myproc();
                for(int i=0;i<pgnum;i++){
                pte_t* pte = walk(p->pagetable,(uint64)sva+i*PGSIZE,0);
                // 映射不存在,或者没有被访问过
                if(!pte || !(*pte & PTE_A)){
                continue;
                }
                kmask = (kmask | (1<<i));
                *pte = (*pte & (~PTE_A));
                }
                copyout(p->pagetable,(uint64)bitmask,(char*)(&kmask),sizeof(int));
                return 1;
                }
                +
                // in sys_sbrk()
                if(n >= 0){
                uint64 tmp = n + sz;
                if(tmp > MAXVA || n + sz < n) return -1;
                myproc()->sz = tmp;
                //printf("haha sb!sz=%p,n=%p\n",myproc()->sz,n);
                }
                -

                A kernel page table per process

                -

                The goal of this section and the next is to allow the kernel to directly dereference user pointers.

                +

                然后发现了这样的输出:

                +

                image-20230116225124363

                +

                可以看到,最后一次sz发生了数值溢出。

                +

                但是,此时我并没有悔改。我反而认为,“原本代码就是这么写的”。也就是说,我认为int sz是它原本内核代码给的。。。。。。在这样的情况下,我选择加上这样的条件判断:

                +
                if(tmp > MAXVA || ((tmp >> 31)& 1) == 1)       return -1;
                + +

                之后确实没有溢出了,但是test fail了。此时我想,为什么非要用int而不用uint64呢?一阵令人不寒而栗的预感袭来,我连忙去看了proc.h里的sz的定义,发现,sz原本就应该是uint64类型的,是我错辣【悲】

                +

                只能说起到一种很好的教训。主要是这种问题实在没有想过自己会犯

                +

                代码

                +
                  +
                • Handle the parent-to-child memory copy in fork() correctly.
                • +
                +
                // in vm.c uvmcopy()
                if((pte = walk(old, i, 0)) == 0)
                continue;
                //panic("uvmcopy: pte should exist");
                if((*pte & PTE_V) == 0)
                continue;
                //panic("uvmcopy: page not present");
                +
                -

                Your first job is to modify the kernel so that every process uses its own copy of the kernel page table when executing in the kernel.

                -

                Modify struct proc to maintain a kernel page table for each process, and modify the scheduler to switch kernel page tables when switching processes. For this step, each per-process kernel page table should be identical to the existing global kernel page table. You pass this part of the lab if usertests runs correctly.

                +
                  +
                • Handle negative sbrk() arguments.
                • +
                -

                感想

                这个其实平心而论不难,思路很简单。写着不难是不难,但想明白花费了我很多时间。

                -

                它这个要求我们修改kernel,使得每个进程都有一份自己的kernel page。至于要改什么,围绕着proc.c中,参照pagetable的生命周期摁改就行。还有一个地方它也提示了,就是要在swtch之前更换一下satp的值。

                -

                接下来,我说说我思考的几个点以及犯错的地方。

                -
                为什么要这么干

                看完题目,我的第一印象是,这么干有啥用。。。因为我觉得以前那个所有进程共用内核页表确实很好了,没有必要每个进程配一个后来才发现,这个跟下面那个是连在一起的,目的是 allow the kernel to directly dereference user pointers.。所以,我们下面会把用户的pgtbl和这里dump出来的kpgtbl合在一起。

                -

                具体来说:

                -

                通常,进行地址翻译的时候,计算机硬件(即内存管理单元MMU)都会自动的查找对应的映射进行翻译(需要设置satp寄存器,将需要使用的页表的地址交给该寄存器)。

                -

                然而,在xv6内核需要翻译用户的虚拟地址时,因为内核页表不含对应的映射,计算机硬件不能自动帮助完成这件事。因此,我们需要先找到用户程序的页表,仿照硬件翻译的流程,一步一步的找到对应的物理地址,再对其进行访问。walkaddr】这也就会导致copyin之类需要涉及内核和用户态交互的函数效率低下。

                -

                为了解决这个问题,我们尝试将用户页表也囊括进内核页表映射来。但是,如果将所有进程的用户页表都合并到同一个内核全局页表是不现实的。因而,我们决定换一个角度,让每个进程都仅有一张内核态和用户态共用的页表,每次切换进程时切换页表,这样就构造出了个全局的假象。

                -

                这两次实验就是为了实现该任务。在本次实验中,我们首先先实现内核页表的分离。

                -
                关于myproc()

                在allocproc中初始化的时候,我一开始是这么写的:

                -
                // in proc.c allocproc()
                perproc_kvminit();
                - -
                // in vm.c
                pagetable_t
                perproc_kvminit()
                {
                struct proc* p = myproc();
                p->kpgtbl = (pagetable_t) kalloc();
                memset(p->kpgtbl, 0, PGSIZE);

                // uart registers
                pkvmmap(p->kpgtbl,UART0, UART0, PGSIZE, PTE_R | PTE_W);
                // ...
                return pt;
                }
                +
                uint64
                sys_sbrk(void)
                {
                int addr;
                int n;

                if(argint(0, &n) < 0)
                return -1;
                uint64 sz = myproc()->sz;
                addr = sz;
                if(n >= 0){
                uint64 tmp = n + sz;
                if(tmp > MAXVA || n + sz < n) return -1;
                myproc()->sz = tmp;
                //printf("haha sb!sz=%p,n=%p\n",myproc()->sz,n);
                } else{
                if(n + sz > 0)
                myproc()->sz = uvmdealloc(myproc()->pagetable, sz, sz + n);
                else
                return -1;
                }
                //if(growproc(n) < 0)
                // return -1;
                return addr;
                }
                -

                这样会死得很惨,爆出如下panic:

                -

                image-20230114011100370

                -

                通过hints的调试贴士

                -

                A missing page table mapping will likely cause the kernel to encounter a page fault. It will print an error that includes sepc=0x00000000XXXXXXXX. You can find out where the fault occurred by searching for XXXXXXXX in kernel/kernel.asm.

                +
                  +
                • Kill a process if it page-faults on a virtual memory address higher than any allocated with sbrk().

                  +
                • +
                • Handle out-of-memory correctly: if kalloc() fails in the page fault handler, kill the current process.

                  +
                • +
                • Handle faults on the invalid page below the user stack.

                  +
                • +
                -

                我发现程序在这里绷掉了:

                -
                p->kpgtbl = (pagetable_t) kalloc();
                +
                else if(scause == 13 || scause == 15){
                // 缺页中断
                uint64 va = r_stval();
                if(va >= p->sz || va < PGROUNDUP(p->trapframe->sp) ||PGROUNDDOWN(va) >= MAXVA){
                //printf("va=%p stack=%p\n",va,PGROUNDUP(r_sp()));
                p->killed = 1;
                } else{
                char* mem = kalloc();
                if(mem != 0){
                memset(mem, 0, PGSIZE);
                //printf("trap: %d %p\n",p->pid,PGROUNDDOWN(va));
                if(mappages(p->pagetable,PGROUNDDOWN(va), PGSIZE, (uint64)mem, PTE_W|PTE_X|PTE_R|PTE_U) != 0){
                kfree(mem);
                p->killed = 1;
                }
                } else {
                p->killed = 1;
                }
                }
                }
                -

                而且显而易见,是系统启动时崩的。

                -

                经过了漫长的思考,我震惊地发现了它为什么崩了()

                -

                首先,这段代码语法上是没有问题的。它固然犯了发布未初始化完成的对象这样的并发错误【我有罪】,也破坏了proc的封装性【proc中的很多私有属性本来应该作用域仅在proc.c中的。此处为了能让vm.c访问到proc中的属性,不得不给vm.c添上了proc.h的头文件】,但是它并不是语法错误,还是能用的。我做了这样的测试样例证明它没有问题:

                -
                #include <stdio.h>
                #define MAX 10
                typedef int pagetable_t;

                struct proc{
                pagetable_t kpgtbl;
                };

                struct proc processes[MAX];

                struct proc* myproc(){
                return &processes[0];
                };

                void kvminit(){
                myproc()->kpgtbl = 1;
                }

                int main(){
                struct proc* p = &processes[0];
                kvminit();
                printf("%d",p->kpgtbl);
                return 0;
                }
                +
                +
                  +
                • Handle the case in which a process passes a valid address from sbrk() to a system call such as read or write, but the memory for that address has not yet been allocated.
                • +
                +

                我认为这里要是引起一个缺页中断可能会更酷,可能可以像lazytests里面这么做:

                +
                char *i, *prev_end, *new_end;

                prev_end = sbrk(REGION_SZ);
                new_end = prev_end + REGION_SZ;

                // 这里触发了多次缺页中断
                for (i = prev_end + PGSIZE; i < new_end; i += PGSIZE * PGSIZE)
                *(char **)i = i;
                -

                我一路顺着os启动的路径找,也想不出来这能有什么错,因而非常迷茫。

                -

                此时我灵光一闪,会不会是myproc()在os刚启动的时候是发挥不了作用的?于是我一路顺着myproc的代码看下去:

                -
                struct proc*
                myproc(void) {
                push_off();
                struct cpu *c = mycpu();
                struct proc *p = c->proc;
                pop_off();
                return p;
                }
                +

                之后有机会再试试233

                +

                【试了一下,发现是可以的。在COW fork的 感想—一些错误和思考—在内核态中引发并处理缺页中断 这部分内容中详细说明了具体要怎么做。】

                +
                +

                Lab: Copy-on-Write Fork for xv6

                +

                Parent and child can safely share phyical memory using copy-on-write fork, driven by page faults.

                +

                RISC-V has three different kinds of page fault: load page faults (when a load instruction cannot translate its virtual address), store page faults (when a store instruction cannot translate its virtual address), and instruction page faults (when the address for an instruction doesn’t translate).

                +

                The basic plan in COW fork is for the parent and child to initially share all physical pages, but to map them read-only. Thus, when the child or parent executes a store instruction, the RISC-V CPU raises a page-fault exception. In response to this exception, the kernel makes a copy of the page that contains the faulted address. It maps one copy read/write in the child’s address space and the other copy read/write in the parent’s address space. After updating the page tables, the kernel resumes the faulting process at the instruction that caused the fault.

                +

                PS【这个很重要】: COW fork() makes freeing of the physical pages that implement user memory a little trickier. A given physical page may be referred to by multiple processes’ page tables, and should be freed only when the last reference disappears.

                +
                +

                感想

                思路

                思路还是很直观的。

                +

                我们只需要在fork的时候,标记父子进程的所有PTE都为read-only,然后之后就会遇到不同scause的缺页中断,针对特定的scause,新建物理页面,拷贝物理页面,然后重新设置映射即可。而对于其提出的需要标记某页是否能够释放,则需要统计每页的ref数,当ref==1的时候才可以释放。

                +

                分析

                总分析

                其实可以把任务简单拆分为三部分。第一部分是实现基本的cow fork的逻辑,第二部分是引用计数释放内存,第三部分是解决copyin/copyout时在内核态发生的缺页中断。我认为本实验的难点事实上在第二部分【悲】我可能有大于3/4的时间都花在第二部分上了吧。

                +

                第一部分是实现cow fork的基本逻辑,也就是修改fork中对页表的拷贝以及在usertrap中添加对缺页中断的处理,这很直观,没什么好说的。

                +

                第三部分要么跟上面的lazy allocation一样,在kernel/vm.c walkaddr()中把缺页中断搬过去,要么向我在主要难点与错误—在内核态中引发并处理缺页中断这一部分那样做。

                +

                我们分析这一部分主要讲的是我认为最难的地方,也就是第二部分。其实第二部分的思路也很直观:创建一个数组,index为所有能用的内存的address/PGSIZE,用来记录引用数;然后在每次增加引用时,对应元素++;每次减少引用时,对应元素–。

                +

                虽然思路很简单很直观,但是实现起来非常地非常地非常地考验细节(我就非常不擅长这一点)。下面,我就先阐述一下第二部分的这个方法需要分割为哪几部分,其他我遇到的印象较深的bug和对一些地方的思考,都放在了下一部分,也即主要难点与错误

                +
                引用数实现分析
                +

                创建一个数组,index为所有能用的内存的address/PGSIZE,用来记录引用数;然后在每次增加引用时,对应元素++;每次减少引用时,对应元素–。

                +
                +
                数组的大小和数据类型

                kernel/kalloc.c中的kinit()

                +
                void
                kinit()
                {
                initlock(&kmem.lock, "kmem");
                // freerange用来把参数地址范围内的物理页加入freelist中
                // end是内核的结束地址
                freerange(end, (void*)PHYSTOP);
                }
                -

                那么,mycpu()获得的cpu的proc是怎么得到的呢?

                -

                我搜寻了一下os启动代码,发现了cpu的proc得到的路径。

                -
                void
                main()
                {
                if(cpuid() == 0){
                consoleinit();
                printfinit();
                printf("\n");
                printf("xv6 kernel is booting\n");
                printf("\n");
                //...很多很多init
                userinit(); // first user process
                __sync_synchronize();
                started = 1;
                } else {
                // ...
                }

                //调度执行第一个进程
                scheduler();
                }
                +

                可知,事实上,我们整个程序,包括用户和内核,能用的内存空间为0~PHYSTOP。因而,我们事实上只需要建一个PHYSTOP/PGSIZE这么大的数组就行。我算了一下大概是2^19次方。

                +

                然后,我感觉这种小系统应该不会有过多的对某一页的重复引用,因而,为了节省空间,我将数据类型定为了char。最好还是别定成uchar,因为这东西要是0–的话会溢出变为255,很可怕。

                +
                什么时候增减引用
                +

                我认为这里是非常考验细节和头脑清晰度的,也就是我卡了很久最后也没弄出来的部分【悲】

                +
                +

                可以分为三种情况来讲。我们的引用计数必须完美适应这三种情况:

                +
                  +
                1. 不经由页表,通过kalloc和kfree直接使用物理页

                  +

                  这就要求我们在kalloc的时候置引用数为1,然后kfree的时候对引用数先-1,再判断是否归零。

                  +
                2. +
                3. 经由页表,但与cow fork无关

                  +

                  增加页表项:mappages->kalloc,因而满足要求1即可。

                  +

                  删除页表项:uvmunmap。当do_free==1时,满足要求1即可。

                  +
                4. +
                5. 经由页表,与cow fork有关

                  +

                  copy父进程页表时:在cowcopy中,每增加一次子进程的映射,就需要增加一次引用数

                  +

                  在用户态/内核态发生缺页中断:发生缺页中断后,对原来物理页的引用数需要-1【我就是漏了这一点……】

                  +

                  删除页表项:uvmunmap。当do_free==0时,当对应页表项有COW标记,则减少引用数

                  +
                6. +
                +

                所以,我们需要在三个文件进行修改:

                +
                  +
                1. kalloc.c

                  +

                  增加数组定义,在kalloc和kfree中增加引用数修改

                  +
                2. +
                3. vm.c

                  +

                  在cowcopy和uvmunmap中增加引用数修改

                  +
                4. +
                5. trap.c

                  +

                  在usertrap的缺页中断中增加引用计数修改

                  +
                6. +
                +
                并发安全
                +

                这里我也没想到【悲】

                +
                +

                由于我们的pages数组会在多个文件、多个进程间使用,所以它必须在被锁保护的区域中被使用。

                +

                主要难点与错误

                scause=2

                image-20230117161404719

                +

                这个发生在我还没有实现第二部分的时候。搜索了一下,scause=2为Illegal instruction,而且sepc的这个1004的值也非常诡异。这应该是因为fork子进程释放了指令段内存,导致主进程执行错误

                +
                kernel无法启动

                在kinit中

                +
                void
                kinit()
                {
                initlock(&kmem.lock, "kmem");
                initlock(&pages_lock,"pages");
                memset(pages, 0, (2<<19));
                freerange(end, (void*)PHYSTOP);
                }
                -

                创建完进程后,就进入scheduler进行进程的调度:

                -
                void
                scheduler(void)
                {
                struct proc *p;
                struct cpu *c = mycpu();
                // ...
                int found = 0;
                for(p = proc; p < &proc[NPROC]; p++) {
                // ...
                //在这里!!!!
                c->proc = p;
                swtch(&c->context, &p->context);

                c->proc = 0;
                // ...
                +

                会通过freerange初始化freelist。在freerange中:

                +
                void
                freerange(void *pa_start, void *pa_end)
                {
                char *p;
                p = (char*)PGROUNDUP((uint64)pa_start);
                for(; p + PGSIZE <= (char*)pa_end; p += PGSIZE){
                kfree(p);
                }
                }
                -

                因而,c->proc是在创建进程的第一次调度后初始化的,也即,myproc只有在执行第一次scheduler之后才可以调用。而!!!

                -

                当执行调度前的userinit时:

                -
                void
                userinit(void)
                {
                struct proc *p;

                p = allocproc();
                initproc = p;
                +

                会对每一项进行一次kfree。因而,我们需要在kfree前先增加一次引用,要不然会寄。

                +
                在缺页中断时减少对物理页的引用数

                image-20230117213903706

                +

                注意此处不能直接让pages[pa/PGSIZE]–,一定要借助kfree。当此进程为引用pa的最后一个进程的时候,如果仅减少引用数,就会造成内存泄漏。kfree可以既减少引用数,又在适当的时候对物理页释放,可谓一举两得。kfree的这个双重作用思想也在uvmunmap中体现了。

                +
                在内核态中引发并处理缺页中断
                +

                Modify copyout() to use the same scheme as page faults when it encounters a COW page.

                +
                +

                我们所做的第一第二部分仅仅是完成了对来自用户态的缺页中断的完美处理,还尚未处理来自内核态的缺页中断。因而,这个修改copyin和copyout的点实际上就是要我们处理内核态的缺页中断。

                +

                这次实验跟上次的lazy allocation一样,都可以直接在walkaddr进行特殊处理,并且差不多要把usertrap的全部代码挪过来【具体见lazy allocation的代码】。不过,我想出了另一个流氓的方法(也就是说其实原理感觉是不大对233)。我选择直接在kernel引发一个访问用户页面的缺页中断,然后在kerneltrap中处理这个中断,就像usertrap一样。

                +

                但由于在walkaddr中发生的中断处于内核状态下,所以就进不了usertrap。我们应该在kerneltrap中再次添加和usertrap一样的中断处理。我们会像这样引发一个中断:

                +
                if((*pte & PTE_COW) != 0){
                if((*pte & PTE_W) == 0){
                *(char*)va = 1;// 此处不能用pa哦
                -

                它进行了allocproc。我们亲爱的allocproc接下来就会调用perproc_kvminit,然后perproc_kvminit中调用myproc。此时尚未进行初次调度,因而c->proc未初始化,myproc返回的是0,也即null。这样一来,myproc()->kpgtbl就发生了空指针异常,也即scause = 15——写入页错误。

                -

                因而,对于myproc()的调用需要慎之又慎。

                -
                系统调用

                系统调用时,是如何知道要用的是p中的内核页表而非global内核页表呢?

                -

                依然还是从os的启动说起。

                -

                在main.c中,kvminithart开启了页表,此时的页表为全局的内核页表:

                -
                // Switch h/w page table register to the kernel's page table,
                // and enable paging.
                void
                kvminithart()
                {
                w_satp(MAKE_SATP(kernel_pagetable));
                sfence_vma();
                }
                +

                然后在kerneltrap中这样处理:

                +
                if(r_scause() == 15){
                // 只要写入引起的缺页中断
                uint64 va = r_stval();
                pte_t *pte;
                uint64 pa;
                uint flags;

                if((pte = walk(p->pagetable, va, 0)) == 0)
                p->killed = 1;
                else if((*pte & PTE_V) == 0)
                p->killed = 1;
                else {
                sepc += 4;
                pa = PTE2PA(*pte);
                flags = PTE_FLAGS(*pte);

                char* mem;
                if((mem = kalloc())!=0){
                memmove(mem, (char*)pa, PGSIZE);
                // 设置为新的物理页地址
                *pte = PA2PTE(mem);
                kfree((void*)pa);
                // 设置新的flag,标记为可写
                flags = (flags | PTE_W | PTE_COW);
                *pte = ((*pte) | flags);
                } else{
                p->killed = 1;
                }
                }
                } else if((which_dev = devintr()) == 0){
                -

                当userinit被调度时,全局的内核页表被换成了proc中的内核页表:

                -
                // in proc.c scheduler()
                p->state = RUNNING;
                w_satp(MAKE_SATP(p->kpgtbl));
                sfence_vma();
                c->proc = p;
                swtch(&c->context, &p->context);
                +

                但是,这样做是不行的。

                +
                +

                会在这里卡住,会无限次不断进入kerneltrap。

                +

                image-20230117235028133

                +
                +

                造成这个的原因,经过一番曲折的debug之后,我发现,只要像usertrap中的syscall分支一样:

                +
                if(r_scause() == 8){
                // system call

                // 重点是这里
                // sepc points to the ecall instruction,
                // but we want to return to the next instruction.
                p->trapframe->epc += 4;
                // ...
                }
                -

                但是这样还没有结束。因为我们除了得更换目前的页表,还得更换trapframe中的内核页表相关的东西:

                -
                struct trapframe {
                /* 0 */ uint64 kernel_satp; // kernel page table
                /* 8 */ uint64 kernel_sp; // top of process's kernel stack
                }
                +

                加上这句话就行:

                +
                sepc += 4;
                -

                为啥还要更换trapframe中的呢?因为以后系统调用的时候,uservec是从这里读取值来作为内核栈和内核页表的来源的:

                -
                # in uservec
                # restore kernel stack pointer from p->trapframe->kernel_sp
                # 完成了内核栈的切换
                ld sp, 8(a0)

                # 完成了页表的切换
                # restore kernel page table from p->trapframe->kernel_satp
                ld t1, 0(a0)
                csrw satp, t1
                sfence.vma zero, zero
                +

                所以,结果就非常显而易见了,是因为一直卡在这句话执行不下去:

                +
                *(char*)va = 1;
                -

                所以,为了以后系统调用能顺利自发进行,我们需要把栈帧也一起换掉。怎么换呢?我们是否还要在一些地方人工把trapframe的值设置为我们自己的内核栈内核页表?答案是,不用!这些会由其他代码自动完成。

                -

                前面说到userinit的进程p被调度,satp换成了我们自己的内核页表。那么,在之后的内核态,satp都将保持我们自己的内核页表。当要返回用户态时,会执行如下代码:

                -
                // in usertrapret
                // 重置trapframe
                p->trapframe->kernel_satp = r_satp(); // kernel page table
                p->trapframe->kernel_sp = p->kstack + PGSIZE; // process's kernel stack
                +

                这是因为缺页中断会返回到原代码句中执行,所以就会继续回到这句话。而我们知道,此时正处于内核态,并没有开启地址映射,所以此处其实是非法地址越界了。但是我们的目的确实已经达到了(因为va通过stval寄存器被传递到了kerneltrap),所以我们这里只需跳过这句即可。

                +

                这也是为什么说我这个方法虽然实现了,但本质上其实非常流氓,不算是kerneltrap。

                +
                +

                Update:验收的时候跟学长说了一下这个点,学长表示不算流氓,反而在内核(至少内核赛)中算是一个比较通用的手法hh没想到还误打误撞上了

                +

                它带来的好处是,当地址不合法的时候可以减少开销。

                +

                具体来说,内核中一般会将地址空间分为多个vma,因而检查地址越界无需像xv6那样简单查页表,只需查地址是否在对应的vma中即可。所以,直接把这东西转到一个硬件的缺页中断中实现,事实上确实是减少了地址非法时的开销。

                +
                +

                除了这一点外,还有一点很重要的是,由于walkaddr是需要返回一个pa的,因而我们需要手动再把pa在缺页中断后更新一下:

                +
                pa = PTE2PA(*pte);
                if((*pte & PTE_COW) != 0){
                if((*pte & PTE_W) == 0){
                *(char*)va = 1;
                pa = PTE2PA(*pte);
                }
                }
                return pa;
                -

                satp内的值为我们自己的内核页表,而非全局页表。因而这样栈帧中的页表就会被自然而然地写入为进程的内核页表。之后返回用户态,以及之后之后的各种中断,就都会一直使用自己的内核页表了。【试了一下,这里如果改成非即时从satp读,而是默认的kernel_pagetable的话,会一直死循环】

                -

                不得不说,真是设计精妙啊!!!不过我觉得,要是这里写成kernel_pagetable,然后让我们自己改的话将是薄纱(。当然它应该也不会这么做,因为,kernel_pagetable事实上是不对外发布的。它这里这么写热读,最直接的原因还是因为读不到kernel_pagetable。这算是无心插柳柳成荫吗233

                -
                释放页表但不释放物理内存

                其实答案就在它给的proc_freepagetable里。

                -
                // Free a process's page table, and free the
                // physical memory it refers to.
                void
                proc_freepagetable(pagetable_t pagetable, uint64 sz)
                {
                uvmunmap(pagetable, TRAMPOLINE, 1, 0);
                uvmunmap(pagetable, TRAPFRAME, 1, 0);
                uvmfree(pagetable, sz);
                }
                +

                总之,做了这两个关键步骤后,也能启动了,也能过cowtest了。所以下面的代码也就贴上了这里的版本。

                +

                心得

                本次实验耗时经典五小时(包含笔记时间就是六个半小时了hhh),算是平均水平。很遗憾也很难受的一点是,我的错误最终还是没有自己想出来,而是参考了别人的代码才改对的。思路很简单,但是细节也依然非常多非常坑,还是得再加把劲。

                +

                代码

                我们只需要在fork的时候,标记父子进程的所有PTE都为read-only,然后之后就会遇到不同scause的缺页中断,针对特定的scause,新建物理页面,拷贝物理页面,然后重新设置映射即可。而对于其提出的需要标记某页是否能够释放,则需要统计每页的ref数,当ref==1的时候才可以释放。

                +

                定义COW标记

                // in kernel/riscv.h
                #define PTE_V (1L << 0)
                // ...
                #define PTE_COW (1L << 5)
                -

                uvmfree遍历页表,对每个存在的页表项,都试图找到其物理内存,并且释放物理内存和表项。如果页表项存在,但页表项对应的物理内存不存在,就会抛出freewalk leaf的异常。

                -

                uvmunmap会释放掉参数给的va的页表项,最后一个参数表示释放or不释放。

                -

                在这里,使用这两个的组合技,就可以达到不释放TRAMPOLINETRAPFRAME的物理内存,又不会让uvmfree出错的效果。

                -

                代码

                初始化

                初始化kpgtbl。由于现在内核栈存在各自的内核页表而非global内核页表中,所以在procinit中的对内核栈的初始化也得放在这:

                -
                // in proc.c allocproc()
                // An empty user page table.
                p->pagetable = proc_pagetable(p);
                if(p->pagetable == 0){
                freeproc(p);
                release(&p->lock);
                return 0;
                }

                p->kpgtbl = perproc_kvminit();

                char *pa = kalloc();
                if(pa == 0)
                panic("kalloc");
                uint64 va = KSTACK((int) (p - proc));
                pkvmmap(p->kpgtbl,va, (uint64)pa, PGSIZE, PTE_R | PTE_W);
                p->kstack = va;
                +

                引用数组初始化

                // in kernel/kalloc.c
                char pages[(2<<19)];
                struct spinlock pages_lock;

                void
                kinit()
                {
                initlock(&kmem.lock, "kmem");
                initlock(&pages_lock,"pages");
                memset(pages, 0, (2<<19));
                freerange(end, (void*)PHYSTOP);
                }
                void
                freerange(void *pa_start, void *pa_end)
                {
                char *p;
                p = (char*)PGROUNDUP((uint64)pa_start);
                for(; p + PGSIZE <= (char*)pa_end; p += PGSIZE){
                pages[(uint64)p/PGSIZE] = 1;
                kfree(p);
                }
                }
                -
                // in vm.c
                pagetable_t
                perproc_kvminit()
                {
                pagetable_t pt = (pagetable_t) kalloc();
                memset(pt, 0, PGSIZE);

                // uart registers
                pkvmmap(pt,UART0, UART0, PGSIZE, PTE_R | PTE_W);

                // virtio mmio disk interface
                pkvmmap(pt,VIRTIO0, VIRTIO0, PGSIZE, PTE_R | PTE_W);

                // CLINT
                pkvmmap(pt,CLINT, CLINT, 0x10000, PTE_R | PTE_W);

                // PLIC
                pkvmmap(pt,PLIC, PLIC, 0x400000, PTE_R | PTE_W);

                // map kernel text executable and read-only.
                pkvmmap(pt,KERNBASE, KERNBASE, (uint64)etext-KERNBASE, PTE_R | PTE_X);

                // map kernel data and the physical RAM we'll make use of.
                pkvmmap(pt,(uint64)etext, (uint64)etext, PHYSTOP-(uint64)etext, PTE_R | PTE_W);

                // map the trampoline for trap entry/exit to
                // the highest virtual address in the kernel.
                pkvmmap(pt,TRAMPOLINE, (uint64)trampoline, PGSIZE, PTE_R | PTE_X);
                return pt;
                }
                +

                申请和释放页时增删引用

                // in kernel/kalloc.c
                void *
                kalloc(void)
                {
                struct run *r;

                acquire(&kmem.lock);
                r = kmem.freelist;
                if(r)
                kmem.freelist = r->next;
                release(&kmem.lock);
                if(r){
                acquire(&pages_lock);
                // 在这
                pages[(uint64)r/PGSIZE] = 1;
                release(&pages_lock);
                }
                if(r)
                memset((char*)r, 5, PGSIZE); // fill with junk
                return (void*)r;
                }
                void
                kfree(void *pa)
                {
                struct run *r;

                acquire(&pages_lock);
                if(((uint64)pa % PGSIZE) != 0 || (char*)pa < end || (uint64)pa >= PHYSTOP || pages[(uint64)pa/PGSIZE] <= 0 )
                panic("kfree");
                // 每次kfree都会减少引用
                pages[(uint64)pa/PGSIZE]--;
                // 说明此时页面还被其他东西引用着,不能释放
                if(pages[((uint64)pa)/PGSIZE] > 0){
                release(&pages_lock);
                return;
                }
                release(&pages_lock);

                // Fill with junk to catch dangling refs.
                memset(pa, 1, PGSIZE);
                r = (struct run*)pa;

                acquire(&kmem.lock);
                r->next = kmem.freelist;
                kmem.freelist = r;
                release(&kmem.lock);
                }
                -
                // in vm.c
                void
                pkvmmap(pagetable_t pgtbl,uint64 va, uint64 pa, uint64 sz, int perm)
                {
                // 当第一个进程开始时,mycpu->proc = null,所以这里不能调用myproc
                if(mappages(pgtbl, va, sz, pa, perm) != 0)
                panic("kvmmap");
                }
                +

                修改fork时对页表的复制操作,并标记引用数增加

                // in kernel/proc.c fork()
                // Copy user memory from parent to child.
                if(cowcopy(p->pagetable, np->pagetable, p->sz) < 0){
                freeproc(np);
                release(&np->lock);
                return -1;
                }
                // in kernel/vm.c
                int
                cowcopy(pagetable_t old, pagetable_t new, uint64 sz)
                {
                pte_t *pte;
                uint64 pa, i;
                uint flags;

                for(i = 0; i < sz; i += PGSIZE){
                if((pte = walk(old, i, 0)) == 0)
                panic("cowcopy: pte should exist");
                if((*pte & PTE_V) == 0)
                panic("cowcopy: page not present");
                pa = PTE2PA(*pte);
                flags = PTE_FLAGS(*pte);
                // 去除flag中的PTE_W,并且给父子的都安上没有PTE_W的flag
                flags = (flags & (~PTE_W));
                flags = (flags | PTE_COW);
                *pte = ((*pte) & (~PTE_W));
                *pte = ((*pte) | PTE_COW);
                if(mappages(new, i, PGSIZE, (uint64)pa, flags) != 0){
                goto err;
                }
                // 标记物理页的引用数增加
                acquire(&pages_lock);
                pages[pa/PGSIZE]++;
                release(&pages_lock);
                }
                return 0;
                err:
                // 失败了不能释放物理内存
                uvmunmap(new, 0, i / PGSIZE, 0);
                return -1;
                }
                -
                swtch时切换页表
                // in proc.c scheduler()
                p->state = RUNNING;
                w_satp(MAKE_SATP(p->kpgtbl));
                sfence_vma();
                c->proc = p;
                swtch(&c->context, &p->context);

                //...

                #if !defined (LAB_FS)
                if(found == 0) {
                // 没有进程运行时使用全局kernel_pagetable
                kvminithart();
                intr_on();
                asm volatile("wfi");
                }
                +

                处理缺页中断,标记引用数减少

                } else if(r_scause() == 15){
                // 只要求写入引起的缺页中断
                uint64 va = r_stval();
                pte_t *pte;
                uint64 pa;
                uint flags;

                if((pte = walk(p->pagetable, va, 0)) == 0)
                p->killed = 1;
                else if((*pte & PTE_V) == 0)
                p->killed = 1;
                else {
                pa = PTE2PA(*pte);
                flags = PTE_FLAGS(*pte);

                char* mem;
                if((mem = kalloc())!=0){
                memmove(mem, (char*)pa, PGSIZE);
                // 设置为新的物理页地址
                *pte = PA2PTE(mem);
                // 减少引用,引用归零时释放
                kfree((void*)pa);
                // 设置新的flag,标记为可写
                flags = (flags | PTE_W | PTE_COW);
                *pte = ((*pte) | flags);
                } else{
                p->killed = 1;
                }
                }
                }
                -
                修改kvmpa
                #include "spinlock.h"
                #include "proc.h"

                uint64
                kvmpa(uint64 va)
                {
                uint64 off = va % PGSIZE;
                pte_t *pte;
                uint64 pa;

                pte = walk(myproc()->kpgtbl, va, 0);
                if(pte == 0)
                panic("kvmpa");
                if((*pte & PTE_V) == 0)
                panic("kvmpa");
                pa = PTE2PA(*pte);
                return pa+off;
                }
                +

                uvmunmap时减少引用数

                void
                uvmunmap(pagetable_t pagetable, uint64 va, uint64 npages, int do_free)
                {
                uint64 a;
                pte_t *pte;
                // ...

                for(a = va; a < va + npages*PGSIZE; a += PGSIZE){
                // ...
                uint64 pa = PTE2PA(*pte);
                if(do_free){
                kfree((void*)pa);
                }else {
                acquire(&pages_lock);
                if(((*pte) & PTE_COW) != 0){
                pages[pa/PGSIZE]--;
                }
                release(&pages_lock);
                }
                *pte = 0;
                }
                }
                -
                释放
                // in kernel.proc.c freeproc()
                if(p->kpgtbl)
                proc_freekpgtbl(p->kpgtbl,p->kstack);
                p->kpgtbl = 0;
                +

                修改walkaddr

                在walkaddr中触发缺页中断
                uint64
                walkaddr(pagetable_t pagetable, uint64 va)
                {
                pte_t *pte;
                uint64 pa;

                if(va >= MAXVA)
                return 0;

                pte = walk(pagetable, va, 0);
                if(pte == 0)
                return 0;
                if((*pte & PTE_V) == 0)
                return 0;
                if((*pte & PTE_U) == 0)
                return 0;
                pa = PTE2PA(*pte);
                // 在这里
                if((*pte & PTE_COW) != 0){
                if((*pte & PTE_W) == 0){
                // 触发缺页中断
                *(char*)va = 1;
                // 更新pa值
                pa = PTE2PA(*pte);
                }
                }
                return pa;
                }
                -
                extern char etext[];  // kernel.ld sets this to end of kernel code.

                void
                proc_freekpgtbl(pagetable_t pagetable,uint64 stack )
                {
                uvmunmap(pagetable, UART0, 1, 0);
                uvmunmap(pagetable, VIRTIO0, 1, 0);
                uvmunmap(pagetable, CLINT, 0x10000/(uint64)PGSIZE, 0);
                uvmunmap(pagetable, PLIC, 0X400000/(uint64)PGSIZE, 0);
                uvmunmap(pagetable, KERNBASE, (uint64)((uint64)etext-KERNBASE)/PGSIZE, 0);
                uvmunmap(pagetable, (uint64)etext,(PHYSTOP-(uint64)etext)/PGSIZE, 0);
                //kvmmap(KERNBASE, KERNBASE, (uint64)etext-KERNBASE, PTE_R | PTE_X);
                uvmunmap(pagetable, TRAMPOLINE, 1, 0);
                uvmunmap(pagetable, stack, 1,1 );
                uvmfree(pagetable, 0);
                }
                +
                在kerneltrap内补上对缺页中断的处理
                void
                kerneltrap()
                {
                uint64 sepc = r_sepc();
                // ...
                if(r_scause() == 15){
                // 只要写入引起的缺页中断
                uint64 va = r_stval();
                pte_t *pte;
                uint64 pa;
                uint flags;

                if((pte = walk(p->pagetable, va, 0)) == 0)
                p->killed = 1;
                else if((*pte & PTE_V) == 0)
                p->killed = 1;
                else {
                // 注意,这个很重要!!!!!
                sepc += 4;
                pa = PTE2PA(*pte);
                flags = PTE_FLAGS(*pte);

                char* mem;
                if((mem = kalloc())!=0){
                memmove(mem, (char*)pa, PGSIZE);
                // 设置为新的物理页地址
                *pte = PA2PTE(mem);
                kfree((void*)pa);
                // 设置新的flag,标记为可写
                flags = (flags | PTE_W | PTE_COW);
                *pte = ((*pte) | flags);
                } else{
                p->killed = 1;
                }
                }
                } else if((which_dev = devintr()) == 0){
                printf("scause %p\n", scause);
                printf("sepc=%p stval=%p\n", r_sepc(), r_stval());
                panic("kerneltrap");
                }
                // ...
                }
                -

                Simplify copyin/copyinstr

                -

                参考:

                -

                6.S081学习记录-lab3

                -
                +]]> + + + Page tables + /2023/01/10/xv6$chap3/ + Page tables

                Paging hardware

                为什么需要页表

                将主存储器以及各种外设接口卡里面内置的存储器连接起来,就形成了内存地址空间。内存地址空间中的地址是真实的物理地址。RISC-V架构的指令使用的地址是虚拟地址。为了通过指令中的虚拟地址访问到真实的物理内存,需要进行从虚拟地址到物理地址的转换。从虚拟地址到物理地址的转换,就需要通过页表来实现。

                +

                页表如何运作

                在RISC-V指令集中,当我们需要开启页表服务时,我们需要将我们预先配置好的页表首地址放入 satp 寄存器中。从此之后, 计算机硬件 将把访存的地址 均视为虚拟地址 ,都需要通过硬件查询页表,将其 翻译成为物理地址 ,然后将其作为地址发送给内存进行访存。

                +

                xv6采用的指令集标准为RISC-V标准,其中页表的标准为SV39标准,也就是虚拟地址最多为39位。

                +

                虚实地址翻译流程:

                +
                  +
                1. 获得一个虚拟地址。根页表基地址已经被装填至寄存器 satp 中。
                2. +
                3. 通过 satp 找到根页表的物理页帧号,转成物理地址(Offset为0),通过虚拟地址的L2索引,找到对应的页表项。
                4. +
                5. 通过页表项可以找到找到 次页表 的物理页帧号,转成物理地址(Offset为0),通过虚拟地址的L1索引,找到对应的页表项。
                6. +
                7. 通过页表项可以找到找到 叶子页表 的物理页帧号,转成物理地址(Offset为0),通过虚拟地址的L0索引,找到对应的页表项。
                8. +
                9. 通过页表项可以找到找到 物理地址 的物理页帧号,通过虚拟地址的Offset,转成物理地址(Offset和虚拟地址Offset相同)。
                10. +
                +

                页表组成

                页表项

                页表由页表项PTE(Page Table Entries)构成,每个页表项由44位的PPN(Physical Page Number)和一些参数flag组成。

                +

                image-20230109153937459

                -

                The kernel’s copyin function reads memory pointed to by user pointers. It does this by translating them to physical addresses, which the kernel can directly dereference. It performs this translation by walking the process page-table in software. Your job in this part of the lab is to add user mappings to each process’s kernel page table (created in the previous section) that allow copyin (and the related string function copyinstr) to directly dereference user pointers.

                +

                Each PTE contains flflag bits that tell the paging hardware how the associated virtual address is allowed to be used. PTE_V indicates whether the PTE is present: if it is not set, a reference to the page causes an exception (i.e. is not allowed). PTE_R controls whether instructions are allowed to read to the page. PTE_W controls whether instructions are allowed to write to the page. PTE_X controls whether the CPU may interpret the content of the page as instructions and execute them. PTE_U controls whether instructions in user mode are allowed to access the page; if PTE_U is not set, the PTE can be used only in supervisor mode.

                +

                这个表项的几个参数定义在kernel/riscv.h中的341行左右。

                +

                虚拟地址有64bit,其中25bits未使用,39bits包含了27位的PTE索引号以及12位的offset。

                +

                物理地址有56位,由PPN和offset拼接组成。

                +

                单页表和多级页表

                以单页表为例,物理地址形成过程如下图所示。

                +

                image

                +

                每个页表项PTE索引着一页。因而,每一页的大小为2^12=4096B。单页表中PTE的索引号有2^27个,因而单页表中表项有134217728个,即可以代表134217728页。页表实际上也是以页的形式存储的。因而单页表需要的存储空间为(2^27x7)/2^12=2^15x7=229376页。

                +

                RISC-V架构中真实情况是会有三级页表。三级页表结构相比于单级页表结构,会占据更多的物理存储空间

                +

                image-20230109151346780

                +

                每个页表项PTE索引着一页,这一页可能代表着另一个页表,也可能代表着内存中需要的指令和数据。因而,每一页的大小为2^12=4096B。三页表中,一级页表中PTE的索引号有512个,可以代表的物理内存页数有512x515x512=2^27页,即可以代表134217728页。页表实际上也是以页的形式存储的,一个页表有2^9x7个字节,可以存储在1页中。因而三页表需要的存储空间为1+2^9+2^18 = 262657页。

                +

                三级页表结构相比于单级页表结构,可以节省更多内存空间

                -

                Replace the body of copyin in kernel/vm.c with a call to copyin_new (defined in kernel/vmcopyin.c); do the same for copyinstr and copyinstr_new. Add mappings for user addresses to each process’s kernel page table so that copyin_new and copyinstr_new work.

                +

                参考:页表是啥以及为啥多级页表能够节省空间

                -

                感想

                这题很直观的思路是,在每个user pagetable添加映射的地方也添加kpgtbl的映射。但问题是,“每个user pagetable添加映射的地方”都是哪?

                -
                误入幻想

                我一开始想着偷偷懒,直接在proc.c和vm.c中每个操纵pagetable的地方都加上对kpgtbl的操纵。但很快我就给搞晕了。这时候,我心中萌生一计【PS:下面说的最后都没成功】:我直接快进到把proc结构中的pagetable属性给删了,然后每个出现p->pagetable的地方,都用p->kpgtbl代替,直接让两表合为一表,然后之后make的时候哪里报错改哪里,这不就一劳永逸地把所有出现pagetable的地方都改为kpgtbl了嘛。我振奋地去试了一下,将所有地方出现的pagetable都替换成了kpgtbl,把proc.c中的proc_pagetable()proc_freepagetable()的出现的地方都换成了perproc_kvminit()以及proc_freekpgtbl(),还做了一个小细节,就是在userinit中调用的uvminit中,我把这样:

                -
                void
                uvminit(pagetable_t pagetable, uchar *src, uint sz)
                {
                char *mem;

                if(sz >= PGSIZE)
                panic("inituvm: more than a page");
                mem = kalloc();
                memset(mem, 0, PGSIZE);
                mappages(pagetable, 0, PGSIZE, (uint64)mem, PTE_W|PTE_R|PTE_X|PTE_U);
                memmove(mem, src, sz);
                }
                - -

                换成了这样:

                -
                void
                uvminit(struct proc* p, uchar *src, uint sz)
                {
                char *mem;

                if(sz >= PGSIZE)
                panic("inituvm: more than a page");
                mem = kalloc();
                memset(mem, 0, PGSIZE);
                mappages(p->kpgtbl, 0, PGSIZE, (uint64)mem, PTE_W|PTE_R|PTE_X|PTE_U);
                memmove(mem, src, sz);
                }
                - -

                最后,在启动的时候,卡在了初次调度切换不到initcode这边,没有调用exec。没有panic,似乎只在死循环。我也实在想不出是什么原因,最后把代码删了【悲】想想我应该用git保存一下改前改后的。这下实在是难受了,我的想法也暂时没有机会实践了。等到明年大三说不定还得再交一次这玩意,到时候再探究探究吧hhh

                -
                走上正途

                发现这个最后没成还改了半天的我最后非常沮丧地去看了hints【又一心浮气躁耐心不足的表现,但确实绷不住了】,发现它居然说只用修改三个地方:fork、exec以及sbrk。

                -

                我把kernel/下的每个文件都搜了一遍,发现确实,只有这三个,以及proc.c,vm.c,涉及到对页表项的增删。而在用户态中,想要对进程的内存进行管理,似乎只能通过系统调用sbrk。而proc.c和vm.c中确实没什么好改的。因为里面增加的映射,都是trapframe、trampoline、inicode这种不会一般在copyin中用到的虚拟地址。所以,要改的地方,确确实实,只有fork、exec以及sbrk

                +

                考虑到这样一个进程:

                +

                watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2Z1eXVhbmRl,size_16,color_FFFFFF,t_70

                +

                进程使用页表时,需要将整个页表读入内存。

                +

                如果使用单级页表,尽管一个进程仅使用到页表中的某两项,也需要把整个页表都读入内存,光是页表就占据了2^15x7x4k/2^20 约为1G的内存空间。

                +

                如果使用三级页表,一个进程需要用到某两页。假设这两页存储在不同的二级页表中,则只需要读入1+2+2=5页 约为20K的内存空间。

                +

                两者相对比,显然用三级页表比单级页表顶多了。三级页表相较于一级页表,多用了13%的物理空间,却可以节省99.998%的空间。

                +

                页表使用

                每个进程会保留自己的一份用户级别的页表地址。当轮到自己使用CPU时,会将CPU的satp寄存器更换为自己的页表地址。

                +

                Kernel address space

                介绍了xv6中内核的页表结构。

                -

                Xv6 applications ask the kernel for heap memory using the sbrk() system call.

                +

                这里为了方便,就把三级页表省略了,只留下va和pa的对比

                -

                很悲伤,我的初见思路是错误的()

                -

                而这三个地方的共同点,就是都会对页表进行大量的copy。

                -
                //in proc.c fork()
                // Copy user memory from parent to child.
                if(uvmcopy(p->pagetable, np->pagetable, p->sz) < 0){
                freeproc(np);
                release(&np->lock);
                return -1;
                }
                - -
                //in exec.c
                // Commit to the user image.
                oldpagetable = p->pagetable;
                p->pagetable = pagetable;
                - -
                //in syscall.c
                uint64
                sys_sbrk(void)
                {
                int addr;
                int n;

                if(argint(0, &n) < 0)
                return -1;
                addr = myproc()->sz;
                if(growproc(n) < 0)
                return -1;
                return addr;
                }
                //in proc.c growproc()
                uvmalloc(p->pagetable, sz, sz + n)) == 0
                +

                每个进程都有一个用户级别的页表。xv6给内核提供了一个单独的内核地址空间的页表。其层级映射关系如下:

                +

                p3

                +

                在kernel/memlayout.h中正记录了这些参数:

                +
                // Physical memory layout

                // qemu -machine virt is set up like this,
                // based on qemu's hw/riscv/virt.c:
                //
                // 00001000 -- boot ROM, provided by qemu
                // 02000000 -- CLINT
                // 0C000000 -- PLIC
                // 10000000 -- uart0
                // 10001000 -- virtio disk
                // 80000000 -- boot ROM jumps here in machine mode
                // -kernel loads the kernel here
                // unused RAM after 80000000.

                // the kernel uses physical memory thus:
                // 80000000 -- entry.S, then kernel text and data
                // end -- start of kernel page allocation area
                // PHYSTOP -- end RAM used by the kernel

                // qemu puts UART registers here in physical memory.
                #define UART0 0x10000000L
                #define UART0_IRQ 10

                // virtio mmio interface
                #define VIRTIO0 0x10001000
                #define VIRTIO0_IRQ 1

                // core local interruptor (CLINT), which contains the timer.
                // ...
                -

                所以,我们要做的事情很简单:写一个坐收渔翁之利的函数,内容为把一个页表的所有内容复制到另一个页表。然后再在这几个地方调用这个函数即可。

                -

                代码

                -

                注意:由于我写得实在是太烦了,已经思考不下去了。为了放过我自己,我写了个虽然能过得去测试但是其实毛病重重的代码。垃圾点为以下几点:

                +

                由图可知,一直从0x0到0x86400000,都是采取的直接映射的方式,虚拟地址=物理地址,这段是内核使用的空间。在0x0-0x800000000阶段,物理地址代表着各种IO设备的存储器。

                +

                但是注意,在0x86400000(PHYSTOP)以上的地址都不是直接映射,这些非直接映射的层级包含两类:

                  -
                1. 需要去掉freewalk中的panic

                  -

                  我的kvmcopy的实现是,user pagetable(下面简称up)和tp的相同虚拟地址共用同一页物理内存。也就是说,页表不一样,但所指向的物理内存是同一个。这样设计的目的是为了能够让tp及时用到up的更新后的数据。

                  -

                  这会导致啥呢?在进程释放时,需要一起调用proc_freepagetableproc_freekpgtblproc_freepagetable调用完后,所指向的那堆物理内存已经寄完了,如果再调用proc_freekpgtbl,显然,就会发生页表未释放但页表对应内存已经释放的问题,freewalk就会panic。因此,我简单粗暴地直接把freewalk的panic删掉了【抖】也许有别的解决方法,但我真是烦得不想想了放过我吧(

                  +
                2. trampoline

                  +
                  +

                  It is mapped at the top of the virtual address space; user page tables have this same mapping.

                  +
                  +

                  它有一点很特殊的是,它实际对应的物理内存是0x80000000开始的一段。也就是说,0x80000000开始的这段内存,既被直接映射了,也被trampoline通过虚拟地址映射了。它被映射了两次。

                3. -
                4. 好像暂时没有第二点了()

                  +
                5. 内核栈

                  +
                  +

                  Each process has its own kernel stack, which is mapped high so that below it xv6 can leave an unmapped guard page. The guard page’s PTE is invalid (i.e., PTE_V is not set), so that if the kernel overflflows a kernel stack, it will likely cause an exception and the kernel will panic.

                  +

                  guard page可以用来防止内核栈溢出。

                  +
                -
                -
                渔翁之利函数
                // in vm.c
                // 效仿的是vm.c中的uvmcopy
                int
                kvmcopy(pagetable_t up, pagetable_t kp, uint64 sz)
                {
                pte_t *pte;
                uint64 pa, i;
                uint flags;

                for(i = 0; i < sz; i += PGSIZE){
                if((pte = walk(up, i, 0)) == 0 || (*pte & PTE_V) == 0){
                if(walk(kp,i,0) == 0){
                //如果up不存在此项,kp存在,就直接删了
                uvmunmap(kp,i,PGSIZE,0);
                }
                continue;
                }
                pa = PTE2PA(*pte);
                flags = PTE_FLAGS(*pte);
                // 注意去除PTE_U,否则内核态无法访问
                flags = (flags & (~PTE_U));
                if(mappages(kp, i, PGSIZE, pa, flags) != 0){
                goto err;
                }
                }
                return 0;

                err:
                uvmunmap(kp, 0, i / PGSIZE, 1);
                return -1;
                }
                - -
                修改fork、exec、sbrk
                fork
                // in proc.c fork()
                // Copy user memory from parent to child.
                if(uvmcopy(p->pagetable, np->pagetable, p->sz) < 0){
                freeproc(np);
                release(&np->lock);
                return -1;
                }
                if(kvmcopy(np->pagetable, np->kpgtbl, p->sz) < 0){
                freeproc(np);
                release(&np->lock);
                return -1;
                }
                +

                内核使用PTE_R和PTE_X权限映射trampoline和kernel text。这表明这份内存段可以读,可以被当做指令块执行,但不能写。其他的块都是可读可写的,除了guard page被设置为不可访问。

                +

                Code: creating an address space

                vm.c

                操作地址空间和页表部分的代码都在kernel/vm.c中。代表页表的数据结构是pagetable_t

                +

                vm.c的主要函数有walk、mappages等。walk用来在三级页表中找到某个虚拟地址表项,或者创建一个新的表项。mappages用来新建一个表项,主要用到了walk函数。

                +

                vm.c中,以kvm开头的代表操纵内核页表,以uvm开头的代表操纵进程里的用户页表。

                +

                以初始化为例介绍各个函数

                创建页表

                一开始操作系统初始化时,会调用vm.c中的kvminit来创建内核页表。主要就是在以内核地址空间的页表结构在填写页表。

                +
                void
                kvminit(void)
                {
                kernel_pagetable = kvmmake();
                }
                // Make a direct-map page table for the kernel.
                pagetable_t
                kvmmake(void)
                {
                //内核页表
                pagetable_t kpgtbl;
                //申请新的一页
                kpgtbl = (pagetable_t) kalloc();
                memset(kpgtbl, 0, PGSIZE);

                //给内核页表初始化表项,结构详见上面的内核地址空间部分
                // uart registers
                kvmmap(kpgtbl, UART0, UART0, PGSIZE, PTE_R | PTE_W);

                // virtio mmio disk interface
                kvmmap(kpgtbl, VIRTIO0, VIRTIO0, PGSIZE, PTE_R | PTE_W);

                // PLIC
                kvmmap(kpgtbl, PLIC, PLIC, 0x400000, PTE_R | PTE_W);

                // map kernel text executable and read-only.
                kvmmap(kpgtbl, KERNBASE, KERNBASE, (uint64)etext-KERNBASE, PTE_R | PTE_X);

                // map kernel data and the physical RAM we'll make use of.
                kvmmap(kpgtbl, (uint64)etext, (uint64)etext, PHYSTOP-(uint64)etext, PTE_R | PTE_W);

                // map the trampoline for trap entry/exit to
                // the highest virtual address in the kernel.
                kvmmap(kpgtbl, TRAMPOLINE, (uint64)trampoline, PGSIZE, PTE_R | PTE_X);

                // allocate and map a kernel stack for each process.
                proc_mapstacks(kpgtbl);

                return kpgtbl;
                }
                -
                exec
                // in exec.c
                // Commit to the user image.
                oldpagetable = p->pagetable;
                p->pagetable = pagetable;

                p->sz = sz;
                p->trapframe->epc = elf.entry; // initial program counter = main
                p->trapframe->sp = sp; // initial stack pointer
                proc_freepagetable(oldpagetable, oldsz);

                // 添上此句
                kvmcopy(p->pagetable, p->kpgtbl, p->sz);
                +

                其中,kvmmap用来在内核页表中添加一个新的表项。其函数形式为

                +
                // add a mapping to the kernel page table.
                // only used when booting.
                // does not flush TLB or enable paging.
                void
                kvmmap(pagetable_t kpgtbl, uint64 va, uint64 pa, uint64 sz, int perm)
                {
                if(mappages(kpgtbl, va, sz, pa, perm) != 0)
                panic("kvmmap");
                }
                -
                sbrk
                uint64
                sys_sbrk(void)
                {
                int addr;
                int n;

                if(argint(0, &n) < 0)
                return -1;
                addr = myproc()->sz;
                if(addr+n >= PLIC) return -1;
                if(growproc(n) < 0)
                return -1;
                return addr;
                }
                +

                实现主要逻辑的是mappages函数

                +
                // Create PTEs for virtual addresses starting at va that refer to
                // physical addresses starting at pa. va and size might not
                // be page-aligned. Returns 0 on success, -1 if walk() couldn't
                // allocate a needed page-table page.
                int
                mappages(pagetable_t pagetable, uint64 va, uint64 size, uint64 pa, int perm)
                {
                uint64 a, last;
                pte_t *pte;

                if(size == 0)
                panic("mappages: size");

                a = PGROUNDDOWN(va);
                last = PGROUNDDOWN(va + size - 1);
                for(;;){
                //walk函数通过虚拟地址新建一个第三级页表的表项并返回其指针,之后只需要填这个表项即可
                if((pte = walk(pagetable, a, 1)) == 0)
                return -1;
                //如果pte存在并且标记为已使用,说明该虚拟地址映射已经存在
                if(*pte & PTE_V)
                panic("mappages: remap");
                //填写表项:物理地址 flags
                *pte = PA2PTE(pa) | perm | PTE_V;
                if(a == last)
                break;
                //每两个表项间隔PGSIZE个字节
                a += PGSIZE;
                pa += PGSIZE;
                }
                return 0;
                }
                -
                // in proc.c
                // Grow or shrink user memory by n bytes.
                // Return 0 on success, -1 on failure.
                int
                growproc(int n)
                {
                uint sz;
                struct proc *p = myproc();

                sz = p->sz;
                // ...
                p->sz = sz;
                // 加这个
                kvmcopy(p->pagetable, p->kpgtbl, p->sz);
                return 0;
                }
                +

                通过虚拟地址获取表项主要是通过walk实现的

                +
                // Return the address of the PTE in page table pagetable
                // that corresponds to virtual address va. If alloc!=0,
                // create any required page-table pages.
                //
                // The risc-v Sv39 scheme has three levels of page-table
                // pages. A page-table page contains 512 64-bit PTEs.
                // A 64-bit virtual address is split into five fields:
                // 39..63 -- must be zero.
                // 30..38 -- 9 bits of level-2 index.
                // 21..29 -- 9 bits of level-1 index.
                // 12..20 -- 9 bits of level-0 index.
                // 0..11 -- 12 bits of byte offset within the page.
                // 虚拟地址的格式:UNUSED 页表索引 offset,其中页表索引在三级页表中被划分为了三个,分别是
                // level0-level2,分别代表了第三级、第二级、第一级页表的索引【具体可见页表组成中的图】
                // walk的目的就是要在这三级页表中找到虚拟地址对应的页表项。当alloc!=0时,则要求找不到就新建一个
                pte_t *
                walk(pagetable_t pagetable, uint64 va, int alloc)
                {
                if(va >= MAXVA)
                panic("walk");

                for(int level = 2; level > 0; level--) {
                pte_t *pte = &pagetable[PX(level, va)];
                if(*pte & PTE_V) {
                // 取出PTE中表示下一级页表地址的字节
                pagetable = (pagetable_t)PTE2PA(*pte);
                } else {
                // 页表不存在的情况,要么返回0,要么新建一页
                if(!alloc || (pagetable = (pde_t*)kalloc()) == 0)
                return 0;
                memset(pagetable, 0, PGSIZE);
                *pte = PA2PTE(pagetable) | PTE_V;
                }
                }
                // 最终返回第三级页表的对应表项
                return &pagetable[PX(0, va)];
                }
                -
                userinit
                -

                这一步不能忽视,因为内核启动的时候就需要用到copyinstr。

                -
                -
                // in proc.c userinit()
                uvminit(p->pagetable, initcode, sizeof(initcode));
                p->sz = PGSIZE;
                // 加这个!
                kvmcopy(p->pagetable, p->kpgtbl, p->sz);
                +
                装上页表

                使用的是kvminithart函数。它将内核页表的root page table的物理地址写入了satp寄存器。从这个函数之后,就开启了内存映射

                +
                // Switch h/w page table register to the kernel's page table,
                // and enable paging.
                void
                kvminithart()
                {
                // wait for any previous writes to the page table memory to finish.
                sfence_vma();

                w_satp(MAKE_SATP(kernel_pagetable));

                // flush stale entries from the TLB.
                sfence_vma();
                }
                -
                删掉freewalk的panic(我特有的缺点)
                // in vm.c freewalk()    
                if((pte & PTE_V) && (pte & (PTE_R|PTE_W|PTE_X)) == 0){
                // ...
                } else if(pte & PTE_V){
                //panic("freewalk: leaf");
                }
                -]]>
                -
                - - Traps and system calls - /2023/01/10/xv6$chap4/ - Traps and system calls

                traps=系统调用+异常+中断。本章着重讲traps概述以及traps中的系统调用。

                -

                对trap的处理包含四个部分:硬件处理、中断向量、trap handler、对应的处理函数

                -

                RISC-V trap machinery

                -

                image-20230618162437636

                -

                image-20230618162500928

                -

                在RISC-V中,异常通常是由于程序执行过程中的错误或非预期事件而引起的,包括故障(faults)、陷阱(traps)和中止(aborts)。中断(interrupts)则是由外部事件触发的,例如定时器到期、外部设备请求等。中断是异步事件,与当前正在执行的指令无关,因此会在任何时候发生。

                -

                xv6是基于RISC-V架构的。因此,发生异常的时候,就会跳转到统一的kernel trap,然后再在里面通过读取scause来进行相应处理。

                -

                发生中断的处理方式就和x86差不多了,都是通过中断向量实现的。

                +

                其中sfence_vma()的用途是强制更新TLB的旧页表,类似于Java volatile的作用。

                +
                疑问

                附上书里的详细解释:

                +

                image-20230109222917346

                +

                TLB与页表类似于cache与主存的关系。TLB保存了页表的一部分。

                +
                我的错误想法

                我怎么感觉怪怪的啊?因为TLB既然是高速缓存,那么读写页表也应该优先从TLB读写【注:应该就是从这里开始错的hhh写应该是直接写入页表】。所以说,会陈旧的应该是主存中的页表,而不是TLB中的页表。但是,书里是说,改完页表必须通知TLB更改。也就是说,读写页表不是从TLB读写的,那该是从哪里?是TLB以外的free memory吗?

                +

                不过,要是从多CPU的角度思考,说不定他这个意思是某个CPU的TLB变了,需要通知其他所有CPU的TLB也变。虽然不同CPU当前执行的进程是不一样的,使用的页表项不一样,切换进程的时候也会把用户地址空间的页表项flush掉。但是内核地址空间的页表项一般是不会随着进程切换而flush掉的。所以内核页表修改就需要手动多CPU同步。

                +

                我认为多CPU角度考虑更加合理,因为它最后说了,xv6会在内核页表init后flush,以及在从内核态切换回用户态的时候flush。这两个(好像)都影响内核页表比较多,所以就需要手动flush一下。

                +
                解答

                之后学了缺页异常后,可以发现这里其实是没问题的。

                +

                计算机体系结构 – 虚拟内存

                +

                v2-e15454bf032baa4dc088b6e41ed4f4a4_1440w

                +

                页表的管理(创建、更新、删除等)是由操作系统负责的。地址转换时,页表检索是由硬件内存管理单元(Memory Management Unit, MMU)负责的。MMU通常由两部分构成:表查找单元(Table Walk Unit, TWU)和转换旁路缓冲(Translation Lookaside Buffer, TLB)[2]。TWU负责链式的访问PDE、PTE,完成上述的查表过程。

                +

                应用多级页表之后,想要完成一次地址转换,需要访问多级目录和页表,这么多次的内存访问会严重降低性能。

                +

                为了优化地址转换速度,人们在MMU中增加了一块高速cache,专门用来缓存虚拟地址到物理地址的映射,这块cache就是TLB[7][8]。MMU在做地址转换的时候,会先检索TLB,如果命中则直接返回对应的物理地址,如果不命中则会调用TWU查找页表。

                +

                TLB中缓存的是虚拟地址到物理地址映射。然而,多级页表的查找是一个链式的过程,对于在虚拟地址空间中连续的两个页,它们的各级目录项可能都是一样的,只有最后一级页号不一样。查找完第一个虚拟页之后,我们可以将相同的前级目录项都缓存起来。查找第二个虚拟页时,可以直接使用缓存好的前几级目录项,节省查找时间。这种缓存叫做Page Structure Cache[9]

                +

                而当TLB和MMU中都没有该物理页,就会发生缺页异常。但是操作系统仅会对页表更新,而不会被TLB更新。故而,TBL中数据可能陈旧,需要手动flush。

                +

                Physical memory allocation

                在内核运行的时候,需要申请很多空间用来存放各种数据。

                +
                +

                The kernel must allocate and free physical memory at run-time for page tables, user memory, kernel stacks, and pipe buffers.

                -

                control register

                risc-v为trap提供了一组寄存器:

                -
                  -
                1. stvec

                  -

                  trap handler的入口地址

                  -
                2. -
                3. sepc

                  -

                  原程序PC

                  -
                4. -
                5. scause

                  -

                  中断号

                  -
                6. -
                7. sscratch

                  -

                  TRAPFRAME地址

                  -
                8. -
                9. sstatus

                  -

                  是否允许中断,以及中断来自内核态还是用户态

                  -
                10. -
                +

                用的是这段空闲内存:

                +

                image-20230109225700837

                -

                The above registers relate to traps handled in supervisor mode, and they cannot be read or written in user mode.

                -

                There is an equivalent set of control registers for traps handled in machine mode; xv6 uses them only for the special case of timer interrupts.

                +

                It keeps track of which pages are free by threading a linked list through the pages themselves.

                -

                每个CPU都有自己的一套这样的控制寄存器。

                -

                硬件处理步骤

                时钟中断、device interrupt以及关中断的情况下,不会做以下步骤。

                +

                kalloc.c中就是这么实现的。

                +

                Code: Physical memory allocator

                内核运行时申请释放空闲物理空间是通过kernel/kalloc.c完成的。它为内核栈、用户进程、页表和管道buffer服务。

                -

                \1. If the trap is a device interrupt, and the sstatus SIE bit is clear, don’t do any of the following.

                -

                \2. Disable interrupts by clearing SIE.关中断

                -

                \3. Copy the pc to sepc.保存PC

                -

                \4. Save the current mode (user or supervisor) in the SPP bit in sstatus.保存mode

                -

                \5. Set scause to reflflect the trap’s cause.保存中断号

                -

                \6. Set the mode to supervisor.切换到内核态

                -

                \7. Copy stvec to the pc.将trap handler写入pc,开始执行trap handler【uservec or kernelvec?】

                +

                kalloc.c用来在运行时申请分配新的一页,上面的vm.c正是用了kalloc申请一页,要么作为页表,要么作为存储数据的第三级页表指向的物理内存。

                -

                切换到内核页表、切换内核栈、保存寄存器现场这些工作交给操作系统完成。

                -

                Traps from user space

                从用户态来的trap会经历怎么样的过程?

                -

                前面说到,下面需要进行页表的切换,页表的切换必然是接下来要做的指令的某个环节。那么为了让页表切换之后,CPU还知道要从哪里取指执行,就要让某段物理内存在内核空间和用户空间的虚拟地址一样。这样,不论页表是用户的还是内核的,都可以通过同样的虚拟地址访问到该段存放指令的物理内存从而继续执行。

                -

                这段虚拟地址就是trampoline。它在内核页表和用户页表都位于MAXVA的位置。

                +

                最后应该会在空闲内存内形成这样的结构:

                +

                内存分成一页一页的,每页内存中的前几个字节存储着其对应队列中下一块内存的物理地址。不一定是从小地址到大地址顺序连接。

                -

                我感觉这段大概可以这么理解:

                -

                通过查看代码,可知trampoline段实际上存储的是trampoline.S中的数据,也即uservec和userret的汇编代码,也即执行切换页表我们实际上就是在执行trampoline里的代码。trampoline的存在,就可以使得每个页表的这部分都是这两个的代码,这样一来切换页表也就不影响指令流的执行。

                +

                It store each free page’s run structure in the free page itself, since there’s nothing else stored there.

                -

                p3

                -

                stevc存储的正是trampoline段中的uservec。

                -

                uservec

                sscratch里面存的是trapframe的值。

                -

                trapframe存在于用户空间中,并且每个进程的trapframe所处位置固定是在trampoline下方。

                -

                image-20230111203357767

                -

                首先将寄存器的值都存入trapframe中;然后,再从trapframe中读取内核栈指针、当前CPUid,下一步要跳转的usertrap的地址,以及内核页表。最后,uservec切换到内核页表,并且jmp到usertrap。

                -
                #in kernel/trampoline.S
                .section trampsec
                .globl trampoline
                trampoline:
                .align 4
                .globl uservec
                uservec:
                #
                # trap.c sets stvec to point here, so
                # traps from user space start here,
                # in supervisor mode, but with a
                # user page table.
                #
                # sscratch points to where the process's p->trapframe is
                # mapped into user space, at TRAPFRAME.
                #

                # swap a0 and sscratch
                # so that a0 is TRAPFRAME
                csrrw a0, sscratch, a0

                # save the user registers in TRAPFRAME
                sd ra, 40(a0)
                sd sp, 48(a0)
                # ...
                sd t6, 280(a0)

                # save the user a0 in p->trapframe->a0
                csrr t0, sscratch
                sd t0, 112(a0)

                # restore kernel stack pointer from p->trapframe->kernel_sp
                # 完成了内核栈的切换
                ld sp, 8(a0)

                # make tp hold the current hartid, from p->trapframe->kernel_hartid
                ld tp, 32(a0)

                # load the address of usertrap(), p->trapframe->kernel_trap
                ld t0, 16(a0)

                # restore kernel page table from p->trapframe->kernel_satp
                ld t1, 0(a0)
                # 这里完成了页表的切换
                csrw satp, t1
                sfence.vma zero, zero

                # a0 is no longer valid, since the kernel page
                # table does not specially map p->tf.

                # jump to usertrap(), which does not return
                jr t0
                +
                // Physical memory allocator, for user processes,
                // kernel stacks, page-table pages,
                // and pipe buffers. Allocates whole 4096-byte pages.

                #include "types.h"
                #include "param.h"
                #include "memlayout.h"
                #include "spinlock.h"
                #include "riscv.h"
                #include "defs.h"

                // 释放在这范围内的物理内存空间
                void freerange(void *pa_start, void *pa_end);

                // 也就是上面说的free memory的起始位置
                extern char end[]; // first address after kernel.
                // defined by kernel.ld.

                // run代表的是一页内存
                struct run {
                struct run *next;
                };

                // 代表了整个内核空闲的物理空间
                struct {
                struct spinlock lock;
                struct run *freelist;
                } kmem;

                void
                kinit()
                {
                initlock(&kmem.lock, "kmem");
                // init的时候先清空空闲空间,建立空闲页队列
                freerange(end, (void*)PHYSTOP);
                }

                void
                freerange(void *pa_start, void *pa_end)
                {
                char *p;
                // PGROUNDUP和PGROUNDDOWN是用于将地址四舍五入到PGSIZE
                p = (char*)PGROUNDUP((uint64)pa_start);
                for(; p + PGSIZE <= (char*)pa_end; p += PGSIZE)
                kfree(p);
                }

                // Free the page of physical memory pointed at by pa,
                // which normally should have been returned by a
                // call to kalloc(). (The exception is when
                // initializing the allocator; see kinit above.)
                void
                kfree(void *pa)
                {
                struct run *r;

                // pa得是整数页,并且得在内核物理内存范围之间
                if(((uint64)pa % PGSIZE) != 0 || (char*)pa < end || (uint64)pa >= PHYSTOP)
                panic("kfree");

                // Fill with junk to catch dangling refs.
                memset(pa, 1, PGSIZE);

                // 之后将在pa对应的那一页的前几个字节写入next字段
                r = (struct run*)pa;

                // 这意思就是在空闲内存的链表队列中新增一块
                acquire(&kmem.lock);
                r->next = kmem.freelist;
                kmem.freelist = r;
                release(&kmem.lock);
                }

                // Allocate one 4096-byte page of physical memory.
                // Returns a pointer that the kernel can use.
                // Returns 0 if the memory cannot be allocated.
                void *
                kalloc(void)
                {
                struct run *r;

                acquire(&kmem.lock);
                r = kmem.freelist;
                if(r)
                kmem.freelist = r->next;
                release(&kmem.lock);

                if(r)
                memset((char*)r, 5, PGSIZE); // fill with junk
                return (void*)r;
                }
                +

                Process address space

                当用户进程叫xv6分配内存时,xv6会用kalloc去取,然后登记在页表上。

                -

                注:trampoline和trapframe有一些相通点。

                -

                trampoline为了保障某段物理内存的虚拟地址在内核栈和用户栈中不变,做出的努力是,在内核栈和用户栈都分配同一位置的PTE。

                -

                trapframe用于保护现场、用户态向内核态传递参数等等,做出的努力是,在用户栈分配同一位置的PTE,在内核态的局部变量中保存了自己的物理地址。

                -

                这两个说实话有点容易混起来,因为我想了半天trampoline可不可以用类似trapframe一样的方法,结论是不行。因为你trampoline的作用是维持指令序列依然不变,不会突然没掉;而trapframe段是用来存储数据而非执行的,对其的控制也是需要指令的。如果trampoline使用第二种方法,指令流就会断掉,更别说别的了。

                +

                The stack is a single page, and is shown with the initial contents as created by exec. Strings containing the command-line arguments, as well as an array of pointers to them, are at the very top of the stack. Just under that are values that allow a program to start at main as if the function main(argc, argv) had just been called.

                -

                usertrap

                作用是得到trap发生的原因,并且执行对应的处理程序,然后返回结果。

                -
                // handle an interrupt, exception, or system call from user space.
                // called from trampoline.S
                void
                usertrap(void)
                {
                int which_dev = 0;

                if((r_sstatus() & SSTATUS_SPP) != 0)
                panic("usertrap: not from user mode");

                // send interrupts and exceptions to kerneltrap(),
                // since we're now in the kernel.
                //首先把trap handler切换到kernel的,这样一来如果在kernel中发生trap就会由kernel的handler处理
                w_stvec((uint64)kernelvec);

                struct proc *p = myproc();

                //在当前进程中再次保存用户程序的原PC,防止之后sepc被覆盖
                // save user program counter.
                p->trapframe->epc = r_sepc();

                //根据cause号不同处理
                if(r_scause() == 8){
                // system call

                if(p->killed)
                exit(-1);

                // sepc points to the ecall instruction,
                // but we want to return to the next instruction.
                p->trapframe->epc += 4;

                // an interrupt will change sstatus &c registers,
                // so don't enable until done with those registers.
                //注意,在此处开启了中断
                intr_on();

                //调用syscall处理
                syscall();
                } else if((which_dev = devintr()) != 0){
                // ok
                } else {
                printf("usertrap(): unexpected scause %p pid=%d\n", r_scause(), p->pid);
                printf(" sepc=%p stval=%p\n", r_sepc(), r_stval());
                p->killed = 1;
                }

                if(p->killed)
                exit(-1);

                // give up the CPU if this is a timer interrupt.
                if(which_dev == 2)
                yield();

                usertrapret();
                }
                - -

                执行对应的处理函数

                比如说system call会修改trapframe中的a0为返回的结果,会获取trapframe中的各个参数。这个“保护现场“感觉是非常微妙的,它兼顾了保护现场和传递参数两个作用

                -

                usertrapret

                回到用户态。之前陷入内核态对stvec、satp、sp、hartid、trap handler都做了适应内核态的改变,因而这里就需要改回原来适应用户态的样子,然后返回用户态。

                -
                // return to user space
                void
                usertrapret(void)
                {
                struct proc *p = myproc();

                // 关中断
                // we're about to switch the destination of traps from
                // kerneltrap() to usertrap(), so turn off interrupts until
                // we're back in user space, where usertrap() is correct.
                intr_off();

                // 重置trapframe
                // send syscalls, interrupts, and exceptions to trampoline.S
                w_stvec(TRAMPOLINE + (uservec - trampoline));
                // set up trapframe values that uservec will need when
                // the process next re-enters the kernel.
                p->trapframe->kernel_satp = r_satp(); // kernel page table
                p->trapframe->kernel_sp = p->kstack + PGSIZE; // process's kernel stack
                p->trapframe->kernel_trap = (uint64)usertrap;
                p->trapframe->kernel_hartid = r_tp(); // hartid for cpuid()

                // set up the registers that trampoline.S's sret will use
                // to get to user space.

                // set S Previous Privilege mode to User.
                unsigned long x = r_sstatus();
                x &= ~SSTATUS_SPP; // clear SPP to 0 for user mode
                x |= SSTATUS_SPIE; // enable interrupts in user mode
                w_sstatus(x);

                // set S Exception Program Counter to the saved user pc.
                w_sepc(p->trapframe->epc);

                // tell trampoline.S the user page table to switch to.
                uint64 satp = MAKE_SATP(p->pagetable);

                // jump to trampoline.S at the top of memory, which
                // switches to the user page table, restores user registers,
                // and switches to user mode with sret.
                uint64 fn = TRAMPOLINE + (userret - trampoline);
                ((void (*)(uint64,uint64))fn)(TRAPFRAME, satp);
                }
                +

                image-20230109234930690

                +

                Code: sbrk

                +

                Sbrk is the system call for a process to shrink or grow its memory. The system call is implemented by the function growproc (kernel/proc.c:239).

                +
                +
                // Grow or shrink user memory by n bytes.注意单位是bytes,grow n+,shrink n-
                // Return 0 on success, -1 on failure.
                // 主要逻辑还是通过vm.c实现
                int
                growproc(int n)
                {
                uint64 sz;//size
                struct proc *p = myproc();

                sz = p->sz;
                if(n > 0){
                if((sz = uvmalloc(p->pagetable, sz, sz + n, PTE_W)) == 0) {
                return -1;
                }
                } else if(n < 0){
                sz = uvmdealloc(p->pagetable, sz, sz + n);
                }
                p->sz = sz;
                return 0;
                }
                -

                userret

                .globl userret
                userret:
                # userret(TRAPFRAME, pagetable)
                # switch from kernel to user.
                # usertrapret() calls here.
                # a0: TRAPFRAME, in user page table.
                # a1: user page table, for satp.

                # 切换为用户页表
                # switch to the user page table.
                csrw satp, a1
                sfence.vma zero, zero

                # put the saved user a0 in sscratch, so we
                # can swap it with our a0 (TRAPFRAME) in the last step.
                ld t0, 112(a0)
                csrw sscratch, t0

                # restore all but a0 from TRAPFRAME
                ld ra, 40(a0)
                ld sp, 48(a0)
                # ...
                ld t6, 280(a0)

                # restore user a0, and save TRAPFRAME in sscratch
                csrrw a0, sscratch, a0

                # return to user mode and user pc.
                # usertrapret() set up sstatus and sepc.
                sret
                +
                // Allocate PTEs and physical memory to grow process from oldsz to
                // newsz, which need not be page aligned.不需要页对齐 Returns new size or 0 on error.
                uint64
                uvmalloc(pagetable_t pagetable, uint64 oldsz, uint64 newsz, int xperm)
                {
                char *mem;
                uint64 a;

                if(newsz < oldsz)
                return oldsz;

                // oldsz向上取整
                oldsz = PGROUNDUP(oldsz);
                // 每页alloc
                for(a = oldsz; a < newsz; a += PGSIZE){
                mem = kalloc();
                if(mem == 0){
                // 说明失败,恢复到原状
                // 这里不用像下面一样kfree是因为这里压根没有alloc成功
                uvmdealloc(pagetable, a, oldsz);
                return 0;
                }
                // 除去junk data
                memset(mem, 0, PGSIZE);
                // 放入页表
                if(mappages(pagetable, a, PGSIZE, (uint64)mem, PTE_R|PTE_U|xperm) != 0){
                // 不成功
                // dealloc原理是顺着页表一个个free的。由于mem此处没有成功放入页表,所以就得单独free掉
                kfree(mem);
                uvmdealloc(pagetable, a, oldsz);
                return 0;
                }
                }
                return newsz;
                }
                -

                Code: calling system calls

                -

                Chapter 2 ended with initcode.S invoking the exec system call (user/initcode.S:11). Let’s look at how the user call makes its way to the exec system call’s implementation in the kernel.要讲如何从用户态找到exec的代码了。

                +

                Code:exec

                +

                Exec is the system call that creates the user part of an address space. It initializes the user part of an address space from a fifile stored in the fifile system.

                +

                exec是创建地址空间的用户部分的系统调用。它使用一个存储在文件系统中的文件初始化地址空间的用户部分。

                -

                Code: system call arguments

                讲的是系统调用时,是如何把用户态传递的地址转化为内核态地址的。

                -

                这个部分可以看看hit实验的实验3 6.7,讲得很详细,而且流程是差不多的。linux0.11的get_fs_byte()就相当于xv6的copyinstr

                -

                Traps from kernel space

                kernelvec

                不同于用户态还得先潜入内核再潜出内核,内核的trap可简单多了,省去了切来切去各种东西的步骤,只需当做一个普通的函数调用就行。

                -
                # in kernel/kernelvec.S
                .globl kerneltrap
                .globl kernelvec
                .align 4
                kernelvec:
                // make room to save registers.
                addi sp, sp, -256

                // save the registers.
                sd ra, 0(sp)
                sd sp, 8(sp)
                # ...
                sd t6, 240(sp)

                // call the C trap handler in trap.c
                call kerneltrap

                // restore registers.
                ld ra, 0(sp)
                ld sp, 8(sp)
                # ...
                ld t5, 232(sp)
                ld t6, 240(sp)

                addi sp, sp, 256

                // return to whatever we were doing in the kernel.
                sret
                +
                int
                exec(char *path, char **argv)
                {
                char *s, *last;
                int i, off;
                uint64 argc, sz = 0, sp, ustack[MAXARG], stackbase;
                struct elfhdr elf;
                struct inode *ip;
                struct proghdr ph;
                pagetable_t pagetable = 0, oldpagetable;
                struct proc *p = myproc();

                //开始打开文件的意思吧(
                begin_op();

                //ip是一个inode
                //打开路径为path的文件
                if((ip = namei(path)) == 0){
                end_op();
                return -1;
                }
                //暂时锁住文件,别人不许动
                ilock(ip);

                //之后应该就是把文件读入内存吧
                // Check ELF header
                if(readi(ip, 0, (uint64)&elf, 0, sizeof(elf)) != sizeof(elf))
                goto bad;

                if(elf.magic != ELF_MAGIC)
                goto bad;

                //分配新页表
                if((pagetable = proc_pagetable(p)) == 0)
                goto bad;

                //elfhd应该指的是可执行文件头
                // Load program into memory.
                for(i=0, off=elf.phoff; i<elf.phnum; i++, off+=sizeof(ph)){
                if(readi(ip, 0, (uint64)&ph, off, sizeof(ph)) != sizeof(ph))
                goto bad;
                if(ph.type != ELF_PROG_LOAD)
                continue;
                if(ph.memsz < ph.filesz)
                goto bad;
                if(ph.vaddr + ph.memsz < ph.vaddr)
                goto bad;
                if(ph.vaddr % PGSIZE != 0)
                goto bad;
                //总之顺利读到了
                uint64 sz1;
                //读到了就给它分配新空间并且填入页表
                if((sz1 = uvmalloc(pagetable, sz, ph.vaddr + ph.memsz, flags2perm(ph.flags))) == 0)
                goto bad;
                sz = sz1;
                if(loadseg(pagetable, ph.vaddr, ip, ph.off, ph.filesz) < 0)
                goto bad;
                }
                //在这里解锁
                iunlockput(ip);
                end_op();
                ip = 0;

                p = myproc();
                uint64 oldsz = p->sz;

                //读完文件,开始造一个新的用户栈【fork之后用户栈是不会清空的】
                // Allocate two pages at the next page boundary.
                // Make the first inaccessible as a stack guard.
                // Use the second as the user stack.
                sz = PGROUNDUP(sz);
                uint64 sz1;
                if((sz1 = uvmalloc(pagetable, sz, sz + 2*PGSIZE, PTE_W)) == 0)
                goto bad;
                sz = sz1;
                // mark a PTE invalid for user access.造guard page
                uvmclear(pagetable, sz-2*PGSIZE);
                // sp为栈顶
                sp = sz;
                // 应该指的是栈尾
                stackbase = sp - PGSIZE;

                // 开始往栈中填入执行参数
                // Push argument strings, prepare rest of stack in ustack.
                for(argc = 0; argv[argc]; argc++) {
                if(argc >= MAXARG)
                goto bad;
                sp -= strlen(argv[argc]) + 1;
                sp -= sp % 16; // riscv sp must be 16-byte aligned
                if(sp < stackbase)
                goto bad;
                //argv来自用户空间,所以需要使用copyout
                if(copyout(pagetable, sp, argv[argc], strlen(argv[argc]) + 1) < 0)
                goto bad;
                //这什么东西
                //exec一次将参数中的一个字符串复制到栈顶,并在ustack中记录指向它们的指针
                ustack[argc] = sp;
                }
                //放置空指针
                ustack[argc] = 0;

                // push the array of argv[] pointers.
                sp -= (argc+1) * sizeof(uint64);
                sp -= sp % 16;
                if(sp < stackbase)
                goto bad;
                if(copyout(pagetable, sp, (char *)ustack, (argc+1)*sizeof(uint64)) < 0)
                goto bad;

                // arguments to user main(argc, argv)
                // argc is returned via the system call return
                // value, which goes in a0.
                p->trapframe->a1 = sp;

                // Save program name for debugging.
                for(last=s=path; *s; s++)
                if(*s == '/')
                last = s+1;
                safestrcpy(p->name, last, sizeof(p->name));

                //只有成功了才会来到这,才会覆盖掉旧的内存镜像
                // Commit to the user image.
                oldpagetable = p->pagetable;
                p->pagetable = pagetable;
                p->sz = sz;
                p->trapframe->epc = elf.entry; // initial program counter = main
                p->trapframe->sp = sp; // initial stack pointer
                proc_freepagetable(oldpagetable, oldsz);

                return argc; // this ends up in a0, the first argument to main(argc, argv)

                bad:
                //释放新镜像,不改变旧镜像
                if(pagetable)
                proc_freepagetable(pagetable, sz);
                if(ip){
                iunlockput(ip);
                end_op();
                }
                return -1;
                }
                -

                kerneltrap

                -

                kerneltrap is prepared for two types of traps: device interrrupts and exceptions.

                -

                It calls devintr (kernel/trap.c:177) to check for and handle the former. If the trap isn’t a device interrupt, it must be an exception, and that is always a fatal error if it occurs in the xv6 kernel; the kernel calls panic and stops executing.

                -

                If kerneltrap was called due to a timer interrupt, and a process’s kernel thread is running (rather than a scheduler thread), kerneltrap calls yield to give other threads a chance to run.

                +

                Real world

                image-20230110010651653

                +

                xv6内核缺少一个类似malloc可以为小对象提供内存的分配器,这使得内核无法使用需要动态分配的复杂数据结构。【确实,感觉一分配就是一页(】

                +

                内存分配是一个长期的热门话题,基本问题是有效使用有限的内存并为将来的未知请求做好准备。今天,人们更关心速度而不是空间效率。此外,一个更复杂的内核可能会分配许多不同大小的小块,而不是(如xv6中)只有4096字节的块;一个真正的内核分配器需要处理小分配和大分配。

                +

                Lab:Pagetable

                +

                In this lab you will explore page tables and modify them to to speed up certain system calls and to detect which pages have been accessed.

                -
                // in kernel/trap.c
                // interrupts and exceptions from kernel code go here via kernelvec,
                // on whatever the current kernel stack is.
                void
                kerneltrap()
                {
                int which_dev = 0;
                uint64 sepc = r_sepc();
                uint64 sstatus = r_sstatus();
                uint64 scause = r_scause();

                if((sstatus & SSTATUS_SPP) == 0)
                panic("kerneltrap: not from supervisor mode");
                if(intr_get() != 0)
                panic("kerneltrap: interrupts enabled");

                // 在此处的devintr对不同的设备进行不同的处理方式
                if((which_dev = devintr()) == 0){
                printf("scause %p\n", scause);
                printf("sepc=%p stval=%p\n", r_sepc(), r_stval());
                panic("kerneltrap");
                }

                // give up the CPU if this is a timer interrupt.
                if(which_dev == 2 && myproc() != 0 && myproc()->state == RUNNING)
                yield();

                // the yield() may have caused some traps to occur,
                // so restore trap registers for use by kernelvec.S's sepc instruction.
                w_sepc(sepc);
                w_sstatus(sstatus);
                }
                - -

                Page-fault exceptions

                似乎xv6是没有这个缺页exception的。这里主要讲解了三个可以利用缺页中断实现的优化:COW fork、lazy allocation、paging from disk。还提及了automatically extending stacks 以及memory-mapped fifiles。

                -

                Lab:Trap

                -

                This lab explores how system calls are implemented using traps. You will first do a warm-up exercises with stacks and then you will implement an example of user-level trap handling.

                +

                不过遗憾的是usertests还有好几个没通过,具体都标注了。

                +

                Speed up system calls

                +

                When each process is created, map one read-only page at USYSCALL (a VA defined in memlayout.h). At the start of this page, store a struct usyscall (also defined in memlayout.h), and initialize it to store the PID of the current process. For this lab, ugetpid() has been provided on the userspace side and will automatically use the USYSCALL mapping. You will receive full credit for this part of the lab if the ugetpid test case passes when running pgtbltest.

                +

                参考文章:MIT 6.S081 2021: Lab page tables

                -

                RISC-V assembly

                题目和答案

                -

                参考:

                -

                Lab4: traps

                -

                There is a file user/call.c in your xv6 repo. make fs.img compiles it and also produces a readable assembly version of the program in user/call.asm.

                -

                Read the code in call.asm for the functions g, f, and main. Here are some questions that you should answer:

                +

                感想

                乌龙

                这里好像是因为实验改版了,我下的是2020年的实验包,在memlayout压根找不到USYSCALL和struct usyscall这俩东西。最后翻了下网上的总算找到了。

                +

                我一开始没找到,还以为USYSCALL以及usyscall这两个都得自己写在memlayout里面,想了很久都没想出来USYSCALL的值应该设置为多少。我认为只需满足两个条件即可:1.所处内存段应该是free memory那段,也即自kernel结束(PHYSTOP)到MAXVA这一大块。2.得确保能被用户和内核都能访问到。

                +

                前者意为虚拟地址在MAXVA和PHYSTOP之间,后者意为那段内存应该标记为PTE_U。这个范围是很宽泛的,我实在不知道要分配这期间的哪块内存,感觉也不大可能是真的自由度那么大。所以我就偷偷看了hints【悲】,想看它对这个USYSCALL应该写什么值有没有建议。结果发现这东西是实验给我们定的。遂去网上找到了它给的真正的USYSCALL值。

                +
                #define USYSCALL (TRAPFRAME - PGSIZE)

                struct usyscall{
                int pid;
                };
                + +

                用户的ugetpid只找到了一个截图:

                +

                v2-0c2603da4c8102e46ae390a0d0b1191d_1440w

                +

                恕我愚钝实在不知道该把这段代码放在哪orz于是接下来写的东西就没有自测。

                +
                panic:freewalk leaf

                一开始写好代码准备启动xv6的时候爆出了这么一个panic,搜了一下得到如下解答:

                +
                +

                来源:MIT-6.S081-2020实验(xv6-riscv64)十:mmap

                +

                这时运行会发现freewalk函数panic:freewalk: leaf,这是因为freewalk希望所有虚拟地址已经被解绑并释放对应的物理空间了,该函数只负责释放页表。

                +

                让我得知freewalk在vm.c下面【吐槽,我一开始还以为是自由自在地走(,看到这个才反应过来是free walk,跟页表有关的】。结合freewalk的代码

                +

                image-20230110225359361

                +

                可以知道,造成这个panic的原因是需要手动释放页表项。而在这里

                +
                // in proc.c  freeproc()
                if(p->usyscall)
                kfree((void*)p->usyscall);
                p->usyscall = 0;
                + +

                仅仅是释放掉了对应的物理页,页表项并没有被释放

                +

                对比了一下别人写的,才发现原来这里也需要修改:

                +
                // Free a process's page table, and free the
                // physical memory it refers to.
                void
                proc_freepagetable(pagetable_t pagetable, uint64 sz)
                {
                uvmunmap(pagetable, TRAMPOLINE, 1, 0);
                uvmunmap(pagetable, TRAPFRAME, 1, 0);
                //添加此句
                uvmunmap(pagetable, USYSCALL, 1, 0);
                uvmfree(pagetable, sz);
                }
                + +

                这样一来,问题就解决了。

                +
                总结

                因而,可以看到,如果进程想使用页的话,需要经历以下四步:

                  -
                1. a2

                  -
                2. -
                3. 被inline掉了

                  -
                4. -
                5. 0x64A

                  -

                  image-20230111224927837

                  -

                  auipc的作用是把立即数左移12位,低12位补0,和pc相加赋给指定寄存器。这里立即数是0,指定寄存器是ra,即ra=pc=0x30=48。jalr作用是跳转到立即数+指定寄存器处并且把ra的值置为下一条指令。因此jalr会跳转1562+48=1594=0x64A处,观察汇编代码可知确实在000000000000064a处。

                  -
                6. -
                7. 0x38

                  -
                8. -
                9. -

                  Run the following code.

                  -
                  unsigned int i = 0x00646c72;
                  printf("H%x Wo%s", 57616, &i);
                  +
                10. 通过kalloc获取物理页地址(可以通过该地址对页进行读写),并且记录在进程proc结构中(否则之后就获取不了了)
                11. +
                12. 建立mappages映射
                13. +
                14. 释放物理页
                15. +
                16. 释放PTE映射
                17. +
                +

                可见12和34都是分别一一对应的。

                +

                代码

                // Look in the process table for an UNUSED proc.
                // If found, initialize state required to run in the kernel,
                // and return with p->lock held.
                // If there are no free procs, or a memory allocation fails, return 0.
                static struct proc*
                allocproc(void)
                {
                struct proc *p;

                //有线程池那味了
                for(p = proc; p < &proc[NPROC]; p++) {
                acquire(&p->lock);
                if(p->state == UNUSED) {
                goto found;
                } else {
                release(&p->lock);
                }
                }
                return 0;

                found:
                p->pid = allocpid();

                // Allocate a trapframe page.
                if((p->trapframe = (struct trapframe *)kalloc()) == 0){
                release(&p->lock);
                return 0;
                }
                // Allocate a usyscall page.
                if((p->usyscall = (struct usyscall *)kalloc()) == 0){
                release(&p->lock);
                return 0;
                }
                //在USYSCALL写入usyscall结构体
                p->usyscall->pid = p->pid;

                // An empty user page table.
                p->pagetable = proc_pagetable(p);
                if(p->pagetable == 0){
                freeproc(p);
                release(&p->lock);
                return 0;
                }

                // Set up new context to start executing at forkret,
                // which returns to user space.
                memset(&p->context, 0, sizeof(p->context));
                p->context.ra = (uint64)forkret;
                p->context.sp = p->kstack + PGSIZE;

                return p;
                }

                // free a proc structure and the data hanging from it,
                // including user pages.
                // p->lock must be held.
                static void
                freeproc(struct proc *p)
                {
                if(p->trapframe)
                kfree((void*)p->trapframe);
                p->trapframe = 0;
                if(p->pagetable)
                proc_freepagetable(p->pagetable, p->sz);
                p->pagetable = 0;
                if(p->usyscall)
                kfree((void*)p->usyscall);
                p->usyscall = 0;
                p->sz = 0;
                p->pid = 0;
                p->parent = 0;
                p->name[0] = 0;
                p->chan = 0;
                p->killed = 0;
                p->xstate = 0;
                p->state = UNUSED;
                }

                // Create a user page table for a given process,
                // with no user memory, but with trampoline pages.
                pagetable_t
                proc_pagetable(struct proc *p)
                {
                pagetable_t pagetable;

                // An empty page table.
                pagetable = uvmcreate();
                if(pagetable == 0)
                return 0;

                // map the trampoline code (for system call return)
                // at the highest user virtual address.
                // only the supervisor uses it, on the way
                // to/from user space, so not PTE_U.
                if(mappages(pagetable, TRAMPOLINE, PGSIZE,
                (uint64)trampoline, PTE_R | PTE_X) < 0){
                uvmfree(pagetable, 0);
                return 0;
                }

                // map the trapframe just below TRAMPOLINE, for trampoline.S.
                if(mappages(pagetable, TRAPFRAME, PGSIZE,
                (uint64)(p->trapframe), PTE_R | PTE_W) < 0){
                uvmunmap(pagetable, TRAMPOLINE, 1, 0);
                uvmfree(pagetable, 0);
                return 0;
                }

                // 映射USYSCALL
                if(mappages(pagetable, USYSCALL, PGSIZE,
                (uint64)(p->usyscall), PTE_R|PTE_U) < 0){
                uvmunmap(pagetable, TRAMPOLINE, 1, 0);
                uvmunmap(pagetable, TRAPFRAME, 1, 0);
                uvmfree(pagetable, 0);
                return 0;
                }
                return pagetable;
                }

                // Free a process's page table, and free the
                // physical memory it refers to.
                void
                proc_freepagetable(pagetable_t pagetable, uint64 sz)
                {
                uvmunmap(pagetable, TRAMPOLINE, 1, 0);
                uvmunmap(pagetable, TRAPFRAME, 1, 0);
                uvmunmap(pagetable, USYSCALL, 1, 0);
                uvmfree(pagetable, sz);
                }
                -

                What is the output? Here’s an ASCII table that maps bytes to characters.

                -

                The output depends on that fact that the RISC-V is little-endian. If the RISC-V were instead big-endian what would you set i to in order to yield the same output? Would you need to change 57616 to a different value?

                -

                format,png

                +

                问答题

                +

                Which other xv6 system call(s) could be made faster using this shared page? Explain how.

                - -
              3. 取决于寄存器a2(第3个参数)的值。

                -
              4. -
              -

              Backtrace

              -

              For debugging it is often useful to have a backtrace: a list of the function calls on the stack above the point at which the error occurred.

              -

              image-20230111232323444

              -

              The compiler puts in each stack frame a frame pointer that holds the address of the caller’s frame pointer. Your backtrace should use these frame pointers to walk up the stack and print the saved return address in each stack frame.

              +

              我觉得如果能在fork的父子进程用shared page共享页表应该会节省很多时间和空间,用个读时写。其他的倒是想不到了。不过这题会不会问的是那些在内核态和用户态穿梭频繁的system call呢?这个的话我就想不出来了。

              +
              +

              write a function that prints the contents of a page table.

              +

              Define a function called vmprint().

              +

              It should take a pagetable_t argument, and print that pagetable in the format described below.

              +

              Insert if(p->pid==1) vmprint(p->pagetable) in exec.c just before the return argc, to print the first process’s page table.

              +

              image-20230110231020570

              +

              The first line displays the argument to vmprint. After that there is a line for each PTE, including PTEs that refer to page-table pages deeper in the tree. Each PTE line is indented by a number of " .." that indicates its depth in the tree.

              +

              Each PTE line shows the PTE index in its page-table page, the pte bits, and the physical address extracted from the PTE. Don’t print PTEs that are not valid.

              +

              In the above example, the top-level page-table page has mappings for entries 0 and 255. The next level down for entry 0 has only index 0 mapped, and the bottom-level for that index 0 has entries 0, 1, and 2 mapped.

              -
              -

              Some hints:

              -
                -
              • Add the prototype for backtrace to kernel/defs.h so that you can invoke backtrace in sys_sleep.

                -
              • -
              • The GCC compiler stores the frame pointer of the currently executing function in the register s0. Add the following function to kernel/riscv.h:

                -
                static inline uint64
                r_fp()
                {
                uint64 x;
                asm volatile("mv %0, s0" : "=r" (x) );
                return x;
                }
                +

                感想

                image-20230111000329475

                +

                很可惜,我在上面检索freewalk leaf到底是什么东西的时候,不小心看到了这题需要去参照freewalk这个提示【悲】其实我觉得这点还是需要绕点弯才能想到的,可能直接想到有点难【谁知道呢,世界线已经变动了】。

                +

                它这个打印页表其实最主要是考查如何遍历页表,这让人想起了walk这样的东西。但是walk是根据虚拟地址一级级找PTE的,中间很多地方会被跳过。有没有一个过程会在做事的时候遍历整个页表呢?答案是,这个过程就是释放页表的过程。释放页表才会一个个地看是否需要释放。释放页表的函数是freewalk,因而这道题参考freewalk的代码即可。

                +

                我觉得从“遍历页表”联想到“释放页表”这点是很巧的。不过也不会很突兀,毕竟学数据结构时就知道释放就需要遍历,逆向思维有点难但问题不大。

                +

                其他的就都挺简单的,不多赘述。

                +

                代码

                记得在defs.h中添加声明

                +
                //在vm.c下
                void
                vmprint_helper(pagetable_t pagetable,int level)
                {
                // there are 2^9 = 512 PTEs in a page table.
                for(int i = 0; i < 512; i++){
                pte_t pte = pagetable[i];
                if(pte & PTE_V){
                for(int j=0;j<level;j++){
                printf(" ..");
                }
                printf("%d: pte %p pa %p\n",i,(uint64)pte,(uint64)(PTE2PA(pte)));
                if((pte & (PTE_R|PTE_W|PTE_X)) == 0){
                // this PTE points to a lower-level page table.
                uint64 child = PTE2PA(pte);
                vmprint_helper((pagetable_t)child,level+1);
                }
                }
                }
                }

                // 打印页表
                void
                vmprint(pagetable_t pagetable)
                {
                // typedef uint64 *pagetable_t;所以pagetable可以以%p形式打印
                printf("page table %p\n",(uint64)pagetable);
                vmprint_helper(pagetable,1);
                }
                + +

                问答题

                +

                Explain the output of vmprint in terms of Fig 3-4 from the text.

                +

                What does page 0 contain?

                +

                What is in page 2? When running in user mode, could the process read/write the memory mapped by page 1?

                +

                What does the third to last page contain?

                +
                +

                从上面操作系统的启动来看,进程1应该是在main.c中的userinit()中创建的进程,也是shell的父进程。【确实,经实践可得shell的pid为2】

                +

                可以来看一下userint的代码:

                +
                void
                userinit(void)
                {
                struct proc *p;

                p = allocproc();
                initproc = p;

                // 申请一页,将initcode的指令和数据放进去
                // allocate one user page and copy initcode's instructions
                // and data into it.
                /*
                uvminit的注释:
                // Load the user initcode into address 0 of pagetable,
                // for the very first process.
                // sz must be less than a page.
                */
                uvminit(p->pagetable, initcode, sizeof(initcode));
                p->sz = PGSIZE;

                //为内核态到用户态的转变做准备
                // prepare for the very first "return" from kernel to user.
                /*
                Trap Frame是指中断、自陷、异常进入内核后,在堆栈上形成的一种数据结构
                */
                p->trapframe->epc = 0; // user program counter
                p->trapframe->sp = PGSIZE; // user stack pointer

                // 修改进程名
                safestrcpy(p->name, "initcode", sizeof(p->name));
                p->cwd = namei("/");

                //这个也许是为了能被优先调度
                p->state = RUNNABLE;

                release(&p->lock);
                }
                + +

                可见,page0是initcode的代码和数据,page1和page2用作了进程的栈,其中page1应该是guard page,page2是stack。

                +

                不过这里从exec的角度解释其实更通用

                +
                int
                exec(char *path, char **argv)
                {
                //分配新页表
                if((pagetable = proc_pagetable(p)) == 0)
                goto bad;

                //elfhd应该指的是可执行文件头
                // Load program into memory.
                for(i=0, off=elf.phoff; i<elf.phnum; i++, off+=sizeof(ph)){
                //...
                //总之顺利读到了
                uint64 sz1;
                //读到了就给它分配新空间并且填入页表
                if((sz1 = uvmalloc(pagetable, sz, ph.vaddr + ph.memsz, flags2perm(ph.flags))) == 0)
                goto bad;
                sz = sz1;
                }

                //读完文件,开始造一个新的用户栈【fork之后用户栈是不会清空的】
                sz = PGROUNDUP(sz);
                uint64 sz1;
                if((sz1 = uvmalloc(pagetable, sz, sz + 2*PGSIZE, PTE_W)) == 0)
                goto bad;
                sz = sz1;
                // mark a PTE invalid for user access.造guard page
                uvmclear(pagetable, sz-2*PGSIZE);
                // sp为栈顶
                sp = sz;
                // 应该指的是栈尾
                stackbase = sp - PGSIZE;
                //...
                }
                -

                and call this function in backtrace to read the current frame pointer. This function uses in-line assembly to read s0.

                -
              • -
              • These lecture notes have a picture of the layout of stack frames. Note that the return address lives at a fixed offset (-8) from the frame pointer of a stackframe, and that the saved frame pointer lives at fixed offset (-16) from the frame pointer.

                -
              • -
              • Xv6 allocates one page for each stack in the xv6 kernel at PAGE-aligned address. You can compute the top and bottom address of the stack page by using PGROUNDDOWN(fp) and PGROUNDUP(fp) (see kernel/riscv.h. These number are helpful for backtrace to terminate its loop.

                -
              • -
              +

              page0就填程序。这里重点说明一下为什么page1和page2分别是guard page和stack。

              +

              按照它的那个算术关系,stack和guard page的虚拟内存位置关系应该是这样的:

              +

              image-20230111004330079

              +

              那为什么最后在页表中,变成了page1是gurad page,page2是stack这样上下颠倒了呢?看vm.c中的uvmalloc就能明白。

              +

              image-20230111004500827

              +

              在253行设置了新映射。可以看到,这里设置映射的顺序是sz->sz+PGSIZE,也即先设置guard page的映射,再设置stack的映射。所以,这两位才会上下颠倒了。

              +

              Detecting which pages have been accessed

              +

              Some garbage collectors (a form of automatic memory management) can benefit from information about which pages have been accessed (read or write). In this part of the lab, you will add a new feature to xv6 that detects and reports this information to userspace by inspecting the access bits in the RISC-V page table. The RISC-V hardware page walker marks these bits in the PTE whenever it resolves a TLB miss.

              -

              感想

              我超,这题真的是那怎能只叫一个拷打……

              -
              存储在s0中的栈帧指针

              这个应该是risc-v的约定成俗的特性。我搜了一下risc-v的栈帧指针保存在哪个寄存器,看到了这样一篇文章:

              -

              risc-v 栈分析

              -

              image-20230112012358601

              +

              Your job is to implement pgaccess(), a system call that reports which pages have been accessed.

              +

              The system call takes three arguments. First, it takes the starting virtual address of the first user page to check. Second, it takes the number of pages to check. Finally, it takes a user address to a buffer to store the results into a bitmask (a datastructure that uses one bit per page and where the first page corresponds to the least significant bit).

              +

              You will receive full credit for this part of the lab if the pgaccess test case passes when running pgtbltest.

              -

              这个信息没有放在题干提示,是在考察信息检索能力吗(

              -
              栈的结构与栈帧的理解

              image-20230112010749756

              -

              这是来自hint的栈结构。整个栈存储在一页中,由高地址向低地址增长。栈帧代表了一次函数调用,其中会存储如函数名、函数参数、局部变量等等信息。有几次函数调用就有几个栈帧,栈由栈帧组成。

              -

              s0中存储的栈帧指针fp指向的是栈帧的最高地址,如图fp所示。

              +

              感想

              实验内容:

              +

              实现void pgaccess(uint64 sva,int pgnum,int* bitmask);,一个系统调用。在这里面,我们要做的是,访问从svasva+pgnum*PGSIZE这一范围内的虚拟地址对应的PTE,然后查看PTE的标记项是否有PTE_A。有的话则在bitmask对应位标记为1.

              +

              应该注意的点:

              +

              1.需要进行内核态到用户态的参数传递 2.需要进行系统调用的必要步骤 3.PTE_A需要自己定义

              +

              以上是初见。做完了发现,确实就是那么简单,我主要时间花费在下的实验版本不对,折腾来折腾去了可能有一个小时,最后还是选择了直接把测试函数搬过来手工调用。已经换到正确的年份版本了【泪目】

              +

              有一点我忽视了,看了提示才知道:

              -

              我理解错了栈帧的定义,都怪我基础不大牢固也不认真思考【悲】我一开始以为stack frame指的是一个栈,也即一页空间【我知道栈帧这个中文名词,但遇到英语就短路了】。老师画的这个图也被我理解为多个栈,也即多页拼在一起,要打印的Return Address处于页的最顶部。我就在这个思路上一去不复返了,压根没有意识到一个进程只有一个栈【大悲】然后顺带脑补把r_fp()也曲解了,以为它的意思是读取当前栈【非常自然地认为有很多个栈←】的下一个栈的最低地址【因为栈换掉了,所以s0也会变成父亲的栈的地址】。于是就写出了这样的代码:

              -
              void
              backtrace(){
              printf("backtrace:\n");
              uint64 kstack = PGROUNDUP((uint64)(myproc()->kstack)+1);
              uint64 nstack = 0;
              while((nstack=(uint64)r_fp())!=0){
              printf("%p\n",*((uint64*)(kstack-8)));
              kstack = nstack;
              }
              }
              - -

              结果最后死循环了。去看了别人的代码发现他们写的结构就跟我完全不一样。琢磨着画着图,最后找了stack frame的定义,才恍然大悟(

              -
              -
              思路形成

              我们只需遍历栈中所有栈帧,打印每个栈帧的Return Address部分就行。通过r_fp()获取第一个栈帧的位置,其他栈帧的位置由Prev.Frame获取。循环的界限是PGROUNDUP(r_fp()),因为栈只有一页的空间。

              -

              代码

              void
              backtrace(){
              printf("backtrace:\n");
              uint64 stack = r_fp();
              uint64 nstack = 0;
              uint64 top = PGROUNDUP(stack);
              while(stack!=top){
              nstack=*((uint64*)(stack-16));
              printf("%p\n",*((uint64*)(stack-8)));
              stack = nstack;
              }
              }
              - -

              Alarm

              -

              In this exercise you’ll add a feature to xv6 that periodically alerts a process as it uses CPU time. This might be useful for compute-bound processes that want to limit how much CPU time they chew up, or for processes that want to compute but also want to take some periodic action.

              -

              More generally, you’ll be implementing a primitive form of user-level interrupt/fault handlers; you could use something similar to handle page faults in the application, for example.

              -

              You should add a new sigalarm(interval, handler) system call. If an application calls sigalarm(n, fn), then after every n “ticks” of CPU time that the program consumes, the kernel should cause application function fn to be called. When fn returns, the application should resume where it left off.

              +

              Be sure to clear PTE_A after checking if it is set. Otherwise, it won’t be possible to determine if the page was accessed since the last time pgaccess() was called (i.e., the bit will be set forever).

              -

              感觉从alarm中可以窥见信号的实现思路:

              -

              image-20231218164244526

              -

              而alarm的机理感觉也有点类似。用户先通过sigalarm注册定时函数,内核在时钟中断的时候对该标记位进行检查,然后去do_signal回到用户态执行用户的signal handler,再通过sigreturn回到用户模式。sigalarm相当于一个信号注册函数,sigreturn也就是上图的sigreturn。

              -

              分析

              初见思路

              思路:sigalarm需要在用户程序在用户态运行的情况下,监测到用户程序已经运行了n个时间片,然后发出中断请求。我们会新设置一个中断类型alarm。kerneltrap接收到sigalarm的中断请求,检测到中断类型为alarm,就会在处理的时候调用fn。fn调用完就自然而然利用中断恢复到原来的现场了。所以要做的可以分为两部分。但问题是,如何让sigalarm在用户程序运行的同时监测n个时间片呢?难道得fork一个新的进程吗?然后父进程返回,子进程执行类似sleep里面那样的监测,直到时间片到了,就发送一个中断请求,让父进程停止,执行完fn回来之后就exit。

              -
              正确思路

              可以看到,初见思路很多地方跟最后是不一样的。其中错得最离谱的,也是比较隐坑很容易因为想不明白就寄了的,是handler是个用户态的函数(。你不可能在内核态中调用fn,然后fn执行完后再自然而然地通过中断机制返回,因为你想要执行fn就必须进入用户态。这一点是需要一开始明确的。

              -

              明确了这一点后,让人更加不知道该怎么办了。那就一步步跟着指导书的脚步来思考吧。

              -
              part 1

              首先,明确你需要实现什么。你需要实现两个系统调用,一个是sigalarm,一个是sigreturn。结合提示,可知实验设计者给我们的思路是,通过sigalarm设置定时函数,通过sigalarm(0,0)取消定时函数。每次时钟中断检测当前定时时间是否达到,若已达到,则跳到定时函数执行。定时函数执行完后,需要借助sigreturn,才能正确返回时钟中断前的程序点。

              -
              part 2

              这又可拆解为几个要点:

              -
                -
              1. 如何实现“定时”?
              2. -
              3. 时钟中断在内核态的usertrap被检测。怎么从usertrap出来跳到定时函数而非原程序执行点?
              4. -
              5. 执行完定时函数后,怎么样才能回到原程序执行点?
              6. -
              -
              part 3

              一个个来说,首先是如何实现定时。这个很简单。参照sys_sleep的代码:

              -
              uint64
              sys_sleep(void)
              {
              int n;
              uint ticks0;

              backtrace();
              if(argint(0, &n) < 0)
              return -1;
              acquire(&tickslock);
              ticks0 = ticks;
              while(ticks - ticks0 < n){
              if(myproc()->killed){
              release(&tickslock);
              return -1;
              }
              sleep(&ticks, &tickslock);
              }
              release(&tickslock);

              return 0;
              }
              +

              也就是说每次检查到一个,就需要手动清除掉PTE_A标记。

              +

              还有一点以前一直没注意到的,头文件的引用需要注意次序。比如说要是把spinlock.h放在proc.h后面,就会寄得很彻底。

              +

              代码

              那些系统调用的登记步骤就先省略了。

              +
              // kernel/sysproc.c
              uint64
              sys_pgaccess(void)
              {
              uint64 sva;
              int pgnum;
              uint64 bitmask;

              if(argaddr(0,&sva) < 0 || argint(1, &pgnum) < 0 || argaddr(2, &bitmask) < 0)
              return -1;
              return pgaccess((void*)sva,pgnum,(void*)bitmask);
              }
              -

              可知,我们可以用ticks表示当前系统滴答数。这样,我们就可以在proc域里维护一个变量lasttick,记录上一次执行handler时的滴答数。每次在时钟中断时检测,所以需要写在kernel/trap.c中的usertrap中。

              -
              part 4

              然后,是怎么从usertrap出来跳到指定程序结束点。在书中,我们知道,sepc寄存器保存了中断前原程序的下一个执行点,sepc的备份存储在了proc域中的栈帧中。当中断返回时(在usertrapret中),会从栈帧中的epc字段读取sepc的备份赋值给sepc,再由sret帮助我们跳转到原程序点。因而,如果想要改变跳转点,我们只需要修改p->trapframe->epc就行。

              -
              part 5

              最后,是如何从periodic回到原程序执行点。

              -

              image-20230113002057893

              -

              这是每次进行时钟中断时的栈情况和执行代码链:t1->trampoline->usertrap->handler。

              -

              再然后,handler调用了sigreturn,用户栈中就会产生sigreturn的栈帧:

              -

              image-20230113002434093

              -

              此时,如果sigreturn执行完,就会在这样的情况下执行handler的ret指令:

              -

              image-20230113002542335

              -

              ret指令会把栈帧弄走,也就是说会直接回到某个错误的地方去。这显然不大合适。所以,我们要做的,就是在sigreturn之后,不执行handler的ret指令,也不执行sigreturn的ret指令,而是直接恢复到时钟中断前的上下文。时钟中断前的上下文,会因在handler中调用sigreturn系统调用,而被覆盖,因而,我们就需要记录时钟中断前的上下文,也即在proc域中保存trapframe的一份拷贝savedtrap,每次时钟中断都更新一次savedtrap,然后在sigreturn调用的时候将proc原本的trapframe替换为savedtrap即可。这样一来,就完成了这道题。

              -
              感想

              这题目确实最终代码看起来完全不难,但是非常地拷打。。。。我前前后后修修补补差不多一共花了五个小时之久。

              -

              计时怎么计,以及使用trapframe->epc来跳转这两点还是很容易想到的【话虽如此,其实也很曲折】。主要难点还是在怎么恢复现场。怎么说呢,我花了这么久做实验,但是实际笔记却写不出个鬼来,足以看出其复杂程度。

              -

              我主要还是思维固化了点,一直在想,怎么确保它正确返回现场。我一开始以为proc域保存一个寄存器状态,且只用在一开始设置定时函数也即sigalarm的时候保存一次就行了,并且认为其是epc。然而实际操作后发现usertrap崩了,并且epc中存储的并不是程序被时钟中断的地方,而是各种神奇的地方,具体我也忘了,反正不能行。我印象最深刻的是有一次停在了usys.S中的sigreturn的最后一个ret处。我就在想,也许是栈出了问题。于是我就想着直接在sigreturn的时候把epc指向栈帧中的return address,直接回到原执行段。我百度了一下,确实有这么个寄存器ra,存储着return address。于是我就把proc域的状态换成了ra,依然仅保存一次,最后发现还是不行,程序在test0之后就异常终止了,main也回不去,十分古怪,十分匪夷所思。我实在没忍住,百度了一下大家怎么做的,发现大家压根没有我这样的二选一的烦恼,是直接保存整个栈帧。而且也不是仅保存一次,而是每次时钟中断触发都保存一次。我觉得十分奇怪震惊,但此时已是差不多晚饭时间,我就先去吃了个饭()

              -

              回来之后,我细细画了图【向正确思路part5中的那样】,发现我原来那个只保存两者之一,且都只保存一次的方式,确实完全不能行。但是,我发现两个一起保存,并且每次时钟中断保存的方法,似乎能行,而且,比保存一整个栈帧要聪明得多。于是我就去试了,发现还是不行。我再细想了一遍,发现,如果想回去原程序的现场,除了ra和epc,还有一个很重要的东西需要保存,那就是——用户栈指针sp!

              -

              也就是说,只需保存ra、epc、sp,就可以保证回到正确的时钟中断前的位置

              -

              image-20230113005100895

              -

              此为handler中sigreturn执行完要返回时的状态。

              -

              当处在handler中时,sp的值为sigreturn处的栈帧。执行系统调用时,proc域中的上下文被覆盖,也即时钟中断前的上下文被覆盖。如果此时不对栈帧中的sp进行恢复,仅恢复ra和epc,在从sigreturn返回到epc对应处也即t1,t1执行ret想回到main的时候,就会回不去,而是回到了sigreturn要回的位置,也即handler的位置,然后不知不觉就寄了。所以,就需要防止sp被覆盖。因而,再保存一个状态sp,就可以保障回到正确的地方了。测试出来,kernel确实不再会panic了。

              -

              但是由于运行时很多除这三个以外的寄存器都被改过了,回是回得去,接下来干的活就不一定对了。因此为了保险以及通用性以及便利性来看,还是像别人那样直接保存栈帧比较ok。

              -

              还有一件事,就是上述错误中经常会出现的一个输出结果:

              -
              usertrap:unexpected cause scause = 0x0c
              +
              // kernel/pgaccess.c
              #include "types.h"
              #include "param.h"
              #include "memlayout.h"
              #include "riscv.h"
              #include "spinlock.h"
              #include "defs.h"
              #include "proc.h"
              int
              pgaccess(void* sva,int pgnum,void* bitmask){
              if(pgnum > 32){
              printf("pgaccess: range too big.\n");
              exit(1);
              }
              int kmask = 0;
              struct proc* p = myproc();
              for(int i=0;i<pgnum;i++){
              pte_t* pte = walk(p->pagetable,(uint64)sva+i*PGSIZE,0);
              // 映射不存在,或者没有被访问过
              if(!pte || !(*pte & PTE_A)){
              continue;
              }
              kmask = (kmask | (1<<i));
              *pte = (*pte & (~PTE_A));
              }
              copyout(p->pagetable,(uint64)bitmask,(char*)(&kmask),sizeof(int));
              return 1;
              }
              -

              我留意了一下是什么意思。网上搜索得,scause=12,说明这是一个instruction page fault,而这个缺页错误说明了什么?:

              -

              image-20230113012355740

              -

              这样,一切都明朗了。出现了scause=0x0c的意思就是说pc里的值不恰当,也就是说上面错误的方法都会跳转到错误的地方去。

              -

              Lab:xv6 lazy page allocation

              -

              参考:https://blog.csdn.net/m0_53157173/article/details/131349366

              +

              A kernel page table per process

              +

              The goal of this section and the next is to allow the kernel to directly dereference user pointers.

              -

              来自书本:

              -

              Another widely-used feature is called lazy allocation, which has two parts:

              -
                -
              1. First, when an application calls sbrk, the kernel grows the address space, but marks the new addresses as not valid in the page table.
              2. -
              3. Second, on a page fault on one of those new addresses, the kernel allocates physical memory and maps it into the page table.
              4. -
              -

              The kernel allocates memory only when the application actually uses it.

              -
              -

              Eliminate allocation from sbrk()

              -

              Your first task is to delete page allocation from the sbrk(n).

              -

              The sbrk(n) system call grows the process’s memory size by n bytes, and then returns the start of the newly allocated region (i.e., the old size). Your new sbrk(n) should just increment the process’s size (myproc()->sz) by n and return the old size. It should not allocate memory – so you should delete the call to growproc() (but you still need to increase the process’s size!).

              +

              Your first job is to modify the kernel so that every process uses its own copy of the kernel page table when executing in the kernel.

              +

              Modify struct proc to maintain a kernel page table for each process, and modify the scheduler to switch kernel page tables when switching processes. For this step, each per-process kernel page table should be identical to the existing global kernel page table. You pass this part of the lab if usertests runs correctly.

              -
              uint64
              sys_sbrk(void)
              {
              int addr;
              int n;

              if(argint(0, &n) < 0)
              return -1;
              int sz = myproc()->sz;
              addr = sz;
              myproc()->sz = sz + n;
              //if(growproc(n) < 0)
              // return -1;
              return addr;
              }
              +

              感想

              这个其实平心而论不难,思路很简单。写着不难是不难,但想明白花费了我很多时间。

              +

              它这个要求我们修改kernel,使得每个进程都有一份自己的kernel page。至于要改什么,围绕着proc.c中,参照pagetable的生命周期摁改就行。还有一个地方它也提示了,就是要在swtch之前更换一下satp的值。

              +

              接下来,我说说我思考的几个点以及犯错的地方。

              +
              为什么要这么干

              看完题目,我的第一印象是,这么干有啥用。。。因为我觉得以前那个所有进程共用内核页表确实很好了,没有必要每个进程配一个后来才发现,这个跟下面那个是连在一起的,目的是 allow the kernel to directly dereference user pointers.。所以,我们下面会把用户的pgtbl和这里dump出来的kpgtbl合在一起。

              +

              具体来说:

              +

              通常,进行地址翻译的时候,计算机硬件(即内存管理单元MMU)都会自动的查找对应的映射进行翻译(需要设置satp寄存器,将需要使用的页表的地址交给该寄存器)。

              +

              然而,在xv6内核需要翻译用户的虚拟地址时,因为内核页表不含对应的映射,计算机硬件不能自动帮助完成这件事。因此,我们需要先找到用户程序的页表,仿照硬件翻译的流程,一步一步的找到对应的物理地址,再对其进行访问。walkaddr】这也就会导致copyin之类需要涉及内核和用户态交互的函数效率低下。

              +

              为了解决这个问题,我们尝试将用户页表也囊括进内核页表映射来。但是,如果将所有进程的用户页表都合并到同一个内核全局页表是不现实的。因而,我们决定换一个角度,让每个进程都仅有一张内核态和用户态共用的页表,每次切换进程时切换页表,这样就构造出了个全局的假象。

              +

              这两次实验就是为了实现该任务。在本次实验中,我们首先先实现内核页表的分离。

              +
              关于myproc()

              在allocproc中初始化的时候,我一开始是这么写的:

              +
              // in proc.c allocproc()
              perproc_kvminit();
              -

              Lazy allocation

              -

              Modify the code in trap.c to respond to a page fault from user space by mapping a newly-allocated page of physical memory at the faulting address, and then returning back to user space to let the process continue executing.

              -

              You should add your code just before the printf call that produced the “usertrap(): …” message. Modify whatever other xv6 kernel code you need to in order to get echo hi to work.

              +
              // in vm.c
              pagetable_t
              perproc_kvminit()
              {
              struct proc* p = myproc();
              p->kpgtbl = (pagetable_t) kalloc();
              memset(p->kpgtbl, 0, PGSIZE);

              // uart registers
              pkvmmap(p->kpgtbl,UART0, UART0, PGSIZE, PTE_R | PTE_W);
              // ...
              return pt;
              }
              + +

              这样会死得很惨,爆出如下panic:

              +

              image-20230114011100370

              +

              通过hints的调试贴士

              +
              +

              A missing page table mapping will likely cause the kernel to encounter a page fault. It will print an error that includes sepc=0x00000000XXXXXXXX. You can find out where the fault occurred by searching for XXXXXXXX in kernel/kernel.asm.

              -

              感想

              思路

              首先,要知道缺页中断的scause为13或15.【论我怎么知道的:被以前的实验逼出来的hhh】然后,要写在if条件的第二个分支。在该分支内,我们需要先获取出问题的地方的虚拟地址的值,然后申请新的一页,再map到当前页表中。

              -
              一个难以察觉的错误
              描述

              思路是很简单的,就是有小细节需要格外注意。

              -

              trap.c在mappages时,一定不能直接传入va,必须传入PGROUNDDOWN(va)。如果直接传入va,会爆出如下错误:

              -

              image-20230116154004538

              -

              但是,查看mappages的代码:

              -
              int
              mappages(pagetable_t pagetable, uint64 va, uint64 size, uint64 pa, int perm)
              {
              uint64 a, last;
              pte_t *pte;

              a = PGROUNDDOWN(va);
              last = PGROUNDDOWN(va + size - 1);
              for(;;){
              if((pte = walk(pagetable, a, 1)) == 0)
              return -1;
              if(*pte & PTE_V)
              panic("remap");
              *pte = PA2PTE(pa) | perm | PTE_V;
              if(a == last)
              break;
              a += PGSIZE;
              pa += PGSIZE;
              }
              return 0;
              }
              +

              我发现程序在这里绷掉了:

              +
              p->kpgtbl = (pagetable_t) kalloc();
              -

              我们可以看到,它在里面已经对va进行了处理了,使它变成了page-align的a变量。那么为什么,我们还要在外面再对va处理一次呢?

              -

              其实问题不是出在mappages中的a变量上,而是出现在mappages中的last变量上。比如,令va=PGSIZE+200,则a=PGSIZE,last=2*PGSIZE。这样一来,在下面的循环中,除了添加了刚刚申请的那页的映射以外,我们还多添加了新的一页,其物理地址为mem+PGSIZE。

              -

              这十分地危险!假设你要申请的va为proc->size的最后一页,那么,经过本次缺页中断之后,你事实上申请了两页,两页的地址分别为va和va+PGSIZE。而va+PGSIZE大于proc->size。也就是说,地址溢出了!

              -

              这会导致页表释放的时候出问题。以下是页表释放的路径。

              -
              // in proc.c
              void
              proc_freepagetable(pagetable_t pagetable, uint64 sz)
              {
              uvmunmap(pagetable, TRAMPOLINE, 1, 0);
              uvmunmap(pagetable, TRAPFRAME, 1, 0);
              uvmfree(pagetable, sz);
              }

              // in vm.c
              void
              uvmfree(pagetable_t pagetable, uint64 sz)
              {
              if(sz > 0)
              uvmunmap(pagetable, 0, PGROUNDUP(sz)/PGSIZE, 1);
              freewalk(pagetable);
              }

              // in vm.c
              void
              uvmunmap(pagetable_t pagetable, uint64 va, uint64 npages, int do_free)
              {
              uint64 a;
              pte_t *pte;

              if((va % PGSIZE) != 0)
              panic("uvmunmap: not aligned");

              for(a = va; a < va + npages*PGSIZE; a += PGSIZE){
              if((pte = walk(pagetable, a, 0)) == 0){
              printf("uvmunmap walk: %p\n",a);
              continue;
              }
              if((*pte & PTE_V) == 0){
              printf("uvmunmap: %p\n",a);
              continue;
              }
              printf("uvmunmap OK: %p\n",a);
              if(PTE_FLAGS(*pte) == PTE_V)
              panic("uvmunmap: not a leaf");
              if(do_free){
              uint64 pa = PTE2PA(*pte);
              kfree((void*)pa);
              }
              *pte = 0;
              }
              }
              +

              而且显而易见,是系统启动时崩的。

              +

              经过了漫长的思考,我震惊地发现了它为什么崩了()

              +

              首先,这段代码语法上是没有问题的。它固然犯了发布未初始化完成的对象这样的并发错误【我有罪】,也破坏了proc的封装性【proc中的很多私有属性本来应该作用域仅在proc.c中的。此处为了能让vm.c访问到proc中的属性,不得不给vm.c添上了proc.h的头文件】,但是它并不是语法错误,还是能用的。我做了这样的测试样例证明它没有问题:

              +
              #include <stdio.h>
              #define MAX 10
              typedef int pagetable_t;

              struct proc{
              pagetable_t kpgtbl;
              };

              struct proc processes[MAX];

              struct proc* myproc(){
              return &processes[0];
              };

              void kvminit(){
              myproc()->kpgtbl = 1;
              }

              int main(){
              struct proc* p = &processes[0];
              kvminit();
              printf("%d",p->kpgtbl);
              return 0;
              }
              -

              freewalk要求在uvmunmap中已经释放完所有的叶子结点。而由于uvmunmap中释放结点的va是从0递增到proc->size的,也因而,前面的那个大于proc->size的那页虽然还在页表中存在,但是不会被uvmunmap释放!这也就导致,接下来调用freewalk的时候,会发现该页的叶子结点仍然存在,从而导致freewalk: leaf

              -

              可以结合uvmunmap和trap.c中的调试语句看下图的过程,可以看到非常清晰明了,0x14000这一页并没有在uvmunmap中释放!

              -

              trap.c中的调试语句:

              -
              printf("trap: %p,+PGSIZE = %p\n",PGROUNDDOWN(va),PGROUNDDOWN(va)+PGSIZE);
              if(mappages(p->pagetable,va, PGSIZE, (uint64)mem, PTE_W|PTE_X|PTE_R|PTE_U) != 0){
              kfree(mem);
              }
              +

              我一路顺着os启动的路径找,也想不出来这能有什么错,因而非常迷茫。

              +

              此时我灵光一闪,会不会是myproc()在os刚启动的时候是发挥不了作用的?于是我一路顺着myproc的代码看下去:

              +
              struct proc*
              myproc(void) {
              push_off();
              struct cpu *c = mycpu();
              struct proc *p = c->proc;
              pop_off();
              return p;
              }
              -

              图:

              -

              image-20230116165910356

              -
              debug过程

              看到freewalk: leaf这一错误,很容易联想到跟页表的释放有关。并且加上PGGROUNDDOWN就没问题,不加上才有问题,也很容易联想到跟mappages中多申请的那一页有关。但是具体是什么关系,这一点想要想到对我来说还是非常曲折的。

              -

              我一开始,以为是因为多申请的那一页(下面简称为B页好了)很有可能是其他进程在使用的,然后其他进程在echo进程释放页表前释放了页表,从而导致B页已经free了,这样一来uvmunmap说不定就能监测到对应物理页已经free,然后爆出panic。我一开始认为uvmunmap的这句话是用来监测物理页是否free的:

              -
              if((*pte & PTE_V) == 0){
              continue;
              //panic("uvmunmap: not mapped");
              }
              +

              那么,mycpu()获得的cpu的proc是怎么得到的呢?

              +

              我搜寻了一下os启动代码,发现了cpu的proc得到的路径。

              +
              void
              main()
              {
              if(cpuid() == 0){
              consoleinit();
              printfinit();
              printf("\n");
              printf("xv6 kernel is booting\n");
              printf("\n");
              //...很多很多init
              userinit(); // first user process
              __sync_synchronize();
              started = 1;
              } else {
              // ...
              }

              //调度执行第一个进程
              scheduler();
              }
              -

              然后顺理成章地,这边条件==0成立,然后continue,然后直到freewalk才发现该pte未释放。

              -

              但是,我仔细过脑子想了想,发现,就算物理页已经free了,但是,*pte依然存在,PTE_V也依然为1,这个条件是不成立的。也就是说,B页不会continue,而是会继续下面的正常释放的流程。也就是说,B页是可以正常释放的,我们的“B页已经free导致uvmunmap释放失败”的推论是错误的。

              -

              但究竟是为什么呢?肯定跟B页有关系,但是又不是这种关系,这让我十分地苦恼且烦躁,于是我就去打了会儿游戏。边玩的时候突然注意到一件非常可疑的事情。

              -

              image-20230116154004538

              -

              这是发生错误时退出的截图。有一个点引起了我的注意,就是echo hi并没有打印hi在console上。也就是说,这个panic是在echo执行前产生的!那么这个执行前是在哪呢?答案就是在exec中!

              -
              // in exec.c
              // Commit to the user image.
              oldpagetable = p->pagetable;
              p->pagetable = pagetable;
              p->sz = sz;
              p->trapframe->epc = elf.entry; // initial program counter = main
              p->trapframe->sp = sp; // initial stack pointer
              // 这里!!!!!
              proc_freepagetable(oldpagetable, oldsz);
              +

              创建完进程后,就进入scheduler进行进程的调度:

              +
              void
              scheduler(void)
              {
              struct proc *p;
              struct cpu *c = mycpu();
              // ...
              int found = 0;
              for(p = proc; p < &proc[NPROC]; p++) {
              // ...
              //在这里!!!!
              c->proc = p;
              swtch(&c->context, &p->context);

              c->proc = 0;
              // ...
              -

              可以看到,这里调用了proc_freetable,从而跟freewalk有了联系。

              -

              但是,如果我们还坚持是因为B页错的,就需要找到一个可能会产生B页的地方,也就是验证shell准备执行echo命令,fork出一个子进程之后,又在exec free页表前,已经调用过sbrk函数,并且已经触发过缺页中断。这个验证其实很简单,只需要找sbrk在哪被调用过,哪边使用过heap内存【也即哪边涉及了指针赋值】就行了。

              -

              通过全局搜索,可知sbrk在user/umalloc.c下的malloc()被使用过,而在user/sh.c中,fork子进程之后:

              -
              if(fork1() == 0)
              runcmd(parsecmd(buf));
              +

              因而,c->proc是在创建进程的第一次调度后初始化的,也即,myproc只有在执行第一次scheduler之后才可以调用。而!!!

              +

              当执行调度前的userinit时:

              +
              void
              userinit(void)
              {
              struct proc *p;

              p = allocproc();
              initproc = p;
              -

              这其中的parsecmd()中malloc被使用过,并且发生了指针赋值!!也就是说,“是因为B页错的”这个结论是对的。

              -

              虽然这一段debug没有改变我们要证明“是B页在释放内存中出错的”的这个目的,但是确实带给了我很多这种执行时申请内存的知识,并且也让我突然想起了可以用printf debug。于是,我就去做了上面那个在trap.c中和uvmunmap中printf的调试语句,最终成功发现了结论。

              -

              实在是太艰苦了()这告诉我们以后千万千万要注意,是否需要用到PGGROUNDDOWN。

              -
              一个漏掉未考虑的细节

              摘自https://blog.csdn.net/m0_53157173/article/details/131349366

              -

              image-20231209011856305

              -

              image-20231209011932500

              -

              不过我以前好像是有考虑到这个的,但是我是这么做的:

              -

              image-20231209015238219

              -

              也就是相当于把它在addr parse的那段代码移进了walkaddr中。但是这样是不行的,查找可知argaddr的应用范围可比walkaddr广得多……

              -

              而为什么我下面的COW也是修改了walkaddr,而非修改argaddr,就可以达到同样的效果呢?这是因为cow只需对在内核中写用户页这种情况进行特殊处理,而这只有一个情况,也即只在copyout中发生。因而,我们只需修改walkaddr,就可以完全防范该情况了。

              -

              Lazytests and Usertests

              -

              We’ve supplied you with lazytests, an xv6 user program that tests some specific situations that may stress your lazy memory allocator. Modify your kernel code so that all of both lazytests and usertests pass.

              -
              -

              感想

              一个绷不住的错误

              其实很简单,按照提示一步步做就行了。为什么我做得那么久那么崩溃呢?知道原因后我都笑嘻了。

              -

              在第一步修改sys_sbrk()的时候,我一下子没多想,使用了一句int sz = myproc()->sz,其实本来应该使用uint64的,使用int会溢出。这个伏笔就一直隐含到这里,然后大坑了我一笔。

              -

              一开始是发现lazytests的第二个,也就是oom过不去。我想了很久,也去网上找了别人的代码一步步对比下来看了,没有发现特别大的问题。于是我就在walk和sys_sbrk分别留下了调试信息:

              -
              // in walk()
              if(va >= MAXVA){
              printf("walk:va=%p,p->sz=%p,MAXVA=%p,pgva=%p\n",va,myproc()->sz,MAXVA,PGROUNDDOWN(va));
              panic("walk");
              }
              +

              它进行了allocproc。我们亲爱的allocproc接下来就会调用perproc_kvminit,然后perproc_kvminit中调用myproc。此时尚未进行初次调度,因而c->proc未初始化,myproc返回的是0,也即null。这样一来,myproc()->kpgtbl就发生了空指针异常,也即scause = 15——写入页错误。

              +

              因而,对于myproc()的调用需要慎之又慎。

              +
              系统调用

              系统调用时,是如何知道要用的是p中的内核页表而非global内核页表呢?

              +

              依然还是从os的启动说起。

              +

              在main.c中,kvminithart开启了页表,此时的页表为全局的内核页表:

              +
              // Switch h/w page table register to the kernel's page table,
              // and enable paging.
              void
              kvminithart()
              {
              w_satp(MAKE_SATP(kernel_pagetable));
              sfence_vma();
              }
              -
              // in sys_sbrk()
              if(n >= 0){
              uint64 tmp = n + sz;
              if(tmp > MAXVA || n + sz < n) return -1;
              myproc()->sz = tmp;
              //printf("haha sb!sz=%p,n=%p\n",myproc()->sz,n);
              }
              +

              当userinit被调度时,全局的内核页表被换成了proc中的内核页表:

              +
              // in proc.c scheduler()
              p->state = RUNNING;
              w_satp(MAKE_SATP(p->kpgtbl));
              sfence_vma();
              c->proc = p;
              swtch(&c->context, &p->context);
              -

              然后发现了这样的输出:

              -

              image-20230116225124363

              -

              可以看到,最后一次sz发生了数值溢出。

              -

              但是,此时我并没有悔改。我反而认为,“原本代码就是这么写的”。也就是说,我认为int sz是它原本内核代码给的。。。。。。在这样的情况下,我选择加上这样的条件判断:

              -
              if(tmp > MAXVA || ((tmp >> 31)& 1) == 1)       return -1;
              +

              但是这样还没有结束。因为我们除了得更换目前的页表,还得更换trapframe中的内核页表相关的东西:

              +
              struct trapframe {
              /* 0 */ uint64 kernel_satp; // kernel page table
              /* 8 */ uint64 kernel_sp; // top of process's kernel stack
              }
              -

              之后确实没有溢出了,但是test fail了。此时我想,为什么非要用int而不用uint64呢?一阵令人不寒而栗的预感袭来,我连忙去看了proc.h里的sz的定义,发现,sz原本就应该是uint64类型的,是我错辣【悲】

              -

              只能说起到一种很好的教训。主要是这种问题实在没有想过自己会犯

              -

              代码

              -
                -
              • Handle the parent-to-child memory copy in fork() correctly.
              • -
              -
              -
              // in vm.c uvmcopy()
              if((pte = walk(old, i, 0)) == 0)
              continue;
              //panic("uvmcopy: pte should exist");
              if((*pte & PTE_V) == 0)
              continue;
              //panic("uvmcopy: page not present");
              +

              为啥还要更换trapframe中的呢?因为以后系统调用的时候,uservec是从这里读取值来作为内核栈和内核页表的来源的:

              +
              # in uservec
              # restore kernel stack pointer from p->trapframe->kernel_sp
              # 完成了内核栈的切换
              ld sp, 8(a0)

              # 完成了页表的切换
              # restore kernel page table from p->trapframe->kernel_satp
              ld t1, 0(a0)
              csrw satp, t1
              sfence.vma zero, zero
              -
              -
                -
              • Handle negative sbrk() arguments.
              • -
              -
              -
              uint64
              sys_sbrk(void)
              {
              int addr;
              int n;

              if(argint(0, &n) < 0)
              return -1;
              uint64 sz = myproc()->sz;
              addr = sz;
              if(n >= 0){
              uint64 tmp = n + sz;
              if(tmp > MAXVA || n + sz < n) return -1;
              myproc()->sz = tmp;
              //printf("haha sb!sz=%p,n=%p\n",myproc()->sz,n);
              } else{
              if(n + sz > 0)
              myproc()->sz = uvmdealloc(myproc()->pagetable, sz, sz + n);
              else
              return -1;
              }
              //if(growproc(n) < 0)
              // return -1;
              return addr;
              }
              +

              所以,为了以后系统调用能顺利自发进行,我们需要把栈帧也一起换掉。怎么换呢?我们是否还要在一些地方人工把trapframe的值设置为我们自己的内核栈内核页表?答案是,不用!这些会由其他代码自动完成。

              +

              前面说到userinit的进程p被调度,satp换成了我们自己的内核页表。那么,在之后的内核态,satp都将保持我们自己的内核页表。当要返回用户态时,会执行如下代码:

              +
              // in usertrapret
              // 重置trapframe
              p->trapframe->kernel_satp = r_satp(); // kernel page table
              p->trapframe->kernel_sp = p->kstack + PGSIZE; // process's kernel stack
              -
              -
                -
              • Kill a process if it page-faults on a virtual memory address higher than any allocated with sbrk().

                -
              • -
              • Handle out-of-memory correctly: if kalloc() fails in the page fault handler, kill the current process.

                -
              • -
              • Handle faults on the invalid page below the user stack.

                -
              • -
              -
              -
              else if(scause == 13 || scause == 15){
              // 缺页中断
              uint64 va = r_stval();
              if(va >= p->sz || va < PGROUNDUP(p->trapframe->sp) ||PGROUNDDOWN(va) >= MAXVA){
              //printf("va=%p stack=%p\n",va,PGROUNDUP(r_sp()));
              p->killed = 1;
              } else{
              char* mem = kalloc();
              if(mem != 0){
              memset(mem, 0, PGSIZE);
              //printf("trap: %d %p\n",p->pid,PGROUNDDOWN(va));
              if(mappages(p->pagetable,PGROUNDDOWN(va), PGSIZE, (uint64)mem, PTE_W|PTE_X|PTE_R|PTE_U) != 0){
              kfree(mem);
              p->killed = 1;
              }
              } else {
              p->killed = 1;
              }
              }
              }
              +

              satp内的值为我们自己的内核页表,而非全局页表。因而这样栈帧中的页表就会被自然而然地写入为进程的内核页表。之后返回用户态,以及之后之后的各种中断,就都会一直使用自己的内核页表了。【试了一下,这里如果改成非即时从satp读,而是默认的kernel_pagetable的话,会一直死循环】

              +

              不得不说,真是设计精妙啊!!!不过我觉得,要是这里写成kernel_pagetable,然后让我们自己改的话将是薄纱(。当然它应该也不会这么做,因为,kernel_pagetable事实上是不对外发布的。它这里这么写热读,最直接的原因还是因为读不到kernel_pagetable。这算是无心插柳柳成荫吗233

              +
              释放页表但不释放物理内存

              其实答案就在它给的proc_freepagetable里。

              +
              // Free a process's page table, and free the
              // physical memory it refers to.
              void
              proc_freepagetable(pagetable_t pagetable, uint64 sz)
              {
              uvmunmap(pagetable, TRAMPOLINE, 1, 0);
              uvmunmap(pagetable, TRAPFRAME, 1, 0);
              uvmfree(pagetable, sz);
              }
              -
              -
                -
              • Handle the case in which a process passes a valid address from sbrk() to a system call such as read or write, but the memory for that address has not yet been allocated.
              • -
              -

              我认为这里要是引起一个缺页中断可能会更酷,可能可以像lazytests里面这么做:

              -
              char *i, *prev_end, *new_end;

              prev_end = sbrk(REGION_SZ);
              new_end = prev_end + REGION_SZ;

              // 这里触发了多次缺页中断
              for (i = prev_end + PGSIZE; i < new_end; i += PGSIZE * PGSIZE)
              *(char **)i = i;
              +

              uvmfree遍历页表,对每个存在的页表项,都试图找到其物理内存,并且释放物理内存和表项。如果页表项存在,但页表项对应的物理内存不存在,就会抛出freewalk leaf的异常。

              +

              uvmunmap会释放掉参数给的va的页表项,最后一个参数表示释放or不释放。

              +

              在这里,使用这两个的组合技,就可以达到不释放TRAMPOLINETRAPFRAME的物理内存,又不会让uvmfree出错的效果。

              +

              代码

              初始化

              初始化kpgtbl。由于现在内核栈存在各自的内核页表而非global内核页表中,所以在procinit中的对内核栈的初始化也得放在这:

              +
              // in proc.c allocproc()
              // An empty user page table.
              p->pagetable = proc_pagetable(p);
              if(p->pagetable == 0){
              freeproc(p);
              release(&p->lock);
              return 0;
              }

              p->kpgtbl = perproc_kvminit();

              char *pa = kalloc();
              if(pa == 0)
              panic("kalloc");
              uint64 va = KSTACK((int) (p - proc));
              pkvmmap(p->kpgtbl,va, (uint64)pa, PGSIZE, PTE_R | PTE_W);
              p->kstack = va;
              + +
              // in vm.c
              pagetable_t
              perproc_kvminit()
              {
              pagetable_t pt = (pagetable_t) kalloc();
              memset(pt, 0, PGSIZE);

              // uart registers
              pkvmmap(pt,UART0, UART0, PGSIZE, PTE_R | PTE_W);

              // virtio mmio disk interface
              pkvmmap(pt,VIRTIO0, VIRTIO0, PGSIZE, PTE_R | PTE_W);

              // CLINT
              pkvmmap(pt,CLINT, CLINT, 0x10000, PTE_R | PTE_W);

              // PLIC
              pkvmmap(pt,PLIC, PLIC, 0x400000, PTE_R | PTE_W);

              // map kernel text executable and read-only.
              pkvmmap(pt,KERNBASE, KERNBASE, (uint64)etext-KERNBASE, PTE_R | PTE_X);

              // map kernel data and the physical RAM we'll make use of.
              pkvmmap(pt,(uint64)etext, (uint64)etext, PHYSTOP-(uint64)etext, PTE_R | PTE_W);

              // map the trampoline for trap entry/exit to
              // the highest virtual address in the kernel.
              pkvmmap(pt,TRAMPOLINE, (uint64)trampoline, PGSIZE, PTE_R | PTE_X);
              return pt;
              }
              -

              之后有机会再试试233

              -

              【试了一下,发现是可以的。在COW fork的 感想—一些错误和思考—在内核态中引发并处理缺页中断 这部分内容中详细说明了具体要怎么做。】

              -
              -

              Lab: Copy-on-Write Fork for xv6

              -

              Parent and child can safely share phyical memory using copy-on-write fork, driven by page faults.

              -

              RISC-V has three different kinds of page fault: load page faults (when a load instruction cannot translate its virtual address), store page faults (when a store instruction cannot translate its virtual address), and instruction page faults (when the address for an instruction doesn’t translate).

              -

              The basic plan in COW fork is for the parent and child to initially share all physical pages, but to map them read-only. Thus, when the child or parent executes a store instruction, the RISC-V CPU raises a page-fault exception. In response to this exception, the kernel makes a copy of the page that contains the faulted address. It maps one copy read/write in the child’s address space and the other copy read/write in the parent’s address space. After updating the page tables, the kernel resumes the faulting process at the instruction that caused the fault.

              -

              PS【这个很重要】: COW fork() makes freeing of the physical pages that implement user memory a little trickier. A given physical page may be referred to by multiple processes’ page tables, and should be freed only when the last reference disappears.

              -
              -

              感想

              思路

              思路还是很直观的。

              -

              我们只需要在fork的时候,标记父子进程的所有PTE都为read-only,然后之后就会遇到不同scause的缺页中断,针对特定的scause,新建物理页面,拷贝物理页面,然后重新设置映射即可。而对于其提出的需要标记某页是否能够释放,则需要统计每页的ref数,当ref==1的时候才可以释放。

              -

              分析

              总分析

              其实可以把任务简单拆分为三部分。第一部分是实现基本的cow fork的逻辑,第二部分是引用计数释放内存,第三部分是解决copyin/copyout时在内核态发生的缺页中断。我认为本实验的难点事实上在第二部分【悲】我可能有大于3/4的时间都花在第二部分上了吧。

              -

              第一部分是实现cow fork的基本逻辑,也就是修改fork中对页表的拷贝以及在usertrap中添加对缺页中断的处理,这很直观,没什么好说的。

              -

              第三部分要么跟上面的lazy allocation一样,在kernel/vm.c walkaddr()中把缺页中断搬过去,要么向我在主要难点与错误—在内核态中引发并处理缺页中断这一部分那样做。

              -

              我们分析这一部分主要讲的是我认为最难的地方,也就是第二部分。其实第二部分的思路也很直观:创建一个数组,index为所有能用的内存的address/PGSIZE,用来记录引用数;然后在每次增加引用时,对应元素++;每次减少引用时,对应元素–。

              -

              虽然思路很简单很直观,但是实现起来非常地非常地非常地考验细节(我就非常不擅长这一点)。下面,我就先阐述一下第二部分的这个方法需要分割为哪几部分,其他我遇到的印象较深的bug和对一些地方的思考,都放在了下一部分,也即主要难点与错误

              -
              引用数实现分析
              -

              创建一个数组,index为所有能用的内存的address/PGSIZE,用来记录引用数;然后在每次增加引用时,对应元素++;每次减少引用时,对应元素–。

              -
              -
              数组的大小和数据类型

              kernel/kalloc.c中的kinit()

              -
              void
              kinit()
              {
              initlock(&kmem.lock, "kmem");
              // freerange用来把参数地址范围内的物理页加入freelist中
              // end是内核的结束地址
              freerange(end, (void*)PHYSTOP);
              }
              +
              // in vm.c
              void
              pkvmmap(pagetable_t pgtbl,uint64 va, uint64 pa, uint64 sz, int perm)
              {
              // 当第一个进程开始时,mycpu->proc = null,所以这里不能调用myproc
              if(mappages(pgtbl, va, sz, pa, perm) != 0)
              panic("kvmmap");
              }
              -

              可知,事实上,我们整个程序,包括用户和内核,能用的内存空间为0~PHYSTOP。因而,我们事实上只需要建一个PHYSTOP/PGSIZE这么大的数组就行。我算了一下大概是2^19次方。

              -

              然后,我感觉这种小系统应该不会有过多的对某一页的重复引用,因而,为了节省空间,我将数据类型定为了char。最好还是别定成uchar,因为这东西要是0–的话会溢出变为255,很可怕。

              -
              什么时候增减引用
              -

              我认为这里是非常考验细节和头脑清晰度的,也就是我卡了很久最后也没弄出来的部分【悲】

              -
              -

              可以分为三种情况来讲。我们的引用计数必须完美适应这三种情况:

              -
                -
              1. 不经由页表,通过kalloc和kfree直接使用物理页

                -

                这就要求我们在kalloc的时候置引用数为1,然后kfree的时候对引用数先-1,再判断是否归零。

                -
              2. -
              3. 经由页表,但与cow fork无关

                -

                增加页表项:mappages->kalloc,因而满足要求1即可。

                -

                删除页表项:uvmunmap。当do_free==1时,满足要求1即可。

                -
              4. -
              5. 经由页表,与cow fork有关

                -

                copy父进程页表时:在cowcopy中,每增加一次子进程的映射,就需要增加一次引用数

                -

                在用户态/内核态发生缺页中断:发生缺页中断后,对原来物理页的引用数需要-1【我就是漏了这一点……】

                -

                删除页表项:uvmunmap。当do_free==0时,当对应页表项有COW标记,则减少引用数

                -
              6. -
              -

              所以,我们需要在三个文件进行修改:

              -
                -
              1. kalloc.c

                -

                增加数组定义,在kalloc和kfree中增加引用数修改

                -
              2. -
              3. vm.c

                -

                在cowcopy和uvmunmap中增加引用数修改

                -
              4. -
              5. trap.c

                -

                在usertrap的缺页中断中增加引用计数修改

                -
              6. -
              -
              并发安全
              -

              这里我也没想到【悲】

              -
              -

              由于我们的pages数组会在多个文件、多个进程间使用,所以它必须在被锁保护的区域中被使用。

              -

              主要难点与错误

              scause=2

              image-20230117161404719

              -

              这个发生在我还没有实现第二部分的时候。搜索了一下,scause=2为Illegal instruction,而且sepc的这个1004的值也非常诡异。这应该是因为fork子进程释放了指令段内存,导致主进程执行错误

              -
              kernel无法启动

              在kinit中

              -
              void
              kinit()
              {
              initlock(&kmem.lock, "kmem");
              initlock(&pages_lock,"pages");
              memset(pages, 0, (2<<19));
              freerange(end, (void*)PHYSTOP);
              }
              +
              swtch时切换页表
              // in proc.c scheduler()
              p->state = RUNNING;
              w_satp(MAKE_SATP(p->kpgtbl));
              sfence_vma();
              c->proc = p;
              swtch(&c->context, &p->context);

              //...

              #if !defined (LAB_FS)
              if(found == 0) {
              // 没有进程运行时使用全局kernel_pagetable
              kvminithart();
              intr_on();
              asm volatile("wfi");
              }
              -

              会通过freerange初始化freelist。在freerange中:

              -
              void
              freerange(void *pa_start, void *pa_end)
              {
              char *p;
              p = (char*)PGROUNDUP((uint64)pa_start);
              for(; p + PGSIZE <= (char*)pa_end; p += PGSIZE){
              kfree(p);
              }
              }
              +
              修改kvmpa
              #include "spinlock.h"
              #include "proc.h"

              uint64
              kvmpa(uint64 va)
              {
              uint64 off = va % PGSIZE;
              pte_t *pte;
              uint64 pa;

              pte = walk(myproc()->kpgtbl, va, 0);
              if(pte == 0)
              panic("kvmpa");
              if((*pte & PTE_V) == 0)
              panic("kvmpa");
              pa = PTE2PA(*pte);
              return pa+off;
              }
              -

              会对每一项进行一次kfree。因而,我们需要在kfree前先增加一次引用,要不然会寄。

              -
              在缺页中断时减少对物理页的引用数

              image-20230117213903706

              -

              注意此处不能直接让pages[pa/PGSIZE]–,一定要借助kfree。当此进程为引用pa的最后一个进程的时候,如果仅减少引用数,就会造成内存泄漏。kfree可以既减少引用数,又在适当的时候对物理页释放,可谓一举两得。kfree的这个双重作用思想也在uvmunmap中体现了。

              -
              在内核态中引发并处理缺页中断
              -

              Modify copyout() to use the same scheme as page faults when it encounters a COW page.

              -
              -

              我们所做的第一第二部分仅仅是完成了对来自用户态的缺页中断的完美处理,还尚未处理来自内核态的缺页中断。因而,这个修改copyin和copyout的点实际上就是要我们处理内核态的缺页中断。

              -

              这次实验跟上次的lazy allocation一样,都可以直接在walkaddr进行特殊处理,并且差不多要把usertrap的全部代码挪过来【具体见lazy allocation的代码】。不过,我想出了另一个流氓的方法(也就是说其实原理感觉是不大对233)。我选择直接在kernel引发一个访问用户页面的缺页中断,然后在kerneltrap中处理这个中断,就像usertrap一样。

              -

              但由于在walkaddr中发生的中断处于内核状态下,所以就进不了usertrap。我们应该在kerneltrap中再次添加和usertrap一样的中断处理。我们会像这样引发一个中断:

              -
              if((*pte & PTE_COW) != 0){
              if((*pte & PTE_W) == 0){
              *(char*)va = 1;// 此处不能用pa哦
              +
              释放
              // in kernel.proc.c freeproc()
              if(p->kpgtbl)
              proc_freekpgtbl(p->kpgtbl,p->kstack);
              p->kpgtbl = 0;
              -

              然后在kerneltrap中这样处理:

              -
              if(r_scause() == 15){
              // 只要写入引起的缺页中断
              uint64 va = r_stval();
              pte_t *pte;
              uint64 pa;
              uint flags;

              if((pte = walk(p->pagetable, va, 0)) == 0)
              p->killed = 1;
              else if((*pte & PTE_V) == 0)
              p->killed = 1;
              else {
              sepc += 4;
              pa = PTE2PA(*pte);
              flags = PTE_FLAGS(*pte);

              char* mem;
              if((mem = kalloc())!=0){
              memmove(mem, (char*)pa, PGSIZE);
              // 设置为新的物理页地址
              *pte = PA2PTE(mem);
              kfree((void*)pa);
              // 设置新的flag,标记为可写
              flags = (flags | PTE_W | PTE_COW);
              *pte = ((*pte) | flags);
              } else{
              p->killed = 1;
              }
              }
              } else if((which_dev = devintr()) == 0){
              +
              extern char etext[];  // kernel.ld sets this to end of kernel code.

              void
              proc_freekpgtbl(pagetable_t pagetable,uint64 stack )
              {
              uvmunmap(pagetable, UART0, 1, 0);
              uvmunmap(pagetable, VIRTIO0, 1, 0);
              uvmunmap(pagetable, CLINT, 0x10000/(uint64)PGSIZE, 0);
              uvmunmap(pagetable, PLIC, 0X400000/(uint64)PGSIZE, 0);
              uvmunmap(pagetable, KERNBASE, (uint64)((uint64)etext-KERNBASE)/PGSIZE, 0);
              uvmunmap(pagetable, (uint64)etext,(PHYSTOP-(uint64)etext)/PGSIZE, 0);
              //kvmmap(KERNBASE, KERNBASE, (uint64)etext-KERNBASE, PTE_R | PTE_X);
              uvmunmap(pagetable, TRAMPOLINE, 1, 0);
              uvmunmap(pagetable, stack, 1,1 );
              uvmfree(pagetable, 0);
              }
              -

              但是,这样做是不行的。

              +

              Simplify copyin/copyinstr

              +

              参考:

              +

              6.S081学习记录-lab3

              +
              -

              会在这里卡住,会无限次不断进入kerneltrap。

              -

              image-20230117235028133

              +

              The kernel’s copyin function reads memory pointed to by user pointers. It does this by translating them to physical addresses, which the kernel can directly dereference. It performs this translation by walking the process page-table in software. Your job in this part of the lab is to add user mappings to each process’s kernel page table (created in the previous section) that allow copyin (and the related string function copyinstr) to directly dereference user pointers.

              -

              造成这个的原因,经过一番曲折的debug之后,我发现,只要像usertrap中的syscall分支一样:

              -
              if(r_scause() == 8){
              // system call

              // 重点是这里
              // sepc points to the ecall instruction,
              // but we want to return to the next instruction.
              p->trapframe->epc += 4;
              // ...
              }
              - -

              加上这句话就行:

              -
              sepc += 4;
              +
              +

              Replace the body of copyin in kernel/vm.c with a call to copyin_new (defined in kernel/vmcopyin.c); do the same for copyinstr and copyinstr_new. Add mappings for user addresses to each process’s kernel page table so that copyin_new and copyinstr_new work.

              +
              +

              感想

              这题很直观的思路是,在每个user pagetable添加映射的地方也添加kpgtbl的映射。但问题是,“每个user pagetable添加映射的地方”都是哪?

              +
              误入幻想

              我一开始想着偷偷懒,直接在proc.c和vm.c中每个操纵pagetable的地方都加上对kpgtbl的操纵。但很快我就给搞晕了。这时候,我心中萌生一计【PS:下面说的最后都没成功】:我直接快进到把proc结构中的pagetable属性给删了,然后每个出现p->pagetable的地方,都用p->kpgtbl代替,直接让两表合为一表,然后之后make的时候哪里报错改哪里,这不就一劳永逸地把所有出现pagetable的地方都改为kpgtbl了嘛。我振奋地去试了一下,将所有地方出现的pagetable都替换成了kpgtbl,把proc.c中的proc_pagetable()proc_freepagetable()的出现的地方都换成了perproc_kvminit()以及proc_freekpgtbl(),还做了一个小细节,就是在userinit中调用的uvminit中,我把这样:

              +
              void
              uvminit(pagetable_t pagetable, uchar *src, uint sz)
              {
              char *mem;

              if(sz >= PGSIZE)
              panic("inituvm: more than a page");
              mem = kalloc();
              memset(mem, 0, PGSIZE);
              mappages(pagetable, 0, PGSIZE, (uint64)mem, PTE_W|PTE_R|PTE_X|PTE_U);
              memmove(mem, src, sz);
              }
              -

              所以,结果就非常显而易见了,是因为一直卡在这句话执行不下去:

              -
              *(char*)va = 1;
              +

              换成了这样:

              +
              void
              uvminit(struct proc* p, uchar *src, uint sz)
              {
              char *mem;

              if(sz >= PGSIZE)
              panic("inituvm: more than a page");
              mem = kalloc();
              memset(mem, 0, PGSIZE);
              mappages(p->kpgtbl, 0, PGSIZE, (uint64)mem, PTE_W|PTE_R|PTE_X|PTE_U);
              memmove(mem, src, sz);
              }
              -

              这是因为缺页中断会返回到原代码句中执行,所以就会继续回到这句话。而我们知道,此时正处于内核态,并没有开启地址映射,所以此处其实是非法地址越界了。但是我们的目的确实已经达到了(因为va通过stval寄存器被传递到了kerneltrap),所以我们这里只需跳过这句即可。

              -

              这也是为什么说我这个方法虽然实现了,但本质上其实非常流氓,不算是kerneltrap。

              +

              最后,在启动的时候,卡在了初次调度切换不到initcode这边,没有调用exec。没有panic,似乎只在死循环。我也实在想不出是什么原因,最后把代码删了【悲】想想我应该用git保存一下改前改后的。这下实在是难受了,我的想法也暂时没有机会实践了。等到明年大三说不定还得再交一次这玩意,到时候再探究探究吧hhh

              +
              走上正途

              发现这个最后没成还改了半天的我最后非常沮丧地去看了hints【又一心浮气躁耐心不足的表现,但确实绷不住了】,发现它居然说只用修改三个地方:fork、exec以及sbrk。

              +

              我把kernel/下的每个文件都搜了一遍,发现确实,只有这三个,以及proc.c,vm.c,涉及到对页表项的增删。而在用户态中,想要对进程的内存进行管理,似乎只能通过系统调用sbrk。而proc.c和vm.c中确实没什么好改的。因为里面增加的映射,都是trapframe、trampoline、inicode这种不会一般在copyin中用到的虚拟地址。所以,要改的地方,确确实实,只有fork、exec以及sbrk

              -

              Update:验收的时候跟学长说了一下这个点,学长表示不算流氓,反而在内核(至少内核赛)中算是一个比较通用的手法hh没想到还误打误撞上了

              -

              它带来的好处是,当地址不合法的时候可以减少开销。

              -

              具体来说,内核中一般会将地址空间分为多个vma,因而检查地址越界无需像xv6那样简单查页表,只需查地址是否在对应的vma中即可。所以,直接把这东西转到一个硬件的缺页中断中实现,事实上确实是减少了地址非法时的开销。

              +

              Xv6 applications ask the kernel for heap memory using the sbrk() system call.

              -

              除了这一点外,还有一点很重要的是,由于walkaddr是需要返回一个pa的,因而我们需要手动再把pa在缺页中断后更新一下:

              -
              pa = PTE2PA(*pte);
              if((*pte & PTE_COW) != 0){
              if((*pte & PTE_W) == 0){
              *(char*)va = 1;
              pa = PTE2PA(*pte);
              }
              }
              return pa;
              +

              很悲伤,我的初见思路是错误的()

              +

              而这三个地方的共同点,就是都会对页表进行大量的copy。

              +
              //in proc.c fork()
              // Copy user memory from parent to child.
              if(uvmcopy(p->pagetable, np->pagetable, p->sz) < 0){
              freeproc(np);
              release(&np->lock);
              return -1;
              }
              -

              总之,做了这两个关键步骤后,也能启动了,也能过cowtest了。所以下面的代码也就贴上了这里的版本。

              -

              心得

              本次实验耗时经典五小时(包含笔记时间就是六个半小时了hhh),算是平均水平。很遗憾也很难受的一点是,我的错误最终还是没有自己想出来,而是参考了别人的代码才改对的。思路很简单,但是细节也依然非常多非常坑,还是得再加把劲。

              -

              代码

              我们只需要在fork的时候,标记父子进程的所有PTE都为read-only,然后之后就会遇到不同scause的缺页中断,针对特定的scause,新建物理页面,拷贝物理页面,然后重新设置映射即可。而对于其提出的需要标记某页是否能够释放,则需要统计每页的ref数,当ref==1的时候才可以释放。

              -

              定义COW标记

              // in kernel/riscv.h
              #define PTE_V (1L << 0)
              // ...
              #define PTE_COW (1L << 5)
              +
              //in exec.c
              // Commit to the user image.
              oldpagetable = p->pagetable;
              p->pagetable = pagetable;
              -

              引用数组初始化

              // in kernel/kalloc.c
              char pages[(2<<19)];
              struct spinlock pages_lock;

              void
              kinit()
              {
              initlock(&kmem.lock, "kmem");
              initlock(&pages_lock,"pages");
              memset(pages, 0, (2<<19));
              freerange(end, (void*)PHYSTOP);
              }
              void
              freerange(void *pa_start, void *pa_end)
              {
              char *p;
              p = (char*)PGROUNDUP((uint64)pa_start);
              for(; p + PGSIZE <= (char*)pa_end; p += PGSIZE){
              pages[(uint64)p/PGSIZE] = 1;
              kfree(p);
              }
              }
              +
              //in syscall.c
              uint64
              sys_sbrk(void)
              {
              int addr;
              int n;

              if(argint(0, &n) < 0)
              return -1;
              addr = myproc()->sz;
              if(growproc(n) < 0)
              return -1;
              return addr;
              }
              //in proc.c growproc()
              uvmalloc(p->pagetable, sz, sz + n)) == 0
              -

              申请和释放页时增删引用

              // in kernel/kalloc.c
              void *
              kalloc(void)
              {
              struct run *r;

              acquire(&kmem.lock);
              r = kmem.freelist;
              if(r)
              kmem.freelist = r->next;
              release(&kmem.lock);
              if(r){
              acquire(&pages_lock);
              // 在这
              pages[(uint64)r/PGSIZE] = 1;
              release(&pages_lock);
              }
              if(r)
              memset((char*)r, 5, PGSIZE); // fill with junk
              return (void*)r;
              }
              void
              kfree(void *pa)
              {
              struct run *r;

              acquire(&pages_lock);
              if(((uint64)pa % PGSIZE) != 0 || (char*)pa < end || (uint64)pa >= PHYSTOP || pages[(uint64)pa/PGSIZE] <= 0 )
              panic("kfree");
              // 每次kfree都会减少引用
              pages[(uint64)pa/PGSIZE]--;
              // 说明此时页面还被其他东西引用着,不能释放
              if(pages[((uint64)pa)/PGSIZE] > 0){
              release(&pages_lock);
              return;
              }
              release(&pages_lock);

              // Fill with junk to catch dangling refs.
              memset(pa, 1, PGSIZE);
              r = (struct run*)pa;

              acquire(&kmem.lock);
              r->next = kmem.freelist;
              kmem.freelist = r;
              release(&kmem.lock);
              }
              +

              所以,我们要做的事情很简单:写一个坐收渔翁之利的函数,内容为把一个页表的所有内容复制到另一个页表。然后再在这几个地方调用这个函数即可。

              +

              代码

              +

              注意:由于我写得实在是太烦了,已经思考不下去了。为了放过我自己,我写了个虽然能过得去测试但是其实毛病重重的代码。垃圾点为以下几点:

              +
                +
              1. 需要去掉freewalk中的panic

                +

                我的kvmcopy的实现是,user pagetable(下面简称up)和tp的相同虚拟地址共用同一页物理内存。也就是说,页表不一样,但所指向的物理内存是同一个。这样设计的目的是为了能够让tp及时用到up的更新后的数据。

                +

                这会导致啥呢?在进程释放时,需要一起调用proc_freepagetableproc_freekpgtblproc_freepagetable调用完后,所指向的那堆物理内存已经寄完了,如果再调用proc_freekpgtbl,显然,就会发生页表未释放但页表对应内存已经释放的问题,freewalk就会panic。因此,我简单粗暴地直接把freewalk的panic删掉了【抖】也许有别的解决方法,但我真是烦得不想想了放过我吧(

                +
              2. +
              3. 好像暂时没有第二点了()

                +
              4. +
              +
              +
              渔翁之利函数
              // in vm.c
              // 效仿的是vm.c中的uvmcopy
              int
              kvmcopy(pagetable_t up, pagetable_t kp, uint64 sz)
              {
              pte_t *pte;
              uint64 pa, i;
              uint flags;

              for(i = 0; i < sz; i += PGSIZE){
              if((pte = walk(up, i, 0)) == 0 || (*pte & PTE_V) == 0){
              if(walk(kp,i,0) == 0){
              //如果up不存在此项,kp存在,就直接删了
              uvmunmap(kp,i,PGSIZE,0);
              }
              continue;
              }
              pa = PTE2PA(*pte);
              flags = PTE_FLAGS(*pte);
              // 注意去除PTE_U,否则内核态无法访问
              flags = (flags & (~PTE_U));
              if(mappages(kp, i, PGSIZE, pa, flags) != 0){
              goto err;
              }
              }
              return 0;

              err:
              uvmunmap(kp, 0, i / PGSIZE, 1);
              return -1;
              }
              -

              修改fork时对页表的复制操作,并标记引用数增加

              // in kernel/proc.c fork()
              // Copy user memory from parent to child.
              if(cowcopy(p->pagetable, np->pagetable, p->sz) < 0){
              freeproc(np);
              release(&np->lock);
              return -1;
              }
              // in kernel/vm.c
              int
              cowcopy(pagetable_t old, pagetable_t new, uint64 sz)
              {
              pte_t *pte;
              uint64 pa, i;
              uint flags;

              for(i = 0; i < sz; i += PGSIZE){
              if((pte = walk(old, i, 0)) == 0)
              panic("cowcopy: pte should exist");
              if((*pte & PTE_V) == 0)
              panic("cowcopy: page not present");
              pa = PTE2PA(*pte);
              flags = PTE_FLAGS(*pte);
              // 去除flag中的PTE_W,并且给父子的都安上没有PTE_W的flag
              flags = (flags & (~PTE_W));
              flags = (flags | PTE_COW);
              *pte = ((*pte) & (~PTE_W));
              *pte = ((*pte) | PTE_COW);
              if(mappages(new, i, PGSIZE, (uint64)pa, flags) != 0){
              goto err;
              }
              // 标记物理页的引用数增加
              acquire(&pages_lock);
              pages[pa/PGSIZE]++;
              release(&pages_lock);
              }
              return 0;
              err:
              // 失败了不能释放物理内存
              uvmunmap(new, 0, i / PGSIZE, 0);
              return -1;
              }
              +
              修改fork、exec、sbrk
              fork
              // in proc.c fork()
              // Copy user memory from parent to child.
              if(uvmcopy(p->pagetable, np->pagetable, p->sz) < 0){
              freeproc(np);
              release(&np->lock);
              return -1;
              }
              if(kvmcopy(np->pagetable, np->kpgtbl, p->sz) < 0){
              freeproc(np);
              release(&np->lock);
              return -1;
              }
              -

              处理缺页中断,标记引用数减少

              } else if(r_scause() == 15){
              // 只要求写入引起的缺页中断
              uint64 va = r_stval();
              pte_t *pte;
              uint64 pa;
              uint flags;

              if((pte = walk(p->pagetable, va, 0)) == 0)
              p->killed = 1;
              else if((*pte & PTE_V) == 0)
              p->killed = 1;
              else {
              pa = PTE2PA(*pte);
              flags = PTE_FLAGS(*pte);

              char* mem;
              if((mem = kalloc())!=0){
              memmove(mem, (char*)pa, PGSIZE);
              // 设置为新的物理页地址
              *pte = PA2PTE(mem);
              // 减少引用,引用归零时释放
              kfree((void*)pa);
              // 设置新的flag,标记为可写
              flags = (flags | PTE_W | PTE_COW);
              *pte = ((*pte) | flags);
              } else{
              p->killed = 1;
              }
              }
              }
              +
              exec
              // in exec.c
              // Commit to the user image.
              oldpagetable = p->pagetable;
              p->pagetable = pagetable;

              p->sz = sz;
              p->trapframe->epc = elf.entry; // initial program counter = main
              p->trapframe->sp = sp; // initial stack pointer
              proc_freepagetable(oldpagetable, oldsz);

              // 添上此句
              kvmcopy(p->pagetable, p->kpgtbl, p->sz);
              -

              uvmunmap时减少引用数

              void
              uvmunmap(pagetable_t pagetable, uint64 va, uint64 npages, int do_free)
              {
              uint64 a;
              pte_t *pte;
              // ...

              for(a = va; a < va + npages*PGSIZE; a += PGSIZE){
              // ...
              uint64 pa = PTE2PA(*pte);
              if(do_free){
              kfree((void*)pa);
              }else {
              acquire(&pages_lock);
              if(((*pte) & PTE_COW) != 0){
              pages[pa/PGSIZE]--;
              }
              release(&pages_lock);
              }
              *pte = 0;
              }
              }
              +
              sbrk
              uint64
              sys_sbrk(void)
              {
              int addr;
              int n;

              if(argint(0, &n) < 0)
              return -1;
              addr = myproc()->sz;
              if(addr+n >= PLIC) return -1;
              if(growproc(n) < 0)
              return -1;
              return addr;
              }
              -

              修改walkaddr

              在walkaddr中触发缺页中断
              uint64
              walkaddr(pagetable_t pagetable, uint64 va)
              {
              pte_t *pte;
              uint64 pa;

              if(va >= MAXVA)
              return 0;

              pte = walk(pagetable, va, 0);
              if(pte == 0)
              return 0;
              if((*pte & PTE_V) == 0)
              return 0;
              if((*pte & PTE_U) == 0)
              return 0;
              pa = PTE2PA(*pte);
              // 在这里
              if((*pte & PTE_COW) != 0){
              if((*pte & PTE_W) == 0){
              // 触发缺页中断
              *(char*)va = 1;
              // 更新pa值
              pa = PTE2PA(*pte);
              }
              }
              return pa;
              }
              +
              // in proc.c
              // Grow or shrink user memory by n bytes.
              // Return 0 on success, -1 on failure.
              int
              growproc(int n)
              {
              uint sz;
              struct proc *p = myproc();

              sz = p->sz;
              // ...
              p->sz = sz;
              // 加这个
              kvmcopy(p->pagetable, p->kpgtbl, p->sz);
              return 0;
              }
              -
              在kerneltrap内补上对缺页中断的处理
              void
              kerneltrap()
              {
              uint64 sepc = r_sepc();
              // ...
              if(r_scause() == 15){
              // 只要写入引起的缺页中断
              uint64 va = r_stval();
              pte_t *pte;
              uint64 pa;
              uint flags;

              if((pte = walk(p->pagetable, va, 0)) == 0)
              p->killed = 1;
              else if((*pte & PTE_V) == 0)
              p->killed = 1;
              else {
              // 注意,这个很重要!!!!!
              sepc += 4;
              pa = PTE2PA(*pte);
              flags = PTE_FLAGS(*pte);

              char* mem;
              if((mem = kalloc())!=0){
              memmove(mem, (char*)pa, PGSIZE);
              // 设置为新的物理页地址
              *pte = PA2PTE(mem);
              kfree((void*)pa);
              // 设置新的flag,标记为可写
              flags = (flags | PTE_W | PTE_COW);
              *pte = ((*pte) | flags);
              } else{
              p->killed = 1;
              }
              }
              } else if((which_dev = devintr()) == 0){
              printf("scause %p\n", scause);
              printf("sepc=%p stval=%p\n", r_sepc(), r_stval());
              panic("kerneltrap");
              }
              // ...
              }
              +
              userinit
              +

              这一步不能忽视,因为内核启动的时候就需要用到copyinstr。

              +
              +
              // in proc.c userinit()
              uvminit(p->pagetable, initcode, sizeof(initcode));
              p->sz = PGSIZE;
              // 加这个!
              kvmcopy(p->pagetable, p->kpgtbl, p->sz);
              +
              删掉freewalk的panic(我特有的缺点)
              // in vm.c freewalk()    
              if((pte & PTE_V) && (pte & (PTE_R|PTE_W|PTE_X)) == 0){
              // ...
              } else if(pte & PTE_V){
              //panic("freewalk: leaf");
              }
              ]]> @@ -11499,978 +11499,1317 @@ url访问填写http://localhost/webdemo4_war/*.do

              之后写学校实验时回过头来看,发现之前的实现是不对的,在同时进入bfree函数时有死锁风险。经过修改后虽然粒度大了但是安全了。对了,额外附上一版不知道为啥错了的细粒度版本……看了真感觉没什么问题,但依然是会在bfree时panic两次free。等以后有精力再继续研究吧(泪目)

              错误版本的思路就是,使用每个block块自己的锁(b->lock)和每个桶的锁来实现细粒度加锁。我是左看右看感觉每个block从在bget中获取一直到brelse释放的b->lock锁是一直持有的,但确实依然有可能发生两个进程同时获取同一个block的锁的情况。实在不知道怎么办了,想了很久还是没想出细粒度好方法(泪)总之代码先放在这里。

              -
              正确版本

              请见我的github。

              -
              错误版本
              static struct buf*
              bget(uint dev, uint blockno)
              {
              // printf("bget\n");
              uint hash = blockno % 13;

              acquire(&(bcache.dev_locks[hash]));

              // Is the block already cached?
              for(struct buf* b = bcache.dev_heads[hash].next; b != &(bcache.dev_heads[hash]); b = b->next){
              int initial_hold = holdingsleep(&b->lock);
              release(&(bcache.dev_locks[hash]));

              if (!initial_hold)
              acquiresleep(&b->lock);

              if(b->blockno == blockno&&b->dev == dev){ // 找到了
              b->refcnt++;
              b->timestamp = ticks;
              // release(&(bcache.dev_locks[hash]));
              // acquiresleep(&b->lock);
              return b;
              }

              if (!initial_hold)
              releasesleep(&b->lock);
              acquire(&(bcache.dev_locks[hash]));
              }

              release(&(bcache.dev_locks[hash]));

              // 没找到,进行LRU
              // 遍历hash table,找到LRU,也即时间戳最小的且refcnt小于0的那一项

              uint min_time = 4294967295;// uint的最大值。此处不能使用(uint)(-1)
              struct buf* goal = 0;
              for(int i = 0; i < NBUCKET; i++) {
              uint time = 0;
              acquire(&(bcache.dev_locks[i]));
              for(struct buf* b = bcache.dev_heads[i].prev; b != &(bcache.dev_heads[i]); b = b->prev){
              int initial_hold = holdingsleep(&b->lock);
              release(&(bcache.dev_locks[i]));
              if (!initial_hold)
              acquiresleep(&b->lock);

              if(b->refcnt == 0) {
              time = b->timestamp;
              if(time < min_time){
              min_time = time;
              if (goal) releasesleep(&goal->lock);
              goal = b;
              }
              }
              if (!initial_hold && goal != b) releasesleep(&b->lock);
              acquire(&(bcache.dev_locks[i]));
              }
              release(&(bcache.dev_locks[i]));
              }
              // hashtable中存在着空闲buf
              if(goal != 0){
              // acquiresleep(&goal->lock);
              goal->dev = dev;
              goal->blockno = blockno;
              goal->valid = 0;
              goal->refcnt = 1;

              // 将goal从其所在双向链表中移除
              acquire(&(bcache.dev_locks[hash]));

              goal->prev->next = goal->next;
              goal->next->prev = goal->prev;

              // 在新双向链表中添加goal
              goal->prev = &(bcache.dev_heads[hash]);
              goal->next = bcache.dev_heads[hash].next;

              bcache.dev_heads[hash].next->prev = goal;
              bcache.dev_heads[hash].next = goal;

              release(&(bcache.dev_locks[hash]));

              return goal;
              }
              panic("bget: no buffers");
              }
              +
              正确版本

              请见我的github。

              +
              错误版本
              static struct buf*
              bget(uint dev, uint blockno)
              {
              // printf("bget\n");
              uint hash = blockno % 13;

              acquire(&(bcache.dev_locks[hash]));

              // Is the block already cached?
              for(struct buf* b = bcache.dev_heads[hash].next; b != &(bcache.dev_heads[hash]); b = b->next){
              int initial_hold = holdingsleep(&b->lock);
              release(&(bcache.dev_locks[hash]));

              if (!initial_hold)
              acquiresleep(&b->lock);

              if(b->blockno == blockno&&b->dev == dev){ // 找到了
              b->refcnt++;
              b->timestamp = ticks;
              // release(&(bcache.dev_locks[hash]));
              // acquiresleep(&b->lock);
              return b;
              }

              if (!initial_hold)
              releasesleep(&b->lock);
              acquire(&(bcache.dev_locks[hash]));
              }

              release(&(bcache.dev_locks[hash]));

              // 没找到,进行LRU
              // 遍历hash table,找到LRU,也即时间戳最小的且refcnt小于0的那一项

              uint min_time = 4294967295;// uint的最大值。此处不能使用(uint)(-1)
              struct buf* goal = 0;
              for(int i = 0; i < NBUCKET; i++) {
              uint time = 0;
              acquire(&(bcache.dev_locks[i]));
              for(struct buf* b = bcache.dev_heads[i].prev; b != &(bcache.dev_heads[i]); b = b->prev){
              int initial_hold = holdingsleep(&b->lock);
              release(&(bcache.dev_locks[i]));
              if (!initial_hold)
              acquiresleep(&b->lock);

              if(b->refcnt == 0) {
              time = b->timestamp;
              if(time < min_time){
              min_time = time;
              if (goal) releasesleep(&goal->lock);
              goal = b;
              }
              }
              if (!initial_hold && goal != b) releasesleep(&b->lock);
              acquire(&(bcache.dev_locks[i]));
              }
              release(&(bcache.dev_locks[i]));
              }
              // hashtable中存在着空闲buf
              if(goal != 0){
              // acquiresleep(&goal->lock);
              goal->dev = dev;
              goal->blockno = blockno;
              goal->valid = 0;
              goal->refcnt = 1;

              // 将goal从其所在双向链表中移除
              acquire(&(bcache.dev_locks[hash]));

              goal->prev->next = goal->next;
              goal->next->prev = goal->prev;

              // 在新双向链表中添加goal
              goal->prev = &(bcache.dev_heads[hash]);
              goal->next = bcache.dev_heads[hash].next;

              bcache.dev_heads[hash].next->prev = goal;
              bcache.dev_heads[hash].next = goal;

              release(&(bcache.dev_locks[hash]));

              return goal;
              }
              panic("bget: no buffers");
              }
              + +
              void
              brelse(struct buf *b)
              {
              if(!holdingsleep(&b->lock))
              panic("brelse");

              uint hash = b->blockno%NBUCKET;

              acquire(&(bcache.dev_locks[hash]));
              b->refcnt--;
              b->timestamp = ticks;
              if (b->refcnt == 0) {
              // no one is waiting for it.
              b->next->prev = b->prev;
              b->prev->next = b->next;

              b->next = bcache.dev_heads[hash].next;
              b->prev = &bcache.dev_heads[hash];
              bcache.dev_heads[hash].next->prev = b;
              bcache.dev_heads[hash].next = b;
              }
              release(&(bcache.dev_locks[hash]));
              releasesleep(&b->lock);
              }
              + +]]> + + + File system + /2023/01/10/xv6$chap8/ + File system
              +

              来到指导书最高点!太美丽了xv6。哎呀那不文件系统吗(

              +

              这里是自底向上讲起的。之后可以看看hit网课的自顶向下。

              +
              +
              +

              image-20230121160555370

              +
              +

              Overview

              image-20230121160641718

              +
              +

              The disk layer reads and writes blocks on an virtio hard drive.

              +

              The buffer cache layer caches disk blocks and synchronizes access to them, making sure that only one kernel process at a time can modify the data stored in any particular block.

              +

              The logging layer allows higher layers to wrap updates to several blocks in a transaction, and ensures that the blocks are updated atomically in the face of crashes (i.e., all of them are updated or none). 【日志记录层允许更高层将更新包装到一个事务中的多个块,并确保在崩溃时以原子方式更新块(即,所有块都更新或不更新)。可以类比一下数据库的那个概念。】

              +

              The inode layer provides individual files, each represented as an inode with a unique i-number and some blocks holding the file’s data.

              +

              The directory layer implements each directory as a special kind of inode whose content is a sequence of directory entries, each of which contains a file’s name and i-number.

              +

              The pathname layer provides hierarchical path names like /usr/rtm/xv6/fs.c, and resolves them with recursive lookup.

              +

              The file descriptor layer abstracts many Unix resources (e.g., pipes, devices, fifiles, etc.) using the file system interface, simplifying the lives of application programmers.

              +
              +

              image-20230121162324747

              +
              +

              The file system must have a plan for where it stores inodes and content blocks on the disk. To do so, xv6 divides the disk into several sections, as Figure 8.2 shows.

              +

              The file system does not use block 0 (it holds the boot sector).

              +

              Block 1 is called the superblock; it contains metadata about the file system (the file system size in blocks, the number of data blocks, the number of inodes, and the number of blocks in the log). The superblock is filled in by a separate program, called mkfs, which builds an initial file system.

              +

              Blocks starting at 2 hold the log.

              +

              After the log are the inodes, with multiple inodes per block.

              +

              After those come bitmap blocks tracking which data blocks are in use. 【应该是用来标识每个块是否空闲的吧】

              +

              The remaining blocks are data blocks; each is either marked free in the bitmap block, or holds content for a file or directory【要么空闲要么是文件或目录】.

              +
              +

              Buffer cache

              +

              The buffer cache has two jobs:

              +
                +
              1. synchronize access to disk blocks to ensure that only one copy of a block is in memory and that only one kernel thread at a time uses that copy;
              2. +
              3. cache popular blocks so that they don’t need to be re-read from the slow disk.
              4. +
              +

              The code is in bio.c.

              +

              Buffer cache中保存磁盘块的缓冲区数量固定,这意味着如果文件系统请求还未存放在缓存中的块,Buffer cache必须回收当前保存其他块内容的缓冲区。Buffer cache为新块回收最近使用最少的缓冲区。这样做的原因是认为最近使用最少的缓冲区是最不可能近期再次使用的缓冲区。

              +
              +

              image-20230124151719288

              +

              数据结构定义

              struct buf {
              int valid; // has data been read from disk?缓冲区是否包含块的副本
              int disk; // does disk "own" buf?缓冲区内容是否已交给磁盘
              uint dev;
              uint blockno;
              struct sleeplock lock;
              uint refcnt;
              struct buf *prev; // LRU cache list
              struct buf *next;
              uchar data[BSIZE];
              };
              + +

              这应该代表着一个磁盘块。

              +
              struct {
              struct spinlock lock;
              struct buf buf[NBUF];

              // Linked list of all buffers, through prev/next.
              // Sorted by how recently the buffer was used.
              // head.next is most recent, head.prev is least.
              struct buf head;
              } bcache;
              + +

              大概buf数组里存储着所有buf的内容。buf本身通过最近使用排序的双向链表连接,head是链表的头。

              +

              初始化

              // called by main.c
              void
              binit(void)
              {
              struct buf *b;

              initlock(&bcache.lock, "bcache");

              // Create linked list of buffers
              // 把b插在head之后
              bcache.head.prev = &bcache.head;
              bcache.head.next = &bcache.head;
              for(b = bcache.buf; b < bcache.buf+NBUF; b++){
              b->next = bcache.head.next;
              b->prev = &bcache.head;
              initsleeplock(&b->lock, "buffer");
              bcache.head.next->prev = b;
              bcache.head.next = b;
              }
              }
              + +

              上层接口

              +

              The main interface exported by the buffer cache consists of bread and bwrite.

              +

              The buffer cache uses a per-buffer sleep-lock to ensure concurrent security.

              +
              +

              bread

              +

              bread obtains a buf containing a copy of a block which can be read or modified in memory.

              +

              依据给定设备号和给定扇区号寻找cache的buf。返回的buf是locked的。

              +
              +
              // Return a locked buf with the contents of the indicated block.
              struct buf*
              bread(uint dev, uint blockno)
              {
              struct buf *b;

              // 获取buf块
              b = bget(dev, blockno);
              if(!b->valid) {
              // 说明cache未命中,需要从磁盘读入
              virtio_disk_rw(b, 0);
              b->valid = 1;
              }
              return b;
              }
              + +

              bwrite

              +

              writes a modified buffer to the appropriate block on the disk

              +
              +
              // Write b's contents to disk.  Must be locked.
              void
              bwrite(struct buf *b)
              {
              // 必须持有b的锁
              if(!holdingsleep(&b->lock))
              panic("bwrite");
              // 写入磁盘
              virtio_disk_rw(b, 1);
              }
              + +

              brelse

              +

              A kernel thread must release a buffer by calling brelse when it is done with it.

              +
              +
              // Release a locked buffer.
              // Move to the head of the most-recently-used list.
              void
              brelse(struct buf *b)
              {
              if(!holdingsleep(&b->lock))
              panic("brelse");

              releasesleep(&b->lock);

              acquire(&bcache.lock);
              b->refcnt--;
              if (b->refcnt == 0) {
              // no one is waiting for it.
              // 移动到头结点和头结点的下一个结点之间的位置
              b->next->prev = b->prev;
              b->prev->next = b->next;
              b->next = bcache.head.next;
              b->prev = &bcache.head;
              bcache.head.next->prev = b;
              bcache.head.next = b;
              }

              release(&bcache.lock);
              }
              + +

              具体细节

              bget

              用于获取cache中是否存在block。如果不存在,则新申请一个buf,并把该buf以上锁状态返回

              +
              // Look through buffer cache for block on device dev.
              // If not found, allocate a buffer.
              // In either case, return locked buffer.
              static struct buf*
              bget(uint dev, uint blockno)
              {
              struct buf *b;

              acquire(&bcache.lock);

              // Is the block already cached?
              // 这个循环条件很有意思,充分用到了双向链表的特性
              for(b = bcache.head.next; b != &bcache.head; b = b->next){
              if(b->dev == dev && b->blockno == blockno){
              // 引用数增加
              b->refcnt++;
              release(&bcache.lock);
              // 锁定
              acquiresleep(&b->lock);
              return b;
              }
              }

              // Not cached.
              // Recycle the least recently used (LRU) unused buffer.
              // 从尾部开始遍历,确实就是最少使用的了
              for(b = bcache.head.prev; b != &bcache.head; b = b->prev){
              // 如果该buf空闲
              if(b->refcnt == 0) {
              b->dev = dev;
              b->blockno = blockno;
              // 仅是新建了一个buf,还未从磁盘读取对应磁盘块的副本,因而设valid为0以供上层函数调用处理
              b->valid = 0;
              b->refcnt = 1;
              release(&bcache.lock);
              // 锁定
              acquiresleep(&b->lock);
              return b;
              }
              }
              // cache不够用了
              panic("bget: no buffers");
              }
              + +

              Logging layer

              简介

              +

              Xv6通过简单的日志记录形式解决了文件系统操作期间的崩溃问题。

              +

              xv6系统调用不会直接写入磁盘上的文件系统数据结构。相反,它会在磁盘上的log(日志)中放置它希望进行的所有磁盘写入的描述。一旦系统调用记录了它的所有写入操作,它就会向磁盘写入一条特殊的commit(提交)记录,表明日志包含一个完整的操作。此时,系统调用将写操作复制到磁盘上的文件系统数据结构。完成这些写入后,系统调用将擦除磁盘上的日志。

              +
              +
              +

              如果系统崩溃并重新启动,则在运行任何进程之前,文件系统代码将按如下方式从崩溃中恢复:

              +

              如果日志标记为包含完整操作,则恢复代码会将写操作复制到磁盘文件系统中它们所属的位置,然后擦除日志。如果日志没有标记为包含完整操作,则恢复代码将忽略该日志,然后擦除日志。

              +
              +

              这就保证了原子性。

              +

              Log design

              image-20230121162324747

              +

              superblock记录了log的存储位置。

              +
              +

              它由一个头块(header block)和一系列更新块的副本(logged block)组成。

              +

              头块包含一个扇区号(sector)数组(每个logged block对应一个扇区号)以及日志块的计数。

              +

              磁盘上的头块中的计数为零表示日志中没有事务,为非零表示日志包含一个完整的已提交事务,并具有指定数量的logged block。

              +

              在事务提交(commit)时Xv6才向头块写入数据,在此之前不会写入。在将logged blocks复制到文件系统后,头块的计数将被设置为零。

              +

              因此,事务中途崩溃将导致日志头块中的计数为零;提交后的崩溃将导致非零计数。

              +
              +
              +

              为了允许不同进程并发执行文件系统操作,日志系统可以将多个系统调用的写入累积到一个事务中。因此,单个提交可能涉及多个完整系统调用的写入。为了避免在事务之间拆分系统调用,日志系统仅在没有文件系统调用进行时提交。

              +

              同时提交多个事务的想法称为组提交(group commit)。组提交减少了磁盘操作的数量,因为成本固定的一次提交分摊了多个操作。组提交还同时为磁盘系统提供更多并发写操作,可能允许磁盘在一个磁盘旋转时间内写入所有这些操作。Xv6的virtio驱动程序不支持这种批处理,但是Xv6的文件系统设计允许这样做。

              +

              【这感觉实现得也还挺简略的】

              +
              +
              +

              Xv6在磁盘上留出固定的空间来保存日志。事务中系统调用写入的块总数必须可容纳于该空间。这导致两个后果:

              +
                +
              1. 任何单个系统调用都不允许写入超过日志空间的不同块。

                +

                【这段话我一个字没看懂】

                +

                这对于大多数系统调用来说都不是问题,但其中两个可能会写入许多块:writeunlink。一个大文件的write可以写入多个数据块和多个位图块以及一个inode块;unlink大文件可能会写入许多位图块和inode。Xv6的write系统调用将大的写入分解为适合日志的多个较小的写入,unlink不会导致此问题,因为实际上Xv6文件系统只使用一个位图块。

                +
              2. +
              3. 日志空间有限的另一个后果是,除非确定系统调用的写入将可容纳于日志中剩余的空间,否则日志系统无法允许启动系统调用。

                +
              4. +
              +
              +

              Code: logging

              +

              log的原理是这样的:

              +

              在每个系统调用的开始调用begin_op表示事务开始,然后之后新申请一块block,也即把该block的内容读入内存,并且把该block的blockno记录到log的header中。此后程序正常修改在内存中的block,磁盘中的block保持不变。最后commit的时候遍历log header中的blockno,一块块地把内存中的block写入日志和磁盘中。

              +

              如果程序在commit前崩溃,则内存消失,同时磁盘也不会写入;如果在commit后崩溃,那也无事发生。

              +

              在每次启动的时候,都会执行log的初始化,届时可以顺便恢复数据。

              +

              完美实现了日志的功能。

              +
              +

              image-20230123212753931

              +

              数据结构

              // Contents of the header block, used for both the on-disk header block
              // and to keep track in memory of logged block# before commit.
              struct logheader {
              int n;
              // 扇区号也即blockno的数组
              int block[LOGSIZE];
              };

              // 代表log磁盘块
              struct log {
              struct spinlock lock;
              int start;// log磁盘块的开始。start开始的第一块为log header,之后皆为写入的block
              int size;
              int outstanding; // how many FS sys calls are executing.
              int committing; // in commit(), please wait.
              int dev;
              struct logheader lh;
              };
              struct log log;
              -
              void
              brelse(struct buf *b)
              {
              if(!holdingsleep(&b->lock))
              panic("brelse");

              uint hash = b->blockno%NBUCKET;

              acquire(&(bcache.dev_locks[hash]));
              b->refcnt--;
              b->timestamp = ticks;
              if (b->refcnt == 0) {
              // no one is waiting for it.
              b->next->prev = b->prev;
              b->prev->next = b->next;

              b->next = bcache.dev_heads[hash].next;
              b->prev = &bcache.dev_heads[hash];
              bcache.dev_heads[hash].next->prev = b;
              bcache.dev_heads[hash].next = b;
              }
              release(&(bcache.dev_locks[hash]));
              releasesleep(&b->lock);
              }
              +

              关键函数

              begin_op()
              +

              begin_op等待直到日志系统当前未处于提交中,并且直到有足够的未被占用的日志空间来保存此调用的写入。

              +

              log.outstanding统计预定了日志空间的系统调用数;为此保留的总空间为log.outstanding乘以MAXOPBLOCKS(10)。递增log.outstanding会预定空间并防止在此系统调用期间发生提交(if的第二个分支)。代码保守地假设每个系统调用最多可以写入MAXOPBLOCKS(10)个不同的块。

              +
              +
              // called at the start of each FS system call.
              void
              begin_op(void)
              {
              acquire(&log.lock);
              while(1){
              // 正在提交则等待日志空闲
              if(log.committing){
              sleep(&log, &log.lock);
              // 日志空间不足则等待空间充足
              } else if(log.lh.n + (log.outstanding+1)*MAXOPBLOCKS > LOGSIZE){
              // this op might exhaust log space此操作可能会耗尽日志空间; wait for commit.
              sleep(&log, &log.lock);
              } else {
              log.outstanding += 1;
              release(&log.lock);
              break;
              }
              }
              }
              -]]>
              -
              - - Scheduling - /2023/01/10/xv6$chap7/ - Scheduling

              Code: Context switching

              xv6中,每个CPU中的scheduler都有一个专用的线程。这里线程的概念是,有自己的栈,有自己的寄存器状态。

              -

              当发生时钟中断时,当前进程调用yield,yield再通过swtch切换到scheduler线程。scheduler线程会通过swtch跳转到另外一个进程去执行。当另外一个进程发生时钟中断,又会通过yield回到scheduler,scheduler再调度原进程继续执行,如此周而复始。

              -
              -

              Linux的调度原理也差不多类似这样。每个CPU都有一个调度类为SCHED_CLASS_IDLE的IDLE进程,IDLE进程体大概就是间歇不断地执行__schedule()函数,CPU空闲时就会不断执行IDLE线程。

              -

              而当有新的任务产生时(或任务被唤醒。可以从此看出task new和task wakeup的共通点,可以联想到竞赛中对该消息的处理方法),它首先通过调度类对应的select_cpu选择一个合适的(可以被抢占&&在该task对应的cpumask中)的cpu,迁移到cpu对应的rq;目标cpu通过IDLE进程体或者中断返回时检查到了NEED_SCHEDULE标记位,从而调用schedule函数pick新任务,然后进行context_switch切换到目标线程。如此周而复始。

              +
              log_write
              +

              log_write充当bwrite的代理。它将块的扇区号记录在内存中,在磁盘上的日志中预定一个槽位,并调用bpin将缓存固定在block cache中,以防止block cache将其逐出【具体原理就是让refcnt++,这样就不会被当成空闲block用掉了】。

              +

              为啥要防止换出呢?换出不是就正好自动写入磁盘了吗?这里一是为了保障前面提到的原子性,防止换入换出导致的单一写入磁盘;二是换出自动写入的是磁盘对应位而不一定是日志所在的blocks。

              -

              image-20230118221757367

              -

              下面就来讲讲这个所谓的“线程”以及xv6的上下文切换是怎么实现的。

              -

              context

              上下文切换的操作对象是上下文,因而首先了解一下上下文的结构。各种寄存器的状态即是上下文context。xv6中的context定义如下:

              -
              struct context {
              uint64 ra;
              uint64 sp;

              // callee-saved
              uint64 s0;
              uint64 s1;
              uint64 s2;
              uint64 s3;
              uint64 s4;
              uint64 s5;
              uint64 s6;
              uint64 s7;
              uint64 s8;
              uint64 s9;
              uint64 s10;
              uint64 s11;
              };
              +
              void
              log_write(struct buf *b)
              {
              int i;
              // #define LOGSIZE (MAXOPBLOCKS*3) // max data blocks in on-disk log
              // 30
              if (log.lh.n >= LOGSIZE || log.lh.n >= log.size - 1)
              panic("too big a transaction");
              if (log.outstanding < 1)
              panic("log_write outside of trans");

              acquire(&log.lock);
              // log_write会注意到在单个事务中多次写入一个块的情况,并在日志中为该块分配相同的槽位。
              // 这种优化通常称为合并(absorption)
              for (i = 0; i < log.lh.n; i++) {
              if (log.lh.block[i] == b->blockno) // log absorbtion
              break;
              }
              // 这里还是挺巧妙的。
              // 如果存在log.lh.block[i] == b->blockno的情况,执行此句话也无妨
              // 如果不存在,则给log新增一块,填入log.lh.block[log.lh.n]的位置,再++log.lh.n
              log.lh.block[i] = b->blockno;
              if (i == log.lh.n) { // Add new block to log?
              bpin(b);
              log.lh.n++;
              }
              release(&log.lock);
              }
              -

              上下文切换需要修改栈和pc,context中确实有sp寄存器,但是没有pc寄存器,这主要还是因为当swtch返回时,会回到ra所指向的地方,所以仅保存ra就足够了。

              -

              swtch

              上下文的切换是通过swtch实现的。

              -
              void            swtch(struct context*, struct context*);
              +
              end_op
              // called at the end of each FS system call.
              // 如果这是最后一层outstanding就会执行commit操作
              // commits if this was the last outstanding operation.
              void
              end_op(void)
              {
              int do_commit = 0;

              acquire(&log.lock);
              log.outstanding -= 1;
              if(log.committing)
              panic("log.committing");
              if(log.outstanding == 0){
              do_commit = 1;
              log.committing = 1;
              } else {
              // begin_op() may be waiting for log space,
              // and decrementing log.outstanding has decreased
              // the amount of reserved space.
              wakeup(&log);
              }
              release(&log.lock);

              if(do_commit){
              // call commit w/o holding locks, since not allowed
              // to sleep with locks.
              commit();
              acquire(&log.lock);
              log.committing = 0;
              wakeup(&log);
              release(&log.lock);
              }
              }
              -

              swtch会把当前进程的上下文保存在第一个context中,再切换到第二个context保存的上下文,具体实现就是写读保存寄存器:

              -
              # in kernel/swtch.S
              # a0和a1分别保存着两个参数的值,也即第一个context的地址和第二个context的地址
              .globl swtch
              swtch:
              sd ra, 0(a0)
              sd sp, 8(a0)
              sd s0, 16(a0)
              sd s1, 24(a0)
              # ...
              sd s11, 104(a0)

              ld ra, 0(a1)
              ld sp, 8(a1)
              ld s0, 16(a1)
              ld s1, 24(a1)
              # ...
              ld 11, 104(a1)

              ret
              +
              commit
              static void
              commit()
              {
              if (log.lh.n > 0) {
              // cache -> log block
              write_log(); // Write modified blocks from cache to log
              // head(in stack/heap) -> log block
              // 此可以说为commit完成的标志。
              // 因为无论接下来是否崩溃,数据最终都会被写入disk,不同在于是在recover时还是接下来写入
              write_head(); // Write header to disk -- the real commit
              // log block -> real position
              install_trans(0); // Now install writes to home locations
              log.lh.n = 0;
              // 擦除
              write_head(); // Erase the transaction from the log
              }
              }
              -

              sched

              在sleep、yield和wakeup中,都会通过sched中的swtch进入scheduler线程。

              -
              void
              sched(void)
              {
              int intena;
              struct proc *p = myproc();

              if(!holding(&p->lock))
              panic("sched p->lock");
              if(mycpu()->noff != 1)
              panic("sched locks");
              if(p->state == RUNNING)
              panic("sched running");
              if(intr_get()) // 当持有锁时一定为关中断状态
              panic("sched interruptible");

              intena = mycpu()->intena;
              swtch(&p->context, &mycpu()->context);
              mycpu()->intena = intena;
              }
              +
              write_log
              // Copy modified blocks from cache to log.
              static void
              write_log(void)
              {
              int tail;

              for (tail = 0; tail < log.lh.n; tail++) {
              struct buf *to = bread(log.dev, log.start+tail+1); // log block
              struct buf *from = bread(log.dev, log.lh.block[tail]); // cache block
              memmove(to->data, from->data, BSIZE);
              bwrite(to); // write the log
              brelse(from);// 此处的brelse呼应了外界调用的bread
              brelse(to);
              }
              }
              -

              cpu中存储着的是scheduler线程的context。因而,这样就可以保存当前进程的context,读取scheduler线程的context,然后转换到scheduler的context执行了。

              +
              write_head
              // Write in-memory log header to disk.
              // 这是事务提交的标志
              // This is the true point at which the
              // current transaction commits.
              static void
              write_head(void)
              {
              struct buf *buf = bread(log.dev, log.start);
              struct logheader *hb = (struct logheader *) (buf->data);
              int i;
              hb->n = log.lh.n;
              for (i = 0; i < log.lh.n; i++) {
              hb->block[i] = log.lh.block[i];
              }
              bwrite(buf);
              brelse(buf);
              }
              + +
              install_trans
              // Copy committed blocks from log to their home location
              static void
              install_trans(int recovering)
              {
              int tail;

              for (tail = 0; tail < log.lh.n; tail++) {
              struct buf *lbuf = bread(log.dev, log.start+tail+1); // read log block
              struct buf *dbuf = bread(log.dev, log.lh.block[tail]); // read dst
              memmove(dbuf->data, lbuf->data, BSIZE); // copy block to dst
              bwrite(dbuf); // write dst to disk
              if(recovering == 0)
              bunpin(dbuf);// 如果不是在recover的过程中
              brelse(lbuf);
              brelse(dbuf);
              }
              }
              + +

              恢复与初始化

              上面介绍了log的一次事务提交的流程。接下来介绍它是怎么恢复的。

              -

              可以发现这里是有个很完美的组合技的。由sched()保存context到process结构体中,再由scheduler()读取process对应的context回归到sched()继续执行,我感觉调度设计这点真是帅的一匹。

              +

              recover_from_log是由initlog调用的,而它又是在第一个用户进程运行之前的引导期间由fsinit调用的。

              -

              scheduler

              void
              scheduler(void)
              {
              struct proc *p;
              struct cpu *c = mycpu();

              c->proc = 0;
              for(;;){
              // Avoid deadlock by ensuring that devices can interrupt.
              intr_on();

              int nproc = 0;
              for(p = proc; p < &proc[NPROC]; p++) {
              acquire(&p->lock);
              if(p->state != UNUSED) {
              nproc++;
              }
              if(p->state == RUNNABLE) {
              // Switch to chosen process. It is the process's job
              // to release its lock and then reacquire it
              // before jumping back to us.
              p->state = RUNNING;
              c->proc = p;
              swtch(&c->context, &p->context);

              // Process is done running for now.
              // It should have changed its p->state before coming back.
              c->proc = 0;
              }
              release(&p->lock);
              }
              if(nproc <= 2) { // only init and sh exist
              intr_on();
              asm volatile("wfi");
              }
              }
              }
              +
              第一个进程运行之前

              由前面scheduler一章的知识可知,每个进程被初次调度的时候会先来执行forkret。这时候就做了log的恢复工作。

              +

              注释解释了为什么不选择在main.c中初始化,而选择在此处初始化。确实,它需要调用sleep,如果在main.c中调用sleep感觉会乱套()毕竟那时候scheduler线程尚未被初始化。

              +
              // A fork child's very first scheduling by scheduler()
              // will swtch to forkret.
              void
              forkret(void)
              {
              // static变量仅会被初始化一次
              static int first = 1;

              // Still holding p->lock from scheduler.
              release(&myproc()->lock);

              // 如果是第一个进程
              if (first) {
              // File system initialization must be run in the context of a
              // regular process (e.g., because it calls sleep), and thus cannot
              // be run from main().
              first = 0;
              fsinit(ROOTDEV);
              }

              usertrapret();
              }
              -

              通过swtch进入scheduler线程后,会继续执行scheduler中swtch的下一个指令,完成下一次调度。

              -

              一些补充

              以上是书本的介绍内容。看到这想必会有很多疑惑,至少有以下两点:

              -
                -
              1. 为什么cpu->context会存储着scheduler的上下文?这是什么时候,又是怎么初始化的?
              2. -
              3. 为什么从sched中swtch会来到scheduler中swtch的下一句?
              4. -
              -

              先从第一点入手。实际上,这个初始化的工作,是在操作系统启动时的main.c中完成的。

              -
              void
              main()
              {
              if(cpuid() == 0){
              // ...
              } else {
              // ...
              }

              scheduler();
              }
              +
              fsinit
              // Init fs
              void
              fsinit(int dev) {
              // ...
              initlog(dev, &sb);
              }
              -

              在这之前,创建了第一个进程proc。在这里,每个cpu都调用了scheduler。

              -
              void
              scheduler(void)
              {
              struct proc *p;
              struct cpu *c = mycpu();

              c->proc = 0;
              for(;;){
              intr_on();

              int nproc = 0;
              for(p = proc; p < &proc[NPROC]; p++) {
              acquire(&p->lock);
              // ...
              if(p->state == RUNNABLE) {
              p->state = RUNNING;
              c->proc = p;
              swtch(&c->context, &p->context);

              c->proc = 0;
              }
              release(&p->lock);
              }
              // ...
              }
              }
              +
              initlog
              void
              initlog(int dev, struct superblock *sb)
              {
              if (sizeof(struct logheader) >= BSIZE)
              panic("initlog: too big logheader");

              initlock(&log.lock, "log");
              // 从super block中获取必要参数
              log.start = sb->logstart;
              log.size = sb->nlog;
              log.dev = dev;
              recover_from_log();
              }
              -

              每个cpu都在scheduler线程的swtch(&c->context, &p->context);中,将当前的context,也即scheduler的context存入了mycpu()->context。随后,CPU中的某个去执行下一个进程,其他的就在scheduler线程的无限循环中等待,直到有别的进程产生。

              -

              去执行进程的CPU通过swtch切换上下文,切到了另一个进程中,此时在swtch中保存的ra是scheduler线程的swtch的下一句(因为scheduler->swtch也是个函数调用的过程)。会切到另一个进程的sched的下一句【因为它正是从那边swtch过来的】,或者是那个进程开始执行的地方【下面会提到是forkret】。另一个进程通过sched切换回来的时候,就正会切到ra所指向的位置,也即切到scheduler中的swtch后面。

              -

              这样一来,两个问题就都得到了解答。

              -

              从这,我们也能知道xv6是如何让CPU运转的:scheduler线程是CPU的IDLE状态。无事的时候在scheduler中等待,并且一直监测是否有进程需要执行。有的话,则去执行该进程;该进程又会通过sched切换回scheduler线程,继续等待。这样一来,就完成了进程管理的基本的自动机图像。

              -

              Code: Scheduling

              sched前要做的事

              -

              A process that wants to give up the CPU must do three things:

              +
              recover_from_log
              static void
              recover_from_log(void)
              {
              // 读取head
              read_head();
              // 注意,commit中会把header写入log block,而这里从log block读出header
              // 也就是说,如果header的n不为零,那么说明已经commit了,但可能未写入,重复写入保障安全
              // 如果header的n为零,说明未commit,在install_trans的逻辑中会什么也不做
              // 两种情况完美满足
              install_trans(1); // if committed, copy from log to disk
              log.lh.n = 0;
              // 擦除
              write_head(); // clear the log
              }
              + +

              Code: Block allocator

              个人理解

              说实话没怎么懂,也不大清楚它有什么用,先大概推测一下:

              +

              之前的bread和bwrite这些,就是你给一个设备号和扇区号,它就帮你加载进内存cache。你如果要用的话,肯定还是使用地址方便。所以block allocator的作用之一就是给bread和bwrite加一层封装,将获取的block封装为地址返回,你可以直接操纵这个地址,而无需知道下层的细节。

              +

              这个过程要注意的有两点:

                -
              1. acquire its own process lock p->lock, release any other locks it is holding
              2. -
              3. update its own state (p->state)
              4. -
              5. call sched
              6. +
              7. 封装返回的地址具体是什么,怎么工作的

                +

                封装返回的地址实质上是buffer cache中的buf的data字段的地址【差不多】。之后的上层应用在该地址上写入,也即写入了buf,最后会通过log层真正写入磁盘。

                +
              8. +
              9. 结合bcache的LRU,详细谈谈工作机制

                +

                我们可以看到,在balloc中有这么一段逻辑:

                +
                bp = bread(dev, BBLOCK(b, sb));
                // ...
                log_write(bp);
                brelse(bp);
                return b + bi;
                + +

                看到的第一反应就是,我们需求的那块buf是bp,但是这里先是bread了一次,又是brelse了一次,这样bp的refcnt不就为0,很容易被替换掉了吗?

                +

                会有这个反应,一定程度上是因为没有很好地理解LRU。事实上,正是它可能被替换掉,才满足了LRU的条件。因为它可能被替掉才能说明它可能是最近最少使用的。

                +
              -

              yield (kernel/proc.c:515) follows this convention, as do sleep and exit.

              -

              sched double-checks those conditions (kernel/proc.c:499-504) and then an implication of those conditions: since a lock is held, interrupts should be disabled.

              +

              bitmap

              +

              文件和目录内容存储在磁盘块中,磁盘块必须从空闲池中分配。xv6的块分配器在磁盘上维护一个空闲位图,每一位代表一个块。0表示对应的块是空闲的;1表示它正在使用中。

              +

              引导扇区、超级块、日志块、inode块和位图块的比特位是由程序mkfs初始化设置的:

              +

              image-20230123234919055

              -

              sched与scheduler

              在上面的描述我们可以看到,schedscheduler联系非常密切,他们俩通过swtch相互切来切去,并且一直都只在这几行切来切去:

              -
              // in scheduler()
              swtch(&c->context, &p->context);
              c->proc = 0;
              // in sched()
              swtch(&p->context, &mycpu()->context);
              mycpu()->intena = intena;
              +

              allocator

              类似于memory allocator,块分配器也提供了两个函数:bfreeballoc

              +

              balloc

              +

              Balloc从块0到sb.size(文件系统中的块数)遍历每个块。它查找位图中位为零的空闲块。如果balloc找到这样一个块,它将更新位图并返回该块。

              +

              为了提高效率,循环被分成两部分。外部循环读取位图中的每个块。内部循环检查单个位图块中的所有BPB位。由于任何一个位图块在buffer cache中一次只允许一个进程使用【 bread(dev, BBLOCK(b, sb))会返回一个上锁的block,breadbrelse隐含的独占使用避免了显式锁定的需要】,因此,如果两个进程同时尝试分配一个块也是并发安全的。

              +
              +
              // Allocate a zeroed disk block.
              static uint
              balloc(uint dev)
              {
              int b, bi, m;
              struct buf *bp;

              bp = 0;
              for(b = 0; b < sb.size; b += BPB){
              bp = bread(dev, BBLOCK(b, sb));
              for(bi = 0; bi < BPB && b + bi < sb.size; bi++){
              m = 1 << (bi % 8);
              if((bp->data[bi/8] & m) == 0){ // Is block free?
              bp->data[bi/8] |= m; // Mark block in use.
              log_write(bp);
              brelse(bp);
              bzero(dev, b + bi);
              return b + bi;
              }
              }
              brelse(bp);
              }
              panic("balloc: out of blocks");
              }
              -

              在两个线程之间进行这种样式化切换的过程有时被称为协程(coroutines)。

              -
              -

              存在一种情况使得调度程序对swtch的调用没有以sched结束。一个新进程第一次被调度时,它从forkretkernel/proc.c:527)开始。Forkret是为了释放p->lock而包装的,要不然,新进程可以从usertrapret开始。

              +

              bfree

              // Free a disk block.
              static void
              bfree(int dev, uint b)
              {
              struct buf *bp;
              int bi, m;

              bp = bread(dev, BBLOCK(b, sb));
              bi = b % BPB;
              m = 1 << (bi % 8);
              if((bp->data[bi/8] & m) == 0)
              panic("freeing free block");
              bp->data[bi/8] &= ~m;
              log_write(bp);
              brelse(bp);
              }
              + +

              Inode layer

              inode

              +

              术语inode(即索引结点)可以具有两种相关含义之一。它可能是指包含文件大小和数据块编号列表的磁盘上的数据结构【on-disk inode】。或者“inode”可能指内存中的inode【in-memory inode】,它包含磁盘上inode的副本以及内核中所需的额外信息。

              -

              p->lock保证了并发安全性

              -

              考虑调度代码结构的一种方法是,它为每个进程强制维持一个不变性条件的集合,并在这些不变性条件不成立时持有p->lock

              -

              其中一个不变性条件是:如果进程是RUNNING状态,计时器中断的yield必须能够安全地从进程中切换出去;这意味着CPU寄存器必须保存进程的寄存器值(即swtch没有将它们移动到context中),并且c->proc必须指向进程。另一个不变性条件是:如果进程是RUNNABLE状态,空闲CPU的调度程序必须安全地运行它;这意味着p->context必须保存进程的寄存器(即,它们实际上不在实际寄存器中),没有CPU在进程的内核栈上执行,并且没有CPU的c->proc引用进程。

              -

              维护上述不变性条件是xv6经常在一个线程中获取p->lock并在另一个线程中释放它的原因,在保持p->lock时,这些属性通常不成立。

              -

              例如在yield中获取并在scheduler中释放。一旦yield开始修改一个RUNNING进程的状态为RUNNABLE,锁必须保持被持有状态,直到不变量恢复:最早的正确释放点是scheduler(在其自身栈上运行)清除c->proc之后。类似地,一旦scheduler开始将RUNNABLE进程转换为RUNNING,在内核线程完全运行之前(在swtch之后,例如在yield中)绝不能释放锁。

              -

              p->lock还保护其他东西:exitwait之间的相互作用,避免丢失wakeup的机制(参见第7.5节),以及避免一个进程退出和其他进程读写其状态之间的争用(例如,exit系统调用查看p->pid并设置p->killed(kernel/proc.c:611))。为了清晰起见,也许为了性能起见,有必要考虑一下p->lock的不同功能是否可以拆分。

              +

              image-20230121162324747

              +

              on-disk inode

              +

              The on-disk inodes are packed into a contiguous area of disk called the inode blocks.

              +

              Every inode is the same size, so it is easy, given a number n, to find the nth inode on the disk. In fact, this number n, called the inode number or i-number, is how inodes are identifified in the implementation.

              -

              p->lock在每次scheduler开始的时候获取,swtch到p进程的时候在yield等调用完sched的地方释放。而调用yield时获取的锁,又会在scheduler中释放。

              -
              // Give up the CPU for one scheduling round.
              void
              yield(void)
              {
              struct proc *p = myproc();
              acquire(&p->lock);// 该锁会在scheduler中释放
              p->state = RUNNABLE;
              sched();
              release(&p->lock);// 该锁释放的是scheduler中得到的锁
              }
              +
              // in fs.h
              // On-disk inode structure
              struct dinode {
              // 为0表示free
              short type; // File type
              short major; // Major device number (T_DEVICE only)
              short minor; // Minor device number (T_DEVICE only)
              // The nlink field counts the number of directory entries that refer to this inode,
              // in order to recognize when the on-disk inode and its data blocks should be freed.
              short nlink; // Number of links to inode in file system
              uint size; // Size of file (bytes)
              uint addrs[NDIRECT+1]; // Data block addresses
              };
              -
              // in kernel/proc.c scheduler()
              acquire(&p->lock);// 该锁会在yield等地被释放
              // ...
              swtch(&c->context, &p->context);
              // ...
              release(&p->lock);// 该锁会释放yield等地中获得的锁
              +

              in-memory inode

              +

              The kernel keeps the set of active inodes in memory.

              +

              The kernel stores an inode in memory only if there are C pointers referring to that inode.当且仅当ref==0才会从内核中释放。

              +

              如果nlinks==0就会从物理block中释放。

              +

              The iget and iput functions acquire and release pointers to an inode, modifying the reference count.【相当于buffer cache的ballocbfree】Pointers to an inode can come from file descriptors, current working directories, and transient kernel code such as exec.

              +

              iget返回的struct inode可能没有任何有用的内容。为了确保它保存磁盘inode的副本,代码必须调用ilock。这将锁定inode(以便没有其他进程可以对其进行ilock),并从磁盘读取尚未读取的inode。iunlock释放inode上的锁。将inode指针的获取与锁定分离有助于在某些情况下避免死锁,例如在目录查找期间。多个进程可以持有指向iget返回的inode的C指针,但一次只能有一个进程锁定inode。

              +
              +
              //in file.h
              // in-memory copy of an inode
              struct inode {
              uint dev; // Device number
              uint inum; // Inode number
              int ref; // Reference count
              struct sleeplock lock; // protects everything below here
              int valid; // inode has been read from disk?

              short type; // copy of disk inode
              short major;
              short minor;
              short nlink;
              uint size;
              uint addrs[NDIRECT+1];// 存储着inode数据的blocks的地址,从balloc中获取
              };
              -

              不得不说,这结构实在是太精妙了。这中间的如此多的复杂过程,就这样成功地被锁保护了起来。

              -

              Code: mycpu and myproc

              // Per-CPU state.
              struct cpu {
              struct proc *proc; // The process running on this cpu, or null.
              struct context context; // swtch() here to enter scheduler().
              int noff; // Depth of push_off() nesting.
              int intena; // Were interrupts enabled before push_off()?
              };
              +

              Code: inode

              +

              主要是在讲inode layer这一层的方法,以及给上层提供的接口。

              +
              +

              Overview

              image-20230124153309132

              +

              底层接口

              +

              iget iput

              +
              +
              iget

              逻辑还是跟buffer cache非常相似的,不过可以看出这个的数据结构简单许多,也不用实现LRU。

              +
              +

              A struct inode pointer returned by iget() is guaranteed to be valid until the corresponding call to iput(): the inode won’t be deleted, and the memory referred to by the pointer won’t be re-used for a different inode. 【通过ref++实现。】

              +

              不同于buffer cache的bgetiget()提供对inode的非独占访问,因此可以有许多指向同一inode的指针。文件系统代码的许多部分都依赖于iget()的这种行为,既可以保存对inode的长期引用(如打开的文件和当前目录),也可以防止争用,同时避免操纵多个inode(如路径名查找)的代码产生死锁。

              +
              +
              // Find the inode with number inum on device dev
              // and return the in-memory copy. Does not lock
              // the inode and does not read it from disk.
              static struct inode*
              iget(uint dev, uint inum)
              {
              struct inode *ip, *empty;

              acquire(&icache.lock);

              // Is the inode already cached?
              empty = 0;
              for(ip = &icache.inode[0]; ip < &icache.inode[NINODE]; ip++){
              if(ip->ref > 0 && ip->dev == dev && ip->inum == inum){
              ip->ref++;
              release(&icache.lock);
              return ip;
              }
              // 由于不用实现LRU,所以只需一次循环记录即可。
              if(empty == 0 && ip->ref == 0) // Remember empty slot.
              empty = ip;
              }

              // Recycle an inode cache entry.
              if(empty == 0)
              panic("iget: no inodes");

              ip = empty;
              ip->dev = dev;
              ip->inum = inum;
              ip->ref = 1;
              // does not read from disk
              ip->valid = 0;
              release(&icache.lock);

              return ip;
              }
              -

              mycpu是通过获取当前cpuid来获取cpu结构的。当前使用的cpuid约定俗成地存在了tp寄存器里。为了让mycpu有效工作,必须确保tp寄存器始终存放的是当前cpu的hartid。

              -

              首先是在操作系统初始化的时候要把cpuid存入tp寄存器。RISC-V规定,mhartid也即cpuid的存放点只能在machine mode被读取。因而这项工作得在start.c中完成:

              -
              // in kernel/start.c 
              // keep each CPU's hartid in its tp register, for cpuid().
              int id = r_mhartid();
              w_tp(id);
              // in kernel/riscv.h
              // which hart (core) is this?
              static inline uint64
              r_mhartid()
              {
              uint64 x;
              asm volatile("csrr %0, mhartid" : "=r" (x) );
              return x;
              }
              +
              iput
              +

              iput()可以写入磁盘。这意味着任何使用文件系统的系统调用都可能写入磁盘,因为系统调用可能是最后一个引用该文件的系统调用。即使像read()这样看起来是只读的调用,也可能最终调用iput()。这反过来意味着,即使是只读系统调用,如果它们使用文件系统,也必须在事务中进行包装。

              +

              iput()和崩溃之间存在一种具有挑战性的交互。iput()不会在文件的链接计数降至零时立即截断文件,因为某些进程可能仍在内存中保留对inode的引用:进程可能仍在读取和写入该文件,因为它已成功打开该文件。但是,如果在最后一个进程关闭该文件的文件描述符之前发生崩溃,则该文件将被标记为已在磁盘上分配,但没有目录项指向它。如果不做任何处理措施的话,这块磁盘就再也用不了了。

              +

              文件系统以两种方式之一处理这种情况。简单的解决方案用于恢复时:重新启动后,文件系统会扫描整个文件系统,以查找标记为已分配但没有指向它们的目录项的文件。如果存在任何此类文件,接下来可以将其释放。

              +

              第二种解决方案不需要扫描文件系统。在此解决方案中,文件系统在磁盘(例如在超级块中)上记录链接计数降至零但引用计数不为零的文件的i-number。如果文件系统在其引用计数达到0时删除该文件,则会通过从列表中删除该inode来更新磁盘列表。重新启动时,文件系统将释放列表中的所有文件。

              +

              Xv6没有实现这两种解决方案,这意味着inode可能被标记为已在磁盘上分配,即使它们不再使用。这意味着随着时间的推移,xv6可能会面临磁盘空间不足的风险。

              +
              +
              // Drop a reference to an in-memory inode.
              // If that was the last reference, the inode cache entry can
              // be recycled.【refvnt==0 可以回收】
              // 注意这个回收过程无需特别处理,只需自然--refcnt就行,不用像buffer cache那么烦
              // If that was the last reference and the inode has no links
              // to it, free the inode (and its content) on disk.【nlinks==0 copy和本体都得扔掉】
              // All calls to iput() must be inside a transaction in
              // case it has to free the inode.任何需要iput的地方都需要包裹在事务内,因为它可能会释放inode
              void
              iput(struct inode *ip)
              {
              acquire(&icache.lock);

              if(ip->ref == 1 && ip->valid && ip->nlink == 0){
              // inode has no links and no other references: truncate and free.

              // ip->ref == 1 means no other process can have ip locked,
              // so this acquiresleep() won't block (or deadlock).
              acquiresleep(&ip->lock);

              release(&icache.lock);

              // 最终调用bfree,会标记bitmap,完全释放block
              itrunc(ip);
              ip->type = 0;

              /*iupdate:
              // Copy a modified in-memory inode to disk.
              // Must be called after every change to an ip->xxx field
              // that lives on disk, since i-node cache is write-through.
              write-through:
              CPU向cache写入数据时,同时向memory(后端存储)也写一份,使cache和memory的数据保持一致。
              */
              // 这里修改的type是dinode也有的字段,所以需要update一下。
              // 下面的valid是dinode没有的字段,所以随便改,无需update
              iupdate(ip);
              ip->valid = 0;

              releasesleep(&ip->lock);

              acquire(&icache.lock);
              }

              ip->ref--;
              release(&icache.lock);
              }
              -

              在内核态中,编译器被设置为保证不会以其他方式使用tp寄存器。因而初始化之后,内核态中每个CPU的tp寄存器就始终存放着自己的cpuid。

              -

              但这在用户进程是不成立的。因而必须在用户进程进入陷阱的时候做一些工作。

              -
              # in kernel/trampoline.S uservec
              sd tp, 64(a0)
              # make tp hold the current hartid, from p->trapframe->kernel_hartid
              ld tp, 32(a0)
              +

              上层接口

              获取和释放inode
              ialloc
              // Allocate an inode on device dev.
              // Mark it as allocated by giving it type type.
              // Returns an unlocked but allocated and referenced inode.
              struct inode*
              ialloc(uint dev, short type)
              {
              int inum;
              struct buf *bp;
              struct dinode *dip;

              for(inum = 1; inum < sb.ninodes; inum++){
              bp = bread(dev, IBLOCK(inum, sb));
              dip = (struct dinode*)bp->data + inum%IPB;
              if(dip->type == 0){ // a free inode通过type判断是否free
              memset(dip, 0, sizeof(*dip));// zerod
              dip->type = type;
              log_write(bp); // mark it allocated on the disk
              brelse(bp);
              return iget(dev, inum);
              }
              brelse(bp);
              }
              panic("ialloc: no inodes");
              }
              -
              struct trapframe {
              /* 32 */ uint64 kernel_hartid; // saved kernel tp
              /* 64 */ uint64 tp;
              // ...
              }
              +
              inode的锁保护

              前面说到,inode的设计使得有多个指针同时指向一个inode成为了可能。因而,修改使用inode的时候就要对其进行独占访问。使用ialloc获取和用ifree释放的inode必须被保护在ilockiunlock区域中。

              +
              ilock

              ilock既可以实现对inode的独占访问,同时也可以给未初始化的inode进行初始化工作。

              +
              +

              iget返回的struct inode可能没有任何有用的内容。为了确保它保存磁盘inode的副本,代码必须调用ilock。这将锁定inode(以便没有其他进程可以对其进行ilock),并从磁盘读取尚未读取的inode。

              +
              +
              // Lock the given inode and reads the inode from disk if necessary.
              void
              ilock(struct inode *ip)
              {
              struct buf *bp;
              struct dinode *dip;

              if(ip == 0 || ip->ref < 1)
              panic("ilock");

              acquiresleep(&ip->lock);

              if(ip->valid == 0){
              // 通过inode索引号和superblock算出扇区号
              bp = bread(ip->dev, IBLOCK(ip->inum, sb));
              dip = (struct dinode*)bp->data + ip->inum%IPB;
              // 填充ip
              ip->type = dip->type;
              ip->major = dip->major;
              ip->minor = dip->minor;
              ip->nlink = dip->nlink;
              ip->size = dip->size;
              memmove(ip->addrs, dip->addrs, sizeof(ip->addrs));
              brelse(bp);
              ip->valid = 1;
              if(ip->type == 0)
              panic("ilock: no type");
              }
              }
              -

              必须在trampoline保存用户态中使用的tp值,以及内核态中对应的hartid。

              -

              最后再在返回用户态的时候恢复用户态的tp值以及更新trampoline的tp值。

              -
              // in kernel/trap.c usertrapret()
              p->trapframe->kernel_hartid = r_tp(); // hartid for cpuid()
              +
              iunlock
              +

              iunlock释放inode上的锁。

              +

              将inode指针的获取与锁定分离有助于在某些情况下避免死锁,例如在目录查找期间。多个进程可以持有指向iget返回的inode的C指针,但一次只能有一个进程锁定inode。

              +
              +
              // Unlock the given inode.
              void
              iunlock(struct inode *ip)
              {
              if(ip == 0 || !holdingsleep(&ip->lock) || ip->ref < 1)
              panic("iunlock");

              releasesleep(&ip->lock);
              }
              -
              # in trampoline.S userret
              ld tp, 64(a0)
              +

              Code: inode content

              Overview

              +

              主要讲的是inode本身存储数据的结构

              +
              +
              +

              磁盘上的inode结构体struct dinode包含一个size和一个块号数组(见图8.3),数组内罗列着存储着该inode数据的块号。

              +

              前面的NDIRECT个数据块被列在数组中的前NDIRECT个元素中;这些块称为直接块(direct blocks)。接下来的NINDIRECT个数据块不在inode中列出,而是在称为间接块(indirect block)的数据块中列出。addrs数组中的最后一个元素给出了间接块的地址。

              +

              因此,可以从inode中列出的块加载文件的前12 kB(NDIRECT x BSIZE)字节,而只有在查阅间接块后才能加载下一个256 kB(NINDIRECT x BSIZE)字节。

              +
              +
              // On-disk inode structure
              struct dinode {
              // ...
              uint addrs[NDIRECT+1]; // Data block addresses
              };
              -

              注意,更新trampoline的tp值这一步很重要。因为如果在用户态发生的是时钟中断,就会引起yield,可能造成CPU的切换。这时候就需要在返回用户态的时候修改一下trapframe中的tp为当前CPU的tp。这样一来才能保证,在本次时钟中断结束,以及到下一次时钟中断修改CPU这一期间,trapframe中的tp寄存器以及内核态中的tp寄存器都是正确的。

              -

              通过mycpu()获取cpuid其实是非常脆弱的。因为你可能获取完cpuid,进程就被切到别的CPU去执行了,这就会有一个先检查后执行的竞态条件,具有并发安全隐患。因而,xv6要求使用mycpu()返回值的这段代码需要关中断,这样就可以避免时钟中断造成的进程切换了。比如说像myproc()这样:

              -
              // Return the current struct proc *, or zero if none.
              struct proc*
              myproc(void) {
              push_off();
              struct cpu *c = mycpu();
              struct proc *p = c->proc;
              pop_off();
              return p;
              }
              +

              image-20230124163025094

              +

              bmap

              +

              函数bmap负责封装这个寻找数据块的过程,以便实现我们将很快看到的如readiwritei这样的更高级例程。

              +

              bmap(struct inode *ip, uint bn)返回inodeip的第bn个数据块的磁盘块号。如果ip还没有这样的块,bmap会分配一个。

              +

              Bmap使readiwritei很容易获取inode的数据。

              +
              +
              // Inode content
              //
              // The content (data) associated with each inode is stored
              // in blocks on the disk. The first NDIRECT block numbers
              // are listed in ip->addrs[]. The next NINDIRECT blocks are
              // listed in block ip->addrs[NDIRECT].

              // Return the disk block address of the nth block in inode ip.
              // If there is no such block, bmap allocates one.
              static uint
              bmap(struct inode *ip, uint bn)
              {
              uint addr, *a;
              struct buf *bp;

              // 如果为direct block
              if(bn < NDIRECT){
              if((addr = ip->addrs[bn]) == 0)
              ip->addrs[bn] = addr = balloc(ip->dev);
              return addr;
              }
              bn -= NDIRECT;

              // 如果为indirect block
              if(bn < NINDIRECT){
              // Load indirect block, allocating if necessary.
              if((addr = ip->addrs[NDIRECT]) == 0)
              ip->addrs[NDIRECT] = addr = balloc(ip->dev);
              bp = bread(ip->dev, addr);
              a = (uint*)bp->data;
              if((addr = a[bn]) == 0){
              // 如果没有,会分配一个
              a[bn] = addr = balloc(ip->dev);
              log_write(bp);
              }
              brelse(bp);
              return addr;
              }

              panic("bmap: out of range");
              }
              -

              注意,不同于mycpu(),使用myproc()的返回值不需要进行开关中断保护。因为当前进程的指针不论处于哪个CPU都是不变的。

              -

              Sleep and wakeup

              前面我们已经介绍了进程隔离性的基本图像,接下来要讲xv6是如何让进程之间互动的。xv6使用的是经典的sleep and wakeup,也叫序列协调(sequence coordination)条件同步机制(conditional synchronization mechanisms。下面,将从最基本的自旋锁实现信号量开始,来逐步讲解xv6的sleep and wakeup机制。

              -

              自旋锁实现信号量

              image-20230120150659730

              -

              image-20230120150715925

              -

              缺点就是自旋太久了,因而我们需要在等待的时候调用yield,直到资源生产出来之后再继续执行。

              -

              不安全的sleep and wakeup

              -

              Let’s imagine a pair of calls, sleep and wakeup, that work as follows:

              -
                -
              1. sleep(chan)

                -

                Sleeps on the arbitrary value chan, called the wait channel. Sleep puts the calling process to sleep, releasing the CPU for other work.

                -
              2. -
              3. wakeup(chan)

                -

                Wakes all processes sleeping on chan (if any), causing their sleep calls to return. If no processes are waiting on chan, wakeup does nothing.

                -
              4. -
              +

              itrunc

              +

              itrunc释放文件的块,将inode的size重置为零。

              +

              Itrunc首先释放直接块,然后释放间接块中列出的块,最后释放间接块本身。

              -

              这样一来,信号量实现就可修改为这样了:

              -

              image-20230120151051989

              -

              但是,我们可以注意到,在212-213行这里产生了一个先检查后执行的竞态条件。

              -
              -

              如果消费者进程执行到212-213中间,此时生产者进程已经调用结束,也就是说wakeup并没有唤醒任何消费者进程。消费者进程就会一直在sleep中没人唤醒,除非生产者进程再执行一次。这样就会造成lost wake-up 这个问题。

              +

              readi

              +

              readiwritei都是从检查ip->type == T_DEV开始的。这种情况处理的是数据不在文件系统中的特殊设备;我们将在文件描述符层返回到这种情况。

              -

              所以,我们可以选择把这个竞态条件也放入s->lock这个锁区域保护。

              -

              image-20230120151353712

              -

              但是这样一来又会产生死锁问题。因而,我们可以尝试着修改sleep和wakeup的接口定义。

              -

              sleep and wakeup

              -

              We’ll fix the preceding scheme by changing sleep’s interface:

              -

              The caller must pass the condition lock to sleep so it can release the lock after the calling process is marked as asleep and waiting on the sleep channel. The lock will force a concurrent V to wait until P has finished putting itself to sleep, so that the wakeup will find the sleeping consumer and wake it up. Once the consumer is awake again sleep reacquires the lock before returning.

              -

              也即在sleep中:

              -
              sleep(s,&s->lock){
              // do something
              release(&s->lock);
              //wait until wakeup
              acquire(&s->lock);
              return;
              }
              +
              // Read data from inode.数据大小为n,从off开始,读到dst处
              // Caller must hold ip->lock.
              // If user_dst==1, then dst is a user virtual address;
              // otherwise, dst is a kernel address.
              int
              readi(struct inode *ip, int user_dst, uint64 dst, uint off, uint n)
              {
              uint tot, m;
              struct buf *bp;

              if(off > ip->size || off + n < off)
              return 0;
              if(off + n > ip->size)
              n = ip->size - off;

              // 主循环处理文件的每个块,将数据从缓冲区复制到dst
              for(tot=0; tot<n; tot+=m, off+=m, dst+=m){
              bp = bread(ip->dev, bmap(ip, off/BSIZE));
              m = min(n - tot, BSIZE - off%BSIZE);
              if(either_copyout(user_dst, dst, bp->data + (off % BSIZE), m) == -1) {
              brelse(bp);
              tot = -1;
              break;
              }
              brelse(bp);
              }
              return tot;
              }
              + +

              writei

              // Write data to inode.
              // Caller must hold ip->lock.
              // If user_src==1, then src is a user virtual address;
              // otherwise, src is a kernel address.
              int
              writei(struct inode *ip, int user_src, uint64 src, uint off, uint n)
              {
              uint tot, m;
              struct buf *bp;

              if(off > ip->size || off + n < off)
              return -1;
              // writei会自动增长文件,除非达到文件的最大大小
              if(off + n > MAXFILE*BSIZE)
              return -1;

              for(tot=0; tot<n; tot+=m, off+=m, src+=m){
              bp = bread(ip->dev, bmap(ip, off/BSIZE));
              m = min(n - tot, BSIZE - off%BSIZE);
              if(either_copyin(bp->data + (off % BSIZE), user_src, src, m) == -1) {
              brelse(bp);
              n = -1;
              break;
              }
              log_write(bp);
              brelse(bp);
              }

              if(n > 0){
              if(off > ip->size)
              // 说明扩大了文件大小,需要修改
              ip->size = off;
              // write the i-node back to disk even if the size didn't change
              // because the loop above might have called bmap() and added a new
              // block to ip->addrs[].
              iupdate(ip);
              }

              return n;
              }
              + +

              stati

              +

              函数stati将inode元数据复制到stat结构体中,该结构通过stat系统调用向用户程序公开。

              -

              这样一来,信号量就可以完美实现了:

              -

              image-20230120151807102

              -

              image-20230120151820455

              -
              -

              注:严格地说,wakeup只需跟在acquire之后就足够了(也就是说,可以在release之后调用wakeup

              -

              【想了一下,有一说一确实,放在release前后都不影响】

              +

              defs.h中可看到inode结构体是private的,而stat是public的。

              +

              Directory layer

              数据结构

              +

              目录的内部实现很像文件。其inode的typeT_DIR,其数据是directory entries的集合。

              +

              每个entry都是一个struct dirent

              -
              -

              原始Unix内核的sleep只是禁用了中断,这就足够了,因为Unix运行在单CPU系统上。因为xv6在多处理器上运行,所以它为sleep添加了一个显式锁。

              +

              也就是说这一层其实本质上是一个大小一定的map,该map自身也存放在inode中,大小为inode的大小,每个表项entry映射了目录名和文件inode。所以接下来介绍的函数我们完全可以从hashmap增删改查的角度去理解。

              +
              // Directory is a file containing a sequence of dirent structures.
              #define DIRSIZ 14

              struct dirent {
              ushort inum;// 如果为0,说明该entry free
              char name[DIRSIZ];
              };
              + +

              image-20230124173241241

              +

              相关函数

              dirlookup

              +

              函数dirlookup在directory中搜索具有给定名称的entry。

              +

              它返回的指向enrty.inum相应的inode是非独占的【通过iget获取】,也即无锁状态。它还会把*poff设置为所需的entry的字节偏移量。

              +

              为什么要返回未锁定的inode?是因为调用者已锁定dp,因此,如果对.进行查找,则在返回之前尝试锁定inode将导致重新锁定dp并产生死锁【确实】(还有更复杂的死锁场景,涉及多个进程和..,父目录的别名。.不是唯一的问题。)

              +

              所以锁定交给caller来做。caller可以解锁dp,然后锁定该函数返回的ip,确保它一次只持有一个锁。

              -

              Code: Sleep and wakeup

              // Atomically release lock and sleep on chan.
              // Reacquires lock when awakened.
              void
              sleep(void *chan, struct spinlock *lk)
              {
              struct proc *p = myproc();

              // Must acquire p->lock in order to
              // change p->state and then call sched.
              // Once we hold p->lock, we can be
              // guaranteed that we won't miss any wakeup
              // (wakeup locks p->lock),
              // so it's okay to release lk.
              if(lk != &p->lock){ //DOC: sleeplock0
              // 获取进程锁,释放外部锁
              // 此进程锁将在scheduler线程中释放
              acquire(&p->lock); //DOC: sleeplock1
              release(lk);
              }

              // Go to sleep.
              p->chan = chan;
              p->state = SLEEPING;

              sched();
              // 到这里来,说明已经被wakeup且被调度了

              // Tidy up.
              p->chan = 0;

              // Reacquire original lock.
              if(lk != &p->lock){
              //释放进程锁,获取外部锁
              // 此进程锁是在scheduler中获取到的
              release(&p->lock);
              acquire(lk);
              }
              }
              +
              // Look for a directory entry in a directory.
              // If found, set *poff to byte offset of entry.
              struct inode*
              dirlookup(struct inode *dp, char *name, uint *poff)
              {
              uint off, inum;
              struct dirent de;

              if(dp->type != T_DIR)
              panic("dirlookup not DIR");
              // new level of abstraction,可以把directory的inode看作一个表文件,每个表项都是一个entry
              for(off = 0; off < dp->size; off += sizeof(de)){
              // 从directory中获取entry,也即从inode中获取数据
              if(readi(dp, 0, (uint64)&de, off, sizeof(de)) != sizeof(de))
              panic("dirlookup read");
              // free
              if(de.inum == 0)
              continue;
              if(namecmp(name, de.name) == 0){
              // entry matches path element
              if(poff)
              *poff = off;
              inum = de.inum;
              return iget(dp->dev, inum);
              }
              }

              return 0;
              }
              -

              注意,如果lk为p->lock,那么lk依然会在scheduler线程中被暂时释放

              -
              // Wake up all processes sleeping on chan.
              // Must be called without any p->lock.
              void
              wakeup(void *chan)
              {
              struct proc *p;

              for(p = proc; p < &proc[NPROC]; p++) {
              acquire(&p->lock);
              if(p->state == SLEEPING && p->chan == chan) {
              p->state = RUNNABLE;
              }
              release(&p->lock);
              }
              }
              +
              // Write a new directory entry (name, inum) into the directory dp.
              int
              dirlink(struct inode *dp, char *name, uint inum)
              {
              int off;
              struct dirent de;
              struct inode *ip;

              // Check that name is not present.
              if((ip = dirlookup(dp, name, 0)) != 0){
              iput(ip);
              return -1;
              }

              // Look for an empty dirent.
              for(off = 0; off < dp->size; off += sizeof(de)){
              if(readi(dp, 0, (uint64)&de, off, sizeof(de)) != sizeof(de))
              panic("dirlink read");
              if(de.inum == 0)
              break;
              }

              // 如果没找到空闲的则调用writei自动增长inode,添加新表项
              strncpy(de.name, name, DIRSIZ);
              de.inum = inum;
              if(writei(dp, 0, (uint64)&de, off, sizeof(de)) != sizeof(de))
              panic("dirlink");

              return 0;
              }
              -

              可以注意到,关于chan这一变量的取值是非常任意的,仅需取一个约定俗成的值就OK。这里取为了信号量的地址,同时满足了逻辑需求和语义需求。

              -
              -

              Callers of sleep and wakeup can use any mutually convenient number as the channel. Xv6 often uses the address of a kernel data structure involved in the waiting.

              +

              Pathname layer

              +

              Path name lookup involves a succession of calls to dirlookup, one for each path component.

              -

              这里也解释了为什么需要while循环

              -
              -

              有时,多个进程在同一个通道上睡眠;例如,多个进程读取同一个管道。一个单独的wakeup调用就能把他们全部唤醒。其中一个将首先运行并获取与sleep一同调用的锁,并且(在管道例子中)读取在管道中等待的任何数据。尽管被唤醒,其他进程将发现没有要读取的数据。从他们的角度来看,醒来是“虚假的”,他们必须再次睡眠。因此,在检查条件的循环中总是调用sleep

              +

              namei和nameiparent

              +

              Namei (kernel/fs.c:661) evaluates path and returns the corresponding inode.

              +

              函数nameiparent是一个变体:它在最后一个元素之前停止,返回父目录的inode并将最后一个元素复制到name中。两者都调用通用函数namex来完成实际工作。

              -

              Code: Pipes

              pipes很显然就是生产者消费者模式的一个例证。

              -
              struct pipe {
              struct spinlock lock;
              char data[PIPESIZE];
              uint nread; // number of bytes read
              uint nwrite; // number of bytes written
              int readopen; // read fd is still open
              int writeopen; // write fd is still open
              };
              +
              struct inode*
              namei(char *path)
              {
              char name[DIRSIZ];
              return namex(path, 0, name);
              }
              -
              int
              piperead(struct pipe *pi, uint64 addr, int n)
              {
              int i;
              struct proc *pr = myproc();
              char ch;

              acquire(&pi->lock);
              while(pi->nread == pi->nwrite && pi->writeopen){ //DOC: pipe-empty并且依然有进程在写
              if(pr->killed){
              release(&pi->lock);
              return -1;
              }
              // 等待直到pipe不为空
              sleep(&pi->nread, &pi->lock); //DOC: piperead-sleep
              }
              for(i = 0; i < n; i++){ //DOC: piperead-copy
              if(pi->nread == pi->nwrite)
              break;
              ch = pi->data[pi->nread++ % PIPESIZE];
              if(copyout(pr->pagetable, addr + i, &ch, 1) == -1)
              break;
              }
              // 唤醒写入管道的进程
              wakeup(&pi->nwrite); //DOC: piperead-wakeup
              release(&pi->lock);
              return i;
              }
              +
              struct inode*
              nameiparent(char *path, char *name)
              {
              return namex(path, 1, name);
              }
              -
              int
              pipewrite(struct pipe *pi, uint64 addr, int n)
              {
              int i;
              char ch;
              struct proc *pr = myproc();

              acquire(&pi->lock);
              for(i = 0; i < n; i++){
              while(pi->nwrite == pi->nread + PIPESIZE){ //DOC: pipewrite-full管道满则阻塞
              if(pi->readopen == 0 || pr->killed){
              release(&pi->lock);
              return -1;
              }
              // 唤醒读取管道的进程
              wakeup(&pi->nread);
              sleep(&pi->nwrite, &pi->lock);
              }
              if(copyin(pr->pagetable, &ch, addr + i, 1) == -1)
              break;
              pi->data[pi->nwrite++ % PIPESIZE] = ch;
              }
              wakeup(&pi->nread);
              release(&pi->lock);
              return i;
              }
              +

              namex

              +

              Namex首先决定路径解析的开始位置。

              +

              如果路径以“ / ”开始,则从根目录开始解析;否则,从当前目录开始。

              +

              然后,它使用skipelem依次考察路径的每个元素。循环的每次迭代都必须在当前索引结点ip中查找name

              +

              迭代首先给ip上锁并检查它是否是一个目录。如果不是,则查找失败。

              +

              如果caller是nameiparent,并且这是最后一个路径元素,则根据nameiparent的定义,循环会提前停止;最后一个路径元素已经复制到name中【在上一轮循坏中做了这件事】,因此namex只需返回解锁的ip

              +

              最后,循环将使用dirlookup查找路径元素,并通过设置ip = next为下一次迭代做准备。当循环用完路径元素时,它返回ip

              +

              注:

              +
                +
              1. 在每次迭代中锁定ip是必要的,不是因为ip->type可以被更改,而是因为在ilock运行之前,ip->type不能保证已从磁盘加载,所以得用到ilock保证一定会被加载的这个性质。
              2. +
              +
              +
              // Look up and return the inode for a path name.
              // If parent != 0, return the inode for the parent and copy the final
              // path element into name, which must have room for DIRSIZ bytes.
              // Must be called inside a transaction since it calls iput().
              static struct inode*
              namex(char *path, int nameiparent, char *name)
              {
              struct inode *ip, *next;

              if(*path == '/')
              ip = iget(ROOTDEV, ROOTINO);
              else
              ip = idup(myproc()->cwd);

              // 使用skipelem依次考察路径的每个元素
              while((path = skipelem(path, name)) != 0){
              ilock(ip);
              if(ip->type != T_DIR){
              iunlockput(ip);
              return 0;
              }
              if(nameiparent && *path == '\0'){
              // Stop one level early.
              iunlock(ip);
              return ip;
              }
              if((next = dirlookup(ip, name, 0)) == 0){
              iunlockput(ip);
              return 0;
              }
              iunlockput(ip);
              ip = next;
              }
              if(nameiparent){
              iput(ip);
              return 0;
              }
              return ip;
              }
              -

              一个非常有意思且巧妙的点,就是读写管道等待在不同的chan上,这与上面信号量的例子是不一样的。想想也确实,如果使用同一个管道的话,当唤醒的时候,就会把不论是读还是写的全部进程都唤醒过来,这对性能显然损失较大。

              -

              The pipe code uses separate sleep channels for reader and writer (pi->nread and pi->nwrite); this might make the system more effificient in the unlikely event that there are lots of readers and writers waiting for the same pipe.

              +

              namex过程可能需要很长时间才能完成:它可能涉及多个磁盘操作来读取路径名中所遍历目录的索引节点和目录块(如果它们不在buffer cache中)。

              +

              Xv6 is carefully designed,如果一个内核线程对namex的调用在磁盘I/O上阻塞,另一个查找不同路径名的内核线程可以同时进行。Namex locks each directory in the path separately so that lookups in different directories can proceed in parallel.锁细粒度化

              +

              This concurrency introduces some challenges. For example, while one kernel thread is looking up a pathname another kernel thread may be changing the directory tree by unlinking a directory. A potential risk is that a lookup may be searching a directory that has been deleted by another kernel thread and its blocks have been re-used for another directory or file.一个潜在的风险是,查找可能正在搜索已被另一个内核线程删除且其块已被重新用于另一个目录或文件的目录。

              +

              Xv6避免了这种竞争,也就是说,你查到的inode保证暂时不会被释放,里面的内容还是真的,而不会被重新利用从而导致里面的内容变样。

              +

              例如,在namex中执行dirlookup时,lookup线程持有目录上的锁,dirlookup返回使用iget获得的inode。Iget增加索引节点的引用计数。只有在从dirlookup接收inode之后,namex才会释放目录上的锁。现在,另一个线程可以从目录中取消inode的链接,但是xv6还不会删除inode,因为inode的引用计数仍然大于零

              +

              另一个风险是死锁。例如,查找“.”时,next指向与ip相同的inode【确实】。在释放ip上的锁之前锁定next将导致死锁【为什么???难道不是会由于在acquire时已经持有锁,从而爆panic("acquire")吗?】。为了避免这种死锁,namex在获得下一个目录的锁之前解锁该目录。这里我们再次看到为什么igetilock之间的分离很重要。

              -

              Code: Wait, exit, and kill

              exit和wait

              -

              Sleepwakeup可用于多种等待。第一章介绍的一个有趣的例子是子进程exit和父进程wait之间的交互。

              -

              xv6记录子进程终止直到wait观察到它的方式是让exit将调用方置于ZOMBIE状态,在那里它一直保持到父进程的wait注意到它,将子进程的状态更改为UNUSED,复制子进程的exit状态码,释放子进程,并将子进程ID返回给父进程。

              -

              如果父进程在子进程之前退出,则父进程将子进程交给init进程,init进程将永久调用wait;因此,每个子进程退出后都有一个父进程进行清理。

              +

              File descriptor layer

              +

              Unix的一个很酷的方面是,Unix中的大多数资源都表示为文件,包括控制台、管道等设备,当然还有真实文件。文件描述符层是实现这种一致性的层。

              -

              又是一个生产者消费者模式。只不过此时的chan是父进程,资源是僵尸子进程【草】。由于涉及到进程间的调度切换,因而实现稍微复杂了点。

              -

              为什么需要涉及到进程间的调度呢?子进程设置完僵尸状态后,直接通过函数ret不行吗?答案是不行,因为ret的话就会去到不知道哪的地方【大概率会变成scause=2的情况】,所以这里子进程想要退出,就得做几件事,一是依靠父进程,让父进程杀死子进程,二是把自己设置为一个特殊的状态,使得自己不会被调度从而执行ret指令出错,三是尽快让父进程杀死自己越快越好。综合上述三个原因,exit最终在调度方面的实现方式,就变成了,子进程设置自己为ZOMBIE态->启用调度->父进程杀死ZOMBIE态的子进程。这期间不变性条件的防护,就得依赖于锁,以及sleep和wakeup了。

              -
              void
              exit(int status)
              {
              struct proc *p = myproc();

              // ...

              // we need the parent's lock in order to wake it up from wait().
              // the parent-then-child rule says we have to lock it first.
              // 整个xv6都必须遵守相同的顺序(父级,然后是子级)不论是锁定还是释放,都是先父再子
              acquire(&original_parent->lock);
              acquire(&p->lock);

              // Give any children to init.
              // 把自己的所有孩子都托付给init进程
              // init进程就是在操作系统启动时
              reparent(p);

              // Parent might be sleeping in wait().
              // 唤醒wait中的父进程
              // 这里看上去很诡异,明明子进程状态还未完全,怎么就唤醒父亲了呢?但其实很安全。
              // 此时子进程仍持有父进程的锁,如果有别的CPU中断进入scheduler线程,到父进程那时会卡在aquire
              // 直到子进程完成后续工作后父进程才能被真正唤醒执行
              wakeup1(original_parent);

              p->xstate = status;
              // 设为ZOMBIE态
              p->state = ZOMBIE;

              // 完成后续工作,解除父进程的锁
              release(&original_parent->lock);

              // Jump into the scheduler, never to return.
              // 子进程会在父进程中被释放,所以永远不会回来
              sched();
              panic("zombie exit");
              }
              +

              数据结构

              +

              Xv6为每个进程提供了自己的打开文件表或文件描述符。每个打开的文件都由一个struct file表示,它是inode或管道的封装,加上一个I/O偏移量。

              +

              每次调用open都会创建一个新的打开文件(一个新的struct file):如果多个进程独立地打开同一个文件,那么不同的实例将具有不同的I/O偏移量。

              +

              另一方面,单个打开的文件(同一个struct file)可以多次出现在一个进程的文件表中,也可以出现在多个进程的文件表中。如果一个进程使用open打开文件,然后使用dup创建别名,或使用fork与子进程共享,就会发生这种情况。

              +
              +
              struct file {
              enum { FD_NONE, FD_PIPE, FD_INODE, FD_DEVICE } type;
              int ref; // reference count
              char readable;
              char writable;
              struct pipe *pipe; // FD_PIPE
              struct inode *ip; // FD_INODE and FD_DEVICE
              uint off; // FD_INODE
              short major; // FD_DEVICE
              };
              -
              int
              wait(uint64 addr)
              {
              struct proc *np;
              int havekids, pid;
              struct proc *p = myproc();

              // hold p->lock for the whole time to avoid lost
              // wakeups from a child's exit().
              acquire(&p->lock);

              for(;;){
              // Scan through table looking for exited children.
              havekids = 0;
              for(np = proc; np < &proc[NPROC]; np++){
              // this code uses np->parent without holding np->lock.
              // acquiring the lock first would cause a deadlock,
              // since np might be an ancestor, and we already hold p->lock.
              // 下面的第一点其实一句话就可以搞定:
              // 【它违反了先获取父亲锁,再获取子锁的xv6代码规定】
              // 1.要是在这句话之前acquire的话,acquire到你爸,你爸这时候也刚好执行到这句话
              // 那么就会造成你在自旋【此时你爸在wait一开始就得到了锁】,
              // 你爸也在自旋【你也在wait一开始得到了锁】,这样就造成了死锁
              // 2.并且由于np->parent只有parent才能改,所以数据是否过时是没关系的
              // 因为如果不是你儿子,数据过时与否都知道不是你儿子
              // 如果是你儿子,那数据压根就不会过时
              if(np->parent == p){
              // np->parent can't change between the check and the acquire()
              // because only the parent changes it, and we're the parent.
              acquire(&np->lock);
              havekids = 1;
              if(np->state == ZOMBIE){
              // Found one.
              pid = np->pid;
              // 传递返回参数
              if(addr != 0 && copyout(p->pagetable, addr, (char *)&np->xstate,
              sizeof(np->xstate)) < 0) {
              release(&np->lock);
              release(&p->lock);
              return -1;
              }
              freeproc(np);
              release(&np->lock);
              release(&p->lock);
              return pid;
              }
              release(&np->lock);
              }
              }

              // No point waiting if we don't have any children.
              if(!havekids || p->killed){
              release(&p->lock);
              return -1;
              }

              // Wait for a child to exit.
              // 暂时释放p锁,等待子进程获取退出
              sleep(p, &p->lock); //DOC: wait-sleep
              }
              }
              +

              ftable

              +

              所有在系统中打开的文件都会被放入global file tableftable中。

              +

              ftable具有分配文件(filealloc)、创建重复引用(filedup)、释放引用(fileclose)以及读取和写入数据(filereadfilewrite)的函数。

              +

              前三个都很常规,跟之前的xxalloc、xxfree的思路是一样的。

              +

              函数filestatfilereadfilewrite实现对文件的statreadwrite操作。

              +
              +

              filealloc

              // Allocate a file structure.
              struct file*
              filealloc(void)
              {
              struct file *f;

              acquire(&ftable.lock);
              for(f = ftable.file; f < ftable.file + NFILE; f++){
              if(f->ref == 0){
              f->ref = 1;
              release(&ftable.lock);
              return f;
              }
              }
              release(&ftable.lock);
              return 0;
              }
              -

              其中值得注意的几个点:

              -
                -
              1. wait中的sleep中释放的条件锁是等待进程的p->lock,这是上面提到的特例。

                -
              2. -
              3. exit会将自己的所有子进程交付给一直在等待着的init进程:

                -
                for(;;){
                printf("init: starting sh\n");
                pid = fork();
                // ...
                for(;;){
                // this call to wait() returns if the shell exits,
                // or if a parentless process exits.
                wpid = wait((int *) 0);
                if(wpid == pid){
                // the shell exited; restart it.
                break;
                } else if(wpid < 0){
                printf("init: wait returned an error\n");
                exit(1);
                } else {
                // 这里!!
                // it was a parentless process; do nothing.
                }
                }
                }
                +

                filedup

                // Increment ref count for file f.
                struct file*
                filedup(struct file *f)
                {
                acquire(&ftable.lock);
                if(f->ref < 1)
                panic("filedup");
                f->ref++;
                release(&ftable.lock);
                return f;
                }
                -

                如果子进程退出,就会通过init的wait释放它们。然后init释放完它们后进入第三个if分支,继续进行循环。

                -
              4. -
              5. wakeup1

                -
                -

                Exit calls a specialized wakeup function, wakeup1, that wakes up only the parent, and only if it is sleeping in wait.

                -
                -
                // Wake up p if it is sleeping in wait(); used by exit().
                // Caller must hold p->lock.
                static void
                wakeup1(struct proc *p)
                {
                if(!holding(&p->lock))
                panic("wakeup1");
                if(p->chan == p && p->state == SLEEPING) {
                p->state = RUNNABLE;
                }
                }
              6. -
              -

              kill

              kill其实做得很温和。它只是会把想鲨的进程的p->killed设置为1,然后如果该进程sleeping,则唤醒它。最后的死亡以及销毁由进程自己来做。

              -
              // Kill the process with the given pid.
              // The victim won't exit until it tries to go
              // to kernel space (see usertrap() in trap.c).
              int
              kill(int pid)
              {
              struct proc *p;

              for(p = proc; p < &proc[NPROC]; p++){
              acquire(&p->lock);
              if(p->pid == pid){
              p->killed = 1;
              if(p->state == SLEEPING){
              // Wake process from sleep().
              p->state = RUNNABLE;
              }
              release(&p->lock);
              return 0;
              }
              release(&p->lock);
              }
              return -1;
              }
              // in trap.c usertrap()
              if(p->killed)
              exit(-1);
              +

              fileclose

              // Close file f.  (Decrement ref count, close when reaches 0.)
              void
              fileclose(struct file *f)
              {
              struct file ff;

              acquire(&ftable.lock);
              if(f->ref < 1)
              panic("fileclose");
              if(--f->ref > 0){
              release(&ftable.lock);
              return;
              }
              ff = *f;
              f->ref = 0;
              f->type = FD_NONE;
              release(&ftable.lock);

              if(ff.type == FD_PIPE){
              pipeclose(ff.pipe, ff.writable);
              } else if(ff.type == FD_INODE || ff.type == FD_DEVICE){
              begin_op();
              iput(ff.ip);
              end_op();
              }
              }
              -

              可能这里有一个疑问:调用完exit后,进程会变成ZOMBIE态。谁最终把它释放了呢?其实答案很简单,只有两种:init进程或者是创建它的父进程。

              -

              如果创建它的父进程处于wait中,那么是由父进程把它销毁的,这没什么好说的。但如果创建它的父进程不在wait呢?那么父进程最后也是会调用exit的。父进程调用完exit后,会将其所有子进程过继给init进程。所以,ZOMBIE进程最终还是会迟早被init进程杀死的。

              -

              由这里,可以窥见xv6进程管理的进一步的冰山一角:

              -

              init进程是所有进程的根系进程。它一直处于wait的死循环中,因而可以将需要被杀死的进程杀死。

              -

              可见,wait和exit,实际上就构筑了进程的生命周期的最后一环。

              -

              这种巧妙地将进程生命周期这个大事完全托付给了wait和exit这两个函数的这种结构,实在是非常精妙,太牛了吧。

              -
              -

              一些XV6的sleep循环不检查p->killed,因为代码在应该是原子操作的多步系统调用的中间。virtio驱动程序(*kernel/virtio_disk.c*:242)就是一个例子:它不检查p->killed,因为一个磁盘操作可能是文件系统保持正确状态所需的一组写入操作之一。等待磁盘I/O时被杀死的进程将不会退出,直到它完成当前系统调用并且usertrap看到killed标志

              +

              filestat

              +

              Filestat只允许在inode上操作并且调用了stati

              -
              -

              Xv6对kill的支持并不完全令人满意:有一些sleep循环可能应该检查p->killed。一个相关的问题是,即使对于检查p->killedsleep循环,sleepkill之间也存在竞争;后者可能会设置p->killed,并试图在受害者的循环检查p->killed之后但在调用sleep之前尝试唤醒受害者。如果出现此问题,受害者将不会注意到p->killed,直到其等待的条件发生。这可能比正常情况要晚一点(例如,当virtio驱动程序返回受害者正在等待的磁盘块时)或永远不会发生(例如,如果受害者正在等待来自控制台的输入,但用户没有键入任何输入)。

              +
              // Get metadata about file f.
              // addr is a user virtual address, pointing to a struct stat.
              int
              filestat(struct file *f, uint64 addr)
              {
              struct proc *p = myproc();
              struct stat st;

              // 仅允许文件/设备执行
              if(f->type == FD_INODE || f->type == FD_DEVICE){
              ilock(f->ip);
              stati(f->ip, &st);
              iunlock(f->ip);
              if(copyout(p->pagetable, addr, (char *)&st, sizeof(st)) < 0)
              return -1;
              return 0;
              }
              return -1;
              }
              + +

              fileread

              // Read from file f.
              // addr is a user virtual address.
              int
              fileread(struct file *f, uint64 addr, int n)
              {
              int r = 0;

              // 首先检查是否可读
              if(f->readable == 0)
              return -1;

              if(f->type == FD_PIPE){
              r = piperead(f->pipe, addr, n);
              } else if(f->type == FD_DEVICE){
              if(f->major < 0 || f->major >= NDEV || !devsw[f->major].read)
              return -1;
              r = devsw[f->major].read(1, addr, n);
              } else if(f->type == FD_INODE){
              ilock(f->ip);
              if((r = readi(f->ip, 1, addr, f->off, n)) > 0)
              // 移动文件指针偏移量
              f->off += r;
              iunlock(f->ip);
              } else {
              panic("fileread");
              }

              return r;
              }
              + +

              Code: System calls

              +

              通过使用底层提供的函数,大多数系统调用的实现都很简单(请参阅***kernel/sysfile.c***)。有几个调用值得仔细看看。

              +

              以下介绍的函数都在kernel/sysfile.c中。

              -

              是的,所以这个kill的实现其实是相当玄学的。

              -

              Real world

              -

              xv6调度器实现了一个简单的调度策略:它依次运行每个进程。这一策略被称为轮询调度(round robin)。真实的操作系统实施更复杂的策略,例如,允许进程具有优先级。

              +

              这个函数的功能是给文件old加上一个链接,这个链接存在于文件new的父目录。感觉也就相当于把文件从old复制到new处了。具体实现逻辑就是要给该文件所在目录添加一个entry,name=新名字,inode=该文件的inode。

              +
              // Create the path new as a link to the same inode as old.
              uint64
              sys_link(void)
              {
              char name[DIRSIZ], new[MAXPATH], old[MAXPATH];
              struct inode *dp, *ip;

              if(argstr(0, old, MAXPATH) < 0 || argstr(1, new, MAXPATH) < 0)
              return -1;

              // 首先先增加nlink
              begin_op();
              // 通过path找到ip结点
              if((ip = namei(old)) == 0){
              end_op();
              return -1;
              }

              ilock(ip);
              // directory不能被link
              if(ip->type == T_DIR){
              iunlockput(ip);
              end_op();
              return -1;
              }

              ip->nlink++;
              // 修改一次字段就需要update一次
              iupdate(ip);
              iunlock(ip);

              // 然后再在目录中登记新的entry
              // 找到new的parent,也即new所在目录
              if((dp = nameiparent(new, name)) == 0)
              goto bad;
              ilock(dp);
              // 在目录中添加一个entry,名字为给定的新名字,inode依旧为原来的inode
              // new的父目录必须存在并且与现有inode位于同一设备上
              if(dp->dev != ip->dev || dirlink(dp, name, ip->inum) < 0){
              iunlockput(dp);
              goto bad;
              }
              iunlockput(dp);
              iput(ip);

              end_op();

              return 0;

              bad:
              ilock(ip);
              ip->nlink--;
              iupdate(ip);
              iunlockput(ip);
              end_op();
              return -1;
              }
              + +

              create

              +

              它是三个文件创建系统调用的泛化:带有O_CREATE标志的open生成一个新的普通文件,mkdir生成一个新目录,mkdev生成一个新的设备文件。

              -

              我记得linux0.11用的是时间片轮转+优先级队列完美融合的方法,是真的很牛逼

              -
              -

              复杂的策略可能会导致意外的交互,例如优先级反转(priority inversion)和航队(convoys)。当低优先级进程和高优先级进程共享一个锁时,可能会发生优先级反转,当低优先级进程持有该锁时,可能会阻止高优先级进程前进。当许多高优先级进程正在等待一个获得共享锁的低优先级进程时,可能会形成一个长的等待进程航队;一旦航队形成,它可以持续很长时间。为了避免此类问题,在复杂的调度器中需要额外的机制。

              +

              创建一个新的inode结点,结点名包含在path内。返回一个锁定的inode。

              +

              由于使用了iupdate等,所以该函数只能在事务中被调用。

              +
              static struct inode*
              create(char *path, short type, short major, short minor)
              {
              struct inode *ip, *dp;
              char name[DIRSIZ];

              // 获取结点父目录
              if((dp = nameiparent(path, name)) == 0)
              return 0;

              ilock(dp);

              if((ip = dirlookup(dp, name, 0)) != 0){
              // 说明文件已存在
              iunlockput(dp);
              ilock(ip);
              if(type == T_FILE && (ip->type == T_FILE || ip->type == T_DEVICE))
              // 说明此时caller为open(type == T_FILE),open调用create只能是用于创建文件
              return ip;
              iunlockput(ip);
              return 0;
              }

              if((ip = ialloc(dp->dev, type)) == 0)
              panic("create: ialloc");

              ilock(ip);
              ip->major = major;
              ip->minor = minor;
              ip->nlink = 1;
              iupdate(ip);

              if(type == T_DIR){ // Create . and .. entries.
              dp->nlink++; // for ".."
              iupdate(dp);
              // No ip->nlink++ for ".": avoid cyclic ref count.
              // 所以其实.和..本质上是link
              if(dirlink(ip, ".", ip->inum) < 0 || dirlink(ip, "..", dp->inum) < 0)
              panic("create dots");
              }

              if(dirlink(dp, name, ip->inum) < 0)
              panic("create: dirlink");

              iunlockput(dp);

              return ip;
              }
              + +

              sys_mkdir

              uint64
              sys_mkdir(void)
              {
              char path[MAXPATH];
              struct inode *ip;

              begin_op();
              if(argstr(0, path, MAXPATH) < 0 || (ip = create(path, T_DIR, 0, 0)) == 0){
              end_op();
              return -1;
              }
              iunlockput(ip);
              end_op();
              return 0;
              }
              + +

              sys_open

              +

              Sys_open是最复杂的,因为创建一个新文件只是它能做的一小部分。

              -
              -

              wakeup中扫描整个进程列表以查找具有匹配chan的进程效率低下。一个更好的解决方案是用一个数据结构替换sleepwakeup中的chan,该数据结构包含在该结构上休眠的进程列表,例如Linux的等待队列。

              +
              uint64
              sys_open(void)
              {
              char path[MAXPATH];
              int fd, omode;
              struct file *f;
              struct inode *ip;
              int n;

              if((n = argstr(0, path, MAXPATH)) < 0 || argint(1, &omode) < 0)
              return -1;

              begin_op();

              if(omode & O_CREATE){
              ip = create(path, T_FILE, 0, 0);
              // 创建失败
              if(ip == 0){
              end_op();
              return -1;
              }
              } else {
              // 文件不存在
              if((ip = namei(path)) == 0){
              end_op();
              return -1;
              }
              // Create返回一个锁定的inode,但namei不锁定,因此sys_open必须锁定inode本身。
              ilock(ip);
              // 非文件,为目录并且非只读
              // 所以说想要open一个目录的话只能以只读模式打开
              if(ip->type == T_DIR && omode != O_RDONLY){
              iunlockput(ip);
              end_op();
              return -1;
              }
              }

              if(ip->type == T_DEVICE && (ip->major < 0 || ip->major >= NDEV)){
              iunlockput(ip);
              end_op();
              return -1;
              }

              // 获取file结构体和文件描述符。
              if((f = filealloc()) == 0 || (fd = fdalloc(f)) < 0){
              if(f)
              fileclose(f);
              iunlockput(ip);
              end_op();
              return -1;
              }

              // 没有其他进程可以访问部分初始化的文件,因为它仅位于当前进程的表中,因而这里可以不用上锁
              if(ip->type == T_DEVICE){
              f->type = FD_DEVICE;
              f->major = ip->major;
              } else {
              f->type = FD_INODE;
              f->off = 0;
              }
              f->ip = ip;
              f->readable = !(omode & O_WRONLY);
              f->writable = (omode & O_WRONLY) || (omode & O_RDWR);

              // 如果使用了这个标志,调用 open 函数打开文件的时候会将文件原本的内容全部丢弃,文件大小变为 0。
              if((omode & O_TRUNC) && ip->type == T_FILE){
              itrunc(ip);
              }

              iunlock(ip);
              end_op();

              return fd;
              }
              + +

              sys_pipe

              uint64
              sys_pipe(void)
              {
              uint64 fdarray; // user pointer to array of two integers用来接收pipe两端的文件描述符
              struct file *rf, *wf;
              int fd0, fd1;
              struct proc *p = myproc();

              if(argaddr(0, &fdarray) < 0)
              return -1;
              if(pipealloc(&rf, &wf) < 0)
              return -1;
              fd0 = -1;
              if((fd0 = fdalloc(rf)) < 0 || (fd1 = fdalloc(wf)) < 0){
              if(fd0 >= 0)
              p->ofile[fd0] = 0;
              fileclose(rf);
              fileclose(wf);
              return -1;
              }
              if(copyout(p->pagetable, fdarray, (char*)&fd0, sizeof(fd0)) < 0 ||
              copyout(p->pagetable, fdarray+sizeof(fd0), (char *)&fd1, sizeof(fd1)) < 0){
              p->ofile[fd0] = 0;
              p->ofile[fd1] = 0;
              fileclose(rf);
              fileclose(wf);
              return -1;
              }
              return 0;
              }
              + +

              Real world

              +

              实际操作系统中的buffer cache比xv6复杂得多,但它有两个相同的用途:缓存和同步对磁盘的访问。

              +

              与UNIX V6一样,Xv6的buffer cache使用简单的最近最少使用(LRU)替换策略;有许多更复杂的策略可以实现,每种策略都适用于某些工作场景,而不适用于其他工作场景。更高效的LRU缓存将消除链表,而改为使用哈希表进行查找,并使用堆进行LRU替换【跟我们在lock中实现的一样,再多个堆优化】。现代buffer cache通常与虚拟内存系统集成,以支持内存映射文件。

              +

              Xv6的日志系统效率低下。提交不能与文件系统调用同时发生。系统记录整个块,即使一个块中只有几个字节被更改。它执行同步日志写入,每次写入一个块,每个块可能需要整个磁盘旋转时间。真正的日志系统解决了所有这些问题。

              +

              文件系统布局中最低效的部分是目录,它要求在每次查找期间对所有磁盘块进行线性扫描【确实】。当目录只有几个磁盘块时,这是合理的,但对于包含许多文件的目录来说,开销巨大。Microsoft Windows的NTFS、Mac OS X的HFS和Solaris的ZFS(仅举几例)将目录实现为磁盘上块的平衡树。这很复杂,但可以保证目录查找在对数时间内完成(即时间复杂度为O(logn))。

              +

              Xv6对于磁盘故障的解决很初级:如果磁盘操作失败,Xv6就会调用panic。这是否合理取决于硬件:如果操作系统位于使用冗余屏蔽磁盘故障的特殊硬件之上,那么操作系统可能很少看到故障,因此panic是可以的。另一方面,使用普通磁盘的操作系统应该预料到会出现故障,并能更优雅地处理它们,这样一个文件中的块丢失不会影响文件系统其余部分的使用。

              +

              Xv6要求文件系统安装在单个磁盘设备上,且大小不变。随着大型数据库和多媒体文件对存储的要求越来越高,操作系统正在开发各种方法来消除“每个文件系统一个磁盘”的瓶颈。基本方法是将多个物理磁盘组合成一个逻辑磁盘。RAID等硬件解决方案仍然是最流行的,但当前的趋势是在软件中尽可能多地实现这种逻辑。这些软件实现通常允许通过动态添加或删除磁盘来扩展或缩小逻辑设备等丰富功能。当然,一个能够动态增长或收缩的存储层需要一个能够做到这一点的文件系统:xv6使用的固定大小的inode块阵列在这样的环境中无法正常工作。将磁盘管理与文件系统分离可能是最干净的设计,但两者之间复杂的接口导致了一些系统(如Sun的ZFS)将它们结合起来。

              +

              Xv6的文件系统缺少现代文件系统的许多其他功能;例如,它缺乏对快照和增量备份的支持。

              +

              现代Unix系统允许使用与磁盘存储相同的系统调用访问多种资源:命名管道、网络连接、远程访问的网络文件系统以及监视和控制接口,如/proc。不同于xv6中filereadfilewriteif语句,这些系统通常为每个打开的文件提供一个函数指针表【确实有印象】,每个操作一个,并通过函数指针来援引inode的调用实现。网络文件系统和用户级文件系统提供了将这些调用转换为网络RPC并在返回之前等待响应的函数。

              +

              (注:Linux 内核提供了一种通过/proc文件系统,在运行时访问内核内部数据结构、改变内核设置的机制。proc文件系统是一个伪文件系统,它只存在内存当中,而不占用外存空间。它以文件系统的方式为访问系统内核数据的操作提供接口。)

              -

              是的,linux的那个wakeup真的很牛,我现在都还记得当初学到那的时候的震撼。

              -
              -

              wakeup的实现会唤醒在特定通道上等待的所有进程,可能有许多进程在等待该特定通道。操作系统将安排所有这些进程,它们将竞相检查睡眠条件。进程的这种行为有时被称为惊群效应(thundering herd),最好避免。

              -

              大多数条件变量都有两个用于唤醒的原语:signal用于唤醒一个进程;broadcast用于唤醒所有等待进程。

              +

              Lab: file system

              +

              In this lab you will add large files【大文件支持】 and symbolic links【软链接】 to the xv6 file system.

              +

              不过做完这个实验,给我的一种感觉就是磁盘管理和内存管理真的有很多相似之处,不过也许它们所代表的思想也很普遍。

              -
              -

              一个实际的操作系统将在固定时间内使用空闲列表找到自由的proc结构体,而不是allocproc中的线性时间搜索;xv6使用线性扫描是为了简单起见。

              +

              Large files

              实验内容

              Overview
              +

              In this assignment you’ll increase the maximum size of an xv6 file.

              +

              Currently xv6 files are limited to 268 blocks, or 268*BSIZE bytes (BSIZE is 1024 in xv6). This limit comes from the fact that an xv6 inode contains 12 “direct” block numbers and one “singly-indirect” block number, which refers to a block that holds up to 256 more block numbers, for a total of 12+256=268 blocks.

              +

              You’ll change the xv6 file system code to support a “doubly-indirect” block in each inode, containing 256 addresses of singly-indirect blocks, each of which can contain up to 256 addresses of data blocks. The result will be that a file will be able to consist of up to 65803 blocks, or 256*256+256+11 blocks (11 instead of 12, because we will sacrifice one of the direct block numbers for the double-indirect block).

              -

              Lab: Multithreading

              -

              You will implement switching between threads in a user-level threads package, use multiple threads to speed up a program, and implement a barrier.

              +
              Preliminaries
              +

              If at any point during the lab you find yourself having to rebuild the file system from scratch, you can run make clean which forces make to rebuild fs.img.

              -

              这个introduction看起来还是非常激动人心的,很早就想了解到底线程是怎么实现的了。不过做完发现思想还是很简单的,就是只用切换上下文和栈就行。可以看看提供给的代码。

              -

              Uthread: switching between threads

              -

              In this exercise you will design the context switch mechanism for a user-level threading system, and then implement it.

              -

              To get you started, your xv6 has two files user/uthread.c and user/uthread_switch.S, and a rule in the Makefile to build a uthread program.

              -

              uthread.c contains most of a user-level threading package, and code for three simple test threads. The threading package is missing some of the code to create a thread and to switch between threads.

              -

              You will need to add code to thread_create() and thread_schedule() in user/uthread.c, and thread_switch in user/uthread_switch.S.

              -

              One goal is ensure that when thread_schedule() runs a given thread for the first time, the thread executes the function passed to thread_create(), on its own stack.

              -

              Another goal is to ensure that thread_switch saves the registers of the thread being switched away from, restores the registers of the thread being switched to, and returns to the point in the latter thread’s instructions where it last left off. You will have to decide where to save/restore registers; modifying struct thread to hold registers is a good plan.

              -

              You’ll need to add a call to thread_switch in thread_schedule; you can pass whatever arguments you need to thread_switch, but the intent is to switch from thread t to next_thread.

              +
              What to Look At

              意思就是要我们去看一眼fs.h,bmap,以及了解一下逻辑地址bn如何转化为blockno。这个我是知道的。

              +
              Your Job
              +

              Modify bmap() so that it implements a doubly-indirect block, in addition to direct blocks and a singly-indirect block.

              +

              You’ll have to have only 11 direct blocks, rather than 12, to make room for your new doubly-indirect block; you’re not allowed to change the size of an on-disk inode.

              +

              The first 11 elements of ip->addrs[] should be direct blocks; the 12th should be a singly-indirect block (just like the current one); the 13th should be your new doubly-indirect block. You are done with this exercise when bigfile writes 65803 blocks and usertests runs successfully.

              -

              实现的是用户级线程,其栈保存在对应父进程的地址空间中。

              -

              感想

              思路

              看了一遍它这里面写的题目还是有点抽象的,需要结合着给的代码看,那样就清晰多了。

              -

              首先,要补全的地方有这几个:

              -
              // 1. in thread_schedule()
              if (current_thread != next_thread) { /* switch threads? */
              next_thread->state = RUNNING;
              t = current_thread;
              current_thread = next_thread;
              /* YOUR CODE HERE
              * Invoke thread_switch to switch from t to next_thread:
              * thread_switch(??, ??);
              */
              } else
              next_thread = 0;
              // 2. in thread_create()
              void
              thread_create(void (*func)())
              {
              struct thread *t;

              for (t = all_thread; t < all_thread + MAX_THREAD; t++) {
              if (t->state == FREE) break;
              }
              t->state = RUNNABLE;
              // YOUR CODE HERE
              }
              // 3. in uthread_switch.S
              /*
              * save the old thread's registers,
              * restore the new thread's registers.
              */

              .globl thread_switch
              thread_switch:
              /* YOUR CODE HERE */
              ret /* return to ra */
              +

              感想

              意外地很简单()在此不多做赘述,直接上代码。

              +

              唯一要注意的一点就是记得在itrunc中free掉

              +

              image-20230124232433793

              +

              代码

              修改定义
              // in fs.h
              #define NDIRECT 11
              #define NINDIRECT (BSIZE / sizeof(uint))
              #define NDOUBLEINDIRECT ((BSIZE/sizeof(uint))*(BSIZE/sizeof(uint)))
              #define MAXFILE (NDIRECT + NINDIRECT + NDOUBLEINDIRECT)

              // On-disk inode structure
              struct dinode {
              // ...
              uint addrs[NDIRECT+2]; // Data block addresses
              };
              -

              这几个函数到时候会被如此调用:

              -
              int
              main(int argc, char *argv[])
              {
              a_started = b_started = c_started = 0;
              a_n = b_n = c_n = 0;
              thread_init();
              thread_create(thread_a);
              thread_create(thread_b);
              thread_create(thread_c);
              thread_schedule();
              exit(0);
              }
              +
              // in file.h
              // in-memory copy of an inode
              struct inode {
              // ...
              uint addrs[NDIRECT+2];
              };
              -

              所以,我们在第一个地方要做的,就是要填入swtch的签名。第二个地方要做的,就是要想办法让该线程一被启动就去执行参数的函数指针。第三个地方要做的,就是要完成上下文的切换。

              -

              所以思路其实是很直观的。我们可以模仿进程管理中用来表示上下文的context,在thread_create的时候把里面的ra设置为参数的函数指针入口,sp修改为thread结构体中的栈地址。swtch函数则完全把kernel/swtch.S超过来就行。

              -
              -

              在这个思路中,我们是怎么做到栈的切换的呢?

              -

              每个线程在thread_create的时候,都将自己的context中的sp修改为自己的栈地址。这样一来,在它们被调度的时候,switch会自然而然地从context中读取sp作为之后运行的sp,这样就实现了栈的切换。

              -
              -

              我觉得其他方面都不难,最坑最细节的【也是我完全没有想到的……】就是这里:

              -
              // 修改sp为栈顶
              t->context.sp = (uint64)t->stack + STACK_SIZE;
              +
              修改bmap()
              // in fs.c
              // 调试用
              static int cnt = 0;

              static uint
              bmap(struct inode *ip, uint bn)
              {
              uint addr, *a;
              struct buf *bp;

              if(bn < NDIRECT){
              if((addr = ip->addrs[bn]) == 0)
              ip->addrs[bn] = addr = balloc(ip->dev);
              return addr;
              }
              bn -= NDIRECT;

              if(bn < NINDIRECT){
              // Load indirect block, allocating if necessary.
              if((addr = ip->addrs[NDIRECT]) == 0)
              ip->addrs[NDIRECT] = addr = balloc(ip->dev);
              bp = bread(ip->dev, addr);
              a = (uint*)bp->data;
              if((addr = a[bn]) == 0){
              a[bn] = addr = balloc(ip->dev);
              log_write(bp);
              }
              brelse(bp);
              return addr;
              }

              // CODE HERE
              bn -= NINDIRECT;
              if(bn < NDOUBLEINDIRECT){
              // 调试用
              if(bn/10000 > cnt){
              cnt++;
              printf("double_indirect:%d\n",bn);
              }
              // 第一层
              if((addr = ip->addrs[NDIRECT+1]) == 0)
              ip->addrs[NDIRECT+1] = addr = balloc(ip->dev);
              // 第二层
              bp = bread(ip->dev,addr);
              a = (uint*)bp->data;
              if((addr = a[(bn >> 8)]) == 0){
              a[(bn >> 8)] = addr = balloc(ip->dev);
              log_write(bp);
              }
              brelse(bp);
              // 第三层
              bp = bread(ip->dev,addr);
              a = (uint*)bp->data;
              if((addr = a[(bn & 0x00FF)]) == 0){
              a[(bn & 0x00FF)] = addr = balloc(ip->dev);
              log_write(bp);
              }
              brelse(bp);
              return addr;
              }

              panic("bmap: out of range");
              }
              -

              需要注意,栈顶并不是t->stack

              -

              通过测试程序:

              -
              int main(){
              int a[5]={1,2,3,4,5};
              for(int i=0;i<5;i++){
              printf("%p\n",&a[i]);
              }
              return 0;
              }
              0062feb8
              0062febc
              0062fec0
              0062fec4
              0062fec8
              +
              修改itrunc
              // Truncate inode (discard contents).
              // Caller must hold ip->lock.
              void
              itrunc(struct inode *ip)
              {
              int i, j;
              struct buf *bp;
              uint *a;

              for(i = 0; i < NDIRECT; i++){
              if(ip->addrs[i]){
              bfree(ip->dev, ip->addrs[i]);
              ip->addrs[i] = 0;
              }
              }

              if(ip->addrs[NDIRECT]){
              bp = bread(ip->dev, ip->addrs[NDIRECT]);
              a = (uint*)bp->data;
              for(j = 0; j < NINDIRECT; j++){
              if(a[j])
              bfree(ip->dev, a[j]);
              }
              brelse(bp);
              bfree(ip->dev, ip->addrs[NDIRECT]);
              ip->addrs[NDIRECT] = 0;
              }

              // CODE HERE
              if(ip->addrs[NDIRECT+1]){
              bp = bread(ip->dev, ip->addrs[NDIRECT+1]);
              a = (uint*)bp->data;
              // 双层循环
              for(j = 0; j < NINDIRECT; j++){
              if(a[j]){
              struct buf* tmp_bp = bread(ip->dev,a[j]);
              uint* tmp_a = (uint*)tmp_bp->data;
              for(int k = 0;k < NINDIRECT; k++){
              if(tmp_a[k])
              bfree(ip->dev,tmp_a[k]);
              }
              brelse(tmp_bp);
              bfree(ip->dev,a[j]);
              }
              }
              brelse(bp);
              bfree(ip->dev, ip->addrs[NDIRECT+1]);
              ip->addrs[NDIRECT+1] = 0;
              }

              ip->size = 0;
              iupdate(ip);
              }
              -

              栈是向下增长的,因而,栈顶确实应该是数组的末尾……

              -

              这里完全没有想到,还是吃了基础的亏啊。

              +
              +

              In this exercise you will add symbolic links to xv6.

              +

              Symbolic links (or soft links) refer to a linked file by pathname; when a symbolic link is opened, the kernel follows the link to the referred file.

              +

              Symbolic links resembles hard links, but hard links are restricted to pointing to file on the same disk, while symbolic links can cross disk devices.

              +

              Although xv6 doesn’t support multiple devices, implementing this system call is a good exercise to understand how pathname lookup works.

              +

              You will implement the symlink(char *target, char *path) system call, which creates a new symbolic link at path that refers to file named by target. For further information, see the man page symlink. To test, add symlinktest to the Makefile and run it.

              +
              -

              如果这里将t->stack作为sp,那么运行时会出现非常诡异的现象(打印的是abc三个的thread->state):

              -

              image-20230120232149776

              -

              仅有c【经测试,是仅有最后一个启动的线程】在执行,而ab的state都不是理想中的2,而是很奇怪的值。我确实有想过栈溢出问题,但是马上被我否定了。我完全没有想到是那样错的【悲】

              +

              linux:硬链接和软链接

              +

              硬链接不会创建新的物理文件,但是会使得当前物理文件的引用数加1。当硬链接产生的文件存在时,删除源文件,不会清除实际的物理文件,即对于硬链接“生成的新文件”不会产生任何影响。

              +

              软链接就更像一个指针,只是指向实际物理文件位置,当源文件移动或者删除时,软链接就会失效。

              +

              【所以说,意思就是软链接不会让inode->ulinks++的意思?】

              -
              关于swtch

              Update,验收时学长问为什么这里的uswitch.S为什么无需保存tn这样的寄存器。答案是因为tn是caller-save的,线程这相当于仅仅是执行一个函数,所以只需保存callee-save的寄存器。

              -

              内核的swtch也只保存了这些callee-save的寄存器,也是同一个道理。

              -

              image-20231219000100763

              -

              image-20231219000047041

              -

              tn寄存器被保存在调用者的栈帧中。感觉也能理解为什么那题作业题说上文进程的现场是由栈保存了。

              -

              代码

              增加context结构体定义,修改thread结构体
              struct context {
              uint64 ra;
              uint64 sp;

              // callee-saved
              uint64 s0;
              uint64 s1;
              uint64 s2;
              uint64 s3;
              uint64 s4;
              uint64 s5;
              uint64 s6;
              uint64 s7;
              uint64 s8;
              uint64 s9;
              uint64 s10;
              uint64 s11;
              };


              struct thread {
              char stack[STACK_SIZE]; /* the thread's stack */
              int state; /* FREE, RUNNING, RUNNABLE */
              struct context context;
              };
              +

              感想

              这个实验比上个实验稍难一些,但也确实只是moderate的水平,其复杂程度主要来源于对文件系统的理解,还有如何判断环,以及对锁的获取和释放的应用。我做这个实验居然是没看提示的【非常骄傲<-】,让我有一种自己水平上升了的感觉hhh

              +
              正确思路

              本实验要求实现软链接。首先需要实现创建软链接:写一个系统调用 symlink(char *target, char *path) 用于创建一个指向target的在path的软链接;然后需要实现打开软链接进行自动的跳转:在sys_open中添加对文件类型为软链接的特殊处理。

              +
              初见思路

              我的初见思路是觉得可以完全参照sys_link来写。但其实还是很不一样的。

              +

              sys_link的逻辑:

              +
                +
              1. 获取old的inode
              2. +
              3. 获取new所在目录的inode,称为dp
              4. +
              5. 在dp中添加一项entry指向old
              6. +
              +

              sys_symlink的逻辑:

              +
                +
              1. 通过path创建一个新的inode,作为软链接的文件

                +

                这里选择新建inode,而不是像link那样做,主要还是为了能遵从symlinktest给的接口使用方法(朴实无华的理由)。而且这么做也很方便,符合“一切皆文件”的思想,也能简单化对其在open中的处理。

                +
              2. +
              3. 在inode中填入target的地址

                +

                我们可以把软链接视为文件,文件内容是其target的path。

                +
              4. +
              +

              可以说是毫不相干,所以还是直接自起炉灶比较好。

              +
              一些错误

              其实没什么好说的,虽然debug过程挺久,但是靠常规的printf追踪就都可以看出来是哪里错了。下面我说说一个我印象比较深刻的吧。

              +

              symlinktest中有一个检测点是,软链接不能成环,也即b->a->b是非法的。于是,我就选择了用快慢指针来检测环形链表这个思想,用来看是否出现环。

              +

              symlinktest的另一个检测点中:

              +

              image-20230125173143735

              +

              我出现了如下错误:

              +

              image-20230125162542807

              +

              此时的结构是1[27]->2[28]->3[29]->4,[]内为inode的inum。

              +

              快慢指针的实现方式是当cnt为奇数的时候,慢指针才会移动。而上图中,cnt==0时,两个指针的值都发生了变化,这就非常诡异。

              +

              这其实是因为slow指针所指向的那个inode被释放了,然后又被fast指针的下一个inode捡过来用了,从而导致值覆盖。

              +

              为什么会被释放呢?

              +
                    // 快指针移动
              readi(ip,0,(uint64)path,0,MAXPATH);
              iunlock(ip);
              if((ip = namei(path)) == 0){
              end_op();
              return -1;
              }
              // 在这里!!!
              ilockput(ip);
              -
              修改thread_create
              void
              thread_create(void (*func)())
              {
              struct thread *t;

              for (t = all_thread; t < all_thread + MAX_THREAD; t++) {
              if (t->state == FREE) break;
              }
              t->state = RUNNABLE;
              // YOUR CODE HERE
              // 将当前上下文保存入context
              thread_switch((uint64)(&(t->context)),(uint64)(&(t->context)));
              // 修改sp为栈顶
              t->context.sp = (uint64)t->stack + STACK_SIZE;
              // 修改ra为参数的函数指针入口
              t->context.ra = (uint64)func;
              }
              +

              在这里,我错误地调用了ilockput,从而使inode的ref–,使得它在下一次fast指针调用nameinamei调用iget时,该inode被当做free inode使用,于是就这么寄了。

              +

              所以我们需要把ilockput的调用换成ilock,这样一来就能防止inode被free。至于什么时候再iput?我想还是交给操作系统启动时的清理工作来做吧23333【开摆】

              +

              代码

              image-20230125165612112

              +
              添加定义
              fcntl.c

              open参数

              +
              // 意为只用获取软链接文件本身,而不用顺着软链接去找它的target文件
              #define O_NOFOLLOW 0x100
              -
              修改thread_schedule
              if (current_thread != next_thread) {         /* switch threads?  */
              next_thread->state = RUNNING;
              t = current_thread;
              current_thread = next_thread;
              /* YOUR CODE HERE
              * Invoke thread_switch to switch from t to next_thread:
              * thread_switch(??, ??);
              */
              thread_switch((uint64)(&(t->context)),(uint64)(&(current_thread->context)));
              } else
              next_thread = 0;
              +
              stat.h

              文件类型

              +
              #define T_DIR     1   // Directory
              #define T_FILE 2 // File
              #define T_DEVICE 3 // Device
              #define T_SYMLINK 4 // symbol links
              -
              修改thread_switch

              全部照搬kernel/swtch.S,没什么好说的

              -

              Using threads

              Barrier

              -

              In this assignment you’ll implement a barrier: a point in an application at which all participating threads must wait until all other participating threads reach that point too.

              -
              -

              直接上代码,还是比较简单的

              -
              static void
              barrier()
              {
              // YOUR CODE HERE
              //
              // Block until all threads have called barrier() and
              // then increment bstate.round.
              //
              pthread_mutex_lock(&(bstate.barrier_mutex));
              bstate.nthread++;
              while(bstate.nthread < nthread){
              pthread_cond_wait(&(bstate.barrier_cond), &(bstate.barrier_mutex));
              goto end;
              }
              // 此部分仅一个线程会进入
              pthread_cond_broadcast(&(bstate.barrier_cond));
              bstate.nthread = 0;
              bstate.round++;
              end:
              pthread_mutex_unlock(&(bstate.barrier_mutex));
              }
              +
              添加sys_symlink系统调用
              // in sysfile.c
              uint64
              sys_symlink(void)
              {
              char target[MAXPATH], path[MAXPATH];
              struct inode *ip;

              if(argstr(0, target, MAXPATH) < 0 || argstr(1, path, MAXPATH) < 0)
              return -1;

              begin_op();

              // 创建软链接结点
              ip = create(path,T_SYMLINK,0,0);
              //printf("symlink:before writei,inum = %d\n",ip->inum);
              // 此处可以防止住一些并发错误
              if(ip ==0){
              end_op();
              return 0;
              }
              // 向软链接结点文件内写入其所指向的路径
              writei(ip,0,(uint64)target,0,MAXPATH);
              //printf("symlink:after writei\n");

              // 软链接不需要让nlink++

              // 记得要释放在create()中申请的锁
              iunlockput(ip);

              end_op();

              return 0;
              }
              -
              -

              Update

              -

              关于pthread_cond的实现,也是使用了条件变量的思想,这个值得以后有时间了解一下。

              -
              -]]> - - - xv6 - /2023/01/10/xv6/ - -

              总耗时:120h 约27天

              -

              部分地方的翻译和表格来源参考:xv6指导书翻译

              -

              部分文本来自:操作系统实验指导书 - 2023秋季 | 哈工大(深圳)

              -

              实验官网:6.S081

              -

              代码以github为准,此处记录的有些小瑕疵

              -

              笔记的结构【以第一章Operating system interface为例】:

              -

              image-20230124235649128

              -
              -

              Operating system interface

              Operating system oganization

              Page tables

              Traps and system calls

              Interrupts and device drivers

              Locking

              Scheduling

              File system

              其他的对实验未涉及的思考

              ]]> - - labs - - - - 其他的对实验未涉及的思考 - /2023/01/10/xv6$chap9/ - 其他的对实验未涉及的思考

              由mkfs引发的对虚拟机的学习

              -

              懂了!VMware/KVM/Docker原来是这么回事儿这篇文章对虚拟化、虚拟机技术讲解很到位,写得通俗易懂,非常值得一看

              -

              KVM 的「基于内核的虚拟机」是什么意思?这篇文章对QEMU-KVM架构进行了详细的介绍。还有这篇文章对应的知乎问题下面的高赞回答有机会也可以去看看。

              -

              QEMU/KVM原理概述这篇文章前面的原理和上面那个差不多,后面有使用kvm做一个精简内核的实例,有兴趣/有精力/有需要可以看看。

              -

              MIT6.S081操作系统实验——操作系统是如何在qemu虚拟机中启动的?

              -

              xv6分析–mkfs源代码注释

              -
              -

              以前只是知道,xv6是运行在qemu提供的虚拟环境之上的。qemu是什么,怎么虚拟的,虚拟机和宿主机是怎么交互的,这些一概不通。今天心血来潮想研究下qemu,虚拟机啥的到底是什么玩意,虽然看得有些猪脑过载,但还是写下一些个人的整理。

              -

              qemu

              是什么

              在了解qemu之前,可以先了解一下虚拟化的思想。

              -
              虚拟化
              -

              虚拟化的主要思想是,通过分层将底层的复杂、难用的资源虚拟抽象成简单、易用的资源,提供给上层使用。

              -

              本质上,计算机的发展过程也是虚拟化不断发展的过程,底层的资源或者通过空间的分割,或者通过时间的分割,将下层的资源通过一种简单易用的方式转换成另一种资源,提供给上层使用。

              -

              虚拟化可分为以下几方面:

              +
              修改open
              uint64
              sys_open(void)
              {
              // ...

              begin_op();

              if(omode & O_CREATE){
              ip = create(path, T_FILE, 0, 0);
              if(ip == 0){
              end_op();
              return -1;
              }
              } else {
              // 软链接不可能是以O_CREATE的形式创建的
              if((ip = namei(path)) == 0){
              end_op();
              return -1;
              }
              ilock(ip);
              if(ip->type == T_DIR && omode != O_RDONLY){
              iunlockput(ip);
              end_op();
              return -1;
              }

              // 修改从这里开始
              // 快慢指针
              // ip为快指针,slow为慢指针
              uint cnt = 0;
              struct inode* slow = ip;
              // 可能有多重链接,因而需要持续跳转
              while(ip->type == T_SYMLINK){
              //printf("slow = %d,fast = %d,cnt = %d\n",slow->inum,ip->inum,cnt);
              // 其实这个只需要检测一次就够了。但为了编码方便,仍然把它保留在while循环中
              if(omode & O_NOFOLLOW){
              break;
              }else{
              // 检测到cycle
              if(slow == ip && cnt!=0){
              iunlockput(ip);
              end_op();
              return -1;
              }
              // 快指针移动
              readi(ip,0,(uint64)path,0,MAXPATH);
              // 此处不能用iunlockput(),具体原因见 感想-一些错误
              iunlock(ip);
              if((ip = namei(path)) == 0){
              end_op();
              return -1;
              }
              ilock(ip);
              // 慢指针移动
              // 注意,我慢指针移动的时候没有锁保护,因为用锁太麻烦了()其实还是用锁比较合适
              if(cnt & 1){
              //printf("%d\n",cnt);
              readi(slow,0,(uint64)path,0,MAXPATH);
              if((slow = namei(path) )== 0){
              end_op();
              return -1;
              }
              }
              cnt++;
              }
              }
              // 当跳出循环时,此时的ip必定是锁住的
              }

              if(ip->type == T_DEVICE && (ip->major < 0 || ip->major >= NDEV)){
              iunlockput(ip);
              end_op();
              return -1;
              }
              // ...
              }
              + +

              Lab mmap

              +

              The mmap and munmap system calls allow UNIX programs to exert detailed control over their address spaces.

              +

              They can be used to:

                -
              1. CPU抽象:机器码、汇编语言到C语言、再到高级语言的不断虚拟的过程
              2. -
              3. 存储抽象:操作系统通过文件和目录抽象
              4. -
              5. 网络抽象:TCP/IP协议栈模型将网卡设备中传递的二进制数据,经过网络层、传输层的抽象后,为应用程序提供了便捷的网络包处理接口,而无需关心底层的IP路由、分片等细节
              6. -
              7. 进程抽象:操作系统通过进程抽象为不同的应用程序提供了安全隔离的执行环境,并且有着独立的CPU和内存等资源
              8. +
              9. share memory among processes
              10. +
              11. map files into process address spaces
              12. +
              13. as part of user-level page fault schemes such as the garbage-collection algorithms discussed in lecture.
              +

              In this lab you’ll add mmap and munmap to xv6, focusing on memory-mapped files.

              +

              mmap是系统调用,在用户态被使用。我们这次实验仅实现mmap功能的子集,也即memory-mapped files。

              -

              虚拟化的思想实际上就是我以前一直称为“抽象”的思想,以接口的形式逐层向上服务。

              -
              虚拟机
              -

              虚拟机的核心能力在于提供一个执行环境(隐藏底层细节),并在其中完成用户的指定任务。

              -
              -
              -

              虚拟机有多种不同的形式,包括提供指令执行环境的进程、模拟器和高级语言虚拟机,或者是提供一个完整的系统环境的系统虚拟机。

              -
              -
              进程

              进程实际上就是一种虚拟机。

              -

              进程可以看作是一组资源的集合,有自己独立的进程地址空间以及独立的CPU和寄存器,执行程序员编写的指令,完成一定的任务。

              -

              操作系统可以创建多个进程,每一个进程都可以看成一个独立的虚拟机,它们在执行指令、访问内存的时候并不会相互影响影响。

              -
              -
              模拟器
              高级语言虚拟机
              系统虚拟机
              -

              通过系统虚拟化技术,能够在单个的宿主机硬件平台上运行多个虚拟机,每个虚拟机都有着完整的虚拟机硬件,如虚拟的CPU、内存、虚拟的外设等,并且虚拟机之间能够实现完整的隔离。

              -

              在系统虚拟化中,管理全局物理资源的软件叫作虚拟机监控器(Virtual Machine Monitor,VMM),VMM之于虚拟机就如同操作系统之于进程,VMM利用时分复用或者空分复用的办法将硬件资源在各个虚拟机之间进行分配。

              -
              -
              qemu

              可以看到,qemu就是一种虚拟机。它可以模拟虚拟机硬件,为操作系统提供虚拟硬件环境,从而能够让不同的操作系统能够在不同主机硬件上执行。

              -

              qemu-kvm架构

              诞生的原因
              -

              其对于虚拟化技术的优化,以及发展的前因后果,具体可以看懂了!VMware/KVM/Docker原来是这么回事儿这篇文章。

              -

              概括来讲,大致有以下几个要点:

              -
              -
              两种虚拟化方案

              640

              -

              640-1676793944101-7

              -
              实现上述的虚拟化方案

              一个典型的做法是——陷阱 & 模拟技术

              -

              什么意思?简单来说就是正常情况下直接把虚拟机中的代码指令放到物理的CPU上去执行,一旦执行到一些敏感指令,就触发异常,控制流程交给VMM,由VMM来进行对应的处理,以此来营造出一个虚拟的计算机环境。

              -
              x86架构的问题

              x86架构使得上述做法用不了了。因为它引入了四种权限

              -

              image-20230219160725978

              -
              解决方法
                -
              1. 全虚拟化

                -

                VMware的二进制翻译技术、QEMU的模拟指令集

                +

                declaration for mmap:

                +
                void *mmap(void *addr, size_t length, int prot, int flags,
                int fd, off_t offset);
                + +
                  +
                1. 参数

                  +
                    +
                  1. addr is always zero.

                    +

                    You can assume that addr will always be zero, meaning that the kernel should decide the virtual address at which to map the file.【addr由kernel决定,因而用户态只需传入0即可】

                  2. -
                  3. 半虚拟化

                    +
                  4. length is the number of bytes to map

                    +

                    Might not be the same as the file’s length.

                  5. -
                  6. 硬件辅助虚拟化

                    -

                    硬件辅助虚拟化细节较为复杂,简单来说,新一代CPU在原先的Ring0-Ring3四种工作状态之下,再引入了一个叫工作模式的概念,有VMX root operationVMX non-root operation两种模式,每种模式都具有完整的Ring0-Ring3四种工作状态,前者是VMM运行的模式,后者是虚拟机中的OS运行的模式。

                    -

                    qemu-kvm架构正是借助于此实现的。

                    +
                  7. prot indicates whether the memory should be mapped readable, writeable, and/or executable.

                    +

                    you can assume that prot is PROT_READ or PROT_WRITE or both.

                  8. -
                  -
                  kvm

                  kvm就是借助硬件辅助虚拟化诞生的。可以把kvm看作是一堆系统调用。

                  -
                  -

                  什么是 KVM?

                  -

                  KVM本身是一个内核模块,它导出了一系列的接口到用户空间,用户态程序可以使用这些接口创建虚拟机。

                  -

                  具体而言,KVM 可帮助您将 Linux 转变为虚拟机监控程序,使主机计算机能够运行多个隔离的虚拟环境,即虚拟客户机或虚拟机(VM)。【也即,虚拟机—进程,KVM—操作系统】

                  -
                  -
                  -

                  在虚拟化底层技术上,KVM和VMware后续版本一样,都是基于硬件辅助虚拟化实现。不同的是VMware作为独立的第三方软件可以安装在Linux、Windows、MacOS等多种不同的操作系统之上,而KVM作为一项虚拟化技术已经集成到Linux内核之中,可以认为Linux内核本身就是一个HyperVisor,这也是KVM名字的含义,因此该技术只能在Linux服务器上使用。

                  -
                  -
                  qemu-kvm

                  KVM本身基于硬件辅助虚拟化,仅仅实现CPU和内存的虚拟化,但一台计算机不仅仅有CPU和内存,还需要各种各样的I/O设备,不过KVM不负责这些。这个时候,QEMU就和KVM搭上了线,经过改造后的QEMU,负责外部设备的虚拟,KVM负责底层执行引擎和内存的虚拟,两者彼此互补,成为新一代云计算虚拟化方案的宠儿。

                  -
                  qemu-kvm总体架构

                  KVM只负责最核心的CPU虚拟化和内存虚拟化部分;QEMU作为其用户态组件,负责完成大量外设的模拟

                  -

                  v2-249a3f162de88198bbe415110fc71c7f_1440w

                  -
                  VMX root和VMX non root
                  -

                  VMX root是宿主机模式,此时CPU在运行包括QEMU在内的普通进程和宿主机的操作系统内核;

                  -

                  VMX non-root是虚拟机模式,此时CPU在运行虚拟机中的用户程序和操作系统代码。

                  -
                  -

                  也就是说,虚拟机的程序,包括用户程序和内核程序,都运行在non-root模式。宿主机的所有程序,包括用户程序【包括qemu】和内核程序【包括kvm】,都运行在root模式。

                  -
                  qemu层(左上)

                  上面说到,qemu负责的是大量外设的模拟。它具体要做以下几件事:

                  -
                  -

                  初始化虚拟机:

                  +
                2. flags has two values.

                    -
                  1. 创建模拟的芯片组

                    +
                  2. MAP_SHARED

                    +

                    meaning that modifications to the mapped memory should be written back to the file,

                    +

                    如果标记为此,则当且仅当file本身权限为RW或者WRITABLE的时候,prot才可以标记为PROT_WRITE

                  3. -
                  4. 创建CPU线程来表示虚拟机的CPU

                    -

                    QEMU在初始化虚拟机的CPU线程时,首先设置好相应的虚拟CPU寄存器的值,然后调用KVM的接口将虚拟机运行起来,这样CPU线程就会被调度在物理CPU上执行虚拟机的代码。

                    +
                  5. MAP_PRIVATE

                    +

                    meaning that they should not.

                    +

                    如果标记为此,则无论file本身权限如何,prot都可以标记为PROT_WRITE

                  6. -
                  7. 在QEMU的虚拟地址空间中分配空间作为虚拟机的物理地址

                    +
                3. -
                4. 根据用户在命令行指定的设备为虚拟机创建对应的虚拟设备【如各种IO设备】

                  +
                5. You can assume offset is zero (it’s the starting point in the file at which to map)

                -

                虚拟机运行时:

                -
                  -
                1. 监听多种事件

                  -

                  包括虚拟机对设备的I/O访问、用户对虚拟机管理界面、虚拟设备对应的宿主机上的一些I/O事件(比如虚拟机网络数据的接收)等

                2. -
                3. 调用函数处理

                  +
                4. return

                  +

                  mmap returns that kernel-decided address, or 0xffffffffffffffff if it fails.

                +

                如果两个进程同时对某个文件进行memory map,那么这两个进程可以不共享物理页面。

              -

              可以看到,qemu确实利用了宿主机的各种资源,提供了一个很完美的硬件环境。其资源对应关系为:

              -

              虚拟机的CPU——宿主机的一个线程

              -

              虚拟机的物理地址——qemu在宿主机的虚拟地址

              -

              虚拟机对硬件设备的访问 —→ 对qemu的访问

              -
              kvm层(下方)

              它大概做了两件事:

              +
              +

              munmap(addr, length) should remove mmap mappings in the indicated address range.

              +

              If the process has modified the memory and has it mapped MAP_SHARED, the modifications should first be written to the file. 【如果两个进程的修改发生冲突了怎么办?】

              +

              An munmap call might cover only a portion of an mmap-ed region, but you can assume that it will either unmap at the start, or at the end, or the whole region (but not punch a hole in the middle of a region).

              +
              +

              感想

              这个实验做得我……怎么说,感觉非常地难受吧。虽然我认为我这次做得挺不错的,因为我没有怎么看hints,我的代码差不多全都是我自己想出来的,没有依赖保姆级教学,我认为是一个很好的进步。不过,正因为我没有看hints,导致我的想法比起答案来思路非常地奇诡,导致我第一次错误想法写了一天,看了hints后决心痛改前非,结果第二次错误想法又写了一天emmm

              +

              下面的第一个代码版本虽然可以过掉mmaptest,但确实还是有一个很致命的bug,并且lazy也没有lazy到位,最后的版本离正确思路还有偏差,也就是下面放的第一个代码版本是错误的,但我认为它也不是完全没有亮点。第二个版本才是经过改正的正确版本,但写得着实有点潦草。

              +

              笔记整理得也有点匆忙,毕竟我真的话比较多而且心里很烦。总之,先记录我的全部思路过程,至于价值如何,先不管了2333

              +

              初见思路

              所以说,我们要做的,就是实现一个系统调用mmap,在mmap中,应该首先申请几页用来放file的内容,并且在页表中填入该项,然后再返回该项的虚拟地址。然后在munmap中,再将该file页内容写入file。

              +

              也就是说,直接在mmap把文件的全部内容写入内存,然后各进程读写自己的那块内容块,最后在munmap的时候把修改内容写入文件然后释放该内存块就行了

              +
              问题:在哪里放置file的内容

              题目要求the kernel should decide the **virtual address** at which to map the file.也就是说,在我们的mmap中,需要决定我们要讲文件内容放在哪里。那要放在哪呢……

              +

              我第一反应很奇葩:扫描页表,找到空闲页。但我自己也知道这样不可行,文件内容不止一页,这种零零散散存储需要的数据结构实现起来太麻烦了。

              +

              那怎么办?可以在heap内分配。那么到底怎么样才能在heap里分配?你该怎么知道heap哪里开始是空闲的,哪里是用过的,不还是得扫描页表吗?【思维大僵化】

              +

              其实……道理很简单。我们之间把proc->sz作为mapped-file的起始地址就好了。相信看到这里,你也明白这是什么原理了。能想到这个,我感觉确实很不容易。

              +

              正确思路

              初见思路虽然简单,但是很粗暴,如果文件很大,宝贵的内存空间就会被我们浪费。所以我们借用lazy allocation的思想,先建立memory-file的映射,再在缺页中断中通过文件读写申请内存空间,把文件内容读入内存。

              +

              问题就在于如何“先建立memory-file的映射”。在lazy allocation中,我们是先填好所有的对应页表项,仅是不申请对应的物理内存,也即占着XX不XX。在这次实验中,我们也是这么做,只不过新增了一个难点,那就是如何管理这些页。因为lazy allocation页与页之间没有比较紧密的关系,但是在mmap中页却可以被所属文件这个关键字划分。因而,我们需要一个数据结构,来给页分门别类地组织在一起,并且记录它们的meta data比如说所属文件之类的,这也就是hints里的VMA结构,也即我的filemap结构。

              +

              我们可以将这样的数据结构池化,并且存储在proc域中,以避免对象的重复创建。

              +
              +

              我的lazy法与别人不大一样……我没有想得像他们那么完美。我的做法是,在需要读某个地址的文件内容时,直接确保这个地址前面的所有文件内容都读了进来。也即在filemap中维护一个okva,表明vaokva这段内存已经读入,之后就仅需再读入okvaneed_va这段地址就行。这样虽然lazy了,但没完全lazy。

              +

              我认为这不能体现lazy的思想……因为一读读一坨,还是很占空间啊。

              +
              +

              因而,我们需要做的就是:

                -
              1. 给qemu提供运行时的参数

                -

                通过“/dev/kvm”设备,比如CPU个数、内存布局、运行等。

                -
              2. -
              3. 截获VM Exit事件【下面会讲,用来完成虚拟机和硬件环境的交互】并进行处理。

                -
              4. +
              5. 在mmap中将信息填入该数据结构

                +
                  +
                1. 依据传入的长度扩容proc,原sz作为mapped-file起始地址va
                2. +
                3. 从对象池中寻找到一个空闲的filemap,对其填写信息
                4. +
                5. 返回1所得的va
                -
                虚拟机层(右上)
                  -
                1. CPU——QEMU进程中的一个线程

                  -

                  通过QEMU和KVM的相互协作,虚拟机的线程会被宿主机操作系统正常调度,直接执行虚拟机中的代码

                  -
                2. -
                3. 物理地址——QEMU进程中的虚拟地址

                  -
                4. -
                5. 设备——QEMU实现

                  -

                  在运行过程中,虚拟机操作系统通过设备的I/O端口(Port IO、PIO)或者MMIO(Memory Mapped I/O)进行交互,KVM会截获这个请求【也即VM Exit,下面会讲】,大多数时候KVM会将请求分发到用户空间的QEMU进程中,由QEMU处理这些I/O请求

                  +

                  在我的代码中,还针对proc->sz不满足page-align做出了对策:先把文件的PGROUNDUP(sz)-sz这部分的信息读入,并且更新okva,这样一来,之后在usertrap中,就可以从okva开始一页页地分配地址,做到自然地page-align了。

                  +
                  +

                  为什么要对不满足page-align的情况进行处理?

                  +

                  这是因为,growproc的时候一次性扩充一页,但proc->sz却可以不满足page-align,也就是说,proc->sz所处的这一页已经被分配了。

                  +

                  在我们的lazy思路中,我们如果不预先读入文件页,是只能等待用户陷入缺页中断的情况下才能读入文件内容。

                  +

                  但是,proc->sz这一页已经被分配了。因而,在用户态读取这一页地址的时候,并不会发生缺页中断。因而,就会发生文件内容未读入,用户读到脏数据的情况。

                  +

                  其实还有一种更简单的办法可以强制page-align,那就是,直接让起始地址为PGROUNDUP(proc->sz)……至于为什么我不用这个,而要写这么多麻烦的东西呢?答案是我没想到。()

                  +
                6. +
                7. 在usertrap增加对缺页中断的处理

                  +
                    +
                  1. 依据va找到对应filemap
                  2. +
                  3. 根据对应filemap的信息,使用readi(正确)fileread(错误)读取文件内容并存入物理内存
                  -
                  虚拟机在QEMU-KVM架构的执行方法
                  状态管理虚拟化

                  虚拟机肯定是会与它的硬件环境进行交互的,它的硬件环境也就是QEMU—KVM。

                  -

                  虚拟机的用户程序和内核程序都是直接由宿主机的操作系统正常调度,我们可以将其看作虚拟态。QEMU—KVM可以看作是宿主机的进程,我们可以将其看作宿主态。因而,当虚拟机一些事情希望由QEMU—KVM来做,我们就需要从虚拟态转移到宿主态。

                  -

                  听起来有没有感觉很耳熟?是的,“从用户态陷入内核态”,跟这个的原理是一样的。

                  -

                  因而,虚拟机与硬件环境交互,实际上是虚拟态和宿主态状态的转换,如下图:

                  -

                  v2-9377a260d034d2904b1807d3fe53dcd9_1440w

                  -

                  VM Exit

                  -

                  当虚拟机中的代码是敏感指令或者说满足了一定的退出条件时,CPU会从虚拟态退出到KVM,这叫作VM Exit。

                  -

                  这就像在用户态执行指令陷入内核一样。

                  -

                  VM Exit首先陷入到KVM中进行处理,如果KVM无法处理,比如说虚拟机写了设备的寄存器地址,那么KVM会将这个写操作分派到QEMU中进行处理。

                  -

                  VM Entry

                  -

                  当KVM或者QEMU处好了退出事件之后,又可以将CPU置于虚拟态以运行虚拟机代码,这叫作VM Entry。

                  -
                  内存管理虚拟化
                  -

                  QEMU在初始化的时候会通过mmap分配虚拟内存空间作为虚拟机的物理内存,【感觉思路打开,物理内存与文件对应了起来】QEMU在不断更新内存布局的过程中会持续调用KVM接口通知内核KVM模块虚拟机的内存分布。

                  -
                  -

                  虚拟机在运行过程中,首先需要将虚拟机的虚拟地址(Guest Virtual Address,GVA)转换成虚拟机的物理地址(Guest Physical Address,GPA),然后将虚拟机的物理地址转换成宿主机的虚拟地址(Host Virtual Address,HVA),最终转换成宿主机的物理地址(Host Physical Address,HPA)。

                  -

                  整个寻址过程由硬件实现,具体实现方式为扩展页表(Extended Page Table,EPT)。

                  -

                  在支持EPT的环境中,虚拟机在第一次访问内存的时候就会陷入到KVM,KVM会逐渐建立起所谓的EPT页面【lazy思想贯穿始终,还是该叫自适应?】。这样虚拟机的虚拟CPU在后面访问虚拟机虚拟内存地址的时候,首先会被转换为虚拟机物理地址,接着会查找EPT页表,然后得到宿主机物理地址。【有种TLB的感觉】

                  -

                  v2-942e1ed598eed3d401d00e4719224d27_1440w

                  -
                  外设管理虚拟化

                  设备模拟的本质是要为虚拟机提供一个与物理设备接口完全一致的虚拟接口

                  -

                  虚拟机中的操作系统与设备进行的数据交互或者由QEMU和(或)KVM完成,或者由宿主机上对应的后端设备完成。

                  -

                  QEMU在初始化过程中会创建好模拟芯片组和必要的模拟设备,包括南北桥芯片、PCI根总线、ISA根总线等总线系统,以及各种PCI设备、ISA设备等。

                  -

                  外设虚拟化主要有如下几种方式:

                  +
                8. +
                9. 在munmap中进行释放

                    -
                  1. 纯软件模拟(完全虚拟化)

                    -

                    QEMU最早的方案,虚拟机内核不用做任何修改,每一次对设备的寄存器读写都会陷入到KVM,进而到QEMU,QEMU再对这些请求进行处理并模拟硬件行为。

                    -

                    软件模拟会导致非常多的QEMU/KVM接入,效率低下。

                    +
                  2. 根据标记写入文件页,并且释放对应物理内存
                  3. +
                  4. 修改filemap结构的参数,并且在其失效的时候放回对象池
                  5. +
                10. -
                11. virtio设备(半虚拟化)

                  -

                  virtio设备是一类特殊的设备,并没有对应的物理设备,所以需要虚拟机内部操作系统安装特殊的virtio驱动。

                  -

                  相比软件模拟,virtio方案提高了虚拟设备的性能。

                  +
                12. 修改fork和exit

                  +
                    +
                  1. exit

                    +

                    手动释放map-file域

                    +
                    +

                    为什么不能把这些合并到wait中调用的freepagetable进行释放呢?

                    +

                    因为freepagetable只会释放对应的物理页,没有达到munmap减少文件引用等功能。

                    +
                  2. -
                  3. 设备直通

                    -

                    将物理硬件设备直接挂到虚拟机上,虚拟机直接与物理设备交互,尽可能在I/O路径上减少QEMU/KVM的参与。

                    -

                    设备直通经常搭配硬件虚拟化支持技术SRIOV(Single Root I/O Virtualization,单根输入/输出虚拟化)使用,SRIOV能够将单个的物理硬件高效地虚拟出多个虚拟硬件。

                    +
                  4. fork

                    +

                    手动复制filemap池

                  -

                  v2-555d017ce5b65457f98617a5fdf232af_1440w

                  -
                  中断处理虚拟化

                  操作系统通过写设备的I/O端口或者MMIO地址来与设备交互,设备通过发送中断来通知操作系统事件。

                  -

                  QEMU/KVM一方面需要完成这项中断设备的模拟,另一方面需要模拟中断的请求处理

                  +
                13. +
                +

                我的错误思路们

                第一次错误思路

                上面说到:

                -

                QEMU支持单CPU的Intel 8259中断控制器以及SMP的I/O APIC(I/O Advanced Programmable Interrupt Controller)和LAPIC(Local Advanced Programmable Interrupt Controller)中断控制器。在这种方式下,虚拟外设通过QEMU向虚拟机注入中断,需要先陷入到KVM,然后由KVM向虚拟机注入中断,这是一个非常费时的操作。

                -

                为了提高虚拟机的效率,KVM自己也实现了中断控制器Intel 8259、I/O APIC以及LAPIC。用户可以有选择地让QEMU或者KVM模拟全部中断控制器,也可以让QEMU模拟Intel 8259中断控制器和I/O APIC,让KVM模拟LAPIC。

                +

                问题就在于如何“先建立memory-file的映射”。在lazy allocation中,我们是先填好所有的对应页表项,仅是不申请对应的物理内存,也即占着XX不XX。在这次实验中,我们也是这么做,只不过新增了一个难点,那就是如何管理这些页。因为lazy allocation页与页之间没有比较紧密的关系,但是在mmap中页却可以被所属文件这个关键字划分。因而,我们需要一个数据结构,来给页分门别类地组织在一起,并且记录它们的meta data比如说所属文件之类的,这也就是hints里的VMA结构,也即我的filemap结构。

                -

                xv6的全启动运行过程梳理

                介绍完上述的qemu虚拟化,接下来就可以对xv6的全启动进行一个梳理了。

                -

                首先,在宿主机执行make qemu

                -

                Makefile中可以看到:

                -
                qemu: $K/kernel fs.img
                $(QEMU) $(QEMUOPTS)
                QEMU = qemu-system-riscv64
                +

                官方给出的答案是在proc域里的pool。我……额……是把这些信息,存入在页中(真是自找麻烦呀)

                +

                具体来说,就是,我在mmap的时候给每个文件申请一页,然后在页的开头填上和filemap结构相差无几的那些参数,再加上一个next指针,表示下一个文件页的地址。页的剩下部分就用来存储数据。总的就是一个链表结构。

                +

                这个思路其实很不错,比起上面的直接在proc内存的尾巴扩容,这个空间利用率应该更大,并且不仅能节省物理内存,还能节省虚拟地址空间,实现了lazy上加lazy。

                +

                但问题是……我为什么非要傻瓜式操纵内存,在页的开头填入参数数据,而不是把这种页抽象为一个个node,最终形成一个十字链表的形式(差不多的意思,鱼骨状),组织进proc域,这样不挺好的吗……唔,有时候我头脑昏迷程度让我自己都感到十分震惊。归根结底,还是想得太少就动手了,失策。

                +

                总之放上代码。我没有实现next指针,仅假设文件内容不超过一页。也就是这一页开头在mmap中填meta data,其余部分在usertrap中填入文件内容。【这个分开的点也让我迷惑至极……】

                +
                #define ERRORADDR 0xffffffffffffffff

                void* mmap(void* address,size_t length,int prot,int flags,struct file* file,uint64 offset){
                struct proc* p = myproc();
                // 获取va,也即真正的address
                uint64 va = p->sz;
                if(growproc(PGSIZE) < 0)
                return (void*)ERRORADDR;
                char* mem = kalloc();
                if(mem == 0){
                return (void*)ERRORADDR;
                }
                memset(mem, 0, PGSIZE);
                // 保存信息:file指针、prot(这就是傻瓜式操纵内存的典范)
                uint64* pointer = (uint64*)mem;
                *pointer = (uint64)file;
                pointer++;
                *pointer = (uint64)prot;
                pointer++;
                *pointer = (uint64)length;
                pointer++;
                *pointer = (uint64)flags;
                pointer++;
                *pointer = (uint64)offset;
                pointer++;
                filedup(file);

                if(mappages(p->pagetable, va+PGSIZE, PGSIZE, (uint64)mem, PTE_M|PTE_X|PTE_U) != 0){
                kfree(mem);
                return (void*)ERRORADDR;
                }
                // return start of free memory
                return (void*)(va + (uint64)pointer - (uint64)mem);
                }
                int munmap(void* address,size_t length){
                struct proc* p = myproc();
                pte_t *pte;
                uint64* pa;

                if((pte = walk(p->pagetable, (uint64)address, 0)) == 0)
                return -1;
                if((*pte & PTE_V) == 0 ||(*pte & PTE_M) == 0)
                return -1;
                // the start is where the params save
                pa = (uint64*)(PGROUNDDOWN(PTE2PA(*pte)));
                struct file* file = (struct file*)(*pa);
                pa++;
                int prot = (int)(*pa);
                pa++;
                pa++;
                int flags = (int)(*pa);
                pa++;
                pa++;

                if(flags == MAP_SHARED&&(prot&PROT_WRITE) != 0){
                // 需要更新写内容
                filewrite(file,(uint64)address,length);
                }
                // 最后释放内存
                uvmunmap(p->pagetable, PGROUNDDOWN((uint64)address), 1, 1);
                return 0;
                }
                -

                在log中可以看到:

                -
                ...
                mkfs/mkfs fs.img README user/xargstest.sh user/_cat user/_echo user/_forktest user/_grep user/_init user/_kill user/_ln user/_ls user/_mkdir user/_rm user/_sh user/_stressfs user/_usertests user/_grind user/_wc user/_zombie user/_mmaptest
                ...
                qemu-system-riscv64 -machine virt -bios none -kernel kernel/kernel -m 128M -smp 3 -nographic -drive file=fs.img,if=none,format=raw,id=x0 -device virtio-blk-device,drive=x0,bus=virtio-mmio-bus.0

                xv6 kernel is booting

                hart 2 starting
                hart 1 starting
                init: starting sh
                $
                +
                } else if(r_scause() == 13 || r_scause() == 15){
                uint64 va = r_stval();
                pte_t *pte;
                uint64* pa;
                uint flags;

                if((pte = walk(p->pagetable, va, 0)) == 0)
                p->killed = 1;
                else if((*pte & PTE_V) == 0 ||(*pte & PTE_M) == 0)
                p->killed = 1;
                else {
                // the start is where the params save
                pa = (uint64*)(PGROUNDDOWN(PTE2PA(*pte)));
                flags = PTE_FLAGS(*pte);
                struct file* file = (struct file*)(*pa);
                pa++;
                int prot = (int)(*pa);
                pa++;
                size_t length = (size_t)(*pa);
                pa++;
                pa++;
                pa++;

                if((prot&PROT_READ) != 0){
                fileread(file,va,length);
                flags |= PTE_R;
                if((prot&PROT_WRITE) != 0) flags |= PTE_W;
                else if(r_scause() == 15) p->killed = 1;
                *pte = ((*pte) | flags);
                } else p->killed = 1;
                }
                }
                -

                具体的Makefile相关内容我不大了解,但结合输出,我想大概是先通过riscv64-linux-gnu-gcc编译链接完所有文件,然后再执行mkfs产生fs.img镜像(mkfs后面那些东西应该是文件参数,对应于源码中的读取可执行程序进磁盘的部分),最后再运行qemu-system-riscv64开始对虚拟机进行boot。

                -

                boot直至启动后的所有代码,都是通过QEMU-KVM架构处理,直接运行在宿主机的CPU上的。其余的各种管理,可以详见小标题虚拟机在QEMU-KVM架构的执行方法

                -

                mkfs的作用及源码解读

                作用

                上面的知识表明,操作系统的启动在于文件系统初始化之后,这是因为操作系统本身的启动代码,放在磁盘映像fs.img中,而fs.img正是由文件系统初始化时弄出来的。也就是说,文件系统是操作系统的爸爸。【我以前一直以为是反过来的】

                -

                image-20230121162324747

                +
                为什么下面的代码是错的

                正如开头所说的那样,我并没有完美做好这次实验,下面代码有一个致命的bug。

                +

                先说说致命bug是什么。

                +

                我的filemap结构体其实隐藏了两个具有“offset”这一含义的状态。一个是filemap里面的成员变量offset,另一个是filemap里面的成员变量file的成员变量off:

                +
                // in proc.h
                struct filemap{
                struct file* file;//文件
                uint64 offset;//va相对于file开头的offset
                };
                // in file.h
                struct file {
                uint off; // FD_INODE
                };
                + +

                在我的代码里,它们被赋予了不同的含义。

                +

                filemap->file->off被用于trap.c中,表示的是当前未读入文件内容的起始位置(实际上也就是okva-va的值),用于自然地使用fileread进行文件读入。

                -

                图中的boot块就是操作系统的引导扇区。

                +

                比如说,这次读入PGSIZE,那么off就会在fileread中自增PGSIZE。下次调用fileread就可以直接从下一个位置读入了,这样使代码更加简洁

                -

                mkfs的作用,正是把宿主机提供的虚拟地址空间作为虚拟磁盘,把虚拟地址空间划分为如上图所示的地址结构。它是运行在宿主机当中的。有了mkfs,才能有我们的虚拟机。

                -
                代码解读

                xv6分析–mkfs源代码注释

                -

                yysy这个就写得很好了。

                -

                user mem-allocator

                -

                linux的堆管理

                -

                那么malloc到底是怎么实现的呢?不是每次要申请内存就调一下系统调用,而是程序向操作系统申请⼀块适当⼤⼩的堆空间,然后由程序⾃⼰管理这块空间,⽽具体来讲,管理着堆空间分配的往往是程序的运⾏库

                -

                也就是说,malloc本质上是以运行库而非系统调用形式出现的。它里面用到的是sbrk和mmap这两个系统调用来进货。

                -

                glibc的malloc函数是这样处理⽤户的空间请求的:对于⼩于128KB的请求来说,它会在现有的堆空间⾥⾯,按照堆分配算法为它分配⼀块空间并返回;对于⼤于128KB的请求来说,它会使⽤mmap()函数为它分配⼀块匿名空间,然后在这个匿名空间中为⽤户分配空间。

                +

                filemap->offset被用于munmap中。filewritefileread一样,都是从file->off处开始取数据。munmap所需要取数据的起始位置和trap.c中需要取数据的起始位置肯定不一样,

                +
                +

                想想它们的功能。trap.c的off需要始终指向有效内存段的末尾,但munmap由于要对特定内存段进行写入文件操作,因而off要求可以随机指向。

                -

                在内核态中,我们使用kallockfree来申请和释放内存页。在用户态中,我们使用mallocfree来对动态内存进行管理。【也就是说这个实现的是堆管理

                -

                内核中的最小单位只能是页,但user mem-allocator对外提供的申请内存服务的最小单位不是页,而是sizeof(Header)。因而,这就需要我们的user mem-allocator进行数据结构的管理,来统一这二者的实现。

                -

                数据结构

                环形链表

                user mem-allocator的数据结构是环形链表,起始结点为一个空数据载体。

                -

                image-20230316140158908

                -

                image-20230316140450988

                -

                地址从低到高

                链表的头结点的存储地址/所代表的内存地址的地址数值最小,并且其余结点按遍历顺序地址递增。

                -

                具体实现

                user mem-allocator由三个主要函数组成,分别是morecoremallocfree。一个一个地来说未免有点不符合正常人的思路,所以我接下来会以用户初次调用malloc为例,来整理user mem-allocator的具体实现。

                -

                malloc

                当用户初次调用malloc,此时freep仍为空指针,因而会进入如下分支:

                -
                if((prevp = freep) == 0){
                // 空闲mem为空的情况
                base.s.ptr = freep = prevp = &base;
                base.s.size = 0;
                }
                - -

                也即初始化为这种情况:

                -

                image-20230316143711888

                -

                随后,由于prevp->ptr == freep,故而会在循环中进入该分支:

                -
                for(p = prevp->s.ptr; ; prevp = p, p = p->s.ptr){
                // ...
                if(p == freep) // 一般情况下,此处表明已经完整遍历了一遍环形链表,因为prev的初值是freep,而我们是从prev->next开始遍历的
                if((p = morecore(nunits)) == 0)
                return 0;
                }
                +

                因而,我们可以将当前va对应的文件位置记录在offset中。届时,我们只需要从p->filemaps[i].offset+va-p->filemaps[i].va取数据就行。

                +

                上述两个变量相辅相成,看上去似乎能够完美无缺地实现我们的功能。但是,实际上,不行。为什么呢?因为它们的file指针,filemap->file,如果被两个mmap区域同时使用的话,就会出问题。

                +

                可以来看看mmaptest.c中的这一段代码:

                +
                  makefile(f);
                if ((fd = open(f, O_RDONLY)) == -1)
                err("open");

                unlink(f);
                char *p1 = mmap(0, PGSIZE*2, PROT_READ, MAP_SHARED, fd, 0);
                char *p2 = mmap(0, PGSIZE*2, PROT_READ, MAP_SHARED, fd, 0);

                // read just 2nd page.
                if(*(p1+PGSIZE) != 'A')
                err("fork mismatch (1)");
                if((pid = fork()) < 0)
                err("fork");

                if (pid == 0) {
                // v1是用来触发缺页中断的函数
                _v1(p1);
                munmap(p1, PGSIZE); // just the first page
                exit(0); // tell the parent that the mapping looks OK.
                }

                int status = -1;
                wait(&status);

                if(status != 0){
                printf("fork_test failed\n");
                exit(1);
                }

                // check that the parent's mappings are still there.
                printf("before v1,p1 = %d\n",(uint64)p1);
                _v1(p1);
                printf("after v1,p1 = %d\n",(uint64)p1);
                _v1(p2);


                printf("fork_test OK\n");

                /*输出:
                fork_test starting
                trap:map a page at 53248,okva = 53248
                trap:mem[0]=65,off = 4096,size = 6144
                trap:map a page at 57344,okva = 53248
                trap:mem[0]=65,off = 6144,size = 6144
                before v1,p1 = 53248
                after v1,p1 = 53248
                trap:map a page at 61440,okva = 61440
                trap:mem[0]=0,off = 6144,size = 6144
                mismatch at 0, wanted 'A', got 0x0
                mmaptest: fork_test failed: v1 mismatch (1), pid=3
                */
                -

                调用morecore

                -

                morecore

                进入morecore后,首先会对堆内存进行扩容:

                -
                if(nu < 4096)
                nu = 4096;
                p = sbrk(nu * sizeof(Header));
                if(p == (char*)-1)
                return 0;
                +
                // in trap.c
                printf("trap:map a page at %d,okva = %d\n",start_va,p->filemaps[i].okva);

                fileread(p->filemaps[i].file,start_va,PGSIZE);

                printf("trap:mem[0]=%d,off = %d,size = %d\n",
                mem[0],p->filemaps[i].file->off,p->filemaps[i].file->ip->size);
                -

                其中,nu表示要申请的内存单元数,一个内存单元为sizeof(Header),因而nu在malloc中计算如下:

                -
                nunits = (nbytes + sizeof(Header) - 1)/sizeof(Header) + 1;
                +

                这段代码因为共用fd,导致file指针被两个mmap区域同时使用。

                +
                +

                共用fd,为什么file指针也一起共用了?

                +

                可以追踪一下它们的生命周期:

                +
                // in sys_open()
                // 获取file结构体和文件描述符。
                if((f = filealloc()) == 0 || (fd = fdalloc(f)) < 0){

                // in sysfile.c
                // Allocate a file descriptor for the given file.
                // Takes over file reference from caller on success.
                static int
                fdalloc(struct file *f)
                {
                int fd;
                struct proc *p = myproc();

                for(fd = 0; fd < NOFILE; fd++){
                if(p->ofile[fd] == 0){
                p->ofile[fd] = f;
                return fd;
                }
                }
                return -1;
                }
                -

                为了满足内核以一页为最小内存单位的需求,以及避免过多陷入内核态,它每次会申请至少4096*内存单元的堆空间。

                -

                对堆内存进行扩容完之后,morecore会手动调用一次free,将新申请到的内存加入数据结构中。【此处类似于在knit中调用kfree的原理】

                -

                free

                void free(void *ap){
                Header *bp, *p;

                bp = (Header*)ap - 1;
                for(p = freep; !(bp > p && bp < p->s.ptr); p = p->s.ptr)
                if(p >= p->s.ptr && (bp > p || bp < p->s.ptr))
                break;
                +

                可以看到,它实际上是有一个文件描述符表,key为fd,value为file指针。因而,同一张表,fd相同,file指针相同。

                +

                注:父子进程,同样的fd,file指针也是相同的

                +

                fork出来的父子进程同一个句柄对同一个文件的偏移量是相同的,这个原理应该是因为,父子进程共享的是文件句柄这个结构体对象本身,也就是拷贝的时候是浅拷贝而不是深拷贝。

                +
                // in fork()
                // increment reference counts on open file descriptors.
                for(i = 0; i < NOFILE; i++)
                if(p->ofile[i])
                np->ofile[i] = filedup(p->ofile[i]);
                +
                +

                最后的check that the parent's mappings are still there.环节中,_v1(p1)执行时并没有陷入trap,这是正常的。不正常的是_v1(p2)的执行结果。它陷入了trap,但是却因file->off == file size,导致被判定为已全部读入文件,事实上却是并没有读入文件。

                +

                为什么会这样呢?

                +

                这是因为p1和p2共用同一个fd,也就共用了同一个file指针。共用了一个file指针,那么p1和p2面对的file->off相同。上面说到,file->off用于控制文件映射。那么,当p1完成了对文件的映射,p1的off指针如果不加重置,就会永远停留在file size处。这样一来,当p2想要使用同样的file指针进行文件映射时,就会出问题。

                +

                这个问题的一个解决方法是每次mmap都深拷贝一个船新file结构体。但是这样的话,file域里的ref变量就失去了它的意义,并且file对象池应该也很快就会爆满,非常不符合设计方案。

                +

                这个问题的完美解,是不要赋予file->off这个意义,而是使用readi替代fileread

                +
                fileread(struct file *f, uint64 addr, int n)
                readi(struct inode *ip, int user_dst, uint64 dst, uint off, uint n)
                -

                由于此时freep == freep->str == base,并且我们在morecore中新申请的内存空间ap满足ap > base,故而会跳出循环。

                +

                这样做的好处是,我们可以实时计算offset(前面提到,其恰恰等于okva-va),而不用把这个东西用file的off来表示。

                -

                为什么ap > base呢?

                -

                别忘了我们扩容的原理。我们是以proc->size为起始地址扩容的。ap处在扩容内存中,因而ap>旧size;base处在扩容前内存内,因而base<=旧size。故而有ap>base。

                +

                也确实,我之所以弯弯绕绕那么曲折,是因为只想到了fileread这个函数,压根没注意到还有一个readi……

                -
                if(bp + bp->s.size == p->s.ptr){
                bp->s.size += p->s.ptr->s.size;
                bp->s.ptr = p->s.ptr->s.ptr;
                } else
                bp->s.ptr = p->s.ptr;
                if(p + p->s.size == bp){
                p->s.size += bp->s.size;
                p->s.ptr = bp->s.ptr;
                } else
                p->s.ptr = bp;
                freep = p;
                +

                我在下面的代码仅做了一个能够通过测试,但是上面的bug依然存在的功利性折中代码。我是这么实现的:

                +
                // 在`mmap`的时候初始化`file->off`
                p->filemaps[i].file->off = offset;
                // 在`munmap`的时候清零`file->off`
                p->filemaps[i].file->off = 0;
                -

                跳出循环后,我们会进入第一个if的第二个分支,以及第二个if的第二个分支。经过这些指针操作后,此时我们的数据结构如下图所示:

                -

                image-20230316145733160

                -

                也即形成了一个两节点的环形链表。

                -

                malloc

                经历完上述调用后,我们回到malloc的循环中:

                -
                for(p = prevp->s.ptr; ; prevp = p, p = p->s.ptr){
                // ...
                if(p == freep)
                if((p = morecore(nunits)) == 0)
                return 0;
                }
                -

                morecore的返回值可知,此时我们的p应该指向freep。本轮循环结束后执行 p = p->s.ptr,此时我们的p指向了我们刚在morecore中扩容出来的那一大段内存。

                -

                image-20230316150327569

                -

                在下一轮循环中,由于我们刚刚通过morecore申请了至少nunits的空间,因而我们将进入该分支:

                -
                if(p->s.size >= nunits){
                if(p->s.size == nunits)
                // 如果与所需的内存刚好相等,那就直接返回该小单元就行
                prevp->s.ptr = p->s.ptr;
                else {
                // 不等的话就只划分出一小部分
                // 一次划出几个header单元
                p->s.size -= nunits;
                p += p->s.size;
                p->s.size = nunits;
                }
                freep = prevp;
                return (void*)(p + 1);
                }
                -

                nunits >= 4096,也即p->s.size == nunits,p所指向的地址恰好就是我们接下来会用的地址。因而,我们就将这部分内存空间从我们的freelist中剔除,在之后返回p的地址即可。

                -

                nunits < 4096,也即p->s.size != nunits,说明p所指向的这块内存空间比我们需要的大,那么我们就仅将该段内存空间切割出需要的那一小部分,再把p指向那一小部分开头的地方,返回p地址即可,如图所示。

                -

                image-20230316150846709

                -

                这样一来,我们就成功给用户它所需要的内存空间了。

                -

                free

                进行malloc之后,用户还需要调用free来手动释放内存,防止内存泄漏。

                -

                image-20230316151116462

                -
                for(p = freep; !(bp > p && bp < p->s.ptr); p = p->s.ptr)
                if(p >= p->s.ptr && (bp > p || bp < p->s.ptr))
                break;
                +

                因而,结论是,一步错步步错,一个错误需要更多的错误来弥补,最后还是错的(悲)

                +
                如何把下面的错误思路改成正确思路

                可以做以下几点:

                +
                  +
                1. 正确地lazy

                  +

                  每次trap仅分配一页。

                  +
                2. +
                3. 改用readi函数,修改file->off的语义

                  +
                4. +
                +

                这样一来,大概就可以完美地正确了。

                +

                其他的一些小细节

                file指针的生命周期

                在数据结构中存储file指针至关重要。但仔细想一想,file指针的生命周期似乎长到过分:从sys_mmap被调用,一直到usertrap处理缺页中断,最后到munmap释放,我们要求file指针的值需要保持稳定不变。

                +

                这么长的生命周期,它真的可以做到吗?毕竟file指针归根到底只是一个局部变量,在syscall mmap结束之后,它还有效吗?答案是有效的,这个有效性由mmap实现中对ref的增加来实现保障。

                +

                在用户态中关闭一个文件,需要使用syscallclose(int fd)。不妨来看看close的代码。

                +
                // in kernel/sysfile.c
                uint64
                sys_close(void)
                {
                int fd;
                struct file *f;

                if(argfd(0, &fd, &f) < 0)
                return -1;
                // 一个进程打开的文件都会放入一个以fd为index的文件表里,
                // 在xv6中,这个文件表便是`myproc()->ofile`。
                // 可以看到,关闭一个文件首先需要把它移出文件表
                myproc()->ofile[fd] = 0;
                // 对file指针关闭的主要操作
                fileclose(f);
                return 0;
                }

                // in kernel/file.c
                // Close file f. (Decrement ref count, close when reaches 0.)
                void
                fileclose(struct file *f)
                {
                struct file ff;

                acquire(&ftable.lock);
                // 若ref数<0,就会直接return
                if(--f->ref > 0){
                release(&ftable.lock);
                return;
                }
                // 释放file
                // close不会显式地释放file指针,只会释放file指针所指向的文件,让file指针失效。
                ff = *f;
                f->ref = 0;
                f->type = FD_NONE;
                release(&ftable.lock);

                if(ff.type == FD_PIPE){
                pipeclose(ff.pipe, ff.writable);
                } else if(ff.type == FD_INODE || ff.type == FD_DEVICE){
                begin_op();
                iput(ff.ip);
                end_op();
                }
                }
                -

                由于ap > baseap > 旧p->size = base->ptrbase < base->ptr,故而首先会进行一轮循环。再然后,由于p = 旧p->size,并且p > p->ptr = base,并且ap > 旧size,故而跳出循环。

                +

                可以看到,当ref数>1时,file指针就不会失效。

                +

                这就是为什么我们还需要在mmap中让file的ref数++。

                +
                缺页中断蕴含的设计思想

                如果只存入file指针,用户态要如何对对应的文件进行读写呢?

                +

                我们可以自然想到也许需要设计一个函数,让用户在想要对这块内存读写的时候调用这个函数即可。但是,这样的方法使得用户对内存不能自然地读写,还需要使用我们新设计的这个函数,这显然十分地不美观。所以,我们需要找到一个方法,让上层的用户可以统一地读取任何的内存块,包括memory-mapped file内存块,而隐藏memory-mapped file与其他内存块读写方式不同的这些复杂细节。经历过前面几次实验的你看到这里一定能想到,有一个更加优美更加符合设计规范的方法,那就是:缺页中断

                -

                此处循环中,循环语句内部的这个循环实际上是对遍历到环形链表尾部,即将从头开始遍历,这个边界情况的处理。比较符合逻辑的还是循环语句内的那个条件。

                +

                没做这个实验之前就知道mmap需要借助缺页中断来实现了,但实际自己的第一印象是觉得并不需要缺页中断,直到分析到这里才恍然大悟。

                +

                “让上层的用户可以统一地读取任何的内存块,而隐藏不同类型的内存块读写方式不同的这些复杂细节”

                +

                仔细想想,前面几个关于缺页中断的实验,比如说cow fork,lazy allocation,事实上都是基于这个思想。它们并不是不能与缺页中断分离,只是有了缺页中断,它们的实现更加简洁,更加优美。

                +

                再次感慨os的博大精深。小小一个缺页中断,原理那么简单,居然集中了这么多设计思想,不禁叹服。

                -
                if(bp + bp->s.size == p->s.ptr){
                bp->s.size += p->s.ptr->s.size;
                bp->s.ptr = p->s.ptr->s.ptr;
                } else
                bp->s.ptr = p->s.ptr;
                if(p + p->s.size == bp){
                p->s.size += bp->s.size;
                p->s.ptr = bp->s.ptr;
                } else
                p->s.ptr = bp;
                freep = p;
                +
                正确答案的munmap中如果遇到未映射的页怎么办

                在正确答案的munmap中:

                +
                //释放已经申请的页表项、内存,并且看看是不是需要写回
                while(start_va < bounder){
                if(p->filemaps[i].flags == MAP_SHARED){
                //写回
                filewrite(p->filemaps[i].file,start_va,PGSIZE);
                }
                uvmunmap(p->pagetable,start_va,1,1);
                start_va += PGSIZE;
                }
                -

                此时会进入第二个if的第一个分支。具体情况看图就行,不多bb。

                -

                总结

                主要就是这个数据结构用得很巧妙但也很复杂。它吸取了内核态中分配内存使用一个freelist的特点,同时又巧妙地利用了内存地址有序的特点,从而实现碎片内存管理。我的建议是多画图。

                -

                还有其实有一点我不是很理解。我觉得freep这个变量的用意非常不明,它似乎并不是指代整个freelist的头,因为它在很多个地方都诡异地赋值了一次。我想,它也许始终指向上一次被alloc/被free的内存的前一个吧。。。我猜测这样设计是为了蕴含一些LRU的思想。不大明白。

                -

                m-s-u权限切换

                由os知识可知,机器态、内核态、用户态分别有三种不同的操作权限。xv6是如何对权限切换进行管理的呢?

                -

                这部分知识我在正文的一个小地方记录了下来,详见 chapter2 - Code: starting xv6 and the fifirst process - xv6 - 感想 的第二点。

                -

                Lock实验的评测机制

                在xv6该次实验中,为了实现评测可视化,引入了statistics机制对结果进行评估。下面,我将通过源码简单介绍其实现机制。

                -

                来讲讲这玩意是怎么实现用户态读取锁争用次数的。我们从statistics函数可看出,它的本质是通过读取“文件”,来从内核中读取争用次数的相关数据:

                -
                int statistics(void *buf, int sz) {
                fd = open("statistics", O_RDONLY);
                ...
                if ((n = read(fd, buf+i, sz-i)) < 0) {
                }
                +

                如果map类型为MAP_SHARED,并且该页尚未映射,会怎么样呢?

                +

                追踪filewrite的路径

                +
                // in file.c
                begin_op();
                ilock(f->ip);
                if ((r = writei(f->ip, 1, addr + i, f->off, n1)) > 0)
                f->off += r;
                iunlock(f->ip);
                end_op();
                // in fs.c
                if(either_copyin(bp->data + (off % BSIZE), user_src, src, m) == -1) {
                brelse(bp);
                break;
                }
                log_write(bp);
                // in vm.c copyin()
                int
                copyin(pagetable_t pagetable, char *dst, uint64 srcva, uint64 len)
                {
                uint64 n, va0, pa0;

                while(len > 0){
                va0 = PGROUNDDOWN(srcva);
                pa0 = walkaddr(pagetable, va0);
                if(pa0 == 0)
                return -1;
                // ...
                -

                那么安装以前所学的内容,我们很容易联系到这玩意应该并不是个文件,而是类似于proc文件系统那样的虚拟文件。它应该会在open、read中根据其特有的文件类型进行转发。在init.c中,我们可以看到:

                -
                main(void)
                {
                if(open("console", O_RDWR) < 0){
                mknod("console", CONSOLE, 0);
                mknod("statistics", STATS, 0);
                open("console", O_RDWR);
                }
                +

                copyin最终会在 if(pa0 == 0) return -1;这里终结,但writei并不会在接收到-1的时候爆出panic或者是引发缺页中断,而只会把它当做文件结尾,默默地返回。

                +

                并且,在munmap中是一页一页地释放,而不是直接传参length全部释放,这一点也很重要。因为我们的lazy allocation很可能导致va~va+length这一区间内只是部分页被映射,部分页没有。如果直接传参length释放,那么在遇到第一页未被映射的时候,filewrite就会终止,该页之后的页就没有被写回文件的机会了。

                +

                所以结论是,在正确实现的munmap中遇到未映射的页会自动跳过,什么也不会发生。

                +

                代码

                数据结构

                // in param.h
                #define NFILEMAP 32

                // in proc.h
                struct filemap{
                uint isused;//对象池思想。该filemap是否正在被使用
                uint64 va;//该文件的起始内存页地址
                uint64 okva;//该文件的起始未被读入部分对应的内存地址
                struct file* file;//文件
                size_t length;//需要映射到内存的长度
                int flags;//MAP_SHARED OR MAP_PRIVATE
                int prot;//PROT_READ OR PROT_WRITE
                uint64 offset;//va相对于file开头的offset
                };

                // Per-process state
                struct proc {
                struct filemap filemaps[NFILEMAP];
                };
                -

                这玩意的文件结点实际上是在创建console时整的,并且其有一个特殊的文件类型“STATS”。我们可以进一步追溯到kernel中的main.c

                -
                void main()
                {
                if(cpuid() == 0){
                #if defined(LAB_PGTBL) || defined(LAB_LOCK)
                statsinit();
                +

                mmap

                具体系统调用注册过程略。

                +
                // in sysproc.c
                uint64
                sys_mmap(void){
                uint64 addr;
                int length,prot,flags,offset;
                struct file* file;
                if(argaddr(0,&addr) < 0 || argint(1,&length) < 0 || argint(2,&prot) < 0 || argint(3,&flags) < 0 || argfd(4,0,&file) ||argint(5,&offset) < 0)
                return -1;
                return (uint64)mmap((void*)addr,(size_t)length,prot,flags,file,(uint)offset);
                }
                -
                void
                statsinit(void)
                {
                initlock(&stats.lock, "stats");

                devsw[STATS].read = statsread;
                devsw[STATS].write = statswrite;
                }
                +
                #define ERRORADDR 0xffffffffffffffff

                // 映射file从offset开始长度为length的内容到内存中,返回内存中的文件内容起始地址
                void* mmap(void* address,size_t length,int prot,int flags,struct file* file,uint64 offset){
                // mmap的prot权限必须与file的权限对应,不能file只读但是mmap却可写且shared
                if((prot&PROT_WRITE) != 0&&flags == MAP_SHARED &&file->writable == 0)
                return (void*)ERRORADDR;

                struct proc* p = myproc();
                uint64 va = 0;
                int i=0;

                //找到filemap池中第一个空闲的filemap
                for(i=0;i<NFILEMAP;i++){
                if(!p->filemaps[i].isused){
                // 获取va,也即真正的address
                va = p->sz;
                p->sz += length;
                // 其实这里用一个memcpy会更加优雅,可惜我忘记了()
                p->filemaps[i].isused = 1;
                p->filemaps[i].va = va;
                p->filemaps[i].okva = va;
                p->filemaps[i].length = length;
                p->filemaps[i].prot = prot;
                p->filemaps[i].flags = flags;
                p->filemaps[i].file = file;
                p->filemaps[i].file->off = offset;
                p->filemaps[i].offset = offset;
                // 增加文件引用数
                filedup(file);
                break;
                }
                }
                if(va == 0) return (void*)ERRORADDR;
                // return start of free memory
                uint64 start_va = PGROUNDUP(va);
                // 先读入处于proc已申请的内存页区域(也即没有内存对齐情况下)
                uint64 off = start_va - va;
                if(off < PGSIZE){
                fileread(file,va,off);
                file->off += off;
                p->filemaps[i].okva = va+off;
                }
                return (void*)va;
                }
                -

                可以看到,它给这个STATS文件类型注册了这两个函数。当我们调用read和write时,实际上就是在调用这俩玩意。我们可以看下这两个handler都干了啥。

                -
                #define BUFSZ 4096
                static struct {
                struct spinlock lock;
                char buf[BUFSZ];
                int sz;
                int off;
                } stats;

                int statsread(int user_dst, uint64 dst, int n) {
                int m;
                acquire(&stats.lock);

                if(stats.sz == 0) {
                #ifdef LAB_LOCK
                stats.sz = statslock(stats.buf, BUFSZ); // 把信息copy进自己的缓冲区里
                #endif
                }
                m = stats.sz - stats.off;

                if (m > 0) { // 如果有新东西,就copy到用户缓冲区里
                if(m > n) m = n;
                if(either_copyout(user_dst, dst, stats.buf+stats.off, m) != -1) {
                stats.off += m;
                }
                } else {
                m = -1;
                stats.sz = 0;
                stats.off = 0;
                }
                release(&stats.lock);
                return m;
                }
                +

                usertrap

                错的
                } else if(r_scause() == 13 || r_scause() == 15){
                uint64 va = r_stval();

                for(int i=0;i<NFILEMAP;i++){
                // 找到va对应的filemap
                if(p->filemaps[i].isused&&va>=p->filemaps[i].va
                && va<p->filemaps[i].va+p->filemaps[i].length){
                // 说明本来就不应该写
                if(r_scause() == 15 && ((p->filemaps[i].prot)&PROT_WRITE) == 0){
                p->killed = 1;
                break;
                }
                //说明地址不在文件范围内
                if(p->filemaps[i].va+p->filemaps[i].file->ip->size <= va){
                p->killed = 1;
                break;
                }
                // 能进到这里来的都是产生了缺页中断,也就是说va对应文件数据不存在
                // 我们需要维护一个okva,表示从filemaps.va到okva这段地址已经加载了文件
                // 这样一来,我们这里就只需加载okva~va地址对应的文件了
                // file结构体自带的off成员会由于fileread而自动增长到对应位置,所以文件可以自然地读写
                uint64 start_va = p->filemaps[i].okva;// okva一定是page-align的
                // 加载文件内容
                while(start_va <= va){
                char* mem = kalloc();
                if(mem == 0){
                p->killed = 1;
                break;
                }
                memset(mem, 0, PGSIZE);
                int flag = PTE_X|PTE_R|PTE_U;
                if(((p->filemaps[i].prot)&PROT_WRITE) != 0){
                flag |= PTE_W;
                }
                if(mappages(p->pagetable, start_va, PGSIZE, (uint64)mem, flag) != 0){
                p->killed = 1;
                kfree(mem);
                break;
                }
                // 读入文件内容
                fileread(p->filemaps[i].file,start_va,PGSIZE);
                start_va += PGSIZE;
                }
                p->filemaps[i].okva = start_va;
                break;
                }
                }
                }
                -
                int statswrite(int user_src, uint64 src, int n) { // WARNING: READ ONLY!!!
                return -1;
                }
                +
                对的
                } else if(r_scause() == 13 || r_scause() == 15){
                uint64 va = r_stval();
                for(int i=0;i<NFILEMAP;i++){
                if(p->filemaps[i].isused&&va>=p->filemaps[i].va && va<p->filemaps[i].va+p->filemaps[i].length){
                if(r_scause() == 15 && ((p->filemaps[i].prot)&PROT_WRITE) == 0){
                // 说明本来就不应该写
                p->killed = 1;
                break;
                }
                if(p->filemaps[i].va+p->filemaps[i].file->ip->size <= va){
                //说明地址不在文件范围内
                p->killed = 1;
                break;
                }
                uint64 start_va = PGROUNDDOWN(va);
                char* mem = kalloc();
                if(mem == 0){
                p->killed = 1;
                break;
                }
                memset(mem, 0, PGSIZE);
                int flag = PTE_X|PTE_R|PTE_U;
                if(((p->filemaps[i].prot)&PROT_WRITE) != 0){
                flag |= PTE_W;
                }
                if(mappages(p->pagetable, start_va, PGSIZE, (uint64)mem, flag) != 0){
                p->killed = 1;
                kfree(mem);
                break;
                }
                readi(p->filemaps[i].file->ip,0,(uint64)mem,va-p->filemaps[i].va+p->filemaps[i].offset,PGSIZE);
                break;
                }
                }
                }
                -

                可以看到其本质就是把statslock返回的东西copy到用户空间了。我们来结合最后的输出效果看看statslock的具体实现:

                -

                image-20231024232632816

                -
                int statslock(char *buf, int sz) {
                int n;
                int tot = 0;
                int found = 0;

                acquire(&lock_locks);
                n = snprintf(buf, sz, "--- lock kmem/bcache stats\n");
                for(int i = 0; i < NLOCK; i++) {
                if(locks[i] == 0) break;
                if(strncmp(locks[i]->name, "bcache", strlen("bcache")) == 0 ||
                strncmp(locks[i]->name, "kmem", strlen("kmem")) == 0) {
                tot += locks[i]->nts; // 记入->nts计数
                /*
                snprint_lock: lock: %s: #fetch-and-add %d #acquire() %d\n
                */
                n += snprint_lock(buf +n, sz-n, locks[i]);
                found += 1;
                }
                }

                // Require at least two locks name after kmem/bcache.
                if (found < 2) {
                tot = -1;
                }

                // 简单粗暴地计算前五多争用的进程
                n += snprintf(buf+n, sz-n, "--- top 5 contended locks:\n");
                int last = 100000000;
                // stupid way to compute top 5 contended locks
                for(int t = 0; t < 5; t++) {
                int top = 0;
                for(int i = 0; i < NLOCK; i++) {
                if(locks[i] == 0)
                break;
                if(locks[i]->nts > locks[top]->nts && locks[i]->nts < last) {
                top = i;
                }
                }
                /*
                snprint_lock: lock: %s: #fetch-and-add %d #acquire() %d\n
                */
                n += snprint_lock(buf+n, sz-n, locks[top]);
                last = locks[top]->nts;
                }
                n += snprintf(buf+n, sz-n, "tot= %d\n", tot);
                release(&lock_locks);
                return n;
                }
                +

                munmap

                错的
                uint64 min(uint64 a,uint64 b){return a>b?b:a;}

                // 释放文件映射以address为起始地址,length为长度这个范围内的内存地址空间
                int munmap(void* address,size_t length){
                struct proc* p = myproc();
                uint64 va = (uint64)address;

                // 找到对应的filemap
                for(int i=0;i<NFILEMAP;i++){
                if(p->filemaps[i].isused&&p->filemaps[i].va<=va&&p->filemaps[i].va+length>va){
                // 开始释放的内存地址
                uint64 start_va;
                if(va == p->filemaps[i].va)
                start_va = PGROUNDUP(p->filemaps[i].va);
                else
                start_va = PGROUNDDOWN(va);
                // 结束释放的内存地址
                uint64 bounder = p->filemaps[i].va + min(p->filemaps[i].file->ip->size,length);

                //file的off在trap中用于表示文件已加载的位置
                //在这里需要用off进行filewrite,所以需要对原本在usertrap用于记录加载位置的off进行手动保存
                uint64 tmp_off = p->filemaps[i].file->off;
                p->filemaps[i].file->off = p->filemaps[i].offset+va-p->filemaps[i].va;

                //释放已经申请的页表项、内存,并且看看是不是需要写回
                while(start_va < bounder && start_va < p->filemaps[i].okva){
                if(p->filemaps[i].flags == MAP_SHARED){
                //写回
                filewrite(p->filemaps[i].file,start_va,PGSIZE);
                }
                uvmunmap(p->pagetable,start_va,1,1);
                start_va += PGSIZE;
                }

                //修改filemap结构体的起始地址va和长度,offset也要变,因为他记录va对应的是文件哪个位置
                if(va == p->filemaps[i].va){
                //释放的是头几页
                p->filemaps[i].offset += length;
                p->filemaps[i].va = va+length;
                p->filemaps[i].length -= length;
                }else {
                //释放的是尾几页
                p->filemaps[i].length -= p->filemaps[i].length - va;
                }
                p->filemaps[i].file->off = tmp_off;
                // 检验map的合理性
                if(p->filemaps[i].length == 0 || p->filemaps[i].va >= p->filemaps[i].va+length
                || p->filemaps[i].file->off > p->filemaps[i].file->ip->size){
                p->filemaps[i].isused = 0;//释放

                // 注意!!!!这句话对我的错误代码来说非常重要
                p->filemaps[i].file->off = 0;
                fileclose(p->filemaps[i].file);
                }
                }
                }
                return 0;
                }
                -

                可以看到其争用本质计算是通过spinlock::nts字段记录。我们来看看这玩意的引用:

                -
                void initlock(struct spinlock *lk, char *name) {
                #ifdef LAB_LOCK
                lk->nts = 0;
                #endif
                }

                void acquire(struct spinlock *lk) {
                ...
                while(__sync_lock_test_and_set(&lk->locked, 1) != 0) {
                #ifdef LAB_LOCK
                __sync_fetch_and_add(&(lk->nts), 1);
                #else
                ;
                #endif
                }
                +
                对的
                uint64 min(uint64 a,uint64 b){return a>b?b:a;}

                int munmap(void* address,size_t length){
                struct proc* p = myproc();
                uint64 va = (uint64)address;
                for(int i=0;i<NFILEMAP;i++){
                if(p->filemaps[i].isused&&p->filemaps[i].va<=va&&p->filemaps[i].va+length>va){
                uint64 start_va;
                if(va == p->filemaps[i].va)
                start_va = PGROUNDUP(p->filemaps[i].va);
                else
                start_va = PGROUNDDOWN(va);
                uint64 bounder = p->filemaps[i].va + min(p->filemaps[i].file->ip->size,length);
                //在这里需要用off进行读写,所以需要对原本的加载处off手动保存
                uint64 tmp_off = p->filemaps[i].file->off;
                p->filemaps[i].file->off = p->filemaps[i].offset+va-p->filemaps[i].va;

                //释放已经申请的页表项、内存,并且看看是不是需要写回
                while(start_va < bounder){
                if(p->filemaps[i].flags == MAP_SHARED){
                //写回
                filewrite(p->filemaps[i].file,start_va,PGSIZE);
                }
                uvmunmap(p->pagetable,start_va,1,1);
                start_va += PGSIZE;
                }

                //修改filemap结构体的起始地址va和长度,offset也要变,因为他记录va对应的是文件哪个位置
                if(va == p->filemaps[i].va){
                //释放的是头几页
                p->filemaps[i].offset += length;
                p->filemaps[i].va = va+length;
                p->filemaps[i].length -= length;
                }else {
                //释放的是尾几页
                p->filemaps[i].length -= p->filemaps[i].length - va;
                }
                // 检验map的合理性
                if(p->filemaps[i].length == 0 || p->filemaps[i].va >= p->filemaps[i].va+length
                || p->filemaps[i].file->off > p->filemaps[i].file->ip->size){
                p->filemaps[i].isused = 0;//释放
                fileclose(p->filemaps[i].file);
                }
                p->filemaps[i].file->off = tmp_off;
                }
                }
                return 0;
                }
                -

                很好,逻辑很简单,就是记录acquire时等待的次数,非常简单粗暴(((

                -

                总的来说这个思路还是挺酷的,而且这个“一切皆文件”的思想再次震撼了我,一个小小的xv6确实能做到那么多。

                +

                exit和fork

                exit
                // 关闭map-file
                for(int i=0;i<NFILEMAP;i++){
                if(p->filemaps[i].isused){
                munmap((void*)(p->filemaps[i].va),p->filemaps[i].length);
                }
                }
                + +
                fork
                for(int i=0;i<NFILEMAP;i++){
                np->filemaps[i].isused = p->filemaps[i].isused;
                np->filemaps[i].va = p->filemaps[i].va;
                np->filemaps[i].okva = p->filemaps[i].okva;
                np->filemaps[i].file = p->filemaps[i].file;
                np->filemaps[i].length = p->filemaps[i].length;
                np->filemaps[i].flags = p->filemaps[i].flags;
                np->filemaps[i].offset = p->filemaps[i].offset;
                np->filemaps[i].prot = p->filemaps[i].prot;
                if(np->filemaps[i].file)
                filedup(np->filemaps[i].file);
                }
                + +

                修改uvmcopy和uvmunmap

                // in uvmunmap()
                if((*pte & PTE_V) == 0){
                *pte = 0;
                continue;
                }
                // in uvmcopy()
                if((*pte & PTE_V) == 0)
                //panic("uvmcopy: page not present");
                continue;
                ]]> - 哈工大操作系统实验 - /2022/10/04/%E5%93%88%E5%B7%A5%E5%A4%A7%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E5%AE%9E%E9%AA%8C/ + xv6 + /2023/01/10/xv6/ -

                实验入口
                主要参考文章
                lseek()函数:用于移动打开文件的指针
                linux系统调用之write源码解析(基于linux0.11)
                get_fs_bytes解析
                VIM与系统剪贴板的复制粘贴
                操作系统实验六 信号量的实现和应用(哈工大李治军)
                哈工大操作系统实验6 信号量的实现 pc.c 编译时报错 对‘sem_open‘未定义的引用
                Linux 文件编程 open函数
                哈工大-操作系统-HitOSlab-李治军-实验5-信号量的实现和应用

                -
                -

                地址映射与共享

                -

                参考文章

                -

                Linux进程间通信(六):共享内存 shmget()、shmat()、shmdt()、shmctl()

                -

                操作系统实验七 地址映射与共享(哈工大李治军)

                +

                总耗时:120h 约27天

                +

                部分地方的翻译和表格来源参考:xv6指导书翻译

                +

                部分文本来自:操作系统实验指导书 - 2023秋季 | 哈工大(深圳)

                +

                实验官网:6.S081

                +

                代码以github为准,此处记录的有些小瑕疵

                +

                笔记的结构【以第一章Operating system interface为例】:

                +

                image-20230124235649128

                -

                必备知识

                要点1 共享内存

                顾名思义,共享内存就是允许两个不相关的进程访问同一个逻辑内存。共享内存是在两个正在运行的进程之间共享和传递数据的一种非常有效的方式。不同进程之间共享的内存通常安排为同一段物理内存。如果某个进程向共享内存写入数据,所做的改动将立即影响到可以访问同一段共享内存的任何其他进程。

                -

                注:共享内存并未提供同步机制,所以我们需要用信号量来实现同步。

                -

                Linux提供了一组接口用于使用共享内存,它们声明在头文件 sys/shm.h 中。

                -

                1.shmget

                程序先通过调用shmget()函数并提供一个键,再由系统生成一个相应的共享内存标识符(shmget()函数的返回值)。

                -
                int shmget(key_t key, size_t size, int shmflg);
                - -

                key为共享内存段名字,size为大小,shmflg是权限标志

                -

                注:

                -

                ① key:非0整数,共享内存段的命名

                -

                ② shmflag:作用与open函数的mode参数一样,比如IPC_CREAT,或连接

                -

                共享内存的权限标志与文件的读写权限一样,举例来说,0644表示允许一个进程创建的共享内存被内存创建者所拥有的进程向共享内存读取和写入数据,同时其他用户创建的进程只能读取共享内存

                -

                ③ return:成功时返回一个与key相关的共享内存标识符(非负整数)。调用失败返回-1

                -

                不相关的进程可以返回值(共享内存标识符)访问同一共享内存

                -

                2.shmat

                第一次创建完共享内存时,它还不能被任何进程访问,需要shmat启动对该共享内存的访问,并把共享内存连接到当前进程的地址空间。

                -
                void *shmat(int shm_id, const void *shm_addr, int shmflg);
                - -

                ① shm_id:共享内存标识符

                -

                ② shm_addr:指定共享内存连接到当前进程中的地址位置,通常为空,表示让系统来选择共享内存的地址

                -

                ③ shm_flg:一组标志位,通常为0

                -

                ④ return:成功时返回一个指向共享内存第一个字节的指针,失败返回-1

                -

                3.shmdt

                用于将共享内存从当前进程中分离,使该共享内存对当前进程不再可用。

                -
                int shmdt(const void *shmaddr);
                - -

                ① shmaddr:shmat返回的共享内存指针

                -

                ② return:成功0,失败1

                -

                4.shmctl

                用来控制共享内存

                -
                int shmctl(int shm_id, int command, struct shmid_ds *buf);
                - -

                ① shm_id:共享内存标识符

                -

                ② command:要采取的操作,它可以取下面的三个值 :

                -
                  -
                • IPC_STAT:把shmid_ds结构中的数据设置为共享内存的当前关联值,即用共享内存的当前关联值覆盖shmid_ds的值。
                • -
                • IPC_SET:如果进程有足够的权限,就把共享内存的当前关联值设置为shmid_ds结构中给出的值
                • -
                • IPC_RMID:删除共享内存段
                • -
                -

                ③ buf:结构指针

                -

                shmid_ds结构 至少包括以下成员:

                -
                struct shmid_ds
                {
                uid_t shm_perm.uid;
                uid_t shm_perm.gid;
                mode_t shm_perm.mode;
                };
                - -

                实验1 在Ubuntu下编写程序“基于共享内存的生产者消费者模型”

                -

                本项实验在 Ubuntu 下完成,与信号量实验中的 pc.c 的功能要求基本一致,仅有两点不同:

                -
                  -
                • 不用文件做缓冲区,而是使用共享内存;
                • -
                • 生产者和消费者分别是不同的程序。生产者是 producer.c,消费者是 consumer.c。两个程序都是单进程的,通过信号量和缓冲区进行通信。
                • -
                -

                Linux 下,可以通过 shmget()shmat() 两个系统调用使用共享内存。

                +

                Operating system interface

                Operating system oganization

                Page tables

                Traps and system calls

                Interrupts and device drivers

                Locking

                Scheduling

                File system

                其他的对实验未涉及的思考

                ]]> + + labs + + + + Scheduling + /2023/01/10/xv6$chap7/ + Scheduling

                Code: Context switching

                xv6中,每个CPU中的scheduler都有一个专用的线程。这里线程的概念是,有自己的栈,有自己的寄存器状态。

                +

                当发生时钟中断时,当前进程调用yield,yield再通过swtch切换到scheduler线程。scheduler线程会通过swtch跳转到另外一个进程去执行。当另外一个进程发生时钟中断,又会通过yield回到scheduler,scheduler再调度原进程继续执行,如此周而复始。

                +
                +

                Linux的调度原理也差不多类似这样。每个CPU都有一个调度类为SCHED_CLASS_IDLE的IDLE进程,IDLE进程体大概就是间歇不断地执行__schedule()函数,CPU空闲时就会不断执行IDLE线程。

                +

                而当有新的任务产生时(或任务被唤醒。可以从此看出task new和task wakeup的共通点,可以联想到竞赛中对该消息的处理方法),它首先通过调度类对应的select_cpu选择一个合适的(可以被抢占&&在该task对应的cpumask中)的cpu,迁移到cpu对应的rq;目标cpu通过IDLE进程体或者中断返回时检查到了NEED_SCHEDULE标记位,从而调用schedule函数pick新任务,然后进行context_switch切换到目标线程。如此周而复始。

                -

                直接上代码。感觉比文件操作简单多了2333

                -

                consumer.c

                -
                #include <stdio.h>
                #include <fcntl.h>
                #include <sys/types.h>
                #include <sys/stat.h>
                #include <string.h>
                #include <sys/shm.h>
                #include <errno.h>
                #include <unistd.h>
                #include <semaphore.h>

                sem_t* empty;
                sem_t* full;
                sem_t* mutex;
                char* p;

                void Consumer(){
                sem_wait(full);
                sem_wait(mutex);
                char s[5]={0};
                //there read s from buffer...
                memcpy(s,p,sizeof(char)*4);
                p+=4;
                printf("%d : %s",getpid(),s);
                sem_post(mutex);
                sem_post(empty);
                }


                int main(){
                empty=sem_open("empty",O_CREAT,0644,10);
                full=sem_open("full",O_CREAT,0644,0);
                mutex=sem_open("mutex",O_CREAT,0644,1);

                int shm_id=shmget(521,sizeof(char)*4*600,0644|IPC_CREAT);
                char* shm=(char*)shmat(shm_id,NULL,0);
                p=shm;

                int cnt=500;
                while(cnt--){
                Consumer();
                }

                shmdt(shm);
                shmctl(shm_id,IPC_RMID,0);
                sem_unlink("mutex");
                sem_unlink("full");
                sem_unlink("empty");
                return 0;
                }
                - -

                producer.c

                -
                #include <stdio.h>
                #include <string.h>
                #include <sys/shm.h>
                #include <errno.h>
                #include <unistd.h>
                #include <semaphore.h>
                #include <fcntl.h>
                #include <sys/types.h>
                #include <sys/stat.h>

                sem_t* empty;
                sem_t* full;
                sem_t* mutex;
                char* p;

                void Producer(){
                int i;
                char s[5]={0};
                for(i=0;i<=500;i++){
                sem_wait(empty);
                sem_wait(mutex);
                sprintf(s,"%03d\n",i);
                //there write s into buffer...
                memcpy(p,s,sizeof(char)*4);
                p+=4;
                printf("Write into success!%s\n",s);
                sem_post(mutex);
                sem_post(full);
                }
                }

                int main(){
                empty=sem_open("empty",O_CREAT,0644,10);
                full=sem_open("full",O_CREAT,0644,0);
                mutex=sem_open("mutex",O_CREAT,0644,1);

                int shm_id=shmget(521,sizeof(char)*4*600,0644|IPC_CREAT);
                char* shm=(char*)shmat(shm_id,NULL,0);
                p=shm;

                Producer();

                shmdt(shm);
                shmctl(shm_id,IPC_RMID,0);
                sem_unlink("mutex");
                sem_unlink("full");
                sem_unlink("empty");
                return 0;
                }
                - -

                编译运行指令

                -
                gcc -o producer producer.c -pthread
                gcc -o consumer consumer.c -pthread
                ./producer > p.txt &
                ./consumer > c.txt
                - -

                运行结果c.txt(仅展示部分)

                -
                27696 : 000
                27696 : 001
                27696 : 002
                27696 : 003
                27696 : 004
                27696 : 005
                27696 : 006
                27696 : 007
                27696 : 008
                27696 : 009
                27696 : 010
                27696 : 011
                27696 : 012
                +

                image-20230118221757367

                +

                下面就来讲讲这个所谓的“线程”以及xv6的上下文切换是怎么实现的。

                +

                context

                上下文切换的操作对象是上下文,因而首先了解一下上下文的结构。各种寄存器的状态即是上下文context。xv6中的context定义如下:

                +
                struct context {
                uint64 ra;
                uint64 sp;

                // callee-saved
                uint64 s0;
                uint64 s1;
                uint64 s2;
                uint64 s3;
                uint64 s4;
                uint64 s5;
                uint64 s6;
                uint64 s7;
                uint64 s8;
                uint64 s9;
                uint64 s10;
                uint64 s11;
                };
                +

                上下文切换需要修改栈和pc,context中确实有sp寄存器,但是没有pc寄存器,这主要还是因为当swtch返回时,会回到ra所指向的地方,所以仅保存ra就足够了。

                +

                swtch

                上下文的切换是通过swtch实现的。

                +
                void            swtch(struct context*, struct context*);
                +

                swtch会把当前进程的上下文保存在第一个context中,再切换到第二个context保存的上下文,具体实现就是写读保存寄存器:

                +
                # in kernel/swtch.S
                # a0和a1分别保存着两个参数的值,也即第一个context的地址和第二个context的地址
                .globl swtch
                swtch:
                sd ra, 0(a0)
                sd sp, 8(a0)
                sd s0, 16(a0)
                sd s1, 24(a0)
                # ...
                sd s11, 104(a0)

                ld ra, 0(a1)
                ld sp, 8(a1)
                ld s0, 16(a1)
                ld s1, 24(a1)
                # ...
                ld 11, 104(a1)

                ret
                -

                实验2 在Linux0.11实现共享内存

                -

                进程之间可以通过页共享进行通信,被共享的页叫做共享内存,结构如下图所示:

                - +

                sched

                在sleep、yield和wakeup中,都会通过sched中的swtch进入scheduler线程。

                +
                void
                sched(void)
                {
                int intena;
                struct proc *p = myproc();

                if(!holding(&p->lock))
                panic("sched p->lock");
                if(mycpu()->noff != 1)
                panic("sched locks");
                if(p->state == RUNNING)
                panic("sched running");
                if(intr_get()) // 当持有锁时一定为关中断状态
                panic("sched interruptible");

                intena = mycpu()->intena;
                swtch(&p->context, &mycpu()->context);
                mycpu()->intena = intena;
                }
                -

                本部分实验内容是在 Linux 0.11 上实现上述页面共享,并将上一部分实现的 producer.c 和 consumer.c 移植过来,验证页面共享的有效性。

                -
                +

                cpu中存储着的是scheduler线程的context。因而,这样就可以保存当前进程的context,读取scheduler线程的context,然后转换到scheduler的context执行了。

                -

                具体要求在 mm/shm.c 中实现 shmget()shmat() 两个系统调用。它们能支持 producer.cconsumer.c 的运行即可,不需要完整地实现 POSIX 所规定的功能。

                -
                  -
                • shmget()
                • -
                -
                int shmget(key_t key, size_t size, int shmflg);
                - -

                shmget() 会新建/打开一页内存,并返回该页共享内存的 shmid(该块共享内存在操作系统内部的 id)。

                -

                所有使用同一块共享内存的进程都要使用相同的 key 参数。

                -

                如果 key 所对应的共享内存已经建立,则直接返回 shmid。如果 size 超过一页内存的大小,返回 -1,并置 errnoEINVAL。如果系统无空闲内存,返回 -1,并置 errnoENOMEM

                -

                shmflg 参数可忽略。

                -
                  -
                • shmat()
                • -
                -
                void *shmat(int shmid, const void *shmaddr, int shmflg);
                - -

                shmat() 会将 shmid 指定的共享页面映射到当前进程的虚拟地址空间中,并将其首地址返回。

                -

                如果 shmid 非法,返回 -1,并置 errnoEINVAL

                -

                shmaddrshmflg 参数可忽略。

                +

                可以发现这里是有个很完美的组合技的。由sched()保存context到process结构体中,再由scheduler()读取process对应的context回归到sched()继续执行,我感觉调度设计这点真是帅的一匹。

                -

                思路:

                -

                1.shmget:由其论述,我们可以知道,我们需要建立一个映射表,其中成员为结构体({key_t key,size_t size,unsigned long page}),每次只需查找映射表,如果有对应key则返回下标,如果没有则新建页表,填入映射体,再返回对应下标。

                -

                以下为了图省事,对映射表的实现进行了简化,把key直接当做int类型,作为映射表下标,映射表成员为page,unsigned long。

                -

                2.shmat:

                -

                首先由指导书的提示:

                -
                // 建立线性地址和物理地址的映射
                put_page(tmp, address);
                - -

                我们知道在shmat中,要建立shmget得到的共享物理页面与其虚拟地址的映射,就需要使用这个put_page函数。

                -

                但是put_page函数的参数为页和address。页就是我们的shm_map[key],address=虚拟地址+段基址。那么如何得到虚拟地址呢?

                -

                通过指导书的提示:

                -
                code_base = get_base(current->ldt[1]);
                data_base = code_base;

                // 数据段基址 = 代码段基址
                set_base(current->ldt[1],code_base);
                set_limit(current->ldt[1],code_limit);
                set_base(current->ldt[2],data_base);
                set_limit(current->ldt[2],data_limit);
                __asm__("pushl $0x17\n\tpop %%fs":: );

                // 从数据段的末尾开始
                data_base += data_limit;

                // 向前处理
                for (i=MAX_ARG_PAGES-1 ; i>=0 ; i--) {
                // 一次处理一页
                data_base -= PAGE_SIZE;
                // 建立线性地址到物理页的映射
                if (page[i]) put_page(page[i],data_base);
                }
                - -

                再结合所学知识,我们可以知道几点:

                -

                ① 数据段的基址可由current->ldt[2]给出 ② address=虚拟地址+段基址 ③ 我们需要分配给当前共享内存一段空闲的虚拟地址段

                -

                则该小段空闲数据段的虚拟地址就是我们的return值,address=return+data_base。

                -

                问题就转化成了如何获取一段空闲数据段。

                -

                我们由下图:

                - +

                scheduler

                void
                scheduler(void)
                {
                struct proc *p;
                struct cpu *c = mycpu();

                c->proc = 0;
                for(;;){
                // Avoid deadlock by ensuring that devices can interrupt.
                intr_on();

                int nproc = 0;
                for(p = proc; p < &proc[NPROC]; p++) {
                acquire(&p->lock);
                if(p->state != UNUSED) {
                nproc++;
                }
                if(p->state == RUNNABLE) {
                // Switch to chosen process. It is the process's job
                // to release its lock and then reacquire it
                // before jumping back to us.
                p->state = RUNNING;
                c->proc = p;
                swtch(&c->context, &p->context);

                // Process is done running for now.
                // It should have changed its p->state before coming back.
                c->proc = 0;
                }
                release(&p->lock);
                }
                if(nproc <= 2) { // only init and sh exist
                intr_on();
                asm volatile("wfi");
                }
                }
                }
                -

                可知,brk指针指向堆区顶部,即空闲堆的起始位置。因而我们可以用这段空间作为我们要的空闲数据段,当前brk即为虚拟地址。

                -

                我们的页有PAGE_SIZE那么大,因而自然也就要用PAGE_SIZE那么大的空闲数据段了。

                -

                解说完毕,以下上代码~

                -
                #include <unistd.h>
                #include <unistd.h>
                #define __LIBRARY__
                #include <linux/kernel.h>
                #include <linux/sched.h>
                #include <linux/mm.h>

                unsigned long shm_map[600];

                int sys_shmget(int key,size_t size,int shmflg){
                if(key<0||key>=600) return -1;
                if(shm_map[key]!=0) return key;

                unsigned long tmp=get_free_page();
                shm_map[key]=tmp;

                return key;
                }

                void* sys_shmat(int shmid,const void* shmaddr,int shmflg){
                if(shmid<0||shmid>=600) return -1;

                unsigned long page=shm_map[shmid];
                //得到数据段的基址
                unsigned long data_base=get_base(current->ldt[2]);
                //brk指针指向空闲数据段的开始
                unsigned long brk=current->brk+data_base;
                current->brk+=PAGE_SIZE;
                //建立内存映射
                put_page(page,brk);
                return (void*)(brk-data_base);
                }
                +

                通过swtch进入scheduler线程后,会继续执行scheduler中swtch的下一个指令,完成下一次调度。

                +

                一些补充

                以上是书本的介绍内容。看到这想必会有很多疑惑,至少有以下两点:

                +
                  +
                1. 为什么cpu->context会存储着scheduler的上下文?这是什么时候,又是怎么初始化的?
                2. +
                3. 为什么从sched中swtch会来到scheduler中swtch的下一句?
                4. +
                +

                先从第一点入手。实际上,这个初始化的工作,是在操作系统启动时的main.c中完成的。

                +
                void
                main()
                {
                if(cpuid() == 0){
                // ...
                } else {
                // ...
                }

                scheduler();
                }
                -

                信号量

                任务一 实现pc.c

                在 Ubuntu 上编写应用程序“pc.c”,解决经典的生产者—消费者问题,完成下面的功能:

                1.建立一个生产者进程,N 个消费者进程(N>1);
                2.用文件建立一个共享缓冲区;
                3.生产者进程依次向缓冲区写入整数 0,1,2,...,M,M>=500;
                4.消费者进程从缓冲区读数,每次读一个,并将读出的数字从缓冲区删除,然后将本进程 ID 和 + 数字输出到标准输出;
                5.缓冲区同时最多只能保存 10 个数。
                其中 ID 的顺序会有较大变化,但冒号后的数字一定是从 0 开始递增加一的。
                +

                在这之前,创建了第一个进程proc。在这里,每个cpu都调用了scheduler。

                +
                void
                scheduler(void)
                {
                struct proc *p;
                struct cpu *c = mycpu();

                c->proc = 0;
                for(;;){
                intr_on();

                int nproc = 0;
                for(p = proc; p < &proc[NPROC]; p++) {
                acquire(&p->lock);
                // ...
                if(p->state == RUNNABLE) {
                p->state = RUNNING;
                c->proc = p;
                swtch(&c->context, &p->context);

                c->proc = 0;
                }
                release(&p->lock);
                }
                // ...
                }
                }
                -

                先附上我的代码吧【注:我没做到从缓冲区删除,但其他都完成了】

                -
                #include <stdio.h>
                #include <errno.h>
                #include <unistd.h>
                #include <sys/types.h>
                #include <sys/stat.h>
                #include <fcntl.h>
                #include <semaphore.h>

                const char* filename="buffer.txt";

                sem_t* empty;
                sem_t* full;
                sem_t* mutex;

                int fd;

                void Producer(){
                int i;
                char s[5]={0};
                for(i=0;i<=500;i++){
                sem_wait(empty);
                sem_wait(mutex);
                int tmp=lseek(fd,0,SEEK_CUR);
                lseek(fd,0,SEEK_END);
                sprintf(s,"%03d\n",i);
                write(fd,s,4);
                lseek(fd,tmp,SEEK_SET);
                sem_post(mutex);
                sem_post(full);
                }
                }

                void Consumer(){
                sem_wait(full);
                sem_wait(mutex);
                char s[5]={0};
                read(fd,s,4);
                printf("%d : %s",getpid(),s);
                sem_post(mutex);
                sem_post(empty);
                }

                int main(){
                sem_unlink("empty");
                sem_unlink("full");
                sem_unlink("mutex");
                fd=open(filename,O_RDWR|O_CREAT);
                printf("%d\n",errno);
                empty=sem_open("empty",O_CREAT,0644,10);
                full=sem_open("full",O_CREAT,0644,0);
                mutex=sem_open("mutex",O_CREAT,0644,1);

                if(!fork()){
                Producer();
                return 0;
                }

                int i;
                for(i=0;i<10;i++){
                if(!fork()){
                while(1) Consumer();
                }
                }
                close(fd);
                return 0;
                }
                +

                每个cpu都在scheduler线程的swtch(&c->context, &p->context);中,将当前的context,也即scheduler的context存入了mycpu()->context。随后,CPU中的某个去执行下一个进程,其他的就在scheduler线程的无限循环中等待,直到有别的进程产生。

                +

                去执行进程的CPU通过swtch切换上下文,切到了另一个进程中,此时在swtch中保存的ra是scheduler线程的swtch的下一句(因为scheduler->swtch也是个函数调用的过程)。会切到另一个进程的sched的下一句【因为它正是从那边swtch过来的】,或者是那个进程开始执行的地方【下面会提到是forkret】。另一个进程通过sched切换回来的时候,就正会切到ra所指向的位置,也即切到scheduler中的swtch后面。

                +

                这样一来,两个问题就都得到了解答。

                +

                从这,我们也能知道xv6是如何让CPU运转的:scheduler线程是CPU的IDLE状态。无事的时候在scheduler中等待,并且一直监测是否有进程需要执行。有的话,则去执行该进程;该进程又会通过sched切换回scheduler线程,继续等待。这样一来,就完成了进程管理的基本的自动机图像。

                +

                Code: Scheduling

                sched前要做的事

                +

                A process that wants to give up the CPU must do three things:

                +
                  +
                1. acquire its own process lock p->lock, release any other locks it is holding
                2. +
                3. update its own state (p->state)
                4. +
                5. call sched
                6. +
                +

                yield (kernel/proc.c:515) follows this convention, as do sleep and exit.

                +

                sched double-checks those conditions (kernel/proc.c:499-504) and then an implication of those conditions: since a lock is held, interrupts should be disabled.

                +
                +

                sched与scheduler

                在上面的描述我们可以看到,schedscheduler联系非常密切,他们俩通过swtch相互切来切去,并且一直都只在这几行切来切去:

                +
                // in scheduler()
                swtch(&c->context, &p->context);
                c->proc = 0;
                // in sched()
                swtch(&p->context, &mycpu()->context);
                mycpu()->intena = intena;
                -

                运行效果:

                - +

                在两个线程之间进行这种样式化切换的过程有时被称为协程(coroutines)。

                +
                +

                存在一种情况使得调度程序对swtch的调用没有以sched结束。一个新进程第一次被调度时,它从forkretkernel/proc.c:527)开始。Forkret是为了释放p->lock而包装的,要不然,新进程可以从usertrapret开始。

                +
                +

                p->lock保证了并发安全性

                +

                考虑调度代码结构的一种方法是,它为每个进程强制维持一个不变性条件的集合,并在这些不变性条件不成立时持有p->lock

                +

                其中一个不变性条件是:如果进程是RUNNING状态,计时器中断的yield必须能够安全地从进程中切换出去;这意味着CPU寄存器必须保存进程的寄存器值(即swtch没有将它们移动到context中),并且c->proc必须指向进程。另一个不变性条件是:如果进程是RUNNABLE状态,空闲CPU的调度程序必须安全地运行它;这意味着p->context必须保存进程的寄存器(即,它们实际上不在实际寄存器中),没有CPU在进程的内核栈上执行,并且没有CPU的c->proc引用进程。

                +

                维护上述不变性条件是xv6经常在一个线程中获取p->lock并在另一个线程中释放它的原因,在保持p->lock时,这些属性通常不成立。

                +

                例如在yield中获取并在scheduler中释放。一旦yield开始修改一个RUNNING进程的状态为RUNNABLE,锁必须保持被持有状态,直到不变量恢复:最早的正确释放点是scheduler(在其自身栈上运行)清除c->proc之后。类似地,一旦scheduler开始将RUNNABLE进程转换为RUNNING,在内核线程完全运行之前(在swtch之后,例如在yield中)绝不能释放锁。

                +

                p->lock还保护其他东西:exitwait之间的相互作用,避免丢失wakeup的机制(参见第7.5节),以及避免一个进程退出和其他进程读写其状态之间的争用(例如,exit系统调用查看p->pid并设置p->killed(kernel/proc.c:611))。为了清晰起见,也许为了性能起见,有必要考虑一下p->lock的不同功能是否可以拆分。

                +
                +

                p->lock在每次scheduler开始的时候获取,swtch到p进程的时候在yield等调用完sched的地方释放。而调用yield时获取的锁,又会在scheduler中释放。

                +
                // Give up the CPU for one scheduling round.
                void
                yield(void)
                {
                struct proc *p = myproc();
                acquire(&p->lock);// 该锁会在scheduler中释放
                p->state = RUNNABLE;
                sched();
                release(&p->lock);// 该锁释放的是scheduler中得到的锁
                }
                -

                要点1 系统调用的IO读写

                这部分耗费了我海量时间,主要原因还是因为我没有好好学就直接上手写导致很多地方都因为不清楚而寄了。。。

                -

                先大致讲讲文件读写的原理吧。打开一个文件作为数据流,有一个文件指针,该指针指向的地方就是之后读写开始的地方,读写还有lseek都可以让指针移动。

                -

                再放个各个系统调用的签名。

                -
                @param 文件名 模式

                @return 所需文件描述符

                int open(char* filename,int flag);
                +
                // in kernel/proc.c scheduler()
                acquire(&p->lock);// 该锁会在yield等地被释放
                // ...
                swtch(&c->context, &p->context);
                // ...
                release(&p->lock);// 该锁会释放yield等地中获得的锁
                -

                其中flag的可能取值:

                - +

                不得不说,这结构实在是太精妙了。这中间的如此多的复杂过程,就这样成功地被锁保护了起来。

                +

                Code: mycpu and myproc

                // Per-CPU state.
                struct cpu {
                struct proc *proc; // The process running on this cpu, or null.
                struct context context; // swtch() here to enter scheduler().
                int noff; // Depth of push_off() nesting.
                int intena; // Were interrupts enabled before push_off()?
                };
                - +

                mycpu是通过获取当前cpuid来获取cpu结构的。当前使用的cpuid约定俗成地存在了tp寄存器里。为了让mycpu有效工作,必须确保tp寄存器始终存放的是当前cpu的hartid。

                +

                首先是在操作系统初始化的时候要把cpuid存入tp寄存器。RISC-V规定,mhartid也即cpuid的存放点只能在machine mode被读取。因而这项工作得在start.c中完成:

                +
                // in kernel/start.c 
                // keep each CPU's hartid in its tp register, for cpuid().
                int id = r_mhartid();
                w_tp(id);
                // in kernel/riscv.h
                // which hart (core) is this?
                static inline uint64
                r_mhartid()
                {
                uint64 x;
                asm volatile("csrr %0, mhartid" : "=r" (x) );
                return x;
                }
                -

                如果想要多个方式并行,则可以用|连接。【联系一下原理,这大概是用了标志位吧,每个标志只有一位是1】

                -

                这部分踩过的坑:

                -

                ① 选择O_CREAT,如果文件已经存在,居然是会报错?【表现为errno=13,还会输出一堆奇怪的东西】

                -
                @param 文件描述符  写入字符串  写入长度
                @return 报错信息
                int read(int fd,char* string,size_t size);
                +

                在内核态中,编译器被设置为保证不会以其他方式使用tp寄存器。因而初始化之后,内核态中每个CPU的tp寄存器就始终存放着自己的cpuid。

                +

                但这在用户进程是不成立的。因而必须在用户进程进入陷阱的时候做一些工作。

                +
                # in kernel/trampoline.S uservec
                sd tp, 64(a0)
                # make tp hold the current hartid, from p->trapframe->kernel_hartid
                ld tp, 32(a0)
                -

                read会读出size个字节然后存进string里面,同时也会移动文件指针向前size个字节。

                -
                @param 文件描述符  写入字符串  写入长度
                @return 报错信息
                int write(int fd,char* string,size_t size);
                +
                struct trapframe {
                /* 32 */ uint64 kernel_hartid; // saved kernel tp
                /* 64 */ uint64 tp;
                // ...
                }
                -

                基本同write。

                -

                这部分踩过的坑:

                -

                write(fd,NULL,0) ——合法

                -

                write(fd,NULL,a),a>0 ——寄!

                -

                这还是因为write的具体实现了。

                -

                write里面有个判断

                -
                int sys_write(unsigned int fd,char *buf,int count){
                //...
                if(!count) return 0;
                //...
                while(c-->0) *(p++)=get_fs_byte(buf++);
                }
                +

                必须在trampoline保存用户态中使用的tp值,以及内核态中对应的hartid。

                +

                最后再在返回用户态的时候恢复用户态的tp值以及更新trampoline的tp值。

                +
                // in kernel/trap.c usertrapret()
                p->trapframe->kernel_hartid = r_tp(); // hartid for cpuid()
                -

                而get_fs_byte:

                - +
                # in trampoline.S userret
                ld tp, 64(a0)
                -

                确实感觉空的话挺危险的【】

                -
                @param 文件描述符
                @return 报错信息
                int close(fd);
                +

                注意,更新trampoline的tp值这一步很重要。因为如果在用户态发生的是时钟中断,就会引起yield,可能造成CPU的切换。这时候就需要在返回用户态的时候修改一下trapframe中的tp为当前CPU的tp。这样一来才能保证,在本次时钟中断结束,以及到下一次时钟中断修改CPU这一期间,trapframe中的tp寄存器以及内核态中的tp寄存器都是正确的。

                +

                通过mycpu()获取cpuid其实是非常脆弱的。因为你可能获取完cpuid,进程就被切到别的CPU去执行了,这就会有一个先检查后执行的竞态条件,具有并发安全隐患。因而,xv6要求使用mycpu()返回值的这段代码需要关中断,这样就可以避免时钟中断造成的进程切换了。比如说像myproc()这样:

                +
                // Return the current struct proc *, or zero if none.
                struct proc*
                myproc(void) {
                push_off();
                struct cpu *c = mycpu();
                struct proc *p = c->proc;
                pop_off();
                return p;
                }
                -

                这个没啥好说的,记得关就是了

                -

                要点2 信号量的调用

                这方面看linux自带的man文档就行,写得很清楚。

                -

                输入指令:

                -
                man sem_overview
                +

                注意,不同于mycpu(),使用myproc()的返回值不需要进行开关中断保护。因为当前进程的指针不论处于哪个CPU都是不变的。

                +

                Sleep and wakeup

                前面我们已经介绍了进程隔离性的基本图像,接下来要讲xv6是如何让进程之间互动的。xv6使用的是经典的sleep and wakeup,也叫序列协调(sequence coordination)条件同步机制(conditional synchronization mechanisms。下面,将从最基本的自旋锁实现信号量开始,来逐步讲解xv6的sleep and wakeup机制。

                +

                自旋锁实现信号量

                image-20230120150659730

                +

                image-20230120150715925

                +

                缺点就是自旋太久了,因而我们需要在等待的时候调用yield,直到资源生产出来之后再继续执行。

                +

                不安全的sleep and wakeup

                +

                Let’s imagine a pair of calls, sleep and wakeup, that work as follows:

                +
                  +
                1. sleep(chan)

                  +

                  Sleeps on the arbitrary value chan, called the wait channel. Sleep puts the calling process to sleep, releasing the CPU for other work.

                  +
                2. +
                3. wakeup(chan)

                  +

                  Wakes all processes sleeping on chan (if any), causing their sleep calls to return. If no processes are waiting on chan, wakeup does nothing.

                  +
                4. +
                +
                +

                这样一来,信号量实现就可修改为这样了:

                +

                image-20230120151051989

                +

                但是,我们可以注意到,在212-213行这里产生了一个先检查后执行的竞态条件。

                +
                +

                如果消费者进程执行到212-213中间,此时生产者进程已经调用结束,也就是说wakeup并没有唤醒任何消费者进程。消费者进程就会一直在sleep中没人唤醒,除非生产者进程再执行一次。这样就会造成lost wake-up 这个问题。

                +
                +

                所以,我们可以选择把这个竞态条件也放入s->lock这个锁区域保护。

                +

                image-20230120151353712

                +

                但是这样一来又会产生死锁问题。因而,我们可以尝试着修改sleep和wakeup的接口定义。

                +

                sleep and wakeup

                +

                We’ll fix the preceding scheme by changing sleep’s interface:

                +

                The caller must pass the condition lock to sleep so it can release the lock after the calling process is marked as asleep and waiting on the sleep channel. The lock will force a concurrent V to wait until P has finished putting itself to sleep, so that the wakeup will find the sleeping consumer and wake it up. Once the consumer is awake again sleep reacquires the lock before returning.

                +

                也即在sleep中:

                +
                sleep(s,&s->lock){
                // do something
                release(&s->lock);
                //wait until wakeup
                acquire(&s->lock);
                return;
                }
                +
                +

                这样一来,信号量就可以完美实现了:

                +

                image-20230120151807102

                +

                image-20230120151820455

                +
                +

                注:严格地说,wakeup只需跟在acquire之后就足够了(也就是说,可以在release之后调用wakeup

                +

                【想了一下,有一说一确实,放在release前后都不影响】

                +
                +
                +

                原始Unix内核的sleep只是禁用了中断,这就足够了,因为Unix运行在单CPU系统上。因为xv6在多处理器上运行,所以它为sleep添加了一个显式锁。

                +
                +

                Code: Sleep and wakeup

                // Atomically release lock and sleep on chan.
                // Reacquires lock when awakened.
                void
                sleep(void *chan, struct spinlock *lk)
                {
                struct proc *p = myproc();

                // Must acquire p->lock in order to
                // change p->state and then call sched.
                // Once we hold p->lock, we can be
                // guaranteed that we won't miss any wakeup
                // (wakeup locks p->lock),
                // so it's okay to release lk.
                if(lk != &p->lock){ //DOC: sleeplock0
                // 获取进程锁,释放外部锁
                // 此进程锁将在scheduler线程中释放
                acquire(&p->lock); //DOC: sleeplock1
                release(lk);
                }

                // Go to sleep.
                p->chan = chan;
                p->state = SLEEPING;

                sched();
                // 到这里来,说明已经被wakeup且被调度了

                // Tidy up.
                p->chan = 0;

                // Reacquire original lock.
                if(lk != &p->lock){
                //释放进程锁,获取外部锁
                // 此进程锁是在scheduler中获取到的
                release(&p->lock);
                acquire(lk);
                }
                }
                -

                这部分踩过的坑:

                -

                千万注意最后不使用信号量时要释放,使用sem_unlink。不然最后的输出结果会非常诡异。

                -

                要点3 编写程序

                以上差不多就是涉及到的需要自己了解的课外知识点了,接下来就需要自己编写程序。

                -

                总体框架就按它给的差不多:

                -
                Producer()
                {
                // 空闲缓存资源
                P(Empty);

                // 互斥信号量
                P(Mutex);

                //生产并放一个item进缓冲区

                V(Mutex);
                V(Full);
                }

                Consumer()
                {
                P(Full);
                P(Mutex);

                //从缓存区取出一个赋值给item并消费;

                V(Mutex);
                V(Empty);
                }
                +

                注意,如果lk为p->lock,那么lk依然会在scheduler线程中被暂时释放

                +
                // Wake up all processes sleeping on chan.
                // Must be called without any p->lock.
                void
                wakeup(void *chan)
                {
                struct proc *p;

                for(p = proc; p < &proc[NPROC]; p++) {
                acquire(&p->lock);
                if(p->state == SLEEPING && p->chan == chan) {
                p->state = RUNNABLE;
                }
                release(&p->lock);
                }
                }
                -

                有个点挺有趣的,就是它实际上把文件指针也看成一种资源了,因此也需要在同步段对其进行更新。

                -

                printf的stdout也是资源。

                -

                故以上两者都只能在锁内同步段进行更新。

                -

                main函数就照本宣科地用fork建立子进程就行。

                -

                任务二 自己实现信号量

                Linux 在 0.11 版还没有实现信号量,Linus 把这件富有挑战的工作留给了你。如果能实现一套山寨版的完全符合 POSIX 规范的信号量,无疑是很有成就感的。但时间暂时不允许我们这么做,所以先弄一套缩水版的类 POSIX 信号量,它的函数原型和标准并不完全相同,而且只包含如下系统调用:
                sem_t *sem_open(const char *name, unsigned int value);
                int sem_wait(sem_t *sem);
                int sem_post(sem_t *sem);
                int sem_unlink(const char *name);

                sem_open() 的功能是创建一个信号量,或打开一个已经存在的信号量。
                sem_t 是信号量类型,根据实现的需要自定义。
                name 是信号量的名字。不同的进程可以通过提供同样的 name 而共享同一个信号量。如果该信号量不存在,就创建新的名为 name 的信号量;如果存在,就打开已经存在的名为 name 的信号量。
                value 是信号量的初值,仅当新建信号量时,此参数才有效,其余情况下它被忽略。当成功时,返回值是该信号量的唯一标识(比如,在内核的地址、ID 等),由另两个系统调用使用。如失败,返回值是 NULL。
                sem_wait() 就是信号量的 P 原子操作。如果继续运行的条件不满足,则令调用进程等待在信号量 sem 上。返回 0 表示成功,返回 -1 表示失败。
                sem_post() 就是信号量的 V 原子操作。如果有等待 sem 的进程,它会唤醒其中的一个。返回 0 表示成功,返回 -1 表示失败。
                sem_unlink() 的功能是删除名为 name 的信号量。返回 0 表示成功,返回 -1 表示失败。
                在 kernel 目录下新建 sem.c 文件实现如上功能。然后将 pc.c 从 Ubuntu 移植到 0.11 下,测试自己实现的信号量。
                +

                可以注意到,关于chan这一变量的取值是非常任意的,仅需取一个约定俗成的值就OK。这里取为了信号量的地址,同时满足了逻辑需求和语义需求。

                +
                +

                Callers of sleep and wakeup can use any mutually convenient number as the channel. Xv6 often uses the address of a kernel data structure involved in the waiting.

                +
                +

                这里也解释了为什么需要while循环

                +
                +

                有时,多个进程在同一个通道上睡眠;例如,多个进程读取同一个管道。一个单独的wakeup调用就能把他们全部唤醒。其中一个将首先运行并获取与sleep一同调用的锁,并且(在管道例子中)读取在管道中等待的任何数据。尽管被唤醒,其他进程将发现没有要读取的数据。从他们的角度来看,醒来是“虚假的”,他们必须再次睡眠。因此,在检查条件的循环中总是调用sleep

                +
                +

                Code: Pipes

                pipes很显然就是生产者消费者模式的一个例证。

                +
                struct pipe {
                struct spinlock lock;
                char data[PIPESIZE];
                uint nread; // number of bytes read
                uint nwrite; // number of bytes written
                int readopen; // read fd is still open
                int writeopen; // write fd is still open
                };
                -

                由于不小心写完的实验代码被销毁了,因此差不多参考的是这篇文章【戳这里】,修改了一些地方,构成了我的回忆版代码。

                -

                要点1 系统调用修改

                详见文章,写得很清楚。

                -

                要点2 sem.c文件的编写

                sem_t定义

                -
                /* 定义的信号量数据结构: */
                # ifndef _SEM_H_
                # define _SEM_H_

                #include<linux/sched.h>

                typedef struct semaphore_t
                {
                char name[20];/* 信号量的名称 */
                int value; /* 信号量的值 */
                int active;//我自己加的,是对象池思想,感觉写得还挺好的2333
                struct tast_struct *queue;/* 指向阻塞队列的指针 */
                } sem_t;

                #endif
                +
                int
                piperead(struct pipe *pi, uint64 addr, int n)
                {
                int i;
                struct proc *pr = myproc();
                char ch;

                acquire(&pi->lock);
                while(pi->nread == pi->nwrite && pi->writeopen){ //DOC: pipe-empty并且依然有进程在写
                if(pr->killed){
                release(&pi->lock);
                return -1;
                }
                // 等待直到pipe不为空
                sleep(&pi->nread, &pi->lock); //DOC: piperead-sleep
                }
                for(i = 0; i < n; i++){ //DOC: piperead-copy
                if(pi->nread == pi->nwrite)
                break;
                ch = pi->data[pi->nread++ % PIPESIZE];
                if(copyout(pr->pagetable, addr + i, &ch, 1) == -1)
                break;
                }
                // 唤醒写入管道的进程
                wakeup(&pi->nwrite); //DOC: piperead-wakeup
                release(&pi->lock);
                return i;
                }
                -
                #include <unistd.h> 
                #include <linux/sem.h>
                #include <asm/segment.h>
                #include <asm/system.h>

                #define SEM_LIST_LENGTH 50

                //信号量表
                sem_t sem_list[SEM_LIST_LENGTH];

                int str_cmp(const char* s1,const char* s2){
                char* p=s1;
                int i,len1=0,len2=0;
                while(*p!='\0'){
                p++;
                len1++;
                }
                p=s2;
                while(*p!='\0'){
                p++;
                len2++;
                }
                if(len1!=len2) return 1;
                for(i=0;i<len1;i++){
                if(s1[i]!=s2[i]) return 1;
                }
                return 0;
                }
                +
                int
                pipewrite(struct pipe *pi, uint64 addr, int n)
                {
                int i;
                char ch;
                struct proc *pr = myproc();

                acquire(&pi->lock);
                for(i = 0; i < n; i++){
                while(pi->nwrite == pi->nread + PIPESIZE){ //DOC: pipewrite-full管道满则阻塞
                if(pi->readopen == 0 || pr->killed){
                release(&pi->lock);
                return -1;
                }
                // 唤醒读取管道的进程
                wakeup(&pi->nread);
                sleep(&pi->nwrite, &pi->lock);
                }
                if(copyin(pr->pagetable, &ch, addr + i, 1) == -1)
                break;
                pi->data[pi->nwrite++ % PIPESIZE] = ch;
                }
                wakeup(&pi->nread);
                release(&pi->lock);
                return i;
                }
                -

                sem_open

                -
                /*
                sem_open()的功能是创建一个信号量,或打开一个已经存在的信号量。
                */

                sem_t *sys_sem_open(const char * name,unsigned int value)
                {
                if (name == NULL)
                {
                errno = 1;
                return NULL;
                }
                /* 首先将信号量的名称赋值到新建的缓冲区中 */
                char nbuf[20];
                int i;
                for(i = 0; i< 20; i++)
                {
                nbuf[i] = get_fs_byte(name+i);
                }

                /* 然后开始遍历已有的信号量数组,如果有该名字的信号量,直接返回信号量的地址 */
                for(i = 0; i < SEM_LIST_LENGTH; i++)
                {
                if(sem_list[i].active==1&&!str_cmp(sem_list[i].name, nbuf))
                {
                return &sem_list[i];
                }
                }
                /* 如果找不到信号量,就开始新建一个名字为name的信号量 */
                for(i = 0; i < SEM_LIST_LENGTH; i++)
                {
                if(sem_list[i].active==0)
                {
                strcpy(sem_list[i].name, nbuf);
                sem_list[i].value = value;
                sem_list[i].active=1;
                sem_list[i].queue = NULL;
                return &sem_list[i];
                }
                }

                //表已满
                errno = 1;
                return NULL;
                }

                +

                一个非常有意思且巧妙的点,就是读写管道等待在不同的chan上,这与上面信号量的例子是不一样的。想想也确实,如果使用同一个管道的话,当唤醒的时候,就会把不论是读还是写的全部进程都唤醒过来,这对性能显然损失较大。

                +
                +

                The pipe code uses separate sleep channels for reader and writer (pi->nread and pi->nwrite); this might make the system more effificient in the unlikely event that there are lots of readers and writers waiting for the same pipe.

                +
                +

                Code: Wait, exit, and kill

                exit和wait

                +

                Sleepwakeup可用于多种等待。第一章介绍的一个有趣的例子是子进程exit和父进程wait之间的交互。

                +

                xv6记录子进程终止直到wait观察到它的方式是让exit将调用方置于ZOMBIE状态,在那里它一直保持到父进程的wait注意到它,将子进程的状态更改为UNUSED,复制子进程的exit状态码,释放子进程,并将子进程ID返回给父进程。

                +

                如果父进程在子进程之前退出,则父进程将子进程交给init进程,init进程将永久调用wait;因此,每个子进程退出后都有一个父进程进行清理。

                +
                +

                又是一个生产者消费者模式。只不过此时的chan是父进程,资源是僵尸子进程【草】。由于涉及到进程间的调度切换,因而实现稍微复杂了点。

                +

                为什么需要涉及到进程间的调度呢?子进程设置完僵尸状态后,直接通过函数ret不行吗?答案是不行,因为ret的话就会去到不知道哪的地方【大概率会变成scause=2的情况】,所以这里子进程想要退出,就得做几件事,一是依靠父进程,让父进程杀死子进程,二是把自己设置为一个特殊的状态,使得自己不会被调度从而执行ret指令出错,三是尽快让父进程杀死自己越快越好。综合上述三个原因,exit最终在调度方面的实现方式,就变成了,子进程设置自己为ZOMBIE态->启用调度->父进程杀死ZOMBIE态的子进程。这期间不变性条件的防护,就得依赖于锁,以及sleep和wakeup了。

                +
                void
                exit(int status)
                {
                struct proc *p = myproc();

                // ...

                // we need the parent's lock in order to wake it up from wait().
                // the parent-then-child rule says we have to lock it first.
                // 整个xv6都必须遵守相同的顺序(父级,然后是子级)不论是锁定还是释放,都是先父再子
                acquire(&original_parent->lock);
                acquire(&p->lock);

                // Give any children to init.
                // 把自己的所有孩子都托付给init进程
                // init进程就是在操作系统启动时
                reparent(p);

                // Parent might be sleeping in wait().
                // 唤醒wait中的父进程
                // 这里看上去很诡异,明明子进程状态还未完全,怎么就唤醒父亲了呢?但其实很安全。
                // 此时子进程仍持有父进程的锁,如果有别的CPU中断进入scheduler线程,到父进程那时会卡在aquire
                // 直到子进程完成后续工作后父进程才能被真正唤醒执行
                wakeup1(original_parent);

                p->xstate = status;
                // 设为ZOMBIE态
                p->state = ZOMBIE;

                // 完成后续工作,解除父进程的锁
                release(&original_parent->lock);

                // Jump into the scheduler, never to return.
                // 子进程会在父进程中被释放,所以永远不会回来
                sched();
                panic("zombie exit");
                }
                -

                sem_wait

                -
                /*
                sem_wait()就是信号量的P原子操作。
                如果继续运行的条件不满足,则令调用进程等待在信号量sem上。
                返回0表示成功,返回-1表示失败。
                */
                int sys_sem_wait(sem_t * sem)
                {
                /* 判断:如果传入的信号量是无效信号量,P操作失败,返回-1 */
                if(sem == NULL || sem < sem_list || sem > sem_list + SEM_LIST_LENGTH)
                {
                errno=1;
                return -1;
                }
                /* 关中断 */
                cli();
                while(sem->value < 0)
                {
                sleep_on(&(sem->queue));
                }
                sem->value--;
                /* 开中断 */
                sti();
                return 0;
                }
                +
                int
                wait(uint64 addr)
                {
                struct proc *np;
                int havekids, pid;
                struct proc *p = myproc();

                // hold p->lock for the whole time to avoid lost
                // wakeups from a child's exit().
                acquire(&p->lock);

                for(;;){
                // Scan through table looking for exited children.
                havekids = 0;
                for(np = proc; np < &proc[NPROC]; np++){
                // this code uses np->parent without holding np->lock.
                // acquiring the lock first would cause a deadlock,
                // since np might be an ancestor, and we already hold p->lock.
                // 下面的第一点其实一句话就可以搞定:
                // 【它违反了先获取父亲锁,再获取子锁的xv6代码规定】
                // 1.要是在这句话之前acquire的话,acquire到你爸,你爸这时候也刚好执行到这句话
                // 那么就会造成你在自旋【此时你爸在wait一开始就得到了锁】,
                // 你爸也在自旋【你也在wait一开始得到了锁】,这样就造成了死锁
                // 2.并且由于np->parent只有parent才能改,所以数据是否过时是没关系的
                // 因为如果不是你儿子,数据过时与否都知道不是你儿子
                // 如果是你儿子,那数据压根就不会过时
                if(np->parent == p){
                // np->parent can't change between the check and the acquire()
                // because only the parent changes it, and we're the parent.
                acquire(&np->lock);
                havekids = 1;
                if(np->state == ZOMBIE){
                // Found one.
                pid = np->pid;
                // 传递返回参数
                if(addr != 0 && copyout(p->pagetable, addr, (char *)&np->xstate,
                sizeof(np->xstate)) < 0) {
                release(&np->lock);
                release(&p->lock);
                return -1;
                }
                freeproc(np);
                release(&np->lock);
                release(&p->lock);
                return pid;
                }
                release(&np->lock);
                }
                }

                // No point waiting if we don't have any children.
                if(!havekids || p->killed){
                release(&p->lock);
                return -1;
                }

                // Wait for a child to exit.
                // 暂时释放p锁,等待子进程获取退出
                sleep(p, &p->lock); //DOC: wait-sleep
                }
                }
                -

                sem_post

                -
                /*
                sem_post()就是信号量的V原子操作。
                如果有等待sem的进程,它会唤醒其中的一个。
                返回0表示成功,返回-1表示失败。
                */
                int sys_sem_post(sem_t * sem)
                {
                /* 判断:如果传入的信号量是无效信号量,V操作失败,返回-1 */
                if(sem == NULL || sem < sem_list || sem > sem_list + SEM_LIST_LENGTH)
                {
                return -1;
                }
                /* 关中断 */
                cli();
                sem->value++;
                /* 如果有等待sem的进程,它会唤醒其中的一个。 */
                if(sem->value <= 0)
                {
                wake_up(&(sem->queue));
                }
                /* 开中断 */
                sti();
                return 0;
                }
                +

                其中值得注意的几个点:

                +
                  +
                1. wait中的sleep中释放的条件锁是等待进程的p->lock,这是上面提到的特例。

                  +
                2. +
                3. exit会将自己的所有子进程交付给一直在等待着的init进程:

                  +
                  for(;;){
                  printf("init: starting sh\n");
                  pid = fork();
                  // ...
                  for(;;){
                  // this call to wait() returns if the shell exits,
                  // or if a parentless process exits.
                  wpid = wait((int *) 0);
                  if(wpid == pid){
                  // the shell exited; restart it.
                  break;
                  } else if(wpid < 0){
                  printf("init: wait returned an error\n");
                  exit(1);
                  } else {
                  // 这里!!
                  // it was a parentless process; do nothing.
                  }
                  }
                  }
                  -

                  sem_unlink

                  -
                  /*
                  sem_unlink()的功能是删除名为name的信号量。
                  返回0表示成功,返回-1表示失败。
                  */
                  int sys_sem_unlink(const char *name)
                  {
                  if (name == NULL){
                  errno = 1;
                  return -1;
                  }
                  /* 首先将信号量的名称赋值到新建的缓冲区中 */
                  char nbuf[20];
                  int i;
                  for (i = 0; i < 20; i++)
                  {
                  nbuf[i] = get_fs_byte(name + i);
                  if (nbuf[i] == '\0')
                  break;
                  }
                  for (i = 0; i < SEM_LIST_LENGTH; i++)
                  {
                  if (str_cmp(sem_list[i].name, nbuf)==0)
                  {
                  sem_list[i].active=0;
                  return 0;
                  }
                  }
                  return -1;
                  }
                  +

                  如果子进程退出,就会通过init的wait释放它们。然后init释放完它们后进入第三个if分支,继续进行循环。

                  +
                4. +
                5. wakeup1

                  +
                  +

                  Exit calls a specialized wakeup function, wakeup1, that wakes up only the parent, and only if it is sleeping in wait.

                  +
                  +
                  // Wake up p if it is sleeping in wait(); used by exit().
                  // Caller must hold p->lock.
                  static void
                  wakeup1(struct proc *p)
                  {
                  if(!holding(&p->lock))
                  panic("wakeup1");
                  if(p->chan == p && p->state == SLEEPING) {
                  p->state = RUNNABLE;
                  }
                  }
                6. +
                +

                kill

                kill其实做得很温和。它只是会把想鲨的进程的p->killed设置为1,然后如果该进程sleeping,则唤醒它。最后的死亡以及销毁由进程自己来做。

                +
                // Kill the process with the given pid.
                // The victim won't exit until it tries to go
                // to kernel space (see usertrap() in trap.c).
                int
                kill(int pid)
                {
                struct proc *p;

                for(p = proc; p < &proc[NPROC]; p++){
                acquire(&p->lock);
                if(p->pid == pid){
                p->killed = 1;
                if(p->state == SLEEPING){
                // Wake process from sleep().
                p->state = RUNNABLE;
                }
                release(&p->lock);
                return 0;
                }
                release(&p->lock);
                }
                return -1;
                }
                // in trap.c usertrap()
                if(p->killed)
                exit(-1);
                +

                可能这里有一个疑问:调用完exit后,进程会变成ZOMBIE态。谁最终把它释放了呢?其实答案很简单,只有两种:init进程或者是创建它的父进程。

                +

                如果创建它的父进程处于wait中,那么是由父进程把它销毁的,这没什么好说的。但如果创建它的父进程不在wait呢?那么父进程最后也是会调用exit的。父进程调用完exit后,会将其所有子进程过继给init进程。所以,ZOMBIE进程最终还是会迟早被init进程杀死的。

                +

                由这里,可以窥见xv6进程管理的进一步的冰山一角:

                +

                init进程是所有进程的根系进程。它一直处于wait的死循环中,因而可以将需要被杀死的进程杀死。

                +

                可见,wait和exit,实际上就构筑了进程的生命周期的最后一环。

                +

                这种巧妙地将进程生命周期这个大事完全托付给了wait和exit这两个函数的这种结构,实在是非常精妙,太牛了吧。

                +
                +

                一些XV6的sleep循环不检查p->killed,因为代码在应该是原子操作的多步系统调用的中间。virtio驱动程序(*kernel/virtio_disk.c*:242)就是一个例子:它不检查p->killed,因为一个磁盘操作可能是文件系统保持正确状态所需的一组写入操作之一。等待磁盘I/O时被杀死的进程将不会退出,直到它完成当前系统调用并且usertrap看到killed标志

                +
                +
                +

                Xv6对kill的支持并不完全令人满意:有一些sleep循环可能应该检查p->killed。一个相关的问题是,即使对于检查p->killedsleep循环,sleepkill之间也存在竞争;后者可能会设置p->killed,并试图在受害者的循环检查p->killed之后但在调用sleep之前尝试唤醒受害者。如果出现此问题,受害者将不会注意到p->killed,直到其等待的条件发生。这可能比正常情况要晚一点(例如,当virtio驱动程序返回受害者正在等待的磁盘块时)或永远不会发生(例如,如果受害者正在等待来自控制台的输入,但用户没有键入任何输入)。

                +
                +

                是的,所以这个kill的实现其实是相当玄学的。

                +

                Real world

                +

                xv6调度器实现了一个简单的调度策略:它依次运行每个进程。这一策略被称为轮询调度(round robin)。真实的操作系统实施更复杂的策略,例如,允许进程具有优先级。

                +
                +

                我记得linux0.11用的是时间片轮转+优先级队列完美融合的方法,是真的很牛逼

                +
                +

                复杂的策略可能会导致意外的交互,例如优先级反转(priority inversion)和航队(convoys)。当低优先级进程和高优先级进程共享一个锁时,可能会发生优先级反转,当低优先级进程持有该锁时,可能会阻止高优先级进程前进。当许多高优先级进程正在等待一个获得共享锁的低优先级进程时,可能会形成一个长的等待进程航队;一旦航队形成,它可以持续很长时间。为了避免此类问题,在复杂的调度器中需要额外的机制。

                +
                +
                +

                wakeup中扫描整个进程列表以查找具有匹配chan的进程效率低下。一个更好的解决方案是用一个数据结构替换sleepwakeup中的chan,该数据结构包含在该结构上休眠的进程列表,例如Linux的等待队列。

                +
                +

                是的,linux的那个wakeup真的很牛,我现在都还记得当初学到那的时候的震撼。

                +
                +

                wakeup的实现会唤醒在特定通道上等待的所有进程,可能有许多进程在等待该特定通道。操作系统将安排所有这些进程,它们将竞相检查睡眠条件。进程的这种行为有时被称为惊群效应(thundering herd),最好避免。

                +

                大多数条件变量都有两个用于唤醒的原语:signal用于唤醒一个进程;broadcast用于唤醒所有等待进程。

                +
                +
                +

                一个实际的操作系统将在固定时间内使用空闲列表找到自由的proc结构体,而不是allocproc中的线性时间搜索;xv6使用线性扫描是为了简单起见。

                +
                +

                Lab: Multithreading

                +

                You will implement switching between threads in a user-level threads package, use multiple threads to speed up a program, and implement a barrier.

                +
                +

                这个introduction看起来还是非常激动人心的,很早就想了解到底线程是怎么实现的了。不过做完发现思想还是很简单的,就是只用切换上下文和栈就行。可以看看提供给的代码。

                +

                Uthread: switching between threads

                +

                In this exercise you will design the context switch mechanism for a user-level threading system, and then implement it.

                +

                To get you started, your xv6 has two files user/uthread.c and user/uthread_switch.S, and a rule in the Makefile to build a uthread program.

                +

                uthread.c contains most of a user-level threading package, and code for three simple test threads. The threading package is missing some of the code to create a thread and to switch between threads.

                +

                You will need to add code to thread_create() and thread_schedule() in user/uthread.c, and thread_switch in user/uthread_switch.S.

                +

                One goal is ensure that when thread_schedule() runs a given thread for the first time, the thread executes the function passed to thread_create(), on its own stack.

                +

                Another goal is to ensure that thread_switch saves the registers of the thread being switched away from, restores the registers of the thread being switched to, and returns to the point in the latter thread’s instructions where it last left off. You will have to decide where to save/restore registers; modifying struct thread to hold registers is a good plan.

                +

                You’ll need to add a call to thread_switch in thread_schedule; you can pass whatever arguments you need to thread_switch, but the intent is to switch from thread t to next_thread.

                +
                +

                实现的是用户级线程,其栈保存在对应父进程的地址空间中。

                +

                感想

                思路

                看了一遍它这里面写的题目还是有点抽象的,需要结合着给的代码看,那样就清晰多了。

                +

                首先,要补全的地方有这几个:

                +
                // 1. in thread_schedule()
                if (current_thread != next_thread) { /* switch threads? */
                next_thread->state = RUNNING;
                t = current_thread;
                current_thread = next_thread;
                /* YOUR CODE HERE
                * Invoke thread_switch to switch from t to next_thread:
                * thread_switch(??, ??);
                */
                } else
                next_thread = 0;
                // 2. in thread_create()
                void
                thread_create(void (*func)())
                {
                struct thread *t;

                for (t = all_thread; t < all_thread + MAX_THREAD; t++) {
                if (t->state == FREE) break;
                }
                t->state = RUNNABLE;
                // YOUR CODE HERE
                }
                // 3. in uthread_switch.S
                /*
                * save the old thread's registers,
                * restore the new thread's registers.
                */

                .globl thread_switch
                thread_switch:
                /* YOUR CODE HERE */
                ret /* return to ra */
                +

                这几个函数到时候会被如此调用:

                +
                int
                main(int argc, char *argv[])
                {
                a_started = b_started = c_started = 0;
                a_n = b_n = c_n = 0;
                thread_init();
                thread_create(thread_a);
                thread_create(thread_b);
                thread_create(thread_c);
                thread_schedule();
                exit(0);
                }
                -

                pc.c

                -
                #define __LIBRARY__
                #include <stdio.h>
                #include <errno.h>
                #include <linux/sem.h>
                #include <unistd.h>
                #include <sys/stat.h>
                #include <sys/types.h>
                #include <fcntl.h>

                _syscall2(sem_t *, sem_open, const char *, name, unsigned int, value);
                _syscall1(int, sem_wait, sem_t *, sem);
                _syscall1(int, sem_post, sem_t *, sem);
                _syscall1(int, sem_unlink, const char *, name);

                const char* filename="buffer.txt";

                sem_t* empty;
                sem_t* full;
                sem_t* mutex;

                int fd;

                void Producer(){
                int i;
                int tmp;
                char s[5]={0};
                for(i=0;i<=500;i++){
                sem_wait(empty);
                sem_wait(mutex);
                tmp=lseek(fd,0,SEEK_CUR);
                lseek(fd,0,SEEK_END);
                sprintf(s,"%03d\n",i);
                write(fd,s,4);
                lseek(fd,tmp,SEEK_SET);
                sem_post(mutex);
                sem_post(full);
                }
                }

                void Consumer(){
                char s[5]={0};
                sem_wait(full);
                sem_wait(mutex);
                read(fd,s,4);
                printf("%d : %s",getpid(),s);
                sem_post(mutex);
                sem_post(empty);
                }

                int main(){
                int i;
                fd=open(filename,O_RDWR|O_CREAT);
                printf("%d\n",errno);
                empty=sem_open("empty",10);
                full=sem_open("full",0);
                mutex=sem_open("mutex",1);

                if(!fork()){
                Producer();
                return 0;
                }

                for(i=0;i<10;i++){
                if(!fork()){
                int c=50;
                while(c--) Consumer();
                }
                }
                close(fd);
                sem_unlink("empty");
                sem_unlink("full");
                sem_unlink("mutex");

                return 0;
                }
                +

                所以,我们在第一个地方要做的,就是要填入swtch的签名。第二个地方要做的,就是要想办法让该线程一被启动就去执行参数的函数指针。第三个地方要做的,就是要完成上下文的切换。

                +

                所以思路其实是很直观的。我们可以模仿进程管理中用来表示上下文的context,在thread_create的时候把里面的ra设置为参数的函数指针入口,sp修改为thread结构体中的栈地址。swtch函数则完全把kernel/swtch.S超过来就行。

                +
                +

                在这个思路中,我们是怎么做到栈的切换的呢?

                +

                每个线程在thread_create的时候,都将自己的context中的sp修改为自己的栈地址。这样一来,在它们被调度的时候,switch会自然而然地从context中读取sp作为之后运行的sp,这样就实现了栈的切换。

                +
                +

                我觉得其他方面都不难,最坑最细节的【也是我完全没有想到的……】就是这里:

                +
                // 修改sp为栈顶
                t->context.sp = (uint64)t->stack + STACK_SIZE;
                +

                需要注意,栈顶并不是t->stack

                +

                通过测试程序:

                +
                int main(){
                int a[5]={1,2,3,4,5};
                for(int i=0;i<5;i++){
                printf("%p\n",&a[i]);
                }
                return 0;
                }
                0062feb8
                0062febc
                0062fec0
                0062fec4
                0062fec8
                +

                栈是向下增长的,因而,栈顶确实应该是数组的末尾……

                +

                这里完全没有想到,还是吃了基础的亏啊。

                +
                +

                如果这里将t->stack作为sp,那么运行时会出现非常诡异的现象(打印的是abc三个的thread->state):

                +

                image-20230120232149776

                +

                仅有c【经测试,是仅有最后一个启动的线程】在执行,而ab的state都不是理想中的2,而是很奇怪的值。我确实有想过栈溢出问题,但是马上被我否定了。我完全没有想到是那样错的【悲】

                +
                +
                关于swtch

                Update,验收时学长问为什么这里的uswitch.S为什么无需保存tn这样的寄存器。答案是因为tn是caller-save的,线程这相当于仅仅是执行一个函数,所以只需保存callee-save的寄存器。

                +

                内核的swtch也只保存了这些callee-save的寄存器,也是同一个道理。

                +

                image-20231219000100763

                +

                image-20231219000047041

                +

                tn寄存器被保存在调用者的栈帧中。感觉也能理解为什么那题作业题说上文进程的现场是由栈保存了。

                +

                代码

                增加context结构体定义,修改thread结构体
                struct context {
                uint64 ra;
                uint64 sp;

                // callee-saved
                uint64 s0;
                uint64 s1;
                uint64 s2;
                uint64 s3;
                uint64 s4;
                uint64 s5;
                uint64 s6;
                uint64 s7;
                uint64 s8;
                uint64 s9;
                uint64 s10;
                uint64 s11;
                };


                struct thread {
                char stack[STACK_SIZE]; /* the thread's stack */
                int state; /* FREE, RUNNING, RUNNABLE */
                struct context context;
                };
                -

                这部分踩过的坑:

                -
                  -
                1. 在用户态和核心态之间传递参数【这个我没考虑到】

                  -
                  指针参数传递的是应用程序所在地址空间的逻辑地址,
                  在内核中如果直接访问这个地址,访问到的是内核空间中的数据,不会是用户空间的。
                  所以这里还需要一点儿特殊工作,才能在内核中从用户空间得到数据。
                  +
                  修改thread_create
                  void
                  thread_create(void (*func)())
                  {
                  struct thread *t;

                  for (t = all_thread; t < all_thread + MAX_THREAD; t++) {
                  if (t->state == FREE) break;
                  }
                  t->state = RUNNABLE;
                  // YOUR CODE HERE
                  // 将当前上下文保存入context
                  thread_switch((uint64)(&(t->context)),(uint64)(&(t->context)));
                  // 修改sp为栈顶
                  t->context.sp = (uint64)t->stack + STACK_SIZE;
                  // 修改ra为参数的函数指针入口
                  t->context.ra = (uint64)func;
                  }
                  -

                  这段代码就是在做这个。

                  -
                  /* 首先将信号量的名称赋值到新建的缓冲区中 */
                  char nbuf[20];
                  int i = 0;
                  for(; i< 20; i++)
                  {
                  nbuf[i] = get_fs_byte(name+i);
                  }
                2. -
                3. 这一段代码值得学习

                  -
                  # ifndef _SEM_H_
                  # define _SEM_H_
                4. -
                5. 一个第一眼看傻掉了的问题

                  -
                  //sleep函数的签名
                  void sleep_on(struct task_struct **p);
                  //一开始初始化队列为空
                  sem_list[i].queue = NULL;
                  //使用sleep
                  sleep_on(&(sem->queue));
                  +
                  修改thread_schedule
                  if (current_thread != next_thread) {         /* switch threads?  */
                  next_thread->state = RUNNING;
                  t = current_thread;
                  current_thread = next_thread;
                  /* YOUR CODE HERE
                  * Invoke thread_switch to switch from t to next_thread:
                  * thread_switch(??, ??);
                  */
                  thread_switch((uint64)(&(t->context)),(uint64)(&(current_thread->context)));
                  } else
                  next_thread = 0;
                  -

                  如果队列为空的时候,传入sleep_on的是不是NULL呢?

                  -

                  其实这个本质上是type* p=NULL,&p是不是NULL的问题。虽然知道不是,但还是写个程序测试一下:

                  -
                  #include <stdio.h> 
                  typedef struct {
                  int value;
                  }haha;

                  void isNULL(haha** a){
                  printf("%d",a==NULL);
                  }

                  int main(){
                  haha *h=NULL;
                  isNULL(&h);
                  return 0;
                  }
                  //result:0
                6. -
                7. sem_post签名与实现矛盾

                  -
                  wake_up() 的功能是唤醒链表上睡眠的所有进程。
                  sem_post() 就是信号量的 V 原子操作。如果有等待 sem 的进程,它会唤醒其中的一个。
                  +
                  修改thread_switch

                  全部照搬kernel/swtch.S,没什么好说的

                  +

                  Using threads

                  Barrier

                  +

                  In this assignment you’ll implement a barrier: a point in an application at which all participating threads must wait until all other participating threads reach that point too.

                  +
                  +

                  直接上代码,还是比较简单的

                  +
                  static void
                  barrier()
                  {
                  // YOUR CODE HERE
                  //
                  // Block until all threads have called barrier() and
                  // then increment bstate.round.
                  //
                  pthread_mutex_lock(&(bstate.barrier_mutex));
                  bstate.nthread++;
                  while(bstate.nthread < nthread){
                  pthread_cond_wait(&(bstate.barrier_cond), &(bstate.barrier_mutex));
                  goto end;
                  }
                  // 此部分仅一个线程会进入
                  pthread_cond_broadcast(&(bstate.barrier_cond));
                  bstate.nthread = 0;
                  bstate.round++;
                  end:
                  pthread_mutex_unlock(&(bstate.barrier_mutex));
                  }
                  -

                  以上都是指导书的内容。这个“所有”和“一个”的用意我不大明白。也许唤醒所有进程,其中一个抢到了锁,其他的全睡了,这个也被认为是唤醒其中一个吧()

                  +
                  +

                  Update

                  +

                  关于pthread_cond的实现,也是使用了条件变量的思想,这个值得以后有时间了解一下。

                  +
                  +]]> + + + 其他的对实验未涉及的思考 + /2023/01/10/xv6$chap9/ + 其他的对实验未涉及的思考

                  由mkfs引发的对虚拟机的学习

                  +

                  懂了!VMware/KVM/Docker原来是这么回事儿这篇文章对虚拟化、虚拟机技术讲解很到位,写得通俗易懂,非常值得一看

                  +

                  KVM 的「基于内核的虚拟机」是什么意思?这篇文章对QEMU-KVM架构进行了详细的介绍。还有这篇文章对应的知乎问题下面的高赞回答有机会也可以去看看。

                  +

                  QEMU/KVM原理概述这篇文章前面的原理和上面那个差不多,后面有使用kvm做一个精简内核的实例,有兴趣/有精力/有需要可以看看。

                  +

                  MIT6.S081操作系统实验——操作系统是如何在qemu虚拟机中启动的?

                  +

                  xv6分析–mkfs源代码注释

                  +
                  +

                  以前只是知道,xv6是运行在qemu提供的虚拟环境之上的。qemu是什么,怎么虚拟的,虚拟机和宿主机是怎么交互的,这些一概不通。今天心血来潮想研究下qemu,虚拟机啥的到底是什么玩意,虽然看得有些猪脑过载,但还是写下一些个人的整理。

                  +

                  qemu

                  是什么

                  在了解qemu之前,可以先了解一下虚拟化的思想。

                  +
                  虚拟化
                  +

                  虚拟化的主要思想是,通过分层将底层的复杂、难用的资源虚拟抽象成简单、易用的资源,提供给上层使用。

                  +

                  本质上,计算机的发展过程也是虚拟化不断发展的过程,底层的资源或者通过空间的分割,或者通过时间的分割,将下层的资源通过一种简单易用的方式转换成另一种资源,提供给上层使用。

                  +

                  虚拟化可分为以下几方面:

                  +
                    +
                  1. CPU抽象:机器码、汇编语言到C语言、再到高级语言的不断虚拟的过程
                  2. +
                  3. 存储抽象:操作系统通过文件和目录抽象
                  4. +
                  5. 网络抽象:TCP/IP协议栈模型将网卡设备中传递的二进制数据,经过网络层、传输层的抽象后,为应用程序提供了便捷的网络包处理接口,而无需关心底层的IP路由、分片等细节
                  6. +
                  7. 进程抽象:操作系统通过进程抽象为不同的应用程序提供了安全隔离的执行环境,并且有着独立的CPU和内存等资源
                  8. +
                  +
                  +

                  虚拟化的思想实际上就是我以前一直称为“抽象”的思想,以接口的形式逐层向上服务。

                  +
                  虚拟机
                  +

                  虚拟机的核心能力在于提供一个执行环境(隐藏底层细节),并在其中完成用户的指定任务。

                  +
                  +
                  +

                  虚拟机有多种不同的形式,包括提供指令执行环境的进程、模拟器和高级语言虚拟机,或者是提供一个完整的系统环境的系统虚拟机。

                  +
                  +
                  进程

                  进程实际上就是一种虚拟机。

                  +
                  +

                  进程可以看作是一组资源的集合,有自己独立的进程地址空间以及独立的CPU和寄存器,执行程序员编写的指令,完成一定的任务。

                  +

                  操作系统可以创建多个进程,每一个进程都可以看成一个独立的虚拟机,它们在执行指令、访问内存的时候并不会相互影响影响。

                  +
                  +
                  模拟器
                  高级语言虚拟机
                  系统虚拟机
                  +

                  通过系统虚拟化技术,能够在单个的宿主机硬件平台上运行多个虚拟机,每个虚拟机都有着完整的虚拟机硬件,如虚拟的CPU、内存、虚拟的外设等,并且虚拟机之间能够实现完整的隔离。

                  +

                  在系统虚拟化中,管理全局物理资源的软件叫作虚拟机监控器(Virtual Machine Monitor,VMM),VMM之于虚拟机就如同操作系统之于进程,VMM利用时分复用或者空分复用的办法将硬件资源在各个虚拟机之间进行分配。

                  +
                  +
                  qemu

                  可以看到,qemu就是一种虚拟机。它可以模拟虚拟机硬件,为操作系统提供虚拟硬件环境,从而能够让不同的操作系统能够在不同主机硬件上执行。

                  +

                  qemu-kvm架构

                  诞生的原因
                  +

                  其对于虚拟化技术的优化,以及发展的前因后果,具体可以看懂了!VMware/KVM/Docker原来是这么回事儿这篇文章。

                  +

                  概括来讲,大致有以下几个要点:

                  +
                  +
                  两种虚拟化方案

                  640

                  +

                  640-1676793944101-7

                  +
                  实现上述的虚拟化方案

                  一个典型的做法是——陷阱 & 模拟技术

                  +

                  什么意思?简单来说就是正常情况下直接把虚拟机中的代码指令放到物理的CPU上去执行,一旦执行到一些敏感指令,就触发异常,控制流程交给VMM,由VMM来进行对应的处理,以此来营造出一个虚拟的计算机环境。

                  +
                  x86架构的问题

                  x86架构使得上述做法用不了了。因为它引入了四种权限

                  +

                  image-20230219160725978

                  +
                  解决方法
                    +
                  1. 全虚拟化

                    +

                    VMware的二进制翻译技术、QEMU的模拟指令集

                    +
                  2. +
                  3. 半虚拟化

                    +
                  4. +
                  5. 硬件辅助虚拟化

                    +

                    硬件辅助虚拟化细节较为复杂,简单来说,新一代CPU在原先的Ring0-Ring3四种工作状态之下,再引入了一个叫工作模式的概念,有VMX root operationVMX non-root operation两种模式,每种模式都具有完整的Ring0-Ring3四种工作状态,前者是VMM运行的模式,后者是虚拟机中的OS运行的模式。

                    +

                    qemu-kvm架构正是借助于此实现的。

                    +
                  6. +
                  +
                  kvm

                  kvm就是借助硬件辅助虚拟化诞生的。可以把kvm看作是一堆系统调用。

                  +
                  +

                  什么是 KVM?

                  +

                  KVM本身是一个内核模块,它导出了一系列的接口到用户空间,用户态程序可以使用这些接口创建虚拟机。

                  +

                  具体而言,KVM 可帮助您将 Linux 转变为虚拟机监控程序,使主机计算机能够运行多个隔离的虚拟环境,即虚拟客户机或虚拟机(VM)。【也即,虚拟机—进程,KVM—操作系统】

                  +
                  +
                  +

                  在虚拟化底层技术上,KVM和VMware后续版本一样,都是基于硬件辅助虚拟化实现。不同的是VMware作为独立的第三方软件可以安装在Linux、Windows、MacOS等多种不同的操作系统之上,而KVM作为一项虚拟化技术已经集成到Linux内核之中,可以认为Linux内核本身就是一个HyperVisor,这也是KVM名字的含义,因此该技术只能在Linux服务器上使用。

                  +
                  +
                  qemu-kvm

                  KVM本身基于硬件辅助虚拟化,仅仅实现CPU和内存的虚拟化,但一台计算机不仅仅有CPU和内存,还需要各种各样的I/O设备,不过KVM不负责这些。这个时候,QEMU就和KVM搭上了线,经过改造后的QEMU,负责外部设备的虚拟,KVM负责底层执行引擎和内存的虚拟,两者彼此互补,成为新一代云计算虚拟化方案的宠儿。

                  +
                  qemu-kvm总体架构

                  KVM只负责最核心的CPU虚拟化和内存虚拟化部分;QEMU作为其用户态组件,负责完成大量外设的模拟

                  +

                  v2-249a3f162de88198bbe415110fc71c7f_1440w

                  +
                  VMX root和VMX non root
                  +

                  VMX root是宿主机模式,此时CPU在运行包括QEMU在内的普通进程和宿主机的操作系统内核;

                  +

                  VMX non-root是虚拟机模式,此时CPU在运行虚拟机中的用户程序和操作系统代码。

                  +
                  +

                  也就是说,虚拟机的程序,包括用户程序和内核程序,都运行在non-root模式。宿主机的所有程序,包括用户程序【包括qemu】和内核程序【包括kvm】,都运行在root模式。

                  +
                  qemu层(左上)

                  上面说到,qemu负责的是大量外设的模拟。它具体要做以下几件事:

                  +
                  +

                  初始化虚拟机:

                  +
                    +
                  1. 创建模拟的芯片组

                    +
                  2. +
                  3. 创建CPU线程来表示虚拟机的CPU

                    +

                    QEMU在初始化虚拟机的CPU线程时,首先设置好相应的虚拟CPU寄存器的值,然后调用KVM的接口将虚拟机运行起来,这样CPU线程就会被调度在物理CPU上执行虚拟机的代码。

                    +
                  4. +
                  5. 在QEMU的虚拟地址空间中分配空间作为虚拟机的物理地址

                    +
                  6. +
                  7. 根据用户在命令行指定的设备为虚拟机创建对应的虚拟设备【如各种IO设备】

                    +
                  8. +
                  +

                  虚拟机运行时:

                  +
                    +
                  1. 监听多种事件

                    +

                    包括虚拟机对设备的I/O访问、用户对虚拟机管理界面、虚拟设备对应的宿主机上的一些I/O事件(比如虚拟机网络数据的接收)等

                    +
                  2. +
                  3. 调用函数处理

                    +
                  4. +
                  +
                  +

                  可以看到,qemu确实利用了宿主机的各种资源,提供了一个很完美的硬件环境。其资源对应关系为:

                  +

                  虚拟机的CPU——宿主机的一个线程

                  +

                  虚拟机的物理地址——qemu在宿主机的虚拟地址

                  +

                  虚拟机对硬件设备的访问 —→ 对qemu的访问

                  +
                  kvm层(下方)

                  它大概做了两件事:

                  +
                    +
                  1. 给qemu提供运行时的参数

                    +

                    通过“/dev/kvm”设备,比如CPU个数、内存布局、运行等。

                    +
                  2. +
                  3. 截获VM Exit事件【下面会讲,用来完成虚拟机和硬件环境的交互】并进行处理。

                    +
                  4. +
                  +
                  虚拟机层(右上)
                    +
                  1. CPU——QEMU进程中的一个线程

                    +

                    通过QEMU和KVM的相互协作,虚拟机的线程会被宿主机操作系统正常调度,直接执行虚拟机中的代码

                    +
                  2. +
                  3. 物理地址——QEMU进程中的虚拟地址

                    +
                  4. +
                  5. 设备——QEMU实现

                    +

                    在运行过程中,虚拟机操作系统通过设备的I/O端口(Port IO、PIO)或者MMIO(Memory Mapped I/O)进行交互,KVM会截获这个请求【也即VM Exit,下面会讲】,大多数时候KVM会将请求分发到用户空间的QEMU进程中,由QEMU处理这些I/O请求

                    +
                  6. +
                  +
                  虚拟机在QEMU-KVM架构的执行方法
                  状态管理虚拟化

                  虚拟机肯定是会与它的硬件环境进行交互的,它的硬件环境也就是QEMU—KVM。

                  +

                  虚拟机的用户程序和内核程序都是直接由宿主机的操作系统正常调度,我们可以将其看作虚拟态。QEMU—KVM可以看作是宿主机的进程,我们可以将其看作宿主态。因而,当虚拟机一些事情希望由QEMU—KVM来做,我们就需要从虚拟态转移到宿主态。

                  +

                  听起来有没有感觉很耳熟?是的,“从用户态陷入内核态”,跟这个的原理是一样的。

                  +

                  因而,虚拟机与硬件环境交互,实际上是虚拟态和宿主态状态的转换,如下图:

                  +

                  v2-9377a260d034d2904b1807d3fe53dcd9_1440w

                  +

                  VM Exit

                  +

                  当虚拟机中的代码是敏感指令或者说满足了一定的退出条件时,CPU会从虚拟态退出到KVM,这叫作VM Exit。

                  +

                  这就像在用户态执行指令陷入内核一样。

                  +

                  VM Exit首先陷入到KVM中进行处理,如果KVM无法处理,比如说虚拟机写了设备的寄存器地址,那么KVM会将这个写操作分派到QEMU中进行处理。

                  +

                  VM Entry

                  +

                  当KVM或者QEMU处好了退出事件之后,又可以将CPU置于虚拟态以运行虚拟机代码,这叫作VM Entry。

                  +
                  内存管理虚拟化
                  +

                  QEMU在初始化的时候会通过mmap分配虚拟内存空间作为虚拟机的物理内存,【感觉思路打开,物理内存与文件对应了起来】QEMU在不断更新内存布局的过程中会持续调用KVM接口通知内核KVM模块虚拟机的内存分布。

                  +
                  +

                  虚拟机在运行过程中,首先需要将虚拟机的虚拟地址(Guest Virtual Address,GVA)转换成虚拟机的物理地址(Guest Physical Address,GPA),然后将虚拟机的物理地址转换成宿主机的虚拟地址(Host Virtual Address,HVA),最终转换成宿主机的物理地址(Host Physical Address,HPA)。

                  +

                  整个寻址过程由硬件实现,具体实现方式为扩展页表(Extended Page Table,EPT)。

                  +

                  在支持EPT的环境中,虚拟机在第一次访问内存的时候就会陷入到KVM,KVM会逐渐建立起所谓的EPT页面【lazy思想贯穿始终,还是该叫自适应?】。这样虚拟机的虚拟CPU在后面访问虚拟机虚拟内存地址的时候,首先会被转换为虚拟机物理地址,接着会查找EPT页表,然后得到宿主机物理地址。【有种TLB的感觉】

                  +

                  v2-942e1ed598eed3d401d00e4719224d27_1440w

                  +
                  外设管理虚拟化

                  设备模拟的本质是要为虚拟机提供一个与物理设备接口完全一致的虚拟接口

                  +

                  虚拟机中的操作系统与设备进行的数据交互或者由QEMU和(或)KVM完成,或者由宿主机上对应的后端设备完成。

                  +

                  QEMU在初始化过程中会创建好模拟芯片组和必要的模拟设备,包括南北桥芯片、PCI根总线、ISA根总线等总线系统,以及各种PCI设备、ISA设备等。

                  +

                  外设虚拟化主要有如下几种方式:

                  +
                    +
                  1. 纯软件模拟(完全虚拟化)

                    +

                    QEMU最早的方案,虚拟机内核不用做任何修改,每一次对设备的寄存器读写都会陷入到KVM,进而到QEMU,QEMU再对这些请求进行处理并模拟硬件行为。

                    +

                    软件模拟会导致非常多的QEMU/KVM接入,效率低下。

                  2. -
                  3. 聪明的越界处理【未考虑到】

                    -
                    /* 判断:如果传入的信号量是无效信号量,V操作失败,返回-1 */
                    if(sem == NULL || sem < sem_list || sem > sem_list + SEM_LIST_LENGTH)
                    {
                    return -1;
                    }
                    - -

                    毕竟有效的信号量都是引用的信号量表的信号量。所以地址越界的自然无效。

                    +
                  4. virtio设备(半虚拟化)

                    +

                    virtio设备是一类特殊的设备,并没有对应的物理设备,所以需要虚拟机内部操作系统安装特殊的virtio驱动。

                    +

                    相比软件模拟,virtio方案提高了虚拟设备的性能。

                  5. -
                  6. 最坑的一点

                    -

                    其实指导书提醒了

                    -
                    -

                    下面描述的问题未必具有普遍意义,仅做为提醒,请实验者注意。

                    -

                    include/string.h 实现了全套的 C 语言字符串操作,而且都是采用汇编 + inline 方式优化。

                    -

                    但在使用中,某些情况下可能会遇到一些奇怪的问题。比如某人就遇到 strcmp() 会破坏参数内容的问题。如果调试中遇到有些 “诡异” 的情况,可以试试不包含头文件,一般都能解决。不包含 string.h,就不会用 inline 方式调用这些函数,它们工作起来就趋于正常了。

                    -
                    -

                    但是具体表现跟它说的差距有点大()

                    -

                    我是全部检查没问题了,然后上linux0.11真机运行。PID:number这样的信息全部打印出来了,没啥问题,但是打印完操作系统就会寄,大多数极端情况就直接重启了,小部分还会温和地提醒以下报错信息然后死循环

                    -
                    kernel panic: trying to free up swapper memory space
                    in swapper task - not syncing
                    - -

                    最后尝试着修改去掉string.h,才得到了正确的结果,泪目。

                    +
                  7. 设备直通

                    +

                    将物理硬件设备直接挂到虚拟机上,虚拟机直接与物理设备交互,尽可能在I/O路径上减少QEMU/KVM的参与。

                    +

                    设备直通经常搭配硬件虚拟化支持技术SRIOV(Single Root I/O Virtualization,单根输入/输出虚拟化)使用,SRIOV能够将单个的物理硬件高效地虚拟出多个虚拟硬件。

                  -

                  proc文件系统

                  -

                  参考文章:

                  -
                  操作系统实验08-proc文件系统的实现
                  +

                  v2-555d017ce5b65457f98617a5fdf232af_1440w

                  +
                  中断处理虚拟化

                  操作系统通过写设备的I/O端口或者MMIO地址来与设备交互,设备通过发送中断来通知操作系统事件。

                  +

                  QEMU/KVM一方面需要完成这项中断设备的模拟,另一方面需要模拟中断的请求处理

                  -

                  在 Linux 0.11 上实现 procfs(proc 文件系统)内的 psinfo 结点。当读取此结点的内容时,可得到系统当前所有进程的状态信息。例如,用 cat 命令显示 /proc/psinfo/proc/hdinfo的内容,可得到:

                  -
                  $ cat /proc/psinfo
                  pid state father counter start_time
                  0 1 -1 0 0
                  1 1 0 28 1
                  4 1 1 1 73
                  3 1 1 27 63
                  6 0 4 12 817
                  $ cat /proc/hdinfo
                  total_blocks: 62000;
                  free_blocks: 39037;
                  used_blocks: 22963;
                  ...
                  - -

                  procfs 及其结点要在内核启动时自动创建。

                  -

                  相关功能实现在 fs/proc.c 文件内。

                  -
                  -

                  必备知识

                  要点1 procfs简介

                  -

                  正式的 Linux 内核实现了 procfs,它是一个**虚拟文件系统**,通常被 mount(挂载) 到 /proc 目录上,通过虚拟文件和虚拟目录的方式提供访问系统参数的机会,所以有人称它为 “了解系统信息的一个窗口”。

                  -

                  这些虚拟的文件和目录**并没有真实地存在在磁盘**上,而是内核中各种数据的一种直观表示。虽然是虚拟的,但它们都可以通过标准的系统调用(open()read() 等)访问。

                  -

                  其实,Linux 的很多系统命令就是通过读取 /proc 实现的。例如 uname -a 的部分信息就来自 /proc/version,而 uptime 的部分信息来自 /proc/uptime/proc/loadavg

                  -
                  -

                  要点2 基本思路

                  -

                  Linux 是通过文件系统接口实现 procfs,并在启动时自动将其 mount 到 /proc 目录上。

                  -

                  此目录下的所有内容都是随着系统的运行自动建立、删除和更新的,而且它们完全存在于内存中,不占用任何外存空间。

                  -

                  Linux 0.11 还没有实现虚拟文件系统,也就是,还没有提供增加新文件系统支持的接口。所以本实验只能在现有文件系统的基础上,通过打补丁的方式模拟一个 procfs

                  -

                  Linux 0.11 使用的是 Minix 的文件系统,这是一个典型的基于 inode 的文件系统,《注释》一书对它有详细描述。它的每个文件都要对应至少一个 inode,而 inode 中记录着文件的各种属性,包括文件类型。文件类型有普通文件、目录、字符设备文件和块设备文件等。在内核中,每种类型的文件都有不同的处理函数与之对应。我们可以增加一种新的文件类型——proc 文件,并在相应的处理函数内实现 procfs 要实现的功能

                  +

                  QEMU支持单CPU的Intel 8259中断控制器以及SMP的I/O APIC(I/O Advanced Programmable Interrupt Controller)和LAPIC(Local Advanced Programmable Interrupt Controller)中断控制器。在这种方式下,虚拟外设通过QEMU向虚拟机注入中断,需要先陷入到KVM,然后由KVM向虚拟机注入中断,这是一个非常费时的操作。

                  +

                  为了提高虚拟机的效率,KVM自己也实现了中断控制器Intel 8259、I/O APIC以及LAPIC。用户可以有选择地让QEMU或者KVM模拟全部中断控制器,也可以让QEMU模拟Intel 8259中断控制器和I/O APIC,让KVM模拟LAPIC。

                  -

                  步骤

                  要点1 新增proc文件类型

                  include/sys/stat.h 新增:

                  -
                  #define S_IFPROC 0050000

                  #define S_ISPROC(m) (((m) & S_IFMT) == S_IFPROC)
                  +

                  xv6的全启动运行过程梳理

                  介绍完上述的qemu虚拟化,接下来就可以对xv6的全启动进行一个梳理了。

                  +

                  首先,在宿主机执行make qemu

                  +

                  Makefile中可以看到:

                  +
                  qemu: $K/kernel fs.img
                  $(QEMU) $(QEMUOPTS)
                  QEMU = qemu-system-riscv64
                  -

                  要点2 修改mknod()函数和init()函数

                  -

                  psinfo 结点要通过 mknod() 系统调用建立,所以要让它支持新的文件类型。

                  -
                  -

                  直接修改 fs/namei.c 文件中的 sys_mknod() 函数中的一行代码,如下:

                  -
                  if (S_ISBLK(mode) || S_ISCHR(mode) || S_ISPROC(mode))
                  inode->i_zone[0] = dev;
                  // 文件系统初始化
                  +

                  在log中可以看到:

                  +
                  ...
                  mkfs/mkfs fs.img README user/xargstest.sh user/_cat user/_echo user/_forktest user/_grep user/_init user/_kill user/_ln user/_ls user/_mkdir user/_rm user/_sh user/_stressfs user/_usertests user/_grind user/_wc user/_zombie user/_mmaptest
                  ...
                  qemu-system-riscv64 -machine virt -bios none -kernel kernel/kernel -m 128M -smp 3 -nographic -drive file=fs.img,if=none,format=raw,id=x0 -device virtio-blk-device,drive=x0,bus=virtio-mmio-bus.0

                  xv6 kernel is booting

                  hart 2 starting
                  hart 1 starting
                  init: starting sh
                  $
                  +

                  具体的Makefile相关内容我不大了解,但结合输出,我想大概是先通过riscv64-linux-gnu-gcc编译链接完所有文件,然后再执行mkfs产生fs.img镜像(mkfs后面那些东西应该是文件参数,对应于源码中的读取可执行程序进磁盘的部分),最后再运行qemu-system-riscv64开始对虚拟机进行boot。

                  +

                  boot直至启动后的所有代码,都是通过QEMU-KVM架构处理,直接运行在宿主机的CPU上的。其余的各种管理,可以详见小标题虚拟机在QEMU-KVM架构的执行方法

                  +

                  mkfs的作用及源码解读

                  作用

                  上面的知识表明,操作系统的启动在于文件系统初始化之后,这是因为操作系统本身的启动代码,放在磁盘映像fs.img中,而fs.img正是由文件系统初始化时弄出来的。也就是说,文件系统是操作系统的爸爸。【我以前一直以为是反过来的】

                  +

                  image-20230121162324747

                  -

                  内核初始化的全部工作是在 main() 中完成,而 main() 在最后从内核态切换到用户态,并调用 init()

                  -

                  init() 做的第一件事情就是挂载根文件系统:

                  -
                  void init(void) { 
                  // ……
                  setup((void *) &drive_info);
                  // ……
                  }
                  +

                  图中的boot块就是操作系统的引导扇区。

                  +
                  +

                  mkfs的作用,正是把宿主机提供的虚拟地址空间作为虚拟磁盘,把虚拟地址空间划分为如上图所示的地址结构。它是运行在宿主机当中的。有了mkfs,才能有我们的虚拟机。

                  +
                  代码解读

                  xv6分析–mkfs源代码注释

                  +

                  yysy这个就写得很好了。

                  +

                  user mem-allocator

                  +

                  linux的堆管理

                  +

                  那么malloc到底是怎么实现的呢?不是每次要申请内存就调一下系统调用,而是程序向操作系统申请⼀块适当⼤⼩的堆空间,然后由程序⾃⼰管理这块空间,⽽具体来讲,管理着堆空间分配的往往是程序的运⾏库

                  +

                  也就是说,malloc本质上是以运行库而非系统调用形式出现的。它里面用到的是sbrk和mmap这两个系统调用来进货。

                  +

                  glibc的malloc函数是这样处理⽤户的空间请求的:对于⼩于128KB的请求来说,它会在现有的堆空间⾥⾯,按照堆分配算法为它分配⼀块空间并返回;对于⼤于128KB的请求来说,它会使⽤mmap()函数为它分配⼀块匿名空间,然后在这个匿名空间中为⽤户分配空间。

                  +
                  +

                  在内核态中,我们使用kallockfree来申请和释放内存页。在用户态中,我们使用mallocfree来对动态内存进行管理。【也就是说这个实现的是堆管理

                  +

                  内核中的最小单位只能是页,但user mem-allocator对外提供的申请内存服务的最小单位不是页,而是sizeof(Header)。因而,这就需要我们的user mem-allocator进行数据结构的管理,来统一这二者的实现。

                  +

                  数据结构

                  环形链表

                  user mem-allocator的数据结构是环形链表,起始结点为一个空数据载体。

                  +

                  image-20230316140158908

                  +

                  image-20230316140450988

                  +

                  地址从低到高

                  链表的头结点的存储地址/所代表的内存地址的地址数值最小,并且其余结点按遍历顺序地址递增。

                  +

                  具体实现

                  user mem-allocator由三个主要函数组成,分别是morecoremallocfree。一个一个地来说未免有点不符合正常人的思路,所以我接下来会以用户初次调用malloc为例,来整理user mem-allocator的具体实现。

                  +

                  malloc

                  当用户初次调用malloc,此时freep仍为空指针,因而会进入如下分支:

                  +
                  if((prevp = freep) == 0){
                  // 空闲mem为空的情况
                  base.s.ptr = freep = prevp = &base;
                  base.s.size = 0;
                  }
                  -

                  procfs 的初始化工作**应该在根文件系统挂载之后开始**。它包括两个步骤:

                  -
                    -
                  • (1)建立 /proc 目录;建立 /proc 目录下的各个结点。本实验只建立 /proc/psinfo

                    -
                  • -
                  • (2)建立目录和结点分别需要调用 mkdir()mknod() 系统调用。因为初始化时已经在用户态,所以不能直接调用 sys_mkdir()sys_mknod()。必须在初始化代码所在文件中实现这两个系统调用的用户态接口。

                    -
                    #ifndef __LIBRARY__
                    #define __LIBRARY__
                    #endif

                    _syscall2(int,mkdir,const char*,name,mode_t,mode);
                    _syscall3(int,mknod,const char*,filename,mode_t,mode,dev_t,dev);
                    +

                    也即初始化为这种情况:

                    +

                    image-20230316143711888

                    +

                    随后,由于prevp->ptr == freep,故而会在循环中进入该分支:

                    +
                    for(p = prevp->s.ptr; ; prevp = p, p = p->s.ptr){
                    // ...
                    if(p == freep) // 一般情况下,此处表明已经完整遍历了一遍环形链表,因为prev的初值是freep,而我们是从prev->next开始遍历的
                    if((p = morecore(nunits)) == 0)
                    return 0;
                    }
                    -

                    mkdir() 时 mode 参数的值可以是 “0755”(对应 rwxr-xr-x),表示只允许 root 用户改写此目录,其它人只能进入和读取此目录。

                    -

                    procfs 是一个只读文件系统,所以用 mknod() 建立 psinfo 结点时,必须通过 mode 参数将其设为只读。建议使用 S_IFPROC|0444 做为 mode 值,表示这是一个 proc 文件,权限为 0444(r–r–r–),对所有用户只读。

                    -

                    mknod() 的第三个参数 dev 用来说明结点所代表的设备编号。对于 procfs 来说,此编号可以完全自定义。proc 文件的处理函数将通过这个编号决定对应文件包含的信息是什么。例如,可以把 0 对应 psinfo,1 对应 meminfo,2 对应 cpuinfo。

                    -
                  • -
                  -
                -

                也就是说,打开linux-0.11/init/main.c

                -

                加入:

                -
                #ifndef __LIBRARY__
                #define __LIBRARY__
                #endif

                _syscall2(int,mkdir,const char*,name,mode_t,mode);
                _syscall3(int,mknod,const char*,filename,mode_t,mode,dev_t,dev);
                +

                调用morecore

                +

                morecore

                进入morecore后,首先会对堆内存进行扩容:

                +
                if(nu < 4096)
                nu = 4096;
                p = sbrk(nu * sizeof(Header));
                if(p == (char*)-1)
                return 0;
                -

                在init函数中,添加:

                -
                mkdir("/proc",0755);
                mknod("/proc/psinfo",S_IFPROC|0400,0);
                //其余文件以此类推...
                +

                其中,nu表示要申请的内存单元数,一个内存单元为sizeof(Header),因而nu在malloc中计算如下:

                +
                nunits = (nbytes + sizeof(Header) - 1)/sizeof(Header) + 1;
                -

                编译运行即可看到:

                - +

                为了满足内核以一页为最小内存单位的需求,以及避免过多陷入内核态,它每次会申请至少4096*内存单元的堆空间。

                +

                对堆内存进行扩容完之后,morecore会手动调用一次free,将新申请到的内存加入数据结构中。【此处类似于在knit中调用kfree的原理】

                +

                free

                void free(void *ap){
                Header *bp, *p;

                bp = (Header*)ap - 1;
                for(p = freep; !(bp > p && bp < p->s.ptr); p = p->s.ptr)
                if(p >= p->s.ptr && (bp > p || bp < p->s.ptr))
                break;
                +

                由于此时freep == freep->str == base,并且我们在morecore中新申请的内存空间ap满足ap > base,故而会跳出循环。

                -

                这些信息至少说明,psinfo 被正确 open() 了。所以我们不需要对 sys_open() 动任何手脚,唯一要打补丁的,是 sys_read()

                -
                -

                要点3 修改read(),让proc可读

                -

                首先分析 sys_read(在文件 fs/read_write.c 中)

                -

                要在这里一群 if 的排比中,加上 S_IFPROC() 的分支,进入对 proc 文件的处理函数。需要传给处理函数的参数包括:

                -
                  -
                • inode->i_zone[0],这就是 mknod() 时指定的 dev ——设备编号
                • -
                • buf,指向用户空间,就是 read() 的第二个参数,用来接收数据
                • -
                • count,就是 read() 的第三个参数,说明 buf 指向的缓冲区大小
                • -
                • &file->f_posf_pos 是上一次读文件结束时“文件位置指针”的指向。这里必须传指针,因为处理函数需要根据传给 buf 的数据量修改 f_pos 的值。
                • -
                +

                为什么ap > base呢?

                +

                别忘了我们扩容的原理。我们是以proc->size为起始地址扩容的。ap处在扩容内存中,因而ap>旧size;base处在扩容前内存内,因而base<=旧size。故而有ap>base。

                -

                依照指导书,在read_write.c添加如下语句:

                -
                extern int proc_handler(unsigned short dev,char* buf,int count,off_t* f_pos);

                int sys_read(...){
                // ...
                if(S_ISPROC(inode->i_mode)){
                return proc_handler(inode->i_zone[0],buf,count,&file->f_pos);
                }
                // ...
                }
                +
                if(bp + bp->s.size == p->s.ptr){
                bp->s.size += p->s.ptr->s.size;
                bp->s.ptr = p->s.ptr->s.ptr;
                } else
                bp->s.ptr = p->s.ptr;
                if(p + p->s.size == bp){
                p->s.size += bp->s.size;
                p->s.ptr = bp->s.ptr;
                } else
                p->s.ptr = bp;
                freep = p;
                -

                要点4 编写pro文件的处理函数

                -

                proc 文件的处理函数的功能是根据设备编号,把不同的内容写入到用户空间的 buf。写入的数据要从 f_pos 指向的位置开始,每次最多写 count 个字节,并根据实际写入的字节数调整 f_pos 的值,最后返回实际写入的字节数。当设备编号表明要读的是 psinfo 的内容时,就要按照 psinfo 的形式组织数据。

                -

                实现此函数可能要用到如下几个函数:

                -
                  -
                • malloc() 函数
                • -
                • free() 函数
                • -
                -

                包含 linux/kernel.h 头文件后,就可以使用 malloc()free() 函数。它们是可以被核心态代码调用的,唯一的限制是一次申请的内存大小不能超过一个页面。

                -
                -
                -

                进程的信息就来源于内核全局结构数组 struct task_struct * task[NR_TASKS] 中,具体读取细节可参照 sched.c 中的函数 schedule()

                -

                可以借鉴一下代码:

                -
                for(p = &LAST_TASK ; p > &FIRST_TASK ; --p)

                if (*p)
                (*p)->counter = ((*p)->counter >> 1)+...;
                -
                +

                跳出循环后,我们会进入第一个if的第二个分支,以及第二个if的第二个分支。经过这些指针操作后,此时我们的数据结构如下图所示:

                +

                image-20230316145733160

                +

                也即形成了一个两节点的环形链表。

                +

                malloc

                经历完上述调用后,我们回到malloc的循环中:

                +
                for(p = prevp->s.ptr; ; prevp = p, p = p->s.ptr){
                // ...
                if(p == freep)
                if((p = morecore(nunits)) == 0)
                return 0;
                }
                + +

                morecore的返回值可知,此时我们的p应该指向freep。本轮循环结束后执行 p = p->s.ptr,此时我们的p指向了我们刚在morecore中扩容出来的那一大段内存。

                +

                image-20230316150327569

                +

                在下一轮循环中,由于我们刚刚通过morecore申请了至少nunits的空间,因而我们将进入该分支:

                +
                if(p->s.size >= nunits){
                if(p->s.size == nunits)
                // 如果与所需的内存刚好相等,那就直接返回该小单元就行
                prevp->s.ptr = p->s.ptr;
                else {
                // 不等的话就只划分出一小部分
                // 一次划出几个header单元
                p->s.size -= nunits;
                p += p->s.size;
                p->s.size = nunits;
                }
                freep = prevp;
                return (void*)(p + 1);
                }
                + +

                nunits >= 4096,也即p->s.size == nunits,p所指向的地址恰好就是我们接下来会用的地址。因而,我们就将这部分内存空间从我们的freelist中剔除,在之后返回p的地址即可。

                +

                nunits < 4096,也即p->s.size != nunits,说明p所指向的这块内存空间比我们需要的大,那么我们就仅将该段内存空间切割出需要的那一小部分,再把p指向那一小部分开头的地方,返回p地址即可,如图所示。

                +

                image-20230316150846709

                +

                这样一来,我们就成功给用户它所需要的内存空间了。

                +

                free

                进行malloc之后,用户还需要调用free来手动释放内存,防止内存泄漏。

                +

                image-20230316151116462

                +
                for(p = freep; !(bp > p && bp < p->s.ptr); p = p->s.ptr)
                if(p >= p->s.ptr && (bp > p || bp < p->s.ptr))
                break;
                + +

                由于ap > baseap > 旧p->size = base->ptrbase < base->ptr,故而首先会进行一轮循环。再然后,由于p = 旧p->size,并且p > p->ptr = base,并且ap > 旧size,故而跳出循环。

                -

                cat 是 Linux 下的一个常用命令,功能是将文件的内容打印到标准输出。

                -

                它核心实现大体如下:

                -
                #include <stdio.h>
                #include <unistd.h>
                int main(int argc, char* argv[])
                {
                char buf[513] = {'\0'};
                int nread;

                int fd = open(argv[1], O_RDONLY, 0);
                while(nread = read(fd, buf, 512))
                {
                buf[nread] = '\0';
                puts(buf);
                }

                return 0;
                }
                +

                此处循环中,循环语句内部的这个循环实际上是对遍历到环形链表尾部,即将从头开始遍历,这个边界情况的处理。比较符合逻辑的还是循环语句内的那个条件。

                -

                在cat的代码中,open函数返回了psinfo的文件描述符,read函数读到该文件描述符,就会识别出我们要读写的文件是PROC类型的,因此就会跳转到我们的proc_handler去执行,再进一步跳转到psinfo_handler执行。根据cat的代码和指导书的提示,不难得出,我们的目标就是把进程的信息按照格式给弄进buf里面,就可以了。

                -

                而这也正体现了proc作为“**虚拟文件**”的特点。对它进行读写,它的信息并非存放在磁盘中,而是全部由放在内存中的逻辑和数据【由task_struct提供】来完成。

                -

                在fs文件夹下创建文件proc_dev.c,编写proc文件的处理函数。代码如下:

                -
                #include <linux/fs.h>
                #include <unistd.h>
                #include <asm/segment.h>
                #include <stdarg.h>
                #include <linux/sched.h>
                #include <sys/types.h>
                #include <linux/kernel.h>

                #define set_bit(nr,addr) ({\
                register int res ; \
                __asm__ __volatile__("btsl %2,%3\n\tsetb %%al": \
                "=a" (res):"0" (0),"r" (nr),"m" (*(addr))); \
                res;})

                struct task_struct** p=&FIRST_TASK;
                char s[100];
                int flag;

                extern int psinfo_handler(off_t* f_pos,char* buf);
                extern int hdinfo_handler(off_t* f_pos,char* buf);

                int proc_handler(unsigned short dev,char* buf,int count,off_t* f_pos){
                //根据设备编号,把不同的内容写入到用户空间的 buf
                switch(dev){
                case 0:
                return psinfo_handler(f_pos,buf);
                case 1:
                return hdinfo_handler(f_pos,buf);
                default:
                break;
                }
                return -1;
                }

                //在内核态和用户态间传递数据
                int put_into_buf(char* buf,char* s){
                int cnt=0;
                while(s[cnt]!='\0'){
                put_fs_byte(s[cnt++],buf++);
                }
                return cnt;
                }

                int sprintf(char* buf,const char* fmt,...){
                va_list args;int i;
                va_start(args,fmt);
                i=vsprintf(buf,fmt,args);
                va_end(args);
                return i;
                }

                int psinfo_handler(off_t* f_pos,char* buf){
                int i;
                //初始化字符串
                for(i=0;i<100;i++) s[i]=0;

                //如果是第一次read,需要在屏幕上打印列表头,并且重置p指针为进程队列头
                if((*f_pos)==0){
                sprintf(s,"pid\tstate\tfather\tcounter\tstart_time\n");
                p=&FIRST_TASK;
                }
                //到达文件末尾
                if((*p)==NULL){
                return 0;
                }

                //每次仅输出一行
                if((*f_pos)!=0){
                sprintf(s,"%ld\t%ld\t%ld\t%ld\t%ld\n",(*p)->pid,(*p)->state,(*p)->father,(*p)->counter,(*p)->start_time);
                p++;
                }

                int cnt=put_into_buf(buf,s);
                *f_pos+=cnt;

                return cnt;
                }

                //可参考fs/super.c mount_root()
                int hdinfo_handler(off_t* f_pos,char* buf){
                //防止循环多次打印
                if(flag==1){
                flag=0;
                return -1;
                }
                struct super_block* sb;
                sb=get_super(0x301);/*磁盘设备号 3*256+1*/

                int free=0;
                int i=sb->s_nzones;
                while (-- i >= 0)
                if (!set_bit(i&8191,sb->s_zmap[i>>13]->b_data))
                free++;
                sprintf(s,"total_blocks:\t%d\nfree_blocks:\t%d\nused_blocks:\t%d\n",sb->s_nzones,free,sb->s_nzones-free);
                int cnt=put_into_buf(buf,s);
                flag=1;
                return cnt;
                }

                +
                if(bp + bp->s.size == p->s.ptr){
                bp->s.size += p->s.ptr->s.size;
                bp->s.ptr = p->s.ptr->s.ptr;
                } else
                bp->s.ptr = p->s.ptr;
                if(p + p->s.size == bp){
                p->s.size += bp->s.size;
                p->s.ptr = bp->s.ptr;
                } else
                p->s.ptr = bp;
                freep = p;
                -

                运行结果:

                - +

                此时会进入第二个if的第一个分支。具体情况看图就行,不多bb。

                +

                总结

                主要就是这个数据结构用得很巧妙但也很复杂。它吸取了内核态中分配内存使用一个freelist的特点,同时又巧妙地利用了内存地址有序的特点,从而实现碎片内存管理。我的建议是多画图。

                +

                还有其实有一点我不是很理解。我觉得freep这个变量的用意非常不明,它似乎并不是指代整个freelist的头,因为它在很多个地方都诡异地赋值了一次。我想,它也许始终指向上一次被alloc/被free的内存的前一个吧。。。我猜测这样设计是为了蕴含一些LRU的思想。不大明白。

                +

                m-s-u权限切换

                由os知识可知,机器态、内核态、用户态分别有三种不同的操作权限。xv6是如何对权限切换进行管理的呢?

                +

                这部分知识我在正文的一个小地方记录了下来,详见 chapter2 - Code: starting xv6 and the fifirst process - xv6 - 感想 的第二点。

                +

                Lock实验的评测机制

                在xv6该次实验中,为了实现评测可视化,引入了statistics机制对结果进行评估。下面,我将通过源码简单介绍其实现机制。

                +

                来讲讲这玩意是怎么实现用户态读取锁争用次数的。我们从statistics函数可看出,它的本质是通过读取“文件”,来从内核中读取争用次数的相关数据:

                +
                int statistics(void *buf, int sz) {
                fd = open("statistics", O_RDONLY);
                ...
                if ((n = read(fd, buf+i, sz-i)) < 0) {
                }
                -

                这部分踩过的坑:

                1.LAST_TASK 的定义

                -

                对于LAST_TASK,我本来的理解是,当前所有进程的最后一个。

                -

                本来我设的是跟schedule一样,另p=LAST_TASK,从末尾开始打印。我那时其余代码跟上面一样,就只是把上面的FIRST改成LAST,结果输出为空,调试发现LAST_TASK==NULL。

                -

                然后打开sched.h,看到LAST_TASK的定义:

                -
                #define LAST_TASK task[NR_TASKS-1]
                +

                那么安装以前所学的内容,我们很容易联系到这玩意应该并不是个文件,而是类似于proc文件系统那样的虚拟文件。它应该会在open、read中根据其特有的文件类型进行转发。在init.c中,我们可以看到:

                +
                main(void)
                {
                if(open("console", O_RDWR) < 0){
                mknod("console", CONSOLE, 0);
                mknod("statistics", STATS, 0);
                open("console", O_RDWR);
                }
                -

                原来它就是单纯简单粗暴地指“最后一个”进程23333

                -

                我们目前当前的进程数量远远小于进程的最大数量,因此最大数量编号的那个进程自然也就是空的了。

                -

                2.char s[100]={0};

                -

                用这个的时候编译报错:undefined reference to ’memset‘

                -

                说明这个简略写法其实本质是用的memset,而要用memset的话需要包含头文件string.h。经测试得包含了string.h后确实就好使了。

                -
                //s_imap_blocks、ns_zmap_blocks、

                //total_blocks、free_blocks、used_blocks、total_inodes

                for(i=0;is_zmap_blocks;i++)

                {

                bh=sb->s_zmap[i];

                db=(char*)bh->b_data;

                for(j=0;j<1024;j++){

                for(k=1;k<=8;k++){

                if((used_blocks+free_blocks)>=total_blocks)

                break;

                if( *(db+j) & k)
                used_blocks++;

                else

                free_blocks++;

                }
                +

                这玩意的文件结点实际上是在创建console时整的,并且其有一个特殊的文件类型“STATS”。我们可以进一步追溯到kernel中的main.c

                +
                void main()
                {
                if(cpuid() == 0){
                #if defined(LAB_PGTBL) || defined(LAB_LOCK)
                statsinit();
                -

                3.我发现一件事

                -

                我第一次把init/main.c写错了,写成:

                -
                mkdir("/proc",0755);
                mknod("/proc/psinfo",S_IFPROC|0400,0);
                mknod("/proc/hdinfo",S_IFPROC|0400,0);
                mknod("/proc/inodeinfo",S_IFPROC|0400,0);
                +
                void
                statsinit(void)
                {
                initlock(&stats.lock, "stats");

                devsw[STATS].read = statsread;
                devsw[STATS].write = statswrite;
                }
                -

                设别号忘了改了。然后进行了一次编译,运行。

                -

                之后我发现错了,就改成了

                -
                mkdir("/proc",0755);
                mknod("/proc/psinfo",S_IFPROC|0400,0);
                mknod("/proc/hdinfo",S_IFPROC|0400,1);
                mknod("/proc/inodeinfo",S_IFPROC|0400,2);
                +

                可以看到,它给这个STATS文件类型注册了这两个函数。当我们调用read和write时,实际上就是在调用这俩玩意。我们可以看下这两个handler都干了啥。

                +
                #define BUFSZ 4096
                static struct {
                struct spinlock lock;
                char buf[BUFSZ];
                int sz;
                int off;
                } stats;

                int statsread(int user_dst, uint64 dst, int n) {
                int m;
                acquire(&stats.lock);

                if(stats.sz == 0) {
                #ifdef LAB_LOCK
                stats.sz = statslock(stats.buf, BUFSZ); // 把信息copy进自己的缓冲区里
                #endif
                }
                m = stats.sz - stats.off;

                if (m > 0) { // 如果有新东西,就copy到用户缓冲区里
                if(m > n) m = n;
                if(either_copyout(user_dst, dst, stats.buf+stats.off, m) != -1) {
                stats.off += m;
                }
                } else {
                m = -1;
                stats.sz = 0;
                stats.off = 0;
                }
                release(&stats.lock);
                return m;
                }
                -

                再次编译运行,结果上面的那个错还是没改回来

                -

                直到我手动把proc文件夹删了,再重新读一次磁盘加载proc文件夹,才回归正常。

                -

                感想

                本次实验耗时:下午一点到晚上九点半()

                -

                本实验通过对proc虚拟文件的编写流程,实际上让我们体会到了“一切皆文件”的思想。

                -

                什么东西都可以是文件,只不过它们有不同的文件类型和不同的read/write处理函数。

                -

                对于终端设备和磁盘,其read/write函数本质上是在用out指令跟它的缓冲区交互,只不过磁盘比终端设备抽象层次更深,包含了文件系统的层层封装。

                -

                对于虚拟文件,其read/write函数本质上就是与内存交互,通过一段逻辑【处理函数】将内存存储的当前操作系统信息实时显示出来,而不需要存储。

                -

                还有,参考文章那篇的代码写的很好,快去看!

                +
                int statswrite(int user_src, uint64 src, int n) { // WARNING: READ ONLY!!!
                return -1;
                }
                + +

                可以看到其本质就是把statslock返回的东西copy到用户空间了。我们来结合最后的输出效果看看statslock的具体实现:

                +

                image-20231024232632816

                +
                int statslock(char *buf, int sz) {
                int n;
                int tot = 0;
                int found = 0;

                acquire(&lock_locks);
                n = snprintf(buf, sz, "--- lock kmem/bcache stats\n");
                for(int i = 0; i < NLOCK; i++) {
                if(locks[i] == 0) break;
                if(strncmp(locks[i]->name, "bcache", strlen("bcache")) == 0 ||
                strncmp(locks[i]->name, "kmem", strlen("kmem")) == 0) {
                tot += locks[i]->nts; // 记入->nts计数
                /*
                snprint_lock: lock: %s: #fetch-and-add %d #acquire() %d\n
                */
                n += snprint_lock(buf +n, sz-n, locks[i]);
                found += 1;
                }
                }

                // Require at least two locks name after kmem/bcache.
                if (found < 2) {
                tot = -1;
                }

                // 简单粗暴地计算前五多争用的进程
                n += snprintf(buf+n, sz-n, "--- top 5 contended locks:\n");
                int last = 100000000;
                // stupid way to compute top 5 contended locks
                for(int t = 0; t < 5; t++) {
                int top = 0;
                for(int i = 0; i < NLOCK; i++) {
                if(locks[i] == 0)
                break;
                if(locks[i]->nts > locks[top]->nts && locks[i]->nts < last) {
                top = i;
                }
                }
                /*
                snprint_lock: lock: %s: #fetch-and-add %d #acquire() %d\n
                */
                n += snprint_lock(buf+n, sz-n, locks[top]);
                last = locks[top]->nts;
                }
                n += snprintf(buf+n, sz-n, "tot= %d\n", tot);
                release(&lock_locks);
                return n;
                }
                + +

                可以看到其争用本质计算是通过spinlock::nts字段记录。我们来看看这玩意的引用:

                +
                void initlock(struct spinlock *lk, char *name) {
                #ifdef LAB_LOCK
                lk->nts = 0;
                #endif
                }

                void acquire(struct spinlock *lk) {
                ...
                while(__sync_lock_test_and_set(&lk->locked, 1) != 0) {
                #ifdef LAB_LOCK
                __sync_fetch_and_add(&(lk->nts), 1);
                #else
                ;
                #endif
                }
                + +

                很好,逻辑很简单,就是记录acquire时等待的次数,非常简单粗暴(((

                +

                总的来说这个思路还是挺酷的,而且这个“一切皆文件”的思想再次震撼了我,一个小小的xv6确实能做到那么多。

                +]]> + + + 各种配环境中遇到的问题 + /2023/10/12/%E5%90%84%E7%A7%8D%E9%85%8D%E7%8E%AF%E5%A2%83%E4%B8%AD%E9%81%87%E5%88%B0%E7%9A%84%E9%97%AE%E9%A2%98/ + +
              6. 记录一次vm扩容

                +
              7. +
              8. 开发中遇到的链接小问题

                +
              9. +
              10. rtt硬件环境搭建

                +
              11. +
              12. 内核编译

                +
              13. +
              14. 防火墙

                +
                sudo ufw status numbered # 查看
                sudo ufw delete 数字 # 删除某条记录
                # 开放IP地址XX.XX.XX.XX的22 tcp端口
                sudo ufw allow from XX.XX.XX.XX to any port 22 proto tcp
              15. +
              ]]>
              - - labs -
              存储简单入门 @@ -12560,283 +12899,68 @@ url访问填写http://localhost/webdemo4_war/*.do

              iSCSI

              iSCSI将SCSI命令和数据(SCSI 协议)封装为IP包,通过TCP/IP传输。

              1. 主机连接

                -

                有标准NIC、TOE NIC、iSCSI HBA三种方案。

                -

                标准NIC就是标准网卡只提供IP层,TCP、iSCSI还需要os来处理;TOE NIC只卸载了TCP/IP 协议层,iSCSI还需要os来处理;iSCSI HBA一下全卸载了 iSCSI 协议层和TCP/IP 协议层,主机p都不用管。

                -
              2. -
              3. 拓扑结构

                -

                iSCSI的拓扑结构可分为两类:原生模式和桥接模式。

                -

                原生模式没有FC(光纤通信)组件。iSCSI 发起方可以直接连接到到目的方或通过 IP 网络连接到目的方;桥接模式通过提供iSCSI到FC的桥接功能以实现FC与IP共存。例如,iSCSI发起方可以在IP环境中,而存储设备仍然留在FC SAN 环境中。

                -

                image-20231004163418180

                -
              4. -
              -

              FCIP

              FCIP是一种隧道协议,使分散的FC SAN 孤岛通过现有的IP 网络进行互联。相当于就是在原本的FC SAN(光纤网络)之间加了个IP协议,把孤岛FC SAN联结起来。

              -

              image-20231004170009815

              -

              FCoE

              就相当于是把FC交换机和IP交换机组合起来了,减少服务器的端口。聚合网络适配器(CNA) 是一个结合了标准NIC 和FC HBA 两者功能的适配器,实现了两种流量的合并。有了CNA就不必配置两种适配器,它和线缆分别用于FC和以太网通信,减少了所需的服务器插槽数和交换机端口数。FCoE交换机同时具有以太网交换机和FC交换机的功能。

              -

              文件级别(NAS)

              网络链接存储(network-attached storage,NAS)

              -

              SAN是以块为单位进行收发数据的,NAS是以文件为单位。它允许用户通过局域网或互联网连接到存储设备并访问和管理其中的数据。NAS 协议有多种实现方式,包括 NFS、CIFS/SMB、FTP、HTTP 和 iSCSI 等。它是一种基于文件级别的存储方案,可以提供高效的数据共享和备份,并支持多用户同时访问。

              -

              涉及文件IO和块级IO的转化:

              -

              image-20231004220458822

              -

              网络文件共享的实例:

              -
                -
              1. FTP(File Transfer Protocol) 通过网络来传输数据的客户/服务器协议
              2. -
              3. DFS(Distributed File System) 提供了主机直接访问整个文件系统的能力。使用这种协议,客户端可载专用文件服务器上的远程文件系统
              4. -
              5. P2P 客户端之间可以直接进行文件共享。客户端使用的文件共享软件可搜索其他客户端。
              6. -
              -

              统一NAS提供文件服务,同时负责存储文件数据,并提供块级数据访问。它支持用于文件访问的CIFS和NFS协议,以及用于块级访问的SCSI和FC协议。

              -

              对象级别(OSD)

              基于对象的存储(object-based storage devices,OSD)

              -

              OSD以对象的形式存储数据。它使用扁平地址空间(flat address space)来存储数据。这种地址空间中没有目录和文件的分层,所以我们访问内容就会采用按内容寻址(CAS)的方法。CAS通常使用哈希值来唯一标识数据块,并通过维护哈希值与存储块物理地址之间的映射关系来实现查找。

              -

              image-20231004221709232

              -

              一个节点就是一台运行OSD操作系统、提供数据存储、获取和管理服务的服务器,负责维护对象ID与文件系统名称空间的映射。

              -

              image-20231006152123066

              -

              备份、归档与复制

              备份

              简介与基本方法

              备份有三个目的:灾难恢复、业务性恢复和归档。

              -

              三种形式:全备份、增量备份、累计备份。增量备份是相对于上一次的增量,累积备份是针对于上一次全备份的增量。

              -

              image-20231004223943465

              -

              image-20231004224250026

              -

              image-20231004224424502

              -

              也没什么好说的,就记得客户端就是发备份数据的,服务器端有点类似CPU,指挥客户端发数据、存储结点接数据,以及存储结点把数据存入备份设备。

              -

              备份环境中采用三种基础的拓扑结构:直接连接备份、基于局域网备份、基于SAN备份。

              -

              image-20231004224537060

              -

              相当于本来app和备份设备直连,现在改成网络连接,优化备份设备利用率:

              -

              image-20231004224635907

              -

              然而上面方法数据流太大,网络性能不行。所以我们只对元数据采用LAN,而对备份数据流采用更快的FC SAN:

              -

              image-20231004224911377

              -

              数据去重

              数据去重(data deduplication,又称重复数据删除)是识别重复数据并将其删除的过程。在备份时如果检测到重复数据,就会将其丢弃,然后创建指针指向已备份过的数据副本。

              -

              去重有两种方法:文件级或子文件级。

              -

              子文件去重将文件划分为更小的块,有两种实现形式:固定长度块和长度可变段。固定长度块去重将文件划分为固定长度的块,使用哈希算法找出重复的数据。

              -

              固定长度块虽然简单,但是可能会错过不少重复数据,因为相似数据的块边界可能不同;在长度可变段去重中,如果一个段中有变化,那么只有此段的边界被调整,剩余段不变。与固定块方法相比,这一方法大幅提升了识别重复数据段的能力。

              -

              复制

              本地复制

              这段话详细介绍了副本与备份的区别:

              -
                -
              1. 副本是可以立即被应用所使用,而备份件则需要通过备份软件恢复后才能被应用使用;
              2. -
              3. 备份必须是按时间点复制,而副本既可以按时间点复制,也可以连续复制;
              4. -
              5. 备份通常用于操作或者灾备,而副本除了用于恢复和重启之外,也可以用于其他操作,例如,备份、报表和测试;
              6. -
              7. 副本比从备份恢复的RTO更快。
              8. -
              -

              本章大概讲的就是本地复制,将产生副本的具体几种技术,如LVM镜像和文件系统镜像(基于本地主机)、全卷镜像指针全复制指针虚拟复制(基于存储阵列)、CDP连续数据保护(基于网络)。它们的共通思想就是使用COW/COA(access)、虚拟指针和全复制的差异,以及日志/位图。

              -

              远程复制

              有同步和异步之分,同步就是同时写源和目标,全部写完再响应;异步就是先写源,然后响应,然后再写目标。

              -

              具体的方法依然是有三个角度,LVM逻辑卷同步异步写和仅传输日志(基于主机)、基于磁盘缓存的远程复制(基于存储阵列)、远程CDP(相当于通过SAN连接本地CDP和远程CDP,基于网络)

              -

              image-20231005161420265

              -

              然后更常见的是使用三站点法。

              -
                -
              1. 直线型

                -

                源 (同步复制) 中间站点(异步复制/磁盘缓冲)远程站点

                -
              2. -
              3. 三角型

                -

                image-20231006152227664

                -
              4. -
              -]]> - - books - -
              - - 状态机 - /2023/03/10/%E5%AF%B9moore%E5%9E%8B%E5%92%8Cmealy%E5%9E%8B%E7%8A%B6%E6%80%81%E6%9C%BA%E7%9A%84%E7%90%86%E8%A7%A3/ - 复习数电时,一道密码锁题令我十分不解:

              -image-20230213201519568 - -

              看到题目时,我首先联想到的是mealy型状态机,因为我联想到了序列检测。课内的序列检测讲的时候是把它当做mealy型的。但看了标准作答之后,才发现它其实应该是moore型。这让我对这二者的区别产生了深深的不解。

              -

              原来对于moore型状态机和mealy型状态机的理解仅仅停留在概念上,“moore型状态机的输出与输入无关,只与当前状态有关”“mealy型状态机输出与输入和现态都有关”。但这其实是一句非常抽象的话:什么是“无关”,什么是“有关”?moore型状态机的状态不也是依据输入进行转移的吗?那么这算不算“有关”?

              -

              探究之后,我得到了更精确的“有关”“无关”的定义。

              -
              -

              来自:Moore状态机和Mealy状态机的区别

              -image-20230213202110900 - -image-20230213202124943 -
              -
              -

              来自:Moore状态机和Mealy状态机的区别(以序列检测器为例)

              -

              Moore状态机输出只与此时的状态有关,因此假如需要检测宽度为4的序列,则需要五个状态

              -

              Mealy状态机输出与此时的状态以及输入有关,因此假如需要检测宽度为4的序列,只需要四个状态即可。

              -
              -

              联想到我们课上学习的序列检测:

              -image-20230213202320825 - -

              它这明明长度为3的序列用了4个状态,应该算是moore型,为什么我们却被教说序列检测器是mealy型状态机呢?

              -

              原因是因为,我们进行了状态化简这一步,将moore型状态机转化为了mealy型状态机

              -
              -

              这俩是可以相互转化的

              -

              来自:[转载][FPGA]有限状态机FSM学习笔记(二)

              -

              把Moore机转换为Mealy机的办法为,把次态的输出修改为对应现态的输出,同时合并一些具有等价性能的状态。把Mealy机转换为Moore机的办法是,把当前态的输出修改为对应次态的输出,同时添加一些状态。如图1所示,为把Mealy机状态图转化为Moore机状态图。

              -

              img
                      图1  Mealy型机转换为Moore型机

              -

                如图1所示,把Mealy型机转换为Moore型机,只要把现时输出改变为下一时刻输出。对于状态A,有4个箭头指向它,表示在当前状态下有4个状态可以转换为下一状态的A;同时当前输出均为0,可以把0移入状态A内部,表示在Moore机中状态A的输出为0。同理,可以把0分别移位B/C状态。但对于状态D,有两个箭头指向且具有不同的输出值,需要把状态D分解成两个状态D1和D2(每个状态对应一个输出,当输出不同需要利用不同的状态表示,这即是Moore机具有更多状态的原因),得到完整的Moore机状态模型。

              -

                同理,若把上图的Moore机转换为Mealy机,只要把Moore机中下一状态的输出改变成Mealy机中当前状态的输出,由于D1/D2两状态处于A/C两状态之间,且相当于A/C节点之间的一个等效节点,可以把D1/D2两状态合并为一个状态。

              -
              -
              -

              来自:Moore型状态机和Mealy型状态机

              -

              并非所有时序电路都可以使用Mealy模型实现。 一些时序电路只能作为摩尔机器实现。

              -
              -

              所以,我们可以出此暴论:在课程范围内,首先以moore的思想来设计状态机。如果该状态机可以被化简,那么这道题就要用mealy型的来做;如果不能,那么这道题就是得用moore型状态机来做。

              -

              一开始的那个时序锁的moore状态机不能化简,因此它是moore型。

              -
              -

              这个点本来可以讲得更清楚一些的……只教会我们做题的套路有啥意思呢←_←

              -
              -]]>
              -
              - - 对GRUB和initramfs的小探究 - /2023/06/17/%E5%AF%B9GRUB%E5%92%8Cinitramfs%E7%9A%84%E5%B0%8F%E6%8E%A2%E7%A9%B6/ - 竞赛时对操作系统启动过程产生了些疑问,于是问题导向地浅浅探究了下GRUB和initramfs相关机制,相关笔记先放在这里了。

              -

              内核启动流程

              在传统的BIOS系统中,计算机具体的启动流程如下:

              -
                -
              1. 电源启动:当计算机的电源打开时,电源供电给计算机的硬件设备。
              2. -
              3. BIOS自检:计算机的BIOS固件会自检硬件设备,包括RAM、处理器、硬盘等,以确保它们正常工作。
              4. -
              5. 引导设备选择:BIOS会根据预先定义的启动顺序(通常是硬盘、光驱、USB等)选择一个启动设备。
              6. -
              7. MBR(Master Boot Record)加载:如果选择的启动设备是硬盘,BIOS会加载该硬盘的MBR,其中包含了引导加载程序。
              8. -
              9. GRUB加载:MBR中的引导加载程序通常是GRUB(或其他引导加载程序)。GRUB会被加载到计算机的内存中,并开始执行。
              10. -
              11. GRUB菜单:GRUB会显示一个菜单,列出可供选择的操作系统或内核。
              12. -
              13. 操作系统加载:用户选择操作系统后,GRUB会加载相应的操作系统或内核,并将控制权交给它。
              14. -
              -

              在本次内核编译配置过程中,最主要探究的是文件系统的装载过程,也即介于6-7之间的部分。

              -

              概述

              文件系统在启动流程中的发展历程可以分为以下三个部分:

              -
                -
              1. GRUB文件系统

                -

                由 GRUB 自身通过 BIOS 提供的服务加载

                -
              2. -
              3. initramfs

                -

                由GRUB加载,用于挂载真正的文件系统

                -
              4. -
              5. 真正的根文件系统

                -
              6. -
              -

              下面,将介绍1和2两个流程。

              -

              GRUB

              -

              GRUB(GNU GRand Unified Bootloader)是一种常用的引导加载程序,用于在计算机启动时加载操作系统。

              -

              GRUB的主要功能是在计算机启动时提供一个菜单,让用户选择要启动的操作系统或内核。它支持多个操作系统,包括各种版本的Linux、Windows、BSD等。通过GRUB,用户可以在多个操作系统之间轻松切换。

              -

              除了操作系统选择,GRUB还提供了一些高级功能,例如引导参数的设置、内存检测、系统恢复等。它还支持在启动过程中加载内核模块和初始化RAM磁盘映像(initrd或initramfs)。

              -

              GRUB具有高度可配置性,允许用户自定义引导菜单、设置默认启动项、编辑内核参数等。它还支持引导加载程序间的链式引导,可以引导其他引导加载程序,如Windows的NTLDR。

              -
              -

              GRUB的基本作用流程为:

              -
                -
              1. BIOS加载MBR,MBR加载GRUB,开始执行GRUB程序
              2. -
              3. GRUB程序会读取grub.cfg配置文件
              4. -
              5. GRUB程序依据配置文件,进行内核的加载、根文件系统的挂载等操作,最后将主导权转交给内核
              6. -
              -

              grub.cfg

              内核启动时,GRUB程序会读取/boot/grub/目录下的GRUB配置文件grub.cfg,其中记录了所有GRUB菜单可供选择的内核选项(menuentry)及其对应的启动依赖参数。以6.4.0内核选项为例:

              -
              # menuentry标识着GRUB菜单中的一个内核选项
              menuentry 'Ubuntu' --class ubuntu --class gnu-linux --class gnu --class os $menuentry_id_option 'gnulinux-simple-XXX' {
              recordfail # 记录上次启动是否失败,用于处理启动失败的情况
              load_video # 加载视频驱动模块,用于在启动过程中显示图形界面
              gfxmode $linux_gfx_mode # 设置图形模式
              insmod gzio # 加载gzio模块,提供对GZIP压缩和解压缩功能的支持
              # 如果是在Xen虚拟化平台上,则加载xzio和lzopio模块
              if [ x$grub_platform = xxen ]; then insmod xzio; insmod lzopio; fi

              insmod part_gpt # 加载part_gpt模块,支持GUID分区表(GPT)
              insmod ext2 # 加载ext2模块,支持ext2文件系统

              # 设置文件系统的根分区
              set root='hd0,gpt3'
              if [ x$feature_platform_search_hint = xy ]; then
              search --no-floppy --fs-uuid --set=root --hint-bios=hd0,gpt3 --hint-efi=hd0,gpt3 --hint-baremetal=ahci0,gpt3 XXX
              else
              search --no-floppy --fs-uuid --set=root XXX
              fi

              linux /boot/vmlinuz-6.4.0-rc3+ root=UUID=XXX ro text # 指定内核映像的路径和启动参数
              initrd /boot/initrd.img-6.4.0-rc3+ # 指定initramfs映像的路径
              }
              - -

              可以看到,grub.cfg主要记录了一些该内核启动需要的依赖module,以及内核映像和initramfs映像的路径

              -

              menuentry的代码中,有以下几个要点值得注意:

              -
                -
              1. insmod gzio

                -

                由于加载gzio模块,提供对GZIP压缩和解压缩功能的支持。

                -

                看到这里我第一反应是觉得有点割裂,为啥这看着比较无关紧要的解压缩功能要在内核启动之前就需要有呢?于是我想起来在配置内核时,有一个选项是这样的:

                -

                image-20230616143835953

                -

                在配置选项中,我们选择了对initramfs的支持,并且勾选了Support initial ramdisk/ramfs compressed using gzip ,也即在编译时通过gzip压缩initramfs的大小以节省空间。

                -

                所以说,我们在内核启动之前,持有的initramfs处于被压缩的状态。故而,我们自然需要在内核启动之前安装gzio模块,从而支持之后对initramfs的解压缩了。

                -
              2. -
              3. insmod ext2

                -

                这句代码说明,GRUB的临时文件系统为ext2类型,这句代码事实上是在安装GRUB建立临时文件的必要依赖包,从而GRUB程序之后才能建立其临时文件系统、从/boot/initrd.img获取initramfs映像。

                -
              4. -
              5. linux /boot/vmlinuz-6.4.0-rc3+ root=UUID=XXX ro text

                -

                指定了启动参数,也即将根文件系统以只读(ro)的方式挂载在root=UUID=XXX对应的块设备上,并且默认以text方式(也即非图形化的Shell界面)启动内核。

                -

                此处的启动参数可在下一个部分介绍的grub文件中个性化。

                -
              6. -
              -

              grub.cfg的生成与修改

              实际运用中,很多时候需要对启动参数进行一些修改。下面介绍两种修改grub.cfg的方法。

              -

              /etc/default/grub

              可以看到,grub.cfg其实格式较为固定(也即由一系列内容也比较相似的menuentry构成)。因而,实际上我们是通过grub.d生成grub.cfg的(6.S081实验中事实上也涉及了这一点),而/etc/default/grub则是GRUB程序以及grub.cfg生成的配置文件。下面介绍下该文件主要有哪些配置选项。

              -
              # If you change this file, run 'update-grub' afterwards to update
              # /boot/grub/grub.cfg.
              # For full documentation of the options in this file, see:
              # info -f grub -n 'Simple configuration'

              # 开机时GRUB界面的持续时间,此处设置为30s
              GRUB_TIMEOUT=30
              GRUB_CMDLINE_LINUX=""

              # 不使用图形化界面
              #GRUB_TERMINAL=console
              # 图形化界面的大小
              #GRUB_GFXMODE=640x480
              # 不使用UUID
              #GRUB_DISABLE_LINUX_UUID=true

              # 隐藏recovery mode
              #GRUB_DISABLE_RECOVERY="true"
              - -

              重点看下这几个参数:

              -
                -
              1. GRUB_CMDLINE_LINUX

                -

                表示最终生成的grub.cfg中的每一个menuentry中的linux那一行需要附加什么参数。

                -

                例如说,如果设置为:

                -
                # 表示initramfs在挂载真正的根文件系统之前,需要等待120s,用于防止磁盘没准备好导致的挂载失败
                GRUB_CMDLINE_LINUX="rootdelay=120"
                - -

                那么,最终在menuentry中的启动参数就为:

                -
                linux   /boot/vmlinuz-6.4.0-rc3+ root=UUID=XXX ro rootdelay=120 text
                - -

                其他一些常见的选项:

                -
                # 直接以路径来标识块设备而非使用UUID。此为old option,建议尽量使用UUID
                GRUB_CMDLINE_LINUX="root=/dev/sda3"
                # 标明init进程(启动后第一个进程)的具体路径。此处指明为`/bin/sh`
                GRUB_CMDLINE_LINUX="init=/bin/sh"
              2. -
              3. GRUB_DEFAULT

                -

                参考 可以用来指定重启时的内核选项。如GRUB_DEFAULT="1> 0"表示选择第一个菜单界面的第2栏(Advanced for Ubuntu)和第二个菜单的第1个内核。

                -
              4. -
              -

              在修改完grub文件之后,我们需要执行sudo update-grub,来重新生成grub.cfg文件供下次启动使用。

              -

              在GRUB界面直接修改

              image-20230616151055620

              -

              我们可以在GRUB界面选中所需内核,按下e键:

              -

              image-20230616151122738

              -

              然后就可以对启动参数进行修改,^X退出。

              -

              值得注意的是,此修改仅对本次启动有效。如果需要长期修改,建议还是通过第一种方法去修改。

              -

              initramfs

              GRUB程序会通过initrd.img启动initramfs,从而进行真正的根文件系统挂载。

              -
              -

              initrd.img是一个Linux系统中的初始化内存盘(initial RAM disk)的映像文件。它是一个压缩的文件系统映像,通常在引导过程中加载到内存中,并提供了一种临时的根文件系统,以便在正式的根文件系统(通常位于硬盘上)可用之前提供必要的功能和模块。

              -
              -

              我们可以通过unmkinitramfs /boot/initrd.img-6.4.0-rc3+ /tmp/initrd/命令解压initrd,探究里面到底有什么玩意。

              -
              ├── bin -> usr/bin
              ├── conf
              ├── etc
              ├── init
              ├── lib -> usr/lib
              ├── lib32 -> usr/lib32
              ├── lib64 -> usr/lib64
              ├── libx32 -> usr/libx32
              ├── run
              ├── sbin -> usr/sbin
              ├── scripts
              ├── usr
              └── var
              init
              - -

              可以看到,这实际上就是一个小型的文件系统,也即initramfs。它有自己的built-in Shell(BusyBox):

              -

              image-20230616151938951

              -

              有一些较少的Shell命令(bin和sbin目录下),以及用来挂载真正的根文件系统的代码逻辑(存储在scripts目录下)。【我猜】在正常情况下,系统会执行scripts下的脚本代码挂载真正的文件系统。当挂载出现异常时,系统就会将控制权交给initramfs内置的Shell BusyBox,由用户自己探究出了什么问题。

              -

              我们接下来可以追踪下initramfs的script目录下的文件系统挂载流程。

              -

              挂载真正文件系统的主要函数为local_mount_root

              -
              # 仅展示主要流程代码
              local_mount_root()
              {
              # 预处理,获取参数等(也即上面grub.cfg配置的root=UUID)
              local_top
              if [ -z "${ROOT}" ]; then
              panic "No root device specified. Boot arguments must include a root= parameter."
              fi

              # 根据UUID获取对应的块设备
              local_device_setup "${ROOT}" "root file system"
              ROOT="${DEV}"

              # 挂载前的预处理
              local_premount

              # 挂载
              mount ${roflag} ${FSTYPE:+-t "${FSTYPE}"} ${ROOTFLAGS} "${ROOT}" "${rootmnt?}"
              }
              - -

              由于研究这个是错误驱动(乐),因而我只主要看了下local_device_setup

              -
              # $1=device ID to mount设备ID
              # $2=optionname (for root and etc)要挂载的是什么玩意,此处应为root file system
              # $3=panic if device is missing (true or false, default: true)
              # Sets $DEV to the resolved device node $DEV是最终获取到的块设备
              local_device_setup()
              {
              local dev_id="$1"
              local name="$2"
              local may_panic="${3:-true}"
              local real_dev
              local time_elapsed
              local count

              # 获取grub.cfg的rootdelay参数的设备等待时间。如果没有该参数,默认是30秒
              local slumber=30
              if [ "${ROOTDELAY:-0}" -gt $slumber ]; then
              slumber=$ROOTDELAY
              fi

              # 等待设备
              case "$dev_id" in
              UUID=*|LABEL=*|PARTUUID=*|/dev/*)
              FSTYPE=$( wait-for-root "$dev_id" "$slumber" )
              ;;
              *)
              wait_for_udev 10
              ;;
              esac

              # 等待结束了。如果条件为真,说明还是获取不到对应的设备,那就只能说明这个设备死了
              # 所以我们就得把问题告诉用户,让用户自己解决,并且进入BusyBox Shell
              # We've given up, but we'll let the user fix matters if they can
              while ! real_dev=$(resolve_device "${dev_id}") ||
              ! get_fstype "${real_dev}" >/dev/null; do
              if ! $may_panic; then
              echo "Gave up waiting for ${name}"
              return 1
              fi
              echo "Gave up waiting for ${name} device. Common problems:"
              echo " - Boot args (cat /proc/cmdline)"
              echo " - Check rootdelay= (did the system wait long enough?)"
              if [ "${name}" = root ]; then
              echo " - Check root= (did the system wait for the right device?)"
              fi
              echo " - Missing modules (cat /proc/modules; ls /dev)"
              panic "ALERT! ${dev_id} does not exist. Dropping to a shell!"
              done

              DEV="${real_dev}"
              }
              - -

              可以看到,这里如果进入错误状态,最终就是这样的效果2333:

              -

              image-20230616153420011

              -]]>
              - - os竞赛 - -
              - - 网络是怎样连接的 - /2023/10/06/%E7%BD%91%E7%BB%9C%E6%98%AF%E6%80%8E%E6%A0%B7%E8%BF%9E%E6%8E%A5%E7%9A%84/ - -

              此为《信息存储与管理(第二版):数字信息的存储、管理和保护》的看书总结,相当于是对存储技术的一个简单的名词入门。

              -
              -

              浏览器生成消息

              本章节我印象最深的还是以前就不大了解的DNS,今天看到书的描写真有种豁然开朗的感觉。

              -

              DNS服务器用于保存域名—IP地址的映射对,为了增加查找效率,DNS根据域名的分级采用树形组织,例如hitsz.edu.cn/可以相当于是/cn/edu/hitsz,包含了/cnedu这几个域。根DNS服务器存储着根域,记录了所有一级域名对应DNS服务器的IP地址。所有的DNS服务器都会保存根服务器的IP地址。

              -
              -

              世界上只有13个根DNS服务器IP地址,但是有很多台根DNS服务器。

              -
              -

              主机需要手动配置DNS服务器地址。

              -

              当浏览器需要填写请求头时,它需要通过系统调用向操作系统发送DNS查询请求。操作系统将DNS请求发送给配置在主机上的DNS服务器(下称A),A再向根DNS服务器发送请求。根DNS服务器解析域名,返回下一级DNS服务器的IP地址。A再向下级DNS服务器再次发送请求,下级再返回下下级IP地址。以此类推,最终A就能得到目标IP地址的正确响应。整个过程如下图所示:

              -

              image-20231010132718116

              -

              与此同时,各个DNS服务器都会有定时刷新的缓存,从而加速了查找效率。

              -

              用电信号传输TCP/IP数据

              TCP/IP

              本章前面大多讨论TCP/IP具体协议内容,以前已经了解过很多次了就不多赘述。所以TCP/IP部分就以分点的形式随意列举一下:

              -
                -
              1. IP 中还包括 ICMPA 协议和 ARPB 协议。ICMP 用于告知网络包传送过程中产生的错误以及各种控制消息,ARP 用于根据 IP 地址查询相应的以太网 MAC 地址。

                -
              2. -
              3. 套接字中记录了用于控制通信操作的各种控制信息,协议栈则需要根据这些信息判断下一步的行动,【包括应用程序信息和协议栈状态信息】这就是套接字的作用。所以需要针对不同协议栈实现不同的socket。

                +

                有标准NIC、TOE NIC、iSCSI HBA三种方案。

                +

                标准NIC就是标准网卡只提供IP层,TCP、iSCSI还需要os来处理;TOE NIC只卸载了TCP/IP 协议层,iSCSI还需要os来处理;iSCSI HBA一下全卸载了 iSCSI 协议层和TCP/IP 协议层,主机p都不用管。

              4. -
              5. 是的,回想当初CS144,也是socket来负责有特定消息时调用TCP相关函数来通知处理。

                +
              6. 拓扑结构

                +

                iSCSI的拓扑结构可分为两类:原生模式和桥接模式。

                +

                原生模式没有FC(光纤通信)组件。iSCSI 发起方可以直接连接到到目的方或通过 IP 网络连接到目的方;桥接模式通过提供iSCSI到FC的桥接功能以实现FC与IP共存。例如,iSCSI发起方可以在IP环境中,而存储设备仍然留在FC SAN 环境中。

                +

                image-20231004163418180

              7. -
              8. 连接 connect

                -

                连接实际上是通信双方交换控制信息,在套接字中记录这些必要信息并准备数据收发的一连串操作。

                +
              +

              FCIP

              FCIP是一种隧道协议,使分散的FC SAN 孤岛通过现有的IP 网络进行互联。相当于就是在原本的FC SAN(光纤网络)之间加了个IP协议,把孤岛FC SAN联结起来。

              +

              image-20231004170009815

              +

              FCoE

              就相当于是把FC交换机和IP交换机组合起来了,减少服务器的端口。聚合网络适配器(CNA) 是一个结合了标准NIC 和FC HBA 两者功能的适配器,实现了两种流量的合并。有了CNA就不必配置两种适配器,它和线缆分别用于FC和以太网通信,减少了所需的服务器插槽数和交换机端口数。FCoE交换机同时具有以太网交换机和FC交换机的功能。

              +

              文件级别(NAS)

              网络链接存储(network-attached storage,NAS)

              +

              SAN是以块为单位进行收发数据的,NAS是以文件为单位。它允许用户通过局域网或互联网连接到存储设备并访问和管理其中的数据。NAS 协议有多种实现方式,包括 NFS、CIFS/SMB、FTP、HTTP 和 iSCSI 等。它是一种基于文件级别的存储方案,可以提供高效的数据共享和备份,并支持多用户同时访问。

              +

              涉及文件IO和块级IO的转化:

              +

              image-20231004220458822

              +

              网络文件共享的实例:

                -
              1. 应用程序向协议栈传ip地址
              2. -
              3. 本机向服务器发通信请求
              4. -
              5. 过程中分配通信缓冲区
              6. +
              7. FTP(File Transfer Protocol) 通过网络来传输数据的客户/服务器协议
              8. +
              9. DFS(Distributed File System) 提供了主机直接访问整个文件系统的能力。使用这种协议,客户端可载专用文件服务器上的远程文件系统
              10. +
              11. P2P 客户端之间可以直接进行文件共享。客户端使用的文件共享软件可搜索其他客户端。
              - -
            5. 动态调整等待时间

              -

              image-20231012113725655

              -
            6. +

              统一NAS提供文件服务,同时负责存储文件数据,并提供块级数据访问。它支持用于文件访问的CIFS和NFS协议,以及用于块级访问的SCSI和FC协议。

              +

              对象级别(OSD)

              基于对象的存储(object-based storage devices,OSD)

              +

              OSD以对象的形式存储数据。它使用扁平地址空间(flat address space)来存储数据。这种地址空间中没有目录和文件的分层,所以我们访问内容就会采用按内容寻址(CAS)的方法。CAS通常使用哈希值来唯一标识数据块,并通过维护哈希值与存储块物理地址之间的映射关系来实现查找。

              +

              image-20231004221709232

              +

              一个节点就是一台运行OSD操作系统、提供数据存储、获取和管理服务的服务器,负责维护对象ID与文件系统名称空间的映射。

              +

              image-20231006152123066

              +

              备份、归档与复制

              备份

              简介与基本方法

              备份有三个目的:灾难恢复、业务性恢复和归档。

              +

              三种形式:全备份、增量备份、累计备份。增量备份是相对于上一次的增量,累积备份是针对于上一次全备份的增量。

              +

              image-20231004223943465

              +

              image-20231004224250026

              +

              image-20231004224424502

              +

              也没什么好说的,就记得客户端就是发备份数据的,服务器端有点类似CPU,指挥客户端发数据、存储结点接数据,以及存储结点把数据存入备份设备。

              +

              备份环境中采用三种基础的拓扑结构:直接连接备份、基于局域网备份、基于SAN备份。

              +

              image-20231004224537060

              +

              相当于本来app和备份设备直连,现在改成网络连接,优化备份设备利用率:

              +

              image-20231004224635907

              +

              然而上面方法数据流太大,网络性能不行。所以我们只对元数据采用LAN,而对备份数据流采用更快的FC SAN:

              +

              image-20231004224911377

              +

              数据去重

              数据去重(data deduplication,又称重复数据删除)是识别重复数据并将其删除的过程。在备份时如果检测到重复数据,就会将其丢弃,然后创建指针指向已备份过的数据副本。

              +

              去重有两种方法:文件级或子文件级。

              +

              子文件去重将文件划分为更小的块,有两种实现形式:固定长度块和长度可变段。固定长度块去重将文件划分为固定长度的块,使用哈希算法找出重复的数据。

              +

              固定长度块虽然简单,但是可能会错过不少重复数据,因为相似数据的块边界可能不同;在长度可变段去重中,如果一个段中有变化,那么只有此段的边界被调整,剩余段不变。与固定块方法相比,这一方法大幅提升了识别重复数据段的能力。

              +

              复制

              本地复制

              这段话详细介绍了副本与备份的区别:

              +
                +
              1. 副本是可以立即被应用所使用,而备份件则需要通过备份软件恢复后才能被应用使用;
              2. +
              3. 备份必须是按时间点复制,而副本既可以按时间点复制,也可以连续复制;
              4. +
              5. 备份通常用于操作或者灾备,而副本除了用于恢复和重启之外,也可以用于其他操作,例如,备份、报表和测试;
              6. +
              7. 副本比从备份恢复的RTO更快。
              -

              以太网

                -
              1. 以太网的定义

                -

                image-20231012113840636

                -
              2. -
              3. 系统初始化时MAC地址的设置

                -

                MAC地址是上电后由驱动程序从ROM中读取的,而非自动获取的

                -
              4. -
              5. 电信号转换【这个帅得不行】

                -

                为了区分连续的1或0,我们就需要同时发送数据信号和时钟信号,然而这样开销太大,因而我们引入了上升沿

                -

                上升沿本质上是数据信号和时钟信号叠加而成的结果,叠加方式是异或

                -

                image-20231012114006378

                -

                提到异或是否感觉豁然开朗?是的,这东西恢复时也是使用了异或的性质:接收方从帧头获取时钟频率从而得到时钟信号,跟收到的叠加信号进行再次叠加(异或),就可以获得原来的数据信号了。

                -

                我只能说牛逼,一直以来对异或的视角还停留在单纯的数字,这个波形的物理概念真的惊到我了。

                -

                实例:

                -

                image-20231012114350599

                -
              6. -
              7. 半双工模式【同一时刻只能进行发or收】使用集线器,全双工模式【发or收可以并行】使用交换机。半双工模式需要进行载波监听碰撞检测。

                +

                本章大概讲的就是本地复制,将产生副本的具体几种技术,如LVM镜像和文件系统镜像(基于本地主机)、全卷镜像指针全复制指针虚拟复制(基于存储阵列)、CDP连续数据保护(基于网络)。它们的共通思想就是使用COW/COA(access)、虚拟指针和全复制的差异,以及日志/位图。

                +

                远程复制

                有同步和异步之分,同步就是同时写源和目标,全部写完再响应;异步就是先写源,然后响应,然后再写目标。

                +

                具体的方法依然是有三个角度,LVM逻辑卷同步异步写和仅传输日志(基于主机)、基于磁盘缓存的远程复制(基于存储阵列)、远程CDP(相当于通过SAN连接本地CDP和远程CDP,基于网络)

                +

                image-20231005161420265

                +

                然后更常见的是使用三站点法。

                +
                  +
                1. 直线型

                  +

                  源 (同步复制) 中间站点(异步复制/磁盘缓冲)远程站点

                2. -
                3. 服务器的操作系统具备和路由器相同的包转发功能,当打开这一功能时,它就可以像路由器一样对包进行转发。在这种情况下,当收到不是发给自己的包的时候,就会像路由器一样执行包转发操作。

                  +
                4. 三角型

                  +

                  image-20231006152227664

                ]]> @@ -12845,723 +12969,538 @@ url访问填写http://localhost/webdemo4_war/*.do
              8. - File system - /2023/01/10/xv6$chap8/ - File system
                -

                来到指导书最高点!太美丽了xv6。哎呀那不文件系统吗(

                -

                这里是自底向上讲起的。之后可以看看hit网课的自顶向下。

                -
                -
                -

                image-20230121160555370

                -
                -

                Overview

                image-20230121160641718

                -
                -

                The disk layer reads and writes blocks on an virtio hard drive.

                -

                The buffer cache layer caches disk blocks and synchronizes access to them, making sure that only one kernel process at a time can modify the data stored in any particular block.

                -

                The logging layer allows higher layers to wrap updates to several blocks in a transaction, and ensures that the blocks are updated atomically in the face of crashes (i.e., all of them are updated or none). 【日志记录层允许更高层将更新包装到一个事务中的多个块,并确保在崩溃时以原子方式更新块(即,所有块都更新或不更新)。可以类比一下数据库的那个概念。】

                -

                The inode layer provides individual files, each represented as an inode with a unique i-number and some blocks holding the file’s data.

                -

                The directory layer implements each directory as a special kind of inode whose content is a sequence of directory entries, each of which contains a file’s name and i-number.

                -

                The pathname layer provides hierarchical path names like /usr/rtm/xv6/fs.c, and resolves them with recursive lookup.

                -

                The file descriptor layer abstracts many Unix resources (e.g., pipes, devices, fifiles, etc.) using the file system interface, simplifying the lives of application programmers.

                -
                -

                image-20230121162324747

                -
                -

                The file system must have a plan for where it stores inodes and content blocks on the disk. To do so, xv6 divides the disk into several sections, as Figure 8.2 shows.

                -

                The file system does not use block 0 (it holds the boot sector).

                -

                Block 1 is called the superblock; it contains metadata about the file system (the file system size in blocks, the number of data blocks, the number of inodes, and the number of blocks in the log). The superblock is filled in by a separate program, called mkfs, which builds an initial file system.

                -

                Blocks starting at 2 hold the log.

                -

                After the log are the inodes, with multiple inodes per block.

                -

                After those come bitmap blocks tracking which data blocks are in use. 【应该是用来标识每个块是否空闲的吧】

                -

                The remaining blocks are data blocks; each is either marked free in the bitmap block, or holds content for a file or directory【要么空闲要么是文件或目录】.

                + 哈工大操作系统实验 + /2022/10/04/%E5%93%88%E5%B7%A5%E5%A4%A7%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E5%AE%9E%E9%AA%8C/ + +

                实验入口
                主要参考文章
                lseek()函数:用于移动打开文件的指针
                linux系统调用之write源码解析(基于linux0.11)
                get_fs_bytes解析
                VIM与系统剪贴板的复制粘贴
                操作系统实验六 信号量的实现和应用(哈工大李治军)
                哈工大操作系统实验6 信号量的实现 pc.c 编译时报错 对‘sem_open‘未定义的引用
                Linux 文件编程 open函数
                哈工大-操作系统-HitOSlab-李治军-实验5-信号量的实现和应用

                -

                Buffer cache

                -

                The buffer cache has two jobs:

                -
                  -
                1. synchronize access to disk blocks to ensure that only one copy of a block is in memory and that only one kernel thread at a time uses that copy;
                2. -
                3. cache popular blocks so that they don’t need to be re-read from the slow disk.
                4. -
                -

                The code is in bio.c.

                -

                Buffer cache中保存磁盘块的缓冲区数量固定,这意味着如果文件系统请求还未存放在缓存中的块,Buffer cache必须回收当前保存其他块内容的缓冲区。Buffer cache为新块回收最近使用最少的缓冲区。这样做的原因是认为最近使用最少的缓冲区是最不可能近期再次使用的缓冲区。

                +

                地址映射与共享

                +

                参考文章

                +

                Linux进程间通信(六):共享内存 shmget()、shmat()、shmdt()、shmctl()

                +

                操作系统实验七 地址映射与共享(哈工大李治军)

                -

                image-20230124151719288

                -

                数据结构定义

                struct buf {
                int valid; // has data been read from disk?缓冲区是否包含块的副本
                int disk; // does disk "own" buf?缓冲区内容是否已交给磁盘
                uint dev;
                uint blockno;
                struct sleeplock lock;
                uint refcnt;
                struct buf *prev; // LRU cache list
                struct buf *next;
                uchar data[BSIZE];
                };
                +

                必备知识

                要点1 共享内存

                顾名思义,共享内存就是允许两个不相关的进程访问同一个逻辑内存。共享内存是在两个正在运行的进程之间共享和传递数据的一种非常有效的方式。不同进程之间共享的内存通常安排为同一段物理内存。如果某个进程向共享内存写入数据,所做的改动将立即影响到可以访问同一段共享内存的任何其他进程。

                +

                注:共享内存并未提供同步机制,所以我们需要用信号量来实现同步。

                +

                Linux提供了一组接口用于使用共享内存,它们声明在头文件 sys/shm.h 中。

                +

                1.shmget

                程序先通过调用shmget()函数并提供一个键,再由系统生成一个相应的共享内存标识符(shmget()函数的返回值)。

                +
                int shmget(key_t key, size_t size, int shmflg);
                -

                这应该代表着一个磁盘块。

                -
                struct {
                struct spinlock lock;
                struct buf buf[NBUF];

                // Linked list of all buffers, through prev/next.
                // Sorted by how recently the buffer was used.
                // head.next is most recent, head.prev is least.
                struct buf head;
                } bcache;
                +

                key为共享内存段名字,size为大小,shmflg是权限标志

                +

                注:

                +

                ① key:非0整数,共享内存段的命名

                +

                ② shmflag:作用与open函数的mode参数一样,比如IPC_CREAT,或连接

                +

                共享内存的权限标志与文件的读写权限一样,举例来说,0644表示允许一个进程创建的共享内存被内存创建者所拥有的进程向共享内存读取和写入数据,同时其他用户创建的进程只能读取共享内存

                +

                ③ return:成功时返回一个与key相关的共享内存标识符(非负整数)。调用失败返回-1

                +

                不相关的进程可以返回值(共享内存标识符)访问同一共享内存

                +

                2.shmat

                第一次创建完共享内存时,它还不能被任何进程访问,需要shmat启动对该共享内存的访问,并把共享内存连接到当前进程的地址空间。

                +
                void *shmat(int shm_id, const void *shm_addr, int shmflg);
                -

                大概buf数组里存储着所有buf的内容。buf本身通过最近使用排序的双向链表连接,head是链表的头。

                -

                初始化

                // called by main.c
                void
                binit(void)
                {
                struct buf *b;

                initlock(&bcache.lock, "bcache");

                // Create linked list of buffers
                // 把b插在head之后
                bcache.head.prev = &bcache.head;
                bcache.head.next = &bcache.head;
                for(b = bcache.buf; b < bcache.buf+NBUF; b++){
                b->next = bcache.head.next;
                b->prev = &bcache.head;
                initsleeplock(&b->lock, "buffer");
                bcache.head.next->prev = b;
                bcache.head.next = b;
                }
                }
                +

                ① shm_id:共享内存标识符

                +

                ② shm_addr:指定共享内存连接到当前进程中的地址位置,通常为空,表示让系统来选择共享内存的地址

                +

                ③ shm_flg:一组标志位,通常为0

                +

                ④ return:成功时返回一个指向共享内存第一个字节的指针,失败返回-1

                +

                3.shmdt

                用于将共享内存从当前进程中分离,使该共享内存对当前进程不再可用。

                +
                int shmdt(const void *shmaddr);
                -

                上层接口

                -

                The main interface exported by the buffer cache consists of bread and bwrite.

                -

                The buffer cache uses a per-buffer sleep-lock to ensure concurrent security.

                -
                -

                bread

                -

                bread obtains a buf containing a copy of a block which can be read or modified in memory.

                -

                依据给定设备号和给定扇区号寻找cache的buf。返回的buf是locked的。

                -
                -
                // Return a locked buf with the contents of the indicated block.
                struct buf*
                bread(uint dev, uint blockno)
                {
                struct buf *b;

                // 获取buf块
                b = bget(dev, blockno);
                if(!b->valid) {
                // 说明cache未命中,需要从磁盘读入
                virtio_disk_rw(b, 0);
                b->valid = 1;
                }
                return b;
                }
                +

                ① shmaddr:shmat返回的共享内存指针

                +

                ② return:成功0,失败1

                +

                4.shmctl

                用来控制共享内存

                +
                int shmctl(int shm_id, int command, struct shmid_ds *buf);
                -

                bwrite

                -

                writes a modified buffer to the appropriate block on the disk

                -
                -
                // Write b's contents to disk.  Must be locked.
                void
                bwrite(struct buf *b)
                {
                // 必须持有b的锁
                if(!holdingsleep(&b->lock))
                panic("bwrite");
                // 写入磁盘
                virtio_disk_rw(b, 1);
                }
                +

                ① shm_id:共享内存标识符

                +

                ② command:要采取的操作,它可以取下面的三个值 :

                +
                  +
                • IPC_STAT:把shmid_ds结构中的数据设置为共享内存的当前关联值,即用共享内存的当前关联值覆盖shmid_ds的值。
                • +
                • IPC_SET:如果进程有足够的权限,就把共享内存的当前关联值设置为shmid_ds结构中给出的值
                • +
                • IPC_RMID:删除共享内存段
                • +
                +

                ③ buf:结构指针

                +

                shmid_ds结构 至少包括以下成员:

                +
                struct shmid_ds
                {
                uid_t shm_perm.uid;
                uid_t shm_perm.gid;
                mode_t shm_perm.mode;
                };
                -

                brelse

                -

                A kernel thread must release a buffer by calling brelse when it is done with it.

                +

                实验1 在Ubuntu下编写程序“基于共享内存的生产者消费者模型”

                +

                本项实验在 Ubuntu 下完成,与信号量实验中的 pc.c 的功能要求基本一致,仅有两点不同:

                +
                  +
                • 不用文件做缓冲区,而是使用共享内存;
                • +
                • 生产者和消费者分别是不同的程序。生产者是 producer.c,消费者是 consumer.c。两个程序都是单进程的,通过信号量和缓冲区进行通信。
                • +
                +

                Linux 下,可以通过 shmget()shmat() 两个系统调用使用共享内存。

                -
                // Release a locked buffer.
                // Move to the head of the most-recently-used list.
                void
                brelse(struct buf *b)
                {
                if(!holdingsleep(&b->lock))
                panic("brelse");

                releasesleep(&b->lock);

                acquire(&bcache.lock);
                b->refcnt--;
                if (b->refcnt == 0) {
                // no one is waiting for it.
                // 移动到头结点和头结点的下一个结点之间的位置
                b->next->prev = b->prev;
                b->prev->next = b->next;
                b->next = bcache.head.next;
                b->prev = &bcache.head;
                bcache.head.next->prev = b;
                bcache.head.next = b;
                }

                release(&bcache.lock);
                }
                +

                直接上代码。感觉比文件操作简单多了2333

                +

                consumer.c

                +
                #include <stdio.h>
                #include <fcntl.h>
                #include <sys/types.h>
                #include <sys/stat.h>
                #include <string.h>
                #include <sys/shm.h>
                #include <errno.h>
                #include <unistd.h>
                #include <semaphore.h>

                sem_t* empty;
                sem_t* full;
                sem_t* mutex;
                char* p;

                void Consumer(){
                sem_wait(full);
                sem_wait(mutex);
                char s[5]={0};
                //there read s from buffer...
                memcpy(s,p,sizeof(char)*4);
                p+=4;
                printf("%d : %s",getpid(),s);
                sem_post(mutex);
                sem_post(empty);
                }


                int main(){
                empty=sem_open("empty",O_CREAT,0644,10);
                full=sem_open("full",O_CREAT,0644,0);
                mutex=sem_open("mutex",O_CREAT,0644,1);

                int shm_id=shmget(521,sizeof(char)*4*600,0644|IPC_CREAT);
                char* shm=(char*)shmat(shm_id,NULL,0);
                p=shm;

                int cnt=500;
                while(cnt--){
                Consumer();
                }

                shmdt(shm);
                shmctl(shm_id,IPC_RMID,0);
                sem_unlink("mutex");
                sem_unlink("full");
                sem_unlink("empty");
                return 0;
                }
                -

                具体细节

                bget

                用于获取cache中是否存在block。如果不存在,则新申请一个buf,并把该buf以上锁状态返回

                -
                // Look through buffer cache for block on device dev.
                // If not found, allocate a buffer.
                // In either case, return locked buffer.
                static struct buf*
                bget(uint dev, uint blockno)
                {
                struct buf *b;

                acquire(&bcache.lock);

                // Is the block already cached?
                // 这个循环条件很有意思,充分用到了双向链表的特性
                for(b = bcache.head.next; b != &bcache.head; b = b->next){
                if(b->dev == dev && b->blockno == blockno){
                // 引用数增加
                b->refcnt++;
                release(&bcache.lock);
                // 锁定
                acquiresleep(&b->lock);
                return b;
                }
                }

                // Not cached.
                // Recycle the least recently used (LRU) unused buffer.
                // 从尾部开始遍历,确实就是最少使用的了
                for(b = bcache.head.prev; b != &bcache.head; b = b->prev){
                // 如果该buf空闲
                if(b->refcnt == 0) {
                b->dev = dev;
                b->blockno = blockno;
                // 仅是新建了一个buf,还未从磁盘读取对应磁盘块的副本,因而设valid为0以供上层函数调用处理
                b->valid = 0;
                b->refcnt = 1;
                release(&bcache.lock);
                // 锁定
                acquiresleep(&b->lock);
                return b;
                }
                }
                // cache不够用了
                panic("bget: no buffers");
                }
                +

                producer.c

                +
                #include <stdio.h>
                #include <string.h>
                #include <sys/shm.h>
                #include <errno.h>
                #include <unistd.h>
                #include <semaphore.h>
                #include <fcntl.h>
                #include <sys/types.h>
                #include <sys/stat.h>

                sem_t* empty;
                sem_t* full;
                sem_t* mutex;
                char* p;

                void Producer(){
                int i;
                char s[5]={0};
                for(i=0;i<=500;i++){
                sem_wait(empty);
                sem_wait(mutex);
                sprintf(s,"%03d\n",i);
                //there write s into buffer...
                memcpy(p,s,sizeof(char)*4);
                p+=4;
                printf("Write into success!%s\n",s);
                sem_post(mutex);
                sem_post(full);
                }
                }

                int main(){
                empty=sem_open("empty",O_CREAT,0644,10);
                full=sem_open("full",O_CREAT,0644,0);
                mutex=sem_open("mutex",O_CREAT,0644,1);

                int shm_id=shmget(521,sizeof(char)*4*600,0644|IPC_CREAT);
                char* shm=(char*)shmat(shm_id,NULL,0);
                p=shm;

                Producer();

                shmdt(shm);
                shmctl(shm_id,IPC_RMID,0);
                sem_unlink("mutex");
                sem_unlink("full");
                sem_unlink("empty");
                return 0;
                }
                -

                Logging layer

                简介

                -

                Xv6通过简单的日志记录形式解决了文件系统操作期间的崩溃问题。

                -

                xv6系统调用不会直接写入磁盘上的文件系统数据结构。相反,它会在磁盘上的log(日志)中放置它希望进行的所有磁盘写入的描述。一旦系统调用记录了它的所有写入操作,它就会向磁盘写入一条特殊的commit(提交)记录,表明日志包含一个完整的操作。此时,系统调用将写操作复制到磁盘上的文件系统数据结构。完成这些写入后,系统调用将擦除磁盘上的日志。

                -
                -
                -

                如果系统崩溃并重新启动,则在运行任何进程之前,文件系统代码将按如下方式从崩溃中恢复:

                -

                如果日志标记为包含完整操作,则恢复代码会将写操作复制到磁盘文件系统中它们所属的位置,然后擦除日志。如果日志没有标记为包含完整操作,则恢复代码将忽略该日志,然后擦除日志。

                -
                -

                这就保证了原子性。

                -

                Log design

                image-20230121162324747

                -

                superblock记录了log的存储位置。

                -
                -

                它由一个头块(header block)和一系列更新块的副本(logged block)组成。

                -

                头块包含一个扇区号(sector)数组(每个logged block对应一个扇区号)以及日志块的计数。

                -

                磁盘上的头块中的计数为零表示日志中没有事务,为非零表示日志包含一个完整的已提交事务,并具有指定数量的logged block。

                -

                在事务提交(commit)时Xv6才向头块写入数据,在此之前不会写入。在将logged blocks复制到文件系统后,头块的计数将被设置为零。

                -

                因此,事务中途崩溃将导致日志头块中的计数为零;提交后的崩溃将导致非零计数。

                -
                -
                -

                为了允许不同进程并发执行文件系统操作,日志系统可以将多个系统调用的写入累积到一个事务中。因此,单个提交可能涉及多个完整系统调用的写入。为了避免在事务之间拆分系统调用,日志系统仅在没有文件系统调用进行时提交。

                -

                同时提交多个事务的想法称为组提交(group commit)。组提交减少了磁盘操作的数量,因为成本固定的一次提交分摊了多个操作。组提交还同时为磁盘系统提供更多并发写操作,可能允许磁盘在一个磁盘旋转时间内写入所有这些操作。Xv6的virtio驱动程序不支持这种批处理,但是Xv6的文件系统设计允许这样做。

                -

                【这感觉实现得也还挺简略的】

                -
                -
                -

                Xv6在磁盘上留出固定的空间来保存日志。事务中系统调用写入的块总数必须可容纳于该空间。这导致两个后果:

                -
                  -
                1. 任何单个系统调用都不允许写入超过日志空间的不同块。

                  -

                  【这段话我一个字没看懂】

                  -

                  这对于大多数系统调用来说都不是问题,但其中两个可能会写入许多块:writeunlink。一个大文件的write可以写入多个数据块和多个位图块以及一个inode块;unlink大文件可能会写入许多位图块和inode。Xv6的write系统调用将大的写入分解为适合日志的多个较小的写入,unlink不会导致此问题,因为实际上Xv6文件系统只使用一个位图块。

                  -
                2. -
                3. 日志空间有限的另一个后果是,除非确定系统调用的写入将可容纳于日志中剩余的空间,否则日志系统无法允许启动系统调用。

                  -
                4. -
                -
                -

                Code: logging

                -

                log的原理是这样的:

                -

                在每个系统调用的开始调用begin_op表示事务开始,然后之后新申请一块block,也即把该block的内容读入内存,并且把该block的blockno记录到log的header中。此后程序正常修改在内存中的block,磁盘中的block保持不变。最后commit的时候遍历log header中的blockno,一块块地把内存中的block写入日志和磁盘中。

                -

                如果程序在commit前崩溃,则内存消失,同时磁盘也不会写入;如果在commit后崩溃,那也无事发生。

                -

                在每次启动的时候,都会执行log的初始化,届时可以顺便恢复数据。

                -

                完美实现了日志的功能。

                -
                -

                image-20230123212753931

                -

                数据结构

                // Contents of the header block, used for both the on-disk header block
                // and to keep track in memory of logged block# before commit.
                struct logheader {
                int n;
                // 扇区号也即blockno的数组
                int block[LOGSIZE];
                };

                // 代表log磁盘块
                struct log {
                struct spinlock lock;
                int start;// log磁盘块的开始。start开始的第一块为log header,之后皆为写入的block
                int size;
                int outstanding; // how many FS sys calls are executing.
                int committing; // in commit(), please wait.
                int dev;
                struct logheader lh;
                };
                struct log log;
                +

                编译运行指令

                +
                gcc -o producer producer.c -pthread
                gcc -o consumer consumer.c -pthread
                ./producer > p.txt &
                ./consumer > c.txt
                -

                关键函数

                begin_op()
                -

                begin_op等待直到日志系统当前未处于提交中,并且直到有足够的未被占用的日志空间来保存此调用的写入。

                -

                log.outstanding统计预定了日志空间的系统调用数;为此保留的总空间为log.outstanding乘以MAXOPBLOCKS(10)。递增log.outstanding会预定空间并防止在此系统调用期间发生提交(if的第二个分支)。代码保守地假设每个系统调用最多可以写入MAXOPBLOCKS(10)个不同的块。

                +

                运行结果c.txt(仅展示部分)

                +
                27696 : 000
                27696 : 001
                27696 : 002
                27696 : 003
                27696 : 004
                27696 : 005
                27696 : 006
                27696 : 007
                27696 : 008
                27696 : 009
                27696 : 010
                27696 : 011
                27696 : 012
                + + + +

                实验2 在Linux0.11实现共享内存

                +

                进程之间可以通过页共享进行通信,被共享的页叫做共享内存,结构如下图所示:

                + + +

                本部分实验内容是在 Linux 0.11 上实现上述页面共享,并将上一部分实现的 producer.c 和 consumer.c 移植过来,验证页面共享的有效性。

                -
                // called at the start of each FS system call.
                void
                begin_op(void)
                {
                acquire(&log.lock);
                while(1){
                // 正在提交则等待日志空闲
                if(log.committing){
                sleep(&log, &log.lock);
                // 日志空间不足则等待空间充足
                } else if(log.lh.n + (log.outstanding+1)*MAXOPBLOCKS > LOGSIZE){
                // this op might exhaust log space此操作可能会耗尽日志空间; wait for commit.
                sleep(&log, &log.lock);
                } else {
                log.outstanding += 1;
                release(&log.lock);
                break;
                }
                }
                }
                +
                +

                具体要求在 mm/shm.c 中实现 shmget()shmat() 两个系统调用。它们能支持 producer.cconsumer.c 的运行即可,不需要完整地实现 POSIX 所规定的功能。

                +
                  +
                • shmget()
                • +
                +
                int shmget(key_t key, size_t size, int shmflg);
                -
                log_write
                -

                log_write充当bwrite的代理。它将块的扇区号记录在内存中,在磁盘上的日志中预定一个槽位,并调用bpin将缓存固定在block cache中,以防止block cache将其逐出【具体原理就是让refcnt++,这样就不会被当成空闲block用掉了】。

                -

                为啥要防止换出呢?换出不是就正好自动写入磁盘了吗?这里一是为了保障前面提到的原子性,防止换入换出导致的单一写入磁盘;二是换出自动写入的是磁盘对应位而不一定是日志所在的blocks。

                +

                shmget() 会新建/打开一页内存,并返回该页共享内存的 shmid(该块共享内存在操作系统内部的 id)。

                +

                所有使用同一块共享内存的进程都要使用相同的 key 参数。

                +

                如果 key 所对应的共享内存已经建立,则直接返回 shmid。如果 size 超过一页内存的大小,返回 -1,并置 errnoEINVAL。如果系统无空闲内存,返回 -1,并置 errnoENOMEM

                +

                shmflg 参数可忽略。

                +
                  +
                • shmat()
                • +
                +
                void *shmat(int shmid, const void *shmaddr, int shmflg);
                + +

                shmat() 会将 shmid 指定的共享页面映射到当前进程的虚拟地址空间中,并将其首地址返回。

                +

                如果 shmid 非法,返回 -1,并置 errnoEINVAL

                +

                shmaddrshmflg 参数可忽略。

                -
                void
                log_write(struct buf *b)
                {
                int i;
                // #define LOGSIZE (MAXOPBLOCKS*3) // max data blocks in on-disk log
                // 30
                if (log.lh.n >= LOGSIZE || log.lh.n >= log.size - 1)
                panic("too big a transaction");
                if (log.outstanding < 1)
                panic("log_write outside of trans");

                acquire(&log.lock);
                // log_write会注意到在单个事务中多次写入一个块的情况,并在日志中为该块分配相同的槽位。
                // 这种优化通常称为合并(absorption)
                for (i = 0; i < log.lh.n; i++) {
                if (log.lh.block[i] == b->blockno) // log absorbtion
                break;
                }
                // 这里还是挺巧妙的。
                // 如果存在log.lh.block[i] == b->blockno的情况,执行此句话也无妨
                // 如果不存在,则给log新增一块,填入log.lh.block[log.lh.n]的位置,再++log.lh.n
                log.lh.block[i] = b->blockno;
                if (i == log.lh.n) { // Add new block to log?
                bpin(b);
                log.lh.n++;
                }
                release(&log.lock);
                }
                +

                思路:

                +

                1.shmget:由其论述,我们可以知道,我们需要建立一个映射表,其中成员为结构体({key_t key,size_t size,unsigned long page}),每次只需查找映射表,如果有对应key则返回下标,如果没有则新建页表,填入映射体,再返回对应下标。

                +

                以下为了图省事,对映射表的实现进行了简化,把key直接当做int类型,作为映射表下标,映射表成员为page,unsigned long。

                +

                2.shmat:

                +

                首先由指导书的提示:

                +
                // 建立线性地址和物理地址的映射
                put_page(tmp, address);
                -
                end_op
                // called at the end of each FS system call.
                // 如果这是最后一层outstanding就会执行commit操作
                // commits if this was the last outstanding operation.
                void
                end_op(void)
                {
                int do_commit = 0;

                acquire(&log.lock);
                log.outstanding -= 1;
                if(log.committing)
                panic("log.committing");
                if(log.outstanding == 0){
                do_commit = 1;
                log.committing = 1;
                } else {
                // begin_op() may be waiting for log space,
                // and decrementing log.outstanding has decreased
                // the amount of reserved space.
                wakeup(&log);
                }
                release(&log.lock);

                if(do_commit){
                // call commit w/o holding locks, since not allowed
                // to sleep with locks.
                commit();
                acquire(&log.lock);
                log.committing = 0;
                wakeup(&log);
                release(&log.lock);
                }
                }
                +

                我们知道在shmat中,要建立shmget得到的共享物理页面与其虚拟地址的映射,就需要使用这个put_page函数。

                +

                但是put_page函数的参数为页和address。页就是我们的shm_map[key],address=虚拟地址+段基址。那么如何得到虚拟地址呢?

                +

                通过指导书的提示:

                +
                code_base = get_base(current->ldt[1]);
                data_base = code_base;

                // 数据段基址 = 代码段基址
                set_base(current->ldt[1],code_base);
                set_limit(current->ldt[1],code_limit);
                set_base(current->ldt[2],data_base);
                set_limit(current->ldt[2],data_limit);
                __asm__("pushl $0x17\n\tpop %%fs":: );

                // 从数据段的末尾开始
                data_base += data_limit;

                // 向前处理
                for (i=MAX_ARG_PAGES-1 ; i>=0 ; i--) {
                // 一次处理一页
                data_base -= PAGE_SIZE;
                // 建立线性地址到物理页的映射
                if (page[i]) put_page(page[i],data_base);
                }
                -
                commit
                static void
                commit()
                {
                if (log.lh.n > 0) {
                // cache -> log block
                write_log(); // Write modified blocks from cache to log
                // head(in stack/heap) -> log block
                // 此可以说为commit完成的标志。
                // 因为无论接下来是否崩溃,数据最终都会被写入disk,不同在于是在recover时还是接下来写入
                write_head(); // Write header to disk -- the real commit
                // log block -> real position
                install_trans(0); // Now install writes to home locations
                log.lh.n = 0;
                // 擦除
                write_head(); // Erase the transaction from the log
                }
                }
                +

                再结合所学知识,我们可以知道几点:

                +

                ① 数据段的基址可由current->ldt[2]给出 ② address=虚拟地址+段基址 ③ 我们需要分配给当前共享内存一段空闲的虚拟地址段

                +

                则该小段空闲数据段的虚拟地址就是我们的return值,address=return+data_base。

                +

                问题就转化成了如何获取一段空闲数据段。

                +

                我们由下图:

                + -
                write_log
                // Copy modified blocks from cache to log.
                static void
                write_log(void)
                {
                int tail;

                for (tail = 0; tail < log.lh.n; tail++) {
                struct buf *to = bread(log.dev, log.start+tail+1); // log block
                struct buf *from = bread(log.dev, log.lh.block[tail]); // cache block
                memmove(to->data, from->data, BSIZE);
                bwrite(to); // write the log
                brelse(from);// 此处的brelse呼应了外界调用的bread
                brelse(to);
                }
                }
                +

                可知,brk指针指向堆区顶部,即空闲堆的起始位置。因而我们可以用这段空间作为我们要的空闲数据段,当前brk即为虚拟地址。

                +

                我们的页有PAGE_SIZE那么大,因而自然也就要用PAGE_SIZE那么大的空闲数据段了。

                +

                解说完毕,以下上代码~

                +
                #include <unistd.h>
                #include <unistd.h>
                #define __LIBRARY__
                #include <linux/kernel.h>
                #include <linux/sched.h>
                #include <linux/mm.h>

                unsigned long shm_map[600];

                int sys_shmget(int key,size_t size,int shmflg){
                if(key<0||key>=600) return -1;
                if(shm_map[key]!=0) return key;

                unsigned long tmp=get_free_page();
                shm_map[key]=tmp;

                return key;
                }

                void* sys_shmat(int shmid,const void* shmaddr,int shmflg){
                if(shmid<0||shmid>=600) return -1;

                unsigned long page=shm_map[shmid];
                //得到数据段的基址
                unsigned long data_base=get_base(current->ldt[2]);
                //brk指针指向空闲数据段的开始
                unsigned long brk=current->brk+data_base;
                current->brk+=PAGE_SIZE;
                //建立内存映射
                put_page(page,brk);
                return (void*)(brk-data_base);
                }
                -
                write_head
                // Write in-memory log header to disk.
                // 这是事务提交的标志
                // This is the true point at which the
                // current transaction commits.
                static void
                write_head(void)
                {
                struct buf *buf = bread(log.dev, log.start);
                struct logheader *hb = (struct logheader *) (buf->data);
                int i;
                hb->n = log.lh.n;
                for (i = 0; i < log.lh.n; i++) {
                hb->block[i] = log.lh.block[i];
                }
                bwrite(buf);
                brelse(buf);
                }
                +

                信号量

                任务一 实现pc.c

                在 Ubuntu 上编写应用程序“pc.c”,解决经典的生产者—消费者问题,完成下面的功能:

                1.建立一个生产者进程,N 个消费者进程(N>1);
                2.用文件建立一个共享缓冲区;
                3.生产者进程依次向缓冲区写入整数 0,1,2,...,M,M>=500;
                4.消费者进程从缓冲区读数,每次读一个,并将读出的数字从缓冲区删除,然后将本进程 ID 和 + 数字输出到标准输出;
                5.缓冲区同时最多只能保存 10 个数。
                其中 ID 的顺序会有较大变化,但冒号后的数字一定是从 0 开始递增加一的。
                -
                install_trans
                // Copy committed blocks from log to their home location
                static void
                install_trans(int recovering)
                {
                int tail;

                for (tail = 0; tail < log.lh.n; tail++) {
                struct buf *lbuf = bread(log.dev, log.start+tail+1); // read log block
                struct buf *dbuf = bread(log.dev, log.lh.block[tail]); // read dst
                memmove(dbuf->data, lbuf->data, BSIZE); // copy block to dst
                bwrite(dbuf); // write dst to disk
                if(recovering == 0)
                bunpin(dbuf);// 如果不是在recover的过程中
                brelse(lbuf);
                brelse(dbuf);
                }
                }
                +

                先附上我的代码吧【注:我没做到从缓冲区删除,但其他都完成了】

                +
                #include <stdio.h>
                #include <errno.h>
                #include <unistd.h>
                #include <sys/types.h>
                #include <sys/stat.h>
                #include <fcntl.h>
                #include <semaphore.h>

                const char* filename="buffer.txt";

                sem_t* empty;
                sem_t* full;
                sem_t* mutex;

                int fd;

                void Producer(){
                int i;
                char s[5]={0};
                for(i=0;i<=500;i++){
                sem_wait(empty);
                sem_wait(mutex);
                int tmp=lseek(fd,0,SEEK_CUR);
                lseek(fd,0,SEEK_END);
                sprintf(s,"%03d\n",i);
                write(fd,s,4);
                lseek(fd,tmp,SEEK_SET);
                sem_post(mutex);
                sem_post(full);
                }
                }

                void Consumer(){
                sem_wait(full);
                sem_wait(mutex);
                char s[5]={0};
                read(fd,s,4);
                printf("%d : %s",getpid(),s);
                sem_post(mutex);
                sem_post(empty);
                }

                int main(){
                sem_unlink("empty");
                sem_unlink("full");
                sem_unlink("mutex");
                fd=open(filename,O_RDWR|O_CREAT);
                printf("%d\n",errno);
                empty=sem_open("empty",O_CREAT,0644,10);
                full=sem_open("full",O_CREAT,0644,0);
                mutex=sem_open("mutex",O_CREAT,0644,1);

                if(!fork()){
                Producer();
                return 0;
                }

                int i;
                for(i=0;i<10;i++){
                if(!fork()){
                while(1) Consumer();
                }
                }
                close(fd);
                return 0;
                }
                -

                恢复与初始化

                上面介绍了log的一次事务提交的流程。接下来介绍它是怎么恢复的。

                -
                -

                recover_from_log是由initlog调用的,而它又是在第一个用户进程运行之前的引导期间由fsinit调用的。

                -
                -
                第一个进程运行之前

                由前面scheduler一章的知识可知,每个进程被初次调度的时候会先来执行forkret。这时候就做了log的恢复工作。

                -

                注释解释了为什么不选择在main.c中初始化,而选择在此处初始化。确实,它需要调用sleep,如果在main.c中调用sleep感觉会乱套()毕竟那时候scheduler线程尚未被初始化。

                -
                // A fork child's very first scheduling by scheduler()
                // will swtch to forkret.
                void
                forkret(void)
                {
                // static变量仅会被初始化一次
                static int first = 1;

                // Still holding p->lock from scheduler.
                release(&myproc()->lock);

                // 如果是第一个进程
                if (first) {
                // File system initialization must be run in the context of a
                // regular process (e.g., because it calls sleep), and thus cannot
                // be run from main().
                first = 0;
                fsinit(ROOTDEV);
                }

                usertrapret();
                }
                +

                运行效果:

                + -
                fsinit
                // Init fs
                void
                fsinit(int dev) {
                // ...
                initlog(dev, &sb);
                }
                +

                要点1 系统调用的IO读写

                这部分耗费了我海量时间,主要原因还是因为我没有好好学就直接上手写导致很多地方都因为不清楚而寄了。。。

                +

                先大致讲讲文件读写的原理吧。打开一个文件作为数据流,有一个文件指针,该指针指向的地方就是之后读写开始的地方,读写还有lseek都可以让指针移动。

                +

                再放个各个系统调用的签名。

                +
                @param 文件名 模式

                @return 所需文件描述符

                int open(char* filename,int flag);
                -
                initlog
                void
                initlog(int dev, struct superblock *sb)
                {
                if (sizeof(struct logheader) >= BSIZE)
                panic("initlog: too big logheader");

                initlock(&log.lock, "log");
                // 从super block中获取必要参数
                log.start = sb->logstart;
                log.size = sb->nlog;
                log.dev = dev;
                recover_from_log();
                }
                +

                其中flag的可能取值:

                + -
                recover_from_log
                static void
                recover_from_log(void)
                {
                // 读取head
                read_head();
                // 注意,commit中会把header写入log block,而这里从log block读出header
                // 也就是说,如果header的n不为零,那么说明已经commit了,但可能未写入,重复写入保障安全
                // 如果header的n为零,说明未commit,在install_trans的逻辑中会什么也不做
                // 两种情况完美满足
                install_trans(1); // if committed, copy from log to disk
                log.lh.n = 0;
                // 擦除
                write_head(); // clear the log
                }
                + -

                Code: Block allocator

                个人理解

                说实话没怎么懂,也不大清楚它有什么用,先大概推测一下:

                -

                之前的bread和bwrite这些,就是你给一个设备号和扇区号,它就帮你加载进内存cache。你如果要用的话,肯定还是使用地址方便。所以block allocator的作用之一就是给bread和bwrite加一层封装,将获取的block封装为地址返回,你可以直接操纵这个地址,而无需知道下层的细节。

                -

                这个过程要注意的有两点:

                -
                  -
                1. 封装返回的地址具体是什么,怎么工作的

                  -

                  封装返回的地址实质上是buffer cache中的buf的data字段的地址【差不多】。之后的上层应用在该地址上写入,也即写入了buf,最后会通过log层真正写入磁盘。

                  -
                2. -
                3. 结合bcache的LRU,详细谈谈工作机制

                  -

                  我们可以看到,在balloc中有这么一段逻辑:

                  -
                  bp = bread(dev, BBLOCK(b, sb));
                  // ...
                  log_write(bp);
                  brelse(bp);
                  return b + bi;
                  +

                  如果想要多个方式并行,则可以用|连接。【联系一下原理,这大概是用了标志位吧,每个标志只有一位是1】

                  +

                  这部分踩过的坑:

                  +

                  ① 选择O_CREAT,如果文件已经存在,居然是会报错?【表现为errno=13,还会输出一堆奇怪的东西】

                  +
                  @param 文件描述符  写入字符串  写入长度
                  @return 报错信息
                  int read(int fd,char* string,size_t size);
                  -

                  看到的第一反应就是,我们需求的那块buf是bp,但是这里先是bread了一次,又是brelse了一次,这样bp的refcnt不就为0,很容易被替换掉了吗?

                  -

                  会有这个反应,一定程度上是因为没有很好地理解LRU。事实上,正是它可能被替换掉,才满足了LRU的条件。因为它可能被替掉才能说明它可能是最近最少使用的。

                  -
                4. -
                -

                bitmap

                -

                文件和目录内容存储在磁盘块中,磁盘块必须从空闲池中分配。xv6的块分配器在磁盘上维护一个空闲位图,每一位代表一个块。0表示对应的块是空闲的;1表示它正在使用中。

                -

                引导扇区、超级块、日志块、inode块和位图块的比特位是由程序mkfs初始化设置的:

                -

                image-20230123234919055

                -
                -

                allocator

                类似于memory allocator,块分配器也提供了两个函数:bfreeballoc

                -

                balloc

                -

                Balloc从块0到sb.size(文件系统中的块数)遍历每个块。它查找位图中位为零的空闲块。如果balloc找到这样一个块,它将更新位图并返回该块。

                -

                为了提高效率,循环被分成两部分。外部循环读取位图中的每个块。内部循环检查单个位图块中的所有BPB位。由于任何一个位图块在buffer cache中一次只允许一个进程使用【 bread(dev, BBLOCK(b, sb))会返回一个上锁的block,breadbrelse隐含的独占使用避免了显式锁定的需要】,因此,如果两个进程同时尝试分配一个块也是并发安全的。

                -
                -
                // Allocate a zeroed disk block.
                static uint
                balloc(uint dev)
                {
                int b, bi, m;
                struct buf *bp;

                bp = 0;
                for(b = 0; b < sb.size; b += BPB){
                bp = bread(dev, BBLOCK(b, sb));
                for(bi = 0; bi < BPB && b + bi < sb.size; bi++){
                m = 1 << (bi % 8);
                if((bp->data[bi/8] & m) == 0){ // Is block free?
                bp->data[bi/8] |= m; // Mark block in use.
                log_write(bp);
                brelse(bp);
                bzero(dev, b + bi);
                return b + bi;
                }
                }
                brelse(bp);
                }
                panic("balloc: out of blocks");
                }
                +

                read会读出size个字节然后存进string里面,同时也会移动文件指针向前size个字节。

                +
                @param 文件描述符  写入字符串  写入长度
                @return 报错信息
                int write(int fd,char* string,size_t size);
                -

                bfree

                // Free a disk block.
                static void
                bfree(int dev, uint b)
                {
                struct buf *bp;
                int bi, m;

                bp = bread(dev, BBLOCK(b, sb));
                bi = b % BPB;
                m = 1 << (bi % 8);
                if((bp->data[bi/8] & m) == 0)
                panic("freeing free block");
                bp->data[bi/8] &= ~m;
                log_write(bp);
                brelse(bp);
                }
                +

                基本同write。

                +

                这部分踩过的坑:

                +

                write(fd,NULL,0) ——合法

                +

                write(fd,NULL,a),a>0 ——寄!

                +

                这还是因为write的具体实现了。

                +

                write里面有个判断

                +
                int sys_write(unsigned int fd,char *buf,int count){
                //...
                if(!count) return 0;
                //...
                while(c-->0) *(p++)=get_fs_byte(buf++);
                }
                -

                Inode layer

                inode

                -

                术语inode(即索引结点)可以具有两种相关含义之一。它可能是指包含文件大小和数据块编号列表的磁盘上的数据结构【on-disk inode】。或者“inode”可能指内存中的inode【in-memory inode】,它包含磁盘上inode的副本以及内核中所需的额外信息。

                -
                -

                image-20230121162324747

                -

                on-disk inode

                -

                The on-disk inodes are packed into a contiguous area of disk called the inode blocks.

                -

                Every inode is the same size, so it is easy, given a number n, to find the nth inode on the disk. In fact, this number n, called the inode number or i-number, is how inodes are identifified in the implementation.

                -
                -
                // in fs.h
                // On-disk inode structure
                struct dinode {
                // 为0表示free
                short type; // File type
                short major; // Major device number (T_DEVICE only)
                short minor; // Minor device number (T_DEVICE only)
                // The nlink field counts the number of directory entries that refer to this inode,
                // in order to recognize when the on-disk inode and its data blocks should be freed.
                short nlink; // Number of links to inode in file system
                uint size; // Size of file (bytes)
                uint addrs[NDIRECT+1]; // Data block addresses
                };
                +

                而get_fs_byte:

                + -

                in-memory inode

                -

                The kernel keeps the set of active inodes in memory.

                -

                The kernel stores an inode in memory only if there are C pointers referring to that inode.当且仅当ref==0才会从内核中释放。

                -

                如果nlinks==0就会从物理block中释放。

                -

                The iget and iput functions acquire and release pointers to an inode, modifying the reference count.【相当于buffer cache的ballocbfree】Pointers to an inode can come from file descriptors, current working directories, and transient kernel code such as exec.

                -

                iget返回的struct inode可能没有任何有用的内容。为了确保它保存磁盘inode的副本,代码必须调用ilock。这将锁定inode(以便没有其他进程可以对其进行ilock),并从磁盘读取尚未读取的inode。iunlock释放inode上的锁。将inode指针的获取与锁定分离有助于在某些情况下避免死锁,例如在目录查找期间。多个进程可以持有指向iget返回的inode的C指针,但一次只能有一个进程锁定inode。

                -
                -
                //in file.h
                // in-memory copy of an inode
                struct inode {
                uint dev; // Device number
                uint inum; // Inode number
                int ref; // Reference count
                struct sleeplock lock; // protects everything below here
                int valid; // inode has been read from disk?

                short type; // copy of disk inode
                short major;
                short minor;
                short nlink;
                uint size;
                uint addrs[NDIRECT+1];// 存储着inode数据的blocks的地址,从balloc中获取
                };
                +

                确实感觉空的话挺危险的【】

                +
                @param 文件描述符
                @return 报错信息
                int close(fd);
                -

                Code: inode

                -

                主要是在讲inode layer这一层的方法,以及给上层提供的接口。

                -
                -

                Overview

                image-20230124153309132

                -

                底层接口

                -

                iget iput

                -
                -
                iget

                逻辑还是跟buffer cache非常相似的,不过可以看出这个的数据结构简单许多,也不用实现LRU。

                -
                -

                A struct inode pointer returned by iget() is guaranteed to be valid until the corresponding call to iput(): the inode won’t be deleted, and the memory referred to by the pointer won’t be re-used for a different inode. 【通过ref++实现。】

                -

                不同于buffer cache的bgetiget()提供对inode的非独占访问,因此可以有许多指向同一inode的指针。文件系统代码的许多部分都依赖于iget()的这种行为,既可以保存对inode的长期引用(如打开的文件和当前目录),也可以防止争用,同时避免操纵多个inode(如路径名查找)的代码产生死锁。

                -
                -
                // Find the inode with number inum on device dev
                // and return the in-memory copy. Does not lock
                // the inode and does not read it from disk.
                static struct inode*
                iget(uint dev, uint inum)
                {
                struct inode *ip, *empty;

                acquire(&icache.lock);

                // Is the inode already cached?
                empty = 0;
                for(ip = &icache.inode[0]; ip < &icache.inode[NINODE]; ip++){
                if(ip->ref > 0 && ip->dev == dev && ip->inum == inum){
                ip->ref++;
                release(&icache.lock);
                return ip;
                }
                // 由于不用实现LRU,所以只需一次循环记录即可。
                if(empty == 0 && ip->ref == 0) // Remember empty slot.
                empty = ip;
                }

                // Recycle an inode cache entry.
                if(empty == 0)
                panic("iget: no inodes");

                ip = empty;
                ip->dev = dev;
                ip->inum = inum;
                ip->ref = 1;
                // does not read from disk
                ip->valid = 0;
                release(&icache.lock);

                return ip;
                }
                +

                这个没啥好说的,记得关就是了

                +

                要点2 信号量的调用

                这方面看linux自带的man文档就行,写得很清楚。

                +

                输入指令:

                +
                man sem_overview
                -
                iput
                -

                iput()可以写入磁盘。这意味着任何使用文件系统的系统调用都可能写入磁盘,因为系统调用可能是最后一个引用该文件的系统调用。即使像read()这样看起来是只读的调用,也可能最终调用iput()。这反过来意味着,即使是只读系统调用,如果它们使用文件系统,也必须在事务中进行包装。

                -

                iput()和崩溃之间存在一种具有挑战性的交互。iput()不会在文件的链接计数降至零时立即截断文件,因为某些进程可能仍在内存中保留对inode的引用:进程可能仍在读取和写入该文件,因为它已成功打开该文件。但是,如果在最后一个进程关闭该文件的文件描述符之前发生崩溃,则该文件将被标记为已在磁盘上分配,但没有目录项指向它。如果不做任何处理措施的话,这块磁盘就再也用不了了。

                -

                文件系统以两种方式之一处理这种情况。简单的解决方案用于恢复时:重新启动后,文件系统会扫描整个文件系统,以查找标记为已分配但没有指向它们的目录项的文件。如果存在任何此类文件,接下来可以将其释放。

                -

                第二种解决方案不需要扫描文件系统。在此解决方案中,文件系统在磁盘(例如在超级块中)上记录链接计数降至零但引用计数不为零的文件的i-number。如果文件系统在其引用计数达到0时删除该文件,则会通过从列表中删除该inode来更新磁盘列表。重新启动时,文件系统将释放列表中的所有文件。

                -

                Xv6没有实现这两种解决方案,这意味着inode可能被标记为已在磁盘上分配,即使它们不再使用。这意味着随着时间的推移,xv6可能会面临磁盘空间不足的风险。

                -
                -
                // Drop a reference to an in-memory inode.
                // If that was the last reference, the inode cache entry can
                // be recycled.【refvnt==0 可以回收】
                // 注意这个回收过程无需特别处理,只需自然--refcnt就行,不用像buffer cache那么烦
                // If that was the last reference and the inode has no links
                // to it, free the inode (and its content) on disk.【nlinks==0 copy和本体都得扔掉】
                // All calls to iput() must be inside a transaction in
                // case it has to free the inode.任何需要iput的地方都需要包裹在事务内,因为它可能会释放inode
                void
                iput(struct inode *ip)
                {
                acquire(&icache.lock);

                if(ip->ref == 1 && ip->valid && ip->nlink == 0){
                // inode has no links and no other references: truncate and free.

                // ip->ref == 1 means no other process can have ip locked,
                // so this acquiresleep() won't block (or deadlock).
                acquiresleep(&ip->lock);

                release(&icache.lock);

                // 最终调用bfree,会标记bitmap,完全释放block
                itrunc(ip);
                ip->type = 0;

                /*iupdate:
                // Copy a modified in-memory inode to disk.
                // Must be called after every change to an ip->xxx field
                // that lives on disk, since i-node cache is write-through.
                write-through:
                CPU向cache写入数据时,同时向memory(后端存储)也写一份,使cache和memory的数据保持一致。
                */
                // 这里修改的type是dinode也有的字段,所以需要update一下。
                // 下面的valid是dinode没有的字段,所以随便改,无需update
                iupdate(ip);
                ip->valid = 0;

                releasesleep(&ip->lock);

                acquire(&icache.lock);
                }

                ip->ref--;
                release(&icache.lock);
                }
                +

                这部分踩过的坑:

                +

                千万注意最后不使用信号量时要释放,使用sem_unlink。不然最后的输出结果会非常诡异。

                +

                要点3 编写程序

                以上差不多就是涉及到的需要自己了解的课外知识点了,接下来就需要自己编写程序。

                +

                总体框架就按它给的差不多:

                +
                Producer()
                {
                // 空闲缓存资源
                P(Empty);

                // 互斥信号量
                P(Mutex);

                //生产并放一个item进缓冲区

                V(Mutex);
                V(Full);
                }

                Consumer()
                {
                P(Full);
                P(Mutex);

                //从缓存区取出一个赋值给item并消费;

                V(Mutex);
                V(Empty);
                }
                -

                上层接口

                获取和释放inode
                ialloc
                // Allocate an inode on device dev.
                // Mark it as allocated by giving it type type.
                // Returns an unlocked but allocated and referenced inode.
                struct inode*
                ialloc(uint dev, short type)
                {
                int inum;
                struct buf *bp;
                struct dinode *dip;

                for(inum = 1; inum < sb.ninodes; inum++){
                bp = bread(dev, IBLOCK(inum, sb));
                dip = (struct dinode*)bp->data + inum%IPB;
                if(dip->type == 0){ // a free inode通过type判断是否free
                memset(dip, 0, sizeof(*dip));// zerod
                dip->type = type;
                log_write(bp); // mark it allocated on the disk
                brelse(bp);
                return iget(dev, inum);
                }
                brelse(bp);
                }
                panic("ialloc: no inodes");
                }
                +

                有个点挺有趣的,就是它实际上把文件指针也看成一种资源了,因此也需要在同步段对其进行更新。

                +

                printf的stdout也是资源。

                +

                故以上两者都只能在锁内同步段进行更新。

                +

                main函数就照本宣科地用fork建立子进程就行。

                +

                任务二 自己实现信号量

                Linux 在 0.11 版还没有实现信号量,Linus 把这件富有挑战的工作留给了你。如果能实现一套山寨版的完全符合 POSIX 规范的信号量,无疑是很有成就感的。但时间暂时不允许我们这么做,所以先弄一套缩水版的类 POSIX 信号量,它的函数原型和标准并不完全相同,而且只包含如下系统调用:
                sem_t *sem_open(const char *name, unsigned int value);
                int sem_wait(sem_t *sem);
                int sem_post(sem_t *sem);
                int sem_unlink(const char *name);

                sem_open() 的功能是创建一个信号量,或打开一个已经存在的信号量。
                sem_t 是信号量类型,根据实现的需要自定义。
                name 是信号量的名字。不同的进程可以通过提供同样的 name 而共享同一个信号量。如果该信号量不存在,就创建新的名为 name 的信号量;如果存在,就打开已经存在的名为 name 的信号量。
                value 是信号量的初值,仅当新建信号量时,此参数才有效,其余情况下它被忽略。当成功时,返回值是该信号量的唯一标识(比如,在内核的地址、ID 等),由另两个系统调用使用。如失败,返回值是 NULL。
                sem_wait() 就是信号量的 P 原子操作。如果继续运行的条件不满足,则令调用进程等待在信号量 sem 上。返回 0 表示成功,返回 -1 表示失败。
                sem_post() 就是信号量的 V 原子操作。如果有等待 sem 的进程,它会唤醒其中的一个。返回 0 表示成功,返回 -1 表示失败。
                sem_unlink() 的功能是删除名为 name 的信号量。返回 0 表示成功,返回 -1 表示失败。
                在 kernel 目录下新建 sem.c 文件实现如上功能。然后将 pc.c 从 Ubuntu 移植到 0.11 下,测试自己实现的信号量。
                -
                inode的锁保护

                前面说到,inode的设计使得有多个指针同时指向一个inode成为了可能。因而,修改使用inode的时候就要对其进行独占访问。使用ialloc获取和用ifree释放的inode必须被保护在ilockiunlock区域中。

                -
                ilock

                ilock既可以实现对inode的独占访问,同时也可以给未初始化的inode进行初始化工作。

                -
                -

                iget返回的struct inode可能没有任何有用的内容。为了确保它保存磁盘inode的副本,代码必须调用ilock。这将锁定inode(以便没有其他进程可以对其进行ilock),并从磁盘读取尚未读取的inode。

                -
                -
                // Lock the given inode and reads the inode from disk if necessary.
                void
                ilock(struct inode *ip)
                {
                struct buf *bp;
                struct dinode *dip;

                if(ip == 0 || ip->ref < 1)
                panic("ilock");

                acquiresleep(&ip->lock);

                if(ip->valid == 0){
                // 通过inode索引号和superblock算出扇区号
                bp = bread(ip->dev, IBLOCK(ip->inum, sb));
                dip = (struct dinode*)bp->data + ip->inum%IPB;
                // 填充ip
                ip->type = dip->type;
                ip->major = dip->major;
                ip->minor = dip->minor;
                ip->nlink = dip->nlink;
                ip->size = dip->size;
                memmove(ip->addrs, dip->addrs, sizeof(ip->addrs));
                brelse(bp);
                ip->valid = 1;
                if(ip->type == 0)
                panic("ilock: no type");
                }
                }
                +

                由于不小心写完的实验代码被销毁了,因此差不多参考的是这篇文章【戳这里】,修改了一些地方,构成了我的回忆版代码。

                +

                要点1 系统调用修改

                详见文章,写得很清楚。

                +

                要点2 sem.c文件的编写

                sem_t定义

                +
                /* 定义的信号量数据结构: */
                # ifndef _SEM_H_
                # define _SEM_H_

                #include<linux/sched.h>

                typedef struct semaphore_t
                {
                char name[20];/* 信号量的名称 */
                int value; /* 信号量的值 */
                int active;//我自己加的,是对象池思想,感觉写得还挺好的2333
                struct tast_struct *queue;/* 指向阻塞队列的指针 */
                } sem_t;

                #endif
                -
                iunlock
                -

                iunlock释放inode上的锁。

                -

                将inode指针的获取与锁定分离有助于在某些情况下避免死锁,例如在目录查找期间。多个进程可以持有指向iget返回的inode的C指针,但一次只能有一个进程锁定inode。

                -
                -
                // Unlock the given inode.
                void
                iunlock(struct inode *ip)
                {
                if(ip == 0 || !holdingsleep(&ip->lock) || ip->ref < 1)
                panic("iunlock");

                releasesleep(&ip->lock);
                }
                +
                #include <unistd.h> 
                #include <linux/sem.h>
                #include <asm/segment.h>
                #include <asm/system.h>

                #define SEM_LIST_LENGTH 50

                //信号量表
                sem_t sem_list[SEM_LIST_LENGTH];

                int str_cmp(const char* s1,const char* s2){
                char* p=s1;
                int i,len1=0,len2=0;
                while(*p!='\0'){
                p++;
                len1++;
                }
                p=s2;
                while(*p!='\0'){
                p++;
                len2++;
                }
                if(len1!=len2) return 1;
                for(i=0;i<len1;i++){
                if(s1[i]!=s2[i]) return 1;
                }
                return 0;
                }
                -

                Code: inode content

                Overview

                -

                主要讲的是inode本身存储数据的结构

                -
                -
                -

                磁盘上的inode结构体struct dinode包含一个size和一个块号数组(见图8.3),数组内罗列着存储着该inode数据的块号。

                -

                前面的NDIRECT个数据块被列在数组中的前NDIRECT个元素中;这些块称为直接块(direct blocks)。接下来的NINDIRECT个数据块不在inode中列出,而是在称为间接块(indirect block)的数据块中列出。addrs数组中的最后一个元素给出了间接块的地址。

                -

                因此,可以从inode中列出的块加载文件的前12 kB(NDIRECT x BSIZE)字节,而只有在查阅间接块后才能加载下一个256 kB(NINDIRECT x BSIZE)字节。

                -
                -
                // On-disk inode structure
                struct dinode {
                // ...
                uint addrs[NDIRECT+1]; // Data block addresses
                };
                +

                sem_open

                +
                /*
                sem_open()的功能是创建一个信号量,或打开一个已经存在的信号量。
                */

                sem_t *sys_sem_open(const char * name,unsigned int value)
                {
                if (name == NULL)
                {
                errno = 1;
                return NULL;
                }
                /* 首先将信号量的名称赋值到新建的缓冲区中 */
                char nbuf[20];
                int i;
                for(i = 0; i< 20; i++)
                {
                nbuf[i] = get_fs_byte(name+i);
                }

                /* 然后开始遍历已有的信号量数组,如果有该名字的信号量,直接返回信号量的地址 */
                for(i = 0; i < SEM_LIST_LENGTH; i++)
                {
                if(sem_list[i].active==1&&!str_cmp(sem_list[i].name, nbuf))
                {
                return &sem_list[i];
                }
                }
                /* 如果找不到信号量,就开始新建一个名字为name的信号量 */
                for(i = 0; i < SEM_LIST_LENGTH; i++)
                {
                if(sem_list[i].active==0)
                {
                strcpy(sem_list[i].name, nbuf);
                sem_list[i].value = value;
                sem_list[i].active=1;
                sem_list[i].queue = NULL;
                return &sem_list[i];
                }
                }

                //表已满
                errno = 1;
                return NULL;
                }

                -

                image-20230124163025094

                -

                bmap

                -

                函数bmap负责封装这个寻找数据块的过程,以便实现我们将很快看到的如readiwritei这样的更高级例程。

                -

                bmap(struct inode *ip, uint bn)返回inodeip的第bn个数据块的磁盘块号。如果ip还没有这样的块,bmap会分配一个。

                -

                Bmap使readiwritei很容易获取inode的数据。

                -
                -
                // Inode content
                //
                // The content (data) associated with each inode is stored
                // in blocks on the disk. The first NDIRECT block numbers
                // are listed in ip->addrs[]. The next NINDIRECT blocks are
                // listed in block ip->addrs[NDIRECT].

                // Return the disk block address of the nth block in inode ip.
                // If there is no such block, bmap allocates one.
                static uint
                bmap(struct inode *ip, uint bn)
                {
                uint addr, *a;
                struct buf *bp;

                // 如果为direct block
                if(bn < NDIRECT){
                if((addr = ip->addrs[bn]) == 0)
                ip->addrs[bn] = addr = balloc(ip->dev);
                return addr;
                }
                bn -= NDIRECT;

                // 如果为indirect block
                if(bn < NINDIRECT){
                // Load indirect block, allocating if necessary.
                if((addr = ip->addrs[NDIRECT]) == 0)
                ip->addrs[NDIRECT] = addr = balloc(ip->dev);
                bp = bread(ip->dev, addr);
                a = (uint*)bp->data;
                if((addr = a[bn]) == 0){
                // 如果没有,会分配一个
                a[bn] = addr = balloc(ip->dev);
                log_write(bp);
                }
                brelse(bp);
                return addr;
                }

                panic("bmap: out of range");
                }
                +

                sem_wait

                +
                /*
                sem_wait()就是信号量的P原子操作。
                如果继续运行的条件不满足,则令调用进程等待在信号量sem上。
                返回0表示成功,返回-1表示失败。
                */
                int sys_sem_wait(sem_t * sem)
                {
                /* 判断:如果传入的信号量是无效信号量,P操作失败,返回-1 */
                if(sem == NULL || sem < sem_list || sem > sem_list + SEM_LIST_LENGTH)
                {
                errno=1;
                return -1;
                }
                /* 关中断 */
                cli();
                while(sem->value < 0)
                {
                sleep_on(&(sem->queue));
                }
                sem->value--;
                /* 开中断 */
                sti();
                return 0;
                }
                -

                itrunc

                -

                itrunc释放文件的块,将inode的size重置为零。

                -

                Itrunc首先释放直接块,然后释放间接块中列出的块,最后释放间接块本身。

                -
                -

                readi

                -

                readiwritei都是从检查ip->type == T_DEV开始的。这种情况处理的是数据不在文件系统中的特殊设备;我们将在文件描述符层返回到这种情况。

                -
                -
                // Read data from inode.数据大小为n,从off开始,读到dst处
                // Caller must hold ip->lock.
                // If user_dst==1, then dst is a user virtual address;
                // otherwise, dst is a kernel address.
                int
                readi(struct inode *ip, int user_dst, uint64 dst, uint off, uint n)
                {
                uint tot, m;
                struct buf *bp;

                if(off > ip->size || off + n < off)
                return 0;
                if(off + n > ip->size)
                n = ip->size - off;

                // 主循环处理文件的每个块,将数据从缓冲区复制到dst
                for(tot=0; tot<n; tot+=m, off+=m, dst+=m){
                bp = bread(ip->dev, bmap(ip, off/BSIZE));
                m = min(n - tot, BSIZE - off%BSIZE);
                if(either_copyout(user_dst, dst, bp->data + (off % BSIZE), m) == -1) {
                brelse(bp);
                tot = -1;
                break;
                }
                brelse(bp);
                }
                return tot;
                }
                +

                sem_post

                +
                /*
                sem_post()就是信号量的V原子操作。
                如果有等待sem的进程,它会唤醒其中的一个。
                返回0表示成功,返回-1表示失败。
                */
                int sys_sem_post(sem_t * sem)
                {
                /* 判断:如果传入的信号量是无效信号量,V操作失败,返回-1 */
                if(sem == NULL || sem < sem_list || sem > sem_list + SEM_LIST_LENGTH)
                {
                return -1;
                }
                /* 关中断 */
                cli();
                sem->value++;
                /* 如果有等待sem的进程,它会唤醒其中的一个。 */
                if(sem->value <= 0)
                {
                wake_up(&(sem->queue));
                }
                /* 开中断 */
                sti();
                return 0;
                }
                -

                writei

                // Write data to inode.
                // Caller must hold ip->lock.
                // If user_src==1, then src is a user virtual address;
                // otherwise, src is a kernel address.
                int
                writei(struct inode *ip, int user_src, uint64 src, uint off, uint n)
                {
                uint tot, m;
                struct buf *bp;

                if(off > ip->size || off + n < off)
                return -1;
                // writei会自动增长文件,除非达到文件的最大大小
                if(off + n > MAXFILE*BSIZE)
                return -1;

                for(tot=0; tot<n; tot+=m, off+=m, src+=m){
                bp = bread(ip->dev, bmap(ip, off/BSIZE));
                m = min(n - tot, BSIZE - off%BSIZE);
                if(either_copyin(bp->data + (off % BSIZE), user_src, src, m) == -1) {
                brelse(bp);
                n = -1;
                break;
                }
                log_write(bp);
                brelse(bp);
                }

                if(n > 0){
                if(off > ip->size)
                // 说明扩大了文件大小,需要修改
                ip->size = off;
                // write the i-node back to disk even if the size didn't change
                // because the loop above might have called bmap() and added a new
                // block to ip->addrs[].
                iupdate(ip);
                }

                return n;
                }
                +

                sem_unlink

                +
                /*
                sem_unlink()的功能是删除名为name的信号量。
                返回0表示成功,返回-1表示失败。
                */
                int sys_sem_unlink(const char *name)
                {
                if (name == NULL){
                errno = 1;
                return -1;
                }
                /* 首先将信号量的名称赋值到新建的缓冲区中 */
                char nbuf[20];
                int i;
                for (i = 0; i < 20; i++)
                {
                nbuf[i] = get_fs_byte(name + i);
                if (nbuf[i] == '\0')
                break;
                }
                for (i = 0; i < SEM_LIST_LENGTH; i++)
                {
                if (str_cmp(sem_list[i].name, nbuf)==0)
                {
                sem_list[i].active=0;
                return 0;
                }
                }
                return -1;
                }
                -

                stati

                -

                函数stati将inode元数据复制到stat结构体中,该结构通过stat系统调用向用户程序公开。

                -
                -

                defs.h中可看到inode结构体是private的,而stat是public的。

                -

                Directory layer

                数据结构

                -

                目录的内部实现很像文件。其inode的typeT_DIR,其数据是directory entries的集合。

                -

                每个entry都是一个struct dirent

                -
                -

                也就是说这一层其实本质上是一个大小一定的map,该map自身也存放在inode中,大小为inode的大小,每个表项entry映射了目录名和文件inode。所以接下来介绍的函数我们完全可以从hashmap增删改查的角度去理解。

                -
                // Directory is a file containing a sequence of dirent structures.
                #define DIRSIZ 14

                struct dirent {
                ushort inum;// 如果为0,说明该entry free
                char name[DIRSIZ];
                };
                -

                image-20230124173241241

                -

                相关函数

                dirlookup

                -

                函数dirlookup在directory中搜索具有给定名称的entry。

                -

                它返回的指向enrty.inum相应的inode是非独占的【通过iget获取】,也即无锁状态。它还会把*poff设置为所需的entry的字节偏移量。

                -

                为什么要返回未锁定的inode?是因为调用者已锁定dp,因此,如果对.进行查找,则在返回之前尝试锁定inode将导致重新锁定dp并产生死锁【确实】(还有更复杂的死锁场景,涉及多个进程和..,父目录的别名。.不是唯一的问题。)

                -

                所以锁定交给caller来做。caller可以解锁dp,然后锁定该函数返回的ip,确保它一次只持有一个锁。

                -
                -
                // Look for a directory entry in a directory.
                // If found, set *poff to byte offset of entry.
                struct inode*
                dirlookup(struct inode *dp, char *name, uint *poff)
                {
                uint off, inum;
                struct dirent de;

                if(dp->type != T_DIR)
                panic("dirlookup not DIR");
                // new level of abstraction,可以把directory的inode看作一个表文件,每个表项都是一个entry
                for(off = 0; off < dp->size; off += sizeof(de)){
                // 从directory中获取entry,也即从inode中获取数据
                if(readi(dp, 0, (uint64)&de, off, sizeof(de)) != sizeof(de))
                panic("dirlookup read");
                // free
                if(de.inum == 0)
                continue;
                if(namecmp(name, de.name) == 0){
                // entry matches path element
                if(poff)
                *poff = off;
                inum = de.inum;
                return iget(dp->dev, inum);
                }
                }

                return 0;
                }
                -
                // Write a new directory entry (name, inum) into the directory dp.
                int
                dirlink(struct inode *dp, char *name, uint inum)
                {
                int off;
                struct dirent de;
                struct inode *ip;

                // Check that name is not present.
                if((ip = dirlookup(dp, name, 0)) != 0){
                iput(ip);
                return -1;
                }

                // Look for an empty dirent.
                for(off = 0; off < dp->size; off += sizeof(de)){
                if(readi(dp, 0, (uint64)&de, off, sizeof(de)) != sizeof(de))
                panic("dirlink read");
                if(de.inum == 0)
                break;
                }

                // 如果没找到空闲的则调用writei自动增长inode,添加新表项
                strncpy(de.name, name, DIRSIZ);
                de.inum = inum;
                if(writei(dp, 0, (uint64)&de, off, sizeof(de)) != sizeof(de))
                panic("dirlink");

                return 0;
                }
                +

                pc.c

                +
                #define __LIBRARY__
                #include <stdio.h>
                #include <errno.h>
                #include <linux/sem.h>
                #include <unistd.h>
                #include <sys/stat.h>
                #include <sys/types.h>
                #include <fcntl.h>

                _syscall2(sem_t *, sem_open, const char *, name, unsigned int, value);
                _syscall1(int, sem_wait, sem_t *, sem);
                _syscall1(int, sem_post, sem_t *, sem);
                _syscall1(int, sem_unlink, const char *, name);

                const char* filename="buffer.txt";

                sem_t* empty;
                sem_t* full;
                sem_t* mutex;

                int fd;

                void Producer(){
                int i;
                int tmp;
                char s[5]={0};
                for(i=0;i<=500;i++){
                sem_wait(empty);
                sem_wait(mutex);
                tmp=lseek(fd,0,SEEK_CUR);
                lseek(fd,0,SEEK_END);
                sprintf(s,"%03d\n",i);
                write(fd,s,4);
                lseek(fd,tmp,SEEK_SET);
                sem_post(mutex);
                sem_post(full);
                }
                }

                void Consumer(){
                char s[5]={0};
                sem_wait(full);
                sem_wait(mutex);
                read(fd,s,4);
                printf("%d : %s",getpid(),s);
                sem_post(mutex);
                sem_post(empty);
                }

                int main(){
                int i;
                fd=open(filename,O_RDWR|O_CREAT);
                printf("%d\n",errno);
                empty=sem_open("empty",10);
                full=sem_open("full",0);
                mutex=sem_open("mutex",1);

                if(!fork()){
                Producer();
                return 0;
                }

                for(i=0;i<10;i++){
                if(!fork()){
                int c=50;
                while(c--) Consumer();
                }
                }
                close(fd);
                sem_unlink("empty");
                sem_unlink("full");
                sem_unlink("mutex");

                return 0;
                }
                -

                Pathname layer

                -

                Path name lookup involves a succession of calls to dirlookup, one for each path component.

                -
                -

                namei和nameiparent

                -

                Namei (kernel/fs.c:661) evaluates path and returns the corresponding inode.

                -

                函数nameiparent是一个变体:它在最后一个元素之前停止,返回父目录的inode并将最后一个元素复制到name中。两者都调用通用函数namex来完成实际工作。

                -
                -
                struct inode*
                namei(char *path)
                {
                char name[DIRSIZ];
                return namex(path, 0, name);
                }
                -
                struct inode*
                nameiparent(char *path, char *name)
                {
                return namex(path, 1, name);
                }
                -

                namex

                -

                Namex首先决定路径解析的开始位置。

                -

                如果路径以“ / ”开始,则从根目录开始解析;否则,从当前目录开始。

                -

                然后,它使用skipelem依次考察路径的每个元素。循环的每次迭代都必须在当前索引结点ip中查找name

                -

                迭代首先给ip上锁并检查它是否是一个目录。如果不是,则查找失败。

                -

                如果caller是nameiparent,并且这是最后一个路径元素,则根据nameiparent的定义,循环会提前停止;最后一个路径元素已经复制到name中【在上一轮循坏中做了这件事】,因此namex只需返回解锁的ip

                -

                最后,循环将使用dirlookup查找路径元素,并通过设置ip = next为下一次迭代做准备。当循环用完路径元素时,它返回ip

                -

                注:

                -
                  -
                1. 在每次迭代中锁定ip是必要的,不是因为ip->type可以被更改,而是因为在ilock运行之前,ip->type不能保证已从磁盘加载,所以得用到ilock保证一定会被加载的这个性质。
                2. -
                +

                这部分踩过的坑:

                +
                  +
                1. 在用户态和核心态之间传递参数【这个我没考虑到】

                  +
                  指针参数传递的是应用程序所在地址空间的逻辑地址,
                  在内核中如果直接访问这个地址,访问到的是内核空间中的数据,不会是用户空间的。
                  所以这里还需要一点儿特殊工作,才能在内核中从用户空间得到数据。
                  + +

                  这段代码就是在做这个。

                  +
                  /* 首先将信号量的名称赋值到新建的缓冲区中 */
                  char nbuf[20];
                  int i = 0;
                  for(; i< 20; i++)
                  {
                  nbuf[i] = get_fs_byte(name+i);
                  }
                2. +
                3. 这一段代码值得学习

                  +
                  # ifndef _SEM_H_
                  # define _SEM_H_
                4. +
                5. 一个第一眼看傻掉了的问题

                  +
                  //sleep函数的签名
                  void sleep_on(struct task_struct **p);
                  //一开始初始化队列为空
                  sem_list[i].queue = NULL;
                  //使用sleep
                  sleep_on(&(sem->queue));
                  + +

                  如果队列为空的时候,传入sleep_on的是不是NULL呢?

                  +

                  其实这个本质上是type* p=NULL,&p是不是NULL的问题。虽然知道不是,但还是写个程序测试一下:

                  +
                  #include <stdio.h> 
                  typedef struct {
                  int value;
                  }haha;

                  void isNULL(haha** a){
                  printf("%d",a==NULL);
                  }

                  int main(){
                  haha *h=NULL;
                  isNULL(&h);
                  return 0;
                  }
                  //result:0
                6. +
                7. sem_post签名与实现矛盾

                  +
                  wake_up() 的功能是唤醒链表上睡眠的所有进程。
                  sem_post() 就是信号量的 V 原子操作。如果有等待 sem 的进程,它会唤醒其中的一个。
                  + +

                  以上都是指导书的内容。这个“所有”和“一个”的用意我不大明白。也许唤醒所有进程,其中一个抢到了锁,其他的全睡了,这个也被认为是唤醒其中一个吧()

                  +
                8. +
                9. 聪明的越界处理【未考虑到】

                  +
                  /* 判断:如果传入的信号量是无效信号量,V操作失败,返回-1 */
                  if(sem == NULL || sem < sem_list || sem > sem_list + SEM_LIST_LENGTH)
                  {
                  return -1;
                  }
                  + +

                  毕竟有效的信号量都是引用的信号量表的信号量。所以地址越界的自然无效。

                  +
                10. +
                11. 最坑的一点

                  +

                  其实指导书提醒了

                  +
                  +

                  下面描述的问题未必具有普遍意义,仅做为提醒,请实验者注意。

                  +

                  include/string.h 实现了全套的 C 语言字符串操作,而且都是采用汇编 + inline 方式优化。

                  +

                  但在使用中,某些情况下可能会遇到一些奇怪的问题。比如某人就遇到 strcmp() 会破坏参数内容的问题。如果调试中遇到有些 “诡异” 的情况,可以试试不包含头文件,一般都能解决。不包含 string.h,就不会用 inline 方式调用这些函数,它们工作起来就趋于正常了。

                  -
                  // Look up and return the inode for a path name.
                  // If parent != 0, return the inode for the parent and copy the final
                  // path element into name, which must have room for DIRSIZ bytes.
                  // Must be called inside a transaction since it calls iput().
                  static struct inode*
                  namex(char *path, int nameiparent, char *name)
                  {
                  struct inode *ip, *next;

                  if(*path == '/')
                  ip = iget(ROOTDEV, ROOTINO);
                  else
                  ip = idup(myproc()->cwd);

                  // 使用skipelem依次考察路径的每个元素
                  while((path = skipelem(path, name)) != 0){
                  ilock(ip);
                  if(ip->type != T_DIR){
                  iunlockput(ip);
                  return 0;
                  }
                  if(nameiparent && *path == '\0'){
                  // Stop one level early.
                  iunlock(ip);
                  return ip;
                  }
                  if((next = dirlookup(ip, name, 0)) == 0){
                  iunlockput(ip);
                  return 0;
                  }
                  iunlockput(ip);
                  ip = next;
                  }
                  if(nameiparent){
                  iput(ip);
                  return 0;
                  }
                  return ip;
                  }
                  +

                  但是具体表现跟它说的差距有点大()

                  +

                  我是全部检查没问题了,然后上linux0.11真机运行。PID:number这样的信息全部打印出来了,没啥问题,但是打印完操作系统就会寄,大多数极端情况就直接重启了,小部分还会温和地提醒以下报错信息然后死循环

                  +
                  kernel panic: trying to free up swapper memory space
                  in swapper task - not syncing
                  +

                  最后尝试着修改去掉string.h,才得到了正确的结果,泪目。

                  +
                12. +
                +

                proc文件系统

                +

                参考文章:

                +
                操作系统实验08-proc文件系统的实现
                -

                namex过程可能需要很长时间才能完成:它可能涉及多个磁盘操作来读取路径名中所遍历目录的索引节点和目录块(如果它们不在buffer cache中)。

                -

                Xv6 is carefully designed,如果一个内核线程对namex的调用在磁盘I/O上阻塞,另一个查找不同路径名的内核线程可以同时进行。Namex locks each directory in the path separately so that lookups in different directories can proceed in parallel.锁细粒度化

                -

                This concurrency introduces some challenges. For example, while one kernel thread is looking up a pathname another kernel thread may be changing the directory tree by unlinking a directory. A potential risk is that a lookup may be searching a directory that has been deleted by another kernel thread and its blocks have been re-used for another directory or file.一个潜在的风险是,查找可能正在搜索已被另一个内核线程删除且其块已被重新用于另一个目录或文件的目录。

                -

                Xv6避免了这种竞争,也就是说,你查到的inode保证暂时不会被释放,里面的内容还是真的,而不会被重新利用从而导致里面的内容变样。

                -

                例如,在namex中执行dirlookup时,lookup线程持有目录上的锁,dirlookup返回使用iget获得的inode。Iget增加索引节点的引用计数。只有在从dirlookup接收inode之后,namex才会释放目录上的锁。现在,另一个线程可以从目录中取消inode的链接,但是xv6还不会删除inode,因为inode的引用计数仍然大于零

                -

                另一个风险是死锁。例如,查找“.”时,next指向与ip相同的inode【确实】。在释放ip上的锁之前锁定next将导致死锁【为什么???难道不是会由于在acquire时已经持有锁,从而爆panic("acquire")吗?】。为了避免这种死锁,namex在获得下一个目录的锁之前解锁该目录。这里我们再次看到为什么igetilock之间的分离很重要。

                +

                在 Linux 0.11 上实现 procfs(proc 文件系统)内的 psinfo 结点。当读取此结点的内容时,可得到系统当前所有进程的状态信息。例如,用 cat 命令显示 /proc/psinfo/proc/hdinfo的内容,可得到:

                +
                $ cat /proc/psinfo
                pid state father counter start_time
                0 1 -1 0 0
                1 1 0 28 1
                4 1 1 1 73
                3 1 1 27 63
                6 0 4 12 817
                $ cat /proc/hdinfo
                total_blocks: 62000;
                free_blocks: 39037;
                used_blocks: 22963;
                ...
                + +

                procfs 及其结点要在内核启动时自动创建。

                +

                相关功能实现在 fs/proc.c 文件内。

                -

                File descriptor layer

                -

                Unix的一个很酷的方面是,Unix中的大多数资源都表示为文件,包括控制台、管道等设备,当然还有真实文件。文件描述符层是实现这种一致性的层。

                +

                必备知识

                要点1 procfs简介

                +

                正式的 Linux 内核实现了 procfs,它是一个**虚拟文件系统**,通常被 mount(挂载) 到 /proc 目录上,通过虚拟文件和虚拟目录的方式提供访问系统参数的机会,所以有人称它为 “了解系统信息的一个窗口”。

                +

                这些虚拟的文件和目录**并没有真实地存在在磁盘**上,而是内核中各种数据的一种直观表示。虽然是虚拟的,但它们都可以通过标准的系统调用(open()read() 等)访问。

                +

                其实,Linux 的很多系统命令就是通过读取 /proc 实现的。例如 uname -a 的部分信息就来自 /proc/version,而 uptime 的部分信息来自 /proc/uptime/proc/loadavg

                -

                数据结构

                -

                Xv6为每个进程提供了自己的打开文件表或文件描述符。每个打开的文件都由一个struct file表示,它是inode或管道的封装,加上一个I/O偏移量。

                -

                每次调用open都会创建一个新的打开文件(一个新的struct file):如果多个进程独立地打开同一个文件,那么不同的实例将具有不同的I/O偏移量。

                -

                另一方面,单个打开的文件(同一个struct file)可以多次出现在一个进程的文件表中,也可以出现在多个进程的文件表中。如果一个进程使用open打开文件,然后使用dup创建别名,或使用fork与子进程共享,就会发生这种情况。

                +

                要点2 基本思路

                +

                Linux 是通过文件系统接口实现 procfs,并在启动时自动将其 mount 到 /proc 目录上。

                +

                此目录下的所有内容都是随着系统的运行自动建立、删除和更新的,而且它们完全存在于内存中,不占用任何外存空间。

                +

                Linux 0.11 还没有实现虚拟文件系统,也就是,还没有提供增加新文件系统支持的接口。所以本实验只能在现有文件系统的基础上,通过打补丁的方式模拟一个 procfs

                +

                Linux 0.11 使用的是 Minix 的文件系统,这是一个典型的基于 inode 的文件系统,《注释》一书对它有详细描述。它的每个文件都要对应至少一个 inode,而 inode 中记录着文件的各种属性,包括文件类型。文件类型有普通文件、目录、字符设备文件和块设备文件等。在内核中,每种类型的文件都有不同的处理函数与之对应。我们可以增加一种新的文件类型——proc 文件,并在相应的处理函数内实现 procfs 要实现的功能

                -
                struct file {
                enum { FD_NONE, FD_PIPE, FD_INODE, FD_DEVICE } type;
                int ref; // reference count
                char readable;
                char writable;
                struct pipe *pipe; // FD_PIPE
                struct inode *ip; // FD_INODE and FD_DEVICE
                uint off; // FD_INODE
                short major; // FD_DEVICE
                };
                +

                步骤

                要点1 新增proc文件类型

                include/sys/stat.h 新增:

                +
                #define S_IFPROC 0050000

                #define S_ISPROC(m) (((m) & S_IFMT) == S_IFPROC)
                -

                ftable

                -

                所有在系统中打开的文件都会被放入global file tableftable中。

                -

                ftable具有分配文件(filealloc)、创建重复引用(filedup)、释放引用(fileclose)以及读取和写入数据(filereadfilewrite)的函数。

                -

                前三个都很常规,跟之前的xxalloc、xxfree的思路是一样的。

                -

                函数filestatfilereadfilewrite实现对文件的statreadwrite操作。

                +

                要点2 修改mknod()函数和init()函数

                +

                psinfo 结点要通过 mknod() 系统调用建立,所以要让它支持新的文件类型。

                -

                filealloc

                // Allocate a file structure.
                struct file*
                filealloc(void)
                {
                struct file *f;

                acquire(&ftable.lock);
                for(f = ftable.file; f < ftable.file + NFILE; f++){
                if(f->ref == 0){
                f->ref = 1;
                release(&ftable.lock);
                return f;
                }
                }
                release(&ftable.lock);
                return 0;
                }
                +

                直接修改 fs/namei.c 文件中的 sys_mknod() 函数中的一行代码,如下:

                +
                if (S_ISBLK(mode) || S_ISCHR(mode) || S_ISPROC(mode))
                inode->i_zone[0] = dev;
                // 文件系统初始化
                -

                filedup

                // Increment ref count for file f.
                struct file*
                filedup(struct file *f)
                {
                acquire(&ftable.lock);
                if(f->ref < 1)
                panic("filedup");
                f->ref++;
                release(&ftable.lock);
                return f;
                }
                +
                +

                内核初始化的全部工作是在 main() 中完成,而 main() 在最后从内核态切换到用户态,并调用 init()

                +

                init() 做的第一件事情就是挂载根文件系统:

                +
                void init(void) { 
                // ……
                setup((void *) &drive_info);
                // ……
                }
                -

                fileclose

                // Close file f.  (Decrement ref count, close when reaches 0.)
                void
                fileclose(struct file *f)
                {
                struct file ff;

                acquire(&ftable.lock);
                if(f->ref < 1)
                panic("fileclose");
                if(--f->ref > 0){
                release(&ftable.lock);
                return;
                }
                ff = *f;
                f->ref = 0;
                f->type = FD_NONE;
                release(&ftable.lock);

                if(ff.type == FD_PIPE){
                pipeclose(ff.pipe, ff.writable);
                } else if(ff.type == FD_INODE || ff.type == FD_DEVICE){
                begin_op();
                iput(ff.ip);
                end_op();
                }
                }
                +

                procfs 的初始化工作**应该在根文件系统挂载之后开始**。它包括两个步骤:

                +
                  +
                • (1)建立 /proc 目录;建立 /proc 目录下的各个结点。本实验只建立 /proc/psinfo

                  +
                • +
                • (2)建立目录和结点分别需要调用 mkdir()mknod() 系统调用。因为初始化时已经在用户态,所以不能直接调用 sys_mkdir()sys_mknod()。必须在初始化代码所在文件中实现这两个系统调用的用户态接口。

                  +
                  #ifndef __LIBRARY__
                  #define __LIBRARY__
                  #endif

                  _syscall2(int,mkdir,const char*,name,mode_t,mode);
                  _syscall3(int,mknod,const char*,filename,mode_t,mode,dev_t,dev);
                  -

                  filestat

                  -

                  Filestat只允许在inode上操作并且调用了stati

                  +

                  mkdir() 时 mode 参数的值可以是 “0755”(对应 rwxr-xr-x),表示只允许 root 用户改写此目录,其它人只能进入和读取此目录。

                  +

                  procfs 是一个只读文件系统,所以用 mknod() 建立 psinfo 结点时,必须通过 mode 参数将其设为只读。建议使用 S_IFPROC|0444 做为 mode 值,表示这是一个 proc 文件,权限为 0444(r–r–r–),对所有用户只读。

                  +

                  mknod() 的第三个参数 dev 用来说明结点所代表的设备编号。对于 procfs 来说,此编号可以完全自定义。proc 文件的处理函数将通过这个编号决定对应文件包含的信息是什么。例如,可以把 0 对应 psinfo,1 对应 meminfo,2 对应 cpuinfo。

                  +
                • +
                -
                // Get metadata about file f.
                // addr is a user virtual address, pointing to a struct stat.
                int
                filestat(struct file *f, uint64 addr)
                {
                struct proc *p = myproc();
                struct stat st;

                // 仅允许文件/设备执行
                if(f->type == FD_INODE || f->type == FD_DEVICE){
                ilock(f->ip);
                stati(f->ip, &st);
                iunlock(f->ip);
                if(copyout(p->pagetable, addr, (char *)&st, sizeof(st)) < 0)
                return -1;
                return 0;
                }
                return -1;
                }
                +

                也就是说,打开linux-0.11/init/main.c

                +

                加入:

                +
                #ifndef __LIBRARY__
                #define __LIBRARY__
                #endif

                _syscall2(int,mkdir,const char*,name,mode_t,mode);
                _syscall3(int,mknod,const char*,filename,mode_t,mode,dev_t,dev);
                -

                fileread

                // Read from file f.
                // addr is a user virtual address.
                int
                fileread(struct file *f, uint64 addr, int n)
                {
                int r = 0;

                // 首先检查是否可读
                if(f->readable == 0)
                return -1;

                if(f->type == FD_PIPE){
                r = piperead(f->pipe, addr, n);
                } else if(f->type == FD_DEVICE){
                if(f->major < 0 || f->major >= NDEV || !devsw[f->major].read)
                return -1;
                r = devsw[f->major].read(1, addr, n);
                } else if(f->type == FD_INODE){
                ilock(f->ip);
                if((r = readi(f->ip, 1, addr, f->off, n)) > 0)
                // 移动文件指针偏移量
                f->off += r;
                iunlock(f->ip);
                } else {
                panic("fileread");
                }

                return r;
                }
                +

                在init函数中,添加:

                +
                mkdir("/proc",0755);
                mknod("/proc/psinfo",S_IFPROC|0400,0);
                //其余文件以此类推...
                -

                Code: System calls

                -

                通过使用底层提供的函数,大多数系统调用的实现都很简单(请参阅***kernel/sysfile.c***)。有几个调用值得仔细看看。

                -

                以下介绍的函数都在kernel/sysfile.c中。

                -
                -

                这个函数的功能是给文件old加上一个链接,这个链接存在于文件new的父目录。感觉也就相当于把文件从old复制到new处了。具体实现逻辑就是要给该文件所在目录添加一个entry,name=新名字,inode=该文件的inode。

                -
                // Create the path new as a link to the same inode as old.
                uint64
                sys_link(void)
                {
                char name[DIRSIZ], new[MAXPATH], old[MAXPATH];
                struct inode *dp, *ip;

                if(argstr(0, old, MAXPATH) < 0 || argstr(1, new, MAXPATH) < 0)
                return -1;

                // 首先先增加nlink
                begin_op();
                // 通过path找到ip结点
                if((ip = namei(old)) == 0){
                end_op();
                return -1;
                }

                ilock(ip);
                // directory不能被link
                if(ip->type == T_DIR){
                iunlockput(ip);
                end_op();
                return -1;
                }

                ip->nlink++;
                // 修改一次字段就需要update一次
                iupdate(ip);
                iunlock(ip);

                // 然后再在目录中登记新的entry
                // 找到new的parent,也即new所在目录
                if((dp = nameiparent(new, name)) == 0)
                goto bad;
                ilock(dp);
                // 在目录中添加一个entry,名字为给定的新名字,inode依旧为原来的inode
                // new的父目录必须存在并且与现有inode位于同一设备上
                if(dp->dev != ip->dev || dirlink(dp, name, ip->inum) < 0){
                iunlockput(dp);
                goto bad;
                }
                iunlockput(dp);
                iput(ip);

                end_op();

                return 0;

                bad:
                ilock(ip);
                ip->nlink--;
                iupdate(ip);
                iunlockput(ip);
                end_op();
                return -1;
                }
                +

                编译运行即可看到:

                + -

                create

                -

                它是三个文件创建系统调用的泛化:带有O_CREATE标志的open生成一个新的普通文件,mkdir生成一个新目录,mkdev生成一个新的设备文件。

                +
                +

                这些信息至少说明,psinfo 被正确 open() 了。所以我们不需要对 sys_open() 动任何手脚,唯一要打补丁的,是 sys_read()

                -

                创建一个新的inode结点,结点名包含在path内。返回一个锁定的inode。

                -

                由于使用了iupdate等,所以该函数只能在事务中被调用。

                -
                static struct inode*
                create(char *path, short type, short major, short minor)
                {
                struct inode *ip, *dp;
                char name[DIRSIZ];

                // 获取结点父目录
                if((dp = nameiparent(path, name)) == 0)
                return 0;

                ilock(dp);

                if((ip = dirlookup(dp, name, 0)) != 0){
                // 说明文件已存在
                iunlockput(dp);
                ilock(ip);
                if(type == T_FILE && (ip->type == T_FILE || ip->type == T_DEVICE))
                // 说明此时caller为open(type == T_FILE),open调用create只能是用于创建文件
                return ip;
                iunlockput(ip);
                return 0;
                }

                if((ip = ialloc(dp->dev, type)) == 0)
                panic("create: ialloc");

                ilock(ip);
                ip->major = major;
                ip->minor = minor;
                ip->nlink = 1;
                iupdate(ip);

                if(type == T_DIR){ // Create . and .. entries.
                dp->nlink++; // for ".."
                iupdate(dp);
                // No ip->nlink++ for ".": avoid cyclic ref count.
                // 所以其实.和..本质上是link
                if(dirlink(ip, ".", ip->inum) < 0 || dirlink(ip, "..", dp->inum) < 0)
                panic("create dots");
                }

                if(dirlink(dp, name, ip->inum) < 0)
                panic("create: dirlink");

                iunlockput(dp);

                return ip;
                }
                - -

                sys_mkdir

                uint64
                sys_mkdir(void)
                {
                char path[MAXPATH];
                struct inode *ip;

                begin_op();
                if(argstr(0, path, MAXPATH) < 0 || (ip = create(path, T_DIR, 0, 0)) == 0){
                end_op();
                return -1;
                }
                iunlockput(ip);
                end_op();
                return 0;
                }
                - -

                sys_open

                -

                Sys_open是最复杂的,因为创建一个新文件只是它能做的一小部分。

                +

                要点3 修改read(),让proc可读

                +

                首先分析 sys_read(在文件 fs/read_write.c 中)

                +

                要在这里一群 if 的排比中,加上 S_IFPROC() 的分支,进入对 proc 文件的处理函数。需要传给处理函数的参数包括:

                +
                  +
                • inode->i_zone[0],这就是 mknod() 时指定的 dev ——设备编号
                • +
                • buf,指向用户空间,就是 read() 的第二个参数,用来接收数据
                • +
                • count,就是 read() 的第三个参数,说明 buf 指向的缓冲区大小
                • +
                • &file->f_posf_pos 是上一次读文件结束时“文件位置指针”的指向。这里必须传指针,因为处理函数需要根据传给 buf 的数据量修改 f_pos 的值。
                • +
                -
                uint64
                sys_open(void)
                {
                char path[MAXPATH];
                int fd, omode;
                struct file *f;
                struct inode *ip;
                int n;

                if((n = argstr(0, path, MAXPATH)) < 0 || argint(1, &omode) < 0)
                return -1;

                begin_op();

                if(omode & O_CREATE){
                ip = create(path, T_FILE, 0, 0);
                // 创建失败
                if(ip == 0){
                end_op();
                return -1;
                }
                } else {
                // 文件不存在
                if((ip = namei(path)) == 0){
                end_op();
                return -1;
                }
                // Create返回一个锁定的inode,但namei不锁定,因此sys_open必须锁定inode本身。
                ilock(ip);
                // 非文件,为目录并且非只读
                // 所以说想要open一个目录的话只能以只读模式打开
                if(ip->type == T_DIR && omode != O_RDONLY){
                iunlockput(ip);
                end_op();
                return -1;
                }
                }

                if(ip->type == T_DEVICE && (ip->major < 0 || ip->major >= NDEV)){
                iunlockput(ip);
                end_op();
                return -1;
                }

                // 获取file结构体和文件描述符。
                if((f = filealloc()) == 0 || (fd = fdalloc(f)) < 0){
                if(f)
                fileclose(f);
                iunlockput(ip);
                end_op();
                return -1;
                }

                // 没有其他进程可以访问部分初始化的文件,因为它仅位于当前进程的表中,因而这里可以不用上锁
                if(ip->type == T_DEVICE){
                f->type = FD_DEVICE;
                f->major = ip->major;
                } else {
                f->type = FD_INODE;
                f->off = 0;
                }
                f->ip = ip;
                f->readable = !(omode & O_WRONLY);
                f->writable = (omode & O_WRONLY) || (omode & O_RDWR);

                // 如果使用了这个标志,调用 open 函数打开文件的时候会将文件原本的内容全部丢弃,文件大小变为 0。
                if((omode & O_TRUNC) && ip->type == T_FILE){
                itrunc(ip);
                }

                iunlock(ip);
                end_op();

                return fd;
                }
                - -

                sys_pipe

                uint64
                sys_pipe(void)
                {
                uint64 fdarray; // user pointer to array of two integers用来接收pipe两端的文件描述符
                struct file *rf, *wf;
                int fd0, fd1;
                struct proc *p = myproc();

                if(argaddr(0, &fdarray) < 0)
                return -1;
                if(pipealloc(&rf, &wf) < 0)
                return -1;
                fd0 = -1;
                if((fd0 = fdalloc(rf)) < 0 || (fd1 = fdalloc(wf)) < 0){
                if(fd0 >= 0)
                p->ofile[fd0] = 0;
                fileclose(rf);
                fileclose(wf);
                return -1;
                }
                if(copyout(p->pagetable, fdarray, (char*)&fd0, sizeof(fd0)) < 0 ||
                copyout(p->pagetable, fdarray+sizeof(fd0), (char *)&fd1, sizeof(fd1)) < 0){
                p->ofile[fd0] = 0;
                p->ofile[fd1] = 0;
                fileclose(rf);
                fileclose(wf);
                return -1;
                }
                return 0;
                }
                +

                依照指导书,在read_write.c添加如下语句:

                +
                extern int proc_handler(unsigned short dev,char* buf,int count,off_t* f_pos);

                int sys_read(...){
                // ...
                if(S_ISPROC(inode->i_mode)){
                return proc_handler(inode->i_zone[0],buf,count,&file->f_pos);
                }
                // ...
                }
                -

                Real world

                -

                实际操作系统中的buffer cache比xv6复杂得多,但它有两个相同的用途:缓存和同步对磁盘的访问。

                -

                与UNIX V6一样,Xv6的buffer cache使用简单的最近最少使用(LRU)替换策略;有许多更复杂的策略可以实现,每种策略都适用于某些工作场景,而不适用于其他工作场景。更高效的LRU缓存将消除链表,而改为使用哈希表进行查找,并使用堆进行LRU替换【跟我们在lock中实现的一样,再多个堆优化】。现代buffer cache通常与虚拟内存系统集成,以支持内存映射文件。

                -

                Xv6的日志系统效率低下。提交不能与文件系统调用同时发生。系统记录整个块,即使一个块中只有几个字节被更改。它执行同步日志写入,每次写入一个块,每个块可能需要整个磁盘旋转时间。真正的日志系统解决了所有这些问题。

                -

                文件系统布局中最低效的部分是目录,它要求在每次查找期间对所有磁盘块进行线性扫描【确实】。当目录只有几个磁盘块时,这是合理的,但对于包含许多文件的目录来说,开销巨大。Microsoft Windows的NTFS、Mac OS X的HFS和Solaris的ZFS(仅举几例)将目录实现为磁盘上块的平衡树。这很复杂,但可以保证目录查找在对数时间内完成(即时间复杂度为O(logn))。

                -

                Xv6对于磁盘故障的解决很初级:如果磁盘操作失败,Xv6就会调用panic。这是否合理取决于硬件:如果操作系统位于使用冗余屏蔽磁盘故障的特殊硬件之上,那么操作系统可能很少看到故障,因此panic是可以的。另一方面,使用普通磁盘的操作系统应该预料到会出现故障,并能更优雅地处理它们,这样一个文件中的块丢失不会影响文件系统其余部分的使用。

                -

                Xv6要求文件系统安装在单个磁盘设备上,且大小不变。随着大型数据库和多媒体文件对存储的要求越来越高,操作系统正在开发各种方法来消除“每个文件系统一个磁盘”的瓶颈。基本方法是将多个物理磁盘组合成一个逻辑磁盘。RAID等硬件解决方案仍然是最流行的,但当前的趋势是在软件中尽可能多地实现这种逻辑。这些软件实现通常允许通过动态添加或删除磁盘来扩展或缩小逻辑设备等丰富功能。当然,一个能够动态增长或收缩的存储层需要一个能够做到这一点的文件系统:xv6使用的固定大小的inode块阵列在这样的环境中无法正常工作。将磁盘管理与文件系统分离可能是最干净的设计,但两者之间复杂的接口导致了一些系统(如Sun的ZFS)将它们结合起来。

                -

                Xv6的文件系统缺少现代文件系统的许多其他功能;例如,它缺乏对快照和增量备份的支持。

                -

                现代Unix系统允许使用与磁盘存储相同的系统调用访问多种资源:命名管道、网络连接、远程访问的网络文件系统以及监视和控制接口,如/proc。不同于xv6中filereadfilewriteif语句,这些系统通常为每个打开的文件提供一个函数指针表【确实有印象】,每个操作一个,并通过函数指针来援引inode的调用实现。网络文件系统和用户级文件系统提供了将这些调用转换为网络RPC并在返回之前等待响应的函数。

                -

                (注:Linux 内核提供了一种通过/proc文件系统,在运行时访问内核内部数据结构、改变内核设置的机制。proc文件系统是一个伪文件系统,它只存在内存当中,而不占用外存空间。它以文件系统的方式为访问系统内核数据的操作提供接口。)

                -
                -

                Lab: file system

                -

                In this lab you will add large files【大文件支持】 and symbolic links【软链接】 to the xv6 file system.

                -

                不过做完这个实验,给我的一种感觉就是磁盘管理和内存管理真的有很多相似之处,不过也许它们所代表的思想也很普遍。

                -
                -

                Large files

                实验内容

                Overview
                -

                In this assignment you’ll increase the maximum size of an xv6 file.

                -

                Currently xv6 files are limited to 268 blocks, or 268*BSIZE bytes (BSIZE is 1024 in xv6). This limit comes from the fact that an xv6 inode contains 12 “direct” block numbers and one “singly-indirect” block number, which refers to a block that holds up to 256 more block numbers, for a total of 12+256=268 blocks.

                -

                You’ll change the xv6 file system code to support a “doubly-indirect” block in each inode, containing 256 addresses of singly-indirect blocks, each of which can contain up to 256 addresses of data blocks. The result will be that a file will be able to consist of up to 65803 blocks, or 256*256+256+11 blocks (11 instead of 12, because we will sacrifice one of the direct block numbers for the double-indirect block).

                +

                要点4 编写pro文件的处理函数

                +

                proc 文件的处理函数的功能是根据设备编号,把不同的内容写入到用户空间的 buf。写入的数据要从 f_pos 指向的位置开始,每次最多写 count 个字节,并根据实际写入的字节数调整 f_pos 的值,最后返回实际写入的字节数。当设备编号表明要读的是 psinfo 的内容时,就要按照 psinfo 的形式组织数据。

                +

                实现此函数可能要用到如下几个函数:

                +
                  +
                • malloc() 函数
                • +
                • free() 函数
                • +
                +

                包含 linux/kernel.h 头文件后,就可以使用 malloc()free() 函数。它们是可以被核心态代码调用的,唯一的限制是一次申请的内存大小不能超过一个页面。

                -
                Preliminaries
                -

                If at any point during the lab you find yourself having to rebuild the file system from scratch, you can run make clean which forces make to rebuild fs.img.

                +
                +

                进程的信息就来源于内核全局结构数组 struct task_struct * task[NR_TASKS] 中,具体读取细节可参照 sched.c 中的函数 schedule()

                +

                可以借鉴一下代码:

                +
                for(p = &LAST_TASK ; p > &FIRST_TASK ; --p)

                if (*p)
                (*p)->counter = ((*p)->counter >> 1)+...;
                -
                What to Look At

                意思就是要我们去看一眼fs.h,bmap,以及了解一下逻辑地址bn如何转化为blockno。这个我是知道的。

                -
                Your Job
                -

                Modify bmap() so that it implements a doubly-indirect block, in addition to direct blocks and a singly-indirect block.

                -

                You’ll have to have only 11 direct blocks, rather than 12, to make room for your new doubly-indirect block; you’re not allowed to change the size of an on-disk inode.

                -

                The first 11 elements of ip->addrs[] should be direct blocks; the 12th should be a singly-indirect block (just like the current one); the 13th should be your new doubly-indirect block. You are done with this exercise when bigfile writes 65803 blocks and usertests runs successfully.

                +
                +

                cat 是 Linux 下的一个常用命令,功能是将文件的内容打印到标准输出。

                +

                它核心实现大体如下:

                +
                #include <stdio.h>
                #include <unistd.h>
                int main(int argc, char* argv[])
                {
                char buf[513] = {'\0'};
                int nread;

                int fd = open(argv[1], O_RDONLY, 0);
                while(nread = read(fd, buf, 512))
                {
                buf[nread] = '\0';
                puts(buf);
                }

                return 0;
                }
                -

                感想

                意外地很简单()在此不多做赘述,直接上代码。

                -

                唯一要注意的一点就是记得在itrunc中free掉

                -

                image-20230124232433793

                -

                代码

                修改定义
                // in fs.h
                #define NDIRECT 11
                #define NINDIRECT (BSIZE / sizeof(uint))
                #define NDOUBLEINDIRECT ((BSIZE/sizeof(uint))*(BSIZE/sizeof(uint)))
                #define MAXFILE (NDIRECT + NINDIRECT + NDOUBLEINDIRECT)

                // On-disk inode structure
                struct dinode {
                // ...
                uint addrs[NDIRECT+2]; // Data block addresses
                };
                - -
                // in file.h
                // in-memory copy of an inode
                struct inode {
                // ...
                uint addrs[NDIRECT+2];
                };
                - -
                修改bmap()
                // in fs.c
                // 调试用
                static int cnt = 0;

                static uint
                bmap(struct inode *ip, uint bn)
                {
                uint addr, *a;
                struct buf *bp;

                if(bn < NDIRECT){
                if((addr = ip->addrs[bn]) == 0)
                ip->addrs[bn] = addr = balloc(ip->dev);
                return addr;
                }
                bn -= NDIRECT;

                if(bn < NINDIRECT){
                // Load indirect block, allocating if necessary.
                if((addr = ip->addrs[NDIRECT]) == 0)
                ip->addrs[NDIRECT] = addr = balloc(ip->dev);
                bp = bread(ip->dev, addr);
                a = (uint*)bp->data;
                if((addr = a[bn]) == 0){
                a[bn] = addr = balloc(ip->dev);
                log_write(bp);
                }
                brelse(bp);
                return addr;
                }

                // CODE HERE
                bn -= NINDIRECT;
                if(bn < NDOUBLEINDIRECT){
                // 调试用
                if(bn/10000 > cnt){
                cnt++;
                printf("double_indirect:%d\n",bn);
                }
                // 第一层
                if((addr = ip->addrs[NDIRECT+1]) == 0)
                ip->addrs[NDIRECT+1] = addr = balloc(ip->dev);
                // 第二层
                bp = bread(ip->dev,addr);
                a = (uint*)bp->data;
                if((addr = a[(bn >> 8)]) == 0){
                a[(bn >> 8)] = addr = balloc(ip->dev);
                log_write(bp);
                }
                brelse(bp);
                // 第三层
                bp = bread(ip->dev,addr);
                a = (uint*)bp->data;
                if((addr = a[(bn & 0x00FF)]) == 0){
                a[(bn & 0x00FF)] = addr = balloc(ip->dev);
                log_write(bp);
                }
                brelse(bp);
                return addr;
                }

                panic("bmap: out of range");
                }
                +

                在cat的代码中,open函数返回了psinfo的文件描述符,read函数读到该文件描述符,就会识别出我们要读写的文件是PROC类型的,因此就会跳转到我们的proc_handler去执行,再进一步跳转到psinfo_handler执行。根据cat的代码和指导书的提示,不难得出,我们的目标就是把进程的信息按照格式给弄进buf里面,就可以了。

                +

                而这也正体现了proc作为“**虚拟文件**”的特点。对它进行读写,它的信息并非存放在磁盘中,而是全部由放在内存中的逻辑和数据【由task_struct提供】来完成。

                +

                在fs文件夹下创建文件proc_dev.c,编写proc文件的处理函数。代码如下:

                +
                #include <linux/fs.h>
                #include <unistd.h>
                #include <asm/segment.h>
                #include <stdarg.h>
                #include <linux/sched.h>
                #include <sys/types.h>
                #include <linux/kernel.h>

                #define set_bit(nr,addr) ({\
                register int res ; \
                __asm__ __volatile__("btsl %2,%3\n\tsetb %%al": \
                "=a" (res):"0" (0),"r" (nr),"m" (*(addr))); \
                res;})

                struct task_struct** p=&FIRST_TASK;
                char s[100];
                int flag;

                extern int psinfo_handler(off_t* f_pos,char* buf);
                extern int hdinfo_handler(off_t* f_pos,char* buf);

                int proc_handler(unsigned short dev,char* buf,int count,off_t* f_pos){
                //根据设备编号,把不同的内容写入到用户空间的 buf
                switch(dev){
                case 0:
                return psinfo_handler(f_pos,buf);
                case 1:
                return hdinfo_handler(f_pos,buf);
                default:
                break;
                }
                return -1;
                }

                //在内核态和用户态间传递数据
                int put_into_buf(char* buf,char* s){
                int cnt=0;
                while(s[cnt]!='\0'){
                put_fs_byte(s[cnt++],buf++);
                }
                return cnt;
                }

                int sprintf(char* buf,const char* fmt,...){
                va_list args;int i;
                va_start(args,fmt);
                i=vsprintf(buf,fmt,args);
                va_end(args);
                return i;
                }

                int psinfo_handler(off_t* f_pos,char* buf){
                int i;
                //初始化字符串
                for(i=0;i<100;i++) s[i]=0;

                //如果是第一次read,需要在屏幕上打印列表头,并且重置p指针为进程队列头
                if((*f_pos)==0){
                sprintf(s,"pid\tstate\tfather\tcounter\tstart_time\n");
                p=&FIRST_TASK;
                }
                //到达文件末尾
                if((*p)==NULL){
                return 0;
                }

                //每次仅输出一行
                if((*f_pos)!=0){
                sprintf(s,"%ld\t%ld\t%ld\t%ld\t%ld\n",(*p)->pid,(*p)->state,(*p)->father,(*p)->counter,(*p)->start_time);
                p++;
                }

                int cnt=put_into_buf(buf,s);
                *f_pos+=cnt;

                return cnt;
                }

                //可参考fs/super.c mount_root()
                int hdinfo_handler(off_t* f_pos,char* buf){
                //防止循环多次打印
                if(flag==1){
                flag=0;
                return -1;
                }
                struct super_block* sb;
                sb=get_super(0x301);/*磁盘设备号 3*256+1*/

                int free=0;
                int i=sb->s_nzones;
                while (-- i >= 0)
                if (!set_bit(i&8191,sb->s_zmap[i>>13]->b_data))
                free++;
                sprintf(s,"total_blocks:\t%d\nfree_blocks:\t%d\nused_blocks:\t%d\n",sb->s_nzones,free,sb->s_nzones-free);
                int cnt=put_into_buf(buf,s);
                flag=1;
                return cnt;
                }

                -
                修改itrunc
                // Truncate inode (discard contents).
                // Caller must hold ip->lock.
                void
                itrunc(struct inode *ip)
                {
                int i, j;
                struct buf *bp;
                uint *a;

                for(i = 0; i < NDIRECT; i++){
                if(ip->addrs[i]){
                bfree(ip->dev, ip->addrs[i]);
                ip->addrs[i] = 0;
                }
                }

                if(ip->addrs[NDIRECT]){
                bp = bread(ip->dev, ip->addrs[NDIRECT]);
                a = (uint*)bp->data;
                for(j = 0; j < NINDIRECT; j++){
                if(a[j])
                bfree(ip->dev, a[j]);
                }
                brelse(bp);
                bfree(ip->dev, ip->addrs[NDIRECT]);
                ip->addrs[NDIRECT] = 0;
                }

                // CODE HERE
                if(ip->addrs[NDIRECT+1]){
                bp = bread(ip->dev, ip->addrs[NDIRECT+1]);
                a = (uint*)bp->data;
                // 双层循环
                for(j = 0; j < NINDIRECT; j++){
                if(a[j]){
                struct buf* tmp_bp = bread(ip->dev,a[j]);
                uint* tmp_a = (uint*)tmp_bp->data;
                for(int k = 0;k < NINDIRECT; k++){
                if(tmp_a[k])
                bfree(ip->dev,tmp_a[k]);
                }
                brelse(tmp_bp);
                bfree(ip->dev,a[j]);
                }
                }
                brelse(bp);
                bfree(ip->dev, ip->addrs[NDIRECT+1]);
                ip->addrs[NDIRECT+1] = 0;
                }

                ip->size = 0;
                iupdate(ip);
                }
                +

                运行结果:

                + -
                -

                In this exercise you will add symbolic links to xv6.

                -

                Symbolic links (or soft links) refer to a linked file by pathname; when a symbolic link is opened, the kernel follows the link to the referred file.

                -

                Symbolic links resembles hard links, but hard links are restricted to pointing to file on the same disk, while symbolic links can cross disk devices.

                -

                Although xv6 doesn’t support multiple devices, implementing this system call is a good exercise to understand how pathname lookup works.

                -

                You will implement the symlink(char *target, char *path) system call, which creates a new symbolic link at path that refers to file named by target. For further information, see the man page symlink. To test, add symlinktest to the Makefile and run it.

                -
                -
                -

                linux:硬链接和软链接

                -

                硬链接不会创建新的物理文件,但是会使得当前物理文件的引用数加1。当硬链接产生的文件存在时,删除源文件,不会清除实际的物理文件,即对于硬链接“生成的新文件”不会产生任何影响。

                -

                软链接就更像一个指针,只是指向实际物理文件位置,当源文件移动或者删除时,软链接就会失效。

                -

                【所以说,意思就是软链接不会让inode->ulinks++的意思?】

                -
                -

                感想

                这个实验比上个实验稍难一些,但也确实只是moderate的水平,其复杂程度主要来源于对文件系统的理解,还有如何判断环,以及对锁的获取和释放的应用。我做这个实验居然是没看提示的【非常骄傲<-】,让我有一种自己水平上升了的感觉hhh

                -
                正确思路

                本实验要求实现软链接。首先需要实现创建软链接:写一个系统调用 symlink(char *target, char *path) 用于创建一个指向target的在path的软链接;然后需要实现打开软链接进行自动的跳转:在sys_open中添加对文件类型为软链接的特殊处理。

                -
                初见思路

                我的初见思路是觉得可以完全参照sys_link来写。但其实还是很不一样的。

                -

                sys_link的逻辑:

                -
                  -
                1. 获取old的inode
                2. -
                3. 获取new所在目录的inode,称为dp
                4. -
                5. 在dp中添加一项entry指向old
                6. -
                -

                sys_symlink的逻辑:

                -
                  -
                1. 通过path创建一个新的inode,作为软链接的文件

                  -

                  这里选择新建inode,而不是像link那样做,主要还是为了能遵从symlinktest给的接口使用方法(朴实无华的理由)。而且这么做也很方便,符合“一切皆文件”的思想,也能简单化对其在open中的处理。

                  -
                2. -
                3. 在inode中填入target的地址

                  -

                  我们可以把软链接视为文件,文件内容是其target的path。

                  -
                4. -
                -

                可以说是毫不相干,所以还是直接自起炉灶比较好。

                -
                一些错误

                其实没什么好说的,虽然debug过程挺久,但是靠常规的printf追踪就都可以看出来是哪里错了。下面我说说一个我印象比较深刻的吧。

                -

                symlinktest中有一个检测点是,软链接不能成环,也即b->a->b是非法的。于是,我就选择了用快慢指针来检测环形链表这个思想,用来看是否出现环。

                -

                symlinktest的另一个检测点中:

                -

                image-20230125173143735

                -

                我出现了如下错误:

                -

                image-20230125162542807

                -

                此时的结构是1[27]->2[28]->3[29]->4,[]内为inode的inum。

                -

                快慢指针的实现方式是当cnt为奇数的时候,慢指针才会移动。而上图中,cnt==0时,两个指针的值都发生了变化,这就非常诡异。

                -

                这其实是因为slow指针所指向的那个inode被释放了,然后又被fast指针的下一个inode捡过来用了,从而导致值覆盖。

                -

                为什么会被释放呢?

                -
                      // 快指针移动
                readi(ip,0,(uint64)path,0,MAXPATH);
                iunlock(ip);
                if((ip = namei(path)) == 0){
                end_op();
                return -1;
                }
                // 在这里!!!
                ilockput(ip);
                +

                这部分踩过的坑:

                1.LAST_TASK 的定义

                +

                对于LAST_TASK,我本来的理解是,当前所有进程的最后一个。

                +

                本来我设的是跟schedule一样,另p=LAST_TASK,从末尾开始打印。我那时其余代码跟上面一样,就只是把上面的FIRST改成LAST,结果输出为空,调试发现LAST_TASK==NULL。

                +

                然后打开sched.h,看到LAST_TASK的定义:

                +
                #define LAST_TASK task[NR_TASKS-1]
                -

                在这里,我错误地调用了ilockput,从而使inode的ref–,使得它在下一次fast指针调用nameinamei调用iget时,该inode被当做free inode使用,于是就这么寄了。

                -

                所以我们需要把ilockput的调用换成ilock,这样一来就能防止inode被free。至于什么时候再iput?我想还是交给操作系统启动时的清理工作来做吧23333【开摆】

                -

                代码

                image-20230125165612112

                -
                添加定义
                fcntl.c

                open参数

                -
                // 意为只用获取软链接文件本身,而不用顺着软链接去找它的target文件
                #define O_NOFOLLOW 0x100
                +

                原来它就是单纯简单粗暴地指“最后一个”进程23333

                +

                我们目前当前的进程数量远远小于进程的最大数量,因此最大数量编号的那个进程自然也就是空的了。

                +

                2.char s[100]={0};

                +

                用这个的时候编译报错:undefined reference to ’memset‘

                +

                说明这个简略写法其实本质是用的memset,而要用memset的话需要包含头文件string.h。经测试得包含了string.h后确实就好使了。

                +
                //s_imap_blocks、ns_zmap_blocks、

                //total_blocks、free_blocks、used_blocks、total_inodes

                for(i=0;is_zmap_blocks;i++)

                {

                bh=sb->s_zmap[i];

                db=(char*)bh->b_data;

                for(j=0;j<1024;j++){

                for(k=1;k<=8;k++){

                if((used_blocks+free_blocks)>=total_blocks)

                break;

                if( *(db+j) & k)
                used_blocks++;

                else

                free_blocks++;

                }
                -
                stat.h

                文件类型

                -
                #define T_DIR     1   // Directory
                #define T_FILE 2 // File
                #define T_DEVICE 3 // Device
                #define T_SYMLINK 4 // symbol links
                +

                3.我发现一件事

                +

                我第一次把init/main.c写错了,写成:

                +
                mkdir("/proc",0755);
                mknod("/proc/psinfo",S_IFPROC|0400,0);
                mknod("/proc/hdinfo",S_IFPROC|0400,0);
                mknod("/proc/inodeinfo",S_IFPROC|0400,0);
                -
                添加sys_symlink系统调用
                // in sysfile.c
                uint64
                sys_symlink(void)
                {
                char target[MAXPATH], path[MAXPATH];
                struct inode *ip;

                if(argstr(0, target, MAXPATH) < 0 || argstr(1, path, MAXPATH) < 0)
                return -1;

                begin_op();

                // 创建软链接结点
                ip = create(path,T_SYMLINK,0,0);
                //printf("symlink:before writei,inum = %d\n",ip->inum);
                // 此处可以防止住一些并发错误
                if(ip ==0){
                end_op();
                return 0;
                }
                // 向软链接结点文件内写入其所指向的路径
                writei(ip,0,(uint64)target,0,MAXPATH);
                //printf("symlink:after writei\n");

                // 软链接不需要让nlink++

                // 记得要释放在create()中申请的锁
                iunlockput(ip);

                end_op();

                return 0;
                }
                +

                设别号忘了改了。然后进行了一次编译,运行。

                +

                之后我发现错了,就改成了

                +
                mkdir("/proc",0755);
                mknod("/proc/psinfo",S_IFPROC|0400,0);
                mknod("/proc/hdinfo",S_IFPROC|0400,1);
                mknod("/proc/inodeinfo",S_IFPROC|0400,2);
                -
                修改open
                uint64
                sys_open(void)
                {
                // ...

                begin_op();

                if(omode & O_CREATE){
                ip = create(path, T_FILE, 0, 0);
                if(ip == 0){
                end_op();
                return -1;
                }
                } else {
                // 软链接不可能是以O_CREATE的形式创建的
                if((ip = namei(path)) == 0){
                end_op();
                return -1;
                }
                ilock(ip);
                if(ip->type == T_DIR && omode != O_RDONLY){
                iunlockput(ip);
                end_op();
                return -1;
                }

                // 修改从这里开始
                // 快慢指针
                // ip为快指针,slow为慢指针
                uint cnt = 0;
                struct inode* slow = ip;
                // 可能有多重链接,因而需要持续跳转
                while(ip->type == T_SYMLINK){
                //printf("slow = %d,fast = %d,cnt = %d\n",slow->inum,ip->inum,cnt);
                // 其实这个只需要检测一次就够了。但为了编码方便,仍然把它保留在while循环中
                if(omode & O_NOFOLLOW){
                break;
                }else{
                // 检测到cycle
                if(slow == ip && cnt!=0){
                iunlockput(ip);
                end_op();
                return -1;
                }
                // 快指针移动
                readi(ip,0,(uint64)path,0,MAXPATH);
                // 此处不能用iunlockput(),具体原因见 感想-一些错误
                iunlock(ip);
                if((ip = namei(path)) == 0){
                end_op();
                return -1;
                }
                ilock(ip);
                // 慢指针移动
                // 注意,我慢指针移动的时候没有锁保护,因为用锁太麻烦了()其实还是用锁比较合适
                if(cnt & 1){
                //printf("%d\n",cnt);
                readi(slow,0,(uint64)path,0,MAXPATH);
                if((slow = namei(path) )== 0){
                end_op();
                return -1;
                }
                }
                cnt++;
                }
                }
                // 当跳出循环时,此时的ip必定是锁住的
                }

                if(ip->type == T_DEVICE && (ip->major < 0 || ip->major >= NDEV)){
                iunlockput(ip);
                end_op();
                return -1;
                }
                // ...
                }
                +

                再次编译运行,结果上面的那个错还是没改回来

                +

                直到我手动把proc文件夹删了,再重新读一次磁盘加载proc文件夹,才回归正常。

                +

                感想

                本次实验耗时:下午一点到晚上九点半()

                +

                本实验通过对proc虚拟文件的编写流程,实际上让我们体会到了“一切皆文件”的思想。

                +

                什么东西都可以是文件,只不过它们有不同的文件类型和不同的read/write处理函数。

                +

                对于终端设备和磁盘,其read/write函数本质上是在用out指令跟它的缓冲区交互,只不过磁盘比终端设备抽象层次更深,包含了文件系统的层层封装。

                +

                对于虚拟文件,其read/write函数本质上就是与内存交互,通过一段逻辑【处理函数】将内存存储的当前操作系统信息实时显示出来,而不需要存储。

                +

                还有,参考文章那篇的代码写的很好,快去看!

                +]]> + + labs + + + + 状态机 + /2023/03/10/%E5%AF%B9moore%E5%9E%8B%E5%92%8Cmealy%E5%9E%8B%E7%8A%B6%E6%80%81%E6%9C%BA%E7%9A%84%E7%90%86%E8%A7%A3/ + 复习数电时,一道密码锁题令我十分不解:

                +image-20230213201519568 -

                Lab mmap

                -

                The mmap and munmap system calls allow UNIX programs to exert detailed control over their address spaces.

                -

                They can be used to:

                -
                  -
                1. share memory among processes
                2. -
                3. map files into process address spaces
                4. -
                5. as part of user-level page fault schemes such as the garbage-collection algorithms discussed in lecture.
                6. -
                -

                In this lab you’ll add mmap and munmap to xv6, focusing on memory-mapped files.

                -

                mmap是系统调用,在用户态被使用。我们这次实验仅实现mmap功能的子集,也即memory-mapped files。

                -
                +

                看到题目时,我首先联想到的是mealy型状态机,因为我联想到了序列检测。课内的序列检测讲的时候是把它当做mealy型的。但看了标准作答之后,才发现它其实应该是moore型。这让我对这二者的区别产生了深深的不解。

                +

                原来对于moore型状态机和mealy型状态机的理解仅仅停留在概念上,“moore型状态机的输出与输入无关,只与当前状态有关”“mealy型状态机输出与输入和现态都有关”。但这其实是一句非常抽象的话:什么是“无关”,什么是“有关”?moore型状态机的状态不也是依据输入进行转移的吗?那么这算不算“有关”?

                +

                探究之后,我得到了更精确的“有关”“无关”的定义。

                -

                declaration for mmap:

                -
                void *mmap(void *addr, size_t length, int prot, int flags,
                int fd, off_t offset);
                +

                来自:Moore状态机和Mealy状态机的区别

                +image-20230213202110900 -
                  -
                1. 参数

                  -
                    -
                  1. addr is always zero.

                    -

                    You can assume that addr will always be zero, meaning that the kernel should decide the virtual address at which to map the file.【addr由kernel决定,因而用户态只需传入0即可】

                    -
                  2. -
                  3. length is the number of bytes to map

                    -

                    Might not be the same as the file’s length.

                    -
                  4. -
                  5. prot indicates whether the memory should be mapped readable, writeable, and/or executable.

                    -

                    you can assume that prot is PROT_READ or PROT_WRITE or both.

                    -
                  6. -
                  7. flags has two values.

                    -
                      -
                    1. MAP_SHARED

                      -

                      meaning that modifications to the mapped memory should be written back to the file,

                      -

                      如果标记为此,则当且仅当file本身权限为RW或者WRITABLE的时候,prot才可以标记为PROT_WRITE

                      -
                    2. -
                    3. MAP_PRIVATE

                      -

                      meaning that they should not.

                      -

                      如果标记为此,则无论file本身权限如何,prot都可以标记为PROT_WRITE

                      -
                    4. -
                    -
                  8. -
                  9. You can assume offset is zero (it’s the starting point in the file at which to map)

                    -
                  10. -
                  -
                2. -
                3. return

                  -

                  mmap returns that kernel-decided address, or 0xffffffffffffffff if it fails.

                  -
                4. -
                -

                如果两个进程同时对某个文件进行memory map,那么这两个进程可以不共享物理页面。

                +image-20230213202124943
                -

                munmap(addr, length) should remove mmap mappings in the indicated address range.

                -

                If the process has modified the memory and has it mapped MAP_SHARED, the modifications should first be written to the file. 【如果两个进程的修改发生冲突了怎么办?】

                -

                An munmap call might cover only a portion of an mmap-ed region, but you can assume that it will either unmap at the start, or at the end, or the whole region (but not punch a hole in the middle of a region).

                +

                来自:Moore状态机和Mealy状态机的区别(以序列检测器为例)

                +

                Moore状态机输出只与此时的状态有关,因此假如需要检测宽度为4的序列,则需要五个状态

                +

                Mealy状态机输出与此时的状态以及输入有关,因此假如需要检测宽度为4的序列,只需要四个状态即可。

                -

                感想

                这个实验做得我……怎么说,感觉非常地难受吧。虽然我认为我这次做得挺不错的,因为我没有怎么看hints,我的代码差不多全都是我自己想出来的,没有依赖保姆级教学,我认为是一个很好的进步。不过,正因为我没有看hints,导致我的想法比起答案来思路非常地奇诡,导致我第一次错误想法写了一天,看了hints后决心痛改前非,结果第二次错误想法又写了一天emmm

                -

                下面的第一个代码版本虽然可以过掉mmaptest,但确实还是有一个很致命的bug,并且lazy也没有lazy到位,最后的版本离正确思路还有偏差,也就是下面放的第一个代码版本是错误的,但我认为它也不是完全没有亮点。第二个版本才是经过改正的正确版本,但写得着实有点潦草。

                -

                笔记整理得也有点匆忙,毕竟我真的话比较多而且心里很烦。总之,先记录我的全部思路过程,至于价值如何,先不管了2333

                -

                初见思路

                所以说,我们要做的,就是实现一个系统调用mmap,在mmap中,应该首先申请几页用来放file的内容,并且在页表中填入该项,然后再返回该项的虚拟地址。然后在munmap中,再将该file页内容写入file。

                -

                也就是说,直接在mmap把文件的全部内容写入内存,然后各进程读写自己的那块内容块,最后在munmap的时候把修改内容写入文件然后释放该内存块就行了

                -
                问题:在哪里放置file的内容

                题目要求the kernel should decide the **virtual address** at which to map the file.也就是说,在我们的mmap中,需要决定我们要讲文件内容放在哪里。那要放在哪呢……

                -

                我第一反应很奇葩:扫描页表,找到空闲页。但我自己也知道这样不可行,文件内容不止一页,这种零零散散存储需要的数据结构实现起来太麻烦了。

                -

                那怎么办?可以在heap内分配。那么到底怎么样才能在heap里分配?你该怎么知道heap哪里开始是空闲的,哪里是用过的,不还是得扫描页表吗?【思维大僵化】

                -

                其实……道理很简单。我们之间把proc->sz作为mapped-file的起始地址就好了。相信看到这里,你也明白这是什么原理了。能想到这个,我感觉确实很不容易。

                -

                正确思路

                初见思路虽然简单,但是很粗暴,如果文件很大,宝贵的内存空间就会被我们浪费。所以我们借用lazy allocation的思想,先建立memory-file的映射,再在缺页中断中通过文件读写申请内存空间,把文件内容读入内存。

                -

                问题就在于如何“先建立memory-file的映射”。在lazy allocation中,我们是先填好所有的对应页表项,仅是不申请对应的物理内存,也即占着XX不XX。在这次实验中,我们也是这么做,只不过新增了一个难点,那就是如何管理这些页。因为lazy allocation页与页之间没有比较紧密的关系,但是在mmap中页却可以被所属文件这个关键字划分。因而,我们需要一个数据结构,来给页分门别类地组织在一起,并且记录它们的meta data比如说所属文件之类的,这也就是hints里的VMA结构,也即我的filemap结构。

                -

                我们可以将这样的数据结构池化,并且存储在proc域中,以避免对象的重复创建。

                +

                联想到我们课上学习的序列检测:

                +image-20230213202320825 + +

                它这明明长度为3的序列用了4个状态,应该算是moore型,为什么我们却被教说序列检测器是mealy型状态机呢?

                +

                原因是因为,我们进行了状态化简这一步,将moore型状态机转化为了mealy型状态机

                -

                我的lazy法与别人不大一样……我没有想得像他们那么完美。我的做法是,在需要读某个地址的文件内容时,直接确保这个地址前面的所有文件内容都读了进来。也即在filemap中维护一个okva,表明vaokva这段内存已经读入,之后就仅需再读入okvaneed_va这段地址就行。这样虽然lazy了,但没完全lazy。

                -

                我认为这不能体现lazy的思想……因为一读读一坨,还是很占空间啊。

                +

                这俩是可以相互转化的

                +

                来自:[转载][FPGA]有限状态机FSM学习笔记(二)

                +

                把Moore机转换为Mealy机的办法为,把次态的输出修改为对应现态的输出,同时合并一些具有等价性能的状态。把Mealy机转换为Moore机的办法是,把当前态的输出修改为对应次态的输出,同时添加一些状态。如图1所示,为把Mealy机状态图转化为Moore机状态图。

                +

                img
                        图1  Mealy型机转换为Moore型机

                +

                  如图1所示,把Mealy型机转换为Moore型机,只要把现时输出改变为下一时刻输出。对于状态A,有4个箭头指向它,表示在当前状态下有4个状态可以转换为下一状态的A;同时当前输出均为0,可以把0移入状态A内部,表示在Moore机中状态A的输出为0。同理,可以把0分别移位B/C状态。但对于状态D,有两个箭头指向且具有不同的输出值,需要把状态D分解成两个状态D1和D2(每个状态对应一个输出,当输出不同需要利用不同的状态表示,这即是Moore机具有更多状态的原因),得到完整的Moore机状态模型。

                +

                  同理,若把上图的Moore机转换为Mealy机,只要把Moore机中下一状态的输出改变成Mealy机中当前状态的输出,由于D1/D2两状态处于A/C两状态之间,且相当于A/C节点之间的一个等效节点,可以把D1/D2两状态合并为一个状态。

                -

                因而,我们需要做的就是:

                -
                  -
                1. 在mmap中将信息填入该数据结构

                  -
                    -
                  1. 依据传入的长度扩容proc,原sz作为mapped-file起始地址va
                  2. -
                  3. 从对象池中寻找到一个空闲的filemap,对其填写信息
                  4. -
                  5. 返回1所得的va
                  6. -
                  -

                  在我的代码中,还针对proc->sz不满足page-align做出了对策:先把文件的PGROUNDUP(sz)-sz这部分的信息读入,并且更新okva,这样一来,之后在usertrap中,就可以从okva开始一页页地分配地址,做到自然地page-align了。

                  -

                  为什么要对不满足page-align的情况进行处理?

                  -

                  这是因为,growproc的时候一次性扩充一页,但proc->sz却可以不满足page-align,也就是说,proc->sz所处的这一页已经被分配了。

                  -

                  在我们的lazy思路中,我们如果不预先读入文件页,是只能等待用户陷入缺页中断的情况下才能读入文件内容。

                  -

                  但是,proc->sz这一页已经被分配了。因而,在用户态读取这一页地址的时候,并不会发生缺页中断。因而,就会发生文件内容未读入,用户读到脏数据的情况。

                  -

                  其实还有一种更简单的办法可以强制page-align,那就是,直接让起始地址为PGROUNDUP(proc->sz)……至于为什么我不用这个,而要写这么多麻烦的东西呢?答案是我没想到。()

                  +

                  来自:Moore型状态机和Mealy型状态机

                  +

                  并非所有时序电路都可以使用Mealy模型实现。 一些时序电路只能作为摩尔机器实现。

                  -
                2. -
                3. 在usertrap增加对缺页中断的处理

                  -
                    -
                  1. 依据va找到对应filemap
                  2. -
                  3. 根据对应filemap的信息,使用readi(正确)fileread(错误)读取文件内容并存入物理内存
                  4. -
                  -
                4. -
                5. 在munmap中进行释放

                  +

                  所以,我们可以出此暴论:在课程范围内,首先以moore的思想来设计状态机。如果该状态机可以被化简,那么这道题就要用mealy型的来做;如果不能,那么这道题就是得用moore型状态机来做。

                  +

                  一开始的那个时序锁的moore状态机不能化简,因此它是moore型。

                  +
                  +

                  这个点本来可以讲得更清楚一些的……只教会我们做题的套路有啥意思呢←_←

                  +
                  +]]> + + + 对GRUB和initramfs的小探究 + /2023/06/17/%E5%AF%B9GRUB%E5%92%8Cinitramfs%E7%9A%84%E5%B0%8F%E6%8E%A2%E7%A9%B6/ + 竞赛时对操作系统启动过程产生了些疑问,于是问题导向地浅浅探究了下GRUB和initramfs相关机制,相关笔记先放在这里了。

                  +

                  内核启动流程

                  在传统的BIOS系统中,计算机具体的启动流程如下:

                    -
                  1. 根据标记写入文件页,并且释放对应物理内存
                  2. -
                  3. 修改filemap结构的参数,并且在其失效的时候放回对象池
                  4. +
                  5. 电源启动:当计算机的电源打开时,电源供电给计算机的硬件设备。
                  6. +
                  7. BIOS自检:计算机的BIOS固件会自检硬件设备,包括RAM、处理器、硬盘等,以确保它们正常工作。
                  8. +
                  9. 引导设备选择:BIOS会根据预先定义的启动顺序(通常是硬盘、光驱、USB等)选择一个启动设备。
                  10. +
                  11. MBR(Master Boot Record)加载:如果选择的启动设备是硬盘,BIOS会加载该硬盘的MBR,其中包含了引导加载程序。
                  12. +
                  13. GRUB加载:MBR中的引导加载程序通常是GRUB(或其他引导加载程序)。GRUB会被加载到计算机的内存中,并开始执行。
                  14. +
                  15. GRUB菜单:GRUB会显示一个菜单,列出可供选择的操作系统或内核。
                  16. +
                  17. 操作系统加载:用户选择操作系统后,GRUB会加载相应的操作系统或内核,并将控制权交给它。
                  -
                6. -
                7. 修改fork和exit

                  +

                  在本次内核编译配置过程中,最主要探究的是文件系统的装载过程,也即介于6-7之间的部分。

                  +

                  概述

                  文件系统在启动流程中的发展历程可以分为以下三个部分:

                    -
                  1. exit

                    -

                    手动释放map-file域

                    -
                    -

                    为什么不能把这些合并到wait中调用的freepagetable进行释放呢?

                    -

                    因为freepagetable只会释放对应的物理页,没有达到munmap减少文件引用等功能。

                    -
                    +
                  2. GRUB文件系统

                    +

                    由 GRUB 自身通过 BIOS 提供的服务加载

                  3. -
                  4. fork

                    -

                    手动复制filemap池

                    +
                  5. initramfs

                    +

                    由GRUB加载,用于挂载真正的文件系统

                  6. -
                  +
                8. 真正的根文件系统

                -

                我的错误思路们

                第一次错误思路

                上面说到:

                -
                -

                问题就在于如何“先建立memory-file的映射”。在lazy allocation中,我们是先填好所有的对应页表项,仅是不申请对应的物理内存,也即占着XX不XX。在这次实验中,我们也是这么做,只不过新增了一个难点,那就是如何管理这些页。因为lazy allocation页与页之间没有比较紧密的关系,但是在mmap中页却可以被所属文件这个关键字划分。因而,我们需要一个数据结构,来给页分门别类地组织在一起,并且记录它们的meta data比如说所属文件之类的,这也就是hints里的VMA结构,也即我的filemap结构。

                -
                -

                官方给出的答案是在proc域里的pool。我……额……是把这些信息,存入在页中(真是自找麻烦呀)

                -

                具体来说,就是,我在mmap的时候给每个文件申请一页,然后在页的开头填上和filemap结构相差无几的那些参数,再加上一个next指针,表示下一个文件页的地址。页的剩下部分就用来存储数据。总的就是一个链表结构。

                -

                这个思路其实很不错,比起上面的直接在proc内存的尾巴扩容,这个空间利用率应该更大,并且不仅能节省物理内存,还能节省虚拟地址空间,实现了lazy上加lazy。

                -

                但问题是……我为什么非要傻瓜式操纵内存,在页的开头填入参数数据,而不是把这种页抽象为一个个node,最终形成一个十字链表的形式(差不多的意思,鱼骨状),组织进proc域,这样不挺好的吗……唔,有时候我头脑昏迷程度让我自己都感到十分震惊。归根结底,还是想得太少就动手了,失策。

                -

                总之放上代码。我没有实现next指针,仅假设文件内容不超过一页。也就是这一页开头在mmap中填meta data,其余部分在usertrap中填入文件内容。【这个分开的点也让我迷惑至极……】

                -
                #define ERRORADDR 0xffffffffffffffff

                void* mmap(void* address,size_t length,int prot,int flags,struct file* file,uint64 offset){
                struct proc* p = myproc();
                // 获取va,也即真正的address
                uint64 va = p->sz;
                if(growproc(PGSIZE) < 0)
                return (void*)ERRORADDR;
                char* mem = kalloc();
                if(mem == 0){
                return (void*)ERRORADDR;
                }
                memset(mem, 0, PGSIZE);
                // 保存信息:file指针、prot(这就是傻瓜式操纵内存的典范)
                uint64* pointer = (uint64*)mem;
                *pointer = (uint64)file;
                pointer++;
                *pointer = (uint64)prot;
                pointer++;
                *pointer = (uint64)length;
                pointer++;
                *pointer = (uint64)flags;
                pointer++;
                *pointer = (uint64)offset;
                pointer++;
                filedup(file);

                if(mappages(p->pagetable, va+PGSIZE, PGSIZE, (uint64)mem, PTE_M|PTE_X|PTE_U) != 0){
                kfree(mem);
                return (void*)ERRORADDR;
                }
                // return start of free memory
                return (void*)(va + (uint64)pointer - (uint64)mem);
                }
                int munmap(void* address,size_t length){
                struct proc* p = myproc();
                pte_t *pte;
                uint64* pa;

                if((pte = walk(p->pagetable, (uint64)address, 0)) == 0)
                return -1;
                if((*pte & PTE_V) == 0 ||(*pte & PTE_M) == 0)
                return -1;
                // the start is where the params save
                pa = (uint64*)(PGROUNDDOWN(PTE2PA(*pte)));
                struct file* file = (struct file*)(*pa);
                pa++;
                int prot = (int)(*pa);
                pa++;
                pa++;
                int flags = (int)(*pa);
                pa++;
                pa++;

                if(flags == MAP_SHARED&&(prot&PROT_WRITE) != 0){
                // 需要更新写内容
                filewrite(file,(uint64)address,length);
                }
                // 最后释放内存
                uvmunmap(p->pagetable, PGROUNDDOWN((uint64)address), 1, 1);
                return 0;
                }
                - -
                } else if(r_scause() == 13 || r_scause() == 15){
                uint64 va = r_stval();
                pte_t *pte;
                uint64* pa;
                uint flags;

                if((pte = walk(p->pagetable, va, 0)) == 0)
                p->killed = 1;
                else if((*pte & PTE_V) == 0 ||(*pte & PTE_M) == 0)
                p->killed = 1;
                else {
                // the start is where the params save
                pa = (uint64*)(PGROUNDDOWN(PTE2PA(*pte)));
                flags = PTE_FLAGS(*pte);
                struct file* file = (struct file*)(*pa);
                pa++;
                int prot = (int)(*pa);
                pa++;
                size_t length = (size_t)(*pa);
                pa++;
                pa++;
                pa++;

                if((prot&PROT_READ) != 0){
                fileread(file,va,length);
                flags |= PTE_R;
                if((prot&PROT_WRITE) != 0) flags |= PTE_W;
                else if(r_scause() == 15) p->killed = 1;
                *pte = ((*pte) | flags);
                } else p->killed = 1;
                }
                }
                - -
                为什么下面的代码是错的

                正如开头所说的那样,我并没有完美做好这次实验,下面代码有一个致命的bug。

                -

                先说说致命bug是什么。

                -

                我的filemap结构体其实隐藏了两个具有“offset”这一含义的状态。一个是filemap里面的成员变量offset,另一个是filemap里面的成员变量file的成员变量off:

                -
                // in proc.h
                struct filemap{
                struct file* file;//文件
                uint64 offset;//va相对于file开头的offset
                };
                // in file.h
                struct file {
                uint off; // FD_INODE
                };
                - -

                在我的代码里,它们被赋予了不同的含义。

                -

                filemap->file->off被用于trap.c中,表示的是当前未读入文件内容的起始位置(实际上也就是okva-va的值),用于自然地使用fileread进行文件读入。

                -
                -

                比如说,这次读入PGSIZE,那么off就会在fileread中自增PGSIZE。下次调用fileread就可以直接从下一个位置读入了,这样使代码更加简洁

                -
                -

                filemap->offset被用于munmap中。filewritefileread一样,都是从file->off处开始取数据。munmap所需要取数据的起始位置和trap.c中需要取数据的起始位置肯定不一样,

                -
                -

                想想它们的功能。trap.c的off需要始终指向有效内存段的末尾,但munmap由于要对特定内存段进行写入文件操作,因而off要求可以随机指向。

                -
                -

                因而,我们可以将当前va对应的文件位置记录在offset中。届时,我们只需要从p->filemaps[i].offset+va-p->filemaps[i].va取数据就行。

                -

                上述两个变量相辅相成,看上去似乎能够完美无缺地实现我们的功能。但是,实际上,不行。为什么呢?因为它们的file指针,filemap->file,如果被两个mmap区域同时使用的话,就会出问题。

                -

                可以来看看mmaptest.c中的这一段代码:

                -
                  makefile(f);
                if ((fd = open(f, O_RDONLY)) == -1)
                err("open");

                unlink(f);
                char *p1 = mmap(0, PGSIZE*2, PROT_READ, MAP_SHARED, fd, 0);
                char *p2 = mmap(0, PGSIZE*2, PROT_READ, MAP_SHARED, fd, 0);

                // read just 2nd page.
                if(*(p1+PGSIZE) != 'A')
                err("fork mismatch (1)");
                if((pid = fork()) < 0)
                err("fork");

                if (pid == 0) {
                // v1是用来触发缺页中断的函数
                _v1(p1);
                munmap(p1, PGSIZE); // just the first page
                exit(0); // tell the parent that the mapping looks OK.
                }

                int status = -1;
                wait(&status);

                if(status != 0){
                printf("fork_test failed\n");
                exit(1);
                }

                // check that the parent's mappings are still there.
                printf("before v1,p1 = %d\n",(uint64)p1);
                _v1(p1);
                printf("after v1,p1 = %d\n",(uint64)p1);
                _v1(p2);


                printf("fork_test OK\n");

                /*输出:
                fork_test starting
                trap:map a page at 53248,okva = 53248
                trap:mem[0]=65,off = 4096,size = 6144
                trap:map a page at 57344,okva = 53248
                trap:mem[0]=65,off = 6144,size = 6144
                before v1,p1 = 53248
                after v1,p1 = 53248
                trap:map a page at 61440,okva = 61440
                trap:mem[0]=0,off = 6144,size = 6144
                mismatch at 0, wanted 'A', got 0x0
                mmaptest: fork_test failed: v1 mismatch (1), pid=3
                */
                - -
                // in trap.c
                printf("trap:map a page at %d,okva = %d\n",start_va,p->filemaps[i].okva);

                fileread(p->filemaps[i].file,start_va,PGSIZE);

                printf("trap:mem[0]=%d,off = %d,size = %d\n",
                mem[0],p->filemaps[i].file->off,p->filemaps[i].file->ip->size);
                - -

                这段代码因为共用fd,导致file指针被两个mmap区域同时使用。

                -
                -

                共用fd,为什么file指针也一起共用了?

                -

                可以追踪一下它们的生命周期:

                -
                // in sys_open()
                // 获取file结构体和文件描述符。
                if((f = filealloc()) == 0 || (fd = fdalloc(f)) < 0){

                // in sysfile.c
                // Allocate a file descriptor for the given file.
                // Takes over file reference from caller on success.
                static int
                fdalloc(struct file *f)
                {
                int fd;
                struct proc *p = myproc();

                for(fd = 0; fd < NOFILE; fd++){
                if(p->ofile[fd] == 0){
                p->ofile[fd] = f;
                return fd;
                }
                }
                return -1;
                }
                - -

                可以看到,它实际上是有一个文件描述符表,key为fd,value为file指针。因而,同一张表,fd相同,file指针相同。

                -

                注:父子进程,同样的fd,file指针也是相同的

                -

                fork出来的父子进程同一个句柄对同一个文件的偏移量是相同的,这个原理应该是因为,父子进程共享的是文件句柄这个结构体对象本身,也就是拷贝的时候是浅拷贝而不是深拷贝。

                -
                // in fork()
                // increment reference counts on open file descriptors.
                for(i = 0; i < NOFILE; i++)
                if(p->ofile[i])
                np->ofile[i] = filedup(p->ofile[i]);
                +

                下面,将介绍1和2两个流程。

                +

                GRUB

                +

                GRUB(GNU GRand Unified Bootloader)是一种常用的引导加载程序,用于在计算机启动时加载操作系统。

                +

                GRUB的主要功能是在计算机启动时提供一个菜单,让用户选择要启动的操作系统或内核。它支持多个操作系统,包括各种版本的Linux、Windows、BSD等。通过GRUB,用户可以在多个操作系统之间轻松切换。

                +

                除了操作系统选择,GRUB还提供了一些高级功能,例如引导参数的设置、内存检测、系统恢复等。它还支持在启动过程中加载内核模块和初始化RAM磁盘映像(initrd或initramfs)。

                +

                GRUB具有高度可配置性,允许用户自定义引导菜单、设置默认启动项、编辑内核参数等。它还支持引导加载程序间的链式引导,可以引导其他引导加载程序,如Windows的NTLDR。

                -

                最后的check that the parent's mappings are still there.环节中,_v1(p1)执行时并没有陷入trap,这是正常的。不正常的是_v1(p2)的执行结果。它陷入了trap,但是却因file->off == file size,导致被判定为已全部读入文件,事实上却是并没有读入文件。

                -

                为什么会这样呢?

                -

                这是因为p1和p2共用同一个fd,也就共用了同一个file指针。共用了一个file指针,那么p1和p2面对的file->off相同。上面说到,file->off用于控制文件映射。那么,当p1完成了对文件的映射,p1的off指针如果不加重置,就会永远停留在file size处。这样一来,当p2想要使用同样的file指针进行文件映射时,就会出问题。

                -

                这个问题的一个解决方法是每次mmap都深拷贝一个船新file结构体。但是这样的话,file域里的ref变量就失去了它的意义,并且file对象池应该也很快就会爆满,非常不符合设计方案。

                -

                这个问题的完美解,是不要赋予file->off这个意义,而是使用readi替代fileread

                -
                fileread(struct file *f, uint64 addr, int n)
                readi(struct inode *ip, int user_dst, uint64 dst, uint off, uint n)
                +

                GRUB的基本作用流程为:

                +
                  +
                1. BIOS加载MBR,MBR加载GRUB,开始执行GRUB程序
                2. +
                3. GRUB程序会读取grub.cfg配置文件
                4. +
                5. GRUB程序依据配置文件,进行内核的加载、根文件系统的挂载等操作,最后将主导权转交给内核
                6. +
                +

                grub.cfg

                内核启动时,GRUB程序会读取/boot/grub/目录下的GRUB配置文件grub.cfg,其中记录了所有GRUB菜单可供选择的内核选项(menuentry)及其对应的启动依赖参数。以6.4.0内核选项为例:

                +
                # menuentry标识着GRUB菜单中的一个内核选项
                menuentry 'Ubuntu' --class ubuntu --class gnu-linux --class gnu --class os $menuentry_id_option 'gnulinux-simple-XXX' {
                recordfail # 记录上次启动是否失败,用于处理启动失败的情况
                load_video # 加载视频驱动模块,用于在启动过程中显示图形界面
                gfxmode $linux_gfx_mode # 设置图形模式
                insmod gzio # 加载gzio模块,提供对GZIP压缩和解压缩功能的支持
                # 如果是在Xen虚拟化平台上,则加载xzio和lzopio模块
                if [ x$grub_platform = xxen ]; then insmod xzio; insmod lzopio; fi

                insmod part_gpt # 加载part_gpt模块,支持GUID分区表(GPT)
                insmod ext2 # 加载ext2模块,支持ext2文件系统

                # 设置文件系统的根分区
                set root='hd0,gpt3'
                if [ x$feature_platform_search_hint = xy ]; then
                search --no-floppy --fs-uuid --set=root --hint-bios=hd0,gpt3 --hint-efi=hd0,gpt3 --hint-baremetal=ahci0,gpt3 XXX
                else
                search --no-floppy --fs-uuid --set=root XXX
                fi

                linux /boot/vmlinuz-6.4.0-rc3+ root=UUID=XXX ro text # 指定内核映像的路径和启动参数
                initrd /boot/initrd.img-6.4.0-rc3+ # 指定initramfs映像的路径
                }
                -

                这样做的好处是,我们可以实时计算offset(前面提到,其恰恰等于okva-va),而不用把这个东西用file的off来表示。

                -
                -

                也确实,我之所以弯弯绕绕那么曲折,是因为只想到了fileread这个函数,压根没注意到还有一个readi……

                -
                -

                我在下面的代码仅做了一个能够通过测试,但是上面的bug依然存在的功利性折中代码。我是这么实现的:

                -
                // 在`mmap`的时候初始化`file->off`
                p->filemaps[i].file->off = offset;
                // 在`munmap`的时候清零`file->off`
                p->filemaps[i].file->off = 0;
                +

                可以看到,grub.cfg主要记录了一些该内核启动需要的依赖module,以及内核映像和initramfs映像的路径

                +

                menuentry的代码中,有以下几个要点值得注意:

                +
                  +
                1. insmod gzio

                  +

                  由于加载gzio模块,提供对GZIP压缩和解压缩功能的支持。

                  +

                  看到这里我第一反应是觉得有点割裂,为啥这看着比较无关紧要的解压缩功能要在内核启动之前就需要有呢?于是我想起来在配置内核时,有一个选项是这样的:

                  +

                  image-20230616143835953

                  +

                  在配置选项中,我们选择了对initramfs的支持,并且勾选了Support initial ramdisk/ramfs compressed using gzip ,也即在编译时通过gzip压缩initramfs的大小以节省空间。

                  +

                  所以说,我们在内核启动之前,持有的initramfs处于被压缩的状态。故而,我们自然需要在内核启动之前安装gzio模块,从而支持之后对initramfs的解压缩了。

                  +
                2. +
                3. insmod ext2

                  +

                  这句代码说明,GRUB的临时文件系统为ext2类型,这句代码事实上是在安装GRUB建立临时文件的必要依赖包,从而GRUB程序之后才能建立其临时文件系统、从/boot/initrd.img获取initramfs映像。

                  +
                4. +
                5. linux /boot/vmlinuz-6.4.0-rc3+ root=UUID=XXX ro text

                  +

                  指定了启动参数,也即将根文件系统以只读(ro)的方式挂载在root=UUID=XXX对应的块设备上,并且默认以text方式(也即非图形化的Shell界面)启动内核。

                  +

                  此处的启动参数可在下一个部分介绍的grub文件中个性化。

                  +
                6. +
                +

                grub.cfg的生成与修改

                实际运用中,很多时候需要对启动参数进行一些修改。下面介绍两种修改grub.cfg的方法。

                +

                /etc/default/grub

                可以看到,grub.cfg其实格式较为固定(也即由一系列内容也比较相似的menuentry构成)。因而,实际上我们是通过grub.d生成grub.cfg的(6.S081实验中事实上也涉及了这一点),而/etc/default/grub则是GRUB程序以及grub.cfg生成的配置文件。下面介绍下该文件主要有哪些配置选项。

                +
                # If you change this file, run 'update-grub' afterwards to update
                # /boot/grub/grub.cfg.
                # For full documentation of the options in this file, see:
                # info -f grub -n 'Simple configuration'

                # 开机时GRUB界面的持续时间,此处设置为30s
                GRUB_TIMEOUT=30
                GRUB_CMDLINE_LINUX=""

                # 不使用图形化界面
                #GRUB_TERMINAL=console
                # 图形化界面的大小
                #GRUB_GFXMODE=640x480
                # 不使用UUID
                #GRUB_DISABLE_LINUX_UUID=true

                # 隐藏recovery mode
                #GRUB_DISABLE_RECOVERY="true"
                +

                重点看下这几个参数:

                +
                  +
                1. GRUB_CMDLINE_LINUX

                  +

                  表示最终生成的grub.cfg中的每一个menuentry中的linux那一行需要附加什么参数。

                  +

                  例如说,如果设置为:

                  +
                  # 表示initramfs在挂载真正的根文件系统之前,需要等待120s,用于防止磁盘没准备好导致的挂载失败
                  GRUB_CMDLINE_LINUX="rootdelay=120"
                  +

                  那么,最终在menuentry中的启动参数就为:

                  +
                  linux   /boot/vmlinuz-6.4.0-rc3+ root=UUID=XXX ro rootdelay=120 text
                  -

                  因而,结论是,一步错步步错,一个错误需要更多的错误来弥补,最后还是错的(悲)

                  -
                  如何把下面的错误思路改成正确思路

                  可以做以下几点:

                  -
                    -
                  1. 正确地lazy

                    -

                    每次trap仅分配一页。

                    -
                  2. -
                  3. 改用readi函数,修改file->off的语义

                    +

                    其他一些常见的选项:

                    +
                    # 直接以路径来标识块设备而非使用UUID。此为old option,建议尽量使用UUID
                    GRUB_CMDLINE_LINUX="root=/dev/sda3"
                    # 标明init进程(启动后第一个进程)的具体路径。此处指明为`/bin/sh`
                    GRUB_CMDLINE_LINUX="init=/bin/sh"
                  4. +
                  5. GRUB_DEFAULT

                    +

                    参考 可以用来指定重启时的内核选项。如GRUB_DEFAULT="1> 0"表示选择第一个菜单界面的第2栏(Advanced for Ubuntu)和第二个菜单的第1个内核。

                  -

                  这样一来,大概就可以完美地正确了。

                  -

                  其他的一些小细节

                  file指针的生命周期

                  在数据结构中存储file指针至关重要。但仔细想一想,file指针的生命周期似乎长到过分:从sys_mmap被调用,一直到usertrap处理缺页中断,最后到munmap释放,我们要求file指针的值需要保持稳定不变。

                  -

                  这么长的生命周期,它真的可以做到吗?毕竟file指针归根到底只是一个局部变量,在syscall mmap结束之后,它还有效吗?答案是有效的,这个有效性由mmap实现中对ref的增加来实现保障。

                  -

                  在用户态中关闭一个文件,需要使用syscallclose(int fd)。不妨来看看close的代码。

                  -
                  // in kernel/sysfile.c
                  uint64
                  sys_close(void)
                  {
                  int fd;
                  struct file *f;

                  if(argfd(0, &fd, &f) < 0)
                  return -1;
                  // 一个进程打开的文件都会放入一个以fd为index的文件表里,
                  // 在xv6中,这个文件表便是`myproc()->ofile`。
                  // 可以看到,关闭一个文件首先需要把它移出文件表
                  myproc()->ofile[fd] = 0;
                  // 对file指针关闭的主要操作
                  fileclose(f);
                  return 0;
                  }

                  // in kernel/file.c
                  // Close file f. (Decrement ref count, close when reaches 0.)
                  void
                  fileclose(struct file *f)
                  {
                  struct file ff;

                  acquire(&ftable.lock);
                  // 若ref数<0,就会直接return
                  if(--f->ref > 0){
                  release(&ftable.lock);
                  return;
                  }
                  // 释放file
                  // close不会显式地释放file指针,只会释放file指针所指向的文件,让file指针失效。
                  ff = *f;
                  f->ref = 0;
                  f->type = FD_NONE;
                  release(&ftable.lock);

                  if(ff.type == FD_PIPE){
                  pipeclose(ff.pipe, ff.writable);
                  } else if(ff.type == FD_INODE || ff.type == FD_DEVICE){
                  begin_op();
                  iput(ff.ip);
                  end_op();
                  }
                  }
                  - -

                  可以看到,当ref数>1时,file指针就不会失效。

                  -

                  这就是为什么我们还需要在mmap中让file的ref数++。

                  -
                  缺页中断蕴含的设计思想

                  如果只存入file指针,用户态要如何对对应的文件进行读写呢?

                  -

                  我们可以自然想到也许需要设计一个函数,让用户在想要对这块内存读写的时候调用这个函数即可。但是,这样的方法使得用户对内存不能自然地读写,还需要使用我们新设计的这个函数,这显然十分地不美观。所以,我们需要找到一个方法,让上层的用户可以统一地读取任何的内存块,包括memory-mapped file内存块,而隐藏memory-mapped file与其他内存块读写方式不同的这些复杂细节。经历过前面几次实验的你看到这里一定能想到,有一个更加优美更加符合设计规范的方法,那就是:缺页中断

                  +

                  在修改完grub文件之后,我们需要执行sudo update-grub,来重新生成grub.cfg文件供下次启动使用。

                  +

                  在GRUB界面直接修改

                  image-20230616151055620

                  +

                  我们可以在GRUB界面选中所需内核,按下e键:

                  +

                  image-20230616151122738

                  +

                  然后就可以对启动参数进行修改,^X退出。

                  +

                  值得注意的是,此修改仅对本次启动有效。如果需要长期修改,建议还是通过第一种方法去修改。

                  +

                  initramfs

                  GRUB程序会通过initrd.img启动initramfs,从而进行真正的根文件系统挂载。

                  -

                  没做这个实验之前就知道mmap需要借助缺页中断来实现了,但实际自己的第一印象是觉得并不需要缺页中断,直到分析到这里才恍然大悟。

                  -

                  “让上层的用户可以统一地读取任何的内存块,而隐藏不同类型的内存块读写方式不同的这些复杂细节”

                  -

                  仔细想想,前面几个关于缺页中断的实验,比如说cow fork,lazy allocation,事实上都是基于这个思想。它们并不是不能与缺页中断分离,只是有了缺页中断,它们的实现更加简洁,更加优美。

                  -

                  再次感慨os的博大精深。小小一个缺页中断,原理那么简单,居然集中了这么多设计思想,不禁叹服。

                  +

                  initrd.img是一个Linux系统中的初始化内存盘(initial RAM disk)的映像文件。它是一个压缩的文件系统映像,通常在引导过程中加载到内存中,并提供了一种临时的根文件系统,以便在正式的根文件系统(通常位于硬盘上)可用之前提供必要的功能和模块。

                  -
                  正确答案的munmap中如果遇到未映射的页怎么办

                  在正确答案的munmap中:

                  -
                  //释放已经申请的页表项、内存,并且看看是不是需要写回
                  while(start_va < bounder){
                  if(p->filemaps[i].flags == MAP_SHARED){
                  //写回
                  filewrite(p->filemaps[i].file,start_va,PGSIZE);
                  }
                  uvmunmap(p->pagetable,start_va,1,1);
                  start_va += PGSIZE;
                  }
                  - -

                  如果map类型为MAP_SHARED,并且该页尚未映射,会怎么样呢?

                  -

                  追踪filewrite的路径

                  -
                  // in file.c
                  begin_op();
                  ilock(f->ip);
                  if ((r = writei(f->ip, 1, addr + i, f->off, n1)) > 0)
                  f->off += r;
                  iunlock(f->ip);
                  end_op();
                  // in fs.c
                  if(either_copyin(bp->data + (off % BSIZE), user_src, src, m) == -1) {
                  brelse(bp);
                  break;
                  }
                  log_write(bp);
                  // in vm.c copyin()
                  int
                  copyin(pagetable_t pagetable, char *dst, uint64 srcva, uint64 len)
                  {
                  uint64 n, va0, pa0;

                  while(len > 0){
                  va0 = PGROUNDDOWN(srcva);
                  pa0 = walkaddr(pagetable, va0);
                  if(pa0 == 0)
                  return -1;
                  // ...
                  - -

                  copyin最终会在 if(pa0 == 0) return -1;这里终结,但writei并不会在接收到-1的时候爆出panic或者是引发缺页中断,而只会把它当做文件结尾,默默地返回。

                  -

                  并且,在munmap中是一页一页地释放,而不是直接传参length全部释放,这一点也很重要。因为我们的lazy allocation很可能导致va~va+length这一区间内只是部分页被映射,部分页没有。如果直接传参length释放,那么在遇到第一页未被映射的时候,filewrite就会终止,该页之后的页就没有被写回文件的机会了。

                  -

                  所以结论是,在正确实现的munmap中遇到未映射的页会自动跳过,什么也不会发生。

                  -

                  代码

                  数据结构

                  // in param.h
                  #define NFILEMAP 32

                  // in proc.h
                  struct filemap{
                  uint isused;//对象池思想。该filemap是否正在被使用
                  uint64 va;//该文件的起始内存页地址
                  uint64 okva;//该文件的起始未被读入部分对应的内存地址
                  struct file* file;//文件
                  size_t length;//需要映射到内存的长度
                  int flags;//MAP_SHARED OR MAP_PRIVATE
                  int prot;//PROT_READ OR PROT_WRITE
                  uint64 offset;//va相对于file开头的offset
                  };

                  // Per-process state
                  struct proc {
                  struct filemap filemaps[NFILEMAP];
                  };
                  - -

                  mmap

                  具体系统调用注册过程略。

                  -
                  // in sysproc.c
                  uint64
                  sys_mmap(void){
                  uint64 addr;
                  int length,prot,flags,offset;
                  struct file* file;
                  if(argaddr(0,&addr) < 0 || argint(1,&length) < 0 || argint(2,&prot) < 0 || argint(3,&flags) < 0 || argfd(4,0,&file) ||argint(5,&offset) < 0)
                  return -1;
                  return (uint64)mmap((void*)addr,(size_t)length,prot,flags,file,(uint)offset);
                  }
                  - -
                  #define ERRORADDR 0xffffffffffffffff

                  // 映射file从offset开始长度为length的内容到内存中,返回内存中的文件内容起始地址
                  void* mmap(void* address,size_t length,int prot,int flags,struct file* file,uint64 offset){
                  // mmap的prot权限必须与file的权限对应,不能file只读但是mmap却可写且shared
                  if((prot&PROT_WRITE) != 0&&flags == MAP_SHARED &&file->writable == 0)
                  return (void*)ERRORADDR;

                  struct proc* p = myproc();
                  uint64 va = 0;
                  int i=0;

                  //找到filemap池中第一个空闲的filemap
                  for(i=0;i<NFILEMAP;i++){
                  if(!p->filemaps[i].isused){
                  // 获取va,也即真正的address
                  va = p->sz;
                  p->sz += length;
                  // 其实这里用一个memcpy会更加优雅,可惜我忘记了()
                  p->filemaps[i].isused = 1;
                  p->filemaps[i].va = va;
                  p->filemaps[i].okva = va;
                  p->filemaps[i].length = length;
                  p->filemaps[i].prot = prot;
                  p->filemaps[i].flags = flags;
                  p->filemaps[i].file = file;
                  p->filemaps[i].file->off = offset;
                  p->filemaps[i].offset = offset;
                  // 增加文件引用数
                  filedup(file);
                  break;
                  }
                  }
                  if(va == 0) return (void*)ERRORADDR;
                  // return start of free memory
                  uint64 start_va = PGROUNDUP(va);
                  // 先读入处于proc已申请的内存页区域(也即没有内存对齐情况下)
                  uint64 off = start_va - va;
                  if(off < PGSIZE){
                  fileread(file,va,off);
                  file->off += off;
                  p->filemaps[i].okva = va+off;
                  }
                  return (void*)va;
                  }
                  - -

                  usertrap

                  错的
                  } else if(r_scause() == 13 || r_scause() == 15){
                  uint64 va = r_stval();

                  for(int i=0;i<NFILEMAP;i++){
                  // 找到va对应的filemap
                  if(p->filemaps[i].isused&&va>=p->filemaps[i].va
                  && va<p->filemaps[i].va+p->filemaps[i].length){
                  // 说明本来就不应该写
                  if(r_scause() == 15 && ((p->filemaps[i].prot)&PROT_WRITE) == 0){
                  p->killed = 1;
                  break;
                  }
                  //说明地址不在文件范围内
                  if(p->filemaps[i].va+p->filemaps[i].file->ip->size <= va){
                  p->killed = 1;
                  break;
                  }
                  // 能进到这里来的都是产生了缺页中断,也就是说va对应文件数据不存在
                  // 我们需要维护一个okva,表示从filemaps.va到okva这段地址已经加载了文件
                  // 这样一来,我们这里就只需加载okva~va地址对应的文件了
                  // file结构体自带的off成员会由于fileread而自动增长到对应位置,所以文件可以自然地读写
                  uint64 start_va = p->filemaps[i].okva;// okva一定是page-align的
                  // 加载文件内容
                  while(start_va <= va){
                  char* mem = kalloc();
                  if(mem == 0){
                  p->killed = 1;
                  break;
                  }
                  memset(mem, 0, PGSIZE);
                  int flag = PTE_X|PTE_R|PTE_U;
                  if(((p->filemaps[i].prot)&PROT_WRITE) != 0){
                  flag |= PTE_W;
                  }
                  if(mappages(p->pagetable, start_va, PGSIZE, (uint64)mem, flag) != 0){
                  p->killed = 1;
                  kfree(mem);
                  break;
                  }
                  // 读入文件内容
                  fileread(p->filemaps[i].file,start_va,PGSIZE);
                  start_va += PGSIZE;
                  }
                  p->filemaps[i].okva = start_va;
                  break;
                  }
                  }
                  }
                  - -
                  对的
                  } else if(r_scause() == 13 || r_scause() == 15){
                  uint64 va = r_stval();
                  for(int i=0;i<NFILEMAP;i++){
                  if(p->filemaps[i].isused&&va>=p->filemaps[i].va && va<p->filemaps[i].va+p->filemaps[i].length){
                  if(r_scause() == 15 && ((p->filemaps[i].prot)&PROT_WRITE) == 0){
                  // 说明本来就不应该写
                  p->killed = 1;
                  break;
                  }
                  if(p->filemaps[i].va+p->filemaps[i].file->ip->size <= va){
                  //说明地址不在文件范围内
                  p->killed = 1;
                  break;
                  }
                  uint64 start_va = PGROUNDDOWN(va);
                  char* mem = kalloc();
                  if(mem == 0){
                  p->killed = 1;
                  break;
                  }
                  memset(mem, 0, PGSIZE);
                  int flag = PTE_X|PTE_R|PTE_U;
                  if(((p->filemaps[i].prot)&PROT_WRITE) != 0){
                  flag |= PTE_W;
                  }
                  if(mappages(p->pagetable, start_va, PGSIZE, (uint64)mem, flag) != 0){
                  p->killed = 1;
                  kfree(mem);
                  break;
                  }
                  readi(p->filemaps[i].file->ip,0,(uint64)mem,va-p->filemaps[i].va+p->filemaps[i].offset,PGSIZE);
                  break;
                  }
                  }
                  }
                  - -

                  munmap

                  错的
                  uint64 min(uint64 a,uint64 b){return a>b?b:a;}

                  // 释放文件映射以address为起始地址,length为长度这个范围内的内存地址空间
                  int munmap(void* address,size_t length){
                  struct proc* p = myproc();
                  uint64 va = (uint64)address;

                  // 找到对应的filemap
                  for(int i=0;i<NFILEMAP;i++){
                  if(p->filemaps[i].isused&&p->filemaps[i].va<=va&&p->filemaps[i].va+length>va){
                  // 开始释放的内存地址
                  uint64 start_va;
                  if(va == p->filemaps[i].va)
                  start_va = PGROUNDUP(p->filemaps[i].va);
                  else
                  start_va = PGROUNDDOWN(va);
                  // 结束释放的内存地址
                  uint64 bounder = p->filemaps[i].va + min(p->filemaps[i].file->ip->size,length);

                  //file的off在trap中用于表示文件已加载的位置
                  //在这里需要用off进行filewrite,所以需要对原本在usertrap用于记录加载位置的off进行手动保存
                  uint64 tmp_off = p->filemaps[i].file->off;
                  p->filemaps[i].file->off = p->filemaps[i].offset+va-p->filemaps[i].va;

                  //释放已经申请的页表项、内存,并且看看是不是需要写回
                  while(start_va < bounder && start_va < p->filemaps[i].okva){
                  if(p->filemaps[i].flags == MAP_SHARED){
                  //写回
                  filewrite(p->filemaps[i].file,start_va,PGSIZE);
                  }
                  uvmunmap(p->pagetable,start_va,1,1);
                  start_va += PGSIZE;
                  }

                  //修改filemap结构体的起始地址va和长度,offset也要变,因为他记录va对应的是文件哪个位置
                  if(va == p->filemaps[i].va){
                  //释放的是头几页
                  p->filemaps[i].offset += length;
                  p->filemaps[i].va = va+length;
                  p->filemaps[i].length -= length;
                  }else {
                  //释放的是尾几页
                  p->filemaps[i].length -= p->filemaps[i].length - va;
                  }
                  p->filemaps[i].file->off = tmp_off;
                  // 检验map的合理性
                  if(p->filemaps[i].length == 0 || p->filemaps[i].va >= p->filemaps[i].va+length
                  || p->filemaps[i].file->off > p->filemaps[i].file->ip->size){
                  p->filemaps[i].isused = 0;//释放

                  // 注意!!!!这句话对我的错误代码来说非常重要
                  p->filemaps[i].file->off = 0;
                  fileclose(p->filemaps[i].file);
                  }
                  }
                  }
                  return 0;
                  }
                  - -
                  对的
                  uint64 min(uint64 a,uint64 b){return a>b?b:a;}

                  int munmap(void* address,size_t length){
                  struct proc* p = myproc();
                  uint64 va = (uint64)address;
                  for(int i=0;i<NFILEMAP;i++){
                  if(p->filemaps[i].isused&&p->filemaps[i].va<=va&&p->filemaps[i].va+length>va){
                  uint64 start_va;
                  if(va == p->filemaps[i].va)
                  start_va = PGROUNDUP(p->filemaps[i].va);
                  else
                  start_va = PGROUNDDOWN(va);
                  uint64 bounder = p->filemaps[i].va + min(p->filemaps[i].file->ip->size,length);
                  //在这里需要用off进行读写,所以需要对原本的加载处off手动保存
                  uint64 tmp_off = p->filemaps[i].file->off;
                  p->filemaps[i].file->off = p->filemaps[i].offset+va-p->filemaps[i].va;

                  //释放已经申请的页表项、内存,并且看看是不是需要写回
                  while(start_va < bounder){
                  if(p->filemaps[i].flags == MAP_SHARED){
                  //写回
                  filewrite(p->filemaps[i].file,start_va,PGSIZE);
                  }
                  uvmunmap(p->pagetable,start_va,1,1);
                  start_va += PGSIZE;
                  }

                  //修改filemap结构体的起始地址va和长度,offset也要变,因为他记录va对应的是文件哪个位置
                  if(va == p->filemaps[i].va){
                  //释放的是头几页
                  p->filemaps[i].offset += length;
                  p->filemaps[i].va = va+length;
                  p->filemaps[i].length -= length;
                  }else {
                  //释放的是尾几页
                  p->filemaps[i].length -= p->filemaps[i].length - va;
                  }
                  // 检验map的合理性
                  if(p->filemaps[i].length == 0 || p->filemaps[i].va >= p->filemaps[i].va+length
                  || p->filemaps[i].file->off > p->filemaps[i].file->ip->size){
                  p->filemaps[i].isused = 0;//释放
                  fileclose(p->filemaps[i].file);
                  }
                  p->filemaps[i].file->off = tmp_off;
                  }
                  }
                  return 0;
                  }
                  +

                  我们可以通过unmkinitramfs /boot/initrd.img-6.4.0-rc3+ /tmp/initrd/命令解压initrd,探究里面到底有什么玩意。

                  +
                  ├── bin -> usr/bin
                  ├── conf
                  ├── etc
                  ├── init
                  ├── lib -> usr/lib
                  ├── lib32 -> usr/lib32
                  ├── lib64 -> usr/lib64
                  ├── libx32 -> usr/libx32
                  ├── run
                  ├── sbin -> usr/sbin
                  ├── scripts
                  ├── usr
                  └── var
                  init
                  -

                  exit和fork

                  exit
                  // 关闭map-file
                  for(int i=0;i<NFILEMAP;i++){
                  if(p->filemaps[i].isused){
                  munmap((void*)(p->filemaps[i].va),p->filemaps[i].length);
                  }
                  }
                  +

                  可以看到,这实际上就是一个小型的文件系统,也即initramfs。它有自己的built-in Shell(BusyBox):

                  +

                  image-20230616151938951

                  +

                  有一些较少的Shell命令(bin和sbin目录下),以及用来挂载真正的根文件系统的代码逻辑(存储在scripts目录下)。【我猜】在正常情况下,系统会执行scripts下的脚本代码挂载真正的文件系统。当挂载出现异常时,系统就会将控制权交给initramfs内置的Shell BusyBox,由用户自己探究出了什么问题。

                  +

                  我们接下来可以追踪下initramfs的script目录下的文件系统挂载流程。

                  +

                  挂载真正文件系统的主要函数为local_mount_root

                  +
                  # 仅展示主要流程代码
                  local_mount_root()
                  {
                  # 预处理,获取参数等(也即上面grub.cfg配置的root=UUID)
                  local_top
                  if [ -z "${ROOT}" ]; then
                  panic "No root device specified. Boot arguments must include a root= parameter."
                  fi

                  # 根据UUID获取对应的块设备
                  local_device_setup "${ROOT}" "root file system"
                  ROOT="${DEV}"

                  # 挂载前的预处理
                  local_premount

                  # 挂载
                  mount ${roflag} ${FSTYPE:+-t "${FSTYPE}"} ${ROOTFLAGS} "${ROOT}" "${rootmnt?}"
                  }
                  -
                  fork
                  for(int i=0;i<NFILEMAP;i++){
                  np->filemaps[i].isused = p->filemaps[i].isused;
                  np->filemaps[i].va = p->filemaps[i].va;
                  np->filemaps[i].okva = p->filemaps[i].okva;
                  np->filemaps[i].file = p->filemaps[i].file;
                  np->filemaps[i].length = p->filemaps[i].length;
                  np->filemaps[i].flags = p->filemaps[i].flags;
                  np->filemaps[i].offset = p->filemaps[i].offset;
                  np->filemaps[i].prot = p->filemaps[i].prot;
                  if(np->filemaps[i].file)
                  filedup(np->filemaps[i].file);
                  }
                  +

                  由于研究这个是错误驱动(乐),因而我只主要看了下local_device_setup

                  +
                  # $1=device ID to mount设备ID
                  # $2=optionname (for root and etc)要挂载的是什么玩意,此处应为root file system
                  # $3=panic if device is missing (true or false, default: true)
                  # Sets $DEV to the resolved device node $DEV是最终获取到的块设备
                  local_device_setup()
                  {
                  local dev_id="$1"
                  local name="$2"
                  local may_panic="${3:-true}"
                  local real_dev
                  local time_elapsed
                  local count

                  # 获取grub.cfg的rootdelay参数的设备等待时间。如果没有该参数,默认是30秒
                  local slumber=30
                  if [ "${ROOTDELAY:-0}" -gt $slumber ]; then
                  slumber=$ROOTDELAY
                  fi

                  # 等待设备
                  case "$dev_id" in
                  UUID=*|LABEL=*|PARTUUID=*|/dev/*)
                  FSTYPE=$( wait-for-root "$dev_id" "$slumber" )
                  ;;
                  *)
                  wait_for_udev 10
                  ;;
                  esac

                  # 等待结束了。如果条件为真,说明还是获取不到对应的设备,那就只能说明这个设备死了
                  # 所以我们就得把问题告诉用户,让用户自己解决,并且进入BusyBox Shell
                  # We've given up, but we'll let the user fix matters if they can
                  while ! real_dev=$(resolve_device "${dev_id}") ||
                  ! get_fstype "${real_dev}" >/dev/null; do
                  if ! $may_panic; then
                  echo "Gave up waiting for ${name}"
                  return 1
                  fi
                  echo "Gave up waiting for ${name} device. Common problems:"
                  echo " - Boot args (cat /proc/cmdline)"
                  echo " - Check rootdelay= (did the system wait long enough?)"
                  if [ "${name}" = root ]; then
                  echo " - Check root= (did the system wait for the right device?)"
                  fi
                  echo " - Missing modules (cat /proc/modules; ls /dev)"
                  panic "ALERT! ${dev_id} does not exist. Dropping to a shell!"
                  done

                  DEV="${real_dev}"
                  }
                  -

                  修改uvmcopy和uvmunmap

                  // in uvmunmap()
                  if((*pte & PTE_V) == 0){
                  *pte = 0;
                  continue;
                  }
                  // in uvmcopy()
                  if((*pte & PTE_V) == 0)
                  //panic("uvmcopy: page not present");
                  continue;
                  -]]> - - - 各种配环境中遇到的问题 - /2023/10/12/%E5%90%84%E7%A7%8D%E9%85%8D%E7%8E%AF%E5%A2%83%E4%B8%AD%E9%81%87%E5%88%B0%E7%9A%84%E9%97%AE%E9%A2%98/ - -
                2. 记录一次vm扩容

                  -
                3. -
                4. 开发中遇到的链接小问题

                  -
                5. -
                6. rtt硬件环境搭建

                  -
                7. -
                8. 内核编译

                  -
                9. -
                10. 防火墙

                  -
                  sudo ufw status numbered # 查看
                  sudo ufw delete 数字 # 删除某条记录
                  # 开放IP地址XX.XX.XX.XX的22 tcp端口
                  sudo ufw allow from XX.XX.XX.XX to any port 22 proto tcp
                11. -
                +

                可以看到,这里如果进入错误状态,最终就是这样的效果2333:

                +

                image-20230616153420011

                ]]> + + os竞赛 + 记录一次vm扩容 @@ -13728,1007 +13667,1068 @@ url访问填写http://localhost/webdemo4_war/*.do - 阅读JDK容器部分源码的心得体会2【Map部分】 - /2022/10/22/%E9%98%85%E8%AF%BBJDK%E5%AE%B9%E5%99%A8%E9%83%A8%E5%88%86%E6%BA%90%E7%A0%81%E7%9A%84%E5%BF%83%E5%BE%97%E4%BD%93%E4%BC%9A2%E3%80%90Map%E9%83%A8%E5%88%86%E3%80%91/ + 网络是怎样连接的 + /2023/10/06/%E7%BD%91%E7%BB%9C%E6%98%AF%E6%80%8E%E6%A0%B7%E8%BF%9E%E6%8E%A5%E7%9A%84/ -

                idea 替换注释正则表达式/\*{1,2}[\s\S]*?\*/

                -

                typora 替换图片asset

                -

                \!\[.*\]\(D:\\aWorkStorage\\hexo\\blog\\source\\_posts\\阅读JDK容器部分源码的心得体会2【Map部分】\\(.*)\.png\)

                -

                替换结果{% asset_img $1.png %}

                -
                -

                Map(I)

                -

                A map cannot contain duplicate keys; each key can map to at most one value.

                -

                This interface takes the place of the Dictionary class.

                -

                The Map interface provides three collection views, which allow a map’s contents to be viewed as a set of keys, collection of values, or set of key-value mappings.

                -

                The order of a map is defined as the order in which the iterators on the map’s collection views return their elements. map的元素顺序取决于集合元素顺序的意思?

                -

                Note: great care must be exercised if mutable objects are used as map keys. The behavior of a map is not specified if the value of an object is changed in a manner that affects equals comparisons while the object is a key in the map. 【这个跟set的那个是一样的】

                -
                -

                map没有迭代器

                - - -

                代码:

                public interface Map<K,V> {
                // Query Operations

                int size();

                boolean isEmpty();

                boolean containsKey(Object key);

                //This operation will probably require time linear in the map size
                //for most implementations of the Map interface.
                boolean containsValue(Object value);

                //1
                V get(Object key);

                // Modification Operations

                V put(K key, V value);

                V remove(Object key);

                // Bulk Operations

                void putAll(Map<? extends K, ? extends V> m);

                void clear();

                // Views

                //Returns a Set view of the keys contained in this map.
                //The set is backed by the map, so changes to the map are reflected in the set, and vice-versa.
                //The set supports element removal,
                //which removes the corresponding mapping from the map,
                //via the Iterator.remove, Set.remove, removeAll, retainAll, and clear operations.
                //It does not support the add or addAll operations.
                //2
                Set<K> keySet();

                //跟上面一样,也只支持remove,不支持add
                Collection<V> values();

                //3
                //跟上面一样,也只支持remove,不支持add
                Set<Map.Entry<K, V>> entrySet();

                //The only way to obtain a reference to a map entry is from the iterator of this collection-view.
                //These Map.Entry objects are valid only for the duration of the iteration;
                //more formally, the behavior of a map entry is undefined if the backing map has been
                //modified after the entry was returned by the iterator,
                //except through the setValue operation on the map entry.迭代器也不行了
                //可见度为default,包内可见
                interface Entry<K,V> {

                K getKey();

                V getValue();

                V setValue(V value);

                boolean equals(Object o);

                int hashCode();

                public static <K extends Comparable<? super K>, V> Comparator<Map.Entry<K,V>> comparingByKey() {
                return (Comparator<Map.Entry<K, V>> & Serializable)
                (c1, c2) -> c1.getKey().compareTo(c2.getKey());
                }

                public static <K, V extends Comparable<? super V>> Comparator<Map.Entry<K,V>> comparingByValue() {
                return (Comparator<Map.Entry<K, V>> & Serializable)
                (c1, c2) -> c1.getValue().compareTo(c2.getValue());
                }

                public static <K, V> Comparator<Map.Entry<K, V>> comparingByKey(Comparator<? super K> cmp) {
                Objects.requireNonNull(cmp);
                return (Comparator<Map.Entry<K, V>> & Serializable)
                (c1, c2) -> cmp.compare(c1.getKey(), c2.getKey());
                }

                public static <K, V> Comparator<Map.Entry<K, V>> comparingByValue(Comparator<? super V> cmp) {
                Objects.requireNonNull(cmp);
                return (Comparator<Map.Entry<K, V>> & Serializable)
                (c1, c2) -> cmp.compare(c1.getValue(), c2.getValue());
                }
                }

                // Comparison and hashing

                boolean equals(Object o);

                int hashCode();

                // Defaultable methods

                //1
                default V getOrDefault(Object key, V defaultValue) {
                V v;
                return (((v = get(key)) != null) || containsKey(key))
                ? v
                : defaultValue;
                }

                default void forEach(BiConsumer<? super K, ? super V> action) {
                Objects.requireNonNull(action);
                for (Map.Entry<K, V> entry : entrySet()) {
                K k;
                V v;
                try {
                k = entry.getKey();
                v = entry.getValue();
                } catch(IllegalStateException ise) {
                // this usually means the entry is no longer in the map.
                //确实说明这时候应该并发修改异常了
                throw new ConcurrentModificationException(ise);
                }
                action.accept(k, v);
                }
                }

                default void replaceAll(BiFunction<? super K, ? super V, ? extends V> function) {
                Objects.requireNonNull(function);
                for (Map.Entry<K, V> entry : entrySet()) {
                K k;
                V v;
                try {
                k = entry.getKey();
                v = entry.getValue();
                } catch(IllegalStateException ise) {
                // this usually means the entry is no longer in the map.
                throw new ConcurrentModificationException(ise);
                }

                // ise thrown from function is not a cme.
                v = function.apply(k, v);

                try {
                entry.setValue(v);
                } catch(IllegalStateException ise) {
                // this usually means the entry is no longer in the map.
                throw new ConcurrentModificationException(ise);
                }
                }
                }

                //If the specified key 没有mapping或者对应值为空
                //associates it with the given value and returns null,
                //else returns the current value.
                default V putIfAbsent(K key, V value) {
                V v = get(key);
                if (v == null) {
                v = put(key, value);
                }

                return v;
                }

                //当所给的key对应的curValue==value时,就remove掉这对mapping
                default boolean remove(Object key, Object value) {
                Object curValue = get(key);
                if (!Objects.equals(curValue, value) ||
                (curValue == null && !containsKey(key))) {
                return false;
                }
                remove(key);
                return true;
                }

                default boolean replace(K key, V oldValue, V newValue) {
                Object curValue = get(key);
                if (!Objects.equals(curValue, oldValue) ||
                (curValue == null && !containsKey(key))) {
                return false;
                }
                put(key, newValue);
                return true;
                }

                //如果映射存在就replace,返回旧值
                default V replace(K key, V value) {
                V curValue;
                if (((curValue = get(key)) != null) || containsKey(key)) {
                curValue = put(key, value);
                }
                return curValue;
                }

                //通过mappingFunction来用key计算value
                //4
                default V computeIfAbsent(K key,
                Function<? super K, ? extends V> mappingFunction) {
                Objects.requireNonNull(mappingFunction);
                V v;
                if ((v = get(key)) == null) {
                V newValue;
                if ((newValue = mappingFunction.apply(key)) != null) {
                put(key, newValue);
                return newValue;
                }
                }

                return v;
                }

                //If the value for the specified key is present and non-null, attempts to compute a new mapping given the key and its current mapped value.
                //If the function returns null, the mapping is removed.【此时传入的function计算得出value=NULL】
                //If the function itself throws an (unchecked) exception, the exception is rethrown, and the current mapping is left unchanged.
                default V computeIfPresent(K key,
                BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
                Objects.requireNonNull(remappingFunction);
                V oldValue;
                if ((oldValue = get(key)) != null) {
                V newValue = remappingFunction.apply(key, oldValue);
                if (newValue != null) {
                put(key, newValue);
                return newValue;
                } else {
                remove(key);
                return null;
                }
                } else {
                return null;
                }
                }

                //跟上面那个的差别好像在,当oldValue==NULL,newValue不等于NULL时,下面这个会放入mapp(key,new)
                //上面那个什么也不做。毕竟上面的叫computeIfPresent嘛
                default V compute(K key,
                BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
                Objects.requireNonNull(remappingFunction);
                V oldValue = get(key);

                V newValue = remappingFunction.apply(key, oldValue);
                if (newValue == null) {
                // delete mapping
                //新value==NULL,就delete
                if (oldValue != null || containsKey(key)) {
                // something to remove
                remove(key);
                return null;
                } else {
                // nothing to do. Leave things as they were.
                return null;
                }
                } else {
                // add or replace old mapping
                put(key, newValue);
                return newValue;
                }
                }

                //把新旧值通过function合并
                //5
                default V merge(K key, V value,
                BiFunction<? super V, ? super V, ? extends V> remappingFunction) {
                Objects.requireNonNull(remappingFunction);
                Objects.requireNonNull(value);
                V oldValue = get(key);
                //传入function的必定非空
                V newValue = (oldValue == null) ? value :
                remappingFunction.apply(oldValue, value);
                if(newValue == null) {
                remove(key);
                } else {
                put(key, newValue);
                }
                return newValue;
                }

                //6 迭代
                }
                - -

                其中:

                  -
                1. get return null时的情况

                  get return null when value==NULL or key不存在。

                  -

                  为了区分这两种情况,写代码时可以用:

                  -
                  if !containsKey(key){
                  key不存在
                  }
                  Obj obj=get(key);
                  - -

                  其实源码中的getordefault方法就给出了应用典范

                  -
                  default V getOrDefault(Object key, V defaultValue) {
                  V v;
                  return (((v = get(key)) != null) || containsKey(key))
                  ? v
                  : defaultValue;
                  }
                2. -
                3. view

                  -

                  //Returns a Set view of the keys contained in this map.

                  -

                  //The set is backed by the map, so changes to the map are reflected in the set, and vice-versa.

                  -
                  -

                  如下代码测试:

                  -
                      public static void main(String[] args) {
                  HashMap<String,Integer> map = new HashMap<>();
                  map.put("Lily",15);
                  map.put("Sam",20);
                  map.put("Mary",11);
                  map.put("Lee",111);
                  Set set=map.keySet();
                  for( Object str : set){
                  System.out.print((String) str+" ");
                  }
                  System.out.println();
                  set.remove("Lee");
                  //set.add("haha");
                  for( Object str : set){
                  System.out.print((String) str+" ");
                  }
                  System.out.println();
                  for (Object str : map.keySet()){
                  System.out.print((String) str+" ");
                  }
                  System.out.println();
                  System.out.println(map.containsKey("Lee"));
                  }
                  /*
                  Lee Lily Sam Mary
                  Lily Sam Mary
                  Lily Sam Mary
                  false
                  */
                  - -

                  可得,与之前的List一样,这个view都是纯粹基于原数组的,实时变化的。

                  -

                  在应用中可发现,可以通过map的key和value的set来对map进行遍历。

                  -
                4. -
                5. entrySet

                  -
                  //The map can be modified while an iteration over the set is in progress 
                  //when using the setValue operation on a map entry returned by the iterator
                  //or through the iterator's own remove operation
                  -
                  -

                  相比于其它的view,多了第二句话

                  -
                6. -
                7. computeIfAbsent

                  -

                  If the function returns null no mapping is recorded.

                  -

                  If the function itself throws an (unchecked) exception, the exception is rethrown, and no mapping is recorded.

                  -

                  The most common usage is to construct a new object serving as an initial mapped value or memoized result, as in:

                  -
                  map.computeIfAbsent(key, k -> new Value(f(k)));
                  - -

                  Or to implement a multi-value map, Map<K,Collection>, supporting multiple values per key:

                  -
                  map.computeIfAbsent(key, k -> new HashSet<V>()).add(v);
                  +

                  此为《信息存储与管理(第二版):数字信息的存储、管理和保护》的看书总结,相当于是对存储技术的一个简单的名词入门。

                  -

                  它这说的还是很抽象的,下面给出一个使用computeIfAbsent的优雅实例:

                  -

                  TreeMap()) Treemap With Object

                  +

                  浏览器生成消息

                  本章节我印象最深的还是以前就不大了解的DNS,今天看到书的描写真有种豁然开朗的感觉。

                  +

                  DNS服务器用于保存域名—IP地址的映射对,为了增加查找效率,DNS根据域名的分级采用树形组织,例如hitsz.edu.cn/可以相当于是/cn/edu/hitsz,包含了/cnedu这几个域。根DNS服务器存储着根域,记录了所有一级域名对应DNS服务器的IP地址。所有的DNS服务器都会保存根服务器的IP地址。

                  -

                  computeIfAbsent returns the value that is already present in the map, or otherwise evaluates the lambda and puts that into the map, and returns that value.

                  +

                  世界上只有13个根DNS服务器IP地址,但是有很多台根DNS服务器。

                  -
                  var line = "line";      
                  var mp = new TreeMap<String,TreeMap<String,Integer>>();
                  var m = mp.computeIfAbsent(line, k -> new TreeMap<>());
                  m.put("content", 5);
                  System.out.println(mp);
                  //output:{line={content=5}}
                  - -

                  computIfAbsent发现此时map里面没有这个“line”key,就执行第二个参数的lambda表达式,把一个new TreeMap<>以line为关键字放入,并且返回该TreeMap。

                  +

                  主机需要手动配置DNS服务器地址。

                  +

                  当浏览器需要填写请求头时,它需要通过系统调用向操作系统发送DNS查询请求。操作系统将DNS请求发送给配置在主机上的DNS服务器(下称A),A再向根DNS服务器发送请求。根DNS服务器解析域名,返回下一级DNS服务器的IP地址。A再向下级DNS服务器再次发送请求,下级再返回下下级IP地址。以此类推,最终A就能得到目标IP地址的正确响应。整个过程如下图所示:

                  +

                  image-20231010132718116

                  +

                  与此同时,各个DNS服务器都会有定时刷新的缓存,从而加速了查找效率。

                  +

                  用电信号传输TCP/IP数据

                  TCP/IP

                  本章前面大多讨论TCP/IP具体协议内容,以前已经了解过很多次了就不多赘述。所以TCP/IP部分就以分点的形式随意列举一下:

                  +
                    +
                  1. IP 中还包括 ICMPA 协议和 ARPB 协议。ICMP 用于告知网络包传送过程中产生的错误以及各种控制消息,ARP 用于根据 IP 地址查询相应的以太网 MAC 地址。

                  2. -
                  3. merge

                    -

                    看起来非常实用:

                    -

                    This method may be of use when combining multiple mapped values for a key【相同key不同value合并】. For example, to either create or append a String msg to a value mapping:

                    -
                    map.merge(key, msg, String::concat)
                    - -

                    所举代码段意为把新值通过字符串拼接接在旧值后面。

                    -

                    应该也可以用于集合合并。总之具体实现方法取决于传入的function参数,非常实用

                    -
                    +
                  4. 套接字中记录了用于控制通信操作的各种控制信息,协议栈则需要根据这些信息判断下一步的行动,【包括应用程序信息和协议栈状态信息】这就是套接字的作用。所以需要针对不同协议栈实现不同的socket。

                  5. -
                  6. 迭代器

                    map本身没有迭代器。

                    -

                    因而在对map进行遍历时,只能通过其keyset、valueset以及entryset来实现。

                    -

                    具体详见:HashMap的四种遍历方式

                    +
                  7. 是的,回想当初CS144,也是socket来负责有特定消息时调用TCP相关函数来通知处理。

                  8. +
                  9. 连接 connect

                    +

                    连接实际上是通信双方交换控制信息,在套接字中记录这些必要信息并准备数据收发的一连串操作。

                    +
                      +
                    1. 应用程序向协议栈传ip地址
                    2. +
                    3. 本机向服务器发通信请求
                    4. +
                    5. 过程中分配通信缓冲区
                    -

                    AbstractMap

                    -

                    To implement an unmodifiable map, the programmer needs only to extend this class and provide an implementation for the entrySet method, which returns a set-view of the map’s mappings. Typically, the returned set will, in turn, be implemented atop AbstractSet. This set should not support the add or remove methods, and its iterator should not support the remove method.

                    -

                    To implement a modifiable map, the programmer must additionally override this class’s put method (which otherwise throws an UnsupportedOperationException), and the iterator returned by entrySet().iterator() must additionally implement its remove method.

                    -
                    - - -

                    最核心的还是entrySet。其余所有的方法,都是通过enrtSet实现的。而给定了enrty这个数据结构的实现方式,剩下的就是entrySet具体怎么实现了。AbstractMap把entrySet的实现抽象了出来,交给了其实现类去具体实现。

                    -

                    代码:

                    public abstract class AbstractMap<K,V> implements Map<K,V> {

                    protected AbstractMap() {
                    }

                    // Query Operations

                    public int size() {
                    //还真确实是set的大小
                    return entrySet().size();
                    }

                    public boolean isEmpty() {
                    return size() == 0;
                    }

                    public boolean containsValue(Object value) {
                    Iterator<Entry<K,V>> i = entrySet().iterator();
                    if (value==null) {
                    while (i.hasNext()) {
                    //entrySet的元素是Entry
                    Entry<K,V> e = i.next();
                    if (e.getValue()==null)
                    return true;
                    }
                    } else {
                    while (i.hasNext()) {
                    Entry<K,V> e = i.next();
                    if (value.equals(e.getValue()))
                    return true;
                    }
                    }
                    return false;
                    }

                    public boolean containsKey(Object key) {
                    Iterator<Map.Entry<K,V>> i = entrySet().iterator();
                    if (key==null) {
                    while (i.hasNext()) {
                    Entry<K,V> e = i.next();
                    if (e.getKey()==null)
                    return true;
                    }
                    } else {
                    while (i.hasNext()) {
                    Entry<K,V> e = i.next();
                    if (key.equals(e.getKey()))
                    return true;
                    }
                    }
                    return false;
                    }

                    public V get(Object key) {
                    Iterator<Entry<K,V>> i = entrySet().iterator();
                    if (key==null) {
                    while (i.hasNext()) {
                    Entry<K,V> e = i.next();
                    if (e.getKey()==null)
                    return e.getValue();
                    }
                    } else {
                    while (i.hasNext()) {
                    Entry<K,V> e = i.next();
                    if (key.equals(e.getKey()))
                    return e.getValue();
                    }
                    }
                    return null;
                    }

                    // Modification Operations

                    public V put(K key, V value) {
                    throw new UnsupportedOperationException();
                    }

                    //为啥unmodifiable map还可以remove
                    public V remove(Object key) {
                    Iterator<Entry<K,V>> i = entrySet().iterator();
                    Entry<K,V> correctEntry = null;
                    if (key==null) {
                    while (correctEntry==null && i.hasNext()) {
                    Entry<K,V> e = i.next();
                    if (e.getKey()==null)
                    correctEntry = e;
                    }
                    } else {
                    while (correctEntry==null && i.hasNext()) {
                    Entry<K,V> e = i.next();
                    if (key.equals(e.getKey()))
                    correctEntry = e;
                    }
                    }

                    V oldValue = null;
                    if (correctEntry !=null) {
                    oldValue = correctEntry.getValue();
                    i.remove();
                    }
                    return oldValue;
                    }

                    // Bulk Operations

                    public void putAll(Map<? extends K, ? extends V> m) {
                    for (Map.Entry<? extends K, ? extends V> e : m.entrySet())
                    put(e.getKey(), e.getValue());
                    }

                    public void clear() {
                    //还真是
                    entrySet().clear();
                    }

                    // Views
                    //1
                    transient Set<K> keySet;
                    transient Collection<V> values;

                    //The set supports element removal via
                    //the Iterator.remove, Set.remove, removeAll, retainAll, and clear operations.
                    //It does not support the add or addAll operations.
                    //只删不加
                    public Set<K> keySet() {
                    //引用成员变量,减少访问堆次数
                    Set<K> ks = keySet;
                    //首次建立视图
                    if (ks == null) {
                    ks = new AbstractSet<K>() {
                    public Iterator<K> iterator() {
                    return new Iterator<K>() {
                    private Iterator<Entry<K,V>> i = entrySet().iterator();

                    public boolean hasNext() {
                    return i.hasNext();
                    }

                    public K next() {
                    return i.next().getKey();
                    }

                    public void remove() {
                    i.remove();
                    }
                    };
                    }

                    //AbtractMap的这些方法都是通过其entryset实现的。因此其实最主要的还是entryset怎么实现的
                    public int size() {
                    return AbstractMap.this.size();
                    }

                    public boolean isEmpty() {
                    return AbstractMap.this.isEmpty();
                    }

                    public void clear() {
                    AbstractMap.this.clear();
                    }

                    public boolean contains(Object k) {
                    return AbstractMap.this.containsKey(k);
                    }
                    };
                    //赋值回给成员变量
                    keySet = ks;
                    }
                    return ks;
                    }

                    public Collection<V> values() {
                    Collection<V> vals = values;
                    if (vals == null) {
                    vals = new AbstractCollection<V>() {
                    public Iterator<V> iterator() {
                    return new Iterator<V>() {
                    private Iterator<Entry<K,V>> i = entrySet().iterator();

                    public boolean hasNext() {
                    return i.hasNext();
                    }

                    public V next() {
                    return i.next().getValue();
                    }

                    public void remove() {
                    i.remove();
                    }
                    };
                    }

                    public int size() {
                    return AbstractMap.this.size();
                    }

                    public boolean isEmpty() {
                    return AbstractMap.this.isEmpty();
                    }

                    public void clear() {
                    AbstractMap.this.clear();
                    }

                    public boolean contains(Object v) {
                    return AbstractMap.this.containsValue(v);
                    }
                    };
                    values = vals;
                    }
                    return vals;
                    }

                    //有待不同的数据结构实现了
                    public abstract Set<Entry<K,V>> entrySet();

                    // Comparison and hashing

                    public boolean equals(Object o) {
                    if (o == this)
                    return true;

                    if (!(o instanceof Map))
                    return false;
                    Map<?,?> m = (Map<?,?>) o;
                    if (m.size() != size())
                    return false;

                    try {
                    Iterator<Entry<K,V>> i = entrySet().iterator();
                    while (i.hasNext()) {
                    Entry<K,V> e = i.next();
                    K key = e.getKey();
                    V value = e.getValue();
                    if (value == null) {
                    if (!(m.get(key)==null && m.containsKey(key)))
                    return false;
                    } else {
                    if (!value.equals(m.get(key)))
                    return false;
                    }
                    }
                    } catch (ClassCastException unused) {
                    return false;
                    } catch (NullPointerException unused) {
                    return false;
                    }

                    return true;
                    }

                    public int hashCode() {
                    int h = 0;
                    Iterator<Entry<K,V>> i = entrySet().iterator();
                    while (i.hasNext())
                    h += i.next().hashCode();
                    return h;
                    }

                    public String toString() {
                    Iterator<Entry<K,V>> i = entrySet().iterator();
                    if (! i.hasNext())
                    return "{}";

                    StringBuilder sb = new StringBuilder();
                    sb.append('{');
                    for (;;) {
                    Entry<K,V> e = i.next();
                    K key = e.getKey();
                    V value = e.getValue();
                    //经典防自环
                    sb.append(key == this ? "(this Map)" : key);
                    sb.append('=');
                    sb.append(value == this ? "(this Map)" : value);
                    if (! i.hasNext())
                    return sb.append('}').toString();
                    sb.append(',').append(' ');
                    }
                    }

                    protected Object clone() throws CloneNotSupportedException {
                    AbstractMap<?,?> result = (AbstractMap<?,?>)super.clone();
                    //也就只有这两个成员变量了
                    result.keySet = null;
                    result.values = null;
                    return result;
                    }

                    private static boolean eq(Object o1, Object o2) {
                    return o1 == null ? o2 == null : o1.equals(o2);
                    }

                    // Implementation Note: SimpleEntry and SimpleImmutableEntry
                    // are distinct unrelated classes, even though they share
                    // some code. Since you can't add or subtract final-ness
                    // of a field in a subclass, they can't share representations,
                    // and the amount of duplicated code is too small to warrant
                    // exposing a common abstract class.
                    //意思就是说,这两个类一个表示key不可变value可变的entry,也就是可变map,
                    //另一个表示key和value都不可变的entry,也就是固定map,
                    //这俩有很多重复代码,但不能统一到一起,是因为前者有一个final字段,后者有两个,
                    //无法对这个final字段做一个统一,因此只能分成两个了

                    //静态内部类
                    //对Entry接口的一个简单实现【key不可变,value可变】
                    public static class SimpleEntry<K,V>
                    implements Entry<K,V>, java.io.Serializable
                    {
                    private static final long serialVersionUID = -8499721149061103585L;

                    //key不可修改,value可修改
                    private final K key;
                    private V value;

                    public SimpleEntry(K key, V value) {
                    this.key = key;
                    this.value = value;
                    }

                    public SimpleEntry(Entry<? extends K, ? extends V> entry) {
                    this.key = entry.getKey();
                    this.value = entry.getValue();
                    }

                    public K getKey() {
                    return key;
                    }

                    public V getValue() {
                    return value;
                    }

                    public V setValue(V value) {
                    V oldValue = this.value;
                    this.value = value;
                    return oldValue;
                    }

                    public boolean equals(Object o) {
                    if (!(o instanceof Map.Entry))
                    return false;
                    Map.Entry<?,?> e = (Map.Entry<?,?>)o;
                    return eq(key, e.getKey()) && eq(value, e.getValue());
                    }

                    public int hashCode() {
                    //注意这里是异或
                    return (key == null ? 0 : key.hashCode()) ^
                    (value == null ? 0 : value.hashCode());
                    }

                    public String toString() {
                    return key + "=" + value;
                    }

                    }

                    //静态内部类
                    //对Entry接口的一个简单实现【key不可变,value不可变】
                    public static class SimpleImmutableEntry<K,V>
                    implements Entry<K,V>, java.io.Serializable
                    {
                    private static final long serialVersionUID = 7138329143949025153L;

                    private final K key;
                    private final V value;

                    public SimpleImmutableEntry(K key, V value) {
                    this.key = key;
                    this.value = value;
                    }

                    public SimpleImmutableEntry(Entry<? extends K, ? extends V> entry) {
                    this.key = entry.getKey();
                    this.value = entry.getValue();
                    }

                    public K getKey() {
                    return key;
                    }

                    public V getValue() {
                    return value;
                    }

                    public V setValue(V value) {
                    //exception
                    throw new UnsupportedOperationException();
                    }

                    public boolean equals(Object o) {
                    if (!(o instanceof Map.Entry))
                    return false;
                    Map.Entry<?,?> e = (Map.Entry<?,?>)o;
                    return eq(key, e.getKey()) && eq(value, e.getValue());
                    }

                    public int hashCode() {
                    return (key == null ? 0 : key.hashCode()) ^
                    (value == null ? 0 : value.hashCode());
                    }

                    public String toString() {
                    return key + "=" + value;
                    }
                    }
                    }
                    - -

                    其中:

                      -
                    1. view

                      -

                      Each of these fields are initialized to contain an instance of the appropriate view the first time this view is requested. The views are stateless, so there’s no reason to create more than one of each.

                      -
                      -

                      不同于之前List的sublist和sorted set的subset,它俩是调用创建view方法时才构造出一个新的对象,map是直接把values和keys视图放入成员变量了,因为Collection的视图从实用角度来说有起始和终点更实用,map不需要这个性质,因此作为成员变量花费更小

                    2. -
                    -

                    HashMap

                    哈希表+链表/红黑树

                    -
                    -

                    permits null values and the null key允许空,其hash应该是0

                    -

                    The HashMap class is roughly equivalent to Hashtable, except that it is unsynchronized and permits nulls.不同步

                    -

                    This class makes no guarantees as to the order of the map; in particular, it does not guarantee that the order will remain constant over time.无序

                    -

                    这应该差不多就是个桶链表

                    -

                    An instance of HashMap has two parameters that affect its performance: initial capacity and load factor.

                    -

                    The capacity is the number of buckets in the hash table.桶数量=capacity

                    -

                    The load factor is a measure of how full the hash table is allowed to get before its capacity is automatically increased. 如果装载百分比达到load factor,hashmap的capacity就会自动增长。

                    -

                    When the number of entries in the hash table exceeds the product of the load factor and the current capacity, the hash table is rehashed.如果元素数量>=load factor*capacity,就会自动增长并且重新hash。

                    -

                    默认的load factor是0.75.【我其实觉得这个数很有意思。它是二进制意义上的整除数,因而计算应该很方便:它可以被整整表示,并且计算时可以拆成“2^-1+2^-2”以供移位简化】

                    -

                    我们设置capacity和load factor的意图应该是要尽量减少rehash的次数。

                    -

                    Note that using many keys with the same hashCode() is a sure way to slow down performance of any hash table使用多个相同的key【指hashcode相同】会降低性能【?】

                    -

                    https://stackoverflow.com/questions/43911369/hashmap-java-8-implementation等会看看

                    -
                    -

                    总之意思差不多就是,hashmap的数据结构:

                    -

                    table数组,每个成员都是一个桶,桶里面装着结点。table默认长度为16

                    -

                    每个桶内结点的结构依具体情况(该桶内元素多少)来决定,桶内元素多则用树状结构,少就用简单的线性表结构。线性结构为Node<K,V>,树状结构为TreeNode<K,V>。

                    -

                    当一个线性表桶内结点多于临界值,就需要进行树化,会从链表变成红黑树;当整个hashmap结点数多于临界值,就需要增长capacity并且进行rehash。

                    -

                    hashmap的桶的装配:首先通过key的hashcode算出一个hash值,然后再把该hash值与n-1相与就能得到桶编号。接下来再在桶内找到应插入的结点就行。

                    -

                    代码:

                    public class HashMap<K,V> extends AbstractMap<K,V>
                    implements Map<K,V>, Cloneable, Serializable {

                    private static final long serialVersionUID = 362498820763181265L;

                    /*
                    此映射通常充当分箱(分桶)哈希表,但当箱变得太大时,它们会转换为 TreeNode 的箱,
                    每个结构类似于 java.util.TreeMap 中的结构。
                    大多数方法尝试使用正常的 bin,但出于实用性有时候会过渡到 TreeNode 方法(只需检查节点的实例)。
                    TreeNode 的 bin 可以像任何其他 bin 一样被遍历和使用,但在填充过多时还支持更快的查找。
                    但是,由于绝大多数正常使用的 bin 并没有过度填充,
                    因此在 table 方法的过程中检查树 bin 的存在可能会白花时间。

                    因为 TreeNode 的大小大约是常规节点的两倍,
                    所以我们仅在 bin 包含足够的节点以保证使用时才使用它们(请参阅 TREEIFY_THRESHOLD)。
                    当它们变得太小(由于移除或调整大小)时,它们会被转换回plain bins。
                    */

                    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

                    static final int MAXIMUM_CAPACITY = 1 << 30;

                    static final float DEFAULT_LOAD_FACTOR = 0.75f;

                    /*
                    The bin count 临界值 for using a tree rather than list for a bin.
                    当桶内节点数大于等于该值时,桶将由链表连接转化为树状结构。
                    该值必须大于 2 并且应该至少为 8,以便与树移除中关于在收缩时转换回普通 bin 的假设相吻合。
                    */
                    static final int TREEIFY_THRESHOLD = 8;

                    //The bin count threshold for untreeifying a (split) bin during a resize operation.
                    static final int UNTREEIFY_THRESHOLD = 6;

                    /*
                    The smallest table capacity for which bins may be treeified.
                    (Otherwise the table is resized if too many nodes in a bin.)
                    Should be at least 4 * TREEIFY_THRESHOLD to avoid conflicts between
                    resizing and treeification thresholds.
                    */
                    static final int MIN_TREEIFY_CAPACITY = 64;

                    static class Node<K,V> implements Map.Entry<K,V> {
                    //一旦被构造器初始化,就不可变。
                    final int hash;
                    //结点的键不变,但值可变
                    final K key;
                    V value;
                    //链表结构
                    Node<K,V> next;

                    Node(int hash, K key, V value, Node<K,V> next) {
                    this.hash = hash;
                    this.key = key;
                    this.value = value;
                    this.next = next;
                    }

                    public final K getKey() { return key; }
                    public final V getValue() { return value; }
                    public final String toString() { return key + "=" + value; }

                    //也就是说它自己的hashcode和构造时给它的hash是不一样的
                    public final int hashCode() {
                    return Objects.hashCode(key) ^ Objects.hashCode(value);
                    }

                    public final V setValue(V newValue) {
                    V oldValue = value;
                    value = newValue;
                    return oldValue;
                    }

                    public final boolean equals(Object o) {
                    if (o == this)
                    return true;
                    if (o instanceof Map.Entry) {
                    Map.Entry<?,?> e = (Map.Entry<?,?>)o;
                    if (Objects.equals(key, e.getKey()) &&
                    Objects.equals(value, e.getValue()))
                    return true;
                    }
                    return false;
                    }
                    }

                    /* ----------------静态共用方法-------------- */

                    //hash的计算方法
                    //1
                    static final int hash(Object key) {
                    int h;
                    //逻辑右移
                    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
                    }

                    //3
                    static Class<?> comparableClassFor(Object x) {
                    if (x instanceof Comparable) {
                    Class<?> c; Type[] ts, as; Type t; ParameterizedType p;
                    if ((c = x.getClass()) == String.class) // bypass checks
                    return c;
                    //检查所有接口
                    if ((ts = c.getGenericInterfaces()) != null) {
                    for (int i = 0; i < ts.length; ++i) {
                    if (((t = ts[i]) instanceof ParameterizedType) &&
                    ((p = (ParameterizedType)t).getRawType() ==
                    Comparable.class) &&
                    (as = p.getActualTypeArguments()) != null &&
                    as.length == 1 && as[0] == c) // type arg is c
                    return c;
                    }
                    }
                    }
                    return null;
                    }

                    @SuppressWarnings({"rawtypes","unchecked"}) // for cast to Comparable
                    static int compareComparables(Class<?> kc, Object k, Object x) {
                    return (x == null || x.getClass() != kc ? 0 :
                    //会调用最新版本的方法
                    ((Comparable)k).compareTo(x));
                    }

                    //这一通操作可以得到比cap大的,且离cap最近的2的幂次方数
                    static final int tableSizeFor(int cap) {
                    int n = cap - 1;
                    n |= n >>> 1;
                    n |= n >>> 2;
                    n |= n >>> 4;
                    n |= n >>> 8;
                    n |= n >>> 16;
                    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
                    }

                    /* ---------------- Fields -------------- */

                    /*
                    The table, initialized on first use, and resized as necessary.
                    长度是2的幂次或者0【初始】
                    */
                    transient Node<K,V>[] table;

                    //4
                    transient Set<Map.Entry<K,V>> entrySet;

                    //初始为0,每put一次元素就++。
                    transient int size;

                    transient int modCount;

                    //达到此值时hashmap需要增长capacity并且rehash
                    // (可序列化
                    // Additionally, if the table array has not been allocated, this
                    // field holds the initial array capacity, or zero signifying
                    // DEFAULT_INITIAL_CAPACITY.)
                    int threshold;

                    final float loadFactor;

                    /* ---------------- Public operations -------------- */

                    public HashMap(int initialCapacity, float loadFactor) {
                    if (initialCapacity < 0)
                    throw new IllegalArgumentException("Illegal initial capacity: " +
                    initialCapacity);
                    if (initialCapacity > MAXIMUM_CAPACITY)
                    initialCapacity = MAXIMUM_CAPACITY;
                    if (loadFactor <= 0 || Float.isNaN(loadFactor))
                    throw new IllegalArgumentException("Illegal load factor: " +
                    loadFactor);
                    this.loadFactor = loadFactor;
                    this.threshold = tableSizeFor(initialCapacity);
                    }

                    public HashMap(int initialCapacity) {
                    this(initialCapacity, DEFAULT_LOAD_FACTOR);
                    }

                    public HashMap() {
                    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
                    }

                    public HashMap(Map<? extends K, ? extends V> m) {
                    this.loadFactor = DEFAULT_LOAD_FACTOR;
                    putMapEntries(m, false);
                    }

                    //Implements Map.putAll and 上面的Map constructor的辅助方法
                    //evict – false when initially constructing this map, else true
                    final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
                    int s = m.size();
                    if (s > 0) {
                    if (table == null) { // pre-size
                    //+1保证了至少比m大
                    float ft = ((float)s / loadFactor) + 1.0F;
                    int t = ((ft < (float)MAXIMUM_CAPACITY) ?
                    (int)ft : MAXIMUM_CAPACITY);
                    if (t > threshold)
                    threshold = tableSizeFor(t);
                    //延迟resize,随处可见的懒汉思想,很聪明
                    }
                    else if (s > threshold)
                    //就地resize
                    resize();
                    for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
                    K key = e.getKey();
                    V value = e.getValue();
                    putVal(hash(key), key, value, false, evict);
                    }
                    }
                    }

                    public int size() {
                    return size;
                    }

                    public boolean isEmpty() {
                    return size == 0;
                    }

                    public V get(Object key) {
                    Node<K,V> e;
                    return (e = getNode(hash(key), key)) == null ? null : e.value;
                    }

                    final Node<K,V> getNode(int hash, Object key) {
                    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
                    if ((tab = table) != null && (n = tab.length) > 0 &&
                    (first = tab[(n - 1) & hash]) != null) {
                    if (first.hash == hash && // always check first node
                    ((k = first.key) == key || (key != null && key.equals(k))))
                    return first;
                    if ((e = first.next) != null) {
                    if (first instanceof TreeNode)
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                    do {
                    if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
                    } while ((e = e.next) != null);
                    }
                    }
                    return null;
                    }

                    public boolean containsKey(Object key) {
                    return getNode(hash(key), key) != null;
                    }

                    //put方法的实现
                    public V put(K key, V value) {
                    //计算key的哈希值
                    return putVal(hash(key), key, value, false, true);
                    }

                    //evict – false when initially constructing this map, else true
                    //Implements Map.put and related methods.
                    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                    boolean evict) {
                    Node<K,V>[] tab; Node<K,V> p; int n, i;
                    if ((tab = table) == null || (n = tab.length) == 0)
                    //此处调用resize初始化
                    n = (tab = resize()).length;
                    //n为table大小
                    //首先先找到所在桶
                    //如果所在桶不存在,就直接申请一个新桶(结点)放
                    //2此处找桶的方式
                    if ((p = tab[i = (n - 1) & hash]) == null)
                    tab[i] = newNode(hash, key, value, null);
                    //所在桶存在
                    else {
                    //e为要塞进去value的结点,k为临时变量,用于存储key值
                    Node<K,V> e; K k;
                    //如果p的哈希值为key的哈希值,并且p的key==key,说明键本来就存在,并且正好是桶内第一个元素,只需修改旧键值对的value就行
                    if (p.hash == hash &&
                    ((k = p.key) == key || (key != null && key.equals(k))))
                    //e=旧结点
                    e = p;
                    //否则需要沿着桶的结构继续往下找,这时候就需要看桶内用的是树状结构还是顺序结构了
                    //如果此时用的是树状结构
                    else if (p instanceof TreeNode)
                    e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
                    //用的是顺序结构
                    else {
                    for (int binCount = 0; ; ++binCount) {
                    //走到桶尽头,此时e==NULL
                    if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    //到达临界点,需要树化
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                    treeifyBin(tab, hash);
                    break;
                    }
                    //一直走,直到找到
                    if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                    //两个指针来回交替往下走
                    p = e;
                    }
                    }
                    //上面可以看到,只有原来就存在键值对才会满足此条件
                    if (e != null) { // existing mapping for key
                    V oldValue = e.value;
                    //onlyIfAbsent – if true, don't change existing value 除非旧值为空
                    if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                    //空操作,方便LinkedHashMap的后续实现
                    afterNodeAccess(e);
                    //存在旧键值对的情况至此结束
                    return oldValue;
                    }
                    }
                    //走到这说明是新建了一个结点
                    ++modCount;
                    if (++size > threshold)
                    resize();
                    //空操作,方便LinkedHashMap的后续实现
                    afterNodeInsertion(evict);
                    return null;
                    }

                    //Initializes or doubles table size.
                    final Node<K,V>[] resize() {
                    Node<K,V>[] oldTab = table;
                    int oldCap = (oldTab == null) ? 0 : oldTab.length;
                    int oldThr = threshold;
                    int newCap, newThr = 0;

                    //决定newCap和newThr
                    if (oldCap > 0) {
                    if (oldCap >= MAXIMUM_CAPACITY) {
                    threshold = Integer.MAX_VALUE;
                    return oldTab;
                    }
                    //扩容两倍
                    else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                    oldCap >= DEFAULT_INITIAL_CAPACITY)
                    newThr = oldThr << 1; // double threshold
                    }
                    else if (oldThr > 0) // initial capacity was placed in threshold
                    //因为此时capacity已经需要向threshold转变了,因而newThr需要再计算
                    newCap = oldThr;
                    else { // zero initial threshold signifies using defaults
                    newCap = DEFAULT_INITIAL_CAPACITY;
                    newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
                    }
                    if (newThr == 0) {
                    float ft = (float)newCap * loadFactor;
                    newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                    (int)ft : Integer.MAX_VALUE);
                    }
                    threshold = newThr;

                    @SuppressWarnings({"rawtypes","unchecked"})
                    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
                    table = newTab;
                    if (oldTab != null) {
                    //需要复制原oldTab中的每个结点
                    for (int j = 0; j < oldCap; ++j) {
                    Node<K,V> e;
                    if ((e = oldTab[j]) != null) {
                    oldTab[j] = null;
                    //该桶只有一个结点
                    if (e.next == null)
                    newTab[e.hash & (newCap - 1)] = e;
                    else if (e instanceof TreeNode)
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { // preserve order
                    //5
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    do {
                    next = e.next;
                    if ((e.hash & oldCap) == 0) {
                    if (loTail == null)
                    loHead = e;
                    else
                    loTail.next = e;
                    loTail = e;
                    }
                    else {
                    if (hiTail == null)
                    hiHead = e;
                    else
                    hiTail.next = e;
                    hiTail = e;
                    }
                    } while ((e = next) != null);
                    if (loTail != null) {
                    loTail.next = null;
                    newTab[j] = loHead;
                    }
                    if (hiTail != null) {
                    hiTail.next = null;
                    newTab[j + oldCap] = hiHead;
                    }
                    }
                    }
                    }
                    }
                    return newTab;
                    }

                    //树化桶
                    final void treeifyBin(Node<K,V>[] tab, int hash) {
                    int n, index; Node<K,V> e;
                    //如果表的一个桶结点数大于8(TREEIFY_THRESHOLD),但是表的总结点数小于64(MIN_TREEIFY_CAPACITY)也是不会树化的,只会resize重新hash
                    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
                    resize();
                    //需要树化
                    //取得该桶的头结点e
                    else if ((e = tab[index = (n - 1) & hash]) != null) {
                    TreeNode<K,V> hd = null, tl = null;
                    do {
                    //replacementTreeNode return new TreeNode<>(p.hash, p.key, p.value, next);
                    TreeNode<K,V> p = replacementTreeNode(e, null);
                    if (tl == null)
                    //此时有0个结点
                    hd = p;
                    else {
                    p.prev = tl;
                    tl.next = p;
                    }
                    tl = p;
                    } while ((e = e.next) != null);
                    if ((tab[index] = hd) != null)
                    //只树化该桶
                    hd.treeify(tab);
                    }
                    }

                    //对于重复键需替换
                    public void putAll(Map<? extends K, ? extends V> m) {
                    putMapEntries(m, true);
                    }

                    //Returns:the previous value
                    public V remove(Object key) {
                    Node<K,V> e;
                    return (e = removeNode(hash(key), key, null, false, true)) == null ?
                    null : e.value;
                    }

                    //matchValue – if true only remove if value is equal
                    //value – the value to match if matchValue, else ignored
                    //movable – if false do not move other nodes while removing用于树
                    final Node<K,V> removeNode(int hash, Object key, Object value,
                    boolean matchValue, boolean movable) {
                    Node<K,V>[] tab; Node<K,V> p; int n, index;
                    //table和键都存在
                    if ((tab = table) != null && (n = tab.length) > 0 &&
                    (p = tab[index = (n - 1) & hash]) != null) {
                    //node为要移走的结点
                    Node<K,V> node = null, e; K k; V v;
                    //检查头结点
                    if (p.hash == hash &&
                    ((k = p.key) == key || (key != null && key.equals(k))))
                    node = p;
                    else if ((e = p.next) != null) {
                    if (p instanceof TreeNode)
                    node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
                    else {
                    do {
                    if (e.hash == hash &&
                    ((k = e.key) == key ||
                    (key != null && key.equals(k)))) {
                    node = e;
                    break;
                    }
                    p = e;
                    } while ((e = e.next) != null);
                    }
                    }
                    //需要移走
                    if (node != null && (!matchValue || (v = node.value) == value ||
                    (value != null && value.equals(v)))) {
                    if (node instanceof TreeNode)
                    ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
                    //由上文可知,此时node==p==头结点
                    //能找到这个差异点也是真牛逼
                    else if (node == p)
                    tab[index] = node.next;
                    //此时p.next=node
                    else
                    p.next = node.next;
                    ++modCount;
                    --size;
                    afterNodeRemoval(node);
                    return node;
                    }
                    }
                    return null;
                    }

                    public void clear() {
                    Node<K,V>[] tab;
                    modCount++;
                    if ((tab = table) != null && size > 0) {
                    size = 0;
                    for (int i = 0; i < tab.length; ++i)
                    tab[i] = null;//我知道你要说什么:let GC do its work
                    }
                    }

                    //遍历。有树优化的话可以减少时间开销。
                    public boolean containsValue(Object value) {
                    Node<K,V>[] tab; V v;
                    if ((tab = table) != null && size > 0) {
                    for (int i = 0; i < tab.length; ++i) {
                    for (Node<K,V> e = tab[i]; e != null; e = e.next) {
                    if ((v = e.value) == value ||
                    (value != null && value.equals(v)))
                    return true;
                    }
                    }
                    }
                    return false;
                    }

                    public Set<K> keySet() {
                    Set<K> ks = keySet;
                    if (ks == null) {
                    //是HashMap自己实现的keyset
                    ks = new KeySet();
                    keySet = ks;
                    }
                    return ks;
                    }

                    final class KeySet extends AbstractSet<K> {
                    public final int size() { return size; }
                    public final void clear() { HashMap.this.clear(); }
                    public final Iterator<K> iterator() { return new KeyIterator(); }
                    public final boolean contains(Object o) { return containsKey(o); }
                    public final boolean remove(Object key) {
                    return removeNode(hash(key), key, null, false, true) != null;
                    }
                    public final Spliterator<K> spliterator() {
                    return new KeySpliterator<>(HashMap.this, 0, -1, 0, 0);
                    }
                    public final void forEach(Consumer<? super K> action) {
                    Node<K,V>[] tab;
                    if (action == null)
                    throw new NullPointerException();
                    if (size > 0 && (tab = table) != null) {
                    int mc = modCount;
                    for (int i = 0; i < tab.length; ++i) {
                    for (Node<K,V> e = tab[i]; e != null; e = e.next)
                    action.accept(e.key);
                    }
                    if (modCount != mc)
                    throw new ConcurrentModificationException();
                    }
                    }
                    }

                    public Collection<V> values() {
                    Collection<V> vs = values;
                    if (vs == null) {
                    vs = new Values();
                    values = vs;
                    }
                    return vs;
                    }

                    final class Values extends AbstractCollection<V> {
                    public final int size() { return size; }
                    public final void clear() { HashMap.this.clear(); }
                    public final Iterator<V> iterator() { return new ValueIterator(); }
                    public final boolean contains(Object o) { return containsValue(o); }
                    public final Spliterator<V> spliterator() {
                    return new ValueSpliterator<>(HashMap.this, 0, -1, 0, 0);
                    }
                    public final void forEach(Consumer<? super V> action) {
                    Node<K,V>[] tab;
                    if (action == null)
                    throw new NullPointerException();
                    if (size > 0 && (tab = table) != null) {
                    int mc = modCount;
                    for (int i = 0; i < tab.length; ++i) {
                    for (Node<K,V> e = tab[i]; e != null; e = e.next)
                    action.accept(e.value);
                    }
                    if (modCount != mc)
                    throw new ConcurrentModificationException();
                    }
                    }
                    }

                    public Set<Map.Entry<K,V>> entrySet() {
                    Set<Map.Entry<K,V>> es;
                    return (es = entrySet) == null ? (entrySet = new EntrySet()) : es;
                    }

                    final class EntrySet extends AbstractSet<Map.Entry<K,V>> {
                    public final int size() { return size; }
                    public final void clear() { HashMap.this.clear(); }
                    public final Iterator<Map.Entry<K,V>> iterator() {
                    return new EntryIterator();
                    }
                    //不如直接用map的contains、remove等等等
                    public final boolean contains(Object o) {
                    if (!(o instanceof Map.Entry))
                    return false;
                    Map.Entry<?,?> e = (Map.Entry<?,?>) o;
                    Object key = e.getKey();
                    Node<K,V> candidate = getNode(hash(key), key);
                    return candidate != null && candidate.equals(e);
                    }
                    public final boolean remove(Object o) {
                    if (o instanceof Map.Entry) {
                    Map.Entry<?,?> e = (Map.Entry<?,?>) o;
                    Object key = e.getKey();
                    Object value = e.getValue();
                    //只在值相等的时候remove
                    return removeNode(hash(key), key, value, true, true) != null;
                    }
                    return false;
                    }
                    public final Spliterator<Map.Entry<K,V>> spliterator() {
                    return new EntrySpliterator<>(HashMap.this, 0, -1, 0, 0);
                    }
                    public final void forEach(Consumer<? super Map.Entry<K,V>> action) {
                    Node<K,V>[] tab;
                    if (action == null)
                    throw new NullPointerException();
                    if (size > 0 && (tab = table) != null) {
                    int mc = modCount;
                    for (int i = 0; i < tab.length; ++i) {
                    for (Node<K,V> e = tab[i]; e != null; e = e.next)
                    action.accept(e);
                    }
                    if (modCount != mc)
                    throw new ConcurrentModificationException();
                    }
                    }
                    }

                    // Overrides of JDK8 Map extension methods

                    //Returns the value to which the specified key is mapped,
                    //or defaultValue if this map contains no mapping for the key.
                    @Override
                    public V getOrDefault(Object key, V defaultValue) {
                    Node<K,V> e;
                    return (e = getNode(hash(key), key)) == null ? defaultValue : e.value;
                    }

                    //If the specified key is not already associated with a value (or is mapped to null)
                    //associates it with the given value and returns null,
                    //else returns the current value.
                    @Override
                    public V putIfAbsent(K key, V value) {
                    return putVal(hash(key), key, value, true, true);
                    }

                    //只有在curVal==value且key存在的情况下才remove掉键值对
                    @Override
                    public boolean remove(Object key, Object value) {
                    return removeNode(hash(key), key, value, true, true) != null;
                    }

                    @Override
                    public boolean replace(K key, V oldValue, V newValue) {
                    Node<K,V> e; V v;
                    if ((e = getNode(hash(key), key)) != null &&
                    ((v = e.value) == oldValue || (v != null && v.equals(oldValue)))) {
                    e.value = newValue;
                    afterNodeAccess(e);
                    return true;
                    }
                    return false;
                    }

                    @Override
                    public V replace(K key, V value) {
                    Node<K,V> e;
                    if ((e = getNode(hash(key), key)) != null) {
                    V oldValue = e.value;
                    e.value = value;
                    afterNodeAccess(e);
                    return oldValue;
                    }
                    return null;
                    }

                    //如果key对应键值对不存在,就创建一个新的,并把它的值置为paramFunction(key)
                    //返回的是修改后的值。
                    //其他详见Map的第4点
                    @Override
                    public V computeIfAbsent(K key,
                    Function<? super K, ? extends V> mappingFunction) {
                    if (mappingFunction == null)
                    throw new NullPointerException();
                    int hash = hash(key);
                    Node<K,V>[] tab; Node<K,V> first; int n, i;
                    int binCount = 0;
                    TreeNode<K,V> t = null;
                    Node<K,V> old = null;
                    if (size > threshold || (tab = table) == null ||
                    (n = tab.length) == 0)
                    n = (tab = resize()).length;
                    if ((first = tab[i = (n - 1) & hash]) != null) {
                    if (first instanceof TreeNode)
                    old = (t = (TreeNode<K,V>)first).getTreeNode(hash, key);
                    else {
                    Node<K,V> e = first; K k;
                    do {
                    if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k)))) {
                    old = e;
                    break;
                    }
                    ++binCount;
                    } while ((e = e.next) != null);
                    }
                    V oldValue;
                    if (old != null && (oldValue = old.value) != null) {
                    afterNodeAccess(old);
                    return oldValue;
                    }
                    }
                    V v = mappingFunction.apply(key);
                    if (v == null) {
                    return null;
                    } else if (old != null) {
                    old.value = v;
                    afterNodeAccess(old);
                    return v;
                    }
                    else if (t != null)
                    t.putTreeVal(this, tab, hash, key, v);
                    else {
                    tab[i] = newNode(hash, key, v, first);
                    if (binCount >= TREEIFY_THRESHOLD - 1)
                    treeifyBin(tab, hash);
                    }
                    ++modCount;
                    //++size后不用再check是否>threshold吗 ?为啥要交给上面一开始的时候判断
                    ++size;
                    afterNodeInsertion(true);
                    return v;
                    }

                    //return 新值
                    public V computeIfPresent(K key,
                    BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
                    if (remappingFunction == null)
                    throw new NullPointerException();
                    Node<K,V> e; V oldValue;
                    int hash = hash(key);
                    if ((e = getNode(hash, key)) != null &&
                    (oldValue = e.value) != null) {
                    V v = remappingFunction.apply(key, oldValue);
                    if (v != null) {
                    e.value = v;
                    afterNodeAccess(e);
                    return v;
                    }
                    else
                    removeNode(hash, key, null, false, true);
                    }
                    return null;
                    }

                    @Override
                    public V compute(K key,
                    BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
                    if (remappingFunction == null)
                    throw new NullPointerException();
                    int hash = hash(key);
                    Node<K,V>[] tab; Node<K,V> first; int n, i;
                    int binCount = 0;
                    TreeNode<K,V> t = null;
                    Node<K,V> old = null;
                    if (size > threshold || (tab = table) == null ||
                    (n = tab.length) == 0)
                    n = (tab = resize()).length;
                    if ((first = tab[i = (n - 1) & hash]) != null) {
                    if (first instanceof TreeNode)
                    old = (t = (TreeNode<K,V>)first).getTreeNode(hash, key);
                    else {
                    Node<K,V> e = first; K k;
                    do {
                    if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k)))) {
                    old = e;
                    break;
                    }
                    ++binCount;
                    } while ((e = e.next) != null);
                    }
                    }
                    V oldValue = (old == null) ? null : old.value;
                    V v = remappingFunction.apply(key, oldValue);
                    if (old != null) {
                    if (v != null) {
                    old.value = v;
                    afterNodeAccess(old);
                    }
                    else
                    removeNode(hash, key, null, false, true);
                    }
                    else if (v != null) {
                    if (t != null)
                    t.putTreeVal(this, tab, hash, key, v);
                    else {
                    tab[i] = newNode(hash, key, v, first);
                    if (binCount >= TREEIFY_THRESHOLD - 1)
                    treeifyBin(tab, hash);
                    }
                    ++modCount;
                    ++size;
                    afterNodeInsertion(true);
                    }
                    return v;
                    }

                    @Override
                    public V merge(K key, V value,
                    BiFunction<? super V, ? super V, ? extends V> remappingFunction) {
                    if (value == null)
                    throw new NullPointerException();
                    if (remappingFunction == null)
                    throw new NullPointerException();
                    int hash = hash(key);
                    Node<K,V>[] tab; Node<K,V> first; int n, i;
                    int binCount = 0;
                    TreeNode<K,V> t = null;
                    Node<K,V> old = null;
                    if (size > threshold || (tab = table) == null ||
                    (n = tab.length) == 0)
                    n = (tab = resize()).length;
                    if ((first = tab[i = (n - 1) & hash]) != null) {
                    if (first instanceof TreeNode)
                    old = (t = (TreeNode<K,V>)first).getTreeNode(hash, key);
                    else {
                    Node<K,V> e = first; K k;
                    do {
                    if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k)))) {
                    old = e;
                    break;
                    }
                    ++binCount;
                    } while ((e = e.next) != null);
                    }
                    }
                    if (old != null) {
                    V v;
                    if (old.value != null)
                    v = remappingFunction.apply(old.value, value);
                    else
                    v = value;
                    if (v != null) {
                    old.value = v;
                    afterNodeAccess(old);
                    }
                    else
                    removeNode(hash, key, null, false, true);
                    return v;
                    }
                    if (value != null) {
                    if (t != null)
                    t.putTreeVal(this, tab, hash, key, value);
                    else {
                    tab[i] = newNode(hash, key, value, first);
                    if (binCount >= TREEIFY_THRESHOLD - 1)
                    treeifyBin(tab, hash);
                    }
                    ++modCount;
                    ++size;
                    afterNodeInsertion(true);
                    }
                    return value;
                    }

                    @Override
                    public void forEach(BiConsumer<? super K, ? super V> action) {
                    Node<K,V>[] tab;
                    if (action == null)
                    throw new NullPointerException();
                    if (size > 0 && (tab = table) != null) {
                    int mc = modCount;
                    for (int i = 0; i < tab.length; ++i) {
                    for (Node<K,V> e = tab[i]; e != null; e = e.next)
                    action.accept(e.key, e.value);
                    }
                    if (modCount != mc)
                    throw new ConcurrentModificationException();
                    }
                    }

                    @Override
                    public void replaceAll(BiFunction<? super K, ? super V, ? extends V> function) {
                    Node<K,V>[] tab;
                    if (function == null)
                    throw new NullPointerException();
                    if (size > 0 && (tab = table) != null) {
                    int mc = modCount;
                    for (int i = 0; i < tab.length; ++i) {
                    for (Node<K,V> e = tab[i]; e != null; e = e.next) {
                    e.value = function.apply(e.key, e.value);
                    }
                    }
                    if (modCount != mc)
                    throw new ConcurrentModificationException();
                    }
                    }

                    // Cloning and serialization

                    @SuppressWarnings("unchecked")
                    @Override
                    public Object clone() {
                    HashMap<K,V> result;
                    try {
                    result = (HashMap<K,V>)super.clone();
                    } catch (CloneNotSupportedException e) {
                    // this shouldn't happen, since we are Cloneable
                    throw new InternalError(e);
                    }
                    result.reinitialize();
                    result.putMapEntries(this, false);
                    return result;
                    }

                    // These methods are also used when serializing HashSets
                    final float loadFactor() { return loadFactor; }
                    final int capacity() {
                    return (table != null) ? table.length :
                    (threshold > 0) ? threshold :
                    DEFAULT_INITIAL_CAPACITY;
                    }

                    private void writeObject(java.io.ObjectOutputStream s)
                    throws IOException {...}

                    private void readObject(ObjectInputStream s)
                    throws IOException, ClassNotFoundException {...}

                    // Support for resetting final field during deserializing
                    private static final class UnsafeHolder {...}

                    // iterators

                    //7
                    abstract class HashIterator {
                    Node<K,V> next; // next entry to return
                    Node<K,V> current; // current entry
                    int expectedModCount; // for fast-fail
                    int index; // current slot

                    HashIterator() {
                    expectedModCount = modCount;
                    Node<K,V>[] t = table;
                    current = next = null;
                    index = 0;
                    //指向第一个非空表项
                    if (t != null && size > 0) { // advance to first entry
                    do {} while (index < t.length && (next = t[index++]) == null);
                    }
                    }

                    public final boolean hasNext() {
                    return next != null;
                    }

                    final Node<K,V> nextNode() {
                    Node<K,V>[] t;
                    Node<K,V> e = next;
                    if (modCount != expectedModCount)
                    throw new ConcurrentModificationException();
                    if (e == null)
                    throw new NoSuchElementException();
                    //移动桶内指针
                    if ((next = (current = e).next) == null && (t = table) != null) {
                    //如果桶内表到达尽头,则移动选择桶的指针
                    do {} while (index < t.length && (next = t[index++]) == null);
                    }
                    return e;
                    }

                    public final void remove() {
                    Node<K,V> p = current;
                    if (p == null)
                    throw new IllegalStateException();
                    if (modCount != expectedModCount)
                    throw new ConcurrentModificationException();
                    current = null;
                    K key = p.key;
                    removeNode(hash(key), key, null, false, false);
                    expectedModCount = modCount;
                    }
                    }

                    final class KeyIterator extends HashIterator
                    implements Iterator<K> {
                    public final K next() { return nextNode().key; }
                    }

                    final class ValueIterator extends HashIterator
                    implements Iterator<V> {
                    public final V next() { return nextNode().value; }
                    }

                    final class EntryIterator extends HashIterator
                    implements Iterator<Map.Entry<K,V>> {
                    public final Map.Entry<K,V> next() { return nextNode(); }
                    }

                    // spliterators

                    static class HashMapSpliterator<K,V> {...}

                    static final class KeySpliterator<K,V>
                    extends HashMapSpliterator<K,V>
                    implements Spliterator<K> {...}

                    static final class ValueSpliterator<K,V>
                    extends HashMapSpliterator<K,V>
                    implements Spliterator<V> {...}

                    static final class EntrySpliterator<K,V>
                    extends HashMapSpliterator<K,V>
                    implements Spliterator<Map.Entry<K,V>> {...}

                    // LinkedHashMap support

                    // Create a regular (non-tree) node
                    Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) {
                    return new Node<>(hash, key, value, next);
                    }

                    // For conversion from TreeNodes to plain nodes
                    Node<K,V> replacementNode(Node<K,V> p, Node<K,V> next) {
                    return new Node<>(p.hash, p.key, p.value, next);
                    }

                    // Create a tree bin node
                    TreeNode<K,V> newTreeNode(int hash, K key, V value, Node<K,V> next) {
                    return new TreeNode<>(hash, key, value, next);
                    }

                    // For treeifyBin
                    TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) {
                    return new TreeNode<>(p.hash, p.key, p.value, next);
                    }

                    void reinitialize() {
                    table = null;
                    entrySet = null;
                    keySet = null;
                    values = null;
                    modCount = 0;
                    threshold = 0;
                    size = 0;
                    }

                    // Callbacks to allow LinkedHashMap post-actions
                    void afterNodeAccess(Node<K,V> p) { }
                    void afterNodeInsertion(boolean evict) { }
                    void afterNodeRemoval(Node<K,V> p) { }

                    // Called only from writeObject, to ensure compatible ordering.
                    void internalWriteEntries(java.io.ObjectOutputStream s) throws IOException {...}

                    // Tree bins

                    //6红黑树介绍,此部分具体的红黑树实现省略
                    static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {...}
                    }
                    - -

                    其中:

                      -
                    1. hash()

                      - -

                      hash=原hashcode^(原hashcode逻辑右移16位)

                      -

                      这样的话,由于右移16位补零,此时高位的所有比特位都跟原来一样,低位的比特位变成了融合高低位特点的东西,这样就可以减少冲突,增加均匀性

                      +
                    2. 动态调整等待时间

                      +

                      image-20231012113725655

                    3. -
                    4. table[(n-1)&hash]

                      具体看这个视频,讲得非常不错

                      -

                      【Java面试必问】HashMap中是如何计算数组下标的?

                      -

                      假设table此时为默认长度16.则n-1=15

                      -

                      写出15的二进制形式:0000 1111,可以发现,任何数跟它相与,结果都一定为0000 xxxx,永不越界。

                      -

                      写出16的二进制形式:0001 0000,可以发现,任何数跟它相与,结果都一定为16或者0.

                      -

                      可以发现15有非常好的性质。

                      -

                      而扩展出来,任何2的幂次方-1都具有这样的良好的性质。**这也是为什么hashmap要求表的长度应该为2的幂次。**

                      -

                      而且,除了不会越界,还有一点就是,这个任何数与15相与的与操作就相当于,任何数对16取余的取余操作。这点实在是佩服啊,把复杂的取余操作在该场景下直接用一个位运算就搞定了。

                      +
                    +

                    以太网

                      +
                    1. 以太网的定义

                      +

                      image-20231012113840636

                    2. -
                    3. comparableClassFor

                      树状结构时结点的默认排序方式是by hashCode。但如果两个结点元素之间是同一个class C,并且这个C实现了Comparable方法,那么就不会按照它们的hashCode比较,而是会调用class C的compareTo方法。

                      -

                      (We conservatively(保守地) check generic types via reflection to validate(证实) this – see method comparableClassFor).

                      -

                      也就是说这个comparableClassFor方法的意图就是,如果这个类是comparable的,就返回它具体类型,如果不是返回null。

                      +
                    4. 系统初始化时MAC地址的设置

                      +

                      MAC地址是上电后由驱动程序从ROM中读取的,而非自动获取的

                    5. -
                    6. entrySet

                      不同于AbstractMap中entrySet的核心作用,HashMap的put、get、clear等等等核心函数都不依赖于entrySet了,毕竟结构改变得比较多了。因而这里的entrySet字段保留,只是为了呼应AbstractMap中keyset和valueset的实现,以及补充AbstractMap中未给出的EntrySet实现。

                      +
                    7. 电信号转换【这个帅得不行】

                      +

                      为了区分连续的1或0,我们就需要同时发送数据信号和时钟信号,然而这样开销太大,因而我们引入了上升沿

                      +

                      上升沿本质上是数据信号和时钟信号叠加而成的结果,叠加方式是异或

                      +

                      image-20231012114006378

                      +

                      提到异或是否感觉豁然开朗?是的,这东西恢复时也是使用了异或的性质:接收方从帧头获取时钟频率从而得到时钟信号,跟收到的叠加信号进行再次叠加(异或),就可以获得原来的数据信号了。

                      +

                      我只能说牛逼,一直以来对异或的视角还停留在单纯的数字,这个波形的物理概念真的惊到我了。

                      +

                      实例:

                      +

                      image-20231012114350599

                    8. -
                    9. resize()扩容旧表到新表的转移

                      此时需要复制oldTab中的所有结点。但注意,由于此时发生了扩容,hash的计算发生了变化,因而不能全部照搬不动oldTab中的下标,否则产生错误。因而我们需要了解一下如何调整下标。

                      -

                      首先由代码可得,对于oldTab!=NULL的情况下newCap一定是扩为原来的两倍的。因而以下只需讨论扩容为两倍的情况。

                      -

                      由第2点可知,假设现在容量为16,扩容为原来的两倍,则hash掩码应该为0000 1111,扩容后,hash掩码应该为0001 1111,可见就只是多了一位,因而,oldTab中,若这一位的值为0,则在新表和旧表中位置的下标应该是一样的;若这一位的值为1,则新表下标=旧表下标+offset,offset正是等于0001 0000.而这个“0001 0000”,正是oldCap!

                      -

                      对于容量为其他值,全部道理都是一样的。

                      -

                      因而我们要做的,是对旧表的每一个桶内的所有结点,把它们分成两类,一类为(e.hash & oldCap) == 0【也就是这一位值为0 情况】和(e.hash & oldCap) == 1,然后对这两类进行在新表中分别映射即可。这段代码便做了这样的事。

                      -
                                        //5
                      //low index head,下标保持不变
                      Node<K,V> loHead = null, loTail = null;
                      //high index head,下标需要增长偏移量
                      Node<K,V> hiHead = null, hiTail = null;
                      Node<K,V> next;
                      do {
                      next = e.next;
                      //第一类
                      if ((e.hash & oldCap) == 0) {
                      //一个简单的队列操作
                      if (loTail == null)
                      loHead = e;
                      else
                      loTail.next = e;
                      loTail = e;
                      }
                      //第二类
                      else {
                      if (hiTail == null)
                      hiHead = e;
                      else
                      hiTail.next = e;
                      hiTail = e;
                      }
                      } while ((e = next) != null);
                      //对于第一类
                      if (loTail != null) {
                      loTail.next = null;
                      newTab[j] = loHead;
                      }
                      //对于第二类
                      if (hiTail != null) {
                      hiTail.next = null;
                      newTab[j + oldCap] = hiHead;
                    10. -
                    11. 红黑树

                      红黑树快速入门

                      -

                      这篇文章也写得很好:

                      -

                      算法:基于红黑树的 TreeMap

                      +
                    12. 半双工模式【同一时刻只能进行发or收】使用集线器,全双工模式【发or收可以并行】使用交换机。半双工模式需要进行载波监听碰撞检测。

                    13. -
                    14. HashIterator

                      注意点有二:

                      -

                      ①不继承Iterator接口

                      -

                      ②抽象,具体实现类为EntryIterator、KeyIterator和ValueIterator

                      -

                      ③map的接口定义是没有iterator的,因此map不能通过hashiterator迭代,只能通过其vie来实现【三个具体实现类】

                      +
                    15. 服务器的操作系统具备和路由器相同的包转发功能,当打开这一功能时,它就可以像路由器一样对包进行转发。在这种情况下,当收到不是发给自己的包的时候,就会像路由器一样执行包转发操作。

                    -

                    LinkedHashMap

                    哈希表+链表/红黑树+有序队列

                    -
                    -

                    Hash table and linked list implementation of the Map interface, with predictable iteration order.

                    -

                    This implementation differs from HashMap in that it maintains a doubly-linked list running through all of its entries.

                    -

                    This linked list defines the iteration ordering, which is normally the order in which keys were inserted into the map (insertion-order).有序,顺序为元素插入的顺序

                    -

                    Note that insertion order is not affected if a key is re-inserted into the map. 当修改key的value值时,key的插入序不变

                    -

                    此实现既让hashmap变得有序,又不会像TreeMap一样有高成本。

                    -

                    It can be used to produce a copy of a map that has the same order as the original, regardless of the original map’s implementation.

                    - - -

                    这样可以保持copymap的原有顺序

                    -

                    A special constructor is provided to create a linked hash map whose order of iteration is the order in which its entries were last accessed, from least-recently accessed to most-recently (access-order). This kind of map is well-suited to building LRU caches. 可以有一个排序方式,顺序为最近最少访问->最近访问,这可以用来构建LRU cache【LRU是Least Recently Used的缩写,即最近最少使用,是一种常用的页面置换算法,选择最近最久未使用的页面予以淘汰。

                    -

                    至于这个“access”怎么定义:

                    -
                    -

                    Invoking the put, putIfAbsent, get, getOrDefault, compute, computeIfAbsent, computeIfPresent, or merge methods results in an access to the corresponding entry (assuming it exists after the invocation completes). The replace methods only result in an access of the entry if the value is replaced. The putAll method generates one entry access for each mapping in the specified map, in the order that key-value mappings are provided by the specified map’s entry set iterator.

                    -

                    注意没有remove

                    +]]> + + books + + + + 阅读JDK容器部分源码的心得体会1【Collection部分】 + /2022/10/16/%E9%98%85%E8%AF%BBJDK%E5%AE%B9%E5%99%A8%E9%83%A8%E5%88%86%E6%BA%90%E7%A0%81%E7%9A%84%E5%BF%83%E5%BE%97%E4%BD%93%E4%BC%9A1%E3%80%90Collection%E9%83%A8%E5%88%86%E3%80%91/ + +

                    idea 替换注释正则表达式/\*{1,2}[\s\S]*?\*/

                    +

                    typora 替换图片asset

                    +

                    \!\[.*\]\(D:\\aWorkStorage\\hexo\\blog\\source\\_posts\\阅读JDK容器部分源码的心得体会1【Collection部分】\\(.*)\.png\)

                    +

                    替换结果{% asset_img $1.png %}

                    -

                    也因此,对map视图【各个set】的访问不算access。【因为不调用任意一个上面方法】

                    -

                    可以重写 removeEldestEntry(Map.Entry) 方法,以在将新映射添加到映射时自动删除陈旧映射的策略。

                    - + + +

                    迭代器相关接口

                    Iterable(I)

                    /*实现这个接口的类可用于for-each循环*/

                    public interface Iterable<T> {

                    Iterator<T> iterator();

                    /*对Iterator内每个元素实施此操作,直到遍历完或者抛出异常。*/
                    default void forEach(Consumer<? super T> action) {
                    Objects.requireNonNull(action);
                    for (T t : this) {
                    action.accept(t);
                    }
                    }

                    default Spliterator<T> spliterator() {
                    return Spliterators.spliteratorUnknownSize(iterator(), 0);
                    }
                    }
                    -

                    //1

                    -

                    Iteration over the collection-views of a LinkedHashMap requires time proportional to the size of the map, regardless of its capacity.不同于hashmap,迭代时间与容量无关。

                    -

                    In access-ordered linked hash maps, merely querying the map with get is a structural modification.注意,对于access-ordered的lhm来说,**get也是一个structural modification,因为可能会修改排序顺序**。所以迭代时只能使用Iterator的next方法来得到结点,迭代器访问不会对accessorder有影响

                    -

                    代码测试:

                    -
                            LinkedHashMap<String,Integer> map = new LinkedHashMap<>(16,0.75f,true);
                    map.put("Lily",15);
                    map.put("Sam",20);
                    map.put("Mary",11);
                    map.put("Lee",111);

                    for(Iterator i = map.entrySet().iterator();i.hasNext();){
                    map.get("Lily");
                    System.out.println(i.next().toString());
                    }
                    /*
                    Exception in thread "main" java.util.ConcurrentModificationException
                    at java.util.LinkedHashMap$LinkedHashIterator.nextNode(LinkedHashMap.java:719)
                    */
                    +
                    +

                    引入迭代器的目的是为了**统一**“对容器里的元素进行遍历”这一操作。

                    -

                    总之意思就是,LinkedHashMap的数据结构:

                    -

                    在HashMap哈希表+链表/红黑树的基础上,添加一个双端队列,该双端队列的作用是来维持内部的有序,因而开销比较大。应该只提供插入序和LRU序,其他需要用到compare的排序方法需要对某些方法(如afternodeXXX)进行重写,或者直接使用sorted map。

                    -

                    LHM的一个很特殊的地方就是,它可以实现一个LRU这样的cache结构,只需要你重载removeEldestEntry return true。还可以在LHM的基础上实现有限长度map,只需要你重载removeEldestEntry 当元素>=某值时返回true。总而言之,你可以建造一个类在LHM的基础上,如果需要对map的长度有限制。

                    -

                    LHM对LRU的实现是,一旦某个结点用到了,就立刻把他移到最队尾,然后每次淘汰淘汰队首。

                    -

                    代码:

                    public class LinkedHashMap<K,V>
                    extends HashMap<K,V>
                    implements Map<K,V>
                    {

                    static class Entry<K,V> extends HashMap.Node<K,V> {
                    //原来只有next的
                    //双端队列
                    Entry<K,V> before, after;
                    Entry(int hash, K key, V value, Node<K,V> next) {
                    super(hash, key, value, next);
                    }
                    }

                    private static final long serialVersionUID = 3801124242820219131L;

                    //The head (eldest) of the doubly linked list.
                    transient LinkedHashMap.Entry<K,V> head;

                    //The tail (youngest) of the doubly linked list.
                    transient LinkedHashMap.Entry<K,V> tail;

                    //true:access顺序 false:插入顺序
                    final boolean accessOrder;

                    // internal utilities

                    // link at the end of list
                    private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
                    LinkedHashMap.Entry<K,V> last = tail;
                    tail = p;
                    if (last == null)
                    head = p;
                    else {
                    p.before = last;
                    last.after = p;
                    }
                    }

                    // apply src's links to dst
                    //相当于用dst把src取代了
                    private void transferLinks(LinkedHashMap.Entry<K,V> src,
                    LinkedHashMap.Entry<K,V> dst) {
                    LinkedHashMap.Entry<K,V> b = dst.before = src.before;
                    LinkedHashMap.Entry<K,V> a = dst.after = src.after;
                    if (b == null)
                    head = dst;
                    else
                    b.after = dst;
                    if (a == null)
                    tail = dst;
                    else
                    a.before = dst;
                    }

                    // overrides of HashMap hook methods

                    void reinitialize() {
                    super.reinitialize();
                    head = tail = null;
                    }

                    Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
                    LinkedHashMap.Entry<K,V> p =
                    new LinkedHashMap.Entry<K,V>(hash, key, value, e);
                    linkNodeLast(p);
                    return p;
                    }

                    Node<K,V> replacementNode(Node<K,V> p, Node<K,V> next) {
                    LinkedHashMap.Entry<K,V> q = (LinkedHashMap.Entry<K,V>)p;
                    LinkedHashMap.Entry<K,V> t =
                    new LinkedHashMap.Entry<K,V>(q.hash, q.key, q.value, next);
                    transferLinks(q, t);
                    return t;
                    }

                    TreeNode<K,V> newTreeNode(int hash, K key, V value, Node<K,V> next) {
                    TreeNode<K,V> p = new TreeNode<K,V>(hash, key, value, next);
                    linkNodeLast(p);
                    return p;
                    }

                    TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) {
                    LinkedHashMap.Entry<K,V> q = (LinkedHashMap.Entry<K,V>)p;
                    TreeNode<K,V> t = new TreeNode<K,V>(q.hash, q.key, q.value, next);
                    transferLinks(q, t);
                    return t;
                    }

                    //用于reove结点之后,之所以要存在就是因为LHM和HM的Node结构不一样,前者多了after和before
                    void afterNodeRemoval(Node<K,V> e) { // unlink
                    LinkedHashMap.Entry<K,V> p =
                    (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
                    p.before = p.after = null;
                    if (b == null)
                    head = a;
                    else
                    b.after = a;
                    if (a == null)
                    tail = b;
                    else
                    a.before = b;
                    }

                    //调用于put、各种compute、merge
                    void afterNodeInsertion(boolean evict) { // possibly remove eldest
                    LinkedHashMap.Entry<K,V> first;
                    //head是最老的结点
                    //如果需要插入新节点同时移去旧结点
                    if (evict && (first = head) != null && removeEldestEntry(first)) {
                    K key = first.key;
                    removeNode(hash(key), key, null, false, true);
                    }
                    }

                    void afterNodeAccess(Node<K,V> e) { // move node to last把用到的结点移到队尾
                    LinkedHashMap.Entry<K,V> last;
                    if (accessOrder && (last = tail) != e) {
                    LinkedHashMap.Entry<K,V> p =
                    (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
                    p.after = null;
                    if (b == null)
                    head = a;
                    else
                    b.after = a;
                    if (a != null)
                    a.before = b;
                    else
                    last = b;
                    if (last == null)
                    head = p;
                    else {
                    p.before = last;
                    last.after = p;
                    }
                    tail = p;
                    ++modCount;
                    }
                    }

                    void internalWriteEntries(java.io.ObjectOutputStream s) throws IOException {
                    for (LinkedHashMap.Entry<K,V> e = head; e != null; e = e.after) {
                    s.writeObject(e.key);
                    s.writeObject(e.value);
                    }
                    }

                    public LinkedHashMap(int initialCapacity, float loadFactor) {
                    super(initialCapacity, loadFactor);
                    accessOrder = false;
                    }

                    public LinkedHashMap(int initialCapacity) {
                    super(initialCapacity);
                    accessOrder = false;
                    }

                    public LinkedHashMap() {
                    super();
                    accessOrder = false;
                    }

                    public LinkedHashMap(Map<? extends K, ? extends V> m) {
                    super();
                    accessOrder = false;
                    putMapEntries(m, false);
                    }

                    //用以构造accessOrder==true的情况
                    public LinkedHashMap(int initialCapacity,
                    float loadFactor,
                    boolean accessOrder) {
                    super(initialCapacity, loadFactor);
                    this.accessOrder = accessOrder;
                    }

                    //遍历构造的队列
                    public boolean containsValue(Object value) {
                    for (LinkedHashMap.Entry<K,V> e = head; e != null; e = e.after) {
                    V v = e.value;
                    if (v == value || (value != null && value.equals(v)))
                    return true;
                    }
                    return false;
                    }

                    public V get(Object key) {
                    Node<K,V> e;
                    if ((e = getNode(hash(key), key)) == null)
                    return null;
                    if (accessOrder)
                    //structural modification
                    afterNodeAccess(e);
                    return e.value;
                    }

                    public V getOrDefault(Object key, V defaultValue) {
                    Node<K,V> e;
                    if ((e = getNode(hash(key), key)) == null)
                    return defaultValue;
                    if (accessOrder)
                    //structural modification
                    afterNodeAccess(e);
                    return e.value;
                    }

                    public void clear() {
                    super.clear();
                    head = tail = null;
                    }

                    /*
                    Returns true if this map should remove its eldest entry.
                    It provides the implementor with the opportunity to remove the eldest entry each time a new one is added.
                    This is useful if the map represents a LRU cache or other interesting implementations
                    */
                    protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
                    return false;
                    }

                    public Set<K> keySet() {
                    Set<K> ks = keySet;
                    if (ks == null) {
                    ks = new LinkedKeySet();
                    keySet = ks;
                    }
                    return ks;
                    }

                    //HashMap中这几个类都是final,所以继承不了了
                    final class LinkedKeySet extends AbstractSet<K> {
                    public final int size() { return size; }
                    public final void clear() { LinkedHashMap.this.clear(); }
                    public final Iterator<K> iterator() {
                    return new LinkedKeyIterator();
                    }
                    public final boolean contains(Object o) { return containsKey(o); }
                    public final boolean remove(Object key) {
                    return removeNode(hash(key), key, null, false, true) != null;
                    }
                    public final Spliterator<K> spliterator() {
                    return Spliterators.spliterator(this, Spliterator.SIZED |
                    Spliterator.ORDERED |
                    Spliterator.DISTINCT);
                    }
                    public final void forEach(Consumer<? super K> action) {
                    if (action == null)
                    throw new NullPointerException();
                    int mc = modCount;
                    //遍历队列
                    for (LinkedHashMap.Entry<K,V> e = head; e != null; e = e.after)
                    action.accept(e.key);
                    //保证此间代码同步
                    if (modCount != mc)
                    throw new ConcurrentModificationException();
                    }
                    }

                    public Collection<V> values() {
                    Collection<V> vs = values;
                    if (vs == null) {
                    vs = new LinkedValues();
                    values = vs;
                    }
                    return vs;
                    }

                    final class LinkedValues extends AbstractCollection<V> {
                    public final int size() { return size; }
                    public final void clear() { LinkedHashMap.this.clear(); }
                    public final Iterator<V> iterator() {
                    return new LinkedValueIterator();
                    }
                    public final boolean contains(Object o) { return containsValue(o); }
                    public final Spliterator<V> spliterator() {
                    return Spliterators.spliterator(this, Spliterator.SIZED |
                    Spliterator.ORDERED);
                    }
                    public final void forEach(Consumer<? super V> action) {
                    if (action == null)
                    throw new NullPointerException();
                    int mc = modCount;
                    for (LinkedHashMap.Entry<K,V> e = head; e != null; e = e.after)
                    action.accept(e.value);
                    if (modCount != mc)
                    throw new ConcurrentModificationException();
                    }
                    }

                    public Set<Map.Entry<K,V>> entrySet() {
                    Set<Map.Entry<K,V>> es;
                    return (es = entrySet) == null ? (entrySet = new LinkedEntrySet()) : es;
                    }

                    final class LinkedEntrySet extends AbstractSet<Map.Entry<K,V>> {
                    public final int size() { return size; }
                    public final void clear() { LinkedHashMap.this.clear(); }
                    public final Iterator<Map.Entry<K,V>> iterator() {
                    return new LinkedEntryIterator();
                    }
                    public final boolean contains(Object o) {
                    if (!(o instanceof Map.Entry))
                    return false;
                    Map.Entry<?,?> e = (Map.Entry<?,?>) o;
                    Object key = e.getKey();
                    Node<K,V> candidate = getNode(hash(key), key);
                    return candidate != null && candidate.equals(e);
                    }
                    public final boolean remove(Object o) {
                    if (o instanceof Map.Entry) {
                    Map.Entry<?,?> e = (Map.Entry<?,?>) o;
                    Object key = e.getKey();
                    Object value = e.getValue();
                    return removeNode(hash(key), key, value, true, true) != null;
                    }
                    return false;
                    }
                    public final Spliterator<Map.Entry<K,V>> spliterator() {
                    return Spliterators.spliterator(this, Spliterator.SIZED |
                    Spliterator.ORDERED |
                    Spliterator.DISTINCT);
                    }
                    public final void forEach(Consumer<? super Map.Entry<K,V>> action) {
                    if (action == null)
                    throw new NullPointerException();
                    int mc = modCount;
                    for (LinkedHashMap.Entry<K,V> e = head; e != null; e = e.after)
                    action.accept(e);
                    if (modCount != mc)
                    throw new ConcurrentModificationException();
                    }
                    }

                    // Map overrides

                    public void forEach(BiConsumer<? super K, ? super V> action) {
                    if (action == null)
                    throw new NullPointerException();
                    int mc = modCount;
                    for (LinkedHashMap.Entry<K,V> e = head; e != null; e = e.after)
                    action.accept(e.key, e.value);
                    if (modCount != mc)
                    throw new ConcurrentModificationException();
                    }

                    public void replaceAll(BiFunction<? super K, ? super V, ? extends V> function) {
                    if (function == null)
                    throw new NullPointerException();
                    int mc = modCount;
                    for (LinkedHashMap.Entry<K,V> e = head; e != null; e = e.after)
                    e.value = function.apply(e.key, e.value);
                    if (modCount != mc)
                    throw new ConcurrentModificationException();
                    }

                    // Iterators

                    abstract class LinkedHashIterator {
                    LinkedHashMap.Entry<K,V> next;
                    LinkedHashMap.Entry<K,V> current;
                    int expectedModCount;

                    LinkedHashIterator() {
                    next = head;
                    expectedModCount = modCount;
                    current = null;
                    }

                    public final boolean hasNext() {
                    return next != null;
                    }

                    final LinkedHashMap.Entry<K,V> nextNode() {
                    LinkedHashMap.Entry<K,V> e = next;
                    if (modCount != expectedModCount)
                    throw new ConcurrentModificationException();
                    if (e == null)
                    throw new NoSuchElementException();
                    current = e;
                    next = e.after;
                    return e;
                    }

                    public final void remove() {
                    Node<K,V> p = current;
                    if (p == null)
                    throw new IllegalStateException();
                    if (modCount != expectedModCount)
                    throw new ConcurrentModificationException();
                    current = null;
                    K key = p.key;
                    removeNode(hash(key), key, null, false, false);
                    expectedModCount = modCount;
                    }
                    }

                    final class LinkedKeyIterator extends LinkedHashIterator
                    implements Iterator<K> {
                    public final K next() { return nextNode().getKey(); }
                    }

                    final class LinkedValueIterator extends LinkedHashIterator
                    implements Iterator<V> {
                    public final V next() { return nextNode().value; }
                    }

                    final class LinkedEntryIterator extends LinkedHashIterator
                    implements Iterator<Map.Entry<K,V>> {
                    public final Map.Entry<K,V> next() { return nextNode(); }
                    }

                    }
                    +

                    Iterator(I)

                    public interface Iterator<E> {
                    //之后还有没有元素
                    boolean hasNext();

                    //返回当前所指元素,并且将iterator指针下移
                    E next();

                    //This call can be made only if neither remove nor add have been called after the last call to next or previous.
                    default void remove() {
                    throw new UnsupportedOperationException("remove");
                    }

                    default void forEachRemaining(Consumer<? super E> action) {
                    Objects.requireNonNull(action);
                    while (hasNext())
                    action.accept(next());
                    }
                    }
                    -

                    其中:

                      -
                    1. 迭代时间与容量无关

                      LinkedHashMap的结构跟HashMap是一样的,也就是都baked by array。此处为什么“迭代时间与容量无关”,是因为LinkedHashMap内部维护了一个简单的链表队列【包含所有元素】,迭代的时候是对这个队列进行迭代,而不是像HashMap一样通过表迭代。

                      -

                      怪不得读源码时觉得有些地方明明不重写HashMap也可以它却重写了。原来是因为这个性能问题啊

                      -
                    2. -
                    -

                    SortedMap(I)

                    -

                    A Map that further provides a total ordering on its keys.

                    -

                    The map is ordered according to the natural ordering of its keys, or by a Comparator typically provided at sorted map creation time.

                    -

                    All keys inserted into a sorted map must implement the Comparable interface (or be accepted by the specified comparator).

                    - +
                    +

                    注意,iterator并不指向具体元素,它指向的是元素的间隙

                    +

                    这些^就是iterator所指的位置。这样就能理解iterator的next和previous了吧2333

                    + -

                    关于这部分,详细见sorted set

                    +

                    而set()所修改的元素,是其上一次调用next()或者previous()方法所返回的元素。

                    +

                    What’s the meaning of this source code of interface Collection in JAVA?

                    -

                    最大的特点就是可以人为定义有序并且有sub map

                    -

                    代码:

                    public interface SortedMap<K,V> extends Map<K,V> {

                    Comparator<? super K> comparator();

                    SortedMap<K,V> subMap(K fromKey, K toKey);

                    SortedMap<K,V> headMap(K toKey);

                    SortedMap<K,V> tailMap(K fromKey);

                    //也是默认第一个是低的最后一个是高的,就跟LHM的第一个是最少使用,最后一个是最近使用一样
                    //Returns the first (lowest) key currently in this map.
                    K firstKey();

                    //Returns the last (highest) key currently in this map.
                    K lastKey();

                    Set<K> keySet();

                    Collection<V> values();

                    Set<Map.Entry<K, V>> entrySet();
                    }
                    +

                    ListIterator(I)

                    public interface ListIterator<E> extends Iterator<E> {
                    boolean hasNext();
                    E next();
                    void remove();

                    //newly added as iterator below:
                    boolean hasPrevious();

                    E previous();

                    //返回后续调用 next 将返回的元素的索引。[应该就是当前元素索引]
                    int nextIndex();

                    int previousIndex();

                    //This call can be made only if neither remove nor add have been called after the last call to next or previous.
                    void set(E e);

                    //The element is inserted immediately before the element that would be returned by next, if any, and after the element that would be returned by previous, if any.
                    void add(E e);
                    }
                    -
                    -

                    A SortedMap extended with navigation methods returning the closest matches for given search targets.

                    -

                    The performance of ascending operations and views is likely to be faster than that of descending ones.

                    -

                    submap都多加了几个参数:inclusive or exclusive

                    -

                    其entry不支持setValue,只能通过map自身的put方法改变value。因为要求前者只是map的快照

                    +
                    +

                    Note that the remove and set(Object) methods are not defined in terms of the cursor position; they are defined to operate on the last element returned by a call to next or previous().

                    -

                    跟navigable set差不多的定义

                    -

                    代码:

                    public interface NavigableMap<K,V> extends SortedMap<K,V> {

                    Map.Entry<K,V> lowerEntry(K key);

                    K lowerKey(K key);

                    Map.Entry<K,V> floorEntry(K key);

                    K floorKey(K key);

                    Map.Entry<K,V> ceilingEntry(K key);

                    K ceilingKey(K key);

                    Map.Entry<K,V> higherEntry(K key);

                    K higherKey(K key);

                    Map.Entry<K,V> firstEntry();

                    Map.Entry<K,V> lastEntry();

                    Map.Entry<K,V> pollFirstEntry();

                    Map.Entry<K,V> pollLastEntry();

                    NavigableMap<K,V> descendingMap();

                    NavigableSet<K> navigableKeySet();

                    NavigableSet<K> descendingKeySet();

                    NavigableMap<K,V> subMap(K fromKey, boolean fromInclusive,
                    K toKey, boolean toInclusive);

                    NavigableMap<K,V> headMap(K toKey, boolean inclusive);

                    NavigableMap<K,V> tailMap(K fromKey, boolean inclusive);

                    SortedMap<K,V> subMap(K fromKey, K toKey);

                    SortedMap<K,V> headMap(K toKey);

                    SortedMap<K,V> tailMap(K fromKey);
                    }
                    +

                    Collection

                    Collection(I)

                    代码

                    /*
                    无直接实现类,一般用于需要使用多态传递参数的场合
                    其所有子类都必须有两个构造器: a void (no arguments) constructor, which creates an empty collection, and a constructor with a single argument of type Collection, which creates a new collection with the same elements as its argument. 这点语法上不会强制实现(因为接口不能强制构造方法),但其实是约定成俗的。

                    */
                    public interface Collection<E> extends Iterable<E> {

                    int size();

                    boolean isEmpty();

                    boolean contains(Object o);

                    Iterator<E> iterator();

                    //有关“safe”的问题讨论见下
                    Object[] toArray();

                    /*
                    Like the toArray() method, this method acts as bridge between array-based and
                    collection-based APIs. Further, this method allows precise control over the runtime
                    type of the output array, and may, under certain circumstances, be used to save
                    allocation costs.

                    Suppose x is a collection known to contain only strings. The following code can be
                    used to dump the collection into a newly allocated array of String:
                    String[] y = x.toArray(new String[0]);

                    说明还是有类型限制的
                    ArrayStoreException – if the runtime type of the specified array is not a supertype
                    of the runtime type of every element in this collection
                    */
                    <T> T[] toArray(T[] a);

                    /*
                    给集合增加一个元素。
                    If a collection refuses to add a particular element for any reason other than that it
                    already contains the element, it must throw an exception (rather than returning
                    false).
                    */
                    boolean add(E e);

                    boolean remove(Object o);

                    boolean containsAll(Collection<?> c);

                    //重复了怎么办
                    boolean addAll(Collection<? extends E> c);

                    //重复的会全弄走吗
                    boolean removeAll(Collection<?> c);

                    /*
                    删除此集合中满足给定谓词的所有元素。
                    在迭代期间或由谓词引发的错误或运行时异常将转发给调用者。
                    @return true if any elements were removed
                    */
                    default boolean removeIf(Predicate<? super E> filter) {
                    //非空filter
                    Objects.requireNonNull(filter);
                    boolean removed = false;
                    //迭代该集合
                    final Iterator<E> each = iterator();
                    while (each.hasNext()) {
                    if (filter.test(each.next())) {
                    each.remove();
                    removed = true;
                    }
                    }
                    return removed;
                    }

                    /*
                    把集合c中没有的元素全部移除
                    @return true if this collection changed as a result of the call
                    */
                    boolean retainAll(Collection<?> c);

                    void clear();

                    boolean equals(Object o);

                    /*
                    Any class that overrides the Object.equals method must also override the
                    Object.hashCode method.
                    c1.equals(c2) 相当于 c1.hashCode()==c2.hashCode().
                    */
                    int hashCode();

                    @Override
                    default Spliterator<E> spliterator() {
                    return Spliterators.spliterator(this, 0);
                    }

                    default Stream<E> stream() {
                    return StreamSupport.stream(spliterator(), false);
                    }

                    default Stream<E> parallelStream() {
                    return StreamSupport.stream(spliterator(), true);
                    }
                    }
                    -

                    TreeMap

                    -

                    NavigableMap的红黑树实现

                    -

                    key不允许空,空会抛出异常

                    -

                    Note that this implementation is not synchronized.

                    -

                    fail-fast

                    -

                    All Map.Entry pairs returned by methods in this class and its views represent snapshots of mappings at the time they were produced. They do not support the Entry.setValue method. (Note however that it is possible to change mappings in the associated map using put.)【navigable map的性质】

                    -
                    -

                    具体代码就不看了

                    -

                    对Collection和Map的总结

                      -
                    1. fail-fast

                      -

                      The iterators returned by all of this class’s “collection view methods” are fail-fast: if the map is structurally modified at any time after the iterator is created, in any way except through the iterator’s own remove method, the iterator will throw a ConcurrentModificationException. Thus, in the face of concurrent modification, the iterator fails quickly and cleanly, rather than risking arbitrary, non-deterministic behavior at an undetermined time in the future.

                      -

                      Note that the fail-fast behavior of an iterator cannot be guaranteed as it is, generally speaking, impossible to make any hard guarantees in the presence of unsynchronized concurrent modification. Fail-fast iterators throw ConcurrentModificationException on a best-effort basis. Therefore, it would be wrong to write a program that depended on this exception for its correctness: the fail-fast behavior of iterators should be used only to detect bugs.

                      -
                      -

                      都使用了modcount进行并发检查,都具有fail-fast的特点(关于此的详细解说,可见AbstractList第四点和List第二点),因而只允许在迭代中使用迭代器的remove方法进行结构性改变。【注意:对于LinkedHashMap中access order排序,get方法也是structural modification,因而也只能通过迭代器的next方法获取元素】

                      +

                      其中,

                        +
                      1. “Bags or multisets (unordered collections that may contain duplicate elements) should implement this interface directly.”

                        Set是不允许重复的元素集合的ADT,【ADT:抽象数据结构】

                        +

                        Bag是元素集合的ADT,允许重复.

                        +

                        通常,任何包含元素的东西都是Collection.

                        +

                        任何允许重复的集合都是Bag,否则就是Set.

                        +

                        通过索引访问元素的任何包都是List.

                        +

                        在最后一个之后附加新元素并且具有从头部(第一索引)移除元素的方法的Bag是Queue.

                        +

                        在最后一个之后附加新元素并且具有从尾部(最后一个索引)移除元素的方法的Bag是Stack.
                        ————————————————
                        原文链接:https://blog.csdn.net/weixin_34239718/article/details/114036886

                      2. -
                      3. not synchronized

                        上面介绍到的几个类,除了Vector外,都是线程不同步的。可以用此方式让其线程同步。

                        -
                        Map m = Collections.synchronizedMap(new LinkedHashMap(...));
                      4. -
                      5. 是否允许null

                        除了TreeSet、TreeMap、ArrayDeque之外,都是允许空(key/value)的

                        +
                      6. “destructive” methods 和”undestructive” methods

                        这回答里写得很清楚:What are destructive and non-destructive methods in java?

                      7. -
                      8. 是否有序

                        List都是插入序,HashSet无需,HashMap也无序(但其实算是有内部桶序的),LinkedHashMap有插入序和LRU序(依靠内部增加简单队列的消耗),TreeSet有序,TreeMap有序【这俩靠红黑树的遍历顺序(二叉搜索树嘛)】。

                        +
                      9. recursive traversal of the collection
                        +

                        Some collection operations which perform recursive traversal of the collection may fail with an exception for self-referential instances where the collection directly or indirectly contains itself.

                        +
                        +

                        Java 8 vs Java 7 Collection Interface: self-referential instance

                        +

                        这个的第二个回答【较长的那个】写得很棒,较短的那个似乎是错误的。

                        +

                        正如描述所说的,“directly or indirectly contains itself”,回答里那个例子正是因为“indirectly contains itself”。

                        +

                        不仅仅是集合,每个Object都可能出现这样的错误(因为都包含有toString)

                        +

                        但是注意一点:

                        +
                        ArrayList l1 = new ArrayList();
                        l1.add(l1);
                        System.out.println(l1.toString());
                        //输出:[(this Collection)]
                        + +

                        这段代码是正常的,是因为ArrayList里面toString的实现:

                        +
                        sb.append('[');
                        for (;;) {
                        E e = it.next();
                        //注意此句
                        sb.append(e == this ? "(this Collection)" : e);
                        if (! it.hasNext())
                        return sb.append(']').toString();
                        sb.append(',').append(' ');
                        }
                        + +

                        规避了这种风险。

                        +

                        下面的hashcode是不正常的,因为hashcode实现没有规避这种风险。

                      10. -
                      11. 实现的约定接口

                        都Cloneable,Serializable

                        -

                        ArrayList/Vector:RandomAccess

                        +
                      12. 关于toArray的讨论
                        For toArray() :
                        The returned array will be "safe" in that no references to it are maintained by this collection. (In other words, this method must allocate a new array even if this collection is backed by an array). The caller is thus free to modify the returned array.
                        + +
                        List<String> list = Arrays.asList("foo", "bar", "baz");
                        String[] array = list.toArray(new String[0]);
                        array[0] = "qux";
                        //修改数组不会修改列表
                        System.out.println(list.get(0)); // still "foo"
                        list.set(0,"haha");
                        //修改列表也跟修改数组无关了
                        System.out.println(list.get(0)+array[0]);
                        + +
                        ArrayList<Student> a = new ArrayList<>();
                        a.add(new Student("Sarah",17));
                        Student[] s = a.toArray(new Student[0]);
                        //换一个引用对象
                        s[0]=new Student("Lily",20);
                        System.out.println(a.get(0)==s[0]);//false
                        + +
                        ArrayList<Student> a = new ArrayList<>();
                        a.add(new Student("Sarah",17));
                        Student[] s = a.toArray(new Student[0]);
                        //修改引用对象
                        s[0].name = "Lily";
                        System.out.println(a.get(0).name);//Lily
                        + +

                        这也就说明,toArray()实际上是把list的元素复制一份弄成array,直接把值粘贴进去。

                        +

                        对于引用对象,list的元素实际上应该存的是对象在堆中的地址。所谓的“安全”指的是,修改array中的元素的值【也即对象地址】,也就是换一个气球牵,是不会影响原来list的元素的值的。

                        +

                        因而,对于样例1和2,我们其实给array的元素换了个气球牵,或者是把list换了个气球牵,相互对象不同,没什么影响。

                        +

                        对于样例3,我们修改了list和array共同指向的对象【就像C语言的指针那样】

                        +

                        以上参考自What does “Safe” mean in the Collections.toArray() JavaDoc?

                        +
                      13. +
                      14. 关于default关键字

                        java中default关键字

                        +

                        starkoverflow关于为什么要设立default的讨论:

                        +

                        What is the purpose of the default keyword in Java

                        +

                        Default methods were added to Java 8 primarily to support lambda expressions.

                      -]]> - - Java - - - - 链接、装载与运行库 - /2023/09/18/%E9%9D%99%E6%80%81%E9%93%BE%E6%8E%A5%E4%B8%8E%E5%8A%A8%E6%80%81%E9%93%BE%E6%8E%A5/ - -

                      此为《程序员的自我修养:链接、装载与库》(俞甲子,石凡,潘爱民)的看书总结。

                      +

                      AbstractCollection(A)

                      +

                      To implement an unmodifiable collection, the programmer needs only to extend this class and provide implementations for the iterator and size methods.
                      To implement a modifiable collection, the programmer must additionally[也要搞上面的] override this class’s add method (which otherwise throws an UnsupportedOperationException), and the iterator returned by the iterator method must additionally implement its remove method.

                      -

                      链接前与装载

                      链接前的编译阶段可以生成.o文件,.o文件是ELF文件,里面含有段表、符号表、bss段、common段等链接辅助段。

                      -

                      执行可执行文件时,首先要通过fork创建一个新的子进程,然后要通过exec为子进程制定可执行文件装载逻辑。在exec系统调用中,会进行elf文件的读取解析。它会解析elf header,根据其各种信息将程序copy到内存(进程的虚拟地址空间)中,后者也就是我们所研究的装载。

                      -

                      装载不同于“节(section),是以”“(segment)为单位。进程虚拟地址空间被分为很多个VMA,每个VMA都有不同的属性(如权限,可读可写可执行)。ELF可执行文件除去在链接/编译过程中被视为一个个连续的节之外,它还会在链接的时候根据每个节的属性不同重排节,把属性相同的节连续放在一起成为一个个段。链接的时候还会形成程序头表。对应关系:节——段表,段——程序头表

                      -

                      装载进内存时,是以段为单位,一个段就对应着一个VMA。

                      -

                      内核通过execve系统调用装载完ELF可执行文件以后就返回到用户空间,将控制权交给程序的入口。

                      -

                      对于不同链接形式的ELF可执行文件,这个程序的入口是有区别的。对于静态链接的可执行文件来说,程序的入口就是ELF文件头里面的e_entry指定的入口地址;对于动态链接的可执行文件来说,如果这时候把控制权交给e_entry指定的入口地址,那么肯定是不行的,因为可执行文件所依赖的共享库还没有被装载,也没有进行动态链接。所以对于动态链接的可执行文件,内核会分析它的动态链接器地址(在“.interp”段),将动态链接器映射至进程地址空间,然后把控制权交给动态链接器

                      -

                      ELF

                      编译器编译源代码后生成的文件叫做目标文件(Object文件),目标文件从结构上讲,它是已经编译后的可执行文件格式,只是还没有经过链接的过程,其中可能有些符号或有些地址还没有被调整。其实它本身就是按照可执行文件格式存储的,只是跟真正的可执行文件在结构上稍有不同。

                      -

                      Linux的可执行文件遵从ELF的结构模式。

                      -

                      ELF可以分为这几类:

                      -

                      image-20230913084511590

                      -

                      也即.o(可重定位文件)、.exe(无后缀)(可执行文件)、.so(动态链接库)、.a(静态链接库)、core dump。

                      -

                      Object文件的结构

                      目标文件中的内容至少有编译后的机器指令代码、数据。没错,除了这些内容以外,目标文件中还包括了链接时所须要的一些信息,比如符号表、调试信息、字符串等。一般目标文件将这些信息按不同的属性,以“节”(Section)的形式存储,有时候也叫“”(Segment)。

                      -

                      image-20230918100932220

                      -

                      符号修饰

                      众所周知,强大而又复杂的C++拥有类、继承、虚机制、重载、名称空间等这些特性,它们使得符号管理更为复杂。最简单的例子,两个相同名字的函数func(int)和func(double),尽管函数名相同,但是参数列表不同,这是C++里面函数重载的最简单的一种情况,那么编译器和链接器在链接过程中如何区分这两个函数呢?为了支持C++这些复杂的特性,⼈们发明了符号修饰(Name Decoration)或符号改编(Name Mangling)的机制。

                      -

                      image-20230913090407013

                      -

                      C++为了与C兼容,在符号的管理上,C++有一个用来声明或定义一个C的符号的“extern “C””关键字用法:

                      -
                      extern ”C” {
                      int func(int);
                      int var;
                      }
                      +
                      public abstract class AbstractCollection<E> implements Collection<E> {

                      //唯一的构造函数。 (用于子类构造函数的调用,通常是隐式的。)
                      protected AbstractCollection() {
                      }

                      // 查询操作
                      public abstract Iterator<E> iterator();
                      public abstract int size();
                      public boolean isEmpty() {
                      return size() == 0;
                      }

                      public boolean contains(Object o) {
                      Iterator<E> it = iterator();
                      if (o==null) {
                      while (it.hasNext())
                      if (it.next()==null)
                      return true;
                      } else {
                      while (it.hasNext())
                      if (o.equals(it.next()))
                      return true;
                      }
                      return false;
                      }


                      public Object[] toArray() {
                      // Estimate size of array; be prepared to see more or fewer elements
                      Object[] r = new Object[size()];
                      Iterator<E> it = iterator();
                      for (int i = 0; i < r.length; i++) {
                      if (! it.hasNext()) // fewer elements than expected
                      return Arrays.copyOf(r, i);
                      r[i] = it.next();
                      }
                      return it.hasNext() ? finishToArray(r, it) : r;
                      }


                      @SuppressWarnings("unchecked")
                      public <T> T[] toArray(T[] a) {
                      // Estimate size of array; be prepared to see more or fewer elements
                      int size = size();
                      //通过反射得到a同类型的数组实例
                      T[] r = a.length >= size ? a :
                      (T[])java.lang.reflect.Array
                      .newInstance(a.getClass().getComponentType(), size);
                      Iterator<E> it = iterator();

                      for (int i = 0; i < r.length; i++) {
                      if (! it.hasNext()) { // fewer elements than expected
                      if (a == r) {
                      r[i] = null; // null-terminate用null来标记数组结束。但如果数组里也有null该怎么办?从前面的contains来看也是有可能的
                      } else if (a.length < i) {
                      return Arrays.copyOf(r, i);
                      } else {
                      System.arraycopy(r, 0, a, 0, i);
                      if (a.length > i) {
                      a[i] = null;
                      }
                      }
                      return a;
                      }
                      r[i] = (T)it.next();
                      }
                      // more elements than expected
                      return it.hasNext() ? finishToArray(r, it) : r;
                      }

                      /*
                      为啥要-8?
                      Some VMs reserve some header words in an array. Attempts to allocate larger arrays may result in OutOfMemoryError: Requested array size exceeds VM limit
                      好像是因为要给8个字节保留作为数组的头标题。
                      */
                      private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;


                      //Reallocates the array being used within toArray when the iterator returned more elements than expected, and finishes filling it from the iterator.
                      @SuppressWarnings("unchecked")
                      private static <T> T[] finishToArray(T[] r, Iterator<?> it) {
                      int i = r.length;
                      while (it.hasNext()) {
                      int cap = r.length;
                      //说明此时需要扩容
                      if (i == cap) {
                      //每次扩当前大小的1/2
                      int newCap = cap + (cap >> 1) + 1;
                      // overflow-conscious code 溢出
                      if (newCap - MAX_ARRAY_SIZE > 0)
                      //cap+1 扩容后至少应该比原大小大1
                      newCap = hugeCapacity(cap + 1);
                      //申请一个船新数组空间
                      r = Arrays.copyOf(r, newCap);
                      }
                      r[i++] = (T)it.next();
                      }
                      // 如果过度扩容了就缩小回刚刚好
                      return (i == r.length) ? r : Arrays.copyOf(r, i);
                      }

                      private static int hugeCapacity(int minCapacity) {
                      //如果最小扩容也失败,说明要的东西太多了,救不了
                      if (minCapacity < 0) // overflow
                      throw new OutOfMemoryError
                      ("Required array size too large");
                      //否则
                      return (minCapacity > MAX_ARRAY_SIZE) ?
                      Integer.MAX_VALUE :
                      MAX_ARRAY_SIZE;
                      }

                      //修改操作

                      public boolean add(E e) {
                      throw new UnsupportedOperationException();
                      }

                      public boolean remove(Object o) {
                      Iterator<E> it = iterator();
                      if (o==null) {
                      while (it.hasNext()) {
                      if (it.next()==null) {
                      it.remove();
                      return true;
                      }
                      }
                      } else {
                      while (it.hasNext()) {
                      if (o.equals(it.next())) {
                      it.remove();
                      return true;
                      }
                      }
                      }
                      return false;
                      }

                      //批量操作
                      //O(n^2)
                      public boolean containsAll(Collection<?> c) {
                      for (Object e : c)
                      if (!contains(e))
                      return false;
                      return true;
                      }

                      public boolean addAll(Collection<? extends E> c) {
                      boolean modified = false;
                      for (E e : c)
                      if (add(e))
                      modified = true;
                      return modified;
                      }

                      public boolean removeAll(Collection<?> c) {
                      Objects.requireNonNull(c);
                      boolean modified = false;
                      Iterator<?> it = iterator();
                      while (it.hasNext()) {
                      if (c.contains(it.next())) {
                      it.remove();
                      modified = true;
                      }
                      }
                      return modified;
                      }

                      public boolean retainAll(Collection<?> c) {
                      Objects.requireNonNull(c);
                      boolean modified = false;
                      Iterator<E> it = iterator();
                      while (it.hasNext()) {
                      if (!c.contains(it.next())) {
                      it.remove();
                      modified = true;
                      }
                      }
                      return modified;
                      }

                      public void clear() {
                      Iterator<E> it = iterator();
                      while (it.hasNext()) {
                      it.next();
                      it.remove();
                      }
                      }

                      //字符串操作

                      public String toString() {
                      Iterator<E> it = iterator();
                      if (! it.hasNext())
                      return "[]";

                      StringBuilder sb = new StringBuilder();
                      sb.append('[');
                      for (;;) {
                      E e = it.next();
                      sb.append(e == this ? "(this Collection)" : e);
                      if (! it.hasNext())
                      return sb.append(']').toString();
                      sb.append(',').append(' ');
                      }
                      }
                      }
                      -

                      C++编译器会将在extern “C” 的大括号内部的代码当作C语言代码处理。所以很明显,上面的代码中,C++的名称修饰机制将不会起作用。

                      -

                      强符号和弱符号

                      多个目标文件中含有相同名字全局符号的定义,那么这些目标文件链接的时候将会出现符号重复定义的错误。这种符号的定义可以被称为强符号(Strong Symbol)。有些符号的定义可以被称为弱符号(Weak Symbol)。对于C/C++语言来说,编译器默认函数和初始化了的全局变量为强符号,未初始化的全局变量为弱符号。我们也可以通过GCC的__attribute__((weak))来定义任何一个强符号为弱符号。

                      -

                      image-20230913090719480

                      -

                      目前我们所看到的对外部目标文件的符号引用在目标文件被最终链接成可执行文件时,它们须要被正确决议,如果没有找到该符号的定义,链接器就会报符号未定义错误,这种被称为强引用(Strong Reference)。与之相对应还有一种弱引用(Weak Reference),在处理弱引用时,如果该符号有定义,则链接器将该符号的引用决议;如果该符号未被定义,则链接器对于该引用不报错。

                      -

                      链接器处理强引用和弱引用的过程几乎一样,只是对于未定义的弱引用,链接器不认为它是一个错误。一般对于未定义的弱引用,链接器默认其为0,或者是一个特殊的值,以便于程序代码能够识别。

                      -
                      __attribute__ ((weakref)) void foo();

                      int main()
                      {
                      if(foo) foo();
                      }
                      +

                      其中:

                      +
                        +
                      1. 在toArray方法中,为什么需要写这么奇怪的代码?

                        what’s the usage of the code in the implementation of AbstractCollection’s toArray Method

                        +
                        Yes, you're right, as the javadoc sais, this method is prepared to return correctlly even if the Collection has been modified in the mean time.【并发安全】 That's why the initial size is just a hint. The usage of the iterator also ensures avoidance from the "concurrent modification" exception.
                      2. +
                      +

                      Queue

                      Queue(I)

                      + -

                      这种弱符号和弱引用对于库来说十分有用,比如库中定义的弱符号可以被用户定义的强符号所覆盖,从而使得程序可以使用自定义版本的库函数。

                      -
                      -

                      这里很帅,有条件编译那味了

                      +

                      The Queue interface does not define the blocking queue methods, which are common in concurrent programming. These methods, which wait for elements to appear or for space to become available, are defined in the java.util.concurrent.BlockingQueue interface, which extends this interface.

                      +

                      Queue implementations generally do not define element-based versions of methods equals and hashCode【就是不会像之前的list一样遍历一遍通过单个元素的hashcode计算整体的hashcode】 but instead inherit the identity based versions from class Object【hashcode由对象决定】, because element-based equality is not always well-defined for queues with the same elements but different ordering properties.

                      +

                      Each of these methods exists in two forms: one throws an exception if the operation fails, the other returns a special value (either null or false, depending on the operation). 容量受限的队列推荐使用第二种form

                      -

                      在Linux程序的设计中,如果一个程序被设计成可以支持单线程或多线程的模式,就可以通过弱引用的方法来判断当前的程序是链接到了单线程的Glibc库还是多线程的Glibc库(是否在编译时有-lpthread选项),从而执行单线程版本的程序或多线程版本的程序。我们可以在程序中定义一个pthread_create函数的弱引用,然后程序在运行时动态判断是否链接到pthread库从而决定执行多线程版本还是单线程版本:

                      -
                      #include <stdio.h>
                      #include <pthread.h>

                      int pthread_create(pthread_t*, const pthread_attr_t*, void* (*)(void*),
                      void*) __attribute__ ((weak));

                      int main()
                      {
                      if(pthread_create) {
                      printf("This is multi-thread
                      version!\n");
                      // run the multi-thread version
                      // main_multi_thread()
                      } else {
                      printf("This is single-thread
                      version!\n");
                      // run the single-thread version
                      // main_single_thread()
                      }
                      }
                      +

                      代码:

                      public interface Queue<E> extends Collection<E> {
                      /*
                      The offer method inserts an element if possible, otherwise returning false.
                      不同于Collection的add方法,offer添加失败时不会抛出异常,而是直接return false
                      */
                      boolean add(E e);
                      boolean offer(E e);
                      /*
                      The remove() and poll() methods differ only in their behavior
                      when the queue is empty:
                      the remove() method throws an exception, while the poll() method returns null.
                      */
                      E remove();
                      E poll();
                      /*
                      The element() and peek() methods return,
                      but do not remove, the head of the queue.
                      */
                      E element();
                      E peek();
                      }
                      + +

                      Deque(I)

                      +

                      双端队列。

                      +

                      The name deque is short for “double ended queue” and is usually pronounced “deck”.

                      + + +

                      This interface provides two methods to remove interior elements, removeFirstOccurrence and removeLastOccurrence.

                      + + + + -
                      -

                      弱符号大概是说该变量可以被定义多次,最终链接时再进行决议;弱引用大概是说该变量(函数)可以不被定义。

                      -

                      静态链接

                      静态链接库(.a文件)本质上是一堆.o文件的集合。静态链接的基本过程:

                      -
                      test.c ——(compile)——>test.o——(link)——>test(ELF exe)

                      lib.a
                      +

                      代码:

                      public interface Deque<E> extends Queue<E> {

                      void addFirst(E e);

                      void addLast(E e);

                      boolean offerFirst(E e);

                      boolean offerLast(E e);

                      E removeFirst();

                      E removeLast();

                      E pollFirst();

                      E pollLast();

                      E getFirst();

                      E getLast();

                      E peekFirst();

                      E peekLast();

                      boolean removeFirstOccurrence(Object o);

                      boolean removeLastOccurrence(Object o);

                      // *** Queue methods ***

                      boolean add(E e);

                      boolean offer(E e);

                      E remove();

                      E poll();

                      E element();

                      E peek();

                      // *** Stack methods ***

                      void push(E e);

                      E pop();

                      // *** Collection methods ***

                      boolean remove(Object o);

                      boolean contains(Object o);

                      public int size();

                      Iterator<E> iterator();
                      //Returns an iterator over the elements in this deque in reverse sequential order.
                      Iterator<E> descendingIterator();

                      }
                      -

                      静态链接其实也就是分为两大步骤:

                      -
                        -
                      1. 空间与地址分配

                        -

                        将input file的各个段都连在一起,并且为符号分配虚拟地址

                        +

                        ArrayDeque

                        +

                        Resizable-array implementation of the Deque interface.

                        +

                        不允许空

                        +

                        Array deques have no capacity restrictions; they grow as necessary to support usage.

                        +

                        not thread-safe【相比于由vector实现的线程安全的Stack】

                        +

                        This class is likely to be faster than Stack when used as a stack, and faster than LinkedList when used as a queue.【6】

                        +

                        fail-fast

                        +
                        +

                        代码:

                        public class ArrayDeque<E> extends AbstractCollection<E>
                        implements Deque<E>, Cloneable, Serializable
                        {
                        //非private以让内部类能够访问到
                        /*
                        The capacity of the deque is the length of this array,
                        which is always a power of two. capacity只能是2的幂次
                        不能满【原理应该跟循环队列差不多,是怕头尾混淆】
                        但允许短暂的满之后马上扩容
                        所有不包含元素的数组单元为空
                        */
                        transient Object[] elements;

                        //The index of the element at the head of the deque
                        //(which is the element that would be removed by remove() or pop());
                        //or an arbitrary number equal to tail if the deque is empty.
                        //head在下标大的地方
                        transient int head;

                        //The index at which the 【next】 element would be added to the tail of the deque
                        //(via addLast(E), add(E), or push(E)).
                        //tail在下标小的地方
                        transient int tail;

                        private static final int MIN_INITIAL_CAPACITY = 8;

                        // ****** Array allocation and resizing utilities ******

                        private static int calculateSize(int numElements) {
                        int initialCapacity = MIN_INITIAL_CAPACITY;
                        // Find the best power of two to hold elements.
                        // Tests "<=" because arrays aren't kept full.
                        if (numElements >= initialCapacity) {
                        //原理类似HashMap.tableSizeFor
                        //这一通操作可以得到比cap大的,且离cap最近的2的幂次方数
                        initialCapacity = numElements;
                        initialCapacity |= (initialCapacity >>> 1);
                        initialCapacity |= (initialCapacity >>> 2);
                        initialCapacity |= (initialCapacity >>> 4);
                        initialCapacity |= (initialCapacity >>> 8);
                        initialCapacity |= (initialCapacity >>> 16);
                        initialCapacity++;

                        if (initialCapacity < 0) // Too many elements, must back off
                        initialCapacity >>>= 1;// Good luck allocating 2 ^ 30 elements
                        }
                        return initialCapacity;
                        }


                        private void allocateElements(int numElements) {
                        elements = new Object[calculateSize(numElements)];
                        }


                        private void doubleCapacity() {
                        assert head == tail;
                        int p = head;
                        int n = elements.length;
                        int r = n - p; // number of elements to the right of p
                        int newCapacity = n << 1;
                        if (newCapacity < 0)
                        throw new IllegalStateException("Sorry, deque too big");
                        Object[] a = new Object[newCapacity];
                        //以head==tail为分界线,右边那段移到开头,左边那段移到后面
                        System.arraycopy(elements, p, a, 0, r);
                        System.arraycopy(elements, 0, a, r, p);
                        elements = a;
                        head = 0;
                        tail = n;
                        }

                        private <T> T[] copyElements(T[] a) {
                        if (head < tail) {
                        System.arraycopy(elements, head, a, 0, size());
                        } else if (head > tail) {
                        int headPortionLen = elements.length - head;
                        System.arraycopy(elements, head, a, 0, headPortionLen);
                        System.arraycopy(elements, 0, a, headPortionLen, tail);
                        }
                        return a;
                        }

                        //1
                        public ArrayDeque() {
                        elements = new Object[16];
                        }

                        public ArrayDeque(int numElements) {
                        allocateElements(numElements);
                        }

                        public ArrayDeque(Collection<? extends E> c) {
                        allocateElements(c.size());
                        addAll(c);
                        }

                        // The main insertion and extraction methods are addFirst,
                        // addLast, pollFirst, pollLast. The other methods are defined in
                        // terms of these.就是说这几个最重要,别的方法都是这四个的附庸

                        public void addFirst(E e) {
                        if (e == null)
                        throw new NullPointerException();
                        //2
                        elements[head = (head - 1) & (elements.length - 1)] = e;
                        if (head == tail)
                        //队列满
                        doubleCapacity();
                        }

                        public void addLast(E e) {
                        if (e == null)
                        throw new NullPointerException();
                        elements[tail] = e;
                        if ( (tail = (tail + 1) & (elements.length - 1)) == head)
                        //队列满
                        doubleCapacity();
                        }

                        public boolean offerFirst(E e) {
                        addFirst(e);
                        return true;
                        }

                        public boolean offerLast(E e) {
                        addLast(e);
                        return true;
                        }

                        public E removeFirst() {
                        E x = pollFirst();
                        if (x == null)
                        throw new NoSuchElementException();
                        return x;
                        }

                        public E removeLast() {
                        E x = pollLast();
                        if (x == null)
                        throw new NoSuchElementException();
                        return x;
                        }

                        public E pollFirst() {
                        int h = head;
                        @SuppressWarnings("unchecked")
                        E result = (E) elements[h];
                        // Element is null if deque empty
                        if (result == null)
                        return null;
                        elements[h] = null; // Must null out slot
                        head = (h + 1) & (elements.length - 1);
                        return result;
                        }

                        public E pollLast() {
                        int t = (tail - 1) & (elements.length - 1);
                        @SuppressWarnings("unchecked")
                        E result = (E) elements[t];
                        if (result == null)
                        return null;
                        elements[t] = null;
                        tail = t;
                        return result;
                        }

                        public E getFirst() {
                        @SuppressWarnings("unchecked")
                        E result = (E) elements[head];
                        if (result == null)
                        throw new NoSuchElementException();
                        return result;
                        }

                        public E getLast() {
                        @SuppressWarnings("unchecked")
                        E result = (E) elements[(tail - 1) & (elements.length - 1)];
                        if (result == null)
                        throw new NoSuchElementException();
                        return result;
                        }

                        @SuppressWarnings("unchecked")
                        public E peekFirst() {
                        // elements[head] is null if deque empty
                        return (E) elements[head];
                        }

                        @SuppressWarnings("unchecked")
                        public E peekLast() {
                        return (E) elements[(tail - 1) & (elements.length - 1)];
                        }

                        public boolean removeFirstOccurrence(Object o) {
                        if (o == null)
                        return false;
                        //掩码
                        int mask = elements.length - 1;
                        int i = head;
                        Object x;
                        while ( (x = elements[i]) != null) {
                        if (o.equals(x)) {
                        delete(i);
                        return true;
                        }
                        //头->尾
                        i = (i + 1) & mask;
                        }
                        return false;
                        }

                        public boolean removeLastOccurrence(Object o) {
                        if (o == null)
                        return false;
                        int mask = elements.length - 1;
                        int i = (tail - 1) & mask;
                        Object x;
                        while ( (x = elements[i]) != null) {
                        if (o.equals(x)) {
                        delete(i);
                        return true;
                        }
                        //尾->头
                        i = (i - 1) & mask;
                        }
                        return false;
                        }

                        // *** Queue methods ***

                        public boolean add(E e) {
                        addLast(e);
                        return true;
                        }

                        public boolean offer(E e) {
                        return offerLast(e);
                        }

                        public E remove() {
                        return removeFirst();
                        }

                        public E poll() {
                        return pollFirst();
                        }

                        public E element() {
                        return getFirst();
                        }

                        public E peek() {
                        return peekFirst();
                        }

                        // *** Stack methods ***

                        public void push(E e) {
                        addFirst(e);
                        }

                        public E pop() {
                        return removeFirst();
                        }

                        //检查队列情况正常
                        private void checkInvariants() {
                        assert elements[tail] == null;
                        //如果成立,只能是队列空;不成立的话,不能有空元素
                        assert head == tail ? elements[head] == null :
                        (elements[head] != null &&
                        elements[(tail - 1) & (elements.length - 1)] != null);
                        assert elements[(head - 1) & (elements.length - 1)] == null;
                        }

                        //Returns: true if elements moved backwards而不是操作是否成功
                        private boolean delete(int i) {
                        checkInvariants();
                        final Object[] elements = this.elements;
                        final int mask = elements.length - 1;
                        final int h = head;
                        final int t = tail;
                        final int front = (i - h) & mask;
                        final int back = (t - i) & mask;

                        // Invariant: head <= i < tail mod circularity
                        if (front >= ((t - h) & mask))
                        throw new ConcurrentModificationException();

                        // Optimize for least element motion
                        if (front < back) {
                        if (h <= i) {
                        //把i覆盖掉了
                        System.arraycopy(elements, h, elements, h + 1, front);
                        } else { // Wrap around
                        System.arraycopy(elements, 0, elements, 1, i);
                        elements[0] = elements[mask];
                        System.arraycopy(elements, h, elements, h + 1, mask - h);
                        }
                        elements[h] = null;
                        head = (h + 1) & mask;
                        return false;
                        } else {
                        if (i < t) { // Copy the null tail as well
                        System.arraycopy(elements, i + 1, elements, i, back);
                        //注意没设置空,因为确实不用
                        tail = t - 1;
                        } else { // Wrap around
                        System.arraycopy(elements, i + 1, elements, i, mask - i);
                        elements[mask] = elements[0];
                        System.arraycopy(elements, 1, elements, 0, t);
                        tail = (t - 1) & mask;
                        }
                        return true;
                        }
                        }

                        // *** Collection Methods ***

                        public int size() {
                        return (tail - head) & (elements.length - 1);
                        }

                        public boolean isEmpty() {
                        return head == tail;
                        }

                        //The elements will be ordered from first (head) to last (tail).
                        //This is the same order that elements would be dequeued
                        //(via successive calls to remove or popped (via successive calls to pop).
                        public Iterator<E> iterator() {
                        return new DeqIterator();
                        }

                        public Iterator<E> descendingIterator() {
                        return new DescendingIterator();
                        }

                        private class DeqIterator implements Iterator<E> {

                        private int cursor = head;

                        //Tail recorded at construction (also in remove), to stop iterator[怪不得叫fence]
                        //and also to check for comodification[检查并发].
                        private int fence = tail;

                        private int lastRet = -1;

                        public boolean hasNext() {
                        return cursor != fence;
                        }

                        public E next() {
                        if (cursor == fence)
                        throw new NoSuchElementException();
                        @SuppressWarnings("unchecked")
                        E result = (E) elements[cursor];
                        // This check doesn't catch all possible comodifications,
                        // but does catch the ones that corrupt traversal【破坏遍历的】
                        //tail!=fence说明迭代时修改。
                        if (tail != fence || result == null)
                        throw new ConcurrentModificationException();
                        lastRet = cursor;
                        cursor = (cursor + 1) & (elements.length - 1);
                        return result;
                        }

                        public void remove() {
                        if (lastRet < 0)
                        throw new IllegalStateException();
                        //3
                        if (delete(lastRet)) { // if left-shifted, undo increment in next()
                        cursor = (cursor - 1) & (elements.length - 1);
                        //update
                        fence = tail;
                        }
                        lastRet = -1;
                        }

                        public void forEachRemaining(Consumer<? super E> action) {
                        Objects.requireNonNull(action);
                        Object[] a = elements;
                        int m = a.length - 1, f = fence, i = cursor;
                        //4执行完后直接迭代结束
                        cursor = f;
                        while (i != f) {
                        @SuppressWarnings("unchecked") E e = (E)a[i];
                        i = (i + 1) & m;
                        if (e == null)
                        throw new ConcurrentModificationException();
                        action.accept(e);
                        }
                        }
                        }

                        private class DescendingIterator implements Iterator<E> {

                        private int cursor = tail;
                        //终点为fence,此时终点为head
                        private int fence = head;
                        private int lastRet = -1;

                        public boolean hasNext() {
                        return cursor != fence;
                        }

                        public E next() {
                        if (cursor == fence)
                        throw new NoSuchElementException();
                        cursor = (cursor - 1) & (elements.length - 1);
                        @SuppressWarnings("unchecked")
                        E result = (E) elements[cursor];
                        if (head != fence || result == null)
                        throw new ConcurrentModificationException();
                        lastRet = cursor;
                        return result;
                        }

                        public void remove() {
                        if (lastRet < 0)
                        throw new IllegalStateException();
                        if (!delete(lastRet)) {
                        cursor = (cursor + 1) & (elements.length - 1);
                        fence = head;
                        }
                        lastRet = -1;
                        }
                        //不支持for-each了吧233
                        }

                        public boolean contains(Object o) {
                        if (o == null)
                        return false;
                        int mask = elements.length - 1;
                        int i = head;
                        Object x;
                        while ( (x = elements[i]) != null) {
                        if (o.equals(x))
                        return true;
                        i = (i + 1) & mask;
                        }
                        return false;
                        }

                        public boolean remove(Object o) {
                        return removeFirstOccurrence(o);
                        }

                        public void clear() {
                        int h = head;
                        int t = tail;
                        if (h != t) { // clear all cells
                        head = tail = 0;
                        int i = h;
                        int mask = elements.length - 1;
                        do {
                        elements[i] = null;
                        i = (i + 1) & mask;
                        } while (i != t);
                        }
                        }

                        public Object[] toArray() {
                        return copyElements(new Object[size()]);
                        }

                        @SuppressWarnings("unchecked")
                        public <T> T[] toArray(T[] a) {
                        int size = size();
                        if (a.length < size)
                        a = (T[])java.lang.reflect.Array.newInstance(
                        a.getClass().getComponentType(), size);
                        copyElements(a);
                        if (a.length > size)
                        a[size] = null;
                        return a;
                        }

                        // *** Object methods ***

                        public ArrayDeque<E> clone() {
                        try {
                        @SuppressWarnings("unchecked")
                        ArrayDeque<E> result = (ArrayDeque<E>) super.clone();
                        result.elements = Arrays.copyOf(elements, elements.length);
                        return result;
                        } catch (CloneNotSupportedException e) {
                        throw new AssertionError();
                        }
                        }

                        private static final long serialVersionUID = 2340985798034038923L;

                        private void writeObject(java.io.ObjectOutputStream s)
                        throws java.io.IOException {...}

                        private void readObject(java.io.ObjectInputStream s)
                        throws java.io.IOException, ClassNotFoundException {...}

                        public Spliterator<E> spliterator() {
                        return new DeqSpliterator<E>(this, -1, -1);
                        }

                        static final class DeqSpliterator<E> implements Spliterator<E> {...}

                        }
                        + +

                        其中:

                          +
                        1. 默认容量

                          空构造器的默认容量为16

                        2. -
                        3. 符号解析与重定位

                          -
                            -
                          1. 扫描所有输入文件的符号表形成全局符号表;

                            +
                          2. (head - 1) & (elements.length - 1)

                            是一个便捷的截位取余操作,这跟hashmap一个原理,详见hashmap第二点。

                            +
                          3. +
                          4. if (delete(lastRet))

                            delete方法返回true,说明右移数组,此时next指针需要++

                            +

                            delete方法返回false,说明左移数组,此时next指针不变

                          5. -
                          6. 重定向

                            -

                            可执行文件基本可以确定自己在进程虚拟空间中的起始位置,因为可执行文件往往是第一个被加载的文件,它可以选择一个固定空闲的地址。因而,在link中,可执行文件的地址都以确定,就可以开始进行重定向。

                            -

                            通过重定向表对所有UNDEF的符号进行地址修正,包括相对地址修正和绝对地址修正。

                            +
                          7. forEachRemaining

                            跟差不多所有的迭代器实现一样,此方法执行完毕之后,cursor直接跳到数组最末,相当于迭代结束

                          +

                          List

                          List(I)

                          有序、支持随机访问

                          +

                          代码

                          /*
                          List 接口提供了一个特殊的迭代器,称为 ListIterator,
                          除了 Iterator 接口提供的正常操作之外,它还允许元素插入和替换以及双向访问。
                          List 接口提供了两种方法来搜索指定的对象。 从性能的角度来看,应谨慎使用这些方法。
                          在许多实现中,它们将执行代价高昂的线性搜索。
                          Note: While it is permissible for lists to contain themselves as elements, extreme caution is advised: the equals and hashCode methods are no longer well defined on such a list.【这跟上面的引用点4是一样的。】

                          */
                          public interface List<E> extends Collection<E> {
                          int size();
                          boolean isEmpty();
                          boolean contains(Object o);
                          Iterator<E> iterator();
                          Object[] toArray();
                          <T> T[] toArray(T[] a);
                          boolean add(E e);
                          boolean remove(Object o);
                          boolean containsAll(Collection<?> c);
                          boolean addAll(Collection<? extends E> c);
                          boolean removeAll(Collection<?> c);
                          boolean retainAll(Collection<?> c);
                          //Returns true if and only if the specified object is also a list,
                          //both lists have the same size, and all corresponding pairs of elements in the two lists are equal.
                          //In other words, two lists are defined to be equal if they contain the same elements in the same order.
                          //注意,对于未重载equal方法的类,引用对象的相等指的是地址相等也就是说必须是一模一样的对象,两个不同对象但值相同,这种情况是不算equal的。
                          boolean equals(Object o);
                          int hashCode();

                          //newly add or change below

                          //将此列表的每个元素替换为将运算符应用于该元素的结果。
                          default void replaceAll(UnaryOperator<E> operator) {
                          Objects.requireNonNull(operator);
                          final ListIterator<E> li = this.listIterator();
                          while (li.hasNext()) {
                          li.set(operator.apply(li.next()));
                          }
                          }

                          //@SuppressWarings注解 作用:用于抑制编译器产生警告信息。
                          @SuppressWarnings({"unchecked", "rawtypes"})
                          default void sort(Comparator<? super E> c) {
                          Object[] a = this.toArray();
                          //借助Arrays的sort方法
                          Arrays.sort(a, (Comparator) c);
                          ListIterator<E> i = this.listIterator();
                          //再线性逐一替换
                          for (Object e : a) {
                          i.next();
                          // set:
                          // Replaces the last element returned by next or previous
                          // with the specified element
                          i.set((E) e);
                          }
                          }

                          E get(int index);

                          E set(int index, E element);

                          //插入元素,把当前位及其以后的元素都往后挪一位
                          void add(int index, E element);

                          //移除元素,把当前位及其以后的元素都往前挪一位
                          E remove(int index);

                          //-1 if this list does not contain the element
                          //ClassCastException:if the type of the specified element
                          //is incompatible with this list
                          int indexOf(Object o);

                          ListIterator<E> listIterator();

                          //指定的索引指示初始调用next将返回的第一个元素.对 previous 的初始调用将返回具有指定索引减一的元素。
                          ListIterator<E> listIterator(int index);

                          //[fromIndex,toIndex).If fromIndex==toIndex then return==null.
                          List<E> subList(int fromIndex, int toIndex);

                          @Override
                          default Spliterator<E> spliterator() {
                          return Spliterators.spliterator(this, Spliterator.ORDERED);
                          }

                          }
                          + +

                          其中:

                            +
                          1. hashcode不允许自环

                            Note: While it is permissible for lists to contain themselves as elements, extreme caution is advised: the equals and hashCode methods are no longer well defined on such a list.【这跟上面的引用点4是差不多的。】

                            +
                            ArrayList l1 = new ArrayList();
                            l1.add(l1);
                            System.out.println(l1.hashCode());
                            //输出:Stack Overflow
                          2. +
                          3. structural and non-structural change in list

                            Difference between structural and non-structural for lists

                          4. -
                          -

                          在link中,会读取test.o以及lib.a中的符号表,完成重定向(绝对地址和相对地址)以及节的重排组织,最终组合形成以段为单位的可执行文件test

                          -

                          可执行文件test会通过系统调用exevec被装载进物理内存(lazy allocation),分段映射到进程的虚拟地址空间。

                          -

                          静态链接的缺陷是,由于重定向在link过程完成,故而同一份共享库在物理内存中会有多份copy,极大占用物理内存和磁盘空间。优点是速度快。

                          -

                          动态链接

                          (下文注意区分两个概念:可执行文件和动态链接库)

                          -

                          动态链接库(.so)不同于静态链接库。

                          -
                          test.c ——(compile)——>test.o——(link)——>test(ELF exe)

                          lib.so
                          +
                        4. 关于sublist
                          ① view

                          sublist的document有一个很有趣的点,就是把sublist称为原list的视图view,这不禁让人想起了数据库里的表和表的视图。

                          +

                          但是还是有差别的。

                          +

                          在数据库中,视图仅仅是表或表的一部分的快照,修改视图对原表没有影响。但此处,对sublist的结构性修改和非结构性修改都会使原list的对应元素发生改变。

                          +

                          但有一点是相同的。如果对原表/原list修改,那么视图就会没用/会寄掉。

                          +

                          可以像这样来对主list指定范围内的元素进行操作,免去复杂的下标。

                          +
                          //For example, the following idiom removes a range of elements from a list:
                          list.subList(from, to).clear();
                          -

                          在link中,仅会读入动态链接库的符号表,对于动态链接库的符号仅会将其标记为动态符号,而不会对其进行重定向。

                          -

                          可执行文件test会通过系统调用exevec被装载进物理内存(lazy allocation),分段映射到进程的虚拟地址空间。

                          -

                          静态链接是per-process一份库,内存中有多份库;动态链接是per-process一份库,内存也只有一份库。并且虚拟地址动态分配,也即库映射到进程地址空间的哪块VMA是不确定的。

                          -

                          由于动态链接库被装载时的虚拟地址不确定,所以对于动态链接库和可执行文件代码中与动态链接相关的绝对地址,不能简单采用装载时重定向的方法来对其重定向,否则会破坏其共享性和不变性。

                          -
                          -

                          试想一下,每个进程加载的动态链接库的地址都不同,那岂不是每个进程的动态链接库的重定向结果都不一样,指令都不一样,不就寄了。

                          -
                          -

                          所以我们此时进行了一个牛逼到家、惊天动地、无人能比的操作。

                          -

                          我们可以分离.text和.data,前者作为“共享”语义保持不变性,后者则在每个进程地址空间中都留存一个copy。然后,我们将所有立即数绝对寻址的地方,换为间接寻址!也即,把那个立即数绝对地址改成一个变量,变量值在.data段中存储。这样一来,就成功把绝对寻址替换成了相对寻址。加载的时候也只需将虚拟地址填进可变的.data就行。不得不说真是十分地巧妙。

                          -

                          这个从相对地址——绝对地址的转换过程,由ELF中的一个新段GOT表(.got)来实现。在link时加入了动态链接库符号表的可执行文件,以及动态链接库本身,都使用了.got段,以PIC形式出现。

                          -

                          这个操作就是所谓的“地址无关代码”,通过-fPIC选项,就可以将代码编译为一个地址无关的程序。使用PIC模式编译的共享对象,对于模块内部的函数调用也是采用跟模块外部函数调用一样的方式,即使用GOT/PLT的方式。

                          -
                          -

                          小trick:如何区分一个DSO是否为PIC

                          -
                          readelf -d foo.so | grep TEXTREL
                          +
                          ② 留下一个问题
                          public class Main {
                          public static void main(String[] args) {
                          ArrayList<Student> a=new ArrayList<>();
                          a.add(new Student("Lily",16));
                          a.add(new Student("Sam",16));
                          a.add(new Student("Tom",16));
                          a.add(new Student("Mary",16));
                          a.add(new Student("Mark",16));
                          a.add(new Student("John",16));

                          List<Student> suba=a.subList(2,5);

                          System.out.println("Firstly,suba:");
                          printall(suba);

                          System.out.println("Change the value of age:");
                          suba.get(1).age=300;
                          System.out.println("suba:");
                          printall(suba);
                          System.out.println("a:");
                          printall(a);

                          System.out.println("Change the address:");
                          suba.set(1,new Student("haha",200));
                          System.out.println("suba:");
                          printall(suba);
                          System.out.println("a:");
                          printall(a);

                          System.out.println("Change the structure of sublist:");
                          suba.remove(0);
                          System.out.println("suba:");
                          printall(suba);
                          System.out.println("a:");
                          printall(a);
                          }
                          public static void printall(List<Student> list){
                          for (Student a : list){
                          System.out.print(a+"\t");
                          }
                          System.out.println();
                          }
                          }
                          /*运行结果
                          Firstly,suba:
                          name :Tomage :16 name :Maryage :16 name :Markage :16
                          Change the value of age:
                          suba:
                          name :Tomage :16 name :Maryage :300 name :Markage :16
                          a:
                          name :Lilyage :16 name :Samage :16 name :Tomage :16 name :Maryage :300 name :Markage :16 name :Johnage :16
                          Change the address:
                          suba:
                          name :Tomage :16 name :hahaage :200 name :Markage :16
                          a:
                          name :Lilyage :16 name :Samage :16 name :Tomage :16 name :hahaage :200 name :Markage :16 name :Johnage :16
                          Change the structure of sublist:
                          suba:
                          name :hahaage :200 name :Markage :16
                          a:
                          name :Lilyage :16 name :Samage :16 name :hahaage :200 name :Markage :16 name :Johnage :16
                          */
                          -

                          如果上面的命令有任何输出,那么foo.so就不是PIC的,否则就是PIC的。PIC的DSO是不会包含任何代码段重定位表的,TEXTREL表⽰代码段重定位表地址。

                          -

                          这也很好理解,因为PIC本质上就是把代码段重定位转化为了数据段重定位

                          -
                          -

                          除了动态链接库中的寻址(对变量和函数)需要使用PIC之外,对可执行文件的全局变量也需要使用特殊的机制。ELF共享库中的全局变量都类似以弱引用形式存在。当全局变量在主程序extern时,若该变量在共享库中初始化了,那么加载之后要把共享库的数据copy进主程序;否则,该变量值都以主模块为准。

                          -
                          -

                          这段原因解释看书真没懂,详情340页开始。

                          -

                          不过感觉它可能说的有点问题,我个人认为全局变量需要使用这种以方式存在,是为了保证进程资源独立。如果变量都以共享库中的数据值为准,那各个进程共享共享库不就乱了。。。你改一下我改一下

                          -
                          -

                          因而,总的装载流程是:

                          -

                          未优化情况下,在可执行文件被装载之前,先将其依赖的所有动态链接库加载进内存。若其所需的动态链接库已经被映射到物理内存,则将其装载到进程虚拟地址空间;否则,则映射到物理内存,并且装载到进程虚拟地址空间。然后,在装载动态链接库后,扫描可执行文件.got段符号进行装载时重定向(依据已经装载了的动态链接库虚拟地址来计算符号地址)即可。

                          -

                          但可以注意到这一步还是有优化空间。所以我们采取延迟绑定(PLT)的方法,第一次访问到动态链接库符号时,才对其进行重定向并填入.got中。

                          -

                          动态链接的缺点就是太慢了,一是因为PIC导致模块内部函数和全局变量也需要以.got形式访问,加了层寻址;二是运行时重定向开销巨大。对于前者,模块内部函数可以使用static关键字修饰;对于后者,采用PLT。

                          -

                          image-20230913231031432

                          -

                          显式运行时链接

                          支持动态链接的系统往往都支持一种更加灵活的模块加载方式,叫做显式运行时链接(Explicit Runtime Linking),有时候也叫做运行时加载。也就是让程序自己在运行时控制加载指定的模块,并且可以在不需要该模块时将其卸载。

                          +

                          为什么之前的toArray得到的array换了个对象,原list不会变【详见Collection说明第五点】;而这里的sublist得到的子list换了个对象,原list也会变呢?

                          +

                          也许这跟间接寻址的级数有关?之后看过ArrayList的具体实现再来解答。

                          -

                          也就是说,前面介绍的动态链接库是由动态链接器自动完成的,程序啥也不知道;这里的动态装载库是程序自己控制的,所以会提供给程序各种API。

                          +

                          穿越回来:

                          +

                          ​ toArray得到array,是新开辟了存储空间,里面存放了原list对象的地址。因而,修改新存储空间的地址内容,原list是不变的。

                          +

                          ​ 但sublist连新开辟存储空间也没有,其差不多所有操作都是从list进行的。因而它换了个对象,具体实现就是让原list也换了个对象。

                          +

                          ​ 所以说其实前者更像是数据库里面的“视图”概念。

                          -

                          而动态库的装载则是通过一系列由动态链接器提供的API,具体地讲共有4个函数:打开动态库(dlopen)、查找符号(dlsym)、错误处理(dlerror)以及关闭动态库(dlclose),程序可以通过这几个API对动态库进行操作。这几个API的实现是在/lib/libdl.so.2里面,它们的声明和相关常量被定义在系统标准头文件<dlfcn.h>

                          +
                          ③ 关于sublis和原list、non-structral和structural的区别:

                          Can structural changes in sublist reflected in the original list, in JAVA?

                          -

                          很有意思的是,如果我们将filename这个参数设置为0,那么dlopen返回的将是全局符号表的句柄,也就是说我们可以在运行时找到全局符号表里面的任何一个符号,并且可以执行它们,这有些类似高级语言反射(Reflection)的特性。全局符号表包括了程序的可执行文件本身、被动态链接器加载到进程中的所有共享模块以及在运行时通过dlopen打开并且使用了RTLD_GLOBAL方式的模块中的符号。

                          -
                          -

                          它接下来举的例子很有意思,可惜不知道为啥在我这一直segment fault。。。好像是它用的内联汇编是32位什么的,我折腾了半天还是没办法,算了

                          -
                          /*
                          我们这个例子中将实现一个更为灵活的叫做runso的程序,这个程序可以通过命令行来执行共享对象里面的任意一个函数。
                          它在理论上很简单,基本的步骤就是:由命令行给出共享对象路径、函数名和相关参数,然后程序通过运行时加载将该模块加载
                          到进程中,查找相应的函数,并且执行它,然后将执行结果打印出来。
                          为了表示参数和返回值类型,我们假设字母d表示double、i表示int、s表示char*、v表示void
                          比如说,如果要调用/lib/libfoo.so里面一个void bar(char* str, int i)的函数,可以使用如下命令行:
                          $./RunSo /lib/libfoo.so bar sHello i10
                          */

                          #include <stdio.h>
                          #include <dlfcn.h>
                          #include <stdint.h>

                          // 大概就是根据参数类型把参数压入栈
                          #define SETUP_STACK \
                          i = 2; \
                          while (++i < argc - 1) { \
                          switch(argv[i][0]) { \
                          case 'i': \
                          int res = atoi(&argv[i][1]); \
                          asm volatile(".code32\n" \
                          "push %0" :: \
                          "r"(res )); \
                          asm volatile(".code64\n"); \
                          esp += 4; \
                          break; \
                          case 'd': \
                          atof(&argv[i][1]); \
                          asm volatile("subl $8,%esp\n" \
                          "fstpl (%esp)" ); \
                          esp += 8; \
                          break; \
                          case 's': \
                          asm volatile("push %0" :: \
                          "r"(&argv[i][1]) ); \
                          esp += 4; \
                          break; \
                          default: \
                          printf("error argument type"); \
                          goto exit_runso; \
                          } \
                          }

                          // 大概就是相当于pop,给esp加上我们之前申请的栈空间esp
                          #define RESTORE_STACK \
                          asm volatile("add %0,%%esp"::"r"(esp))

                          int main(int argc, char* argv[])
                          {
                          void* handle;
                          char* error;
                          int i;
                          int esp = 0;
                          void* func;

                          handle = dlopen(argv[1], RTLD_NOW);
                          if(handle == 0) {
                          printf("Can't find library: %s\n", argv[1]);
                          return -1;
                          }

                          func = dlsym(handle, argv[2]);
                          if( (error = dlerror()) != NULL ) {
                          printf("Find symbol %s error: %s\n", argv[2], error);
                          goto exit_runso;
                          }

                          // 根据返回值不同构造函数指针
                          switch(argv[argc-1][0]){
                          case 'i':
                          {
                          int (*func_int)() = func;
                          SETUP_STACK;
                          int ret = func_int();
                          RESTORE_STACK;
                          printf("ret = %d\n", ret );
                          break;
                          }
                          case 'd':
                          {
                          double (*func_double)() = func;
                          SETUP_STACK;
                          double ret = func_double();
                          RESTORE_STACK;
                          printf("ret = %f\n", ret );
                          break;
                          }
                          case 's':
                          {
                          char* (*func_str)() = func;
                          SETUP_STACK;
                          char* ret = func_str();
                          RESTORE_STACK;
                          printf("ret = %s\n", ret );
                          break;
                          }
                          case 'v':
                          {
                          void (*func_void)() = func;
                          SETUP_STACK;
                          func_void();
                          RESTORE_STACK;
                          printf("ret = void");
                          break;
                          }
                          } // end of switch

                          exit_runso:
                          dlclose(handle);
                          }
                          +

                          In the document of List class:

                          +
                          /*
                          The returned list is backed by this list, so non-structural changes in the returned list are reflected in this list, and vice-versa.
                          */
                          List<E> subList(int fromIndex, int toIndex);
                          +

                          Can structural changes in sublist reflected in the original list in JAVA?

                          +
                      +

                      答案是会改变的。实例可看问题中代码,或者point3的代码。

                      +

                      但是既然都会变的话,为什么文档里面要特意强调“non-structural”呢?这是因为,对sublist进行结构性改变会让原list也正常地一起变,但是对原list进行结构性改变却会让sublist寄掉:

                      +

                      文档下面接着写道:

                      +
                      The semantics of the list returned by this method become undefined if the backing list (i.e., this list) is structurally modified in any way other than via the returned list. (Structural modifications are those that change the size of this list, or otherwise perturb it in such a fashion that iterations in progress may yield incorrect results.)
                      /*
                      sublist会寄,如果其原list被结构性改变了,且是以除了通过sublist结构性改变的方式外的其他方式改变的。(结构性改变是指那些会改变list的长度,或者会使迭代失败的那些改变)
                      */
                      +

                      通过代码验证可得,确实寄了

                      +
                      public static void main(String[] args) {
                      ArrayList<Student> a=new ArrayList<>();
                      a.add(new Student("Lily",16));
                      a.add(new Student("Sam",16));
                      a.add(new Student("Tom",16));
                      a.add(new Student("Mary",16));
                      a.add(new Student("Mark",16));
                      a.add(new Student("John",16));

                      List<Student> suba=a.subList(2,5);

                      System.out.println("Firstly,suba:");
                      printall(suba);
                      printall(a);

                      System.out.println("Change the origin structure:");
                      a.remove(3);
                      System.out.println("a:");
                      printall(a);
                      System.out.println("suba:");
                      printall(suba);
                      }
                      /*运行结果
                      Firstly,suba:
                      name :Tomage :16 name :Maryage :16 name :Markage :16
                      name :Lilyage :16 name :Samage :16 name :Tomage :16 name :Maryage :16 name :Markage :16 name :Johnage :16
                      Change the origin structure:
                      a:
                      name :Lilyage :16 name :Samage :16 name :Tomage :16 name :Markage :16 name :Johnage :16
                      suba:
                      Exception in thread "main" java.util.ConcurrentModificationException
                      at java.util.ArrayList$SubList.checkForComodification(ArrayList.java:1241)
                      */
                      -

                      运行库

                      初始化

                      操作系统装载程序之后,首先运行的代码并不是main的第一行,而是某些别的代码,这些代码负责准备好main函数执行所需要的环境,并且负责调用main函数,这时候你才可以在main函数里放心大胆地写各种代码:申请内存、使用系统调用、触发异常、访问I/O。在main返回之后,它会记录main函数的返回值,调用atexit注册的函数,然后结束进程。

                      -

                      运行这些代码的函数称为入口函数或入口点(Entry Point),视平台的不同而有不同的名字。程序的入口点实际上是一个程序的初始化和结束部分,它往往是运行库的一部分

                      -

                      一个典型的程序运行步骤大致如下:

                      +

                      通过以上可以感觉,估计sublist和原list的元素是共享存储空间的,只不过可能对象里有相关维护的变量。Any method accessing the list through the sub list effectively does index + offset.故而要是list变了,sublist的相关维护变量不变,就会依然傻傻地进行offset+index操作,这样就会寄。此猜想有待验证23333

                      +
                      +

                      穿越回来:

                        -
                      1. 操作系统在创建进程后,把控制权交到了程序的入口,这个入口往往是运行库中的某个入口函数。

                        -
                      2. -
                      3. 入口函数对运行库和程序运行环境进行初始化,包括堆、I/O、线程、全局变量构造,等等。

                        -
                      4. -
                      5. 入口函数在完成初始化之后,调用main函数,正式开始执行程序主体部分。

                        -
                      6. -
                      7. main函数执行完毕以后,返回到入口函数,入口函数进行清理⼯作,包括全局变量析构、堆销毁、关闭I/O等,然后进行系统调用结束进程。

                        -
                      8. +
                      9. 确实是共享存储空间的,不如说sublist就直接引用了原list的变量,所有操作实质上都是在原list上进行。
                      10. +
                      11. sublist傻傻地进行offset+index操作,这样体现在原list上,可能导致下标越界或者结果并非我们想要的。
                      -

                      Linux中的C语言运行库就是glibc

                      -

                      运行库

                      运行时库(Runtime Library)为入口函数及其所依赖的函数所构成的函数、各种标准库函数的实现的集合。可以通过sudo apt-get install glibc-source安装glibc的源代码。

                      -

                      一个C语言运行库大致包含了如下功能:

                      -
                        -
                      1. 启动与退出:包括入口函数及入口函数所依赖的其他函数等。

                        -
                      2. -
                      3. 标准函数:由C语言标准规定的C语言标准库所拥有的函数实现。

                        -
                      4. -
                      5. I/O:I/O功能的封装和实现,参见上一节中I/O初始化部分。

                        -

                        应该指的是比如说提供File*指针、IO stream之类的高级功能封装。

                        -
                      6. -
                      7. 堆:堆的封装和实现,参见上一节中堆初始化部分。

                        -
                        -

                        这点让我耳目一新!因为我以前一直以为堆栈都是操作系统实现的,现在想来才发现确实,操作系统只负责通过sbrk系统调用给内存,具体的堆分配算法由glibc的malloc实现。

                      8. -
                      9. 语言实现:语言中一些特殊功能的实现。

                        -
                      10. -
                      11. 调试:实现调试功能的代码。

                        -
                      -

                      库函数介绍

                      它这里主要讲了两个比较特殊的库,还挺有意思的:变长参数(stdarg.h)和非局部跳转(setjmp.h)。

                      -
                        -
                      1. 变长参数

                        -

                        讲这玩意其实用作是printf的实现。看下下面这两个代码相信你就能明白printf的基本原理了:

                        -
                        // Code 1
                        int sum(unsigned num, ...)
                        {
                        int* p = &num + 1;
                        int ret = 0;
                        while (num--)
                        ret += *p++;
                        return ret;
                        }

                        call:
                        int n = sum(3, 16, 38, 53);
                        +

                        AbstractList(A)

                        +

                        提供随机访问list的基本骨架

                        +

                        To implement an unmodifiable list, the programmer needs only to extend this class and provide implementations for the get(int) and size() methods.

                        +

                        To implement a modifiable list, the programmer must additionally override the set(int, Object) method

                        +

                        If the list is variable-size,the programmer must additionally override the add(int, E) and remove(int) methods.

                        +

                        Unlike the other abstract collection implementations, the programmer does not have to provide an iterator implementation; the iterator and list iterator are implemented by this class, on top of the “random access“ methods: get(int), set(int, E), add(int, E) and remove(int).这里的迭代器反而是通过类里面的方法来实现的

                        +
                        +

                        代码

                        public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E> {

                        protected AbstractList() {
                        }
                        //重写add方法
                        public boolean add(E e) {
                        //此为下面的public void add(int index, E element);
                        add(size(), e);
                        return true;
                        }
                        //交给具体实现
                        abstract public E get(int index);
                        //假设不能修改
                        public E set(int index, E element) {
                        throw new UnsupportedOperationException();
                        }
                        public void add(int index, E element) {
                        throw new UnsupportedOperationException();
                        }
                        public E remove(int index) {
                        throw new UnsupportedOperationException();
                        }

                        // Search Operations

                        public int indexOf(Object o) {
                        ListIterator<E> it = listIterator();
                        if (o==null) {
                        while (it.hasNext())
                        if (it.next()==null)
                        return it.previousIndex();
                        } else {
                        while (it.hasNext())
                        if (o.equals(it.next()))
                        return it.previousIndex();
                        }
                        return -1;
                        }

                        public int lastIndexOf(Object o) {
                        //从后往前遍历
                        ListIterator<E> it = listIterator(size());
                        if (o==null) {
                        while (it.hasPrevious())
                        if (it.previous()==null)
                        return it.nextIndex();
                        } else {
                        while (it.hasPrevious())
                        if (o.equals(it.previous()))
                        return it.nextIndex();
                        }
                        return -1;
                        }

                        // Bulk Operations

                        public void clear() {
                        removeRange(0, size());
                        }

                        public boolean addAll(int index, Collection<? extends E> c) {
                        rangeCheckForAdd(index);
                        boolean modified = false;
                        for (E e : c) {
                        add(index++, e);
                        modified = true;
                        }
                        return modified;
                        }

                        // Iterators

                        public Iterator<E> iterator() {
                        return new Itr();
                        }


                        public ListIterator<E> listIterator() {
                        return listIterator(0);
                        }


                        public ListIterator<E> listIterator(final int index) {
                        rangeCheckForAdd(index);

                        return new ListItr(index);
                        }

                        //内部类诶 1
                        private class Itr implements Iterator<E> {
                        //Index of element to be returned by subsequent call to next.
                        int cursor = 0;
                        //Index of element returned by most recent call to next or previous. Reset to -1 if this element is deleted by a call to remove.
                        int lastRet = -1;

                        int expectedModCount = modCount;

                        public boolean hasNext() {
                        return cursor != size();
                        }

                        public E next() {
                        checkForComodification();
                        try {
                        int i = cursor;
                        E next = get(i);
                        lastRet = i;
                        cursor = i + 1;
                        return next;
                        } catch (IndexOutOfBoundsException e) {
                        checkForComodification();
                        throw new NoSuchElementException();
                        }
                        }

                        public void remove() {
                        if (lastRet < 0)
                        throw new IllegalStateException();
                        checkForComodification();

                        try {
                        AbstractList.this.remove(lastRet);
                        if (lastRet < cursor)
                        cursor--;
                        lastRet = -1;
                        expectedModCount = modCount;
                        } catch (IndexOutOfBoundsException e) {
                        throw new ConcurrentModificationException();
                        }
                        }

                        final void checkForComodification() {
                        if (modCount != expectedModCount)
                        throw new ConcurrentModificationException();
                        }
                        }

                        private class ListItr extends Itr implements ListIterator<E> {
                        ListItr(int index) {
                        cursor = index;
                        }

                        public boolean hasPrevious() {
                        return cursor != 0;
                        }

                        public E previous() {
                        checkForComodification();
                        try {
                        int i = cursor - 1;
                        E previous = get(i);
                        //lastRet=i;cursor=i;
                        lastRet = cursor = i;
                        return previous;
                        } catch (IndexOutOfBoundsException e) {
                        checkForComodification();
                        throw new NoSuchElementException();
                        }
                        }

                        public int nextIndex() {
                        return cursor;
                        }

                        public int previousIndex() {
                        return cursor-1;
                        }

                        public void set(E e) {
                        if (lastRet < 0)
                        throw new IllegalStateException();
                        checkForComodification();

                        try {
                        AbstractList.this.set(lastRet, e);
                        expectedModCount = modCount;
                        } catch (IndexOutOfBoundsException ex) {
                        throw new ConcurrentModificationException();
                        }
                        }

                        public void add(E e) {
                        checkForComodification();

                        try {
                        int i = cursor;
                        AbstractList.this.add(i, e);
                        lastRet = -1;
                        cursor = i + 1;
                        expectedModCount = modCount;
                        } catch (IndexOutOfBoundsException ex) {
                        throw new ConcurrentModificationException();
                        }
                        }
                        }

                        //迭代器定义结束

                        public List<E> subList(int fromIndex, int toIndex) {
                        //Sublist和RandomAccessSubList在后面都作为内部类定义
                        //这俩的分支主要是能否支持高性能随机访问,而这点在Java是依靠是否实现RandomAccess接口
                        //来体现的,要实现接口必须得是一个类。因此,为了分歧,这里不得不创建两个类来表示两种情况。
                        //这两个类的方法应该是大致相同的。
                        //2
                        return (this instanceof RandomAccess ?
                        new RandomAccessSubList<>(this, fromIndex, toIndex) :
                        new SubList<>(this, fromIndex, toIndex));
                        }

                        // Comparison and hashing

                        public boolean equals(Object o) {
                        if (o == this)
                        return true;
                        if (!(o instanceof List))
                        return false;

                        ListIterator<E> e1 = listIterator();
                        ListIterator<?> e2 = ((List<?>) o).listIterator();
                        while (e1.hasNext() && e2.hasNext()) {
                        E o1 = e1.next();
                        Object o2 = e2.next();
                        //如果这东西没有重载equals方法,那此处就是单纯object对象是否相同了
                        if (!(o1==null ? o2==null : o1.equals(o2)))
                        return false;
                        }
                        //为啥size不一样不在一开始就比呢?那样不是更省花销吗
                        return !(e1.hasNext() || e2.hasNext());
                        }

                        public int hashCode() {
                        int hashCode = 1;
                        for (E e : this)
                        hashCode = 31*hashCode + (e==null ? 0 : e.hashCode());
                        return hashCode;
                        }

                        protected void removeRange(int fromIndex, int toIndex) {
                        ListIterator<E> it = listIterator(fromIndex);
                        for (int i=0, n=toIndex-fromIndex; i<n; i++) {
                        it.next();
                        it.remove();
                        }
                        }

                        //The number of times this list has been structurally modified.
                        //3
                        protected transient int modCount = 0;

                        private void rangeCheckForAdd(int index) {
                        if (index < 0 || index > size())
                        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
                        }

                        private String outOfBoundsMsg(int index) {
                        return "Index: "+index+", Size: "+size();
                        }
                        }


                        class SubList<E> extends AbstractList<E> {
                        //这大概是对父list的引用
                        private final AbstractList<E> l;
                        //这大概是在父list的起始偏移量
                        private final int offset;
                        //sublist的长度
                        private int size;

                        SubList(AbstractList<E> list, int fromIndex, int toIndex) {
                        if (fromIndex < 0)
                        throw new IndexOutOfBoundsException("fromIndex = " + fromIndex);
                        if (toIndex > list.size())
                        throw new IndexOutOfBoundsException("toIndex = " + toIndex);
                        if (fromIndex > toIndex)
                        throw new IllegalArgumentException("fromIndex(" + fromIndex +
                        ") > toIndex(" + toIndex + ")");
                        l = list;
                        offset = fromIndex;
                        size = toIndex - fromIndex;
                        this.modCount = l.modCount;
                        }

                        public E set(int index, E element) {
                        rangeCheck(index);
                        checkForComodification();
                        //改变sublist也会改变父list,是否成功取决于对父list的改变是否成功
                        return l.set(index+offset, element);
                        }

                        public E get(int index) {
                        rangeCheck(index);
                        checkForComodification();
                        //直接取父类值
                        return l.get(index+offset);
                        }

                        public int size() {
                        checkForComodification();
                        return size;
                        }

                        public void add(int index, E element) {
                        rangeCheckForAdd(index);
                        checkForComodification();
                        l.add(index+offset, element);
                        this.modCount = l.modCount;
                        size++;
                        }

                        public E remove(int index) {
                        rangeCheck(index);
                        checkForComodification();
                        E result = l.remove(index+offset);
                        this.modCount = l.modCount;
                        size--;
                        return result;
                        }

                        protected void removeRange(int fromIndex, int toIndex) {
                        checkForComodification();
                        l.removeRange(fromIndex+offset, toIndex+offset);
                        this.modCount = l.modCount;
                        size -= (toIndex-fromIndex);
                        }

                        public boolean addAll(Collection<? extends E> c) {
                        return addAll(size, c);
                        }

                        public boolean addAll(int index, Collection<? extends E> c) {
                        rangeCheckForAdd(index);
                        int cSize = c.size();
                        if (cSize==0)
                        return false;

                        checkForComodification();
                        l.addAll(offset+index, c);
                        this.modCount = l.modCount;
                        size += cSize;
                        return true;
                        }

                        public Iterator<E> iterator() {
                        return listIterator();
                        }

                        public ListIterator<E> listIterator(final int index) {
                        checkForComodification();
                        rangeCheckForAdd(index);

                        //改变迭代器实现
                        return new ListIterator<E>() {
                        //子类迭代器=父类迭代器+offset
                        private final ListIterator<E> i = l.listIterator(index+offset);

                        public boolean hasNext() {
                        return nextIndex() < size;
                        }

                        public E next() {
                        if (hasNext())
                        return i.next();
                        else
                        throw new NoSuchElementException();
                        }

                        public boolean hasPrevious() {
                        return previousIndex() >= 0;
                        }

                        public E previous() {
                        if (hasPrevious())
                        return i.previous();
                        else
                        throw new NoSuchElementException();
                        }

                        public int nextIndex() {
                        return i.nextIndex() - offset;
                        }

                        public int previousIndex() {
                        return i.previousIndex() - offset;
                        }

                        public void remove() {
                        i.remove();
                        SubList.this.modCount = l.modCount;
                        size--;
                        }

                        public void set(E e) {
                        i.set(e);
                        }

                        public void add(E e) {
                        i.add(e);
                        SubList.this.modCount = l.modCount;
                        size++;
                        }
                        };
                        }

                        //层层套娃啊
                        public List<E> subList(int fromIndex, int toIndex) {
                        return new SubList<>(this, fromIndex, toIndex);
                        }

                        private void rangeCheck(int index) {
                        if (index < 0 || index >= size)
                        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
                        }

                        private void rangeCheckForAdd(int index) {
                        if (index < 0 || index > size)
                        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
                        }

                        private String outOfBoundsMsg(int index) {
                        return "Index: "+index+", Size: "+size;
                        }

                        private void checkForComodification() {
                        if (this.modCount != l.modCount)
                        throw new ConcurrentModificationException();
                        }
                        }

                        //确实实现上没什么区别,主要是多了个RandomAccess的约定。
                        class RandomAccessSubList<E> extends SubList<E> implements RandomAccess {
                        RandomAccessSubList(AbstractList<E> list, int fromIndex, int toIndex) {
                        super(list, fromIndex, toIndex);
                        }

                        public List<E> subList(int fromIndex, int toIndex) {
                        return new RandomAccessSubList<>(this, fromIndex, toIndex);
                        }
                        }

                        -
                        #include <stdarg.h>
                        // Code 2
                        void arg_match(const char* fmt, ...) {
                        va_list ap; // 本质char * / void *
                        va_start(ap, fmt); // 之后ap就会指向fmt后的第一个可变参数

                        int idx = 0;
                        for (int i = 0; i < strlen(fmt); i ++) {
                        if (fmt[i] != '%') continue;
                        idx ++;
                        switch (fmt[i + 1]) {
                        case 'd':
                        int argv_i = va_arg(ap, int);
                        printf("第%d个参数为:%d\n", idx, argv_i);
                        break;
                        case 's':
                        char* argv_s = va_arg(ap, char*);
                        printf("第%d个参数为:%s\n", idx, argv_s);
                        break;
                        default:
                        printf("unknown.\n");
                        break;
                        }
                        }
                        }

                        call:
                        arg_match("%d %d %s\n", 1, 2, "333");
                        +

                        其中:

                          +
                        1. 内部类ListItr和Itr的实现

                          Itr的实现需要用到AbstratcList类的get和set方法,而显然不同Collection的get和set不一样。为了避免混淆,Itr就只能作为私有类。为了避免胡乱引用,Itr就可以直接作为内部类,共享其外部类的所有资源。

                          +

                          ListItr作为内部私有类很容易理解,毕竟只有list才需要它。

                          +
                        2. +
                        3. RandomAccess

                          RandomAccess是一个空接口,它应该代表一个约定俗成的规定,即它的implementations的随机访问都是性能较高的。这个空接口思想很常见,源码带给我们的智慧。

                          +
                          按经验来说, a List implementation should implement this interface, if this loop:
                          for (int i=0, n=list.size(); i < n; i++)
                          list.get(i);

                          runs faster than this loop:
                          for (Iterator i=list.iterator(); i.hasNext(); )
                          i.next();
                        4. +
                        5. 关于modCount字段与fail-fast机制

                          你真的知道集合中modCount字段作用吗?

                          +

                          modCount字段就是保证一定程度并发安全的变量,fail-fast就是指马上抛出异常。

                          + -

                          除此之外,我们也可以实现变长参数宏

                          -

                          在GCC编译器下,变长参数宏可以使用“##”宏字符串连接操作实现。

                          -
                          #define printf(args…) fprintf(stdout, ##args)
                        6. -
                        7. 非局部跳转

                          -

                          这位更是重量级

                          -
                          #include <setjmp.h>
                          #include <stdio.h>

                          jmp_buf b;

                          void f()
                          {
                          longjmp(b, 1);
                          }

                          int main()
                          {
                          if (setjmp(b))
                          printf("World!");
                          else
                          {
                          printf("Hello ");
                          f();
                          }
                          }
                          +

                          modCount不能保证绝对的并发安全,因为它只负责防范结构改变,而不负责看某位置的数据更新。

                          +

                          在现实中要实现对集合的边迭代边修改,下面三种方式都是错的:

                          + -

                          事实上的输出是:

                          -

                          Hello World!

                          +

                          第一种方法也可以把for里的size换成list.size()

                          +

                          其中乐观锁与悲观锁可见:乐观锁

                          -

                          实际上,当setjmp正常返回的时候,会返回0,因此会打印出“Hello”的字样。而longjmp的作用,就是让程序的执行流回到当初setjmp返回的时刻,并且返回由longjmp指定的返回值(longjmp的参数2),也就是1,自然接着会打印出“World!”并退出。换句话说,longjmp可以让程序“时光倒流”回setjmp返回的时刻,并改变其行为,以至于改变了未来。

                          +
                            +
                          • 乐观锁:总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,只在更新的时候会判断一下在此期间别人有没有去更新这个数据。
                          • +
                          +
                          +

                          注意“在此期间”的含义是拿到数据到更新数据的这段时间。因为没有加锁,所以别的线程可能会更改。还有一点那就是乐观锁其实是不加锁的来保证某个变量一系列操作原子性的一种方法。

                          +
                          +
                            +
                          • 悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞,直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java中synchronizedReentrantLock等独占锁就是悲观锁思想的实现。
                          • +
                        -

                        glibc

                        glibc是对标准C运行库的扩展(如增加了pthread),全称GNU C Library,是GNU旗下的C标准库。

                        -

                        生命周期

                        于是,我们可以完整串联整个运行程序的生命周期:

                        -

                        由链接器ld将所有.o文件的_init段和_finit段(包含glibc对堆空间的初始化和释放、编译器对C++全局对象构造析构的实现以及app自己实现的init和finit函数)分别串在一起,并且链接上glibc库的包含了_start(会调用_init)的crt.o文件,最后就形成了包含各种glibc标准库和真·用户代码的可执行文件。

                        -

                        可执行文件被装载到进程地址空间后,首先会进行动态链接。然后,从程序入口_start开始进行各种初始化,调用可执行文件的这个_init段的内容。init完成之后,glibc就调用程序中的入口main。main执行过程中会用到glibc的各种标准库函数。main执行完后就会继续执行_finit段来结束一切。

                        -

                        C++的全局对象构造析构

                          -
                        1. 构造(_init

                          -

                          编译器会将每个全局对象的构造函数以如下形式包装:

                          -
                          static void GLOBAL__I_Hw(void)
                          {
                          Hw::Hw(); // 构造对象
                          atexit(__tcf_1); // 一个神秘的函数叫做__tcf_1被注册到了exit
                          }
                          +

                          AbstractList帮我们实现了差不多所有方法,除了Tget(int)size()set(int, Object)add(int, E)remove(int) 。因而,接下来的两个实现中,重点关注这些就行。

                          +

                          ArrayList

                          代码

                          +

                          Implements all optional list operations, and permits all elements, including null.

                          +

                          This class is roughly equivalent to Vector, except that it is unsynchronized.

                          +

                          size(),isEmpty(),get(),set(),iterator(),listIterator()的时间复杂符是**常量级别(constant time),add()方法的时间复杂度是可变常量级别(amortized constant time),即为O(n)。大致上说,剩余方法的时间复杂度都是线性时间(linear time)。相较于LinkedList的实现,ArrayList的常量因子(constant factor)**较低。

                          +

                          An application can increase the capacity of an ArrayList instance before adding a large number of elements using the ensureCapacity operation. This may reduce the amount of incremental reallocation.

                          +

                          本身是线程不安全的,但可以通过封装类来实现线程同步。可以使用Collections.synchronizedList method.

                          +
                          List list = Collections.synchronizedList(new ArrayList(...));
                          +
                          +

                          总体来说实现的很多方法跟想象中差别不大,有几个比较惊艳

                          +
                          //1 cloneable
                          public class ArrayList<E> extends AbstractList<E>
                          implements List<E>, RandomAccess, Cloneable, java.io.Serializable
                          {
                          //4
                          private static final long serialVersionUID = 8683452581122892189L;

                          //可以注意一下,初始=10.
                          private static final int DEFAULT_CAPACITY = 10;

                          //Shared empty array instance used for empty instances.
                          //7
                          private static final Object[] EMPTY_ELEMENTDATA = {};

                          private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

                          //5、6
                          /*
                          The array buffer into which the elements of the ArrayList are stored.缓冲
                          ArrayList的容量=此数组的length
                          Any empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA will be expanded to DEFAULT_CAPACITY when the first element is added.
                          */
                          transient Object[] elementData; // non-private to simplify nested class access

                          private int size;

                          public ArrayList(int initialCapacity) {
                          if (initialCapacity > 0) {
                          this.elementData = new Object[initialCapacity];
                          } else if (initialCapacity == 0) {
                          this.elementData = EMPTY_ELEMENTDATA;
                          } else {
                          throw new IllegalArgumentException("Illegal Capacity: "+
                          initialCapacity);
                          }
                          }

                          public ArrayList() {
                          this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
                          }

                          //in the order they are returned by c's iterator.
                          public ArrayList(Collection<? extends E> c) {
                          Object[] a = c.toArray();
                          if ((size = a.length) != 0) {
                          if (c.getClass() == ArrayList.class) {
                          elementData = a;
                          } else {
                          elementData = Arrays.copyOf(a, size, Object[].class);
                          }
                          //size==0
                          } else {
                          // replace with empty array.
                          elementData = EMPTY_ELEMENTDATA;
                          }
                          }

                          //把容量缩小为当前size。An application can use this operation to minimize the storage of an ArrayList instance.
                          public void trimToSize() {
                          //涉及List的结构性改变
                          modCount++;
                          if (size < elementData.length) {
                          elementData = (size == 0)
                          ? EMPTY_ELEMENTDATA
                          : Arrays.copyOf(elementData, size);
                          }
                          }

                          //8
                          public void ensureCapacity(int minCapacity) {
                          int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
                          //如果原size!=0,除非minCapacity=0,否则必须是要扩容一次的【函数要求】,
                          //因此设置为最小值0,以确保下面的if条件一定为true。
                          ? 0
                          //如果为默认大小0,此处是必须扩为默认大小的
                          : DEFAULT_CAPACITY;

                          if (minCapacity > minExpand) {
                          ensureExplicitCapacity(minCapacity);
                          }
                          }

                          private static int calculateCapacity(Object[] elementData, int minCapacity) {
                          if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
                          return Math.max(DEFAULT_CAPACITY, minCapacity);
                          }
                          return minCapacity;
                          }

                          private void ensureCapacityInternal(int minCapacity) {
                          ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
                          }

                          private void ensureExplicitCapacity(int minCapacity) {
                          //修改list结构
                          //若minCapacity<elementData.length,本句modCount++始终执行。
                          modCount++;
                          // overflow-conscious code
                          if (minCapacity - elementData.length > 0)
                          grow(minCapacity);
                          }

                          private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

                          private void grow(int minCapacity) {
                          // overflow-conscious code
                          //capacity=length of elementData
                          int oldCapacity = elementData.length;
                          //每次扩1/2
                          int newCapacity = oldCapacity + (oldCapacity >> 1);
                          if (newCapacity - minCapacity < 0)
                          newCapacity = minCapacity;
                          //防止溢出
                          if (newCapacity - MAX_ARRAY_SIZE > 0)
                          newCapacity = hugeCapacity(minCapacity);
                          // minCapacity is usually close to size, so this is a win:
                          elementData = Arrays.copyOf(elementData, newCapacity);
                          }

                          private static int hugeCapacity(int minCapacity) {
                          if (minCapacity < 0) // overflow
                          throw new OutOfMemoryError();
                          return (minCapacity > MAX_ARRAY_SIZE) ?
                          Integer.MAX_VALUE :
                          MAX_ARRAY_SIZE;
                          }

                          public int size() {
                          return size;
                          }

                          public boolean isEmpty() {
                          return size == 0;
                          }

                          public boolean contains(Object o) {
                          return indexOf(o) >= 0;
                          }

                          public int indexOf(Object o) {
                          if (o == null) {
                          for (int i = 0; i < size; i++)
                          if (elementData[i]==null)
                          return i;
                          } else {
                          for (int i = 0; i < size; i++)
                          if (o.equals(elementData[i]))
                          return i;
                          }
                          return -1;
                          }

                          public int lastIndexOf(Object o) {
                          if (o == null) {
                          for (int i = size-1; i >= 0; i--)
                          if (elementData[i]==null)
                          return i;
                          } else {
                          for (int i = size-1; i >= 0; i--)
                          if (o.equals(elementData[i]))
                          return i;
                          }
                          return -1;
                          }
                          //2
                          public Object clone() {
                          try {
                          //from Object.clone():
                          //the returned object should be obtained by calling super.clone.
                          ArrayList<?> v = (ArrayList<?>) super.clone();
                          //浅拷贝,只拷贝地址
                          v.elementData = Arrays.copyOf(elementData, size);
                          v.modCount = 0;
                          return v;
                          } catch (CloneNotSupportedException e) {
                          // this shouldn't happen, since we are Cloneable
                          throw new InternalError(e);
                          }
                          }

                          public Object[] toArray() {
                          return Arrays.copyOf(elementData, size);
                          }

                          @SuppressWarnings("unchecked")
                          public <T> T[] toArray(T[] a) {
                          if (a.length < size)
                          // Make a new array of a's runtime type, but my contents:
                          return (T[]) Arrays.copyOf(elementData, size, a.getClass());
                          System.arraycopy(elementData, 0, a, 0, size);
                          if (a.length > size)
                          a[size] = null;
                          return a;
                          }

                          // Positional Access Operations

                          @SuppressWarnings("unchecked")
                          E elementData(int index) {
                          return (E) elementData[index];
                          }

                          //这里我挺迷惑的,为什么还要再套一层elementData??
                          public E get(int index) {
                          rangeCheck(index);

                          return elementData(index);
                          }

                          public E set(int index, E element) {
                          rangeCheck(index);

                          E oldValue = elementData(index);
                          elementData[index] = element;
                          return oldValue;
                          }

                          public boolean add(E e) {
                          //既增加了modcount,也保证了capacity够用
                          ensureCapacityInternal(size + 1);
                          elementData[size++] = e;
                          return true;
                          }

                          public void add(int index, E element) {
                          rangeCheckForAdd(index);
                          ensureCapacityInternal(size + 1); // Increments modCount!!
                          System.arraycopy(elementData, index, elementData, index + 1,
                          size - index);//src dest 移动数组
                          elementData[index] = element;
                          size++;
                          }

                          public E remove(int index) {
                          rangeCheck(index);

                          modCount++;
                          E oldValue = elementData(index);

                          int numMoved = size - index - 1;
                          if (numMoved > 0)
                          System.arraycopy(elementData, index+1, elementData, index,
                          numMoved);
                          elementData[--size] = null; // clear to let GC do its work,自动清除无引用对象

                          return oldValue;
                          }

                          public boolean remove(Object o) {
                          if (o == null) {
                          for (int index = 0; index < size; index++)
                          if (elementData[index] == null) {
                          fastRemove(index);
                          return true;
                          }
                          } else {
                          for (int index = 0; index < size; index++)
                          if (o.equals(elementData[index])) {
                          fastRemove(index);
                          return true;
                          }
                          }
                          return false;
                          }

                          //这不是跟上面一模一样吗,为啥还要再写一遍?
                          private void fastRemove(int index) {
                          modCount++;
                          int numMoved = size - index - 1;
                          if (numMoved > 0)
                          System.arraycopy(elementData, index+1, elementData, index,
                          numMoved);
                          elementData[--size] = null; // clear to let GC do its work
                          }

                          public void clear() {
                          modCount++;

                          //9 我能不能直接猛一点:elementData=new Object[elementData.length]?
                          // clear to let GC do its work
                          for (int i = 0; i < size; i++)
                          elementData[i] = null;

                          size = 0;
                          }

                          public boolean addAll(Collection<? extends E> c) {
                          Object[] a = c.toArray();
                          int numNew = a.length;
                          ensureCapacityInternal(size + numNew); // Increments modCount
                          System.arraycopy(a, 0, elementData, size, numNew);
                          size += numNew;
                          return numNew != 0;
                          }

                          public boolean addAll(int index, Collection<? extends E> c) {
                          rangeCheckForAdd(index);

                          Object[] a = c.toArray();
                          int numNew = a.length;
                          ensureCapacityInternal(size + numNew); // Increments modCount

                          int numMoved = size - index;
                          if (numMoved > 0)
                          System.arraycopy(elementData, index, elementData, index + numNew,
                          numMoved);

                          System.arraycopy(a, 0, elementData, index, numNew);
                          size += numNew;
                          return numNew != 0;
                          }

                          protected void removeRange(int fromIndex, int toIndex) {
                          modCount++;
                          int numMoved = size - toIndex;
                          System.arraycopy(elementData, toIndex, elementData, fromIndex,
                          numMoved);

                          // clear to let GC do its work
                          int newSize = size - (toIndex-fromIndex);
                          for (int i = newSize; i < size; i++) {
                          elementData[i] = null;
                          }
                          size = newSize;
                          }

                          private void rangeCheck(int index) {
                          if (index >= size)
                          throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
                          }

                          private void rangeCheckForAdd(int index) {
                          if (index > size || index < 0)
                          throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
                          }

                          private String outOfBoundsMsg(int index) {
                          return "Index: "+index+", Size: "+size;
                          }

                          public boolean removeAll(Collection<?> c) {
                          Objects.requireNonNull(c);
                          return batchRemove(c, false);
                          }

                          public boolean retainAll(Collection<?> c) {
                          Objects.requireNonNull(c);
                          return batchRemove(c, true);
                          }

                          //这个complement做得很漂亮,兼顾实际意义又统一了代码
                          private boolean batchRemove(Collection<?> c, boolean complement) {
                          //局部变量
                          final Object[] elementData = this.elementData;
                          int r = 0, w = 0;
                          boolean modified = false;

                          try {
                          for (; r < size; r++)
                          if (c.contains(elementData[r]) == complement)
                          //原地平移,nice
                          //如果是removeall,此条件成立说明c不含有该元素,则保留该元素
                          //如果是retainall,此条件成立说明c含有该元素,则保留该元素
                          elementData[w++] = elementData[r];
                          } finally {
                          // Preserve behavioral compatibility with AbstractCollection,
                          // even if c.contains() throws.
                          if (r != size) {
                          System.arraycopy(elementData, r,
                          elementData, w,
                          size - r);
                          w += size - r;
                          }
                          if (w != size) {
                          // clear to let GC do its work
                          for (int i = w; i < size; i++)
                          elementData[i] = null;
                          modCount += size - w;
                          size = w;
                          modified = true;
                          }
                          }
                          return modified;
                          }

                          //序列化
                          private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException{
                          // Write out element count, and any hidden stuff
                          int expectedModCount = modCount;
                          /*from ObjectOutputStream:
                          Write the non-static and non-transient fields of the current class
                          to this stream.*/
                          s.defaultWriteObject();

                          //为啥要特地强调write size?
                          s.writeInt(size);

                          for (int i=0; i<size; i++) {
                          s.writeObject(elementData[i]);
                          }

                          //保证一定基础的序列化同步
                          if (modCount != expectedModCount) {
                          throw new ConcurrentModificationException();
                          }
                          }

                          private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException {
                          elementData = EMPTY_ELEMENTDATA;
                          // Read in size, and any hidden stuff
                          s.defaultReadObject();
                          //不大懂为什么这里的值被ignore了?
                          // Read in capacity
                          s.readInt(); // ignored

                          if (size > 0) {
                          // be like clone(), allocate array based upon size not capacity
                          int capacity = calculateCapacity(elementData, size);
                          SharedSecrets.getJavaOISAccess().checkArray(s, Object[].class, capacity);
                          ensureCapacityInternal(size);

                          Object[] a = elementData;
                          // Read in all elements in the proper order.
                          for (int i=0; i<size; i++) {
                          a[i] = s.readObject();
                          }
                          }
                          }

                          public ListIterator<E> listIterator(int index) {
                          if (index < 0 || index > size)
                          throw new IndexOutOfBoundsException("Index: "+index);
                          return new ListItr(index);
                          }

                          public ListIterator<E> listIterator() {
                          return new ListItr(0);
                          }

                          public Iterator<E> iterator() {
                          return new Itr();
                          }

                          /**
                          * AbstractList.Itr的优化版本
                          */
                          private class Itr implements Iterator<E> {
                          int cursor; // index of next element to return
                          int lastRet = -1; // index of last element returned; -1 if no such
                          int expectedModCount = modCount;

                          Itr() {}

                          public boolean hasNext() {
                          return cursor != size;
                          }

                          @SuppressWarnings("unchecked")
                          public E next() {
                          checkForComodification();
                          int i = cursor;
                          if (i >= size)
                          throw new NoSuchElementException();
                          //内部类访问外部类this的方法
                          Object[] elementData = ArrayList.this.elementData;
                          if (i >= elementData.length)
                          throw new ConcurrentModificationException();
                          cursor = i + 1;
                          return (E) elementData[lastRet = i];
                          }

                          public void remove() {
                          if (lastRet < 0)
                          throw new IllegalStateException();
                          checkForComodification();

                          try {
                          //change here
                          //确实AbstractList的那个remove应该更适用于LinkedList
                          ArrayList.this.remove(lastRet);
                          cursor = lastRet;
                          lastRet = -1;
                          expectedModCount = modCount;
                          } catch (IndexOutOfBoundsException ex) {
                          throw new ConcurrentModificationException();
                          }
                          }

                          @Override
                          @SuppressWarnings("unchecked")
                          public void forEachRemaining(Consumer<? super E> consumer) {
                          Objects.requireNonNull(consumer);
                          final int size = ArrayList.this.size;
                          int i = cursor;
                          //这里不抛异常吗
                          if (i >= size) {
                          return;
                          }
                          final Object[] elementData = ArrayList.this.elementData;
                          //好像确实是并发修改了,毕竟上面已经test过i<size<capacity了
                          if (i >= elementData.length) {
                          throw new ConcurrentModificationException();
                          }
                          while (i != size && modCount == expectedModCount) {
                          consumer.accept((E) elementData[i++]);
                          }
                          // update once at end of iteration to reduce heap write traffic
                          //10
                          cursor = i;
                          lastRet = i - 1;
                          checkForComodification();
                          }

                          final void checkForComodification() {
                          if (modCount != expectedModCount)
                          throw new ConcurrentModificationException();
                          }
                          }

                          /**
                          * AbstractList.ListItr的优化版本
                          */
                          private class ListItr extends Itr implements ListIterator<E> {
                          ListItr(int index) {
                          super();
                          cursor = index;
                          }

                          public boolean hasPrevious() {
                          return cursor != 0;
                          }

                          public int nextIndex() {
                          return cursor;
                          }

                          public int previousIndex() {
                          return cursor - 1;
                          }

                          @SuppressWarnings("unchecked")
                          public E previous() {
                          checkForComodification();
                          int i = cursor - 1;
                          if (i < 0)
                          throw new NoSuchElementException();
                          Object[] elementData = ArrayList.this.elementData;
                          //11 防止并发修改,比如在这期间进行了trim
                          if (i >= elementData.length)
                          throw new ConcurrentModificationException();
                          cursor = i;
                          return (E) elementData[lastRet = i];
                          }

                          public void set(E e) {
                          if (lastRet < 0)
                          throw new IllegalStateException();
                          checkForComodification();

                          try {
                          ArrayList.this.set(lastRet, e);
                          } catch (IndexOutOfBoundsException ex) {
                          throw new ConcurrentModificationException();
                          }
                          }

                          public void add(E e) {
                          checkForComodification();

                          try {
                          int i = cursor;
                          ArrayList.this.add(i, e);
                          cursor = i + 1;
                          lastRet = -1;
                          expectedModCount = modCount;
                          } catch (IndexOutOfBoundsException ex) {
                          throw new ConcurrentModificationException();
                          }
                          }
                          }

                          public List<E> subList(int fromIndex, int toIndex) {
                          subListRangeCheck(fromIndex, toIndex, size);
                          return new SubList(this, 0, fromIndex, toIndex);
                          }

                          static void subListRangeCheck(int fromIndex, int toIndex, int size) {
                          if (fromIndex < 0)
                          throw new IndexOutOfBoundsException("fromIndex = " + fromIndex);
                          if (toIndex > size)
                          throw new IndexOutOfBoundsException("toIndex = " + toIndex);
                          if (fromIndex > toIndex)
                          throw new IllegalArgumentException("fromIndex(" + fromIndex +
                          ") > toIndex(" + toIndex + ")");
                          }

                          //依然内部类。不过这里应该用了个作用域的性质。相比于AbstractList定义在List类外部的
                          //Sublist,应该会更优先使用定义在内部的Sublist
                          //12 注意,这里的sublist没有直接extends ArrayList
                          private class SubList extends AbstractList<E> implements RandomAccess {
                          private final AbstractList<E> parent;
                          //新增成员
                          private final int parentOffset;
                          private final int offset;
                          int size;

                          SubList(AbstractList<E> parent,
                          int offset, int fromIndex, int toIndex) {
                          this.parent = parent;
                          //这个parentOffset应该是指相较于最近的父亲的偏移量,offset应该就是相较于最底层的父亲的偏移量
                          this.parentOffset = fromIndex;
                          this.offset = offset + fromIndex;
                          this.size = toIndex - fromIndex;
                          this.modCount = ArrayList.this.modCount;
                          }

                          public E set(int index, E e) {
                          rangeCheck(index);
                          checkForComodification();
                          E oldValue = ArrayList.this.elementData(offset + index);
                          ArrayList.this.elementData[offset + index] = e;
                          return oldValue;
                          }

                          public E get(int index) {
                          rangeCheck(index);
                          checkForComodification();
                          return ArrayList.this.elementData(offset + index);
                          }

                          public int size() {
                          checkForComodification();
                          return this.size;
                          }

                          public void add(int index, E e) {
                          rangeCheckForAdd(index);
                          checkForComodification();
                          //13
                          parent.add(parentOffset + index, e);
                          this.modCount = parent.modCount;
                          this.size++;
                          }

                          public E remove(int index) {
                          rangeCheck(index);
                          checkForComodification();
                          E result = parent.remove(parentOffset + index);
                          this.modCount = parent.modCount;
                          this.size--;
                          return result;
                          }

                          protected void removeRange(int fromIndex, int toIndex) {
                          checkForComodification();
                          parent.removeRange(parentOffset + fromIndex,
                          parentOffset + toIndex);
                          this.modCount = parent.modCount;
                          this.size -= toIndex - fromIndex;
                          }

                          public boolean addAll(Collection<? extends E> c) {
                          return addAll(this.size, c);
                          }

                          public boolean addAll(int index, Collection<? extends E> c) {
                          rangeCheckForAdd(index);
                          int cSize = c.size();
                          if (cSize==0)
                          return false;

                          checkForComodification();
                          parent.addAll(parentOffset + index, c);
                          this.modCount = parent.modCount;
                          this.size += cSize;
                          return true;
                          }

                          public Iterator<E> iterator() {
                          return listIterator();
                          }

                          public ListIterator<E> listIterator(final int index) {
                          checkForComodification();
                          rangeCheckForAdd(index);
                          final int offset = this.offset;

                          return new ListIterator<E>() {
                          int cursor = index;
                          int lastRet = -1;
                          int expectedModCount = ArrayList.this.modCount;

                          public boolean hasNext() {
                          //内部内部类还可以访问外部类和外部外部类
                          return cursor != SubList.this.size;
                          }

                          @SuppressWarnings("unchecked")
                          public E next() {
                          checkForComodification();
                          int i = cursor;
                          if (i >= SubList.this.size)
                          throw new NoSuchElementException();
                          Object[] elementData = ArrayList.this.elementData;
                          if (offset + i >= elementData.length)
                          throw new ConcurrentModificationException();
                          cursor = i + 1;
                          return (E) elementData[offset + (lastRet = i)];
                          }

                          public boolean hasPrevious() {
                          return cursor != 0;
                          }

                          @SuppressWarnings("unchecked")
                          public E previous() {
                          checkForComodification();
                          int i = cursor - 1;
                          if (i < 0)
                          throw new NoSuchElementException();
                          Object[] elementData = ArrayList.this.elementData;
                          if (offset + i >= elementData.length)
                          throw new ConcurrentModificationException();
                          cursor = i;
                          return (E) elementData[offset + (lastRet = i)];
                          }

                          @SuppressWarnings("unchecked")
                          public void forEachRemaining(Consumer<? super E> consumer) {
                          Objects.requireNonNull(consumer);
                          final int size = SubList.this.size;
                          int i = cursor;
                          if (i >= size) {
                          return;
                          }
                          final Object[] elementData = ArrayList.this.elementData;
                          if (offset + i >= elementData.length) {
                          throw new ConcurrentModificationException();
                          }
                          while (i != size && modCount == expectedModCount) {
                          consumer.accept((E) elementData[offset + (i++)]);
                          }
                          // update once at end of iteration to reduce heap write traffic
                          lastRet = cursor = i;
                          checkForComodification();
                          }

                          public int nextIndex() {
                          return cursor;
                          }

                          public int previousIndex() {
                          return cursor - 1;
                          }

                          public void remove() {
                          if (lastRet < 0)
                          throw new IllegalStateException();
                          checkForComodification();

                          try {
                          SubList.this.remove(lastRet);
                          cursor = lastRet;
                          lastRet = -1;
                          expectedModCount = ArrayList.this.modCount;
                          } catch (IndexOutOfBoundsException ex) {
                          //确实要是IndexOutOfBounds的话,就说明lastRet改了,说明Concurrent了
                          throw new ConcurrentModificationException();
                          }
                          }

                          public void set(E e) {
                          if (lastRet < 0)
                          throw new IllegalStateException();
                          checkForComodification();

                          try {
                          ArrayList.this.set(offset + lastRet, e);
                          } catch (IndexOutOfBoundsException ex) {
                          throw new ConcurrentModificationException();
                          }
                          }

                          public void add(E e) {
                          checkForComodification();

                          try {
                          int i = cursor;
                          SubList.this.add(i, e);
                          cursor = i + 1;
                          lastRet = -1;
                          expectedModCount = ArrayList.this.modCount;
                          } catch (IndexOutOfBoundsException ex) {
                          throw new ConcurrentModificationException();
                          }
                          }

                          final void checkForComodification() {
                          if (expectedModCount != ArrayList.this.modCount)
                          throw new ConcurrentModificationException();
                          }
                          };
                          }

                          public List<E> subList(int fromIndex, int toIndex) {
                          subListRangeCheck(fromIndex, toIndex, size);
                          return new SubList(this, offset, fromIndex, toIndex);
                          }

                          public Spliterator<E> spliterator() {
                          checkForComodification();
                          return new ArrayListSpliterator<E>(ArrayList.this, offset,
                          offset + this.size, this.modCount);
                          }
                          }

                          @Override
                          public void forEach(Consumer<? super E> action) {
                          Objects.requireNonNull(action);
                          final int expectedModCount = modCount;
                          @SuppressWarnings("unchecked")
                          final E[] elementData = (E[]) this.elementData;
                          final int size = this.size;
                          //一修改就会寄
                          for (int i=0; modCount == expectedModCount && i < size; i++) {
                          action.accept(elementData[i]);
                          }
                          if (modCount != expectedModCount) {
                          throw new ConcurrentModificationException();
                          }
                          }

                          @Override
                          public Spliterator<E> spliterator() {
                          return new ArrayListSpliterator<>(this, 0, -1, 0);
                          }

                          /** Index-based split-by-two, lazily initialized Spliterator */
                          //基于索引的二分法,懒加载的 Spliterator
                          static final class ArrayListSpliterator<E> implements Spliterator<E> {...}

                          @Override
                          public boolean removeIf(Predicate<? super E> filter) {
                          Objects.requireNonNull(filter);

                          int removeCount = 0;
                          //666,用了类似掩码的思想,这样就能避免多次移动数组了,实现O(n),很不错
                          final BitSet removeSet = new BitSet(size);
                          final int expectedModCount = modCount;
                          final int size = this.size;
                          for (int i=0; modCount == expectedModCount && i < size; i++) {
                          @SuppressWarnings("unchecked")
                          final E element = (E) elementData[i];
                          if (filter.test(element)) {
                          //set:Sets the bit at the specified index to true.
                          removeSet.set(i);
                          removeCount++;
                          }
                          }
                          if (modCount != expectedModCount) {
                          throw new ConcurrentModificationException();
                          }

                          // shift surviving elements left over the spaces left by removed elements
                          final boolean anyToRemove = removeCount > 0;
                          if (anyToRemove) {
                          final int newSize = size - removeCount;
                          for (int i=0, j=0; (i < size) && (j < newSize); i++, j++) {
                          //nextClearBit:Returns the index of the first bit that is set to false
                          //false表示不移走,true表示移走
                          i = removeSet.nextClearBit(i);
                          elementData[j] = elementData[i];
                          }
                          for (int k=newSize; k < size; k++) {
                          elementData[k] = null; // Let gc do its work
                          }
                          this.size = newSize;
                          if (modCount != expectedModCount) {
                          throw new ConcurrentModificationException();
                          }
                          modCount++;
                          }

                          return anyToRemove;
                          }

                          @Override
                          @SuppressWarnings("unchecked")
                          public void replaceAll(UnaryOperator<E> operator) {
                          Objects.requireNonNull(operator);
                          final int expectedModCount = modCount;
                          final int size = this.size;
                          for (int i=0; modCount == expectedModCount && i < size; i++) {
                          elementData[i] = operator.apply((E) elementData[i]);
                          }
                          if (modCount != expectedModCount) {
                          throw new ConcurrentModificationException();
                          }
                          modCount++;
                          }

                          @Override
                          @SuppressWarnings("unchecked")
                          public void sort(Comparator<? super E> c) {
                          final int expectedModCount = modCount;
                          Arrays.sort((E[]) elementData, 0, size, c);
                          if (modCount != expectedModCount) {
                          throw new ConcurrentModificationException();
                          }
                          modCount++;
                          }
                          }
                          -

                          然后将这个GLOBAL__I_Hw放进.o文件的一个.ctor段中,最后由ld将各个.o文件的.ctor段链接起来,并计算出全局对象数量填入crtbegin.o即可。

                          -

                          之后在_init段中遍历.ctor的各个函数指针进行构造函数调用就行了

                          +

                          其中:

                            +
                          1. Cloneable接口

                            与RandomAccess一样,都是一个规定性质的接口。

                            -

                            后日谈:今天又在rtt中看到了这一牛掰操作。rtt也是大概通过这个原理实现的帅的一匹的“Automatic Initialization Mechanism”。

                            -

                            原理感觉也是将其放入一个特殊的”rti_fn$f”段,并且用rti_start和end来标识该段结束,

                            -
                            // xiunian: INIT_EXPORT应该是这个
                            INIT_EXPORT(fn, "1.0") 宏展开:
                            const char __rti_level_fn[] = ".rti_fn." "1.0";
                            // 指示编译器将特定的变量或数据结构分配到名为 "rti_fn$f" 的内存段(Memory Segment)中
                            __declspec(allocate("rti_fn$f"))
                            rt_used const struct rt_init_desc __rt_init_msc_fn = {__rti_level_fn, fn };
                            +

                            A class implements the Cloneable interface to indicate to the Object.clone() method that it is legal for that method to make a field-for-field copy of instances of that class.

                            +

                            Classes that implement this interface should override Object.clone (which is protected) with a public method.

                            +
                            +
                          2. +
                          3. clone与浅拷贝(shallow copy)
                                public static void main(String[] args) {
                            ArrayList<Student> arr = new ArrayList<>();
                            arr.add(new Student("lylt",15));
                            ArrayList<Student> cl = (ArrayList)arr.clone();
                            System.out.println(cl.get(0).toString());
                            cl.get(0).name="Sam";
                            System.out.println(cl.get(0).toString());
                            System.out.println(arr.get(0).toString());
                            cl.set(0,new Student("HWX",19));
                            System.out.println(cl.get(0).toString());
                            System.out.println(arr.get(0).toString());
                            }
                            /*运行结果:
                            name :lyltage :15
                            name :Samage :15
                            name :Samage :15
                            name :HWXage :19
                            name :Samage :15
                            */
                            -
                            /*
                            * xiunian: 牛逼,这里颇有学链接时的感觉了
                            * 这里介绍了组件初始化顺序
                            * Components Initialization will initialize some driver and components as following
                            * order:
                            * rti_start --> 0
                            * BOARD_EXPORT --> 1
                            * rti_board_end --> 1.end
                            *
                            * DEVICE_EXPORT --> 2
                            * COMPONENT_EXPORT --> 3
                            * FS_EXPORT --> 4
                            * ENV_EXPORT --> 5
                            * APP_EXPORT --> 6
                            *
                            * rti_end --> 6.end
                            *
                            * These automatically initialization, the driver or component initial function must
                            * be defined with:
                            * INIT_BOARD_EXPORT(fn);
                            * INIT_DEVICE_EXPORT(fn);
                            * ...
                            * INIT_APP_EXPORT(fn);
                            * etc.
                            */
                            static int rti_start(void)
                            {
                            return 0;
                            }
                            INIT_EXPORT(rti_start, "0");

                            static int rti_board_start(void)
                            {
                            return 0;
                            }
                            INIT_EXPORT(rti_board_start, "0.end");

                            static int rti_board_end(void)
                            {
                            return 0;
                            }
                            INIT_EXPORT(rti_board_end, "1.end");

                            static int rti_end(void)
                            {
                            return 0;
                            }
                            INIT_EXPORT(rti_end, "6.end");
                            +

                            结合内部代码可知,确实跟上面那个toArray的原理是一样的,只拷贝地址。

                            +

                            clone是浅拷贝。

                            +

                            浅拷贝与深拷贝的区别

                            +
                          4. +
                          5. 关于子类继承到的父类内部类

                            本来在犹豫,子类默认继承到的内部类里面用到的外部类方法的版本是取父还是取子,经过以下实验可知,是取能访问到的最新版本。

                            +
                            public class Main {
                            public static void main(String[] args) {
                            ChildOuter chldot = new ChildOuter();
                            chldot.printname();
                            chldot.in.printall();
                            }
                            }
                            /*结果:子类声明为private的成员字段不能被从父类继承而来的方法访问到
                            Father
                            I am father!*/
                            class FatherOuter{
                            String name="Father";
                            private void print(){
                            System.out.println("I am father!");
                            }
                            public Inner in = new Inner();
                            public class Inner{
                            public int haha=1;
                            void printall(){
                            print();
                            }
                            }
                            public void printname(){
                            System.out.println(name);
                            }
                            }

                            class ChildOuter extends FatherOuter{
                            private String name="Child";
                            private void print(){System.out.println("I am child!");}
                            }
                            -

                            之后真正初始化只需遍历然后调用函数指针即可

                            -
                            void rt_components_board_init(void)
                            {
                            volatile const init_fn_t *fn_ptr;

                            for (fn_ptr = &__rt_init_rti_board_start; fn_ptr < &__rt_init_rti_board_end; fn_ptr++)
                            {
                            (*fn_ptr)();
                            }
                            }
                      -
                    2. -
                    3. 析构(_finit

                      -

                      早期同理可得。现在变了,变成直接在GLOBAL__I_Hw中注册atexit了。

                      -
                      static void __tcf_1(void) //这个名字由编译器生成
                      {
                      Hw.~HelloWorld();
                      }
                    4. -
                    -

                    实现小型运行库

                    -

                    看到标题就知道接下来有多帅了

                    +

                    如若把Father和Child内的print类都换成public:

                    +
                    class FatherOuter{
                    //...
                    public void print(){
                    System.out.println("I am father!");
                    }
                    //...
                    }

                    class ChildOuter extends FatherOuter{
                    //...
                    public void print(){System.out.println("I am child!");}
                    }
                    /*结果:访问最新版本
                    I am child!*/
                    + +
                    //结论:内部类可以访问外部类所有不论私有还是公有的资源;会优先访问最新版本【父子类而言】
                    //子类声明为private的成员字段不能被从父类继承而来的方法访问到,只会访问能访问到的最新版本
                  10. +
                  11. 序列化versionID

                    ArrayList实现了java.io.Serializable接口,故而可以被序列化和反序列化,就需要有个序列化版本ID

                    +

                    What is a serialVersionUID and why should I use it?

                    +
                    +

                    这东西是用来在反序列化的时候保证收到的对象和发送的对象的类是相同的。If the receiver has loaded a class for the object that has a different serialVersionUID than that of the corresponding sender’s class, then deserialization will result in an InvalidClassException.

                    +

                    The field serialVersionUID must be static, final, and of type long.

                    +

                    If a serializable class does not explicitly declare a serialVersionUID, then the serialization runtime will calculate a default serialVersionUID value for that class based on various aspects of the class, as described in the Java(TM) Object Serialization Specification.

                    -

                    在这一章我们仅实现CRT几个关键的部分。虽然这个迷你CRT仅仅实现了为数不多的功能,但是它已经具备了CRT的关键功能:入口函数、初始化、堆管理、基本IO,甚至还将实现堆C++的new/delete、stream和string的支持。

                    -

                    本章主要分为两个部分,首先实现一个仅仅支持C语言的运行库,即传统意义上的CRT。其次,将为这个CRT加入一部分以支持C++语言的运行时特性。

                    -

                    相关代码放在github了,其实感觉差不多是按它写的抄了一遍。可以现在稍微整理下文件结构。

                    -
                      -
                    1. just for C

                      -

                      前面说到,CRT的作用是执行init和finit段、进行堆的管理、进行IO的封装管理以及提供各种标准C语言库。因而,我们可以分别用如下几个文件来实现这几个功能:

                      -
                        -
                      1. entry.c

                        -

                        用于实现入口函数mini_crt_entry。入口函数中主要要做:调用main之前的栈构造、堆初始化、IO初始化,最后调用main函数。main函数返回后,通过系统调用exit来杀死进程。

                      2. -
                      3. malloc.c

                        -

                        用于实现堆的管理,主要实现了mallocfree。使用了空闲链表的小内存管理法,实现简单。

                        +
                      4. transient

                        Java中transient关键字的详细总结

                        +
                        +

                        transient是短暂的意思。对于transient 修饰的成员变量,在类的实例对象的序列化处理过程中会被忽略。 因此,transient变量不会贯穿对象的序列化和反序列化,生命周期仅存于调用者的内存中而不会写到磁盘里进行持久化。

                        +

                        在持久化对象时,对于一些特殊的数据成员(如用户的密码,银行卡号等),我们不想用序列化机制来保存它。为了在一个特定对象的一个成员变量上关闭序列化,可以在这个成员变量前加上关键字transient。

                        +

                        注意static修饰的静态变量天然就是不可序列化的。一个静态变量不管是否被transient修饰,均不能被序列化(如果反序列化后类中static变量还有值,则值为当前JVM中对应static变量的值)。序列化保存的是对象状态,静态变量保存的是类状态,因此序列化并不保存静态变量。

                        +

                        使用场景
                        (1)类中的字段值可以根据其它字段推导出来,如一个长方形类有三个属性长度、宽度、面积,面积不需要序列化。
                        (2) 一些安全性的信息,一般情况下是不能离开JVM的。
                        (3)如果类中使用了Logger实例,那么Logger实例也是不需要序列化的

                        +
                        +

                        但其实还有一个问题,它这边源码对这个transient的注释是:non-private to simplify nested class access,“非私有以简化嵌套类访问”。问题是这个transient和类的公有还是私有有什么关系呢?

                        +

                        Why is the data array in java.util.ArrayList package-private?

                        +

                        其实没怎么看懂23333

                      5. -
                      6. stdio.c

                        -

                        用于实现IO封装,freadfwritefopenfclosefseek。实现简单,因而只是系统调用的封装

                        +
                      7. ArrayList的elementData虽然被transient修饰,但仍然能够序列化

                        ArrayList中elementData为何被transient修饰?

                      8. -
                      9. string.c

                        -

                        以字符串操作为例,提供的标准C语言库。

                        +
                      10. 关于static的空数组

                        EMPTY_ELEMENTDATA和DEFAULTCAPACITY_EMPTY_ELEMENTDATA这两个空数组可用于表示两种情况:

                        +

                        new ArrayList(0) ->EMPTY_ELEMENTDATA

                        +

                        new ArrayList() ->DEFAULTCAPACITY_EMPTY_ELEMENTDATA

                        +

                        之所以用静态,是提取了共性:不论是需要什么ArrayList,其空形态不都一样吗(

                        +

                        这样可以避免了制造对象的浪费。very good。

                      11. -
                      -

                      之后,我们将其以如下参数编译为静态库:

                      -
                      $ gcc -c -fno-builtin -nostdlib -fno-stack-protector entry.c malloc.c stdio.c string.c printf.c
                      $ ar -rs minicrt.a malloc.o printf.o stdio.o string.o
                      # 编译测试用例
                      $ gcc -m32 -c -ggdb -fno-builtin -nostdlib -fno-stack-protector test.c
                      - -

                      再指定mini_crt_entry为入口进行静态链接:

                      -
                      $ ld -m elf_i386 -static -e mini_crt_entry entry.o test.o minicrt.a -o test
                    2. -
                    3. C++

                      -

                      如果要实现对C++的支持,除了在上述基础上,我们还需增加以下几个内容:全局对象(cout)构造/析构的实现、new/delete、类的实现(string和iostream)。具体来说,会支持下面这个简单的代码:

                      -
                      #include "iostream"
                      #include "string"
                      using namespace std;

                      int main(int argc, char* argv[])
                      {
                      string* msg = new string("Hello World");
                      cout << *msg << endl;
                      delete msg;
                      return 0;
                      }
                      +
                    4. 关于扩容的连环计

                      我其实觉得不必写那么麻烦……

                      +

                      In the JAVA ArrayList source code, why does array expansion should be divided into ensureCapacityInternal and ensureCapacity two sides?

                      +

                      经过测试替换可得,确实可以像我那样写。

                      +
                      		//此处的ArrayList是魔改过的
                      //now the capacity is 0
                      ArrayList a = new ArrayList<>();
                      System.out.println(a.getCapacity());
                      //扩容到DEFAULT
                      a.add(new Student("Lily",15));
                      System.out.println(a.getCapacity());
                      addStudent(a);
                      System.out.println(a.getCapacity());
                      /*运行结果:
                      0
                      10
                      33
                      */
                    5. +
                    6. 关于clear我的写法

                      In the Java ArrayList source code, in the clear function, can my rewrite more efficient than the origin source code?

                      +
                      ArrayList a = new ArrayList<>();
                      addStudent(a);
                      long sum1=0;
                      for (int i=0;i<50000;i++){
                      long startTime = System.currentTimeMillis();
                      a.clear1();
                      addStudent(a);
                      long endTime = System.currentTimeMillis();
                      sum1+=endTime-startTime;
                      }
                      //sum1/=500;
                      System.out.println("clear1: "+sum1);
                      sum1=0;
                      for (int i=0;i<50000;i++){
                      long startTime = System.currentTimeMillis();
                      a.clear2();
                      addStudent(a);
                      long endTime = System.currentTimeMillis();
                      sum1+=endTime-startTime;
                      }
                      //sum1/=500;
                      System.out.println("clear2: "+sum1);
                      -

                      我们可以分步实现这些功能:

                      -
                        -
                      1. new/delete实现

                        -

                        简单地使用运算符重载功能即可:

                        -
                        void* operator new(unsigned int size);
                        void operator delete(void* p);
                      2. -
                      3. 类的实现

                        -

                        不多说

                        +

                        经测试发现不分伯仲()也确实差距应该很小2333

                        +

                        不过依照一个回答:

                        +
                        +

                        Your approach explicitly throws the backing array away. The existing implementation attempts to reuse it. So even if your approach is faster in isolation, in practice it will almost certainly be less performant. Since calling clear() is a sign you intend to reuse the ArrayList.

                        +
                        +

                        其实感觉我的clear说不定花销更大,毕竟要创建一个新对象(

                      4. -
                      5. 全局对象的构造/析构

                        -
                          -
                        1. 构造

                          -

                          全局对象的构造在entry中进行:

                          -
                          void mini_crt_entry(void)
                          {
                          ...
                          // 构造所有全局对象
                          do_global_ctors();
                          ret = main(argc,argv);
                          }
                          +
                        2. heap write traffic

                          What is heap write traffic and why it is required in ArrayList?

                          +

                          详见第二个答案。

                          + -

                          前文说过,在Linux中,每个.o文件的全局构造最后都会放在.ctor段。ld在链接阶段中将所有目标文件(包括用于标识.ctor段开始和结束的crtbegin.ocrtend.o)的.ctor段连在一起。所以,我们就需要实现三个文件:

                          -
                            -
                          1. ctors.c

                            -

                            主要是用于实现do_global_ctors()。既然都有.ctor段存在了,那么它的实现就很简单,就是遍历.ctor段的所有函数指针并且调用它们。

                            -
                            void run_hooks();
                            extern "C" void do_global_ctors()
                            {
                            run_hooks();
                            }
                            +

                            栈放在一级缓存,堆放在二级缓存

                            +

                            总之意思就是成员变量写在堆里,局部变量写在栈里,做

                            +
                            while (i != size && modCount == expectedModCount) {
                            consumer.accept((E) elementData[i++]);
                            // Update cursor while iterating
                            cursor = i;
                            }
                            -
                            void run_hooks()
                            {
                            const ctor_func *list = ctors_begin;
                            // 逐个调用ctors段里的东西
                            while ((int)*++list != -1) (**list)();
                            }
                          2. -
                          3. crtbegin.c

                            -

                            前文说到,按规定,ld将会以如下顺序连接.o文件:

                            -
                            ld crtbegin.o 其他文件 crtend.o -o test
                            +

                            比做

                            +
                            while (cursor != size && modCount == expectedModCount) {
                            consumer.accept((E) elementData[cursor++]);
                            }
                            -

                            因而,crtbegin.c.ctor段会被链接在第一个。其作用是标识.ctor函数指针的数量,将在链接时由ld计算并且填写。因而在这里,我们只需将其初始化为一个特殊值(-1)就行:

                            -
                            typedef void (*ctor_func)(void);

                            ctor_func ctors_begin[1] __attribute__((section(".ctors"))) = {
                            (ctor_func)-1
                            };
                          4. -
                          5. crtend.c

                            -

                            同样,crtend.c.ctor段标识着.ctor段的结束。因而我们也将其初始化为一个特殊值(-1):

                            -
                            typedef void (*ctor_func)(void);

                            // 转化-1为函数指针,标识结束
                            ctor_func crt_end[1] __attribute__((section(".ctors"))) = {
                            (ctor_func) - 1
                            };
                          6. -
                          +

                          花销更小

                        3. -
                        4. 析构

                          -

                          全局对象的析构同样在entry中进行:

                          -
                          void mini_crt_entry(void)
                          {
                          ...
                          ret = main(argc,argv);
                          exit(ret);
                          }

                          void exit(int exitCode)
                          {
                          // 执行atexit,完成所有finit钩子
                          mini_crt_call_exit_routine();
                          // 调用exit系统调用
                          asm( "movl %0,%%ebx \n\t"
                          "movl $1,%%eax \n\t"
                          "int $0x80 \n\t"
                          "hlt \n\t"::"m"(exitCode));
                          }
                          +
                        5. if (i >= elementData.length)

                          is i >= elementData.length in ArrayList::iterator redundant?

                          +
                          public E previous() {
                          checkForComodification();
                          int i = cursor - 1;
                          if (i < 0)
                          throw new NoSuchElementException();
                          Object[] elementData = ArrayList.this.elementData;
                          if (i >= elementData.length)
                          throw new ConcurrentModificationException();
                          cursor = i;
                          return (E) elementData[lastRet = i];
                          }
                          -

                          具体也是以链表形式管理所有的函数指针,在atexit中注册(加入链表),在mini_crt_call_exit_routine中真正调用,不多分析。

                          +

                          如果 user invoke trimToSize method ,就会导致在checkForComodification();if (i >= elementData.length)之间发生ArrayIndexOutOfBounds。而在if (i >= elementData.length)之后trim没有影响,因为我们的局部变量已经保存了原来的elementData,此时再trim只是修改成员变量的elementData,局部变量依然不变。

                        6. -
                        +
                      6. sublist不可序列化,且not cloneable
                        private class SubList extends AbstractList<E> implements RandomAccess 
                        + +

                        sublist没有extends Cloneable, java.io.Serializable这两个接口

                      7. -
                      +
                    7. parent和ArrayList.this

                      首先,这两个是同一个吗?其次,这俩是否是同一个跟sub的级数有关系吗,就比如一级sub都是同一个,多级sub就不是同一个了?

                      +

                      经过对ArrayList的一些public和以下代码的测试,得出结论:这两个只有在第一级sub的时候是同一个。parent指向直系父亲,ArrayList.this指向root父亲。

                      +
                      //In ArrayList:
                      public final AbstractList<E> parent;
                      public ArrayList getRoot(){return ArrayList.this;}
                      //In test main:
                      public static void main(String[] args) {
                      ArrayList a=new ArrayList();
                      addStudent(a);
                      System.out.println(a.hashCode());

                      ArrayList.SubList suba= (ArrayList.SubList) a.subList(1,7);
                      System.out.println(suba.getRoot().hashCode()+"\t"+suba.parent.hashCode());
                      System.out.println(suba.hashCode());

                      ArrayList.SubList subsuba=(ArrayList.SubList) suba.subList(2,4);
                      System.out.println(subsuba.getRoot().hashCode()+"\t"+subsuba.parent.hashCode());
                      System.out.println(subsuba.hashCode());

                      ArrayList.SubList subsubsuba=(ArrayList.SubList) subsuba.subList(0,1);
                      System.out.println(subsubsuba.getRoot().hashCode()+"\t"+subsubsuba.parent.hashCode());
                      }

                      /*输出结果:
                      779301330
                      779301330 779301330
                      -954172011
                      779301330 -954172011
                      -95519366
                      779301330 -95519366
                      */
                      + +

                      总之,ArrayList的sublist实现方式相当于串成了一条父子继承串,多级sub,至于这么干相比原来的只有两级父子关系的方法好在哪就不知道了

                    -

                    特辑:开发中遇到的链接小问题

                      -
                    1. 已经在LD_LIBRARY_PATH中加入某个静态库的路径,但是仍然报错error while loading shared libraries: libssl.so.1.1: cannot open shared object file: No such file or directory

                      -

                      解决方法:执行sudo ldconfig

                      -
                      -

                      ldconfig命令的作用就是将这些共享库的路径添加到动态链接器的缓存中,以便系统能够找到它们。

                      -

                      具体而言,ldconfig会检查默认的共享库路径(通常是/lib/usr/lib),以及在/etc/ld.so.conf/etc/ld.so.conf.d/目录中定义的其他路径。然后,它会更新动态链接器缓存,这样系统就知道在哪里查找共享库。

                      -

                      Q:也就是说ld不是像gcc找头文件,是根据-I选项规定的路径即时查找的,而是只根据缓存吗?所以尽管我们通过ld path环境变量设置了新查找路径,我们还是得手动刷新下ld缓存。

                      -

                      A:是的,你理解得很对。ldconfig主要是更新系统中的共享库缓存,而不是在每次程序运行时实时查找库文件的位置。动态链接器在运行时会根据缓存中的信息找到所需的共享库,以提高性能并避免在每次程序启动时重新搜索所有库。

                      -

                      当你通过LD_LIBRARY_PATH环境变量设置新的查找路径时,这只是告诉动态链接器在运行时应该额外搜索这些路径。然而,为了确保系统广泛地认识这些新的路径,以及其中的共享库,你需要运行ldconfig来更新缓存。

                      +

                      AbstractSequentialList(A)

                      +

                      提供顺序访问list的基本骨架

                      +

                      To implement a list the programmer needs only to extend this class and provide implementations for the listIterator and size methods. For an unmodifiable list, the programmer need only implement the list iterator’s hasNext, next, hasPrevious, previous and index methods.
                      For a modifiable list the programmer should additionally implement the list iterator’s set method. For a variable-size list the programmer should additionally implement the list iterator’s remove and add methods.

                      +

                      代码:

                      public abstract class AbstractSequentialList<E> extends AbstractList<E> {

                      protected AbstractSequentialList() {
                      }

                      public E get(int index) {
                      try {
                      //1
                      return listIterator(index).next();
                      } catch (NoSuchElementException exc) {
                      throw new IndexOutOfBoundsException("Index: "+index);
                      }
                      }

                      public E set(int index, E element) {
                      try {
                      ListIterator<E> e = listIterator(index);
                      E oldVal = e.next();
                      e.set(element);
                      return oldVal;
                      } catch (NoSuchElementException exc) {
                      throw new IndexOutOfBoundsException("Index: "+index);
                      }
                      }

                      public void add(int index, E element) {
                      try {
                      listIterator(index).add(element);
                      } catch (NoSuchElementException exc) {
                      throw new IndexOutOfBoundsException("Index: "+index);
                      }
                      }

                      public E remove(int index) {
                      try {
                      ListIterator<E> e = listIterator(index);
                      E outCast = e.next();
                      e.remove();
                      return outCast;
                      } catch (NoSuchElementException exc) {
                      throw new IndexOutOfBoundsException("Index: "+index);
                      }
                      }
                      // Bulk Operations

                      public boolean addAll(int index, Collection<? extends E> c) {
                      try {
                      boolean modified = false;
                      ListIterator<E> e1 = listIterator(index);
                      Iterator<? extends E> e2 = c.iterator();
                      while (e2.hasNext()) {
                      e1.add(e2.next());
                      modified = true;
                      }
                      return modified;
                      } catch (NoSuchElementException exc) {
                      throw new IndexOutOfBoundsException("Index: "+index);
                      }
                      }
                      // Iterators

                      public Iterator<E> iterator() {
                      return listIterator();
                      }

                      public abstract ListIterator<E> listIterator(int index);
                      }
                      + +

                      其中:

                        +
                      1. get和set方法通过Iterator实现

                        随机访问的AbstractList的iterator的方法借助了主类的get和set,跟这里正好反过来。但注意哈,下面的LinkedList实现把以上差不多所有的方法都重写了,因而get和set之类的方法,LinkedList并不是依靠迭代器的。

                      2. -
                      -]]> - - books - - - - 阅读JDK容器部分源码的心得体会1【Collection部分】 - /2022/10/16/%E9%98%85%E8%AF%BBJDK%E5%AE%B9%E5%99%A8%E9%83%A8%E5%88%86%E6%BA%90%E7%A0%81%E7%9A%84%E5%BF%83%E5%BE%97%E4%BD%93%E4%BC%9A1%E3%80%90Collection%E9%83%A8%E5%88%86%E3%80%91/ - -

                      idea 替换注释正则表达式/\*{1,2}[\s\S]*?\*/

                      -

                      typora 替换图片asset

                      -

                      \!\[.*\]\(D:\\aWorkStorage\\hexo\\blog\\source\\_posts\\阅读JDK容器部分源码的心得体会1【Collection部分】\\(.*)\.png\)

                      -

                      替换结果{% asset_img $1.png %}

                      +

                      LinkedList

                      +

                      双向链表,实现List和Deque

                      +

                      并发不安全。List list = Collections.synchronizedList(new LinkedList(…));

                      +

                      印象:漂亮的指针操作,以及好像很少抛出异常,还有很多很多繁琐的方法(

                      - - - +

                      代码:

                      public class LinkedList<E>
                      extends AbstractSequentialList<E>
                      implements List<E>, Deque<E>, Cloneable, java.io.Serializable
                      {
                      //怎么连size也是transient?这不是代表着该对象的信息吗(
                      transient int size = 0;

                      transient Node<E> first;

                      transient Node<E> last;

                      public LinkedList() {
                      }

                      public LinkedList(Collection<? extends E> c) {
                      //调用空构造器
                      this();
                      addAll(c);
                      }

                      //把e接在链表头
                      private void linkFirst(E e) {
                      final Node<E> f = first;
                      final Node<E> newNode = new Node<>(null, e, f);
                      first = newNode;
                      if (f == null)
                      last = newNode;
                      else
                      f.prev = newNode;
                      size++;
                      modCount++;
                      }

                      void linkLast(E e) {
                      final Node<E> l = last;
                      final Node<E> newNode = new Node<>(l, e, null);
                      last = newNode;
                      if (l == null)
                      first = newNode;
                      else
                      l.next = newNode;
                      size++;
                      modCount++;
                      }

                      // Inserts element e before non-null Node succ.
                      void linkBefore(E e, Node<E> succ) {
                      // assert succ != null;
                      final Node<E> pred = succ.prev;
                      final Node<E> newNode = new Node<>(pred, e, succ);
                      succ.prev = newNode;
                      if (pred == null)
                      first = newNode;
                      else
                      pred.next = newNode;
                      size++;
                      modCount++;
                      }

                      private E unlinkFirst(Node<E> f) {
                      // assert f == first && f != null;
                      //这里就只靠注释会不会危险了()不过也确实会自动帮我们抛出NullPointerException的
                      //而且我在想,不是first已经指向头结点了吗,那你为什么还要把头结点作为参数传进来。。。
                      final E element = f.item;
                      final Node<E> next = f.next;
                      f.item = null;
                      f.next = null; // help GC
                      first = next;
                      if (next == null)
                      last = null;
                      else
                      next.prev = null;
                      size--;
                      modCount++;
                      return element;
                      }

                      //3
                      private E unlinkLast(Node<E> l) {
                      // assert l == last && l != null;
                      final E element = l.item;
                      final Node<E> prev = l.prev;
                      l.item = null;
                      l.prev = null; // help GC
                      last = prev;
                      if (prev == null)
                      first = null;
                      else
                      prev.next = null;
                      size--;
                      modCount++;
                      return element;
                      }

                      E unlink(Node<E> x) {
                      // assert x != null;
                      final E element = x.item;
                      final Node<E> next = x.next;
                      final Node<E> prev = x.prev;

                      if (prev == null) {
                      first = next;
                      } else {
                      prev.next = next;
                      x.prev = null;
                      }

                      if (next == null) {
                      last = prev;
                      } else {
                      next.prev = prev;
                      x.next = null;
                      }

                      x.item = null;
                      size--;
                      modCount++;
                      return element;
                      }

                      public E getFirst() {
                      final Node<E> f = first;
                      if (f == null)
                      throw new NoSuchElementException();
                      return f.item;
                      }

                      public E getLast() {
                      final Node<E> l = last;
                      if (l == null)
                      throw new NoSuchElementException();
                      return l.item;
                      }

                      public E removeFirst() {
                      final Node<E> f = first;
                      if (f == null)
                      throw new NoSuchElementException();
                      return unlinkFirst(f);
                      }

                      public E removeLast() {
                      final Node<E> l = last;
                      if (l == null)
                      throw new NoSuchElementException();
                      return unlinkLast(l);
                      }

                      public void addFirst(E e) {linkFirst(e);}

                      public void addLast(E e) {linkLast(e);}

                      public boolean contains(Object o) {return indexOf(o) != -1;}

                      public int size() {return size;}

                      public boolean add(E e) {
                      linkLast(e);
                      return true;
                      }

                      public boolean remove(Object o) {
                      if (o == null) {
                      for (Node<E> x = first; x != null; x = x.next) {
                      if (x.item == null) {
                      unlink(x);
                      return true;
                      }
                      }
                      } else {
                      for (Node<E> x = first; x != null; x = x.next) {
                      if (o.equals(x.item)) {
                      unlink(x);
                      return true;
                      }
                      }
                      }
                      return false;
                      }

                      public boolean addAll(Collection<? extends E> c) {
                      return addAll(size, c);
                      }

                      public boolean addAll(int index, Collection<? extends E> c) {
                      checkPositionIndex(index);

                      Object[] a = c.toArray();
                      int numNew = a.length;
                      if (numNew == 0)
                      return false;

                      //分成两段
                      Node<E> pred, succ;
                      if (index == size) {
                      succ = null;
                      pred = last;
                      } else {
                      succ = node(index);
                      pred = succ.prev;
                      }

                      //往中间加料
                      for (Object o : a) {
                      @SuppressWarnings("unchecked") E e = (E) o;
                      Node<E> newNode = new Node<>(pred, e, null);
                      if (pred == null)
                      first = newNode;
                      else
                      pred.next = newNode;
                      pred = newNode;
                      }

                      //合起来
                      if (succ == null) {
                      last = pred;
                      } else {
                      pred.next = succ;
                      succ.prev = pred;
                      }

                      size += numNew;
                      modCount++;
                      return true;
                      }


                      public void clear() {
                      // 1
                      // Clearing all of the links between nodes is "unnecessary", but:
                      // - helps a generational GC if the discarded nodes inhabit
                      // more than one generation
                      // - is sure to free memory even if there is a reachable Iterator
                      for (Node<E> x = first; x != null; ) {
                      Node<E> next = x.next;
                      x.item = null;
                      x.next = null;
                      x.prev = null;
                      x = next;
                      }
                      first = last = null;
                      size = 0;
                      modCount++;
                      }

                      // Positional Access Operations

                      public E get(int index) {
                      checkElementIndex(index);
                      return node(index).item;
                      }

                      public E set(int index, E element) {
                      checkElementIndex(index);
                      Node<E> x = node(index);
                      E oldVal = x.item;
                      x.item = element;
                      return oldVal;
                      }

                      public void add(int index, E element) {
                      checkPositionIndex(index);

                      if (index == size)
                      linkLast(element);
                      else
                      linkBefore(element, node(index));
                      }

                      public E remove(int index) {
                      checkElementIndex(index);
                      return unlink(node(index));
                      }

                      Node<E> node(int index) {
                      // assert isElementIndex(index);
                      //所以为啥不能check一下?

                      //做了个小小的优化,如果在前半就从开头找,在后半就从最后往前找
                      if (index < (size >> 1)) {
                      Node<E> x = first;
                      for (int i = 0; i < index; i++)
                      x = x.next;
                      return x;
                      } else {
                      Node<E> x = last;
                      for (int i = size - 1; i > index; i--)
                      x = x.prev;
                      return x;
                      }
                      }

                      // Search Operations

                      public int indexOf(Object o) {...}

                      public int lastIndexOf(Object o) {...}

                      // Queue operations.
                      //部分省略

                      //always return true
                      public boolean offer(E e) {
                      return add(e);
                      }

                      // Deque operations
                      //省略

                      public boolean removeFirstOccurrence(Object o) {
                      //666
                      return remove(o);
                      }

                      public boolean removeLastOccurrence(Object o) {...}

                      public ListIterator<E> listIterator(int index) {
                      checkPositionIndex(index);
                      return new ListItr(index);
                      }

                      //非常聪明非常漂亮的指针操作
                      private class ListItr implements ListIterator<E> {
                      private Node<E> lastReturned;
                      private Node<E> next;
                      private int nextIndex;
                      private int expectedModCount = modCount;

                      ListItr(int index) {
                      // assert isPositionIndex(index);
                      next = (index == size) ? null : node(index);
                      nextIndex = index;
                      }

                      public boolean hasNext() {
                      return nextIndex < size;
                      }

                      public E next() {
                      checkForComodification();
                      if (!hasNext())
                      throw new NoSuchElementException();

                      lastReturned = next;
                      next = next.next;
                      nextIndex++;
                      return lastReturned.item;
                      }

                      public boolean hasPrevious() {
                      return nextIndex > 0;
                      }

                      public E previous() {
                      checkForComodification();
                      if (!hasPrevious())
                      throw new NoSuchElementException();

                      //注意这个next是内部类里的成员变量,last是外部类的成员变量
                      lastReturned = next = (next == null) ? last : next.prev;
                      nextIndex--;
                      return lastReturned.item;
                      }

                      public int nextIndex() {
                      return nextIndex;
                      }

                      public int previousIndex() {
                      return nextIndex - 1;
                      }

                      public void remove() {
                      checkForComodification();
                      if (lastReturned == null)
                      throw new IllegalStateException();

                      Node<E> lastNext = lastReturned.next;
                      unlink(lastReturned);
                      if (next == lastReturned)
                      next = lastNext;
                      else
                      nextIndex--;
                      lastReturned = null;
                      expectedModCount++;
                      }

                      public void set(E e) {
                      if (lastReturned == null)
                      throw new IllegalStateException();
                      checkForComodification();
                      lastReturned.item = e;
                      }

                      public void add(E e) {
                      checkForComodification();
                      lastReturned = null;
                      if (next == null)
                      linkLast(e);
                      else
                      linkBefore(e, next);
                      nextIndex++;
                      expectedModCount++;
                      }

                      public void forEachRemaining(Consumer<? super E> action) {
                      Objects.requireNonNull(action);
                      while (modCount == expectedModCount && nextIndex < size) {
                      action.accept(next.item);
                      lastReturned = next;
                      next = next.next;
                      nextIndex++;
                      }
                      checkForComodification();
                      }

                      final void checkForComodification() {
                      if (modCount != expectedModCount)
                      throw new ConcurrentModificationException();
                      }
                      }

                      //节点类,平平无奇链表捏
                      private static class Node<E> {
                      E item;
                      Node<E> next;
                      Node<E> prev;

                      Node(Node<E> prev, E element, Node<E> next) {
                      this.item = element;
                      this.next = next;
                      this.prev = prev;
                      }
                      }

                      public Iterator<E> descendingIterator() {
                      return new DescendingIterator();
                      }

                      // 2 降序迭代器
                      private class DescendingIterator implements Iterator<E> {
                      //借助升序迭代器实现
                      private final ListItr itr = new ListItr(size());

                      public boolean hasNext() {
                      return itr.hasPrevious();
                      }
                      public E next() {
                      return itr.previous();
                      }
                      public void remove() {
                      itr.remove();
                      }
                      }

                      @SuppressWarnings("unchecked")
                      private LinkedList<E> superClone() {
                      try {
                      return (LinkedList<E>) super.clone();
                      } catch (CloneNotSupportedException e) {
                      throw new InternalError(e);
                      }
                      }

                      public Object clone() {
                      LinkedList<E> clone = superClone();

                      // 初始化
                      clone.first = clone.last = null;
                      clone.size = 0;
                      clone.modCount = 0;

                      // Initialize clone with our elements
                      for (Node<E> x = first; x != null; x = x.next)
                      clone.add(x.item);

                      return clone;
                      }

                      public Object[] toArray() {...}

                      @SuppressWarnings("unchecked")
                      public <T> T[] toArray(T[] a) {...}

                      private static final long serialVersionUID = 876323262645176354L;

                      private void writeObject(java.io.ObjectOutputStream s)
                      throws java.io.IOException {...}

                      @SuppressWarnings("unchecked")
                      private void readObject(java.io.ObjectInputStream s)
                      throws java.io.IOException, ClassNotFoundException {...}

                      @Override
                      public Spliterator<E> spliterator() {
                      return new LLSpliterator<E>(this, -1, 0);
                      }

                      static final class LLSpliterator<E> implements Spliterator<E> {...}
                      //4
                      }
                      -

                      迭代器相关接口

                      Iterable(I)

                      /*实现这个接口的类可用于for-each循环*/

                      public interface Iterable<T> {

                      Iterator<T> iterator();

                      /*对Iterator内每个元素实施此操作,直到遍历完或者抛出异常。*/
                      default void forEach(Consumer<? super T> action) {
                      Objects.requireNonNull(action);
                      for (T t : this) {
                      action.accept(t);
                      }
                      }

                      default Spliterator<T> spliterator() {
                      return Spliterators.spliteratorUnknownSize(iterator(), 0);
                      }
                      }
                      +

                      其中

                        +
                      1. 关于分代GC

                        In the clear() function:

                        +
                        // Clearing all of the links between nodes is "unnecessary", but:
                        // - helps a generational GC if the discarded nodes inhabit
                        // more than one generation
                        // - is sure to free memory even if there is a reachable Iterator
                        +

                        还没看懂,插个眼

                        +
                      2. +
                      3. 降序迭代器

                        一切都反过来了,也没有升序迭代器恁多方法:

                        +

                        不支持foreach循环,只支持单向遍历,没有add set 只有remove。

                        +
                      4. +
                      5. 关于unlinkLast/First的参数问题

                        In Java LinkedList source code, why the unlinkFirst function should have a param pointing to the first node?

                        +

                        事实证明确实人家也觉得无参比较合理(

                        +
                      6. +
                      7. sublist

                        LinkedList用了从AbstractList继承来的sublist相关类和方法,没有特别的优化,其sublist不可序列化,且not cloneable。

                        +
                      8. +
                      9. transient的三个成员变量

                        In Java LinkedList, why the “first”, “last” and “size” variable are transient?

                        -

                        引入迭代器的目的是为了**统一**“对容器里的元素进行遍历”这一操作。

                        +

                        LinkedList provide its own method for serializing and de-serializing.

                        +

                        When serializing, it only writes the size, and the values of the list.

                        +

                        When deserializing, it reads the size, then build the list from scratch, each node for each value at a time.

                        +

                        If the author did not provide their own read and write methods, then they would need to make size, first, and last non-transient. They would also need to make the Node class serializable.

                        -

                        Iterator(I)

                        public interface Iterator<E> {
                        //之后还有没有元素
                        boolean hasNext();

                        //返回当前所指元素,并且将iterator指针下移
                        E next();

                        //This call can be made only if neither remove nor add have been called after the last call to next or previous.
                        default void remove() {
                        throw new UnsupportedOperationException("remove");
                        }

                        default void forEachRemaining(Consumer<? super E> action) {
                        Objects.requireNonNull(action);
                        while (hasNext())
                        action.accept(next());
                        }
                        }
                        - +

                        As all the member variables of java LinkedList is transient, what will be the use of implementing Serializable?

                        -

                        注意,iterator并不指向具体元素,它指向的是元素的间隙

                        -

                        这些^就是iterator所指的位置。这样就能理解iterator的next和previous了吧2333

                        - - -

                        而set()所修改的元素,是其上一次调用next()或者previous()方法所返回的元素。

                        -

                        What’s the meaning of this source code of interface Collection in JAVA?

                        +

                        Otherwise serializing would be by default, which would be recursive, and for a large list would easily blow the stack.

                        -

                        ListIterator(I)

                        public interface ListIterator<E> extends Iterator<E> {
                        boolean hasNext();
                        E next();
                        void remove();

                        //newly added as iterator below:
                        boolean hasPrevious();

                        E previous();

                        //返回后续调用 next 将返回的元素的索引。[应该就是当前元素索引]
                        int nextIndex();

                        int previousIndex();

                        //This call can be made only if neither remove nor add have been called after the last call to next or previous.
                        void set(E e);

                        //The element is inserted immediately before the element that would be returned by next, if any, and after the element that would be returned by previous, if any.
                        void add(E e);
                        }
                        - -
                        -

                        Note that the remove and set(Object) methods are not defined in terms of the cursor position; they are defined to operate on the last element returned by a call to next or previous().

                        +

                        意思就是序列化时不记录这些信息,反序列化时会重新构建。还有说如果用默认的序列化方法是递归的可能爆栈?还有我觉得有一点可能是如果把所有node都序列化了,可能反序列化后,本来分配到那段内存空间要是被占用了,但指针值不变还是会有问题?等待之后解答。

                        +
                      10. +
                      +

                      Vector

                      +

                      Unlike the new collection implementations, Vector is synchronized. If a thread-safe implementation is not needed, it is recommended to use ArrayList in place of Vector.

                      -

                      Collection

                      Collection(I)

                      代码

                      /*
                      无直接实现类,一般用于需要使用多态传递参数的场合
                      其所有子类都必须有两个构造器: a void (no arguments) constructor, which creates an empty collection, and a constructor with a single argument of type Collection, which creates a new collection with the same elements as its argument. 这点语法上不会强制实现(因为接口不能强制构造方法),但其实是约定成俗的。

                      */
                      public interface Collection<E> extends Iterable<E> {

                      int size();

                      boolean isEmpty();

                      boolean contains(Object o);

                      Iterator<E> iterator();

                      //有关“safe”的问题讨论见下
                      Object[] toArray();

                      /*
                      Like the toArray() method, this method acts as bridge between array-based and
                      collection-based APIs. Further, this method allows precise control over the runtime
                      type of the output array, and may, under certain circumstances, be used to save
                      allocation costs.

                      Suppose x is a collection known to contain only strings. The following code can be
                      used to dump the collection into a newly allocated array of String:
                      String[] y = x.toArray(new String[0]);

                      说明还是有类型限制的
                      ArrayStoreException – if the runtime type of the specified array is not a supertype
                      of the runtime type of every element in this collection
                      */
                      <T> T[] toArray(T[] a);

                      /*
                      给集合增加一个元素。
                      If a collection refuses to add a particular element for any reason other than that it
                      already contains the element, it must throw an exception (rather than returning
                      false).
                      */
                      boolean add(E e);

                      boolean remove(Object o);

                      boolean containsAll(Collection<?> c);

                      //重复了怎么办
                      boolean addAll(Collection<? extends E> c);

                      //重复的会全弄走吗
                      boolean removeAll(Collection<?> c);

                      /*
                      删除此集合中满足给定谓词的所有元素。
                      在迭代期间或由谓词引发的错误或运行时异常将转发给调用者。
                      @return true if any elements were removed
                      */
                      default boolean removeIf(Predicate<? super E> filter) {
                      //非空filter
                      Objects.requireNonNull(filter);
                      boolean removed = false;
                      //迭代该集合
                      final Iterator<E> each = iterator();
                      while (each.hasNext()) {
                      if (filter.test(each.next())) {
                      each.remove();
                      removed = true;
                      }
                      }
                      return removed;
                      }

                      /*
                      把集合c中没有的元素全部移除
                      @return true if this collection changed as a result of the call
                      */
                      boolean retainAll(Collection<?> c);

                      void clear();

                      boolean equals(Object o);

                      /*
                      Any class that overrides the Object.equals method must also override the
                      Object.hashCode method.
                      c1.equals(c2) 相当于 c1.hashCode()==c2.hashCode().
                      */
                      int hashCode();

                      @Override
                      default Spliterator<E> spliterator() {
                      return Spliterators.spliterator(this, 0);
                      }

                      default Stream<E> stream() {
                      return StreamSupport.stream(spliterator(), false);
                      }

                      default Stream<E> parallelStream() {
                      return StreamSupport.stream(spliterator(), true);
                      }
                      }
                      +

                      代码:

                      public class Vector<E>
                      extends AbstractList<E>
                      implements List<E>, RandomAccess, Cloneable, java.io.Serializable
                      {

                      protected Object[] elementData;

                      protected int elementCount;

                      protected int capacityIncrement;

                      private static final long serialVersionUID = -2767605614048989439L;

                      public Vector(int initialCapacity, int capacityIncrement) {
                      super();
                      if (initialCapacity < 0)
                      throw new IllegalArgumentException("Illegal Capacity: "+
                      initialCapacity);
                      this.elementData = new Object[initialCapacity];
                      this.capacityIncrement = capacityIncrement;
                      }

                      public Vector(int initialCapacity) {
                      this(initialCapacity, 0);
                      }

                      public Vector() {
                      //1
                      this(10);
                      }

                      public Vector(Collection<? extends E> c) {
                      Object[] a = c.toArray();
                      elementCount = a.length;
                      if (c.getClass() == ArrayList.class) {
                      elementData = a;
                      } else {
                      elementData = Arrays.copyOf(a, elementCount, Object[].class);
                      }
                      }

                      public synchronized void copyInto(Object[] anArray) {
                      System.arraycopy(elementData, 0, anArray, 0, elementCount);
                      }

                      //2
                      public synchronized void trimToSize() {
                      modCount++;
                      int oldCapacity = elementData.length;
                      if (elementCount < oldCapacity) {
                      elementData = Arrays.copyOf(elementData, elementCount);
                      }
                      }

                      public synchronized void ensureCapacity(int minCapacity) {
                      if (minCapacity > 0) {
                      modCount++;
                      ensureCapacityHelper(minCapacity);
                      }
                      }

                      private void ensureCapacityHelper(int minCapacity) {
                      // overflow-conscious code
                      if (minCapacity - elementData.length > 0)
                      grow(minCapacity);
                      }

                      private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

                      private void grow(int minCapacity) {...}

                      private static int hugeCapacity(int minCapacity) {...}

                      /*
                      Sets the size of this vector. If the new size is greater than the current size, new null items are added to the end of the vector. If the new size is less than the current size, all components at index newSize and greater are discarded.
                      */
                      public synchronized void setSize(int newSize) {
                      modCount++;
                      if (newSize > elementCount) {
                      ensureCapacityHelper(newSize);
                      } else {
                      for (int i = newSize ; i < elementCount ; i++)
                      //3 GC帮大忙,注意这里没有trim。
                      elementData[i] = null;
                      }
                      }
                      elementCount = newSize;
                      }

                      //capacity 、size 、isEmpty省略

                      //4
                      public Enumeration<E> elements() {
                      return new Enumeration<E>() {
                      int count = 0;
                      public boolean hasMoreElements() {
                      return count < elementCount;
                      }
                      public E nextElement() {
                      synchronized (Vector.this) {
                      if (count < elementCount) {
                      return elementData(count++);
                      }
                      }
                      throw new NoSuchElementException("Vector Enumeration");
                      }
                      };
                      }

                      //contains、indexof、lastIndexOf省略

                      public synchronized E elementAt(int index) {
                      if (index >= elementCount) {
                      throw new ArrayIndexOutOfBoundsException(index + " >= " + elementCount);
                      }

                      return elementData(index);
                      }

                      public synchronized E firstElement() {
                      if (elementCount == 0) {
                      throw new NoSuchElementException();
                      }
                      return elementData(0);
                      }

                      public synchronized E lastElement() {
                      if (elementCount == 0) {
                      throw new NoSuchElementException();
                      }
                      return elementData(elementCount - 1);
                      }

                      public synchronized void setElementAt(E obj, int index) {...}

                      public synchronized void removeElementAt(int index) {...}

                      public synchronized void insertElementAt(E obj, int index) {...}

                      public synchronized void addElement(E obj) {...}

                      public synchronized boolean removeElement(Object obj) {...}

                      //clear
                      public synchronized void removeAllElements() {
                      modCount++;
                      // Let gc do its work
                      for (int i = 0; i < elementCount; i++)
                      elementData[i] = null;

                      elementCount = 0;
                      }

                      public synchronized Object clone() {...}

                      public synchronized Object[] toArray() {
                      return Arrays.copyOf(elementData, elementCount);
                      }

                      @SuppressWarnings("unchecked")
                      public synchronized <T> T[] toArray(T[] a) {...}

                      // Positional Access Operations
                      //add set get remove clear省略

                      // Bulk Operations
                      // xxxAll省略

                      //用了AbstractList的equal、hashcode、tostring,省略

                      public synchronized List<E> subList(int fromIndex, int toIndex) {
                      return Collections.synchronizedList(super.subList(fromIndex, toIndex),
                      this);
                      }

                      protected synchronized void removeRange(int fromIndex, int toIndex) {...}

                      //跟AL不一样
                      private void readObject(ObjectInputStream in)
                      throws IOException, ClassNotFoundException {
                      ObjectInputStream.GetField gfields = in.readFields();
                      int count = gfields.get("elementCount", 0);
                      Object[] data = (Object[])gfields.get("elementData", null);
                      if (count < 0 || data == null || count > data.length) {
                      throw new StreamCorruptedException("Inconsistent vector internals");
                      }
                      elementCount = count;
                      elementData = data.clone();
                      }

                      private void writeObject(java.io.ObjectOutputStream s)
                      throws java.io.IOException {
                      final java.io.ObjectOutputStream.PutField fields = s.putFields();
                      final Object[] data;
                      synchronized (this) {
                      fields.put("capacityIncrement", capacityIncrement);
                      fields.put("elementCount", elementCount);
                      data = elementData.clone();
                      }
                      fields.put("elementData", data);
                      s.writeFields();
                      }

                      public synchronized ListIterator<E> listIterator(int index) {
                      if (index < 0 || index > elementCount)
                      throw new IndexOutOfBoundsException("Index: "+index);
                      return new ListItr(index);
                      }

                      public synchronized ListIterator<E> listIterator() {
                      return new ListItr(0);
                      }

                      public synchronized Iterator<E> iterator() {
                      return new Itr();
                      }

                      private class Itr implements Iterator<E> {...}

                      final class ListItr extends Itr implements ListIterator<E> {...}

                      @Override
                      public synchronized void forEach(Consumer<? super E> action) {...}

                      @Override
                      @SuppressWarnings("unchecked")
                      public synchronized boolean removeIf(Predicate<? super E> filter) {...}

                      @Override
                      @SuppressWarnings("unchecked")
                      public synchronized void replaceAll(UnaryOperator<E> operator) {...}

                      @SuppressWarnings("unchecked")
                      @Override
                      public synchronized void sort(Comparator<? super E> c) {...}

                      @Override
                      public Spliterator<E> spliterator() {
                      return new VectorSpliterator<>(this, null, 0, -1, 0);
                      }

                      static final class VectorSpliterator<E> implements Spliterator<E> {...}
                      }
                      -

                      其中,

                        -
                      1. “Bags or multisets (unordered collections that may contain duplicate elements) should implement this interface directly.”

                        Set是不允许重复的元素集合的ADT,【ADT:抽象数据结构】

                        -

                        Bag是元素集合的ADT,允许重复.

                        -

                        通常,任何包含元素的东西都是Collection.

                        -

                        任何允许重复的集合都是Bag,否则就是Set.

                        -

                        通过索引访问元素的任何包都是List.

                        -

                        在最后一个之后附加新元素并且具有从头部(第一索引)移除元素的方法的Bag是Queue.

                        -

                        在最后一个之后附加新元素并且具有从尾部(最后一个索引)移除元素的方法的Bag是Stack.
                        ————————————————
                        原文链接:https://blog.csdn.net/weixin_34239718/article/details/114036886

                        +

                        其中:

                          +
                        1. 默认容量

                          空构造器Vector()创建出来的默认容量为10,不同于ArrayList是个空集合。

                        2. -
                        3. “destructive” methods 和”undestructive” methods

                          这回答里写得很清楚:What are destructive and non-destructive methods in java?

                          +
                        4. 扩容操作

                          与ArrayList基本上是雷同的,就是都是synchronized。

                        5. -
                        6. recursive traversal of the collection
                          -

                          Some collection operations which perform recursive traversal of the collection may fail with an exception for self-referential instances where the collection directly or indirectly contains itself.

                          -
                          -

                          Java 8 vs Java 7 Collection Interface: self-referential instance

                          -

                          这个的第二个回答【较长的那个】写得很棒,较短的那个似乎是错误的。

                          -

                          正如描述所说的,“directly or indirectly contains itself”,回答里那个例子正是因为“indirectly contains itself”。

                          -

                          不仅仅是集合,每个Object都可能出现这样的错误(因为都包含有toString)

                          -

                          但是注意一点:

                          -
                          ArrayList l1 = new ArrayList();
                          l1.add(l1);
                          System.out.println(l1.toString());
                          //输出:[(this Collection)]
                          - -

                          这段代码是正常的,是因为ArrayList里面toString的实现:

                          -
                          sb.append('[');
                          for (;;) {
                          E e = it.next();
                          //注意此句
                          sb.append(e == this ? "(this Collection)" : e);
                          if (! it.hasNext())
                          return sb.append(']').toString();
                          sb.append(',').append(' ');
                          }
                          +
                        7. setsize不改变容量

                          实现中只是把东西设置为空,并没有trim,因而容量不变

                          +
                        8. +
                        9. Vector可生成枚举类
                        10. +
                        +

                        Set

                        Set(I)

                        代码:

                        /* 1
                        Note: 当set里包含可变的对象时,要多加小心。
                        The behavior of a set is not specified if the value of a set object is changed in a manner that affects equals comparisons.
                        A special case of this prohibition is that it is not permissible for a set to contain itself as an element.
                        */
                        public interface Set<E> extends Collection<E> {
                        // Query Operations
                        int size();

                        boolean isEmpty();

                        boolean contains(Object o);
                        /*Returns an iterator over the elements in this set.
                        The elements are returned in no particular order
                        (unless this set is an instance of some class that provides a guarantee).*/
                        Iterator<E> iterator();

                        Object[] toArray();

                        <T> T[] toArray(T[] a);

                        // Modification Operations

                        /*
                        If this set already contains the element,
                        the call leaves the set unchanged and returns false.
                        */
                        boolean add(E e);

                        boolean remove(Object o);

                        // Bulk Operations

                        /*
                        Returns true if this set contains all of the elements of the specified collection.
                        If the specified collection is also a set,
                        this method returns true if it is a subset of this set.[这个subset的定义有点意思]
                        */
                        boolean containsAll(Collection<?> c);

                        //2
                        //取并集
                        boolean addAll(Collection<? extends E> c);

                        //取交集
                        boolean retainAll(Collection<?> c);

                        //集合差
                        boolean removeAll(Collection<?> c);

                        void clear();

                        // Comparison and hashing

                        boolean equals(Object o);

                        int hashCode();

                        @Override
                        default Spliterator<E> spliterator() {
                        return Spliterators.spliterator(this, Spliterator.DISTINCT);
                        }
                        }
                        -

                        规避了这种风险。

                        -

                        下面的hashcode是不正常的,因为hashcode实现没有规避这种风险。

                        +

                        其中:

                          +
                        1. set中包含可变对象

                          需要规避可修改对象,使其与集合中另一个元素重复的问题

                          +

                          详见此:

                          +

                          Java HashSet contains duplicates if contained element is modified

                          +
                          +

                          The correct solution is to stick to the contract of Set and not modify objects after adding them to the collection.

                          +

                          You can avoid this problem by either:

                          +
                            +
                          • using an immutable type for your set elements,
                          • +
                          • making a copy of the objects as you put them into the set and / or pull them out of the set,
                          • +
                          • writing your code so that it “knows“ not to change the objects for the duration …
                          • +
                          +

                          From the perspective of correctness and robustness, the first option is clearly best.

                          +
                        2. -
                        3. 关于toArray的讨论
                          For toArray() :
                          The returned array will be "safe" in that no references to it are maintained by this collection. (In other words, this method must allocate a new array even if this collection is backed by an array). The caller is thus free to modify the returned array.
                          +
                        4. 集合操作

                          set抽象自数学的集合,因此有很多对应的集合操作:

                          +
                          +

                          addAll ∪

                          +

                          retainAll ∩

                          +

                          removeAll -

                          +
                          +
                        5. +
                        +

                        AbstratcSet(A)

                        +

                        Note that this class does not override any of the implementations from the AbstractCollection class. It merely adds implementations for equals and hashCode.

                        +
                        +

                        代码:

                        public abstract class AbstractSet<E> extends AbstractCollection<E> implements Set<E> {

                        protected AbstractSet() {
                        }

                        // Comparison and hashing

                        public boolean equals(Object o) {
                        if (o == this)
                        return true;

                        if (!(o instanceof Set))
                        return false;
                        Collection<?> c = (Collection<?>) o;
                        if (c.size() != size())
                        return false;
                        try {
                        return containsAll(c);
                        } catch (ClassCastException unused) {
                        return false;
                        } catch (NullPointerException unused) {
                        return false;
                        }
                        }

                        public int hashCode() {
                        int h = 0;
                        Iterator<E> i = iterator();
                        while (i.hasNext()) {
                        E obj = i.next();
                        if (obj != null)
                        h += obj.hashCode();
                        }
                        return h;
                        }

                        //不大明白为啥要修改实现,用AbstractCollection的不好吗
                        public boolean removeAll(Collection<?> c) {
                        Objects.requireNonNull(c);
                        boolean modified = false;

                        if (size() > c.size()) {
                        for (Iterator<?> i = c.iterator(); i.hasNext(); )
                        modified |= remove(i.next());
                        } else {
                        for (Iterator<?> i = iterator(); i.hasNext(); ) {
                        if (c.contains(i.next())) {
                        i.remove();
                        modified = true;
                        }
                        }
                        }
                        return modified;
                        }
                        }
                        -
                        List<String> list = Arrays.asList("foo", "bar", "baz");
                        String[] array = list.toArray(new String[0]);
                        array[0] = "qux";
                        //修改数组不会修改列表
                        System.out.println(list.get(0)); // still "foo"
                        list.set(0,"haha");
                        //修改列表也跟修改数组无关了
                        System.out.println(list.get(0)+array[0]);
                        +

                        HashSet

                        +

                        In particular, it does not guarantee that the order will remain constant over time.

                        +

                        Iterating over this set requires time proportional to the sum of the HashSet instance’s size (the number of elements) plus the “capacity” of the backing HashMap instance (the number of buckets).

                        +

                        Thus, it’s very important not to set the initial capacity too high (or the load factor too low) if iteration performance is important.

                        +

                        This implementation is not synchronized. Set s = Collections.synchronizedSet(new HashSet(...));

                        +
                        +

                        代码:

                        public class HashSet<E>
                        extends AbstractSet<E>
                        implements Set<E>, Cloneable, java.io.Serializable
                        {
                        static final long serialVersionUID = -5024744406713321676L;

                        //通过hashmap实现
                        //map不可序列化
                        private transient HashMap<E,Object> map;

                        // Dummy value to associate with an Object in the backing Map
                        // 1
                        private static final Object PRESENT = new Object();

                        //Constructs a new, empty set;
                        //the backing HashMap instance has default initial capacity (16)
                        //and load factor (0.75).
                        public HashSet() {
                        map = new HashMap<>();
                        }

                        //The HashMap is created with default load factor (0.75)
                        //and an initial capacity sufficient to contain the elements in c
                        public HashSet(Collection<? extends E> c) {
                        //看来这个load factor=size/0.75
                        map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
                        addAll(c);
                        }

                        public HashSet(int initialCapacity, float loadFactor) {
                        map = new HashMap<>(initialCapacity, loadFactor);
                        }

                        public HashSet(int initialCapacity) {
                        map = new HashMap<>(initialCapacity);
                        }

                        //This package private constructor is only used by LinkedHashSet.
                        //@param: dummy – ignored (distinguishes this constructor from other constructor.)
                        HashSet(int initialCapacity, float loadFactor, boolean dummy) {
                        //此处为LinkeHashMap
                        map = new LinkedHashMap<>(initialCapacity, loadFactor);
                        }

                        public Iterator<E> iterator() {return map.keySet().iterator();}

                        public int size() {return map.size();}

                        public boolean isEmpty() {return map.isEmpty();}

                        public boolean contains(Object o) {return map.containsKey(o);}

                        //@return true if this set did not already contain the specified element
                        //map.put返回已有的oldValue,返回空表示没有oldValue,插入成功;否则失败
                        public boolean add(E e) {
                        return map.put(e, PRESENT)==null;
                        }

                        public boolean remove(Object o) {
                        return map.remove(o)==PRESENT;
                        }

                        public void clear() {
                        map.clear();
                        }

                        @SuppressWarnings("unchecked")
                        public Object clone() {
                        try {
                        HashSet<E> newSet = (HashSet<E>) super.clone();
                        newSet.map = (HashMap<E, Object>) map.clone();
                        return newSet;
                        } catch (CloneNotSupportedException e) {
                        throw new InternalError(e);
                        }
                        }

                        private void writeObject(java.io.ObjectOutputStream s)
                        throws java.io.IOException {...}

                        private void readObject(java.io.ObjectInputStream s)
                        throws java.io.IOException, ClassNotFoundException {...}

                        public Spliterator<E> spliterator() {
                        return new HashMap.KeySpliterator<E,Object>(map, 0, -1, 0, 0);
                        }
                        }
                        -
                        ArrayList<Student> a = new ArrayList<>();
                        a.add(new Student("Sarah",17));
                        Student[] s = a.toArray(new Student[0]);
                        //换一个引用对象
                        s[0]=new Student("Lily",20);
                        System.out.println(a.get(0)==s[0]);//false
                        +

                        其中:

                          +
                        1. PRESENT

                          正如它的解释:

                          +
                          +

                          Dummy value to associate with an Object in the backing Map

                          +
                          +

                          set 以map作为内部支持,其实主要用的是map对于key的高效去重。也就是说,set其实只需要用map的key这一半就好了。所以我们另一半value就都统一用一个new Object【也就是PRESENT】来统一就行。

                          +

                          不得不说这点很聪明,值得学习。

                          +
                          private static final Object PRESENT = new Object();
                          public boolean add(E e) {
                          return map.put(e, PRESENT)==null;
                          }
                          public boolean remove(Object o) {
                          return map.remove(o)==PRESENT;
                          }
                        2. +
                        +

                        SortedSet(I)

                        +

                        A Set that further provides a total ordering on its elements.

                        +

                        The set’s iterator will traverse the set in ascending element order.

                        +

                        All elements inserted into a sorted set must implement the Comparable interface (or be accepted by the specified comparator).否则导致ClassCastException

                        +

                        Note that the ordering maintained by a sorted set (whether or not an explicit comparator is provided) must be consistent with equals if the sorted set is to correctly implement the Set interface.

                        +

                        【consistent with equals:if and only if c.compare(e1, e2)==0 has the same boolean value as e1.equals(e2) for every e1 and e2 in S.】

                        +

                        //2

                        +

                        Note: several methods return subsets with restricted ranges. 区间是前闭后开的。

                        +

                        If you need a closed range , and the element type allows for calculation of the successor of a given value, merely request the subrange from lowEndpoint to successor(highEndpoint).

                        +

                        For example, suppose that s is a sorted set of strings. [low,hight]
                        SortedSet<String> sub = s.subSet(low, high+"\0");
                        A similar technique can be used to generate an open range (low,hight)
                        SortedSet<String> sub = s.subSet(low+"\0", high);

                        +

                        66666

                        +
                        +

                        代码

                        public interface SortedSet<E> extends Set<E> {
                        //null if this set uses the natural ordering
                        Comparator<? super E> comparator();
                        //1
                        SortedSet<E> subSet(E fromElement, E toElement);

                        SortedSet<E> headSet(E toElement);

                        SortedSet<E> tailSet(E fromElement);

                        E first();

                        E last();

                        @Override
                        default Spliterator<E> spliterator() {
                        return new Spliterators.IteratorSpliterator<E>(
                        this, Spliterator.DISTINCT | Spliterator.SORTED | Spliterator.ORDERED) {
                        @Override
                        public Comparator<? super E> getComparator() {
                        return SortedSet.this.comparator();
                        }
                        };
                        }
                        }
                        -
                        ArrayList<Student> a = new ArrayList<>();
                        a.add(new Student("Sarah",17));
                        Student[] s = a.toArray(new Student[0]);
                        //修改引用对象
                        s[0].name = "Lily";
                        System.out.println(a.get(0).name);//Lily
                        +

                        其中:

                          +
                        1. subset

                          只有sorted set才有subset,想想也确实

                          +
                          /*
                          Throws:
                          ClassCastException – if fromElement and toElement cannot be compared to one another using this set's comparator
                          NullPointerException – if fromElement or toElement is null and this set does not permit null elements
                          IllegalArgumentException – if fromElement is greater than toElement; or fromElement or toElement lies outside the bounds of the restricted range of the set
                          */
                          //The returned set will throw an IllegalArgumentException
                          //on an attempt to insert an element outside its range.
                          SortedSet<E> subSet(E fromElement, E toElement);
                          -

                          这也就说明,toArray()实际上是把list的元素复制一份弄成array,直接把值粘贴进去。

                          -

                          对于引用对象,list的元素实际上应该存的是对象在堆中的地址。所谓的“安全”指的是,修改array中的元素的值【也即对象地址】,也就是换一个气球牵,是不会影响原来list的元素的值的。

                          -

                          因而,对于样例1和2,我们其实给array的元素换了个气球牵,或者是把list换了个气球牵,相互对象不同,没什么影响。

                          -

                          对于样例3,我们修改了list和array共同指向的对象【就像C语言的指针那样】

                          -

                          以上参考自What does “Safe” mean in the Collections.toArray() JavaDoc?

                          +

                          以及注意此处是Element,不是Index

                        2. -
                        3. 关于default关键字

                          java中default关键字

                          -

                          starkoverflow关于为什么要设立default的讨论:

                          -

                          What is the purpose of the default keyword in Java

                          -

                          Default methods were added to Java 8 primarily to support lambda expressions.

                          +
                        4. subSet(low, high+”\0”);

                          为什么加个”\0”就可以,具体可以看看这个:

                          +

                          Adding “\0” to a subset range end

                          +

                          原因就是sub的这个range取的是在此区间的元素,low和high这两个param不一定要包含在这个set里面。因此,按照set的排序,high+”\0”比high大,因而high就落入此区间,也就可以被包含在range中了。

                        -

                        AbstractCollection(A)

                        -

                        To implement an unmodifiable collection, the programmer needs only to extend this class and provide implementations for the iterator and size methods.
                        To implement a modifiable collection, the programmer must additionally[也要搞上面的] override this class’s add method (which otherwise throws an UnsupportedOperationException), and the iterator returned by the iterator method must additionally implement its remove method.

                        +
                        +

                        比起sorted set,navigable set最特殊的点在于它提供了对某一元素附近元素的导航。

                        +

                        Method usage

                        +

                        lower less than最大的,比所给ele小的元素

                        +

                        floor less than or equal最大的,比所给ele小或者等于的元素

                        +

                        ceiling greater than or equal最小的,比所给ele大或者等于的元素

                        +

                        higher greater than最小的,比所给ele大的元素

                        +

                        The descendingSet method returns a view of the set with the senses of all relational and directional methods inverted.

                        +

                        This interface additionally defines methods pollFirst and pollLast that return and remove the lowest and highest element, if one exists, else returning null. 有点堆的感觉

                        +

                        Methods subSet, headSet, and tailSet differ from the like-named SortedSet methods in accepting additional arguments describing whether lower and upper bounds are inclusive versus exclusive.

                        -
                        public abstract class AbstractCollection<E> implements Collection<E> {

                        //唯一的构造函数。 (用于子类构造函数的调用,通常是隐式的。)
                        protected AbstractCollection() {
                        }

                        // 查询操作
                        public abstract Iterator<E> iterator();
                        public abstract int size();
                        public boolean isEmpty() {
                        return size() == 0;
                        }

                        public boolean contains(Object o) {
                        Iterator<E> it = iterator();
                        if (o==null) {
                        while (it.hasNext())
                        if (it.next()==null)
                        return true;
                        } else {
                        while (it.hasNext())
                        if (o.equals(it.next()))
                        return true;
                        }
                        return false;
                        }


                        public Object[] toArray() {
                        // Estimate size of array; be prepared to see more or fewer elements
                        Object[] r = new Object[size()];
                        Iterator<E> it = iterator();
                        for (int i = 0; i < r.length; i++) {
                        if (! it.hasNext()) // fewer elements than expected
                        return Arrays.copyOf(r, i);
                        r[i] = it.next();
                        }
                        return it.hasNext() ? finishToArray(r, it) : r;
                        }


                        @SuppressWarnings("unchecked")
                        public <T> T[] toArray(T[] a) {
                        // Estimate size of array; be prepared to see more or fewer elements
                        int size = size();
                        //通过反射得到a同类型的数组实例
                        T[] r = a.length >= size ? a :
                        (T[])java.lang.reflect.Array
                        .newInstance(a.getClass().getComponentType(), size);
                        Iterator<E> it = iterator();

                        for (int i = 0; i < r.length; i++) {
                        if (! it.hasNext()) { // fewer elements than expected
                        if (a == r) {
                        r[i] = null; // null-terminate用null来标记数组结束。但如果数组里也有null该怎么办?从前面的contains来看也是有可能的
                        } else if (a.length < i) {
                        return Arrays.copyOf(r, i);
                        } else {
                        System.arraycopy(r, 0, a, 0, i);
                        if (a.length > i) {
                        a[i] = null;
                        }
                        }
                        return a;
                        }
                        r[i] = (T)it.next();
                        }
                        // more elements than expected
                        return it.hasNext() ? finishToArray(r, it) : r;
                        }

                        /*
                        为啥要-8?
                        Some VMs reserve some header words in an array. Attempts to allocate larger arrays may result in OutOfMemoryError: Requested array size exceeds VM limit
                        好像是因为要给8个字节保留作为数组的头标题。
                        */
                        private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;


                        //Reallocates the array being used within toArray when the iterator returned more elements than expected, and finishes filling it from the iterator.
                        @SuppressWarnings("unchecked")
                        private static <T> T[] finishToArray(T[] r, Iterator<?> it) {
                        int i = r.length;
                        while (it.hasNext()) {
                        int cap = r.length;
                        //说明此时需要扩容
                        if (i == cap) {
                        //每次扩当前大小的1/2
                        int newCap = cap + (cap >> 1) + 1;
                        // overflow-conscious code 溢出
                        if (newCap - MAX_ARRAY_SIZE > 0)
                        //cap+1 扩容后至少应该比原大小大1
                        newCap = hugeCapacity(cap + 1);
                        //申请一个船新数组空间
                        r = Arrays.copyOf(r, newCap);
                        }
                        r[i++] = (T)it.next();
                        }
                        // 如果过度扩容了就缩小回刚刚好
                        return (i == r.length) ? r : Arrays.copyOf(r, i);
                        }

                        private static int hugeCapacity(int minCapacity) {
                        //如果最小扩容也失败,说明要的东西太多了,救不了
                        if (minCapacity < 0) // overflow
                        throw new OutOfMemoryError
                        ("Required array size too large");
                        //否则
                        return (minCapacity > MAX_ARRAY_SIZE) ?
                        Integer.MAX_VALUE :
                        MAX_ARRAY_SIZE;
                        }

                        //修改操作

                        public boolean add(E e) {
                        throw new UnsupportedOperationException();
                        }

                        public boolean remove(Object o) {
                        Iterator<E> it = iterator();
                        if (o==null) {
                        while (it.hasNext()) {
                        if (it.next()==null) {
                        it.remove();
                        return true;
                        }
                        }
                        } else {
                        while (it.hasNext()) {
                        if (o.equals(it.next())) {
                        it.remove();
                        return true;
                        }
                        }
                        }
                        return false;
                        }

                        //批量操作
                        //O(n^2)
                        public boolean containsAll(Collection<?> c) {
                        for (Object e : c)
                        if (!contains(e))
                        return false;
                        return true;
                        }

                        public boolean addAll(Collection<? extends E> c) {
                        boolean modified = false;
                        for (E e : c)
                        if (add(e))
                        modified = true;
                        return modified;
                        }

                        public boolean removeAll(Collection<?> c) {
                        Objects.requireNonNull(c);
                        boolean modified = false;
                        Iterator<?> it = iterator();
                        while (it.hasNext()) {
                        if (c.contains(it.next())) {
                        it.remove();
                        modified = true;
                        }
                        }
                        return modified;
                        }

                        public boolean retainAll(Collection<?> c) {
                        Objects.requireNonNull(c);
                        boolean modified = false;
                        Iterator<E> it = iterator();
                        while (it.hasNext()) {
                        if (!c.contains(it.next())) {
                        it.remove();
                        modified = true;
                        }
                        }
                        return modified;
                        }

                        public void clear() {
                        Iterator<E> it = iterator();
                        while (it.hasNext()) {
                        it.next();
                        it.remove();
                        }
                        }

                        //字符串操作

                        public String toString() {
                        Iterator<E> it = iterator();
                        if (! it.hasNext())
                        return "[]";

                        StringBuilder sb = new StringBuilder();
                        sb.append('[');
                        for (;;) {
                        E e = it.next();
                        sb.append(e == this ? "(this Collection)" : e);
                        if (! it.hasNext())
                        return sb.append(']').toString();
                        sb.append(',').append(' ');
                        }
                        }
                        }
                        - -

                        其中:

                        -
                          -
                        1. 在toArray方法中,为什么需要写这么奇怪的代码?

                          what’s the usage of the code in the implementation of AbstractCollection’s toArray Method

                          -
                          Yes, you're right, as the javadoc sais, this method is prepared to return correctlly even if the Collection has been modified in the mean time.【并发安全】 That's why the initial size is just a hint. The usage of the iterator also ensures avoidance from the "concurrent modification" exception.
                        2. -
                        -

                        Queue

                        Queue(I)

                        - +

                        代码:

                        public interface NavigableSet<E> extends SortedSet<E> {
                        //Returns the greatest element in this set strictly less than the given element
                        E lower(E e);

                        E floor(E e);

                        E ceiling(E e);

                        E higher(E e);

                        //Removes the first (lowest) element, or returns null if this set is empty.
                        E pollFirst();

                        E pollLast();

                        //in ascending order
                        Iterator<E> iterator();

                        /*
                        The returned set has an ordering equivalent to
                        Collections.reverseOrder(comparator()).
                        The expression s.descendingSet().descendingSet()
                        returns a view of s essentially equivalent(基本等价) to s
                        */
                        NavigableSet<E> descendingSet();

                        //Equivalent in effect to descendingSet().iterator()
                        Iterator<E> descendingIterator();

                        NavigableSet<E> subSet(E fromElement, boolean fromInclusive,
                        E toElement, boolean toInclusive);

                        NavigableSet<E> headSet(E toElement, boolean inclusive);

                        NavigableSet<E> tailSet(E fromElement, boolean inclusive);

                        SortedSet<E> subSet(E fromElement, E toElement);

                        SortedSet<E> headSet(E toElement);

                        SortedSet<E> tailSet(E fromElement);
                        }
                        -

                        The Queue interface does not define the blocking queue methods, which are common in concurrent programming. These methods, which wait for elements to appear or for space to become available, are defined in the java.util.concurrent.BlockingQueue interface, which extends this interface.

                        -

                        Queue implementations generally do not define element-based versions of methods equals and hashCode【就是不会像之前的list一样遍历一遍通过单个元素的hashcode计算整体的hashcode】 but instead inherit the identity based versions from class Object【hashcode由对象决定】, because element-based equality is not always well-defined for queues with the same elements but different ordering properties.

                        -

                        Each of these methods exists in two forms: one throws an exception if the operation fails, the other returns a special value (either null or false, depending on the operation). 容量受限的队列推荐使用第二种form

                        +

                        TreeSet

                        +

                        This implementation provides guaranteed log(n) time cost for the basic operations (add, remove and contains).

                        +

                        Note that this implementation is not synchronized.

                        +

                        SortedSet s = Collections.synchronizedSortedSet(new TreeSet(...));

                        -

                        代码:

                        public interface Queue<E> extends Collection<E> {
                        /*
                        The offer method inserts an element if possible, otherwise returning false.
                        不同于Collection的add方法,offer添加失败时不会抛出异常,而是直接return false
                        */
                        boolean add(E e);
                        boolean offer(E e);
                        /*
                        The remove() and poll() methods differ only in their behavior
                        when the queue is empty:
                        the remove() method throws an exception, while the poll() method returns null.
                        */
                        E remove();
                        E poll();
                        /*
                        The element() and peek() methods return,
                        but do not remove, the head of the queue.
                        */
                        E element();
                        E peek();
                        }
                        - -

                        Deque(I)

                        -

                        双端队列。

                        -

                        The name deque is short for “double ended queue” and is usually pronounced “deck”.

                        - - -

                        This interface provides two methods to remove interior elements, removeFirstOccurrence and removeLastOccurrence.

                        - +

                        代码:

                        public class TreeSet<E> extends AbstractSet<E>
                        implements NavigableSet<E>, Cloneable, java.io.Serializable
                        {

                        //依然用了个map
                        private transient NavigableMap<E,Object> m;

                        // Dummy value to associate with an Object in the backing Map
                        private static final Object PRESENT = new Object();

                        TreeSet(NavigableMap<E,Object> m) {
                        this.m = m;
                        }

                        public TreeSet() {
                        this(new TreeMap<E,Object>());
                        }

                        public TreeSet(Comparator<? super E> comparator) {
                        this(new TreeMap<>(comparator));
                        }

                        public TreeSet(Collection<? extends E> c) {
                        this();
                        addAll(c);
                        }

                        public TreeSet(SortedSet<E> s) {
                        this(s.comparator());
                        addAll(s);
                        }

                        public Iterator<E> iterator() {
                        return m.navigableKeySet().iterator();
                        }

                        public Iterator<E> descendingIterator() {
                        return m.descendingKeySet().iterator();
                        }

                        //确实直接让map倒序就可以了
                        public NavigableSet<E> descendingSet() {
                        return new TreeSet<>(m.descendingMap());
                        }

                        public int size() {
                        return m.size();
                        }

                        public boolean isEmpty() {
                        return m.isEmpty();
                        }

                        public boolean contains(Object o) {
                        return m.containsKey(o);
                        }

                        public boolean add(E e) {
                        return m.put(e, PRESENT)==null;
                        }

                        public boolean remove(Object o) {
                        return m.remove(o)==PRESENT;
                        }

                        public void clear() {
                        m.clear();
                        }

                        public boolean addAll(Collection<? extends E> c) {
                        // Use linear-time version if applicable
                        if (m.size()==0 && c.size() > 0 &&
                        c instanceof SortedSet &&
                        m instanceof TreeMap) {
                        SortedSet<? extends E> set = (SortedSet<? extends E>) c;
                        TreeMap<E,Object> map = (TreeMap<E, Object>) m;
                        Comparator<?> cc = set.comparator();
                        Comparator<? super E> mc = map.comparator();
                        if (cc==mc || (cc != null && cc.equals(mc))) {
                        map.addAllForTreeSet(set, PRESENT);
                        return true;
                        }
                        }
                        return super.addAll(c);
                        }

                        public NavigableSet<E> subSet(E fromElement, boolean fromInclusive,
                        E toElement, boolean toInclusive) {
                        return new TreeSet<>(m.subMap(fromElement, fromInclusive,
                        toElement, toInclusive));
                        }

                        public NavigableSet<E> headSet(E toElement, boolean inclusive) {
                        return new TreeSet<>(m.headMap(toElement, inclusive));
                        }

                        public NavigableSet<E> tailSet(E fromElement, boolean inclusive) {
                        return new TreeSet<>(m.tailMap(fromElement, inclusive));
                        }

                        public SortedSet<E> subSet(E fromElement, E toElement) {
                        return subSet(fromElement, true, toElement, false);
                        }

                        public SortedSet<E> headSet(E toElement) {
                        return headSet(toElement, false);
                        }

                        //注意此处为true。严格遵循左闭右开
                        public SortedSet<E> tailSet(E fromElement) {
                        return tailSet(fromElement, true);
                        }

                        public Comparator<? super E> comparator() {
                        return m.comparator();
                        }

                        public E first() {
                        return m.firstKey();
                        }

                        public E last() {
                        return m.lastKey();
                        }

                        // NavigableSet API methods

                        public E lower(E e) {
                        return m.lowerKey(e);
                        }

                        public E floor(E e) {
                        return m.floorKey(e);
                        }

                        public E ceiling(E e) {
                        return m.ceilingKey(e);
                        }

                        public E higher(E e) {
                        return m.higherKey(e);
                        }

                        public E pollFirst() {
                        Map.Entry<E,?> e = m.pollFirstEntry();
                        return (e == null) ? null : e.getKey();
                        }

                        public E pollLast() {
                        Map.Entry<E,?> e = m.pollLastEntry();
                        return (e == null) ? null : e.getKey();
                        }

                        @SuppressWarnings("unchecked")
                        public Object clone() {
                        TreeSet<E> clone;
                        try {
                        clone = (TreeSet<E>) super.clone();
                        } catch (CloneNotSupportedException e) {
                        throw new InternalError(e);
                        }

                        clone.m = new TreeMap<>(m);
                        return clone;
                        }

                        private void writeObject(java.io.ObjectOutputStream s)
                        throws java.io.IOException {...}

                        private void readObject(java.io.ObjectInputStream s)
                        throws java.io.IOException, ClassNotFoundException {...}

                        public Spliterator<E> spliterator() {
                        return TreeMap.keySpliteratorFor(m);
                        }

                        private static final long serialVersionUID = -2479143000061671589L;
                        }
                        - +]]> + + Java + + + + 阅读JDK容器部分源码的心得体会2【Map部分】 + /2022/10/22/%E9%98%85%E8%AF%BBJDK%E5%AE%B9%E5%99%A8%E9%83%A8%E5%88%86%E6%BA%90%E7%A0%81%E7%9A%84%E5%BF%83%E5%BE%97%E4%BD%93%E4%BC%9A2%E3%80%90Map%E9%83%A8%E5%88%86%E3%80%91/ + +

                        idea 替换注释正则表达式/\*{1,2}[\s\S]*?\*/

                        +

                        typora 替换图片asset

                        +

                        \!\[.*\]\(D:\\aWorkStorage\\hexo\\blog\\source\\_posts\\阅读JDK容器部分源码的心得体会2【Map部分】\\(.*)\.png\)

                        +

                        替换结果{% asset_img $1.png %}

                        +
                        +

                        Map(I)

                        +

                        A map cannot contain duplicate keys; each key can map to at most one value.

                        +

                        This interface takes the place of the Dictionary class.

                        +

                        The Map interface provides three collection views, which allow a map’s contents to be viewed as a set of keys, collection of values, or set of key-value mappings.

                        +

                        The order of a map is defined as the order in which the iterators on the map’s collection views return their elements. map的元素顺序取决于集合元素顺序的意思?

                        +

                        Note: great care must be exercised if mutable objects are used as map keys. The behavior of a map is not specified if the value of an object is changed in a manner that affects equals comparisons while the object is a key in the map. 【这个跟set的那个是一样的】

                        +
                        +

                        map没有迭代器

                        + +

                        代码:

                        public interface Map<K,V> {
                        // Query Operations

                        int size();

                        boolean isEmpty();

                        boolean containsKey(Object key);

                        //This operation will probably require time linear in the map size
                        //for most implementations of the Map interface.
                        boolean containsValue(Object value);

                        //1
                        V get(Object key);

                        // Modification Operations

                        V put(K key, V value);

                        V remove(Object key);

                        // Bulk Operations

                        void putAll(Map<? extends K, ? extends V> m);

                        void clear();

                        // Views

                        //Returns a Set view of the keys contained in this map.
                        //The set is backed by the map, so changes to the map are reflected in the set, and vice-versa.
                        //The set supports element removal,
                        //which removes the corresponding mapping from the map,
                        //via the Iterator.remove, Set.remove, removeAll, retainAll, and clear operations.
                        //It does not support the add or addAll operations.
                        //2
                        Set<K> keySet();

                        //跟上面一样,也只支持remove,不支持add
                        Collection<V> values();

                        //3
                        //跟上面一样,也只支持remove,不支持add
                        Set<Map.Entry<K, V>> entrySet();

                        //The only way to obtain a reference to a map entry is from the iterator of this collection-view.
                        //These Map.Entry objects are valid only for the duration of the iteration;
                        //more formally, the behavior of a map entry is undefined if the backing map has been
                        //modified after the entry was returned by the iterator,
                        //except through the setValue operation on the map entry.迭代器也不行了
                        //可见度为default,包内可见
                        interface Entry<K,V> {

                        K getKey();

                        V getValue();

                        V setValue(V value);

                        boolean equals(Object o);

                        int hashCode();

                        public static <K extends Comparable<? super K>, V> Comparator<Map.Entry<K,V>> comparingByKey() {
                        return (Comparator<Map.Entry<K, V>> & Serializable)
                        (c1, c2) -> c1.getKey().compareTo(c2.getKey());
                        }

                        public static <K, V extends Comparable<? super V>> Comparator<Map.Entry<K,V>> comparingByValue() {
                        return (Comparator<Map.Entry<K, V>> & Serializable)
                        (c1, c2) -> c1.getValue().compareTo(c2.getValue());
                        }

                        public static <K, V> Comparator<Map.Entry<K, V>> comparingByKey(Comparator<? super K> cmp) {
                        Objects.requireNonNull(cmp);
                        return (Comparator<Map.Entry<K, V>> & Serializable)
                        (c1, c2) -> cmp.compare(c1.getKey(), c2.getKey());
                        }

                        public static <K, V> Comparator<Map.Entry<K, V>> comparingByValue(Comparator<? super V> cmp) {
                        Objects.requireNonNull(cmp);
                        return (Comparator<Map.Entry<K, V>> & Serializable)
                        (c1, c2) -> cmp.compare(c1.getValue(), c2.getValue());
                        }
                        }

                        // Comparison and hashing

                        boolean equals(Object o);

                        int hashCode();

                        // Defaultable methods

                        //1
                        default V getOrDefault(Object key, V defaultValue) {
                        V v;
                        return (((v = get(key)) != null) || containsKey(key))
                        ? v
                        : defaultValue;
                        }

                        default void forEach(BiConsumer<? super K, ? super V> action) {
                        Objects.requireNonNull(action);
                        for (Map.Entry<K, V> entry : entrySet()) {
                        K k;
                        V v;
                        try {
                        k = entry.getKey();
                        v = entry.getValue();
                        } catch(IllegalStateException ise) {
                        // this usually means the entry is no longer in the map.
                        //确实说明这时候应该并发修改异常了
                        throw new ConcurrentModificationException(ise);
                        }
                        action.accept(k, v);
                        }
                        }

                        default void replaceAll(BiFunction<? super K, ? super V, ? extends V> function) {
                        Objects.requireNonNull(function);
                        for (Map.Entry<K, V> entry : entrySet()) {
                        K k;
                        V v;
                        try {
                        k = entry.getKey();
                        v = entry.getValue();
                        } catch(IllegalStateException ise) {
                        // this usually means the entry is no longer in the map.
                        throw new ConcurrentModificationException(ise);
                        }

                        // ise thrown from function is not a cme.
                        v = function.apply(k, v);

                        try {
                        entry.setValue(v);
                        } catch(IllegalStateException ise) {
                        // this usually means the entry is no longer in the map.
                        throw new ConcurrentModificationException(ise);
                        }
                        }
                        }

                        //If the specified key 没有mapping或者对应值为空
                        //associates it with the given value and returns null,
                        //else returns the current value.
                        default V putIfAbsent(K key, V value) {
                        V v = get(key);
                        if (v == null) {
                        v = put(key, value);
                        }

                        return v;
                        }

                        //当所给的key对应的curValue==value时,就remove掉这对mapping
                        default boolean remove(Object key, Object value) {
                        Object curValue = get(key);
                        if (!Objects.equals(curValue, value) ||
                        (curValue == null && !containsKey(key))) {
                        return false;
                        }
                        remove(key);
                        return true;
                        }

                        default boolean replace(K key, V oldValue, V newValue) {
                        Object curValue = get(key);
                        if (!Objects.equals(curValue, oldValue) ||
                        (curValue == null && !containsKey(key))) {
                        return false;
                        }
                        put(key, newValue);
                        return true;
                        }

                        //如果映射存在就replace,返回旧值
                        default V replace(K key, V value) {
                        V curValue;
                        if (((curValue = get(key)) != null) || containsKey(key)) {
                        curValue = put(key, value);
                        }
                        return curValue;
                        }

                        //通过mappingFunction来用key计算value
                        //4
                        default V computeIfAbsent(K key,
                        Function<? super K, ? extends V> mappingFunction) {
                        Objects.requireNonNull(mappingFunction);
                        V v;
                        if ((v = get(key)) == null) {
                        V newValue;
                        if ((newValue = mappingFunction.apply(key)) != null) {
                        put(key, newValue);
                        return newValue;
                        }
                        }

                        return v;
                        }

                        //If the value for the specified key is present and non-null, attempts to compute a new mapping given the key and its current mapped value.
                        //If the function returns null, the mapping is removed.【此时传入的function计算得出value=NULL】
                        //If the function itself throws an (unchecked) exception, the exception is rethrown, and the current mapping is left unchanged.
                        default V computeIfPresent(K key,
                        BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
                        Objects.requireNonNull(remappingFunction);
                        V oldValue;
                        if ((oldValue = get(key)) != null) {
                        V newValue = remappingFunction.apply(key, oldValue);
                        if (newValue != null) {
                        put(key, newValue);
                        return newValue;
                        } else {
                        remove(key);
                        return null;
                        }
                        } else {
                        return null;
                        }
                        }

                        //跟上面那个的差别好像在,当oldValue==NULL,newValue不等于NULL时,下面这个会放入mapp(key,new)
                        //上面那个什么也不做。毕竟上面的叫computeIfPresent嘛
                        default V compute(K key,
                        BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
                        Objects.requireNonNull(remappingFunction);
                        V oldValue = get(key);

                        V newValue = remappingFunction.apply(key, oldValue);
                        if (newValue == null) {
                        // delete mapping
                        //新value==NULL,就delete
                        if (oldValue != null || containsKey(key)) {
                        // something to remove
                        remove(key);
                        return null;
                        } else {
                        // nothing to do. Leave things as they were.
                        return null;
                        }
                        } else {
                        // add or replace old mapping
                        put(key, newValue);
                        return newValue;
                        }
                        }

                        //把新旧值通过function合并
                        //5
                        default V merge(K key, V value,
                        BiFunction<? super V, ? super V, ? extends V> remappingFunction) {
                        Objects.requireNonNull(remappingFunction);
                        Objects.requireNonNull(value);
                        V oldValue = get(key);
                        //传入function的必定非空
                        V newValue = (oldValue == null) ? value :
                        remappingFunction.apply(oldValue, value);
                        if(newValue == null) {
                        remove(key);
                        } else {
                        put(key, newValue);
                        }
                        return newValue;
                        }

                        //6 迭代
                        }
                        -
                        -

                        代码:

                        public interface Deque<E> extends Queue<E> {

                        void addFirst(E e);

                        void addLast(E e);

                        boolean offerFirst(E e);

                        boolean offerLast(E e);

                        E removeFirst();

                        E removeLast();

                        E pollFirst();

                        E pollLast();

                        E getFirst();

                        E getLast();

                        E peekFirst();

                        E peekLast();

                        boolean removeFirstOccurrence(Object o);

                        boolean removeLastOccurrence(Object o);

                        // *** Queue methods ***

                        boolean add(E e);

                        boolean offer(E e);

                        E remove();

                        E poll();

                        E element();

                        E peek();

                        // *** Stack methods ***

                        void push(E e);

                        E pop();

                        // *** Collection methods ***

                        boolean remove(Object o);

                        boolean contains(Object o);

                        public int size();

                        Iterator<E> iterator();
                        //Returns an iterator over the elements in this deque in reverse sequential order.
                        Iterator<E> descendingIterator();

                        }
                        +

                        其中:

                          +
                        1. get return null时的情况

                          get return null when value==NULL or key不存在。

                          +

                          为了区分这两种情况,写代码时可以用:

                          +
                          if !containsKey(key){
                          key不存在
                          }
                          Obj obj=get(key);
                          -

                          ArrayDeque

                          -

                          Resizable-array implementation of the Deque interface.

                          -

                          不允许空

                          -

                          Array deques have no capacity restrictions; they grow as necessary to support usage.

                          -

                          not thread-safe【相比于由vector实现的线程安全的Stack】

                          -

                          This class is likely to be faster than Stack when used as a stack, and faster than LinkedList when used as a queue.【6】

                          -

                          fail-fast

                          +

                          其实源码中的getordefault方法就给出了应用典范

                          +
                          default V getOrDefault(Object key, V defaultValue) {
                          V v;
                          return (((v = get(key)) != null) || containsKey(key))
                          ? v
                          : defaultValue;
                          }
                        2. +
                        3. view

                          +

                          //Returns a Set view of the keys contained in this map.

                          +

                          //The set is backed by the map, so changes to the map are reflected in the set, and vice-versa.

                          -

                          代码:

                          public class ArrayDeque<E> extends AbstractCollection<E>
                          implements Deque<E>, Cloneable, Serializable
                          {
                          //非private以让内部类能够访问到
                          /*
                          The capacity of the deque is the length of this array,
                          which is always a power of two. capacity只能是2的幂次
                          不能满【原理应该跟循环队列差不多,是怕头尾混淆】
                          但允许短暂的满之后马上扩容
                          所有不包含元素的数组单元为空
                          */
                          transient Object[] elements;

                          //The index of the element at the head of the deque
                          //(which is the element that would be removed by remove() or pop());
                          //or an arbitrary number equal to tail if the deque is empty.
                          //head在下标大的地方
                          transient int head;

                          //The index at which the 【next】 element would be added to the tail of the deque
                          //(via addLast(E), add(E), or push(E)).
                          //tail在下标小的地方
                          transient int tail;

                          private static final int MIN_INITIAL_CAPACITY = 8;

                          // ****** Array allocation and resizing utilities ******

                          private static int calculateSize(int numElements) {
                          int initialCapacity = MIN_INITIAL_CAPACITY;
                          // Find the best power of two to hold elements.
                          // Tests "<=" because arrays aren't kept full.
                          if (numElements >= initialCapacity) {
                          //原理类似HashMap.tableSizeFor
                          //这一通操作可以得到比cap大的,且离cap最近的2的幂次方数
                          initialCapacity = numElements;
                          initialCapacity |= (initialCapacity >>> 1);
                          initialCapacity |= (initialCapacity >>> 2);
                          initialCapacity |= (initialCapacity >>> 4);
                          initialCapacity |= (initialCapacity >>> 8);
                          initialCapacity |= (initialCapacity >>> 16);
                          initialCapacity++;

                          if (initialCapacity < 0) // Too many elements, must back off
                          initialCapacity >>>= 1;// Good luck allocating 2 ^ 30 elements
                          }
                          return initialCapacity;
                          }


                          private void allocateElements(int numElements) {
                          elements = new Object[calculateSize(numElements)];
                          }


                          private void doubleCapacity() {
                          assert head == tail;
                          int p = head;
                          int n = elements.length;
                          int r = n - p; // number of elements to the right of p
                          int newCapacity = n << 1;
                          if (newCapacity < 0)
                          throw new IllegalStateException("Sorry, deque too big");
                          Object[] a = new Object[newCapacity];
                          //以head==tail为分界线,右边那段移到开头,左边那段移到后面
                          System.arraycopy(elements, p, a, 0, r);
                          System.arraycopy(elements, 0, a, r, p);
                          elements = a;
                          head = 0;
                          tail = n;
                          }

                          private <T> T[] copyElements(T[] a) {
                          if (head < tail) {
                          System.arraycopy(elements, head, a, 0, size());
                          } else if (head > tail) {
                          int headPortionLen = elements.length - head;
                          System.arraycopy(elements, head, a, 0, headPortionLen);
                          System.arraycopy(elements, 0, a, headPortionLen, tail);
                          }
                          return a;
                          }

                          //1
                          public ArrayDeque() {
                          elements = new Object[16];
                          }

                          public ArrayDeque(int numElements) {
                          allocateElements(numElements);
                          }

                          public ArrayDeque(Collection<? extends E> c) {
                          allocateElements(c.size());
                          addAll(c);
                          }

                          // The main insertion and extraction methods are addFirst,
                          // addLast, pollFirst, pollLast. The other methods are defined in
                          // terms of these.就是说这几个最重要,别的方法都是这四个的附庸

                          public void addFirst(E e) {
                          if (e == null)
                          throw new NullPointerException();
                          //2
                          elements[head = (head - 1) & (elements.length - 1)] = e;
                          if (head == tail)
                          //队列满
                          doubleCapacity();
                          }

                          public void addLast(E e) {
                          if (e == null)
                          throw new NullPointerException();
                          elements[tail] = e;
                          if ( (tail = (tail + 1) & (elements.length - 1)) == head)
                          //队列满
                          doubleCapacity();
                          }

                          public boolean offerFirst(E e) {
                          addFirst(e);
                          return true;
                          }

                          public boolean offerLast(E e) {
                          addLast(e);
                          return true;
                          }

                          public E removeFirst() {
                          E x = pollFirst();
                          if (x == null)
                          throw new NoSuchElementException();
                          return x;
                          }

                          public E removeLast() {
                          E x = pollLast();
                          if (x == null)
                          throw new NoSuchElementException();
                          return x;
                          }

                          public E pollFirst() {
                          int h = head;
                          @SuppressWarnings("unchecked")
                          E result = (E) elements[h];
                          // Element is null if deque empty
                          if (result == null)
                          return null;
                          elements[h] = null; // Must null out slot
                          head = (h + 1) & (elements.length - 1);
                          return result;
                          }

                          public E pollLast() {
                          int t = (tail - 1) & (elements.length - 1);
                          @SuppressWarnings("unchecked")
                          E result = (E) elements[t];
                          if (result == null)
                          return null;
                          elements[t] = null;
                          tail = t;
                          return result;
                          }

                          public E getFirst() {
                          @SuppressWarnings("unchecked")
                          E result = (E) elements[head];
                          if (result == null)
                          throw new NoSuchElementException();
                          return result;
                          }

                          public E getLast() {
                          @SuppressWarnings("unchecked")
                          E result = (E) elements[(tail - 1) & (elements.length - 1)];
                          if (result == null)
                          throw new NoSuchElementException();
                          return result;
                          }

                          @SuppressWarnings("unchecked")
                          public E peekFirst() {
                          // elements[head] is null if deque empty
                          return (E) elements[head];
                          }

                          @SuppressWarnings("unchecked")
                          public E peekLast() {
                          return (E) elements[(tail - 1) & (elements.length - 1)];
                          }

                          public boolean removeFirstOccurrence(Object o) {
                          if (o == null)
                          return false;
                          //掩码
                          int mask = elements.length - 1;
                          int i = head;
                          Object x;
                          while ( (x = elements[i]) != null) {
                          if (o.equals(x)) {
                          delete(i);
                          return true;
                          }
                          //头->尾
                          i = (i + 1) & mask;
                          }
                          return false;
                          }

                          public boolean removeLastOccurrence(Object o) {
                          if (o == null)
                          return false;
                          int mask = elements.length - 1;
                          int i = (tail - 1) & mask;
                          Object x;
                          while ( (x = elements[i]) != null) {
                          if (o.equals(x)) {
                          delete(i);
                          return true;
                          }
                          //尾->头
                          i = (i - 1) & mask;
                          }
                          return false;
                          }

                          // *** Queue methods ***

                          public boolean add(E e) {
                          addLast(e);
                          return true;
                          }

                          public boolean offer(E e) {
                          return offerLast(e);
                          }

                          public E remove() {
                          return removeFirst();
                          }

                          public E poll() {
                          return pollFirst();
                          }

                          public E element() {
                          return getFirst();
                          }

                          public E peek() {
                          return peekFirst();
                          }

                          // *** Stack methods ***

                          public void push(E e) {
                          addFirst(e);
                          }

                          public E pop() {
                          return removeFirst();
                          }

                          //检查队列情况正常
                          private void checkInvariants() {
                          assert elements[tail] == null;
                          //如果成立,只能是队列空;不成立的话,不能有空元素
                          assert head == tail ? elements[head] == null :
                          (elements[head] != null &&
                          elements[(tail - 1) & (elements.length - 1)] != null);
                          assert elements[(head - 1) & (elements.length - 1)] == null;
                          }

                          //Returns: true if elements moved backwards而不是操作是否成功
                          private boolean delete(int i) {
                          checkInvariants();
                          final Object[] elements = this.elements;
                          final int mask = elements.length - 1;
                          final int h = head;
                          final int t = tail;
                          final int front = (i - h) & mask;
                          final int back = (t - i) & mask;

                          // Invariant: head <= i < tail mod circularity
                          if (front >= ((t - h) & mask))
                          throw new ConcurrentModificationException();

                          // Optimize for least element motion
                          if (front < back) {
                          if (h <= i) {
                          //把i覆盖掉了
                          System.arraycopy(elements, h, elements, h + 1, front);
                          } else { // Wrap around
                          System.arraycopy(elements, 0, elements, 1, i);
                          elements[0] = elements[mask];
                          System.arraycopy(elements, h, elements, h + 1, mask - h);
                          }
                          elements[h] = null;
                          head = (h + 1) & mask;
                          return false;
                          } else {
                          if (i < t) { // Copy the null tail as well
                          System.arraycopy(elements, i + 1, elements, i, back);
                          //注意没设置空,因为确实不用
                          tail = t - 1;
                          } else { // Wrap around
                          System.arraycopy(elements, i + 1, elements, i, mask - i);
                          elements[mask] = elements[0];
                          System.arraycopy(elements, 1, elements, 0, t);
                          tail = (t - 1) & mask;
                          }
                          return true;
                          }
                          }

                          // *** Collection Methods ***

                          public int size() {
                          return (tail - head) & (elements.length - 1);
                          }

                          public boolean isEmpty() {
                          return head == tail;
                          }

                          //The elements will be ordered from first (head) to last (tail).
                          //This is the same order that elements would be dequeued
                          //(via successive calls to remove or popped (via successive calls to pop).
                          public Iterator<E> iterator() {
                          return new DeqIterator();
                          }

                          public Iterator<E> descendingIterator() {
                          return new DescendingIterator();
                          }

                          private class DeqIterator implements Iterator<E> {

                          private int cursor = head;

                          //Tail recorded at construction (also in remove), to stop iterator[怪不得叫fence]
                          //and also to check for comodification[检查并发].
                          private int fence = tail;

                          private int lastRet = -1;

                          public boolean hasNext() {
                          return cursor != fence;
                          }

                          public E next() {
                          if (cursor == fence)
                          throw new NoSuchElementException();
                          @SuppressWarnings("unchecked")
                          E result = (E) elements[cursor];
                          // This check doesn't catch all possible comodifications,
                          // but does catch the ones that corrupt traversal【破坏遍历的】
                          //tail!=fence说明迭代时修改。
                          if (tail != fence || result == null)
                          throw new ConcurrentModificationException();
                          lastRet = cursor;
                          cursor = (cursor + 1) & (elements.length - 1);
                          return result;
                          }

                          public void remove() {
                          if (lastRet < 0)
                          throw new IllegalStateException();
                          //3
                          if (delete(lastRet)) { // if left-shifted, undo increment in next()
                          cursor = (cursor - 1) & (elements.length - 1);
                          //update
                          fence = tail;
                          }
                          lastRet = -1;
                          }

                          public void forEachRemaining(Consumer<? super E> action) {
                          Objects.requireNonNull(action);
                          Object[] a = elements;
                          int m = a.length - 1, f = fence, i = cursor;
                          //4执行完后直接迭代结束
                          cursor = f;
                          while (i != f) {
                          @SuppressWarnings("unchecked") E e = (E)a[i];
                          i = (i + 1) & m;
                          if (e == null)
                          throw new ConcurrentModificationException();
                          action.accept(e);
                          }
                          }
                          }

                          private class DescendingIterator implements Iterator<E> {

                          private int cursor = tail;
                          //终点为fence,此时终点为head
                          private int fence = head;
                          private int lastRet = -1;

                          public boolean hasNext() {
                          return cursor != fence;
                          }

                          public E next() {
                          if (cursor == fence)
                          throw new NoSuchElementException();
                          cursor = (cursor - 1) & (elements.length - 1);
                          @SuppressWarnings("unchecked")
                          E result = (E) elements[cursor];
                          if (head != fence || result == null)
                          throw new ConcurrentModificationException();
                          lastRet = cursor;
                          return result;
                          }

                          public void remove() {
                          if (lastRet < 0)
                          throw new IllegalStateException();
                          if (!delete(lastRet)) {
                          cursor = (cursor + 1) & (elements.length - 1);
                          fence = head;
                          }
                          lastRet = -1;
                          }
                          //不支持for-each了吧233
                          }

                          public boolean contains(Object o) {
                          if (o == null)
                          return false;
                          int mask = elements.length - 1;
                          int i = head;
                          Object x;
                          while ( (x = elements[i]) != null) {
                          if (o.equals(x))
                          return true;
                          i = (i + 1) & mask;
                          }
                          return false;
                          }

                          public boolean remove(Object o) {
                          return removeFirstOccurrence(o);
                          }

                          public void clear() {
                          int h = head;
                          int t = tail;
                          if (h != t) { // clear all cells
                          head = tail = 0;
                          int i = h;
                          int mask = elements.length - 1;
                          do {
                          elements[i] = null;
                          i = (i + 1) & mask;
                          } while (i != t);
                          }
                          }

                          public Object[] toArray() {
                          return copyElements(new Object[size()]);
                          }

                          @SuppressWarnings("unchecked")
                          public <T> T[] toArray(T[] a) {
                          int size = size();
                          if (a.length < size)
                          a = (T[])java.lang.reflect.Array.newInstance(
                          a.getClass().getComponentType(), size);
                          copyElements(a);
                          if (a.length > size)
                          a[size] = null;
                          return a;
                          }

                          // *** Object methods ***

                          public ArrayDeque<E> clone() {
                          try {
                          @SuppressWarnings("unchecked")
                          ArrayDeque<E> result = (ArrayDeque<E>) super.clone();
                          result.elements = Arrays.copyOf(elements, elements.length);
                          return result;
                          } catch (CloneNotSupportedException e) {
                          throw new AssertionError();
                          }
                          }

                          private static final long serialVersionUID = 2340985798034038923L;

                          private void writeObject(java.io.ObjectOutputStream s)
                          throws java.io.IOException {...}

                          private void readObject(java.io.ObjectInputStream s)
                          throws java.io.IOException, ClassNotFoundException {...}

                          public Spliterator<E> spliterator() {
                          return new DeqSpliterator<E>(this, -1, -1);
                          }

                          static final class DeqSpliterator<E> implements Spliterator<E> {...}

                          }
                          +

                          如下代码测试:

                          +
                              public static void main(String[] args) {
                          HashMap<String,Integer> map = new HashMap<>();
                          map.put("Lily",15);
                          map.put("Sam",20);
                          map.put("Mary",11);
                          map.put("Lee",111);
                          Set set=map.keySet();
                          for( Object str : set){
                          System.out.print((String) str+" ");
                          }
                          System.out.println();
                          set.remove("Lee");
                          //set.add("haha");
                          for( Object str : set){
                          System.out.print((String) str+" ");
                          }
                          System.out.println();
                          for (Object str : map.keySet()){
                          System.out.print((String) str+" ");
                          }
                          System.out.println();
                          System.out.println(map.containsKey("Lee"));
                          }
                          /*
                          Lee Lily Sam Mary
                          Lily Sam Mary
                          Lily Sam Mary
                          false
                          */
                          -

                          其中:

                            -
                          1. 默认容量

                            空构造器的默认容量为16

                            -
                          2. -
                          3. (head - 1) & (elements.length - 1)

                            是一个便捷的截位取余操作,这跟hashmap一个原理,详见hashmap第二点。

                            -
                          4. -
                          5. if (delete(lastRet))

                            delete方法返回true,说明右移数组,此时next指针需要++

                            -

                            delete方法返回false,说明左移数组,此时next指针不变

                            -
                          6. -
                          7. forEachRemaining

                            跟差不多所有的迭代器实现一样,此方法执行完毕之后,cursor直接跳到数组最末,相当于迭代结束

                            +

                            可得,与之前的List一样,这个view都是纯粹基于原数组的,实时变化的。

                            +

                            在应用中可发现,可以通过map的key和value的set来对map进行遍历。

                          8. -
                          -

                          List

                          List(I)

                          有序、支持随机访问

                          -

                          代码

                          /*
                          List 接口提供了一个特殊的迭代器,称为 ListIterator,
                          除了 Iterator 接口提供的正常操作之外,它还允许元素插入和替换以及双向访问。
                          List 接口提供了两种方法来搜索指定的对象。 从性能的角度来看,应谨慎使用这些方法。
                          在许多实现中,它们将执行代价高昂的线性搜索。
                          Note: While it is permissible for lists to contain themselves as elements, extreme caution is advised: the equals and hashCode methods are no longer well defined on such a list.【这跟上面的引用点4是一样的。】

                          */
                          public interface List<E> extends Collection<E> {
                          int size();
                          boolean isEmpty();
                          boolean contains(Object o);
                          Iterator<E> iterator();
                          Object[] toArray();
                          <T> T[] toArray(T[] a);
                          boolean add(E e);
                          boolean remove(Object o);
                          boolean containsAll(Collection<?> c);
                          boolean addAll(Collection<? extends E> c);
                          boolean removeAll(Collection<?> c);
                          boolean retainAll(Collection<?> c);
                          //Returns true if and only if the specified object is also a list,
                          //both lists have the same size, and all corresponding pairs of elements in the two lists are equal.
                          //In other words, two lists are defined to be equal if they contain the same elements in the same order.
                          //注意,对于未重载equal方法的类,引用对象的相等指的是地址相等也就是说必须是一模一样的对象,两个不同对象但值相同,这种情况是不算equal的。
                          boolean equals(Object o);
                          int hashCode();

                          //newly add or change below

                          //将此列表的每个元素替换为将运算符应用于该元素的结果。
                          default void replaceAll(UnaryOperator<E> operator) {
                          Objects.requireNonNull(operator);
                          final ListIterator<E> li = this.listIterator();
                          while (li.hasNext()) {
                          li.set(operator.apply(li.next()));
                          }
                          }

                          //@SuppressWarings注解 作用:用于抑制编译器产生警告信息。
                          @SuppressWarnings({"unchecked", "rawtypes"})
                          default void sort(Comparator<? super E> c) {
                          Object[] a = this.toArray();
                          //借助Arrays的sort方法
                          Arrays.sort(a, (Comparator) c);
                          ListIterator<E> i = this.listIterator();
                          //再线性逐一替换
                          for (Object e : a) {
                          i.next();
                          // set:
                          // Replaces the last element returned by next or previous
                          // with the specified element
                          i.set((E) e);
                          }
                          }

                          E get(int index);

                          E set(int index, E element);

                          //插入元素,把当前位及其以后的元素都往后挪一位
                          void add(int index, E element);

                          //移除元素,把当前位及其以后的元素都往前挪一位
                          E remove(int index);

                          //-1 if this list does not contain the element
                          //ClassCastException:if the type of the specified element
                          //is incompatible with this list
                          int indexOf(Object o);

                          ListIterator<E> listIterator();

                          //指定的索引指示初始调用next将返回的第一个元素.对 previous 的初始调用将返回具有指定索引减一的元素。
                          ListIterator<E> listIterator(int index);

                          //[fromIndex,toIndex).If fromIndex==toIndex then return==null.
                          List<E> subList(int fromIndex, int toIndex);

                          @Override
                          default Spliterator<E> spliterator() {
                          return Spliterators.spliterator(this, Spliterator.ORDERED);
                          }

                          }
                          - -

                          其中:

                            -
                          1. hashcode不允许自环

                            Note: While it is permissible for lists to contain themselves as elements, extreme caution is advised: the equals and hashCode methods are no longer well defined on such a list.【这跟上面的引用点4是差不多的。】

                            -
                            ArrayList l1 = new ArrayList();
                            l1.add(l1);
                            System.out.println(l1.hashCode());
                            //输出:Stack Overflow
                          2. -
                          3. structural and non-structural change in list

                            Difference between structural and non-structural for lists

                            +
                          4. entrySet

                            +
                            //The map can be modified while an iteration over the set is in progress 
                            //when using the setValue operation on a map entry returned by the iterator
                            //or through the iterator's own remove operation
                            +
                            +

                            相比于其它的view,多了第二句话

                          5. -
                          6. 关于sublist
                            ① view

                            sublist的document有一个很有趣的点,就是把sublist称为原list的视图view,这不禁让人想起了数据库里的表和表的视图。

                            -

                            但是还是有差别的。

                            -

                            在数据库中,视图仅仅是表或表的一部分的快照,修改视图对原表没有影响。但此处,对sublist的结构性修改和非结构性修改都会使原list的对应元素发生改变。

                            -

                            但有一点是相同的。如果对原表/原list修改,那么视图就会没用/会寄掉。

                            -

                            可以像这样来对主list指定范围内的元素进行操作,免去复杂的下标。

                            -
                            //For example, the following idiom removes a range of elements from a list:
                            list.subList(from, to).clear();
                            - -
                            ② 留下一个问题
                            public class Main {
                            public static void main(String[] args) {
                            ArrayList<Student> a=new ArrayList<>();
                            a.add(new Student("Lily",16));
                            a.add(new Student("Sam",16));
                            a.add(new Student("Tom",16));
                            a.add(new Student("Mary",16));
                            a.add(new Student("Mark",16));
                            a.add(new Student("John",16));

                            List<Student> suba=a.subList(2,5);

                            System.out.println("Firstly,suba:");
                            printall(suba);

                            System.out.println("Change the value of age:");
                            suba.get(1).age=300;
                            System.out.println("suba:");
                            printall(suba);
                            System.out.println("a:");
                            printall(a);

                            System.out.println("Change the address:");
                            suba.set(1,new Student("haha",200));
                            System.out.println("suba:");
                            printall(suba);
                            System.out.println("a:");
                            printall(a);

                            System.out.println("Change the structure of sublist:");
                            suba.remove(0);
                            System.out.println("suba:");
                            printall(suba);
                            System.out.println("a:");
                            printall(a);
                            }
                            public static void printall(List<Student> list){
                            for (Student a : list){
                            System.out.print(a+"\t");
                            }
                            System.out.println();
                            }
                            }
                            /*运行结果
                            Firstly,suba:
                            name :Tomage :16 name :Maryage :16 name :Markage :16
                            Change the value of age:
                            suba:
                            name :Tomage :16 name :Maryage :300 name :Markage :16
                            a:
                            name :Lilyage :16 name :Samage :16 name :Tomage :16 name :Maryage :300 name :Markage :16 name :Johnage :16
                            Change the address:
                            suba:
                            name :Tomage :16 name :hahaage :200 name :Markage :16
                            a:
                            name :Lilyage :16 name :Samage :16 name :Tomage :16 name :hahaage :200 name :Markage :16 name :Johnage :16
                            Change the structure of sublist:
                            suba:
                            name :hahaage :200 name :Markage :16
                            a:
                            name :Lilyage :16 name :Samage :16 name :hahaage :200 name :Markage :16 name :Johnage :16
                            */
                            +
                          7. computeIfAbsent

                            +

                            If the function returns null no mapping is recorded.

                            +

                            If the function itself throws an (unchecked) exception, the exception is rethrown, and no mapping is recorded.

                            +

                            The most common usage is to construct a new object serving as an initial mapped value or memoized result, as in:

                            +
                            map.computeIfAbsent(key, k -> new Value(f(k)));
                            -

                            为什么之前的toArray得到的array换了个对象,原list不会变【详见Collection说明第五点】;而这里的sublist得到的子list换了个对象,原list也会变呢?

                            -

                            也许这跟间接寻址的级数有关?之后看过ArrayList的具体实现再来解答。

                            -
                            -

                            穿越回来:

                            -

                            ​ toArray得到array,是新开辟了存储空间,里面存放了原list对象的地址。因而,修改新存储空间的地址内容,原list是不变的。

                            -

                            ​ 但sublist连新开辟存储空间也没有,其差不多所有操作都是从list进行的。因而它换了个对象,具体实现就是让原list也换了个对象。

                            -

                            ​ 所以说其实前者更像是数据库里面的“视图”概念。

                            +

                            Or to implement a multi-value map, Map<K,Collection>, supporting multiple values per key:

                            +
                            map.computeIfAbsent(key, k -> new HashSet<V>()).add(v);
                            -
                            ③ 关于sublis和原list、non-structral和structural的区别:

                            Can structural changes in sublist reflected in the original list, in JAVA?

                            +

                            它这说的还是很抽象的,下面给出一个使用computeIfAbsent的优雅实例:

                            +

                            TreeMap()) Treemap With Object

                            -

                            In the document of List class:

                            -
                            /*
                            The returned list is backed by this list, so non-structural changes in the returned list are reflected in this list, and vice-versa.
                            */
                            List<E> subList(int fromIndex, int toIndex);
                            - -

                            Can structural changes in sublist reflected in the original list in JAVA?

                            +

                            computeIfAbsent returns the value that is already present in the map, or otherwise evaluates the lambda and puts that into the map, and returns that value.

                            -

                            答案是会改变的。实例可看问题中代码,或者point3的代码。

                            -

                            但是既然都会变的话,为什么文档里面要特意强调“non-structural”呢?这是因为,对sublist进行结构性改变会让原list也正常地一起变,但是对原list进行结构性改变却会让sublist寄掉:

                            -

                            文档下面接着写道:

                            -
                            The semantics of the list returned by this method become undefined if the backing list (i.e., this list) is structurally modified in any way other than via the returned list. (Structural modifications are those that change the size of this list, or otherwise perturb it in such a fashion that iterations in progress may yield incorrect results.)
                            /*
                            sublist会寄,如果其原list被结构性改变了,且是以除了通过sublist结构性改变的方式外的其他方式改变的。(结构性改变是指那些会改变list的长度,或者会使迭代失败的那些改变)
                            */
                            +
                            var line = "line";      
                            var mp = new TreeMap<String,TreeMap<String,Integer>>();
                            var m = mp.computeIfAbsent(line, k -> new TreeMap<>());
                            m.put("content", 5);
                            System.out.println(mp);
                            //output:{line={content=5}}
                            -

                            通过代码验证可得,确实寄了

                            -
                            public static void main(String[] args) {
                            ArrayList<Student> a=new ArrayList<>();
                            a.add(new Student("Lily",16));
                            a.add(new Student("Sam",16));
                            a.add(new Student("Tom",16));
                            a.add(new Student("Mary",16));
                            a.add(new Student("Mark",16));
                            a.add(new Student("John",16));

                            List<Student> suba=a.subList(2,5);

                            System.out.println("Firstly,suba:");
                            printall(suba);
                            printall(a);

                            System.out.println("Change the origin structure:");
                            a.remove(3);
                            System.out.println("a:");
                            printall(a);
                            System.out.println("suba:");
                            printall(suba);
                            }
                            /*运行结果
                            Firstly,suba:
                            name :Tomage :16 name :Maryage :16 name :Markage :16
                            name :Lilyage :16 name :Samage :16 name :Tomage :16 name :Maryage :16 name :Markage :16 name :Johnage :16
                            Change the origin structure:
                            a:
                            name :Lilyage :16 name :Samage :16 name :Tomage :16 name :Markage :16 name :Johnage :16
                            suba:
                            Exception in thread "main" java.util.ConcurrentModificationException
                            at java.util.ArrayList$SubList.checkForComodification(ArrayList.java:1241)
                            */
                            +

                            computIfAbsent发现此时map里面没有这个“line”key,就执行第二个参数的lambda表达式,把一个new TreeMap<>以line为关键字放入,并且返回该TreeMap。

                            +
                          8. +
                          9. merge

                            +

                            看起来非常实用:

                            +

                            This method may be of use when combining multiple mapped values for a key【相同key不同value合并】. For example, to either create or append a String msg to a value mapping:

                            +
                            map.merge(key, msg, String::concat)
                            -

                            通过以上可以感觉,估计sublist和原list的元素是共享存储空间的,只不过可能对象里有相关维护的变量。Any method accessing the list through the sub list effectively does index + offset.故而要是list变了,sublist的相关维护变量不变,就会依然傻傻地进行offset+index操作,这样就会寄。此猜想有待验证23333

                            -
                            -

                            穿越回来:

                            -
                              -
                            1. 确实是共享存储空间的,不如说sublist就直接引用了原list的变量,所有操作实质上都是在原list上进行。
                            2. -
                            3. sublist傻傻地进行offset+index操作,这样体现在原list上,可能导致下标越界或者结果并非我们想要的。
                            4. -
                            +

                            所举代码段意为把新值通过字符串拼接接在旧值后面。

                            +

                            应该也可以用于集合合并。总之具体实现方法取决于传入的function参数,非常实用

                          10. +
                          11. 迭代器

                            map本身没有迭代器。

                            +

                            因而在对map进行遍历时,只能通过其keyset、valueset以及entryset来实现。

                            +

                            具体详见:HashMap的四种遍历方式

                            +
                          -

                          AbstractList(A)

                          -

                          提供随机访问list的基本骨架

                          -

                          To implement an unmodifiable list, the programmer needs only to extend this class and provide implementations for the get(int) and size() methods.

                          -

                          To implement a modifiable list, the programmer must additionally override the set(int, Object) method

                          -

                          If the list is variable-size,the programmer must additionally override the add(int, E) and remove(int) methods.

                          -

                          Unlike the other abstract collection implementations, the programmer does not have to provide an iterator implementation; the iterator and list iterator are implemented by this class, on top of the “random access“ methods: get(int), set(int, E), add(int, E) and remove(int).这里的迭代器反而是通过类里面的方法来实现的

                          +

                          AbstractMap

                          +

                          To implement an unmodifiable map, the programmer needs only to extend this class and provide an implementation for the entrySet method, which returns a set-view of the map’s mappings. Typically, the returned set will, in turn, be implemented atop AbstractSet. This set should not support the add or remove methods, and its iterator should not support the remove method.

                          +

                          To implement a modifiable map, the programmer must additionally override this class’s put method (which otherwise throws an UnsupportedOperationException), and the iterator returned by entrySet().iterator() must additionally implement its remove method.

                          -

                          代码

                          public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E> {

                          protected AbstractList() {
                          }
                          //重写add方法
                          public boolean add(E e) {
                          //此为下面的public void add(int index, E element);
                          add(size(), e);
                          return true;
                          }
                          //交给具体实现
                          abstract public E get(int index);
                          //假设不能修改
                          public E set(int index, E element) {
                          throw new UnsupportedOperationException();
                          }
                          public void add(int index, E element) {
                          throw new UnsupportedOperationException();
                          }
                          public E remove(int index) {
                          throw new UnsupportedOperationException();
                          }

                          // Search Operations

                          public int indexOf(Object o) {
                          ListIterator<E> it = listIterator();
                          if (o==null) {
                          while (it.hasNext())
                          if (it.next()==null)
                          return it.previousIndex();
                          } else {
                          while (it.hasNext())
                          if (o.equals(it.next()))
                          return it.previousIndex();
                          }
                          return -1;
                          }

                          public int lastIndexOf(Object o) {
                          //从后往前遍历
                          ListIterator<E> it = listIterator(size());
                          if (o==null) {
                          while (it.hasPrevious())
                          if (it.previous()==null)
                          return it.nextIndex();
                          } else {
                          while (it.hasPrevious())
                          if (o.equals(it.previous()))
                          return it.nextIndex();
                          }
                          return -1;
                          }

                          // Bulk Operations

                          public void clear() {
                          removeRange(0, size());
                          }

                          public boolean addAll(int index, Collection<? extends E> c) {
                          rangeCheckForAdd(index);
                          boolean modified = false;
                          for (E e : c) {
                          add(index++, e);
                          modified = true;
                          }
                          return modified;
                          }

                          // Iterators

                          public Iterator<E> iterator() {
                          return new Itr();
                          }


                          public ListIterator<E> listIterator() {
                          return listIterator(0);
                          }


                          public ListIterator<E> listIterator(final int index) {
                          rangeCheckForAdd(index);

                          return new ListItr(index);
                          }

                          //内部类诶 1
                          private class Itr implements Iterator<E> {
                          //Index of element to be returned by subsequent call to next.
                          int cursor = 0;
                          //Index of element returned by most recent call to next or previous. Reset to -1 if this element is deleted by a call to remove.
                          int lastRet = -1;

                          int expectedModCount = modCount;

                          public boolean hasNext() {
                          return cursor != size();
                          }

                          public E next() {
                          checkForComodification();
                          try {
                          int i = cursor;
                          E next = get(i);
                          lastRet = i;
                          cursor = i + 1;
                          return next;
                          } catch (IndexOutOfBoundsException e) {
                          checkForComodification();
                          throw new NoSuchElementException();
                          }
                          }

                          public void remove() {
                          if (lastRet < 0)
                          throw new IllegalStateException();
                          checkForComodification();

                          try {
                          AbstractList.this.remove(lastRet);
                          if (lastRet < cursor)
                          cursor--;
                          lastRet = -1;
                          expectedModCount = modCount;
                          } catch (IndexOutOfBoundsException e) {
                          throw new ConcurrentModificationException();
                          }
                          }

                          final void checkForComodification() {
                          if (modCount != expectedModCount)
                          throw new ConcurrentModificationException();
                          }
                          }

                          private class ListItr extends Itr implements ListIterator<E> {
                          ListItr(int index) {
                          cursor = index;
                          }

                          public boolean hasPrevious() {
                          return cursor != 0;
                          }

                          public E previous() {
                          checkForComodification();
                          try {
                          int i = cursor - 1;
                          E previous = get(i);
                          //lastRet=i;cursor=i;
                          lastRet = cursor = i;
                          return previous;
                          } catch (IndexOutOfBoundsException e) {
                          checkForComodification();
                          throw new NoSuchElementException();
                          }
                          }

                          public int nextIndex() {
                          return cursor;
                          }

                          public int previousIndex() {
                          return cursor-1;
                          }

                          public void set(E e) {
                          if (lastRet < 0)
                          throw new IllegalStateException();
                          checkForComodification();

                          try {
                          AbstractList.this.set(lastRet, e);
                          expectedModCount = modCount;
                          } catch (IndexOutOfBoundsException ex) {
                          throw new ConcurrentModificationException();
                          }
                          }

                          public void add(E e) {
                          checkForComodification();

                          try {
                          int i = cursor;
                          AbstractList.this.add(i, e);
                          lastRet = -1;
                          cursor = i + 1;
                          expectedModCount = modCount;
                          } catch (IndexOutOfBoundsException ex) {
                          throw new ConcurrentModificationException();
                          }
                          }
                          }

                          //迭代器定义结束

                          public List<E> subList(int fromIndex, int toIndex) {
                          //Sublist和RandomAccessSubList在后面都作为内部类定义
                          //这俩的分支主要是能否支持高性能随机访问,而这点在Java是依靠是否实现RandomAccess接口
                          //来体现的,要实现接口必须得是一个类。因此,为了分歧,这里不得不创建两个类来表示两种情况。
                          //这两个类的方法应该是大致相同的。
                          //2
                          return (this instanceof RandomAccess ?
                          new RandomAccessSubList<>(this, fromIndex, toIndex) :
                          new SubList<>(this, fromIndex, toIndex));
                          }

                          // Comparison and hashing

                          public boolean equals(Object o) {
                          if (o == this)
                          return true;
                          if (!(o instanceof List))
                          return false;

                          ListIterator<E> e1 = listIterator();
                          ListIterator<?> e2 = ((List<?>) o).listIterator();
                          while (e1.hasNext() && e2.hasNext()) {
                          E o1 = e1.next();
                          Object o2 = e2.next();
                          //如果这东西没有重载equals方法,那此处就是单纯object对象是否相同了
                          if (!(o1==null ? o2==null : o1.equals(o2)))
                          return false;
                          }
                          //为啥size不一样不在一开始就比呢?那样不是更省花销吗
                          return !(e1.hasNext() || e2.hasNext());
                          }

                          public int hashCode() {
                          int hashCode = 1;
                          for (E e : this)
                          hashCode = 31*hashCode + (e==null ? 0 : e.hashCode());
                          return hashCode;
                          }

                          protected void removeRange(int fromIndex, int toIndex) {
                          ListIterator<E> it = listIterator(fromIndex);
                          for (int i=0, n=toIndex-fromIndex; i<n; i++) {
                          it.next();
                          it.remove();
                          }
                          }

                          //The number of times this list has been structurally modified.
                          //3
                          protected transient int modCount = 0;

                          private void rangeCheckForAdd(int index) {
                          if (index < 0 || index > size())
                          throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
                          }

                          private String outOfBoundsMsg(int index) {
                          return "Index: "+index+", Size: "+size();
                          }
                          }


                          class SubList<E> extends AbstractList<E> {
                          //这大概是对父list的引用
                          private final AbstractList<E> l;
                          //这大概是在父list的起始偏移量
                          private final int offset;
                          //sublist的长度
                          private int size;

                          SubList(AbstractList<E> list, int fromIndex, int toIndex) {
                          if (fromIndex < 0)
                          throw new IndexOutOfBoundsException("fromIndex = " + fromIndex);
                          if (toIndex > list.size())
                          throw new IndexOutOfBoundsException("toIndex = " + toIndex);
                          if (fromIndex > toIndex)
                          throw new IllegalArgumentException("fromIndex(" + fromIndex +
                          ") > toIndex(" + toIndex + ")");
                          l = list;
                          offset = fromIndex;
                          size = toIndex - fromIndex;
                          this.modCount = l.modCount;
                          }

                          public E set(int index, E element) {
                          rangeCheck(index);
                          checkForComodification();
                          //改变sublist也会改变父list,是否成功取决于对父list的改变是否成功
                          return l.set(index+offset, element);
                          }

                          public E get(int index) {
                          rangeCheck(index);
                          checkForComodification();
                          //直接取父类值
                          return l.get(index+offset);
                          }

                          public int size() {
                          checkForComodification();
                          return size;
                          }

                          public void add(int index, E element) {
                          rangeCheckForAdd(index);
                          checkForComodification();
                          l.add(index+offset, element);
                          this.modCount = l.modCount;
                          size++;
                          }

                          public E remove(int index) {
                          rangeCheck(index);
                          checkForComodification();
                          E result = l.remove(index+offset);
                          this.modCount = l.modCount;
                          size--;
                          return result;
                          }

                          protected void removeRange(int fromIndex, int toIndex) {
                          checkForComodification();
                          l.removeRange(fromIndex+offset, toIndex+offset);
                          this.modCount = l.modCount;
                          size -= (toIndex-fromIndex);
                          }

                          public boolean addAll(Collection<? extends E> c) {
                          return addAll(size, c);
                          }

                          public boolean addAll(int index, Collection<? extends E> c) {
                          rangeCheckForAdd(index);
                          int cSize = c.size();
                          if (cSize==0)
                          return false;

                          checkForComodification();
                          l.addAll(offset+index, c);
                          this.modCount = l.modCount;
                          size += cSize;
                          return true;
                          }

                          public Iterator<E> iterator() {
                          return listIterator();
                          }

                          public ListIterator<E> listIterator(final int index) {
                          checkForComodification();
                          rangeCheckForAdd(index);

                          //改变迭代器实现
                          return new ListIterator<E>() {
                          //子类迭代器=父类迭代器+offset
                          private final ListIterator<E> i = l.listIterator(index+offset);

                          public boolean hasNext() {
                          return nextIndex() < size;
                          }

                          public E next() {
                          if (hasNext())
                          return i.next();
                          else
                          throw new NoSuchElementException();
                          }

                          public boolean hasPrevious() {
                          return previousIndex() >= 0;
                          }

                          public E previous() {
                          if (hasPrevious())
                          return i.previous();
                          else
                          throw new NoSuchElementException();
                          }

                          public int nextIndex() {
                          return i.nextIndex() - offset;
                          }

                          public int previousIndex() {
                          return i.previousIndex() - offset;
                          }

                          public void remove() {
                          i.remove();
                          SubList.this.modCount = l.modCount;
                          size--;
                          }

                          public void set(E e) {
                          i.set(e);
                          }

                          public void add(E e) {
                          i.add(e);
                          SubList.this.modCount = l.modCount;
                          size++;
                          }
                          };
                          }

                          //层层套娃啊
                          public List<E> subList(int fromIndex, int toIndex) {
                          return new SubList<>(this, fromIndex, toIndex);
                          }

                          private void rangeCheck(int index) {
                          if (index < 0 || index >= size)
                          throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
                          }

                          private void rangeCheckForAdd(int index) {
                          if (index < 0 || index > size)
                          throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
                          }

                          private String outOfBoundsMsg(int index) {
                          return "Index: "+index+", Size: "+size;
                          }

                          private void checkForComodification() {
                          if (this.modCount != l.modCount)
                          throw new ConcurrentModificationException();
                          }
                          }

                          //确实实现上没什么区别,主要是多了个RandomAccess的约定。
                          class RandomAccessSubList<E> extends SubList<E> implements RandomAccess {
                          RandomAccessSubList(AbstractList<E> list, int fromIndex, int toIndex) {
                          super(list, fromIndex, toIndex);
                          }

                          public List<E> subList(int fromIndex, int toIndex) {
                          return new RandomAccessSubList<>(this, fromIndex, toIndex);
                          }
                          }

                          + -

                          其中:

                            -
                          1. 内部类ListItr和Itr的实现

                            Itr的实现需要用到AbstratcList类的get和set方法,而显然不同Collection的get和set不一样。为了避免混淆,Itr就只能作为私有类。为了避免胡乱引用,Itr就可以直接作为内部类,共享其外部类的所有资源。

                            -

                            ListItr作为内部私有类很容易理解,毕竟只有list才需要它。

                            +

                            最核心的还是entrySet。其余所有的方法,都是通过enrtSet实现的。而给定了enrty这个数据结构的实现方式,剩下的就是entrySet具体怎么实现了。AbstractMap把entrySet的实现抽象了出来,交给了其实现类去具体实现。

                            +

                            代码:

                            public abstract class AbstractMap<K,V> implements Map<K,V> {

                            protected AbstractMap() {
                            }

                            // Query Operations

                            public int size() {
                            //还真确实是set的大小
                            return entrySet().size();
                            }

                            public boolean isEmpty() {
                            return size() == 0;
                            }

                            public boolean containsValue(Object value) {
                            Iterator<Entry<K,V>> i = entrySet().iterator();
                            if (value==null) {
                            while (i.hasNext()) {
                            //entrySet的元素是Entry
                            Entry<K,V> e = i.next();
                            if (e.getValue()==null)
                            return true;
                            }
                            } else {
                            while (i.hasNext()) {
                            Entry<K,V> e = i.next();
                            if (value.equals(e.getValue()))
                            return true;
                            }
                            }
                            return false;
                            }

                            public boolean containsKey(Object key) {
                            Iterator<Map.Entry<K,V>> i = entrySet().iterator();
                            if (key==null) {
                            while (i.hasNext()) {
                            Entry<K,V> e = i.next();
                            if (e.getKey()==null)
                            return true;
                            }
                            } else {
                            while (i.hasNext()) {
                            Entry<K,V> e = i.next();
                            if (key.equals(e.getKey()))
                            return true;
                            }
                            }
                            return false;
                            }

                            public V get(Object key) {
                            Iterator<Entry<K,V>> i = entrySet().iterator();
                            if (key==null) {
                            while (i.hasNext()) {
                            Entry<K,V> e = i.next();
                            if (e.getKey()==null)
                            return e.getValue();
                            }
                            } else {
                            while (i.hasNext()) {
                            Entry<K,V> e = i.next();
                            if (key.equals(e.getKey()))
                            return e.getValue();
                            }
                            }
                            return null;
                            }

                            // Modification Operations

                            public V put(K key, V value) {
                            throw new UnsupportedOperationException();
                            }

                            //为啥unmodifiable map还可以remove
                            public V remove(Object key) {
                            Iterator<Entry<K,V>> i = entrySet().iterator();
                            Entry<K,V> correctEntry = null;
                            if (key==null) {
                            while (correctEntry==null && i.hasNext()) {
                            Entry<K,V> e = i.next();
                            if (e.getKey()==null)
                            correctEntry = e;
                            }
                            } else {
                            while (correctEntry==null && i.hasNext()) {
                            Entry<K,V> e = i.next();
                            if (key.equals(e.getKey()))
                            correctEntry = e;
                            }
                            }

                            V oldValue = null;
                            if (correctEntry !=null) {
                            oldValue = correctEntry.getValue();
                            i.remove();
                            }
                            return oldValue;
                            }

                            // Bulk Operations

                            public void putAll(Map<? extends K, ? extends V> m) {
                            for (Map.Entry<? extends K, ? extends V> e : m.entrySet())
                            put(e.getKey(), e.getValue());
                            }

                            public void clear() {
                            //还真是
                            entrySet().clear();
                            }

                            // Views
                            //1
                            transient Set<K> keySet;
                            transient Collection<V> values;

                            //The set supports element removal via
                            //the Iterator.remove, Set.remove, removeAll, retainAll, and clear operations.
                            //It does not support the add or addAll operations.
                            //只删不加
                            public Set<K> keySet() {
                            //引用成员变量,减少访问堆次数
                            Set<K> ks = keySet;
                            //首次建立视图
                            if (ks == null) {
                            ks = new AbstractSet<K>() {
                            public Iterator<K> iterator() {
                            return new Iterator<K>() {
                            private Iterator<Entry<K,V>> i = entrySet().iterator();

                            public boolean hasNext() {
                            return i.hasNext();
                            }

                            public K next() {
                            return i.next().getKey();
                            }

                            public void remove() {
                            i.remove();
                            }
                            };
                            }

                            //AbtractMap的这些方法都是通过其entryset实现的。因此其实最主要的还是entryset怎么实现的
                            public int size() {
                            return AbstractMap.this.size();
                            }

                            public boolean isEmpty() {
                            return AbstractMap.this.isEmpty();
                            }

                            public void clear() {
                            AbstractMap.this.clear();
                            }

                            public boolean contains(Object k) {
                            return AbstractMap.this.containsKey(k);
                            }
                            };
                            //赋值回给成员变量
                            keySet = ks;
                            }
                            return ks;
                            }

                            public Collection<V> values() {
                            Collection<V> vals = values;
                            if (vals == null) {
                            vals = new AbstractCollection<V>() {
                            public Iterator<V> iterator() {
                            return new Iterator<V>() {
                            private Iterator<Entry<K,V>> i = entrySet().iterator();

                            public boolean hasNext() {
                            return i.hasNext();
                            }

                            public V next() {
                            return i.next().getValue();
                            }

                            public void remove() {
                            i.remove();
                            }
                            };
                            }

                            public int size() {
                            return AbstractMap.this.size();
                            }

                            public boolean isEmpty() {
                            return AbstractMap.this.isEmpty();
                            }

                            public void clear() {
                            AbstractMap.this.clear();
                            }

                            public boolean contains(Object v) {
                            return AbstractMap.this.containsValue(v);
                            }
                            };
                            values = vals;
                            }
                            return vals;
                            }

                            //有待不同的数据结构实现了
                            public abstract Set<Entry<K,V>> entrySet();

                            // Comparison and hashing

                            public boolean equals(Object o) {
                            if (o == this)
                            return true;

                            if (!(o instanceof Map))
                            return false;
                            Map<?,?> m = (Map<?,?>) o;
                            if (m.size() != size())
                            return false;

                            try {
                            Iterator<Entry<K,V>> i = entrySet().iterator();
                            while (i.hasNext()) {
                            Entry<K,V> e = i.next();
                            K key = e.getKey();
                            V value = e.getValue();
                            if (value == null) {
                            if (!(m.get(key)==null && m.containsKey(key)))
                            return false;
                            } else {
                            if (!value.equals(m.get(key)))
                            return false;
                            }
                            }
                            } catch (ClassCastException unused) {
                            return false;
                            } catch (NullPointerException unused) {
                            return false;
                            }

                            return true;
                            }

                            public int hashCode() {
                            int h = 0;
                            Iterator<Entry<K,V>> i = entrySet().iterator();
                            while (i.hasNext())
                            h += i.next().hashCode();
                            return h;
                            }

                            public String toString() {
                            Iterator<Entry<K,V>> i = entrySet().iterator();
                            if (! i.hasNext())
                            return "{}";

                            StringBuilder sb = new StringBuilder();
                            sb.append('{');
                            for (;;) {
                            Entry<K,V> e = i.next();
                            K key = e.getKey();
                            V value = e.getValue();
                            //经典防自环
                            sb.append(key == this ? "(this Map)" : key);
                            sb.append('=');
                            sb.append(value == this ? "(this Map)" : value);
                            if (! i.hasNext())
                            return sb.append('}').toString();
                            sb.append(',').append(' ');
                            }
                            }

                            protected Object clone() throws CloneNotSupportedException {
                            AbstractMap<?,?> result = (AbstractMap<?,?>)super.clone();
                            //也就只有这两个成员变量了
                            result.keySet = null;
                            result.values = null;
                            return result;
                            }

                            private static boolean eq(Object o1, Object o2) {
                            return o1 == null ? o2 == null : o1.equals(o2);
                            }

                            // Implementation Note: SimpleEntry and SimpleImmutableEntry
                            // are distinct unrelated classes, even though they share
                            // some code. Since you can't add or subtract final-ness
                            // of a field in a subclass, they can't share representations,
                            // and the amount of duplicated code is too small to warrant
                            // exposing a common abstract class.
                            //意思就是说,这两个类一个表示key不可变value可变的entry,也就是可变map,
                            //另一个表示key和value都不可变的entry,也就是固定map,
                            //这俩有很多重复代码,但不能统一到一起,是因为前者有一个final字段,后者有两个,
                            //无法对这个final字段做一个统一,因此只能分成两个了

                            //静态内部类
                            //对Entry接口的一个简单实现【key不可变,value可变】
                            public static class SimpleEntry<K,V>
                            implements Entry<K,V>, java.io.Serializable
                            {
                            private static final long serialVersionUID = -8499721149061103585L;

                            //key不可修改,value可修改
                            private final K key;
                            private V value;

                            public SimpleEntry(K key, V value) {
                            this.key = key;
                            this.value = value;
                            }

                            public SimpleEntry(Entry<? extends K, ? extends V> entry) {
                            this.key = entry.getKey();
                            this.value = entry.getValue();
                            }

                            public K getKey() {
                            return key;
                            }

                            public V getValue() {
                            return value;
                            }

                            public V setValue(V value) {
                            V oldValue = this.value;
                            this.value = value;
                            return oldValue;
                            }

                            public boolean equals(Object o) {
                            if (!(o instanceof Map.Entry))
                            return false;
                            Map.Entry<?,?> e = (Map.Entry<?,?>)o;
                            return eq(key, e.getKey()) && eq(value, e.getValue());
                            }

                            public int hashCode() {
                            //注意这里是异或
                            return (key == null ? 0 : key.hashCode()) ^
                            (value == null ? 0 : value.hashCode());
                            }

                            public String toString() {
                            return key + "=" + value;
                            }

                            }

                            //静态内部类
                            //对Entry接口的一个简单实现【key不可变,value不可变】
                            public static class SimpleImmutableEntry<K,V>
                            implements Entry<K,V>, java.io.Serializable
                            {
                            private static final long serialVersionUID = 7138329143949025153L;

                            private final K key;
                            private final V value;

                            public SimpleImmutableEntry(K key, V value) {
                            this.key = key;
                            this.value = value;
                            }

                            public SimpleImmutableEntry(Entry<? extends K, ? extends V> entry) {
                            this.key = entry.getKey();
                            this.value = entry.getValue();
                            }

                            public K getKey() {
                            return key;
                            }

                            public V getValue() {
                            return value;
                            }

                            public V setValue(V value) {
                            //exception
                            throw new UnsupportedOperationException();
                            }

                            public boolean equals(Object o) {
                            if (!(o instanceof Map.Entry))
                            return false;
                            Map.Entry<?,?> e = (Map.Entry<?,?>)o;
                            return eq(key, e.getKey()) && eq(value, e.getValue());
                            }

                            public int hashCode() {
                            return (key == null ? 0 : key.hashCode()) ^
                            (value == null ? 0 : value.hashCode());
                            }

                            public String toString() {
                            return key + "=" + value;
                            }
                            }
                            }
                            + +

                            其中:

                              +
                            1. view

                              +

                              Each of these fields are initialized to contain an instance of the appropriate view the first time this view is requested. The views are stateless, so there’s no reason to create more than one of each.

                              +
                              +

                              不同于之前List的sublist和sorted set的subset,它俩是调用创建view方法时才构造出一个新的对象,map是直接把values和keys视图放入成员变量了,因为Collection的视图从实用角度来说有起始和终点更实用,map不需要这个性质,因此作为成员变量花费更小

                            2. -
                            3. RandomAccess

                              RandomAccess是一个空接口,它应该代表一个约定俗成的规定,即它的implementations的随机访问都是性能较高的。这个空接口思想很常见,源码带给我们的智慧。

                              -
                              按经验来说, a List implementation should implement this interface, if this loop:
                              for (int i=0, n=list.size(); i < n; i++)
                              list.get(i);

                              runs faster than this loop:
                              for (Iterator i=list.iterator(); i.hasNext(); )
                              i.next();
                            4. -
                            5. 关于modCount字段与fail-fast机制

                              你真的知道集合中modCount字段作用吗?

                              -

                              modCount字段就是保证一定程度并发安全的变量,fail-fast就是指马上抛出异常。

                              - +
                            +

                            HashMap

                            哈希表+链表/红黑树

                            +
                            +

                            permits null values and the null key允许空,其hash应该是0

                            +

                            The HashMap class is roughly equivalent to Hashtable, except that it is unsynchronized and permits nulls.不同步

                            +

                            This class makes no guarantees as to the order of the map; in particular, it does not guarantee that the order will remain constant over time.无序

                            +

                            这应该差不多就是个桶链表

                            +

                            An instance of HashMap has two parameters that affect its performance: initial capacity and load factor.

                            +

                            The capacity is the number of buckets in the hash table.桶数量=capacity

                            +

                            The load factor is a measure of how full the hash table is allowed to get before its capacity is automatically increased. 如果装载百分比达到load factor,hashmap的capacity就会自动增长。

                            +

                            When the number of entries in the hash table exceeds the product of the load factor and the current capacity, the hash table is rehashed.如果元素数量>=load factor*capacity,就会自动增长并且重新hash。

                            +

                            默认的load factor是0.75.【我其实觉得这个数很有意思。它是二进制意义上的整除数,因而计算应该很方便:它可以被整整表示,并且计算时可以拆成“2^-1+2^-2”以供移位简化】

                            +

                            我们设置capacity和load factor的意图应该是要尽量减少rehash的次数。

                            +

                            Note that using many keys with the same hashCode() is a sure way to slow down performance of any hash table使用多个相同的key【指hashcode相同】会降低性能【?】

                            +

                            https://stackoverflow.com/questions/43911369/hashmap-java-8-implementation等会看看

                            +
                            +

                            总之意思差不多就是,hashmap的数据结构:

                            +

                            table数组,每个成员都是一个桶,桶里面装着结点。table默认长度为16

                            +

                            每个桶内结点的结构依具体情况(该桶内元素多少)来决定,桶内元素多则用树状结构,少就用简单的线性表结构。线性结构为Node<K,V>,树状结构为TreeNode<K,V>。

                            +

                            当一个线性表桶内结点多于临界值,就需要进行树化,会从链表变成红黑树;当整个hashmap结点数多于临界值,就需要增长capacity并且进行rehash。

                            +

                            hashmap的桶的装配:首先通过key的hashcode算出一个hash值,然后再把该hash值与n-1相与就能得到桶编号。接下来再在桶内找到应插入的结点就行。

                            +

                            代码:

                            public class HashMap<K,V> extends AbstractMap<K,V>
                            implements Map<K,V>, Cloneable, Serializable {

                            private static final long serialVersionUID = 362498820763181265L;

                            /*
                            此映射通常充当分箱(分桶)哈希表,但当箱变得太大时,它们会转换为 TreeNode 的箱,
                            每个结构类似于 java.util.TreeMap 中的结构。
                            大多数方法尝试使用正常的 bin,但出于实用性有时候会过渡到 TreeNode 方法(只需检查节点的实例)。
                            TreeNode 的 bin 可以像任何其他 bin 一样被遍历和使用,但在填充过多时还支持更快的查找。
                            但是,由于绝大多数正常使用的 bin 并没有过度填充,
                            因此在 table 方法的过程中检查树 bin 的存在可能会白花时间。

                            因为 TreeNode 的大小大约是常规节点的两倍,
                            所以我们仅在 bin 包含足够的节点以保证使用时才使用它们(请参阅 TREEIFY_THRESHOLD)。
                            当它们变得太小(由于移除或调整大小)时,它们会被转换回plain bins。
                            */

                            static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

                            static final int MAXIMUM_CAPACITY = 1 << 30;

                            static final float DEFAULT_LOAD_FACTOR = 0.75f;

                            /*
                            The bin count 临界值 for using a tree rather than list for a bin.
                            当桶内节点数大于等于该值时,桶将由链表连接转化为树状结构。
                            该值必须大于 2 并且应该至少为 8,以便与树移除中关于在收缩时转换回普通 bin 的假设相吻合。
                            */
                            static final int TREEIFY_THRESHOLD = 8;

                            //The bin count threshold for untreeifying a (split) bin during a resize operation.
                            static final int UNTREEIFY_THRESHOLD = 6;

                            /*
                            The smallest table capacity for which bins may be treeified.
                            (Otherwise the table is resized if too many nodes in a bin.)
                            Should be at least 4 * TREEIFY_THRESHOLD to avoid conflicts between
                            resizing and treeification thresholds.
                            */
                            static final int MIN_TREEIFY_CAPACITY = 64;

                            static class Node<K,V> implements Map.Entry<K,V> {
                            //一旦被构造器初始化,就不可变。
                            final int hash;
                            //结点的键不变,但值可变
                            final K key;
                            V value;
                            //链表结构
                            Node<K,V> next;

                            Node(int hash, K key, V value, Node<K,V> next) {
                            this.hash = hash;
                            this.key = key;
                            this.value = value;
                            this.next = next;
                            }

                            public final K getKey() { return key; }
                            public final V getValue() { return value; }
                            public final String toString() { return key + "=" + value; }

                            //也就是说它自己的hashcode和构造时给它的hash是不一样的
                            public final int hashCode() {
                            return Objects.hashCode(key) ^ Objects.hashCode(value);
                            }

                            public final V setValue(V newValue) {
                            V oldValue = value;
                            value = newValue;
                            return oldValue;
                            }

                            public final boolean equals(Object o) {
                            if (o == this)
                            return true;
                            if (o instanceof Map.Entry) {
                            Map.Entry<?,?> e = (Map.Entry<?,?>)o;
                            if (Objects.equals(key, e.getKey()) &&
                            Objects.equals(value, e.getValue()))
                            return true;
                            }
                            return false;
                            }
                            }

                            /* ----------------静态共用方法-------------- */

                            //hash的计算方法
                            //1
                            static final int hash(Object key) {
                            int h;
                            //逻辑右移
                            return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
                            }

                            //3
                            static Class<?> comparableClassFor(Object x) {
                            if (x instanceof Comparable) {
                            Class<?> c; Type[] ts, as; Type t; ParameterizedType p;
                            if ((c = x.getClass()) == String.class) // bypass checks
                            return c;
                            //检查所有接口
                            if ((ts = c.getGenericInterfaces()) != null) {
                            for (int i = 0; i < ts.length; ++i) {
                            if (((t = ts[i]) instanceof ParameterizedType) &&
                            ((p = (ParameterizedType)t).getRawType() ==
                            Comparable.class) &&
                            (as = p.getActualTypeArguments()) != null &&
                            as.length == 1 && as[0] == c) // type arg is c
                            return c;
                            }
                            }
                            }
                            return null;
                            }

                            @SuppressWarnings({"rawtypes","unchecked"}) // for cast to Comparable
                            static int compareComparables(Class<?> kc, Object k, Object x) {
                            return (x == null || x.getClass() != kc ? 0 :
                            //会调用最新版本的方法
                            ((Comparable)k).compareTo(x));
                            }

                            //这一通操作可以得到比cap大的,且离cap最近的2的幂次方数
                            static final int tableSizeFor(int cap) {
                            int n = cap - 1;
                            n |= n >>> 1;
                            n |= n >>> 2;
                            n |= n >>> 4;
                            n |= n >>> 8;
                            n |= n >>> 16;
                            return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
                            }

                            /* ---------------- Fields -------------- */

                            /*
                            The table, initialized on first use, and resized as necessary.
                            长度是2的幂次或者0【初始】
                            */
                            transient Node<K,V>[] table;

                            //4
                            transient Set<Map.Entry<K,V>> entrySet;

                            //初始为0,每put一次元素就++。
                            transient int size;

                            transient int modCount;

                            //达到此值时hashmap需要增长capacity并且rehash
                            // (可序列化
                            // Additionally, if the table array has not been allocated, this
                            // field holds the initial array capacity, or zero signifying
                            // DEFAULT_INITIAL_CAPACITY.)
                            int threshold;

                            final float loadFactor;

                            /* ---------------- Public operations -------------- */

                            public HashMap(int initialCapacity, float loadFactor) {
                            if (initialCapacity < 0)
                            throw new IllegalArgumentException("Illegal initial capacity: " +
                            initialCapacity);
                            if (initialCapacity > MAXIMUM_CAPACITY)
                            initialCapacity = MAXIMUM_CAPACITY;
                            if (loadFactor <= 0 || Float.isNaN(loadFactor))
                            throw new IllegalArgumentException("Illegal load factor: " +
                            loadFactor);
                            this.loadFactor = loadFactor;
                            this.threshold = tableSizeFor(initialCapacity);
                            }

                            public HashMap(int initialCapacity) {
                            this(initialCapacity, DEFAULT_LOAD_FACTOR);
                            }

                            public HashMap() {
                            this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
                            }

                            public HashMap(Map<? extends K, ? extends V> m) {
                            this.loadFactor = DEFAULT_LOAD_FACTOR;
                            putMapEntries(m, false);
                            }

                            //Implements Map.putAll and 上面的Map constructor的辅助方法
                            //evict – false when initially constructing this map, else true
                            final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
                            int s = m.size();
                            if (s > 0) {
                            if (table == null) { // pre-size
                            //+1保证了至少比m大
                            float ft = ((float)s / loadFactor) + 1.0F;
                            int t = ((ft < (float)MAXIMUM_CAPACITY) ?
                            (int)ft : MAXIMUM_CAPACITY);
                            if (t > threshold)
                            threshold = tableSizeFor(t);
                            //延迟resize,随处可见的懒汉思想,很聪明
                            }
                            else if (s > threshold)
                            //就地resize
                            resize();
                            for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
                            K key = e.getKey();
                            V value = e.getValue();
                            putVal(hash(key), key, value, false, evict);
                            }
                            }
                            }

                            public int size() {
                            return size;
                            }

                            public boolean isEmpty() {
                            return size == 0;
                            }

                            public V get(Object key) {
                            Node<K,V> e;
                            return (e = getNode(hash(key), key)) == null ? null : e.value;
                            }

                            final Node<K,V> getNode(int hash, Object key) {
                            Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
                            if ((tab = table) != null && (n = tab.length) > 0 &&
                            (first = tab[(n - 1) & hash]) != null) {
                            if (first.hash == hash && // always check first node
                            ((k = first.key) == key || (key != null && key.equals(k))))
                            return first;
                            if ((e = first.next) != null) {
                            if (first instanceof TreeNode)
                            return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                            do {
                            if (e.hash == hash &&
                            ((k = e.key) == key || (key != null && key.equals(k))))
                            return e;
                            } while ((e = e.next) != null);
                            }
                            }
                            return null;
                            }

                            public boolean containsKey(Object key) {
                            return getNode(hash(key), key) != null;
                            }

                            //put方法的实现
                            public V put(K key, V value) {
                            //计算key的哈希值
                            return putVal(hash(key), key, value, false, true);
                            }

                            //evict – false when initially constructing this map, else true
                            //Implements Map.put and related methods.
                            final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                            boolean evict) {
                            Node<K,V>[] tab; Node<K,V> p; int n, i;
                            if ((tab = table) == null || (n = tab.length) == 0)
                            //此处调用resize初始化
                            n = (tab = resize()).length;
                            //n为table大小
                            //首先先找到所在桶
                            //如果所在桶不存在,就直接申请一个新桶(结点)放
                            //2此处找桶的方式
                            if ((p = tab[i = (n - 1) & hash]) == null)
                            tab[i] = newNode(hash, key, value, null);
                            //所在桶存在
                            else {
                            //e为要塞进去value的结点,k为临时变量,用于存储key值
                            Node<K,V> e; K k;
                            //如果p的哈希值为key的哈希值,并且p的key==key,说明键本来就存在,并且正好是桶内第一个元素,只需修改旧键值对的value就行
                            if (p.hash == hash &&
                            ((k = p.key) == key || (key != null && key.equals(k))))
                            //e=旧结点
                            e = p;
                            //否则需要沿着桶的结构继续往下找,这时候就需要看桶内用的是树状结构还是顺序结构了
                            //如果此时用的是树状结构
                            else if (p instanceof TreeNode)
                            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
                            //用的是顺序结构
                            else {
                            for (int binCount = 0; ; ++binCount) {
                            //走到桶尽头,此时e==NULL
                            if ((e = p.next) == null) {
                            p.next = newNode(hash, key, value, null);
                            //到达临界点,需要树化
                            if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                            break;
                            }
                            //一直走,直到找到
                            if (e.hash == hash &&
                            ((k = e.key) == key || (key != null && key.equals(k))))
                            break;
                            //两个指针来回交替往下走
                            p = e;
                            }
                            }
                            //上面可以看到,只有原来就存在键值对才会满足此条件
                            if (e != null) { // existing mapping for key
                            V oldValue = e.value;
                            //onlyIfAbsent – if true, don't change existing value 除非旧值为空
                            if (!onlyIfAbsent || oldValue == null)
                            e.value = value;
                            //空操作,方便LinkedHashMap的后续实现
                            afterNodeAccess(e);
                            //存在旧键值对的情况至此结束
                            return oldValue;
                            }
                            }
                            //走到这说明是新建了一个结点
                            ++modCount;
                            if (++size > threshold)
                            resize();
                            //空操作,方便LinkedHashMap的后续实现
                            afterNodeInsertion(evict);
                            return null;
                            }

                            //Initializes or doubles table size.
                            final Node<K,V>[] resize() {
                            Node<K,V>[] oldTab = table;
                            int oldCap = (oldTab == null) ? 0 : oldTab.length;
                            int oldThr = threshold;
                            int newCap, newThr = 0;

                            //决定newCap和newThr
                            if (oldCap > 0) {
                            if (oldCap >= MAXIMUM_CAPACITY) {
                            threshold = Integer.MAX_VALUE;
                            return oldTab;
                            }
                            //扩容两倍
                            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                            oldCap >= DEFAULT_INITIAL_CAPACITY)
                            newThr = oldThr << 1; // double threshold
                            }
                            else if (oldThr > 0) // initial capacity was placed in threshold
                            //因为此时capacity已经需要向threshold转变了,因而newThr需要再计算
                            newCap = oldThr;
                            else { // zero initial threshold signifies using defaults
                            newCap = DEFAULT_INITIAL_CAPACITY;
                            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
                            }
                            if (newThr == 0) {
                            float ft = (float)newCap * loadFactor;
                            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                            (int)ft : Integer.MAX_VALUE);
                            }
                            threshold = newThr;

                            @SuppressWarnings({"rawtypes","unchecked"})
                            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
                            table = newTab;
                            if (oldTab != null) {
                            //需要复制原oldTab中的每个结点
                            for (int j = 0; j < oldCap; ++j) {
                            Node<K,V> e;
                            if ((e = oldTab[j]) != null) {
                            oldTab[j] = null;
                            //该桶只有一个结点
                            if (e.next == null)
                            newTab[e.hash & (newCap - 1)] = e;
                            else if (e instanceof TreeNode)
                            ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                            else { // preserve order
                            //5
                            Node<K,V> loHead = null, loTail = null;
                            Node<K,V> hiHead = null, hiTail = null;
                            Node<K,V> next;
                            do {
                            next = e.next;
                            if ((e.hash & oldCap) == 0) {
                            if (loTail == null)
                            loHead = e;
                            else
                            loTail.next = e;
                            loTail = e;
                            }
                            else {
                            if (hiTail == null)
                            hiHead = e;
                            else
                            hiTail.next = e;
                            hiTail = e;
                            }
                            } while ((e = next) != null);
                            if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                            }
                            if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                            }
                            }
                            }
                            }
                            }
                            return newTab;
                            }

                            //树化桶
                            final void treeifyBin(Node<K,V>[] tab, int hash) {
                            int n, index; Node<K,V> e;
                            //如果表的一个桶结点数大于8(TREEIFY_THRESHOLD),但是表的总结点数小于64(MIN_TREEIFY_CAPACITY)也是不会树化的,只会resize重新hash
                            if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
                            resize();
                            //需要树化
                            //取得该桶的头结点e
                            else if ((e = tab[index = (n - 1) & hash]) != null) {
                            TreeNode<K,V> hd = null, tl = null;
                            do {
                            //replacementTreeNode return new TreeNode<>(p.hash, p.key, p.value, next);
                            TreeNode<K,V> p = replacementTreeNode(e, null);
                            if (tl == null)
                            //此时有0个结点
                            hd = p;
                            else {
                            p.prev = tl;
                            tl.next = p;
                            }
                            tl = p;
                            } while ((e = e.next) != null);
                            if ((tab[index] = hd) != null)
                            //只树化该桶
                            hd.treeify(tab);
                            }
                            }

                            //对于重复键需替换
                            public void putAll(Map<? extends K, ? extends V> m) {
                            putMapEntries(m, true);
                            }

                            //Returns:the previous value
                            public V remove(Object key) {
                            Node<K,V> e;
                            return (e = removeNode(hash(key), key, null, false, true)) == null ?
                            null : e.value;
                            }

                            //matchValue – if true only remove if value is equal
                            //value – the value to match if matchValue, else ignored
                            //movable – if false do not move other nodes while removing用于树
                            final Node<K,V> removeNode(int hash, Object key, Object value,
                            boolean matchValue, boolean movable) {
                            Node<K,V>[] tab; Node<K,V> p; int n, index;
                            //table和键都存在
                            if ((tab = table) != null && (n = tab.length) > 0 &&
                            (p = tab[index = (n - 1) & hash]) != null) {
                            //node为要移走的结点
                            Node<K,V> node = null, e; K k; V v;
                            //检查头结点
                            if (p.hash == hash &&
                            ((k = p.key) == key || (key != null && key.equals(k))))
                            node = p;
                            else if ((e = p.next) != null) {
                            if (p instanceof TreeNode)
                            node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
                            else {
                            do {
                            if (e.hash == hash &&
                            ((k = e.key) == key ||
                            (key != null && key.equals(k)))) {
                            node = e;
                            break;
                            }
                            p = e;
                            } while ((e = e.next) != null);
                            }
                            }
                            //需要移走
                            if (node != null && (!matchValue || (v = node.value) == value ||
                            (value != null && value.equals(v)))) {
                            if (node instanceof TreeNode)
                            ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
                            //由上文可知,此时node==p==头结点
                            //能找到这个差异点也是真牛逼
                            else if (node == p)
                            tab[index] = node.next;
                            //此时p.next=node
                            else
                            p.next = node.next;
                            ++modCount;
                            --size;
                            afterNodeRemoval(node);
                            return node;
                            }
                            }
                            return null;
                            }

                            public void clear() {
                            Node<K,V>[] tab;
                            modCount++;
                            if ((tab = table) != null && size > 0) {
                            size = 0;
                            for (int i = 0; i < tab.length; ++i)
                            tab[i] = null;//我知道你要说什么:let GC do its work
                            }
                            }

                            //遍历。有树优化的话可以减少时间开销。
                            public boolean containsValue(Object value) {
                            Node<K,V>[] tab; V v;
                            if ((tab = table) != null && size > 0) {
                            for (int i = 0; i < tab.length; ++i) {
                            for (Node<K,V> e = tab[i]; e != null; e = e.next) {
                            if ((v = e.value) == value ||
                            (value != null && value.equals(v)))
                            return true;
                            }
                            }
                            }
                            return false;
                            }

                            public Set<K> keySet() {
                            Set<K> ks = keySet;
                            if (ks == null) {
                            //是HashMap自己实现的keyset
                            ks = new KeySet();
                            keySet = ks;
                            }
                            return ks;
                            }

                            final class KeySet extends AbstractSet<K> {
                            public final int size() { return size; }
                            public final void clear() { HashMap.this.clear(); }
                            public final Iterator<K> iterator() { return new KeyIterator(); }
                            public final boolean contains(Object o) { return containsKey(o); }
                            public final boolean remove(Object key) {
                            return removeNode(hash(key), key, null, false, true) != null;
                            }
                            public final Spliterator<K> spliterator() {
                            return new KeySpliterator<>(HashMap.this, 0, -1, 0, 0);
                            }
                            public final void forEach(Consumer<? super K> action) {
                            Node<K,V>[] tab;
                            if (action == null)
                            throw new NullPointerException();
                            if (size > 0 && (tab = table) != null) {
                            int mc = modCount;
                            for (int i = 0; i < tab.length; ++i) {
                            for (Node<K,V> e = tab[i]; e != null; e = e.next)
                            action.accept(e.key);
                            }
                            if (modCount != mc)
                            throw new ConcurrentModificationException();
                            }
                            }
                            }

                            public Collection<V> values() {
                            Collection<V> vs = values;
                            if (vs == null) {
                            vs = new Values();
                            values = vs;
                            }
                            return vs;
                            }

                            final class Values extends AbstractCollection<V> {
                            public final int size() { return size; }
                            public final void clear() { HashMap.this.clear(); }
                            public final Iterator<V> iterator() { return new ValueIterator(); }
                            public final boolean contains(Object o) { return containsValue(o); }
                            public final Spliterator<V> spliterator() {
                            return new ValueSpliterator<>(HashMap.this, 0, -1, 0, 0);
                            }
                            public final void forEach(Consumer<? super V> action) {
                            Node<K,V>[] tab;
                            if (action == null)
                            throw new NullPointerException();
                            if (size > 0 && (tab = table) != null) {
                            int mc = modCount;
                            for (int i = 0; i < tab.length; ++i) {
                            for (Node<K,V> e = tab[i]; e != null; e = e.next)
                            action.accept(e.value);
                            }
                            if (modCount != mc)
                            throw new ConcurrentModificationException();
                            }
                            }
                            }

                            public Set<Map.Entry<K,V>> entrySet() {
                            Set<Map.Entry<K,V>> es;
                            return (es = entrySet) == null ? (entrySet = new EntrySet()) : es;
                            }

                            final class EntrySet extends AbstractSet<Map.Entry<K,V>> {
                            public final int size() { return size; }
                            public final void clear() { HashMap.this.clear(); }
                            public final Iterator<Map.Entry<K,V>> iterator() {
                            return new EntryIterator();
                            }
                            //不如直接用map的contains、remove等等等
                            public final boolean contains(Object o) {
                            if (!(o instanceof Map.Entry))
                            return false;
                            Map.Entry<?,?> e = (Map.Entry<?,?>) o;
                            Object key = e.getKey();
                            Node<K,V> candidate = getNode(hash(key), key);
                            return candidate != null && candidate.equals(e);
                            }
                            public final boolean remove(Object o) {
                            if (o instanceof Map.Entry) {
                            Map.Entry<?,?> e = (Map.Entry<?,?>) o;
                            Object key = e.getKey();
                            Object value = e.getValue();
                            //只在值相等的时候remove
                            return removeNode(hash(key), key, value, true, true) != null;
                            }
                            return false;
                            }
                            public final Spliterator<Map.Entry<K,V>> spliterator() {
                            return new EntrySpliterator<>(HashMap.this, 0, -1, 0, 0);
                            }
                            public final void forEach(Consumer<? super Map.Entry<K,V>> action) {
                            Node<K,V>[] tab;
                            if (action == null)
                            throw new NullPointerException();
                            if (size > 0 && (tab = table) != null) {
                            int mc = modCount;
                            for (int i = 0; i < tab.length; ++i) {
                            for (Node<K,V> e = tab[i]; e != null; e = e.next)
                            action.accept(e);
                            }
                            if (modCount != mc)
                            throw new ConcurrentModificationException();
                            }
                            }
                            }

                            // Overrides of JDK8 Map extension methods

                            //Returns the value to which the specified key is mapped,
                            //or defaultValue if this map contains no mapping for the key.
                            @Override
                            public V getOrDefault(Object key, V defaultValue) {
                            Node<K,V> e;
                            return (e = getNode(hash(key), key)) == null ? defaultValue : e.value;
                            }

                            //If the specified key is not already associated with a value (or is mapped to null)
                            //associates it with the given value and returns null,
                            //else returns the current value.
                            @Override
                            public V putIfAbsent(K key, V value) {
                            return putVal(hash(key), key, value, true, true);
                            }

                            //只有在curVal==value且key存在的情况下才remove掉键值对
                            @Override
                            public boolean remove(Object key, Object value) {
                            return removeNode(hash(key), key, value, true, true) != null;
                            }

                            @Override
                            public boolean replace(K key, V oldValue, V newValue) {
                            Node<K,V> e; V v;
                            if ((e = getNode(hash(key), key)) != null &&
                            ((v = e.value) == oldValue || (v != null && v.equals(oldValue)))) {
                            e.value = newValue;
                            afterNodeAccess(e);
                            return true;
                            }
                            return false;
                            }

                            @Override
                            public V replace(K key, V value) {
                            Node<K,V> e;
                            if ((e = getNode(hash(key), key)) != null) {
                            V oldValue = e.value;
                            e.value = value;
                            afterNodeAccess(e);
                            return oldValue;
                            }
                            return null;
                            }

                            //如果key对应键值对不存在,就创建一个新的,并把它的值置为paramFunction(key)
                            //返回的是修改后的值。
                            //其他详见Map的第4点
                            @Override
                            public V computeIfAbsent(K key,
                            Function<? super K, ? extends V> mappingFunction) {
                            if (mappingFunction == null)
                            throw new NullPointerException();
                            int hash = hash(key);
                            Node<K,V>[] tab; Node<K,V> first; int n, i;
                            int binCount = 0;
                            TreeNode<K,V> t = null;
                            Node<K,V> old = null;
                            if (size > threshold || (tab = table) == null ||
                            (n = tab.length) == 0)
                            n = (tab = resize()).length;
                            if ((first = tab[i = (n - 1) & hash]) != null) {
                            if (first instanceof TreeNode)
                            old = (t = (TreeNode<K,V>)first).getTreeNode(hash, key);
                            else {
                            Node<K,V> e = first; K k;
                            do {
                            if (e.hash == hash &&
                            ((k = e.key) == key || (key != null && key.equals(k)))) {
                            old = e;
                            break;
                            }
                            ++binCount;
                            } while ((e = e.next) != null);
                            }
                            V oldValue;
                            if (old != null && (oldValue = old.value) != null) {
                            afterNodeAccess(old);
                            return oldValue;
                            }
                            }
                            V v = mappingFunction.apply(key);
                            if (v == null) {
                            return null;
                            } else if (old != null) {
                            old.value = v;
                            afterNodeAccess(old);
                            return v;
                            }
                            else if (t != null)
                            t.putTreeVal(this, tab, hash, key, v);
                            else {
                            tab[i] = newNode(hash, key, v, first);
                            if (binCount >= TREEIFY_THRESHOLD - 1)
                            treeifyBin(tab, hash);
                            }
                            ++modCount;
                            //++size后不用再check是否>threshold吗 ?为啥要交给上面一开始的时候判断
                            ++size;
                            afterNodeInsertion(true);
                            return v;
                            }

                            //return 新值
                            public V computeIfPresent(K key,
                            BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
                            if (remappingFunction == null)
                            throw new NullPointerException();
                            Node<K,V> e; V oldValue;
                            int hash = hash(key);
                            if ((e = getNode(hash, key)) != null &&
                            (oldValue = e.value) != null) {
                            V v = remappingFunction.apply(key, oldValue);
                            if (v != null) {
                            e.value = v;
                            afterNodeAccess(e);
                            return v;
                            }
                            else
                            removeNode(hash, key, null, false, true);
                            }
                            return null;
                            }

                            @Override
                            public V compute(K key,
                            BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
                            if (remappingFunction == null)
                            throw new NullPointerException();
                            int hash = hash(key);
                            Node<K,V>[] tab; Node<K,V> first; int n, i;
                            int binCount = 0;
                            TreeNode<K,V> t = null;
                            Node<K,V> old = null;
                            if (size > threshold || (tab = table) == null ||
                            (n = tab.length) == 0)
                            n = (tab = resize()).length;
                            if ((first = tab[i = (n - 1) & hash]) != null) {
                            if (first instanceof TreeNode)
                            old = (t = (TreeNode<K,V>)first).getTreeNode(hash, key);
                            else {
                            Node<K,V> e = first; K k;
                            do {
                            if (e.hash == hash &&
                            ((k = e.key) == key || (key != null && key.equals(k)))) {
                            old = e;
                            break;
                            }
                            ++binCount;
                            } while ((e = e.next) != null);
                            }
                            }
                            V oldValue = (old == null) ? null : old.value;
                            V v = remappingFunction.apply(key, oldValue);
                            if (old != null) {
                            if (v != null) {
                            old.value = v;
                            afterNodeAccess(old);
                            }
                            else
                            removeNode(hash, key, null, false, true);
                            }
                            else if (v != null) {
                            if (t != null)
                            t.putTreeVal(this, tab, hash, key, v);
                            else {
                            tab[i] = newNode(hash, key, v, first);
                            if (binCount >= TREEIFY_THRESHOLD - 1)
                            treeifyBin(tab, hash);
                            }
                            ++modCount;
                            ++size;
                            afterNodeInsertion(true);
                            }
                            return v;
                            }

                            @Override
                            public V merge(K key, V value,
                            BiFunction<? super V, ? super V, ? extends V> remappingFunction) {
                            if (value == null)
                            throw new NullPointerException();
                            if (remappingFunction == null)
                            throw new NullPointerException();
                            int hash = hash(key);
                            Node<K,V>[] tab; Node<K,V> first; int n, i;
                            int binCount = 0;
                            TreeNode<K,V> t = null;
                            Node<K,V> old = null;
                            if (size > threshold || (tab = table) == null ||
                            (n = tab.length) == 0)
                            n = (tab = resize()).length;
                            if ((first = tab[i = (n - 1) & hash]) != null) {
                            if (first instanceof TreeNode)
                            old = (t = (TreeNode<K,V>)first).getTreeNode(hash, key);
                            else {
                            Node<K,V> e = first; K k;
                            do {
                            if (e.hash == hash &&
                            ((k = e.key) == key || (key != null && key.equals(k)))) {
                            old = e;
                            break;
                            }
                            ++binCount;
                            } while ((e = e.next) != null);
                            }
                            }
                            if (old != null) {
                            V v;
                            if (old.value != null)
                            v = remappingFunction.apply(old.value, value);
                            else
                            v = value;
                            if (v != null) {
                            old.value = v;
                            afterNodeAccess(old);
                            }
                            else
                            removeNode(hash, key, null, false, true);
                            return v;
                            }
                            if (value != null) {
                            if (t != null)
                            t.putTreeVal(this, tab, hash, key, value);
                            else {
                            tab[i] = newNode(hash, key, value, first);
                            if (binCount >= TREEIFY_THRESHOLD - 1)
                            treeifyBin(tab, hash);
                            }
                            ++modCount;
                            ++size;
                            afterNodeInsertion(true);
                            }
                            return value;
                            }

                            @Override
                            public void forEach(BiConsumer<? super K, ? super V> action) {
                            Node<K,V>[] tab;
                            if (action == null)
                            throw new NullPointerException();
                            if (size > 0 && (tab = table) != null) {
                            int mc = modCount;
                            for (int i = 0; i < tab.length; ++i) {
                            for (Node<K,V> e = tab[i]; e != null; e = e.next)
                            action.accept(e.key, e.value);
                            }
                            if (modCount != mc)
                            throw new ConcurrentModificationException();
                            }
                            }

                            @Override
                            public void replaceAll(BiFunction<? super K, ? super V, ? extends V> function) {
                            Node<K,V>[] tab;
                            if (function == null)
                            throw new NullPointerException();
                            if (size > 0 && (tab = table) != null) {
                            int mc = modCount;
                            for (int i = 0; i < tab.length; ++i) {
                            for (Node<K,V> e = tab[i]; e != null; e = e.next) {
                            e.value = function.apply(e.key, e.value);
                            }
                            }
                            if (modCount != mc)
                            throw new ConcurrentModificationException();
                            }
                            }

                            // Cloning and serialization

                            @SuppressWarnings("unchecked")
                            @Override
                            public Object clone() {
                            HashMap<K,V> result;
                            try {
                            result = (HashMap<K,V>)super.clone();
                            } catch (CloneNotSupportedException e) {
                            // this shouldn't happen, since we are Cloneable
                            throw new InternalError(e);
                            }
                            result.reinitialize();
                            result.putMapEntries(this, false);
                            return result;
                            }

                            // These methods are also used when serializing HashSets
                            final float loadFactor() { return loadFactor; }
                            final int capacity() {
                            return (table != null) ? table.length :
                            (threshold > 0) ? threshold :
                            DEFAULT_INITIAL_CAPACITY;
                            }

                            private void writeObject(java.io.ObjectOutputStream s)
                            throws IOException {...}

                            private void readObject(ObjectInputStream s)
                            throws IOException, ClassNotFoundException {...}

                            // Support for resetting final field during deserializing
                            private static final class UnsafeHolder {...}

                            // iterators

                            //7
                            abstract class HashIterator {
                            Node<K,V> next; // next entry to return
                            Node<K,V> current; // current entry
                            int expectedModCount; // for fast-fail
                            int index; // current slot

                            HashIterator() {
                            expectedModCount = modCount;
                            Node<K,V>[] t = table;
                            current = next = null;
                            index = 0;
                            //指向第一个非空表项
                            if (t != null && size > 0) { // advance to first entry
                            do {} while (index < t.length && (next = t[index++]) == null);
                            }
                            }

                            public final boolean hasNext() {
                            return next != null;
                            }

                            final Node<K,V> nextNode() {
                            Node<K,V>[] t;
                            Node<K,V> e = next;
                            if (modCount != expectedModCount)
                            throw new ConcurrentModificationException();
                            if (e == null)
                            throw new NoSuchElementException();
                            //移动桶内指针
                            if ((next = (current = e).next) == null && (t = table) != null) {
                            //如果桶内表到达尽头,则移动选择桶的指针
                            do {} while (index < t.length && (next = t[index++]) == null);
                            }
                            return e;
                            }

                            public final void remove() {
                            Node<K,V> p = current;
                            if (p == null)
                            throw new IllegalStateException();
                            if (modCount != expectedModCount)
                            throw new ConcurrentModificationException();
                            current = null;
                            K key = p.key;
                            removeNode(hash(key), key, null, false, false);
                            expectedModCount = modCount;
                            }
                            }

                            final class KeyIterator extends HashIterator
                            implements Iterator<K> {
                            public final K next() { return nextNode().key; }
                            }

                            final class ValueIterator extends HashIterator
                            implements Iterator<V> {
                            public final V next() { return nextNode().value; }
                            }

                            final class EntryIterator extends HashIterator
                            implements Iterator<Map.Entry<K,V>> {
                            public final Map.Entry<K,V> next() { return nextNode(); }
                            }

                            // spliterators

                            static class HashMapSpliterator<K,V> {...}

                            static final class KeySpliterator<K,V>
                            extends HashMapSpliterator<K,V>
                            implements Spliterator<K> {...}

                            static final class ValueSpliterator<K,V>
                            extends HashMapSpliterator<K,V>
                            implements Spliterator<V> {...}

                            static final class EntrySpliterator<K,V>
                            extends HashMapSpliterator<K,V>
                            implements Spliterator<Map.Entry<K,V>> {...}

                            // LinkedHashMap support

                            // Create a regular (non-tree) node
                            Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) {
                            return new Node<>(hash, key, value, next);
                            }

                            // For conversion from TreeNodes to plain nodes
                            Node<K,V> replacementNode(Node<K,V> p, Node<K,V> next) {
                            return new Node<>(p.hash, p.key, p.value, next);
                            }

                            // Create a tree bin node
                            TreeNode<K,V> newTreeNode(int hash, K key, V value, Node<K,V> next) {
                            return new TreeNode<>(hash, key, value, next);
                            }

                            // For treeifyBin
                            TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) {
                            return new TreeNode<>(p.hash, p.key, p.value, next);
                            }

                            void reinitialize() {
                            table = null;
                            entrySet = null;
                            keySet = null;
                            values = null;
                            modCount = 0;
                            threshold = 0;
                            size = 0;
                            }

                            // Callbacks to allow LinkedHashMap post-actions
                            void afterNodeAccess(Node<K,V> p) { }
                            void afterNodeInsertion(boolean evict) { }
                            void afterNodeRemoval(Node<K,V> p) { }

                            // Called only from writeObject, to ensure compatible ordering.
                            void internalWriteEntries(java.io.ObjectOutputStream s) throws IOException {...}

                            // Tree bins

                            //6红黑树介绍,此部分具体的红黑树实现省略
                            static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {...}
                            }
                            -

                            modCount不能保证绝对的并发安全,因为它只负责防范结构改变,而不负责看某位置的数据更新。

                            -

                            在现实中要实现对集合的边迭代边修改,下面三种方式都是错的:

                            - +

                            其中:

                              +
                            1. hash()

                              -

                              第一种方法也可以把for里的size换成list.size()

                              -

                              其中乐观锁与悲观锁可见:乐观锁

                              +

                              hash=原hashcode^(原hashcode逻辑右移16位)

                              +

                              这样的话,由于右移16位补零,此时高位的所有比特位都跟原来一样,低位的比特位变成了融合高低位特点的东西,这样就可以减少冲突,增加均匀性

                              +
                            2. +
                            3. table[(n-1)&hash]

                              具体看这个视频,讲得非常不错

                              +

                              【Java面试必问】HashMap中是如何计算数组下标的?

                              +

                              假设table此时为默认长度16.则n-1=15

                              +

                              写出15的二进制形式:0000 1111,可以发现,任何数跟它相与,结果都一定为0000 xxxx,永不越界。

                              +

                              写出16的二进制形式:0001 0000,可以发现,任何数跟它相与,结果都一定为16或者0.

                              +

                              可以发现15有非常好的性质。

                              +

                              而扩展出来,任何2的幂次方-1都具有这样的良好的性质。**这也是为什么hashmap要求表的长度应该为2的幂次。**

                              +

                              而且,除了不会越界,还有一点就是,这个任何数与15相与的与操作就相当于,任何数对16取余的取余操作。这点实在是佩服啊,把复杂的取余操作在该场景下直接用一个位运算就搞定了。

                              +
                            4. +
                            5. comparableClassFor

                              树状结构时结点的默认排序方式是by hashCode。但如果两个结点元素之间是同一个class C,并且这个C实现了Comparable方法,那么就不会按照它们的hashCode比较,而是会调用class C的compareTo方法。

                              +

                              (We conservatively(保守地) check generic types via reflection to validate(证实) this – see method comparableClassFor).

                              +

                              也就是说这个comparableClassFor方法的意图就是,如果这个类是comparable的,就返回它具体类型,如果不是返回null。

                              +
                            6. +
                            7. entrySet

                              不同于AbstractMap中entrySet的核心作用,HashMap的put、get、clear等等等核心函数都不依赖于entrySet了,毕竟结构改变得比较多了。因而这里的entrySet字段保留,只是为了呼应AbstractMap中keyset和valueset的实现,以及补充AbstractMap中未给出的EntrySet实现。

                              +
                            8. +
                            9. resize()扩容旧表到新表的转移

                              此时需要复制oldTab中的所有结点。但注意,由于此时发生了扩容,hash的计算发生了变化,因而不能全部照搬不动oldTab中的下标,否则产生错误。因而我们需要了解一下如何调整下标。

                              +

                              首先由代码可得,对于oldTab!=NULL的情况下newCap一定是扩为原来的两倍的。因而以下只需讨论扩容为两倍的情况。

                              +

                              由第2点可知,假设现在容量为16,扩容为原来的两倍,则hash掩码应该为0000 1111,扩容后,hash掩码应该为0001 1111,可见就只是多了一位,因而,oldTab中,若这一位的值为0,则在新表和旧表中位置的下标应该是一样的;若这一位的值为1,则新表下标=旧表下标+offset,offset正是等于0001 0000.而这个“0001 0000”,正是oldCap!

                              +

                              对于容量为其他值,全部道理都是一样的。

                              +

                              因而我们要做的,是对旧表的每一个桶内的所有结点,把它们分成两类,一类为(e.hash & oldCap) == 0【也就是这一位值为0 情况】和(e.hash & oldCap) == 1,然后对这两类进行在新表中分别映射即可。这段代码便做了这样的事。

                              +
                                                //5
                              //low index head,下标保持不变
                              Node<K,V> loHead = null, loTail = null;
                              //high index head,下标需要增长偏移量
                              Node<K,V> hiHead = null, hiTail = null;
                              Node<K,V> next;
                              do {
                              next = e.next;
                              //第一类
                              if ((e.hash & oldCap) == 0) {
                              //一个简单的队列操作
                              if (loTail == null)
                              loHead = e;
                              else
                              loTail.next = e;
                              loTail = e;
                              }
                              //第二类
                              else {
                              if (hiTail == null)
                              hiHead = e;
                              else
                              hiTail.next = e;
                              hiTail = e;
                              }
                              } while ((e = next) != null);
                              //对于第一类
                              if (loTail != null) {
                              loTail.next = null;
                              newTab[j] = loHead;
                              }
                              //对于第二类
                              if (hiTail != null) {
                              hiTail.next = null;
                              newTab[j + oldCap] = hiHead;
                            10. +
                            11. 红黑树

                              红黑树快速入门

                              +

                              这篇文章也写得很好:

                              +

                              算法:基于红黑树的 TreeMap

                              +
                            12. +
                            13. HashIterator

                              注意点有二:

                              +

                              ①不继承Iterator接口

                              +

                              ②抽象,具体实现类为EntryIterator、KeyIterator和ValueIterator

                              +

                              ③map的接口定义是没有iterator的,因此map不能通过hashiterator迭代,只能通过其vie来实现【三个具体实现类】

                              +
                            14. +
                            +

                            LinkedHashMap

                            哈希表+链表/红黑树+有序队列

                            -
                              -
                            • 乐观锁:总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,只在更新的时候会判断一下在此期间别人有没有去更新这个数据。
                            • -
                            +

                            Hash table and linked list implementation of the Map interface, with predictable iteration order.

                            +

                            This implementation differs from HashMap in that it maintains a doubly-linked list running through all of its entries.

                            +

                            This linked list defines the iteration ordering, which is normally the order in which keys were inserted into the map (insertion-order).有序,顺序为元素插入的顺序

                            +

                            Note that insertion order is not affected if a key is re-inserted into the map. 当修改key的value值时,key的插入序不变

                            +

                            此实现既让hashmap变得有序,又不会像TreeMap一样有高成本。

                            +

                            It can be used to produce a copy of a map that has the same order as the original, regardless of the original map’s implementation.

                            + + +

                            这样可以保持copymap的原有顺序

                            +

                            A special constructor is provided to create a linked hash map whose order of iteration is the order in which its entries were last accessed, from least-recently accessed to most-recently (access-order). This kind of map is well-suited to building LRU caches. 可以有一个排序方式,顺序为最近最少访问->最近访问,这可以用来构建LRU cache【LRU是Least Recently Used的缩写,即最近最少使用,是一种常用的页面置换算法,选择最近最久未使用的页面予以淘汰。

                            +

                            至于这个“access”怎么定义:

                            -

                            注意“在此期间”的含义是拿到数据到更新数据的这段时间。因为没有加锁,所以别的线程可能会更改。还有一点那就是乐观锁其实是不加锁的来保证某个变量一系列操作原子性的一种方法。

                            +

                            Invoking the put, putIfAbsent, get, getOrDefault, compute, computeIfAbsent, computeIfPresent, or merge methods results in an access to the corresponding entry (assuming it exists after the invocation completes). The replace methods only result in an access of the entry if the value is replaced. The putAll method generates one entry access for each mapping in the specified map, in the order that key-value mappings are provided by the specified map’s entry set iterator.

                            +

                            注意没有remove

                            -
                              -
                            • 悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞,直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java中synchronizedReentrantLock等独占锁就是悲观锁思想的实现。
                            • -
                            +

                            也因此,对map视图【各个set】的访问不算access。【因为不调用任意一个上面方法】

                            +

                            可以重写 removeEldestEntry(Map.Entry) 方法,以在将新映射添加到映射时自动删除陈旧映射的策略。

                            + + + + +

                            //1

                            +

                            Iteration over the collection-views of a LinkedHashMap requires time proportional to the size of the map, regardless of its capacity.不同于hashmap,迭代时间与容量无关。

                            +

                            In access-ordered linked hash maps, merely querying the map with get is a structural modification.注意,对于access-ordered的lhm来说,**get也是一个structural modification,因为可能会修改排序顺序**。所以迭代时只能使用Iterator的next方法来得到结点,迭代器访问不会对accessorder有影响

                            +

                            代码测试:

                            +
                                    LinkedHashMap<String,Integer> map = new LinkedHashMap<>(16,0.75f,true);
                            map.put("Lily",15);
                            map.put("Sam",20);
                            map.put("Mary",11);
                            map.put("Lee",111);

                            for(Iterator i = map.entrySet().iterator();i.hasNext();){
                            map.get("Lily");
                            System.out.println(i.next().toString());
                            }
                            /*
                            Exception in thread "main" java.util.ConcurrentModificationException
                            at java.util.LinkedHashMap$LinkedHashIterator.nextNode(LinkedHashMap.java:719)
                            */
                            +

                            总之意思就是,LinkedHashMap的数据结构:

                            +

                            在HashMap哈希表+链表/红黑树的基础上,添加一个双端队列,该双端队列的作用是来维持内部的有序,因而开销比较大。应该只提供插入序和LRU序,其他需要用到compare的排序方法需要对某些方法(如afternodeXXX)进行重写,或者直接使用sorted map。

                            +

                            LHM的一个很特殊的地方就是,它可以实现一个LRU这样的cache结构,只需要你重载removeEldestEntry return true。还可以在LHM的基础上实现有限长度map,只需要你重载removeEldestEntry 当元素>=某值时返回true。总而言之,你可以建造一个类在LHM的基础上,如果需要对map的长度有限制。

                            +

                            LHM对LRU的实现是,一旦某个结点用到了,就立刻把他移到最队尾,然后每次淘汰淘汰队首。

                            +

                            代码:

                            public class LinkedHashMap<K,V>
                            extends HashMap<K,V>
                            implements Map<K,V>
                            {

                            static class Entry<K,V> extends HashMap.Node<K,V> {
                            //原来只有next的
                            //双端队列
                            Entry<K,V> before, after;
                            Entry(int hash, K key, V value, Node<K,V> next) {
                            super(hash, key, value, next);
                            }
                            }

                            private static final long serialVersionUID = 3801124242820219131L;

                            //The head (eldest) of the doubly linked list.
                            transient LinkedHashMap.Entry<K,V> head;

                            //The tail (youngest) of the doubly linked list.
                            transient LinkedHashMap.Entry<K,V> tail;

                            //true:access顺序 false:插入顺序
                            final boolean accessOrder;

                            // internal utilities

                            // link at the end of list
                            private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
                            LinkedHashMap.Entry<K,V> last = tail;
                            tail = p;
                            if (last == null)
                            head = p;
                            else {
                            p.before = last;
                            last.after = p;
                            }
                            }

                            // apply src's links to dst
                            //相当于用dst把src取代了
                            private void transferLinks(LinkedHashMap.Entry<K,V> src,
                            LinkedHashMap.Entry<K,V> dst) {
                            LinkedHashMap.Entry<K,V> b = dst.before = src.before;
                            LinkedHashMap.Entry<K,V> a = dst.after = src.after;
                            if (b == null)
                            head = dst;
                            else
                            b.after = dst;
                            if (a == null)
                            tail = dst;
                            else
                            a.before = dst;
                            }

                            // overrides of HashMap hook methods

                            void reinitialize() {
                            super.reinitialize();
                            head = tail = null;
                            }

                            Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
                            LinkedHashMap.Entry<K,V> p =
                            new LinkedHashMap.Entry<K,V>(hash, key, value, e);
                            linkNodeLast(p);
                            return p;
                            }

                            Node<K,V> replacementNode(Node<K,V> p, Node<K,V> next) {
                            LinkedHashMap.Entry<K,V> q = (LinkedHashMap.Entry<K,V>)p;
                            LinkedHashMap.Entry<K,V> t =
                            new LinkedHashMap.Entry<K,V>(q.hash, q.key, q.value, next);
                            transferLinks(q, t);
                            return t;
                            }

                            TreeNode<K,V> newTreeNode(int hash, K key, V value, Node<K,V> next) {
                            TreeNode<K,V> p = new TreeNode<K,V>(hash, key, value, next);
                            linkNodeLast(p);
                            return p;
                            }

                            TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) {
                            LinkedHashMap.Entry<K,V> q = (LinkedHashMap.Entry<K,V>)p;
                            TreeNode<K,V> t = new TreeNode<K,V>(q.hash, q.key, q.value, next);
                            transferLinks(q, t);
                            return t;
                            }

                            //用于reove结点之后,之所以要存在就是因为LHM和HM的Node结构不一样,前者多了after和before
                            void afterNodeRemoval(Node<K,V> e) { // unlink
                            LinkedHashMap.Entry<K,V> p =
                            (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
                            p.before = p.after = null;
                            if (b == null)
                            head = a;
                            else
                            b.after = a;
                            if (a == null)
                            tail = b;
                            else
                            a.before = b;
                            }

                            //调用于put、各种compute、merge
                            void afterNodeInsertion(boolean evict) { // possibly remove eldest
                            LinkedHashMap.Entry<K,V> first;
                            //head是最老的结点
                            //如果需要插入新节点同时移去旧结点
                            if (evict && (first = head) != null && removeEldestEntry(first)) {
                            K key = first.key;
                            removeNode(hash(key), key, null, false, true);
                            }
                            }

                            void afterNodeAccess(Node<K,V> e) { // move node to last把用到的结点移到队尾
                            LinkedHashMap.Entry<K,V> last;
                            if (accessOrder && (last = tail) != e) {
                            LinkedHashMap.Entry<K,V> p =
                            (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
                            p.after = null;
                            if (b == null)
                            head = a;
                            else
                            b.after = a;
                            if (a != null)
                            a.before = b;
                            else
                            last = b;
                            if (last == null)
                            head = p;
                            else {
                            p.before = last;
                            last.after = p;
                            }
                            tail = p;
                            ++modCount;
                            }
                            }

                            void internalWriteEntries(java.io.ObjectOutputStream s) throws IOException {
                            for (LinkedHashMap.Entry<K,V> e = head; e != null; e = e.after) {
                            s.writeObject(e.key);
                            s.writeObject(e.value);
                            }
                            }

                            public LinkedHashMap(int initialCapacity, float loadFactor) {
                            super(initialCapacity, loadFactor);
                            accessOrder = false;
                            }

                            public LinkedHashMap(int initialCapacity) {
                            super(initialCapacity);
                            accessOrder = false;
                            }

                            public LinkedHashMap() {
                            super();
                            accessOrder = false;
                            }

                            public LinkedHashMap(Map<? extends K, ? extends V> m) {
                            super();
                            accessOrder = false;
                            putMapEntries(m, false);
                            }

                            //用以构造accessOrder==true的情况
                            public LinkedHashMap(int initialCapacity,
                            float loadFactor,
                            boolean accessOrder) {
                            super(initialCapacity, loadFactor);
                            this.accessOrder = accessOrder;
                            }

                            //遍历构造的队列
                            public boolean containsValue(Object value) {
                            for (LinkedHashMap.Entry<K,V> e = head; e != null; e = e.after) {
                            V v = e.value;
                            if (v == value || (value != null && value.equals(v)))
                            return true;
                            }
                            return false;
                            }

                            public V get(Object key) {
                            Node<K,V> e;
                            if ((e = getNode(hash(key), key)) == null)
                            return null;
                            if (accessOrder)
                            //structural modification
                            afterNodeAccess(e);
                            return e.value;
                            }

                            public V getOrDefault(Object key, V defaultValue) {
                            Node<K,V> e;
                            if ((e = getNode(hash(key), key)) == null)
                            return defaultValue;
                            if (accessOrder)
                            //structural modification
                            afterNodeAccess(e);
                            return e.value;
                            }

                            public void clear() {
                            super.clear();
                            head = tail = null;
                            }

                            /*
                            Returns true if this map should remove its eldest entry.
                            It provides the implementor with the opportunity to remove the eldest entry each time a new one is added.
                            This is useful if the map represents a LRU cache or other interesting implementations
                            */
                            protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
                            return false;
                            }

                            public Set<K> keySet() {
                            Set<K> ks = keySet;
                            if (ks == null) {
                            ks = new LinkedKeySet();
                            keySet = ks;
                            }
                            return ks;
                            }

                            //HashMap中这几个类都是final,所以继承不了了
                            final class LinkedKeySet extends AbstractSet<K> {
                            public final int size() { return size; }
                            public final void clear() { LinkedHashMap.this.clear(); }
                            public final Iterator<K> iterator() {
                            return new LinkedKeyIterator();
                            }
                            public final boolean contains(Object o) { return containsKey(o); }
                            public final boolean remove(Object key) {
                            return removeNode(hash(key), key, null, false, true) != null;
                            }
                            public final Spliterator<K> spliterator() {
                            return Spliterators.spliterator(this, Spliterator.SIZED |
                            Spliterator.ORDERED |
                            Spliterator.DISTINCT);
                            }
                            public final void forEach(Consumer<? super K> action) {
                            if (action == null)
                            throw new NullPointerException();
                            int mc = modCount;
                            //遍历队列
                            for (LinkedHashMap.Entry<K,V> e = head; e != null; e = e.after)
                            action.accept(e.key);
                            //保证此间代码同步
                            if (modCount != mc)
                            throw new ConcurrentModificationException();
                            }
                            }

                            public Collection<V> values() {
                            Collection<V> vs = values;
                            if (vs == null) {
                            vs = new LinkedValues();
                            values = vs;
                            }
                            return vs;
                            }

                            final class LinkedValues extends AbstractCollection<V> {
                            public final int size() { return size; }
                            public final void clear() { LinkedHashMap.this.clear(); }
                            public final Iterator<V> iterator() {
                            return new LinkedValueIterator();
                            }
                            public final boolean contains(Object o) { return containsValue(o); }
                            public final Spliterator<V> spliterator() {
                            return Spliterators.spliterator(this, Spliterator.SIZED |
                            Spliterator.ORDERED);
                            }
                            public final void forEach(Consumer<? super V> action) {
                            if (action == null)
                            throw new NullPointerException();
                            int mc = modCount;
                            for (LinkedHashMap.Entry<K,V> e = head; e != null; e = e.after)
                            action.accept(e.value);
                            if (modCount != mc)
                            throw new ConcurrentModificationException();
                            }
                            }

                            public Set<Map.Entry<K,V>> entrySet() {
                            Set<Map.Entry<K,V>> es;
                            return (es = entrySet) == null ? (entrySet = new LinkedEntrySet()) : es;
                            }

                            final class LinkedEntrySet extends AbstractSet<Map.Entry<K,V>> {
                            public final int size() { return size; }
                            public final void clear() { LinkedHashMap.this.clear(); }
                            public final Iterator<Map.Entry<K,V>> iterator() {
                            return new LinkedEntryIterator();
                            }
                            public final boolean contains(Object o) {
                            if (!(o instanceof Map.Entry))
                            return false;
                            Map.Entry<?,?> e = (Map.Entry<?,?>) o;
                            Object key = e.getKey();
                            Node<K,V> candidate = getNode(hash(key), key);
                            return candidate != null && candidate.equals(e);
                            }
                            public final boolean remove(Object o) {
                            if (o instanceof Map.Entry) {
                            Map.Entry<?,?> e = (Map.Entry<?,?>) o;
                            Object key = e.getKey();
                            Object value = e.getValue();
                            return removeNode(hash(key), key, value, true, true) != null;
                            }
                            return false;
                            }
                            public final Spliterator<Map.Entry<K,V>> spliterator() {
                            return Spliterators.spliterator(this, Spliterator.SIZED |
                            Spliterator.ORDERED |
                            Spliterator.DISTINCT);
                            }
                            public final void forEach(Consumer<? super Map.Entry<K,V>> action) {
                            if (action == null)
                            throw new NullPointerException();
                            int mc = modCount;
                            for (LinkedHashMap.Entry<K,V> e = head; e != null; e = e.after)
                            action.accept(e);
                            if (modCount != mc)
                            throw new ConcurrentModificationException();
                            }
                            }

                            // Map overrides

                            public void forEach(BiConsumer<? super K, ? super V> action) {
                            if (action == null)
                            throw new NullPointerException();
                            int mc = modCount;
                            for (LinkedHashMap.Entry<K,V> e = head; e != null; e = e.after)
                            action.accept(e.key, e.value);
                            if (modCount != mc)
                            throw new ConcurrentModificationException();
                            }

                            public void replaceAll(BiFunction<? super K, ? super V, ? extends V> function) {
                            if (function == null)
                            throw new NullPointerException();
                            int mc = modCount;
                            for (LinkedHashMap.Entry<K,V> e = head; e != null; e = e.after)
                            e.value = function.apply(e.key, e.value);
                            if (modCount != mc)
                            throw new ConcurrentModificationException();
                            }

                            // Iterators

                            abstract class LinkedHashIterator {
                            LinkedHashMap.Entry<K,V> next;
                            LinkedHashMap.Entry<K,V> current;
                            int expectedModCount;

                            LinkedHashIterator() {
                            next = head;
                            expectedModCount = modCount;
                            current = null;
                            }

                            public final boolean hasNext() {
                            return next != null;
                            }

                            final LinkedHashMap.Entry<K,V> nextNode() {
                            LinkedHashMap.Entry<K,V> e = next;
                            if (modCount != expectedModCount)
                            throw new ConcurrentModificationException();
                            if (e == null)
                            throw new NoSuchElementException();
                            current = e;
                            next = e.after;
                            return e;
                            }

                            public final void remove() {
                            Node<K,V> p = current;
                            if (p == null)
                            throw new IllegalStateException();
                            if (modCount != expectedModCount)
                            throw new ConcurrentModificationException();
                            current = null;
                            K key = p.key;
                            removeNode(hash(key), key, null, false, false);
                            expectedModCount = modCount;
                            }
                            }

                            final class LinkedKeyIterator extends LinkedHashIterator
                            implements Iterator<K> {
                            public final K next() { return nextNode().getKey(); }
                            }

                            final class LinkedValueIterator extends LinkedHashIterator
                            implements Iterator<V> {
                            public final V next() { return nextNode().value; }
                            }

                            final class LinkedEntryIterator extends LinkedHashIterator
                            implements Iterator<Map.Entry<K,V>> {
                            public final Map.Entry<K,V> next() { return nextNode(); }
                            }

                            }
                            + +

                            其中:

                              +
                            1. 迭代时间与容量无关

                              LinkedHashMap的结构跟HashMap是一样的,也就是都baked by array。此处为什么“迭代时间与容量无关”,是因为LinkedHashMap内部维护了一个简单的链表队列【包含所有元素】,迭代的时候是对这个队列进行迭代,而不是像HashMap一样通过表迭代。

                              +

                              怪不得读源码时觉得有些地方明明不重写HashMap也可以它却重写了。原来是因为这个性能问题啊

                            -

                            AbstractList帮我们实现了差不多所有方法,除了Tget(int)size()set(int, Object)add(int, E)remove(int) 。因而,接下来的两个实现中,重点关注这些就行。

                            -

                            ArrayList

                            代码

                            -

                            Implements all optional list operations, and permits all elements, including null.

                            -

                            This class is roughly equivalent to Vector, except that it is unsynchronized.

                            -

                            size(),isEmpty(),get(),set(),iterator(),listIterator()的时间复杂符是**常量级别(constant time),add()方法的时间复杂度是可变常量级别(amortized constant time),即为O(n)。大致上说,剩余方法的时间复杂度都是线性时间(linear time)。相较于LinkedList的实现,ArrayList的常量因子(constant factor)**较低。

                            -

                            An application can increase the capacity of an ArrayList instance before adding a large number of elements using the ensureCapacity operation. This may reduce the amount of incremental reallocation.

                            -

                            本身是线程不安全的,但可以通过封装类来实现线程同步。可以使用Collections.synchronizedList method.

                            -
                            List list = Collections.synchronizedList(new ArrayList(...));
                            -
                            -

                            总体来说实现的很多方法跟想象中差别不大,有几个比较惊艳

                            -
                            //1 cloneable
                            public class ArrayList<E> extends AbstractList<E>
                            implements List<E>, RandomAccess, Cloneable, java.io.Serializable
                            {
                            //4
                            private static final long serialVersionUID = 8683452581122892189L;

                            //可以注意一下,初始=10.
                            private static final int DEFAULT_CAPACITY = 10;

                            //Shared empty array instance used for empty instances.
                            //7
                            private static final Object[] EMPTY_ELEMENTDATA = {};

                            private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

                            //5、6
                            /*
                            The array buffer into which the elements of the ArrayList are stored.缓冲
                            ArrayList的容量=此数组的length
                            Any empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA will be expanded to DEFAULT_CAPACITY when the first element is added.
                            */
                            transient Object[] elementData; // non-private to simplify nested class access

                            private int size;

                            public ArrayList(int initialCapacity) {
                            if (initialCapacity > 0) {
                            this.elementData = new Object[initialCapacity];
                            } else if (initialCapacity == 0) {
                            this.elementData = EMPTY_ELEMENTDATA;
                            } else {
                            throw new IllegalArgumentException("Illegal Capacity: "+
                            initialCapacity);
                            }
                            }

                            public ArrayList() {
                            this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
                            }

                            //in the order they are returned by c's iterator.
                            public ArrayList(Collection<? extends E> c) {
                            Object[] a = c.toArray();
                            if ((size = a.length) != 0) {
                            if (c.getClass() == ArrayList.class) {
                            elementData = a;
                            } else {
                            elementData = Arrays.copyOf(a, size, Object[].class);
                            }
                            //size==0
                            } else {
                            // replace with empty array.
                            elementData = EMPTY_ELEMENTDATA;
                            }
                            }

                            //把容量缩小为当前size。An application can use this operation to minimize the storage of an ArrayList instance.
                            public void trimToSize() {
                            //涉及List的结构性改变
                            modCount++;
                            if (size < elementData.length) {
                            elementData = (size == 0)
                            ? EMPTY_ELEMENTDATA
                            : Arrays.copyOf(elementData, size);
                            }
                            }

                            //8
                            public void ensureCapacity(int minCapacity) {
                            int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
                            //如果原size!=0,除非minCapacity=0,否则必须是要扩容一次的【函数要求】,
                            //因此设置为最小值0,以确保下面的if条件一定为true。
                            ? 0
                            //如果为默认大小0,此处是必须扩为默认大小的
                            : DEFAULT_CAPACITY;

                            if (minCapacity > minExpand) {
                            ensureExplicitCapacity(minCapacity);
                            }
                            }

                            private static int calculateCapacity(Object[] elementData, int minCapacity) {
                            if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
                            return Math.max(DEFAULT_CAPACITY, minCapacity);
                            }
                            return minCapacity;
                            }

                            private void ensureCapacityInternal(int minCapacity) {
                            ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
                            }

                            private void ensureExplicitCapacity(int minCapacity) {
                            //修改list结构
                            //若minCapacity<elementData.length,本句modCount++始终执行。
                            modCount++;
                            // overflow-conscious code
                            if (minCapacity - elementData.length > 0)
                            grow(minCapacity);
                            }

                            private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

                            private void grow(int minCapacity) {
                            // overflow-conscious code
                            //capacity=length of elementData
                            int oldCapacity = elementData.length;
                            //每次扩1/2
                            int newCapacity = oldCapacity + (oldCapacity >> 1);
                            if (newCapacity - minCapacity < 0)
                            newCapacity = minCapacity;
                            //防止溢出
                            if (newCapacity - MAX_ARRAY_SIZE > 0)
                            newCapacity = hugeCapacity(minCapacity);
                            // minCapacity is usually close to size, so this is a win:
                            elementData = Arrays.copyOf(elementData, newCapacity);
                            }

                            private static int hugeCapacity(int minCapacity) {
                            if (minCapacity < 0) // overflow
                            throw new OutOfMemoryError();
                            return (minCapacity > MAX_ARRAY_SIZE) ?
                            Integer.MAX_VALUE :
                            MAX_ARRAY_SIZE;
                            }

                            public int size() {
                            return size;
                            }

                            public boolean isEmpty() {
                            return size == 0;
                            }

                            public boolean contains(Object o) {
                            return indexOf(o) >= 0;
                            }

                            public int indexOf(Object o) {
                            if (o == null) {
                            for (int i = 0; i < size; i++)
                            if (elementData[i]==null)
                            return i;
                            } else {
                            for (int i = 0; i < size; i++)
                            if (o.equals(elementData[i]))
                            return i;
                            }
                            return -1;
                            }

                            public int lastIndexOf(Object o) {
                            if (o == null) {
                            for (int i = size-1; i >= 0; i--)
                            if (elementData[i]==null)
                            return i;
                            } else {
                            for (int i = size-1; i >= 0; i--)
                            if (o.equals(elementData[i]))
                            return i;
                            }
                            return -1;
                            }
                            //2
                            public Object clone() {
                            try {
                            //from Object.clone():
                            //the returned object should be obtained by calling super.clone.
                            ArrayList<?> v = (ArrayList<?>) super.clone();
                            //浅拷贝,只拷贝地址
                            v.elementData = Arrays.copyOf(elementData, size);
                            v.modCount = 0;
                            return v;
                            } catch (CloneNotSupportedException e) {
                            // this shouldn't happen, since we are Cloneable
                            throw new InternalError(e);
                            }
                            }

                            public Object[] toArray() {
                            return Arrays.copyOf(elementData, size);
                            }

                            @SuppressWarnings("unchecked")
                            public <T> T[] toArray(T[] a) {
                            if (a.length < size)
                            // Make a new array of a's runtime type, but my contents:
                            return (T[]) Arrays.copyOf(elementData, size, a.getClass());
                            System.arraycopy(elementData, 0, a, 0, size);
                            if (a.length > size)
                            a[size] = null;
                            return a;
                            }

                            // Positional Access Operations

                            @SuppressWarnings("unchecked")
                            E elementData(int index) {
                            return (E) elementData[index];
                            }

                            //这里我挺迷惑的,为什么还要再套一层elementData??
                            public E get(int index) {
                            rangeCheck(index);

                            return elementData(index);
                            }

                            public E set(int index, E element) {
                            rangeCheck(index);

                            E oldValue = elementData(index);
                            elementData[index] = element;
                            return oldValue;
                            }

                            public boolean add(E e) {
                            //既增加了modcount,也保证了capacity够用
                            ensureCapacityInternal(size + 1);
                            elementData[size++] = e;
                            return true;
                            }

                            public void add(int index, E element) {
                            rangeCheckForAdd(index);
                            ensureCapacityInternal(size + 1); // Increments modCount!!
                            System.arraycopy(elementData, index, elementData, index + 1,
                            size - index);//src dest 移动数组
                            elementData[index] = element;
                            size++;
                            }

                            public E remove(int index) {
                            rangeCheck(index);

                            modCount++;
                            E oldValue = elementData(index);

                            int numMoved = size - index - 1;
                            if (numMoved > 0)
                            System.arraycopy(elementData, index+1, elementData, index,
                            numMoved);
                            elementData[--size] = null; // clear to let GC do its work,自动清除无引用对象

                            return oldValue;
                            }

                            public boolean remove(Object o) {
                            if (o == null) {
                            for (int index = 0; index < size; index++)
                            if (elementData[index] == null) {
                            fastRemove(index);
                            return true;
                            }
                            } else {
                            for (int index = 0; index < size; index++)
                            if (o.equals(elementData[index])) {
                            fastRemove(index);
                            return true;
                            }
                            }
                            return false;
                            }

                            //这不是跟上面一模一样吗,为啥还要再写一遍?
                            private void fastRemove(int index) {
                            modCount++;
                            int numMoved = size - index - 1;
                            if (numMoved > 0)
                            System.arraycopy(elementData, index+1, elementData, index,
                            numMoved);
                            elementData[--size] = null; // clear to let GC do its work
                            }

                            public void clear() {
                            modCount++;

                            //9 我能不能直接猛一点:elementData=new Object[elementData.length]?
                            // clear to let GC do its work
                            for (int i = 0; i < size; i++)
                            elementData[i] = null;

                            size = 0;
                            }

                            public boolean addAll(Collection<? extends E> c) {
                            Object[] a = c.toArray();
                            int numNew = a.length;
                            ensureCapacityInternal(size + numNew); // Increments modCount
                            System.arraycopy(a, 0, elementData, size, numNew);
                            size += numNew;
                            return numNew != 0;
                            }

                            public boolean addAll(int index, Collection<? extends E> c) {
                            rangeCheckForAdd(index);

                            Object[] a = c.toArray();
                            int numNew = a.length;
                            ensureCapacityInternal(size + numNew); // Increments modCount

                            int numMoved = size - index;
                            if (numMoved > 0)
                            System.arraycopy(elementData, index, elementData, index + numNew,
                            numMoved);

                            System.arraycopy(a, 0, elementData, index, numNew);
                            size += numNew;
                            return numNew != 0;
                            }

                            protected void removeRange(int fromIndex, int toIndex) {
                            modCount++;
                            int numMoved = size - toIndex;
                            System.arraycopy(elementData, toIndex, elementData, fromIndex,
                            numMoved);

                            // clear to let GC do its work
                            int newSize = size - (toIndex-fromIndex);
                            for (int i = newSize; i < size; i++) {
                            elementData[i] = null;
                            }
                            size = newSize;
                            }

                            private void rangeCheck(int index) {
                            if (index >= size)
                            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
                            }

                            private void rangeCheckForAdd(int index) {
                            if (index > size || index < 0)
                            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
                            }

                            private String outOfBoundsMsg(int index) {
                            return "Index: "+index+", Size: "+size;
                            }

                            public boolean removeAll(Collection<?> c) {
                            Objects.requireNonNull(c);
                            return batchRemove(c, false);
                            }

                            public boolean retainAll(Collection<?> c) {
                            Objects.requireNonNull(c);
                            return batchRemove(c, true);
                            }

                            //这个complement做得很漂亮,兼顾实际意义又统一了代码
                            private boolean batchRemove(Collection<?> c, boolean complement) {
                            //局部变量
                            final Object[] elementData = this.elementData;
                            int r = 0, w = 0;
                            boolean modified = false;

                            try {
                            for (; r < size; r++)
                            if (c.contains(elementData[r]) == complement)
                            //原地平移,nice
                            //如果是removeall,此条件成立说明c不含有该元素,则保留该元素
                            //如果是retainall,此条件成立说明c含有该元素,则保留该元素
                            elementData[w++] = elementData[r];
                            } finally {
                            // Preserve behavioral compatibility with AbstractCollection,
                            // even if c.contains() throws.
                            if (r != size) {
                            System.arraycopy(elementData, r,
                            elementData, w,
                            size - r);
                            w += size - r;
                            }
                            if (w != size) {
                            // clear to let GC do its work
                            for (int i = w; i < size; i++)
                            elementData[i] = null;
                            modCount += size - w;
                            size = w;
                            modified = true;
                            }
                            }
                            return modified;
                            }

                            //序列化
                            private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException{
                            // Write out element count, and any hidden stuff
                            int expectedModCount = modCount;
                            /*from ObjectOutputStream:
                            Write the non-static and non-transient fields of the current class
                            to this stream.*/
                            s.defaultWriteObject();

                            //为啥要特地强调write size?
                            s.writeInt(size);

                            for (int i=0; i<size; i++) {
                            s.writeObject(elementData[i]);
                            }

                            //保证一定基础的序列化同步
                            if (modCount != expectedModCount) {
                            throw new ConcurrentModificationException();
                            }
                            }

                            private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException {
                            elementData = EMPTY_ELEMENTDATA;
                            // Read in size, and any hidden stuff
                            s.defaultReadObject();
                            //不大懂为什么这里的值被ignore了?
                            // Read in capacity
                            s.readInt(); // ignored

                            if (size > 0) {
                            // be like clone(), allocate array based upon size not capacity
                            int capacity = calculateCapacity(elementData, size);
                            SharedSecrets.getJavaOISAccess().checkArray(s, Object[].class, capacity);
                            ensureCapacityInternal(size);

                            Object[] a = elementData;
                            // Read in all elements in the proper order.
                            for (int i=0; i<size; i++) {
                            a[i] = s.readObject();
                            }
                            }
                            }

                            public ListIterator<E> listIterator(int index) {
                            if (index < 0 || index > size)
                            throw new IndexOutOfBoundsException("Index: "+index);
                            return new ListItr(index);
                            }

                            public ListIterator<E> listIterator() {
                            return new ListItr(0);
                            }

                            public Iterator<E> iterator() {
                            return new Itr();
                            }

                            /**
                            * AbstractList.Itr的优化版本
                            */
                            private class Itr implements Iterator<E> {
                            int cursor; // index of next element to return
                            int lastRet = -1; // index of last element returned; -1 if no such
                            int expectedModCount = modCount;

                            Itr() {}

                            public boolean hasNext() {
                            return cursor != size;
                            }

                            @SuppressWarnings("unchecked")
                            public E next() {
                            checkForComodification();
                            int i = cursor;
                            if (i >= size)
                            throw new NoSuchElementException();
                            //内部类访问外部类this的方法
                            Object[] elementData = ArrayList.this.elementData;
                            if (i >= elementData.length)
                            throw new ConcurrentModificationException();
                            cursor = i + 1;
                            return (E) elementData[lastRet = i];
                            }

                            public void remove() {
                            if (lastRet < 0)
                            throw new IllegalStateException();
                            checkForComodification();

                            try {
                            //change here
                            //确实AbstractList的那个remove应该更适用于LinkedList
                            ArrayList.this.remove(lastRet);
                            cursor = lastRet;
                            lastRet = -1;
                            expectedModCount = modCount;
                            } catch (IndexOutOfBoundsException ex) {
                            throw new ConcurrentModificationException();
                            }
                            }

                            @Override
                            @SuppressWarnings("unchecked")
                            public void forEachRemaining(Consumer<? super E> consumer) {
                            Objects.requireNonNull(consumer);
                            final int size = ArrayList.this.size;
                            int i = cursor;
                            //这里不抛异常吗
                            if (i >= size) {
                            return;
                            }
                            final Object[] elementData = ArrayList.this.elementData;
                            //好像确实是并发修改了,毕竟上面已经test过i<size<capacity了
                            if (i >= elementData.length) {
                            throw new ConcurrentModificationException();
                            }
                            while (i != size && modCount == expectedModCount) {
                            consumer.accept((E) elementData[i++]);
                            }
                            // update once at end of iteration to reduce heap write traffic
                            //10
                            cursor = i;
                            lastRet = i - 1;
                            checkForComodification();
                            }

                            final void checkForComodification() {
                            if (modCount != expectedModCount)
                            throw new ConcurrentModificationException();
                            }
                            }

                            /**
                            * AbstractList.ListItr的优化版本
                            */
                            private class ListItr extends Itr implements ListIterator<E> {
                            ListItr(int index) {
                            super();
                            cursor = index;
                            }

                            public boolean hasPrevious() {
                            return cursor != 0;
                            }

                            public int nextIndex() {
                            return cursor;
                            }

                            public int previousIndex() {
                            return cursor - 1;
                            }

                            @SuppressWarnings("unchecked")
                            public E previous() {
                            checkForComodification();
                            int i = cursor - 1;
                            if (i < 0)
                            throw new NoSuchElementException();
                            Object[] elementData = ArrayList.this.elementData;
                            //11 防止并发修改,比如在这期间进行了trim
                            if (i >= elementData.length)
                            throw new ConcurrentModificationException();
                            cursor = i;
                            return (E) elementData[lastRet = i];
                            }

                            public void set(E e) {
                            if (lastRet < 0)
                            throw new IllegalStateException();
                            checkForComodification();

                            try {
                            ArrayList.this.set(lastRet, e);
                            } catch (IndexOutOfBoundsException ex) {
                            throw new ConcurrentModificationException();
                            }
                            }

                            public void add(E e) {
                            checkForComodification();

                            try {
                            int i = cursor;
                            ArrayList.this.add(i, e);
                            cursor = i + 1;
                            lastRet = -1;
                            expectedModCount = modCount;
                            } catch (IndexOutOfBoundsException ex) {
                            throw new ConcurrentModificationException();
                            }
                            }
                            }

                            public List<E> subList(int fromIndex, int toIndex) {
                            subListRangeCheck(fromIndex, toIndex, size);
                            return new SubList(this, 0, fromIndex, toIndex);
                            }

                            static void subListRangeCheck(int fromIndex, int toIndex, int size) {
                            if (fromIndex < 0)
                            throw new IndexOutOfBoundsException("fromIndex = " + fromIndex);
                            if (toIndex > size)
                            throw new IndexOutOfBoundsException("toIndex = " + toIndex);
                            if (fromIndex > toIndex)
                            throw new IllegalArgumentException("fromIndex(" + fromIndex +
                            ") > toIndex(" + toIndex + ")");
                            }

                            //依然内部类。不过这里应该用了个作用域的性质。相比于AbstractList定义在List类外部的
                            //Sublist,应该会更优先使用定义在内部的Sublist
                            //12 注意,这里的sublist没有直接extends ArrayList
                            private class SubList extends AbstractList<E> implements RandomAccess {
                            private final AbstractList<E> parent;
                            //新增成员
                            private final int parentOffset;
                            private final int offset;
                            int size;

                            SubList(AbstractList<E> parent,
                            int offset, int fromIndex, int toIndex) {
                            this.parent = parent;
                            //这个parentOffset应该是指相较于最近的父亲的偏移量,offset应该就是相较于最底层的父亲的偏移量
                            this.parentOffset = fromIndex;
                            this.offset = offset + fromIndex;
                            this.size = toIndex - fromIndex;
                            this.modCount = ArrayList.this.modCount;
                            }

                            public E set(int index, E e) {
                            rangeCheck(index);
                            checkForComodification();
                            E oldValue = ArrayList.this.elementData(offset + index);
                            ArrayList.this.elementData[offset + index] = e;
                            return oldValue;
                            }

                            public E get(int index) {
                            rangeCheck(index);
                            checkForComodification();
                            return ArrayList.this.elementData(offset + index);
                            }

                            public int size() {
                            checkForComodification();
                            return this.size;
                            }

                            public void add(int index, E e) {
                            rangeCheckForAdd(index);
                            checkForComodification();
                            //13
                            parent.add(parentOffset + index, e);
                            this.modCount = parent.modCount;
                            this.size++;
                            }

                            public E remove(int index) {
                            rangeCheck(index);
                            checkForComodification();
                            E result = parent.remove(parentOffset + index);
                            this.modCount = parent.modCount;
                            this.size--;
                            return result;
                            }

                            protected void removeRange(int fromIndex, int toIndex) {
                            checkForComodification();
                            parent.removeRange(parentOffset + fromIndex,
                            parentOffset + toIndex);
                            this.modCount = parent.modCount;
                            this.size -= toIndex - fromIndex;
                            }

                            public boolean addAll(Collection<? extends E> c) {
                            return addAll(this.size, c);
                            }

                            public boolean addAll(int index, Collection<? extends E> c) {
                            rangeCheckForAdd(index);
                            int cSize = c.size();
                            if (cSize==0)
                            return false;

                            checkForComodification();
                            parent.addAll(parentOffset + index, c);
                            this.modCount = parent.modCount;
                            this.size += cSize;
                            return true;
                            }

                            public Iterator<E> iterator() {
                            return listIterator();
                            }

                            public ListIterator<E> listIterator(final int index) {
                            checkForComodification();
                            rangeCheckForAdd(index);
                            final int offset = this.offset;

                            return new ListIterator<E>() {
                            int cursor = index;
                            int lastRet = -1;
                            int expectedModCount = ArrayList.this.modCount;

                            public boolean hasNext() {
                            //内部内部类还可以访问外部类和外部外部类
                            return cursor != SubList.this.size;
                            }

                            @SuppressWarnings("unchecked")
                            public E next() {
                            checkForComodification();
                            int i = cursor;
                            if (i >= SubList.this.size)
                            throw new NoSuchElementException();
                            Object[] elementData = ArrayList.this.elementData;
                            if (offset + i >= elementData.length)
                            throw new ConcurrentModificationException();
                            cursor = i + 1;
                            return (E) elementData[offset + (lastRet = i)];
                            }

                            public boolean hasPrevious() {
                            return cursor != 0;
                            }

                            @SuppressWarnings("unchecked")
                            public E previous() {
                            checkForComodification();
                            int i = cursor - 1;
                            if (i < 0)
                            throw new NoSuchElementException();
                            Object[] elementData = ArrayList.this.elementData;
                            if (offset + i >= elementData.length)
                            throw new ConcurrentModificationException();
                            cursor = i;
                            return (E) elementData[offset + (lastRet = i)];
                            }

                            @SuppressWarnings("unchecked")
                            public void forEachRemaining(Consumer<? super E> consumer) {
                            Objects.requireNonNull(consumer);
                            final int size = SubList.this.size;
                            int i = cursor;
                            if (i >= size) {
                            return;
                            }
                            final Object[] elementData = ArrayList.this.elementData;
                            if (offset + i >= elementData.length) {
                            throw new ConcurrentModificationException();
                            }
                            while (i != size && modCount == expectedModCount) {
                            consumer.accept((E) elementData[offset + (i++)]);
                            }
                            // update once at end of iteration to reduce heap write traffic
                            lastRet = cursor = i;
                            checkForComodification();
                            }

                            public int nextIndex() {
                            return cursor;
                            }

                            public int previousIndex() {
                            return cursor - 1;
                            }

                            public void remove() {
                            if (lastRet < 0)
                            throw new IllegalStateException();
                            checkForComodification();

                            try {
                            SubList.this.remove(lastRet);
                            cursor = lastRet;
                            lastRet = -1;
                            expectedModCount = ArrayList.this.modCount;
                            } catch (IndexOutOfBoundsException ex) {
                            //确实要是IndexOutOfBounds的话,就说明lastRet改了,说明Concurrent了
                            throw new ConcurrentModificationException();
                            }
                            }

                            public void set(E e) {
                            if (lastRet < 0)
                            throw new IllegalStateException();
                            checkForComodification();

                            try {
                            ArrayList.this.set(offset + lastRet, e);
                            } catch (IndexOutOfBoundsException ex) {
                            throw new ConcurrentModificationException();
                            }
                            }

                            public void add(E e) {
                            checkForComodification();

                            try {
                            int i = cursor;
                            SubList.this.add(i, e);
                            cursor = i + 1;
                            lastRet = -1;
                            expectedModCount = ArrayList.this.modCount;
                            } catch (IndexOutOfBoundsException ex) {
                            throw new ConcurrentModificationException();
                            }
                            }

                            final void checkForComodification() {
                            if (expectedModCount != ArrayList.this.modCount)
                            throw new ConcurrentModificationException();
                            }
                            };
                            }

                            public List<E> subList(int fromIndex, int toIndex) {
                            subListRangeCheck(fromIndex, toIndex, size);
                            return new SubList(this, offset, fromIndex, toIndex);
                            }

                            public Spliterator<E> spliterator() {
                            checkForComodification();
                            return new ArrayListSpliterator<E>(ArrayList.this, offset,
                            offset + this.size, this.modCount);
                            }
                            }

                            @Override
                            public void forEach(Consumer<? super E> action) {
                            Objects.requireNonNull(action);
                            final int expectedModCount = modCount;
                            @SuppressWarnings("unchecked")
                            final E[] elementData = (E[]) this.elementData;
                            final int size = this.size;
                            //一修改就会寄
                            for (int i=0; modCount == expectedModCount && i < size; i++) {
                            action.accept(elementData[i]);
                            }
                            if (modCount != expectedModCount) {
                            throw new ConcurrentModificationException();
                            }
                            }

                            @Override
                            public Spliterator<E> spliterator() {
                            return new ArrayListSpliterator<>(this, 0, -1, 0);
                            }

                            /** Index-based split-by-two, lazily initialized Spliterator */
                            //基于索引的二分法,懒加载的 Spliterator
                            static final class ArrayListSpliterator<E> implements Spliterator<E> {...}

                            @Override
                            public boolean removeIf(Predicate<? super E> filter) {
                            Objects.requireNonNull(filter);

                            int removeCount = 0;
                            //666,用了类似掩码的思想,这样就能避免多次移动数组了,实现O(n),很不错
                            final BitSet removeSet = new BitSet(size);
                            final int expectedModCount = modCount;
                            final int size = this.size;
                            for (int i=0; modCount == expectedModCount && i < size; i++) {
                            @SuppressWarnings("unchecked")
                            final E element = (E) elementData[i];
                            if (filter.test(element)) {
                            //set:Sets the bit at the specified index to true.
                            removeSet.set(i);
                            removeCount++;
                            }
                            }
                            if (modCount != expectedModCount) {
                            throw new ConcurrentModificationException();
                            }

                            // shift surviving elements left over the spaces left by removed elements
                            final boolean anyToRemove = removeCount > 0;
                            if (anyToRemove) {
                            final int newSize = size - removeCount;
                            for (int i=0, j=0; (i < size) && (j < newSize); i++, j++) {
                            //nextClearBit:Returns the index of the first bit that is set to false
                            //false表示不移走,true表示移走
                            i = removeSet.nextClearBit(i);
                            elementData[j] = elementData[i];
                            }
                            for (int k=newSize; k < size; k++) {
                            elementData[k] = null; // Let gc do its work
                            }
                            this.size = newSize;
                            if (modCount != expectedModCount) {
                            throw new ConcurrentModificationException();
                            }
                            modCount++;
                            }

                            return anyToRemove;
                            }

                            @Override
                            @SuppressWarnings("unchecked")
                            public void replaceAll(UnaryOperator<E> operator) {
                            Objects.requireNonNull(operator);
                            final int expectedModCount = modCount;
                            final int size = this.size;
                            for (int i=0; modCount == expectedModCount && i < size; i++) {
                            elementData[i] = operator.apply((E) elementData[i]);
                            }
                            if (modCount != expectedModCount) {
                            throw new ConcurrentModificationException();
                            }
                            modCount++;
                            }

                            @Override
                            @SuppressWarnings("unchecked")
                            public void sort(Comparator<? super E> c) {
                            final int expectedModCount = modCount;
                            Arrays.sort((E[]) elementData, 0, size, c);
                            if (modCount != expectedModCount) {
                            throw new ConcurrentModificationException();
                            }
                            modCount++;
                            }
                            }
                            +

                            SortedMap(I)

                            +

                            A Map that further provides a total ordering on its keys.

                            +

                            The map is ordered according to the natural ordering of its keys, or by a Comparator typically provided at sorted map creation time.

                            +

                            All keys inserted into a sorted map must implement the Comparable interface (or be accepted by the specified comparator).

                            + -

                            其中:

                              -
                            1. Cloneable接口

                              与RandomAccess一样,都是一个规定性质的接口。

                              -
                              -

                              A class implements the Cloneable interface to indicate to the Object.clone() method that it is legal for that method to make a field-for-field copy of instances of that class.

                              -

                              Classes that implement this interface should override Object.clone (which is protected) with a public method.

                              +

                              关于这部分,详细见sorted set

                              -
                            2. -
                            3. clone与浅拷贝(shallow copy)
                                  public static void main(String[] args) {
                              ArrayList<Student> arr = new ArrayList<>();
                              arr.add(new Student("lylt",15));
                              ArrayList<Student> cl = (ArrayList)arr.clone();
                              System.out.println(cl.get(0).toString());
                              cl.get(0).name="Sam";
                              System.out.println(cl.get(0).toString());
                              System.out.println(arr.get(0).toString());
                              cl.set(0,new Student("HWX",19));
                              System.out.println(cl.get(0).toString());
                              System.out.println(arr.get(0).toString());
                              }
                              /*运行结果:
                              name :lyltage :15
                              name :Samage :15
                              name :Samage :15
                              name :HWXage :19
                              name :Samage :15
                              */
                              - -

                              结合内部代码可知,确实跟上面那个toArray的原理是一样的,只拷贝地址。

                              -

                              clone是浅拷贝。

                              -

                              浅拷贝与深拷贝的区别

                              -
                            4. -
                            5. 关于子类继承到的父类内部类

                              本来在犹豫,子类默认继承到的内部类里面用到的外部类方法的版本是取父还是取子,经过以下实验可知,是取能访问到的最新版本。

                              -
                              public class Main {
                              public static void main(String[] args) {
                              ChildOuter chldot = new ChildOuter();
                              chldot.printname();
                              chldot.in.printall();
                              }
                              }
                              /*结果:子类声明为private的成员字段不能被从父类继承而来的方法访问到
                              Father
                              I am father!*/
                              class FatherOuter{
                              String name="Father";
                              private void print(){
                              System.out.println("I am father!");
                              }
                              public Inner in = new Inner();
                              public class Inner{
                              public int haha=1;
                              void printall(){
                              print();
                              }
                              }
                              public void printname(){
                              System.out.println(name);
                              }
                              }

                              class ChildOuter extends FatherOuter{
                              private String name="Child";
                              private void print(){System.out.println("I am child!");}
                              }
                              +

                              最大的特点就是可以人为定义有序并且有sub map

                              +

                              代码:

                              public interface SortedMap<K,V> extends Map<K,V> {

                              Comparator<? super K> comparator();

                              SortedMap<K,V> subMap(K fromKey, K toKey);

                              SortedMap<K,V> headMap(K toKey);

                              SortedMap<K,V> tailMap(K fromKey);

                              //也是默认第一个是低的最后一个是高的,就跟LHM的第一个是最少使用,最后一个是最近使用一样
                              //Returns the first (lowest) key currently in this map.
                              K firstKey();

                              //Returns the last (highest) key currently in this map.
                              K lastKey();

                              Set<K> keySet();

                              Collection<V> values();

                              Set<Map.Entry<K, V>> entrySet();
                              }
                              -

                              如若把Father和Child内的print类都换成public:

                              -
                              class FatherOuter{
                              //...
                              public void print(){
                              System.out.println("I am father!");
                              }
                              //...
                              }

                              class ChildOuter extends FatherOuter{
                              //...
                              public void print(){System.out.println("I am child!");}
                              }
                              /*结果:访问最新版本
                              I am child!*/
                              +
                              +

                              A SortedMap extended with navigation methods returning the closest matches for given search targets.

                              +

                              The performance of ascending operations and views is likely to be faster than that of descending ones.

                              +

                              submap都多加了几个参数:inclusive or exclusive

                              +

                              其entry不支持setValue,只能通过map自身的put方法改变value。因为要求前者只是map的快照

                              +
                              +

                              跟navigable set差不多的定义

                              +

                              代码:

                              public interface NavigableMap<K,V> extends SortedMap<K,V> {

                              Map.Entry<K,V> lowerEntry(K key);

                              K lowerKey(K key);

                              Map.Entry<K,V> floorEntry(K key);

                              K floorKey(K key);

                              Map.Entry<K,V> ceilingEntry(K key);

                              K ceilingKey(K key);

                              Map.Entry<K,V> higherEntry(K key);

                              K higherKey(K key);

                              Map.Entry<K,V> firstEntry();

                              Map.Entry<K,V> lastEntry();

                              Map.Entry<K,V> pollFirstEntry();

                              Map.Entry<K,V> pollLastEntry();

                              NavigableMap<K,V> descendingMap();

                              NavigableSet<K> navigableKeySet();

                              NavigableSet<K> descendingKeySet();

                              NavigableMap<K,V> subMap(K fromKey, boolean fromInclusive,
                              K toKey, boolean toInclusive);

                              NavigableMap<K,V> headMap(K toKey, boolean inclusive);

                              NavigableMap<K,V> tailMap(K fromKey, boolean inclusive);

                              SortedMap<K,V> subMap(K fromKey, K toKey);

                              SortedMap<K,V> headMap(K toKey);

                              SortedMap<K,V> tailMap(K fromKey);
                              }
                              -
                              //结论:内部类可以访问外部类所有不论私有还是公有的资源;会优先访问最新版本【父子类而言】
                              //子类声明为private的成员字段不能被从父类继承而来的方法访问到,只会访问能访问到的最新版本
                            6. -
                            7. 序列化versionID

                              ArrayList实现了java.io.Serializable接口,故而可以被序列化和反序列化,就需要有个序列化版本ID

                              -

                              What is a serialVersionUID and why should I use it?

                              -
                              -

                              这东西是用来在反序列化的时候保证收到的对象和发送的对象的类是相同的。If the receiver has loaded a class for the object that has a different serialVersionUID than that of the corresponding sender’s class, then deserialization will result in an InvalidClassException.

                              -

                              The field serialVersionUID must be static, final, and of type long.

                              -

                              If a serializable class does not explicitly declare a serialVersionUID, then the serialization runtime will calculate a default serialVersionUID value for that class based on various aspects of the class, as described in the Java(TM) Object Serialization Specification.

                              +

                              TreeMap

                              +

                              NavigableMap的红黑树实现

                              +

                              key不允许空,空会抛出异常

                              +

                              Note that this implementation is not synchronized.

                              +

                              fail-fast

                              +

                              All Map.Entry pairs returned by methods in this class and its views represent snapshots of mappings at the time they were produced. They do not support the Entry.setValue method. (Note however that it is possible to change mappings in the associated map using put.)【navigable map的性质】

                              -
                            8. -
                            9. transient

                              Java中transient关键字的详细总结

                              -
                              -

                              transient是短暂的意思。对于transient 修饰的成员变量,在类的实例对象的序列化处理过程中会被忽略。 因此,transient变量不会贯穿对象的序列化和反序列化,生命周期仅存于调用者的内存中而不会写到磁盘里进行持久化。

                              -

                              在持久化对象时,对于一些特殊的数据成员(如用户的密码,银行卡号等),我们不想用序列化机制来保存它。为了在一个特定对象的一个成员变量上关闭序列化,可以在这个成员变量前加上关键字transient。

                              -

                              注意static修饰的静态变量天然就是不可序列化的。一个静态变量不管是否被transient修饰,均不能被序列化(如果反序列化后类中static变量还有值,则值为当前JVM中对应static变量的值)。序列化保存的是对象状态,静态变量保存的是类状态,因此序列化并不保存静态变量。

                              -

                              使用场景
                              (1)类中的字段值可以根据其它字段推导出来,如一个长方形类有三个属性长度、宽度、面积,面积不需要序列化。
                              (2) 一些安全性的信息,一般情况下是不能离开JVM的。
                              (3)如果类中使用了Logger实例,那么Logger实例也是不需要序列化的

                              +

                              具体代码就不看了

                              +

                              对Collection和Map的总结

                                +
                              1. fail-fast

                                +

                                The iterators returned by all of this class’s “collection view methods” are fail-fast: if the map is structurally modified at any time after the iterator is created, in any way except through the iterator’s own remove method, the iterator will throw a ConcurrentModificationException. Thus, in the face of concurrent modification, the iterator fails quickly and cleanly, rather than risking arbitrary, non-deterministic behavior at an undetermined time in the future.

                                +

                                Note that the fail-fast behavior of an iterator cannot be guaranteed as it is, generally speaking, impossible to make any hard guarantees in the presence of unsynchronized concurrent modification. Fail-fast iterators throw ConcurrentModificationException on a best-effort basis. Therefore, it would be wrong to write a program that depended on this exception for its correctness: the fail-fast behavior of iterators should be used only to detect bugs.

                                -

                                但其实还有一个问题,它这边源码对这个transient的注释是:non-private to simplify nested class access,“非私有以简化嵌套类访问”。问题是这个transient和类的公有还是私有有什么关系呢?

                                -

                                Why is the data array in java.util.ArrayList package-private?

                                -

                                其实没怎么看懂23333

                                +

                                都使用了modcount进行并发检查,都具有fail-fast的特点(关于此的详细解说,可见AbstractList第四点和List第二点),因而只允许在迭代中使用迭代器的remove方法进行结构性改变。【注意:对于LinkedHashMap中access order排序,get方法也是structural modification,因而也只能通过迭代器的next方法获取元素】

                              2. -
                              3. ArrayList的elementData虽然被transient修饰,但仍然能够序列化

                                ArrayList中elementData为何被transient修饰?

                                +
                              4. not synchronized

                                上面介绍到的几个类,除了Vector外,都是线程不同步的。可以用此方式让其线程同步。

                                +
                                Map m = Collections.synchronizedMap(new LinkedHashMap(...));
                              5. +
                              6. 是否允许null

                                除了TreeSet、TreeMap、ArrayDeque之外,都是允许空(key/value)的

                              7. -
                              8. 关于static的空数组

                                EMPTY_ELEMENTDATA和DEFAULTCAPACITY_EMPTY_ELEMENTDATA这两个空数组可用于表示两种情况:

                                -

                                new ArrayList(0) ->EMPTY_ELEMENTDATA

                                -

                                new ArrayList() ->DEFAULTCAPACITY_EMPTY_ELEMENTDATA

                                -

                                之所以用静态,是提取了共性:不论是需要什么ArrayList,其空形态不都一样吗(

                                -

                                这样可以避免了制造对象的浪费。very good。

                                +
                              9. 是否有序

                                List都是插入序,HashSet无需,HashMap也无序(但其实算是有内部桶序的),LinkedHashMap有插入序和LRU序(依靠内部增加简单队列的消耗),TreeSet有序,TreeMap有序【这俩靠红黑树的遍历顺序(二叉搜索树嘛)】。

                              10. -
                              11. 关于扩容的连环计

                                我其实觉得不必写那么麻烦……

                                -

                                In the JAVA ArrayList source code, why does array expansion should be divided into ensureCapacityInternal and ensureCapacity two sides?

                                -

                                经过测试替换可得,确实可以像我那样写。

                                -
                                		//此处的ArrayList是魔改过的
                                //now the capacity is 0
                                ArrayList a = new ArrayList<>();
                                System.out.println(a.getCapacity());
                                //扩容到DEFAULT
                                a.add(new Student("Lily",15));
                                System.out.println(a.getCapacity());
                                addStudent(a);
                                System.out.println(a.getCapacity());
                                /*运行结果:
                                0
                                10
                                33
                                */
                              12. -
                              13. 关于clear我的写法

                                In the Java ArrayList source code, in the clear function, can my rewrite more efficient than the origin source code?

                                -
                                ArrayList a = new ArrayList<>();
                                addStudent(a);
                                long sum1=0;
                                for (int i=0;i<50000;i++){
                                long startTime = System.currentTimeMillis();
                                a.clear1();
                                addStudent(a);
                                long endTime = System.currentTimeMillis();
                                sum1+=endTime-startTime;
                                }
                                //sum1/=500;
                                System.out.println("clear1: "+sum1);
                                sum1=0;
                                for (int i=0;i<50000;i++){
                                long startTime = System.currentTimeMillis();
                                a.clear2();
                                addStudent(a);
                                long endTime = System.currentTimeMillis();
                                sum1+=endTime-startTime;
                                }
                                //sum1/=500;
                                System.out.println("clear2: "+sum1);
                                +
                              14. 实现的约定接口

                                都Cloneable,Serializable

                                +

                                ArrayList/Vector:RandomAccess

                                +
                              15. +
                              +]]> + + Java + + + + 链接、装载与运行库 + /2023/09/18/%E9%9D%99%E6%80%81%E9%93%BE%E6%8E%A5%E4%B8%8E%E5%8A%A8%E6%80%81%E9%93%BE%E6%8E%A5/ + +

                              此为《程序员的自我修养:链接、装载与库》(俞甲子,石凡,潘爱民)的看书总结。

                              +
                              +

                              链接前与装载

                              链接前的编译阶段可以生成.o文件,.o文件是ELF文件,里面含有段表、符号表、bss段、common段等链接辅助段。

                              +

                              执行可执行文件时,首先要通过fork创建一个新的子进程,然后要通过exec为子进程制定可执行文件装载逻辑。在exec系统调用中,会进行elf文件的读取解析。它会解析elf header,根据其各种信息将程序copy到内存(进程的虚拟地址空间)中,后者也就是我们所研究的装载。

                              +

                              装载不同于“节(section),是以”“(segment)为单位。进程虚拟地址空间被分为很多个VMA,每个VMA都有不同的属性(如权限,可读可写可执行)。ELF可执行文件除去在链接/编译过程中被视为一个个连续的节之外,它还会在链接的时候根据每个节的属性不同重排节,把属性相同的节连续放在一起成为一个个段。链接的时候还会形成程序头表。对应关系:节——段表,段——程序头表

                              +

                              装载进内存时,是以段为单位,一个段就对应着一个VMA。

                              +

                              内核通过execve系统调用装载完ELF可执行文件以后就返回到用户空间,将控制权交给程序的入口。

                              +

                              对于不同链接形式的ELF可执行文件,这个程序的入口是有区别的。对于静态链接的可执行文件来说,程序的入口就是ELF文件头里面的e_entry指定的入口地址;对于动态链接的可执行文件来说,如果这时候把控制权交给e_entry指定的入口地址,那么肯定是不行的,因为可执行文件所依赖的共享库还没有被装载,也没有进行动态链接。所以对于动态链接的可执行文件,内核会分析它的动态链接器地址(在“.interp”段),将动态链接器映射至进程地址空间,然后把控制权交给动态链接器

                              +

                              ELF

                              编译器编译源代码后生成的文件叫做目标文件(Object文件),目标文件从结构上讲,它是已经编译后的可执行文件格式,只是还没有经过链接的过程,其中可能有些符号或有些地址还没有被调整。其实它本身就是按照可执行文件格式存储的,只是跟真正的可执行文件在结构上稍有不同。

                              +

                              Linux的可执行文件遵从ELF的结构模式。

                              +

                              ELF可以分为这几类:

                              +

                              image-20230913084511590

                              +

                              也即.o(可重定位文件)、.exe(无后缀)(可执行文件)、.so(动态链接库)、.a(静态链接库)、core dump。

                              +

                              Object文件的结构

                              目标文件中的内容至少有编译后的机器指令代码、数据。没错,除了这些内容以外,目标文件中还包括了链接时所须要的一些信息,比如符号表、调试信息、字符串等。一般目标文件将这些信息按不同的属性,以“节”(Section)的形式存储,有时候也叫“”(Segment)。

                              +

                              image-20230918100932220

                              +

                              符号修饰

                              众所周知,强大而又复杂的C++拥有类、继承、虚机制、重载、名称空间等这些特性,它们使得符号管理更为复杂。最简单的例子,两个相同名字的函数func(int)和func(double),尽管函数名相同,但是参数列表不同,这是C++里面函数重载的最简单的一种情况,那么编译器和链接器在链接过程中如何区分这两个函数呢?为了支持C++这些复杂的特性,⼈们发明了符号修饰(Name Decoration)或符号改编(Name Mangling)的机制。

                              +

                              image-20230913090407013

                              +

                              C++为了与C兼容,在符号的管理上,C++有一个用来声明或定义一个C的符号的“extern “C””关键字用法:

                              +
                              extern ”C” {
                              int func(int);
                              int var;
                              }
                              -

                              经测试发现不分伯仲()也确实差距应该很小2333

                              -

                              不过依照一个回答:

                              +

                              C++编译器会将在extern “C” 的大括号内部的代码当作C语言代码处理。所以很明显,上面的代码中,C++的名称修饰机制将不会起作用。

                              +

                              强符号和弱符号

                              多个目标文件中含有相同名字全局符号的定义,那么这些目标文件链接的时候将会出现符号重复定义的错误。这种符号的定义可以被称为强符号(Strong Symbol)。有些符号的定义可以被称为弱符号(Weak Symbol)。对于C/C++语言来说,编译器默认函数和初始化了的全局变量为强符号,未初始化的全局变量为弱符号。我们也可以通过GCC的__attribute__((weak))来定义任何一个强符号为弱符号。

                              +

                              image-20230913090719480

                              +

                              目前我们所看到的对外部目标文件的符号引用在目标文件被最终链接成可执行文件时,它们须要被正确决议,如果没有找到该符号的定义,链接器就会报符号未定义错误,这种被称为强引用(Strong Reference)。与之相对应还有一种弱引用(Weak Reference),在处理弱引用时,如果该符号有定义,则链接器将该符号的引用决议;如果该符号未被定义,则链接器对于该引用不报错。

                              +

                              链接器处理强引用和弱引用的过程几乎一样,只是对于未定义的弱引用,链接器不认为它是一个错误。一般对于未定义的弱引用,链接器默认其为0,或者是一个特殊的值,以便于程序代码能够识别。

                              +
                              __attribute__ ((weakref)) void foo();

                              int main()
                              {
                              if(foo) foo();
                              }
                              + +

                              这种弱符号和弱引用对于库来说十分有用,比如库中定义的弱符号可以被用户定义的强符号所覆盖,从而使得程序可以使用自定义版本的库函数。

                              -

                              Your approach explicitly throws the backing array away. The existing implementation attempts to reuse it. So even if your approach is faster in isolation, in practice it will almost certainly be less performant. Since calling clear() is a sign you intend to reuse the ArrayList.

                              +

                              这里很帅,有条件编译那味了

                              -

                              其实感觉我的clear说不定花销更大,毕竟要创建一个新对象(

                              -
                            10. -
                            11. heap write traffic

                              What is heap write traffic and why it is required in ArrayList?

                              -

                              详见第二个答案。

                              - - -

                              栈放在一级缓存,堆放在二级缓存

                              -

                              总之意思就是成员变量写在堆里,局部变量写在栈里,做

                              -
                              while (i != size && modCount == expectedModCount) {
                              consumer.accept((E) elementData[i++]);
                              // Update cursor while iterating
                              cursor = i;
                              }
                              +

                              在Linux程序的设计中,如果一个程序被设计成可以支持单线程或多线程的模式,就可以通过弱引用的方法来判断当前的程序是链接到了单线程的Glibc库还是多线程的Glibc库(是否在编译时有-lpthread选项),从而执行单线程版本的程序或多线程版本的程序。我们可以在程序中定义一个pthread_create函数的弱引用,然后程序在运行时动态判断是否链接到pthread库从而决定执行多线程版本还是单线程版本:

                              +
                              #include <stdio.h>
                              #include <pthread.h>

                              int pthread_create(pthread_t*, const pthread_attr_t*, void* (*)(void*),
                              void*) __attribute__ ((weak));

                              int main()
                              {
                              if(pthread_create) {
                              printf("This is multi-thread
                              version!\n");
                              // run the multi-thread version
                              // main_multi_thread()
                              } else {
                              printf("This is single-thread
                              version!\n");
                              // run the single-thread version
                              // main_single_thread()
                              }
                              }
                              -

                              比做

                              -
                              while (cursor != size && modCount == expectedModCount) {
                              consumer.accept((E) elementData[cursor++]);
                              }
                              +
                              +

                              弱符号大概是说该变量可以被定义多次,最终链接时再进行决议;弱引用大概是说该变量(函数)可以不被定义。

                              +
                              +

                              静态链接

                              静态链接库(.a文件)本质上是一堆.o文件的集合。静态链接的基本过程:

                              +
                              test.c ——(compile)——>test.o——(link)——>test(ELF exe)

                              lib.a
                              -

                              花销更小

                              +

                              静态链接其实也就是分为两大步骤:

                              +
                                +
                              1. 空间与地址分配

                                +

                                将input file的各个段都连在一起,并且为符号分配虚拟地址

                              2. -
                              3. if (i >= elementData.length)

                                is i >= elementData.length in ArrayList::iterator redundant?

                                -
                                public E previous() {
                                checkForComodification();
                                int i = cursor - 1;
                                if (i < 0)
                                throw new NoSuchElementException();
                                Object[] elementData = ArrayList.this.elementData;
                                if (i >= elementData.length)
                                throw new ConcurrentModificationException();
                                cursor = i;
                                return (E) elementData[lastRet = i];
                                }
                                - -

                                如果 user invoke trimToSize method ,就会导致在checkForComodification();if (i >= elementData.length)之间发生ArrayIndexOutOfBounds。而在if (i >= elementData.length)之后trim没有影响,因为我们的局部变量已经保存了原来的elementData,此时再trim只是修改成员变量的elementData,局部变量依然不变。

                                +
                              4. 符号解析与重定位

                                +
                                  +
                                1. 扫描所有输入文件的符号表形成全局符号表;

                                2. -
                                3. sublist不可序列化,且not cloneable
                                  private class SubList extends AbstractList<E> implements RandomAccess 
                                  - -

                                  sublist没有extends Cloneable, java.io.Serializable这两个接口

                                  +
                                4. 重定向

                                  +

                                  可执行文件基本可以确定自己在进程虚拟空间中的起始位置,因为可执行文件往往是第一个被加载的文件,它可以选择一个固定空闲的地址。因而,在link中,可执行文件的地址都以确定,就可以开始进行重定向。

                                  +

                                  通过重定向表对所有UNDEF的符号进行地址修正,包括相对地址修正和绝对地址修正。

                                5. -
                                6. parent和ArrayList.this

                                  首先,这两个是同一个吗?其次,这俩是否是同一个跟sub的级数有关系吗,就比如一级sub都是同一个,多级sub就不是同一个了?

                                  -

                                  经过对ArrayList的一些public和以下代码的测试,得出结论:这两个只有在第一级sub的时候是同一个。parent指向直系父亲,ArrayList.this指向root父亲。

                                  -
                                  //In ArrayList:
                                  public final AbstractList<E> parent;
                                  public ArrayList getRoot(){return ArrayList.this;}
                                  //In test main:
                                  public static void main(String[] args) {
                                  ArrayList a=new ArrayList();
                                  addStudent(a);
                                  System.out.println(a.hashCode());

                                  ArrayList.SubList suba= (ArrayList.SubList) a.subList(1,7);
                                  System.out.println(suba.getRoot().hashCode()+"\t"+suba.parent.hashCode());
                                  System.out.println(suba.hashCode());

                                  ArrayList.SubList subsuba=(ArrayList.SubList) suba.subList(2,4);
                                  System.out.println(subsuba.getRoot().hashCode()+"\t"+subsuba.parent.hashCode());
                                  System.out.println(subsuba.hashCode());

                                  ArrayList.SubList subsubsuba=(ArrayList.SubList) subsuba.subList(0,1);
                                  System.out.println(subsubsuba.getRoot().hashCode()+"\t"+subsubsuba.parent.hashCode());
                                  }

                                  /*输出结果:
                                  779301330
                                  779301330 779301330
                                  -954172011
                                  779301330 -954172011
                                  -95519366
                                  779301330 -95519366
                                  */
                                  - -

                                  总之,ArrayList的sublist实现方式相当于串成了一条父子继承串,多级sub,至于这么干相比原来的只有两级父子关系的方法好在哪就不知道了

                                  +
                              -

                              AbstractSequentialList(A)

                              -

                              提供顺序访问list的基本骨架

                              -

                              To implement a list the programmer needs only to extend this class and provide implementations for the listIterator and size methods. For an unmodifiable list, the programmer need only implement the list iterator’s hasNext, next, hasPrevious, previous and index methods.
                              For a modifiable list the programmer should additionally implement the list iterator’s set method. For a variable-size list the programmer should additionally implement the list iterator’s remove and add methods.

                              +

                              在link中,会读取test.o以及lib.a中的符号表,完成重定向(绝对地址和相对地址)以及节的重排组织,最终组合形成以段为单位的可执行文件test

                              +

                              可执行文件test会通过系统调用exevec被装载进物理内存(lazy allocation),分段映射到进程的虚拟地址空间。

                              +

                              静态链接的缺陷是,由于重定向在link过程完成,故而同一份共享库在物理内存中会有多份copy,极大占用物理内存和磁盘空间。优点是速度快。

                              +

                              动态链接

                              (下文注意区分两个概念:可执行文件和动态链接库)

                              +

                              动态链接库(.so)不同于静态链接库。

                              +
                              test.c ——(compile)——>test.o——(link)——>test(ELF exe)

                              lib.so
                              + +

                              在link中,仅会读入动态链接库的符号表,对于动态链接库的符号仅会将其标记为动态符号,而不会对其进行重定向。

                              +

                              可执行文件test会通过系统调用exevec被装载进物理内存(lazy allocation),分段映射到进程的虚拟地址空间。

                              +

                              静态链接是per-process一份库,内存中有多份库;动态链接是per-process一份库,内存也只有一份库。并且虚拟地址动态分配,也即库映射到进程地址空间的哪块VMA是不确定的。

                              +

                              由于动态链接库被装载时的虚拟地址不确定,所以对于动态链接库和可执行文件代码中与动态链接相关的绝对地址,不能简单采用装载时重定向的方法来对其重定向,否则会破坏其共享性和不变性。

                              +
                              +

                              试想一下,每个进程加载的动态链接库的地址都不同,那岂不是每个进程的动态链接库的重定向结果都不一样,指令都不一样,不就寄了。

                              -

                              代码:

                              public abstract class AbstractSequentialList<E> extends AbstractList<E> {

                              protected AbstractSequentialList() {
                              }

                              public E get(int index) {
                              try {
                              //1
                              return listIterator(index).next();
                              } catch (NoSuchElementException exc) {
                              throw new IndexOutOfBoundsException("Index: "+index);
                              }
                              }

                              public E set(int index, E element) {
                              try {
                              ListIterator<E> e = listIterator(index);
                              E oldVal = e.next();
                              e.set(element);
                              return oldVal;
                              } catch (NoSuchElementException exc) {
                              throw new IndexOutOfBoundsException("Index: "+index);
                              }
                              }

                              public void add(int index, E element) {
                              try {
                              listIterator(index).add(element);
                              } catch (NoSuchElementException exc) {
                              throw new IndexOutOfBoundsException("Index: "+index);
                              }
                              }

                              public E remove(int index) {
                              try {
                              ListIterator<E> e = listIterator(index);
                              E outCast = e.next();
                              e.remove();
                              return outCast;
                              } catch (NoSuchElementException exc) {
                              throw new IndexOutOfBoundsException("Index: "+index);
                              }
                              }
                              // Bulk Operations

                              public boolean addAll(int index, Collection<? extends E> c) {
                              try {
                              boolean modified = false;
                              ListIterator<E> e1 = listIterator(index);
                              Iterator<? extends E> e2 = c.iterator();
                              while (e2.hasNext()) {
                              e1.add(e2.next());
                              modified = true;
                              }
                              return modified;
                              } catch (NoSuchElementException exc) {
                              throw new IndexOutOfBoundsException("Index: "+index);
                              }
                              }
                              // Iterators

                              public Iterator<E> iterator() {
                              return listIterator();
                              }

                              public abstract ListIterator<E> listIterator(int index);
                              }
                              +

                              所以我们此时进行了一个牛逼到家、惊天动地、无人能比的操作。

                              +

                              我们可以分离.text和.data,前者作为“共享”语义保持不变性,后者则在每个进程地址空间中都留存一个copy。然后,我们将所有立即数绝对寻址的地方,换为间接寻址!也即,把那个立即数绝对地址改成一个变量,变量值在.data段中存储。这样一来,就成功把绝对寻址替换成了相对寻址。加载的时候也只需将虚拟地址填进可变的.data就行。不得不说真是十分地巧妙。

                              +

                              这个从相对地址——绝对地址的转换过程,由ELF中的一个新段GOT表(.got)来实现。在link时加入了动态链接库符号表的可执行文件,以及动态链接库本身,都使用了.got段,以PIC形式出现。

                              +

                              这个操作就是所谓的“地址无关代码”,通过-fPIC选项,就可以将代码编译为一个地址无关的程序。使用PIC模式编译的共享对象,对于模块内部的函数调用也是采用跟模块外部函数调用一样的方式,即使用GOT/PLT的方式。

                              +
                              +

                              小trick:如何区分一个DSO是否为PIC

                              +
                              readelf -d foo.so | grep TEXTREL
                              -

                              其中:

                                -
                              1. get和set方法通过Iterator实现

                                随机访问的AbstractList的iterator的方法借助了主类的get和set,跟这里正好反过来。但注意哈,下面的LinkedList实现把以上差不多所有的方法都重写了,因而get和set之类的方法,LinkedList并不是依靠迭代器的。

                                -
                              2. -
                              -

                              LinkedList

                              -

                              双向链表,实现List和Deque

                              -

                              并发不安全。List list = Collections.synchronizedList(new LinkedList(…));

                              -

                              印象:漂亮的指针操作,以及好像很少抛出异常,还有很多很多繁琐的方法(

                              +

                              如果上面的命令有任何输出,那么foo.so就不是PIC的,否则就是PIC的。PIC的DSO是不会包含任何代码段重定位表的,TEXTREL表⽰代码段重定位表地址。

                              +

                              这也很好理解,因为PIC本质上就是把代码段重定位转化为了数据段重定位

                              +
                              +

                              除了动态链接库中的寻址(对变量和函数)需要使用PIC之外,对可执行文件的全局变量也需要使用特殊的机制。ELF共享库中的全局变量都类似以弱引用形式存在。当全局变量在主程序extern时,若该变量在共享库中初始化了,那么加载之后要把共享库的数据copy进主程序;否则,该变量值都以主模块为准。

                              +
                              +

                              这段原因解释看书真没懂,详情340页开始。

                              +

                              不过感觉它可能说的有点问题,我个人认为全局变量需要使用这种以方式存在,是为了保证进程资源独立。如果变量都以共享库中的数据值为准,那各个进程共享共享库不就乱了。。。你改一下我改一下

                              +
                              +

                              因而,总的装载流程是:

                              +

                              未优化情况下,在可执行文件被装载之前,先将其依赖的所有动态链接库加载进内存。若其所需的动态链接库已经被映射到物理内存,则将其装载到进程虚拟地址空间;否则,则映射到物理内存,并且装载到进程虚拟地址空间。然后,在装载动态链接库后,扫描可执行文件.got段符号进行装载时重定向(依据已经装载了的动态链接库虚拟地址来计算符号地址)即可。

                              +

                              但可以注意到这一步还是有优化空间。所以我们采取延迟绑定(PLT)的方法,第一次访问到动态链接库符号时,才对其进行重定向并填入.got中。

                              +

                              动态链接的缺点就是太慢了,一是因为PIC导致模块内部函数和全局变量也需要以.got形式访问,加了层寻址;二是运行时重定向开销巨大。对于前者,模块内部函数可以使用static关键字修饰;对于后者,采用PLT。

                              +

                              image-20230913231031432

                              +

                              显式运行时链接

                              支持动态链接的系统往往都支持一种更加灵活的模块加载方式,叫做显式运行时链接(Explicit Runtime Linking),有时候也叫做运行时加载。也就是让程序自己在运行时控制加载指定的模块,并且可以在不需要该模块时将其卸载。

                              +
                              +

                              也就是说,前面介绍的动态链接库是由动态链接器自动完成的,程序啥也不知道;这里的动态装载库是程序自己控制的,所以会提供给程序各种API。

                              +
                              +

                              而动态库的装载则是通过一系列由动态链接器提供的API,具体地讲共有4个函数:打开动态库(dlopen)、查找符号(dlsym)、错误处理(dlerror)以及关闭动态库(dlclose),程序可以通过这几个API对动态库进行操作。这几个API的实现是在/lib/libdl.so.2里面,它们的声明和相关常量被定义在系统标准头文件<dlfcn.h>

                              +
                              +

                              很有意思的是,如果我们将filename这个参数设置为0,那么dlopen返回的将是全局符号表的句柄,也就是说我们可以在运行时找到全局符号表里面的任何一个符号,并且可以执行它们,这有些类似高级语言反射(Reflection)的特性。全局符号表包括了程序的可执行文件本身、被动态链接器加载到进程中的所有共享模块以及在运行时通过dlopen打开并且使用了RTLD_GLOBAL方式的模块中的符号。

                              -

                              代码:

                              public class LinkedList<E>
                              extends AbstractSequentialList<E>
                              implements List<E>, Deque<E>, Cloneable, java.io.Serializable
                              {
                              //怎么连size也是transient?这不是代表着该对象的信息吗(
                              transient int size = 0;

                              transient Node<E> first;

                              transient Node<E> last;

                              public LinkedList() {
                              }

                              public LinkedList(Collection<? extends E> c) {
                              //调用空构造器
                              this();
                              addAll(c);
                              }

                              //把e接在链表头
                              private void linkFirst(E e) {
                              final Node<E> f = first;
                              final Node<E> newNode = new Node<>(null, e, f);
                              first = newNode;
                              if (f == null)
                              last = newNode;
                              else
                              f.prev = newNode;
                              size++;
                              modCount++;
                              }

                              void linkLast(E e) {
                              final Node<E> l = last;
                              final Node<E> newNode = new Node<>(l, e, null);
                              last = newNode;
                              if (l == null)
                              first = newNode;
                              else
                              l.next = newNode;
                              size++;
                              modCount++;
                              }

                              // Inserts element e before non-null Node succ.
                              void linkBefore(E e, Node<E> succ) {
                              // assert succ != null;
                              final Node<E> pred = succ.prev;
                              final Node<E> newNode = new Node<>(pred, e, succ);
                              succ.prev = newNode;
                              if (pred == null)
                              first = newNode;
                              else
                              pred.next = newNode;
                              size++;
                              modCount++;
                              }

                              private E unlinkFirst(Node<E> f) {
                              // assert f == first && f != null;
                              //这里就只靠注释会不会危险了()不过也确实会自动帮我们抛出NullPointerException的
                              //而且我在想,不是first已经指向头结点了吗,那你为什么还要把头结点作为参数传进来。。。
                              final E element = f.item;
                              final Node<E> next = f.next;
                              f.item = null;
                              f.next = null; // help GC
                              first = next;
                              if (next == null)
                              last = null;
                              else
                              next.prev = null;
                              size--;
                              modCount++;
                              return element;
                              }

                              //3
                              private E unlinkLast(Node<E> l) {
                              // assert l == last && l != null;
                              final E element = l.item;
                              final Node<E> prev = l.prev;
                              l.item = null;
                              l.prev = null; // help GC
                              last = prev;
                              if (prev == null)
                              first = null;
                              else
                              prev.next = null;
                              size--;
                              modCount++;
                              return element;
                              }

                              E unlink(Node<E> x) {
                              // assert x != null;
                              final E element = x.item;
                              final Node<E> next = x.next;
                              final Node<E> prev = x.prev;

                              if (prev == null) {
                              first = next;
                              } else {
                              prev.next = next;
                              x.prev = null;
                              }

                              if (next == null) {
                              last = prev;
                              } else {
                              next.prev = prev;
                              x.next = null;
                              }

                              x.item = null;
                              size--;
                              modCount++;
                              return element;
                              }

                              public E getFirst() {
                              final Node<E> f = first;
                              if (f == null)
                              throw new NoSuchElementException();
                              return f.item;
                              }

                              public E getLast() {
                              final Node<E> l = last;
                              if (l == null)
                              throw new NoSuchElementException();
                              return l.item;
                              }

                              public E removeFirst() {
                              final Node<E> f = first;
                              if (f == null)
                              throw new NoSuchElementException();
                              return unlinkFirst(f);
                              }

                              public E removeLast() {
                              final Node<E> l = last;
                              if (l == null)
                              throw new NoSuchElementException();
                              return unlinkLast(l);
                              }

                              public void addFirst(E e) {linkFirst(e);}

                              public void addLast(E e) {linkLast(e);}

                              public boolean contains(Object o) {return indexOf(o) != -1;}

                              public int size() {return size;}

                              public boolean add(E e) {
                              linkLast(e);
                              return true;
                              }

                              public boolean remove(Object o) {
                              if (o == null) {
                              for (Node<E> x = first; x != null; x = x.next) {
                              if (x.item == null) {
                              unlink(x);
                              return true;
                              }
                              }
                              } else {
                              for (Node<E> x = first; x != null; x = x.next) {
                              if (o.equals(x.item)) {
                              unlink(x);
                              return true;
                              }
                              }
                              }
                              return false;
                              }

                              public boolean addAll(Collection<? extends E> c) {
                              return addAll(size, c);
                              }

                              public boolean addAll(int index, Collection<? extends E> c) {
                              checkPositionIndex(index);

                              Object[] a = c.toArray();
                              int numNew = a.length;
                              if (numNew == 0)
                              return false;

                              //分成两段
                              Node<E> pred, succ;
                              if (index == size) {
                              succ = null;
                              pred = last;
                              } else {
                              succ = node(index);
                              pred = succ.prev;
                              }

                              //往中间加料
                              for (Object o : a) {
                              @SuppressWarnings("unchecked") E e = (E) o;
                              Node<E> newNode = new Node<>(pred, e, null);
                              if (pred == null)
                              first = newNode;
                              else
                              pred.next = newNode;
                              pred = newNode;
                              }

                              //合起来
                              if (succ == null) {
                              last = pred;
                              } else {
                              pred.next = succ;
                              succ.prev = pred;
                              }

                              size += numNew;
                              modCount++;
                              return true;
                              }


                              public void clear() {
                              // 1
                              // Clearing all of the links between nodes is "unnecessary", but:
                              // - helps a generational GC if the discarded nodes inhabit
                              // more than one generation
                              // - is sure to free memory even if there is a reachable Iterator
                              for (Node<E> x = first; x != null; ) {
                              Node<E> next = x.next;
                              x.item = null;
                              x.next = null;
                              x.prev = null;
                              x = next;
                              }
                              first = last = null;
                              size = 0;
                              modCount++;
                              }

                              // Positional Access Operations

                              public E get(int index) {
                              checkElementIndex(index);
                              return node(index).item;
                              }

                              public E set(int index, E element) {
                              checkElementIndex(index);
                              Node<E> x = node(index);
                              E oldVal = x.item;
                              x.item = element;
                              return oldVal;
                              }

                              public void add(int index, E element) {
                              checkPositionIndex(index);

                              if (index == size)
                              linkLast(element);
                              else
                              linkBefore(element, node(index));
                              }

                              public E remove(int index) {
                              checkElementIndex(index);
                              return unlink(node(index));
                              }

                              Node<E> node(int index) {
                              // assert isElementIndex(index);
                              //所以为啥不能check一下?

                              //做了个小小的优化,如果在前半就从开头找,在后半就从最后往前找
                              if (index < (size >> 1)) {
                              Node<E> x = first;
                              for (int i = 0; i < index; i++)
                              x = x.next;
                              return x;
                              } else {
                              Node<E> x = last;
                              for (int i = size - 1; i > index; i--)
                              x = x.prev;
                              return x;
                              }
                              }

                              // Search Operations

                              public int indexOf(Object o) {...}

                              public int lastIndexOf(Object o) {...}

                              // Queue operations.
                              //部分省略

                              //always return true
                              public boolean offer(E e) {
                              return add(e);
                              }

                              // Deque operations
                              //省略

                              public boolean removeFirstOccurrence(Object o) {
                              //666
                              return remove(o);
                              }

                              public boolean removeLastOccurrence(Object o) {...}

                              public ListIterator<E> listIterator(int index) {
                              checkPositionIndex(index);
                              return new ListItr(index);
                              }

                              //非常聪明非常漂亮的指针操作
                              private class ListItr implements ListIterator<E> {
                              private Node<E> lastReturned;
                              private Node<E> next;
                              private int nextIndex;
                              private int expectedModCount = modCount;

                              ListItr(int index) {
                              // assert isPositionIndex(index);
                              next = (index == size) ? null : node(index);
                              nextIndex = index;
                              }

                              public boolean hasNext() {
                              return nextIndex < size;
                              }

                              public E next() {
                              checkForComodification();
                              if (!hasNext())
                              throw new NoSuchElementException();

                              lastReturned = next;
                              next = next.next;
                              nextIndex++;
                              return lastReturned.item;
                              }

                              public boolean hasPrevious() {
                              return nextIndex > 0;
                              }

                              public E previous() {
                              checkForComodification();
                              if (!hasPrevious())
                              throw new NoSuchElementException();

                              //注意这个next是内部类里的成员变量,last是外部类的成员变量
                              lastReturned = next = (next == null) ? last : next.prev;
                              nextIndex--;
                              return lastReturned.item;
                              }

                              public int nextIndex() {
                              return nextIndex;
                              }

                              public int previousIndex() {
                              return nextIndex - 1;
                              }

                              public void remove() {
                              checkForComodification();
                              if (lastReturned == null)
                              throw new IllegalStateException();

                              Node<E> lastNext = lastReturned.next;
                              unlink(lastReturned);
                              if (next == lastReturned)
                              next = lastNext;
                              else
                              nextIndex--;
                              lastReturned = null;
                              expectedModCount++;
                              }

                              public void set(E e) {
                              if (lastReturned == null)
                              throw new IllegalStateException();
                              checkForComodification();
                              lastReturned.item = e;
                              }

                              public void add(E e) {
                              checkForComodification();
                              lastReturned = null;
                              if (next == null)
                              linkLast(e);
                              else
                              linkBefore(e, next);
                              nextIndex++;
                              expectedModCount++;
                              }

                              public void forEachRemaining(Consumer<? super E> action) {
                              Objects.requireNonNull(action);
                              while (modCount == expectedModCount && nextIndex < size) {
                              action.accept(next.item);
                              lastReturned = next;
                              next = next.next;
                              nextIndex++;
                              }
                              checkForComodification();
                              }

                              final void checkForComodification() {
                              if (modCount != expectedModCount)
                              throw new ConcurrentModificationException();
                              }
                              }

                              //节点类,平平无奇链表捏
                              private static class Node<E> {
                              E item;
                              Node<E> next;
                              Node<E> prev;

                              Node(Node<E> prev, E element, Node<E> next) {
                              this.item = element;
                              this.next = next;
                              this.prev = prev;
                              }
                              }

                              public Iterator<E> descendingIterator() {
                              return new DescendingIterator();
                              }

                              // 2 降序迭代器
                              private class DescendingIterator implements Iterator<E> {
                              //借助升序迭代器实现
                              private final ListItr itr = new ListItr(size());

                              public boolean hasNext() {
                              return itr.hasPrevious();
                              }
                              public E next() {
                              return itr.previous();
                              }
                              public void remove() {
                              itr.remove();
                              }
                              }

                              @SuppressWarnings("unchecked")
                              private LinkedList<E> superClone() {
                              try {
                              return (LinkedList<E>) super.clone();
                              } catch (CloneNotSupportedException e) {
                              throw new InternalError(e);
                              }
                              }

                              public Object clone() {
                              LinkedList<E> clone = superClone();

                              // 初始化
                              clone.first = clone.last = null;
                              clone.size = 0;
                              clone.modCount = 0;

                              // Initialize clone with our elements
                              for (Node<E> x = first; x != null; x = x.next)
                              clone.add(x.item);

                              return clone;
                              }

                              public Object[] toArray() {...}

                              @SuppressWarnings("unchecked")
                              public <T> T[] toArray(T[] a) {...}

                              private static final long serialVersionUID = 876323262645176354L;

                              private void writeObject(java.io.ObjectOutputStream s)
                              throws java.io.IOException {...}

                              @SuppressWarnings("unchecked")
                              private void readObject(java.io.ObjectInputStream s)
                              throws java.io.IOException, ClassNotFoundException {...}

                              @Override
                              public Spliterator<E> spliterator() {
                              return new LLSpliterator<E>(this, -1, 0);
                              }

                              static final class LLSpliterator<E> implements Spliterator<E> {...}
                              //4
                              }
                              +

                              它接下来举的例子很有意思,可惜不知道为啥在我这一直segment fault。。。好像是它用的内联汇编是32位什么的,我折腾了半天还是没办法,算了

                              +
                              /*
                              我们这个例子中将实现一个更为灵活的叫做runso的程序,这个程序可以通过命令行来执行共享对象里面的任意一个函数。
                              它在理论上很简单,基本的步骤就是:由命令行给出共享对象路径、函数名和相关参数,然后程序通过运行时加载将该模块加载
                              到进程中,查找相应的函数,并且执行它,然后将执行结果打印出来。
                              为了表示参数和返回值类型,我们假设字母d表示double、i表示int、s表示char*、v表示void
                              比如说,如果要调用/lib/libfoo.so里面一个void bar(char* str, int i)的函数,可以使用如下命令行:
                              $./RunSo /lib/libfoo.so bar sHello i10
                              */

                              #include <stdio.h>
                              #include <dlfcn.h>
                              #include <stdint.h>

                              // 大概就是根据参数类型把参数压入栈
                              #define SETUP_STACK \
                              i = 2; \
                              while (++i < argc - 1) { \
                              switch(argv[i][0]) { \
                              case 'i': \
                              int res = atoi(&argv[i][1]); \
                              asm volatile(".code32\n" \
                              "push %0" :: \
                              "r"(res )); \
                              asm volatile(".code64\n"); \
                              esp += 4; \
                              break; \
                              case 'd': \
                              atof(&argv[i][1]); \
                              asm volatile("subl $8,%esp\n" \
                              "fstpl (%esp)" ); \
                              esp += 8; \
                              break; \
                              case 's': \
                              asm volatile("push %0" :: \
                              "r"(&argv[i][1]) ); \
                              esp += 4; \
                              break; \
                              default: \
                              printf("error argument type"); \
                              goto exit_runso; \
                              } \
                              }

                              // 大概就是相当于pop,给esp加上我们之前申请的栈空间esp
                              #define RESTORE_STACK \
                              asm volatile("add %0,%%esp"::"r"(esp))

                              int main(int argc, char* argv[])
                              {
                              void* handle;
                              char* error;
                              int i;
                              int esp = 0;
                              void* func;

                              handle = dlopen(argv[1], RTLD_NOW);
                              if(handle == 0) {
                              printf("Can't find library: %s\n", argv[1]);
                              return -1;
                              }

                              func = dlsym(handle, argv[2]);
                              if( (error = dlerror()) != NULL ) {
                              printf("Find symbol %s error: %s\n", argv[2], error);
                              goto exit_runso;
                              }

                              // 根据返回值不同构造函数指针
                              switch(argv[argc-1][0]){
                              case 'i':
                              {
                              int (*func_int)() = func;
                              SETUP_STACK;
                              int ret = func_int();
                              RESTORE_STACK;
                              printf("ret = %d\n", ret );
                              break;
                              }
                              case 'd':
                              {
                              double (*func_double)() = func;
                              SETUP_STACK;
                              double ret = func_double();
                              RESTORE_STACK;
                              printf("ret = %f\n", ret );
                              break;
                              }
                              case 's':
                              {
                              char* (*func_str)() = func;
                              SETUP_STACK;
                              char* ret = func_str();
                              RESTORE_STACK;
                              printf("ret = %s\n", ret );
                              break;
                              }
                              case 'v':
                              {
                              void (*func_void)() = func;
                              SETUP_STACK;
                              func_void();
                              RESTORE_STACK;
                              printf("ret = void");
                              break;
                              }
                              } // end of switch

                              exit_runso:
                              dlclose(handle);
                              }
                              -

                              其中

                                -
                              1. 关于分代GC

                                In the clear() function:

                                -
                                // Clearing all of the links between nodes is "unnecessary", but:
                                // - helps a generational GC if the discarded nodes inhabit
                                // more than one generation
                                // - is sure to free memory even if there is a reachable Iterator
                                -

                                还没看懂,插个眼

                                -
                              2. -
                              3. 降序迭代器

                                一切都反过来了,也没有升序迭代器恁多方法:

                                -

                                不支持foreach循环,只支持单向遍历,没有add set 只有remove。

                                + +

                                运行库

                                初始化

                                操作系统装载程序之后,首先运行的代码并不是main的第一行,而是某些别的代码,这些代码负责准备好main函数执行所需要的环境,并且负责调用main函数,这时候你才可以在main函数里放心大胆地写各种代码:申请内存、使用系统调用、触发异常、访问I/O。在main返回之后,它会记录main函数的返回值,调用atexit注册的函数,然后结束进程。

                                +

                                运行这些代码的函数称为入口函数或入口点(Entry Point),视平台的不同而有不同的名字。程序的入口点实际上是一个程序的初始化和结束部分,它往往是运行库的一部分

                                +

                                一个典型的程序运行步骤大致如下:

                                +
                                  +
                                1. 操作系统在创建进程后,把控制权交到了程序的入口,这个入口往往是运行库中的某个入口函数。

                                2. -
                                3. 关于unlinkLast/First的参数问题

                                  In Java LinkedList source code, why the unlinkFirst function should have a param pointing to the first node?

                                  -

                                  事实证明确实人家也觉得无参比较合理(

                                  +
                                4. 入口函数对运行库和程序运行环境进行初始化,包括堆、I/O、线程、全局变量构造,等等。

                                5. -
                                6. sublist

                                  LinkedList用了从AbstractList继承来的sublist相关类和方法,没有特别的优化,其sublist不可序列化,且not cloneable。

                                  +
                                7. 入口函数在完成初始化之后,调用main函数,正式开始执行程序主体部分。

                                8. -
                                9. transient的三个成员变量

                                  In Java LinkedList, why the “first”, “last” and “size” variable are transient?

                                  -
                                  -

                                  LinkedList provide its own method for serializing and de-serializing.

                                  -

                                  When serializing, it only writes the size, and the values of the list.

                                  -

                                  When deserializing, it reads the size, then build the list from scratch, each node for each value at a time.

                                  -

                                  If the author did not provide their own read and write methods, then they would need to make size, first, and last non-transient. They would also need to make the Node class serializable.

                                  -
                                  -

                                  As all the member variables of java LinkedList is transient, what will be the use of implementing Serializable?

                                  -
                                  -

                                  Otherwise serializing would be by default, which would be recursive, and for a large list would easily blow the stack.

                                  -
                                  -

                                  意思就是序列化时不记录这些信息,反序列化时会重新构建。还有说如果用默认的序列化方法是递归的可能爆栈?还有我觉得有一点可能是如果把所有node都序列化了,可能反序列化后,本来分配到那段内存空间要是被占用了,但指针值不变还是会有问题?等待之后解答。

                                  +
                                10. main函数执行完毕以后,返回到入口函数,入口函数进行清理⼯作,包括全局变量析构、堆销毁、关闭I/O等,然后进行系统调用结束进程。

                                -

                                Vector

                                -

                                Unlike the new collection implementations, Vector is synchronized. If a thread-safe implementation is not needed, it is recommended to use ArrayList in place of Vector.

                                +

                                Linux中的C语言运行库就是glibc

                                +

                                运行库

                                运行时库(Runtime Library)为入口函数及其所依赖的函数所构成的函数、各种标准库函数的实现的集合。可以通过sudo apt-get install glibc-source安装glibc的源代码。

                                +

                                一个C语言运行库大致包含了如下功能:

                                +
                                  +
                                1. 启动与退出:包括入口函数及入口函数所依赖的其他函数等。

                                  +
                                2. +
                                3. 标准函数:由C语言标准规定的C语言标准库所拥有的函数实现。

                                  +
                                4. +
                                5. I/O:I/O功能的封装和实现,参见上一节中I/O初始化部分。

                                  +

                                  应该指的是比如说提供File*指针、IO stream之类的高级功能封装。

                                  +
                                6. +
                                7. 堆:堆的封装和实现,参见上一节中堆初始化部分。

                                  +
                                  +

                                  这点让我耳目一新!因为我以前一直以为堆栈都是操作系统实现的,现在想来才发现确实,操作系统只负责通过sbrk系统调用给内存,具体的堆分配算法由glibc的malloc实现。

                                  -

                                  代码:

                                  public class Vector<E>
                                  extends AbstractList<E>
                                  implements List<E>, RandomAccess, Cloneable, java.io.Serializable
                                  {

                                  protected Object[] elementData;

                                  protected int elementCount;

                                  protected int capacityIncrement;

                                  private static final long serialVersionUID = -2767605614048989439L;

                                  public Vector(int initialCapacity, int capacityIncrement) {
                                  super();
                                  if (initialCapacity < 0)
                                  throw new IllegalArgumentException("Illegal Capacity: "+
                                  initialCapacity);
                                  this.elementData = new Object[initialCapacity];
                                  this.capacityIncrement = capacityIncrement;
                                  }

                                  public Vector(int initialCapacity) {
                                  this(initialCapacity, 0);
                                  }

                                  public Vector() {
                                  //1
                                  this(10);
                                  }

                                  public Vector(Collection<? extends E> c) {
                                  Object[] a = c.toArray();
                                  elementCount = a.length;
                                  if (c.getClass() == ArrayList.class) {
                                  elementData = a;
                                  } else {
                                  elementData = Arrays.copyOf(a, elementCount, Object[].class);
                                  }
                                  }

                                  public synchronized void copyInto(Object[] anArray) {
                                  System.arraycopy(elementData, 0, anArray, 0, elementCount);
                                  }

                                  //2
                                  public synchronized void trimToSize() {
                                  modCount++;
                                  int oldCapacity = elementData.length;
                                  if (elementCount < oldCapacity) {
                                  elementData = Arrays.copyOf(elementData, elementCount);
                                  }
                                  }

                                  public synchronized void ensureCapacity(int minCapacity) {
                                  if (minCapacity > 0) {
                                  modCount++;
                                  ensureCapacityHelper(minCapacity);
                                  }
                                  }

                                  private void ensureCapacityHelper(int minCapacity) {
                                  // overflow-conscious code
                                  if (minCapacity - elementData.length > 0)
                                  grow(minCapacity);
                                  }

                                  private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

                                  private void grow(int minCapacity) {...}

                                  private static int hugeCapacity(int minCapacity) {...}

                                  /*
                                  Sets the size of this vector. If the new size is greater than the current size, new null items are added to the end of the vector. If the new size is less than the current size, all components at index newSize and greater are discarded.
                                  */
                                  public synchronized void setSize(int newSize) {
                                  modCount++;
                                  if (newSize > elementCount) {
                                  ensureCapacityHelper(newSize);
                                  } else {
                                  for (int i = newSize ; i < elementCount ; i++)
                                  //3 GC帮大忙,注意这里没有trim。
                                  elementData[i] = null;
                                  }
                                  }
                                  elementCount = newSize;
                                  }

                                  //capacity 、size 、isEmpty省略

                                  //4
                                  public Enumeration<E> elements() {
                                  return new Enumeration<E>() {
                                  int count = 0;
                                  public boolean hasMoreElements() {
                                  return count < elementCount;
                                  }
                                  public E nextElement() {
                                  synchronized (Vector.this) {
                                  if (count < elementCount) {
                                  return elementData(count++);
                                  }
                                  }
                                  throw new NoSuchElementException("Vector Enumeration");
                                  }
                                  };
                                  }

                                  //contains、indexof、lastIndexOf省略

                                  public synchronized E elementAt(int index) {
                                  if (index >= elementCount) {
                                  throw new ArrayIndexOutOfBoundsException(index + " >= " + elementCount);
                                  }

                                  return elementData(index);
                                  }

                                  public synchronized E firstElement() {
                                  if (elementCount == 0) {
                                  throw new NoSuchElementException();
                                  }
                                  return elementData(0);
                                  }

                                  public synchronized E lastElement() {
                                  if (elementCount == 0) {
                                  throw new NoSuchElementException();
                                  }
                                  return elementData(elementCount - 1);
                                  }

                                  public synchronized void setElementAt(E obj, int index) {...}

                                  public synchronized void removeElementAt(int index) {...}

                                  public synchronized void insertElementAt(E obj, int index) {...}

                                  public synchronized void addElement(E obj) {...}

                                  public synchronized boolean removeElement(Object obj) {...}

                                  //clear
                                  public synchronized void removeAllElements() {
                                  modCount++;
                                  // Let gc do its work
                                  for (int i = 0; i < elementCount; i++)
                                  elementData[i] = null;

                                  elementCount = 0;
                                  }

                                  public synchronized Object clone() {...}

                                  public synchronized Object[] toArray() {
                                  return Arrays.copyOf(elementData, elementCount);
                                  }

                                  @SuppressWarnings("unchecked")
                                  public synchronized <T> T[] toArray(T[] a) {...}

                                  // Positional Access Operations
                                  //add set get remove clear省略

                                  // Bulk Operations
                                  // xxxAll省略

                                  //用了AbstractList的equal、hashcode、tostring,省略

                                  public synchronized List<E> subList(int fromIndex, int toIndex) {
                                  return Collections.synchronizedList(super.subList(fromIndex, toIndex),
                                  this);
                                  }

                                  protected synchronized void removeRange(int fromIndex, int toIndex) {...}

                                  //跟AL不一样
                                  private void readObject(ObjectInputStream in)
                                  throws IOException, ClassNotFoundException {
                                  ObjectInputStream.GetField gfields = in.readFields();
                                  int count = gfields.get("elementCount", 0);
                                  Object[] data = (Object[])gfields.get("elementData", null);
                                  if (count < 0 || data == null || count > data.length) {
                                  throw new StreamCorruptedException("Inconsistent vector internals");
                                  }
                                  elementCount = count;
                                  elementData = data.clone();
                                  }

                                  private void writeObject(java.io.ObjectOutputStream s)
                                  throws java.io.IOException {
                                  final java.io.ObjectOutputStream.PutField fields = s.putFields();
                                  final Object[] data;
                                  synchronized (this) {
                                  fields.put("capacityIncrement", capacityIncrement);
                                  fields.put("elementCount", elementCount);
                                  data = elementData.clone();
                                  }
                                  fields.put("elementData", data);
                                  s.writeFields();
                                  }

                                  public synchronized ListIterator<E> listIterator(int index) {
                                  if (index < 0 || index > elementCount)
                                  throw new IndexOutOfBoundsException("Index: "+index);
                                  return new ListItr(index);
                                  }

                                  public synchronized ListIterator<E> listIterator() {
                                  return new ListItr(0);
                                  }

                                  public synchronized Iterator<E> iterator() {
                                  return new Itr();
                                  }

                                  private class Itr implements Iterator<E> {...}

                                  final class ListItr extends Itr implements ListIterator<E> {...}

                                  @Override
                                  public synchronized void forEach(Consumer<? super E> action) {...}

                                  @Override
                                  @SuppressWarnings("unchecked")
                                  public synchronized boolean removeIf(Predicate<? super E> filter) {...}

                                  @Override
                                  @SuppressWarnings("unchecked")
                                  public synchronized void replaceAll(UnaryOperator<E> operator) {...}

                                  @SuppressWarnings("unchecked")
                                  @Override
                                  public synchronized void sort(Comparator<? super E> c) {...}

                                  @Override
                                  public Spliterator<E> spliterator() {
                                  return new VectorSpliterator<>(this, null, 0, -1, 0);
                                  }

                                  static final class VectorSpliterator<E> implements Spliterator<E> {...}
                                  }
                                  - -

                                  其中:

                                    -
                                  1. 默认容量

                                    空构造器Vector()创建出来的默认容量为10,不同于ArrayList是个空集合。

                                  2. -
                                  3. 扩容操作

                                    与ArrayList基本上是雷同的,就是都是synchronized。

                                    +
                                  4. 语言实现:语言中一些特殊功能的实现。

                                  5. -
                                  6. setsize不改变容量

                                    实现中只是把东西设置为空,并没有trim,因而容量不变

                                    +
                                  7. 调试:实现调试功能的代码。

                                  8. -
                                  9. Vector可生成枚举类
                                  -

                                  Set

                                  Set(I)

                                  代码:

                                  /* 1
                                  Note: 当set里包含可变的对象时,要多加小心。
                                  The behavior of a set is not specified if the value of a set object is changed in a manner that affects equals comparisons.
                                  A special case of this prohibition is that it is not permissible for a set to contain itself as an element.
                                  */
                                  public interface Set<E> extends Collection<E> {
                                  // Query Operations
                                  int size();

                                  boolean isEmpty();

                                  boolean contains(Object o);
                                  /*Returns an iterator over the elements in this set.
                                  The elements are returned in no particular order
                                  (unless this set is an instance of some class that provides a guarantee).*/
                                  Iterator<E> iterator();

                                  Object[] toArray();

                                  <T> T[] toArray(T[] a);

                                  // Modification Operations

                                  /*
                                  If this set already contains the element,
                                  the call leaves the set unchanged and returns false.
                                  */
                                  boolean add(E e);

                                  boolean remove(Object o);

                                  // Bulk Operations

                                  /*
                                  Returns true if this set contains all of the elements of the specified collection.
                                  If the specified collection is also a set,
                                  this method returns true if it is a subset of this set.[这个subset的定义有点意思]
                                  */
                                  boolean containsAll(Collection<?> c);

                                  //2
                                  //取并集
                                  boolean addAll(Collection<? extends E> c);

                                  //取交集
                                  boolean retainAll(Collection<?> c);

                                  //集合差
                                  boolean removeAll(Collection<?> c);

                                  void clear();

                                  // Comparison and hashing

                                  boolean equals(Object o);

                                  int hashCode();

                                  @Override
                                  default Spliterator<E> spliterator() {
                                  return Spliterators.spliterator(this, Spliterator.DISTINCT);
                                  }
                                  }
                                  +

                                  库函数介绍

                                  它这里主要讲了两个比较特殊的库,还挺有意思的:变长参数(stdarg.h)和非局部跳转(setjmp.h)。

                                  +
                                    +
                                  1. 变长参数

                                    +

                                    讲这玩意其实用作是printf的实现。看下下面这两个代码相信你就能明白printf的基本原理了:

                                    +
                                    // Code 1
                                    int sum(unsigned num, ...)
                                    {
                                    int* p = &num + 1;
                                    int ret = 0;
                                    while (num--)
                                    ret += *p++;
                                    return ret;
                                    }

                                    call:
                                    int n = sum(3, 16, 38, 53);
                                    -

                                    其中:

                                      -
                                    1. set中包含可变对象

                                      需要规避可修改对象,使其与集合中另一个元素重复的问题

                                      -

                                      详见此:

                                      -

                                      Java HashSet contains duplicates if contained element is modified

                                      +
                                      #include <stdarg.h>
                                      // Code 2
                                      void arg_match(const char* fmt, ...) {
                                      va_list ap; // 本质char * / void *
                                      va_start(ap, fmt); // 之后ap就会指向fmt后的第一个可变参数

                                      int idx = 0;
                                      for (int i = 0; i < strlen(fmt); i ++) {
                                      if (fmt[i] != '%') continue;
                                      idx ++;
                                      switch (fmt[i + 1]) {
                                      case 'd':
                                      int argv_i = va_arg(ap, int);
                                      printf("第%d个参数为:%d\n", idx, argv_i);
                                      break;
                                      case 's':
                                      char* argv_s = va_arg(ap, char*);
                                      printf("第%d个参数为:%s\n", idx, argv_s);
                                      break;
                                      default:
                                      printf("unknown.\n");
                                      break;
                                      }
                                      }
                                      }

                                      call:
                                      arg_match("%d %d %s\n", 1, 2, "333");
                                      + +

                                      除此之外,我们也可以实现变长参数宏

                                      +

                                      在GCC编译器下,变长参数宏可以使用“##”宏字符串连接操作实现。

                                      +
                                      #define printf(args…) fprintf(stdout, ##args)
                                    2. +
                                    3. 非局部跳转

                                      +

                                      这位更是重量级

                                      +
                                      #include <setjmp.h>
                                      #include <stdio.h>

                                      jmp_buf b;

                                      void f()
                                      {
                                      longjmp(b, 1);
                                      }

                                      int main()
                                      {
                                      if (setjmp(b))
                                      printf("World!");
                                      else
                                      {
                                      printf("Hello ");
                                      f();
                                      }
                                      }
                                      + +

                                      事实上的输出是:

                                      +

                                      Hello World!

                                      -

                                      The correct solution is to stick to the contract of Set and not modify objects after adding them to the collection.

                                      -

                                      You can avoid this problem by either:

                                      -
                                        -
                                      • using an immutable type for your set elements,
                                      • -
                                      • making a copy of the objects as you put them into the set and / or pull them out of the set,
                                      • -
                                      • writing your code so that it “knows“ not to change the objects for the duration …
                                      • -
                                      -

                                      From the perspective of correctness and robustness, the first option is clearly best.

                                      +

                                      实际上,当setjmp正常返回的时候,会返回0,因此会打印出“Hello”的字样。而longjmp的作用,就是让程序的执行流回到当初setjmp返回的时刻,并且返回由longjmp指定的返回值(longjmp的参数2),也就是1,自然接着会打印出“World!”并退出。换句话说,longjmp可以让程序“时光倒流”回setjmp返回的时刻,并改变其行为,以至于改变了未来。

                                    4. -
                                    5. 集合操作

                                      set抽象自数学的集合,因此有很多对应的集合操作:

                                      +
                                    +

                                    glibc

                                    glibc是对标准C运行库的扩展(如增加了pthread),全称GNU C Library,是GNU旗下的C标准库。

                                    +

                                    生命周期

                                    于是,我们可以完整串联整个运行程序的生命周期:

                                    +

                                    由链接器ld将所有.o文件的_init段和_finit段(包含glibc对堆空间的初始化和释放、编译器对C++全局对象构造析构的实现以及app自己实现的init和finit函数)分别串在一起,并且链接上glibc库的包含了_start(会调用_init)的crt.o文件,最后就形成了包含各种glibc标准库和真·用户代码的可执行文件。

                                    +

                                    可执行文件被装载到进程地址空间后,首先会进行动态链接。然后,从程序入口_start开始进行各种初始化,调用可执行文件的这个_init段的内容。init完成之后,glibc就调用程序中的入口main。main执行过程中会用到glibc的各种标准库函数。main执行完后就会继续执行_finit段来结束一切。

                                    +

                                    C++的全局对象构造析构

                                      +
                                    1. 构造(_init

                                      +

                                      编译器会将每个全局对象的构造函数以如下形式包装:

                                      +
                                      static void GLOBAL__I_Hw(void)
                                      {
                                      Hw::Hw(); // 构造对象
                                      atexit(__tcf_1); // 一个神秘的函数叫做__tcf_1被注册到了exit
                                      }
                                      + +

                                      然后将这个GLOBAL__I_Hw放进.o文件的一个.ctor段中,最后由ld将各个.o文件的.ctor段链接起来,并计算出全局对象数量填入crtbegin.o即可。

                                      +

                                      之后在_init段中遍历.ctor的各个函数指针进行构造函数调用就行了

                                      -

                                      addAll ∪

                                      -

                                      retainAll ∩

                                      -

                                      removeAll -

                                      -
                                      +

                                      后日谈:今天又在rtt中看到了这一牛掰操作。rtt也是大概通过这个原理实现的帅的一匹的“Automatic Initialization Mechanism”。

                                      +

                                      原理感觉也是将其放入一个特殊的”rti_fn$f”段,并且用rti_start和end来标识该段结束,

                                      +
                                      // xiunian: INIT_EXPORT应该是这个
                                      INIT_EXPORT(fn, "1.0") 宏展开:
                                      const char __rti_level_fn[] = ".rti_fn." "1.0";
                                      // 指示编译器将特定的变量或数据结构分配到名为 "rti_fn$f" 的内存段(Memory Segment)中
                                      __declspec(allocate("rti_fn$f"))
                                      rt_used const struct rt_init_desc __rt_init_msc_fn = {__rti_level_fn, fn };
                                      + +
                                      /*
                                      * xiunian: 牛逼,这里颇有学链接时的感觉了
                                      * 这里介绍了组件初始化顺序
                                      * Components Initialization will initialize some driver and components as following
                                      * order:
                                      * rti_start --> 0
                                      * BOARD_EXPORT --> 1
                                      * rti_board_end --> 1.end
                                      *
                                      * DEVICE_EXPORT --> 2
                                      * COMPONENT_EXPORT --> 3
                                      * FS_EXPORT --> 4
                                      * ENV_EXPORT --> 5
                                      * APP_EXPORT --> 6
                                      *
                                      * rti_end --> 6.end
                                      *
                                      * These automatically initialization, the driver or component initial function must
                                      * be defined with:
                                      * INIT_BOARD_EXPORT(fn);
                                      * INIT_DEVICE_EXPORT(fn);
                                      * ...
                                      * INIT_APP_EXPORT(fn);
                                      * etc.
                                      */
                                      static int rti_start(void)
                                      {
                                      return 0;
                                      }
                                      INIT_EXPORT(rti_start, "0");

                                      static int rti_board_start(void)
                                      {
                                      return 0;
                                      }
                                      INIT_EXPORT(rti_board_start, "0.end");

                                      static int rti_board_end(void)
                                      {
                                      return 0;
                                      }
                                      INIT_EXPORT(rti_board_end, "1.end");

                                      static int rti_end(void)
                                      {
                                      return 0;
                                      }
                                      INIT_EXPORT(rti_end, "6.end");
                                      + +

                                      之后真正初始化只需遍历然后调用函数指针即可

                                      +
                                      void rt_components_board_init(void)
                                      {
                                      volatile const init_fn_t *fn_ptr;

                                      for (fn_ptr = &__rt_init_rti_board_start; fn_ptr < &__rt_init_rti_board_end; fn_ptr++)
                                      {
                                      (*fn_ptr)();
                                      }
                                      }
                              4. +
                              5. 析构(_finit

                                +

                                早期同理可得。现在变了,变成直接在GLOBAL__I_Hw中注册atexit了。

                                +
                                static void __tcf_1(void) //这个名字由编译器生成
                                {
                                Hw.~HelloWorld();
                                }
                              -

                              AbstratcSet(A)

                              -

                              Note that this class does not override any of the implementations from the AbstractCollection class. It merely adds implementations for equals and hashCode.

                              +

                              实现小型运行库

                              +

                              看到标题就知道接下来有多帅了

                              -

                              代码:

                              public abstract class AbstractSet<E> extends AbstractCollection<E> implements Set<E> {

                              protected AbstractSet() {
                              }

                              // Comparison and hashing

                              public boolean equals(Object o) {
                              if (o == this)
                              return true;

                              if (!(o instanceof Set))
                              return false;
                              Collection<?> c = (Collection<?>) o;
                              if (c.size() != size())
                              return false;
                              try {
                              return containsAll(c);
                              } catch (ClassCastException unused) {
                              return false;
                              } catch (NullPointerException unused) {
                              return false;
                              }
                              }

                              public int hashCode() {
                              int h = 0;
                              Iterator<E> i = iterator();
                              while (i.hasNext()) {
                              E obj = i.next();
                              if (obj != null)
                              h += obj.hashCode();
                              }
                              return h;
                              }

                              //不大明白为啥要修改实现,用AbstractCollection的不好吗
                              public boolean removeAll(Collection<?> c) {
                              Objects.requireNonNull(c);
                              boolean modified = false;

                              if (size() > c.size()) {
                              for (Iterator<?> i = c.iterator(); i.hasNext(); )
                              modified |= remove(i.next());
                              } else {
                              for (Iterator<?> i = iterator(); i.hasNext(); ) {
                              if (c.contains(i.next())) {
                              i.remove();
                              modified = true;
                              }
                              }
                              }
                              return modified;
                              }
                              }
                              +

                              在这一章我们仅实现CRT几个关键的部分。虽然这个迷你CRT仅仅实现了为数不多的功能,但是它已经具备了CRT的关键功能:入口函数、初始化、堆管理、基本IO,甚至还将实现堆C++的new/delete、stream和string的支持。

                              +

                              本章主要分为两个部分,首先实现一个仅仅支持C语言的运行库,即传统意义上的CRT。其次,将为这个CRT加入一部分以支持C++语言的运行时特性。

                              +

                              相关代码放在github了,其实感觉差不多是按它写的抄了一遍。可以现在稍微整理下文件结构。

                              +
                                +
                              1. just for C

                                +

                                前面说到,CRT的作用是执行init和finit段、进行堆的管理、进行IO的封装管理以及提供各种标准C语言库。因而,我们可以分别用如下几个文件来实现这几个功能:

                                +
                                  +
                                1. entry.c

                                  +

                                  用于实现入口函数mini_crt_entry。入口函数中主要要做:调用main之前的栈构造、堆初始化、IO初始化,最后调用main函数。main函数返回后,通过系统调用exit来杀死进程。

                                  +
                                2. +
                                3. malloc.c

                                  +

                                  用于实现堆的管理,主要实现了mallocfree。使用了空闲链表的小内存管理法,实现简单。

                                  +
                                4. +
                                5. stdio.c

                                  +

                                  用于实现IO封装,freadfwritefopenfclosefseek。实现简单,因而只是系统调用的封装

                                  +
                                6. +
                                7. string.c

                                  +

                                  以字符串操作为例,提供的标准C语言库。

                                  +
                                8. +
                                +

                                之后,我们将其以如下参数编译为静态库:

                                +
                                $ gcc -c -fno-builtin -nostdlib -fno-stack-protector entry.c malloc.c stdio.c string.c printf.c
                                $ ar -rs minicrt.a malloc.o printf.o stdio.o string.o
                                # 编译测试用例
                                $ gcc -m32 -c -ggdb -fno-builtin -nostdlib -fno-stack-protector test.c
                                -

                                HashSet

                                -

                                In particular, it does not guarantee that the order will remain constant over time.

                                -

                                Iterating over this set requires time proportional to the sum of the HashSet instance’s size (the number of elements) plus the “capacity” of the backing HashMap instance (the number of buckets).

                                -

                                Thus, it’s very important not to set the initial capacity too high (or the load factor too low) if iteration performance is important.

                                -

                                This implementation is not synchronized. Set s = Collections.synchronizedSet(new HashSet(...));

                                -
                                -

                                代码:

                                public class HashSet<E>
                                extends AbstractSet<E>
                                implements Set<E>, Cloneable, java.io.Serializable
                                {
                                static final long serialVersionUID = -5024744406713321676L;

                                //通过hashmap实现
                                //map不可序列化
                                private transient HashMap<E,Object> map;

                                // Dummy value to associate with an Object in the backing Map
                                // 1
                                private static final Object PRESENT = new Object();

                                //Constructs a new, empty set;
                                //the backing HashMap instance has default initial capacity (16)
                                //and load factor (0.75).
                                public HashSet() {
                                map = new HashMap<>();
                                }

                                //The HashMap is created with default load factor (0.75)
                                //and an initial capacity sufficient to contain the elements in c
                                public HashSet(Collection<? extends E> c) {
                                //看来这个load factor=size/0.75
                                map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
                                addAll(c);
                                }

                                public HashSet(int initialCapacity, float loadFactor) {
                                map = new HashMap<>(initialCapacity, loadFactor);
                                }

                                public HashSet(int initialCapacity) {
                                map = new HashMap<>(initialCapacity);
                                }

                                //This package private constructor is only used by LinkedHashSet.
                                //@param: dummy – ignored (distinguishes this constructor from other constructor.)
                                HashSet(int initialCapacity, float loadFactor, boolean dummy) {
                                //此处为LinkeHashMap
                                map = new LinkedHashMap<>(initialCapacity, loadFactor);
                                }

                                public Iterator<E> iterator() {return map.keySet().iterator();}

                                public int size() {return map.size();}

                                public boolean isEmpty() {return map.isEmpty();}

                                public boolean contains(Object o) {return map.containsKey(o);}

                                //@return true if this set did not already contain the specified element
                                //map.put返回已有的oldValue,返回空表示没有oldValue,插入成功;否则失败
                                public boolean add(E e) {
                                return map.put(e, PRESENT)==null;
                                }

                                public boolean remove(Object o) {
                                return map.remove(o)==PRESENT;
                                }

                                public void clear() {
                                map.clear();
                                }

                                @SuppressWarnings("unchecked")
                                public Object clone() {
                                try {
                                HashSet<E> newSet = (HashSet<E>) super.clone();
                                newSet.map = (HashMap<E, Object>) map.clone();
                                return newSet;
                                } catch (CloneNotSupportedException e) {
                                throw new InternalError(e);
                                }
                                }

                                private void writeObject(java.io.ObjectOutputStream s)
                                throws java.io.IOException {...}

                                private void readObject(java.io.ObjectInputStream s)
                                throws java.io.IOException, ClassNotFoundException {...}

                                public Spliterator<E> spliterator() {
                                return new HashMap.KeySpliterator<E,Object>(map, 0, -1, 0, 0);
                                }
                                }
                                +

                                再指定mini_crt_entry为入口进行静态链接:

                                +
                                $ ld -m elf_i386 -static -e mini_crt_entry entry.o test.o minicrt.a -o test
                              2. +
                              3. C++

                                +

                                如果要实现对C++的支持,除了在上述基础上,我们还需增加以下几个内容:全局对象(cout)构造/析构的实现、new/delete、类的实现(string和iostream)。具体来说,会支持下面这个简单的代码:

                                +
                                #include "iostream"
                                #include "string"
                                using namespace std;

                                int main(int argc, char* argv[])
                                {
                                string* msg = new string("Hello World");
                                cout << *msg << endl;
                                delete msg;
                                return 0;
                                }
                                -

                                其中:

                                  -
                                1. PRESENT

                                  正如它的解释:

                                  -
                                  -

                                  Dummy value to associate with an Object in the backing Map

                                  -
                                  -

                                  set 以map作为内部支持,其实主要用的是map对于key的高效去重。也就是说,set其实只需要用map的key这一半就好了。所以我们另一半value就都统一用一个new Object【也就是PRESENT】来统一就行。

                                  -

                                  不得不说这点很聪明,值得学习。

                                  -
                                  private static final Object PRESENT = new Object();
                                  public boolean add(E e) {
                                  return map.put(e, PRESENT)==null;
                                  }
                                  public boolean remove(Object o) {
                                  return map.remove(o)==PRESENT;
                                  }
                                2. -
                                -

                                SortedSet(I)

                                -

                                A Set that further provides a total ordering on its elements.

                                -

                                The set’s iterator will traverse the set in ascending element order.

                                -

                                All elements inserted into a sorted set must implement the Comparable interface (or be accepted by the specified comparator).否则导致ClassCastException

                                -

                                Note that the ordering maintained by a sorted set (whether or not an explicit comparator is provided) must be consistent with equals if the sorted set is to correctly implement the Set interface.

                                -

                                【consistent with equals:if and only if c.compare(e1, e2)==0 has the same boolean value as e1.equals(e2) for every e1 and e2 in S.】

                                -

                                //2

                                -

                                Note: several methods return subsets with restricted ranges. 区间是前闭后开的。

                                -

                                If you need a closed range , and the element type allows for calculation of the successor of a given value, merely request the subrange from lowEndpoint to successor(highEndpoint).

                                -

                                For example, suppose that s is a sorted set of strings. [low,hight]
                                SortedSet<String> sub = s.subSet(low, high+"\0");
                                A similar technique can be used to generate an open range (low,hight)
                                SortedSet<String> sub = s.subSet(low+"\0", high);

                                -

                                66666

                                -
                                -

                                代码

                                public interface SortedSet<E> extends Set<E> {
                                //null if this set uses the natural ordering
                                Comparator<? super E> comparator();
                                //1
                                SortedSet<E> subSet(E fromElement, E toElement);

                                SortedSet<E> headSet(E toElement);

                                SortedSet<E> tailSet(E fromElement);

                                E first();

                                E last();

                                @Override
                                default Spliterator<E> spliterator() {
                                return new Spliterators.IteratorSpliterator<E>(
                                this, Spliterator.DISTINCT | Spliterator.SORTED | Spliterator.ORDERED) {
                                @Override
                                public Comparator<? super E> getComparator() {
                                return SortedSet.this.comparator();
                                }
                                };
                                }
                                }
                                +

                                我们可以分步实现这些功能:

                                +
                                  +
                                1. new/delete实现

                                  +

                                  简单地使用运算符重载功能即可:

                                  +
                                  void* operator new(unsigned int size);
                                  void operator delete(void* p);
                                2. +
                                3. 类的实现

                                  +

                                  不多说

                                  +
                                4. +
                                5. 全局对象的构造/析构

                                  +
                                    +
                                  1. 构造

                                    +

                                    全局对象的构造在entry中进行:

                                    +
                                    void mini_crt_entry(void)
                                    {
                                    ...
                                    // 构造所有全局对象
                                    do_global_ctors();
                                    ret = main(argc,argv);
                                    }
                                    -

                                    其中:

                                      -
                                    1. subset

                                      只有sorted set才有subset,想想也确实

                                      -
                                      /*
                                      Throws:
                                      ClassCastException – if fromElement and toElement cannot be compared to one another using this set's comparator
                                      NullPointerException – if fromElement or toElement is null and this set does not permit null elements
                                      IllegalArgumentException – if fromElement is greater than toElement; or fromElement or toElement lies outside the bounds of the restricted range of the set
                                      */
                                      //The returned set will throw an IllegalArgumentException
                                      //on an attempt to insert an element outside its range.
                                      SortedSet<E> subSet(E fromElement, E toElement);
                                      +

                                      前文说过,在Linux中,每个.o文件的全局构造最后都会放在.ctor段。ld在链接阶段中将所有目标文件(包括用于标识.ctor段开始和结束的crtbegin.ocrtend.o)的.ctor段连在一起。所以,我们就需要实现三个文件:

                                      +
                                        +
                                      1. ctors.c

                                        +

                                        主要是用于实现do_global_ctors()。既然都有.ctor段存在了,那么它的实现就很简单,就是遍历.ctor段的所有函数指针并且调用它们。

                                        +
                                        void run_hooks();
                                        extern "C" void do_global_ctors()
                                        {
                                        run_hooks();
                                        }
                                        -

                                        以及注意此处是Element,不是Index

                                        +
                                        void run_hooks()
                                        {
                                        const ctor_func *list = ctors_begin;
                                        // 逐个调用ctors段里的东西
                                        while ((int)*++list != -1) (**list)();
                                        }
                                      2. +
                                      3. crtbegin.c

                                        +

                                        前文说到,按规定,ld将会以如下顺序连接.o文件:

                                        +
                                        ld crtbegin.o 其他文件 crtend.o -o test
                                        + +

                                        因而,crtbegin.c.ctor段会被链接在第一个。其作用是标识.ctor函数指针的数量,将在链接时由ld计算并且填写。因而在这里,我们只需将其初始化为一个特殊值(-1)就行:

                                        +
                                        typedef void (*ctor_func)(void);

                                        ctor_func ctors_begin[1] __attribute__((section(".ctors"))) = {
                                        (ctor_func)-1
                                        };
                                      4. +
                                      5. crtend.c

                                        +

                                        同样,crtend.c.ctor段标识着.ctor段的结束。因而我们也将其初始化为一个特殊值(-1):

                                        +
                                        typedef void (*ctor_func)(void);

                                        // 转化-1为函数指针,标识结束
                                        ctor_func crt_end[1] __attribute__((section(".ctors"))) = {
                                        (ctor_func) - 1
                                        };
                                      6. +
                                    2. -
                                    3. subSet(low, high+”\0”);

                                      为什么加个”\0”就可以,具体可以看看这个:

                                      -

                                      Adding “\0” to a subset range end

                                      -

                                      原因就是sub的这个range取的是在此区间的元素,low和high这两个param不一定要包含在这个set里面。因此,按照set的排序,high+”\0”比high大,因而high就落入此区间,也就可以被包含在range中了。

                                      +
                                    4. 析构

                                      +

                                      全局对象的析构同样在entry中进行:

                                      +
                                      void mini_crt_entry(void)
                                      {
                                      ...
                                      ret = main(argc,argv);
                                      exit(ret);
                                      }

                                      void exit(int exitCode)
                                      {
                                      // 执行atexit,完成所有finit钩子
                                      mini_crt_call_exit_routine();
                                      // 调用exit系统调用
                                      asm( "movl %0,%%ebx \n\t"
                                      "movl $1,%%eax \n\t"
                                      "int $0x80 \n\t"
                                      "hlt \n\t"::"m"(exitCode));
                                      }
                                      + +

                                      具体也是以链表形式管理所有的函数指针,在atexit中注册(加入链表),在mini_crt_call_exit_routine中真正调用,不多分析。

                                    -
                                    -

                                    比起sorted set,navigable set最特殊的点在于它提供了对某一元素附近元素的导航。

                                    -

                                    Method usage

                                    -

                                    lower less than最大的,比所给ele小的元素

                                    -

                                    floor less than or equal最大的,比所给ele小或者等于的元素

                                    -

                                    ceiling greater than or equal最小的,比所给ele大或者等于的元素

                                    -

                                    higher greater than最小的,比所给ele大的元素

                                    -

                                    The descendingSet method returns a view of the set with the senses of all relational and directional methods inverted.

                                    -

                                    This interface additionally defines methods pollFirst and pollLast that return and remove the lowest and highest element, if one exists, else returning null. 有点堆的感觉

                                    -

                                    Methods subSet, headSet, and tailSet differ from the like-named SortedSet methods in accepting additional arguments describing whether lower and upper bounds are inclusive versus exclusive.

                                    -
                                    -

                                    代码:

                                    public interface NavigableSet<E> extends SortedSet<E> {
                                    //Returns the greatest element in this set strictly less than the given element
                                    E lower(E e);

                                    E floor(E e);

                                    E ceiling(E e);

                                    E higher(E e);

                                    //Removes the first (lowest) element, or returns null if this set is empty.
                                    E pollFirst();

                                    E pollLast();

                                    //in ascending order
                                    Iterator<E> iterator();

                                    /*
                                    The returned set has an ordering equivalent to
                                    Collections.reverseOrder(comparator()).
                                    The expression s.descendingSet().descendingSet()
                                    returns a view of s essentially equivalent(基本等价) to s
                                    */
                                    NavigableSet<E> descendingSet();

                                    //Equivalent in effect to descendingSet().iterator()
                                    Iterator<E> descendingIterator();

                                    NavigableSet<E> subSet(E fromElement, boolean fromInclusive,
                                    E toElement, boolean toInclusive);

                                    NavigableSet<E> headSet(E toElement, boolean inclusive);

                                    NavigableSet<E> tailSet(E fromElement, boolean inclusive);

                                    SortedSet<E> subSet(E fromElement, E toElement);

                                    SortedSet<E> headSet(E toElement);

                                    SortedSet<E> tailSet(E fromElement);
                                    }
                                    - -

                                    TreeSet

                                    -

                                    This implementation provides guaranteed log(n) time cost for the basic operations (add, remove and contains).

                                    -

                                    Note that this implementation is not synchronized.

                                    -

                                    SortedSet s = Collections.synchronizedSortedSet(new TreeSet(...));

                                    +
                                  2. +
                                  +
                                6. +
                                +

                                特辑:开发中遇到的链接小问题

                                  +
                                1. 已经在LD_LIBRARY_PATH中加入某个静态库的路径,但是仍然报错error while loading shared libraries: libssl.so.1.1: cannot open shared object file: No such file or directory

                                  +

                                  解决方法:执行sudo ldconfig

                                  +
                                  +

                                  ldconfig命令的作用就是将这些共享库的路径添加到动态链接器的缓存中,以便系统能够找到它们。

                                  +

                                  具体而言,ldconfig会检查默认的共享库路径(通常是/lib/usr/lib),以及在/etc/ld.so.conf/etc/ld.so.conf.d/目录中定义的其他路径。然后,它会更新动态链接器缓存,这样系统就知道在哪里查找共享库。

                                  +

                                  Q:也就是说ld不是像gcc找头文件,是根据-I选项规定的路径即时查找的,而是只根据缓存吗?所以尽管我们通过ld path环境变量设置了新查找路径,我们还是得手动刷新下ld缓存。

                                  +

                                  A:是的,你理解得很对。ldconfig主要是更新系统中的共享库缓存,而不是在每次程序运行时实时查找库文件的位置。动态链接器在运行时会根据缓存中的信息找到所需的共享库,以提高性能并避免在每次程序启动时重新搜索所有库。

                                  +

                                  当你通过LD_LIBRARY_PATH环境变量设置新的查找路径时,这只是告诉动态链接器在运行时应该额外搜索这些路径。然而,为了确保系统广泛地认识这些新的路径,以及其中的共享库,你需要运行ldconfig来更新缓存。

                                  -

                                  代码:

                                  public class TreeSet<E> extends AbstractSet<E>
                                  implements NavigableSet<E>, Cloneable, java.io.Serializable
                                  {

                                  //依然用了个map
                                  private transient NavigableMap<E,Object> m;

                                  // Dummy value to associate with an Object in the backing Map
                                  private static final Object PRESENT = new Object();

                                  TreeSet(NavigableMap<E,Object> m) {
                                  this.m = m;
                                  }

                                  public TreeSet() {
                                  this(new TreeMap<E,Object>());
                                  }

                                  public TreeSet(Comparator<? super E> comparator) {
                                  this(new TreeMap<>(comparator));
                                  }

                                  public TreeSet(Collection<? extends E> c) {
                                  this();
                                  addAll(c);
                                  }

                                  public TreeSet(SortedSet<E> s) {
                                  this(s.comparator());
                                  addAll(s);
                                  }

                                  public Iterator<E> iterator() {
                                  return m.navigableKeySet().iterator();
                                  }

                                  public Iterator<E> descendingIterator() {
                                  return m.descendingKeySet().iterator();
                                  }

                                  //确实直接让map倒序就可以了
                                  public NavigableSet<E> descendingSet() {
                                  return new TreeSet<>(m.descendingMap());
                                  }

                                  public int size() {
                                  return m.size();
                                  }

                                  public boolean isEmpty() {
                                  return m.isEmpty();
                                  }

                                  public boolean contains(Object o) {
                                  return m.containsKey(o);
                                  }

                                  public boolean add(E e) {
                                  return m.put(e, PRESENT)==null;
                                  }

                                  public boolean remove(Object o) {
                                  return m.remove(o)==PRESENT;
                                  }

                                  public void clear() {
                                  m.clear();
                                  }

                                  public boolean addAll(Collection<? extends E> c) {
                                  // Use linear-time version if applicable
                                  if (m.size()==0 && c.size() > 0 &&
                                  c instanceof SortedSet &&
                                  m instanceof TreeMap) {
                                  SortedSet<? extends E> set = (SortedSet<? extends E>) c;
                                  TreeMap<E,Object> map = (TreeMap<E, Object>) m;
                                  Comparator<?> cc = set.comparator();
                                  Comparator<? super E> mc = map.comparator();
                                  if (cc==mc || (cc != null && cc.equals(mc))) {
                                  map.addAllForTreeSet(set, PRESENT);
                                  return true;
                                  }
                                  }
                                  return super.addAll(c);
                                  }

                                  public NavigableSet<E> subSet(E fromElement, boolean fromInclusive,
                                  E toElement, boolean toInclusive) {
                                  return new TreeSet<>(m.subMap(fromElement, fromInclusive,
                                  toElement, toInclusive));
                                  }

                                  public NavigableSet<E> headSet(E toElement, boolean inclusive) {
                                  return new TreeSet<>(m.headMap(toElement, inclusive));
                                  }

                                  public NavigableSet<E> tailSet(E fromElement, boolean inclusive) {
                                  return new TreeSet<>(m.tailMap(fromElement, inclusive));
                                  }

                                  public SortedSet<E> subSet(E fromElement, E toElement) {
                                  return subSet(fromElement, true, toElement, false);
                                  }

                                  public SortedSet<E> headSet(E toElement) {
                                  return headSet(toElement, false);
                                  }

                                  //注意此处为true。严格遵循左闭右开
                                  public SortedSet<E> tailSet(E fromElement) {
                                  return tailSet(fromElement, true);
                                  }

                                  public Comparator<? super E> comparator() {
                                  return m.comparator();
                                  }

                                  public E first() {
                                  return m.firstKey();
                                  }

                                  public E last() {
                                  return m.lastKey();
                                  }

                                  // NavigableSet API methods

                                  public E lower(E e) {
                                  return m.lowerKey(e);
                                  }

                                  public E floor(E e) {
                                  return m.floorKey(e);
                                  }

                                  public E ceiling(E e) {
                                  return m.ceilingKey(e);
                                  }

                                  public E higher(E e) {
                                  return m.higherKey(e);
                                  }

                                  public E pollFirst() {
                                  Map.Entry<E,?> e = m.pollFirstEntry();
                                  return (e == null) ? null : e.getKey();
                                  }

                                  public E pollLast() {
                                  Map.Entry<E,?> e = m.pollLastEntry();
                                  return (e == null) ? null : e.getKey();
                                  }

                                  @SuppressWarnings("unchecked")
                                  public Object clone() {
                                  TreeSet<E> clone;
                                  try {
                                  clone = (TreeSet<E>) super.clone();
                                  } catch (CloneNotSupportedException e) {
                                  throw new InternalError(e);
                                  }

                                  clone.m = new TreeMap<>(m);
                                  return clone;
                                  }

                                  private void writeObject(java.io.ObjectOutputStream s)
                                  throws java.io.IOException {...}

                                  private void readObject(java.io.ObjectInputStream s)
                                  throws java.io.IOException, ClassNotFoundException {...}

                                  public Spliterator<E> spliterator() {
                                  return TreeMap.keySpliteratorFor(m);
                                  }

                                  private static final long serialVersionUID = -2479143000061671589L;
                                  }
                                  - +
                                2. +
                                3. +
                                ]]> - Java + books diff --git a/tag/index.html b/tag/index.html index ec67503d..858a0463 100644 --- a/tag/index.html +++ b/tag/index.html @@ -145,11 +145,11 @@

                                - Tag Cloud -

                                os竞赛(2) - labs(4) + Java(3) books(5) - Java(3) + labs(4) intern(2)