From 47ccb263420244a1a4e65be7870150d9a7bc81a7 Mon Sep 17 00:00:00 2001 From: username Date: Thu, 7 Dec 2023 15:01:55 +0800 Subject: [PATCH] Site updated: 2023-12-07 15:01:49 --- .../index.html" | 2 +- .../index.html" | 2 +- .../index.html" | 2 +- .../index.html" | 2 +- 2022/12/21/JavaWeb/index.html | 2 +- 2023/01/10/xv6$chap1/index.html | 2 +- 2023/01/10/xv6$chap2/index.html | 2 +- 2023/01/10/xv6$chap3/index.html | 2 +- 2023/01/10/xv6$chap4/index.html | 2 +- 2023/01/10/xv6$chap5/index.html | 2 +- 2023/01/10/xv6$chap6/index.html | 2 +- 2023/01/10/xv6$chap7/index.html | 2 +- 2023/01/10/xv6$chap8/index.html | 4 +- 2023/01/10/xv6$chap9/index.html | 4 +- 2023/01/10/xv6/index.html | 2 +- 2023/02/25/cs144$else/index.html | 2 +- 2023/02/25/cs144$lab0/index.html | 2 +- 2023/02/25/cs144$lab1/index.html | 2 +- 2023/02/25/cs144$lab2/index.html | 2 +- 2023/02/25/cs144$lab3/index.html | 2 +- 2023/02/25/cs144$lab4/index.html | 2 +- 2023/02/25/cs144$lab5/index.html | 2 +- 2023/02/25/cs144$lab6/index.html | 2 +- 2023/02/25/cs144/index.html | 2 +- .../index.html" | 2 +- 2023/03/13/cmu15445$lab0/index.html | 2 +- 2023/03/13/cmu15445$lab1/index.html | 4 +- .../cmu15445$lab2/image-20231203165014734.png | Bin 0 -> 170340 bytes .../cmu15445$lab2/image-20231203181652821.png | Bin 0 -> 22913 bytes .../cmu15445$lab2/image-20231204001136531.png | Bin 0 -> 64385 bytes .../cmu15445$lab2/image-20231204161729128.png | Bin 0 -> 53676 bytes .../cmu15445$lab2/image-20231204163052528.png | Bin 0 -> 47515 bytes .../cmu15445$lab2/image-20231204170030245.png | Bin 0 -> 16733 bytes 2023/03/13/cmu15445$lab2/index.html | 34 +- 2023/03/13/cmu15445$lab2/out.png | Bin 0 -> 137862 bytes 2023/03/13/cmu15445/index.html | 2 +- .../index.html" | 2 +- 2023/06/21/comporgan/index.html | 2 +- .../index.html" | 2 +- 2023/08/12/kernel_compile/index.html | 211 + 2023/08/27/2023-os-comp/index.html | 2 +- .../index.html" | 2 +- .../index.html" | 2 +- .../index.html" | 2 +- .../index.html" | 2 +- 2023/10/07/git/index.html | 2 +- .../index.html" | 2 +- .../index.html" | 13 +- 2023/10/19/open-source-9.19-10.19/index.html | 2 +- 2023/10/27/driver_develop/index.html | 2 +- 2023/11/18/compilation_principle/index.html | 2 +- 2023/11/26/cryptography/index.html | 2 +- 2023/11/26/database/index.html | 2 +- about/index.html | 39 +- archives/2022/10/index.html | 2 +- archives/2022/11/index.html | 2 +- archives/2022/12/index.html | 2 +- archives/2022/index.html | 2 +- archives/2023/01/index.html | 2 +- archives/2023/02/index.html | 2 +- archives/2023/03/index.html | 2 +- archives/2023/06/index.html | 2 +- archives/2023/08/index.html | 2 +- archives/2023/09/index.html | 2 +- archives/2023/10/index.html | 2 +- archives/2023/11/index.html | 2 +- archives/2023/index.html | 2 +- archives/2023/page/2/index.html | 2 +- archives/2023/page/3/index.html | 2 +- archives/2023/page/4/index.html | 2 +- archives/2023/page/5/index.html | 34 + archives/index.html | 2 +- archives/page/2/index.html | 2 +- archives/page/3/index.html | 2 +- archives/page/4/index.html | 2 +- archives/page/5/index.html | 2 +- index.html | 2 +- search.xml | 16213 ++++++++-------- tags/Java/index.html | 2 +- tags/books/index.html | 2 +- tags/intern/index.html | 2 +- tags/labs/index.html | 2 +- tags/mylife/index.html | 2 +- "tags/os\347\253\236\350\265\233/index.html" | 2 +- 84 files changed, 8569 insertions(+), 8123 deletions(-) create mode 100644 2023/03/13/cmu15445$lab2/image-20231203165014734.png create mode 100644 2023/03/13/cmu15445$lab2/image-20231203181652821.png create mode 100644 2023/03/13/cmu15445$lab2/image-20231204001136531.png create mode 100644 2023/03/13/cmu15445$lab2/image-20231204161729128.png create mode 100644 2023/03/13/cmu15445$lab2/image-20231204163052528.png create mode 100644 2023/03/13/cmu15445$lab2/image-20231204170030245.png create mode 100644 2023/03/13/cmu15445$lab2/out.png create mode 100644 2023/08/12/kernel_compile/index.html create mode 100644 archives/2023/page/5/index.html diff --git "a/2022/10/04/\345\223\210\345\267\245\345\244\247\346\223\215\344\275\234\347\263\273\347\273\237\345\256\236\351\252\214/index.html" "b/2022/10/04/\345\223\210\345\267\245\345\244\247\346\223\215\344\275\234\347\263\273\347\273\237\345\256\236\351\252\214/index.html" index 35c74ab8..0e474d5b 100644 --- "a/2022/10/04/\345\223\210\345\267\245\345\244\247\346\223\215\344\275\234\347\263\273\347\273\237\345\256\236\351\252\214/index.html" +++ "b/2022/10/04/\345\223\210\345\267\245\345\244\247\346\223\215\344\275\234\347\263\273\347\273\237\345\256\236\351\252\214/index.html" @@ -45,7 +45,7 @@ // Wait for 1 second before switching API hosts rotate: 1000, }, -};

哈工大操作系统实验

+};

哈工大操作系统实验

实验入口
主要参考文章
lseek()函数:用于移动打开文件的指针
linux系统调用之write源码解析(基于linux0.11)
get_fs_bytes解析
VIM与系统剪贴板的复制粘贴
操作系统实验六 信号量的实现和应用(哈工大李治军)
哈工大操作系统实验6 信号量的实现 pc.c 编译时报错 对‘sem_open‘未定义的引用
Linux 文件编程 open函数
哈工大-操作系统-HitOSlab-李治军-实验5-信号量的实现和应用

地址映射与共享

diff --git "a/2022/10/16/\351\230\205\350\257\273JDK\345\256\271\345\231\250\351\203\250\345\210\206\346\272\220\347\240\201\347\232\204\345\277\203\345\276\227\344\275\223\344\274\2321\343\200\220Collection\351\203\250\345\210\206\343\200\221/index.html" "b/2022/10/16/\351\230\205\350\257\273JDK\345\256\271\345\231\250\351\203\250\345\210\206\346\272\220\347\240\201\347\232\204\345\277\203\345\276\227\344\275\223\344\274\2321\343\200\220Collection\351\203\250\345\210\206\343\200\221/index.html" index 6a466f7e..b4df034b 100644 --- "a/2022/10/16/\351\230\205\350\257\273JDK\345\256\271\345\231\250\351\203\250\345\210\206\346\272\220\347\240\201\347\232\204\345\277\203\345\276\227\344\275\223\344\274\2321\343\200\220Collection\351\203\250\345\210\206\343\200\221/index.html" +++ "b/2022/10/16/\351\230\205\350\257\273JDK\345\256\271\345\231\250\351\203\250\345\210\206\346\272\220\347\240\201\347\232\204\345\277\203\345\276\227\344\275\223\344\274\2321\343\200\220Collection\351\203\250\345\210\206\343\200\221/index.html" @@ -48,7 +48,7 @@ // Wait for 1 second before switching API hosts rotate: 1000, }, -};

阅读JDK容器部分源码的心得体会1【Collection部分】

+};

阅读JDK容器部分源码的心得体会1【Collection部分】

idea 替换注释正则表达式/\*{1,2}[\s\S]*?\*/

typora 替换图片asset

\!\[.*\]\(D:\\aWorkStorage\\hexo\\blog\\source\\_posts\\阅读JDK容器部分源码的心得体会1【Collection部分】\\(.*)\.png\)

diff --git "a/2022/10/22/\351\230\205\350\257\273JDK\345\256\271\345\231\250\351\203\250\345\210\206\346\272\220\347\240\201\347\232\204\345\277\203\345\276\227\344\275\223\344\274\2322\343\200\220Map\351\203\250\345\210\206\343\200\221/index.html" "b/2022/10/22/\351\230\205\350\257\273JDK\345\256\271\345\231\250\351\203\250\345\210\206\346\272\220\347\240\201\347\232\204\345\277\203\345\276\227\344\275\223\344\274\2322\343\200\220Map\351\203\250\345\210\206\343\200\221/index.html" index 0127707b..5144a550 100644 --- "a/2022/10/22/\351\230\205\350\257\273JDK\345\256\271\345\231\250\351\203\250\345\210\206\346\272\220\347\240\201\347\232\204\345\277\203\345\276\227\344\275\223\344\274\2322\343\200\220Map\351\203\250\345\210\206\343\200\221/index.html" +++ "b/2022/10/22/\351\230\205\350\257\273JDK\345\256\271\345\231\250\351\203\250\345\210\206\346\272\220\347\240\201\347\232\204\345\277\203\345\276\227\344\275\223\344\274\2322\343\200\220Map\351\203\250\345\210\206\343\200\221/index.html" @@ -43,7 +43,7 @@ // Wait for 1 second before switching API hosts rotate: 1000, }, -};

阅读JDK容器部分源码的心得体会2【Map部分】

+};

阅读JDK容器部分源码的心得体会2【Map部分】

idea 替换注释正则表达式/\*{1,2}[\s\S]*?\*/

typora 替换图片asset

\!\[.*\]\(D:\\aWorkStorage\\hexo\\blog\\source\\_posts\\阅读JDK容器部分源码的心得体会2【Map部分】\\(.*)\.png\)

diff --git "a/2022/11/06/Java\345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230/index.html" "b/2022/11/06/Java\345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230/index.html" index 602deacf..ce3cd971 100644 --- "a/2022/11/06/Java\345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230/index.html" +++ "b/2022/11/06/Java\345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230/index.html" @@ -382,7 +382,7 @@ // Wait for 1 second before switching API hosts rotate: 1000, }, -};

Java并发编程实战

+};

Java并发编程实战

idea 替换注释正则表达式/\*{1,2}[\s\S]*?\*/

第一章 简介

线程的作用

diff --git a/2022/12/21/JavaWeb/index.html b/2022/12/21/JavaWeb/index.html index 9118fb07..0cbab8c7 100644 --- a/2022/12/21/JavaWeb/index.html +++ b/2022/12/21/JavaWeb/index.html @@ -152,7 +152,7 @@ // Wait for 1 second before switching API hosts rotate: 1000, }, -};

JavaWeb

第一部分 Java基础

JUnit单元测试

JUnit是白盒测试。

+};

JavaWeb

第一部分 Java基础

JUnit单元测试

JUnit是白盒测试。

简要使用步骤

定义测试类

包含各种测试用例。

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

定义测试方法

测试方法可以独立运行。

diff --git a/2023/01/10/xv6$chap1/index.html b/2023/01/10/xv6$chap1/index.html index 36f8ae3a..f3991270 100644 --- a/2023/01/10/xv6$chap1/index.html +++ b/2023/01/10/xv6$chap1/index.html @@ -48,7 +48,7 @@ // Wait for 1 second before switching API hosts rotate: 1000, }, -};

Operating system interface

Operating system interface

本节大概是在讲操作系统的接口,系统调用占了很大一部分。

+};

Operating system interface

Operating system interface

本节大概是在讲操作系统的接口,系统调用占了很大一部分。

diff --git a/2023/01/10/xv6$chap2/index.html b/2023/01/10/xv6$chap2/index.html index 7f2bb7eb..1f5a3265 100644 --- a/2023/01/10/xv6$chap2/index.html +++ b/2023/01/10/xv6$chap2/index.html @@ -43,7 +43,7 @@ // Wait for 1 second before switching API hosts rotate: 1000, }, -};

Operating system oganization

Operating system oganization

+};

Operating system oganization

Operating system oganization

Before you start coding, read Chapter 2 of the xv6 book, and Sections 4.3 and 4.4 of Chapter 4, and related source files:

1
2
3
4
5
6
7
8
9
10
11
12
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

1
2
3
4
5
6
7
8
9
// in uvmunmap()
if((*pte & PTE_V) == 0){
*pte = 0;
continue;
}
// in uvmcopy()
if((*pte & PTE_V) == 0)
//panic("uvmcopy: page not present");
continue;
-
I'm so cute. Please give me money.
Hexo 驱动 v5.4.2|主题 - Yun v1.10.11
\ No newline at end of file +
I'm so cute. Please give me money.
Hexo 驱动 v5.4.2|主题 - Yun v1.10.11
\ No newline at end of file diff --git a/2023/01/10/xv6$chap9/index.html b/2023/01/10/xv6$chap9/index.html index 50ae6316..56381d7c 100644 --- a/2023/01/10/xv6$chap9/index.html +++ b/2023/01/10/xv6$chap9/index.html @@ -52,7 +52,7 @@ // Wait for 1 second before switching API hosts rotate: 1000, }, -};

其他的对实验未涉及的思考

其他的对实验未涉及的思考

由mkfs引发的对虚拟机的学习

+};

其他的对实验未涉及的思考

其他的对实验未涉及的思考

由mkfs引发的对虚拟机的学习

懂了!VMware/KVM/Docker原来是这么回事儿这篇文章对虚拟化、虚拟机技术讲解很到位,写得通俗易懂,非常值得一看

KVM 的「基于内核的虚拟机」是什么意思?这篇文章对QEMU-KVM架构进行了详细的介绍。还有这篇文章对应的知乎问题下面的高赞回答有机会也可以去看看。

QEMU/KVM原理概述这篇文章前面的原理和上面那个差不多,后面有使用kvm做一个精简内核的实例,有兴趣/有精力/有需要可以看看。

@@ -328,4 +328,4 @@

I'm so cute. Please give me money.

Hexo 驱动 v5.4.2|主题 - Yun v1.10.11
\ No newline at end of file +
I'm so cute. Please give me money.
Hexo 驱动 v5.4.2|主题 - Yun v1.10.11
\ No newline at end of file diff --git a/2023/01/10/xv6/index.html b/2023/01/10/xv6/index.html index ebd92eed..2fba6779 100644 --- a/2023/01/10/xv6/index.html +++ b/2023/01/10/xv6/index.html @@ -38,7 +38,7 @@ // Wait for 1 second before switching API hosts rotate: 1000, }, -};

xv6

+};

xv6

总耗时:120h 约27天

部分地方的翻译和表格来源参考:xv6指导书翻译

部分文本来自:操作系统实验指导书 - 2023秋季 | 哈工大(深圳)

diff --git a/2023/02/25/cs144$else/index.html b/2023/02/25/cs144$else/index.html index 1cfb13db..a95e710a 100644 --- a/2023/02/25/cs144$else/index.html +++ b/2023/02/25/cs144$else/index.html @@ -40,7 +40,7 @@ // Wait for 1 second before switching API hosts rotate: 1000, }, -};

其他的对实验未涉及的思考

其他的对实验未涉及的思考

网络层实现

在我们的协议栈实现中,我们负责了运输层的TCP协议、网络层的ARP协议以及数据链路层的ETH协议的编写,剩下的网络层的IP协议则由官方给定。接下来我们就来探究下网络层的实现。

+};

其他的对实验未涉及的思考

其他的对实验未涉及的思考

网络层实现

在我们的协议栈实现中,我们负责了运输层的TCP协议、网络层的ARP协议以及数据链路层的ETH协议的编写,剩下的网络层的IP协议则由官方给定。接下来我们就来探究下网络层的实现。

总体架构

You’ve done this already.

In Lab 4, we gave:

diff --git a/2023/02/25/cs144$lab0/index.html b/2023/02/25/cs144$lab0/index.html index 95ca1316..47f04d8b 100644 --- a/2023/02/25/cs144$lab0/index.html +++ b/2023/02/25/cs144$lab0/index.html @@ -44,7 +44,7 @@ // Wait for 1 second before switching API hosts rotate: 1000, }, -};

Lab0

Lab0

+};

Lab0

Lab0

本次实验一直在强调的一点就是,TCP的功能是将底层的零散数据包,拼接成一个reliable in-order的byte stream。这个对我来说非常“振聋发聩”(夸张了233),以前只是背诵地知道TCP的可靠性,这次我算是第一次知道了所谓“可靠”究竟可靠在哪:一是保证了序列有序性,二是保证了数据不丢失(从软件层面)。

还有一个就是大致了解了cs144的主题:实现TCP协议。也就是说,运输层下面的那些层是不用管的吗?不过这样也挺恰好,我正好在学校的实验做过对下面这些层的实现了,就差一个TCP23333这样一来,我的协议栈就可以完整了。

diff --git a/2023/02/25/cs144$lab1/index.html b/2023/02/25/cs144$lab1/index.html index d1f204a0..d1fb2e29 100644 --- a/2023/02/25/cs144$lab1/index.html +++ b/2023/02/25/cs144$lab1/index.html @@ -39,7 +39,7 @@ // Wait for 1 second before switching API hosts rotate: 1000, }, -};

Lab1 StreamReassembler

Lab1 StreamReassembler

+};

Lab1 StreamReassembler

Lab1 StreamReassembler

TCP managed to produce a pair of reliable in-order byte streams (one from you to the server, and one in the opposite direction), even though the underlying network only delivers “best-effort” datagrams.

You also implemented the byte-stream abstraction yourself, in memory within one computer.

Over the next four weeks, you’ll implement TCP, to provide the byte-stream abstraction between a pair of computers separated by an unreliable datagram network.

diff --git a/2023/02/25/cs144$lab2/index.html b/2023/02/25/cs144$lab2/index.html index f6c8463d..b9764f50 100644 --- a/2023/02/25/cs144$lab2/index.html +++ b/2023/02/25/cs144$lab2/index.html @@ -48,7 +48,7 @@ // Wait for 1 second before switching API hosts rotate: 1000, }, -};

Lab2 TCPReceiver

Lab2 TCPReceiver

前置学习

Overview

承上启下

在前两个实验中,我们可以说只是做了点算法上的抽象工作,跟TCP协议还是没什么显著的关系的。但来到了本次实验,一切就都不一样了。

+};

Lab2 TCPReceiver

Lab2 TCPReceiver

前置学习

Overview

承上启下

在前两个实验中,我们可以说只是做了点算法上的抽象工作,跟TCP协议还是没什么显著的关系的。但来到了本次实验,一切就都不一样了。

image-20230226194708398

依然还是这张图。相信此时做过前两个实验之后,看到这张图就会有了不一样的发现。

我们对TCP协议的实现是由内向外的,先实现里面再实现最外层。前两节实验,我们由内而外实现了ByteStreamStreamReassembler;在这次实验中,我们会实现更外层一点的TCPReceiver

diff --git a/2023/02/25/cs144$lab3/index.html b/2023/02/25/cs144$lab3/index.html index f25cbf70..40f17bd2 100644 --- a/2023/02/25/cs144$lab3/index.html +++ b/2023/02/25/cs144$lab3/index.html @@ -38,7 +38,7 @@ // Wait for 1 second before switching API hosts rotate: 1000, }, -};

Lab3 TCPSender

Lab3 TCPSender

前置知识

在TCP协议中,TCPSender负责对ack进行处理,将字节流封装为TCP报文,根据拥塞窗口的大小传输数据,以及管理超时重传。

+};

Lab3 TCPSender

Lab3 TCPSender

前置知识

在TCP协议中,TCPSender负责对ack进行处理,将字节流封装为TCP报文,根据拥塞窗口的大小传输数据,以及管理超时重传。

我们的TCPSender需要做的是:

  1. 维护拥塞窗口

    diff --git a/2023/02/25/cs144$lab4/index.html b/2023/02/25/cs144$lab4/index.html index 7c5cc447..e54323cd 100644 --- a/2023/02/25/cs144$lab4/index.html +++ b/2023/02/25/cs144$lab4/index.html @@ -61,7 +61,7 @@ // Wait for 1 second before switching API hosts rotate: 1000, }, -};

    Lab4 TCPConnection

    Lab4 TCPConnection

    心得

    耗时情况

    【长舒一口气】

    +};

    Lab4 TCPConnection

    Lab4 TCPConnection

    心得

    耗时情况

    【长舒一口气】

    最开始先记录下总体耗时情况吧。本次实验共耗费我16h+【不包括接下来写笔记的时间23333】,共耗费三个工作日。第一天看完了指导书,写完了代码,过掉了#45 reorder之前的所有测试。第二天过掉了#55 t_ucS_1M_32k之前的所有测试,直到第三天才过完了所有测试。

    我觉得这整个过程还是挺有意义的,每天都有新的进展,看到test case越过越多是真的很高兴。但是可以说第二天以来就都是面向测试用例改bug了,非常折磨非常坐牢,既要去再次理清之前写过的shit山,又得搞清楚很多让人一头雾水不知从何下手的地方。但总之,这三天很充实,并不会让人觉得心累。

    放个通关截图吧,感人至深。

    diff --git a/2023/02/25/cs144$lab5/index.html b/2023/02/25/cs144$lab5/index.html index 59473fe4..c99cc7b2 100644 --- a/2023/02/25/cs144$lab5/index.html +++ b/2023/02/25/cs144$lab5/index.html @@ -39,7 +39,7 @@ // Wait for 1 second before switching API hosts rotate: 1000, }, -};

    Lab5 NetworkInterface

    Lab5 NetworkInterface

    Overview

    在前面的lab0-4中,我们实现了TCP协议。而在本次实验,以及接下来的实验6中,我们会将目光从顶层转移到底层——我们将着眼于运输层以下的协议。在本次实验中,我们将实现ETH协议,实现对IP数据报的封装以及对物理地址的查询转发;在下一次实验中,我们将实现网络层的路由转发算法。

    +};

    Lab5 NetworkInterface

    Lab5 NetworkInterface

    Overview

    在前面的lab0-4中,我们实现了TCP协议。而在本次实验,以及接下来的实验6中,我们会将目光从顶层转移到底层——我们将着眼于运输层以下的协议。在本次实验中,我们将实现ETH协议,实现对IP数据报的封装以及对物理地址的查询转发;在下一次实验中,我们将实现网络层的路由转发算法。

    承上启下

    协议栈架构

    我们在前面的实验已经实现了TCP协议,那么,TCP报文究竟是如何进行封装,最终到达peer那边的?我们的协议栈架构究竟是怎么样的?对于这个过程的实现协议栈,我们可以有如下三种选择。

    TCP-UDP-IP

    在此架构中,TCP不由操作系统的内核实现,而是运行在用户态。【事实上这三种选择TCP都是运行在用户态的,正如我们实现的这样】

    用户态会将上层app传来的数据封装在TCP报文段中,用户态只需向操作系统提供的一个接口传入TCP报文段以及目的地址进入该接口就行。在此接口中,操作系统会给用户传进来的数据报文增加UDP、IP、ETH等等协议头,添加端口号等等等。

    diff --git a/2023/02/25/cs144$lab6/index.html b/2023/02/25/cs144$lab6/index.html index 362f3073..21f11201 100644 --- a/2023/02/25/cs144$lab6/index.html +++ b/2023/02/25/cs144$lab6/index.html @@ -39,7 +39,7 @@ // Wait for 1 second before switching API hosts rotate: 1000, }, -};

    Lab6 Router

    Lab6 Router

    心得

    要做什么

    本次实验要实现的是IP层的路由工作,但是只用实现对路由表进行操作的部分,比如说增加表项以及查询路由表等,其他的什么RIP、OSPF都不用我们实现,所以这样一来其实就简单非常多了()

    +};

    Lab6 Router

    Lab6 Router

    心得

    要做什么

    本次实验要实现的是IP层的路由工作,但是只用实现对路由表进行操作的部分,比如说增加表项以及查询路由表等,其他的什么RIP、OSPF都不用我们实现,所以这样一来其实就简单非常多了()

    有一点需要注意的是,它一直在强调一个“最长前缀匹配”。也就是:

    image-20230309142032359

    image-20230309141949757

    diff --git a/2023/02/25/cs144/index.html b/2023/02/25/cs144/index.html index f89413f7..c1412709 100644 --- a/2023/02/25/cs144/index.html +++ b/2023/02/25/cs144/index.html @@ -36,7 +36,7 @@ // Wait for 1 second before switching API hosts rotate: 1000, }, -};

    cs144

    +};

    cs144

    总耗时:65h 约17天

    实验官网

    感恩

    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 b8cc74d3..97e01ef7 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" @@ -41,7 +41,7 @@ // Wait for 1 second before switching API hosts rotate: 1000, }, -};

    状态机

    复习数电时,一道密码锁题令我十分不解:

    +};

    状态机

    复习数电时,一道密码锁题令我十分不解:

    image-20230213201519568

    看到题目时,我首先联想到的是mealy型状态机,因为我联想到了序列检测。课内的序列检测讲的时候是把它当做mealy型的。但看了标准作答之后,才发现它其实应该是moore型。这让我对这二者的区别产生了深深的不解。

    diff --git a/2023/03/13/cmu15445$lab0/index.html b/2023/03/13/cmu15445$lab0/index.html index 98776360..3f663731 100644 --- a/2023/03/13/cmu15445$lab0/index.html +++ b/2023/03/13/cmu15445$lab0/index.html @@ -54,7 +54,7 @@ // Wait for 1 second before switching API hosts rotate: 1000, }, -};

    Project0 C++ Primer

    Project0 C++ Primer

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

    +};

    Project0 C++ Primer

    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

    diff --git a/2023/03/13/cmu15445$lab1/index.html b/2023/03/13/cmu15445$lab1/index.html index 1061f538..aba138f2 100644 --- a/2023/03/13/cmu15445$lab1/index.html +++ b/2023/03/13/cmu15445$lab1/index.html @@ -54,7 +54,7 @@ // Wait for 1 second before switching API hosts rotate: 1000, }, -};

    Project1 Buffer Pool

    Project1 Buffer Pool

    先放个通关记录~

    +}; \ No newline at end of file +
    I'm so cute. Please give me money.
    Hexo 驱动 v5.4.2|主题 - Yun v1.10.11
    \ No newline at end of file diff --git a/2023/03/13/cmu15445$lab2/image-20231203165014734.png b/2023/03/13/cmu15445$lab2/image-20231203165014734.png new file mode 100644 index 0000000000000000000000000000000000000000..26a7aaee15c11c83b4ff89f894ecf1b5f7cf1d6d GIT binary patch literal 170340 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%0DucE58RFLf=xP$Tk&9`3ki2;*)E*ON|opvhm{g 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 literal 0 HcmV?d00001 diff --git a/2023/03/13/cmu15445$lab2/image-20231203181652821.png b/2023/03/13/cmu15445$lab2/image-20231203181652821.png new file mode 100644 index 0000000000000000000000000000000000000000..9b954c96093fc15641ee752f0be02126f534b71d GIT binary patch literal 22913 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

    |YHKCn9qcXAQo1?yLx4aQ7X+3^eL1 z=zXkeZ|-ILlk77+0PGAjwqb%qvPzqu0(3`sVn^Nma`UelH>gC2k!MRo`^Zivda^xMC(tZ|Gv`3v3gC;n5I1y@`3#!5YPd^;5?K^`~?d{oUNH$nU|~trJIV9O#blXb|F0? zpQnL7d%9U)b~W3+KY30nuJniVq~pK)IUZzJ^7$Cy94+vD5tlL2r}v_O8&M9y&Q#34146t}k}w4De#ElsprCmkA~R+0yj&86>z{Fy#V&7>i3 znvpS%k&O`&FBdPLYU-Y<{_1!5vR=$dC)g&`=Ai@zRKYOqT})z}B3HNaBgxsHzj|$u za4-0rJY0^)f(v3W`_-gsKDsk?@@tH@p>k>OOk-uK&trW4d1>oCmDNV^9I*W^xTH?d zgQ2!m_5ne06X1Cr`)_6G>{V^rN-c#Lw|xi_G)3u@?^Xv@=%|5p!r4=6RZa(6OLUL$ z*6CN{hkk0Xx~e;d)Hz8HdsJEy5A<60YOnytGP-u}0r=8AQCOBR8pJY;_Rw$sz|qKL z_Ged}t$KHB$0sv8!93MQq+!W?>JDA1dvM2mL(t# zNVEfbB84iYx_`r?!qOq#<#;rfF%E6|{$}3Q&SrP6i4Rm%gw!i^q!JEm2c(A;j4D+| z?(HROBU+&-lo(ItA`K}uu6^ri$cU_;3&!cyL_L-9t@P;F=(?w7x)y2A+2QhOEMT!b zqYcM{&F_H7%x)pIlh{fJ)E*VArvAG!Q;=r&)cm|&ID_cbL8iNr4C~*r)n<9nJ5M(D zbp>Fs`_iZny=S%U^!G#yR(Y#HAa(c1CvN$PcS3*_q8)6uxhHbfEP zU`h|oUoU!j*hE2cZcECKbgJ020kOE5B^(7ftIET-7~ZlLIX(R#4NxIW_74h?lNP%) zJbVDixy6K@yw7gAp2yqSdCJH?8XLl&iyVGE{%8 z$~aritExr$bF@s!eAJ)J7wl|CG->1`9xunx<7D${?9*DHkw%C5iu3&0sdHVPVvFv3 zvlAS@QWVTh1rqKE5{ab-TbavPn!a(g7+;c>7(BeN`*A^NbvAt>dR^# zjGwdhZw5%e97cuuN}6GdNZ|rtY=y=QE^i7|m8AU-($K#WPXCYWrPN9)r8CEbaH`#f z#(%t~ci|yp0{mg~8R%Fy@TYE|QIcVVQW*x9vgc||XTSt>Y!ug)S-@Dw~V zkU9aNc%Nx5$=k3?A;Jps+L6nwyTF4tVpXqKutJB588SE}bh)g|)f#v_Dg(*60^smJ f+4KK5n?Rvn{y^pEmU-#guZ7wgdg_&b*ggLzr&BAd literal 0 HcmV?d00001 diff --git a/2023/03/13/cmu15445$lab2/image-20231204001136531.png b/2023/03/13/cmu15445$lab2/image-20231204001136531.png new file mode 100644 index 0000000000000000000000000000000000000000..bdaa419cf63db1777cbdff48d809edaec47ff08c GIT binary patch literal 64385 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

    >7688IEVaxR1ouE&nI@RRaT8epmGsctm=U&Zg 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 LW_P;G#QXXmY;aoW literal 0 HcmV?d00001 diff --git a/2023/03/13/cmu15445$lab2/image-20231204163052528.png b/2023/03/13/cmu15445$lab2/image-20231204163052528.png new file mode 100644 index 0000000000000000000000000000000000000000..91b337cef65b6e6efeab52f7d0cc6c390140793d GIT binary patch literal 47515 zcmb4qWmud`)8>P_2X_hX?hrh<46cFTmf$W4?(R--cbDMq?(Xi+a?bmHyZdu@e$31> zJy%y(b=BQ>S62rs%1a`_;LOc?;6$3MPT!h(OikCU0|0{{u|RqPAMCG8~L zRYOG*JJ1_W2AnL^2U6_QPY7}GFHm)~Gg{eNP<5C~S_oxchSs_kb&3rlb#r<3Jr)Rc zj0SbT=o-=MXbZExOm0Ab5rch3hKt&H{w)A0B|<`dEy&M5*k(UIva+%}lFYWR*kj1D zL%RQ#oY${cDK6Kr#4B`PYfjzGd*D*k_*kU|aU$B%=wW7MWo2kM)oWaj z$o0J+$W)%+@Q+@5jaOCvx`Oa0?K05IY)?HSOuneHmy?9VexS>-0uVuOM~E8HCTp@{%4(ZNIRwLdjtpfBLQ!Ut}J z!>YXd9TSK9@F8IMp?~RCPbApF969o&XEQjwD#sVlIP;f)&iyJTotFc(pLWt77oIKQ zPVC~}(T`kl{?bE*wF)fF;2AT0B|)W-3J%_YxG-V7s4pSDJw9M_04-2{2vKAa!#qJp zRgDrpfDEFQB0+^c6(+JYnk-|$h7bAjY<9L1ZiJ{sWA-w)k{;eMjq7wiz7j*!%t{y- z8^g84k&>UFNtul-NN2-`kJ!Od{v=KJAuL5{zzA%@vHrLjSppK=ccKYZwNgg-L#b>P zl425HVUpV4CNi2@0rtV3T99-wrv46Jez#8{TV#&de6;~-;Gv|H2=vwOb}+dQa&Gnp z5MKRUWc8^I7FiU_elgg-s_Cybvw1zMT#Z}OHGKignizIaSxrdk5Uq+~MTjLser$wH znS=ih9q{?$4=6pehqu9Kkr6wU_^5cTF3eR@YcRq$bIkDU`bTj^dK2#-&C{wt>NvT$ zPIsR|W=@Y}ZR>(G4`gwQcxen;qxIb!^6zR_8k@uX7T)(@@+}E0wm%T?ne1DzHKu>Q zO`~QCs|@9X*+?2bM!nEx%KA8u{jLt)`WXRp5t{?yX#xLkuw5UCo4Tr4x>u{hsYLDV zCJy*?#P+B*@X5kq!((bOoP{E-h-F({g$zK8`G=<*?b!SEmRPCrXRco0TjOoiS}sN$ z{JG0ez%_Fa{M*v4Q{UZJn-4ZZ7(QM=CQyvlIBecY32%&H+=&%wys5O(CDCF}?(rch zRrb~Pt2wuj35PL;|J83H8DBj#SV&c2&|gL{C8%IpYRnvXbk$0zf2SSv>twEPrzFzP zr+oa++%MER*{56wAZd18$Cb5(d}SdDwbQY&MORxW{p7QDpf}URt^!p>9IBdex;%}v zVZ0&yU`SCj2%^e%KwUQ0ec6$Fi@H@$)r9g6j=D&LO9#ud#`dXvw=N5zaL zjC`g?4pPz1Wv!#KyxT??Th(FMXxjSjXULiM`SRWpKgDD6=%kGxoHh6azDuZH1Hzy1 zW41P1g=BO7jCdv%WD}BLEYiR0<};!sqs=;)TAP~6^?;5+3UjiEPk25bBYocUtCv$F4z&(X4K%A zo8K3}?^uNpY*A{7+4s=GEb0EouhN1M<%dyda*6GN#`)UcgoEgleH9`@J542T-sBAJ zK6<&WS#}cVx@p0RN(U%+NF`Fsj@ZK{;}Ju~^s|%F)1V>PJ<$fRjz_VXVRpM)c9>2C zk5AJl%JgA|=YRp%LU*#CAQ`G2N)YYB=Z_#!g34o$EBsaLSNKZ|lL8f1IZE?+;A=-e zOd&Kx_-@6Y<|K^4C1oI`=yd3^c_?~O0IiCgNCK?sb~ousWN(6$ zPpJoH9V5R`ehC!u@S=i8DsN7zpE;EZjBJQbgZUwsFvXi7vf>Pt+myCN^0+p)X+ z^X@N_MrVdIviQ%75ot!kacf@PN0zsD6VBhaa4F);xqw zCbJnJO%xXFp54>)C8qk2df-U2)f?^Q@WWn!;)%$iS0D-x^*TO@Z3WlBKka%wTk}1> z-)&G;n9?W6osp{r&E^&IrFh^0>TwTyzzTghXPiI95o| zb9BY_cnz0>tvfvEQs}6;Sz}mtt6+Q$EkjwI;&acjGKoyjk2B)2N9BKvRYA8Qdt;yf zOTBgJ)+`oIS983eRafTHZaAoJW}WC%mvoJ!oaY+VzpQ!2gI6kIM0V1aWzli%_r3hTysmCO2-9G-DdC1ZVmVVv_<$M4MONhy6T%$C#KfT zf_uGu1RquBU@QsoXGt)pS8_7!X6<`$!0br71yXG6q6Wg{a3m;9V(U9!Cj5pOn&ewDfS*gHvYpuzU{;~kLU9J;<5O`}caYc2BH@X?C4mN9pC3xQ3EjitjM&xV?kc8%( z3hE9;KmyC!KI9kPL`H-48}l#xr3?IvqC8@lAPzCiQoe}7UmsJ7R;`5(^qs= z&LzxTeV?$pG2=`bprL^c=HJJ0V9>p~Rn*m?-uMK^E#74a0MBpXj;0x$Y6QE9i=#AQ z&G-YcG&2n+pOx#O_`m+ta#ZC%(ycc zeiK4t_n7Pu&ERmW1>T36fA44P>4!B$OxPK9mdAo#+|sjNJ#FPB2k66GM|QwUPEvG* zIOWR$vHQkw5ZM9b1Mj5vOL3Ac@RXgc7ZDDXe53^~Oxm#BhZTmqiFCcm;9}}G9+gJv z+<=1g!(xR&Mur?HbnVJ!rlkE1!~HC87st)y;BKpL4WO|o;1%h!rYk&7q9ExWl&(KS z>)Y?4jmgK-FH1!FboEf@!!qAH#0RCy*V(g*#zte#j=A~}`b71It7_U%P=Wj0Q%Bx4 zt)PAw`S*W}JcmV5Y(O4FTPPbus{x?|1qUhe9L+)H_*2Ps6sDhLn@hf?W=f@GHKR)l zx2q{7xpC{)YzG@2%oa;VgWY0lO5{u0PLDReLPw+9@m6c~(R2nUJCw1Lt#Y@h9jQv8 z?V?OQ-0Grpsp&y_C3QsXUn5I}8dahQn{rWH2Y(2_*#2A#i%kIP&(UyC6+?9>Y=EqBQTVlNaXCLYf#gz^B^hwj)T(D10~N3=fvWo> zh(mjC^;u&KfzCyVp^>sYBnYW5vW(b;_!I*Ug1ZHekAJk54j}ybK{5%}xspI4HZp+w zW+NmcAz#rXKz`*aT!z~6v%NghGsjM}OJ%jqQpZYJ6D<$$SnZFZFM{l{{II7g%0Jk% zz{C2%45&V!2=QMNBYils<87_4A|)LuREnSaIEc zXBHvR7^2)`uj=(9&?&QZ9kWDFza`=C(qzmoW4Lg6@tAC#Ft{^ZbTl;Uj5!2JOa^SI zRW}Y@8Xk}&SHO7)wv>@sxmJA6i_rwZ4ip?+9&fGJ3>j0eu8+zPEBPy-1?!x@B>;{0 z-8n#je+%0rGo0T%Ob&c8K@PkVr5Glp5>!0YKUzzXILLgj&mZK7By;f6Py^A5F_ zCU2H}Ins?Tb{dmoDJCz&utGBVX$|T-nL+-7?TsZ#O-U{?^u$&SF&e;qJcQq)Lo=f{ zm=CWv*y)o*sr0Vxx5>ML&n1#WRMW`T#vpzIi;PgIeJad~iVj`zh#xkv8jR$2H@bPO-^`0pfcxq=>RR&e(uxys$Hkv~znvli^bv7C z(fBAKfRivez6Ln}5WLr80i0zXZ?$E}oq>VG+ve%ajYCPOp=2}ypKkavNcOh0z+qjc zj~&eC4;^X#)p39DLq|K(f7sK%I?hRSd#NA-7v+@FE;0xUk*r`70ZGfXjGJG#vdI8w zm4I0&C!3CuGjJ|`QnC6+;zb$Sh&PRM<0rr+t<~fl+cSX8=V~EkfPSjd{vX2Dj60_` zU_n$t0Nm82IDO_IV;8k>&0}4dOocaqVbfwgCA`Orh3iEx99(mr9s16F=M0T{NkUOy zOE+Aa%jqK!`l_=S6E9-RXG#dr@7-#^5hf91G`FE=JvBwB{T*REhqs3T2-SIvc)$Ro zpb|EpuB<5c>cd+HDpPytPGrv>6Crrx`O@>)7AVTLupiFcD4PRM`@!db zeXKqr&SH1HD0?-OcoRO|X#MYU*fZ9j^4GbWIF(?{DpCz4W4*Ez6}yZw`FM{F>{tzU znY|M9o{gQ@h$NV0zzFs4IrqK4GtrP2l~&?g*?c4ISUdF5Px?go2?X0Y>#A52T=~X1 z1_DUiB}EpRl-C6uO#4qG0H*cf-3CIXafC;X5T(zV_i30;J*KyLZgq7|@a#7>)O^H1 zROTng9-~e$6cefu?Wy!O$wu&?pnEN+2we>o^2~nTt<4xzC~j!k+Zb=xm=jO+GFc6I zja?b2hqtqi3goXFb8k;%Uf=Newni)>$wT}Uj$o7PW*{wL+#UaTgS4>IX6kS3bb5h> zqB9~%jTJnkmg*=^n>y_qWFnl5yS4PHFu%-2?nLVIfDUyJ$PFB3IbS+loQmLs|{@X#Yxf<=X0|=`>Aw7wPmoA86TfdpDWJEQDM_R&_WgFUz{xZz{x_L zAleV>qr{WA@&qMz7f@gLIr_X>%d}$Gl^*Exu4fNn^^9+H{c^qiswObTJ&5b^h z4xXd;xyddkC!-m0VbTSq+x3UoK$yHzJu5$AJ#*h}&SPc=Q)UXGvp<^%LO0>6$E75O!T} zU8K0KMoIVq=IbN^0iXFU;_MW9D}`XF>e!ZYdK-*iAAJN|$`bOaTK}Z2D!S>Glm6-x ze$SHTJeS#4+lcM!pVzd6F;QJG;GcXA1q7+@g_{WI%ST$^%ZGVIC_k1&cqOPW|0HW# zzMzHn7TbuLUSDjA8W|a3V8k*q)qj zA3pGrM*4Wt6Ah}XlIiZiXm3MjzPd9b!jhbf`A-w9-#qv~q4>{}3t=?53}W_mb|lIG z0h3Cj**|0aZx0Cn$#Q*k>*)8u`9DAZw-pw0SJ(p|?xRsIfQ4L#UvxJ7-ywX+F8ojL z@&9T6@6i79@ym{6PU7G3{qJr*;F%z(F8V(T{GUHl2cF?HD1~-UGBXQT=ncgsr&nsT5@?3TOLB z`={WAh4HrfuO?L&%l`bPq^X*(Y`jOUOPReKqvzHBR?~FXLTd%*JCW8U%?dhy;<>T*!=cgklm0{tb%HzUgDp5Mh|Hqj1O8I@a^7k=rg z=>t0hnR8Tj9DF5$;Jz16Id@k%i$gVKjKXwBn9i%*Wd|H^XQ1?07XeNdnb}d2);U;& zDfIJ#-I8WpOojVQ?ifW{YxTqKSE`5~w7f0i6&#;e`Wwzn;tQK`ZILz8j^}w6X^G&Z z#IJWXYvQrpi<@yPU@8Vn(iKD9BoGA-+3R*#^Y&VnxT^+-wyM;Mt>Z!LDz=((2$NE? zha9G$Im9A2lf1n56+K^9pR%zw&&ZSJrwI-w!`K2z5E0@!lF+A(HSO8a!yHi6_zVBj zg1v!3=id1XJqKtt!!4xS>tXy@YnwqU`@PWxuPBEBim96{RubNz@!YqoOpZ^7$D0=Zb z5r?gx1=F=v!hFAF*H!0_)xNcpk%I?#*UPrKmB|eO9ETs>HBb`qLs%7>NrrFFuBROf zGa!TO29!A@kQ36!hC=@Av(nPHxXjX5>GTjiA?3iO!dkg0C3n}!o^8pxL^oa=`Ci7h zKbV{MZFZ*7H?ddKTUUxJyNSgZAtd3t+YNAtAbkjy6gdC_IMFL19Ar)t> zM#A>_eokk+^44z88nZaKutbuEALO1vT67HW9m79)>m%N%d*Rt3lb+QzvY@Ju-Ot_HPMpqo1X@*&LYfCiy5Avg&}bW{E~)@nuP`taFL_jlsX9~D$}>1{@z}jp#R4jlVq^v8j*)NU4LzY8g*%rh|vJWA|{b>;98< z>%G5DMvT-e&S^14`roErz64kkXM0F1$6x>eT=3y%m(jBD-lqNXzOxaA(eA7cp&Ip3 zgooMd3wmJSZT~T}+{F4MzQs88u!d=t+-nNoO7AtkOxe@NwXZ$&01Xo`5E7G}$D~tf zq>Wl$O0s_+nKEKp$Wa@9akg_aV#|>l__}Y+?{uTs>KSzJa$Ww~c~t$WcS0`>nq=-d zUdTo#uO1Ju_!`XHF|9T}h}`l1v|6i;YW;R(Y?;C2iJ<0c@w2NiHxZka!O8y|$gJ+6IvCc*wqa?4WraS|Tb zLF1ckv*l?2N+1z&zC>X@?josswI-B7>nhigH>Z;u9-yf7mkz_J*2w6q+xLORi4)2O zBV3{5z1Pyz`)jT@ZpTUy<>6T3G}&Phd`mcZTjL@)oE9OS!!T}&;@?^852~Hd366GOJzfe_MWU>JarO28`klrv<-$JIL7;crCGG# zNE0G(v$*ffRLi@V?D&f(zY4di^=JudtPUaVxBY&ARq}%V-$`!_Ln9+FZeFK3|8*$p z7C$@{qsZ+~sYRbsc!8g;XtXUGS~c*wWtKDBq*YkX+$8vruj1}kORv_EDISRnE zt-`!rzv3+Gr2vY0DZ2D08sUl>LjuGh&Ba;t1zGfxZ0=98!+S6j5)nV~ld+N^Q!CQ78VsFq%p07}>5HZg z9y@@i}|E%|lUufHYp z_SIt0CB8IO8Cf;YxqS73v}_~37#}=P^I7Q0dGebJVbW|}M%_n9THj^Z;-0^Xtv2@= z$cpufypl?M>&Cd2bV#F(`b z9SytC0(HB;8q#Qw%LSCiGvalbOnlDbf2`_ygn2jo=TnX)MB?*|+@5NiX$xmLKvz>$S|(GKvp(w^la-X6J4Ig6IbUnruuEz;$%?LhHWeGKeYhzp?GR^ z(C%ydegXr6KSs<>J>oUzM_1-Eu`142c#C)tJlrZfCRZI*onNI^t-H0oXNF}{=DY-% zTOIIOIGny!k6AfC(h6WE@!M}*?W**r;vapz-O9rCs&abiI^34Ln+Er}x><&KyL{n! zy}R6oF%8!is&ewPWW7FCXHGkC?hVw-*WbLi$U@|g&ru5E;#eH}+hvCDz>Yq6nVvvN-3(g6x zk8O3V)7J>yebof{+K)!U*?|KN$CZ2#HK|y%BsAoc_?QTC$4u_5wM;&Cvz5+4SU740s;YyaPRKH(y0@SVyYEH zf-8>aEHaRyDYI^|FQevA*2J^EH=dEM9Dzksl(hks=V2auM!Bv9GamPNtD4az6t-%o zE&D(aXQ_**Y2V&m3y7Gv1@u`W~tja$($c;ItF8yOVFpbDge36bwqtUI?JkZJOK%4_@uWsnmc^CC!RbpKq@9y2LH^QCK& zE!qQ+xW@Ln+m5-!2*5eKojICRX&8_Ct0L%PyZtmscp#+lO19@klf|)axpoo_UH5?r zN3}#IVM=#ld&7mQ@;r1Oq{zVUvUSl~e(yj|tH>0R;#{_E{&eftw2HbHHDu*+;n!c) zX~p6jV9XUBMwv#o02G&(R$>Nx8Hcwgag+O*LkX5_xX<7Slam)q-P{bU@Uq9sd;7vJ zJg{s-!4a2heu#S@{fNQ;un(EF`i`KU1J9}W2oBWtDK|f{X74u(rGwY{t$W41yH!wA z42XX@bOg>>C8&)fvclSi-G>)zIi=wi{+inS0ZVDNJ=baV3cq%-tkL5YaBy&V)S-b8}lZf+EW$c{_fTXGr^d)0MnB6c8<*dcSx4o1i^BP3B~pOoNBpmcWR_ zTO8UG$;Uxc8h#;>TOG`vFYr*;g4ASQSg!1L_Iak%+~*SaS!ExBcbg?_P!zmk~GTSSqf zmpCoVFV_vHOGcPpULnKlK5XNsg4h zxVi+EF_`1&Ha0$6*hf$C&Fu*hp4G4H@iin!5uip*V`SoeLDZITN zM~|*UimOMwo-4~_bK&ZH2zZBxSp{&q&4}Hd{@$I4-4-XmNAD|u;Oe^YZ$K-lvyOJCawRv(Y)=n-TPpLZpCxTN$>K9gRQ`L(Ih zcv`xNm!Z~Ul&-|7YeOM&0DDFp@sdlh-Tr9x`Uy(J=rCo#f~U0kmFt+D+3u2W8dH&H z-C<@)ajh3>zISD#nVpq0OHR~Q-$E!hKaW++xm3vXB5n8!U)rN zKu4NyW%7V4hkY9?aeP=yPv4|frovPuvah%>CvjALq}LjM125>Dv7q$FMEJ64<+W|y zFoA*3g5{T9W&O2{C0x3;3;zHac~0JwKXc4&U~-U;@jIo1zG>fy^?660f7J@m<%W2) zY&;2S7BMz@J?!aIA&ganb^aagG-`icp+Ou>Od{dB9IDx#H2>*LyR8h?`7LIxHd4a# zu3X$wl%0dRcbVk!hvLO$$>Kb5*+Rp9OA!Nu^Y$vQH#A^1NAI$(bp)B8;zuZ~aq;ky z*Rz5jOw*Fd_jr+4}03X_DtM(c+I2} z2yCDdI^1pxvvrnPW;wm@@J`P?aHo4edn0B_-_?_r(yHhBk)mokHZVPMA}2Q*AfL3V z0O3>Wt2SM3qtWa%KJ6>xY`e&wYs*(W4yVuEKFHQe)UnNDd9p;>M9wny{=`-@&_UfH zdB~)dai6cz8XtCY7yRGaB#PcE;sPOQ7$42oWw+Yh6+=pRu;201AdK>3j{GzyQ~U9S z&lb11CDF1pLWOVNo8@o3Qf^4*2IbYXF00un3r6&bMQAQR2cH%Bu{YLuHoPTm zc3Wa;b|jk0meB`j>^$~Mk}%T^E$F2$-=BmMv$kM~W?jPqYB&rlFqyQSEpMFUr%TGY zt&T@0@%7KR^(`$RQQaG6ytj3a%TV~Xp9uJc?A|%`m-*E$-b?2*rDKRhPgT2e3xI=q zn{8uUzL1bDCa>%3#fjQdtAo4qqIHjIpEnFmQ!~ zZm-dK6HVve$^cocaitMW>ertHfCo#CH$IfBy7RjdSd#ZM+E+|!D&wvG_ASYCj_;+= zFuzt^cr+Z&(=PW7(+|Ng1mRPP=5r)>o|E z1QEK_)9m$LJjScERLFfAkHv);+xKd>%?XN-UhE)TaqJ<--t%?KT&q#ko5FeD+Yq4NzVO^m zYS{LSmGz%|Oe|XLJn*w|c;-J=)|5J=pH`3u?~BtE8ElWCHz`Z6{&SakqJZ$}H)ert z_6hax)tUyT+FxP(Gv$oV^NiE;<4XUm3n+2aSNQ4w(Tb_S8|oE+pgUdPJ1#2f;Lh$v z{sZr9K{2{Z$GP6Eko}HqVCHXR8OzJ`vZenp>mf@Zp*hosXtpt=2k3ZSi?*`jq*$~j zft?MTS1Ac}?@hi~K?x`9&(V+fO|3Bwt`z&F&4e8;_e`hS3<;iZ9#!O=kV*Tmb$>E| z0A%Zyxqfn9cwUkQQ-^c3D-HSL{*X*r}$1x%*C+A@p3@!3pC#2P^Zi5hk)?o8?+l~pi6%6P$33RQC zift~~jr*p>MXXAp1NX{ky@h>k{d{k=$hzjflR_++Q?-LNk;*G%Wic)HQafQyMgDle zX7%FN7ZJ{STj)CiBi;lEGRJx9$$9=+SAS=w3}}fV*-eXK7Wbhz94^+ z%VHIMp;y%TfwM~-l37h7n#%hE=5pDNV?2SJjhHNVm>p3Hkz+bc`h?8X`mJa}M*Tv{ zUI=KB4x9@>|(Q=9P!JGOjr!WUi9B|ZXN+(i%1@*dj`Efy1H1=CB2?jzM2BcI>{ zEwTo-g`*{n%xma~2o*)-kJjw(cSfw6X|`|3jxlM6{^opg$)-CyGu%Esr#ic>DkZeq zrFeUA?J74)vz5H(sm|KG%RfBwxL@vUkVGM{Di?=P)Qxu9K8o>g`iZ7+*J@ki7=r9` zGEEiWx#*Ba`1oC;gDRY>^?up?*_6hPO?za;Wh>yZfJnP5Q9;4I*7Q7bnjxj#EJS5% z2smhE10^KZGoLPNV?Rt=SK`kN)nF=6J`CyWf-%+1=)lZ-BG3s;K?<|pV5x1eLiGa@b_ zWh{kDIwbyFo!nTWWJN))MkohoKKUXOio$ch0g~V;KnKFy@kZP%TPtGY%Xfh4jV}HE zqS6Bd$%g=KfQY)`OMR9ky_a4=Zn z*sHvBP;2Ek(6cw9>|TRcl9RNSdfuq_NxwYaD>>$;FXhdq3^XP+cJe75@p6ZWV0rU; z+EJYFD4F7QCItM)ZcBa|vJ*#xCia`C2|vWNXu&+M3~QQ8iRtY%cep##?YCz)V{{i( zvvv5N zsSPY$a$T`G$E(;dA_{HvN^O7&v;2gReW3~sAaYtx6lVmyHCKH#!C7ujo7r{F*~$Pn z(#5Z6%0F7{I8D9VYI$g#I6geD{jO?-wp59-=T&uoK#Ut-G&tYBjLx@UIAb-BmP;S^ z><*t1pdYa#+HG}(#}q1&tLEz7Gv2C?)ag{nze(YCgyJaGL8zWSva4YI?ML}xtC?-# z#eEba+xh3~mZM*&{s-hqU}cbIl-~_4S}L6N%6S3kDlI3?Dg6v+=6dWH+t;f7p`wLS zOMU>y(CF>&);)F$@>DE{^@`D>;dx#XDAuD;`vX4R@$yiyYm|a%Dg8HI$Vn9lI2IyQ z@fP~33dP3C3heB2B`0vOv)48>sH>kRB%(>iHQ@tc3QB=90*maU)ZIxg%CuuV!7JN_ zlPM-F#KQ&-3B0vq4+hmyF;zM`&_BPTL%|D_WeBK4Of0JvuBc~8O0BImag4>xwiRH~ zzYe%ee&d%q1B1zPRdj+s(RH)emJ6A#vaAhdTz|ZzWnQoUXZ{xMT8OWi#-&YO4)ey!Z#sCohr-qDO`f}HW)Q| zi|RQ|7Yo((b* zIdOy-ky)7`T!C+aEc#{6CvL84$1JXN7lAeP-t>=8Q}u^*G2WW*uYr4VviQ#LbxwRM zDLx!RpUXh>Xe~Mulx2dg>WyqS;`&S-k=A_e?TlZ8U;ku0U~^?mPkLDzwJ)gvT0?LW zlj2dT_!t)@U?hQT$Id{O`IOa-4?I}G9};ySZ+nf#%Ha0wW_$T1*75~_* zHk}QIt44;iv#F;YmE9&Ow(#t7s={u>7H^uM zBZtLz?@vrIsr*&;-u#`fvSiqnc_bIk0lN3lt~p@M+OPv&M^YvnC8{OdqoDXQU!o4f z%R-v+OS7Ron;9R3$U1$2ON8lHdv%j_pSCc3nd?kCA}e?9mT4A$WW|nsHuVzHDVnr(<7Ijj zFd5*iu#JFva=V;6NB-FF+@jC!%ub%ubjKve)Hbq{GP{KAq*z*b)5-MM7qed3lX!0! zI{u~^Zm(?d-v0sW8jTq$Fn6v$ZOfgkS@)PPvLVN`JBSmDv)`57$W@-Oo_h$hn&NMA z2lcrFK)&Y=Yrqi>64X{yGM#WJ{IB}F{VVGXh7*tQY&8v(*?yz%Sch4*&k50SsTqxB z&<2Oh)$}c)T})-rJ|&5iI#2*cz}a_gl7a_3D$i^65zm6voO}7>_3vT2x;=F52@4mm zA_GEnZf2)f_2#`BF7Fq;5I({4wJ1-HQcQYW)Z`&YUE9Jh-?O>*DIAzoB&FCSjdgrs zJJsV%kflBiD@5BXhfN!<;f46Yctk=V<@e#$n&zHRD3)pbuB%Z7I{$)_HCL_XI9FR|6XM&u#k@On94t)s1%Y1Bp@zB!FCNpiU%mDja_5a__N)R}Nw zJESvL+rT@giWXCj-#D>4Yl`<5A8{z&@!Hp7UlSMCI-FjF4Gm+MkEOfT)zR5YM|1?8 zJ)QrQD_cc~tetvVW@7&0^tx^xl#A=H?&o4F_Fuu_>p&sgHbqlGG9KO_7+xz@F>sx6L3R1I~v&RAVW$BdQ*`$hnu+hGCM8XRjOg! zQ8%D>X=3&%0ish;)Od_dTPvZ1#-ta`>+;g8*rvx)*ye?G&t}h_yZu_Vu%OMjz(jo5 zbRF;s>d-tfLT~ZY0Z@M+`1B3weKF}dO=LkqSh&6Xd9}pTV?l~Eund;0y>8trVb0@- zbo>xyOy>o=$RBzAUHd?KzptQN3q)MtTsePLnWBKMsmWS>1_)amjKh59(^o|XprD|9 zM9ilO-N^f&BM6?wot{QzoFfE>sA*l%+-@dD``NfW4M^3)lJ-CX&Go0s5||Dv{FF$s zE@}g-xkuIN0K42S0;E{cp~w=Q;DoB74?pZnV-uBQ>%2O#JBLTvS159h8{e5222dU{ zP$Vwbmw7d@e=qzSL&RcA=HftOC%(pz1kP((oMxy@^Ra-%(!W0-vJa7^u+24;yFhKE z!;YTUZdPb+Q!iZgOYEcfD{IG|k?UIQf=ue%3UB>wB=%#bcsU{I>!{xGLgDS~%Vju% zlq%Fman(PEa_P0!v;mojKnnT93U~pJSScKcQu#qVCX&YbYYC(A0N4;}uXeT?2p)jI zWi9&}6YV7mX**Q>xdl^vkPf8gF24l@aJOuWZ3zbJc3}gG(FqXj?ZNg_V|uPc*om=8 znhl>@JgWk=HA3F>2FhOTwbhtsp4%H3ZuWx05j-M2PcP^%FUrf<@E^z2xGbg=GvhlR z0kZ_lUE@ABK#H&3ZXFlyP}Kuy%*-svipZU_{Bw=XmtNvBZaCJLCKRillJ?Wf`XU{Q zZQhHuu+1EdE|YNm2U`>R6VZ8NWLzUc^|TFmXZOa&C=3O12_v-6)=56Er7qjycthBqII67ufyL)? z*SFNGc8Ko{oJU9I5WURxt331Vm-ZUYKG%K0MJjJ}rb5#4nco;aS;K6scA2lE;4Ih9 z37od-qD(nu@^GzqCWEnoq-D<;WWs_Lks_;QY(Hg9QgiE%ibPg^FWe6J1CV7dEELcw zR~$o%SMNjkmU(>s3o94FiPvG&q=ITy5Ly^aZ#NO#ng?BNo?Aq%o%S$`oe~tjhNaR< zpC9*KR{-l#($4(ol`UIMFmtJ4J8wvV4=dvV-Xb@@$sNg(vY^{tOc6Y;3v`4Dbh-pg zq$oY+%QCh39^m4FYbITdl{58ojoFr(UcWs#qC|LRI+GrG6(=^`DAFzki0TL;^5yTc zoDTU5MZBYx=0b!KlQ>frfh!bx?6(+=g%Pfu%4@%S zy*;T*6{coP5A~1xC<;|7LsQK*0UBlT$JI^@VM&Zl?4ZM-0}J4STcLvUJH2lfAaVC) z9a7`$)%DiIhr_I@ok5el%L|4<^8MO@C>U$|UU5vugkr9Zefz_1kbW9x(mc#=g@614KbDytL zap;zCvP``MpU%?s{A+T%{2!I1K8fFl|z%M5(^$= zNg$AMUF#bYQ683sv!4FqBCh>~2dYY4=p~yBKV_|hr##grTix_JDh5k}am3`5DP`7) z#T0M*@!tJLzT=iW*8BaU z`z7+MXHplh!@$VjT}{y2>TeU`>dMZp5@r1HwaSB^^NdK9xjp<|w-S>>XCWV((Uu+6 z-ty8lNCjISMt|B`37E@}nK_Ds z(47|4=i0riHa7YZkuqkfRpA^jqW%IGJCo_0fu4PB5@5pe0j&{;v*3@|k(MaLeE`rd z{CB~i(8G;=D-x0Ppx%oZG3E_oV3pjurV5C9lrE<$XOBey+*niW*XhlQP{2B~B%fC# zi}IdcGi@`dAS?Hk0>{J9KK;>Oe+1BL^w?3xbgmI6=FEmU>T+^E0XR#_3E24=?N^dK z23%h4p3bd_OGx7GumDI<_`0R)1T7D}6I3N1$cK`6hYt(cOdOuX>l^o;TbzTvrvRtRj7t$pyzLuP~Sk<@Kx8gF;BSUiAs2G9QndsxJ2(`M++A zAa+5XNG1bjq9e;vhA$MCifF)Zn(h=HrmMw3!yzyac59Im-(WZfLmN79pWk>WY}9i1 z6Bz^r;1fxDvvB+M-kG#OYv9ccXQ&h8lZYwMz_CctFRcAzQIg+lsqJLzn$e@UHeFz@ zmph4O^s9vt~ z+Row_%KE!|tltcf^-ai7kWhGu80M^D^XJO42S@>E9gs0+)k3LrKLYOzO?gqYBi+cq z0L*aksrR};w+DNf_cCBLObN0?D^nMmXGG?y5g>>tmkwcMd^2OEB_!ZTje+71G!Q5B z&E_q0c&~#@dA(G8mH!D7DEwc%xc<=-XE(yWi-GaNjySvj^AV7k-b)A{bbGqdJtBG&9*$4j9uuo?t0c?eBF z!|nhsoNw9DYE4!AForCXg^`+Eznjn~pqa9FmSnN%x^4$2y#Nd4j&|d`aZ@Y0mtgNT(UE;j(1fL z%^Vc-Bee$50iF6P$STbD4<+)q{5X94cee?t1QYvQbQ><`rvILZkP9w*A{6QKQsb)K z5f{8~s20>v8{wO*ID3TzqaNVbsAO{2@>)J00RM9b*3E3CkNZ{fU~s_vBFRL9{qT|v z*H8{Zz}&Bt!P=6~@CUtfP;0yGaVBxiH1R+LfZdQyRt!Gi;_AF{vdf^8gXW9DJiS$9 ztVOWkA^5Q&E#ZT^1&Y&cs5%{VjC5CS z92KK4Z3Zfo<-&hi6BUt+L^d!Tr}h4uKhgsAw8CJ#sFJ&P7O&&Gbz@cd2guF6u{#$ zK+A?2=j*RwxkFpru`A#r4OZVUalRii4$S%#5DzzL=4Y(F$=D1jdQ{~k(c>7C=yjgD z5`u1sqrqr{ox*nmbAvaufWiwRv&hl!o)yg5XX~4=4{mtZn$E9=4K%l7B%7~FP`H)v zma@nc_cVUOPiKmStARRAKb$t(1npUHyxBo&uy1vCBx zv&H`coa`-^rpC84TI(O8UDJRw!$t+W8Nq><_YL0AIsgG2UMHl%fzgM0$XqTEUC6qr z#kXFKjcBeixNHe+HTkCL_3ymv06onfnKrkBzDbERFG($)lvDs$@|&o+i^+W6jU@o) z87+Ctt;!mhSYC z%$*F}-S~!_dfJ{VCICh1S_8^@bs*p#KxmbBK`RQGkfGBbd~uOMPix_UH4EIc4v@Zf z&%Rdb#r#!gMno%`20M5OX0p(hvacw2v&|4NYmYn~Pzd}M$ZRt&{lX@VQF5HcpCNj$noxs^2xtVhMB}U3A_uYcUO+nU~!NRx0$vaFB0}e@$75mT~SEV8p7(I&7IC z9a4zInKyA&&114Vi(B5OK1}&6d3Ub5e5=yHll0~nk4Pd}-aW<9i1&dHA>h~Zq-WIK zm4w4F-wnMUS<}ru`U<9}QvJ_r#Q_Ga3KEFjq%M{o{u`E%%;N@X`3*?!9QF6L3VQ-5sZuv;q{bXl`cKr>IIAj$2zMGwtRtmi+Ob zK;nqk26qkbz8-SQ6w(gFWl{z7?YK2y105A?ST1jB9T z#aI^fiL=v{FuujJKPVf&uO|EUopv&Q4+-ZD4?r5^$hmE>(BAd7E={5h9jxyJT@U^7`IA=3>qdJ!P5E@b zn&}4~FSbpvBbvvzWnIekz?(EUtdQ|DPq1>W!#m8KuyY6MLUWFDt z+gdC4{3o{o-NSPzhV9gsust4cqh5Ax9F^5xdI(n0bARoU3sB7C;v|>nZR3PCu@JN^8h=^&A*d$8z2p_eUcPX>~n-? z5lRjGA>EB7i^bkeXRfp}sa)Qkh%+`;~fK!WdYQrN7rz|Bsg!tooAX;MhAIyG4 zmt5V##^JO3K_UCyljh;)#|tITyJc<*=f>-m$BfBmvPZI9Ti!|ndvpoC4Q3EHl1+CufpRZJ`zJQp>T^etC(WYO$OL8H(XEog_xavbQ>}hd~yi<=t$Cx!svyZ&bm$E{CZ# zh$q6Vlf5UZpxi?|zquy1=O%lpxG23ocx8%GA>6pt^F#JI^-azUwKI zvkctJS%lgsb(Zb!Jw-C%QRHz)fA2hNrKQy?0UO%Y$GgZG4nNH%X3{ru->zWy0nBXn zwLvpIxr!B)SBcNe7|6ih_^H?UZ)!I3R|UPp0zRH>b!vDNgXt7YyY9^48`Fj1wh;Ap zUzovQ^UybyBsJJx4Ym&5(C0~9J15GP_)3nnajo||>~M{wQ&%N#1rs9Q2D>8rnws%n zQc)drBQXMAD1cW3W~N#;Ru^|dHH+_9a&=x{UkBis>gjy_%U_*+?0~e!q^!h(Ta>}+ zDyqkb1-4!1E;(#MIa!5xJ}bWQUBCgZ{aZkNdua4~;s%X|Yr>tsm`(MvCauPxQg8jr zVy~*!LP$-cov)bpgp6nx+zXI;PkzI-E{XT5YMr|u;mK+|W!Bc3GozJniNBw=wM-xp zUluMeE{Up>&C=rw)75a`G+T)bc>2A8J^g3Ag6*aUGer#>F|IM!kVoujE;?oOyxl>T zXyjsfX#3s5gO>_LG`s4|sk-OQq5{C=S?rumWy|H*X2xPIyZM#&TbKGUWM+mtE>qK_ zWS0O>rU%T`E*==AJ`9xX*ZiKkET+N(9LRRYbeQ%ke0%#S?rEEy0nBW_B^HVw|Gjtk zO6*Slwese>axg3t6PUBdJ_TpdnWAh$YufWKy1!-gBTuBqoz;XdZp!`r1z|(?;D9Xd zDz^qf#UZ1fr_o%QL1pIhPKoB=gh_7=LTb^w0FXyefhrm1sOYY?KV~|wddX3diK@3}X?$rzR`_CN)S-uY8S*Lz;EsC>VJ$n&0E=SK7ncxRXdmtq8Z<)}&1J ztD|j`{*@_(v;FNLxuWIf7EM$;aMK@YvwJ0ug0A3a_$)Q)y~m%(_eejn%>qKsWyCqE zc)XWam=k@n(mF5S5_?yr-wl zNV_y$UOk{Mv?Lw0IXpO;auVh(ru1aEt!~OcZ7t$7krjE7w;F{)saB+_$nLtN&lHuG zUNVj1WLz96>FKQ}8>sKs`+Uz)+y|Z3=)dh!?rPx}9_e~#0ztv8+vF}?)4tdY$OObsI*&~$2s&q$wSUQ#Ih5KW9TObjud-hb^s-bnY7C$F{k#XW z-ljjhRzOb`DPSwk7b%b{E;l)dWee>4J|*0r>68=}a<+;T$(^0OedNqukUdq+Vre+L?QEy@!WTL&7-9Ij)48eaN>7l>MjjGX4`vQv48iCU;< zM%SV=as9c_&1LkO#;iLJu3lL|?@K_aUG*`|d-2z>)Zq6*k39zWKlST%n~(U8+W5de zwSp#_&;5QcZu*D^~u_L<%)#uhUmhk3)_-+X=-Y+4x_?R7Ln(Y&tsyEXA2mOU@% zswzeMu5O&>8O_xRA{r)paz&!2eOjghAkBJ1Q zcl)E_0ggKIHx1@byQN4`<|L9opG-xvx#`z?NaHh*pAyl#jrv{Z^{rvA7!ip9TTD8g zQo?TVnUPb=+8>@di5WEJ3kEFgZ$W8Z10TN7Tzig4uuQ(iWE{P#ts&p`^D^$n!2$+n z%KHm{x+;*FSK!*C?7{X+tSKrxwTTxTnXYXhPo*%eRP_`LWXYrvk3N$C^hmmX(-7k^ zbj9PL8=`>baZWI+ysloFV@Tv-s#jTb8j|sU@M=tKB5Vy=s|HsAW8U*X9we1u5VWlzpKZ5oZO7oYIjyIW$v>- zimYyqSF%?5e9TooF&PtB?pBm|bb!LsAff8m(!BjfZUb&)xiN9n=*nHZDJ2#I>+_jy z)W4l)!|M}`X*F}M6a{kwbz;0+w?B1~8fd>}0pmzsd;J%(5z3RD3_$g1%&!iw{1A?> zTd~t5L^3fuRAfIKN|&ISx5dbg^t&W_p1=S7;(lZIL%q295ha3umv@oq za=uDedOD zJ2)<~O_cQeeqOXXN?Ft=J(fP3$%9GAL+kx}WFq+DE=E<7MI`xlVcvK0hveH7&_7D2 z>fz1hy~^#HpXfA_{-slD+T-PA%IN*DOoD-{`E+nr<663G6q~Q}T4QU4z9wK@+acNh zQ46Ty-!IONSi0b&Zd*2Tojzub6wnFupXr*tWX*F&OB0K8{V-6x76k4(c@S(6TMape zP3l~}X5bzP_kZ?2b}(2p;98>GLVhP+&(yn2$=2U7+~2!zt=s&m6f&tdrQ%MRLGWqA zFehj*ZrrxDF80RM_vS#eVnlA$UGZr>H-kNNJoUC}FVSFl`OZj6xXaJsZPNOx+T6m6 zJ}KYy@5JotWHj%(?}N+Yc8Moe>n>=D=oSafk8E$WiKl|2>ESPJ6vBL8h#95ec!P%B z=4ikRHn{6%^?mLx1Z^28x+#Y4&>7$9X*xPd_S01)Z6OY>b>EJ z<8}Na(gux9vlx84&e4TNBq}E<0pi_@btzpou-LHF~2kMQTEY8x8MxZ~m3*|F! zWjbu87(aYnOTVdSdz0RNVAglh>p1MPzkZ4D9SYuL5V)}m3f?_DfAH~%E^c~wkv^v| z>oqFO7W6niEg{Q(2KsqXgRdjoCqayaEH;z(VE4hvl~8Zm&n#cxE1nEZhKm+P4o>X5 zC{^CTq79p!?8q66aBtVHrQyYFbJ-cs2d&ji#_7tk^wPF?32oSHpH1OW! z;O0pQsjA1H19}HNI|sHWGS7xjw<%+l>dMe2)YnhyeK(#ldUwCSOYMeB|KU$yT^O*4 zYa=PEv-yje>y1CQzd#%7<~AZ}o|g_kKZuh3TzNX-n)G~RPB8!o_#v>W?5o1v!l|E>-E63qo`E>epZ8GU3Qv%!&&<} zdaToeq^BTNSpR3td8P5g&2yK=k#8Qw*^gv=sA`-K-x1)5BDsJ(`xH!Z{ImP0RlyG9 zrnf`7GLrnS|bxm7yEZ>OH)2=HHHANnLpbuzOCLqmH| z1hH`ZEPSFK(6T`&gO`5TMA&2qB0p%1VFB}5d2>iUW;Gga|3 ziWM!!FlMi(>6r6Zl{sW&EkH+F#*#9YNh#+M=D)wJ6OLnuwAN+u%Aj}=M_T+omR+|^ z!kwXE{M#}8^W*mqU;f7~=KN378d&DPrlej$K>>|!x-Nsp8In{LdGdS`OkPl!E2l!1 z!1;+DsotOZa$3P~$XEngS=?}$o)`b`Z?I)X$2)-UeypfiWF=6l^F#{kMGBBHSIzgi zbym{)zu(N5O#jcL|8eGD);9w{f_JkT_!E%H&_}gO6&00zkgS5j;^HDn?u=VpmeI>Y zIp^LT0;!k1wJhHywY}E{BQ0HF zTJ5BDem|})bf-}8C&(pTp@<33z)>O5h$+Rm65UpAK+TZD{%ECKT+}i4?mXfTtz1gn zNk?qoLT+!Ckm7{uo=fV<0^@BzTWg#mEBz?K>j4jj>i4$falRc|EL0tbu=J?kJAvW= zq2tsq03C4za5C3v5J}4*h+%r$%;$u)K8Uj{LG+{hmd}wx@CzM)sot=9!;{}QC`ex8 zbQF1KGr@$U)Blm@XRHeSW}W>(AB7C6Ym-hkESGIdpTX$)QEz1s)}_Z_|5XeX3>d*8 z??78X^3{A5=51E8WbL|*J0!Eg|7Q$wKrZPq6&9S1xRT;N6%xk}Nb~zy>x!nIAnW7F z^yuGjHM*K;fa-&_3!`}}MH;Mp;&xKLOvWw)#2Yme)OmlK+*zN-&Y1?pHvybD-H((N zF))f6CV2QAAU>e4Y2yzn-p^z|3os{iBf^i65>Ac{0oLZ-t@Bc}H?7L5*` z@O?aljlIiYPcJA=$A zonbg7$=d3F12xNi4gqW7k@i5J|Cqn^i@yIG1Py&P$`(2it$$*@=CoS%rK^4Ne2AMH zumoPMUn==M8_fq^+c;Xng#lYClr2;=e`U1qeNL{D!CgBt8$W^e}&OrrvXp%}Cl&tNs1(1mdpPOT*gQ*5Q8nn_Q)Bb#IbyoX192d^7b) zdhFW13D5R!@C^nQ9FQ+85DShP$sIK;5FplLu7vfCF^t2&uHB#8dU$#@(A*)S>;3-H zs-~d9zp8u)>o+GczcbT3$K2#b^ZC))*Pekbmr&wGEoJ>!YK#hg*FDm7ms5Im%p!rS zjPh5qidSsG!nc`0_NK=LqJnQmizk0|yN$R*n`U}$$lU<7aXZjnq4sK7PDgh7w&opX zEIFzaV^QplzFEJ#C@Gk^;bn^&1r#uznWGthhQx!?GQ!~2+NT+4zO z-?F3P425kpbW(wA5dCe|HEhzUW54Xv32S)NV$4cuVOn{=plk>KYgHe;k#HkO(8cyC_?}?*aveTvu`n(uJooJFE^BRv zH$LXG5|+;wMXY=(%~vw9o(VCfj}YTfC_;>r)Tn3I{?6&JLLycG811v|N4et?c@>t9 zR=?v=L&&74S@G7MM@Wek)W zV+o%xS2gkr3!br;QKH+(0YiMdQv(6mw#^7Fso1GVLX2FFpMj3!f0JEN3{Li5sra5F z9R@_Y=`QjfGueNBqov40slGDbK*N#lWo1!cjne36|GC<3()Rw{zp8|`{bpW!+V77D zpzE2)JyF5_6|?0ItU^h{oZmHiKhPpm*_6OKFPp*n8pGMkN_mx+7th#nICUdvzG>vr z?ymp5Nh#4^=5Fu=9Optx-D`n7W*SkYk63VRo7^oXN)Rg)V~Gi0eYGB?VeG zdSnIgwZv}L@Tce7U<>-d0vu9m)oNRHVv&8_5w(l&X=PtoiMtgyVf-rDA29+TtCSPZ zuEnk|C+cK5d~R0Hbh{T1CZ!ifYhE5Z)V6{$+qS|h^~}S*6sfHOn5c72 z;)&NOuqht*!oHuaU(r?OIx@a(8Eg z-&@9k+o#dA85YdZ2?MP6gVL(BX4m=U1TXGOFca}-_4P9EhI5Zs@GHF4{RfX}&7tEm z97G({*;bvq_ulK$Q26-0FSit5r#gOHl7`SY;+l)DP)Du$mU!Dz91}M(jI@+J{O1_f|!OOu% zW*rkt*M7af48l%~K%L{R?0wzvxVF;O+wXDn)0N%>dpsPPX<>8=WwI(wy7buk51NCO z7Zrz9#rmfWs*w0I2-4!PF|hYITT2*PXL5mnKV`hd4YWz-6d_=_W?aU^k(`_&cI%{9 zSwRWfj@UYak5IQ8$DtU1JF_Lvf&3Iy~TOQJ2ZB& zpIaZ>sx6rfF5noAF^dvm98Hf`{kgCDo^5h45b3pdVdDtT zH3UO6SgxTS7F>1?m86m)ITcbIjuMDB>!%YQcK4s)s4;gt$%HS^0a{ga-0>*H+vUhx zQbSffuRO_1`*_1&&;vR{CfM-wI7!?5;&``<7n8ns>Spf#-9lkFymoG>6qJ7ySA2I- z{K!RPJMtIQGiat2Ry!Ey{N{8yqa`T6_T{L%c5vQxt*gt`s;O-|aw_<9#%+zOr_?JZUv*`7nWhY6vDoNZuh_2;lg1 zdUB)8D(~!oOA0f_0&*e(fi5=o9|!?GmzA6pGAVR)Fe&y)-hkO*X=JM1 z0<(rkY);2`5^dOSmjlmv(>sRz?Uy@iu})h8cF-l53*v7!F|{9F?}^6o*zrBR*=TE~ z9nMmBj;bt9d1ymYx@?rFx^@Mdr4&Szs*@r9MX4H_3Q3*uzazbW)>bTg#sG&i3plU_eJ7T$ml* z)*J3vd$LVTS&>csA)e_4y%U?QuGJ-v3zYv(D7;h{53h zoGsuGTD3m7OI#<_5x8B}Pyo3KLi$HBe42hGdC}|fsVgj}VP*A^Ag(1C445*k9fB;& zJ5`!>(^JeTq0Mj@EGoyT1AM) zQoUx&Do~W2BmFGdW&xP+4OlN~X~(8I>BDkGnnC8}&SsVTh^S4m-L{sgJE4FiYIJ;{ z*!X|wnRrxYP{S}e)QZj8ox+rs;?GH1)z;O>?1%VaVWuG63n^@1fKEgtLtGxbjlsi0@Bvp6Elv{6E@Dy=%I;nYC~2iILWGwU(|?~fy`qY6 zi4;oOx>@&nKUBH9Rd(*+VF8Ed27Tvyg3CdHzj}qUs@T?>zq@i+s_ARF^fp}i8Q^=# ze~35h%x2`!xG~hgDh%nw@nWF3<0W>%TogqODBSmS$k^SMp4-Q08g@fxvT|O*1`McI z&GX{dsq~t$8`%CCU}>6_-5(9sU@=mg9kaeof+>UIYm@4`sP1Ncp;PKRgxoko_%XJRooz)w*b#=yKN_jl|+O0 zV&Lliu29$b%4V~=;bic_LR*v2?;fUeV!CD+B$Dx6ovUIhRM*>oH^*_b>hETUCX!w6 zdN!Trs+3A??7;`Lmkt@|JB{Zw4dC=?-@(aF=WfJ=;`;%!=)gw7LW< zt=5fpr!>x1QEWp^vFtxRwL)E;3g&$~TOY1n&G`cHDcyu^mRc&IUS&=8rw<_n06f7` z{UTSfgMHXff2__=RjW2ym<05YaCuW{Ya8%a2b3^B78EUHr`AN~KPFc}O%e0g9wr}C zzp;A18UPk+I1Hef9M{@c15rgCq$Er2Y>rwEX90{7jb9C*6-g|4<{2Mo-2gp-6KR_+ zq`>{*a#jOJl=z?y!a_D*P6?}(oD(3`hP8FGx|FPI0>IJ5KrNmTs`z;M>8ySpT67iD zy7kYzUAtWapf6vas=N)_N?Vl}unTwR2KFg^Gw$W6D}9r;;Lbu8au*PI_Bzk^Oo&g0 z>$b6EEotS%wc^A8U|iWZEjgng04B^ftxDm7>Y^r}$KqK%$idZJ)+5EY_QR1Mf+0m5 z&)or3qsVklhmBS@T6+4kllij6O1(C(>q5zItsY?gQ5&!iL|~Es>n}P%xulSf6@>-r z{R`DAwSnJbMn|Bw{0_=HhIJhHKYfocs2Dy8eV)mkQt{`4?GMK<9J`t^_A>v?S0}0z zix4>a+a}LB;irIB^f6HsM*ybx1oye-J&x-O`HB9hb8i~cr!mA0mDY$|C&%puikDRp({7x}fdGTxac?RAF*YIxd^Yb5ZkqO!&SsVXYG6f| zK0gKh`YSCsZivit;6o|uM&dUQ20b{^)dpLjW)N9HJ0B(>%=vm-@z7M+hn4)Ps{C3C zPK$hA=FP4qy1+|rE`B3J729z0rp|SVR>8c6)6?l4ldE65nl-FxrcvK;isg0--ZLTj z!|cRqBtH>(BDuxS_SDRFmt&;plO8(s;74V^-#v$o_BBIKnP5>Qz~4|YA=W&HG>##% zDP}h%TX`Q_$>x20vtEmpdN=r0^FZ@EZZCJ;cGvdwTmofg?H|n3!|fZh*d1$hqs(p_ zrJwDS@Sj=px08dzQ_sr!;P?_Z;S7sjp)*dvAN&vkhUhnu-d4AAu5UMR2NF;6AmlXBFOV zG}@n&eDNOi!xJPmesPcvo!mPd00^zz5oLp|E)=y80a(AmF!KnLijAS?pM+yA{cB@D zC0x$ZJF_!T&C3xbIL6pyE%g*ji+-xbHFkvTtPi<2b0NK*D_g&H;SJlRcn4esn%v)_ zc;H1kNB~&;R8VOot%Hg;Ob3E-(2U(}4G0i#tQ$nmX}>0v89NtdG-qvXO(qegT9#8; zxxp`?tb%nKNrj}0W&2-L2KCQ#AeIz0IJw8%0sde;EwpvmfjYAhBKK@Fd`2I^lz}KY zmaR2cz*@W~S>EU%kq4K1bEL|FP<;z}rMZ_%ETX65wVc5zwpxLXzF|$GfY}9hlR_dS zgB0>;nFvJo=Pq(pqITrOWLo(|EyP;&xeIKus)O;R#YpSnJBJeO>Q;`~1(%|;Q}$$Z zMt%%hDg^_#Esq6@x>U#q4j2&tUS3A#vu3WVWnil0{pTye`|%;fhwYEoE}P;7J*$mw z<24iE3K^?pKy~hd?=d4dP_{bBPm~T~jQ6AVvS-RxXWq9GnqNdNd?=rBB)Go;V$x}B z`j3(&0>S>S=$>;@CcWA}dMwZXfE^m1$Vj%csvjcwNwQJb7Ms~Ty@Ck=rr)h>HU`Cs z(bUs(XKj$hq!>i^VN&`kHc2WUjC8Yr?I-8S#2&(8w4Mf0BK3^LwdjiDJFKNRXP7QcuSc773=H0Tu^*e&hZDv_2@t#eYaGSYJH>43v#K##`rRXm>l#~@(v5C!s{a8%4 zCtE@ope&;757;)($_qK&->M=yRnRg%WA_hyuwblo`Rn4m-Y{99*Rep4YY4ubhTe{P zW$6Te3QyzNL~ZZ}nG-h-05QavOK^}V+fEXT#LN=rNh$zaS~VA;Kr}kLB|StTccmE4 zXtSAz9}9|L1U@-xF93?oCQ@tq!dy`@IiVA}=QsFXLDPkU)XzNZT_WTS-Kzsrhn9&7 ztJ&&=Z5yBtq0Xf_hxd{{=yVBaeoQH?J?!)R9F(2%nP73QF8fP6xy&TtJ3@O`9)*GE z5eN*eiditw50iGVyL>xA8^*}-vCzIw#;zUBO19CGxvqs(Vit?npBe0zSEoE^a|kXL zIVNb}q7-JIO)lX~uSXE;(T~VAOmEs`2@6~&5bII5{U^k}LxdP-1;d_AF&O2~-gf4I z$;&9Vjq;P0T2*Fly#2?#r}zgy;aI|e#>-}S5_I2jLVY+u%fs92!a2z?KqPViO32CH zPli5rG};rk+acK@z{IZe6u;?fa%=79w+x#n;ScYoGLN+(kGPzGm?ZW4y<_HEC{pgO zip<35y^7OW3(X3@&3M$Ix_e)r#vUJl-9~FoMIB}-ZP5@bLr9)e57vh|#;^o|mX(eR z*Zt_xce`V9)Y(h`pumHq-r2v6k!+S5;CSpF{(e(??f<(E@JxG8TD9V{EEWd0Iack6 zycdM|(SbyVC{2b?P*7AWaxg~cO|$>yszZ9 zIaf0#Y5YDcj0URul%+3HG6fb9`s~mFwo;RVr&*I{FW&$Qujw>@RYCUl)B#38#c1eO zBoK-F@t?%~AKA@(swFu{@@-wUtcnBeUkolp=y5Ccim(?L0S!ny7@&9Mw=m$>gce#{ zu6^?TGpq;$I2MuWU7}%BqGR@Kp)F;Kv7jRqfC~5%bCeWkcf!*$d5c*ajK{0F_D*cV1hJV44=nn;wHJ-Seq(^uV@5sWRK0d2rJ z`C(%HQhnO+SZ1Yp8dZ4odLw_{20_`l4rj_1GgbfBuPy&-_5(?QaR#n z5t4QNsGvDB!s;pq-sye)G5Oi;g+@9m}O71yF?e#XWhSOU1RQs!@9EzV*-pWaJR339l0 zP+D9s%kylfPSjs>0+e0&Hy@eV)V`JZE!Xa}mrLpwFM0%~g@hE+0k%kXBo0RbLJZP7 zuUux|_euqMEfk>UHad+S>?O@@V-9I6yl3jtsjs4KOFdYFYeGkH<%Cjd4$r16h5g0_ zy&f@RnSOW}?rubgm$7(g{XyRPn>EA*)ost^R=5+6+CDBpy^IRem<|;HR62%p!?8(= zaE2!3?Klytm0VcUptBiGT>v=8F|c_!-hE^_j*p8w5foDx1puPMZITk$UW1FacEHg9 zPkEx%s$jXbj!%#U7Pn3Jdv5`N!1q?h*VM^(uZ~6Dwm2%&xDYC&yYK&sQGvOnfPZ3y z7ax*#-?8fD3KhoOdo*dOCOj%TK72fQS@}wmsR8C(QZ4FM0OQW&Ps%3%w=rU$mU^6v=bZ^{P^TUCy$Tx-?~? z1&2#Nw5%&(qw`A;Ym|XQHF0)!Hj!XR)>|Rkpi6wzhXJBbbB(wqI84_{TOOGg!fJ#a zrfU%$Ex7wQ|7DebxAkTtv7*7T3CKZ1;*89D&~yBWD>N&P|6V$tD`_F0hr%8X@GGc^ z_@u3H8))qk#&4ds@3?VwWZ8ZSp$HMCWi%`ouw$F1#XQN7 zEW$ULVY$$E?a=sTm}p$o0dHTNg1C9TcIeLC`~p35Y>xguO){@BZ7)xvU7z}VSN^Lk z#!A<;*vMoEOGkqHAeRRQ5PhNd9Jnc%HWrz{Y5ygck;GFAVoq(DWnYI3(*o|R?m^uU z3IQt|VVP0D@fmq@G&uDynF9h*$va_Wzcj@=TblXk8Wt@U);dgibJQ(JM9sgZ#-mp0 zDQeCn9redv7O4oSGiNFNk_nSljhU;@fQZrE26g(KfKGY7dtz!3=WbD1t;wfhWJ1cO z<695jI7|e=*FW1GOotmUZ@Rb9g`n(qyOKRGY>jyIS2)}hZaK?sSADOu$qZVdt|oCg zONwy($Q|6TvM8kVzp{z9rj&yuke2f&aR73tn)qwxXrHEmHdvJ44Kmw@$cxLZW z+oVo>;!#$az1+^U*>XDtzfQw5&vo;xfw_gu_tvvvwQ%$a4gyvGAjq1@nfm?9))k}P zCEmZy1;?!p%7aGV7(JgpAf>GTI3|2gUq1hmedJ^E7?!-~rFzFrOLHU&0WoXt0AiDbeY!WGh~slyI+yKP$7K-1G}p5WUp zxrNMISMp-U9Fk~b>shdgK>8wb^ohJJB(;Hh4o59H(TvdD+g?&-H(={R4+zY-@m~IA z)bIpZJM47%E+WYF!>wPwyX-;*Ol*A^=& zj8H=3OJ>LNn5bLZej7g?Rvdo7EM0fX-{>?i_0=CRElka_+*TVrtXc>%uxiwP<=4UC z1Oyav4_3U<;uIJR!5XrJtcyMwrA2eXD<-7rR`E>%I5;mN4sFq4&XPtZH61g#0iI5j zRgF_uaJN)h9yNe!?L)rKV9Y|c{VfHv?f~rT0k*|UJ1!56i?+=7EHeHNVm%wb{&kOk zehU(fD`3G%c`QJ9+|si9oVtRdA`l3Kj>nRZQyJerWD31e+QgclMiGu7m^SB}Xqeb` zSCl*%sf(hCdHJSOSaCe`QNlU0{qL54Db-L%$**rn*Z32WurCUq;k<568AfpuzqwCm ziClVmrEX62k9{usrPu=6bnpYq;1$Zh-ChvTtCv9moc4g?`UnQU$WAsBm@0whKiEN` zf<6Nr1kBm_J}wu%L1w)hZof6{m;W*mTpBTwWlI|Fr}}$17yo*2`St79)S`o?^TplS zs@wZYkpfA-e<&J}HY8}#`fpQGQc_dfKR@5_dR}?HNm%;)f2MTy#Ka*I`CtAC^2!`< zto%cC@>k<52oZ;3pa;s<&)P^Kz4Yv=Ae-XCbhR|aroxJGw6zamYoyWgM3fOz!k+{S zvD>f6KtTb9u@ty15#0yT@L;;5Kp(^{wXUYmOHlsgm2Yq6HYZ1;ER6loTWr2dw^&!< z!eEQf0X^&Ikm`TmY~N8ddQy+8=O2HWM`emIv?m6PfL?os{n03og) z_~)za{Z=_LIB;P`NAESsR(b5d^VH?Gt0SHW#+TAe zk7}>xwi73nib7o@kXpX^`cH)fys$8FAsn!)*teQkrO`haotVO`y|J9G{>*)%R(a7e z+%ob@YWDVwDOdd1YHIWcHzZuco4QPWBM2+w;igFvQM~;6rp`tHX(LjnLHY$N4eWQk zGB$+gbQ4CcQXct)p8YAscIa_O_Qu(J^3!+8Xh5rc?HVeGfXzd5bhW&`=t+s; zy?o55^1T!Shw?4)K$c<2REa~qaPP=)DzZl{MPd*SQUnmF0i9Yv5TYoEc)ww_o%uJIWQIMDta z{*b!m$)FEz3Iqw1O1xjY;&5$7+@HdcZ)s_+`7oG&!ru|43A zJ34>Ne(!J)$<9*~pjj1$plg}o5Tigtu!Dcx7}*pZbB9glKdSE-_Iu8S*SX!?tP00A zol`34+=aVT;-|PVfabE ztfUAb2N=KpKf~u_(8;$RJ2RCWhW~L_NqY61MB7&47tfbDi;m|GUgveomx zYpvh9ja-lfyOXASp2^Xkw>kcE!yL_Tw$vy`{5%RH-ESJ%v4a4E(w-n`OWzkn ziym!#$!*H$#`z}E-`Ys!wZ9KY`KUgLhMsl3^o=_d zgbMG{Q~0#Ae}lhB!LFaxmsy^rg>6UQoS+IC&P|A>K((GtY6z(=)u6^y>&l&=lo6<& z<7`6#jJ;Mp$WPhkrZRCmJ2VJ2*u1%7XaAbPet6*Rlx$KBri{&X7AktC7V(ukCj3dvlal%GHyT4uDs&TtaK&UYNO!1OQOcQK_rdrJ_gTza7kxStv^8!BZJIrCNB4U3d#4QSm?COKq$B#qj{Nq?~@Vi!{+pa@}w+a@1|Gq$<*2nh)Zy3MlloW5%Zt z!W72OAT$g#ql(E2#1cZ*KH15j;@Pj~`=G1<&o1ON9ZyW-8V)lI& zi?D(J6Vzk=e*o$!FZ|yG^+deu<~5TrBUKc_O>yA7t~Dv7wG$5V`(X^Zcp9h;ARo6x z(FGMh)LfhvO4a;5Tw8Q6`48m7OCZd?`=Nr#DJ~goad)u){TN5Q;OeW!dm>fO9*Kai z?c!C(=R1K4-cwCx z8QV8;qrn^(?Hq}Ox>$DiP4|1BH_!5s{};(u{GV;r^Zu7@>Hr~@Y&!Tgc1I&yY zjp2bAId7x|5E2DI<*VPyv8cO-CMcKj)GQbL%0qJMp`^Yn$m~7>KEbW`DVz z!pOTNuLaFT0^`xKb#Hm?eOBr!NEGuaWA#Q5lK=~!dk6^SY(BHpyUC^lXRhNe1P}Vn zzX$ba;2a$f&8YcosSOo~<-U~9EM(KK$gU0#AUz(-4KV&IpczJ_n<1QwB8)T-!7#Z7 z-uEJJ*M?u>u@u#m;D@5<~HrWAnVC&Rtkvsp$Yj)kN`a0KVV_U2-R&{N208U4h% z|66Te9TeB|<$Hku37Q0lAi)Dcf(8f{2m~g`U;zdg2<`-TCj@ub;O_2_;O;uOySuz0 z-~Dav?%S<;?^V6Ne=uFQXt{mP=bU@ar~8&4(vbd+;WhGFubK}Wb<(+L8~vn)vm%BK ziWnV5AcRaa9PIDIKSRyd12CGe?^*of_&}vyxpzssKVT5?pV@sUPvi&hxby2a(X}HO zQs%DdEcX_&r4_z?c8fU(vWihNf1{rl_ul;awfPTy=~Z!YI;i8|uPwK}rr+!KdkyE! zGQs{|_nJ;G;=kjv)x+(Hs1H5b#HYWeg$ceQs<_AraL=gX6QnK>6keUo%;2-`Y`)*! zZZ<$GwF(k?usML0@8zG+06$(kRdp(91MedhvwbP~9Cnyei?t5Z;+>yqmh5%Ze$f`P z2Zx?k6tei9QnzE1BealYnM4)rE3JM^QTN1`{D?095Qxe;cHy34EYlfzEO zPA4i#SdU8@uy`jR(w*}Ip8li+XwQ2(C#>!8JS(2?Eb6hR7-u3!`ef_oELV9`x_&;# z)9gdr;Q76I9-Z~!?K%?t%=847lz_K}jCSMZ{qw7B75eZmx%bsj8r`OzN`4^&m<~$$ zPhJY2#n9Z04+M9(f4E5#!mF$W-9LlLJ=tW|m~`%s-`_X9_Gp$#${CA2-`=i}!Izqx zFc>et=97lL!wFq=CUGoJn~p}JeMlMCS=<^9KrOEPfDLS+;UfDmO9If#)lyR>M$j2aM=Gu^)s`MXlB(6F~7r794Ll3PXicJW+=YPG`dS$ z++2aCvM>?C6y{G}rR?xmfk1Sy#K!W;r?@rvuFmA>Qm?gL&V7@{^ScPe7}?zElpnou z9=%-llZ@;NVEd~-RpsB0!o%e>pZ^}~UJq(#w*6Ar0sHPRAe0pm#|3gr`@U@mwsoZK zm^7nVapwtvUOc;TXgaIkCN?LnNjUE%kJ{9)u^2d{)sVB)B=pQd^4D{&Iv`Qhx=fBj2=Aad%#Y!#-JjWzYfiQg(|icSS-PrivlLRFb=K_Rw@`bl#pzmBC4hYOq=`>DBrWI&dfR@E>&~5ok zuQfr^=Hq?hgshti^H0U9i`9$)m6p&h=@6_&bpJ6pE9~Mi%NClpc#{72F9rOKJ>x7% zw5=u(O>^jD=edpCPc~72bth^@#-+9Z5xC|OfD)C90WCB~cebVhIpMq468qli8W6NA zOPgSHFZPK{9en5z$xIrThObR;`oG&+2 z{ASr#UU#-e6LdbuTwFVhKLKEPwvf}-R^X945KF*$oMG^O}7IbK62r;a`yxt zXZe5P3i2N?&#n66BOE63XmBnd)rI~zna#($Dl;7`=xknj`q|P|#z1S^w<(<`T?|Pk zM2|)?!9?NJ-C3Zze*5ziJ0`R>yAD)Ja4P*2=+Wzf^FiUebmad2Dv2?)>6~&IrXd>o zY8P2n`ve02`kO<1bLG=`u?faq$Y@wjE({xBt2dJ5Jnmb`IAcfFdimp_wc1lp$&MIT zKj}1u>glYtWn+InpE`f_vLz1yxF{prN&8`m+p{endyZuHiDgLzBa^!@h}I@sA`CH5+w z2l&Hm#hC$}+ixHdZ4sAL9tp?2v3))_&ZI$j=6Ht82SSTkx5^7tdT-!+S`heSFsJ?F zHuIe7B5nF6hRUri{P8-dXn33iW*g4yqge0MXo-W8w#-+x1ig&4TZ!|sbFoqjWCRQR)kK8h3e2Wk`v^UklAq`Saum$AV<*2Yp12=Hz<}~%5LO^k%&asY-XV%$$E!_JlLKHc5{G%G?nCEI-%=4!d30#_w>Yaxd>5{XNnW{ zPon$o)ADZ(F#&Z2vnHh2sV}+|!uScHjjr35dd-~i!Le*_cV=Ly1kAlW5m9!o?1a2O z+z9~;8yiJ1L0|YX|G+d7w;h4-He64`XfxzS--U?7$n=pl zhy}5S;P_-pqKp$H%|F1y)`I;rRd#a(&3%$@bxV_$w+ERFOx`~Qjz!zVWXLn5()H?) zTRocXBwFr!?o{CZi}pK4`xr8J_2yfqBGXUB=FX%pM@?lipVi38mWXiF_Ii93 z1gK`IL8072$#?pZ8GH5mnrm<#7;!SVI?+tnvV)%^-ZONs)^y{oc$FWV%QrNnJutsz zhAd=%BIac#iWfn9Acc#d0C4_(1Wmpa{oewh=RGfO-{XR=P%r1tj)1 zjVhp{=#!>g#L?;ROU`S#jYm3B6$*n(zW#3(P$Q?3eBM%p2bdd7tmJmsDWv;oP*%aI za%TZm#f6{2@0)55{vE5Vq55|4ZJ2(v0=;7C-3dxQ{kIfk0Q>1ko2r$wk4j>{T@qPlteTrOCKDx&|1 zY;+uS1FHDWCvBfPTi<6qB`Ma&pt=6jS3q-!bsV>0U$T8e8@;_nve*yvprnM$29=^V zWmay0jhL%sABx#1Q0UV8Cms>bQT4n&NdNEHfd$vvxExHKPDm|Lv4Q$Ta6G*8Y^aof z-c+!#?XR*&Cl%7oh_u_dSGgmY+VO1$2;EpHlo#2LUhWO|?JwVkJ#y26nsBNVg@c~} zeVC5-zi!pWeh>ztS}3Ycja)Sz$c}md`|~WOj;^5|_ySPzM4r0Q?Cud0(KC4ZRPt~9 zKpb4c58rkQ_j#(}k%gNITWTA_Zk%>(td%|Py0W;L@g%iO?kmQR>?>$R%!2(NxEjr9 zH128{qX!gk>FZe<;v0@u_i9#SYeRZvu1oTq^3RgEq;dLr)u})G5!oGKfIy^CYab2U z%hP4#)oDJZOE^jFtJR?iRT)p8m!LHJA0K}K=zIEoMUtj({LQmOAEImoaw+mN9J`yD z&eb`J{bmLeBJvP-6I`RQ;FR09=O`@uZiv~+=(b`=N-wx z;XUE(ALsj|=WDHfd_9(jTi zqcA7yeFC7dhc6HR!S$8{TN=qZ+lpC&&u*}FX@ZDzlMij--WPUbBWFXa$gvv&g^DFP zVVZJU?8h)&o~(gc=)0cV~LrO{xSnI6bXb0GPCs@*&tyFQI3=k59j-WMY)a-NSB~b)n8;^ zT0CEBk9zXc$KOK`aRV8B6qgg71JnRBw(o^z{)~%L%gB{MgvkRO6;qY;7=IdL9*S$*gDb6o(l+fV{L``vaK9*9*;=Io>`OjNK_ z>GmN?t`{u2{1#8kbT8~Q_dit7KXGrssC-clHS6MXTOzkjk6KS45~er#CjPQckY6s; z=}UwYn>ydfY(ddSXRlgzU`)T-`Mx`Ac^11NKgiHnS|tZ>?MHK*k?UK(Bos(Y%t9fK zKK~H*`y7GzwAd}nV!@c7ywAj1i++Mb;n&a;LS!OjzdB0oK`u0JJ-{Nr48tJwW|x!< zfx)8ia=*r11B;pEET*?eG5Naismz@Ek9LZ|q346LNm=dR1(BX#Vw7=|Ih|6RMd&q? z8tA8jldUiQ6dFyPs??jVzhrUJf<1gRS5}^k|%X{@&J2m(rs@%O3qc(tCaaXw;_)@cf5KLc=-ohO3QY@97w#IT3$Ub`Utc#&RZqNrIts zac@DqVZVS6EWCJqYW*r=Sx2&J=H`u-RAz#hmHyaPz2LNhGfCUhlCTx6-vREoe5RKR z4Acv^+QGm4nK=ccQ@bv^sM3$}*Phk(TrIB$TS(Z@b0+}T)ee$O^!CWeZfPY13d#hv)bsVl3CHBrx;oFd-%A4PPVi*H|MKY80OaA9viiBnomiUQC$T>Vg`WHDU81X@Yfy@MJP zSY|ugr;wYNPi$9tnwps249GSWtZHw~O z2EjyByCH|2*mqtPloqpriMV~clT1T}NJEZe6CO2-#XPe`wBV!NaaI>KhIf4f`nYe};=I%sy7dib;pEq_l7hy$?>^3i z?L1&1p~c#(F(7GR8xfgexV-(+)aTa-dzH)-qL7(Z=R)McVCF3cV1`XpG;->sLF6vZ zr0kpBsD-=h`6&6Anz$S&j{x&nsi#OE9S~a7F<)f3n(Hf; zL{Pr2;i`9uN`c)f>anpJH*7Rwl7())cWOiiUd%3Is-HvCcoBg+vWYjEfxQwV-l(IM zS=V7TmJO2?UDja_9rD>#sC*aiZlj4~2D6admDRYHSC?0x2}QV2WDRw_T)f-uM^KY! zHZkb&`+c~z4&c7gFYdB*^GL2`r)O+J1lAFgM9QibHLndK_0#X25|Cs~WcrL;+2wRdBajd$?TF30Yti)` zkmv=Yi|YI16Cg+}nI}KiYtG0G^wBjJ*2>jyq6uOKG*H}2v5+t;Z}%o~0M?nU(yZCJ z&`$%Tg05!vyTyA319}^s^iz26;3k8sUQ|pt{n31Q!VwQ{KCVrucIiS&ae?8|cTQ1u z=Mj>++xKg6Q9=i@`z#i`EJ{O;Yyf|_$1+Fn{VL05}5a_qUp;$ze3>GPqrA})$yrN#qQNR~p2 ztnUi;)WRp)|H4*wW%C&3I9fgq3VU zW#^w6NfTc|51*DIupJ|6Tv0NJfmehOv=LUEIDQ;Jc5AU3J% z_RFwRkefkLGGdLB#7~2BE{AWTEh+Ja4EA;BSrG}#uF*S<$iIRL34x-|AsLL7Q9_wn ztRUjE{n(_VvW@_DCAi?pz}_fEA_>(}(i)8??sho&-er6jL3XEf8tnximEG34S(y6B zeOIWnI*ZaCbmP!oMpmWu-8LGerjZ)~Skwo+pd<9&hRY}ubv zhBYom4P}QW^3@a-$7r!mrzx}JnD{iORxYY>Nk#1UYO$9cS(~`9YV(+Z>7tF%LfW7s zHJpT^?vSLq{ekNbY%XQZpVUrmlaNo(oJ^N)Ww=e>G!sW$55|t6G@c|qNj1ZGF8!My zevHZ#9B8OA)f^73g+r4}(-rHPgVdQDZ@%LBvZ!&YXa5ktpc5B7H(2#qxX>ZTa=5*XA%)T?5)o(%7uheOk#T)kx3uXPc1>6LYnWx&*T)P zhiWs@^BkJMC)Nv8s+`9I+Qo5X9<$QBJ+ji)cb8`9BwABx0!t~xiB9wuDgZ!4Ddm1_g# zh$p4O(c+nPB7fMn5@SeqlYNHMzlu(ND1h?x_UzQA{`vf7pvX_vLLc=-z(OqJ+>7W! zd9S%PtNQ`aK|ha`V$sx{VmPk_QMoM<(G_0CasrZ;gmzJ+E2OCVtZ!V8Epxez}7~%s_CnIR zo{RdfbDRE;6y?<7r=dXy6L!(Q^}O}5XBcE zVQy_e&<)#zV&_DkC91pU=`JHc%frK6+B{Rw7l#y@)IZWK*7mx0{>~sjyFPnpDC;O= zGT|lAK6{>M-mS_t*yFZ0@jPrRTAzC9XUlKF=86L(aEo$zybJ*kI?{UG-py7qa=_ev zmDOPk*%_3a+PW&&aFlM%!dOp!^$CU zg0ygflX`^PN||?P^Qiv_92|19AT~BOkc(fHy1Ke55C}FQYbrW(M2_~=nXjS8#>tS= zRnw$~C5N7cC?1IppDDq|CYInsa|X&cc2Z7n>G7ZXF$TwumHHCo{(fKWu$O<3f3)## z!TBz+)rb=Srg>+z7!~$OWRg^^$z#+jP~8%z@K#w^EUd0Bezj!B2gTyi3EXWLLt(Dg z=QSrm`Wg#Ih?q~Yq#S1brvcV50fzM(j#!6Yy0G5FObXm#hcA>crv$L~J1Y-?U$tJK|t_*09c*U~# z{p5WoCcI7k*ViUXHlo|Bwfg7H6`j zQbPuJEoAd>sfKSajvT!tnDsTGD+1vJL5}YxLA;mruWjCXwpu;hZM@^EIlV7(BV{rR zWn-t@a3N!?SC)fJ?TTsK<~cB8BP8#5IrG`NGrT`MuP*f=tf8csO26W~;&<4R8)tf| z*L*PB*GHsyyL_|)Ga+|0`DS1D`FMWmGFV_nMpU$48#hDxuLIe;G%ZGHv>+-!;WTI~ zI-k5^E}Ar6`{oy~m2T*t_K5~awHd6=#;I$>MPtmt-bZqBn>4p6EXG=N?>&`{%WiI> z_t||v3>vbDu(`BYzAng&^lP5vc`dvAMwa(XcusW|rd5S=jvj=Ioa5*>brikDY;Y(R zX{tXCftYIrHj)>eOI_F6uXR-o5QLYB@hn=i=UO7*2bUAU@=M+DSIh_}R84|0+9ncj zC@Hsx2$6HGG;vLc(>1flJe}llUR3&N6b0PN2W9#nm7`9QZ@xr!wS8p<20Jy$^vDUX zzIB)vv^XDG{Cr+NEUUBGPDEiSI*KTOXfmAQj zD}JO~B+rlcFNap&mSDA6_9bX*>>BfIc6uP-Jz zF?Z($nih9UD<{GFC`^1loF@>`F>u(;Ead$J$%OPOh~De2dm}LIsq2p|>_!*><21E! zK>zA?Cx5nny3Lsko2i4qAJ7>5RjP_hO!%=M16A9q(oMQ8Z;^Ig_kiDv?Km)BzXB#l z&YK?)eX3?f28zSdPnI58M?1Qm%}L+WJ|eYZKqWTIzoD1+lv(BO*y`^$s^C&eNoZ`E z4(85R)}2|gLCP1;CsP(f8;pYVL#p<%A1A>R=xOQ;4u!8cZ(uZa&B&>ZAbyN6tFfc( zoUsz#;_3Uye(u+{7g(f}Lnn^o)0=<3VJ@!l877IEXrZL~w&Tq2XxMy5m5r?3 z3rtp+iFyVPv~L|(?V~=n4|ljtgZ^pX8$Z!BlCEbtkK#PEfXvKrjAVd@DaP=yN)@7j^zrB$Ic%97D_tZJptuf9B#M9!Er(z8aeXN%wF%v-z+s8%&y@Q}7`gHs zqS&q>K8T)+xvu+CxL45|_y#~L4wAaH6WE&ewIl*ilOab46MXznM@>THM@jCY7n?MF zE0-*X1Y;sMcP1Bh!S#KqMDTll+^W?0fHpqzPD$Ze3rNA-PgQr$4cjMqbSzjq-yDD5 zGr&WF_fd*Z-QS_TgikH(dmw!Ml@l>iAAWoG5`lePc|xJDn~{Nu_2`I{GYle{svJRj z7aCo4BV_OMovivT#u0fDd0QOd50Vj;-n37~5a}^_CHy7hkUs3gnH({E+YrLaN7Jtq zmYXOFE0Q+PhKdXMMBqKDKTX%YBj*ma#b;0}q+7vAX1x#k5 zKetO*Ai4bI*z3peJLvKGeDMaCsQ-Q1X~8X1#w5Lxj7DVkm=|h$JIwpD>E~DI5&`L} z-Nr)#Fb@?PAl4%~=wl0LCRv z{+$krSVBmx!k@(rs8oG?mb$5I@GUN@p8i<(B8zTdj1?yqp=Ew0&Z+nHrr=?IyVc}l zrqW1?2vDB7FdyEwc5Op|bp&-pH2$jdq_T`usIu{e_mYDS$85U4Qjv4Vem*1SF@yn_ zDMoWLBx@fWK9%(_ZvW&?As?oOEW~->^^_m%Zc~2bK2v^IS6|}0WcP?sFv0&YzjU0$t{GIJt+NF) zPDcqyc?SLlDM9H|pl){1IFtoJrRt@7i!im>)|LqA95^8X>2 z6JJNi7?pbyqpyV6DB^;lv|*SLbF&L>M4+Z4!;q4TG(=E{P69o@8!PnwD=9w%hds-Kgv>8mY7NVN?*!9_2XI&l4S`r& z`8ZgH+oN{m*!-LBBfR3=|IDnBgF>-AK5_Tg{wa0-qoIg{ zT(+;84Y|!w-6vsJ#bWT$jceFO%5qKO4 zIRD=??}t9gEI$)-&-W+xBhOZncbdK_VGlP>XwkuuEDh_&KUzYZY-)0W61&GUKH>6r z1YE8Ei~zH@=~3eTmN3IZ6RLkzUc{jcay??#-}3+G7C0sgo{_Zy=}PEAF1&uuMCC1D zyO35vrzc<(lxe7e@Ufq2;%40FO-ztdh_Kzy_H)Z@#NkEcFMKzTXo^&O&k%I067Nj4 zhX}k6eqJ|-HpRW+$F+q46Ff8ATSXohm)#S{x}mH#GleBdf2rI#WAQwnbjwQNxY-k` zui6Yyin676ZM0#&Kt7%aOvOs5kweN>6d#&Qhlz|*@>~T^Z8Vj?*Io@DX+y(K&3E=c zDDQ1iILSx3+;**%e}Yx=lVhnbPtv-ip6BqJW(ImDVWevsYvE>K`G7-FRpe{3%Y$Yp z9URw_+5I`9#C{6`d<;w`d6?UggOQMX5*VUwB|*c2$Oh&@WxSz_$LV}o%q+P<4&p(jaGRa5+35~ zC_XG!doF|gtg0TTDZP`pNbbF{hvs*C2?lz9h&dyRU9^SI!q5t)&9Vfc^9)}EDpmqc zrcupcD2@W7ZhZ@4jT}~=o1IjHmp@j15b(B4|b=8L}#&<8Yw63a=ZOfMlF1aex2K+R>f@XElWKNl-xWU;Zp1rP3$WZp#TJJ&#(+tj$&-K8=!hN%``2B(A zHuuK-jpH^>8-4)-Ldh{UbRBxaD(D)&<2SXQAjrxy;vFT9kl;G|&C_$Pz#FLGmMn^4 zh0YhF#hs>?+|E9iv>h;xa(<@P-yb?I(C{)M6T{MAS(_zAEg}IaVX1@pVj?%l{5Bf3 zb+>Rd>%$CQUSUOqmx;pBjjJmi_9wVamA%w_$ za625F4-$3Oew=zd2Dd_E!xkRe3K{f?Wa#PhQ+E(UuP#+1m7}}oU z(N7=PKWG*N=t>pkjztnMo>=wtk*|vHWcE*=9&F+Y?baQhd`D8fK{v38{~LmFf(xa_uxD`mocJMIhwdyN>m$UIr>? zchm_LyuKXUIKYNKG%4C>yIWLF_eD(MRx=yz1U;AQ=~KgAX&aQUAtu*UB0qx;{l&3TM)yF#n+_@&LHx@$ zoV$vdJcjha)kqbpG~@u?L7lNlB8@M?Ace) z$sT>*4hzWIM6RrRIsO80p%w{B6dNAT8J$38y5kzO5K;H>V|+Pibl5eQ3O{>Lbtk;y zqsqtE6tD13M{*|T#^WEDD`Jse=gD`5Z}Lf%mzkB<99IE zs3jAZuWfcm9$R8Ms>TTlOrQ8HmWu4(+SWSi^lyrOYCQKZk+N#Dj;6R;C$~D?Ta4<7 zN623<3gSN8>Vk2)ZiGlq6{|ic8>1&@TwXYnX+-Re^IgdO8j`(EkBBXk-L#zcx+al^ zFTrMf;apmvrjxTSR=F_~F^vcYy^5H(T=Ws_Do1Eo7dVUa-4R@v)!$V}F+ZnX`0{xm z;c^CvL?66&6SLP4?r!P^0W4VA; z@}~QW+3a`oaT=SuuwJXx?L^boYon4@BSnc>Y|(wic0Is-5*I8R3LQ!zJs2ZrxMCi$ zJXf5WBEP3m9yzr0=H0pr>2yOGrfhCd)WEhMlFR2MuC{fOzI8PvTYR4u@(>sX(hIgR zdEfo1u;;_Q%a?(om9G(OK+AV;+2xJWRZKrCt5L1KFBVQE@e)GvAJA+s1JGndU2o-A z?YJ$`mfp89tp1JzeIOB?iawRUSor+a%W+%mJ}S_b*Jw|_epykX>>}f4;&WhuWahy2 z<-+irewkdxXwwB33R*##U&wvejx$$%NxM6yb{p}Dk`BAG^6Wh%`}dWRBQ{#bJxc=X z>E^el2Ol6-xdB2S#@cV+ne)Bs`T(I~ zLdT;MBS`%~PTZNdm*7G)xG0M|@V_WCDa$xos%awIb7^~-uIc-T*9I0rk8!>Of^<#N zN7czn)4cs9e{*kFsS6&$MV*enS~SL4z$O77K!!W|9&|6x4zBqG3ufzRv{LWuxW^VJ dc>m7ep4?L0-7V8FLJEKviBHm^MISXi{}=m3r9c1x literal 0 HcmV?d00001 diff --git a/2023/03/13/cmu15445$lab2/image-20231204170030245.png b/2023/03/13/cmu15445$lab2/image-20231204170030245.png new file mode 100644 index 0000000000000000000000000000000000000000..96adc2fae64d0fdfde24b6b5445ad4aaeb23474a GIT binary patch literal 16733 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 literal 0 HcmV?d00001 diff --git a/2023/03/13/cmu15445$lab2/index.html b/2023/03/13/cmu15445$lab2/index.html index aeefd711..2004a552 100644 --- a/2023/03/13/cmu15445$lab2/index.html +++ b/2023/03/13/cmu15445$lab2/index.html @@ -22,8 +22,14 @@ + + + + + + - +

    Project2 B+Tree

    Project2 B+Tree

    +};

    Project2 B+Tree

    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.

    @@ -164,4 +170,28 @@

    我服了。

    不过可能有更好的解决方法?可惜我c++水平不大够,所以暂时想不出来了。

    +

    Task4 Remove

    感想

    由于有了insert的沉淀,remove的实现便相较不大困难了,写完代码到通过内置的delete测试只花了一天的时间。

    +

    思路

    Task5 Concurrency

    感想

    这位可更是重量级,足足花了我三天的时间。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    auto BufferPoolManager::FetchPage(page_id_t page_id, [[maybe_unused]] AccessType access_type) -> Page * {
    latch_.lock();

    auto it = page_table_.find(page_id);
    if (it != page_table_.end()) {
    frame_id_t fid = it->second;

    pages_latch_[fid].lock();
    replacer_->RecordAccess(fid, access_type);
    replacer_->SetEvictable(fid, false);
    latch_.unlock();

    pages_[fid].pin_count_++;

    auto res = &(pages_[fid]);

    pages_latch_[fid].unlock();

    return res;
    }

    if (free_list_.empty()) {
    frame_id_t fid;
    if (!replacer_->Evict(&fid)) {
    latch_.unlock();
    return nullptr;
    }

    pages_latch_[fid].lock();
    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);

    if (pages_[fid].IsDirty()) {
    disk_manager_->WritePage(pages_[fid].GetPageId(), pages_[fid].GetData());
    }
    latch_.unlock();

    Page *res = &(pages_[fid]);
    res->ResetMemory();
    // read from disk
    disk_manager_->ReadPage(page_id, res->GetData());

    res->is_dirty_ = false;
    res->page_id_ = page_id;
    res->pin_count_ = 1;

    pages_latch_[fid].unlock();
    return res;
    }
    }
    + + + +

    这个并发问题是这样的,我原来是先evict,然后再写回,写回过程中磁盘没加bpm锁。这就会出现这样一个情况:

    +

    一个page被进程A evict,进程A还没执行写回的时候这个page又被进程B捡回来了,因为还没写入所以磁盘空空如也。这时候pages_latch_这个细粒度锁不能防范这种情况,是因为此时这个page对应的container不是同一个,所以fid不同,细粒度锁不同导致寄。

    +

    解决方法是要么写的时候持有bpm锁,但是这太太慢了。另一个就是干脆直接在unpin的时候不带bpm锁顺便写回了。

    +

    https://zhuanlan.zhihu.com/p/661208232

    +

    image-20231203165014734

    +

    这种情况我的做法是持有header page的读锁

    +

    image-20231203181652821

    +

    现在是这样,root即将分裂,两个进程同时插入一个key。一个进程A先得到header page的读锁和root的写锁,另一个B只得到header page读锁,等待写锁。

    +

    此时,A缩小了旧root结点,造了新结点,把root drop掉,再去获取header page的写锁;B获取了root的写锁,检查到它并非max size,插入,从而导致错误。

    +

    这时候修改header page和root的锁释放顺序会导致死锁,所以很遗憾我们只能先拿着header page的写锁,检测到无需分裂root再拿读锁了。

    +

    目前应该是算把insert的锁整好了,接下来修下delete的bug和锁应该也就差不多了

    +

    牛逼,接下来就只剩个remove的并发了

    +

    image-20231204001136531

    +

    image-20231204163052528

    +

    我服了。。。。。。。。。

    +

    image-20231204170030245

    +

    好像还是有点垃圾乐,算了先这样吧

    +

    out

    I'm so cute. Please give me money.
    Hexo 驱动 v5.4.2|主题 - Yun v1.10.11
    \ No newline at end of file diff --git a/2023/03/13/cmu15445$lab2/out.png b/2023/03/13/cmu15445$lab2/out.png new file mode 100644 index 0000000000000000000000000000000000000000..094d5703d94d34e52cba6323356ff884ed09d024 GIT binary patch literal 137862 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 PG;`a~{6^zH?#cfJECOXR literal 0 HcmV?d00001 diff --git a/2023/03/13/cmu15445/index.html b/2023/03/13/cmu15445/index.html index 746dac19..3e0d9561 100644 --- a/2023/03/13/cmu15445/index.html +++ b/2023/03/13/cmu15445/index.html @@ -36,7 +36,7 @@ // Wait for 1 second before switching API hosts rotate: 1000, }, -};

    CMU15445

    +};

    CMU15445

    实验官网

    代码

    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 67a62778..c32301eb 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" @@ -42,7 +42,7 @@ // Wait for 1 second before switching API hosts rotate: 1000, }, -};

    对GRUB和initramfs的小探究

    竞赛时对操作系统启动过程产生了些疑问,于是问题导向地浅浅探究了下GRUB和initramfs相关机制,相关笔记先放在这里了。

    +};

    对GRUB和initramfs的小探究

    竞赛时对操作系统启动过程产生了些疑问,于是问题导向地浅浅探究了下GRUB和initramfs相关机制,相关笔记先放在这里了。

    内核启动流程

    在传统的BIOS系统中,计算机具体的启动流程如下:

    1. 电源启动:当计算机的电源打开时,电源供电给计算机的硬件设备。
    2. diff --git a/2023/06/21/comporgan/index.html b/2023/06/21/comporgan/index.html index c40c6a87..cd78eca0 100644 --- a/2023/06/21/comporgan/index.html +++ b/2023/06/21/comporgan/index.html @@ -365,7 +365,7 @@ // Wait for 1 second before switching API hosts rotate: 1000, }, -};

      计算机组成原理

      概述

      架构

      冯诺依曼

      以运算器为中心,指令和数据同等地位(不满足摩尔定律)

      +};

      计算机组成原理

      概述

      架构

      冯诺依曼

      以运算器为中心,指令和数据同等地位(不满足摩尔定律)

      image-20230617133555268

      存储器为中心

      image-20230617133840406

      哈佛架构

      哈佛结构数据空间和程序空间是分开的

      diff --git "a/2023/06/21/\350\257\276\347\250\213\345\255\246\344\271\240/index.html" "b/2023/06/21/\350\257\276\347\250\213\345\255\246\344\271\240/index.html" index 2a8101d9..615a505e 100644 --- "a/2023/06/21/\350\257\276\347\250\213\345\255\246\344\271\240/index.html" +++ "b/2023/06/21/\350\257\276\347\250\213\345\255\246\344\271\240/index.html" @@ -38,7 +38,7 @@ // Wait for 1 second before switching API hosts rotate: 1000, }, -};

      课程学习

      卷学分绩的过程中,从应试的角度学习这些基础课,也给我带来了不少收获。因而,我选择将课内复习的笔记包含在这里,并且除去一些非必要的糟粕,仅保留最精华的部分,以供以后学习参考使用。

      +};

      课程学习

      卷学分绩的过程中,从应试的角度学习这些基础课,也给我带来了不少收获。因而,我选择将课内复习的笔记包含在这里,并且除去一些非必要的糟粕,仅保留最精华的部分,以供以后学习参考使用。

      计算机组成原理

      编译原理

      yysy,感觉编译原理这门课应该能算大三学了之后收获最大的专业课。理论课从形式化的原理出发,讲述了编译器的构成:词法分析、语法分析、语义分析、中间代码生成、代码优化、目标代码生成。实验课又实现了一个简单的具有基本功能的编译器。这二者相辅相成,学完这门课之后真的对编译系统的构造有了一个较为全面的了解。

      1. 词法分析

        diff --git a/2023/08/12/kernel_compile/index.html b/2023/08/12/kernel_compile/index.html new file mode 100644 index 00000000..de45c138 --- /dev/null +++ b/2023/08/12/kernel_compile/index.html @@ -0,0 +1,211 @@ +内核编译 | 修年 + + + + + + + + + +

        内核编译

        我们在开发过程中所用的具体硬件配置如下:

        +
        +

        CPU: Intel(R) Core(TM) i7-8700 CPU @ 3 with hyper-threading

        +

        Memory: 14GB

        +

        OS: Ubuntu 22.04.3 LTS

        +

        ​ with Linux kernel 6.4.0+(COS) && 6.4.0-rc3+(EXT) && 5.11.0+(ghOSt)

        +
        +

        COS环境搭建

        COS可以部署在Linux物理机和虚拟机上,但建议部署在Linux物理机以获取更好的性能效果。下文将详细介绍环境搭建的详细步骤。

        +

        COS内核

        内核编译

        安装内核编译所需包:

        +
        1
        sudo apt-get update && sudo apt-get install build-essential gcc g++ make libncurses5-dev libssl-dev bison flex bc libelf-dev
        + +

        克隆COS内核:

        +
        1
        2
        git clone https://github.com/shootfirst/cos_kernel.git
        cd cos_kernel/
        + +

        生成内核编译配置文件:

        +
        1
        make localmodconfig
        + +

        修改.config文件:

        +
        1
        vim .config
        + +
          +
        1. 删除PSI监测

          +

          查找CONFIG_PSI,将其对应行修改为:

          +
          1
          CONFIG_PSI=n
        2. +
        3. 删除系统吊销密钥

          +

          查找CONFIG_PSI,将其对应行修改为:

          +
          1
          CONFIG_SYSTEM_REVOCATION_KEYS=""
        4. +
        +

        然后就可以进行内核编译:

        +
        1
        make -j12 && sudo make modules_install && sudo make install
        + + + +

        修改grub

        打开grub配置文件:

        +
        1
        sudo vim /etc/default/grub
        + +

        进行以下修改:

        +
          +
        1. 注释GRUB_TIMEOUT_STYLE=hidden
        2. +
        3. GRUB_CMDLINE_LINUX_DEFAULT设置为”text”
        4. +
        5. GRUB_TIMEOUT修改成30
        6. +
        +

        然后保存退出,更新grub:

        +
        1
        sudo update-grub
        + + + +

        进入COS内核

        完成上述步骤后,重启虚拟机:

        +
        1
        sudo reboot
        + +

        在进入GRUB界面时选择Advanced Ubuntu,然后选择内核版本6.4.0+即可。

        +

        COS用户态

        在完成COS内核编译,并进入COS内核之后,就完成了COS的基本环境搭建。接下来,将介绍如何搭建COS用户态环境,从而运行Shinjuku Scheduler和RocksDB实验。

        +

        首先,确保所处内核正确:

        +
        1
        2
        $ uname -r
        6.4.0+
        + +

        依赖安装

        apt包

        安装编译用户态需要的包:

        +
        1
        sudo apt-get install cmake python2 python3 libtbb-dev libsnappy-dev zlib1g-dev libgflags-dev libbz2-dev liblz4-dev libzstd-dev
        + + + +

        RocksDB

        获取RocksDB 6.15.5版本release:

        +
        1
        2
        3
        wget https://github.com/facebook/rocksdb/archive/refs/tags/v6.15.5.tar.gz
        tar -xvf v6.15.5.tar.gz
        cd rocksdb-6.15.5/
        + +
        +

        注:测试得发现本RocksDB负载同最新版本RocksDB不兼容,故而建议使用上述命令对应版本,也即v6.15.5。

        +
        +

        修改CMakeLists:

        +
        1
        vim CMakeLists.txt
        + +
          +
        1. 搜索WITH_TBB,将这一项改为ON:

          +
          1
          option(WITH_TBB "build with Threading Building Blocks (TBB)" ON)
        2. +
        3. 搜索ROCKSDB_LITE 确保这一项为OFF

          +
          1
          option(ROCKSDB_LITE "Build RocksDBLite version" OFF)
        4. +
        +

        保存退出后进行编译安装:

        +
        1
        mkdir build && cd build && cmake .. && make -j12 && sudo make install
        + + + +

        运行COS

        克隆COS用户态代码:

        +
        1
        2
        git clone https://gitlab.eduxiji.net/202318123111334/cos_userspace.git
        cd cos_userspace
        + +

        编译COS用户态:

        +
        1
        mkdir build && cd build && cmake .. && make -j($nproc)
        + +

        然后就可以开始运行COS用户态了。在此以Fifo Scheduler为例。

        +

        打开两个终端,在其中一个运行Fifo Scheduler:

        +
        1
        2
        pwd # 确保在cos_userspace/build目录下
        sudo ./fifo_scheduler
        + +

        另一个运行GTest测试:

        +
        1
        2
        pwd # 确保在cos_userspace/build目录下
        sudo ./simple_exp
        + +

        等待测试完成即可。

        +

        若要运行展示中所提到的测试,可详细见性能测试教程

        +

        EXT环境搭建

        EXT内核

        依赖安装

        llvm

        SCHED-EXT内核由于用到新eBPF特性,故而编译需要用到还未发行到apt包管理器的clang最新版本,因此需要手动拉取并且编译一些依赖包。

        +
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        # 克隆llvm仓库
        git clone --depth=1 https://github.com/llvm/llvm-project.git

        # 编译llvm项目
        cd llvm-project
        mkdir build
        cd build
        cmake -DLLVM_ENABLE_PROJECTS=clang -DCMAKE_BUILD_TYPE=Release -G "Unix Makefiles" ../llvm
        make -j($nproc)

        # 在~/.bashrc文件添加
        export PATH=$PATH:/yourpath/llvm-project/build/bin

        # 确认
        echo $PATH
        clang --version # >= 17.0.0
        llvm-config --version # >= 17.0.0
        + + + +

        pahole

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        # 克隆pahole项目
        git clone git://git.kernel.org/pub/scm/devel/pahole/pahole.git

        # 编译pahole项目
        cd pahole/
        mkdir build
        cd build
        cmake -D__LIB=lib -DBUILD_SHARED_LIBS=OFF ..
        make
        sudo make install

        # 检查
        $ pahole --version # >= v1.25
        + + + +

        rust-nightly

        1
        2
        3
        4
        5
        curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
        rustup toolchain install nightly
        rustup default nightly
        # 查看rust版本
        rustc --version
        + + + +

        内核编译

        安装内核编译所需包:

        +
        1
        sudo apt-get update && sudo apt-get install build-essential gcc g++ make libncurses5-dev libssl-dev bison flex bc libelf-dev
        + +

        克隆COS内核:

        +
        1
        2
        git clone https://gitlab.eduxiji.net/202318123111334/ext-kernel.git
        cd ext-kernel/
        + +

        生成内核编译配置文件:

        +
        1
        make localmodconfig
        + +

        对生成的.config配置文件做如下修改:

        +
          +
        1. CONFIG_SYSTEM_TRUSTED_KEYS="debian/canonical-certs.pem"修改为CONFIG_SYSTEM_TRUSTED_KEYS=""
        2. +
        3. 添加CONFIG_SCHED_CLASS_EXT=y
        4. +
        5. 确保CONFIG_DEBUG_INFO以及CONFIG_DEBUG_INFO_BTF为开启状态
        6. +
        +

        编译安装内核:

        +
        1
        make -j12 && sudo make modules_install && sudo make install
        + +

        修改grub

        详见COS环境搭建的对应部分。在此不做赘述。

        +

        EXT用户态

        1
        2
        pwd # 确保在ext-kernel/项目根目录下
        cd ext-kernel/tools
        + +

        随后,我们需要替换原有EXT使用示例为我们搭建的EXT用户态框架。

        +
        1
        2
        rm -rf sched_ext/
        git clone https://gitlab.eduxiji.net/202318123111334/proj134-cfs-based-userspace-scheduler.git sched_ext
        + +

        然后就可以运行EXT用户态框架了。

        +

        若要运行展示中所提到的测试,可详细见性能测试教程

        +

        ghOSt环境搭建

        由于测试中需要以ghOSt作为比较对象,故在运行测试之前,必须搭建ghOSt环境。

        +

        ghOSt内核

        内核编译

        安装内核编译所需包:

        +
        1
        sudo apt-get update && sudo apt-get install build-essential gcc g++ make libncurses5-dev libssl-dev bison flex bc libelf-dev
        + +

        克隆ghOSt内核:

        +
        1
        2
        git clone https://github.com/google/ghost-kernel
        cd ghost-kernel/
        + +

        生成内核编译配置文件:

        +
        1
        make localmodconfig
        + +

        对生成的.config配置文件做如下修改:

        +
          +
        1. CONFIG_SYSTEM_TRUSTED_KEYS="debian/canonical-certs.pem"修改为CONFIG_SYSTEM_TRUSTED_KEYS=""
        2. +
        3. 添加CONFIG_SCHED_CLASS_GHOST=y
        4. +
        +

        编译安装内核:

        +
        1
        make -j12 && sudo make modules_install && sudo make install
        + + + +

        修改grub

        详见COS环境搭建的对应部分。在此不做赘述。

        +

        ghOSt用户态

        依赖安装

        bazel

        +

        详细安装可参照官方文档Installing Bazel on Ubuntu,在此仅给出其中第一种方法。

        +
        +

        将Bazel分发URL添加为软件包来源

        +
        1
        2
        3
        4
        5
        # 在~目录下
        sudo apt install apt-transport-https curl gnupg -y
        curl -fsSL https://bazel.build/bazel-release.pub.gpg | gpg --dearmor >bazel-archive-keyring.gpg
        sudo mv bazel-archive-keyring.gpg /usr/share/keyrings
        echo "deb [arch=amd64 signed-by=/usr/share/keyrings/bazel-archive-keyring.gpg] https://storage.googleapis.com/bazel-apt stable jdk1.8" | sudo tee /etc/apt/sources.list.d/bazel.list
        + +

        安装和更新Bazel

        +
        1
        2
        3
        sudo apt update && sudo apt install bazel
        # 将bazel升级到最新版本
        sudo apt update && sudo apt full-upgrade
        + + + +

        apt包

        1
        2
        sudo apt update
        sudo apt install libnuma-dev libcap-dev libelf-dev libbfd-dev gcc clang-12 llvm zlib1g-dev python-is-python3 libabsl-dev
        + + + +

        运行ghOSt

        克隆ghOSt用户态:

        +
        1
        2
        git clone https://gitlab.eduxiji.net/202318123111334/ghost_userspace.git
        cd ghost-userspace
        + +
        I'm so cute. Please give me money.
        Hexo 驱动 v5.4.2|主题 - Yun v1.10.11
        \ No newline at end of file diff --git a/2023/08/27/2023-os-comp/index.html b/2023/08/27/2023-os-comp/index.html index 198ce2f1..a9eafe04 100644 --- a/2023/08/27/2023-os-comp/index.html +++ b/2023/08/27/2023-os-comp/index.html @@ -38,7 +38,7 @@ // Wait for 1 second before switching API hosts rotate: 1000, }, -};

        总结—2023全国大学生计算机系统能力大赛-OS功能

        基本信息

          +};

          总结—2023全国大学生计算机系统能力大赛-OS功能

          基本信息

          • 竞赛简介

            全国大学生计算机系统能力大赛可以说是计算机专业除了ACM以外专业性最高的比赛之一了。内分多个赛道,如CPU设计(“龙芯杯”)、编译器设计(“毕昇杯”)、数据库设计以及操作系统设计。其中操作系统设计赛又分为内核实现和功能挑战两个赛道,我这一次参加的是功能赛道

            功能赛道的主要任务就是,从给定的265个赛题中选出一个赛题,并围绕此赛题做项目。项目周期为半年左右(其中3.28~6.07为初赛阶段,6.10~8.15为决赛阶段),并在最后有线下答辩以及代码现场检查验收。

            diff --git "a/2023/09/18/\351\235\231\346\200\201\351\223\276\346\216\245\344\270\216\345\212\250\346\200\201\351\223\276\346\216\245/index.html" "b/2023/09/18/\351\235\231\346\200\201\351\223\276\346\216\245\344\270\216\345\212\250\346\200\201\351\223\276\346\216\245/index.html" index 085310f4..9fefcb69 100644 --- "a/2023/09/18/\351\235\231\346\200\201\351\223\276\346\216\245\344\270\216\345\212\250\346\200\201\351\223\276\346\216\245/index.html" +++ "b/2023/09/18/\351\235\231\346\200\201\351\223\276\346\216\245\344\270\216\345\212\250\346\200\201\351\223\276\346\216\245/index.html" @@ -42,7 +42,7 @@ // Wait for 1 second before switching API hosts rotate: 1000, }, -};

            链接、装载与运行库

            +};

            链接、装载与运行库

            此为《程序员的自我修养:链接、装载与库》(俞甲子,石凡,潘爱民)的看书总结。

            链接前与装载

            链接前的编译阶段可以生成.o文件,.o文件是ELF文件,里面含有段表、符号表、bss段、common段等链接辅助段。

            diff --git "a/2023/09/27/\350\256\260\345\275\225\344\270\200\346\254\241vm\346\211\251\345\256\271/index.html" "b/2023/09/27/\350\256\260\345\275\225\344\270\200\346\254\241vm\346\211\251\345\256\271/index.html" index d08edaa2..408e9a50 100644 --- "a/2023/09/27/\350\256\260\345\275\225\344\270\200\346\254\241vm\346\211\251\345\256\271/index.html" +++ "b/2023/09/27/\350\256\260\345\275\225\344\270\200\346\254\241vm\346\211\251\345\256\271/index.html" @@ -38,7 +38,7 @@ // Wait for 1 second before switching API hosts rotate: 1000, }, -};

            记录一次vm扩容

            扩容

            容量寄,但是df -h发现这次好像是不大一样的:

            +};

            记录一次vm扩容

            扩容

            容量寄,但是df -h发现这次好像是不大一样的:

            image-20230927191332920

            查了一下,原来这是逻辑卷管理(LVM,Logical Volume Manger)。

            diff --git "a/2023/10/06/\345\255\230\345\202\250\347\256\200\345\215\225\345\205\245\351\227\250/index.html" "b/2023/10/06/\345\255\230\345\202\250\347\256\200\345\215\225\345\205\245\351\227\250/index.html" index 7019b732..a7467f3f 100644 --- "a/2023/10/06/\345\255\230\345\202\250\347\256\200\345\215\225\345\205\245\351\227\250/index.html" +++ "b/2023/10/06/\345\255\230\345\202\250\347\256\200\345\215\225\345\205\245\351\227\250/index.html" @@ -60,7 +60,7 @@ // Wait for 1 second before switching API hosts rotate: 1000, }, -};

            存储简单入门

            +};

            存储简单入门

            此为《信息存储与管理(第二版):数字信息的存储、管理和保护》的看书总结,相当于是对存储技术的一个简单的名词入门。

            本书大致可以分为三部分:存储系统,存储网络技术,备份、归档与复制。

            diff --git "a/2023/10/06/\347\275\221\347\273\234\346\230\257\346\200\216\346\240\267\350\277\236\346\216\245\347\232\204/index.html" "b/2023/10/06/\347\275\221\347\273\234\346\230\257\346\200\216\346\240\267\350\277\236\346\216\245\347\232\204/index.html" index f86bf333..52f90d28 100644 --- "a/2023/10/06/\347\275\221\347\273\234\346\230\257\346\200\216\346\240\267\350\277\236\346\216\245\347\232\204/index.html" +++ "b/2023/10/06/\347\275\221\347\273\234\346\230\257\346\200\216\346\240\267\350\277\236\346\216\245\347\232\204/index.html" @@ -42,7 +42,7 @@ // Wait for 1 second before switching API hosts rotate: 1000, }, -};

            网络是怎样连接的

            +};

            网络是怎样连接的

            此为《信息存储与管理(第二版):数字信息的存储、管理和保护》的看书总结,相当于是对存储技术的一个简单的名词入门。

            浏览器生成消息

            本章节我印象最深的还是以前就不大了解的DNS,今天看到书的描写真有种豁然开朗的感觉。

            diff --git a/2023/10/07/git/index.html b/2023/10/07/git/index.html index fa37cdde..0e218675 100644 --- a/2023/10/07/git/index.html +++ b/2023/10/07/git/index.html @@ -40,7 +40,7 @@ // Wait for 1 second before switching API hosts rotate: 1000, }, -};

            git使用记录

            +};

            git使用记录

            记录一些git的原理学习,以及工作学习中遇到的一些git的操作问题。

            操作

            pull request

              diff --git "a/2023/10/12/rtt\347\241\254\344\273\266\347\216\257\345\242\203\346\220\255\345\273\272/index.html" "b/2023/10/12/rtt\347\241\254\344\273\266\347\216\257\345\242\203\346\220\255\345\273\272/index.html" index 22194647..55156d79 100644 --- "a/2023/10/12/rtt\347\241\254\344\273\266\347\216\257\345\242\203\346\220\255\345\273\272/index.html" +++ "b/2023/10/12/rtt\347\241\254\344\273\266\347\216\257\345\242\203\346\220\255\345\273\272/index.html" @@ -40,7 +40,7 @@ // Wait for 1 second before switching API hosts rotate: 1000, }, -};

              rtt硬件环境搭建

              SDK/内核编译

              编译内核和sdk时到没有太大问题,都能靠gpt or 修改menuconfig解决。比较棘手的果然还是只能靠自己摸索硬件hhh

              +};

              rtt硬件环境搭建

              SDK/内核编译

              编译内核和sdk时到没有太大问题,都能靠gpt or 修改menuconfig解决。比较棘手的果然还是只能靠自己摸索硬件hhh

              烧录

              这个也是狠狠折磨了我许久,gpt也一直满嘴跑火车,毕竟一开始串口连错了所以一直以为自己是烧录没对,整了半天【而且还是正确的操作反反复复尝试……仿佛坐牢】。不过这其中也学到了挺多。

              f2753b5e6a40f9fea328223b197ede3

              具体要做的是:

              diff --git "a/2023/10/12/\345\220\204\347\247\215\351\205\215\347\216\257\345\242\203\344\270\255\351\201\207\345\210\260\347\232\204\351\227\256\351\242\230/index.html" "b/2023/10/12/\345\220\204\347\247\215\351\205\215\347\216\257\345\242\203\344\270\255\351\201\207\345\210\260\347\232\204\351\227\256\351\242\230/index.html" index b9e6cd15..b013df51 100644 --- "a/2023/10/12/\345\220\204\347\247\215\351\205\215\347\216\257\345\242\203\344\270\255\351\201\207\345\210\260\347\232\204\351\227\256\351\242\230/index.html" +++ "b/2023/10/12/\345\220\204\347\247\215\351\205\215\347\216\257\345\242\203\344\270\255\351\201\207\345\210\260\347\232\204\351\227\256\351\242\230/index.html" @@ -8,15 +8,15 @@ + - + - +

              各种配环境中遇到的问题

              记录一次vm扩容

              -

              开发中遇到的链接小问题

              -

              rtt硬件环境搭建

              +};
              Hexo 驱动 v5.4.2|主题 - Yun v1.10.11
              \ No newline at end of file diff --git a/2023/10/19/open-source-9.19-10.19/index.html b/2023/10/19/open-source-9.19-10.19/index.html index 10a8f301..d83e5290 100644 --- a/2023/10/19/open-source-9.19-10.19/index.html +++ b/2023/10/19/open-source-9.19-10.19/index.html @@ -45,7 +45,7 @@ // Wait for 1 second before switching API hosts rotate: 1000, }, -};

              开源的第一个月

              +};

              开源的第一个月

              其实是9.10就开始了,但9.19才办理入职所以少算了几天xxxx不然这不显得我菜hh

              引言

              也许有许多programer与我一样,在初次接触到“开源”这个概念时,便对其产生了无限的向往。千万人通过共同的事业联结在一起,为了行业的进步不求回报地压缩自己的时间,贡献出自己的一份力,这是何等的浪漫。再加上听了无数遍的Linux发展历程故事,对“开源”我是愈发地憧憬。

              diff --git a/2023/10/27/driver_develop/index.html b/2023/10/27/driver_develop/index.html index a19cf0f5..65b2e2e5 100644 --- a/2023/10/27/driver_develop/index.html +++ b/2023/10/27/driver_develop/index.html @@ -41,7 +41,7 @@ // Wait for 1 second before switching API hosts rotate: 1000, }, -};

              驱动开发小记

              此为PLCT Lab BJ71实习内容,我的工作是为RT-Thread完善Milk-v Duo开发板支持。

              +};

              驱动开发小记

              此为PLCT Lab BJ71实习内容,我的工作是为RT-Thread完善Milk-v Duo开发板支持。

              GPIO

              感受之前已经详细记录过了,在此便不再赘述。这里只写一些开发流程和最终代码框架展示。

              开发流程

              10.13—概述

              是什么

              首先了解一下gpio是什么。

              芯片上的引脚一般分为 4 类:电源、时钟、控制与 I/O,I/O 口在使用模式上又分为 General Purpose Input Output(通用输入 / 输出),简称 GPIO,与功能复用 I/O(如 SPI/I2C/UART 等)。

              diff --git a/2023/11/18/compilation_principle/index.html b/2023/11/18/compilation_principle/index.html index 175fcd2e..143a5e4e 100644 --- a/2023/11/18/compilation_principle/index.html +++ b/2023/11/18/compilation_principle/index.html @@ -460,7 +460,7 @@ // Wait for 1 second before switching API hosts rotate: 1000, }, -};

              编译原理

              第一章 绪论

              概述

              picture

              +};

              编译原理

              第一章 绪论

              概述

              picture

              可重定位的代码通过linker和loader重定位这部分内容就是在之前那本书学过的。

              picture

              从中,我们也可以看到有语法分析、中间代码的影子。

              diff --git a/2023/11/26/cryptography/index.html b/2023/11/26/cryptography/index.html index 2bf1cf40..80c5f1a0 100644 --- a/2023/11/26/cryptography/index.html +++ b/2023/11/26/cryptography/index.html @@ -401,7 +401,7 @@ // Wait for 1 second before switching API hosts rotate: 1000, }, -};

              密码学基础

              +};

              密码学基础

              学习目的:顺利过考试,以及获取基本的密码学知识,数学原理不重要

              第一章 概述

              image

              diff --git a/2023/11/26/database/index.html b/2023/11/26/database/index.html index 5002b6e7..8f2a0a63 100644 --- a/2023/11/26/database/index.html +++ b/2023/11/26/database/index.html @@ -464,7 +464,7 @@ // Wait for 1 second before switching API hosts rotate: 1000, }, -};

              数据库原理

              第一章 绪论

              可以看到,选择题主要考了一下几个概念:

              +};

              数据库原理

              第一章 绪论

              可以看到,选择题主要考了一下几个概念:

              1. 基于文件系统的管理方法

                数据存储于文件中,由应用程序经过文件系统进行管理。

                diff --git a/about/index.html b/about/index.html index 368403fc..58e929d8 100644 --- a/about/index.html +++ b/about/index.html @@ -24,23 +24,7 @@ const setting = localStorage.getItem('darken-mode') || 'auto' if (setting === 'dark' || (prefersDark && setting !== 'light')) document.documentElement.classList.toggle('dark', true) -})() - - live2d-demo - - - - - - - - - - - - -

                about

                修年

                  +};

                  about

                  Hexo 驱动 v5.4.2|主题 - Yun v1.10.11
                  -
                  - - \ No newline at end of file +
                Hexo 驱动 v5.4.2|主题 - Yun v1.10.11
                \ No newline at end of file diff --git a/archives/2022/10/index.html b/archives/2022/10/index.html index 297dcec0..e8e8e475 100644 --- a/archives/2022/10/index.html +++ b/archives/2022/10/index.html @@ -31,4 +31,4 @@ // Wait for 1 second before switching API hosts rotate: 1000, }, -};
                Hexo 驱动 v5.4.2|主题 - Yun v1.10.11
                \ No newline at end of file +};
                Hexo 驱动 v5.4.2|主题 - Yun v1.10.11
                \ No newline at end of file diff --git a/archives/2022/11/index.html b/archives/2022/11/index.html index 7c4e4332..11d1dd41 100644 --- a/archives/2022/11/index.html +++ b/archives/2022/11/index.html @@ -31,4 +31,4 @@ // Wait for 1 second before switching API hosts rotate: 1000, }, -};

                归档

                共计 45 篇日志

                2022

                没有更多的黑历史了_(:з」∠)_
                Hexo 驱动 v5.4.2|主题 - Yun v1.10.11
                \ No newline at end of file +};

                归档

                共计 46 篇日志

                2022

                没有更多的黑历史了_(:з」∠)_
                Hexo 驱动 v5.4.2|主题 - Yun v1.10.11
                \ No newline at end of file diff --git a/archives/2022/12/index.html b/archives/2022/12/index.html index b8679e29..bb0f532b 100644 --- a/archives/2022/12/index.html +++ b/archives/2022/12/index.html @@ -31,4 +31,4 @@ // Wait for 1 second before switching API hosts rotate: 1000, }, -};

                归档

                共计 45 篇日志

                2022

                没有更多的黑历史了_(:з」∠)_
                Hexo 驱动 v5.4.2|主题 - Yun v1.10.11
                \ No newline at end of file +};

                归档

                共计 46 篇日志

                2022

                没有更多的黑历史了_(:з」∠)_
                Hexo 驱动 v5.4.2|主题 - Yun v1.10.11
                \ No newline at end of file diff --git a/archives/2022/index.html b/archives/2022/index.html index faaa7b9a..016195d0 100644 --- a/archives/2022/index.html +++ b/archives/2022/index.html @@ -31,4 +31,4 @@ // Wait for 1 second before switching API hosts rotate: 1000, }, -};
                Hexo 驱动 v5.4.2|主题 - Yun v1.10.11
                \ No newline at end of file +};
                Hexo 驱动 v5.4.2|主题 - Yun v1.10.11
                \ No newline at end of file diff --git a/archives/2023/01/index.html b/archives/2023/01/index.html index 6ff17f1f..b55e39f0 100644 --- a/archives/2023/01/index.html +++ b/archives/2023/01/index.html @@ -31,4 +31,4 @@ // Wait for 1 second before switching API hosts rotate: 1000, }, -};

                归档

                共计 45 篇日志

                2023

                没有更多的黑历史了_(:з」∠)_
                Hexo 驱动 v5.4.2|主题 - Yun v1.10.11
                \ No newline at end of file +};

                归档

                共计 46 篇日志

                2023

                没有更多的黑历史了_(:з」∠)_
                Hexo 驱动 v5.4.2|主题 - Yun v1.10.11
                \ No newline at end of file diff --git a/archives/2023/02/index.html b/archives/2023/02/index.html index 7f47b91d..f24c088b 100644 --- a/archives/2023/02/index.html +++ b/archives/2023/02/index.html @@ -31,4 +31,4 @@ // Wait for 1 second before switching API hosts rotate: 1000, }, -};

                归档

                共计 45 篇日志

                2023

                没有更多的黑历史了_(:з」∠)_
                Hexo 驱动 v5.4.2|主题 - Yun v1.10.11
                \ No newline at end of file +};

                归档

                共计 46 篇日志

                2023

                没有更多的黑历史了_(:з」∠)_
                Hexo 驱动 v5.4.2|主题 - Yun v1.10.11
                \ No newline at end of file diff --git a/archives/2023/03/index.html b/archives/2023/03/index.html index bf81981f..82a8e87e 100644 --- a/archives/2023/03/index.html +++ b/archives/2023/03/index.html @@ -31,4 +31,4 @@ // Wait for 1 second before switching API hosts rotate: 1000, }, -};

                归档

                共计 45 篇日志

                2023

                没有更多的黑历史了_(:з」∠)_
                Hexo 驱动 v5.4.2|主题 - Yun v1.10.11
                \ No newline at end of file +};

                归档

                共计 46 篇日志

                2023

                没有更多的黑历史了_(:з」∠)_
                Hexo 驱动 v5.4.2|主题 - Yun v1.10.11
                \ No newline at end of file diff --git a/archives/2023/06/index.html b/archives/2023/06/index.html index 735b726a..f275c612 100644 --- a/archives/2023/06/index.html +++ b/archives/2023/06/index.html @@ -31,4 +31,4 @@ // Wait for 1 second before switching API hosts rotate: 1000, }, -};

                归档

                共计 45 篇日志

                2023

                没有更多的黑历史了_(:з」∠)_
                Hexo 驱动 v5.4.2|主题 - Yun v1.10.11
                \ No newline at end of file +};

                归档

                共计 46 篇日志

                2023

                没有更多的黑历史了_(:з」∠)_
                Hexo 驱动 v5.4.2|主题 - Yun v1.10.11
                \ No newline at end of file diff --git a/archives/2023/08/index.html b/archives/2023/08/index.html index efb45434..783b3b43 100644 --- a/archives/2023/08/index.html +++ b/archives/2023/08/index.html @@ -31,4 +31,4 @@ // Wait for 1 second before switching API hosts rotate: 1000, }, -};

                归档

                共计 45 篇日志

                2023

                没有更多的黑历史了_(:з」∠)_
                Hexo 驱动 v5.4.2|主题 - Yun v1.10.11
                \ No newline at end of file +};

                归档

                共计 46 篇日志

                2023

                没有更多的黑历史了_(:з」∠)_
                Hexo 驱动 v5.4.2|主题 - Yun v1.10.11
                \ No newline at end of file diff --git a/archives/2023/09/index.html b/archives/2023/09/index.html index 7e5b7aaf..53437d89 100644 --- a/archives/2023/09/index.html +++ b/archives/2023/09/index.html @@ -31,4 +31,4 @@ // Wait for 1 second before switching API hosts rotate: 1000, }, -};

                归档

                共计 45 篇日志

                2023

                没有更多的黑历史了_(:з」∠)_
                Hexo 驱动 v5.4.2|主题 - Yun v1.10.11
                \ No newline at end of file +};

                归档

                共计 46 篇日志

                2023

                没有更多的黑历史了_(:з」∠)_
                Hexo 驱动 v5.4.2|主题 - Yun v1.10.11
                \ No newline at end of file diff --git a/archives/2023/10/index.html b/archives/2023/10/index.html index 61b45a4a..dab95d1e 100644 --- a/archives/2023/10/index.html +++ b/archives/2023/10/index.html @@ -31,4 +31,4 @@ // Wait for 1 second before switching API hosts rotate: 1000, }, -};

                归档

                共计 45 篇日志

                2023

                没有更多的黑历史了_(:з」∠)_
                Hexo 驱动 v5.4.2|主题 - Yun v1.10.11
                \ No newline at end of file +};

                归档

                共计 46 篇日志

                2023

                没有更多的黑历史了_(:з」∠)_
                Hexo 驱动 v5.4.2|主题 - Yun v1.10.11
                \ No newline at end of file diff --git a/archives/2023/11/index.html b/archives/2023/11/index.html index ac267d83..b3d7d573 100644 --- a/archives/2023/11/index.html +++ b/archives/2023/11/index.html @@ -31,4 +31,4 @@ // Wait for 1 second before switching API hosts rotate: 1000, }, -};

                归档

                共计 45 篇日志
                没有更多的黑历史了_(:з」∠)_
                Hexo 驱动 v5.4.2|主题 - Yun v1.10.11
                \ No newline at end of file +};

                归档

                共计 46 篇日志
                没有更多的黑历史了_(:з」∠)_
                Hexo 驱动 v5.4.2|主题 - Yun v1.10.11
                \ No newline at end of file diff --git a/archives/2023/index.html b/archives/2023/index.html index 75a4d18d..5333d75b 100644 --- a/archives/2023/index.html +++ b/archives/2023/index.html @@ -31,4 +31,4 @@ // Wait for 1 second before switching API hosts rotate: 1000, }, -};
                Hexo 驱动 v5.4.2|主题 - Yun v1.10.11
                \ No newline at end of file +};
                Hexo 驱动 v5.4.2|主题 - Yun v1.10.11
                \ No newline at end of file diff --git a/archives/2023/page/2/index.html b/archives/2023/page/2/index.html index b1ae0696..7228de9e 100644 --- a/archives/2023/page/2/index.html +++ b/archives/2023/page/2/index.html @@ -31,4 +31,4 @@ // Wait for 1 second before switching API hosts rotate: 1000, }, -};
                Hexo 驱动 v5.4.2|主题 - Yun v1.10.11
                \ No newline at end of file +};
                Hexo 驱动 v5.4.2|主题 - Yun v1.10.11
                \ No newline at end of file diff --git a/archives/2023/page/3/index.html b/archives/2023/page/3/index.html index e5028139..42af1a6a 100644 --- a/archives/2023/page/3/index.html +++ b/archives/2023/page/3/index.html @@ -31,4 +31,4 @@ // Wait for 1 second before switching API hosts rotate: 1000, }, -};

                归档

                共计 45 篇日志

                2023

                Hexo 驱动 v5.4.2|主题 - Yun v1.10.11
                \ No newline at end of file +};

                归档

                共计 46 篇日志

                2023

                Hexo 驱动 v5.4.2|主题 - Yun v1.10.11
                \ No newline at end of file diff --git a/archives/2023/page/4/index.html b/archives/2023/page/4/index.html index 9ba457f2..ec9402ac 100644 --- a/archives/2023/page/4/index.html +++ b/archives/2023/page/4/index.html @@ -31,4 +31,4 @@ // Wait for 1 second before switching API hosts rotate: 1000, }, -};

                归档

                共计 45 篇日志

                2023

                没有更多的黑历史了_(:з」∠)_
                Hexo 驱动 v5.4.2|主题 - Yun v1.10.11
                \ No newline at end of file +};

                归档

                共计 46 篇日志

                2023

                Hexo 驱动 v5.4.2|主题 - Yun v1.10.11
                \ No newline at end of file diff --git a/archives/2023/page/5/index.html b/archives/2023/page/5/index.html new file mode 100644 index 00000000..ae655a1e --- /dev/null +++ b/archives/2023/page/5/index.html @@ -0,0 +1,34 @@ +归档 | 修年 + + + + + +

                归档

                共计 46 篇日志

                2023

                没有更多的黑历史了_(:з」∠)_
                Hexo 驱动 v5.4.2|主题 - Yun v1.10.11
                \ No newline at end of file diff --git a/archives/index.html b/archives/index.html index 4179e227..97719901 100644 --- a/archives/index.html +++ b/archives/index.html @@ -31,4 +31,4 @@ // Wait for 1 second before switching API hosts rotate: 1000, }, -};
                Hexo 驱动 v5.4.2|主题 - Yun v1.10.11
                \ No newline at end of file +};
                Hexo 驱动 v5.4.2|主题 - Yun v1.10.11
                \ No newline at end of file diff --git a/archives/page/2/index.html b/archives/page/2/index.html index f3e766d7..c00906f1 100644 --- a/archives/page/2/index.html +++ b/archives/page/2/index.html @@ -31,4 +31,4 @@ // Wait for 1 second before switching API hosts rotate: 1000, }, -};
                Hexo 驱动 v5.4.2|主题 - Yun v1.10.11
                \ No newline at end of file +};
                Hexo 驱动 v5.4.2|主题 - Yun v1.10.11
                \ No newline at end of file diff --git a/archives/page/3/index.html b/archives/page/3/index.html index edbc2ec7..ae779e2d 100644 --- a/archives/page/3/index.html +++ b/archives/page/3/index.html @@ -31,4 +31,4 @@ // Wait for 1 second before switching API hosts rotate: 1000, }, -};

                归档

                共计 45 篇日志

                2023

                Hexo 驱动 v5.4.2|主题 - Yun v1.10.11
                \ No newline at end of file +};

                归档

                共计 46 篇日志

                2023

                Hexo 驱动 v5.4.2|主题 - Yun v1.10.11
                \ No newline at end of file diff --git a/archives/page/4/index.html b/archives/page/4/index.html index ff26e34f..d4566330 100644 --- a/archives/page/4/index.html +++ b/archives/page/4/index.html @@ -31,4 +31,4 @@ // Wait for 1 second before switching API hosts rotate: 1000, }, -};

                归档

                共计 45 篇日志

                2023

                Hexo 驱动 v5.4.2|主题 - Yun v1.10.11
                \ No newline at end of file +};

                归档

                共计 46 篇日志

                2023

                Hexo 驱动 v5.4.2|主题 - Yun v1.10.11
                \ No newline at end of file diff --git a/archives/page/5/index.html b/archives/page/5/index.html index 1e98f72c..a0c4446c 100644 --- a/archives/page/5/index.html +++ b/archives/page/5/index.html @@ -31,4 +31,4 @@ // Wait for 1 second before switching API hosts rotate: 1000, }, -};
                Hexo 驱动 v5.4.2|主题 - Yun v1.10.11
                \ No newline at end of file +};
                Hexo 驱动 v5.4.2|主题 - Yun v1.10.11
                \ No newline at end of file diff --git a/index.html b/index.html index d00dcf97..a87df51c 100644 --- a/index.html +++ b/index.html @@ -31,4 +31,4 @@ // Wait for 1 second before switching API hosts rotate: 1000, }, -};
                喜欢您来!
                Hexo 驱动 v5.4.2|主题 - Yun v1.10.11
                \ No newline at end of file +};
                喜欢您来!
                Hexo 驱动 v5.4.2|主题 - Yun v1.10.11
                \ No newline at end of file diff --git a/search.xml b/search.xml index dfac7159..edbe77ec 100644 --- a/search.xml +++ b/search.xml @@ -413,20 +413,32 @@

                我服了。

                不过可能有更好的解决方法?可惜我c++水平不大够,所以暂时想不出来了。

                +

                Task4 Remove

                感想

                由于有了insert的沉淀,remove的实现便相较不大困难了,写完代码到通过内置的delete测试只花了一天的时间。

                +

                思路

                Task5 Concurrency

                感想

                这位可更是重量级,足足花了我三天的时间。

                +
                auto BufferPoolManager::FetchPage(page_id_t page_id, [[maybe_unused]] AccessType access_type) -> Page * {
                latch_.lock();

                auto it = page_table_.find(page_id);
                if (it != page_table_.end()) {
                frame_id_t fid = it->second;

                pages_latch_[fid].lock();
                replacer_->RecordAccess(fid, access_type);
                replacer_->SetEvictable(fid, false);
                latch_.unlock();

                pages_[fid].pin_count_++;

                auto res = &(pages_[fid]);

                pages_latch_[fid].unlock();

                return res;
                }

                if (free_list_.empty()) {
                frame_id_t fid;
                if (!replacer_->Evict(&fid)) {
                latch_.unlock();
                return nullptr;
                }

                pages_latch_[fid].lock();
                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);

                if (pages_[fid].IsDirty()) {
                disk_manager_->WritePage(pages_[fid].GetPageId(), pages_[fid].GetData());
                }
                latch_.unlock();

                Page *res = &(pages_[fid]);
                res->ResetMemory();
                // read from disk
                disk_manager_->ReadPage(page_id, res->GetData());

                res->is_dirty_ = false;
                res->page_id_ = page_id;
                res->pin_count_ = 1;

                pages_latch_[fid].unlock();
                return res;
                }
                }
                + + + +

                这个并发问题是这样的,我原来是先evict,然后再写回,写回过程中磁盘没加bpm锁。这就会出现这样一个情况:

                +

                一个page被进程A evict,进程A还没执行写回的时候这个page又被进程B捡回来了,因为还没写入所以磁盘空空如也。这时候pages_latch_这个细粒度锁不能防范这种情况,是因为此时这个page对应的container不是同一个,所以fid不同,细粒度锁不同导致寄。

                +

                解决方法是要么写的时候持有bpm锁,但是这太太慢了。另一个就是干脆直接在unpin的时候不带bpm锁顺便写回了。

                +

                https://zhuanlan.zhihu.com/p/661208232

                +

                image-20231203165014734

                +

                这种情况我的做法是持有header page的读锁

                +

                image-20231203181652821

                +

                现在是这样,root即将分裂,两个进程同时插入一个key。一个进程A先得到header page的读锁和root的写锁,另一个B只得到header page读锁,等待写锁。

                +

                此时,A缩小了旧root结点,造了新结点,把root drop掉,再去获取header page的写锁;B获取了root的写锁,检查到它并非max size,插入,从而导致错误。

                +

                这时候修改header page和root的锁释放顺序会导致死锁,所以很遗憾我们只能先拿着header page的写锁,检测到无需分裂root再拿读锁了。

                +

                目前应该是算把insert的锁整好了,接下来修下delete的bug和锁应该也就差不多了

                +

                牛逼,接下来就只剩个remove的并发了

                +

                image-20231204001136531

                +

                image-20231204163052528

                +

                我服了。。。。。。。。。

                +

                image-20231204170030245

                +

                好像还是有点垃圾乐,算了先这样吧

                +

                out

                ]]> - - CMU15445 - /2023/03/13/cmu15445/ - -

                实验官网

                -

                代码

                -
              -

              Project0 C++ Primer

              Project1 Buffer Pool

              Project2 B+Tree Index

              ]]> - - labs - - Project1 Buffer Pool /2023/03/13/cmu15445$lab1/ @@ -662,5559 +674,5730 @@ ]]> - 编译原理 - /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

              + CMU15445 + /2023/03/13/cmu15445/ + +

              实验官网

              +

              代码

              +
              +

              Project0 C++ Primer

              Project1 Buffer Pool

              Project2 B+Tree Index

              ]]> + + labs + + + + 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. 0型

              -

              picture

              -
            2. -
            3. 1型

              -

              picture

              -

              之所以是上下文有关,是因为只有A的上下文为a1和a2时才能替换为β【666666,第一次懂】

              -

              CSG不包含空产生式。

              +
            4. 状态

              +

              状态是指存储在状态变量里的数据,如成员变量、静态域等等等。对象的状态还可能包括其他依赖对象的域,如HashMap的状态包括Map.Entry的状态。

            5. -
            6. 2型

              -

              picture

              -

              左部只能是一个非终结符。

              +
            7. 共享和可变

              +

              共享意味着变量可以由多个线程同时访问,可变意味着变量的值在生命周期可发生变化

            8. -
            9. 3型

              -

              picture

              -

              产生式右部最多只有一个非终结符,且要在同一侧

              -

              picture

              -

              看起来还能转(是的,自动机教的已经全忘了())

              +
            10. 是否需要线程安全

              +

              取决于它是否被多个线程访问。比如说,如果一个局部变量仅在某个函数体中同时只被一个线程访问,那么它就不需要线程安全,不需要同步机制。

            -

            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边(蓝色)和加新边(红色),

            -]]> - - - 密码学基础 - /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

            -]]> - - - 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 StatelessFactorize implements Servlet{
            public void service(ServletRequest req,ServletResponse resp){
            BigInteger i = extractFromRequest(req);
            BigInteger[] factors = factor(i);
            encodeIntoResponse(resp,factors);
            }
            }
            -

            所以需要上一章学的安全发布。

            - -
            //通过封闭机制保证线程安全
            @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();}
            }
            }
            +

            这是线程不安全的,因为++count包含了三个动作:读取—修改—写入

            +
            mov reg,count
            add reg,1
            mov count,reg
            -

            就是把原来的collection给实例封闭了,之后的访问都用了同步锁。

            -

            Java监视器模式

            使用内置锁

            +

            它并不具有原子性。

            +

            在并发编程中,这种由于时序原因产生错误的情况叫做“竞态条件”。

            +

            竞态条件

            -

            直白点来说,就是把所有要访问自己状态的地方/方法通通synchronized。

            - +

            竞态条件有两种常见的类型。两种竞态条件的本质其实都是“基于对象之前的状态来定义对象状态的转换”。对于读取-修改-写入,是先copy原值,然后对原值+1,再写回,这是基于对象之前的状态来定义对象状态的转换;对于先检查后执行,很显然就是判断原值然后再转换到下一个状态,这就不必说了。

            +

            读取-修改-写入

            如上引例

            +

            先检查后执行

            实例:懒加载,延迟初始化中的竞态条件

            +
            public class LazyInitRace {
            private ExpensiveObject instance = null;

            public ExpensiveObject getInstance() {
            if (instance == null)
            instance = new ExpensiveObject();
            return instance;
            }
            }
            -
            //监视器模式
            @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);
            }
            }
            - +

            这书里讲得云里雾里的,百度了一下:

            + +

            比如说书给例子,线程向共享对象读写数据,线程是操作对象A,共享对象是被操作对象B。则:

            +

            竞态条件:在乎的是被线程操控的共享对象的结果是否正确

            +

            数据竞争:在乎的是操作共享对象后,线程的结果是否正确。

            + +

            确实,书里对数据竞争强调的是一个读一个写,对竞态条件更像是两个同时写

            +

            复合操作

            -

            将线程安全性委托给独立的状态变量

            + -

            定义

            - -

            意思就是保证一个类里面仅有一个状态,只要该状态是线程安全的,那么该类也就是线程安全的

            -

            示例

            //线程安全
            public class Point {
            public final int x,y;

            public Point(int x, int y) {
            this.x = x;
            this.y = y;
            }
            }
            +

            我们可以用一个线程安全类来解决前面的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);
            }
            }
            -
            //将线程安全委托给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 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);
            }
            }
            }
            -

            也就是说这些对象彼此不会构成不变性条件。

            -
            //将线程安全性委托给多个状态变量
            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());
            }
            }
            + - +

            这段论述非常精彩,昭示了两个道理:1.分析线程安全性的时候,可以从“不变性条件不被破坏”开始考虑,首先考虑不变性条件应该是什么。2.在不变性条件涉及的多个变量彼此不独立,因而这些变量需要同时同步更新,上面那个例子就是因为不变性约束条件中的两个不独立变量没有同时同步更新。

            + -

            根本原因就是因为不独立。

            - + +

            确实,非常重要的一点就是在两个需要连续同时修改的变量之间有了并行的时间间隔,导致此期间并行的线程的不变性被破坏

            +

            内置锁

            -

            发布被委托的状态变量

            什么时候可以发布

            - +

            同步代码块包含两部分,锁的引用和保护的代码段。关键字synchronized修饰的方法就是一段同步代码段,其锁对象为当前实例【非静态方法】或者是当前class的实例【静态方法】。

            +
            +

            这个具体的“锁”是什么以前是真不知道。已知的是所有Object都有wait和什么什么notify方法。不过想想也确实。所有线程争抢着访问一个对象的某个同步方法段,这不正跟所有线程争抢着一个锁是差不多意思的吗?“锁”的定义其实是很宽泛的

            +
            + -

            示例

            +

            java的内置锁并非无饥饿的。当线程B永远不释放锁,A会一直等待下去。

            + -
            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;
            }
            }
            +

            我们可以用synchronized来解决上面的计数器问题,即直接给service方法设为synchronized。当然这种方法性能很糟糕,因为它极大降低了并发度。

            +

            重入

            -
            //跟上面那个委托没什么差,区别只在于上面的那个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”

            - +
            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时,该实例的锁【注意,是同一个实例】已经被占用还未释放,因此产生死锁。有重入就可以避免此问题

            +

            用锁来保护状态

            -
            @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;
            }
            }
            }
            -

            评价

            +

            活跃性与性能

            上面那个直接对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);
            }
            }
            - + -

            它非常依赖于其他类的客户端加锁机制。

            - +

            毕竟因数分解的时候无需同步保护,因为这时候参与运算的都是局部变量。

            + -

            确实,毕竟你锁被外界拿去用了。

            -

            组合

            -
            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接口其他未实现的方法
            }
            - +

            第三章 安全地共享对象

            上一章讲述了,线程安全的本质就是对共享和可变状态进行管理,以及介绍了用锁来保护状态。

            +

            本章将引入同步除原子性外的另一特性——可见性,然后再介绍如何构建线程安全类,并且安全地发布和共享对象。

            +

            关键词:可见性 Volatile 线程封闭 不可变对象

            + -

            是的,跟synchronizedList非常像

            - + -

            这也就是用的java的监视器模式了

            -

            第五章 基础构建模块

            -

            保证独立即可委托,从而构建一个线程安全类

            - -

            同步容器类

            +

            可见性

            引例——可见性的定义

            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;
            }
            }
            -

            差不多都是使用的监视器模式。

            -

            同步容器类: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);
            }
            }
            +

            其实原因非常显而易见:主线程改了之后不会立刻把变量刷新到主存【可能默认是在ret时刷新,或者定时刷新,前者会导致相互等待的死锁,后者也会产生性能问题】,导致主线程的那个修改的flag变量对t1线程是**不可见**的,因此t1会继续循环等待。

            +
            +

            失效数据

            -

            除此之外,迭代也是一种经典的复合操作。我们可以通过下面这种粗粒度加锁来避免:

            -
            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);
            }
            }
            -
            - +

            注意,最低安全性不适用于非volatile类型的64位数据。

            + - + -

            可见同步容器类还是有很多局限性的。

            -

            隐藏迭代器

            有时候,迭代会隐藏起来。要一个个揪出需要加锁的地方是非常麻烦的。

            -
            //隐藏在字符串连接中的迭代操作
            @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);
            }
            }

            - - +

            加锁与可见性

            - + +

            要实现这种操作,我们可以设想一下关于内存可见性这一块内置锁的实现原理:lock时绑定指定变量,unlock时再刷新这个/些绑定变量的内存

            + +

            所以说得有锁,并且锁还得是对的。

            +

            看着看着有种always语句块的感觉了2333

            + -

            并发容器类

            同步容器类的加锁太粗粒度了,导致并发性弱。因而引入并发容器类来解决问题。

            - -

            ConcurrentHashMap —— HashMap

            -

            CopyOnWriteArrayList —— List

            - - +

            Volatile关键字

            Volatile保证内存可见性

            到其他线程。【我想大概就是一改变了,就马上刷新内存中的旧值,然后也许通过什么嗅探检测到值变化,通知所有线程改变自己持有的旧值。】 -

            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 倍!

            +
            +

            Volatile不保证原子性

            +
            -

            关于ConcurrentHashMap的弱一致性:ConcurrentHashMap的弱一致性

            -

            get方法是弱一致的,是什么含义?可能你期望往ConcurrentHashMap底层数据结构中加入一个元素后,立马能对get可见,但ConcurrentHashMap并不能如你所愿。换句话说,put操作将一个元素加入到底层数据结构后,get可能在某段时间内还看不到这个元素,若不考虑内存模型,单从代码逻辑上来看,却是应该可以看得到的。

            -
            - +

            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仅确保单次读写的瞬时线程安全

            + -
            -

            关于AQS框架:重大发现,AQS加锁机制竟然跟Synchronized有惊人的相似

            -

            在并发多线程的情况下,为了保证数据安全性,一般我们会对数据进行加锁,通常使用Synchronized或者ReentrantLock同步锁。Synchronized是基于JVM实现,而ReentrantLock是基于Java代码层面实现的,底层是继承的AQS

            -

            AQS全称**AbstractQueuedSynchronizer**,即抽象队列同步器,是一种用来构建锁和同步器的框架。

            -

            我们常见的并发锁ReentrantLockCountDownLatchSemaphoreCyclicBarrier都是基于AQS实现的,所以说不懂AQS实现原理的,就不能说了解Java锁。

            -
            -
            -

            并发容器类不能实现独占访问:

            -

            类似ConcurrentHashMap的并发容器不能采用客户端加锁机制,因为并发容器没有采用synchronized内置锁而大多基于AQS框架(不是独占式的锁),所以使用客户端加锁机制来扩展并发容器的方法是不能实现的。

            -

            所以说不能客户端加锁不是不提倡,而是真的不行【】

            +

            以下是别人的理解扩展:

            +

            对volatile不具有原子性的理解

            +

            volatile 无法保证原子性一个简单示例的疑问

            +

            Java并发编程:volatile关键字解析

            -

            所以最好还是用并发容器类替代同步容器类

            -

            对部分复合操作的支持

            +

            Volatile的使用方法

            -

            客户端加锁不能使用,就只能用它提供的东西了。

            -

            CopyOnWriteArrayList

            Copy-On-Write意为“写入时复制”,仅当要修改的时候,才会重新创建一次副本,实现可变性。犹记得第一次接触到这个思想是在操作系统的fork()创建子进程的原理那个地方,那可真是有些惊为天人23333

            - + - +

            下面给出一个volatile的典型用法:检查某个状态标记以判断是否退出循环。【也就是上文那个例子】

            +
            volatile boolean asleep;
            //...
            while(!asleep) countingSheep();
            -

            也就是说,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。

            - - +

            什么时候会发生发布和逸出

            外部方法

            当把一个对象传递给某个外部方法,就相当于发布了这个对象

            +

            外部方法:

            +
            发布内部的类实例

            “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 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 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;
            }
            }
            -
            //启动生产者-消费者程序
            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();
            }
            }
            +

            线程封闭

            线程封闭是什么

            -

            串行线程封闭

            + -

            安全发布

            - +

            线程封闭一般有三种方法,这三种方法的规范性是逐级递增的。

            +

            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;
            +
            +

            栈封闭

            -

            6666666

            -

            之所以叫“串行”,想必是因为这个过程:发布-转接-放弃访问权是一个串行过程。

            - +

            也就是我们前面说的,局部变量只能在该线程内访问,除非逸出了,否则是非常安全的。

            + + +

            对于基本类型

            -

            总而言之,串行线程封闭的具体做法就是,一个线程将一个安全发布的对象的所有权完全转移给另一个线程,保证之后自己不会再使用。这样一来,该对象就相当于被另一个线程封闭了。而如何保证“自己以后不再使用”呢?最简单的方法就是安全发布完这个东西后直接把这个东西给踢出去

            -

            阻塞队列是自动会把这个东西安全发布然后就踢出去的,所以说阻塞队列简化了这个工作。

            - +意思就是说,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
            }
            -

            当某方法抛出InterruptedException时,表明该方法为阻塞方法,也即这个方法会在执行过程中由于各种原因而被阻塞。如果这个方法被中断,它将会努力提前结束阻塞状态。

            -

            中断方法

            +

            而不用:

            +
            AtomicInteger myID = getID();
            void method(AtomicInteger myID){
            //do something
            }
            - +

            这样大大简化了实现。

            +

            再比如:

            + -

            处理InterruptedException的两种选择

            +
            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();
            }
            -

            传递InterruptedException

            + -

            恢复中断

            +

            关于这个的大概代码猜想:

            +

            这样一来,在一个线程中使用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】的方式实现的。

            -

            此处关于为什么如果代码是Runnable的一部分就不能抛出异常:

            -

            是因为java的异常继承体系。

            -

            在重写的run方法中,我们只能够进行异常的捕获而不能够抛出异常,原因是因为在父类Runnable接口中,run方法没有抛出异常,则实现Runnable的子类就无法抛出异常

            -

            所以实际上是语法层面上不允许。

            +

            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域只能在声明的成员域或者构造函数中初始化,两者本质上都是在构造函数中初始化的。

            +

            并且不可变对象也更加安全。

            + -

            闭锁 CountDownLatch

            作用

            +

            不可变对象与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值都是一样的【每次计算都得到相同的结果】,所以就不会产生多个线程计算出不同值的情况,导致不同步的发生。

            + -

            CountDownLatch

            +

            意思是说之所以hashcode值一样,是因为这个hashcode计算是基于不可变对象的:

            +
            private final char value[];
            -
            //使用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;
            }

            }
            +

            并且重复计算性能代价可能远没有加锁的消耗来得大,因而这里仅使用了栈封闭来保证一定程度上的线程同步。

            +
            +

            可变对象基础上构建不可变类

            - +
            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);
            }
            }
            - + -

            是的,这样测试出来的时间应该更加平均,性能更加准确。

            -

            FutureTask

            +

            也就是说,实现的核心是保证可变对象不变即可。

            + -
            -

            java的future机制原理

            -

            关于Future:

            - -

            其中get方法是阻塞的。

            -

            获取异步任务执行完后的结果。

            -

            关于FutureTask:

            -

            FutureTask既包含了Future的语义,又包含了Runnable的语义。

            -

            它其实内部封装了一个Runnable Task。调用FutureTask的run,其实本质上就是调用Task的run,只不过要多一些检查和存储结果之类的手续。

            -

            所以说它其实就是通过内部封装一个线程,然后就能获取这个线程运行的状态和运行的结果等等等,这样来实现Future语义的。

            -
            -
            -

            关于Callable

            -

            Runnable里面的run方法是不能传参,也没有返回值的。Callable相当于有返回值的Runnable,也即书中说的“有生成结果的Runnable”。

            -
            - - +

            Final域

            -
            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);
            }
            }
            +

            final不仅保证了引用对象的不可变,还保证了不可变对象初始化过程中的线程安全性

            + - +

            所以说还是尽量多用final

            +

            Volatile与不可变对象提供弱原子性

            也就是这个图片中所说的:

            + -

            也就是调用start线程启动后,可以就去做别的事情,回来就可以拿到结果了,通过这样实现异步调用。

            - +

            使用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);
            }
            }
            }
            -

            这里其实是在讲异常的事了。可以给我们一个启发式思路:

            -

            Callable抛出的Exception这种抽象的异常集合该如何分解处理:首先分解出受检查的异常【也就是说我们调用该方法就知道该方法可能会抛出的异常】,然后针对其他未检查异常,再进行处理。此例中是把这些未检查异常分成了Error和RuntimeException。

            -

            信号量 Semaphore

            +

            如今,利用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);
            }
            }
            }
            -
            -

            注意

            - -
            -

            这种情况下,其实信号量跟BlockingQueue语义十分近似:

            - + -

            信号量还可以用来将非阻塞容器包装为有界阻塞容器

            - +

            每当需要对一组相关数据以原子方式执行某个操作时,就可以考虑创建一个不可变的类来包含这些数据,其本质就是利用不可变性消除了访问和更新多个变量的竞态条件。

            + - +

            因为依然满足该程序的不变性原理:factor数组中各个数字的乘积=lastNumber,也就是说容器对象的两个值都是正确对应的,因而容器对象处于一致的状态。又或者是因为volatile及时刷新,因此确保了各个线程的内存可见性。

            + -
            //使用信号量为容器设置边界,有界+阻塞
            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 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:

            + -

            常常出现那种需要等待所有线程都完成某一步操作才能进行下一步操作的情况,所以栅栏不得不说非常实用。

            -
            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();
            }
            }
            +

            可知,new一个对象并非原子操作,并且很有可能先得到内存引用才初始化对象。

            +

            因而,在上面那段不安全代码的语境下可分析:

            +
            Holder的错误发布有三点如下:

            首先明确,引用,和引用的对象的状态,这两个是两个需要独立考虑的方面。前者是一个指针值,后者是指针所指的数据。下面的点1仅考虑引用的更新,点2考虑了引用对象的状态更新。

            +

            \1. 发布对象的那个线程给holder初始化之后,holder这个引用没有及时刷新到内存,因而对其他线程不可见,其他线程读到的holder引用是旧的。

            +

            \2. 又或者,发布了holder还没初始化完毕的时候,别的进程读取到未完成初始化的holder这个引用,但这个引用指向的状态却是旧的,因为它还没完成初始化,其状态值为旧值或者默认值。【发生了上面new一个对象的指令重排】

            +

            \3. 如果在assert方法中两次读取n发生了上面第二条,就可能会导致前后的n不唯一,抛出异常。

            +

            由书中表述,如果将Holder转化为不可变类,那么该发布是安全的。

            + +

            至于为什么,可见下个标题。

            -

            注:

            -

            的目的

            - -
            -

            Exchanger

            - -

            这个“写满”大概对应着栅栏思想里的“全部到达”

            - +

            此处插入思考:是否可以将public Holder holder修改为public final Holder holder,或者volatile修饰,来解决上述问题呢?

            +

            java多线程关键字final和static详解

            +

            通过看该文章得知:

            + +

            volatile和final都会禁止字段引用的对象在构造对象过程中发生指令重排,别的线程得到引用的时候构造已经完成,而不会先得到引用再完成构造,并且两个标志都可以保证可见性。

            +

            不过继续读下去,书中给出了答案:我说的这个方法也是可行的。

            + +

            我的疑问就是第二点和第三点。

            +
            +

            不可变对象的初始化安全性

            -

            示例:构建高效且可伸缩的结果缓存

            + - -
            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;
            }
            }
            +

            volatile、final以及AtomicReferance保护引用

            详见上面那个不正确案例最后的思考

            +

            由锁保护的区域

            这个区域除了是通过程序构造的,也可以是使用Java自带的线程安全类库

            + -

            保证了线程安全,但是可伸缩性极差,而且很有可能变成普通的串行排队计算。

            - +

            事实不可变对象

            安全发布可以保证发布时的线程安全所以说你如果承诺发布后可以一直保证不可变,那就一直都是线程安全的。

            + -

            使用并发容器类

            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框架

            +

            666666

            + -
            /*
            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);
            }
            - - +

            实例封闭

            什么是实例封闭

            - + +

            所以需要上一章学的安全发布。

            + +
            //通过封闭机制保证线程安全
            @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);
            }
            }
            -

            示例:基于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);
            }
            });
            }
            }
            }
            + - + - +

            阅读源码可知:

            +
            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();}
            }
            }
            -

            思考问题

            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)其实就只是分为三个部分。

            - +

            就是把原来的collection给实例封闭了,之后的访问都用了同步锁。

            +

            Java监视器模式

            使用内置锁

            -
            ThreadPoolExecutor
              -
            1. ExecutorService

              -

              ThreadPoolExecutor继承了该接口。

              -

              是Executor接口的加强版,包含了更多方法,具体为:

              -

              ① 自身生命周期的管理 shutdown、isshutdown等等

              -

              ② 对异步任务的支持 返回Future的submit方法

              -

              ③ 对批处理任务的支持 invokeall

              -
            2. -
            3. 内部原理

              -

              当空闲的线程足够多,直接执行;当线程不够多,进入阻塞队列;当阻塞队列满,使用拒绝策略。

              -

              内部的线程池分为救急线程和核心线程。核心线程一直存在,当阻塞队列和核心线程都不够用,就会新开几个救急线程。

              -
            4. -
            -

            执行策略

            +

            直白点来说,就是把所有要访问自己状态的地方/方法通通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);
            }
            }
            - + - -

            说得非常全面

            - - +

            将线程安全性委托给独立的状态变量

            -

            66666

            - +

            定义

            + +

            意思就是保证一个类里面仅有一个状态,只要该状态是线程安全的,那么该类也就是线程安全的

            +

            示例

            //线程安全
            public class Point {
            public final int x,y;

            public Point(int x, int y) {
            this.x = x;
            this.y = y;
            }
            }
            -

            Executor的生命周期

            +
            //将线程安全委托给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();
            }
            }
            }
            -

            我们结束executor,可以采取或温和或粗暴的方法:可以让它不接受新的,慢慢执行完全部再结束;也可以让它直接全部结束,管它有没有执行完或者有没有还没被执行,就跟断电一样。

            - + - + - +
            public Map<String,Point> getLocations(){
            return Collections.unmodifiableMap(new HashMap<>(locations));
            }
            -
            -

            此处疑问:不应该先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);
            }
            }
            +

            委托给多个状态变量

            +

            也就是说这些对象彼此不会构成不变性条件。

            +
            //将线程安全性委托给多个状态变量
            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);
            }
            }
            -

            延迟任务与周期任务

            + -

            Timer类的缺陷

            单线程带来的精确性问题
            +

            而且键盘监听和鼠标监听彼此独立。

            +

            不独立多个状态变量不能委托

            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());
            }
            }
            -
            线程泄漏
            + - +

            根本原因就是因为不独立。

            + - -
            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();
            }
            }
            }
            +

            发布被委托的状态变量

            什么时候可以发布

            + -

            找出可利用的并行性

            +

            示例

            -

            所以并发编程最难的其实还是建模,如何从串行中挖掘出并行性。

            - +
            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);
            }
            }
            -

            这个把文字的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

            +

            这仅仅是一个委托发布的实例。

            +

            在现有的线程安全类中添加功能

            引论

            - +

            比如说想给vector添加一个“put-if-absent”

            + -

            意思就是Callable比Runnable有时候更灵活,因为Callable可以抛出异常,也可以有返回值。

            -

            Future

            +

            可以用子类扩展法,也可以直接加源代码。后者有时候源代码不可访问,前者的父类很多域可能不对子类开发,并且非常脆弱。因而下面介绍几种比较好的机制。

            +

            客户端加锁机制

            定义和实例

            - +
            @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;
            }
            }
            -

            这个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;
            +

            我曹,66666666

            +

            也就是说,这里加的是ListHelper的锁,只能让别的线程不能通过putIfAbsent方法同时修改list,但别的线程完全可以直接获取list再修改。

            + -

            其中,get的方法取决于任务的状态

            - +

            客户端指的是我们的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;
            }
            }
            }
            - +

            评价

            -

            可以利用返回的Future实例来对任务线程进行管理。

            - + +

            它非常依赖于其他类的客户端加锁机制。

            + +

            确实,毕竟你锁被外界拿去用了。

            +

            组合

            -

            Future实现并行渲染

            将要求分解为两个任务:渲染文本和渲染图像。

            - +
            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接口其他未实现的方法
            }
            -
            //使用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());
            }
            }
            }
            + +

            是的,跟synchronizedList非常像

            + +

            这也就是用的java的监视器模式了

            +

            第五章 基础构建模块

            -

            在异构任务并行化中存在的局限

            +

            保证独立即可委托,从而构建一个线程安全类

            + - +

            同步容器类

            - +

            差不多都是使用的监视器模式。

            +

            同步容器类:Vector、Hashtable、Collections.synchronizedXxx

            +

            同步容器类的问题

            -

            所以难点还是分解同构任务。

            -

            CompletionServiceExecutor和BlockingQueue

            +

            也就是需要避免两个原子操作之间的非线程安全的时间间隔。

            +
               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);
            }
            -

            CompletionService的思想其实和这个差不多。它主要就是多包装一层,数据结构的管理不用你写,更加方便。

            - + - +

            它这段话说得非常好。由于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
            }
            }
            }
            -

            也就是说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());
            }
            }
            }
            - +

            迭代器与ConcurrentModificationException

            -

            疑问

            我这里写了一个自己用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());
            }
            }
            }
            +

            所以才会引入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);
            }
            }

            - + - + -
            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);
            +

            并发容器类

            同步容器类的加锁太粗粒度了,导致并发性弱。因而引入并发容器类来解决问题。

            + +

            ConcurrentHashMap —— HashMap

            +

            CopyOnWriteArrayList —— List

            + + -

            示例:旅行预定门户网站

            +

            BlockingQueue

            + - +

            ConcurrentSkipListMap —— TreeMap

            +

            ConcurrentSkipListSet —— TreeSet

            +

            ConcurrentHashMap

            - +

            使用分段锁来细粒度加锁。

            + -

            也就是说,跟前面的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;

            }
            +
            +

            关于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。

            + -

            使用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 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 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;}
            }
            +
            //启动生产者-消费者程序
            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();
            }
            }
            - -

            所以解决方法其实很简单,只要让阻塞状态下我们还能知道要取消任务就行。这靠我们在表层写代码是做不到的,需要用到Java提供的另一种协作机制:线程中断。

            -

            中断

            是什么

            - +

            串行线程封闭

            -

            所以说中断其实就是为取消而量身定做的。

            - +

            安全发布

            + -
            public class Thread{
            public void interrupt();
            public boolean isInterrupted();
            public static boolean interrupted();
            }
            + - +

            6666666

            +

            之所以叫“串行”,想必是因为这个过程:发布-转接-放弃访问权是一个串行过程。

            + - -

            所以作为上层开发者,我们仅需捕获中断异常即可。

            - -

            也就是说,打断阻塞状态下的线程会清空中断状态,打断正常状态的线程会保持中断状态。而正常状态的线程如果不对中断状态处理,就会一直保持中断状态然后继续运行,也就是屏蔽中断状态。

            - +

            总而言之,串行线程封闭的具体做法就是,一个线程将一个安全发布的对象的所有权完全转移给另一个线程,保证之后自己不会再使用。这样一来,该对象就相当于被另一个线程封闭了。而如何保证“自己以后不再使用”呢?最简单的方法就是安全发布完这个东西后直接把这个东西给踢出去

            +

            阻塞队列是自动会把这个东西安全发布然后就踢出去的,所以说阻塞队列简化了这个工作。

            + -

            具体检查方法还是定期看标记。在看到标记后可以做善后工作再决定停不停。

            - -

            程序清单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();
            +

            当某方法抛出InterruptedException时,表明该方法为阻塞方法,也即这个方法会在执行过程中由于各种原因而被阻塞。如果这个方法被中断,它将会努力提前结束阻塞状态。

            +

            中断方法

            + +

            处理InterruptedException的两种选择

            -

            一个有待解决的疑问

            此处编写主函数运行时,不小心产生了一个错误: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();
            }
            +

            传递InterruptedException

            -

            这段代码跑起来的最终结果就是进程永远无法终止。至于为什么:

            -

            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();
            }
            +
            +

            此处关于为什么如果代码是Runnable的一部分就不能抛出异常:

            +

            是因为java的异常继承体系。

            +

            在重写的run方法中,我们只能够进行异常的捕获而不能够抛出异常,原因是因为在父类Runnable接口中,run方法没有抛出异常,则实现Runnable的子类就无法抛出异常

            +

            所以实际上是语法层面上不允许。

            +
            + -

            但我还是有个奇思妙想。可不可以沿用一开始那个错误的主方法版本,然后修改PrimeProducer类为:

            -
            //此处修改为Runnable
            public class PrimeProducer implements Runnable {
            //......

            //此处修改
            public void cancel(){Thread.currentThread().interrupt();}

            //......
            }
            + -

            结果还是跑不起来,不知道为什么,有待解答。

            -

            中断策略

            - - +

            同步工具类

            -

            意思就是,单个的任务是非线程所有者,因为它们是被分配到线程池所有的线程执行的。所以它们不能直接对中断进行处理,需要把中断异常抛给那个目前还不知道是谁的所有者线程,让调用者决定自己该怎么做。

            - +

            实现同步的方法:使用同步容器类/并发容器类、使用锁、使用同步工具类

            + - - - +

            闭锁 CountDownLatch

            作用

            + +

            CountDownLatch

            -

            以下的地方一个字也看不懂,写自己的思考也没什么意义。就附上正确代码模板吧。

            -
            //通过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);
            }
            }
            +
            //使用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

            -]]> - - books - - - - 计算机组成原理 - /2023/06/21/comporgan/ - 概述

            架构

            冯诺依曼

            以运算器为中心,指令和数据同等地位(不满足摩尔定律)

            -

            image-20230617133555268

            -

            存储器为中心

            image-20230617133840406

            -

            哈佛架构

            哈佛结构数据空间和程序空间是分开的

            -

            大部分ROM操作部分是采用了冯诺依曼结构

            -

            有些需要CPU与ROM之间快速的响应和交互,采用的是5级流水的哈佛结构。

            -

            早期(如X86)采用冯诺依曼

            -

            DSP和ARM用改进哈佛

            -

            image-20230617134010940

            -

            现代计算机

            image-20230617134045099

            -

            RISC-V

            数的表示

            无符号数与有符号数

            机器数与真值

            image-20230619211124996

            -

            意思就是真值有±符号,机器数把±符号换成了数字罢了

            -

            原码/补码/反码/移码

            image-20230617142534411

            -
            原码

            整数用逗号隔开,小数用小数点隔开

            -
            整数

            image-20230617140847726

            -
            小数

            image-20230617140920563

            -
            注意0的特殊情况

            image-20230617141011120

            -
            补码
            -

            对于负数,原码->补码符号位不变,数值位按位取反再加一的理论由来。神奇的是对于补码->原码,也是按位取反再加一。

            -

            简单证明一下:

            -

            设x为补码,y为原码,n为位数

            -

            已知 x = !(y - 2^n) +1

            -

            则反转一下可得 y = !(x - 1) + 2^n

            -
            -

            符号位不变,按位取反再加一

            -
            整数

            image-20230617141209384

            -
            小数

            image-20230617141232325

            -
            特殊

            [y]补连同符号位在内,每位取反末位加1,即得**[-y]补**

            -

            后面那三个是真的抽象

            -

            image-20230617141627719

            -
            反码

            对于正数,反码和原码一致;

            -

            对于负数,反码为原码的数值位取反

            -
            整数

            image-20230617141351461

            -
            小数

            image-20230617141418617

            -
            0

            image-20230617141525918

            -
            移码
            -

            注意,移码只有整数形式的定义,这与它的用途有关。计算机中,移码通常用来标识浮点数的阶码【阶码是整数】。

            -
            -

            与补码数值位计算方式相同,区别是符号位相反

            -

            image-20230617142858076

            -

            image-20230617145309340

            -

            注意,移码的0为100000,最小值为000000

            -

            浮点表示

            表示形式和范围

            image-20230617143220170

            -

            注意,这边的上溢和下溢只与阶码有关,与尾数无关。

            -

            这个溢出条件及其处理方式需要记,会考

            -

            image-20230617143328674

            -

            规格化

            image-20230617143654003

            -
            特例

            image-20230617143731873

            -
            范围

            image-20230617143912465

            -

            image-20230617145251169

            -
            题型 表示范围

            image-20230617145826541

            -

            看得我cpu快烧了

            -

            https://www.jianshu.com/p/7b9dd240685c

            -

            image-20230617145857698

            -

            之所以最小负数不一样,是因为原码不能表示-1,补码可以;

            -

            之所以规格化最大负数是那玩意,是因为最大负数本应为2^-8,为了规格化必须再加个2^-1,然后原码转补码就变成那样了

            -
            -

            IEEE754标准

            难绷,最沙比的来了

            -

            image-20230617150024955

            -

            没有阶符和数符力

            -

            image-20230617150101092

            -

            image-20230617150816446

            -

            它就相当于指数是移码表示的,并且注意到一点就是指数的0和255被征用表示特殊的数了,所以指数范围为1-254

            -

            image-20230617151013979

            -

            题型 把数转化为IEEE754

            首先背一下上面那个数的范围图,然后判断下是规格化还是非规格化,然后套公式就行了

            -

            image-20230617152014370

            -

            image-20230617152309065

            -

            算术移位与逻辑移位

            -

            来自 https://blog.csdn.net/qq_34283722/article/details/107093193

            -

            img

            -

            这应该与补码的运算机制有关。

            -
            -

            image-20230617152527973

            -

            反码不论是左还是右都添1

            -

            image-20230617152938114

            -

            注意,符号位不变!!!这点在左移的时候需要尤其注意,很容易出错

            -

            RISC-V概述

            ISA

            ISA位宽:通用寄存器的宽度,决定了寻址范围大小、数据运算强弱。

            -

            CISC-RISC

            image-20230619211742901

            -

            X86 & MIPS

            相比于上述的差异,还有以下几个:

            -
              -
            1. x86有8个通用寄存器,MIPS有32个
            2. -
            3. x86有标志寄存器,MIPS没有
            4. -
            5. x86为两地址指令,MIPS为三地址
            6. -
            7. x86有堆栈指令,MIPS没有
            8. -
            9. x86有IO指令,MIPS设备统一编址
            10. -
            11. x86函数参数只用栈帧,MIPS用4寄存器+栈帧
            12. -
            13. X86的字为2字节,MIPS/RISC-V的字为4字节
            14. -
            -

            RISC-V的特点

              -
            1. RISC-V是小端,也即低字节放在低地址

              -
            2. -
            3. 支持字节(8位)、半字(16位)、字(32位)、双字(64位,64位架构)的数据传输

              -

              主存按照字节进行编址

              -
            4. -
            5. 采用哈佛结构

              -
            6. -
            7. 三种特权模式

              -

              image-20230617155657577

              -
            8. -
            9. 模块化设计

              -

              image-20230617155224429

              -
            10. -
            -

            RISC-V汇编语言

            寄存器

            image-20230619212236116

            -

            image-20230617194244388

            -

            x3的全局指的是全局的静态数据区

            -

            指令详解

            image-20230617160729611

            -

            算术指令

            RISC-V 忽略溢出问题,高位被截断,低位写入目标寄存器

            -

            如果想要保留乘法所有位:

            -

            image-20230617183814367

            -

            image-20230617183924758

            -

            image-20230617184010487

            -

            逻辑指令

            image-20230617184151843

            -

            移位指令

            image-20230617185225785

            -

            shift left logical,shift left arithmetic

            -

            数据传输

            ld/sd,lw/sw,lh/sh(半字,也即2字节),lb/sb,以及load指令对应的无符号数(+后缀u)版本。

            -

            bAddrReg+offset为4的倍数,数据传输指令除了字节指令(lb sb lbu)外都需要按字对齐。

            -

            注意,如果为有符号数取数,放入寄存器时会自动进行符号扩展

            -

            image-20230617191543433

            -

            比较指令

            image-20230617192731684

            -

            条件跳转指令

            image-20230617193045409

            -

            无条件跳转指令

            image-20230617193346852

            -

            j:+label,用于实现无条件跳转,使用相对于当前 PC(程序计数器)的偏移量来计算目标地址,跳转范围较广

            -

            jr:+寄存器,用于实现通过寄存器的值进行跳转,跳转的目标是存储在寄存器中的地址,而不是相对于 PC 的偏移量

            -

            伪指令

            image-20230617193450180

            -

            image-20230617193521343

            -

            函数调用及栈的使用

            image-20230617195109547

            -

            六种指令格式

            image-20230617213801145

            -

            注意,jalr属于I型指令,而非J型指令!!!

            -

            image-20230617213815058

            -

            image-20230620164851924

            -

            R型指令

            image-20230617214018489

            -

            image-20230617214139338

            -

            I型指令

            image-20230617214544561

            -

            image-20230617214659517

            -

            image-20230617214735023

            -
            特例1 load

            image-20230617215007481

            -
            特例2 jalr

            image-20230617215304022

            -

            注意,jalr也属于I型指令,且其funct3为0

            -

            S型指令

            image-20230617215730749

            -

            image-20230617215842401

            -

            B型指令

            image-20230617220521906

            -

            这个计算过程很值得注意

            -

            image-20230617220833542

            -

            image-20230617220903564

            -

            image-20230617221131122

            -

            U型指令

            image-20230617221220495

            -

            image-20230617221323268

            -

            image-20230617221508473

            -

            666

            -

            J型指令

            image-20230617222154873

            -

            寻址方式(x86)

            -

            img

            -

            img

            -

            img

            -

            imgimg

            -

            尽管A很小,但可以让EA很大,从而扩展寻址范围。同时相对于上面的直接寻址,它更容易编程,因为只用修改A存储的那个地址值,而不用修改指令【比如说对数组进行循环,这个间接寻址就只用A++就行,而不用去修改指令里的那个“A”。】。

            -

            That is 指针【】

            -

            img

            -

            img至于为啥间接寻址不便于循环,也许是因为间接寻址是访存两次比较慢,要是真用来循环还了得

            -

            imgimg

            -

            程序动态定位

            -

            img循环数组时,可以用A作为数组地址,IX作为数组下标???【为什么不能用基址寻址?】

            -

            应该是因为基址寻址的基址是系统内定的,数组循环问题需要用户指定数组起始地址,所以不能用基址寻址,只能用面向用户的变址寻址。

            -

            img区别就在于直接寻址直接把指令参数****硬编码在内存****中,非常耗费空间。变址寻址则把指令参数作为变量了。

            -

            img更应该像是指令寻址方式。

            -

            程序浮动:程序在内存单元的位置出现变化【毕竟不可能同一个程序在每台电脑都是在同一个物理地址,相当于又减少了硬编码】

            -

            imgimg【为2002H是因为假设字长为2byte】

            -

            imgimg

            -

            一般栈顶地址最低。

            +

            java的future机制原理

            +

            关于Future:

            + + +

            其中get方法是阻塞的。

            +

            获取异步任务执行完后的结果。

            +

            关于FutureTask:

            +

            FutureTask既包含了Future的语义,又包含了Runnable的语义。

            +

            它其实内部封装了一个Runnable Task。调用FutureTask的run,其实本质上就是调用Task的run,只不过要多一些检查和存储结果之类的手续。

            +

            所以说它其实就是通过内部封装一个线程,然后就能获取这个线程运行的状态和运行的结果等等等,这样来实现Future语义的。

            -

            运算方法

            定点运算

            一位乘法运算

            原码一位乘

            -

            image-20230621195833297

            -

            大致明白了:

            -

            ①乘积一共有四位,故而需要两个寄存器来保存。

            -

            ②按照上面的原理公式,每次右移一位,被移出的那一位也是最后的结果(相当于竖式中每次相加的最后一位),需要把它存储在另一个寄存器中。

            -

            ③我们选择了存乘数的寄存器,因为乘数已经乘过的位是没用的。存乘数的那个寄存器的乘数不断被结果的低位所替代。

            -

            故****基本流程****:

            -

            ①准备阶段:清零ACC【置部分积=0】,在MQ中放乘数,X中放被乘数

            -

            ②判断MQ中乘数最低位,若为1,则ACC部分积加上X中的被乘数;若为0,则ACC不变

            -

            ③将ACC和MQ中四位数字视作一个整体,符号位也算上,进行逻辑右移,左侧补0.

            -

            ④重复上述过程,按移位次数来控制结束。

            -

            ⑤则最后,ACC中存储的就是乘法结果的高位,MQ中存储的结果就是乘法中的低位。

            -

            这其实就是我们用的列竖式一行一行加起来的一个过程。

            -

            img

            -

            S是符号位,GM是乘法标志位。

            -

            控制门:当最后一位是1时,控制门打开,X中的被乘数进入加法器。

            +
            +

            关于Callable

            +

            Runnable里面的run方法是不能传参,也没有返回值的。Callable相当于有返回值的Runnable,也即书中说的“有生成结果的Runnable”。

            -
              -
            1. 部分积 乘数

              -
            2. -
            3. 乘数不用符号位,写数值位即可

              -
            4. -
            5. 按照是0是1,要么+被乘数要么+0

              -
            6. -
            7. 右移(连符号位一起逻辑右移)

              -

              image-20230620232152706

              -
            8. -
            9. 直到乘数全部移完

              -
            10. -
            -

            Booth算法

              -
            1. 部分积 乘数 y补(一开始为0)

              -
            2. -
            3. 部分积双符号位,乘数单符号位且参与运算

              -

              image-20230620212054291

              -
            4. -
            5. 每次依据乘数和y补的关系,进行是否加被乘数的决策:

              -

              注意右移不同于原码,是算术右移

              -

              image-20230620212122222

              -
            6. -
            7. 最后一步不用移位

              -

              image-20230620212150280

              -
            8. -
            -

            除法运算

            逻辑左移

            -

            最后得到的余数还得乘个2的-n次方

            -

            恢复余数法

              -
            1. 被除数(余数) 商
            2. -
            3. 先加上 - 除数的补
            4. -
            5. 如果得到结果≥0,则上商1,左移
            6. -
            7. 如果小于0,则上商0,+除数补,左移
            8. -
            9. 左移5次(商包括符号位的所有数字被填满),最后一次上商不用移位
            10. -
            -

            不恢复余数法(加减交替法)

            -

            image-20230621200001791

            -

            总结一下,大概流程:

            -

            ①准备阶段:MQ清零【存放商】,ACC放入被除数,X放入除数

            -

            ②ACC - X中的值

            -

            ③若ACC中值【上一轮的余数】为负,则上商0;为正,则上商1.ACC左移一位。判断MQ的最后一位【上商的值】,若为负,则ACC + X中的y;为正,ACC - X中的y。【注意,若为第一次减去X,则当余数为正时,就即刻发生溢出错误退出】

            -

            ④重复③,直到移位n次。

            -

            img

            -

            V表示是否溢出。

            + + + + +
            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

            + +
            +

            注意

            +
            -
              -
            1. 被除数(余数) 商

              -
            2. -
            3. 先加上 - 除数的补

              -
            4. -
            5. 如果得到结果≥0,则上商1,左移,下一次继续加 - 除数的补

              -
            6. -
            7. 如果小于0,则上商0,左移,下一次加除数的补

              -

              image-20230620234054088

              -

              逻辑左移

              -
            8. -
            9. 左移5次(商包括符号位的所有数字被填满),最后一次上商不用移位

              -
            10. -
            -

            浮点运算

            舍入

            -

            (1)意思是,舍去的要是1,就在保留数+1.如果是0就直接舍去。

            -

            img这意思难道是说可以一次性右移,最后再看要不要+1,而不是移一下加一次1?【不过想了一下,这两种顺序得到的结果好像是一样的。】

            +

            这种情况下,其实信号量跟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*/
            -

            快速进位链

            -

            https://www.bilibili.com/video/BV1AB4y1p7ax?spm_id_from=333.880.my_history.page.click&vd_source=ac571aae41aa0b588dd184591f27f582

            -

            以及老师在这讲的也挺好的【p88】

            -

            imgimg

            -

            当AiBi都为1时,无论Ci是什么,都必定进位1;当AiBi有一个为1时,Ci才会起决定性作用;当AiBi都为0时,无论Ci是什么,都不会进位。因此,AiBi为本地进位,Ai+Bi为传送条件。(乘号表示且,加号表示或)

            -

            img进位链是影响加法器速度的瓶颈

            -

            img但问题是电路太复杂了,因此给出折中方案:

            -

            img

            -

            4先产生进位,传给3,3再产生进位,传给4,依次下去。

            -

            img

            -

            imgimg

            -

            imgimg

            -

            相当于又套了一层并行进位链。

            -

            img实在是太强了。感受到还要再套一层分组的必要性了。

            +

            栅栏 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线程池其实不难,只要搞清楚来龙去脉

            -

            处理器

            RISC-V数据通路的组件选择

            image-20230617232028775

            -

            RISC CPU采用哈佛架构。

            -

            存储器

              -
            1. DM Data Memory 数据存储器

              -

              读异步,写有写使能

              +
              解耦

              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. IM Instruction Memory 指令存储器

                -

                一般read only

                +
              4. 内部原理

                +

                当空闲的线程足够多,直接执行;当线程不够多,进入阻塞队列;当阻塞队列满,使用拒绝策略。

                +

                内部的线程池分为救急线程和核心线程。核心线程一直存在,当阻塞队列和核心线程都不够用,就会新开几个救急线程。

              -

              寄存器堆

              同步写异步读

              -

              image-20230617233101214

              -

              立即数扩展(生成)部件

              零扩展、符号扩展

              -

              PC(程序计数器)

              支持两种加法:+4、+立即数

              -

              ALU

              -

              【以下运算器结构适用于累加型运算器。累加器好像意思是一次最多两个输入。 】

              -

              运算器的功能是运算,因此其核心就是ALU(算术逻辑单元)。ALU是一个组合电路,组合电路的特点是,如果输入撤销了,那么输出结果也会撤销【组合逻辑电路】。因而,为了让ALU的结果能被保存,必须在输入端加上两个寄存器来保证信号持续输入。这两个寄存器一个叫做ACC,另一个叫做x,也叫做数据寄存器。

              -

              imgMQ也是寄存器,用于保存计算过程中溢出的位数。

              -

              img具体见第六章,弹幕说汇编语言也有讲。乘法要这样放是为了防止乘积低位覆盖乘数。

              -

              img

              -

              imgACC里存放着上面的操作或者与外部交流得到的被乘数,按照约定需要转移到X里。我猜M放在MQ而不是ACC,可能是因为第一二步是并行的,如果放在ACC就需要一些等待。

              -

              并且乘法做的是移位累加【可能相当于上面乘法原理的第一个图吧】,ACC用来存储这些累加的暂时交换成果,因而需要将ACC先清空为0.

              -

              这些操作的先后顺序由控制器进行控制。

              -

              img

              -

              MQ也称乘商寄存器

              -
              -

              运算类型:加、减、或、比较、slt、nor

              -

              操作数:寄存器或立即数

              -

              image-20230619214753499

              -

              RISC-V部分指令的数据通路设计

              取数指令的完成过程

              -

              image-20230621192714673

              -

              下面是取数指令的完成过程。

              -

              完成一条指令有三个阶段:取指令、分析指令、执行指令。

              -

              取指令:PC把地址送到MAR,MAR把地址送到存储体。存储体在控制器的控制下,把地址所对应的指令的内容发给MDR,MDR把取出的指令送到IR.

              -

              分析指令:IR将指令的操作码部分交予CU,CU控制IR,IR将指令中的地址码部分交予MAR,MAR给存储体,存储体在控制器控制下给MDR,MDR送给ACC。

              -

              【这个过程正像是计算机网络,只不过此处全靠硬件完成,计算机网络只能依靠协议】

              -
              -

              流水线周期

              RISC-V

              image-20230617233444017

              -

              image-20230618170512788

              -

              注意,在ID阶段还会发生读寄存器

              -

              image-20230617233433422

              -

              X86

              -

              一、指令周期

              -
                -
              1. 基本概念
              2. -
              -

              ① 指令周期

              -

              ② 每条指令的指令周期不同

              -

              imgADD取指阶段和执行阶段都需要一次访存

              -

              ③ 具有间接寻址的指令周期

              -

              img

              -

              三个周期各需要访存一次。【****现在暂时还不知道这有毛用****】

              -

              ④ 具有中断周期的指令周期

              -

              img

              -

              ⑤ 指令周期的流程

              -

              img

              -

              ⑥ CPU工作周期的标志

              -

              指令周期的不同阶段,控制器要做不同的操作,要发出不同的命令。因而,控制器需要知道当前处于指令周期的哪一个阶段。

              -

              img用四个触发器

              -
                -
              1. 指令周期的数据流
              2. -
              -

              ① 取指周期

              -

              img

              -

              首先,PC把自己里面存的地址放进MAR,再通过地址总线传输给存储器。

              -

              CU通过控制总线向存储器发出读控制信号。

              -

              存储器执行读操作,通过数据总线传输取到的指令给MDR,MDR再传给IR。

              -

              CU把加一后的地址保存在PC中,为下一条指令取指做准备。

              -

              ② 间址周期

              -

              img

              -

              如果指令的数据部分采用的是间接寻址的方式,那么此时,MDR中的地址部分不是有效地址,而是存储存储有效地址的存储单元的地址值。因而,我们需要再通过一次访存操作,把有效地址值存储在MDR中。

              -

              ③ 执行周期

              -

              img留给第九章介绍。

              -

              ④ 中断周期

              -

              做了三件事:保存断点、形成服务程序入口地址、中断返回

              -

              img

              -

              首先,保存断点。由CU来确定断电保存在内存单元的哪里。CU把地址传给MAR,MAR将其发到存储器,CU给存储器写命令。PC将自己的值【也就是下一条要执行的命令的地址值】交付给MDR,MDR传给存储器。【MDR在读写操作时都充当了缓冲区的角色。】

              -

              然后,CU形成中断服务程序入口地址,并直接把它写入到CU。

              +

              执行策略

              + + + + + +

              线程池

              + + + + + +

              说得非常全面

              + + + + +

              66666

              + + + + +

              Executor的生命周期

              + +

              我们结束executor,可以采取或温和或粗暴的方法:可以让它不接受新的,慢慢执行完全部再结束;也可以让它直接全部结束,管它有没有执行完或者有没有还没被执行,就跟断电一样。

              + + + + + + +
              +

              此处疑问:不应该先shutdown再awaitTermination吗?我百度了,也都是说先shutdown。毕竟awaitTermination方法是阻塞的。

              -

              流水线处理器

              流水线概述

              流水线

              image-20230618150003983

              -

              这点我觉得讲得挺好的。以前只知道流水线通过并行来加速指令执行,但这里给出了一个新的思路:如果是单周期处理器,则RISC-V的时钟周期受执行时间最长的指令限制;如果是流水线处理器,时钟周期就可以由某个步骤决定,主频就可以加快。这个出发点很有意思。

              -

              如果流水线各阶段平衡,也即每个阶段需要的执行时间差不多,则

              -

              image-20230618150515599

              -

              也即在理想条件和有大量指令的情况下,流水线带来的加速比约等于流水线的级数,若各阶段不完全平衡,加速比会变小。

              -

              流水线技术是通过提高指令的吞吐率来提高性能的。

              -

              RISC-V与流水线

              我们可以看到,比起X86,RISC-V是面向流水线设计的,其特性与流水线高度相关:

              -
                -
              1. 指令长度相同

                -

                简化IF和ID

                -
              2. -
              3. 只有六种指令格式,格式整齐

                -

                能在一个阶段内完成译码和读寄存器(ID)

                -
              4. -
              5. 只通过load、store访存

                -

                可以利用EX阶段计算存储器地址,然后在下一阶段访存(MEM)

                -
              6. -
              -

              流水线冒险

              image-20230618151040625

              -

              结构冒险

              image-20230618151208189

              -

              数据冒险

              image-20230618151243620

              -
              解决方法
              前递

              image-20230618151601721

              -
              编译重排

              image-20230618151706080

              -
              停顿(气泡)

              实在不行只能暂停流水线了

              -

              image-20230618151637437

              -

              控制冒险

              image-20230619220308876

              -
              解决方法
              硬件支持

              image-20230619220259945

              -
              分支预测
                -
              1. 遇到分支预测就停顿

                -
              2. -
              3. 分支预测

                + + +
                //支持关闭操作的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 + + + + 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. 静态分支预测

                  -

                  image-20230618153106322

                  -
                2. -
                3. 动态分支预测

                  -

                  image-20230618153125295

                  -
                4. -
                +
              4. mysql的数据类型表

                +

                image-20221205222127740

                +

                其中:

                +

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

                +

                ② 关于三个时间类型

                +

                image-20221205222307284

                +

                所以timestamp常用作插入时间。

                +

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

                +

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

                +

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

              -

              流水线数据通路和控制

              流水线数据通路

              流水线寄存器

              image-20230618154028950

              -

              image-20230618154608423

              -

              66666,这个帅

              -

              image-20230618154815411

              -

              流水线控制

              数据冒险:前递与停顿

              前递

              分类

              前递有两种情况:

              -

              image-20230618155952018

              -
              前递产生条件
                -
              1. RegWrite != 0(写有效)
              2. -
              3. Rd != x0
              4. -
              -
              解决方法

              流水线寄存器解决:

              -

              image-20230618160141827

              -

              并且增加前递所需硬件。

              -

              停顿

              流水线寄存器解决:

              -
                -
              1. 置ID/EX寄存器中控制信号为0(防止寄存器和存储器被写入数据),执行空指令nop

                -
              2. -
              3. 禁止PC寄存器和IF/ID寄存器内容改变

                -

                下一条指令就能重新取指

                +
                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. -
                -

                控制冒险

                image-20230618161320779

                -

                image-20230618161333487

                -

                缩短分支延迟的方法:

                -

                硬件支持

                image-20230618161429557

                -

                动态分支预测

                image-20230618161807387

                -

                image-20230618161932429

                -

                计算目标地址

                image-20230618162114338

                -

                流水线的多发技术

                img

                -

                img

                -

                超流水技术要求一个时钟周期内不同的指令不能相互叠加干扰。

                -

                img

                -

                意思就是多条指令并成一条,有公共的取指、译码、写回阶段,但是执行阶段各不相同且并行执行,应该是这样。

                -

                例外和中断

                概述

                image-20230618162247586

                -

                内部的一定是例外,外部的只有IO请求和硬件故障是中断

                -

                image-20230618162302149

                -

                image-20230618162437636

                -

                image-20230618162500928

                -

                哦哦哦WOC!!!!!

                -

                这让我想起来在做xv6的时候,的那个kerneltrap和usertrap,应该就是这里的这个统一入口地址。

                -

                xv6是RISC-V架构,故而发生中断的时候,就会跳转到统一的kernel trap,然后再在里面通过scause进行读取。666

                -

                不过盘问了下gpt,RISC-V对于exception和interruption的处理方式是不一样的:

                -

                在RISC-V中,异常通常是由于程序执行过程中的错误或非预期事件而引起的,包括故障(faults)、陷阱(traps)和中止(aborts)。中断(interrupts)则是由外部事件触发的,例如定时器到期、外部设备请求等。中断是异步事件,与当前正在执行的指令无关,因此会在任何时候发生。

                -

                例外是通过统一入口地址处理,中断则是中断向量的方式

                -

                流水线中的例外

                image-20230618163521639

                -

                微操作(X86)

                X86将一条指令的执行分为多个微操作。

                -
                -

                一、微操作命令分析

                -

                微操作命令是控制单元在完成一大条指令时所需要细分完成的一条条微小的命令

                -

                image-20230621201435702

                -
                  -
                1. 取值周期

                  -

                  image-20230621201345807

                  +
                2. 逻辑运算符

                  +

                  AND、OR

                3. -
                4. 间址周期

                  -

                  image-20230621201351939

                  +
                5. BETWEEN AND

                6. -
                7. 执行周期 ①访存指令 ②非访存指令 ③转移指令 ④三类指令的指令周期

                  -

                  image-20230621201358396

                  -

                  imgimg

                  -

                  image-20230621201423245

                  +
                8. IN后跟集合

                  +

                  image-20221205230824086

                9. -
                10. 中断周期 硬件法和软件法

                  -

                  imgimg

                  -

                  硬件和软件法。

                  +
                11. IS、IS NOT

                  +

                  image-20221205230902110

                12. +
                13. LIKE 模糊查询

                  +

                  类似正则使用占位符匹配

                  +

                  image-20221205231037021

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

                二、控制单元的功能

                -
                  -
                1. 输入信号

                  -

                  ①时钟信号 ②指令寄存器【控制信号与操作码有关】 ③标志 ④外来信号【中断请求、总线请求】

                  -
                2. -
                3. 输出信号

                  -

                  ①CPU内各种控制信号【比如(PC)+1->PC这种】

                  -

                  ②送至控制总线的信号【比如中断响应、总线响应】

                  -
                4. -
                5. 控制信号举例

                  -

                  ①不使用内部总线

                  -

                  ②采用内部总线

                  -
                6. -
                7. 多级时序系统

                  +

                  各种函数一样的东西

                  排序函数
                  order by 排序字段1 排序方式1,排序字段2 排序方式2;
                  +
                    -
                  1. 机器周期

                    -

                    取指周期=机器周期=最复杂的微操作所需时间【访存】

                    -

                    在机器周期内部也需要有时钟来控制微操作的执行顺序

                    -
                  2. -
                  3. 时钟周期(节拍、状态)

                    -

                    每个指令周期都可分为若干个机器周期,每个机器周期都可分为若干个节拍(时钟周期)。一个机器周期内包含多少节拍与需要发送多少控制信号、控制信号复杂度、控制信号能否并行有关。

                    -

                    时钟产生节拍信号,不同的节拍信号有不同的先后顺序。

                    -

                    一个时钟周期产生一个或几个【并行的几个,或者是操作时间很短,虽然有一定的先后顺序,但可以在一个节拍内完成】微操作命令

                    -

                    时钟信号利用上升沿让CU发出控制命令【微操作】控制各个不同部件。

                    +
                  4. 默认升序。

                  5. -
                  +
                8. ASC、DESC

                9. -
                10. 控制方式

                  -

                  ①同步控制方式 采用定长的机器周期、不定长的机器周期、中央控制和局部控制相结合

                  -

                  ​ 当指令大多都是可以提前确定的,就用同步。当一条微操作的时间很难控制,可以采用异步控制。

                  -

                  ②异步控制方式 等待IO读写

                  -

                  ③联合控制方式 同步与异步结合

                  -

                  ④人工控制

                  +
                11. 多关键字排序

                  +

                  image-20221205231606255

                  +

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

                -

                三、组合逻辑设计

                +
                聚合函数

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

                +

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

                +
                select count(ifnull(math,0)) from students;
                +
                  -
                1. 组合逻辑控制单元框图

                  -

                  ①CU外特性 ②节拍信号

                  -
                2. -
                3. 微操作的节拍安排

                  -

                  ①安排微操作时序的原则

                  -

                  原则一:先后顺序不更改。

                  -

                  原则二:可以并行执行的,且微操作间没有先后顺序的,就尽量把它们安排在一个节拍中。

                  -

                  原则三:时间较短微操作尽量在一个节拍内且可以有先后顺序。

                  -

                  ②取值周期间址周期执行周期的

                  -

                  image-20230621201709245

                  -

                  image-20230621201717225

                  -

                  image-20230621201722561

                  -

                  image-20230621201732549

                  -
                4. -
                -
                -

                存储器

                概述

                分类

                image-20230618183618033

                -

                层次结构

                -

                寄存器分为两类,体系结构寄存器和非体系结构寄存器。前者可以让程序员调度使用,后者不行。

                -
                -

                image-20230618183742599

                -

                image-20230618183845067

                -

                主存储器

                概述

                基本组成

                -

                MAR中的地址需要经过译码器才能得到对应存储体中的位置。MDR中的数据是读是写需要通过读写电路控制,读写电路接收控制电路的读写信号。

                -
                -

                image-20230618184214916

                -

                与CPU连接

                image-20230618211118255

                -

                小端模式

                image-20230618211717123

                -

                技术指标

                image-20230618211918511

                -

                半导体存储芯片简介

                基本结构

                image-20230618212044462

                -

                image-20230618212116854

                -

                译码驱动方式

                线选法

                image-20230618212207454

                -
                重合法

                image-20230618212227553

                -

                RAM 随机存取存储器

                DRAM和SRAM

                image-20230618222428159

                -

                SRAM

                基本电路
                -

                image-20230621193330855

                -

                核心就是利用****触发器(T1—T4)****来表示0和1的

                -

                用T5和T6行开关来控制对触发器部件读写,用T7和T8列开关……【对应上面说的重合法?】

                -

                写入要在A段写入数据,同时在A’段写入数据的非【因为触发器是双稳态的,要求两边输入的信号相反。】对应的,写选择那边输入数据也得对称经过门和非门。

                -
                -
                经典芯片

                image-20230618212447162

                -
                读写

                img

                -

                上面的部分是64*64的基本电路矩阵。我们按列分,每十六列为一组,则分成了四组。因为2^4=16,因而我们用四位来表示地址控制信号。

                -

                对于行,当地址控制信号为0000时,表示选择存储矩阵的第一行的数据,为0001时,选择第二行的……依此类推。

                -

                对于列,当地址控制信号为0000时,表示选择每一组的第一列的数据,为0001时,选择第二列的……依此类推。

                -

                每一组只能有一列被选中,这就达到了一次读写四位的目的。【一个字节分开存】

                -

                DRAM

                基本电路
                -

                主要是通过电容的充放电实现的

                -

                img

                -

                左侧三管那个中,读数据线读出的跟存储的是相反的,存0读1,存1读0.但写入跟输入的信息是相同的。

                -

                右侧单管中,读出时数据线有电流则是1,没有则是0.写入时,对Cs充电则为1,Cs放电(输入信号为低电平)则为0.

                -
                -

                image-20230618215704699

                -
                经典芯片/读写

                image-20230618215803608

                -
                -

                img

                -

                14位的地址分了两次传,分别作为行列地址。

                -

                RAS:行选控制信号 CAS:列选控制信号 WE:读写控制信号。产生的时钟控制了芯片内部的读写操作

                -

                img

                -

                如果读放大器左边有电,那么右边输出没电;左没电右有电.这样,读放大器左边的部分,有电表示0,没电表示1 ;读放大器右边的部分,有电表示1,没电表示0.

                -
                -
                刷新

                为什么要刷新:

                -

                image-20230619224003589

                -
                集中刷新

                image-20230618221439701

                -
                分散刷新

                image-20230618221603727

                -
                异步刷新

                image-20230618222049113

                -

                ROM 只读存储器

                  -
                1. 掩膜ROM(MROM) 用户不能修改

                  -

                  image-20230618222716561

                  -
                2. -
                3. PROM(一次性编程) 破坏性编程

                  -

                  image-20230618223116398

                  +
                4. count 计算个数

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

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

                5. -
                6. EPROM(多次性编程)

                  -

                  image-20230618223151914

                  +
                7. max、min

                8. -
                9. EEPROM(电可擦写)

                  -

                  image-20230618223229585

                  +
                10. sum 求和

                11. -
                12. Flash Memory(闪速型存储器)

                  -

                  image-20230618223252821

                  +
                13. avg 平均值

                -

                存储器与CPU的连接

                存储器容量的扩展

                位扩展

                image-20230618224638384

                -
                字扩展

                image-20230618224745584

                -

                带了片选思想

                -
                位字扩展

                image-20230618224955557

                -

                存储器与CPU的连接

                存储器的校验

                image-20230620221830055

                -

                汉明码组成

                image-20230620222021345

                -

                image-20230620222133679

                -

                image-20230620222301502

                -

                n为数据的位数

                -

                image-20230620222225994

                -

                image-20230620222518156

                -

                汉明码纠错

                image-20230620222951821

                -

                跟组成的步骤是一样的

                -

                提高访存速度的措施

                image-20230618233156234

                -

                image-20230618233234559

                -

                image-20230618233325427

                -

                image-20230619224341497

                -

                image-20230618233855438

                -

                不过这里也帅得一批,非常有那种从小到大的抽象思维在。

                -

                之前的单独一块RAM芯片,一个字节是分开存;这里的一个主存堆,一个块是分主存存。

                -

                Cache 高速缓冲存储器

                概述

                image-20230618233726163

                -

                技术指标

                image-20230618233800847

                -

                image-20230618234113714

                -

                因为在一个存取周期当中,每体都可以取一个字,16体就可以取16字,因而一个存取周期可以取出16个字出来。

                -

                image-20230618234151529

                -

                但是这个公式前提是访问cache和主存并行。如果换用另一个策略,即先看cache有没有,没有再去主存,计算公式就不一样。

                -

                Cache的读写操作

                -

                img

                -

                cache接收CPU发来的地址信号。CPU发出的地址中的块内地址无需转换,而块号需要通过主存cache地址映射变换机构转化成cache内的块号。【所以说CPU访问cache的时候,传给cache的地址是主存的物理地址吧?然后再通过主存cache地址映射转化为cache的块内地址。】

                -

                如果命中,则转换机构工作,传递地址给cache存储体,存储体通过数据总线发送信号。

                -

                如果不命中,并且cache没装满,则发送信号给主存。

                -

                如果不命中,且cache装满了,则cache替换机构使用替换算法,淘汰cache中一些块,同时发送信号给主存。

                -

                主存收到信号,在数据总线上发给cpu要的东西之后,再将所在块发给cache

                -

                image-20230621193732489

                -
                -

                image-20230618234639119

                -

                image-20230618234752772

                -

                Cache-主存映射

                直接映射

                image-20230618234904098

                -

                image-20230618234921789

                -

                全相联映射

                image-20230618235007647

                -

                组相联映射

                image-20230618235147739

                -

                缓存替换算法

                image-20230618235750985

                -

                改进

                -

                现在很多处理器至少有三级cache。比如每个核一个cache,多个核还有一个公用的cache。

                -

                流水线计算机很多都分了指令cache和数据cache,避免资源冲突。

                -

                注意,每个层次的cache采用的映射可能不一样。

                -

                靠近CPU采用直接相连或者路数(r)少的组相连【其实直接相连就相当于是一路的组相联了】。中间的用组相联。距离CPU较远的用全相联。

                -

                距离越远,对速度要求越低,对利用率要求越高。

                +
                分组查询

                分组之后查询的字段只能是两种:① 分组字段 ② 聚合函数。因为分组了之后再查具有个人特色的东西就没意义了。【高版本的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;
                + +
                +

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

                -

                虚拟存储器

                与Cache的差异

                image-20230619000944183

                -

                虚拟存储器

                image-20230618235955557

                -

                image-20230619000042556

                -

                相当于把主存-辅存(磁盘)看成另一个cache-主存。这也就类似于内存页面换入换出了。原来这玩意叫虚拟存储器啊,不过这也类似于虚拟地址空间的叫法就是了。

                -

                image-20230619000208477

                -

                image-20230619000304314

                -

                页表结构

                image-20230619000428020

                -

                访问流程

                image-20230619000542665

                -

                TLB

                image-20230619000628910

                -

                image-20230619000804374

                -

                image-20230619000827244

                -

                image-20230619000851370

                -

                辅助存储器

                硬盘、U盘、软盘、磁带、光盘

                -

                RAID

                image-20230619142112318

                -

                image-20230619142148621

                -

                image-20230619143428427

                -

                image-20230619143411577

                -

                系统总线

                概述

                是啥

                总线两个特点:分时共享

                -

                遵循协议标准,方便计算机系统集成、扩展和进化

                -

                总线的猝发传输方式:在一个总线周期内,传输存储地址连续的多个数据字的总线传输方式。

                -

                分类

                image-20230618173335493

                -

                image-20230618173414845

                -

                总线结构

                单总线

                注意,单总线是默认统一编址的?

                -

                image-20230618175005524

                -

                面向CPU的双总线

                image-20230618175035406

                -

                存储器为中心

                image-20230618175435591

                -

                有通道的多总线结构

                image-20230618175533637

                -

                image-20230618175631552

                -

                -

                image-20230618175705273

                -

                image-20230618175743501

                -

                总线控制

                总线判优控制

                image-20230618180345695

                -

                image-20230618180704321

                -

                注意,独立请求是最快的

                -

                链式查询

                -

                所有设备可在BR线发布总线请求,主设备通过BG线表态,争得总线的设备要通过BS线告诉其他设备总线忙。

                -

                BG线中,总线同意信号会依次遍历每一个设备,直到找到第一个提出请求的设备。

                -

                可见,这个遍历顺序就代表了各个IO设备的优先级顺序。

                -

                这样相当于分离出格外的线来控制信号。这种方式对电路故障非常敏感。

                +

                主键约束

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

                +

                添加主键约束

                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`;
                + +
                +

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

                -

                image-20230618180431836

                -

                计数器定时查询

                -

                意思好像是,在BR线提出请求,主设备接收到请求后,可以响应的情况下,启动计数器,计数器初始值为零。计数器的值通过设备地址线输出。如果计数器为0,则观察接口0有没有请求,没有的话计数器++,继续看下一个,以此类推,直到找到第一个对应接口,则开始传输数据,BS线启用。

                -

                设备地址线需要给所有设备地址进行编码,因此宽度与设备数有关。

                -

                这个的优点在于,优先级的确定更加灵活了。比如说,计数器不一定从零开始而是从上一次停止的地方开始(循环优先级,这样的话每个设备的机会均等),或者用软件控制优先级初始值,或者每一次不一定++而是有其他计算规则。

                +

                显式内连接

                语法: 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

                -

                image-20230618180602116

                -

                独立请求方式

                -

                优先级由主设备内部逻辑(排队器)规定。也可以用自适应、计数器等等等。

                +

                左外连接

                语法: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-20230618180645765

                -

                总线通信控制

                image-20230618180848436

                -

                这玩意传输周期还考了

                -

                image-20230618180912414

                -

                这个通信方式有哪几种也要求默写了

                -

                image-20230618181827840

                -

                这个同步和异步的特点总结得很棒

                -

                同步、异步、半同步三者的共同点:

                -

                image-20230618181948854

                -

                同步

                -

                img定宽定距的时钟

                -

                白色菱形代表有地址、命令、数据;紫色阴影代表没有东西

                -

                数字电路中,数字电平从低电平(数字“0”)变为高电平(数字“1”)的那一瞬间(时刻)叫作上升沿。数字电平从高电平(数字“1”)变为低电平(数字“0”)的那一瞬间叫作下降沿。

                -

                有固定的时间点,和在每个固定时间点固定要做的事

                -

                第一部分:主设备要给出地址信号

                -

                第二部分:给出读命令(控制信号)

                -

                第三部分:从设备传输数据给主设备

                -

                第四部分:读命令、数据信号撤销

                -

                第五部分:地址信号撤销

                -

                img

                -

                *先给数据能保证命令到达立刻写入正确数据。菱形那段表示电平并非瞬间稳定*

                -

                *如果数据是并行就先给数据,再给读写信号,直接锁存;如果是串行数据,就先给读写信号,再给数据*

                -

                有固定的时间点,和在每个固定时间点固定要做的事

                -

                第一部分:主设备要给出地址信号

                -

                第二部分:主设备给出数据信号

                -

                第三部分:主设备给出写入信号

                -

                第四部分:写入

                -

                第五部分:读命令、数据信号撤销

                -

                第六部分:地址信号撤销

                +

                不相关子查询

                +

                image-20221219160800762

                -

                同步通信通常只适用于总线长度短的。

                -

                因为是并行总线,总线长度长了很难做到等长,到达设备后就不同步了

                -

                因为需要统一时标;总线长,需要迁就最远的设备;读写时间差距大,需要迁就最慢的设备

                -

                异步

                image-20230618181416220

                -
                不互锁

                CPU从主存读信息

                -

                主要用在单机不同设备之间的通信中

                -
                半互锁

                多机系统中,某个CPU需要访问共享存储器时

                -
                全互锁

                主要用于网络通信,如TCP三握手

                -

                半同步通信

                输入数据为例:

                -

                image-20230618181924196

                -

                分离式通信

                -

                在子周期2中,从模块实际上从从模块变成了主模板,因为它发起了占用总线的请求。

                +
                子查询结果单行单列

                可用于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-20230618182050912

                -

                IO

                概述

                发展概况

                image-20230619144243913

                -

                image-20230619144359313

                -

                image-20230619144452679

                -

                组成

                image-20230619144602504

                +

                事务

                基本介绍

                概念

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

                +

                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  "级别字符串";
                +
                -

                ① IO指令

                -

                操作码相当于标志,标志这个指令是IO的。命令码才算是操作码,指出对IO设备做什么。设备码给出IO设备或者设备中某一个寄存器【端口】的编址。

                -

                ② 通道指令

                -

                通道是小型DMA处理器,可以实现IO设备与主机之间进行信息交互。

                -

                通道有自己的控制器,有的通道还有存储器。

                -

                通道能够执行由通道指令组成的通道程序。

                -

                通常情况下,编程人员在应用程序当中,为了调用外部设备,应用程序中需要增加广义IO指令【这意思是封装吧】。广义IO指令要指出参加数据传输的IO设备、数据传输主存的首地址、传输数据的长度、传输方向。操作系统根据广义IO指令给出的参数以及要求的操作,会编写一个由通道指令组成的通道程序,并且会把程序放到内存或者是通道内存的指定位置,之后启动通道进行工作。

                +

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

                +

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

                -

                连接方式

                编址

                image-20230619144651480

                -

                选址和传送

                image-20230619144727325

                -

                联络方式

                image-20230619144851937

                -

                image-20230619145010236

                -

                连接方式

                image-20230619145037444

                -

                控制方式

                image-20230619145313853

                -

                程序查询方式

                image-20230619145133293

                -

                程序中断方式

                image-20230619145154206

                -

                image-20230619145214775

                -

                DMA方式

                image-20230619145252864

                -

                外部设备

                概述

                image-20230619145414624

                -

                IO接口

                概述

                image-20230619151239077

                -

                功能和组成

                image-20230619151310223

                -

                image-20230619151421396

                -

                image-20230619151442848

                -

                接口类型

                image-20230619151602920

                -

                程序查询方式

                image-20230619151713642

                -

                image-20230619152130068

                -

                image-20230619152909693

                -

                程序中断方式

                中断

                概述

                image-20230619153352974

                -

                image-20230619153557165

                -

                接口电路

                image-20230619153715848

                -
                中断请求触发器和中断屏蔽触发器

                image-20230619153949008

                -

                image-20230619154445642

                -

                中断分类

                外部中断一般是由计算机外设发出的中断请求,如:键盘中断、打印机中断、定时器中断等。

                -

                外部中断一般指io高低电平(下降沿等由寄存器配置)来触发并响应io中断函数。

                -

                接口电路

                排队器

                image-20230619155014803

                -

                image-20230619155029933

                -
                硬件实现

                image-20230619155054248

                -
                -

                以下介绍的是链式排队器

                -
                  -
                1. INTR默认为0,取非为1. 经&后整个排队电路为1
                2. -
                3. 当i设备发出请求,INTRi=1,取非为0,经&后变为0,INTPi之后的电路清零,只有i之前的INTP为1
                4. -
                5. 3在一连串的显示为 1 的INTP中,最后一个显示1的设备优先级最高。因为按照我们的分析,是它发出了请求
                6. +

                  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. 释放资源

                    +
                  -

                  使用与非+非而不是直接与门是因为与非门+非更便宜。

                  -

                  我猜这个意思是,链式排队的话,越前面的优先级越高,现在我们讲的是怎么快速****找出****最高的最前面的是哪一个。之所以为什么越前面的优先级最高,可从这个电路中得知。如果一个东西发出请求,那么它后面的INTPi’都会被置零,因而它肯定比它后面的高级。因此越前面的优先级越高。

                  -

                  https://www.likecs.com/show-390301.html

                  -

                  img

                  -

                  这个可以验证我的观点。至于这个轮询方式,应该在第三章的总线那边讲过,应该用的是链式查询。

                  -
                -
                软件实现

                程序查询

                -

                image-20230619155116410

                -

                中断向量形成部件

                硬件向量法

                image-20230619155154822

                -
                软件查询法

                image-20230619161630629

                -

                接口电路组成

                image-20230619161803883

                +
                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类干的。这个静态块仅仅用于简化代码书写。

                -

                应该意思就是,参照上面那个程序电路图,首先CPU先发送一个启动IO设备的命令,然后就去忙了。

                -

                与此同时,IO接口接到命令开始准备,比如说对DBR的整理【因读写而异】。

                -

                IO接口准备完之后会卡在INTR那边,等待CPU的中断查询信号。

                -

                CPU本来一直在不断边干自己的活边发送中断查询信号【在每条指令执行阶段的结束前】,终于逮到这个时候发现IO接口已经准备好了,就回复中断响应信号,CPU进入中断周期,执行中断隐指令。

                -

                IO接口发出中断请求后就排好队选好设备了,收到CPU的中断响应信号,就给CPU发向量地址,CPU根据地址去内存中找到中断服务程序并开始执行,之后就可以开始数据传输了。

                -

                可见这个过程是异步的。

                +

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

                +

                image-20221220151045275

                +

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

                -

                中断响应(中断处理过程)

                image-20230619162001376

                -

                image-20230619162057309

                -

                IO中断处理过程

                image-20230619162142239

                -

                image-20230619201800980

                -

                单重/多重中断服务流程(CPU)

                image-20230619201934346

                -

                image-20230619202014591

                -

                image-20230619202106846

                -

                中断屏蔽技术(CPU)

                image-20230619202207357

                -

                image-20230619202219859

                -

                image-20230619202321781

                -

                image-20230619202355070

                -

                image-20230619202434146

                -

                DMA方式

                特点

                image-20230619202548302

                -

                实现方案

                image-20230619202628150

                -

                沙比

                -

                image-20230619202708423

                -

                image-20230619202739633

                -

                功能和组成

                image-20230619202841809

                -

                image-20230619203043097

                -

                工作过程

                DMA传送过程

                预处理、数据传送、后处理

                -

                image-20230619203248171

                -

                注意还有个传送字数,看来有点安全设定。如果溢出了就需要中断

                -

                image-20230619203423482

                -

                image-20230619203535039

                -

                连接方式

                image-20230619204520342

                -

                image-20230619204537086

                -

                与程序中断比较

                image-20230619204641555

                -]]> - - - 其他的对实验未涉及的思考 - /2023/02/25/cs144$else/ - 其他的对实验未涉及的思考

                网络层实现

                在我们的协议栈实现中,我们负责了运输层的TCP协议、网络层的ARP协议以及数据链路层的ETH协议的编写,剩下的网络层的IP协议则由官方给定。接下来我们就来探究下网络层的实现。

                -

                总体架构

                -

                You’ve done this already.

                -

                In Lab 4, we gave:

                +
                获取数据库连接
                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. an object that represents an Internet datagram and knows how to parse and serialize itself (tcp_helpers/ipv4_datagram.{hh,cc}) 表示了Internet datagram的数据结构,它可以自己序列化。
                2. -
                3. the logic to encapsulate(封装) TCP segments in IP (now found in tcp_helpers/tcp_over_ip.cc).
                4. +
                5. 这东西也得Close

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

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

                  而应该这样:

                  +
                  return resultSet.next();
                -

                The CS144TCPSocket uses these tools to connect your TCPConnection to a TUN device.

                -
                -

                也即,IP协议主要由两个文件实现,一个是IP数据报抽象为的类ipv4_datagram.{hh,cc},另一个是将TCP报文封装为IP报文的类tcp_helpers/tcp_over_ip.cc;除此之外,IP协议还负责与下次协议连接,在实验0-4中它通过CS144TCPSocket与TUN连接,在实验5-6则与TAN连接。

                -

                连接部分暂且先放到下一部分讲,下面来看看IP协议的具体实现。

                -

                具体实现

                ipv4_datagram.hh && ipv4_header.hh

                ipv4_datagram没什么好说的,跟TCPSegment的结构一模一样。ipv4_header也没什么好说的,就纯纯是IP数据报的报头、

                -

                tcp_over_ip

                头文件

                它的头文件很简单,只包含一个类的定义:

                -
                // A converter from TCP segments to serialized IPv4 datagrams
                class TCPOverIPv4Adapter : public FdAdapterBase {
                public:
                std::optional<TCPSegment> unwrap_tcp_in_ip(const InternetDatagram &ip_dgram);

                InternetDatagram wrap_tcp_in_ip(TCPSegment &seg);
                };
                #endif // SPONGE_LIBSPONGE_TCP_OVER_IP_HH
                +

                PreparedStatement

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

                可以看到,相比于TCP和ETH/ARP协议,IP协议的实现可以说是非常简单。它作为一个中间层,只需要把上面给的东西包装下再传到下面,或者把下面给的东西解包下再传给上面,无需其他复杂的算法和数据结构(比如TCP的reliable transmission和ETH/ARP的地址自学习),也无需跟外界打交道。

                -

                除了打包解包外,它只需确保一件事,那就是一台主机只能同时拥有一个TCP连接。这样一来也能简化其实现:填写IP协议头时,它就只需从自己保存的config中取参数就行。

                -
                // 用来拆IP数据包为一个TCP数据包
                //! If this succeeds, it then checks that the received segment is related to the
                //! current connection. When a TCP connection has been established, this means
                // 如果TCP连接已建立,则会检查src和dst端口号的正确性
                //! checking that the source and destination ports in the TCP header are correct.
                //!
                //! If the TCP connection is listening 如果处于listening状态,并且参数为SYN报文
                //! and the TCP segment read from the wire includes a SYN, this function clears the
                // 就需要解除listening的flag,记录下src和dst的地址和端口号
                //! `_listen` flag and records the source and destination addresses and port numbers
                // 目的是为了 filter future reads
                // 这说明我们的sponge实现是单线程的,也就是一台主机只能同时建立一个TCP连接
                // 并且在此时会忽略其他主机发过来的数据包
                //! from the TCP header; it uses this information to filter future reads.

                // returns a std::optional<TCPSegment> that is empty if the segment was invalid or unrelated
                optional<TCPSegment> TCPOverIPv4Adapter::unwrap_tcp_in_ip(const InternetDatagram &ip_dgram) {
                // is the IPv4 datagram for us?
                // Note: it's valid to bind to address "0" (INADDR_ANY) and reply from actual address contacted
                if (not listening() and (ip_dgram.header().dst != config().source.ipv4_numeric())) {
                return {};
                }

                // is the IPv4 datagram from our peer?
                // 过滤非peer发来的其他数据包
                if (not listening() and (ip_dgram.header().src != config().destination.ipv4_numeric())) {
                return {};
                }

                // does the IPv4 datagram claim that its payload is a TCP segment?
                // 我们只需解包TCP数据报
                if (ip_dgram.header().proto != IPv4Header::PROTO_TCP) {
                return {};
                }

                // is the payload a valid TCP segment?
                TCPSegment tcp_seg;
                if (ParseResult::NoError != tcp_seg.parse(ip_dgram.payload(), ip_dgram.header().pseudo_cksum())) {
                return {};
                }

                // is the TCP segment for us?
                if (tcp_seg.header().dport != config().source.port()) {
                return {};
                }

                // should we target this source addr/port (and use its destination addr as our source) in reply?
                if (listening()) {
                // records the source and destination addresses and port numbers
                if (tcp_seg.header().syn and not tcp_seg.header().rst) {
                config_mutable().source = {inet_ntoa({htobe32(ip_dgram.header().dst)}), config().source.port()};
                config_mutable().destination = {inet_ntoa({htobe32(ip_dgram.header().src)}), tcp_seg.header().sport};
                set_listening(false);
                } else {
                return {};
                }
                }

                // is the TCP segment from our peer?
                if (tcp_seg.header().sport != config().destination.port()) {
                return {};
                }

                return tcp_seg;
                }

                //! Takes a TCP segment, sets port numbers as necessary, and wraps it in an IPv4 datagram
                //! \param[in] seg is the TCP segment to convert
                InternetDatagram TCPOverIPv4Adapter::wrap_tcp_in_ip(TCPSegment &seg) {
                // set the port numbers in the TCP segment
                seg.header().sport = config().source.port();
                seg.header().dport = config().destination.port();

                // create an Internet Datagram and set its addresses and length
                InternetDatagram ip_dgram;
                ip_dgram.header().src = config().source.ipv4_numeric();
                ip_dgram.header().dst = config().destination.ipv4_numeric();
                // uint8_t hlen = LENGTH / 4; //!< header length
                // uint8_t doff = LENGTH / 4; //!< data offset
                ip_dgram.header().len = ip_dgram.header().hlen * 4 + seg.header().doff * 4 + seg.payload().size();

                // set payload, calculating TCP checksum using information from IP header
                ip_dgram.payload() = seg.serialize(ip_dgram.header().pseudo_cksum());

                return ip_dgram;
                }
                +

                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);
                }
                }
                }
                -

                Socket实现

                最top的话可以分为CS144TCPSocketFullStackSocket

                -

                继承关系如下图:

                -

                Inheritance graph

                -

                其中,TCPSocket是完完全全的包装类,它的所有协议栈都是在内核态中实现(也就是跟我们之后写的没半毛钱关系),它的存在意义应该是用在lab0来写webget的测试。而CS144TCPSocket就是我们在lab0-4用的了,它的数据链路层由内核实现,网络层和运输层由用户实现。FullStackSocket就是加上了我们在lab5做的用户态数据链路层。

                -

                最主要的部分是TCPSpongeSocket的实现,其他就是一些包装类没什么好说的。

                -

                FileDescriptor

                将socket看作是fd,将网络看作是IO,这一抽象简直是太伟大了,牛逼到爆。

                -

                头文件

                //! A reference-counted handle to a file descriptor
                class FileDescriptor {
                //! \brief A handle on a kernel file descriptor.
                //! \details FileDescriptor objects contain a std::shared_ptr to a FDWrapper.
                class FDWrapper {
                public:
                int _fd; // file descriptor number returned by the kernel
                bool _eof = false; // fd是否eof
                bool _closed = false; // fd是否close
                // fd被读写的次数
                unsigned _read_count = 0;
                unsigned _write_count = 0;

                //! Construct from a file descriptor number returned by the kernel
                explicit FDWrapper(const int fd);
                //! Closes the file descriptor upon destruction
                ~FDWrapper();
                //! Calls [close(2)](\ref man2::close) on FDWrapper::_fd
                void close();
                //! An FDWrapper cannot be copied or moved
                FDWrapper(const FDWrapper &other) = delete;
                FDWrapper &operator=(const FDWrapper &other) = delete;
                FDWrapper(FDWrapper &&other) = delete;
                FDWrapper &operator=(FDWrapper &&other) = delete;
                };

                //! A reference-counted handle to a shared FDWrapper
                std::shared_ptr<FDWrapper> _internal_fd;

                // private constructor used to duplicate the FileDescriptor (increase the reference count) 这个构造函数会增加其参数传进来的那个fd的引用,也许相当于dump
                explicit FileDescriptor(std::shared_ptr<FDWrapper> other_shared_ptr);

                protected:
                void register_read() { ++_internal_fd->_read_count; } //!< increment read count
                void register_write() { ++_internal_fd->_write_count; } //!< increment write count

                public:
                //! Construct from a file descriptor number returned by the kernel
                explicit FileDescriptor(const int fd);

                //! Free the std::shared_ptr; the FDWrapper destructor calls close() when the refcount goes to zero.
                ~FileDescriptor() = default;

                /* 读写 */
                std::string read(const size_t limit = std::numeric_limits<size_t>::max());
                void read(std::string &str, const size_t limit = std::numeric_limits<size_t>::max());
                // possibly blocking until all is written
                size_t write(const char *str, const bool write_all = true) { return write(BufferViewList(str), write_all); }
                size_t write(const std::string &str, const bool write_all = true) { return write(BufferViewList(str), write_all); }
                size_t write(BufferViewList buffer, const bool write_all = true);

                //! Close the underlying file descriptor
                void close() { _internal_fd->close(); }

                //! Copy a FileDescriptor explicitly, increasing the FDWrapper refcount
                FileDescriptor duplicate() const;

                //! Set blocking(true) or non-blocking(false)
                void set_blocking(const bool blocking_state);
                // ...
                +
                用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);
                }
                }
                }
                }
                -

                具体实现

                差不多就是全程调用系统调用没什么好说的,记录下几个有意思的点

                -
                包装系统调用

                可以看下其调用系统调用的方式,看起来很有意思:

                -
                void FileDescriptor::set_blocking(const bool blocking_state) {
                int flags = SystemCall("fcntl", fcntl(fd_num(), F_GETFL));
                if (blocking_state) {
                flags ^= (flags & O_NONBLOCK);
                } else {
                flags |= O_NONBLOCK;
                }

                SystemCall("fcntl", fcntl(fd_num(), F_SETFL, flags));
                }
                +

                使用

                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;
                }
                -

                比如说这里设置文件读写是否阻塞就是通过系统调用实现的。

                -

                在写os实验时,你应该就能很深刻感受到,很多时候调用完一个系统调用后,对它的返回结果进行合法性判断以及错误处理还是有点烦的(举例来说,如if(kalloc() == 0)或者if(mappages() == 0),出错后杀死进程等等等)。在那会我们还可以直接就这么冗余地干了,但是这里不行,一是我们要用面向对象的思想,二是我们的重点事实上并不是操作系统而是网络,因而最好还是这么封装下以减少冗余代码。

                -

                而它除了会调用系统调用外,还使用了一个包装性的方法SystemCall来保障调用的安全性和合理性。看看SystemCall的具体实现方式,确实就是包了层安全检查。

                -
                int SystemCall(const char *attempt, const int return_value, const int errno_mask) {
                if (return_value >= 0 || errno == errno_mask) {
                return return_value;
                }

                throw unix_error(attempt);
                }

                // os内核看不懂c++,所以要注意转换为c-style的字符串
                int SystemCall(const string &attempt, const int return_value, const int errno_mask) {
                return SystemCall(attempt.c_str(), return_value, errno_mask);
                }
                +

                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);
                }
                }
                }
                -

                Socket

                没什么好说的,只是操作系统socket接口的包装类。

                -

                头文件

                // Base class for network sockets (TCP, UDP, etc.)
                // Socket is generally used via a subclass. See TCPSocket and UDPSocket for usage examples.
                class Socket : public FileDescriptor {
                private:
                //! Get the local or peer address the socket is connected to
                Address get_address(const std::string &name_of_function,
                const std::function<int(int, sockaddr *, socklen_t *)> &function) const;

                protected:
                Socket(const int domain, const int type);
                Socket(FileDescriptor &&fd, const int domain, const int type);

                template <typename option_type>
                void setsockopt(const int level, const int option, const option_type &option_value);
                public:
                // Bind a socket to a local address, usually for listen/accept
                void bind(const Address &address);
                // Connect a socket to a peer address
                void connect(const Address &address);
                // Shut down a socket
                void shutdown(const int how);
                //! Get local address of socket
                Address local_address() const;
                //! Get peer address of socket
                Address peer_address() const;
                //! Allow local address to be reused sooner
                void set_reuseaddr();
                };

                //! A wrapper around [UDP sockets](\ref man7::udp)
                class UDPSocket : public Socket {
                protected:
                //! \brief Construct from FileDescriptor (used by TCPOverUDPSocketAdapter)
                //! \param[in] fd is the FileDescriptor from which to construct
                explicit UDPSocket(FileDescriptor &&fd) : Socket(std::move(fd), AF_INET, SOCK_DGRAM) {}

                public:
                //! Default: construct an unbound, unconnected UDP socket
                UDPSocket() : Socket(AF_INET, SOCK_DGRAM) {}

                // carries received data and information about the sender
                struct received_datagram {
                Address source_address; //!< Address from which this datagram was received
                std::string payload; //!< UDP datagram payload
                };
                //! Receive a datagram and the Address of its sender
                received_datagram recv(const size_t mtu = 65536);
                //! Receive a datagram and the Address of its sender (caller can allocate storage)
                void recv(received_datagram &datagram, const size_t mtu = 65536);

                //! Send a datagram to specified Address
                void sendto(const Address &destination, const BufferViewList &payload);
                //! Send datagram to the socket's connected address (must call connect() first)
                void send(const BufferViewList &payload);
                };

                //! A wrapper around [TCP sockets](\ref man7::tcp)
                class TCPSocket : public Socket {
                private:
                // Construct from FileDescriptor (used by accept())
                // fd is the FileDescriptor from which to construct
                explicit TCPSocket(FileDescriptor &&fd) : Socket(std::move(fd), AF_INET, SOCK_STREAM) {}
                public:
                //! Default: construct an unbound, unconnected TCP socket
                TCPSocket() : Socket(AF_INET, SOCK_STREAM) {}
                //! Mark a socket as listening for incoming connections
                void listen(const int backlog = 16);
                //! Accept a new incoming connection
                TCPSocket accept();
                };

                //! A wrapper around [Unix-domain stream sockets](\ref man7::unix)
                class LocalStreamSocket : public Socket {
                public:
                // ...构造器
                };
                #endif // SPONGE_LIBSPONGE_SOCKET_HH
                +

                数据库连接池

                其实就是上面的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);
                }
                -

                具体实现

                构造器的参数

                参考文章

                -

                也是系统调用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();
                }
                // ...
                +

                就可以传入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);
                }
                }
                }
                -

                * 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.
                +

                Druid

                基本使用

                设置配置文件

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

                +
                driverClassName=com.mysql.jdbc.Driver
                url=jdbc:mysql://localhost:3306/helloworld
                username=root
                password=root
                initialSize=5
                maxActive=10
                maxWait=3000
                -

                事件监听

                完成事件监听的核心部分是方法_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(); });
                }
                +
                使用
                //导入配置文件
                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);
                }
                }
                }
                -
                _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;
                }
                }
                }
                +

                定义工具类

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

                +
                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);
                }
                }
                }
                }
                -
                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);
                +

                使用同上的JDBCUtils

                +

                Spring JDBC

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

                +

                使用方法

                带参(PreparedStatement)

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

                事件类型events可以为下列值:

                -
                POLLIN:有数据可读
                POLLRDNORM:有普通数据可读,等效于POLLIN
                POLLRDBAND:有优先数据可读
                POLLPRI:有紧迫数据可读
                POLLOUT:写数据不会导致阻塞
                POLLWRNORM:写普通数据不会导致阻塞
                POLLWRBAND:写优先数据不会导致阻塞
                POLLMSG:SIGPOLL消息可用
                POLLER:指定的文件描述符发生错误
                POLLHUP:指定的文件描述符挂起事件
                POLLNVAL:无效的请求,打不开指定的文件描述符
                -
                -

                我们在前面的eventloop的rule初始化中:

                -
                _eventloop.add_rule(_datagram_adapter,
                Direction::In,
                [&] { ... });
                +

                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
                */
                -

                这个的意思是针对_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.
                };
                +

                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}
                -

                可见,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);
                }
                +
                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]
                */
                -
                _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;
                }
                }
                +
                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}]
                */
                -
                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);
                }
                +
                常用的

                使用包装好的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}]
                */
                -

                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>;
                +

                注意:

                +
                  +
                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);
                -

                它很完美地以策略模式的形式,凝结出了我们本次实验所需的各种协议栈的共同代码,放进了TCPSpongeSocket,而将涉及到协议栈差异的部分用adapter完成。

                -

                TCPSpongeSocket中,adapter主要完成了如下操作:

                +

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

                Web概述

                  +
                • JavaWeb:

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

                    -
                  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. +
                  9. C/S: Client/Server 客户端/服务器端
                      +
                    • 在用户本地有一个客户端程序,在远程有一个服务器端程序
                    • +
                    • 如:QQ,迅雷…
                    • +
                    • 优点:
                        +
                      1. 用户体验好
                      -

                      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的结合体,订阅事件+循环等待。由于跟前面类似,在此就不放代码了。

                      -

                      其他

                      其他都太复杂了,感觉我水平一般还不大能理解,也懒得看了【草】总之先咕咕咕

                      -]]> - - - JavaWeb - /2022/12/21/JavaWeb/ - 第一部分 Java基础

                      JUnit单元测试

                      JUnit是白盒测试。

                      -

                      简要使用步骤

                      定义测试类

                      包含各种测试用例。

                      -

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

                      -

                      定义测试方法

                      测试方法可以独立运行。

                      -

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

                      -

                      给方法加@Test标签

                      加入JUnit依赖包

                      具体细节

                      断言

                      Assert.assertEquals(3,result);
                      +
                    • +
                    • 缺点:
                        +
                      1. 开发、安装,部署,维护 麻烦
                      2. +
                      +
                    • +
                    +
                  10. +
                  11. B/S: Browser/Server 浏览器/服务器端
                      +
                    • 只需要一个浏览器,用户通过不同的网址(URL),客户访问不同的服务器端程序
                    • +
                    • 优点:
                        +
                      1. 开发、安装,部署,维护 简单
                      2. +
                      +
                    • +
                    • 缺点:
                        +
                      1. 如果应用过大,用户的体验可能会受到影响
                      2. +
                      3. 对硬件要求过高
                      4. +
                      +
                    • +
                    +
                  12. +
                  +
                • +
                • 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>
                -

                @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*/
                +

                注意点

                  +
                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>
                -

                同一个字节码文件(*.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;
                }
                }
                +

                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();//注意是数组
                -
                Student stu = new Student("张三",321,1000,57.7);
                Class stuC = stu.getClass();
                +
                创建DOM对象
                createAttribute(name);
                createComment();
                createElement();
                createTextNode();
                -

                字段

                获取字段

                常用方法:

                -
                //获取所有公有字段
                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*/
                +
                Element
                removeAttribute();
                setAttribute(属性名,属性值);
                -
                使用字段
                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} */
                +
                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>
                -

                构造方法

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

                -
                //获取构造方法
                //获取无参私有构造方法
                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}*/
                +

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

                +
                //使用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>";
                }
                -

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

                -
                System.out.println(stuC.newInstance());
                +
                事件
                练习:全选/全不选/反选+行变色

                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>
                -

                方法

                //获取方法
                //可以获取静态方法
                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...*/
                +
                练习:表单校验
                <!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>
                -

                使用反射的案例

                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());
                }
                }
                +

                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>
                -

                注解

                image-20221205211932790

                -

                image-20221205212028989

                -

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

                -

                生成doc文档

                javadoc XXX.java
                +

                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>
                -

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

                -

                JDK预定义注解

                image-20221205212443918

                -

                自定义注解

                注解类的本质

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

                响应式布局

                实现依赖于栅格系统。

                +

                栅格系统

                将一行平均分成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>
                -

                本质上

                -
                public @interface Override{}
                +

                样式

                看文档。

                +

                练习:用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>
                -

                等价于

                -
                public interface Override extends java.lang.annotation.Annotation{}
                +
                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;
                }
                -

                注解的属性

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

                -

                image-20221205213135522

                -
                public @interface MyAnno {
                String name() default "haha";
                }
                +

                注意点

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

                +
                  +
                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>
                  -
                  @MyAnno(name = "haha")
                  public class Student{}
                  +

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

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

                  元注解

                  描述注解的注解

                  -

                  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
                  +

                  也即多加一个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>
                  -
                  @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());
                  }
                  }
                  +

                  其实是非常直观的,相信以后你看到这段应该也能理解(。提示一点,栅格系统其实好像是相对于父类的。也就是说,不是“把整个页面分成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>
                -

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

                -

                使用案例:测试框架

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

                细说

                语法

                文档声明

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

                +
                属性

                id属性值唯一

                +
                文本

                image-20221225222029698

                +

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

                +

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

                +
                <code>

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

                </code>
                -

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

                -

                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次异常
                */
                +

                约束

                只能写约束文件内的标签

                +
                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>
                -

                image-20221205220053258

                -

                第二部分 数据库

                Mysql

                登录方式

                mysql -h[IP地址] -u[用户名] -p
                +
                引入方式
                //外部引入
                <!DOCTYPE 根标签名 SYSTEM "dtd文件的位置">
                <!DOCTYPE 根标签名 PUBLIC "dtd文件名字" "dtd文件的位置URL">
                -

                文件结构

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

                -

                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;
                +

                或者可以直接在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>
                -
                drop database 数据库名称;
                drop database if exists 数据库名称;
                +
                使用
                <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>
                -
                alter database 数据库名称 charactor set 修改后新值;
                +
                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>
                -
                show databases;# 查询所有数据库名称
                show create database 数据库名称;# 显示指定数据库创建时的指令内容
                +
                引入方式
                <?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>
                -
                使用
                select database();# 查询正在使用的数据库名称
                use 数据库名称;
                +

                它这意识就是,每个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());
                }
                }
                -

                操纵表

                create table students(
                name varchar(20),
                age int,
                score double(3,1),
                birthday date,
                insert_time timestamp
                );# 创建表
                create table students2 like students;# 复制表
                +
                细说
                  +
                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. mysql的数据类型表

                    -

                    image-20221205222127740

                    -

                    其中:

                    -

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

                    -

                    ② 关于三个时间类型

                    -

                    image-20221205222307284

                    -

                    所以timestamp常用作插入时间。

                    -

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

                    -

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

                    -

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

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

                    +
                      +
                    • war包会自动解压缩
                    • +
                    +
                  3. +
                  4. 配置conf/server.xml文件
                    <Host>标签体中配置
                    <Context docBase="C:\aWorkSpace\Projects\Java\JavaWeb" path="/web" />

                  -
                  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 表名;# 查询某个表的结构
                  - +
                  然后之后访问时输入`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>
                -

                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);
                +

                即可,可用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){
                }
                }
                -

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

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

                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!!!");
                }
                }

                -

                image-20221205224600869

                -

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

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

                这两个方法的区别就是,当使用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);
                }
                }
                -

                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;
                +

                于是最后就融合入httpservlet了。

                +

                url-pattern配置

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

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

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

                    条件查询

                    运算符
                      -
                    1. 基本运算符

                      -

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

                      +url访问填写http://localhost/webdemo4_war/*.do
                    2. +
                    +
                  6. +
                  +

                  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. -
                      11. 逻辑运算符

                        -

                        AND、OR

                        +
                    • -
                    • BETWEEN AND

                      +
                  4. -
                  5. IN后跟集合

                    -

                    image-20221205230824086

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

                  7. -
                  8. IS、IS NOT

                    -

                    image-20221205230902110

                    +
                  9. 请求体(正文):

                    +
                      +
                    • 封装POST请求消息的请求参数的
                    • +
                  10. -
                  11. LIKE 模糊查询

                    -

                    类似正则使用占位符匹配

                    -

                    image-20221205231037021

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

                  各种函数一样的东西

                  排序函数
                  order by 排序字段1 排序方式1,排序字段2 排序方式2;
                  +

                  字符串格式:

                  +
                  //请求行
                  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. -
                  3. ASC、DESC

                    -
                  4. -
                  5. 多关键字排序

                    -

                    image-20221205231606255

                    -

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

                    +
                  6. 响应行

                    +
                      +
                    1. 组成:协议/版本 响应状态码 状态码描述 HTTP/1.1 200 OK

                    2. -
                    -
                    聚合函数

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

                    -

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

                    -
                    select count(ifnull(math,0)) from students;
                    - +
                  7. 响应状态码:服务器告诉客户端浏览器本次请求和响应的一个状态, 状态码都是3位数字. 分类:

                      -
                    1. count 计算个数

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

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

                      +
                    2. 1xx:服务器接收客户端消息,但没有接收完成,等待一段时间后,发送1xx多状态码,询问是否还要继续发

                    3. -
                    4. max、min

                      +
                    5. 2xx:成功。代表:200

                    6. -
                    7. sum 求和

                      +
                    8. 3xx:重定向。代表:302(重定向),304(访问缓存)

                      +

                      image-20230103151112791

                      +

                      需要自动重定向到另一个C去

                      +

                      image-20230103151237984

                      +

                      发现资源未变化且本地有缓存

                    9. -
                    10. avg 平均值

                      +
                    11. 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);
                        }
                      -
                      分组查询

                      分组之后查询的字段只能是两种:① 分组字段 ② 聚合函数。因为分组了之后再查具有个人特色的东西就没意义了。【高版本的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. + +
                      5. 5xx:服务器端错误。

                        +

                        代表:500(服务器内部出现Exception)

                        +
                        int i = 3/0;
                      -
                      分页查询

                      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. 注册驱动

                        +
                      +
                    12. +
                    13. 响应头:

                      +
                        +
                      1. 格式: [头名称 : 值]

                        +
                      2. +
                      3. 常见的响应头:

                        +
                          +
                        1. Content-Type:服务器告诉客户端本次响应体 数据格式以及编码格式

                          +

                          浏览器依照编码格式来对该页面进行解码。

                        2. -
                        3. 获取数据库连接对象 Connection

                          +
                        4. Content-disposition:服务器告诉客户端以什么格式打开响应体数据

                          +
                            +
                          • 值:
                              +
                            • in-line:默认值,在当前页面内打开
                            • +
                            • attachment;filename=xxx:以附件形式打开响应体。也即点击超链接后开始文件下载
                            • +
                          • -
                          • 定义sql语句

                            +
                        5. -
                        6. 获取执行sql语句的对象 Statement

                          +
                      4. -
                      5. 执行sql,接收返回的结果

                        +
                    14. -
                    15. 处理结果

                      +
                    16. 响应空行

                    17. -
                    18. 释放资源

                      +
                    19. 响应体:传输的数据

                    -
                    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();
                    }
                    }
                    +

                    字符串格式:

                    +
                    //响应行
                    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>
                    -

                    优化版【增加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!");
                    }
                    }
                    +

                    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();
                      -

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

                      -

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

                      -

                      image-20221220151045275

                      -

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

                      +

                      URL:统一资源定位符 : http://localhost/day14/demo1 中华人民共和国
                      URI:统一资源标识符 : /day14/demo1 共和国

                      +

                      URI的代表范围更大

                      -
                      获取数据库连接
                      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;
                      +
                    10. +
                    11. 获取协议及版本 HTTP/1.1

                      +
                      String getProtocol()
                    12. +
                    13. 获取访问的客户机的IP地址

                      +
                      String getRemoteAddr()
                    14. +
                    +
                    获取请求头
                      +
                    1. 通过请求头的名称获取请求头的值

                      +
                      String getHeader(String name)
                    2. +
                    3. 获取所有的请求头名称

                      +
                      Enumeration<String> getHeaderNames()
                      -
                      管理事务
                      开启事务
                      void setAutoCommit(boolean autoCommit) throws SQLException;
                      +

                      返回的是一个迭代器

                      +
                    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
                    -

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

                    -
                    提交事务
                    void commit() throws SQLException;
                    +

                    这些请求头名称正是上面的键值对里的键。

                    +
                    获取请求体

                    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);
                    }
                    }
                    }
                    -
                    回滚事务
                    void rollback() throws SQLException;
                    +

                    请求体中键值对会在一行里,用&分割

                    +
                    +

                    获取时中文乱码

                    +
                      +
                    • get方式:tomcat 8 已经将get方式乱码问题解决了

                      +
                    • +
                    • post方式:会乱码

                      +
                                         * 解决:在获取参数前,设置流的编码:
                       
                      -

                      Statement

                      -

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

                      +
                      request.setCharacterEncoding("utf-8");
                      +
                      +
                    • +
                    -
                    执行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;
                    }
                    - -

                    注意:

                    +
                    获取请求参数通用的方法(通用指对get和post通用)

                    这里的请求参数应该是指上面Post的请求体、Get的请求行里的参数,请求头里的参数是获取不到的。

                      -
                    1. 这东西也得Close

                      +
                    2. 根据参数名称获取参数值

                      +
                      String getParameter(String name)
                      + +

                      如 username=zs&password=123,getParameter(“username”)会得到zs。

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

                      -
                      if (resultSet != null)  return true;
                      else return false;
                      +
                    5. 根据参数名称获取参数值的数组

                      +
                      String[] getParameterValues(String name)
                      -

                      而应该这样:

                      -
                      return resultSet.next();
                    6. +

                      如 hobby=xx&hobby=game,会得到{xx,game}

                      + +
                    7. 获取所有请求的参数名称

                      +
                      Enumeration<String> getParameterNames()
                    8. +
                    9. 取所有参数的map集合

                      +
                      Map<String,String[]> getParameterMap()
                    -

                    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);
                    }
                    }
                    }
                    +
                    请求转发

                    在服务器内部资源跳转

                    +

                    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());
                    }
                    }
                    -

                    Druid

                    基本使用

                    设置配置文件

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

                    -
                    driverClassName=com.mysql.jdbc.Driver
                    url=jdbc:mysql://localhost:3306/helloworld
                    username=root
                    password=root
                    initialSize=5
                    maxActive=10
                    maxWait=3000
                    +

                    特点:

                    +
                      +
                    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()
                    -
                    使用
                    //导入配置文件
                    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);
                    }
                    }
                    }
                    +
                    练习:结合数据库与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
                    -

                    定义工具类

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

                    -
                    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);
                    }
                    }
                    }
                    }
                    +
                    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>
                    -

                    使用同上的JDBCUtils

                    -

                    Spring JDBC

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

                    -

                    使用方法

                    带参(PreparedStatement)

                    jdbcTemplate.update("update usr set money = ? where uname = ?",10,"Mary");
                    +
                    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("登录失败,用户名或密码错误");
                    }
                    }
                    -

                    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
                    */
                    +
                    @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")+",欢迎您");
                    }
                    }
                    -

                    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}
                    +
                    @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);
                    }

                    }
                    }
                    -
                    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]
                    */
                    +
                    +

                    关于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);
                    }
                    }
                    -
                    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}]
                    */
                    +

                    Response

                    功能

                    设置响应消息。

                    +
                    设置响应行

                    设置状态码

                    +
                    setStatus(int sc);
                    -
                    常用的

                    使用包装好的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}]
                    */
                    +
                    设置响应头
                    setHeader(String name, String value) 
                    -

                    注意:

                    +
                    设置响应体

                    以流的方式传输数据。

                    +

                    使用步骤:

                      -
                    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. +
                          3. 字节输出流

                            +
                            ServletOutputStream getOutputStream()
                          4. +
                          5. 字符输出流

                            +
                            PrintWriter getWriter()
                        • -
                        -
                      2. -
                      3. B/S: Browser/Server 浏览器/服务器端
                          -
                        • 只需要一个浏览器,用户通过不同的网址(URL),客户访问不同的服务器端程序
                        • -
                        • 优点:
                            -
                          1. 开发、安装,部署,维护 简单
                          2. -
                          +
                        • 使用输出流,将数据输出到客户端浏览器

                        • -
                        • 缺点:
                            -
                          1. 如果应用过大,用户的体验可能会受到影响
                          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对象共享数据
                        - -
                      5. B/S架构详解

                        -
                          -
                        • 资源分类:

                          +

                          路径写法:

                            -
                          1. 静态资源:
                              -
                            • 使用静态网页开发技术发布的资源。
                            • -
                            • 特点:
                                -
                              • 所有用户访问,得到的结果是一样的。
                              • -
                              • 如:文本,图片,音频、视频, HTML,CSS,JavaScript
                              • -
                              • 如果用户请求的是静态资源,那么服务器会直接将静态资源发送给浏览器。浏览器中内置了静态资源的解析引擎,可以展示静态资源
                              • -
                              -
                            • +
                            • 相对路径:通过相对路径不可以确定唯一资源

                              +
                                +
                              • 规则:找到当前资源和目标资源之间的相对位置关系
                            • -
                            • 动态资源:
                                -
                              • 使用动态网页及时发布的资源。
                              • -
                              • 特点:
                                  -
                                • 所有用户访问,得到的结果可能不一样。
                                • -
                                • 如:jsp/servlet,php,asp…
                                • -
                                • 如果用户请求的是动态资源,那么服务器会执行动态资源,转换为静态资源,再发送给浏览器
                                • -
                                +
                              • 绝对路径:通过绝对路径可以确定唯一资源

                                +
                                  +
                                • 如:http://localhost/day15/responseDemo2 /day15/responseDemo2

                                  +
                                  <form action="/webdemo4_war/check" method="post">
                                • +
                                • 以/开头的路径

                                • +
                                • 规则:判断定义的路径是给谁用的?判断请求将来从哪儿发出

                                  +
                                    +
                                  • 客户端浏览器使用:需要加虚拟目录(项目的访问路径)

                                    +

                                    比如说在页面中弄了个a标签,将来是要给客户端点的,那么这个a标签的href就要用绝对路径。

                                    +

                                    再比如说重定向:

                                    +
                                    //要填的是完整资源路径。
                                    resp.setHeader("location","/practice_war/demo2");
                                    + +

                                    这个路径将来是给客户端将来要使用的路径,是客户端路径,所以要加虚拟目录。

                                    +
                                  • -
                          -
                        • -
                        • 我们要学习动态资源,必须先学习静态资源!

                          -
                        • -
                        • 静态资源:

                          +
                        • 服务器使用:不需要加虚拟目录

                          +

                          比如说之前的请求转发

                            -
                          • 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

                    -

                    练习

                    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>
                    +
                    服务器输出字符数据到浏览器
                    @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);
                    }
                    }
                    -

                    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>
                    +
                    服务器输出字节数据到浏览器
                    @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);
                    }
                    }
                    -
                    *{
                    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");
                    }
                    +
                    验证码
                    @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);
                    }
                    }
                    -

                    JavaScript

                    对象

                    function
                    1. 创建:
                    -   1. var fun = new Function(形式参数列表,方法体);  //忘掉吧
                    -   2. function 方法名称(形式参数列表){
                    -          方法体
                    -      }
                    +
                    <!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>
                    - 3. var 方法名 = function(形式参数列表){ - 方法体 - } -2. 方法: +

                    ServletContext对象

                    概念

                    代表整个web应用,可以和servlet容器(服务器)通信

                    +

                    获取

                    通过request对象获取
                    ServletContext getServletContext()
                    -3. 属性: - length:代表形参的个数 -4. 特点: - 1. 方法定义是,形参的类型不用写,返回值类型也不写。 - 2. 方法是一个对象,如果定义名称相同的方法,会覆盖 - 3. 在JS中,方法的调用只与方法的名称有关,和参数列表无关 - 4. 在方法声明中有一个隐藏的内置对象(数组),**arguments**,封装所有的实际参数 -5. 调用: - 方法名称(实际参数列表); +
                    通过HttpServlet获取
                    this.getContext();
                    + +

                    功能

                    获取MIME类型

                    MIME是在互联网通信过程中定义的一种文件数据类型

                    +
                            * 格式: 大类型/小类型   text/html        image/jpeg
                     
                    -
                    /**
                    * 求任意个数的和
                    */
                    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);
                    +
                    /*
                    @param: 文件的后缀扩展名,如.txt
                    */
                    String getMimeType(String file);
                    -
                    Global
                      -
                    1. 特点:全局对象,这个Global中封装的方法不需要对象就可以直接调用。 方法名();

                      +

                      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. +
                      • -
                      • 方法:
                        encodeURI():url编码
                        decodeURI():url解码

                        -

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

                        -

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

                        +
                      +

                      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);
                      }
                      + +

                      会话

                      会话:一次会话中包含多次请求和响应。

                        -
                      • 逐一判断每一个字符是否是数字,直到不是数字为止,将前边数字部分转为number
                        isNaN():判断一个值是否是NaN
                          -
                        • NaN六亲不认,连自己都不认。NaN参与的==比较全部问false
                        • +
                        • 一次会话:浏览器第一次给服务器资源发送请求,会话建立,直到有一方断开为止

                          +
                        • +
                        • 功能:请求之间本来是相互独立的。将多次请求组织在一次会话中,就可以让请求之间进行数据的共享。

                          +
                        • +
                        • 方式:

                          +
                            +
                          • 客户端会话技术 Cookie

                            +

                            把数据存进客户端

                            +
                          • +
                          • 服务器端会话技术 Session

                            +

                            把数据存进服务器端

                            +
                          • +
                          +
                        • +
                        +

                        概念

                        客户端会话技术,将数据保存到客户端

                        +

                        快速入门

                          +
                        • 使用步骤:

                          +
                            +
                          1. 创建Cookie对象,绑定数据【为了从服务器端发送cookie给客户端】
                              +
                            • new Cookie(String name, String value)
                            • +
                            • 可以看到,Cookie其实就是一种name-value这样的键值对对象
                          2. +
                          3. 发送Cookie对象【因为要发送给客户端,所以应该在response里存】
                              +
                            • response.addCookie(Cookie cookie)
                            -

                            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. + +
                          5. 获取Cookie,拿到数据【因为是来自客户端,所以要从request里要】
                              +
                            • Cookie[] request.getCookies()
                            • +
                            +
                          -

                          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>
                          +
                        • +
                        • 代码

                          +
                          @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);
                          }
                          }
                          -
                          练习:表单校验
                          <!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>
                          +
                          @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)
                        -

                        BOM

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

                        -

                        image-20221224152804747

                        -

                        Window对象包含DOM对象。

                        -

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

                        -
                        Window

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

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

                          -

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

                          +

                          参数:

                          +
                            +
                          1. 正数:将Cookie数据写到硬盘的文件中。持久化存储。并指定cookie存活时间,时间到后,cookie文件自动失效
                          2. +
                          3. 负数:默认值
                          4. +
                          5. 零:删除cookie信息
                          6. +
                          +
                          中文问题

                          在tomcat 8 之前 cookie中不能直接存储中文数据,需要将中文数据转码——一般采用URL编码(%E3)

                          +

                          在tomcat 8 之后,cookie支持中文数据。

                          +
                          获取范围
                            +
                          1. 假设在一个tomcat服务器中,部署了多个web项目,那么在这些web项目中cookie能不能共享?

                            +
                              +
                            • 默认情况下cookie不能共享

                            • -
                            • 与开关有关的方法

                              -

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

                              +
                            • 共享方法:

                              +

                              setPath(String path):设置cookie的获取范围。默认情况下,设置当前的虚拟目录

                              +

                              如果要共享,则可以将path设置为”/“

                            • -
                            • 定时器

                              -
                              //只执行一次
                              setTimeout();
                              clearTimeout();
                              //间隔执行多次
                              setInterval();
                              clearInterval();
                              +
                            +
                          2. +
                          3. 不同的tomcat服务器间cookie共享问题?

                            +

                            比如说:

                            +image-20230221225514567 -
                            //一次性定时器
                            //setTimeout("fun();",2000);
                            var id = setTimeout(fun,2000);
                            //取消
                            clearTimeout(id);
                            function fun(){
                            alert('boom~~');
                            }

                            //循环定时器
                            var id = setInterval(fun,2000);
                            clearInterval(id);
                          4. +
                              +
                            • setDomain(String path):如果设置一级域名相同,那么多个服务器之间cookie可以共享

                              +

                              setDomain(".baidu.com"),那么tieba.baidu.com和news.baidu.com中cookie可以共享)

                              +
                            • +
                            +
                          -
                          属性
                            -
                          1. 获取其他BOM对象

                            -

                            history、location、navigator、screen

                            +

                            作用和特点

                            特点:

                            +
                              +
                            1. cookie存储数据在客户端浏览器

                              +

                              因而它相对不安全

                            2. -
                            3. 获取DOM对象

                              -

                              document

                              +
                            4. 浏览器对于单个cookie 的大小有限制(4kb) 以及 对同一个域名下的总cookie数量也有限制(20个)

                            -
                            练习:轮播图
                            <!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方法

                              +

                              作用:

                              +
                                +
                              1. cookie一般用于存出少量的不太敏感的数据

                              2. -
                              3. 设置或返回完整的url

                                -

                                location.href属性

                                +
                              4. 在不登录的情况下,完成服务器对客户端的身份识别

                                +

                                比如说,以不登录情况下对某个网页进行属性设置,你下次打开的时候属性设置依然在,这是因为你的属性设置的cookie在设置后被存入到你的电脑中,下次访问该网页发出请求,服务器端就能根据请求中cookie里的属性设置信息来做出响应了。

                              -
                              练习:自动返回首页
                              <!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>
                              +

                              案例:记住上一次访问时间

                              需求:
                              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);
                              }
                              }
                              -

                              响应式布局

                              实现依赖于栅格系统。

                              -

                              栅格系统

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

                              -

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

                              -
                              基本原理
                                -
                              1. 定义容器。相当于之前的table
                              2. -
                              -
                                -
                              • 容器分类

                                +

                                Session

                                概念

                                服务器端会话技术,在一次会话的多次请求间共享数据,将数据保存在服务器端的对象中。HttpSession

                                +

                                应用场合

                                比如说购物网站的购物车这种,就会存在session。想想也是(

                                  -
                                1. container:两边留白
                                2. -
                                3. container-fluid:每一种设备都是100%宽度
                                4. +
                                5. session用于存储一次会话的多次请求的数据,存在服务器端

                                  +

                                  比如说,当我们做重定向的时候,就可以选择用session共享数据(会话域)而非使用ServletContext(此范围过大)

                                  +
                                6. +
                                7. session可以存储任意类型,任意大小的数据

                                  +
                                8. +
                                9. session与Cookie的区别:

                                  +
                                    +
                                  1. session存储数据在服务器端,Cookie在客户端
                                  2. +
                                  3. session没有数据大小限制,Cookie有
                                  4. +
                                  5. session数据安全,Cookie相对于不安全
                                10. -
                              -
                                -
                              1. 定义行。相当于之前的tr 样式:row
                              2. -
                              3. 定义元素。指定该元素在不同的设备上,所占的格子数目。样式:col-设备代号-格子数目
                              -
                                -
                              • 设备代号:

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

                                  快速入门

                                    +
                                  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是同一个吗?

                                    +
                                  +
                                    +
                                  • 不是同一个,但是要确保数据不丢失。tomcat自动(IDEA不会活化)完成以下工作
                                      +
                                    • session的钝化:(序列化)
                                          * 在服务器正常关闭之前,将session对象序列化到硬盘上
                                      +
                                    -
                                    使用方法
                                    <!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>
                                  + ![image-20230223104447097](./JavaWeb/image-20230223104447097.png) + +* session的活化:(反序列化 + * 在服务器启动后,将session文件转化为内存中的session对象即可。 -

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

                                  +我想,cookie应该在这点上不会像session这么做,因为cookie本质上是保存在客户端的数据,按理来说服务器端把cookie发出去之后就可以销毁了,在服务器序列化一点意义都没有。 +
                                  +
                                    +
                                  1. 销毁时间

                                    +
                                      +
                                    1. 服务器关闭

                                    2. -
                                    3. 关于hr标签

                                      -

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

                                      +
                                    4. session对象调用invalidate() 。

                                    5. +
                                    6. session默认失效时间 30分钟
                                      选择性配置修改

                                      +

                                      可以在每个项目的子配置文件(如下图)或者总的项目的父配置文件apache-tomcat-8.5.83\conf\web.xml中配置

                                      +

                                      image-20230223105053170

                                      +
                                      <session-config>
                                      <session-timeout>30</session-timeout>
                                      </session-config>
                                    -

                                    XML

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

                                    -

                                    image-20221225220356932

                                    -

                                    快速入门

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

                                  案例

                                  +

                                  需求:

                                  +
                                    +
                                  1. 访问带有验证码的登录页面login.jsp
                                  2. +
                                  3. 用户输入用户名,密码以及验证码。
                                      +
                                    • 如果用户名和密码输入有误,跳转登录页面,提示:用户名或密码错误
                                    • +
                                    • 如果验证码输入有误,跳转登录页面,提示:验证码错误
                                    • +
                                    • 如果全部输入正确,则跳转到主页success.jsp,显示:用户名,欢迎您
                                    -
                                    <?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>
                                    +
                                  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>
                                  -

                                  细说

                                  语法

                                  文档声明

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

                                  -
                                  属性

                                  id属性值唯一

                                  -
                                  文本

                                  image-20221225222029698

                                  -

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

                                  -

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

                                  -
                                  <code>

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

                                  </code>
                                  +
                                  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);
                                  }
                                  }
                                  -

                                  约束

                                  只能写约束文件内的标签

                                  -
                                  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>
                                  +
                                  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);
                                  }
                                  }
                                  -
                                  引入方式
                                  //外部引入
                                  <!DOCTYPE 根标签名 SYSTEM "dtd文件的位置">
                                  <!DOCTYPE 根标签名 PUBLIC "dtd文件名字" "dtd文件的位置URL">
                                  +

                                  老师的写法是将错误信息直接写在原登录界面,和我的略有不同:

                                  +
                                  // in loginServlet
                                  if (!session.getAttribute("verifycode").equals(verifycode)){
                                  request.setAttribute("message","checkcode_fail");
                                  request.getRequestDispatcher("/login.jsp").forward(request,response);
                                  return;
                                  }
                                  -

                                  或者可以直接在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>
                                  +
                                  // 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("用户名或密码错误!");
                                  }
                                  }
                                  %>
                                  -
                                  使用
                                  <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>
                                  +

                                  以及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")+",欢迎您");
                                  }
                                  }
                                  -
                                  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>
                                  +

                                  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>
                                  -
                                  引入方式
                                  <?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>
                                  +

                                  最终效果:

                                  +image-20230222230929893 -

                                  它这意识就是,每个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());
                                  }
                                  }
                                  +

                                  原理

                                  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" %>
                                  -
                                  细说
                                    -
                                  1. Jsoup:工具类,可以解析html或xml文档,返回Document

                                    +

                                    用来配置jsp的资源页面信息

                                      -
                                    • parse:解析html或xml文档,返回Document
                                        -
                                      • parse(File in, String charsetName):解析xml或html文件的。
                                      • -
                                      • parse(String html):解析xml或html字符串
                                      • -
                                      • parse(URL url, int timeoutMillis):通过网络路径获取指定的html或xml的文档对象
                                      • -
                                      +
                                    • 分类:
                                        +
                                      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
                                        • -
                                        • Document:文档对象。代表内存中的dom树

                                          -
                                            -
                                          • 获取Element对象
                                              -
                                            • getElementById(String id):根据id属性值获取唯一的element对象
                                            • -
                                            • getElementsByTag(String tagName):根据标签名称获取元素对象集合
                                            • -
                                            • getElementsByAttribute(String key):根据属性名称获取元素对象集合
                                            • -
                                            • getElementsByAttributeValue(String key, String value):根据对应的属性名和属性值获取元素对象集合
                                          • +
                                          • include : 页面包含的。导入页面的资源文件,可以引入其它的jsp或者html,引入之后就会展示同样的内容。
                                              +
                                            • <%@include file=”top.jsp”%>
                                          • -
                                          • Elements:元素Element对象的集合。可以当做 ArrayList来使用

                                            +
                                          • taglib导入资源。用来导入标签库
                                                * <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
                                            +        * prefix:前缀,自定义的。之后就可以用`<c:XXX>`了。相当于什么std::。
                                            +
                                          • -
                                          • Element:元素对象

                                            -
                                              -
                                            1. 获取子元素对象
                                            -

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

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

                                            中文乱码

                                            但是注意一点

                                            +
                                            //获取流对象之前,设置流的默认编码: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()类似

                                                -
                                              • String attr(String key):根据属性名称获取属性值
                                              • +
                                              • response.getWriter()和out.write()的区别: 在tomcat服务器真正给客户端做出响应之前,会先找response缓冲区数据,再找out缓冲区数据。因而response.getWriter()数据输出永远在out.write()之前 所以说,用out更好,因为它跟随你布局变化,你out写在哪,这句话最终就会输出在哪
                                              -
                                                -
                                              1. 获取文本内容
                                              2. + +
                                              3. pageContext PageContext 当前页面共享数据,还可以获取其他八个内置对象

                                                +
                                              4. +
                                              5. session HttpSession 一次会话的多个请求间

                                                +
                                              6. +
                                              7. application ServletContext 所有用户间共享数据

                                                +
                                              8. +
                                              9. page Object 当前页面(Servlet)的对象,相当于this

                                                +
                                              10. +
                                              11. config ServletConfig Servlet的配置对象

                                                +
                                              12. +
                                              13. exception Throwable 异常对象。只在page指令的isErrorPage为true的情况下才能使用此对象。

                                                +
                                              -
                                                -
                                              • String text():获取字标签的所有纯文本内容
                                              • -
                                              • String html():获取标签体的所有内容(包括字标签的字符串内容)
                                              • +

                                                其中,

                                                +

                                                image-20230307144539506

                                                +

                                                这四个为用来共享数据的域对象

                                                +

                                                演变:MVC开发模式

                                                jsp的演变

                                                image-20230307145442383

                                                +

                                                MVC模式

                                                将程序分成三个部分,分别是M-V-C。

                                                +
                                                  +
                                                1. M:Model,模型。JavaBean
                                                    +
                                                  • 完成具体的业务操作,如:查询数据库,封装对象
                                                2. -
                                                3. Node:节点对象

                                                  -
                                                    -
                                                  • 是Document和Element的父类
                                                  • +
                                                  • V:View,视图。JSP
                                                      +
                                                    • 展示数据
                                                    • +
                                                    +
                                                  • +
                                                  • C:Controller,控制器。Servlet
                                                      +
                                                    • 获取用户的输入
                                                    • +
                                                    • 调用模型
                                                    • +
                                                    • 将数据交给视图进行展示【域对象共享数据】
                                                -
                                                快速查找
                                                  -
                                                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. +

                                                  image-20230307150845273

                                                  +

                                                  服务器将接收的请求给控制器处理,控制器控制model完成必要的运算,model把算出的东西返回给控制器,控制器再把数据交给视图展示,数据最终就回到了浏览器客户端。

                                                  +

                                                  这就算是一个微型CPU了吧,控制器就是CU,模型就是ALU,也许客户端和视图什么的可以视为IO接口。

                                                  +
                                                    +
                                                  • 优缺点:

                                                    +
                                                      +
                                                    1. 优点:

                                                      +
                                                        +
                                                      1. 耦合性低,方便维护,可以利于分工协作
                                                      2. +
                                                      3. 重用性高
                                                      -

                                                      第四部分 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目录下。

                                                          +
                                                        2. 使得项目架构变得复杂,对开发人员要求高
                                                        3. +
                                                        +
                                                      • +
                                                    +
                                                  • +
                                                  +

                                                  那么,我们可以知道,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. +
                                                        -
                                                      • war包会自动解压缩
                                                      • +
                                                      • 域名称:
                                                          +
                                                        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. 配置conf/server.xml文件
                                                      <Host>标签体中配置
                                                      <Context docBase="C:\aWorkSpace\Projects\Java\JavaWeb" path="/web" />

                                                      +
                                                    6. 案例

                                                      +

                                                      这样一来,访问/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. +
                                                      +
                                                    7. +
                                                    8. 获取非字符串类型的值

                                                      +
                                                        +
                                                      1. 对象

                                                        +
                                                      2. +
                                                      3. 集合(List、Map等)

                                                      -
                                                      然后之后访问时输入`localhost/web/JavaWeb.html`即可
                                                      -
                                                      -* docBase:项目存放的路径
                                                      -* path:虚拟目录
                                                      -
                                                      -
                                                        -
                                                      1. 在conf\Catalina\localhost创建任意名称的xml文件。在文件中编写
                                                        <Context docBase="C:\aWorkSpace\Projects\Java\JavaWeb" />
                                                          -
                                                        • 虚拟目录:xml文件的名称
                                                        • -
                                                      2. +
                                                      -
                                                      注意,该方法是热部署的。也就是说,可以不关闭服务器的情况下,去增删xml文件,会马上变化,而不是像上面两种方式一样重启生效。
                                                      -
                                                    9. -
                                              -

                                              动态项目目录结构

                                              项目都存放在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项目

                                              +
                                            +]]> + + Java + + + + 计算机组成原理 + /2023/06/21/comporgan/ + 概述

                                            架构

                                            冯诺依曼

                                            以运算器为中心,指令和数据同等地位(不满足摩尔定律)

                                            +

                                            image-20230617133555268

                                            +

                                            存储器为中心

                                            image-20230617133840406

                                            +

                                            哈佛架构

                                            哈佛结构数据空间和程序空间是分开的

                                            +

                                            大部分ROM操作部分是采用了冯诺依曼结构

                                            +

                                            有些需要CPU与ROM之间快速的响应和交互,采用的是5级流水的哈佛结构。

                                            +

                                            早期(如X86)采用冯诺依曼

                                            +

                                            DSP和ARM用改进哈佛

                                            +

                                            image-20230617134010940

                                            +

                                            现代计算机

                                            image-20230617134045099

                                            +

                                            RISC-V

                                            数的表示

                                            无符号数与有符号数

                                            机器数与真值

                                            image-20230619211124996

                                            +

                                            意思就是真值有±符号,机器数把±符号换成了数字罢了

                                            +

                                            原码/补码/反码/移码

                                            image-20230617142534411

                                            +
                                            原码

                                            整数用逗号隔开,小数用小数点隔开

                                            +
                                            整数

                                            image-20230617140847726

                                            +
                                            小数

                                            image-20230617140920563

                                            +
                                            注意0的特殊情况

                                            image-20230617141011120

                                            +
                                            补码
                                            +

                                            对于负数,原码->补码符号位不变,数值位按位取反再加一的理论由来。神奇的是对于补码->原码,也是按位取反再加一。

                                            +

                                            简单证明一下:

                                            +

                                            设x为补码,y为原码,n为位数

                                            +

                                            已知 x = !(y - 2^n) +1

                                            +

                                            则反转一下可得 y = !(x - 1) + 2^n

                                            +
                                            +

                                            符号位不变,按位取反再加一

                                            +
                                            整数

                                            image-20230617141209384

                                            +
                                            小数

                                            image-20230617141232325

                                            +
                                            特殊

                                            [y]补连同符号位在内,每位取反末位加1,即得**[-y]补**

                                            +

                                            后面那三个是真的抽象

                                            +

                                            image-20230617141627719

                                            +
                                            反码

                                            对于正数,反码和原码一致;

                                            +

                                            对于负数,反码为原码的数值位取反

                                            +
                                            整数

                                            image-20230617141351461

                                            +
                                            小数

                                            image-20230617141418617

                                            +
                                            0

                                            image-20230617141525918

                                            +
                                            移码
                                            +

                                            注意,移码只有整数形式的定义,这与它的用途有关。计算机中,移码通常用来标识浮点数的阶码【阶码是整数】。

                                            +

                                            与补码数值位计算方式相同,区别是符号位相反

                                            +

                                            image-20230617142858076

                                            +

                                            image-20230617145309340

                                            +

                                            注意,移码的0为100000,最小值为000000

                                            +

                                            浮点表示

                                            表示形式和范围

                                            image-20230617143220170

                                            +

                                            注意,这边的上溢和下溢只与阶码有关,与尾数无关。

                                            +

                                            这个溢出条件及其处理方式需要记,会考

                                            +

                                            image-20230617143328674

                                            +

                                            规格化

                                            image-20230617143654003

                                            +
                                            特例

                                            image-20230617143731873

                                            +
                                            范围

                                            image-20230617143912465

                                            +

                                            image-20230617145251169

                                            +
                                            题型 表示范围

                                            image-20230617145826541

                                            +

                                            看得我cpu快烧了

                                            -

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

                                            -

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

                                            +

                                            https://www.jianshu.com/p/7b9dd240685c

                                            +

                                            image-20230617145857698

                                            +

                                            之所以最小负数不一样,是因为原码不能表示-1,补码可以;

                                            +

                                            之所以规格化最大负数是那玩意,是因为最大负数本应为2^-8,为了规格化必须再加个2^-1,然后原码转补码就变成那样了

                                            -

                                            原理

                                            执行原理

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

                                                IEEE754标准

                                                难绷,最沙比的来了

                                                +

                                                image-20230617150024955

                                                +

                                                没有阶符和数符力

                                                +

                                                image-20230617150101092

                                                +

                                                image-20230617150816446

                                                +

                                                它就相当于指数是移码表示的,并且注意到一点就是指数的0和255被征用表示特殊的数了,所以指数范围为1-254

                                                +

                                                image-20230617151013979

                                                +

                                                题型 把数转化为IEEE754

                                                首先背一下上面那个数的范围图,然后判断下是规格化还是非规格化,然后套公式就行了

                                                +

                                                image-20230617152014370

                                                +

                                                image-20230617152309065

                                                +

                                                算术移位与逻辑移位

                                                +

                                                来自 https://blog.csdn.net/qq_34283722/article/details/107093193

                                                +

                                                img

                                                +

                                                这应该与补码的运算机制有关。

                                                +
                                                +

                                                image-20230617152527973

                                                +

                                                反码不论是左还是右都添1

                                                +

                                                image-20230617152938114

                                                +

                                                注意,符号位不变!!!这点在左移的时候需要尤其注意,很容易出错

                                                +

                                                RISC-V概述

                                                ISA

                                                ISA位宽:通用寄存器的宽度,决定了寻址范围大小、数据运算强弱。

                                                +

                                                CISC-RISC

                                                image-20230619211742901

                                                +

                                                X86 & MIPS

                                                相比于上述的差异,还有以下几个:

                                                +
                                                  +
                                                1. x86有8个通用寄存器,MIPS有32个
                                                2. +
                                                3. x86有标志寄存器,MIPS没有
                                                4. +
                                                5. x86为两地址指令,MIPS为三地址
                                                6. +
                                                7. x86有堆栈指令,MIPS没有
                                                8. +
                                                9. x86有IO指令,MIPS设备统一编址
                                                10. +
                                                11. x86函数参数只用栈帧,MIPS用4寄存器+栈帧
                                                12. +
                                                13. X86的字为2字节,MIPS/RISC-V的字为4字节
                                                +

                                                RISC-V的特点

                                                  +
                                                1. RISC-V是小端,也即低字节放在低地址

                                                2. -
                                            -

                                            生命周期

                                            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进行交互。

                                            +
                                          • 支持字节(8位)、半字(16位)、字(32位)、双字(64位,64位架构)的数据传输

                                            +

                                            主存按照字节进行编址

                                            +
                                          • +
                                          • 采用哈佛结构

                                            +
                                          • +
                                          • 三种特权模式

                                            +

                                            image-20230617155657577

                                            +
                                          • +
                                          • 模块化设计

                                            +

                                            image-20230617155224429

                                            +
                                          • +
                                      +

                                      RISC-V汇编语言

                                      寄存器

                                      image-20230619212236116

                                      +

                                      image-20230617194244388

                                      +

                                      x3的全局指的是全局的静态数据区

                                      +

                                      指令详解

                                      image-20230617160729611

                                      +

                                      算术指令

                                      RISC-V 忽略溢出问题,高位被截断,低位写入目标寄存器

                                      +

                                      如果想要保留乘法所有位:

                                      +

                                      image-20230617183814367

                                      +

                                      image-20230617183924758

                                      +

                                      image-20230617184010487

                                      +

                                      逻辑指令

                                      image-20230617184151843

                                      +

                                      移位指令

                                      image-20230617185225785

                                      +

                                      shift left logical,shift left arithmetic

                                      +

                                      数据传输

                                      ld/sd,lw/sw,lh/sh(半字,也即2字节),lb/sb,以及load指令对应的无符号数(+后缀u)版本。

                                      +

                                      bAddrReg+offset为4的倍数,数据传输指令除了字节指令(lb sb lbu)外都需要按字对齐。

                                      +

                                      注意,如果为有符号数取数,放入寄存器时会自动进行符号扩展

                                      +

                                      image-20230617191543433

                                      +

                                      比较指令

                                      image-20230617192731684

                                      +

                                      条件跳转指令

                                      image-20230617193045409

                                      +

                                      无条件跳转指令

                                      image-20230617193346852

                                      +

                                      j:+label,用于实现无条件跳转,使用相对于当前 PC(程序计数器)的偏移量来计算目标地址,跳转范围较广

                                      +

                                      jr:+寄存器,用于实现通过寄存器的值进行跳转,跳转的目标是存储在寄存器中的地址,而不是相对于 PC 的偏移量

                                      +

                                      伪指令

                                      image-20230617193450180

                                      +

                                      image-20230617193521343

                                      +

                                      函数调用及栈的使用

                                      image-20230617195109547

                                      +

                                      六种指令格式

                                      image-20230617213801145

                                      +

                                      注意,jalr属于I型指令,而非J型指令!!!

                                      +

                                      image-20230617213815058

                                      +

                                      image-20230620164851924

                                      +

                                      R型指令

                                      image-20230617214018489

                                      +

                                      image-20230617214139338

                                      +

                                      I型指令

                                      image-20230617214544561

                                      +

                                      image-20230617214659517

                                      +

                                      image-20230617214735023

                                      +
                                      特例1 load

                                      image-20230617215007481

                                      +
                                      特例2 jalr

                                      image-20230617215304022

                                      +

                                      注意,jalr也属于I型指令,且其funct3为0

                                      +

                                      S型指令

                                      image-20230617215730749

                                      +

                                      image-20230617215842401

                                      +

                                      B型指令

                                      image-20230617220521906

                                      +

                                      这个计算过程很值得注意

                                      +

                                      image-20230617220833542

                                      +

                                      image-20230617220903564

                                      +

                                      image-20230617221131122

                                      +

                                      U型指令

                                      image-20230617221220495

                                      +

                                      image-20230617221323268

                                      +

                                      image-20230617221508473

                                      +

                                      666

                                      +

                                      J型指令

                                      image-20230617222154873

                                      +

                                      寻址方式(x86)

                                      +

                                      img

                                      +

                                      img

                                      +

                                      img

                                      +

                                      imgimg

                                      +

                                      尽管A很小,但可以让EA很大,从而扩展寻址范围。同时相对于上面的直接寻址,它更容易编程,因为只用修改A存储的那个地址值,而不用修改指令【比如说对数组进行循环,这个间接寻址就只用A++就行,而不用去修改指令里的那个“A”。】。

                                      +

                                      That is 指针【】

                                      +

                                      img

                                      +

                                      img至于为啥间接寻址不便于循环,也许是因为间接寻址是访存两次比较慢,要是真用来循环还了得

                                      +

                                      imgimg

                                      +

                                      程序动态定位

                                      +

                                      img循环数组时,可以用A作为数组地址,IX作为数组下标???【为什么不能用基址寻址?】

                                      +

                                      应该是因为基址寻址的基址是系统内定的,数组循环问题需要用户指定数组起始地址,所以不能用基址寻址,只能用面向用户的变址寻址。

                                      +

                                      img区别就在于直接寻址直接把指令参数****硬编码在内存****中,非常耗费空间。变址寻址则把指令参数作为变量了。

                                      +

                                      img更应该像是指令寻址方式。

                                      +

                                      程序浮动:程序在内存单元的位置出现变化【毕竟不可能同一个程序在每台电脑都是在同一个物理地址,相当于又减少了硬编码】

                                      +

                                      imgimg【为2002H是因为假设字长为2byte】

                                      +

                                      imgimg

                                      +

                                      一般栈顶地址最低。

                                      -
                                      -

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

                                      -

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

                                      -

                                      image-20221230215927404

                                      -

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

                                      +

                                      运算方法

                                      定点运算

                                      一位乘法运算

                                      原码一位乘

                                      +

                                      image-20230621195833297

                                      +

                                      大致明白了:

                                      +

                                      ①乘积一共有四位,故而需要两个寄存器来保存。

                                      +

                                      ②按照上面的原理公式,每次右移一位,被移出的那一位也是最后的结果(相当于竖式中每次相加的最后一位),需要把它存储在另一个寄存器中。

                                      +

                                      ③我们选择了存乘数的寄存器,因为乘数已经乘过的位是没用的。存乘数的那个寄存器的乘数不断被结果的低位所替代。

                                      +

                                      故****基本流程****:

                                      +

                                      ①准备阶段:清零ACC【置部分积=0】,在MQ中放乘数,X中放被乘数

                                      +

                                      ②判断MQ中乘数最低位,若为1,则ACC部分积加上X中的被乘数;若为0,则ACC不变

                                      +

                                      ③将ACC和MQ中四位数字视作一个整体,符号位也算上,进行逻辑右移,左侧补0.

                                      +

                                      ④重复上述过程,按移位次数来控制结束。

                                      +

                                      ⑤则最后,ACC中存储的就是乘法结果的高位,MQ中存储的结果就是乘法中的低位。

                                      +

                                      这其实就是我们用的列竖式一行一行加起来的一个过程。

                                      +

                                      img

                                      +

                                      S是符号位,GM是乘法标志位。

                                      +

                                      控制门:当最后一位是1时,控制门打开,X中的被乘数进入加法器。

                                      -
                                      深层一些的问题
                                      分成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. 部分积 乘数

                                      5. -
                                      -

                                      service参数

                                      image-20230102005833327

                                      -

                                      http协议

                                      概述

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

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

                                        +
                                      • 乘数不用符号位,写数值位即可

                                      • -
                                      • 特点:

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

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

                                          -
                                          +
                                        4. 按照是0是1,要么+被乘数要么+0

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

                                        历史版本:

                                        -
                                          -
                                        • 1.0:每一次请求响应都会建立新的连接
                                        • -
                                        • 1.1:复用连接
                                        • -
                                        +
                                      • 右移(连符号位一起逻辑右移)

                                        +

                                        image-20230620232152706

                                      • -
                                      -
                                      报文格式
                                      请求

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

                                      -

                                      数据格式:

                                      -
                                        -
                                      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. 相对安全
                                              +

                                              Booth算法

                                                +
                                              1. 部分积 乘数 y补(一开始为0)

                                              2. -
                                            +
                                          • 部分积双符号位,乘数单符号位且参与运算

                                            +

                                            image-20230620212054291

                                          • -
                                          +
                                        • 每次依据乘数和y补的关系,进行是否加被乘数的决策:

                                          +

                                          注意右移不同于原码,是算术右移

                                          +

                                          image-20230620212122222

                                        • -
                                        +
                                      2. 最后一步不用移位

                                        +

                                        image-20230620212150280

                                      3. -
                                      4. 请求头:客户端浏览器告诉服务器一些信息
                                        请求头名称: 请求头值

                                        -
                                          -
                                        • 常见的请求头:

                                          +
                                      +

                                      除法运算

                                      逻辑左移

                                      +

                                      最后得到的余数还得乘个2的-n次方

                                      +

                                      恢复余数法

                                        +
                                      1. 被除数(余数) 商
                                      2. +
                                      3. 先加上 - 除数的补
                                      4. +
                                      5. 如果得到结果≥0,则上商1,左移
                                      6. +
                                      7. 如果小于0,则上商0,+除数补,左移
                                      8. +
                                      9. 左移5次(商包括符号位的所有数字被填满),最后一次上商不用移位
                                      10. +
                                      +

                                      不恢复余数法(加减交替法)

                                      +

                                      image-20230621200001791

                                      +

                                      总结一下,大概流程:

                                      +

                                      ①准备阶段:MQ清零【存放商】,ACC放入被除数,X放入除数

                                      +

                                      ②ACC - X中的值

                                      +

                                      ③若ACC中值【上一轮的余数】为负,则上商0;为正,则上商1.ACC左移一位。判断MQ的最后一位【上商的值】,若为负,则ACC + X中的y;为正,ACC - X中的y。【注意,若为第一次减去X,则当余数为正时,就即刻发生溢出错误退出】

                                      +

                                      ④重复③,直到移位n次。

                                      +

                                      img

                                      +

                                      V表示是否溢出。

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

                                        +
                                      2. 被除数(余数) 商

                                      3. -
                                      4. Accept:可以支持的响应格式

                                        +
                                      5. 先加上 - 除数的补

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

                                        +
                                      8. 如果得到结果≥0,则上商1,左移,下一次继续加 - 除数的补

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

                                        -
                                          -
                                        • 作用:
                                        • -
                                        +
                                      11. 如果小于0,则上商0,左移,下一次加除数的补

                                        +

                                        image-20230620234054088

                                        +

                                        逻辑左移

                                        +
                                      12. +
                                      13. 左移5次(商包括符号位的所有数字被填满),最后一次上商不用移位

                                        +
                                      14. +
                                      +

                                      浮点运算

                                      舍入

                                      +

                                      (1)意思是,舍去的要是1,就在保留数+1.如果是0就直接舍去。

                                      +

                                      img这意思难道是说可以一次性右移,最后再看要不要+1,而不是移一下加一次1?【不过想了一下,这两种顺序得到的结果好像是一样的。】

                                      +
                                      +

                                      快速进位链

                                      +

                                      https://www.bilibili.com/video/BV1AB4y1p7ax?spm_id_from=333.880.my_history.page.click&vd_source=ac571aae41aa0b588dd184591f27f582

                                      +

                                      以及老师在这讲的也挺好的【p88】

                                      +

                                      imgimg

                                      +

                                      当AiBi都为1时,无论Ci是什么,都必定进位1;当AiBi有一个为1时,Ci才会起决定性作用;当AiBi都为0时,无论Ci是什么,都不会进位。因此,AiBi为本地进位,Ai+Bi为传送条件。(乘号表示且,加号表示或)

                                      +

                                      img进位链是影响加法器速度的瓶颈

                                      +

                                      img但问题是电路太复杂了,因此给出折中方案:

                                      +

                                      img

                                      +

                                      4先产生进位,传给3,3再产生进位,传给4,依次下去。

                                      +

                                      img

                                      +

                                      imgimg

                                      +

                                      imgimg

                                      +

                                      相当于又套了一层并行进位链。

                                      +

                                      img实在是太强了。感受到还要再套一层分组的必要性了。

                                      +
                                      +

                                      处理器

                                      RISC-V数据通路的组件选择

                                      image-20230617232028775

                                      +

                                      RISC CPU采用哈佛架构。

                                      +

                                      存储器

                                        +
                                      1. DM Data Memory 数据存储器

                                        +

                                        读异步,写有写使能

                                        +
                                      2. +
                                      3. IM Instruction Memory 指令存储器

                                        +

                                        一般read only

                                        +
                                      4. +
                                      +

                                      寄存器堆

                                      同步写异步读

                                      +

                                      image-20230617233101214

                                      +

                                      立即数扩展(生成)部件

                                      零扩展、符号扩展

                                      +

                                      PC(程序计数器)

                                      支持两种加法:+4、+立即数

                                      +

                                      ALU

                                      +

                                      【以下运算器结构适用于累加型运算器。累加器好像意思是一次最多两个输入。 】

                                      +

                                      运算器的功能是运算,因此其核心就是ALU(算术逻辑单元)。ALU是一个组合电路,组合电路的特点是,如果输入撤销了,那么输出结果也会撤销【组合逻辑电路】。因而,为了让ALU的结果能被保存,必须在输入端加上两个寄存器来保证信号持续输入。这两个寄存器一个叫做ACC,另一个叫做x,也叫做数据寄存器。

                                      +

                                      imgMQ也是寄存器,用于保存计算过程中溢出的位数。

                                      +

                                      img具体见第六章,弹幕说汇编语言也有讲。乘法要这样放是为了防止乘积低位覆盖乘数。

                                      +

                                      img

                                      +

                                      imgACC里存放着上面的操作或者与外部交流得到的被乘数,按照约定需要转移到X里。我猜M放在MQ而不是ACC,可能是因为第一二步是并行的,如果放在ACC就需要一些等待。

                                      +

                                      并且乘法做的是移位累加【可能相当于上面乘法原理的第一个图吧】,ACC用来存储这些累加的暂时交换成果,因而需要将ACC先清空为0.

                                      +

                                      这些操作的先后顺序由控制器进行控制。

                                      +

                                      img

                                      +

                                      MQ也称乘商寄存器

                                      +
                                      +

                                      运算类型:加、减、或、比较、slt、nor

                                      +

                                      操作数:寄存器或立即数

                                      +

                                      image-20230619214753499

                                      +

                                      RISC-V部分指令的数据通路设计

                                      取数指令的完成过程

                                      +

                                      image-20230621192714673

                                      +

                                      下面是取数指令的完成过程。

                                      +

                                      完成一条指令有三个阶段:取指令、分析指令、执行指令。

                                      +

                                      取指令:PC把地址送到MAR,MAR把地址送到存储体。存储体在控制器的控制下,把地址所对应的指令的内容发给MDR,MDR把取出的指令送到IR.

                                      +

                                      分析指令:IR将指令的操作码部分交予CU,CU控制IR,IR将指令中的地址码部分交予MAR,MAR给存储体,存储体在控制器控制下给MDR,MDR送给ACC。

                                      +

                                      【这个过程正像是计算机网络,只不过此处全靠硬件完成,计算机网络只能依靠协议】

                                      +
                                      +

                                      流水线周期

                                      RISC-V

                                      image-20230617233444017

                                      +

                                      image-20230618170512788

                                      +

                                      注意,在ID阶段还会发生读寄存器

                                      +

                                      image-20230617233433422

                                      +

                                      X86

                                      +

                                      一、指令周期

                                        -
                                      1. 防盗链:image-20230101235437317如果ref头非合法就不播放
                                      2. -
                                      3. 统计工作:看从哪个网站来的人数多
                                      4. +
                                      5. 基本概念
                                      6. +
                                      +

                                      ① 指令周期

                                      +

                                      ② 每条指令的指令周期不同

                                      +

                                      imgADD取指阶段和执行阶段都需要一次访存

                                      +

                                      ③ 具有间接寻址的指令周期

                                      +

                                      img

                                      +

                                      三个周期各需要访存一次。【****现在暂时还不知道这有毛用****】

                                      +

                                      ④ 具有中断周期的指令周期

                                      +

                                      img

                                      +

                                      ⑤ 指令周期的流程

                                      +

                                      img

                                      +

                                      ⑥ CPU工作周期的标志

                                      +

                                      指令周期的不同阶段,控制器要做不同的操作,要发出不同的命令。因而,控制器需要知道当前处于指令周期的哪一个阶段。

                                      +

                                      img用四个触发器

                                      +
                                        +
                                      1. 指令周期的数据流
                                      +

                                      ① 取指周期

                                      +

                                      img

                                      +

                                      首先,PC把自己里面存的地址放进MAR,再通过地址总线传输给存储器。

                                      +

                                      CU通过控制总线向存储器发出读控制信号。

                                      +

                                      存储器执行读操作,通过数据总线传输取到的指令给MDR,MDR再传给IR。

                                      +

                                      CU把加一后的地址保存在PC中,为下一条指令取指做准备。

                                      +

                                      ② 间址周期

                                      +

                                      img

                                      +

                                      如果指令的数据部分采用的是间接寻址的方式,那么此时,MDR中的地址部分不是有效地址,而是存储存储有效地址的存储单元的地址值。因而,我们需要再通过一次访存操作,把有效地址值存储在MDR中。

                                      +

                                      ③ 执行周期

                                      +

                                      img留给第九章介绍。

                                      +

                                      ④ 中断周期

                                      +

                                      做了三件事:保存断点、形成服务程序入口地址、中断返回

                                      +

                                      img

                                      +

                                      首先,保存断点。由CU来确定断电保存在内存单元的哪里。CU把地址传给MAR,MAR将其发到存储器,CU给存储器写命令。PC将自己的值【也就是下一条要执行的命令的地址值】交付给MDR,MDR传给存储器。【MDR在读写操作时都充当了缓冲区的角色。】

                                      +

                                      然后,CU形成中断服务程序入口地址,并直接把它写入到CU。

                                      +
                                      +

                                      流水线处理器

                                      流水线概述

                                      流水线

                                      image-20230618150003983

                                      +

                                      这点我觉得讲得挺好的。以前只知道流水线通过并行来加速指令执行,但这里给出了一个新的思路:如果是单周期处理器,则RISC-V的时钟周期受执行时间最长的指令限制;如果是流水线处理器,时钟周期就可以由某个步骤决定,主频就可以加快。这个出发点很有意思。

                                      +

                                      如果流水线各阶段平衡,也即每个阶段需要的执行时间差不多,则

                                      +

                                      image-20230618150515599

                                      +

                                      也即在理想条件和有大量指令的情况下,流水线带来的加速比约等于流水线的级数,若各阶段不完全平衡,加速比会变小。

                                      +

                                      流水线技术是通过提高指令的吞吐率来提高性能的。

                                      +

                                      RISC-V与流水线

                                      我们可以看到,比起X86,RISC-V是面向流水线设计的,其特性与流水线高度相关:

                                      +
                                        +
                                      1. 指令长度相同

                                        +

                                        简化IF和ID

                                      2. -
                                      3. Connection:连接是否活着

                                        +
                                      4. 只有六种指令格式,格式整齐

                                        +

                                        能在一个阶段内完成译码和读寄存器(ID)

                                      5. -
                                      +
                                    • 只通过load、store访存

                                      +

                                      可以利用EX阶段计算存储器地址,然后在下一阶段访存(MEM)

                                    • -
                                    +
                                  +

                                  流水线冒险

                                  image-20230618151040625

                                  +

                                  结构冒险

                                  image-20230618151208189

                                  +

                                  数据冒险

                                  image-20230618151243620

                                  +
                                  解决方法
                                  前递

                                  image-20230618151601721

                                  +
                                  编译重排

                                  image-20230618151706080

                                  +
                                  停顿(气泡)

                                  实在不行只能暂停流水线了

                                  +

                                  image-20230618151637437

                                  +

                                  控制冒险

                                  image-20230619220308876

                                  +
                                  解决方法
                                  硬件支持

                                  image-20230619220259945

                                  +
                                  分支预测
                                    +
                                  1. 遇到分支预测就停顿

                                  2. -
                                  3. 请求空行
                                    空行,就是用于分割POST请求的请求头,和请求体的。

                                    +
                                  4. 分支预测

                                    +
                                      +
                                    1. 静态分支预测

                                      +

                                      image-20230618153106322

                                    2. -
                                    3. 请求体(正文):

                                      -
                                        -
                                      • 封装POST请求消息的请求参数的
                                      • -
                                      +
                                    4. 动态分支预测

                                      +

                                      image-20230618153125295

                                    -

                                    字符串格式:

                                    -
                                    //请求行
                                    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位数字. 分类:

                                        +
                                      +

                                      流水线数据通路和控制

                                      流水线数据通路

                                      流水线寄存器

                                      image-20230618154028950

                                      +

                                      image-20230618154608423

                                      +

                                      66666,这个帅

                                      +

                                      image-20230618154815411

                                      +

                                      流水线控制

                                      数据冒险:前递与停顿

                                      前递

                                      分类

                                      前递有两种情况:

                                      +

                                      image-20230618155952018

                                      +
                                      前递产生条件
                                        +
                                      1. RegWrite != 0(写有效)
                                      2. +
                                      3. Rd != x0
                                      4. +
                                      +
                                      解决方法

                                      流水线寄存器解决:

                                      +

                                      image-20230618160141827

                                      +

                                      并且增加前递所需硬件。

                                      +

                                      停顿

                                      流水线寄存器解决:

                                        -
                                      1. 1xx:服务器接收客户端消息,但没有接收完成,等待一段时间后,发送1xx多状态码,询问是否还要继续发

                                        -
                                      2. -
                                      3. 2xx:成功。代表:200

                                        +
                                      4. 置ID/EX寄存器中控制信号为0(防止寄存器和存储器被写入数据),执行空指令nop

                                      5. -
                                      6. 3xx:重定向。代表:302(重定向),304(访问缓存)

                                        -

                                        image-20230103151112791

                                        -

                                        需要自动重定向到另一个C去

                                        -

                                        image-20230103151237984

                                        -

                                        发现资源未变化且本地有缓存

                                        +
                                      7. 禁止PC寄存器和IF/ID寄存器内容改变

                                        +

                                        下一条指令就能重新取指

                                      8. -
                                      9. 4xx:由客户端造成的错误

                                        -

                                        代表:

                                        +
                                      +

                                      控制冒险

                                      image-20230618161320779

                                      +

                                      image-20230618161333487

                                      +

                                      缩短分支延迟的方法:

                                      +

                                      硬件支持

                                      image-20230618161429557

                                      +

                                      动态分支预测

                                      image-20230618161807387

                                      +

                                      image-20230618161932429

                                      +

                                      计算目标地址

                                      image-20230618162114338

                                      +

                                      流水线的多发技术

                                      img

                                      +

                                      img

                                      +

                                      超流水技术要求一个时钟周期内不同的指令不能相互叠加干扰。

                                      +

                                      img

                                      +

                                      意思就是多条指令并成一条,有公共的取指、译码、写回阶段,但是执行阶段各不相同且并行执行,应该是这样。

                                      +

                                      例外和中断

                                      概述

                                      image-20230618162247586

                                      +

                                      内部的一定是例外,外部的只有IO请求和硬件故障是中断

                                      +

                                      image-20230618162302149

                                      +

                                      image-20230618162437636

                                      +

                                      image-20230618162500928

                                      +

                                      哦哦哦WOC!!!!!

                                      +

                                      这让我想起来在做xv6的时候,的那个kerneltrap和usertrap,应该就是这里的这个统一入口地址。

                                      +

                                      xv6是RISC-V架构,故而发生中断的时候,就会跳转到统一的kernel trap,然后再在里面通过scause进行读取。666

                                      +

                                      不过盘问了下gpt,RISC-V对于exception和interruption的处理方式是不一样的:

                                      +

                                      在RISC-V中,异常通常是由于程序执行过程中的错误或非预期事件而引起的,包括故障(faults)、陷阱(traps)和中止(aborts)。中断(interrupts)则是由外部事件触发的,例如定时器到期、外部设备请求等。中断是异步事件,与当前正在执行的指令无关,因此会在任何时候发生。

                                      +

                                      例外是通过统一入口地址处理,中断则是中断向量的方式

                                      +

                                      流水线中的例外

                                      image-20230618163521639

                                      +

                                      微操作(X86)

                                      X86将一条指令的执行分为多个微操作。

                                      +
                                      +

                                      一、微操作命令分析

                                      +

                                      微操作命令是控制单元在完成一大条指令时所需要细分完成的一条条微小的命令

                                      +

                                      image-20230621201435702

                                        -
                                      1. 404(请求路径没有对应的资源,可能路径输错了)

                                        +
                                      2. 取值周期

                                        +

                                        image-20230621201345807

                                      3. -
                                      4. 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);
                                        }
                                      5. -
                                      +
                                    2. 间址周期

                                      +

                                      image-20230621201351939

                                    3. -
                                    4. 5xx:服务器端错误。

                                      -

                                      代表:500(服务器内部出现Exception)

                                      -
                                      int i = 3/0;
                                    5. -
                                    +
                                  5. 执行周期 ①访存指令 ②非访存指令 ③转移指令 ④三类指令的指令周期

                                    +

                                    image-20230621201358396

                                    +

                                    imgimg

                                    +

                                    image-20230621201423245

                                  6. -
                                  +
                                9. 中断周期 硬件法和软件法

                                  +

                                  imgimg

                                  +

                                  硬件和软件法。

                                10. -
                                11. 响应头:

                                  +
                                +

                                二、控制单元的功能

                                  -
                                1. 格式: [头名称 : 值]

                                  +
                                2. 输入信号

                                  +

                                  ①时钟信号 ②指令寄存器【控制信号与操作码有关】 ③标志 ④外来信号【中断请求、总线请求】

                                3. -
                                4. 常见的响应头:

                                  -
                                    -
                                  1. Content-Type:服务器告诉客户端本次响应体 数据格式以及编码格式

                                    -

                                    浏览器依照编码格式来对该页面进行解码。

                                    +
                                  2. 输出信号

                                    +

                                    ①CPU内各种控制信号【比如(PC)+1->PC这种】

                                    +

                                    ②送至控制总线的信号【比如中断响应、总线响应】

                                  3. -
                                  4. Content-disposition:服务器告诉客户端以什么格式打开响应体数据

                                    -
                                      -
                                    • 值:
                                        -
                                      • in-line:默认值,在当前页面内打开
                                      • -
                                      • attachment;filename=xxx:以附件形式打开响应体。也即点击超链接后开始文件下载
                                      • -
                                      +
                                    • 控制信号举例

                                      +

                                      ①不使用内部总线

                                      +

                                      ②采用内部总线

                                    • -
                                    +
                                  5. 多级时序系统

                                    +
                                      +
                                    1. 机器周期

                                      +

                                      取指周期=机器周期=最复杂的微操作所需时间【访存】

                                      +

                                      在机器周期内部也需要有时钟来控制微操作的执行顺序

                                    2. -
                                    +
                                  6. 时钟周期(节拍、状态)

                                    +

                                    每个指令周期都可分为若干个机器周期,每个机器周期都可分为若干个节拍(时钟周期)。一个机器周期内包含多少节拍与需要发送多少控制信号、控制信号复杂度、控制信号能否并行有关。

                                    +

                                    时钟产生节拍信号,不同的节拍信号有不同的先后顺序。

                                    +

                                    一个时钟周期产生一个或几个【并行的几个,或者是操作时间很短,虽然有一定的先后顺序,但可以在一个节拍内完成】微操作命令

                                    +

                                    时钟信号利用上升沿让CU发出控制命令【微操作】控制各个不同部件。

                                5. -
                                6. 响应空行

                                  -
                                7. -
                                8. 响应体:传输的数据

                                  +
                                9. 控制方式

                                  +

                                  ①同步控制方式 采用定长的机器周期、不定长的机器周期、中央控制和局部控制相结合

                                  +

                                  ​ 当指令大多都是可以提前确定的,就用同步。当一条微操作的时间很难控制,可以采用异步控制。

                                  +

                                  ②异步控制方式 等待IO读写

                                  +

                                  ③联合控制方式 同步与异步结合

                                  +

                                  ④人工控制

                                -

                                字符串格式:

                                -
                                //响应行
                                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的代表范围更大

                                  -
                                  +

                                  三、组合逻辑设计

                                  +
                                    +
                                  1. 组合逻辑控制单元框图

                                    +

                                    ①CU外特性 ②节拍信号

                                  2. -
                                  3. 获取协议及版本 HTTP/1.1

                                    -
                                    String getProtocol()
                                  4. -
                                  5. 获取访问的客户机的IP地址

                                    -
                                    String getRemoteAddr()
                                  6. -
                                  -
                                  获取请求头
                                    -
                                  1. 通过请求头的名称获取请求头的值

                                    -
                                    String getHeader(String name)
                                  2. -
                                  3. 获取所有的请求头名称

                                    -
                                    Enumeration<String> getHeaderNames()
                                    - -

                                    返回的是一个迭代器

                                    +
                                  4. 微操作的节拍安排

                                    +

                                    ①安排微操作时序的原则

                                    +

                                    原则一:先后顺序不更改。

                                    +

                                    原则二:可以并行执行的,且微操作间没有先后顺序的,就尽量把它们安排在一个节拍中。

                                    +

                                    原则三:时间较短微操作尽量在一个节拍内且可以有先后顺序。

                                    +

                                    ②取值周期间址周期执行周期的

                                    +

                                    image-20230621201709245

                                    +

                                    image-20230621201717225

                                    +

                                    image-20230621201722561

                                    +

                                    image-20230621201732549

                                  -
                                  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);
                                  }
                                  }
                                  }
                                  - -

                                  请求体中键值对会在一行里,用&分割

                                  +
                +

                存储器

                概述

                分类

                image-20230618183618033

                +

                层次结构

                +

                寄存器分为两类,体系结构寄存器和非体系结构寄存器。前者可以让程序员调度使用,后者不行。

                +
                +

                image-20230618183742599

                +

                image-20230618183845067

                +

                主存储器

                概述

                基本组成

                +

                MAR中的地址需要经过译码器才能得到对应存储体中的位置。MDR中的数据是读是写需要通过读写电路控制,读写电路接收控制电路的读写信号。

                +
                +

                image-20230618184214916

                +

                与CPU连接

                image-20230618211118255

                +

                小端模式

                image-20230618211717123

                +

                技术指标

                image-20230618211918511

                +

                半导体存储芯片简介

                基本结构

                image-20230618212044462

                +

                image-20230618212116854

                +

                译码驱动方式

                线选法

                image-20230618212207454

                +
                重合法

                image-20230618212227553

                +

                RAM 随机存取存储器

                DRAM和SRAM

                image-20230618222428159

                +

                SRAM

                基本电路
                +

                image-20230621193330855

                +

                核心就是利用****触发器(T1—T4)****来表示0和1的

                +

                用T5和T6行开关来控制对触发器部件读写,用T7和T8列开关……【对应上面说的重合法?】

                +

                写入要在A段写入数据,同时在A’段写入数据的非【因为触发器是双稳态的,要求两边输入的信号相反。】对应的,写选择那边输入数据也得对称经过门和非门。

                +
                +
                经典芯片

                image-20230618212447162

                +
                读写

                img

                +

                上面的部分是64*64的基本电路矩阵。我们按列分,每十六列为一组,则分成了四组。因为2^4=16,因而我们用四位来表示地址控制信号。

                +

                对于行,当地址控制信号为0000时,表示选择存储矩阵的第一行的数据,为0001时,选择第二行的……依此类推。

                +

                对于列,当地址控制信号为0000时,表示选择每一组的第一列的数据,为0001时,选择第二列的……依此类推。

                +

                每一组只能有一列被选中,这就达到了一次读写四位的目的。【一个字节分开存】

                +

                DRAM

                基本电路
                +

                主要是通过电容的充放电实现的

                +

                img

                +

                左侧三管那个中,读数据线读出的跟存储的是相反的,存0读1,存1读0.但写入跟输入的信息是相同的。

                +

                右侧单管中,读出时数据线有电流则是1,没有则是0.写入时,对Cs充电则为1,Cs放电(输入信号为低电平)则为0.

                +
                +

                image-20230618215704699

                +
                经典芯片/读写

                image-20230618215803608

                -

                获取时中文乱码

                -
                  -
                • get方式:tomcat 8 已经将get方式乱码问题解决了

                  +

                  img

                  +

                  14位的地址分了两次传,分别作为行列地址。

                  +

                  RAS:行选控制信号 CAS:列选控制信号 WE:读写控制信号。产生的时钟控制了芯片内部的读写操作

                  +

                  img

                  +

                  如果读放大器左边有电,那么右边输出没电;左没电右有电.这样,读放大器左边的部分,有电表示0,没电表示1 ;读放大器右边的部分,有电表示1,没电表示0.

                  +
                +
                刷新

                为什么要刷新:

                +

                image-20230619224003589

                +
                集中刷新

                image-20230618221439701

                +
                分散刷新

                image-20230618221603727

                +
                异步刷新

                image-20230618222049113

                +

                ROM 只读存储器

                  +
                1. 掩膜ROM(MROM) 用户不能修改

                  +

                  image-20230618222716561

                2. -
                3. post方式:会乱码

                  -
                                     * 解决:在获取参数前,设置流的编码:
                  -
                  -    
                  request.setCharacterEncoding("utf-8");
                  -
                  +
                4. PROM(一次性编程) 破坏性编程

                  +

                  image-20230618223116398

                5. -
          -
    -
    获取请求参数通用的方法(通用指对get和post通用)

    这里的请求参数应该是指上面Post的请求体、Get的请求行里的参数,请求头里的参数是获取不到的。

    -
      -
    1. 根据参数名称获取参数值

      -
      String getParameter(String name)
      - -

      如 username=zs&password=123,getParameter(“username”)会得到zs。

      +
    2. EPROM(多次性编程)

      +

      image-20230618223151914

    3. -
    4. 根据参数名称获取参数值的数组

      -
      String[] getParameterValues(String name)
      - -

      如 hobby=xx&hobby=game,会得到{xx,game}

      +
    5. EEPROM(电可擦写)

      +

      image-20230618223229585

      +
    6. +
    7. Flash Memory(闪速型存储器)

      +

      image-20230618223252821

    8. -
    9. 获取所有请求的参数名称

      -
      Enumeration<String> getParameterNames()
    10. -
    11. 取所有参数的map集合

      -
      Map<String,String[]> getParameterMap()
    12. -
    -
    请求转发

    在服务器内部资源跳转

    -

    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. 转发是一次请求,多个资源使用的是同一次请求
    -
    共享数据

    接力工作的两个Servlet可以通过request对象进行数据通信。

    -
    * 方法:
    -
    +

    存储器与CPU的连接

    存储器容量的扩展

    位扩展

    image-20230618224638384

    +
    字扩展

    image-20230618224745584

    +

    带了片选思想

    +
    位字扩展

    image-20230618224955557

    +

    存储器与CPU的连接

    存储器的校验

    image-20230620221830055

    +

    汉明码组成

    image-20230620222021345

    +

    image-20230620222133679

    +

    image-20230620222301502

    +

    n为数据的位数

    +

    image-20230620222225994

    +

    image-20230620222518156

    +

    汉明码纠错

    image-20230620222951821

    +

    跟组成的步骤是一样的

    +

    提高访存速度的措施

    image-20230618233156234

    +

    image-20230618233234559

    +

    image-20230618233325427

    +

    image-20230619224341497

    +

    image-20230618233855438

    +

    不过这里也帅得一批,非常有那种从小到大的抽象思维在。

    +

    之前的单独一块RAM芯片,一个字节是分开存;这里的一个主存堆,一个块是分主存存。

    +

    Cache 高速缓冲存储器

    概述

    image-20230618233726163

    +

    技术指标

    image-20230618233800847

    +

    image-20230618234113714

    +

    因为在一个存取周期当中,每体都可以取一个字,16体就可以取16字,因而一个存取周期可以取出16个字出来。

    +

    image-20230618234151529

    +

    但是这个公式前提是访问cache和主存并行。如果换用另一个策略,即先看cache有没有,没有再去主存,计算公式就不一样。

    +

    Cache的读写操作

    +

    img

    +

    cache接收CPU发来的地址信号。CPU发出的地址中的块内地址无需转换,而块号需要通过主存cache地址映射变换机构转化成cache内的块号。【所以说CPU访问cache的时候,传给cache的地址是主存的物理地址吧?然后再通过主存cache地址映射转化为cache的块内地址。】

    +

    如果命中,则转换机构工作,传递地址给cache存储体,存储体通过数据总线发送信号。

    +

    如果不命中,并且cache没装满,则发送信号给主存。

    +

    如果不命中,且cache装满了,则cache替换机构使用替换算法,淘汰cache中一些块,同时发送信号给主存。

    +

    主存收到信号,在数据总线上发给cpu要的东西之后,再将所在块发给cache

    +

    image-20230621193732489

    +
    +

    image-20230618234639119

    +

    image-20230618234752772

    +

    Cache-主存映射

    直接映射

    image-20230618234904098

    +

    image-20230618234921789

    +

    全相联映射

    image-20230618235007647

    +

    组相联映射

    image-20230618235147739

    +

    缓存替换算法

    image-20230618235750985

    +

    改进

    +

    现在很多处理器至少有三级cache。比如每个核一个cache,多个核还有一个公用的cache。

    +

    流水线计算机很多都分了指令cache和数据cache,避免资源冲突。

    +

    注意,每个层次的cache采用的映射可能不一样。

    +

    靠近CPU采用直接相连或者路数(r)少的组相连【其实直接相连就相当于是一路的组相联了】。中间的用组相联。距离CPU较远的用全相联。

    +

    距离越远,对速度要求越低,对利用率要求越高。

    +
    +

    虚拟存储器

    与Cache的差异

    image-20230619000944183

    +

    虚拟存储器

    image-20230618235955557

    +

    image-20230619000042556

    +

    相当于把主存-辅存(磁盘)看成另一个cache-主存。这也就类似于内存页面换入换出了。原来这玩意叫虚拟存储器啊,不过这也类似于虚拟地址空间的叫法就是了。

    +

    image-20230619000208477

    +

    image-20230619000304314

    +

    页表结构

    image-20230619000428020

    +

    访问流程

    image-20230619000542665

    +

    TLB

    image-20230619000628910

    +

    image-20230619000804374

    +

    image-20230619000827244

    +

    image-20230619000851370

    +

    辅助存储器

    硬盘、U盘、软盘、磁带、光盘

    +

    RAID

    image-20230619142112318

    +

    image-20230619142148621

    +

    image-20230619143428427

    +

    image-20230619143411577

    +

    系统总线

    概述

    是啥

    总线两个特点:分时共享

    +

    遵循协议标准,方便计算机系统集成、扩展和进化

    +

    总线的猝发传输方式:在一个总线周期内,传输存储地址连续的多个数据字的总线传输方式。

    +

    分类

    image-20230618173335493

    +

    image-20230618173414845

    +

    总线结构

    单总线

    注意,单总线是默认统一编址的?

    +

    image-20230618175005524

    +

    面向CPU的双总线

    image-20230618175035406

    +

    存储器为中心

    image-20230618175435591

    +

    有通道的多总线结构

    image-20230618175533637

    +

    image-20230618175631552

    +

    +

    image-20230618175705273

    +

    image-20230618175743501

    +

    总线控制

    总线判优控制

    image-20230618180345695

    +

    image-20230618180704321

    +

    注意,独立请求是最快的

    +

    链式查询

    +

    所有设备可在BR线发布总线请求,主设备通过BG线表态,争得总线的设备要通过BS线告诉其他设备总线忙。

    +

    BG线中,总线同意信号会依次遍历每一个设备,直到找到第一个提出请求的设备。

    +

    可见,这个遍历顺序就代表了各个IO设备的优先级顺序。

    +

    这样相当于分离出格外的线来控制信号。这种方式对电路故障非常敏感。

    +
    +

    image-20230618180431836

    +

    计数器定时查询

    +

    意思好像是,在BR线提出请求,主设备接收到请求后,可以响应的情况下,启动计数器,计数器初始值为零。计数器的值通过设备地址线输出。如果计数器为0,则观察接口0有没有请求,没有的话计数器++,继续看下一个,以此类推,直到找到第一个对应接口,则开始传输数据,BS线启用。

    +

    设备地址线需要给所有设备地址进行编码,因此宽度与设备数有关。

    +

    这个的优点在于,优先级的确定更加灵活了。比如说,计数器不一定从零开始而是从上一次停止的地方开始(循环优先级,这样的话每个设备的机会均等),或者用软件控制优先级初始值,或者每一次不一定++而是有其他计算规则。

    +
    +

    image-20230618180602116

    +

    独立请求方式

    +

    优先级由主设备内部逻辑(排队器)规定。也可以用自适应、计数器等等等。

    +
    +

    image-20230618180645765

    +

    总线通信控制

    image-20230618180848436

    +

    这玩意传输周期还考了

    +

    image-20230618180912414

    +

    这个通信方式有哪几种也要求默写了

    +

    image-20230618181827840

    +

    这个同步和异步的特点总结得很棒

    +

    同步、异步、半同步三者的共同点:

    +

    image-20230618181948854

    +

    同步

    +

    img定宽定距的时钟

    +

    白色菱形代表有地址、命令、数据;紫色阴影代表没有东西

    +

    数字电路中,数字电平从低电平(数字“0”)变为高电平(数字“1”)的那一瞬间(时刻)叫作上升沿。数字电平从高电平(数字“1”)变为低电平(数字“0”)的那一瞬间叫作下降沿。

    +

    有固定的时间点,和在每个固定时间点固定要做的事

    +

    第一部分:主设备要给出地址信号

    +

    第二部分:给出读命令(控制信号)

    +

    第三部分:从设备传输数据给主设备

    +

    第四部分:读命令、数据信号撤销

    +

    第五部分:地址信号撤销

    +

    img

    +

    *先给数据能保证命令到达立刻写入正确数据。菱形那段表示电平并非瞬间稳定*

    +

    *如果数据是并行就先给数据,再给读写信号,直接锁存;如果是串行数据,就先给读写信号,再给数据*

    +

    有固定的时间点,和在每个固定时间点固定要做的事

    +

    第一部分:主设备要给出地址信号

    +

    第二部分:主设备给出数据信号

    +

    第三部分:主设备给出写入信号

    +

    第四部分:写入

    +

    第五部分:读命令、数据信号撤销

    +

    第六部分:地址信号撤销

    +
    +

    同步通信通常只适用于总线长度短的。

    +

    因为是并行总线,总线长度长了很难做到等长,到达设备后就不同步了

    +

    因为需要统一时标;总线长,需要迁就最远的设备;读写时间差距大,需要迁就最慢的设备

    +

    异步

    image-20230618181416220

    +
    不互锁

    CPU从主存读信息

    +

    主要用在单机不同设备之间的通信中

    +
    半互锁

    多机系统中,某个CPU需要访问共享存储器时

    +
    全互锁

    主要用于网络通信,如TCP三握手

    +

    半同步通信

    输入数据为例:

    +

    image-20230618181924196

    +

    分离式通信

    +

    在子周期2中,从模块实际上从从模块变成了主模板,因为它发起了占用总线的请求。

    +
    +

    image-20230618182050912

    +

    IO

    概述

    发展概况

    image-20230619144243913

    +

    image-20230619144359313

    +

    image-20230619144452679

    +

    组成

    image-20230619144602504

    +
    +

    ① IO指令

    +

    操作码相当于标志,标志这个指令是IO的。命令码才算是操作码,指出对IO设备做什么。设备码给出IO设备或者设备中某一个寄存器【端口】的编址。

    +

    ② 通道指令

    +

    通道是小型DMA处理器,可以实现IO设备与主机之间进行信息交互。

    +

    通道有自己的控制器,有的通道还有存储器。

    +

    通道能够执行由通道指令组成的通道程序。

    +

    通常情况下,编程人员在应用程序当中,为了调用外部设备,应用程序中需要增加广义IO指令【这意思是封装吧】。广义IO指令要指出参加数据传输的IO设备、数据传输主存的首地址、传输数据的长度、传输方向。操作系统根据广义IO指令给出的参数以及要求的操作,会编写一个由通道指令组成的通道程序,并且会把程序放到内存或者是通道内存的指定位置,之后启动通道进行工作。

    +
    +

    连接方式

    编址

    image-20230619144651480

    +

    选址和传送

    image-20230619144727325

    +

    联络方式

    image-20230619144851937

    +

    image-20230619145010236

    +

    连接方式

    image-20230619145037444

    +

    控制方式

    image-20230619145313853

    +

    程序查询方式

    image-20230619145133293

    +

    程序中断方式

    image-20230619145154206

    +

    image-20230619145214775

    +

    DMA方式

    image-20230619145252864

    +

    外部设备

    概述

    image-20230619145414624

    +

    IO接口

    概述

    image-20230619151239077

    +

    功能和组成

    image-20230619151310223

    +

    image-20230619151421396

    +

    image-20230619151442848

    +

    接口类型

    image-20230619151602920

    +

    程序查询方式

    image-20230619151713642

    +

    image-20230619152130068

    +

    image-20230619152909693

    +

    程序中断方式

    中断

    概述

    image-20230619153352974

    +

    image-20230619153557165

    +

    接口电路

    image-20230619153715848

    +
    中断请求触发器和中断屏蔽触发器

    image-20230619153949008

    +

    image-20230619154445642

    +

    中断分类

    外部中断一般是由计算机外设发出的中断请求,如:键盘中断、打印机中断、定时器中断等。

    +

    外部中断一般指io高低电平(下降沿等由寄存器配置)来触发并响应io中断函数。

    +

    接口电路

    排队器

    image-20230619155014803

    +

    image-20230619155029933

    +
    硬件实现

    image-20230619155054248

    +
    +

    以下介绍的是链式排队器

      -
    1. 存储键值对

      -
      void setAttribute(String name,Object obj)
    2. -
    3. 获取值

      -
      Object getAttitude(String name)
    4. -
    5. 移除键值对

      -
      void removeAttribute(String name)
    6. +
    7. INTR默认为0,取非为1. 经&后整个排队电路为1
    8. +
    9. 当i设备发出请求,INTRi=1,取非为0,经&后变为0,INTPi之后的电路清零,只有i之前的INTP为1
    10. +
    11. 3在一连串的显示为 1 的INTP中,最后一个显示1的设备优先级最高。因为按照我们的分析,是它发出了请求
    -
    获取ServletContext
    ServletContext getServletContext()
    - -
    练习:结合数据库与Servlet进行用户登录
    -

    要求:

    -

    1.编写login.html登录页面
    username & password 两个输入框
    2.使用Druid数据库连接池技术,操作mysql,day14数据库中user表
    3.使用JdbcTemplate技术封装JDBC
    4.登录成功跳转到SuccessServlet展示:登录成功!用户名,欢迎您
    5.登录失败跳转到FailServlet展示:登录失败,用户名或密码错误

    +

    使用与非+非而不是直接与门是因为与非门+非更便宜。

    +

    我猜这个意思是,链式排队的话,越前面的优先级越高,现在我们讲的是怎么快速****找出****最高的最前面的是哪一个。之所以为什么越前面的优先级最高,可从这个电路中得知。如果一个东西发出请求,那么它后面的INTPi’都会被置零,因而它肯定比它后面的高级。因此越前面的优先级越高。

    +

    https://www.likecs.com/show-390301.html

    +

    img

    +

    这个可以验证我的观点。至于这个轮询方式,应该在第三章的总线那边讲过,应该用的是链式查询。

    -
    文件结构

    ![屏幕截图 2023-01-02 235207](./JavaWeb/屏幕截图 2023-01-02 235207.png)

    +
    软件实现

    程序查询

    +

    image-20230619155116410

    +

    中断向量形成部件

    硬件向量法

    image-20230619155154822

    +
    软件查询法

    image-20230619161630629

    +

    接口电路组成

    image-20230619161803883

    -

    错误历程

    +

    应该意思就是,参照上面那个程序电路图,首先CPU先发送一个启动IO设备的命令,然后就去忙了。

    +

    与此同时,IO接口接到命令开始准备,比如说对DBR的整理【因读写而异】。

    +

    IO接口准备完之后会卡在INTR那边,等待CPU的中断查询信号。

    +

    CPU本来一直在不断边干自己的活边发送中断查询信号【在每条指令执行阶段的结束前】,终于逮到这个时候发现IO接口已经准备好了,就回复中断响应信号,CPU进入中断周期,执行中断隐指令。

    +

    IO接口发出中断请求后就排好队选好设备了,收到CPU的中断响应信号,就给CPU发向量地址,CPU根据地址去内存中找到中断服务程序并开始执行,之后就可以开始数据传输了。

    +

    可见这个过程是异步的。

    +
    +

    中断响应(中断处理过程)

    image-20230619162001376

    +

    image-20230619162057309

    +

    IO中断处理过程

    image-20230619162142239

    +

    image-20230619201800980

    +

    单重/多重中断服务流程(CPU)

    image-20230619201934346

    +

    image-20230619202014591

    +

    image-20230619202106846

    +

    中断屏蔽技术(CPU)

    image-20230619202207357

    +

    image-20230619202219859

    +

    image-20230619202321781

    +

    image-20230619202355070

    +

    image-20230619202434146

    +

    DMA方式

    特点

    image-20230619202548302

    +

    实现方案

    image-20230619202628150

    +

    沙比

    +

    image-20230619202708423

    +

    image-20230619202739633

    +

    功能和组成

    image-20230619202841809

    +

    image-20230619203043097

    +

    工作过程

    DMA传送过程

    预处理、数据传送、后处理

    +

    image-20230619203248171

    +

    注意还有个传送字数,看来有点安全设定。如果溢出了就需要中断

    +

    image-20230619203423482

    +

    image-20230619203535039

    +

    连接方式

    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. 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. +
    5. 分组密码一般指对称分组密码
    -
    -
    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的

    +

    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

    +]]> + + + 编译原理 + /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. 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. 收集标识符的属性信息,并将其存入符号表
    7. +
    +

    picture

    +

    种属就是比如是函数还是数组之类的。

    +

    picture

    +
      +
    1. 语义检查
    2. +
    +

    picture

    +
      +
    1. 静态绑定

      +

      包括绑定代码相对地址(子程序)、数据相对地址(变量)

    -
    -
    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. 获取输出流

      +

      中间代码生成

      picture

      +

      picture

      +

      波兰也就是前序遍历二叉树(中左右),逆波兰也就是后序遍历二叉树(左右中)

      +

      picture

      +

      代码优化

      picture

        -
      1. 字节输出流

        -
        ServletOutputStream getOutputStream()
      2. -
      3. 字符输出流

        -
        PrintWriter getWriter()
      4. -
      +
    2. 无关机器

      +

      picture

    3. -
    4. 使用输出流,将数据输出到客户端浏览器

      +
    5. 有关机器

      +

      picture

    -
    案例
    重定向

    资源跳转的一种方式。

    -

    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值相同。这两个对象实质上是不一样的。

    -
    -

    重定向的特点(与请求转发完全相反):

    +

    目标代码生成

    picture

    +

    表格管理

    这也挺好理解,相当于管理符号表吧。

    +

    picture

    +

    错误处理

    picture

    +

    编译程序的组织

    了解了编译程序的基本结构,那么我们就可以想想该怎么实现这个编译器了。

    +

    最直观的想法是,我们有几个步骤就对代码进行多少次扫描:

      -
    1. 浏览器地址栏路径改变
    2. -
    3. 可以访问其他站点的资源
    4. -
    5. 使用多次请求,不能使用request对象共享数据
    6. +
    7. 首先扫一次,进行词法分析,将所有标识符写入到符号表中,同时进行语法分析,看看有没有错,如果出错了就转到错误处理,没有的话就进行语义分析;(三合一)
    8. +
    9. 然后再针对得出来的语义分析树进行中间代码生成;
    10. +
    11. 再对得出来的中间代码进行代码优化,最后对优化出来的代码进行翻译处理。(二合一)
    -

    路径写法:

    +

    picture

    +

    picture

    +

    picture

    +

    实现编译器

    picture

    +

    T形图

    picture

    +

    自展

    picture

    +

    也就是说:

      -
    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. +
      7. P0是汇编语言,可以用来编译C语言子集;(P0:汇编语言,C子集→汇编)
      8. +
      9. P1是机器语言,可以用来把汇编语言翻译为机器语言;(P1:机器语言,汇编→机器)
      10. +
      11. 所以我们就得到了P2,也即一个可以用来编译C语言子集的机器语言程序;(P2:机器语言,C子集→汇编)
      12. +
      13. 然后我们就可以用C语言子集来写C语言编译程序P3,再用P2翻译P3,就可以得到工具P4。(P4:汇编语言,C→汇编)
      +

      image-20230912153726618

      +

      帅的。

      +

      移植

      picture

      +

      picture

      +

      本机编译器的利用

      picture

      +

      编译程序的自动生成

      这大概是描述了我们到时候会怎么实现这两个阶段代码。

      +

      不过确实,词法分析可以看作是正则匹配,语法分析可以看作是产生式。

      +

      picture

      +

      picture

      +

      第二章 文法等概念

      image-20231111160656018

      +

      基本概念

        +
      1. 字母表

        +

        picture

        +

        picture

        +

        picture

        +

        picture

      2. -
    -

    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);
    }
    - -

    会话

    会话:一次会话中包含多次请求和响应。

    -
      -
    • 一次会话:浏览器第一次给服务器资源发送请求,会话建立,直到有一方断开为止

      -
    • -
    • 功能:请求之间本来是相互独立的。将多次请求组织在一次会话中,就可以让请求之间进行数据的共享。

      +
    • +

      克林闭包中的每一个元素都称为是字母表Σ上的一个串

      +

      picture

      +

      picture

      +

      picture

    • -
    • 方式:

      -
        -
      • 客户端会话技术 Cookie

        -

        把数据存进客户端

        +
+

文法

picture

+

如果文法用于描述单词,基本符号就是字母;用于描述句子,基本符号就是单词

+
    +
  1. 文法的形式化定义

    +

    picture

    +

    picture

    +

    由于可以从它们推出其他语法成分,故而称之为非终结符

    +

    picture

    +

    picture

    +

    还真是最大的语法成分

  2. -
  3. 服务器端会话技术 Session

    -

    把数据存进服务器端

    +
  4. 产生式

    +

    picture

  5. - +
  6. 符号约定

    +

    picture

    +

    picture

    +

    picture

    +

    文法符号串应该就是指既包含终结符也包含非终结符的,也可能是空串的串。

    +

    注意终结符号串也包括空串。

  7. - -

    概念

    客户端会话技术,将数据保存到客户端

    -

    快速入门

      -
    • 使用步骤:

      +
+

语言

picture

+

这部分就是要讲怎么看一个串是否满足文法规则,那么我们就需要先从什么样的串是满足文法规则的串开始说起,也即引入“语言”的概念。

    -
  1. 创建Cookie对象,绑定数据【为了从服务器端发送cookie给客户端】
      -
    • new Cookie(String name, String value)
    • -
    • 可以看到,Cookie其实就是一种name-value这样的键值对对象
    • -
    +
  2. 推导与归约

    +

    picture

    +

    然后也分为最左推导和最右推导,对应最右归约和最左归约。

    +

    picture

    +

    故而,如果从开始符号可以推导(派生)出该句子,或者从该句子可以归约到开始符号,那么该句子就是该语言的句子。

  3. -
  4. 发送Cookie对象【因为要发送给客户端,所以应该在response里存】
      -
    • response.addCookie(Cookie cookie)
    • -
    +
  5. 句子与句型

    +

    picture

    +

    句型就是可以有非终结符,句子就是只能有终结符

  6. -
  7. 获取Cookie,拿到数据【因为是来自客户端,所以要从request里要】
      -
    • Cookie[] request.getCookies()
    • -
    +
  8. 语言

    +

    picture

    +

    文法解决了无穷语言的有穷表示问题。

    +

    picture

    +

    picture

    +

    emm,就是好像没有∩运算

    +

    picture

    +

    有正则那味了

+

乔姆斯基文法体系

picture

+

picture

+
    +
  1. 0型

    +

    picture

  2. -
  3. 代码

    -
    @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);
    }
    }
  4. -
  5. 得到效果

    -

    运行服务器,首先访问/demo,然后在同一个浏览器再次访问/demo2,就可以在控制台看到输出。

    -

    这个过程发生了什么呢?

    -

    首先,访问/demo就相当于建立了会话。/demo的Servlet获取到请求之后,在response中将cookie填入。

    -

    保持浏览器窗口不变,会话也不变。

    -

    再次访问/demo2,cookie信息自动保存在request对象中。/demo2的Servlet获取到请求之后,在控制台中打印输出了cookie。

    +
  6. 1型

    +

    picture

    +

    之所以是上下文有关,是因为只有A的上下文为a1和a2时才能替换为β【666666,第一次懂】

    +

    CSG不包含空产生式。

  7. - -

    细节学习

    一次发送多个cookie

    你看它那个API叫add,就知道数据结构差不多是个list,所以多次add就行。

    -
    保存时间

    默认情况下,浏览器关闭则cookie就马上被销毁。

    -

    如果需要持久化存储:

    -
    cookie.setMaxAge(int seconds)
    - -

    参数:

    +
  8. 2型

    +

    picture

    +

    左部只能是一个非终结符。

    +
  9. +
  10. 3型

    +

    picture

    +

    产生式右部最多只有一个非终结符,且要在同一侧

    +

    picture

    +

    看起来还能转(是的,自动机教的已经全忘了())

    +
  11. +
+

CFG

正则文法用于判定大多数标识,但是无法判断句子构造

    -
  1. 正数:将Cookie数据写到硬盘的文件中。持久化存储。并指定cookie存活时间,时间到后,cookie文件自动失效
  2. -
  3. 负数:默认值
  4. -
  5. 零:删除cookie信息
  6. +
  7. 分析树
-
中文问题

在tomcat 8 之前 cookie中不能直接存储中文数据,需要将中文数据转码——一般采用URL编码(%E3)

-

在tomcat 8 之后,cookie支持中文数据。

-
获取范围
    -
  1. 假设在一个tomcat服务器中,部署了多个web项目,那么在这些web项目中cookie能不能共享?

    -
      -
    • 默认情况下cookie不能共享

      -
    • -
    • 共享方法:

      -

      setPath(String path):设置cookie的获取范围。默认情况下,设置当前的虚拟目录

      -

      如果要共享,则可以将path设置为”/“

      +

      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. -
    +
  2. 否则,看输入符号是否与Follow集元素匹配

    +
      +
    1. +

      若B无空产生式,报错;否则,使用B的空产生式(相当于消了一个符号但不变输入带指针)

    2. -
    3. 不同的tomcat服务器间cookie共享问题?

      -

      比如说:

      -image-20230221225514567 - -
        -
      • setDomain(String path):如果设置一级域名相同,那么多个服务器之间cookie可以共享

        -

        setDomain(".baidu.com"),那么tieba.baidu.com和news.baidu.com中cookie可以共享)

        +
      • +

        报错

      • -
      +
-

作用和特点

特点:

+

picture

+

这个感觉跟first集有点像,相当于是右部只能以终结符开始的形式,所以下面的LL文法会增强定义。

+

当该非终结符对应的所有SELECT集不相交,就可以进行确定的自顶向下语法分析。这个思想也将贯穿下面的LL文法

+

picture

+

LL(1)文法

picture

+

picture

+

最后,如果同一非终结符的各个产生式的可选集互不相交,就可以进行确定的自顶向下分析:

+

picture

+

picture

+

总结

这几个推理下来,真是让人感觉酣畅淋漓!

+

确定的自顶向下分析的核心就是,给定一个当前所处的非终结符和一个输入字符[E, a],我们可以唯一确定一个产生式P用于构建语法分析树。

+

picture

+

也即,同一个非终结符的所有产生式的SELECT集必须是不交的【才能确保选择产生式的唯一性】。因而,问题就转化为了如何让SELECT集不交

+

我们需要对空产生式和正常产生式的SELECT集计算做一个分类讨论。

    -
  1. cookie存储数据在客户端浏览器

    -

    因而它相对不安全

    +
  2. 空产生式

    +

    由于可以推导出空,相当于把该符号啥了去读下一个符号,因此我们的问题就转化为输入字符a是否能够跟该符号后面紧跟着的字符相匹配。而紧跟着的字符集我们将其成为FOLLOW集,如果a在follow集中,那么就可以接受,否则不行。

    +

    对于LL(1)文法,相当于是进一步处理了简介推出空的串:

    +

    ​ 由于α串->*空,则α串必定仅由非终结符构成。那么它能推导出的所有可能即为SELECT集。故而为First(α)∪Follow(α)

  3. -
  4. 浏览器对于单个cookie 的大小有限制(4kb) 以及 对同一个域名下的总cookie数量也有限制(20个)

    +
  5. 非空产生式

    +

    很简单,就是其First集。

-

作用:

+

故而,只需要让这些计算出来的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. cookie一般用于存出少量的不太敏感的数据

    +
  2. 移进状态

    +

    ·后为终结符

    +
  3. +
  4. 待约状态

    +

    ·后为非终结符

  5. -
  6. 在不登录的情况下,完成服务器对客户端的身份识别

    -

    比如说,以不登录情况下对某个网页进行属性设置,你下次打开的时候属性设置依然在,这是因为你的属性设置的cookie在设置后被存入到你的电脑中,下次访问该网页发出请求,服务器端就能根据请求中cookie里的属性设置信息来做出响应了。

    +
  7. 归约状态

    +

    ·后为空

-

案例:记住上一次访问时间

需求:
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。想想也是(

+

picture

+

picture

+

以前感觉一直很难理解GOTO表的作用,现在感觉稍微明白了点了,你想想,归约之后的那个结果是不是有可能是另一个产生式的右部成分之一,也即一个新的句柄?并且这个也是由你栈顶刚归约好的那个左部和下面的输入符号决定的。那么你自然而然需要切换一下当前状态,以便之后遇到那个产生式的时候能发现到了。

+

那么,剩下的问题就是如何构造LR分析表了:

+

picture

+

算符分析

picture

+

也就是它会整一个终结符之间的优先级关系。。。

+

picture

+

picture

+

也就是说:

    -
  1. session用于存储一次会话的多次请求的数据,存在服务器端

    -

    比如说,当我们做重定向的时候,就可以选择用session共享数据(会话域)而非使用ServletContext(此范围过大)

    +
  2. a=b

    +

    相邻

  3. -
  4. session可以存储任意类型,任意大小的数据

    +
  5. a<b

    +

    也即在A->aB时,b在FIRSTOP(B)中(理解一下,这个First指在前面。。。)

  6. -
  7. session与Cookie的区别:

    -
      -
    1. session存储数据在服务器端,Cookie在客户端
    2. -
    3. session没有数据大小限制,Cookie有
    4. -
    5. session数据安全,Cookie相对于不安全
    6. -
    +
  8. a>b

    +

    也即在A->Bb时,a在LASTOP(B)中(理解一下,这个LAST指在后面。。。)

-

快速入门

    -
  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. +

    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. 这笔包含了语法分析、语义分析、中间代码生成
    -
      -
    • 不是同一个,但是要确保数据不丢失。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. +
      7. 通过为文法符号设置语义属性,来表达语义信息
      8. +
      9. 通过与产生式(语法规则)相关联的语义规则来计算符号的语义属性值
      -
    2. +

      也可能是先入为主吧,感觉用实验的方法来理解语义分析比较便利。语义分析相当于定义一连串事件,附加在每个产生式上。当该产生式进行归约的时候,就执行对应的语义事件。而由于执行语义分析时需要的符号在语法分析栈中,所以我们也同样需要维护一个语义分析栈,在移进时也需要进栈。

      +

      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. 可行的求值序列就是拓扑排序
      -

      案例

      -

      需求:

      +

      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. 访问带有验证码的登录页面login.jsp
      2. -
      3. 用户输入用户名,密码以及验证码。
          -
        • 如果用户名和密码输入有误,跳转登录页面,提示:用户名或密码错误
        • -
        • 如果验证码输入有误,跳转登录页面,提示:验证码错误
        • -
        • 如果全部输入正确,则跳转到主页success.jsp,显示:用户名,欢迎您
        • -
        -
      4. +
      5. 语义动作代码就是执行
      6. +
      7. 综合属性代码就是赋给关联语义动作
      8. +
      9. 非终结符就是选一个它作为左部的产生式,然后看看要不要用到它自身的属性对右部子属性进行复制(体现了继承属性)
      +

      递归的预测分析

      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边(蓝色)和加新边(红色),

      +]]> + + + Lab0 + /2023/02/25/cs144$lab0/ + Lab0
      +

      本次实验一直在强调的一点就是,TCP的功能是将底层的零散数据包,拼接成一个reliable in-order的byte stream。这个对我来说非常“振聋发聩”(夸张了233),以前只是背诵地知道TCP的可靠性,这次我算是第一次知道了所谓“可靠”究竟可靠在哪:一是保证了序列有序性,二是保证了数据不丢失(从软件层面)。

      +

      还有一个就是大致了解了cs144的主题:实现TCP协议。也就是说,运输层下面的那些层是不用管的吗?不过这样也挺恰好,我正好在学校的实验做过对下面这些层的实现了,就差一个TCP23333这样一来,我的协议栈就可以完整了。

      -
      初见思路

      我们可以在服务器端使用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 这种耦合杂乱的页面代码,但是模板引擎的思路大致相同,还是可以看一看的

      +
      +

      本次实验与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

      这个确实不难,就是这个地方有点坑:

      -

      关于热更新的机制可以看看这篇文章,水平有限还看不懂就先放在这了:

      -

      JSP热部署的实现原理[通俗易懂]

      +

      Please note that in HTTP, each line must be ended with “\r\n” (it’s not sufficient to use just “\n” or endl).

      -

      概念

      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>
      +

      导致我跟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();
      }
      -

      最终效果:

      -image-20230222230929893 +

      还有一点值得注意的是,当我这样时:

      +
      TCPSocket sock;
      sock.set_blocking(false);
      sock.connect(Address(host,"http"));
      -

      原理

      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" %>
      +

      会报错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 ++;
      }
      -

      用来配置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");
      +

      结果就是测试用例Timeout。我找了很久都不知道错在了哪,最后求助了场外观众【罪过……这次实验太不独立了】,学着他把length改成了这样:

      +
      int length = min(data.length(),capacity-buffer.size());
      -

      这样做在Servlet不会导致中文乱码,但JSP不行,这个大概是因为两者原理不一样。

      -

      Servlet的中文乱码:

      -image-20230222231756277 +

      发现成了。

      +

      我去看了看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;
      -

      JSP的:

      -

      image-20230222231820358

      -

      Servlet乱码是因为客户端和response请求体编码不一致,JSP乱码与JSP的原理有关,是只跟服务器端有关

      +

      具体实现

      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(); }
      +]]> + + + Lab1 StreamReassembler + /2023/02/25/cs144$lab1/ + Lab1 StreamReassembler
      +

      TCP managed to produce a pair of reliable in-order byte streams (one from you to the server, and one in the opposite direction), even though the underlying network only delivers “best-effort” datagrams.

      +

      You also implemented the byte-stream abstraction yourself, in memory within one computer.

      +

      Over the next four weeks, you’ll implement TCP, to provide the byte-stream abstraction between a pair of computers separated by an unreliable datagram network.

      +
      +

      我们的任务是实现一个StreamReassembler。它的具体功能相信看下数据传输路径就很明了了:

      -

      编译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

      +

      receiver的数据传输路径:network → StreamReassembler整流 →(write)ByteStream(read)→ app

      -

      指定方法是在JSP开头添加:

      -
      <%@ page pageEncoding="UTF-8"%>
      +

      感想

      先放个通关截图在这。

      +

      image-20230225192145829

      +

      这个实验我前前后后总共做了大概有9h+……写我下面放上来的屎山代码可能大概用了5h+。我总共使用了140+行代码实现我的核心函数push_substring

      +

      整个过程,包括思路和代码都十分复杂,但最后的表现相比于别人好像也没好到哪去,让我不禁怀疑自己是不是想错了……以及,这样的复杂性也给我带来很多担忧,担心会不会在以后的实验因为这个的bug而寄,毕竟我在写笔记的同时都已经找到了不止一个bug了()希望人没事。

      +

      总而言之,先把我的思路和一步步的代码拆解放上来吧。

      +
      +

      后记:

      +

      不得不说,这个东西太健壮了,给后面的TCPReceiver省去了好多功夫……

      +

      比如说,TCPReceiver无需考虑ack怎么算,因为这里就帮你算好了;TCPReceiver无需考虑数据包重叠或者重复,因为这里已经考虑到这个情况了;TCPReceiver无需担忧FIN是否会因为容量满丢弃一部分数据而未达到真正的FIN,只需调用其相关接口判断就行。

      +

      它虽然帮助了TCPReceiver那么多,但很神奇的是,它们的耦合性并不高。你把StreamReassembler单独拆出来看,左看右看,它都确实仅仅只是一个健壮的区间合并算法

      +

      这得益于实验设计的精良,得益于设计TCPReceiver时的野心。这些边界情况都这么麻烦,而且都只与区间合并有关,那么我们为什么不直接把它抽象进区间合并进行处理呢?这种想法极富胆识,事实证明最后也确实能实现。这种设计理念让我受益很深。

      +
      +

      为什么我的思路那么复杂

      看了感恩的代码,我发现我俩的大题思路其实差不多是一模一样的,都是先进行两轮的区间合并,然后再处理但为啥我看起来就那么复杂呢?

      +

      一是题意理解问题。

      +

      我发现他对_capacity的理解跟我的理解不一样emmm……

      +

      额好像怪我没认真看指导书的图。

      +

      image-20230225232723083-1677339210527-2

      +

      我理解的capacity:绿色部分和红色部分的净含量

      +

      似乎是真正的capacity:绿色部分+红色部分+空部分??也就是说capacity只是一个跟index差不多的索引下标??…………

      +
       // out of bound
      if (start >= hope_to_rec + _capacity - _output.buffer_size()) {
      return;
      }
      -

      内置对象

      在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. 缺点:

          +

          也就是说我能过测试是因为偶然吗??

          +

          我其实感觉正确理解的capacity意义好怪啊,这怎么就能节省内存了呢?我觉得我理解的反而比较有道理(倔强)

          +

          笑一笑算了家人们。总之先这么写吧,以后的实验寄掉了再回来改。

          +
          +

          UPDATE: 确实寄掉了,并且已经改过来了,也不复杂,只需要添加对right边界的处理就行。【指去掉超出start+capacity的部分。】

          +
          +

          二是代码规范问题。

          +

          首先他代码规范性强,看起来非常舒服。其次他会用类似upper_bound()这样的函数(反观我压根没想起来),这样就显得比我的循环简洁了很多很多。

          +

          三是设计问题。

          +

          他用的是map我用的是set。确实是map比较合理,它既有find功能也兼具了有序的特性。

          +

          我的思路

          我们要做的,是将零散的数据包拼接成完整的字节流,并且将整流过的数据送入ByteStream中,这通过核心函数push_substring实现。我们可以先来看看push_substring的定义:

          +
          void StreamReassembler::push_substring(const string &data, const size_t index, const bool eof) ;
          + +

          data为数据包,indexdata中第一个字符在整个字节流的下标,eof用来标识这是字节流的最后一个数据包。

          +
          +

          详细说明:

          +

          比方说有字节流“abcdefg”,则合法的参数对有如:{“abc”,0,0},{“cdef”,2,0},{“g”,6,1}

          +
          +

          通俗来说,我们这个函数的功能就是,把一堆起始下标为indexdata(无序、可能重叠)拼接为一个完整的字节流。

          +

          听起来有没有觉得很耳熟?是的,我认为这正是“区间合并”问题。我接下来便通过区间合并的思想,对问题进行如下数据结构以及算法的设计。

          +

          数据结构

          区间

          由于是区间合并问题,所以就先需要定义区间。

          +
          struct node {
          size_t left; // 当前data位于总体的左index(闭)
          size_t right; // 右index(开)
          string data;
          bool operator<(const node &b) const { return left < b.left; }
          };
          + +

          集合

          我们需要维护一个左端点升序的区间集合,故使用内部红黑树实现的有序集合set。

          +
          set<node> buffer;   // 存储结构
          + +

          算法

          我们要做的,是对数据包进行整流,并且把整流过的部分输送到ByteStream中。由于存储结构存在_capacity的上限,因而,我们需要尽可能早地把存储结构中已经整流好的数据送入ByteStream中。

          +

          那么,如何定义“已经整流好的数据”呢?它需要满足跟“之前已经整流好了的数据”的有序性,也即,比方说[0,1000]已经整流完毕送入app,那么下一个送入app的数据一定满足index=1001

          +

          因而,我们可以维护一个变量left_bound,表示下一个将被app接受的数据的index(如上例的1001)。为了达到“尽早”目的,我们需要在每次push_substring执行完区间合并之后,检查buffer的第一个区间的左端点是否与left_bound相等,是的话则将第一个区间写入ByteStream,不是的话就什么也不做。

          +

          因而,在push_substring中,对于一个新来的数据包,我们大致需要进行以下几步:

            -
          1. 使得项目架构变得复杂,对开发人员要求高
          2. -
          -
        4. +
        5. 将参数所给的区间( [index, index+data.length()) )并入区间集合buffer
        6. +
        7. 查看是否需要ByteStream
        -
      • -
      -

      那么,我们可以知道,jsp就只需负责数据的展示了。那怎么展示数据呢?这就需要用到jsp的几个技术了:

      -

      EL表达式

      -

      注意,servlet3.0以来默认关闭el表达式解析,需要手动在page上加属性打开,详见 jsp文件中的el表达式失效问题解决

      +

      区间合并

      问题定义

      问题可抽象为:

      +
      +

      给定一个有序区间集合buffer,以及一个小区间node,你需要把node塞进buffer里。

      +

      Example: buffer = {[1,3),[5,7)} , node = [6,8) 输出:buffer = {[1,3), [5,8)}

      -

      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. 语法:

          +
          算法思路

          判断区间重叠统一只检查左端点。注意,两次重叠的判断条件不一样,是因为相对性发生了改变。第一次相当于node的左端点在buffer[i]中,第二次相当于buffer[i]的左端点在node中。

            -
          1. ${域名称.键名}:从指定域中获取指定键的值
          2. -
          -
            -
          • 域名称:
              -
            1. pageScope –> pageContext
            2. -
            3. requestScope –> request
            4. -
            5. sessionScope –> session
            6. -
            7. applicationScope –> application(ServletContext)
            8. -
            +
          • buffer进行第一轮扫描

            +

            如果node与buffer[i]产生重叠((left >= it->left && left <= it->right)),那么更新node为node∪buffer[i],并且将buffer[i]从buffer中删去。

            +

            在第一次找到重叠的区间,就应该break退出第一轮循环。

          • -
          • 举例:在request域中存储了name=张三,获取:${requestScope.name}
          • -
          -
            -
          1. ${键名}:表示依次从最小的域中查找是否有该键对应的值,直到找到为止。
          2. -
          +
        4. buffer进行第二轮扫描

          +

          如果node与buffer[i]产生重叠( (it->left >= left && it->left <= right)),那么更新node为node∪buffer[i],并且将buffer[i]从buffer中删去。

        5. -
        6. 案例

          -

          这样一来,访问/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>
          -
        7. -
        8. 获取非字符串类型的值

          +

          我们在合并区间时,不仅需要对struct node的左端点left和右端点right进行更新,还需要对其数据域data也进行合并拼接。我们维护变量res作为维护的目标区间的数据域。对于res,我们应该进行如下操作:

            -
          1. 对象

            +
          2. 初始化为data

          3. -
          4. 集合(List、Map等)

            +
          5. 除去[left, left_bound)这一区间内的数据

            +

            这部分数据我们已经整流过并且写入ByteStream

          6. -
          +
        9. 在两轮合并中对其进行正确拼接

        10. -
        -
      4. +
        图解

        image-20230225200605175

        +
        代码
        size_t left = index, right = index + data.length();  // 初始化左右区间
        size_t o_left = left, o_right = right; // keep the original value
        node tmp = {0, 0, ""};

        if (right < left_bound) return; // must be duplicated
        left = left < left_bound ? left_bound : left; // 左边已经接受过的数据就不要了
        string res = data.substr(left - o_left, right - left);// 掐头

        /* 开始区间合并。需要扫描两次 */
        // 第一次
        for (auto it = buffer.begin(); it != buffer.end(); it++) {
        if (left >= it->left && left <= it->right) { // 区间重叠
        size_t r = right,l = left;
        // 更新左右端点
        right = max(right, it->right);
        left = min(left, it->left);
        if (r <= it->right) // 如果目标区间被包裹在it内
        // res需要更新为it头+data掐头后的全长+it尾,也即将it中间重叠部分用data替换
        res = it->data.substr(0, l - it->left) + data.substr(l - o_left) +
        it->data.substr(r - it->left, it->right - r);
        else
        res = it->data.substr(0, l - it->left) + data.substr(l - o_left);
        // 删除原来的结点
        buffer.erase(it);
        break;
        }
        }

        // 第二次
        for (auto it = buffer.begin(); it != buffer.end(); it++) {
        if (it->left >= left && it->left <= right) {
        if (it->right <= right);// it这个区间被包含在目标区间内,则什么也不做
        else {
        // 需要加上it的尾
        res += it->data.substr(right - it->left, it->right - right);
        // 更新右端点
        right = it->right;
        }
        // 删除
        buffer.erase(it);
        }
        }
        // 将维护区间插入区间集合
        tmp = {left, right, res};
        buffer.insert(tmp);
        + +

        写入ByteStream

        思路

        我们需要检查buffer内的第一个区间,如果其左端点与left_bound相等,则把第一个区间填入ByteStream,然后更新left_bound,从buffer中删去该区间;如果不相等(只可能是left > left_bound)则什么也不做。

        +

        在把区间数据填入ByteStream的过程中,可能造成ByteStream满。因而我们就只能填入第一个区间内的一部分数据,更新left_bound,将第一个区间的剩余数据继续存在buffer中。

        +
        代码
        auto iterator = buffer.begin();  
        iterator = buffer.begin();
        // write into the ByteStream
        if (iterator != buffer.end() && iterator->left == left_bound) {
        // 防止_output的容量超过
        size_t out_rem = _output.remaining_capacity();
        if (out_rem < iterator->data.length()) { // ByteStream剩余容量小于第一个区间长度
        _output.write(iterator->data.substr(0, out_rem));// 写入尽量多数据
        left_bound = iterator->left + out_rem;// 更新左边界
        // 由于iterator只读,因而我们不能直接修改其左端点和data域
        tmp = {left_bound, iterator->right, iterator->data.substr(out_rem)};
        buffer.erase(iterator);
        buffer.insert(tmp);
        } else {
        _output.write(iterator->data);
        left_bound = iterator->right;
        buffer.erase(iterator);
        }
        }
        + +

        buffer的最大容量_capacity

        背景

        维护“存储结构的容量不超过capacity”这个不变性条件可以说是这个实验最恶心最难的地方……也正是它,让我的代码写成了一坨shit山()

        +

        为什么说它最难最恶心呢?其实它本来也许不算难,但在这个思路下想要保持这个不变性条件,就显得非常地困难。

        +

        一开始没过脑子的时候,我觉得这样就行:

        +
        void StreamReassembler::push_substring(const string &data, const size_t index, const bool eof) {
        if(data.length() + unassemble_bytes() > capacity) return;
        }
        + +

        但是这样很明显有两个问题。

        +

        一是就算你超过了,你也不能直接丢弃掉data,得把没超过的部分填满。

        +

        二是,data.length() + unassemble_bytes()有时,甚至是很多时候,都不会是将data并入buffer之后buffer的容量。因为data和buffer很大概率会存在重叠区间。

        +

        那么,你能不能在区间合并完之后,再进行该不变性条件的判断,并且将没超过的部分填满,超过的部分丢弃呢?

        +

        答案是,也不能。因为经过两轮合并,你的data和buffer里原有的数据早已你中有我我中有你了,你无法在最后将它们分开,找出data超过capacity的数据并且丢弃它。

        +

        因而,头尾都不行的话,唯一的答案就是,我们只能在两轮区间合并中途,去时刻追踪当前容量是否超过capacity

        +

        这听起来就令人十分地头大。但事实证明,并不是无法实现的,坚持下去,就算是shit山也能跑起来()下面便是我的实现思路。

        +
        思路

        维护一个变量remaining,表示当前还有多少容量。维护start,表示未判断是否可以写入buffer的数据起点。我们要做的事:

        +
          +
        1. 初始化remaining为capacity - 当前容量,start为掐头后的left
        2. +
        3. 在第一轮循环中更新start
        4. +
        5. 在第二轮循环中通过start和remaining来判断是否能够写入buffer。尽可能多地写入,把写入不了的部分丢弃。
        +
        例子

        下面举个例子来说明整个流程。

        +
        initial:  
        buffer = { [1,3], [5,8],[10,12],[13,14] } , capacity = 12, remaining = 12-9=3, data = [2,11], start = 2
        + +

        start为2,是因为data的从2开始的这段数据还不知道能不能被成功存入buffer中。

        +

        第一次合并后,buffer = { [5,8],[10,12],[13,14] } ,data=[1,11],start = 3.

        +

        start为3,是因为[2,11]与[1,3]合并,由于[2,3]这段本来就在buffer中,因而可以不用占用remaining,但从3开始这段数据还不知道能不能成功存入buffer中。

        +

        在第二轮合并中,首先扫描到[5,8]。由于[start,it->left]也即[3,5]这段数据长度为2<remaining=3,故而这段数据可以存入buffer,remaining更新为3-2=1,start更新为it->right=8.

        +

        /*

        +

        注意,此处无需再对[9,10]进行同样的操作。对于这种情况:

        +
        buffer = { [1,3], [5,8] } , capacity = 12, remaining = 12-9=3, data = [2,11], start = 2
        + +

        在循环结束的这里已经处理:

        +
        if (remaining < right - start) {
        right = remaining + start;
        if (eof == 1)
        is_eof = false;
        }
        + +

        */

        +

        然后扫描到[10,12]。由于[start,it->left]也即[8,10]这段数据长度为2>remaining=1,故而这段数据只能把[8,9]这部分存入buffer。因而,我们把到此为止的[1,9]结点存入buffer,剩下的[10,11]部分直接丢弃,也即直接跳入到最后的写入ByteStream部分。

        +
        代码
        void StreamReassembler::push_substring(const string &data, const size_t index, const bool eof) {
        size_t remaining = _capacity - unassembled_bytes(); // how much room left?

        size_t left = index, right = index + data.length(); // 初始化左右区间
        size_t o_left = left, o_right = right; // keep the original value

        size_t start = left; // the begin of the unused data-zone of variable data
        // if(remaining == 0) goto end; // buffer满

        // 开始区间合并。需要扫描两次
        for (auto it = buffer.begin(); it != buffer.end(); it++) {
        if (left >= it->left && left <= it->right) {
        right = max(right, it->right);
        left = min(left, it->left);
        // 说明目标区间完全被包裹,也即目标区间一定可以塞进buffer中
        if (right == it->right) start = o_right;
        // 说明仅仅部分重叠,去重部分从it->right开始
        else start = it->right;
        // ...
        break;
        }
        }

        // 第二次
        for (auto it = buffer.begin(); it != buffer.end(); it++) {
        if (it->left >= left && it->left <= right) {
        // 比remaining满
        // 第一个条件是为了防止unsigned溢出
        if (it->left > start && remaining < it->left - start) {
        // 截取能塞得下的部分
        tmp = {left, start + remaining, res.substr(0, start - left + remaining)};
        remaining = 0;
        // 此时塞进去是肯定不重叠的,因为tmp.right < it->left
        buffer.insert(tmp);
        // 剩下的直接丢弃
        goto end;
        }
        // 塞得下
        remaining -= it->left - start;
        start = it->right;
        // ...
        }
        }

        // 边界处理
        if (remaining < right - start) {
        // 扔掉塞不下的部分
        right = remaining + start;
        }
        tmp = {left, right, res};
        buffer.insert(tmp);

        end:
        iterator = buffer.begin();
        // write into the ByteStream
        // ...
        }
        + +

        capacity还没结束

        背景

        你以为做到上面那个小标题那样就万无一失了吗?答案是,并不!

        +

        我们还有哪里想得不周全呢?考虑这样一个案例,ByteStream未满,但是在更新remaining时发现buffer已满塞不下了。这时候,我们上面的做法是直接扔掉塞不下的部分。但其实,我们还可以查看buffer的一部分数据是否能够再塞进ByteStream,如果能的话,就又能省下一笔空间了!

        +

        所以,我们在发现remaining不够时,应该首先检查能不能塞一部分buffer的数据进入ByteStream中用来腾出空间。

        +
        代码
        // 第二次
        for (auto it = buffer.begin(); it != buffer.end(); it++) {
        if (it->left >= left && it->left <= right) {
        // 比remaining满
        // 第一个条件是为了防止unsigned溢出
        if (it->left > start && remaining < it->left - start) {
        // 先看看能不能塞进ByteStream腾空间。需要满足两个条件
        // buffer的第一个区间正好是left_bound
        if (left == left_bound) {
        size_t out_mem = _output.remaining_capacity();
        // ByteStream有位置
        if (out_mem > 0) {
        // 腾出的空间很充足,完全可以不改变remaining
        if (out_mem >= it->left - start) {
        // 写入ByteStream
        _output.write(res.substr(0, it->left - start));
        // 更新
        res = res.substr(it->left - start);
        left_bound = it->left - start + left;
        left = left_bound;
        // 加上腾出的空间【在ok标签处减掉】
        remaining += it->left-start;
        goto ok;
        } else {
        // 空间不足以完全不改变remaining
        _output.write(res.substr(0, out_mem));
        res = res.substr(out_mem);
        left_bound = out_mem + left;
        left = left_bound;
        // 加上腾出的空间
        remaining += out_mem;
        // 如果两个加起来就行,则ok
        if(it->left>start && remaining>=it->left - start){
        goto ok;
        }
        // 否则remaining依然不充足
        }
        }
        }
        // 截取能塞得下的部分
        tmp = {left, start + remaining, res.substr(0, start - left + remaining)};
        remaining = 0;
        // 此时塞进去是肯定不重叠的,因为tmp.right < it->left
        buffer.insert(tmp);
        // 剩下的直接丢弃
        goto end;
        }
        ok:
        // 塞得下
        remaining -= it->left - start;
        start = it->right;
        // ...
        }
        }
        + +
        小细节

        因而,在一开始发现remaining满的时候,不能直接goto end。

        +
        // if(remaining == 0)	goto end; // buffer满
        + +

        因为还得看看ByteStream能不能腾空间。

        +

        eof

        对于eof的处理也是需要注意的一个小细节。

        +

        我们不能这么写:

        +
        if (eof)
        _output.end_input();
        + +

        原因有二,一是最后一个数据包有可能是部分丢失,二是整流可能还未结束。

        +

        所以我们应该在成员变量中维护is_eof,记录是否收到过最后一个数据包,并且在最后一个数据包部分丢失的时候置它为false。当且仅当is_eof == true且buffer非空时,才能说明输入结束。

        +

        相关代码:

        +
        void StreamReassembler::push_substring(const string &data, const size_t index, const bool eof) {
        if (eof == 1)
        is_eof = true;
        // ...
        // 第二次
        for (auto it = buffer.begin(); it != buffer.end(); it++) {
        if (it->left >= left && it->left <= right) {
        // 此时空间不够,先尝试下能不能写入一部分到_output中
        if (it->left > start && remaining < it->left - start) {
        // ...
        tmp = {left, start + remaining, res.substr(0, start - left + remaining)};
        remaining = 0;
        if (eof == 1)
        is_eof = false;
        buffer.insert(tmp);
        goto end;
        }
        ok:
        // ...
        }

        if (remaining < right - start) {
        right = remaining + start;
        if (eof == 1)
        is_eof = false;
        }
        // ...
        end:
        // ...
        // 满足两个条件才是真的eof
        if (is_eof && buffer.empty()){
        is_eof = false;
        _output.end_input();
        }
        }
        + +

        代码

        头文件声明

        class StreamReassembler {
        private:
        // Your code here -- add private members as necessary.
        struct node {
        size_t left; // 当前data位于总体的左index(闭)
        size_t right; // 右index(开)
        string data;
        bool operator<(const node &b) const { return left < b.left; }
        };
        bool is_eof; // 文件的末尾是否接收成功
        size_t left_bound; // 当前已经成功接收到left_bound之前的数据
        set<node> buffer; // 存储结构

        ByteStream _output; //!< The reassembled in-order byte stream
        size_t _capacity; //!< The maximum number of bytes

        public:
        // ...

        // used by the TCPReceiver
        void set_is_eof() { is_eof = false; }
        }
        + +

        具体实现

        #include "stream_reassembler.hh"
        #include <iostream>
        #include <map>

        template <typename... Targs>
        void DUMMY_CODE(Targs &&... /* unused */) {}

        using namespace std;

        StreamReassembler::StreamReassembler(const size_t capacity)
        : is_eof(false), left_bound(0), buffer(), _output(capacity), _capacity(capacity) {}

        void StreamReassembler::push_substring(const string &data, const size_t index, const bool eof) {
        if (eof == 1)
        is_eof = true;

        size_t unass = unassembled_bytes();
        size_t remaining = _capacity > unass ? _capacity - unass : 0; // how much room left?

        size_t left = index, right = index + data.length(); // 初始化左右区间
        size_t o_left = left, o_right = right; // keep the original value
        auto iterator = buffer.begin(); // 这些变量在这里声明是为了防止后面goto报错
        node tmp = {0, 0, ""};

        if (right < left_bound) return; // must be duplicated
        left = left < left_bound ? left_bound : left; // 左边已经接受过的数据就不要了
        right = right <= left_bound + _capacity ? right : left_bound + _capacity; // 右边越界的也不要
        o_right = right;
        string res = data.substr(left - o_left, right - left);

        size_t start = left; // the begin of the unused data-zone of variable data
        if (data.compare("") == 0 || res.compare("") == 0) goto end; // 如果data是空串也直接不要
        if (o_left >= left_bound + _capacity) goto end; // 越界的不要

        // 开始区间合并。需要扫描两次
        for (auto it = buffer.begin(); it != buffer.end(); it++) {
        if (left >= it->left && left <= it->right) {
        size_t r = right;
        size_t l = left;
        right = max(right, it->right);
        left = min(left, it->left);
        if (right == it->right) {
        start = o_right;
        } else {
        start = it->right;
        }
        if (r <= it->right) {
        res = it->data.substr(0, l - it->left) + data.substr(l - o_left) +
        it->data.substr(r - it->left, it->right - r);
        } else {
        res = it->data.substr(0, l - it->left) + data.substr(l - o_left);
        }
        // 删除这一步很关键。
        buffer.erase(it);
        break;
        }
        }

        // 第二次
        for (auto it = buffer.begin(); it != buffer.end(); it++) {
        if (it->left >= left && it->left <= right) {
        // 此时空间不够,先尝试下能不能写入一部分到_output中
        if (it->left > start && remaining < it->left - start) {
        if (left == left_bound) {
        size_t out_mem = _output.remaining_capacity();
        if (out_mem > 0) {
        // out的区域本身就很充足
        if (out_mem >= it->left - start) {
        // 写入ByteStream
        _output.write(res.substr(0, it->left - start));
        // 更新
        res = res.substr(it->left - start);
        left_bound = it->left - start + left;
        left = left_bound;
        remaining += it->left - start;
        // out剩下的空位会在最后写入
        goto ok;
        } else {
        // 光是out不够的话,那就先能腾多少空间腾多少
        _output.write(res.substr(0, out_mem));
        res = res.substr(out_mem);
        left_bound = out_mem + left;
        left = left_bound;
        remaining += out_mem;
        // 如果腾出空间加上原来空间足够,那就非常ok
        if (it->left > start && remaining >= it->left - start) {
        goto ok;
        }
        // 否则进入错误处理代码
        }
        }
        }
        tmp = {left, start + remaining, res.substr(0, start - left + remaining)};
        remaining = 0;
        if (eof == 1)
        is_eof = false;
        buffer.insert(tmp);
        goto end;
        }
        ok:
        remaining -= it->left - start;
        start = it->right;
        if (it->right > right){
        res += it->data.substr(right - it->left, it->right - right);
        right = it->right;
        }
        buffer.erase(it);
        }
        }

        if (start < o_right && remaining < o_right - start) {
        right = start + remaining;
        if (eof == 1)
        is_eof = false;
        }
        tmp = {left, right, res};
        if (left < right)
        buffer.insert(tmp);

        end:
        iterator = buffer.begin();
        // write into the ByteStream
        if (iterator != buffer.end() && iterator->left == left_bound) {
        size_t out_rem = _output.remaining_capacity();
        if (out_rem < iterator->data.length()) {
        _output.write(iterator->data.substr(0, out_rem));
        left_bound = iterator->left + out_rem;
        tmp = {left_bound, iterator->right, iterator->data.substr(out_rem)};
        buffer.erase(iterator);
        buffer.insert(tmp);
        } else {
        _output.write(iterator->data);
        left_bound = iterator->right;
        buffer.erase(iterator);
        }
        }

        // 满足两个条件才是真的eof
        if (is_eof && buffer.empty()){
        is_eof = false;
        _output.end_input();
        }
        }

        size_t StreamReassembler::unassembled_bytes() const {
        // 可以跟上面的write合起来的,但在此处我采取了最保守的做法。
        size_t res = 0;
        for (auto it = buffer.begin(); it != buffer.end(); it++) {
        res += it->right - it->left;
        }
        return res;
        }

        bool StreamReassembler::empty() const { return buffer.empty()&&is_eof; }
        + ]]> - - Java - - 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++;
        }
        + 其他的对实验未涉及的思考 + /2023/02/25/cs144$else/ + 其他的对实验未涉及的思考

        网络层实现

        在我们的协议栈实现中,我们负责了运输层的TCP协议、网络层的ARP协议以及数据链路层的ETH协议的编写,剩下的网络层的IP协议则由官方给定。接下来我们就来探究下网络层的实现。

        +

        总体架构

        +

        You’ve done this already.

        +

        In Lab 4, we gave:

        +
          +
        1. an object that represents an Internet datagram and knows how to parse and serialize itself (tcp_helpers/ipv4_datagram.{hh,cc}) 表示了Internet datagram的数据结构,它可以自己序列化。
        2. +
        3. the logic to encapsulate(封装) TCP segments in IP (now found in tcp_helpers/tcp_over_ip.cc).
        4. +
        +

        The CS144TCPSocket uses these tools to connect your TCPConnection to a TUN device.

        -

        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

        这个确实不难,就是这个地方有点坑:

        +

        也即,IP协议主要由两个文件实现,一个是IP数据报抽象为的类ipv4_datagram.{hh,cc},另一个是将TCP报文封装为IP报文的类tcp_helpers/tcp_over_ip.cc;除此之外,IP协议还负责与下次协议连接,在实验0-4中它通过CS144TCPSocket与TUN连接,在实验5-6则与TAN连接。

        +

        连接部分暂且先放到下一部分讲,下面来看看IP协议的具体实现。

        +

        具体实现

        ipv4_datagram.hh && ipv4_header.hh

        ipv4_datagram没什么好说的,跟TCPSegment的结构一模一样。ipv4_header也没什么好说的,就纯纯是IP数据报的报头、

        +

        tcp_over_ip

        头文件

        它的头文件很简单,只包含一个类的定义:

        +
        // A converter from TCP segments to serialized IPv4 datagrams
        class TCPOverIPv4Adapter : public FdAdapterBase {
        public:
        std::optional<TCPSegment> unwrap_tcp_in_ip(const InternetDatagram &ip_dgram);

        InternetDatagram wrap_tcp_in_ip(TCPSegment &seg);
        };
        #endif // SPONGE_LIBSPONGE_TCP_OVER_IP_HH
        + +
        具体实现

        可以看到,相比于TCP和ETH/ARP协议,IP协议的实现可以说是非常简单。它作为一个中间层,只需要把上面给的东西包装下再传到下面,或者把下面给的东西解包下再传给上面,无需其他复杂的算法和数据结构(比如TCP的reliable transmission和ETH/ARP的地址自学习),也无需跟外界打交道。

        +

        除了打包解包外,它只需确保一件事,那就是一台主机只能同时拥有一个TCP连接。这样一来也能简化其实现:填写IP协议头时,它就只需从自己保存的config中取参数就行。

        +
        // 用来拆IP数据包为一个TCP数据包
        //! If this succeeds, it then checks that the received segment is related to the
        //! current connection. When a TCP connection has been established, this means
        // 如果TCP连接已建立,则会检查src和dst端口号的正确性
        //! checking that the source and destination ports in the TCP header are correct.
        //!
        //! If the TCP connection is listening 如果处于listening状态,并且参数为SYN报文
        //! and the TCP segment read from the wire includes a SYN, this function clears the
        // 就需要解除listening的flag,记录下src和dst的地址和端口号
        //! `_listen` flag and records the source and destination addresses and port numbers
        // 目的是为了 filter future reads
        // 这说明我们的sponge实现是单线程的,也就是一台主机只能同时建立一个TCP连接
        // 并且在此时会忽略其他主机发过来的数据包
        //! from the TCP header; it uses this information to filter future reads.

        // returns a std::optional<TCPSegment> that is empty if the segment was invalid or unrelated
        optional<TCPSegment> TCPOverIPv4Adapter::unwrap_tcp_in_ip(const InternetDatagram &ip_dgram) {
        // is the IPv4 datagram for us?
        // Note: it's valid to bind to address "0" (INADDR_ANY) and reply from actual address contacted
        if (not listening() and (ip_dgram.header().dst != config().source.ipv4_numeric())) {
        return {};
        }

        // is the IPv4 datagram from our peer?
        // 过滤非peer发来的其他数据包
        if (not listening() and (ip_dgram.header().src != config().destination.ipv4_numeric())) {
        return {};
        }

        // does the IPv4 datagram claim that its payload is a TCP segment?
        // 我们只需解包TCP数据报
        if (ip_dgram.header().proto != IPv4Header::PROTO_TCP) {
        return {};
        }

        // is the payload a valid TCP segment?
        TCPSegment tcp_seg;
        if (ParseResult::NoError != tcp_seg.parse(ip_dgram.payload(), ip_dgram.header().pseudo_cksum())) {
        return {};
        }

        // is the TCP segment for us?
        if (tcp_seg.header().dport != config().source.port()) {
        return {};
        }

        // should we target this source addr/port (and use its destination addr as our source) in reply?
        if (listening()) {
        // records the source and destination addresses and port numbers
        if (tcp_seg.header().syn and not tcp_seg.header().rst) {
        config_mutable().source = {inet_ntoa({htobe32(ip_dgram.header().dst)}), config().source.port()};
        config_mutable().destination = {inet_ntoa({htobe32(ip_dgram.header().src)}), tcp_seg.header().sport};
        set_listening(false);
        } else {
        return {};
        }
        }

        // is the TCP segment from our peer?
        if (tcp_seg.header().sport != config().destination.port()) {
        return {};
        }

        return tcp_seg;
        }

        //! Takes a TCP segment, sets port numbers as necessary, and wraps it in an IPv4 datagram
        //! \param[in] seg is the TCP segment to convert
        InternetDatagram TCPOverIPv4Adapter::wrap_tcp_in_ip(TCPSegment &seg) {
        // set the port numbers in the TCP segment
        seg.header().sport = config().source.port();
        seg.header().dport = config().destination.port();

        // create an Internet Datagram and set its addresses and length
        InternetDatagram ip_dgram;
        ip_dgram.header().src = config().source.ipv4_numeric();
        ip_dgram.header().dst = config().destination.ipv4_numeric();
        // uint8_t hlen = LENGTH / 4; //!< header length
        // uint8_t doff = LENGTH / 4; //!< data offset
        ip_dgram.header().len = ip_dgram.header().hlen * 4 + seg.header().doff * 4 + seg.payload().size();

        // set payload, calculating TCP checksum using information from IP header
        ip_dgram.payload() = seg.serialize(ip_dgram.header().pseudo_cksum());

        return ip_dgram;
        }
        + +

        Socket实现

        最top的话可以分为CS144TCPSocketFullStackSocket

        +

        继承关系如下图:

        +

        Inheritance graph

        +

        其中,TCPSocket是完完全全的包装类,它的所有协议栈都是在内核态中实现(也就是跟我们之后写的没半毛钱关系),它的存在意义应该是用在lab0来写webget的测试。而CS144TCPSocket就是我们在lab0-4用的了,它的数据链路层由内核实现,网络层和运输层由用户实现。FullStackSocket就是加上了我们在lab5做的用户态数据链路层。

        +

        最主要的部分是TCPSpongeSocket的实现,其他就是一些包装类没什么好说的。

        +

        FileDescriptor

        将socket看作是fd,将网络看作是IO,这一抽象简直是太伟大了,牛逼到爆。

        +

        头文件

        //! A reference-counted handle to a file descriptor
        class FileDescriptor {
        //! \brief A handle on a kernel file descriptor.
        //! \details FileDescriptor objects contain a std::shared_ptr to a FDWrapper.
        class FDWrapper {
        public:
        int _fd; // file descriptor number returned by the kernel
        bool _eof = false; // fd是否eof
        bool _closed = false; // fd是否close
        // fd被读写的次数
        unsigned _read_count = 0;
        unsigned _write_count = 0;

        //! Construct from a file descriptor number returned by the kernel
        explicit FDWrapper(const int fd);
        //! Closes the file descriptor upon destruction
        ~FDWrapper();
        //! Calls [close(2)](\ref man2::close) on FDWrapper::_fd
        void close();
        //! An FDWrapper cannot be copied or moved
        FDWrapper(const FDWrapper &other) = delete;
        FDWrapper &operator=(const FDWrapper &other) = delete;
        FDWrapper(FDWrapper &&other) = delete;
        FDWrapper &operator=(FDWrapper &&other) = delete;
        };

        //! A reference-counted handle to a shared FDWrapper
        std::shared_ptr<FDWrapper> _internal_fd;

        // private constructor used to duplicate the FileDescriptor (increase the reference count) 这个构造函数会增加其参数传进来的那个fd的引用,也许相当于dump
        explicit FileDescriptor(std::shared_ptr<FDWrapper> other_shared_ptr);

        protected:
        void register_read() { ++_internal_fd->_read_count; } //!< increment read count
        void register_write() { ++_internal_fd->_write_count; } //!< increment write count

        public:
        //! Construct from a file descriptor number returned by the kernel
        explicit FileDescriptor(const int fd);

        //! Free the std::shared_ptr; the FDWrapper destructor calls close() when the refcount goes to zero.
        ~FileDescriptor() = default;

        /* 读写 */
        std::string read(const size_t limit = std::numeric_limits<size_t>::max());
        void read(std::string &str, const size_t limit = std::numeric_limits<size_t>::max());
        // possibly blocking until all is written
        size_t write(const char *str, const bool write_all = true) { return write(BufferViewList(str), write_all); }
        size_t write(const std::string &str, const bool write_all = true) { return write(BufferViewList(str), write_all); }
        size_t write(BufferViewList buffer, const bool write_all = true);

        //! Close the underlying file descriptor
        void close() { _internal_fd->close(); }

        //! Copy a FileDescriptor explicitly, increasing the FDWrapper refcount
        FileDescriptor duplicate() const;

        //! Set blocking(true) or non-blocking(false)
        void set_blocking(const bool blocking_state);
        // ...
        + +

        具体实现

        差不多就是全程调用系统调用没什么好说的,记录下几个有意思的点

        +
        包装系统调用

        可以看下其调用系统调用的方式,看起来很有意思:

        +
        void FileDescriptor::set_blocking(const bool blocking_state) {
        int flags = SystemCall("fcntl", fcntl(fd_num(), F_GETFL));
        if (blocking_state) {
        flags ^= (flags & O_NONBLOCK);
        } else {
        flags |= O_NONBLOCK;
        }

        SystemCall("fcntl", fcntl(fd_num(), F_SETFL, flags));
        }
        + +

        比如说这里设置文件读写是否阻塞就是通过系统调用实现的。

        +

        在写os实验时,你应该就能很深刻感受到,很多时候调用完一个系统调用后,对它的返回结果进行合法性判断以及错误处理还是有点烦的(举例来说,如if(kalloc() == 0)或者if(mappages() == 0),出错后杀死进程等等等)。在那会我们还可以直接就这么冗余地干了,但是这里不行,一是我们要用面向对象的思想,二是我们的重点事实上并不是操作系统而是网络,因而最好还是这么封装下以减少冗余代码。

        +

        而它除了会调用系统调用外,还使用了一个包装性的方法SystemCall来保障调用的安全性和合理性。看看SystemCall的具体实现方式,确实就是包了层安全检查。

        +
        int SystemCall(const char *attempt, const int return_value, const int errno_mask) {
        if (return_value >= 0 || errno == errno_mask) {
        return return_value;
        }

        throw unix_error(attempt);
        }

        // os内核看不懂c++,所以要注意转换为c-style的字符串
        int SystemCall(const string &attempt, const int return_value, const int errno_mask) {
        return SystemCall(attempt.c_str(), return_value, errno_mask);
        }
        + +

        Socket

        没什么好说的,只是操作系统socket接口的包装类。

        +

        头文件

        // Base class for network sockets (TCP, UDP, etc.)
        // Socket is generally used via a subclass. See TCPSocket and UDPSocket for usage examples.
        class Socket : public FileDescriptor {
        private:
        //! Get the local or peer address the socket is connected to
        Address get_address(const std::string &name_of_function,
        const std::function<int(int, sockaddr *, socklen_t *)> &function) const;

        protected:
        Socket(const int domain, const int type);
        Socket(FileDescriptor &&fd, const int domain, const int type);

        template <typename option_type>
        void setsockopt(const int level, const int option, const option_type &option_value);
        public:
        // Bind a socket to a local address, usually for listen/accept
        void bind(const Address &address);
        // Connect a socket to a peer address
        void connect(const Address &address);
        // Shut down a socket
        void shutdown(const int how);
        //! Get local address of socket
        Address local_address() const;
        //! Get peer address of socket
        Address peer_address() const;
        //! Allow local address to be reused sooner
        void set_reuseaddr();
        };

        //! A wrapper around [UDP sockets](\ref man7::udp)
        class UDPSocket : public Socket {
        protected:
        //! \brief Construct from FileDescriptor (used by TCPOverUDPSocketAdapter)
        //! \param[in] fd is the FileDescriptor from which to construct
        explicit UDPSocket(FileDescriptor &&fd) : Socket(std::move(fd), AF_INET, SOCK_DGRAM) {}

        public:
        //! Default: construct an unbound, unconnected UDP socket
        UDPSocket() : Socket(AF_INET, SOCK_DGRAM) {}

        // carries received data and information about the sender
        struct received_datagram {
        Address source_address; //!< Address from which this datagram was received
        std::string payload; //!< UDP datagram payload
        };
        //! Receive a datagram and the Address of its sender
        received_datagram recv(const size_t mtu = 65536);
        //! Receive a datagram and the Address of its sender (caller can allocate storage)
        void recv(received_datagram &datagram, const size_t mtu = 65536);

        //! Send a datagram to specified Address
        void sendto(const Address &destination, const BufferViewList &payload);
        //! Send datagram to the socket's connected address (must call connect() first)
        void send(const BufferViewList &payload);
        };

        //! A wrapper around [TCP sockets](\ref man7::tcp)
        class TCPSocket : public Socket {
        private:
        // Construct from FileDescriptor (used by accept())
        // fd is the FileDescriptor from which to construct
        explicit TCPSocket(FileDescriptor &&fd) : Socket(std::move(fd), AF_INET, SOCK_STREAM) {}
        public:
        //! Default: construct an unbound, unconnected TCP socket
        TCPSocket() : Socket(AF_INET, SOCK_STREAM) {}
        //! Mark a socket as listening for incoming connections
        void listen(const int backlog = 16);
        //! Accept a new incoming connection
        TCPSocket accept();
        };

        //! A wrapper around [Unix-domain stream sockets](\ref man7::unix)
        class LocalStreamSocket : public Socket {
        public:
        // ...构造器
        };
        #endif // SPONGE_LIBSPONGE_SOCKET_HH
        + +

        具体实现

        构造器的参数

        参考文章

        +

        也是系统调用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机制来进行事件监听的。

        -

        Please note that in HTTP, each line must be ended with “\r\n” (it’s not sufficient to use just “\n” or endl).

        +

        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:无效的请求,打不开指定的文件描述符
        -

        导致我跟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();
        }
        +

        我们在前面的eventloop的rule初始化中:

        +
        _eventloop.add_rule(_datagram_adapter,
        Direction::In,
        [&] { ... });
        -

        还有一点值得注意的是,当我这样时:

        -
        TCPSocket sock;
        sock.set_blocking(false);
        sock.connect(Address(host,"http"));
        +

        这个的意思是针对_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.
        };
        -

        会报错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 ++;
        }
        +

        可见,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);
        }
        -

        结果就是测试用例Timeout。我找了很久都不知道错在了哪,最后求助了场外观众【罪过……这次实验太不独立了】,学着他把length改成了这样:

        -
        int length = min(data.length(),capacity-buffer.size());
        +
        _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;
        }
        }
        -

        发现成了。

        -

        我去看了看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;
        +
        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);
        }
        -

        具体实现

        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(); }
        +

        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的结合体,订阅事件+循环等待。由于跟前面类似,在此就不放代码了。

        +

        其他

        其他都太复杂了,感觉我水平一般还不大能理解,也懒得看了【草】总之先咕咕咕

        ]]>
        @@ -6304,229 +6487,94 @@ url访问填写http://localhost/webdemo4_war/*.do

        本质上是“the distance between the first unassembled index and the first unacceptable index”

      -

      也即,ackno为拥塞窗口的左端点,ackno+window_size为拥塞窗口的右端点

      - -
    -

    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;
    }
    - -

    SYN很直观,没什么好说的。

    -

    FIN比较烧。之所以不是这么写:

    -
    if(header.fin){
    ack += 1;
    }
    - -

    也即一发现FIN报文到了就++,是因为可能会发生这种情况:

    -

    image-20230227224055002

    -

    也即FIN报文虽然到了,但是中间有一段数据还没到,ack应该等于中间那段数据的开头,你这时候想要跳过FIN而把ack+1那肯定是不对的。

    -

    也因而,我们需要记录fin是否有过,并且仅当:

    -
    bool StreamReassembler::empty() const { return buffer.empty()&&is_eof; }
    - -

    成立时,才能表示数据传输真正结束,让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();
    }
    -]]> - - - Lab1 StreamReassembler - /2023/02/25/cs144$lab1/ - Lab1 StreamReassembler
    -

    TCP managed to produce a pair of reliable in-order byte streams (one from you to the server, and one in the opposite direction), even though the underlying network only delivers “best-effort” datagrams.

    -

    You also implemented the byte-stream abstraction yourself, in memory within one computer.

    -

    Over the next four weeks, you’ll implement TCP, to provide the byte-stream abstraction between a pair of computers separated by an unreliable datagram network.

    -
    -

    我们的任务是实现一个StreamReassembler。它的具体功能相信看下数据传输路径就很明了了:

    -
    -

    receiver的数据传输路径:network → StreamReassembler整流 →(write)ByteStream(read)→ app

    -
    -

    感想

    先放个通关截图在这。

    -

    image-20230225192145829

    -

    这个实验我前前后后总共做了大概有9h+……写我下面放上来的屎山代码可能大概用了5h+。我总共使用了140+行代码实现我的核心函数push_substring

    -

    整个过程,包括思路和代码都十分复杂,但最后的表现相比于别人好像也没好到哪去,让我不禁怀疑自己是不是想错了……以及,这样的复杂性也给我带来很多担忧,担心会不会在以后的实验因为这个的bug而寄,毕竟我在写笔记的同时都已经找到了不止一个bug了()希望人没事。

    -

    总而言之,先把我的思路和一步步的代码拆解放上来吧。

    -
    -

    后记:

    -

    不得不说,这个东西太健壮了,给后面的TCPReceiver省去了好多功夫……

    -

    比如说,TCPReceiver无需考虑ack怎么算,因为这里就帮你算好了;TCPReceiver无需考虑数据包重叠或者重复,因为这里已经考虑到这个情况了;TCPReceiver无需担忧FIN是否会因为容量满丢弃一部分数据而未达到真正的FIN,只需调用其相关接口判断就行。

    -

    它虽然帮助了TCPReceiver那么多,但很神奇的是,它们的耦合性并不高。你把StreamReassembler单独拆出来看,左看右看,它都确实仅仅只是一个健壮的区间合并算法

    -

    这得益于实验设计的精良,得益于设计TCPReceiver时的野心。这些边界情况都这么麻烦,而且都只与区间合并有关,那么我们为什么不直接把它抽象进区间合并进行处理呢?这种想法极富胆识,事实证明最后也确实能实现。这种设计理念让我受益很深。

    -
    -

    为什么我的思路那么复杂

    看了感恩的代码,我发现我俩的大题思路其实差不多是一模一样的,都是先进行两轮的区间合并,然后再处理但为啥我看起来就那么复杂呢?

    -

    一是题意理解问题。

    -

    我发现他对_capacity的理解跟我的理解不一样emmm……

    -

    额好像怪我没认真看指导书的图。

    -

    image-20230225232723083-1677339210527-2

    -

    我理解的capacity:绿色部分和红色部分的净含量

    -

    似乎是真正的capacity:绿色部分+红色部分+空部分??也就是说capacity只是一个跟index差不多的索引下标??…………

    -
     // out of bound
    if (start >= hope_to_rec + _capacity - _output.buffer_size()) {
    return;
    }
    - -

    也就是说我能过测试是因为偶然吗??

    -

    我其实感觉正确理解的capacity意义好怪啊,这怎么就能节省内存了呢?我觉得我理解的反而比较有道理(倔强)

    -

    笑一笑算了家人们。总之先这么写吧,以后的实验寄掉了再回来改。

    -
    -

    UPDATE: 确实寄掉了,并且已经改过来了,也不复杂,只需要添加对right边界的处理就行。【指去掉超出start+capacity的部分。】

    -
    -

    二是代码规范问题。

    -

    首先他代码规范性强,看起来非常舒服。其次他会用类似upper_bound()这样的函数(反观我压根没想起来),这样就显得比我的循环简洁了很多很多。

    -

    三是设计问题。

    -

    他用的是map我用的是set。确实是map比较合理,它既有find功能也兼具了有序的特性。

    -

    我的思路

    我们要做的,是将零散的数据包拼接成完整的字节流,并且将整流过的数据送入ByteStream中,这通过核心函数push_substring实现。我们可以先来看看push_substring的定义:

    -
    void StreamReassembler::push_substring(const string &data, const size_t index, const bool eof) ;
    - -

    data为数据包,indexdata中第一个字符在整个字节流的下标,eof用来标识这是字节流的最后一个数据包。

    -
    -

    详细说明:

    -

    比方说有字节流“abcdefg”,则合法的参数对有如:{“abc”,0,0},{“cdef”,2,0},{“g”,6,1}

    -
    -

    通俗来说,我们这个函数的功能就是,把一堆起始下标为indexdata(无序、可能重叠)拼接为一个完整的字节流。

    -

    听起来有没有觉得很耳熟?是的,我认为这正是“区间合并”问题。我接下来便通过区间合并的思想,对问题进行如下数据结构以及算法的设计。

    -

    数据结构

    区间

    由于是区间合并问题,所以就先需要定义区间。

    -
    struct node {
    size_t left; // 当前data位于总体的左index(闭)
    size_t right; // 右index(开)
    string data;
    bool operator<(const node &b) const { return left < b.left; }
    };
    - -

    集合

    我们需要维护一个左端点升序的区间集合,故使用内部红黑树实现的有序集合set。

    -
    set<node> buffer;   // 存储结构
    - -

    算法

    我们要做的,是对数据包进行整流,并且把整流过的部分输送到ByteStream中。由于存储结构存在_capacity的上限,因而,我们需要尽可能早地把存储结构中已经整流好的数据送入ByteStream中。

    -

    那么,如何定义“已经整流好的数据”呢?它需要满足跟“之前已经整流好了的数据”的有序性,也即,比方说[0,1000]已经整流完毕送入app,那么下一个送入app的数据一定满足index=1001

    -

    因而,我们可以维护一个变量left_bound,表示下一个将被app接受的数据的index(如上例的1001)。为了达到“尽早”目的,我们需要在每次push_substring执行完区间合并之后,检查buffer的第一个区间的左端点是否与left_bound相等,是的话则将第一个区间写入ByteStream,不是的话就什么也不做。

    -

    因而,在push_substring中,对于一个新来的数据包,我们大致需要进行以下几步:

    -
      -
    1. 将参数所给的区间( [index, index+data.length()) )并入区间集合buffer
    2. -
    3. 查看是否需要ByteStream
    4. -
    -

    区间合并

    问题定义

    问题可抽象为:

    -
    -

    给定一个有序区间集合buffer,以及一个小区间node,你需要把node塞进buffer里。

    -

    Example: buffer = {[1,3),[5,7)} , node = [6,8) 输出:buffer = {[1,3), [5,8)}

    -
    -
    算法思路

    判断区间重叠统一只检查左端点。注意,两次重叠的判断条件不一样,是因为相对性发生了改变。第一次相当于node的左端点在buffer[i]中,第二次相当于buffer[i]的左端点在node中。

    -
      -
    1. buffer进行第一轮扫描

      -

      如果node与buffer[i]产生重叠((left >= it->left && left <= it->right)),那么更新node为node∪buffer[i],并且将buffer[i]从buffer中删去。

      -

      在第一次找到重叠的区间,就应该break退出第一轮循环。

      -
    2. -
    3. buffer进行第二轮扫描

      -

      如果node与buffer[i]产生重叠( (it->left >= left && it->left <= right)),那么更新node为node∪buffer[i],并且将buffer[i]从buffer中删去。

      -
    4. -
    -

    我们在合并区间时,不仅需要对struct node的左端点left和右端点right进行更新,还需要对其数据域data也进行合并拼接。我们维护变量res作为维护的目标区间的数据域。对于res,我们应该进行如下操作:

    -
      -
    1. 初始化为data

      -
    2. -
    3. 除去[left, left_bound)这一区间内的数据

      -

      这部分数据我们已经整流过并且写入ByteStream

      -
    4. -
    5. 在两轮合并中对其进行正确拼接

      -
    6. -
    -
    图解

    image-20230225200605175

    -
    代码
    size_t left = index, right = index + data.length();  // 初始化左右区间
    size_t o_left = left, o_right = right; // keep the original value
    node tmp = {0, 0, ""};

    if (right < left_bound) return; // must be duplicated
    left = left < left_bound ? left_bound : left; // 左边已经接受过的数据就不要了
    string res = data.substr(left - o_left, right - left);// 掐头

    /* 开始区间合并。需要扫描两次 */
    // 第一次
    for (auto it = buffer.begin(); it != buffer.end(); it++) {
    if (left >= it->left && left <= it->right) { // 区间重叠
    size_t r = right,l = left;
    // 更新左右端点
    right = max(right, it->right);
    left = min(left, it->left);
    if (r <= it->right) // 如果目标区间被包裹在it内
    // res需要更新为it头+data掐头后的全长+it尾,也即将it中间重叠部分用data替换
    res = it->data.substr(0, l - it->left) + data.substr(l - o_left) +
    it->data.substr(r - it->left, it->right - r);
    else
    res = it->data.substr(0, l - it->left) + data.substr(l - o_left);
    // 删除原来的结点
    buffer.erase(it);
    break;
    }
    }

    // 第二次
    for (auto it = buffer.begin(); it != buffer.end(); it++) {
    if (it->left >= left && it->left <= right) {
    if (it->right <= right);// it这个区间被包含在目标区间内,则什么也不做
    else {
    // 需要加上it的尾
    res += it->data.substr(right - it->left, it->right - right);
    // 更新右端点
    right = it->right;
    }
    // 删除
    buffer.erase(it);
    }
    }
    // 将维护区间插入区间集合
    tmp = {left, right, res};
    buffer.insert(tmp);
    - -

    写入ByteStream

    思路

    我们需要检查buffer内的第一个区间,如果其左端点与left_bound相等,则把第一个区间填入ByteStream,然后更新left_bound,从buffer中删去该区间;如果不相等(只可能是left > left_bound)则什么也不做。

    -

    在把区间数据填入ByteStream的过程中,可能造成ByteStream满。因而我们就只能填入第一个区间内的一部分数据,更新left_bound,将第一个区间的剩余数据继续存在buffer中。

    -
    代码
    auto iterator = buffer.begin();  
    iterator = buffer.begin();
    // write into the ByteStream
    if (iterator != buffer.end() && iterator->left == left_bound) {
    // 防止_output的容量超过
    size_t out_rem = _output.remaining_capacity();
    if (out_rem < iterator->data.length()) { // ByteStream剩余容量小于第一个区间长度
    _output.write(iterator->data.substr(0, out_rem));// 写入尽量多数据
    left_bound = iterator->left + out_rem;// 更新左边界
    // 由于iterator只读,因而我们不能直接修改其左端点和data域
    tmp = {left_bound, iterator->right, iterator->data.substr(out_rem)};
    buffer.erase(iterator);
    buffer.insert(tmp);
    } else {
    _output.write(iterator->data);
    left_bound = iterator->right;
    buffer.erase(iterator);
    }
    }
    - -

    buffer的最大容量_capacity

    背景

    维护“存储结构的容量不超过capacity”这个不变性条件可以说是这个实验最恶心最难的地方……也正是它,让我的代码写成了一坨shit山()

    -

    为什么说它最难最恶心呢?其实它本来也许不算难,但在这个思路下想要保持这个不变性条件,就显得非常地困难。

    -

    一开始没过脑子的时候,我觉得这样就行:

    -
    void StreamReassembler::push_substring(const string &data, const size_t index, const bool eof) {
    if(data.length() + unassemble_bytes() > capacity) return;
    }
    - -

    但是这样很明显有两个问题。

    -

    一是就算你超过了,你也不能直接丢弃掉data,得把没超过的部分填满。

    -

    二是,data.length() + unassemble_bytes()有时,甚至是很多时候,都不会是将data并入buffer之后buffer的容量。因为data和buffer很大概率会存在重叠区间。

    -

    那么,你能不能在区间合并完之后,再进行该不变性条件的判断,并且将没超过的部分填满,超过的部分丢弃呢?

    -

    答案是,也不能。因为经过两轮合并,你的data和buffer里原有的数据早已你中有我我中有你了,你无法在最后将它们分开,找出data超过capacity的数据并且丢弃它。

    -

    因而,头尾都不行的话,唯一的答案就是,我们只能在两轮区间合并中途,去时刻追踪当前容量是否超过capacity

    -

    这听起来就令人十分地头大。但事实证明,并不是无法实现的,坚持下去,就算是shit山也能跑起来()下面便是我的实现思路。

    -
    思路

    维护一个变量remaining,表示当前还有多少容量。维护start,表示未判断是否可以写入buffer的数据起点。我们要做的事:

    +

    也即,ackno为拥塞窗口的左端点,ackno+window_size为拥塞窗口的右端点

    + +
+

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. 初始化remaining为capacity - 当前容量,start为掐头后的left
  2. -
  3. 在第一轮循环中更新start
  4. -
  5. 在第二轮循环中通过start和remaining来判断是否能够写入buffer。尽可能多地写入,把写入不了的部分丢弃。
  6. +
  7. 如果收到SYN报文,则对一些参数进行初始化,并且标记数据传输开始信号syn为true
  8. +
  9. 如果syn为true,则计算index后传入整流器
  10. +
  11. 判断是否需要加上FIN报文的比特位
-
例子

下面举个例子来说明整个流程。

-
initial:  
buffer = { [1,3], [5,8],[10,12],[13,14] } , capacity = 12, remaining = 12-9=3, data = [2,11], start = 2
- -

start为2,是因为data的从2开始的这段数据还不知道能不能被成功存入buffer中。

-

第一次合并后,buffer = { [5,8],[10,12],[13,14] } ,data=[1,11],start = 3.

-

start为3,是因为[2,11]与[1,3]合并,由于[2,3]这段本来就在buffer中,因而可以不用占用remaining,但从3开始这段数据还不知道能不能成功存入buffer中。

-

在第二轮合并中,首先扫描到[5,8]。由于[start,it->left]也即[3,5]这段数据长度为2<remaining=3,故而这段数据可以存入buffer,remaining更新为3-2=1,start更新为it->right=8.

-

/*

-

注意,此处无需再对[9,10]进行同样的操作。对于这种情况:

-
buffer = { [1,3], [5,8] } , capacity = 12, remaining = 12-9=3, data = [2,11], start = 2
- -

在循环结束的这里已经处理:

-
if (remaining < right - start) {
right = remaining + start;
if (eof == 1)
is_eof = false;
}
- -

*/

-

然后扫描到[10,12]。由于[start,it->left]也即[8,10]这段数据长度为2>remaining=1,故而这段数据只能把[8,9]这部分存入buffer。因而,我们把到此为止的[1,9]结点存入buffer,剩下的[10,11]部分直接丢弃,也即直接跳入到最后的写入ByteStream部分。

-
代码
void StreamReassembler::push_substring(const string &data, const size_t index, const bool eof) {
size_t remaining = _capacity - unassembled_bytes(); // how much room left?

size_t left = index, right = index + data.length(); // 初始化左右区间
size_t o_left = left, o_right = right; // keep the original value

size_t start = left; // the begin of the unused data-zone of variable data
// if(remaining == 0) goto end; // buffer满

// 开始区间合并。需要扫描两次
for (auto it = buffer.begin(); it != buffer.end(); it++) {
if (left >= it->left && left <= it->right) {
right = max(right, it->right);
left = min(left, it->left);
// 说明目标区间完全被包裹,也即目标区间一定可以塞进buffer中
if (right == it->right) start = o_right;
// 说明仅仅部分重叠,去重部分从it->right开始
else start = it->right;
// ...
break;
}
}

// 第二次
for (auto it = buffer.begin(); it != buffer.end(); it++) {
if (it->left >= left && it->left <= right) {
// 比remaining满
// 第一个条件是为了防止unsigned溢出
if (it->left > start && remaining < it->left - start) {
// 截取能塞得下的部分
tmp = {left, start + remaining, res.substr(0, start - left + remaining)};
remaining = 0;
// 此时塞进去是肯定不重叠的,因为tmp.right < it->left
buffer.insert(tmp);
// 剩下的直接丢弃
goto end;
}
// 塞得下
remaining -= it->left - start;
start = it->right;
// ...
}
}

// 边界处理
if (remaining < right - start) {
// 扔掉塞不下的部分
right = remaining + start;
}
tmp = {left, right, res};
buffer.insert(tmp);

end:
iterator = buffer.begin();
// write into the ByteStream
// ...
}
- -

capacity还没结束

背景

你以为做到上面那个小标题那样就万无一失了吗?答案是,并不!

-

我们还有哪里想得不周全呢?考虑这样一个案例,ByteStream未满,但是在更新remaining时发现buffer已满塞不下了。这时候,我们上面的做法是直接扔掉塞不下的部分。但其实,我们还可以查看buffer的一部分数据是否能够再塞进ByteStream,如果能的话,就又能省下一笔空间了!

-

所以,我们在发现remaining不够时,应该首先检查能不能塞一部分buffer的数据进入ByteStream中用来腾出空间。

-
代码
// 第二次
for (auto it = buffer.begin(); it != buffer.end(); it++) {
if (it->left >= left && it->left <= right) {
// 比remaining满
// 第一个条件是为了防止unsigned溢出
if (it->left > start && remaining < it->left - start) {
// 先看看能不能塞进ByteStream腾空间。需要满足两个条件
// buffer的第一个区间正好是left_bound
if (left == left_bound) {
size_t out_mem = _output.remaining_capacity();
// ByteStream有位置
if (out_mem > 0) {
// 腾出的空间很充足,完全可以不改变remaining
if (out_mem >= it->left - start) {
// 写入ByteStream
_output.write(res.substr(0, it->left - start));
// 更新
res = res.substr(it->left - start);
left_bound = it->left - start + left;
left = left_bound;
// 加上腾出的空间【在ok标签处减掉】
remaining += it->left-start;
goto ok;
} else {
// 空间不足以完全不改变remaining
_output.write(res.substr(0, out_mem));
res = res.substr(out_mem);
left_bound = out_mem + left;
left = left_bound;
// 加上腾出的空间
remaining += out_mem;
// 如果两个加起来就行,则ok
if(it->left>start && remaining>=it->left - start){
goto ok;
}
// 否则remaining依然不充足
}
}
}
// 截取能塞得下的部分
tmp = {left, start + remaining, res.substr(0, start - left + remaining)};
remaining = 0;
// 此时塞进去是肯定不重叠的,因为tmp.right < it->left
buffer.insert(tmp);
// 剩下的直接丢弃
goto end;
}
ok:
// 塞得下
remaining -= it->left - start;
start = it->right;
// ...
}
}
- -
小细节

因而,在一开始发现remaining满的时候,不能直接goto end。

-
// if(remaining == 0)	goto end; // buffer满
+
一些细节
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;
}
-

因为还得看看ByteStream能不能腾空间。

-

eof

对于eof的处理也是需要注意的一个小细节。

-

我们不能这么写:

-
if (eof)
_output.end_input();
+

SYN很直观,没什么好说的。

+

FIN比较烧。之所以不是这么写:

+
if(header.fin){
ack += 1;
}
-

原因有二,一是最后一个数据包有可能是部分丢失,二是整流可能还未结束。

-

所以我们应该在成员变量中维护is_eof,记录是否收到过最后一个数据包,并且在最后一个数据包部分丢失的时候置它为false。当且仅当is_eof == true且buffer非空时,才能说明输入结束。

-

相关代码:

-
void StreamReassembler::push_substring(const string &data, const size_t index, const bool eof) {
if (eof == 1)
is_eof = true;
// ...
// 第二次
for (auto it = buffer.begin(); it != buffer.end(); it++) {
if (it->left >= left && it->left <= right) {
// 此时空间不够,先尝试下能不能写入一部分到_output中
if (it->left > start && remaining < it->left - start) {
// ...
tmp = {left, start + remaining, res.substr(0, start - left + remaining)};
remaining = 0;
if (eof == 1)
is_eof = false;
buffer.insert(tmp);
goto end;
}
ok:
// ...
}

if (remaining < right - start) {
right = remaining + start;
if (eof == 1)
is_eof = false;
}
// ...
end:
// ...
// 满足两个条件才是真的eof
if (is_eof && buffer.empty()){
is_eof = false;
_output.end_input();
}
}
+

也即一发现FIN报文到了就++,是因为可能会发生这种情况:

+

image-20230227224055002

+

也即FIN报文虽然到了,但是中间有一段数据还没到,ack应该等于中间那段数据的开头,你这时候想要跳过FIN而把ack+1那肯定是不对的。

+

也因而,我们需要记录fin是否有过,并且仅当:

+
bool StreamReassembler::empty() const { return buffer.empty()&&is_eof; }
-

代码

头文件声明

class StreamReassembler {
private:
// Your code here -- add private members as necessary.
struct node {
size_t left; // 当前data位于总体的左index(闭)
size_t right; // 右index(开)
string data;
bool operator<(const node &b) const { return left < b.left; }
};
bool is_eof; // 文件的末尾是否接收成功
size_t left_bound; // 当前已经成功接收到left_bound之前的数据
set<node> buffer; // 存储结构

ByteStream _output; //!< The reassembled in-order byte stream
size_t _capacity; //!< The maximum number of bytes

public:
// ...

// used by the TCPReceiver
void set_is_eof() { is_eof = false; }
}
+

成立时,才能表示数据传输真正结束,让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;
}
-

具体实现

#include "stream_reassembler.hh"
#include <iostream>
#include <map>

template <typename... Targs>
void DUMMY_CODE(Targs &&... /* unused */) {}

using namespace std;

StreamReassembler::StreamReassembler(const size_t capacity)
: is_eof(false), left_bound(0), buffer(), _output(capacity), _capacity(capacity) {}

void StreamReassembler::push_substring(const string &data, const size_t index, const bool eof) {
if (eof == 1)
is_eof = true;

size_t unass = unassembled_bytes();
size_t remaining = _capacity > unass ? _capacity - unass : 0; // how much room left?

size_t left = index, right = index + data.length(); // 初始化左右区间
size_t o_left = left, o_right = right; // keep the original value
auto iterator = buffer.begin(); // 这些变量在这里声明是为了防止后面goto报错
node tmp = {0, 0, ""};

if (right < left_bound) return; // must be duplicated
left = left < left_bound ? left_bound : left; // 左边已经接受过的数据就不要了
right = right <= left_bound + _capacity ? right : left_bound + _capacity; // 右边越界的也不要
o_right = right;
string res = data.substr(left - o_left, right - left);

size_t start = left; // the begin of the unused data-zone of variable data
if (data.compare("") == 0 || res.compare("") == 0) goto end; // 如果data是空串也直接不要
if (o_left >= left_bound + _capacity) goto end; // 越界的不要

// 开始区间合并。需要扫描两次
for (auto it = buffer.begin(); it != buffer.end(); it++) {
if (left >= it->left && left <= it->right) {
size_t r = right;
size_t l = left;
right = max(right, it->right);
left = min(left, it->left);
if (right == it->right) {
start = o_right;
} else {
start = it->right;
}
if (r <= it->right) {
res = it->data.substr(0, l - it->left) + data.substr(l - o_left) +
it->data.substr(r - it->left, it->right - r);
} else {
res = it->data.substr(0, l - it->left) + data.substr(l - o_left);
}
// 删除这一步很关键。
buffer.erase(it);
break;
}
}

// 第二次
for (auto it = buffer.begin(); it != buffer.end(); it++) {
if (it->left >= left && it->left <= right) {
// 此时空间不够,先尝试下能不能写入一部分到_output中
if (it->left > start && remaining < it->left - start) {
if (left == left_bound) {
size_t out_mem = _output.remaining_capacity();
if (out_mem > 0) {
// out的区域本身就很充足
if (out_mem >= it->left - start) {
// 写入ByteStream
_output.write(res.substr(0, it->left - start));
// 更新
res = res.substr(it->left - start);
left_bound = it->left - start + left;
left = left_bound;
remaining += it->left - start;
// out剩下的空位会在最后写入
goto ok;
} else {
// 光是out不够的话,那就先能腾多少空间腾多少
_output.write(res.substr(0, out_mem));
res = res.substr(out_mem);
left_bound = out_mem + left;
left = left_bound;
remaining += out_mem;
// 如果腾出空间加上原来空间足够,那就非常ok
if (it->left > start && remaining >= it->left - start) {
goto ok;
}
// 否则进入错误处理代码
}
}
}
tmp = {left, start + remaining, res.substr(0, start - left + remaining)};
remaining = 0;
if (eof == 1)
is_eof = false;
buffer.insert(tmp);
goto end;
}
ok:
remaining -= it->left - start;
start = it->right;
if (it->right > right){
res += it->data.substr(right - it->left, it->right - right);
right = it->right;
}
buffer.erase(it);
}
}

if (start < o_right && remaining < o_right - start) {
right = start + remaining;
if (eof == 1)
is_eof = false;
}
tmp = {left, right, res};
if (left < right)
buffer.insert(tmp);

end:
iterator = buffer.begin();
// write into the ByteStream
if (iterator != buffer.end() && iterator->left == left_bound) {
size_t out_rem = _output.remaining_capacity();
if (out_rem < iterator->data.length()) {
_output.write(iterator->data.substr(0, out_rem));
left_bound = iterator->left + out_rem;
tmp = {left_bound, iterator->right, iterator->data.substr(out_rem)};
buffer.erase(iterator);
buffer.insert(tmp);
} else {
_output.write(iterator->data);
left_bound = iterator->right;
buffer.erase(iterator);
}
}

// 满足两个条件才是真的eof
if (is_eof && buffer.empty()){
is_eof = false;
_output.end_input();
}
}

size_t StreamReassembler::unassembled_bytes() const {
// 可以跟上面的write合起来的,但在此处我采取了最保守的做法。
size_t res = 0;
for (auto it = buffer.begin(); it != buffer.end(); it++) {
res += it->right - it->left;
}
return res;
}

bool StreamReassembler::empty() const { return buffer.empty()&&is_eof; }
+

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();
}
]]> + + + 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,因为无需对它进行查找操作;转发表的内容可能跟路由表差不多,但是由于它要被进行频繁的查找工作,因而其数据结构需要对查找的消耗较低。

+

不过在我们这边,一般不区分路由表和转发表的概念。

+
+

感想

说实话思路很直观很简单,懒得说了,直接看代码吧【开摆】

+

我唯一卡得比较久的有两个地方,一个是一开始数据结构选用的是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{};
// ...
+ +

具体实现

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();
}
}
}
]]>
Lab3 TCPSender @@ -6599,237 +6647,44 @@ url访问填写http://localhost/webdemo4_war/*.do

当timer触发时,我们需要重传tmp_segments 队列头。

如果空间足够,直接重传就行了,然后double RTO,然后用RTO reset timer,然后再次启动timer。

-

如果空间不足够,只做上面那个的后两步,也即reset timer,然后再次启动timer。

- -
  • ack_received

    -
      -
    1. 更新window_size和ackno

      -
    2. -
    3. 重置超时重传

      -

      如果接收到的ackno比以前的大,则重置RTO,重启timer(如果tmp_segments不为空),重置cons_retran

      -
    4. -
    5. 从tmp_segments中删除元素

      -
    6. -
    7. 调用fill_window

      -
    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.

    -
    -
  • - -

    细节补充

    实现起来虽然很复杂,但思路确实很简单,正确思路和初见思路差不多,指导书写得很好很详细【以至于一开始我被指导书这么多内容给吓到了】。在这里只记录点实现过程中遇到的一些小错误以及我各个部分的实现细节补充。

    -

    timer实现

    指导书的建议是实现一个类,但是我太懒了()而且确实这个timer的状态也很少,因而我就直接把它写在sender里面了。

    -

    SYN报文是否可以带数据

    此实验未涉及这个。本次全部的测试用例都是SYN报文不携带数据的情况。【因为发出syn报文之后才将window_size设置为非0情况】

    -

    如果需要SYN报文不携带数据,可以在fill_window中把这句话:

    -
    if (!(_stream.buffer_empty() || remaining == 0)) {
    - -

    修改为这句话:

    -
    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);
    }
    ]]> - - - Lab4 TCPConnection - /2023/02/25/cs144$lab4/ - Lab4 TCPConnection

    心得

    耗时情况

    【长舒一口气】

    -

    最开始先记录下总体耗时情况吧。本次实验共耗费我16h+【不包括接下来写笔记的时间23333】,共耗费三个工作日。第一天看完了指导书,写完了代码,过掉了#45 reorder之前的所有测试。第二天过掉了#55 t_ucS_1M_32k之前的所有测试,直到第三天才过完了所有测试。

    -

    我觉得这整个过程还是挺有意义的,每天都有新的进展,看到test case越过越多是真的很高兴。但是可以说第二天以来就都是面向测试用例改bug了,非常折磨非常坐牢,既要去再次理清之前写过的shit山,又得搞清楚很多让人一头雾水不知从何下手的地方。但总之,这三天很充实,并不会让人觉得心累。

    -

    放个通关截图吧,感人至深。

    -

    image-20230305160608116

    -

    思路

    TCPConnection中具体要做什么,指导书已经写得很详细了,跟着指导书就行。代码部分还是不折磨的,思路直观清晰。

    -

    指导书内容

    -

    Here are the basic rules the TCPConnection has to follow:

    -
    -
      -
    1. Receiving segments

      -

      大概是在segment_received中做

      -

      image-20230303103640065

      -
        -
      1. 检查RST flag

        -

        如果RST被设置,sets both the inbound and outbound streams to the error state,杀死当前connection

        -

        return;

        -

        具体实现中,杀死connection可以置_linger_after_streams_finish为false。 the inbound and outbound streams对应着receiver和sender里的stream。让它们都处于error状态,只需设置ByteStream中的error字段

        -
      2. -
      3. 如果收到的segment with an invalid sequence number,connection需要发送empty segment应答

        -

        image-20230303110238265

        -
      4. -
      5. 转发segment给receiver

        -
      6. -
      7. 如果ACK,则把ackno和win_size给sender

        -
      8. -
      -
    2. -
    3. Sending segments

      -
        -
      1. 任何时候sender把segment放进其out流,你都要从中取出来
      2. -
      3. 从receiver处获取ackno和window_size,填入segment中
      4. -
      5. 放到自己的segment_out中
      6. -
      -

      从上述表述中,我们需要注意两点:

      -
        -
      1. 顺带ACK

        -

        可以看到,这跟我们上课的时候所学的一样,是“顺带ACK”,也即ACK报文并非独立发送,而是在下一次要发送其他数据报文的时候携带发送。这也一定程度上使得ack报文发送不会太频繁也不会太稀疏。

        -
      2. -
      3. 一定要经由sender

        -

        我们如果想要发送一个报文,一定得先把它存入sender中,再从sender的segment_out中取出来。这样做的目的是把该报文列入sender的超时重传管辖范围,你如果直接把报文发送到自己的segment_out中,就无法管理其超时重传了

        -
      4. -
      -
    4. -
    5. When time passes

      -

      tick()

      -
        -
      1. 调用sender的tick()
      2. -
      3. 检查sender的连续超时重传次数,如果大于MAX RETX ATTEMPTS,则关闭连接,并且发送RST标志的空报文
      4. -
      5. end the connection cleanly if necessary
      6. -
      -
    6. -
    -

    再注意一点对于connection的关闭。它要求有一个time pass

    -

    image-20230303111131650

    -

    image-20230303112817891

    -

    第一点挺好实现的,第二点需要在析构函数中检测。

    -

    最后的5.1部分值得一看。

    -

    接口说明

    TCPConnection的public函数接口定义以及具体要做什么如下。结合上面的指导书内容,TCPConnection的实现就很简单了,我就不多bb了。

    -
     private:
    TCPConfig _cfg;
    // 一个endpoint可以同时作为sender和receiver。
    TCPReceiver _receiver{_cfg.recv_capacity};
    TCPSender _sender{_cfg.send_capacity, _cfg.rt_timeout, _cfg.fixed_isn};

    //! outbound queue of segments that the TCPConnection wants sent
    // 把要发送的segment放在这里就行了
    std::queue<TCPSegment> _segments_out{};

    //! Should the TCPConnection stay active (and keep ACKing)
    //! for 10 * _cfg.rt_timeout milliseconds after both streams have ended,
    //! in case the remote TCPConnection doesn't know we've received its whole stream?
    bool _linger_after_streams_finish{true};

    public:
    // 也许需要调用TCPSender的fill_window(),然后从其segment_out中取出来,再发送给自己的segment_out
    // Initiate a connection by sending a SYN segment初始化connection并且发送SYN
    void connect();

    /* 这几个都很好实现,都很直观,只需调sender和receiver的API就行 */
    // 由上层socket调用,data路径 socket->connection->sender.stream_in().write()
    //! \brief Write data to the outbound byte stream, and send it over TCP if possible
    //! \returns the number of bytes from `data` that were actually written.
    size_t write(const std::string &data);
    //! \returns the number of `bytes` that can be written right now.
    size_t remaining_outbound_capacity() const;
    //! \brief Shut down the outbound byte stream (still allows reading incoming data)
    void end_input_stream();
    //! \brief The inbound byte stream received from the peer
    ByteStream &inbound_stream() { return _receiver.stream_out(); }
    // number of bytes sent and not yet acknowledged, counting SYN/FIN each as one byte
    size_t bytes_in_flight() const;
    //! \brief number of bytes not yet reassembled
    size_t unassembled_bytes() const;

    //! \brief Number of milliseconds since the last segment was received
    size_t time_since_last_segment_received() const;

    // debug用
    //!< \brief summarize the state of the sender, receiver, and the connection
    TCPState state() const { return {_sender, _receiver, active(), _linger_after_streams_finish}; };

    // 这些函数都会由上层在某些时候调用
    // 时钟滴答、收到segment以及从segment_out中取数据,这些都是由os调用相应函数实现的
    // 这也正是所谓“协议”的接口意义!
    //! \name Methods for the owner or operating system to call
    //! Called when a new segment has been received from the network
    void segment_received(const TCPSegment &seg);

    //! Called periodically when time elapses
    void tick(const size_t ms_since_last_tick);

    //! \brief TCPSegments that the TCPConnection has enqueued for transmission.
    //! \note The owner or operating system will dequeue these and
    //! put each one into the payload of a lower-layer datagram (usually Internet datagrams (IP),
    //! but could also be user datagrams (UDP) or any other kind).
    std::queue<TCPSegment> &segments_out() { return _segments_out; }

    //! \brief Is the connection still alive in any way?
    //! \returns `true` if either stream is still running or if the TCPConnection is lingering
    //! after both streams have finished (e.g. to ACK retransmissions from the peer)
    bool active() const;

    //! Construct a new connection from a configuration
    explicit TCPConnection(const TCPConfig &cfg) : _cfg{cfg} {}

    //! \name construction and destruction
    //! moving is allowed; copying is disallowed; default construction not possible

    ~TCPConnection(); //!< destructor sends a RST if the connection is still open
    TCPConnection() = delete;
    TCPConnection(TCPConnection &&other) = default;
    TCPConnection &operator=(TCPConnection &&other) = default;
    TCPConnection(const TCPConnection &other) = delete;
    TCPConnection &operator=(const TCPConnection &other) = delete;
    - -

    测试程序

    这部分暂时还不大明白,随便瞎写一点()

    -

    首先是socket实现,似乎要涉及到对一些事件,比如说segment receive的监听。它具体是这么做的:

    -
    // in libsponge/tcp_helper/tcp_sponge_socket.cc  _initialize_TCP()
    // 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)
    //
    // 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)
    //
    // 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)
    //
    // 4) Outbound segment generated by TCP (needs to be
    // given to underlying datagram socket)
    - -

    比如说event4:

    -

    image-20230304171810877

    -
    -

    什么是eventloop

    -

    事件循环(event loop)就是 任务在主线程不断进栈出栈的一个循环过程。任务会在将要执行时进入主线程,在执行完毕后会退出主线程。

    -

    这里的大致意思就是增加了一个监听事件,一旦tcp_connection的segments_out有元素,就会马上取出来

    -
    -

    这部分不大懂,不知道后面会不会涉及对socket的编写?

    -

    还有一点是对测试脚本好像有了点了解。比如在build/CTestTestfile.cmake中可以看到每个测试的对应脚本以及使用的options:

    -

    image-20230304170132380

    -

    如果不知道option的用法可以这么做:

    -

    image-20230305232621024

    -

    这些脚本实现的对应代码在sponge/apps中。

    -

    又比如,在sponge/etc/tests.cmake中,可以找到各个测试程序执行的参数,就可以比如说修改测试的Timeout时间:

    -

    image-20230305232913611

    -

    总结:状态机

    我们已经完整实现了整个TCP协议,是时候该对其做出一个总结了。

    -

    TCP协议本质上是一个状态机

    -

    在我们的sponge TCP中,我们将一个endpoint的TCP协议分成了两个状态机,一个是TCPReceiver的状态机,另一个是TCPSender的状态机。它们依据外界的输入【从app或者互联网】来进行状态的转移。

    -

    以下几张图完美地体现了状态转移关系【具体的状态体现标注在代码中了】:

    -

    image-20230226200935395

    -

    image-20230226202631406

    -

    image-20230305225738049

    -

    image-20230225232723083

    -

    TCPConnection并不是状态机,它是两个状态机和外界联通的桥梁。它的职能有:

    -
      -
    1. 给状态机提供输入

      -

      包括:

      -
        -
      1. app调用write传进来的数据
      2. -
      3. peer通过segment_received传进来的数据
      4. -
      +

      如果空间不足够,只做上面那个的后两步,也即reset timer,然后再次启动timer。

    2. -
    3. 处理状态机的输出

      -

      包括:

      +
    4. ack_received

        -
      1. app调用receiver的stream接口获取数据
      2. -
      3. 通过send_segment向peer传递数据
      4. -
      +
    5. 更新window_size和ackno

      +
    6. +
    7. 重置超时重传

      +

      如果接收到的ackno比以前的大,则重置RTO,重启timer(如果tmp_segments不为空),重置cons_retran

      +
    8. +
    9. 从tmp_segments中删除元素

      +
    10. +
    11. 调用fill_window

    -

    也可以说,它具有显式推动状态机状态转移的作用,比如说:

    -
      -
    1. 给状态机传递外界数据让他们转移
    2. -
    3. connect通过调用fill_window推动_sender从CLOSED状态转移到SYN_SENT状态
    4. -
    5. 转移到ERROR状态的条件判断
    6. -
    -

    等等等。

    -

    也因而,TCPConnection并不包含复杂的逻辑和算法,它仅仅是做一些条件判断,以及一些数据转发的工作。

    -

    喜闻乐见的bug合集

    相比于代码的编写,本次实验最难的部分是测试。由于lab4基于lab0-3,因而前面没有发现的bug在本次黑压压162个测试之下会全部涌现出来。有些bug我还是不知道怎么回事,并且debug过程也不像xv6那样条理清晰步步为营,感觉充满着不少玄幻色彩,所以也没有很多干货好说。在这里就先记录下印象比较深刻,耗时比较久的bug吧。

    -

    TCP produced ‘ackno=1’

    image-20230303214944132

    -

    需要发送一个ackno=2的帧,但是不知道为什么却发送了一个ackno=1的,并且无论我怎么找,在哪里print,都只能找到一个ackno=2的,连1的影子都看不到。这个现象确实很诡异,但其实它的内因很简单。它是由于我对空的ACK帧发送条件限制得不恰当才出现的。

    -
    -

    有没有觉得这里有点跳跃?我是怎么通过这个现象得知是ACK发送不恰当导致的?

    -

    答案是我当时也没想到这一点,无头苍蝇般转了可能有一个小时,这里print一下那里print一下都没有发现异常。最后我放弃了这个用例去看下一个错误的用例,才发现了这个小bug,改了一下发现这个也一起过了【绷】

    -
    -

    本来我是这么写的:

    -
    // 把_sender的所有segment都发送出去
    void TCPConnection::segment_send() {
    // 当没有要发送的帧,无法进行顺带ACK时,就只能发送一个只有ACK的空帧
    if (_sender.segments_out().empty()) {
    if (_receiver.ackno().has_value()) {
    _sender.send_empty_ack_segment(_receiver.ackno().value());
    }
    }
    while (!_sender.segments_out().empty()) {
    // ...
    }
    }

    // in segment_received()
    //if (seg.length_in_sequence_space() != 0) {
    // empty_ack_send();
    //}
    segment_send();
    - -

    如果这么写的话,当这台endpoint收到peer的一个empty ACK后,它就也会以示敬意回复一个empty ACK,这样除了本应发过去的ackno=2的报文,就多了个幽灵般的ackno=1的empty ACK,从而导致上面的错误。

    -

    因而,正确的做法是,我们在receive时只对**!empty**的seg进行ACK回复就行。具体写法可以看看我下面的代码。

    -

    超时重传时间翻倍问题

    image-20230303224104016

    -

    image-20230303224053277

    -

    可以看到,它是想要我们在1000ms后再发一次FIN的,也即rto依然等于1000,但是我们的rto却是2000.为啥呢?那就去看看超时重传呗。

    -

    原来的超时重传代码:

    -
    // in libsponge/tcp_sender.cc
    if (timer_start && ticks > timer_ticks && ticks - timer_ticks >= rto) {
    // resend
    if (!tmp_segments.empty()) {
    _segments_out.push(tmp_segments.front().segment);
    }

    if (window_size != 0) {
    cons_retran++;
    rto *= 2;
    }
    timer_ticks = ticks;
    }
    - -

    改完后:

    -
    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;
    }
    - -

    想想超时重传的定义,是不是重传了之后才会double时间呀()

    -

    assembler

    image-20230304163111298

    -

    这个test花了我半个下午的时间排查和修改。大致流程及报错信息是,一方发了65000个byte,但是另一方只能收到<<65000个。最后print了一下,发现是streamassembler写错了,在stream end的时候仍然有很大一部分数据未被整流。

    -
    -

    这个直面屎山的经历极大地鼓舞了我

    -
    -

    之前在写streamassembler的时候就知道有个地方是错的了,那就是我对capacity的理解【具体见前面的笔记】。现在只用改一下就好了。修改方式很简单,加上这两句话就行:

    -
    right = right <= left_bound + _capacity ? right : left_bound + _capacity;  // 右边越界的也不要
    if (o_left >= left_bound + _capacity) goto end; // 越界的不要
    - -

    t_udp_client_send超时

    image-20230304215748823

    -

    这个错因非常地诡异,我到最后也还是没有自己找出来。直到我瞎搜来搜去看到了这篇文章:

    + +
  • fill_window

    +

    如果window_size - tmp_size <= 0 或者 byte stream空,则什么也不做

    +

    否则根据syn和fin标记创建一个new segment,然后写入out stream

    -

    我也是真的很佩服这篇文章的作者能找到这个点

    -

    image-20230304221420748

    -

    https://www.cnblogs.com/lawliet12/p/17066719.html

    +

    no bigger than the value given by TCPConfig::MAX PAYLOAD SIZE (1452 bytes)

    -

    image-20230305215310021

    -

    噔噔咚。

    -

    我为什么不用_cfg.rt_timeout呢?答案是我当初脑子一抽以为rt_timeout是static、const的,就写了个TCPConfig::rt_timeout然后报错了,我懒得思考了就换成了上面的那个,结果……就这东西,又花费了我好久好久【悲】怪我没有认真看,没发现rt_timeout不是一个静态常量。

    -

    t_ucS_1M_32K超时

    image-20230305162239877

    -

    以及其后面的其他test也都超时了。

    -

    说实话我真是百思不得其解,这里打印来那里打印去,也都看得眼花缭乱什么也看不出来,使用指导书那些手动测试的方法,还有抓包,都十分地正常,但它自动测试就是会timeout。

    -

    我折腾来折腾去,这里print那里print,最后还怀疑是电脑问题就放到服务器上跑了一下结果还是不行。绝望之际,我只能使出了万策尽之时的迫不得已的非法手段:将我的一部分代码替换成别人的看看会怎么样。【传统艺能23333】

    -

    最终我定位发现是TCPSender出了问题,我猜测是因为状态机出错了。我比对着别人的代码【知道这不对,但我心态已经崩了。。。】,以及指导书提供的状态机,发现是这个地方出了小问题:

    -

    image-20230305220614819

    -
    // in tcp_sender.cc fill_window()
    // 注释的是以前写的错误版本
    // if (_stream.input_ended() && !fin && remaining > 0) {
    if (_stream.eof() && !fin && remaining > 0) {
    // last segment
    segment.header().fin = true;
    fin = true;
    remaining -= 1;
    }
    - -

    这里不应该是input_ended,而应该是eof……

    -

    改了之后立刻所有测试都能跑通了【悲】

    -

    那么问题来了,为什么错误版本就会timeout呢?我的猜测如下:

    -

    eof的条件如下:

    -
    bool ByteStream::eof() const { return is_input_end && buffer.empty(); }
    - -

    可以看到,eof既要求input_ended,又要求缓冲区内所有数据成功发送。这也很符合FIN_SENT的语义:在数据流终止时(所有数据成功发送,不要求fully acked)发送FIN。

    -

    如果按照我错误版本的写法,会导致数据还没发送完毕(!buffer.empty()),就发送了FIN。之后数据虽然还能正常进入receiver的bytestream,并且发送给peer的receiver。但是会存在这也一个空窗期:FIN之后的数据还没到的时候,peer的receiver接收到FIN,并且peer的app从socket将receiver接收到的数据全部读出。出现了这样的空窗期,就会导致peer的receiver的stream达到eof状态:

    -
    // in tcp_receiver.cc segment_received()
    if (abs_seqno != 0)
    _reassembler.push_substring(data, index, header.fin);
    // in streamassembler.cc
    if (is_eof && buffer.empty()) {
    _output.end_input();
    }
    - -

    【接下来就是猜了】由于bytestream eof了,socket就停止读了。后来的数据再来,receiver的stream的缓冲区就满了,receiver就只能一直丢包。【接下来是真的纯猜】而且由于测试脚本问题,在这之后都不会调用tick方法了,故而超时重传检测不会被触发,而sender也会因为没有ack,而一直重传重传,就死循环然后timeout寄掉了。

    -

    纯猜部分的依据是:

    -

    image-20230305173110776

    -

    image-20230305173152469

    -

    可以看到,tick方法一直被调用,但是ticks却不变。数据报文一直被重传,但是retran一直不变。ticks-timer_ticks一直大于rto,但却始终无法进入那句if(经测试是这样的)。这非常奇怪,我也不知道为什么。

    +

    If the receiver has announced a window size of zero, the fifill window method should act like the window size is one.

    -

    代码

    【珍贵的调试用代码没删的版本放在github了。】

    -

    TCPConnection.hh

    class TCPConnection {
    private:
    // ...
    size_t rec_tick{};// 上一次收到segment时的ticks数
    size_t ticks = 0;
    public:
    void segment_send();
    void empty_ack_send();
    void set_rst();
    // ...
    +
  • + +

    细节补充

    实现起来虽然很复杂,但思路确实很简单,正确思路和初见思路差不多,指导书写得很好很详细【以至于一开始我被指导书这么多内容给吓到了】。在这里只记录点实现过程中遇到的一些小错误以及我各个部分的实现细节补充。

    +

    timer实现

    指导书的建议是实现一个类,但是我太懒了()而且确实这个timer的状态也很少,因而我就直接把它写在sender里面了。

    +

    SYN报文是否可以带数据

    此实验未涉及这个。本次全部的测试用例都是SYN报文不携带数据的情况。【因为发出syn报文之后才将window_size设置为非0情况】

    +

    如果需要SYN报文不携带数据,可以在fill_window中把这句话:

    +
    if (!(_stream.buffer_empty() || remaining == 0)) {
    -

    TCPConnection.cc

    如果想要以状态机的视角来看待,可以看看感恩的代码。他写得很清晰。

    -
    #include "tcp_connection.hh"
    #include <iostream>

    template <typename... Targs>
    void DUMMY_CODE(Targs &&... /* unused */) {}

    using namespace std;

    size_t TCPConnection::remaining_outbound_capacity() const { return _sender.stream_in().remaining_capacity(); }

    size_t TCPConnection::bytes_in_flight() const { return _sender.bytes_in_flight(); }

    size_t TCPConnection::unassembled_bytes() const { return _receiver.unassembled_bytes(); }

    size_t TCPConnection::time_since_last_segment_received() const { return ticks - rec_tick; }

    // 发送一个只有ACK的空帧,仅在segment_received中调用
    // 当没有要发送的帧,无法进行顺带ACK时,
    // 为了保障一定有ACK发送,就只能发送一个只有ACK的空帧
    void TCPConnection::empty_ack_send() {
    if (_sender.segments_out().empty()) {
    if (_receiver.ackno().has_value()) {
    _sender.send_empty_ack_segment(_receiver.ackno().value());
    }
    }
    }

    // 把_sender的所有segment都发送出去
    void TCPConnection::segment_send() {
    while (!_sender.segments_out().empty()) {
    TCPSegment seg = _sender.segments_out().front();
    // 顺带ACK
    if (_receiver.ackno().has_value()) {
    seg.header().ack = true;
    seg.header().ackno = _receiver.ackno().value();
    }
    seg.header().win = _receiver.window_size();
    _segments_out.push(seg);
    _sender.segments_out().pop();
    }
    }

    // connection被置为error状态的部分必要操作
    void TCPConnection::set_rst() {
    _sender.stream_in().set_error();
    _receiver.stream_out().set_error();
    _linger_after_streams_finish = false;
    }

    void TCPConnection::segment_received(const TCPSegment &seg) {
    // 重置发送的ticks
    rec_tick = ticks;

    if (seg.header().rst) {
    // RST is set
    set_rst();
    return;
    }

    // 回复对方问你是死是活的信息
    if (_receiver.ackno().has_value() && seg.length_in_sequence_space() == 0 &&
    seg.header().seqno - _receiver.ackno().value() < 0) {
    _sender.send_empty_segment();
    segment_send();
    return;
    }

    _receiver.segment_received(seg);

    if (seg.header().ack) { // ack_received也会调用fill_window
    _sender.ack_received(seg.header().ackno, seg.header().win);
    } else
    _sender.fill_window();

    // 只在本次收到的seg需要被ACK的时候才要ACK。
    // 需要被ACK:FIN/SYN/携带数据 总之就是length!=0
    // 不得不说,FIN和SYN都会占一个序列号这个点给ACK设计带来了简便,同时也增加了安全性
    if (seg.length_in_sequence_space() != 0) {
    empty_ack_send();
    }

    segment_send();

    // If the inbound stream ends before the TCPConnection has reached EOF
    // on its outbound stream, this variable needs to be set to false
    // 如果receiver的那个stream比sender的stream早结束,就不用等待
    // 为什么呢?因为receiver的stream结束说明了全部的seg都成功接收并且全部整流【参见assembler实现】
    // 也就说明对方不发送数据了,并且已经把FIN也发过来了
    // 也即对方进入了FIN_WAIT状态
    // 而我们的sender还在输出,也即我们在CLOSE_WAIT状态
    // 因而我们只需输出完剩余数据再发送AF,最后直接关闭就行
    // 因为我们知道对方已经关闭了,无需再进行linger。
    if (_receiver.stream_out().input_ended() && !_sender.stream_in().eof()) {
    // peer:FIN_WAIT self:CLOSE_WAIT
    _linger_after_streams_finish = false;
    }
    }

    bool TCPConnection::active() const{
    // 处于error状态
    if (!_linger_after_streams_finish && _receiver.stream_out().error() && _sender.stream_in().error()) {
    return false;
    }

    // 满足条件1-3
    if (_receiver.stream_out().input_ended() &&
    _sender.stream_in().eof() && _sender.bytes_in_flight() == 0 && _sender.fully_acked()) {
    // 无需等待的话就直接返回false
    if (!_linger_after_streams_finish)
    return false;
    // 否则需要等待10*timeout
    else if (time_since_last_segment_received() >= 10 * _cfg.rt_timeout){
    return false;
    }
    }
    return true;
    }

    size_t TCPConnection::write(const string &data) {
    size_t res = _sender.stream_in().write(data);
    // 注意此处需要手动调一下fill_window和send方法
    _sender.fill_window();
    segment_send();
    return res;
    }

    // ms_since_last_tick: number of milliseconds since the last call to this method
    void TCPConnection::tick(const size_t ms_since_last_tick) {
    ticks += ms_since_last_tick;
    _sender.tick(ms_since_last_tick);
    if (_sender.consecutive_retransmissions() > _cfg.MAX_RETX_ATTEMPTS) {
    while (!_sender.segments_out().empty())
    _sender.segments_out().pop(); // 清除sender遗留的所有帧
    _sender.send_empty_rst_segment();// 只发送rst帧
    set_rst();
    }
    segment_send();

    // end the connection cleanly if necessary
    if (_receiver.stream_out().input_ended() &&
    _sender.stream_in().eof() && _sender.bytes_in_flight() == 0 && _sender.fully_acked()
    && time_since_last_segment_received() >= 10 * _cfg.rt_timeout) {
    // 等待结束
    _linger_after_streams_finish = false;
    }

    }

    void TCPConnection::end_input_stream() {
    _sender.stream_in().end_input();
    _sender.fill_window();
    segment_send();
    }

    void TCPConnection::connect() {
    _sender.fill_window();
    // send_segment重复代码。目的是防止发送SYN外还发送别的东西
    if (!_sender.segments_out().empty()) {
    TCPSegment seg = _sender.segments_out().front();
    if (_receiver.ackno().has_value()) {
    seg.header().ack = true;
    seg.header().ackno = _receiver.ackno().value();
    }
    seg.header().win = _receiver.window_size();
    _segments_out.push(seg);
    _sender.segments_out().pop();
    }
    }

    TCPConnection::~TCPConnection() {
    try {
    // shutdown uncleanly
    if (active()) {
    set_rst();
    _sender.send_empty_rst_segment();
    segment_send();
    }
    } catch (const exception &e) {
    std::cerr << "Exception destructing TCP FSM: " << e.what() << std::endl;
    }
    }
    +

    修改为这句话:

    +
    if (!segment.header().syn&&!(_stream.buffer_empty() || remaining == 0)) {
    -

    debug函数

    // in libsponge/tcp_helper/tcp_segment.hh
    void print_seg() const{
    std::cerr<<" flag="<<(header().syn?"S":"")<<(header().ack?"A":"")<<(header().fin?"F":"")
    <<" seqno="<<header().seqno.raw_value()<<" ackno="<<header().ackno.raw_value()
    <<" payload_size:"<<payload().size()<<std::endl;
    }
    +

    代码

    头文件

    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; }
    -

    in libsponge/tcp_helper/fd_adapter.cc

    -

    image-20230304172207234

    -]]>
    +

    具体实现

    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 @@ -6978,74 +6833,6 @@ url访问填写http://localhost/webdemo4_war/*.do

    具体实现

    #include "network_interface.hh"
    #include "arp_message.hh"
    #include "ethernet_frame.hh"
    #include <iostream>

    template <typename... Targs>
    void DUMMY_CODE(Targs &&... /* unused */) {}

    using namespace std;


    NetworkInterface::NetworkInterface(const EthernetAddress &ethernet_address, const Address &ip_address)
    : _ethernet_address(ethernet_address), _ip_address(ip_address) { }

    // param: target ip address , target ethernet address , and the arp opcode
    // if is the request, pass BROADCAST as the param;
    // if is the response, pass arprequest.eth_add as the param.
    void NetworkInterface::send_arp(uint32_t target_ip, EthernetAddress eth_add, uint16_t opcode) {
    // create the eth frame
    EthernetFrame frame;
    /* create the payload */
    // create the arp message
    ARPMessage arp_mes;
    arp_mes.sender_ethernet_address = _ethernet_address;
    arp_mes.sender_ip_address = _ip_address.ipv4_numeric();
    arp_mes.opcode = opcode;
    arp_mes.target_ip_address = target_ip;
    if (opcode == ARPMessage::OPCODE_REPLY)
    arp_mes.target_ethernet_address = eth_add;
    else // if is the REQUEST, arp target mac is unknown and should be set to zero
    arp_mes.target_ethernet_address = {0, 0, 0, 0, 0, 0};
    // serialize and put it into the payload
    frame.payload() = BufferList(arp_mes.serialize());
    /* fill in the header */
    EthernetHeader header;
    header.src = _ethernet_address;
    header.dst = eth_add;
    header.type = EthernetHeader::TYPE_ARP;
    frame.header() = header;

    // send it
    _frames_out.push(frame);
    }

    //! \param[in] dgram the IPv4 datagram to be sent
    //! \param[in] next_hop the IP address of the interface to send it to (typically a router or default gateway, but may also be another host if directly connected to the same network as the destination)
    //! (Note: the Address type can be converted to a uint32_t (raw 32-bit IP address) with the Address::ipv4_numeric() method.)
    // 处理从上层协议接收到的信息
    void NetworkInterface::send_datagram(const InternetDatagram &dgram, const Address &next_hop) {
    // convert IP address of next hop to raw 32-bit representation (used in ARP header)
    const uint32_t next_hop_ip = next_hop.ipv4_numeric();
    // create the eth frame
    EthernetFrame frame;
    // fill in the payload
    frame.payload() = dgram.serialize();
    // fill in the header
    EthernetHeader header;
    // dst is reserved
    header.src = _ethernet_address;
    header.type = EthernetHeader::TYPE_IPv4;

    auto tmp_arp = arp_mappings.find(next_hop_ip);// find the mac from the arp mappings
    if (tmp_arp == arp_mappings.end()) { // not exist
    frame.header() = header; // remember that the dst field is reserved
    auto tmp_wait = waiting_frames.find(next_hop_ip);// is there a waiting queue?
    // the arp request hasn't been sent if there is not a waiting queue,
    if (tmp_wait == waiting_frames.end()) {
    // create the waiting queue
    vector<EthernetFrame> frames;
    frames.push_back(frame);
    waiting_node node;
    node.frames = frames;
    node.latest_ticks = ticks;
    waiting_frames.insert(make_pair(next_hop_ip, node));
    // send arp request
    send_arp(next_hop_ip, ETHERNET_BROADCAST, ARPMessage::OPCODE_REQUEST);
    } else { // the arp request has been sended
    if (ticks - tmp_wait->second.latest_ticks > 5*1000) {// send before 5 seconds
    // resend arp request
    send_arp(next_hop_ip, ETHERNET_BROADCAST, ARPMessage::OPCODE_REQUEST);
    // update the time only when the arp request was sent
    tmp_wait->second.latest_ticks = ticks;
    }
    tmp_wait->second.frames.push_back(frame);// add the frame to the waiting list
    }
    return;
    }
    // recall that the dst field is reserved
    header.dst = tmp_arp->second.mac;
    frame.header() = header;

    // send it right away
    _frames_out.push(frame);
    }

    //! \param[in] frame the incoming Ethernet frame
    optional<InternetDatagram> NetworkInterface::recv_frame(const EthernetFrame &frame) {
    if (frame.header().dst != _ethernet_address && frame.header().dst != ETHERNET_BROADCAST)
    return {}; // should not accept
    if (frame.header().type == EthernetHeader::TYPE_IPv4) {
    InternetDatagram ip_data;
    ParseResult res = ip_data.parse(frame.payload());
    if (res == ParseResult::NoError) {
    return ip_data; // return to send it to the upper protocal
    } else
    return {};
    }
    if (frame.header().type != EthernetHeader::TYPE_ARP)
    return {};

    ARPMessage arp_mes;
    ParseResult res = arp_mes.parse(frame.payload());
    if (res != ParseResult::NoError)
    return {};
    // I'm not the arp target
    if (arp_mes.target_ip_address != _ip_address.ipv4_numeric())
    return {};

    auto tmp_arp = arp_mappings.find(arp_mes.sender_ip_address);
    if (tmp_arp == arp_mappings.end()) { // arp mapping not exist, create one
    arp_node node;
    node.ticks = ticks;
    node.mac = arp_mes.sender_ethernet_address;
    arp_mappings.insert(make_pair(arp_mes.sender_ip_address, node));
    } else {
    tmp_arp->second.ticks = ticks; // update the record ticks
    }
    if (arp_mes.opcode == ARPMessage::OPCODE_REQUEST) {
    send_arp(arp_mes.sender_ip_address, arp_mes.sender_ethernet_address, ARPMessage::OPCODE_REPLY);
    // shouldn't return now, maybe we are also waiting for the sender's mac address
    // return {};
    }
    // send all frames in the waiting queue
    auto tmp_wait = waiting_frames.find(arp_mes.sender_ip_address);
    // have no frames waiting for the address
    if (tmp_wait == waiting_frames.end())
    return {};
    while (!tmp_wait->second.frames.empty()) {
    EthernetFrame f = tmp_wait->second.frames.back();
    // recall that the dst field is reserved
    f.header().dst = arp_mes.sender_ethernet_address;
    _frames_out.push(f);
    tmp_wait->second.frames.pop_back();
    }
    // erase all the waiting frames
    waiting_frames.erase(tmp_wait);
    return {};
    }

    //! \param[in] ms_since_last_tick the number of milliseconds since the last call to this method
    void NetworkInterface::tick(const size_t ms_since_last_tick) {
    ticks += ms_since_last_tick;
    // walk the arp mappings to check whether a mapping is out-of-date
    for (auto it = arp_mappings.begin(); it != arp_mappings.end();) {
    if (ticks - it->second.ticks > 30 * 1000) {
    it = arp_mappings.erase(it);
    } else
    it++;
    }
    }
    ]]>
    - - 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,因为无需对它进行查找操作;转发表的内容可能跟路由表差不多,但是由于它要被进行频繁的查找工作,因而其数据结构需要对查找的消耗较低。

    -

    不过在我们这边,一般不区分路由表和转发表的概念。

    -
    -

    感想

    说实话思路很直观很简单,懒得说了,直接看代码吧【开摆】

    -

    我唯一卡得比较久的有两个地方,一个是一开始数据结构选用的是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{};
    // ...
    - -

    具体实现

    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();
    }
    }
    }
    ]]>
    -
    - - cs144 - /2023/02/25/cs144/ - -

    总耗时:65h 约17天

    -

    实验官网

    -

    感恩

    -
    -

    总结

    本实验总体思维和代码上的难度还是不难的(至少比xv6简单),我认为其难点主要集中在TCP协议本身就很复杂很多的细节问题,以及需要我们有一种面向测试用例编程、直面自己往日写过的屎山的勇气(。

    -

    下面,我将对本实验的完成情况即心得进行一个总结,也算是本篇博客/本次实验的一个导读

    -
    -

    类似于这样的块引用中的部分是我自认为的精华部分哦

    -
    -

    本实验对TCP-IP-ETH协议栈的实验是自顶向下的,其中对TCP协议的实现是由内而外的。

    -

    后者很容易导致,在对TCP协议的实现中,当你写完了lab0-3,你还是不知道自己到底写了个啥,以及TCP又究竟怎么通过你写的那几个类run起来。直到lab4结束,你完成了对TCP状态机的组织,并且出于debug目的钻研过部分socket的代码、熟悉了(其实差不多已经背下来了)TCP的三握手四挥手的过程,至此你才会对TCP的实现有较为清晰的理解。这个过程很痛苦,但是也真的非常爽。

    -
    -

    关于TCP状态机的理解总结,请参见Lab4 TCPConnection——心得——总结:状态机部分。

    -
    -

    然而,实现了TCP协议之后,我们还是不知道,在TCPConnection中发送的数据包,又是如何到达网络上的另一个host处的,我们究竟又写了个啥。

    -

    这时,官方贴心地为我们指了条明路:它告诉我们,我们在lab0-4实现的是TCP-IP协议栈,其中运输层和网络层由用户实现,其他更底层则由内核实现,二者通过操作系统提供的TUN接口进行交互。也即,我们之前实现的是用户态TCP协议!而我们接下来的学习目标,就是从内核中再夺走一些权力:数据链路层也要由我们自己实现!

    -
    -

    关于此处的TCP-IP架构等,请参见Lab5 NetworkInterface——Overview——承上启下

    -
    -

    故而,接下来,我们将实现用户态的TCP-IP-ETH协议栈。在lab5,我们将目光投向TCP层以下的数据链路层(网络层官方已经帮我们实现了),实现ETH协议和ARP协议;在lab6,我们则需要实现路由查找的功能。

    -

    至此,所有实验已经结束。写完了上述实验,我们对协议栈已经具有了很深刻的了解,对TCP—IP—ETH—TAN—Internet—TAN—ETH—IP—TCP的这个数据传输过程也已经是懂王了。

    -

    然而,我们在TCPConnection,只知道会有好心人,在上层app有数据传进来的时候调用write、在下层协议栈有segment传进来的时候调用segment_received、取出_segment_out的segment向底层协议栈发送、读走outputstream的内容。但是这个所谓的“好心人”具体是怎么做到的,怎么实现的,我们一概不知。

    -

    答案是,这个所谓的“好心人”,其实就是我们的TCPSpongeSocket。它向上将协议栈与上层app连接,向下又将协议栈与TAN接口结合。

    -
    -

    发送数据时,数据流向:上层app→(通过TCPSpongeSocketTCPConnection→(通过write方法)ByteStreamTCPSender→(通过从_sender.segments_out读)TCPConnection→(通过TCPSpongeSocket的adapter)TAN

    -

    接收数据时,数据流向:TAN→(通过TCPSpongeSocket的adapter)TCPConnectionTCPReceiver→(中间经过StreamAssemblerBYteStream→(通过TCPSpongeSocket读)上层app

    -

    TCPSpongeSocket的adapter中:TCPsegment←→IP数据报←→ETH帧

    -

    至于ETH帧进入TAN之后的过程?在xv6的网卡驱动那一节我们事实上已经实现过了!

    -
    -

    CS144TCPSocketFullStackTCPSokect都继承自TCPSpongeSocketTCPSpongeSocket通过一个包装了操作系统提供的socket的包装类_thread_data来与上层app进行交互,通过adapter_datagram_adapter来与协议栈进行交互(adapter本质上也是调用了操作系统的TUN/TAN接口)。

    -

    由于_thread_data_datagram_adapter本质上都是文件描述符【牛逼吧】,因而,TCPSpongeSocket需要跟上下层进行交互的需求,就可以通过操作系统提供的POLL机制来实现,也即,app←→TCP、TCP←→协议栈的这四种数据交互情况,都用事件监听来实现!这样就能做到“及时”“高效”了。

    -
    -

    关于此处TCPSpongeSocket的事件监听机制以及其它实现细节,详见其它的对…——Socket实现——TCPSpongeSocket

    -
    -

    至此以来,我们的协议栈才算真正完整了。

    -

    Lab0

    Lab1 StreamReassembler

    Lab2 TCPReceiver

    Lab3 TCPSender

    Lab4 TCPConnection

    Lab5 NetworkInterface

    Lab6 Router

    其他的对实验未涉及的思考

    ]]> - - labs - - 数据库原理 /2023/11/26/database/ @@ -7995,870 +7782,959 @@ url访问填写http://localhost/webdemo4_war/*.do

    整体架构

    在rtt中,提供了一系列回调函数用于实现gpio:

    struct rt_pin_ops
    {
    // 设置gpio的模式(读/写)
    void (*pin_mode)(struct rt_device *device, rt_base_t pin, rt_uint8_t mode);
    // 读写gpio
    void (*pin_write)(struct rt_device *device, rt_base_t pin, rt_uint8_t value);
    rt_int8_t (*pin_read)(struct rt_device *device, rt_base_t pin);
    // rtt特有中断回调,mode可以设置为电平/边缘触发,以及触发极性
    rt_err_t (*pin_attach_irq)(struct rt_device *device, rt_base_t pin,
    rt_uint8_t mode, void (*hdr)(void *args), void *args);
    rt_err_t (*pin_detach_irq)(struct rt_device *device, rt_base_t pin);
    rt_err_t (*pin_irq_enable)(struct rt_device *device, rt_base_t pin, rt_uint8_t enabled);
    // 引脚映射
    rt_base_t (*pin_get)(const char *name);
    };
    -

    milkv上有ABCDE五个gpio组件:

    -
    gpio@03020000 {
    bank-name = "porta";
    };

    gpio@03021000 {
    bank-name = "portb";
    };

    gpio@03022000 {
    bank-name = "portc";
    };

    gpio@03023000 {
    bank-name = "portd";
    };

    gpio@05021000 {
    bank-name = "porte";
    };
    - -

    但是,在实现中,这五个gpio组件会被注册为同一个设备,而不是像uart那样有多少个就注册多少个:

    -
    rt_device_pin_register("gpio", &_dwapb_ops, RT_NULL);
    - -

    外界交互

    应用程序会通过引脚名,使用rt_pin_get获取驱动内部自定义的引脚号,然后就可以通过引脚号对其进行模式设置和读写:

    -
    int DUO_LED = rt_pin_get("C24");
    rt_pin_mode(DUO_LED, PIN_MODE_OUTPUT);
    rt_pin_write(DUO_LED, PIN_LOW);
    - -

    寄存器

    配置

    这坨宏我是从linux里搬过来的。各个寄存器的含义可以详见data book,在此不多赘述。

    -

    访问

    在rtt中,外设地址也是统一编址到内存地址空间的。由其它gpio实现可知,当开启RT_USING_LWP(轻量级进程支持)时,外设地址不再是一一映射,需要我们手动在init中调用ioremap进行映射:

    -
    #ifdef RT_USING_LWP
    #define BSP_IOREMAP_GPIO_DEVICE(no) \
    rt_ioremap((void *)(DWAPB_GPIOA_BASE + (no) * DWAPB_GPIO_SIZE), DWAPB_GPIO_SIZE);

    dwapb_gpio_base = (rt_size_t)BSP_IOREMAP_GPIO_DEVICE(0);
    BSP_IOREMAP_GPIO_DEVICE(1);
    BSP_IOREMAP_GPIO_DEVICE(2);
    BSP_IOREMAP_GPIO_DEVICE(3);
    dwapb_gpio_base_e = (rt_size_t)rt_ioremap((void *)DWAPB_GPIOE_BASE, DWAPB_GPIO_SIZE);
    #endif
    - -

    然后,之后就可以在各个方法中借助这两个函数传入寄存器地址直接进行读写了:

    -
    rt_inline rt_uint32_t dwapb_read32(rt_ubase_t addr)
    {
    return HWREG32(addr);
    }

    rt_inline void dwapb_write32(rt_ubase_t addr, rt_uint32_t value)
    {
    HWREG32(addr) = value;
    }
    #define HWREG32(x) (*((volatile rt_uint32_t *)(x)))
    - -

    引脚映射

    摸索了半天,最终还是猜测这个引脚映射规定应该是自己决定的,于是我就从别的bsp那边把规则搬了过来:

    -
    // port && no -> pin
    #define PIN_NUM(port, no) (((((port) & 0xFu) << 8) | ((no) & 0xFFu)))
    // pin -> port
    #define PIN_PORT(pin) ((uint8_t)(((pin) >> 8) & 0xFu))
    // pin -> no
    #define PIN_NO(pin) ((uint8_t)((pin) & 0xFFu))
    - -

    例如,“C24”就可转化为((((('C' - 'A') & 0xFu) << 8) | ((24) & 0xFFu)))

    -

    根据此规则实现get_pin回调即可。具体板子上哪个引脚是哪个port,详见milkv的schematic book。

    -

    中断

    回调执行

    rtt提供了很好用的中断回调。参考别的bsp以及以前的经验,很容易知道attach_irqdetach_irq就是注册和注销回调函数。那么,回调是什么时候被调用的呢?查阅其他bsp,也可得知它这是采取了一个非常巧妙的委托:在rtt给的rt_hw_interrupt_install(DWAPB_GPIOE_IRQNUM, rt_hw_gpio_isr, RT_NULL, "gpio");注册的rt_hw_gpio_isr中做即可。这个层层外包的思想让我不禁想起Linux的调度类机制和用户态调度框架的实现原理,实在是牛逼至极。

    -

    为了记录所有回调签名,我们需要为每个gpio组件整一个数据结构:

    -
    static struct dwapb_event
    {
    void (*(hdr[DWAPB_GPIO_NR]))(void *args);
    void *args[DWAPB_GPIO_NR];
    rt_uint8_t is_both_edge[DWAPB_GPIO_NR];
    } _dwapb_events[DWAPB_GPIO_PORT_NR];
    - -

    rt_hw_gpio_isr中:

    -
      -
    1. 根据硬件寄存器判断是否发生中断
    2. -
    3. 调用相应回调
    4. -
    5. 清除中断位表明完成中断处理
    6. -
    -

    即可。

    -

    both-edge实现

    这个是抄自linux。本质逻辑就是先设个上升沿触发,然后在rt_hw_gpio_isr执行完回调后再改成反方向也即下降沿触发,以此类推。不得不说确实帅。

    -

    I2C

    Wait todo…

    -]]> - - intern - -
    - - git使用记录 - /2023/10/07/git/ - -

    记录一些git的原理学习,以及工作学习中遇到的一些git的操作问题。

    -
    -

    操作

    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
    +

    milkv上有ABCDE五个gpio组件:

    +
    gpio@03020000 {
    bank-name = "porta";
    };

    gpio@03021000 {
    bank-name = "portb";
    };

    gpio@03022000 {
    bank-name = "portc";
    };

    gpio@03023000 {
    bank-name = "portd";
    };

    gpio@05021000 {
    bank-name = "porte";
    };
    -

    然后更新ref到某个版本号

    -
    cd ..
    tail -n 2 .git/logs/refs/heads/master
    git show xxxx(版本号)
    git update-ref HEAD xxxx(版本号)
    git fsck
    +

    但是,在实现中,这五个gpio组件会被注册为同一个设备,而不是像uart那样有多少个就注册多少个:

    +
    rt_device_pin_register("gpio", &_dwapb_ops, RT_NULL);
    -

    如果还不能用,继续:

    -
    rm .git/index
    git reset
    git fsck
    +

    外界交互

    应用程序会通过引脚名,使用rt_pin_get获取驱动内部自定义的引脚号,然后就可以通过引脚号对其进行模式设置和读写:

    +
    int DUO_LED = rt_pin_get("C24");
    rt_pin_mode(DUO_LED, PIN_MODE_OUTPUT);
    rt_pin_write(DUO_LED, PIN_LOW);
    -

    我到这里之后显示:

    -

    image-20231030101123181

    -

    继续执行:

    -
      -
    1. 修复 refs/remotes/origin/master:

      -
      bashCopy codegit update-ref -d refs/remotes/origin/master
      git fetch origin
      +

      寄存器

      配置

      这坨宏我是从linux里搬过来的。各个寄存器的含义可以详见data book,在此不多赘述。

      +

      访问

      在rtt中,外设地址也是统一编址到内存地址空间的。由其它gpio实现可知,当开启RT_USING_LWP(轻量级进程支持)时,外设地址不再是一一映射,需要我们手动在init中调用ioremap进行映射:

      +
      #ifdef RT_USING_LWP
      #define BSP_IOREMAP_GPIO_DEVICE(no) \
      rt_ioremap((void *)(DWAPB_GPIOA_BASE + (no) * DWAPB_GPIO_SIZE), DWAPB_GPIO_SIZE);

      dwapb_gpio_base = (rt_size_t)BSP_IOREMAP_GPIO_DEVICE(0);
      BSP_IOREMAP_GPIO_DEVICE(1);
      BSP_IOREMAP_GPIO_DEVICE(2);
      BSP_IOREMAP_GPIO_DEVICE(3);
      dwapb_gpio_base_e = (rt_size_t)rt_ioremap((void *)DWAPB_GPIOE_BASE, DWAPB_GPIO_SIZE);
      #endif
      -

      这将删除损坏的 origin/master 引用,然后从远程仓库重新获取。

      -
    2. -
    3. 修复 dangling blob:

      -

      如果 git fsck 显示了 dangling blob,你可以尝试删除这些对象:

      -
      bashCopy codegit reflog expire --expire=now --all
      git gc --prune=now
      +

      然后,之后就可以在各个方法中借助这两个函数传入寄存器地址直接进行读写了:

      +
      rt_inline rt_uint32_t dwapb_read32(rt_ubase_t addr)
      {
      return HWREG32(addr);
      }

      rt_inline void dwapb_write32(rt_ubase_t addr, rt_uint32_t value)
      {
      HWREG32(addr) = value;
      }
      #define HWREG32(x) (*((volatile rt_uint32_t *)(x)))
      -

      这将清理无用的 dangling 对象。

      -
    4. -
    -

    成功。

    -

    原理

      -
    1. merge与rebase的差异

      -

      merge:

      -

      merge

      -

      rebase:

      -

      rebase

      -
    2. -
    3. -
    -]]> - - - 开源的第一个月 - /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的基本介绍,详情可见其文档中心。我印象最深(也是开发过程中接触得最多的)的几个点有:

    +

    引脚映射

    摸索了半天,最终还是猜测这个引脚映射规定应该是自己决定的,于是我就从别的bsp那边把规则搬了过来:

    +
    // port && no -> pin
    #define PIN_NUM(port, no) (((((port) & 0xFu) << 8) | ((no) & 0xFFu)))
    // pin -> port
    #define PIN_PORT(pin) ((uint8_t)(((pin) >> 8) & 0xFu))
    // pin -> no
    #define PIN_NO(pin) ((uint8_t)((pin) & 0xFFu))
    + +

    例如,“C24”就可转化为((((('C' - 'A') & 0xFu) << 8) | ((24) & 0xFFu)))

    +

    根据此规则实现get_pin回调即可。具体板子上哪个引脚是哪个port,详见milkv的schematic book。

    +

    中断

    回调执行

    rtt提供了很好用的中断回调。参考别的bsp以及以前的经验,很容易知道attach_irqdetach_irq就是注册和注销回调函数。那么,回调是什么时候被调用的呢?查阅其他bsp,也可得知它这是采取了一个非常巧妙的委托:在rtt给的rt_hw_interrupt_install(DWAPB_GPIOE_IRQNUM, rt_hw_gpio_isr, RT_NULL, "gpio");注册的rt_hw_gpio_isr中做即可。这个层层外包的思想让我不禁想起Linux的调度类机制和用户态调度框架的实现原理,实在是牛逼至极。

    +

    为了记录所有回调签名,我们需要为每个gpio组件整一个数据结构:

    +
    static struct dwapb_event
    {
    void (*(hdr[DWAPB_GPIO_NR]))(void *args);
    void *args[DWAPB_GPIO_NR];
    rt_uint8_t is_both_edge[DWAPB_GPIO_NR];
    } _dwapb_events[DWAPB_GPIO_PORT_NR];
    + +

    rt_hw_gpio_isr中:

      -
    1. 接口设计

      -

      与Linux一样,rtt也采用了精简的接口设计。

      -
    2. -
    3. 自动初始化机制

      -

      帅的一匹,具体详见

      -
    4. +
    5. 根据硬件寄存器判断是否发生中断
    6. +
    7. 调用相应回调
    8. +
    9. 清除中断位表明完成中断处理
    -

    其余的只能说不甚了解,还有待挖掘。

    -

    配置环境

    由于确实对这种东西毫无所知,所以配环境这个过程也是比较漫长,而且很折磨很痛苦(。

    -

    硬件方面,一开始拿到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设备驱动等的学习,总体来说还是十分甚至九分地开心。希望下个月能再接再厉。

    +

    即可。

    +

    both-edge实现

    这个是抄自linux。本质逻辑就是先设个上升沿触发,然后在rt_hw_gpio_isr执行完回调后再改成反方向也即下降沿触发,以此类推。不得不说确实帅。

    +

    I2C

    Wait todo…

    ]]> - mylife + intern - rtt硬件环境搭建 - /2023/10/12/rtt%E7%A1%AC%E4%BB%B6%E7%8E%AF%E5%A2%83%E6%90%AD%E5%BB%BA/ - SDK/内核编译

    编译内核和sdk时到没有太大问题,都能靠gpt or 修改menuconfig解决。比较棘手的果然还是只能靠自己摸索硬件hhh

    -

    烧录

    这个也是狠狠折磨了我许久,gpt也一直满嘴跑火车,毕竟一开始串口连错了所以一直以为自己是烧录没对,整了半天【而且还是正确的操作反反复复尝试……仿佛坐牢】。不过这其中也学到了挺多。

    -

    f2753b5e6a40f9fea328223b197ede3

    -

    具体要做的是:

    -
    sudo fdisk /dev/sdb
    - + 内核编译 + /2023/08/12/kernel_compile/ + 我们在开发过程中所用的具体硬件配置如下:

    -

    关于fdisk及part这样的分区工具,这篇文章介绍得很详细:Linux 系统中关于磁盘分区工具的使用

    +

    CPU: Intel(R) Core(TM) i7-8700 CPU @ 3 with hyper-threading

    +

    Memory: 14GB

    +

    OS: Ubuntu 22.04.3 LTS

    +

    ​ with Linux kernel 6.4.0+(COS) && 6.4.0-rc3+(EXT) && 5.11.0+(ghOSt)

    -

    然后进入后,n命令创建新分区,d删除现有分区。我们需要用n创建两个分区(大小随意),第一个用于存放bin文件。创建完分区之后,使用a命令将分区1标记为Boot,并且使用t将分区1的类型修改为W95 FAT32 (LBA),随后就可以保存退出了。具体流程及操作结果如下:

    -
    $ sudo fdisk /dev/sdb

    Welcome to fdisk (util-linux 2.37.2).
    Changes will remain in memory only, until you decide to write them.
    Be careful before using the write command.

    Command (m for help): p

    Disk /dev/sdb: 29.72 GiB, 31914983424 bytes, 62333952 sectors
    Disk model: Storage Device
    Units: sectors of 1 * 512 = 512 bytes
    Sector size (logical/physical): 512 bytes / 512 bytes
    I/O size (minimum/optimal): 512 bytes / 512 bytes
    Disklabel type: dos
    Disk identifier: 0xd4c6d64f

    Command (m for help): n
    Partition type
    p primary (0 primary, 0 extended, 4 free)
    e extended (container for logical partitions)
    Select (default p):

    Using default response p.
    Partition number (1-4, default 1):
    First sector (2048-62333951, default 2048):
    Last sector, +/-sectors or +/-size{K,M,G,T,P} (2048-62333951, default 62333951): +128M

    Created a new partition 1 of type 'Linux' and of size 128 MiB.
    Partition #1 contains a vfat signature.

    Do you want to remove the signature? [Y]es/[N]o: yes

    The signature will be removed by a write command.

    Command (m for help): n
    Partition type
    p primary (1 primary, 0 extended, 3 free)
    e extended (container for logical partitions)
    Select (default p):

    Using default response p.
    Partition number (2-4, default 2):
    First sector (264192-62333951, default 264192):
    Last sector, +/-sectors or +/-size{K,M,G,T,P} (264192-62333951, default 62333951):

    Created a new partition 2 of type 'Linux' and of size 29.6 GiB.

    Command (m for help): t
    Partition number (1,2, default 2): 0c
    Value out of range.
    Partition number (1,2, default 2): 1
    Hex code or alias (type L to list all): 0c

    Changed type of partition 'Linux' to 'W95 FAT32 (LBA)'.

    Command (m for help): a
    Partition number (1,2, default 2): 1

    The bootable flag on partition 1 is enabled now.

    Command (m for help): p
    Disk /dev/sdb: 29.72 GiB, 31914983424 bytes, 62333952 sectors
    Disk model: Storage Device
    Units: sectors of 1 * 512 = 512 bytes
    Sector size (logical/physical): 512 bytes / 512 bytes
    I/O size (minimum/optimal): 512 bytes / 512 bytes
    Disklabel type: dos
    Disk identifier: 0xd4c6d64f

    Device Boot Start End Sectors Size Id Type
    /dev/sdb1 * 2048 264191 262144 128M c W95 FAT32 (LBA)
    /dev/sdb2 264192 62333951 62069760 29.6G 83 Linux

    Filesystem/RAID signature on partition 1 will be wiped.

    Command (m for help): w
    The partition table has been altered.
    Calling ioctl() to re-read partition table.
    Syncing disks.
    +

    COS环境搭建

    COS可以部署在Linux物理机和虚拟机上,但建议部署在Linux物理机以获取更好的性能效果。下文将详细介绍环境搭建的详细步骤。

    +

    COS内核

    内核编译

    安装内核编译所需包:

    +
    sudo apt-get update && sudo apt-get install build-essential gcc g++ make libncurses5-dev libssl-dev bison flex bc libelf-dev
    -

    然后更新分区表,格式化文件系统,复制bin和sd文件、弹出sd卡:

    -
    sudo partprobe 
    sudo mkfs.vfat -F 32 /dev/sdb1
    sudo mkfs.vfat -F 32 /dev/sdb2
    sudo mount /dev/sdb1 /mnt/sdb1
    sudo cp boot.sd fip.bin /mnt/sdb1
    sudo umount /mnt/sdb1
    sudo eject /dev/sdb
    +

    克隆COS内核:

    +
    git clone https://github.com/shootfirst/cos_kernel.git
    cd cos_kernel/
    + +

    生成内核编译配置文件:

    +
    make localmodconfig
    + +

    修改.config文件:

    +
    vim .config
    -

    就烧录ok了。

    -

    与此同时也顺便学了下Windows磁盘管理,感觉也挺方便。

    -

    串口连接

    不得不说十分惨痛……

    -

    也是因为我太狂妄自大不仔细了,一直以为RX就得连RX,TX就得连TX,然后下午焦灼之中才突然想起来这俩是不是以前教过得反着连……搜了一下果然是草。

    -

    这个错误害我起码花了三个小时在纠结烧录问题,一直以为没输出是因为烧录不对,发现结果那一刻我真没蚌珠。哎,以后还是别对自己太自信,毕竟本质上这种东西不是专业的学得也不够扎实,做什么都得先查一下该怎么做。

    -

    另一个问题是,乱码:

    -

    58c63791e1b4cac06a827f0ffa6b329

    -

    好像是说使用了个ch340的usb-ttl,似乎这个东西波特率有点问题不准确,具体不大懂:CH340G U-BOOT阶段乱码 总之最后简单粗暴地靠修改波特率为117200解决了。

    -

    然后别的倒是没什么问题,虽然摸索也花了点时间,都是小事。对着这张图参照就行了:

    -

    mmexport1697102534057

    -

    开发过程

    感觉有一些东西,还是比较靠悟性23333在此记录些开发过程中的小发现。

      -
    1. 关于引脚

      -

      两个引脚可以通过image-20231019162502551这玩意连起来,这样就能使一个引脚的output作为另一个引脚的input。

      -

      这是我在开发gpio中断时意识到的。这个道理虽然非常简单,但对于零基础胡乱摸索的我,知道这个还是需要一些灵光一闪。

      -
    2. -
    3. +
    4. 删除PSI监测

      +

      查找CONFIG_PSI,将其对应行修改为:

      +
      CONFIG_PSI=n
    5. +
    6. 删除系统吊销密钥

      +

      查找CONFIG_PSI,将其对应行修改为:

      +
      CONFIG_SYSTEM_REVOCATION_KEYS=""
    -]]>
    -
    - - Operating system interface - /2023/01/10/xv6$chap1/ - Operating system interface

    本节大概是在讲操作系统的接口,系统调用占了很大一部分。

    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    系统调用描述
    int fork()创建一个进程,返回子进程的PID
    int exit(int status)终止当前进程,并将状态报告给wait()函数。无返回
    int wait(int *status)等待一个子进程退出; 将退出状态存入*status; 返回子进程PID。
    int kill(int pid)终止对应PID的进程,返回0,或返回-1表示错误
    int getpid()返回当前进程的PID
    int sleep(int n)暂停n个时钟节拍
    int exec(char *file, char *argv[])加载一个文件并使用参数执行它; 只有在出错时才返回
    char *sbrk(int n)按n 字节增长进程的内存。返回新内存的开始
    int open(char *file, int flags)打开一个文件;flags表示read/write;返回一个fd(文件描述符)
    int write(int fd, char *buf, int n)从buf 写n 个字节到文件描述符fd; 返回n
    int read(int fd, char *buf, int n)将n 个字节读入buf;返回读取的字节数;如果文件结束,返回0
    int close(int fd)释放打开的文件fd
    int dup(int fd)返回一个新的文件描述符,指向与fd 相同的文件
    int pipe(int p[])创建一个管道,把read/write文件描述符放在p[0]和p[1]中
    int chdir(char *dir)改变当前的工作目录
    int mkdir(char *dir)创建一个新目录
    int mknod(char *file, int, int)创建一个设备文件
    int fstat(int fd, struct stat *st)将打开文件fd的信息放入*st
    int stat(char *file, struct stat *st)将指定名称的文件信息放入*st
    int link(char *file1, char *file2)为文件file1创建另一个名称(file2)
    int unlink(char *file)删除一个文件
    -

    表1.2:xv6系统调用(除非另外声明,这些系统调用返回0表示无误,返回-1表示出错)

    -

    Process and memory

    fork

    int pid = fork();
    if(pid > 0){
    printf("parent: child's pid = %d\n",pid);
    pid = wait(0);
    printf("child %d is done.\n",pid);
    } else if(pid == 0){
    printf("child : exiting\n");
    } else {
    printf("fork error\n");
    }
    +

    然后就可以进行内核编译:

    +
    make -j12 && sudo make modules_install && sudo make install
    -

    这是一个利用fork的返回值对于父子进程来说不同这一特点进行编写的例程。其中比较不熟的还是wait(0)这一句的用法。这点具体可以看书中笔记和上面的系统调用表。

    -

    exec

    exec是一个系统调用,它跟exe文件被执行的原理密切相关。当程序调用exec,就会跳转到exec参数文件去执行,原程序exec下面的指令都不再被执行,除非exec因错误而退出。

    -

    exec与fork

    由shell的源码中main函数这一段

    -
    // Read and run input commands.
    while(getcmd(buf, sizeof(buf)) >= 0){
    if(buf[0] == 'c' && buf[1] == 'd' && buf[2] == ' '){
    // Chdir must be called by the parent, not the child.
    buf[strlen(buf)-1] = 0; // chop \n
    if(chdir(buf+3) < 0)
    fprintf(2, "cannot cd %s\n", buf+3);
    continue;
    }
    if(fork1() == 0)
    runcmd(parsecmd(buf));
    wait(0);
    }
    exit(0);
    -
    void runcmd(struct cmd *cmd)
    {
    if(cmd == 0)
    exit(1);

    switch(cmd->type){
    ...
    case EXEC:
    ecmd = (struct execcmd*)cmd;
    exec(ecmd->argv[0], ecmd->argv);
    fprintf(2, "exec %s failed\n", ecmd->argv[0]);
    break;
    ...

    exit(0);
    }
    -

    可以看到shell其实本质上就是这样的架构架构:

    -
    while(true){
    if(读到了command&&fork()==0){
    exec(command);
    printf("失败信息");
    }
    wait(0);
    }
    +

    修改grub

    打开grub配置文件:

    +
    sudo vim /etc/default/grub
    + +

    进行以下修改:

    +
      +
    1. 注释GRUB_TIMEOUT_STYLE=hidden
    2. +
    3. GRUB_CMDLINE_LINUX_DEFAULT设置为”text”
    4. +
    5. GRUB_TIMEOUT修改成30
    6. +
    +

    然后保存退出,更新grub:

    +
    sudo update-grub
    + + + +

    进入COS内核

    完成上述步骤后,重启虚拟机:

    +
    sudo reboot
    + +

    在进入GRUB界面时选择Advanced Ubuntu,然后选择内核版本6.4.0+即可。

    +

    COS用户态

    在完成COS内核编译,并进入COS内核之后,就完成了COS的基本环境搭建。接下来,将介绍如何搭建COS用户态环境,从而运行Shinjuku Scheduler和RocksDB实验。

    +

    首先,确保所处内核正确:

    +
    $ uname -r
    6.4.0+
    + +

    依赖安装

    apt包

    安装编译用户态需要的包:

    +
    sudo apt-get install cmake python2 python3 libtbb-dev libsnappy-dev zlib1g-dev libgflags-dev libbz2-dev liblz4-dev libzstd-dev
    + + + +

    RocksDB

    获取RocksDB 6.15.5版本release:

    +
    wget https://github.com/facebook/rocksdb/archive/refs/tags/v6.15.5.tar.gz
    tar -xvf v6.15.5.tar.gz
    cd rocksdb-6.15.5/
    -

    也即父进程创建出子进程来执行command,并且父进程等待子进程执行完再继续等待输入。

    -

    可以看到,fork和exec的使用是非常紧密的,联合使用也是非常顺理成章的。那么,如果干从fork的exec的对于内存管理的原理来讲,就会不免产生一点问题。

    -
    -

    问题描述:

    -

    fork的内存原理,实质上是开辟一片新的与父进程等大的内存空间,然后把父进程的数据都copy一份进这个新内存空间。exec的原理是用一片可以容纳得下文件指令及其所需空间的内存空间去替代调用进程原有的那片内存空间。

    -

    可以看到,如果fork和exec接连使用,理论上其实是会产生一点浪费的,fork创建子进程复制完了一片内存空间,这片新复制的内存空间又马上被扔掉了,取而代之的用的是exec的内存空间。

    -
    -

    为了解决这个问题,kernel使用了copy-on-write技术优化。

    -

    I/O and File descriptors

    文件描述符

    句柄就是一个int值,它代表了一个由内核管理的,可以被进程读写的对象.

    -
    -

    A process may obtain a file descriptor by opening a file, directory, or device, or by creating a pipe, or by duplicating an existing descriptor.

    -
    -

    每个进程的其三个句柄有默认值:

    -

    By convention, a process reads from file descriptor 0 (standard input), writes output to file descriptor 1 (standard output), and writes error messages to file descriptor 2 (standard error).

    +

    注:测试得发现本RocksDB负载同最新版本RocksDB不兼容,故而建议使用上述命令对应版本,也即v6.15.5。

    -

    句柄0对应着standard input,1对应着standard output,2对应着standard error。

    -

    read、write

    read和write的参数都是句柄,buf,读/写长度。都会导致文件指针的移动。使用如下例程【类似cat的原理】:

    -
    char buf[512];
    int n;

    for(;;){
    n = read(0);//从标准输入读
    if(n == 0){
    break;
    }
    if(n < 0){
    fprintf(2,"read error\n");
    exit(1);
    }
    if(write(1,buf,n) != n){//向标准输出写
    fprintf(2,"write error\n");
    exit(1);
    }
    }
    +

    修改CMakeLists:

    +
    vim CMakeLists.txt
    -

    close

    close函数释放了一个句柄,以后它释放掉的这个句柄就可以被用来表示别的文件了。

    -

    open

    open函数会给参数的file分配一个句柄。这个句柄通常是目前空闲的句柄中值最小的那个。

    -

    重定向的实现

    char *argv[2];

    argv[0] = "cat";
    argc[1] = 0;
    if(fork() == 0){
    close(0);
    open("input.txt",O_RDONLY);
    exec("cat",argv);
    }
    +
      +
    1. 搜索WITH_TBB,将这一项改为ON:

      +
      option(WITH_TBB "build with Threading Building Blocks (TBB)" ON)
    2. +
    3. 搜索ROCKSDB_LITE 确保这一项为OFF

      +
      option(ROCKSDB_LITE "Build RocksDBLite version" OFF)
    4. +
    +

    保存退出后进行编译安装:

    +
    mkdir build && cd build && cmake .. && make -j12 && sudo make install
    -

    xv6的重定向实现跟这个原理差不多:

    -
    case REDIR:
    rcmd = (struct redircmd*)cmd;
    close(rcmd->fd);
    if(open(rcmd->file, rcmd->mode) < 0){
    fprintf(2, "open %s failed\n", rcmd->file);
    exit(1);
    }
    runcmd(rcmd->cmd);
    break;
    -

    共享偏移量

    fork出来的父子进程同一个句柄对同一个文件的偏移量是相同的,这个原理应该是因为,父子进程共享的是文件句柄这个结构体对象本身,也就是拷贝的时候是浅拷贝而不是深拷贝。

    -
    if(fork() == 0) {
    write(1, "hello ", 6);
    exit(0);
    } else {
    wait(0);
    write(1, "world\n", 6);
    }
    -

    dup

    dup系统调用复制一个现有的文件描述符,返回一个引用自同一个底层I/O对象的新文件描述符。

    -

    dup和open一样,都是会占用一个新的句柄的,而且都是优先分配数值小的。比如说fd = dup(3),得到fd=4,那么结果就是句柄3和句柄4指向同一个文件,并且偏移量一样。

    -

    dup可以让这样的指令变得可以实现:

    -
    ls existing-file non-existing-file > tmp1 2>&1
    +

    运行COS

    克隆COS用户态代码:

    +
    git clone https://gitlab.eduxiji.net/202318123111334/cos_userspace.git
    cd cos_userspace
    -
    -

    这个指令的意思是,先把stderr的结果重定向到stdout,再把stdout的结果重定向到tmp1中。

    -

    关于2>&1的解释,可以看这个 shell中的”2>&1”是什么意思?

    +

    编译COS用户态:

    +
    mkdir build && cd build && cmake .. && make -j($nproc)
    + +

    然后就可以开始运行COS用户态了。在此以Fifo Scheduler为例。

    +

    打开两个终端,在其中一个运行Fifo Scheduler:

    +
    pwd # 确保在cos_userspace/build目录下
    sudo ./fifo_scheduler
    + +

    另一个运行GTest测试:

    +
    pwd # 确保在cos_userspace/build目录下
    sudo ./simple_exp
    + +

    等待测试完成即可。

    +

    若要运行展示中所提到的测试,可详细见性能测试教程

    +

    EXT环境搭建

    EXT内核

    依赖安装

    llvm

    SCHED-EXT内核由于用到新eBPF特性,故而编译需要用到还未发行到apt包管理器的clang最新版本,因此需要手动拉取并且编译一些依赖包。

    +
    # 克隆llvm仓库
    git clone --depth=1 https://github.com/llvm/llvm-project.git

    # 编译llvm项目
    cd llvm-project
    mkdir build
    cd build
    cmake -DLLVM_ENABLE_PROJECTS=clang -DCMAKE_BUILD_TYPE=Release -G "Unix Makefiles" ../llvm
    make -j($nproc)

    # 在~/.bashrc文件添加
    export PATH=$PATH:/yourpath/llvm-project/build/bin

    # 确认
    echo $PATH
    clang --version # >= 17.0.0
    llvm-config --version # >= 17.0.0
    + + + +

    pahole

    # 克隆pahole项目
    git clone git://git.kernel.org/pub/scm/devel/pahole/pahole.git

    # 编译pahole项目
    cd pahole/
    mkdir build
    cd build
    cmake -D__LIB=lib -DBUILD_SHARED_LIBS=OFF ..
    make
    sudo make install

    # 检查
    $ pahole --version # >= v1.25
    + + + +

    rust-nightly

    curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
    rustup toolchain install nightly
    rustup default nightly
    # 查看rust版本
    rustc --version
    + + + +

    内核编译

    安装内核编译所需包:

    +
    sudo apt-get update && sudo apt-get install build-essential gcc g++ make libncurses5-dev libssl-dev bison flex bc libelf-dev
    + +

    克隆COS内核:

    +
    git clone https://gitlab.eduxiji.net/202318123111334/ext-kernel.git
    cd ext-kernel/
    + +

    生成内核编译配置文件:

    +
    make localmodconfig
    + +

    对生成的.config配置文件做如下修改:

    +
      +
    1. CONFIG_SYSTEM_TRUSTED_KEYS="debian/canonical-certs.pem"修改为CONFIG_SYSTEM_TRUSTED_KEYS=""
    2. +
    3. 添加CONFIG_SCHED_CLASS_EXT=y
    4. +
    5. 确保CONFIG_DEBUG_INFO以及CONFIG_DEBUG_INFO_BTF为开启状态
    6. +
    +

    编译安装内核:

    +
    make -j12 && sudo make modules_install && sudo make install
    + +

    修改grub

    详见COS环境搭建的对应部分。在此不做赘述。

    +

    EXT用户态

    pwd # 确保在ext-kernel/项目根目录下
    cd ext-kernel/tools
    + +

    随后,我们需要替换原有EXT使用示例为我们搭建的EXT用户态框架。

    +
    rm -rf sched_ext/
    git clone https://gitlab.eduxiji.net/202318123111334/proj134-cfs-based-userspace-scheduler.git sched_ext
    + +

    然后就可以运行EXT用户态框架了。

    +

    若要运行展示中所提到的测试,可详细见性能测试教程

    +

    ghOSt环境搭建

    由于测试中需要以ghOSt作为比较对象,故在运行测试之前,必须搭建ghOSt环境。

    +

    ghOSt内核

    内核编译

    安装内核编译所需包:

    +
    sudo apt-get update && sudo apt-get install build-essential gcc g++ make libncurses5-dev libssl-dev bison flex bc libelf-dev
    + +

    克隆ghOSt内核:

    +
    git clone https://github.com/google/ghost-kernel
    cd ghost-kernel/
    + +

    生成内核编译配置文件:

    +
    make localmodconfig
    + +

    对生成的.config配置文件做如下修改:

    +
      +
    1. CONFIG_SYSTEM_TRUSTED_KEYS="debian/canonical-certs.pem"修改为CONFIG_SYSTEM_TRUSTED_KEYS=""
    2. +
    3. 添加CONFIG_SCHED_CLASS_GHOST=y
    4. +
    +

    编译安装内核:

    +
    make -j12 && sudo make modules_install && sudo make install
    + + + +

    修改grub

    详见COS环境搭建的对应部分。在此不做赘述。

    +

    ghOSt用户态

    依赖安装

    bazel

    +

    详细安装可参照官方文档Installing Bazel on Ubuntu,在此仅给出其中第一种方法。

    -

    这个的实现就要用到dup了。我们会fork一个子进程,在子进程里面close(2),然后再dup(1)。这样一来,我们就成功实现了句柄1和2指向同一个文件

    -

    Pipe

    使用

    int pipe(int p[]) 创建一个管道,把read/write文件描述符放在p[0]和p[1]中

    -
    int p[2];
    char* argv[2];

    argv[0] = "wc";
    argv[1] = 0;

    pipe(p);
    if(fork() == 0){
    close(0);
    dup(p[0]);
    close(p[0]);
    close(p[1]);
    } else{
    close(p[0]);
    write(p[1],"hello world\n",12);
    close(p[1]);
    }
    +

    将Bazel分发URL添加为软件包来源

    +
    # 在~目录下
    sudo apt install apt-transport-https curl gnupg -y
    curl -fsSL https://bazel.build/bazel-release.pub.gpg | gpg --dearmor >bazel-archive-keyring.gpg
    sudo mv bazel-archive-keyring.gpg /usr/share/keyrings
    echo "deb [arch=amd64 signed-by=/usr/share/keyrings/bazel-archive-keyring.gpg] https://storage.googleapis.com/bazel-apt stable jdk1.8" | sudo tee /etc/apt/sources.list.d/bazel.list
    -

    完成了父进程-pipe-子进程的一个重定向。

    -

    pipe是阻塞的生产者消费者模式。对管道的read,在没有数据输入时会阻塞,直到读到数据,或者所有的write方向都被关闭。示例代码中,如果不使用pipe就需要显示close(p[0]) close(p[1]),正是为了防止没有数据输入时write方向不为0导致死锁的情况出现。

    -

    实现管道命令

    管道命令的实现正是通过pipe。

    -

    执行原理就是,创建两个子进程分别执行左右两侧的句子,然后左侧子进程的out重定向到pip的write,右侧子进程的in重定向到pip的read。

    -
     case PIPE:
    pcmd = (struct pipecmd*)cmd;
    if(pipe(p) < 0)
    panic("pipe");
    //左
    if(fork1() == 0){
    close(1);
    dup(p[1]);
    close(p[0]);
    close(p[1]);
    runcmd(pcmd->left);
    }
    //右
    if(fork1() == 0){
    close(0);
    dup(p[0]);
    close(p[0]);
    close(p[1]);
    runcmd(pcmd->right);
    }
    //中
    close(p[0]);
    close(p[1]);
    //wait
    wait(0);
    wait(0);
    break;
    +

    安装和更新Bazel

    +
    sudo apt update && sudo apt install bazel
    # 将bazel升级到最新版本
    sudo apt update && sudo apt full-upgrade
    -

    这实际上是二叉树的左右中递归过程。

    -
    -

    附:对于管道命令的解读

    -
    cat a.txt | echo
    -

    我的本意是觉得,这意思就是把cat a.txt的输出连到echo的输入,这个命令结果跟cat a.txt是没什么差的。但具体执行出来发现最后的结果却是跟:

    -
    echo
    -

    这个指令的效果是一样的,也就是cat a.txt的output,即echo的input完全被丢弃了。

    -

    我想这是因为,echo这个命令的执行过程并没有用到stdin,仅仅用到了参数,也就是说管道read端的接入对它并没有什么影响。

    -

    这也是为啥

    -
    sleep 10 | echo hi
    +

    apt包

    sudo apt update
    sudo apt install libnuma-dev libcap-dev libelf-dev libbfd-dev gcc clang-12 llvm zlib1g-dev python-is-python3 libabsl-dev
    -

    这个命令最后的结果是,秒速出hi,然后等待10s后结束,了。由于echo的输出与stdin没有关系,所以,echo不会阻塞读入stdin,等待管道关闭,而是会即刻输出hi。

    + + +

    运行ghOSt

    克隆ghOSt用户态:

    +
    git clone https://gitlab.eduxiji.net/202318123111334/ghost_userspace.git
    cd ghost-userspace
    + +]]> + + + Lab4 TCPConnection + /2023/02/25/cs144$lab4/ + Lab4 TCPConnection

    心得

    耗时情况

    【长舒一口气】

    +

    最开始先记录下总体耗时情况吧。本次实验共耗费我16h+【不包括接下来写笔记的时间23333】,共耗费三个工作日。第一天看完了指导书,写完了代码,过掉了#45 reorder之前的所有测试。第二天过掉了#55 t_ucS_1M_32k之前的所有测试,直到第三天才过完了所有测试。

    +

    我觉得这整个过程还是挺有意义的,每天都有新的进展,看到test case越过越多是真的很高兴。但是可以说第二天以来就都是面向测试用例改bug了,非常折磨非常坐牢,既要去再次理清之前写过的shit山,又得搞清楚很多让人一头雾水不知从何下手的地方。但总之,这三天很充实,并不会让人觉得心累。

    +

    放个通关截图吧,感人至深。

    +

    image-20230305160608116

    +

    思路

    TCPConnection中具体要做什么,指导书已经写得很详细了,跟着指导书就行。代码部分还是不折磨的,思路直观清晰。

    +

    指导书内容

    +

    Here are the basic rules the TCPConnection has to follow:

    -

    管道实际上就相当于:

    -
    echo hello world | wc
    echo hello world > /tmp/xyz; wc < /tmp/xyz
    +
      +
    1. Receiving segments

      +

      大概是在segment_received中做

      +

      image-20230303103640065

      +
        +
      1. 检查RST flag

        +

        如果RST被设置,sets both the inbound and outbound streams to the error state,杀死当前connection

        +

        return;

        +

        具体实现中,杀死connection可以置_linger_after_streams_finish为false。 the inbound and outbound streams对应着receiver和sender里的stream。让它们都处于error状态,只需设置ByteStream中的error字段

        +
      2. +
      3. 如果收到的segment with an invalid sequence number,connection需要发送empty segment应答

        +

        image-20230303110238265

        +
      4. +
      5. 转发segment给receiver

        +
      6. +
      7. 如果ACK,则把ackno和win_size给sender

        +
      8. +
      +
    2. +
    3. Sending segments

      +
        +
      1. 任何时候sender把segment放进其out流,你都要从中取出来
      2. +
      3. 从receiver处获取ackno和window_size,填入segment中
      4. +
      5. 放到自己的segment_out中
      6. +
      +

      从上述表述中,我们需要注意两点:

      +
        +
      1. 顺带ACK

        +

        可以看到,这跟我们上课的时候所学的一样,是“顺带ACK”,也即ACK报文并非独立发送,而是在下一次要发送其他数据报文的时候携带发送。这也一定程度上使得ack报文发送不会太频繁也不会太稀疏。

        +
      2. +
      3. 一定要经由sender

        +

        我们如果想要发送一个报文,一定得先把它存入sender中,再从sender的segment_out中取出来。这样做的目的是把该报文列入sender的超时重传管辖范围,你如果直接把报文发送到自己的segment_out中,就无法管理其超时重传了

        +
      4. +
      +
    4. +
    5. When time passes

      +

      tick()

      +
        +
      1. 调用sender的tick()
      2. +
      3. 检查sender的连续超时重传次数,如果大于MAX RETX ATTEMPTS,则关闭连接,并且发送RST标志的空报文
      4. +
      5. end the connection cleanly if necessary
      6. +
      +
    6. +
    +

    再注意一点对于connection的关闭。它要求有一个time pass

    +

    image-20230303111131650

    +

    image-20230303112817891

    +

    第一点挺好实现的,第二点需要在析构函数中检测。

    +

    最后的5.1部分值得一看。

    +

    接口说明

    TCPConnection的public函数接口定义以及具体要做什么如下。结合上面的指导书内容,TCPConnection的实现就很简单了,我就不多bb了。

    +
     private:
    TCPConfig _cfg;
    // 一个endpoint可以同时作为sender和receiver。
    TCPReceiver _receiver{_cfg.recv_capacity};
    TCPSender _sender{_cfg.send_capacity, _cfg.rt_timeout, _cfg.fixed_isn};

    //! outbound queue of segments that the TCPConnection wants sent
    // 把要发送的segment放在这里就行了
    std::queue<TCPSegment> _segments_out{};

    //! Should the TCPConnection stay active (and keep ACKing)
    //! for 10 * _cfg.rt_timeout milliseconds after both streams have ended,
    //! in case the remote TCPConnection doesn't know we've received its whole stream?
    bool _linger_after_streams_finish{true};

    public:
    // 也许需要调用TCPSender的fill_window(),然后从其segment_out中取出来,再发送给自己的segment_out
    // Initiate a connection by sending a SYN segment初始化connection并且发送SYN
    void connect();

    /* 这几个都很好实现,都很直观,只需调sender和receiver的API就行 */
    // 由上层socket调用,data路径 socket->connection->sender.stream_in().write()
    //! \brief Write data to the outbound byte stream, and send it over TCP if possible
    //! \returns the number of bytes from `data` that were actually written.
    size_t write(const std::string &data);
    //! \returns the number of `bytes` that can be written right now.
    size_t remaining_outbound_capacity() const;
    //! \brief Shut down the outbound byte stream (still allows reading incoming data)
    void end_input_stream();
    //! \brief The inbound byte stream received from the peer
    ByteStream &inbound_stream() { return _receiver.stream_out(); }
    // number of bytes sent and not yet acknowledged, counting SYN/FIN each as one byte
    size_t bytes_in_flight() const;
    //! \brief number of bytes not yet reassembled
    size_t unassembled_bytes() const;

    //! \brief Number of milliseconds since the last segment was received
    size_t time_since_last_segment_received() const;

    // debug用
    //!< \brief summarize the state of the sender, receiver, and the connection
    TCPState state() const { return {_sender, _receiver, active(), _linger_after_streams_finish}; };

    // 这些函数都会由上层在某些时候调用
    // 时钟滴答、收到segment以及从segment_out中取数据,这些都是由os调用相应函数实现的
    // 这也正是所谓“协议”的接口意义!
    //! \name Methods for the owner or operating system to call
    //! Called when a new segment has been received from the network
    void segment_received(const TCPSegment &seg);

    //! Called periodically when time elapses
    void tick(const size_t ms_since_last_tick);

    //! \brief TCPSegments that the TCPConnection has enqueued for transmission.
    //! \note The owner or operating system will dequeue these and
    //! put each one into the payload of a lower-layer datagram (usually Internet datagrams (IP),
    //! but could also be user datagrams (UDP) or any other kind).
    std::queue<TCPSegment> &segments_out() { return _segments_out; }

    //! \brief Is the connection still alive in any way?
    //! \returns `true` if either stream is still running or if the TCPConnection is lingering
    //! after both streams have finished (e.g. to ACK retransmissions from the peer)
    bool active() const;

    //! Construct a new connection from a configuration
    explicit TCPConnection(const TCPConfig &cfg) : _cfg{cfg} {}

    //! \name construction and destruction
    //! moving is allowed; copying is disallowed; default construction not possible

    ~TCPConnection(); //!< destructor sends a RST if the connection is still open
    TCPConnection() = delete;
    TCPConnection(TCPConnection &&other) = default;
    TCPConnection &operator=(TCPConnection &&other) = default;
    TCPConnection(const TCPConnection &other) = delete;
    TCPConnection &operator=(const TCPConnection &other) = delete;
    -

    在这种情况下,管道相比临时文件至少有四个优势

    -
      -
    • 首先,不用删文件
    • -
    • 其次,管道可以任意传递长的数据流
    • -
    • 第三,管道允许一定程度上的并行
    • -
    • 第四,如果实现进程间通讯,管道的块读写比文件的非块语义更有效率。
    • -
    -

    File system

    inode:代表文件本体,包括文件类型、文件长度、文件内容在磁盘位置、文件的链接数

    -

    link:指向文件的链接,一个文件可以有多个link,link内包含文件名和对inode的引用

    -

    当链接数=0,且句柄数=0,文件的磁盘空间和inode索引就会被释放

    -

    Lab Xv6 and Unix utilities

    配置实验环境

    -

    参考文章:

    -

    xv6环境搭建

    -

    【MIT6.S081/6.828】手把手教你搭建开发环境

    +

    测试程序

    这部分暂时还不大明白,随便瞎写一点()

    +

    首先是socket实现,似乎要涉及到对一些事件,比如说segment receive的监听。它具体是这么做的:

    +
    // in libsponge/tcp_helper/tcp_sponge_socket.cc  _initialize_TCP()
    // 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)
    //
    // 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)
    //
    // 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)
    //
    // 4) Outbound segment generated by TCP (needs to be
    // given to underlying datagram socket)
    + +

    比如说event4:

    +

    image-20230304171810877

    +
    +

    什么是eventloop

    +

    事件循环(event loop)就是 任务在主线程不断进栈出栈的一个循环过程。任务会在将要执行时进入主线程,在执行完毕后会退出主线程。

    +

    这里的大致意思就是增加了一个监听事件,一旦tcp_connection的segments_out有元素,就会马上取出来

    -

    下载工具链

    $ sudo apt-get install git build-essential gdb-multiarch qemu-system-misc gcc-riscv64-linux-gnu binutils-riscv64-linux-gnu 
    +

    这部分不大懂,不知道后面会不会涉及对socket的编写?

    +

    还有一点是对测试脚本好像有了点了解。比如在build/CTestTestfile.cmake中可以看到每个测试的对应脚本以及使用的options:

    +

    image-20230304170132380

    +

    如果不知道option的用法可以这么做:

    +

    image-20230305232621024

    +

    这些脚本实现的对应代码在sponge/apps中。

    +

    又比如,在sponge/etc/tests.cmake中,可以找到各个测试程序执行的参数,就可以比如说修改测试的Timeout时间:

    +

    image-20230305232913611

    +

    总结:状态机

    我们已经完整实现了整个TCP协议,是时候该对其做出一个总结了。

    +

    TCP协议本质上是一个状态机

    +

    在我们的sponge TCP中,我们将一个endpoint的TCP协议分成了两个状态机,一个是TCPReceiver的状态机,另一个是TCPSender的状态机。它们依据外界的输入【从app或者互联网】来进行状态的转移。

    +

    以下几张图完美地体现了状态转移关系【具体的状态体现标注在代码中了】:

    +

    image-20230226200935395

    +

    image-20230226202631406

    +

    image-20230305225738049

    +

    image-20230225232723083

    +

    TCPConnection并不是状态机,它是两个状态机和外界联通的桥梁。它的职能有:

    +
      +
    1. 给状态机提供输入

      +

      包括:

      +
        +
      1. app调用write传进来的数据
      2. +
      3. peer通过segment_received传进来的数据
      4. +
      +
    2. +
    3. 处理状态机的输出

      +

      包括:

      +
        +
      1. app调用receiver的stream接口获取数据
      2. +
      3. 通过send_segment向peer传递数据
      4. +
      +
    4. +
    +

    也可以说,它具有显式推动状态机状态转移的作用,比如说:

    +
      +
    1. 给状态机传递外界数据让他们转移
    2. +
    3. connect通过调用fill_window推动_sender从CLOSED状态转移到SYN_SENT状态
    4. +
    5. 转移到ERROR状态的条件判断
    6. +
    +

    等等等。

    +

    也因而,TCPConnection并不包含复杂的逻辑和算法,它仅仅是做一些条件判断,以及一些数据转发的工作。

    +

    喜闻乐见的bug合集

    相比于代码的编写,本次实验最难的部分是测试。由于lab4基于lab0-3,因而前面没有发现的bug在本次黑压压162个测试之下会全部涌现出来。有些bug我还是不知道怎么回事,并且debug过程也不像xv6那样条理清晰步步为营,感觉充满着不少玄幻色彩,所以也没有很多干货好说。在这里就先记录下印象比较深刻,耗时比较久的bug吧。

    +

    TCP produced ‘ackno=1’

    image-20230303214944132

    +

    需要发送一个ackno=2的帧,但是不知道为什么却发送了一个ackno=1的,并且无论我怎么找,在哪里print,都只能找到一个ackno=2的,连1的影子都看不到。这个现象确实很诡异,但其实它的内因很简单。它是由于我对空的ACK帧发送条件限制得不恰当才出现的。

    +
    +

    有没有觉得这里有点跳跃?我是怎么通过这个现象得知是ACK发送不恰当导致的?

    +

    答案是我当时也没想到这一点,无头苍蝇般转了可能有一个小时,这里print一下那里print一下都没有发现异常。最后我放弃了这个用例去看下一个错误的用例,才发现了这个小bug,改了一下发现这个也一起过了【绷】

    +
    +

    本来我是这么写的:

    +
    // 把_sender的所有segment都发送出去
    void TCPConnection::segment_send() {
    // 当没有要发送的帧,无法进行顺带ACK时,就只能发送一个只有ACK的空帧
    if (_sender.segments_out().empty()) {
    if (_receiver.ackno().has_value()) {
    _sender.send_empty_ack_segment(_receiver.ackno().value());
    }
    }
    while (!_sender.segments_out().empty()) {
    // ...
    }
    }

    // in segment_received()
    //if (seg.length_in_sequence_space() != 0) {
    // empty_ack_send();
    //}
    segment_send();
    -

    测试安装ok:

    -
    $ qemu-system-riscv64 --version
    QEMU emulator version 5.1.0
    //下面其中之一正常就行
    $ riscv64-linux-gnu-gcc --version
    riscv64-linux-gnu-gcc (Debian 10.3.0-8) 10.3.0
    ...
    $ riscv64-unknown-elf-gcc --version
    riscv64-unknown-elf-gcc (GCC) 10.1.0
    ...
    $ riscv64-unknown-linux-gnu-gcc --version
    riscv64-unknown-linux-gnu-gcc (GCC) 10.1.0
    ...
    +

    如果这么写的话,当这台endpoint收到peer的一个empty ACK后,它就也会以示敬意回复一个empty ACK,这样除了本应发过去的ackno=2的报文,就多了个幽灵般的ackno=1的empty ACK,从而导致上面的错误。

    +

    因而,正确的做法是,我们在receive时只对**!empty**的seg进行ACK回复就行。具体写法可以看看我下面的代码。

    +

    超时重传时间翻倍问题

    image-20230303224104016

    +

    image-20230303224053277

    +

    可以看到,它是想要我们在1000ms后再发一次FIN的,也即rto依然等于1000,但是我们的rto却是2000.为啥呢?那就去看看超时重传呗。

    +

    原来的超时重传代码:

    +
    // in libsponge/tcp_sender.cc
    if (timer_start && ticks > timer_ticks && ticks - timer_ticks >= rto) {
    // resend
    if (!tmp_segments.empty()) {
    _segments_out.push(tmp_segments.front().segment);
    }

    if (window_size != 0) {
    cons_retran++;
    rto *= 2;
    }
    timer_ticks = ticks;
    }
    + +

    改完后:

    +
    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;
    }
    + +

    想想超时重传的定义,是不是重传了之后才会double时间呀()

    +

    assembler

    image-20230304163111298

    +

    这个test花了我半个下午的时间排查和修改。大致流程及报错信息是,一方发了65000个byte,但是另一方只能收到<<65000个。最后print了一下,发现是streamassembler写错了,在stream end的时候仍然有很大一部分数据未被整流。

    +
    +

    这个直面屎山的经历极大地鼓舞了我

    +
    +

    之前在写streamassembler的时候就知道有个地方是错的了,那就是我对capacity的理解【具体见前面的笔记】。现在只用改一下就好了。修改方式很简单,加上这两句话就行:

    +
    right = right <= left_bound + _capacity ? right : left_bound + _capacity;  // 右边越界的也不要
    if (o_left >= left_bound + _capacity) goto end; // 越界的不要
    + +

    t_udp_client_send超时

    image-20230304215748823

    +

    这个错因非常地诡异,我到最后也还是没有自己找出来。直到我瞎搜来搜去看到了这篇文章:

    +
    +

    我也是真的很佩服这篇文章的作者能找到这个点

    +

    image-20230304221420748

    +

    https://www.cnblogs.com/lawliet12/p/17066719.html

    +
    +

    image-20230305215310021

    +

    噔噔咚。

    +

    我为什么不用_cfg.rt_timeout呢?答案是我当初脑子一抽以为rt_timeout是static、const的,就写了个TCPConfig::rt_timeout然后报错了,我懒得思考了就换成了上面的那个,结果……就这东西,又花费了我好久好久【悲】怪我没有认真看,没发现rt_timeout不是一个静态常量。

    +

    t_ucS_1M_32K超时

    image-20230305162239877

    +

    以及其后面的其他test也都超时了。

    +

    说实话我真是百思不得其解,这里打印来那里打印去,也都看得眼花缭乱什么也看不出来,使用指导书那些手动测试的方法,还有抓包,都十分地正常,但它自动测试就是会timeout。

    +

    我折腾来折腾去,这里print那里print,最后还怀疑是电脑问题就放到服务器上跑了一下结果还是不行。绝望之际,我只能使出了万策尽之时的迫不得已的非法手段:将我的一部分代码替换成别人的看看会怎么样。【传统艺能23333】

    +

    最终我定位发现是TCPSender出了问题,我猜测是因为状态机出错了。我比对着别人的代码【知道这不对,但我心态已经崩了。。。】,以及指导书提供的状态机,发现是这个地方出了小问题:

    +

    image-20230305220614819

    +
    // in tcp_sender.cc fill_window()
    // 注释的是以前写的错误版本
    // if (_stream.input_ended() && !fin && remaining > 0) {
    if (_stream.eof() && !fin && remaining > 0) {
    // last segment
    segment.header().fin = true;
    fin = true;
    remaining -= 1;
    }
    +

    这里不应该是input_ended,而应该是eof……

    +

    改了之后立刻所有测试都能跑通了【悲】

    -

    注,这里出现了一个问题,qemu-system-riscv64 --version打出来发现qemu-system-riscv64 command not found。似乎是我的ubuntu16.04版本太低了【悲】去看了下网上,可以按照这个来做:

    -

    rCore qemu risc-v 实验环境配置

    -
    -

    下载编译xv6源码

    随后,进入一个你喜欢的文件夹clone xv6的实验源码,输入

    -
    $ git clone git://g.csail.mit.edu/xv6-labs-2020
    $ cd xv6-labs-2020
    $ git checkout util
    +

    那么问题来了,为什么错误版本就会timeout呢?我的猜测如下:

    +

    eof的条件如下:

    +
    bool ByteStream::eof() const { return is_input_end && buffer.empty(); }
    -

    然后进行编译

    -
    $ make
    $ make qemu
    +

    可以看到,eof既要求input_ended,又要求缓冲区内所有数据成功发送。这也很符合FIN_SENT的语义:在数据流终止时(所有数据成功发送,不要求fully acked)发送FIN。

    +

    如果按照我错误版本的写法,会导致数据还没发送完毕(!buffer.empty()),就发送了FIN。之后数据虽然还能正常进入receiver的bytestream,并且发送给peer的receiver。但是会存在这也一个空窗期:FIN之后的数据还没到的时候,peer的receiver接收到FIN,并且peer的app从socket将receiver接收到的数据全部读出。出现了这样的空窗期,就会导致peer的receiver的stream达到eof状态:

    +
    // in tcp_receiver.cc segment_received()
    if (abs_seqno != 0)
    _reassembler.push_substring(data, index, header.fin);
    // in streamassembler.cc
    if (is_eof && buffer.empty()) {
    _output.end_input();
    }
    -

    如果此处发生错误:unrecognized command line option -mno-relax,则按照此说法 xv6环境搭建更新gcc版本

    -
    $ sudo apt install gcc-8-riscv64-linux-gnu
    $ sudo update-alternatives --install /usr/bin/riscv64-linux-gnu-gcc riscv64-linux-gnu-gcc /usr/bin/riscv64-linux-gnu-gcc-8 8
    +

    【接下来就是猜了】由于bytestream eof了,socket就停止读了。后来的数据再来,receiver的stream的缓冲区就满了,receiver就只能一直丢包。【接下来是真的纯猜】而且由于测试脚本问题,在这之后都不会调用tick方法了,故而超时重传检测不会被触发,而sender也会因为没有ack,而一直重传重传,就死循环然后timeout寄掉了。

    +

    纯猜部分的依据是:

    +

    image-20230305173110776

    +

    image-20230305173152469

    +

    可以看到,tick方法一直被调用,但是ticks却不变。数据报文一直被重传,但是retran一直不变。ticks-timer_ticks一直大于rto,但却始终无法进入那句if(经测试是这样的)。这非常奇怪,我也不知道为什么。

    +
    +

    代码

    【珍贵的调试用代码没删的版本放在github了。】

    +

    TCPConnection.hh

    class TCPConnection {
    private:
    // ...
    size_t rec_tick{};// 上一次收到segment时的ticks数
    size_t ticks = 0;
    public:
    void segment_send();
    void empty_ack_send();
    void set_rst();
    // ...
    -

    再执行一次

    -
    $ make
    $ make qemu
    +

    TCPConnection.cc

    如果想要以状态机的视角来看待,可以看看感恩的代码。他写得很清晰。

    +
    #include "tcp_connection.hh"
    #include <iostream>

    template <typename... Targs>
    void DUMMY_CODE(Targs &&... /* unused */) {}

    using namespace std;

    size_t TCPConnection::remaining_outbound_capacity() const { return _sender.stream_in().remaining_capacity(); }

    size_t TCPConnection::bytes_in_flight() const { return _sender.bytes_in_flight(); }

    size_t TCPConnection::unassembled_bytes() const { return _receiver.unassembled_bytes(); }

    size_t TCPConnection::time_since_last_segment_received() const { return ticks - rec_tick; }

    // 发送一个只有ACK的空帧,仅在segment_received中调用
    // 当没有要发送的帧,无法进行顺带ACK时,
    // 为了保障一定有ACK发送,就只能发送一个只有ACK的空帧
    void TCPConnection::empty_ack_send() {
    if (_sender.segments_out().empty()) {
    if (_receiver.ackno().has_value()) {
    _sender.send_empty_ack_segment(_receiver.ackno().value());
    }
    }
    }

    // 把_sender的所有segment都发送出去
    void TCPConnection::segment_send() {
    while (!_sender.segments_out().empty()) {
    TCPSegment seg = _sender.segments_out().front();
    // 顺带ACK
    if (_receiver.ackno().has_value()) {
    seg.header().ack = true;
    seg.header().ackno = _receiver.ackno().value();
    }
    seg.header().win = _receiver.window_size();
    _segments_out.push(seg);
    _sender.segments_out().pop();
    }
    }

    // connection被置为error状态的部分必要操作
    void TCPConnection::set_rst() {
    _sender.stream_in().set_error();
    _receiver.stream_out().set_error();
    _linger_after_streams_finish = false;
    }

    void TCPConnection::segment_received(const TCPSegment &seg) {
    // 重置发送的ticks
    rec_tick = ticks;

    if (seg.header().rst) {
    // RST is set
    set_rst();
    return;
    }

    // 回复对方问你是死是活的信息
    if (_receiver.ackno().has_value() && seg.length_in_sequence_space() == 0 &&
    seg.header().seqno - _receiver.ackno().value() < 0) {
    _sender.send_empty_segment();
    segment_send();
    return;
    }

    _receiver.segment_received(seg);

    if (seg.header().ack) { // ack_received也会调用fill_window
    _sender.ack_received(seg.header().ackno, seg.header().win);
    } else
    _sender.fill_window();

    // 只在本次收到的seg需要被ACK的时候才要ACK。
    // 需要被ACK:FIN/SYN/携带数据 总之就是length!=0
    // 不得不说,FIN和SYN都会占一个序列号这个点给ACK设计带来了简便,同时也增加了安全性
    if (seg.length_in_sequence_space() != 0) {
    empty_ack_send();
    }

    segment_send();

    // If the inbound stream ends before the TCPConnection has reached EOF
    // on its outbound stream, this variable needs to be set to false
    // 如果receiver的那个stream比sender的stream早结束,就不用等待
    // 为什么呢?因为receiver的stream结束说明了全部的seg都成功接收并且全部整流【参见assembler实现】
    // 也就说明对方不发送数据了,并且已经把FIN也发过来了
    // 也即对方进入了FIN_WAIT状态
    // 而我们的sender还在输出,也即我们在CLOSE_WAIT状态
    // 因而我们只需输出完剩余数据再发送AF,最后直接关闭就行
    // 因为我们知道对方已经关闭了,无需再进行linger。
    if (_receiver.stream_out().input_ended() && !_sender.stream_in().eof()) {
    // peer:FIN_WAIT self:CLOSE_WAIT
    _linger_after_streams_finish = false;
    }
    }

    bool TCPConnection::active() const{
    // 处于error状态
    if (!_linger_after_streams_finish && _receiver.stream_out().error() && _sender.stream_in().error()) {
    return false;
    }

    // 满足条件1-3
    if (_receiver.stream_out().input_ended() &&
    _sender.stream_in().eof() && _sender.bytes_in_flight() == 0 && _sender.fully_acked()) {
    // 无需等待的话就直接返回false
    if (!_linger_after_streams_finish)
    return false;
    // 否则需要等待10*timeout
    else if (time_since_last_segment_received() >= 10 * _cfg.rt_timeout){
    return false;
    }
    }
    return true;
    }

    size_t TCPConnection::write(const string &data) {
    size_t res = _sender.stream_in().write(data);
    // 注意此处需要手动调一下fill_window和send方法
    _sender.fill_window();
    segment_send();
    return res;
    }

    // ms_since_last_tick: number of milliseconds since the last call to this method
    void TCPConnection::tick(const size_t ms_since_last_tick) {
    ticks += ms_since_last_tick;
    _sender.tick(ms_since_last_tick);
    if (_sender.consecutive_retransmissions() > _cfg.MAX_RETX_ATTEMPTS) {
    while (!_sender.segments_out().empty())
    _sender.segments_out().pop(); // 清除sender遗留的所有帧
    _sender.send_empty_rst_segment();// 只发送rst帧
    set_rst();
    }
    segment_send();

    // end the connection cleanly if necessary
    if (_receiver.stream_out().input_ended() &&
    _sender.stream_in().eof() && _sender.bytes_in_flight() == 0 && _sender.fully_acked()
    && time_since_last_segment_received() >= 10 * _cfg.rt_timeout) {
    // 等待结束
    _linger_after_streams_finish = false;
    }

    }

    void TCPConnection::end_input_stream() {
    _sender.stream_in().end_input();
    _sender.fill_window();
    segment_send();
    }

    void TCPConnection::connect() {
    _sender.fill_window();
    // send_segment重复代码。目的是防止发送SYN外还发送别的东西
    if (!_sender.segments_out().empty()) {
    TCPSegment seg = _sender.segments_out().front();
    if (_receiver.ackno().has_value()) {
    seg.header().ack = true;
    seg.header().ackno = _receiver.ackno().value();
    }
    seg.header().win = _receiver.window_size();
    _segments_out.push(seg);
    _sender.segments_out().pop();
    }
    }

    TCPConnection::~TCPConnection() {
    try {
    // shutdown uncleanly
    if (active()) {
    set_rst();
    _sender.send_empty_rst_segment();
    segment_send();
    }
    } catch (const exception &e) {
    std::cerr << "Exception destructing TCP FSM: " << e.what() << std::endl;
    }
    }
    -

    就ok了。

    -

    关闭qemu

    qemu退出操作

    -

    在这里记个强制方法:

    -
    ps -elf | grep qemu
    +

    debug函数

    // in libsponge/tcp_helper/tcp_segment.hh
    void print_seg() const{
    std::cerr<<" flag="<<(header().syn?"S":"")<<(header().ack?"A":"")<<(header().fin?"F":"")
    <<" seqno="<<header().seqno.raw_value()<<" ackno="<<header().ackno.raw_value()
    <<" payload_size:"<<payload().size()<<std::endl;
    }
    -

    image-20230105153458808

    -

    记住第二个的pid

    -

    然后

    -
    kill 3303
    +

    in libsponge/tcp_helper/fd_adapter.cc

    +

    image-20230304172207234

    +]]>
    +
    + + rtt硬件环境搭建 + /2023/10/12/rtt%E7%A1%AC%E4%BB%B6%E7%8E%AF%E5%A2%83%E6%90%AD%E5%BB%BA/ + SDK/内核编译

    编译内核和sdk时到没有太大问题,都能靠gpt or 修改menuconfig解决。比较棘手的果然还是只能靠自己摸索硬件hhh

    +

    烧录

    这个也是狠狠折磨了我许久,gpt也一直满嘴跑火车,毕竟一开始串口连错了所以一直以为自己是烧录没对,整了半天【而且还是正确的操作反反复复尝试……仿佛坐牢】。不过这其中也学到了挺多。

    +

    f2753b5e6a40f9fea328223b197ede3

    +

    具体要做的是:

    +
    sudo fdisk /dev/sdb
    -

    测试gdb是否ok

    见该文章最后一部分

    -

    【MIT6.S081/6.828】手把手教你搭建开发环境

    -

    自测方法

    make grade
    +
    +

    关于fdisk及part这样的分区工具,这篇文章介绍得很详细:Linux 系统中关于磁盘分区工具的使用

    +
    +

    然后进入后,n命令创建新分区,d删除现有分区。我们需要用n创建两个分区(大小随意),第一个用于存放bin文件。创建完分区之后,使用a命令将分区1标记为Boot,并且使用t将分区1的类型修改为W95 FAT32 (LBA),随后就可以保存退出了。具体流程及操作结果如下:

    +
    $ sudo fdisk /dev/sdb

    Welcome to fdisk (util-linux 2.37.2).
    Changes will remain in memory only, until you decide to write them.
    Be careful before using the write command.

    Command (m for help): p

    Disk /dev/sdb: 29.72 GiB, 31914983424 bytes, 62333952 sectors
    Disk model: Storage Device
    Units: sectors of 1 * 512 = 512 bytes
    Sector size (logical/physical): 512 bytes / 512 bytes
    I/O size (minimum/optimal): 512 bytes / 512 bytes
    Disklabel type: dos
    Disk identifier: 0xd4c6d64f

    Command (m for help): n
    Partition type
    p primary (0 primary, 0 extended, 4 free)
    e extended (container for logical partitions)
    Select (default p):

    Using default response p.
    Partition number (1-4, default 1):
    First sector (2048-62333951, default 2048):
    Last sector, +/-sectors or +/-size{K,M,G,T,P} (2048-62333951, default 62333951): +128M

    Created a new partition 1 of type 'Linux' and of size 128 MiB.
    Partition #1 contains a vfat signature.

    Do you want to remove the signature? [Y]es/[N]o: yes

    The signature will be removed by a write command.

    Command (m for help): n
    Partition type
    p primary (1 primary, 0 extended, 3 free)
    e extended (container for logical partitions)
    Select (default p):

    Using default response p.
    Partition number (2-4, default 2):
    First sector (264192-62333951, default 264192):
    Last sector, +/-sectors or +/-size{K,M,G,T,P} (264192-62333951, default 62333951):

    Created a new partition 2 of type 'Linux' and of size 29.6 GiB.

    Command (m for help): t
    Partition number (1,2, default 2): 0c
    Value out of range.
    Partition number (1,2, default 2): 1
    Hex code or alias (type L to list all): 0c

    Changed type of partition 'Linux' to 'W95 FAT32 (LBA)'.

    Command (m for help): a
    Partition number (1,2, default 2): 1

    The bootable flag on partition 1 is enabled now.

    Command (m for help): p
    Disk /dev/sdb: 29.72 GiB, 31914983424 bytes, 62333952 sectors
    Disk model: Storage Device
    Units: sectors of 1 * 512 = 512 bytes
    Sector size (logical/physical): 512 bytes / 512 bytes
    I/O size (minimum/optimal): 512 bytes / 512 bytes
    Disklabel type: dos
    Disk identifier: 0xd4c6d64f

    Device Boot Start End Sectors Size Id Type
    /dev/sdb1 * 2048 264191 262144 128M c W95 FAT32 (LBA)
    /dev/sdb2 264192 62333951 62069760 29.6G 83 Linux

    Filesystem/RAID signature on partition 1 will be wiped.

    Command (m for help): w
    The partition table has been altered.
    Calling ioctl() to re-read partition table.
    Syncing disks.
    -

    或者如果只想测其中一个,可以:

    -
    ./grade-lab-util sleep
    +

    然后更新分区表,格式化文件系统,复制bin和sd文件、弹出sd卡:

    +
    sudo partprobe 
    sudo mkfs.vfat -F 32 /dev/sdb1
    sudo mkfs.vfat -F 32 /dev/sdb2
    sudo mount /dev/sdb1 /mnt/sdb1
    sudo cp boot.sd fip.bin /mnt/sdb1
    sudo umount /mnt/sdb1
    sudo eject /dev/sdb
    -

    make qemu后卡住

    疑似qemu版本不对。解决方法

    -

    实验内容

    编写sleep.c

    -

    Implement the UNIX program sleep for xv6; your sleep should pause for a user-specified number of ticks. A tick is a notion of time defined by the xv6 kernel, namely the time between two interrupts from the timer chip. Your solution should be in the file user/sleep.c.

    -

    image-20230105164146100.png

    -
    -
    体会
    参数

    注意,他要求我们实现的sleep的参数是ticks的数量,不是秒数。我花了半天找时钟周期大小这个参数在哪,找了许久没找到,估计是没考虑到这一点。

    -

    比如说,我翻了一下linux0.11的源码,在include/linux/time.h下有这句:

    -

    image-20230105162505574.png

    -

    说明了时钟频率大小。在xv6好像没有看到对这个的显式说明。

    -
    系统调用过程

    感受了一下xv6的系统调用过程,跟linux0.11还是很相像的。

    -

    这个好像是lab2的内容,我暂且先在此放下我体会到的感受。

    +

    就烧录ok了。

    +

    与此同时也顺便学了下Windows磁盘管理,感觉也挺方便。

    +

    串口连接

    不得不说十分惨痛……

    +

    也是因为我太狂妄自大不仔细了,一直以为RX就得连RX,TX就得连TX,然后下午焦灼之中才突然想起来这俩是不是以前教过得反着连……搜了一下果然是草。

    +

    这个错误害我起码花了三个小时在纠结烧录问题,一直以为没输出是因为烧录不对,发现结果那一刻我真没蚌珠。哎,以后还是别对自己太自信,毕竟本质上这种东西不是专业的学得也不够扎实,做什么都得先查一下该怎么做。

    +

    另一个问题是,乱码:

    +

    58c63791e1b4cac06a827f0ffa6b329

    +

    好像是说使用了个ch340的usb-ttl,似乎这个东西波特率有点问题不准确,具体不大懂:CH340G U-BOOT阶段乱码 总之最后简单粗暴地靠修改波特率为117200解决了。

    +

    然后别的倒是没什么问题,虽然摸索也花了点时间,都是小事。对着这张图参照就行了:

    +

    mmexport1697102534057

    +

    开发过程

    感觉有一些东西,还是比较靠悟性23333在此记录些开发过程中的小发现。

      -
    1. xv6

      -

      首先是从用户态到内核态的切换。

      -

      在user/user.h中有各个系统调用外化的函数签名。在用户程序中调用里面的函数签名,就会执行【说实话,我没看懂为什么这里会知道要从user.h跳到usys.S中执行,也许是Makefile里有写?】user/usys.S中对应的汇编代码,比如说这种:

      -

      image-20230105170701334

      -

      然后这个SYS_close这种,其实是系统调用号宏,被定义在kernel/syscall.h中:

      -

      image-20230105171327076.pn

      -

      li a7,SYS_call就是把SYS_call的值放入a7寄存器,大概就是传参的意思。ecall是从用户态转到内核态的指令。这样一来,就完成了从用户态到内核态的切换。

      -

      然后是在内核态的执行。

      -

      切换到内核态之后的执行步骤跟linux0.11可以说是完全一样。

      -

      首先应该是会去执行kernel/syscall.c中的syscall函数,具体应该是通过ecall引发0x80中断,然后查表得知这个syscall是中断处理函数

      -

      image-20230105172110475.pn

      -

      可以看到,syscall获取了a7里的参数,然后查了系统调用表

      -

      image-20230105173019159

      -

      然后去sysproc.c文件下执行相应的sys_xxx函数。这个函数指针用得真是牛逼。

      -

      再然后,sys_xxx函数中会从栈中取出调用参数,再跳转到xxx(args)函数中去(这些xxx函数一般在kernel中以单独文件形式出现)。

      -

      这样一来,就完成了一次系统调用。

      -
    2. -
    3. linux0.11

      -

      首先是用户态到内核态的切换。

      -

      在用户态中比方说调用system call close(),则会调用lib/close.c下的:

      -

      image-20230105173820813

      -

      展开这个宏之后,是这样的:

      -

      image-20230105173845317

      -

      具体意思就是把close的系统调用号存入参数寄存器,然后引发0x80中断,进入内核态。

      -

      然后是在内核态的执行。

      -

      查表会得知sys_call函数是0x80中断的中断处理函数,然后就会根据参数里的系统调用名字去找系统调用表执行

      -

      image-20230105174832400

      -

      这部分跟xv6差不多,不再赘述

      +
    4. 关于引脚

      +

      两个引脚可以通过image-20231019162502551这玩意连起来,这样就能使一个引脚的output作为另一个引脚的input。

      +

      这是我在开发gpio中断时意识到的。这个道理虽然非常简单,但对于零基础胡乱摸索的我,知道这个还是需要一些灵光一闪。

    5. +
    -

    可见,这两个系统在内核态的实现是差不多的,只是在用户态有点稍稍不一样。感觉linux0.11会更加精妙一些。

    -

    编写pingpong程序

    -

    Write a program that uses UNIX system calls to ‘’ping-pong’’ a byte between two processes over a pair of pipes, one for each direction. The parent should send a byte to the child; the child should print “: received ping”, where is its process ID, write the byte on the pipe to the parent, and exit; the parent should read the byte from the child, print “: received pong”, and exit. Your solution should be in the file user/pingpong.c.

    +]]> + + + cs144 + /2023/02/25/cs144/ + +

    总耗时:65h 约17天

    +

    实验官网

    +

    感恩

    -
    体会

    思路很简单,我之所以写了那么久是因为走了好大的弯路……

    -

    题目要求输出格式为”: received ping”,我的思路固化为:先把pid化成数字,再用字符串拼接串成整个。为了实现我的思路,我就需要额外再写两个工具函数,一个是itoa,一个是strcat。而又由于malloc函数暂待实现,itoa和strcat的实现就仍然不够优雅。折腾了半天终于OK了,结果看到别人是怎么做到这个输出格式的呢?↓

    -
    fprintf(1,"%d: received ping\n",getpid());
    - -

    这下是真的尴尬了23333

    -

    但总而言之,自己写了那俩不够优雅的函数还算是有点用【大概】。以下是我的代码

    -

    编写primes

    -

    参考:

    -

    MIT操作系统实验lab1(案例:primes(质数筛选)附代码、详解)

    -

    XV6实验-Lab0 Utilities

    +

    总结

    本实验总体思维和代码上的难度还是不难的(至少比xv6简单),我认为其难点主要集中在TCP协议本身就很复杂很多的细节问题,以及需要我们有一种面向测试用例编程、直面自己往日写过的屎山的勇气(。

    +

    下面,我将对本实验的完成情况即心得进行一个总结,也算是本篇博客/本次实验的一个导读

    +
    +

    类似于这样的块引用中的部分是我自认为的精华部分哦

    +

    本实验对TCP-IP-ETH协议栈的实验是自顶向下的,其中对TCP协议的实现是由内而外的。

    +

    后者很容易导致,在对TCP协议的实现中,当你写完了lab0-3,你还是不知道自己到底写了个啥,以及TCP又究竟怎么通过你写的那几个类run起来。直到lab4结束,你完成了对TCP状态机的组织,并且出于debug目的钻研过部分socket的代码、熟悉了(其实差不多已经背下来了)TCP的三握手四挥手的过程,至此你才会对TCP的实现有较为清晰的理解。这个过程很痛苦,但是也真的非常爽。

    -

    Write a concurrent version of prime sieve using pipes. This idea is due to Doug McIlroy, inventor of Unix pipes. The picture halfway down this page and the surrounding text explain how to do it. Your solution should be in the file user/primes.c.

    +

    关于TCP状态机的理解总结,请参见Lab4 TCPConnection——心得——总结:状态机部分。

    -

    其实就是用生产者消费者模式来写素数计算的并发版本,这个我熟

    -

    ……以上是第一印象。然后我看着超链接文章里的素数筛的图片,以及指导书给的提示:

    +

    然而,实现了TCP协议之后,我们还是不知道,在TCPConnection中发送的数据包,又是如何到达网络上的另一个host处的,我们究竟又写了个啥。

    +

    这时,官方贴心地为我们指了条明路:它告诉我们,我们在lab0-4实现的是TCP-IP协议栈,其中运输层和网络层由用户实现,其他更底层则由内核实现,二者通过操作系统提供的TUN接口进行交互。也即,我们之前实现的是用户态TCP协议!而我们接下来的学习目标,就是从内核中再夺走一些权力:数据链路层也要由我们自己实现!

    -

    Your goal is to use pipe and fork to set up the pipeline. The first process feeds the numbers 2 through 35 into the pipeline. For each prime number, you will arrange to create one process that reads from its left neighbor over a pipe and writes to its right neighbor over another pipe. Since xv6 has limited number of file descriptors and processes, the first process can stop at 35.

    -
      -
    • Be careful to close file descriptors that a process doesn’t need, because otherwise your program will run xv6 out of resources before the first process reaches 35.
    • -
    +

    关于此处的TCP-IP架构等,请参见Lab5 NetworkInterface——Overview——承上启下

    +
    +

    故而,接下来,我们将实现用户态的TCP-IP-ETH协议栈。在lab5,我们将目光投向TCP层以下的数据链路层(网络层官方已经帮我们实现了),实现ETH协议和ARP协议;在lab6,我们则需要实现路由查找的功能。

    +

    至此,所有实验已经结束。写完了上述实验,我们对协议栈已经具有了很深刻的了解,对TCP—IP—ETH—TAN—Internet—TAN—ETH—IP—TCP的这个数据传输过程也已经是懂王了。

    +

    然而,我们在TCPConnection,只知道会有好心人,在上层app有数据传进来的时候调用write、在下层协议栈有segment传进来的时候调用segment_received、取出_segment_out的segment向底层协议栈发送、读走outputstream的内容。但是这个所谓的“好心人”具体是怎么做到的,怎么实现的,我们一概不知。

    +

    答案是,这个所谓的“好心人”,其实就是我们的TCPSpongeSocket。它向上将协议栈与上层app连接,向下又将协议栈与TAN接口结合。

    +
    +

    发送数据时,数据流向:上层app→(通过TCPSpongeSocketTCPConnection→(通过write方法)ByteStreamTCPSender→(通过从_sender.segments_out读)TCPConnection→(通过TCPSpongeSocket的adapter)TAN

    +

    接收数据时,数据流向:TAN→(通过TCPSpongeSocket的adapter)TCPConnectionTCPReceiver→(中间经过StreamAssemblerBYteStream→(通过TCPSpongeSocket读)上层app

    +

    TCPSpongeSocket的adapter中:TCPsegment←→IP数据报←→ETH帧

    +

    至于ETH帧进入TAN之后的过程?在xv6的网卡驱动那一节我们事实上已经实现过了!

    +
    +

    CS144TCPSocketFullStackTCPSokect都继承自TCPSpongeSocketTCPSpongeSocket通过一个包装了操作系统提供的socket的包装类_thread_data来与上层app进行交互,通过adapter_datagram_adapter来与协议栈进行交互(adapter本质上也是调用了操作系统的TUN/TAN接口)。

    +

    由于_thread_data_datagram_adapter本质上都是文件描述符【牛逼吧】,因而,TCPSpongeSocket需要跟上下层进行交互的需求,就可以通过操作系统提供的POLL机制来实现,也即,app←→TCP、TCP←→协议栈的这四种数据交互情况,都用事件监听来实现!这样就能做到“及时”“高效”了。

    +
    +

    关于此处TCPSpongeSocket的事件监听机制以及其它实现细节,详见其它的对…——Socket实现——TCPSpongeSocket

    +
    +

    至此以来,我们的协议栈才算真正完整了。

    +

    Lab0

    Lab1 StreamReassembler

    Lab2 TCPReceiver

    Lab3 TCPSender

    Lab4 TCPConnection

    Lab5 NetworkInterface

    Lab6 Router

    其他的对实验未涉及的思考

    ]]> + + labs + + + + git使用记录 + /2023/10/07/git/ + +

    记录一些git的原理学习,以及工作学习中遇到的一些git的操作问题。

    +
    +

    操作

    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
      + +

      这将清理无用的 dangling 对象。

      +
    4. +
    +

    成功。

    +

    原理

      +
    1. merge与rebase的差异

      +

      merge:

      +

      merge

      +

      rebase:

      +

      rebase

      +
    2. +
    3. +
    +]]>
    +
    + + 开源的第一个月 + /2023/10/19/open-source-9.19-10.19/ + +

    其实是9.10就开始了,但9.19才办理入职所以少算了几天xxxx不然这不显得我菜hh

    -

    义无反顾地……使用了35个管道hhhhh

    -

    然后不知道为什么不行,也焦头烂额地感觉我思路太离谱了,去看了下发现大家都是只用一个管道……

    -

    我也搞了个单管道的出来,但是思路受第一篇的影响非常地串行,也即先筛完再创建子进程。看到

    -

    XV6实验-Lab0 Utilities

    -

    这篇文章,才发现还可以那样双管道并行……我虽然也考虑过双管道,但是觉得实现不了【因为我是用循环的思路,如果要双管道的话切换会很麻烦】就没写了,没想到还可以向他那样【他选择的是一个在外部定义的p,和一个作用域更小在每次循环内定义的p1,再加上递归传递参数这个技巧,就可以接连不断递归下去了】,深感佩服。写得是真好,可以去参考学习一下,我懒得改了(

    -
    #include"user/user.h"

    int main(){
    int p[2];
    pipe(p);

    if(fork() == 0){
    while(1){
    char buf[3];
    //读入第一个数字
    read(p[0],buf,3);
    int prime = atoi(buf);
    if(prime == 36){
    close(p[0]);
    close(p[1]);
    exit(0);
    }
    fprintf(1,"prime %d\n",prime);
    //读入其他数字
    int tmp = atoi(buf);
    while(1){
    read(p[0],buf,3);
    tmp = atoi(buf);
    //输入结束
    if(tmp == 36){
    break;
    }
    if(tmp%prime!=0){
    write(p[1],buf,3);
    }
    }
    //作为标记,标志着输入序列结束
    itoa(36,buf);
    write(p[1],buf,3);
    if(fork()){
    }
    else{
    close(p[0]);
    close(p[1]);
    wait(0);
    exit(0);
    }
    }
    } else{
    close(p[0]);
    char buf[3];
    for(int i=2;i<=35;i++){
    itoa(i,buf);
    write(p[1],buf,3);
    }
    //作为标记,标志着输入序列结束
    itoa(36,buf);
    write(p[1],buf,3);
    close(p[1]);
    wait(0);
    }
    exit(0);
    }
    - -

    编写find

    -

    Write a simple version of the UNIX find program: find all the files in a directory tree with a specific name. Your solution should be in the file user/find.c.

    +

    引言

    也许有许多programer与我一样,在初次接触到“开源”这个概念时,便对其产生了无限的向往。千万人通过共同的事业联结在一起,为了行业的进步不求回报地压缩自己的时间,贡献出自己的一份力,这是何等的浪漫。再加上听了无数遍的Linux发展历程故事,对“开源”我是愈发地憧憬。

    +

    然而,由于学习cs的时间尚不足,也不知道该从何入手去参与社区贡献,尽管心中怀有对参与这份事业的渴望,我还是会“望而却步”。转折点在今年参加竞赛时,在使用某个开源项目时因为遇到了一点问题而去提了个issue。虽然这个问题本质很傻,但在与开发者你来我往的交流中,我深切地感受到自己仿佛离憧憬更近了一步。因而,在今年竞赛结束后的九月,我毫不犹豫地向PLCT Lab提交了简历,试图追逐那个长存我心的幻影。

    +

    而现在,距离第一次提issue已有半年,距离考核通过也已有一个半月之久,我想也是时候好好总结一下我这一个月以来的心路历程了。

    +

    我的第一个月

    +

    从9.10过审核开始写了很久的日报,当然除了实习还包括别的内容:

    +

    image-20231019155950614

    -
    初始版

    直接照着ls的模板改,改成递归就ok了。值得注意的是,目录也是一种文件,也可以通过read读取。目录文件的内容就是目录里的所有文件的名字。因而,我们在递归时可以忽略文件,只对目录处理,因为目录中就包含着所有文件名的信息。

    -
    附加题:支持正则表达式

    把user/grep.c里面的匹配函数拿来就行。

    -

    编写xargs

    -

    Write a simple version of the UNIX xargs program: read lines from the standard input and run a command for each line, supplying the line as arguments to the command. Your solution should be in the file user/xargs.c.

    +

    这一个月以来,我学到了很多东西,学习周期大致可分为三个阶段:初步了解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,这部分还是比较多好写的,虽然还是有点模糊(待我之后有时间整理下放个链接

    -
    体会

    思路还是很直观的,就是从stdin一行一行读入数据,然后把这数据处理成参数,最后调用exec就行。就是中间有很多小细节值得注意。

    -

    有一点比较坑的是,main方法的那个argc的计算方法是这样的,不是直接用数组的长度:

    -
    for(argc = 0; argv[argc]; argc++) 
    - -

    可以看到,合格的argv的形式应该是:参1 参2 参3 “\0”,最后一个元素要以”\0”标志结束。

    -

    这个应该是编写者约定俗成的。在user/sh.c的parseexec,大概445行左右:

    -

    image-20230106172133338

    -

    shell处理命令时是会默认把最后一个清零的。

    -
    -

    确实,后面在学内存的时候,用户空间的构成如图所示:

    -

    image-20230109234930690

    -

    可以看到栈那边,参数列完了之后是会有一个用以terminate的空指针的

    +

    设备树还是比较复杂,而且因为本人的不审慎,导致对其理解出了偏差,还麻烦了社区看我的代码(悲)只能说个人出道的开源社区还是需要对自己的所有笔代码负责。

    +

    gpio driver

    +

    pr:

    +

    image-20231021105306299

    +

    涉及到的各种硬件手册:

    +

    image-20231019165451893

    -

    附加题:改善shell

    看起来又难又多所以我先摸了【润】等之后有时间再回来弄吧

    +

    这也是我最后这一周在做的工作,虽说只有短短一周,但是每天都研究这个花了我不少时间和精力()目前算是写完了它的所有功能(大概),并且已经能把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设备驱动等的学习,总体来说还是十分甚至九分地开心。希望下个月能再接再厉。

    ]]> + + mylife + - Operating system oganization - /2023/01/10/xv6$chap2/ - Operating system oganization
    -

    Before you start coding, read Chapter 2 of the xv6 book, and Sections 4.3 and 4.4 of Chapter 4, and related source files:

    -
      -
    • The user-space code for systems calls is in user/user.h and user/usys.pl.
    • -
    • The kernel-space code is kernel/syscall.h, kernel/syscall.c.
    • -
    • The process-related code is kernel/proc.h and kernel/proc.c.
    • -
    -
    -

    这章主要是讲了操作系统为了兼顾并发性、隔离性、交互性做出的基本架构。

    -

    Kernel organization

    宏内核与微内核

    操作系统一个很重要的设计问题就是,哪部分的代码需要run在内核态,哪部分的需要run在用户态。

    -

    如果将操作系统所有系统调用统统都在内核态run,这种设计方式就叫宏内核monolithic kernel

    -

    如果仅将系统调用中必要的部分在内核态run,其他部分都在用户态run,并且采取Client/Server这样的异步通信方式,这种设计方式就叫微内核microkernel

    -

    image-20230107232802540

    -
    -

    由于客户/服务器(Client/Server)模式,具有非常多的优点,故在单机微内核操作系统中几乎无一例外地都采用客户/服务器模式,将操作系统中最基本的部分放入内核中,而把操作系统的绝大部分功能都放在微内核外面的一组服务器(进程)中实现。

    -
    -

    在微内核中,内核接口由一些用于启动应用程序、发送消息、访问设备硬件等的低级功能组成。这种组织允许内核相对简单,因为大多数操作系统驻留在用户级服务器中。

    -

    像大多数Unix操作系统一样,Xv6是作为一个宏内核实现的。因此,xv6内核接口对应于操作系统接口,内核实现了完整的操作系统。

    -

    Code: xv6 organization

    XV6的源代码位于kernel子目录中,源代码按照模块化的概念划分为多个文件,图2.2列出了这些文件,模块间的接口都被定义在了def.hkernel/defs.h)。

    + Operating system interface + /2023/01/10/xv6$chap1/ + Operating system interface

    本节大概是在讲操作系统的接口,系统调用占了很大一部分。

    - + - - - - - - - - - - - - - - - - - - - - - - - - - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + +
    文件系统调用 描述
    bio.c文件系统的磁盘块缓存
    console.c连接到用户的键盘和屏幕
    entry.S首次启动指令
    exec.cexec()系统调用
    file.c文件描述符支持
    fs.c文件系统
    kalloc.c物理页面分配器int fork()创建一个进程,返回子进程的PID
    kernelvec.S处理来自内核的陷入指令以及计时器中断int exit(int status)终止当前进程,并将状态报告给wait()函数。无返回
    log.c文件系统日志记录以及崩溃修复int wait(int *status)等待一个子进程退出; 将退出状态存入*status; 返回子进程PID。
    main.c在启动过程中控制其他模块初始化int kill(int pid)终止对应PID的进程,返回0,或返回-1表示错误
    pipe.c管道int getpid()返回当前进程的PID
    plic.cRISC-V中断控制器int sleep(int n)暂停n个时钟节拍
    printf.c格式化输出到控制台int exec(char *file, char *argv[])加载一个文件并使用参数执行它; 只有在出错时才返回
    proc.c进程和调度char *sbrk(int n)按n 字节增长进程的内存。返回新内存的开始
    sleeplock.cLocks that yield the CPUint open(char *file, int flags)打开一个文件;flags表示read/write;返回一个fd(文件描述符)
    spinlock.cLocks that don’t yield the CPU.int write(int fd, char *buf, int n)从buf 写n 个字节到文件描述符fd; 返回n
    start.c早期机器模式启动代码int read(int fd, char *buf, int n)将n 个字节读入buf;返回读取的字节数;如果文件结束,返回0
    string.c字符串和字节数组库int close(int fd)释放打开的文件fd
    swtch.c线程切换int dup(int fd)返回一个新的文件描述符,指向与fd 相同的文件
    syscall.cDispatch system calls to handling function.int pipe(int p[])创建一个管道,把read/write文件描述符放在p[0]和p[1]中
    sysfile.c文件相关的系统调用int chdir(char *dir)改变当前的工作目录
    sysproc.c进程相关的系统调用int mkdir(char *dir)创建一个新目录
    trampoline.S用于在用户和内核之间切换的汇编代码int mknod(char *file, int, int)创建一个设备文件
    trap.c对陷入指令和中断进行处理并返回的C代码int fstat(int fd, struct stat *st)将打开文件fd的信息放入*st
    uart.c串口控制台设备驱动程序int stat(char *file, struct stat *st)将指定名称的文件信息放入*st
    virtio_disk.c磁盘设备驱动程序int link(char *file1, char *file2)为文件file1创建另一个名称(file2)
    vm.c管理页表和地址空间int unlink(char *file)删除一个文件
    -

    图2.2:XV6内核源文件

    -

    Process overview

    内核用来实现进程的机制包括用户态内核态标志、地址空间和进程的时间切片。

    -

    为了帮助加强隔离,进程抽象给程序提供了一种错觉,即它有自己的专用机器。进程为程序提供了一个看起来像是私有内存系统或地址空间的东西,其他进程不能读取或写入。进程还为程序提供了看起来像是自己的CPU来执行程序的指令。

    -

    Xv6使用页表(由硬件实现)为每个进程提供自己的地址空间。RISC-V页表将虚拟地址(RISC-V指令操纵的地址)转换(或“映射”)为物理地址(CPU芯片发送到主存储器的地址)。

    -

    每个进程也有自己的页表,页表中记录了以虚拟地址0开始的内存区域。

    -

    image-20230107233741922

    -

    xv6内核为每个进程维护许多状态片段,并将它们聚集到一个proc(*kernel/proc.h*:86)结构体中。一个进程最重要的内核状态片段是它的页表、内核栈区和运行状态。我们将使用符号p->xxx来引用proc结构体的元素;例如,p->pagetable是一个指向该进程页表的指针。

    -
    -

    这应该相当于pcb表。

    -
    -

    Code: starting xv6 and the first process

    看完一遍说实话还乱乱的。。。。我整理整理跟linux的对比学习一下吧。

    -

    xv6

    加载操作系统

    系统加电,启动BIOS初始化硬件 -> BIOS从引导扇区将加载程序读入内存 -> 加载程序将操作系统镜像读入内存RAM。

    -
    -

    这个过程由qemu模拟。

    -

    首先会通过mkfs造出操作系统镜像。然后由qemu将引导扇区,也即下面的filesys这图里的第0块:

    -

    image-20230121162324747

    -

    读入到主存中,然后开始执行引导扇区的程序,下同。

    -
    -

    boot loader目的是把xv6加载进内存到0x8000 0000,然后跳转到xv6初始化程序。

    -
    -

    The reason it places the kernel at 0x80000000 rather than 0x0 is because the address range 0x0:0x80000000 contains I/O devices.

    -
    -

    操作系统初始化

    entry.S配置栈空间

    此时,目前的机器状态是,1.没有开启地址映射,也即虚拟地址=真实物理地址。2.运行在machine mode

    -

    xv6会在kernel/entry.S下的这里开始执行,目的是配置好栈,以开始C语言代码start.c的执行:

    -
    .global _entry
    _entry:
    # set up a stack for C.
    # 这段主要是在计算栈顶指针sp
    # stack0 is declared in start.c,
    # with a 4096-byte stack per CPU.
    # sp = stack0 + (hartid * 4096)
    la sp, stack0
    li a0, 1024*4
    csrr a1, mhartid
    addi a1, a1, 1
    mul a0, a0, a1
    add sp, sp, a0
    # 已经有栈了,就可以开始执行C语言代码了
    # jump to start()
    call start
    +

    表1.2:xv6系统调用(除非另外声明,这些系统调用返回0表示无误,返回-1表示出错)

    +

    Process and memory

    fork

    int pid = fork();
    if(pid > 0){
    printf("parent: child's pid = %d\n",pid);
    pid = wait(0);
    printf("child %d is done.\n",pid);
    } else if(pid == 0){
    printf("child : exiting\n");
    } else {
    printf("fork error\n");
    }
    + +

    这是一个利用fork的返回值对于父子进程来说不同这一特点进行编写的例程。其中比较不熟的还是wait(0)这一句的用法。这点具体可以看书中笔记和上面的系统调用表。

    +

    exec

    exec是一个系统调用,它跟exe文件被执行的原理密切相关。当程序调用exec,就会跳转到exec参数文件去执行,原程序exec下面的指令都不再被执行,除非exec因错误而退出。

    +

    exec与fork

    由shell的源码中main函数这一段

    +
    // Read and run input commands.
    while(getcmd(buf, sizeof(buf)) >= 0){
    if(buf[0] == 'c' && buf[1] == 'd' && buf[2] == ' '){
    // Chdir must be called by the parent, not the child.
    buf[strlen(buf)-1] = 0; // chop \n
    if(chdir(buf+3) < 0)
    fprintf(2, "cannot cd %s\n", buf+3);
    continue;
    }
    if(fork1() == 0)
    runcmd(parsecmd(buf));
    wait(0);
    }
    exit(0);
    + +
    void runcmd(struct cmd *cmd)
    {
    if(cmd == 0)
    exit(1);

    switch(cmd->type){
    ...
    case EXEC:
    ecmd = (struct execcmd*)cmd;
    exec(ecmd->argv[0], ecmd->argv);
    fprintf(2, "exec %s failed\n", ecmd->argv[0]);
    break;
    ...

    exit(0);
    }
    -

    其中start0:

    -
    __attribute__ ((aligned (16))) char stack0[4096 * NCPU];
    +

    可以看到shell其实本质上就是这样的架构架构:

    +
    while(true){
    if(读到了command&&fork()==0){
    exec(command);
    printf("失败信息");
    }
    wait(0);
    }
    -
    start.c

    在start.c中,我们的任务是在machine mode下,获取machine mode才能访问到的硬件参数,做在machine mode 下才能做的时钟初始化【 it programs the clock chip to generate timer interrupts】,然后进行machine mode到内核态的切换,最后跳转到main.c进行操作系统的初始化和第一个进程的启动。

    -

    而其中,如果想从machine mode切换到内核态,就需要使用mret指令。但是mret指令除了会切换mode之外,还有一个“ret”的作用,并且是从machine mode ret到内核态。

    +

    也即父进程创建出子进程来执行command,并且父进程等待子进程执行完再继续等待输入。

    +

    可以看到,fork和exec的使用是非常紧密的,联合使用也是非常顺理成章的。那么,如果干从fork的exec的对于内存管理的原理来讲,就会不免产生一点问题。

    -

    This instruction( mret ) is most often used to return from a previous call from supervisor mode to machine mode.

    +

    问题描述:

    +

    fork的内存原理,实质上是开辟一片新的与父进程等大的内存空间,然后把父进程的数据都copy一份进这个新内存空间。exec的原理是用一片可以容纳得下文件指令及其所需空间的内存空间去替代调用进程原有的那片内存空间。

    +

    可以看到,如果fork和exec接连使用,理论上其实是会产生一点浪费的,fork创建子进程复制完了一片内存空间,这片新复制的内存空间又马上被扔掉了,取而代之的用的是exec的内存空间。

    -

    所以,我们实际上可以把最后两步连起来,用mret一个指令就完成。也即,mret指令既完成了从machine mode到内核态的切换,又完成了从start.c到main.c的跳转。

    -

    这其实很容易,只需在栈中将调用者(此时应该是entry.S)的地址替换为main.c的地址,并且将调用者的mode改为内核态,这样就ok了。

    +

    为了解决这个问题,kernel使用了copy-on-write技术优化。

    +

    I/O and File descriptors

    文件描述符

    句柄就是一个int值,它代表了一个由内核管理的,可以被进程读写的对象.

    -

    it sets the previous privilege mode to supervisor in the register mstatus, it sets the return address to main by writing main’s address into the register mepc, disables virtual address translation in supervisor mode by writing 0 into the page-table register satp, and delegates all interrupts and exceptions to supervisor mode

    -

    后面两点不大明白。为什么为了mret,就还得让内核态跟machine mode一样关闭虚拟地址映射,还得把什么中断和异常委托给内核态??

    -

    【我猜测是因为现在页表还没初始化好所以当然得关闭虚拟地址映射();后者大概是开中断的意思?】

    +

    A process may obtain a file descriptor by opening a file, directory, or device, or by creating a pipe, or by duplicating an existing descriptor.

    -

    代码如下:

    -
    // entry.S jumps here in machine mode on stack0.
    void
    start()
    {
    //修改调用者为内核态
    // set M Previous Privilege mode to Supervisor, for mret.
    unsigned long x = r_mstatus();
    x &= ~MSTATUS_MPP_MASK;
    x |= MSTATUS_MPP_S;
    w_mstatus(x);

    // set M Exception Program Counter to main, for mret.
    // requires gcc -mcmodel=medany
    w_mepc((uint64)main);

    // disable paging for now.
    w_satp(0);

    // delegate all interrupts and exceptions to supervisor mode.
    w_medeleg(0xffff);
    w_mideleg(0xffff);
    w_sie(r_sie() | SIE_SEIE | SIE_STIE | SIE_SSIE);

    // configure Physical Memory Protection to give supervisor mode
    // access to all of physical memory.
    w_pmpaddr0(0x3fffffffffffffull);
    w_pmpcfg0(0xf);

    // ask for clock interrupts.
    timerinit();

    // keep each CPU's hartid in its tp register, for cpuid().
    int id = r_mhartid();
    w_tp(id);

    // switch to supervisor mode and jump to main().
    asm volatile("mret");
    }
    - -
    main.c

    main.c的作用是做很多很多init。其中,它通过userinit();来创建第一个进程,这个第一个进程再由main调用scheduler()来被调度执行。

    -
    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 {
    while(started == 0)
    ;
    __sync_synchronize();
    /*
    RISC-V的处理器对底层提供了一种特殊的抽象,Hardware Thread,简称为Hart。简单来说,Hart是真实物理CPU(bare metal)提供的一种模拟
    */
    printf("hart %d starting\n", cpuid());
    kvminithart(); // turn on paging
    trapinithart(); // install kernel trap vector
    plicinithart(); // ask PLIC for device interrupts
    }

    //调用第一个scheduler,完成对scheduler线程的初始化,并且调度去执行第一个进程
    scheduler();
    }
    - +

    每个进程的其三个句柄有默认值:

    -

    注:关于里面的cpuid,我查了一下,指的是CPU的序列号,用来唯一标识cpu的。我想这个if架构的目的应该跟fork()==0差不多。也就是说,一开始的那个init仅有cpuid==0的CPU执行,其他的CPU就乖乖wait,只有CPU0执行初始化的程序。等到CPU0执行完所有init,才置标记位start=1,然后通过条件变量start控制抢占调度,轮流初始化自己。其中__sync_synchronize是GNU内置指令,起内存屏障作用。在竞赛中深刻地了解过了内存屏障,在这里再次跟老熟人再会感觉还是很有意思的。

    +

    By convention, a process reads from file descriptor 0 (standard input), writes output to file descriptor 1 (standard output), and writes error messages to file descriptor 2 (standard error).

    -
    proc.c中的userinit()

    userinit的作用就是新创建一个进程信息proc,然后开始给第一个程序(initcode)填信息填入proc。这个进程创建完后,在main中的scheduler被调度执行。

    -
    void
    userinit(void)
    {
    struct proc *p;

    p = allocproc();
    initproc = p;

    // 申请一页,将initcode的指令和数据放进去
    // allocate one user page and copy initcode's instructions
    // and data into it.
    uvmfirst(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对应着standard input,1对应着standard output,2对应着standard error。

    +

    read、write

    read和write的参数都是句柄,buf,读/写长度。都会导致文件指针的移动。使用如下例程【类似cat的原理】:

    +
    char buf[512];
    int n;

    for(;;){
    n = read(0);//从标准输入读
    if(n == 0){
    break;
    }
    if(n < 0){
    fprintf(2,"read error\n");
    exit(1);
    }
    if(write(1,buf,n) != n){//向标准输出写
    fprintf(2,"write error\n");
    exit(1);
    }
    }
    -
    initcode.S

    以上程序都位于kernel/下。这个位于user/下。

    -

    它调用exec系统调用进入了内核态。当exec完成后,它就跳转到了用户态user/init.c中。【这里估计又用了修改返回地址的trick】

    -
    .globl start
    start:
    la a0, init
    la a1, argv
    li a7, SYS_exec
    ecall
    # char init[] = "/init\0";
    init:
    .string "/init\0"

    # char *argv[] = { init, 0 };
    .p2align 2
    argv:
    .long init
    .long 0
    +

    close

    close函数释放了一个句柄,以后它释放掉的这个句柄就可以被用来表示别的文件了。

    +

    open

    open函数会给参数的file分配一个句柄。这个句柄通常是目前空闲的句柄中值最小的那个。

    +

    重定向的实现

    char *argv[2];

    argv[0] = "cat";
    argc[1] = 0;
    if(fork() == 0){
    close(0);
    open("input.txt",O_RDONLY);
    exec("cat",argv);
    }
    -
    init.c

    在init.c中,创建了console设备文件,打开了012文件描述符,并且fork了一个子进程,开始执行shell。这样一来,操作系统就完成了全部的启动。

    -

    感想

    -

    我的疑点有三个:

    -
      -
    1. 见start.c

      -
    2. -
    3. 是怎么完成从内核态到用户态的切换的?是执行了return就会自动切换吗?userinit中设置了initcode的信息为用户态的,然后就直接能进入用户态,这里感觉有点模糊。

      -

      其实用户态和内核态本质上好像差别不大,似乎也就只有两方面,一个是页表(虚拟地址),另一个就是权限问题了。前者很好说,在main.c中完成了页表初始化,开启了虚拟地址:

      -
      kvminit();       // create kernel page table
      kvminithart(); // turn on paging
      +

      xv6的重定向实现跟这个原理差不多:

      +
      case REDIR:
      rcmd = (struct redircmd*)cmd;
      close(rcmd->fd);
      if(open(rcmd->file, rcmd->mode) < 0){
      fprintf(2, "open %s failed\n", rcmd->file);
      exit(1);
      }
      runcmd(rcmd->cmd);
      break;
      -

      后者的话,从用户态切到内核态使用ecall指令,从machine mode到内核态需要修改mstatus寄存器并且使用mret指令:

      -
      // set M Previous Privilege mode to Supervisor, for mret.
      unsigned long x = r_mstatus();
      x &= ~MSTATUS_MPP_MASK;
      x |= MSTATUS_MPP_S;
      w_mstatus(x);
      ...
      // switch to supervisor mode and jump to main().
      asm volatile("mret");
      +

      共享偏移量

      fork出来的父子进程同一个句柄对同一个文件的偏移量是相同的,这个原理应该是因为,父子进程共享的是文件句柄这个结构体对象本身,也就是拷贝的时候是浅拷贝而不是深拷贝。

      +
      if(fork() == 0) {
      write(1, "hello ", 6);
      exit(0);
      } else {
      wait(0);
      write(1, "world\n", 6);
      }
      + +

      dup

      dup系统调用复制一个现有的文件描述符,返回一个引用自同一个底层I/O对象的新文件描述符。

      +

      dup和open一样,都是会占用一个新的句柄的,而且都是优先分配数值小的。比如说fd = dup(3),得到fd=4,那么结果就是句柄3和句柄4指向同一个文件,并且偏移量一样。

      +

      dup可以让这样的指令变得可以实现:

      +
      ls existing-file non-existing-file > tmp1 2>&1
      -

      因而从内核态切换到用户态应该也是需要类似这段对mstatus寄存器的修改的,并且其对应修改的是sstatus寄存器。

      -

      但是,我只在普通的用户态-trap入内核态-用户态这个过程的usertrapret中看到对sstatus寄存器的写入,并没有在init的时候对这个玩意进行写入。

      -

      所以,最后,我初步猜测,是会在scheduler()中的上下文切换中修改sstatus寄存器的内容为user mode,从而实现由内核态向用户态进程(initcode)的切换。不过这也仅仅是【猜想】,因为我并没有在switch的汇编代码中看到对sstatus的修改。真是令人麻木。。。

      -
    4. -
    -

    步骤十分直接且有理由:

    -

    加载操作系统——为了能执行C语言需要一个栈,所以得执行造一个的代码,然后再进入C语言zone——做点machine mode才能做的事,然后从machine mode切换到内核态——做点内核态才能做的事,从内核态切换到用户态

    -
    -

    linux0.11

    bootsect -> setup -> head.s ->main.c

    -

    加载操作系统

    系统加电,启动BIOS初始化硬件 -> BIOS从引导扇区将加载程序读入内存 -> 加载程序将操作系统镜像读入内存RAM。

    -

    其中,第二三步做进一步的细化。

    -
    读入bootsect.s

    加载程序的512个字节被读入到内存从0x7c00开始的一段内存中,并且BIOS设置CS=07c0,ip=0,开始执行加载程序的每一条指令。

    -
    bootsect.s

    加载程序的代码为bootsect.s。在bootsect.s中,首先将自身从7c00处移动到了9000处【留下空间放操作系统】,然后分别依次读取磁盘的setupsystem模块,最后bootsect将控制权转交给setup。

    -
    setup.s

    setup首先获取操作系统运行的必要硬件参数

    -

    image-20230108011824655

    -

    再然后,将system代码移到0地址。然后,我们就需要进入system代码块。

    -

    image-20230108012316631

    -

    最后一句jmpi指令本来应该是要跳到system代码段首0地址处的的,可此处却跳到了80处,这显然不合理。但它写的肯定是没错的。之所以会有这样的矛盾,是因为setup在此之前,还做了一件事情:改变寻址方式。jmpi上面的那条mov指令便做了这点。

    -

    我们之前的寻址方式一直是cs<<4+ip。但是这东西只能是16位的内存,无法满足寻址需求。故而setup要从16位切换到32位。32位模式也叫保护模式。

    -
    -

    至于怎么切的呢?要注意到一点,改变寻址方式也即改变cs和ip的地址计算方法,也即换一条硬件电路实现。计算机给我们提供了一个简单的方式操纵保护模式的转变,即修改cr0寄存器的内容。

    -
    -

    在保护模式下,寻址方式发生了改变。此时cs不再代表基址,而是表示地址在gdb表global description table中的偏移下标。真正的基址放在表项中。cs被称为selector,从表中取得基址,再和ip加在一起得到地址。

    -

    gdt表的内容由setup初始化

    -

    image-20230108013156116

    -
    -

    这样一来,就正确跳到了system模块。

    -

    操作系统初始化

    head.s

    跳到system第一个文件,也就是head.s去执行。

    -

    head.s也是在保护模式下进行的,是在保护模式下的初始化。

    -

    head.s建立了真正的gdt表,然后就要跳转到main.c执行初始化和Shell的启动。此处有汇编语言和C语言的转化,也就是push参数然后push main的地址。

    -
    main.c

    对各种东西的初始化。

    -

    image-20230108013849664

    -

    最后完成从内核态到用户态的切换。

    -

    感想

    -

    linux0.11的启动的具体思路是:

    -

    加载操作系统,获取硬件参数,进入保护模式,跳转到操作系统第一行代码——操作系统初始化,切换到用户态

    -

    linux0.11相比于xv6更加复杂,上课的时候隐藏了很多实现细节但依旧理解很费劲(。

    -

    这两个步骤思路其实都是差不多的,区别在于linux0.11好像没有machine mode这个概念。感觉也不能锐评什么,因为看完了感觉两个都很有道理,两个都一样很难懂(。

    -

    【注:为什么没有machine mode呢?是因为这个mode的划分是RISC-V架构做的,而linux0.11是基于X86架构。】

    -

    不过linux0.11这里进入保护模式后改变寻址方式是因为机器问题(好像是),xv6难道也是因为硬件问题吗?因为一开始的时候操作系统还未进行内存分页页表初始化,所以用不了地址映射?有待学习。

    -

    关于保护模式,可以看看这篇文章,今天太晚了先睡了:

    -

    Linux从头学08:Linux 是如何保护内核代码的?【从实模式到保护模式】

    -
    -

    Real world

    现实中,大多数操作系统都会兼顾宏内核与微内核。

    -

    大多数操作系统都支持与xv6类似的process进程概念,也有很多系统还支持线程概念。

    -

    Lab system calls

    -

    To start the lab, switch to the syscall branch:

    -
    $ git fetch
    $ git checkout syscall
    $ make clean
    -
    -

    trace

    -

    In this assignment you will add a system call tracing feature that may help you when debugging later labs.

    -

    You’ll create a new trace system call that will control tracing. It should take one argument, an integer “mask”, whose bits specify which system calls to trace.

    -

    For example, to trace the fork system call, a program calls trace(1 << SYS_fork). You have to modify the xv6 kernel to print out a line when each system call is about to return. The line should contain the process id, the name of the system call and the return value; you don’t need to print the system call arguments.

    -

    The trace system call should enable tracing for the process that calls it and any children that it subsequently forks, but should not affect other processes.

    +

    这个指令的意思是,先把stderr的结果重定向到stdout,再把stdout的结果重定向到tmp1中。

    +

    关于2>&1的解释,可以看这个 shell中的”2>&1”是什么意思?

    -

    感想

    一开始为了把trace做得封装性良好一些尽量不改别的代码,想了好久好久,最后就只能想出,在syscall.c获取系统调用返回值处加个条件打印,在trace中维护一个map,映射进程pid和进程当前的mask,并且给外界提供查询当前进程是否对某个系统调用有mask作为syscall条件打印的接口。

    -

    这个最后还是失败了,失败的点在于不知道要创建多大的数组来作为map映射所有进程,因为pid分配估计是递增的,是会超过最大进程数的,所以pid会是多少是不确定的。还有一点就是fork之后子进程不能自动继承父进程的mask,还得手动调用一下trace,这更加不封装了(。

    -

    总之先放上我原来的代码吧。

    -
    // in kernel/syscall.c
    #include "types.h"
    #include "param.h"
    #include "memlayout.h"
    #include "riscv.h"
    #include "spinlock.h"
    #include "proc.h"
    #include "syscall.h"
    #include "defs.h"

    //...

    void strcpy(char* buf,const char* tmp){
    int i=0;
    while((*tmp)!='\0'){
    buf[i++] = *tmp;
    tmp++;
    }
    buf[i] = '\0';

    }

    void getname(int callid,char* buf){
    switch(callid){
    case SYS_fork: strcpy(buf,"fork"); break;
    case SYS_exit: strcpy(buf,"exit"); break;
    case SYS_wait: strcpy(buf,"wait"); break;
    case SYS_pipe: strcpy(buf,"pipe"); break;
    case SYS_read: strcpy(buf,"read"); break;
    case SYS_kill: strcpy(buf,"kill"); break;
    case SYS_exec: strcpy(buf,"exec"); break;
    case SYS_fstat: strcpy(buf,"fstat"); break;
    case SYS_chdir: strcpy(buf,"chdir"); break;
    case SYS_dup: strcpy(buf,"dup"); break;
    case SYS_getpid: strcpy(buf,"getpid"); break;
    case SYS_sbrk: strcpy(buf,"sbrk"); break;
    case SYS_sleep: strcpy(buf,"sleep"); break;
    case SYS_uptime: strcpy(buf,"uptime"); break;
    case SYS_open: strcpy(buf,"open"); break;
    case SYS_write: strcpy(buf,"write"); break;
    case SYS_mknod: strcpy(buf,"mknod"); break;
    case SYS_unlink: strcpy(buf,"unlink"); break;
    case SYS_link: strcpy(buf,"link"); break;
    case SYS_mkdir: strcpy(buf,"mkdir"); break;
    case SYS_close: strcpy(buf,"close"); break;
    case SYS_trace: strcpy(buf,"trace"); break;
    default: return;
    }
    }

    void
    syscall(void)
    {
    int num;
    struct proc *p = myproc();

    num = p->trapframe->a7;
    if(num > 0 && num < NELEM(syscalls) && syscalls[num]) {
    p->trapframe->a0 = syscalls[num]();
    char buf[32];
    getname(num,buf);
    // 在此处添加条件打印
    if(istraced(num))
    printf("syscall %s -> %d\n",buf,p->trapframe->a0);
    } else {
    printf("%d %s: unknown sys call %d\n",
    p->pid, p->name, num);
    p->trapframe->a0 = -1;
    }
    }
    +

    这个的实现就要用到dup了。我们会fork一个子进程,在子进程里面close(2),然后再dup(1)。这样一来,我们就成功实现了句柄1和2指向同一个文件

    +

    Pipe

    使用

    int pipe(int p[]) 创建一个管道,把read/write文件描述符放在p[0]和p[1]中

    +
    int p[2];
    char* argv[2];

    argv[0] = "wc";
    argv[1] = 0;

    pipe(p);
    if(fork() == 0){
    close(0);
    dup(p[0]);
    close(p[0]);
    close(p[1]);
    } else{
    close(p[0]);
    write(p[1],"hello world\n",12);
    close(p[1]);
    }
    -
    // in kernel/trace.c
    #include "types.h"
    #include "param.h"
    #include "memlayout.h"
    #include "riscv.h"
    #include "spinlock.h"
    #include "proc.h"
    #include "defs.h"
    #include "elf.h"

    int m_mask[NPROC];

    int
    trace(int mask){
    struct proc *p = myproc();
    m_mask[p->pid] = mask;
    return 1;
    }

    //提供给外界查询的接口
    int
    istraced(int callid){
    struct proc *p = myproc();
    //printf("from trace ,pid = %d\n",p->pid);
    if(((m_mask[p->pid] >> callid) & 1) == 1){
    return 1;
    } else{
    return 0;
    }
    }
    +

    完成了父进程-pipe-子进程的一个重定向。

    +

    pipe是阻塞的生产者消费者模式。对管道的read,在没有数据输入时会阻塞,直到读到数据,或者所有的write方向都被关闭。示例代码中,如果不使用pipe就需要显示close(p[0]) close(p[1]),正是为了防止没有数据输入时write方向不为0导致死锁的情况出现。

    +

    实现管道命令

    管道命令的实现正是通过pipe。

    +

    执行原理就是,创建两个子进程分别执行左右两侧的句子,然后左侧子进程的out重定向到pip的write,右侧子进程的in重定向到pip的read。

    +
     case PIPE:
    pcmd = (struct pipecmd*)cmd;
    if(pipe(p) < 0)
    panic("pipe");
    //左
    if(fork1() == 0){
    close(1);
    dup(p[1]);
    close(p[0]);
    close(p[1]);
    runcmd(pcmd->left);
    }
    //右
    if(fork1() == 0){
    close(0);
    dup(p[0]);
    close(p[0]);
    close(p[1]);
    runcmd(pcmd->right);
    }
    //中
    close(p[0]);
    close(p[1]);
    //wait
    wait(0);
    wait(0);
    break;
    -

    下面是按照hints修改后的正确代码。

    -

    代码步骤

    实际上,标答跟我的思路差不多,只不过它没有像我一样创建数组作为map,而是在proc结构体里添加了一个属性,这本质上也是利用了map。

    -
    在各种文件添加签名
    user/user.h
    user/usys.pl
    syscall.h

    添加系统调用号

    -
    syscall.c

    添加系统调用号和sys_trace映射

    -
    修改Makefile
      -
    1. 在第一个OBJS添加trace.o
    2. -
    3. 在UPROGS添加user中的trace
    4. -
    -
    代码
    修改proc.h
    // Per-process state
    struct proc {
    // ...
    int mask; //记录trace的mask
    };
    +

    这实际上是二叉树的左右中递归过程。

    +
    +

    附:对于管道命令的解读

    +
    cat a.txt | echo
    -
    编写trace.c
    #include "types.h"
    #include "param.h"
    #include "memlayout.h"
    #include "riscv.h"
    #include "spinlock.h"
    #include "proc.h"
    #include "defs.h"
    #include "elf.h"

    int
    trace(int mask){
    struct proc *p = myproc();
    p->mask = mask;
    return 1;
    }

    int
    istraced(int callid){
    struct proc *p = myproc();
    if(((p->mask >> callid) & 1) == 1){
    return 1;
    } else{
    return 0;
    }
    }
    +

    我的本意是觉得,这意思就是把cat a.txt的输出连到echo的输入,这个命令结果跟cat a.txt是没什么差的。但具体执行出来发现最后的结果却是跟:

    +
    echo
    -
    修改syscall.c
    // in kernel/syscall.c
    #include "types.h"
    #include "param.h"
    #include "memlayout.h"
    #include "riscv.h"
    #include "spinlock.h"
    #include "proc.h"
    #include "syscall.h"
    #include "defs.h"

    //...

    void strcpy(char* buf,const char* tmp){
    int i=0;
    while((*tmp)!='\0'){
    buf[i++] = *tmp;
    tmp++;
    }
    buf[i] = '\0';

    }

    void getname(int callid,char* buf){
    switch(callid){
    case SYS_fork: strcpy(buf,"fork"); break;
    case SYS_exit: strcpy(buf,"exit"); break;
    case SYS_wait: strcpy(buf,"wait"); break;
    case SYS_pipe: strcpy(buf,"pipe"); break;
    case SYS_read: strcpy(buf,"read"); break;
    case SYS_kill: strcpy(buf,"kill"); break;
    case SYS_exec: strcpy(buf,"exec"); break;
    case SYS_fstat: strcpy(buf,"fstat"); break;
    case SYS_chdir: strcpy(buf,"chdir"); break;
    case SYS_dup: strcpy(buf,"dup"); break;
    case SYS_getpid: strcpy(buf,"getpid"); break;
    case SYS_sbrk: strcpy(buf,"sbrk"); break;
    case SYS_sleep: strcpy(buf,"sleep"); break;
    case SYS_uptime: strcpy(buf,"uptime"); break;
    case SYS_open: strcpy(buf,"open"); break;
    case SYS_write: strcpy(buf,"write"); break;
    case SYS_mknod: strcpy(buf,"mknod"); break;
    case SYS_unlink: strcpy(buf,"unlink"); break;
    case SYS_link: strcpy(buf,"link"); break;
    case SYS_mkdir: strcpy(buf,"mkdir"); break;
    case SYS_close: strcpy(buf,"close"); break;
    case SYS_trace: strcpy(buf,"trace"); break;
    default: return;
    }
    }

    void
    syscall(void)
    {
    int num;
    struct proc *p = myproc();

    num = p->trapframe->a7;
    if(num > 0 && num < NELEM(syscalls) && syscalls[num]) {
    p->trapframe->a0 = syscalls[num]();
    char buf[32];
    getname(num,buf);
    // 在此处添加条件打印
    if(istraced(num))
    printf("syscall %s -> %d\n",buf,p->trapframe->a0);
    } else {
    printf("%d %s: unknown sys call %d\n",
    p->pid, p->name, num);
    p->trapframe->a0 = -1;
    }
    }
    +

    这个指令的效果是一样的,也就是cat a.txt的output,即echo的input完全被丢弃了。

    +

    我想这是因为,echo这个命令的执行过程并没有用到stdin,仅仅用到了参数,也就是说管道read端的接入对它并没有什么影响。

    +

    这也是为啥

    +
    sleep 10 | echo hi
    -
    在sysproc.c中添加系统调用
    uint64
    sys_trace(void)
    {
    int mask;
    if(argint(0,&mask)<0)
    return -1;
    trace(mask);
    return 0;
    }
    +

    这个命令最后的结果是,秒速出hi,然后等待10s后结束,了。由于echo的输出与stdin没有关系,所以,echo不会阻塞读入stdin,等待管道关闭,而是会即刻输出hi。

    +
    +

    管道实际上就相当于:

    +
    echo hello world | wc
    echo hello world > /tmp/xyz; wc < /tmp/xyz
    -
    修改fork

    继承父进程的mask

    -
    np->mask = p->mask;
    +

    在这种情况下,管道相比临时文件至少有四个优势

    +
      +
    • 首先,不用删文件
    • +
    • 其次,管道可以任意传递长的数据流
    • +
    • 第三,管道允许一定程度上的并行
    • +
    • 第四,如果实现进程间通讯,管道的块读写比文件的非块语义更有效率。
    • +
    +

    File system

    inode:代表文件本体,包括文件类型、文件长度、文件内容在磁盘位置、文件的链接数

    +

    link:指向文件的链接,一个文件可以有多个link,link内包含文件名和对inode的引用

    +

    当链接数=0,且句柄数=0,文件的磁盘空间和inode索引就会被释放

    +

    Lab Xv6 and Unix utilities

    配置实验环境

    +

    参考文章:

    +

    xv6环境搭建

    +

    【MIT6.S081/6.828】手把手教你搭建开发环境

    +
    +

    下载工具链

    $ sudo apt-get install git build-essential gdb-multiarch qemu-system-misc gcc-riscv64-linux-gnu binutils-riscv64-linux-gnu 
    -
    在defs.h中添加需要public的函数签名
    // trace.c
    int trace(int);
    int istraced(int);
    +

    测试安装ok:

    +
    $ qemu-system-riscv64 --version
    QEMU emulator version 5.1.0
    //下面其中之一正常就行
    $ riscv64-linux-gnu-gcc --version
    riscv64-linux-gnu-gcc (Debian 10.3.0-8) 10.3.0
    ...
    $ riscv64-unknown-elf-gcc --version
    riscv64-unknown-elf-gcc (GCC) 10.1.0
    ...
    $ riscv64-unknown-linux-gnu-gcc --version
    riscv64-unknown-linux-gnu-gcc (GCC) 10.1.0
    ...
    -

    sysinfotest

    -

    In this assignment you will add a system call, sysinfo, that collects information about the running system.

    -

    The system call takes one argument: a pointer to a struct sysinfo (see kernel/sysinfo.h).

    -

    The kernel should fill out the fields of this struct: the freemem field should be set to the number of bytes of free memory, and the nproc field should be set to the number of processes whose state is not UNUSED.

    -

    We provide a test program sysinfotest; you pass this assignment if it prints “sysinfotest: OK”.

    +
    +

    注,这里出现了一个问题,qemu-system-riscv64 --version打出来发现qemu-system-riscv64 command not found。似乎是我的ubuntu16.04版本太低了【悲】去看了下网上,可以按照这个来做:

    +

    rCore qemu risc-v 实验环境配置

    -
    // kernel/sysinfo.h
    struct sysinfo {
    uint64 freemem; // amount of free memory (bytes)
    uint64 nproc; // number of process
    };
    +

    下载编译xv6源码

    随后,进入一个你喜欢的文件夹clone xv6的实验源码,输入

    +
    $ git clone git://g.csail.mit.edu/xv6-labs-2020
    $ cd xv6-labs-2020
    $ git checkout util
    -

    感想

    代码

    系统调用要做的事情同上。

    -

    有一个我在hit实验没想到,在这里依然没有想到的点是,参数的指针来自用户空间,所以不能直接对其指向的空间进行写入,需要借助copyout函数。

    -

    还有一件事,就是不知道该怎么统计free mem的数量,后来在hints提示下才知道要去kalloc.c中找。【之前只找过了vm.c】这里其实是很后悔提前看了提示的。我应该先去看一下上面关于kernel各个文件用途的笔记,再去继续自己找的,不能太过依赖提示。

    -

    还有一点做的不好的地方是,标答是选择了将两个计数函数放在各自的文件中,我是选择直接将成员变量在头文件中extern 公开出来,比如说在proc.h中这么写:

    -
    extern struct proc proc[NPROC];
    +

    然后进行编译

    +
    $ make
    $ make qemu
    -

    hints采取了比我封装性更好的操作,这也是非常顺理成章的,我没有想到这样真是有点惭愧(。

    -

    总而言之,这个还是挺简单的,就是我很后悔我心浮气躁看了提示,要不然收获会更多。

    -
    sysinfo.c
    #include "types.h"
    #include "param.h"
    #include "memlayout.h"
    #include "riscv.h"
    #include "spinlock.h"
    #include "proc.h"
    #include "defs.h"
    #include "elf.h"
    #include "sysinfo.h"

    int
    sysinfo(struct sysinfo* info){
    struct sysinfo res;
    res.nproc = countproc();
    res.freemem = countfree();
    struct proc *p = myproc();
    if(copyout(p->pagetable, (uint64)info,(char *)(&res), sizeof(res)) != 0)
    return -1;
    return 1;
    }
    +

    如果此处发生错误:unrecognized command line option -mno-relax,则按照此说法 xv6环境搭建更新gcc版本

    +
    $ sudo apt install gcc-8-riscv64-linux-gnu
    $ sudo update-alternatives --install /usr/bin/riscv64-linux-gnu-gcc riscv64-linux-gnu-gcc /usr/bin/riscv64-linux-gnu-gcc-8 8
    -
    sysproc.c中
    uint64
    sys_sysinfo(void){
    uint64 addr;
    if(argaddr(0, &addr) < 0)
    return -1;
    return sysinfo((struct sysinfo*)addr);
    }
    +

    再执行一次

    +
    $ make
    $ make qemu
    -
    kalloc.c中
    // 采用的是链表结构,run代表一页
    struct run {
    struct run *next;
    };

    struct {
    struct spinlock lock;
    // 指向第一个空闲页
    struct run *freelist;
    } kmem;

    int
    countfree(){
    int npage = 0;
    struct run* r = kmem.freelist;
    while(r){
    r = r->next;
    npage++;
    }
    return npage*PGSIZE;
    }
    +

    就ok了。

    +

    关闭qemu

    qemu退出操作

    +

    在这里记个强制方法:

    +
    ps -elf | grep qemu
    + +

    image-20230105153458808

    +

    记住第二个的pid

    +

    然后

    +
    kill 3303
    + +

    测试gdb是否ok

    见该文章最后一部分

    +

    【MIT6.S081/6.828】手把手教你搭建开发环境

    +

    自测方法

    make grade
    + +

    或者如果只想测其中一个,可以:

    +
    ./grade-lab-util sleep
    + +

    make qemu后卡住

    疑似qemu版本不对。解决方法

    +

    实验内容

    编写sleep.c

    +

    Implement the UNIX program sleep for xv6; your sleep should pause for a user-specified number of ticks. A tick is a notion of time defined by the xv6 kernel, namely the time between two interrupts from the timer chip. Your solution should be in the file user/sleep.c.

    +

    image-20230105164146100.png

    +
    +
    体会
    参数

    注意,他要求我们实现的sleep的参数是ticks的数量,不是秒数。我花了半天找时钟周期大小这个参数在哪,找了许久没找到,估计是没考虑到这一点。

    +

    比如说,我翻了一下linux0.11的源码,在include/linux/time.h下有这句:

    +

    image-20230105162505574.png

    +

    说明了时钟频率大小。在xv6好像没有看到对这个的显式说明。

    +
    系统调用过程

    感受了一下xv6的系统调用过程,跟linux0.11还是很相像的。

    +

    这个好像是lab2的内容,我暂且先在此放下我体会到的感受。

    +
      +
    1. xv6

      +

      首先是从用户态到内核态的切换。

      +

      在user/user.h中有各个系统调用外化的函数签名。在用户程序中调用里面的函数签名,就会执行【说实话,我没看懂为什么这里会知道要从user.h跳到usys.S中执行,也许是Makefile里有写?】user/usys.S中对应的汇编代码,比如说这种:

      +

      image-20230105170701334

      +

      然后这个SYS_close这种,其实是系统调用号宏,被定义在kernel/syscall.h中:

      +

      image-20230105171327076.pn

      +

      li a7,SYS_call就是把SYS_call的值放入a7寄存器,大概就是传参的意思。ecall是从用户态转到内核态的指令。这样一来,就完成了从用户态到内核态的切换。

      +

      然后是在内核态的执行。

      +

      切换到内核态之后的执行步骤跟linux0.11可以说是完全一样。

      +

      首先应该是会去执行kernel/syscall.c中的syscall函数,具体应该是通过ecall引发0x80中断,然后查表得知这个syscall是中断处理函数

      +

      image-20230105172110475.pn

      +

      可以看到,syscall获取了a7里的参数,然后查了系统调用表

      +

      image-20230105173019159

      +

      然后去sysproc.c文件下执行相应的sys_xxx函数。这个函数指针用得真是牛逼。

      +

      再然后,sys_xxx函数中会从栈中取出调用参数,再跳转到xxx(args)函数中去(这些xxx函数一般在kernel中以单独文件形式出现)。

      +

      这样一来,就完成了一次系统调用。

      +
    2. +
    3. linux0.11

      +

      首先是用户态到内核态的切换。

      +

      在用户态中比方说调用system call close(),则会调用lib/close.c下的:

      +

      image-20230105173820813

      +

      展开这个宏之后,是这样的:

      +

      image-20230105173845317

      +

      具体意思就是把close的系统调用号存入参数寄存器,然后引发0x80中断,进入内核态。

      +

      然后是在内核态的执行。

      +

      查表会得知sys_call函数是0x80中断的中断处理函数,然后就会根据参数里的系统调用名字去找系统调用表执行

      +

      image-20230105174832400

      +

      这部分跟xv6差不多,不再赘述

      +
    4. +
    +

    可见,这两个系统在内核态的实现是差不多的,只是在用户态有点稍稍不一样。感觉linux0.11会更加精妙一些。

    +

    编写pingpong程序

    +

    Write a program that uses UNIX system calls to ‘’ping-pong’’ a byte between two processes over a pair of pipes, one for each direction. The parent should send a byte to the child; the child should print “: received ping”, where is its process ID, write the byte on the pipe to the parent, and exit; the parent should read the byte from the child, print “: received pong”, and exit. Your solution should be in the file user/pingpong.c.

    +
    +
    体会

    思路很简单,我之所以写了那么久是因为走了好大的弯路……

    +

    题目要求输出格式为”: received ping”,我的思路固化为:先把pid化成数字,再用字符串拼接串成整个。为了实现我的思路,我就需要额外再写两个工具函数,一个是itoa,一个是strcat。而又由于malloc函数暂待实现,itoa和strcat的实现就仍然不够优雅。折腾了半天终于OK了,结果看到别人是怎么做到这个输出格式的呢?↓

    +
    fprintf(1,"%d: received ping\n",getpid());
    -
    proc.c中
    struct proc proc[NPROC];
    int
    countproc(){
    int nproc = 0;
    for(int i=0;i<NPROC;i++){
    if(proc[i].state != UNUSED){
    nproc++;
    }
    }
    return nproc;
    }
    +

    这下是真的尴尬了23333

    +

    但总而言之,自己写了那俩不够优雅的函数还算是有点用【大概】。以下是我的代码

    +

    编写primes

    +

    参考:

    +

    MIT操作系统实验lab1(案例:primes(质数筛选)附代码、详解)

    +

    XV6实验-Lab0 Utilities

    +
    +
    +

    Write a concurrent version of prime sieve using pipes. This idea is due to Doug McIlroy, inventor of Unix pipes. The picture halfway down this page and the surrounding text explain how to do it. Your solution should be in the file user/primes.c.

    +
    +

    其实就是用生产者消费者模式来写素数计算的并发版本,这个我熟

    +

    ……以上是第一印象。然后我看着超链接文章里的素数筛的图片,以及指导书给的提示:

    +
    +

    Your goal is to use pipe and fork to set up the pipeline. The first process feeds the numbers 2 through 35 into the pipeline. For each prime number, you will arrange to create one process that reads from its left neighbor over a pipe and writes to its right neighbor over another pipe. Since xv6 has limited number of file descriptors and processes, the first process can stop at 35.

    +
      +
    • Be careful to close file descriptors that a process doesn’t need, because otherwise your program will run xv6 out of resources before the first process reaches 35.
    • +
    +
    +

    义无反顾地……使用了35个管道hhhhh

    +

    然后不知道为什么不行,也焦头烂额地感觉我思路太离谱了,去看了下发现大家都是只用一个管道……

    +

    我也搞了个单管道的出来,但是思路受第一篇的影响非常地串行,也即先筛完再创建子进程。看到

    +

    XV6实验-Lab0 Utilities

    +

    这篇文章,才发现还可以那样双管道并行……我虽然也考虑过双管道,但是觉得实现不了【因为我是用循环的思路,如果要双管道的话切换会很麻烦】就没写了,没想到还可以向他那样【他选择的是一个在外部定义的p,和一个作用域更小在每次循环内定义的p1,再加上递归传递参数这个技巧,就可以接连不断递归下去了】,深感佩服。写得是真好,可以去参考学习一下,我懒得改了(

    +
    #include"user/user.h"

    int main(){
    int p[2];
    pipe(p);

    if(fork() == 0){
    while(1){
    char buf[3];
    //读入第一个数字
    read(p[0],buf,3);
    int prime = atoi(buf);
    if(prime == 36){
    close(p[0]);
    close(p[1]);
    exit(0);
    }
    fprintf(1,"prime %d\n",prime);
    //读入其他数字
    int tmp = atoi(buf);
    while(1){
    read(p[0],buf,3);
    tmp = atoi(buf);
    //输入结束
    if(tmp == 36){
    break;
    }
    if(tmp%prime!=0){
    write(p[1],buf,3);
    }
    }
    //作为标记,标志着输入序列结束
    itoa(36,buf);
    write(p[1],buf,3);
    if(fork()){
    }
    else{
    close(p[0]);
    close(p[1]);
    wait(0);
    exit(0);
    }
    }
    } else{
    close(p[0]);
    char buf[3];
    for(int i=2;i<=35;i++){
    itoa(i,buf);
    write(p[1],buf,3);
    }
    //作为标记,标志着输入序列结束
    itoa(36,buf);
    write(p[1],buf,3);
    close(p[1]);
    wait(0);
    }
    exit(0);
    }
    -

    附加题

    trace plus
    -

    Print the system call arguments for traced system calls.

    +

    编写find

    +

    Write a simple version of the UNIX find program: find all the files in a directory tree with a specific name. Your solution should be in the file user/find.c.

    -

    这个实现起来要说简单也简单,麻烦也麻烦。这里就先摆了【实际上尝试了半小时发现太烦了看别人写的也不大满意就放弃了】

    -
    sysinfo plus
    -

    Compute the load average and export it through sysinfo

    +
    初始版

    直接照着ls的模板改,改成递归就ok了。值得注意的是,目录也是一种文件,也可以通过read读取。目录文件的内容就是目录里的所有文件的名字。因而,我们在递归时可以忽略文件,只对目录处理,因为目录中就包含着所有文件名的信息。

    +
    附加题:支持正则表达式

    把user/grep.c里面的匹配函数拿来就行。

    +

    编写xargs

    +

    Write a simple version of the UNIX xargs program: read lines from the standard input and run a command for each line, supplying the line as arguments to the command. Your solution should be in the file user/xargs.c.

    -

    说实话没太看懂,不就加个 running process/ncpu就行了吗?

    +
    体会

    思路还是很直观的,就是从stdin一行一行读入数据,然后把这数据处理成参数,最后调用exec就行。就是中间有很多小细节值得注意。

    +

    有一点比较坑的是,main方法的那个argc的计算方法是这样的,不是直接用数组的长度:

    +
    for(argc = 0; argv[argc]; argc++) 
    + +

    可以看到,合格的argv的形式应该是:参1 参2 参3 “\0”,最后一个元素要以”\0”标志结束。

    +

    这个应该是编写者约定俗成的。在user/sh.c的parseexec,大概445行左右:

    +

    image-20230106172133338

    +

    shell处理命令时是会默认把最后一个清零的。

    +
    +

    确实,后面在学内存的时候,用户空间的构成如图所示:

    +

    image-20230109234930690

    +

    可以看到栈那边,参数列完了之后是会有一个用以terminate的空指针的

    +
    +

    附加题:改善shell

    看起来又难又多所以我先摸了【润】等之后有时间再回来弄吧

    ]]> @@ -9323,1195 +9199,1233 @@ url访问填写http://localhost/webdemo4_war/*.do。 ]]> - Interrupts and device drivers - /2023/01/10/xv6$chap5/ - Interrupts and device drivers
    -

    A driver is the code in an operating system that manages a particular device:

    + Page tables + /2023/01/10/xv6$chap3/ + Page tables

    Paging hardware

    为什么需要页表

    将主存储器以及各种外设接口卡里面内置的存储器连接起来,就形成了内存地址空间。内存地址空间中的地址是真实的物理地址。RISC-V架构的指令使用的地址是虚拟地址。为了通过指令中的虚拟地址访问到真实的物理内存,需要进行从虚拟地址到物理地址的转换。从虚拟地址到物理地址的转换,就需要通过页表来实现。

    +

    页表如何运作

    在RISC-V指令集中,当我们需要开启页表服务时,我们需要将我们预先配置好的页表首地址放入 satp 寄存器中。从此之后, 计算机硬件 将把访存的地址 均视为虚拟地址 ,都需要通过硬件查询页表,将其 翻译成为物理地址 ,然后将其作为地址发送给内存进行访存。

    +

    xv6采用的指令集标准为RISC-V标准,其中页表的标准为SV39标准,也就是虚拟地址最多为39位。

    +

    虚实地址翻译流程:

      -
    1. configures the device hardware
    2. -
    3. tells the device to perform operations
    4. -
    5. handles the resulting interrupts
    6. -
    7. interacts with processes that may be waiting for I/O from the device
    8. +
    9. 获得一个虚拟地址。根页表基地址已经被装填至寄存器 satp 中。
    10. +
    11. 通过 satp 找到根页表的物理页帧号,转成物理地址(Offset为0),通过虚拟地址的L2索引,找到对应的页表项。
    12. +
    13. 通过页表项可以找到找到 次页表 的物理页帧号,转成物理地址(Offset为0),通过虚拟地址的L1索引,找到对应的页表项。
    14. +
    15. 通过页表项可以找到找到 叶子页表 的物理页帧号,转成物理地址(Offset为0),通过虚拟地址的L0索引,找到对应的页表项。
    16. +
    17. 通过页表项可以找到找到 物理地址 的物理页帧号,通过虚拟地址的Offset,转成物理地址(Offset和虚拟地址Offset相同)。
    -

    Driver code can be tricky because a driver executes concurrently with the device that it manages.

    -

    In addition, the driver must understand the device’s hardware interface, which can be complex and poorly documented.

    -
    -

    如果devices需要让操作系统对某些事情做出响应,就要采取中断的方法。在kerneltrap中,内核响应中断,并且根据设备类型来决定中断处理函数。

    +

    页表组成

    页表项

    页表由页表项PTE(Page Table Entries)构成,每个页表项由44位的PPN(Physical Page Number)和一些参数flag组成。

    +

    image-20230109153937459

    -

    image-20230115160523827

    -

    这段对设备中断的概述总结得非常到位

    -

    也就是说,一个device driver可以分为两部分实现,一部分是接收请求,然后开启read/write;另一部分是接收中断,这个中断有可能是设备完成IO,也可能是设备需要IO,它会通知设备具体怎么做,它也会唤醒恰当的进程。

    +

    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行左右。

    -

    Code: Console input

    console driver是driver structure的一个实现案例。

    -

    上层逻辑

    shell获取用户输入console的信息是通过系统调用read()实现的。read通过文件描述符,最终转向consoleread()来实现具体的逻辑。

    -
    // in file.c fileread()
    } 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);// 在这里转向console
    }
    // in console.c consoleinit()
    void
    consoleinit(void)
    {
    initlock(&cons.lock, "cons");

    uartinit();

    // 在这里完成devsw的初始化
    // connect read and write system calls
    // to consoleread and consolewrite.
    devsw[CONSOLE].read = consoleread;
    devsw[CONSOLE].write = consolewrite;
    }
    - -

    对console的读写事实上是对cons结构体里buf的读写。这个buf则是由底层逻辑管理的。consoleread()每次读取buf中的一行,当未读满一行且无字符输入时会阻塞,直到底层逻辑将字符放入buf。读满了一行后,consoleread将该行copy进用户空间,随后返回read

    -
    // in kernel/console.c 

    #define INPUT_BUF_SIZE 128
    struct {
    struct spinlock lock;
    char buf[INPUT_BUF_SIZE];
    uint r; // Read index
    uint w; // Write index
    uint e; // Edit index
    } cons;


    // user read()s from the console go here.
    // copy (up to) a whole input line to dst.
    // user_dist indicates whether dst is a user
    // or kernel address.
    int
    consoleread(int user_dst, uint64 dst, int n)
    {
    uint target;
    int c;
    char cbuf;

    target = n;
    acquire(&cons.lock);
    while(n > 0){
    // wait until interrupt handler has put some
    // input into cons.buffer.
    // read和write的index一样,说明此时没有数据输入,阻塞
    while(cons.r == cons.w){
    if(killed(myproc())){
    release(&cons.lock);
    return -1;
    }
    sleep(&cons.r, &cons.lock);
    }
    // 产生数据输入,接收数据
    c = cons.buf[cons.r++ % INPUT_BUF_SIZE];

    if(c == C('D')){ // end-of-file
    if(n < target){
    // Save ^D for next time, to make sure
    // caller gets a 0-byte result.
    // 这样下一次也能访问到eof
    cons.r--;
    }
    break;
    }

    // copy the input byte to the user-space buffer.
    cbuf = c;
    if(either_copyout(user_dst, dst, &cbuf, 1) == -1)
    break;

    dst++;
    --n;

    if(c == '\n'){
    // a whole line has arrived, return to
    // the user-level read().
    break;
    }
    }
    release(&cons.lock);

    return target - n;
    }
    - -

    底层逻辑

    底层逻辑维护了与上层逻辑交互的buf。

    -

    console接收数据对buf的读,是通过中断来实现的。

    -

    当用户输入字符,UART硬件检测到读,会向操作系统发送中断。中断在kerneltrap()中被接收处理,然后通过devintr()对该中断分门别类地进行转发。console的转发路径为devintr->uartintr->consoleintr。

    -

    UART

    UART的全称是Universal Asynchronous Receiver and Transmitter,即异步发送和接收。它的软件上的表示形式是a set of memory-mapped control registers。CPU通过物理地址与这些寄存器交互,也即它们跟RAM是同一个地址空间。在xv6中,UART的地址空间从UART0(0x1000 0000)开始。这些寄存器地址关于UART0的偏移量定义如下:

    -
    // the UART control registers.
    // some have different meanings for read vs write.
    // see http://byterunner.com/16550.html
    #define RHR 0 // 接收寄存器receive holding register (for input bytes)
    #define THR 0 // 发送寄存器transmit holding register (for output bytes)
    #define IER 1 // 开关中断寄存器
    #define IER_RX_ENABLE (1<<0) // 如果该位被设置,则在接收寄存器有数据,即想向外界发送数据时,UART会搓出一个中断
    #define IER_TX_ENABLE (1<<1) // 如果该位被设置,则在发送寄存器有数据,即外界向硬件发送数据时,UART会搓出一个中断
    #define FCR 2 // FIFO control register
    #define FCR_FIFO_ENABLE (1<<0)
    #define FCR_FIFO_CLEAR (3<<1) // clear the content of the two FIFOs
    // ...
    #define LCR 3 // line control register
    // ...
    #define LSR 5 // line status register
    #define LSR_RX_READY (1<<0) // input is waiting to be read from RHR
    #define LSR_TX_IDLE (1<<5) // THR can accept another character to send
    - +

    虚拟地址有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页。

    +

    三级页表结构相比于单级页表结构,可以节省更多内存空间

    -

    image-20230115170107044

    -

    例如,LSR寄存器包含指示输入字符是否正在等待软件读取的位。这些字符(如果有的话)可用于从RHR寄存器读取。每次读取一个字符,UART硬件都会从等待字符的内部FIFO寄存器中删除它,并在FIFO为空时清除LSR中的“就绪”位。UART传输硬件在很大程度上独立于接收硬件;如果软件向THR写入一个字节,则UART传输该字节。

    +

    参考:页表是啥以及为啥多级页表能够节省空间

    -

    kerneltrap

    // in kerneltrap()
    // 在此处的devintr对不同的设备进行不同的处理方式
    if((which_dev = devintr()) == 0){
    printf("scause %p\n", scause);
    printf("sepc=%p stval=%p\n", r_sepc(), r_stval());
    panic("kerneltrap");
    }
    - -

    devintr

    devintr处在trap.c中,作用是对中断归类,然后分门别类地转发到下一层级的handler。

    +

    考虑到这样一个进程:

    +

    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中内核的页表结构。

    -

    注:

    -
      -
    1. 外中断和内中断

      -

      外部中断和内部中断详解

      -

      根据中断源的不同,可以把中断分为硬件中断和软件中断两大类,而硬件中断又可以分为外部中断和内部中断两类。

      -

      外部中断一般是指由计算机外设发出的中断请求,如:键盘中断、打印机中断、定时器中断等。外部中断是可以屏蔽的中断,也就是说,利用中断控制器可以屏蔽这些外部设备的中断请求。

      -

      内部中断是指因硬件出错(如突然掉电、奇偶校验错等)或运算出错(除数为零、运算溢出、单步中断等)所引起的中断。内部中断是不可屏蔽的中断。

      -

      软件中断其实并不是真正的中断,它们只是可被调用执行的一般程序。例如:ROM BIOS中的各种外部设备管理中断服务程序(键盘管理中断、显示器管理中断、打印机管理 中断等,)以及DOS的系统功能调用(INT 21H)等都是软件中断。【比如说系统调用之类的】

      -
    2. -
    +

    这里为了方便,就把三级页表省略了,只留下va和pa的对比

    -
    // check if it's an external interrupt or software interrupt,
    // and handle it.
    // returns 2 if timer interrupt,
    // 1 if other device,
    // 0 if not recognized.
    int
    devintr()
    {
    // 获取scause,辨析中断类型
    uint64 scause = r_scause();

    // 如果来自外中断(在这里应该只指device interrupt)
    if((scause & 0x8000000000000000L) &&
    (scause & 0xff) == 9){
    // this is a supervisor external interrupt, via PLIC.

    // irq indicates which device interrupted.
    // 通过PLIC硬件获取中断设备信息
    int irq = plic_claim();

    // 分别转发
    if(irq == UART0_IRQ){
    uartintr();
    } else if(irq == VIRTIO0_IRQ){
    virtio_disk_intr();
    } else if(irq){
    printf("unexpected interrupt irq=%d\n", irq);
    }

    // 中断处理完成了,可以再次开启中断
    // the PLIC allows each device to raise at most one
    // interrupt at a time; tell the PLIC the device is
    // now allowed to interrupt again.
    if(irq)
    plic_complete(irq);

    return 1;
    // 来自时钟中断
    } else if(scause == 0x8000000000000001L){
    // ...
    return 2;
    } else {
    return 0;
    }
    }
    - -

    uartintr

    这代码其实乍一看是看不懂的,这是因为uartintr不止负责读中断。它还负责另一个中断(发送区空余中断),下面会细说。

    -
    // handle a uart interrupt, raised because input has
    // arrived, or the uart is ready for more output, or
    // both. called from devintr().
    void
    uartintr(void)
    {
    // read and process incoming characters.
    while(1){
    int c = uartgetc();
    // return -1 if none is waiting,说明读完了
    if(c == -1)
    break;
    // 每读入一个字符就转交给console
    consoleintr(c);
    }

    // send buffered characters.
    acquire(&uart_tx_lock);
    uartstart();
    release(&uart_tx_lock);
    }
    - -

    consoleintr

    向buf中放入字符c

    -
    void
    consoleintr(int c)
    {
    acquire(&cons.lock);

    switch(c){
    case C('P'): // Print process list.
    // ...一堆特殊情况处理...
    default:
    if(c != 0 && cons.e-cons.r < INPUT_BUF_SIZE){
    c = (c == '\r') ? '\n' : c;

    // echo back to the user.
    consputc(c);

    // store for consumption by consoleread().
    cons.buf[cons.e++ % INPUT_BUF_SIZE] = c;

    if(c == '\n' || c == C('D') || cons.e-cons.r == INPUT_BUF_SIZE){
    // wake up consoleread() if a whole line (or end-of-file)
    // has arrived.
    // 中断处理并不会做很多事情,只是会与缓冲区交互
    // 涉及到复杂的事情,比如说将数据拷贝到用户空间
    //就唤醒上层逻辑来做
    cons.w = cons.e;
    wakeup(&cons.r);
    }
    }
    break;
    }

    release(&cons.lock);
    }
    - -

    Code: Console output

    外部通过write这个系统调用来对console写。

    -

    uartputc

    最先到达这里。

    -

    uart内置了一个缓冲区。

    -
    char uart_tx_buf[UART_TX_BUF_SIZE];
    - -

    用户仅需通过uartputc对buf进行写入即可,具体的buf数据向UART转移由uartputc通过调用uartstart实现。

    -
    // add a character to the output buffer and tell the
    // UART to start sending if it isn't already.
    // blocks if the output buffer is full.缓冲区满则阻塞
    // because it may block, it can't be called
    // from interrupts; it's only suitable for use
    // by write().这段话很有意思,说它由于会阻塞所以最好别在中断的时候用。
    void
    uartputc(int c)
    {
    acquire(&uart_tx_lock);

    if(panicked){
    for(;;)
    ;
    }
    // 阻塞
    while(uart_tx_w == uart_tx_r + UART_TX_BUF_SIZE){
    // buffer is full.
    // wait for uartstart() to open up space in the buffer.
    sleep(&uart_tx_r, &uart_tx_lock);
    }
    uart_tx_buf[uart_tx_w % UART_TX_BUF_SIZE] = c;
    uart_tx_w += 1;
    uartstart();
    release(&uart_tx_lock);
    }
    - -

    uartstart

    uartstart的作用是从缓冲区取数据向UART硬件发送。不阻塞。

    -
    // if the UART is idle, and a character is waiting
    // in the transmit buffer, send it.
    // caller must hold uart_tx_lock.
    // called from both the top- and bottom-half.
    void
    uartstart()
    {
    while(1){
    if(uart_tx_w == uart_tx_r){
    // transmit buffer is empty.
    return;
    }

    if((ReadReg(LSR) & LSR_TX_IDLE) == 0){
    // the UART transmit holding register is full,
    // so we cannot give it another byte.
    // it will interrupt when it's ready for a new byte.
    // 当缓冲区满没有选择阻塞,而是先结束
    // 当UART硬件准备好继续接收的时候,UART会发送transmit complete中断,到时候会再继续从buf读取
    return;
    }

    // 一个字符一个字符写
    int c = uart_tx_buf[uart_tx_r % UART_TX_BUF_SIZE];
    uart_tx_r += 1;

    // maybe uartputc() is waiting for space in the buffer.
    wakeup(&uart_tx_r);

    WriteReg(THR, c);
    }
    }
    - -

    当传输过程非常流畅,UART硬件没有阻塞时,以上的代码就能完美阐述发送的过程。但是当UART硬件的transmit阻塞时,过程就会有许多改动。

    -

    transmit complete interrupt

    uartstart中,当UART硬件的transmit满,uartstart就直接return了。

    -

    当UART硬件的transmit空,就会发送transmit complete中断。中断在kerneltrap被接收,经过devintr转发,最终来到了uartintr:

    -
    // handle a uart interrupt, raised because input has
    // arrived, or the uart is ready for more output, or
    // both. called from devintr().
    void
    uartintr(void)
    {
    // read and process incoming characters.
    while(1){
    int c = uartgetc();
    if(c == -1)
    break;
    consoleintr(c);
    }

    // send buffered characters.
    acquire(&uart_tx_lock);
    uartstart();
    release(&uart_tx_lock);
    }
    - -

    此时,第一个while循环会直接退出,因为压根没有get到字符。所以,这时候,就会去执行uartstart,然后继续读未完成读取的缓冲区。

    -

    等到所有都读完了,最后一次发送transmit complete中断时,会在uartstart进入该分支:

    -
    if(uart_tx_w == uart_tx_r){
    // transmit buffer is empty.
    return;
    }
    +

    每个进程都有一个用户级别的页表。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.
    // ...
    -

    然后就不会再发送transmit中断了。

    -

    感觉这点是真的牛逼。uartintr这个函数完美兼顾了两种情况【这也归功于uartstart做得很健壮】:1. 外部输入数据到console,2. 接收数据未结束,继续接收

    -

    Concurrency in drivers

    用户进程与设备之间的读写交流,比如说上面的console,重点依靠于uart_tx_bufcons.buf这两个的正确性。因而,就需要保障它们的并发安全。在上面的代码中,使用到这两个的地方都被锁保护着。

    -

    在kernel中还需要格外注意的一点并发是,一个进程A在等待来自设备的中断,但此时另一个进程B在运行。这时候设备发出中断信号,CPU转入中断处理程序处理中断。此时,中断处理程序的执行不应该涉及到当前被中断进程的代码。例如,中断处理程序不能安全地使用当前进程的页表调用copyout(页表正是跟当前进程息息相关的)。中断处理程序通常做相对较少的工作(例如,只需将输入数据复制到缓冲区),并唤醒上半部分代码来完成其余工作。

    -

    Timer interrupts

    -

    Xv6 uses timer interrupts to maintain its clock and to enable it to switch among compute-bound processes; the yield calls in usertrap and kerneltrap cause this switching.

    +

    由图可知,一直从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.

      -
      // in kernel/trap.c usertrap()
      // give up the CPU if this is a timer interrupt.
      if(which_dev == 2)
      yield();
      - -
      // in kernel/trap.c kerneltrap()
      // give up the CPU if this is a timer interrupt.
      if(which_dev == 2 && myproc() != 0 && myproc()->state == RUNNING)
      yield();
      - +

      它有一点很特殊的是,它实际对应的物理内存是0x80000000开始的一段。也就是说,0x80000000开始的这段内存,既被直接映射了,也被trampoline通过虚拟地址映射了。它被映射了两次。

      +
    2. +
    3. 内核栈

      -

      RISC-V requires that timer interrupts be taken in machine mode, not supervisor mode. As a result, xv6 handles timer interrupts completely separately from the trap mechanism laid out above.

      +

      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可以用来防止内核栈溢出。

      -

      xv6启动时调用过start.cstart.c处于机器态,并准备向内核态过渡。start.c中就对时钟进行了初始化timeinit()。要做的有以下几件事:

      -
        -
      1. program the CLINT hardware (core-local interruptor) to generate an interrupt after a certain delay.
      2. -
      3. set up a scratch area to help the timer interrupt handler save registers and the address of the CLINT registers
      4. -
      5. start sets mtvec to timervec and enables timer interrupts.
      6. +
      -
      // arrange to receive timer interrupts.
      // they will arrive in machine mode at
      // at timervec in kernelvec.S,
      // which turns them into software interrupts for
      // devintr() in trap.c.
      void
      timerinit()
      {
      // each CPU has a separate source of timer interrupts.
      int id = r_mhartid();

      // ask the CLINT for a timer interrupt.
      int interval = 1000000; // cycles; about 1/10th second in qemu.
      *(uint64*)CLINT_MTIMECMP(id) = *(uint64*)CLINT_MTIME + interval;

      // prepare information in scratch[] for timervec.
      // scratch[0..2] : space for timervec to save registers.
      // scratch[3] : address of CLINT MTIMECMP register.
      // scratch[4] : desired interval (in cycles) between timer interrupts.
      uint64 *scratch = &timer_scratch[id][0];
      scratch[3] = CLINT_MTIMECMP(id);
      scratch[4] = interval;
      w_mscratch((uint64)scratch);

      // set the machine-mode trap handler.
      w_mtvec((uint64)timervec);

      // enable machine-mode interrupts.
      w_mstatus(r_mstatus() | MSTATUS_MIE);

      // enable machine-mode timer interrupts.
      w_mie(r_mie() | MIE_MTIE);
      }
      +

      内核使用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;
      }
      -
      -

      计时器中断处理程序必须保证不干扰中断的内核代码。基本策略是处理程序要求RISC-V发出“软件中断”并立即返回。RISC-V用普通陷阱机制将软件中断传递给内核,并允许内核禁用它们。处理由定时器中断产生的软件中断的代码可以在devintr (kernel/trap.c:204)中看到:

      -
      -
      // in kernel/trap.c devintr()
      } else if(scause == 0x8000000000000001L){
      // software interrupt from a machine-mode timer interrupt,
      // forwarded by timervec in kernelvec.S.

      // 只看其中一个CPU的时钟中断计数的意思吗?确实,要是好几个一起来加倍了非常不合理
      if(cpuid() == 0){
      clockintr();
      }

      // acknowledge the software interrupt by clearing
      // the SSIP bit in sip.
      w_sip(r_sip() & ~2);

      return 2;
      }

      void
      clockintr()
      {
      acquire(&tickslock);
      ticks++;
      wakeup(&ticks);
      release(&tickslock);
      }
      +

      其中,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");
      }
      -

      注意,w_sip(r_sip() & ~2);就对应着“RISC-V用普通陷阱机制将软件中断传递给内核”。【应该吧个人理解】

      -
      -

      来源:rCore 手册(rCore tutorial doc)

      -

      riscv 中的中断寄存器

      -

      S 态的中断寄存器主要有 sie(Supervisor Interrupt Enable,监管中断使能), sip (Supervisor Interrupt Pending,监管中断待处理)两个,其中 s 表示 S 态,i 表示中断, e/p 表示 enable (使能)/ pending (提交申请)。 处理的中断分为三种:

      -
        -
      1. SI(Software Interrupt),软件中断
      2. -
      3. TI(Timer Interrupt),时钟中断
      4. -
      5. EI(External Interrupt),外部中断
      6. -
      -

      比如 sie 有一个 STIE 位, 对应 sip 有一个 STIP 位,与时钟中断 TI 有关。当硬件决定触发时钟中断时,会将 STIP 设置为 1,当一条指令执行完毕后,如果发现 STIP 为 1,此时如果时钟中断使能,即 sieSTIE 位也为 1 ,就会进入 S 态时钟中断的处理程序。

      -

      可能SSIP跟这里的STIP差不多吧,都是时钟中断的标志。如果把SSIP clear掉,那么则说明不是时钟中断了,而是软中断了。

      -
      -

      Real world

      -

      UART驱动程序读取UART控制寄存器,一次检索一字节的数据;因为软件驱动数据移动,这种模式被称为程序I/O(Programmed I/O)。程序I/O很简单,但速度太慢,无法在高数据速率下使用。需要高速移动大量数据的设备通常使用直接内存访问(DMA)。DMA设备硬件直接将传入数据写入内存,并从内存中读取传出数据。现代磁盘和网络设备使用DMA。DMA设备的驱动程序将在RAM中准备数据,然后使用对控制寄存器的单次写入来告诉设备处理准备好的数据。

      -

      当一个设备在不可预知的时间需要注意时,中断是有意义的,而且不是太频繁。但是中断有很高的CPU开销。因此,如网络和磁盘控制器的高速设备,使用一些技巧减少中断需求。一个技巧是对整批传入或传出的请求发出单个中断。另一个技巧是驱动程序完全禁用中断,并定期检查设备是否需要注意。这种技术被称为轮询(polling)。如果设备执行操作非常快,轮询是有意义的,但是如果设备大部分空闲,轮询会浪费CPU时间。一些驱动程序根据当前设备负载在轮询和中断之间动态切换。

      -

      UART驱动程序首先将传入的数据复制到内核中的缓冲区,然后复制到用户空间。这在低数据速率下是可行的,但是这种双重复制会显著降低快速生成或消耗数据的设备的性能。一些操作系统能够直接在用户空间缓冲区和设备硬件之间移动数据,通常带有DMA。

      -
      -

      Lab: networking

      -

      In this lab you will write an xv6 device driver for a network interface card (NIC).

      -

      这个概述光是听起来就让人觉得热血沸腾。网络的本质其实就是IO设备,这一点我一直觉得很牛逼,而现在我居然要亲手实现网络……That’s very cool.

      -
      -
      -

      On this emulated LAN, xv6 (the “guest”) has an IP address of 10.0.2.15.

      -

      Qemu also arranges for the computer running qemu to appear on the LAN with IP address 10.0.2.2.

      -

      When xv6 uses the E1000 to send a packet to 10.0.2.2, qemu delivers the packet to the appropriate application on the (real) computer on which you’re running qemu (the “host”).

      -
      -
      -

      We’ve added some files to the xv6 repository for this lab.

      -

      The file kernel/e1000.c contains initialization code for the E1000 as well as empty functions for transmitting and receiving packets, which you’ll fill in.

      -

      kernel/e1000_dev.h contains definitions for registers and flag bits defined by the E1000 and described in the Intel E1000 Software Developer’s Manual.

      -

      kernel/net.c and kernel/net.h contain a simple network stack that implements the IP, UDP, and ARP protocols.

      -

      These files also contain code for a flexible data structure to hold packets, called an mbuf.

      -

      Finally, kernel/pci.c contains code that searches for an E1000 card on the PCI bus when xv6 boots.

      -
      +

      实现主要逻辑的是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;
      }
      + +

      通过虚拟地址获取表项主要是通过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)];
      }
      + +
      装上页表

      使用的是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();
      }
      + +

      其中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

      在内核运行的时候,需要申请很多空间用来存放各种数据。

      -

      Your job:

      -

      Your job is to complete e1000_transmit() and e1000_recv(), both in kernel/e1000.c, so that the driver can transmit and receive packets. You are done when make grade says your solution passes all the tests.

      +

      The kernel must allocate and free physical memory at run-time for page tables, user memory, kernel stacks, and pipe buffers.

      -

      感想

      说实话,一开始看题的时候真是感觉非常地哈人……但其实文档看着看着,心中也逐渐有了个大概,最后再结合下指导书的提示【当然不是后面那些保姆级的Hints】,最后写的也就八九不离十了。总体上来说,我觉得这次实验的代码还是很简单的,它主要难在探究过程,也就是从一开始什么也不懂,然后去阅读硬件设备的文档,结合代码尝试去理解,最后一步步写出来的过程。本次实验耗时六小时,我觉得肯定有不少于一半,甚至可能达到2/3的时间都耗费在理解上。这种从零开始探究的过程给了我很大的收获,同时也稍微提高了我面对挫折的能力。

      -

      这个实验确实设计得很有教育意义。除了我上面说的它锻炼了我的能力以外,它其实还具有比较深刻的工业意义。在看书的时候,书中这么写道:

      +

      用的是这段空闲内存:

      +

      image-20230109225700837

      -

      In addition, the driver must understand the device’s hardware interface, which can be complex and poorly documented.

      +

      It keeps track of which pages are free by threading a linked list through the pages themselves.

      -

      本次实验正是上述描述的简化版:E1000的文档很详细,并且我们只用掌握一部分它的功能就行了。但虽然简化了,其探究过程的内在逻辑还是不会改变的。

      -

      总之,我很喜欢这次实验的设计。我的评价是牛逼。

      -

      思路

      正确思路

      Hints写得很详细,不做赘述了。主要就是明确一下数据结构的问题:

      -
        -
      1. rx_ring和tx_ring是两个分开的队列

        -

        它们只是结构一模一样,都是阴影部分表示software持有,白色部分表示硬件持有。

        -

        因而,对于rx来说,白色部分表示需要传给协议栈的包,因而我们需要把白色部分转化为阴影部分;对于tx来说,白色部分表示网卡将要发送的包,因而我们需要把阴影部分转化为白色部分。

        -

        image-20230220234406239

        -
      2. -
      3. rx_mbufs和tx_mbufs

        -

        一开始不知道这俩是啥,后来才意识到,这俩和第1点的那俩其实是下标一一对应的关系。也就是说rx_ring[i]这个descriptor接收到的数据存在rx_mbufs[i],tx_ring[i]要发送的数据存在tx_mbufs[i]。知道了这个之后,代码就简单了。

        +

        kalloc.c中就是这么实现的。

        +

        Code: Physical memory allocator

        内核运行时申请释放空闲物理空间是通过kernel/kalloc.c完成的。它为内核栈、用户进程、页表和管道buffer服务。

        -

        忏悔:我一开始真没反应过来。计网我记得是有一模一样的结构的,看来算是白做了2333

        +

        kalloc.c用来在运行时申请分配新的一页,上面的vm.c正是用了kalloc申请一页,要么作为页表,要么作为存储数据的第三级页表指向的物理内存。

        -
      4. -
      -

      个人的推理过程

      一开始就先懵懵懂懂地看指导书,直到看到这句话:

      +

      最后应该会在空闲内存内形成这样的结构:

      +

      内存分成一页一页的,每页内存中的前几个字节存储着其对应队列中下一块内存的物理地址。不一定是从小地址到大地址顺序连接。

      -

      Browse the E1000 Software Developer’s Manual.

      +

      It store each free page’s run structure in the free page itself, since there’s nothing else stored there.

      -

      然后我这时连自己要干什么都迷迷糊糊,但姑且还是按他下面说的,准备先浏览第二章了。然而,我发现要我看我也还是看不懂啊,所以我就直接放弃了。【经验1:看不懂就算了,别死磕了

      -

      我放弃了第二章后,就再次从头开始细细看了一遍这句话之前的指导书,也结合了一下它给的代码。这次总算是差不多弄懂这次要做什么了:

      -

      实现driver的两个函数,从而实现对网卡进行数据的取出和送入。数据是eth frame。数据取出后要通过net_rx传递给上层协议栈。数据是mbuf类型的。

      -

      所以我们只需实现协议栈最底下的部分,也即从网卡读写数据,其他一些别的东西比如协议栈什么的都已经写好了。

      -

      但是那些什么rx_ring,还有各种奇奇怪怪的寄存器,我都看不懂,所以我就去看第三章了。初次略过一遍感觉还是一脸懵逼不知道干什么,但我带着“我们要做的是driver”这样的想法,在第二遍细看的时候有意区分开什么是网卡硬件帮我们做的,什么是我们的driver软件需要做的(经验2:明确要做什么。我们需要做的是软件部分,它的文档一般会说Software should XXX,密切关注这部分就行),就差不多有了点实现的雏形:

      -
      for recv:
      // 通过net_rx,网络包可以发送到udp顶层.
      // 所以说,我们在这里的目的就是,通过与硬件网卡e1000进行交互,
      // 取出e1000所接收到的数据包,检查数据的完整性,然后再把数据封装进mbuf结构体中,再通过net_rx传到上层

      // 取出数据包
      // 数据包存储在网卡的缓冲区中
      // 一是获取网卡缓冲区长度的长度
      // 网卡缓冲区长度存储在RCTL.BSIZE & RCTL.BSEX中
      /*
      *RCTL.BSEX = 0b:
      00b = 2048 Bytes.
      01b = 1024 Bytes.
      10b = 512 Bytes.
      1b1 = 256 Bytes.
      RCTL.BSEX = 1b:
      00b = Reserved; software should not program this value.
      01b = 16384 Bytes.
      10b = 8192 Bytes.
      11b = 4096 Bytes
      *
      * */
      // 二是获取数据包存放在哪个地址
      // 数据包的buffer cache的地址存储在descriptor的字段中
      // 必须读取多个descriptor以确定跨越多个接收缓冲区的数据包的完整长度。
      // 那么我们要读取的这些descriptor存放在哪呢?
      // 看文档,似乎差不多意思是这些descriptor被以环形队列的形式组织在一起,也许正是
      // 本文件内的rx_ring这个数组。
      // 当有descriptor到达e1000,e1000就会把它从host memory中取出来,存入到descriptor ring
      // 也即我们rx_ring数组
      //
      // 所以我们要做的,就是遍历rx_ring数组,如果rx_ring数组中的元素是used的,那么表明它就是数据包的一部分
      // 也即它地址所指向的buf里存放的是数据包的一部分数据
      //
      // 那么我们怎么知道这个rx_ring的元素有没有used,以及它是第几个呢?
      // 检查descriptor有没有used:status字段不为全0则为used
      // 并且硬件要求,我们在发现这个descriptor的status不为0,并且用完这个descriptor之后,需要将
      // 其status字段置零,以供硬件使用
      // Status information indicates whether the descriptor has been used and whether the referenced
      // buffer is the last one for the packet.

      // 三是获取数据包的数据
      // 我们需要获取decriptor的该字段,然后再从这个地址读取数据包数据
      // 网卡和内存统一编址,这个数据实际上就是网卡的buffer
      // 我们应该直接通过read这个系统调用就可以对其进行读写了

      // check数据包
      // 检查RDESC.ERRORS位,如果包发生了错误,再检查,如果发现RCTL.SBP、RCTL.UPE/MPE都被标记,
      // 就接收这个包,否则直接丢弃
      +
      // 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去取,然后登记在页表上。

      -

      When the E1000 receives each packet from the ethernet, it first DMAs the packet to the mbuf pointed to by the next RX (receive) ring descriptor, and then generates an interrupt. 【这句话可得知,descriptor们存放在代码中的rx_ring中。】

      -

      Your e1000_recv() code must scan the RX ring and deliver each new packet’s mbuf to the network stack (in net.c) by calling net_rx(). You will then need to allocate a new mbuf and place it into the descriptor, so that when the E1000 reaches that point in the RX ring again it finds a fresh buffer into which to DMA a new packet.

      -
      -

      就差不多是正确思路了。transmit的实现也是同理

      -

      代码

      -

      以下代码不知道为什么过不了test,我跟别人的逻辑一模一样也还是不行emmm

      -

      它的问题是,不会接收到外界的返ping,导致进程一直等待网卡IO,所以kerneltrap一直触发不了,无法正常网卡读写,从而导致fileread会一直处于sleep等待状态,整个系统就沉睡了【】我感觉应该是transmit没发成功。

      -

      等以后有精力再来看看吧。

      +

      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.

      -
      int
      e1000_transmit(struct mbuf *m)
      {
      acquire(&e1000_lock);
      struct tx_desc tx = tx_ring[regs[E1000_TDT]];
      if((tx.status & 1) == 0){
      release(&e1000_lock);
      return -1;
      }
      if(tx_mbufs[regs[E1000_TDT]] != 0) mbuffree(tx_mbufs[regs[E1000_TDT]]);
      tx.addr = (uint64) m->head;
      tx.length = m->len;
      tx.status |= 1;// EOP
      tx.cmd |= 1;//EOP
      tx.cmd |= 8;//RS
      tx_mbufs[regs[E1000_TDT]] = m;
      regs[E1000_TDT] = (regs[E1000_TDT]+1)%TX_RING_SIZE;
      // printf("send successful!\n");
      release(&e1000_lock);
      return 0;
      }

      static void
      e1000_recv(void)
      {
      printf("go into e1000_recv\n");
      acquire(&e1000_lock);
      while(1){
      //while(regs[E1000_RDT]!=regs[E1000_RDH]){
      printf("go into while\n");
      regs[E1000_RDT] = (regs[E1000_RDT] + 1)%RX_RING_SIZE;
      int i=regs[E1000_RDT];
      if(rx_ring[i].status != 0){
      // 包含所需数据包
      // 检查是否发生了错误
      //if((rx_ring[i].status & 1) !=0 && (rx_ring[i].status & 2) != 0){
      // // error字段有效
      // if(rx_ring[i].errors != 0){
      // 发生错误,直接丢弃
      // goto end;
      // }
      if((rx_ring[i].status & 1) == 0){
      release(&e1000_lock);
      return ;
      }
      // 将地址对应数据包发送
      struct mbuf* m = rx_mbufs[i];
      m->len = rx_ring[i].length;
      net_rx(m);
      rx_ring[i].status = 0;
      struct mbuf* mbuf = mbufalloc(MBUF_DEFAULT_HEADROOM);
      rx_ring[i].addr = (uint64) mbuf->head;
      rx_mbufs[i] = mbuf;
      }
      }

      release(&e1000_lock);
      }
      -]]> - - - Locking - /2023/01/10/xv6$chap6/ - Locking

      很多概念在看Java并发的时候都学习过,这些重复的地方就不做赘述了。

      -

      Code: spinlock

      -

      spinlock 使用介绍

      -

      一、spinlock 简介
      自旋锁(spinlock):是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,不断尝试获取锁,直到获取到锁才会退出循环

      -

      二、自旋锁与互斥锁的区别
      自旋锁与互斥锁类似,它们都是为了解决对某项资源的互斥使用,在任何时刻最多只能有一个线程获得锁
      对于互斥锁,如果资源已经被占用,调用者将进入睡眠状态
      对于自旋锁,如果资源已经被占用,调用者就一直循环在那里,看是否自旋锁的保持者已经释放了锁

      -

      三、自旋锁的优缺点
      自旋锁不会发生进程切换,不会使进程进入阻塞状态,减少了不必要的上下文切换,执行速度快。非自旋锁在获取不到锁的时候会进入阻塞状态,从而进入内核态,当获取到锁的时候需要从内核态恢复,需要线程上下文切换,影响性能
      如果某个线程持有锁的时间过长,就会导致其它等待获取锁的线程长时间循环等待消耗CPU,造成CPU使用率极高

      +

      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).

      -

      spinlock

      // Mutual exclusion lock.
      struct spinlock {
      uint locked; // Is the lock held?

      // For debugging:
      char *name; // Name of lock.
      struct cpu *cpu; // The cpu holding the lock.
      };
      - -

      acquire

      大概是这么个原理:

      -

      image-20230115231857670

      -

      当然这有竞态条件。xv6用的是CPU提供的amoswap原子指令来消除竞态条件的。

      -
      // in kernel/spinlock.c
      // Acquire the lock.
      // Loops (spins) until the lock is acquired.
      void
      acquire(struct spinlock *lk)
      {
      // 关中断
      // xv6允许禁止中断。但是由于xv6是一个多核系统,单个core被禁止中断并不会影响其他core。
      push_off(); // disable interrupts to avoid deadlock.

      // holding(): Check whether this cpu is holding the lock.
      if(holding(lk))
      panic("acquire");

      // On RISC-V, sync_lock_test_and_set turns into an atomic swap:
      // a5 = 1
      // s1 = &lk->locked
      // amoswap.w.aq a5, a5, (s1)
      // amoswap: 交换a5和(s1)的值,返回(s1)原来的值
      // 也即是如图所示的竞态条件的原子指令
      while(__sync_lock_test_and_set(&lk->locked, 1) != 0)
      ;

      __sync_synchronize();

      // Record info about lock acquisition for holding() and debugging.
      lk->cpu = mycpu();
      }
      - -

      __sync_synchronize();

      代码里的官方注释:

      -
      // Tell the C compiler and the processor to not move loads or stores
      // past this point, to ensure that the critical section's memory
      // references happen strictly after the lock is acquired.
      // On RISC-V, this emits a fence instruction.
      +
      // 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;
      }
      -

      这个注释其实没太看明白。我去翻了一下asm代码,发现这句话正如它最后一句所说的被翻译成fence指令:

      -

      image-20230115231457971

      -
      -

      处理器中的存储系统(一):RISC-V的FENCE、FENCE.I指令

      -

      顾名思义,FENCE指令犹如一道屏障,把前面的存储操作和后面的存储操作隔离开来,前面的决不能到后面再执行,后面的决不能先于FENCE前的指令执行。

      -
      -

      这个就好明白多了。

      -

      这样一来,acquire和release的两个fence就形成了两道屏障:

      -
      acquire();
      l->nexy = list;
      list = l;
      release();
      +
      // 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;
      }
      -

      中间那部分的指令可以重排,但是中间的指令就绝不会跑到临界区外。

      -

      push_off和pop_off

      -

      当CPU未持有自旋锁时,xv6重新启用中断;它必须做一些记录来处理嵌套的临界区域。acquire调用push_off (*kernel/spinlock.c*:89) 并且release调用pop_off (*kernel/spinlock.c*:100)来跟踪当前CPU上锁的嵌套级别。当计数达到零时,pop_off恢复最外层临界区域开始时存在的中断使能状态。intr_offintr_on函数执行RISC-V指令分别用来禁用和启用中断。

      +

      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是创建地址空间的用户部分的系统调用。它使用一个存储在文件系统中的文件初始化地址空间的用户部分。

      -

      release

      // in kernel/spinlock.c
      // Release the lock.
      void
      release(struct spinlock *lk)
      {
      if(!holding(lk))
      panic("release");

      lk->cpu = 0;

      __sync_synchronize();

      // Release the lock, equivalent to lk->locked = 0.
      // This code doesn't use a C assignment, since the C standard
      // implies that an assignment might be implemented with
      // multiple store instructions.
      // On RISC-V, sync_lock_release turns into an atomic swap:
      // s1 = &lk->locked
      // amoswap.w zero, zero, (s1)
      __sync_lock_release(&lk->locked);

      // 开中断
      pop_off();
      }
      +
      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;
      }
      -

      Code: Using locks

      -

      作为粗粒度锁的一个例子,xv6的kalloc.c有一个由单个锁保护的空闲列表。如果不同CPU上的多个进程试图同时分配页面,每个进程在获得锁之前将必须在acquire中自旋等待。自旋会降低性能,因为它只是无用的等待。如果对锁的争夺浪费了很大一部分CPU时间,也许可以通过改变内存分配的设计来提高性能,使其拥有多个空闲列表,每个列表都有自己的锁,以允许真正的并行分配。【很棒的思路】

      -

      作为细粒度锁的一个例子,xv6对每个文件都有一个单独的锁,这样操作不同文件的进程通常可以不需等待彼此的锁而继续进行。文件锁的粒度可以进一步细化,以允许进程同时写入同一个文件的不同区域。最终的锁粒度决策需要由性能测试和复杂性考量来驱动。

      -
      - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
      描述
      bcache.lock保护块缓冲区缓存项(block buffer cache entries)的分配
      cons.lock串行化对控制台硬件的访问,避免混合输出
      ftable.lock串行化文件表中文件结构体的分配
      icache.lock保护索引结点缓存项(inode cache entries)的分配
      vdisk_lock串行化对磁盘硬件和DMA描述符队列的访问
      kmem.lock串行化内存分配
      log.lock串行化事务日志操作
      管道的pi->lock串行化每个管道的操作
      pid_lock串行化next_pid的增量
      进程的p->lock串行化进程状态的改变
      tickslock串行化时钟计数操作
      索引结点的 ip->lock串行化索引结点及其内容的操作
      缓冲区的b->lock串行化每个块缓冲区的操作
      -

      Figure 6.3: Locks in xv6

      -

      Deadlock and lock ordering

      -

      如果在内核中执行的代码路径必须同时持有数个锁,那么所有代码路径以相同的顺序获取这些锁是很重要的。如果它们不这样做,就有死锁的风险。假设xv6中的两个代码路径需要锁A和B,但是代码路径1按照先A后B的顺序获取锁,另一个路径按照先B后A的顺序获取锁。为了避免这种死锁,所有代码路径必须以相同的顺序获取锁。全局锁获取顺序的需求意味着锁实际上是每个函数规范的一部分:调用者必须以一种使锁按照约定顺序被获取的方式调用函数。

      -

      由于sleep的工作方式(见第7章),Xv6有许多包含每个进程的锁(每个struct proc中的锁)在内的长度为2的锁顺序链。例如,consoleintr (*kernel/console.c*:138)是处理键入字符的中断例程。当换行符到达时,任何等待控制台输入的进程都应该被唤醒。为此,consoleintr在调用wakeup时持有cons.lockwakeup获取等待进程的锁以唤醒它。因此,全局避免死锁的锁顺序包括必须在任何进程锁之前获取cons.lock的规则。【这段不怎么能看懂,学完第七章再回来看看】

      -

      文件系统代码包含xv6最长的锁链。例如,创建一个文件需要同时持有目录上的锁、新文件inode上的锁、磁盘块缓冲区上的锁、磁盘驱动程序的vdisk_lock和调用进程的p->lock。为了避免死锁,文件系统代码总是按照前一句中提到的顺序获取锁。

      +

      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.

      -

      Locks and interrupt handlers

      -

      Xv6 is more conservative: when a CPU acquires any lock, xv6 always disables interrupts on that CPU. Interrupts may still occur on other CPUs, so an interrupt’s acquire can wait for a thread to release a spinlock; just not on the same CPU.看来是通过开关中断来保护临界区的

      -

      acquire调用push_off (*kernel/spinlock.c*:89) 并且release调用pop_off (*kernel/spinlock.c*:100)来跟踪当前CPU上锁的嵌套级别。当计数达到零时,pop_off恢复最外层临界区域开始时存在的中断使能状态。intr_offintr_on函数执行RISC-V指令分别用来禁用和启用中断。

      -

      严格的在设置lk->locked (kernel/spinlock.c:28)之前让acquire调用push_off是很重要的。如果两者颠倒,会存在一个既持有锁又启用了中断的短暂窗口期,不幸的话定时器中断会使系统死锁。同样,只有在释放锁之后,release才调用pop_off也是很重要的(*kernel/spinlock.c*:66)。

      +

      不过遗憾的是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

      -

      一个解决了一半的疑问

      问题

      -

      Xv6更保守:当CPU获取任何锁时,xv6总是禁用该CPU上的中断。中断仍然可能发生在其他CPU上,此时中断的acquire可以等待线程释放自旋锁;由于不在同一CPU上,不会造成死锁。

      -

      进展:似乎书中说到,“sleep atomically yields the CPU and releases the spinlock”。等了解完sleep,也即读完第七章之后再来看看。

      +

      感想

      乌龙

      这里好像是因为实验改版了,我下的是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希望所有虚拟地址已经被解绑并释放对应的物理空间了,该函数只负责释放页表。

      -

      在处理时钟中断的trap.c中:

      -
      // in kernel/trap.c devintr()
      } else if(scause == 0x8000000000000001L){

      // 这里!!
      // in kernel/trap.c devintr()
      if(cpuid() == 0){
      clockintr();
      }

      w_sip(r_sip() & ~2);
      return 2;
      }

      void
      clockintr()
      {
      acquire(&tickslock);
      ticks++;
      wakeup(&ticks);
      release(&tickslock);
      }
      +

      让我得知freewalk在vm.c下面【吐槽,我一开始还以为是自由自在地走(,看到这个才反应过来是free walk,跟页表有关的】。结合freewalk的代码

      +

      image-20230110225359361

      +

      可以知道,造成这个panic的原因是需要手动释放页表项。而在这里

      +
      // in proc.c  freeproc()
      if(p->usyscall)
      kfree((void*)p->usyscall);
      p->usyscall = 0;
      -

      可见只有CPU0才会进入clockintr【因为要求cpuid==0】,锁住ticks引起ticks递增。

      -

      而当sys_sleep获得锁之后,其结束循环的条件是ticks - ticks0 < n:

      -
      uint64
      sys_sleep(void)
      {
      int n;
      uint ticks0;
      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;
      }
      +

      仅仅是释放掉了对应的物理页,页表项并没有被释放

      +

      对比了一下别人写的,才发现原来这里也需要修改:

      +
      // 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);
      }
      -

      我认为,这会导致死锁情况。假设计算机为多CPU,且从零开始依次递增编号。对该死锁情况的讨论,可以分为以下两类:

      +

      这样一来,问题就解决了。

      +
      总结

      因而,可以看到,如果进程想使用页的话,需要经历以下四步:

        -
      1. sys_sleep在CPU2(或者其他编号非零的CPU)运行,且先获取了tickslock的锁。这时候,ticks将会停止增长,sys_sleep结束循环的条件将无法结束。

        -

        理由:对于CPU0,它可以进入clockintr的代码段,但是由于锁已经被获取,所以就只能一直在那边死锁等待;对于其他CPU来说,压根执行不了那段增加ticks的代码段,所以ticks压根不会增加。这样一来,CPU2进程等待ticks增加,从而获取结束循环的条件;CPU0等待CPU2进程结束,从而使得ticks增加,就造成了死锁。

        -
      2. -
      3. sys_sleep在CPU0运行,且先获取了tickslock的锁。这时候,ticks将会停止增长,sys_sleep结束循环的条件将无法结束。

        -

        理由:由于xv6会在获取锁和释放锁期间关闭中断,因而CPU0无法进行时钟中断而发生进程的切换,只能一直在sys_sleep中等待,所以ticks更不可能增加,造成了死锁。

        -
      4. +
      5. 通过kalloc获取物理页地址(可以通过该地址对页进行读写),并且记录在进程proc结构中(否则之后就获取不了了)
      6. +
      7. 建立mappages映射
      8. +
      9. 释放物理页
      10. +
      11. 释放PTE映射
      -

      暂时没有很充分的理由反驳这两点。。。

      -

      解答

      学习完下一章的内容后可知,sleep(&ticks, &tickslock);会释放掉tickslock的锁,这样CPU0就可以进入clockintr增加ticks了。

      -

      再详细梳理一次,这里的具体机制是这样的:

      -

      可以把ticks看做信号量,sys_sleep为消费者,clockintr为生产者。

      -
      // in sys_sleep()
      acquire(&tickslock);
      while(ticks < 某个数字){
      sleep(&ticks, &tickslock);
      }
      release(&tickslock);
      +

      可见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);
      }
      -
      void
      clockintr()
      {
      acquire(&tickslock);
      ticks++;
      wakeup(&ticks);
      release(&tickslock);
      }
      +

      问答题

      +

      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);
      }
      -

      可以看到,这是非常典型的生产者消费者模型。生产者每生产一次ticks,就会唤醒消费者,让消费者检查条件。如果条件错误,则继续sleep等待消费者下一次唤醒,如此循环往复。

      -

      只不过,还有一个小疑点,就是clockintr这段只有CPU0可以执行这一点是否为真依然存疑。如果确实只有CPU0可以执行的话,假若sys_sleep在CPU0上执行,那么还是依然会造成死锁。所以我猜想是不是CPU0是无法关中断的?也就是说CPU0是一个后盾一般的保护角色?或者是别的CPU也能进入本段代码?如果别的CPU也能进,那是怎么实现的?因为很明显这段代码确实只有CPU0可以进入。

      -

      Sleep locks

      关于sleep lock的由来和优点,书里描述得很详细,简单来说就是:

      -
      -

      Thus we’d like a type of lock that yields the CPU while waiting to acquire, and allows yields (and interrupts) while the lock is held.

      -

      因为等待会浪费CPU时间,所以自旋锁最适合短的临界区域;睡眠锁对于冗长的操作效果很好。

      +

      问答题

      +

      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?

      -
      void
      acquiresleep(struct sleeplock *lk)
      {
      acquire(&lk->lk);
      // 等待
      while (lk->locked) {
      // sleep atomically yields the CPU and releases the spinlock
      sleep(lk, &lk->lk);
      }
      // 占用
      lk->locked = 1;
      lk->pid = myproc()->pid;
      release(&lk->lk);
      }

      void
      releasesleep(struct sleeplock *lk)
      {
      acquire(&lk->lk);
      lk->locked = 0;
      lk->pid = 0;
      // 到时候可以留意一下wakeup是会唤醒一个还是多个
      wakeup(lk);
      release(&lk->lk);
      }
      +

      从上面操作系统的启动来看,进程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);
      }
      -

      有一点值得注意:

      -
      -

      Because sleep-locks leave interrupts enabled, they cannot be used in interrupt handlers. Because acquiresleep may yield the CPU, sleep-locks cannot be used inside spinlock critical sections (though spinlocks can be used inside sleep-lock critical sections).

      +

      可见,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;
      //...
      }
      + +

      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.

      -

      这实际上是因为自旋锁内不能sleep,因而也就不能使用sleep lock。

      -

      为什么不能sleep?我猜测应该是因为sleep中会释放自旋锁然后再调度别的进程。此时,临界区就不受保护了很危险,不符合spinlock在临界区结束才能释放的规范。

      -

      在查阅别人的说法的时候,我还看到了这个讨论:

      -

      中断中为什么不能sleep | Linux内核的评论区

      -

      在中断服务程序中,无法sleep的原因应该是sleep后,调度程序将CPU窃走,由于调度的基本单位是线程(中断服务程序不是线程),因此中断服务程序无法再被调度回来,即中断程序中sleep后的部分永远无法得到执行。

      -
      -

      Real world

      -

      大多数操作系统都支持POSIX线程(Pthread),它允许一个用户进程在不同的CPU上同时运行几个线程。Pthread支持用户级锁(user-level locks)、障碍(barriers)等。支持Pthread需要操作系统的支持。例如,如果一个Pthread在系统调用中阻塞,同一进程的另一个Pthread应当能够在该CPU上运行。另一个例子是,如果一个线程改变了其进程的地址空间(例如,映射或取消映射内存),内核必须安排运行同一进程下的线程的其他CPU更新其硬件页表,以反映地址空间的变化。

      -
      -

      Lab: locks

      -

      In this lab you’ll gain experience in re-designing code to increase parallelism. You’ll do this for the xv6 memory allocator and block cache.

      -
      -

      Memory allocator

      -

      The program user/kalloctest stresses xv6’s memory allocator: three processes grow and shrink their address spaces, resulting in many calls to kalloc and kfree. kalloc and kfree obtain kmem.lock. kalloctest prints (as “#fetch-and-add”) the number of loop iterations in acquire due to attempts to acquire a lock that another core already holds, for the kmem lock and a few other locks. The number of loop iterations in acquire is a rough measure of lock contention.

      -

      To remove lock contention, you will have to redesign the memory allocator to avoid a single lock and list. 也就是说要把kalloc中的整个列表上锁,修改为每个CPU有自己的列表The basic idea is to maintain a free list per CPU, each list with its own lock. Allocations and frees on different CPUs can run in parallel, because each CPU will operate on a different list. The main challenge will be to deal with the case in which one CPU’s free list is empty, but another CPU’s list has free memory; in that case, the one CPU must “steal” part of the other CPU’s free list. Stealing may introduce lock contention, but that will hopefully be infrequent.主要挑战将是处理一个 CPU 的空闲列表为空,但另一个 CPU 的列表有空闲内存的情况; 在这种情况下,一个 CPU 必须“窃取”另一个 CPU 的空闲列表的一部分。

      -

      Your job is to implement per-CPU freelists, and stealing when a CPU’s free list is empty.

      -

      You must give all of your locks names that start with “kmem”. That is, you should call initlock for each of your locks, and pass a name that starts with “kmem”.

      -

      Run kalloctest to see if your implementation has reduced lock contention. To check that it can still allocate all of memory, run usertests sbrkmuch. Your output will look similar to that shown below, with much-reduced contention in total on kmem locks, although the specific numbers will differ.

      +

      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.

      -

      感想

      总之,意思就是kalloc里面本来是多核CPU共用一个空闲页list,现在要做的就是给每一核的CPU独立分配一个空闲页list。我觉得可以分为如下几步来做:

      -
        -
      1. 定义list数组以及对应的锁

        -

        cpu的数量是一定的;cpuid可以用来作为数组下标索引

        -
      2. -
      3. 在init时初始化锁,在freelist的时候把空闲页均分给CPU

        -
      4. -
      5. 当kalloc和kfree的时候,获取当前cpuid上锁

        -
      6. -
      7. 当一个CPU的内存不够的时候,去向另一个CPU窃取。窃取之前,首先应该获取另一个CPU的锁。

        -
      8. -
      -

      以上是初见思路。正确思路确实跟上面的一样,编码过程也比较简单,没有很恶心的细节和奇奇怪怪的bug,没什么好说的。

      -

      第二步中,hints是推荐把所有空闲页都分给CPU0。

      -

      第四步的时候我是一次窃取一页。我看到一个一次窃取多页的做法,我觉得很有想法,在这里附上链接:

      +

      感想

      实验内容:

      +

      实现void pgaccess(uint64 sva,int pgnum,int* bitmask);,一个系统调用。在这里面,我们要做的是,访问从svasva+pgnum*PGSIZE这一范围内的虚拟地址对应的PTE,然后查看PTE的标记项是否有PTE_A。有的话则在bitmask对应位标记为1.

      +

      应该注意的点:

      +

      1.需要进行内核态到用户态的参数传递 2.需要进行系统调用的必要步骤 3.PTE_A需要自己定义

      +

      以上是初见。做完了发现,确实就是那么简单,我主要时间花费在下的实验版本不对,折腾来折腾去了可能有一个小时,最后还是选择了直接把测试函数搬过来手工调用。已经换到正确的年份版本了【泪目】

      +

      有一点我忽视了,看了提示才知道:

      -

      MIT6.S081 lab8 locks

      +

      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).

      -

      代码

      定义
      struct {
      struct spinlock kmem_locks[NCPU];
      struct run *freelists[NCPU];
      } kmem;
      +

      也就是说每次检查到一个,就需要手动清除掉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);
      }
      -
      初始化

      由于kinit仅会由一个cpu执行一次【详情见main.c】,故而我这里在kinit的做法是由一个CPU初始化所有CPU,而没有选择去修改main.c从而使每个CPU都执行一次kinit。

      -
      void
      kinit()
      {
      for(int i=0;i<NCPU;i++){
      char buf[8];
      snprintf(buf,6,"kmem%d",i);
      initlock(&kmem.kmem_locks[i], buf);
      }
      freerange(end, (void*)PHYSTOP);
      }

      // 多带一个参数表示cpuid,仅在kinit的freerange中使用
      void
      kfree_init(void *pa,int i)
      {
      struct run *r;

      if(((uint64)pa % PGSIZE) != 0 || (char*)pa < end || (uint64)pa >= PHYSTOP)
      panic("kfree");

      // Fill with junk to catch dangling refs.
      memset(pa, 1, PGSIZE);

      r = (struct run*)pa;

      r->next = kmem.freelists[i];
      kmem.freelists[i] = r;
      }
      void
      freerange(void *pa_start, void *pa_end)
      {
      char *p;
      p = (char*)PGROUNDUP((uint64)pa_start);

      // 把空闲内存页均分给每个CPU
      uint64 sz = ((uint64)pa_end - (uint64)pa_start)/NCPU;
      uint64 tmp = PGROUNDDOWN(sz) + (uint64)p;
      for(int i=0;i<NCPU;i++){
      for(; p + PGSIZE <= (char*)tmp; p += PGSIZE)
      kfree_init(p,i);
      tmp += PGROUNDDOWN(sz);
      if(i == NCPU-2){
      tmp = (uint64)pa_end;
      }
      }
      }
      +
      // 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;
      }
      -
      kfree
      void
      kfree(void *pa)
      {
      // ...

      r = (struct run*)pa;
      // 在这
      push_off();
      int id = cpuid();

      acquire(&kmem.kmem_locks[id]);
      r->next = kmem.freelists[id];
      kmem.freelists[id] = r;
      release(&kmem.kmem_locks[id]);
      pop_off();
      }
      +

      A kernel page table per process

      +

      The goal of this section and the next is to allow the kernel to directly dereference user pointers.

      +
      +
      +

      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.

      +
      +

      感想

      这个其实平心而论不难,思路很简单。写着不难是不难,但想明白花费了我很多时间。

      +

      它这个要求我们修改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();
      -
      kalloc
      void *
      kalloc(void)
      {
      struct run *r;

      push_off();
      int id = cpuid();

      acquire(&kmem.kmem_locks[id]);
      r = kmem.freelists[id];
      if(r){
      kmem.freelists[id] = r->next;
      }
      release(&kmem.kmem_locks[id]);
      pop_off();

      // 如果无空闲页,则窃取
      if(!r){
      for(int i=NCPU-1;i>=0;i--){
      acquire(&kmem.kmem_locks[i]);
      r = kmem.freelists[i];
      if(r){
      kmem.freelists[i] = r->next;
      release(&kmem.kmem_locks[i]);
      break;
      }
      release(&kmem.kmem_locks[i]);
      }
      }

      if(r)
      memset((char*)r, 5, PGSIZE); // fill with junk
      return (void*)r;
      }
      +
      // 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;
      }
      -

      Buffer cache

      buffer cache的结构其实跟kalloc的内存分配结构有一定的类似之处,都是采用链表管理,但是buffer cache的实现相较于kalloc更为复杂。

      +

      这样会死得很惨,爆出如下panic:

      +

      image-20230114011100370

      +

      通过hints的调试贴士

      -

      Reducing contention in the block cache is more tricky than for kalloc, because bcache buffers are truly shared among processes (and thus CPUs).

      -

      For kalloc, one could eliminate most contention by giving each CPU its own allocator; that won’t work for the block cache.

      -

      We suggest you look up block numbers in the cache with a hash table that has a lock per hash bucket.

      +

      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.

      -

      感想

      初见思路

      我想我们可以这么实现:

      -

      首先有一个双向链表,接收着所有空闲无设备分配的buf。然后再有多个双向链表桶,以设备号为索引值。

      -

      设备号数量,也即hash table的大小定义在kernel/param.h中:

      -
      #define NDEV         10 
      +

      我发现程序在这里绷掉了:

      +
      p->kpgtbl = (pagetable_t) kalloc();
      -

      bget中,第一个循环仅需在设备链表中查找即可,第二个循环需要先看设备链表是否有空闲的对象,如果没有,则去接收所有空闲无设备分配的那个双向链表中窃取一个对象。

      -

      brelse中,则把要释放的buf对象添加在head中即可。

      -

      因而,我们要做以下几件事:

      -
        -
      1. 修改bcache的定义

        -

        添加数量为设备号的head数组,以及对应的锁

        -
      2. -
      3. 初始化bcache

        -
      4. -
      5. 添加工具函数:将一个buf加入一个双向链表;从一个双向链表中得到一个buf

        -
      6. -
      7. bgetbrelse

        -
      8. -
      -

      看起来确实好像可以实现的样子,但是这个问题在于,这么做就直接破坏了LRU的这个规则。所以还是不能这么写的。但总之先把我的代码放上来。

      -

      以下代码是不能正常运行的。比如说在执行ls命令时,会发生如下错误:

      -

      image-20230122163938601

      -

      会打印出一些乱七八糟的东西,并且这些东西似乎是固定的,每次都会发生,看来应该不是多进程的问题,而是代码有哪里出现逻辑错误了。不过注意到会产生“stopforking”、“bigarg-ok”,这两个似乎是在usertest中的两个文件名,很奇怪。

      -

      很遗憾我暂时没有精力debug了。姑且先把错误代码放在这里吧。

      -
      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;
      struct buf dev_heads[NDEV];
      struct spinlock dev_locks[NDEV];
      } bcache;
      +

      而且显而易见,是系统启动时崩的。

      +

      经过了漫长的思考,我震惊地发现了它为什么崩了()

      +

      首先,这段代码语法上是没有问题的。它固然犯了发布未初始化完成的对象这样的并发错误【我有罪】,也破坏了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;
      }
      -
      void
      binit(void)
      {
      struct buf *b;

      initlock(&bcache.lock, "bcache");
      for(int i=0;i<NDEV;i++){
      char buf[10];
      snprintf(buf,9,"bcache%02d",i);
      initlock(&(bcache.dev_locks[i]), buf);
      bcache.dev_heads[i].prev = &(bcache.dev_heads[i]);
      bcache.dev_heads[i].next = &(bcache.dev_heads[i]);
      }

      // 初始时,每一个桶内都有一个buf结点
      b = bcache.buf;
      for(int i=0;i<NDEV;i++){
      b->next = bcache.dev_heads[i].next;
      b->prev = &bcache.dev_heads[i];
      initsleeplock(&b->lock, "buffer");
      bcache.dev_heads[i].next->prev = b;
      bcache.dev_heads[i].next = b;
      b++;
      }

      // Create linked list of buffers
      bcache.head.prev = &bcache.head;
      bcache.head.next = &bcache.head;
      for(; 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;
      }
      }
      +

      我一路顺着os启动的路径找,也想不出来这能有什么错,因而非常迷茫。

      +

      此时我灵光一闪,会不会是myproc()在os刚启动的时候是发挥不了作用的?于是我一路顺着myproc的代码看下去:

      +
      struct proc*
      myproc(void) {
      push_off();
      struct cpu *c = mycpu();
      struct proc *p = c->proc;
      pop_off();
      return p;
      }
      -
      void
      brelse(struct buf *b)
      {
      if(!holdingsleep(&b->lock))
      panic("brelse");

      uint dev = b->dev;
      releasesleep(&b->lock);

      acquire(&(bcache.dev_locks[dev]));
      b->refcnt--;
      if (b->refcnt == 0) {
      b->next->prev = b->prev;
      b->prev->next = b->next;
      release(&(bcache.dev_locks[dev]));

      acquire(&bcache.lock);
      b->next = bcache.head.next;
      b->prev = &bcache.head;
      bcache.head.next->prev = b;
      bcache.head.next = b;
      release(&bcache.lock);
      }else
      release(&(bcache.dev_locks[dev]));
      }
      +

      那么,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();
      }
      -
      static struct buf*
      bget(uint dev, uint blockno)
      {
      acquire(&(bcache.dev_locks[dev]));

      // Is the block already cached?
      for(struct buf* b = bcache.dev_heads[dev].next; b != &(bcache.dev_heads[dev]); b = b->next){
      if(b->blockno == blockno){
      b->refcnt++;
      release(&(bcache.dev_locks[dev]));
      acquiresleep(&b->lock);
      return b;
      }
      }
      release(&(bcache.dev_locks[dev]));

      // 在head中找
      acquire(&bcache.lock);
      // Recycle the least recently used (LRU) unused buffer.
      for(struct buf* b = bcache.head.prev; b != &(bcache.head); b = b->prev){
      if(b->refcnt == 0) {
      b->dev = dev;
      b->blockno = blockno;
      b->valid = 0;
      b->refcnt = 1;
      release(&bcache.lock);
      acquiresleep(&b->lock);
      return b;
      }
      }
      panic("bget: no buffers");
      }
      +

      创建完进程后,就进入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;
      // ...
      -
      正确思路

      首先,大家似乎都是用blockno来hash的,这点就跟我的原始思路不一样了(。其实也很对,因为每个设备的使用频率是不平均的,用blockno来hash比用dev来hash其实会让访问次数更加平均。

      -

      然后就是怎么保证LRU依然OK。hints的做法是使用时间戳。我们可以在brelse的时候记录时间戳字段,在bget缺块的时候遍历hash table,找出对应timestamp最小的block即可。

      -

      历经了几小时的debug,代码最终正确。正确版本在下面的代码模块处。

      -
      debug过程

      coding过程其实很短暂,毕竟思路很直观。我一开始是按初见思路写的代码,然后再从初见思路改到正确思路,这个过程,给我埋下了极大的安全隐患【悲】

      -

      其实几个小时下来,很多细节都已经忘记了,接下来就说点印象比较深的吧。

      -

      首先,我使用了正确思路以来,依然出现了跟初见思路一样的错误,也即xv6正常boot,但是执行ls命令会有错误。但是,当我make clean之后再次make qemu,错误改变了,变成了xv6 boot失败,并且爆出错误panic:ilock:no type

      -
      -

      注:关于此处的make clean,有两点需要解释。一是为什么会做出make clean的行为,二是这个变化的原理是什么。

      -

      此处突然做出make clean的行为,是因为参照了该文章:

      -

      MIT6.S081 lab8 locks

      -

      image-20230123172138766

      -

      没想到我make clean之后反而就变成了他这样的问题23333也是感觉蛮惊讶的

      -

      这个的原理说实话我不大清楚。猜想可能是make qemu的某段访问磁盘初始化之类的代码只会执行一次,只有make clean之后才会让其执行第二次。所以我们手动完全boot了一遍操作系统,才会导致这个错误爆出来,否则,操作系统就会使用原本的正确boot版本启动,之后再执行命令就当然是错误的了。

      -

      我想知乎文章里也应该是这个原因。操作系统本来使用的是错误版本,make clean后才会重新使用正确版本。

      -

      我之后写对了又尝试了一下,觉得我的猜想应该是对的。我的执行路线:

      -
        -
      1. make qemu,得到正确结果
      2. -
      3. bio.c改回错误版本
      4. -
      5. 再次make qemu,发现xv6正常boot,但是执行ls命令会出以上同样的错误
      6. -
      7. make clean,然后make qemu,爆出panic:ilock: no type
      8. -
      -

      挺完美地符合了我的猜想。

      -

      【来自之后的学习:

      -

      in lab file system:

      -

      mkfs 程序创建 xv6 文件系统磁盘映像并确定文件系统总共有多少个块; 这个大小由 kernel/param.h 中的 FSSIZE 控制。 您会看到本实验存储库中的 FSSIZE 设置为 200,000 个块。 您应该在 make 输出中看到 mkfs/mkfs 的以下输出:
      nmeta 70 (boot, super, log blocks 30 inode blocks 13, bitmap blocks 25) blocks 199930 total 200000
      这一行描述了 mkfs/mkfs 构建的文件系统:它有 70 个元数据块(用于描述文件系统的块)和 199,930 个数据块,共计 200,000 个块。
      如果在实验期间的任何时候您发现自己必须从头开始重建文件系统,您可以运行 make clean 来强制 make 重建 fs.img

      -

      可以看到,我们上面就是做了强制重构fs.img。】

      -
      -

      我想来想去不知道这个错到底怎么爆的,看了下ilock()对应报错点:

      -
      // in fs.c ilock()
      if(ip->valid == 0){
      //printf("ilock begin.\n");
      bp = bread(ip->dev, IBLOCK(ip->inum, sb));
      dip = (struct dinode*)bp->data + ip->inum%IPB;
      ip->type = dip->type;
      // ...
      ip->size = dip->size;
      memmove(ip->addrs, dip->addrs, sizeof(ip->addrs));
      brelse(bp);
      ip->valid = 1;
      if(ip->type == 0){
      //print_buf();
      printf("bp->blockno = %d, bp->refcnt = %d\n",bp->blockno,bp->refcnt);
      panic("ilock: no type");
      }
      }
      +

      因而,c->proc是在创建进程的第一次调度后初始化的,也即,myproc只有在执行第一次scheduler之后才可以调用。而!!!

      +

      当执行调度前的userinit时:

      +
      void
      userinit(void)
      {
      struct proc *p;

      p = allocproc();
      initproc = p;
      -

      可知大概就是,ip的type为0这个非法数值就报错了,而ip的type来源于dip,dip又指向了bp的data,bp也就是我们在bio.c一直在打交道的buf结构体。所以说,其实问题是出在了buf上,我们的bread返回的是一个错误的buf。

      -

      那么,究竟是buf的哪里出错了呢?这个问题想了我很久很久很久,依然没想出来。我一直认为是我的hashtable+双向链表这个数据结构哪里写错了,反反复复看了三四遍,其他地方的逻辑也反反复复研究了好几遍,依然没有结论。当然此过程也抓出了很多bug,但抓完bug后报错仍在,非常坚挺。

      -

      快要放弃的时候,我发现了错误。这很大一部分归功于我用于调试的这个函数:

      -
      // 打印出hashtable的所有结点
      void
      print_buf(){
      printf("**********************\n");
      printf("cnt = %d,dec = %d\n",cnt,dec);
      for(int i=0;i<NBUCKET;i++){
      int should = !holding(&(bcache.dev_locks[i]));
      if(should)
      acquire(&(bcache.dev_locks[i]));
      printf("--------------\n");
      int tmp_cnt = 0;
      struct buf* b;
      for(b = bcache.dev_heads[i].next; b != &(bcache.dev_heads[i]); b = b->next){
      //for(b = bcache.dev_heads[i].prev; b != &(bcache.dev_heads[i]); b = b->prev){
      tmp_cnt++;
      printf("b.refcnt = %d,b.dev = %d,b.blockno = %d\n",b->refcnt,b->dev,b->blockno);
      }
      printf("%d:total:%d\n",i,tmp_cnt);
      printf("--------------\n");
      if(should)
      release(&(bcache.dev_locks[i]));
      }
      }
      +

      它进行了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();
      }
      -

      我在ilock()的panic前面调用了这个函数,并且打印了出问题的buf的blockno:

      -
      if(ip->type == 0){
      print_buf();
      printf("bp->blockno = %d, bp->refcnt = %d\n",bp->blockno,bp->refcnt);
      panic("ilock: no type");
      }
      +

      当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-20230123174332113

      -

      可以看到,出问题的这里blockno=33,而在桶7中,首先有两个blockno==33的结点,这已经违反了不变性条件;其次有一个refcnt==1的结点,那个是所需结点,但我们却没有找到那个结点,反而去新申请了一个结点。这显然非常地古怪。

      -

      于是随后,我就在bio.cbget()中添加了这么几句话:

      -

      image-20230123174620818

      -

      最终结果是会打印出两个blockno==0的结点,但是blockno==33的结点没有访问到。

      -

      这就很奇怪了。print_buf中以及bget的这个地方,都是遍历hashtable的某个双向链表,但是,为什么print_buf可以访问到,但是bget不行呢?

      -

      我首先对比出来的,是print_buf是逆序遍历,而bget是顺序遍历,所以我就又猜想是因为我的数据结构写错了,然而又看了一遍发现并不是。

      -

      这时候,可能我的视力恢复了吧,我猛然发现::

      -

      image-20230123174921100

      -

      我超,这里是不是应该用hash。。。。。改完这处之后,果然就非常顺利地pass了所有测试【悲】

      -

      可以看到伏笔回收了。我是在旧思路代码基础上改过来的。旧思路代码是用dev作为index的,这个for循环忘记改了。因而,就这样,就这么寄了,看了我三四个小时【悲】

      -

      不过这倒是可以解释得通所有的错误了。之所以ilock中buf出错,没有正确找到已经映射在cache中的buf而是自己新建了一个,是因为,我压根就没有在正确的桶里找,而是在别的桶中找,这样自然就找不到了,就会自己新建一个,然后就寄了。

      -

      这个故事告诉我们,还是得谨慎写代码()以及,我在旧代码基础上改的时候,其实可以用更聪明的替换方法:修改dev的变量名为hash->把参数里的dev变量名改为dev。这样就不会出错了。很遗憾,我并没有想到,只是很急很急地手动一个个改了,之后也没有检查,才发生如此错误。忏悔。

      -

      本次bug虽然很sb,但确实让我在debug过程中收获了些许,至少毅力变强了()途中无数次想要放弃,还好我坚持了下来,才能看到如此感动的OK一片:

      -

      image-20230123170805666

      -

      代码

      -

      之后写学校实验时回过头来看,发现之前的实现是不对的,在同时进入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");
      }
      +

      但是这样还没有结束。因为我们除了得更换目前的页表,还得更换trapframe中的内核页表相关的东西:

      +
      struct trapframe {
      /* 0 */ uint64 kernel_satp; // kernel page table
      /* 8 */ uint64 kernel_sp; // top of process's kernel stack
      }
      -
      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);
      }
      +

      为啥还要更换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
      -]]> - - - 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切换到目标线程。如此周而复始。

      -
      -

      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;
      };
      +

      所以,为了以后系统调用能顺利自发进行,我们需要把栈帧也一起换掉。怎么换呢?我们是否还要在一些地方人工把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
      + +

      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);
      }
      + +

      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;
      -

      上下文切换需要修改栈和pc,context中确实有sp寄存器,但是没有pc寄存器,这主要还是因为当swtch返回时,会回到ra所指向的地方,所以仅保存ra就足够了。

      -

      swtch

      上下文的切换是通过swtch实现的。

      -
      void            swtch(struct context*, struct context*);
      +
      // 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;
      }
      -

      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
      +
      // 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");
      }
      -

      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;
      }
      +
      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");
      }
      -

      cpu中存储着的是scheduler线程的context。因而,这样就可以保存当前进程的context,读取scheduler线程的context,然后转换到scheduler的context执行了。

      -
      -

      可以发现这里是有个很完美的组合技的。由sched()保存context到process结构体中,再由scheduler()读取process对应的context回归到sched()继续执行,我感觉调度设计这点真是帅的一匹。

      -
      -

      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");
      }
      }
      }
      +
      修改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;
      }
      -

      通过swtch进入scheduler线程后,会继续执行scheduler中swtch的下一个指令,完成下一次调度。

      -

      一些补充

      以上是书本的介绍内容。看到这想必会有很多疑惑,至少有以下两点:

      -
        -
      1. 为什么cpu->context会存储着scheduler的上下文?这是什么时候,又是怎么初始化的?
      2. -
      3. 为什么从sched中swtch会来到scheduler中swtch的下一句?
      4. -
      -

      先从第一点入手。实际上,这个初始化的工作,是在操作系统启动时的main.c中完成的。

      -
      void
      main()
      {
      if(cpuid() == 0){
      // ...
      } else {
      // ...
      }

      scheduler();
      }
      +
      释放
      // in kernel.proc.c freeproc()
      if(p->kpgtbl)
      proc_freekpgtbl(p->kpgtbl,p->kstack);
      p->kpgtbl = 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);
      }
      // ...
      }
      }
      +
      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);
      }
      -

      每个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.

      +

      Simplify copyin/copyinstr

      +

      参考:

      +

      6.S081学习记录-lab3

      -

      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开始。

      +

      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.

      -

      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的不同功能是否可以拆分。

      +
      +

      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.

      -

      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 kernel/proc.c scheduler()
      acquire(&p->lock);// 该锁会在yield等地被释放
      // ...
      swtch(&c->context, &p->context);
      // ...
      release(&p->lock);// 该锁会释放yield等地中获得的锁
      - -

      不得不说,这结构实在是太精妙了。这中间的如此多的复杂过程,就这样成功地被锁保护了起来。

      -

      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;
      }
      - -

      在内核态中,编译器被设置为保证不会以其他方式使用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)
      +

      感想

      这题很直观的思路是,在每个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);
      }
      -
      struct trapframe {
      /* 32 */ uint64 kernel_hartid; // saved kernel tp
      /* 64 */ uint64 tp;
      // ...
      }
      +

      换成了这样:

      +
      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);
      }
      -

      必须在trampoline保存用户态中使用的tp值,以及内核态中对应的hartid。

      -

      最后再在返回用户态的时候恢复用户态的tp值以及更新trampoline的tp值。

      -
      // in kernel/trap.c usertrapret()
      p->trapframe->kernel_hartid = r_tp(); // hartid for cpuid()
      +

      最后,在启动的时候,卡在了初次调度切换不到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

      +
      +

      Xv6 applications ask the kernel for heap memory using the sbrk() system call.

      +
      +

      很悲伤,我的初见思路是错误的()

      +

      而这三个地方的共同点,就是都会对页表进行大量的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 trampoline.S userret
      ld tp, 64(a0)
      +
      //in exec.c
      // Commit to the user image.
      oldpagetable = p->pagetable;
      p->pagetable = pagetable;
      -

      注意,更新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;
      }
      +
      //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
      -

      注意,不同于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. 需要去掉freewalk中的panic

        +

        我的kvmcopy的实现是,user pagetable(下面简称up)和tp的相同虚拟地址共用同一页物理内存。也就是说,页表不一样,但所指向的物理内存是同一个。这样设计的目的是为了能够让tp及时用到up的更新后的数据。

        +

        这会导致啥呢?在进程释放时,需要一起调用proc_freepagetableproc_freekpgtblproc_freepagetable调用完后,所指向的那堆物理内存已经寄完了,如果再调用proc_freekpgtbl,显然,就会发生页表未释放但页表对应内存已经释放的问题,freewalk就会panic。因此,我简单粗暴地直接把freewalk的panic删掉了【抖】也许有别的解决方法,但我真是烦得不想想了放过我吧(

      3. -
      4. 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.

        +
      5. 好像暂时没有第二点了()

      -

      这样一来,信号量实现就可修改为这样了:

      -

      image-20230120151051989

      -

      但是,我们可以注意到,在212-213行这里产生了一个先检查后执行的竞态条件。

      +
      渔翁之利函数
      // 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;
      }
      + +
      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);
      + +
      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;
      }
      + +
      // 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;
      }
      + +
      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");
      }
      +]]> + + + Operating system oganization + /2023/01/10/xv6$chap2/ + Operating system oganization
      +

      Before you start coding, read Chapter 2 of the xv6 book, and Sections 4.3 and 4.4 of Chapter 4, and related source files:

      +
        +
      • The user-space code for systems calls is in user/user.h and user/usys.pl.
      • +
      • The kernel-space code is kernel/syscall.h, kernel/syscall.c.
      • +
      • The process-related code is kernel/proc.h and kernel/proc.c.
      • +
      +
      +

      这章主要是讲了操作系统为了兼顾并发性、隔离性、交互性做出的基本架构。

      +

      Kernel organization

      宏内核与微内核

      操作系统一个很重要的设计问题就是,哪部分的代码需要run在内核态,哪部分的需要run在用户态。

      +

      如果将操作系统所有系统调用统统都在内核态run,这种设计方式就叫宏内核monolithic kernel

      +

      如果仅将系统调用中必要的部分在内核态run,其他部分都在用户态run,并且采取Client/Server这样的异步通信方式,这种设计方式就叫微内核microkernel

      +

      image-20230107232802540

      +
      +

      由于客户/服务器(Client/Server)模式,具有非常多的优点,故在单机微内核操作系统中几乎无一例外地都采用客户/服务器模式,将操作系统中最基本的部分放入内核中,而把操作系统的绝大部分功能都放在微内核外面的一组服务器(进程)中实现。

      +
      +

      在微内核中,内核接口由一些用于启动应用程序、发送消息、访问设备硬件等的低级功能组成。这种组织允许内核相对简单,因为大多数操作系统驻留在用户级服务器中。

      +

      像大多数Unix操作系统一样,Xv6是作为一个宏内核实现的。因此,xv6内核接口对应于操作系统接口,内核实现了完整的操作系统。

      +

      Code: xv6 organization

      XV6的源代码位于kernel子目录中,源代码按照模块化的概念划分为多个文件,图2.2列出了这些文件,模块间的接口都被定义在了def.hkernel/defs.h)。

      + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      文件描述
      bio.c文件系统的磁盘块缓存
      console.c连接到用户的键盘和屏幕
      entry.S首次启动指令
      exec.cexec()系统调用
      file.c文件描述符支持
      fs.c文件系统
      kalloc.c物理页面分配器
      kernelvec.S处理来自内核的陷入指令以及计时器中断
      log.c文件系统日志记录以及崩溃修复
      main.c在启动过程中控制其他模块初始化
      pipe.c管道
      plic.cRISC-V中断控制器
      printf.c格式化输出到控制台
      proc.c进程和调度
      sleeplock.cLocks that yield the CPU
      spinlock.cLocks that don’t yield the CPU.
      start.c早期机器模式启动代码
      string.c字符串和字节数组库
      swtch.c线程切换
      syscall.cDispatch system calls to handling function.
      sysfile.c文件相关的系统调用
      sysproc.c进程相关的系统调用
      trampoline.S用于在用户和内核之间切换的汇编代码
      trap.c对陷入指令和中断进行处理并返回的C代码
      uart.c串口控制台设备驱动程序
      virtio_disk.c磁盘设备驱动程序
      vm.c管理页表和地址空间
      +

      图2.2:XV6内核源文件

      +

      Process overview

      内核用来实现进程的机制包括用户态内核态标志、地址空间和进程的时间切片。

      +

      为了帮助加强隔离,进程抽象给程序提供了一种错觉,即它有自己的专用机器。进程为程序提供了一个看起来像是私有内存系统或地址空间的东西,其他进程不能读取或写入。进程还为程序提供了看起来像是自己的CPU来执行程序的指令。

      +

      Xv6使用页表(由硬件实现)为每个进程提供自己的地址空间。RISC-V页表将虚拟地址(RISC-V指令操纵的地址)转换(或“映射”)为物理地址(CPU芯片发送到主存储器的地址)。

      +

      每个进程也有自己的页表,页表中记录了以虚拟地址0开始的内存区域。

      +

      image-20230107233741922

      +

      xv6内核为每个进程维护许多状态片段,并将它们聚集到一个proc(*kernel/proc.h*:86)结构体中。一个进程最重要的内核状态片段是它的页表、内核栈区和运行状态。我们将使用符号p->xxx来引用proc结构体的元素;例如,p->pagetable是一个指向该进程页表的指针。

      -

      如果消费者进程执行到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;
      }
      +

      这应该相当于pcb表。

      -

      这样一来,信号量就可以完美实现了:

      -

      image-20230120151807102

      -

      image-20230120151820455

      +

      Code: starting xv6 and the first process

      看完一遍说实话还乱乱的。。。。我整理整理跟linux的对比学习一下吧。

      +

      xv6

      加载操作系统

      系统加电,启动BIOS初始化硬件 -> BIOS从引导扇区将加载程序读入内存 -> 加载程序将操作系统镜像读入内存RAM。

      -

      注:严格地说,wakeup只需跟在acquire之后就足够了(也就是说,可以在release之后调用wakeup

      -

      【想了一下,有一说一确实,放在release前后都不影响】

      +

      这个过程由qemu模拟。

      +

      首先会通过mkfs造出操作系统镜像。然后由qemu将引导扇区,也即下面的filesys这图里的第0块:

      +

      image-20230121162324747

      +

      读入到主存中,然后开始执行引导扇区的程序,下同。

      +

      boot loader目的是把xv6加载进内存到0x8000 0000,然后跳转到xv6初始化程序。

      -

      原始Unix内核的sleep只是禁用了中断,这就足够了,因为Unix运行在单CPU系统上。因为xv6在多处理器上运行,所以它为sleep添加了一个显式锁。

      +

      The reason it places the kernel at 0x80000000 rather than 0x0 is because the address range 0x0:0x80000000 contains I/O devices.

      -

      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);
      }
      }
      +

      操作系统初始化

      entry.S配置栈空间

      此时,目前的机器状态是,1.没有开启地址映射,也即虚拟地址=真实物理地址。2.运行在machine mode

      +

      xv6会在kernel/entry.S下的这里开始执行,目的是配置好栈,以开始C语言代码start.c的执行:

      +
      .global _entry
      _entry:
      # set up a stack for C.
      # 这段主要是在计算栈顶指针sp
      # stack0 is declared in start.c,
      # with a 4096-byte stack per CPU.
      # sp = stack0 + (hartid * 4096)
      la sp, stack0
      li a0, 1024*4
      csrr a1, mhartid
      addi a1, a1, 1
      mul a0, a0, a1
      add sp, sp, a0
      # 已经有栈了,就可以开始执行C语言代码了
      # jump to start()
      call start
      -

      注意,如果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);
      }
      }
      +

      其中start0:

      +
      __attribute__ ((aligned (16))) char stack0[4096 * NCPU];
      -

      可以注意到,关于chan这一变量的取值是非常任意的,仅需取一个约定俗成的值就OK。这里取为了信号量的地址,同时满足了逻辑需求和语义需求。

      +
      start.c

      在start.c中,我们的任务是在machine mode下,获取machine mode才能访问到的硬件参数,做在machine mode 下才能做的时钟初始化【 it programs the clock chip to generate timer interrupts】,然后进行machine mode到内核态的切换,最后跳转到main.c进行操作系统的初始化和第一个进程的启动。

      +

      而其中,如果想从machine mode切换到内核态,就需要使用mret指令。但是mret指令除了会切换mode之外,还有一个“ret”的作用,并且是从machine mode ret到内核态。

      -

      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.

      +

      This instruction( mret ) is most often used to return from a previous call from supervisor mode to machine mode.

      -

      这里也解释了为什么需要while循环

      +

      所以,我们实际上可以把最后两步连起来,用mret一个指令就完成。也即,mret指令既完成了从machine mode到内核态的切换,又完成了从start.c到main.c的跳转。

      +

      这其实很容易,只需在栈中将调用者(此时应该是entry.S)的地址替换为main.c的地址,并且将调用者的mode改为内核态,这样就ok了。

      -

      有时,多个进程在同一个通道上睡眠;例如,多个进程读取同一个管道。一个单独的wakeup调用就能把他们全部唤醒。其中一个将首先运行并获取与sleep一同调用的锁,并且(在管道例子中)读取在管道中等待的任何数据。尽管被唤醒,其他进程将发现没有要读取的数据。从他们的角度来看,醒来是“虚假的”,他们必须再次睡眠。因此,在检查条件的循环中总是调用sleep

      +

      it sets the previous privilege mode to supervisor in the register mstatus, it sets the return address to main by writing main’s address into the register mepc, disables virtual address translation in supervisor mode by writing 0 into the page-table register satp, and delegates all interrupts and exceptions to supervisor mode

      +

      后面两点不大明白。为什么为了mret,就还得让内核态跟machine mode一样关闭虚拟地址映射,还得把什么中断和异常委托给内核态??

      +

      【我猜测是因为现在页表还没初始化好所以当然得关闭虚拟地址映射();后者大概是开中断的意思?】

      -

      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
      };
      - -
      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;
      }
      +

      代码如下:

      +
      // entry.S jumps here in machine mode on stack0.
      void
      start()
      {
      //修改调用者为内核态
      // set M Previous Privilege mode to Supervisor, for mret.
      unsigned long x = r_mstatus();
      x &= ~MSTATUS_MPP_MASK;
      x |= MSTATUS_MPP_S;
      w_mstatus(x);

      // set M Exception Program Counter to main, for mret.
      // requires gcc -mcmodel=medany
      w_mepc((uint64)main);

      // disable paging for now.
      w_satp(0);

      // delegate all interrupts and exceptions to supervisor mode.
      w_medeleg(0xffff);
      w_mideleg(0xffff);
      w_sie(r_sie() | SIE_SEIE | SIE_STIE | SIE_SSIE);

      // configure Physical Memory Protection to give supervisor mode
      // access to all of physical memory.
      w_pmpaddr0(0x3fffffffffffffull);
      w_pmpcfg0(0xf);

      // ask for clock interrupts.
      timerinit();

      // keep each CPU's hartid in its tp register, for cpuid().
      int id = r_mhartid();
      w_tp(id);

      // switch to supervisor mode and jump to main().
      asm volatile("mret");
      }
      -
      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;
      }
      +
      main.c

      main.c的作用是做很多很多init。其中,它通过userinit();来创建第一个进程,这个第一个进程再由main调用scheduler()来被调度执行。

      +
      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 {
      while(started == 0)
      ;
      __sync_synchronize();
      /*
      RISC-V的处理器对底层提供了一种特殊的抽象,Hardware Thread,简称为Hart。简单来说,Hart是真实物理CPU(bare metal)提供的一种模拟
      */
      printf("hart %d starting\n", cpuid());
      kvminithart(); // turn on paging
      trapinithart(); // install kernel trap vector
      plicinithart(); // ask PLIC for device interrupts
      }

      //调用第一个scheduler,完成对scheduler线程的初始化,并且调度去执行第一个进程
      scheduler();
      }
      -

      一个非常有意思且巧妙的点,就是读写管道等待在不同的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;因此,每个子进程退出后都有一个父进程进行清理。

      +

      注:关于里面的cpuid,我查了一下,指的是CPU的序列号,用来唯一标识cpu的。我想这个if架构的目的应该跟fork()==0差不多。也就是说,一开始的那个init仅有cpuid==0的CPU执行,其他的CPU就乖乖wait,只有CPU0执行初始化的程序。等到CPU0执行完所有init,才置标记位start=1,然后通过条件变量start控制抢占调度,轮流初始化自己。其中__sync_synchronize是GNU内置指令,起内存屏障作用。在竞赛中深刻地了解过了内存屏障,在这里再次跟老熟人再会感觉还是很有意思的。

      -

      又是一个生产者消费者模式。只不过此时的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");
      }
      +
      proc.c中的userinit()

      userinit的作用就是新创建一个进程信息proc,然后开始给第一个程序(initcode)填信息填入proc。这个进程创建完后,在main中的scheduler被调度执行。

      +
      void
      userinit(void)
      {
      struct proc *p;

      p = allocproc();
      initproc = p;

      // 申请一页,将initcode的指令和数据放进去
      // allocate one user page and copy initcode's instructions
      // and data into it.
      uvmfirst(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);
      }
      -
      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
      }
      }
      +
      initcode.S

      以上程序都位于kernel/下。这个位于user/下。

      +

      它调用exec系统调用进入了内核态。当exec完成后,它就跳转到了用户态user/init.c中。【这里估计又用了修改返回地址的trick】

      +
      .globl start
      start:
      la a0, init
      la a1, argv
      li a7, SYS_exec
      ecall
      # char init[] = "/init\0";
      init:
      .string "/init\0"

      # char *argv[] = { init, 0 };
      .p2align 2
      argv:
      .long init
      .long 0
      -

      其中值得注意的几个点:

      +
      init.c

      在init.c中,创建了console设备文件,打开了012文件描述符,并且fork了一个子进程,开始执行shell。这样一来,操作系统就完成了全部的启动。

      +

      感想

      +

      我的疑点有三个:

        -
      1. wait中的sleep中释放的条件锁是等待进程的p->lock,这是上面提到的特例。

        +
      2. 见start.c

      3. -
      4. 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.
        }
        }
        }
        +
      5. 是怎么完成从内核态到用户态的切换的?是执行了return就会自动切换吗?userinit中设置了initcode的信息为用户态的,然后就直接能进入用户态,这里感觉有点模糊。

        +

        其实用户态和内核态本质上好像差别不大,似乎也就只有两方面,一个是页表(虚拟地址),另一个就是权限问题了。前者很好说,在main.c中完成了页表初始化,开启了虚拟地址:

        +
        kvminit();       // create kernel page table
        kvminithart(); // turn on paging
        -

        如果子进程退出,就会通过init的wait释放它们。然后init释放完它们后进入第三个if分支,继续进行循环。

        +

        后者的话,从用户态切到内核态使用ecall指令,从machine mode到内核态需要修改mstatus寄存器并且使用mret指令:

        +
        // set M Previous Privilege mode to Supervisor, for mret.
        unsigned long x = r_mstatus();
        x &= ~MSTATUS_MPP_MASK;
        x |= MSTATUS_MPP_S;
        w_mstatus(x);
        ...
        // switch to supervisor mode and jump to main().
        asm volatile("mret");
        + +

        因而从内核态切换到用户态应该也是需要类似这段对mstatus寄存器的修改的,并且其对应修改的是sstatus寄存器。

        +

        但是,我只在普通的用户态-trap入内核态-用户态这个过程的usertrapret中看到对sstatus寄存器的写入,并没有在init的时候对这个玩意进行写入。

        +

        所以,最后,我初步猜测,是会在scheduler()中的上下文切换中修改sstatus寄存器的内容为user mode,从而实现由内核态向用户态进程(initcode)的切换。不过这也仅仅是【猜想】,因为我并没有在switch的汇编代码中看到对sstatus的修改。真是令人麻木。。。

      6. -
      7. 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;
        }
        }
      -

      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)。当低优先级进程和高优先级进程共享一个锁时,可能会发生优先级反转,当低优先级进程持有该锁时,可能会阻止高优先级进程前进。当许多高优先级进程正在等待一个获得共享锁的低优先级进程时,可能会形成一个长的等待进程航队;一旦航队形成,它可以持续很长时间。为了避免此类问题,在复杂的调度器中需要额外的机制。

      +

      步骤十分直接且有理由:

      +

      加载操作系统——为了能执行C语言需要一个栈,所以得执行造一个的代码,然后再进入C语言zone——做点machine mode才能做的事,然后从machine mode切换到内核态——做点内核态才能做的事,从内核态切换到用户态

      +

      linux0.11

      bootsect -> setup -> head.s ->main.c

      +

      加载操作系统

      系统加电,启动BIOS初始化硬件 -> BIOS从引导扇区将加载程序读入内存 -> 加载程序将操作系统镜像读入内存RAM。

      +

      其中,第二三步做进一步的细化。

      +
      读入bootsect.s

      加载程序的512个字节被读入到内存从0x7c00开始的一段内存中,并且BIOS设置CS=07c0,ip=0,开始执行加载程序的每一条指令。

      +
      bootsect.s

      加载程序的代码为bootsect.s。在bootsect.s中,首先将自身从7c00处移动到了9000处【留下空间放操作系统】,然后分别依次读取磁盘的setupsystem模块,最后bootsect将控制权转交给setup。

      +
      setup.s

      setup首先获取操作系统运行的必要硬件参数

      +

      image-20230108011824655

      +

      再然后,将system代码移到0地址。然后,我们就需要进入system代码块。

      +

      image-20230108012316631

      +

      最后一句jmpi指令本来应该是要跳到system代码段首0地址处的的,可此处却跳到了80处,这显然不合理。但它写的肯定是没错的。之所以会有这样的矛盾,是因为setup在此之前,还做了一件事情:改变寻址方式。jmpi上面的那条mov指令便做了这点。

      +

      我们之前的寻址方式一直是cs<<4+ip。但是这东西只能是16位的内存,无法满足寻址需求。故而setup要从16位切换到32位。32位模式也叫保护模式。

      -

      wakeup中扫描整个进程列表以查找具有匹配chan的进程效率低下。一个更好的解决方案是用一个数据结构替换sleepwakeup中的chan,该数据结构包含在该结构上休眠的进程列表,例如Linux的等待队列。

      +

      至于怎么切的呢?要注意到一点,改变寻址方式也即改变cs和ip的地址计算方法,也即换一条硬件电路实现。计算机给我们提供了一个简单的方式操纵保护模式的转变,即修改cr0寄存器的内容。

      -

      是的,linux的那个wakeup真的很牛,我现在都还记得当初学到那的时候的震撼。

      +

      在保护模式下,寻址方式发生了改变。此时cs不再代表基址,而是表示地址在gdb表global description table中的偏移下标。真正的基址放在表项中。cs被称为selector,从表中取得基址,再和ip加在一起得到地址。

      -

      wakeup的实现会唤醒在特定通道上等待的所有进程,可能有许多进程在等待该特定通道。操作系统将安排所有这些进程,它们将竞相检查睡眠条件。进程的这种行为有时被称为惊群效应(thundering herd),最好避免。

      -

      大多数条件变量都有两个用于唤醒的原语:signal用于唤醒一个进程;broadcast用于唤醒所有等待进程。

      +

      gdt表的内容由setup初始化

      +

      image-20230108013156116

      -
      -

      一个实际的操作系统将在固定时间内使用空闲列表找到自由的proc结构体,而不是allocproc中的线性时间搜索;xv6使用线性扫描是为了简单起见。

      +

      这样一来,就正确跳到了system模块。

      +

      操作系统初始化

      head.s

      跳到system第一个文件,也就是head.s去执行。

      +

      head.s也是在保护模式下进行的,是在保护模式下的初始化。

      +

      head.s建立了真正的gdt表,然后就要跳转到main.c执行初始化和Shell的启动。此处有汇编语言和C语言的转化,也就是push参数然后push main的地址。

      +
      main.c

      对各种东西的初始化。

      +

      image-20230108013849664

      +

      最后完成从内核态到用户态的切换。

      +

      感想

      +

      linux0.11的启动的具体思路是:

      +

      加载操作系统,获取硬件参数,进入保护模式,跳转到操作系统第一行代码——操作系统初始化,切换到用户态

      +

      linux0.11相比于xv6更加复杂,上课的时候隐藏了很多实现细节但依旧理解很费劲(。

      +

      这两个步骤思路其实都是差不多的,区别在于linux0.11好像没有machine mode这个概念。感觉也不能锐评什么,因为看完了感觉两个都很有道理,两个都一样很难懂(。

      +

      【注:为什么没有machine mode呢?是因为这个mode的划分是RISC-V架构做的,而linux0.11是基于X86架构。】

      +

      不过linux0.11这里进入保护模式后改变寻址方式是因为机器问题(好像是),xv6难道也是因为硬件问题吗?因为一开始的时候操作系统还未进行内存分页页表初始化,所以用不了地址映射?有待学习。

      +

      关于保护模式,可以看看这篇文章,今天太晚了先睡了:

      +

      Linux从头学08:Linux 是如何保护内核代码的?【从实模式到保护模式】

      -

      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.

      +

      Real world

      现实中,大多数操作系统都会兼顾宏内核与微内核。

      +

      大多数操作系统都支持与xv6类似的process进程概念,也有很多系统还支持线程概念。

      +

      Lab system calls

      +

      To start the lab, switch to the syscall branch:

      +
      $ git fetch
      $ git checkout syscall
      $ make clean
      -

      这个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.

      +

      trace

      +

      In this assignment you will add a system call tracing feature that may help you when debugging later labs.

      +

      You’ll create a new trace system call that will control tracing. It should take one argument, an integer “mask”, whose bits specify which system calls to trace.

      +

      For example, to trace the fork system call, a program calls trace(1 << SYS_fork). You have to modify the xv6 kernel to print out a line when each system call is about to return. The line should contain the process id, the name of the system call and the return value; you don’t need to print the system call arguments.

      +

      The trace system call should enable tracing for the process that calls it and any children that it subsequently forks, but should not affect other processes.

      -

      感想

      思路

      看了一遍它这里面写的题目还是有点抽象的,需要结合着给的代码看,那样就清晰多了。

      -

      首先,要补全的地方有这几个:

      -
      // 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 */
      +

      感想

      一开始为了把trace做得封装性良好一些尽量不改别的代码,想了好久好久,最后就只能想出,在syscall.c获取系统调用返回值处加个条件打印,在trace中维护一个map,映射进程pid和进程当前的mask,并且给外界提供查询当前进程是否对某个系统调用有mask作为syscall条件打印的接口。

      +

      这个最后还是失败了,失败的点在于不知道要创建多大的数组来作为map映射所有进程,因为pid分配估计是递增的,是会超过最大进程数的,所以pid会是多少是不确定的。还有一点就是fork之后子进程不能自动继承父进程的mask,还得手动调用一下trace,这更加不封装了(。

      +

      总之先放上我原来的代码吧。

      +
      // in kernel/syscall.c
      #include "types.h"
      #include "param.h"
      #include "memlayout.h"
      #include "riscv.h"
      #include "spinlock.h"
      #include "proc.h"
      #include "syscall.h"
      #include "defs.h"

      //...

      void strcpy(char* buf,const char* tmp){
      int i=0;
      while((*tmp)!='\0'){
      buf[i++] = *tmp;
      tmp++;
      }
      buf[i] = '\0';

      }

      void getname(int callid,char* buf){
      switch(callid){
      case SYS_fork: strcpy(buf,"fork"); break;
      case SYS_exit: strcpy(buf,"exit"); break;
      case SYS_wait: strcpy(buf,"wait"); break;
      case SYS_pipe: strcpy(buf,"pipe"); break;
      case SYS_read: strcpy(buf,"read"); break;
      case SYS_kill: strcpy(buf,"kill"); break;
      case SYS_exec: strcpy(buf,"exec"); break;
      case SYS_fstat: strcpy(buf,"fstat"); break;
      case SYS_chdir: strcpy(buf,"chdir"); break;
      case SYS_dup: strcpy(buf,"dup"); break;
      case SYS_getpid: strcpy(buf,"getpid"); break;
      case SYS_sbrk: strcpy(buf,"sbrk"); break;
      case SYS_sleep: strcpy(buf,"sleep"); break;
      case SYS_uptime: strcpy(buf,"uptime"); break;
      case SYS_open: strcpy(buf,"open"); break;
      case SYS_write: strcpy(buf,"write"); break;
      case SYS_mknod: strcpy(buf,"mknod"); break;
      case SYS_unlink: strcpy(buf,"unlink"); break;
      case SYS_link: strcpy(buf,"link"); break;
      case SYS_mkdir: strcpy(buf,"mkdir"); break;
      case SYS_close: strcpy(buf,"close"); break;
      case SYS_trace: strcpy(buf,"trace"); break;
      default: return;
      }
      }

      void
      syscall(void)
      {
      int num;
      struct proc *p = myproc();

      num = p->trapframe->a7;
      if(num > 0 && num < NELEM(syscalls) && syscalls[num]) {
      p->trapframe->a0 = syscalls[num]();
      char buf[32];
      getname(num,buf);
      // 在此处添加条件打印
      if(istraced(num))
      printf("syscall %s -> %d\n",buf,p->trapframe->a0);
      } else {
      printf("%d %s: unknown sys call %d\n",
      p->pid, p->name, num);
      p->trapframe->a0 = -1;
      }
      }
      -

      这几个函数到时候会被如此调用:

      -
      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 kernel/trace.c
      #include "types.h"
      #include "param.h"
      #include "memlayout.h"
      #include "riscv.h"
      #include "spinlock.h"
      #include "proc.h"
      #include "defs.h"
      #include "elf.h"

      int m_mask[NPROC];

      int
      trace(int mask){
      struct proc *p = myproc();
      m_mask[p->pid] = mask;
      return 1;
      }

      //提供给外界查询的接口
      int
      istraced(int callid){
      struct proc *p = myproc();
      //printf("from trace ,pid = %d\n",p->pid);
      if(((m_mask[p->pid] >> callid) & 1) == 1){
      return 1;
      } else{
      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;
      +

      下面是按照hints修改后的正确代码。

      +

      代码步骤

      实际上,标答跟我的思路差不多,只不过它没有像我一样创建数组作为map,而是在proc结构体里添加了一个属性,这本质上也是利用了map。

      +
      在各种文件添加签名
      user/user.h
      user/usys.pl
      syscall.h

      添加系统调用号

      +
      syscall.c

      添加系统调用号和sys_trace映射

      +
      修改Makefile
        +
      1. 在第一个OBJS添加trace.o
      2. +
      3. 在UPROGS添加user中的trace
      4. +
      +
      代码
      修改proc.h
      // Per-process state
      struct proc {
      // ...
      int mask; //记录trace的mask
      };
      -

      需要注意,栈顶并不是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
      +
      编写trace.c
      #include "types.h"
      #include "param.h"
      #include "memlayout.h"
      #include "riscv.h"
      #include "spinlock.h"
      #include "proc.h"
      #include "defs.h"
      #include "elf.h"

      int
      trace(int mask){
      struct proc *p = myproc();
      p->mask = mask;
      return 1;
      }

      int
      istraced(int callid){
      struct proc *p = myproc();
      if(((p->mask >> callid) & 1) == 1){
      return 1;
      } else{
      return 0;
      }
      }
      -

      栈是向下增长的,因而,栈顶确实应该是数组的末尾……

      -

      这里完全没有想到,还是吃了基础的亏啊。

      -
      -

      如果这里将t->stack作为sp,那么运行时会出现非常诡异的现象(打印的是abc三个的thread->state):

      -

      image-20230120232149776

      -

      仅有c【经测试,是仅有最后一个启动的线程】在执行,而ab的state都不是理想中的2,而是很奇怪的值。我确实有想过栈溢出问题,但是马上被我否定了。我完全没有想到是那样错的【悲】

      +
      修改syscall.c
      // in kernel/syscall.c
      #include "types.h"
      #include "param.h"
      #include "memlayout.h"
      #include "riscv.h"
      #include "spinlock.h"
      #include "proc.h"
      #include "syscall.h"
      #include "defs.h"

      //...

      void strcpy(char* buf,const char* tmp){
      int i=0;
      while((*tmp)!='\0'){
      buf[i++] = *tmp;
      tmp++;
      }
      buf[i] = '\0';

      }

      void getname(int callid,char* buf){
      switch(callid){
      case SYS_fork: strcpy(buf,"fork"); break;
      case SYS_exit: strcpy(buf,"exit"); break;
      case SYS_wait: strcpy(buf,"wait"); break;
      case SYS_pipe: strcpy(buf,"pipe"); break;
      case SYS_read: strcpy(buf,"read"); break;
      case SYS_kill: strcpy(buf,"kill"); break;
      case SYS_exec: strcpy(buf,"exec"); break;
      case SYS_fstat: strcpy(buf,"fstat"); break;
      case SYS_chdir: strcpy(buf,"chdir"); break;
      case SYS_dup: strcpy(buf,"dup"); break;
      case SYS_getpid: strcpy(buf,"getpid"); break;
      case SYS_sbrk: strcpy(buf,"sbrk"); break;
      case SYS_sleep: strcpy(buf,"sleep"); break;
      case SYS_uptime: strcpy(buf,"uptime"); break;
      case SYS_open: strcpy(buf,"open"); break;
      case SYS_write: strcpy(buf,"write"); break;
      case SYS_mknod: strcpy(buf,"mknod"); break;
      case SYS_unlink: strcpy(buf,"unlink"); break;
      case SYS_link: strcpy(buf,"link"); break;
      case SYS_mkdir: strcpy(buf,"mkdir"); break;
      case SYS_close: strcpy(buf,"close"); break;
      case SYS_trace: strcpy(buf,"trace"); break;
      default: return;
      }
      }

      void
      syscall(void)
      {
      int num;
      struct proc *p = myproc();

      num = p->trapframe->a7;
      if(num > 0 && num < NELEM(syscalls) && syscalls[num]) {
      p->trapframe->a0 = syscalls[num]();
      char buf[32];
      getname(num,buf);
      // 在此处添加条件打印
      if(istraced(num))
      printf("syscall %s -> %d\n",buf,p->trapframe->a0);
      } else {
      printf("%d %s: unknown sys call %d\n",
      p->pid, p->name, num);
      p->trapframe->a0 = -1;
      }
      }
      + +
      在sysproc.c中添加系统调用
      uint64
      sys_trace(void)
      {
      int mask;
      if(argint(0,&mask)<0)
      return -1;
      trace(mask);
      return 0;
      }
      + +
      修改fork

      继承父进程的mask

      +
      np->mask = p->mask;
      + +
      在defs.h中添加需要public的函数签名
      // trace.c
      int trace(int);
      int istraced(int);
      + +

      sysinfotest

      +

      In this assignment you will add a system call, sysinfo, that collects information about the running system.

      +

      The system call takes one argument: a pointer to a struct sysinfo (see kernel/sysinfo.h).

      +

      The kernel should fill out the fields of this struct: the freemem field should be set to the number of bytes of free memory, and the nproc field should be set to the number of processes whose state is not UNUSED.

      +

      We provide a test program sysinfotest; you pass this assignment if it prints “sysinfotest: OK”.

      -

      代码

      增加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;
      };
      +
      // kernel/sysinfo.h
      struct sysinfo {
      uint64 freemem; // amount of free memory (bytes)
      uint64 nproc; // number of process
      };
      + +

      感想

      代码

      系统调用要做的事情同上。

      +

      有一个我在hit实验没想到,在这里依然没有想到的点是,参数的指针来自用户空间,所以不能直接对其指向的空间进行写入,需要借助copyout函数。

      +

      还有一件事,就是不知道该怎么统计free mem的数量,后来在hints提示下才知道要去kalloc.c中找。【之前只找过了vm.c】这里其实是很后悔提前看了提示的。我应该先去看一下上面关于kernel各个文件用途的笔记,再去继续自己找的,不能太过依赖提示。

      +

      还有一点做的不好的地方是,标答是选择了将两个计数函数放在各自的文件中,我是选择直接将成员变量在头文件中extern 公开出来,比如说在proc.h中这么写:

      +
      extern struct proc proc[NPROC];
      -
      修改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;
      }
      +

      hints采取了比我封装性更好的操作,这也是非常顺理成章的,我没有想到这样真是有点惭愧(。

      +

      总而言之,这个还是挺简单的,就是我很后悔我心浮气躁看了提示,要不然收获会更多。

      +
      sysinfo.c
      #include "types.h"
      #include "param.h"
      #include "memlayout.h"
      #include "riscv.h"
      #include "spinlock.h"
      #include "proc.h"
      #include "defs.h"
      #include "elf.h"
      #include "sysinfo.h"

      int
      sysinfo(struct sysinfo* info){
      struct sysinfo res;
      res.nproc = countproc();
      res.freemem = countfree();
      struct proc *p = myproc();
      if(copyout(p->pagetable, (uint64)info,(char *)(&res), sizeof(res)) != 0)
      return -1;
      return 1;
      }
      -
      修改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;
      +
      sysproc.c中
      uint64
      sys_sysinfo(void){
      uint64 addr;
      if(argaddr(0, &addr) < 0)
      return -1;
      return sysinfo((struct sysinfo*)addr);
      }
      -
      修改thread_switch

      全部照搬kernel/swtch.S,没什么好说的

      -

      Using threads

      一步步细粒度化,最后,每个桶用单独一把锁,仅在调用insert处加锁就行。

      -
      pthread_mutex_t locks[NBUCKET];// 在main中初始化

      static
      void put(int key, int value)
      {
      int i = key % NBUCKET;

      // is the key already present?
      struct entry *e = 0;
      for (e = table[i]; e != 0; e = e->next) {
      if (e->key == key)
      break;
      }
      if(e){
      // update the existing key.
      e->value = value;
      } else {
      // the new is new.
      pthread_mutex_lock(&locks[i]);
      insert(key, value, &table[i], table[i]);
      pthread_mutex_unlock(&locks[i]);
      }
      }
      +
      kalloc.c中
      // 采用的是链表结构,run代表一页
      struct run {
      struct run *next;
      };

      struct {
      struct spinlock lock;
      // 指向第一个空闲页
      struct run *freelist;
      } kmem;

      int
      countfree(){
      int npage = 0;
      struct run* r = kmem.freelist;
      while(r){
      r = r->next;
      npage++;
      }
      return npage*PGSIZE;
      }
      -

      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.

      +
      proc.c中
      struct proc proc[NPROC];
      int
      countproc(){
      int nproc = 0;
      for(int i=0;i<NPROC;i++){
      if(proc[i].state != UNUSED){
      nproc++;
      }
      }
      return nproc;
      }
      + +

      附加题

      trace plus
      +

      Print the system call arguments for traced system calls.

      -

      直接上代码,还是比较简单的

      -
      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));
      }
      +

      这个实现起来要说简单也简单,麻烦也麻烦。这里就先摆了【实际上尝试了半小时发现太烦了看别人写的也不大满意就放弃了】

      +
      sysinfo plus
      +

      Compute the load average and export it through sysinfo

      +
      +

      说实话没太看懂,不就加个 running process/ncpu就行了吗?

      ]]> - 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

      -
      -

      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行左右。

      + Locking + /2023/01/10/xv6$chap6/ + Locking

      很多概念在看Java并发的时候都学习过,这些重复的地方就不做赘述了。

      +

      Code: spinlock

      +

      spinlock 使用介绍

      +

      一、spinlock 简介
      自旋锁(spinlock):是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,不断尝试获取锁,直到获取到锁才会退出循环

      +

      二、自旋锁与互斥锁的区别
      自旋锁与互斥锁类似,它们都是为了解决对某项资源的互斥使用,在任何时刻最多只能有一个线程获得锁
      对于互斥锁,如果资源已经被占用,调用者将进入睡眠状态
      对于自旋锁,如果资源已经被占用,调用者就一直循环在那里,看是否自旋锁的保持者已经释放了锁

      +

      三、自旋锁的优缺点
      自旋锁不会发生进程切换,不会使进程进入阻塞状态,减少了不必要的上下文切换,执行速度快。非自旋锁在获取不到锁的时候会进入阻塞状态,从而进入内核态,当获取到锁的时候需要从内核态恢复,需要线程上下文切换,影响性能
      如果某个线程持有锁的时间过长,就会导致其它等待获取锁的线程长时间循环等待消耗CPU,造成CPU使用率极高

      -

      虚拟地址有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页。

      -

      三级页表结构相比于单级页表结构,可以节省更多内存空间

      +

      spinlock

      // Mutual exclusion lock.
      struct spinlock {
      uint locked; // Is the lock held?

      // For debugging:
      char *name; // Name of lock.
      struct cpu *cpu; // The cpu holding the lock.
      };
      + +

      acquire

      大概是这么个原理:

      +

      image-20230115231857670

      +

      当然这有竞态条件。xv6用的是CPU提供的amoswap原子指令来消除竞态条件的。

      +
      // in kernel/spinlock.c
      // Acquire the lock.
      // Loops (spins) until the lock is acquired.
      void
      acquire(struct spinlock *lk)
      {
      // 关中断
      // xv6允许禁止中断。但是由于xv6是一个多核系统,单个core被禁止中断并不会影响其他core。
      push_off(); // disable interrupts to avoid deadlock.

      // holding(): Check whether this cpu is holding the lock.
      if(holding(lk))
      panic("acquire");

      // On RISC-V, sync_lock_test_and_set turns into an atomic swap:
      // a5 = 1
      // s1 = &lk->locked
      // amoswap.w.aq a5, a5, (s1)
      // amoswap: 交换a5和(s1)的值,返回(s1)原来的值
      // 也即是如图所示的竞态条件的原子指令
      while(__sync_lock_test_and_set(&lk->locked, 1) != 0)
      ;

      __sync_synchronize();

      // Record info about lock acquisition for holding() and debugging.
      lk->cpu = mycpu();
      }
      + +

      __sync_synchronize();

      代码里的官方注释:

      +
      // Tell the C compiler and the processor to not move loads or stores
      // past this point, to ensure that the critical section's memory
      // references happen strictly after the lock is acquired.
      // On RISC-V, this emits a fence instruction.
      + +

      这个注释其实没太看明白。我去翻了一下asm代码,发现这句话正如它最后一句所说的被翻译成fence指令:

      +

      image-20230115231457971

      -

      参考:页表是啥以及为啥多级页表能够节省空间

      +

      处理器中的存储系统(一):RISC-V的FENCE、FENCE.I指令

      +

      顾名思义,FENCE指令犹如一道屏障,把前面的存储操作和后面的存储操作隔离开来,前面的决不能到后面再执行,后面的决不能先于FENCE前的指令执行。

      -

      考虑到这样一个进程:

      -

      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中内核的页表结构。

      -
      -

      这里为了方便,就把三级页表省略了,只留下va和pa的对比

      +

      这个就好明白多了。

      +

      这样一来,acquire和release的两个fence就形成了两道屏障:

      +
      acquire();
      l->nexy = list;
      list = l;
      release();
      + +

      中间那部分的指令可以重排,但是中间的指令就绝不会跑到临界区外。

      +

      push_off和pop_off

      +

      当CPU未持有自旋锁时,xv6重新启用中断;它必须做一些记录来处理嵌套的临界区域。acquire调用push_off (*kernel/spinlock.c*:89) 并且release调用pop_off (*kernel/spinlock.c*:100)来跟踪当前CPU上锁的嵌套级别。当计数达到零时,pop_off恢复最外层临界区域开始时存在的中断使能状态。intr_offintr_on函数执行RISC-V指令分别用来禁用和启用中断。

      -

      每个进程都有一个用户级别的页表。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.
      // ...
      +

      release

      // in kernel/spinlock.c
      // Release the lock.
      void
      release(struct spinlock *lk)
      {
      if(!holding(lk))
      panic("release");

      lk->cpu = 0;

      __sync_synchronize();

      // Release the lock, equivalent to lk->locked = 0.
      // This code doesn't use a C assignment, since the C standard
      // implies that an assignment might be implemented with
      // multiple store instructions.
      // On RISC-V, sync_lock_release turns into an atomic swap:
      // s1 = &lk->locked
      // amoswap.w zero, zero, (s1)
      __sync_lock_release(&lk->locked);

      // 开中断
      pop_off();
      }
      -

      由图可知,一直从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.

        +

        Code: Using locks

        +

        作为粗粒度锁的一个例子,xv6的kalloc.c有一个由单个锁保护的空闲列表。如果不同CPU上的多个进程试图同时分配页面,每个进程在获得锁之前将必须在acquire中自旋等待。自旋会降低性能,因为它只是无用的等待。如果对锁的争夺浪费了很大一部分CPU时间,也许可以通过改变内存分配的设计来提高性能,使其拥有多个空闲列表,每个列表都有自己的锁,以允许真正的并行分配。【很棒的思路】

        +

        作为细粒度锁的一个例子,xv6对每个文件都有一个单独的锁,这样操作不同文件的进程通常可以不需等待彼此的锁而继续进行。文件锁的粒度可以进一步细化,以允许进程同时写入同一个文件的不同区域。最终的锁粒度决策需要由性能测试和复杂性考量来驱动。

        -

        它有一点很特殊的是,它实际对应的物理内存是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可以用来防止内核栈溢出。

        + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
        描述
        bcache.lock保护块缓冲区缓存项(block buffer cache entries)的分配
        cons.lock串行化对控制台硬件的访问,避免混合输出
        ftable.lock串行化文件表中文件结构体的分配
        icache.lock保护索引结点缓存项(inode cache entries)的分配
        vdisk_lock串行化对磁盘硬件和DMA描述符队列的访问
        kmem.lock串行化内存分配
        log.lock串行化事务日志操作
        管道的pi->lock串行化每个管道的操作
        pid_lock串行化next_pid的增量
        进程的p->lock串行化进程状态的改变
        tickslock串行化时钟计数操作
        索引结点的 ip->lock串行化索引结点及其内容的操作
        缓冲区的b->lock串行化每个块缓冲区的操作
        +

        Figure 6.3: Locks in xv6

        +

        Deadlock and lock ordering

        +

        如果在内核中执行的代码路径必须同时持有数个锁,那么所有代码路径以相同的顺序获取这些锁是很重要的。如果它们不这样做,就有死锁的风险。假设xv6中的两个代码路径需要锁A和B,但是代码路径1按照先A后B的顺序获取锁,另一个路径按照先B后A的顺序获取锁。为了避免这种死锁,所有代码路径必须以相同的顺序获取锁。全局锁获取顺序的需求意味着锁实际上是每个函数规范的一部分:调用者必须以一种使锁按照约定顺序被获取的方式调用函数。

        +

        由于sleep的工作方式(见第7章),Xv6有许多包含每个进程的锁(每个struct proc中的锁)在内的长度为2的锁顺序链。例如,consoleintr (*kernel/console.c*:138)是处理键入字符的中断例程。当换行符到达时,任何等待控制台输入的进程都应该被唤醒。为此,consoleintr在调用wakeup时持有cons.lockwakeup获取等待进程的锁以唤醒它。因此,全局避免死锁的锁顺序包括必须在任何进程锁之前获取cons.lock的规则。【这段不怎么能看懂,学完第七章再回来看看】

        +

        文件系统代码包含xv6最长的锁链。例如,创建一个文件需要同时持有目录上的锁、新文件inode上的锁、磁盘块缓冲区上的锁、磁盘驱动程序的vdisk_lock和调用进程的p->lock。为了避免死锁,文件系统代码总是按照前一句中提到的顺序获取锁。

        -
      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;
      }
      +

      Locks and interrupt handlers

      +

      Xv6 is more conservative: when a CPU acquires any lock, xv6 always disables interrupts on that CPU. Interrupts may still occur on other CPUs, so an interrupt’s acquire can wait for a thread to release a spinlock; just not on the same CPU.看来是通过开关中断来保护临界区的

      +

      acquire调用push_off (*kernel/spinlock.c*:89) 并且release调用pop_off (*kernel/spinlock.c*:100)来跟踪当前CPU上锁的嵌套级别。当计数达到零时,pop_off恢复最外层临界区域开始时存在的中断使能状态。intr_offintr_on函数执行RISC-V指令分别用来禁用和启用中断。

      +

      严格的在设置lk->locked (kernel/spinlock.c:28)之前让acquire调用push_off是很重要的。如果两者颠倒,会存在一个既持有锁又启用了中断的短暂窗口期,不幸的话定时器中断会使系统死锁。同样,只有在释放锁之后,release才调用pop_off也是很重要的(*kernel/spinlock.c*:66)。

      +
      +

      一个解决了一半的疑问

      问题

      +

      Xv6更保守:当CPU获取任何锁时,xv6总是禁用该CPU上的中断。中断仍然可能发生在其他CPU上,此时中断的acquire可以等待线程释放自旋锁;由于不在同一CPU上,不会造成死锁。

      +

      进展:似乎书中说到,“sleep atomically yields the CPU and releases the spinlock”。等了解完sleep,也即读完第七章之后再来看看。

      +
      +

      在处理时钟中断的trap.c中:

      +
      // in kernel/trap.c devintr()
      } else if(scause == 0x8000000000000001L){

      // 这里!!
      // in kernel/trap.c devintr()
      if(cpuid() == 0){
      clockintr();
      }

      w_sip(r_sip() & ~2);
      return 2;
      }

      void
      clockintr()
      {
      acquire(&tickslock);
      ticks++;
      wakeup(&ticks);
      release(&tickslock);
      }
      -

      其中,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");
      }
      +

      可见只有CPU0才会进入clockintr【因为要求cpuid==0】,锁住ticks引起ticks递增。

      +

      而当sys_sleep获得锁之后,其结束循环的条件是ticks - ticks0 < n:

      +
      uint64
      sys_sleep(void)
      {
      int n;
      uint ticks0;
      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;
      }
      -

      实现主要逻辑的是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;
      }
      +

      我认为,这会导致死锁情况。假设计算机为多CPU,且从零开始依次递增编号。对该死锁情况的讨论,可以分为以下两类:

      +
        +
      1. sys_sleep在CPU2(或者其他编号非零的CPU)运行,且先获取了tickslock的锁。这时候,ticks将会停止增长,sys_sleep结束循环的条件将无法结束。

        +

        理由:对于CPU0,它可以进入clockintr的代码段,但是由于锁已经被获取,所以就只能一直在那边死锁等待;对于其他CPU来说,压根执行不了那段增加ticks的代码段,所以ticks压根不会增加。这样一来,CPU2进程等待ticks增加,从而获取结束循环的条件;CPU0等待CPU2进程结束,从而使得ticks增加,就造成了死锁。

        +
      2. +
      3. sys_sleep在CPU0运行,且先获取了tickslock的锁。这时候,ticks将会停止增长,sys_sleep结束循环的条件将无法结束。

        +

        理由:由于xv6会在获取锁和释放锁期间关闭中断,因而CPU0无法进行时钟中断而发生进程的切换,只能一直在sys_sleep中等待,所以ticks更不可能增加,造成了死锁。

        +
      4. +
      +

      暂时没有很充分的理由反驳这两点。。。

      +

      解答

      学习完下一章的内容后可知,sleep(&ticks, &tickslock);会释放掉tickslock的锁,这样CPU0就可以进入clockintr增加ticks了。

      +

      再详细梳理一次,这里的具体机制是这样的:

      +

      可以把ticks看做信号量,sys_sleep为消费者,clockintr为生产者。

      +
      // in sys_sleep()
      acquire(&tickslock);
      while(ticks < 某个数字){
      sleep(&ticks, &tickslock);
      }
      release(&tickslock);
      -

      通过虚拟地址获取表项主要是通过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)];
      }
      +
      void
      clockintr()
      {
      acquire(&tickslock);
      ticks++;
      wakeup(&ticks);
      release(&tickslock);
      }
      -
      装上页表

      使用的是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();
      }
      +

      可以看到,这是非常典型的生产者消费者模型。生产者每生产一次ticks,就会唤醒消费者,让消费者检查条件。如果条件错误,则继续sleep等待消费者下一次唤醒,如此循环往复。

      +

      只不过,还有一个小疑点,就是clockintr这段只有CPU0可以执行这一点是否为真依然存疑。如果确实只有CPU0可以执行的话,假若sys_sleep在CPU0上执行,那么还是依然会造成死锁。所以我猜想是不是CPU0是无法关中断的?也就是说CPU0是一个后盾一般的保护角色?或者是别的CPU也能进入本段代码?如果别的CPU也能进,那是怎么实现的?因为很明显这段代码确实只有CPU0可以进入。

      +

      Sleep locks

      关于sleep lock的由来和优点,书里描述得很详细,简单来说就是:

      +
      +

      Thus we’d like a type of lock that yields the CPU while waiting to acquire, and allows yields (and interrupts) while the lock is held.

      +

      因为等待会浪费CPU时间,所以自旋锁最适合短的临界区域;睡眠锁对于冗长的操作效果很好。

      +
      +
      void
      acquiresleep(struct sleeplock *lk)
      {
      acquire(&lk->lk);
      // 等待
      while (lk->locked) {
      // sleep atomically yields the CPU and releases the spinlock
      sleep(lk, &lk->lk);
      }
      // 占用
      lk->locked = 1;
      lk->pid = myproc()->pid;
      release(&lk->lk);
      }

      void
      releasesleep(struct sleeplock *lk)
      {
      acquire(&lk->lk);
      lk->locked = 0;
      lk->pid = 0;
      // 到时候可以留意一下wakeup是会唤醒一个还是多个
      wakeup(lk);
      release(&lk->lk);
      }
      -

      其中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

      在内核运行的时候,需要申请很多空间用来存放各种数据。

      +

      有一点值得注意:

      +
      +

      Because sleep-locks leave interrupts enabled, they cannot be used in interrupt handlers. Because acquiresleep may yield the CPU, sleep-locks cannot be used inside spinlock critical sections (though spinlocks can be used inside sleep-lock critical sections).

      +
      +

      这实际上是因为自旋锁内不能sleep,因而也就不能使用sleep lock。

      +

      为什么不能sleep?我猜测应该是因为sleep中会释放自旋锁然后再调度别的进程。此时,临界区就不受保护了很危险,不符合spinlock在临界区结束才能释放的规范。

      +

      在查阅别人的说法的时候,我还看到了这个讨论:

      -

      The kernel must allocate and free physical memory at run-time for page tables, user memory, kernel stacks, and pipe buffers.

      +

      中断中为什么不能sleep | Linux内核的评论区

      +

      在中断服务程序中,无法sleep的原因应该是sleep后,调度程序将CPU窃走,由于调度的基本单位是线程(中断服务程序不是线程),因此中断服务程序无法再被调度回来,即中断程序中sleep后的部分永远无法得到执行。

      -

      用的是这段空闲内存:

      -

      image-20230109225700837

      -
      -

      It keeps track of which pages are free by threading a linked list through the pages themselves.

      +

      Real world

      +

      大多数操作系统都支持POSIX线程(Pthread),它允许一个用户进程在不同的CPU上同时运行几个线程。Pthread支持用户级锁(user-level locks)、障碍(barriers)等。支持Pthread需要操作系统的支持。例如,如果一个Pthread在系统调用中阻塞,同一进程的另一个Pthread应当能够在该CPU上运行。另一个例子是,如果一个线程改变了其进程的地址空间(例如,映射或取消映射内存),内核必须安排运行同一进程下的线程的其他CPU更新其硬件页表,以反映地址空间的变化。

      -

      kalloc.c中就是这么实现的。

      -

      Code: Physical memory allocator

      内核运行时申请释放空闲物理空间是通过kernel/kalloc.c完成的。它为内核栈、用户进程、页表和管道buffer服务。

      -
      -

      kalloc.c用来在运行时申请分配新的一页,上面的vm.c正是用了kalloc申请一页,要么作为页表,要么作为存储数据的第三级页表指向的物理内存。

      +

      Lab: locks

      +

      In this lab you’ll gain experience in re-designing code to increase parallelism. You’ll do this for the xv6 memory allocator and block cache.

      -

      最后应该会在空闲内存内形成这样的结构:

      -

      内存分成一页一页的,每页内存中的前几个字节存储着其对应队列中下一块内存的物理地址。不一定是从小地址到大地址顺序连接。

      -
      -

      It store each free page’s run structure in the free page itself, since there’s nothing else stored there.

      +

      Memory allocator

      +

      The program user/kalloctest stresses xv6’s memory allocator: three processes grow and shrink their address spaces, resulting in many calls to kalloc and kfree. kalloc and kfree obtain kmem.lock. kalloctest prints (as “#fetch-and-add”) the number of loop iterations in acquire due to attempts to acquire a lock that another core already holds, for the kmem lock and a few other locks. The number of loop iterations in acquire is a rough measure of lock contention.

      +

      To remove lock contention, you will have to redesign the memory allocator to avoid a single lock and list. 也就是说要把kalloc中的整个列表上锁,修改为每个CPU有自己的列表The basic idea is to maintain a free list per CPU, each list with its own lock. Allocations and frees on different CPUs can run in parallel, because each CPU will operate on a different list. The main challenge will be to deal with the case in which one CPU’s free list is empty, but another CPU’s list has free memory; in that case, the one CPU must “steal” part of the other CPU’s free list. Stealing may introduce lock contention, but that will hopefully be infrequent.主要挑战将是处理一个 CPU 的空闲列表为空,但另一个 CPU 的列表有空闲内存的情况; 在这种情况下,一个 CPU 必须“窃取”另一个 CPU 的空闲列表的一部分。

      +

      Your job is to implement per-CPU freelists, and stealing when a CPU’s free list is empty.

      +

      You must give all of your locks names that start with “kmem”. That is, you should call initlock for each of your locks, and pass a name that starts with “kmem”.

      +

      Run kalloctest to see if your implementation has reduced lock contention. To check that it can still allocate all of memory, run usertests sbrkmuch. Your output will look similar to that shown below, with much-reduced contention in total on kmem locks, although the specific numbers will differ.

      -
      // 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去取,然后登记在页表上。

      +

      感想

      总之,意思就是kalloc里面本来是多核CPU共用一个空闲页list,现在要做的就是给每一核的CPU独立分配一个空闲页list。我觉得可以分为如下几步来做:

      +
        +
      1. 定义list数组以及对应的锁

        +

        cpu的数量是一定的;cpuid可以用来作为数组下标索引

        +
      2. +
      3. 在init时初始化锁,在freelist的时候把空闲页均分给CPU

        +
      4. +
      5. 当kalloc和kfree的时候,获取当前cpuid上锁

        +
      6. +
      7. 当一个CPU的内存不够的时候,去向另一个CPU窃取。窃取之前,首先应该获取另一个CPU的锁。

        +
      8. +
      +

      以上是初见思路。正确思路确实跟上面的一样,编码过程也比较简单,没有很恶心的细节和奇奇怪怪的bug,没什么好说的。

      +

      第二步中,hints是推荐把所有空闲页都分给CPU0。

      +

      第四步的时候我是一次窃取一页。我看到一个一次窃取多页的做法,我觉得很有想法,在这里附上链接:

      -

      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.

      -
      -

      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).

      +

      MIT6.S081 lab8 locks

      -
      // 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;
      }
      +

      代码

      定义
      struct {
      struct spinlock kmem_locks[NCPU];
      struct run *freelists[NCPU];
      } kmem;
      -
      // 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;
      }
      +
      初始化

      由于kinit仅会由一个cpu执行一次【详情见main.c】,故而我这里在kinit的做法是由一个CPU初始化所有CPU,而没有选择去修改main.c从而使每个CPU都执行一次kinit。

      +
      void
      kinit()
      {
      for(int i=0;i<NCPU;i++){
      char buf[8];
      snprintf(buf,6,"kmem%d",i);
      initlock(&kmem.kmem_locks[i], buf);
      }
      freerange(end, (void*)PHYSTOP);
      }

      // 多带一个参数表示cpuid,仅在kinit的freerange中使用
      void
      kfree_init(void *pa,int i)
      {
      struct run *r;

      if(((uint64)pa % PGSIZE) != 0 || (char*)pa < end || (uint64)pa >= PHYSTOP)
      panic("kfree");

      // Fill with junk to catch dangling refs.
      memset(pa, 1, PGSIZE);

      r = (struct run*)pa;

      r->next = kmem.freelists[i];
      kmem.freelists[i] = r;
      }
      void
      freerange(void *pa_start, void *pa_end)
      {
      char *p;
      p = (char*)PGROUNDUP((uint64)pa_start);

      // 把空闲内存页均分给每个CPU
      uint64 sz = ((uint64)pa_end - (uint64)pa_start)/NCPU;
      uint64 tmp = PGROUNDDOWN(sz) + (uint64)p;
      for(int i=0;i<NCPU;i++){
      for(; p + PGSIZE <= (char*)tmp; p += PGSIZE)
      kfree_init(p,i);
      tmp += PGROUNDDOWN(sz);
      if(i == NCPU-2){
      tmp = (uint64)pa_end;
      }
      }
      }
      -

      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是创建地址空间的用户部分的系统调用。它使用一个存储在文件系统中的文件初始化地址空间的用户部分。

      -
      -
      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;
      }
      +
      kfree
      void
      kfree(void *pa)
      {
      // ...

      r = (struct run*)pa;
      // 在这
      push_off();
      int id = cpuid();

      acquire(&kmem.kmem_locks[id]);
      r->next = kmem.freelists[id];
      kmem.freelists[id] = r;
      release(&kmem.kmem_locks[id]);
      pop_off();
      }
      -

      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.

      -
      -

      不过遗憾的是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

      -
      -

      感想

      乌龙

      这里好像是因为实验改版了,我下的是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;
      };
      +
      kalloc
      void *
      kalloc(void)
      {
      struct run *r;

      push_off();
      int id = cpuid();

      acquire(&kmem.kmem_locks[id]);
      r = kmem.freelists[id];
      if(r){
      kmem.freelists[id] = r->next;
      }
      release(&kmem.kmem_locks[id]);
      pop_off();

      // 如果无空闲页,则窃取
      if(!r){
      for(int i=NCPU-1;i>=0;i--){
      acquire(&kmem.kmem_locks[i]);
      r = kmem.freelists[i];
      if(r){
      kmem.freelists[i] = r->next;
      release(&kmem.kmem_locks[i]);
      break;
      }
      release(&kmem.kmem_locks[i]);
      }
      }

      if(r)
      memset((char*)r, 5, PGSIZE); // fill with junk
      return (void*)r;
      }
      -

      用户的ugetpid只找到了一个截图:

      -

      v2-0c2603da4c8102e46ae390a0d0b1191d_1440w

      -

      恕我愚钝实在不知道该把这段代码放在哪orz于是接下来写的东西就没有自测。

      -
      panic:freewalk leaf

      一开始写好代码准备启动xv6的时候爆出了这么一个panic,搜了一下得到如下解答:

      +

      Buffer cache

      buffer cache的结构其实跟kalloc的内存分配结构有一定的类似之处,都是采用链表管理,但是buffer cache的实现相较于kalloc更为复杂。

      -

      来源:MIT-6.S081-2020实验(xv6-riscv64)十:mmap

      -

      这时运行会发现freewalk函数panic:freewalk: leaf,这是因为freewalk希望所有虚拟地址已经被解绑并释放对应的物理空间了,该函数只负责释放页表。

      +

      Reducing contention in the block cache is more tricky than for kalloc, because bcache buffers are truly shared among processes (and thus CPUs).

      +

      For kalloc, one could eliminate most contention by giving each CPU its own allocator; that won’t work for the block cache.

      +

      We suggest you look up block numbers in the cache with a hash table that has a lock per hash bucket.

      -

      让我得知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);
      }
      +

      感想

      初见思路

      我想我们可以这么实现:

      +

      首先有一个双向链表,接收着所有空闲无设备分配的buf。然后再有多个双向链表桶,以设备号为索引值。

      +

      设备号数量,也即hash table的大小定义在kernel/param.h中:

      +
      #define NDEV         10 
      -

      这样一来,问题就解决了。

      -
      总结

      因而,可以看到,如果进程想使用页的话,需要经历以下四步:

      +

      bget中,第一个循环仅需在设备链表中查找即可,第二个循环需要先看设备链表是否有空闲的对象,如果没有,则去接收所有空闲无设备分配的那个双向链表中窃取一个对象。

      +

      brelse中,则把要释放的buf对象添加在head中即可。

      +

      因而,我们要做以下几件事:

        -
      1. 通过kalloc获取物理页地址(可以通过该地址对页进行读写),并且记录在进程proc结构中(否则之后就获取不了了)
      2. -
      3. 建立mappages映射
      4. -
      5. 释放物理页
      6. -
      7. 释放PTE映射
      8. +
      9. 修改bcache的定义

        +

        添加数量为设备号的head数组,以及对应的锁

        +
      10. +
      11. 初始化bcache

        +
      12. +
      13. 添加工具函数:将一个buf加入一个双向链表;从一个双向链表中得到一个buf

        +
      14. +
      15. bgetbrelse

        +
      -

      可见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);
      }
      +

      看起来确实好像可以实现的样子,但是这个问题在于,这么做就直接破坏了LRU的这个规则。所以还是不能这么写的。但总之先把我的代码放上来。

      +

      以下代码是不能正常运行的。比如说在执行ls命令时,会发生如下错误:

      +

      image-20230122163938601

      +

      会打印出一些乱七八糟的东西,并且这些东西似乎是固定的,每次都会发生,看来应该不是多进程的问题,而是代码有哪里出现逻辑错误了。不过注意到会产生“stopforking”、“bigarg-ok”,这两个似乎是在usertest中的两个文件名,很奇怪。

      +

      很遗憾我暂时没有精力debug了。姑且先把错误代码放在这里吧。

      +
      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;
      struct buf dev_heads[NDEV];
      struct spinlock dev_locks[NDEV];
      } bcache;
      -

      问答题

      -

      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);
      }
      +
      void
      binit(void)
      {
      struct buf *b;

      initlock(&bcache.lock, "bcache");
      for(int i=0;i<NDEV;i++){
      char buf[10];
      snprintf(buf,9,"bcache%02d",i);
      initlock(&(bcache.dev_locks[i]), buf);
      bcache.dev_heads[i].prev = &(bcache.dev_heads[i]);
      bcache.dev_heads[i].next = &(bcache.dev_heads[i]);
      }

      // 初始时,每一个桶内都有一个buf结点
      b = bcache.buf;
      for(int i=0;i<NDEV;i++){
      b->next = bcache.dev_heads[i].next;
      b->prev = &bcache.dev_heads[i];
      initsleeplock(&b->lock, "buffer");
      bcache.dev_heads[i].next->prev = b;
      bcache.dev_heads[i].next = b;
      b++;
      }

      // Create linked list of buffers
      bcache.head.prev = &bcache.head;
      bcache.head.next = &bcache.head;
      for(; 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;
      }
      }
      -

      问答题

      -

      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);
      }
      +
      void
      brelse(struct buf *b)
      {
      if(!holdingsleep(&b->lock))
      panic("brelse");

      uint dev = b->dev;
      releasesleep(&b->lock);

      acquire(&(bcache.dev_locks[dev]));
      b->refcnt--;
      if (b->refcnt == 0) {
      b->next->prev = b->prev;
      b->prev->next = b->next;
      release(&(bcache.dev_locks[dev]));

      acquire(&bcache.lock);
      b->next = bcache.head.next;
      b->prev = &bcache.head;
      bcache.head.next->prev = b;
      bcache.head.next = b;
      release(&bcache.lock);
      }else
      release(&(bcache.dev_locks[dev]));
      }
      -

      可见,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;
      //...
      }
      +
      static struct buf*
      bget(uint dev, uint blockno)
      {
      acquire(&(bcache.dev_locks[dev]));

      // Is the block already cached?
      for(struct buf* b = bcache.dev_heads[dev].next; b != &(bcache.dev_heads[dev]); b = b->next){
      if(b->blockno == blockno){
      b->refcnt++;
      release(&(bcache.dev_locks[dev]));
      acquiresleep(&b->lock);
      return b;
      }
      }
      release(&(bcache.dev_locks[dev]));

      // 在head中找
      acquire(&bcache.lock);
      // Recycle the least recently used (LRU) unused buffer.
      for(struct buf* b = bcache.head.prev; b != &(bcache.head); b = b->prev){
      if(b->refcnt == 0) {
      b->dev = dev;
      b->blockno = blockno;
      b->valid = 0;
      b->refcnt = 1;
      release(&bcache.lock);
      acquiresleep(&b->lock);
      return b;
      }
      }
      panic("bget: no buffers");
      }
      -

      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需要自己定义

      -

      以上是初见。做完了发现,确实就是那么简单,我主要时间花费在下的实验版本不对,折腾来折腾去了可能有一个小时,最后还是选择了直接把测试函数搬过来手工调用。已经换到正确的年份版本了【泪目】

      -

      有一点我忽视了,看了提示才知道:

      +
      正确思路

      首先,大家似乎都是用blockno来hash的,这点就跟我的原始思路不一样了(。其实也很对,因为每个设备的使用频率是不平均的,用blockno来hash比用dev来hash其实会让访问次数更加平均。

      +

      然后就是怎么保证LRU依然OK。hints的做法是使用时间戳。我们可以在brelse的时候记录时间戳字段,在bget缺块的时候遍历hash table,找出对应timestamp最小的block即可。

      +

      历经了几小时的debug,代码最终正确。正确版本在下面的代码模块处。

      +
      debug过程

      coding过程其实很短暂,毕竟思路很直观。我一开始是按初见思路写的代码,然后再从初见思路改到正确思路,这个过程,给我埋下了极大的安全隐患【悲】

      +

      其实几个小时下来,很多细节都已经忘记了,接下来就说点印象比较深的吧。

      +

      首先,我使用了正确思路以来,依然出现了跟初见思路一样的错误,也即xv6正常boot,但是执行ls命令会有错误。但是,当我make clean之后再次make qemu,错误改变了,变成了xv6 boot失败,并且爆出错误panic:ilock:no type

      -

      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).

      +

      注:关于此处的make clean,有两点需要解释。一是为什么会做出make clean的行为,二是这个变化的原理是什么。

      +

      此处突然做出make clean的行为,是因为参照了该文章:

      +

      MIT6.S081 lab8 locks

      +

      image-20230123172138766

      +

      没想到我make clean之后反而就变成了他这样的问题23333也是感觉蛮惊讶的

      +

      这个的原理说实话我不大清楚。猜想可能是make qemu的某段访问磁盘初始化之类的代码只会执行一次,只有make clean之后才会让其执行第二次。所以我们手动完全boot了一遍操作系统,才会导致这个错误爆出来,否则,操作系统就会使用原本的正确boot版本启动,之后再执行命令就当然是错误的了。

      +

      我想知乎文章里也应该是这个原因。操作系统本来使用的是错误版本,make clean后才会重新使用正确版本。

      +

      我之后写对了又尝试了一下,觉得我的猜想应该是对的。我的执行路线:

      +
        +
      1. make qemu,得到正确结果
      2. +
      3. bio.c改回错误版本
      4. +
      5. 再次make qemu,发现xv6正常boot,但是执行ls命令会出以上同样的错误
      6. +
      7. make clean,然后make qemu,爆出panic:ilock: no type
      8. +
      +

      挺完美地符合了我的猜想。

      +

      【来自之后的学习:

      +

      in lab file system:

      +

      mkfs 程序创建 xv6 文件系统磁盘映像并确定文件系统总共有多少个块; 这个大小由 kernel/param.h 中的 FSSIZE 控制。 您会看到本实验存储库中的 FSSIZE 设置为 200,000 个块。 您应该在 make 输出中看到 mkfs/mkfs 的以下输出:
      nmeta 70 (boot, super, log blocks 30 inode blocks 13, bitmap blocks 25) blocks 199930 total 200000
      这一行描述了 mkfs/mkfs 构建的文件系统:它有 70 个元数据块(用于描述文件系统的块)和 199,930 个数据块,共计 200,000 个块。
      如果在实验期间的任何时候您发现自己必须从头开始重建文件系统,您可以运行 make clean 来强制 make 重建 fs.img

      +

      可以看到,我们上面就是做了强制重构fs.img。】

      -

      也就是说每次检查到一个,就需要手动清除掉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);
      }
      +

      我想来想去不知道这个错到底怎么爆的,看了下ilock()对应报错点:

      +
      // in fs.c ilock()
      if(ip->valid == 0){
      //printf("ilock begin.\n");
      bp = bread(ip->dev, IBLOCK(ip->inum, sb));
      dip = (struct dinode*)bp->data + ip->inum%IPB;
      ip->type = dip->type;
      // ...
      ip->size = dip->size;
      memmove(ip->addrs, dip->addrs, sizeof(ip->addrs));
      brelse(bp);
      ip->valid = 1;
      if(ip->type == 0){
      //print_buf();
      printf("bp->blockno = %d, bp->refcnt = %d\n",bp->blockno,bp->refcnt);
      panic("ilock: no type");
      }
      }
      -
      // 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;
      }
      +

      可知大概就是,ip的type为0这个非法数值就报错了,而ip的type来源于dip,dip又指向了bp的data,bp也就是我们在bio.c一直在打交道的buf结构体。所以说,其实问题是出在了buf上,我们的bread返回的是一个错误的buf。

      +

      那么,究竟是buf的哪里出错了呢?这个问题想了我很久很久很久,依然没想出来。我一直认为是我的hashtable+双向链表这个数据结构哪里写错了,反反复复看了三四遍,其他地方的逻辑也反反复复研究了好几遍,依然没有结论。当然此过程也抓出了很多bug,但抓完bug后报错仍在,非常坚挺。

      +

      快要放弃的时候,我发现了错误。这很大一部分归功于我用于调试的这个函数:

      +
      // 打印出hashtable的所有结点
      void
      print_buf(){
      printf("**********************\n");
      printf("cnt = %d,dec = %d\n",cnt,dec);
      for(int i=0;i<NBUCKET;i++){
      int should = !holding(&(bcache.dev_locks[i]));
      if(should)
      acquire(&(bcache.dev_locks[i]));
      printf("--------------\n");
      int tmp_cnt = 0;
      struct buf* b;
      for(b = bcache.dev_heads[i].next; b != &(bcache.dev_heads[i]); b = b->next){
      //for(b = bcache.dev_heads[i].prev; b != &(bcache.dev_heads[i]); b = b->prev){
      tmp_cnt++;
      printf("b.refcnt = %d,b.dev = %d,b.blockno = %d\n",b->refcnt,b->dev,b->blockno);
      }
      printf("%d:total:%d\n",i,tmp_cnt);
      printf("--------------\n");
      if(should)
      release(&(bcache.dev_locks[i]));
      }
      }
      -

      A kernel page table per process

      -

      The goal of this section and the next is to allow the kernel to directly dereference user pointers.

      -
      -
      -

      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.

      -
      -

      感想

      这个其实平心而论不难,思路很简单。写着不难是不难,但想明白花费了我很多时间。

      -

      它这个要求我们修改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();
      +

      我在ilock()的panic前面调用了这个函数,并且打印了出问题的buf的blockno:

      +
      if(ip->type == 0){
      print_buf();
      printf("bp->blockno = %d, bp->refcnt = %d\n",bp->blockno,bp->refcnt);
      panic("ilock: no type");
      }
      + +

      image-20230123174332113

      +

      可以看到,出问题的这里blockno=33,而在桶7中,首先有两个blockno==33的结点,这已经违反了不变性条件;其次有一个refcnt==1的结点,那个是所需结点,但我们却没有找到那个结点,反而去新申请了一个结点。这显然非常地古怪。

      +

      于是随后,我就在bio.cbget()中添加了这么几句话:

      +

      image-20230123174620818

      +

      最终结果是会打印出两个blockno==0的结点,但是blockno==33的结点没有访问到。

      +

      这就很奇怪了。print_buf中以及bget的这个地方,都是遍历hashtable的某个双向链表,但是,为什么print_buf可以访问到,但是bget不行呢?

      +

      我首先对比出来的,是print_buf是逆序遍历,而bget是顺序遍历,所以我就又猜想是因为我的数据结构写错了,然而又看了一遍发现并不是。

      +

      这时候,可能我的视力恢复了吧,我猛然发现::

      +

      image-20230123174921100

      +

      我超,这里是不是应该用hash。。。。。改完这处之后,果然就非常顺利地pass了所有测试【悲】

      +

      可以看到伏笔回收了。我是在旧思路代码基础上改过来的。旧思路代码是用dev作为index的,这个for循环忘记改了。因而,就这样,就这么寄了,看了我三四个小时【悲】

      +

      不过这倒是可以解释得通所有的错误了。之所以ilock中buf出错,没有正确找到已经映射在cache中的buf而是自己新建了一个,是因为,我压根就没有在正确的桶里找,而是在别的桶中找,这样自然就找不到了,就会自己新建一个,然后就寄了。

      +

      这个故事告诉我们,还是得谨慎写代码()以及,我在旧代码基础上改的时候,其实可以用更聪明的替换方法:修改dev的变量名为hash->把参数里的dev变量名改为dev。这样就不会出错了。很遗憾,我并没有想到,只是很急很急地手动一个个改了,之后也没有检查,才发生如此错误。忏悔。

      +

      本次bug虽然很sb,但确实让我在debug过程中收获了些许,至少毅力变强了()途中无数次想要放弃,还好我坚持了下来,才能看到如此感动的OK一片:

      +

      image-20230123170805666

      +

      代码

      +

      之后写学校实验时回过头来看,发现之前的实现是不对的,在同时进入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");
      }
      -
      // 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;
      }
      +
      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);
      }
      -

      这样会死得很惨,爆出如下panic:

      -

      image-20230114011100370

      -

      通过hints的调试贴士

      +]]> + + + Interrupts and device drivers + /2023/01/10/xv6$chap5/ + Interrupts and device drivers
      +

      A driver is the code in an operating system that manages a particular device:

      +
        +
      1. configures the device hardware
      2. +
      3. tells the device to perform operations
      4. +
      5. handles the resulting interrupts
      6. +
      7. interacts with processes that may be waiting for I/O from the device
      8. +
      +

      Driver code can be tricky because a driver executes concurrently with the device that it manages.

      +

      In addition, the driver must understand the device’s hardware interface, which can be complex and poorly documented.

      +
      +

      如果devices需要让操作系统对某些事情做出响应,就要采取中断的方法。在kerneltrap中,内核响应中断,并且根据设备类型来决定中断处理函数。

      -

      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.

      +

      image-20230115160523827

      +

      这段对设备中断的概述总结得非常到位

      +

      也就是说,一个device driver可以分为两部分实现,一部分是接收请求,然后开启read/write;另一部分是接收中断,这个中断有可能是设备完成IO,也可能是设备需要IO,它会通知设备具体怎么做,它也会唤醒恰当的进程。

      -

      我发现程序在这里绷掉了:

      -
      p->kpgtbl = (pagetable_t) kalloc();
      - -

      而且显而易见,是系统启动时崩的。

      -

      经过了漫长的思考,我震惊地发现了它为什么崩了()

      -

      首先,这段代码语法上是没有问题的。它固然犯了发布未初始化完成的对象这样的并发错误【我有罪】,也破坏了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;
      }
      - -

      我一路顺着os启动的路径找,也想不出来这能有什么错,因而非常迷茫。

      -

      此时我灵光一闪,会不会是myproc()在os刚启动的时候是发挥不了作用的?于是我一路顺着myproc的代码看下去:

      -
      struct proc*
      myproc(void) {
      push_off();
      struct cpu *c = mycpu();
      struct proc *p = c->proc;
      pop_off();
      return p;
      }
      - -

      那么,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();
      }
      +

      Code: Console input

      console driver是driver structure的一个实现案例。

      +

      上层逻辑

      shell获取用户输入console的信息是通过系统调用read()实现的。read通过文件描述符,最终转向consoleread()来实现具体的逻辑。

      +
      // in file.c fileread()
      } 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);// 在这里转向console
      }
      // in console.c consoleinit()
      void
      consoleinit(void)
      {
      initlock(&cons.lock, "cons");

      uartinit();

      // 在这里完成devsw的初始化
      // connect read and write system calls
      // to consoleread and consolewrite.
      devsw[CONSOLE].read = consoleread;
      devsw[CONSOLE].write = consolewrite;
      }
      -

      创建完进程后,就进入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;
      // ...
      +

      对console的读写事实上是对cons结构体里buf的读写。这个buf则是由底层逻辑管理的。consoleread()每次读取buf中的一行,当未读满一行且无字符输入时会阻塞,直到底层逻辑将字符放入buf。读满了一行后,consoleread将该行copy进用户空间,随后返回read

      +
      // in kernel/console.c 

      #define INPUT_BUF_SIZE 128
      struct {
      struct spinlock lock;
      char buf[INPUT_BUF_SIZE];
      uint r; // Read index
      uint w; // Write index
      uint e; // Edit index
      } cons;


      // user read()s from the console go here.
      // copy (up to) a whole input line to dst.
      // user_dist indicates whether dst is a user
      // or kernel address.
      int
      consoleread(int user_dst, uint64 dst, int n)
      {
      uint target;
      int c;
      char cbuf;

      target = n;
      acquire(&cons.lock);
      while(n > 0){
      // wait until interrupt handler has put some
      // input into cons.buffer.
      // read和write的index一样,说明此时没有数据输入,阻塞
      while(cons.r == cons.w){
      if(killed(myproc())){
      release(&cons.lock);
      return -1;
      }
      sleep(&cons.r, &cons.lock);
      }
      // 产生数据输入,接收数据
      c = cons.buf[cons.r++ % INPUT_BUF_SIZE];

      if(c == C('D')){ // end-of-file
      if(n < target){
      // Save ^D for next time, to make sure
      // caller gets a 0-byte result.
      // 这样下一次也能访问到eof
      cons.r--;
      }
      break;
      }

      // copy the input byte to the user-space buffer.
      cbuf = c;
      if(either_copyout(user_dst, dst, &cbuf, 1) == -1)
      break;

      dst++;
      --n;

      if(c == '\n'){
      // a whole line has arrived, return to
      // the user-level read().
      break;
      }
      }
      release(&cons.lock);

      return target - n;
      }
      -

      因而,c->proc是在创建进程的第一次调度后初始化的,也即,myproc只有在执行第一次scheduler之后才可以调用。而!!!

      -

      当执行调度前的userinit时:

      -
      void
      userinit(void)
      {
      struct proc *p;

      p = allocproc();
      initproc = p;
      +

      底层逻辑

      底层逻辑维护了与上层逻辑交互的buf。

      +

      console接收数据对buf的读,是通过中断来实现的。

      +

      当用户输入字符,UART硬件检测到读,会向操作系统发送中断。中断在kerneltrap()中被接收处理,然后通过devintr()对该中断分门别类地进行转发。console的转发路径为devintr->uartintr->consoleintr。

      +

      UART

      UART的全称是Universal Asynchronous Receiver and Transmitter,即异步发送和接收。它的软件上的表示形式是a set of memory-mapped control registers。CPU通过物理地址与这些寄存器交互,也即它们跟RAM是同一个地址空间。在xv6中,UART的地址空间从UART0(0x1000 0000)开始。这些寄存器地址关于UART0的偏移量定义如下:

      +
      // the UART control registers.
      // some have different meanings for read vs write.
      // see http://byterunner.com/16550.html
      #define RHR 0 // 接收寄存器receive holding register (for input bytes)
      #define THR 0 // 发送寄存器transmit holding register (for output bytes)
      #define IER 1 // 开关中断寄存器
      #define IER_RX_ENABLE (1<<0) // 如果该位被设置,则在接收寄存器有数据,即想向外界发送数据时,UART会搓出一个中断
      #define IER_TX_ENABLE (1<<1) // 如果该位被设置,则在发送寄存器有数据,即外界向硬件发送数据时,UART会搓出一个中断
      #define FCR 2 // FIFO control register
      #define FCR_FIFO_ENABLE (1<<0)
      #define FCR_FIFO_CLEAR (3<<1) // clear the content of the two FIFOs
      // ...
      #define LCR 3 // line control register
      // ...
      #define LSR 5 // line status register
      #define LSR_RX_READY (1<<0) // input is waiting to be read from RHR
      #define LSR_TX_IDLE (1<<5) // THR can accept another character to send
      -

      它进行了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();
      }
      +
      +

      image-20230115170107044

      +

      例如,LSR寄存器包含指示输入字符是否正在等待软件读取的位。这些字符(如果有的话)可用于从RHR寄存器读取。每次读取一个字符,UART硬件都会从等待字符的内部FIFO寄存器中删除它,并在FIFO为空时清除LSR中的“就绪”位。UART传输硬件在很大程度上独立于接收硬件;如果软件向THR写入一个字节,则UART传输该字节。

      +
      +

      kerneltrap

      // in kerneltrap()
      // 在此处的devintr对不同的设备进行不同的处理方式
      if((which_dev = devintr()) == 0){
      printf("scause %p\n", scause);
      printf("sepc=%p stval=%p\n", r_sepc(), r_stval());
      panic("kerneltrap");
      }
      -

      当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);
      +

      devintr

      devintr处在trap.c中,作用是对中断归类,然后分门别类地转发到下一层级的handler。

      +
      +

      注:

      +
        +
      1. 外中断和内中断

        +

        外部中断和内部中断详解

        +

        根据中断源的不同,可以把中断分为硬件中断和软件中断两大类,而硬件中断又可以分为外部中断和内部中断两类。

        +

        外部中断一般是指由计算机外设发出的中断请求,如:键盘中断、打印机中断、定时器中断等。外部中断是可以屏蔽的中断,也就是说,利用中断控制器可以屏蔽这些外部设备的中断请求。

        +

        内部中断是指因硬件出错(如突然掉电、奇偶校验错等)或运算出错(除数为零、运算溢出、单步中断等)所引起的中断。内部中断是不可屏蔽的中断。

        +

        软件中断其实并不是真正的中断,它们只是可被调用执行的一般程序。例如:ROM BIOS中的各种外部设备管理中断服务程序(键盘管理中断、显示器管理中断、打印机管理 中断等,)以及DOS的系统功能调用(INT 21H)等都是软件中断。【比如说系统调用之类的】

        +
      2. +
      +
      +
      // check if it's an external interrupt or software interrupt,
      // and handle it.
      // returns 2 if timer interrupt,
      // 1 if other device,
      // 0 if not recognized.
      int
      devintr()
      {
      // 获取scause,辨析中断类型
      uint64 scause = r_scause();

      // 如果来自外中断(在这里应该只指device interrupt)
      if((scause & 0x8000000000000000L) &&
      (scause & 0xff) == 9){
      // this is a supervisor external interrupt, via PLIC.

      // irq indicates which device interrupted.
      // 通过PLIC硬件获取中断设备信息
      int irq = plic_claim();

      // 分别转发
      if(irq == UART0_IRQ){
      uartintr();
      } else if(irq == VIRTIO0_IRQ){
      virtio_disk_intr();
      } else if(irq){
      printf("unexpected interrupt irq=%d\n", irq);
      }

      // 中断处理完成了,可以再次开启中断
      // the PLIC allows each device to raise at most one
      // interrupt at a time; tell the PLIC the device is
      // now allowed to interrupt again.
      if(irq)
      plic_complete(irq);

      return 1;
      // 来自时钟中断
      } else if(scause == 0x8000000000000001L){
      // ...
      return 2;
      } else {
      return 0;
      }
      }
      -

      但是这样还没有结束。因为我们除了得更换目前的页表,还得更换trapframe中的内核页表相关的东西:

      -
      struct trapframe {
      /* 0 */ uint64 kernel_satp; // kernel page table
      /* 8 */ uint64 kernel_sp; // top of process's kernel stack
      }
      +

      uartintr

      这代码其实乍一看是看不懂的,这是因为uartintr不止负责读中断。它还负责另一个中断(发送区空余中断),下面会细说。

      +
      // handle a uart interrupt, raised because input has
      // arrived, or the uart is ready for more output, or
      // both. called from devintr().
      void
      uartintr(void)
      {
      // read and process incoming characters.
      while(1){
      int c = uartgetc();
      // return -1 if none is waiting,说明读完了
      if(c == -1)
      break;
      // 每读入一个字符就转交给console
      consoleintr(c);
      }

      // send buffered characters.
      acquire(&uart_tx_lock);
      uartstart();
      release(&uart_tx_lock);
      }
      -

      为啥还要更换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
      +

      consoleintr

      向buf中放入字符c

      +
      void
      consoleintr(int c)
      {
      acquire(&cons.lock);

      switch(c){
      case C('P'): // Print process list.
      // ...一堆特殊情况处理...
      default:
      if(c != 0 && cons.e-cons.r < INPUT_BUF_SIZE){
      c = (c == '\r') ? '\n' : c;

      // echo back to the user.
      consputc(c);

      // store for consumption by consoleread().
      cons.buf[cons.e++ % INPUT_BUF_SIZE] = c;

      if(c == '\n' || c == C('D') || cons.e-cons.r == INPUT_BUF_SIZE){
      // wake up consoleread() if a whole line (or end-of-file)
      // has arrived.
      // 中断处理并不会做很多事情,只是会与缓冲区交互
      // 涉及到复杂的事情,比如说将数据拷贝到用户空间
      //就唤醒上层逻辑来做
      cons.w = cons.e;
      wakeup(&cons.r);
      }
      }
      break;
      }

      release(&cons.lock);
      }
      -

      所以,为了以后系统调用能顺利自发进行,我们需要把栈帧也一起换掉。怎么换呢?我们是否还要在一些地方人工把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
      +

      Code: Console output

      外部通过write这个系统调用来对console写。

      +

      uartputc

      最先到达这里。

      +

      uart内置了一个缓冲区。

      +
      char uart_tx_buf[UART_TX_BUF_SIZE];
      -

      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);
      }
      +

      用户仅需通过uartputc对buf进行写入即可,具体的buf数据向UART转移由uartputc通过调用uartstart实现。

      +
      // add a character to the output buffer and tell the
      // UART to start sending if it isn't already.
      // blocks if the output buffer is full.缓冲区满则阻塞
      // because it may block, it can't be called
      // from interrupts; it's only suitable for use
      // by write().这段话很有意思,说它由于会阻塞所以最好别在中断的时候用。
      void
      uartputc(int c)
      {
      acquire(&uart_tx_lock);

      if(panicked){
      for(;;)
      ;
      }
      // 阻塞
      while(uart_tx_w == uart_tx_r + UART_TX_BUF_SIZE){
      // buffer is full.
      // wait for uartstart() to open up space in the buffer.
      sleep(&uart_tx_r, &uart_tx_lock);
      }
      uart_tx_buf[uart_tx_w % UART_TX_BUF_SIZE] = c;
      uart_tx_w += 1;
      uartstart();
      release(&uart_tx_lock);
      }
      -

      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;
      +

      uartstart

      uartstart的作用是从缓冲区取数据向UART硬件发送。不阻塞。

      +
      // if the UART is idle, and a character is waiting
      // in the transmit buffer, send it.
      // caller must hold uart_tx_lock.
      // called from both the top- and bottom-half.
      void
      uartstart()
      {
      while(1){
      if(uart_tx_w == uart_tx_r){
      // transmit buffer is empty.
      return;
      }

      if((ReadReg(LSR) & LSR_TX_IDLE) == 0){
      // the UART transmit holding register is full,
      // so we cannot give it another byte.
      // it will interrupt when it's ready for a new byte.
      // 当缓冲区满没有选择阻塞,而是先结束
      // 当UART硬件准备好继续接收的时候,UART会发送transmit complete中断,到时候会再继续从buf读取
      return;
      }

      // 一个字符一个字符写
      int c = uart_tx_buf[uart_tx_r % UART_TX_BUF_SIZE];
      uart_tx_r += 1;

      // maybe uartputc() is waiting for space in the buffer.
      wakeup(&uart_tx_r);

      WriteReg(THR, c);
      }
      }
      -
      // 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;
      }
      +

      当传输过程非常流畅,UART硬件没有阻塞时,以上的代码就能完美阐述发送的过程。但是当UART硬件的transmit阻塞时,过程就会有许多改动。

      +

      transmit complete interrupt

      uartstart中,当UART硬件的transmit满,uartstart就直接return了。

      +

      当UART硬件的transmit空,就会发送transmit complete中断。中断在kerneltrap被接收,经过devintr转发,最终来到了uartintr:

      +
      // handle a uart interrupt, raised because input has
      // arrived, or the uart is ready for more output, or
      // both. called from devintr().
      void
      uartintr(void)
      {
      // read and process incoming characters.
      while(1){
      int c = uartgetc();
      if(c == -1)
      break;
      consoleintr(c);
      }

      // send buffered characters.
      acquire(&uart_tx_lock);
      uartstart();
      release(&uart_tx_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");
      }
      +

      此时,第一个while循环会直接退出,因为压根没有get到字符。所以,这时候,就会去执行uartstart,然后继续读未完成读取的缓冲区。

      +

      等到所有都读完了,最后一次发送transmit complete中断时,会在uartstart进入该分支:

      +
      if(uart_tx_w == uart_tx_r){
      // transmit buffer is empty.
      return;
      }
      -
      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");
      }
      +

      然后就不会再发送transmit中断了。

      +

      感觉这点是真的牛逼。uartintr这个函数完美兼顾了两种情况【这也归功于uartstart做得很健壮】:1. 外部输入数据到console,2. 接收数据未结束,继续接收

      +

      Concurrency in drivers

      用户进程与设备之间的读写交流,比如说上面的console,重点依靠于uart_tx_bufcons.buf这两个的正确性。因而,就需要保障它们的并发安全。在上面的代码中,使用到这两个的地方都被锁保护着。

      +

      在kernel中还需要格外注意的一点并发是,一个进程A在等待来自设备的中断,但此时另一个进程B在运行。这时候设备发出中断信号,CPU转入中断处理程序处理中断。此时,中断处理程序的执行不应该涉及到当前被中断进程的代码。例如,中断处理程序不能安全地使用当前进程的页表调用copyout(页表正是跟当前进程息息相关的)。中断处理程序通常做相对较少的工作(例如,只需将输入数据复制到缓冲区),并唤醒上半部分代码来完成其余工作。

      +

      Timer interrupts

      +

      Xv6 uses timer interrupts to maintain its clock and to enable it to switch among compute-bound processes; the yield calls in usertrap and kerneltrap cause this switching.

      +
      +
      // in kernel/trap.c usertrap()
      // give up the CPU if this is a timer interrupt.
      if(which_dev == 2)
      yield();
      -
      修改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;
      }
      +
      // in kernel/trap.c kerneltrap()
      // give up the CPU if this is a timer interrupt.
      if(which_dev == 2 && myproc() != 0 && myproc()->state == RUNNING)
      yield();
      -
      释放
      // in kernel.proc.c freeproc()
      if(p->kpgtbl)
      proc_freekpgtbl(p->kpgtbl,p->kstack);
      p->kpgtbl = 0;
      +
      +

      RISC-V requires that timer interrupts be taken in machine mode, not supervisor mode. As a result, xv6 handles timer interrupts completely separately from the trap mechanism laid out above.

      +
      +

      xv6启动时调用过start.cstart.c处于机器态,并准备向内核态过渡。start.c中就对时钟进行了初始化timeinit()。要做的有以下几件事:

      +
        +
      1. program the CLINT hardware (core-local interruptor) to generate an interrupt after a certain delay.
      2. +
      3. set up a scratch area to help the timer interrupt handler save registers and the address of the CLINT registers
      4. +
      5. start sets mtvec to timervec and enables timer interrupts.
      6. +
      +
      // arrange to receive timer interrupts.
      // they will arrive in machine mode at
      // at timervec in kernelvec.S,
      // which turns them into software interrupts for
      // devintr() in trap.c.
      void
      timerinit()
      {
      // each CPU has a separate source of timer interrupts.
      int id = r_mhartid();

      // ask the CLINT for a timer interrupt.
      int interval = 1000000; // cycles; about 1/10th second in qemu.
      *(uint64*)CLINT_MTIMECMP(id) = *(uint64*)CLINT_MTIME + interval;

      // prepare information in scratch[] for timervec.
      // scratch[0..2] : space for timervec to save registers.
      // scratch[3] : address of CLINT MTIMECMP register.
      // scratch[4] : desired interval (in cycles) between timer interrupts.
      uint64 *scratch = &timer_scratch[id][0];
      scratch[3] = CLINT_MTIMECMP(id);
      scratch[4] = interval;
      w_mscratch((uint64)scratch);

      // set the machine-mode trap handler.
      w_mtvec((uint64)timervec);

      // enable machine-mode interrupts.
      w_mstatus(r_mstatus() | MSTATUS_MIE);

      // enable machine-mode timer interrupts.
      w_mie(r_mie() | MIE_MTIE);
      }
      -
      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);
      }
      +
      +

      计时器中断处理程序必须保证不干扰中断的内核代码。基本策略是处理程序要求RISC-V发出“软件中断”并立即返回。RISC-V用普通陷阱机制将软件中断传递给内核,并允许内核禁用它们。处理由定时器中断产生的软件中断的代码可以在devintr (kernel/trap.c:204)中看到:

      +
      +
      // in kernel/trap.c devintr()
      } else if(scause == 0x8000000000000001L){
      // software interrupt from a machine-mode timer interrupt,
      // forwarded by timervec in kernelvec.S.

      // 只看其中一个CPU的时钟中断计数的意思吗?确实,要是好几个一起来加倍了非常不合理
      if(cpuid() == 0){
      clockintr();
      }

      // acknowledge the software interrupt by clearing
      // the SSIP bit in sip.
      w_sip(r_sip() & ~2);

      return 2;
      }

      void
      clockintr()
      {
      acquire(&tickslock);
      ticks++;
      wakeup(&ticks);
      release(&tickslock);
      }
      -

      Simplify copyin/copyinstr

      -

      参考:

      -

      6.S081学习记录-lab3

      +

      注意,w_sip(r_sip() & ~2);就对应着“RISC-V用普通陷阱机制将软件中断传递给内核”。【应该吧个人理解】

      +
      +

      来源:rCore 手册(rCore tutorial doc)

      +

      riscv 中的中断寄存器

      +

      S 态的中断寄存器主要有 sie(Supervisor Interrupt Enable,监管中断使能), sip (Supervisor Interrupt Pending,监管中断待处理)两个,其中 s 表示 S 态,i 表示中断, e/p 表示 enable (使能)/ pending (提交申请)。 处理的中断分为三种:

      +
        +
      1. SI(Software Interrupt),软件中断
      2. +
      3. TI(Timer Interrupt),时钟中断
      4. +
      5. EI(External Interrupt),外部中断
      6. +
      +

      比如 sie 有一个 STIE 位, 对应 sip 有一个 STIP 位,与时钟中断 TI 有关。当硬件决定触发时钟中断时,会将 STIP 设置为 1,当一条指令执行完毕后,如果发现 STIP 为 1,此时如果时钟中断使能,即 sieSTIE 位也为 1 ,就会进入 S 态时钟中断的处理程序。

      +

      可能SSIP跟这里的STIP差不多吧,都是时钟中断的标志。如果把SSIP clear掉,那么则说明不是时钟中断了,而是软中断了。

      +
      +

      Real world

      +

      UART驱动程序读取UART控制寄存器,一次检索一字节的数据;因为软件驱动数据移动,这种模式被称为程序I/O(Programmed I/O)。程序I/O很简单,但速度太慢,无法在高数据速率下使用。需要高速移动大量数据的设备通常使用直接内存访问(DMA)。DMA设备硬件直接将传入数据写入内存,并从内存中读取传出数据。现代磁盘和网络设备使用DMA。DMA设备的驱动程序将在RAM中准备数据,然后使用对控制寄存器的单次写入来告诉设备处理准备好的数据。

      +

      当一个设备在不可预知的时间需要注意时,中断是有意义的,而且不是太频繁。但是中断有很高的CPU开销。因此,如网络和磁盘控制器的高速设备,使用一些技巧减少中断需求。一个技巧是对整批传入或传出的请求发出单个中断。另一个技巧是驱动程序完全禁用中断,并定期检查设备是否需要注意。这种技术被称为轮询(polling)。如果设备执行操作非常快,轮询是有意义的,但是如果设备大部分空闲,轮询会浪费CPU时间。一些驱动程序根据当前设备负载在轮询和中断之间动态切换。

      +

      UART驱动程序首先将传入的数据复制到内核中的缓冲区,然后复制到用户空间。这在低数据速率下是可行的,但是这种双重复制会显著降低快速生成或消耗数据的设备的性能。一些操作系统能够直接在用户空间缓冲区和设备硬件之间移动数据,通常带有DMA。

      +
      +

      Lab: networking

      +

      In this lab you will write an xv6 device driver for a network interface card (NIC).

      +

      这个概述光是听起来就让人觉得热血沸腾。网络的本质其实就是IO设备,这一点我一直觉得很牛逼,而现在我居然要亲手实现网络……That’s very cool.

      -

      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.

      +

      On this emulated LAN, xv6 (the “guest”) has an IP address of 10.0.2.15.

      +

      Qemu also arranges for the computer running qemu to appear on the LAN with IP address 10.0.2.2.

      +

      When xv6 uses the E1000 to send a packet to 10.0.2.2, qemu delivers the packet to the appropriate application on the (real) computer on which you’re running qemu (the “host”).

      -

      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.

      +

      We’ve added some files to the xv6 repository for this lab.

      +

      The file kernel/e1000.c contains initialization code for the E1000 as well as empty functions for transmitting and receiving packets, which you’ll fill in.

      +

      kernel/e1000_dev.h contains definitions for registers and flag bits defined by the E1000 and described in the Intel E1000 Software Developer’s Manual.

      +

      kernel/net.c and kernel/net.h contain a simple network stack that implements the IP, UDP, and ARP protocols.

      +

      These files also contain code for a flexible data structure to hold packets, called an mbuf.

      +

      Finally, kernel/pci.c contains code that searches for an E1000 card on the PCI bus when xv6 boots.

      -

      感想

      这题很直观的思路是,在每个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

      -

      Xv6 applications ask the kernel for heap memory using the sbrk() system call.

      +

      Your job:

      +

      Your job is to complete e1000_transmit() and e1000_recv(), both in kernel/e1000.c, so that the driver can transmit and receive packets. You are done when make grade says your solution passes all the tests.

      -

      很悲伤,我的初见思路是错误的()

      -

      而这三个地方的共同点,就是都会对页表进行大量的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
      - -

      所以,我们要做的事情很简单:写一个坐收渔翁之利的函数,内容为把一个页表的所有内容复制到另一个页表。然后再在这几个地方调用这个函数即可。

      -

      代码

      -

      注意:由于我写得实在是太烦了,已经思考不下去了。为了放过我自己,我写了个虽然能过得去测试但是其实毛病重重的代码。垃圾点为以下几点:

      +

      感想

      说实话,一开始看题的时候真是感觉非常地哈人……但其实文档看着看着,心中也逐渐有了个大概,最后再结合下指导书的提示【当然不是后面那些保姆级的Hints】,最后写的也就八九不离十了。总体上来说,我觉得这次实验的代码还是很简单的,它主要难在探究过程,也就是从一开始什么也不懂,然后去阅读硬件设备的文档,结合代码尝试去理解,最后一步步写出来的过程。本次实验耗时六小时,我觉得肯定有不少于一半,甚至可能达到2/3的时间都耗费在理解上。这种从零开始探究的过程给了我很大的收获,同时也稍微提高了我面对挫折的能力。

      +

      这个实验确实设计得很有教育意义。除了我上面说的它锻炼了我的能力以外,它其实还具有比较深刻的工业意义。在看书的时候,书中这么写道:

      +
      +

      In addition, the driver must understand the device’s hardware interface, which can be complex and poorly documented.

      +
      +

      本次实验正是上述描述的简化版:E1000的文档很详细,并且我们只用掌握一部分它的功能就行了。但虽然简化了,其探究过程的内在逻辑还是不会改变的。

      +

      总之,我很喜欢这次实验的设计。我的评价是牛逼。

      +

      思路

      正确思路

      Hints写得很详细,不做赘述了。主要就是明确一下数据结构的问题:

        -
      1. 需要去掉freewalk中的panic

        -

        我的kvmcopy的实现是,user pagetable(下面简称up)和tp的相同虚拟地址共用同一页物理内存。也就是说,页表不一样,但所指向的物理内存是同一个。这样设计的目的是为了能够让tp及时用到up的更新后的数据。

        -

        这会导致啥呢?在进程释放时,需要一起调用proc_freepagetableproc_freekpgtblproc_freepagetable调用完后,所指向的那堆物理内存已经寄完了,如果再调用proc_freekpgtbl,显然,就会发生页表未释放但页表对应内存已经释放的问题,freewalk就会panic。因此,我简单粗暴地直接把freewalk的panic删掉了【抖】也许有别的解决方法,但我真是烦得不想想了放过我吧(

        +
      2. rx_ring和tx_ring是两个分开的队列

        +

        它们只是结构一模一样,都是阴影部分表示software持有,白色部分表示硬件持有。

        +

        因而,对于rx来说,白色部分表示需要传给协议栈的包,因而我们需要把白色部分转化为阴影部分;对于tx来说,白色部分表示网卡将要发送的包,因而我们需要把阴影部分转化为白色部分。

        +

        image-20230220234406239

      3. -
      4. 好像暂时没有第二点了()

        +
      5. rx_mbufs和tx_mbufs

        +

        一开始不知道这俩是啥,后来才意识到,这俩和第1点的那俩其实是下标一一对应的关系。也就是说rx_ring[i]这个descriptor接收到的数据存在rx_mbufs[i],tx_ring[i]要发送的数据存在tx_mbufs[i]。知道了这个之后,代码就简单了。

        +
        +

        忏悔:我一开始真没反应过来。计网我记得是有一模一样的结构的,看来算是白做了2333

        +
      +

      个人的推理过程

      一开始就先懵懵懂懂地看指导书,直到看到这句话:

      +
      +

      Browse the E1000 Software Developer’s Manual.

      -
      渔翁之利函数
      // 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;
      }
      - -
      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);
      - -
      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;
      }
      - -
      // 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;
      }
      +

      然后我这时连自己要干什么都迷迷糊糊,但姑且还是按他下面说的,准备先浏览第二章了。然而,我发现要我看我也还是看不懂啊,所以我就直接放弃了。【经验1:看不懂就算了,别死磕了

      +

      我放弃了第二章后,就再次从头开始细细看了一遍这句话之前的指导书,也结合了一下它给的代码。这次总算是差不多弄懂这次要做什么了:

      +

      实现driver的两个函数,从而实现对网卡进行数据的取出和送入。数据是eth frame。数据取出后要通过net_rx传递给上层协议栈。数据是mbuf类型的。

      +

      所以我们只需实现协议栈最底下的部分,也即从网卡读写数据,其他一些别的东西比如协议栈什么的都已经写好了。

      +

      但是那些什么rx_ring,还有各种奇奇怪怪的寄存器,我都看不懂,所以我就去看第三章了。初次略过一遍感觉还是一脸懵逼不知道干什么,但我带着“我们要做的是driver”这样的想法,在第二遍细看的时候有意区分开什么是网卡硬件帮我们做的,什么是我们的driver软件需要做的(经验2:明确要做什么。我们需要做的是软件部分,它的文档一般会说Software should XXX,密切关注这部分就行),就差不多有了点实现的雏形:

      +
      for recv:
      // 通过net_rx,网络包可以发送到udp顶层.
      // 所以说,我们在这里的目的就是,通过与硬件网卡e1000进行交互,
      // 取出e1000所接收到的数据包,检查数据的完整性,然后再把数据封装进mbuf结构体中,再通过net_rx传到上层

      // 取出数据包
      // 数据包存储在网卡的缓冲区中
      // 一是获取网卡缓冲区长度的长度
      // 网卡缓冲区长度存储在RCTL.BSIZE & RCTL.BSEX中
      /*
      *RCTL.BSEX = 0b:
      00b = 2048 Bytes.
      01b = 1024 Bytes.
      10b = 512 Bytes.
      1b1 = 256 Bytes.
      RCTL.BSEX = 1b:
      00b = Reserved; software should not program this value.
      01b = 16384 Bytes.
      10b = 8192 Bytes.
      11b = 4096 Bytes
      *
      * */
      // 二是获取数据包存放在哪个地址
      // 数据包的buffer cache的地址存储在descriptor的字段中
      // 必须读取多个descriptor以确定跨越多个接收缓冲区的数据包的完整长度。
      // 那么我们要读取的这些descriptor存放在哪呢?
      // 看文档,似乎差不多意思是这些descriptor被以环形队列的形式组织在一起,也许正是
      // 本文件内的rx_ring这个数组。
      // 当有descriptor到达e1000,e1000就会把它从host memory中取出来,存入到descriptor ring
      // 也即我们rx_ring数组
      //
      // 所以我们要做的,就是遍历rx_ring数组,如果rx_ring数组中的元素是used的,那么表明它就是数据包的一部分
      // 也即它地址所指向的buf里存放的是数据包的一部分数据
      //
      // 那么我们怎么知道这个rx_ring的元素有没有used,以及它是第几个呢?
      // 检查descriptor有没有used:status字段不为全0则为used
      // 并且硬件要求,我们在发现这个descriptor的status不为0,并且用完这个descriptor之后,需要将
      // 其status字段置零,以供硬件使用
      // Status information indicates whether the descriptor has been used and whether the referenced
      // buffer is the last one for the packet.

      // 三是获取数据包的数据
      // 我们需要获取decriptor的该字段,然后再从这个地址读取数据包数据
      // 网卡和内存统一编址,这个数据实际上就是网卡的buffer
      // 我们应该直接通过read这个系统调用就可以对其进行读写了

      // check数据包
      // 检查RDESC.ERRORS位,如果包发生了错误,再检查,如果发现RCTL.SBP、RCTL.UPE/MPE都被标记,
      // 就接收这个包,否则直接丢弃
      -
      userinit
      -

      这一步不能忽视,因为内核启动的时候就需要用到copyinstr。

      +

      可以看到,跟正确思路虽然很多细节理解上有点问题,但是大体框架还是大差不差。然后再阅读指导书:

      +
      +

      When the E1000 receives each packet from the ethernet, it first DMAs the packet to the mbuf pointed to by the next RX (receive) ring descriptor, and then generates an interrupt. 【这句话可得知,descriptor们存放在代码中的rx_ring中。】

      +

      Your e1000_recv() code must scan the RX ring and deliver each new packet’s mbuf to the network stack (in net.c) by calling net_rx(). You will then need to allocate a new mbuf and place it into the descriptor, so that when the E1000 reaches that point in the RX ring again it finds a fresh buffer into which to DMA a new packet.

      +
      +

      就差不多是正确思路了。transmit的实现也是同理

      +

      代码

      +

      以下代码不知道为什么过不了test,我跟别人的逻辑一模一样也还是不行emmm

      +

      它的问题是,不会接收到外界的返ping,导致进程一直等待网卡IO,所以kerneltrap一直触发不了,无法正常网卡读写,从而导致fileread会一直处于sleep等待状态,整个系统就沉睡了【】我感觉应该是transmit没发成功。

      +

      等以后有精力再来看看吧。

      -
      // 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");
      }
      +
      int
      e1000_transmit(struct mbuf *m)
      {
      acquire(&e1000_lock);
      struct tx_desc tx = tx_ring[regs[E1000_TDT]];
      if((tx.status & 1) == 0){
      release(&e1000_lock);
      return -1;
      }
      if(tx_mbufs[regs[E1000_TDT]] != 0) mbuffree(tx_mbufs[regs[E1000_TDT]]);
      tx.addr = (uint64) m->head;
      tx.length = m->len;
      tx.status |= 1;// EOP
      tx.cmd |= 1;//EOP
      tx.cmd |= 8;//RS
      tx_mbufs[regs[E1000_TDT]] = m;
      regs[E1000_TDT] = (regs[E1000_TDT]+1)%TX_RING_SIZE;
      // printf("send successful!\n");
      release(&e1000_lock);
      return 0;
      }

      static void
      e1000_recv(void)
      {
      printf("go into e1000_recv\n");
      acquire(&e1000_lock);
      while(1){
      //while(regs[E1000_RDT]!=regs[E1000_RDH]){
      printf("go into while\n");
      regs[E1000_RDT] = (regs[E1000_RDT] + 1)%RX_RING_SIZE;
      int i=regs[E1000_RDT];
      if(rx_ring[i].status != 0){
      // 包含所需数据包
      // 检查是否发生了错误
      //if((rx_ring[i].status & 1) !=0 && (rx_ring[i].status & 2) != 0){
      // // error字段有效
      // if(rx_ring[i].errors != 0){
      // 发生错误,直接丢弃
      // goto end;
      // }
      if((rx_ring[i].status & 1) == 0){
      release(&e1000_lock);
      return ;
      }
      // 将地址对应数据包发送
      struct mbuf* m = rx_mbufs[i];
      m->len = rx_ring[i].length;
      net_rx(m);
      rx_ring[i].status = 0;
      struct mbuf* mbuf = mbufalloc(MBUF_DEFAULT_HEADROOM);
      rx_ring[i].addr = (uint64) mbuf->head;
      rx_mbufs[i] = mbuf;
      }
      }

      release(&e1000_lock);
      }
      ]]> - 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/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/ + 记录一次vm扩容

      +

      开发中遇到的链接小问题

      +

      rtt硬件环境搭建

      +

      内核编译

      +]]>
      其他的对实验未涉及的思考 @@ -10659,851 +10573,1140 @@ url访问填写http://localhost/webdemo4_war/*.do
    4. QEMU在初始化过程中会创建好模拟芯片组和必要的模拟设备,包括南北桥芯片、PCI根总线、ISA根总线等总线系统,以及各种PCI设备、ISA设备等。

      外设虚拟化主要有如下几种方式:

        -
      1. 纯软件模拟(完全虚拟化)

        -

        QEMU最早的方案,虚拟机内核不用做任何修改,每一次对设备的寄存器读写都会陷入到KVM,进而到QEMU,QEMU再对这些请求进行处理并模拟硬件行为。

        -

        软件模拟会导致非常多的QEMU/KVM接入,效率低下。

        -
      2. -
      3. virtio设备(半虚拟化)

        -

        virtio设备是一类特殊的设备,并没有对应的物理设备,所以需要虚拟机内部操作系统安装特殊的virtio驱动。

        -

        相比软件模拟,virtio方案提高了虚拟设备的性能。

        +
      4. 纯软件模拟(完全虚拟化)

        +

        QEMU最早的方案,虚拟机内核不用做任何修改,每一次对设备的寄存器读写都会陷入到KVM,进而到QEMU,QEMU再对这些请求进行处理并模拟硬件行为。

        +

        软件模拟会导致非常多的QEMU/KVM接入,效率低下。

        +
      5. +
      6. virtio设备(半虚拟化)

        +

        virtio设备是一类特殊的设备,并没有对应的物理设备,所以需要虚拟机内部操作系统安装特殊的virtio驱动。

        +

        相比软件模拟,virtio方案提高了虚拟设备的性能。

        +
      7. +
      8. 设备直通

        +

        将物理硬件设备直接挂到虚拟机上,虚拟机直接与物理设备交互,尽可能在I/O路径上减少QEMU/KVM的参与。

        +

        设备直通经常搭配硬件虚拟化支持技术SRIOV(Single Root I/O Virtualization,单根输入/输出虚拟化)使用,SRIOV能够将单个的物理硬件高效地虚拟出多个虚拟硬件。

        +
      9. +
      +

      v2-555d017ce5b65457f98617a5fdf232af_1440w

      +
      中断处理虚拟化

      操作系统通过写设备的I/O端口或者MMIO地址来与设备交互,设备通过发送中断来通知操作系统事件。

      +

      QEMU/KVM一方面需要完成这项中断设备的模拟,另一方面需要模拟中断的请求处理

      +
      +

      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。

      +
      +

      xv6的全启动运行过程梳理

      介绍完上述的qemu虚拟化,接下来就可以对xv6的全启动进行一个梳理了。

      +

      首先,在宿主机执行make qemu

      +

      Makefile中可以看到:

      +
      qemu: $K/kernel fs.img
      $(QEMU) $(QEMUOPTS)
      QEMU = qemu-system-riscv64
      + +

      在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

      +
      +

      图中的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;
      }
      + +

      也即初始化为这种情况:

      +

      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;
      }
      + +

      调用morecore

      +

      morecore

      进入morecore后,首先会对堆内存进行扩容:

      +
      if(nu < 4096)
      nu = 4096;
      p = sbrk(nu * sizeof(Header));
      if(p == (char*)-1)
      return 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,故而会跳出循环。

      +
      +

      为什么ap > base呢?

      +

      别忘了我们扩容的原理。我们是以proc->size为起始地址扩容的。ap处在扩容内存中,因而ap>旧size;base处在扩容前内存内,因而base<=旧size。故而有ap>base。

      +
      +
      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的第二个分支,以及第二个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,故而跳出循环。

      +
      +

      此处循环中,循环语句内部的这个循环实际上是对遍历到环形链表尾部,即将从头开始遍历,这个边界情况的处理。比较符合逻辑的还是循环语句内的那个条件。

      +
      +
      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) {
      }
      + +

      那么安装以前所学的内容,我们很容易联系到这玩意应该并不是个文件,而是类似于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);
      }
      + +

      这玩意的文件结点实际上是在创建console时整的,并且其有一个特殊的文件类型“STATS”。我们可以进一步追溯到kernel中的main.c

      +
      void main()
      {
      if(cpuid() == 0){
      #if defined(LAB_PGTBL) || defined(LAB_LOCK)
      statsinit();
      + +
      void
      statsinit(void)
      {
      initlock(&stats.lock, "stats");

      devsw[STATS].read = statsread;
      devsw[STATS].write = statswrite;
      }
      + +

      可以看到,它给这个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;
      }
      + +
      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确实能做到那么多。

      +]]> + + + 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. 设备直通

        -

        将物理硬件设备直接挂到虚拟机上,虚拟机直接与物理设备交互,尽可能在I/O路径上减少QEMU/KVM的参与。

        -

        设备直通经常搭配硬件虚拟化支持技术SRIOV(Single Root I/O Virtualization,单根输入/输出虚拟化)使用,SRIOV能够将单个的物理硬件高效地虚拟出多个虚拟硬件。

        +
      4. 日志空间有限的另一个后果是,除非确定系统调用的写入将可容纳于日志中剩余的空间,否则日志系统无法允许启动系统调用。

      -

      v2-555d017ce5b65457f98617a5fdf232af_1440w

      -
      中断处理虚拟化

      操作系统通过写设备的I/O端口或者MMIO地址来与设备交互,设备通过发送中断来通知操作系统事件。

      -

      QEMU/KVM一方面需要完成这项中断设备的模拟,另一方面需要模拟中断的请求处理

      -
      -

      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。

      -

      xv6的全启动运行过程梳理

      介绍完上述的qemu虚拟化,接下来就可以对xv6的全启动进行一个梳理了。

      -

      首先,在宿主机执行make qemu

      -

      Makefile中可以看到:

      -
      qemu: $K/kernel fs.img
      $(QEMU) $(QEMUOPTS)
      QEMU = qemu-system-riscv64
      +

      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;
      -

      在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
      $
      +

      关键函数

      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;
      }
      }
      }
      -

      具体的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

      -
      -

      图中的boot块就是操作系统的引导扇区。

      +
      log_write
      +

      log_write充当bwrite的代理。它将块的扇区号记录在内存中,在磁盘上的日志中预定一个槽位,并调用bpin将缓存固定在block cache中,以防止block cache将其逐出【具体原理就是让refcnt++,这样就不会被当成空闲block用掉了】。

      +

      为啥要防止换出呢?换出不是就正好自动写入磁盘了吗?这里一是为了保障前面提到的原子性,防止换入换出导致的单一写入磁盘;二是换出自动写入的是磁盘对应位而不一定是日志所在的blocks。

      -

      mkfs的作用,正是把宿主机提供的虚拟地址空间作为虚拟磁盘,把虚拟地址空间划分为如上图所示的地址结构。它是运行在宿主机当中的。有了mkfs,才能有我们的虚拟机。

      -
      代码解读

      xv6分析–mkfs源代码注释

      -

      yysy这个就写得很好了。

      -

      user mem-allocator

      -

      linux的堆管理

      -

      那么malloc到底是怎么实现的呢?不是每次要申请内存就调一下系统调用,而是程序向操作系统申请⼀块适当⼤⼩的堆空间,然后由程序⾃⼰管理这块空间,⽽具体来讲,管理着堆空间分配的往往是程序的运⾏库

      -

      也就是说,malloc本质上是以运行库而非系统调用形式出现的。它里面用到的是sbrk和mmap这两个系统调用来进货。

      -

      glibc的malloc函数是这样处理⽤户的空间请求的:对于⼩于128KB的请求来说,它会在现有的堆空间⾥⾯,按照堆分配算法为它分配⼀块空间并返回;对于⼤于128KB的请求来说,它会使⽤mmap()函数为它分配⼀块匿名空间,然后在这个匿名空间中为⽤户分配空间。

      +
      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);
      }
      + +
      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);
      }
      }
      + +
      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
      }
      }
      + +
      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);
      }
      }
      + +
      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的一次事务提交的流程。接下来介绍它是怎么恢复的。

      +
      +

      recover_from_log是由initlog调用的,而它又是在第一个用户进程运行之前的引导期间由fsinit调用的。

      -

      在内核态中,我们使用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;
      }
      +
      第一个进程运行之前

      由前面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();
      }
      -

      也即初始化为这种情况:

      -

      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;
      }
      +
      fsinit
      // Init fs
      void
      fsinit(int dev) {
      // ...
      initlog(dev, &sb);
      }
      -

      调用morecore

      -

      morecore

      进入morecore后,首先会对堆内存进行扩容:

      -
      if(nu < 4096)
      nu = 4096;
      p = sbrk(nu * sizeof(Header));
      if(p == (char*)-1)
      return 0;
      +
      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();
      }
      -

      其中,nu表示要申请的内存单元数,一个内存单元为sizeof(Header),因而nu在malloc中计算如下:

      -
      nunits = (nbytes + sizeof(Header) - 1)/sizeof(Header) + 1;
      +
      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
      }
      -

      为了满足内核以一页为最小内存单位的需求,以及避免过多陷入内核态,它每次会申请至少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;
      +

      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;
        -

        由于此时freep == freep->str == base,并且我们在morecore中新申请的内存空间ap满足ap > base,故而会跳出循环。

        +

        看到的第一反应就是,我们需求的那块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");
      }
      + +

      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的副本以及内核中所需的额外信息。

      +
      +

      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
      };
      + +

      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: inode

      +

      主要是在讲inode layer这一层的方法,以及给上层提供的接口。

      +
      +

      Overview

      image-20230124153309132

      +

      底层接口

      +

      iget iput

      +
      +
      iget

      逻辑还是跟buffer cache非常相似的,不过可以看出这个的数据结构简单许多,也不用实现LRU。

      -

      为什么ap > base呢?

      -

      别忘了我们扩容的原理。我们是以proc->size为起始地址扩容的。ap处在扩容内存中,因而ap>旧size;base处在扩容前内存内,因而base<=旧size。故而有ap>base。

      +

      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(如路径名查找)的代码产生死锁。

      -
      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;
      +
      // 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;
      }
      -

      跳出循环后,我们会进入第一个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;
      }
      +
      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);
      }
      -

      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);
      }
      +

      上层接口

      获取和释放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");
      }
      -

      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;
      +
      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");
      }
      }
      -

      由于ap > baseap > 旧p->size = base->ptrbase < base->ptr,故而首先会进行一轮循环。再然后,由于p = 旧p->size,并且p > p->ptr = base,并且ap > 旧size,故而跳出循环。

      +
      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);
      }
      + +

      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)字节。

      -
      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;
      +
      // On-disk inode structure
      struct dinode {
      // ...
      uint addrs[NDIRECT+1]; // Data block addresses
      };
      -

      此时会进入第二个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) {
      }
      +

      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");
      }
      -

      那么安装以前所学的内容,我们很容易联系到这玩意应该并不是个文件,而是类似于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);
      }
      +

      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;
      }
      -

      这玩意的文件结点实际上是在创建console时整的,并且其有一个特殊的文件类型“STATS”。我们可以进一步追溯到kernel中的main.c

      -
      void main()
      {
      if(cpuid() == 0){
      #if defined(LAB_PGTBL) || defined(LAB_LOCK)
      statsinit();
      +

      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;
      }
      -
      void
      statsinit(void)
      {
      initlock(&stats.lock, "stats");

      devsw[STATS].read = statsread;
      devsw[STATS].write = statswrite;
      }
      +

      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];
      };
      -

      可以看到,它给这个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;
      }
      +

      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;
      }
      -
      int statswrite(int user_src, uint64 src, int n) { // WARNING: READ ONLY!!!
      return -1;
      }
      +
      // 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;
      }
      -

      可以看到其本质就是把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;
      }
      +

      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);
      }
      -

      可以看到其争用本质计算是通过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
      }
      +
      struct inode*
      nameiparent(char *path, char *name)
      {
      return namex(path, 1, name);
      }
      -

      很好,逻辑很简单,就是记录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/ - 记录一次vm扩容

      -

      开发中遇到的链接小问题

      -

      rtt硬件环境搭建

      -]]>
      -
      - - File system - /2023/01/10/xv6$chap8/ - File system
      -

      来到指导书最高点!太美丽了xv6。哎呀那不文件系统吗(

      -

      这里是自底向上讲起的。之后可以看看hit网课的自顶向下。

      +

      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;
      }
      +
      -

      image-20230121160555370

      +

      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之间的分离很重要。

      -

      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.

      +

      File descriptor layer

      +

      Unix的一个很酷的方面是,Unix中的大多数资源都表示为文件,包括控制台、管道等设备,当然还有真实文件。文件描述符层是实现这种一致性的层。

      -

      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【要么空闲要么是文件或目录】.

      +

      数据结构

      +

      Xv6为每个进程提供了自己的打开文件表或文件描述符。每个打开的文件都由一个struct file表示,它是inode或管道的封装,加上一个I/O偏移量。

      +

      每次调用open都会创建一个新的打开文件(一个新的struct file):如果多个进程独立地打开同一个文件,那么不同的实例将具有不同的I/O偏移量。

      +

      另一方面,单个打开的文件(同一个struct file)可以多次出现在一个进程的文件表中,也可以出现在多个进程的文件表中。如果一个进程使用open打开文件,然后使用dup创建别名,或使用fork与子进程共享,就会发生这种情况。

      -

      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为新块回收最近使用最少的缓冲区。这样做的原因是认为最近使用最少的缓冲区是最不可能近期再次使用的缓冲区。

      +
      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
      };
      + +

      ftable

      +

      所有在系统中打开的文件都会被放入global file tableftable中。

      +

      ftable具有分配文件(filealloc)、创建重复引用(filedup)、释放引用(fileclose)以及读取和写入数据(filereadfilewrite)的函数。

      +

      前三个都很常规,跟之前的xxalloc、xxfree的思路是一样的。

      +

      函数filestatfilereadfilewrite实现对文件的statreadwrite操作。

      -

      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];
      };
      +

      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;
      }
      -

      这应该代表着一个磁盘块。

      -
      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;
      +

      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;
      }
      -

      大概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;
      }
      }
      +

      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();
      }
      }
      -

      上层接口

      -

      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.

      +

      filestat

      +

      Filestat只允许在inode上操作并且调用了stati

      -

      bread

      -

      bread obtains a buf containing a copy of a block which can be read or modified in memory.

      -

      依据给定设备号和给定扇区号寻找cache的buf。返回的buf是locked的。

      +
      // 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中。

      -
      // 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;
      }
      +

      这个函数的功能是给文件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;
      }
      -

      bwrite

      -

      writes a modified buffer to the appropriate block on the disk

      +

      create

      +

      它是三个文件创建系统调用的泛化:带有O_CREATE标志的open生成一个新的普通文件,mkdir生成一个新目录,mkdev生成一个新的设备文件。

      -
      // 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);
      }
      +

      创建一个新的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;
      }
      -

      brelse

      -

      A kernel thread must release a buffer by calling brelse when it is done with it.

      +

      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是最复杂的,因为创建一个新文件只是它能做的一小部分。

      -
      // 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);
      }
      +
      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;
      }
      -

      具体细节

      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");
      }
      +

      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;
      }
      -

      Logging layer

      简介

      -

      Xv6通过简单的日志记录形式解决了文件系统操作期间的崩溃问题。

      -

      xv6系统调用不会直接写入磁盘上的文件系统数据结构。相反,它会在磁盘上的log(日志)中放置它希望进行的所有磁盘写入的描述。一旦系统调用记录了它的所有写入操作,它就会向磁盘写入一条特殊的commit(提交)记录,表明日志包含一个完整的操作。此时,系统调用将写操作复制到磁盘上的文件系统数据结构。完成这些写入后,系统调用将擦除磁盘上的日志。

      +

      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.

      +

      不过做完这个实验,给我的一种感觉就是磁盘管理和内存管理真的有很多相似之处,不过也许它们所代表的思想也很普遍。

      -

      这就保证了原子性。

      -

      Log design

      image-20230121162324747

      -

      superblock记录了log的存储位置。

      -
      -

      它由一个头块(header block)和一系列更新块的副本(logged block)组成。

      -

      头块包含一个扇区号(sector)数组(每个logged block对应一个扇区号)以及日志块的计数。

      -

      磁盘上的头块中的计数为零表示日志中没有事务,为非零表示日志包含一个完整的已提交事务,并具有指定数量的logged block。

      -

      在事务提交(commit)时Xv6才向头块写入数据,在此之前不会写入。在将logged blocks复制到文件系统后,头块的计数将被设置为零。

      -

      因此,事务中途崩溃将导致日志头块中的计数为零;提交后的崩溃将导致非零计数。

      +

      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).

      -
      -

      为了允许不同进程并发执行文件系统操作,日志系统可以将多个系统调用的写入累积到一个事务中。因此,单个提交可能涉及多个完整系统调用的写入。为了避免在事务之间拆分系统调用,日志系统仅在没有文件系统调用进行时提交。

      -

      同时提交多个事务的想法称为组提交(group commit)。组提交减少了磁盘操作的数量,因为成本固定的一次提交分摊了多个操作。组提交还同时为磁盘系统提供更多并发写操作,可能允许磁盘在一个磁盘旋转时间内写入所有这些操作。Xv6的virtio驱动程序不支持这种批处理,但是Xv6的文件系统设计允许这样做。

      -

      【这感觉实现得也还挺简略的】

      +
      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.

      +
      +
      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.

      +
      +

      感想

      意外地很简单()在此不多做赘述,直接上代码。

      +

      唯一要注意的一点就是记得在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");
      }
      + +
      修改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;
      // 双层循环。这里其实不应该用NINDIRECT这个宏定义的,因为意义其实不大一样。
      // 但是由于数值一样,这里就先凑合着用了
      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.

      -

      Xv6在磁盘上留出固定的空间来保存日志。事务中系统调用写入的块总数必须可容纳于该空间。这导致两个后果:

      +

      linux:硬链接和软链接

      +

      硬链接不会创建新的物理文件,但是会使得当前物理文件的引用数加1。当硬链接产生的文件存在时,删除源文件,不会清除实际的物理文件,即对于硬链接“生成的新文件”不会产生任何影响。

      +

      软链接就更像一个指针,只是指向实际物理文件位置,当源文件移动或者删除时,软链接就会失效。

      +

      【所以说,意思就是软链接不会让inode->ulinks++的意思?】

      +
      +

      感想

      这个实验比上个实验稍难一些,但也确实只是moderate的水平,其复杂程度主要来源于对文件系统的理解,还有如何判断环,以及对锁的获取和释放的应用。我做这个实验居然是没看提示的【非常骄傲<-】,让我有一种自己水平上升了的感觉hhh

      +
      正确思路

      本实验要求实现软链接。首先需要实现创建软链接:写一个系统调用 symlink(char *target, char *path) 用于创建一个指向target的在path的软链接;然后需要实现打开软链接进行自动的跳转:在sys_open中添加对文件类型为软链接的特殊处理。

      +
      初见思路

      我的初见思路是觉得可以完全参照sys_link来写。但其实还是很不一样的。

      +

      sys_link的逻辑:

        -
      1. 任何单个系统调用都不允许写入超过日志空间的不同块。

        -

        【这段话我一个字没看懂】

        -

        这对于大多数系统调用来说都不是问题,但其中两个可能会写入许多块:writeunlink。一个大文件的write可以写入多个数据块和多个位图块以及一个inode块;unlink大文件可能会写入许多位图块和inode。Xv6的write系统调用将大的写入分解为适合日志的多个较小的写入,unlink不会导致此问题,因为实际上Xv6文件系统只使用一个位图块。

        +
      2. 获取old的inode
      3. +
      4. 获取new所在目录的inode,称为dp
      5. +
      6. 在dp中添加一项entry指向old
      7. +
      +

      sys_symlink的逻辑:

      +
        +
      1. 通过path创建一个新的inode,作为软链接的文件

        +

        这里选择新建inode,而不是像link那样做,主要还是为了能遵从symlinktest给的接口使用方法(朴实无华的理由)。而且这么做也很方便,符合“一切皆文件”的思想,也能简单化对其在open中的处理。

      2. -
      3. 日志空间有限的另一个后果是,除非确定系统调用的写入将可容纳于日志中剩余的空间,否则日志系统无法允许启动系统调用。

        +
      4. 在inode中填入target的地址

        +

        我们可以把软链接视为文件,文件内容是其target的path。

      -
      -

      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;
      - -

      关键函数

      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;
      }
      }
      }
      - -
      log_write
      -

      log_write充当bwrite的代理。它将块的扇区号记录在内存中,在磁盘上的日志中预定一个槽位,并调用bpin将缓存固定在block cache中,以防止block cache将其逐出【具体原理就是让refcnt++,这样就不会被当成空闲block用掉了】。

      -

      为啥要防止换出呢?换出不是就正好自动写入磁盘了吗?这里一是为了保障前面提到的原子性,防止换入换出导致的单一写入磁盘;二是换出自动写入的是磁盘对应位而不一定是日志所在的blocks。

      -
      -
      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);
      }
      - -
      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);
      }
      }
      +

      可以说是毫不相干,所以还是直接自起炉灶比较好。

      +
      一些错误

      其实没什么好说的,虽然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);
      -
      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
      }
      }
      +

      在这里,我错误地调用了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
      -
      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);
      }
      }
      +
      stat.h

      文件类型

      +
      #define T_DIR     1   // Directory
      #define T_FILE 2 // File
      #define T_DEVICE 3 // Device
      #define T_SYMLINK 4 // symbol links
      -
      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);
      }
      +
      添加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;
      }
      -
      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);
      }
      }
      +
      修改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;
      }
      // ...
      }
      -

      恢复与初始化

      上面介绍了log的一次事务提交的流程。接下来介绍它是怎么恢复的。

      -
      -

      recover_from_log是由initlog调用的,而它又是在第一个用户进程运行之前的引导期间由fsinit调用的。

      +

      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。

      -
      第一个进程运行之前

      由前面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);
      }
      - -
      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();
      }
      - -
      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
      }
      +
      +

      declaration for mmap:

      +
      void *mmap(void *addr, size_t length, int prot, int flags,
      int fd, off_t offset);
      -

      Code: Block allocator

      个人理解

      说实话没怎么懂,也不大清楚它有什么用,先大概推测一下:

      -

      之前的bread和bwrite这些,就是你给一个设备号和扇区号,它就帮你加载进内存cache。你如果要用的话,肯定还是使用地址方便。所以block allocator的作用之一就是给bread和bwrite加一层封装,将获取的block封装为地址返回,你可以直接操纵这个地址,而无需知道下层的细节。

      -

      这个过程要注意的有两点:

        -
      1. 封装返回的地址具体是什么,怎么工作的

        -

        封装返回的地址实质上是buffer cache中的buf的data字段的地址【差不多】。之后的上层应用在该地址上写入,也即写入了buf,最后会通过log层真正写入磁盘。

        +
      2. 参数

        +
          +
        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. 结合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的条件。因为它可能被替掉才能说明它可能是最近最少使用的。

          +
        4. length is the number of bytes to map

          +

          Might not be the same as the file’s length.

          +
        5. +
        6. 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.

          +
        7. +
        8. 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

          -

          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");
          }
          - -

          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的副本以及内核中所需的额外信息。

          -
          -

          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
          };
          - -

          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: inode

          -

          主要是在讲inode layer这一层的方法,以及给上层提供的接口。

          -
          -

          Overview

          image-20230124153309132

          -

          底层接口

          -

          iget iput

          +
        9. +
        10. You can assume offset is zero (it’s the starting point in the file at which to map)

          +
        11. +
        +
      3. +
      4. return

        +

        mmap returns that kernel-decided address, or 0xffffffffffffffff if it fails.

        +
      5. +
      +

      如果两个进程同时对某个文件进行memory map,那么这两个进程可以不共享物理页面。

      -
      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;
      }
      - -
      iput
      -

      iput()可以写入磁盘。这意味着任何使用文件系统的系统调用都可能写入磁盘,因为系统调用可能是最后一个引用该文件的系统调用。即使像read()这样看起来是只读的调用,也可能最终调用iput()。这反过来意味着,即使是只读系统调用,如果它们使用文件系统,也必须在事务中进行包装。

      -

      iput()和崩溃之间存在一种具有挑战性的交互。iput()不会在文件的链接计数降至零时立即截断文件,因为某些进程可能仍在内存中保留对inode的引用:进程可能仍在读取和写入该文件,因为它已成功打开该文件。但是,如果在最后一个进程关闭该文件的文件描述符之前发生崩溃,则该文件将被标记为已在磁盘上分配,但没有目录项指向它。如果不做任何处理措施的话,这块磁盘就再也用不了了。

      -

      文件系统以两种方式之一处理这种情况。简单的解决方案用于恢复时:重新启动后,文件系统会扫描整个文件系统,以查找标记为已分配但没有指向它们的目录项的文件。如果存在任何此类文件,接下来可以将其释放。

      -

      第二种解决方案不需要扫描文件系统。在此解决方案中,文件系统在磁盘(例如在超级块中)上记录链接计数降至零但引用计数不为零的文件的i-number。如果文件系统在其引用计数达到0时删除该文件,则会通过从列表中删除该inode来更新磁盘列表。重新启动时,文件系统将释放列表中的所有文件。

      -

      Xv6没有实现这两种解决方案,这意味着inode可能被标记为已在磁盘上分配,即使它们不再使用。这意味着随着时间的推移,xv6可能会面临磁盘空间不足的风险。

      +

      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).

      -
      // 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);
      }
      - -

      上层接口

      获取和释放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");
      }
      - -
      inode的锁保护

      前面说到,inode的设计使得有多个指针同时指向一个inode成为了可能。因而,修改使用inode的时候就要对其进行独占访问。使用ialloc获取和用ifree释放的inode必须被保护在ilockiunlock区域中。

      -
      ilock

      ilock既可以实现对inode的独占访问,同时也可以给未初始化的inode进行初始化工作。

      +

      感想

      这个实验做得我……怎么说,感觉非常地难受吧。虽然我认为我这次做得挺不错的,因为我没有怎么看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域中,以避免对象的重复创建。

      -

      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");
      }
      }
      - -
      iunlock
      -

      iunlock释放inode上的锁。

      -

      将inode指针的获取与锁定分离有助于在某些情况下避免死锁,例如在目录查找期间。多个进程可以持有指向iget返回的inode的C指针,但一次只能有一个进程锁定inode。

      +

      我的lazy法与别人不大一样……我没有想得像他们那么完美。我的做法是,在需要读某个地址的文件内容时,直接确保这个地址前面的所有文件内容都读了进来。也即在filemap中维护一个okva,表明vaokva这段内存已经读入,之后就仅需再读入okvaneed_va这段地址就行。这样虽然lazy了,但没完全lazy。

      +

      我认为这不能体现lazy的思想……因为一读读一坨,还是很占空间啊。

      -
      // Unlock the given inode.
      void
      iunlock(struct inode *ip)
      {
      if(ip == 0 || !holdingsleep(&ip->lock) || ip->ref < 1)
      panic("iunlock");

      releasesleep(&ip->lock);
      }
      - -

      Code: inode content

      Overview

      -

      主要讲的是inode本身存储数据的结构

      +

      因而,我们需要做的就是:

      +
        +
      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)……至于为什么我不用这个,而要写这么多麻烦的东西呢?答案是我没想到。()

        +
      2. +
      3. 在usertrap增加对缺页中断的处理

        +
          +
        1. 依据va找到对应filemap
        2. +
        3. 根据对应filemap的信息,使用readi(正确)fileread(错误)读取文件内容并存入物理内存
        4. +
        +
      4. +
      5. 在munmap中进行释放

        +
          +
        1. 根据标记写入文件页,并且释放对应物理内存
        2. +
        3. 修改filemap结构的参数,并且在其失效的时候放回对象池
        4. +
        +
      6. +
      7. 修改fork和exit

        +
          +
        1. exit

          +

          手动释放map-file域

          -

          磁盘上的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)字节。

          +

          为什么不能把这些合并到wait中调用的freepagetable进行释放呢?

          +

          因为freepagetable只会释放对应的物理页,没有达到munmap减少文件引用等功能。

          -
          // On-disk inode structure
          struct dinode {
          // ...
          uint addrs[NDIRECT+1]; // Data block addresses
          };
          - -

          image-20230124163025094

          -

          bmap

          -

          函数bmap负责封装这个寻找数据块的过程,以便实现我们将很快看到的如readiwritei这样的更高级例程。

          -

          bmap(struct inode *ip, uint bn)返回inodeip的第bn个数据块的磁盘块号。如果ip还没有这样的块,bmap会分配一个。

          -

          Bmap使readiwritei很容易获取inode的数据。

          +
        2. +
        3. fork

          +

          手动复制filemap池

          +
        4. +
        +
      8. +
      +

      我的错误思路们

      第一次错误思路

      上面说到:

      +
      +

      问题就在于如何“先建立memory-file的映射”。在lazy allocation中,我们是先填好所有的对应页表项,仅是不申请对应的物理内存,也即占着XX不XX。在这次实验中,我们也是这么做,只不过新增了一个难点,那就是如何管理这些页。因为lazy allocation页与页之间没有比较紧密的关系,但是在mmap中页却可以被所属文件这个关键字划分。因而,我们需要一个数据结构,来给页分门别类地组织在一起,并且记录它们的meta data比如说所属文件之类的,这也就是hints里的VMA结构,也即我的filemap结构。

      -
      // 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");
      }
      +

      官方给出的答案是在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;
      }
      -

      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;
      }
      +
      } 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;
      }
      }
      -

      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;
      }
      +
      为什么下面的代码是错的

      正如开头所说的那样,我并没有完美做好这次实验,下面代码有一个致命的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
      };
      -

      stati

      -

      函数stati将inode元数据复制到stat结构体中,该结构通过stat系统调用向用户程序公开。

      +

      在我的代码里,它们被赋予了不同的含义。

      +

      filemap->file->off被用于trap.c中,表示的是当前未读入文件内容的起始位置(实际上也就是okva-va的值),用于自然地使用fileread进行文件读入。

      +
      +

      比如说,这次读入PGSIZE,那么off就会在fileread中自增PGSIZE。下次调用fileread就可以直接从下一个位置读入了,这样使代码更加简洁

      -

      defs.h中可看到inode结构体是private的,而stat是public的。

      -

      Directory layer

      数据结构

      -

      目录的内部实现很像文件。其inode的typeT_DIR,其数据是directory entries的集合。

      -

      每个entry都是一个struct dirent

      +

      filemap->offset被用于munmap中。filewritefileread一样,都是从file->off处开始取数据。munmap所需要取数据的起始位置和trap.c中需要取数据的起始位置肯定不一样,

      +
      +

      想想它们的功能。trap.c的off需要始终指向有效内存段的末尾,但munmap由于要对特定内存段进行写入文件操作,因而off要求可以随机指向。

      -

      也就是说这一层其实本质上是一个大小一定的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];
      };
      +

      因而,我们可以将当前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
      */
      -

      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;
      }
      +
      // 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);
      -
      // 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;
      }
      +

      这段代码因为共用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;
      }
      -

      Pathname layer

      -

      Path name lookup involves a succession of calls to dirlookup, one for each path component.

      +

      可以看到,它实际上是有一个文件描述符表,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]);
      -

      namei和nameiparent

      -

      Namei (kernel/fs.c:661) evaluates path and returns the corresponding inode.

      -

      函数nameiparent是一个变体:它在最后一个元素之前停止,返回父目录的inode并将最后一个元素复制到name中。两者都调用通用函数namex来完成实际工作。

      +

      最后的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)
      + +

      这样做的好处是,我们可以实时计算offset(前面提到,其恰恰等于okva-va),而不用把这个东西用file的off来表示。

      +
      +

      也确实,我之所以弯弯绕绕那么曲折,是因为只想到了fileread这个函数,压根没注意到还有一个readi……

      -
      struct inode*
      namei(char *path)
      {
      char name[DIRSIZ];
      return namex(path, 0, name);
      }
      +

      我在下面的代码仅做了一个能够通过测试,但是上面的bug依然存在的功利性折中代码。我是这么实现的:

      +
      // 在`mmap`的时候初始化`file->off`
      p->filemaps[i].file->off = offset;
      // 在`munmap`的时候清零`file->off`
      p->filemaps[i].file->off = 0;
      -
      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. +
      3. 正确地lazy

        +

        每次trap仅分配一页。

        +
      4. +
      5. 改用readi函数,修改file->off的语义

        +
      -
      -
      // 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;
      }
      +

      这样一来,大概就可以完美地正确了。

      +

      其他的一些小细节

      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与其他内存块读写方式不同的这些复杂细节。经历过前面几次实验的你看到这里一定能想到,有一个更加优美更加符合设计规范的方法,那就是:缺页中断

      -

      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之间的分离很重要。

      -
      -

      File descriptor layer

      -

      Unix的一个很酷的方面是,Unix中的大多数资源都表示为文件,包括控制台、管道等设备,当然还有真实文件。文件描述符层是实现这种一致性的层。

      -
      -

      数据结构

      -

      Xv6为每个进程提供了自己的打开文件表或文件描述符。每个打开的文件都由一个struct file表示,它是inode或管道的封装,加上一个I/O偏移量。

      -

      每次调用open都会创建一个新的打开文件(一个新的struct file):如果多个进程独立地打开同一个文件,那么不同的实例将具有不同的I/O偏移量。

      -

      另一方面,单个打开的文件(同一个struct file)可以多次出现在一个进程的文件表中,也可以出现在多个进程的文件表中。如果一个进程使用open打开文件,然后使用dup创建别名,或使用fork与子进程共享,就会发生这种情况。

      +

      没做这个实验之前就知道mmap需要借助缺页中断来实现了,但实际自己的第一印象是觉得并不需要缺页中断,直到分析到这里才恍然大悟。

      +

      “让上层的用户可以统一地读取任何的内存块,而隐藏不同类型的内存块读写方式不同的这些复杂细节”

      +

      仔细想想,前面几个关于缺页中断的实验,比如说cow fork,lazy allocation,事实上都是基于这个思想。它们并不是不能与缺页中断分离,只是有了缺页中断,它们的实现更加简洁,更加优美。

      +

      再次感慨os的博大精深。小小一个缺页中断,原理那么简单,居然集中了这么多设计思想,不禁叹服。

      -
      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
      };
      +
      正确答案的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;
      }
      -

      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;
      }
      +

      如果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;
      // ...
      -

      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;
      }
      +

      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];
      };
      -

      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();
      }
      }
      +

      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);
      }
      -

      filestat

      -

      Filestat只允许在inode上操作并且调用了stati

      -
      -
      // 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;
      }
      +
      #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;
      }
      -

      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;
      }
      +

      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;
      }
      }
      }
      -

      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;
      }
      +
      对的
      } 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;
      }
      }
      }
      -

      create

      -

      它是三个文件创建系统调用的泛化:带有O_CREATE标志的open生成一个新的普通文件,mkdir生成一个新目录,mkdev生成一个新的设备文件。

      -
      -

      创建一个新的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;
      }
      +

      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;
      }
      -

      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;
      }
      +
      对的
      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;
      }
      -

      sys_open

      -

      Sys_open是最复杂的,因为创建一个新文件只是它能做的一小部分。

      -
      -
      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;
      }
      +

      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);
      }
      }
      -

      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;
      }
      +
      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);
      }
      -

      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).

      -
      -
      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.

      -
      -
      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.

      +

      修改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;
      +]]> + + + 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切换到目标线程。如此周而复始。

      -

      感想

      意外地很简单()在此不多做赘述,直接上代码。

      -

      唯一要注意的一点就是记得在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
      };
      +

      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;
      };
      -
      // in file.h
      // in-memory copy of an inode
      struct inode {
      // ...
      uint addrs[NDIRECT+2];
      };
      +

      上下文切换需要修改栈和pc,context中确实有sp寄存器,但是没有pc寄存器,这主要还是因为当swtch返回时,会回到ra所指向的地方,所以仅保存ra就足够了。

      +

      swtch

      上下文的切换是通过swtch实现的。

      +
      void            swtch(struct context*, struct context*);
      -
      修改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");
      }
      +

      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
      -
      修改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;
      // 双层循环。这里其实不应该用NINDIRECT这个宏定义的,因为意义其实不大一样。
      // 但是由于数值一样,这里就先凑合着用了
      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);
      }
      +

      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;
      }
      -
      -

      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.

      -
      +

      cpu中存储着的是scheduler线程的context。因而,这样就可以保存当前进程的context,读取scheduler线程的context,然后转换到scheduler的context执行了。

      -

      linux:硬链接和软链接

      -

      硬链接不会创建新的物理文件,但是会使得当前物理文件的引用数加1。当硬链接产生的文件存在时,删除源文件,不会清除实际的物理文件,即对于硬链接“生成的新文件”不会产生任何影响。

      -

      软链接就更像一个指针,只是指向实际物理文件位置,当源文件移动或者删除时,软链接就会失效。

      -

      【所以说,意思就是软链接不会让inode->ulinks++的意思?】

      +

      可以发现这里是有个很完美的组合技的。由sched()保存context到process结构体中,再由scheduler()读取process对应的context回归到sched()继续执行,我感觉调度设计这点真是帅的一匹。

      -

      感想

      这个实验比上个实验稍难一些,但也确实只是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的逻辑:

      +

      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");
      }
      }
      }
      + +

      通过swtch进入scheduler线程后,会继续执行scheduler中swtch的下一个指令,完成下一次调度。

      +

      一些补充

      以上是书本的介绍内容。看到这想必会有很多疑惑,至少有以下两点:

        -
      1. 通过path创建一个新的inode,作为软链接的文件

        -

        这里选择新建inode,而不是像link那样做,主要还是为了能遵从symlinktest给的接口使用方法(朴实无华的理由)。而且这么做也很方便,符合“一切皆文件”的思想,也能简单化对其在open中的处理。

        -
      2. -
      3. 在inode中填入target的地址

        -

        我们可以把软链接视为文件,文件内容是其target的path。

        -
      4. +
      5. 为什么cpu->context会存储着scheduler的上下文?这是什么时候,又是怎么初始化的?
      6. +
      7. 为什么从sched中swtch会来到scheduler中swtch的下一句?
      -

      可以说是毫不相干,所以还是直接自起炉灶比较好。

      -
      一些错误

      其实没什么好说的,虽然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);
      - -

      在这里,我错误地调用了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
      - -
      stat.h

      文件类型

      -
      #define T_DIR     1   // Directory
      #define T_FILE 2 // File
      #define T_DEVICE 3 // Device
      #define T_SYMLINK 4 // symbol links
      - -
      添加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;
      }
      +

      先从第一点入手。实际上,这个初始化的工作,是在操作系统启动时的main.c中完成的。

      +
      void
      main()
      {
      if(cpuid() == 0){
      // ...
      } else {
      // ...
      }

      scheduler();
      }
      -
      修改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。在这里,每个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);
      }
      // ...
      }
      }
      -

      Lab mmap

      -

      The mmap and munmap system calls allow UNIX programs to exert detailed control over their address spaces.

      -

      They can be used to:

      +

      每个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. 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. +
      7. acquire its own process lock p->lock, release any other locks it is holding
      8. +
      9. update its own state (p->state)
      10. +
      11. call sched
      -

      In this lab you’ll add mmap and munmap to xv6, focusing on memory-mapped files.

      -

      mmap是系统调用,在用户态被使用。我们这次实验仅实现mmap功能的子集,也即memory-mapped files。

      +

      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)。

      -

      declaration for mmap:

      -
      void *mmap(void *addr, size_t length, int prot, int flags,
      int fd, off_t offset);
      +

      存在一种情况使得调度程序对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中得到的锁
      }
      +
      // in kernel/proc.c scheduler()
      acquire(&p->lock);// 该锁会在yield等地被释放
      // ...
      swtch(&c->context, &p->context);
      // ...
      release(&p->lock);// 该锁会释放yield等地中获得的锁
      + +

      不得不说,这结构实在是太精妙了。这中间的如此多的复杂过程,就这样成功地被锁保护了起来。

      +

      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;
      }
      + +

      在内核态中,编译器被设置为保证不会以其他方式使用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)
      + +
      struct trapframe {
      /* 32 */ uint64 kernel_hartid; // saved kernel tp
      /* 64 */ uint64 tp;
      // ...
      }
      + +

      必须在trampoline保存用户态中使用的tp值,以及内核态中对应的hartid。

      +

      最后再在返回用户态的时候恢复用户态的tp值以及更新trampoline的tp值。

      +
      // in kernel/trap.c usertrapret()
      p->trapframe->kernel_hartid = r_tp(); // hartid for cpuid()
      + +
      # in trampoline.S userret
      ld tp, 64(a0)
      + +

      注意,更新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;
      }
      + +

      注意,不同于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. 参数

        -
          -
        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. 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.

      3. -
      4. return

        -

        mmap returns that kernel-decided address, or 0xffffffffffffffff if it fails.

        +
      5. 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.

      -

      如果两个进程同时对某个文件进行memory map,那么这两个进程可以不共享物理页面。

      +

      这样一来,信号量实现就可修改为这样了:

      +

      image-20230120151051989

      +

      但是,我们可以注意到,在212-213行这里产生了一个先检查后执行的竞态条件。

      -

      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).

      +

      如果消费者进程执行到212-213中间,此时生产者进程已经调用结束,也就是说wakeup并没有唤醒任何消费者进程。消费者进程就会一直在sleep中没人唤醒,除非生产者进程再执行一次。这样就会造成lost wake-up 这个问题。

      -

      感想

      这个实验做得我……怎么说,感觉非常地难受吧。虽然我认为我这次做得挺不错的,因为我没有怎么看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域中,以避免对象的重复创建。

      +

      所以,我们可以选择把这个竞态条件也放入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

      -

      我的lazy法与别人不大一样……我没有想得像他们那么完美。我的做法是,在需要读某个地址的文件内容时,直接确保这个地址前面的所有文件内容都读了进来。也即在filemap中维护一个okva,表明vaokva这段内存已经读入,之后就仅需再读入okvaneed_va这段地址就行。这样虽然lazy了,但没完全lazy。

      -

      我认为这不能体现lazy的思想……因为一读读一坨,还是很占空间啊。

      +

      注:严格地说,wakeup只需跟在acquire之后就足够了(也就是说,可以在release之后调用wakeup

      +

      【想了一下,有一说一确实,放在release前后都不影响】

      -

      因而,我们需要做的就是:

      -
        -
      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)……至于为什么我不用这个,而要写这么多麻烦的东西呢?答案是我没想到。()

        +

        原始Unix内核的sleep只是禁用了中断,这就足够了,因为Unix运行在单CPU系统上。因为xv6在多处理器上运行,所以它为sleep添加了一个显式锁。

        -
      2. -
      3. 在usertrap增加对缺页中断的处理

        +

        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);
        }
        }
        + +

        注意,如果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);
        }
        }
        + +

        可以注意到,关于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
        };
        + +
        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;
        }
        + +
        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;
        }
        + +

        一个非常有意思且巧妙的点,就是读写管道等待在不同的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");
        }
        + +
        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
        }
        }
        + +

        其中值得注意的几个点:

          -
        1. 依据va找到对应filemap
        2. -
        3. 根据对应filemap的信息,使用readi(正确)fileread(错误)读取文件内容并存入物理内存
        4. -
        +
      4. wait中的sleep中释放的条件锁是等待进程的p->lock,这是上面提到的特例。

      5. -
      6. 在munmap中进行释放

        -
          -
        1. 根据标记写入文件页,并且释放对应物理内存
        2. -
        3. 修改filemap结构的参数,并且在其失效的时候放回对象池
        4. -
        +
      7. 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.
        }
        }
        }
        + +

        如果子进程退出,就会通过init的wait释放它们。然后init释放完它们后进入第三个if分支,继续进行循环。

      8. -
      9. 修改fork和exit

        -
          -
        1. exit

          -

          手动释放map-file域

          +
        2. wakeup1

          -

          为什么不能把这些合并到wait中调用的freepagetable进行释放呢?

          -

          因为freepagetable只会释放对应的物理页,没有达到munmap减少文件引用等功能。

          +

          Exit calls a specialized wakeup function, wakeup1, that wakes up only the parent, and only if it is sleeping in wait.

          -
        3. -
        4. fork

          -

          手动复制filemap池

          -
        5. -
        -
      10. +
        // 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;
        }
        }
      -

      我的错误思路们

      第一次错误思路

      上面说到:

      +

      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这两个函数的这种结构,实在是非常精妙,太牛了吧。

      -

      问题就在于如何“先建立memory-file的映射”。在lazy allocation中,我们是先填好所有的对应页表项,仅是不申请对应的物理内存,也即占着XX不XX。在这次实验中,我们也是这么做,只不过新增了一个难点,那就是如何管理这些页。因为lazy allocation页与页之间没有比较紧密的关系,但是在mmap中页却可以被所属文件这个关键字划分。因而,我们需要一个数据结构,来给页分门别类地组织在一起,并且记录它们的meta data比如说所属文件之类的,这也就是hints里的VMA结构,也即我的filemap结构。

      +

      一些XV6的sleep循环不检查p->killed,因为代码在应该是原子操作的多步系统调用的中间。virtio驱动程序(*kernel/virtio_disk.c*:242)就是一个例子:它不检查p->killed,因为一个磁盘操作可能是文件系统保持正确状态所需的一组写入操作之一。等待磁盘I/O时被杀死的进程将不会退出,直到它完成当前系统调用并且usertrap看到killed标志

      -

      官方给出的答案是在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就可以直接从下一个位置读入了,这样使代码更加简洁

      +

      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)。当低优先级进程和高优先级进程共享一个锁时,可能会发生优先级反转,当低优先级进程持有该锁时,可能会阻止高优先级进程前进。当许多高优先级进程正在等待一个获得共享锁的低优先级进程时,可能会形成一个长的等待进程航队;一旦航队形成,它可以持续很长时间。为了避免此类问题,在复杂的调度器中需要额外的机制。

      -

      filemap->offset被用于munmap中。filewritefileread一样,都是从file->off处开始取数据。munmap所需要取数据的起始位置和trap.c中需要取数据的起始位置肯定不一样,

      -

      想想它们的功能。trap.c的off需要始终指向有效内存段的末尾,但munmap由于要对特定内存段进行写入文件操作,因而off要求可以随机指向。

      +

      wakeup中扫描整个进程列表以查找具有匹配chan的进程效率低下。一个更好的解决方案是用一个数据结构替换sleepwakeup中的chan,该数据结构包含在该结构上休眠的进程列表,例如Linux的等待队列。

      -

      因而,我们可以将当前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区域同时使用。

      +

      是的,linux的那个wakeup真的很牛,我现在都还记得当初学到那的时候的震撼。

      -

      共用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]);
      +

      wakeup的实现会唤醒在特定通道上等待的所有进程,可能有许多进程在等待该特定通道。操作系统将安排所有这些进程,它们将竞相检查睡眠条件。进程的这种行为有时被称为惊群效应(thundering herd),最好避免。

      +

      大多数条件变量都有两个用于唤醒的原语:signal用于唤醒一个进程;broadcast用于唤醒所有等待进程。

      -

      最后的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)
      - -

      这样做的好处是,我们可以实时计算offset(前面提到,其恰恰等于okva-va),而不用把这个东西用file的off来表示。

      -

      也确实,我之所以弯弯绕绕那么曲折,是因为只想到了fileread这个函数,压根没注意到还有一个readi……

      +

      一个实际的操作系统将在固定时间内使用空闲列表找到自由的proc结构体,而不是allocproc中的线性时间搜索;xv6使用线性扫描是为了简单起见。

      -

      我在下面的代码仅做了一个能够通过测试,但是上面的bug依然存在的功利性折中代码。我是这么实现的:

      -
      // 在`mmap`的时候初始化`file->off`
      p->filemaps[i].file->off = offset;
      // 在`munmap`的时候清零`file->off`
      p->filemaps[i].file->off = 0;
      - - +

      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 */
      -

      因而,结论是,一步错步步错,一个错误需要更多的错误来弥补,最后还是错的(悲)

      -
      如何把下面的错误思路改成正确思路

      可以做以下几点:

      -
        -
      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();
      }
      }
      +

      这几个函数到时候会被如此调用:

      +
      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);
      }
      -

      可以看到,当ref数>1时,file指针就不会失效。

      -

      这就是为什么我们还需要在mmap中让file的ref数++。

      -
      缺页中断蕴含的设计思想

      如果只存入file指针,用户态要如何对对应的文件进行读写呢?

      -

      我们可以自然想到也许需要设计一个函数,让用户在想要对这块内存读写的时候调用这个函数即可。但是,这样的方法使得用户对内存不能自然地读写,还需要使用我们新设计的这个函数,这显然十分地不美观。所以,我们需要找到一个方法,让上层的用户可以统一地读取任何的内存块,包括memory-mapped file内存块,而隐藏memory-mapped file与其他内存块读写方式不同的这些复杂细节。经历过前面几次实验的你看到这里一定能想到,有一个更加优美更加符合设计规范的方法,那就是:缺页中断

      +

      所以,我们在第一个地方要做的,就是要填入swtch的签名。第二个地方要做的,就是要想办法让该线程一被启动就去执行参数的函数指针。第三个地方要做的,就是要完成上下文的切换。

      +

      所以思路其实是很直观的。我们可以模仿进程管理中用来表示上下文的context,在thread_create的时候把里面的ra设置为参数的函数指针入口,sp修改为thread结构体中的栈地址。swtch函数则完全把kernel/swtch.S超过来就行。

      -

      没做这个实验之前就知道mmap需要借助缺页中断来实现了,但实际自己的第一印象是觉得并不需要缺页中断,直到分析到这里才恍然大悟。

      -

      “让上层的用户可以统一地读取任何的内存块,而隐藏不同类型的内存块读写方式不同的这些复杂细节”

      -

      仔细想想,前面几个关于缺页中断的实验,比如说cow fork,lazy allocation,事实上都是基于这个思想。它们并不是不能与缺页中断分离,只是有了缺页中断,它们的实现更加简洁,更加优美。

      -

      再次感慨os的博大精深。小小一个缺页中断,原理那么简单,居然集中了这么多设计思想,不禁叹服。

      +

      在这个思路中,我们是怎么做到栈的切换的呢?

      +

      每个线程在thread_create的时候,都将自己的context中的sp修改为自己的栈地址。这样一来,在它们被调度的时候,switch会自然而然地从context中读取sp作为之后运行的sp,这样就实现了栈的切换。

      -
      正确答案的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;
      }
      }
      }
      +

      我觉得其他方面都不难,最坑最细节的【也是我完全没有想到的……】就是这里:

      +
      // 修改sp为栈顶
      t->context.sp = (uint64)t->stack + STACK_SIZE;
      -
      对的
      } 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;
      }
      }
      }
      +

      需要注意,栈顶并不是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
      -

      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;
      }
      +

      栈是向下增长的,因而,栈顶确实应该是数组的末尾……

      +

      这里完全没有想到,还是吃了基础的亏啊。

      +
      +

      如果这里将t->stack作为sp,那么运行时会出现非常诡异的现象(打印的是abc三个的thread->state):

      +

      image-20230120232149776

      +

      仅有c【经测试,是仅有最后一个启动的线程】在执行,而ab的state都不是理想中的2,而是很奇怪的值。我确实有想过栈溢出问题,但是马上被我否定了。我完全没有想到是那样错的【悲】

      +
      +

      代码

      增加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;
      };
      -
      对的
      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;
      }
      +
      修改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;
      }
      -

      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);
      }
      }
      +
      修改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;
      -
      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);
      }
      +
      修改thread_switch

      全部照搬kernel/swtch.S,没什么好说的

      +

      Using threads

      一步步细粒度化,最后,每个桶用单独一把锁,仅在调用insert处加锁就行。

      +
      pthread_mutex_t locks[NBUCKET];// 在main中初始化

      static
      void put(int key, int value)
      {
      int i = key % NBUCKET;

      // is the key already present?
      struct entry *e = 0;
      for (e = table[i]; e != 0; e = e->next) {
      if (e->key == key)
      break;
      }
      if(e){
      // update the existing key.
      e->value = value;
      } else {
      // the new is new.
      pthread_mutex_lock(&locks[i]);
      insert(key, value, &table[i], table[i]);
      pthread_mutex_unlock(&locks[i]);
      }
      }
      -

      修改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;
      +

      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));
      }
      ]]> + + 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 + +
      哈工大操作系统实验 /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/ @@ -11884,6 +12087,50 @@ url访问填写http://localhost/webdemo4_war/*.dolabs + + 状态机 + /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型。

      +
      +

      这个点本来可以讲得更清楚一些的……只教会我们做题的套路有啥意思呢←_←

      +
      +]]>
      +
      存储简单入门 /2023/10/06/%E5%AD%98%E5%82%A8%E7%AE%80%E5%8D%95%E5%85%A5%E9%97%A8/ @@ -12126,74 +12373,30 @@ url访问填写http://localhost/webdemo4_war/*.do

      我们可以在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/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型。

      +

      值得注意的是,此修改仅对本次启动有效。如果需要长期修改,建议还是通过第一种方法去修改。

      +

      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竞赛 +
      网络是怎样连接的 @@ -12388,14 +12591,254 @@ url访问填写http://localhost/webdemo4_war/*.do
    5. 在vmware中扩展磁盘容量

      image-20230927191636392

    6. -
    7. sudo fdisk /dev/sda,进行磁盘分区

      -

      在fdisk中输入n,新建sda4分区,然后w保存。

      +
    8. sudo fdisk /dev/sda,进行磁盘分区

      +

      在fdisk中输入n,新建sda4分区,然后w保存。

      +
    9. +
    10. 执行下列命令:

      +
      sudo pvcreate /dev/sda4
      sudo vgcreate ubuntu-vg /dev/sda4
      sudo vgextend ubuntu-vg /dev/sda4
      sudo vgdisplay # 此时应发现FREE变成了100G
      sudo lvresize -l +100%FREE /dev/mapper/ubuntu--vg-ubuntu--lv
      sudo resize2fs /dev/mapper/ubuntu--vg-ubuntu--lv
      sudo df -h # 验证成功
    11. +
    +

    一开始按照的这个,然后被坑惨了(悲)把lvm sig给抹了,导致之后resize2fs的时候报错,然后之后又不小心重启了,最后的最后只能重装。。。又是一晚上配环境。。。。

    +]]> + + + 阅读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 迭代
    }
    + +

    其中:

      +
    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

      +
      +

      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.

      +
      +
      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。

      +
    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)
      + +

      所举代码段意为把新值通过字符串拼接接在旧值后面。

      +

      应该也可以用于集合合并。总之具体实现方法取决于传入的function参数,非常实用

      +
      +
    10. +
    11. 迭代器

      map本身没有迭代器。

      +

      因而在对map进行遍历时,只能通过其keyset、valueset以及entryset来实现。

      +

      具体详见:HashMap的四种遍历方式

      +
    12. +
    +

    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. +
    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

    +
    +

    也因此,对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也可以它却重写了。原来是因为这个性能问题啊

      +
    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).

    + + +

    关于这部分,详细见sorted set

    +
    +

    最大的特点就是可以人为定义有序并且有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();
    }
    + +
    +

    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);
    }
    + +

    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方法获取元素】

      +
    2. +
    3. not synchronized

      上面介绍到的几个类,除了Vector外,都是线程不同步的。可以用此方式让其线程同步。

      +
      Map m = Collections.synchronizedMap(new LinkedHashMap(...));
    4. +
    5. 是否允许null

      除了TreeSet、TreeMap、ArrayDeque之外,都是允许空(key/value)的

      +
    6. +
    7. 是否有序

      List都是插入序,HashSet无需,HashMap也无序(但其实算是有内部桶序的),LinkedHashMap有插入序和LRU序(依靠内部增加简单队列的消耗),TreeSet有序,TreeMap有序【这俩靠红黑树的遍历顺序(二叉搜索树嘛)】。

      +
    8. +
    9. 实现的约定接口

      都Cloneable,Serializable

      +

      ArrayList/Vector:RandomAccess

    10. -
    11. 执行下列命令:

      -
      sudo pvcreate /dev/sda4
      sudo vgcreate ubuntu-vg /dev/sda4
      sudo vgextend ubuntu-vg /dev/sda4
      sudo vgdisplay # 此时应发现FREE变成了100G
      sudo lvresize -l +100%FREE /dev/mapper/ubuntu--vg-ubuntu--lv
      sudo resize2fs /dev/mapper/ubuntu--vg-ubuntu--lv
      sudo df -h # 验证成功
    -

    一开始按照的这个,然后被坑惨了(悲)把lvm sig给抹了,导致之后resize2fs的时候报错,然后之后又不小心重启了,最后的最后只能重装。。。又是一晚上配环境。。。。

    ]]>
    + + Java +
    链接、装载与运行库 @@ -12679,246 +13122,6 @@ url访问填写http://localhost/webdemo4_war/*.dobooks - - 阅读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 迭代
    }
    - -

    其中:

      -
    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

      -
      -

      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.

      -
      -
      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。

      -
    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)
      - -

      所举代码段意为把新值通过字符串拼接接在旧值后面。

      -

      应该也可以用于集合合并。总之具体实现方法取决于传入的function参数,非常实用

      -
      -
    10. -
    11. 迭代器

      map本身没有迭代器。

      -

      因而在对map进行遍历时,只能通过其keyset、valueset以及entryset来实现。

      -

      具体详见:HashMap的四种遍历方式

      -
    12. -
    -

    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. -
    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

    -
    -

    也因此,对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也可以它却重写了。原来是因为这个性能问题啊

      -
    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).

    - - -

    关于这部分,详细见sorted set

    -
    -

    最大的特点就是可以人为定义有序并且有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();
    }
    - -
    -

    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);
    }
    - -

    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方法获取元素】

      -
    2. -
    3. not synchronized

      上面介绍到的几个类,除了Vector外,都是线程不同步的。可以用此方式让其线程同步。

      -
      Map m = Collections.synchronizedMap(new LinkedHashMap(...));
    4. -
    5. 是否允许null

      除了TreeSet、TreeMap、ArrayDeque之外,都是允许空(key/value)的

      -
    6. -
    7. 是否有序

      List都是插入序,HashSet无需,HashMap也无序(但其实算是有内部桶序的),LinkedHashMap有插入序和LRU序(依靠内部增加简单队列的消耗),TreeSet有序,TreeMap有序【这俩靠红黑树的遍历顺序(二叉搜索树嘛)】。

      -
    8. -
    9. 实现的约定接口

      都Cloneable,Serializable

      -

      ArrayList/Vector:RandomAccess

      -
    10. -
    -]]> - - Java - - 阅读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/ diff --git a/tags/Java/index.html b/tags/Java/index.html index 9e967b95..2aa4b42a 100644 --- a/tags/Java/index.html +++ b/tags/Java/index.html @@ -31,4 +31,4 @@ // Wait for 1 second before switching API hosts rotate: 1000, }, -};
    Hexo 驱动 v5.4.2|主题 - Yun v1.10.11
    \ No newline at end of file +};
    Hexo 驱动 v5.4.2|主题 - Yun v1.10.11
    \ No newline at end of file diff --git a/tags/books/index.html b/tags/books/index.html index 86d185d2..ed5e4eda 100644 --- a/tags/books/index.html +++ b/tags/books/index.html @@ -31,4 +31,4 @@ // Wait for 1 second before switching API hosts rotate: 1000, }, -};

    books

    2023

    2022

    没有更多的黑历史了_(:з」∠)_
    Hexo 驱动 v5.4.2|主题 - Yun v1.10.11
    \ No newline at end of file +};

    books

    2023

    2022

    没有更多的黑历史了_(:з」∠)_
    Hexo 驱动 v5.4.2|主题 - Yun v1.10.11
    \ No newline at end of file diff --git a/tags/intern/index.html b/tags/intern/index.html index 7fcd07e2..ba5891fd 100644 --- a/tags/intern/index.html +++ b/tags/intern/index.html @@ -31,4 +31,4 @@ // Wait for 1 second before switching API hosts rotate: 1000, }, -};

    intern

    2023

    没有更多的黑历史了_(:з」∠)_
    Hexo 驱动 v5.4.2|主题 - Yun v1.10.11
    \ No newline at end of file +};

    intern

    2023

    没有更多的黑历史了_(:з」∠)_
    Hexo 驱动 v5.4.2|主题 - Yun v1.10.11
    \ No newline at end of file diff --git a/tags/labs/index.html b/tags/labs/index.html index 29b47dcd..4c7c6429 100644 --- a/tags/labs/index.html +++ b/tags/labs/index.html @@ -31,4 +31,4 @@ // Wait for 1 second before switching API hosts rotate: 1000, }, -};

    labs

    2023

    2022

    没有更多的黑历史了_(:з」∠)_
    Hexo 驱动 v5.4.2|主题 - Yun v1.10.11
    \ No newline at end of file +};

    labs

    2023

    2022

    没有更多的黑历史了_(:з」∠)_
    Hexo 驱动 v5.4.2|主题 - Yun v1.10.11
    \ No newline at end of file diff --git a/tags/mylife/index.html b/tags/mylife/index.html index 0ab08b0b..bae4d947 100644 --- a/tags/mylife/index.html +++ b/tags/mylife/index.html @@ -31,4 +31,4 @@ // Wait for 1 second before switching API hosts rotate: 1000, }, -};

    mylife

    2023

    没有更多的黑历史了_(:з」∠)_
    Hexo 驱动 v5.4.2|主题 - Yun v1.10.11
    \ No newline at end of file +};

    mylife

    2023

    没有更多的黑历史了_(:з」∠)_
    Hexo 驱动 v5.4.2|主题 - Yun v1.10.11
    \ No newline at end of file diff --git "a/tags/os\347\253\236\350\265\233/index.html" "b/tags/os\347\253\236\350\265\233/index.html" index d8aca5d4..90924193 100644 --- "a/tags/os\347\253\236\350\265\233/index.html" +++ "b/tags/os\347\253\236\350\265\233/index.html" @@ -31,4 +31,4 @@ // Wait for 1 second before switching API hosts rotate: 1000, }, -};

    os竞赛

    2023

    没有更多的黑历史了_(:з」∠)_
    Hexo 驱动 v5.4.2|主题 - Yun v1.10.11
    \ No newline at end of file +};

    os竞赛

    2023

    没有更多的黑历史了_(:з」∠)_
    Hexo 驱动 v5.4.2|主题 - Yun v1.10.11
    \ No newline at end of file